diff --git a/.gitignore b/.gitignore index c6b1e37..727b13c 100644 --- a/.gitignore +++ b/.gitignore @@ -27,5 +27,6 @@ Thumbs.db Debug Release ipch +* gitignore documentation diff --git a/Demo/VisualStudio/Demo.vcxproj b/Demo/VisualStudio/Demo.vcxproj index 47e443e..21e54d4 100644 --- a/Demo/VisualStudio/Demo.vcxproj +++ b/Demo/VisualStudio/Demo.vcxproj @@ -123,6 +123,7 @@ <ClCompile Include="..\..\tinkerbell\src\tb_window.cpp" /> <ClCompile Include="..\..\tinkerbell\src\tests\tb_test.cpp" /> <ClCompile Include="..\..\tinkerbell\src\tests\test_tb_linklist.cpp" /> + <ClCompile Include="..\..\tinkerbell\src\tests\test_tb_parser.cpp" /> <ClCompile Include="..\..\tinkerbell\src\tests\test_tb_space_allocator.cpp" /> <ClCompile Include="..\..\tinkerbell\src\tests\test_tb_style_edit.cpp" /> <ClCompile Include="..\..\tinkerbell\src\tests\test_tb_tempbuffer.cpp" /> diff --git a/Demo/VisualStudio/Demo.vcxproj.filters b/Demo/VisualStudio/Demo.vcxproj.filters index aa8a7e4..21df8cc 100644 --- a/Demo/VisualStudio/Demo.vcxproj.filters +++ b/Demo/VisualStudio/Demo.vcxproj.filters @@ -189,6 +189,9 @@ <ClCompile Include="..\..\tinkerbell\src\tests\test_tb_tempbuffer.cpp"> <Filter>Source Files\tinkerbell\tests</Filter> </ClCompile> + <ClCompile Include="..\..\tinkerbell\src\tests\test_tb_parser.cpp"> + <Filter>Source Files\tinkerbell\tests</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <Library Include="..\freeglut.lib"> diff --git a/Demo/ui_resources/test_textwindow.tb.txt b/Demo/ui_resources/test_textwindow.tb.txt index 85f8989..60ba0ce 100644 --- a/Demo/ui_resources/test_textwindow.tb.txt +++ b/Demo/ui_resources/test_textwindow.tb.txt @@ -18,4 +18,15 @@ TBLayout: axis: y, distribution: available TBTextField: text: Menu skin TBButton.flat TBEditField: multiline: 1, styling: 1, gravity: all, id: editfield, autofocus: 1 - text <u>Tinkerbell UI Toolkit!</u>\n\nThings to test:\n • resizing\n • right clicking\n • searching\n • keyboard navigation\n • scrolling\n • mouse wheel & dragging.\n • This is a list item that should wrap in a smart way.\n • Embedded content in textfield, such as this button: <widget TBButton: text: ":)">\n • <color #f44>Red</color>, <color #6f6>Green</color>, <color #aaf>Blue</color> + text: "Tinkerbell UI Toolkit\n\n" \ + "<color #0794f8>Test zone</color>\n" \ + "The menu to the left spawns some examples of what tinkerbell can do.\n\n" \ + "The code in Demo/* is more like a developers test zone than organized tutorial-like samples. " \ + "This would be good to fix of course (help is welcome! ;)\n\n" \ + "<color #0794f8>Some things to try out</color>\n" \ + " • All layouts provide panning of content automatically when squashed below the minimal size, so try resizing windows and pan.\n" \ + " • Lines starting with a bullet sequence (like this one) should wrap in a smart way.\n" \ + " • <u>Underline</u>, <color #f44>Red</color>, <color #6f6>Green</color>, <color #aaf>Blue</color>\n\n" \ + "<color #0794f8>Good to know</color>\n" \ + " • The text component you're reading in handles styling and embedded content (such as this widget: <widget TBButton: text: ':)'>), but does not handle editing of those fully. Undo/Redo should work fine, but caret movement and position updates doesn't.\n" \ + " • All resources are UTF-8." diff --git a/Makefile b/Makefile index c40ab8f..b96b3c7 100644 --- a/Makefile +++ b/Makefile @@ -65,6 +65,7 @@ SRC = tinkerbell/src/tb_layout.cpp \ tinkerbell/src/tests/test_tb_space_allocator.cpp \ tinkerbell/src/tests/test_tb_widget_value.cpp \ tinkerbell/src/tests/test_tb_linklist.cpp \ + tinkerbell/src/tests/test_tb_parser.cpp \ tinkerbell/src/tests/test_tb_tempbuffer.cpp \ tinkerbell/src/tests/test_tb_test.cpp \ stb_image/tb_image_loader_stb.cpp \ diff --git a/tinkerbell/src/parser/TBParser.cpp b/tinkerbell/src/parser/TBParser.cpp index 3d73040..a43d4f6 100644 --- a/tinkerbell/src/parser/TBParser.cpp +++ b/tinkerbell/src/parser/TBParser.cpp @@ -9,6 +9,65 @@ namespace tinkerbell { +// == Util functions ==================================================================== + +void UnescapeString(char *str) +{ + char *dst = str, *src = str; + while (*src) + { + if (*src == '\\') + { + bool code_found = true; + if (src[1] == 'n') + *dst = '\n'; + else if (src[1] == 'r') + *dst = '\r'; + else if (src[1] == 't') + *dst = '\t'; + else if (src[1] == '\"') + *dst = '\"'; + else if (src[1] == '\'') + *dst = '\''; + else if (src[1] == '\\') + *dst = '\\'; + else + code_found = false; + if (code_found) + { + src += 2; + dst++; + continue; + } + } + *dst = *src; + dst++; + src++; + } + *dst = 0; +} + +bool is_white_space(const char *str) +{ + switch (*str) + { + case ' ': + case '\t': + return true; + default: + return false; + } +} + +bool is_pending_multiline(const char *str) +{ + while (is_white_space(str)) + str++; + return str[0] == '\\' && str[1] == 0; +} + +// == Parser ============================================================================ + Parser::STATUS Parser::Read(ParserStream *stream, ParserTarget *target) { TBTempBuffer line, work; @@ -17,6 +76,8 @@ Parser::STATUS Parser::Read(ParserStream *stream, ParserTarget *target) current_indent = 0; current_line_nr = 1; + pending_multiline = false; + multi_line_sub_level = 0; while (int read_len = stream->GetMoreData((char *)work.GetData(), work.GetCapacity())) { @@ -83,42 +144,6 @@ Parser::STATUS Parser::Read(ParserStream *stream, ParserTarget *target) return STATUS_OK; } -void UnescapeString(char *str) -{ - char *dst = str, *src = str; - while (*src) - { - if (*src == '\\') - { - bool code_found = true; - if (src[1] == 'n') - *dst = '\n'; - else if (src[1] == 'r') - *dst = '\r'; - else if (src[1] == 't') - *dst = '\t'; - else if (src[1] == '\"') - *dst = '\"'; - else if (src[1] == '\'') - *dst = '\''; - else if (src[1] == '\\') - *dst = '\\'; - else - code_found = false; - if (code_found) - { - src += 2; - dst++; - continue; - } - } - *dst = *src; - dst++; - src++; - } - *dst = 0; -} - void Parser::OnLine(char *line, ParserTarget *target) { if (*line == '#') @@ -126,6 +151,11 @@ void Parser::OnLine(char *line, ParserTarget *target) target->OnComment(line + 1); return; } + if (pending_multiline) + { + OnMultiline(line, target); + return; + } // Check indent int indent = 0; @@ -161,11 +191,11 @@ void Parser::OnLine(char *line, ParserTarget *target) { char *token = line; // Read line while consuming it and copy over to token buf - while (*line != ' ' && *line != 0) + while (!is_white_space(line) && *line != 0) line++; int token_len = line - token; // Consume any white space after the token - while (*line == ' ') + while (is_white_space(line)) line++; bool is_compact_line = token_len && token[token_len - 1] == ':'; @@ -175,6 +205,19 @@ void Parser::OnLine(char *line, ParserTarget *target) { token_len--; token[token_len] = 0; + + // Check if the first argument is not a child but the value for this token + if (is_number(line) || *line == '[' || *line == '\"' || *line == '\'' || *line == '@') + { + ConsumeValue(value, line); + + if (pending_multiline) + { + // The value wrapped to the next line, so we should remember the token and continue. + multi_line_token.Set(token); + return; + } + } } else if (token[token_len]) { @@ -192,9 +235,9 @@ void Parser::OnLine(char *line, ParserTarget *target) /** Check if buf is pointing at a end quote. It may need to iterate buf backwards toward buf_start to check if any preceding backslashes make it a escaped quote (which should not be the end quote) */ -bool IsEndQuote(const char *buf_start, const char *buf) +bool IsEndQuote(const char *buf_start, const char *buf, const char quote_type) { - if (*buf != '\"') + if (*buf != quote_type) return false; int num_backslashes = 0; while (buf_start < buf && *(buf-- - 1) == '\\') @@ -208,7 +251,7 @@ void Parser::OnCompactLine(char *line, ParserTarget *target) while (*line) { // consume any whitespace - while (*line == ' ') + while (is_white_space(line)) line++; // Find token @@ -220,46 +263,19 @@ void Parser::OnCompactLine(char *line, ParserTarget *target) *line++ = 0; // consume any whitespace - while (*line == ' ') + while (is_white_space(line)) line++; TBValue v; + ConsumeValue(v, line); - // Find value (As quoted string, or as auto) - char *value = line; - if (*line == '\"') + if (pending_multiline) { - // Consume starting quote - line++; - value++; - // Find ending quote or end - while (!IsEndQuote(value, line) && *line != 0) - line++; - // Terminate away the quote - if (*line == '\"') - *line++ = 0; - - // consume any whitespace - while (*line == ' ') - line++; - // consume any comma - if (*line == ',') - line++; - - UnescapeString(value); - v.SetString(value, TBValue::SET_AS_STATIC); - } - else - { - // Find next comma or end - while (*line != ',' && *line != 0) - line++; - // Terminate away the comma - if (*line == ',') - *line++ = 0; - - UnescapeString(value); - v.SetFromStringAuto(value, TBValue::SET_AS_STATIC); + // The value wrapped to the next line, so we should remember the token and continue. + multi_line_token.Set(token); + // Since we need to call target->Leave when the multiline is ready, set multi_line_sub_level. + multi_line_sub_level = 1; + return; } // Ready @@ -269,4 +285,77 @@ void Parser::OnCompactLine(char *line, ParserTarget *target) target->Leave(); } +void Parser::OnMultiline(char *line, ParserTarget *target) +{ + // consume any whitespace + while (is_white_space(line)) + line++; + + TBValue value; + ConsumeValue(value, line); + + if (!pending_multiline) + { + // Ready with all lines + value.SetString(multi_line_value.GetData(), TBValue::SET_AS_STATIC); + target->OnToken(multi_line_token, value); + + if (multi_line_sub_level) + target->Leave(); + + // Reset + multi_line_value.SetAppendPos(0); + multi_line_sub_level = 0; + } +} + +void Parser::ConsumeValue(TBValue &dst_value, char *&line) +{ + // Find value (As quoted string, or as auto) + char *value = line; + if (*line == '\"' || *line == '\'') + { + const char quote_type = *line; + // Consume starting quote + line++; + value++; + // Find ending quote or end + while (!IsEndQuote(value, line, quote_type) && *line != 0) + line++; + // Terminate away the quote + if (*line == quote_type) + *line++ = 0; + + // consume any whitespace + while (is_white_space(line)) + line++; + // consume any comma + if (*line == ',') + line++; + + UnescapeString(value); + dst_value.SetString(value, TBValue::SET_AS_STATIC); + } + else + { + // Find next comma or end + while (*line != ',' && *line != 0) + line++; + // Terminate away the comma + if (*line == ',') + *line++ = 0; + + UnescapeString(value); + dst_value.SetFromStringAuto(value, TBValue::SET_AS_STATIC); + } + + // Check if we still have pending value data on the following line and set pending_multiline. + bool continuing_multiline = pending_multiline; + pending_multiline = is_pending_multiline(line); + + // Append the multi line value to the buffer. + if (continuing_multiline || pending_multiline) + multi_line_value.AppendString(dst_value.GetString()); +} + }; // namespace tinkerbell diff --git a/tinkerbell/src/parser/TBParser.h b/tinkerbell/src/parser/TBParser.h index 77df617..6dde56a 100644 --- a/tinkerbell/src/parser/TBParser.h +++ b/tinkerbell/src/parser/TBParser.h @@ -7,6 +7,7 @@ #define TBParser_H #include "tb_value.h" +#include "tb_tempbuffer.h" namespace tinkerbell { @@ -41,8 +42,14 @@ public: private: int current_indent; int current_line_nr; + TBStr multi_line_token; + TBTempBuffer multi_line_value; + int multi_line_sub_level; + bool pending_multiline; void OnLine(char *line, ParserTarget *target); void OnCompactLine(char *line, ParserTarget *target); + void OnMultiline(char *line, ParserTarget *target); + void ConsumeValue(TBValue &dst_value, char *&line); }; }; // namespace tinkerbell diff --git a/tinkerbell/src/tb_value.cpp b/tinkerbell/src/tb_value.cpp index c89e837..27863c3 100644 --- a/tinkerbell/src/tb_value.cpp +++ b/tinkerbell/src/tb_value.cpp @@ -19,14 +19,14 @@ namespace tinkerbell { // == Helper functions ============================ -bool p_is_number(const char *str) +bool is_number(const char *str) { - if (*str == '-') + if (*str == '-' || *str == '.') str++; return *str >= '0' && *str <= '9'; } -bool p_is_number_float(const char *str) +bool is_number_float(const char *str) { while (*str) if (*str++ == '.') return true; return false; @@ -215,7 +215,7 @@ void TBValue::SetFromStringAuto(const char *str, SET set) { if (!str) SetNull(); - else if (p_is_number(str)) + else if (is_number(str)) { // If the number has spaces, we'll assume a list of numbers (example: "10 -4 3.5") if (strstr(str, " ")) @@ -238,7 +238,7 @@ void TBValue::SetFromStringAuto(const char *str, SET set) SetArray(arr, SET_TAKE_OWNERSHIP); } } - else if (p_is_number_float(str)) + else if (is_number_float(str)) SetFloat((float)atof(str)); else SetInt(atoi(str)); diff --git a/tinkerbell/src/tb_value.h b/tinkerbell/src/tb_value.h index 9c57a1e..69036ff 100644 --- a/tinkerbell/src/tb_value.h +++ b/tinkerbell/src/tb_value.h @@ -13,6 +13,13 @@ namespace tinkerbell { class TBValue; +/** Return true if the given string starts with a number. */ +bool is_number(const char *str); + +/** Return true if the given number string is a float number. + Should only be called when you've verified it's a number with is_number(). */ +bool is_number_float(const char *str); + /** TBValueArray is a array of TBValue */ class TBValueArray { diff --git a/tinkerbell/src/tests/test_tb_parser.cpp b/tinkerbell/src/tests/test_tb_parser.cpp new file mode 100644 index 0000000..db2bd1e --- /dev/null +++ b/tinkerbell/src/tests/test_tb_parser.cpp @@ -0,0 +1,128 @@ +// ================================================================================ +// == This file is a part of Tinkerbell UI Toolkit. (C) 2011-2012, Emil Seger�s == +// == See tinkerbell.h for more information. == +// ================================================================================ + +#include "tb_test.h" +#include "parser/TBNodeTree.h" + +#ifdef TB_UNIT_TESTING + +using namespace tinkerbell; + +TB_TEST_GROUP(tb_parser) +{ + TBNode node; + TB_TEST(Init) + { + TB_VERIFY(node.ReadFile(TB_TEST_FILE("test_tb_parser.tb.txt"))); + } + + TB_TEST(strings) + { + TB_VERIFY_STR(node.GetValueString("strings>string1", ""), "A string"); + TB_VERIFY_STR(node.GetValueString("strings>string2", ""), "\"A string\""); + TB_VERIFY_STR(node.GetValueString("strings>string3", ""), "\'A string\'"); + TB_VERIFY_STR(node.GetValueString("strings>string4", ""), "\"\'A string\'\""); + TB_VERIFY_STR(node.GetValueString("strings>string5", ""), "Foo\nBar"); + } + + TB_TEST(strings_compact) + { + TB_VERIFY_STR(node.GetValueString("strings_compact>string1", ""), ""); + TB_VERIFY_STR(node.GetValueString("strings_compact>string2", ""), "A string"); + TB_VERIFY_STR(node.GetValueString("strings_compact>string3", ""), "A string"); + TB_VERIFY_STR(node.GetValueString("strings_compact>string4", ""), "'A string'"); + TB_VERIFY_STR(node.GetValueString("strings_compact>string5", ""), "\"A string\""); + TB_VERIFY_STR(node.GetValueString("strings_compact>string6", ""), "\"A string\""); + TB_VERIFY_STR(node.GetValueString("strings_compact>string7", ""), "\\"); + TB_VERIFY_STR(node.GetValueString("strings_compact>string8", ""), "\""); + TB_VERIFY_STR(node.GetValueString("strings_compact>string9", ""), "\\\\\\\\"); + TB_VERIFY_STR(node.GetValueString("strings_compact>string10", ""), "\\\\\""); + TB_VERIFY_STR(node.GetValueString("strings_compact>string11", ""), "\"\"\'\'"); + TB_VERIFY_STR(node.GetValueString("strings_compact>string12", ""), "@language_string_token"); + } + + TB_TEST(numbers_compact) + { + TB_VERIFY(node.GetValueInt("numbers_compact>integer", 0) == -10); + TB_VERIFY_FLOAT(node.GetValueFloat("numbers_compact>float", 0), 1.0); + } + + TB_TEST(compact_with_children) + { + TB_VERIFY_STR(node.GetValueString("compact_with_children>string", ""), "A string"); + TB_VERIFY_STR(node.GetValueString("compact_with_children>string>child1", ""), "Child 1"); + TB_VERIFY_STR(node.GetValueString("compact_with_children>string>child2", ""), "Child 2"); + + TB_VERIFY(node.GetValueInt("compact_with_children>integer", 0) == -10); + TB_VERIFY(node.GetValueInt("compact_with_children>integer>child1", 0) == 1); + TB_VERIFY(node.GetValueInt("compact_with_children>integer>child2", 0) == 2); + + TB_VERIFY_FLOAT(node.GetValueFloat("compact_with_children>float", 0), 1); + TB_VERIFY_FLOAT(node.GetValueFloat("compact_with_children>float>child1", 0), 1); + TB_VERIFY_FLOAT(node.GetValueFloat("compact_with_children>float>child2", 0), 2); + } + + TB_TEST(compact_no_value) + { + TB_VERIFY_STR(node.GetValueString("compact_no_value>string", ""), "A string"); + TB_VERIFY(node.GetValueInt("compact_no_value>int", 0) == 42); + TB_VERIFY_FLOAT(node.GetValueFloat("compact_no_value>float", 0), 3.14); + TB_VERIFY_STR(node.GetValueString("compact_no_value>subgroup>string1", ""), "A string, with \"comma\""); + TB_VERIFY_STR(node.GetValueString("compact_no_value>subgroup>string2", ""), "'Another string'"); + TB_VERIFY_STR(node.GetValueString("compact_no_value>subgroup>string3", ""), "And another string"); + } + + TB_TEST(arrays_numbers) + { + TBNode *arr_n = node.GetNode("arrays>numbers"); + TB_VERIFY(arr_n); + TB_VERIFY(arr_n->GetValue().GetArrayLength() == 5); + TBValueArray *arr = arr_n->GetValue().GetArray(); + TB_VERIFY(arr->GetValue(0)->GetInt() == 1); + TB_VERIFY(arr->GetValue(1)->GetInt() == 2); + TB_VERIFY_FLOAT(arr->GetValue(2)->GetFloat(), 0.5); + TB_VERIFY_FLOAT(arr->GetValue(3)->GetFloat(), 1.0E-8); + TB_VERIFY(arr->GetValue(4)->GetInt() == 1000000000); + } + +//FIX: Not supported yet +// TB_TEST(arrays_strings) +// { +// TBNode *arr_n = node.GetNode("arrays>strings"); +// TB_VERIFY(arr_n); +// TB_VERIFY(arr_n->GetValue().GetArrayLength() == 5); +// TBValueArray *arr = arr_n->GetValue().GetArray(); +// TB_VERIFY_STR(arr->GetValue(0)->GetString(), "Foo"); +// TB_VERIFY_STR(arr->GetValue(1)->GetString(), "'Foo'"); +// TB_VERIFY_STR(arr->GetValue(2)->GetString(), "Foo"); +// TB_VERIFY_STR(arr->GetValue(3)->GetString(), "\"Foo\""); +// TB_VERIFY_STR(arr->GetValue(4)->GetString(), "Foo 'bar'"); +// } +// +// TB_TEST(arrays_mixed) +// { +// TBNode *arr_n = node.GetNode("arrays>mixed"); +// TB_VERIFY(arr_n); +// TB_VERIFY(arr_n->GetValue().GetArrayLength() == 4); +// TBValueArray *arr = arr_n->GetValue().GetArray(); +// TB_VERIFY_STR(arr->GetValue(0)->GetString(), "Foo"); +// TB_VERIFY(arr->GetValue(1)->GetInt() == 2); +// TB_VERIFY_STR(arr->GetValue(2)->GetString(), "bar"); +// TB_VERIFY(arr->GetValue(3)->GetFloat() == 4.0f); +// } + + TB_TEST(strings_multiline) + { + TB_VERIFY_STR(node.GetValueString("strings_multiline>string1", ""), "Line 1\nLine 2\nLine 3"); + TB_VERIFY_STR(node.GetValueString("strings_multiline>string2", ""), "abc"); + TB_VERIFY_STR(node.GetValueString("strings_multiline>string3", ""), "AB"); + TB_VERIFY_STR(node.GetValueString("strings_multiline>string4", ""), "Line 1\nLine 2\nLine 3\n"); + TB_VERIFY_STR(node.GetValueString("strings_multiline>subgroup>first", ""), "Foo"); + TB_VERIFY_STR(node.GetValueString("strings_multiline>subgroup>second", ""), "AB"); + TB_VERIFY_STR(node.GetValueString("strings_multiline>string5", ""), "The last string"); + } +} + +#endif // TB_UNIT_TESTING diff --git a/tinkerbell/src/tests/test_tb_parser.tb.txt b/tinkerbell/src/tests/test_tb_parser.tb.txt new file mode 100644 index 0000000..5043048 --- /dev/null +++ b/tinkerbell/src/tests/test_tb_parser.tb.txt @@ -0,0 +1,64 @@ + +# Test file for the text node system and its parser +# Everything is a tree of nodes with a name and value. +# The values may be strings, numbers or arrays. + +# If there is no colon, the entire line will be treated as a value and no quotes is needed around strings. +# Any quotes will be part of the string. +strings + string1 A string + string2 "A string" + string3 'A string' + string4 "'A string'" + string5 Foo\nBar + +# If there is a colon (compact mode), there must be quotes around strings. +# First comes the value (optional) +# Then comes other child nodes (separated by comma) +strings_compact + string1: this should fail + string2: "A string" + string3: 'A string' + string4: "'A string'" + string5: '"A string"' + string6: "\"A string\"" + string7: "\\" + string8: "\"" + string9: "\\\\\\\\" + string10: "\\\\\"" + string11: "\"\"\'\'" + string12: @language_string_token + +numbers_compact + integer: -10 + float: 1.0 + +compact_with_children + string: "A string", child1: "Child 1", child2: "Child 2" + integer: -10, child1: 1, child2: 2 + float: 1.0, child1: 1.0, child2: 2.0 + +compact_no_value: string: "A string", int: 42, float: 3.14 + subgroup: string1: "A string, with \"comma\"", string2: "'Another string'" + string3: "And another string" + +arrays + numbers 1 2 .5 1.0E-8 1000000000 +# Not supported yet +# strings: "Foo" "'Foo'" 'Foo' '"Foo"' "Foo 'bar'" +# mixed: "foo" 2 "bar" 4.0 + +# Strings can span over multiple lines by ending with a \ +strings_multiline + string1: "Line 1\nLine 2\nLine 3" + string2: "a" \ + "b" \ + "c" + string3: 'A' \ + 'B' + string4: 'Line 1\n' \ + 'Line 2\n'\ + 'Line 3\n' + subgroup: first: "Foo", second: "A" \ + "B" + string5: "The last string" diff --git a/stb_font/vera.ttf b/vera.ttf similarity index 100% rename from stb_font/vera.ttf rename to vera.ttf