first commit
This commit is contained in:
@@ -0,0 +1,333 @@
|
||||
|
||||
!"#$%&'()*+,-./
|
||||
0123456789
|
||||
:;<=>?@
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||
[\]^_`
|
||||
abcdefghijklmnopqrstuvwxyz
|
||||
{|}~
¡¢£¤¥¦§¨©
|
||||
ª
|
||||
«¬®¯°±
|
||||
²³
|
||||
´
|
||||
µ
|
||||
¶·¸
|
||||
¹º
|
||||
»
|
||||
¼½¾
|
||||
¿
|
||||
ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ
|
||||
×
|
||||
ØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõö
|
||||
÷
|
||||
øùúûüýþÿĀāĂ㥹ĆćĈĉĊċČčĎďĐđĒēĔĕĖėĘęĚěĜĝĞğĠġĢģĤĥĦħĨĩĪīĬĭĮįİıIJijĴĵĶķĸĹĺĻļĽľĿŀŁłŃńŅņŇňʼnŊŋŌōŎŏŐőŒœŔŕŖŗŘřŚśŜŝŞşŠšŢţŤťŦŧŨũŪūŬŭŮůŰűŲųŴŵŶŷŸŹźŻżŽžſƀƁƂƃƄƅƆƇƈƉƊƋƌƍƎƏƐƑƒƓƔƕƖƗƘƙƚƛƜƝƞƟƠơƢƣƤƥƦƧƨƩƪƫƬƭƮƯưƱƲƳƴƵƶƷƸƹƺƻƼƽƾƿǀǁǂǃDŽDždžLJLjljNJNjnjǍǎǏǐǑǒǓǔǕǖǗǘǙǚǛǜǝǞǟǠǡǢǣǤǥǦǧǨǩǪǫǬǭǮǯǰDZDzdzǴǵǶǷǸǹǺǻǼǽǾǿȀȁȂȃȄȅȆȇȈȉȊȋȌȍȎȏȐȑȒȓȔȕȖȗȘșȚțȜȝȞȟȠȡȢȣȤȥȦȧȨȩȪȫȬȭȮȯȰȱȲȳȴȵȶȷȸȹȺȻȼȽȾȿɀɁɂɃɄɅɆɇɈɉɊɋɌɍɎɏɐɑɒɓɔɕɖɗɘəɚɛɜɝɞɟɠɡɢɣɤɥɦɧɨɩɪɫɬɭɮɯɰɱɲɳɴɵɶɷɸɹɺɻɼɽɾɿʀʁʂʃʄʅʆʇʈʉʊʋʌʍʎʏʐʑʒʓʔʕʖʗʘʙʚʛʜʝʞʟʠʡʢʣʤʥʦʧʨʩʪʫʬʭʮʯʰʱʲʳʴʵʶʷʸʹʺʻʼʽʾʿˀˁ
|
||||
˂˃˄˅
|
||||
ˆˇˈˉˊˋˌˍˎˏːˑ
|
||||
˒˓˔˕˖˗˘˙˚˛˜˝˞˟
|
||||
ˠˡˢˣˤ
|
||||
˥˦˧˨˩˪˫
|
||||
ˬ
|
||||
˭
|
||||
ˮ
|
||||
˯˰˱˲˳˴˵˶˷˸˹˺˻˼˽˾˿
|
||||
̴̵̶̷̸̡̢̧̨̛̖̗̘̙̜̝̞̟̠̣̤̥̦̩̪̫̬̭̮̯̰̱̲̳̹̺̻̼͇͈͉͍͎̀́̂̃̄̅̆̇̈̉̊̋̌̍̎̏̐̑̒̓̔̽̾̿̀́͂̓̈́͆͊͋͌̕̚ͅ͏͓͔͕͖͙͚͐͑͒͗͛ͣͤͥͦͧͨͩͪͫͬͭͮͯ͘͜͟͢͝͞͠͡ͰͱͲͳʹ
|
||||
͵
|
||||
Ͷͷͺͻͼͽ
|
||||
;΄΅
|
||||
Ά
|
||||
·
|
||||
ΈΉΊΌΎΏΐΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩΪΫάέήίΰαβγδεζηθικλμνξοπρςστυφχψωϊϋόύώϏϐϑϒϓϔϕϖϗϘϙϚϛϜϝϞϟϠϡϢϣϤϥϦϧϨϩϪϫϬϭϮϯϰϱϲϳϴϵ
|
||||
϶
|
||||
ϷϸϹϺϻϼϽϾϿЀЁЂЃЄЅІЇЈЉЊЋЌЍЎЏАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюяѐёђѓєѕіїјљњћќѝўџѠѡѢѣѤѥѦѧѨѩѪѫѬѭѮѯѰѱѲѳѴѵѶѷѸѹѺѻѼѽѾѿҀҁ
|
||||
҂
|
||||
҃҄҅҆҇҈҉ҊҋҌҍҎҏҐґҒғҔҕҖҗҘҙҚқҜҝҞҟҠҡҢңҤҥҦҧҨҩҪҫҬҭҮүҰұҲҳҴҵҶҷҸҹҺһҼҽҾҿӀӁӂӃӄӅӆӇӈӉӊӋӌӍӎӏӐӑӒӓӔӕӖӗӘәӚӛӜӝӞӟӠӡӢӣӤӥӦӧӨөӪӫӬӭӮӯӰӱӲӳӴӵӶӷӸӹӺӻӼӽӾӿԀԁԂԃԄԅԆԇԈԉԊԋԌԍԎԏԐԑԒԓԔԕԖԗԘԙԚԛԜԝԞԟԠԡԢԣԤԥԱԲԳԴԵԶԷԸԹԺԻԼԽԾԿՀՁՂՃՄՅՆՇՈՉՊՋՌՍՎՏՐՑՒՓՔՕՖՙ
|
||||
՚՛՜՝՞՟
|
||||
աբգդեզէըթժիլխծկհձղճմյնշոչպջռսվտրցւփքօֆև
|
||||
։֊
|
||||
ְֱֲֳִֵֶַָֹֺֻּֽ֑֖֛֢֣֤֥֦֧֪֚֭֮֒֓֔֕֗֘֙֜֝֞֟֠֡֨֩֫֬֯
|
||||
־
|
||||
ֿ
|
||||
׀
|
||||
ׁׂ
|
||||
׃
|
||||
ׅׄ
|
||||
׆
|
||||
ׇאבגדהוזחטיךכלםמןנסעףפץצקרשתװױײ
|
||||
׳״؆؇؈؉؊؋،؍؎؏
|
||||
ؘؙؚؐؑؒؓؔؕؖؗ
|
||||
؛؞؟
|
||||
ءآأؤإئابةتثجحخدذرزسشصضطظعغػؼؽؾؿـفقكلمنهوىيًٌٍَُِّْٕٖٜٓٔٗ٘ٙٚٛٝٞ٠١٢٣٤٥٦٧٨٩
|
||||
٪٫٬٭
|
||||
ٮٯٰٱٲٳٴٵٶٷٸٹٺٻټٽپٿڀځڂڃڄڅچڇڈډڊڋڌڍڎڏڐڑڒړڔڕږڗژڙښڛڜڝڞڟڠڡڢڣڤڥڦڧڨکڪګڬڭڮگڰڱڲڳڴڵڶڷڸڹںڻڼڽھڿۀہۂۃۄۅۆۇۈۉۊۋیۍێۏېۑےۓ
|
||||
۔
|
||||
ەۖۗۘۙۚۛۜ
|
||||
|
||||
۞ۣ۟۠ۡۢۤۥۦۧۨ
|
||||
۩
|
||||
۪ۭ۫۬ۮۯ۰۱۲۳۴۵۶۷۸۹ۺۻۼ
|
||||
۽۾
|
||||
ۿ
|
||||
܀܁܂܃܄܅܆܇܈܉܊܋܌܍
|
||||
ܐܑܒܓܔܕܖܗܘܙܚܛܜܝܞܟܠܡܢܣܤܥܦܧܨܩܪܫܬܭܮܯܱܴܷܸܹܻܼܾ݂݄݆݈ܰܲܳܵܶܺܽܿ݀݁݃݅݇݉݊ݍݎݏݐݑݒݓݔݕݖݗݘݙݚݛݜݝݞݟݠݡݢݣݤݥݦݧݨݩݪݫݬݭݮݯݰݱݲݳݴݵݶݷݸݹݺݻݼݽݾݿހށނރބޅކއވމފދތލގޏސޑޒޓޔޕޖޗޘޙޚޛޜޝޞޟޠޡޢޣޤޥަާިީުޫެޭޮޯްޱ߀߁߂߃߄߅߆߇߈߉ߊߋߌߍߎߏߐߑߒߓߔߕߖߗߘߙߚߛߜߝߞߟߠߡߢߣߤߥߦߧߨߩߪ߲߫߬߭߮߯߰߱߳ߴߵ
|
||||
߶߷߸߹
|
||||
ߺࠀࠁࠂࠃࠄࠅࠆࠇࠈࠉࠊࠋࠌࠍࠎࠏࠐࠑࠒࠓࠔࠕࠖࠗ࠘࠙ࠚࠛࠜࠝࠞࠟࠠࠡࠢࠣࠤࠥࠦࠧࠨࠩࠪࠫࠬ࠭
|
||||
࠰࠱࠲࠳࠴࠵࠶࠷࠸࠹࠺࠻࠼࠽࠾
|
||||
ऀँंःऄअआइईउऊऋऌऍऎएऐऑऒओऔकखगघङचछजझञटठडढणतथदधनऩपफबभमयरऱलळऴवशषसह़ऽािीुूृॄॅॆेैॉॊोौ्ॎॐ॒॑॓॔ॕक़ख़ग़ज़ड़ढ़फ़य़ॠॡॢॣ
|
||||
।॥
|
||||
०१२३४५६७८९
|
||||
॰
|
||||
ॱॲॹॺॻॼॽॾॿঁংঃঅআইঈউঊঋঌএঐওঔকখগঘঙচছজঝঞটঠডঢণতথদধনপফবভমযরলশষসহ়ঽািীুূৃৄেৈোৌ্ৎৗড়ঢ়য়ৠৡৢৣ০১২৩৪৫৬৭৮৯ৰৱ
|
||||
৲৳
|
||||
৴৵৶৷৸৹
|
||||
৺৻
|
||||
ਁਂਃਅਆਇਈਉਊਏਐਓਔਕਖਗਘਙਚਛਜਝਞਟਠਡਢਣਤਥਦਧਨਪਫਬਭਮਯਰਲਲ਼ਵਸ਼ਸਹ਼ਾਿੀੁੂੇੈੋੌ੍ੑਖ਼ਗ਼ਜ਼ੜਫ਼੦੧੨੩੪੫੬੭੮੯ੰੱੲੳੴੵઁંઃઅઆઇઈઉઊઋઌઍએઐઑઓઔકખગઘઙચછજઝઞટઠડઢણતથદધનપફબભમયરલળવશષસહ઼ઽાિીુૂૃૄૅેૈૉોૌ્ૐૠૡૢૣ૦૧૨૩૪૫૬૭૮૯
|
||||
૱
|
||||
ଁଂଃଅଆଇଈଉଊଋଌଏଐଓଔକଖଗଘଙଚଛଜଝଞଟଠଡଢଣତଥଦଧନପଫବଭମଯରଲଳଵଶଷସହ଼ଽାିୀୁୂୃୄେୈୋୌ୍ୖୗଡ଼ଢ଼ୟୠୡୢୣ୦୧୨୩୪୫୬୭୮୯
|
||||
୰
|
||||
ୱஂஃஅஆஇஈஉஊஎஏஐஒஓஔகஙசஜஞடணதநனபமயரறலளழவஶஷஸஹாிீுூெேைொோௌ்ௐௗ௦௧௨௩௪௫௬௭௮௯௰௱௲
|
||||
௳௴௵௶௷௸௹௺
|
||||
ఁంఃఅఆఇఈఉఊఋఌఎఏఐఒఓఔకఖగఘఙచఛజఝఞటఠడఢణతథదధనపఫబభమయరఱలళవశషసహఽాిీుూృౄెేైొోౌ్ౕౖౘౙౠౡౢౣ౦౧౨౩౪౫౬౭౮౯౸౹౺౻౼౽౾
|
||||
౿
|
||||
ಂಃಅಆಇಈಉಊಋಌಎಏಐಒಓಔಕಖಗಘಙಚಛಜಝಞಟಠಡಢಣತಥದಧನಪಫಬಭಮಯರಱಲಳವಶಷಸಹ಼ಽಾಿೀುೂೃೄೆೇೈೊೋೌ್ೕೖೞೠೡೢೣ೦೧೨೩೪೫೬೭೮೯
|
||||
ೱೲ
|
||||
ംഃഅആഇഈഉഊഋഌഎഏഐഒഓഔകഖഗഘങചഛജഝഞടഠഡഢണതഥദധനപഫബഭമയരറലളഴവശഷസഹഽാിീുൂൃൄെേൈൊോൌ്ൗൠൡൢൣ൦൧൨൩൪൫൬൭൮൯൰൱൲൳൴൵
|
||||
൹
|
||||
ൺൻർൽൾൿංඃඅආඇඈඉඊඋඌඍඎඏඐඑඒඓඔඕඖකඛගඝඞඟචඡජඣඤඥඦටඨඩඪණඬතථදධනඳපඵබභමඹයරලවශෂසහළෆ්ාැෑිීුූෘෙේෛොෝෞෟෲෳ
|
||||
෴
|
||||
กขฃคฅฆงจฉชซฌญฎฏฐฑฒณดตถทธนบปผฝพฟภมยรฤลฦวศษสหฬอฮฯะัาำิีึืฺุู
|
||||
฿
|
||||
เแโใไๅๆ็่้๊๋์ํ๎
|
||||
๏
|
||||
๐๑๒๓๔๕๖๗๘๙
|
||||
๚๛
|
||||
ກຂຄງຈຊຍດຕຖທນບປຜຝພຟມຢຣລວສຫອຮຯະັາຳິີຶືຸູົຼຽເແໂໃໄໆ່້໊໋໌ໍ໐໑໒໓໔໕໖໗໘໙ໜໝༀ
|
||||
༁༂༃༄༅༆༇༈༉༊་༌།༎༏༐༑༒༓༔༕༖༗
|
||||
༘༙
|
||||
༚༛༜༝༞༟
|
||||
༠༡༢༣༤༥༦༧༨༩༪༫༬༭༮༯༰༱༲༳
|
||||
༴
|
||||
༵
|
||||
༶
|
||||
༷
|
||||
༸
|
||||
༹
|
||||
༺༻༼༽
|
||||
༾༿ཀཁགགྷངཅཆཇཉཊཋཌཌྷཎཏཐདདྷནཔཕབབྷམཙཚཛཛྷཝཞཟའཡརལཤཥསཧཨཀྵཪཫཬཱཱཱིིུུྲྀཷླྀཹེཻོཽཾཿ྄ཱྀྀྂྃ
|
||||
྅
|
||||
྆྇ྈྉྊྋྐྑྒྒྷྔྕྖྗྙྚྛྜྜྷྞྟྠྡྡྷྣྤྥྦྦྷྨྩྪྫྫྷྭྮྯྰྱྲླྴྵྶྷྸྐྵྺྻྼ
|
||||
྾྿࿀࿁࿂࿃࿄࿅
|
||||
࿆
|
||||
࿇࿈࿉࿊࿋࿌࿎࿏࿐࿑࿒࿓࿔࿕࿖࿗࿘
|
||||
ကခဂဃငစဆဇဈဉညဋဌဍဎဏတထဒဓနပဖဗဘမယရလဝသဟဠအဢဣဤဥဦဧဨဩဪါာိီုူေဲဳဴဵံ့း္်ျြွှဿ၀၁၂၃၄၅၆၇၈၉
|
||||
၊။၌၍၎၏
|
||||
ၐၑၒၓၔၕၖၗၘၙၚၛၜၝၞၟၠၡၢၣၤၥၦၧၨၩၪၫၬၭၮၯၰၱၲၳၴၵၶၷၸၹၺၻၼၽၾၿႀႁႂႃႄႅႆႇႈႉႊႋႌႍႎႏ႐႑႒႓႔႕႖႗႘႙ႚႛႜႝ
|
||||
႞႟
|
||||
ႠႡႢႣႤႥႦႧႨႩႪႫႬႭႮႯႰႱႲႳႴႵႶႷႸႹႺႻႼႽႾႿჀჁჂჃჄჅაბგდევზთიკლმნოპჟრსტუფქღყშჩცძწჭხჯჰჱჲჳჴჵჶჷჸჹჺ
|
||||
჻
|
||||
ჼᄀᄁᄂᄃᄄᄅᄆᄇᄈᄉᄊᄋᄌᄍᄎᄏᄐᄑᄒᄓᄔᄕᄖᄗᄘᄙᄚᄛᄜᄝᄞᄟᄠᄡᄢᄣᄤᄥᄦᄧᄨᄩᄪᄫᄬᄭᄮᄯᄰᄱᄲᄳᄴᄵᄶᄷᄸᄹᄺᄻᄼᄽᄾᄿᅀᅁᅂᅃᅄᅅᅆᅇᅈᅉᅊᅋᅌᅍᅎᅏᅐᅑᅒᅓᅔᅕᅖᅗᅘᅙᅚᅛᅜᅝᅞᅟᅠᅡᅢᅣᅤᅥᅦᅧᅨᅩᅪᅫᅬᅭᅮᅯᅰᅱᅲᅳᅴᅵᅶᅷᅸᅹᅺᅻᅼᅽᅾᅿᆀᆁᆂᆃᆄᆅᆆᆇᆈᆉᆊᆋᆌᆍᆎᆏᆐᆑᆒᆓᆔᆕᆖᆗᆘᆙᆚᆛᆜᆝᆞᆟᆠᆡᆢᆣᆤᆥᆦᆧᆨᆩᆪᆫᆬᆭᆮᆯᆰᆱᆲᆳᆴᆵᆶᆷᆸᆹᆺᆻᆼᆽᆾᆿᇀᇁᇂᇃᇄᇅᇆᇇᇈᇉᇊᇋᇌᇍᇎᇏᇐᇑᇒᇓᇔᇕᇖᇗᇘᇙᇚᇛᇜᇝᇞᇟᇠᇡᇢᇣᇤᇥᇦᇧᇨᇩᇪᇫᇬᇭᇮᇯᇰᇱᇲᇳᇴᇵᇶᇷᇸᇹᇺᇻᇼᇽᇾᇿሀሁሂሃሄህሆሇለሉሊላሌልሎሏሐሑሒሓሔሕሖሗመሙሚማሜምሞሟሠሡሢሣሤሥሦሧረሩሪራሬርሮሯሰሱሲሳሴስሶሷሸሹሺሻሼሽሾሿቀቁቂቃቄቅቆቇቈቊቋቌቍቐቑቒቓቔቕቖቘቚቛቜቝበቡቢባቤብቦቧቨቩቪቫቬቭቮቯተቱቲታቴትቶቷቸቹቺቻቼችቾቿኀኁኂኃኄኅኆኇኈኊኋኌኍነኑኒናኔንኖኗኘኙኚኛኜኝኞኟአኡኢኣኤእኦኧከኩኪካኬክኮኯኰኲኳኴኵኸኹኺኻኼኽኾዀዂዃዄዅወዉዊዋዌውዎዏዐዑዒዓዔዕዖዘዙዚዛዜዝዞዟዠዡዢዣዤዥዦዧየዩዪያዬይዮዯደዱዲዳዴድዶዷዸዹዺዻዼዽዾዿጀጁጂጃጄጅጆጇገጉጊጋጌግጎጏጐጒጓጔጕጘጙጚጛጜጝጞጟጠጡጢጣጤጥጦጧጨጩጪጫጬጭጮጯጰጱጲጳጴጵጶጷጸጹጺጻጼጽጾጿፀፁፂፃፄፅፆፇፈፉፊፋፌፍፎፏፐፑፒፓፔፕፖፗፘፙፚ፟
|
||||
፠፡።፣፤፥፦፧፨
|
||||
፩፪፫፬፭፮፯፰፱፲፳፴፵፶፷፸፹፺፻፼ᎀᎁᎂᎃᎄᎅᎆᎇᎈᎉᎊᎋᎌᎍᎎᎏ
|
||||
᎐᎑᎒᎓᎔᎕᎖᎗᎘᎙
|
||||
ᎠᎡᎢᎣᎤᎥᎦᎧᎨᎩᎪᎫᎬᎭᎮᎯᎰᎱᎲᎳᎴᎵᎶᎷᎸᎹᎺᎻᎼᎽᎾᎿᏀᏁᏂᏃᏄᏅᏆᏇᏈᏉᏊᏋᏌᏍᏎᏏᏐᏑᏒᏓᏔᏕᏖᏗᏘᏙᏚᏛᏜᏝᏞᏟᏠᏡᏢᏣᏤᏥᏦᏧᏨᏩᏪᏫᏬᏭᏮᏯᏰᏱᏲᏳᏴ
|
||||
᐀
|
||||
ᐁᐂᐃᐄᐅᐆᐇᐈᐉᐊᐋᐌᐍᐎᐏᐐᐑᐒᐓᐔᐕᐖᐗᐘᐙᐚᐛᐜᐝᐞᐟᐠᐡᐢᐣᐤᐥᐦᐧᐨᐩᐪᐫᐬᐭᐮᐯᐰᐱᐲᐳᐴᐵᐶᐷᐸᐹᐺᐻᐼᐽᐾᐿᑀᑁᑂᑃᑄᑅᑆᑇᑈᑉᑊᑋᑌᑍᑎᑏᑐᑑᑒᑓᑔᑕᑖᑗᑘᑙᑚᑛᑜᑝᑞᑟᑠᑡᑢᑣᑤᑥᑦᑧᑨᑩᑪᑫᑬᑭᑮᑯᑰᑱᑲᑳᑴᑵᑶᑷᑸᑹᑺᑻᑼᑽᑾᑿᒀᒁᒂᒃᒄᒅᒆᒇᒈᒉᒊᒋᒌᒍᒎᒏᒐᒑᒒᒓᒔᒕᒖᒗᒘᒙᒚᒛᒜᒝᒞᒟᒠᒡᒢᒣᒤᒥᒦᒧᒨᒩᒪᒫᒬᒭᒮᒯᒰᒱᒲᒳᒴᒵᒶᒷᒸᒹᒺᒻᒼᒽᒾᒿᓀᓁᓂᓃᓄᓅᓆᓇᓈᓉᓊᓋᓌᓍᓎᓏᓐᓑᓒᓓᓔᓕᓖᓗᓘᓙᓚᓛᓜᓝᓞᓟᓠᓡᓢᓣᓤᓥᓦᓧᓨᓩᓪᓫᓬᓭᓮᓯᓰᓱᓲᓳᓴᓵᓶᓷᓸᓹᓺᓻᓼᓽᓾᓿᔀᔁᔂᔃᔄᔅᔆᔇᔈᔉᔊᔋᔌᔍᔎᔏᔐᔑᔒᔓᔔᔕᔖᔗᔘᔙᔚᔛᔜᔝᔞᔟᔠᔡᔢᔣᔤᔥᔦᔧᔨᔩᔪᔫᔬᔭᔮᔯᔰᔱᔲᔳᔴᔵᔶᔷᔸᔹᔺᔻᔼᔽᔾᔿᕀᕁᕂᕃᕄᕅᕆᕇᕈᕉᕊᕋᕌᕍᕎᕏᕐᕑᕒᕓᕔᕕᕖᕗᕘᕙᕚᕛᕜᕝᕞᕟᕠᕡᕢᕣᕤᕥᕦᕧᕨᕩᕪᕫᕬᕭᕮᕯᕰᕱᕲᕳᕴᕵᕶᕷᕸᕹᕺᕻᕼᕽᕾᕿᖀᖁᖂᖃᖄᖅᖆᖇᖈᖉᖊᖋᖌᖍᖎᖏᖐᖑᖒᖓᖔᖕᖖᖗᖘᖙᖚᖛᖜᖝᖞᖟᖠᖡᖢᖣᖤᖥᖦᖧᖨᖩᖪᖫᖬᖭᖮᖯᖰᖱᖲᖳᖴᖵᖶᖷᖸᖹᖺᖻᖼᖽᖾᖿᗀᗁᗂᗃᗄᗅᗆᗇᗈᗉᗊᗋᗌᗍᗎᗏᗐᗑᗒᗓᗔᗕᗖᗗᗘᗙᗚᗛᗜᗝᗞᗟᗠᗡᗢᗣᗤᗥᗦᗧᗨᗩᗪᗫᗬᗭᗮᗯᗰᗱᗲᗳᗴᗵᗶᗷᗸᗹᗺᗻᗼᗽᗾᗿᘀᘁᘂᘃᘄᘅᘆᘇᘈᘉᘊᘋᘌᘍᘎᘏᘐᘑᘒᘓᘔᘕᘖᘗᘘᘙᘚᘛᘜᘝᘞᘟᘠᘡᘢᘣᘤᘥᘦᘧᘨᘩᘪᘫᘬᘭᘮᘯᘰᘱᘲᘳᘴᘵᘶᘷᘸᘹᘺᘻᘼᘽᘾᘿᙀᙁᙂᙃᙄᙅᙆᙇᙈᙉᙊᙋᙌᙍᙎᙏᙐᙑᙒᙓᙔᙕᙖᙗᙘᙙᙚᙛᙜᙝᙞᙟᙠᙡᙢᙣᙤᙥᙦᙧᙨᙩᙪᙫᙬ
|
||||
᙭᙮
|
||||
ᙯᙰᙱᙲᙳᙴᙵᙶᙷᙸᙹᙺᙻᙼᙽᙾᙿ
|
||||
|
||||
ᚁᚂᚃᚄᚅᚆᚇᚈᚉᚊᚋᚌᚍᚎᚏᚐᚑᚒᚓᚔᚕᚖᚗᚘᚙᚚ
|
||||
᚛᚜
|
||||
ᚠᚡᚢᚣᚤᚥᚦᚧᚨᚩᚪᚫᚬᚭᚮᚯᚰᚱᚲᚳᚴᚵᚶᚷᚸᚹᚺᚻᚼᚽᚾᚿᛀᛁᛂᛃᛄᛅᛆᛇᛈᛉᛊᛋᛌᛍᛎᛏᛐᛑᛒᛓᛔᛕᛖᛗᛘᛙᛚᛛᛜᛝᛞᛟᛠᛡᛢᛣᛤᛥᛦᛧᛨᛩᛪ
|
||||
᛫᛬᛭
|
||||
ᛮᛯᛰᜀᜁᜂᜃᜄᜅᜆᜇᜈᜉᜊᜋᜌᜎᜏᜐᜑᜒᜓ᜔ᜠᜡᜢᜣᜤᜥᜦᜧᜨᜩᜪᜫᜬᜭᜮᜯᜰᜱᜲᜳ᜴
|
||||
᜵᜶
|
||||
ᝀᝁᝂᝃᝄᝅᝆᝇᝈᝉᝊᝋᝌᝍᝎᝏᝐᝑᝒᝓᝠᝡᝢᝣᝤᝥᝦᝧᝨᝩᝪᝫᝬᝮᝯᝰᝲᝳកខគឃងចឆជឈញដឋឌឍណតថទធនបផពភមយរលវឝឞសហឡអឣឤឥឦឧឨឩឪឫឬឭឮឯឰឱឲឳ
|
||||
឴឵
|
||||
ាិីឹឺុូួើឿៀេែៃោៅំះៈ៉៊់៌៍៎៏័៑្៓
|
||||
។៕៖
|
||||
ៗ
|
||||
៘៙៚៛
|
||||
ៜ៝០១២៣៤៥៦៧៨៩៰៱៲៳៴៵៶៷៸៹
|
||||
᠀᠁᠂᠃᠄᠅᠆᠇᠈᠉᠊
|
||||
᠋᠌᠍
|
||||
|
||||
᠐᠑᠒᠓᠔᠕᠖᠗᠘᠙ᠠᠡᠢᠣᠤᠥᠦᠧᠨᠩᠪᠫᠬᠭᠮᠯᠰᠱᠲᠳᠴᠵᠶᠷᠸᠹᠺᠻᠼᠽᠾᠿᡀᡁᡂᡃᡄᡅᡆᡇᡈᡉᡊᡋᡌᡍᡎᡏᡐᡑᡒᡓᡔᡕᡖᡗᡘᡙᡚᡛᡜᡝᡞᡟᡠᡡᡢᡣᡤᡥᡦᡧᡨᡩᡪᡫᡬᡭᡮᡯᡰᡱᡲᡳᡴᡵᡶᡷᢀᢁᢂᢃᢄᢅᢆᢇᢈᢉᢊᢋᢌᢍᢎᢏᢐᢑᢒᢓᢔᢕᢖᢗᢘᢙᢚᢛᢜᢝᢞᢟᢠᢡᢢᢣᢤᢥᢦᢧᢨᢩᢪᢰᢱᢲᢳᢴᢵᢶᢷᢸᢹᢺᢻᢼᢽᢾᢿᣀᣁᣂᣃᣄᣅᣆᣇᣈᣉᣊᣋᣌᣍᣎᣏᣐᣑᣒᣓᣔᣕᣖᣗᣘᣙᣚᣛᣜᣝᣞᣟᣠᣡᣢᣣᣤᣥᣦᣧᣨᣩᣪᣫᣬᣭᣮᣯᣰᣱᣲᣳᣴᣵᤀᤁᤂᤃᤄᤅᤆᤇᤈᤉᤊᤋᤌᤍᤎᤏᤐᤑᤒᤓᤔᤕᤖᤗᤘᤙᤚᤛᤜᤠᤡᤢᤣᤤᤥᤦᤧᤨᤩᤪᤫᤰᤱᤲᤳᤴᤵᤶᤷᤸ᤻᤹᤺
|
||||
᥀᥄᥅
|
||||
᥆᥇᥈᥉᥊᥋᥌᥍᥎᥏ᥐᥑᥒᥓᥔᥕᥖᥗᥘᥙᥚᥛᥜᥝᥞᥟᥠᥡᥢᥣᥤᥥᥦᥧᥨᥩᥪᥫᥬᥭᥰᥱᥲᥳᥴᦀᦁᦂᦃᦄᦅᦆᦇᦈᦉᦊᦋᦌᦍᦎᦏᦐᦑᦒᦓᦔᦕᦖᦗᦘᦙᦚᦛᦜᦝᦞᦟᦠᦡᦢᦣᦤᦥᦦᦧᦨᦩᦪᦫᦰᦱᦲᦳᦴᦵᦶᦷᦸᦹᦺᦻᦼᦽᦾᦿᧀᧁᧂᧃᧄᧅᧆᧇᧈᧉ᧐᧑᧒᧓᧔᧕᧖᧗᧘᧙᧚
|
||||
᧞᧟᧠᧡᧢᧣᧤᧥᧦᧧᧨᧩᧪᧫᧬᧭᧮᧯᧰᧱᧲᧳᧴᧵᧶᧷᧸᧹᧺᧻᧼᧽᧾᧿
|
||||
ᨀᨁᨂᨃᨄᨅᨆᨇᨈᨉᨊᨋᨌᨍᨎᨏᨐᨑᨒᨓᨔᨕᨖᨘᨗᨙᨚᨛ
|
||||
᨞᨟
|
||||
ᨠᨡᨢᨣᨤᨥᨦᨧᨨᨩᨪᨫᨬᨭᨮᨯᨰᨱᨲᨳᨴᨵᨶᨷᨸᨹᨺᨻᨼᨽᨾᨿᩀᩁᩂᩃᩄᩅᩆᩇᩈᩉᩊᩋᩌᩍᩎᩏᩐᩑᩒᩓᩔᩕᩖᩗᩘᩙᩚᩛᩜᩝᩞ᩠ᩡᩢᩣᩤᩥᩦᩧᩨᩩᩪᩫᩬᩭᩮᩯᩰᩱᩲᩳᩴ᩿᩵᩶᩷᩸᩹᩺᩻᩼᪀᪁᪂᪃᪄᪅᪆᪇᪈᪉᪐᪑᪒᪓᪔᪕᪖᪗᪘᪙
|
||||
᪠᪡᪢᪣᪤᪥᪦
|
||||
ᪧ
|
||||
᪨᪩᪪᪫᪬᪭
|
||||
ᬀᬁᬂᬃᬄᬅᬆᬇᬈᬉᬊᬋᬌᬍᬎᬏᬐᬑᬒᬓᬔᬕᬖᬗᬘᬙᬚᬛᬜᬝᬞᬟᬠᬡᬢᬣᬤᬥᬦᬧᬨᬩᬪᬫᬬᬭᬮᬯᬰᬱᬲᬳ᬴ᬵᬶᬷᬸᬹᬺᬻᬼᬽᬾᬿᭀᭁᭂᭃ᭄ᭅᭆᭇᭈᭉᭊᭋ᭐᭑᭒᭓᭔᭕᭖᭗᭘᭙
|
||||
᭚᭛᭜᭝᭞᭟᭠᭡᭢᭣᭤᭥᭦᭧᭨᭩᭪
|
||||
᭬᭫᭭᭮᭯᭰᭱᭲᭳
|
||||
᭴᭵᭶᭷᭸᭹᭺᭻᭼
|
||||
ᮀᮁᮂᮃᮄᮅᮆᮇᮈᮉᮊᮋᮌᮍᮎᮏᮐᮑᮒᮓᮔᮕᮖᮗᮘᮙᮚᮛᮜᮝᮞᮟᮠᮡᮢᮣᮤᮥᮦᮧᮨᮩ᮪ᮮᮯ᮰᮱᮲᮳᮴᮵᮶᮷᮸᮹ᰀᰁᰂᰃᰄᰅᰆᰇᰈᰉᰊᰋᰌᰍᰎᰏᰐᰑᰒᰓᰔᰕᰖᰗᰘᰙᰚᰛᰜᰝᰞᰟᰠᰡᰢᰣᰤᰥᰦᰧᰨᰩᰪᰫᰬᰭᰮᰯᰰᰱᰲᰳᰴᰵᰶ᰷
|
||||
᰻᰼᰽᰾᰿
|
||||
᱀᱁᱂᱃᱄᱅᱆᱇᱈᱉ᱍᱎᱏ᱐᱑᱒᱓᱔᱕᱖᱗᱘᱙ᱚᱛᱜᱝᱞᱟᱠᱡᱢᱣᱤᱥᱦᱧᱨᱩᱪᱫᱬᱭᱮᱯᱰᱱᱲᱳᱴᱵᱶᱷᱸᱹᱺᱻᱼᱽ
|
||||
᱾᱿
|
||||
᳐᳑᳒
|
||||
᳓
|
||||
᳔᳕᳖᳗᳘᳙᳜᳝᳞᳟᳚᳛᳠᳡᳢᳣᳤᳥᳦᳧᳨ᳩᳪᳫᳬ᳭ᳮᳯᳰᳱᳲᴀᴁᴂᴃᴄᴅᴆᴇᴈᴉᴊᴋᴌᴍᴎᴏᴐᴑᴒᴓᴔᴕᴖᴗᴘᴙᴚᴛᴜᴝᴞᴟᴠᴡᴢᴣᴤᴥᴦᴧᴨᴩᴪᴫᴬᴭᴮᴯᴰᴱᴲᴳᴴᴵᴶᴷᴸᴹᴺᴻᴼᴽᴾᴿᵀᵁᵂᵃᵄᵅᵆᵇᵈᵉᵊᵋᵌᵍᵎᵏᵐᵑᵒᵓᵔᵕᵖᵗᵘᵙᵚᵛᵜᵝᵞᵟᵠᵡᵢᵣᵤᵥᵦᵧᵨᵩᵪᵫᵬᵭᵮᵯᵰᵱᵲᵳᵴᵵᵶᵷᵸᵹᵺᵻᵼᵽᵾᵿᶀᶁᶂᶃᶄᶅᶆᶇᶈᶉᶊᶋᶌᶍᶎᶏᶐᶑᶒᶓᶔᶕᶖᶗᶘᶙᶚᶛᶜᶝᶞᶟᶠᶡᶢᶣᶤᶥᶦᶧᶨᶩᶪᶫᶬᶭᶮᶯᶰᶱᶲᶳᶴᶵᶶᶷᶸᶹᶺᶻᶼᶽᶾᶿ᷐᷎᷂᷊᷏᷽᷿᷀᷁᷃᷄᷅᷆᷇᷈᷉᷋᷌᷑᷒ᷓᷔᷕᷖᷗᷘᷙᷚᷛᷜᷝᷞᷟᷠᷡᷢᷣᷤᷥᷦ᷾᷍ḀḁḂḃḄḅḆḇḈḉḊḋḌḍḎḏḐḑḒḓḔḕḖḗḘḙḚḛḜḝḞḟḠḡḢḣḤḥḦḧḨḩḪḫḬḭḮḯḰḱḲḳḴḵḶḷḸḹḺḻḼḽḾḿṀṁṂṃṄṅṆṇṈṉṊṋṌṍṎṏṐṑṒṓṔṕṖṗṘṙṚṛṜṝṞṟṠṡṢṣṤṥṦṧṨṩṪṫṬṭṮṯṰṱṲṳṴṵṶṷṸṹṺṻṼṽṾṿẀẁẂẃẄẅẆẇẈẉẊẋẌẍẎẏẐẑẒẓẔẕẖẗẘẙẚẛẜẝẞẟẠạẢảẤấẦầẨẩẪẫẬậẮắẰằẲẳẴẵẶặẸẹẺẻẼẽẾếỀềỂểỄễỆệỈỉỊịỌọỎỏỐốỒồỔổỖỗỘộỚớỜờỞởỠỡỢợỤụỦủỨứỪừỬửỮữỰựỲỳỴỵỶỷỸỹỺỻỼỽỾỿἀἁἂἃἄἅἆἇἈἉἊἋἌἍἎἏἐἑἒἓἔἕἘἙἚἛἜἝἠἡἢἣἤἥἦἧἨἩἪἫἬἭἮἯἰἱἲἳἴἵἶἷἸἹἺἻἼἽἾἿὀὁὂὃὄὅὈὉὊὋὌὍὐὑὒὓὔὕὖὗὙὛὝὟὠὡὢὣὤὥὦὧὨὩὪὫὬὭὮὯὰάὲέὴήὶίὸόὺύὼώᾀᾁᾂᾃᾄᾅᾆᾇᾈᾉᾊᾋᾌᾍᾎᾏᾐᾑᾒᾓᾔᾕᾖᾗᾘᾙᾚᾛᾜᾝᾞᾟᾠᾡᾢᾣᾤᾥᾦᾧᾨᾩᾪᾫᾬᾭᾮᾯᾰᾱᾲᾳᾴᾶᾷᾸᾹᾺΆᾼ
|
||||
᾽
|
||||
ι
|
||||
᾿῀῁
|
||||
ῂῃῄῆῇῈΈῊΉῌ
|
||||
῍῎῏
|
||||
ῐῑῒΐῖῗῘῙῚΊ
|
||||
῝῞῟
|
||||
ῠῡῢΰῤῥῦῧῨῩῪΎῬ
|
||||
῭΅`
|
||||
ῲῳῴῶῷῸΌῺΏῼ
|
||||
´῾ ‐‑‒–—―‖‗‘’‚‛“”„‟†‡•‣․‥…‧
‰‱′″‴‵‶‷‸‹›※‼‽‾‿⁀⁁⁂⁃⁄⁅⁆⁇⁈⁉⁊⁋⁌⁍⁎⁏⁐⁑⁒⁓⁔⁕⁖⁗⁘⁙⁚⁛⁜⁝⁞
|
||||
⁰ⁱ⁴⁵⁶⁷⁸⁹
|
||||
⁺⁻⁼⁽⁾
|
||||
ⁿ₀₁₂₃₄₅₆₇₈₉
|
||||
₊₋₌₍₎
|
||||
ₐₑₒₓₔ
|
||||
₠₡₢₣₤₥₦₧₨₩₪₫€₭₮₯₰₱₲₳₴₵₶₷₸
|
||||
⃒⃓⃘⃙⃚⃐⃑⃔⃕⃖⃗⃛⃜⃝⃞⃟⃠⃡⃢⃣⃤⃥⃦⃪⃫⃨⃬⃭⃮⃯⃧⃩⃰
|
||||
℀℁
|
||||
ℂ
|
||||
℃℄℅℆
|
||||
ℇ
|
||||
℈℉
|
||||
ℊℋℌℍℎℏℐℑℒℓ
|
||||
℔
|
||||
ℕ
|
||||
№℗℘
|
||||
ℙℚℛℜℝ
|
||||
℞℟℠℡™℣
|
||||
ℤ
|
||||
℥
|
||||
Ω
|
||||
℧
|
||||
ℨ
|
||||
℩
|
||||
KÅℬℭ
|
||||
℮
|
||||
ℯℰℱℲℳℴℵℶℷℸℹ
|
||||
℺℻
|
||||
ℼℽℾℿ
|
||||
⅀⅁⅂⅃⅄
|
||||
ⅅⅆⅇⅈⅉ
|
||||
⅊⅋⅌⅍
|
||||
ⅎ
|
||||
⅏
|
||||
⅐⅑⅒⅓⅔⅕⅖⅗⅘⅙⅚⅛⅜⅝⅞⅟ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩⅪⅫⅬⅭⅮⅯⅰⅱⅲⅳⅴⅵⅶⅷⅸⅹⅺⅻⅼⅽⅾⅿↀↁↂↃↄↅↆↇↈ↉
|
||||
←↑→↓↔↕↖↗↘↙↚↛↜↝↞↟↠↡↢↣↤↥↦↧↨↩↪↫↬↭↮↯↰↱↲↳↴↵↶↷↸↹↺↻↼↽↾↿⇀⇁⇂⇃⇄⇅⇆⇇⇈⇉⇊⇋⇌⇍⇎⇏⇐⇑⇒⇓⇔⇕⇖⇗⇘⇙⇚⇛⇜⇝⇞⇟⇠⇡⇢⇣⇤⇥⇦⇧⇨⇩⇪⇫⇬⇭⇮⇯⇰⇱⇲⇳⇴⇵⇶⇷⇸⇹⇺⇻⇼⇽⇾⇿∀∁∂∃∄∅∆∇∈∉∊∋∌∍∎∏∐∑−∓∔∕∖∗∘∙√∛∜∝∞∟∠∡∢∣∤∥∦∧∨∩∪∫∬∭∮∯∰∱∲∳∴∵∶∷∸∹∺∻∼∽∾∿≀≁≂≃≄≅≆≇≈≉≊≋≌≍≎≏≐≑≒≓≔≕≖≗≘≙≚≛≜≝≞≟≠≡≢≣≤≥≦≧≨≩≪≫≬≭≮≯≰≱≲≳≴≵≶≷≸≹≺≻≼≽≾≿⊀⊁⊂⊃⊄⊅⊆⊇⊈⊉⊊⊋⊌⊍⊎⊏⊐⊑⊒⊓⊔⊕⊖⊗⊘⊙⊚⊛⊜⊝⊞⊟⊠⊡⊢⊣⊤⊥⊦⊧⊨⊩⊪⊫⊬⊭⊮⊯⊰⊱⊲⊳⊴⊵⊶⊷⊸⊹⊺⊻⊼⊽⊾⊿⋀⋁⋂⋃⋄⋅⋆⋇⋈⋉⋊⋋⋌⋍⋎⋏⋐⋑⋒⋓⋔⋕⋖⋗⋘⋙⋚⋛⋜⋝⋞⋟⋠⋡⋢⋣⋤⋥⋦⋧⋨⋩⋪⋫⋬⋭⋮⋯⋰⋱⋲⋳⋴⋵⋶⋷⋸⋹⋺⋻⋼⋽⋾⋿⌀⌁⌂⌃⌄⌅⌆⌇⌈⌉⌊⌋⌌⌍⌎⌏⌐⌑⌒⌓⌔⌕⌖⌗⌘⌙⌚⌛⌜⌝⌞⌟⌠⌡⌢⌣⌤⌥⌦⌧⌨〈〉⌫⌬⌭⌮⌯⌰⌱⌲⌳⌴⌵⌶⌷⌸⌹⌺⌻⌼⌽⌾⌿⍀⍁⍂⍃⍄⍅⍆⍇⍈⍉⍊⍋⍌⍍⍎⍏⍐⍑⍒⍓⍔⍕⍖⍗⍘⍙⍚⍛⍜⍝⍞⍟⍠⍡⍢⍣⍤⍥⍦⍧⍨⍩⍪⍫⍬⍭⍮⍯⍰⍱⍲⍳⍴⍵⍶⍷⍸⍹⍺⍻⍼⍽⍾⍿⎀⎁⎂⎃⎄⎅⎆⎇⎈⎉⎊⎋⎌⎍⎎⎏⎐⎑⎒⎓⎔⎕⎖⎗⎘⎙⎚⎛⎜⎝⎞⎟⎠⎡⎢⎣⎤⎥⎦⎧⎨⎩⎪⎫⎬⎭⎮⎯⎰⎱⎲⎳⎴⎵⎶⎷⎸⎹⎺⎻⎼⎽⎾⎿⏀⏁⏂⏃⏄⏅⏆⏇⏈⏉⏊⏋⏌⏍⏎⏏⏐⏑⏒⏓⏔⏕⏖⏗⏘⏙⏚⏛⏜⏝⏞⏟⏠⏡⏢⏣⏤⏥⏦⏧⏨␀␁␂␃␄␅␆␇␈␉␊␋␌␍␎␏␐␑␒␓␔␕␖␗␘␙␚␛␜␝␞␟␠␡␢␣␥␦⑀⑁⑂⑃⑄⑅⑆⑇⑈⑉⑊
|
||||
①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳⑴⑵⑶⑷⑸⑹⑺⑻⑼⑽⑾⑿⒀⒁⒂⒃⒄⒅⒆⒇⒈⒉⒊⒋⒌⒍⒎⒏⒐⒑⒒⒓⒔⒕⒖⒗⒘⒙⒚⒛
|
||||
⒜⒝⒞⒟⒠⒡⒢⒣⒤⒥⒦⒧⒨⒩⒪⒫⒬⒭⒮⒯⒰⒱⒲⒳⒴⒵ⒶⒷⒸⒹⒺⒻⒼⒽⒾⒿⓀⓁⓂⓃⓄⓅⓆⓇⓈⓉⓊⓋⓌⓍⓎⓏⓐⓑⓒⓓⓔⓕⓖⓗⓘⓙⓚⓛⓜⓝⓞⓟⓠⓡⓢⓣⓤⓥⓦⓧⓨⓩ
|
||||
⓪⓫⓬⓭⓮⓯⓰⓱⓲⓳⓴⓵⓶⓷⓸⓹⓺⓻⓼⓽⓾⓿
|
||||
─━│┃┄┅┆┇┈┉┊┋┌┍┎┏┐┑┒┓└┕┖┗┘┙┚┛├┝┞┟┠┡┢┣┤┥┦┧┨┩┪┫┬┭┮┯┰┱┲┳┴┵┶┷┸┹┺┻┼┽┾┿╀╁╂╃╄╅╆╇╈╉╊╋╌╍╎╏═║╒╓╔╕╖╗╘╙╚╛╜╝╞╟╠╡╢╣╤╥╦╧╨╩╪╫╬╭╮╯╰╱╲╳╴╵╶╷╸╹╺╻╼╽╾╿▀▁▂▃▄▅▆▇█▉▊▋▌▍▎▏▐░▒▓▔▕▖▗▘▙▚▛▜▝▞▟■□▢▣▤▥▦▧▨▩▪▫▬▭▮▯▰▱▲△▴▵▶▷▸▹►▻▼▽▾▿◀◁◂◃◄◅◆◇◈◉◊○◌◍◎●◐◑◒◓◔◕◖◗◘◙◚◛◜◝◞◟◠◡◢◣◤◥◦◧◨◩◪◫◬◭◮◯◰◱◲◳◴◵◶◷◸◹◺◻◼◽◾◿☀☁☂☃☄★☆☇☈☉☊☋☌☍☎☏☐☑☒☓☔☕☖☗☘☙☚☛☜☝☞☟☠☡☢☣☤☥☦☧☨☩☪☫☬☭☮☯☰☱☲☳☴☵☶☷☸☹☺☻☼☽☾☿♀♁♂♃♄♅♆♇♈♉♊♋♌♍♎♏♐♑♒♓♔♕♖♗♘♙♚♛♜♝♞♟♠♡♢♣♤♥♦♧♨♩♪♫♬♭♮♯♰♱♲♳♴♵♶♷♸♹♺♻♼♽♾♿⚀⚁⚂⚃⚄⚅⚆⚇⚈⚉⚊⚋⚌⚍⚎⚏⚐⚑⚒⚓⚔⚕⚖⚗⚘⚙⚚⚛⚜⚝⚞⚟⚠⚡⚢⚣⚤⚥⚦⚧⚨⚩⚪⚫⚬⚭⚮⚯⚰⚱⚲⚳⚴⚵⚶⚷⚸⚹⚺⚻⚼⚽⚾⚿⛀⛁⛂⛃⛄⛅⛆⛇⛈⛉⛊⛋⛌⛍⛏⛐⛑⛒⛓⛔⛕⛖⛗⛘⛙⛚⛛⛜⛝⛞⛟⛠⛡⛣⛨⛩⛪⛫⛬⛭⛮⛯⛰⛱⛲⛳⛴⛵⛶⛷⛸⛹⛺⛻⛼⛽⛾⛿✁✂✃✄✆✇✈✉✌✍✎✏✐✑✒✓✔✕✖✗✘✙✚✛✜✝✞✟✠✡✢✣✤✥✦✧✩✪✫✬✭✮✯✰✱✲✳✴✵✶✷✸✹✺✻✼✽✾✿❀❁❂❃❄❅❆❇❈❉❊❋❍❏❐❑❒❖❗❘❙❚❛❜❝❞❡❢❣❤❥❦❧❨❩❪❫❬❭❮❯❰❱❲❳❴❵
|
||||
❶❷❸❹❺❻❼❽❾❿➀➁➂➃➄➅➆➇➈➉➊➋➌➍➎➏➐➑➒➓
|
||||
➔➘➙➚➛➜➝➞➟➠➡➢➣➤➥➦➧➨➩➪➫➬➭➮➯➱➲➳➴➵➶➷➸➹➺➻➼➽➾⟀⟁⟂⟃⟄⟅⟆⟇⟈⟉⟊⟌⟐⟑⟒⟓⟔⟕⟖⟗⟘⟙⟚⟛⟜⟝⟞⟟⟠⟡⟢⟣⟤⟥⟦⟧⟨⟩⟪⟫⟬⟭⟮⟯⟰⟱⟲⟳⟴⟵⟶⟷⟸⟹⟺⟻⟼⟽⟾⟿⠀⠁⠂⠃⠄⠅⠆⠇⠈⠉⠊⠋⠌⠍⠎⠏⠐⠑⠒⠓⠔⠕⠖⠗⠘⠙⠚⠛⠜⠝⠞⠟⠠⠡⠢⠣⠤⠥⠦⠧⠨⠩⠪⠫⠬⠭⠮⠯⠰⠱⠲⠳⠴⠵⠶⠷⠸⠹⠺⠻⠼⠽⠾⠿⡀⡁⡂⡃⡄⡅⡆⡇⡈⡉⡊⡋⡌⡍⡎⡏⡐⡑⡒⡓⡔⡕⡖⡗⡘⡙⡚⡛⡜⡝⡞⡟⡠⡡⡢⡣⡤⡥⡦⡧⡨⡩⡪⡫⡬⡭⡮⡯⡰⡱⡲⡳⡴⡵⡶⡷⡸⡹⡺⡻⡼⡽⡾⡿⢀⢁⢂⢃⢄⢅⢆⢇⢈⢉⢊⢋⢌⢍⢎⢏⢐⢑⢒⢓⢔⢕⢖⢗⢘⢙⢚⢛⢜⢝⢞⢟⢠⢡⢢⢣⢤⢥⢦⢧⢨⢩⢪⢫⢬⢭⢮⢯⢰⢱⢲⢳⢴⢵⢶⢷⢸⢹⢺⢻⢼⢽⢾⢿⣀⣁⣂⣃⣄⣅⣆⣇⣈⣉⣊⣋⣌⣍⣎⣏⣐⣑⣒⣓⣔⣕⣖⣗⣘⣙⣚⣛⣜⣝⣞⣟⣠⣡⣢⣣⣤⣥⣦⣧⣨⣩⣪⣫⣬⣭⣮⣯⣰⣱⣲⣳⣴⣵⣶⣷⣸⣹⣺⣻⣼⣽⣾⣿⤀⤁⤂⤃⤄⤅⤆⤇⤈⤉⤊⤋⤌⤍⤎⤏⤐⤑⤒⤓⤔⤕⤖⤗⤘⤙⤚⤛⤜⤝⤞⤟⤠⤡⤢⤣⤤⤥⤦⤧⤨⤩⤪⤫⤬⤭⤮⤯⤰⤱⤲⤳⤴⤵⤶⤷⤸⤹⤺⤻⤼⤽⤾⤿⥀⥁⥂⥃⥄⥅⥆⥇⥈⥉⥊⥋⥌⥍⥎⥏⥐⥑⥒⥓⥔⥕⥖⥗⥘⥙⥚⥛⥜⥝⥞⥟⥠⥡⥢⥣⥤⥥⥦⥧⥨⥩⥪⥫⥬⥭⥮⥯⥰⥱⥲⥳⥴⥵⥶⥷⥸⥹⥺⥻⥼⥽⥾⥿⦀⦁⦂⦃⦄⦅⦆⦇⦈⦉⦊⦋⦌⦍⦎⦏⦐⦑⦒⦓⦔⦕⦖⦗⦘⦙⦚⦛⦜⦝⦞⦟⦠⦡⦢⦣⦤⦥⦦⦧⦨⦩⦪⦫⦬⦭⦮⦯⦰⦱⦲⦳⦴⦵⦶⦷⦸⦹⦺⦻⦼⦽⦾⦿⧀⧁⧂⧃⧄⧅⧆⧇⧈⧉⧊⧋⧌⧍⧎⧏⧐⧑⧒⧓⧔⧕⧖⧗⧘⧙⧚⧛⧜⧝⧞⧟⧠⧡⧢⧣⧤⧥⧦⧧⧨⧩⧪⧫⧬⧭⧮⧯⧰⧱⧲⧳⧴⧵⧶⧷⧸⧹⧺⧻⧼⧽⧾⧿⨀⨁⨂⨃⨄⨅⨆⨇⨈⨉⨊⨋⨌⨍⨎⨏⨐⨑⨒⨓⨔⨕⨖⨗⨘⨙⨚⨛⨜⨝⨞⨟⨠⨡⨢⨣⨤⨥⨦⨧⨨⨩⨪⨫⨬⨭⨮⨯⨰⨱⨲⨳⨴⨵⨶⨷⨸⨹⨺⨻⨼⨽⨾⨿⩀⩁⩂⩃⩄⩅⩆⩇⩈⩉⩊⩋⩌⩍⩎⩏⩐⩑⩒⩓⩔⩕⩖⩗⩘⩙⩚⩛⩜⩝⩞⩟⩠⩡⩢⩣⩤⩥⩦⩧⩨⩩⩪⩫⩬⩭⩮⩯⩰⩱⩲⩳⩴⩵⩶⩷⩸⩹⩺⩻⩼⩽⩾⩿⪀⪁⪂⪃⪄⪅⪆⪇⪈⪉⪊⪋⪌⪍⪎⪏⪐⪑⪒⪓⪔⪕⪖⪗⪘⪙⪚⪛⪜⪝⪞⪟⪠⪡⪢⪣⪤⪥⪦⪧⪨⪩⪪⪫⪬⪭⪮⪯⪰⪱⪲⪳⪴⪵⪶⪷⪸⪹⪺⪻⪼⪽⪾⪿⫀⫁⫂⫃⫄⫅⫆⫇⫈⫉⫊⫋⫌⫍⫎⫏⫐⫑⫒⫓⫔⫕⫖⫗⫘⫙⫚⫛⫝̸⫝⫞⫟⫠⫡⫢⫣⫤⫥⫦⫧⫨⫩⫪⫫⫬⫭⫮⫯⫰⫱⫲⫳⫴⫵⫶⫷⫸⫹⫺⫻⫼⫽⫾⫿⬀⬁⬂⬃⬄⬅⬆⬇⬈⬉⬊⬋⬌⬍⬎⬏⬐⬑⬒⬓⬔⬕⬖⬗⬘⬙⬚⬛⬜⬝⬞⬟⬠⬡⬢⬣⬤⬥⬦⬧⬨⬩⬪⬫⬬⬭⬮⬯⬰⬱⬲⬳⬴⬵⬶⬷⬸⬹⬺⬻⬼⬽⬾⬿⭀⭁⭂⭃⭄⭅⭆⭇⭈⭉⭊⭋⭌⭐⭑⭒⭓⭔⭕⭖⭗⭘⭙
|
||||
ⰀⰁⰂⰃⰄⰅⰆⰇⰈⰉⰊⰋⰌⰍⰎⰏⰐⰑⰒⰓⰔⰕⰖⰗⰘⰙⰚⰛⰜⰝⰞⰟⰠⰡⰢⰣⰤⰥⰦⰧⰨⰩⰪⰫⰬⰭⰮⰰⰱⰲⰳⰴⰵⰶⰷⰸⰹⰺⰻⰼⰽⰾⰿⱀⱁⱂⱃⱄⱅⱆⱇⱈⱉⱊⱋⱌⱍⱎⱏⱐⱑⱒⱓⱔⱕⱖⱗⱘⱙⱚⱛⱜⱝⱞⱠⱡⱢⱣⱤⱥⱦⱧⱨⱩⱪⱫⱬⱭⱮⱯⱰⱱⱲⱳⱴⱵⱶⱷⱸⱹⱺⱻⱼⱽⱾⱿⲀⲁⲂⲃⲄⲅⲆⲇⲈⲉⲊⲋⲌⲍⲎⲏⲐⲑⲒⲓⲔⲕⲖⲗⲘⲙⲚⲛⲜⲝⲞⲟⲠⲡⲢⲣⲤⲥⲦⲧⲨⲩⲪⲫⲬⲭⲮⲯⲰⲱⲲⲳⲴⲵⲶⲷⲸⲹⲺⲻⲼⲽⲾⲿⳀⳁⳂⳃⳄⳅⳆⳇⳈⳉⳊⳋⳌⳍⳎⳏⳐⳑⳒⳓⳔⳕⳖⳗⳘⳙⳚⳛⳜⳝⳞⳟⳠⳡⳢⳣⳤ
|
||||
⳥⳦⳧⳨⳩⳪
|
||||
ⳫⳬⳭⳮ⳯⳰⳱
|
||||
⳹⳺⳻⳼
|
||||
⳽
|
||||
⳾⳿
|
||||
ⴀⴁⴂⴃⴄⴅⴆⴇⴈⴉⴊⴋⴌⴍⴎⴏⴐⴑⴒⴓⴔⴕⴖⴗⴘⴙⴚⴛⴜⴝⴞⴟⴠⴡⴢⴣⴤⴥⴰⴱⴲⴳⴴⴵⴶⴷⴸⴹⴺⴻⴼⴽⴾⴿⵀⵁⵂⵃⵄⵅⵆⵇⵈⵉⵊⵋⵌⵍⵎⵏⵐⵑⵒⵓⵔⵕⵖⵗⵘⵙⵚⵛⵜⵝⵞⵟⵠⵡⵢⵣⵤⵥⵯⶀⶁⶂⶃⶄⶅⶆⶇⶈⶉⶊⶋⶌⶍⶎⶏⶐⶑⶒⶓⶔⶕⶖⶠⶡⶢⶣⶤⶥⶦⶨⶩⶪⶫⶬⶭⶮⶰⶱⶲⶳⶴⶵⶶⶸⶹⶺⶻⶼⶽⶾⷀⷁⷂⷃⷄⷅⷆⷈⷉⷊⷋⷌⷍⷎⷐⷑⷒⷓⷔⷕⷖⷘⷙⷚⷛⷜⷝⷞⷠⷡⷢⷣⷤⷥⷦⷧⷨⷩⷪⷫⷬⷭⷮⷯⷰⷱⷲⷳⷴⷵⷶⷷⷸⷹⷺⷻⷼⷽⷾⷿ
|
||||
⸀⸁⸂⸃⸄⸅⸆⸇⸈⸉⸊⸋⸌⸍⸎⸏⸐⸑⸒⸓⸔⸕⸖⸗⸘⸙⸚⸛⸜⸝⸞⸟⸠⸡⸢⸣⸤⸥⸦⸧⸨⸩⸪⸫⸬⸭⸮
|
||||
ⸯ
|
||||
⸰⸱⺀⺁⺂⺃⺄⺅⺆⺇⺈⺉⺊⺋⺌⺍⺎⺏⺐⺑⺒⺓⺔⺕⺖⺗⺘⺙⺛⺜⺝⺞⺟⺠⺡⺢⺣⺤⺥⺦⺧⺨⺩⺪⺫⺬⺭⺮⺯⺰⺱⺲⺳⺴⺵⺶⺷⺸⺹⺺⺻⺼⺽⺾⺿⻀⻁⻂⻃⻄⻅⻆⻇⻈⻉⻊⻋⻌⻍⻎⻏⻐⻑⻒⻓⻔⻕⻖⻗⻘⻙⻚⻛⻜⻝⻞⻟⻠⻡⻢⻣⻤⻥⻦⻧⻨⻩⻪⻫⻬⻭⻮⻯⻰⻱⻲⻳⼀⼁⼂⼃⼄⼅⼆⼇⼈⼉⼊⼋⼌⼍⼎⼏⼐⼑⼒⼓⼔⼕⼖⼗⼘⼙⼚⼛⼜⼝⼞⼟⼠⼡⼢⼣⼤⼥⼦⼧⼨⼩⼪⼫⼬⼭⼮⼯⼰⼱⼲⼳⼴⼵⼶⼷⼸⼹⼺⼻⼼⼽⼾⼿⽀⽁⽂⽃⽄⽅⽆⽇⽈⽉⽊⽋⽌⽍⽎⽏⽐⽑⽒⽓⽔⽕⽖⽗⽘⽙⽚⽛⽜⽝⽞⽟⽠⽡⽢⽣⽤⽥⽦⽧⽨⽩⽪⽫⽬⽭⽮⽯⽰⽱⽲⽳⽴⽵⽶⽷⽸⽹⽺⽻⽼⽽⽾⽿⾀⾁⾂⾃⾄⾅⾆⾇⾈⾉⾊⾋⾌⾍⾎⾏⾐⾑⾒⾓⾔⾕⾖⾗⾘⾙⾚⾛⾜⾝⾞⾟⾠⾡⾢⾣⾤⾥⾦⾧⾨⾩⾪⾫⾬⾭⾮⾯⾰⾱⾲⾳⾴⾵⾶⾷⾸⾹⾺⾻⾼⾽⾾⾿⿀⿁⿂⿃⿄⿅⿆⿇⿈⿉⿊⿋⿌⿍⿎⿏⿐⿑⿒⿓⿔⿕⿰⿱⿲⿳⿴⿵⿶⿷⿸⿹⿺⿻ 、。〃〄
|
||||
々〆〇
|
||||
〈〉《》「」『』【】〒〓〔〕〖〗〘〙〚〛〜〝〞〟〠
|
||||
〡〢〣〤〥〦〧〨〩〪〭〮〯〫〬
|
||||
〰
|
||||
〱〲〳〴〵
|
||||
〶〷
|
||||
〸〹〺〻〼
|
||||
〽〾〿
|
||||
ぁあぃいぅうぇえぉおかがきぎくぐけげこごさざしじすずせぜそぞただちぢっつづてでとどなにぬねのはばぱひびぴふぶぷへべぺほぼぽまみむめもゃやゅゆょよらりるれろゎわゐゑをんゔゕゖ゙゚
|
||||
゛゜
|
||||
ゝゞゟ
|
||||
゠
|
||||
ァアィイゥウェエォオカガキギクグケゲコゴサザシジスズセゼソゾタダチヂッツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミムメモャヤュユョヨラリルレロヮワヰヱヲンヴヵヶヷヸヹヺ
|
||||
・
|
||||
ーヽヾヿㄅㄆㄇㄈㄉㄊㄋㄌㄍㄎㄏㄐㄑㄒㄓㄔㄕㄖㄗㄘㄙㄚㄛㄜㄝㄞㄟㄠㄡㄢㄣㄤㄥㄦㄧㄨㄩㄪㄫㄬㄭㄱㄲㄳㄴㄵㄶㄷㄸㄹㄺㄻㄼㄽㄾㄿㅀㅁㅂㅃㅄㅅㅆㅇㅈㅉㅊㅋㅌㅍㅎㅏㅐㅑㅒㅓㅔㅕㅖㅗㅘㅙㅚㅛㅜㅝㅞㅟㅠㅡㅢㅣㅤㅥㅦㅧㅨㅩㅪㅫㅬㅭㅮㅯㅰㅱㅲㅳㅴㅵㅶㅷㅸㅹㅺㅻㅼㅽㅾㅿㆀㆁㆂㆃㆄㆅㆆㆇㆈㆉㆊㆋㆌㆍㆎ
|
||||
㆐㆑
|
||||
㆒㆓㆔㆕
|
||||
㆖㆗㆘㆙㆚㆛㆜㆝㆞㆟
|
||||
ㆠㆡㆢㆣㆤㆥㆦㆧㆨㆩㆪㆫㆬㆭㆮㆯㆰㆱㆲㆳㆴㆵㆶㆷ
|
||||
㇀㇁㇂㇃㇄㇅㇆㇇㇈㇉㇊㇋㇌㇍㇎㇏㇐㇑㇒㇓㇔㇕㇖㇗㇘㇙㇚㇛㇜㇝㇞㇟㇠㇡㇢㇣
|
||||
ㇰㇱㇲㇳㇴㇵㇶㇷㇸㇹㇺㇻㇼㇽㇾㇿ
|
||||
㈀㈁㈂㈃㈄㈅㈆㈇㈈㈉㈊㈋㈌㈍㈎㈏㈐㈑㈒㈓㈔㈕㈖㈗㈘㈙㈚㈛㈜㈝㈞
|
||||
㈠㈡㈢㈣㈤㈥㈦㈧㈨㈩
|
||||
㈪㈫㈬㈭㈮㈯㈰㈱㈲㈳㈴㈵㈶㈷㈸㈹㈺㈻㈼㈽㈾㈿㉀㉁㉂㉃㉄㉅㉆㉇㉈㉉㉊㉋㉌㉍㉎㉏㉐
|
||||
㉑㉒㉓㉔㉕㉖㉗㉘㉙㉚㉛㉜㉝㉞㉟
|
||||
㉠㉡㉢㉣㉤㉥㉦㉧㉨㉩㉪㉫㉬㉭㉮㉯㉰㉱㉲㉳㉴㉵㉶㉷㉸㉹㉺㉻㉼㉽㉾㉿
|
||||
㊀㊁㊂㊃㊄㊅㊆㊇㊈㊉
|
||||
㊊㊋㊌㊍㊎㊏㊐㊑㊒㊓㊔㊕㊖㊗㊘㊙㊚㊛㊜㊝㊞㊟㊠㊡㊢㊣㊤㊥㊦㊧㊨㊩㊪㊫㊬㊭㊮㊯㊰
|
||||
㊱㊲㊳㊴㊵㊶㊷㊸㊹㊺㊻㊼㊽㊾㊿
|
||||
㋀㋁㋂㋃㋄㋅㋆㋇㋈㋉㋊㋋㋌㋍㋎㋏㋐㋑㋒㋓㋔㋕㋖㋗㋘㋙㋚㋛㋜㋝㋞㋟㋠㋡㋢㋣㋤㋥㋦㋧㋨㋩㋪㋫㋬㋭㋮㋯㋰㋱㋲㋳㋴㋵㋶㋷㋸㋹㋺㋻㋼㋽㋾㌀㌁㌂㌃㌄㌅㌆㌇㌈㌉㌊㌋㌌㌍㌎㌏㌐㌑㌒㌓㌔㌕㌖㌗㌘㌙㌚㌛㌜㌝㌞㌟㌠㌡㌢㌣㌤㌥㌦㌧㌨㌩㌪㌫㌬㌭㌮㌯㌰㌱㌲㌳㌴㌵㌶㌷㌸㌹㌺㌻㌼㌽㌾㌿㍀㍁㍂㍃㍄㍅㍆㍇㍈㍉㍊㍋㍌㍍㍎㍏㍐㍑㍒㍓㍔㍕㍖㍗㍘㍙㍚㍛㍜㍝㍞㍟㍠㍡㍢㍣㍤㍥㍦㍧㍨㍩㍪㍫㍬㍭㍮㍯㍰㍱㍲㍳㍴㍵㍶㍷㍸㍹㍺㍻㍼㍽㍾㍿㎀㎁㎂㎃㎄㎅㎆㎇㎈㎉㎊㎋㎌㎍㎎㎏㎐㎑㎒㎓㎔㎕㎖㎗㎘㎙㎚㎛㎜㎝㎞㎟㎠㎡㎢㎣㎤㎥㎦㎧㎨㎩㎪㎫㎬㎭㎮㎯㎰㎱㎲㎳㎴㎵㎶㎷㎸㎹㎺㎻㎼㎽㎾㎿㏀㏁㏂㏃㏄㏅㏆㏇㏈㏉㏊㏋㏌㏍㏎㏏㏐㏑㏒㏓㏔㏕㏖㏗㏘㏙㏚㏛㏜㏝㏞㏟㏠㏡㏢㏣㏤㏥㏦㏧㏨㏩㏪㏫㏬㏭㏮㏯㏰㏱㏲㏳㏴㏵㏶㏷㏸㏹㏺㏻㏼㏽㏾㏿
|
||||
㐀䶵
|
||||
䷀䷁䷂䷃䷄䷅䷆䷇䷈䷉䷊䷋䷌䷍䷎䷏䷐䷑䷒䷓䷔䷕䷖䷗䷘䷙䷚䷛䷜䷝䷞䷟䷠䷡䷢䷣䷤䷥䷦䷧䷨䷩䷪䷫䷬䷭䷮䷯䷰䷱䷲䷳䷴䷵䷶䷷䷸䷹䷺䷻䷼䷽䷾䷿
|
||||
一鿋ꀀꀁꀂꀃꀄꀅꀆꀇꀈꀉꀊꀋꀌꀍꀎꀏꀐꀑꀒꀓꀔꀕꀖꀗꀘꀙꀚꀛꀜꀝꀞꀟꀠꀡꀢꀣꀤꀥꀦꀧꀨꀩꀪꀫꀬꀭꀮꀯꀰꀱꀲꀳꀴꀵꀶꀷꀸꀹꀺꀻꀼꀽꀾꀿꁀꁁꁂꁃꁄꁅꁆꁇꁈꁉꁊꁋꁌꁍꁎꁏꁐꁑꁒꁓꁔꁕꁖꁗꁘꁙꁚꁛꁜꁝꁞꁟꁠꁡꁢꁣꁤꁥꁦꁧꁨꁩꁪꁫꁬꁭꁮꁯꁰꁱꁲꁳꁴꁵꁶꁷꁸꁹꁺꁻꁼꁽꁾꁿꂀꂁꂂꂃꂄꂅꂆꂇꂈꂉꂊꂋꂌꂍꂎꂏꂐꂑꂒꂓꂔꂕꂖꂗꂘꂙꂚꂛꂜꂝꂞꂟꂠꂡꂢꂣꂤꂥꂦꂧꂨꂩꂪꂫꂬꂭꂮꂯꂰꂱꂲꂳꂴꂵꂶꂷꂸꂹꂺꂻꂼꂽꂾꂿꃀꃁꃂꃃꃄꃅꃆꃇꃈꃉꃊꃋꃌꃍꃎꃏꃐꃑꃒꃓꃔꃕꃖꃗꃘꃙꃚꃛꃜꃝꃞꃟꃠꃡꃢꃣꃤꃥꃦꃧꃨꃩꃪꃫꃬꃭꃮꃯꃰꃱꃲꃳꃴꃵꃶꃷꃸꃹꃺꃻꃼꃽꃾꃿꄀꄁꄂꄃꄄꄅꄆꄇꄈꄉꄊꄋꄌꄍꄎꄏꄐꄑꄒꄓꄔꄕꄖꄗꄘꄙꄚꄛꄜꄝꄞꄟꄠꄡꄢꄣꄤꄥꄦꄧꄨꄩꄪꄫꄬꄭꄮꄯꄰꄱꄲꄳꄴꄵꄶꄷꄸꄹꄺꄻꄼꄽꄾꄿꅀꅁꅂꅃꅄꅅꅆꅇꅈꅉꅊꅋꅌꅍꅎꅏꅐꅑꅒꅓꅔꅕꅖꅗꅘꅙꅚꅛꅜꅝꅞꅟꅠꅡꅢꅣꅤꅥꅦꅧꅨꅩꅪꅫꅬꅭꅮꅯꅰꅱꅲꅳꅴꅵꅶꅷꅸꅹꅺꅻꅼꅽꅾꅿꆀꆁꆂꆃꆄꆅꆆꆇꆈꆉꆊꆋꆌꆍꆎꆏꆐꆑꆒꆓꆔꆕꆖꆗꆘꆙꆚꆛꆜꆝꆞꆟꆠꆡꆢꆣꆤꆥꆦꆧꆨꆩꆪꆫꆬꆭꆮꆯꆰꆱꆲꆳꆴꆵꆶꆷꆸꆹꆺꆻꆼꆽꆾꆿꇀꇁꇂꇃꇄꇅꇆꇇꇈꇉꇊꇋꇌꇍꇎꇏꇐꇑꇒꇓꇔꇕꇖꇗꇘꇙꇚꇛꇜꇝꇞꇟꇠꇡꇢꇣꇤꇥꇦꇧꇨꇩꇪꇫꇬꇭꇮꇯꇰꇱꇲꇳꇴꇵꇶꇷꇸꇹꇺꇻꇼꇽꇾꇿꈀꈁꈂꈃꈄꈅꈆꈇꈈꈉꈊꈋꈌꈍꈎꈏꈐꈑꈒꈓꈔꈕꈖꈗꈘꈙꈚꈛꈜꈝꈞꈟꈠꈡꈢꈣꈤꈥꈦꈧꈨꈩꈪꈫꈬꈭꈮꈯꈰꈱꈲꈳꈴꈵꈶꈷꈸꈹꈺꈻꈼꈽꈾꈿꉀꉁꉂꉃꉄꉅꉆꉇꉈꉉꉊꉋꉌꉍꉎꉏꉐꉑꉒꉓꉔꉕꉖꉗꉘꉙꉚꉛꉜꉝꉞꉟꉠꉡꉢꉣꉤꉥꉦꉧꉨꉩꉪꉫꉬꉭꉮꉯꉰꉱꉲꉳꉴꉵꉶꉷꉸꉹꉺꉻꉼꉽꉾꉿꊀꊁꊂꊃꊄꊅꊆꊇꊈꊉꊊꊋꊌꊍꊎꊏꊐꊑꊒꊓꊔꊕꊖꊗꊘꊙꊚꊛꊜꊝꊞꊟꊠꊡꊢꊣꊤꊥꊦꊧꊨꊩꊪꊫꊬꊭꊮꊯꊰꊱꊲꊳꊴꊵꊶꊷꊸꊹꊺꊻꊼꊽꊾꊿꋀꋁꋂꋃꋄꋅꋆꋇꋈꋉꋊꋋꋌꋍꋎꋏꋐꋑꋒꋓꋔꋕꋖꋗꋘꋙꋚꋛꋜꋝꋞꋟꋠꋡꋢꋣꋤꋥꋦꋧꋨꋩꋪꋫꋬꋭꋮꋯꋰꋱꋲꋳꋴꋵꋶꋷꋸꋹꋺꋻꋼꋽꋾꋿꌀꌁꌂꌃꌄꌅꌆꌇꌈꌉꌊꌋꌌꌍꌎꌏꌐꌑꌒꌓꌔꌕꌖꌗꌘꌙꌚꌛꌜꌝꌞꌟꌠꌡꌢꌣꌤꌥꌦꌧꌨꌩꌪꌫꌬꌭꌮꌯꌰꌱꌲꌳꌴꌵꌶꌷꌸꌹꌺꌻꌼꌽꌾꌿꍀꍁꍂꍃꍄꍅꍆꍇꍈꍉꍊꍋꍌꍍꍎꍏꍐꍑꍒꍓꍔꍕꍖꍗꍘꍙꍚꍛꍜꍝꍞꍟꍠꍡꍢꍣꍤꍥꍦꍧꍨꍩꍪꍫꍬꍭꍮꍯꍰꍱꍲꍳꍴꍵꍶꍷꍸꍹꍺꍻꍼꍽꍾꍿꎀꎁꎂꎃꎄꎅꎆꎇꎈꎉꎊꎋꎌꎍꎎꎏꎐꎑꎒꎓꎔꎕꎖꎗꎘꎙꎚꎛꎜꎝꎞꎟꎠꎡꎢꎣꎤꎥꎦꎧꎨꎩꎪꎫꎬꎭꎮꎯꎰꎱꎲꎳꎴꎵꎶꎷꎸꎹꎺꎻꎼꎽꎾꎿꏀꏁꏂꏃꏄꏅꏆꏇꏈꏉꏊꏋꏌꏍꏎꏏꏐꏑꏒꏓꏔꏕꏖꏗꏘꏙꏚꏛꏜꏝꏞꏟꏠꏡꏢꏣꏤꏥꏦꏧꏨꏩꏪꏫꏬꏭꏮꏯꏰꏱꏲꏳꏴꏵꏶꏷꏸꏹꏺꏻꏼꏽꏾꏿꐀꐁꐂꐃꐄꐅꐆꐇꐈꐉꐊꐋꐌꐍꐎꐏꐐꐑꐒꐓꐔꐕꐖꐗꐘꐙꐚꐛꐜꐝꐞꐟꐠꐡꐢꐣꐤꐥꐦꐧꐨꐩꐪꐫꐬꐭꐮꐯꐰꐱꐲꐳꐴꐵꐶꐷꐸꐹꐺꐻꐼꐽꐾꐿꑀꑁꑂꑃꑄꑅꑆꑇꑈꑉꑊꑋꑌꑍꑎꑏꑐꑑꑒꑓꑔꑕꑖꑗꑘꑙꑚꑛꑜꑝꑞꑟꑠꑡꑢꑣꑤꑥꑦꑧꑨꑩꑪꑫꑬꑭꑮꑯꑰꑱꑲꑳꑴꑵꑶꑷꑸꑹꑺꑻꑼꑽꑾꑿꒀꒁꒂꒃꒄꒅꒆꒇꒈꒉꒊꒋꒌ
|
||||
꒐꒑꒒꒓꒔꒕꒖꒗꒘꒙꒚꒛꒜꒝꒞꒟꒠꒡꒢꒣꒤꒥꒦꒧꒨꒩꒪꒫꒬꒭꒮꒯꒰꒱꒲꒳꒴꒵꒶꒷꒸꒹꒺꒻꒼꒽꒾꒿꓀꓁꓂꓃꓄꓅꓆
|
||||
ꓐꓑꓒꓓꓔꓕꓖꓗꓘꓙꓚꓛꓜꓝꓞꓟꓠꓡꓢꓣꓤꓥꓦꓧꓨꓩꓪꓫꓬꓭꓮꓯꓰꓱꓲꓳꓴꓵꓶꓷꓸꓹꓺꓻꓼꓽ
|
||||
꓾꓿
|
||||
ꔀꔁꔂꔃꔄꔅꔆꔇꔈꔉꔊꔋꔌꔍꔎꔏꔐꔑꔒꔓꔔꔕꔖꔗꔘꔙꔚꔛꔜꔝꔞꔟꔠꔡꔢꔣꔤꔥꔦꔧꔨꔩꔪꔫꔬꔭꔮꔯꔰꔱꔲꔳꔴꔵꔶꔷꔸꔹꔺꔻꔼꔽꔾꔿꕀꕁꕂꕃꕄꕅꕆꕇꕈꕉꕊꕋꕌꕍꕎꕏꕐꕑꕒꕓꕔꕕꕖꕗꕘꕙꕚꕛꕜꕝꕞꕟꕠꕡꕢꕣꕤꕥꕦꕧꕨꕩꕪꕫꕬꕭꕮꕯꕰꕱꕲꕳꕴꕵꕶꕷꕸꕹꕺꕻꕼꕽꕾꕿꖀꖁꖂꖃꖄꖅꖆꖇꖈꖉꖊꖋꖌꖍꖎꖏꖐꖑꖒꖓꖔꖕꖖꖗꖘꖙꖚꖛꖜꖝꖞꖟꖠꖡꖢꖣꖤꖥꖦꖧꖨꖩꖪꖫꖬꖭꖮꖯꖰꖱꖲꖳꖴꖵꖶꖷꖸꖹꖺꖻꖼꖽꖾꖿꗀꗁꗂꗃꗄꗅꗆꗇꗈꗉꗊꗋꗌꗍꗎꗏꗐꗑꗒꗓꗔꗕꗖꗗꗘꗙꗚꗛꗜꗝꗞꗟꗠꗡꗢꗣꗤꗥꗦꗧꗨꗩꗪꗫꗬꗭꗮꗯꗰꗱꗲꗳꗴꗵꗶꗷꗸꗹꗺꗻꗼꗽꗾꗿꘀꘁꘂꘃꘄꘅꘆꘇꘈꘉꘊꘋꘌ
|
||||
꘍꘎꘏
|
||||
ꘐꘑꘒꘓꘔꘕꘖꘗꘘꘙꘚꘛꘜꘝꘞꘟ꘠꘡꘢꘣꘤꘥꘦꘧꘨꘩ꘪꘫꙀꙁꙂꙃꙄꙅꙆꙇꙈꙉꙊꙋꙌꙍꙎꙏꙐꙑꙒꙓꙔꙕꙖꙗꙘꙙꙚꙛꙜꙝꙞꙟꙢꙣꙤꙥꙦꙧꙨꙩꙪꙫꙬꙭꙮ꙯꙰꙱꙲
|
||||
꙳
|
||||
꙼꙽
|
||||
꙾
|
||||
ꙿꚀꚁꚂꚃꚄꚅꚆꚇꚈꚉꚊꚋꚌꚍꚎꚏꚐꚑꚒꚓꚔꚕꚖꚗꚠꚡꚢꚣꚤꚥꚦꚧꚨꚩꚪꚫꚬꚭꚮꚯꚰꚱꚲꚳꚴꚵꚶꚷꚸꚹꚺꚻꚼꚽꚾꚿꛀꛁꛂꛃꛄꛅꛆꛇꛈꛉꛊꛋꛌꛍꛎꛏꛐꛑꛒꛓꛔꛕꛖꛗꛘꛙꛚꛛꛜꛝꛞꛟꛠꛡꛢꛣꛤꛥꛦꛧꛨꛩꛪꛫꛬꛭꛮꛯ꛰꛱
|
||||
꛲꛳꛴꛵꛶꛷꜀꜁꜂꜃꜄꜅꜆꜇꜈꜉꜊꜋꜌꜍꜎꜏꜐꜑꜒꜓꜔꜕꜖
|
||||
ꜗꜘꜙꜚꜛꜜꜝꜞꜟ
|
||||
꜠꜡
|
||||
ꜢꜣꜤꜥꜦꜧꜨꜩꜪꜫꜬꜭꜮꜯꜰꜱꜲꜳꜴꜵꜶꜷꜸꜹꜺꜻꜼꜽꜾꜿꝀꝁꝂꝃꝄꝅꝆꝇꝈꝉꝊꝋꝌꝍꝎꝏꝐꝑꝒꝓꝔꝕꝖꝗꝘꝙꝚꝛꝜꝝꝞꝟꝠꝡꝢꝣꝤꝥꝦꝧꝨꝩꝪꝫꝬꝭꝮꝯꝰꝱꝲꝳꝴꝵꝶꝷꝸꝹꝺꝻꝼꝽꝾꝿꞀꞁꞂꞃꞄꞅꞆꞇꞈ
|
||||
꞉꞊
|
||||
Ꞌꞌꟻꟼꟽꟾꟿꠀꠁꠂꠃꠄꠅ꠆ꠇꠈꠉꠊꠋꠌꠍꠎꠏꠐꠑꠒꠓꠔꠕꠖꠗꠘꠙꠚꠛꠜꠝꠞꠟꠠꠡꠢꠣꠤꠥꠦꠧ
|
||||
꠨꠩꠪꠫
|
||||
꠰꠱꠲꠳꠴꠵
|
||||
꠶꠷꠸꠹
|
||||
ꡀꡁꡂꡃꡄꡅꡆꡇꡈꡉꡊꡋꡌꡍꡎꡏꡐꡑꡒꡓꡔꡕꡖꡗꡘꡙꡚꡛꡜꡝꡞꡟꡠꡡꡢꡣꡤꡥꡦꡧꡨꡩꡪꡫꡬꡭꡮꡯꡰꡱꡲꡳ
|
||||
꡴꡵꡶꡷
|
||||
ꢀꢁꢂꢃꢄꢅꢆꢇꢈꢉꢊꢋꢌꢍꢎꢏꢐꢑꢒꢓꢔꢕꢖꢗꢘꢙꢚꢛꢜꢝꢞꢟꢠꢡꢢꢣꢤꢥꢦꢧꢨꢩꢪꢫꢬꢭꢮꢯꢰꢱꢲꢳꢴꢵꢶꢷꢸꢹꢺꢻꢼꢽꢾꢿꣀꣁꣂꣃ꣄
|
||||
꣎꣏
|
||||
꣐꣑꣒꣓꣔꣕꣖꣗꣘꣙꣠꣡꣢꣣꣤꣥꣦꣧꣨꣩꣪꣫꣬꣭꣮꣯꣰꣱ꣲꣳꣴꣵꣶꣷ
|
||||
꣸꣹꣺
|
||||
ꣻ꤀꤁꤂꤃꤄꤅꤆꤇꤈꤉ꤊꤋꤌꤍꤎꤏꤐꤑꤒꤓꤔꤕꤖꤗꤘꤙꤚꤛꤜꤝꤞꤟꤠꤡꤢꤣꤤꤥꤦꤧꤨꤩꤪ꤫꤬꤭
|
||||
꤮꤯
|
||||
ꤰꤱꤲꤳꤴꤵꤶꤷꤸꤹꤺꤻꤼꤽꤾꤿꥀꥁꥂꥃꥄꥅꥆꥇꥈꥉꥊꥋꥌꥍꥎꥏꥐꥑꥒ꥓
|
||||
꥟
|
||||
ꥠꥡꥢꥣꥤꥥꥦꥧꥨꥩꥪꥫꥬꥭꥮꥯꥰꥱꥲꥳꥴꥵꥶꥷꥸꥹꥺꥻꥼꦀꦁꦂꦃꦄꦅꦆꦇꦈꦉꦊꦋꦌꦍꦎꦏꦐꦑꦒꦓꦔꦕꦖꦗꦘꦙꦚꦛꦜꦝꦞꦟꦠꦡꦢꦣꦤꦥꦦꦧꦨꦩꦪꦫꦬꦭꦮꦯꦰꦱꦲ꦳ꦴꦵꦶꦷꦸꦹꦺꦻꦼꦽꦾꦿ꧀
|
||||
꧁꧂꧃꧄꧅꧆꧇꧈꧉꧊꧋꧌꧍
|
||||
ꧏ꧐꧑꧒꧓꧔꧕꧖꧗꧘꧙
|
||||
꧞꧟
|
||||
ꨀꨁꨂꨃꨄꨅꨆꨇꨈꨉꨊꨋꨌꨍꨎꨏꨐꨑꨒꨓꨔꨕꨖꨗꨘꨙꨚꨛꨜꨝꨞꨟꨠꨡꨢꨣꨤꨥꨦꨧꨨꨩꨪꨫꨬꨭꨮꨯꨰꨱꨲꨳꨴꨵꨶꩀꩁꩂꩃꩄꩅꩆꩇꩈꩉꩊꩋꩌꩍ꩐꩑꩒꩓꩔꩕꩖꩗꩘꩙
|
||||
꩜꩝꩞꩟
|
||||
ꩠꩡꩢꩣꩤꩥꩦꩧꩨꩩꩪꩫꩬꩭꩮꩯꩰꩱꩲꩳꩴꩵꩶ
|
||||
꩷꩸꩹
|
||||
ꩺꩻꪀꪁꪂꪃꪄꪅꪆꪇꪈꪉꪊꪋꪌꪍꪎꪏꪐꪑꪒꪓꪔꪕꪖꪗꪘꪙꪚꪛꪜꪝꪞꪟꪠꪡꪢꪣꪤꪥꪦꪧꪨꪩꪪꪫꪬꪭꪮꪯꪰꪱꪴꪲꪳꪵꪶꪷꪸꪹꪺꪻꪼꪽꪾ꪿ꫀ꫁ꫂꫛꫜꫝ
|
||||
꫞꫟
|
||||
ꯀꯁꯂꯃꯄꯅꯆꯇꯈꯉꯊꯋꯌꯍꯎꯏꯐꯑꯒꯓꯔꯕꯖꯗꯘꯙꯚꯛꯜꯝꯞꯟꯠꯡꯢꯣꯤꯥꯦꯧꯨꯩꯪ
|
||||
꯫
|
||||
꯬꯭꯰꯱꯲꯳꯴꯵꯶꯷꯸꯹가힣ힰힱힲힳힴힵힶힷힸힹힺힻힼힽힾힿퟀퟁퟂퟃퟄퟅퟆퟋퟌퟍퟎퟏퟐퟑퟒퟓퟔퟕퟖퟗퟘퟙퟚퟛퟜퟝퟞퟟퟠퟡퟢퟣퟤퟥퟦퟧퟨퟩퟪퟫퟬퟭퟮퟯퟰퟱퟲퟳퟴퟵퟶퟷퟸퟹퟺퟻ
|
||||
|
||||
豈更車賈滑串句龜龜契金喇奈懶癩羅蘿螺裸邏樂洛烙珞落酪駱亂卵欄爛蘭鸞嵐濫藍襤拉臘蠟廊朗浪狼郎來冷勞擄櫓爐盧老蘆虜路露魯鷺碌祿綠菉錄鹿論壟弄籠聾牢磊賂雷壘屢樓淚漏累縷陋勒肋凜凌稜綾菱陵讀拏樂諾丹寧怒率異北磻便復不泌數索參塞省葉說殺辰沈拾若掠略亮兩凉梁糧良諒量勵呂女廬旅濾礪閭驪麗黎力曆歷轢年憐戀撚漣煉璉秊練聯輦蓮連鍊列劣咽烈裂說廉念捻殮簾獵令囹寧嶺怜玲瑩羚聆鈴零靈領例禮醴隸惡了僚寮尿料樂燎療蓼遼龍暈阮劉杻柳流溜琉留硫紐類六戮陸倫崙淪輪律慄栗率隆利吏履易李梨泥理痢罹裏裡里離匿溺吝燐璘藺隣鱗麟林淋臨立笠粒狀炙識什茶刺切度拓糖宅洞暴輻行降見廓兀嗀﨎﨏塚﨑晴﨓﨔凞猪益礼神祥福靖精羽﨟蘒﨡諸﨣﨤逸都﨧﨨﨩飯飼館鶴侮僧免勉勤卑喝嘆器塀墨層屮悔慨憎懲敏既暑梅海渚漢煮爫琢碑社祉祈祐祖祝禍禎穀突節練縉繁署者臭艹艹著褐視謁謹賓贈辶逸難響頻恵𤋮舘並况全侀充冀勇勺喝啕喙嗢塚墳奄奔婢嬨廒廙彩徭惘慎愈憎慠懲戴揄搜摒敖晴朗望杖歹殺流滛滋漢瀞煮瞧爵犯猪瑱甆画瘝瘟益盛直睊着磌窱節类絛練缾者荒華蝹襁覆視調諸請謁諾諭謹變贈輸遲醙鉶陼難靖韛響頋頻鬒龜𢡊𢡄𣏕㮝䀘䀹𥉉𥳐𧻓齃龎fffiflffifflſtstﬓﬔﬕﬖﬗיִﬞײַﬠﬡﬢﬣﬤﬥﬦﬧﬨ
|
||||
﬩
|
||||
שׁשׂשּׁשּׂאַאָאּבּגּדּהּוּזּטּיּךּכּלּמּנּסּףּפּצּקּרּשּתּוֹבֿכֿפֿﭏﭐﭑﭒﭓﭔﭕﭖﭗﭘﭙﭚﭛﭜﭝﭞﭟﭠﭡﭢﭣﭤﭥﭦﭧﭨﭩﭪﭫﭬﭭﭮﭯﭰﭱﭲﭳﭴﭵﭶﭷﭸﭹﭺﭻﭼﭽﭾﭿﮀﮁﮂﮃﮄﮅﮆﮇﮈﮉﮊﮋﮌﮍﮎﮏﮐﮑﮒﮓﮔﮕﮖﮗﮘﮙﮚﮛﮜﮝﮞﮟﮠﮡﮢﮣﮤﮥﮦﮧﮨﮩﮪﮫﮬﮭﮮﮯﮰﮱﯓﯔﯕﯖﯗﯘﯙﯚﯛﯜﯝﯞﯟﯠﯡﯢﯣﯤﯥﯦﯧﯨﯩﯪﯫﯬﯭﯮﯯﯰﯱﯲﯳﯴﯵﯶﯷﯸﯹﯺﯻﯼﯽﯾﯿﰀﰁﰂﰃﰄﰅﰆﰇﰈﰉﰊﰋﰌﰍﰎﰏﰐﰑﰒﰓﰔﰕﰖﰗﰘﰙﰚﰛﰜﰝﰞﰟﰠﰡﰢﰣﰤﰥﰦﰧﰨﰩﰪﰫﰬﰭﰮﰯﰰﰱﰲﰳﰴﰵﰶﰷﰸﰹﰺﰻﰼﰽﰾﰿﱀﱁﱂﱃﱄﱅﱆﱇﱈﱉﱊﱋﱌﱍﱎﱏﱐﱑﱒﱓﱔﱕﱖﱗﱘﱙﱚﱛﱜﱝﱞﱟﱠﱡﱢﱣﱤﱥﱦﱧﱨﱩﱪﱫﱬﱭﱮﱯﱰﱱﱲﱳﱴﱵﱶﱷﱸﱹﱺﱻﱼﱽﱾﱿﲀﲁﲂﲃﲄﲅﲆﲇﲈﲉﲊﲋﲌﲍﲎﲏﲐﲑﲒﲓﲔﲕﲖﲗﲘﲙﲚﲛﲜﲝﲞﲟﲠﲡﲢﲣﲤﲥﲦﲧﲨﲩﲪﲫﲬﲭﲮﲯﲰﲱﲲﲳﲴﲵﲶﲷﲸﲹﲺﲻﲼﲽﲾﲿﳀﳁﳂﳃﳄﳅﳆﳇﳈﳉﳊﳋﳌﳍﳎﳏﳐﳑﳒﳓﳔﳕﳖﳗﳘﳙﳚﳛﳜﳝﳞﳟﳠﳡﳢﳣﳤﳥﳦﳧﳨﳩﳪﳫﳬﳭﳮﳯﳰﳱﳲﳳﳴﳵﳶﳷﳸﳹﳺﳻﳼﳽﳾﳿﴀﴁﴂﴃﴄﴅﴆﴇﴈﴉﴊﴋﴌﴍﴎﴏﴐﴑﴒﴓﴔﴕﴖﴗﴘﴙﴚﴛﴜﴝﴞﴟﴠﴡﴢﴣﴤﴥﴦﴧﴨﴩﴪﴫﴬﴭﴮﴯﴰﴱﴲﴳﴴﴵﴶﴷﴸﴹﴺﴻﴼﴽ
|
||||
﴾﴿
|
||||
ﵐﵑﵒﵓﵔﵕﵖﵗﵘﵙﵚﵛﵜﵝﵞﵟﵠﵡﵢﵣﵤﵥﵦﵧﵨﵩﵪﵫﵬﵭﵮﵯﵰﵱﵲﵳﵴﵵﵶﵷﵸﵹﵺﵻﵼﵽﵾﵿﶀﶁﶂﶃﶄﶅﶆﶇﶈﶉﶊﶋﶌﶍﶎﶏﶒﶓﶔﶕﶖﶗﶘﶙﶚﶛﶜﶝﶞﶟﶠﶡﶢﶣﶤﶥﶦﶧﶨﶩﶪﶫﶬﶭﶮﶯﶰﶱﶲﶳﶴﶵﶶﶷﶸﶹﶺﶻﶼﶽﶾﶿﷀﷁﷂﷃﷄﷅﷆﷇﷰﷱﷲﷳﷴﷵﷶﷷﷸﷹﷺﷻ
|
||||
﷼﷽
|
||||
︀︁︂︃︄︅︆︇︈︉︊︋︌︍︎️
|
||||
︐︑︒︓︔︕︖︗︘︙
|
||||
︠︡︢︣︤︥︦
|
||||
︰︱︲︳︴︵︶︷︸︹︺︻︼︽︾︿﹀﹁﹂﹃﹄﹅﹆﹇﹈﹉﹊﹋﹌﹍﹎﹏﹐﹑﹒﹔﹕﹖﹗﹘﹙﹚﹛﹜﹝﹞﹟﹠﹡﹢﹣﹤﹥﹦﹨﹩﹪﹫
|
||||
ﹰﹱﹲﹳﹴﹶﹷﹸﹹﹺﹻﹼﹽﹾﹿﺀﺁﺂﺃﺄﺅﺆﺇﺈﺉﺊﺋﺌﺍﺎﺏﺐﺑﺒﺓﺔﺕﺖﺗﺘﺙﺚﺛﺜﺝﺞﺟﺠﺡﺢﺣﺤﺥﺦﺧﺨﺩﺪﺫﺬﺭﺮﺯﺰﺱﺲﺳﺴﺵﺶﺷﺸﺹﺺﺻﺼﺽﺾﺿﻀﻁﻂﻃﻄﻅﻆﻇﻈﻉﻊﻋﻌﻍﻎﻏﻐﻑﻒﻓﻔﻕﻖﻗﻘﻙﻚﻛﻜﻝﻞﻟﻠﻡﻢﻣﻤﻥﻦﻧﻨﻩﻪﻫﻬﻭﻮﻯﻰﻱﻲﻳﻴﻵﻶﻷﻸﻹﻺﻻﻼ
|
||||
!"#$%&'()*+,-./
|
||||
0123456789
|
||||
:;<=>?@
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||
[\]^_`
|
||||
abcdefghijklmnopqrstuvwxyz
|
||||
{|}~⦅⦆。「」、・
|
||||
ヲァィゥェォャュョッーアイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワン゙゚ᅠᄀᄁᆪᄂᆬᆭᄃᄄᄅᆰᆱᆲᆳᆴᆵᄚᄆᄇᄈᄡᄉᄊᄋᄌᄍᄎᄏᄐᄑ하ᅢᅣᅤᅥᅦᅧᅨᅩᅪᅫᅬᅭᅮᅯᅰᅱᅲᅳᅴᅵ
|
||||
¢£¬ ̄¦¥₩│←↑→↓■○�
|
||||
𐀀
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
name: 'Search Date Query Alter'
|
||||
type: module
|
||||
description: 'Test module that adds date conditions to node searches.'
|
||||
package: Testing
|
||||
# version: VERSION
|
||||
# core: 8.x
|
||||
|
||||
# Information added by Drupal.org packaging script on 2017-04-19
|
||||
version: '8.3.1'
|
||||
core: '8.x'
|
||||
project: 'drupal'
|
||||
datestamp: 1492622988
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Adds date conditions to node searches.
|
||||
*/
|
||||
|
||||
use Drupal\Core\Database\Query\AlterableInterface;
|
||||
|
||||
/**
|
||||
* Implements hook_query_TAG_alter(): tag search_$type with $type node_search.
|
||||
*/
|
||||
function search_date_query_alter_query_search_node_search_alter(AlterableInterface $query) {
|
||||
// Start date Sat, 19 Mar 2016 00:00:00 GMT.
|
||||
$query->condition('n.created', 1458345600, '>=');
|
||||
// End date Sun, 20 Mar 2016 00:00:00 GMT.
|
||||
$query->condition('n.created', 1458432000, '<');
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
name: 'Search Embedded Form'
|
||||
type: module
|
||||
description: 'Support module for Search module testing of embedded forms.'
|
||||
package: Testing
|
||||
# version: VERSION
|
||||
# core: 8.x
|
||||
|
||||
# Information added by Drupal.org packaging script on 2017-04-19
|
||||
version: '8.3.1'
|
||||
core: '8.x'
|
||||
project: 'drupal'
|
||||
datestamp: 1492622988
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Test module implementing a form that can be embedded in search results.
|
||||
*
|
||||
* A sample use of an embedded form is an e-commerce site where each search
|
||||
* result may include an embedded form with buttons like "Add to cart" for each
|
||||
* individual product (node) listed in the search results.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Adds the test form to search results.
|
||||
*/
|
||||
function search_embedded_form_preprocess_search_result(&$variables) {
|
||||
$form = \Drupal::formBuilder()->getForm('Drupal\search_embedded_form\Form\SearchEmbeddedForm');
|
||||
$variables['snippet'] = array_merge($variables['snippet'], $form);
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
search_embedded_form.test_embedded_form:
|
||||
path: '/search_embedded_form'
|
||||
defaults:
|
||||
_title: 'Search_Embed_Form'
|
||||
_form: '\Drupal\search_embedded_form\Form\SearchEmbeddedForm'
|
||||
requirements:
|
||||
_permission: 'search content'
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\search_embedded_form\Form;
|
||||
|
||||
use Drupal\Core\Form\FormBase;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
|
||||
/**
|
||||
* Form controller for search_embedded_form form.
|
||||
*/
|
||||
class SearchEmbeddedForm extends FormBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFormId() {
|
||||
return 'search_embedded_form';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(array $form, FormStateInterface $form_state) {
|
||||
$count = \Drupal::state()->get('search_embedded_form.submit_count');
|
||||
|
||||
$form['name'] = [
|
||||
'#type' => 'textfield',
|
||||
'#title' => $this->t('Your name'),
|
||||
'#maxlength' => 255,
|
||||
'#default_value' => '',
|
||||
'#required' => TRUE,
|
||||
'#description' => $this->t('Times form has been submitted: %count', ['%count' => $count]),
|
||||
];
|
||||
|
||||
$form['actions'] = ['#type' => 'actions'];
|
||||
$form['actions']['submit'] = [
|
||||
'#type' => 'submit',
|
||||
'#value' => $this->t('Send away'),
|
||||
];
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function submitForm(array &$form, FormStateInterface $form_state) {
|
||||
$state = \Drupal::state();
|
||||
$submit_count = (int) $state->get('search_embedded_form.submit_count');
|
||||
$state->set('search_embedded_form.submit_count', $submit_count + 1);
|
||||
drupal_set_message($this->t('Test form was submitted'));
|
||||
}
|
||||
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
id: dummy_search_type
|
||||
label: 'Dummy search type'
|
||||
status: true
|
||||
langcode: en
|
||||
path: dummy_path
|
||||
plugin: search_extra_type_search
|
||||
configuration: { }
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
# Schema for the configuration files of the Search Extra Type module.
|
||||
|
||||
search_extra_type.settings:
|
||||
type: mapping
|
||||
label: 'Test search type settings'
|
||||
mapping:
|
||||
boost:
|
||||
type: string
|
||||
label: 'String'
|
||||
|
||||
# Plugin \Drupal\search_extra_type\Plugin\Search\SearchExtraTypeSearch
|
||||
search.plugin.search_extra_type_search:
|
||||
type: mapping
|
||||
label: 'Extra type settings'
|
||||
mapping:
|
||||
boost:
|
||||
type: string
|
||||
label: 'Boost method'
|
||||
@@ -0,0 +1,15 @@
|
||||
name: 'Test Search Type'
|
||||
type: module
|
||||
description: 'Support module for Search module testing.'
|
||||
package: Testing
|
||||
# version: VERSION
|
||||
# core: 8.x
|
||||
dependencies:
|
||||
- test_page_test
|
||||
|
||||
|
||||
# Information added by Drupal.org packaging script on 2017-04-19
|
||||
version: '8.3.1'
|
||||
core: '8.x'
|
||||
project: 'drupal'
|
||||
datestamp: 1492622988
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\search_extra_type\Plugin\Search;
|
||||
|
||||
use Drupal\Component\Utility\SafeMarkup;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Routing\UrlGeneratorTrait;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\search\Plugin\ConfigurableSearchPluginBase;
|
||||
|
||||
/**
|
||||
* Executes a dummy keyword search.
|
||||
*
|
||||
* @SearchPlugin(
|
||||
* id = "search_extra_type_search",
|
||||
* title = @Translation("Dummy search type")
|
||||
* )
|
||||
*/
|
||||
class SearchExtraTypeSearch extends ConfigurableSearchPluginBase {
|
||||
|
||||
use UrlGeneratorTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setSearch($keywords, array $parameters, array $attributes) {
|
||||
if (empty($parameters['search_conditions'])) {
|
||||
$parameters['search_conditions'] = '';
|
||||
}
|
||||
parent::setSearch($keywords, $parameters, $attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies if the given parameters are valid enough to execute a search for.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if there are keywords or search conditions in the query.
|
||||
*/
|
||||
public function isSearchExecutable() {
|
||||
return (bool) ($this->keywords || !empty($this->searchParameters['search_conditions']));
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the search.
|
||||
*
|
||||
* This is a dummy search, so when search "executes", we just return a dummy
|
||||
* result containing the keywords and a list of conditions.
|
||||
*
|
||||
* @return array
|
||||
* A structured list of search results
|
||||
*/
|
||||
public function execute() {
|
||||
$results = [];
|
||||
if (!$this->isSearchExecutable()) {
|
||||
return $results;
|
||||
}
|
||||
return [
|
||||
[
|
||||
'link' => Url::fromRoute('test_page_test.test_page')->toString(),
|
||||
'type' => 'Dummy result type',
|
||||
'title' => 'Dummy title',
|
||||
'snippet' => SafeMarkup::format("Dummy search snippet to display. Keywords: @keywords\n\nConditions: @search_parameters", ['@keywords' => $this->keywords, '@search_parameters' => print_r($this->searchParameters, TRUE)]),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildResults() {
|
||||
$results = $this->execute();
|
||||
$output['prefix']['#markup'] = '<h2>Test page text is here</h2> <ol class="search-results">';
|
||||
|
||||
foreach ($results as $entry) {
|
||||
$output[] = [
|
||||
'#theme' => 'search_result',
|
||||
'#result' => $entry,
|
||||
'#plugin_id' => 'search_extra_type_search',
|
||||
];
|
||||
}
|
||||
$pager = [
|
||||
'#type' => 'pager',
|
||||
];
|
||||
$output['suffix']['#markup'] = '</ol>' . drupal_render($pager);
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
|
||||
// Output form for defining rank factor weights.
|
||||
$form['extra_type_settings'] = [
|
||||
'#type' => 'fieldset',
|
||||
'#title' => t('Extra type settings'),
|
||||
'#tree' => TRUE,
|
||||
];
|
||||
|
||||
$form['extra_type_settings']['boost'] = [
|
||||
'#type' => 'select',
|
||||
'#title' => t('Boost method'),
|
||||
'#options' => [
|
||||
'bi' => t('Bistromathic'),
|
||||
'ii' => t('Infinite Improbability'),
|
||||
],
|
||||
'#default_value' => $this->configuration['boost'],
|
||||
];
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
|
||||
$this->configuration['boost'] = $form_state->getValue(['extra_type_settings', 'boost']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function defaultConfiguration() {
|
||||
return [
|
||||
'boost' => 'bi',
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
name: 'Test search entity langcode'
|
||||
type: module
|
||||
description: 'Support module for search module testing.'
|
||||
package: Testing
|
||||
# version: VERSION
|
||||
# core: 8.x
|
||||
|
||||
# Information added by Drupal.org packaging script on 2017-04-19
|
||||
version: '8.3.1'
|
||||
core: '8.x'
|
||||
project: 'drupal'
|
||||
datestamp: 1492622988
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Test module setting up two tests, one for checking if the entity $langcode is
|
||||
* being passed on and another one sets up the alternate verb forms for the
|
||||
* stemming test.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Implements hook_search_preprocess().
|
||||
*/
|
||||
function search_langcode_test_search_preprocess($text, $langcode = NULL) {
|
||||
if (isset($langcode) && $langcode == 'en') {
|
||||
// Add the alternate verb forms for the word "testing".
|
||||
if ($text == 'we are testing') {
|
||||
$text .= ' test tested';
|
||||
}
|
||||
// Prints the langcode for testPreprocessLangcode() and adds some
|
||||
// extra text.
|
||||
else {
|
||||
drupal_set_message('Langcode Preprocess Test: ' . $langcode);
|
||||
$text .= 'Additional text';
|
||||
}
|
||||
}
|
||||
// Prints the langcode for testPreprocessLangcode().
|
||||
elseif (isset($langcode)) {
|
||||
drupal_set_message('Langcode Preprocess Test: ' . $langcode);
|
||||
|
||||
// Preprocessing for the excerpt test.
|
||||
if ($langcode == 'ex') {
|
||||
$text = str_replace('finding', 'find', $text);
|
||||
$text = str_replace('finds', 'find', $text);
|
||||
$text = str_replace('dic', ' dependency injection container', $text);
|
||||
$text = str_replace('hypertext markup language', 'html', $text);
|
||||
}
|
||||
}
|
||||
|
||||
return $text;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
name: 'Test Search Query Alter'
|
||||
type: module
|
||||
description: 'Support module for Search module testing.'
|
||||
package: Testing
|
||||
# version: VERSION
|
||||
# core: 8.x
|
||||
|
||||
# Information added by Drupal.org packaging script on 2017-04-19
|
||||
version: '8.3.1'
|
||||
core: '8.x'
|
||||
project: 'drupal'
|
||||
datestamp: 1492622988
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Test module that alters search queries.
|
||||
*/
|
||||
|
||||
use Drupal\Core\Database\Query\AlterableInterface;
|
||||
|
||||
/**
|
||||
* Implements hook_query_TAG_alter(): tag search_$type with $type node_search.
|
||||
*/
|
||||
function search_query_alter_query_search_node_search_alter(AlterableInterface $query) {
|
||||
// For testing purposes, restrict the query to node type 'article' only.
|
||||
$query->condition('n.type', 'article');
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\search\Functional;
|
||||
|
||||
use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface;
|
||||
use Drupal\comment\Tests\CommentTestTrait;
|
||||
|
||||
/**
|
||||
* Tests that comment count display toggles properly on comment status of node.
|
||||
*
|
||||
* Issue 537278
|
||||
*
|
||||
* - Nodes with comment status set to Open should always how comment counts
|
||||
* - Nodes with comment status set to Closed should show comment counts
|
||||
* only when there are comments
|
||||
* - Nodes with comment status set to Hidden should never show comment counts
|
||||
*
|
||||
* @group search
|
||||
*/
|
||||
class SearchCommentCountToggleTest extends SearchTestBase {
|
||||
|
||||
use CommentTestTrait;
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = ['node', 'comment'];
|
||||
|
||||
/**
|
||||
* A user with permission to search and post comments.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $searchingUser;
|
||||
|
||||
/**
|
||||
* Array of nodes available to search.
|
||||
*
|
||||
* @var \Drupal\node\NodeInterface[]
|
||||
*/
|
||||
protected $searchableNodes;
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
// Create searching user.
|
||||
$this->searchingUser = $this->drupalCreateUser(['search content', 'access content', 'access comments', 'post comments', 'skip comment approval']);
|
||||
|
||||
// Log in with sufficient privileges.
|
||||
$this->drupalLogin($this->searchingUser);
|
||||
|
||||
// Add a comment field.
|
||||
$this->addDefaultCommentField('node', 'article');
|
||||
// Create initial nodes.
|
||||
$node_params = ['type' => 'article', 'body' => [['value' => 'SearchCommentToggleTestCase']]];
|
||||
|
||||
$this->searchableNodes['1 comment'] = $this->drupalCreateNode($node_params);
|
||||
$this->searchableNodes['0 comments'] = $this->drupalCreateNode($node_params);
|
||||
|
||||
// Create a comment array
|
||||
$edit_comment = [];
|
||||
$edit_comment['subject[0][value]'] = $this->randomMachineName();
|
||||
$edit_comment['comment_body[0][value]'] = $this->randomMachineName();
|
||||
|
||||
// Post comment to the test node with comment
|
||||
$this->drupalPostForm('comment/reply/node/' . $this->searchableNodes['1 comment']->id() . '/comment', $edit_comment, t('Save'));
|
||||
|
||||
// First update the index. This does the initial processing.
|
||||
$this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex();
|
||||
|
||||
// Then, run the shutdown function. Testing is a unique case where indexing
|
||||
// and searching has to happen in the same request, so running the shutdown
|
||||
// function manually is needed to finish the indexing process.
|
||||
search_update_totals();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that comment count display toggles properly on comment status of node
|
||||
*/
|
||||
public function testSearchCommentCountToggle() {
|
||||
// Search for the nodes by string in the node body.
|
||||
$edit = [
|
||||
'keys' => "'SearchCommentToggleTestCase'",
|
||||
];
|
||||
$this->drupalGet('search/node');
|
||||
|
||||
// Test comment count display for nodes with comment status set to Open
|
||||
$this->drupalPostForm(NULL, $edit, t('Search'));
|
||||
$this->assertText(t('0 comments'), 'Empty comment count displays for nodes with comment status set to Open');
|
||||
$this->assertText(t('1 comment'), 'Non-empty comment count displays for nodes with comment status set to Open');
|
||||
|
||||
// Test comment count display for nodes with comment status set to Closed
|
||||
$this->searchableNodes['0 comments']->set('comment', CommentItemInterface::CLOSED);
|
||||
$this->searchableNodes['0 comments']->save();
|
||||
$this->searchableNodes['1 comment']->set('comment', CommentItemInterface::CLOSED);
|
||||
$this->searchableNodes['1 comment']->save();
|
||||
|
||||
$this->drupalPostForm(NULL, $edit, t('Search'));
|
||||
$this->assertNoText(t('0 comments'), 'Empty comment count does not display for nodes with comment status set to Closed');
|
||||
$this->assertText(t('1 comment'), 'Non-empty comment count displays for nodes with comment status set to Closed');
|
||||
|
||||
// Test comment count display for nodes with comment status set to Hidden
|
||||
$this->searchableNodes['0 comments']->set('comment', CommentItemInterface::HIDDEN);
|
||||
$this->searchableNodes['0 comments']->save();
|
||||
$this->searchableNodes['1 comment']->set('comment', CommentItemInterface::HIDDEN);
|
||||
$this->searchableNodes['1 comment']->save();
|
||||
|
||||
$this->drupalPostForm(NULL, $edit, t('Search'));
|
||||
$this->assertNoText(t('0 comments'), 'Empty comment count does not display for nodes with comment status set to Hidden');
|
||||
$this->assertNoText(t('1 comment'), 'Non-empty comment count does not display for nodes with comment status set to Hidden');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\search\Functional;
|
||||
|
||||
use Drupal\language\Entity\ConfigurableLanguage;
|
||||
|
||||
/**
|
||||
* Tests searching with date filters that exclude some translations.
|
||||
*
|
||||
* @group search
|
||||
*/
|
||||
class SearchDateIntervalTest extends SearchTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
public static $modules = ['language', 'search_date_query_alter'];
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
// Create and log in user.
|
||||
$test_user = $this->drupalCreateUser(['access content', 'search content', 'use advanced search', 'administer nodes', 'administer languages', 'access administration pages', 'administer site configuration']);
|
||||
$this->drupalLogin($test_user);
|
||||
|
||||
// Add a new language.
|
||||
ConfigurableLanguage::createFromLangcode('es')->save();
|
||||
|
||||
// Set up times to be applied to the English and Spanish translations of the
|
||||
// node create time, so that they are filtered in/out in the
|
||||
// search_date_query_alter test module.
|
||||
$created_time_en = new \DateTime('February 10 2016 10PM');
|
||||
$created_time_es = new \DateTime('March 19 2016 10PM');
|
||||
$default_format = filter_default_format();
|
||||
|
||||
$node = $this->drupalCreateNode([
|
||||
'title' => 'Node EN',
|
||||
'type' => 'page',
|
||||
'body' => [
|
||||
'value' => $this->randomMachineName(32),
|
||||
'format' => $default_format,
|
||||
],
|
||||
'langcode' => 'en',
|
||||
'created' => $created_time_en->getTimestamp(),
|
||||
]);
|
||||
|
||||
// Add Spanish translation to the node.
|
||||
$translation = $node->addTranslation('es', ['title' => 'Node ES']);
|
||||
$translation->body->value = $this->randomMachineName(32);
|
||||
$translation->created->value = $created_time_es->getTimestamp();
|
||||
$node->save();
|
||||
|
||||
// Update the index.
|
||||
$plugin = $this->container->get('plugin.manager.search')->createInstance('node_search');
|
||||
$plugin->updateIndex();
|
||||
search_update_totals();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests searching with date filters that exclude some translations.
|
||||
*/
|
||||
public function testDateIntervalQueryAlter() {
|
||||
// Search for keyword node.
|
||||
$edit = ['keys' => 'node'];
|
||||
$this->drupalPostForm('search/node', $edit, t('Search'));
|
||||
|
||||
// The nodes must have the same node ID but the created date is different.
|
||||
// So only the Spanish translation must appear.
|
||||
$this->assertLink('Node ES', 0, 'Spanish translation found in search results');
|
||||
$this->assertNoLink('Node EN', 'Search results do not contain English node');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\search\Functional;
|
||||
|
||||
/**
|
||||
* Tests that searching for a phrase gets the correct page count.
|
||||
*
|
||||
* @group search
|
||||
*/
|
||||
class SearchExactTest extends SearchTestBase {
|
||||
/**
|
||||
* Tests that the correct number of pager links are found for both keywords and phrases.
|
||||
*/
|
||||
public function testExactQuery() {
|
||||
// Log in with sufficient privileges.
|
||||
$user = $this->drupalCreateUser(['create page content', 'search content']);
|
||||
$this->drupalLogin($user);
|
||||
|
||||
$settings = [
|
||||
'type' => 'page',
|
||||
'title' => 'Simple Node',
|
||||
];
|
||||
// Create nodes with exact phrase.
|
||||
for ($i = 0; $i <= 17; $i++) {
|
||||
$settings['body'] = [['value' => 'love pizza']];
|
||||
$this->drupalCreateNode($settings);
|
||||
}
|
||||
// Create nodes containing keywords.
|
||||
for ($i = 0; $i <= 17; $i++) {
|
||||
$settings['body'] = [['value' => 'love cheesy pizza']];
|
||||
$this->drupalCreateNode($settings);
|
||||
}
|
||||
// Create another node and save it for later.
|
||||
$settings['body'] = [['value' => 'Druplicon']];
|
||||
$node = $this->drupalCreateNode($settings);
|
||||
|
||||
// Update the search index.
|
||||
$this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex();
|
||||
search_update_totals();
|
||||
|
||||
// Refresh variables after the treatment.
|
||||
$this->refreshVariables();
|
||||
|
||||
// Test that the correct number of pager links are found for keyword search.
|
||||
$edit = ['keys' => 'love pizza'];
|
||||
$this->drupalPostForm('search/node', $edit, t('Search'));
|
||||
$this->assertLinkByHref('page=1', 0, '2nd page link is found for keyword search.');
|
||||
$this->assertLinkByHref('page=2', 0, '3rd page link is found for keyword search.');
|
||||
$this->assertLinkByHref('page=3', 0, '4th page link is found for keyword search.');
|
||||
$this->assertNoLinkByHref('page=4', '5th page link is not found for keyword search.');
|
||||
|
||||
// Test that the correct number of pager links are found for exact phrase search.
|
||||
$edit = ['keys' => '"love pizza"'];
|
||||
$this->drupalPostForm('search/node', $edit, t('Search'));
|
||||
$this->assertLinkByHref('page=1', 0, '2nd page link is found for exact phrase search.');
|
||||
$this->assertNoLinkByHref('page=2', '3rd page link is not found for exact phrase search.');
|
||||
|
||||
// Check that with post settings turned on the post information is displayed.
|
||||
$node_type_config = \Drupal::configFactory()->getEditable('node.type.page');
|
||||
$node_type_config->set('display_submitted', TRUE);
|
||||
$node_type_config->save();
|
||||
|
||||
$edit = ['keys' => 'Druplicon'];
|
||||
$this->drupalPostForm('search/node', $edit, t('Search'));
|
||||
$this->assertText($user->getUsername(), 'Basic page node displays author name when post settings are on.');
|
||||
$this->assertText(format_date($node->getChangedTime(), 'short'), 'Basic page node displays post date when post settings are on.');
|
||||
|
||||
// Check that with post settings turned off the user and changed date
|
||||
// information is not displayed.
|
||||
$node_type_config->set('display_submitted', FALSE);
|
||||
$node_type_config->save();
|
||||
$edit = ['keys' => 'Druplicon'];
|
||||
$this->drupalPostForm('search/node', $edit, t('Search'));
|
||||
$this->assertNoText($user->getUsername(), 'Basic page node does not display author name when post settings are off.');
|
||||
$this->assertNoText(format_date($node->getChangedTime(), 'short'), 'Basic page node does not display post date when post settings are off.');
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\search\Functional;
|
||||
|
||||
use Drupal\Component\Utility\Html;
|
||||
|
||||
/**
|
||||
* Verify the search without keywords set and extra conditions.
|
||||
*
|
||||
* Verifies that a plugin can override the isSearchExecutable() method to allow
|
||||
* searching without keywords set and that GET query parameters are made
|
||||
* available to plugins during search execution.
|
||||
*
|
||||
* @group search
|
||||
*/
|
||||
class SearchKeywordsConditionsTest extends SearchTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = ['comment', 'search_extra_type', 'test_page_test'];
|
||||
|
||||
/**
|
||||
* A user with permission to search and post comments.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $searchingUser;
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
// Create searching user.
|
||||
$this->searchingUser = $this->drupalCreateUser(['search content', 'access content', 'access comments', 'skip comment approval']);
|
||||
// Log in with sufficient privileges.
|
||||
$this->drupalLogin($this->searchingUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the keywords are captured and conditions respected.
|
||||
*/
|
||||
public function testSearchKeywordsConditions() {
|
||||
// No keys, not conditions - no results.
|
||||
$this->drupalGet('search/dummy_path');
|
||||
$this->assertNoText('Dummy search snippet to display');
|
||||
// With keys - get results.
|
||||
$keys = 'bike shed ' . $this->randomMachineName();
|
||||
$this->drupalGet("search/dummy_path", ['query' => ['keys' => $keys]]);
|
||||
$this->assertText("Dummy search snippet to display. Keywords: {$keys}");
|
||||
$keys = 'blue drop ' . $this->randomMachineName();
|
||||
$this->drupalGet("search/dummy_path", ['query' => ['keys' => $keys]]);
|
||||
$this->assertText("Dummy search snippet to display. Keywords: {$keys}");
|
||||
// Add some conditions and keys.
|
||||
$keys = 'moving drop ' . $this->randomMachineName();
|
||||
$this->drupalGet("search/dummy_path", ['query' => ['keys' => 'bike', 'search_conditions' => $keys]]);
|
||||
$this->assertText("Dummy search snippet to display.");
|
||||
$this->assertRaw(Html::escape(print_r(['keys' => 'bike', 'search_conditions' => $keys], TRUE)));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\search\Functional;
|
||||
|
||||
use Drupal\field\Entity\FieldStorageConfig;
|
||||
use Drupal\language\Entity\ConfigurableLanguage;
|
||||
|
||||
/**
|
||||
* Tests entities with multilingual fields.
|
||||
*
|
||||
* @group search
|
||||
*/
|
||||
class SearchMultilingualEntityTest extends SearchTestBase {
|
||||
|
||||
/**
|
||||
* List of searchable nodes.
|
||||
*
|
||||
* @var \Drupal\node\NodeInterface[]
|
||||
*/
|
||||
protected $searchableNodes = [];
|
||||
|
||||
/**
|
||||
* Node search plugin.
|
||||
*
|
||||
* @var \Drupal\node\Plugin\Search\NodeSearch
|
||||
*/
|
||||
protected $plugin;
|
||||
|
||||
public static $modules = ['language', 'locale', 'comment'];
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
// Create a user who can administer search, do searches, see the status
|
||||
// report, and administer cron. Log in.
|
||||
$user = $this->drupalCreateUser(['administer search', 'search content', 'use advanced search', 'access content', 'access site reports', 'administer site configuration']);
|
||||
$this->drupalLogin($user);
|
||||
|
||||
// Set up the search plugin.
|
||||
$this->plugin = $this->container->get('plugin.manager.search')->createInstance('node_search');
|
||||
|
||||
// Check indexing counts before adding any nodes.
|
||||
$this->assertIndexCounts(0, 0, 'before adding nodes');
|
||||
$this->assertDatabaseCounts(0, 0, 'before adding nodes');
|
||||
|
||||
// Add two new languages.
|
||||
ConfigurableLanguage::createFromLangcode('hu')->save();
|
||||
ConfigurableLanguage::createFromLangcode('sv')->save();
|
||||
|
||||
// Make the body field translatable. The title is already translatable by
|
||||
// definition. The parent class has already created the article and page
|
||||
// content types.
|
||||
$field_storage = FieldStorageConfig::loadByName('node', 'body');
|
||||
$field_storage->setTranslatable(TRUE);
|
||||
$field_storage->save();
|
||||
|
||||
// Create a few page nodes with multilingual body values.
|
||||
$default_format = filter_default_format();
|
||||
$nodes = [
|
||||
[
|
||||
'title' => 'First node en',
|
||||
'type' => 'page',
|
||||
'body' => [['value' => $this->randomMachineName(32), 'format' => $default_format]],
|
||||
'langcode' => 'en',
|
||||
],
|
||||
[
|
||||
'title' => 'Second node this is the English title',
|
||||
'type' => 'page',
|
||||
'body' => [['value' => $this->randomMachineName(32), 'format' => $default_format]],
|
||||
'langcode' => 'en',
|
||||
],
|
||||
[
|
||||
'title' => 'Third node en',
|
||||
'type' => 'page',
|
||||
'body' => [['value' => $this->randomMachineName(32), 'format' => $default_format]],
|
||||
'langcode' => 'en',
|
||||
],
|
||||
// After the third node, we don't care what the settings are. But we
|
||||
// need to have at least 5 to make sure the throttling is working
|
||||
// correctly. So, let's make 8 total.
|
||||
[
|
||||
],
|
||||
[
|
||||
],
|
||||
[
|
||||
],
|
||||
[
|
||||
],
|
||||
[
|
||||
],
|
||||
];
|
||||
$this->searchableNodes = [];
|
||||
foreach ($nodes as $setting) {
|
||||
$this->searchableNodes[] = $this->drupalCreateNode($setting);
|
||||
}
|
||||
|
||||
// Add a single translation to the second node.
|
||||
$translation = $this->searchableNodes[1]->addTranslation('hu', ['title' => 'Second node hu']);
|
||||
$translation->body->value = $this->randomMachineName(32);
|
||||
$this->searchableNodes[1]->save();
|
||||
|
||||
// Add two translations to the third node.
|
||||
$translation = $this->searchableNodes[2]->addTranslation('hu', ['title' => 'Third node this is the Hungarian title']);
|
||||
$translation->body->value = $this->randomMachineName(32);
|
||||
$translation = $this->searchableNodes[2]->addTranslation('sv', ['title' => 'Third node sv']);
|
||||
$translation->body->value = $this->randomMachineName(32);
|
||||
$this->searchableNodes[2]->save();
|
||||
|
||||
// Verify that we have 8 nodes left to do.
|
||||
$this->assertIndexCounts(8, 8, 'before updating the search index');
|
||||
$this->assertDatabaseCounts(0, 0, 'before updating the search index');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the indexing throttle and search results with multilingual nodes.
|
||||
*/
|
||||
public function testMultilingualSearch() {
|
||||
// Index only 2 nodes per cron run. We cannot do this setting in the UI,
|
||||
// because it doesn't go this low.
|
||||
$this->config('search.settings')->set('index.cron_limit', 2)->save();
|
||||
// Get a new search plugin, to make sure it has this setting.
|
||||
$this->plugin = $this->container->get('plugin.manager.search')->createInstance('node_search');
|
||||
|
||||
// Update the index. This does the initial processing.
|
||||
$this->plugin->updateIndex();
|
||||
// Run the shutdown function. Testing is a unique case where indexing
|
||||
// and searching has to happen in the same request, so running the shutdown
|
||||
// function manually is needed to finish the indexing process.
|
||||
search_update_totals();
|
||||
$this->assertIndexCounts(6, 8, 'after updating partially');
|
||||
$this->assertDatabaseCounts(2, 0, 'after updating partially');
|
||||
|
||||
// Now index the rest of the nodes.
|
||||
// Make sure index throttle is high enough, via the UI.
|
||||
$this->drupalPostForm('admin/config/search/pages', ['cron_limit' => 20], t('Save configuration'));
|
||||
$this->assertEqual(20, $this->config('search.settings')->get('index.cron_limit', 100), 'Config setting was saved correctly');
|
||||
// Get a new search plugin, to make sure it has this setting.
|
||||
$this->plugin = $this->container->get('plugin.manager.search')->createInstance('node_search');
|
||||
|
||||
$this->plugin->updateIndex();
|
||||
search_update_totals();
|
||||
$this->assertIndexCounts(0, 8, 'after updating fully');
|
||||
$this->assertDatabaseCounts(8, 0, 'after updating fully');
|
||||
|
||||
// Click the reindex button on the admin page, verify counts, and reindex.
|
||||
$this->drupalPostForm('admin/config/search/pages', [], t('Re-index site'));
|
||||
$this->drupalPostForm(NULL, [], t('Re-index site'));
|
||||
$this->assertIndexCounts(8, 8, 'after reindex');
|
||||
$this->assertDatabaseCounts(8, 0, 'after reindex');
|
||||
$this->plugin->updateIndex();
|
||||
search_update_totals();
|
||||
|
||||
// Test search results.
|
||||
|
||||
// This should find two results for the second and third node.
|
||||
$this->plugin->setSearch('English OR Hungarian', [], []);
|
||||
$search_result = $this->plugin->execute();
|
||||
$this->assertEqual(count($search_result), 2, 'Found two results.');
|
||||
// Nodes are saved directly after each other and have the same created time
|
||||
// so testing for the order is not possible.
|
||||
$results = [$search_result[0]['title'], $search_result[1]['title']];
|
||||
$this->assertTrue(in_array('Third node this is the Hungarian title', $results), 'The search finds the correct Hungarian title.');
|
||||
$this->assertTrue(in_array('Second node this is the English title', $results), 'The search finds the correct English title.');
|
||||
|
||||
// Now filter for Hungarian results only.
|
||||
$this->plugin->setSearch('English OR Hungarian', ['f' => ['language:hu']], []);
|
||||
$search_result = $this->plugin->execute();
|
||||
|
||||
$this->assertEqual(count($search_result), 1, 'The search found only one result');
|
||||
$this->assertEqual($search_result[0]['title'], 'Third node this is the Hungarian title', 'The search finds the correct Hungarian title.');
|
||||
|
||||
// Test for search with common key word across multiple languages.
|
||||
$this->plugin->setSearch('node', [], []);
|
||||
$search_result = $this->plugin->execute();
|
||||
|
||||
$this->assertEqual(count($search_result), 6, 'The search found total six results');
|
||||
|
||||
// Test with language filters and common key word.
|
||||
$this->plugin->setSearch('node', ['f' => ['language:hu']], []);
|
||||
$search_result = $this->plugin->execute();
|
||||
|
||||
$this->assertEqual(count($search_result), 2, 'The search found 2 results');
|
||||
|
||||
// Test to check for the language of result items.
|
||||
foreach ($search_result as $result) {
|
||||
$this->assertEqual($result['langcode'], 'hu', 'The search found the correct Hungarian result');
|
||||
}
|
||||
|
||||
// Mark one of the nodes for reindexing, using the API function, and
|
||||
// verify indexing status.
|
||||
search_mark_for_reindex('node_search', $this->searchableNodes[0]->id());
|
||||
$this->assertIndexCounts(1, 8, 'after marking one node to reindex via API function');
|
||||
|
||||
// Update the index and verify the totals again.
|
||||
$this->plugin = $this->container->get('plugin.manager.search')->createInstance('node_search');
|
||||
$this->plugin->updateIndex();
|
||||
search_update_totals();
|
||||
$this->assertIndexCounts(0, 8, 'after indexing again');
|
||||
|
||||
// Mark one node for reindexing by saving it, and verify indexing status.
|
||||
$this->searchableNodes[1]->save();
|
||||
$this->assertIndexCounts(1, 8, 'after marking one node to reindex via save');
|
||||
|
||||
// The request time is always the same throughout test runs. Update the
|
||||
// request time to a previous time, to simulate it having been marked
|
||||
// previously.
|
||||
$current = REQUEST_TIME;
|
||||
$old = $current - 10;
|
||||
db_update('search_dataset')
|
||||
->fields(['reindex' => $old])
|
||||
->condition('reindex', $current, '>=')
|
||||
->execute();
|
||||
|
||||
// Save the node again. Verify that the request time on it is not updated.
|
||||
$this->searchableNodes[1]->save();
|
||||
$result = db_select('search_dataset', 'd')
|
||||
->fields('d', ['reindex'])
|
||||
->condition('type', 'node_search')
|
||||
->condition('sid', $this->searchableNodes[1]->id())
|
||||
->execute()
|
||||
->fetchField();
|
||||
$this->assertEqual($result, $old, 'Reindex time was not updated if node was already marked');
|
||||
|
||||
// Add a bogus entry to the search index table using a different search
|
||||
// type. This will not appear in the index status, because it is not
|
||||
// managed by a plugin.
|
||||
search_index('foo', $this->searchableNodes[0]->id(), 'en', 'some text');
|
||||
$this->assertIndexCounts(1, 8, 'after adding a different index item');
|
||||
|
||||
// Mark just this "foo" index for reindexing.
|
||||
search_mark_for_reindex('foo');
|
||||
$this->assertIndexCounts(1, 8, 'after reindexing the other search type');
|
||||
|
||||
// Mark everything for reindexing.
|
||||
search_mark_for_reindex();
|
||||
$this->assertIndexCounts(8, 8, 'after reindexing everything');
|
||||
|
||||
// Clear one item from the index, but with wrong language.
|
||||
$this->assertDatabaseCounts(8, 1, 'before clear');
|
||||
search_index_clear('node_search', $this->searchableNodes[0]->id(), 'hu');
|
||||
$this->assertDatabaseCounts(8, 1, 'after clear with wrong language');
|
||||
// Clear using correct language.
|
||||
search_index_clear('node_search', $this->searchableNodes[0]->id(), 'en');
|
||||
$this->assertDatabaseCounts(7, 1, 'after clear with right language');
|
||||
// Don't specify language.
|
||||
search_index_clear('node_search', $this->searchableNodes[1]->id());
|
||||
$this->assertDatabaseCounts(6, 1, 'unspecified language clear');
|
||||
// Clear everything in 'foo'.
|
||||
search_index_clear('foo');
|
||||
$this->assertDatabaseCounts(6, 0, 'other index clear');
|
||||
// Clear everything.
|
||||
search_index_clear();
|
||||
$this->assertDatabaseCounts(0, 0, 'complete clear');
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the indexing status counts.
|
||||
*
|
||||
* @param int $remaining
|
||||
* Count of remaining items to verify.
|
||||
* @param int $total
|
||||
* Count of total items to verify.
|
||||
* @param string $message
|
||||
* Message to use, something like "after updating the search index".
|
||||
*/
|
||||
protected function assertIndexCounts($remaining, $total, $message) {
|
||||
// Check status via plugin method call.
|
||||
$status = $this->plugin->indexStatus();
|
||||
$this->assertEqual($status['remaining'], $remaining, 'Remaining items ' . $message . ' is ' . $remaining);
|
||||
$this->assertEqual($status['total'], $total, 'Total items ' . $message . ' is ' . $total);
|
||||
|
||||
// Check text in progress section of Search settings page. Note that this
|
||||
// test avoids using
|
||||
// \Drupal\Core\StringTranslation\TranslationInterface::formatPlural(), so
|
||||
// it tests for fragments of text.
|
||||
$indexed = $total - $remaining;
|
||||
$percent = ($total > 0) ? floor(100 * $indexed / $total) : 100;
|
||||
$this->drupalGet('admin/config/search/pages');
|
||||
$this->assertText($percent . '% of the site has been indexed.', 'Progress percent text at top of Search settings page is correct at: ' . $message);
|
||||
$this->assertText($remaining . ' item', 'Remaining text at top of Search settings page is correct at: ' . $message);
|
||||
|
||||
// Check text in pages section of Search settings page.
|
||||
$this->assertText($indexed . ' of ' . $total . ' indexed', 'Progress text in pages section of Search settings page is correct at: ' . $message);
|
||||
|
||||
// Check text on status report page.
|
||||
$this->drupalGet('admin/reports/status');
|
||||
$this->assertText('Search index progress', 'Search status section header is present on status report page');
|
||||
$this->assertText($percent . '%', 'Correct percentage is shown on status report page at: ' . $message);
|
||||
$this->assertText('(' . $remaining . ' remaining)', 'Correct remaining value is shown on status report page at: ' . $message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks actual database counts of items in the search index.
|
||||
*
|
||||
* @param int $count_node
|
||||
* Count of node items to assert.
|
||||
* @param int $count_foo
|
||||
* Count of "foo" items to assert.
|
||||
* @param string $message
|
||||
* Message suffix to use.
|
||||
*/
|
||||
protected function assertDatabaseCounts($count_node, $count_foo, $message) {
|
||||
// Count number of distinct nodes by ID.
|
||||
$results = db_select('search_dataset', 'i')
|
||||
->fields('i', ['sid'])
|
||||
->condition('type', 'node_search')
|
||||
->groupBy('sid')
|
||||
->execute()
|
||||
->fetchCol();
|
||||
$this->assertEqual($count_node, count($results), 'Node count was ' . $count_node . ' for ' . $message);
|
||||
|
||||
// Count number of "foo" records.
|
||||
$results = db_select('search_dataset', 'i')
|
||||
->fields('i', ['sid'])
|
||||
->condition('type', 'foo')
|
||||
->execute()
|
||||
->fetchCol();
|
||||
$this->assertEqual($count_foo, count($results), 'Foo count was ' . $count_foo . ' for ' . $message);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\search\Functional;
|
||||
|
||||
/**
|
||||
* Tests search functionality with diacritics.
|
||||
*
|
||||
* @group search
|
||||
*/
|
||||
class SearchNodeDiacriticsTest extends SearchTestBase {
|
||||
|
||||
/**
|
||||
* A user with permission to use advanced search.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
public $testUser;
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
node_access_rebuild();
|
||||
|
||||
// Create a test user and log in.
|
||||
$this->testUser = $this->drupalCreateUser(['access content', 'search content', 'use advanced search', 'access user profiles']);
|
||||
$this->drupalLogin($this->testUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that search returns results with diacritics in the search phrase.
|
||||
*/
|
||||
public function testPhraseSearchPunctuation() {
|
||||
$body_text = 'The Enricþment Center is cómmīŦŧęđ to the well BɆĬŇĜ of æll påŔťıçȉpǎǹţș. ';
|
||||
$body_text .= 'Also meklēt (see #731298)';
|
||||
$this->drupalCreateNode(['body' => [['value' => $body_text]]]);
|
||||
|
||||
// Update the search index.
|
||||
$this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex();
|
||||
search_update_totals();
|
||||
|
||||
// Refresh variables after the treatment.
|
||||
$this->refreshVariables();
|
||||
|
||||
$edit = ['keys' => 'meklet'];
|
||||
$this->drupalPostForm('search/node', $edit, t('Search'));
|
||||
$this->assertRaw('<strong>meklēt</strong>');
|
||||
|
||||
$edit = ['keys' => 'meklēt'];
|
||||
$this->drupalPostForm('search/node', $edit, t('Search'));
|
||||
$this->assertRaw('<strong>meklēt</strong>');
|
||||
|
||||
$edit = ['keys' => 'cómmīŦŧęđ BɆĬŇĜ påŔťıçȉpǎǹţș'];
|
||||
$this->drupalPostForm('search/node', $edit, t('Search'));
|
||||
$this->assertRaw('<strong>cómmīŦŧęđ</strong>');
|
||||
$this->assertRaw('<strong>BɆĬŇĜ</strong>');
|
||||
$this->assertRaw('<strong>påŔťıçȉpǎǹţș</strong>');
|
||||
|
||||
$edit = ['keys' => 'committed being participants'];
|
||||
$this->drupalPostForm('search/node', $edit, t('Search'));
|
||||
$this->assertRaw('<strong>cómmīŦŧęđ</strong>');
|
||||
$this->assertRaw('<strong>BɆĬŇĜ</strong>');
|
||||
$this->assertRaw('<strong>påŔťıçȉpǎǹţș</strong>');
|
||||
|
||||
$edit = ['keys' => 'Enricþment'];
|
||||
$this->drupalPostForm('search/node', $edit, t('Search'));
|
||||
$this->assertRaw('<strong>Enricþment</strong>');
|
||||
|
||||
$edit = ['keys' => 'Enritchment'];
|
||||
$this->drupalPostForm('search/node', $edit, t('Search'));
|
||||
$this->assertNoRaw('<strong>Enricþment</strong>');
|
||||
|
||||
$edit = ['keys' => 'æll'];
|
||||
$this->drupalPostForm('search/node', $edit, t('Search'));
|
||||
$this->assertRaw('<strong>æll</strong>');
|
||||
|
||||
$edit = ['keys' => 'all'];
|
||||
$this->drupalPostForm('search/node', $edit, t('Search'));
|
||||
$this->assertNoRaw('<strong>æll</strong>');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\search\Functional;
|
||||
|
||||
/**
|
||||
* Tests search functionality with punctuation and HTML entities.
|
||||
*
|
||||
* @group search
|
||||
*/
|
||||
class SearchNodePunctuationTest extends SearchTestBase {
|
||||
|
||||
/**
|
||||
* A user with permission to use advanced search.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
public $testUser;
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
node_access_rebuild();
|
||||
|
||||
// Create a test user and log in.
|
||||
$this->testUser = $this->drupalCreateUser(['access content', 'search content', 'use advanced search', 'access user profiles']);
|
||||
$this->drupalLogin($this->testUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that search works with punctuation and HTML entities.
|
||||
*/
|
||||
public function testPhraseSearchPunctuation() {
|
||||
$node = $this->drupalCreateNode(['body' => [['value' => "The bunny's ears were fluffy."]]]);
|
||||
$node2 = $this->drupalCreateNode(['body' => [['value' => 'Dignissim Aliquam & Quieligo meus natu quae quia te. Damnum© erat— neo pneum. Facilisi feugiat ibidem ratis.']]]);
|
||||
|
||||
// Update the search index.
|
||||
$this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex();
|
||||
search_update_totals();
|
||||
|
||||
// Refresh variables after the treatment.
|
||||
$this->refreshVariables();
|
||||
|
||||
// Submit a phrase wrapped in double quotes to include the punctuation.
|
||||
$edit = ['keys' => '"bunny\'s"'];
|
||||
$this->drupalPostForm('search/node', $edit, t('Search'));
|
||||
$this->assertText($node->label());
|
||||
|
||||
// Check if the author is linked correctly to the user profile page.
|
||||
$username = $node->getOwner()->getUsername();
|
||||
$this->assertLink($username);
|
||||
|
||||
// Search for "&" and verify entities are not broken up in the output.
|
||||
$edit = ['keys' => '&'];
|
||||
$this->drupalPostForm('search/node', $edit, t('Search'));
|
||||
$this->assertNoRaw('<strong>&</strong>amp;');
|
||||
$this->assertText('You must include at least one keyword');
|
||||
|
||||
$edit = ['keys' => '&'];
|
||||
$this->drupalPostForm('search/node', $edit, t('Search'));
|
||||
$this->assertNoRaw('<strong>&</strong>amp;');
|
||||
$this->assertText('You must include at least one keyword');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\search\Functional;
|
||||
|
||||
/**
|
||||
* Tests if the result page can be overridden.
|
||||
*
|
||||
* Verifies that a plugin can override the buildResults() method to
|
||||
* control what the search results page looks like.
|
||||
*
|
||||
* @group search
|
||||
*/
|
||||
class SearchPageOverrideTest extends SearchTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = ['search_extra_type'];
|
||||
|
||||
/**
|
||||
* A user with permission to administer search.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
public $searchUser;
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
// Log in as a user that can create and search content.
|
||||
$this->searchUser = $this->drupalCreateUser(['search content', 'administer search']);
|
||||
$this->drupalLogin($this->searchUser);
|
||||
}
|
||||
|
||||
public function testSearchPageHook() {
|
||||
$keys = 'bike shed ' . $this->randomMachineName();
|
||||
$this->drupalGet("search/dummy_path", ['query' => ['keys' => $keys]]);
|
||||
$this->assertText('Dummy search snippet', 'Dummy search snippet is shown');
|
||||
$this->assertText('Test page text is here', 'Page override is working');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\search\Functional;
|
||||
|
||||
/**
|
||||
* Tests that search works with numeric locale settings.
|
||||
*
|
||||
* @group search
|
||||
*/
|
||||
class SearchSetLocaleTest extends SearchTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = ['comment'];
|
||||
|
||||
/**
|
||||
* A node search plugin instance.
|
||||
*
|
||||
* @var \Drupal\search\Plugin\SearchInterface
|
||||
*/
|
||||
protected $nodeSearchPlugin;
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
// Create a plugin instance.
|
||||
$this->nodeSearchPlugin = $this->container->get('plugin.manager.search')->createInstance('node_search');
|
||||
// Create a node with a very simple body.
|
||||
$this->drupalCreateNode(['body' => [['value' => 'tapir']]]);
|
||||
// Update the search index.
|
||||
$this->nodeSearchPlugin->updateIndex();
|
||||
search_update_totals();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify that search works with a numeric locale set.
|
||||
*/
|
||||
public function testSearchWithNumericLocale() {
|
||||
// French decimal point is comma.
|
||||
setlocale(LC_NUMERIC, 'fr_FR');
|
||||
$this->nodeSearchPlugin->setSearch('tapir', [], []);
|
||||
// The call to execute will throw an exception if a float in the wrong
|
||||
// format is passed in the query to the database, so an assertion is not
|
||||
// necessary here.
|
||||
$this->nodeSearchPlugin->execute();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\search\Functional;
|
||||
use Drupal\Component\Utility\Unicode;
|
||||
|
||||
/**
|
||||
* Tests that the search_simply() function works as intended.
|
||||
*
|
||||
* @group search
|
||||
*/
|
||||
class SearchSimplifyTest extends SearchTestBase {
|
||||
/**
|
||||
* Tests that all Unicode characters simplify correctly.
|
||||
*/
|
||||
public function testSearchSimplifyUnicode() {
|
||||
// This test uses a file that was constructed so that the even lines are
|
||||
// boundary characters, and the odd lines are valid word characters. (It
|
||||
// was generated as a sequence of all the Unicode characters, and then the
|
||||
// boundary characters (punctuation, spaces, etc.) were split off into
|
||||
// their own lines). So the even-numbered lines should simplify to nothing,
|
||||
// and the odd-numbered lines we need to split into shorter chunks and
|
||||
// verify that simplification doesn't lose any characters.
|
||||
$input = file_get_contents(\Drupal::root() . '/core/modules/search/tests/UnicodeTest.txt');
|
||||
$basestrings = explode(chr(10), $input);
|
||||
$strings = [];
|
||||
foreach ($basestrings as $key => $string) {
|
||||
if ($key % 2) {
|
||||
// Even line - should simplify down to a space.
|
||||
$simplified = search_simplify($string);
|
||||
$this->assertIdentical($simplified, ' ', "Line $key is excluded from the index");
|
||||
}
|
||||
else {
|
||||
// Odd line, should be word characters.
|
||||
// Split this into 30-character chunks, so we don't run into limits
|
||||
// of truncation in search_simplify().
|
||||
$start = 0;
|
||||
while ($start < Unicode::strlen($string)) {
|
||||
$newstr = Unicode::substr($string, $start, 30);
|
||||
// Special case: leading zeros are removed from numeric strings,
|
||||
// and there's one string in this file that is numbers starting with
|
||||
// zero, so prepend a 1 on that string.
|
||||
if (preg_match('/^[0-9]+$/', $newstr)) {
|
||||
$newstr = '1' . $newstr;
|
||||
}
|
||||
$strings[] = $newstr;
|
||||
$start += 30;
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($strings as $key => $string) {
|
||||
$simplified = search_simplify($string);
|
||||
$this->assertTrue(Unicode::strlen($simplified) >= Unicode::strlen($string), "Nothing is removed from string $key.");
|
||||
}
|
||||
|
||||
// Test the low-numbered ASCII control characters separately. They are not
|
||||
// in the text file because they are problematic for diff, especially \0.
|
||||
$string = '';
|
||||
for ($i = 0; $i < 32; $i++) {
|
||||
$string .= chr($i);
|
||||
}
|
||||
$this->assertIdentical(' ', search_simplify($string), 'Search simplify works for ASCII control characters.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that search_simplify() does the right thing with punctuation.
|
||||
*/
|
||||
public function testSearchSimplifyPunctuation() {
|
||||
$cases = [
|
||||
['20.03/94-28,876', '20039428876', 'Punctuation removed from numbers'],
|
||||
['great...drupal--module', 'great drupal module', 'Multiple dot and dashes are word boundaries'],
|
||||
['very_great-drupal.module', 'verygreatdrupalmodule', 'Single dot, dash, underscore are removed'],
|
||||
['regular,punctuation;word', 'regular punctuation word', 'Punctuation is a word boundary'],
|
||||
];
|
||||
|
||||
foreach ($cases as $case) {
|
||||
$out = trim(search_simplify($case[0]));
|
||||
$this->assertEqual($out, $case[1], $case[2]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\search\Functional;
|
||||
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
use Drupal\Component\Utility\SafeMarkup;
|
||||
|
||||
/**
|
||||
* Defines the common search test code.
|
||||
*/
|
||||
abstract class SearchTestBase extends BrowserTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = ['node', 'search', 'dblog'];
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
// Create Basic page and Article node types.
|
||||
if ($this->profile != 'standard') {
|
||||
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
|
||||
$this->drupalCreateContentType(['type' => 'article', 'name' => 'Article']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulates submission of a form using GET instead of POST.
|
||||
*
|
||||
* Forms that use the GET method cannot be submitted with
|
||||
* WebTestBase::drupalPostForm(), which explicitly uses POST to submit the
|
||||
* form. So this method finds the form, verifies that it has input fields and
|
||||
* a submit button matching the inputs to this method, and then calls
|
||||
* WebTestBase::drupalGet() to simulate the form submission to the 'action'
|
||||
* URL of the form (if set, or the current URL if not).
|
||||
*
|
||||
* See WebTestBase::drupalPostForm() for more detailed documentation of the
|
||||
* function parameters.
|
||||
*
|
||||
* @param string $path
|
||||
* Location of the form to be submitted: either a Drupal path, absolute
|
||||
* path, or NULL to use the current page.
|
||||
* @param array $edit
|
||||
* Form field data to submit. Unlike drupalPostForm(), this does not support
|
||||
* file uploads.
|
||||
* @param string $submit
|
||||
* Value of the submit button to submit clicking. Unlike drupalPostForm(),
|
||||
* this does not support AJAX.
|
||||
* @param string $form_html_id
|
||||
* (optional) HTML ID of the form, to disambiguate.
|
||||
*/
|
||||
protected function submitGetForm($path, $edit, $submit, $form_html_id = NULL) {
|
||||
if (isset($path)) {
|
||||
$this->drupalGet($path);
|
||||
}
|
||||
|
||||
if ($this->parse()) {
|
||||
// Iterate over forms to find one that matches $edit and $submit.
|
||||
$edit_save = $edit;
|
||||
$xpath = '//form';
|
||||
if (!empty($form_html_id)) {
|
||||
$xpath .= "[@id='" . $form_html_id . "']";
|
||||
}
|
||||
$forms = $this->xpath($xpath);
|
||||
foreach ($forms as $form) {
|
||||
// Try to set the fields of this form as specified in $edit.
|
||||
$edit = $edit_save;
|
||||
$post = [];
|
||||
$upload = [];
|
||||
$submit_matches = $this->handleForm($post, $edit, $upload, $submit, $form);
|
||||
if (!$edit && $submit_matches) {
|
||||
// Everything matched, so "submit" the form.
|
||||
$action = isset($form['action']) ? $this->getAbsoluteUrl((string) $form['action']) : NULL;
|
||||
$this->drupalGet($action, ['query' => $post]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// We have not found a form which contained all fields of $edit and
|
||||
// the submit button.
|
||||
foreach ($edit as $name => $value) {
|
||||
$this->fail(SafeMarkup::format('Failed to set field @name to @value', ['@name' => $name, '@value' => $value]));
|
||||
}
|
||||
$this->assertTrue($submit_matches, format_string('Found the @submit button', ['@submit' => $submit]));
|
||||
$this->fail(format_string('Found the requested form fields at @path', ['@path' => $path]));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\search\Functional;
|
||||
use Drupal\Component\Utility\Unicode;
|
||||
|
||||
/**
|
||||
* Tests that CJK tokenizer works as intended.
|
||||
*
|
||||
* @group search
|
||||
*/
|
||||
class SearchTokenizerTest extends SearchTestBase {
|
||||
|
||||
/**
|
||||
* Verifies that strings of CJK characters are tokenized.
|
||||
*
|
||||
* The search_simplify() function does special things with numbers, symbols,
|
||||
* and punctuation. So we only test that CJK characters that are not in these
|
||||
* character classes are tokenized properly. See PREG_CLASS_CKJ for more
|
||||
* information.
|
||||
*/
|
||||
public function testTokenizer() {
|
||||
// Set the minimum word size to 1 (to split all CJK characters) and make
|
||||
// sure CJK tokenizing is turned on.
|
||||
$this->config('search.settings')
|
||||
->set('index.minimum_word_size', 1)
|
||||
->set('index.overlap_cjk', TRUE)
|
||||
->save();
|
||||
$this->refreshVariables();
|
||||
|
||||
// Create a string of CJK characters from various character ranges in
|
||||
// the Unicode tables.
|
||||
|
||||
// Beginnings of the character ranges.
|
||||
$starts = [
|
||||
'CJK unified' => 0x4e00,
|
||||
'CJK Ext A' => 0x3400,
|
||||
'CJK Compat' => 0xf900,
|
||||
'Hangul Jamo' => 0x1100,
|
||||
'Hangul Ext A' => 0xa960,
|
||||
'Hangul Ext B' => 0xd7b0,
|
||||
'Hangul Compat' => 0x3131,
|
||||
'Half non-punct 1' => 0xff21,
|
||||
'Half non-punct 2' => 0xff41,
|
||||
'Half non-punct 3' => 0xff66,
|
||||
'Hangul Syllables' => 0xac00,
|
||||
'Hiragana' => 0x3040,
|
||||
'Katakana' => 0x30a1,
|
||||
'Katakana Ext' => 0x31f0,
|
||||
'CJK Reserve 1' => 0x20000,
|
||||
'CJK Reserve 2' => 0x30000,
|
||||
'Bomofo' => 0x3100,
|
||||
'Bomofo Ext' => 0x31a0,
|
||||
'Lisu' => 0xa4d0,
|
||||
'Yi' => 0xa000,
|
||||
];
|
||||
|
||||
// Ends of the character ranges.
|
||||
$ends = [
|
||||
'CJK unified' => 0x9fcf,
|
||||
'CJK Ext A' => 0x4dbf,
|
||||
'CJK Compat' => 0xfaff,
|
||||
'Hangul Jamo' => 0x11ff,
|
||||
'Hangul Ext A' => 0xa97f,
|
||||
'Hangul Ext B' => 0xd7ff,
|
||||
'Hangul Compat' => 0x318e,
|
||||
'Half non-punct 1' => 0xff3a,
|
||||
'Half non-punct 2' => 0xff5a,
|
||||
'Half non-punct 3' => 0xffdc,
|
||||
'Hangul Syllables' => 0xd7af,
|
||||
'Hiragana' => 0x309f,
|
||||
'Katakana' => 0x30ff,
|
||||
'Katakana Ext' => 0x31ff,
|
||||
'CJK Reserve 1' => 0x2fffd,
|
||||
'CJK Reserve 2' => 0x3fffd,
|
||||
'Bomofo' => 0x312f,
|
||||
'Bomofo Ext' => 0x31b7,
|
||||
'Lisu' => 0xa4fd,
|
||||
'Yi' => 0xa48f,
|
||||
];
|
||||
|
||||
// Generate characters consisting of starts, midpoints, and ends.
|
||||
$chars = [];
|
||||
$charcodes = [];
|
||||
foreach ($starts as $key => $value) {
|
||||
$charcodes[] = $starts[$key];
|
||||
$chars[] = $this->code2utf($starts[$key]);
|
||||
$mid = round(0.5 * ($starts[$key] + $ends[$key]));
|
||||
$charcodes[] = $mid;
|
||||
$chars[] = $this->code2utf($mid);
|
||||
$charcodes[] = $ends[$key];
|
||||
$chars[] = $this->code2utf($ends[$key]);
|
||||
}
|
||||
|
||||
// Merge into a string and tokenize.
|
||||
$string = implode('', $chars);
|
||||
$out = trim(search_simplify($string));
|
||||
$expected = Unicode::strtolower(implode(' ', $chars));
|
||||
|
||||
// Verify that the output matches what we expect.
|
||||
$this->assertEqual($out, $expected, 'CJK tokenizer worked on all supplied CJK characters');
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that strings of non-CJK characters are not tokenized.
|
||||
*
|
||||
* This is just a sanity check - it verifies that strings of letters are
|
||||
* not tokenized.
|
||||
*/
|
||||
public function testNoTokenizer() {
|
||||
// Set the minimum word size to 1 (to split all CJK characters) and make
|
||||
// sure CJK tokenizing is turned on.
|
||||
$this->config('search.settings')
|
||||
->set('index.minimum_word_size', 1)
|
||||
->set('index.overlap_cjk', TRUE)
|
||||
->save();
|
||||
$this->refreshVariables();
|
||||
|
||||
$letters = 'abcdefghijklmnopqrstuvwxyz';
|
||||
$out = trim(search_simplify($letters));
|
||||
|
||||
$this->assertEqual($letters, $out, 'Letters are not CJK tokenized');
|
||||
}
|
||||
|
||||
/**
|
||||
* Like PHP chr() function, but for unicode characters.
|
||||
*
|
||||
* chr() only works for ASCII characters up to character 255. This function
|
||||
* converts a number to the corresponding unicode character. Adapted from
|
||||
* functions supplied in comments on several functions on php.net.
|
||||
*/
|
||||
public function code2utf($num) {
|
||||
if ($num < 128) {
|
||||
return chr($num);
|
||||
}
|
||||
|
||||
if ($num < 2048) {
|
||||
return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
|
||||
}
|
||||
|
||||
if ($num < 65536) {
|
||||
return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
|
||||
}
|
||||
|
||||
if ($num < 2097152) {
|
||||
return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\search\Kernel\Migrate\d6;
|
||||
|
||||
use Drupal\Core\Database\Database;
|
||||
use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
|
||||
use Drupal\search\Entity\SearchPage;
|
||||
|
||||
/**
|
||||
* Upgrade search rank settings to search.page.*.yml.
|
||||
*
|
||||
* @group migrate_drupal_6
|
||||
*/
|
||||
class MigrateSearchPageTest extends MigrateDrupal6TestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = ['search'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
$this->executeMigration('search_page');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests Drupal 6 search settings to Drupal 8 search page entity migration.
|
||||
*/
|
||||
public function testSearchPage() {
|
||||
$id = 'node_search';
|
||||
/** @var \Drupal\search\Entity\SearchPage $search_page */
|
||||
$search_page = SearchPage::load($id);
|
||||
$this->assertIdentical($id, $search_page->id());
|
||||
$configuration = $search_page->getPlugin()->getConfiguration();
|
||||
$this->assertIdentical($configuration['rankings'], [
|
||||
'comments' => 5,
|
||||
'promote' => 0,
|
||||
'recent' => 0,
|
||||
'relevance' => 2,
|
||||
'sticky' => 8,
|
||||
'views' => 1,
|
||||
]);
|
||||
$this->assertIdentical('node', $search_page->getPath());
|
||||
|
||||
// Test that we can re-import using the EntitySearchPage destination.
|
||||
Database::getConnection('default', 'migrate')
|
||||
->update('variable')
|
||||
->fields(['value' => serialize(4)])
|
||||
->condition('name', 'node_rank_comments')
|
||||
->execute();
|
||||
|
||||
/** @var \Drupal\migrate\Plugin\MigrationInterface $migration */
|
||||
$migration = $this->getMigration('search_page');
|
||||
// Indicate we're rerunning a migration that's already run.
|
||||
$migration->getIdMap()->prepareUpdate();
|
||||
$this->executeMigration($migration);
|
||||
|
||||
$configuration = SearchPage::load($id)->getPlugin()->getConfiguration();
|
||||
$this->assertIdentical(4, $configuration['rankings']['comments']);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\search\Kernel\Migrate\d6;
|
||||
|
||||
use Drupal\Tests\SchemaCheckTestTrait;
|
||||
use Drupal\Tests\migrate_drupal\Kernel\d6\MigrateDrupal6TestBase;
|
||||
|
||||
/**
|
||||
* Upgrade variables to search.settings.yml.
|
||||
*
|
||||
* @group migrate_drupal_6
|
||||
*/
|
||||
class MigrateSearchSettingsTest extends MigrateDrupal6TestBase {
|
||||
|
||||
use SchemaCheckTestTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = ['search'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
$this->executeMigration('d6_search_settings');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests migration of search variables to search.settings.yml.
|
||||
*/
|
||||
public function testSearchSettings() {
|
||||
$config = $this->config('search.settings');
|
||||
$this->assertIdentical(3, $config->get('index.minimum_word_size'));
|
||||
$this->assertIdentical(TRUE, $config->get('index.overlap_cjk'));
|
||||
$this->assertIdentical(100, $config->get('index.cron_limit'));
|
||||
$this->assertIdentical(TRUE, $config->get('logging'));
|
||||
$this->assertConfigSchema(\Drupal::service('config.typed'), 'search.settings', $config->get());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\search\Kernel\Migrate\d7;
|
||||
|
||||
use Drupal\Core\Database\Database;
|
||||
use Drupal\Tests\migrate_drupal\Kernel\d7\MigrateDrupal7TestBase;
|
||||
use Drupal\search\Entity\SearchPage;
|
||||
|
||||
/**
|
||||
* Upgrade search rank settings to search.page.*.yml.
|
||||
*
|
||||
* @group migrate_drupal_7
|
||||
*/
|
||||
class MigrateSearchPageTest extends MigrateDrupal7TestBase {
|
||||
|
||||
/**
|
||||
* The modules to be enabled during the test.
|
||||
*
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = ['node', 'search'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
$this->executeMigration('search_page');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests Drupal 7 search ranking to Drupal 8 search page entity migration.
|
||||
*/
|
||||
public function testSearchPage() {
|
||||
$id = 'node_search';
|
||||
/** @var \Drupal\search\Entity\SearchPage $search_page */
|
||||
$search_page = SearchPage::load($id);
|
||||
$this->assertIdentical($id, $search_page->id());
|
||||
$configuration = $search_page->getPlugin()->getConfiguration();
|
||||
$expected_rankings = [
|
||||
'comments' => 0,
|
||||
'promote' => 0,
|
||||
'relevance' => 2,
|
||||
'sticky' => 0,
|
||||
'views' => 0,
|
||||
];
|
||||
$this->assertIdentical($expected_rankings, $configuration['rankings']);
|
||||
$this->assertIdentical('node', $search_page->getPath());
|
||||
|
||||
// Test that we can re-import using the EntitySearchPage destination.
|
||||
Database::getConnection('default', 'migrate')
|
||||
->update('variable')
|
||||
->fields(['value' => serialize(4)])
|
||||
->condition('name', 'node_rank_comments')
|
||||
->execute();
|
||||
|
||||
/** @var \Drupal\migrate\Plugin\MigrationInterface $migration */
|
||||
$migration = $this->getMigration('search_page');
|
||||
// Indicate we're rerunning a migration that's already run.
|
||||
$migration->getIdMap()->prepareUpdate();
|
||||
$this->executeMigration($migration);
|
||||
|
||||
$configuration = SearchPage::load($id)->getPlugin()->getConfiguration();
|
||||
$this->assertIdentical(4, $configuration['rankings']['comments']);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\search\Kernel\Migrate\d7;
|
||||
|
||||
use Drupal\Tests\migrate_drupal\Kernel\d7\MigrateDrupal7TestBase;
|
||||
|
||||
/**
|
||||
* Tests migration of Search variables to configuration.
|
||||
*
|
||||
* @group search
|
||||
*/
|
||||
class MigrateSearchSettingsTest extends MigrateDrupal7TestBase {
|
||||
|
||||
public static $modules = ['search'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
$this->executeMigration('d7_search_settings');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the migration of Search's variables to configuration.
|
||||
*/
|
||||
public function testSearchSettings() {
|
||||
$config = $this->config('search.settings');
|
||||
$this->assertIdentical('node_search', $config->get('default_page'));
|
||||
$this->assertIdentical(4, $config->get('index.minimum_word_size'));
|
||||
$this->assertTrue($config->get('index.overlap_cjk'));
|
||||
$this->assertIdentical(100, $config->get('index.cron_limit'));
|
||||
$this->assertIdentical(7, $config->get('and_or_limit'));
|
||||
$this->assertIdentical(25, $config->get('index.tag_weights.h1'));
|
||||
$this->assertIdentical(18, $config->get('index.tag_weights.h2'));
|
||||
$this->assertIdentical(15, $config->get('index.tag_weights.h3'));
|
||||
$this->assertIdentical(12, $config->get('index.tag_weights.h4'));
|
||||
$this->assertIdentical(9, $config->get('index.tag_weights.h5'));
|
||||
$this->assertIdentical(6, $config->get('index.tag_weights.h6'));
|
||||
$this->assertIdentical(3, $config->get('index.tag_weights.u'));
|
||||
$this->assertIdentical(3, $config->get('index.tag_weights.b'));
|
||||
$this->assertIdentical(3, $config->get('index.tag_weights.i'));
|
||||
$this->assertIdentical(3, $config->get('index.tag_weights.strong'));
|
||||
$this->assertIdentical(3, $config->get('index.tag_weights.em'));
|
||||
$this->assertIdentical(10, $config->get('index.tag_weights.a'));
|
||||
$this->assertTrue($config->get('logging'));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\search\Kernel;
|
||||
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
|
||||
/**
|
||||
* Tests the search_excerpt() function.
|
||||
*
|
||||
* @group search
|
||||
*/
|
||||
class SearchExcerptTest extends KernelTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = ['search', 'search_langcode_test'];
|
||||
|
||||
/**
|
||||
* Tests search_excerpt() with several simulated search keywords.
|
||||
*
|
||||
* Passes keywords and a sample marked up string, "The quick
|
||||
* brown fox jumps over the lazy dog", and compares it to the
|
||||
* correctly marked up string. The correctly marked up string
|
||||
* contains either highlighted keywords or the original marked
|
||||
* up string if no keywords matched the string.
|
||||
*/
|
||||
public function testSearchExcerpt() {
|
||||
// Make some text with entities and tags.
|
||||
$text = 'The <strong>quick</strong> <a href="#">brown</a> fox & jumps <h2>over</h2> the lazy dog';
|
||||
$expected = 'The quick brown fox & jumps over the lazy dog';
|
||||
$result = $this->doSearchExcerpt('nothing', $text);
|
||||
$this->assertEqual(preg_replace('| +|', ' ', $result), $expected, 'Entire string, stripped of HTML tags, is returned when keyword is not found in short string');
|
||||
|
||||
$result = $this->doSearchExcerpt('fox', $text);
|
||||
$this->assertEqual($result, 'The quick brown <strong>fox</strong> & jumps over the lazy dog', 'Found keyword is highlighted');
|
||||
|
||||
$expected = '<strong>The</strong> quick brown fox & jumps over <strong>the</strong> lazy dog';
|
||||
$result = $this->doSearchExcerpt('The', $text);
|
||||
$this->assertEqual(preg_replace('| +|', ' ', $result), $expected, 'Keyword is highlighted at beginning of short string');
|
||||
|
||||
$expected = 'The quick brown fox & jumps over the lazy <strong>dog</strong>';
|
||||
$result = $this->doSearchExcerpt('dog', $text);
|
||||
$this->assertEqual(preg_replace('| +|', ' ', $result), $expected, 'Keyword is highlighted at end of short string');
|
||||
|
||||
$longtext = str_repeat(str_replace('brown', 'silver', $text) . ' ', 10) . $text . str_repeat(' ' . str_replace('brown', 'pink', $text), 10);
|
||||
$result = $this->doSearchExcerpt('brown', $longtext);
|
||||
$expected = '… silver fox & jumps over the lazy dog The quick <strong>brown</strong> fox & jumps over the lazy dog The quick …';
|
||||
$this->assertEqual($result, $expected, 'Snippet around keyword in long text is correctly capped');
|
||||
|
||||
$longtext = str_repeat($text . ' ', 10);
|
||||
$result = $this->doSearchExcerpt('nothing', $longtext);
|
||||
$expected = 'The quick brown fox & jumps over the lazy dog';
|
||||
$this->assertTrue(strpos($result, $expected) === 0, 'When keyword is not found in long string, return value starts as expected');
|
||||
|
||||
$entities = str_repeat('készítése ', 20);
|
||||
$result = $this->doSearchExcerpt('nothing', $entities);
|
||||
$this->assertFalse(strpos($result, '&'), 'Entities are not present in excerpt');
|
||||
$this->assertTrue(strpos($result, 'í') > 0, 'Entities are converted in excerpt');
|
||||
|
||||
// The node body that will produce this rendered $text is:
|
||||
// 123456789 HTMLTest +123456789+‘ +‘ +‘ +‘ +12345678 +‘ +‘ +‘ ‘
|
||||
$text = "<div class=\"field field--name-body field--type-text-with-summary field--label-hidden\"><div class=\"field__items\"><div class=\"field__item even\" property=\"content:encoded\"><p>123456789 HTMLTest +123456789+‘ +‘ +‘ +‘ +12345678 +‘ +‘ +‘ ‘</p>\n</div></div></div> ";
|
||||
$result = $this->doSearchExcerpt('HTMLTest', $text);
|
||||
$this->assertFalse(empty($result), 'Rendered Multi-byte HTML encodings are not corrupted in search excerpts');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests search_excerpt() with search keywords matching simplified words.
|
||||
*
|
||||
* Excerpting should handle keywords that are matched only after going through
|
||||
* search_simplify(). This test passes keywords that match simplified words
|
||||
* and compares them with strings that contain the original unsimplified word.
|
||||
*/
|
||||
public function testSearchExcerptSimplified() {
|
||||
$start_time = microtime(TRUE);
|
||||
|
||||
$lorem1 = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam vitae arcu at leo cursus laoreet. Curabitur dui tortor, adipiscing malesuada tempor in, bibendum ac diam. Cras non tellus a libero pellentesque condimentum. What is a Drupalism? Suspendisse ac lacus libero. Ut non est vel nisl faucibus interdum nec sed leo. Pellentesque sem risus, vulputate eu semper eget, auctor in libero.';
|
||||
$lorem2 = 'Ut fermentum est vitae metus convallis scelerisque. Phasellus pellentesque rhoncus tellus, eu dignissim purus posuere id. Quisque eu fringilla ligula. Morbi ullamcorper, lorem et mattis egestas, tortor neque pretium velit, eget eleifend odio turpis eu purus. Donec vitae metus quis leo pretium tincidunt a pulvinar sem. Morbi adipiscing laoreet mauris vel placerat. Nullam elementum, nisl sit amet scelerisque malesuada, dolor nunc hendrerit quam, eu ultrices erat est in orci.';
|
||||
|
||||
// Make some text with some keywords that will get simplified.
|
||||
$text = $lorem1 . ' Number: 123456.7890 Hyphenated: one-two abc,def ' . $lorem2;
|
||||
// Note: The search_excerpt() function adds some extra spaces -- not
|
||||
// important for HTML formatting. Remove these for comparison.
|
||||
$result = $this->doSearchExcerpt('123456.7890', $text);
|
||||
$this->assertTrue(strpos($result, 'Number: <strong>123456.7890</strong>') !== FALSE, 'Numeric keyword is highlighted with exact match');
|
||||
|
||||
$result = $this->doSearchExcerpt('1234567890', $text);
|
||||
$this->assertTrue(strpos($result, 'Number: <strong>123456.7890</strong>') !== FALSE, 'Numeric keyword is highlighted with simplified match');
|
||||
|
||||
$result = $this->doSearchExcerpt('Number 1234567890', $text);
|
||||
$this->assertTrue(strpos($result, '<strong>Number</strong>: <strong>123456.7890</strong>') !== FALSE, 'Punctuated and numeric keyword is highlighted with simplified match');
|
||||
|
||||
$result = $this->doSearchExcerpt('"Number 1234567890"', $text);
|
||||
$this->assertTrue(strpos($result, '<strong>Number: 123456.7890</strong>') !== FALSE, 'Phrase with punctuated and numeric keyword is highlighted with simplified match');
|
||||
|
||||
$result = $this->doSearchExcerpt('"Hyphenated onetwo"', $text);
|
||||
$this->assertTrue(strpos($result, '<strong>Hyphenated: one-two</strong>') !== FALSE, 'Phrase with punctuated and hyphenated keyword is highlighted with simplified match');
|
||||
|
||||
$result = $this->doSearchExcerpt('"abc def"', $text);
|
||||
$this->assertTrue(strpos($result, '<strong>abc,def</strong>') !== FALSE, 'Phrase with keyword simplified into two separate words is highlighted with simplified match');
|
||||
|
||||
// Test phrases with characters which are being truncated.
|
||||
$result = $this->doSearchExcerpt('"ipsum _"', $text);
|
||||
$this->assertTrue(strpos($result, '<strong>ipsum</strong>') !== FALSE, 'Only valid part of the phrase is highlighted and invalid part containing "_" is ignored.');
|
||||
|
||||
$result = $this->doSearchExcerpt('"ipsum 0000"', $text);
|
||||
$this->assertTrue(strpos($result, '<strong>ipsum</strong>') !== FALSE, 'Only valid part of the phrase is highlighted and invalid part "0000" is ignored.');
|
||||
|
||||
// Test combination of the valid keyword and keyword containing only
|
||||
// characters which are being truncated during simplification.
|
||||
$result = $this->doSearchExcerpt('ipsum _', $text);
|
||||
$this->assertTrue(strpos($result, '<strong>ipsum</strong>') !== FALSE, 'Only valid keyword is highlighted and invalid keyword "_" is ignored.');
|
||||
|
||||
$result = $this->doSearchExcerpt('ipsum 0000', $text);
|
||||
$this->assertTrue(strpos($result, '<strong>ipsum</strong>') !== FALSE, 'Only valid keyword is highlighted and invalid keyword "0000" is ignored.');
|
||||
|
||||
// Test using the hook_search_preprocess() from the test module.
|
||||
// The hook replaces "finding" or "finds" with "find".
|
||||
// So, if we search for "find" or "finds" or "finding", we should
|
||||
// highlight "finding".
|
||||
$text = "this tests finding a string";
|
||||
$result = $this->doSearchExcerpt('finds', $text, 'ex');
|
||||
$this->assertTrue(strpos($result, '<strong>finding</strong>') !== FALSE, 'Search excerpt works with preprocess hook, search for finds');
|
||||
$result = $this->doSearchExcerpt('find', $text, 'ex');
|
||||
$this->assertTrue(strpos($result, '<strong>finding</strong>') !== FALSE, 'Search excerpt works with preprocess hook, search for find');
|
||||
|
||||
// Just to be sure, test with the replacement at the beginning and end.
|
||||
$text = "finding at the beginning";
|
||||
$result = $this->doSearchExcerpt('finds', $text, 'ex');
|
||||
$this->assertTrue(strpos($result, '<strong>finding</strong>') !== FALSE, 'Search excerpt works with preprocess hook, text at start');
|
||||
|
||||
$text = "at the end finding";
|
||||
$result = $this->doSearchExcerpt('finds', $text, 'ex');
|
||||
$this->assertTrue(strpos($result, '<strong>finding</strong>') !== FALSE, 'Search excerpt works with preprocess hook, text at end');
|
||||
|
||||
// Testing with a one-to-many replacement: the test module replaces DIC
|
||||
// with Dependency Injection Container.
|
||||
$text = "something about the DIC is happening";
|
||||
$result = $this->doSearchExcerpt('Dependency', $text, 'ex');
|
||||
$this->assertTrue(strpos($result, '<strong>DIC</strong>') !== FALSE, 'Search excerpt works with preprocess hook, acronym first word');
|
||||
|
||||
$result = $this->doSearchExcerpt('Injection', $text, 'ex');
|
||||
$this->assertTrue(strpos($result, '<strong>DIC</strong>') !== FALSE, 'Search excerpt works with preprocess hook, acronym second word');
|
||||
|
||||
$result = $this->doSearchExcerpt('Container', $text, 'ex');
|
||||
$this->assertTrue(strpos($result, '<strong>DIC</strong>') !== FALSE, 'Search excerpt works with preprocess hook, acronym third word');
|
||||
|
||||
// Testing with a many-to-one replacement: the test module replaces
|
||||
// hypertext markup language with HTML.
|
||||
$text = "we always use hypertext markup language to describe things";
|
||||
$result = $this->doSearchExcerpt('html', $text, 'ex');
|
||||
$this->assertTrue(strpos($result, '<strong>hypertext markup language</strong>') !== FALSE, 'Search excerpt works with preprocess hook, acronym many to one');
|
||||
|
||||
// Test with accents and caps in a longer piece of text with the target
|
||||
// near the end.
|
||||
$text = str_repeat($lorem2, 20) . ' ' . $lorem1;
|
||||
$result = $this->doSearchExcerpt('Lìbêró', $text);
|
||||
$this->assertTrue(strpos($result, '<strong>libero</strong>') !== FALSE, 'Search excerpt works with caps and accents in longer text');
|
||||
|
||||
// Test with an acronym provided by the hook, with the target text in the
|
||||
// middle of a long string.
|
||||
$text = str_repeat($lorem2, 10) . ' DIC ' . str_repeat($lorem2, 10);
|
||||
$result = $this->doSearchExcerpt('Dependency', $text, 'ex');
|
||||
$this->assertTrue(strpos($result, '<strong>DIC</strong>') !== FALSE, 'Search excerpt works with acronym in longer text');
|
||||
|
||||
// Test a long string with a lot of whitespace in it.
|
||||
$lorem3 = str_replace(' ', str_repeat(" \n", 20), $lorem2);
|
||||
$text = str_repeat($lorem3, 20) . ' ' . $lorem1;
|
||||
$result = $this->doSearchExcerpt('Lìbêró', $text);
|
||||
$this->assertTrue(strpos($result, '<strong>libero</strong>') !== FALSE, 'Search excerpt works with caps and accents in longer text with whitespace');
|
||||
|
||||
$this->verbose('Elapsed time: ' . (microtime(TRUE) - $start_time));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls search_excerpt() and renders output.
|
||||
*
|
||||
* @param string $keys
|
||||
* A string containing a search query.
|
||||
* @param string $render_array
|
||||
* The text to extract fragments from.
|
||||
* @param string|null $langcode
|
||||
* Language code for the language of $text, if known.
|
||||
*
|
||||
* @return string
|
||||
* A string containing HTML for the excerpt.
|
||||
*/
|
||||
protected function doSearchExcerpt($keys, $render_array, $langcode = NULL) {
|
||||
$render_array = search_excerpt($keys, $render_array, $langcode);
|
||||
$text = \Drupal::service('renderer')->renderPlain($render_array);
|
||||
// The search_excerpt() function adds some extra spaces -- not
|
||||
// important for HTML formatting or this test. Remove these for comparison.
|
||||
return preg_replace('| +|', ' ', $text);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\search\Kernel;
|
||||
|
||||
use Drupal\Core\Language\LanguageInterface;
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
|
||||
/**
|
||||
* Indexes content and queries it.
|
||||
*
|
||||
* @group search
|
||||
*/
|
||||
class SearchMatchTest extends KernelTestBase {
|
||||
|
||||
// The search index can contain different types of content. Typically the type
|
||||
// is 'node'. Here we test with _test_ and _test2_ as the type.
|
||||
const SEARCH_TYPE = '_test_';
|
||||
const SEARCH_TYPE_2 = '_test2_';
|
||||
const SEARCH_TYPE_JPN = '_test3_';
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = ['search'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
$this->installSchema('search', ['search_index', 'search_dataset', 'search_total']);
|
||||
$this->installConfig(['search']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test search indexing.
|
||||
*/
|
||||
public function testMatching() {
|
||||
$this->_setup();
|
||||
$this->_testQueries();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up a small index of items to test against.
|
||||
*/
|
||||
public function _setup() {
|
||||
$this->config('search.settings')->set('index.minimum_word_size', 3)->save();
|
||||
|
||||
for ($i = 1; $i <= 7; ++$i) {
|
||||
search_index(static::SEARCH_TYPE, $i, LanguageInterface::LANGCODE_NOT_SPECIFIED, $this->getText($i));
|
||||
}
|
||||
for ($i = 1; $i <= 5; ++$i) {
|
||||
search_index(static::SEARCH_TYPE_2, $i + 7, LanguageInterface::LANGCODE_NOT_SPECIFIED, $this->getText2($i));
|
||||
}
|
||||
// No getText builder function for Japanese text; just a simple array.
|
||||
foreach ([
|
||||
13 => '以呂波耳・ほへとち。リヌルヲ。',
|
||||
14 => 'ドルーパルが大好きよ!',
|
||||
15 => 'コーヒーとケーキ',
|
||||
] as $i => $jpn) {
|
||||
search_index(static::SEARCH_TYPE_JPN, $i, LanguageInterface::LANGCODE_NOT_SPECIFIED, $jpn);
|
||||
}
|
||||
search_update_totals();
|
||||
}
|
||||
|
||||
/**
|
||||
* _test_: Helper method for generating snippets of content.
|
||||
*
|
||||
* Generated items to test against:
|
||||
* 1 ipsum
|
||||
* 2 dolore sit
|
||||
* 3 sit am ut
|
||||
* 4 am ut enim am
|
||||
* 5 ut enim am minim veniam
|
||||
* 6 enim am minim veniam es cillum
|
||||
* 7 am minim veniam es cillum dolore eu
|
||||
*/
|
||||
public function getText($n) {
|
||||
$words = explode(' ', "Ipsum dolore sit am. Ut enim am minim veniam. Es cillum dolore eu.");
|
||||
return implode(' ', array_slice($words, $n - 1, $n));
|
||||
}
|
||||
|
||||
/**
|
||||
* _test2_: Helper method for generating snippets of content.
|
||||
*
|
||||
* Generated items to test against:
|
||||
* 8 dear
|
||||
* 9 king philip
|
||||
* 10 philip came over
|
||||
* 11 came over from germany
|
||||
* 12 over from germany swimming
|
||||
*/
|
||||
public function getText2($n) {
|
||||
$words = explode(' ', "Dear King Philip came over from Germany swimming.");
|
||||
return implode(' ', array_slice($words, $n - 1, $n));
|
||||
}
|
||||
|
||||
/**
|
||||
* Run predefine queries looking for indexed terms.
|
||||
*/
|
||||
public function _testQueries() {
|
||||
// Note: OR queries that include short words in OR groups are only accepted
|
||||
// if the ORed terms are ANDed with at least one long word in the rest of
|
||||
// the query. Examples:
|
||||
// enim dolore OR ut = enim (dolore OR ut) = (enim dolor) OR (enim ut)
|
||||
// is good, and
|
||||
// dolore OR ut = (dolore) OR (ut)
|
||||
// is bad. This is a design limitation to avoid full table scans.
|
||||
$queries = [
|
||||
// Simple AND queries.
|
||||
'ipsum' => [1],
|
||||
'enim' => [4, 5, 6],
|
||||
'xxxxx' => [],
|
||||
'enim minim' => [5, 6],
|
||||
'enim xxxxx' => [],
|
||||
'dolore eu' => [7],
|
||||
'dolore xx' => [],
|
||||
'ut minim' => [5],
|
||||
'xx minim' => [],
|
||||
'enim veniam am minim ut' => [5],
|
||||
// Simple OR and AND/OR queries.
|
||||
'dolore OR ipsum' => [1, 2, 7],
|
||||
'dolore OR xxxxx' => [2, 7],
|
||||
'dolore OR ipsum OR enim' => [1, 2, 4, 5, 6, 7],
|
||||
'ipsum OR dolore sit OR cillum' => [2, 7],
|
||||
'minim dolore OR ipsum' => [7],
|
||||
'dolore OR ipsum veniam' => [7],
|
||||
'minim dolore OR ipsum OR enim' => [5, 6, 7],
|
||||
'dolore xx OR yy' => [],
|
||||
'xxxxx dolore OR ipsum' => [],
|
||||
// Sequence of OR queries.
|
||||
'minim' => [5, 6, 7],
|
||||
'minim OR xxxx' => [5, 6, 7],
|
||||
'minim OR xxxx OR minim' => [5, 6, 7],
|
||||
'minim OR xxxx minim' => [5, 6, 7],
|
||||
'minim OR xxxx minim OR yyyy' => [5, 6, 7],
|
||||
'minim OR xxxx minim OR cillum' => [6, 7, 5],
|
||||
'minim OR xxxx minim OR xxxx' => [5, 6, 7],
|
||||
// Negative queries.
|
||||
'dolore -sit' => [7],
|
||||
'dolore -eu' => [2],
|
||||
'dolore -xxxxx' => [2, 7],
|
||||
'dolore -xx' => [2, 7],
|
||||
// Phrase queries.
|
||||
'"dolore sit"' => [2],
|
||||
'"sit dolore"' => [],
|
||||
'"am minim veniam es"' => [6, 7],
|
||||
'"minim am veniam es"' => [],
|
||||
// Mixed queries.
|
||||
'"am minim veniam es" OR dolore' => [2, 6, 7],
|
||||
'"minim am veniam es" OR "dolore sit"' => [2],
|
||||
'"minim am veniam es" OR "sit dolore"' => [],
|
||||
'"am minim veniam es" -eu' => [6],
|
||||
'"am minim veniam" -"cillum dolore"' => [5, 6],
|
||||
'"am minim veniam" -"dolore cillum"' => [5, 6, 7],
|
||||
'xxxxx "minim am veniam es" OR dolore' => [],
|
||||
'xx "minim am veniam es" OR dolore' => []
|
||||
];
|
||||
foreach ($queries as $query => $results) {
|
||||
$result = db_select('search_index', 'i')
|
||||
->extend('Drupal\search\SearchQuery')
|
||||
->searchExpression($query, static::SEARCH_TYPE)
|
||||
->execute();
|
||||
|
||||
$set = $result ? $result->fetchAll() : [];
|
||||
$this->_testQueryMatching($query, $set, $results);
|
||||
$this->_testQueryScores($query, $set, $results);
|
||||
}
|
||||
|
||||
// These queries are run against the second index type, SEARCH_TYPE_2.
|
||||
$queries = [
|
||||
// Simple AND queries.
|
||||
'ipsum' => [],
|
||||
'enim' => [],
|
||||
'enim minim' => [],
|
||||
'dear' => [8],
|
||||
'germany' => [11, 12],
|
||||
];
|
||||
foreach ($queries as $query => $results) {
|
||||
$result = db_select('search_index', 'i')
|
||||
->extend('Drupal\search\SearchQuery')
|
||||
->searchExpression($query, static::SEARCH_TYPE_2)
|
||||
->execute();
|
||||
|
||||
$set = $result ? $result->fetchAll() : [];
|
||||
$this->_testQueryMatching($query, $set, $results);
|
||||
$this->_testQueryScores($query, $set, $results);
|
||||
}
|
||||
|
||||
// These queries are run against the third index type, SEARCH_TYPE_JPN.
|
||||
$queries = [
|
||||
// Simple AND queries.
|
||||
'呂波耳' => [13],
|
||||
'以呂波耳' => [13],
|
||||
'ほへと ヌルヲ' => [13],
|
||||
'とちリ' => [],
|
||||
'ドルーパル' => [14],
|
||||
'パルが大' => [14],
|
||||
'コーヒー' => [15],
|
||||
'ヒーキ' => [],
|
||||
];
|
||||
foreach ($queries as $query => $results) {
|
||||
$result = db_select('search_index', 'i')
|
||||
->extend('Drupal\search\SearchQuery')
|
||||
->searchExpression($query, static::SEARCH_TYPE_JPN)
|
||||
->execute();
|
||||
|
||||
$set = $result ? $result->fetchAll() : [];
|
||||
$this->_testQueryMatching($query, $set, $results);
|
||||
$this->_testQueryScores($query, $set, $results);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the matching abilities of the engine.
|
||||
*
|
||||
* Verify if a query produces the correct results.
|
||||
*/
|
||||
public function _testQueryMatching($query, $set, $results) {
|
||||
// Get result IDs.
|
||||
$found = [];
|
||||
foreach ($set as $item) {
|
||||
$found[] = $item->sid;
|
||||
}
|
||||
|
||||
// Compare $results and $found.
|
||||
sort($found);
|
||||
sort($results);
|
||||
$this->assertEqual($found, $results, "Query matching '$query'");
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the scoring abilities of the engine.
|
||||
*
|
||||
* Verify if a query produces normalized, monotonous scores.
|
||||
*/
|
||||
public function _testQueryScores($query, $set, $results) {
|
||||
// Get result scores.
|
||||
$scores = [];
|
||||
foreach ($set as $item) {
|
||||
$scores[] = $item->calculated_score;
|
||||
}
|
||||
|
||||
// Check order.
|
||||
$sorted = $scores;
|
||||
sort($sorted);
|
||||
$this->assertEqual($scores, array_reverse($sorted), "Query order '$query'");
|
||||
|
||||
// Check range.
|
||||
$this->assertEqual(!count($scores) || (min($scores) > 0.0 && max($scores) <= 1.0001), TRUE, "Query scoring '$query'");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Tests\search\Unit\SearchPageRepositoryTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\Tests\search\Unit;
|
||||
|
||||
use Drupal\search\Entity\SearchPage;
|
||||
use Drupal\search\SearchPageRepository;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\search\SearchPageRepository
|
||||
* @group search
|
||||
*/
|
||||
class SearchPageRepositoryTest extends UnitTestCase {
|
||||
|
||||
/**
|
||||
* The search page repository.
|
||||
*
|
||||
* @var \Drupal\search\SearchPageRepository
|
||||
*/
|
||||
protected $searchPageRepository;
|
||||
|
||||
/**
|
||||
* The entity query object.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\Query\QueryInterface|\PHPUnit_Framework_MockObject_MockObject
|
||||
*/
|
||||
protected $query;
|
||||
|
||||
/**
|
||||
* The search page storage.
|
||||
*
|
||||
* @var \Drupal\Core\Config\Entity\ConfigEntityStorageInterface|\PHPUnit_Framework_MockObject_MockObject
|
||||
*/
|
||||
protected $storage;
|
||||
|
||||
/**
|
||||
* The config factory.
|
||||
*
|
||||
* @var \Drupal\Core\Config\ConfigFactoryInterface|\PHPUnit_Framework_MockObject_MockObject
|
||||
*/
|
||||
protected $configFactory;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
$this->query = $this->getMock('Drupal\Core\Entity\Query\QueryInterface');
|
||||
|
||||
$this->storage = $this->getMock('Drupal\Core\Config\Entity\ConfigEntityStorageInterface');
|
||||
$this->storage->expects($this->any())
|
||||
->method('getQuery')
|
||||
->will($this->returnValue($this->query));
|
||||
|
||||
$entity_manager = $this->getMock('Drupal\Core\Entity\EntityManagerInterface');
|
||||
$entity_manager->expects($this->any())
|
||||
->method('getStorage')
|
||||
->will($this->returnValue($this->storage));
|
||||
|
||||
$this->configFactory = $this->getMock('Drupal\Core\Config\ConfigFactoryInterface');
|
||||
$this->searchPageRepository = new SearchPageRepository($this->configFactory, $entity_manager);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the getActiveSearchPages() method.
|
||||
*/
|
||||
public function testGetActiveSearchPages() {
|
||||
$this->query->expects($this->once())
|
||||
->method('condition')
|
||||
->with('status', TRUE)
|
||||
->will($this->returnValue($this->query));
|
||||
$this->query->expects($this->once())
|
||||
->method('execute')
|
||||
->will($this->returnValue(['test' => 'test', 'other_test' => 'other_test']));
|
||||
|
||||
$entities = [];
|
||||
$entities['test'] = $this->getMock('Drupal\search\SearchPageInterface');
|
||||
$entities['other_test'] = $this->getMock('Drupal\search\SearchPageInterface');
|
||||
$this->storage->expects($this->once())
|
||||
->method('loadMultiple')
|
||||
->with(['test' => 'test', 'other_test' => 'other_test'])
|
||||
->will($this->returnValue($entities));
|
||||
|
||||
$result = $this->searchPageRepository->getActiveSearchPages();
|
||||
$this->assertSame($entities, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the isSearchActive() method.
|
||||
*/
|
||||
public function testIsSearchActive() {
|
||||
$this->query->expects($this->once())
|
||||
->method('condition')
|
||||
->with('status', TRUE)
|
||||
->will($this->returnValue($this->query));
|
||||
$this->query->expects($this->once())
|
||||
->method('range')
|
||||
->with(0, 1)
|
||||
->will($this->returnValue($this->query));
|
||||
$this->query->expects($this->once())
|
||||
->method('execute')
|
||||
->will($this->returnValue(['test' => 'test']));
|
||||
|
||||
$this->assertSame(TRUE, $this->searchPageRepository->isSearchActive());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the getIndexableSearchPages() method.
|
||||
*/
|
||||
public function testGetIndexableSearchPages() {
|
||||
$this->query->expects($this->once())
|
||||
->method('condition')
|
||||
->with('status', TRUE)
|
||||
->will($this->returnValue($this->query));
|
||||
$this->query->expects($this->once())
|
||||
->method('execute')
|
||||
->will($this->returnValue(['test' => 'test', 'other_test' => 'other_test']));
|
||||
|
||||
$entities = [];
|
||||
$entities['test'] = $this->getMock('Drupal\search\SearchPageInterface');
|
||||
$entities['test']->expects($this->once())
|
||||
->method('isIndexable')
|
||||
->will($this->returnValue(TRUE));
|
||||
$entities['other_test'] = $this->getMock('Drupal\search\SearchPageInterface');
|
||||
$entities['other_test']->expects($this->once())
|
||||
->method('isIndexable')
|
||||
->will($this->returnValue(FALSE));
|
||||
$this->storage->expects($this->once())
|
||||
->method('loadMultiple')
|
||||
->with(['test' => 'test', 'other_test' => 'other_test'])
|
||||
->will($this->returnValue($entities));
|
||||
|
||||
$result = $this->searchPageRepository->getIndexableSearchPages();
|
||||
$this->assertCount(1, $result);
|
||||
$this->assertSame($entities['test'], reset($result));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the clearDefaultSearchPage() method.
|
||||
*/
|
||||
public function testClearDefaultSearchPage() {
|
||||
$config = $this->getMockBuilder('Drupal\Core\Config\Config')
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
$config->expects($this->once())
|
||||
->method('clear')
|
||||
->with('default_page')
|
||||
->will($this->returnValue($config));
|
||||
$this->configFactory->expects($this->once())
|
||||
->method('getEditable')
|
||||
->with('search.settings')
|
||||
->will($this->returnValue($config));
|
||||
$this->searchPageRepository->clearDefaultSearchPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the getDefaultSearchPage() method when the default is active.
|
||||
*/
|
||||
public function testGetDefaultSearchPageWithActiveDefault() {
|
||||
$this->query->expects($this->once())
|
||||
->method('condition')
|
||||
->with('status', TRUE)
|
||||
->will($this->returnValue($this->query));
|
||||
$this->query->expects($this->once())
|
||||
->method('execute')
|
||||
->will($this->returnValue(['test' => 'test', 'other_test' => 'other_test']));
|
||||
|
||||
$config = $this->getMockBuilder('Drupal\Core\Config\Config')
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
$config->expects($this->once())
|
||||
->method('get')
|
||||
->with('default_page')
|
||||
->will($this->returnValue('test'));
|
||||
$this->configFactory->expects($this->once())
|
||||
->method('get')
|
||||
->with('search.settings')
|
||||
->will($this->returnValue($config));
|
||||
|
||||
$this->assertSame('test', $this->searchPageRepository->getDefaultSearchPage());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the getDefaultSearchPage() method when the default is inactive.
|
||||
*/
|
||||
public function testGetDefaultSearchPageWithInactiveDefault() {
|
||||
$this->query->expects($this->once())
|
||||
->method('condition')
|
||||
->with('status', TRUE)
|
||||
->will($this->returnValue($this->query));
|
||||
$this->query->expects($this->once())
|
||||
->method('execute')
|
||||
->will($this->returnValue(['test' => 'test']));
|
||||
|
||||
$config = $this->getMockBuilder('Drupal\Core\Config\Config')
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
$config->expects($this->once())
|
||||
->method('get')
|
||||
->with('default_page')
|
||||
->will($this->returnValue('other_test'));
|
||||
$this->configFactory->expects($this->once())
|
||||
->method('get')
|
||||
->with('search.settings')
|
||||
->will($this->returnValue($config));
|
||||
|
||||
$this->assertSame('test', $this->searchPageRepository->getDefaultSearchPage());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the setDefaultSearchPage() method.
|
||||
*/
|
||||
public function testSetDefaultSearchPage() {
|
||||
$id = 'bananas';
|
||||
$config = $this->getMockBuilder('Drupal\Core\Config\Config')
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
$config->expects($this->once())
|
||||
->method('set')
|
||||
->with('default_page', $id)
|
||||
->will($this->returnValue($config));
|
||||
$config->expects($this->once())
|
||||
->method('save')
|
||||
->will($this->returnValue($config));
|
||||
$this->configFactory->expects($this->once())
|
||||
->method('getEditable')
|
||||
->with('search.settings')
|
||||
->will($this->returnValue($config));
|
||||
|
||||
$search_page = $this->getMock('Drupal\search\SearchPageInterface');
|
||||
$search_page->expects($this->once())
|
||||
->method('id')
|
||||
->will($this->returnValue($id));
|
||||
$search_page->expects($this->once())
|
||||
->method('enable')
|
||||
->will($this->returnValue($search_page));
|
||||
$search_page->expects($this->once())
|
||||
->method('save')
|
||||
->will($this->returnValue($search_page));
|
||||
$this->searchPageRepository->setDefaultSearchPage($search_page);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the sortSearchPages() method.
|
||||
*/
|
||||
public function testSortSearchPages() {
|
||||
$entity_type = $this->getMock('Drupal\Core\Entity\EntityTypeInterface');
|
||||
$entity_type->expects($this->any())
|
||||
->method('getClass')
|
||||
->will($this->returnValue('Drupal\Tests\search\Unit\TestSearchPage'));
|
||||
$this->storage->expects($this->once())
|
||||
->method('getEntityType')
|
||||
->will($this->returnValue($entity_type));
|
||||
|
||||
// Declare entities out of their expected order so we can be sure they were
|
||||
// sorted. We cannot mock these because of uasort(), see
|
||||
// https://bugs.php.net/bug.php?id=50688.
|
||||
$unsorted_entities['test4'] = new TestSearchPage(['weight' => 0, 'status' => FALSE, 'label' => 'Test4']);
|
||||
$unsorted_entities['test3'] = new TestSearchPage(['weight' => 10, 'status' => TRUE, 'label' => 'Test3']);
|
||||
$unsorted_entities['test2'] = new TestSearchPage(['weight' => 0, 'status' => TRUE, 'label' => 'Test2']);
|
||||
$unsorted_entities['test1'] = new TestSearchPage(['weight' => 0, 'status' => TRUE, 'label' => 'Test1']);
|
||||
$expected = $unsorted_entities;
|
||||
ksort($expected);
|
||||
|
||||
$sorted_entities = $this->searchPageRepository->sortSearchPages($unsorted_entities);
|
||||
$this->assertSame($expected, $sorted_entities);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class TestSearchPage extends SearchPage {
|
||||
public function __construct(array $values) {
|
||||
foreach ($values as $key => $value) {
|
||||
$this->$key = $value;
|
||||
}
|
||||
}
|
||||
public function label($langcode = NULL) {
|
||||
return $this->label;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\Tests\search\Unit;
|
||||
|
||||
use Drupal\search\Plugin\SearchPluginCollection;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\search\Plugin\SearchPluginCollection
|
||||
* @group search
|
||||
*/
|
||||
class SearchPluginCollectionTest extends UnitTestCase {
|
||||
|
||||
/**
|
||||
* The mocked plugin manager.
|
||||
*
|
||||
* @var \Drupal\Component\Plugin\PluginManagerInterface|\PHPUnit_Framework_MockObject_MockObject
|
||||
*/
|
||||
protected $pluginManager;
|
||||
|
||||
/**
|
||||
* The tested plugin collection.
|
||||
*
|
||||
* @var \Drupal\search\Plugin\SearchPluginCollection
|
||||
*/
|
||||
protected $searchPluginCollection;
|
||||
|
||||
/**
|
||||
* Stores all setup plugin instances.
|
||||
*
|
||||
* @var \Drupal\search\Plugin\SearchInterface[]
|
||||
*/
|
||||
protected $pluginInstances;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
$this->pluginManager = $this->getMock('Drupal\Component\Plugin\PluginManagerInterface');
|
||||
$this->searchPluginCollection = new SearchPluginCollection($this->pluginManager, 'banana', ['id' => 'banana', 'color' => 'yellow'], 'fruit_stand');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the get() method.
|
||||
*/
|
||||
public function testGet() {
|
||||
$plugin = $this->getMock('Drupal\search\Plugin\SearchInterface');
|
||||
$this->pluginManager->expects($this->once())
|
||||
->method('createInstance')
|
||||
->will($this->returnValue($plugin));
|
||||
$this->assertSame($plugin, $this->searchPluginCollection->get('banana'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the get() method with a configurable plugin.
|
||||
*/
|
||||
public function testGetWithConfigurablePlugin() {
|
||||
$plugin = $this->getMock('Drupal\search\Plugin\ConfigurableSearchPluginInterface');
|
||||
$plugin->expects($this->once())
|
||||
->method('setSearchPageId')
|
||||
->with('fruit_stand')
|
||||
->will($this->returnValue($plugin));
|
||||
|
||||
$this->pluginManager->expects($this->once())
|
||||
->method('createInstance')
|
||||
->will($this->returnValue($plugin));
|
||||
|
||||
$this->assertSame($plugin, $this->searchPluginCollection->get('banana'));
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user