From fb54672815daa322bcb8b76aed120ef3decb8769 Mon Sep 17 00:00:00 2001 From: Nimetu Date: Fri, 5 Apr 2019 21:57:42 +0300 Subject: [PATCH] Changed: Implement HTML renderer with CSS styling --HG-- branch : develop --- code/nel/include/nel/gui/css_parser.h | 113 +- code/nel/include/nel/gui/css_selector.h | 107 + code/nel/include/nel/gui/css_style.h | 90 +- code/nel/include/nel/gui/group_html.h | 125 +- code/nel/include/nel/gui/html_element.h | 86 + code/nel/include/nel/gui/html_parser.h | 20 +- code/nel/include/nel/gui/libwww.h | 3 + code/nel/include/nel/misc/common.h | 11 + code/nel/src/gui/css_parser.cpp | 658 +++++ code/nel/src/gui/css_selector.cpp | 314 +++ code/nel/src/gui/css_style.cpp | 494 +++- code/nel/src/gui/group_html.cpp | 2438 +++++++++++++++-- code/nel/src/gui/html_element.cpp | 168 ++ code/nel/src/gui/html_parser.cpp | 188 +- code/nel/src/gui/libwww.cpp | 10 + code/nel/src/gui/libwww_types.cpp | 2 + .../src/interface_v3/group_quick_help.cpp | 58 +- .../src/interface_v3/group_quick_help.h | 3 +- 18 files changed, 4587 insertions(+), 301 deletions(-) create mode 100644 code/nel/include/nel/gui/css_selector.h create mode 100644 code/nel/include/nel/gui/html_element.h create mode 100644 code/nel/src/gui/css_selector.cpp create mode 100644 code/nel/src/gui/html_element.cpp diff --git a/code/nel/include/nel/gui/css_parser.h b/code/nel/include/nel/gui/css_parser.h index cb4cea62e..cfaf03caa 100644 --- a/code/nel/include/nel/gui/css_parser.h +++ b/code/nel/include/nel/gui/css_parser.h @@ -19,6 +19,7 @@ #include "nel/misc/types_nl.h" #include "nel/gui/css_style.h" +#include "nel/gui/css_selector.h" namespace NLGUI { @@ -31,8 +32,118 @@ namespace NLGUI public: // parse style declaration, eg "color: red; font-size: 10px;" static TStyle parseDecls(const std::string &styleString); - }; + // parse css stylesheet + void parseStylesheet(const std::string &cssString, std::vector &rules); + + private: + // stylesheet currently parsed + ucstring _Style; + // keep track of current position in _Style + size_t _Position; + + std::vector _Rules; + + private: + // @media ( .. ) { .. } + void readAtRule(); + + // a#id.class[attr=val] { .. } + void readRule(); + + // move past whitespace + void skipWhitespace(); + + // skip valid IDENT + bool skipIdentifier(); + + // skip over {}, (), or [] block + void skipBlock(); + + // skip over string quoted with ' or " + void skipString(); + + // backslash escape + void escape(); + + // normalize newline chars and remove comments + void preprocess(); + + // parse selectors + combinators + std::vector parse_selector(const ucstring &sel, std::string &pseudoElement) const; + + // parse selector and style + void parseRule(const ucstring &selectorString, const ucstring &styleString); + + inline bool is_eof() const + { + return _Position >= _Style.size(); + } + + inline bool is_whitespace(ucchar ch) const + { + return (ch == (ucchar)' ' || ch == (ucchar)'\t' || ch == (ucchar)'\n'); + } + + inline bool is_hex(ucchar ch) const + { + return ((ch >= (ucchar)'0' && ch <= (ucchar)'9') || + (ch >= (ucchar)'a' && ch <= (ucchar)'f') || + (ch >= (ucchar)'A' && ch <= (ucchar)'F')); + } + + inline bool maybe_escape() const + { + // escaping newline (\n) only allowed inside strings + return (_Style.size() - _Position) >= 1 && _Style[_Position] == (ucchar)'\\' && _Style[_Position+1] != '\n'; + } + + inline bool is_quote(ucchar ch) const + { + return ch== (ucchar)'"' || ch == (ucchar)'\''; + } + + inline bool is_block_open(ucchar ch) const + { + return ch == (ucchar)'{' || ch == (ucchar)'[' || ch == (ucchar)'('; + } + + inline bool is_block_close(ucchar ch, ucchar open) const + { + return ((open == '{' && ch == (ucchar)'}') || + (open == '[' && ch == (ucchar)']') || + (open == '(' && ch == (ucchar)')')); + } + + inline bool is_comment_open() const + { + if (_Position+1 > _Style.size()) + return false; + + return _Style[_Position] == (ucchar)'/' && _Style[_Position+1] == (ucchar)'*'; + } + + inline bool is_nonascii(ucchar ch) const + { + return ch >= 0x80 /*&& ch <= 255*/; + } + + inline bool is_alpha(ucchar ch) const + { + return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z'); + } + + inline bool is_digit(ucchar ch) const + { + return ch >= '0' && ch <= '9'; + } + + inline bool is_nmchar(ucchar ch) const + { + // checking escape here does not check if next char is '\n' or not + return ch == '_' || ch == '-' || is_alpha(ch) || is_digit(ch) || is_nonascii(ch) || ch == '\\'/*is_escape(ch)*/; + } + }; }//namespace #endif // CL_CSS_PARSER_H diff --git a/code/nel/include/nel/gui/css_selector.h b/code/nel/include/nel/gui/css_selector.h new file mode 100644 index 000000000..84b039089 --- /dev/null +++ b/code/nel/include/nel/gui/css_selector.h @@ -0,0 +1,107 @@ +// Ryzom - MMORPG Framework +// Copyright (C) 2010 Winch Gate Property Limited +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +#ifndef CL_CSS_SELECTOR_H +#define CL_CSS_SELECTOR_H + +#include "nel/misc/types_nl.h" + +namespace NLGUI +{ + class CHtmlElement; + + /** + * \brief CSS selector + * \date 2019-03-15 10:50 GMT + * \author Meelis Mägi (Nimetu) + */ + class CCssSelector + { + public: + enum ECombinator { + NONE = 0, + GENERAL_CHILD, + ADJACENT_SIBLING, + GENERAL_SIBLING, + CHILD_OF + }; + + struct SAttribute { + std::string key; + std::string value; + char op; // =, ~, |, ^, $, * + SAttribute(const std::string &k, const std::string &v, char o) + :key(k),value(v),op(o) + {} + }; + + std::string Element; + std::string Id; + std::vector Class; + std::vector Attr; + std::vector PseudoClass; + + // css combinator or \0 missing (first element) + char Combinator; + + public: + // TODO: rewrite for ECombinator enum + CCssSelector(std::string elm="", std::string id="", std::string cls="", char comb = '\0'); + + // helper for sorting + uint32 specificity() const; + + // set classes used, eg 'class1 class2' + void setClass(const std::string &cls); + + // add attribute to selector + // ' ' op means 'key exists, ignore value' + void addAttribute(const std::string &key, const std::string &val = "", char op = ' '); + + // add pseudo class to selector, eg 'first-child' + void addPseudoClass(const std::string &key); + + // true if no rules have been defined + bool empty() const + { + return Element.empty() && Id.empty() && Class.empty() && Attr.empty() && PseudoClass.empty(); + } + + // Test current selector to html DOM element + // NOTE: Does not check combinator + bool match(const CHtmlElement &elm) const; + + private: + bool matchClass(const CHtmlElement &elm) const; + bool matchAttributes(const CHtmlElement &elm) const; + bool matchPseudoClass(const CHtmlElement &elm) const; + + // match An+B rule to child index (1 based) + bool matchNth(sint childNr, sint a, sint b) const; + + // parse nth-child string to 'a' and 'b' components + // :nth-child(odd) + // :nth-child(even) + // :nth-child(An+B) + // :nth-child(-An+b) + void parseNth(const std::string &pseudo, sint &a, sint &b) const; + + }; + +}//namespace + +#endif // CL_CSS_SELECTOR_H + diff --git a/code/nel/include/nel/gui/css_style.h b/code/nel/include/nel/gui/css_style.h index 2a854a6d9..dea4a1f43 100644 --- a/code/nel/include/nel/gui/css_style.h +++ b/code/nel/include/nel/gui/css_style.h @@ -19,9 +19,12 @@ #include "nel/misc/types_nl.h" #include "nel/misc/rgba.h" +#include "nel/gui/css_selector.h" namespace NLGUI { + class CHtmlElement; + typedef std::map TStyle; /** @@ -44,7 +47,7 @@ namespace NLGUI sint32 X; sint32 Y; NLMISC::CRGBA Color; - }; + }; public: CStyleParams () : FontFamily(""), TextColor(255,255,255,255), TextShadow() { @@ -62,6 +65,19 @@ namespace NLGUI BackgroundColor=NLMISC::CRGBA::Black; BackgroundColorOver=NLMISC::CRGBA::Black; } + + bool hasStyle(const std::string &key) const + { + return StyleRules.find(key) != StyleRules.end(); + } + + std::string getStyle(const std::string &key) const + { + TStyle::const_iterator it = StyleRules.find(key); + return (it != StyleRules.end() ? it->second : ""); + } + + public: uint FontSize; uint FontWeight; bool FontOblique; @@ -78,11 +94,26 @@ namespace NLGUI sint32 BorderWidth; NLMISC::CRGBA BackgroundColor; NLMISC::CRGBA BackgroundColorOver; + + std::string WhiteSpace; + std::string TextAlign; + std::string VerticalAlign; + + TStyle StyleRules; }; class CCssStyle { public: + struct SStyleRule { + std::vector Selector; + TStyle Properties; + // pseudo element like ':before' + std::string PseudoElement; + + // returns selector specificity + uint specificity() const; + }; // 'browser' style, overwriten with '' CStyleParams Root; @@ -90,18 +121,37 @@ namespace NLGUI // current element style CStyleParams Current; + // known style rules sorted by specificity + std::vector _StyleRules; + private: std::vector _StyleStack; - // test if str is one of "thin/medium/thick" and return its pixel value + // test if str is one of "thin/medium/thick" and return its pixel value bool scanCssLength(const std::string& str, uint32 &px) const; // read style attribute void getStyleParams(const std::string &styleString, CStyleParams &style, const CStyleParams ¤t) const; + void getStyleParams(const TStyle &styleRules, CStyleParams &style, const CStyleParams ¤t) const; + + // merge src into dest by overwriting key in dest + void merge(TStyle &dst, const TStyle &src) const; + + // match selector to dom path + bool match(const std::vector &selector, const CHtmlElement &elm) const; + + // parse 'background' into 'background-color', 'background-image', etc + void parseBackgroundShorthand(const std::string &value, CStyleParams &style) const; public: void reset(); + // parse tag or css file content + void parseStylesheet(const std::string &styleString); + + // set element style from matching css rules + void getStyleFor(CHtmlElement &elm) const; + inline uint getFontSizeSmaller() const { if (Current.FontSize < 5) @@ -109,15 +159,29 @@ namespace NLGUI return Current.FontSize-2; } + sint styleStackIndex = 0; + inline void pushStyle() { + styleStackIndex++; _StyleStack.push_back(Current); + + Current.Width=-1; + Current.Height=-1; + Current.MaxWidth=-1; + Current.MaxHeight=-1; + Current.BorderWidth=1; + + Current.StyleRules.clear(); } inline void popStyle() { + styleStackIndex--; if (_StyleStack.empty()) + { Current = Root; + } else { Current = _StyleStack.back(); @@ -125,16 +189,32 @@ namespace NLGUI } } - // apply style string to this.Root + // apply style to this.Root void applyRootStyle(const std::string &styleString); + void applyRootStyle(const TStyle &styleRules); - // apply style string to this.Current + // apply style to this.Current void applyStyle(const std::string &styleString); + void applyStyle(const TStyle &styleRules); void applyCssMinMax(sint32 &width, sint32 &height, sint32 minw=0, sint32 minh=0, sint32 maxw=0, sint32 maxh=0) const; - }; + // check if current style property matches value + bool checkStyle(const std::string &key, const std::string &val) const + { + return Current.hasStyle(key) && Current.getStyle(key) == val; + } + bool hasStyle(const std::string &key) const + { + return Current.hasStyle(key); + } + + std::string getStyle(const std::string &key) const + { + return Current.getStyle(key); + } + }; }//namespace #endif // CL_CSS_STYLE_H diff --git a/code/nel/include/nel/gui/group_html.h b/code/nel/include/nel/gui/group_html.h index d8935f431..6a8760533 100644 --- a/code/nel/include/nel/gui/group_html.h +++ b/code/nel/include/nel/gui/group_html.h @@ -24,6 +24,7 @@ #include "nel/gui/ctrl_button.h" #include "nel/gui/group_table.h" #include "nel/gui/libwww_types.h" +#include "nel/gui/html_element.h" #include "nel/gui/css_style.h" // forward declaration @@ -76,7 +77,7 @@ namespace NLGUI static SWebOptions options; // ImageDownload system - enum TDataType {ImgType= 0, BnpType}; + enum TDataType {ImgType= 0, BnpType, StylesheetType}; enum TImageType {NormalImage=0, OverImage}; // Constructor @@ -97,6 +98,9 @@ namespace NLGUI // Browse virtual void browse (const char *url); + // load css from local file and insert into active stylesheet collection + void parseStylesheetFile(const std::string &fname); + // parse html string using libxml2 parser bool parseHtml(const std::string &htmlString); @@ -273,16 +277,10 @@ namespace NLGUI virtual void addText (const char * buf, int len); // A new begin HTML element has been parsed ( for exemple) - virtual void beginElement (uint element_number, const std::vector &present, const std::vector &value); + virtual void beginElement(CHtmlElement &elm); // A new end HTML element has been parsed ( for exemple) - virtual void endElement (uint element_number); - - // A new begin unparsed element has been found - virtual void beginUnparsedElement(const char *buffer, int length); - - // A new end unparsed element has been found - virtual void endUnparsedElement(const char *buffer, int length); + virtual void endElement(CHtmlElement &elm); // Add GET params to the url virtual void addHTTPGetParams (std::string &url, bool trustedDomain); @@ -296,6 +294,9 @@ namespace NLGUI // Get Home URL virtual std::string home(); + // parse dom node and all child nodes recursively + void renderDOM(CHtmlElement &elm); + // Clear style stack and restore default style void resetCssStyle(); @@ -326,7 +327,7 @@ namespace NLGUI void addString(const ucstring &str); // Add an image in the current paragraph - void addImage(const std::string &id, const char *image, bool reloadImg=false, const CStyleParams &style = CStyleParams()); + void addImage(const std::string &id, const std::string &img, bool reloadImg=false, const CStyleParams &style = CStyleParams()); // Add a text area in the current paragraph CInterfaceGroup *addTextArea (const std::string &templateName, const char *name, uint rows, uint cols, bool multiLine, const ucstring &content, uint maxlength); @@ -365,6 +366,14 @@ namespace NLGUI // Current URL std::string _DocumentUrl; std::string _DocumentDomain; + std::string _DocumentHtml; // not updated, only set by first render + // If true, then render _DocumentHtml on next update (replaces content) + bool _RenderNextTime; + // true if renderer is waiting for css files to finish downloading (link rel=stylesheet) + bool _WaitingForStylesheet; + // list of css file urls that are queued up for download + std::vector _StylesheetQueue; + // Valid base href was found bool _IgnoreBaseUrlTag; // Fragment from loading url @@ -484,6 +493,11 @@ namespace NLGUI // Keep track of current element style CCssStyle _Style; + CHtmlElement _HtmlDOM; + CHtmlElement *_CurrentHTMLElement; + // Backup of CurrentHTMLElement->nextSibling before ::beginElement() is called + // for luaParseHtml() to insert nodes into right place in right order + CHtmlElement *_CurrentHTMLNextSibling; // Current link std::vector _Link; @@ -722,6 +736,12 @@ namespace NLGUI private: friend class CHtmlParser; + // TODO: beginElement is overwritten in client quick help class, merge it here? + void beginElementDeprecated(uint element_number, const std::vector &present, const std::vector &value); + void endElementDeprecated(uint element_number); + + // move src->Children into CurrentHtmlElement.parent.children element + void spliceFragment(std::list::iterator src); // decode all HTML entities static ucstring decodeHTMLEntities(const ucstring &str); @@ -764,6 +784,8 @@ namespace NLGUI int RunningCurls; bool startCurlDownload(CDataDownload &download); + void finishCurlDownload(CDataDownload &download); + void pumpCurlDownloads(); void initImageDownload(); void checkImageDownload(); @@ -782,11 +804,94 @@ namespace NLGUI bool addBnpDownload(std::string url, const std::string &action, const std::string &script, const std::string &md5sum); std::string localBnpName(const std::string &url); + // add css file from to download queue + void addStylesheetDownload(std::vector links); + void releaseDownloads(); void checkDownloads(); // HtmlType download finished void htmlDownloadFinished(const std::string &content, const std::string &type, long code); + + // stylesheet finished downloading. if local file does not exist, then it failed (404) + void cssDownloadFinished(const std::string &url, const std::string &local); + + // read common table/tr/td parameters and push them to _CellParams + void getCellsParameters(const CHtmlElement &elm, bool inherit); + + // render _HtmlDOM + void renderDocument(); + + // :before, :after rendering + void renderPseudoElement(const std::string &pseudo, const CHtmlElement &elm); + + // HTML elements + void htmlA(const CHtmlElement &elm); + void htmlAend(const CHtmlElement &elm); + void htmlBASE(const CHtmlElement &elm); + void htmlBODY(const CHtmlElement &elm); + void htmlBR(const CHtmlElement &elm); + void htmlDD(const CHtmlElement &elm); + void htmlDDend(const CHtmlElement &elm); + //void htmlDEL(const CHtmlElement &elm); + void htmlDIV(const CHtmlElement &elm); + void htmlDIVend(const CHtmlElement &elm); + void htmlDL(const CHtmlElement &elm); + void htmlDLend(const CHtmlElement &elm); + void htmlDT(const CHtmlElement &elm); + void htmlDTend(const CHtmlElement &elm); + //void htmlEM(const CHtmlElement &elm); + void htmlFONT(const CHtmlElement &elm); + void htmlFORM(const CHtmlElement &elm); + void htmlH(const CHtmlElement &elm); + void htmlHend(const CHtmlElement &elm); + void htmlHEAD(const CHtmlElement &elm); + void htmlHEADend(const CHtmlElement &elm); + void htmlHR(const CHtmlElement &elm); + void htmlHTML(const CHtmlElement &elm); + void htmlI(const CHtmlElement &elm); + void htmlIend(const CHtmlElement &elm); + void htmlIMG(const CHtmlElement &elm); + void htmlINPUT(const CHtmlElement &elm); + void htmlLI(const CHtmlElement &elm); + void htmlLIend(const CHtmlElement &elm); + void htmlLUA(const CHtmlElement &elm); + void htmlLUAend(const CHtmlElement &elm); + void htmlMETA(const CHtmlElement &elm); + void htmlOBJECT(const CHtmlElement &elm); + void htmlOBJECTend(const CHtmlElement &elm); + void htmlOL(const CHtmlElement &elm); + void htmlOLend(const CHtmlElement &elm); + void htmlOPTION(const CHtmlElement &elm); + void htmlOPTIONend(const CHtmlElement &elm); + void htmlP(const CHtmlElement &elm); + void htmlPend(const CHtmlElement &elm); + void htmlPRE(const CHtmlElement &elm); + void htmlPREend(const CHtmlElement &elm); + void htmlSCRIPT(const CHtmlElement &elm); + void htmlSCRIPTend(const CHtmlElement &elm); + void htmlSELECT(const CHtmlElement &elm); + void htmlSELECTend(const CHtmlElement &elm); + //void htmlSMALL(const CHtmlElement &elm); + //void htmlSPAN(const CHtmlElement &elm); + //void htmlSTRONG(const CHtmlElement &elm); + void htmlSTYLE(const CHtmlElement &elm); + void htmlSTYLEend(const CHtmlElement &elm); + void htmlTABLE(const CHtmlElement &elm); + void htmlTABLEend(const CHtmlElement &elm); + void htmlTD(const CHtmlElement &elm); + void htmlTDend(const CHtmlElement &elm); + void htmlTEXTAREA(const CHtmlElement &elm); + void htmlTEXTAREAend(const CHtmlElement &elm); + void htmlTH(const CHtmlElement &elm); + void htmlTHend(const CHtmlElement &elm); + void htmlTITLE(const CHtmlElement &elm); + void htmlTITLEend(const CHtmlElement &elm); + void htmlTR(const CHtmlElement &elm); + void htmlTRend(const CHtmlElement &elm); + //void htmlU(const CHtmlElement &elm); + void htmlUL(const CHtmlElement &elm); + void htmlULend(const CHtmlElement &elm); }; // adapter group that store y offset for inputs inside an html form diff --git a/code/nel/include/nel/gui/html_element.h b/code/nel/include/nel/gui/html_element.h new file mode 100644 index 000000000..bac681c1c --- /dev/null +++ b/code/nel/include/nel/gui/html_element.h @@ -0,0 +1,86 @@ +// Ryzom - MMORPG Framework +// Copyright (C) 2010 Winch Gate Property Limited +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +#ifndef CL_HTML_ELEMENT_H +#define CL_HTML_ELEMENT_H + +#include "nel/misc/types_nl.h" +#include "nel/gui/css_style.h" + +namespace NLGUI +{ + /** + * \brief HTML element + * \date 2019-04-25 18:23 GMT + * \author Meelis Mägi (Nimetu) + */ + class CHtmlElement + { + public: + enum ENodeType { + NONE = 0, + ELEMENT_NODE = 1, + TEXT_NODE = 3, + }; + + uint ID; // libwww element enum + ENodeType Type; + std::string Value; // text node value or element node name + std::map Attributes; + std::list Children; + + // class names for css matching + std::set ClassNames; + + // defined style and :before/:after pseudo elements + TStyle Style; + TStyle StyleBefore; + TStyle StyleAfter; + + // hierarchy + CHtmlElement *parent; + CHtmlElement *previousSibling; + CHtmlElement *nextSibling; + + // n'th ELEMENT_NODE in parent.Children, for :nth-child() rules + uint childIndex; + + CHtmlElement(ENodeType type = NONE, std::string value = ""); + + // returns true if rhs is same pointer + friend bool operator==(const CHtmlElement &lhs, const CHtmlElement &rhs) + { + return &lhs == &rhs; + } + + bool hasAttribute(const std::string &key) const; + + bool hasNonEmptyAttribute(const std::string &key) const; + + std::string getAttribute(const std::string &key) const; + + bool hasClass(const std::string &key) const; + + // update Children index/parent/next/prevSibling pointers + void reindexChilds(); + + // debug + std::string toString(bool tree = false, uint depth = 0) const; + }; +} + +#endif + diff --git a/code/nel/include/nel/gui/html_parser.h b/code/nel/include/nel/gui/html_parser.h index 132c4ac88..760640234 100644 --- a/code/nel/include/nel/gui/html_parser.h +++ b/code/nel/include/nel/gui/html_parser.h @@ -21,7 +21,7 @@ namespace NLGUI { - class CGroupHTML; + class CHtmlElement; /** * \brief HTML parsing @@ -31,21 +31,21 @@ namespace NLGUI class CHtmlParser { public: - CHtmlParser(CGroupHTML *group) : _GroupHtml(group) - {} + bool parseHtml(std::string htmlString) const; - bool parseHtml(std::string htmlString); + // parse html string into DOM, extract + bool useStyle = true; + if (elm.hasAttribute("media")) + { + std::string media = trim(toLower(elm.Attributes["media"])); + useStyle = media.empty() || media.find("all") != std::string::npos || media.find("screen") != std::string::npos; + + //