Skip to content

Commit

Permalink
Refactor Text Icons #2 - Self-Closing Decorators
Browse files Browse the repository at this point in the history
- Support {decorator/} as a shorthand for {decorator}{/decorator}.
- Update Text_Parser to handle self-closing tokens and add tests for it.
  • Loading branch information
firas-assaad committed May 14, 2021
1 parent de3992e commit d47b215
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 39 deletions.
1 change: 1 addition & 0 deletions include/text_parser.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ struct Token {
bool unmatched = false;
int start_index = 0;
int end_index = 0;
bool self_closing = false;
std::string to_string() const;
Token to_opening_token(const std::string& val = "") const;
Token to_closing_token() const;
Expand Down
1 change: 1 addition & 0 deletions src/scripting_interface.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -936,6 +936,7 @@ void Scripting_Interface::setup_scripts() {
token_type["unmatched"] = sol::readonly(&Token::unmatched);
token_type["start_index"] = sol::readonly(&Token::start_index);
token_type["end_index"] = sol::readonly(&Token::end_index);
token_type["self_closing"] = sol::readonly(&Token::self_closing);

// Text parser
auto parser_type = lua.new_usertype<Text_Parser>("Text_Parser",
Expand Down
18 changes: 17 additions & 1 deletion src/tests/text_parser_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ namespace detail {
BOOST_CHECK_EQUAL(actual.unmatched, expected.unmatched);
BOOST_CHECK_EQUAL(actual.start_index, expected.start_index);
BOOST_CHECK_EQUAL(actual.end_index, expected.end_index);
BOOST_CHECK_EQUAL(actual.self_closing, expected.self_closing);
}

void validate_tokens(const Text_Parser& parser, const std::string& text, std::vector<Token> expectedTokens) {
Expand All @@ -38,14 +39,15 @@ namespace detail {
}
}

Token build_token(Token_Type type, std::string tag, std::string value, bool unmatched, int start, int end) {
Token build_token(Token_Type type, std::string tag, std::string value, bool unmatched, int start, int end, bool self_closing = false) {
Token token;
token.type = type;
token.tag = tag;
token.value = value;
token.unmatched = unmatched;
token.start_index = start;
token.end_index = end;
token.self_closing = self_closing;
return token;
}
}
Expand Down Expand Up @@ -182,6 +184,20 @@ BOOST_AUTO_TEST_CASE(text_parser_parses_simple_tag_with_empty_text) {
detail::validate_tokens(parser, "{b}{/b}", tokens);
}

BOOST_AUTO_TEST_CASE(text_parser_parses_self_closing_tags) {
Text_Parser parser;

std::vector<Token> tokens = {
detail::build_token(Token_Type::OPENING_TAG, "b", "", false, 0, 3, true),
detail::build_token(Token_Type::CLOSING_TAG, "b", "", false, 3, 3, true),
detail::build_token(Token_Type::TEXT, "", "x", false, 4, 4),
detail::build_token(Token_Type::OPENING_TAG, "c", "1", false, 5, 10, true),
detail::build_token(Token_Type::CLOSING_TAG, "c", "", false, 10, 10, true)
};

detail::validate_tokens(parser, "{b/}x{c=1/}", tokens);
}

BOOST_AUTO_TEST_CASE(text_parser_parses_multiple_tags) {
Text_Parser parser;

Expand Down
98 changes: 64 additions & 34 deletions src/text_parser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ std::vector<Token> Text_Parser::parse(const std::string& text, bool permissive)
// Handle tags
auto next_char = start + 1 != end ? *(start + 1) : '_';
if (*start == '{' && next_char != '{') {
Token tag_token;
Token tag_token, close_token;
tag_token.unmatched = false;
tag_token.start_index = utf8::distance(text.begin(), start);
start++;
Expand All @@ -85,6 +85,47 @@ std::vector<Token> Text_Parser::parse(const std::string& text, bool permissive)
std::string value;
bool has_value = false;
bool error = false;

auto match_closing_tag = [&unmatched_tokens, &tokens](Token& tag_token) {
if (unmatched_tokens[tag_token.tag].empty()) {
// A closing tag with no matched open tag
tag_token.unmatched = true;
} else {
auto& opening_token = tokens[unmatched_tokens[tag_token.tag].back()];
// Found the closing tag for an unmatched open tag
if (opening_token.start_index < tag_token.start_index) {
opening_token.unmatched = false;
unmatched_tokens[tag_token.tag].pop_back();
}
}
};

auto close_tag = [&]() {
tag_token.end_index = utf8::distance(text.begin(), start);
if (tag_token.tag.empty()) {
if (validate_condition(tag_name.empty(), "empty tag")) {
error = true;
return false;
}
tag_token.tag = tag_name;
} else {
if (validate_condition(value.empty(), "empty value")) {
error = true;
return false;
}
tag_token.value = value;
}

// Closing the tag and matching it
if (tag_token.type == Token_Type::OPENING_TAG) {
unmatched_tokens[tag_token.tag].push_back(tokens.size());
tag_token.unmatched = true;
} else {
match_closing_tag(tag_token);
}
return true;
};

while (start != end) {
if (std::find(std::begin(special), std::end(special), *start) == std::end(special)) {
// Read tag name or value
Expand All @@ -104,39 +145,23 @@ std::vector<Token> Text_Parser::parse(const std::string& text, bool permissive)
break;
}
} else if (*start == '}') {
tag_token.end_index = utf8::distance(text.begin(), start);
if (tag_token.tag.empty()) {
if (validate_condition(tag_name.empty(), "empty tag")) {
error = true;
break;
}
tag_token.tag = tag_name;
} else {
if (validate_condition(value.empty(), "empty value")) {
error = true;
break;
}
tag_token.value = value;
}
if (!close_tag()) break;
start++;
break;
} else if (*start == '/') {
// Handle self-closing tokens, e.g. {a/}
start++;
if (validate_condition(start == end, "close brace at the end of tag")) break;
if (validate_condition(*start != '}', "unexpected / in tag")) break;

// Closing the tag and matching it
if (tag_token.type == Token_Type::OPENING_TAG) {
unmatched_tokens[tag_token.tag].push_back(tokens.size());
tag_token.unmatched = true;
} else {
if (unmatched_tokens[tag_token.tag].empty()) {
// A closing tag with no matched open tag
tag_token.unmatched = true;
} else {
auto& opening_token = tokens[unmatched_tokens[tag_token.tag].back()];
// Found the closing tag for an unmatched open tag
if (opening_token.start_index < tag_token.start_index) {
opening_token.unmatched = false;
unmatched_tokens[tag_token.tag].pop_back();
}
}
}
tag_token.self_closing = true;
if (!close_tag()) break;

close_token.type = Token_Type::CLOSING_TAG;
close_token.tag = tag_token.tag;
close_token.start_index = tag_token.end_index;
close_token.end_index = tag_token.end_index;
close_token.self_closing = true;
start++;
break;
} else {
Expand All @@ -148,10 +173,15 @@ std::vector<Token> Text_Parser::parse(const std::string& text, bool permissive)
start++;
}

// Reached the end of the string but tag wasn't closed
// Add tokens unless they were not properly closed
auto unclosed_tag = tag_token.tag.empty() || (has_value && tag_token.value.empty());
if (!error && !validate_condition(unclosed_tag, "tag was not closed "))
if (!error && !validate_condition(unclosed_tag, "tag was not closed ")) {
tokens.push_back(tag_token);
if (!close_token.tag.empty()) {
match_closing_tag(close_token);
tokens.push_back(close_token);
}
}
}

// Not a tag token, so we read it as a text token until we find a tag
Expand Down
19 changes: 15 additions & 4 deletions src/xd/graphics/text_formatter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -976,8 +976,8 @@ void xd::text_formatter::parse(const std::string& text, detail::text_formatter::
// get the decorator name
std::string decorator_name;
while (start != end && (*start != '=' || !open_decorator)
&& !detail::text_formatter::safe_equal(m_decorator_close_delim, start, end))
{
&& (!detail::text_formatter::safe_equal(m_decorator_terminate_delim, start, end) || !open_decorator)
&& !detail::text_formatter::safe_equal(m_decorator_close_delim, start, end)) {
utf8::append(utf8::next(start, end), std::back_inserter(decorator_name));
}

Expand All @@ -989,7 +989,9 @@ void xd::text_formatter::parse(const std::string& text, detail::text_formatter::

// parse arguments
std::string arg;
while (start != end && !detail::text_formatter::safe_equal(m_decorator_close_delim, start, end)) {
while (start != end
&& !detail::text_formatter::safe_equal(m_decorator_close_delim, start, end)
&& !detail::text_formatter::safe_equal(m_decorator_terminate_delim, start, end)) {
if (*start == ',') {
// push and reset the argument
args.m_args.push_back(arg);
Expand All @@ -1005,6 +1007,13 @@ void xd::text_formatter::parse(const std::string& text, detail::text_formatter::
args.m_args.push_back(arg);
}

// Check for self-closing decorator
auto self_closing = false;
if (start != end && open_decorator && detail::text_formatter::safe_equal(m_decorator_terminate_delim, start, end)) {
utf8::advance(start, utf8::distance(m_decorator_terminate_delim.begin(), m_decorator_terminate_delim.end()), end);
self_closing = true;
}

// closing delimiter not found
if (start == end) {
throw text_formatter_parse_exception(text, "closing delimiter for decorator \""+decorator_name+"\" not found");
Expand Down Expand Up @@ -1035,7 +1044,9 @@ void xd::text_formatter::parse(const std::string& text, detail::text_formatter::
tok.callback = decorator_pos->second;
tokens.push_back(tok);
}
} else {
}

if (!open_decorator || self_closing) {
// check that tags are closed in correct order
if (open_decorators.size() == 0) {
throw text_formatter_parse_exception(text, "decorator \""+decorator_name+"\" closed without opening tag");
Expand Down

0 comments on commit d47b215

Please sign in to comment.