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 {}"));