/*
* This source file is part of RmlUi, the HTML/CSS Interface Middleware
*
* For the latest information, see http://github.com/mikke89/RmlUi
*
* Copyright (c) 2008-2010 CodePoint Ltd, Shift Technology Ltd
* Copyright (c) 2019 The RmlUi Team, and contributors
*
* 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 "precompiled.h"
#include "../../Include/RmlUi/Core/PropertySpecification.h"
#include "PropertyShorthandDefinition.h"
#include "../../Include/RmlUi/Core/Log.h"
#include "../../Include/RmlUi/Core/PropertyDefinition.h"
#include "../../Include/RmlUi/Core/PropertyDictionary.h"
namespace Rml {
namespace Core {
PropertySpecification::PropertySpecification(size_t reserve_num_properties, size_t reserve_num_shorthands) :
// Increment reserve numbers by one because the 'invalid' property occupies the first element
properties(reserve_num_properties + 1, nullptr), shorthands(reserve_num_shorthands + 1, nullptr), property_map(reserve_num_properties + 1), shorthand_map(reserve_num_shorthands + 1)
{
property_names.reserve(reserve_num_properties);
}
PropertySpecification::~PropertySpecification()
{
for (PropertyDefinition* p : properties)
delete p;
for (ShorthandDefinition* s : shorthands)
delete s;
}
// Registers a property with a new definition.
PropertyDefinition& PropertySpecification::RegisterProperty(const String& property_name, const String& default_value, bool inherited, bool forces_layout, PropertyId id)
{
if (id == PropertyId::Invalid)
id = property_map.GetOrCreateId(property_name);
else
property_map.AddPair(id, property_name);
size_t index = (size_t)id;
RMLUI_ASSERT(index < (size_t)INT16_MAX);
if (index < properties.size())
{
// We don't want to owerwrite an existing entry.
if (properties[index])
{
Log::Message(Log::LT_ERROR, "While registering property '%s': The property is already registered, ignoring.", property_name.c_str());
return *properties[index];
}
}
else
{
// Resize vector to hold the new index
properties.resize((index*3)/2 + 1, nullptr);
}
// Create and insert the new property
PropertyDefinition* property_definition = new PropertyDefinition(id, default_value, inherited, forces_layout);
properties[index] = property_definition;
property_names.insert(id);
if (inherited)
inherited_property_names.insert(id);
return *property_definition;
}
// Returns a property definition.
const PropertyDefinition* PropertySpecification::GetProperty(PropertyId id) const
{
if (id == PropertyId::Invalid || (size_t)id >= properties.size())
return nullptr;
return properties[(size_t)id];
}
const PropertyDefinition* PropertySpecification::GetProperty(const String& property_name) const
{
return GetProperty(property_map.GetId(property_name));
}
// Fetches a list of the names of all registered property definitions.
const PropertyNameList& PropertySpecification::GetRegisteredProperties(void) const
{
return property_names;
}
// Fetches a list of the names of all registered property definitions.
const PropertyNameList& PropertySpecification::GetRegisteredInheritedProperties(void) const
{
return inherited_property_names;
}
// Registers a shorthand property definition.
ShorthandId PropertySpecification::RegisterShorthand(const String& shorthand_name, const String& property_names, ShorthandType type, ShorthandId id)
{
if (id == ShorthandId::Invalid)
id = shorthand_map.GetOrCreateId(shorthand_name);
else
shorthand_map.AddPair(id, shorthand_name);
StringList property_list;
StringUtilities::ExpandString(property_list, ToLower(property_names));
ShorthandItemIdList id_list;
id_list.reserve(property_list.size());
for (auto& name : property_list)
{
PropertyId property_id = property_map.GetId(name);
if (property_id != PropertyId::Invalid)
id_list.emplace_back(property_id);
else
{
ShorthandId shorthand_id = shorthand_map.GetId(name);
if (shorthand_id != ShorthandId::Invalid)
id_list.emplace_back(shorthand_id);
}
}
if (id_list.empty() || id_list.size() != property_list.size())
return ShorthandId::Invalid;
if (id_list.empty())
return ShorthandId::Invalid;
// Construct the new shorthand definition and resolve its properties.
UniquePtr property_shorthand(new ShorthandDefinition());
for (size_t i = 0; i < id_list.size(); i++)
{
ShorthandItem item;
if (id_list[i].type == ShorthandItemType::Property)
{
const PropertyDefinition* property = GetProperty(id_list[i].property_id);
if(property)
item = ShorthandItem(id_list[i].property_id, property);
}
else if (id_list[i].type == ShorthandItemType::Shorthand && (type == ShorthandType::Recursive || type == ShorthandType::RecursiveCommaSeparated))
{
// The recursive types (and only those) can hold other shorthands
const ShorthandDefinition* shorthand = GetShorthand(id_list[i].shorthand_id);
if (shorthand)
item = ShorthandItem(id_list[i].shorthand_id, shorthand);
}
if (item.type == ShorthandItemType::Invalid)
{
Log::Message(Log::LT_ERROR, "Shorthand property '%s' was registered with invalid property '%s'.", shorthand_name.c_str(), property_list[i].c_str());
return ShorthandId::Invalid;
}
property_shorthand->items.push_back(item);
}
property_shorthand->id = id;
property_shorthand->type = type;
const size_t index = (size_t)id;
RMLUI_ASSERT(index < (size_t)INT16_MAX);
if (index < shorthands.size())
{
// We don't want to owerwrite an existing entry.
if (shorthands[index])
{
Log::Message(Log::LT_ERROR, "The shorthand '%s' already exists, ignoring.", shorthand_name.c_str());
return ShorthandId::Invalid;
}
}
else
{
// Resize vector to hold the new index
shorthands.resize((index * 3) / 2 + 1, nullptr);
}
shorthands[index] = property_shorthand.release();
return id;
}
// Returns a shorthand definition.
const ShorthandDefinition* PropertySpecification::GetShorthand(ShorthandId id) const
{
if (id == ShorthandId::Invalid || (size_t)id >= shorthands.size())
return nullptr;
return shorthands[(size_t)id];
}
const ShorthandDefinition* PropertySpecification::GetShorthand(const String& shorthand_name) const
{
return GetShorthand(shorthand_map.GetId(shorthand_name));
}
bool PropertySpecification::ParsePropertyDeclaration(PropertyDictionary& dictionary, const String& property_name, const String& property_value, const String& source_file, int source_line_number) const
{
// Try as a property first
PropertyId property_id = property_map.GetId(property_name);
if (property_id != PropertyId::Invalid)
return ParsePropertyDeclaration(dictionary, property_id, property_value, source_file, source_line_number);
// Then, as a shorthand
ShorthandId shorthand_id = shorthand_map.GetId(property_name);
if (shorthand_id != ShorthandId::Invalid)
return ParseShorthandDeclaration(dictionary, shorthand_id, property_value, source_file, source_line_number);
return false;
}
bool PropertySpecification::ParsePropertyDeclaration(PropertyDictionary& dictionary, PropertyId property_id, const String& property_value, const String& source_file, int source_line_number) const
{
// Parse as a single property.
const PropertyDefinition* property_definition = GetProperty(property_id);
if (!property_definition)
return false;
StringList property_values;
if (!ParsePropertyValues(property_values, property_value, false) || property_values.size() == 0)
return false;
Property new_property;
new_property.source = source_file;
new_property.source_line_number = source_line_number;
if (!property_definition->ParseValue(new_property, property_values[0]))
return false;
dictionary.SetProperty(property_id, new_property);
return true;
}
// Parses a property declaration, setting any parsed and validated properties on the given dictionary.
bool PropertySpecification::ParseShorthandDeclaration(PropertyDictionary& dictionary, ShorthandId shorthand_id, const String& property_value, const String& source_file, int source_line_number) const
{
StringList property_values;
if (!ParsePropertyValues(property_values, property_value, true) || property_values.size() == 0)
return false;
// Parse as a shorthand.
const ShorthandDefinition* shorthand_definition = GetShorthand(shorthand_id);
if (!shorthand_definition)
return false;
// If this definition is a 'box'-style shorthand (x-top, x-right, x-bottom, x-left, etc) and there are fewer
// than four values
if (shorthand_definition->type == ShorthandType::Box &&
property_values.size() < 4)
{
// This array tells which property index each side is parsed from
std::array box_side_to_value_index = { 0,0,0,0 };
switch (property_values.size())
{
case 1:
// Only one value is defined, so it is parsed onto all four sides.
box_side_to_value_index = { 0,0,0,0 };
break;
case 2:
// Two values are defined, so the first one is parsed onto the top and bottom value, the second onto
// the left and right.
box_side_to_value_index = { 0,1,0,1 };
break;
case 3:
// Three values are defined, so the first is parsed into the top value, the second onto the left and
// right, and the third onto the bottom.
box_side_to_value_index = { 0,1,2,1 };
break;
default:
RMLUI_ERROR;
break;
}
for (int i = 0; i < 4; i++)
{
RMLUI_ASSERT(shorthand_definition->items[i].type == ShorthandItemType::Property);
Property new_property;
int value_index = box_side_to_value_index[i];
if (!shorthand_definition->items[i].property_definition->ParseValue(new_property, property_values[value_index]))
return false;
new_property.source = source_file;
new_property.source_line_number = source_line_number;
dictionary.SetProperty(shorthand_definition->items[i].property_definition->GetId(), new_property);
}
}
else if (shorthand_definition->type == ShorthandType::Recursive)
{
bool result = true;
for (size_t i = 0; i < shorthand_definition->items.size(); i++)
{
const ShorthandItem& item = shorthand_definition->items[i];
if (item.type == ShorthandItemType::Property)
result &= ParsePropertyDeclaration(dictionary, item.property_id, property_value, source_file, source_line_number);
else if (item.type == ShorthandItemType::Shorthand)
result &= ParseShorthandDeclaration(dictionary, item.shorthand_id, property_value, source_file, source_line_number);
else
result = false;
}
if (!result)
return false;
}
else if (shorthand_definition->type == ShorthandType::RecursiveCommaSeparated)
{
bool result = true;
StringList subvalues;
StringUtilities::ExpandString(subvalues, property_value);
if (shorthand_definition->items.size() != subvalues.size())
{
// We must declare all subvalues
return false;
}
for (size_t i = 0; i < shorthand_definition->items.size(); i++)
{
const ShorthandItem& item = shorthand_definition->items[i];
if (item.type == ShorthandItemType::Property)
result &= ParsePropertyDeclaration(dictionary, item.property_id, subvalues[i], source_file, source_line_number);
else if (item.type == ShorthandItemType::Shorthand)
result &= ParseShorthandDeclaration(dictionary, item.shorthand_id, subvalues[i], source_file, source_line_number);
else
result = false;
}
if (!result)
return false;
}
else
{
size_t value_index = 0;
size_t property_index = 0;
for (; value_index < property_values.size() && property_index < shorthand_definition->items.size(); property_index++)
{
Property new_property;
new_property.source = source_file;
new_property.source_line_number = source_line_number;
if (!shorthand_definition->items[property_index].property_definition->ParseValue(new_property, property_values[value_index]))
{
// This definition failed to parse; if we're falling through, try the next property. If there is no
// next property, then abort!
if (shorthand_definition->type == ShorthandType::FallThrough)
{
if (property_index + 1 < shorthand_definition->items.size())
continue;
}
return false;
}
dictionary.SetProperty(shorthand_definition->items[property_index].property_id, new_property);
// Increment the value index, unless we're replicating the last value and we're up to the last value.
if (shorthand_definition->type != ShorthandType::Replicate ||
value_index < property_values.size() - 1)
value_index++;
}
}
return true;
}
// Sets all undefined properties in the dictionary to their defaults.
void PropertySpecification::SetPropertyDefaults(PropertyDictionary& dictionary) const
{
for (PropertyDefinition* property : properties)
{
if (property && dictionary.GetProperty(property->GetId()) == NULL)
dictionary.SetProperty(property->GetId(), *property->GetDefaultValue());
}
}
String PropertySpecification::PropertiesToString(const PropertyDictionary& dictionary) const
{
String result;
for (auto& pair : dictionary.GetProperties())
{
result += property_map.GetName(pair.first) + ": " + pair.second.ToString() + '\n';
}
return result;
}
bool PropertySpecification::ParsePropertyValues(StringList& values_list, const String& values, bool split_values) const
{
String value;
enum ParseState { VALUE, VALUE_PARENTHESIS, VALUE_QUOTE };
ParseState state = VALUE;
int open_parentheses = 0;
size_t character_index = 0;
char previous_character = 0;
while (character_index < values.size())
{
char character = values[character_index];
character_index++;
switch (state)
{
case VALUE:
{
if (character == ';')
{
value = StringUtilities::StripWhitespace(value);
if (value.size() > 0)
{
values_list.push_back(value);
value.clear();
}
}
else if (StringUtilities::IsWhitespace(character))
{
if (split_values)
{
value = StringUtilities::StripWhitespace(value);
if (value.size() > 0)
{
values_list.push_back(value);
value.clear();
}
}
else
value += character;
}
else if (character == '"')
{
if (split_values)
{
value = StringUtilities::StripWhitespace(value);
if (value.size() > 0)
{
values_list.push_back(value);
value.clear();
}
state = VALUE_QUOTE;
}
else
{
value += ' ';
state = VALUE_QUOTE;
}
}
else if (character == '(')
{
open_parentheses = 1;
value += character;
state = VALUE_PARENTHESIS;
}
else
{
value += character;
}
}
break;
case VALUE_PARENTHESIS:
{
if (previous_character == '/')
{
if (character == ')' || character == '(')
value += character;
else
{
value += '/';
value += character;
}
}
else
{
if (character == '(')
{
open_parentheses++;
value += character;
}
else if (character == ')')
{
open_parentheses--;
value += character;
if (open_parentheses == 0)
state = VALUE;
}
else if (character != '/')
{
value += character;
}
}
}
break;
case VALUE_QUOTE:
{
if (previous_character == '/')
{
if (character == '"')
value += character;
else
{
value += '/';
value += character;
}
}
else
{
if (character == '"')
{
if (split_values)
{
value = StringUtilities::StripWhitespace(value);
if (value.size() > 0)
{
values_list.push_back(value);
value.clear();
}
}
else
value += ' ';
state = VALUE;
}
else if (character != '/')
{
value += character;
}
}
}
}
previous_character = character;
}
if (state == VALUE)
{
value = StringUtilities::StripWhitespace(value);
if (value.size() > 0)
values_list.push_back(value);
}
return true;
}
}
}