c4fb88089d23999e34d3b00ae7a1b4a76508b0b6
[smdp.git] / src / parser.c
1 /*
2  * Functions necessary to parse a file and transform its content into
3  * a deck of slides containing lines. All based on markdown formating
4  * rules.
5  * Copyright (C) 2014 Michael Goehler
6  *
7  * This file is part of mdp.
8  *
9  * This program is free software: you can redistribute it and/or modify
10  * it under the terms of the GNU General Public License as published by
11  * the Free Software Foundation, either version 3 of the License, or
12  * (at your option) any later version.
13  *
14  * This program is distributed in the hope that it will be useful,
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17  * GNU General Public License for more details.
18  *
19  * You should have received a copy of the GNU General Public License
20  * along with this program. If not, see <http://www.gnu.org/licenses/>.
21  *
22  */
23
24 #include <ctype.h>
25 #include <errno.h>
26 #include <stdio.h>
27 #include <stdlib.h>
28 #include <string.h>
29
30 #include "parser.h"
31
32 deck_t *markdown_load(FILE *input) {
33
34     int c = 0;    // char
35     int i = 0;    // increment
36     int hc = 0;   // header count
37     int lc = 0;   // line count
38     int sc = 1;   // slide count
39     int bits = 0; // markdown bits
40
41     deck_t *deck = new_deck();
42     slide_t *slide = deck->slide;
43     line_t *line = NULL;
44     line_t *tmp = NULL;
45     cstring_t *text = cstring_init();
46
47     while ((c = fgetc(input)) != EOF) {
48         if (ferror(input)) {
49             fprintf(stderr, "markdown_load() failed to read input: %s\n", strerror(errno));
50             exit(EXIT_FAILURE);
51         }
52
53         if(c == '\n') {
54
55             // markdown analyse
56             bits = markdown_analyse(text);
57
58             // if first line in file is markdown hr
59             if(!line && CHECK_BIT(bits, IS_HR)) {
60
61                 // clear text
62                 (text->reset)(text);
63
64             // if text is markdown hr
65             } else if(CHECK_BIT(bits, IS_HR) &&
66                       CHECK_BIT(line->bits, IS_EMPTY)) {
67
68                 slide->lines = lc;
69
70                 // clear text
71                 (text->reset)(text);
72
73                 // create next slide
74                 slide = next_slide(slide);
75                 sc++;
76
77             } else {
78
79                 // if slide ! has line
80                 if(!slide->line) {
81
82                     // create new line
83                     line = new_line();
84                     slide->line = line;
85                     lc = 1;
86
87                 } else {
88
89                     // create next line
90                     line = next_line(line);
91                     lc++;
92
93                 }
94
95                 // add text to line
96                 line->text = text;
97
98                 // add bits to line
99                 line->bits = bits;
100
101                 // calc offset
102                 line->offset = next_nonblank(text, 0);
103
104                 // adjust line length dynamicaly - excluding markup
105                 if(line->text->text)
106                     adjust_line_length(line);
107
108                 // new text
109                 text = cstring_init();
110             }
111
112         } else if(c == '\t') {
113
114             // expand tab to spaces
115             for (i = 0;  i < EXPAND_TABS;  i++) {
116                 (text->expand)(text, ' ');
117             }
118
119         } else if(c == '\\') {
120
121             // add char to line
122             (text->expand)(text, c);
123
124             // if !IS_CODE add next char to line
125             // and do not increase line count
126             if(next_nonblank(text, 0) < CODE_INDENT) {
127
128                 c = fgetc(input);
129                 (text->expand)(text, c);
130
131                 if(is_utf8(c)) {
132
133                     // if utf-8 char > 1 byte add remaing to line
134                     for(i = 0; i < length_utf8(c) - 1; i++) {
135                         c = fgetc(input);
136                         (text->expand)(text, c);
137                     }
138                 }
139
140             }
141
142         } else if(isprint(c) || isspace((unsigned char) c)) {
143
144             // add char to line
145             (text->expand)(text, c);
146
147         } else if(is_utf8(c)) {
148
149             // add char to line
150             (text->expand)(text, c);
151
152             // if utf-8 char > 1 byte add remaing to line
153             for(i = 0; i < length_utf8(c) - 1; i++) {
154                 c = fgetc(input);
155                 (text->expand)(text, c);
156             }
157         }
158     }
159
160     slide->lines = lc;
161     deck->slides = sc;
162
163     // detect header
164     line = deck->slide->line;
165     if(line && line->text->size > 0 && line->text->text[0] == '%') {
166
167         // assign header to deck
168         deck->header = line;
169
170         // find first non-header line
171         while(line->text->size > 0 && line->text->text[0] == '%') {
172             hc++;
173             line = line->next;
174         }
175
176         // split linked list
177         line->prev->next = NULL;
178         line->prev = NULL;
179
180         // remove header lines from slide
181         deck->slide->line = line;
182
183         // adjust counts
184         deck->headers += hc;
185         deck->slide->lines -= hc;
186     }
187
188     slide = deck->slide;
189     while(slide) {
190         line = slide->line;
191         while(line) {
192             if((CHECK_BIT(line->bits, IS_H1) ||
193                 CHECK_BIT(line->bits, IS_H2)) &&
194                CHECK_BIT(line->bits, IS_EMPTY) &&
195                line->prev &&
196                !CHECK_BIT(line->prev->bits, IS_EMPTY)) {
197                 // combine underlined H1/H2 in single line
198
199                 // remove line from linked list
200                 line->prev->next = line->next;
201                 if(line->next)
202                     line->next->prev = line->prev;
203
204                 // set bits on previous line
205                 if(CHECK_BIT(line->bits, IS_H1)) {
206                     SET_BIT(line->prev->bits, IS_H1);
207                 } else {
208                     SET_BIT(line->prev->bits, IS_H2);
209                 }
210
211                 // adjust line count
212                 slide->lines -= 1;
213
214                 // maintain loop condition
215                 tmp = line;
216                 line = line->prev;
217
218                 // delete line
219                 (tmp->text->delete)(tmp->text);
220                 free(tmp);
221
222             } else if(CHECK_BIT(line->bits, IS_UNORDERED_LIST_3)) {
223                 tmp = line->next;
224                 line_t *list_last_level_3 = line;
225
226                 while(tmp &&
227                       CHECK_BIT(tmp->bits, IS_UNORDERED_LIST_3)) {
228                     if(CHECK_BIT(tmp->bits, IS_UNORDERED_LIST_3)) {
229                         list_last_level_3 = tmp;
230                     }
231                     tmp = tmp->next;
232                 }
233
234                 for(tmp = line; tmp != list_last_level_3; tmp = tmp->next) {
235                     SET_BIT(tmp->bits, IS_UNORDERED_LIST_3);
236                 }
237
238             } else if(CHECK_BIT(line->bits, IS_UNORDERED_LIST_2)) {
239                 tmp = line->next;
240                 line_t *list_last_level_2 = line;
241
242                 while(tmp &&
243                       (CHECK_BIT(tmp->bits, IS_UNORDERED_LIST_2) ||
244                        CHECK_BIT(tmp->bits, IS_UNORDERED_LIST_3))) {
245                     if(CHECK_BIT(tmp->bits, IS_UNORDERED_LIST_2)) {
246                         list_last_level_2 = tmp;
247                     }
248                     tmp = tmp->next;
249                 }
250
251                 for(tmp = line; tmp != list_last_level_2; tmp = tmp->next) {
252                     SET_BIT(tmp->bits, IS_UNORDERED_LIST_2);
253                 }
254
255             } else if(CHECK_BIT(line->bits, IS_UNORDERED_LIST_1)) {
256                 tmp = line->next;
257                 line_t *list_last_level_1 = line;
258
259                 while(tmp &&
260                       (CHECK_BIT(tmp->bits, IS_UNORDERED_LIST_1) ||
261                        CHECK_BIT(tmp->bits, IS_UNORDERED_LIST_2) ||
262                        CHECK_BIT(tmp->bits, IS_UNORDERED_LIST_3))) {
263                     if(CHECK_BIT(tmp->bits, IS_UNORDERED_LIST_1)) {
264                         list_last_level_1 = tmp;
265                     }
266                     tmp = tmp->next;
267                 }
268
269                 for(tmp = line; tmp != list_last_level_1; tmp = tmp->next) {
270                     SET_BIT(tmp->bits, IS_UNORDERED_LIST_1);
271                 }
272             }
273
274             line = line->next;
275         }
276         slide = slide->next;
277     }
278
279     return deck;
280 }
281
282 int markdown_analyse(cstring_t *text) {
283
284     static int unordered_list_level = 0;
285     static int unordered_list_level_offset[] = {-1, -1, -1, -1};
286
287     int i = 0;      // increment
288     int bits = 0;   // markdown bits
289     int offset = 0; // text offset
290     int eol    = 0; // end of line
291
292     int equals = 0, hashes = 0,
293         stars  = 0, minus  = 0,
294         spaces = 0, other  = 0; // special character counts
295
296     const int unordered_list_offset = unordered_list_level_offset[unordered_list_level];
297
298     // return IS_EMPTY on null pointers
299     if(!text || !text->text) {
300         SET_BIT(bits, IS_EMPTY);
301         return bits;
302     }
303
304     // count leading spaces
305     offset = next_nonblank(text, 0);
306
307     // strip trailing spaces
308     for(eol = text->size; eol > offset && isspace((unsigned char) text->text[eol - 1]); eol--);
309
310     // IS_UNORDERED_LIST_#
311     if(text->size >= offset + 2 &&
312        (text->text[offset] == '*' || text->text[offset] == '-') &&
313        text->text[offset + 1] == ' ') {
314
315         for(i = offset; i<eol; i++) {
316             if(text->text[i] != '*' &&
317                text->text[i] != '-' &&
318                text->text[i] != ' ') {
319                 if(offset > unordered_list_offset + CODE_INDENT) {
320                     SET_BIT(bits, IS_CODE);
321                 } else if(offset != unordered_list_offset) {
322                     for(i = unordered_list_level; i >= 0; i--) {
323                         if(unordered_list_level_offset[i] == offset) {
324                             unordered_list_level = i;
325                             break;
326                         }
327                     }
328                     if(i != unordered_list_level) {
329                         unordered_list_level = MIN(unordered_list_level + 1, UNORDERED_LIST_MAX_LEVEL);
330                         unordered_list_level_offset[unordered_list_level] = offset;
331                     }
332                 }
333
334                 if(unordered_list_level == 0) {
335                     unordered_list_level = 1;
336                     unordered_list_level_offset[1] = offset;
337                 }
338
339                 switch(unordered_list_level) {
340                     case 1: SET_BIT(bits, IS_UNORDERED_LIST_1); break;
341                     case 2: SET_BIT(bits, IS_UNORDERED_LIST_2); break;
342                     case 3: SET_BIT(bits, IS_UNORDERED_LIST_3); break;
343                     default: break;
344                 }
345
346                 break;
347             }
348         }
349     }
350
351     if(!CHECK_BIT(bits, IS_UNORDERED_LIST_1) &&
352        !CHECK_BIT(bits, IS_UNORDERED_LIST_2) &&
353        !CHECK_BIT(bits, IS_UNORDERED_LIST_3)) {
354
355         unordered_list_level = 0;
356
357         // IS_CODE
358         if(offset >= CODE_INDENT) {
359             SET_BIT(bits, IS_CODE);
360
361         } else {
362
363             // IS_QUOTE
364             if(text->text[offset] == '>') {
365                 SET_BIT(bits, IS_QUOTE);
366             }
367
368             // IS_CENTER
369             if(text->size >= offset + 3 &&
370                text->text[offset] == '-' &&
371                text->text[offset + 1] == '>' &&
372                text->text[offset + 2] == ' ') {
373                 SET_BIT(bits, IS_CENTER);
374
375                 // remove start tag
376                 (text->strip)(text, offset, 3);
377                 eol -= 3;
378
379                 if(text->size >= offset + 3 &&
380                    text->text[eol - 1] == '-' &&
381                    text->text[eol - 2] == '<' &&
382                    text->text[eol - 3] == ' ') {
383
384                     // remove end tags
385                     (text->strip)(text, eol - 3, 3);
386
387                     // adjust end of line
388                     for(eol = text->size; eol > offset && isspace((unsigned char) text->text[eol - 1]); eol--);
389
390                 }
391             }
392
393             for(i = offset; i < eol; i++) {
394
395                 if(text->text[i] == ' ') {
396                     spaces++;
397
398                 } else {
399                     switch(text->text[i]) {
400                         case '=': equals++;  break;
401                         case '#': hashes++;  break;
402                         case '*': stars++;   break;
403                         case '-': minus++;   break;
404                         case '\\': other++; i++; break;
405                         default:  other++;   break;
406                     }
407                 }
408             }
409
410             // IS_H1
411             if(equals > 0 &&
412                 hashes + stars + minus + spaces + other == 0) {
413                 SET_BIT(bits, IS_H1);
414             }
415             if(text->text[offset] == '#' &&
416                 text->text[offset+1] == ' ') {
417                 SET_BIT(bits, IS_H1);
418                 SET_BIT(bits, IS_H1_ATX);
419             }
420
421             // IS_H2
422             if(minus > 0 &&
423                 equals + hashes + stars + spaces + other == 0) {
424                 SET_BIT(bits, IS_H2);
425             }
426             if(text->text[offset] == '#' &&
427                 text->text[offset+1] == '#' &&
428                 text->text[offset+2] == ' ') {
429                 SET_BIT(bits, IS_H2);
430                 SET_BIT(bits, IS_H2_ATX);
431             }
432
433             // IS_HR
434             if((minus >= 3 && equals + hashes + stars + other == 0) ||
435                (stars >= 3 && equals + hashes + minus + other == 0)) {
436
437                 SET_BIT(bits, IS_HR);
438             }
439
440             // IS_EMPTY
441             if(other == 0) {
442                 SET_BIT(bits, IS_EMPTY);
443             }
444         }
445     }
446
447     return bits;
448 }
449
450 void markdown_debug(deck_t *deck, int debug) {
451
452     int sc = 0; // slide count
453     int lc = 0; // line count
454
455     int offset;
456     line_t *header;
457
458     if(debug == 1) {
459         fprintf(stderr, "headers: %i\nslides: %i\n", deck->headers, deck->slides);
460
461     } else if(debug > 1) {
462
463         // print header to STDERR
464         if(deck->header) {
465             header = deck->header;
466             while(header &&
467                 header->length > 0 &&
468                 header->text->text[0] == '%') {
469
470                 // skip descriptor word (e.g. %title:)
471                 offset = next_blank(header->text, 0) + 1;
472
473                 fprintf(stderr, "header: %s\n", &header->text->text[offset]);
474                 header = header->next;
475             }
476         }
477     }
478
479     slide_t *slide = deck->slide;
480     line_t *line;
481
482     // print slide/line count to STDERR
483     while(slide) {
484         sc++;
485
486         if(debug == 1) {
487             fprintf(stderr, "  slide %i: %i lines\n", sc, slide->lines);
488
489         } else if(debug > 1) {
490
491             // also print bits and line length
492             fprintf(stderr, "  slide %i:\n", sc);
493             line = slide->line;
494             lc = 0;
495             while(line) {
496                 lc++;
497                 fprintf(stderr, "    line %i: bits = %i, length = %i\n", lc, line->bits, line->length);
498                 line = line->next;
499             }
500         }
501
502         slide = slide->next;
503     }
504 }
505
506 void adjust_line_length(line_t *line) {
507     int l = 0;
508     const static char *special = "\\*_`"; // list of interpreted chars
509     const char *c = &line->text->text[line->offset];
510     cstack_t *stack = cstack_init();
511
512     // for each char in line
513     for(; *c; c++) {
514         // if char is in special char list
515         if(strchr(special, *c)) {
516
517             // closing special char (or second backslash)
518             if((stack->top)(stack, *c)) {
519                 if(*c == '\\') l++;
520                 (stack->pop)(stack);
521
522             // treat special as regular char
523             } else if((stack->top)(stack, '\\')) {
524                 l++;
525                 (stack->pop)(stack);
526
527             // opening special char
528             } else {
529                 (stack->push)(stack, *c);
530             }
531
532         } else {
533             // remove backslash from stack
534             if((stack->top)(stack, '\\'))
535                 (stack->pop)(stack);
536             l++;
537         }
538     }
539
540     if(CHECK_BIT(line->bits, IS_H1_ATX))
541         l -= 2;
542     if(CHECK_BIT(line->bits, IS_H2_ATX))
543         l -= 3;
544
545     line->length = l;
546
547     (stack->delete)(stack);
548 }
549
550 bool is_utf8(char ch) {
551     return (ch & 0x80) != 0x00;
552 }
553
554 int length_utf8(char ch) {
555
556     int i = 0; // increment
557
558     while(is_utf8(ch)) {
559         i++;
560         ch <<= 1;
561     }
562
563     return i;
564 }
565
566 int next_nonblank(cstring_t *text, int i) {
567     while ((i < text->size) && isspace((unsigned char) (text->text)[i]))
568         i++;
569
570     return i;
571 }
572
573 int prev_blank(cstring_t *text, int i) {
574     while ((i > 0) && !isspace((unsigned char) (text->text)[i]))
575         i--;
576
577     return i;
578 }
579
580 int next_blank(cstring_t *text, int i) {
581     while ((i < text->size) && !isspace((unsigned char) (text->text)[i]))
582         i++;
583
584     return i;
585 }
586
587 int next_word(cstring_t *text, int i) {
588     return next_nonblank(text, next_blank(text, i));
589 }