Menu parser: remove d-quotes around title of entry
[wmaker-crm.git] / WINGs / menuparser.c
blob97c6728590718d3aa3836b4a965ae816b3a25f11
1 /*
2 * Window Maker window manager
4 * Copyright (c) 2012 Christophe Curis
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License along
17 * with this program; if not, write to the Free Software Foundation, Inc.,
18 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20 #include "wconfig.h"
22 #include <stdio.h>
23 #include <string.h>
24 #include <ctype.h>
25 #include <stdarg.h>
27 #include <WINGs/WUtil.h>
29 #include "menuparser.h"
31 static WMenuParser menu_parser_create_new(const char *file_name, void *file,
32 const char *include_default_paths);
33 static char *menu_parser_isolate_token(WMenuParser parser, WParserMacro *list_macros);
34 static void menu_parser_get_directive(WMenuParser parser);
35 static Bool menu_parser_include_file(WMenuParser parser);
36 static void menu_parser_condition_ifmacro(WMenuParser parser, Bool check_exists);
37 static void menu_parser_condition_else(WMenuParser parser);
38 static void menu_parser_condition_end(WMenuParser parser);
41 /***** Constructor and Destructor for the Menu Parser object *****/
42 WMenuParser WMenuParserCreate(const char *file_name, void *file,
43 const char *include_default_paths)
45 WMenuParser parser;
47 parser = menu_parser_create_new(file_name, file, include_default_paths);
48 menu_parser_register_preset_macros(parser);
49 return parser;
52 void WMenuParserDelete(WMenuParser parser)
54 if (parser->include_file) {
55 /* Trick: the top parser's data are not wmalloc'd, we point on the
56 provided data so we do not wfree it; however for include files
57 we did wmalloc them.
58 This code should not be used as the wfree is done when we reach
59 the end of an include file; however this may not happen when an
60 early exit occurs (typically when 'readMenuFile' does not find
61 its expected command). */
62 fclose(parser->include_file->file_handle);
63 wfree((char *) parser->include_file->file_name);
64 WMenuParserDelete(parser->include_file);
67 if (parser->macros)
68 menu_parser_free_macros(parser);
70 wfree(parser);
73 static WMenuParser menu_parser_create_new(const char *file_name, void *file,
74 const char *include_default_paths)
76 WMenuParser parser;
78 parser = wmalloc(sizeof(*parser));
79 parser->include_default_paths = include_default_paths;
80 parser->file_name = file_name;
81 parser->file_handle = file;
82 parser->rd = parser->line_buffer;
84 return parser;
87 /***** To report helpfull messages to user *****/
88 const char *WMenuParserGetFilename(WMenuParser parser)
90 return parser->file_name;
93 void WMenuParserError(WMenuParser parser, const char *msg, ...)
95 char buf[MAXLINE];
96 va_list args;
97 WMenuParser parent;
99 while (parser->include_file)
100 parser = parser->include_file;
102 va_start(args, msg);
103 vsnprintf(buf, sizeof(buf), msg, args);
104 va_end(args);
105 __wmessage("WMenuParser", parser->file_name, parser->line_number, WMESSAGE_TYPE_WARNING, "%s", buf);
107 for (parent = parser->parent_file; parent != NULL; parent = parent->parent_file)
108 __wmessage("WMenuParser", parser->file_name, parser->line_number, WMESSAGE_TYPE_WARNING,
109 _(" included from file \"%s\" at line %d"),
110 parent->file_name, parent->line_number);
113 /***** Read one line from file and split content *****/
114 /* The function returns False when the end of file is reached */
115 Bool WMenuParserGetLine(WMenuParser top_parser, char **title, char **command, char **parameter, char **shortcut)
117 WMenuParser cur_parser;
118 enum { GET_TITLE, GET_COMMAND, GET_PARAMETERS, GET_SHORTCUT } scanmode;
119 char *token;
120 char lineparam[MAXLINE];
121 char *params = NULL;
123 lineparam[0] = '\0';
124 *title = NULL;
125 *command = NULL;
126 *parameter = NULL;
127 *shortcut = NULL;
128 scanmode = GET_TITLE;
130 read_next_line_with_filechange:
131 cur_parser = top_parser;
132 while (cur_parser->include_file)
133 cur_parser = cur_parser->include_file;
135 read_next_line:
136 if (fgets(cur_parser->line_buffer, sizeof(cur_parser->line_buffer), cur_parser->file_handle) == NULL) {
137 if (cur_parser->cond.depth > 0) {
138 int i;
140 for (i = 0; i < cur_parser->cond.depth; i++)
141 WMenuParserError(cur_parser, _("missing #endif to match #%s at line %d"),
142 cur_parser->cond.stack[i].name, cur_parser->cond.stack[i].line);
145 if (cur_parser->parent_file == NULL)
146 /* Not inside an included file -> we have reached the end */
147 return False;
149 /* We have only reached the end of an included file -> go back to calling file */
150 fclose(cur_parser->file_handle);
151 wfree((char *) cur_parser->file_name);
152 cur_parser = cur_parser->parent_file;
153 wfree(cur_parser->include_file);
154 cur_parser->include_file = NULL;
155 goto read_next_line_with_filechange;
157 cur_parser->line_number++;
158 cur_parser->rd = cur_parser->line_buffer;
160 for (;;) {
161 if (!menu_parser_skip_spaces_and_comments(cur_parser)) {
162 /* We reached the end of line */
163 if (scanmode == GET_TITLE)
164 goto read_next_line; // Empty line -> skip
165 else
166 break; // Finished reading current line -> return it to caller
168 if ((scanmode == GET_TITLE) && (*cur_parser->rd == '#')) {
169 cur_parser->rd++;
170 menu_parser_get_directive(cur_parser);
171 goto read_next_line_with_filechange;
173 if (cur_parser->cond.stack[0].skip)
174 goto read_next_line;
176 /* Found a word */
177 token = menu_parser_isolate_token(cur_parser, top_parser->macros);
178 switch (scanmode) {
179 case GET_TITLE:
180 *title = token;
181 scanmode = GET_COMMAND;
182 break;
184 case GET_COMMAND:
185 if (strcmp(token, "SHORTCUT") == 0) {
186 scanmode = GET_SHORTCUT;
187 wfree(token);
188 } else {
189 *command = token;
190 scanmode = GET_PARAMETERS;
192 break;
194 case GET_SHORTCUT:
195 if (*shortcut != NULL) {
196 WMenuParserError(top_parser, _("multiple SHORTCUT definition not valid") );
197 wfree(*shortcut);
199 *shortcut = token;
200 scanmode = GET_COMMAND;
201 break;
203 case GET_PARAMETERS:
205 char *src;
207 if (params == NULL) {
208 params = lineparam;
209 } else {
210 if ((params - lineparam) < sizeof(lineparam)-1)
211 *params++ = ' ';
213 src = token;
214 while ((params - lineparam) < sizeof(lineparam)-1)
215 if ( (*params = *src++) == '\0')
216 break;
217 else
218 params++;
219 wfree(token);
221 break;
225 if (title != NULL) {
226 char eot, *src, *dst;
228 src = *title;
229 eot = *src++;
230 if ((eot == '"') || (eot == '\'')) {
231 dst = *title;
232 while (*src != '\0')
233 *dst++ = *src++;
234 if ((dst > *title) && (dst[-1] == eot))
235 dst--;
236 *dst = '\0';
240 if (params != NULL) {
241 lineparam[sizeof(lineparam) - 1] = '\0';
242 *parameter = wstrdup(lineparam);
245 return True;
248 /* Return False when there's nothing left on the line,
249 otherwise increment parser's pointer to next token */
250 Bool menu_parser_skip_spaces_and_comments(WMenuParser parser)
252 for (;;) {
253 while (isspace(*parser->rd))
254 parser->rd++;
256 if (*parser->rd == '\0')
257 return False; // Found the end of current line
259 else if ((parser->rd[0] == '\\') &&
260 (parser->rd[1] == '\n') &&
261 (parser->rd[2] == '\0')) {
262 // Means that the current line is expected to be continued on next line
263 if (fgets(parser->line_buffer, sizeof(parser->line_buffer), parser->file_handle) == NULL) {
264 WMenuParserError(parser, _("premature end of file while expecting a new line after '\\'") );
265 return False;
267 parser->line_number++;
268 parser->rd = parser->line_buffer;
270 } else if (parser->rd[0] == '/') {
271 if (parser->rd[1] == '/') // Single line C comment
272 return False; // Won't find anything more on this line
273 if (parser->rd[1] == '*') {
274 int start_line;
276 start_line = parser->line_number;
277 parser->rd += 2;
278 for (;;) {
279 /* Search end-of-comment marker */
280 while (*parser->rd != '\0') {
281 if (parser->rd[0] == '*')
282 if (parser->rd[1] == '/')
283 goto found_end_of_comment;
284 parser->rd++;
287 /* Marker not found -> load next line */
288 if (fgets(parser->line_buffer, sizeof(parser->line_buffer), parser->file_handle) == NULL) {
289 WMenuParserError(parser, _("reached end of file while searching '*/' for comment started at line %d"), start_line);
290 return False;
292 parser->line_number++;
293 parser->rd = parser->line_buffer;
296 found_end_of_comment:
297 parser->rd += 2; // Skip closing mark
298 continue; // Because there may be spaces after the comment
300 return True; // the '/' was not a comment, treat it as user data
301 } else
302 return True; // Found some data
306 /* read a token (non-spaces suite of characters)
307 the result os wmalloc's, so it needs to be free'd */
308 static char *menu_parser_isolate_token(WMenuParser parser, WParserMacro *list_macros)
310 char *start;
311 char *token;
312 int limit = MAX_NESTED_MACROS;
314 start = parser->rd;
315 restart_token_split:
317 while (*parser->rd != '\0')
318 if (isspace(*parser->rd))
319 break;
320 else if ((parser->rd[0] == '/') &&
321 ((parser->rd[1] == '*') || (parser->rd[1] == '/')))
322 break;
323 else if ((parser->rd[0] == '\\') && (parser->rd[1] == '\n'))
324 break;
325 else if ((*parser->rd == '"' ) || (*parser->rd == '\'')) {
326 char eot = *parser->rd++;
327 while ((*parser->rd != '\0') && (*parser->rd != '\n'))
328 if (*parser->rd++ == eot)
329 goto found_end_quote;
330 WMenuParserError(parser, _("missing closing quote or double-quote before end-of-line") );
331 found_end_quote:
333 } else if (isnamechr(*parser->rd)) {
334 WParserMacro *macro;
335 char *start_macro;
337 start_macro = parser->rd;
338 while (isnamechr(*parser->rd))
339 parser->rd++;
341 macro = menu_parser_find_macro(parser, start_macro);
342 if (macro != NULL) {
343 char *expand_there;
345 /* Copy the chars before the macro to the beginning of the buffer to
346 leave as much room as possible for expansion */
347 expand_there = parser->line_buffer;
348 if (start != parser->line_buffer)
349 while (start < start_macro)
350 *expand_there++ = *start++;
351 start = parser->line_buffer;
353 /* Macro expansion will take care to keep the rest of the line after
354 the macro to the end of the line for future token extraction */
355 menu_parser_expand_macro(parser, macro, expand_there,
356 sizeof(parser->line_buffer) - (start - parser->line_buffer) );
358 /* Restart parsing to allow expansion of sub macro calls */
359 parser->rd = expand_there;
360 if (limit-- > 0)
361 goto restart_token_split;
362 WMenuParserError(parser, _("too many nested macro expansion, breaking loop") );
363 while (isnamechr(*parser->rd))
364 parser->rd++;
365 break;
367 // else: the text was passed over so it will be counted in the token from 'start'
368 } else
369 parser->rd++;
371 token = wmalloc(parser->rd - start + 1);
372 strncpy(token, start, parser->rd - start);
373 token[parser->rd - start] = '\0';
375 return token;
378 /***** Processing of special # directives *****/
379 static void menu_parser_get_directive(WMenuParser parser)
381 char *command;
383 /* Isolate the command */
384 while (isspace(*parser->rd))
385 parser->rd++;
386 command = parser->rd;
387 while (*parser->rd)
388 if (isspace(*parser->rd)) {
389 *parser->rd++ = '\0';
390 break;
391 } else parser->rd++;
393 if (strcmp(command, "include") == 0) {
394 if (!menu_parser_include_file(parser)) return;
396 } else if (strcmp(command, "define") == 0) {
397 menu_parser_define_macro(parser);
399 } else if (strcmp(command, "ifdef") == 0) {
400 menu_parser_condition_ifmacro(parser, 1);
402 } else if (strcmp(command, "ifndef") == 0) {
403 menu_parser_condition_ifmacro(parser, 0);
405 } else if (strcmp(command, "else") == 0) {
406 menu_parser_condition_else(parser);
408 } else if (strcmp(command, "endif") == 0) {
409 menu_parser_condition_end(parser);
411 } else {
412 WMenuParserError(parser, _("unknow directive '#%s'"), command);
413 return;
416 if (menu_parser_skip_spaces_and_comments(parser))
417 WMenuParserError(parser, _("extra text after '#' command is ignored: \"%.16s...\""),
418 parser->rd);
421 /* Extract the file name, search for it in known directories
422 and create a sub-parser to handle it.
423 Returns False if the file could not be found */
424 static Bool menu_parser_include_file(WMenuParser parser)
426 char buffer[MAXLINE];
427 char *req_filename, *fullfilename, *p;
428 char eot;
429 FILE *fh;
431 if (!menu_parser_skip_spaces_and_comments(parser)) {
432 WMenuParserError(parser, _("no file name found for #include") );
433 return False;
435 switch (*parser->rd++) {
436 case '<': eot = '>'; break;
437 case '"': eot = '"'; break;
438 default:
439 WMenuParserError(parser, _("file name must be enclosed in brackets or double-quotes for #define") );
440 return False;
442 req_filename = parser->rd;
443 while (*parser->rd)
444 if (*parser->rd == eot) {
445 *parser->rd++ = '\0';
446 goto found_end_define_fname;
447 } else parser->rd++;
448 WMenuParserError(parser, _("missing closing '%c' in filename specification"), eot);
449 return False;
450 found_end_define_fname:
452 /* If we're inside a #if sequence, we abort now, but not sooner in
453 order to keep the syntax check */
454 if (parser->cond.stack[0].skip)
455 return False;
457 { /* Check we are not nesting too many includes */
458 WMenuParser p;
459 int count;
461 count = 0;
462 for (p = parser; p->parent_file; p = p->parent_file)
463 count++;
464 if (count > MAX_NESTED_INCLUDES) {
465 WMenuParserError(parser, _("too many nested includes") );
466 return False;
470 /* Absolute paths */
471 fullfilename = req_filename;
472 if (req_filename[0] != '/') {
473 /* Search first in the same directory as the current file */
474 p = strrchr(parser->file_name, '/');
475 if (p != NULL) {
476 int len;
478 len = p - parser->file_name + 1;
479 if (len > sizeof(buffer) - 1) len = sizeof(buffer) - 1;
480 strncpy(buffer, parser->file_name, len);
481 strncpy(buffer+len, req_filename, sizeof(buffer) - len - 1);
482 buffer[sizeof(buffer) - 1] = '\0';
483 fullfilename = buffer;
486 fh = fopen(fullfilename, "rb");
488 /* Not found? Search in wmaker's known places */
489 if (fh == NULL) {
490 if (req_filename[0] != '/') {
491 const char *src;
492 int idx;
494 fullfilename = buffer;
495 src = parser->include_default_paths;
496 while (*src != '\0') {
497 idx = 0;
498 if (*src == '~') {
499 char *home = wgethomedir();
500 while (*home != '\0') {
501 if (idx < sizeof(buffer) - 2)
502 buffer[idx++] = *home;
503 home++;
505 src++;
507 while ((*src != '\0') && (*src != ':')) {
508 if (idx < sizeof(buffer) - 2)
509 buffer[idx++] = *src;
510 src++;
512 buffer[idx++] = '/';
513 for (p = req_filename; *p != '\0'; p++)
514 if (idx < sizeof(buffer) - 1)
515 buffer[idx++] = *p;
516 buffer[idx] = '\0';
518 fh = fopen(fullfilename, "rb");
519 if (fh != NULL) goto found_valid_file;
521 if (*src == ':')
522 src++;
525 WMenuParserError(parser, _("could not find file \"%s\" for include"), req_filename);
526 return False;
529 /* Found the file, make it our new source */
530 found_valid_file:
531 parser->include_file = menu_parser_create_new(wstrdup(req_filename), fh, parser->include_default_paths);
532 parser->include_file->parent_file = parser;
533 return True;
536 /* Check wether a macro exists or not, and marks the parser to ignore the
537 following data accordingly */
538 static void menu_parser_condition_ifmacro(WMenuParser parser, Bool check_exists)
540 WParserMacro *macro;
541 int idx;
542 const char *cmd_name, *macro_name;
544 cmd_name = check_exists?"ifdef":"ifndef";
545 if (!menu_parser_skip_spaces_and_comments(parser)) {
546 WMenuParserError(parser, _("missing macro name argument to #%s"), cmd_name);
547 return;
550 /* jump to end of provided name for later checks that no extra stuff is following */
551 macro_name = parser->rd;
552 while (isnamechr(*parser->rd))
553 parser->rd++;
555 /* Add this condition to the stack of conditions */
556 if (parser->cond.depth >= sizeof(parser->cond.stack) / sizeof(parser->cond.stack[0])) {
557 WMenuParserError(parser, _("too many nested #if sequences") );
558 return;
560 for (idx = parser->cond.depth - 1; idx >= 0; idx--)
561 parser->cond.stack[idx + 1] = parser->cond.stack[idx];
562 parser->cond.depth++;
564 if (parser->cond.stack[1].skip)
565 parser->cond.stack[0].skip = True;
566 else {
567 macro = menu_parser_find_macro(parser, macro_name);
568 parser->cond.stack[0].skip =
569 ((check_exists) && (macro == NULL)) ||
570 ((!check_exists) && (macro != NULL)) ;
572 strcpy(parser->cond.stack[0].name, cmd_name);
573 parser->cond.stack[0].line = parser->line_number;
576 /* Swap the 'data ignore' flag because a #else condition was found */
577 static void menu_parser_condition_else(WMenuParser parser)
579 if (parser->cond.depth <= 0) {
580 WMenuParserError(parser, _("found #%s but have no matching #if"), "else" );
581 return;
583 if ((parser->cond.depth > 1) && (parser->cond.stack[1].skip))
584 // The containing #if is false, so we continue skipping anyway
585 parser->cond.stack[0].skip = True;
586 else
587 parser->cond.stack[0].skip = !parser->cond.stack[0].skip;
590 /* Closes the current conditional, removing it from the stack */
591 static void menu_parser_condition_end(WMenuParser parser)
593 int idx;
595 if (parser->cond.depth <= 0) {
596 WMenuParserError(parser, _("found #%s but have no matching #if"), "endif" );
597 return;
600 if (--parser->cond.depth > 0)
601 for (idx = 0; idx < parser->cond.depth; idx++)
602 parser->cond.stack[idx] = parser->cond.stack[idx + 1];
603 else
604 parser->cond.stack[0].skip = False;