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_ */
|