5b08be407b6999c0986a4599473c4521753235ef
[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     int prev = 0; // markdown bits of previous line
41
42     deck_t *deck = new_deck();
43     slide_t *slide = deck->slide;
44     line_t *line = NULL;
45     line_t *tmp = NULL;
46     cstring_t *text = cstring_init();
47
48     // initialize bits as empty line
49     SET_BIT(bits, IS_EMPTY);
50
51     while ((c = fgetc(input)) != EOF) {
52         if (ferror(input)) {
53             fprintf(stderr, "markdown_load() failed to read input: %s\n", strerror(errno));
54             exit(EXIT_FAILURE);
55         }
56
57         if(c == '\n') {
58
59             // markdown analyse
60             prev = bits;
61             bits = markdown_analyse(text, prev);
62
63             // if first line in file is markdown hr
64             if(!line && CHECK_BIT(bits, IS_HR)) {
65
66                 // clear text
67                 (text->reset)(text);
68
69             // if text is markdown hr
70             } else if(CHECK_BIT(bits, IS_HR) &&
71                       CHECK_BIT(line->bits, IS_EMPTY)) {
72
73                 slide->lines = lc;
74
75                 // clear text
76                 (text->reset)(text);
77
78                 // create next slide
79                 slide = next_slide(slide);
80                 sc++;
81
82             } else {
83
84                 // if slide ! has line
85                 if(!slide->line) {
86
87                     // create new line
88                     line = new_line();
89                     slide->line = line;
90                     lc = 1;
91
92                 } else {
93
94                     // create next line
95                     line = next_line(line);
96                     lc++;
97
98                 }
99
100                 // add text to line
101                 line->text = text;
102
103                 // add bits to line
104                 line->bits = bits;
105
106                 // calc offset
107                 line->offset = next_nonblank(text, 0);
108
109                 // adjust line length dynamicaly - excluding markup
110                 if(line->text->text)
111                     adjust_line_length(line);
112
113                 // new text
114                 text = cstring_init();
115             }
116
117         } else if(c == '\t') {
118
119             // expand tab to spaces
120             for (i = 0;  i < EXPAND_TABS;  i++) {
121                 (text->expand)(text, ' ');
122             }
123
124         } else if(c == '\\') {
125
126             // add char to line
127             (text->expand)(text, c);
128
129             // if !IS_CODE add next char to line
130             // and do not increase line count
131             if(next_nonblank(text, 0) < CODE_INDENT) {
132
133                 c = fgetc(input);
134                 (text->expand)(text, c);
135
136                 if(is_utf8(c)) {
137
138                     // if utf-8 char > 1 byte add remaing to line
139                     for(i = 0; i < length_utf8(c) - 1; i++) {
140                         c = fgetc(input);
141                         (text->expand)(text, c);
142                     }
143                 }
144
145             }
146
147         } else if(isprint(c) || isspace((unsigned char) c)) {
148
149             // add char to line
150             (text->expand)(text, c);
151
152         } else if(is_utf8(c)) {
153
154             // add char to line
155             (text->expand)(text, c);
156
157             // if utf-8 char > 1 byte add remaing to line
158             for(i = 0; i < length_utf8(c) - 1; i++) {
159                 c = fgetc(input);
160                 (text->expand)(text, c);
161             }
162         }
163     }
164     (text->delete)(text);
165
166     slide->lines = lc;
167     deck->slides = sc;
168
169     // detect header
170     line = deck->slide->line;
171     if(line && line->text->size > 0 && line->text->text[0] == '%') {
172
173         // assign header to deck
174         deck->header = line;
175
176         // find first non-header line
177         while(line && line->text->size > 0 && line->text->text[0] == '%') {
178             hc++;
179             line = line->next;
180         }
181
182         // only split header if any non-header line is found
183         if(line) {
184
185             // split linked list
186             line->prev->next = NULL;
187             line->prev = NULL;
188
189             // remove header lines from slide
190             deck->slide->line = line;
191
192             // adjust counts
193             deck->headers += hc;
194             deck->slide->lines -= hc;
195         }
196     }
197
198     slide = deck->slide;
199     while(slide) {
200         line = slide->line;
201         while(line) {
202             // combine underlined H1/H2 in single line
203             if((CHECK_BIT(line->bits, IS_H1) ||
204                 CHECK_BIT(line->bits, IS_H2)) &&
205                CHECK_BIT(line->bits, IS_EMPTY) &&
206                line->prev &&
207                !CHECK_BIT(line->prev->bits, IS_EMPTY)) {
208
209
210                 // remove line from linked list
211                 line->prev->next = line->next;
212                 if(line->next)
213                     line->next->prev = line->prev;
214
215                 // set bits on previous line
216                 if(CHECK_BIT(line->bits, IS_H1)) {
217                     SET_BIT(line->prev->bits, IS_H1);
218                 } else {
219                     SET_BIT(line->prev->bits, IS_H2);
220                 }
221
222                 // adjust line count
223                 slide->lines -= 1;
224
225                 // maintain loop condition
226                 tmp = line;
227                 line = line->prev;
228
229                 // delete line
230                 (tmp->text->delete)(tmp->text);
231                 free(tmp);
232
233             // pass enclosing flag IS_UNORDERED_LIST_3
234             // to nested levels for unordered lists
235             } else if(CHECK_BIT(line->bits, IS_UNORDERED_LIST_3)) {
236                 tmp = line->next;
237                 line_t *list_last_level_3 = line;
238
239                 while(tmp &&
240                       CHECK_BIT(tmp->bits, IS_UNORDERED_LIST_3)) {
241                     if(CHECK_BIT(tmp->bits, IS_UNORDERED_LIST_3)) {
242                         list_last_level_3 = tmp;
243                     }
244                     tmp = tmp->next;
245                 }
246
247                 for(tmp = line; tmp != list_last_level_3; tmp = tmp->next) {
248                     SET_BIT(tmp->bits, IS_UNORDERED_LIST_3);
249                 }
250
251             // pass enclosing flag IS_UNORDERED_LIST_2
252             // to nested levels for unordered lists
253             } else if(CHECK_BIT(line->bits, IS_UNORDERED_LIST_2)) {
254                 tmp = line->next;
255                 line_t *list_last_level_2 = line;
256
257                 while(tmp &&
258                       (CHECK_BIT(tmp->bits, IS_UNORDERED_LIST_2) ||
259                        CHECK_BIT(tmp->bits, IS_UNORDERED_LIST_3))) {
260                     if(CHECK_BIT(tmp->bits, IS_UNORDERED_LIST_2)) {
261                         list_last_level_2 = tmp;
262                     }
263                     tmp = tmp->next;
264                 }
265
266                 for(tmp = line; tmp != list_last_level_2; tmp = tmp->next) {
267                     SET_BIT(tmp->bits, IS_UNORDERED_LIST_2);
268                 }
269
270             // pass enclosing flag IS_UNORDERED_LIST_1
271             // to nested levels for unordered lists
272             } else if(CHECK_BIT(line->bits, IS_UNORDERED_LIST_1)) {
273                 tmp = line->next;
274                 line_t *list_last_level_1 = line;
275
276                 while(tmp &&
277                       (CHECK_BIT(tmp->bits, IS_UNORDERED_LIST_1) ||
278                        CHECK_BIT(tmp->bits, IS_UNORDERED_LIST_2) ||
279                        CHECK_BIT(tmp->bits, IS_UNORDERED_LIST_3))) {
280                     if(CHECK_BIT(tmp->bits, IS_UNORDERED_LIST_1)) {
281                         list_last_level_1 = tmp;
282                     }
283                     tmp = tmp->next;
284                 }
285
286                 for(tmp = line; tmp != list_last_level_1; tmp = tmp->next) {
287                     SET_BIT(tmp->bits, IS_UNORDERED_LIST_1);
288                 }
289             }
290
291             line = line->next;
292         }
293         slide = slide->next;
294     }
295
296     return deck;
297 }
298
299 int markdown_analyse(cstring_t *text, int prev) {
300
301     // static variables can not be redeclaired, but changed outside of a declaration
302     // the program remembers their value on every function calls
303     static int unordered_list_level = 0;
304     static int unordered_list_level_offset[] = {-1, -1, -1, -1};
305
306     int i = 0;      // increment
307     int bits = 0;   // markdown bits
308     int offset = 0; // text offset
309     int eol    = 0; // end of line
310
311     int equals = 0, hashes = 0,
312         stars  = 0, minus  = 0,
313         spaces = 0, other  = 0; // special character counts
314
315     const int unordered_list_offset = unordered_list_level_offset[unordered_list_level];
316
317     // return IS_EMPTY on null pointers
318     if(!text || !text->text) {
319         SET_BIT(bits, IS_EMPTY);
320         return bits;
321     }
322
323     // count leading spaces
324     offset = next_nonblank(text, 0);
325
326     // strip trailing spaces
327     for(eol = text->size; eol > offset && isspace((unsigned char) text->text[eol - 1]); eol--);
328
329     // IS_UNORDERED_LIST_#
330     if(text->size >= offset + 2 &&
331        (text->text[offset] == '*' || text->text[offset] == '-') &&
332        text->text[offset + 1] == ' ') {
333
334         // if different from last lines offset
335         if(offset != unordered_list_offset) {
336
337             // test if offset matches a lower indent level
338             for(i = unordered_list_level; i >= 0; i--) {
339                 if(unordered_list_level_offset[i] == offset) {
340                     unordered_list_level = i;
341                     break;
342                 }
343             }
344             // if offset doesn't match any previously stored indent level
345             if(i != unordered_list_level) {
346                 unordered_list_level = MIN(unordered_list_level + 1, UNORDERED_LIST_MAX_LEVEL);
347                 // memorize the offset as next bigger indent level
348                 unordered_list_level_offset[unordered_list_level] = offset;
349             }
350         }
351
352         // if no previous indent level matches, this must be the first line of the list
353         if(unordered_list_level == 0) {
354             unordered_list_level = 1;
355             unordered_list_level_offset[1] = offset;
356         }
357
358         switch(unordered_list_level) {
359             case 1: SET_BIT(bits, IS_UNORDERED_LIST_1); break;
360             case 2: SET_BIT(bits, IS_UNORDERED_LIST_2); break;
361             case 3: SET_BIT(bits, IS_UNORDERED_LIST_3); break;
362             default: break;
363         }
364     }
365
366     if(!CHECK_BIT(bits, IS_UNORDERED_LIST_1) &&
367        !CHECK_BIT(bits, IS_UNORDERED_LIST_2) &&
368        !CHECK_BIT(bits, IS_UNORDERED_LIST_3)) {
369
370         // continue list if indent level is still the same as in previous line
371         if ((CHECK_BIT(prev, IS_UNORDERED_LIST_1) ||
372              CHECK_BIT(prev, IS_UNORDERED_LIST_2) ||
373              CHECK_BIT(prev, IS_UNORDERED_LIST_3)) &&
374             offset >= unordered_list_offset) {
375
376             switch(unordered_list_level) {
377                 case 1: SET_BIT(bits, IS_UNORDERED_LIST_1); break;
378                 case 2: SET_BIT(bits, IS_UNORDERED_LIST_2); break;
379                 case 3: SET_BIT(bits, IS_UNORDERED_LIST_3); break;
380                 default: break;
381             }
382
383             // this line extends the previous list item
384             SET_BIT(bits, IS_UNORDERED_LIST_EXT);
385
386         // or reset indent level
387         } else {
388             unordered_list_level = 0;
389         }
390     }
391
392     if(!CHECK_BIT(bits, IS_UNORDERED_LIST_1) &&
393        !CHECK_BIT(bits, IS_UNORDERED_LIST_2) &&
394        !CHECK_BIT(bits, IS_UNORDERED_LIST_3)) {
395
396         // IS_CODE
397         if(offset >= CODE_INDENT &&
398            (CHECK_BIT(prev, IS_EMPTY) ||
399             CHECK_BIT(prev, IS_CODE))) {
400             SET_BIT(bits, IS_CODE);
401
402         } else {
403
404             // IS_QUOTE
405             if(text->text[offset] == '>') {
406                 SET_BIT(bits, IS_QUOTE);
407             }
408
409             // IS_CENTER
410             if(text->size >= offset + 3 &&
411                text->text[offset] == '-' &&
412                text->text[offset + 1] == '>' &&
413                text->text[offset + 2] == ' ') {
414                 SET_BIT(bits, IS_CENTER);
415
416                 // remove start tag
417                 (text->strip)(text, offset, 3);
418                 eol -= 3;
419
420                 if(text->size >= offset + 3 &&
421                    text->text[eol - 1] == '-' &&
422                    text->text[eol - 2] == '<' &&
423                    text->text[eol - 3] == ' ') {
424
425                     // remove end tags
426                     (text->strip)(text, eol - 3, 3);
427
428                     // adjust end of line
429                     for(eol = text->size; eol > offset && isspace((unsigned char) text->text[eol - 1]); eol--);
430
431                 }
432             }
433
434             for(i = offset; i < eol; i++) {
435
436                 if(text->text[i] == ' ') {
437                     spaces++;
438
439                 } else {
440                     switch(text->text[i]) {
441                         case '=': equals++;  break;
442                         case '#': hashes++;  break;
443                         case '*': stars++;   break;
444                         case '-': minus++;   break;
445                         case '\\': other++; i++; break;
446                         default:  other++;   break;
447                     }
448                 }
449             }
450
451             // IS_H1
452             if(equals > 0 &&
453                 hashes + stars + minus + spaces + other == 0) {
454                 SET_BIT(bits, IS_H1);
455             }
456             if(text->text[offset] == '#' &&
457                 text->text[offset+1] == ' ') {
458                 SET_BIT(bits, IS_H1);
459                 SET_BIT(bits, IS_H1_ATX);
460             }
461
462             // IS_H2
463             if(minus > 0 &&
464                 equals + hashes + stars + spaces + other == 0) {
465                 SET_BIT(bits, IS_H2);
466             }
467             if(text->text[offset] == '#' &&
468                 text->text[offset+1] == '#' &&
469                 text->text[offset+2] == ' ') {
470                 SET_BIT(bits, IS_H2);
471                 SET_BIT(bits, IS_H2_ATX);
472             }
473
474             // IS_HR
475             if((minus >= 3 && equals + hashes + stars + other == 0) ||
476                (stars >= 3 && equals + hashes + minus + other == 0)) {
477
478                 SET_BIT(bits, IS_HR);
479             }
480
481             // IS_EMPTY
482             if(other == 0) {
483                 SET_BIT(bits, IS_EMPTY);
484             }
485         }
486     }
487
488     return bits;
489 }
490
491 void markdown_debug(deck_t *deck, int debug) {
492
493     int sc = 0; // slide count
494     int lc = 0; // line count
495
496     int offset;
497     line_t *header;
498
499     if(debug == 1) {
500         fprintf(stderr, "headers: %i\nslides: %i\n", deck->headers, deck->slides);
501
502     } else if(debug > 1) {
503
504         // print header to STDERR
505         if(deck->header) {
506             header = deck->header;
507             while(header &&
508                 header->length > 0 &&
509                 header->text->text[0] == '%') {
510
511                 // skip descriptor word (e.g. %title:)
512                 offset = next_blank(header->text, 0) + 1;
513
514                 fprintf(stderr, "header: %s\n", &header->text->text[offset]);
515                 header = header->next;
516             }
517         }
518     }
519
520     slide_t *slide = deck->slide;
521     line_t *line;
522
523     // print slide/line count to STDERR
524     while(slide) {
525         sc++;
526
527         if(debug == 1) {
528             fprintf(stderr, "  slide %i: %i lines\n", sc, slide->lines);
529
530         } else if(debug > 1) {
531
532             // also print bits and line length
533             fprintf(stderr, "  slide %i:\n", sc);
534             line = slide->line;
535             lc = 0;
536             while(line) {
537                 lc++;
538                 fprintf(stderr, "    line %i: bits = %i, length = %i\n", lc, line->bits, line->length);
539                 line = line->next;
540             }
541         }
542
543         slide = slide->next;
544     }
545 }
546
547 void adjust_line_length(line_t *line) {
548     int l = 0;
549     const static char *special = "\\*_`"; // list of interpreted chars
550     const char *c = &line->text->text[line->offset];
551     cstack_t *stack = cstack_init();
552
553     // for each char in line
554     for(; *c; c++) {
555         // if char is in special char list
556         if(strchr(special, *c)) {
557
558             // closing special char (or second backslash)
559             if((stack->top)(stack, *c)) {
560                 if(*c == '\\') l++;
561                 (stack->pop)(stack);
562
563             // treat special as regular char
564             } else if((stack->top)(stack, '\\')) {
565                 l++;
566                 (stack->pop)(stack);
567
568             // opening special char
569             } else {
570                 (stack->push)(stack, *c);
571             }
572
573         } else {
574             // remove backslash from stack
575             if((stack->top)(stack, '\\'))
576                 (stack->pop)(stack);
577             l++;
578         }
579     }
580
581     if(CHECK_BIT(line->bits, IS_H1_ATX))
582         l -= 2;
583     if(CHECK_BIT(line->bits, IS_H2_ATX))
584         l -= 3;
585
586     line->length = l;
587
588     (stack->delete)(stack);
589 }
590
591 bool is_utf8(char ch) {
592     return (ch & 0x80) != 0x00;
593 }
594
595 int length_utf8(char ch) {
596
597     int i = 0; // increment
598
599     while(is_utf8(ch)) {
600         i++;
601         ch <<= 1;
602     }
603
604     return i;
605 }
606
607 int next_nonblank(cstring_t *text, int i) {
608     while ((i < text->size) && isspace((unsigned char) (text->text)[i]))
609         i++;
610
611     return i;
612 }
613
614 int prev_blank(cstring_t *text, int i) {
615     while ((i > 0) && !isspace((unsigned char) (text->text)[i]))
616         i--;
617
618     return i;
619 }
620
621 int next_blank(cstring_t *text, int i) {
622     while ((i < text->size) && !isspace((unsigned char) (text->text)[i]))
623         i++;
624
625     return i;
626 }
627
628 int next_word(cstring_t *text, int i) {
629     return next_nonblank(text, next_blank(text, i));
630 }