1 // Written in the D programming language.
2 
3 /**
4 This module is DML (DlangUI Markup Language) parser - similar to QML in QtQuick
5 
6 Synopsis:
7 
8 ----
9 
10 Widget layout = parseML(q{
11     VerticalLayout {
12         TextWidget { text: "Some label" }
13         TextLine { id: editor; text: "Some text to edit" }
14         Button { id: btnOk; text: "Ok" }
15     }
16 });
17 
18 
19 ----
20 
21 Copyright: Vadim Lopatin, 2015
22 License:   Boost License 1.0
23 Authors:   Vadim Lopatin, coolreader.org@gmail.com
24  */
25 module dlangui.dml.parser;
26 
27 import dlangui.core.linestream;
28 import dlangui.core.collections;
29 import dlangui.core.types;
30 import dlangui.widgets.widget;
31 import dlangui.widgets.metadata;
32 import std.conv : to;
33 import std.algorithm : equal, min, max;
34 import std.utf : toUTF32;
35 import std.array : join;
36 public import dlangui.dml.annotations;
37 public import dlangui.dml.tokenizer;
38 
39 /// parser exception - unknown (unregistered) widget name
40 class UnknownWidgetException : ParserException {
41     protected string _objectName;
42 
43     @property string objectName() { return _objectName; }
44 
45     this(string msg, string objectName, string file, int line, int pos) {
46         super(msg is null ? "Unknown widget name: " ~ objectName : msg, file, line, pos);
47         _objectName = objectName;
48     }
49 }
50 
51 /// parser exception - unknown property for widget
52 class UnknownPropertyException : UnknownWidgetException {
53     protected string _propName;
54 
55     @property string propName() { return _propName; }
56 
57     this(string msg, string objectName, string propName, string file, int line, int pos) {
58         super(msg is null ? "Unknown property " ~ objectName ~ "." ~ propName : msg, objectName, file, line, pos);
59     }
60 }
61 
62 class MLParser {
63     protected string _code;
64     protected string _filename;
65     protected bool _ownContext;
66     protected Widget _context;
67     protected Widget _currentWidget;
68     protected Tokenizer _tokenizer;
69     protected Collection!Widget _treeStack;
70 
71     protected this(string code, string filename = "", Widget context = null) {
72         _code = code;
73         _filename = filename;
74         _context = context;
75         _tokenizer = new Tokenizer(code, filename);
76     }
77 
78     protected Token _token;
79 
80     /// move to next token
81     protected void nextToken() {
82         _token = _tokenizer.nextToken();
83         //Log.d("parsed token: ", _token.type, " ", _token.line, ":", _token.pos, " ", _token.text);
84     }
85 
86     /// throw exception if current token is eof
87     protected void checkNoEof() {
88         if (_token.type == TokenType.eof)
89             error("unexpected end of file");
90     }
91 
92     /// move to next token, throw exception if eof
93     protected void nextTokenNoEof() {
94         nextToken();
95         checkNoEof();
96     }
97 
98     protected void skipWhitespaceAndEols() {
99         for (;;) {
100             if (_token.type != TokenType.eol && _token.type != TokenType.whitespace && _token.type != TokenType.comment)
101                 break;
102             nextToken();
103         }
104         if (_token.type == TokenType.error)
105             error("error while parsing ML code");
106     }
107 
108     protected void skipWhitespaceAndEolsNoEof() {
109         skipWhitespaceAndEols();
110         checkNoEof();
111     }
112 
113     protected void skipWhitespaceNoEof() {
114         skipWhitespace();
115         checkNoEof();
116     }
117 
118     protected void skipWhitespace() {
119         for (;;) {
120             if (_token.type != TokenType.whitespace && _token.type != TokenType.comment)
121                 break;
122             nextToken();
123         }
124         if (_token.type == TokenType.error)
125             error("error while parsing ML code");
126     }
127 
128     protected void error(string msg) {
129         _tokenizer.emitError(msg);
130     }
131 
132     protected void unknownObjectError(string objectName) {
133         throw new UnknownWidgetException("Unknown widget type " ~ objectName ~ _tokenizer.getContextSource(), objectName, _tokenizer.filename, _tokenizer.line, _tokenizer.pos);
134     }
135 
136     protected void unknownPropertyError(string objectName, string propName) {
137         throw new UnknownPropertyException("Unknown property " ~ objectName ~ "." ~ propName ~ _tokenizer.getContextSource(), objectName, propName, _tokenizer.filename, _tokenizer.line, _tokenizer.pos);
138     }
139 
140     Widget createWidget(string name) {
141         auto metadata = findWidgetMetadata(name);
142         if (!metadata)
143             error("Cannot create widget " ~ name ~ " : unregistered widget class");
144         return metadata.create();
145     }
146 
147     protected void createContext(string name) {
148         if (_context)
149             error("Context widget is already specified, but identifier " ~ name ~ " is found");
150         _context = createWidget(name);
151         _ownContext = true;
152     }
153 
154     protected int applySuffix(int value, string suffix) {
155         if (suffix.length > 0) {
156             if (suffix.equal("px")) {
157                 // do nothing, value is in px by default
158             } else if (suffix.equal("pt")) {
159                 value = makePointSize(value);
160             } else if (suffix.equal("m") || suffix.equal("em")) {
161                 // todo: implement EMs
162                 value = makePointSize(value);
163             } else if (suffix.equal("%")) {
164                 value = makePercentSize(value);
165             } else
166                 error("unknown number suffix: " ~ suffix);
167         }
168         return value;
169     }
170 
171     protected void setIntProperty(string propName, int value, string suffix = null) {
172         value = applySuffix(value, suffix);
173         if (!_currentWidget.setIntProperty(propName, value))
174             error("unknown int property " ~ propName);
175     }
176 
177     protected void setBoolProperty(string propName, bool value) {
178         if (!_currentWidget.setBoolProperty(propName, value))
179             error("unknown int property " ~ propName);
180     }
181 
182     protected void setFloatProperty(string propName, double value) {
183         if (!_currentWidget.setDoubleProperty(propName, value))
184             error("unknown double property " ~ propName);
185     }
186 
187     protected void setStringListValueProperty(string propName, StringListValue[] values) {
188         if (!_currentWidget.setStringListValueListProperty(propName, values)) {
189             UIString[] strings;
190             foreach(value; values)
191                 strings ~= value.label;
192             if (!_currentWidget.setUIStringListProperty(propName, strings)) {
193                 error("unknown string list property " ~ propName);
194             }
195         }
196     }
197 
198 
199     protected void setRectProperty(string propName, Rect value) {
200         if (!_currentWidget.setRectProperty(propName, value))
201             error("unknown Rect property " ~ propName);
202     }
203 
204     protected void setStringProperty(string propName, string value) {
205         if (propName.equal("id") || propName.equal("styleId") || propName.equal("backgroundImageId")) {
206             if (!_currentWidget.setStringProperty(propName, value))
207                 error("cannot set " ~ propName ~ " property for widget");
208             return;
209         }
210 
211         dstring v = toUTF32(value);
212         if (!_currentWidget.setDstringProperty(propName, v)) {
213             if (!_currentWidget.setStringProperty(propName, value))
214                 error("unknown string property " ~ propName);
215         }
216     }
217 
218     protected void setIdentProperty(string propName, string value) {
219         if (propName.equal("id") || propName.equal("styleId") || propName.equal("backgroundImageId")) {
220             if (!_currentWidget.setStringProperty(propName, value))
221                 error("cannot set id property for widget");
222             return;
223         }
224 
225         if (value.equal("true"))
226             setBoolProperty(propName, true);
227         else if (value.equal("false"))
228             setBoolProperty(propName, false);
229         else if (value.equal("fill") || value.equal("FILL") || value.equal("FILL_PARENT"))
230             setIntProperty(propName, FILL_PARENT);
231         else if (value.equal("wrap") || value.equal("WRAP") || value.equal("WRAP_CONTENT"))
232             setIntProperty(propName, WRAP_CONTENT);
233         else if (value.equal("left") || value.equal("Left"))
234             setIntProperty(propName, Align.Left);
235         else if (value.equal("right") || value.equal("Right"))
236             setIntProperty(propName, Align.Right);
237         else if (value.equal("top") || value.equal("Top"))
238             setIntProperty(propName, Align.Top);
239         else if (value.equal("bottom") || value.equal("Bottom"))
240             setIntProperty(propName, Align.Bottom);
241         else if (value.equal("hcenter") || value.equal("HCenter"))
242             setIntProperty(propName, Align.HCenter);
243         else if (value.equal("vcenter") || value.equal("VCenter"))
244             setIntProperty(propName, Align.VCenter);
245         else if (value.equal("center") || value.equal("Center"))
246             setIntProperty(propName, Align.Center);
247         else if (value.equal("topleft") || value.equal("TopLeft"))
248             setIntProperty(propName, Align.TopLeft);
249         else if (propName.equal("orientation") && (value.equal("vertical") || value.equal("Vertical")))
250             setIntProperty(propName, Orientation.Vertical);
251         else if (propName.equal("orientation") && (value.equal("horizontal") || value.equal("Horizontal")))
252             setIntProperty(propName, Orientation.Horizontal);
253         else if (!_currentWidget.setStringProperty(propName, value))
254             error("unknown ident property " ~ propName);
255     }
256 
257     protected void parseRectProperty(string propName) {
258         // current token is Rect
259         int[4] values = [0, 0, 0, 0];
260         nextToken();
261         skipWhitespaceAndEolsNoEof();
262         if (_token.type != TokenType.curlyOpen)
263             error("{ expected after Rect");
264         nextToken();
265         skipWhitespaceAndEolsNoEof();
266         int index = 0;
267         for (;;) {
268             if (_token.type == TokenType.curlyClose)
269                 break;
270             if (_token.type == TokenType.integer) {
271                 if (index >= 4)
272                     error("too many values in Rect");
273                 int n = applySuffix(_token.intvalue, _token.text);
274                 values[index++] = n;
275                 nextToken();
276                 skipWhitespaceAndEolsNoEof();
277                 if (_token.type == TokenType.comma || _token.type == TokenType.semicolon) {
278                     nextToken();
279                     skipWhitespaceAndEolsNoEof();
280                 }
281             } else if (_token.type == TokenType.ident) {
282                 string name = _token.text;
283                 nextToken();
284                 skipWhitespaceAndEolsNoEof();
285                 if (_token.type != TokenType.colon)
286                     error(": expected after property name " ~ name ~ " in Rect definition");
287                 nextToken();
288                 skipWhitespaceNoEof();
289                 if (_token.type != TokenType.integer)
290                     error("integer expected as Rect property value");
291                 int n = applySuffix(_token.intvalue, _token.text);
292 
293                 if (name.equal("left"))
294                     values[0] = n;
295                 else if (name.equal("top"))
296                     values[1] = n;
297                 else if (name.equal("right"))
298                     values[2] = n;
299                 else if (name.equal("bottom"))
300                     values[3] = n;
301                 else
302                     error("unknown property " ~ name ~ " in Rect");
303 
304                 nextToken();
305                 skipWhitespaceNoEof();
306                 if (_token.type == TokenType.comma || _token.type == TokenType.semicolon) {
307                     nextToken();
308                     skipWhitespaceAndEolsNoEof();
309                 }
310             } else {
311                 error("invalid Rect definition");
312             }
313 
314         }
315         setRectProperty(propName, Rect(values[0], values[1], values[2], values[3]));
316     }
317 
318     // something in []
319     protected void parseArrayProperty(string propName) {
320         // current token is Rect
321         nextToken();
322         skipWhitespaceAndEolsNoEof();
323         StringListValue[] values;
324         for (;;) {
325             if (_token.type == TokenType.squareClose)
326                 break;
327             if (_token.type == TokenType.integer) {
328                 if (_token.text.length)
329                     error("Integer literal suffixes not allowed for [] items");
330                 StringListValue value;
331                 value.intId = _token.intvalue;
332                 value.label = UIString.fromRaw(to!dstring(_token.intvalue));
333                 values ~= value;
334                 nextToken();
335                 skipWhitespaceAndEolsNoEof();
336                 if (_token.type == TokenType.comma || _token.type == TokenType.semicolon) {
337                     nextToken();
338                     skipWhitespaceAndEolsNoEof();
339                 }
340             } else if (_token.type == TokenType.ident) {
341                 string name = _token.text;
342 
343                 StringListValue value;
344                 value.stringId = name;
345                 value.label = UIString.fromRaw(name);
346                 values ~= value;
347 
348                 nextToken();
349                 skipWhitespaceAndEolsNoEof();
350 
351                 if (_token.type == TokenType.comma || _token.type == TokenType.semicolon) {
352                     nextToken();
353                     skipWhitespaceAndEolsNoEof();
354                 }
355             } else if (_token.type == TokenType.str) {
356                 string name = _token.text;
357 
358                 StringListValue value;
359                 value.stringId = name;
360                 value.label = UIString.fromRaw(name.toUTF32);
361                 values ~= value;
362 
363                 nextToken();
364                 skipWhitespaceAndEolsNoEof();
365 
366                 if (_token.type == TokenType.comma || _token.type == TokenType.semicolon) {
367                     nextToken();
368                     skipWhitespaceAndEolsNoEof();
369                 }
370             } else {
371                 error("invalid [] item");
372             }
373 
374         }
375         setStringListValueProperty(propName, values);
376     }
377 
378     protected void parseProperty() {
379         if (_token.type != TokenType.ident)
380             error("identifier expected");
381         string propName = _token.text;
382         nextToken();
383         skipWhitespaceNoEof();
384         if (_token.type == TokenType.colon) { // :
385             nextTokenNoEof(); // skip :
386             skipWhitespaceNoEof();
387             if (_token.type == TokenType.integer)
388                 setIntProperty(propName, _token.intvalue, _token.text);
389             else if (_token.type == TokenType.minus || _token.type == TokenType.plus) {
390                 int sign = _token.type == TokenType.minus ? -1 : 1;
391                 nextTokenNoEof(); // skip :
392                 skipWhitespaceNoEof();
393                 if (_token.type == TokenType.integer) {
394                     setIntProperty(propName, _token.intvalue * sign, _token.text);
395                 } else if (_token.type == TokenType.floating) {
396                     setFloatProperty(propName, _token.floatvalue * sign);
397                 } else
398                     error("number expected after + and -");
399             } else if (_token.type == TokenType.floating)
400                 setFloatProperty(propName, _token.floatvalue);
401             else if (_token.type == TokenType.squareOpen)
402                 parseArrayProperty(propName);
403             else if (_token.type == TokenType.str)
404                 setStringProperty(propName, _token.text);
405             else if (_token.type == TokenType.ident) {
406                 if (_token.text.equal("Rect")) {
407                     parseRectProperty(propName);
408                 } else {
409                     setIdentProperty(propName, _token.text);
410                 }
411             } else
412                 error("int, float, string or identifier are expected as property value");
413             nextTokenNoEof();
414             skipWhitespaceNoEof();
415             if (_token.type == TokenType.semicolon) {
416                 // separated by ;
417                 nextTokenNoEof();
418                 skipWhitespaceAndEolsNoEof();
419                 return;
420             } else if (_token.type == TokenType.eol) {
421                 nextTokenNoEof();
422                 skipWhitespaceAndEolsNoEof();
423                 return;
424             } else if (_token.type == TokenType.curlyClose) {
425                 // it was last property in object
426                 return;
427             }
428             error("; eol or } expected after property definition");
429         } else if (_token.type == TokenType.curlyOpen) { // { -- start of object
430             Widget s = createWidget(propName);
431             parseWidgetProperties(s);
432         } else {
433             error(": or { expected after identifier");
434         }
435 
436     }
437 
438     protected void parseWidgetProperties(Widget w) {
439         if (_token.type != TokenType.curlyOpen) // {
440             error("{ is expected");
441         _treeStack.pushBack(w);
442         if (_currentWidget)
443             _currentWidget.addChild(w);
444         _currentWidget = w;
445         nextToken(); // skip {
446         skipWhitespaceAndEols();
447         for (;;) {
448             checkNoEof();
449             if (_token.type == TokenType.curlyClose) // end of object's internals
450                 break;
451             parseProperty();
452         }
453         if (_token.type != TokenType.curlyClose) // {
454             error("{ is expected");
455         nextToken(); // skip }
456         skipWhitespaceAndEols();
457         _treeStack.popBack();
458         _currentWidget = _treeStack.peekBack();
459     }
460 
461     protected Widget parse() {
462         try {
463             nextToken();
464             skipWhitespaceAndEols();
465             if (_token.type == TokenType.ident) {
466                 createContext(_token.text);
467                 nextToken();
468                 skipWhitespaceAndEols();
469             }
470             if (_token.type != TokenType.curlyOpen) // {
471                 error("{ is expected");
472             if (!_context)
473                 error("No context widget is specified!");
474             parseWidgetProperties(_context);
475 
476             skipWhitespaceAndEols();
477             if (_token.type != TokenType.eof) // {
478                 error("end of file expected");
479             return _context;
480         } catch (Exception e) {
481             Log.e("exception while parsing ML", e);
482             if (_context && _ownContext)
483                 destroy(_context);
484             _context = null;
485             throw e;
486         }
487     }
488 
489     ~this() {
490         destroy(_tokenizer);
491         _tokenizer = null;
492     }
493 
494 }
495 
496 
497 /// Parse DlangUI ML code
498 public T parseML(T = Widget)(string code, string filename = "", Widget context = null) {
499     MLParser parser = new MLParser(code, filename, context);
500     scope(exit) destroy(parser);
501     Widget w = parser.parse();
502     T res = cast(T) w;
503     if (w && !res && !context) {
504         destroy(w);
505         throw new ParserException("Cannot convert parsed widget to " ~ T.stringof, "", 0, 0);
506     }
507     return res;
508 }
509 
510 /// tokenize source into array of tokens (excluding EOF)
511 public Token[] tokenizeML(const(dstring[]) lines) {
512     string code = toUTF8(join(lines, "\n"));
513     return tokenizeML(code);
514 }
515 
516 /// tokenize source into array of tokens (excluding EOF)
517 public Token[] tokenizeML(const(string[]) lines) {
518     string code = join(lines, "\n");
519     return tokenizeML(code);
520 }
521 
522 /// tokenize source into array of tokens (excluding EOF)
523 public Token[] tokenizeML(string code) {
524     Token[] res;
525     auto tokenizer = new Tokenizer(code, "");
526     for (;;) {
527         auto token = tokenizer.nextToken();
528         if (token.type == TokenType.eof)
529             break;
530         res ~= token;
531     }
532     return res;
533 }
534 
535 //pragma(msg, tokenizeML("Widget {}"));