diff --git a/dlanguilib.visualdproj b/dlanguilib.visualdproj
index ddb11da3..4f8010fd 100644
--- a/dlanguilib.visualdproj
+++ b/dlanguilib.visualdproj
@@ -386,6 +386,7 @@
+
diff --git a/src/dlangui/dml/annotations.d b/src/dlangui/dml/annotations.d
new file mode 100644
index 00000000..77ee5eee
--- /dev/null
+++ b/src/dlangui/dml/annotations.d
@@ -0,0 +1,16 @@
+module dlangui.dml.annotations;
+
+/// annotate widget with @dmlwidget UDA to allow using it in DML
+struct dmlwidget {
+ bool dummy;
+}
+
+/// annotate widget property with @dmlproperty UDA to allow using it in DML
+struct dmlproperty {
+ bool dummy;
+}
+
+/// annotate signal with @dmlsignal UDA
+struct dmlsignal {
+ bool dummy;
+}
diff --git a/src/dlangui/dml/dmlhighlight.d b/src/dlangui/dml/dmlhighlight.d
new file mode 100644
index 00000000..09ecfa1e
--- /dev/null
+++ b/src/dlangui/dml/dmlhighlight.d
@@ -0,0 +1,669 @@
+module dlangui.dml.dmlhighlight;
+
+import dlangui.core.editable;
+import dlangui.core.linestream;
+import dlangui.core.textsource;
+import dlangui.core.logger;
+import dlangui.dml.parser;
+import dlangui.widgets.metadata;
+
+class DMLSyntaxSupport : SyntaxSupport {
+
+ EditableContent _content;
+ SourceFile _file;
+ this (string filename) {
+ _file = new SourceFile(filename);
+ }
+
+ TokenPropString[] _props;
+
+ /// returns editable content
+ @property EditableContent content() { return _content; }
+ /// set editable content
+ @property SyntaxSupport content(EditableContent content) {
+ _content = content;
+ return this;
+ }
+
+ private enum BracketMatch {
+ CONTINUE,
+ FOUND,
+ ERROR
+ }
+
+ private static struct BracketStack {
+ dchar[] buf;
+ int pos;
+ bool reverse;
+ void init(bool reverse) {
+ this.reverse = reverse;
+ pos = 0;
+ }
+ void push(dchar ch) {
+ if (buf.length <= pos)
+ buf.length = pos + 16;
+ buf[pos++] = ch;
+ }
+ dchar pop() {
+ if (pos <= 0)
+ return 0;
+ return buf[--pos];
+ }
+ BracketMatch process(dchar ch) {
+ if (reverse) {
+ if (isCloseBracket(ch)) {
+ push(ch);
+ return BracketMatch.CONTINUE;
+ } else {
+ if (pop() != pairedBracket(ch))
+ return BracketMatch.ERROR;
+ if (pos == 0)
+ return BracketMatch.FOUND;
+ return BracketMatch.CONTINUE;
+ }
+ } else {
+ if (isOpenBracket(ch)) {
+ push(ch);
+ return BracketMatch.CONTINUE;
+ } else {
+ if (pop() != pairedBracket(ch))
+ return BracketMatch.ERROR;
+ if (pos == 0)
+ return BracketMatch.FOUND;
+ return BracketMatch.CONTINUE;
+ }
+ }
+ }
+ }
+ BracketStack _bracketStack;
+ static bool isBracket(dchar ch) {
+ return pairedBracket(ch) != 0;
+ }
+ static dchar pairedBracket(dchar ch) {
+ switch (ch) {
+ case '(':
+ return ')';
+ case ')':
+ return '(';
+ case '{':
+ return '}';
+ case '}':
+ return '{';
+ case '[':
+ return ']';
+ case ']':
+ return '[';
+ default:
+ return 0; // not a bracket
+ }
+ }
+ static bool isOpenBracket(dchar ch) {
+ switch (ch) {
+ case '(':
+ case '{':
+ case '[':
+ return true;
+ default:
+ return false;
+ }
+ }
+ static bool isCloseBracket(dchar ch) {
+ switch (ch) {
+ case ')':
+ case '}':
+ case ']':
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ protected dchar nextBracket(int dir, ref TextPosition p) {
+ for (;;) {
+ TextPosition oldpos = p;
+ p = dir < 0 ? _content.prevCharPos(p) : _content.nextCharPos(p);
+ if (p == oldpos)
+ return 0;
+ auto prop = _content.tokenProp(p);
+ if (tokenCategory(prop) == TokenCategory.Op) {
+ dchar ch = _content[p];
+ if (isBracket(ch))
+ return ch;
+ }
+ }
+ }
+
+ /// returns paired bracket {} () [] for char at position p, returns paired char position or p if not found or not bracket
+ override TextPosition findPairedBracket(TextPosition p) {
+ if (p.line < 0 || p.line >= content.length)
+ return p;
+ dstring s = content.line(p.line);
+ if (p.pos < 0 || p.pos >= s.length)
+ return p;
+ dchar ch = content[p];
+ dchar paired = pairedBracket(ch);
+ if (!paired)
+ return p;
+ TextPosition startPos = p;
+ int dir = isOpenBracket(ch) ? 1 : -1;
+ _bracketStack.init(dir < 0);
+ _bracketStack.process(ch);
+ for (;;) {
+ ch = nextBracket(dir, p);
+ if (!ch) // no more brackets
+ return startPos;
+ auto match = _bracketStack.process(ch);
+ if (match == BracketMatch.FOUND)
+ return p;
+ if (match == BracketMatch.ERROR)
+ return startPos;
+ // continue
+ }
+ }
+
+
+ /// return true if toggle line comment is supported for file type
+ override @property bool supportsToggleLineComment() {
+ return true;
+ }
+
+ /// return true if can toggle line comments for specified text range
+ override bool canToggleLineComment(TextRange range) {
+ TextRange r = content.fullLinesRange(range);
+ if (isInsideBlockComment(r.start) || isInsideBlockComment(r.end))
+ return false;
+ return true;
+ }
+
+ protected bool isLineComment(dstring s) {
+ for (int i = 0; i < cast(int)s.length - 1; i++) {
+ if (s[i] == '/' && s[i + 1] == '/')
+ return true;
+ else if (s[i] != ' ' && s[i] != '\t')
+ return false;
+ }
+ return false;
+ }
+
+ protected dstring commentLine(dstring s, int commentX) {
+ dchar[] res;
+ int x = 0;
+ bool commented = false;
+ for (int i = 0; i < s.length; i++) {
+ dchar ch = s[i];
+ if (ch == '\t') {
+ int newX = (x + _content.tabSize) / _content.tabSize * _content.tabSize;
+ if (!commented && newX >= commentX) {
+ commented = true;
+ if (newX != commentX) {
+ // replace tab with space
+ for (; x <= commentX; x++)
+ res ~= ' ';
+ } else {
+ res ~= ch;
+ x = newX;
+ }
+ res ~= "//"d;
+ x += 2;
+ } else {
+ res ~= ch;
+ x = newX;
+ }
+ } else {
+ if (!commented && x == commentX) {
+ commented = true;
+ res ~= "//"d;
+ res ~= ch;
+ x += 3;
+ } else {
+ res ~= ch;
+ x++;
+ }
+ }
+ }
+ if (!commented) {
+ for (; x < commentX; x++)
+ res ~= ' ';
+ res ~= "//"d;
+ }
+ return cast(dstring)res;
+ }
+
+ /// remove single line comment from beginning of line
+ protected dstring uncommentLine(dstring s) {
+ int p = -1;
+ for (int i = 0; i < cast(int)s.length - 1; i++) {
+ if (s[i] == '/' && s[i + 1] == '/') {
+ p = i;
+ break;
+ }
+ }
+ if (p < 0)
+ return s;
+ s = s[0..p] ~ s[p + 2 .. $];
+ for (int i = 0; i < s.length; i++) {
+ if (s[i] != ' ' && s[i] != '\t') {
+ return s;
+ }
+ }
+ return null;
+ }
+
+ /// searches for neares token start before or equal to position
+ protected TextPosition tokenStart(TextPosition pos) {
+ TextPosition p = pos;
+ for (;;) {
+ TextPosition prevPos = content.prevCharPos(p);
+ if (p == prevPos)
+ return p; // begin of file
+ TokenProp prop = content.tokenProp(p);
+ TokenProp prevProp = content.tokenProp(prevPos);
+ if (prop && prop != prevProp)
+ return p;
+ p = prevPos;
+ }
+ }
+
+ static struct TokenWithRange {
+ Token token;
+ TextRange range;
+ @property string toString() {
+ return token.toString ~ range.toString;
+ }
+ }
+
+ protected Token[] _tokens;
+ protected int _tokenIndex;
+
+ protected bool initTokenizer() {
+ _tokens = tokenizeML(content.lines);
+ _tokenIndex = 0;
+ return true;
+ }
+
+ protected TokenWithRange nextToken() {
+ TokenWithRange res;
+ if (_tokenIndex < _tokens.length) {
+ res.range.start = TextPosition(_tokens[_tokenIndex].line, _tokens[_tokenIndex].pos);
+ if (_tokenIndex + 1 < _tokens.length)
+ res.range.end = TextPosition(_tokens[_tokenIndex + 1].line, _tokens[_tokenIndex + 1].pos);
+ else
+ res.range.end = content.endOfFile();
+ res.token = _tokens[_tokenIndex];
+ _tokenIndex++;
+ } else {
+ res.range.end = res.range.start = content.endOfFile();
+ }
+ return res;
+ }
+
+ protected TokenWithRange getPositionToken(TextPosition pos) {
+ initTokenizer();
+ for (;;) {
+ TokenWithRange tokenRange = nextToken();
+ //Log.d("read token: ", tokenRange);
+ if (tokenRange.token.type == TokenType.eof) {
+ //Log.d("end of file");
+ return tokenRange;
+ }
+ if (pos >= tokenRange.range.start && pos < tokenRange.range.end) {
+ //Log.d("found: ", pos, " in ", tokenRange);
+ return tokenRange;
+ }
+ }
+ }
+
+ protected TokenWithRange[] getRangeTokens(TextRange range) {
+ TokenWithRange[] res;
+ initTokenizer();
+ for (;;) {
+ TokenWithRange tokenRange = nextToken();
+ //Log.d("read token: ", tokenRange);
+ if (tokenRange.token.type == TokenType.eof) {
+ return res;
+ }
+ if (tokenRange.range.intersects(range)) {
+ //Log.d("found: ", pos, " in ", tokenRange);
+ res ~= tokenRange;
+ }
+ }
+ }
+
+ protected bool isInsideBlockComment(TextPosition pos) {
+ TokenWithRange tokenRange = getPositionToken(pos);
+ if (tokenRange.token.type == TokenType.comment && tokenRange.token.isMultilineComment)
+ return pos > tokenRange.range.start && pos < tokenRange.range.end;
+ return false;
+ }
+
+ /// toggle line comments for specified text range
+ override void toggleLineComment(TextRange range, Object source) {
+ TextRange r = content.fullLinesRange(range);
+ if (isInsideBlockComment(r.start) || isInsideBlockComment(r.end))
+ return;
+ int lineCount = r.end.line - r.start.line;
+ bool noEolAtEndOfRange = false;
+ if (lineCount == 0 || r.end.pos > 0) {
+ noEolAtEndOfRange = true;
+ lineCount++;
+ }
+ int minLeftX = -1;
+ bool hasComments = false;
+ bool hasNoComments = false;
+ bool hasNonEmpty = false;
+ dstring[] srctext;
+ dstring[] dsttext;
+ for (int i = 0; i < lineCount; i++) {
+ int lineIndex = r.start.line + i;
+ dstring s = content.line(lineIndex);
+ srctext ~= s;
+ TextLineMeasure m = content.measureLine(lineIndex);
+ if (!m.empty) {
+ if (minLeftX < 0 || minLeftX > m.firstNonSpaceX)
+ minLeftX = m.firstNonSpaceX;
+ hasNonEmpty = true;
+ if (isLineComment(s))
+ hasComments = true;
+ else
+ hasNoComments = true;
+ }
+ }
+ if (minLeftX < 0)
+ minLeftX = 0;
+ if (hasNoComments || !hasComments) {
+ // comment
+ for (int i = 0; i < lineCount; i++) {
+ dsttext ~= commentLine(srctext[i], minLeftX);
+ }
+ if (!noEolAtEndOfRange)
+ dsttext ~= ""d;
+ EditOperation op = new EditOperation(EditAction.Replace, r, dsttext);
+ _content.performOperation(op, source);
+ } else {
+ // uncomment
+ for (int i = 0; i < lineCount; i++) {
+ dsttext ~= uncommentLine(srctext[i]);
+ }
+ if (!noEolAtEndOfRange)
+ dsttext ~= ""d;
+ EditOperation op = new EditOperation(EditAction.Replace, r, dsttext);
+ _content.performOperation(op, source);
+ }
+ }
+
+ /// return true if toggle block comment is supported for file type
+ override @property bool supportsToggleBlockComment() {
+ return true;
+ }
+ /// return true if can toggle block comments for specified text range
+ override bool canToggleBlockComment(TextRange range) {
+ TokenWithRange startToken = getPositionToken(range.start);
+ TokenWithRange endToken = getPositionToken(range.end);
+ //Log.d("canToggleBlockComment: startToken=", startToken, " endToken=", endToken);
+ if (startToken.range == endToken.range && startToken.token.isMultilineComment) {
+ //Log.d("canToggleBlockComment: can uncomment");
+ return true;
+ }
+ if (range.empty)
+ return false;
+ TokenWithRange[] tokens = getRangeTokens(range);
+ foreach(ref t; tokens) {
+ if (t.token.type == TokenType.comment) {
+ if (t.token.isMultilineComment) {
+ // disable until nested comments support is implemented
+ return false;
+ } else {
+ // single line comment
+ if (t.range.isInside(range.start) || t.range.isInside(range.end))
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+ /// toggle block comments for specified text range
+ override void toggleBlockComment(TextRange srcrange, Object source) {
+ TokenWithRange startToken = getPositionToken(srcrange.start);
+ TokenWithRange endToken = getPositionToken(srcrange.end);
+ if (startToken.range == endToken.range && startToken.token.isMultilineComment) {
+ TextRange range = startToken.range;
+ dstring[] dsttext;
+ for (int i = range.start.line; i <= range.end.line; i++) {
+ dstring s = content.line(i);
+ int charsRemoved = 0;
+ int minp = 0;
+ if (i == range.start.line) {
+ int maxp = content.lineLength(range.start.line);
+ if (i == range.end.line)
+ maxp = range.end.pos - 2;
+ charsRemoved = 2;
+ for (int j = range.start.pos + charsRemoved; j < maxp; j++) {
+ if (s[j] != s[j - 1])
+ break;
+ charsRemoved++;
+ }
+ //Log.d("line before removing start of comment:", s);
+ s = s[range.start.pos + charsRemoved .. $];
+ //Log.d("line after removing start of comment:", s);
+ charsRemoved += range.start.pos;
+ }
+ if (i == range.end.line) {
+ int endp = range.end.pos;
+ if (charsRemoved > 0)
+ endp -= charsRemoved;
+ int endRemoved = 2;
+ for (int j = endp - endRemoved; j >= 0; j--) {
+ if (s[j] != s[j + 1])
+ break;
+ endRemoved++;
+ }
+ //Log.d("line before removing end of comment:", s);
+ s = s[0 .. endp - endRemoved];
+ //Log.d("line after removing end of comment:", s);
+ }
+ dsttext ~= s;
+ }
+ EditOperation op = new EditOperation(EditAction.Replace, range, dsttext);
+ _content.performOperation(op, source);
+ return;
+ } else {
+ if (srcrange.empty)
+ return;
+ TokenWithRange[] tokens = getRangeTokens(srcrange);
+ foreach(ref t; tokens) {
+ if (t.token.type == TokenType.comment) {
+ if (t.token.isMultilineComment) {
+ // disable until nested comments support is implemented
+ return;
+ } else {
+ // single line comment
+ if (t.range.isInside(srcrange.start) || t.range.isInside(srcrange.end))
+ return;
+ }
+ }
+ }
+ dstring[] dsttext;
+ for (int i = srcrange.start.line; i <= srcrange.end.line; i++) {
+ dstring s = content.line(i);
+ int charsAdded = 0;
+ if (i == srcrange.start.line) {
+ int p = srcrange.start.pos;
+ if (p < s.length) {
+ s = s[p .. $];
+ charsAdded = -p;
+ } else {
+ charsAdded = -(cast(int)s.length);
+ s = null;
+ }
+ s = "/*" ~ s;
+ charsAdded += 2;
+ }
+ if (i == srcrange.end.line) {
+ int p = srcrange.end.pos + charsAdded;
+ s = p > 0 ? s[0..p] : null;
+ s ~= "*/";
+ }
+ dsttext ~= s;
+ }
+ EditOperation op = new EditOperation(EditAction.Replace, srcrange, dsttext);
+ _content.performOperation(op, source);
+ return;
+ }
+
+ }
+
+ /// categorize characters in content by token types
+ void updateHighlight(dstring[] lines, TokenPropString[] props, int changeStartLine, int changeEndLine) {
+
+ initTokenizer();
+ _props = props;
+ changeStartLine = 0;
+ changeEndLine = cast(int)lines.length;
+ int tokenPos = 0;
+ int tokenLine = 0;
+ ubyte category = 0;
+ try {
+ for (;;) {
+ TokenWithRange token = nextToken();
+ if (token.token.type == TokenType.eof) {
+ break;
+ }
+ uint newPos = token.range.start.pos;
+ uint newLine = token.range.start.line;
+
+ // fill with category
+ for (int i = tokenLine; i <= newLine; i++) {
+ int start = i > tokenLine ? 0 : tokenPos;
+ int end = i < newLine ? cast(int)lines[i].length : newPos;
+ for (int j = start; j < end; j++) {
+ if (j < _props[i].length) {
+ _props[i][j] = category;
+ }
+ }
+ }
+
+ // handle token - convert to category
+ switch(token.token.type) {
+ case TokenType.comment:
+ category = TokenCategory.Comment;
+ break;
+ case TokenType.ident:
+ if (isWidgetClassName(token.token.text))
+ category = TokenCategory.Identifier_Class;
+ else
+ category = TokenCategory.Identifier;
+ break;
+ case TokenType.str:
+ category = TokenCategory.String;
+ break;
+ case TokenType.integer:
+ category = TokenCategory.Integer;
+ break;
+ case TokenType.floating:
+ category = TokenCategory.Float;
+ break;
+ case TokenType.error:
+ category = TokenCategory.Error;
+ break;
+ default:
+ if (token.token.type >= TokenType.colon)
+ category = TokenCategory.Op;
+ else
+ category = 0;
+ break;
+ }
+ tokenPos = newPos;
+ tokenLine= newLine;
+
+ }
+ } catch (Exception e) {
+ Log.e("exception while trying to parse DML source", e);
+ }
+ _props = null;
+ }
+
+
+ /// returns true if smart indent is supported
+ override bool supportsSmartIndents() {
+ return true;
+ }
+
+ protected bool _opInProgress;
+ protected void applyNewLineSmartIndent(EditOperation op, Object source) {
+ int line = op.newRange.end.line;
+ if (line == 0)
+ return; // not for first line
+ int prevLine = line - 1;
+ dstring lineText = _content.line(line);
+ TextLineMeasure lineMeasurement = _content.measureLine(line);
+ TextLineMeasure prevLineMeasurement = _content.measureLine(prevLine);
+ while (prevLineMeasurement.empty && prevLine > 0) {
+ prevLine--;
+ prevLineMeasurement = _content.measureLine(prevLine);
+ }
+ if (lineMeasurement.firstNonSpaceX >= 0 && lineMeasurement.firstNonSpaceX < prevLineMeasurement.firstNonSpaceX) {
+ dstring prevLineText = _content.line(prevLine);
+ TokenPropString prevLineTokenProps = _content.lineTokenProps(prevLine);
+ dchar lastOpChar = 0;
+ for (int j = prevLineMeasurement.lastNonSpace; j >= 0; j--) {
+ auto cat = j < prevLineTokenProps.length ? tokenCategory(prevLineTokenProps[j]) : 0;
+ if (cat == TokenCategory.Op) {
+ lastOpChar = prevLineText[j];
+ break;
+ } else if (cat != TokenCategory.Comment && cat != TokenCategory.WhiteSpace) {
+ break;
+ }
+ }
+ int spacex = prevLineMeasurement.firstNonSpaceX;
+ if (lastOpChar == '{')
+ spacex = _content.nextTab(spacex);
+ dstring txt = _content.fillSpace(spacex);
+ EditOperation op2 = new EditOperation(EditAction.Replace, TextRange(TextPosition(line, 0), TextPosition(line, lineMeasurement.firstNonSpace >= 0 ? lineMeasurement.firstNonSpace : 0)), [txt]);
+ _opInProgress = true;
+ _content.performOperation(op2, source);
+ _opInProgress = false;
+ }
+ }
+
+ protected void applyClosingCurlySmartIndent(EditOperation op, Object source) {
+ int line = op.newRange.end.line;
+ TextPosition p2 = findPairedBracket(op.newRange.start);
+ if (p2 == op.newRange.start || p2.line > op.newRange.start.line)
+ return;
+ int prevLine = p2.line;
+ TextLineMeasure lineMeasurement = _content.measureLine(line);
+ TextLineMeasure prevLineMeasurement = _content.measureLine(prevLine);
+ if (lineMeasurement.firstNonSpace != op.newRange.start.pos)
+ return; // not in beginning of line
+ if (lineMeasurement.firstNonSpaceX >= 0 && lineMeasurement.firstNonSpaceX != prevLineMeasurement.firstNonSpaceX) {
+ dstring prevLineText = _content.line(prevLine);
+ TokenPropString prevLineTokenProps = _content.lineTokenProps(prevLine);
+ int spacex = prevLineMeasurement.firstNonSpaceX;
+ if (spacex != lineMeasurement.firstNonSpaceX) {
+ dstring txt = _content.fillSpace(spacex);
+ txt = txt ~ "}";
+ EditOperation op2 = new EditOperation(EditAction.Replace, TextRange(TextPosition(line, 0), TextPosition(line, lineMeasurement.firstNonSpace >= 0 ? lineMeasurement.firstNonSpace + 1 : 0)), [txt]);
+ _opInProgress = true;
+ _content.performOperation(op2, source);
+ _opInProgress = false;
+ }
+ }
+ }
+
+ /// apply smart indent, if supported
+ override void applySmartIndent(EditOperation op, Object source) {
+ if (_opInProgress)
+ return;
+ if (op.isInsertNewLine) {
+ // Enter key pressed - new line inserted or splitted
+ applyNewLineSmartIndent(op, source);
+ } else if (op.singleChar == '}') {
+ // } entered - probably need unindent
+ applyClosingCurlySmartIndent(op, source);
+ } else if (op.singleChar == '{') {
+ // { entered - probably need auto closing }
+ }
+ }
+
+}
+
diff --git a/src/dlangui/dml/parser.d b/src/dlangui/dml/parser.d
new file mode 100644
index 00000000..1baae9f5
--- /dev/null
+++ b/src/dlangui/dml/parser.d
@@ -0,0 +1,854 @@
+// Written in the D programming language.
+
+/**
+This module is DML (DlangUI Markup Language) parser - similar to QML in QtQuick
+
+Synopsis:
+
+----
+
+Widget layout = parseML(q{
+ VerticalLayout {
+ TextWidget { text: "Some label" }
+ TextLine { id: editor; text: "Some text to edit" }
+ Button { id: btnOk; text: "Ok" }
+ }
+}
+
+
+----
+
+Copyright: Vadim Lopatin, 2015
+License: Boost License 1.0
+Authors: Vadim Lopatin, coolreader.org@gmail.com
+ */
+module dlangui.dml.parser;
+
+import dlangui.core.linestream;
+import dlangui.core.collections;
+import dlangui.core.types;
+import dlangui.widgets.widget;
+import dlangui.widgets.metadata;
+import std.conv : to;
+import std.algorithm : equal, min, max;
+import std.utf : toUTF32, toUTF8;
+import std.array : join;
+public import dlangui.dml.annotations;
+
+class ParserException : Exception {
+ protected string _msg;
+ protected string _file;
+ protected int _line;
+ protected int _pos;
+
+ @property string file() { return _file; }
+ @property string msg() { return _msg; }
+ @property int line() { return _line; }
+ @property int pos() { return _pos; }
+
+ this(string msg, string file, int line, int pos) {
+ super(msg ~ " at " ~ file ~ " line " ~ to!string(line) ~ " column " ~ to!string(pos));
+ _msg = msg;
+ _file = file;
+ _line = line;
+ _pos = pos;
+ }
+}
+
+/// parser exception - unknown (unregistered) widget name
+class UnknownWidgetException : ParserException {
+ protected string _objectName;
+
+ @property string objectName() { return _objectName; }
+
+ this(string msg, string objectName, string file, int line, int pos) {
+ super(msg is null ? "Unknown widget name: " ~ objectName : msg, file, line, pos);
+ _objectName = objectName;
+ }
+}
+
+/// parser exception - unknown property for widget
+class UnknownPropertyException : UnknownWidgetException {
+ protected string _propName;
+
+ @property string propName() { return _propName; }
+
+ this(string msg, string objectName, string propName, string file, int line, int pos) {
+ super(msg is null ? "Unknown property " ~ objectName ~ "." ~ propName : msg, objectName, file, line, pos);
+ }
+}
+
+enum TokenType : ushort {
+ /// end of file
+ eof,
+ /// end of line
+ eol,
+ /// whitespace
+ whitespace,
+ /// string literal
+ str,
+ /// integer literal
+ integer,
+ /// floating point literal
+ floating,
+ /// comment
+ comment,
+ /// ident
+ ident,
+ /// error
+ error,
+ // operators
+ /// : operator
+ colon,
+ /// . operator
+ dot,
+ /// ; operator
+ semicolon,
+ /// , operator
+ comma,
+ /// - operator
+ minus,
+ /// + operator
+ plus,
+ /// [
+ curlyOpen,
+ /// ]
+ curlyClose,
+ /// (
+ open,
+ /// )
+ close,
+ /// [
+ squareOpen,
+ /// ]
+ squareClose,
+}
+
+struct Token {
+ TokenType type;
+ ushort line;
+ ushort pos;
+ bool multiline;
+ string text;
+ union {
+ int intvalue;
+ double floatvalue;
+ }
+ public @property string toString() {
+ if (type == TokenType.integer)
+ return "" ~ to!string(line) ~ ":" ~ to!string(pos) ~ " " ~ to!string(type) ~ " " ~ to!string(intvalue);
+ else if (type == TokenType.floating)
+ return "" ~ to!string(line) ~ ":" ~ to!string(pos) ~ " " ~ to!string(type) ~ " " ~ to!string(floatvalue);
+ else
+ return "" ~ to!string(line) ~ ":" ~ to!string(pos) ~ " " ~ to!string(type) ~ " \"" ~ text ~ "\"";
+ }
+ @property bool isMultilineComment() {
+ return type == TokenType.comment && multiline;
+ }
+}
+
+/// simple tokenizer for DlangUI ML
+class Tokenizer {
+ protected LineStream _lines;
+
+ dchar[] _lineText;
+ ushort _line;
+ ushort _pos;
+ int _len;
+ dchar _prevChar;
+ string _filename;
+
+ Token _token;
+
+ enum : int {
+ EOF_CHAR = 0x001A,
+ EOL_CHAR = 0x000A
+ };
+
+ this(string source, string filename = "") {
+ _filename = filename;
+ _lines = LineStream.create(source, filename);
+ _lineText = _lines.readLine();
+ _len = cast(int)_lineText.length;
+ _line = 0;
+ _pos = 0;
+ _prevChar = 0;
+ }
+
+ ~this() {
+ destroy(_lines);
+ _lines = null;
+ }
+
+ protected dchar peekChar() {
+ if (_pos < _len)
+ return _lineText[_pos];
+ else if (_lineText is null)
+ return EOF_CHAR;
+ return EOL_CHAR;
+ }
+
+ protected dchar peekNextChar() {
+ if (_pos < _len - 1)
+ return _lineText[_pos + 1];
+ else if (_lineText is null)
+ return EOF_CHAR;
+ return EOL_CHAR;
+ }
+
+ protected dchar nextChar() {
+ if (_pos < _len)
+ _prevChar = _lineText[_pos++];
+ else if (_lineText is null)
+ _prevChar = EOF_CHAR;
+ else {
+ _lineText = _lines.readLine();
+ _len = cast(int)_lineText.length;
+ _line++;
+ _pos = 0;
+ _prevChar = EOL_CHAR;
+ }
+ return _prevChar;
+ }
+
+ protected dchar skipChar() {
+ nextChar();
+ return peekChar();
+ }
+
+ protected void setTokenStart() {
+ _token.pos = _pos;
+ _token.line = _line;
+ _token.text = null;
+ _token.intvalue = 0;
+ }
+
+ protected ref const(Token) parseEof() {
+ _token.type = TokenType.eof;
+ return _token;
+ }
+
+ protected ref const(Token) parseEol() {
+ _token.type = TokenType.eol;
+ nextChar();
+ return _token;
+ }
+
+ protected ref const(Token) parseWhiteSpace() {
+ _token.type = TokenType.whitespace;
+ for(;;) {
+ dchar ch = skipChar();
+ if (ch != ' ' && ch != '\t')
+ break;
+ }
+ return _token;
+ }
+
+ static bool isAlpha(dchar ch) {
+ return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '_';
+ }
+
+ static bool isNum(dchar ch) {
+ return (ch >= '0' && ch <= '9');
+ }
+
+ static bool isAlphaNum(dchar ch) {
+ return isNum(ch) || isAlpha(ch);
+ }
+
+ private char[] _stringbuf;
+ protected ref const(Token) parseString() {
+ _token.type = TokenType.str;
+ //skipChar(); // skip "
+ bool lastBackslash = false;
+ _stringbuf.length = 0;
+ for(;;) {
+ dchar ch = skipChar();
+ if (ch == '\"') {
+ if (lastBackslash) {
+ _stringbuf ~= ch;
+ lastBackslash = false;
+ } else {
+ skipChar();
+ break;
+ }
+ } else if (ch == '\\') {
+ if (lastBackslash) {
+ _stringbuf ~= ch;
+ lastBackslash = false;
+ } else {
+ lastBackslash = true;
+ }
+ } else if (ch == EOL_CHAR) {
+ skipChar();
+ break;
+ } else if (lastBackslash) {
+ if (ch == 'n')
+ ch = '\n';
+ else if (ch == 't')
+ ch = '\t';
+ _stringbuf ~= ch;
+ lastBackslash = false;
+ } else {
+ _stringbuf ~= ch;
+ lastBackslash = false;
+ }
+ }
+ _token.text = _stringbuf.dup;
+ return _token;
+ }
+
+ protected ref const(Token) parseIdent() {
+ _token.type = TokenType.ident;
+ _stringbuf.length = 0;
+ _stringbuf ~= peekChar();
+ for(;;) {
+ dchar ch = skipChar();
+ if (!isAlphaNum(ch))
+ break;
+ _stringbuf ~= ch;
+ }
+ _token.text = _stringbuf.dup;
+ return _token;
+ }
+
+ protected ref const(Token) parseFloating(int n) {
+ _token.type = TokenType.floating;
+ dchar ch = peekChar();
+ // floating point
+ int div = 0;
+ int n2 = 0;
+ for (;;) {
+ ch = skipChar();
+ if (!isNum(ch))
+ break;
+ n2 = n2 * 10 + (ch - '0');
+ div++;
+ }
+ _token.floatvalue = cast(double)n + (div > 0 ? cast(double)n2 / div : 0.0);
+ string suffix;
+ if (ch == '%') {
+ suffix ~= ch;
+ ch = skipChar();
+ } else {
+ while (ch >= 'a' && ch <= 'z') {
+ suffix ~= ch;
+ ch = skipChar();
+ }
+ }
+ if (isAlphaNum(ch) || ch == '.')
+ return parseError();
+ _token.text = suffix;
+ return _token;
+ }
+
+ protected ref const(Token) parseNumber() {
+ dchar ch = peekChar();
+ uint n = ch - '0';
+ for(;;) {
+ ch = skipChar();
+ if (!isNum(ch))
+ break;
+ n = n * 10 + (ch - '0');
+ }
+ if (ch == '.')
+ return parseFloating(n);
+ string suffix;
+ if (ch == '%') {
+ suffix ~= ch;
+ ch = skipChar();
+ } else {
+ while (ch >= 'a' && ch <= 'z') {
+ suffix ~= ch;
+ ch = skipChar();
+ }
+ }
+ if (isAlphaNum(ch) || ch == '.')
+ return parseError();
+ _token.type = TokenType.integer;
+ _token.intvalue = n;
+ _token.text = suffix;
+ return _token;
+ }
+
+ protected ref const(Token) parseSingleLineComment() {
+ for(;;) {
+ dchar ch = skipChar();
+ if (ch == EOL_CHAR || ch == EOF_CHAR)
+ break;
+ }
+ _token.type = TokenType.comment;
+ _token.multiline = false;
+ return _token;
+ }
+
+ protected ref const(Token) parseMultiLineComment() {
+ skipChar();
+ for(;;) {
+ dchar ch = skipChar();
+ if (ch == '*' && peekNextChar() == '/') {
+ skipChar();
+ skipChar();
+ break;
+ }
+ if (ch == EOF_CHAR)
+ break;
+ }
+ _token.type = TokenType.comment;
+ _token.multiline = true;
+ return _token;
+ }
+
+ protected ref const(Token) parseError() {
+ _token.type = TokenType.error;
+ for(;;) {
+ dchar ch = skipChar();
+ if (ch == ' ' || ch == '\t' || ch == EOL_CHAR || ch == EOF_CHAR)
+ break;
+ }
+ return _token;
+ }
+
+ protected ref const(Token) parseOp(TokenType op) {
+ _token.type = op;
+ skipChar();
+ return _token;
+ }
+
+ /// get next token
+ ref const(Token) nextToken() {
+ setTokenStart();
+ dchar ch = peekChar();
+ if (ch == EOF_CHAR)
+ return parseEof();
+ if (ch == EOL_CHAR)
+ return parseEol();
+ if (ch == ' ' || ch == '\t')
+ return parseWhiteSpace();
+ if (ch == '\"')
+ return parseString();
+ if (isAlpha(ch))
+ return parseIdent();
+ if (isNum(ch))
+ return parseNumber();
+ if (ch == '.' && isNum(peekNextChar()))
+ return parseFloating(0);
+ if (ch == '/' && peekNextChar() == '/')
+ return parseSingleLineComment();
+ if (ch == '/' && peekNextChar() == '*')
+ return parseMultiLineComment();
+ switch (ch) {
+ case '.': return parseOp(TokenType.dot);
+ case ':': return parseOp(TokenType.colon);
+ case ';': return parseOp(TokenType.semicolon);
+ case ',': return parseOp(TokenType.comma);
+ case '-': return parseOp(TokenType.minus);
+ case '+': return parseOp(TokenType.plus);
+ case '{': return parseOp(TokenType.curlyOpen);
+ case '}': return parseOp(TokenType.curlyClose);
+ case '(': return parseOp(TokenType.open);
+ case ')': return parseOp(TokenType.close);
+ case '[': return parseOp(TokenType.squareOpen);
+ case ']': return parseOp(TokenType.squareClose);
+ default:
+ return parseError();
+ }
+ }
+
+ string getContextSource() {
+ string s = toUTF8(_lineText);
+ if (_pos == 0)
+ return " near `^^^" ~ s[0..min($,30)] ~ "`";
+ if (_pos >= _len)
+ return " near `" ~ s[max(_len - 30, 0) .. $] ~ "^^^`";
+ return " near `" ~ s[max(_pos - 15, 0) .. _pos] ~ "^^^" ~ s[_pos .. min(_pos + 15, $)] ~ "`";
+ }
+
+ void emitError(string msg) {
+ throw new ParserException(msg ~ getContextSource(), _filename, _token.line, _token.pos);
+ }
+
+ void emitUnknownPropertyError(string objectName, string propName) {
+ throw new UnknownPropertyException("Unknown property " ~ objectName ~ "." ~ propName ~ getContextSource(), objectName, propName, _filename, _token.line, _token.pos);
+ }
+
+ void emitUnknownObjectError(string objectName) {
+ throw new UnknownWidgetException("Unknown widget type " ~ objectName ~ getContextSource(), objectName, _filename, _token.line, _token.pos);
+ }
+
+ void emitError(string msg, ref const Token token) {
+ throw new ParserException(msg, _filename, token.line, token.pos);
+ }
+}
+
+class MLParser {
+ protected string _code;
+ protected string _filename;
+ protected bool _ownContext;
+ protected Widget _context;
+ protected Widget _currentWidget;
+ protected Tokenizer _tokenizer;
+ protected Collection!Widget _treeStack;
+
+ protected this(string code, string filename = "", Widget context = null) {
+ _code = code;
+ _filename = filename;
+ _context = context;
+ _tokenizer = new Tokenizer(code, filename);
+ }
+
+ protected Token _token;
+
+ /// move to next token
+ protected void nextToken() {
+ _token = _tokenizer.nextToken();
+ Log.d("parsed token: ", _token.type, " ", _token.line, ":", _token.pos, " ", _token.text);
+ }
+
+ /// throw exception if current token is eof
+ protected void checkNoEof() {
+ if (_token.type == TokenType.eof)
+ error("unexpected end of file");
+ }
+
+ /// move to next token, throw exception if eof
+ protected void nextTokenNoEof() {
+ nextToken();
+ checkNoEof();
+ }
+
+ protected void skipWhitespaceAndEols() {
+ for (;;) {
+ if (_token.type != TokenType.eol && _token.type != TokenType.whitespace && _token.type != TokenType.comment)
+ break;
+ nextToken();
+ }
+ if (_token.type == TokenType.error)
+ error("error while parsing ML code");
+ }
+
+ protected void skipWhitespaceAndEolsNoEof() {
+ skipWhitespaceAndEols();
+ checkNoEof();
+ }
+
+ protected void skipWhitespaceNoEof() {
+ skipWhitespace();
+ checkNoEof();
+ }
+
+ protected void skipWhitespace() {
+ for (;;) {
+ if (_token.type != TokenType.whitespace && _token.type != TokenType.comment)
+ break;
+ nextToken();
+ }
+ if (_token.type == TokenType.error)
+ error("error while parsing ML code");
+ }
+
+ protected void error(string msg) {
+ _tokenizer.emitError(msg);
+ }
+
+ protected void unknownObjectError(string objectName) {
+ _tokenizer.emitUnknownObjectError(objectName);
+ }
+
+ protected void unknownPropertyError(string objectName, string propName) {
+ _tokenizer.emitUnknownPropertyError(objectName, propName);
+ }
+
+ Widget createWidget(string name) {
+ auto metadata = findWidgetMetadata(name);
+ if (!metadata)
+ error("Cannot create widget " ~ name ~ " : unregistered widget class");
+ return metadata.create();
+ }
+
+ protected void createContext(string name) {
+ if (_context)
+ error("Context widget is already specified, but identifier " ~ name ~ " is found");
+ _context = createWidget(name);
+ _ownContext = true;
+ }
+
+ protected int applySuffix(int value, string suffix) {
+ if (suffix.length > 0) {
+ if (suffix.equal("px")) {
+ // do nothing, value is in px by default
+ } else if (suffix.equal("pt")) {
+ value = makePointSize(value);
+ } else if (suffix.equal("%")) {
+ value = makePercentSize(value);
+ } else
+ error("unknown number suffix: " ~ suffix);
+ }
+ return value;
+ }
+
+ protected void setIntProperty(string propName, int value, string suffix = null) {
+ value = applySuffix(value, suffix);
+ if (!_currentWidget.setIntProperty(propName, value))
+ error("unknown int property " ~ propName);
+ }
+
+ protected void setBoolProperty(string propName, bool value) {
+ if (!_currentWidget.setBoolProperty(propName, value))
+ error("unknown int property " ~ propName);
+ }
+
+ protected void setFloatProperty(string propName, double value) {
+ if (!_currentWidget.setDoubleProperty(propName, value))
+ error("unknown double property " ~ propName);
+ }
+
+ protected void setRectProperty(string propName, Rect value) {
+ if (!_currentWidget.setRectProperty(propName, value))
+ error("unknown Rect property " ~ propName);
+ }
+
+ protected void setStringProperty(string propName, string value) {
+ if (propName.equal("id") || propName.equal("styleId") || propName.equal("backgroundImageId")) {
+ if (!_currentWidget.setStringProperty(propName, value))
+ error("cannot set " ~ propName ~ " property for widget");
+ return;
+ }
+
+ dstring v = toUTF32(value);
+ if (!_currentWidget.setDstringProperty(propName, v)) {
+ if (!_currentWidget.setStringProperty(propName, value))
+ error("unknown string property " ~ propName);
+ }
+ }
+
+ protected void setIdentProperty(string propName, string value) {
+ if (propName.equal("id") || propName.equal("styleId") || propName.equal("backgroundImageId")) {
+ if (!_currentWidget.setStringProperty(propName, value))
+ error("cannot set id property for widget");
+ return;
+ }
+
+ if (value.equal("true"))
+ setBoolProperty(propName, true);
+ else if (value.equal("false"))
+ setBoolProperty(propName, false);
+ else if (value.equal("FILL") || value.equal("FILL_PARENT"))
+ setIntProperty(propName, FILL_PARENT);
+ else if (value.equal("WRAP") || value.equal("WRAP_CONTENT"))
+ setIntProperty(propName, WRAP_CONTENT);
+ else if (!_currentWidget.setStringProperty(propName, value))
+ error("unknown ident property " ~ propName);
+ }
+
+ protected void parseRectProperty(string propName) {
+ // current token is Rect
+ int[4] values = [0, 0, 0, 0];
+ nextToken();
+ skipWhitespaceAndEolsNoEof();
+ if (_token.type != TokenType.curlyOpen)
+ error("{ expected after Rect");
+ nextToken();
+ skipWhitespaceAndEolsNoEof();
+ int index = 0;
+ for (;;) {
+ if (_token.type == TokenType.curlyClose)
+ break;
+ if (_token.type == TokenType.integer) {
+ if (index >= 4)
+ error("too many values in Rect");
+ int n = applySuffix(_token.intvalue, _token.text);
+ values[index++] = n;
+ nextToken();
+ skipWhitespaceAndEolsNoEof();
+ if (_token.type == TokenType.comma || _token.type == TokenType.semicolon) {
+ nextToken();
+ skipWhitespaceAndEolsNoEof();
+ }
+ } else if (_token.type == TokenType.ident) {
+ string name = _token.text;
+ nextToken();
+ skipWhitespaceAndEolsNoEof();
+ if (_token.type != TokenType.colon)
+ error(": expected after property name " ~ name ~ " in Rect definition");
+ nextToken();
+ skipWhitespaceNoEof();
+ if (_token.type != TokenType.integer)
+ error("integer expected as Rect property value");
+ int n = applySuffix(_token.intvalue, _token.text);
+
+ if (name.equal("left"))
+ values[0] = n;
+ else if (name.equal("top"))
+ values[1] = n;
+ else if (name.equal("right"))
+ values[2] = n;
+ else if (name.equal("bottom"))
+ values[3] = n;
+ else
+ error("unknown property " ~ name ~ " in Rect");
+
+ nextToken();
+ skipWhitespaceNoEof();
+ if (_token.type == TokenType.comma || _token.type == TokenType.semicolon) {
+ nextToken();
+ skipWhitespaceAndEolsNoEof();
+ }
+ } else {
+ error("invalid Rect definition");
+ }
+
+ }
+ setRectProperty(propName, Rect(values[0], values[1], values[2], values[3]));
+ }
+
+ protected void parseProperty() {
+ if (_token.type != TokenType.ident)
+ error("identifier expected");
+ string propName = _token.text;
+ nextToken();
+ skipWhitespaceNoEof();
+ if (_token.type == TokenType.colon) { // :
+ nextTokenNoEof(); // skip :
+ skipWhitespaceNoEof();
+ if (_token.type == TokenType.integer)
+ setIntProperty(propName, _token.intvalue, _token.text);
+ else if (_token.type == TokenType.minus || _token.type == TokenType.plus) {
+ int sign = _token.type == TokenType.minus ? -1 : 1;
+ nextTokenNoEof(); // skip :
+ skipWhitespaceNoEof();
+ if (_token.type == TokenType.integer) {
+ setIntProperty(propName, _token.intvalue * sign, _token.text);
+ } else if (_token.type == TokenType.floating) {
+ setFloatProperty(propName, _token.floatvalue * sign);
+ } else
+ error("number expected after + and -");
+ } else if (_token.type == TokenType.floating)
+ setFloatProperty(propName, _token.floatvalue);
+ else if (_token.type == TokenType.str)
+ setStringProperty(propName, _token.text);
+ else if (_token.type == TokenType.ident) {
+ if (_token.text.equal("Rect")) {
+ parseRectProperty(propName);
+ } else {
+ setIdentProperty(propName, _token.text);
+ }
+ } else
+ error("int, float, string or identifier are expected as property value");
+ nextTokenNoEof();
+ skipWhitespaceNoEof();
+ if (_token.type == TokenType.semicolon) {
+ // separated by ;
+ nextTokenNoEof();
+ skipWhitespaceAndEolsNoEof();
+ return;
+ } else if (_token.type == TokenType.eol) {
+ nextTokenNoEof();
+ skipWhitespaceAndEolsNoEof();
+ return;
+ } else if (_token.type == TokenType.curlyClose) {
+ // it was last property in object
+ return;
+ }
+ error("; eol or } expected after property definition");
+ } else if (_token.type == TokenType.curlyOpen) { // { -- start of object
+ Widget s = createWidget(propName);
+ parseWidgetProperties(s);
+ } else {
+ error(": or { expected after identifier");
+ }
+
+ }
+
+ protected void parseWidgetProperties(Widget w) {
+ if (_token.type != TokenType.curlyOpen) // {
+ error("{ is expected");
+ _treeStack.pushBack(w);
+ if (_currentWidget)
+ _currentWidget.addChild(w);
+ _currentWidget = w;
+ nextToken(); // skip {
+ skipWhitespaceAndEols();
+ for (;;) {
+ checkNoEof();
+ if (_token.type == TokenType.curlyClose) // end of object's internals
+ break;
+ parseProperty();
+ }
+ if (_token.type != TokenType.curlyClose) // {
+ error("{ is expected");
+ nextToken(); // skip }
+ skipWhitespaceAndEols();
+ _treeStack.popBack();
+ _currentWidget = _treeStack.peekBack();
+ }
+
+ protected Widget parse() {
+ try {
+ nextToken();
+ skipWhitespaceAndEols();
+ if (_token.type == TokenType.ident) {
+ createContext(_token.text);
+ nextToken();
+ skipWhitespaceAndEols();
+ }
+ if (_token.type != TokenType.curlyOpen) // {
+ error("{ is expected");
+ if (!_context)
+ error("No context widget is specified!");
+ parseWidgetProperties(_context);
+
+ skipWhitespaceAndEols();
+ if (_token.type != TokenType.eof) // {
+ error("end of file expected");
+ return _context;
+ } catch (Exception e) {
+ Log.e("exception while parsing ML", e);
+ if (_context && _ownContext)
+ destroy(_context);
+ _context = null;
+ throw e;
+ }
+ }
+
+ ~this() {
+ destroy(_tokenizer);
+ _tokenizer = null;
+ }
+
+}
+
+
+/// Parse DlangUI ML code
+public Widget parseML(T = Widget)(string code, string filename = "", Widget context = null) {
+ MLParser parser = new MLParser(code, filename);
+ scope(exit) destroy(parser);
+ Widget w = parser.parse();
+ T res = cast(T) w;
+ if (w && !res && !context) {
+ destroy(w);
+ throw new ParserException("Cannot convert parsed widget to " ~ T.stringof, "", 0, 0);
+ }
+ return res;
+}
+
+/// tokenize source into array of tokens (excluding EOF)
+public Token[] tokenizeML(const(dstring[]) lines) {
+ string code = toUTF8(join(lines, "\n"));
+ return tokenizeML(code);
+}
+
+/// tokenize source into array of tokens (excluding EOF)
+public Token[] tokenizeML(string code) {
+ Token[] res;
+ auto tokenizer = new Tokenizer(code, "");
+ for (;;) {
+ auto token = tokenizer.nextToken();
+ if (token.type == TokenType.eof)
+ break;
+ res ~= token;
+ }
+ return res;
+}
+
+//pragma(msg, tokenizeML("Widget {}"));
diff --git a/src/dlangui/widgets/widget.d b/src/dlangui/widgets/widget.d
index fc0e52b0..2f0b58db 100644
--- a/src/dlangui/widgets/widget.d
+++ b/src/dlangui/widgets/widget.d
@@ -49,6 +49,7 @@ public import dlangui.graphics.colors;
public import dlangui.core.signals;
public import dlangui.platforms.common.platform;
+public import dlangui.dml.annotations;
import std.algorithm;
@@ -134,6 +135,7 @@ enum CursorType {
* Base class for all widgets.
*
*/
+@dmlwidget
class Widget {
/// widget id
protected string _id;