/*************************************************************************/ /* bbcode_parser.cpp */ /*************************************************************************/ /* This file is part of: */ /* PANDEMONIUM ENGINE */ /* https://github.com/Relintai/pandemonium_engine */ /*************************************************************************/ /* Copyright (c) 2022-present Péter Magyar. */ /* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md). */ /* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ /* "Software"), to deal in the Software without restriction, including */ /* without limitation the rights to use, copy, modify, merge, publish, */ /* distribute, sublicense, and/or sell copies of the Software, and to */ /* permit persons to whom the Software is furnished to do so, subject to */ /* the following conditions: */ /* */ /* The above copyright notice and this permission notice shall be */ /* included in all copies or substantial portions of the Software. */ /* */ /* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ /* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ /* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/ /* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ /* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ /* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ #include "bbcode_parser.h" #include "core/error/error_macros.h" #include "core/log/logger.h" String BBCodeParserAttribute::get_attribute() { return _attribute; } void BBCodeParserAttribute::set_attribute(const String &val) { _attribute = val; } String BBCodeParserAttribute::get_data() { return _data; } void BBCodeParserAttribute::set_data(const String &val) { _data = val; } bool BBCodeParserAttribute::get_single() { return _single; } void BBCodeParserAttribute::set_single(const bool &val) { _single = val; } bool BBCodeParserAttribute::match_attrib(const String &attrib) { return _attribute == attrib; } bool BBCodeParserAttribute::match_data(const String &d) { return _data == d; } bool BBCodeParserAttribute::match_all_data(const Vector &d) { // todo return false; } bool BBCodeParserAttribute::match_all_data_bind(const PoolStringArray &d) { // todo return false; } bool BBCodeParserAttribute::contains_data(const String &d) { return _data.find(d) != -1; } String BBCodeParserAttribute::convert_to_string() const { if (_single) { return _attribute; } if (_data.find_char('"' == -1)) { return _attribute + "=\"" + _data + "\""; } else { return _attribute + "=\'" + _data + "\'"; } } void BBCodeParserAttribute::print() const { PLOG_MSG(convert_to_string()); } BBCodeParserAttribute::BBCodeParserAttribute() { _single = false; } BBCodeParserAttribute::~BBCodeParserAttribute() { } void BBCodeParserAttribute::_bind_methods() { ClassDB::bind_method(D_METHOD("get_attribute"), &BBCodeParserAttribute::get_attribute); ClassDB::bind_method(D_METHOD("set_attribute", "val"), &BBCodeParserAttribute::set_attribute); ADD_PROPERTY(PropertyInfo(Variant::STRING, "attribute"), "set_attribute", "get_attribute"); ClassDB::bind_method(D_METHOD("get_data"), &BBCodeParserAttribute::get_data); ClassDB::bind_method(D_METHOD("set_data", "val"), &BBCodeParserAttribute::set_data); ADD_PROPERTY(PropertyInfo(Variant::STRING, "data"), "set_data", "get_data"); ClassDB::bind_method(D_METHOD("get_single"), &BBCodeParserAttribute::get_single); ClassDB::bind_method(D_METHOD("set_single", "val"), &BBCodeParserAttribute::set_single); ADD_PROPERTY(PropertyInfo(Variant::BOOL, "single"), "set_single", "get_single"); ClassDB::bind_method(D_METHOD("match_attrib", "attrib"), &BBCodeParserAttribute::match_attrib); ClassDB::bind_method(D_METHOD("match_data", "data"), &BBCodeParserAttribute::match_data); ClassDB::bind_method(D_METHOD("match_all_data", "data"), &BBCodeParserAttribute::match_all_data_bind); ClassDB::bind_method(D_METHOD("contains_data", "data"), &BBCodeParserAttribute::contains_data); ClassDB::bind_method(D_METHOD("convert_to_string"), &BBCodeParserAttribute::convert_to_string); ClassDB::bind_method(D_METHOD("print"), &BBCodeParserAttribute::print); }; int BBCodeParserTag::get_type() { return _type; } void BBCodeParserTag::set_type(const int &val) { _type = val; } String BBCodeParserTag::get_tag() { return _tag; } void BBCodeParserTag::set_tag(const String &val) { _tag = val; } String BBCodeParserTag::get_data() { return _data; } void BBCodeParserTag::set_data(const String &val) { _data = val; } void BBCodeParserTag::add_child_tag(const Ref &tag) { _tags.push_back(tag); } void BBCodeParserTag::remote_child_tag(const int index) { ERR_FAIL_INDEX(index, _tags.size()); _tags.remove(index); } Ref BBCodeParserTag::get_child_tag(const int index) { ERR_FAIL_INDEX_V(index, _tags.size(), Ref()); return _tags[index]; } int BBCodeParserTag::get_child_tag_count() const { return _tags.size(); } void BBCodeParserTag::clear_child_tags() { _tags.clear(); } Vector BBCodeParserTag::get_child_tags() { Vector r; for (int i = 0; i < _tags.size(); i++) { r.push_back(_tags[i].get_ref_ptr()); } return r; } void BBCodeParserTag::set_child_tags(const Vector &val) { _tags.clear(); for (int i = 0; i < val.size(); i++) { Ref e = Ref(val[i]); _tags.push_back(e); } } void BBCodeParserTag::add_child_attribute(const Ref &tag) { _attributes.push_back(tag); } void BBCodeParserTag::remote_child_attribute(const int index) { ERR_FAIL_INDEX(index, _tags.size()); _attributes.remove(index); } Ref BBCodeParserTag::get_child_attribute(const int index) { ERR_FAIL_INDEX_V(index, _tags.size(), Ref()); return _attributes[index]; } int BBCodeParserTag::get_child_attribute_count() const { return _attributes.size(); } void BBCodeParserTag::clear_child_attributes() { _attributes.clear(); } Vector BBCodeParserTag::get_attributes() { Vector r; for (int i = 0; i < _attributes.size(); i++) { r.push_back(_attributes[i].get_ref_ptr()); } return r; } void BBCodeParserTag::set_attributes(const Vector &val) { _attributes.clear(); for (int i = 0; i < val.size(); i++) { Ref e = Ref(val[i]); _attributes.push_back(e); } } Ref BBCodeParserTag::get_first(const String &t) { if (_tag == t) { return Ref(this); } for (int i = 0; i < _tags.size(); ++i) { Ref ht = _tags.write[i]->get_first(t); if (ht.is_valid()) { return ht; } } return Ref(); } Ref BBCodeParserTag::get_firstc(const String &t, const String &attrib, const String &val) { if (_tag == t) { if (has_attributec(attrib, val)) { return Ref(this); } } for (int i = 0; i < _tags.size(); ++i) { Ref ht = _tags.write[i]->get_firstc(t, attrib, val); if (ht.is_valid()) { return ht; } } return Ref(); } String BBCodeParserTag::get_attribute_value(const String &attrib) { Ref a = get_attribute(attrib); if (a.is_valid()) { return a->get_data(); } return ""; } Ref BBCodeParserTag::get_attribute(const String &attrib) { for (int i = 0; i < _attributes.size(); ++i) { Ref a = _attributes[i]; if (a->match_attrib(attrib)) { return a; } } return Ref(); } bool BBCodeParserTag::has_attribute(const String &attrib) { for (int i = 0; i < _attributes.size(); ++i) { Ref a = _attributes[i]; if (a->match_attrib(attrib)) { return true; } } return false; } Ref BBCodeParserTag::get_attributec(const String &attrib, const String &contains_val) { for (int i = 0; i < _attributes.size(); ++i) { Ref a = _attributes[i]; if (a->match_attrib(attrib) && a->contains_data(contains_val)) { return a; } } return Ref(); } bool BBCodeParserTag::has_attributec(const String &attrib, const String &contains_val) { for (int i = 0; i < _attributes.size(); ++i) { Ref a = _attributes[i]; if (a->match_attrib(attrib) && a->contains_data(contains_val)) { return true; } } return false; } void BBCodeParserTag::process() { if (_type != BBCodeParserTag::BBCODE_PARSER_TAG_TYPE_NONE) { return; } if (_data.length() < 2) { return; } ERR_FAIL_COND(_data[0] != '['); ERR_FAIL_COND(_data[_data.length() - 1] != ']'); int start_index = 1; if (_data[1] == '/') { ++start_index; _type = BBCodeParserTag::BBCODE_PARSER_TAG_TYPE_CLOSING_TAG; } else { String tag_text; if (_data[_data.length() - 2] == '/') { // will catch all that looks like
// tags that look like
will be caught later in a post process, in a way // which also tries to catch erroneously not closed tags that supposed to be closed _type = BBCodeParserTag::BBCODE_PARSER_TAG_TYPE_SELF_CLOSING_TAG; tag_text = _data.substr(1, _data.length() - 3); } else { _type = BBCodeParserTag::BBCODE_PARSER_TAG_TYPE_OPENING_TAG; tag_text = _data.substr(1, _data.length() - 2); } int fspc_index = tag_text.find_char(' '); if (fspc_index == -1) { // no args int feq_ind = tag_text.find_char('='); if (feq_ind == -1) { _tag = tag_text; return; } //Tag is like: [color=white] //tag will be like color _tag = tag_text.substr(0, feq_ind); //Add color=white as argument parse_args(tag_text); return; } // grab the tag itself _tag = tag_text.substr(0, fspc_index); if (fspc_index + 1 == tag_text.length()) { // no args, but had a space like
return; } String args = tag_text.substr(fspc_index + 1, tag_text.length() - fspc_index - 1); parse_args(args); } int tag_end_index = _data.find_char(' ', start_index); if (tag_end_index == -1) { // simple tag _tag = _data.substr(start_index, _data.length() - start_index - 1); return; } } void BBCodeParserTag::parse_args(const String &args) { _attributes.clear(); int i = 0; while (i < args.length()) { if (args[i] == ' ') { //"trim" ++i; continue; } int equals_index = args.find_char('=', i); Ref a; a.instance(); if (equals_index == -1) { a->set_attribute(args.substr(i, args.length() - i)); a->set_single(true); _attributes.push_back(a); return; } a->set_attribute(args.substr(i, equals_index - i)); // todo // a.trim(); int next_char_index = equals_index + 1; if (next_char_index >= args.length()) { // an attribute looks like this "... attrib=" _attributes.push_back(a); return; } // skip spaces while (args[next_char_index] == ' ') { ++next_char_index; if (next_char_index >= args.length()) { // an attribute looks like this "... attrib= " _attributes.push_back(a); return; } } char c = args[next_char_index]; char find_char = ' '; if (c == '"' || c == '\'') { ++next_char_index; find_char = c; } int end_index = args.find_char(find_char, next_char_index); if (end_index == -1) { // missing closing ' or " if c is ' or " // else missing parameter a->set_data(args.substr(next_char_index, args.length() - next_char_index)); _attributes.push_back(a); return; } a->set_data(args.substr(next_char_index, end_index - next_char_index)); _attributes.push_back(a); i = end_index + 1; } } String BBCodeParserTag::convert_to_string(const int level) const { String s; s += String(" ").repeat(level); if (_type == BBCODE_PARSER_TAG_TYPE_CONTENT) { s += _data + "\n"; if (_tags.size() != 0) { s += String(" ").repeat(level); s += "(!CONTENT TAG HAS TAGS!)\n"; for (int i = 0; i < _tags.size(); ++i) { s += _tags[i]->convert_to_string(level + 1) + "\n"; } } } else if (_type == BBCODE_PARSER_TAG_TYPE_OPENING_TAG) { int ln = level + 1; s += "[" + _tag; for (int i = 0; i < _attributes.size(); ++i) { s += " " + _attributes[i]->convert_to_string(); } s += "]\n"; for (int i = 0; i < _tags.size(); ++i) { s += _tags[i]->convert_to_string(ln); } s += String(" ").repeat(level); s += "[/" + _tag + "]\n"; } else if (_type == BBCODE_PARSER_TAG_TYPE_CLOSING_TAG) { // BBCodeParserTag should handle this automatically // it's here for debugging purposes though s += "[/" + _tag + "(!)]"; if (_tags.size() != 0) { s += String(" ").repeat(level); s += "(!CLOSING TAG HAS TAGS!)\n"; for (int i = 0; i < _tags.size(); ++i) { s += _tags[i]->convert_to_string(level + 1) + "\n"; } } } else if (_type == BBCODE_PARSER_TAG_TYPE_SELF_CLOSING_TAG) { s += "[" + _tag; for (int i = 0; i < _attributes.size(); ++i) { s += " " + _attributes[i]->convert_to_string(); } s += "/]\n"; if (_tags.size() != 0) { s += String(" ").repeat(level); for (int i = 0; i < _tags.size(); ++i) { s += _tags[i]->convert_to_string(level + 1) + "\n"; } } } else if (_type == BBCODE_PARSER_TAG_TYPE_NONE) { for (int i = 0; i < _tags.size(); ++i) { s += _tags[i]->convert_to_string(level) + "\n"; s += String(" ").repeat(level); } } return s; } void BBCodeParserTag::print() const { PLOG_MSG(convert_to_string()); } BBCodeParserTag::BBCodeParserTag() { _type = BBCodeParserTag::BBCODE_PARSER_TAG_TYPE_NONE; } BBCodeParserTag::~BBCodeParserTag() { _tags.clear(); _attributes.clear(); } void BBCodeParserTag::_bind_methods() { ClassDB::bind_method(D_METHOD("get_type"), &BBCodeParserTag::get_type); ClassDB::bind_method(D_METHOD("set_type", "val"), &BBCodeParserTag::set_type); ADD_PROPERTY(PropertyInfo(Variant::INT, "type"), "set_type", "get_type"); ClassDB::bind_method(D_METHOD("get_tag"), &BBCodeParserTag::get_tag); ClassDB::bind_method(D_METHOD("set_tag", "val"), &BBCodeParserTag::set_tag); ADD_PROPERTY(PropertyInfo(Variant::STRING, "tag"), "set_tag", "get_tag"); ClassDB::bind_method(D_METHOD("get_data"), &BBCodeParserTag::get_data); ClassDB::bind_method(D_METHOD("set_data", "val"), &BBCodeParserTag::set_data); ADD_PROPERTY(PropertyInfo(Variant::STRING, "data"), "set_data", "get_data"); ClassDB::bind_method(D_METHOD("add_child_tag", "tag"), &BBCodeParserTag::add_child_tag); ClassDB::bind_method(D_METHOD("remote_child_tag", "index"), &BBCodeParserTag::remote_child_tag); ClassDB::bind_method(D_METHOD("get_child_tag", "index"), &BBCodeParserTag::get_child_tag); ClassDB::bind_method(D_METHOD("get_child_tag_count"), &BBCodeParserTag::get_child_tag_count); ClassDB::bind_method(D_METHOD("clear_child_tags"), &BBCodeParserTag::clear_child_tags); ClassDB::bind_method(D_METHOD("get_child_tags"), &BBCodeParserTag::get_child_tags); ClassDB::bind_method(D_METHOD("set_child_tags", "val"), &BBCodeParserTag::set_child_tags); ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "child_tags", PROPERTY_HINT_NONE, "23/20:BBCodeParserTag", PROPERTY_USAGE_DEFAULT, "BBCodeParserTag"), "set_child_tags", "get_child_tags"); ClassDB::bind_method(D_METHOD("add_child_attribute", "tag"), &BBCodeParserTag::add_child_attribute); ClassDB::bind_method(D_METHOD("remote_child_attribute", "index"), &BBCodeParserTag::remote_child_attribute); ClassDB::bind_method(D_METHOD("get_child_attribute", "index"), &BBCodeParserTag::get_child_attribute); ClassDB::bind_method(D_METHOD("get_child_attribute_count"), &BBCodeParserTag::get_child_attribute_count); ClassDB::bind_method(D_METHOD("clear_child_attributes"), &BBCodeParserTag::clear_child_attributes); ClassDB::bind_method(D_METHOD("get_attributes"), &BBCodeParserTag::get_attributes); ClassDB::bind_method(D_METHOD("set_attributes", "val"), &BBCodeParserTag::set_attributes); ADD_PROPERTY(PropertyInfo(Variant::ARRAY, "attributes", PROPERTY_HINT_NONE, "23/20:BBCodeParserAttribute", PROPERTY_USAGE_DEFAULT, "BBCodeParserAttribute"), "set_attributes", "get_attributes"); ClassDB::bind_method(D_METHOD("get_first", "t"), &BBCodeParserTag::get_first); ClassDB::bind_method(D_METHOD("get_firstc", "t", "attrib", "val"), &BBCodeParserTag::get_firstc); ClassDB::bind_method(D_METHOD("get_attribute_value", "attrib"), &BBCodeParserTag::get_attribute_value); ClassDB::bind_method(D_METHOD("get_attribute", "attrib"), &BBCodeParserTag::get_attribute); ClassDB::bind_method(D_METHOD("has_attribute", "attrib"), &BBCodeParserTag::has_attribute); ClassDB::bind_method(D_METHOD("get_attributec", "attrib", "contains_val"), &BBCodeParserTag::get_attributec); ClassDB::bind_method(D_METHOD("has_attributec", "attrib", "contains_val"), &BBCodeParserTag::has_attributec); ClassDB::bind_method(D_METHOD("process"), &BBCodeParserTag::process); ClassDB::bind_method(D_METHOD("parse_args", "args"), &BBCodeParserTag::parse_args); ClassDB::bind_method(D_METHOD("convert_to_string", "level"), &BBCodeParserTag::convert_to_string, 0); ClassDB::bind_method(D_METHOD("print"), &BBCodeParserTag::print); BIND_ENUM_CONSTANT(BBCODE_PARSER_TAG_TYPE_NONE); BIND_ENUM_CONSTANT(BBCODE_PARSER_TAG_TYPE_OPENING_TAG); BIND_ENUM_CONSTANT(BBCODE_PARSER_TAG_TYPE_CLOSING_TAG); BIND_ENUM_CONSTANT(BBCODE_PARSER_TAG_TYPE_SELF_CLOSING_TAG); BIND_ENUM_CONSTANT(BBCODE_PARSER_TAG_TYPE_CONTENT); } Ref BBCodeParser::get_root() { return _root; } void BBCodeParser::parse(const String &data) { Vector> tags; // split into tags for (int i = 0; i < data.length(); ++i) { if (data[i] == '[') { // tag for (int j = i + 1; j < data.length(); ++j) { if (data[j] == ']') { Ref t; t.instance(); t->set_data(data.substr(i, j - i + 1)); t->process(); tags.push_back(t); i = j; break; } } } else { // content for (int j = i + 1; j < data.length(); ++j) { if (data[j] == '[') { Ref t; t.instance(); t->set_data(data.substr(i, j - i)); t->set_type(BBCodeParserTag::BBCODE_PARSER_TAG_TYPE_CONTENT); tags.push_back(t); i = j - 1; break; } } } } _root.instance(); // process tags into hierarchical order Vector> tag_stack; for (int i = 0; i < tags.size(); ++i) { Ref t = tags[i]; ERR_CONTINUE(!t.is_valid()); if (t->get_type() == BBCodeParserTag::BBCODE_PARSER_TAG_TYPE_NONE) { ERR_PRINT("BBCodeParser::parse: t->type == BBCodeParserTag::BBCODE_PARSER_TAG_TYPE_NONE!"); //memdelete(t); tags.write[i].unref(); continue; } else if (t->get_type() == BBCodeParserTag::BBCODE_PARSER_TAG_TYPE_OPENING_TAG) { tag_stack.push_back(t); tags.write[i].unref(); continue; } else if (t->get_type() == BBCodeParserTag::BBCODE_PARSER_TAG_TYPE_SELF_CLOSING_TAG) { if (tag_stack.size() == 0) { _root->add_child_tag(t); } else { tag_stack.write[tag_stack.size() - 1]->add_child_tag(t); } tags.write[i].unref(); continue; } else if (t->get_type() == BBCodeParserTag::BBCODE_PARSER_TAG_TYPE_CONTENT) { if (tag_stack.size() == 0) { _root->add_child_tag(t); } else { tag_stack.write[tag_stack.size() - 1]->add_child_tag(t); } tags.write[i].unref(); continue; } else if (t->get_type() == BBCodeParserTag::BBCODE_PARSER_TAG_TYPE_CLOSING_TAG) { if (tag_stack.size() == 0) { tags.write[i].unref(); // ill-formed html continue; } // find it's pair int tag_index = 0; for (int j = tag_stack.size() - 1; j > 0; --j) { Ref ts = tag_stack[j]; // we sould only have opening tags on the stack if (ts->get_tag() == t->get_tag()) { // found tag_index = j; break; } } Ref opening_tag = tag_stack[tag_index]; // mark everything else that we found before finding the opening tag as self closing, and add them to out opening tag // If the html is ill formed, it just grabs everything from the tag stack for (int j = tag_index + 1; j < tag_stack.size(); ++j) { Ref ts = tag_stack[j]; ts->set_type(BBCodeParserTag::BBCODE_PARSER_TAG_TYPE_SELF_CLOSING_TAG); opening_tag->add_child_tag(ts); } tag_stack.resize(tag_index); if (tag_stack.size() == 0) { _root->add_child_tag(opening_tag); } else { tag_stack.write[tag_stack.size() - 1]->add_child_tag(opening_tag); } tags.write[i].unref(); continue; } } // add everything remaining on the stack to root for (int i = 0; i < tag_stack.size(); ++i) { _root->add_child_tag(tag_stack[i]); } for (int i = 0; i < tags.size(); ++i) { Ref t = tags[i]; if (t.is_valid()) { ERR_PRINT("BBCodeParser::parse(const String &data): tag was not processed!\n"); t->print(); } } } String BBCodeParser::convert_to_string() const { if (!_root.is_valid()) { return ""; } return _root->convert_to_string(); } void BBCodeParser::print() const { if (_root.is_valid()) { _root->print(); } } BBCodeParser::BBCodeParser() { } BBCodeParser::~BBCodeParser() { _root.unref(); } void BBCodeParser::_bind_methods() { ClassDB::bind_method(D_METHOD("get_root"), &BBCodeParser::get_root); ClassDB::bind_method(D_METHOD("parse", "data"), &BBCodeParser::parse); ClassDB::bind_method(D_METHOD("convert_to_string"), &BBCodeParser::convert_to_string); ClassDB::bind_method(D_METHOD("print"), &BBCodeParser::print); };