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 }