Line data Source code
1 : /**
2 : Copyright (c) 2020-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 : #include "SPHtmlParser.h"
25 : #include "SPUrl.h"
26 : #include "SPSearchConfiguration.h"
27 : #include "SPSnowballStopwords.cc"
28 :
29 : namespace STAPPLER_VERSIONIZED stappler::search {
30 :
31 : struct StemmerEnv {
32 : using symbol = unsigned char;
33 :
34 : void *(*memalloc)( void *userData, unsigned int size );
35 : void (*memfree)( void *userData, void *ptr );
36 : void* userData; // User data passed to the allocator functions.
37 :
38 : int (*stem)(StemmerEnv *);
39 :
40 : symbol * p;
41 : int c; int l; int lb; int bra; int ket;
42 : symbol * * S;
43 : int * I;
44 : unsigned char * B;
45 :
46 : const StringView *stopwords;
47 : struct stemmer_modules *mod;
48 : };
49 :
50 : struct stemmer_modules {
51 : Language name;
52 : StemmerEnv * (*create)(StemmerEnv *);
53 : void (*close)(StemmerEnv *);
54 : int (*stem)(StemmerEnv *);
55 : };
56 :
57 : SP_EXTERN_C struct stemmer_modules * sb_stemmer_get(Language lang);
58 : SP_EXTERN_C const unsigned char * sb_stemmer_stem(StemmerEnv * z, const unsigned char * word, int size);
59 :
60 900 : static const StringView *getLanguageStopwords(Language lang) {
61 900 : switch (lang) {
62 0 : case Language::Unknown: return nullptr; break;
63 25 : case Language::Arabic: return nullptr; break;
64 25 : case Language::Danish: return s_danish_stopwords; break;
65 25 : case Language::Dutch: return s_dutch_stopwords; break;
66 200 : case Language::English: return s_english_stopwords; break;
67 25 : case Language::Finnish: return s_finnish_stopwords; break;
68 25 : case Language::French: return s_french_stopwords; break;
69 25 : case Language::German: return s_german_stopwords; break;
70 25 : case Language::Greek: return nullptr; break;
71 25 : case Language::Hungarian: return s_hungarian_stopwords; break;
72 25 : case Language::Indonesian: return nullptr; break;
73 25 : case Language::Irish: return nullptr; break;
74 25 : case Language::Italian: return s_italian_stopwords; break;
75 25 : case Language::Lithuanian: return nullptr; break;
76 25 : case Language::Nepali: return s_nepali_stopwords; break;
77 25 : case Language::Norwegian: return s_norwegian_stopwords; break;
78 25 : case Language::Portuguese: return s_portuguese_stopwords; break;
79 25 : case Language::Romanian: return nullptr; break;
80 175 : case Language::Russian: return s_russian_stopwords; break;
81 25 : case Language::Spanish: return s_spanish_stopwords; break;
82 25 : case Language::Swedish: return s_swedish_stopwords; break;
83 25 : case Language::Tamil: return nullptr; break;
84 25 : case Language::Turkish: return s_turkish_stopwords; break;
85 25 : case Language::Simple: return nullptr; break;
86 : }
87 0 : return nullptr;
88 : }
89 :
90 25 : StringView SearchData::getLanguage() const {
91 25 : return getLanguageName(language);
92 : }
93 :
94 600 : bool isStopword(const StringView &word, Language lang) {
95 600 : if (lang == Language::Unknown) {
96 25 : lang = detectLanguage(word);
97 25 : if (lang == Language::Unknown) {
98 0 : return false;
99 : }
100 : }
101 :
102 600 : if (auto dict = getLanguageStopwords(lang)) {
103 400 : return isStopword(word, dict);
104 : }
105 :
106 200 : return false;
107 : }
108 :
109 1325 : StringView getLanguageName(Language lang) {
110 1325 : switch (lang) {
111 50 : case Language::Unknown: return StringView(); break;
112 25 : case Language::Arabic: return StringView("arabic"); break;
113 25 : case Language::Danish: return StringView("danish"); break;
114 25 : case Language::Dutch: return StringView("dutch"); break;
115 200 : case Language::English: return StringView("english"); break;
116 25 : case Language::Finnish: return StringView("finnish"); break;
117 25 : case Language::French: return StringView("french"); break;
118 25 : case Language::German: return StringView("german"); break;
119 25 : case Language::Greek: return StringView("greek"); break;
120 25 : case Language::Hungarian: return StringView("hungarian"); break;
121 25 : case Language::Indonesian: return StringView("indonesian"); break;
122 25 : case Language::Irish: return StringView("irish"); break;
123 25 : case Language::Italian: return StringView("italian"); break;
124 25 : case Language::Lithuanian: return StringView("lithuanian"); break;
125 25 : case Language::Nepali: return StringView("nepali"); break;
126 25 : case Language::Norwegian: return StringView("norwegian"); break;
127 25 : case Language::Portuguese: return StringView("portuguese"); break;
128 25 : case Language::Romanian: return StringView("romanian"); break;
129 250 : case Language::Russian: return StringView("russian"); break;
130 25 : case Language::Spanish: return StringView("spanish"); break;
131 25 : case Language::Swedish: return StringView("swedish"); break;
132 25 : case Language::Tamil: return StringView("tamil"); break;
133 25 : case Language::Turkish: return StringView("turkish"); break;
134 325 : case Language::Simple: return StringView("simple"); break;
135 : }
136 0 : return StringView();
137 : }
138 :
139 600 : Language parseLanguage(const StringView &lang) {
140 600 : if (lang == "arabic") { return Language::Arabic; }
141 575 : else if (lang == "danish") { return Language::Danish; }
142 550 : else if (lang == "dutch") { return Language::Dutch; }
143 525 : else if (lang == "english") { return Language::English; }
144 500 : else if (lang == "finnish") { return Language::Finnish; }
145 475 : else if (lang == "french") { return Language::French; }
146 450 : else if (lang == "german") { return Language::German; }
147 425 : else if (lang == "greek") { return Language::Greek; }
148 400 : else if (lang == "hungarian") { return Language::Hungarian; }
149 375 : else if (lang == "indonesian") { return Language::Indonesian; }
150 350 : else if (lang == "irish") { return Language::Irish; }
151 325 : else if (lang == "italian") { return Language::Italian; }
152 300 : else if (lang == "nepali") { return Language::Nepali; }
153 275 : else if (lang == "norwegian") { return Language::Norwegian; }
154 250 : else if (lang == "portuguese") { return Language::Portuguese; }
155 225 : else if (lang == "romanian") { return Language::Romanian; }
156 200 : else if (lang == "russian") { return Language::Russian; }
157 175 : else if (lang == "spanish") { return Language::Spanish; }
158 150 : else if (lang == "swedish") { return Language::Swedish; }
159 125 : else if (lang == "tamil") { return Language::Tamil; }
160 100 : else if (lang == "turkish") { return Language::Turkish; }
161 75 : else if (lang == "simple") { return Language::Simple; }
162 50 : return Language::Unknown;
163 : }
164 :
165 350 : Language detectLanguage(const StringView &word) {
166 350 : StringView str(word);
167 350 : str.skipUntil<StringView::CharGroup<CharGroupId::Numbers>, StringView::Chars<'.'>>();
168 350 : if (str.empty()) {
169 200 : StringViewUtf8 r(word.data(), word.size());
170 200 : while (!r.empty()) {
171 : r.skipUntil<StringViewUtf8::MatchCharGroup<CharGroupId::Latin>,
172 : StringViewUtf8::MatchCharGroup<CharGroupId::Cyrillic>,
173 : StringViewUtf8::MatchCharGroup<CharGroupId::GreekBasic>,
174 175 : StringViewUtf8::MatchCharGroup<CharGroupId::Numbers>>();
175 175 : if (r.is<StringViewUtf8::MatchCharGroup<CharGroupId::Latin>>()) {
176 50 : return Language::English;
177 125 : } else if (r.is<StringViewUtf8::MatchCharGroup<CharGroupId::Cyrillic>>()) {
178 100 : return Language::Russian;
179 25 : } else if (r.is<StringViewUtf8::MatchCharGroup<CharGroupId::GreekBasic>>()) {
180 25 : return Language::Greek;
181 : }
182 : }
183 25 : return Language::Unknown;
184 : } else {
185 150 : return Language::Simple;
186 : }
187 : }
188 :
189 475 : StringView getParserTokenName(ParserToken tok) {
190 475 : switch (tok) {
191 25 : case ParserToken::AsciiWord: return "AsciiWord"; break;
192 25 : case ParserToken::Word: return "Word"; break;
193 25 : case ParserToken::NumWord: return "NumWord"; break;
194 25 : case ParserToken::Email: return "Email"; break;
195 25 : case ParserToken::Url: return "Url"; break;
196 25 : case ParserToken::ScientificFloat: return "ScientificFloat"; break;
197 25 : case ParserToken::Version: return "Version"; break;
198 25 : case ParserToken::HyphenatedWord_NumPart: return "HyphenatedWord_NumPart"; break;
199 25 : case ParserToken::HyphenatedWord_Part: return "HyphenatedWord_Part"; break;
200 25 : case ParserToken::HyphenatedWord_AsciiPart: return "HyphenatedWord_AsciiPart"; break;
201 25 : case ParserToken::Blank: return "Blank"; break;
202 25 : case ParserToken::NumHyphenatedWord: return "NumHyphenatedWord"; break;
203 25 : case ParserToken::AsciiHyphenatedWord: return "AsciiHyphenatedWord"; break;
204 25 : case ParserToken::HyphenatedWord: return "HyphenatedWord"; break;
205 25 : case ParserToken::Path: return "Path"; break;
206 25 : case ParserToken::Float: return "Float"; break;
207 25 : case ParserToken::Integer: return "Integer"; break;
208 25 : case ParserToken::XMLEntity: return "XMLEntity"; break;
209 25 : case ParserToken::Custom: return "Custom"; break;
210 : }
211 0 : return StringView();
212 : }
213 :
214 115475 : bool isWordPart(ParserToken tok) {
215 115475 : switch (tok) {
216 1250 : case ParserToken::HyphenatedWord_NumPart:
217 : case ParserToken::HyphenatedWord_Part:
218 : case ParserToken::HyphenatedWord_AsciiPart:
219 1250 : return true;
220 : break;
221 114225 : default:
222 114225 : break;
223 : }
224 114225 : return false;
225 : }
226 :
227 8575 : bool isComplexWord(ParserToken tok) {
228 8575 : switch (tok) {
229 100 : case ParserToken::NumHyphenatedWord:
230 : case ParserToken::AsciiHyphenatedWord:
231 : case ParserToken::HyphenatedWord:
232 100 : return true;
233 : break;
234 8475 : default:
235 8475 : break;
236 : }
237 8475 : return false;
238 : }
239 :
240 : struct UsedCharGroup : chars::Compose<char16_t,
241 : chars::CharGroup<char16_t, CharGroupId::Alphanumeric>,
242 : chars::CharGroup<char16_t, CharGroupId::Cyrillic>,
243 : chars::CharGroup<char16_t, CharGroupId::LatinSuppl1>,
244 : chars::CharGroup<char16_t, CharGroupId::GreekBasic>,
245 : chars::CharGroup<char16_t, CharGroupId::GreekAdvanced>,
246 : chars::Chars<char16_t, u'-', u'_', u'&', u'/'>
247 : > { };
248 :
249 : struct WordCharGroup : chars::Compose<char16_t,
250 : chars::CharGroup<char16_t, CharGroupId::Alphanumeric>,
251 : chars::CharGroup<char16_t, CharGroupId::Cyrillic>,
252 : chars::CharGroup<char16_t, CharGroupId::LatinSuppl1>,
253 : chars::CharGroup<char16_t, CharGroupId::GreekBasic>,
254 : chars::CharGroup<char16_t, CharGroupId::GreekAdvanced>,
255 : chars::Chars<char16_t, char16_t(0xAD)>
256 : > { };
257 :
258 2200 : static ParserStatus parseUrlToken(StringView &r, const Callback<ParserStatus(StringView, ParserToken)> &cb) {
259 2200 : UrlView view;
260 2200 : if (!view.parse(r)) {
261 425 : return ParserStatus::PreventSubdivide;
262 : }
263 :
264 1775 : if (view.isEmail()) {
265 200 : if (cb(view.url, ParserToken::Email) == ParserStatus::Stop) {
266 0 : return ParserStatus::Stop;
267 : }
268 1575 : } else if (view.isPath()) {
269 825 : if (cb(view.url, ParserToken::Path) == ParserStatus::Stop) {
270 0 : return ParserStatus::Stop;
271 : }
272 : } else {
273 750 : if (cb(view.url, ParserToken::Url) == ParserStatus::Stop) {
274 0 : return ParserStatus::Stop;
275 : }
276 : }
277 :
278 1775 : return ParserStatus::Continue;
279 : }
280 :
281 11850 : static ParserStatus tryParseUrl(StringViewUtf8 &tmp2, StringView r, const Callback<ParserStatus(StringView, ParserToken)> &cb) {
282 11850 : if (tmp2.is('_') || tmp2.is('.') || tmp2.is(':') || tmp2.is('@') || tmp2.is('/') || tmp2.is('?') || tmp2.is('#')) {
283 11850 : auto tmp3 = tmp2;
284 11850 : ++ tmp3;
285 11850 : if (tmp3.is<WordCharGroup>() || tmp3.is('/')) {
286 2025 : StringView rv(r.data(), tmp2.size() + (tmp2.data() - r.data()));
287 2025 : switch (parseUrlToken(rv, cb)) {
288 1725 : case ParserStatus::Continue:
289 1725 : tmp2 = rv;
290 1725 : return ParserStatus::Continue;
291 : break;
292 0 : case ParserStatus::Stop:
293 0 : return ParserStatus::Stop;
294 : break;
295 300 : default:
296 300 : break;
297 : }
298 : }
299 : }
300 10125 : return ParserStatus::PreventSubdivide;
301 : }
302 :
303 1150 : static ParserStatus parseDotNumber(StringViewUtf8 &r, StringView tmp, const Callback<ParserStatus(StringView, ParserToken)> &cb, bool allowVersion) {
304 1150 : if (r.is<chars::CharGroup<char16_t, CharGroupId::Numbers>>()) {
305 1125 : auto num = r.readChars<chars::CharGroup<char16_t, CharGroupId::Numbers>>();
306 1125 : if (r.is('.') && allowVersion) {
307 850 : while (r.is('.')) {
308 475 : ++ r;
309 475 : num = r.readChars<chars::CharGroup<char16_t, CharGroupId::Alphanumeric>>();
310 475 : if (num.empty()) {
311 1125 : return ParserStatus::PreventSubdivide;
312 : }
313 : }
314 375 : if (r.is('_') || r.is('@') || r.is(':') || r.is('/') || r.is('?') || r.is('#')) {
315 100 : switch (tryParseUrl(r, tmp, cb)) {
316 25 : case ParserStatus::PreventSubdivide:
317 25 : if (cb(StringView(tmp.data(), r.data() - tmp.data()), ParserToken::Version) == ParserStatus::Stop) { return ParserStatus::Stop; }
318 25 : if (cb(r.sub(0, 1), ParserToken::Blank) == ParserStatus::Stop) { return ParserStatus::Stop; }
319 25 : ++ r;
320 25 : break;
321 0 : case ParserStatus::Stop:
322 0 : return ParserStatus::Stop;
323 : break;
324 75 : default:
325 75 : break;
326 : }
327 100 : return ParserStatus::Continue;
328 275 : } else if (!r.is<WordCharGroup>()) {
329 275 : if (cb(StringView(tmp.data(), r.data() - tmp.data()), ParserToken::Version) == ParserStatus::Stop) { return ParserStatus::Stop; }
330 275 : return ParserStatus::Continue;
331 : }
332 750 : } else if (r.is('e') || r.is('E')) {
333 150 : ++ r;
334 150 : num = r.readChars<chars::CharGroup<char16_t, CharGroupId::Numbers>>();
335 150 : if (!num.empty()) {
336 150 : if (!r.is<WordCharGroup>()) {
337 150 : if (cb(StringView(tmp.data(), r.data() - tmp.data()), ParserToken::ScientificFloat) == ParserStatus::Stop) { return ParserStatus::Stop; }
338 150 : return ParserStatus::Continue;
339 : }
340 : }
341 600 : } else if (r.is('@') || r.is(':') || r.is('/') || r.is('?') || r.is('#')) {
342 150 : switch (tryParseUrl(r, tmp, cb)) {
343 125 : case ParserStatus::PreventSubdivide:
344 125 : if (cb(StringView(tmp.data(), r.data() - tmp.data()), ParserToken::Float) == ParserStatus::Stop) { return ParserStatus::Stop; }
345 125 : if (cb(r.sub(0, 1), ParserToken::Blank) == ParserStatus::Stop) { return ParserStatus::Stop; }
346 125 : ++ r;
347 125 : break;
348 0 : case ParserStatus::Stop:
349 0 : return ParserStatus::Stop;
350 : break;
351 25 : default:
352 25 : break;
353 : }
354 150 : return ParserStatus::Continue;
355 450 : } else if (r.is<WordCharGroup>()) {
356 0 : return ParserStatus::PreventSubdivide;
357 : } else {
358 450 : if (cb(StringView(tmp.data(), r.data() - tmp.data()), ParserToken::Float) == ParserStatus::Stop) { return ParserStatus::Stop; }
359 450 : return ParserStatus::Continue;
360 : }
361 : }
362 25 : return ParserStatus::PreventSubdivide;
363 : }
364 :
365 117700 : static bool pushWord(StringView word, const Callback<ParserStatus(StringView, ParserToken)> &cb, bool hyph = false) {
366 117700 : StringView r(word);
367 117700 : r.readChars<StringView::CharGroup<CharGroupId::Latin>>();
368 117700 : if (r.empty()) {
369 60400 : if (cb(word, hyph ? ParserToken::HyphenatedWord_AsciiPart : ParserToken::AsciiWord) == ParserStatus::Stop) { return false; }
370 57300 : } else if (!r.is<StringView::CharGroup<CharGroupId::Numbers>>()) {
371 42875 : r.readUntil<StringView::CharGroup<CharGroupId::Numbers>>();
372 42875 : if (r.empty()) {
373 42875 : if (cb(word, hyph ? ParserToken::HyphenatedWord_Part : ParserToken::Word) == ParserStatus::Stop) { return false; }
374 : } else {
375 0 : if (cb(word, hyph ? ParserToken::HyphenatedWord_NumPart : ParserToken::NumWord) == ParserStatus::Stop) { return false; }
376 : }
377 : } else {
378 14425 : if (cb(word, hyph ? ParserToken::HyphenatedWord_NumPart : ParserToken::NumWord) == ParserStatus::Stop) { return false; }
379 : }
380 117700 : return true;
381 : }
382 :
383 1375 : static bool pushHWord(StringView word, const Callback<ParserStatus(StringView, ParserToken)> &cb) {
384 1375 : ParserStatus stat = ParserStatus::Continue;
385 1375 : StringView r(word);
386 1375 : r.readChars<StringView::CharGroup<CharGroupId::Latin>, StringView::Chars<'-'>>();
387 1375 : if (r.empty()) {
388 425 : cb(word, ParserToken::AsciiHyphenatedWord);
389 950 : } else if (!r.is<StringView::CharGroup<CharGroupId::Numbers>>()) {
390 675 : r.readUntil<StringView::CharGroup<CharGroupId::Numbers>>();
391 675 : if (r.empty()) {
392 400 : stat = cb(word, ParserToken::HyphenatedWord);
393 : } else {
394 275 : stat = cb(word, ParserToken::NumHyphenatedWord);
395 : }
396 : } else {
397 275 : stat = cb(word, ParserToken::NumHyphenatedWord);
398 : }
399 :
400 1375 : if (stat == ParserStatus::Stop) {
401 0 : return false;
402 1375 : } else if (stat == ParserStatus::PreventSubdivide) {
403 125 : return true;
404 : }
405 :
406 4400 : while (!word.empty()) {
407 3150 : auto sep = word.readChars<StringView::Chars<'-'>>();
408 3150 : if (!sep.empty()) {
409 1900 : if (cb(sep, ParserToken::Blank) == ParserStatus::Stop) { return false; }
410 : }
411 3150 : auto tmp = word.readUntil<StringView::Chars<'-'>>();
412 3150 : if (!tmp.empty()) {
413 3150 : if (!pushWord(tmp, cb, true)) {
414 0 : return false;
415 : }
416 : }
417 : }
418 1250 : return true;
419 : }
420 :
421 118825 : static bool parseHyphenatedWord(StringViewUtf8 &tmp, StringView r, const Callback<ParserStatus(StringView, ParserToken)> &cb, size_t depth) {
422 118825 : auto tmp2 = tmp;
423 118825 : tmp2.skipChars<WordCharGroup>();
424 :
425 115925 : auto doPushWord = [&] {
426 115925 : if (depth == 0) {
427 114550 : return pushWord(StringView(r.data(), tmp2.data() - r.data()), cb);
428 : } else {
429 1375 : return pushHWord(StringView(r.data(), tmp2.data() - r.data()), cb);
430 : }
431 118825 : };
432 :
433 118825 : if (tmp2.is('-')) {
434 2100 : tmp2.skipChars<StringViewUtf8::Chars<u'-'>>();
435 2100 : if (!parseHyphenatedWord(tmp2, StringView(r.data(), tmp.data() - r.data()), cb, depth + 1)) {
436 0 : return false;
437 : }
438 116725 : } else if (tmp2.is('_') || tmp2.is('.') || tmp2.is(':') || tmp2.is('@') || tmp2.is('/') || tmp2.is('?') || tmp2.is('#')) {
439 10600 : switch (tryParseUrl(tmp2, r, cb)) {
440 9800 : case ParserStatus::PreventSubdivide:
441 9800 : if (!doPushWord()) { return false; }
442 9800 : if (cb(tmp2.sub(0, 1), ParserToken::Blank) == ParserStatus::Stop) { return false; }
443 9800 : ++ tmp2;
444 9800 : break;
445 0 : case ParserStatus::Stop:
446 0 : return false;
447 : break;
448 800 : default:
449 800 : break;
450 : }
451 : } else {
452 106125 : if (!doPushWord()) { return false; }
453 : }
454 118825 : tmp = tmp2;
455 118825 : return true;
456 : }
457 :
458 1275 : static ParserStatus readCadasterString(StringViewUtf8 &r, StringView tmp, const Callback<ParserStatus(StringView, ParserToken)> &cb) {
459 : using Numbers = StringViewUtf8::MatchCharGroup<CharGroupId::Numbers>;
460 :
461 : using WhiteSpace = chars::Compose<char16_t,
462 : chars::Range<char16_t, u'\u2000', u'\u200D'>,
463 : chars::Chars<char16_t, u'\u0009', u'\u000B', u'\u000C', u'\u0020', u'\u0085', u'\u00A0', u'\u1680', u'\u2028', u'\u2029',
464 : u'\u202F', u'\u205F', u'\u2060', u'\u3000', u'\uFEFF', u'\uFFFF'>
465 : >;
466 :
467 1275 : if (tmp.size() != 2) {
468 0 : return ParserStatus::PreventSubdivide;
469 : }
470 :
471 1275 : if (r.is(':')) {
472 325 : StringViewUtf8 rv = r;
473 325 : size_t segments = 1;
474 825 : while (rv.is(':') || (rv.is<WhiteSpace>())) {
475 825 : rv.skipChars<StringViewUtf8::Chars<':'>, WhiteSpace>();
476 825 : auto nums = rv.readChars<Numbers>();
477 825 : if (nums.empty()) {
478 125 : if (segments >= 3) {
479 0 : r = rv;
480 200 : break;
481 : } else {
482 125 : return ParserStatus::PreventSubdivide;
483 : }
484 700 : } else if (rv.is(':')) {
485 400 : ++ segments;
486 300 : } else if (rv.is<WhiteSpace>()) {
487 275 : if (segments >= 3) {
488 175 : auto tmp = rv;
489 175 : tmp.skipChars<WhiteSpace>();
490 175 : nums = rv.readChars<Numbers>();
491 175 : if ((nums.size() == 2 && (tmp.is(':') || tmp.is('-') || tmp.is(u'–'))) || nums.empty()) {
492 175 : r = rv;
493 175 : break;
494 : }
495 : } else {
496 100 : auto tmp = rv;
497 100 : tmp.skipChars<WhiteSpace>();
498 100 : if (tmp.is(':')) {
499 25 : rv = tmp;
500 25 : ++ segments;
501 : }
502 : }
503 : } else {
504 25 : if (segments >= 3) {
505 25 : r = rv;
506 : }
507 25 : break;
508 : }
509 : }
510 :
511 200 : if (segments >= 3) {
512 200 : auto code = StringView(tmp.data(), r.data() - tmp.data());
513 200 : code.trimUntil<StringView::CharGroup<CharGroupId::Alphanumeric>>();
514 200 : if (cb(code, ParserToken::Custom) == ParserStatus::Stop) { return ParserStatus::Stop; }
515 200 : r = StringViewUtf8(code.data() + code.size(), (r.data() - code.data() - code.size()) + r.size());
516 200 : return ParserStatus::Continue;
517 : }
518 950 : } else if (r.is('-') || r.is(u'–')) {
519 950 : StringViewUtf8 rv = r;
520 950 : size_t segments = 1;
521 950 : size_t nonWsSegments = 0;
522 950 : if (!r.is<WhiteSpace>()) {
523 950 : ++ nonWsSegments;
524 : }
525 3750 : while (rv.is('-') || rv.is(u'–') || rv.is<WhiteSpace>() || rv.is('/') || rv.is(':')) {
526 3750 : rv.skipChars<StringViewUtf8::Chars<'-', u'–', '/', ':'>, WhiteSpace>();
527 3750 : auto nums = rv.readChars<Numbers>();
528 3750 : if (nums.empty()) {
529 450 : if (segments >= 5) {
530 225 : r = rv;
531 725 : break;
532 : } else {
533 225 : return ParserStatus::PreventSubdivide;
534 : }
535 3300 : } else if (rv.is('-') || rv.is(u'–')) {
536 1300 : ++ segments;
537 1300 : ++ nonWsSegments;
538 2000 : } else if (rv.is('/') && segments > 1) {
539 50 : ++ segments;
540 50 : ++ nonWsSegments;
541 1950 : } else if (rv.is(':') && segments > 1) {
542 1450 : ++ segments;
543 1450 : ++ nonWsSegments;
544 500 : } else if (rv.is<WhiteSpace>()) {
545 250 : if (segments >= 5) {
546 250 : r = rv;
547 250 : break;
548 : }
549 0 : ++ segments;
550 : } else {
551 250 : if (segments >= 5) {
552 25 : r = rv;
553 : }
554 250 : break;
555 : }
556 : }
557 :
558 725 : if (segments >= 5 && nonWsSegments >= 2) {
559 500 : auto code = StringView(tmp.data(), r.data() - tmp.data());
560 500 : code.trimUntil<StringView::CharGroup<CharGroupId::Alphanumeric>>();
561 500 : if (cb(code, ParserToken::Custom) == ParserStatus::Stop) { return ParserStatus::Stop; }
562 500 : r = StringViewUtf8(code.data() + code.size(), (r.data() - code.data() - code.size()) + r.size());
563 500 : return ParserStatus::Continue;
564 : }
565 : }
566 :
567 225 : return ParserStatus::PreventSubdivide;
568 : }
569 :
570 128550 : static bool parseToken(StringViewUtf8 &r, const Callback<ParserStatus(StringView, ParserToken)> &cb) {
571 116725 : auto readWord = [&] () {
572 233450 : auto tmp = r;
573 116725 : if (!parseHyphenatedWord(tmp, StringView(r.data(), 0), cb, 0)) {
574 0 : return false;
575 : }
576 116725 : r = tmp;
577 116725 : return true;
578 128550 : };
579 :
580 128550 : if (r.is('-')) {
581 2200 : auto tmp = r;
582 2200 : ++ tmp;
583 2200 : if (tmp.is<chars::CharGroup<char16_t, CharGroupId::Numbers>>()) {
584 1900 : tmp.readChars<chars::CharGroup<char16_t, CharGroupId::Numbers>>();
585 1900 : if (tmp.is('.')) {
586 700 : auto tmp2 = tmp;
587 700 : ++ tmp2;
588 700 : switch (parseDotNumber(tmp2, StringView(r.data(), tmp.data() - r.data()), cb, false)) {
589 700 : case ParserStatus::Continue:
590 700 : r = tmp2;
591 700 : break;
592 0 : case ParserStatus::PreventSubdivide:
593 0 : if (cb(StringView(r.data(), tmp.data() - r.data()), ParserToken::Integer) == ParserStatus::Stop) { return false; }
594 0 : r = tmp;
595 0 : if (cb(StringView(r.data(), 1), ParserToken::Blank) == ParserStatus::Stop) { return false; }
596 0 : ++ r;
597 0 : break;
598 0 : case ParserStatus::Stop:
599 0 : return false;
600 : break;
601 : }
602 1200 : } else if (r.is<WordCharGroup>()) {
603 0 : if (cb(StringView(r.data(), 1), ParserToken::Blank) == ParserStatus::Stop) { return false; }
604 0 : ++ r;
605 0 : return true;
606 : } else {
607 1200 : if (cb(StringView(r.data(), tmp.data() - r.data()), ParserToken::Integer) == ParserStatus::Stop) { return false; }
608 1200 : r = tmp;
609 : }
610 : } else {
611 300 : if (cb(StringView(r.data(), 1), ParserToken::Blank) == ParserStatus::Stop) { return false; }
612 300 : ++ r;
613 : }
614 126350 : } else if (r.is('/')) {
615 950 : switch (tryParseUrl(r, StringView(r.data(), 0), cb)) {
616 125 : case ParserStatus::PreventSubdivide:
617 125 : if (cb(StringView(r.data(), 1), ParserToken::Blank) == ParserStatus::Stop) { return false; }
618 125 : ++ r;
619 125 : break;
620 0 : case ParserStatus::Stop:
621 0 : return false;
622 : break;
623 825 : default:
624 825 : break;
625 : }
626 125400 : } else if (r.is('&')) {
627 225 : StringView tmp(r.data(), std::min(size_t(8), r.size()));
628 225 : tmp.readUntil<StringView::Chars<';'>>();
629 225 : if (tmp.is(';')) {
630 200 : ++ tmp;
631 200 : if (cb(StringView(r.data(), tmp.data() - r.data()), ParserToken::XMLEntity) == ParserStatus::Stop) { return false; }
632 200 : r = StringViewUtf8(tmp.data(), r.size() - (tmp.data() - r.data()));
633 : } else {
634 25 : if (cb(StringView(r.data(), 1), ParserToken::Blank) == ParserStatus::Stop) { return false; }
635 25 : ++ r;
636 : }
637 125175 : } else if (r.is('_')) {
638 50 : switch (tryParseUrl(r, StringView(r.data(), 0), cb)) {
639 50 : case ParserStatus::PreventSubdivide:
640 50 : if (cb(StringView(r.data(), 1), ParserToken::Blank) == ParserStatus::Stop) { return false; }
641 50 : ++ r;
642 50 : break;
643 0 : case ParserStatus::Stop:
644 0 : return false;
645 : break;
646 0 : default:
647 0 : break;
648 : }
649 125125 : } else if (r.is<chars::CharGroup<char16_t, CharGroupId::Numbers>>()) {
650 8434 : auto tmp = r;
651 8434 : auto num = tmp.readChars<chars::CharGroup<char16_t, CharGroupId::Numbers>>();
652 8434 : if (tmp.is('.')) {
653 450 : auto tmp2 = tmp;
654 450 : ++ tmp2;
655 450 : switch (parseDotNumber(tmp2, StringView(r.data(), tmp.data() - r.data()), cb, true)) {
656 425 : case ParserStatus::Continue:
657 425 : r = tmp2;
658 425 : break;
659 25 : case ParserStatus::PreventSubdivide:
660 25 : if (cb(StringView(r.data(), tmp.data() - r.data()), ParserToken::Integer) == ParserStatus::Stop) { return false; }
661 25 : r = tmp;
662 25 : if (cb(StringView(r.data(), 1), ParserToken::Blank) == ParserStatus::Stop) { return false; }
663 25 : ++ r;
664 25 : break;
665 0 : case ParserStatus::Stop:
666 0 : return false;
667 : break;
668 : }
669 7984 : } else if ((tmp.is(':') || tmp.is('-') || tmp.is(u'–')) && num.size() == 2) {
670 1275 : switch (readCadasterString(tmp, num, cb)) {
671 700 : case ParserStatus::Continue:
672 700 : r = tmp;
673 700 : break;
674 0 : case ParserStatus::Stop:
675 0 : return false;
676 : break;
677 575 : default:
678 575 : if (cb(num, ParserToken::Integer) == ParserStatus::Stop) { return false; }
679 575 : r = tmp;
680 575 : if (cb(StringView(r.data(), 1), ParserToken::Blank) == ParserStatus::Stop) { return false; }
681 575 : ++ r;
682 575 : break;
683 : }
684 6709 : } else if (tmp.is<StringViewUtf8::CharGroup<CharGroupId::WhiteSpace>>()) {
685 1800 : auto tmp2 = tmp;
686 1800 : tmp2.skipChars<StringViewUtf8::CharGroup<CharGroupId::WhiteSpace>>();
687 1800 : if (tmp2.is<StringViewUtf8::CharGroup<CharGroupId::Numbers>>()) {
688 250 : if (cb(num, ParserToken::Integer) == ParserStatus::Stop) { return false; }
689 250 : r = tmp;
690 : } else {
691 1550 : if (cb(StringView(r.data(), tmp.data() - r.data()), ParserToken::Integer) == ParserStatus::Stop) { return false; }
692 1550 : r = tmp;
693 : }
694 4909 : } else if (tmp.is('@')) {
695 175 : StringView rv(r.data(), r.size());
696 175 : switch (parseUrlToken(rv, cb)) {
697 50 : case ParserStatus::Continue:
698 50 : r = rv;
699 50 : break;
700 125 : case ParserStatus::PreventSubdivide:
701 125 : if (cb(StringView(r.data(), tmp.data() - r.data()), ParserToken::Integer) == ParserStatus::Stop) { return false; }
702 125 : r = tmp;
703 125 : break;
704 0 : case ParserStatus::Stop:
705 0 : return false;
706 : break;
707 : }
708 4734 : } else if (tmp.is<WordCharGroup>()) {
709 34 : if (!readWord()) { return false; }
710 : } else {
711 4700 : if (cb(StringView(r.data(), tmp.data() - r.data()), ParserToken::Integer) == ParserStatus::Stop) { return false; }
712 4700 : r = tmp;
713 : }
714 116691 : } else if (r.is<WordCharGroup>()) {
715 116691 : if (!readWord()) { return false; }
716 : } else {
717 0 : ++ r;
718 : }
719 :
720 128550 : return true;
721 : }
722 :
723 : struct Stemmer_Reader {
724 : using String = memory::PoolInterface::StringType;
725 : using StringStream = memory::PoolInterface::StringStreamType;
726 :
727 : enum Type {
728 : None,
729 : Content,
730 : Inline,
731 : Drop,
732 : };
733 :
734 : struct Tag : html::Tag<StringView> {
735 : using html::Tag<StringView>::Tag;
736 :
737 : Type type = None;
738 : bool init = false;
739 : };
740 :
741 : using Parser = html::Parser<Stemmer_Reader, StringView, Stemmer_Reader::Tag>;
742 : using StringReader = Parser::StringReader;
743 :
744 6450 : void write(const StringView &d) {
745 6450 : switch (type) {
746 3800 : case Type::None: break;
747 2450 : case Type::Content: buffer << d; break;
748 0 : case Type::Inline: buffer << d; break;
749 200 : case Type::Drop: break;
750 : }
751 6450 : }
752 :
753 1650 : void processData(Parser &p, const StringView &buf) {
754 1650 : StringView r(buf);
755 1650 : r.trimChars<StringView::CharGroup<CharGroupId::WhiteSpace>>();
756 1650 : if (!r.empty()) {
757 1650 : if (callback) {
758 1650 : callback(p, r);
759 : }
760 : }
761 1650 : }
762 :
763 4800 : Type getTypeByName(const StringView &r) const {
764 8600 : if (r == "a" || r == "abbr" || r == "acronym" || r == "b"
765 3800 : || r == "br" || r == "code" || r == "em" || r == "font"
766 3800 : || r == "i" || r == "img" || r == "ins" || r == "kbd"
767 3200 : || r == "map" || r == "samp" || r == "small" || r == "span"
768 8600 : || r == "strong") {
769 1600 : return Inline;
770 3200 : } else if (r == "sub" || r == "sup") {
771 200 : return Drop;
772 5025 : } else if (r == "p" || r == "h1" || r == "h2" || r == "h3"
773 5025 : || r == "h4" || r == "h5" || r == "h6") {
774 1650 : return Content;
775 : }
776 1350 : return None;
777 : }
778 :
779 4800 : inline void onBeginTag(Parser &p, Tag &tag) { tag.type = getTypeByName(tag.name); }
780 4800 : inline void onEndTag(Parser &p, Tag &tag, bool isClosed) { }
781 7000 : inline void onTagAttribute(Parser &p, Tag &tag, StringReader &name, StringReader &value) { }
782 200 : inline void onInlineTag(Parser &p, Tag &tag) { }
783 :
784 4600 : inline void onPushTag(Parser &p, Tag &tag) {
785 4600 : if (type == None && tag.type == Content) {
786 1650 : buffer.clear();
787 1650 : type = Content;
788 1650 : tag.init = true;
789 2950 : } else if (type == Content && tag.type == Drop) {
790 200 : type = Drop;
791 : }
792 4600 : }
793 :
794 4600 : inline void onPopTag(Parser &p, Tag &tag) {
795 4600 : if (tag.init) {
796 1650 : processData(p, buffer.weak());
797 1650 : buffer.clear();
798 1650 : type = None;
799 2950 : } else if (type == Drop && tag.type == Drop) {
800 200 : if (p.tagStack.size() > 1) {
801 200 : type = p.tagStack.at(p.tagStack.size() - 2).type;
802 : } else {
803 0 : type = None;
804 : }
805 : }
806 4600 : }
807 :
808 6450 : inline void onTagContent(Parser &p, Tag &tag, StringReader &s) { write(s); }
809 :
810 : Type type = Type::None;
811 : StringStream buffer;
812 : Function<void(Parser &, const StringView &)> callback;
813 : };
814 :
815 225 : static void Stemmer_Reader_run(StringView origin, Function<void(const StringView &, const Callback<void()> &cancelCb)> &&cb) {
816 225 : Stemmer_Reader r;
817 1300 : r.callback = [&] (Stemmer_Reader::Parser &parser, const StringView &str) {
818 1300 : cb(str, [&] {
819 50 : parser.cancel();
820 50 : });
821 1525 : };
822 225 : html::parse(r, origin, false);
823 225 : }
824 :
825 50 : void parseHtml(StringView str, const Callback<void(StringView)> &cb) {
826 50 : if (str.empty()) {
827 0 : return;
828 : }
829 :
830 50 : Stemmer_Reader r;
831 350 : r.callback = [&] (Stemmer_Reader::Parser &p, const StringView &str) {
832 350 : cb(str);
833 50 : };
834 50 : html::parse(r, str, false);
835 50 : }
836 :
837 13625 : bool parsePhrase(StringView str, const Callback<ParserStatus(StringView, ParserToken)> &cb) {
838 13625 : StringViewUtf8 r(str);
839 :
840 143400 : while (!r.empty()) {
841 129850 : auto tmp = r.readUntil<UsedCharGroup>();
842 129850 : if (!tmp.empty()) {
843 114550 : if (cb(StringView(tmp.data(), tmp.size()), ParserToken::Blank) == ParserStatus::Stop) {
844 75 : return false;
845 : }
846 : }
847 :
848 129775 : if (!r.empty()) {
849 128550 : auto control = r.data();
850 128550 : if (!parseToken(r, cb)) {
851 0 : return false;
852 : }
853 128550 : if (r.data() == control) {
854 0 : std::cout << "Parsing is stalled\n";
855 : }
856 : }
857 : }
858 13550 : return true;
859 : }
860 :
861 1050 : static void * staticPoolAlloc(void* userData, unsigned int size) {
862 1050 : memory::pool_t *pool = (memory::pool_t *)userData;
863 1050 : size_t s = size;
864 1050 : auto mem = memory::pool::alloc(pool, s);
865 1050 : memset(mem,0, s);
866 1050 : return mem;
867 : }
868 :
869 : SP_COVERAGE_TRIVIAL
870 : static void staticPoolFree(void * userData, void * ptr) { }
871 :
872 700 : StemmerEnv *getStemmer(Language lang) {
873 700 : auto pool = memory::pool::acquire();
874 :
875 700 : auto key = toString("SP.Stemmer.", getLanguageName(lang));
876 :
877 700 : StemmerEnv *data = nullptr;
878 700 : memory::pool::userdata_get((void **)&data, key.data(), pool);
879 700 : if (data) {
880 100 : return data;
881 : }
882 :
883 600 : auto mod = sb_stemmer_get(lang);
884 600 : if (!mod->create) {
885 300 : return nullptr;
886 : }
887 :
888 300 : data = (StemmerEnv *)memory::pool::palloc(pool, sizeof(StemmerEnv));
889 300 : memset(data, 0, sizeof(StemmerEnv));
890 300 : data->memalloc = &staticPoolAlloc;
891 300 : data->memfree = &staticPoolFree;
892 300 : data->userData = pool;
893 :
894 300 : if (auto env = mod->create(data)) {
895 300 : env->stem = mod->stem;
896 300 : env->stopwords = getLanguageStopwords(lang);
897 300 : env->mod = mod;
898 300 : memory::pool::userdata_set(data, key.data(), nullptr, pool);
899 300 : return env;
900 : }
901 0 : return nullptr;
902 700 : }
903 :
904 82000 : bool isStopword(const StringView &word, StemmerEnv *env) {
905 82000 : if (env) {
906 81975 : return isStopword(word, env->stopwords);
907 : }
908 25 : return false;
909 : }
910 :
911 82375 : bool isStopword(const StringView &word, const StringView *stopwords) {
912 82375 : if (stopwords) {
913 10716650 : while (stopwords && !stopwords->empty()) {
914 10642025 : if (word == *stopwords) {
915 7750 : return true;
916 : } else {
917 10634275 : ++ stopwords;
918 : }
919 : }
920 : }
921 74625 : return false;
922 : }
923 :
924 81975 : bool stemWord(StringView word, const Callback<void(StringView)> &cb, StemmerEnv *env) {
925 81975 : if (isStopword(word, env)) {
926 7750 : return false;
927 : }
928 74225 : auto w = sb_stemmer_stem(env, (const unsigned char *)word.data(), int(word.size()));
929 74225 : cb(StringView((const char *)w, size_t(env->l)));
930 74225 : return true;
931 : }
932 :
933 175 : bool stemWord(StringView word, const Callback<void(StringView)> &cb, Language lang) {
934 175 : if (lang == Language::Unknown) {
935 175 : lang = detectLanguage(word);
936 175 : if (lang == Language::Unknown) {
937 0 : return false;
938 : }
939 : }
940 :
941 175 : if (lang == Language::Simple) {
942 100 : cb(word);
943 : }
944 :
945 175 : if (auto stemmer = getStemmer(lang)) {
946 75 : return stemWord(word, cb, stemmer);
947 : }
948 :
949 100 : return false;
950 : }
951 :
952 111550 : String normalizeWord(const StringView &str) {
953 111550 : auto tmp = string::tolower<Interface>(string::toUtf16<Interface>(str));
954 111550 : WideString filtered;
955 774325 : for (auto &it : tmp) {
956 662775 : if (it != char16_t(0xAD)) {
957 662775 : filtered.emplace_back(it);
958 : }
959 : }
960 223100 : return string::toUtf8<Interface>(filtered);
961 111550 : }
962 :
963 : }
|