From 0defa0ada3a0c7294ebfec9d256e836c0d2cd09d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Wed, 20 May 2026 01:39:05 -0300 Subject: [PATCH] Fix hex color string parsing. UIMarkdownView now uses the HTMLFormatter::HTMLtoXML. Removed formatting hack to force strict XML from HTML, now we should always use HTMLFormatter::HTMLtoXML. --- .ecode/project_build.json | 2 +- TODO.md | 2 - src/eepp/system/color.cpp | 39 +++++--- src/eepp/ui/uimarkdownview.cpp | 4 +- src/eepp/ui/uirichtext.cpp | 8 ++ src/eepp/ui/uiscenenode.cpp | 128 +++++++++----------------- src/tests/unit_tests/uihtml_tests.cpp | 2 +- 7 files changed, 82 insertions(+), 103 deletions(-) diff --git a/.ecode/project_build.json b/.ecode/project_build.json index 777457d7f..159f08674 100644 --- a/.ecode/project_build.json +++ b/.ecode/project_build.json @@ -377,7 +377,7 @@ "working_dir": "${project_root}/bin" }, { - "args": "-d1 file:///home/downloads/files/svn/eepp/bin/unit_tests/assets/html/blog_main_incorrect_widths.html", + "args": "-c system --hn-dark", "command": "${project_root}/bin/eepp-ui-html-debug", "name": "eepp-ui-html-debug", "working_dir": "${project_root}/bin" diff --git a/TODO.md b/TODO.md index ffa966bfb..a8cf3c583 100644 --- a/TODO.md +++ b/TODO.md @@ -9,8 +9,6 @@ * Implement TableView and TreeView properties. -* Add automatic font-fallback for lang scripts - * Implement support for setting and creating a model from the XML. * Implement support for very simple state-changes from the XML file (ex: onclick="toggleclass(x)"). diff --git a/src/eepp/system/color.cpp b/src/eepp/system/color.cpp index f4aa58a9e..51456ad11 100644 --- a/src/eepp/system/color.cpp +++ b/src/eepp/system/color.cpp @@ -13,7 +13,7 @@ namespace EE { namespace System { namespace { template inline T _round( T r ) { - return ( r > 0.0f ) ? eefloor( r + 0.5f ) : eeceil( r - 0.5f ); + return ( r > 0.0f ) ? std::floor( r + 0.5f ) : std::ceil( r - 0.5f ); } } // namespace @@ -504,21 +504,29 @@ Color Color::fromString( std::string str ) { return Color::Transparent; if ( str[0] == '#' ) { - str = str.substr( 1 ); + str.erase( 0, 1 ); size = str.size(); - if ( 0 == size ) + if ( size != 3 && size != 4 && size != 6 && size != 8 ) return Color::Transparent; - if ( size < 6 ) { - for ( std::size_t i = size; i < 6; i++ ) - str += str[size - 1]; + // Expand shorthand CSS notation + if ( size == 3 || size == 4 ) { + std::string expanded; + expanded.reserve( size * 2 ); - size = 6; + for ( char c : str ) { + expanded += c; + expanded += c; + } + + str = expanded; + size = str.size(); } - if ( 6 == size ) + // Add opaque alpha if omitted + if ( size == 6 ) str += "FF"; return Color( std::strtoul( str.c_str(), NULL, 16 ) ); @@ -715,8 +723,7 @@ Color Color::fromString( std::string str ) { return Color::Transparent; } -template -bool Color::isColorStringT( StringType str, bool searchColorNames ) { +template bool Color::isColorStringT( StringType str, bool searchColorNames ) { if ( str.empty() ) return false; @@ -831,12 +838,18 @@ bool Color::validHexColorString( String::View hexColor ) { } bool Color::validHexColorString( std::string_view hexColor ) { - if ( hexColor.size() < 2 || hexColor[0] != '#' ) + if ( hexColor.empty() || hexColor[0] != '#' ) + return false; + + const size_t len = hexColor.size() - 1; + + if ( len != 3 && len != 4 && len != 6 && len != 8 ) return false; for ( size_t i = 1; i < hexColor.size(); i++ ) { - if ( !( String::isNumber( hexColor[i] ) || ( hexColor[i] >= 'a' && hexColor[i] <= 'f' ) || - ( hexColor[i] >= 'A' && hexColor[i] <= 'F' ) ) ) { + char c = hexColor[i]; + + if ( !( String::isNumber( c ) || ( c >= 'a' && c <= 'f' ) || ( c >= 'A' && c <= 'F' ) ) ) { return false; } } diff --git a/src/eepp/ui/uimarkdownview.cpp b/src/eepp/ui/uimarkdownview.cpp index b083d5e08..56e5f4b95 100644 --- a/src/eepp/ui/uimarkdownview.cpp +++ b/src/eepp/ui/uimarkdownview.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -28,8 +29,7 @@ bool UIMarkdownView::isType( const Uint32& type ) const { void UIMarkdownView::loadFromString( std::string_view markdown ) { closeAllChildren(); - auto xhtml = Markdown::toXHTML( markdown ); - // printf( "%s", xhtml.c_str() ); + auto xhtml = Tools::HTMLFormatter::HTMLtoXML( Markdown::toXHTML( markdown ) ); getUISceneNode()->loadLayoutFromString( xhtml, this ); } diff --git a/src/eepp/ui/uirichtext.cpp b/src/eepp/ui/uirichtext.cpp index a4153f4fe..441cf2af4 100644 --- a/src/eepp/ui/uirichtext.cpp +++ b/src/eepp/ui/uirichtext.cpp @@ -679,6 +679,14 @@ Float UIRichText::getLineHeightPx() const { mLineHeightPxCache = 0; return 0; } + + // Temporal hack until we support calc + if ( mLineHeightEq.find( "calc(" ) != std::string::npos || + mLineHeightEq.find( "var(" ) != std::string::npos ) { + mLineHeightPxCache = 0; + return 0; + } + bool isUnitless = !mLineHeightEq.empty(); for ( char c : mLineHeightEq ) { if ( c != '-' && c != '+' && c != '.' && !String::isNumber( c, false ) ) { diff --git a/src/eepp/ui/uiscenenode.cpp b/src/eepp/ui/uiscenenode.cpp index 0cac9a60b..8316b3724 100644 --- a/src/eepp/ui/uiscenenode.cpp +++ b/src/eepp/ui/uiscenenode.cpp @@ -1,4 +1,3 @@ -#include "eepp/ui/uirichtext.hpp" #include #include #include @@ -36,9 +35,6 @@ using namespace EE::Network; namespace EE { namespace UI { -static constexpr std::string_view VOIDTAG_REGEX = - "(<(?:img|br|hr|input|meta|link)\\b[^>]*?)(?"; - UISceneNode* UISceneNode::New( EE::Window::Window* window ) { return eeNew( UISceneNode, ( window ) ); } @@ -543,26 +539,14 @@ UIWidget* UISceneNode::loadLayoutFromFile( const std::string& layoutPath, Node* UIWidget* UISceneNode::loadLayoutFromString( const char* layoutString, Node* parent, const Uint32& marker ) { - RegEx voidTagsRegex( VOIDTAG_REGEX ); - pugi::xml_document doc; - pugi::xml_parse_result result; - std::string fixedLayout; - bool needsReplacements = voidTagsRegex.matches( layoutString ); - - if ( needsReplacements ) { - fixedLayout = voidTagsRegex.gsub( layoutString, "%1 />" ); - result = - doc.load_string( fixedLayout.c_str(), pugi::parse_default | pugi::parse_ws_pcdata ); - } else { - result = doc.load_string( layoutString, pugi::parse_default | pugi::parse_ws_pcdata ); - } + pugi::xml_parse_result result = + doc.load_string( layoutString, pugi::parse_default | pugi::parse_ws_pcdata ); if ( result ) { return loadLayoutNodes( doc.first_child(), NULL != parent ? parent : this, marker ); } else { - Log::error( "Couldn't load UI Layout from string: %s", - needsReplacements ? fixedLayout.c_str() : layoutString ); + Log::error( "Couldn't load UI Layout from string: %s", layoutString ); Log::error( "Error description: %s", result.description() ); Log::error( "Error offset: %d", result.offset ); Log::error( "Error context: %s", getErrorContext( result.offset, layoutString ) ); @@ -578,28 +562,15 @@ UIWidget* UISceneNode::loadLayoutFromString( const std::string& layoutString, No UIWidget* UISceneNode::loadLayoutFromMemory( const void* buffer, Int32 bufferSize, Node* parent, const Uint32& marker ) { - RegEx voidTagsRegex( VOIDTAG_REGEX ); - pugi::xml_document doc; - pugi::xml_parse_result result; std::string_view layoutString( static_cast( buffer ), bufferSize ); - std::string fixedLayout; - bool needsReplacements = - voidTagsRegex.matches( static_cast( buffer ), 0, nullptr, bufferSize ); - - if ( needsReplacements ) { - fixedLayout = voidTagsRegex.gsub( layoutString.data(), "%1 />" ); - result = doc.load_buffer( fixedLayout.c_str(), fixedLayout.size(), - pugi::parse_default | pugi::parse_ws_pcdata ); - } else { - result = doc.load_buffer( buffer, bufferSize, pugi::parse_default | pugi::parse_ws_pcdata ); - } + pugi::xml_parse_result result = + doc.load_buffer( buffer, bufferSize, pugi::parse_default | pugi::parse_ws_pcdata ); if ( result ) { return loadLayoutNodes( doc.first_child(), NULL != parent ? parent : this, marker ); } else { - Log::error( "Couldn't load UI Layout from memory: %s", - needsReplacements ? fixedLayout.c_str() : layoutString.data() ); + Log::error( "Couldn't load UI Layout from memory: %s", layoutString.data() ); Log::error( "Error description: %s", result.description() ); Log::error( "Error offset: %d", result.offset ); Log::error( "Error context: %s", @@ -620,32 +591,17 @@ UIWidget* UISceneNode::loadLayoutFromStream( IOStream& stream, Node* parent, TScopedBuffer scopedBuffer( bufferSize ); stream.read( scopedBuffer.get(), scopedBuffer.length() ); - RegEx voidTagsRegex( VOIDTAG_REGEX ); - pugi::xml_document doc; - pugi::xml_parse_result result; std::string_view layoutString( scopedBuffer.get(), scopedBuffer.length() ); - std::string fixedLayout; - bool needsReplacements = - voidTagsRegex.matches( scopedBuffer.get(), 0, nullptr, scopedBuffer.length() ); std::string_view contents; - - if ( needsReplacements ) { - fixedLayout = voidTagsRegex.gsub( layoutString.data(), "%1 />" ); - result = doc.load_buffer( fixedLayout.c_str(), fixedLayout.size(), - pugi::parse_default | pugi::parse_ws_pcdata ); - contents = fixedLayout; - } else { - result = doc.load_buffer( scopedBuffer.get(), scopedBuffer.length(), - pugi::parse_default | pugi::parse_ws_pcdata ); - contents = std::string_view( scopedBuffer.get(), scopedBuffer.length() ); - } + pugi::xml_parse_result result = doc.load_buffer( scopedBuffer.get(), scopedBuffer.length(), + pugi::parse_default | pugi::parse_ws_pcdata ); + contents = std::string_view( scopedBuffer.get(), scopedBuffer.length() ); if ( result ) { return loadLayoutNodes( doc.first_child(), NULL != parent ? parent : this, marker ); } else { - Log::error( "Couldn't load UI Layout from stream: %s", - needsReplacements ? fixedLayout.c_str() : layoutString.data() ); + Log::error( "Couldn't load UI Layout from stream: %s", layoutString.data() ); Log::error( "Error description: %s", result.description() ); Log::error( "Error offset: %d", result.offset ); Log::error( "Error context: %s", getErrorContext( result.offset, contents ) ); @@ -1531,6 +1487,8 @@ Font* UISceneNode::getFontFromNamesList( std::string_view names, Uint32 fontStyl font = fm->getByName( fontFamily ); + // Remove the font style part (ex: `Arial#bold` to `Arial`) + // We need this for SystemFontResolver::genericFamilyFromName if ( fontStyle ) fontFamily.resize( size ); @@ -1548,41 +1506,43 @@ Font* UISceneNode::getFontFromNamesList( std::string_view names, Uint32 fontStyl font = fm->getByName( fontFamily ); } - return font != nullptr; - }, - ',' ); + if ( font == nullptr && SystemFontResolver::isEnabled() ) { + FontWeight weight = + ( fontStyle & Text::Bold ) ? FontWeight::Bold : FontWeight::Normal; + FontDesc desc = SystemFontResolver::instance()->resolveFromNamesList( + std::string{ names }, weight, fontStyle & Text::Italic ); + if ( !desc.path.empty() ) { + std::string family = desc.family; + if ( fontStyle ) + family += "#" + Text::styleFlagToString( fontStyle ); - if ( font == nullptr && SystemFontResolver::isEnabled() ) { - FontWeight weight = ( fontStyle & Text::Bold ) ? FontWeight::Bold : FontWeight::Normal; - FontDesc desc = SystemFontResolver::instance()->resolveFromNamesList( - std::string{ names }, weight, fontStyle & Text::Italic ); - if ( !desc.path.empty() ) { - std::string family = desc.family; - if ( fontStyle ) - family += "#" + Text::styleFlagToString( fontStyle ); + if ( ( font = fm->getByName( family ) ) ) + return true; - if ( ( font = fm->getByName( family ) ) ) - return font; - - FontTrueType* ttf = FontTrueType::New( family, desc.path, desc.faceIndex ); - if ( ttf && ttf->loaded() ) { - font = ttf; - Uint32 weightStyle = fontStyle & ( Text::Bold | Text::Italic ); - if ( weightStyle ) { - Font* regular = fm->getByName( desc.family ); - if ( regular && regular != font && regular->getType() == FontType::TTF ) { - auto* regularFT = static_cast( regular ); - if ( weightStyle == Text::Bold ) - regularFT->setBoldFont( ttf ); - else if ( weightStyle == Text::Italic ) - regularFT->setItalicFont( ttf ); - else - regularFT->setBoldItalicFont( ttf ); + FontTrueType* ttf = FontTrueType::New( family, desc.path, desc.faceIndex ); + if ( ttf && ttf->loaded() ) { + font = ttf; + Uint32 weightStyle = fontStyle & ( Text::Bold | Text::Italic ); + if ( weightStyle ) { + Font* regular = fm->getByName( desc.family ); + if ( regular && regular != font && + regular->getType() == FontType::TTF ) { + auto* regularFT = static_cast( regular ); + if ( weightStyle == Text::Bold ) + regularFT->setBoldFont( ttf ); + else if ( weightStyle == Text::Italic ) + regularFT->setItalicFont( ttf ); + else + regularFT->setBoldItalicFont( ttf ); + } + } } } } - } - } + + return font != nullptr; + }, + ',' ); return font; } diff --git a/src/tests/unit_tests/uihtml_tests.cpp b/src/tests/unit_tests/uihtml_tests.cpp index b66d5c89b..6d2e8b759 100644 --- a/src/tests/unit_tests/uihtml_tests.cpp +++ b/src/tests/unit_tests/uihtml_tests.cpp @@ -1118,7 +1118,7 @@ UTEST( UIHTMLBody, maxWidthResizingBug ) { std::string htmlContent; FileSystem::fileGet( "assets/html/dwarmstrong/dwarmstrong.html", htmlContent ); - sceneNode->loadLayoutFromString( htmlContent ); + sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( htmlContent ) ); sceneNode->getRoot()->setSize( 1024, 768 ); sceneNode->updateDirtyLayouts();