mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-10-19 03:53:57 +03:00
More improvements in Markdown and basic HTML support (WIP!).
This commit is contained in:
@@ -111,10 +111,14 @@ p, ol, ul, pre {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
ol, ul {
|
||||
margin-left: 2em;
|
||||
}
|
||||
|
||||
li {
|
||||
margin: 0.67em 0;
|
||||
padding-left: 2em;
|
||||
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><circle cx='50' cy='50' r='40' fill='currentColor'/></svg>");
|
||||
background-image: url("data:image/svg,<svg viewBox='0 0 24 24' width='12' height='12' fill='#ffffff'><path d='M12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22Z'></path></svg>");
|
||||
background-tint: var(--font);
|
||||
background-position: 0.6em 0.5em;
|
||||
background-size: 0.8em 0.8em;
|
||||
|
||||
@@ -15,7 +15,7 @@ class EE_API UILinearLayout : public UILayout {
|
||||
|
||||
static UILinearLayout* NewHorizontal();
|
||||
|
||||
static UILinearLayout* NewVerticalWidthMatchParent();
|
||||
static UILinearLayout* NewVerticalWidthMatchParent( const std::string& tag );
|
||||
|
||||
virtual Uint32 getType() const;
|
||||
|
||||
|
||||
@@ -671,6 +671,18 @@ class EE_API UISceneNode : public SceneNode {
|
||||
*/
|
||||
CSS::MediaFeatures getMediaFeatures() const;
|
||||
|
||||
/**
|
||||
* @brief Loads UI nodes from XML.
|
||||
*
|
||||
* Core method that parses XML and creates widget hierarchy.
|
||||
*
|
||||
* @param node The XML node to parse.
|
||||
* @param parent The parent to attach widgets to.
|
||||
* @param marker Marker for style association.
|
||||
* @return Vector of root widgets created.
|
||||
*/
|
||||
std::vector<UIWidget*> loadNode( pugi::xml_node node, Node* parent, const Uint32& marker = 0 );
|
||||
|
||||
protected:
|
||||
friend class EE::UI::UIWindow;
|
||||
friend class EE::UI::UIWidget;
|
||||
@@ -885,18 +897,6 @@ class EE_API UISceneNode : public SceneNode {
|
||||
*/
|
||||
void resetTooltips( Node* node );
|
||||
|
||||
/**
|
||||
* @brief Loads UI nodes from XML.
|
||||
*
|
||||
* Core method that parses XML and creates widget hierarchy.
|
||||
*
|
||||
* @param node The XML node to parse.
|
||||
* @param parent The parent to attach widgets to.
|
||||
* @param marker Marker for style association.
|
||||
* @return Vector of root widgets created.
|
||||
*/
|
||||
std::vector<UIWidget*> loadNode( pugi::xml_node node, Node* parent, const Uint32& marker );
|
||||
|
||||
/**
|
||||
* @brief Applies a theme to a node and its subtree.
|
||||
*
|
||||
|
||||
@@ -1257,6 +1257,15 @@ class EE_API UIWidget : public UINode {
|
||||
*/
|
||||
String i18n( const std::string& str, const String& defaultValue );
|
||||
|
||||
/**
|
||||
* @brief Handles widget creation events.
|
||||
*
|
||||
* Called after the widget is created and initialized. This can be overridden
|
||||
* to implement custom initialization behavior.
|
||||
* WARNING: Do not manually call.
|
||||
*/
|
||||
virtual void onWidgetCreated();
|
||||
|
||||
protected:
|
||||
friend class UIManager;
|
||||
friend class UISceneNode;
|
||||
@@ -1418,14 +1427,6 @@ class EE_API UIWidget : public UINode {
|
||||
*/
|
||||
virtual void onAutoSize();
|
||||
|
||||
/**
|
||||
* @brief Handles widget creation events.
|
||||
*
|
||||
* Called after the widget is created and initialized. This can be overridden
|
||||
* to implement custom initialization behavior.
|
||||
*/
|
||||
virtual void onWidgetCreated();
|
||||
|
||||
/**
|
||||
* @brief Handles padding change events.
|
||||
*
|
||||
|
||||
@@ -14,6 +14,7 @@ std::string Markdown::toXHTML( std::string_view markdown, Dialect dialect, int f
|
||||
( dialect == Dialect::CommonMark ? MD_DIALECT_COMMONMARK : MD_DIALECT_GITHUB ) | flags;
|
||||
md_html( markdown.data(), markdown.size(), process_output, &out, dialectFlag,
|
||||
MD_HTML_FLAG_XHTML );
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,8 +21,8 @@ UILinearLayout* UILinearLayout::NewHorizontal() {
|
||||
return ( eeNew( UILinearLayout, () ) )->setOrientation( UIOrientation::Horizontal );
|
||||
}
|
||||
|
||||
UILinearLayout* UILinearLayout::NewVerticalWidthMatchParent() {
|
||||
return ( eeNew( UILinearLayout, () ) )
|
||||
UILinearLayout* UILinearLayout::NewVerticalWidthMatchParent( const std::string& tag ) {
|
||||
return ( eeNew( UILinearLayout, ( tag, UIOrientation::Vertical ) ) )
|
||||
->setLayoutWidthPolicy( SizePolicy::MatchParent )
|
||||
->asType<UILinearLayout>();
|
||||
}
|
||||
|
||||
@@ -29,7 +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() );
|
||||
printf( "%s", xhtml.c_str() );
|
||||
getUISceneNode()->loadLayoutFromString( xhtml, this );
|
||||
}
|
||||
|
||||
|
||||
@@ -344,7 +344,24 @@ void UIRichText::loadFromXmlNode( const pugi::xml_node& node ) {
|
||||
|
||||
UIWidget::loadFromXmlNode( node );
|
||||
|
||||
auto collapseXmlWhitespace = []( const String& text ) -> String {
|
||||
auto collapseXmlWhitespace = []( const String& text, const pugi::xml_node& node ) -> String {
|
||||
auto isInlineNode = []( const pugi::xml_node& node ) {
|
||||
if ( !node )
|
||||
return false;
|
||||
if ( node.type() == pugi::node_pcdata )
|
||||
return true;
|
||||
if ( node.type() != pugi::node_element )
|
||||
return false;
|
||||
std::string_view name( node.name() );
|
||||
return String::iequals( name, "a" ) || String::iequals( name, "span" ) ||
|
||||
String::iequals( name, "textspan" ) || String::iequals( name, "b" ) ||
|
||||
String::iequals( name, "i" ) || String::iequals( name, "strong" ) ||
|
||||
String::iequals( name, "em" ) || String::iequals( name, "s" ) ||
|
||||
String::iequals( name, "u" ) || String::iequals( name, "br" ) ||
|
||||
String::iequals( name, "code" ) || String::iequals( name, "img" ) ||
|
||||
String::iequals( name, "mark" );
|
||||
};
|
||||
|
||||
String res;
|
||||
res.reserve( text.size() );
|
||||
bool inSpace = false;
|
||||
@@ -360,6 +377,16 @@ void UIRichText::loadFromXmlNode( const pugi::xml_node& node ) {
|
||||
inSpace = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool prevInline = isInlineNode( node.previous_sibling() );
|
||||
bool nextInline = isInlineNode( node.next_sibling() );
|
||||
|
||||
if ( !prevInline && !res.empty() && res[0] == ' ' )
|
||||
res = res.substr( 1 );
|
||||
|
||||
if ( !nextInline && !res.empty() && res.back() == ' ' )
|
||||
res = res.substr( 0, res.size() - 1 );
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
@@ -390,14 +417,22 @@ void UIRichText::loadFromXmlNode( const pugi::xml_node& node ) {
|
||||
}
|
||||
} else {
|
||||
// Let parent logic load standard child widget
|
||||
UIWidget* widget = UIWidgetCreator::createFromName( child.name() );
|
||||
if ( widget ) {
|
||||
widget->setParent( this );
|
||||
widget->loadFromXmlNode( child );
|
||||
UIWidget* uiwidget = UIWidgetCreator::createFromName( child.name() );
|
||||
if ( uiwidget ) {
|
||||
uiwidget->setParent( this );
|
||||
uiwidget->loadFromXmlNode( child );
|
||||
|
||||
if ( !uiwidget->loadsItsChildren() ) {
|
||||
if ( child.first_child() && !uiwidget->loadsItsChildren() ) {
|
||||
getUISceneNode()->loadNode( child.first_child(), uiwidget, 0 );
|
||||
}
|
||||
}
|
||||
|
||||
uiwidget->onWidgetCreated();
|
||||
}
|
||||
}
|
||||
} else if ( child.type() == pugi::node_pcdata ) {
|
||||
String text = collapseXmlWhitespace( getTranslatorString( child.value() ) );
|
||||
String text = collapseXmlWhitespace( child.value(), child );
|
||||
if ( !text.empty() ) {
|
||||
UITextSpan* span = UITextSpan::New();
|
||||
span->setParent( this );
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <eepp/system/filesystem.hpp>
|
||||
#include <eepp/system/functionstring.hpp>
|
||||
#include <eepp/system/packmanager.hpp>
|
||||
#include <eepp/system/regex.hpp>
|
||||
#include <eepp/system/virtualfilesystem.hpp>
|
||||
#include <eepp/ui/css/mediaquery.hpp>
|
||||
#include <eepp/ui/css/stylesheetparser.hpp>
|
||||
@@ -430,7 +431,8 @@ UIWidget* UISceneNode::loadLayoutFromFile( const std::string& layoutPath, Node*
|
||||
const Uint32& marker ) {
|
||||
if ( FileSystem::fileExists( layoutPath ) ) {
|
||||
pugi::xml_document doc;
|
||||
pugi::xml_parse_result result = doc.load_file( layoutPath.c_str() );
|
||||
pugi::xml_parse_result result =
|
||||
doc.load_file( layoutPath.c_str(), pugi::parse_default | pugi::parse_ws_pcdata );
|
||||
|
||||
if ( result ) {
|
||||
return loadLayoutNodes( doc.first_child(), NULL != parent ? parent : this, marker );
|
||||
@@ -453,8 +455,19 @@ UIWidget* UISceneNode::loadLayoutFromFile( const std::string& layoutPath, Node*
|
||||
|
||||
UIWidget* UISceneNode::loadLayoutFromString( const char* layoutString, Node* parent,
|
||||
const Uint32& marker ) {
|
||||
thread_local static EE::System::RegEx voidTagsRegex(
|
||||
"(<(?:img|br|hr|input|meta|link)\\b[^>]*?)(?<!/)>" );
|
||||
|
||||
pugi::xml_document doc;
|
||||
pugi::xml_parse_result result = doc.load_string( layoutString );
|
||||
pugi::xml_parse_result result;
|
||||
|
||||
if ( voidTagsRegex.matches( layoutString ) ) {
|
||||
std::string 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 );
|
||||
}
|
||||
|
||||
if ( result ) {
|
||||
return loadLayoutNodes( doc.first_child(), NULL != parent ? parent : this, marker );
|
||||
@@ -474,8 +487,20 @@ UIWidget* UISceneNode::loadLayoutFromString( const std::string& layoutString, No
|
||||
|
||||
UIWidget* UISceneNode::loadLayoutFromMemory( const void* buffer, Int32 bufferSize, Node* parent,
|
||||
const Uint32& marker ) {
|
||||
thread_local static EE::System::RegEx voidTagsRegex(
|
||||
"(<(?:img|br|hr|input|meta|link)\\b[^>]*?)(?<!/)>" );
|
||||
|
||||
pugi::xml_document doc;
|
||||
pugi::xml_parse_result result = doc.load_buffer( buffer, bufferSize );
|
||||
pugi::xml_parse_result result;
|
||||
|
||||
if ( voidTagsRegex.matches( static_cast<const char*>( buffer ), 0, nullptr, bufferSize ) ) {
|
||||
std::string strBuffer( static_cast<const char*>( buffer ), bufferSize );
|
||||
std::string fixedLayout = voidTagsRegex.gsub( strBuffer, "%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 );
|
||||
}
|
||||
|
||||
if ( result ) {
|
||||
return loadLayoutNodes( doc.first_child(), NULL != parent ? parent : this, marker );
|
||||
@@ -497,12 +522,26 @@ UIWidget* UISceneNode::loadLayoutFromStream( IOStream& stream, Node* parent,
|
||||
TScopedBuffer<char> scopedBuffer( bufferSize );
|
||||
stream.read( scopedBuffer.get(), scopedBuffer.length() );
|
||||
|
||||
thread_local static EE::System::RegEx voidTagsRegex(
|
||||
"(<(?:img|br|hr|input|meta|link)\\b[^>]*?)(?<!/)>" );
|
||||
|
||||
pugi::xml_document doc;
|
||||
pugi::xml_parse_result result = doc.load_buffer( scopedBuffer.get(), scopedBuffer.length() );
|
||||
pugi::xml_parse_result result;
|
||||
|
||||
if ( voidTagsRegex.matches( scopedBuffer.get(), 0, nullptr, scopedBuffer.length() ) ) {
|
||||
std::string strBuffer( scopedBuffer.get(), scopedBuffer.length() );
|
||||
std::string fixedLayout = voidTagsRegex.gsub( strBuffer, "%1 />" );
|
||||
result = doc.load_buffer( fixedLayout.c_str(), fixedLayout.size(),
|
||||
pugi::parse_default | pugi::parse_ws_pcdata );
|
||||
} else {
|
||||
result = doc.load_buffer( scopedBuffer.get(), scopedBuffer.length(),
|
||||
pugi::parse_default | pugi::parse_ws_pcdata );
|
||||
}
|
||||
|
||||
if ( result ) {
|
||||
return loadLayoutNodes( doc.first_child(), NULL != parent ? parent : this, marker );
|
||||
} else {
|
||||
// Preserves the unique stream error log
|
||||
Log::error( "Couldn't load UI Layout from stream" );
|
||||
Log::error( "Error description: %s", result.description() );
|
||||
Log::error( "Error offset: %d", result.offset );
|
||||
|
||||
@@ -151,15 +151,19 @@ void UIWidgetCreator::createBaseWidgetList() {
|
||||
registeredWidget["h5"] = UIRichText::NewH5;
|
||||
registeredWidget["h6"] = UIRichText::NewH6;
|
||||
registeredWidget["br"] = UIRichText::NewBr;
|
||||
registeredWidget["ul"] = UILinearLayout::NewVerticalWidthMatchParent;
|
||||
registeredWidget["ol"] = UILinearLayout::NewVerticalWidthMatchParent;
|
||||
registeredWidget["ul"] = [] { return UILinearLayout::NewVerticalWidthMatchParent( "ul" ); };
|
||||
registeredWidget["ol"] = [] { return UILinearLayout::NewVerticalWidthMatchParent( "ol" ); };
|
||||
registeredWidget["li"] = UIRichText::NewListItem;
|
||||
registeredWidget["pre"] = UIRichText::NewPre;
|
||||
registeredWidget["img"] = [] { return UIImage::NewWithTag( "img" ); };
|
||||
registeredWidget["input"] = UITextInput::New;
|
||||
|
||||
registeredWidget["html"] = UILinearLayout::NewVerticalWidthMatchParent;
|
||||
registeredWidget["body"] = UILinearLayout::NewVerticalWidthMatchParent;
|
||||
registeredWidget["html"] = [] {
|
||||
return UILinearLayout::NewVerticalWidthMatchParent( "html" );
|
||||
};
|
||||
registeredWidget["body"] = [] {
|
||||
return UILinearLayout::NewVerticalWidthMatchParent( "body" );
|
||||
};
|
||||
|
||||
sBaseListCreated = true;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
EE_MAIN_FUNC int main( int, char** ) {
|
||||
UIApplication app( { 1280, 720, "eepp - UIMarkdownView Example" } );
|
||||
|
||||
Log::instance()->setLogLevelThreshold( LogLevel::Debug );
|
||||
Log::instance()->setLogToStdOut( true );
|
||||
Log::instance()->setLiveWrite( true );
|
||||
|
||||
app.getUI()->loadLayoutFromString( R"xml(
|
||||
<ScrollView layout_width="match_parent" layout_height="match_parent">
|
||||
<MarkdownView id="markdown_view" layout_width="match_parent" layout_height="wrap_content" padding="16dp">
|
||||
|
||||
@@ -635,3 +635,106 @@ UTEST( UIRichText, UIAnchorTest ) {
|
||||
eeDelete( sceneNode );
|
||||
Engine::destroySingleton();
|
||||
}
|
||||
|
||||
UTEST( UIRichText, WhitespaceCollapseTest ) {
|
||||
Engine::instance()->createWindow( WindowSettings( 800, 600, "RichText Test",
|
||||
WindowStyle::Default, WindowBackend::Default,
|
||||
32, {}, 1, false, true ) );
|
||||
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
|
||||
|
||||
FontTrueType* font = FontTrueType::New( "NotoSans-Regular" );
|
||||
font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
|
||||
|
||||
ASSERT_TRUE( font->loaded() );
|
||||
FontFamily::loadFromRegular( font );
|
||||
|
||||
UI::UISceneNode* sceneNode = UI::UISceneNode::New();
|
||||
UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager();
|
||||
themeManager->setDefaultFont( font );
|
||||
|
||||
String xml = R"xml(
|
||||
<RichText id="rt">
|
||||
<span>Hello</span>
|
||||
<ul>
|
||||
<li>Item</li>
|
||||
</ul>
|
||||
|
||||
</RichText>
|
||||
)xml";
|
||||
|
||||
sceneNode->loadLayoutFromString( xml );
|
||||
|
||||
UI::UIRichText* rt = sceneNode->find<UI::UIRichText>( "rt" );
|
||||
ASSERT_TRUE( rt != nullptr );
|
||||
|
||||
// force layout
|
||||
sceneNode->update( Time::Zero );
|
||||
|
||||
int spanCount = 0;
|
||||
Node* child = rt->getFirstChild();
|
||||
while ( child ) {
|
||||
if ( child->isWidget() && child->isType( UI_TYPE_TEXTSPAN ) ) {
|
||||
UI::UITextSpan* span = static_cast<UI::UITextSpan*>( child );
|
||||
if ( !span->getText().empty() ) {
|
||||
spanCount++;
|
||||
}
|
||||
}
|
||||
child = child->getNextNode();
|
||||
}
|
||||
|
||||
// Only 1 text span ("Hello") should be generated,
|
||||
// the whitespace between <span> and <ul>, and after <ul>
|
||||
// should be correctly collapsed into nothing since they aren't adjacent to inline elements on both sides.
|
||||
EXPECT_EQ( spanCount, 1 );
|
||||
|
||||
eeDelete( sceneNode );
|
||||
Engine::destroySingleton();
|
||||
}
|
||||
|
||||
UTEST( UIRichText, WhitespaceCollapseCodeTest ) {
|
||||
Engine::instance()->createWindow( WindowSettings( 800, 600, "RichText Test",
|
||||
WindowStyle::Default, WindowBackend::Default,
|
||||
32, {}, 1, false, true ) );
|
||||
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
|
||||
|
||||
FontTrueType* font = FontTrueType::New( "NotoSans-Regular" );
|
||||
font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
|
||||
|
||||
ASSERT_TRUE( font->loaded() );
|
||||
FontFamily::loadFromRegular( font );
|
||||
|
||||
UI::UISceneNode* sceneNode = UI::UISceneNode::New();
|
||||
UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager();
|
||||
themeManager->setDefaultFont( font );
|
||||
|
||||
String xml = R"xml(
|
||||
<vbox lw="mp" lh="mp">
|
||||
<RichText id="rt">Hello <a href="#">World</a>. <code>HI in monospace!</code></RichText>
|
||||
</vbox>
|
||||
)xml";
|
||||
|
||||
sceneNode->loadLayoutFromString( xml );
|
||||
|
||||
UI::UIRichText* rt = sceneNode->find<UI::UIRichText>( "rt" );
|
||||
ASSERT_TRUE( rt != nullptr );
|
||||
|
||||
// force layout
|
||||
sceneNode->update( Time::Zero );
|
||||
|
||||
bool foundDotSpace = false;
|
||||
Node* child = rt->getFirstChild();
|
||||
while ( child ) {
|
||||
if ( child->isWidget() && child->isType( UI_TYPE_TEXTSPAN ) ) {
|
||||
UI::UITextSpan* span = static_cast<UI::UITextSpan*>( child );
|
||||
if ( span->getText() == ". " ) {
|
||||
foundDotSpace = true;
|
||||
}
|
||||
}
|
||||
child = child->getNextNode();
|
||||
}
|
||||
|
||||
EXPECT_TRUE( foundDotSpace );
|
||||
|
||||
eeDelete( sceneNode );
|
||||
Engine::destroySingleton();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user