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