LCOV - code coverage report
Current view: top level - core/core/utils - SPHtmlParser.h (source / functions) Hit Total Coverage
Test: coverage.info Lines: 220 226 97.3 %
Date: 2024-05-12 00:16:13 Functions: 54 67 80.6 %

          Line data    Source code
       1             : /**
       2             : Copyright (c) 2016-2022 Roman Katuntsev <sbkarr@stappler.org>
       3             : Copyright (c) 2023 Stappler LLC <admin@stappler.dev>
       4             : 
       5             : Permission is hereby granted, free of charge, to any person obtaining a copy
       6             : of this software and associated documentation files (the "Software"), to deal
       7             : in the Software without restriction, including without limitation the rights
       8             : to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
       9             : copies of the Software, and to permit persons to whom the Software is
      10             : furnished to do so, subject to the following conditions:
      11             : 
      12             : The above copyright notice and this permission notice shall be included in
      13             : all copies or substantial portions of the Software.
      14             : 
      15             : THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
      16             : IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
      17             : FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
      18             : AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
      19             : LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
      20             : OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
      21             : THE SOFTWARE.
      22             : **/
      23             : 
      24             : #ifndef STAPPLER_CORE_UTILS_SPHTMLPARSER_H_
      25             : #define STAPPLER_CORE_UTILS_SPHTMLPARSER_H_
      26             : 
      27             : #include "SPString.h"
      28             : #include "SPStringView.h"
      29             : 
      30             : namespace STAPPLER_VERSIONIZED stappler::html {
      31             : 
      32             : /* Reader sample:
      33             : struct Reader {
      34             :         using Parser = html::Parser<Reader>;
      35             :         using Tag = Parser::Tag;
      36             :         using StringReader = Parser::StringReader;
      37             : 
      38             :         inline void onBeginTag(Parser &p, Tag &tag) {
      39             :                 log::debug("onBeginTag", tag.name);
      40             :         }
      41             : 
      42             :         inline void onEndTag(Parser &p, Tag &tag, bool isClosable) {
      43             :                 log::debug("onEndTag", tag.name);
      44             :         }
      45             : 
      46             :         inline void onTagAttribute(Parser &p, Tag &tag, StringReader &name, StringReader &value) {
      47             :                 log::debug("onTagAttribute", tag.name, ": ", name, " = ", value);
      48             :         }
      49             : 
      50             :         inline void onPushTag(Parser &p, Tag &tag) {
      51             :                 log::debug("onPushTag", tag.name);
      52             :         }
      53             : 
      54             :         inline void onPopTag(Parser &p, Tag &tag) {
      55             :                 log::debug("onPopTag", tag.name);
      56             :         }
      57             : 
      58             :         inline void onInlineTag(Parser &p, Tag &tag) {
      59             :                 log::debug("onInlineTag", tag.name);
      60             :         }
      61             : 
      62             :         inline void onTagContent(Parser &p, Tag &tag, StringReader &s) {
      63             :                 log::debug("onTagContent", tag.name, ": ", s);
      64             :         }
      65             : };
      66             : */
      67             : 
      68             : template <typename StringReader>
      69             : struct Tag;
      70             : 
      71             : template <typename ReaderType, typename StringReader = StringViewUtf8,
      72             :         typename TagType = typename std::conditional<
      73             :                 std::is_same<typename ReaderType::Tag, html::Tag<StringReader>>::value,
      74             :                 html::Tag<StringReader>,
      75             :                 typename ReaderType::Tag
      76             :         >::type>
      77             : void parse(ReaderType &r, const StringReader &s, bool rootOnly = true);
      78             : 
      79             : template <typename T>
      80             : struct ParserTraits {
      81             :         using success = char;
      82             :         using failure = long;
      83             : 
      84             :         InvokerCallTest_MakeCallTest(onBeginTag, success, failure);
      85             :         InvokerCallTest_MakeCallTest(onEndTag, success, failure);
      86             :         InvokerCallTest_MakeCallTest(onTagAttribute, success, failure);
      87             :         InvokerCallTest_MakeCallTest(onPushTag, success, failure);
      88             :         InvokerCallTest_MakeCallTest(onPopTag, success, failure);
      89             :         InvokerCallTest_MakeCallTest(onInlineTag, success, failure);
      90             :         InvokerCallTest_MakeCallTest(onTagContent, success, failure);
      91             :         InvokerCallTest_MakeCallTest(onReadTagName, success, failure);
      92             :         InvokerCallTest_MakeCallTest(onReadAttributeName, success, failure);
      93             :         InvokerCallTest_MakeCallTest(onReadAttributeValue, success, failure);
      94             :         InvokerCallTest_MakeCallTest(shouldParseTag, success, failure);
      95             : 
      96             :         InvokerCallTest_MakeCallTest(onSchemeTag, success, failure); // tags like <?tag ""?> or <!TAG tag>
      97             :         InvokerCallTest_MakeCallTest(onCommentTag, success, failure); // tags like <!-- -->
      98             :         InvokerCallTest_MakeCallTest(onTagAttributeList, success, failure); // string with all attributes
      99             : 
     100             :         InvokerCallTest_MakeCallTest(readTagContent, success, failure); // replace default content reader
     101             : };
     102             : 
     103             : template <typename StringReader>
     104             : auto Tag_readName(StringReader &is) -> StringReader;
     105             : 
     106             : template <typename StringReader>
     107             : auto Tag_readAttrName(StringReader &s) -> StringReader;
     108             : 
     109             : template <typename StringReader>
     110             : auto Tag_readAttrValue(StringReader &s) -> StringReader;
     111             : 
     112             : template <typename __StringReader>
     113             : struct Tag {
     114             :         using StringReader = __StringReader;
     115             : 
     116        7600 :         Tag(const StringReader &n) : name(n) {
     117        7600 :                 if (name.is('!')) {
     118           0 :                         closable = false;
     119             :                 }
     120        7600 :         }
     121             : 
     122        4875 :         const StringReader &getName() const { return name; }
     123             : 
     124        1075 :         void setClosable(bool v) { closable = v; }
     125       12200 :         bool isClosable() const { return closable; }
     126             : 
     127        7675 :         void setHasContent(bool v) { content = v; }
     128             :         bool hasContent() const { return content; }
     129             : 
     130        9050 :         bool isNestedTagsAllowed() const { return nestedTagsAllowed; }
     131             : 
     132             :         StringReader name;
     133             :         bool closable = true;
     134             :         bool content = false;
     135             :         bool nestedTagsAllowed = true;
     136             : };
     137             : 
     138             : template <typename ReaderType, typename __StringReader = StringViewUtf8,
     139             :                 typename TagType = typename html::Tag<__StringReader>,
     140             :                 typename Traits = ParserTraits<ReaderType>>
     141             : struct Parser {
     142             :         using StringReader = __StringReader;
     143             :         using OrigCharType = typename StringReader::CharType;
     144             :         using CharType = typename StringReader::MatchCharType;
     145             :         using Tag = TagType;
     146             : 
     147             :         template <CharType ... Args>
     148             :         using Chars = chars::Chars<CharType, Args...>;
     149             : 
     150             :         template <CharType First, CharType Last>
     151             :         using Range = chars::Chars<CharType, First, Last>;
     152             : 
     153             :         using GroupId = CharGroupId;
     154             : 
     155             :         template <GroupId G>
     156             :         using Group = chars::CharGroup<CharType, G>;
     157             : 
     158             :         using LtChar = Chars<CharType('<')>;
     159             : 
     160         625 :         Parser(ReaderType &r) : reader(&r) {
     161         625 :                 tagStack.reserve_block_optimal();
     162         625 :         }
     163             : 
     164          50 :         inline void cancel() {
     165          50 :                 current.clear();
     166          50 :                 canceled = true;
     167          50 :         }
     168             : 
     169             :         template <CharType C>
     170         150 :         void skipQuoted() {
     171         150 :                 if (current.template is<C>()) {
     172         150 :                         ++ current;
     173             :                 }
     174         350 :                 while (!current.empty() && !current.template is<C>()) {
     175         200 :                         current.template skipUntil<Chars<CharType('\\'), C>>();
     176         200 :                         if (current.is('\\')) {
     177          50 :                                 current += 2;
     178             :                         }
     179             :                 }
     180         150 :                 if (current.template is<C>()) {
     181         150 :                         ++ current;
     182             :                 }
     183         150 :         }
     184             : 
     185         625 :         bool parse(const StringReader &r, bool rootOnly) {
     186         625 :                 current = r;
     187       11425 :                 while (!current.empty()) {
     188       11125 :                         auto prefix = readTagContent();
     189       11125 :                         if (!prefix.empty()) {
     190        7625 :                                 if (!tagStack.empty()) {
     191        6150 :                                         tagStack.back().setHasContent(true);
     192        6150 :                                         onTagContent(tagStack.back(), prefix);
     193             :                                 } else {
     194        1475 :                                         StringReader r;
     195        1475 :                                         Tag t(r);
     196        1475 :                                         t.setHasContent(true);
     197        1475 :                                         onTagContent(t, prefix);
     198          75 :                                 }
     199             :                         }
     200             : 
     201       11125 :                         if (!current.is('<')) {
     202         325 :                                 break; // next tag not found
     203             :                         }
     204             : 
     205       10925 :                         ++ current; // drop '<'
     206       10925 :                         if (current.is('/')) { // close some parsed tag
     207        4650 :                                 ++ current; // drop '/'
     208             : 
     209        4650 :                                 auto tag = current.template readUntil<Chars<CharType('>')>>();
     210        4650 :                                 if (!tag.empty() && current.is('>') && !tagStack.empty()) {
     211        4625 :                                         tag.template trimChars<typename StringReader::WhiteSpace>();
     212        4625 :                                         auto it = tagStack.end();
     213             :                                         do {
     214        4875 :                                                 -- it;
     215        4875 :                                                 auto &name = it->getName();
     216        4875 :                                                 if (tag.size() == name.size() && tag.equals(name.data(), name.size())) {
     217             :                                                         // close all tag after <tag>
     218        4625 :                                                         auto nit = tagStack.end();
     219             :                                                         do {
     220        4875 :                                                                 -- nit;
     221        4875 :                                                                 onPopTag(*nit);
     222        4875 :                                                                 tagStack.pop_back();
     223        4875 :                                                         } while (nit != it);
     224        4625 :                                                         break;
     225             :                                                 }
     226         250 :                                         } while(it != tagStack.begin());
     227             : 
     228        4625 :                                         if (rootOnly && tagStack.empty()) {
     229         100 :                                                 if (current.is('>')) {
     230         100 :                                                         ++ current; // drop '>'
     231             :                                                 }
     232         100 :                                                 break;
     233             :                                         }
     234          25 :                                 } else if (current.empty()) {
     235          25 :                                         break; // fail to parse tag
     236             :                                 }
     237        4525 :                                 ++ current; // drop '>'
     238             :                         } else {
     239        6275 :                                 auto name = onReadTagName(current);
     240        6275 :                                 if (name.empty()) { // found tag without readable name
     241          25 :                                         current.template skipUntil<Chars<CharType('>')>>();
     242          25 :                                         if (current.is('>')) {
     243          25 :                                                 current ++;
     244             :                                         }
     245         175 :                                         continue;
     246             :                                 }
     247             : 
     248             :                                 if constexpr (sizeof(OrigCharType) == 2) {
     249             :                                         if (name.prefix(u"!--", u"!--"_len)) { // process comment
     250             :                                                 current.skipUntilString(u"-->", true);
     251             :                                                 onCommentTag(StringReader(name.data() + u"!--"_len,  current.data() - name.data() - u"!--"_len));
     252             :                                                 current += u"!--"_len;
     253             :                                                 continue;
     254             :                                         }
     255             :                                 } else {
     256        6250 :                                         if (name.prefix("!--", "!--"_len)) { // process comment
     257          25 :                                                 current.skipUntilString("-->", true);
     258          25 :                                                 auto tmp = StringReader(name.data() + "!--"_len, current.data() - name.data() - "!--"_len);
     259          25 :                                                 onCommentTag(tmp);
     260          25 :                                                 current += "!--"_len;
     261          25 :                                                 continue;
     262          25 :                                         }
     263             :                                 }
     264             : 
     265        6225 :                                 if (name.is('!') || name.is('?')) {
     266         125 :                                         StringReader cdata;
     267             :                                         if constexpr (sizeof(OrigCharType) == 2) {
     268             :                                                 if (current.starts_with(u"CDATA[")) {
     269             :                                                         cdata = current.readUntilString(u"]]>");
     270             :                                                         cdata += "CDATA["_len;
     271             :                                                         current += "]]>"_len;
     272             :                                                 }
     273             :                                         } else {
     274         125 :                                                 if (current.starts_with("CDATA[")) {
     275          50 :                                                         cdata = current.readUntilString("]]>");
     276          50 :                                                         cdata += "CDATA["_len;
     277          50 :                                                         current += "]]>"_len;
     278             :                                                 }
     279             :                                         }
     280             : 
     281         125 :                                         if (!cdata.empty()) {
     282          50 :                                                 if (!tagStack.empty()) {
     283          25 :                                                         tagStack.back().setHasContent(true);
     284          25 :                                                         onTagContent(tagStack.back(), cdata);
     285             :                                                 } else {
     286          25 :                                                         StringReader r;
     287          25 :                                                         Tag t(r);
     288          25 :                                                         t.setHasContent(true);
     289          25 :                                                         onTagContent(t, cdata);
     290           0 :                                                 }
     291          50 :                                                 continue;
     292          50 :                                         } else {
     293          75 :                                                 current.template skipChars<typename StringReader::WhiteSpace>();
     294          75 :                                                 auto tmp = current;
     295         175 :                                                 while (!current.empty() && !current.is('>')) {
     296         100 :                                                         current.template skipUntil<Chars<CharType('>'), CharType('"'), CharType('\'')>>();
     297         100 :                                                         if (current.is('\'')) {
     298          50 :                                                                 skipQuoted<CharType('\'')>();
     299          50 :                                                         } else if (current.is('"')) {
     300          50 :                                                                 skipQuoted<CharType('"')>();
     301             :                                                         }
     302             :                                                 }
     303          75 :                                                 if (current.is('>')) {
     304          75 :                                                         auto tag = StringReader(tmp.data(), current.data() - tmp.data());
     305          75 :                                                         onSchemeTag(name, tag);
     306          75 :                                                         ++ current;
     307             :                                                 }
     308          75 :                                                 continue;
     309          75 :                                         }
     310             :                                 }
     311             : 
     312        6100 :                                 TagType tag(name);
     313        6100 :                                 onBeginTag(tag);
     314             : 
     315        6100 :                                 StringReader attrStart = current;
     316        6100 :                                 StringReader attrName;
     317        6100 :                                 StringReader attrValue;
     318       17125 :                                 while (!current.empty() && !current.is('>') && !current.is('/')) {
     319       11025 :                                         attrName.clear();
     320       11025 :                                         attrValue.clear();
     321             : 
     322       11025 :                                         attrName = onReadAttributeName(current);
     323       11025 :                                         if (attrName.empty()) {
     324           0 :                                                 continue;
     325             :                                         }
     326             : 
     327       11025 :                                         attrValue = onReadAttributeValue(current);
     328       11025 :                                         onTagAttribute(tag, attrName, attrValue);
     329             :                                 }
     330             : 
     331        6100 :                                 attrStart = StringReader(attrStart.data(), current.data() - attrStart.data());
     332        6100 :                                 attrStart.template trimChars<typename StringReader::WhiteSpace>();
     333        6100 :                                 if (!attrStart.empty()) {
     334        4400 :                                         onTagAttributeList(tag, attrStart);
     335             :                                 }
     336             : 
     337        6100 :                                 if (current.is('/')) {
     338        1075 :                                         tag.setClosable(false);
     339             :                                 }
     340             : 
     341        6100 :                                 current.template skipUntil<Chars<CharType('>')>>();
     342        6100 :                                 if (current.is('>')) {
     343        6100 :                                         ++ current;
     344             :                                 }
     345             : 
     346        6100 :                                 onEndTag(tag, !tag.isClosable());
     347        6100 :                                 if (tag.isClosable()) {
     348        5025 :                                         onPushTag(tag);
     349        5025 :                                         tagStack.emplace_back(std::move(tag));
     350        5025 :                                         if (!shouldParseTag(tag)) {
     351          25 :                                                 auto start = current;
     352         125 :                                                 while (!current.empty()) {
     353         125 :                                                         current.template skipUntil<Chars<CharType('<')>>();
     354         125 :                                                         if (current.is('<')) {
     355         125 :                                                                 auto tmp = current.sub(1);
     356         125 :                                                                 if (tmp.is('/')) {
     357          50 :                                                                         ++ tmp;
     358          50 :                                                                         if (tmp.starts_with(tag.name)) {
     359          25 :                                                                                 tmp += tag.name.size();
     360          25 :                                                                                 tmp.template skipChars<Group<GroupId::WhiteSpace>>();
     361          25 :                                                                                 if (tmp.is('>')) {
     362          25 :                                                                                         StringReader content(start.data(), current.data() - start.data());
     363          25 :                                                                                         if (!content.empty()) {
     364          25 :                                                                                                 onTagContent(tag, content);
     365             :                                                                                         }
     366          25 :                                                                                         onPopTag(tag);
     367          25 :                                                                                         tagStack.pop_back();
     368             : 
     369          25 :                                                                                         ++ tmp;
     370          25 :                                                                                         current = tmp;
     371          25 :                                                                                         break;
     372             :                                                                                 }
     373             :                                                                         }
     374             :                                                                 }
     375         100 :                                                                 ++ current;
     376             :                                                         }
     377             :                                                 }
     378             :                                         }
     379             :                                 } else {
     380        1075 :                                         onInlineTag(tag);
     381             :                                 }
     382        1025 :                         }
     383             :                 }
     384             : 
     385         625 :                 if (!tagStack.empty()) {
     386          75 :                         auto nit = tagStack.end();
     387             :                         do {
     388         125 :                                 nit --;
     389         125 :                                 onPopTag(*nit);
     390         125 :                                 tagStack.pop_back();
     391         125 :                         } while (nit != tagStack.begin());
     392             :                 }
     393             : 
     394         625 :                 return !canceled;
     395             :         }
     396             : 
     397       11125 :         StringReader readTagContent() {
     398       11125 :                 auto tmp = current;
     399             : 
     400             :                 if constexpr (Traits::readTagContent) {
     401             :                         if (!tagStack.empty()) {
     402             :                                 reader->readTagContent(*this, tagStack.back(), current);
     403             :                                 return StringReader(tmp.data(), current.data() - tmp.data());
     404             :                         }
     405             :                 }
     406             : 
     407       11125 :                 bool nestedAllowed = true;
     408       11125 :                 if (!tagStack.empty()) {
     409        9050 :                         nestedAllowed = tagStack.back().isNestedTagsAllowed();
     410             :                 }
     411             : 
     412       18825 :                 while (!current.empty() && !current.is('<')) {
     413        7725 :                         current.template skipUntil<Chars<CharType('<'), CharType('\''), CharType('"')>>(); // move to next tag
     414        7725 :                         if (current.is('\'')) {
     415          25 :                                 skipQuoted<CharType('\'')>();
     416        7700 :                         } else if (current.is('"')) {
     417          25 :                                 skipQuoted<CharType('"')>();
     418        7675 :                         } else if (!nestedAllowed && current.is('<')) {
     419          75 :                                 if (current[1] == '/') {
     420          50 :                                         auto tag = current.sub(2);
     421          50 :                                         if (tag.starts_with(tagStack.back().name) && tag[tagStack.back().name.size()] == '>') {
     422          25 :                                                 break;
     423             :                                         }
     424             :                                 }
     425          50 :                                 ++ current;
     426             :                         }
     427             :                 }
     428             : 
     429       11125 :                 return StringReader(tmp.data(), current.data() - tmp.data());
     430             :         }
     431             : 
     432        6275 :         inline StringReader onReadTagName(StringReader &str) {
     433             :                 if constexpr (Traits::onReadTagName) {
     434             :                         StringReader ret(str);
     435             :                         reader->onReadTagName(*this, ret);
     436             :                         return ret;
     437             :                 } else {
     438        6275 :                         return Tag_readName(str);
     439             :                 }
     440             :         }
     441             : 
     442       11025 :         inline StringReader onReadAttributeName(StringReader &str) {
     443             :                 if constexpr (Traits::onReadAttributeName) {
     444             :                         StringReader ret(str);
     445             :                         reader->onReadAttributeName(*this, ret);
     446             :                         return ret;
     447             :                 } else {
     448       11025 :                         return Tag_readAttrName(str);
     449             :                 }
     450             :         }
     451             : 
     452       11025 :         inline StringReader onReadAttributeValue(StringReader &str) {
     453             :                 if constexpr (Traits::onReadAttributeValue) {
     454             :                         StringReader ret(str);
     455             :                         reader->onReadAttributeValue(*this, ret);
     456             :                         return ret;
     457             :                 } else {
     458       11025 :                         return Tag_readAttrValue(str);
     459             :                 }
     460             :         }
     461             : 
     462        6100 :         inline void onBeginTag(TagType &tag) {
     463        6100 :                 if constexpr (Traits::onBeginTag) { reader->onBeginTag(*this, tag); }
     464        6100 :         }
     465        6100 :         inline void onEndTag(TagType &tag, bool isClosed) {
     466        6100 :                 if constexpr (Traits::onEndTag) { reader->onEndTag(*this, tag, isClosed); }
     467        6100 :         }
     468       11025 :         inline void onTagAttribute(TagType &tag, StringReader &name, StringReader &value) {
     469       11025 :                 if constexpr (Traits::onTagAttribute) { reader->onTagAttribute(*this, tag, name, value); }
     470       11025 :         }
     471        5025 :         inline void onPushTag(TagType &tag) {
     472        5025 :                 if constexpr (Traits::onPushTag) { reader->onPushTag(*this, tag); }
     473        5025 :         }
     474        5025 :         inline void onPopTag(TagType &tag) {
     475        5025 :                 if constexpr (Traits::onPopTag) { reader->onPopTag(*this, tag); }
     476        5025 :         }
     477        1075 :         inline void onInlineTag(TagType &tag) {
     478        1075 :                 if constexpr (Traits::onInlineTag) { reader->onInlineTag(*this, tag); }
     479        1075 :         }
     480        7700 :         inline void onTagContent(TagType &tag, StringReader &s) {
     481        6600 :                 if constexpr (Traits::onTagContent) { reader->onTagContent(*this, tag, s); }
     482        7700 :         }
     483        5025 :         inline bool shouldParseTag(TagType &tag) {
     484         275 :                 if constexpr (Traits::shouldParseTag) { return reader->shouldParseTag(*this, tag); }
     485        4750 :                 return true;
     486             :         }
     487          75 :         inline void onSchemeTag(StringReader &name, StringReader &value) {
     488          75 :                 if constexpr (Traits::onSchemeTag) { return reader->onSchemeTag(*this, name, value); }
     489           0 :         }
     490          25 :         inline void onCommentTag(StringReader &comment) {
     491          25 :                 if constexpr (Traits::onCommentTag) { return reader->onCommentTag(*this, comment); }
     492           0 :         }
     493        4400 :         inline void onTagAttributeList(TagType &tag, StringReader &data) {
     494           0 :                 if constexpr (Traits::onTagAttributeList) { reader->onTagAttributeList(*this, tag, data); }
     495        4400 :         }
     496             : 
     497             :         bool canceled = false;
     498             :         ReaderType *reader;
     499             :         StringReader current;
     500             :         memory::vector<TagType> tagStack;
     501             : };
     502             : 
     503             : template <typename ReaderType, typename StringReader, typename TagType>
     504         625 : void parse(ReaderType &r, const StringReader &s, bool rootOnly) {
     505         625 :         html::Parser<ReaderType, StringReader, TagType> p(r);
     506         625 :         p.parse(s, rootOnly);
     507         625 : }
     508             : 
     509             : }
     510             : 
     511             : #endif /* STAPPLER_CORE_UTILS_SPHTMLPARSER_H_ */

Generated by: LCOV version 1.14