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