1 // Copyright 2017 The Fuchsia Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include <ctype.h>
6 #include <stdarg.h>
7 #include <stdbool.h>
8 #include <stdio.h>
9 #include <stdlib.h>
10 #include <string.h>
11 #include <unistd.h>
12 
13 //@doc(docs/h2md.md)
14 
15 //@ # h2md - Header To Markdown
16 //
17 // h2md is a simple tool for generating markdown api docs from headers.
18 //
19 // It avoids any dependencies and has a very simple line-oriented parser.
20 // Whitespace at the start and end of lines is ignored.
21 //
22 // Lines starting with `//@` are either a directive to h2md or the start of
23 // a chunk of markdown.
24 //
25 // Markdown chunks are continued on every following line starting
26 // with `//`.  They are ended by a blank line, or a line of source code.
27 //
28 // A line of source code after a markdown chunk is expected to be a function
29 // or method declaration, which will be terminated (on the same line or
30 // a later line) by a `{` or `;`. It will be presented as a code block.
31 //
32 // Lines starting with `//{` begin a code block, and all following lines
33 // will be code until a line starting with `//}` is observed.
34 //
35 // To start a new document, use a doc directive, like
36 // `//@doc(docs/my-markdown.md)`
37 //
38 // From the start of a doc directive until the next doc directive, any
39 // generated markdown will be sent to the file specified in the directive.
40 //
41 //@end
42 
43 typedef enum {
44     IDLE = 0,
45     CODEBLOCK,
46     ONEFUNCTION,
47     MARKDOWN,
48 } state_t;
49 
50 typedef struct {
51     FILE* fin;
52     FILE* fout;
53     char* outfn;
54     state_t state;
55     size_t ws;
56     unsigned verbose;
57 } ctx_t;
58 
emit(ctx_t * ctx,const char * fmt,...)59 void emit(ctx_t* ctx, const char* fmt, ...) {
60     if (ctx->fout) {
61         va_list ap;
62         va_start(ap, fmt);
63         vfprintf(ctx->fout, fmt, ap);
64         va_end(ap);
65     }
66 }
67 
close_outfile(ctx_t * ctx,bool ok)68 int close_outfile(ctx_t* ctx, bool ok) {
69     if (ctx->fout) {
70         fclose(ctx->fout);
71         ctx->fout = NULL;
72         if (ok) {
73             size_t len = strlen(ctx->outfn) + 1;
74             char fn[len];
75             memcpy(fn, ctx->outfn, len);
76             *(strrchr(fn, '.')) = 0;
77             if (rename(ctx->outfn, fn) != 0) {
78                 fprintf(stderr, "h2md: could not rename '%s' to '%s'\n", ctx->outfn, fn);
79                 unlink(ctx->outfn);
80                 return -1;
81             }
82             fprintf(stderr, "h2md: generated '%s'\n", fn);
83         } else {
84             unlink(ctx->outfn);
85         }
86     }
87     return 0;
88 }
89 
open_outfile(ctx_t * ctx,const char * fn)90 int open_outfile(ctx_t* ctx, const char* fn) {
91     if (close_outfile(ctx, true) < 0) {
92         return -1;
93     }
94     size_t len = strlen(fn);
95     if ((ctx->outfn = malloc(len + 6)) == NULL) {
96         fprintf(stderr, "h2md: out of memory\n");
97         return -1;
98     }
99     snprintf(ctx->outfn, len + 6, "%s.h2md", fn);
100     if ((ctx->fout = fopen(ctx->outfn, "w")) == NULL) {
101         fprintf(stderr, "h2md: cannot open '%s' for writing\n", ctx->outfn);
102         return -1;
103     }
104     if (ctx->verbose) {
105         fprintf(stderr, "h2md: generating '%s'\n", ctx->outfn);
106     }
107     return 0;
108 }
109 
newstate(ctx_t * ctx,state_t state)110 void newstate(ctx_t* ctx, state_t state) {
111     switch (ctx->state) {
112     case CODEBLOCK:
113     case ONEFUNCTION:
114         emit(ctx, "```\n");
115         break;
116     default:
117         break;
118     }
119     ctx->state = state;
120 }
121 
process_directive(ctx_t * ctx,char * line,size_t len,size_t ws)122 int process_directive(ctx_t* ctx, char* line, size_t len, size_t ws) {
123     ctx->ws = ws;
124     if (line[0] == '@') {
125         if (!strncmp(line + 1, "end", 3)) {
126             close_outfile(ctx, true);
127             return 0;
128         }
129         if (!strncmp(line + 1, "doc(", 4)) {
130             line += 5;
131             char* x = strchr(line, ')');
132             if (x == NULL) {
133                 fprintf(stderr, "h2md: bad doc directive\n");
134                 return -1;
135             }
136             *x = 0;
137             newstate(ctx, IDLE);
138             return open_outfile(ctx, line);
139         }
140         if (line[1] != ' ') {
141             fprintf(stderr, "h2md: unknown directive: %s\n", line);
142             return -1;
143         }
144         line += 2;
145         newstate(ctx, MARKDOWN);
146         emit(ctx, "\n%s\n", line);
147         return 0;
148     } else if (line[0] == '{') {
149         if (ctx->state == CODEBLOCK) {
150             return 0;
151         }
152         newstate(ctx, CODEBLOCK);
153         emit(ctx, "```\n");
154         return 0;
155     } else if (line[0] == '}') {
156         if (ctx->state == CODEBLOCK) {
157             emit(ctx, "```\n");
158             ctx->state = IDLE;
159         }
160         return 0;
161     } else {
162         fprintf(stderr, "h2md: illegal state\n");
163         return -1;
164     }
165 }
166 
process_comment(ctx_t * ctx,char * line,size_t len,size_t ws)167 int process_comment(ctx_t* ctx, char* line, size_t len, size_t ws) {
168     switch (ctx->state) {
169     case IDLE:
170     case CODEBLOCK:
171         return 0;
172     case MARKDOWN:
173         while (isspace(*line)) {
174             line++;
175         }
176         emit(ctx, "%s\n", line);
177         return 0;
178     case ONEFUNCTION:
179         newstate(ctx, IDLE);
180         return 0;
181     default:
182         fprintf(stderr, "h2md: illegal state\n");
183         return -1;
184     }
185 }
186 
process_source(ctx_t * ctx,char * line,size_t len)187 int process_source(ctx_t* ctx, char* line, size_t len) {
188     switch (ctx->state) {
189     case IDLE:
190         return 0;
191     case CODEBLOCK:
192         emit(ctx, "%s\n", line);
193         return 0;
194     case MARKDOWN:
195         // After a markdown comment, a blank line exits
196         // markdown mode and a non-blank line switches to
197         // "one function" mode
198         newstate(ctx, ONEFUNCTION);
199         emit(ctx, "```\n");
200         // fall through
201     case ONEFUNCTION: {
202         // align whilespace
203         size_t ws = ctx->ws;
204         while ((ws > 0) && isspace(*line)) {
205             line++;
206             len--;
207             ws--;
208         }
209         // omit static inline prefix on decls
210         if (ctx->state == ONEFUNCTION &&
211             !strncmp(line, "static inline ", 14)) {
212             line += 14;
213             len -= 14;
214         }
215         char* x;
216         // ; or { ends the decl/definition
217         if ((x = strchr(line, ';')) || (x = strchr(line, '{'))) {
218             *x-- = 0;
219             while (x > line) {
220                 if (!isspace(*x)) {
221                     break;
222                 }
223                 *x-- = 0;
224             }
225             emit(ctx, "%s;\n", line);
226             newstate(ctx, IDLE);
227         } else {
228             emit(ctx, "%s\n", line);
229         }
230         return 0;
231     }
232     default:
233         fprintf(stderr, "h2md: illegal state\n");
234         return -1;
235     }
236 }
237 
process_empty(ctx_t * ctx)238 int process_empty(ctx_t* ctx) {
239     switch (ctx->state) {
240     case MARKDOWN:
241         newstate(ctx, IDLE);
242         return 0;
243     case CODEBLOCK:
244         emit(ctx, "\n");
245         return 0;
246     default:
247         return 0;
248     }
249 }
250 
process_line(ctx_t * ctx,char * line)251 int process_line(ctx_t* ctx, char* line) {
252     size_t len = strlen(line);
253 
254     // trim trailing whilespace
255     while (len > 0) {
256         if (isspace(line[len - 1])) {
257             line[len - 1] = 0;
258             len--;
259         } else {
260             break;
261         }
262     }
263 
264     // count leading whitespace
265     size_t ws = 0;
266     while (*line && isspace(*line)) {
267         ws++;
268         line++;
269         len--;
270     }
271 
272     if (len == 0) {
273         if (ctx->verbose > 1) {
274             fprintf(stderr, "ZZZ:\n");
275         }
276         return process_empty(ctx);
277     }
278     // check for C++ comment, which may indicate a directive
279     if ((line[0] == '/') && (line[1] == '/')) {
280         if ((line[2] == '@') || (line[2] == '{') || (line[2] == '}')) {
281             if (ctx->verbose > 1) {
282                 fprintf(stderr, "DIR: %s\n", line);
283             }
284             return process_directive(ctx, line + 2, len - 2, ws);
285         } else {
286             if (ctx->verbose > 1) {
287                 fprintf(stderr, "COM: %s\n", line);
288             }
289             return process_comment(ctx, line + 2, len - 2, ws);
290         }
291     } else {
292         if (ctx->verbose > 1) {
293             fprintf(stderr, "SRC: %s\n", line);
294         }
295         return process_source(ctx, line - ws, len + ws);
296     }
297 }
298 
process(const char * fn,unsigned verbose)299 int process(const char* fn, unsigned verbose) {
300     ctx_t ctx = {
301         .fin = NULL,
302         .fout = NULL,
303         .outfn = NULL,
304         .state = IDLE,
305         .ws = 0,
306         .verbose = verbose,
307     };
308 
309     if (ctx.verbose) {
310         fprintf(stderr, "h2md: processing '%s'\n", fn);
311     }
312     if ((ctx.fin = fopen(fn, "r")) == NULL) {
313         fprintf(stderr, "h2md: cannot open '%s'\n", fn);
314         return -1;
315     }
316 
317     int r = 0;
318     for (;;) {
319         char line[4096];
320         if (fgets(line, sizeof(line), ctx.fin) == NULL) {
321             break;
322         }
323         if ((r = process_line(&ctx, line)) < 0) {
324             if (ctx.outfn) {
325                 unlink(ctx.outfn);
326                 goto done;
327             }
328         }
329     }
330 
331 done:
332     fclose(ctx.fin);
333     close_outfile(&ctx, (r == 0) ? true : false);
334     return r;
335 }
336 
main(int argc,char ** argv)337 int main(int argc, char** argv) {
338     unsigned verbose = false;
339     while (argc > 1) {
340         if (!strcmp(argv[1], "-v")) {
341             verbose++;
342         } else if (process(argv[1], verbose) < 0) {
343             return -1;
344         }
345         argc--;
346         argv++;
347     }
348     return 0;
349 }