Welcome to little lamb

Code » qmdoc » commit f538cd5

Add -i/--index & -X/--no-index as well as full TOC

author Olivier Brunel
2023-01-04 13:35:49 UTC
committer Olivier Brunel
2023-01-04 15:10:17 UTC
parent 4fafc5fe443972e98cef8c5a3df0b7db83d940f9

Add -i/--index & -X/--no-index as well as full TOC

Unless --no-index was specified, the FULL_TOC mode is enabled if :
- no index.md was given, or
- index.md was given first, or
- --index as specified

When enabled, the index will be put as first page, yet processed last.
That way, the full TOC (i.e. of all pages (but index) together) can
be featured inside the page, via a new tag <TOC>

dark.css +13 -0
light.css +16 -0
main.c +170 -37
qmdoc.h +7 -0
struct.css +29 -7

diff --git a/dark.css b/dark.css
index 5877e65..5b8cdf5 100644
--- a/dark.css
+++ b/dark.css
@@ -24,9 +24,22 @@
     main header nav > ul > li > a { /* PAGE */
         color: #55a5d9;
     }
+    main section > ul.toc > li > a {
+        color: #112fa2;
+    }
     main header nav > ul > li > ul > li > a { /* h1 */
         color: #a3a802;
     }
+    main section > ul.toc > li > ul > li > a {
+        color: #6c2104;
+    }
+    main section > ul.toc > li > ul > li > ul > li > a,
+    main section > ul.toc > li > ul > li > ul > li > ul > li > a,
+    main section > ul.toc > li > ul > li > ul > li > ul > li > ul > li > a,
+    main section > ul.toc > li > ul > li > ul > li > ul > li > ul > li > ul > li > a,
+    main section > ul.toc > li > ul > li > ul > li > ul > li > ul > li > ul > li > ul > li > a {
+        color: #0f0b37;
+    }
     main, main footer {
         background: #80732d;
     }
diff --git a/light.css b/light.css
index ce73aba..d3a12c5 100644
--- a/light.css
+++ b/light.css
@@ -22,9 +22,25 @@ main header nav ul li a {
 main header nav > ul > li > a { /* PAGE */
     color: #55a5d9;
 }
+main section > ul.toc > li > a {
+    color: #2949ac;
+}
 main header nav > ul > li > ul > li > a { /* h1 */
     color: #a3a802;
 }
+main section > ul.toc > li > ul > li > a {
+    color: #7b681b;
+}
+main section > ul.toc > li > ul > li > ul > li > a,
+main section > ul.toc > li > ul > li > ul > li > ul > li > a,
+main section > ul.toc > li > ul > li > ul > li > ul > li > ul > li > a,
+main section > ul.toc > li > ul > li > ul > li > ul > li > ul > li > ul > li > a,
+main section > ul.toc > li > ul > li > ul > li > ul > li > ul > li > ul > li > ul > li > a {
+    color: #404040;
+}
+main section > ul.toc a:hover {
+    color: blue;
+}
 main, main footer {
     background: #fbf4ce;
 }
diff --git a/main.c b/main.c
index 522dd80..3ce65a4 100644
--- a/main.c
+++ b/main.c
@@ -27,6 +27,8 @@ enum {
     OPT_OVERWRITE   = (1 << 2),
     OPT_NO_TOC      = (1 << 3),
     OPT_BUTTONS     = (1 << 4),
+    OPT_NO_INDEX    = (1 << 5),
+    OPT_INDEX       = (1 << 6),
 };
 
 enum {
@@ -51,6 +53,7 @@ static struct css {
 };
 
 struct page {
+    const char *sce;
     size_t fileoff;
     size_t titleoff;
     int fd;
@@ -60,12 +63,14 @@ struct page {
 enum {
     DOC_HAS_TITLE   = (1 << 0),
     DOC_BUFFERING   = (1 << 1),
+    DOC_FULL_TOC    = (1 << 2),
+    DOC_IS_INDEX    = (1 << 3), /* i.e. enable the <TOC> tag */
 };
 
 struct ctx {
     int options;
     stralloc sa;
-    size_t otoc;
+    size_t otoc; /* where to include to page's TOC */
     stralloc sa_out;
     int toc_lvl;
     struct css *css;
@@ -83,6 +88,7 @@ struct ctx {
     struct {
         stralloc sa;
         size_t salen;
+        size_t otoc; /* where the page's TOC begins */
     } buf;
     union {
         struct {
@@ -348,6 +354,7 @@ enter_block(MD_BLOCKTYPE type, void *details, void *ctx_)
                         || !escape_text(ctx, ctx->doc.title, strlen(ctx->doc.title))
                         || !raw_str(ctx, "</h1></section><nav><ul>"))
                         return ERR_PARSER_ENTER_BLOCK;
+
                     for (int i = 0; i < ctx->nb_pages; ++i) {
                         if (!raw_str(ctx, "<li><a href=\"")
                                 || !escape_text(ctx, str_file(i), strlen(str_file(i)))
@@ -358,15 +365,33 @@ enter_block(MD_BLOCKTYPE type, void *details, void *ctx_)
 
                         /* remember positions for TOC */
                         if (i == ctx->cur_page) {
+                            ctx->doc.flags |= DOC_BUFFERING;
+                            /* full toc */
+                            if (ctx->doc.flags & DOC_FULL_TOC) {
+                                /* starting it? */
+                                if (i == 1) {
+                                    if (!raw_str(ctx, "<ul class=\"toc\">"))
+                                        return ERR_PARSER_TOC;
+                                }
+                                /* adding page title */
+                                if (i > 0 &&
+                                        (!raw_str(ctx, "<li><a href=\"")
+                                        || !escape_text(ctx, str_file(i), strlen(str_file(i)))
+                                        || !raw_str(ctx, "\">")
+                                        || !escape_text(ctx, str_title(i), strlen(str_title(i)))
+                                        || !raw_str(ctx, "</a>")))
+                                    return ERR_PARSER_TOC;
+                            }
+
+                            /* where it begins */
+                            ctx->buf.otoc = ctx->buf.sa.len;
                             /* where to include it */
                             ctx->otoc = ctx->sa_out.len;
 
                             /* open it */
-                            ctx->doc.flags |= DOC_BUFFERING;
-                            if (!raw_str(ctx, "<ul>")) {
-                                ctx->doc.flags &= ~DOC_BUFFERING;
+                            if (!raw_str(ctx, "<ul>"))
                                 return ERR_PARSER_TOC;
-                            }
+
                             ctx->toc_lvl = 1;
                             ctx->doc.flags &= ~DOC_BUFFERING;
                         }
@@ -654,8 +679,11 @@ leave_block(MD_BLOCKTYPE type, void *details, void *ctx_)
                     /* TOC */
                     char toc[l];
                     memcpy(toc, s, l);
+                    const char *file = ctx->sa.s + ctx->pages[ctx->cur_page].fileoff;
                     ctx->doc.flags |= DOC_BUFFERING;
-                    if (!raw_str(ctx, "<li><a href=\"#")
+                    if (!raw_str(ctx, "<li><a href=\"")
+                            || !escape_text(ctx, file, strlen(file))
+                            || !raw_str(ctx, "#")
                             || !anchor(ctx, toc, l)
                             || !raw_str(ctx, "\">")
                             || !strip_tags(ctx, toc, l, 1)
@@ -924,6 +952,15 @@ text(MD_TEXTTYPE type, const MD_CHAR *text, MD_SIZE size, void *ctx_)
             };
 #undef TAG
 
+            if ((ctx->doc.flags & DOC_IS_INDEX) && size == 5
+                    && !strncmp(text, "<TOC>", 5)) {
+                /* 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 0;
+            }
+
             int i, n;
             for (i = 0, n = sizeof(tags) / sizeof(*tags); i < n; ++i) {
                 if (size == tags[i].len && !strncmp(text, tags[i].name, tags[i].len)) {
@@ -943,6 +980,27 @@ text(MD_TEXTTYPE type, const MD_CHAR *text, MD_SIZE size, void *ctx_)
     return 0;
 }
 
+static int
+load_source(struct ctx *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 (allread(p->fd, ctx->sa.s + *salen, p->size) != p->size)
+        ret_strerr_warnwu1sys(ERR_IO, "read source file");
+    ctx->sa.len += p->size;
+
+    /* ending on a new line allows parser optimization */
+    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)
 {
@@ -956,20 +1014,20 @@ convert_page(struct ctx *ctx, int fddest)
         fd = openat_excl(fddest, dst);
     if (fd < 0) ret_strerr_warnwu1sys(ERR_IO, "create destination");
 
-    size_t salen = ctx->sa.len;
-    if (!stralloc_readyplus(&ctx->sa, p->size + 1))
-        return ERR_MEM;
+    size_t salen;
+    const char *sce;
 
-    if (allread(p->fd, ctx->sa.s + salen, p->size) != p->size)
-        ret_strerr_warnwu1sys(ERR_IO, "read source file");
-    ctx->sa.len += p->size;
-
-    /* ending on a new line allows parser optimization */
-    if (ctx->sa.s[ctx->sa.len - 1] != '\n') {
-        stralloc_catb(&ctx->sa, "\n", 1);
+    if (p->fd >= 0) {
+        int r = load_source(ctx, &salen);
+        if (r < 0) return r;
+        sce = ctx->sa.s + salen;
+    } else {
+        salen = ctx->sa.len;
+        sce = index_md;
     }
 
-    ctx->doc.flags = 0;
+    /* reset flags, keeping only DOC_FULL_TOC & DOC_IS_INDEX for all pages */
+    ctx->doc.flags &= (DOC_FULL_TOC | DOC_IS_INDEX);
 
     const MD_PARSER parser = {
         .flags = MD_FLAG_COLLAPSEWHITESPACE | MD_FLAG_PERMISSIVEAUTOLINKS
@@ -982,7 +1040,7 @@ convert_page(struct ctx *ctx, int fddest)
         .leave_span = leave_span,
         .text = text,
     };
-    int r = md_parse(ctx->sa.s + salen, p->size, &parser, ctx);
+    int r = md_parse(sce, p->size, &parser, ctx);
     if (r != 0) {
         char buf[UINT32_FMT];
         buf[uint32_fmt(buf, (uint32) (r < 0) ? -r : r)] = '\0';
@@ -1007,7 +1065,8 @@ convert_page(struct ctx *ctx, int fddest)
     if (    /* up to TOC position */
             allwrite(fd, ctx->sa_out.s, ctx->otoc) != ctx->otoc
             /* then the actual TOC */
-            || allwrite(fd, ctx->buf.sa.s, ctx->buf.sa.len) != ctx->buf.sa.len
+            || allwrite(fd, ctx->buf.sa.s + ctx->buf.otoc, ctx->buf.sa.len - ctx->buf.otoc)
+            != ctx->buf.sa.len - ctx->buf.otoc
             /* and the rest of the page */
             || allwrite(fd, ctx->sa_out.s + ctx->otoc, ctx->sa_out.len - ctx->otoc)
             != ctx->sa_out.len - ctx->otoc
@@ -1015,8 +1074,11 @@ convert_page(struct ctx *ctx, int fddest)
         ret_strerr_warnwu1sys(ERR_IO, "write destination");
     fd_close(fd);
 
-    /* reset TOC/buffer positions */
-    ctx->otoc = ctx->sa_out.len = ctx->buf.sa.len = 0;
+    /* reset sa_out position */
+    ctx->sa_out.len = 0;
+    /* only reset buf.sa (i.e. drop the page's TOC) if we're not doing a full TOC */
+    if (!(ctx->doc.flags & DOC_FULL_TOC))
+        ctx->buf.sa.len = 0;
 
     fd_close(p->fd);
 
@@ -1030,13 +1092,13 @@ load_page_from_file(const char *file, struct page *page, stralloc *sa)
     page->fd = open_read(file);
     if (page->fd < 0) ret_strerr_warnwu3sys(ERR_IO, "open '", file, "'");
 
-    const char *s = strrchr(file, '/');
-    if (!s) s = file;
-    else ++s;
+    page->sce = strrchr(file, '/');
+    if (!page->sce) page->sce = file;
+    else ++page->sce;
 
-    size_t l = strlen(s);
+    size_t l = strlen(page->sce);
     page->fileoff = sa->len;
-    if (!stralloc_catb(sa, s, l - 2)
+    if (!stralloc_catb(sa, page->sce, l - 2)
             || !stralloc_catb(sa, "html", 5))
         ret_strerr_warnwu3sys(ERR_MEM, "load page title from '", file, "'");
 
@@ -1115,10 +1177,12 @@ help(void)
          " -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"
          " -o, --overwrite              Overwrite destination files if already exist\n"
-         " -T, --no-toc                 Don't write a TOC on each page\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"
+         " -X, --no-index               Disable index mode\n"
         );
 }
 
@@ -1160,13 +1224,15 @@ main (int argc, char *argv[])
         { "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' },
         { "overwrite",      no_argument,        NULL,   'o' },
         { "no-toc",         no_argument,        NULL,   'T' },
         { "title",          required_argument,  NULL,   't' },
+        { "no-index",       no_argument,        NULL,   'X' },
         { NULL,             0,                  NULL,    0  },
     };
-    while ((c = getopt_long(argc, argv, "a:bCc:d:F:H:hIl:oTt:", opts, NULL)) != -1) switch (c) {
+    while ((c = getopt_long(argc, argv, "a:bCc:d:F:H:hIil:oTt:X", opts, NULL)) != -1) switch (c) {
         case 'a':
             ctx.doc.author = optarg;
             break;
@@ -1193,6 +1259,9 @@ main (int argc, char *argv[])
         case 'I':
             ctx.options |= OPT_INLINE_CSS;
             break;
+        case 'i':
+            ctx.options |= OPT_INDEX;
+            break;
         case 'l':
             ctx.doc.lang = optarg;
             break;
@@ -1200,28 +1269,35 @@ main (int argc, char *argv[])
             ctx.options |= OPT_OVERWRITE;
             break;
         case 'T':
-            ctx.options |= OPT_NO_TOC;
+            ctx.options |= OPT_NO_TOC | OPT_NO_INDEX;
             break;
         case 't':
             ctx.doc.title = optarg;
             break;
+        case 'X':
+            ctx.options |= OPT_NO_INDEX;
+            break;
         case '?':
             usage(1);
     }
     if (optind == argc) usage(1);
     if ((ctx.options & (OPT_NO_CSS | OPT_INLINE_CSS)) == (OPT_NO_CSS | OPT_INLINE_CSS))
-        strerr_dief1x(-ERR_USAGE, "cannot use '--no-css' and '--inline-css' together");
+        strerr_dief5x(-ERR_USAGE, "cannot use '", "--no-css", "' and '", "--inline-css", "' together");
+    if ((ctx.options & (OPT_INDEX | OPT_NO_INDEX)) == (OPT_INDEX | OPT_NO_INDEX))
+        strerr_dief5x(-ERR_USAGE, "cannot use '", "--index", "' and '", "--no-index", "' together");
 
     int fddest = open(destdir, O_RDONLY | O_DIRECTORY | O_CLOEXEC);
     if (fddest < 0) strerr_diefu3sys(-ERR_IO, "open '", destdir, "'");
 
     if (!(ctx.options & OPT_NO_TOC)) css[CSS_NO_TOC].file = NULL;
 
-    struct page pages[argc - optind];
+    /* +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);
+    ctx.nb_pages = sizeof(pages) / sizeof(*pages) - 1;
 
     int err = 0;
+    int idx_page = -1;
     outse("Scanning pages...");
     for (int i = optind; i < argc; ++i) {
         const char *file = argv[i];
@@ -1236,10 +1312,50 @@ main (int argc, char *argv[])
 
         int r = load_page_from_file(file, &pages[i - optind], &ctx.sa);
         if (r < 0) err = r;
+
+        if (!(ctx.options & OPT_NO_INDEX)
+                && !strcmp(ctx.sa.s + pages[i - optind].fileoff, "index.html"))
+            idx_page = i - optind;
     }
 
     if (err < 0) strerr_diefu1x(-err, "load pages");
 
+    /* enable FULL TOC unless disabled (OPT_NO_INDEX) if:
+     * - index was given as first page, or none given (add our internal tpl),
+     * - OPT_INDEX was given, in which case we'll move index to first place
+     */
+    if (!(ctx.options & OPT_NO_INDEX) && (idx_page <= 0 || (ctx.options & OPT_INDEX))) {
+        ctx.doc.flags |= DOC_FULL_TOC;
+
+        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));
+
+            /* add our internal index */
+            pages[0].sce = "<internal index>";
+            pages[0].fileoff = ctx.sa.len;
+            if (!stralloc_catb(&ctx.sa, "index.html", strlen("index.html") + 1))
+                strerr_diefu3sys(-ERR_MEM, "load page title from '", pages[0].sce, "'");
+
+            pages[0].titleoff = ctx.sa.len;
+            if (!stralloc_catb(&ctx.sa, index_title, strlen(index_title) + 1))
+                strerr_diefu3sys(-ERR_MEM, "load page title from '", pages[0].sce, "'");
+
+            /* fd == -1 means use index_md instead of reading from fd */
+            pages[0].fd = -1;
+            pages[0].size = strlen(index_md);
+
+            ++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));
+            /* 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));
+        }
+    }
+
     if (header || footer) {
         outse("Loading files...");
 
@@ -1282,15 +1398,32 @@ main (int argc, char *argv[])
         }
     }
 
-    for (int i = optind; i < argc; ++i) {
+    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;
+
+        outs("Converting ");
+        outs(pages[ctx.cur_page].sce);
+        outse("...");
+
+        int r = convert_page(&ctx, fddest);
+        if (r < 0) strerr_diefu7x(-r, "convert '", pages[ctx.cur_page].sce,
+                                  "' to '", destdir, "/",
+                                  ctx.sa.s + pages[ctx.cur_page].fileoff, "'");
+    }
+
+    if (ctx.doc.flags & DOC_FULL_TOC) {
+        ctx.cur_page = 0;
+
         outs("Converting ");
-        outs(argv[i]);
+        outs(pages[ctx.cur_page].sce);
         outse("...");
 
-        ctx.cur_page = i - optind;
+        ctx.doc.flags |= DOC_IS_INDEX;
         int r = convert_page(&ctx, fddest);
-        if (r < 0) strerr_diefu7x(-r, "convert '", argv[i], "' to '", destdir, "/",
-                                  ctx.sa.s + pages[i - optind].fileoff, "'");
+        if (r < 0) strerr_diefu7x(-r, "convert '", pages[ctx.cur_page].sce,
+                                  "' to '", destdir, "/",
+                                  ctx.sa.s + pages[ctx.cur_page].fileoff, "'");
     }
 
     stralloc_free(&ctx.buf.sa);
diff --git a/qmdoc.h b/qmdoc.h
index d51b847..b4af1af 100644
--- a/qmdoc.h
+++ b/qmdoc.h
@@ -20,4 +20,11 @@ const char metas[] =
 "<meta name=\"theme-color\" media=\"(prefers-color-scheme: dark)\" content=\"gray\">"
 ;
 
+const char index_title[] = "Table of Contents";
+const char index_md[] =
+"# Table of Contents\n"
+"\n"
+"<TOC>"
+;
+
 #endif /* QMDOC_H */
diff --git a/struct.css b/struct.css
index 6b27b87..c31a5a7 100644
--- a/struct.css
+++ b/struct.css
@@ -75,30 +75,52 @@ main header nav li a {
     display: inline-block;
     width: 100%;
 }
-main header nav > ul > li > a { /* PAGE */
+main section ul.toc, main section ul.toc ul {
+    padding-left: 8px;
+}
+main section ul.toc li {
+    display: block;
+}
+main section ul.toc li a {
+    text-decoration: none;
+}
+main section ul.toc > li {
+    margin-top: 23px;
+}
+main header nav > ul > li > a,
+main section ul.toc > li > a { /* PAGE */
     margin-top: 8px;
     font-weight: 600;
     text-transform: uppercase;
 }
-main header nav > ul > li > ul > li > a { /* h1 */
+main section ul.toc > li > a {
+    font-size: 110%;
+}
+main header nav > ul > li > ul > li > a,
+main section > ul.toc > li > ul > li > a { /* h1 */
     text-transform: uppercase;
     font-weight: 600;
     padding-left: 12px;
 }
-main header nav > ul > li > ul > li > ul > li > a { /* h2 */
+main header nav > ul > li > ul > li > ul > li > a,
+main section > ul.toc > li > ul > li > ul > li > a { /* h2 */
     font-weight: 400;
     padding-left: 24px;
 }
-main header nav > ul > li > ul > li > ul > li > ul > li > a { /* h3 */
+main header nav > ul > li > ul > li > ul > li > ul > li > a,
+main section > ul.toc > li > ul > li > ul > li > ul > li > a { /* h3 */
     padding-left: 36px;
 }
-main header nav > ul > li > ul > li > ul > li > ul > li > ul > li > a { /* h4 */
+main header nav > ul > li > ul > li > ul > li > ul > li > ul > li > a,
+main section > ul.toc > li > ul > li > ul > li > ul > li > ul > li > a { /* h4 */
     padding-left: 48px;
 }
-main header nav > ul > li > ul > li > ul > li > ul > li > ul > li > ul > li > a { /* h5 */
+main header nav > ul > li > ul > li > ul > li > ul > li > ul > li > ul > li > a,
+main section > ul.toc > li > ul > li > ul > li > ul > li > ul > li > ul > li > a { /* h5 */
     padding-left: 60px;
 }
-main header nav > ul > li > ul > li > ul > li > ul > li > ul > li > ul > li > ul > li > a { /* h6 */
+main header nav > ul > li > ul > li > ul > li > ul > li > ul > li > ul > li > ul > li > a,
+main section > ul.toc > li > ul > li > ul > li > ul > li > ul > li > ul > li > ul > li > a { /* h6 */
     padding-left: 72px;
 }
 main > section {