| // 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 <ctype.h> |
| #include <stdarg.h> |
| #include <stdbool.h> |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <unistd.h> |
| |
| //@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; |
| } |