author | Olivier Brunel
<jjk@jjacky.com> 2023-01-04 13:35:49 UTC |
committer | Olivier Brunel
<jjk@jjacky.com> 2023-01-04 15:10:17 UTC |
parent | 4fafc5fe443972e98cef8c5a3df0b7db83d940f9 |
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 {