LCOV - code coverage report
Current view: top level - core/search - SPSearchParser.cc (source / functions) Hit Total Coverage
Test: coverage.info Lines: 568 624 91.0 %
Date: 2024-05-12 00:16:13 Functions: 42 42 100.0 %

          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             : }

Generated by: LCOV version 1.14