// Copyright 2017 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include #include #include #include #include #include #include //@doc(docs/h2md.md) //@ # h2md - Header To Markdown // // h2md is a simple tool for generating markdown api docs from headers. // // It avoids any dependencies and has a very simple line-oriented parser. // Whitespace at the start and end of lines is ignored. // // Lines starting with `//@` are either a directive to h2md or the start of // a chunk of markdown. // // Markdown chunks are continued on every following line starting // with `//`. They are ended by a blank line, or a line of source code. // // A line of source code after a markdown chunk is expected to be a function // or method declaration, which will be terminated (on the same line or // a later line) by a `{` or `;`. It will be presented as a code block. // // Lines starting with `//{` begin a code block, and all following lines // will be code until a line starting with `//}` is observed. // // To start a new document, use a doc directive, like // `//@doc(docs/my-markdown.md)` // // From the start of a doc directive until the next doc directive, any // generated markdown will be sent to the file specified in the directive. // //@end typedef enum { IDLE = 0, CODEBLOCK, ONEFUNCTION, MARKDOWN, } state_t; typedef struct { FILE* fin; FILE* fout; char* outfn; state_t state; size_t ws; unsigned verbose; } ctx_t; void emit(ctx_t* ctx, const char* fmt, ...) { if (ctx->fout) { va_list ap; va_start(ap, fmt); vfprintf(ctx->fout, fmt, ap); va_end(ap); } } int close_outfile(ctx_t* ctx, bool ok) { if (ctx->fout) { fclose(ctx->fout); ctx->fout = NULL; if (ok) { size_t len = strlen(ctx->outfn) + 1; char fn[len]; memcpy(fn, ctx->outfn, len); *(strrchr(fn, '.')) = 0; if (rename(ctx->outfn, fn) != 0) { fprintf(stderr, "h2md: could not rename '%s' to '%s'\n", ctx->outfn, fn); unlink(ctx->outfn); return -1; } fprintf(stderr, "h2md: generated '%s'\n", fn); } else { unlink(ctx->outfn); } } return 0; } int open_outfile(ctx_t* ctx, const char* fn) { if (close_outfile(ctx, true) < 0) { return -1; } size_t len = strlen(fn); if ((ctx->outfn = malloc(len + 6)) == NULL) { fprintf(stderr, "h2md: out of memory\n"); return -1; } snprintf(ctx->outfn, len + 6, "%s.h2md", fn); if ((ctx->fout = fopen(ctx->outfn, "w")) == NULL) { fprintf(stderr, "h2md: cannot open '%s' for writing\n", ctx->outfn); return -1; } if (ctx->verbose) { fprintf(stderr, "h2md: generating '%s'\n", ctx->outfn); } return 0; } void newstate(ctx_t* ctx, state_t state) { switch (ctx->state) { case CODEBLOCK: case ONEFUNCTION: emit(ctx, "```\n"); break; default: break; } ctx->state = state; } int process_directive(ctx_t* ctx, char* line, size_t len, size_t ws) { ctx->ws = ws; if (line[0] == '@') { if (!strncmp(line + 1, "end", 3)) { close_outfile(ctx, true); return 0; } if (!strncmp(line + 1, "doc(", 4)) { line += 5; char* x = strchr(line, ')'); if (x == NULL) { fprintf(stderr, "h2md: bad doc directive\n"); return -1; } *x = 0; newstate(ctx, IDLE); return open_outfile(ctx, line); } if (line[1] != ' ') { fprintf(stderr, "h2md: unknown directive: %s\n", line); return -1; } line += 2; newstate(ctx, MARKDOWN); emit(ctx, "\n%s\n", line); return 0; } else if (line[0] == '{') { if (ctx->state == CODEBLOCK) { return 0; } newstate(ctx, CODEBLOCK); emit(ctx, "```\n"); return 0; } else if (line[0] == '}') { if (ctx->state == CODEBLOCK) { emit(ctx, "```\n"); ctx->state = IDLE; } return 0; } else { fprintf(stderr, "h2md: illegal state\n"); return -1; } } int process_comment(ctx_t* ctx, char* line, size_t len, size_t ws) { switch (ctx->state) { case IDLE: case CODEBLOCK: return 0; case MARKDOWN: while (isspace(*line)) { line++; } emit(ctx, "%s\n", line); return 0; case ONEFUNCTION: newstate(ctx, IDLE); return 0; default: fprintf(stderr, "h2md: illegal state\n"); return -1; } } int process_source(ctx_t* ctx, char* line, size_t len) { switch (ctx->state) { case IDLE: return 0; case CODEBLOCK: emit(ctx, "%s\n", line); return 0; case MARKDOWN: // After a markdown comment, a blank line exits // markdown mode and a non-blank line switches to // "one function" mode newstate(ctx, ONEFUNCTION); emit(ctx, "```\n"); // fall through case ONEFUNCTION: { // align whilespace size_t ws = ctx->ws; while ((ws > 0) && isspace(*line)) { line++; len--; ws--; } // omit static inline prefix on decls if (ctx->state == ONEFUNCTION && !strncmp(line, "static inline ", 14)) { line += 14; len -= 14; } char* x; // ; or { ends the decl/definition if ((x = strchr(line, ';')) || (x = strchr(line, '{'))) { *x-- = 0; while (x > line) { if (!isspace(*x)) { break; } *x-- = 0; } emit(ctx, "%s;\n", line); newstate(ctx, IDLE); } else { emit(ctx, "%s\n", line); } return 0; } default: fprintf(stderr, "h2md: illegal state\n"); return -1; } } int process_empty(ctx_t* ctx) { switch (ctx->state) { case MARKDOWN: newstate(ctx, IDLE); return 0; case CODEBLOCK: emit(ctx, "\n"); return 0; default: return 0; } } int process_line(ctx_t* ctx, char* line) { size_t len = strlen(line); // trim trailing whilespace while (len > 0) { if (isspace(line[len - 1])) { line[len - 1] = 0; len--; } else { break; } } // count leading whitespace size_t ws = 0; while (*line && isspace(*line)) { ws++; line++; len--; } if (len == 0) { if (ctx->verbose > 1) { fprintf(stderr, "ZZZ:\n"); } return process_empty(ctx); } // check for C++ comment, which may indicate a directive if ((line[0] == '/') && (line[1] == '/')) { if ((line[2] == '@') || (line[2] == '{') || (line[2] == '}')) { if (ctx->verbose > 1) { fprintf(stderr, "DIR: %s\n", line); } return process_directive(ctx, line + 2, len - 2, ws); } else { if (ctx->verbose > 1) { fprintf(stderr, "COM: %s\n", line); } return process_comment(ctx, line + 2, len - 2, ws); } } else { if (ctx->verbose > 1) { fprintf(stderr, "SRC: %s\n", line); } return process_source(ctx, line - ws, len + ws); } } int process(const char* fn, unsigned verbose) { ctx_t ctx = { .fin = NULL, .fout = NULL, .outfn = NULL, .state = IDLE, .ws = 0, .verbose = verbose, }; if (ctx.verbose) { fprintf(stderr, "h2md: processing '%s'\n", fn); } if ((ctx.fin = fopen(fn, "r")) == NULL) { fprintf(stderr, "h2md: cannot open '%s'\n", fn); return -1; } int r = 0; for (;;) { char line[4096]; if (fgets(line, sizeof(line), ctx.fin) == NULL) { break; } if ((r = process_line(&ctx, line)) < 0) { if (ctx.outfn) { unlink(ctx.outfn); goto done; } } } done: fclose(ctx.fin); close_outfile(&ctx, (r == 0) ? true : false); return r; } int main(int argc, char** argv) { unsigned verbose = false; while (argc > 1) { if (!strcmp(argv[1], "-v")) { verbose++; } else if (process(argv[1], verbose) < 0) { return -1; } argc--; argv++; } return 0; }