Wîkîferheng
kuwiktionary
https://ku.wiktionary.org/wiki/Destp%C3%AAk
MediaWiki 1.47.0-wmf.20
case-sensitive
Medya
Taybet
Gotûbêj
Bikarhêner
Gotûbêja bikarhêner
Wîkîferheng
Gotûbêja Wîkîferhengê
Wêne
Gotûbêja wêneyî
MediaWiki
Gotûbêja MediaWiki
Şablon
Gotûbêja şablonê
Alîkarî
Gotûbêja alîkariyê
Kategorî
Gotûbêja kategoriyê
Pêvek
Gotûbêja pêvekê
Nimînok
Gotûbêja nimînokê
Portal
Gotûbêja portalê
Tewandin
Gotûbêja tewandinê
Jinûvesazî
Gotûbêja jinûvesaziyê
TimedText
TimedText talk
Modul
Gotûbêja modulê
Event
Event talk
berf
0
3346
6532001
6426871
2026-09-17T20:23:02Z
Wikihez
7344
Kurmancî/Navdêr: ±nimûneya bikaranînê bi amûra « [[Mediawiki:Gadget-wikt.add-examples|Nimûne zêde bike]] » (v2.0) (DefSideBoxes)
6532001
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-IPA}}
* {{deng|ku|LL-Q36368 (kur)-Dildadil-berf.wav|dever=Amed}}
* {{deng|ku|LL-Q36163 (kmr)-Key Mîrza-berf.wav|dever=Bidlîs}}
* {{ku-kîte}}
=== Navdêr ===
[[Wêne:AchenseeWinter01.JPG|thumb|'''berf''']]
{{navdêr|ku|z=m}}
# [[av|Ava]] [[qerisî]] ya wek [[toz]]eke [[spî]] ji [[esman]]an [[barîn|dibare]]. Dema ku gelek [[barîn|dibare]], [[erd]]ê [[spî]] dike; [[paşî]] dema ku [[hewa]] [[germ]] dibe, ew [[helîn|dihele]].
#: {{mk|ku|'''Berf''' [[barîn]], '''berf''' [[hatin]], '''berf''' [[helîn]], '''berf''' [[lê kirin]], '''berf''' [[malîn]], '''berf''' [[veçopirîn]].}}
#* {{jêder|ku|jêgirtin=Di esasê xwe de, navbera we û baranê, '''berfê''' xweş bû. Ew hewqas bê aman, hewqas xedar nebûn li hemberê we. Di şevên sar ên zivistanan de, baranê delal delal xwe berdida jêr. Weke, ew bireqise. Îcar dema '''berfê''', bi bedewiya keçikeke hêjdesalî, xwe berdida û bajar û hawîrdora bajêr dixemiland, zarokên bajêr dilşa dibûn. Barîna '''berfê''' şênayiyek bû. Kuçe û kolanên bajêr ji zarokan tijî dibûn.|''Tu'', [[w:Mehmed Uzun|Mehmed Uzun]], 1984}}
#* {{jêder|ku|jêgirtin='''Berfa''' pêşîn a demsala îsal, li Diyarbekirê bariya. Li herêma derdora Diyarbekirê, kêmcaran di demsala payîzê de '''berf''' dibare. Îsal jî wan wan salan e ku '''berfa''' zivistanê ji payîzê ve bariya.|''[https://www.kurdistan24.net/ku/magazin/0eb1b9b1-58a9-454f-8489-39ba8b26064e/ Li Diyarbekirê berfa yekemîn bariya]'', ''K24'', 2016}}
#* {{jêder-websîte|ku|jêgirtin=Li Stockholma xopan zivistan e, di pacê ra li hewşê dinêrim, '''berfeke''' hûr hûr dibare, her der spî boz bûye.|nivîskar=Zinarê Xamo|websîte=Hindik rindik|sernav=Piştî zivistanê buhar tê...|url=http://zinarexamo.blogspot.com/2016/11/pisti-zivistane-buhar-te.html|sal=2016}}
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Bi alfabeyên din ====
* {{ku-ar|بهرف}}
* {{ku-kr|бәрф}}
==== Formên alternatîv ====
{{kol3|ku
|befr
|befir
|berif
}}
==== Jê ====
{{kol3|ku|cure=Jê
|berfanbar
|berfemêr
|berfemêrik
|berfê-girtî
|berfî
|berfîk
|berfîn
|bûka berfê
|goga berfê
|berf ketin<q:lêker>
|berf girtin<q:lêker>
|berfketin<q:navdêr>
|berfgirtin<q:navdêr>
}}
==== Têkildar ====
{{kol3|ku
|bûz
|cemed
|kevî
|patove
|qerrise
|qeşa
|patove
|sehol
|şîşiltok
}}
==== Etîmolojî ====
Herwiha {{q|ku|befr}}, hevreha soranî {{g|ckb|بهفر}}, [[kurdiya başûrî]] ''[[wefr]]'', lekî ''[[verf]]/[[ver]]'', hewramî ''[[wewre]]'', zazakî ''[[vewre]]'', belûçî {{g|bal|برپ}} (berp), mazenderanî {{g|mzn|ورف}} (verf), farisî {{g|fa|برف}} (berf), tacikî [[барф]] (berf), pehlewî [[𐭥𐭯𐭫]] (wefr), peştûyî {{g|pa|واوره}} (wawre), avestayî ''vefre'' (berf) û ''cefre'' ([[herrî]]), sanskrîtî ''gabhra'' ([[herrî]])... {{ji|ine-pro|ku|-}} *''[[ṷep-]]'' ([[pelate]] kirin, [[belawela]] kirin, [[hilavêtin]]).
Ji bilî zimanên îranî, di şaxên din yên zimanên hindûewropî de [[hevreh]]ên peyva ''berf/befr'' peyda nabin. ''Berf'' bi {{q|ine-pro|*snóygʷʰos}} û ''berf barîn'' jî *''[[sneygʷʰ-]]'' bû ku niha jî [[dûnde]]hên wan di piraniya şaxên hindûewropî de peyda dibin: bo nimûne îngilîzî ''[[snow]]'', rusî [[снег]] ''(snêg)'' û fransî ''[[neige]]''. Heman peyv di zimanên kevn û yên navîn yên îranî de jî hebû: pehlewî [[𐭮𐭭𐭩𐭤𐭫]] (snêxr) û [[𐭮𐭭𐭩𐭰𐭪]] (snēzag) û avestayî [[𐬯𐬥𐬀𐬉𐬲𐬀𐬌𐬙𐬌]] (snaēžaiti: ''berf dibare'') lê ev peyv ji zimanên niha yên îranî winda bûne.
Li aliyekî din, peyva îranî ya niha ketiye piraniya zimanên Hindistan, Pakistan û Nepalê: hindî [[बर्फ़]] /barf/ (li gel [[हिमपात]] /himpāt/ ya xwemalî), ûrdûyî {{g|ur|برف}} (berf), {{g+|pa|برف}} (berf), gujaratî [[બરફ]] /baraph/ (li gel [[હિમ]] /him/ û [[હિમવર્ષા]] /himvarṣā/) yên xwemalî), bengalî [[বরফ]] (borof)... Heman peyv wek {{g|ota|برف}} (berf) ketibû tirkiya osmanî jî û li gel {{g|ota|قار}} (qar) ya xwemalî di edebiyatê de dihat bikaranîn lê ''berf'' ji tirkiya niha ketiye û tenê ''[[kar#Tirkî|kar]]'' maye.
<font style="font-size:10px">'''Çavkanî:''' [[Şablon:Chyet|Chyet]] ''berf'', [[Şablon:Horn|Horn p.47]], [[Şablon:MacKenzie|MacKenzie P.135]], [[Şablon:Lubotsky|Lubotsky]]</font>
==== Bide ber ====
{{kol3|ku
|av
|bahoz
|baran
|barove
|gijlok
|perpûn
|savarok
|şilope
|terg
|teyrok
|zîpik
|xwîs
|xusar
|zîpik
|xunav
|qirav
|zembelîk
|kurtûn
|pûk
}}
==== Cûnên berfê ====
* berfa [[çûr]]: berfa [[sîs]], berfa [[spî]], berfa [[pak]], berfa [[paqij]]
* berfa [[kevî]]: berfa li erdê [[req]] bûye
* berfa [[po]]: berfa [[toz]] ya ku xwe bi hev ve [[girtin|nagire]] [[lew]] [[gulmiçk]] nikarin jê bên çêkirin
* berfa [[şilove]]: berfa [[şil]], [[av]] û berfa têkil
==== Gotinên pêşiyan ====
* adar e berf giha guliyê dar e, nema danê êvare
* axa li kefê ye, mal maye li bin berfê ye
* berf heliya, ling wesiya
* berf jî spî ye, le kûçik di ser da pîsiya xwe dikin (dirîn)
* berf jî spî ye, le tam lê tune ye
* dilê min ji we (ne) xweş e, erd di bin berfê de reş e
* tu dibêjî ji nexweşan ra çûye berfê
* wek berfa spî
* xema berfê ye ku debara kasiba tune
* berf li çiyê barî
* qirfê li te bi berf bin
* berf jî erê gewr e, lê kûçik pê de gû dikin
==== Bi zaravayên din ====
* {{Z|hac}}: {{W-|hac|werwe|sc=Latn}}, {{W-|hac|wewre|sc=Latn}}
* {{Z|ckb}}: {{W-|ckb|بەفر}}, {{W-|ckb|وەفر}}, {{W-|ckb|bewir|sc=Latn}}
* {{Z|zza}}: {{W-|zza|vewre}}, {{W-|zza|vewire}}
* {{Z|sdh}}: {{W-|sdh|wefir|sc=Latn}}, {{W-|sdh|wefr|sc=Latn}}
* {{Z|lki}}: {{W-|lki|ver|sc=Latn}}, {{W-|lki|vefr|sc=Latn}}, {{W-|lki|herrî|sc=Latn}}
==== Werger ====
{{werger-ser}}
* {{Z|abq}}: {{W-|abq|сы}}
* {{Z|ab}}: {{W-|ab|асы}}, {{W-|ab|асы}}
* {{Z|af}}: {{W+|af|sneeu}}
* {{Z|sq}}: {{W+|sq|borë|m}}
* {{Z|ale}}: {{W-|ale|qaniigix}}
* {{Z|de}}: {{W+|de|Schnee|n}}
* {{Z|zgh}}: {{W-|zgh|ⴰⴷⴼⵍ|n}}
* {{Z|am}}: {{W-|am|የበረዶ ብናኝ|sc=Ethi}}
* {{Z|ani}}: {{W-|ani|анзи}}
* {{Z|an}}: {{W+|an|nieu}}
* {{Z|arc}}:
*: {{Z|aii}}: {{W-|aii|ܬܲܠܓܵܐ|tr=tālga|n}}
*: Îbranî: {{W-|arc|תלגא|tr=talgā’|n|sc=Hebr}}
*: Siryanî: {{W-|arc|ܬܠܓܐ|tr=talgā’|n}}
* {{Z|rup}}: {{W+|rup|neao}}, {{W-|rup|neauã|m}}
* {{Z|as}}: {{W-|as|বৰফ|sc=Beng}}
* {{Z|ast}}: {{W+|ast|ñeve}}
* {{Z|av}}: {{W-|av|гӏазу|sc=Cyrl}}
* {{Z|ay}}: {{W+|ay|khunu}}
* {{Z|ain}}: {{W-|ain|ウパㇱ|tr=upas}}
* {{Z|az}}: {{W+|az|qar}}
* {{Z|eu}}: {{W+|eu|elur}}
* {{Z|ba}}: {{W-|ba|ҡар|sc=Cyrl}}
* {{Z|bbl}}: {{W-|bbl|ლავ}}
* {{Z|be}}: {{W+|be|снег|n}}
* {{Z|bal}}: {{W-|bal|برپ|tr=barp}}, {{W-|bal|برف|tr=barf}}
* {{Z|bn}}: {{W+|bn|বরফ|sc=Beng}}
* {{Z|bqi}}: {{W-|bqi|برف|tr=barf|sc=fa-Arab}}
* {{Z|bcl}}: {{W-|bcl|nyebe}}
* {{Z|sit-bok}}: {{W-|sit-bok|tapam}}
* {{Z|br}}: {{W+|br|erc'h|n}}
* {{Z|kxd}}: {{W-|kxd|salji}}
* {{Z|bdk}}: {{W-|bdk|йиз}}
* {{Z|bg}}: {{W+|bg|сняг|n}}
* {{Z|my}}: {{W+|my|နှင်း|sc=Mymr}}
* {{Z|bua}}: {{W-|bua|саһан}}
* {{Z|ce}}: {{W-|ce|ло}}
* {{Z|cs}}: {{W+|cs|sníh|n}}
* {{Z|chr}}: {{W-|chr|ᎤᎾᏥ}}
* {{Z|chy}}: {{W-|chy|hésta'se}}
* {{Z|cic}}: {{W-|cic|okti'}}
* {{Z|zh}}:
*: {{Z|yue}}: {{W-|yue|雪|tr=syut3}}
*: {{Z|dng}}: {{W-|dng|щүә}}
*: {{Z|hak}}: {{W-|hak|雪|tr=siet}}
*: {{Z|cmn}}: {{W+|cmn|雪|tr=xuě}}
*: {{Z|cdo}}: {{W-|cdo|雪|tr=siók}}
*: {{Z|nan}}: {{W-|nan|雪|tr=seh, serh, soat}}
*: {{Z|wuu}}: {{W-|wuu|雪|tr=xiq}}
* {{Z|ckt}}: {{W-|ckt|ыʼԓьыԓ|tr=yʼḷʹyḷ}}
* {{Z|cv}}: {{W-|cv|юр}}
* {{Z|dlm}}: {{W-|dlm|nai|m}}
* {{Z|da}}: {{W+|da|sne|g}}
* {{Z|dv}}: {{W+|dv|ސްނޯ|sc=Thaa}}
* {{Z|dgr}}: {{W-|dgr|zhah}}
* {{Z|dlg}}: {{W-|dlg|каар}}
* {{Z|ovd}}: {{W-|ovd|sniųo|n}}
* {{Z|id}}: {{W+|id|salju}}
* {{Z|ar}}: {{W+|ar|ثَلْج|n}}
*: {{Z|arz}}: {{W-|arz|تلج|n|tr=talg}}
*: {{Z|arz}}: {{W-|arz|ثلج|n|tr=talj}}
*: {{Z|ary}}: {{W-|ary|تلج|n|tr=talj}}
* {{Z|hy}}: {{W+|hy|ձյուն}}
*: {{Z|xcl}}: {{W-|xcl|ձիւն|sc=Armn}}
* {{Z|eo}}: {{W+|eo|neĝo}}
* {{Z|et}}: {{W+|et|lumi}}
* {{Z|eve}}: {{W-|eve|емсамла̄н}}
* {{Z|evn}}: {{W-|evn|иманна|sc=Cyrl}}
* {{Z|ext}}: {{W-|ext|ñevi}}
* {{Z|pal}}: {{W-|pal|𐭥𐭯𐭫|ts=wafr}}, {{W-|pal|𐭮𐭭𐭩𐭤𐭫|ts=snēxr}}, {{W-|pal|𐭮𐭭𐭩𐭰𐭪|ts=snēzag}}
* {{Z|fa}}: {{W+|fa|برف|tr=barf}}
*: {{Z|pal}}: {{W-|pal|snēxr}}, {{W-|pal|snēzag}}, {{W-|pal|wafr}}
* {{Z|fo}}: {{W+|fo|kavi|n}}, {{W-|fo|snjógvur|n}}
* {{Z|fi}}: {{W+|fi|lumi}}
* {{Z|fr}}: {{W+|fr|neige|m}}
* {{Z|fur}}: {{W-|fur|nêf|m}}, {{W-|fur|nêv|m}}
* {{Z|frr}}: {{W-|frr|snä|nt}}
* {{Z|fy}}: {{W+|fy|snie}}
* {{Z|gd}}: {{W-|gd|sneachda|n}}, {{W-|gd|sneachd|n}}
* {{Z|gl}}: {{W+|gl|neve|m}}
* {{Z|gdo}}: {{W-|gdo|анзи}}
* {{Z|got}}: {{W-|got|𐍃𐌽𐌰𐌹𐍅𐍃|n}}
* {{Z|gu}}: {{W-|gu|બરફ|sc=Gujr}}, {{W+|gu|હિમ}}, {{W-|gu|હિમવર્ષા}}
* {{Z|ka}}: {{W-|ka|თოვლი|sc=Geor}}
* {{Z|gn}}: {{W-|gn|yrypy'a}}
* {{Z|ht}}: {{W-|ht|nèj}}
* {{Z|haw}}: {{W-|haw|hau}}
* {{Z|ha}}: {{W-|ha|ƙànƙáráa}}
* {{Z|hi}}: {{W+|hi|बर्फ़|m|sc=Deva}}, {{W+|hi|हिमपात}}
* {{Z|hil}}: {{W-|hil|niyebe}}
* {{Z|nl}}: {{W+|nl|sneeuw|n}}
* {{Z|hop}}: {{W-|hop|nuva}}
* {{Z|hrx}}: {{W-|hrx|Schnee|n}}
* {{Z|huz}}: {{W-|huz|әᵸз}}
* {{Z|he}}: {{W+|he|שֶׁלֶג|n|tr=shéleg}}
* {{Z|io}}: {{W+|io|nivo}}
* {{Z|ig}}: {{W-|ig|sinoo}}
* {{Z|ang}}: {{W-|ang|snāw|n}}
* {{Z|enm}}: {{W-|enm|snow}}
* {{Z|en}}: {{W+|en|snow}}
* {{Z|izh}}: {{W-|izh|lumi}}
* {{Z|inh}}: {{W-|inh|лоа}}
* {{Z|ia}}: {{W-|ia|nive}}
* {{Z|iu}}: {{W-|iu|ᐊᐳᑦ}} (snow in general), {{W-|iu|ᒪᐅᔭ}} (deep soft snow)
* {{Z|sga}}: {{W-|sga|snechtae|n}}
* {{Z|ga}}: {{W+|ga|sneachta|n}}
* {{Z|ruo}}: {{W-|ruo|ne|m}}
* {{Z|it}}: {{W+|it|neve|m}}
* {{Z|is}}: {{W+|is|snjór|n}}, {{W+|is|snær|n}}, {{W+|is|fönn|m}}
* {{Z|ojp}}: {{W-|ojp|雪|tr=yuki1}}
* {{Z|ja}}: {{W+|ja|雪|tr=yuki}}
* {{Z|jv}}: {{W+|jv|ꦱꦭ꧀ꦗꦸ}}
* {{Z|juc}}: {{W-|juc|imanggi}}
* {{Z|kab}}: {{W-|kab|adfel|n}}
* {{Z|tbd}}: {{W-|tbd|sno}}
* {{Z|kl}}: {{W-|kl|aput}}
* {{Z|xal}}: {{W-|xal|цасн}}
* {{Z|kmc}}: {{W-|kmc|nuil}}
* {{Z|kn}}: {{W+|kn|ಅಯ್ಕಿಲ್}}, {{W+|kn|ಹಿಮ}}
* {{Z|pam}}: {{W-|pam|niebi}}, {{W-|pam|yelu}}
* {{Z|kea}}: {{W-|kea|nevi}}
* {{Z|krc}}: {{W-|krc|къар|tr=qar|sc=Cyrl}}
* {{Z|krl}}: {{W-|krl|lumi}}
* {{Z|csb}}: {{W-|csb|snieg}}
* {{Z|ca}}: {{W+|ca|neu|m}}
* {{Z|qu}}: {{W-|qu|rit'i}}, {{W-|qu|riti}}, {{W-|qu|rahu}}
* {{Z|ks}}: {{W+|ks|شیٖن}}
* {{Z|khv}}: {{W-|khv|иᵸса}}
* {{Z|ky}}: {{W+|ky|кар}}
* {{Z|si}}: {{W+|si|හිම|sc=Sinh}}
* {{Z|koi}}: {{W-|koi|лым}}
* {{Z|ko}}: {{W+|ko|눈}}
* {{Z|kw}}: {{W-|kw|ergh|n}}
* {{Z|co}}: {{W+|co|neve}}
* {{Z|lbj}}: {{W-|lbj|ཁ}}
* {{Z|lld}}: {{W-|lld|nëif|m}}
* {{Z|lad}}: {{W-|lad|inyeve|m}}, {{W-|lad|nieve|m}}
* {{Z|lkt}}: {{W-|lkt|wá}}
* {{Z|ltg}}: {{W-|ltg|snīgs|n}}
* {{Z|la}}: {{W+|la|nix|m}}
* {{Z|lv}}: {{W+|lv|sniegs|n}}
* {{Z|lo}}: {{W+|lo|ຫິມະ}}
* {{Z|lzz}}: {{W-|lzz|mturi}}
* {{Z|lez}}: {{W-|lez|жиз}}
* {{Z|lt}}: {{W+|lt|sniegas|n}}
* {{Z|liv}}: {{W-|liv|lum}}
* {{Z|lmo}}: {{W-|lmo|nev|n}}
* {{Z|khb}}: {{W-|khb|ᦵᦖᧀᦉᦏᦱᧇ}}
* {{Z|lb}}: {{W+|lb|Schnéi|n}}
* {{Z|mhn}}: {{W-|mhn|schnea|n}}
* {{Z|mk}}: {{W-|mk|снег|n}}
* {{Z|mg}}: {{W+|mg|oram-panala}}
* {{Z|ml}}: {{W+|ml|മഞ്ഞ്|sc=Mlym}}
* {{Z|ms}}: {{W+|ms|salji}}
* {{Z|mt}}: {{W-|mt|borra|n}}, {{W-|mt|silġ|m}}
* {{Z|mnc}}: {{W-|mnc|ᠨᡳᠮᠠᠩᡤᡳ}}
* {{Z|gv}}: {{W-|gv|sniaghtey|n}}
* {{Z|mns-nor}}: {{W-|mns-nor|тӯйт}}
* {{Z|mi}}: {{W+|mi|huka}}, {{W-|mi|hukarere}}, {{W-|mi|puaheiri}}
* {{Z|mrw}}: {{W-|mrw|ilo}}
* {{Z|mr}}: {{W-|mr|बर्फ|sc=Deva}}
* {{Z|mhr}}: {{W-|mhr|лум|sc=Cyrl}}
* {{Z|mzn}}: {{W-|mzn|ورف|tr=varf}}
* {{Z|hu}}: {{W+|hu|hó}}
* {{Z|xmf}}: {{W-|xmf|თირი}}
* {{Z|egy}}: {{W-egy|sꜣrꜣqw|n|h=Aa18-r:Z1-q-w-N4}}
* {{Z|mwl}}: {{W-|mwl|niebe|m}}
* {{Z|mn}}:
*: Kirîlî: {{W+|mn|цас|sc=Cyrl}}
*: Mongolî: {{W-|mn|ᠴᠠᠰᠤ}}
* {{Z|mos}}: {{W-|mos|sa-ko-gĩsdim}}
* {{Z|nci}}: {{W-|nci|cepayahuitl}}
* {{Z|nap}}: {{W-|nap|neve}}
* {{Z|nv}}: {{W-|nv|zas}}
* {{Z|nds}}: {{W-|nds|Snee|n}}
* {{Z|ne}}: {{W-|ne|हिउँ|sc=Deva}}
* {{Z|nrf}}: {{W-|nrf|né|m}}
* {{Z|no}}:
*: {{Z|no}}: {{W+|no|snø|n}}, {{W+|no|sne|n}}
*: {{Z|nn}}: {{W-|nn|snø|n}}
* {{Z|oj}}: {{W-|oj|goon}}
* {{Z|ryu}}: {{W-|ryu|雪|tr=yuchi}}
* {{Z|oc}}: {{W+|oc|nèu|m}}
* {{Z|one}}: {{W-|one|ónyʌte̲’}}
* {{Z|or}}: {{W+|or|ତୁଷାର}}
* {{Z|oac}}: {{W-|oac|имаса}}
* {{Z|os}}: {{W-|os|мит}}
* {{Z|ota}}: {{W-|ota|قار|tr=kar|sc=ota-Arab}}, {{W-|ota|برف|tr=berf|sc=ota-Arab}}, {{W-|ota|ثلج|tr=selc}}
* {{Z|ug}}: {{W-|ug|قار|sc=ug-Arab}}
* {{Z|uz}}: {{W+|uz|qor}}
* {{Z|pi}}: {{W-|pi|hima}}
* {{Z|ps}}: {{W-|ps|واوره|m|tr=wāwra|sc=ps-Arab}}
* {{Z|pms}}: {{W-|pms|fiòca|m}}
* {{Z|pdt}}: {{W-|pdt|Schnee|n}}
* {{Z|pox}}: {{W-|pox|sneg|n}}
* {{Z|pl}}: {{W+|pl|śnieg|n}}
* {{Z|pt}}: {{W+|pt|neve|m}}
* Puncabî:
*: Şaxmuxî: {{W+|pa|برف|tr=brf}}
*: Gurmuxî: {{W-|pa|ਬਰਫ਼}}
* {{Z|kk}}: {{W+|kk|қар}}
* {{Z|cop}}:
*: Bohaîrî: {{W-|cop|ⲭⲓⲱⲛ|n}}
*: Sahîdî: {{W-|cop|ⲭⲓⲱⲛ|n}}
* {{Z|rhg}}: {{W-|rhg|boróf}}
* {{Z|rgn}}: {{W-|rgn|nēva|m}}
* {{Z|rm}}: {{W-|rm|naiv|n}}, {{W-|rm|neiv|n}}, {{W-|rm|nev|n}}
* {{Z|rom}}: {{W-|rom|iv|n}}, {{W-|rom|jiv|n}}
* {{Z|ro}}: {{W+|ro|zăpadă|m}}, {{W+|ro|nea|m}}, {{W+|ro|omăt|nt}}
* {{Z|ru}}: {{W+|ru|снег|n}}
* {{Z|rue}}: {{W-|rue|снїг|n}}
* {{Z|ksw}}: {{W-|ksw|မူခိၣ်ဖီ}}
* {{Z|str}}: {{W-|str|s'aahling̱aaw}} (wet snow)
* {{Z|str}}: {{W-|str|yíq}} (falling), {{W-|str|máqa}} (on ground)
*: {{Z|str}}: {{W-|str|meqe'}} (on ground)
* {{Z|str}}: {{W-|str|k̕wista̱'yala}} (falling), {{W-|str|na'yi}} (on ground)
* {{Z|str}}: {{W-|str|ĆIḴ}} (falling), {{W-|str|ṈAḴE}} (on ground), {{W-|str|ŚEŚIḴOME¸}} (fine dry snow)
* {{Z|osx}}: {{W-|osx|snēo|n}}
* {{Z|se}}: {{W-|se|muohta}}
* {{Z|sms}}: {{W-|sms|muõtt}}
* {{Z|sm}}: {{W-|sm|kiona}}
* {{Z|sgs}}: {{W-|sgs|snėigs|n}}
* {{Z|sa}}: {{W+|sa|हिम}}
* {{Z|sat}}: {{W-|sat|ᱦᱤᱢ}}
* {{Z|sc}}: {{W-|sc|ni}}, {{W-|sc|nia}}, {{W-|sc|nibe}}, {{W-|sc|nive}}
* {{Z|ceb}}: {{W-|ceb|nyebe}}
* {{Z|scn}}: {{W+|scn|nivi|m}}
* {{Z|szl}}: {{W-|szl|śniyg|n}}
* {{Z|sd}}: {{W-|sd|برف|tr=barfa|sc=sd-Arab}}
* {{Z|srn}}: {{W-|srn|karki}}
* {{Z|sh}}:
*: Kirîlî: {{W-|sh|снег|n|sc=Cyrl}}, {{W-|sh|сније̑г|n|sc=Cyrl}}, {{W-|sh|сниг|n|sc=Cyrl}}
*: Latînî: {{W+|sh|sneg|n}}, {{W+|sh|snijȇg|n}}, {{W-|sh|snig|n}}
* {{Z|sco}}: {{W-|sco|snaw}}
* {{Z|cu}}: {{W-|cu|снѣгъ|n|sc=Cyrs}}
* {{Z|sk}}: {{W+|sk|sneh|n}}
* {{Z|sl}}: {{W+|sl|snég|n}}
* {{Z|so}}: {{W+|so|baraf}}
* Sorbî:
*: {{Z|dsb}}: {{W-|dsb|sněg|n}}
*: {{Z|hsb}}: {{W+|hsb|sněh|n}}
* {{Z|st}}: {{W-|st|lehlwa}}, {{W-|st|lehloa}}
* {{Z|es}}: {{W+|es|nieve|m}}
* {{Z|su}}: {{W-|su|ᮞᮜ᮪ᮏᮥ}}
* {{Z|xsv}}: {{W-|xsv|snaigas}}
* {{Z|sva}}: {{W-|sva|მუს}}
* {{Z|sw}}: {{W+|sw|theluji|c9|c10}}
* {{Z|sv}}: {{W+|sv|snö|g}}
* {{Z|shn}}: {{W-|shn|မူၺ်|sc=Mymr}}
* {{Z|xsr}}: {{W-|xsr|གའ|tr=khaa}}
* {{Z|sn}}: {{W-|sn|chando}}
* {{Z|cjs}}: {{W-|cjs|қар|sc=Cyrl}}
* {{Z|tg}}: {{W+|tg|барф}}
* {{Z|tl}}: {{W+|tl|niyebe}}
* {{Z|ta}}: {{W+|ta|பனி|sc=Taml}}
* {{Z|rif}}: {{W-|rif|adfer|n}}
* {{Z|th}}: {{W+|th|หิมะ}}
* {{Z|te}}: {{W+|te|మంచు|sc=Telu}}, {{W+|te|హిమము|sc=Telu}}
* {{Z|tzm}}: {{W-|tzm|ⴰⴷⴼⵍ|n|sc=Tfng}}
* {{Z|crh}}: {{W-|crh|qar}}
* {{Z|tt}}: {{W+|tt|кар}}
* {{Z|ti}}: {{W-|ti|ውርጪ|sc=Ethi}}
* {{Z|tr}}: {{W+|tr|kar}}, {{W+|tr|pük}}
* {{Z|tk}}: {{W-|tk|gar}}
* {{Z|bo}}: {{W-|bo|གངས|sc=Tibt}}, {{W-|bo|ཁ་བ}}
* {{Z|tli}}: {{W-|tli|dléit}}
* {{Z|tpi}}: {{W-|tpi|sno}}
* {{Z|to}}: {{W-|to|sinou}}
* {{Z|tyv}}: {{W-|tyv|хар}}
* {{Z|ude}}: {{W-|ude|имаха}}
* {{Z|udm}}: {{W-|udm|лымы}}
* {{Z|uga}}: {{W-|uga|𐎂𐎍𐎘}}
* {{Z|ulc}}: {{W-|ulc|sịmata}}
* {{Z|ur}}: {{W-|ur|برف|m|tr=barf|sc=ur-Arab}}
* {{Z|uk}}: {{W+|uk|сніг|n}}
* {{Z|vec}}: {{W-|vec|nève}}, {{W+|vec|neu}}, {{W-|vec|nef}}
* {{Z|vi}}: {{W+|vi|tuyết}} ({{W-|vi|雪}})
* {{Z|vo}}: {{W+|vo|nif}}
* {{Z|vro}}: {{W-|vro|lumi}}
* {{Z|vot}}: {{W-|vot|lumi}}
* {{Z|wa}}: {{W+|wa|nive|m}}, {{W+|wa|nedje|m}}, {{W+|wa|ivier|m}}
* {{Z|cy}}: {{W+|cy|eira|n}}, {{W-|cy|ôd|n}}
* {{Z|wym}}: {{W-|wym|śnej|n}}
* {{Z|km}}: {{W+|km|ព្រិល}}, {{W+|km|ហិមៈ}}
* {{Z|xh}}: {{W-|xh|ikhephu|c5|c6}}
* {{Z|yai}}: {{W-|yai|варф}}
* {{Z|sah}}: {{W-|sah|хаар|sc=Cyrl}}
* {{Z|yag}}: {{W-|yag|panaxa}}
* {{Z|yi}}: {{W-|yi|שניי|n|sc=Hebr}}
* {{Z|ii}}: {{W-|ii|ꃰ}}
* {{Z|yo}}: {{W-|yo|ìrì dídì}}
* {{Z|esu}}: {{W-|esu|qanik}}
* {{Z|el}}: {{W+|el|χιόνι|nt}}
*: Kevn: {{W-|grc|χιών|m}}
* {{Z|zea}}: {{W-|zea|sneêuw|n}}, {{W-|zea|sneê}}
* {{Z|za}}: {{W-|za|nae}}, {{W-|za|siet}}
* {{Z|ase}}: {{W-|ase|5@SideForeheadhigh-PalmDown-5@SideForeheadhigh-PalmDown SlowWiggle-SlowWiggle 5@SideTrunkhigh-Palmdown-5@SideTrunkhigh-PalmDown|sc=Sgnw}}
* {{Z|zu}}: {{W-|zu|iqhwa|c5|c6}}
{{werger-bin}}
==== Anagram ====
* {{anagram|ku|a=befr|befr}}
== {{ziman|cy}} ==
=== Navdêr ===
{{navdêr|cy|z=m}}
# {{mijar|cy|zimannasî}} {{g|ku|lêker}}, {{g|ku|fiil}}
=== Çavkanî ===
{{rûpela wergerê|cy|lêker}}
rkgtia4x8g3ahyzsktwotv7d70qwj7u
kirîn
0
3817
6531994
6531861
2026-09-17T19:05:56Z
Wikihez
7344
Kurmancî/Navdêr: ±nimûneya bikaranînê bi amûra « [[Mediawiki:Gadget-wikt.add-examples|Nimûne zêde bike]] » (v2.0), mane hate guhartin (DefSideBoxes)
6531994
wikitext
text/x-wiki
{{h-bnr|kirin}}
== {{ziman|ku}} ==
=== Bilêvkirin ===
* (''Lêkera rader û navdêr''): {{ku-IPA|k(h)iřîn}}, کڕین
:* {{deng|ku|LL-Q36163 (kmr)-Keyomerd-kirîn.wav|dever=Qoser}}
:* {{deng|ku|LL-Q36163 (kmr)-Key Mîrza-kirîn.wav|dever=Bidlîs}}
:* {{deng|ku|LL-Q36163 (kmr)-Ebulfîda-kirîn.wav|Deng|dever=Bidlîs}}
* (''Forma lêkerê ya dema borî''): {{ku-IPA|"kiřîn}}, کڕین
* {{ku-kîte}}
=== Lêker ===
{{ku-tewîn-lk|kirîn|form=gerguhêz|niha=kir|borî=kirî}}
[[Wêne:Euro_banknotes_Europa_series.png|thumb|Mirov bi [[pare]]/[[pere]] tiştan [[kirîn|dikire]].]]
{{lêker|ku|c=x-gh}}, {{k-ya req}}, {{r-ya req}}
# bi [[pare]][[dan]]ê [[wergirtin]], bi [[dirav]]danê [[bi dest xistin]], bi [[pere]] [[standin]]
#: {{mk|ku|Te [[tirimpêl]]a xwe bi çendê '''kiriye'''?}}
#: {{mk|ku|Min xaniyek bi 150 000 euroyan '''kirî'''.}}
#* {{J:Melayê Cizîrî:Dîwana Melayê Cizîrî|jêgirtin=Fe-bihe l-'enberu fa'ih we-bihe l-misku 'ebîq«</br>Çi bi kuştin çi bi hiştin tu ko ferman '''bikirî'''</br>Ez bi hukmê te xweş im »Ehmedu 'ebdun we-reqîq}}
==== Bi alfabeyên din ====
* {{ku-ar|کڕین}}
==== Formên alternatîv ====
{{kol3|ku
|kirrîn
|kerîn
}}
==== Hevmane ====
{{kol3|ku
|standin
|stendin
}}
==== Dijmane ====
{{kol3|ku
|firotin
}}
==== Jê ====
{{kol3|ku
|bikir
|bikirî
|kiryar
|kiryarî
}}
==== Têkildar ====
{{kol3|ku
|kirê
}}
==== Etîmolojî ====
Ji {{ji|ine-pro|ku|-}}: *'''k<sup>w</sup>rei'''- ("kirîn"),
{{ji|ira-pro||-}}: *'''xrey'''- ("kirîn"),
{{pal}}: '''xiryn'''- ("kirîn"),
{{sog}}: '''xiryn'''- ("kirîn"),
{{ho}}: '''xn'''- ("kirîn"),
{{fa}}: '''xerîden'''/'''xer'''- ("kirîn"),
[[gîlekî]]: '''heên'''/'''hin'''- ("kirîn") ... {{ir}},
{{kmr}}: '''kirîn'''/'''kir'''- ("kirîn") ,
{{zza}}: '''[[herînayen]]'''/'''her'''- ("kirîn"),
{{sa}}: '''kirey''' ("kirîn"),
[[rusî|rusiya kevn]]: '''kirenuti''' ("kirîn")
;{{biçûk|Çavkanî:
* {{Cheung|r=446}}
}}
==== Bide ber ====
{{kol3|ku
|kirin
}}
==== Herwiha binêre ====
{{kol3|ku
|birîn
|çirîn
|dirîn
|hirîn
|mirîn
|nirîn
|qirîn
|sirîn
|şîrîn
|tirîn
|wirîn
|zirîn
}}
==== Werger ====
{{werger-ser}}
* {{Z|ab}}: {{W-|ab|аахәара}}
* {{Z|af}}: {{W+|af|koop}}
* {{Z|xag}}: {{W-|xag|𐔸𐕒𐔲𐕒𐕡𐕆𐔴𐕍𐔴𐕚𐕒𐕡𐕎}}
* {{Z|ak}}: {{W-|ak|tɔ}}
* {{Z|akl}}: {{W-|akl|bakae}}
* {{Z|sq}}: {{W+|sq|blej}}
* {{Z|de}}: {{W+|de|kaufen}}
* {{Z|am}}: {{W-|am|መግዛት|sc=Ethi}}
* Apaçî:
*: {{Z|apw}}: {{W-|apw|nahiʼdiih}}
* {{Z|an}}: {{W-|an|crompar}}
* {{Z|aii}}: {{W-|aii|ܙܵܒ݂ܹܢ|tr=zawen}}
* {{Z|rup}}: {{W-|rup|acumpãr}}
* {{Z|as}}: {{W-|as|কিনা}}
* {{Z|ast}}: {{W-|ast|mercar}}, {{W-|ast|comprar}}
* {{Z|av}}: {{W-|av|бичун босизе|sc=Cyrl}}
* {{Z|ain}}: {{W-|ain|ホㇰ|tr=hok}}
* {{Z|az}}: {{W+|az|almaq}}
* {{Z|bdr}}: {{W-|bdr|beli}}
* {{Z|eu}}: {{W+|eu|erosi}}
* {{Z|ba}}: {{W-|ba|һатып алыу}}, {{W-|ba|алыу}}
* {{Z|beg}}: {{W-|beg|melei}}
* {{Z|be}}: {{W-|be|купля́ць|impf}}, {{W-|be|купі́ць|pf|sc=Cyrl}}
* {{Z|bn}}: {{W+|bn|কেনা|sc=Beng}}
* {{Z|bsb}}: {{W-|bsb|mali}}
* {{Z|br}}: {{W+|br|prenañ}}
* {{Z|kxd}}: {{W-|kxd|bali}}
* {{Z|bg}}: {{W-|bg|купу́вам|impf}}
* {{Z|my}}: {{W+|my|ဝယ်|sc=Mymr}}
* {{Z|cs}}: {{W+|cs|kupovat|impf}}, {{W+|cs|koupit|pf}}
* {{Z|ny}}: {{W-|ny|gula}}
* {{Z|zh}}:
*: {{Z|yue}}: {{W-|yue|買|tr=maai5}}
*: {{Z|cmn}}: {{W+|cmn|買|tr=mǎi}}
* {{Z|chk}}: {{W-|chk|kamö}}
* {{Z|da}}: {{W+|da|købe}}
* {{Z|dtp}}: {{W-|dtp|boli}}
* {{Z|ovd}}: {{W-|ovd|tjyöpa}}
* {{Z|id}}: {{W+|id|beli|cuda=membeli}}
* {{Z|ar}}: {{W+|ar|اِشْتَرَى}}
*: {{Z|arz}}: {{W-|arz|اشترى|tr=ištarā|sc=Arab}}
*: {{Z|ary}}: {{W-|ary|شرى|tr=šra}}
* {{Z|hy}}: {{W+|hy|գնել}}, {{W+|hy|առնել}}
* {{Z|eo}}: {{W+|eo|aĉeti}}
* {{Z|et}}: {{W-|et|ostma}}
* {{Z|evn}}: {{W-|evn|гами}}
* {{Z|gur}}: {{W-|gur|da}}
* {{Z|fa}}: {{W+|fa|خریدن|tr=xaridan}}
* {{Z|fo}}: {{W-|fo|keypa}}
* {{Z|fi}}: {{W+|fi|ostaa}}
* {{Z|frp}}: {{W-|frp|achetar}}
* {{Z|fr}}: {{W+|fr|acheter}}
* {{Z|fur}}: {{W-|fur|comprâ}}
* {{Z|frr}}: {{W-|frr|kuupe}}
* {{Z|fy}}: {{W-|fy|keape}}, {{W-|fy|keapje}}
* {{Z|gd}}: {{W-|gd|ceannaich}}
* {{Z|gl}}: {{W+|gl|mercar}}, {{W+|gl|comprar}}
* {{Z|glk}}: {{W-|glk|هین|tr=he:n}}
* {{Z|got}}: {{W-|got|𐌱𐌿𐌲𐌾𐌰𐌽}}
* {{Z|gu}}: {{W-|gu|ખરીદવું}}
* {{Z|ka}}: {{W-|ka|ყიდვა|sc=Geor}}
* {{Z|gn}}: {{W-|gn|jogua}}
* {{Z|ht}}: {{W-|ht|achte}}
* {{Z|haw}}: {{W-|haw|kūʻai}}
* {{Z|ha}}: {{W-|ha|sáyṓ}}
* {{Z|mba}}: {{W-|mba|palit}}
* {{Z|hi}}: {{W-|hi|मोल लेना}}, {{W-|hi|ख़रीदना|sc=Deva}}
* {{Z|mww}}: {{W-|mww|yuav}}
* {{Z|nl}}: {{W+|nl|kopen}}, {{W+|nl|aanschaffen}}, {{W+|nl|aankopen}}
* {{Z|ibg}}: {{W-|ibg|gatang}}
* {{Z|he}}: {{W-|he|קָנָה|tr=kaná}}
* {{Z|io}}: {{W+|io|komprar}}
* {{Z|ilo}}: {{W-|ilo|gatang}}
* {{Z|ang}}: {{W-|ang|bycgan}}, {{W-|ang|ceapian}}
* {{Z|en}}: {{W+|en|buy}}
* {{Z|izh}}: {{W-|izh|ostaa}}, {{W-|izh|lunastaa}}, {{W-|izh|lunata}}
* {{Z|inh}}: {{W-|inh|эца}}
* {{Z|ia}}: {{W-|ia|comprar}}, {{W-|ia|emer}}
* {{Z|ga}}: {{W-|ga|ceannaigh}}
* {{Z|isd}}: {{W-|isd|xatang}}
* {{Z|ist}}: {{W-|ist|cunprà}}
* {{Z|it}}: {{W+|it|comprare}}
* {{Z|ium}}: {{W-|ium|maaiz}}
* {{Z|ivv}}: {{W-|ivv|manadiw}}
* {{Z|is}}: {{W+|is|kaupa}}
* {{Z|ja}}: {{W+|ja|買う|tr=かう, kau}}, {{W+|ja|購入|tr=こうにゅうする, kōnyū suru|cuda=購入する}}
* {{Z|jv}}: {{W+|jv|tuku}}, {{W-|jv|tumbas}}, {{W+|jv|mundhut}}
* {{Z|kab}}: {{W-|kab|ⴰⵖ|tr=aɣ}}
* {{Z|kl}}: {{W-|kl|sivoq}}
* {{Z|xbr}}: {{W-|xbr|kei}}
* {{Z|pam}}: {{W-|pam|sali}}, {{W-|pam|sumali}}
* {{Z|ca}}: {{W+|ca|comprar}}
* {{Z|kgp}}: {{W-|kgp|kajãm}}
* {{Z|qu}}: {{W+|qu|rantiy}}, {{W-|qu|rantii}}
* {{Z|ky}}: {{W+|ky|сатып алуу|sc=Cyrl}}
* {{Z|kg}}: {{W-|kg|kusumba}}
* {{Z|si}}: {{W-|si|ගන්නවා|sc=Sinh}}
* {{Z|ko}}: {{W+|ko|사다}}, {{W+|ko|구입하다}}
* {{Z|kw}}: {{W-|kw|prena}}
* {{Z|co}}: {{W+|co|cumprà}}
* {{Z|kum}}: {{W-|kum|алмакъ}}
* {{Z|lld}}: {{W-|lld|comprer}}
* {{Z|ltg}}: {{W-|ltg|pierkt}}
* {{Z|la}}: {{W+|la|emō}}, {{W+|la|sūmō}}
* {{Z|lv}}: {{W+|lv|pirkt}}, {{W-|lv|nopirkt}}
* {{Z|lo}}: {{W-|lo|ຊື້}}
* {{Z|lij}}: {{W-|lij|acatâ}}
* {{Z|lt}}: {{W+|lt|pirkti}}
* {{Z|lmo}}: {{W-|lmo|crompâ}}
* {{Z|lb}}: {{W-|lb|kafen}}
* {{Z|mk}}: {{W-|mk|ку́пува|impf}}
* {{Z|ms}}: {{W+|ms|beli}}
* {{Z|mt}}: {{W-|mt|xtara}}
* {{Z|mnc}}: {{W-|mnc|ᡠᡩᠠᠮᠪᡳ}}
* {{Z|msk}}: {{W-|msk|bili}}
* {{Z|hu}}: {{W+|hu|vásárol}}, {{W+|hu|vesz}}
* {{Z|mn}}: {{W+|mn|авах|sc=Cyrl}}, {{W-|mn|худалдаж авах}}
* {{Z|mos}}: {{W-|mos|ra}}
* {{Z|gld}}: {{W-|gld|гаори}}
* {{Z|nap}}: {{W-|nap|accattà}}
* {{Z|nv}}: {{W-|nv|nayiiłniih|sc=Latn}}
* {{Z|nds}}:
*: {{Z|nds-de}}: {{W-|nds-de|köpen}}
* {{Z|ne}}: {{W+|ne|किन्नु|sc=Deva}}
* {{Z|nrf}}: {{W-|nrf|acater}}, {{W-|nrf|acataer}}
* {{Z|non}}: {{W-|non|kaupa}}, {{W-|non|byggja}}
* {{Z|no}}: {{W+|no|kjøpe}}
* {{Z|oj}}: {{W-|oj|adaawen}}
* {{Z|ryu}}: {{W-|ryu|買ーゆん|tr=kōyun}}, {{W-|ryu|買ーいん|tr=kōin}}
* {{Z|oc}}: {{W+|oc|crompar}}
* {{Z|om}}: {{W-|om|bituu}}
* {{Z|os}}: {{W-|os|ӕлхӕнын}}
* {{Z|ug}}: {{W-|ug|سېتىۋالماق}}
* {{Z|uz}}: {{W-|uz|sotib olmoq}}
* {{Z|pap}}: {{W-|pap|kumpra}}
* {{Z|ps}}: {{W+|ps|پېرل|tr=perǝ́l}}, {{W+|ps|پېرودل|tr=perowdᶕl}}
* {{Z|pl}}: {{W+|pl|kupować|impf}}, {{W+|pl|kupić|pf}}, {{W+|pl|nabywać|impf}}, {{W+|pl|nabyć|pf}}
* {{Z|pt}}: {{W+|pt|comprar}}, {{W+|pt|adquirir}}
* {{Z|pa}}: {{W-|pa|ਖ਼ਰੀਦਣਾ}}
* {{Z|kk}}: {{W+|kk|сатып алу|sc=Cyrl}}
* {{Z|rm}}: {{W-|rm|cumprar}}, {{W-|rm|cumprer}}
* {{Z|rom}}: {{W-|rom|kinel}}
* {{Z|ro}}: {{W+|ro|cumpăra}}
* {{Z|ru}}: {{W-|ru|покупа́ть|impf}}, {{W-|ru|купи́ть|pf}}
* {{Z|osx}}: {{W-|osx|buggian}}
* {{Z|se}}: {{W-|se|oastit}}
* {{Z|sgs}}: {{W-|sgs|pėrktė}}
* {{Z|sa}}: {{W+|sa|क्रीणाति}}
* {{Z|sc}}: {{W-|sc|comparai}}, {{W-|sc|comporai}}, {{W-|sc|comporare}}, {{W-|sc|cumporai}}, {{W-|sc|comprai}}
* {{Z|ceb}}: {{W-|ceb|palit}}
* {{Z|sea}}: {{W-|sea|belik}}
* {{Z|scn}}: {{W+|scn|accattari}}, {{W+|scn|cumprari}}
* {{Z|sd}}: {{W-|sd|خريد ڪرڻ}}
* {{Z|sh}}:
*: Kirîlî: {{W-|sh|купити|pf}}, {{W-|sh|куповати|impf}}, {{W-|sh|покуповати}}
*: Latînî: {{W+|sh|kupiti|pf}}, {{W+|sh|kupovati|impf}}, {{W+|sh|pokupovati}}
* {{Z|sk}}: {{W-|sk|kupovať|impf}}, {{W+|sk|kúpiť|pf}}
* {{Z|sl}}: {{W-|sl|kupovati|impf}}, {{W+|sl|kupiti|pf}}
* {{Z|so}}: {{W-|so|gado}}, {{W-|so|iibso}}
* Sorbî:
*: {{Z|dsb}}: {{W-|dsb|kupowaś|impf}}, {{W-|dsb|kupiś|pf}}
*: {{Z|hsb}}: {{W+|hsb|kupować|impf}}, {{W-|hsb|kupić|pf}}
* {{Z|st}}: {{W+|st|reka}}
* {{Z|es}}: {{W+|es|comprar}}
* {{Z|sw}}: {{W-|sw|kununua}}
* {{Z|sv}}: {{W+|sv|köpa}}
* {{Z|zdj}}: {{W-|zdj|hula|cuda=uhula}}
* {{Z|tg}}: {{W+|tg|харидан|sc=Cyrl}}
* {{Z|tl}}: {{W+|tl|bili}}, {{W-|tl|bilhin}}, {{W-|tl|bumili}}
* {{Z|ta}}: {{W+|ta|வாங்கு|sc=Taml}}
* {{Z|tsg}}: {{W-|tsg|bii}}
* {{Z|blt}}: {{W-|blt|ꪏꪳ꫁}}
* {{Z|th}}: {{W+|th|ซื้อ}}
* {{Z|tzm}}: {{W-|tzm|ⵙⵖ}}, {{W-|tzm|ⴰⵖ}}
* {{Z|crh}}: {{W-|crh|satın almaq}}
* {{Z|tet}}: {{W-|tet|sosa}}
* {{Z|tr}}: {{W+|tr|satın almak}}, {{W+|tr|alım yapmak}}
* {{Z|tk}}: {{W-|tk|satyn almak}}
* {{Z|bo}}: {{W-|bo|ཉོས}}, {{W-|bo|གཟིགས}}
* {{Z|txb}}: {{W-|txb|käry-}}
* {{Z|tum}}: {{W-|tum|gula}}
* {{Z|ttg}}: {{W-|ttg|meli}}
* {{Z|tyv}}: {{W-|tyv|садып алыр}}
* {{Z|ur}}: {{W-|ur|مول لینا|tr=mol lēnā}}, {{W-|ur|خریدنا|tr=xarīdnā|sc=ur-Arab}}
* {{Z|uk}}: {{W-|uk|купува́ти|impf}}, {{W-|uk|купи́ти|pf|sc=Cyrl}}
* {{Z|vec}}: {{W-|vec|cronpar}}, {{W-|vec|conprar}}
* {{Z|vi}}: {{W+|vi|mua}}
* {{Z|wa}}: {{W+|wa|atchter}}
* {{Z|war}}: {{W-|war|palit}}, {{W-|war|balyo}}
* {{Z|cy}}: {{W+|cy|prynu}}
* {{Z|km}}: {{W+|km|ទិញ}}
* {{Z|yai}}: {{W-|yai|хиринак}}
* {{Z|yi}}: {{W-|yi|קויפֿן}}
* {{Z|yo}}: {{W-|yo|rà}}
* {{Z|el}}: {{W+|el|αγοράζω}}
*: {{Z|grc}}: {{W-|grc|ἀγοράζω}}, {{W-|grc|ὠνέομαι}}
* {{Z|za}}: {{W-|za|cawx}}
{{werger-bin}}
:{{werger çavkanî|en|buy|oldid=75683870|beş=Ev qismê Wergerê}}
=== Navdêr ===
{{navdêr|ku|z=m}}
# [[standin|Standin]]a bi [[pere]], [[peydakirin]]a bi [[pare]].
#: {{nimûne|ku|'''Kirîna''' xanî biryareke mezin e.}}
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Werger ====
{{werger-ser|navdêr}}
* {{Z|de}}: {{W+|de|Einkauf|n}}, {{W+|de|Kauf|n}}
* {{Z|ba}}: {{W-|ba|һатып алыу}}
* {{Z|be}}: {{W-|be|паку́пка|m}}, {{W-|be|ку́пля|m}}
* {{Z|bg}}: {{W-|bg|поку́пка|m}}
* {{Z|cs}}: {{W+|cs|nákup|n}}
* {{Z|zh}}:
*: {{Z|cmn}}: {{W+|cmn|購買}}
* {{Z|id}}: {{W+|id|pembelian}}
* {{Z|ar}}: {{W-|ar|شِرَاء|n}}, {{W-|ar|اِشْتِرَاء|n}}
* {{Z|hy}}: {{W+|hy|գնում}}
* {{Z|fa}}: {{W+|fa|خرید|tr=xarid}}, {{W+|fa|خریداری|tr=xaridâri}}
* {{Z|fi}}: {{W+|fi|hankinta}}
* {{Z|fr}}: {{W+|fr|achat|n}}, {{W+|fr|acquisition|m}}
* {{Z|frr}}: {{W-|frr|kuup|n}}
* {{Z|gd}}: {{W-|gd|ceannach|n}}
* {{Z|gl}}: {{W+|gl|compra|m}}, {{W+|gl|adquisición|m}}
* {{Z|ka}}: {{W-|ka|შეძენა}}, {{W-|ka|ყიდვა}}
* {{Z|nl}}: {{W+|nl|verwerving}}, {{W+|nl|aanschaf}}
* {{Z|en}}: {{W+|en|purchase}}
* {{Z|ga}}: {{W-|ga|ceannach|n}}
*: {{Z|sga}}: {{W-|sga|creicc|m}}
* {{Z|it}}: {{W+|it|compra|m}}, {{W+|it|acquisto|n}}
* {{Z|ja}}: {{W+|ja|購入|tr=こうにゅう, kōnyū}}, {{W+|ja|購買|tr=こうばい, kōbai}}
* {{Z|pam}}: {{W-|pam|sali}}, {{W-|pam|saluan}}, {{W-|pam|salwan}}
* {{Z|ca}}: {{W+|ca|compra|m}}
* {{Z|ko}}: {{W+|ko|구매}}, {{W+|ko|구입}}
* {{Z|la}}: {{W+|la|emptiō|m}}
* {{Z|mk}}: {{W-|mk|купување|nt}}
* {{Z|ms}}: {{W-|ms|pembelian}}
* {{Z|gv}}: {{W-|gv|kionnaghey|n}}
* {{Z|mi}}: {{W-|mi|hokonga}}
* {{Z|hu}}: {{W+|hu|vásárlás}}, {{W+|hu|megvásárlás}}
* {{Z|nrf}}: {{W-|nrf|acat|n}}
* {{Z|no}}:
*: {{Z|nb}}: {{W+|nb|kjøp|nt}}, {{W-|nb|anskaffelse|n}}
* {{Z|pl}}: {{W+|pl|kupno|nt}}, {{W+|pl|zakup|n}}
* {{Z|pt}}: {{W+|pt|compra|m}}, {{W+|pt|aquisição|m}}
* {{Z|ro}}: {{W+|ro|cumpărare|m}}, {{W+|ro|achiziție|m}}
* {{Z|ru}}: {{W-|ru|поку́пка|m}}, {{W-|ru|приобрете́ние|nt}}, {{W-|ru|ку́пля|m}}
* {{Z|sk}}: {{W-|sk|nákup|n}}
* {{Z|sl}}: {{W-|sl|nakup|n}}
* Sorbî:
*: {{Z|dsb}}: {{W-|dsb|nakup|m}}
* {{Z|es}}: {{W+|es|compra|m}}, {{W+|es|adquisición|m}}
* {{Z|sv}}: {{W+|sv|köp|nt}}
* {{Z|tg}}: {{W-|tg|харид}}
* {{Z|tr}}: {{W+|tr|alma}}
* {{Z|uk}}: {{W-|uk|купі́вля|m}}, {{W-|uk|купува́ння|nt}}
* Yûnanî:
*: {{Z|grc}}: {{W-|grc|ὤνησις|m}}
{{werger-bin}}
:{{werger çavkanî|en|purchase|oldid=75627893|beş=Ev qismê Wergerê}}
=== Formeke lêkerê ===
{{lêker|ku|form=erê}}
# [[form]]a [[pirjimar]] ya [[dema borî ya sade]] ya lêkera '''kirîn''': <br>''Min çend sêv kirîn.''
==== Anagram ====
* {{anagram|ku|a=iîknr|kirnî|kîrin}}
== {{ziman|ckb}} ==
=== Lêker ===
{{lêker|ckb|sc=Latn|c=}}
# {{g|ku|kirîn}}
igq4r9ff7omb7pw9tksbg5ef8fpro47
6532000
6531994
2026-09-17T20:12:10Z
Balyozbot
7867
Bot: Paqijiyên kozmetîk (Lînk paqij kir.)
6532000
wikitext
text/x-wiki
{{h-bnr|kirin}}
== {{ziman|ku}} ==
=== Bilêvkirin ===
* (''Lêkera rader û navdêr''): {{ku-IPA|k(h)iřîn}}, کڕین
:* {{deng|ku|LL-Q36163 (kmr)-Keyomerd-kirîn.wav|dever=Qoser}}
:* {{deng|ku|LL-Q36163 (kmr)-Key Mîrza-kirîn.wav|dever=Bidlîs}}
:* {{deng|ku|LL-Q36163 (kmr)-Ebulfîda-kirîn.wav|Deng|dever=Bidlîs}}
* (''Forma lêkerê ya dema borî''): {{ku-IPA|"kiřîn}}, کڕین
* {{ku-kîte}}
=== Lêker ===
{{ku-tewîn-lk|kirîn|form=gerguhêz|niha=kir|borî=kirî}}
[[Wêne:Euro_banknotes_Europa_series.png|thumb|Mirov bi [[pare]]/[[pere]] tiştan [[kirîn|dikire]].]]
{{lêker|ku|c=x-gh}}, {{k-ya req}}, {{r-ya req}}
# bi [[pare]][[dan]]ê [[wergirtin]], bi [[dirav]]danê [[bi dest xistin]], bi [[pere]] [[standin]]
#: {{mk|ku|Te [[tirimpêl]]a xwe bi çendê '''kiriye'''?}}
#: {{mk|ku|Min xaniyek bi 150 000 euroyan '''kirî'''.}}
#* {{J:Melayê Cizîrî:Dîwana Melayê Cizîrî|jêgirtin=Fe-bihe l-'enberu fa'ih we-bihe l-misku 'ebîq«</br>Çi bi kuştin çi bi hiştin tu ko ferman '''bikirî'''</br>Ez bi hukmê te xweş im »Ehmedu 'ebdun we-reqîq}}
==== Bi alfabeyên din ====
* {{ku-ar|کڕین}}
==== Formên alternatîv ====
{{kol3|ku
|kirrîn
|kerîn
}}
==== Hevmane ====
{{kol3|ku
|standin
|stendin
}}
==== Dijmane ====
{{kol3|ku
|firotin
}}
==== Jê ====
{{kol3|ku
|bikir
|bikirî
|kiryar
|kiryarî
}}
==== Têkildar ====
{{kol3|ku
|kirê
}}
==== Etîmolojî ====
Ji {{ji|ine-pro|ku|-}}: *'''k<sup>w</sup>rei'''- ("kirîn"),
{{ji|ira-pro||-}}: *'''xrey'''- ("kirîn"),
{{pal}}: '''xiryn'''- ("kirîn"),
{{sog}}: '''xiryn'''- ("kirîn"),
{{ho}}: '''xn'''- ("kirîn"),
{{fa}}: '''xerîden'''/'''xer'''- ("kirîn"),
[[gîlekî]]: '''heên'''/'''hin'''- ("kirîn") ... {{ir}},
{{kmr}}: '''kirîn'''/'''kir'''- ("kirîn") ,
{{zza}}: '''[[herînayen]]'''/'''her'''- ("kirîn"),
{{sa}}: '''kirey''' ("kirîn"),
[[rusî|rusiya kevn]]: '''kirenuti''' ("kirîn")
;{{biçûk|Çavkanî:
* {{Cheung|r=446}}
}}
==== Bide ber ====
{{kol3|ku
|kirin
}}
==== Herwiha binêre ====
{{kol3|ku
|birîn
|çirîn
|dirîn
|hirîn
|mirîn
|nirîn
|qirîn
|sirîn
|şîrîn
|tirîn
|wirîn
|zirîn
}}
==== Werger ====
{{werger-ser}}
* {{Z|ab}}: {{W-|ab|аахәара}}
* {{Z|af}}: {{W+|af|koop}}
* {{Z|xag}}: {{W-|xag|𐔸𐕒𐔲𐕒𐕡𐕆𐔴𐕍𐔴𐕚𐕒𐕡𐕎}}
* {{Z|ak}}: {{W-|ak|tɔ}}
* {{Z|akl}}: {{W-|akl|bakae}}
* {{Z|sq}}: {{W+|sq|blej}}
* {{Z|de}}: {{W+|de|kaufen}}
* {{Z|am}}: {{W-|am|መግዛት|sc=Ethi}}
* Apaçî:
*: {{Z|apw}}: {{W-|apw|nahiʼdiih}}
* {{Z|an}}: {{W-|an|crompar}}
* {{Z|aii}}: {{W-|aii|ܙܵܒ݂ܹܢ|tr=zawen}}
* {{Z|rup}}: {{W-|rup|acumpãr}}
* {{Z|as}}: {{W-|as|কিনা}}
* {{Z|ast}}: {{W-|ast|mercar}}, {{W-|ast|comprar}}
* {{Z|av}}: {{W-|av|бичун босизе|sc=Cyrl}}
* {{Z|ain}}: {{W-|ain|ホㇰ|tr=hok}}
* {{Z|az}}: {{W+|az|almaq}}
* {{Z|bdr}}: {{W-|bdr|beli}}
* {{Z|eu}}: {{W+|eu|erosi}}
* {{Z|ba}}: {{W-|ba|һатып алыу}}, {{W-|ba|алыу}}
* {{Z|beg}}: {{W-|beg|melei}}
* {{Z|be}}: {{W-|be|купля́ць|impf}}, {{W-|be|купі́ць|pf|sc=Cyrl}}
* {{Z|bn}}: {{W+|bn|কেনা|sc=Beng}}
* {{Z|bsb}}: {{W-|bsb|mali}}
* {{Z|br}}: {{W+|br|prenañ}}
* {{Z|kxd}}: {{W-|kxd|bali}}
* {{Z|bg}}: {{W-|bg|купу́вам|impf}}
* {{Z|my}}: {{W+|my|ဝယ်|sc=Mymr}}
* {{Z|cs}}: {{W+|cs|kupovat|impf}}, {{W+|cs|koupit|pf}}
* {{Z|ny}}: {{W-|ny|gula}}
* {{Z|zh}}:
*: {{Z|yue}}: {{W-|yue|買|tr=maai5}}
*: {{Z|cmn}}: {{W+|cmn|買|tr=mǎi}}
* {{Z|chk}}: {{W-|chk|kamö}}
* {{Z|da}}: {{W+|da|købe}}
* {{Z|dtp}}: {{W-|dtp|boli}}
* {{Z|ovd}}: {{W-|ovd|tjyöpa}}
* {{Z|id}}: {{W+|id|beli|cuda=membeli}}
* {{Z|ar}}: {{W+|ar|اِشْتَرَى}}
*: {{Z|arz}}: {{W-|arz|اشترى|tr=ištarā|sc=Arab}}
*: {{Z|ary}}: {{W-|ary|شرى|tr=šra}}
* {{Z|hy}}: {{W+|hy|գնել}}, {{W+|hy|առնել}}
* {{Z|eo}}: {{W+|eo|aĉeti}}
* {{Z|et}}: {{W-|et|ostma}}
* {{Z|evn}}: {{W-|evn|гами}}
* {{Z|gur}}: {{W-|gur|da}}
* {{Z|fa}}: {{W+|fa|خریدن|tr=xaridan}}
* {{Z|fo}}: {{W-|fo|keypa}}
* {{Z|fi}}: {{W+|fi|ostaa}}
* {{Z|frp}}: {{W-|frp|achetar}}
* {{Z|fr}}: {{W+|fr|acheter}}
* {{Z|fur}}: {{W-|fur|comprâ}}
* {{Z|frr}}: {{W-|frr|kuupe}}
* {{Z|fy}}: {{W-|fy|keape}}, {{W-|fy|keapje}}
* {{Z|gd}}: {{W-|gd|ceannaich}}
* {{Z|gl}}: {{W+|gl|mercar}}, {{W+|gl|comprar}}
* {{Z|glk}}: {{W-|glk|هین|tr=he:n}}
* {{Z|got}}: {{W-|got|𐌱𐌿𐌲𐌾𐌰𐌽}}
* {{Z|gu}}: {{W-|gu|ખરીદવું}}
* {{Z|ka}}: {{W-|ka|ყიდვა|sc=Geor}}
* {{Z|gn}}: {{W-|gn|jogua}}
* {{Z|ht}}: {{W-|ht|achte}}
* {{Z|haw}}: {{W-|haw|kūʻai}}
* {{Z|ha}}: {{W-|ha|sáyṓ}}
* {{Z|mba}}: {{W-|mba|palit}}
* {{Z|hi}}: {{W-|hi|मोल लेना}}, {{W-|hi|ख़रीदना|sc=Deva}}
* {{Z|mww}}: {{W-|mww|yuav}}
* {{Z|nl}}: {{W+|nl|kopen}}, {{W+|nl|aanschaffen}}, {{W+|nl|aankopen}}
* {{Z|ibg}}: {{W-|ibg|gatang}}
* {{Z|he}}: {{W-|he|קָנָה|tr=kaná}}
* {{Z|io}}: {{W+|io|komprar}}
* {{Z|ilo}}: {{W-|ilo|gatang}}
* {{Z|ang}}: {{W-|ang|bycgan}}, {{W-|ang|ceapian}}
* {{Z|en}}: {{W+|en|buy}}
* {{Z|izh}}: {{W-|izh|ostaa}}, {{W-|izh|lunastaa}}, {{W-|izh|lunata}}
* {{Z|inh}}: {{W-|inh|эца}}
* {{Z|ia}}: {{W-|ia|comprar}}, {{W-|ia|emer}}
* {{Z|ga}}: {{W-|ga|ceannaigh}}
* {{Z|isd}}: {{W-|isd|xatang}}
* {{Z|ist}}: {{W-|ist|cunprà}}
* {{Z|it}}: {{W+|it|comprare}}
* {{Z|ium}}: {{W-|ium|maaiz}}
* {{Z|ivv}}: {{W-|ivv|manadiw}}
* {{Z|is}}: {{W+|is|kaupa}}
* {{Z|ja}}: {{W+|ja|買う|tr=かう, kau}}, {{W+|ja|購入|tr=こうにゅうする, kōnyū suru|cuda=購入する}}
* {{Z|jv}}: {{W+|jv|tuku}}, {{W-|jv|tumbas}}, {{W+|jv|mundhut}}
* {{Z|kab}}: {{W-|kab|ⴰⵖ|tr=aɣ}}
* {{Z|kl}}: {{W-|kl|sivoq}}
* {{Z|xbr}}: {{W-|xbr|kei}}
* {{Z|pam}}: {{W-|pam|sali}}, {{W-|pam|sumali}}
* {{Z|ca}}: {{W+|ca|comprar}}
* {{Z|kgp}}: {{W-|kgp|kajãm}}
* {{Z|qu}}: {{W+|qu|rantiy}}, {{W-|qu|rantii}}
* {{Z|ky}}: {{W+|ky|сатып алуу|sc=Cyrl}}
* {{Z|kg}}: {{W-|kg|kusumba}}
* {{Z|si}}: {{W-|si|ගන්නවා|sc=Sinh}}
* {{Z|ko}}: {{W+|ko|사다}}, {{W+|ko|구입하다}}
* {{Z|kw}}: {{W-|kw|prena}}
* {{Z|co}}: {{W+|co|cumprà}}
* {{Z|kum}}: {{W-|kum|алмакъ}}
* {{Z|lld}}: {{W-|lld|comprer}}
* {{Z|ltg}}: {{W-|ltg|pierkt}}
* {{Z|la}}: {{W+|la|emō}}, {{W+|la|sūmō}}
* {{Z|lv}}: {{W+|lv|pirkt}}, {{W-|lv|nopirkt}}
* {{Z|lo}}: {{W-|lo|ຊື້}}
* {{Z|lij}}: {{W-|lij|acatâ}}
* {{Z|lt}}: {{W+|lt|pirkti}}
* {{Z|lmo}}: {{W-|lmo|crompâ}}
* {{Z|lb}}: {{W-|lb|kafen}}
* {{Z|mk}}: {{W-|mk|ку́пува|impf}}
* {{Z|ms}}: {{W+|ms|beli}}
* {{Z|mt}}: {{W-|mt|xtara}}
* {{Z|mnc}}: {{W-|mnc|ᡠᡩᠠᠮᠪᡳ}}
* {{Z|msk}}: {{W-|msk|bili}}
* {{Z|hu}}: {{W+|hu|vásárol}}, {{W+|hu|vesz}}
* {{Z|mn}}: {{W+|mn|авах|sc=Cyrl}}, {{W-|mn|худалдаж авах}}
* {{Z|mos}}: {{W-|mos|ra}}
* {{Z|gld}}: {{W-|gld|гаори}}
* {{Z|nap}}: {{W-|nap|accattà}}
* {{Z|nv}}: {{W-|nv|nayiiłniih|sc=Latn}}
* {{Z|nds}}:
*: {{Z|nds-de}}: {{W-|nds-de|köpen}}
* {{Z|ne}}: {{W+|ne|किन्नु|sc=Deva}}
* {{Z|nrf}}: {{W-|nrf|acater}}, {{W-|nrf|acataer}}
* {{Z|non}}: {{W-|non|kaupa}}, {{W-|non|byggja}}
* {{Z|no}}: {{W+|no|kjøpe}}
* {{Z|oj}}: {{W-|oj|adaawen}}
* {{Z|ryu}}: {{W-|ryu|買ーゆん|tr=kōyun}}, {{W-|ryu|買ーいん|tr=kōin}}
* {{Z|oc}}: {{W+|oc|crompar}}
* {{Z|om}}: {{W-|om|bituu}}
* {{Z|os}}: {{W-|os|ӕлхӕнын}}
* {{Z|ug}}: {{W-|ug|سېتىۋالماق}}
* {{Z|uz}}: {{W-|uz|sotib olmoq}}
* {{Z|pap}}: {{W-|pap|kumpra}}
* {{Z|ps}}: {{W+|ps|پېرل|tr=perǝ́l}}, {{W+|ps|پېرودل|tr=perowdᶕl}}
* {{Z|pl}}: {{W+|pl|kupować|impf}}, {{W+|pl|kupić|pf}}, {{W+|pl|nabywać|impf}}, {{W+|pl|nabyć|pf}}
* {{Z|pt}}: {{W+|pt|comprar}}, {{W+|pt|adquirir}}
* {{Z|pa}}: {{W-|pa|ਖ਼ਰੀਦਣਾ}}
* {{Z|kk}}: {{W+|kk|сатып алу|sc=Cyrl}}
* {{Z|rm}}: {{W-|rm|cumprar}}, {{W-|rm|cumprer}}
* {{Z|rom}}: {{W-|rom|kinel}}
* {{Z|ro}}: {{W+|ro|cumpăra}}
* {{Z|ru}}: {{W-|ru|покупа́ть|impf}}, {{W-|ru|купи́ть|pf}}
* {{Z|osx}}: {{W-|osx|buggian}}
* {{Z|se}}: {{W-|se|oastit}}
* {{Z|sgs}}: {{W-|sgs|pėrktė}}
* {{Z|sa}}: {{W+|sa|क्रीणाति}}
* {{Z|sc}}: {{W-|sc|comparai}}, {{W-|sc|comporai}}, {{W-|sc|comporare}}, {{W-|sc|cumporai}}, {{W-|sc|comprai}}
* {{Z|ceb}}: {{W-|ceb|palit}}
* {{Z|sea}}: {{W-|sea|belik}}
* {{Z|scn}}: {{W+|scn|accattari}}, {{W+|scn|cumprari}}
* {{Z|sd}}: {{W-|sd|خريد ڪرڻ}}
* {{Z|sh}}:
*: Kirîlî: {{W-|sh|купити|pf}}, {{W-|sh|куповати|impf}}, {{W-|sh|покуповати}}
*: Latînî: {{W+|sh|kupiti|pf}}, {{W+|sh|kupovati|impf}}, {{W+|sh|pokupovati}}
* {{Z|sk}}: {{W-|sk|kupovať|impf}}, {{W+|sk|kúpiť|pf}}
* {{Z|sl}}: {{W-|sl|kupovati|impf}}, {{W+|sl|kupiti|pf}}
* {{Z|so}}: {{W-|so|gado}}, {{W-|so|iibso}}
* Sorbî:
*: {{Z|dsb}}: {{W-|dsb|kupowaś|impf}}, {{W-|dsb|kupiś|pf}}
*: {{Z|hsb}}: {{W+|hsb|kupować|impf}}, {{W-|hsb|kupić|pf}}
* {{Z|st}}: {{W+|st|reka}}
* {{Z|es}}: {{W+|es|comprar}}
* {{Z|sw}}: {{W-|sw|kununua}}
* {{Z|sv}}: {{W+|sv|köpa}}
* {{Z|zdj}}: {{W-|zdj|hula|cuda=uhula}}
* {{Z|tg}}: {{W+|tg|харидан|sc=Cyrl}}
* {{Z|tl}}: {{W+|tl|bili}}, {{W-|tl|bilhin}}, {{W-|tl|bumili}}
* {{Z|ta}}: {{W+|ta|வாங்கு|sc=Taml}}
* {{Z|tsg}}: {{W-|tsg|bii}}
* {{Z|blt}}: {{W-|blt|ꪏꪳ꫁}}
* {{Z|th}}: {{W+|th|ซื้อ}}
* {{Z|tzm}}: {{W-|tzm|ⵙⵖ}}, {{W-|tzm|ⴰⵖ}}
* {{Z|crh}}: {{W-|crh|satın almaq}}
* {{Z|tet}}: {{W-|tet|sosa}}
* {{Z|tr}}: {{W+|tr|satın almak}}, {{W+|tr|alım yapmak}}
* {{Z|tk}}: {{W-|tk|satyn almak}}
* {{Z|bo}}: {{W-|bo|ཉོས}}, {{W-|bo|གཟིགས}}
* {{Z|txb}}: {{W-|txb|käry-}}
* {{Z|tum}}: {{W-|tum|gula}}
* {{Z|ttg}}: {{W-|ttg|meli}}
* {{Z|tyv}}: {{W-|tyv|садып алыр}}
* {{Z|ur}}: {{W-|ur|مول لینا|tr=mol lēnā}}, {{W-|ur|خریدنا|tr=xarīdnā|sc=ur-Arab}}
* {{Z|uk}}: {{W-|uk|купува́ти|impf}}, {{W-|uk|купи́ти|pf|sc=Cyrl}}
* {{Z|vec}}: {{W-|vec|cronpar}}, {{W-|vec|conprar}}
* {{Z|vi}}: {{W+|vi|mua}}
* {{Z|wa}}: {{W+|wa|atchter}}
* {{Z|war}}: {{W-|war|palit}}, {{W-|war|balyo}}
* {{Z|cy}}: {{W+|cy|prynu}}
* {{Z|km}}: {{W+|km|ទិញ}}
* {{Z|yai}}: {{W-|yai|хиринак}}
* {{Z|yi}}: {{W-|yi|קויפֿן}}
* {{Z|yo}}: {{W-|yo|rà}}
* {{Z|el}}: {{W+|el|αγοράζω}}
*: {{Z|grc}}: {{W-|grc|ἀγοράζω}}, {{W-|grc|ὠνέομαι}}
* {{Z|za}}: {{W-|za|cawx}}
{{werger-bin}}
:{{werger çavkanî|en|buy|oldid=75683870|beş=Ev qismê Wergerê}}
=== Navdêr ===
{{navdêr|ku|z=m}}
# [[standin|Standina]] bi [[pere]], [[peydakirin]]a bi [[pare]].
#: {{nimûne|ku|'''Kirîna''' xanî biryareke mezin e.}}
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Werger ====
{{werger-ser|navdêr}}
* {{Z|de}}: {{W+|de|Einkauf|n}}, {{W+|de|Kauf|n}}
* {{Z|ba}}: {{W-|ba|һатып алыу}}
* {{Z|be}}: {{W-|be|паку́пка|m}}, {{W-|be|ку́пля|m}}
* {{Z|bg}}: {{W-|bg|поку́пка|m}}
* {{Z|cs}}: {{W+|cs|nákup|n}}
* {{Z|zh}}:
*: {{Z|cmn}}: {{W+|cmn|購買}}
* {{Z|id}}: {{W+|id|pembelian}}
* {{Z|ar}}: {{W-|ar|شِرَاء|n}}, {{W-|ar|اِشْتِرَاء|n}}
* {{Z|hy}}: {{W+|hy|գնում}}
* {{Z|fa}}: {{W+|fa|خرید|tr=xarid}}, {{W+|fa|خریداری|tr=xaridâri}}
* {{Z|fi}}: {{W+|fi|hankinta}}
* {{Z|fr}}: {{W+|fr|achat|n}}, {{W+|fr|acquisition|m}}
* {{Z|frr}}: {{W-|frr|kuup|n}}
* {{Z|gd}}: {{W-|gd|ceannach|n}}
* {{Z|gl}}: {{W+|gl|compra|m}}, {{W+|gl|adquisición|m}}
* {{Z|ka}}: {{W-|ka|შეძენა}}, {{W-|ka|ყიდვა}}
* {{Z|nl}}: {{W+|nl|verwerving}}, {{W+|nl|aanschaf}}
* {{Z|en}}: {{W+|en|purchase}}
* {{Z|ga}}: {{W-|ga|ceannach|n}}
*: {{Z|sga}}: {{W-|sga|creicc|m}}
* {{Z|it}}: {{W+|it|compra|m}}, {{W+|it|acquisto|n}}
* {{Z|ja}}: {{W+|ja|購入|tr=こうにゅう, kōnyū}}, {{W+|ja|購買|tr=こうばい, kōbai}}
* {{Z|pam}}: {{W-|pam|sali}}, {{W-|pam|saluan}}, {{W-|pam|salwan}}
* {{Z|ca}}: {{W+|ca|compra|m}}
* {{Z|ko}}: {{W+|ko|구매}}, {{W+|ko|구입}}
* {{Z|la}}: {{W+|la|emptiō|m}}
* {{Z|mk}}: {{W-|mk|купување|nt}}
* {{Z|ms}}: {{W-|ms|pembelian}}
* {{Z|gv}}: {{W-|gv|kionnaghey|n}}
* {{Z|mi}}: {{W-|mi|hokonga}}
* {{Z|hu}}: {{W+|hu|vásárlás}}, {{W+|hu|megvásárlás}}
* {{Z|nrf}}: {{W-|nrf|acat|n}}
* {{Z|no}}:
*: {{Z|nb}}: {{W+|nb|kjøp|nt}}, {{W-|nb|anskaffelse|n}}
* {{Z|pl}}: {{W+|pl|kupno|nt}}, {{W+|pl|zakup|n}}
* {{Z|pt}}: {{W+|pt|compra|m}}, {{W+|pt|aquisição|m}}
* {{Z|ro}}: {{W+|ro|cumpărare|m}}, {{W+|ro|achiziție|m}}
* {{Z|ru}}: {{W-|ru|поку́пка|m}}, {{W-|ru|приобрете́ние|nt}}, {{W-|ru|ку́пля|m}}
* {{Z|sk}}: {{W-|sk|nákup|n}}
* {{Z|sl}}: {{W-|sl|nakup|n}}
* Sorbî:
*: {{Z|dsb}}: {{W-|dsb|nakup|m}}
* {{Z|es}}: {{W+|es|compra|m}}, {{W+|es|adquisición|m}}
* {{Z|sv}}: {{W+|sv|köp|nt}}
* {{Z|tg}}: {{W-|tg|харид}}
* {{Z|tr}}: {{W+|tr|alma}}
* {{Z|uk}}: {{W-|uk|купі́вля|m}}, {{W-|uk|купува́ння|nt}}
* Yûnanî:
*: {{Z|grc}}: {{W-|grc|ὤνησις|m}}
{{werger-bin}}
:{{werger çavkanî|en|purchase|oldid=75627893|beş=Ev qismê Wergerê}}
=== Formeke lêkerê ===
{{lêker|ku|form=erê}}
# [[form]]a [[pirjimar]] ya [[dema borî ya sade]] ya lêkera '''kirîn''': <br>''Min çend sêv kirîn.''
==== Anagram ====
* {{anagram|ku|a=iîknr|kirnî|kîrin}}
== {{ziman|ckb}} ==
=== Lêker ===
{{lêker|ckb|sc=Latn|c=}}
# {{g|ku|kirîn}}
bpcclnsag1lmhh1f6l3bqa83xnf5btb
xulam
0
4044
6531965
6339032
2026-09-17T15:56:39Z
Wikihez
7344
6531965
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-IPA|xulam|ẍulam}}
=== Navdêr ===
[[Wêne:A_valet_in_India.jpg|thumb|Xulamekî [[hindistan]]î.]]
{{navdêr|ku|z=n}}
# Kesê di [[xizmet]]a kesekî din de [[xebitîn|dixebite]] (bi taybetî di xizmeta [[axa]]yekî de û yê herdem li mala wî), <br>kesê ku çi [[ferman]] bên dan, wê [[bi cih anîn|bi cih bîne]] (bêyî ku li ziyana bigihe wî bi xwe yan kesek wî bifikire)
#: {{hevmaneyên peyvê|ku|geda|noker|xizmetkar}}
#* {{J:Melayê Cizîrî:Dîwana Melayê Cizîrî|jêgirtin=Ko sed Cimşîd û Keyxwisro<br />'''Xulam''' in ser devê rê da}}
#* {{jêder-pirtûk|ku|jêgirtin=Lê çima, bi mervê [[wisa|usa]] ra tu '''ẍulam''' pê ra tunne ne, bi ecêbmayî [[merî]]yêd nenas dipirsin. |nivîskar=Têmûrê Xelîl Mûradov|sernav=Nezerê Mêrxas|sal=1937|rûpel=12}}
==== Notên bikaranînê ====
{{agahdarî}} Peyva ''xulam'' bi gelemperî tenê bo [[zelam]]an tê bikaranîn. Li şûna wê bo [[jin]]an yan [[keç]]an peyva [[xidam]] heye.
==== Bi alfabeyên din ====
* {{ku-ar|خولام}}, {{g|ku|sc=Arab|غولام|tr=-}}
==== Formên alternatîv ====
* {{g|ku|gulam}}
==== Hevmane ====
* {{g|ku|noker}}
==== Jê ====
* {{g|ku|xulametî}}
* {{g|ku|xulamî}}
* {{g|ku|xulamtî}}
* {{g|ku|xulamik}}
==== Têkildar ====
* {{g|ku|kole}}
* {{g|ku|evd}}
* {{g|ku|benî}}
==== Etîmolojî ====
Ji {{deyn|ar|ku|غلام}} (ẍulam: xulam, xizmetkar, [[kurrik]], [[lawik]], [[xort]]), hevreha {{hv|arc|ܥܠܝܡܐ}} ('ileyma: xulam, lawik, kurrik), {{hv|he|עלם}} ('êlem: [[kurrik]], [[lawik]]). Hevberî [[xeyn]]a erebî di aramî û îbrî de adeten [[eyn]] heye. Bo têkiliya maneyê di navbera "kurrik, lawik" û "xizmetkar" de, bide ber [[garson]] (xizmetkarên xwaringehan / qehwexaneyan) ji {{nehevreh|fr|garçon}} (kurrik, lawik).
==== Herwiha binêre ====
* {{g|ku|bindest}}
* {{g|ku|dîl}}
* {{g|ku|êsîr}}
==== Werger ====
{{werger-ser}}
* {{Z|xag}}: {{W-|xag|𐔱𐔰𐕅𐔱𐔰𐕍𐔰𐔾}}
* {{Z|sq}}: {{W+|sq|shërbyes|n}}, {{W+|sq|shërbyese|m}}
* {{Z|de}}: {{W+|de|Diener|n}}, {{W+|de|Dienerin|m}}, {{W+|de|Lakai|n}}, {{W+|de|Kammerdiener|n}}, {{W+|de|Zofe|m}}, {{W+|de|Bediensteter|n}}, {{W-|de|Bediener|n}}, {{W+|de|Dienstbote|n}}; {{W+|de|Knecht|n}}, {{W+|de|Magd|m}}
* {{Z|am}}: {{W-|am|ሎሌ|n}}
* {{Z|rup}}: {{W-|rup|dul|n}}, {{W-|rup|huzmichear|n}}, {{W-|rup|shcljau|n}}
* {{Z|az}}: {{W+|az|qulluqçu}}, {{W+|az|nökər}}, {{W+|az|xidmətçi}}
* {{Z|ba}}: {{W-|ba|хеҙмәтсе}}
* {{Z|be}}: {{W-|be|слуга́|n}}, {{W-|be|служа́нка|m}}, {{W-|be|прыслу́жнік|n}}, {{W-|be|прыслу́жніца|m}}, {{W-|be|слу́жка|n|m}}
* {{Z|bn}}: {{W-|bn|নোকর}}, {{W+|bn|চাকর}}
* {{Z|bol}}: {{W-|bol|buda}}
* {{Z|br}}: {{W-|br|servijour|n}}, {{W-|br|servijourez|m}}
* {{Z|bg}}: {{W+|bg|слуга́|n}}, {{W+|bg|прислу́жник|n}}, {{W-|bg|прислу́жница|m}}, {{W+|bg|служи́тел|n}}, {{W+|bg|служи́телка|m}}
* {{Z|my}}: {{W+|my|အစေအပါး}}
* {{Z|cim}}: {{W-|cim|diinar|n}}, {{W-|cim|diinaren|m}}
* {{Z|cs}}: {{W+|cs|sluha|n}}, {{W-|cs|služebný|n}}, {{W+|cs|služka|m}}
* {{Z|zh}}:
*: {{Z|cmn}}: {{W+|cmn|僕人|tr=púrén}}, {{W-|cmn|傭人|tr=yōngrén}}, {{W-|cmn|奴僕|tr=núpú}}, {{W+|cmn|奴役|tr=núyì}}
* {{Z|da}}: {{W+|da|tjener|g}}
* {{Z|id}}: {{W+|id|bantu|cuda=pembantu}}
* {{Z|ar}}: {{W-|ar|خَادِم|n}}, {{W-|ar|خَادِمَة|m}}, {{W-|ar|خَدَم|p}}
*: {{Z|arz}}: {{W-|arz|خدام|n|tr=ḵaddām}}, {{W-|arz|خدامة|m|tr=ḵaddāma}}
* {{Z|hy}}: {{W+|hy|ծառա}}, {{W+|hy|սպասավոր}}
* {{Z|myv}}: {{W-|myv|вардо|n}}, {{W-|myv|варданка|m}}
* {{Z|et}}: {{W-|et|teenija}}, {{W-|et|sulane}}
* {{Z|fa}}: {{W+|fa|خدمتکار|tr=xedmatkâr}}, {{W+|fa|نوکر|tr=nokar}}, {{W+|fa|خادم|tr=xâdem}}
* {{Z|fi}}: {{W+|fi|palvelija}}
* {{Z|fr}}: {{W+|fr|domestique|n|m}}, {{W+|fr|servante|m}}, {{W+|fr|serviteur|n}}
* {{Z|fur}}: {{W-|fur|sierf|n}}, {{W-|fur|famei|n}}, {{W-|fur|servidôr|n}}
* {{Z|gd}}: {{W-|gd|searbhanta|n|m}}, {{W-|gd|seirbhiseach|n}}
* {{Z|gl}}: {{W+|gl|servente|n}}, {{W-|gl|criado|n}}
* {{Z|got}}: {{W-|got|𐌸𐌹𐌿𐍃|n}}, {{W-|got|𐌸𐌹𐍅𐌹|m}}, {{W-|got|𐌰𐌽𐌳𐌱𐌰𐌷𐍄𐍃|n}}
* {{Z|ka}}: {{W-|ka|მსახური}}
* {{Z|ha}}: {{W+|ha|yaro}}
* {{Z|mba}}: {{W-|mba|kabulig}}
* {{Z|hi}}: {{W+|hi|नौकर|n}}, {{W+|hi|चाकर|n}}, {{W+|hi|नौकरानी|m}}, {{W+|hi|सेवक|n}}, {{W+|hi|भृत्य|n}}, {{W-|hi|मुलाज़िम|n}}
* {{Z|nl}}: {{W+|nl|hulp}}, {{W+|nl|hulpje}}, {{W-|nl|huishoudhulp}}, {{W+|nl|bediende}}, {{W+|nl|knecht|n}}, {{W+|nl|meid|m}}, {{W+|nl|dienaar|n}}
* {{Z|he}}: {{W+|he|מְשָׁרֵת|n|tr=mesharét}}
* {{Z|ang}}: {{W-|ang|þeġn|n}}
* {{Z|enm}}: {{W-|enm|serjaunt}}, {{W-|enm|servaunt}}
* {{Z|en}}: {{W+|en|servant}}
* {{Z|ga}}: {{W-|ga|seirbhíseach|n}}, {{W-|ga|searbhónta|n}}
* {{Z|it}}: {{W+|it|servo|n}}, {{W+|it|servitore|n}}, {{W+|it|domestico|n}}, {{W+|it|famiglio|n}}
* {{Z|is}}: {{W+|is|þjónn|n}}
* {{Z|ja}}: {{W+|ja|使用人|tr=しようにん, shiyōnin}}, {{W+|ja|召使い|tr=めしつかい, meshitsukai}}
* {{Z|jv}}: {{W-|jv|réwang}}, {{W-|jv|réncang}}
* {{Z|ca}}: {{W+|ca|servent|n}}, {{W+|ca|criat|n}}
* {{Z|ky}}: {{W+|ky|кызматчы}}, {{W+|ky|кул}}, {{W+|ky|малай}}
* {{Z|swb}}: {{W-|swb|mutru wa hazi|c1|c2}}
* {{Z|ko}}: {{W+|ko|하인}}, {{W+|ko|머슴}}, {{W+|ko|사용인}}
* {{Z|la}}: {{W-|la|minister|n}}, {{W-|la|ministra|m}}, {{W-|la|famulus|n}}, {{W-|la|famula|m}}
* {{Z|lv}}: {{W-|lv|kalps|n}}, {{W-|lv|kalpone|m}}, {{W-|lv|sulains|n}}
* {{Z|lo}}: {{W-|lo|ຂະຍົມ}}, {{W+|lo|ຂ້າ}}, {{W-|lo|ບ່າວ}}
* {{Z|lt}}: {{W-|lt|tarnas|n}}
* {{Z|mk}}: {{W-|mk|слу́га|n}}, {{W-|mk|слу́жител|n}}, {{W-|mk|служи́телка|m}}
* {{Z|ms}}: {{W+|ms|hamba}}, {{W-|ms|khadam}}
* {{Z|mt}}: {{W-|mt|qaddej|n}}, {{W-|mt|seftur|n}}
* {{Z|mi}}: {{W-|mi|hāwini}}, {{W-|mi|tūmau}}
* {{Z|hu}}: {{W+|hu|szolga}}, {{W+|hu|cseléd}}
* {{Z|egy}}: {{W-egy|bꜣk|h=bA-k:H_SPACE-A1|n}}, {{W-egy|bꜣkt|h=bA-k:t-B1|m}}
* {{Z|mn}}:
*: Kirîlî: {{W+|mn|зарц}}
*: Mongolî: {{W-|mn|ᠵᠠᠷᠤᠴᠠ}}
* {{Z|nv}}: {{W-|nv|naalʼaʼí}}
* {{Z|no}}:
*: {{Z|nb}}: {{W+|nb|tjener|n}}
* {{Z|oc}}: {{W+|oc|sirvent|n}}
* {{Z|ug}}: {{W-|ug|خىزمەتچى}}, {{W-|ug|مالاي}}
* {{Z|uz}}: {{W+|uz|xizmatkor}}, {{W+|uz|malay}}
* {{Z|pi}}: {{W-|pi|sevaka|n}}
* {{Z|ps}}: {{W-|ps|خدمتګار|n|tr=xedmatgār}}, {{W+|ps|نوکر|n|tr=nokár}}, {{W+|ps|خادم|n|tr=xādém}}
* {{Z|pdt}}: {{W-|pdt|Deena|n}}
* {{Z|pl}}: {{W+|pl|służący|m-pr}}, {{W+|pl|służąca|m}}, {{W+|pl|sługa|n}}
* {{Z|pt}}: {{W+|pt|empregado|n}}, {{W+|pt|criado|n}}, {{W+|pt|serviçal|n|m}}
* {{Z|kk}}: {{W-|kk|даяшы}}, {{W-|kk|малай}}, {{W-|kk|қызметші}}
* {{Z|ro}}: {{W+|ro|servitor|n}}, {{W+|ro|slugă|m}}, {{W+|ro|slujitor|n}}
* {{Z|ru}}: {{W+|ru|слуга́|n}}, {{W+|ru|служа́нка|m}}
* {{Z|sa}}: {{W+|sa|सेवक|n}}, {{W+|sa|सेविका|m}}, {{W+|sa|भृत्य|n}}
* {{Z|scn}}: {{W+|scn|criatu|n}}, {{W+|scn|servu|n}}
* {{Z|sh}}:
*: Kirîlî: {{W-|sh|слу́га|n}}, {{W-|sh|слу̀жа̄вка|m}}, {{W-|sh|слу̏шкиња|m}}
*: Latînî: {{W+|sh|slúga|n}}, {{W+|sh|slùžāvka|m}}, {{W+|sh|slȕškinja|m}}
* {{Z|cu}}:
*: Kirîlî: {{W-|cu|слоуга|n}}, {{W-|cu|слоужитель|n}}
* {{Z|orv}}: {{W-|orv|слуга|n}}
* {{Z|sk}}: {{W-|sk|sluha|n}}, {{W-|sk|slúžka|m}}
* {{Z|sl}}: {{W+|sl|služabnik|n}}, {{W-|sl|služabnica|m}}, {{W-|sl|sluga|n}}, {{W-|sl|služkinja|m}}
* {{Z|es}}: {{W+|es|sirviente|n}}, {{W+|es|criado|n}}, {{W+|es|mozo|n}}, {{W+|es|doméstico|n}}, {{W+|es|empleado|n}}
* {{Z|sw}}: {{W+|sw|mtumishi}}
* {{Z|sv}}: {{W+|sv|betjänt|g}}, {{W+|sv|tjänare|g}}
* {{Z|tg}}: {{W+|tg|хизматгор}}, {{W-|tg|пешхизмат}}, {{W-|tg|чокар}}, {{W+|tg|навкар}}
* {{Z|rif}}: {{W-|rif|tayya|m}}
* {{Z|th}}: {{W+|th|ข้า}}, {{W+|th|บ่าว}}, {{W-|th|ภัจ}}, {{W+|th|คนรับใช้}}, {{W+|th|เสวก}}, {{W+|th|คนใช้}}, {{W+|th|ภฤตย์}}
* {{Z|tt}}: {{W+|tt|хезмәтче}}
* {{Z|ti}}: {{W-|ti|ከዳሚ|n}}
* {{Z|tr}}: {{W+|tr|hizmetçi}}, {{W+|tr|uşak}}, {{W+|tr|kul}}
* {{Z|tk}}: {{W-|tk|gullukçy}}, {{W-|tk|hyzmatkär}}
* {{Z|bo}}: {{W-|bo|བྲན་གཡོག}}
* {{Z|txb}}: {{W-|txb|mañiye}}, {{W-|txb|mañiya}}
* {{Z|ur}}: {{W-|ur|نوکر|n|tr=naukar}}, {{W-|ur|چاکر|n|tr=cākar}}, {{W-|ur|نوکرانی|m|tr=naukrānī}}, {{W-|ur|خادم|n|tr=xādim}}, {{W-|ur|ملازم|n|tr=mulāzim}}
* {{Z|uk}}: {{W-|uk|слуга́|n}}, {{W-|uk|служни́ця|m}}
* {{Z|vec}}: {{W-|vec|servidór|n}}, {{W-|vec|famegio|n}}, {{W-|vec|besariòl|n}}
* {{Z|vi}}: {{W+|vi|đầy tớ}}, {{W+|vi|người hầu}}
* {{Z|km}}: {{W-|km|អ្នកបំរើ}}, {{W+|km|សេវកៈ}}, {{W+|km|បាវ}}, {{W+|km|ព្រាវ}}
* {{Z|yi}}: {{W-|yi|דינסט|nt}}, {{W-|yi|קנעכט|n}}, {{W-|yi|באַדינער|n}}, {{W-|yi|באַדינערטע|m}}
* {{Z|el}}: {{W+|el|υπηρέτης|n}}
*: Kevn: {{W-|grc|θεράπων|n}}, {{W-|grc|θεράπαινα|m}}
{{werger-bin}}
:{{werger çavkanî|en|servant|beş=Ev qismê Wergerê|oldid=75800245}}
slr466lammmieomoqy28uklkjly4402
dîn
0
4528
6531960
6371001
2026-09-17T15:46:30Z
Wikihez
7344
Kurmancî/Rengdêr: beşa «Formên alternatîv» hate nûkirin (DefSideBoxes)
6531960
wikitext
text/x-wiki
{{h-bnr|din}}
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-IPA}}
* {{deng|ku|LL-Q36163 (kmr)-Ebulfîda-dîn.wav|dever=Bidlîs}}
* {{ku-kîte}}
=== Navdêr ===
[[Wêne:Religions_4x5.png|thumb|250px|Sembolên hinek '''dîn''' û baweriyan.]]
{{navdêr|ku|z=n}}
# {{maneid|ku|bawerî}} [[bîr|Bîr]] û [[bawerî|baweriyên]] [[mirov]]an li ser hindê ka [[dinya]] û [[mirov]] çawan çêbûye û [[Xwedê]] yan [[xweda]] çawan in û [[têkilî|têkiliyên]] Wî yan wan û mirovan çawan in
#: {{hevmaneyên peyvê|ku|ayîn|bawerî|ol|peristkarî|diyanet}}
#: {{mk|ku|'''Dînê''' îslamê, '''dînê''' fileyan, '''dînê''' êzidiyan, '''dînê''' zerdeştiyan, '''dînê''' cihûyan.}}
#: {{mk|ku|'''Dîn''' û îman nan e, nan ku tune be, bende îro êzdî ye sibe misilman e}}
#* {{J:Mela Hisênê Bateyî:Mewlûda Kurdî|jêgirtin=Daye me mîrasa Qur'ana Kerîm</br>'''Dînê''' me kir kamil û ni'met-temam}}
==== Tewandin ====
{{ku-tewîn-nav|nêr}}
==== Bi alfabeyên din ====
* {{ku-ar|دين}}
==== Jê ====
{{kol3|ku|cure=Jê
|dîn bûn<crz:lêker>
|dîn kirin<crz:lêker>
|dînbûn<crz:navdêr>
|dînkirin<crz:navdêr>
|dînbûyî<crz:rengdêr>
|bêdîn
|bêdînî
|bidîn
|dîndar
|dîndarî
|dînî
|dînhişk
|dînnerm
|dînperist
|dînperistî
|dînperist
|dînperwer
}}
==== Têkildar ====
{{kol2|ku|cure=Têkildar
|dua
|ewliya
|lava
|lawij
|nimêj
|nivêj
|peristin
|peyamber
|pêxember
|rojî
|xweda
|xwedawend
|xwedawendî
|xwedayî
|Xwedê
|Yezdan
|yezdanî
|zekat
}}
==== Etîmolojî ====
{{navmiletî|ku}}, hevreha {{hv|fa|دین}}, {{hv|pal||dên|sc=Latn}}, {{hv|ae||deêna|sc=Latn}} û belkî {{hv|sa|धर्म|tr=dherme-}}. Peyva "dîn" bi hezaran sal e ku hem di zimanên îranî û hem jî yên samî de heye: avestayî "deêna", pehlewî "dên", akadî "dînu" (di wî zimanî de bi maneya "qanûn"ê). Bi ihtimalek mezin rehê peyvê samî ye lê ji akadî (ku zimanek samî ye) bi rêya zimanek din (belkî elamî) ketiye avestayî û ji avestayî jî ketiye pehlewî û taliyê dîsa li erebî vegeriye. Forma niha ya kurdî/farisî bi ihtimalek mezin bi rêya îslamê ji erebî hatiye wergirtin lê dîroka wê gelek dirêj û hêja ye. Bo peyvek din jî ku pêşî ji zimanên îranî ketiye zimanê erebî û paşî bi forma xwe ya erebî li zimanên îranî vegeriye, binere: {{q|ku|birc}}.
Idiaya [[Şablon:Beghyani|Beghyani]], ku dibêje "dîn" peyveke hindûewropî ye, bêbingeh e ji ber ku ji bilî zimanên îranî ev peyv di zimanên din yên hindûewropî de peyda nabe. Zimanên îranî jî ew ji zimanên rojhilatî wergirtiye.{{çp}} Gelek berî avestayî ew peyv di zimanên samî de - wek akadî - hatiye bikaranîn.{{çp}}
==== Werger ====
{{werger-ser}}
* {{Z|ab}}: {{W-|ab|адин|sc=Cyrl}}
* {{Z|ady}}: {{W-|ady|дин}}
* {{Z|sq}}: {{W+|sq|fe|m}}, {{W+|sq|din|n}}
* {{Z|de}}: {{W+|de|Religion|m}}
* {{Z|am}}: {{W-|am|ሃይማኖት}}
* {{Z|an}}: {{W-|an|relichión|m}}
* {{Z|rup}}: {{W-|rup|pisti|m}}
* {{Z|as}}: {{W-|as|ধৰ্ম}}
* {{Z|ast}}: {{W+|ast|relixón|m}}
* {{Z|aot}}: {{W-|aot|thorom}}
* {{Z|av}}: {{W-|av|дин|sc=Cyrl}}
* {{Z|ae}}: {{W-|ae|𐬛𐬀𐬉𐬥𐬁}}
* {{Z|az}}: {{W+|az|din}}, {{W+|az|məzhəb}}
* {{Z|eu}}: {{W-|eu|erlijio}}
* {{Z|ba}}: {{W-|ba|дин}}
* {{Z|be}}: {{W-|be|рэлі́гія|m|sc=Cyrl}}
* {{Z|bn}}: {{W+|bn|ধর্ম|sc=Beng}}, {{W-|bn|দ্বীন}}
* {{Z|bg}}: {{W-|bg|рели́гия|m}}, {{W-|bg|вероизповеда́ние|nt|sc=Cyrl}}
* {{Z|my}}: {{W+|my|အယူဝါဒ|sc=Mymr}}
* {{Z|bua}}: {{W-|bua|шажан}}
* {{Z|ce}}: {{W-|ce|дин}}
* {{Z|cs}}: {{W+|cs|náboženství|nt}}
* {{Z|chr}}: {{W-|chr|ᏗᏁᎸᏙᏗ}}
* {{Z|zh}}:
*: {{Z|yue}}: {{W-|yue|宗教|tr=zung1 gaau3}}
*: {{Z|dng}}: {{W-|dng|җё}}, {{W-|dng|дин}}
*: {{Z|hak}}: {{W-|hak|宗教|tr=chûng-kau}}
*: {{Z|cmn}}: {{W+|cmn|宗教|tr=zōngjiào}}
*: {{Z|cdo}}: {{W-|cdo|宗教|tr=cŭng-gáu}}
*: {{Z|nan}}: {{W+|nan|宗教|tr=chong-kàu}}
*: {{Z|wuu}}: {{W-|wuu|宗教}}
* {{Z|cv}}: {{W-|cv|тӗн|sc=Cyrl}}
* {{Z|da}}: {{W+|da|religion|g}}
* {{Z|dv}}: {{W-|dv|ދީން}}
* {{Z|id}}: {{W+|id|agama}}, {{W+|id|din}}
* {{Z|ar}}: {{W+|ar|دِين|n}}
*: {{Z|arz}}: {{W-|arz|دين|n|tr=dīn|sc=Arab}}
* {{Z|hy}}: {{W+|hy|կրոն|sc=Armn}}
* {{Z|eo}}: {{W-|eo|religio}}
* {{Z|et}}: {{W-|et|religioon}}
* {{Z|pal}}: {{W-|pal|dyn'|tr=dēn}}
* {{Z|fa}}: {{W+|fa|دین|tr=din}}, {{W+|fa|مذهب|tr=mazhab}}, {{W+|fa|کیش|tr=kiš}}, {{W+|fa|آیین|tr=âyin}}
* {{Z|fo}}: {{W-|fo|átrúnaður|n}}
* {{Z|fi}}: {{W+|fi|uskonto}}
* {{Z|fr}}: {{W+|fr|religion|m}}
* {{Z|fy}}: {{W+|fy|religy|g}}
* {{Z|gd}}: {{W-|gd|creideamh|n}}, {{W-|gd|diadhachd|m}}
* {{Z|gl}}: {{W+|gl|relixión|m}}
* {{Z|gu}}: {{W-|gu|ધર્મ}}
* {{Z|ka}}: {{W-|ka|რელიგია|sc=Geor}}, {{W-|ka|აღმსარებლობა}}, {{W-|ka|სარწმუნოება}}
* {{Z|gn}}: {{W-|gn|tupãrape}}
* {{Z|ht}}: {{W-|ht|relijyon}}
* {{Z|ha}}: {{W-|ha|addini}}
* {{Z|hi}}: {{W+|hi|धर्म|n}}, {{W+|hi|दीन|n}}, {{W-|hi|मज़हब|n|sc=Deva}}
* {{Z|hil}}: {{W-|hil|relihiyon}}
* {{Z|nl}}: {{W+|nl|godsdienst|n}}, {{W+|nl|geloofsovertuiging|m}}, {{W+|nl|religie|m}}
* {{Z|hrx}}: {{W-|hrx|Rëlighion|m}}
* {{Z|he}}: {{W-|he|דָּת|m|tr=dat}}
* {{Z|io}}: {{W+|io|religio}}
* {{Z|ang}}: {{W-|ang|ġelēafa|n}}, {{W-|ang|ǣfæstnes|m}}
* {{Z|en}}: {{W+|en|religion}}
* {{Z|inh}}: {{W-|inh|ди}}
* {{Z|ia}}: {{W-|ia|religion}}
* {{Z|ga}}: {{W-|ga|creideamh|n}}, {{W-|ga|reiligiún|m}}
* {{Z|it}}: {{W+|it|religione|m}}
* {{Z|is}}: {{W+|is|trúarbrögð|nt-p}}, {{W+|is|átrúnaður|n}}
* {{Z|ja}}: {{W+|ja|宗教|tr=しゅうきょう, shūkyō}}
* {{Z|kl}}: {{W-|kl|religioni}}
* {{Z|kn}}: {{W+|kn|ಧರ್ಮ|sc=Knda}}
* {{Z|pam}}: {{W-|pam|kasalpantayanan}}, {{W-|pam|kesalpantayanan}}
* {{Z|ca}}: {{W+|ca|religió|m}}
* {{Z|ks}}: {{W-|ks|مَزہَب}}
* {{Z|ky}}: {{W+|ky|дин|sc=Cyrl}}
* {{Z|si}}: {{W+|si|ආගම|sc=Sinh}}
* {{Z|kok}}: {{W-|kok|धर्म}}
* {{Z|ko}}: {{W+|ko|종교}}
* {{Z|lad}}:
*: Îbranî: {{W-|lad|ריליג׳ייון|m|sc=Hebr}}
*: Latînî: {{W-|lad|relijion|m|sc=Latn}}
* {{Z|la}}: {{W+|la|religiō|m}}
* {{Z|lv}}: {{W+|lv|reliģija|m}}
* {{Z|lo}}: {{W+|lo|ສາດສະໜາ}}
* {{Z|lez}}: {{W-|lez|дин}}
* {{Z|lt}}: {{W+|lt|relìgija|m}}
* {{Z|liv}}: {{W-|liv|relīgij}}
* {{Z|lb}}: {{W+|lb|Relioun|m}}
* {{Z|mk}}: {{W-|mk|религија}}
* {{Z|mg}}: {{W+|mg|fivavahana}}
* {{Z|ml}}: {{W+|ml|മതം|sc=Mlym}}
* {{Z|ms}}: {{W+|ms|agama}}, {{W+|ms|kepercayaan}}, {{W-|ms|anutan}}, {{W+|ms|din}}
* {{Z|mt}}: {{W-|mt|reliġjon}}
* {{Z|gv}}: {{W-|gv|craueeaght|m}}
* {{Z|mr}}: {{W+|mr|धर्म|n|sc=Deva}}
* {{Z|hu}}: {{W+|hu|vallás}}
* {{Z|mwl}}: {{W-|mwl|religion|m}}
* {{Z|mn}}:
*: Kirîlî: {{W+|mn|шашин}}
* {{Z|mos}}: {{W-|mos|wẽnd so-tũudum}}
* {{Z|nv}}: {{W-|nv|nahaghá}}
* {{Z|ne}}: {{W-|ne|धर्म}}
* {{Z|nrf}}: {{W-|nrf|r'ligion|m}}
* {{Z|no}}:
*: {{Z|nb}}: {{W+|nb|religion|n}}, {{W-|nb|trosretning|n|m}}
*: {{Z|nn}}: {{W+|nn|religion|n}}
* {{Z|oc}}: {{W+|oc|religion|m}}
* {{Z|or}}: {{W+|or|ଧର୍ମ}}
* {{Z|os}}: {{W-|os|дин|sc=Cyrl}}
* {{Z|ug}}: {{W+|ug|دىن|sc=ug-Arab}}
* {{Z|uz}}: {{W+|uz|din}}, {{W+|uz|mazhab}}
* {{Z|pi}}: {{W-|pi|धम्म|sc=Deva}}, {{W-|pi|dhamma|n}}
* {{Z|ps}}: {{W+|ps|مذهب|n|tr=mazháb}}, {{W+|ps|دين|n|tr=din}}
* {{Z|pms}}: {{W-|pms|religion|m}}
* {{Z|pih}}: {{W-|pih|rilijin}}
* {{Z|pdt}}: {{W-|pdt|Gloowe|n}}, {{W-|pdt|Reljoon|m}}
* {{Z|pl}}: {{W+|pl|religia|m}}
* {{Z|pt}}: {{W+|pt|religião|m}}
* Puncabî:
*: Şaxmuxî: {{W+|pa|دھرم|n|tr=dharm}}
*: Gurmuxî: {{W-|pa|ਧਰਮ|n}}
* {{Z|kk}}: {{W-|kk|дін}}
* {{Z|cop}}: {{W-|cop|ⲙⲉⲧⲁⲥⲉⲃⲏⲥ|m}}
* {{Z|rm}}: {{W-|rm|religiun|m}}
* {{Z|ro}}: {{W+|ro|religie|m}}
* {{Z|ru}}: {{W-|ru|рели́гия|m}}, {{W-|ru|ве́ра|m}}, {{W-|ru|вероиспове́дание|nt}}
* {{Z|rue}}: {{W-|rue|релі́ґія|m}}
* {{Z|sgs}}: {{W-|sgs|viera|m}}
* {{Z|sa}}: {{W+|sa|धर्म|n}}
* {{Z|sat}}: {{W-|sat|ᱫᱤᱱ}}, {{W-|sat|ᱫᱷᱚᱨᱚᱢ}}
* {{Z|sd}}: {{W-|sd|مذهب}}
* {{Z|sh}}:
*: Kirîlî: {{W-|sh|рѐлӣгија|m}}, {{W-|sh|ве̏ра|m}}, {{W-|sh|вје̏ра|m}}, {{W-|sh|вероѝспове̄ст|m}}, {{W-|sh|вјероѝсповије̄ст|m}}
*: Latînî: {{W+|sh|rèlīgija|m}}, {{W+|sh|vȅra|m}}, {{W+|sh|vjȅra|m}}, {{W+|sh|veroìspovēst|m}}, {{W+|sh|vjeroìspovijēst|m}}
* {{Z|syl}}: {{W-|syl|ꠗꠞ꠆ꠝꠧ}}
* {{Z|sco}}: {{W-|sco|releegion}}
* {{Z|sk}}: {{W+|sk|náboženstvo|nt}}, {{W-|sk|relígia|m}}
* {{Z|sl}}: {{W+|sl|vera|m}}, {{W-|sl|veroizpoved|m}}, {{W+|sl|religija|m}}
* {{Z|so}}: {{W-|so|diin}}
* Sorbî:
*: {{Z|dsb}}: {{W-|dsb|nabóžnina|m}}
*: {{Z|hsb}}: {{W-|hsb|nabožina|m}}
* {{Z|es}}: {{W+|es|religión|m}}
* {{Z|sw}}: {{W+|sw|dini}}
* {{Z|sv}}: {{W+|sv|religion|g}}
* {{Z|tg}}: {{W+|tg|дин|sc=Cyrl}}, {{W-|tg|мазҳаб}}
* {{Z|tl}}: {{W-|tl|relihiyon}}
* {{Z|ta}}: {{W+|ta|சமயம்}}, {{W+|ta|மதம்}}
* {{Z|th}}: {{W+|th|ศาสนา}}
* {{Z|te}}: {{W+|te|మతము}}
* {{Z|crh}}: {{W-|crh|din}}
* {{Z|tt}}: {{W+|tt|дин}}
* {{Z|tr}}: {{W+|tr|din}}, {{W+|tr|inanç}}, {{W+|tr|bağlanç}}, {{W+|tr|mezhep}}
* {{Z|tk}}: {{W-|tk|din}}
* {{Z|bo}}: {{W-|bo|ཆོས་ལུགས|sc=Tibt}}
* {{Z|tyv}}: {{W-|tyv|шажын}}
* {{Z|ur}}: {{W-|ur|دین|n|tr=dīn}}, {{W-|ur|دھرم|n|tr=dharm}}, {{W-|ur|مذہب|n|tr=mazhab|sc=ur-Arab}}
* {{Z|uk}}: {{W-|uk|релі́гія|m}}
* {{Z|vec}}: {{W-|vec|rełijon}}
* {{Z|vi}}: {{W+|vi|tôn giáo}}
* {{Z|wa}}: {{W+|wa|rilidjon|m}}
* {{Z|cy}}: {{W+|cy|crefydd}}
* {{Z|wuh}}: {{W-|wuh|quilek}}
* {{Z|km}}: {{W+|km|សាសនា}}
* {{Z|sah}}: {{W-|sah|итэҕэл}}
* {{Z|yi}}: {{W-|yi|רעליגיע|m}}
* {{Z|el}}: {{W+|el|θρησκεία|m}}
*: {{Z|grc}}: {{W-|grc|θρησκεία|m}}
{{werger-bin}}
:{{werger çavkanî|en|religion|beş=Ev qismê Wergerê|oldid=75709763}}
=== Rengdêr ===
{{ku-tewîn-rd|dîn}}
{{rengdêr|ku}}
# {{maneid|ku|bêhiş}} kesa/ê [[bêaqil]], [[bêhiş]], [[bêmejî]]
# [[har]], [[netebitî]]
==== Formên alternatîv ====
{{kol3|ku
|dhîn
|dîhn
}}
==== Hevmane ====
{{kol3|ku
|şeyda
|şêt
|dîwane
}}
==== Jê ====
{{kol3|ku
|dînatî
|dînî
|dînîtî
|dîntî
}}
==== Etîmolojî ====
Kurtkirina [[dêwîn]], hevreha farisî {{g|fa|دیوانه|tr=dîvanê}} ji {{paş|ku|dêw|w1=bi maneya [[cin]]|-ane}}. Bide ber [[mecnûn]].
==== Werger ====
{{werger-ser}}
* {{Z|sq}}: {{W+|sq|çmendur|cuda=i çmendur}}
* {{Z|de}}: {{W+|de|verrückt}}
* {{Z|am}}: {{W-|am|እብድ}}
* {{Z|az}}: {{W+|az|dəli}}
* {{Z|eu}}: {{W-|eu|zoro}}, {{W-|eu|ero}}
* {{Z|be}}: {{W-|be|шалёны}}, {{W-|be|бязу́мны}}
* {{Z|bn}}: {{W+|bn|দেওয়ানা}}, {{W+|bn|পাগল}}, {{W+|bn|মজনু}}
* {{Z|bg}}: {{W+|bg|луд}}, {{W-|bg|побъ́ркан}}
* {{Z|my}}: {{W+|my|နှမ်း}}
* {{Z|cs}}: {{W+|cs|šílený}}, {{W-|cs|pominutý}}, {{W-|cs|trhlý}}, {{W-|cs|potrhlý}}, {{W-|cs|zbavený rozumu}}, {{W-|cs|ztřeštěný}}, {{W+|cs|bláznivý}}
* {{Z|cic}}: {{W-|cic|haksi}}
* {{Z|zh}}:
*: {{Z|yue}}: {{W-|yue|癡線|tr=ci1 sin3}}, {{W-|yue|癲|tr=din1}}
*: {{Z|cmn}}: {{W+|cmn|瘋狂|tr=fēngkuáng}}, {{W+|cmn|瘋|tr=fēng}}
* {{Z|da}}: {{W-|da|skør}}, {{W+|da|sindssyg}}, {{W-|da|sindsforvirret}}, {{W+|da|gal}}, {{W-|da|gakgak}}
* {{Z|id}}: {{W+|id|gila}}
* {{Z|ar}}: {{W-|ar|مَجْنُون}}, {{W-|ar|مَخْبُول}}, {{W+|ar|مَسْعُور}}, {{W+|ar|أَحْمَق|tr=ʔaḥmaq}}
*: {{Z|ary}}: {{W-|ary|حمق|tr=ḥmaq}}, {{W-|ary|مسطي|tr=msaṭṭi}}, {{W-|ary|هبيل|tr=hbīl}}
* {{Z|hy}}: {{W+|hy|խենթ}}, {{W+|hy|խելագար}}, {{W+|hy|գիժ}}
* {{Z|eo}}: {{W+|eo|freneza}}
* {{Z|et}}: {{W+|et|hull}}
* {{Z|fa}}: {{W+|fa|دیوانه|tr=divâne}}, {{W+|fa|مجنون|tr=majnun}}
* {{Z|fi}}: {{W+|fi|hullu}}, {{W+|fi|mieletön}}, {{W+|fi|järjetön}}, {{W+|fi|kaheli}}
* {{Z|fr}}: {{W+|fr|fou}}, {{W+|fr|insensé}}, {{W+|fr|avoir une araignée au plafond}}, {{W+|fr|chtarbé}}, {{W+|fr|dérangé}}, {{W+|fr|dingo}}, {{W+|fr|taré}}
* {{Z|gd}}: {{W-|gd|craicte}}, {{W-|gd|às a chiall}}
* {{Z|gl}}: {{W+|gl|tolo|n}}, {{W-|gl|doudo|n}}, {{W+|gl|louco|n}}
* {{Z|ka}}: {{W-|ka|შეშლილი}}, {{W-|ka|გადარეული}}, {{W-|ka|გიჟი}}, {{W-|ka|არანორმალური}}, {{W-|ka|შეურაცხადი}}
* {{Z|haw}}: {{W-|haw|pupule}}
* {{Z|mba}}: {{W-|mba|buang}}
* {{Z|hi}}: {{W+|hi|पागल}}, {{W+|hi|दीवाना}}
* {{Z|nl}}: {{W+|nl|zot}}, {{W+|nl|gek}}, {{W+|nl|getikt}}, {{W+|nl|gestoord}}, {{W+|nl|krankjorum}}, {{W+|nl|geschift}}
* {{Z|he}}: {{W-|he|מְטוֹרָף|tr=metoráf}}, {{W-|he|מְשׁוּגָע|tr=meshugá}}
* {{Z|ilo}}: {{W-|ilo|bagtit}}, {{W-|ilo|balla}}, {{W-|ilo|antutit}}, {{W-|ilo|ambagel}}, {{W-|ilo|agmuyot}}, {{W-|ilo|ammangaw}}, {{W-|ilo|kuwatit}}, {{W-|ilo|mauyong}}, {{W-|ilo|alloy}}, {{W-|ilo|attit}}, {{W-|ilo|talpeng}}, {{W-|ilo|appat}}, {{W-|ilo|allot}}, {{W-|ilo|tiwel}}, {{W-|ilo|alintaweng}}, {{W-|ilo|ambreng}}, {{W-|ilo|ullog}}, {{W-|ilo|murtit}}, {{W-|ilo|ketit}}, {{W-|ilo|bultik}}
* {{Z|en}}: {{W+|en|crazy}}, {{W+|en|insane}}, {{W+|en|mad}}, {{W+|en|demented}}
* {{Z|it}}: {{W+|it|pazzo}}, {{W+|it|matto}}, {{W+|it|folle}}
* {{Z|is}}: {{W+|is|brjálaður}}
* {{Z|ja}}: {{W-|ja|[[気]]が[[狂う|狂った]]|tr=きがくるった, ki ga kurutta}}, {{W-|ja|[[頭]]が[[おかしい]]|tr=あたまがおかしい, atama ga okashii}}
* {{Z|pam}}: {{W-|pam|murit}}, {{W-|pam|moret}}
* {{Z|ca}}: {{W+|ca|boig}}, {{W+|ca|foll}}
* {{Z|ky}}: {{W+|ky|жинди}}
* {{Z|ko}}: {{W+|ko|미치다}}
* {{Z|lou}}: {{W-|lou|fou|n}}, {{W-|lou|fòl|m}}, {{W-|lou|pa tou la}}, {{W-|lou|takala}}
* {{Z|lad}}: {{W-|lad|loko}}, {{W-|lad|budala}}
* {{Z|ltg}}: {{W-|ltg|troks}}
* {{Z|la}}: {{W-|la|dēlīrus}}, {{W-|la|vēcors}}, {{W+|la|insānus}}
* {{Z|lv}}: {{W-|lv|traks}}, {{W-|lv|ārprātīgs}}, {{W-|lv|vājprātīgs}}
* {{Z|lt}}: {{W-|lt|pamišęs}}
* {{Z|mk}}: {{W-|mk|луд}}
* {{Z|mg}}: {{W+|mg|adala}}
* {{Z|ms}}: {{W-|ms|gila}}
* {{Z|mt}}: {{W-|mt|miġnun|n}}
* {{Z|mi}}: {{W-|mi|pōrewharewha}}, {{W-|mi|heahea}}, {{W-|mi|rorirori}}, {{W-|mi|wairangi}}, {{W-|mi|pōrangi}}, {{W-|mi|pōrewarewa}}
* {{Z|hu}}: {{W+|hu|bolond}}, {{W+|hu|őrült}}, {{W+|hu|kattant}}
* {{Z|mn}}:
*: Kirîlî: {{W+|mn|солиотой}}, {{W+|mn|галзуу}}
* {{Z|no}}:
*: {{Z|nb}}: {{W-|nb|galen}}, {{W+|nb|gal}}, {{W+|nb|gæren}}, {{W+|nb|sinnsyk}}
* {{Z|oc}}: {{W+|oc|baug}}, {{W+|oc|fòl}}
* {{Z|uz}}: {{W+|uz|jinni}}
* {{Z|ps}}: {{W+|ps|لېونی|tr=lewanay}}, {{W+|ps|مجنون|tr=majnǔn}}
* {{Z|pdt}}: {{W-|pdt|domm}}, {{W-|pdt|errsennich}}
* {{Z|pl}}: {{W+|pl|szalony}}, {{W+|pl|obłąkany}}
* {{Z|pt}}: {{W+|pt|louco}}, {{W+|pt|maluco}}, {{W+|pt|doido}}
* {{Z|kk}}: {{W-|kk|ақылсыз}}, {{W-|kk|есалаң}}
* {{Z|ro}}: {{W+|ro|nebun|n}}, {{W+|ro|înnebunit}}, {{W+|ro|tulburat}}
* {{Z|ru}}: {{W-|ru|сумасше́дший}}, {{W-|ru|безу́мный}}, {{W+|ru|умалишённый}}, {{W-|ru|поме́шанный}}, {{W-|ru|чо́кнутый}}, {{W-|ru|спя́тивший}}, {{W-|ru|неадеква́тный|tr=neadɛkvátnyj}}, {{W-|ru|ошале́вший}}, {{W-|ru|ненорма́льный}}
* {{Z|ceb}}: {{W-|ceb|buang}}
* {{Z|ceb}}: {{W-|ceb|buang}}
* {{Z|srn}}: {{W-|srn|law}}
* {{Z|sh}}:
*: Kirîlî: {{W-|sh|лу̑д}}, {{W-|sh|бѐзӯман}}
*: Latînî: {{W+|sh|lȗd}}, {{W+|sh|bèzūman}}
* {{Z|sk}}: {{W-|sk|bláznivý}}, {{W-|sk|šialený}}
* {{Z|sl}}: {{W-|sl|blazen}}, {{W-|sl|trapast}}
* {{Z|so}}: {{W-|so|xuuf}}
* {{Z|es}}: {{W+|es|loco}}, {{W+|es|colgado}}, {{W+|es|sonado}}, {{W+|es|trastornado}}, {{W+|es|tronado}}, {{W+|es|gagá}}, {{W+|es|zumbado}}, {{W+|es|orate}}, {{W+|es|chalado}}, {{W+|es|tarambana}}
* {{Z|su}}: {{W-|su|gélo}}
* {{Z|sv}}: {{W+|sv|galen}}, {{W+|sv|tok}}, {{W+|sv|tokig}}, {{W+|sv|sinnessjuk}}, {{W+|sv|vansinnig}}
* {{Z|tg}}: {{W+|tg|девона}}, {{W-|tg|маҷнун}}, {{W-|tg|ҷиннӣ}}
* {{Z|tl}}: {{W+|tl|baliw}}, {{W+|tl|loko}}
* {{Z|th}}: {{W+|th|บ้า}}
* {{Z|tr}}: {{W+|tr|aptal}}, {{W+|tr|deli}}, {{W+|tr|manyak}}, {{W+|tr|kaçık}}, {{W+|tr|üşütük}}, {{W+|tr|din}}
* {{Z|tk}}: {{W-|tk|däli}}
* {{Z|ur}}: {{W-|ur|پاگل|tr=pāgal}}, {{W-|ur|دیوانہ|tr=dīvānā}}
* {{Z|uk}}: {{W-|uk|божеві́льний}}, {{W-|uk|безу́мний}}, {{W-|uk|шале́ний}}
* {{Z|vi}}: {{W+|vi|điên}}, {{W+|vi|điên rồ}}, {{W+|vi|cuồng}}, {{W+|vi|khùng}}
* {{Z|vo}}: {{W+|vo|lienetik}}
* {{Z|cy}}: {{W+|cy|gwallgof|}}, {{W+|cy|ynfyd}}, {{W+|cy|gorffwyll}}, {{W+|cy|lloerig}}
* {{Z|km}}: {{W+|km|ឆ្កួត}}, {{W-|km|លលាតែ}}, {{W-|km|ឧម្មត្តកៈ}}
* {{Z|el}}: {{W+|el|τρελός|n}}
*: {{Z|grc}}: {{W-|grc|μανιώδης}}
* {{Z|za}}: {{W-|za|bag}}
{{werger-bin}}
:{{werger çavkanî|en|crazy|beş=Ev qismê Wergerê|oldid=75756391}}
=== Navdêr 2 ===
{{navdêr|ku|z=m}}
# [[dên]], [[dêhn]], [[dihn]], [[bal]], [[mereq]], [[serinc]], [[enteres]]
#* {{jêder|ku|jêgirtin='''Dîna''' xwe bide nik bayê barîn ey birader/ Sitêra sibê îro ji şerqê li me da der/ Rewşen kirî Kurdistan, bi carek wê seraser/ Nîşana sibê ye, fereca xêrê ne dûr e.|Seydayê Tîrêj: Bilbilê Dilşadî}}
==== Tewandin ====
{{ku-tewîn-nav|mê}}
=== Lêker ===
{{lêker|ku}}
# {{guherto|dîtin}}
7xc769eljtaqj9r6d8uy417phluwg8t
6531961
6531960
2026-09-17T15:47:55Z
Wikihez
7344
/* Têkildar */ (bi [[MediaWiki:Gadget-AjaxEdit.js|AjaxEdit]]ê re)
6531961
wikitext
text/x-wiki
{{h-bnr|din}}
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-IPA}}
* {{deng|ku|LL-Q36163 (kmr)-Ebulfîda-dîn.wav|dever=Bidlîs}}
* {{ku-kîte}}
=== Navdêr ===
[[Wêne:Religions_4x5.png|thumb|250px|Sembolên hinek '''dîn''' û baweriyan.]]
{{navdêr|ku|z=n}}
# {{maneid|ku|bawerî}} [[bîr|Bîr]] û [[bawerî|baweriyên]] [[mirov]]an li ser hindê ka [[dinya]] û [[mirov]] çawan çêbûye û [[Xwedê]] yan [[xweda]] çawan in û [[têkilî|têkiliyên]] Wî yan wan û mirovan çawan in
#: {{hevmaneyên peyvê|ku|ayîn|bawerî|ol|peristkarî|diyanet}}
#: {{mk|ku|'''Dînê''' îslamê, '''dînê''' fileyan, '''dînê''' êzidiyan, '''dînê''' zerdeştiyan, '''dînê''' cihûyan.}}
#: {{mk|ku|'''Dîn''' û îman nan e, nan ku tune be, bende îro êzdî ye sibe misilman e}}
#* {{J:Mela Hisênê Bateyî:Mewlûda Kurdî|jêgirtin=Daye me mîrasa Qur'ana Kerîm</br>'''Dînê''' me kir kamil û ni'met-temam}}
==== Tewandin ====
{{ku-tewîn-nav|nêr}}
==== Bi alfabeyên din ====
* {{ku-ar|دين}}
==== Jê ====
{{kol3|ku|cure=Jê
|dîn bûn<crz:lêker>
|dîn kirin<crz:lêker>
|dînbûn<crz:navdêr>
|dînkirin<crz:navdêr>
|dînbûyî<crz:rengdêr>
|bêdîn
|bêdînî
|bidîn
|dîndar
|dîndarî
|dînî
|dînhişk
|dînnerm
|dînperist
|dînperistî
|dînperist
|dînperwer
}}
==== Herwiha binêre ====
{{kol3|ku
|dua
|ewliya
|lava
|lawij
|nimêj
|nivêj
|peristin
|peyamber
|pêxember
|rojî
|xweda
|xwedawend
|xwedawendî
|xwedayî
|Xwedê
|Yezdan
|yezdanî
|zekat
}}
==== Etîmolojî ====
{{navmiletî|ku}}, hevreha {{hv|fa|دین}}, {{hv|pal||dên|sc=Latn}}, {{hv|ae||deêna|sc=Latn}} û belkî {{hv|sa|धर्म|tr=dherme-}}. Peyva "dîn" bi hezaran sal e ku hem di zimanên îranî û hem jî yên samî de heye: avestayî "deêna", pehlewî "dên", akadî "dînu" (di wî zimanî de bi maneya "qanûn"ê). Bi ihtimalek mezin rehê peyvê samî ye lê ji akadî (ku zimanek samî ye) bi rêya zimanek din (belkî elamî) ketiye avestayî û ji avestayî jî ketiye pehlewî û taliyê dîsa li erebî vegeriye. Forma niha ya kurdî/farisî bi ihtimalek mezin bi rêya îslamê ji erebî hatiye wergirtin lê dîroka wê gelek dirêj û hêja ye. Bo peyvek din jî ku pêşî ji zimanên îranî ketiye zimanê erebî û paşî bi forma xwe ya erebî li zimanên îranî vegeriye, binere: {{q|ku|birc}}.
Idiaya [[Şablon:Beghyani|Beghyani]], ku dibêje "dîn" peyveke hindûewropî ye, bêbingeh e ji ber ku ji bilî zimanên îranî ev peyv di zimanên din yên hindûewropî de peyda nabe. Zimanên îranî jî ew ji zimanên rojhilatî wergirtiye.{{çp}} Gelek berî avestayî ew peyv di zimanên samî de - wek akadî - hatiye bikaranîn.{{çp}}
==== Werger ====
{{werger-ser}}
* {{Z|ab}}: {{W-|ab|адин|sc=Cyrl}}
* {{Z|ady}}: {{W-|ady|дин}}
* {{Z|sq}}: {{W+|sq|fe|m}}, {{W+|sq|din|n}}
* {{Z|de}}: {{W+|de|Religion|m}}
* {{Z|am}}: {{W-|am|ሃይማኖት}}
* {{Z|an}}: {{W-|an|relichión|m}}
* {{Z|rup}}: {{W-|rup|pisti|m}}
* {{Z|as}}: {{W-|as|ধৰ্ম}}
* {{Z|ast}}: {{W+|ast|relixón|m}}
* {{Z|aot}}: {{W-|aot|thorom}}
* {{Z|av}}: {{W-|av|дин|sc=Cyrl}}
* {{Z|ae}}: {{W-|ae|𐬛𐬀𐬉𐬥𐬁}}
* {{Z|az}}: {{W+|az|din}}, {{W+|az|məzhəb}}
* {{Z|eu}}: {{W-|eu|erlijio}}
* {{Z|ba}}: {{W-|ba|дин}}
* {{Z|be}}: {{W-|be|рэлі́гія|m|sc=Cyrl}}
* {{Z|bn}}: {{W+|bn|ধর্ম|sc=Beng}}, {{W-|bn|দ্বীন}}
* {{Z|bg}}: {{W-|bg|рели́гия|m}}, {{W-|bg|вероизповеда́ние|nt|sc=Cyrl}}
* {{Z|my}}: {{W+|my|အယူဝါဒ|sc=Mymr}}
* {{Z|bua}}: {{W-|bua|шажан}}
* {{Z|ce}}: {{W-|ce|дин}}
* {{Z|cs}}: {{W+|cs|náboženství|nt}}
* {{Z|chr}}: {{W-|chr|ᏗᏁᎸᏙᏗ}}
* {{Z|zh}}:
*: {{Z|yue}}: {{W-|yue|宗教|tr=zung1 gaau3}}
*: {{Z|dng}}: {{W-|dng|җё}}, {{W-|dng|дин}}
*: {{Z|hak}}: {{W-|hak|宗教|tr=chûng-kau}}
*: {{Z|cmn}}: {{W+|cmn|宗教|tr=zōngjiào}}
*: {{Z|cdo}}: {{W-|cdo|宗教|tr=cŭng-gáu}}
*: {{Z|nan}}: {{W+|nan|宗教|tr=chong-kàu}}
*: {{Z|wuu}}: {{W-|wuu|宗教}}
* {{Z|cv}}: {{W-|cv|тӗн|sc=Cyrl}}
* {{Z|da}}: {{W+|da|religion|g}}
* {{Z|dv}}: {{W-|dv|ދީން}}
* {{Z|id}}: {{W+|id|agama}}, {{W+|id|din}}
* {{Z|ar}}: {{W+|ar|دِين|n}}
*: {{Z|arz}}: {{W-|arz|دين|n|tr=dīn|sc=Arab}}
* {{Z|hy}}: {{W+|hy|կրոն|sc=Armn}}
* {{Z|eo}}: {{W-|eo|religio}}
* {{Z|et}}: {{W-|et|religioon}}
* {{Z|pal}}: {{W-|pal|dyn'|tr=dēn}}
* {{Z|fa}}: {{W+|fa|دین|tr=din}}, {{W+|fa|مذهب|tr=mazhab}}, {{W+|fa|کیش|tr=kiš}}, {{W+|fa|آیین|tr=âyin}}
* {{Z|fo}}: {{W-|fo|átrúnaður|n}}
* {{Z|fi}}: {{W+|fi|uskonto}}
* {{Z|fr}}: {{W+|fr|religion|m}}
* {{Z|fy}}: {{W+|fy|religy|g}}
* {{Z|gd}}: {{W-|gd|creideamh|n}}, {{W-|gd|diadhachd|m}}
* {{Z|gl}}: {{W+|gl|relixión|m}}
* {{Z|gu}}: {{W-|gu|ધર્મ}}
* {{Z|ka}}: {{W-|ka|რელიგია|sc=Geor}}, {{W-|ka|აღმსარებლობა}}, {{W-|ka|სარწმუნოება}}
* {{Z|gn}}: {{W-|gn|tupãrape}}
* {{Z|ht}}: {{W-|ht|relijyon}}
* {{Z|ha}}: {{W-|ha|addini}}
* {{Z|hi}}: {{W+|hi|धर्म|n}}, {{W+|hi|दीन|n}}, {{W-|hi|मज़हब|n|sc=Deva}}
* {{Z|hil}}: {{W-|hil|relihiyon}}
* {{Z|nl}}: {{W+|nl|godsdienst|n}}, {{W+|nl|geloofsovertuiging|m}}, {{W+|nl|religie|m}}
* {{Z|hrx}}: {{W-|hrx|Rëlighion|m}}
* {{Z|he}}: {{W-|he|דָּת|m|tr=dat}}
* {{Z|io}}: {{W+|io|religio}}
* {{Z|ang}}: {{W-|ang|ġelēafa|n}}, {{W-|ang|ǣfæstnes|m}}
* {{Z|en}}: {{W+|en|religion}}
* {{Z|inh}}: {{W-|inh|ди}}
* {{Z|ia}}: {{W-|ia|religion}}
* {{Z|ga}}: {{W-|ga|creideamh|n}}, {{W-|ga|reiligiún|m}}
* {{Z|it}}: {{W+|it|religione|m}}
* {{Z|is}}: {{W+|is|trúarbrögð|nt-p}}, {{W+|is|átrúnaður|n}}
* {{Z|ja}}: {{W+|ja|宗教|tr=しゅうきょう, shūkyō}}
* {{Z|kl}}: {{W-|kl|religioni}}
* {{Z|kn}}: {{W+|kn|ಧರ್ಮ|sc=Knda}}
* {{Z|pam}}: {{W-|pam|kasalpantayanan}}, {{W-|pam|kesalpantayanan}}
* {{Z|ca}}: {{W+|ca|religió|m}}
* {{Z|ks}}: {{W-|ks|مَزہَب}}
* {{Z|ky}}: {{W+|ky|дин|sc=Cyrl}}
* {{Z|si}}: {{W+|si|ආගම|sc=Sinh}}
* {{Z|kok}}: {{W-|kok|धर्म}}
* {{Z|ko}}: {{W+|ko|종교}}
* {{Z|lad}}:
*: Îbranî: {{W-|lad|ריליג׳ייון|m|sc=Hebr}}
*: Latînî: {{W-|lad|relijion|m|sc=Latn}}
* {{Z|la}}: {{W+|la|religiō|m}}
* {{Z|lv}}: {{W+|lv|reliģija|m}}
* {{Z|lo}}: {{W+|lo|ສາດສະໜາ}}
* {{Z|lez}}: {{W-|lez|дин}}
* {{Z|lt}}: {{W+|lt|relìgija|m}}
* {{Z|liv}}: {{W-|liv|relīgij}}
* {{Z|lb}}: {{W+|lb|Relioun|m}}
* {{Z|mk}}: {{W-|mk|религија}}
* {{Z|mg}}: {{W+|mg|fivavahana}}
* {{Z|ml}}: {{W+|ml|മതം|sc=Mlym}}
* {{Z|ms}}: {{W+|ms|agama}}, {{W+|ms|kepercayaan}}, {{W-|ms|anutan}}, {{W+|ms|din}}
* {{Z|mt}}: {{W-|mt|reliġjon}}
* {{Z|gv}}: {{W-|gv|craueeaght|m}}
* {{Z|mr}}: {{W+|mr|धर्म|n|sc=Deva}}
* {{Z|hu}}: {{W+|hu|vallás}}
* {{Z|mwl}}: {{W-|mwl|religion|m}}
* {{Z|mn}}:
*: Kirîlî: {{W+|mn|шашин}}
* {{Z|mos}}: {{W-|mos|wẽnd so-tũudum}}
* {{Z|nv}}: {{W-|nv|nahaghá}}
* {{Z|ne}}: {{W-|ne|धर्म}}
* {{Z|nrf}}: {{W-|nrf|r'ligion|m}}
* {{Z|no}}:
*: {{Z|nb}}: {{W+|nb|religion|n}}, {{W-|nb|trosretning|n|m}}
*: {{Z|nn}}: {{W+|nn|religion|n}}
* {{Z|oc}}: {{W+|oc|religion|m}}
* {{Z|or}}: {{W+|or|ଧର୍ମ}}
* {{Z|os}}: {{W-|os|дин|sc=Cyrl}}
* {{Z|ug}}: {{W+|ug|دىن|sc=ug-Arab}}
* {{Z|uz}}: {{W+|uz|din}}, {{W+|uz|mazhab}}
* {{Z|pi}}: {{W-|pi|धम्म|sc=Deva}}, {{W-|pi|dhamma|n}}
* {{Z|ps}}: {{W+|ps|مذهب|n|tr=mazháb}}, {{W+|ps|دين|n|tr=din}}
* {{Z|pms}}: {{W-|pms|religion|m}}
* {{Z|pih}}: {{W-|pih|rilijin}}
* {{Z|pdt}}: {{W-|pdt|Gloowe|n}}, {{W-|pdt|Reljoon|m}}
* {{Z|pl}}: {{W+|pl|religia|m}}
* {{Z|pt}}: {{W+|pt|religião|m}}
* Puncabî:
*: Şaxmuxî: {{W+|pa|دھرم|n|tr=dharm}}
*: Gurmuxî: {{W-|pa|ਧਰਮ|n}}
* {{Z|kk}}: {{W-|kk|дін}}
* {{Z|cop}}: {{W-|cop|ⲙⲉⲧⲁⲥⲉⲃⲏⲥ|m}}
* {{Z|rm}}: {{W-|rm|religiun|m}}
* {{Z|ro}}: {{W+|ro|religie|m}}
* {{Z|ru}}: {{W-|ru|рели́гия|m}}, {{W-|ru|ве́ра|m}}, {{W-|ru|вероиспове́дание|nt}}
* {{Z|rue}}: {{W-|rue|релі́ґія|m}}
* {{Z|sgs}}: {{W-|sgs|viera|m}}
* {{Z|sa}}: {{W+|sa|धर्म|n}}
* {{Z|sat}}: {{W-|sat|ᱫᱤᱱ}}, {{W-|sat|ᱫᱷᱚᱨᱚᱢ}}
* {{Z|sd}}: {{W-|sd|مذهب}}
* {{Z|sh}}:
*: Kirîlî: {{W-|sh|рѐлӣгија|m}}, {{W-|sh|ве̏ра|m}}, {{W-|sh|вје̏ра|m}}, {{W-|sh|вероѝспове̄ст|m}}, {{W-|sh|вјероѝсповије̄ст|m}}
*: Latînî: {{W+|sh|rèlīgija|m}}, {{W+|sh|vȅra|m}}, {{W+|sh|vjȅra|m}}, {{W+|sh|veroìspovēst|m}}, {{W+|sh|vjeroìspovijēst|m}}
* {{Z|syl}}: {{W-|syl|ꠗꠞ꠆ꠝꠧ}}
* {{Z|sco}}: {{W-|sco|releegion}}
* {{Z|sk}}: {{W+|sk|náboženstvo|nt}}, {{W-|sk|relígia|m}}
* {{Z|sl}}: {{W+|sl|vera|m}}, {{W-|sl|veroizpoved|m}}, {{W+|sl|religija|m}}
* {{Z|so}}: {{W-|so|diin}}
* Sorbî:
*: {{Z|dsb}}: {{W-|dsb|nabóžnina|m}}
*: {{Z|hsb}}: {{W-|hsb|nabožina|m}}
* {{Z|es}}: {{W+|es|religión|m}}
* {{Z|sw}}: {{W+|sw|dini}}
* {{Z|sv}}: {{W+|sv|religion|g}}
* {{Z|tg}}: {{W+|tg|дин|sc=Cyrl}}, {{W-|tg|мазҳаб}}
* {{Z|tl}}: {{W-|tl|relihiyon}}
* {{Z|ta}}: {{W+|ta|சமயம்}}, {{W+|ta|மதம்}}
* {{Z|th}}: {{W+|th|ศาสนา}}
* {{Z|te}}: {{W+|te|మతము}}
* {{Z|crh}}: {{W-|crh|din}}
* {{Z|tt}}: {{W+|tt|дин}}
* {{Z|tr}}: {{W+|tr|din}}, {{W+|tr|inanç}}, {{W+|tr|bağlanç}}, {{W+|tr|mezhep}}
* {{Z|tk}}: {{W-|tk|din}}
* {{Z|bo}}: {{W-|bo|ཆོས་ལུགས|sc=Tibt}}
* {{Z|tyv}}: {{W-|tyv|шажын}}
* {{Z|ur}}: {{W-|ur|دین|n|tr=dīn}}, {{W-|ur|دھرم|n|tr=dharm}}, {{W-|ur|مذہب|n|tr=mazhab|sc=ur-Arab}}
* {{Z|uk}}: {{W-|uk|релі́гія|m}}
* {{Z|vec}}: {{W-|vec|rełijon}}
* {{Z|vi}}: {{W+|vi|tôn giáo}}
* {{Z|wa}}: {{W+|wa|rilidjon|m}}
* {{Z|cy}}: {{W+|cy|crefydd}}
* {{Z|wuh}}: {{W-|wuh|quilek}}
* {{Z|km}}: {{W+|km|សាសនា}}
* {{Z|sah}}: {{W-|sah|итэҕэл}}
* {{Z|yi}}: {{W-|yi|רעליגיע|m}}
* {{Z|el}}: {{W+|el|θρησκεία|m}}
*: {{Z|grc}}: {{W-|grc|θρησκεία|m}}
{{werger-bin}}
:{{werger çavkanî|en|religion|beş=Ev qismê Wergerê|oldid=75709763}}
=== Rengdêr ===
{{ku-tewîn-rd|dîn}}
{{rengdêr|ku}}
# {{maneid|ku|bêhiş}} kesa/ê [[bêaqil]], [[bêhiş]], [[bêmejî]]
# [[har]], [[netebitî]]
==== Formên alternatîv ====
{{kol3|ku
|dhîn
|dîhn
}}
==== Hevmane ====
{{kol3|ku
|şeyda
|şêt
|dîwane
}}
==== Jê ====
{{kol3|ku
|dînatî
|dînî
|dînîtî
|dîntî
}}
==== Etîmolojî ====
Kurtkirina [[dêwîn]], hevreha farisî {{g|fa|دیوانه|tr=dîvanê}} ji {{paş|ku|dêw|w1=bi maneya [[cin]]|-ane}}. Bide ber [[mecnûn]].
==== Werger ====
{{werger-ser}}
* {{Z|sq}}: {{W+|sq|çmendur|cuda=i çmendur}}
* {{Z|de}}: {{W+|de|verrückt}}
* {{Z|am}}: {{W-|am|እብድ}}
* {{Z|az}}: {{W+|az|dəli}}
* {{Z|eu}}: {{W-|eu|zoro}}, {{W-|eu|ero}}
* {{Z|be}}: {{W-|be|шалёны}}, {{W-|be|бязу́мны}}
* {{Z|bn}}: {{W+|bn|দেওয়ানা}}, {{W+|bn|পাগল}}, {{W+|bn|মজনু}}
* {{Z|bg}}: {{W+|bg|луд}}, {{W-|bg|побъ́ркан}}
* {{Z|my}}: {{W+|my|နှမ်း}}
* {{Z|cs}}: {{W+|cs|šílený}}, {{W-|cs|pominutý}}, {{W-|cs|trhlý}}, {{W-|cs|potrhlý}}, {{W-|cs|zbavený rozumu}}, {{W-|cs|ztřeštěný}}, {{W+|cs|bláznivý}}
* {{Z|cic}}: {{W-|cic|haksi}}
* {{Z|zh}}:
*: {{Z|yue}}: {{W-|yue|癡線|tr=ci1 sin3}}, {{W-|yue|癲|tr=din1}}
*: {{Z|cmn}}: {{W+|cmn|瘋狂|tr=fēngkuáng}}, {{W+|cmn|瘋|tr=fēng}}
* {{Z|da}}: {{W-|da|skør}}, {{W+|da|sindssyg}}, {{W-|da|sindsforvirret}}, {{W+|da|gal}}, {{W-|da|gakgak}}
* {{Z|id}}: {{W+|id|gila}}
* {{Z|ar}}: {{W-|ar|مَجْنُون}}, {{W-|ar|مَخْبُول}}, {{W+|ar|مَسْعُور}}, {{W+|ar|أَحْمَق|tr=ʔaḥmaq}}
*: {{Z|ary}}: {{W-|ary|حمق|tr=ḥmaq}}, {{W-|ary|مسطي|tr=msaṭṭi}}, {{W-|ary|هبيل|tr=hbīl}}
* {{Z|hy}}: {{W+|hy|խենթ}}, {{W+|hy|խելագար}}, {{W+|hy|գիժ}}
* {{Z|eo}}: {{W+|eo|freneza}}
* {{Z|et}}: {{W+|et|hull}}
* {{Z|fa}}: {{W+|fa|دیوانه|tr=divâne}}, {{W+|fa|مجنون|tr=majnun}}
* {{Z|fi}}: {{W+|fi|hullu}}, {{W+|fi|mieletön}}, {{W+|fi|järjetön}}, {{W+|fi|kaheli}}
* {{Z|fr}}: {{W+|fr|fou}}, {{W+|fr|insensé}}, {{W+|fr|avoir une araignée au plafond}}, {{W+|fr|chtarbé}}, {{W+|fr|dérangé}}, {{W+|fr|dingo}}, {{W+|fr|taré}}
* {{Z|gd}}: {{W-|gd|craicte}}, {{W-|gd|às a chiall}}
* {{Z|gl}}: {{W+|gl|tolo|n}}, {{W-|gl|doudo|n}}, {{W+|gl|louco|n}}
* {{Z|ka}}: {{W-|ka|შეშლილი}}, {{W-|ka|გადარეული}}, {{W-|ka|გიჟი}}, {{W-|ka|არანორმალური}}, {{W-|ka|შეურაცხადი}}
* {{Z|haw}}: {{W-|haw|pupule}}
* {{Z|mba}}: {{W-|mba|buang}}
* {{Z|hi}}: {{W+|hi|पागल}}, {{W+|hi|दीवाना}}
* {{Z|nl}}: {{W+|nl|zot}}, {{W+|nl|gek}}, {{W+|nl|getikt}}, {{W+|nl|gestoord}}, {{W+|nl|krankjorum}}, {{W+|nl|geschift}}
* {{Z|he}}: {{W-|he|מְטוֹרָף|tr=metoráf}}, {{W-|he|מְשׁוּגָע|tr=meshugá}}
* {{Z|ilo}}: {{W-|ilo|bagtit}}, {{W-|ilo|balla}}, {{W-|ilo|antutit}}, {{W-|ilo|ambagel}}, {{W-|ilo|agmuyot}}, {{W-|ilo|ammangaw}}, {{W-|ilo|kuwatit}}, {{W-|ilo|mauyong}}, {{W-|ilo|alloy}}, {{W-|ilo|attit}}, {{W-|ilo|talpeng}}, {{W-|ilo|appat}}, {{W-|ilo|allot}}, {{W-|ilo|tiwel}}, {{W-|ilo|alintaweng}}, {{W-|ilo|ambreng}}, {{W-|ilo|ullog}}, {{W-|ilo|murtit}}, {{W-|ilo|ketit}}, {{W-|ilo|bultik}}
* {{Z|en}}: {{W+|en|crazy}}, {{W+|en|insane}}, {{W+|en|mad}}, {{W+|en|demented}}
* {{Z|it}}: {{W+|it|pazzo}}, {{W+|it|matto}}, {{W+|it|folle}}
* {{Z|is}}: {{W+|is|brjálaður}}
* {{Z|ja}}: {{W-|ja|[[気]]が[[狂う|狂った]]|tr=きがくるった, ki ga kurutta}}, {{W-|ja|[[頭]]が[[おかしい]]|tr=あたまがおかしい, atama ga okashii}}
* {{Z|pam}}: {{W-|pam|murit}}, {{W-|pam|moret}}
* {{Z|ca}}: {{W+|ca|boig}}, {{W+|ca|foll}}
* {{Z|ky}}: {{W+|ky|жинди}}
* {{Z|ko}}: {{W+|ko|미치다}}
* {{Z|lou}}: {{W-|lou|fou|n}}, {{W-|lou|fòl|m}}, {{W-|lou|pa tou la}}, {{W-|lou|takala}}
* {{Z|lad}}: {{W-|lad|loko}}, {{W-|lad|budala}}
* {{Z|ltg}}: {{W-|ltg|troks}}
* {{Z|la}}: {{W-|la|dēlīrus}}, {{W-|la|vēcors}}, {{W+|la|insānus}}
* {{Z|lv}}: {{W-|lv|traks}}, {{W-|lv|ārprātīgs}}, {{W-|lv|vājprātīgs}}
* {{Z|lt}}: {{W-|lt|pamišęs}}
* {{Z|mk}}: {{W-|mk|луд}}
* {{Z|mg}}: {{W+|mg|adala}}
* {{Z|ms}}: {{W-|ms|gila}}
* {{Z|mt}}: {{W-|mt|miġnun|n}}
* {{Z|mi}}: {{W-|mi|pōrewharewha}}, {{W-|mi|heahea}}, {{W-|mi|rorirori}}, {{W-|mi|wairangi}}, {{W-|mi|pōrangi}}, {{W-|mi|pōrewarewa}}
* {{Z|hu}}: {{W+|hu|bolond}}, {{W+|hu|őrült}}, {{W+|hu|kattant}}
* {{Z|mn}}:
*: Kirîlî: {{W+|mn|солиотой}}, {{W+|mn|галзуу}}
* {{Z|no}}:
*: {{Z|nb}}: {{W-|nb|galen}}, {{W+|nb|gal}}, {{W+|nb|gæren}}, {{W+|nb|sinnsyk}}
* {{Z|oc}}: {{W+|oc|baug}}, {{W+|oc|fòl}}
* {{Z|uz}}: {{W+|uz|jinni}}
* {{Z|ps}}: {{W+|ps|لېونی|tr=lewanay}}, {{W+|ps|مجنون|tr=majnǔn}}
* {{Z|pdt}}: {{W-|pdt|domm}}, {{W-|pdt|errsennich}}
* {{Z|pl}}: {{W+|pl|szalony}}, {{W+|pl|obłąkany}}
* {{Z|pt}}: {{W+|pt|louco}}, {{W+|pt|maluco}}, {{W+|pt|doido}}
* {{Z|kk}}: {{W-|kk|ақылсыз}}, {{W-|kk|есалаң}}
* {{Z|ro}}: {{W+|ro|nebun|n}}, {{W+|ro|înnebunit}}, {{W+|ro|tulburat}}
* {{Z|ru}}: {{W-|ru|сумасше́дший}}, {{W-|ru|безу́мный}}, {{W+|ru|умалишённый}}, {{W-|ru|поме́шанный}}, {{W-|ru|чо́кнутый}}, {{W-|ru|спя́тивший}}, {{W-|ru|неадеква́тный|tr=neadɛkvátnyj}}, {{W-|ru|ошале́вший}}, {{W-|ru|ненорма́льный}}
* {{Z|ceb}}: {{W-|ceb|buang}}
* {{Z|ceb}}: {{W-|ceb|buang}}
* {{Z|srn}}: {{W-|srn|law}}
* {{Z|sh}}:
*: Kirîlî: {{W-|sh|лу̑д}}, {{W-|sh|бѐзӯман}}
*: Latînî: {{W+|sh|lȗd}}, {{W+|sh|bèzūman}}
* {{Z|sk}}: {{W-|sk|bláznivý}}, {{W-|sk|šialený}}
* {{Z|sl}}: {{W-|sl|blazen}}, {{W-|sl|trapast}}
* {{Z|so}}: {{W-|so|xuuf}}
* {{Z|es}}: {{W+|es|loco}}, {{W+|es|colgado}}, {{W+|es|sonado}}, {{W+|es|trastornado}}, {{W+|es|tronado}}, {{W+|es|gagá}}, {{W+|es|zumbado}}, {{W+|es|orate}}, {{W+|es|chalado}}, {{W+|es|tarambana}}
* {{Z|su}}: {{W-|su|gélo}}
* {{Z|sv}}: {{W+|sv|galen}}, {{W+|sv|tok}}, {{W+|sv|tokig}}, {{W+|sv|sinnessjuk}}, {{W+|sv|vansinnig}}
* {{Z|tg}}: {{W+|tg|девона}}, {{W-|tg|маҷнун}}, {{W-|tg|ҷиннӣ}}
* {{Z|tl}}: {{W+|tl|baliw}}, {{W+|tl|loko}}
* {{Z|th}}: {{W+|th|บ้า}}
* {{Z|tr}}: {{W+|tr|aptal}}, {{W+|tr|deli}}, {{W+|tr|manyak}}, {{W+|tr|kaçık}}, {{W+|tr|üşütük}}, {{W+|tr|din}}
* {{Z|tk}}: {{W-|tk|däli}}
* {{Z|ur}}: {{W-|ur|پاگل|tr=pāgal}}, {{W-|ur|دیوانہ|tr=dīvānā}}
* {{Z|uk}}: {{W-|uk|божеві́льний}}, {{W-|uk|безу́мний}}, {{W-|uk|шале́ний}}
* {{Z|vi}}: {{W+|vi|điên}}, {{W+|vi|điên rồ}}, {{W+|vi|cuồng}}, {{W+|vi|khùng}}
* {{Z|vo}}: {{W+|vo|lienetik}}
* {{Z|cy}}: {{W+|cy|gwallgof|}}, {{W+|cy|ynfyd}}, {{W+|cy|gorffwyll}}, {{W+|cy|lloerig}}
* {{Z|km}}: {{W+|km|ឆ្កួត}}, {{W-|km|លលាតែ}}, {{W-|km|ឧម្មត្តកៈ}}
* {{Z|el}}: {{W+|el|τρελός|n}}
*: {{Z|grc}}: {{W-|grc|μανιώδης}}
* {{Z|za}}: {{W-|za|bag}}
{{werger-bin}}
:{{werger çavkanî|en|crazy|beş=Ev qismê Wergerê|oldid=75756391}}
=== Navdêr 2 ===
{{navdêr|ku|z=m}}
# [[dên]], [[dêhn]], [[dihn]], [[bal]], [[mereq]], [[serinc]], [[enteres]]
#* {{jêder|ku|jêgirtin='''Dîna''' xwe bide nik bayê barîn ey birader/ Sitêra sibê îro ji şerqê li me da der/ Rewşen kirî Kurdistan, bi carek wê seraser/ Nîşana sibê ye, fereca xêrê ne dûr e.|Seydayê Tîrêj: Bilbilê Dilşadî}}
==== Tewandin ====
{{ku-tewîn-nav|mê}}
=== Lêker ===
{{lêker|ku}}
# {{guherto|dîtin}}
no4m3lu6h727ztq3wvly05x3jqmlxzo
6531983
6531961
2026-09-17T16:41:09Z
Balyozbot
7867
[[Special:PermanentLink/6298023|Bot]]: rêzkirina beşan
6531983
wikitext
text/x-wiki
{{h-bnr|din}}
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-IPA}}
* {{deng|ku|LL-Q36163 (kmr)-Ebulfîda-dîn.wav|dever=Bidlîs}}
* {{ku-kîte}}
=== Navdêr ===
[[Wêne:Religions_4x5.png|thumb|250px|Sembolên hinek '''dîn''' û baweriyan.]]
{{navdêr|ku|z=n}}
# {{maneid|ku|bawerî}} [[bîr|Bîr]] û [[bawerî|baweriyên]] [[mirov]]an li ser hindê ka [[dinya]] û [[mirov]] çawan çêbûye û [[Xwedê]] yan [[xweda]] çawan in û [[têkilî|têkiliyên]] Wî yan wan û mirovan çawan in
#: {{hevmaneyên peyvê|ku|ayîn|bawerî|ol|peristkarî|diyanet}}
#: {{mk|ku|'''Dînê''' îslamê, '''dînê''' fileyan, '''dînê''' êzidiyan, '''dînê''' zerdeştiyan, '''dînê''' cihûyan.}}
#: {{mk|ku|'''Dîn''' û îman nan e, nan ku tune be, bende îro êzdî ye sibe misilman e}}
#* {{J:Mela Hisênê Bateyî:Mewlûda Kurdî|jêgirtin=Daye me mîrasa Qur'ana Kerîm</br>'''Dînê''' me kir kamil û ni'met-temam}}
==== Tewandin ====
{{ku-tewîn-nav|nêr}}
==== Bi alfabeyên din ====
* {{ku-ar|دين}}
==== Jê ====
{{kol3|ku|cure=Jê
|dîn bûn<crz:lêker>
|dîn kirin<crz:lêker>
|dînbûn<crz:navdêr>
|dînkirin<crz:navdêr>
|dînbûyî<crz:rengdêr>
|bêdîn
|bêdînî
|bidîn
|dîndar
|dîndarî
|dînî
|dînhişk
|dînnerm
|dînperist
|dînperistî
|dînperist
|dînperwer
}}
==== Etîmolojî ====
{{navmiletî|ku}}, hevreha {{hv|fa|دین}}, {{hv|pal||dên|sc=Latn}}, {{hv|ae||deêna|sc=Latn}} û belkî {{hv|sa|धर्म|tr=dherme-}}. Peyva "dîn" bi hezaran sal e ku hem di zimanên îranî û hem jî yên samî de heye: avestayî "deêna", pehlewî "dên", akadî "dînu" (di wî zimanî de bi maneya "qanûn"ê). Bi ihtimalek mezin rehê peyvê samî ye lê ji akadî (ku zimanek samî ye) bi rêya zimanek din (belkî elamî) ketiye avestayî û ji avestayî jî ketiye pehlewî û taliyê dîsa li erebî vegeriye. Forma niha ya kurdî/farisî bi ihtimalek mezin bi rêya îslamê ji erebî hatiye wergirtin lê dîroka wê gelek dirêj û hêja ye. Bo peyvek din jî ku pêşî ji zimanên îranî ketiye zimanê erebî û paşî bi forma xwe ya erebî li zimanên îranî vegeriye, binere: {{q|ku|birc}}.
Idiaya [[Şablon:Beghyani|Beghyani]], ku dibêje "dîn" peyveke hindûewropî ye, bêbingeh e ji ber ku ji bilî zimanên îranî ev peyv di zimanên din yên hindûewropî de peyda nabe. Zimanên îranî jî ew ji zimanên rojhilatî wergirtiye.{{çp}} Gelek berî avestayî ew peyv di zimanên samî de - wek akadî - hatiye bikaranîn.{{çp}}
==== Herwiha binêre ====
{{kol3|ku
|dua
|ewliya
|lava
|lawij
|nimêj
|nivêj
|peristin
|peyamber
|pêxember
|rojî
|xweda
|xwedawend
|xwedawendî
|xwedayî
|Xwedê
|Yezdan
|yezdanî
|zekat
}}
==== Werger ====
{{werger-ser}}
* {{Z|ab}}: {{W-|ab|адин|sc=Cyrl}}
* {{Z|ady}}: {{W-|ady|дин}}
* {{Z|sq}}: {{W+|sq|fe|m}}, {{W+|sq|din|n}}
* {{Z|de}}: {{W+|de|Religion|m}}
* {{Z|am}}: {{W-|am|ሃይማኖት}}
* {{Z|an}}: {{W-|an|relichión|m}}
* {{Z|rup}}: {{W-|rup|pisti|m}}
* {{Z|as}}: {{W-|as|ধৰ্ম}}
* {{Z|ast}}: {{W+|ast|relixón|m}}
* {{Z|aot}}: {{W-|aot|thorom}}
* {{Z|av}}: {{W-|av|дин|sc=Cyrl}}
* {{Z|ae}}: {{W-|ae|𐬛𐬀𐬉𐬥𐬁}}
* {{Z|az}}: {{W+|az|din}}, {{W+|az|məzhəb}}
* {{Z|eu}}: {{W-|eu|erlijio}}
* {{Z|ba}}: {{W-|ba|дин}}
* {{Z|be}}: {{W-|be|рэлі́гія|m|sc=Cyrl}}
* {{Z|bn}}: {{W+|bn|ধর্ম|sc=Beng}}, {{W-|bn|দ্বীন}}
* {{Z|bg}}: {{W-|bg|рели́гия|m}}, {{W-|bg|вероизповеда́ние|nt|sc=Cyrl}}
* {{Z|my}}: {{W+|my|အယူဝါဒ|sc=Mymr}}
* {{Z|bua}}: {{W-|bua|шажан}}
* {{Z|ce}}: {{W-|ce|дин}}
* {{Z|cs}}: {{W+|cs|náboženství|nt}}
* {{Z|chr}}: {{W-|chr|ᏗᏁᎸᏙᏗ}}
* {{Z|zh}}:
*: {{Z|yue}}: {{W-|yue|宗教|tr=zung1 gaau3}}
*: {{Z|dng}}: {{W-|dng|җё}}, {{W-|dng|дин}}
*: {{Z|hak}}: {{W-|hak|宗教|tr=chûng-kau}}
*: {{Z|cmn}}: {{W+|cmn|宗教|tr=zōngjiào}}
*: {{Z|cdo}}: {{W-|cdo|宗教|tr=cŭng-gáu}}
*: {{Z|nan}}: {{W+|nan|宗教|tr=chong-kàu}}
*: {{Z|wuu}}: {{W-|wuu|宗教}}
* {{Z|cv}}: {{W-|cv|тӗн|sc=Cyrl}}
* {{Z|da}}: {{W+|da|religion|g}}
* {{Z|dv}}: {{W-|dv|ދީން}}
* {{Z|id}}: {{W+|id|agama}}, {{W+|id|din}}
* {{Z|ar}}: {{W+|ar|دِين|n}}
*: {{Z|arz}}: {{W-|arz|دين|n|tr=dīn|sc=Arab}}
* {{Z|hy}}: {{W+|hy|կրոն|sc=Armn}}
* {{Z|eo}}: {{W-|eo|religio}}
* {{Z|et}}: {{W-|et|religioon}}
* {{Z|pal}}: {{W-|pal|dyn'|tr=dēn}}
* {{Z|fa}}: {{W+|fa|دین|tr=din}}, {{W+|fa|مذهب|tr=mazhab}}, {{W+|fa|کیش|tr=kiš}}, {{W+|fa|آیین|tr=âyin}}
* {{Z|fo}}: {{W-|fo|átrúnaður|n}}
* {{Z|fi}}: {{W+|fi|uskonto}}
* {{Z|fr}}: {{W+|fr|religion|m}}
* {{Z|fy}}: {{W+|fy|religy|g}}
* {{Z|gd}}: {{W-|gd|creideamh|n}}, {{W-|gd|diadhachd|m}}
* {{Z|gl}}: {{W+|gl|relixión|m}}
* {{Z|gu}}: {{W-|gu|ધર્મ}}
* {{Z|ka}}: {{W-|ka|რელიგია|sc=Geor}}, {{W-|ka|აღმსარებლობა}}, {{W-|ka|სარწმუნოება}}
* {{Z|gn}}: {{W-|gn|tupãrape}}
* {{Z|ht}}: {{W-|ht|relijyon}}
* {{Z|ha}}: {{W-|ha|addini}}
* {{Z|hi}}: {{W+|hi|धर्म|n}}, {{W+|hi|दीन|n}}, {{W-|hi|मज़हब|n|sc=Deva}}
* {{Z|hil}}: {{W-|hil|relihiyon}}
* {{Z|nl}}: {{W+|nl|godsdienst|n}}, {{W+|nl|geloofsovertuiging|m}}, {{W+|nl|religie|m}}
* {{Z|hrx}}: {{W-|hrx|Rëlighion|m}}
* {{Z|he}}: {{W-|he|דָּת|m|tr=dat}}
* {{Z|io}}: {{W+|io|religio}}
* {{Z|ang}}: {{W-|ang|ġelēafa|n}}, {{W-|ang|ǣfæstnes|m}}
* {{Z|en}}: {{W+|en|religion}}
* {{Z|inh}}: {{W-|inh|ди}}
* {{Z|ia}}: {{W-|ia|religion}}
* {{Z|ga}}: {{W-|ga|creideamh|n}}, {{W-|ga|reiligiún|m}}
* {{Z|it}}: {{W+|it|religione|m}}
* {{Z|is}}: {{W+|is|trúarbrögð|nt-p}}, {{W+|is|átrúnaður|n}}
* {{Z|ja}}: {{W+|ja|宗教|tr=しゅうきょう, shūkyō}}
* {{Z|kl}}: {{W-|kl|religioni}}
* {{Z|kn}}: {{W+|kn|ಧರ್ಮ|sc=Knda}}
* {{Z|pam}}: {{W-|pam|kasalpantayanan}}, {{W-|pam|kesalpantayanan}}
* {{Z|ca}}: {{W+|ca|religió|m}}
* {{Z|ks}}: {{W-|ks|مَزہَب}}
* {{Z|ky}}: {{W+|ky|дин|sc=Cyrl}}
* {{Z|si}}: {{W+|si|ආගම|sc=Sinh}}
* {{Z|kok}}: {{W-|kok|धर्म}}
* {{Z|ko}}: {{W+|ko|종교}}
* {{Z|lad}}:
*: Îbranî: {{W-|lad|ריליג׳ייון|m|sc=Hebr}}
*: Latînî: {{W-|lad|relijion|m|sc=Latn}}
* {{Z|la}}: {{W+|la|religiō|m}}
* {{Z|lv}}: {{W+|lv|reliģija|m}}
* {{Z|lo}}: {{W+|lo|ສາດສະໜາ}}
* {{Z|lez}}: {{W-|lez|дин}}
* {{Z|lt}}: {{W+|lt|relìgija|m}}
* {{Z|liv}}: {{W-|liv|relīgij}}
* {{Z|lb}}: {{W+|lb|Relioun|m}}
* {{Z|mk}}: {{W-|mk|религија}}
* {{Z|mg}}: {{W+|mg|fivavahana}}
* {{Z|ml}}: {{W+|ml|മതം|sc=Mlym}}
* {{Z|ms}}: {{W+|ms|agama}}, {{W+|ms|kepercayaan}}, {{W-|ms|anutan}}, {{W+|ms|din}}
* {{Z|mt}}: {{W-|mt|reliġjon}}
* {{Z|gv}}: {{W-|gv|craueeaght|m}}
* {{Z|mr}}: {{W+|mr|धर्म|n|sc=Deva}}
* {{Z|hu}}: {{W+|hu|vallás}}
* {{Z|mwl}}: {{W-|mwl|religion|m}}
* {{Z|mn}}:
*: Kirîlî: {{W+|mn|шашин}}
* {{Z|mos}}: {{W-|mos|wẽnd so-tũudum}}
* {{Z|nv}}: {{W-|nv|nahaghá}}
* {{Z|ne}}: {{W-|ne|धर्म}}
* {{Z|nrf}}: {{W-|nrf|r'ligion|m}}
* {{Z|no}}:
*: {{Z|nb}}: {{W+|nb|religion|n}}, {{W-|nb|trosretning|n|m}}
*: {{Z|nn}}: {{W+|nn|religion|n}}
* {{Z|oc}}: {{W+|oc|religion|m}}
* {{Z|or}}: {{W+|or|ଧର୍ମ}}
* {{Z|os}}: {{W-|os|дин|sc=Cyrl}}
* {{Z|ug}}: {{W+|ug|دىن|sc=ug-Arab}}
* {{Z|uz}}: {{W+|uz|din}}, {{W+|uz|mazhab}}
* {{Z|pi}}: {{W-|pi|धम्म|sc=Deva}}, {{W-|pi|dhamma|n}}
* {{Z|ps}}: {{W+|ps|مذهب|n|tr=mazháb}}, {{W+|ps|دين|n|tr=din}}
* {{Z|pms}}: {{W-|pms|religion|m}}
* {{Z|pih}}: {{W-|pih|rilijin}}
* {{Z|pdt}}: {{W-|pdt|Gloowe|n}}, {{W-|pdt|Reljoon|m}}
* {{Z|pl}}: {{W+|pl|religia|m}}
* {{Z|pt}}: {{W+|pt|religião|m}}
* Puncabî:
*: Şaxmuxî: {{W+|pa|دھرم|n|tr=dharm}}
*: Gurmuxî: {{W-|pa|ਧਰਮ|n}}
* {{Z|kk}}: {{W-|kk|дін}}
* {{Z|cop}}: {{W-|cop|ⲙⲉⲧⲁⲥⲉⲃⲏⲥ|m}}
* {{Z|rm}}: {{W-|rm|religiun|m}}
* {{Z|ro}}: {{W+|ro|religie|m}}
* {{Z|ru}}: {{W-|ru|рели́гия|m}}, {{W-|ru|ве́ра|m}}, {{W-|ru|вероиспове́дание|nt}}
* {{Z|rue}}: {{W-|rue|релі́ґія|m}}
* {{Z|sgs}}: {{W-|sgs|viera|m}}
* {{Z|sa}}: {{W+|sa|धर्म|n}}
* {{Z|sat}}: {{W-|sat|ᱫᱤᱱ}}, {{W-|sat|ᱫᱷᱚᱨᱚᱢ}}
* {{Z|sd}}: {{W-|sd|مذهب}}
* {{Z|sh}}:
*: Kirîlî: {{W-|sh|рѐлӣгија|m}}, {{W-|sh|ве̏ра|m}}, {{W-|sh|вје̏ра|m}}, {{W-|sh|вероѝспове̄ст|m}}, {{W-|sh|вјероѝсповије̄ст|m}}
*: Latînî: {{W+|sh|rèlīgija|m}}, {{W+|sh|vȅra|m}}, {{W+|sh|vjȅra|m}}, {{W+|sh|veroìspovēst|m}}, {{W+|sh|vjeroìspovijēst|m}}
* {{Z|syl}}: {{W-|syl|ꠗꠞ꠆ꠝꠧ}}
* {{Z|sco}}: {{W-|sco|releegion}}
* {{Z|sk}}: {{W+|sk|náboženstvo|nt}}, {{W-|sk|relígia|m}}
* {{Z|sl}}: {{W+|sl|vera|m}}, {{W-|sl|veroizpoved|m}}, {{W+|sl|religija|m}}
* {{Z|so}}: {{W-|so|diin}}
* Sorbî:
*: {{Z|dsb}}: {{W-|dsb|nabóžnina|m}}
*: {{Z|hsb}}: {{W-|hsb|nabožina|m}}
* {{Z|es}}: {{W+|es|religión|m}}
* {{Z|sw}}: {{W+|sw|dini}}
* {{Z|sv}}: {{W+|sv|religion|g}}
* {{Z|tg}}: {{W+|tg|дин|sc=Cyrl}}, {{W-|tg|мазҳаб}}
* {{Z|tl}}: {{W-|tl|relihiyon}}
* {{Z|ta}}: {{W+|ta|சமயம்}}, {{W+|ta|மதம்}}
* {{Z|th}}: {{W+|th|ศาสนา}}
* {{Z|te}}: {{W+|te|మతము}}
* {{Z|crh}}: {{W-|crh|din}}
* {{Z|tt}}: {{W+|tt|дин}}
* {{Z|tr}}: {{W+|tr|din}}, {{W+|tr|inanç}}, {{W+|tr|bağlanç}}, {{W+|tr|mezhep}}
* {{Z|tk}}: {{W-|tk|din}}
* {{Z|bo}}: {{W-|bo|ཆོས་ལུགས|sc=Tibt}}
* {{Z|tyv}}: {{W-|tyv|шажын}}
* {{Z|ur}}: {{W-|ur|دین|n|tr=dīn}}, {{W-|ur|دھرم|n|tr=dharm}}, {{W-|ur|مذہب|n|tr=mazhab|sc=ur-Arab}}
* {{Z|uk}}: {{W-|uk|релі́гія|m}}
* {{Z|vec}}: {{W-|vec|rełijon}}
* {{Z|vi}}: {{W+|vi|tôn giáo}}
* {{Z|wa}}: {{W+|wa|rilidjon|m}}
* {{Z|cy}}: {{W+|cy|crefydd}}
* {{Z|wuh}}: {{W-|wuh|quilek}}
* {{Z|km}}: {{W+|km|សាសនា}}
* {{Z|sah}}: {{W-|sah|итэҕэл}}
* {{Z|yi}}: {{W-|yi|רעליגיע|m}}
* {{Z|el}}: {{W+|el|θρησκεία|m}}
*: {{Z|grc}}: {{W-|grc|θρησκεία|m}}
{{werger-bin}}
:{{werger çavkanî|en|religion|beş=Ev qismê Wergerê|oldid=75709763}}
=== Rengdêr ===
{{ku-tewîn-rd|dîn}}
{{rengdêr|ku}}
# {{maneid|ku|bêhiş}} kesa/ê [[bêaqil]], [[bêhiş]], [[bêmejî]]
# [[har]], [[netebitî]]
==== Formên alternatîv ====
{{kol3|ku
|dhîn
|dîhn
}}
==== Hevmane ====
{{kol3|ku
|şeyda
|şêt
|dîwane
}}
==== Jê ====
{{kol3|ku
|dînatî
|dînî
|dînîtî
|dîntî
}}
==== Etîmolojî ====
Kurtkirina [[dêwîn]], hevreha farisî {{g|fa|دیوانه|tr=dîvanê}} ji {{paş|ku|dêw|w1=bi maneya [[cin]]|-ane}}. Bide ber [[mecnûn]].
==== Werger ====
{{werger-ser}}
* {{Z|sq}}: {{W+|sq|çmendur|cuda=i çmendur}}
* {{Z|de}}: {{W+|de|verrückt}}
* {{Z|am}}: {{W-|am|እብድ}}
* {{Z|az}}: {{W+|az|dəli}}
* {{Z|eu}}: {{W-|eu|zoro}}, {{W-|eu|ero}}
* {{Z|be}}: {{W-|be|шалёны}}, {{W-|be|бязу́мны}}
* {{Z|bn}}: {{W+|bn|দেওয়ানা}}, {{W+|bn|পাগল}}, {{W+|bn|মজনু}}
* {{Z|bg}}: {{W+|bg|луд}}, {{W-|bg|побъ́ркан}}
* {{Z|my}}: {{W+|my|နှမ်း}}
* {{Z|cs}}: {{W+|cs|šílený}}, {{W-|cs|pominutý}}, {{W-|cs|trhlý}}, {{W-|cs|potrhlý}}, {{W-|cs|zbavený rozumu}}, {{W-|cs|ztřeštěný}}, {{W+|cs|bláznivý}}
* {{Z|cic}}: {{W-|cic|haksi}}
* {{Z|zh}}:
*: {{Z|yue}}: {{W-|yue|癡線|tr=ci1 sin3}}, {{W-|yue|癲|tr=din1}}
*: {{Z|cmn}}: {{W+|cmn|瘋狂|tr=fēngkuáng}}, {{W+|cmn|瘋|tr=fēng}}
* {{Z|da}}: {{W-|da|skør}}, {{W+|da|sindssyg}}, {{W-|da|sindsforvirret}}, {{W+|da|gal}}, {{W-|da|gakgak}}
* {{Z|id}}: {{W+|id|gila}}
* {{Z|ar}}: {{W-|ar|مَجْنُون}}, {{W-|ar|مَخْبُول}}, {{W+|ar|مَسْعُور}}, {{W+|ar|أَحْمَق|tr=ʔaḥmaq}}
*: {{Z|ary}}: {{W-|ary|حمق|tr=ḥmaq}}, {{W-|ary|مسطي|tr=msaṭṭi}}, {{W-|ary|هبيل|tr=hbīl}}
* {{Z|hy}}: {{W+|hy|խենթ}}, {{W+|hy|խելագար}}, {{W+|hy|գիժ}}
* {{Z|eo}}: {{W+|eo|freneza}}
* {{Z|et}}: {{W+|et|hull}}
* {{Z|fa}}: {{W+|fa|دیوانه|tr=divâne}}, {{W+|fa|مجنون|tr=majnun}}
* {{Z|fi}}: {{W+|fi|hullu}}, {{W+|fi|mieletön}}, {{W+|fi|järjetön}}, {{W+|fi|kaheli}}
* {{Z|fr}}: {{W+|fr|fou}}, {{W+|fr|insensé}}, {{W+|fr|avoir une araignée au plafond}}, {{W+|fr|chtarbé}}, {{W+|fr|dérangé}}, {{W+|fr|dingo}}, {{W+|fr|taré}}
* {{Z|gd}}: {{W-|gd|craicte}}, {{W-|gd|às a chiall}}
* {{Z|gl}}: {{W+|gl|tolo|n}}, {{W-|gl|doudo|n}}, {{W+|gl|louco|n}}
* {{Z|ka}}: {{W-|ka|შეშლილი}}, {{W-|ka|გადარეული}}, {{W-|ka|გიჟი}}, {{W-|ka|არანორმალური}}, {{W-|ka|შეურაცხადი}}
* {{Z|haw}}: {{W-|haw|pupule}}
* {{Z|mba}}: {{W-|mba|buang}}
* {{Z|hi}}: {{W+|hi|पागल}}, {{W+|hi|दीवाना}}
* {{Z|nl}}: {{W+|nl|zot}}, {{W+|nl|gek}}, {{W+|nl|getikt}}, {{W+|nl|gestoord}}, {{W+|nl|krankjorum}}, {{W+|nl|geschift}}
* {{Z|he}}: {{W-|he|מְטוֹרָף|tr=metoráf}}, {{W-|he|מְשׁוּגָע|tr=meshugá}}
* {{Z|ilo}}: {{W-|ilo|bagtit}}, {{W-|ilo|balla}}, {{W-|ilo|antutit}}, {{W-|ilo|ambagel}}, {{W-|ilo|agmuyot}}, {{W-|ilo|ammangaw}}, {{W-|ilo|kuwatit}}, {{W-|ilo|mauyong}}, {{W-|ilo|alloy}}, {{W-|ilo|attit}}, {{W-|ilo|talpeng}}, {{W-|ilo|appat}}, {{W-|ilo|allot}}, {{W-|ilo|tiwel}}, {{W-|ilo|alintaweng}}, {{W-|ilo|ambreng}}, {{W-|ilo|ullog}}, {{W-|ilo|murtit}}, {{W-|ilo|ketit}}, {{W-|ilo|bultik}}
* {{Z|en}}: {{W+|en|crazy}}, {{W+|en|insane}}, {{W+|en|mad}}, {{W+|en|demented}}
* {{Z|it}}: {{W+|it|pazzo}}, {{W+|it|matto}}, {{W+|it|folle}}
* {{Z|is}}: {{W+|is|brjálaður}}
* {{Z|ja}}: {{W-|ja|[[気]]が[[狂う|狂った]]|tr=きがくるった, ki ga kurutta}}, {{W-|ja|[[頭]]が[[おかしい]]|tr=あたまがおかしい, atama ga okashii}}
* {{Z|pam}}: {{W-|pam|murit}}, {{W-|pam|moret}}
* {{Z|ca}}: {{W+|ca|boig}}, {{W+|ca|foll}}
* {{Z|ky}}: {{W+|ky|жинди}}
* {{Z|ko}}: {{W+|ko|미치다}}
* {{Z|lou}}: {{W-|lou|fou|n}}, {{W-|lou|fòl|m}}, {{W-|lou|pa tou la}}, {{W-|lou|takala}}
* {{Z|lad}}: {{W-|lad|loko}}, {{W-|lad|budala}}
* {{Z|ltg}}: {{W-|ltg|troks}}
* {{Z|la}}: {{W-|la|dēlīrus}}, {{W-|la|vēcors}}, {{W+|la|insānus}}
* {{Z|lv}}: {{W-|lv|traks}}, {{W-|lv|ārprātīgs}}, {{W-|lv|vājprātīgs}}
* {{Z|lt}}: {{W-|lt|pamišęs}}
* {{Z|mk}}: {{W-|mk|луд}}
* {{Z|mg}}: {{W+|mg|adala}}
* {{Z|ms}}: {{W-|ms|gila}}
* {{Z|mt}}: {{W-|mt|miġnun|n}}
* {{Z|mi}}: {{W-|mi|pōrewharewha}}, {{W-|mi|heahea}}, {{W-|mi|rorirori}}, {{W-|mi|wairangi}}, {{W-|mi|pōrangi}}, {{W-|mi|pōrewarewa}}
* {{Z|hu}}: {{W+|hu|bolond}}, {{W+|hu|őrült}}, {{W+|hu|kattant}}
* {{Z|mn}}:
*: Kirîlî: {{W+|mn|солиотой}}, {{W+|mn|галзуу}}
* {{Z|no}}:
*: {{Z|nb}}: {{W-|nb|galen}}, {{W+|nb|gal}}, {{W+|nb|gæren}}, {{W+|nb|sinnsyk}}
* {{Z|oc}}: {{W+|oc|baug}}, {{W+|oc|fòl}}
* {{Z|uz}}: {{W+|uz|jinni}}
* {{Z|ps}}: {{W+|ps|لېونی|tr=lewanay}}, {{W+|ps|مجنون|tr=majnǔn}}
* {{Z|pdt}}: {{W-|pdt|domm}}, {{W-|pdt|errsennich}}
* {{Z|pl}}: {{W+|pl|szalony}}, {{W+|pl|obłąkany}}
* {{Z|pt}}: {{W+|pt|louco}}, {{W+|pt|maluco}}, {{W+|pt|doido}}
* {{Z|kk}}: {{W-|kk|ақылсыз}}, {{W-|kk|есалаң}}
* {{Z|ro}}: {{W+|ro|nebun|n}}, {{W+|ro|înnebunit}}, {{W+|ro|tulburat}}
* {{Z|ru}}: {{W-|ru|сумасше́дший}}, {{W-|ru|безу́мный}}, {{W+|ru|умалишённый}}, {{W-|ru|поме́шанный}}, {{W-|ru|чо́кнутый}}, {{W-|ru|спя́тивший}}, {{W-|ru|неадеква́тный|tr=neadɛkvátnyj}}, {{W-|ru|ошале́вший}}, {{W-|ru|ненорма́льный}}
* {{Z|ceb}}: {{W-|ceb|buang}}
* {{Z|ceb}}: {{W-|ceb|buang}}
* {{Z|srn}}: {{W-|srn|law}}
* {{Z|sh}}:
*: Kirîlî: {{W-|sh|лу̑д}}, {{W-|sh|бѐзӯман}}
*: Latînî: {{W+|sh|lȗd}}, {{W+|sh|bèzūman}}
* {{Z|sk}}: {{W-|sk|bláznivý}}, {{W-|sk|šialený}}
* {{Z|sl}}: {{W-|sl|blazen}}, {{W-|sl|trapast}}
* {{Z|so}}: {{W-|so|xuuf}}
* {{Z|es}}: {{W+|es|loco}}, {{W+|es|colgado}}, {{W+|es|sonado}}, {{W+|es|trastornado}}, {{W+|es|tronado}}, {{W+|es|gagá}}, {{W+|es|zumbado}}, {{W+|es|orate}}, {{W+|es|chalado}}, {{W+|es|tarambana}}
* {{Z|su}}: {{W-|su|gélo}}
* {{Z|sv}}: {{W+|sv|galen}}, {{W+|sv|tok}}, {{W+|sv|tokig}}, {{W+|sv|sinnessjuk}}, {{W+|sv|vansinnig}}
* {{Z|tg}}: {{W+|tg|девона}}, {{W-|tg|маҷнун}}, {{W-|tg|ҷиннӣ}}
* {{Z|tl}}: {{W+|tl|baliw}}, {{W+|tl|loko}}
* {{Z|th}}: {{W+|th|บ้า}}
* {{Z|tr}}: {{W+|tr|aptal}}, {{W+|tr|deli}}, {{W+|tr|manyak}}, {{W+|tr|kaçık}}, {{W+|tr|üşütük}}, {{W+|tr|din}}
* {{Z|tk}}: {{W-|tk|däli}}
* {{Z|ur}}: {{W-|ur|پاگل|tr=pāgal}}, {{W-|ur|دیوانہ|tr=dīvānā}}
* {{Z|uk}}: {{W-|uk|божеві́льний}}, {{W-|uk|безу́мний}}, {{W-|uk|шале́ний}}
* {{Z|vi}}: {{W+|vi|điên}}, {{W+|vi|điên rồ}}, {{W+|vi|cuồng}}, {{W+|vi|khùng}}
* {{Z|vo}}: {{W+|vo|lienetik}}
* {{Z|cy}}: {{W+|cy|gwallgof|}}, {{W+|cy|ynfyd}}, {{W+|cy|gorffwyll}}, {{W+|cy|lloerig}}
* {{Z|km}}: {{W+|km|ឆ្កួត}}, {{W-|km|លលាតែ}}, {{W-|km|ឧម្មត្តកៈ}}
* {{Z|el}}: {{W+|el|τρελός|n}}
*: {{Z|grc}}: {{W-|grc|μανιώδης}}
* {{Z|za}}: {{W-|za|bag}}
{{werger-bin}}
:{{werger çavkanî|en|crazy|beş=Ev qismê Wergerê|oldid=75756391}}
=== Navdêr 2 ===
{{navdêr|ku|z=m}}
# [[dên]], [[dêhn]], [[dihn]], [[bal]], [[mereq]], [[serinc]], [[enteres]]
#* {{jêder|ku|jêgirtin='''Dîna''' xwe bide nik bayê barîn ey birader/ Sitêra sibê îro ji şerqê li me da der/ Rewşen kirî Kurdistan, bi carek wê seraser/ Nîşana sibê ye, fereca xêrê ne dûr e.|Seydayê Tîrêj: Bilbilê Dilşadî}}
==== Tewandin ====
{{ku-tewîn-nav|mê}}
=== Lêker ===
{{lêker|ku}}
# {{guherto|dîtin}}
5npdacitv9kxkmun6eqbviubefzoutv
hunermend
0
4880
6531926
6428517
2026-09-17T13:14:06Z
Wikihez
7344
Kurmancî/Navdêr: mane hate guhartin, beşa «Etîmolojî» hate nûkirin (DefSideBoxes)
6531926
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Navdêr ===
[[Wêne:Artist_at_Porthstinian,_Penmaen_Dewi,_Sir_Benfro_(Pembrokeshire),_Cymru_06.jpg|thumb|250px|Hunermendek]]
{{navdêr|ku|z=mn}}
# Kesa/ê [[karûbar]]ên [[huner]]î dike bo nimûne [[stranbêj]], [[nîgarkêş]] û [[aktor]].
==== Tewandin ====
{{ku-tewîn-nav|mê û nêr}}
==== Bi alfabeyên din ====
* {{ku-ar|هونەرمەند}}
==== Formên alternatîv ====
* [[hinerkar]]
* [[hinermend]]
* [[hunerkar]]
==== Hevmane ====
* [[sin'etkar]]
==== Jê ====
* [[hunermendî]]
==== Etîmolojî ====
{{etîmon|ku|:qertaf|huner|-mend|dar=1|metn=++}}
==== Werger ====
{{werger-ser}}
* {{Z|af}}: {{W-|af|kunstenaar}}
* {{Z|sq}}: {{W+|sq|artist|n}}, {{W+|sq|artiste|m}}, {{W+|sq|piktor|n}}, {{W+|sq|piktore|m}}
* {{Z|de}}: {{W+|de|Künstler|n}}, {{W+|de|Künstlerin|m}}, {{W+|de|Maler|n}}, {{W+|de|Malerin|m}}
* {{Z|ast}}: {{W-|ast|artista|n|m}}
* {{Z|az}}: {{W+|az|rəssam}}
* {{Z|eu}}: {{W-|eu|artista}}
* {{Z|ba}}: {{W-|ba|рәссам}}
* {{Z|be}}: {{W-|be|маста́к|n}}, {{W-|be|маста́чка|m}}, {{W-|be|маля́р|n}}, {{W-|be|маля́рка|m}}, {{W-|be|арты́ст|n}}, {{W-|be|арты́стка|m}}
* {{Z|bn}}: {{W+|bn|শিল্পী}}
* {{Z|bg}}: {{W+|bg|худо́жник}}, {{W-|bg|худо́жничка|m}}, {{W+|bg|арти́ст|n}}, {{W+|bg|арти́стка|m}}
* {{Z|my}}: {{W+|my|ပန်းချီဆရာ}}
* {{Z|ce}}: {{W-|ce|суртдуьллург}}
* {{Z|cs}}: {{W+|cs|umělec|n}}, {{W+|cs|umělkyně|m}}, {{W+|cs|malíř|n}}, {{W+|cs|malířka|m}}
* {{Z|zh}}:
*: {{Z|cmn}}: {{W-|cmn|藝術家|tr=yìshùjiā}}, {{W+|cmn|畫家|tr=huàjiā}}, {{W-|cmn|美術家|tr=měishùjiā}}
* {{Z|da}}: {{W+|da|kunstner|g}}
* {{Z|ovd}}: {{W-|ovd|konstnär|n}}
* {{Z|id}}: {{W+|id|seniman}}, {{W+|id|artis}}
* {{Z|ar}}: {{W-|ar|فَنَّان|n}}, {{W-|ar|فَنَّانَة|m}}; {{W-|ar|رَسَّام|n}}, {{W-|ar|رَسَّامَة|m}}
*: {{Z|arz}}: {{W-|arz|فنان|n|tr=fannān}}
* {{Z|hy}}: {{W+|hy|նկարիչ}}
* {{Z|eo}}: {{W-|eo|artisto}}
* {{Z|et}}: {{W+|et|kunstnik}}, {{W-|et|taidur}}
* {{Z|fa}}: {{W+|fa|هنرمند|tr=honarmand}}, {{W+|fa|آرتیست|tr=ârtist}}, {{W+|fa|نقاش|tr=naqqâš}}, {{W+|fa|نگارگر|tr=negârgar}}
* {{Z|fo}}: {{W-|fo|listamaður|n}}, {{W-|fo|listakona|m}}
* {{Z|fi}}: {{W+|fi|taiteilija}}, {{W+|fi|artisti}}
* {{Z|fr}}: {{W+|fr|artiste|n|m}}, {{W+|fr|peintre|n|m}}, {{W+|fr|peintresse|m}}
* {{Z|gd}}: {{W-|gd|dealbhadair|n}}, {{W-|gd|dealbhaiche|n}}, {{W-|gd|ealantair|n}}, {{W-|gd|neach-ealain|n}}
* {{Z|gl}}: {{W+|gl|artista|n|m}}
* {{Z|ka}}: {{W-|ka|მხატვარი}}, {{W-|ka|ხელოვანი}}
* {{Z|hi}}: {{W+|hi|कलाकार|n}}, {{W-|hi|फ़नकार|n}}, {{W+|hi|शिल्पी|n}}, {{W+|hi|हुनरमंद|n}}, {{W+|hi|आर्टिस्ट}}, {{W+|hi|चित्रकार|n}}
* {{Z|nl}}: {{W+|nl|kunstenaar|n}}, {{W+|nl|kunstenares|m}}
* {{Z|hrx}}: {{W-|hrx|Kinstler|n}}, {{W-|hrx|Kinstlerin|m}}
* {{Z|he}}: {{W+|he|אֳמָן|n|tr=omán}}
* {{Z|ang}}: {{W-|ang|cræftiga|n}}
* {{Z|en}}: {{W+|en|artist}}
* {{Z|ga}}: {{W-|ga|ealaíontóir|n}}
* {{Z|it}}: {{W+|it|artista|n|m}}
* {{Z|is}}: {{W-|is|listamaður|n}}, {{W-|is|listakona|m}}
* {{Z|ja}}: {{W+|ja|芸術家|tr=げいじゅつか, geijutsuka}}, {{W+|ja|画家|tr=がか, gaka}}, {{W-|ja|アーティスト|tr=ātisuto}}
* {{Z|kl}}: {{W-|kl|eqqumiitsuliortoq}}
* {{Z|ca}}: {{W+|ca|artista|n|m}}
* {{Z|ky}}: {{W+|ky|художник}}, {{W-|ky|көркөмөнөрчү}}
* {{Z|ko}}: {{W+|ko|화가}}
* {{Z|la}}: {{W-|la|artifex|m}}
* {{Z|lv}}: {{W-|lv|mākslinieks|n}}, {{W-|lv|māksliniece|m}}
* {{Z|lo}}: {{W+|lo|ສິນລະປິນ}}, {{W-|lo|ຈິດຕະກອນ}}
* {{Z|lt}}: {{W-|lt|menininkas|n}}, {{W-|lt|artistas|n}}
* {{Z|lb}}: {{W-|lb|Kënschtler|n}}, {{W-|lb|Kënschtlerin|m}}
* {{Z|mk}}: {{W-|mk|уметник|n}}, {{W-|mk|уметничка|m}}, {{W-|mk|уметница|m}}
* {{Z|ms}}: {{W-|ms|seniman}}, {{W-|ms|artis}}
* {{Z|mt}}: {{W-|mt|artist|n}}
* {{Z|mi}}: {{W-|mi|ringatoi}}, {{W-|mi|rehetoi}}
* {{Z|hu}}: {{W+|hu|művész}}
* {{Z|mn}}:
*: Kirîlî: {{W+|mn|зураач}}, {{W+|mn|жүжигчин}}
* {{Z|nrf}}: {{W-|nrf|artiste|n|m}}
* {{Z|no}}:
*: {{Z|nb}}: {{W+|nb|kunstner|n}}
*: {{Z|nn}}: {{W-|nn|kunstnar|n}}
* {{Z|oj}}: {{W-|oj|mazinibii'igewinini}}
* {{Z|oc}}: {{W+|oc|artista|n|m}}
* {{Z|ug}}: {{W-|ug|سەنئەتكار}}, {{W-|ug|رەسسام}}
* {{Z|uz}}: {{W+|uz|rassom}}, {{W-|uz|xudojnik}}, {{W-|uz|sanatkor}}
* {{Z|pl}}: {{W+|pl|artysta|n}}, {{W+|pl|artystka|m}}, {{W+|pl|twórca|n}}, {{W+|pl|twórczyni|m}}, {{W+|pl|malarz|n}}, {{W+|pl|malarka|m}}
* {{Z|pt}}: {{W+|pt|artista|n|m}}
* {{Z|kk}}: {{W-|kk|суретші}}, {{W-|kk|әртіс}}
* {{Z|rm}}: {{W-|rm|artist|n}}, {{W-|rm|artista|m}}
* {{Z|ro}}: {{W+|ro|artist|n}}, {{W+|ro|artistă|m}}
* {{Z|ru}}: {{W+|ru|худо́жник|n}}, {{W+|ru|худо́жница|m}}, {{W+|ru|арти́ст|n}}, {{W+|ru|арти́стка|m}}
* {{Z|se}}: {{W-|se|dáiddár}}
* {{Z|sh}}:
*: Kirîlî: {{W-|sh|у̀метнӣк|n}}, {{W-|sh|у̀мјетнӣк|n}}, {{W-|sh|у̀метница|m}}, {{W-|sh|у̀мјетница|m}}
*: Latînî: {{W+|sh|ùmetnīk|n}}, {{W+|sh|ùmjetnik|n}}, {{W-|sh|ùmetnīca|m}}, {{W+|sh|ùmjetnica|m}}
* {{Z|sk}}: {{W-|sk|umelec|n}}, {{W-|sk|umelkyňa|m}}, {{W+|sk|maliar|n}}, {{W-|sk|maliarka|m}}
* {{Z|sl}}: {{W+|sl|umetnik|n}}, {{W-|sl|umetnica|m}}, {{W+|sl|slikar|n}}, {{W-|sl|slikarka|m}}
* {{Z|es}}: {{W+|es|artista|n|m}}
* {{Z|sw}}: {{W+|sw|msanii}}, {{W-|sw|mwanasanaa}}
* {{Z|sv}}: {{W+|sv|artist|n}}, {{W+|sv|konstnär|g}}
* {{Z|tg}}: {{W-|tg|ҳунарманд}}, {{W+|tg|рассом}}, {{W-|tg|наққош}}, {{W-|tg|суратгар}}
* {{Z|tl}}: {{W-|tl|dalubsining}}
* {{Z|th}}: {{W+|th|ศิลปิน}}, {{W-|th|จิตรกร}}
* {{Z|te}}: {{W+|te|కళాకారుడు}}
* {{Z|tt}}: {{W+|tt|рәссам}}
* {{Z|tr}}: {{W+|tr|sanatçı}}, {{W-|tr|dörütçü}}, {{W-|tr|dörütmen}}, {{W+|tr|ressam}}
* {{Z|tk}}: {{W-|tk|suratçy}}, {{W-|tk|hudožnik}}
* {{Z|bo}}: {{W-|bo|རི་མོ་མཁས་པ}}
* {{Z|ur}}: {{W-|ur|فنکار|n|tr=fankār}}, {{W-|ur|فنکارہ|m|tr=fankārā}}
* {{Z|uk}}: {{W-|uk|худо́жник|n}}, {{W-|uk|худо́жниця|m}}, {{W-|uk|ма́ля́р|n}}, {{W-|uk|ма́ля́рка|m}}, {{W+|uk|арти́ст|n}}, {{W+|uk|арти́стка|m}}, {{W-|uk|мите́ць|n}}, {{W-|uk|мистки́ня|m}}
* {{Z|vi}}: {{W+|vi|họa sĩ}}
* {{Z|vo}}: {{W+|vo|lekanan}}, {{W-|vo|hilekanan}}, {{W-|vo|jilekanan}}
* {{Z|cy}}: {{W-|cy|arlunydd|n}}
* {{Z|km}}: {{W+|km|សិល្បករ}}, {{W+|km|វិចិត្រករ}}
* {{Z|yi}}: {{W-|yi|קינסטלער|n}}, {{W-|yi|קינסטלערין|m}}, {{W-|yi|אַרטיסט|n}}, {{W-|yi|אַרטיסטקע|m}}
* {{Z|el}}: {{W+|el|καλλιτέχνης|n}}, {{W+|el|δημιουργός|n|m}}
{{werger-bin}}
:{{werger çavkanî|en|artist|beş=Ev qismê Wergerê|oldid=76042469}}
siui4o2sk91nioz37t0i80wsjoyleao
mast
0
5459
6531996
6463072
2026-09-17T19:20:15Z
Wikihez
7344
Kurmancî/Navdêr: mane hate guhartin, ±nimûneya bikaranînê bi amûra « [[Mediawiki:Gadget-wikt.add-examples|Nimûne zêde bike]] » (v2.0) (DefSideBoxes)
6531996
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-IPA}}
* {{deng|ku|LL-Q36163 (kmr)-Ebulfîda-mast.wav|dever=Bidlîs}}
* {{kîte|ku|mast}}
=== Navdêr ===
[[Wêne:Cacik-1.jpg|thumb|250px|[[xiyarmast]]]]
{{navdêr|ku|z=n}}
# {{f|ku|xwarin}} [[şîr|Şîr]]ê bi [[hêvên]]î [[meyandin|meyandî]] û [[tîr]] kirî.
#* {{J:Firat Cewerî:Girtî:1986|jêgirtin=Pîrka wî satila '''mêst''', textê firaxan û selika nên nîşanî wî da û got: - Tu ji xwe re hinek '''mast''' têxe tacikekê û bixwe {{...}}}}
==== Tewandin ====
{{ku-tewîn-nav|nêr}}
==== Bi alfabeyên din ====
* {{ku-ar|ماست}}
==== Hevmane ====
{{kol3|ku
|yogurt
}}
==== Jê ====
{{kol3|ku
|mastkirî<crz:rengdêr>
}}
==== Etîmolojî ====
Ji {{mîr|ine-pro|ku|*meu-k-|w=[[hilû]], [[tehîsok]], [[hilîsok]], [[şemitok]]}} ku herwiha serkaniya peyva [[masî]] ya kurdî û [[moist]] ya îngilîzî ye.
* {{ji|ira-pro||-}}: *'''mad'''<sup>2</sup>(", [[meyîn]]")
:{{pal}}: '''m's'''("[[meyînn]]; hişk bûn")
:{{xpr}}: '''m's'''("[[meyîn]]--, hişk bûn")
:{{fa}}: '''māsīdan''' ("[[meyîn]]--"), '''māst''' ("mast")
:{{bal}}: '''mast''' ("mast")
:[[erdestanî]]: '''maske''' ("[[rûn]]") ... {{ir}}
:{{kmr}}: '''meyîn''' ("[[meyîn]]"), '''mast''' ("mast")
:{{kmr}}: '''mast''' ("mast")
:{{zza}}: '''mast''' ("mast")
{{sa}}: '''mástu'''([[sertûk]], [[qeymax]]")
;{{biçûk|Çavkanî:
* {{Cheung|r=253}}
* {{Horn|r=214}}
}}
==== Bide ber ====
{{kol3|ku
|dew
|firû
|nîvişk
|sertûk
|şîr
|tûk
|şojî
|penîr
|sûrik
|çolik
|zêç
}}
==== Werger ====
{{werger-ser}}
* {{Z|ab}}: {{W-|ab|ахарҵәы|sc=Cyrl}}
* {{Z|af}}: {{W-|af|jogurt}}
* {{Z|sq}}: {{W+|sq|kos|n}}, {{W-|sq|jogurt|n}}
* {{Z|de}}: {{W+|de|Joghurt|n}}
* {{Z|am}}: {{W-|am|እርጎ}}
* {{Z|rup}}: {{W-|rup|mãrcat|nt}}
* {{Z|as}}: {{W-|as|দৈ}}
* {{Z|ast}}: {{W-|ast|yogur|n}}
* {{Z|az}}: {{W+|az|qatıq}}, {{W+|az|yoğurt}}, {{W-|az|yoqurt}}
* {{Z|eu}}: {{W+|eu|jogurt}}
* {{Z|be}}: {{W-|be|ёгурт|n|sc=Cyrl}}
* {{Z|bn}}: {{W+|bn|দই}}
* {{Z|br}}: {{W-|br|yaourt|n}}
* {{Z|bg}}: {{W-|bg|ки́село мля́ко|nt}}
* {{Z|my}}: {{W+|my|ဒိန်ချဉ်|sc=Mymr}}
* {{Z|bua}}: {{W-|bua|тараг}}
* {{Z|cs}}: {{W+|cs|jogurt|n}}
* {{Z|chr}}: {{W-|chr|ᏧᏂᎿᏦᏯᏍᏔᏅ}}
* {{Z|zh}}:
*: {{Z|yue}}: {{W-|yue|乳酪|tr=jyu5 lok3}}
*: {{Z|hak}}: {{W-|hak|優酪乳|tr=yulólù}}
*: {{Z|cmn}}: {{W+|cmn|酸奶|tr=suānnǎi}}, {{W-|cmn|酸牛奶|tr=suānniúnǎi}}, {{W-|cmn|酸凝酪|tr=suānnínglào, suānníngluò}}, {{W+|cmn|優酪乳|tr=yōulàorǔ, yōuluòrǔ}}, {{W-|cmn|發酵乳}}, {{W+|cmn|乳酪|tr=rǔlào}}, {{W-|cmn|发酵乳|tr=fājiàorǔ, fāxiàorǔ}}, {{W+|cmn|優格|tr=yōugé}}
*: {{Z|nan}}: {{W-|nan|酸奶|tr=sng-lin / sng-ni / suiⁿ-leng}}
*: {{Z|wuu}}: {{W-|wuu|酸奶}}
* {{Z|da}}: {{W-|da|jogurt|g}}, {{W+|da|yoghurt|g}}
* {{Z|id}}: {{W+|id|yoghurt}}
* {{Z|ar}}: {{W+|ar|لَبَن|n}}, {{W-|ar|لَبَن زَبَادِيّ|n}}, {{W-|ar|لَبَن رَائِب|n}}, {{W+|ar|رَوْب|n}}, {{W-|ar|رَوْبَة|m}}, {{W-|ar|مَاسْت|n}}
*: {{Z|arz}}: {{W-|arz|زبادي|n|tr=zabadi|sc=Arab}}
*: {{Z|afb}}: {{W-|afb|روب|tr=rōb}}
*: {{Z|acw}}: {{W-|acw|زبادي|n|tr=zabādi}}
*: {{Z|ary}}: {{W-|ary|دانون|n|tr=danon|sc=Arab}}
* {{Z|hy}}: {{W+|hy|յոգուրտ}}
* {{Z|eo}}: {{W-|eo|jogurto}}, {{W-|eo|jahurto}}
* {{Z|et}}: {{W+|et|jogurt}}
* {{Z|fa}}: {{W+|fa|ماست|tr=mâst}}, {{W+|fa|جغرات|tr=joğrât}}
* {{Z|fo}}: {{W-|fo|jogurt|nt}}
* {{Z|fi}}: {{W+|fi|jogurtti}}
* {{Z|fr}}: {{W+|fr|yaourt|n}}, {{W+|fr|yogourt|n}}
* {{Z|gd}}: {{W-|gd|iogart|n}}
* {{Z|gag}}: {{W-|gag|yuurt}}
* {{Z|gl}}: {{W+|gl|iogur|n}}
* {{Z|gu}}: {{W-|gu|દહીં}}
* {{Z|ka}}: {{W-|ka|იოგურტი|sc=Geor}}, {{W+|ka|მაწონი|sc=Geor}}
* {{Z|hi}}: {{W+|hi|दही|n}}
* {{Z|nl}}: {{W+|nl|yoghurt|n}}
* {{Z|he}}: {{W-|he|יוֹגוּרְט|n|tr=yogurt}}
* {{Z|io}}: {{W+|io|yogurto}}
* {{Z|en}}: {{W+|en|mast}}
* {{Z|ia}}: {{W-|ia|yogurt}}
* {{Z|ga}}: {{W-|ga|iógart|n}}
* {{Z|it}}: {{W+|it|yogurt|n}}, {{W+|it|iogurt|n}}
* {{Z|is}}: {{W-|is|jógúrt}}
* {{Z|ja}}: {{W+|ja|ヨーグルト|tr=yōguruto}}
* {{Z|kn}}: {{W+|kn|ಮೊಸರು}}
* {{Z|ca}}: {{W+|ca|iogurt|n}}
* {{Z|qu}}: {{W-|qu|yugur}}
* {{Z|ky}}: {{W-|ky|йогурт|sc=Cyrl}}, {{W-|ky|катык}}, {{W+|ky|жуурат}}
* {{Z|si}}: {{W-|si|යෝගට්|sc=Sinh}}
* {{Z|rw}}: {{W-|rw|yawurute}}
* {{Z|ko}}: {{W+|ko|요구르트}}
* {{Z|kw}}: {{W-|kw|yogort|n}}
* {{Z|kum}}: {{W-|kum|ювурт}}
* {{Z|lad}}: {{W-|lad|yaúrt}}
* {{Z|la}}: {{W-|la|iogurtum|nt}}
* {{Z|lv}}: {{W-|lv|jogurts|n}}
* {{Z|lo}}: {{W+|lo|ນົມສົ້ມ}}
* {{Z|lt}}: {{W-|lt|jogurtas|n}}
* {{Z|lb}}: {{W+|lb|Jughurt|n}}
* {{Z|mk}}: {{W-|mk|јо́гурт|n}}
* {{Z|ml}}: {{W+|ml|തൈര്|sc=Mlym}}
* {{Z|ms}}: {{W-|ms|yogurt}}
* {{Z|mt}}: {{W-|mt|jowgart}}
* {{Z|gv}}: {{W-|gv|binjean|n}}
* {{Z|mi}}: {{W-|mi|miraka tepe}}, {{W-|mi|waipupuru}}, {{W-|mi|waiū pupuru}}
* {{Z|mr}}: {{W-|mr|दही|nt}}
* {{Z|hu}}: {{W+|hu|joghurt}}
* {{Z|mn}}: {{W+|mn|тараг|sc=Cyrl}}
* {{Z|ne}}: {{W-|ne|दहि|tr=dahi}}
* {{Z|nog}}: {{W-|nog|ювырт}}
* {{Z|no}}:
*: {{Z|nb}}: {{W+|nb|jogurt|n}}, {{W+|nb|yoghurt|n}}
*: {{Z|nn}}: {{W+|nn|yoghurt|n}}, {{W+|nn|jogurt|n}}
* {{Z|om}}: {{W-|om|itittuu}}
* {{Z|ota}}: {{W-|ota|یوغورت|tr=yoğurt}}
* {{Z|oui}}: {{W-|oui|tr=ywγrwt|ts=yoγrut}}
* {{Z|ug}}: {{W-|ug|قايماق}}
* {{Z|uz}}: {{W+|uz|qatiq}}, {{W+|uz|yogurt}}
* {{Z|ps}}: {{W+|ps|ماسته|m-p|tr=māstᶕ}}, {{W-|ps|مستى|m-p|tr=masté}}
* {{Z|pl}}: {{W+|pl|jogurt|n}}
* {{Z|pt}}: {{W+|pt|iogurte|n}}
* {{Z|pa}}: {{W-|pa|یوگرٹ}}
* {{Z|kk}}: {{W-|kk|йогурт|sc=Cyrl}}, {{W-|kk|қатық}}
* {{Z|rm}}: {{W-|rm|jogurt|n}}
* {{Z|ro}}: {{W+|ro|iaurt|nt}}
* {{Z|ru}}: {{W-|ru|йо́гурт|n}}, {{W-|ru|простоква́ша|m}}
* {{Z|ceb}}: {{W-|ceb|yogur}}
* {{Z|scn}}: {{W-|scn|jogurt|n}}
* {{Z|sd}}: {{W+|sd|ڏهي}}
* {{Z|sh}}:
*: Kirîlî: {{W-|sh|јогурт|n|sc=Cyrl}}
*: Latînî: {{W+|sh|jogurt|n}}
* {{Z|syl}}: {{W-|syl|ꠖꠂ}}
* {{Z|sk}}: {{W+|sk|jogurt|n}}
* {{Z|sl}}: {{W+|sl|jogurt|n}}
* Sorbî:
*: {{Z|dsb}}: {{W-|dsb|jogurt|n}}
* {{Z|es}}: {{W+|es|yogur|n}}
* {{Z|sw}}: {{W-|sw|maziwa ya mgando}}
* {{Z|sv}}: {{W+|sv|yoghurt|g}}
* {{Z|tg}}: {{W+|tg|ҷурғот}}, {{W+|tg|мост}}, {{W+|tg|йогурт|sc=Cyrl}}, {{W-|tg|қатиқ}}
* {{Z|tl}}: {{W-|tl|yogart}}
* {{Z|ta}}: {{W+|ta|தயிர்}}
* {{Z|th}}: {{W-|th|โยเกิร์ต}}, {{W-|th|นมเปรี้ยว}}
* {{Z|te}}: {{W+|te|పెరుగు}}
* {{Z|crh}}: {{W-|crh|yoğurt}}, {{W-|crh|qatıq}}
* {{Z|tt}}: {{W+|tt|йогырт|sc=Cyrl}}, {{W+|tt|катык}}
* {{Z|ti}}: {{W-|ti|ርግኦ}}
* {{Z|tr}}: {{W+|tr|yoğurt}}
* {{Z|tk}}: {{W-|tk|gatyk}}, {{W-|tk|süzme}}, {{W-|tk|ýogurt}}
* {{Z|bo}}: {{W-|bo|ཞོ}}
* {{Z|ur}}: {{W-|ur|دہی|n|tr=dahī|sc=ur-Arab}}
* {{Z|uk}}: {{W-|uk|йо́гурт|n}}, {{W-|uk|йо́ґурт|n}}, {{W-|uk|юґу́рт|n}}
* {{Z|vi}}: {{W+|vi|sữa chua}}, {{W+|vi|da ua}}
* {{Z|cy}}: {{W+|cy|iogwrt|n}}
* {{Z|km}}: {{W-|km|ទឹកដោះគោជូរ}}
* {{Z|yi}}: {{W-|yi|יאָגורט|n}}
* {{Z|el}}: {{W+|el|γιαούρτι|nt}}
*: {{Z|grc}}: {{W-|grc|ὀξύγαλα|nt}}
{{werger-bin}}
:{{werger çavkanî|en|mast|oldid=75616522|beş=Ev qismê Wergerê}}
== {{ziman|cs}} ==
=== Navdêr ===
{{navdêr|cs|z=n}}
# {{g|ku|melhem}}, {{g|ku|krêm}}
=== Çavkanî ===
{{rûpela wergerê|cs|melhem}}
== {{ziman|en}} ==
=== Bilêvkirin ===
* {{deng|en|LL-Q1860 (eng)-Wodencafe-mast.wav|Deng|dever=Teksas, DYA}}
=== Navdêr ===
{{navdêr|en}}
# {{f|en|xwarin}} {{g|ku|mast}}
==== Anagram ====
* {{anagram|en|a=amst|ATMs|Mats}}
== {{ziman|sh}} ==
=== Navdêr ===
{{navdêr|sh|mȃst|z=m}}
# {{f|sh|anatomî}} [[bez#ku|bez]]
== {{ziman|sl}} ==
=== Navdêr ===
{{navdêr|sl|z=m}}
# {{f|sl|anatomî}} [[bez#ku|bez]]
oav86umoilx8lsnyu8h3c1fuln29hox
6532007
6531996
2026-09-17T21:12:11Z
Balyozbot
7867
Bot: Paqijiyên kozmetîk (Lînk paqij kir.)
6532007
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-IPA}}
* {{deng|ku|LL-Q36163 (kmr)-Ebulfîda-mast.wav|dever=Bidlîs}}
* {{kîte|ku|mast}}
=== Navdêr ===
[[Wêne:Cacik-1.jpg|thumb|250px|[[xiyarmast]]]]
{{navdêr|ku|z=n}}
# {{f|ku|xwarin}} [[şîr|Şîrê]] bi [[hêvên]]î [[meyandin|meyandî]] û [[tîr]] kirî.
#* {{J:Firat Cewerî:Girtî:1986|jêgirtin=Pîrka wî satila '''mêst''', textê firaxan û selika nên nîşanî wî da û got: - Tu ji xwe re hinek '''mast''' têxe tacikekê û bixwe {{...}}}}
==== Tewandin ====
{{ku-tewîn-nav|nêr}}
==== Bi alfabeyên din ====
* {{ku-ar|ماست}}
==== Hevmane ====
{{kol3|ku
|yogurt
}}
==== Jê ====
{{kol3|ku
|mastkirî<crz:rengdêr>
}}
==== Etîmolojî ====
Ji {{mîr|ine-pro|ku|*meu-k-|w=[[hilû]], [[tehîsok]], [[hilîsok]], [[şemitok]]}} ku herwiha serkaniya peyva [[masî]] ya kurdî û [[moist]] ya îngilîzî ye.
* {{ji|ira-pro||-}}: *'''mad'''<sup>2</sup>(", [[meyîn]]")
:{{pal}}: '''m's'''("[[meyînn]]; hişk bûn")
:{{xpr}}: '''m's'''("[[meyîn]]--, hişk bûn")
:{{fa}}: '''māsīdan''' ("[[meyîn]]--"), '''māst''' ("mast")
:{{bal}}: '''mast''' ("mast")
:[[erdestanî]]: '''maske''' ("[[rûn]]") ... {{ir}}
:{{kmr}}: '''meyîn''' ("[[meyîn]]"), '''mast''' ("mast")
:{{kmr}}: '''mast''' ("mast")
:{{zza}}: '''mast''' ("mast")
{{sa}}: '''mástu'''([[sertûk]], [[qeymax]]")
;{{biçûk|Çavkanî:
* {{Cheung|r=253}}
* {{Horn|r=214}}
}}
==== Bide ber ====
{{kol3|ku
|dew
|firû
|nîvişk
|sertûk
|şîr
|tûk
|şojî
|penîr
|sûrik
|çolik
|zêç
}}
==== Werger ====
{{werger-ser}}
* {{Z|ab}}: {{W-|ab|ахарҵәы|sc=Cyrl}}
* {{Z|af}}: {{W-|af|jogurt}}
* {{Z|sq}}: {{W+|sq|kos|n}}, {{W-|sq|jogurt|n}}
* {{Z|de}}: {{W+|de|Joghurt|n}}
* {{Z|am}}: {{W-|am|እርጎ}}
* {{Z|rup}}: {{W-|rup|mãrcat|nt}}
* {{Z|as}}: {{W-|as|দৈ}}
* {{Z|ast}}: {{W-|ast|yogur|n}}
* {{Z|az}}: {{W+|az|qatıq}}, {{W+|az|yoğurt}}, {{W-|az|yoqurt}}
* {{Z|eu}}: {{W+|eu|jogurt}}
* {{Z|be}}: {{W-|be|ёгурт|n|sc=Cyrl}}
* {{Z|bn}}: {{W+|bn|দই}}
* {{Z|br}}: {{W-|br|yaourt|n}}
* {{Z|bg}}: {{W-|bg|ки́село мля́ко|nt}}
* {{Z|my}}: {{W+|my|ဒိန်ချဉ်|sc=Mymr}}
* {{Z|bua}}: {{W-|bua|тараг}}
* {{Z|cs}}: {{W+|cs|jogurt|n}}
* {{Z|chr}}: {{W-|chr|ᏧᏂᎿᏦᏯᏍᏔᏅ}}
* {{Z|zh}}:
*: {{Z|yue}}: {{W-|yue|乳酪|tr=jyu5 lok3}}
*: {{Z|hak}}: {{W-|hak|優酪乳|tr=yulólù}}
*: {{Z|cmn}}: {{W+|cmn|酸奶|tr=suānnǎi}}, {{W-|cmn|酸牛奶|tr=suānniúnǎi}}, {{W-|cmn|酸凝酪|tr=suānnínglào, suānníngluò}}, {{W+|cmn|優酪乳|tr=yōulàorǔ, yōuluòrǔ}}, {{W-|cmn|發酵乳}}, {{W+|cmn|乳酪|tr=rǔlào}}, {{W-|cmn|发酵乳|tr=fājiàorǔ, fāxiàorǔ}}, {{W+|cmn|優格|tr=yōugé}}
*: {{Z|nan}}: {{W-|nan|酸奶|tr=sng-lin / sng-ni / suiⁿ-leng}}
*: {{Z|wuu}}: {{W-|wuu|酸奶}}
* {{Z|da}}: {{W-|da|jogurt|g}}, {{W+|da|yoghurt|g}}
* {{Z|id}}: {{W+|id|yoghurt}}
* {{Z|ar}}: {{W+|ar|لَبَن|n}}, {{W-|ar|لَبَن زَبَادِيّ|n}}, {{W-|ar|لَبَن رَائِب|n}}, {{W+|ar|رَوْب|n}}, {{W-|ar|رَوْبَة|m}}, {{W-|ar|مَاسْت|n}}
*: {{Z|arz}}: {{W-|arz|زبادي|n|tr=zabadi|sc=Arab}}
*: {{Z|afb}}: {{W-|afb|روب|tr=rōb}}
*: {{Z|acw}}: {{W-|acw|زبادي|n|tr=zabādi}}
*: {{Z|ary}}: {{W-|ary|دانون|n|tr=danon|sc=Arab}}
* {{Z|hy}}: {{W+|hy|յոգուրտ}}
* {{Z|eo}}: {{W-|eo|jogurto}}, {{W-|eo|jahurto}}
* {{Z|et}}: {{W+|et|jogurt}}
* {{Z|fa}}: {{W+|fa|ماست|tr=mâst}}, {{W+|fa|جغرات|tr=joğrât}}
* {{Z|fo}}: {{W-|fo|jogurt|nt}}
* {{Z|fi}}: {{W+|fi|jogurtti}}
* {{Z|fr}}: {{W+|fr|yaourt|n}}, {{W+|fr|yogourt|n}}
* {{Z|gd}}: {{W-|gd|iogart|n}}
* {{Z|gag}}: {{W-|gag|yuurt}}
* {{Z|gl}}: {{W+|gl|iogur|n}}
* {{Z|gu}}: {{W-|gu|દહીં}}
* {{Z|ka}}: {{W-|ka|იოგურტი|sc=Geor}}, {{W+|ka|მაწონი|sc=Geor}}
* {{Z|hi}}: {{W+|hi|दही|n}}
* {{Z|nl}}: {{W+|nl|yoghurt|n}}
* {{Z|he}}: {{W-|he|יוֹגוּרְט|n|tr=yogurt}}
* {{Z|io}}: {{W+|io|yogurto}}
* {{Z|en}}: {{W+|en|mast}}
* {{Z|ia}}: {{W-|ia|yogurt}}
* {{Z|ga}}: {{W-|ga|iógart|n}}
* {{Z|it}}: {{W+|it|yogurt|n}}, {{W+|it|iogurt|n}}
* {{Z|is}}: {{W-|is|jógúrt}}
* {{Z|ja}}: {{W+|ja|ヨーグルト|tr=yōguruto}}
* {{Z|kn}}: {{W+|kn|ಮೊಸರು}}
* {{Z|ca}}: {{W+|ca|iogurt|n}}
* {{Z|qu}}: {{W-|qu|yugur}}
* {{Z|ky}}: {{W-|ky|йогурт|sc=Cyrl}}, {{W-|ky|катык}}, {{W+|ky|жуурат}}
* {{Z|si}}: {{W-|si|යෝගට්|sc=Sinh}}
* {{Z|rw}}: {{W-|rw|yawurute}}
* {{Z|ko}}: {{W+|ko|요구르트}}
* {{Z|kw}}: {{W-|kw|yogort|n}}
* {{Z|kum}}: {{W-|kum|ювурт}}
* {{Z|lad}}: {{W-|lad|yaúrt}}
* {{Z|la}}: {{W-|la|iogurtum|nt}}
* {{Z|lv}}: {{W-|lv|jogurts|n}}
* {{Z|lo}}: {{W+|lo|ນົມສົ້ມ}}
* {{Z|lt}}: {{W-|lt|jogurtas|n}}
* {{Z|lb}}: {{W+|lb|Jughurt|n}}
* {{Z|mk}}: {{W-|mk|јо́гурт|n}}
* {{Z|ml}}: {{W+|ml|തൈര്|sc=Mlym}}
* {{Z|ms}}: {{W-|ms|yogurt}}
* {{Z|mt}}: {{W-|mt|jowgart}}
* {{Z|gv}}: {{W-|gv|binjean|n}}
* {{Z|mi}}: {{W-|mi|miraka tepe}}, {{W-|mi|waipupuru}}, {{W-|mi|waiū pupuru}}
* {{Z|mr}}: {{W-|mr|दही|nt}}
* {{Z|hu}}: {{W+|hu|joghurt}}
* {{Z|mn}}: {{W+|mn|тараг|sc=Cyrl}}
* {{Z|ne}}: {{W-|ne|दहि|tr=dahi}}
* {{Z|nog}}: {{W-|nog|ювырт}}
* {{Z|no}}:
*: {{Z|nb}}: {{W+|nb|jogurt|n}}, {{W+|nb|yoghurt|n}}
*: {{Z|nn}}: {{W+|nn|yoghurt|n}}, {{W+|nn|jogurt|n}}
* {{Z|om}}: {{W-|om|itittuu}}
* {{Z|ota}}: {{W-|ota|یوغورت|tr=yoğurt}}
* {{Z|oui}}: {{W-|oui|tr=ywγrwt|ts=yoγrut}}
* {{Z|ug}}: {{W-|ug|قايماق}}
* {{Z|uz}}: {{W+|uz|qatiq}}, {{W+|uz|yogurt}}
* {{Z|ps}}: {{W+|ps|ماسته|m-p|tr=māstᶕ}}, {{W-|ps|مستى|m-p|tr=masté}}
* {{Z|pl}}: {{W+|pl|jogurt|n}}
* {{Z|pt}}: {{W+|pt|iogurte|n}}
* {{Z|pa}}: {{W-|pa|یوگرٹ}}
* {{Z|kk}}: {{W-|kk|йогурт|sc=Cyrl}}, {{W-|kk|қатық}}
* {{Z|rm}}: {{W-|rm|jogurt|n}}
* {{Z|ro}}: {{W+|ro|iaurt|nt}}
* {{Z|ru}}: {{W-|ru|йо́гурт|n}}, {{W-|ru|простоква́ша|m}}
* {{Z|ceb}}: {{W-|ceb|yogur}}
* {{Z|scn}}: {{W-|scn|jogurt|n}}
* {{Z|sd}}: {{W+|sd|ڏهي}}
* {{Z|sh}}:
*: Kirîlî: {{W-|sh|јогурт|n|sc=Cyrl}}
*: Latînî: {{W+|sh|jogurt|n}}
* {{Z|syl}}: {{W-|syl|ꠖꠂ}}
* {{Z|sk}}: {{W+|sk|jogurt|n}}
* {{Z|sl}}: {{W+|sl|jogurt|n}}
* Sorbî:
*: {{Z|dsb}}: {{W-|dsb|jogurt|n}}
* {{Z|es}}: {{W+|es|yogur|n}}
* {{Z|sw}}: {{W-|sw|maziwa ya mgando}}
* {{Z|sv}}: {{W+|sv|yoghurt|g}}
* {{Z|tg}}: {{W+|tg|ҷурғот}}, {{W+|tg|мост}}, {{W+|tg|йогурт|sc=Cyrl}}, {{W-|tg|қатиқ}}
* {{Z|tl}}: {{W-|tl|yogart}}
* {{Z|ta}}: {{W+|ta|தயிர்}}
* {{Z|th}}: {{W-|th|โยเกิร์ต}}, {{W-|th|นมเปรี้ยว}}
* {{Z|te}}: {{W+|te|పెరుగు}}
* {{Z|crh}}: {{W-|crh|yoğurt}}, {{W-|crh|qatıq}}
* {{Z|tt}}: {{W+|tt|йогырт|sc=Cyrl}}, {{W+|tt|катык}}
* {{Z|ti}}: {{W-|ti|ርግኦ}}
* {{Z|tr}}: {{W+|tr|yoğurt}}
* {{Z|tk}}: {{W-|tk|gatyk}}, {{W-|tk|süzme}}, {{W-|tk|ýogurt}}
* {{Z|bo}}: {{W-|bo|ཞོ}}
* {{Z|ur}}: {{W-|ur|دہی|n|tr=dahī|sc=ur-Arab}}
* {{Z|uk}}: {{W-|uk|йо́гурт|n}}, {{W-|uk|йо́ґурт|n}}, {{W-|uk|юґу́рт|n}}
* {{Z|vi}}: {{W+|vi|sữa chua}}, {{W+|vi|da ua}}
* {{Z|cy}}: {{W+|cy|iogwrt|n}}
* {{Z|km}}: {{W-|km|ទឹកដោះគោជូរ}}
* {{Z|yi}}: {{W-|yi|יאָגורט|n}}
* {{Z|el}}: {{W+|el|γιαούρτι|nt}}
*: {{Z|grc}}: {{W-|grc|ὀξύγαλα|nt}}
{{werger-bin}}
:{{werger çavkanî|en|mast|oldid=75616522|beş=Ev qismê Wergerê}}
== {{ziman|cs}} ==
=== Navdêr ===
{{navdêr|cs|z=n}}
# {{g|ku|melhem}}, {{g|ku|krêm}}
=== Çavkanî ===
{{rûpela wergerê|cs|melhem}}
== {{ziman|en}} ==
=== Bilêvkirin ===
* {{deng|en|LL-Q1860 (eng)-Wodencafe-mast.wav|Deng|dever=Teksas, DYA}}
=== Navdêr ===
{{navdêr|en}}
# {{f|en|xwarin}} {{g|ku|mast}}
==== Anagram ====
* {{anagram|en|a=amst|ATMs|Mats}}
== {{ziman|sh}} ==
=== Navdêr ===
{{navdêr|sh|mȃst|z=m}}
# {{f|sh|anatomî}} [[bez#ku|bez]]
== {{ziman|sl}} ==
=== Navdêr ===
{{navdêr|sl|z=m}}
# {{f|sl|anatomî}} [[bez#ku|bez]]
fc2m300rt1hne5fpjriptsu9g83wmnf
ensîklopedî
0
10102
6531995
6384916
2026-09-17T19:14:31Z
Wikihez
7344
Kurmancî/Navdêr: beşa «Resim» hate nûkirin (DefSideBoxes)
6531995
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-IPA}}
* {{ku-kîte}}
=== Navdêr ===
[[Wêne:Britannica Macropaedia.jpg|thumb|200px|Cildên 21–24an a '''ensîklopediya''' ''Britannica''yê (madeyên ji ''India''yê heta ''Norway''ê dihewîne) li kitêbxaneya [[w:Deutsches Museum|Deutsches Museum]]ê.]]
{{navdêr|ku|z=m}}
# Ferheng an [[kitêb]]a ku tê de [[malûmat]] li ser gelek tiştî heye (bi giştî li gor [[alfabe]]yê yan jî li gor [[mesele]]yan).
#: {{hevmaneyên peyvê|ku|zanistname|zanînname}}
==== Tewandin ====
{{ku-tewîn-nav/nû|mê}}
==== Formên alternatîv ====
{{kol3|ku|cure=Herwiha
| ensîklopediya
| ensîklopedîya
| ensîklopedya
| ansîklopedî
| ansîklopediya
| ansîklopedîya
| ansîklopedya
}}
==== Jê ====
{{kol3|ku|cure=Jê
| ensîklopedîk
| ensîklopedîst
| ensîklopedîstî
}}
==== Etîmolojî ====
{{etîmon|ku|:deyn|fr:encyclopédie<id:ensîklopedî>|dar=1|metn=++|id=kitêba referansê}}
==== Werger ====
{{werger-ser|kitêba malûmatan}}
* {{Z|ace}}: {{W-|ace|ènsiklopèdia}}
* {{Z|af}}: {{W+|af|ensiklopedie}}
* {{Z|sq}}: {{W+|sq|enciklopedi|m}}
* {{Z|de}}: {{W+|de|Enzyklopädie|m}}, {{W+|de|Lexikon|nt}}, {{W+|de|Konversationslexikon|nt}}; {{W-|de|Realencyclopädie|m}}, {{W-|de|Realwörterbuch|nt}}
* {{Z|am}}: {{W-|am|መዝገበ ዕውቀት}}
* {{Z|ast}}: {{W-|ast|enciclopedia|m}}
* {{Z|az}}: {{W-|az|qamus}}, {{W+|az|ensiklopediya}}, {{W-|az|biliklik}}
*: Erebî: {{W-|az|قاموس|tr=qāmūs}}
* {{Z|eu}}: {{W+|eu|entziklopedia}}
* {{Z|be}}: {{W-|be|энцыклапе́дыя|m}}, {{W-|be|энцыкляпэ́дыя|m}}
* {{Z|bal}}: {{W-|bal|زانتنامہ|tr=zāntnāma}}, {{W-|bal|زانتگرد|tr=zántgird}}
* {{Z|bn}}: {{W+|bn|বিশ্বকোষ}} {{W-|bn|এনসাইক্লোপিডিয়া}}
* {{Z|bew}}: {{W-|bew|ènsiklopédi}}
* {{Z|bqi}}: {{W-|bqi|دونسمندنامه|tr=dunismandnāma}}
* {{Z|brh}}: {{W-|brh|zántgird}}
* {{Z|br}}: {{W+|br|holloueziadur}}
* {{Z|bg}}: {{W+|bg|енциклопе́дия|m}}
* {{Z|my}}: {{W+|my|စွယ်စုံကျမ်း}}
* {{Z|cs}}: {{W+|cs|encyklopedie|m}}
* {{Z|zh}}:
*: {{Z|yue}}: {{W-|yue|百科全書}}, {{W-|yue|百科事典}}
* {{Z|cv}}: {{W-|cv|энциклопеди}}
* {{Z|da}}: {{W+|da|encyklopædi}}, {{W+|da|leksikon|nt}}
* {{Z|gsw}}: {{W-|gsw|Enzyklopädie}}
* {{Z|id}}: {{W+|id|ensiklopedia}}
* {{Z|ar}}: {{W+|ar|مَوْسُوعَة|m}}, {{W-|ar|دَائِرَة مَعَارِف|m|tr=dāʔirat maʕārif}}
*: {{Z|ary}}: {{W-|ary|موسوعة|tr=mawsūʕa}}
* {{Z|hy}}: {{W+|hy|հանրագիտարան}}
* {{Z|myv}}: {{W-|myv|содамкундо}}
* {{Z|eo}}: {{W+|eo|enciklopedio}}
* {{Z|et}}: {{W+|et|entsüklopeedia}}
* {{Z|fa}}:
*: {{Z|fa-ira}}: {{W-|fa-ira|دانِشْنامِه}}, {{W-|fa-ira|دایِرَةُالْمَعارِف|tr=dâyerat-ol-ma'âref}}, {{W-|fa|دائِرَةُالْمَعارِف|tr=dâ'erat-ol-ma'âref}}, {{W-|fa-ira|آنْسیکْلوپِدی|tr=ânsiklopedi}}
* {{Z|fo}}: {{W-|fo|alfrøðibók|m}}
* {{Z|fi}}: {{W+|fi|tietosanakirja}}, {{W+|fi|ensyklopedia}}
* {{Z|fr}}: {{W+|fr|encyclopédie|m}}
* {{Z|fy}}: {{W-|fy|ensyklopedy}}
* {{Z|gl}}: {{W+|gl|enciclopedia|m}}
* {{Z|glk}}: {{W-|glk|دانشنامه|tr=dånišnåmä}}
* {{Z|ka}}: {{W-|ka|ენციკლოპედია}}
* {{Z|ht}}: {{W-|ht|ansiklopedi}}
* {{Z|hac}}: {{W-|hac|زانستنامە|tr=zānistnāma}}
* {{Z|hi}}: {{W-|hi|ज्ञानकोष|n}}, {{W+|hi|विश्वकोश|n}}, {{W+|hi|विश्वकोष|n}}, {{W+|hi|साइक्लोपीडिया|m}}, {{W-|hi|इनसाइक्लोपीडिया|m}}
* {{Z|nl}}: {{W+|nl|encyclopedie|n}}
* {{Z|he}}: {{W+|he|אֶנְצִיקְלוֹפֶּדְיָה|m|tr=entsiklopédya}}
* {{Z|io}}: {{W+|io|enciklopedio}}
* {{Z|en}}: {{W+|en|encyclopedia}}
* {{Z|ia}}: {{W+|ia|encyclopedia}}
* {{Z|ga}}: {{W-|ga|ciclipéid|m}}
* {{Z|it}}: {{W+|it|enciclopedia|m}}
* {{Z|is}}: {{W+|is|alfræðibók|m}}, {{W+|is|alfræðiorðabók|m}}, {{W+|is|alfræðirit|nt}}
* {{Z|ja}}: {{W+|ja|百科事典|tr=ひゃっかじてん, hyakkajiten}}, {{W+|ja|百科全書|tr=ひゃっかぜんしょ, hyakkazensho}}
* {{Z|kn}}: {{W-|kn|ವಿಶ್ವಕೋಶ}}
* {{Z|krc}}: {{W-|krc|энциклопедия}}
* {{Z|ca}}: {{W+|ca|enciclopèdia}}
* {{Z|ky}}: {{W+|ky|энциклопедия}}
* {{Z|ko}}: {{W+|ko|백과사전}}, {{W+|ko|백과전서}}
* {{Z|la}}: {{W-|la|encyclopaedīa|m}}, {{W-|la|pandectēs|n}}
* {{Z|lv}}: {{W-|lv|enciklopēdija|m}}
* {{Z|lo}}: {{W+|lo|ສາລານຸກົມ}}
* {{Z|olo}}: {{W-|olo|tiedosanakniigu}}
* {{Z|li}}: {{W-|li|encyklopedie}}
* {{Z|lt}}: {{W+|lt|enciklopedija|m}}
* {{Z|lb}}: {{W-|lb|Enzyklopedie|m}}
* {{Z|lrc}}: {{W-|lrc|دونسمنامٱ|tr=dunismnāma}}
* {{Z|mk}}: {{W-|mk|енциклопе́дија|m}}
* {{Z|ml}}: {{W-|ml|സര്വവിജ്ഞാനകോശം}}, {{W+|ml|വിജ്ഞാനകോശം}}
* {{Z|ms}}: {{W+|ms|ensiklopedia}}, {{W-|ms|jagat katan}}
* {{Z|mi}}: {{W-|mi|mātāpunenga}}
* {{Z|mzn}}: {{W-|mzn|دانشنومه|tr=dānišnuma}}
* {{Z|hu}}: {{W+|hu|enciklopédia}}, {{W+|hu|lexikon}}
* {{Z|mn}}:
*: Kirîlî: {{W-|mn|нэвтэрхий толь}}
* {{Z|nah}}: {{W+|nah|centlamatilizamoxtli}}
* {{Z|nv}}: {{W-|nv|ínsadoobíídiiya}}
* {{Z|nds}}: {{W-|nds|nokieksel}}
* {{Z|no}}:
*: {{Z|nb}}: {{W+|nb|encyklopedi|n}}
*: {{Z|nn}}: {{W-|nn|encyklopedi|n}}
* {{Z|ojb}}: {{W-|ojb|gikendamowin-mazina'igan}}
* {{Z|os}}: {{W-|os|энциклопеди}}
* {{Z|ota}}: {{W-|ota|محیطالمعرف|tr=mühītülmā'rif}}
* {{Z|ug}}: {{W+|ug|قامۇس}}, {{W-|ug|ئېنسىكلوپېدىيە}}, {{W-|ug|ئېنىسىكلوپېدىيە}}
* {{Z|uz}}: {{W+|uz|ensiklopediya}}, {{W+|uz|qomus}}
* {{Z|ps}}: {{W+|ps|پوهنغونډ|n|tr=pohǝnǧwǝnḍ}}
* {{Z|pl}}: {{W+|pl|encyklopedia|m}}
* {{Z|pt}}: {{W+|pt|enciclopédia|m}}
* {{Z|pa}}: {{W-|pa|ਵਿਸ਼ਵਕੋਸ਼|n}}
* {{Z|kk}}: {{W-|kk|энциклопедия}}
* {{Z|ro}}: {{W+|ro|enciclopedie|m}}
* {{Z|ru}}: {{W+|ru|энциклопе́дия|m}}
* {{Z|rue}}: {{W-|rue|енціклопе́дія|m}}
* {{Z|se}}: {{W-|se|diehtosátnegirji}}
* {{Z|sa}}: {{W+|sa|विश्वकोश}}
* {{Z|sh}}:
*: Kirîlî: {{W-|sh|енциклопе́дија|m}}
*: Latînî: {{W-|sh|enciklopédija|m}}
* {{Z|cu}}:
*: Kirîlî: {{W-|cu|єнкѷклопєдїꙗ|m}}
* {{Z|sk}}: {{W+|sk|encyklopédia|m}}
* {{Z|sl}}: {{W+|sl|enciklopedija|m}}
* Sorbî:
*: {{Z|dsb}}: {{W-|dsb|encyklopedija|m}}
*: {{Z|hsb}}: {{W-|hsb|encyklopedija|m}}
* {{Z|es}}: {{W+|es|enciclopedia|m}}
* {{Z|sw}}: {{W-|sw|kamusi elezo}}, {{W-|sw|ensaiklopidia}}
* {{Z|sv}}: {{W+|sv|encyklopedi|g}}, {{W+|sv|konversationslexikon|nt}}, {{W+|sv|uppslagsverk|nt}}
* {{Z|tg}}: {{W+|tg|донишнома}}, {{W-|tg|доиратулмаориф}}, {{W-|tg|энсиклопедия}}
* {{Z|tl}}: {{W-|tl|santalaalaman}}, {{W+|tl|ensiklopedya}}
* {{Z|th}}: {{W+|th|สารานุกรม}}
* {{Z|te}}: {{W-|te|విజ్ఞానసర్వస్వం}}
* {{Z|crh}}: {{W-|crh|entsiklopediya}}
* {{Z|tt}}: {{W-|tt|энциклопедия}}
* {{Z|ti}}: {{W-|ti|ኢንሳይክሎፐድያ}}, {{W-|ti|መዝገበ ፍልጠት}}
* {{Z|tr}}: {{W+|tr|ansiklopedi}}
* {{Z|tk}}: {{W-|tk|ensiklopediýa}}
* {{Z|bo}}: {{W-|bo|ཤེས་བྱ་ཀུན་ཁྱབ་མཛོད|lit=treasury of the universally knowable}}, {{W-|bo|རིག་མཛོད|lit=knowledge treasury}}
* {{Z|ur}}: {{W-|ur|دائِرَۃُ الْمَعارِف|tr=dāiratu l-ma'ārif}}, {{W-|ur|کِتابُ الْمَعارِف|tr=kitābu l-ma'ārif}}, {{W+|ur|قامُوس|n}}, {{W-|ur|اِنْسائِیکِلوپِیڈْیا}}, {{W-|ur|موسوعہ|tr=mausū'a}}, {{W-|ur|احصاء العلوم|tr=ihsā'-ul-u'lūm}}, {{W-|ur|قاموس الاحلام|tr=qāmūs-al-ahlām}}
* {{Z|uk}}: {{W-|uk|енциклопе́дія|m}}
* {{Z|vi}}: {{W+|vi|bách khoa toàn thư}}
* {{Z|vo}}: {{W+|vo|sikloped}}, {{W-|vo|züklopäod}}, {{W-|vo|realasikloped}}
* {{Z|cy}}: {{W+|cy|gwyddoniadur|n}}
* {{Z|km}}: {{W-|km|វិជ្ជាសមោធាន}}, {{W-|km|សៀវភៅប្រជុំវិជ្ជា}}
* {{Z|sah}}: {{W-|sah|энциклопедия}}
* {{Z|yi}}: {{W-|yi|ענציקלאָפּעדיע|m}}
* {{Z|el}}: {{W+|el|εγκυκλοπαίδεια|m}}
*: Kevn: {{W-|grc|πανδέκτης|n}}
* {{Z|zea}}: {{W-|zea|encyclopedie}}
{{werger-bin}}
:{{werger çavkanî|en|encyclopedia|beş=Ev qismê Wergerê|oldid=84819598}}
7vshbtrq2427fb7xi4s0oe7lg60mojm
6532008
6531995
2026-09-17T21:12:15Z
Balyozbot
7867
Bot: Paqijiyên kozmetîk ({{ku-tewîn-nav/nû}} → {{ku-tewîn-nav}}.)
6532008
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-IPA}}
* {{ku-kîte}}
=== Navdêr ===
[[Wêne:Britannica Macropaedia.jpg|thumb|200px|Cildên 21–24an a '''ensîklopediya''' ''Britannica''yê (madeyên ji ''India''yê heta ''Norway''ê dihewîne) li kitêbxaneya [[w:Deutsches Museum|Deutsches Museum]]ê.]]
{{navdêr|ku|z=m}}
# Ferheng an [[kitêb]]a ku tê de [[malûmat]] li ser gelek tiştî heye (bi giştî li gor [[alfabe]]yê yan jî li gor [[mesele]]yan).
#: {{hevmaneyên peyvê|ku|zanistname|zanînname}}
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Formên alternatîv ====
{{kol3|ku|cure=Herwiha
| ensîklopediya
| ensîklopedîya
| ensîklopedya
| ansîklopedî
| ansîklopediya
| ansîklopedîya
| ansîklopedya
}}
==== Jê ====
{{kol3|ku|cure=Jê
| ensîklopedîk
| ensîklopedîst
| ensîklopedîstî
}}
==== Etîmolojî ====
{{etîmon|ku|:deyn|fr:encyclopédie<id:ensîklopedî>|dar=1|metn=++|id=kitêba referansê}}
==== Werger ====
{{werger-ser|kitêba malûmatan}}
* {{Z|ace}}: {{W-|ace|ènsiklopèdia}}
* {{Z|af}}: {{W+|af|ensiklopedie}}
* {{Z|sq}}: {{W+|sq|enciklopedi|m}}
* {{Z|de}}: {{W+|de|Enzyklopädie|m}}, {{W+|de|Lexikon|nt}}, {{W+|de|Konversationslexikon|nt}}; {{W-|de|Realencyclopädie|m}}, {{W-|de|Realwörterbuch|nt}}
* {{Z|am}}: {{W-|am|መዝገበ ዕውቀት}}
* {{Z|ast}}: {{W-|ast|enciclopedia|m}}
* {{Z|az}}: {{W-|az|qamus}}, {{W+|az|ensiklopediya}}, {{W-|az|biliklik}}
*: Erebî: {{W-|az|قاموس|tr=qāmūs}}
* {{Z|eu}}: {{W+|eu|entziklopedia}}
* {{Z|be}}: {{W-|be|энцыклапе́дыя|m}}, {{W-|be|энцыкляпэ́дыя|m}}
* {{Z|bal}}: {{W-|bal|زانتنامہ|tr=zāntnāma}}, {{W-|bal|زانتگرد|tr=zántgird}}
* {{Z|bn}}: {{W+|bn|বিশ্বকোষ}} {{W-|bn|এনসাইক্লোপিডিয়া}}
* {{Z|bew}}: {{W-|bew|ènsiklopédi}}
* {{Z|bqi}}: {{W-|bqi|دونسمندنامه|tr=dunismandnāma}}
* {{Z|brh}}: {{W-|brh|zántgird}}
* {{Z|br}}: {{W+|br|holloueziadur}}
* {{Z|bg}}: {{W+|bg|енциклопе́дия|m}}
* {{Z|my}}: {{W+|my|စွယ်စုံကျမ်း}}
* {{Z|cs}}: {{W+|cs|encyklopedie|m}}
* {{Z|zh}}:
*: {{Z|yue}}: {{W-|yue|百科全書}}, {{W-|yue|百科事典}}
* {{Z|cv}}: {{W-|cv|энциклопеди}}
* {{Z|da}}: {{W+|da|encyklopædi}}, {{W+|da|leksikon|nt}}
* {{Z|gsw}}: {{W-|gsw|Enzyklopädie}}
* {{Z|id}}: {{W+|id|ensiklopedia}}
* {{Z|ar}}: {{W+|ar|مَوْسُوعَة|m}}, {{W-|ar|دَائِرَة مَعَارِف|m|tr=dāʔirat maʕārif}}
*: {{Z|ary}}: {{W-|ary|موسوعة|tr=mawsūʕa}}
* {{Z|hy}}: {{W+|hy|հանրագիտարան}}
* {{Z|myv}}: {{W-|myv|содамкундо}}
* {{Z|eo}}: {{W+|eo|enciklopedio}}
* {{Z|et}}: {{W+|et|entsüklopeedia}}
* {{Z|fa}}:
*: {{Z|fa-ira}}: {{W-|fa-ira|دانِشْنامِه}}, {{W-|fa-ira|دایِرَةُالْمَعارِف|tr=dâyerat-ol-ma'âref}}, {{W-|fa|دائِرَةُالْمَعارِف|tr=dâ'erat-ol-ma'âref}}, {{W-|fa-ira|آنْسیکْلوپِدی|tr=ânsiklopedi}}
* {{Z|fo}}: {{W-|fo|alfrøðibók|m}}
* {{Z|fi}}: {{W+|fi|tietosanakirja}}, {{W+|fi|ensyklopedia}}
* {{Z|fr}}: {{W+|fr|encyclopédie|m}}
* {{Z|fy}}: {{W-|fy|ensyklopedy}}
* {{Z|gl}}: {{W+|gl|enciclopedia|m}}
* {{Z|glk}}: {{W-|glk|دانشنامه|tr=dånišnåmä}}
* {{Z|ka}}: {{W-|ka|ენციკლოპედია}}
* {{Z|ht}}: {{W-|ht|ansiklopedi}}
* {{Z|hac}}: {{W-|hac|زانستنامە|tr=zānistnāma}}
* {{Z|hi}}: {{W-|hi|ज्ञानकोष|n}}, {{W+|hi|विश्वकोश|n}}, {{W+|hi|विश्वकोष|n}}, {{W+|hi|साइक्लोपीडिया|m}}, {{W-|hi|इनसाइक्लोपीडिया|m}}
* {{Z|nl}}: {{W+|nl|encyclopedie|n}}
* {{Z|he}}: {{W+|he|אֶנְצִיקְלוֹפֶּדְיָה|m|tr=entsiklopédya}}
* {{Z|io}}: {{W+|io|enciklopedio}}
* {{Z|en}}: {{W+|en|encyclopedia}}
* {{Z|ia}}: {{W+|ia|encyclopedia}}
* {{Z|ga}}: {{W-|ga|ciclipéid|m}}
* {{Z|it}}: {{W+|it|enciclopedia|m}}
* {{Z|is}}: {{W+|is|alfræðibók|m}}, {{W+|is|alfræðiorðabók|m}}, {{W+|is|alfræðirit|nt}}
* {{Z|ja}}: {{W+|ja|百科事典|tr=ひゃっかじてん, hyakkajiten}}, {{W+|ja|百科全書|tr=ひゃっかぜんしょ, hyakkazensho}}
* {{Z|kn}}: {{W-|kn|ವಿಶ್ವಕೋಶ}}
* {{Z|krc}}: {{W-|krc|энциклопедия}}
* {{Z|ca}}: {{W+|ca|enciclopèdia}}
* {{Z|ky}}: {{W+|ky|энциклопедия}}
* {{Z|ko}}: {{W+|ko|백과사전}}, {{W+|ko|백과전서}}
* {{Z|la}}: {{W-|la|encyclopaedīa|m}}, {{W-|la|pandectēs|n}}
* {{Z|lv}}: {{W-|lv|enciklopēdija|m}}
* {{Z|lo}}: {{W+|lo|ສາລານຸກົມ}}
* {{Z|olo}}: {{W-|olo|tiedosanakniigu}}
* {{Z|li}}: {{W-|li|encyklopedie}}
* {{Z|lt}}: {{W+|lt|enciklopedija|m}}
* {{Z|lb}}: {{W-|lb|Enzyklopedie|m}}
* {{Z|lrc}}: {{W-|lrc|دونسمنامٱ|tr=dunismnāma}}
* {{Z|mk}}: {{W-|mk|енциклопе́дија|m}}
* {{Z|ml}}: {{W-|ml|സര്വവിജ്ഞാനകോശം}}, {{W+|ml|വിജ്ഞാനകോശം}}
* {{Z|ms}}: {{W+|ms|ensiklopedia}}, {{W-|ms|jagat katan}}
* {{Z|mi}}: {{W-|mi|mātāpunenga}}
* {{Z|mzn}}: {{W-|mzn|دانشنومه|tr=dānišnuma}}
* {{Z|hu}}: {{W+|hu|enciklopédia}}, {{W+|hu|lexikon}}
* {{Z|mn}}:
*: Kirîlî: {{W-|mn|нэвтэрхий толь}}
* {{Z|nah}}: {{W+|nah|centlamatilizamoxtli}}
* {{Z|nv}}: {{W-|nv|ínsadoobíídiiya}}
* {{Z|nds}}: {{W-|nds|nokieksel}}
* {{Z|no}}:
*: {{Z|nb}}: {{W+|nb|encyklopedi|n}}
*: {{Z|nn}}: {{W-|nn|encyklopedi|n}}
* {{Z|ojb}}: {{W-|ojb|gikendamowin-mazina'igan}}
* {{Z|os}}: {{W-|os|энциклопеди}}
* {{Z|ota}}: {{W-|ota|محیطالمعرف|tr=mühītülmā'rif}}
* {{Z|ug}}: {{W+|ug|قامۇس}}, {{W-|ug|ئېنسىكلوپېدىيە}}, {{W-|ug|ئېنىسىكلوپېدىيە}}
* {{Z|uz}}: {{W+|uz|ensiklopediya}}, {{W+|uz|qomus}}
* {{Z|ps}}: {{W+|ps|پوهنغونډ|n|tr=pohǝnǧwǝnḍ}}
* {{Z|pl}}: {{W+|pl|encyklopedia|m}}
* {{Z|pt}}: {{W+|pt|enciclopédia|m}}
* {{Z|pa}}: {{W-|pa|ਵਿਸ਼ਵਕੋਸ਼|n}}
* {{Z|kk}}: {{W-|kk|энциклопедия}}
* {{Z|ro}}: {{W+|ro|enciclopedie|m}}
* {{Z|ru}}: {{W+|ru|энциклопе́дия|m}}
* {{Z|rue}}: {{W-|rue|енціклопе́дія|m}}
* {{Z|se}}: {{W-|se|diehtosátnegirji}}
* {{Z|sa}}: {{W+|sa|विश्वकोश}}
* {{Z|sh}}:
*: Kirîlî: {{W-|sh|енциклопе́дија|m}}
*: Latînî: {{W-|sh|enciklopédija|m}}
* {{Z|cu}}:
*: Kirîlî: {{W-|cu|єнкѷклопєдїꙗ|m}}
* {{Z|sk}}: {{W+|sk|encyklopédia|m}}
* {{Z|sl}}: {{W+|sl|enciklopedija|m}}
* Sorbî:
*: {{Z|dsb}}: {{W-|dsb|encyklopedija|m}}
*: {{Z|hsb}}: {{W-|hsb|encyklopedija|m}}
* {{Z|es}}: {{W+|es|enciclopedia|m}}
* {{Z|sw}}: {{W-|sw|kamusi elezo}}, {{W-|sw|ensaiklopidia}}
* {{Z|sv}}: {{W+|sv|encyklopedi|g}}, {{W+|sv|konversationslexikon|nt}}, {{W+|sv|uppslagsverk|nt}}
* {{Z|tg}}: {{W+|tg|донишнома}}, {{W-|tg|доиратулмаориф}}, {{W-|tg|энсиклопедия}}
* {{Z|tl}}: {{W-|tl|santalaalaman}}, {{W+|tl|ensiklopedya}}
* {{Z|th}}: {{W+|th|สารานุกรม}}
* {{Z|te}}: {{W-|te|విజ్ఞానసర్వస్వం}}
* {{Z|crh}}: {{W-|crh|entsiklopediya}}
* {{Z|tt}}: {{W-|tt|энциклопедия}}
* {{Z|ti}}: {{W-|ti|ኢንሳይክሎፐድያ}}, {{W-|ti|መዝገበ ፍልጠት}}
* {{Z|tr}}: {{W+|tr|ansiklopedi}}
* {{Z|tk}}: {{W-|tk|ensiklopediýa}}
* {{Z|bo}}: {{W-|bo|ཤེས་བྱ་ཀུན་ཁྱབ་མཛོད|lit=treasury of the universally knowable}}, {{W-|bo|རིག་མཛོད|lit=knowledge treasury}}
* {{Z|ur}}: {{W-|ur|دائِرَۃُ الْمَعارِف|tr=dāiratu l-ma'ārif}}, {{W-|ur|کِتابُ الْمَعارِف|tr=kitābu l-ma'ārif}}, {{W+|ur|قامُوس|n}}, {{W-|ur|اِنْسائِیکِلوپِیڈْیا}}, {{W-|ur|موسوعہ|tr=mausū'a}}, {{W-|ur|احصاء العلوم|tr=ihsā'-ul-u'lūm}}, {{W-|ur|قاموس الاحلام|tr=qāmūs-al-ahlām}}
* {{Z|uk}}: {{W-|uk|енциклопе́дія|m}}
* {{Z|vi}}: {{W+|vi|bách khoa toàn thư}}
* {{Z|vo}}: {{W+|vo|sikloped}}, {{W-|vo|züklopäod}}, {{W-|vo|realasikloped}}
* {{Z|cy}}: {{W+|cy|gwyddoniadur|n}}
* {{Z|km}}: {{W-|km|វិជ្ជាសមោធាន}}, {{W-|km|សៀវភៅប្រជុំវិជ្ជា}}
* {{Z|sah}}: {{W-|sah|энциклопедия}}
* {{Z|yi}}: {{W-|yi|ענציקלאָפּעדיע|m}}
* {{Z|el}}: {{W+|el|εγκυκλοπαίδεια|m}}
*: Kevn: {{W-|grc|πανδέκτης|n}}
* {{Z|zea}}: {{W-|zea|encyclopedie}}
{{werger-bin}}
:{{werger çavkanî|en|encyclopedia|beş=Ev qismê Wergerê|oldid=84819598}}
i2xscpc0inkses77bhdjidauq9815vi
dînî
0
11693
6531955
6338855
2026-09-17T14:44:07Z
Wikihez
7344
Kurmancî/Mane 1: mane, hevmane hatin guhartin; Kurmancî/Etîmolojî: beşa «Etîmolojî» hate nûkirin (DefSideBoxes)
6531955
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-IPA}}
* {{deng|ku|LL-Q36163 (kmr)-Ebulfîda-dînî.wav|dever=Bidlîs}}
* {{ku-kîte}}
=== Rengdêr ===
{{rengdêr|ku}}
# [[olî]], [[ayînî]], [[mezhebî]], têkildarî [[dîn]]ekî yan [[dîn]]an
==== Jê ====
* [[dînîtî]]
==== Etîmolojî ====
[[dîn]] (1) + [[-î]]
==== Werger ====
{{werger-ser}}
{{werger-bin}}
=== 2. dîn, bêhiş, şêtî ===
==== Mane ====
{{cure?|ku}}
# [[sirişt|Sirişt]]a kesên [[dîn]].
#: {{hevmane|ku|bêhişî|şêtî|bêaqilî}}
==== Etîmolojî ====
Ji {{paş|ku|dîn<id:bêhiş>|-î}}.
==== Jê ====
* [[dînîtî]]
==== Werger ====
{{werger-ser}}
{{werger-bin}}
=== 3. roj ===
{{cure?|ku}}
# li: [[paşînî]], [[eynî]], [[sibeynî]], [[dwênî]]
==== Ji wêjeya klasîk ====
* {{J:Feqiyê Teyran|jêgirtin=Lê ricû ket sihhetê</br>'''Dîniyek''' tirsek hewayek</br>illetek rîşek webayek}}
==== Werger ====
{{werger-ser}}
{{werger-bin}}
=== Formeke navdêrê ===
{{ku-tew-nav|dîn|nêr|çemandî|binavkirî}}
{{navdêr|ku|form=erê}}
# ''Rewşa [[çemandî]] ya yekjimar a binavkirî ya'' '''[[dîn]]'''.
iry7ut5l308kgh3orp83a0fkdyz4kfh
6531956
6531955
2026-09-17T14:46:33Z
Wikihez
7344
[[Bikarhêner:Wikihez/templateScript2.js|TS]]: sererastkirin
6531956
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-IPA}}
* {{deng|ku|LL-Q36163 (kmr)-Ebulfîda-dînî.wav|dever=Bidlîs}}
* {{ku-kîte}}
=== Rengdêr ===
{{rengdêr|ku}}
# Têkildarî [[dîn]]ekî yan [[dîn]]an.
#: {{hevmane|ku|olî|ayînî|mezhebî}}
==== Etîmolojî ====
Ji {{qertaf|ku|dîn<t:bawerî><id:bawerî>|-î}}.
==== Werger ====
{{werger-ser}}
{{werger-bin}}
=== Navdêr ===
{{navdêr|ku}}
# [[sirişt|Sirişt]]a kesên [[dîn]].
#: {{hevmane|ku|bêhişî|şêtî|bêaqilî}}
* {{J:Feqiyê Teyran|jêgirtin=Lê ricû ket sihhetê</br>'''Dîniyek''' tirsek hewayek</br>illetek rîşek webayek}}
==== Etîmolojî ====
Ji {{paş|ku|dîn<id:bêhiş><t:bêhiş>|-î}}.
==== Jê ====
* {{g|ku|dînîtî}}
==== Werger ====
{{werger-ser}}
{{werger-bin}}
=== Hoker ===
{{hoker|ku}}
# li: [[paşînî]], [[eynî]], [[sibeynî]], [[dwênî]]
==== Werger ====
{{werger-ser}}
{{werger-bin}}
=== Formeke navdêrê ===
{{ku-tew-nav|dîn|nêr|çemandî|binavkirî}}
{{navdêr|ku|form=erê}}
# ''Rewşa [[çemandî]] ya yekjimar a binavkirî ya'' '''[[dîn]]'''.
4l950bcbd8cvyqrz9gvwdwf511dewnr
6531957
6531956
2026-09-17T14:46:50Z
Wikihez
7344
/* Navdêr */ (bi [[MediaWiki:Gadget-AjaxEdit.js|AjaxEdit]]ê re)
6531957
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-IPA}}
* {{deng|ku|LL-Q36163 (kmr)-Ebulfîda-dînî.wav|dever=Bidlîs}}
* {{ku-kîte}}
=== Rengdêr ===
{{rengdêr|ku}}
# Têkildarî [[dîn]]ekî yan [[dîn]]an.
#: {{hevmane|ku|olî|ayînî|mezhebî}}
==== Etîmolojî ====
Ji {{qertaf|ku|dîn<t:bawerî><id:bawerî>|-î}}.
==== Werger ====
{{werger-ser}}
{{werger-bin}}
=== Navdêr ===
{{navdêr|ku}}
# [[sirişt|Sirişt]]a kesên [[dîn]].
#: {{hevmane|ku|bêhişî|şêtî|bêaqilî}}
#* {{J:Feqiyê Teyran|jêgirtin=Lê ricû ket sihhetê</br>'''Dîniyek''' tirsek hewayek</br>illetek rîşek webayek}}
==== Etîmolojî ====
Ji {{paş|ku|dîn<id:bêhiş><t:bêhiş>|-î}}.
==== Jê ====
* {{g|ku|dînîtî}}
==== Werger ====
{{werger-ser}}
{{werger-bin}}
=== Hoker ===
{{hoker|ku}}
# li: [[paşînî]], [[eynî]], [[sibeynî]], [[dwênî]]
==== Werger ====
{{werger-ser}}
{{werger-bin}}
=== Formeke navdêrê ===
{{ku-tew-nav|dîn|nêr|çemandî|binavkirî}}
{{navdêr|ku|form=erê}}
# ''Rewşa [[çemandî]] ya yekjimar a binavkirî ya'' '''[[dîn]]'''.
ry5340c4e3f3fhnnlyb34ftfl7xejys
6531959
6531957
2026-09-17T15:41:09Z
Balyozbot
7867
[[Special:PermanentLink/6298023|Bot]]: rêzkirina beşan
6531959
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-IPA}}
* {{deng|ku|LL-Q36163 (kmr)-Ebulfîda-dînî.wav|dever=Bidlîs}}
* {{ku-kîte}}
=== Rengdêr ===
{{rengdêr|ku}}
# Têkildarî [[dîn]]ekî yan [[dîn]]an.
#: {{hevmane|ku|olî|ayînî|mezhebî}}
==== Etîmolojî ====
Ji {{qertaf|ku|dîn<t:bawerî><id:bawerî>|-î}}.
==== Werger ====
{{werger-ser}}
{{werger-bin}}
=== Navdêr ===
{{navdêr|ku}}
# [[sirişt|Sirişt]]a kesên [[dîn]].
#: {{hevmane|ku|bêhişî|şêtî|bêaqilî}}
#* {{J:Feqiyê Teyran|jêgirtin=Lê ricû ket sihhetê</br>'''Dîniyek''' tirsek hewayek</br>illetek rîşek webayek}}
==== Jê ====
* {{g|ku|dînîtî}}
==== Etîmolojî ====
Ji {{paş|ku|dîn<id:bêhiş><t:bêhiş>|-î}}.
==== Werger ====
{{werger-ser}}
{{werger-bin}}
=== Hoker ===
{{hoker|ku}}
# li: [[paşînî]], [[eynî]], [[sibeynî]], [[dwênî]]
==== Werger ====
{{werger-ser}}
{{werger-bin}}
=== Formeke navdêrê ===
{{ku-tew-nav|dîn|nêr|çemandî|binavkirî}}
{{navdêr|ku|form=erê}}
# ''Rewşa [[çemandî]] ya yekjimar a binavkirî ya'' '''[[dîn]]'''.
re55up1nyujsqtoi4uam09v2pxdwrhu
6531976
6531959
2026-09-17T16:12:07Z
Balyozbot
7867
Bot: Paqijiyên kozmetîk (Lînk paqij kir.)
6531976
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-IPA}}
* {{deng|ku|LL-Q36163 (kmr)-Ebulfîda-dînî.wav|dever=Bidlîs}}
* {{ku-kîte}}
=== Rengdêr ===
{{rengdêr|ku}}
# Têkildarî [[dîn]]ekî yan [[dîn]]an.
#: {{hevmane|ku|olî|ayînî|mezhebî}}
==== Etîmolojî ====
Ji {{qertaf|ku|dîn<t:bawerî><id:bawerî>|-î}}.
==== Werger ====
{{werger-ser}}
{{werger-bin}}
=== Navdêr ===
{{navdêr|ku}}
# [[sirişt|Sirişta]] kesên [[dîn]].
#: {{hevmane|ku|bêhişî|şêtî|bêaqilî}}
#* {{J:Feqiyê Teyran|jêgirtin=Lê ricû ket sihhetê</br>'''Dîniyek''' tirsek hewayek</br>illetek rîşek webayek}}
==== Jê ====
* {{g|ku|dînîtî}}
==== Etîmolojî ====
Ji {{paş|ku|dîn<id:bêhiş><t:bêhiş>|-î}}.
==== Werger ====
{{werger-ser}}
{{werger-bin}}
=== Hoker ===
{{hoker|ku}}
# li: [[paşînî]], [[eynî]], [[sibeynî]], [[dwênî]]
==== Werger ====
{{werger-ser}}
{{werger-bin}}
=== Formeke navdêrê ===
{{ku-tew-nav|dîn|nêr|çemandî|binavkirî}}
{{navdêr|ku|form=erê}}
# ''Rewşa [[çemandî]] ya yekjimar a binavkirî ya'' '''[[dîn]]'''.
pms4xehyzpqhrta1ctdaipbp84l27tn
-xane
0
11982
6531920
5636226
2026-09-17T12:56:15Z
Wikihez
7344
/* Etîmolojî */
6531920
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{IPA|ku|/'xɑ:nɛ/}}
=== Paşgir ===
{{paşgir|ku}}
# [[paşgir|Paşgirek]] e dikeve dû hin [[peyv]]an û [[mane]]ya ''[[xanî]]'' yan ''[[ode]]'' yan ''[[cih]]'' didiyê:
#: ''[[avdestxane]], [[firoşxane]], [[mêvanxane]], [[nivistinxane]], [[pirtûkxane]], [[sêwîxane]]''
==== Bide ber ====
* [[-geh]]
==== Etîmolojî ====
{{etîmon|ku|:drş|fa:خانه<tr:xâne><t:xanî, mal>|dar=1|metn=++}}
Hevreha [[xanî]].
==== Jê ====
{{paşgirbibîne|ku}}
* [[-xaneyî]]
==== Werger ====
{{werger-ser}}
* {{Z|tr}}: {{W+|tr|-hane}}
{{werger-bin}}
mpk1pl3l4inb7qi9lr8md38o274relo
6531929
6531920
2026-09-17T13:18:20Z
Wikihez
7344
Kurmancî/Paşgir: beşa «Etîmolojî» hate nûkirin (DefSideBoxes)
6531929
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{IPA|ku|/'xɑ:nɛ/}}
=== Paşgir ===
{{paşgir|ku}}
# [[paşgir|Paşgirek]] e dikeve dû hin [[peyv]]an û [[mane]]ya ''[[xanî]]'' yan ''[[ode]]'' yan ''[[cih]]'' didiyê:
#: ''[[avdestxane]], [[firoşxane]], [[mêvanxane]], [[nivistinxane]], [[pirtûkxane]], [[sêwîxane]]''
==== Bide ber ====
* [[-geh]]
==== Etîmolojî ====
{{etîmon|ku|:drş|fa:خانه<tr:xâne><t:xanî, mal><nediyar>|dar=1|metn=++}}
Hevreha [[xanî]].
==== Jê ====
{{paşgirbibîne|ku}}
* [[-xaneyî]]
==== Werger ====
{{werger-ser}}
* {{Z|tr}}: {{W+|tr|-hane}}
{{werger-bin}}
7vc20awzvfjhz114ua26z669v0tqe8w
6531933
6531929
2026-09-17T13:23:16Z
Wikihez
7344
/* Etîmolojî */ (bi [[MediaWiki:Gadget-AjaxEdit.js|AjaxEdit]]ê re)
6531933
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{IPA|ku|/'xɑ:nɛ/}}
=== Paşgir ===
{{paşgir|ku}}
# [[paşgir|Paşgirek]] e dikeve dû hin [[peyv]]an û [[mane]]ya ''[[xanî]]'' yan ''[[ode]]'' yan ''[[cih]]'' didiyê:
#: ''[[avdestxane]], [[firoşxane]], [[mêvanxane]], [[nivistinxane]], [[pirtûkxane]], [[sêwîxane]]''
==== Bide ber ====
* [[-geh]]
==== Etîmolojî ====
{{etîmon|ku|id=xanî, mal|:drş|fa:خانه<tr:xâne><t:xanî, mal><nediyar>|dar=1|metn=++}}
Hevreha [[xanî]].
==== Jê ====
{{paşgirbibîne|ku}}
* [[-xaneyî]]
==== Werger ====
{{werger-ser}}
* {{Z|tr}}: {{W+|tr|-hane}}
{{werger-bin}}
65gyl3d5717n18c5ui6qmxiv958bq8c
6531937
6531933
2026-09-17T13:41:06Z
Balyozbot
7867
[[Special:PermanentLink/6298023|Bot]]: rêzkirina beşan
6531937
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{IPA|ku|/'xɑ:nɛ/}}
=== Paşgir ===
{{paşgir|ku}}
# [[paşgir|Paşgirek]] e dikeve dû hin [[peyv]]an û [[mane]]ya ''[[xanî]]'' yan ''[[ode]]'' yan ''[[cih]]'' didiyê:
#: ''[[avdestxane]], [[firoşxane]], [[mêvanxane]], [[nivistinxane]], [[pirtûkxane]], [[sêwîxane]]''
==== Jê ====
{{paşgirbibîne|ku}}
* [[-xaneyî]]
==== Etîmolojî ====
{{etîmon|ku|id=xanî, mal|:drş|fa:خانه<tr:xâne><t:xanî, mal><nediyar>|dar=1|metn=++}}
Hevreha [[xanî]].
==== Bide ber ====
* [[-geh]]
==== Werger ====
{{werger-ser}}
* {{Z|tr}}: {{W+|tr|-hane}}
{{werger-bin}}
a27w5vhubpgh7c2a790net71ccvbhqy
6531940
6531937
2026-09-17T13:55:02Z
Wikihez
7344
/* Paşgir */ [[Bikarhêner:Wikihez/templateScript2.js|TS]]: sererastkirin
6531940
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{IPA|ku|/'xɑ:nɛ/}}
=== Paşgir ===
{{paşgir|ku}}
# [[paşgir|Paşgirek]] e dikeve dû hin [[peyv]]an û [[mane]]ya ''[[xanî]]'' yan ''[[ode]]'' yan ''[[cih]]'' didiyê
#: {{qfnim|ku|avdest|avdestxane}}
#: {{qfnim|ku|firotin<cuda:firoş->|firoşxane}}
#: {{qfnim|ku|mêvan|mêvanxane}}
#: {{qfnim|ku|nivistin|nivistinxane}}
#: {{qfnim|ku|pirtûk|pirtûkxane}}
#: {{qfnim|ku|sêwî|sêwîxane}}
==== Jê ====
{{paşgirbibîne|ku}}
* {{g|ku|-xaneyî}}
==== Etîmolojî ====
{{etîmon|ku|id=xanî, mal|:drş|fa:خانه<tr:xâne><t:xanî, mal><nediyar>|dar=1|metn=++}}
Hevreha [[xanî]].
==== Bide ber ====
* {{g|ku|-geh}}
==== Werger ====
{{werger-ser}}
* {{Z|tr}}: {{W+|tr|-hane}}
{{werger-bin}}
7l5u2px5z732lnzxyckjghqodc90wmr
çay
0
16836
6531931
6437966
2026-09-17T13:22:18Z
Wikihez
7344
/* Etîmolojî */
6531931
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{IPA|ku|/t͡ʃʰɑːj/}}
* {{ku-kîte}}
=== Navdêr ===
[[Wêne:Cup of tea, Scotland.jpg|thumb|Fincaneke '''çayê'''. (1)]]
[[Wêne:Teestrauch Detail.jpg|thumb|Riweka '''çayê'''. (2)]]
{{navdêr|ku|z=m}}, {{ç-ya req}}
# {{e|ku|vexwarin}} Vexwarinek e ku bi ava [[kelandin|kelandî]] tê çêkirin û [[reng]]ê wê nêzîkî [[qehweyî]] ye.
#: {{mk|ku|'''Çaya''' tehl/bêşekir, '''çaya''' şîrîn, '''çaya''' zelal, '''çaya''' tarî, '''çaya''' germ, '''çaya''' sar.}}
#: {{mk|ku|Fincaneke '''çayê''', piyaleke '''çayê'''.}}
#: {{mk|ku|Were em '''çayekê''' vexwin.}}
#: {{mk|ku|Hişyar be, '''çay''' germ e, bi xwe de [[riştin|nerêje]]!}}
#* {{jêder|ku|jêgirtin="Di vê bêhnikê de çiqasî dixwest ku çaydanekî '''çayê''' vexwara û bi ser de tam qutiyeke tûtina Xursê bikişanda."|Laleş Qaso: ''Sê şev û sê roj'', roman 2002}}
#* {{jêder-pirtûk|ku|jêgirtin=Got "heyran tu çi zû razayî, ka were em '''çayekê''' vexwin. Ez ji ser cî rabûm min got, "hema ez ji nizanim çawa di xew re çûme."|nivîskar=Mihemed Şarman|sernav=Pirça Winda|cîh=Stembol|weşanxane={{w|Weşanên Avesta|Avesta}}|sal=2016|çap=2|rûpel=82|isbn=978-605-5585-41-9}}
# {{e|ku|riwek}} [[devî|Deviyek]] anku [[teraş]]ek e ku ji [[pel]]ên wê [[vexwarin]]ek tê çêkirin.
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Bi alfabeyên din ====
* {{ku-ar|چای}}
* {{ku-kr|ч'ай}}
==== Formên alternatîv ====
{{kol3|ku
|ça
|çayê<qq:di hin devokan de wek forma ne[[çemandî]]/ne[[tewandî]] jî>
|çey-<qq:di hinek [[peyvên pêkhatî]] anku [[hevedudanî]] de, bo nimûne ''[[çeyxane]]''>
}}
==== Navê zanistî ====
* Navê zanistî yê riwekê: ''[[Camellia sinensis]]''
==== Jê ====
{{kol3|ku
|çaya kesk
|çaya reş
|çaydan
|çaydank
|çaygêr
|çaygêrî
|çayî
|çayvan
|çayvanî
|çayvexwer
|çayxane
|çayxanevan
|çayxanevanî
|çayxwer
|tovça
|tovçay
}}
==== Etîmolojî ====
{{etîmon|ku|id=vexwarina germ|:drş|zh:茶<t:çay>|dar=1}}
Herwiha ''[[ça]]'', taliyê ji {{ji|zh|ku|茶}} (bilêvkirina çîniya [[mandarînî]]: /chá/: çay, ça) û [[茶葉]] (/cháyè/: [[pel]]ên çayê).
{{veşêre-ser|Bêhtir malûmat û hevreh}}
Hevreha soranî {{g|ckb|چای}} û {{g|ckb|چا}}, zazakî ''{{g|zza|çay}}'' û ''{{g|zza|ça}}'', farisî {{g|fa|چای|tr=çay}} û {{g|fa|چا|tr=ça}}, tirkiya osmanî {{g|ota|چای|tr=çay}} û {{g|ota|چا|tr=ça}}, tirkiya niha {{g|tr|çay}}, siryanî [[ܟ݈ܐܝ]] (çay), azerî ''{{g|az|çay}}'', erebî {{g|ar|شای|tr=şay}}, peştûyî {{g|ur|چای|tr=çay}}, ûrdûyî {{g|ur|چای|tr=çay}} û {{g|ur|چا|tr=ça}}, hindî [[चाय]] (çāy)û [[चा]] (çā), {{hv|hy|չայ}} (čʿay), gurcî [[ჩაი]] (čay), {{hv|ru|чай}} (čay), bulgarî [[чай]] (čay), romanî ''[[ceai]]'' (/çay/), tirkmen(istan)î ''[[çaý]]'', qazaxî [[шай]] (şay), ozbekî ''[[choy]]'', albanî ''[[çaj]]'' (/çay/), çekî/kroatî/slovakî/slovenî ''[[čaj]]'' (/çay/), sirbî [[ча̏ј]] (/çay/), portugalî ''{{g|pt|chá}}''
Heman peyva çînî di çîniya [[minanî]] de bi eynî awayî tê nivîsîn lê wek ''/tê/'' tê gotin. Ew serekaniya peyva bi wateya "çay, ça" ya piraniya zimanên Rojavaya Ewropayê ye, bo nimûne: îngilîzî ''{{g|en|tea}}'', fransî ''{{g|fr|thé}}'', almanî ''{{g|de|Tee}}'', spanî ''{{g|es|té}}'', holendî ''[[thee]]'', danmarkî/norwecî/swêdî ''[[te]]'', fînlandî ''[[tee]]''...
Çay ji [[Çîn]]ê li seranserî dinyayê belav bûye. Di rastiyê de teqrîben li tevahiya dinyayê peyveke ji heman peyva çînî wergirtî tê bikaranîn. Sebebê cudahiya peyvên rojhilatî û rojavayî ew e ku yên rojhilatî ji çîniya [[mandarînî]] lê yên rojavayî ji çîniya [[minanî]] hatine wergirtin.
Tevî ku bi giştî peyva bi wateya ''çay, ça'' di zimanên Rojavaya Ewropayê de wek "tea, thé, Tee..." be jî, vê dawiyê bi manaya hinek çayên taybet herwiha forma rojhilatî wek ''[[chai]]'' jî ketiye zimanên Rojavaya Ewropayê jî.
;{{biçûk|Çavkanî
* {{Chyet}}
* {{Wals}}
* {{Tsabolov}}
}}
{{veşêre-bin}}
==== Herwiha binêre ====
{{kol3|ku
|çay [[dem]] kirin
|çay [[îkram]] kirin, çay [[pêşkêş]] kirin
|çay [[kelandin]]
|çay [[tê kirin]]
|çay [[vexwarin]]
}}
==== Werger ====
{{werger-ser}}
* {{Z|abx}}: {{W-|abx|simente}}
* {{Z|af}}: {{W+|af|tee}} {{n}}
* {{Z|aht}}: {{W-|aht|tsaey}}
* {{Z|sq}}: {{W+|sq|çaj}} {{n}}
* {{Z|de}}: {{W+|de|Tee|n}}
* {{Z|alt}}: {{W-|alt|чай}}
* {{Z|ast}}: {{W+|ast|te}} {{n}}
* {{Z|bm}}: {{W-|bm|lipton}}
* {{Z|eu}}: {{W+|eu|te}}
* {{Z|ba}}: {{W-|ba|сәй}}
* {{Z|br}}: {{W+|br|te}}
* {{Z|bg}}: {{W+|bg|чай|R=čaj}} {{n}}
* {{Z|cs}}: {{W+|cs|čaj}}
* {{Z|zh}}: {{W+|zh|茶|R=chá}}, {{W+|zh|茶叶|R=cháyè}}
* {{Z|cv}}: {{W-|cv|чей}}
* {{Z|da}}: {{W+|da|te}}
* {{Z|dlg}}: {{W-|dlg|чаай}}
* {{Z|id}}: {{W+|id|teh}}
* {{Z|ar}}: {{W+|ar|شاي|R=şay}} {{n}}
* {{Z|eo}}: {{W+|eo|teo}}
* {{Z|et}}: {{W+|et|tee}}
* {{Z|fa}}: {{W+|fa|چای|tr=çay}}
* {{Z|fo}}: {{W-|fo|te}}
* {{Z|fi}}: {{W+|fi|tee}}
* {{Z|fr}}: {{W+|fr|thé}}
* {{Z|fy}}: {{W-|fy|tee}} {{n}}
* {{Z|gd}}: {{W-|gd|tì}}
* {{Z|gwi}}: {{W-|gwi|lìdii}}
* {{Z|haw}}: {{W-|haw|kī}}
* {{Z|ha}}: {{W+|ha|shayi}}
* {{Z|hi}}: {{W+|hi|चाय|R=chāya}}
* {{Z|nl}}: {{W+|nl|thee}}
* {{Z|hoi}}: {{W-|hoi|tsay}}
* {{Z|io}}: {{W+|io|teo}}
* {{Z|en}}: {{W+|en|tea}}
* {{Z|ia}}: {{W+|ia|the}}
* {{Z|iu}}: {{W-|iu|ᑏ|R=[[tii]]}}
* {{Z|ga}}: {{W+|ga|tae}} {{n}}
* {{Z|it}}: {{W+|it|tè}} {{n}}
* {{Z|is}}: {{W+|is|te}} {{n}}
* {{Z|ja}}: {{W+|ja|茶|R=cha}}
* {{Z|krc}}: {{W-|krc|шай}}
* {{Z|ca}}: {{W+|ca|te}} {{n}}
* {{Z|ky}}: {{W+|ky|чай}}
* {{Z|ko}}: {{W+|ko|차|R=cha}}
* {{Z|co}}: {{W-|co|tè}}
* {{Z|kum}}: {{W-|kum|чай}}
* {{Z|lv}}: {{W+|lv|tēja}} {{mê}}
* {{Z|lt}}: {{W-|lt|arbata}}
* {{Z|lb}}: {{W-|lb|Téi}} {{n}}
* {{Z|mk}}: {{W-|mk|чај}} {{n}}
* {{Z|mg}}: {{W+|mg|dite}}
* {{Z|ms}}: {{W+|ms|teh}}
* {{Z|mt}}: {{W-|mt|te}}
* {{Z|gv}}: {{W-|gv|tey}}
* {{Z|hu}}: {{W+|hu|tea}}
* {{Z|moh}}: {{W-|moh|iekanerahtóhon}}
* {{Z|nap}}: {{W-|nap|ttè}}
* {{Z|nio}}: {{W-|nio|чаи}}
* {{Z|ngh}}: {{W-|ngh|teesi}}
* {{Z|nog}}: {{W-|nog|шай}}
* {{Z|no}}: {{W+|no|te}}
* {{Z|oc}}: {{W+|oc|tè}}
* {{Z|pap}}: {{W-|pap|té}}
* {{Z|pl}}: {{W+|pl|herbata}}
* {{Z|pt}}: {{W+|pt|chá}} {{n}}
* {{Z|kk}}: {{W+|kk|шай|tr=şay}}
* {{Z|ro}}: {{W+|ro|ceai}} {{n}}
* {{Z|ru}}: {{W+|ru|чай|R=tchaï}} {{n}}
* {{Z|se}}: {{W-|se|deadja}}
* {{Z|srn}}: {{W-|srn|te}}
* {{Z|sh}}: {{W+|sh|čaj}} {{n}}
* {{Z|sh}}: {{W+|sh|čaj}} {{n}}
* {{Z|sh}}: {{W-|sh|чај}} {{n}}
* {{Z|sk}}: {{W+|sk|čaj}} {{n}}
* {{Z|sl}}: {{W+|sl|čaj}} {{n}}
* {{Z|ses}}: {{W-|ses|attay}}
* {{Z|es}}: {{W+|es|té}} {{n}}
* {{Z|sw}}: {{W+|sw|chai}}
* {{Z|sv}}: {{W+|sv|te}} {{nt}}
* {{Z|zdj}}: {{W-|zdj|tshai}}
* {{Z|tl}}: {{W-|tl|tsaá}}
* {{Z|th}}: {{W+|th|ชา}}
* {{Z|crh}}: {{W-|crh|çay}}
* {{Z|tt}}: {{W+|tt|чәй}}
* {{Z|tet}}: {{W-|tet|xà}}
* {{Z|tr}}: {{W+|tr|çay}}
* {{Z|tk}}: {{W-|tk|çaý}}
* {{Z|kim}}: {{W-|kim|шэй}}
* {{Z|tyv}}: {{W-|tyv|шай}}
* {{Z|uk}}: {{W+|uk|чай|R=tchaï}} {{n}}
* {{Z|wa}}: {{W+|wa|té}} {{n}}
* {{Z|cy}}: {{W+|cy|te}} {{n}}
* {{Z|kjh}}: {{W-|kjh|чай}}
* {{Z|sah}}: {{W-|sah|чэй}}
* {{Z|el}}: {{W+|el|τσάι|R=tsái}} {{nt}}
* {{Z|zza}}: {{W|zza|çay|m}}
* {{Z|zu}}: {{W+|zu|ilitiye}}, {{W+|zu|itiye}}
{{werger-bin}}
=== Hevok ===
{{hevok|ku}}
# {{f|ku|devokî}} [[çawa]] [[yî]], tu [[çawa]] [[yî]]?
==== Etîmolojî ====
[[ça]] (2) + [[y]] ([[yî]])
==== Werger ====
{{werger-ser}}
{{werger-bin}}
== {{ziman|zza}} ==
=== Navdêr ===
[[Wêne:Rosehiptea.png|thumb|Çay (1)]]
{{navdêr|zza|z=n}}
# {{e|zza|vexwarin}} {{g|ku|çay}}
# {{e|zza|riwek}} {{g|ku|çay}}
== {{ziman|crh}} ==
=== Navdêr ===
{{navdêr|crh}}
# {{f|crh|vexwarin}} [[çay#ku|çay]]
== {{ziman|az}} ==
=== Navdêr ===
{{navdêr|az}}
# {{f|az|erdnîgarî}} {{g|ku|çem}}, {{g|ku|ro}}, {{g|ku|robar}}
=== Çavkanî ===
{{rûpela wergerê|az|çem}}
== {{ziman|tr}} ==
=== Bilêvkirin ===
* {{deng|tr|LL-Q256 (tur)-ToprakM-çay.wav|Deng|dever=}}
=== Navdêr ===
{{navdêr|tr}}
# {{g|ku|çay}}, [[ça]]
78i1xkfubcp4iu37a2u20paeok9zkld
xeter
0
17558
6531962
6361965
2026-09-17T15:51:06Z
Wikihez
7344
Kurmancî/Navdêr: beşa «Bide ber» hate nûkirin, beşa «Etîmolojî» hate nûkirin (DefSideBoxes)
6531962
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Navdêr ===
{{navdêr|ku|z=m}}
# [[rewş|Rewşa]] ku [[xerab]]iyê, [[tirs]]ê û [[nebaş]]iyê bi xwe re tîne
#: {{hevmane|ku|talûke|metirsî|bive}}
#* {{J:Melayê Cizîrî:Dîwana Melayê Cizîrî|jêgirtin='''Arifê weqt im û Me'rûf-i bila bête ji Kerx</br>Da binasî '''xeterên''' rê tu nebî xafil-i jê</br>Dicle ye lê di pir in bir birê pêl û ber û merx''}}
==== Tewandin ====
{{ku-tewîn-nav/nû|mê}}
==== Jê ====
* {{g|ku|xeternak}}
* {{g|ku|xeterî}}
* {{g|ku|xetirandin}}
==== Etîmolojî ====
Ji {{ji|ar|ku|خَطَر}}.
==== Bide ber ====
{{kol3|ku|cure=Bide ber
|zerar
|tirs
|mirin
|êş
|jan
}}
==== Werger ====
{{werger-ser}}
* {{Z|af}}: {{W-|af|gevaar}}
* {{Z|sq}}: {{W+|sq|rrezik|n}}
* {{Z|de}}: {{W+|de|Gefahr|m}}
* {{Z|as}}: {{W-|as|বিপদ}}
* {{Z|ast}}: {{W-|ast|peligru|n}}
* {{Z|az}}: {{W+|az|təhlükə}}
* {{Z|eu}}: {{W-|eu|arrisku}}
* {{Z|be}}: {{W-|be|небяспе́ка|m}}
* {{Z|bn}}: {{W+|bn|বিপদ}}
* {{Z|bg}}: {{W+|bg|опа́сност|m}}
* {{Z|my}}: {{W+|my|ဘယာ}}, {{W+|my|ဘေး}}, {{W+|my|အန္တရာယ်}}
* {{Z|ce}}: {{W-|ce|кхерам}}
* {{Z|cs}}: {{W+|cs|nebezpečí|nt}}
* {{Z|chr}}: {{W-|chr|ᎦᏂᏰᎩ}}
* {{Z|zh}}:
*: {{Z|yue}}: {{W-|yue|危險|tr=ngai4 him2}}
*: {{Z|cmn}}: {{W+|cmn|危險|tr=wēixiǎn}}
* {{Z|ckt}}: {{W-|ckt|гыаргыргын|tr=gyargyrgyn}}
* {{Z|da}}: {{W+|da|fare|g}}
* {{Z|id}}: {{W+|id|bahaya}}
* {{Z|ar}}: {{W-|ar|خَطَر|n}}
*: {{Z|arz}}: {{W-|arz|خطر|n|tr=ḵaṭar}}
* {{Z|hy}}: {{W+|hy|վտանգ}}
* {{Z|eo}}: {{W-|eo|danĝero}}
* {{Z|et}}: {{W+|et|oht}}, {{W-|et|hädaoht}}
* {{Z|fa}}: {{W+|fa|خطر|tr=xatar}}
* {{Z|fo}}: {{W-|fo|vandi|n}}
* {{Z|fi}}: {{W+|fi|vaara}}, {{W+|fi|uhka}}
* {{Z|fr}}: {{W+|fr|danger|n}}, {{W+|fr|péril}}
* {{Z|fur}}: {{W-|fur|pericul|n}}
* {{Z|gd}}: {{W-|gd|cunnart|n}}
* {{Z|gl}}: {{W+|gl|perigo|n}}
* {{Z|got}}: {{W-|got|𐌱𐌹𐍂𐌴𐌹𐌺𐌴𐌹|m}}, {{W-|got|𐍃𐌻𐌴𐌹𐌸𐌴𐌹|m}}
* {{Z|ka}}: {{W-|ka|საფრთხე}}
* {{Z|ht}}: {{W-|ht|danje}}
* {{Z|hi}}: {{W+|hi|जोखिम|n}}, {{W-|hi|ख़तरा|n}}, {{W+|hi|विपदा|m}}, {{W+|hi|प्रमाद|n}}, {{W+|hi|संकट|n}}, {{W+|hi|विपत्ति|m}}, {{W+|hi|खतरा|n}}
* {{Z|nl}}: {{W+|nl|gevaar|nt}}
* {{Z|he}}: {{W+|he|סַכָּנָה|m|tr=sakaná}}
* {{Z|io}}: {{W+|io|danjero}}
* {{Z|ig}}: {{W-|ig|itu}} {{W-|ig|egwu}}
* {{Z|ang}}: {{W-|ang|frēcennes|m}}
* {{Z|enm}}: {{W-|enm|peril}}, {{W-|enm|danger}}, {{W-|enm|dred}}
* {{Z|en}}: {{W+|en|danger}}
* {{Z|inh}}: {{W-|inh|кхерам}}
* {{Z|ga}}: {{W-|ga|contúirt|m}}, {{W-|ga|dainséar|n}}
* {{Z|it}}: {{W+|it|pericolo|n}}
* {{Z|is}}: {{W+|is|hætta|m}}
* {{Z|ja}}: {{W+|ja|危険|tr=きけん, kiken}}
* {{Z|jv}}: {{W-|jv|bebaya}}
* {{Z|kn}}: {{W+|kn|ಕುತ್ತ}}
* {{Z|ca}}: {{W+|ca|perill|n}}
* {{Z|ky}}: {{W+|ky|коркунуч}}
* {{Z|ki}}: {{W-|ki|ũgwati|c14}}
* {{Z|si}}: {{W-|si|අන්ත්රාව}}
* {{Z|ko}}: {{W+|ko|위험}}
* {{Z|lld}}: {{W-|lld|pericul|n}}
* {{Z|lad}}: {{W-|lad|perikolo|n}}
* {{Z|la}}: {{W+|la|periculum|nt}}
* {{Z|lv}}: {{W-|lv|briesmas|n}}, {{W-|lv|bīstamība|m}}
* {{Z|lo}}: {{W-|lo|ອັນຕະຣາຽ}}, {{W-|lo|ອັນຕະລາຍ}}
* {{Z|lt}}: {{W-|lt|pavojus|n}}
* {{Z|mk}}: {{W-|mk|опасност|m}}
* {{Z|ml}}: {{W+|ml|ആപത്ത്}}, {{W+|ml|അപായം}}
* {{Z|ms}}: {{W+|ms|bahaya}}
* {{Z|mt}}: {{W-|mt|periklu}}
* {{Z|mi}}: {{W-|mi|tatamate}}
* {{Z|hu}}: {{W+|hu|veszély}}
* {{Z|mn}}:
*: Kirîlî: {{W+|mn|аюул}}, {{W-|mn|зэтгэр}}
* {{Z|nci}}: {{W-|nci|ohuihcāyōtl}}
* {{Z|no}}:
*: {{Z|nb}}: {{W+|nb|fare|n}}
* {{Z|oc}}: {{W+|oc|perilh|n}}
* {{Z|om}}: {{W-|om|balaa}}
* {{Z|ug}}: {{W-|ug|خەتەر}}
* {{Z|uz}}: {{W+|uz|xatar}}, {{W+|uz|xavflilik}}
* {{Z|ps}}: {{W+|ps|خطر|n|tr=xatár}}
* {{Z|pdt}}: {{W-|pdt|Jefoa|m}}
* {{Z|pl}}: {{W+|pl|niebezpieczeństwo|nt}}
* {{Z|pt}}: {{W+|pt|perigo|n}}
* {{Z|kk}}: {{W-|kk|қауіп}}, {{W-|kk|қатер}}, {{W-|kk|қауіптілік}}
* {{Z|rm}}: {{W-|rm|privel|n}}, {{W-|rm|prighel|n}}, {{W-|rm|prievel|n}}
* {{Z|ro}}: {{W+|ro|pericol|nt}}, {{W+|ro|primejdie|m}}
* {{Z|ru}}: {{W+|ru|опа́сность|m}}
* {{Z|ssy}}: {{W-|ssy|siraye}}
* {{Z|osx}}: {{W-|osx|fāra|m}}, {{W-|osx|fār|n|m}}, {{W-|osx|gifār|nt}}
* {{Z|sa}}: {{W+|sa|त्यजस्|nt}}
* {{Z|sc}}: {{W-|sc|perículu}}, {{W-|sc|perígulu}}, {{W-|sc|pirígulu}}
* {{Z|scn}}: {{W+|scn|pirìculu}}, {{W+|scn|perìculu}}, {{W+|scn|prìculu}}
* {{Z|sh}}:
*: Kirîlî: {{W-|sh|опа́сно̄ст|m}}
*: Latînî: {{W+|sh|opásnōst|m}}
* {{Z|sk}}: {{W-|sk|nebezpečie|nt}}
* {{Z|sl}}: {{W+|sl|nevarnost|m}}
* {{Z|so}}: {{W-|so|khatar}}
* {{Z|es}}: {{W+|es|peligro|n}}
* {{Z|sw}}: {{W+|sw|hatari}}
* {{Z|sv}}: {{W+|sv|fara|g}}
* {{Z|tg}}: {{W+|tg|хатар}}, {{W+|tg|хавф}}
* {{Z|tl}}: {{W+|tl|panganib}}
* {{Z|ta}}: {{W+|ta|அபாயம்}}, {{W+|ta|ஆபத்து}}
* {{Z|th}}: {{W+|th|อันตราย}}
* {{Z|te}}: {{W+|te|ప్రమాదము}}
* {{Z|tt}}: {{W-|tt|куркыныч}}
* {{Z|tr}}: {{W+|tr|tehlike}}
* {{Z|tk}}: {{W-|tk|howp}}, {{W-|tk|hovp}}
* {{Z|xto}}: {{W-|xto|sanu}}
* {{Z|txb}}: {{W-|txb|ñyātse}}
* {{Z|tyv}}: {{W-|tyv|айыыл}}
* {{Z|ur}}: {{W-|ur|جوکھم|n|tr=jokhim}}, {{W-|ur|خطرہ|n|tr=xatra}}
* {{Z|uk}}: {{W-|uk|небезпе́ка|m}}
* {{Z|vec}}: {{W-|vec|pericoło|n}}, {{W-|vec|pericol}}, {{W-|vec|pericolo}}, {{W-|vec|perigoło}}
* {{Z|vi}}: {{W+|vi|nguy hiểm}}
* {{Z|vo}}: {{W-|vo|pölig}}
* {{Z|wa}}: {{W+|wa|dandjî|n}}
* {{Z|cy}}: {{W+|cy|perygl|n}}, {{W-|cy|peryglau|n-p}}
* {{Z|km}}: {{W+|km|គ្រោះថ្នាក់}}
* {{Z|xh}}: {{W-|xh|ingozi}}
* {{Z|yi}}: {{W-|yi|געפאַר|m|tr=gefar}}
* {{Z|el}}: {{W+|el|κίνδυνος|n}}
*: Kevn: {{W-|grc|κίνδυνος|n}}
* {{Z|zu}}: {{W+|zu|ingozi}}
{{werger-bin}}
:{{werger çavkanî|en|danger|beş=Ev qismê Wergerê|oldid=75479878}}
=== Rengdêr ===
{{rengdêr|ku}}
# {{guherto|xeternak}}
#: ''Ev kar xeter e.'' (Ev kar xeternak e, bitalûke ye, [[bive]] ye.)
==== Anagram ====
* {{anagram|ku|a=eertx|Exter|exter}}
52aysebix6idu9x7yxtr6jdjkop9znx
6531963
6531962
2026-09-17T15:51:49Z
Wikihez
7344
/* Navdêr */ (bi [[MediaWiki:Gadget-AjaxEdit.js|AjaxEdit]]ê re)
6531963
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Navdêr ===
{{navdêr|ku|z=m}}
# [[rewş|Rewşa]] ku [[xerab]]iyê, [[tirs]]ê û [[nebaş]]iyê bi xwe re tîne
#: {{hevmane|ku|talûke|metirsî|bive}}
#* {{J:Melayê Cizîrî:Dîwana Melayê Cizîrî|jêgirtin='Arifê weqt im û Me'rûf-i bila bête ji Kerx</br>Da binasî '''xeterên''' rê tu nebî xafil-i jê</br>Dicle ye lê di pir in bir birê pêl û ber û merx}}
==== Tewandin ====
{{ku-tewîn-nav/nû|mê}}
==== Jê ====
* {{g|ku|xeternak}}
* {{g|ku|xeterî}}
* {{g|ku|xetirandin}}
==== Etîmolojî ====
Ji {{ji|ar|ku|خَطَر}}.
==== Bide ber ====
{{kol3|ku|cure=Bide ber
|zerar
|tirs
|mirin
|êş
|jan
}}
==== Werger ====
{{werger-ser}}
* {{Z|af}}: {{W-|af|gevaar}}
* {{Z|sq}}: {{W+|sq|rrezik|n}}
* {{Z|de}}: {{W+|de|Gefahr|m}}
* {{Z|as}}: {{W-|as|বিপদ}}
* {{Z|ast}}: {{W-|ast|peligru|n}}
* {{Z|az}}: {{W+|az|təhlükə}}
* {{Z|eu}}: {{W-|eu|arrisku}}
* {{Z|be}}: {{W-|be|небяспе́ка|m}}
* {{Z|bn}}: {{W+|bn|বিপদ}}
* {{Z|bg}}: {{W+|bg|опа́сност|m}}
* {{Z|my}}: {{W+|my|ဘယာ}}, {{W+|my|ဘေး}}, {{W+|my|အန္တရာယ်}}
* {{Z|ce}}: {{W-|ce|кхерам}}
* {{Z|cs}}: {{W+|cs|nebezpečí|nt}}
* {{Z|chr}}: {{W-|chr|ᎦᏂᏰᎩ}}
* {{Z|zh}}:
*: {{Z|yue}}: {{W-|yue|危險|tr=ngai4 him2}}
*: {{Z|cmn}}: {{W+|cmn|危險|tr=wēixiǎn}}
* {{Z|ckt}}: {{W-|ckt|гыаргыргын|tr=gyargyrgyn}}
* {{Z|da}}: {{W+|da|fare|g}}
* {{Z|id}}: {{W+|id|bahaya}}
* {{Z|ar}}: {{W-|ar|خَطَر|n}}
*: {{Z|arz}}: {{W-|arz|خطر|n|tr=ḵaṭar}}
* {{Z|hy}}: {{W+|hy|վտանգ}}
* {{Z|eo}}: {{W-|eo|danĝero}}
* {{Z|et}}: {{W+|et|oht}}, {{W-|et|hädaoht}}
* {{Z|fa}}: {{W+|fa|خطر|tr=xatar}}
* {{Z|fo}}: {{W-|fo|vandi|n}}
* {{Z|fi}}: {{W+|fi|vaara}}, {{W+|fi|uhka}}
* {{Z|fr}}: {{W+|fr|danger|n}}, {{W+|fr|péril}}
* {{Z|fur}}: {{W-|fur|pericul|n}}
* {{Z|gd}}: {{W-|gd|cunnart|n}}
* {{Z|gl}}: {{W+|gl|perigo|n}}
* {{Z|got}}: {{W-|got|𐌱𐌹𐍂𐌴𐌹𐌺𐌴𐌹|m}}, {{W-|got|𐍃𐌻𐌴𐌹𐌸𐌴𐌹|m}}
* {{Z|ka}}: {{W-|ka|საფრთხე}}
* {{Z|ht}}: {{W-|ht|danje}}
* {{Z|hi}}: {{W+|hi|जोखिम|n}}, {{W-|hi|ख़तरा|n}}, {{W+|hi|विपदा|m}}, {{W+|hi|प्रमाद|n}}, {{W+|hi|संकट|n}}, {{W+|hi|विपत्ति|m}}, {{W+|hi|खतरा|n}}
* {{Z|nl}}: {{W+|nl|gevaar|nt}}
* {{Z|he}}: {{W+|he|סַכָּנָה|m|tr=sakaná}}
* {{Z|io}}: {{W+|io|danjero}}
* {{Z|ig}}: {{W-|ig|itu}} {{W-|ig|egwu}}
* {{Z|ang}}: {{W-|ang|frēcennes|m}}
* {{Z|enm}}: {{W-|enm|peril}}, {{W-|enm|danger}}, {{W-|enm|dred}}
* {{Z|en}}: {{W+|en|danger}}
* {{Z|inh}}: {{W-|inh|кхерам}}
* {{Z|ga}}: {{W-|ga|contúirt|m}}, {{W-|ga|dainséar|n}}
* {{Z|it}}: {{W+|it|pericolo|n}}
* {{Z|is}}: {{W+|is|hætta|m}}
* {{Z|ja}}: {{W+|ja|危険|tr=きけん, kiken}}
* {{Z|jv}}: {{W-|jv|bebaya}}
* {{Z|kn}}: {{W+|kn|ಕುತ್ತ}}
* {{Z|ca}}: {{W+|ca|perill|n}}
* {{Z|ky}}: {{W+|ky|коркунуч}}
* {{Z|ki}}: {{W-|ki|ũgwati|c14}}
* {{Z|si}}: {{W-|si|අන්ත්රාව}}
* {{Z|ko}}: {{W+|ko|위험}}
* {{Z|lld}}: {{W-|lld|pericul|n}}
* {{Z|lad}}: {{W-|lad|perikolo|n}}
* {{Z|la}}: {{W+|la|periculum|nt}}
* {{Z|lv}}: {{W-|lv|briesmas|n}}, {{W-|lv|bīstamība|m}}
* {{Z|lo}}: {{W-|lo|ອັນຕະຣາຽ}}, {{W-|lo|ອັນຕະລາຍ}}
* {{Z|lt}}: {{W-|lt|pavojus|n}}
* {{Z|mk}}: {{W-|mk|опасност|m}}
* {{Z|ml}}: {{W+|ml|ആപത്ത്}}, {{W+|ml|അപായം}}
* {{Z|ms}}: {{W+|ms|bahaya}}
* {{Z|mt}}: {{W-|mt|periklu}}
* {{Z|mi}}: {{W-|mi|tatamate}}
* {{Z|hu}}: {{W+|hu|veszély}}
* {{Z|mn}}:
*: Kirîlî: {{W+|mn|аюул}}, {{W-|mn|зэтгэр}}
* {{Z|nci}}: {{W-|nci|ohuihcāyōtl}}
* {{Z|no}}:
*: {{Z|nb}}: {{W+|nb|fare|n}}
* {{Z|oc}}: {{W+|oc|perilh|n}}
* {{Z|om}}: {{W-|om|balaa}}
* {{Z|ug}}: {{W-|ug|خەتەر}}
* {{Z|uz}}: {{W+|uz|xatar}}, {{W+|uz|xavflilik}}
* {{Z|ps}}: {{W+|ps|خطر|n|tr=xatár}}
* {{Z|pdt}}: {{W-|pdt|Jefoa|m}}
* {{Z|pl}}: {{W+|pl|niebezpieczeństwo|nt}}
* {{Z|pt}}: {{W+|pt|perigo|n}}
* {{Z|kk}}: {{W-|kk|қауіп}}, {{W-|kk|қатер}}, {{W-|kk|қауіптілік}}
* {{Z|rm}}: {{W-|rm|privel|n}}, {{W-|rm|prighel|n}}, {{W-|rm|prievel|n}}
* {{Z|ro}}: {{W+|ro|pericol|nt}}, {{W+|ro|primejdie|m}}
* {{Z|ru}}: {{W+|ru|опа́сность|m}}
* {{Z|ssy}}: {{W-|ssy|siraye}}
* {{Z|osx}}: {{W-|osx|fāra|m}}, {{W-|osx|fār|n|m}}, {{W-|osx|gifār|nt}}
* {{Z|sa}}: {{W+|sa|त्यजस्|nt}}
* {{Z|sc}}: {{W-|sc|perículu}}, {{W-|sc|perígulu}}, {{W-|sc|pirígulu}}
* {{Z|scn}}: {{W+|scn|pirìculu}}, {{W+|scn|perìculu}}, {{W+|scn|prìculu}}
* {{Z|sh}}:
*: Kirîlî: {{W-|sh|опа́сно̄ст|m}}
*: Latînî: {{W+|sh|opásnōst|m}}
* {{Z|sk}}: {{W-|sk|nebezpečie|nt}}
* {{Z|sl}}: {{W+|sl|nevarnost|m}}
* {{Z|so}}: {{W-|so|khatar}}
* {{Z|es}}: {{W+|es|peligro|n}}
* {{Z|sw}}: {{W+|sw|hatari}}
* {{Z|sv}}: {{W+|sv|fara|g}}
* {{Z|tg}}: {{W+|tg|хатар}}, {{W+|tg|хавф}}
* {{Z|tl}}: {{W+|tl|panganib}}
* {{Z|ta}}: {{W+|ta|அபாயம்}}, {{W+|ta|ஆபத்து}}
* {{Z|th}}: {{W+|th|อันตราย}}
* {{Z|te}}: {{W+|te|ప్రమాదము}}
* {{Z|tt}}: {{W-|tt|куркыныч}}
* {{Z|tr}}: {{W+|tr|tehlike}}
* {{Z|tk}}: {{W-|tk|howp}}, {{W-|tk|hovp}}
* {{Z|xto}}: {{W-|xto|sanu}}
* {{Z|txb}}: {{W-|txb|ñyātse}}
* {{Z|tyv}}: {{W-|tyv|айыыл}}
* {{Z|ur}}: {{W-|ur|جوکھم|n|tr=jokhim}}, {{W-|ur|خطرہ|n|tr=xatra}}
* {{Z|uk}}: {{W-|uk|небезпе́ка|m}}
* {{Z|vec}}: {{W-|vec|pericoło|n}}, {{W-|vec|pericol}}, {{W-|vec|pericolo}}, {{W-|vec|perigoło}}
* {{Z|vi}}: {{W+|vi|nguy hiểm}}
* {{Z|vo}}: {{W-|vo|pölig}}
* {{Z|wa}}: {{W+|wa|dandjî|n}}
* {{Z|cy}}: {{W+|cy|perygl|n}}, {{W-|cy|peryglau|n-p}}
* {{Z|km}}: {{W+|km|គ្រោះថ្នាក់}}
* {{Z|xh}}: {{W-|xh|ingozi}}
* {{Z|yi}}: {{W-|yi|געפאַר|m|tr=gefar}}
* {{Z|el}}: {{W+|el|κίνδυνος|n}}
*: Kevn: {{W-|grc|κίνδυνος|n}}
* {{Z|zu}}: {{W+|zu|ingozi}}
{{werger-bin}}
:{{werger çavkanî|en|danger|beş=Ev qismê Wergerê|oldid=75479878}}
=== Rengdêr ===
{{rengdêr|ku}}
# {{guherto|xeternak}}
#: ''Ev kar xeter e.'' (Ev kar xeternak e, bitalûke ye, [[bive]] ye.)
==== Anagram ====
* {{anagram|ku|a=eertx|Exter|exter}}
m7k33tfzjbwjj0k84ppxn9z7l0syi5e
6531964
6531963
2026-09-17T15:54:13Z
Wikihez
7344
/* Rengdêr */ (bi [[MediaWiki:Gadget-AjaxEdit.js|AjaxEdit]]ê re)
6531964
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Navdêr ===
{{navdêr|ku|z=m}}
# [[rewş|Rewşa]] ku [[xerab]]iyê, [[tirs]]ê û [[nebaş]]iyê bi xwe re tîne
#: {{hevmane|ku|talûke|metirsî|bive}}
#* {{J:Melayê Cizîrî:Dîwana Melayê Cizîrî|jêgirtin='Arifê weqt im û Me'rûf-i bila bête ji Kerx</br>Da binasî '''xeterên''' rê tu nebî xafil-i jê</br>Dicle ye lê di pir in bir birê pêl û ber û merx}}
==== Tewandin ====
{{ku-tewîn-nav/nû|mê}}
==== Jê ====
* {{g|ku|xeternak}}
* {{g|ku|xeterî}}
* {{g|ku|xetirandin}}
==== Etîmolojî ====
Ji {{ji|ar|ku|خَطَر}}.
==== Bide ber ====
{{kol3|ku|cure=Bide ber
|zerar
|tirs
|mirin
|êş
|jan
}}
==== Werger ====
{{werger-ser}}
* {{Z|af}}: {{W-|af|gevaar}}
* {{Z|sq}}: {{W+|sq|rrezik|n}}
* {{Z|de}}: {{W+|de|Gefahr|m}}
* {{Z|as}}: {{W-|as|বিপদ}}
* {{Z|ast}}: {{W-|ast|peligru|n}}
* {{Z|az}}: {{W+|az|təhlükə}}
* {{Z|eu}}: {{W-|eu|arrisku}}
* {{Z|be}}: {{W-|be|небяспе́ка|m}}
* {{Z|bn}}: {{W+|bn|বিপদ}}
* {{Z|bg}}: {{W+|bg|опа́сност|m}}
* {{Z|my}}: {{W+|my|ဘယာ}}, {{W+|my|ဘေး}}, {{W+|my|အန္တရာယ်}}
* {{Z|ce}}: {{W-|ce|кхерам}}
* {{Z|cs}}: {{W+|cs|nebezpečí|nt}}
* {{Z|chr}}: {{W-|chr|ᎦᏂᏰᎩ}}
* {{Z|zh}}:
*: {{Z|yue}}: {{W-|yue|危險|tr=ngai4 him2}}
*: {{Z|cmn}}: {{W+|cmn|危險|tr=wēixiǎn}}
* {{Z|ckt}}: {{W-|ckt|гыаргыргын|tr=gyargyrgyn}}
* {{Z|da}}: {{W+|da|fare|g}}
* {{Z|id}}: {{W+|id|bahaya}}
* {{Z|ar}}: {{W-|ar|خَطَر|n}}
*: {{Z|arz}}: {{W-|arz|خطر|n|tr=ḵaṭar}}
* {{Z|hy}}: {{W+|hy|վտանգ}}
* {{Z|eo}}: {{W-|eo|danĝero}}
* {{Z|et}}: {{W+|et|oht}}, {{W-|et|hädaoht}}
* {{Z|fa}}: {{W+|fa|خطر|tr=xatar}}
* {{Z|fo}}: {{W-|fo|vandi|n}}
* {{Z|fi}}: {{W+|fi|vaara}}, {{W+|fi|uhka}}
* {{Z|fr}}: {{W+|fr|danger|n}}, {{W+|fr|péril}}
* {{Z|fur}}: {{W-|fur|pericul|n}}
* {{Z|gd}}: {{W-|gd|cunnart|n}}
* {{Z|gl}}: {{W+|gl|perigo|n}}
* {{Z|got}}: {{W-|got|𐌱𐌹𐍂𐌴𐌹𐌺𐌴𐌹|m}}, {{W-|got|𐍃𐌻𐌴𐌹𐌸𐌴𐌹|m}}
* {{Z|ka}}: {{W-|ka|საფრთხე}}
* {{Z|ht}}: {{W-|ht|danje}}
* {{Z|hi}}: {{W+|hi|जोखिम|n}}, {{W-|hi|ख़तरा|n}}, {{W+|hi|विपदा|m}}, {{W+|hi|प्रमाद|n}}, {{W+|hi|संकट|n}}, {{W+|hi|विपत्ति|m}}, {{W+|hi|खतरा|n}}
* {{Z|nl}}: {{W+|nl|gevaar|nt}}
* {{Z|he}}: {{W+|he|סַכָּנָה|m|tr=sakaná}}
* {{Z|io}}: {{W+|io|danjero}}
* {{Z|ig}}: {{W-|ig|itu}} {{W-|ig|egwu}}
* {{Z|ang}}: {{W-|ang|frēcennes|m}}
* {{Z|enm}}: {{W-|enm|peril}}, {{W-|enm|danger}}, {{W-|enm|dred}}
* {{Z|en}}: {{W+|en|danger}}
* {{Z|inh}}: {{W-|inh|кхерам}}
* {{Z|ga}}: {{W-|ga|contúirt|m}}, {{W-|ga|dainséar|n}}
* {{Z|it}}: {{W+|it|pericolo|n}}
* {{Z|is}}: {{W+|is|hætta|m}}
* {{Z|ja}}: {{W+|ja|危険|tr=きけん, kiken}}
* {{Z|jv}}: {{W-|jv|bebaya}}
* {{Z|kn}}: {{W+|kn|ಕುತ್ತ}}
* {{Z|ca}}: {{W+|ca|perill|n}}
* {{Z|ky}}: {{W+|ky|коркунуч}}
* {{Z|ki}}: {{W-|ki|ũgwati|c14}}
* {{Z|si}}: {{W-|si|අන්ත්රාව}}
* {{Z|ko}}: {{W+|ko|위험}}
* {{Z|lld}}: {{W-|lld|pericul|n}}
* {{Z|lad}}: {{W-|lad|perikolo|n}}
* {{Z|la}}: {{W+|la|periculum|nt}}
* {{Z|lv}}: {{W-|lv|briesmas|n}}, {{W-|lv|bīstamība|m}}
* {{Z|lo}}: {{W-|lo|ອັນຕະຣາຽ}}, {{W-|lo|ອັນຕະລາຍ}}
* {{Z|lt}}: {{W-|lt|pavojus|n}}
* {{Z|mk}}: {{W-|mk|опасност|m}}
* {{Z|ml}}: {{W+|ml|ആപത്ത്}}, {{W+|ml|അപായം}}
* {{Z|ms}}: {{W+|ms|bahaya}}
* {{Z|mt}}: {{W-|mt|periklu}}
* {{Z|mi}}: {{W-|mi|tatamate}}
* {{Z|hu}}: {{W+|hu|veszély}}
* {{Z|mn}}:
*: Kirîlî: {{W+|mn|аюул}}, {{W-|mn|зэтгэр}}
* {{Z|nci}}: {{W-|nci|ohuihcāyōtl}}
* {{Z|no}}:
*: {{Z|nb}}: {{W+|nb|fare|n}}
* {{Z|oc}}: {{W+|oc|perilh|n}}
* {{Z|om}}: {{W-|om|balaa}}
* {{Z|ug}}: {{W-|ug|خەتەر}}
* {{Z|uz}}: {{W+|uz|xatar}}, {{W+|uz|xavflilik}}
* {{Z|ps}}: {{W+|ps|خطر|n|tr=xatár}}
* {{Z|pdt}}: {{W-|pdt|Jefoa|m}}
* {{Z|pl}}: {{W+|pl|niebezpieczeństwo|nt}}
* {{Z|pt}}: {{W+|pt|perigo|n}}
* {{Z|kk}}: {{W-|kk|қауіп}}, {{W-|kk|қатер}}, {{W-|kk|қауіптілік}}
* {{Z|rm}}: {{W-|rm|privel|n}}, {{W-|rm|prighel|n}}, {{W-|rm|prievel|n}}
* {{Z|ro}}: {{W+|ro|pericol|nt}}, {{W+|ro|primejdie|m}}
* {{Z|ru}}: {{W+|ru|опа́сность|m}}
* {{Z|ssy}}: {{W-|ssy|siraye}}
* {{Z|osx}}: {{W-|osx|fāra|m}}, {{W-|osx|fār|n|m}}, {{W-|osx|gifār|nt}}
* {{Z|sa}}: {{W+|sa|त्यजस्|nt}}
* {{Z|sc}}: {{W-|sc|perículu}}, {{W-|sc|perígulu}}, {{W-|sc|pirígulu}}
* {{Z|scn}}: {{W+|scn|pirìculu}}, {{W+|scn|perìculu}}, {{W+|scn|prìculu}}
* {{Z|sh}}:
*: Kirîlî: {{W-|sh|опа́сно̄ст|m}}
*: Latînî: {{W+|sh|opásnōst|m}}
* {{Z|sk}}: {{W-|sk|nebezpečie|nt}}
* {{Z|sl}}: {{W+|sl|nevarnost|m}}
* {{Z|so}}: {{W-|so|khatar}}
* {{Z|es}}: {{W+|es|peligro|n}}
* {{Z|sw}}: {{W+|sw|hatari}}
* {{Z|sv}}: {{W+|sv|fara|g}}
* {{Z|tg}}: {{W+|tg|хатар}}, {{W+|tg|хавф}}
* {{Z|tl}}: {{W+|tl|panganib}}
* {{Z|ta}}: {{W+|ta|அபாயம்}}, {{W+|ta|ஆபத்து}}
* {{Z|th}}: {{W+|th|อันตราย}}
* {{Z|te}}: {{W+|te|ప్రమాదము}}
* {{Z|tt}}: {{W-|tt|куркыныч}}
* {{Z|tr}}: {{W+|tr|tehlike}}
* {{Z|tk}}: {{W-|tk|howp}}, {{W-|tk|hovp}}
* {{Z|xto}}: {{W-|xto|sanu}}
* {{Z|txb}}: {{W-|txb|ñyātse}}
* {{Z|tyv}}: {{W-|tyv|айыыл}}
* {{Z|ur}}: {{W-|ur|جوکھم|n|tr=jokhim}}, {{W-|ur|خطرہ|n|tr=xatra}}
* {{Z|uk}}: {{W-|uk|небезпе́ка|m}}
* {{Z|vec}}: {{W-|vec|pericoło|n}}, {{W-|vec|pericol}}, {{W-|vec|pericolo}}, {{W-|vec|perigoło}}
* {{Z|vi}}: {{W+|vi|nguy hiểm}}
* {{Z|vo}}: {{W-|vo|pölig}}
* {{Z|wa}}: {{W+|wa|dandjî|n}}
* {{Z|cy}}: {{W+|cy|perygl|n}}, {{W-|cy|peryglau|n-p}}
* {{Z|km}}: {{W+|km|គ្រោះថ្នាក់}}
* {{Z|xh}}: {{W-|xh|ingozi}}
* {{Z|yi}}: {{W-|yi|געפאַר|m|tr=gefar}}
* {{Z|el}}: {{W+|el|κίνδυνος|n}}
*: Kevn: {{W-|grc|κίνδυνος|n}}
* {{Z|zu}}: {{W+|zu|ingozi}}
{{werger-bin}}
:{{werger çavkanî|en|danger|beş=Ev qismê Wergerê|oldid=75479878}}
=== Rengdêr ===
{{rengdêr|ku}}
# {{guherto|xeternak}}
#: {{nm|ku|Ev kar xeter e.|rêz=1|Ev kar xeternak e, bitalûke ye, [[bive]] ye.}}
==== Anagram ====
* {{anagram|ku|a=eertx|Exter|exter}}
sv07xflqh1wbjt8xihch9xyo8t2eii4
6531993
6531964
2026-09-17T17:12:09Z
Balyozbot
7867
Bot: Paqijiyên kozmetîk ({{ku-tewîn-nav/nû}} → {{ku-tewîn-nav}}.)
6531993
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Navdêr ===
{{navdêr|ku|z=m}}
# [[rewş|Rewşa]] ku [[xerab]]iyê, [[tirs]]ê û [[nebaş]]iyê bi xwe re tîne
#: {{hevmane|ku|talûke|metirsî|bive}}
#* {{J:Melayê Cizîrî:Dîwana Melayê Cizîrî|jêgirtin='Arifê weqt im û Me'rûf-i bila bête ji Kerx</br>Da binasî '''xeterên''' rê tu nebî xafil-i jê</br>Dicle ye lê di pir in bir birê pêl û ber û merx}}
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Jê ====
* {{g|ku|xeternak}}
* {{g|ku|xeterî}}
* {{g|ku|xetirandin}}
==== Etîmolojî ====
Ji {{ji|ar|ku|خَطَر}}.
==== Bide ber ====
{{kol3|ku|cure=Bide ber
|zerar
|tirs
|mirin
|êş
|jan
}}
==== Werger ====
{{werger-ser}}
* {{Z|af}}: {{W-|af|gevaar}}
* {{Z|sq}}: {{W+|sq|rrezik|n}}
* {{Z|de}}: {{W+|de|Gefahr|m}}
* {{Z|as}}: {{W-|as|বিপদ}}
* {{Z|ast}}: {{W-|ast|peligru|n}}
* {{Z|az}}: {{W+|az|təhlükə}}
* {{Z|eu}}: {{W-|eu|arrisku}}
* {{Z|be}}: {{W-|be|небяспе́ка|m}}
* {{Z|bn}}: {{W+|bn|বিপদ}}
* {{Z|bg}}: {{W+|bg|опа́сност|m}}
* {{Z|my}}: {{W+|my|ဘယာ}}, {{W+|my|ဘေး}}, {{W+|my|အန္တရာယ်}}
* {{Z|ce}}: {{W-|ce|кхерам}}
* {{Z|cs}}: {{W+|cs|nebezpečí|nt}}
* {{Z|chr}}: {{W-|chr|ᎦᏂᏰᎩ}}
* {{Z|zh}}:
*: {{Z|yue}}: {{W-|yue|危險|tr=ngai4 him2}}
*: {{Z|cmn}}: {{W+|cmn|危險|tr=wēixiǎn}}
* {{Z|ckt}}: {{W-|ckt|гыаргыргын|tr=gyargyrgyn}}
* {{Z|da}}: {{W+|da|fare|g}}
* {{Z|id}}: {{W+|id|bahaya}}
* {{Z|ar}}: {{W-|ar|خَطَر|n}}
*: {{Z|arz}}: {{W-|arz|خطر|n|tr=ḵaṭar}}
* {{Z|hy}}: {{W+|hy|վտանգ}}
* {{Z|eo}}: {{W-|eo|danĝero}}
* {{Z|et}}: {{W+|et|oht}}, {{W-|et|hädaoht}}
* {{Z|fa}}: {{W+|fa|خطر|tr=xatar}}
* {{Z|fo}}: {{W-|fo|vandi|n}}
* {{Z|fi}}: {{W+|fi|vaara}}, {{W+|fi|uhka}}
* {{Z|fr}}: {{W+|fr|danger|n}}, {{W+|fr|péril}}
* {{Z|fur}}: {{W-|fur|pericul|n}}
* {{Z|gd}}: {{W-|gd|cunnart|n}}
* {{Z|gl}}: {{W+|gl|perigo|n}}
* {{Z|got}}: {{W-|got|𐌱𐌹𐍂𐌴𐌹𐌺𐌴𐌹|m}}, {{W-|got|𐍃𐌻𐌴𐌹𐌸𐌴𐌹|m}}
* {{Z|ka}}: {{W-|ka|საფრთხე}}
* {{Z|ht}}: {{W-|ht|danje}}
* {{Z|hi}}: {{W+|hi|जोखिम|n}}, {{W-|hi|ख़तरा|n}}, {{W+|hi|विपदा|m}}, {{W+|hi|प्रमाद|n}}, {{W+|hi|संकट|n}}, {{W+|hi|विपत्ति|m}}, {{W+|hi|खतरा|n}}
* {{Z|nl}}: {{W+|nl|gevaar|nt}}
* {{Z|he}}: {{W+|he|סַכָּנָה|m|tr=sakaná}}
* {{Z|io}}: {{W+|io|danjero}}
* {{Z|ig}}: {{W-|ig|itu}} {{W-|ig|egwu}}
* {{Z|ang}}: {{W-|ang|frēcennes|m}}
* {{Z|enm}}: {{W-|enm|peril}}, {{W-|enm|danger}}, {{W-|enm|dred}}
* {{Z|en}}: {{W+|en|danger}}
* {{Z|inh}}: {{W-|inh|кхерам}}
* {{Z|ga}}: {{W-|ga|contúirt|m}}, {{W-|ga|dainséar|n}}
* {{Z|it}}: {{W+|it|pericolo|n}}
* {{Z|is}}: {{W+|is|hætta|m}}
* {{Z|ja}}: {{W+|ja|危険|tr=きけん, kiken}}
* {{Z|jv}}: {{W-|jv|bebaya}}
* {{Z|kn}}: {{W+|kn|ಕುತ್ತ}}
* {{Z|ca}}: {{W+|ca|perill|n}}
* {{Z|ky}}: {{W+|ky|коркунуч}}
* {{Z|ki}}: {{W-|ki|ũgwati|c14}}
* {{Z|si}}: {{W-|si|අන්ත්රාව}}
* {{Z|ko}}: {{W+|ko|위험}}
* {{Z|lld}}: {{W-|lld|pericul|n}}
* {{Z|lad}}: {{W-|lad|perikolo|n}}
* {{Z|la}}: {{W+|la|periculum|nt}}
* {{Z|lv}}: {{W-|lv|briesmas|n}}, {{W-|lv|bīstamība|m}}
* {{Z|lo}}: {{W-|lo|ອັນຕະຣາຽ}}, {{W-|lo|ອັນຕະລາຍ}}
* {{Z|lt}}: {{W-|lt|pavojus|n}}
* {{Z|mk}}: {{W-|mk|опасност|m}}
* {{Z|ml}}: {{W+|ml|ആപത്ത്}}, {{W+|ml|അപായം}}
* {{Z|ms}}: {{W+|ms|bahaya}}
* {{Z|mt}}: {{W-|mt|periklu}}
* {{Z|mi}}: {{W-|mi|tatamate}}
* {{Z|hu}}: {{W+|hu|veszély}}
* {{Z|mn}}:
*: Kirîlî: {{W+|mn|аюул}}, {{W-|mn|зэтгэр}}
* {{Z|nci}}: {{W-|nci|ohuihcāyōtl}}
* {{Z|no}}:
*: {{Z|nb}}: {{W+|nb|fare|n}}
* {{Z|oc}}: {{W+|oc|perilh|n}}
* {{Z|om}}: {{W-|om|balaa}}
* {{Z|ug}}: {{W-|ug|خەتەر}}
* {{Z|uz}}: {{W+|uz|xatar}}, {{W+|uz|xavflilik}}
* {{Z|ps}}: {{W+|ps|خطر|n|tr=xatár}}
* {{Z|pdt}}: {{W-|pdt|Jefoa|m}}
* {{Z|pl}}: {{W+|pl|niebezpieczeństwo|nt}}
* {{Z|pt}}: {{W+|pt|perigo|n}}
* {{Z|kk}}: {{W-|kk|қауіп}}, {{W-|kk|қатер}}, {{W-|kk|қауіптілік}}
* {{Z|rm}}: {{W-|rm|privel|n}}, {{W-|rm|prighel|n}}, {{W-|rm|prievel|n}}
* {{Z|ro}}: {{W+|ro|pericol|nt}}, {{W+|ro|primejdie|m}}
* {{Z|ru}}: {{W+|ru|опа́сность|m}}
* {{Z|ssy}}: {{W-|ssy|siraye}}
* {{Z|osx}}: {{W-|osx|fāra|m}}, {{W-|osx|fār|n|m}}, {{W-|osx|gifār|nt}}
* {{Z|sa}}: {{W+|sa|त्यजस्|nt}}
* {{Z|sc}}: {{W-|sc|perículu}}, {{W-|sc|perígulu}}, {{W-|sc|pirígulu}}
* {{Z|scn}}: {{W+|scn|pirìculu}}, {{W+|scn|perìculu}}, {{W+|scn|prìculu}}
* {{Z|sh}}:
*: Kirîlî: {{W-|sh|опа́сно̄ст|m}}
*: Latînî: {{W+|sh|opásnōst|m}}
* {{Z|sk}}: {{W-|sk|nebezpečie|nt}}
* {{Z|sl}}: {{W+|sl|nevarnost|m}}
* {{Z|so}}: {{W-|so|khatar}}
* {{Z|es}}: {{W+|es|peligro|n}}
* {{Z|sw}}: {{W+|sw|hatari}}
* {{Z|sv}}: {{W+|sv|fara|g}}
* {{Z|tg}}: {{W+|tg|хатар}}, {{W+|tg|хавф}}
* {{Z|tl}}: {{W+|tl|panganib}}
* {{Z|ta}}: {{W+|ta|அபாயம்}}, {{W+|ta|ஆபத்து}}
* {{Z|th}}: {{W+|th|อันตราย}}
* {{Z|te}}: {{W+|te|ప్రమాదము}}
* {{Z|tt}}: {{W-|tt|куркыныч}}
* {{Z|tr}}: {{W+|tr|tehlike}}
* {{Z|tk}}: {{W-|tk|howp}}, {{W-|tk|hovp}}
* {{Z|xto}}: {{W-|xto|sanu}}
* {{Z|txb}}: {{W-|txb|ñyātse}}
* {{Z|tyv}}: {{W-|tyv|айыыл}}
* {{Z|ur}}: {{W-|ur|جوکھم|n|tr=jokhim}}, {{W-|ur|خطرہ|n|tr=xatra}}
* {{Z|uk}}: {{W-|uk|небезпе́ка|m}}
* {{Z|vec}}: {{W-|vec|pericoło|n}}, {{W-|vec|pericol}}, {{W-|vec|pericolo}}, {{W-|vec|perigoło}}
* {{Z|vi}}: {{W+|vi|nguy hiểm}}
* {{Z|vo}}: {{W-|vo|pölig}}
* {{Z|wa}}: {{W+|wa|dandjî|n}}
* {{Z|cy}}: {{W+|cy|perygl|n}}, {{W-|cy|peryglau|n-p}}
* {{Z|km}}: {{W+|km|គ្រោះថ្នាក់}}
* {{Z|xh}}: {{W-|xh|ingozi}}
* {{Z|yi}}: {{W-|yi|געפאַר|m|tr=gefar}}
* {{Z|el}}: {{W+|el|κίνδυνος|n}}
*: Kevn: {{W-|grc|κίνδυνος|n}}
* {{Z|zu}}: {{W+|zu|ingozi}}
{{werger-bin}}
:{{werger çavkanî|en|danger|beş=Ev qismê Wergerê|oldid=75479878}}
=== Rengdêr ===
{{rengdêr|ku}}
# {{guherto|xeternak}}
#: {{nm|ku|Ev kar xeter e.|rêz=1|Ev kar xeternak e, bitalûke ye, [[bive]] ye.}}
==== Anagram ====
* {{anagram|ku|a=eertx|Exter|exter}}
ihmnm6d6okpc9sbvub66sxmsg7vtk1d
perdaq
0
18687
6531973
6348991
2026-09-17T16:07:24Z
Wikihez
7344
Kurmancî/Navdêr: beşa «Resim» hate nûkirin, beşa «Etîmolojî» hate nûkirin (DefSideBoxes)
6531973
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Navdêr ===
[[Wêne:Leeres Glas volles Glas.jpg|thumb|250px|'''Perdaqek''' vala û yek tijî.]]
{{navdêr|ku|z=m}}
# [[aman|Amana]] [[şûşe]]yî ya bi [[gelemperî]] [[zirav]] û [[dirêj]] ya mirov pê [[av]]ê û [[şile]]yên din [[vexwarin|vedixwe]].
#: {{hevmaneyên peyvê|ku|piyale|glas|şûşe|cam|qedeh}}
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Formên alternatîv ====
{{kol3|ku
|bardaq
|bardax
|berdaq
|berdax
|pardaq
|pardax
|perdax
}}
==== Jê ====
{{kol3|ku
|perdaqik
|perdaqî
|perdaqk
}}
==== Etîmolojî ====
{{etîmon|ku|:deyn|tr:bardak|dar=1|metn=++}}
==== Bide ber ====
{{kol3|ku
|pêk
}}
==== Werger ====
{{werger-ser}}
* {{Z|af}}: {{W+|af|glas}}
* {{Z|sq}}: {{W+|sq|qelq|n}}
* {{Z|de}}: {{W+|de|Glas|nt}}
*: {{Z|gsw}}: {{W-|gsw|Glas|nt}}
* {{Z|ems}}: {{W-|ems|stakanaq}}
* {{Z|am}}: {{W-|am|ብርጭቆ}}
* Apaçî:
*: {{Z|apj}}: {{W-|apj|báaso}}
* {{Z|aii}}: {{W-|aii|ܟܵܣܵܐ|n|tr=kasa}}, {{W-|aii|ܓܠܵܣ|n|tr=glas}}
* {{Z|ast}}: {{W+|ast|vasu|n}}
* {{Z|av}}: {{W-|av|шиша}}
* {{Z|az}}: {{W+|az|stəkan}}
* {{Z|eu}}: {{W+|eu|edalontzi}}
* {{Z|be}}: {{W-|be|шкля́нка|m}}, {{W-|be|стака́н|n}}
* {{Z|bn}}: {{W+|bn|গ্লাস}}
* {{Z|bcl}}: {{W-|bcl|baso}}
* {{Z|br}}: {{W+|br|banne|n}}
* {{Z|kxd}}: {{W-|kxd|galas}}
* {{Z|bg}}: {{W+|bg|ча́ша|m}}
* {{Z|my}}: {{W+|my|ရေခွက်}}
* {{Z|ccc}}: {{W-|ccc|waso}}
* {{Z|ce}}: {{W-|ce|шиша}}
* {{Z|cs}}: {{W+|cs|sklenice|m}}
* {{Z|zh}}:
*: {{Z|yue}}: {{W-|yue|杯|tr=bui1}}
*: {{Z|dng}}: {{W-|dng|җунзы}}
*: {{Z|cmn}}: {{W+|cmn|杯子|tr=bēizi}}
*: {{Z|nan}}: {{W+|nan|杯仔|tr=poe-á}}
*: {{Z|wuu}}: {{W-|wuu|杯子|tr=pe tsr}}
* {{Z|da}}: {{W+|da|glas|nt}}
* {{Z|id}}: {{W+|id|gelas}}
* {{Z|ar}}: {{W+|ar|كَأْس|n}}, {{W+|ar|كُوب|n}}, {{W+|ar|قَدَح|n}}
*: {{Z|arz}}: {{W-|arz|كباية|m|tr=kubbāya}}
*: {{Z|afb}}: {{W-|afb|قلاص|n|tr=gḷāṣ}}
*: {{Z|acw}}: {{W-|acw|كاسة|m|tr=kāsa}}, {{W-|acw|كوب|m|tr=kōb}}
* {{Z|hy}}: {{W+|hy|բաժակ}}
* {{Z|eo}}: {{W+|eo|glaso}}
* {{Z|et}}: {{W+|et|klaas}}
* {{Z|fa}}: {{W+|fa|لیوان|tr=livân}}, {{W+|fa|جام|tr=jâm}}, {{W+|fa|استکان|tr=estekân}}, {{W+|fa|گیلاس|tr=gilâs}}
* {{Z|fo}}: {{W-|fo|glas|nt}}
* {{Z|fi}}: {{W+|fi|lasi}}, {{W+|fi|juomalasi}}
* {{Z|fr}}: {{W+|fr|verre|n}}
* {{Z|gd}}: {{W-|gd|glainne|m}}
* {{Z|gl}}: {{W+|gl|vaso|n}}
* {{Z|gu}}: {{W-|gu|ગ્લાસ}}, {{W-|gu|પ્યાલો}}, {{W-|gu|પવાલું}}
* {{Z|ka}}: {{W+|ka|ჭიქა}}
* {{Z|ht}}: {{W-|ht|vè}}
* {{Z|hi}}: {{W+|hi|ग्लास|n}}
* {{Z|hil}}: {{W-|hil|baso}}
* {{Z|nl}}: {{W+|nl|glas|nt}}
* {{Z|hrx}}: {{W-|hrx|Glaas|nt}}
* {{Z|iba}}: {{W-|iba|gelas}}
* {{Z|he}}: {{W+|he|כּוֹס|m|tr=kos}}
* {{Z|ilo}}: {{W-|ilo|baso}}
* {{Z|en}}: {{W+|en|glass}} (drinking vessels)
* {{Z|ia}}: {{W-|ia|vitro}}
* {{Z|ga}}: {{W+|ga|gloine|m}}
* {{Z|it}}: {{W+|it|bicchiere|n}}, {{W+|it|boccale|n}}
* {{Z|is}}: {{W+|is|glas|nt}}
* {{Z|ja}}: {{W+|ja|グラス|tr=gurasu}}, {{W+|ja|コップ|tr=koppu}}
* {{Z|jv}}: {{W+|jv|gelas}}
* {{Z|kl}}: {{W-|kl|imertarfik}}
* {{Z|kn}}: {{W+|kn|ಲೋಟ}}
* {{Z|pam}}: {{W-|pam|basu}}
* {{Z|ca}}: {{W+|ca|got|n}}, {{W+|ca|vas|n}}
* {{Z|krj}}: {{W-|krj|baso}}
* {{Z|ky}}: {{W+|ky|стакан}}
* {{Z|ki}}: {{W-|ki|ngirathi|c9/10}}
* {{Z|ko}}: {{W-|ko|글라스}}, {{W+|ko|잔}}, {{W+|ko|컵}}
* {{Z|lv}}: {{W+|lv|glāze|m}}
* {{Z|lo}}: {{W-|lo|ກ້ອງ}}, {{W+|lo|ແກ້ວ}}, {{W+|lo|ຈອກແກ້ວ}}
* {{Z|lt}}: {{W-|lt|stiklinė|m}}
* {{Z|lb}}: {{W-|lb|Glas|nt}}
* {{Z|lnd}}: {{W-|lnd|gelas}}
* {{Z|mhn}}: {{W-|mhn|bikera|n}}, {{W-|mhn|glos|nt}}
* {{Z|mk}}: {{W-|mk|ча́ша|m}}
* {{Z|ms}}: {{W+|ms|gelas}}
* {{Z|mt}}: {{W-|mt|tazza|m}}
* {{Z|gv}}: {{W-|gv|glonney}}
* {{Z|mr}}: {{W-|mr|पेला}}, {{W-|mr|प्याला}}
* {{Z|hu}}: {{W+|hu|pohár}}
* {{Z|mel}}: {{W-|mel|gelaih}}
* {{Z|mn}}:
*: Kirîlî: {{W+|mn|шил}}
* {{Z|no}}:
*: {{Z|nb}}: {{W+|nb|glass|nt}}
*: {{Z|nn}}: {{W-|nn|glas|nt}}
* {{Z|oc}}: {{W+|oc|gòt|n}}, {{W+|oc|veire|n}}
* {{Z|ota}}: {{W-|ota|ساغراق|tr=sağrak}}
* {{Z|ug}}: {{W-|ug|ئىستاكان}}
* {{Z|uz}}: {{W+|uz|stakan}}
* {{Z|ps}}: {{W-|ps|ګيلاس|n|tr=gilās}}
* {{Z|pdt}}: {{W-|pdt|Glauss|nt}}
* {{Z|pl}}: {{W+|pl|szklanka|m}}, {{W+|pl|kieliszek|n}}
* {{Z|pt}}: {{W+|pt|copo|n}}, {{W+|pt|taça|m}}
* {{Z|kk}}: {{W-|kk|стакан}}
* {{Z|rhg}}: {{W-|rhg|golóic}}
* {{Z|rm}}: {{W-|rm|magiel|n}}
* {{Z|ro}}: {{W+|ro|pahar|nt}}
* {{Z|ru}}: {{W+|ru|стака́н|n}}, {{W+|ru|рю́мка|m}}, {{W+|ru|скля́нка|m}}, {{W+|ru|бокал}}
* {{Z|sms}}: {{W-|sms|ståkkan}}
* {{Z|sgs}}: {{W-|sgs|skleinīčė|m}}
* {{Z|sc}}: {{W-|sc|bídri}}, {{W-|sc|bidru}}, {{W-|sc|birdi}}, {{W-|sc|bírdiu}}, {{W-|sc|birdu}}, {{W-|sc|bridu}}, {{W-|sc|fidru}}, {{W-|sc|vidru}}
* {{Z|ceb}}: {{W-|ceb|baso}}
* {{Z|sh}}:
*: Kirîlî: {{W-|sh|ча̏ша|m}}
*: Latînî: {{W+|sh|čȁša|m}}
* {{Z|orv}}: {{W-|orv|достоканъ|n}}
* {{Z|sk}}: {{W-|sk|pohár|n}}
* {{Z|sl}}: {{W+|sl|kozarec|n}}
* {{Z|st}}: {{W-|st|kgalase}}
* {{Z|es}}: {{W+|es|vaso|n}}, {{W+|es|copa|m}}
* {{Z|sw}}: {{W+|sw|glasi}}
* {{Z|sv}}: {{W+|sv|glas|nt}}
* {{Z|zdj}}: {{W-|zdj|mɗumu|c3|c4}}
* {{Z|tg}}: {{W-|tg|стакан}}
* {{Z|tl}}: {{W+|tl|baso|nt}}
* {{Z|twf}}: {{W-|twf|bósuną}}
* {{Z|th}}: {{W+|th|แก้ว}}
* {{Z|te}}: {{W+|te|లోటా}}
* {{Z|ti}}: {{W+|ti|ጥርሙዝ}}
* {{Z|tr}}: {{W+|tr|bardak}}
* {{Z|tk}}: {{W-|tk|stakan}}
* {{Z|bo}}: {{W-|bo|ཤེལ་ཕོར}}
* {{Z|to}}: {{W-|to|ipu sio'ata}}
* {{Z|ur}}: {{W-|ur|گلاس|n|tr=glās}}
* {{Z|uk}}: {{W+|uk|скля́нка|m}}, {{W-|uk|стака́н|n}}
* {{Z|vec}}: {{W-|vec|goto|n}}, {{W-|vec|bicer|n}}, {{W-|vec|bicere}}
* {{Z|vi}}: {{W+|vi|ly}}, {{W+|vi|cốc}}
* {{Z|vo}}: {{W+|vo|vär}}
* {{Z|war}}: {{W-|war|baso}}
* {{Z|cy}}: {{W+|cy|gwydr|n}}
* {{Z|km}}: {{W+|km|កែវ}}
* {{Z|yi}}: {{W-|yi|גלאָז|n|m}}
* {{Z|esu}}: {{W-|esu|kelassaq}}, {{W-|esu|estikluuq}}
* {{Z|el}}: {{W+|el|ποτήρι|nt}}
* {{Z|za}}: {{W-|za|cenj}}, {{W-|za|cok}}
{{werger-bin}}
:{{werger çavkanî|en|glass|beş=Ev qismê Wergerê|oldid=75875607}}
fpxpvwkxdquw5whqz84150ln0iq5k1u
qehwe
0
19537
6531951
6439487
2026-09-17T14:35:42Z
Wikihez
7344
Kurmancî/Navdêr 1: beşa «Jê» hate nûkirin, beşa «Etîmolojî» hate nûkirin (DefSideBoxes)
6531951
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-IPA}}
* {{ku-kîte}}
=== Navdêr 1 ===
[[Wêne:Roasted coffee beans.jpg|thumb|[[nok|Nokên]] [[qelandî]] yên '''qehweyê'''.]]
[[Wêne:A small cup of coffee.JPG|thumb|Qehwe ([[vexwirak]])]]
{{navdêr|ku|z=m}}
# {{f|ku|riwek|vexwarin}} [[riwek|Riwekek]] e ji [[nok]]ên wê [[vexwarin]]eke [[tehl]] û bi[[kafeîn]] tê çêkirin.
#* {{J:Albert Camus:Biyanî|jêgirtin=Ji ber ku dilê min nediçû xwarinê wî xwest ji min re '''qehwak''' bi şîr bianiya. E}}
#* {{J:Melayê Cizîrî:Dîwana Melayê Cizîrî|jêgirtin=Me bi bejna te dixwînin ney û qanûn û rebab</br>'''Qehweya''' telxê digel zehra helahil biderî</br>Me ji destê te mubarek şekeramêz e gulab}}
#* {{jêder-pirtûk|ku|jêgirtin=Gumgumê '''qehweyê''' dikeliya. Bêhna qehweyê bi bûx ji devê gumgum derdiket û dihat bêvilên mirov. Qehwecî li argûnê odeyê qehwe diqeland. Taweya doxdirêj li ser sê kuçikan bû. Serbaşê qehweciyan Siyamo bi kefçiyê hesinî qehwe ser û binê hev dikir. Rengê hebên qehweyê berî qelandinê cũn bûn, her ku diqeliyan rengê wan tarî dibûn. Lihevketina kefçiyê hesinî û dolabê deng derdixist, tîngînî jê dihat. Herbî ku diqeliya bêhna qehweyê zêde dibû. Agir boge dibû û qehwecî bi kefçi ser û binê hev dikir, hetanî rind diqeliya. Qehwecî, qehweya gelandî berê di hewangê de dikuta. Paşê her carê kulmek berî aşê destan dida û piyê aş digerand, hetanî rind hûr dikir mîna ard, mîna hinê. Dawiyê qehwe berî gumgumê biçûk dida, dikeliya û diqeliya tîr dibû.|nivîskar=Eyüp Kıran| sernav=Dewrêşê Evdî Kulîlka Bilbizêk û Rim|cîh=Stenbol|weşanxane={{w|Nûbihar}}|sal=2018|çap=4|rûpel=24 |isbn=978-9944-360-95-1}}
==== Tewandin ====
{{ku-tewîn-nav|mê|guherto=hemû}}
==== Bi alfabeyên din ====
* {{ku-ar|قههوه}}
==== Formên alternatîv ====
{{kol3|ku|cure=Herwiha
| {{hw|ku|şalê}}
| {{hw|ku|kahwe}}
| {{hw|ku|kahweh}}
| {{hw|ku|kawe}}
| {{hw|ku|kehwa}}
| {{hw|ku|kehwe}}
| {{hw|ku|kehweh}}
| {{hw|ku|kewa}}
| {{hw|ku|kewe}}
| {{hw|ku|qehweh}}
| {{hw|ku|qehwa}}
| {{hw|ku|qahwe}}
| {{hw|ku|qahweh}}
| {{hw|ku|qawa}}
| {{hw|ku|qawe}}
| {{hw|ku|qewa}}
| {{hw|ku|qewe}}
}}
==== Jê ====
{{kol3|ku|cure=Jê
|qehwegêr
|qehwegêrî
|qehwerengî
|qehwexane
|qehweyî
}}
==== Etîmolojî ====
{{etîmon|ku|:deyn|ar:قَهْوَة|dar=1|metn=+}} Belkî ji [[amharî]] ji navê devera Keffe. Peyvên bi zimanên din jî, wek {{hv|fa|قهوه}} (qehwê), {{hv|tr|kahve}}, {{hv|fr|café}}, {{hv|en|coffee}}.. hemû ji erebî hatine deynkirin.
==== Bi zaravayên din ====
* Soranî: {{W-|ckb|قاوە}}
==== Werger ====
{{werger-ser}}
* {{Z|abx}}: {{W-|abx|kapi}}
* {{Z|abq}}: {{W-|abq|ко́фе}}
* {{Z|ab}}: {{W-|ab|акаҳуа}}
* {{Z|ady}}: {{W-|ady|коф}}
* {{Z|aa}}: {{W-|aa|buun}}, {{W-|aa|buna}}
* {{Z|af}}: {{W+|af|koffie}}
* {{Z|aht}}: {{W-|aht|guuxi}}, {{W-|aht|guuxe'}}
* {{Z|akz}}: {{W-|akz|kafi}}
* {{Z|sq}}: {{W+|sq|kafe|m}}
* {{Z|pdc}}: {{W-|pdc|Kaffi}}
* {{Z|de}}: {{W+|de|Kaffee|n}}
*: {{Z|gmw-cfr}}: {{W-|gmw-cfr|Kaffe}}
* {{Z|am}}: {{W+|am|ቡና}}
* Amuzgo:
*: {{Z|amu}}: {{W-|amu|kajwê}}
*: {{Z|azg}}: {{W-|azg|café}}
* Apaçî:
*: {{Z|apj}}: {{W-|apj|gaʼée}}
*: {{Z|apw}}: {{W-|apw|tudiłhiłi}}
* {{Z|an}}: {{W-|an|café|n}}
* {{Z|arc}}:
*: {{Z|syc}}: {{W-|syc|ܩܗܘܐ|n}}
* {{Z|arp}}: {{W-|arp|woo'teenoowu'}}
* {{Z|rup}}: {{W-|rup|cafe|n}}
* {{Z|as}}: {{W-|as|কফি}}
* {{Z|ast}}: {{W+|ast|café|n}}
* {{Z|av}}: {{W-|av|къагьва}}, {{W-|av|кофе}}
* {{Z|az}}: {{W+|az|qəhvə}}
* {{Z|bde}}: {{W-|bde|sāyín}}, {{W-|bde|sāyì}}
* {{Z|bm}}: {{W-|bm|kafe}}
* {{Z|eu}}: {{W-|eu|kafe}}
* {{Z|ba}}: {{W-|ba|кофе}}, {{W-|ba|ҡәһүә}}
* {{Z|bbl}}: {{W-|bbl|ყავ}}
* {{Z|bar}}: {{W-|bar|Kafää}}
* {{Z|be}}: {{W+|be|ка́ва|m}}
* {{Z|bal}}: {{W-|bal|کاوہ|tr=káwa}}, {{W-|bal|قہوہ|tr=qahwa}}
* {{Z|bn}}: {{W+|bn|কফি}}
* {{Z|bll}}: {{W-|bll|kafi}}
* {{Z|bcl}}: {{W-|bcl|kape}}
* {{Z|bi}}: {{W-|bi|kofi}}, {{W-|bi|kofe}}
* {{Z|bol}}: {{W-|bol|shayì}}
* {{Z|br}}: {{W+|br|kafe|n}}
* {{Z|kxd}}: {{W-|kxd|kupi}}
* {{Z|bxk}}: {{W-|bxk|eemwaani}}
* {{Z|bg}}: {{W+|bg|кафе́|nt}}
* {{Z|my}}: {{W+|my|ကော်ဖီ}}
* {{Z|bua}}: {{W-|bua|кофе}}
* {{Z|dz}}: {{W-|dz|ཀཱ་ཕི།}}
* {{Z|caa}}: {{W-|caa|kahwe}}
* {{Z|ch}}: {{W-|ch|café}}
* {{Z|ce}}: {{W-|ce|къахьо}}, {{W-|ce|кофе}}
* {{Z|cs}}: {{W+|cs|káva|m}}
* {{Z|chr}}: {{W-|chr|ᎧᏫ}}
* {{Z|chy}}: {{W-|chy|mo'ôhtávêhohpe}}
* {{Z|ny}}: {{W-|ny|kofi}}
* Chinantec:
*: {{Z|cle}}: {{W-|cle|café}}
*: {{Z|chz}}: {{W-|chz|jmɨdsɨɨ|cuda=jmɨ{{sup|-}}dsɨɨ{{sup|\}}}}
* {{Z|zh}}:
*: {{Z|yue}}: {{W-|yue|咖啡|tr=gaa3 fe1}}
*: {{Z|dng}}: {{W-|dng|кофе}}
*: {{Z|hak}}: {{W-|hak|咖啡|tr=kâ-pî, kà-pî}}
*: {{Z|cmn}}: {{W+|cmn|咖啡|tr=kāfēi}}
*: {{Z|cdo}}: {{W-|cdo|咖啡|tr=gă-pĭ}}
*: {{Z|nan}}: {{W+|nan|咖啡|tr=ka-pi}}
*: {{Z|wuu}}: {{W-|wuu|咖啡}}
* {{Z|ctu}}: {{W-|ctu|cajpe'}}
* {{Z|cac}}: {{W-|cac|kapeh}}
* {{Z|cv}}: {{W-|cv|кофе}}
* {{Z|dak}}: {{W-|dak|mni k’ata}}, {{W-|dak|pežuta sapa}}
* {{Z|da}}: {{W+|da|kaffe|g}}
* {{Z|dar}}: {{W-|dar|кофе}}
* {{Z|dv}}: {{W-|dv|ގަހުވަ}}, {{W-|dv|ކޮފީ}}
* {{Z|ovd}}: {{W-|ovd|kaffi|nt}}
* {{Z|gsw}}: {{W-|gsw|Kàffe}}
* {{Z|egl}}: {{W-|egl|cafa}}
* {{Z|id}}: {{W+|id|kopi}}
* {{Z|ar}}: {{W+|ar|قَهْوَة|m}}
*: {{Z|arz}}: {{W-|arz|قَهْوَة|m|tr=ʾahwa}}
*: {{Z|afb}}: {{W-|afb|قهوة|tr=gəhwə}}, {{W-|afb|گهوة|tr=gəhwə}}
*: {{Z|acw}}: {{W-|acw|قَهْوَة|m|tr=gahwa}}
*: {{Z|ary}}: {{W-|ary|قهوة|m|tr=qahwa}}
*: {{Z|apd}}: {{W-|apd|جبنة|tr=jabana}}
* {{Z|hy}}: {{W+|hy|սուրճ}}, {{W+|hy|կոֆե}}
* {{Z|myv}}: {{W-|myv|кофе}}
* {{Z|eo}}: {{W+|eo|kafo}}
* {{Z|et}}: {{W+|et|kohv}}
* {{Z|evn}}: {{W-|evn|кофе}}
* {{Z|ext}}: {{W-|ext|café|n}}, {{W-|ext|sidru|n}}
* {{Z|fan}}: {{W-|fan|kofe}}
* {{Z|fa}}: {{W+|fa|قهوه|tr=qahve}}
* {{Z|fo}}: {{W-|fo|kaffi|nt}}
* {{Z|fj}}: {{W-|fj|kofe}}, {{W-|fj|kofi}}
* {{Z|fi}}: {{W+|fi|kahvi}}
* {{Z|vls}}: {{W-|vls|kaffie}}, {{W-|vls|kafje}}
* {{Z|sac}}: {{W-|sac|kâhpîhi}}, {{W-|sac|mahkatêwâpowi}}
* {{Z|fr}}: {{W+|fr|café|n}}
* Frisian:
*: {{Z|frr}}: {{W-|frr|koafje}}
*: {{Z|stq}}: {{W-|stq|Kafe}}
*: {{Z|fy}}: {{W-|fy|kofje|n|m}}
* {{Z|fur}}: {{W-|fur|cafè|n}}
* {{Z|ff}}: {{W-|ff|kofi}}
* {{Z|fud}}: {{W-|fud|kafe}}
* {{Z|gd}}: {{W-|gd|cofaidh|n|m}}
* {{Z|car}}: {{W-|car|kàpe}}
* {{Z|gl}}: {{W+|gl|café|n}}
* {{Z|sdn}}: {{W-|sdn|gaffè|n}}
* {{Z|gub}}: {{W-|gub|kape}}
* {{Z|gu}}: {{W-|gu|કૉફી}}, {{W-|gu|કહવા}}
* {{Z|ka}}: {{W+|ka|ყავა}}
* {{Z|gn}}: {{W-|gn|café}}, {{W-|gn|yaku}}
* {{Z|ht}}: {{W-|ht|kafe}}
* {{Z|haw}}: {{W-|haw|kope}}
* {{Z|ha}}: {{W-|ha|kofi}}, {{W-|ha|kafe}}
* {{Z|hz}}: {{W-|hz|ozo-mbonde}}
* {{Z|hi}}: {{W-|hi|क़हवा|n}}, {{W-|hi|कोफ़ी|m}}, {{W+|hi|काफ़ी|m}}, {{W+|hi|कॉफ़ी|m}}, {{W+|hi|कहवा|n}}, {{W+|hi|काफी|m}}
* {{Z|gin}}: {{W-|gin|ко́пе}}
* {{Z|hil}}: {{W-|hil|kape}}
* {{Z|hnj}}: {{W-|hnj|kas fes}}
* {{Z|mww}}: {{W-|mww|kas fes}}
* {{Z|nl}}: {{W+|nl|koffie|n}}
* {{Z|tee}}: {{W-|tee|capen}}
* {{Z|hrx}}: {{W-|hrx|Kaffi|n}}
* {{Z|mir}}: {{W-|mir|cajueey}}
* {{Z|he}}: {{W+|he|קָפֶה|n|tr=qafé}}
* {{Z|io}}: {{W+|io|kafeo}}
* {{Z|ilo}}: {{W-|ilo|kape}}
* {{Z|en}}: {{W+|en|coffee}}
* {{Z|inh}}: {{W-|inh|кофи}}
* {{Z|ia}}: {{W-|ia|caffe}}
* {{Z|iu}}: {{W-|iu|ᑳᐱ}}
* {{Z|ik}}: {{W-|ik|kuukpiaq}}, {{W-|ik|kuuppiaq}}, {{W-|ik|kaapi}}, {{W-|ik|kaavi}}
* {{Z|ga}}: {{W-|ga|caife|n}}
* {{Z|it}}: {{W+|it|caffè|n}}
* {{Z|is}}: {{W+|is|kaffi|nt}}
* {{Z|ja}}: {{W+|ja|コーヒー|tr=kōhī}}, {{W+|ja|珈琲|tr=kōhī}}, {{W+|ja|カフェ|tr=kafe}}
* {{Z|jv}}: {{W+|jv|kopi}}
* {{Z|quc}}: {{W-|quc|kapeh}}
* {{Z|kab}}: {{W-|kab|taɣlust}}, {{W-|kab|lqahwa}}
* {{Z|kbc}}: {{W-|kbc|gaafi}}
* {{Z|kl}}: {{W-|kl|kaffi}}
* {{Z|xal}}: {{W-|xal|кофе}}
* {{Z|kn}}: {{W+|kn|ಕಾಫಿ}}
* {{Z|ksk}}: {{W-|ksk|mokáⁿ sábe}}
* {{Z|pam}}: {{W-|pam|kapi}}
* {{Z|kea}}: {{W-|kea|kafé}}
* {{Z|krc}}: {{W-|krc|кофе}}
* {{Z|kdr}}: {{W-|kdr|къахъфэ}}
* {{Z|csb}}: {{W-|csb|kawa|m}}
* {{Z|ca}}: {{W+|ca|cafè|n}}
* {{Z|qu}}: {{W-|qu|kaphiy}}
*: {{Z|qu}}: {{W-|qu|cafï}}
* {{Z|ky}}: {{W+|ky|кофе}}, {{W-|ky|каба}}
* {{Z|kg}}: {{W-|kg|káfé}}
* {{Z|si}}: {{W-|si|කෝපි}}
* {{Z|rw}}: {{W-|rw|ikawa}}; {{W-|rw|akawa}}, {{W-|rw|agahawa}}
* Samî:
*: {{Z|se}}: {{W-|se|gáffe}}, {{W-|se|káffe}}
* {{Z|gil}}: {{W-|gil|koobe}}
* {{Z|kpv}}: {{W-|kpv|кофе}}
* {{Z|kok}}: {{W-|kok|कापि}}
* {{Z|ko}}: {{W+|ko|커피}}
* {{Z|kw}}: {{W-|kw|koffi|n}}
* {{Z|co}}: {{W-|co|caffè|n}}
* {{Z|lou}}: {{W-|lou|kafe}}
* {{Z|crk}}: {{W-|crk|ᐱᐦᑲᐦᒉᐋᐧᐳᐢ|tr=pihkahcewâpos}}
* {{Z|qua}}: {{W-|qua|ma-kaⁿ sha}}
* {{Z|kum}}: {{W-|kum|къагьва}}
* {{Z|cuk}}: {{W-|cuk|kabi}}
* {{Z|kj}}: {{W-|kj|okofi}}
* {{Z|lld}}: {{W-|lld|cafè|n}}
* {{Z|lad}}: {{W-|lad|kavé}}
* {{Z|lbe}}: {{W-|lbe|ккупи}}
* {{Z|lkt}}: {{W-|lkt|pejula sapa}}
* {{Z|la}}: {{W-|la|cafaea|m}}, {{W-|la|cafaeum|nt}}, {{W-|la|cafea|m}}, {{W-|la|cafeum|nt}}, {{W-|la|coffaea|m}}, {{W-|la|coffaeum|nt}}, {{W-|la|coffea|m}}, {{W-|la|coffeum|nt}}
* {{Z|lv}}: {{W+|lv|kafija|m}}
* {{Z|lo}}: {{W+|lo|ກາເຟ}}
* {{Z|lez}}: {{W-|lez|кофе}}, {{W-|lez|къагьве}}
* {{Z|ln}}: {{W-|ln|kawa}}
* {{Z|lij}}: {{W-|lij|caffè|n}}, {{W-|lij|cafè|n}}
* {{Z|li}}: {{W+|li|koffie|n}}
* {{Z|lt}}: {{W-|lt|kava|m}}
* {{Z|lmo}}: {{W-|lmo|café|n}}, {{W-|lmo|cafè|n}}
* {{Z|loz}}: {{W-|loz|kofi}}
* {{Z|lua}}: {{W-|lua|kàfê|12|13}}
* {{Z|khb}}: {{W-|khb|ᦆᦱᧈᦵᦝᧀᧈ}}
* {{Z|lg}}: {{W-|lg|emwaanyi}}, {{W-|lg|kaawa}}
* {{Z|lb}}: {{W+|lb|Kaffi|n}}
* {{Z|mhn}}: {{W-|mhn|kafè|n}}
* {{Z|mcb}}: {{W-|mcb|kaju}}
* {{Z|mdh}}: {{W-|mdh|kahawa}}
* {{Z|sks}}: {{W-|sks|kopi}}
* {{Z|mk}}: {{W-|mk|кафе|nt}}
* {{Z|mg}}: {{W+|mg|kafe}}
* {{Z|ml}}: {{W+|ml|കാപ്പി}}
* {{Z|ms}}: {{W+|ms|kopi}}, {{W-|ms|kahwa}}, {{W+|ms|kawa}}
* {{Z|mt}}: {{W-|mt|kafè|n}}
* {{Z|gv}}: {{W-|gv|caffee}}
* {{Z|mi}}: {{W-|mi|kawhe}}
* {{Z|mrw}}: {{W-|mrw|kahawa}}
* {{Z|mr}}: {{W-|mr|काफी|n}}
* {{Z|mhr}}: {{W-|mhr|кофе}}
* {{Z|mh}}: {{W-|mh|ko̧pe}}
* {{Z|yua}}: {{W-|yua|káapej}}
* {{Z|maq}}: {{W-|maq|café|cuda=ca{{sup|3}}fé{{sup|24}}}}
* {{Z|hu}}: {{W+|hu|kávé}}
* {{Z|mwl}}: {{W-|mwl|café|n}}
* {{Z|mdf}}: {{W-|mdf|кофе}}
* {{Z|mn}}:
*: Kirîlî: {{W+|mn|кофе}}
*: Mongolî: {{W-|mn|ᠺᠣᠹᠧ}}
* {{Z|mzp}}: {{W-|mzp|ka꞉pe}}
* {{Z|mse}}: {{W-|mse|kàffēnā}}
* {{Z|nah}}: {{W-|nah|cafentzin}}, {{W-|nah|cafēn}}
*: {{Z|nhx}}: {{W-|nhx|capej}}
* {{Z|nap}}: {{W-|nap|cafè|n}}
* {{Z|nv}}: {{W-|nv|gohwééh}}, {{W-|nv|ahwééh}}
* {{Z|ng}}: {{W-|ng|okoofi}}
* {{Z|nds}}: {{W+|nds|Kaffee|n}}
*: {{Z|nds-nl}}: {{W-|nds-nl|koffie}}, {{W-|nds-nl|kovvie}},
*: {{Z|nds-nl}}: {{W-|nds-nl|kofje}}
* {{Z|ne}}: {{W+|ne|कफी}}
* {{Z|ngi}}: {{W-|ngi|shāyì}}
* {{Z|niu}}: {{W-|niu|kofe}}
* {{Z|nog}}: {{W-|nog|кофе}}
* {{Z|nrf}}: {{W-|nrf|café|n}}
* {{Z|no}}:
*: {{Z|no}}: {{W+|no|kaffe}}
*: {{Z|no}}: {{W+|no|kaffi}}
* {{Z|nuj}}: {{W-|nuj|kaawa}}
* {{Z|oj}}: {{W-|oj|makade-mashkikiwaaboo}}, {{W-|oj|gaapii}}
* {{Z|oc}}: {{W+|oc|cafè|n}}
* {{Z|or}}: {{W+|or|କଫୀ}}
* {{Z|om}}: {{W-|om|buna}}
* {{Z|osa}}: {{W-|osa|mąhkása}}
* {{Z|os}}: {{W-|os|къофи}}, {{W-|os|хъауа}}
* {{Z|ota}}: {{W-|ota|قهوه|tr=ḳahve}}
* {{Z|ote}}: {{W-|ote|'bodehe}}, {{W-|ote|'bothe}}
* {{Z|ug}}: {{W-|ug|قەھۋە}}
* {{Z|uz}}: {{W+|uz|qahva}}, {{W+|uz|kofe}}
* {{Z|pi}}: {{W-|pi|kāphī|m}}
* {{Z|pag}}: {{W-|pag|kape}}
* {{Z|pap}}: {{W-|pap|kòfi}}
* {{Z|pak}}: {{W-|pak|kape}}
* {{Z|ps}}: {{W+|ps|قهوه|m|tr=qahwa}}, {{W-|ps|کاوه|m|tr=kāwá}}, {{W+|ps|کافي|n|tr=kāfí}}
* {{Z|pms}}: {{W-|pms|cafè|n}}
* {{Z|pdt}}: {{W-|pdt|Koffe|n}}
* {{Z|pl}}: {{W+|pl|kawa|m}}
* Popoluca:
*: {{Z|poi}}: {{W-|poi|capel}}
*: {{Z|plo}}: {{W-|plo|cafe't}}
*: {{Z|pos}}: {{W-|pos|café}}
* {{Z|pt}}: {{W+|pt|café|n}}
* Puncabî:
*: Şaxmuxî: {{W-|pa|قہوہ|n}}, {{W+|pa|کافی|m}}
*: Gurmuxî: {{W-|pa|ਕਾਫ਼ੀ|m}}, {{W-|pa|قَہْوَہ|n}}
* {{Z|pua}}: {{W-|pua|kafe}}
* {{Z|kk}}: {{W+|kk|кофе}}
* {{Z|cop}}: {{W-|cop|ⲡⲟⲛ|n}}
* {{Z|rar}}: {{W-|rar|kaope}}
* {{Z|rhg}}: {{W-|rhg|kofí}}
* {{Z|rm}}: {{W-|rm|café|n}}
* {{Z|ro}}: {{W+|ro|cafea|m}}
* {{Z|ru}}: {{W+|ru|ко́фе|n|nt}}
, {{W-|rue|ка́виль|n}}
* {{Z|sm}}: {{W-|sm|kofe}}
* {{Z|sg}}: {{W+|sg|kâfe}}, {{W+|sg|kâwa}}
* {{Z|sa}}: {{W+|sa|पीयूष|n}}, {{W-|sa|काफी}}
* {{Z|sat}}: {{W-|sat|ᱠᱚᱯᱷᱤ}}
* Sardinian:
*: {{Z|sc}}: {{W-|sc|caffei|n}}, {{W-|sc|caffeu|n}}
*: {{Z|sc}}: {{W-|sc|caffei|n}}, {{W-|sc|caffè|n}}
*: {{Z|sdc}}: {{W-|sdc|caffè|n}}
* {{Z|ceb}}: {{W-|ceb|kape}}
* {{Z|sei}}: {{W-|sei|cafee}}, {{W-|sei|xica coopol}}
* {{Z|scn}}: {{W+|scn|cafè|n}}
* {{Z|szl}}: {{W-|szl|kafej|n}}, {{W-|szl|kafyj|n}}
* {{Z|sd}}: {{W-|sd|ڪافي}}, {{W-|sd|قهوو}}
* {{Z|srn}}: {{W-|srn|kofi}}
* {{Z|sh}}:
*: Kirîlî: {{W-|sh|ка̀фа|m}}, {{W-|sh|ка̑ва|m}}, {{W-|sh|кахва|m}}
*: Latînî: {{W+|sh|kàfa|m}}, {{W+|sh|kȃva|m}}, {{W+|sh|kahva|m}}
* {{Z|sco}}: {{W-|sco|coffee}}
* {{Z|sk}}: {{W+|sk|káva|m}}
* {{Z|sl}}: {{W+|sl|káva|m}}
* {{Z|so}}: {{W-|so|bun|n}}, {{W-|so|qahwe}}
* Sorbî:
*: {{Z|dsb}}: {{W-|dsb|kafej|n}}
*: {{Z|hsb}}: {{W-|hsb|kofej|n}}
* {{Z|nso}}: {{W-|nso|ditšhumamelômo}}, {{W-|nso|kôfi}}
* {{Z|st}}: {{W-|st|kofi}}
* {{Z|es}}: {{W+|es|café|n}}, {{W+|es|tinto|n}}, {{W+|es|feca|n}}
* {{Z|sw}}: {{W+|sw|kahawa}}
* {{Z|sv}}: {{W+|sv|kaffe|nt}}
* {{Z|zdj}}: {{W-|zdj|kafe}}, {{W-|zdj|kahawa}}
*: {{Z|swb}}: {{W-|swb|buni}}
* {{Z|sn}}: {{W-|sn|kofi}}
* {{Z|tab}}: {{W-|tab|кофе}}, {{W-|tab|къегьва}}, {{W-|tab|къягьва}}
* {{Z|tg}}: {{W+|tg|қаҳва}}
* {{Z|tl}}: {{W+|tl|kape}}
* {{Z|ty}}: {{W-|ty|taofe}}
* {{Z|tna}}: {{W-|tna|kajue}}
* {{Z|ta}}: {{W+|ta|காபி}}
* {{Z|tar}}: {{W-|tar|cajé}}
* {{Z|shi}}: {{W-|shi|l-qhwa}}
* {{Z|tsg}}: {{W-|tsg|kahawa}}
* {{Z|nod}}: {{W-|nod|ᨠᩣᨼᩯ}}
* {{Z|th}}: {{W+|th|กาแฟ}}
* {{Z|te}}: {{W+|te|కాఫీ}}
* {{Z|pah}}: {{W-|pah|kag̃wi}}
* {{Z|cux}}: {{W-|cux|café|cuda=ca{{sup|3}}fé{{sup|24}}}}
* {{Z|crh}}: {{W-|crh|qave}}
* {{Z|tt}}: {{W-|tt|каһвә}}
* {{Z|tet}}: {{W-|tet|kafé}}
* {{Z|ti}}: {{W+|ti|ቡን}}
* {{Z|tr}}: {{W+|tr|kahve}}, {{W+|tr|konur}}
* {{Z|tk}}: {{W-|tk|kofe}}
* {{Z|bo}}: {{W-|bo|ཁོ་ཕི}}, {{W-|bo|ཁ་ཕེ}}, {{W-|bo|ཇ་ཀོབ་པི}}, {{W-|bo|འཚིག་ཇ}}
* {{Z|tpi}}: {{W-|tpi|kopi}}
* {{Z|tos}}: {{W-|tos|capij}}
* {{Z|top}}: {{W-|top|café}}, {{W-|top|capé}}
* {{Z|mto}}: {{W-|mto|café}}
* {{Z|tus}}: {{W-|tus|káhwih}}
* {{Z|tvl}}: {{W-|tvl|kofe}}
* {{Z|tyv}}: {{W-|tyv|кофе}}
* {{Z|tzh}}: {{W-|tzh|cahpel}}
* {{Z|tzo}}: {{W-|tzo|kajve}}
* {{Z|udm}}: {{W-|udm|кофе}}
* {{Z|ur}}: {{W-|ur|قہوہ|n|tr=qahva}}, {{W-|ur|کوفی|m|tr=kofī}}, {{W-|ur|کافی|m|tr=kāfī}}
* {{Z|uk}}: {{W+|uk|ка́ва|m}}
* {{Z|vec}}: {{W+|vec|cafè|n}}
* {{Z|vep}}: {{W-|vep|kofe}}
* {{Z|vi}}: {{W+|vi|cà phê}}
* {{Z|vo}}: {{W+|vo|kaf}}
* {{Z|vro}}: {{W-|vro|kohv}}
* {{Z|wls}}: {{W-|wls|kafē}}
* {{Z|wa}}: {{W+|wa|cafè|n}}
* {{Z|lwg}}: {{W-|lwg|ikaawa}}
* {{Z|war}}: {{W-|war|kape}}
* {{Z|cy}}: {{W+|cy|coffi|n}}
* {{Z|wo}}: {{W-|wo|kafe}}
* {{Z|kjh}}: {{W-|kjh|кофе}}
* {{Z|km}}: {{W-|km|កាហ្វេ|tr=kahvé}}
* {{Z|xh}}: {{W-|xh|ikofu}}
* {{Z|sah}}: {{W-|sah|кофе}}
* {{Z|yi}}: {{W+|yi|קאַווע|m}}
* {{Z|yo}}: {{W-|yo|ohun mímu}}, {{W-|yo|kofi}}, {{W-|yo|kọfi}}
* {{Z|esu}}: {{W-|esu|kuuvviaq}}
* Zapotec:
*: {{Z|zaa}}: {{W-|zaa|café}}
*: {{Z|zav}}: {{W-|zav|cafe}}
*: {{Z|zpq}}: {{W-|zpq|café}}
* {{Z|el}}: {{W+|el|καφές|n}}
* {{Z|zoo}}: {{W-|zoo|café}}
* {{Z|zea}}: {{W-|zea|koffie}}
* {{Z|za}}: {{W-|za|gahfeih}}, {{W-|za|gyahfeih}}
* Zoque:
*: {{Z|zoc}}: {{W-|zoc|cajwe}}
*: {{Z|zor}}: {{W-|zor|kajwel}}
* {{Z|zu}}: {{W+|zu|ikhofi}}
{{werger-bin}}
=== Navdêr 2 ===
{{navdêr|ku|c=m}}
# [[qehwexane|Qehwexane]].
#* {{jêder-websîte|ku|jêgirtin=Xwendekarên min jî di tenefusan de hey digotin, “dikana dombîlî” û berê xwe didan wê derê, zilaman di nav xwe de digotin, “'''qehweya''' dombîlî” û li wir şerê xwe yê bi lîstikên kaxez û okeyê gûr dikirin.|nivîskar=w:Yaqob Tilermenî|sernav=Ma matematîk jî me dixapîne?|websîte=diyarname.com|url=https://www.diyarname.com/nivis/ma-matematik-ji-me-dixapine_13116|tarîx=25.08.2026}}
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Etîmolojî ====
{{ji?|ku}}
==== Bi zaravayên din ====
{{werger-ser|qehwexane|1}}
{{werger-bin}}
==== Werger ====
{{werger-ser|qehwexane|1}}
{{werger-bin}}
== {{ziman|zza}} ==
=== Navdêr ===
{{navdêr|zza|z=?}}
# {{g|ku|qehwe}}
2nxr641g4xbwnq8mw2psm3vi2c8xjit
6531952
6531951
2026-09-17T14:36:21Z
Wikihez
7344
/* Navdêr 1 */ (bi [[MediaWiki:Gadget-AjaxEdit.js|AjaxEdit]]ê re)
6531952
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-IPA}}
* {{ku-kîte}}
=== Navdêr 1 ===
[[Wêne:Roasted coffee beans.jpg|thumb|[[nok|Nokên]] [[qelandî]] yên '''qehweyê'''.]]
[[Wêne:A small cup of coffee.JPG|thumb|Qehwe ([[vexwirak]])]]
{{navdêr|ku|z=m}}
# {{f|ku|riwek|vexwarin}} [[riwek|Riwekek]] e ji [[nok]]ên wê [[vexwarin]]eke [[tehl]] û bi[[kafeîn]] tê çêkirin.
#* {{J:Melayê Cizîrî:Dîwana Melayê Cizîrî|jêgirtin=Me bi bejna te dixwînin ney û qanûn û rebab</br>'''Qehweya''' telxê digel zehra helahil biderî</br>Me ji destê te mubarek şekeramêz e gulab}}
#* {{jêder-pirtûk|ku|jêgirtin=Gumgumê '''qehweyê''' dikeliya. Bêhna qehweyê bi bûx ji devê gumgum derdiket û dihat bêvilên mirov. Qehwecî li argûnê odeyê qehwe diqeland. Taweya doxdirêj li ser sê kuçikan bû. Serbaşê qehweciyan Siyamo bi kefçiyê hesinî qehwe ser û binê hev dikir. Rengê hebên qehweyê berî qelandinê cũn bûn, her ku diqeliyan rengê wan tarî dibûn. Lihevketina kefçiyê hesinî û dolabê deng derdixist, tîngînî jê dihat. Herbî ku diqeliya bêhna qehweyê zêde dibû. Agir boge dibû û qehwecî bi kefçi ser û binê hev dikir, hetanî rind diqeliya. Qehwecî, qehweya gelandî berê di hewangê de dikuta. Paşê her carê kulmek berî aşê destan dida û piyê aş digerand, hetanî rind hûr dikir mîna ard, mîna hinê. Dawiyê qehwe berî gumgumê biçûk dida, dikeliya û diqeliya tîr dibû.|nivîskar=Eyüp Kıran| sernav=Dewrêşê Evdî Kulîlka Bilbizêk û Rim|cîh=Stenbol|weşanxane={{w|Nûbihar}}|sal=2018|çap=4|rûpel=24 |isbn=978-9944-360-95-1}}
#* {{J:Albert Camus:Biyanî|jêgirtin=Ji ber ku dilê min nediçû xwarinê wî xwest ji min re '''qehwak''' bi şîr bianiya. E}}
==== Tewandin ====
{{ku-tewîn-nav|mê|guherto=hemû}}
==== Bi alfabeyên din ====
* {{ku-ar|قههوه}}
==== Formên alternatîv ====
{{kol3|ku|cure=Herwiha
| {{hw|ku|şalê}}
| {{hw|ku|kahwe}}
| {{hw|ku|kahweh}}
| {{hw|ku|kawe}}
| {{hw|ku|kehwa}}
| {{hw|ku|kehwe}}
| {{hw|ku|kehweh}}
| {{hw|ku|kewa}}
| {{hw|ku|kewe}}
| {{hw|ku|qehweh}}
| {{hw|ku|qehwa}}
| {{hw|ku|qahwe}}
| {{hw|ku|qahweh}}
| {{hw|ku|qawa}}
| {{hw|ku|qawe}}
| {{hw|ku|qewa}}
| {{hw|ku|qewe}}
}}
==== Jê ====
{{kol3|ku|cure=Jê
|qehwegêr
|qehwegêrî
|qehwerengî
|qehwexane
|qehweyî
}}
==== Etîmolojî ====
{{etîmon|ku|:deyn|ar:قَهْوَة|dar=1|metn=+}} Belkî ji [[amharî]] ji navê devera Keffe. Peyvên bi zimanên din jî, wek {{hv|fa|قهوه}} (qehwê), {{hv|tr|kahve}}, {{hv|fr|café}}, {{hv|en|coffee}}.. hemû ji erebî hatine deynkirin.
==== Bi zaravayên din ====
* Soranî: {{W-|ckb|قاوە}}
==== Werger ====
{{werger-ser}}
* {{Z|abx}}: {{W-|abx|kapi}}
* {{Z|abq}}: {{W-|abq|ко́фе}}
* {{Z|ab}}: {{W-|ab|акаҳуа}}
* {{Z|ady}}: {{W-|ady|коф}}
* {{Z|aa}}: {{W-|aa|buun}}, {{W-|aa|buna}}
* {{Z|af}}: {{W+|af|koffie}}
* {{Z|aht}}: {{W-|aht|guuxi}}, {{W-|aht|guuxe'}}
* {{Z|akz}}: {{W-|akz|kafi}}
* {{Z|sq}}: {{W+|sq|kafe|m}}
* {{Z|pdc}}: {{W-|pdc|Kaffi}}
* {{Z|de}}: {{W+|de|Kaffee|n}}
*: {{Z|gmw-cfr}}: {{W-|gmw-cfr|Kaffe}}
* {{Z|am}}: {{W+|am|ቡና}}
* Amuzgo:
*: {{Z|amu}}: {{W-|amu|kajwê}}
*: {{Z|azg}}: {{W-|azg|café}}
* Apaçî:
*: {{Z|apj}}: {{W-|apj|gaʼée}}
*: {{Z|apw}}: {{W-|apw|tudiłhiłi}}
* {{Z|an}}: {{W-|an|café|n}}
* {{Z|arc}}:
*: {{Z|syc}}: {{W-|syc|ܩܗܘܐ|n}}
* {{Z|arp}}: {{W-|arp|woo'teenoowu'}}
* {{Z|rup}}: {{W-|rup|cafe|n}}
* {{Z|as}}: {{W-|as|কফি}}
* {{Z|ast}}: {{W+|ast|café|n}}
* {{Z|av}}: {{W-|av|къагьва}}, {{W-|av|кофе}}
* {{Z|az}}: {{W+|az|qəhvə}}
* {{Z|bde}}: {{W-|bde|sāyín}}, {{W-|bde|sāyì}}
* {{Z|bm}}: {{W-|bm|kafe}}
* {{Z|eu}}: {{W-|eu|kafe}}
* {{Z|ba}}: {{W-|ba|кофе}}, {{W-|ba|ҡәһүә}}
* {{Z|bbl}}: {{W-|bbl|ყავ}}
* {{Z|bar}}: {{W-|bar|Kafää}}
* {{Z|be}}: {{W+|be|ка́ва|m}}
* {{Z|bal}}: {{W-|bal|کاوہ|tr=káwa}}, {{W-|bal|قہوہ|tr=qahwa}}
* {{Z|bn}}: {{W+|bn|কফি}}
* {{Z|bll}}: {{W-|bll|kafi}}
* {{Z|bcl}}: {{W-|bcl|kape}}
* {{Z|bi}}: {{W-|bi|kofi}}, {{W-|bi|kofe}}
* {{Z|bol}}: {{W-|bol|shayì}}
* {{Z|br}}: {{W+|br|kafe|n}}
* {{Z|kxd}}: {{W-|kxd|kupi}}
* {{Z|bxk}}: {{W-|bxk|eemwaani}}
* {{Z|bg}}: {{W+|bg|кафе́|nt}}
* {{Z|my}}: {{W+|my|ကော်ဖီ}}
* {{Z|bua}}: {{W-|bua|кофе}}
* {{Z|dz}}: {{W-|dz|ཀཱ་ཕི།}}
* {{Z|caa}}: {{W-|caa|kahwe}}
* {{Z|ch}}: {{W-|ch|café}}
* {{Z|ce}}: {{W-|ce|къахьо}}, {{W-|ce|кофе}}
* {{Z|cs}}: {{W+|cs|káva|m}}
* {{Z|chr}}: {{W-|chr|ᎧᏫ}}
* {{Z|chy}}: {{W-|chy|mo'ôhtávêhohpe}}
* {{Z|ny}}: {{W-|ny|kofi}}
* Chinantec:
*: {{Z|cle}}: {{W-|cle|café}}
*: {{Z|chz}}: {{W-|chz|jmɨdsɨɨ|cuda=jmɨ{{sup|-}}dsɨɨ{{sup|\}}}}
* {{Z|zh}}:
*: {{Z|yue}}: {{W-|yue|咖啡|tr=gaa3 fe1}}
*: {{Z|dng}}: {{W-|dng|кофе}}
*: {{Z|hak}}: {{W-|hak|咖啡|tr=kâ-pî, kà-pî}}
*: {{Z|cmn}}: {{W+|cmn|咖啡|tr=kāfēi}}
*: {{Z|cdo}}: {{W-|cdo|咖啡|tr=gă-pĭ}}
*: {{Z|nan}}: {{W+|nan|咖啡|tr=ka-pi}}
*: {{Z|wuu}}: {{W-|wuu|咖啡}}
* {{Z|ctu}}: {{W-|ctu|cajpe'}}
* {{Z|cac}}: {{W-|cac|kapeh}}
* {{Z|cv}}: {{W-|cv|кофе}}
* {{Z|dak}}: {{W-|dak|mni k’ata}}, {{W-|dak|pežuta sapa}}
* {{Z|da}}: {{W+|da|kaffe|g}}
* {{Z|dar}}: {{W-|dar|кофе}}
* {{Z|dv}}: {{W-|dv|ގަހުވަ}}, {{W-|dv|ކޮފީ}}
* {{Z|ovd}}: {{W-|ovd|kaffi|nt}}
* {{Z|gsw}}: {{W-|gsw|Kàffe}}
* {{Z|egl}}: {{W-|egl|cafa}}
* {{Z|id}}: {{W+|id|kopi}}
* {{Z|ar}}: {{W+|ar|قَهْوَة|m}}
*: {{Z|arz}}: {{W-|arz|قَهْوَة|m|tr=ʾahwa}}
*: {{Z|afb}}: {{W-|afb|قهوة|tr=gəhwə}}, {{W-|afb|گهوة|tr=gəhwə}}
*: {{Z|acw}}: {{W-|acw|قَهْوَة|m|tr=gahwa}}
*: {{Z|ary}}: {{W-|ary|قهوة|m|tr=qahwa}}
*: {{Z|apd}}: {{W-|apd|جبنة|tr=jabana}}
* {{Z|hy}}: {{W+|hy|սուրճ}}, {{W+|hy|կոֆե}}
* {{Z|myv}}: {{W-|myv|кофе}}
* {{Z|eo}}: {{W+|eo|kafo}}
* {{Z|et}}: {{W+|et|kohv}}
* {{Z|evn}}: {{W-|evn|кофе}}
* {{Z|ext}}: {{W-|ext|café|n}}, {{W-|ext|sidru|n}}
* {{Z|fan}}: {{W-|fan|kofe}}
* {{Z|fa}}: {{W+|fa|قهوه|tr=qahve}}
* {{Z|fo}}: {{W-|fo|kaffi|nt}}
* {{Z|fj}}: {{W-|fj|kofe}}, {{W-|fj|kofi}}
* {{Z|fi}}: {{W+|fi|kahvi}}
* {{Z|vls}}: {{W-|vls|kaffie}}, {{W-|vls|kafje}}
* {{Z|sac}}: {{W-|sac|kâhpîhi}}, {{W-|sac|mahkatêwâpowi}}
* {{Z|fr}}: {{W+|fr|café|n}}
* Frisian:
*: {{Z|frr}}: {{W-|frr|koafje}}
*: {{Z|stq}}: {{W-|stq|Kafe}}
*: {{Z|fy}}: {{W-|fy|kofje|n|m}}
* {{Z|fur}}: {{W-|fur|cafè|n}}
* {{Z|ff}}: {{W-|ff|kofi}}
* {{Z|fud}}: {{W-|fud|kafe}}
* {{Z|gd}}: {{W-|gd|cofaidh|n|m}}
* {{Z|car}}: {{W-|car|kàpe}}
* {{Z|gl}}: {{W+|gl|café|n}}
* {{Z|sdn}}: {{W-|sdn|gaffè|n}}
* {{Z|gub}}: {{W-|gub|kape}}
* {{Z|gu}}: {{W-|gu|કૉફી}}, {{W-|gu|કહવા}}
* {{Z|ka}}: {{W+|ka|ყავა}}
* {{Z|gn}}: {{W-|gn|café}}, {{W-|gn|yaku}}
* {{Z|ht}}: {{W-|ht|kafe}}
* {{Z|haw}}: {{W-|haw|kope}}
* {{Z|ha}}: {{W-|ha|kofi}}, {{W-|ha|kafe}}
* {{Z|hz}}: {{W-|hz|ozo-mbonde}}
* {{Z|hi}}: {{W-|hi|क़हवा|n}}, {{W-|hi|कोफ़ी|m}}, {{W+|hi|काफ़ी|m}}, {{W+|hi|कॉफ़ी|m}}, {{W+|hi|कहवा|n}}, {{W+|hi|काफी|m}}
* {{Z|gin}}: {{W-|gin|ко́пе}}
* {{Z|hil}}: {{W-|hil|kape}}
* {{Z|hnj}}: {{W-|hnj|kas fes}}
* {{Z|mww}}: {{W-|mww|kas fes}}
* {{Z|nl}}: {{W+|nl|koffie|n}}
* {{Z|tee}}: {{W-|tee|capen}}
* {{Z|hrx}}: {{W-|hrx|Kaffi|n}}
* {{Z|mir}}: {{W-|mir|cajueey}}
* {{Z|he}}: {{W+|he|קָפֶה|n|tr=qafé}}
* {{Z|io}}: {{W+|io|kafeo}}
* {{Z|ilo}}: {{W-|ilo|kape}}
* {{Z|en}}: {{W+|en|coffee}}
* {{Z|inh}}: {{W-|inh|кофи}}
* {{Z|ia}}: {{W-|ia|caffe}}
* {{Z|iu}}: {{W-|iu|ᑳᐱ}}
* {{Z|ik}}: {{W-|ik|kuukpiaq}}, {{W-|ik|kuuppiaq}}, {{W-|ik|kaapi}}, {{W-|ik|kaavi}}
* {{Z|ga}}: {{W-|ga|caife|n}}
* {{Z|it}}: {{W+|it|caffè|n}}
* {{Z|is}}: {{W+|is|kaffi|nt}}
* {{Z|ja}}: {{W+|ja|コーヒー|tr=kōhī}}, {{W+|ja|珈琲|tr=kōhī}}, {{W+|ja|カフェ|tr=kafe}}
* {{Z|jv}}: {{W+|jv|kopi}}
* {{Z|quc}}: {{W-|quc|kapeh}}
* {{Z|kab}}: {{W-|kab|taɣlust}}, {{W-|kab|lqahwa}}
* {{Z|kbc}}: {{W-|kbc|gaafi}}
* {{Z|kl}}: {{W-|kl|kaffi}}
* {{Z|xal}}: {{W-|xal|кофе}}
* {{Z|kn}}: {{W+|kn|ಕಾಫಿ}}
* {{Z|ksk}}: {{W-|ksk|mokáⁿ sábe}}
* {{Z|pam}}: {{W-|pam|kapi}}
* {{Z|kea}}: {{W-|kea|kafé}}
* {{Z|krc}}: {{W-|krc|кофе}}
* {{Z|kdr}}: {{W-|kdr|къахъфэ}}
* {{Z|csb}}: {{W-|csb|kawa|m}}
* {{Z|ca}}: {{W+|ca|cafè|n}}
* {{Z|qu}}: {{W-|qu|kaphiy}}
*: {{Z|qu}}: {{W-|qu|cafï}}
* {{Z|ky}}: {{W+|ky|кофе}}, {{W-|ky|каба}}
* {{Z|kg}}: {{W-|kg|káfé}}
* {{Z|si}}: {{W-|si|කෝපි}}
* {{Z|rw}}: {{W-|rw|ikawa}}; {{W-|rw|akawa}}, {{W-|rw|agahawa}}
* Samî:
*: {{Z|se}}: {{W-|se|gáffe}}, {{W-|se|káffe}}
* {{Z|gil}}: {{W-|gil|koobe}}
* {{Z|kpv}}: {{W-|kpv|кофе}}
* {{Z|kok}}: {{W-|kok|कापि}}
* {{Z|ko}}: {{W+|ko|커피}}
* {{Z|kw}}: {{W-|kw|koffi|n}}
* {{Z|co}}: {{W-|co|caffè|n}}
* {{Z|lou}}: {{W-|lou|kafe}}
* {{Z|crk}}: {{W-|crk|ᐱᐦᑲᐦᒉᐋᐧᐳᐢ|tr=pihkahcewâpos}}
* {{Z|qua}}: {{W-|qua|ma-kaⁿ sha}}
* {{Z|kum}}: {{W-|kum|къагьва}}
* {{Z|cuk}}: {{W-|cuk|kabi}}
* {{Z|kj}}: {{W-|kj|okofi}}
* {{Z|lld}}: {{W-|lld|cafè|n}}
* {{Z|lad}}: {{W-|lad|kavé}}
* {{Z|lbe}}: {{W-|lbe|ккупи}}
* {{Z|lkt}}: {{W-|lkt|pejula sapa}}
* {{Z|la}}: {{W-|la|cafaea|m}}, {{W-|la|cafaeum|nt}}, {{W-|la|cafea|m}}, {{W-|la|cafeum|nt}}, {{W-|la|coffaea|m}}, {{W-|la|coffaeum|nt}}, {{W-|la|coffea|m}}, {{W-|la|coffeum|nt}}
* {{Z|lv}}: {{W+|lv|kafija|m}}
* {{Z|lo}}: {{W+|lo|ກາເຟ}}
* {{Z|lez}}: {{W-|lez|кофе}}, {{W-|lez|къагьве}}
* {{Z|ln}}: {{W-|ln|kawa}}
* {{Z|lij}}: {{W-|lij|caffè|n}}, {{W-|lij|cafè|n}}
* {{Z|li}}: {{W+|li|koffie|n}}
* {{Z|lt}}: {{W-|lt|kava|m}}
* {{Z|lmo}}: {{W-|lmo|café|n}}, {{W-|lmo|cafè|n}}
* {{Z|loz}}: {{W-|loz|kofi}}
* {{Z|lua}}: {{W-|lua|kàfê|12|13}}
* {{Z|khb}}: {{W-|khb|ᦆᦱᧈᦵᦝᧀᧈ}}
* {{Z|lg}}: {{W-|lg|emwaanyi}}, {{W-|lg|kaawa}}
* {{Z|lb}}: {{W+|lb|Kaffi|n}}
* {{Z|mhn}}: {{W-|mhn|kafè|n}}
* {{Z|mcb}}: {{W-|mcb|kaju}}
* {{Z|mdh}}: {{W-|mdh|kahawa}}
* {{Z|sks}}: {{W-|sks|kopi}}
* {{Z|mk}}: {{W-|mk|кафе|nt}}
* {{Z|mg}}: {{W+|mg|kafe}}
* {{Z|ml}}: {{W+|ml|കാപ്പി}}
* {{Z|ms}}: {{W+|ms|kopi}}, {{W-|ms|kahwa}}, {{W+|ms|kawa}}
* {{Z|mt}}: {{W-|mt|kafè|n}}
* {{Z|gv}}: {{W-|gv|caffee}}
* {{Z|mi}}: {{W-|mi|kawhe}}
* {{Z|mrw}}: {{W-|mrw|kahawa}}
* {{Z|mr}}: {{W-|mr|काफी|n}}
* {{Z|mhr}}: {{W-|mhr|кофе}}
* {{Z|mh}}: {{W-|mh|ko̧pe}}
* {{Z|yua}}: {{W-|yua|káapej}}
* {{Z|maq}}: {{W-|maq|café|cuda=ca{{sup|3}}fé{{sup|24}}}}
* {{Z|hu}}: {{W+|hu|kávé}}
* {{Z|mwl}}: {{W-|mwl|café|n}}
* {{Z|mdf}}: {{W-|mdf|кофе}}
* {{Z|mn}}:
*: Kirîlî: {{W+|mn|кофе}}
*: Mongolî: {{W-|mn|ᠺᠣᠹᠧ}}
* {{Z|mzp}}: {{W-|mzp|ka꞉pe}}
* {{Z|mse}}: {{W-|mse|kàffēnā}}
* {{Z|nah}}: {{W-|nah|cafentzin}}, {{W-|nah|cafēn}}
*: {{Z|nhx}}: {{W-|nhx|capej}}
* {{Z|nap}}: {{W-|nap|cafè|n}}
* {{Z|nv}}: {{W-|nv|gohwééh}}, {{W-|nv|ahwééh}}
* {{Z|ng}}: {{W-|ng|okoofi}}
* {{Z|nds}}: {{W+|nds|Kaffee|n}}
*: {{Z|nds-nl}}: {{W-|nds-nl|koffie}}, {{W-|nds-nl|kovvie}},
*: {{Z|nds-nl}}: {{W-|nds-nl|kofje}}
* {{Z|ne}}: {{W+|ne|कफी}}
* {{Z|ngi}}: {{W-|ngi|shāyì}}
* {{Z|niu}}: {{W-|niu|kofe}}
* {{Z|nog}}: {{W-|nog|кофе}}
* {{Z|nrf}}: {{W-|nrf|café|n}}
* {{Z|no}}:
*: {{Z|no}}: {{W+|no|kaffe}}
*: {{Z|no}}: {{W+|no|kaffi}}
* {{Z|nuj}}: {{W-|nuj|kaawa}}
* {{Z|oj}}: {{W-|oj|makade-mashkikiwaaboo}}, {{W-|oj|gaapii}}
* {{Z|oc}}: {{W+|oc|cafè|n}}
* {{Z|or}}: {{W+|or|କଫୀ}}
* {{Z|om}}: {{W-|om|buna}}
* {{Z|osa}}: {{W-|osa|mąhkása}}
* {{Z|os}}: {{W-|os|къофи}}, {{W-|os|хъауа}}
* {{Z|ota}}: {{W-|ota|قهوه|tr=ḳahve}}
* {{Z|ote}}: {{W-|ote|'bodehe}}, {{W-|ote|'bothe}}
* {{Z|ug}}: {{W-|ug|قەھۋە}}
* {{Z|uz}}: {{W+|uz|qahva}}, {{W+|uz|kofe}}
* {{Z|pi}}: {{W-|pi|kāphī|m}}
* {{Z|pag}}: {{W-|pag|kape}}
* {{Z|pap}}: {{W-|pap|kòfi}}
* {{Z|pak}}: {{W-|pak|kape}}
* {{Z|ps}}: {{W+|ps|قهوه|m|tr=qahwa}}, {{W-|ps|کاوه|m|tr=kāwá}}, {{W+|ps|کافي|n|tr=kāfí}}
* {{Z|pms}}: {{W-|pms|cafè|n}}
* {{Z|pdt}}: {{W-|pdt|Koffe|n}}
* {{Z|pl}}: {{W+|pl|kawa|m}}
* Popoluca:
*: {{Z|poi}}: {{W-|poi|capel}}
*: {{Z|plo}}: {{W-|plo|cafe't}}
*: {{Z|pos}}: {{W-|pos|café}}
* {{Z|pt}}: {{W+|pt|café|n}}
* Puncabî:
*: Şaxmuxî: {{W-|pa|قہوہ|n}}, {{W+|pa|کافی|m}}
*: Gurmuxî: {{W-|pa|ਕਾਫ਼ੀ|m}}, {{W-|pa|قَہْوَہ|n}}
* {{Z|pua}}: {{W-|pua|kafe}}
* {{Z|kk}}: {{W+|kk|кофе}}
* {{Z|cop}}: {{W-|cop|ⲡⲟⲛ|n}}
* {{Z|rar}}: {{W-|rar|kaope}}
* {{Z|rhg}}: {{W-|rhg|kofí}}
* {{Z|rm}}: {{W-|rm|café|n}}
* {{Z|ro}}: {{W+|ro|cafea|m}}
* {{Z|ru}}: {{W+|ru|ко́фе|n|nt}}
, {{W-|rue|ка́виль|n}}
* {{Z|sm}}: {{W-|sm|kofe}}
* {{Z|sg}}: {{W+|sg|kâfe}}, {{W+|sg|kâwa}}
* {{Z|sa}}: {{W+|sa|पीयूष|n}}, {{W-|sa|काफी}}
* {{Z|sat}}: {{W-|sat|ᱠᱚᱯᱷᱤ}}
* Sardinian:
*: {{Z|sc}}: {{W-|sc|caffei|n}}, {{W-|sc|caffeu|n}}
*: {{Z|sc}}: {{W-|sc|caffei|n}}, {{W-|sc|caffè|n}}
*: {{Z|sdc}}: {{W-|sdc|caffè|n}}
* {{Z|ceb}}: {{W-|ceb|kape}}
* {{Z|sei}}: {{W-|sei|cafee}}, {{W-|sei|xica coopol}}
* {{Z|scn}}: {{W+|scn|cafè|n}}
* {{Z|szl}}: {{W-|szl|kafej|n}}, {{W-|szl|kafyj|n}}
* {{Z|sd}}: {{W-|sd|ڪافي}}, {{W-|sd|قهوو}}
* {{Z|srn}}: {{W-|srn|kofi}}
* {{Z|sh}}:
*: Kirîlî: {{W-|sh|ка̀фа|m}}, {{W-|sh|ка̑ва|m}}, {{W-|sh|кахва|m}}
*: Latînî: {{W+|sh|kàfa|m}}, {{W+|sh|kȃva|m}}, {{W+|sh|kahva|m}}
* {{Z|sco}}: {{W-|sco|coffee}}
* {{Z|sk}}: {{W+|sk|káva|m}}
* {{Z|sl}}: {{W+|sl|káva|m}}
* {{Z|so}}: {{W-|so|bun|n}}, {{W-|so|qahwe}}
* Sorbî:
*: {{Z|dsb}}: {{W-|dsb|kafej|n}}
*: {{Z|hsb}}: {{W-|hsb|kofej|n}}
* {{Z|nso}}: {{W-|nso|ditšhumamelômo}}, {{W-|nso|kôfi}}
* {{Z|st}}: {{W-|st|kofi}}
* {{Z|es}}: {{W+|es|café|n}}, {{W+|es|tinto|n}}, {{W+|es|feca|n}}
* {{Z|sw}}: {{W+|sw|kahawa}}
* {{Z|sv}}: {{W+|sv|kaffe|nt}}
* {{Z|zdj}}: {{W-|zdj|kafe}}, {{W-|zdj|kahawa}}
*: {{Z|swb}}: {{W-|swb|buni}}
* {{Z|sn}}: {{W-|sn|kofi}}
* {{Z|tab}}: {{W-|tab|кофе}}, {{W-|tab|къегьва}}, {{W-|tab|къягьва}}
* {{Z|tg}}: {{W+|tg|қаҳва}}
* {{Z|tl}}: {{W+|tl|kape}}
* {{Z|ty}}: {{W-|ty|taofe}}
* {{Z|tna}}: {{W-|tna|kajue}}
* {{Z|ta}}: {{W+|ta|காபி}}
* {{Z|tar}}: {{W-|tar|cajé}}
* {{Z|shi}}: {{W-|shi|l-qhwa}}
* {{Z|tsg}}: {{W-|tsg|kahawa}}
* {{Z|nod}}: {{W-|nod|ᨠᩣᨼᩯ}}
* {{Z|th}}: {{W+|th|กาแฟ}}
* {{Z|te}}: {{W+|te|కాఫీ}}
* {{Z|pah}}: {{W-|pah|kag̃wi}}
* {{Z|cux}}: {{W-|cux|café|cuda=ca{{sup|3}}fé{{sup|24}}}}
* {{Z|crh}}: {{W-|crh|qave}}
* {{Z|tt}}: {{W-|tt|каһвә}}
* {{Z|tet}}: {{W-|tet|kafé}}
* {{Z|ti}}: {{W+|ti|ቡን}}
* {{Z|tr}}: {{W+|tr|kahve}}, {{W+|tr|konur}}
* {{Z|tk}}: {{W-|tk|kofe}}
* {{Z|bo}}: {{W-|bo|ཁོ་ཕི}}, {{W-|bo|ཁ་ཕེ}}, {{W-|bo|ཇ་ཀོབ་པི}}, {{W-|bo|འཚིག་ཇ}}
* {{Z|tpi}}: {{W-|tpi|kopi}}
* {{Z|tos}}: {{W-|tos|capij}}
* {{Z|top}}: {{W-|top|café}}, {{W-|top|capé}}
* {{Z|mto}}: {{W-|mto|café}}
* {{Z|tus}}: {{W-|tus|káhwih}}
* {{Z|tvl}}: {{W-|tvl|kofe}}
* {{Z|tyv}}: {{W-|tyv|кофе}}
* {{Z|tzh}}: {{W-|tzh|cahpel}}
* {{Z|tzo}}: {{W-|tzo|kajve}}
* {{Z|udm}}: {{W-|udm|кофе}}
* {{Z|ur}}: {{W-|ur|قہوہ|n|tr=qahva}}, {{W-|ur|کوفی|m|tr=kofī}}, {{W-|ur|کافی|m|tr=kāfī}}
* {{Z|uk}}: {{W+|uk|ка́ва|m}}
* {{Z|vec}}: {{W+|vec|cafè|n}}
* {{Z|vep}}: {{W-|vep|kofe}}
* {{Z|vi}}: {{W+|vi|cà phê}}
* {{Z|vo}}: {{W+|vo|kaf}}
* {{Z|vro}}: {{W-|vro|kohv}}
* {{Z|wls}}: {{W-|wls|kafē}}
* {{Z|wa}}: {{W+|wa|cafè|n}}
* {{Z|lwg}}: {{W-|lwg|ikaawa}}
* {{Z|war}}: {{W-|war|kape}}
* {{Z|cy}}: {{W+|cy|coffi|n}}
* {{Z|wo}}: {{W-|wo|kafe}}
* {{Z|kjh}}: {{W-|kjh|кофе}}
* {{Z|km}}: {{W-|km|កាហ្វេ|tr=kahvé}}
* {{Z|xh}}: {{W-|xh|ikofu}}
* {{Z|sah}}: {{W-|sah|кофе}}
* {{Z|yi}}: {{W+|yi|קאַווע|m}}
* {{Z|yo}}: {{W-|yo|ohun mímu}}, {{W-|yo|kofi}}, {{W-|yo|kọfi}}
* {{Z|esu}}: {{W-|esu|kuuvviaq}}
* Zapotec:
*: {{Z|zaa}}: {{W-|zaa|café}}
*: {{Z|zav}}: {{W-|zav|cafe}}
*: {{Z|zpq}}: {{W-|zpq|café}}
* {{Z|el}}: {{W+|el|καφές|n}}
* {{Z|zoo}}: {{W-|zoo|café}}
* {{Z|zea}}: {{W-|zea|koffie}}
* {{Z|za}}: {{W-|za|gahfeih}}, {{W-|za|gyahfeih}}
* Zoque:
*: {{Z|zoc}}: {{W-|zoc|cajwe}}
*: {{Z|zor}}: {{W-|zor|kajwel}}
* {{Z|zu}}: {{W+|zu|ikhofi}}
{{werger-bin}}
=== Navdêr 2 ===
{{navdêr|ku|c=m}}
# [[qehwexane|Qehwexane]].
#* {{jêder-websîte|ku|jêgirtin=Xwendekarên min jî di tenefusan de hey digotin, “dikana dombîlî” û berê xwe didan wê derê, zilaman di nav xwe de digotin, “'''qehweya''' dombîlî” û li wir şerê xwe yê bi lîstikên kaxez û okeyê gûr dikirin.|nivîskar=w:Yaqob Tilermenî|sernav=Ma matematîk jî me dixapîne?|websîte=diyarname.com|url=https://www.diyarname.com/nivis/ma-matematik-ji-me-dixapine_13116|tarîx=25.08.2026}}
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Etîmolojî ====
{{ji?|ku}}
==== Bi zaravayên din ====
{{werger-ser|qehwexane|1}}
{{werger-bin}}
==== Werger ====
{{werger-ser|qehwexane|1}}
{{werger-bin}}
== {{ziman|zza}} ==
=== Navdêr ===
{{navdêr|zza|z=?}}
# {{g|ku|qehwe}}
l5tsus6wp4wye5uwlwlgy1zkil5yqqz
-mend
0
26784
6531927
6248994
2026-09-17T13:14:30Z
Wikihez
7344
/* Binere herwiha */ (bi [[MediaWiki:Gadget-AjaxEdit.js|AjaxEdit]]ê re)
6531927
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Paşgir ===
{{paşgir|ku}}
# Kesê ku tiştek yan hin tişt [[pir]] hene.
#:''[[behremend]]'' (kesê ku pirr bi[[behre]] ye), <br>''[[dewlemend]]'' (kesê ku pirr [[dewlet]] anku [[maliyet]] heye), <br>''[[salmend]]'' (kesê ku [[gelek]] [[sal]] hene anku [[pirr]] [[jîn|jiyaye]]
#:''[[imrmend]]'' (kesê ku pir jî, anku salên xwe gelek in)
#:''hêzmend'' (kesê ku pir hêz heye)
# Kesê ku [[mijûl]]î tiştekî ye.
#: ''[[hunermend]], [[karmend]], [[afirmend]], [[şêwirmend]]''
==== Formên alternatîv ====
* [[-men]] (2)
==== Bide ber ====
* [[-men]]
==== Herwiha binêre ====
* [[-bend]]
==== Jê ====
{{paşgirbibîne|ku}}
* [[-mendî]]
* [[-mendîtî]]
* [[-mendtî]]
8j69vdf59qa6n5lneif269co71yt9nk
6531938
6531927
2026-09-17T13:41:09Z
Balyozbot
7867
[[Special:PermanentLink/6298023|Bot]]: rêzkirina beşan
6531938
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Paşgir ===
{{paşgir|ku}}
# Kesê ku tiştek yan hin tişt [[pir]] hene.
#:''[[behremend]]'' (kesê ku pirr bi[[behre]] ye), <br>''[[dewlemend]]'' (kesê ku pirr [[dewlet]] anku [[maliyet]] heye), <br>''[[salmend]]'' (kesê ku [[gelek]] [[sal]] hene anku [[pirr]] [[jîn|jiyaye]]
#:''[[imrmend]]'' (kesê ku pir jî, anku salên xwe gelek in)
#:''hêzmend'' (kesê ku pir hêz heye)
# Kesê ku [[mijûl]]î tiştekî ye.
#: ''[[hunermend]], [[karmend]], [[afirmend]], [[şêwirmend]]''
==== Formên alternatîv ====
* [[-men]] (2)
==== Jê ====
{{paşgirbibîne|ku}}
* [[-mendî]]
* [[-mendîtî]]
* [[-mendtî]]
==== Bide ber ====
* [[-men]]
==== Herwiha binêre ====
* [[-bend]]
rkrrp9swqbslbsbl4vhguwn9pi1o1vj
biaqil
0
26860
6532009
6444565
2026-09-17T21:33:15Z
Wikihez
7344
Kurmancî/Rengdêr: maneyek hate rakirin, mane, hevmane hatin guhartin (DefSideBoxes)
6532009
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Rengdêr ===
{{ku-tewîn-rd|biaqil}}
{{rengdêr|ku}}
# Ne [[bêhiş]], ne [[şêt]], ne [[dîn]], ne [[bêaqil]], ne [[cahil]], ne [[nezan]].
#: {{hevmane|ku|hişmend|hişyar|aqildar|aqilmend|zekî|jîr|hêgin}}
==== Formên alternatîv ====
{{kol3|ku
|bi'aqil
|bieqil
|bieql
|bi'eqil
|bi'eql
}}
==== Jê ====
{{kol3|ku|cure=Jê
|biaqilbûn<pos:navdêr>
}}
==== Etîmolojî ====
Ji {{pêş|ku|bi-|aqil}}.
==== Werger ====
{{werger-ser}}
* {{Z|af}}: {{W+|af|wys}}
* {{Z|de}}: {{W+|de|gescheit}}, {{W+|de|klug}}, {{W+|de|weise}}, {{W+|de|vernünftig}}, {{W+|de|verständig}}, {{W+|de|raffiniert}}, {{W+|de|sittsam}}, {{W+|de|würdevoll}}, {{W+|de|würdig}}, {{W+|de|überlegt}}, {{W-|de|Intelligent|guman=erê}}, {{W+|de|Würde|guman=erê}}
* {{Z|da}}: {{W+|da|fornuftig}}
* {{Z|eo}}: {{W-|eo|saĝa}}, {{W+|eo|prudenta}}
* {{Z|fa}}: {{W+|fa|خردمند}}, {{W-|fa|عاقل مند}}, {{W-|fa|با هوش}}
* {{Z|fo}}: {{W-|fo|klókur}}, {{W-|fo|vísur}}, {{W-|fo|skilagóður}}, {{W-|fo|hyggin}}, {{W-|fo|rættsiktaður}}
* {{Z|fi}}: {{W+|fi|viisas}}, {{W+|fi|järkevä}}, {{W+|fi|fiksu}}
* {{Z|fr}}: {{W+|fr|sage}}, {{W+|fr|prudent}}, {{W+|fr|raisonnable}}
* {{Z|gd}}: {{W-|gd|glic}}
* {{Z|nl}}: {{W+|nl|verstandig}}, {{W+|nl|vroed}}, {{W+|nl|wijs}}
* {{Z|he}}: {{W+|he|חכם|cuda=חָכָם|R=ḥāxām}}
* {{Z|en}}: {{W+|en|wise}}, {{W+|en|sage}}, {{W+|en|sagacious}}, {{W+|en|prudent}}, {{W+|en|reasonable}}, {{W+|en|sensible}}, {{W+|en|farsighted}}, {{W+|en|judicious}}, {{W+|en|sound}}, {{W+|en|bright}}, {{W+|en|intelligent}}, {{W+|en|sane}}, {{W+|en|sapiential}}, {{W+|en|sapient}}, {{W+|en|clever|guman=erê}}, {{W-|en|çazan|guman=erê}}
* {{Z|it}}: {{W+|it|saggio}}, {{W+|it|sensato}}
* {{Z|ca}}: {{W+|ca|assenyat}}, {{W+|ca|sensat}}, {{W+|ca|prudent}}, {{W+|ca|raonable}}
* {{Z|la}}: {{W-|la|sapiens}}, {{W-|la|sophus}}
* {{Z|pl}}: {{W+|pl|mądry}}
* {{Z|pt}}: {{W+|pt|ajuizado}}, {{W+|pt|prudente}}, {{W+|pt|sábio}}, {{W+|pt|sensato}}, {{W+|pt|judicioso}}
* {{Z|ru}}: {{W+|ru|благоразумный}} (blagorazúmnyĭ)
* {{Z|srn}}: {{W-|srn|krin-ede}}
* {{Z|sk}}: {{W-|sk|rozumný}}, {{W-|sk|múdry}}
* {{Z|es}}: {{W+|es|sensato}}, {{W+|es|juicioso}}, {{W+|es|cuerdo}}, {{W+|es|acertado}}, {{W+|es|escarcha}}
* {{Z|sv}}: {{W+|sv|klok}}, {{W+|sv|vettig}}
* {{Z|tl}}: {{W-|tl|marúnon}}, {{W-|tl|matalíno}}
* {{Z|tr}}: {{W+|tr|akıllı}}, {{W+|tr|beyinli}}, {{W+|tr|upuslu}}, {{W+|tr|uslu akıllı}}, {{W+|tr|uslu}}
* {{Z|vi}}: {{W+|vi|khôn}}
* {{Z|el}}: {{W+|el|σοφός}} (sofós), {{W+|el|φρόνιμος}} (frónimos), {{W+|el|συνετός}} (synetós)
{{werger-bin}}
== {{ziman|zza}} ==
=== Rengdêr ===
{{rengdêr|zza}}
# biaqil
ryh1xs794eg5u38c6ozwd0nh3awl9q4
halxweş
0
26978
6531958
6373800
2026-09-17T14:55:26Z
Wikihez
7344
Kurmancî/Rengdêr: mane, hevmane hatin guhartin (DefSideBoxes)
6531958
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Rengdêr ===
{{ku-tewîn-rd|halxweş}}
{{rengdêr|ku}}
# Kesa/ê ku [[hal]]ê wî (bi taybetî ya [[aborî]]) [[baş]] ê.
#: {{hevmane|ku|zengîn|dewlemend|maldar|paredar}}
#* {{jêder|ku|jêgirtin=Rêberê PKK-ê got Ak Parti dixwaze sîstemeka wekî ya Alayên Hemîdiyeyê ava bike û bi wê rêyê kurdan bi dewletê ve girê bide û got Ak Parti li hemberî PKK-ê destê xwe dirêjî esnaf û '''halxweşên''' kurd dike da pê PKK-ê bişkêne.|Nefel.com, 8/2010}}
==== Formên alternatîv ====
* {{g|ku|halxoş}}
==== Jê ====
{{kol3|ku|cure=Jê
|halxweşî
|halxweş bûn<pos:lêker>
|halxweşbûn<pos:navdêr>
}}
==== Etîmolojî ====
Ji {{pêk|ku|[[hal]] (1)|xweş}}.
==== Bide ber ====
* {{g|ku|xweşhal}}
==== Werger ====
{{werger-ser}}
* {{Z|af}}: {{W+|af|ryk}}
* {{Z|sq}}: {{W+|sq|pasur}}
* {{Z|de}}: {{W+|de|ausgiebig}}, {{W+|de|reich}}, {{W+|de|vermögend}}
* {{Z|cs}}: {{W+|cs|bohatý}}
* {{Z|da}}: {{W+|da|rig}}
* {{Z|eo}}: {{W+|eo|riĉa}}
* {{Z|fa}}: {{W+|fa|ثروتمند}}
* {{Z|fo}}: {{W+|fo|ríkur}}
* {{Z|fi}}: {{W+|fi|varakas}}, {{W+|fi|vauras}}, {{W+|fi|rikas}}
* {{Z|fy}}: {{W+|fy|ryk}}
* {{Z|gd}}: {{W+|gd|beartach}}
* {{Z|nl}}: {{W+|nl|gefortuneerd}}, {{W+|nl|rijk}}, {{W+|nl|vermogend}}
* {{Z|he}}: {{W|he|שמן|cuda=שָׁמֵן}}
* {{Z|ang}}: {{W+|ang|rice}}
* {{Z|en}}: {{W+|en|wealthy}}, {{W+|en|well-off}}, {{W+|en|affluent}}, {{W+|en|well-to-do}}, {{W+|en|rich}}
* {{Z|it}}: {{W+|it|ricco}}
* {{Z|is}}: {{W+|is|ríkur}}
* {{Z|ca}}: {{W+|ca|ric}}, {{W+|ca|abundós}}
* {{Z|rw}}: {{W+|rw|kire}}
* {{Z|yua}}: {{W+|yua|ayik’al}}, {{W+|yua|taak’inal}}
* {{Z|hu}}: {{W+|hu|gazdag}}
* {{Z|no}}: {{W+|no|rik}}
* {{Z|pap}}: {{W+|pap|riko}}, {{W+|pap|riku}}
* {{Z|pl}}: {{W+|pl|bogaty}}
* {{Z|pt}}: {{W+|pt|rico}}
* {{Z|ro}}: {{W+|ro|bogat}}
* {{Z|ru}}: {{W+|ru|богатый}}
* {{Z|srn}}: {{W+|srn|gudu}}
* {{Z|es}}: {{W+|es|acaudalado}}, {{W+|es|acomodado}}, {{W+|es|adinerado}}, {{W+|es|rico}}
* {{Z|sw}}: {{W+|sw|tajiri}}
* {{Z|sv}}: {{W+|sv|rik}}
* {{Z|tl}}: {{W+|tl|mayáman}}
* {{Z|tr}}: {{W+|tr|zengin}}, {{W+|tr|iyi halli}}, {{W+|tr|variyetli}}, {{W+|tr|varlıklı}}, {{W+|tr|burjuva}}, {{W|tr|kapısında maymun oynuyor|guman=erê}}
{{werger-bin}}
==== Anagram ====
* {{anagram|ku|a=aehlşwx|xweşhal}}
bfwjsgqkh3vzjucvw83pmgrzomb1p7k
çayxane
0
30788
6531928
6349289
2026-09-17T13:17:35Z
Wikihez
7344
Kurmancî/Navdêr: beşa «Etîmolojî» hate nûkirin (DefSideBoxes)
6531928
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-IPA}}
* {{ku-kîte}}
=== Navdêr ===
[[Wêne:Slow_Life_in_A_Tea_House_01_edited.jpg|thumb|250x|çayxaneyek]]
{{navdêr|ku|z=m}}
# [[dikan|Dikana]] ku mirov dikare jê [[çay]]a (yan [[qehwe]]ya) [[kelandî]] [[kirîn|bikirre]] û li wir [[rûniştin|rûne]] û [[vexwarin|vexwe]].
#* {{J:Firat Cewerî:Girtî:1986|jêgirtin='''Çayxana''' Beko bi sê-çar sed metreyî dûrî stasyona trênê bû.}}
==== Tewandin ====
{{ku-tewîn-nav|mê|guherto=hemû}}
==== Formên alternatîv ====
{{kol3|ku
|çeyxane
}}
==== Nêzîk ====
{{kol3|ku
|qehwexane
|kafê
|kafeterya
|kantîn
}}
==== Jê ====
{{kol3|ku
|çayxaneçî
|çayxaneçîtî
|çayxanekar
|çayxanekarî
|çayxanevan
|çayxanevanî
}}
==== Etîmolojî ====
{{etîmon|ku|:qertaf|çay<id:vexwarin>|-xane|dar=1|metn=++}}
==== Werger ====
{{werger-ser}}
* {{Z|sq}}: {{W+|sq|çajtore|m}}
* {{Z|de}}: {{W+|de|Teehaus|nt}}, {{W+|de|Teestube|m}}
* {{Z|az}}: {{W-|az|çayevi}}, {{W-|az|çayxana}}
* {{Z|ba}}: {{W-|ba|сәйхана}}
* {{Z|be}}: {{W-|be|гарба́тная|m}}, {{W-|be|чайхана́|m}}
* {{Z|bn}}: {{W-|bn|চাঘর}}
* {{Z|bg}}: {{W-|bg|ча́йна|m}}
* {{Z|my}}: {{W-|my|လက်ဖက်ရည်ဆိုင်}}
* {{Z|cs}}: {{W+|cs|čajovna|m}}
* {{Z|zh}}:
*: {{Z|yue}}: {{W-|yue|茶居|tr=caa4 geoi1}}, {{W-|yue|茶館|tr=caa4 gun2}}
*: {{Z|dng}}: {{W-|dng|цагуан}}
*: {{Z|cmn}}: {{W+|cmn|茶館|tr=cháguǎn}}, {{W+|cmn|茶樓|tr=chálóu}}, {{W-|cmn|茶屋|tr=cháwū}}
*: {{Z|nan}}: {{W+|nan|茶店仔|tr=tê-tiàm-á}}, {{W+|nan|茶館|tr=tê-koán}}
* {{Z|da}}: {{W-|da|tehus|nt}}
* {{Z|ar}}: {{W-|ar|مَقْهًى|n}}, {{W-|ar|دَار شَاي|m}}, {{W-|ar|صَالَة اَلشَّاي|m|tr=ṣālat aš-šāy}}
* {{Z|hy}}: {{W+|hy|թեյարան}}
* {{Z|eo}}: {{W-|eo|teejo}}
* {{Z|et}}: {{W-|et|teemaja}}
* {{Z|fa}}: {{W+|fa|چایخانه|tr=čây-xâne}}
* {{Z|fi}}: {{W+|fi|teehuone}}
* {{Z|fr}}: {{W+|fr|salon de thé|n}}, {{W+|fr|maison de thé|m}}
* {{Z|gag}}: {{W-|gag|çay evi}}
* {{Z|ka}}: {{W-|ka|ჩაიხანა|sc=Geor}}
* {{Z|hi}}: {{W-|hi|चायख़ाना|n}}, {{W-|hi|चाय की दुकान|m}}
* {{Z|nl}}: {{W+|nl|theehuis|nt}}, {{W+|nl|tearoom}}, {{W-|nl|theesalon}}
* {{Z|he}}: {{W-|he|בֵּית תֵּה|n|tr=báyit te}}
* {{Z|io}}: {{W-|io|teerio}}
* {{Z|en}}: {{W+|en|teahouse}}, {{W+|en|tearoom}}
* {{Z|ga}}: {{W-|ga|teach tae|n}}
* {{Z|it}}: {{W-|it|sala da tè|m}}, {{W-|it|casa da tè|m}}
* {{Z|is}}: {{W-|is|tehús|nt}}
* {{Z|ja}}: {{W+|ja|喫茶店|tr=きっさてん, kissaten}}, {{W+|ja|茶店|tr=ちゃみせ, chamise, さてん, saten}}, {{W+|ja|茶室|tr=ちゃしつ, chashitsu}}, {{W+|ja|茶屋|tr=ちゃや, chaya}}, {{W+|ja|茶房|tr=さぼ, sabo}}
* {{Z|ky}}: {{W+|ky|чайкана|sc=Cyrl}}
* {{Z|ko}}: {{W+|ko|찻집}}, {{W+|ko|다방}}
* {{Z|lv}}: {{W-|lv|tējnīca|m}}
* {{Z|lo}}: {{W-|lo|ຮ້ານຊາ}}
* {{Z|lt}}: {{W-|lt|arbatinė|m}}
* {{Z|mk}}: {{W-|mk|чајџилница|m}}
* {{Z|mnc}}: {{W-|mnc|ᠴᠠᡳ ᡦᡠᠰᡝᠯᡳ}}
* {{Z|hu}}: {{W+|hu|teaház}}
* {{Z|mn}}:
*: Kirîlî: {{W-|mn|цайны газар}}
*: Mongolî: {{W-|mn|ᠴᠠᠢ ᠶᠢᠨ ᠭᠠᠵᠠᠷ}}
* {{Z|no}}:
*: {{Z|nb}}: {{W-|nb|tehus|nt}}
* {{Z|ota}}: {{W-|ota|چایخانه|tr=çayhane}}
* {{Z|ug}}: {{W-|ug|چايخانا|sc=ug-Arab}}
* {{Z|uz}}: {{W+|uz|choyxona}}, {{W-|uz|чойхона|sc=Cyrl}}
* {{Z|ps}}: {{W-|ps|چايخانه|m|tr=čâyxāná}}
* {{Z|pl}}: {{W+|pl|herbaciarnia|m}}
* {{Z|pt}}: {{W-|pt|casa de chá|m}}, {{W-|pt|casa de chás|m}}
* {{Z|pa}}: {{W-|pa|ਚਾਹਖ਼ਾਨਾ}}
* {{Z|kk}}: {{W-|kk|шайхана}}
* {{Z|ro}}: {{W+|ro|ceainărie|m}}, {{W-|ro|ceainărie-cofetărie|m}}
* {{Z|ru}}: {{W+|ru|ча́йная|m}}, {{W+|ru|чайхана́|m}}
* {{Z|sh}}:
*: Kirîlî: {{W-|sh|чајџиница|m}}
*: Latînî: {{W-|sh|čajdžinica|m}}
* {{Z|sk}}: {{W+|sk|čajovňa|m}}
* {{Z|sl}}: {{W-|sl|čajnica|m}}
* {{Z|es}}: {{W-|es|casa de té|m}}, {{W-|es|salón de té|n}}
* {{Z|sv}}: {{W-|sv|tehus|nt}}
* {{Z|tg}}: {{W+|tg|чойхона|sc=Cyrl}}
* {{Z|th}}: {{W-|th|โรงน้ำชา}}
* {{Z|tt}}: {{W+|tt|чәйханә}}
* {{Z|tr}}: {{W+|tr|çayhane}}, {{W+|tr|çayevi}}
* {{Z|tk}}: {{W-|tk|çayhana}}
* {{Z|bo}}: {{W-|bo|ཇ་ཁང}}
* {{Z|ur}}: {{W-|ur|چایخانہ|n|tr=cāyxāna}}
* {{Z|uk}}: {{W-|uk|ча́йна|m|sc=Cyrl}}, {{W-|uk|чайхана́|m|sc=Cyrl}}
* {{Z|uum}}: {{W-|uum|чайхане|tr=çayhane}}
* {{Z|vi}}: {{W-|vi|phòng trà}}, {{W-|vi|quán trà}}
* {{Z|cy}}: {{W-|cy|tŷ te|n}}
* {{Z|km}}: {{W-|km|ហាងតែ}}
* {{Z|yi}}: {{W-|yi|טיי-הויז|nt}}
* {{Z|el}}: {{W-|el|τσαγερία|m}}
{{werger-bin}}
:{{werger çavkanî|en|teahouse|beş=Ev qismê Wergerê|oldid=73508214}}
qigaw9xhd7syzx7y0mbmu1dpgwrs9m4
6531934
6531928
2026-09-17T13:23:36Z
Wikihez
7344
/* Etîmolojî */ (bi [[MediaWiki:Gadget-AjaxEdit.js|AjaxEdit]]ê re)
6531934
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-IPA}}
* {{ku-kîte}}
=== Navdêr ===
[[Wêne:Slow_Life_in_A_Tea_House_01_edited.jpg|thumb|250x|çayxaneyek]]
{{navdêr|ku|z=m}}
# [[dikan|Dikana]] ku mirov dikare jê [[çay]]a (yan [[qehwe]]ya) [[kelandî]] [[kirîn|bikirre]] û li wir [[rûniştin|rûne]] û [[vexwarin|vexwe]].
#* {{J:Firat Cewerî:Girtî:1986|jêgirtin='''Çayxana''' Beko bi sê-çar sed metreyî dûrî stasyona trênê bû.}}
==== Tewandin ====
{{ku-tewîn-nav|mê|guherto=hemû}}
==== Formên alternatîv ====
{{kol3|ku
|çeyxane
}}
==== Nêzîk ====
{{kol3|ku
|qehwexane
|kafê
|kafeterya
|kantîn
}}
==== Jê ====
{{kol3|ku
|çayxaneçî
|çayxaneçîtî
|çayxanekar
|çayxanekarî
|çayxanevan
|çayxanevanî
}}
==== Etîmolojî ====
{{etîmon|ku|:qertaf|çay<id:vexwarina germ>|-xane|dar=1|metn=++}}
==== Werger ====
{{werger-ser}}
* {{Z|sq}}: {{W+|sq|çajtore|m}}
* {{Z|de}}: {{W+|de|Teehaus|nt}}, {{W+|de|Teestube|m}}
* {{Z|az}}: {{W-|az|çayevi}}, {{W-|az|çayxana}}
* {{Z|ba}}: {{W-|ba|сәйхана}}
* {{Z|be}}: {{W-|be|гарба́тная|m}}, {{W-|be|чайхана́|m}}
* {{Z|bn}}: {{W-|bn|চাঘর}}
* {{Z|bg}}: {{W-|bg|ча́йна|m}}
* {{Z|my}}: {{W-|my|လက်ဖက်ရည်ဆိုင်}}
* {{Z|cs}}: {{W+|cs|čajovna|m}}
* {{Z|zh}}:
*: {{Z|yue}}: {{W-|yue|茶居|tr=caa4 geoi1}}, {{W-|yue|茶館|tr=caa4 gun2}}
*: {{Z|dng}}: {{W-|dng|цагуан}}
*: {{Z|cmn}}: {{W+|cmn|茶館|tr=cháguǎn}}, {{W+|cmn|茶樓|tr=chálóu}}, {{W-|cmn|茶屋|tr=cháwū}}
*: {{Z|nan}}: {{W+|nan|茶店仔|tr=tê-tiàm-á}}, {{W+|nan|茶館|tr=tê-koán}}
* {{Z|da}}: {{W-|da|tehus|nt}}
* {{Z|ar}}: {{W-|ar|مَقْهًى|n}}, {{W-|ar|دَار شَاي|m}}, {{W-|ar|صَالَة اَلشَّاي|m|tr=ṣālat aš-šāy}}
* {{Z|hy}}: {{W+|hy|թեյարան}}
* {{Z|eo}}: {{W-|eo|teejo}}
* {{Z|et}}: {{W-|et|teemaja}}
* {{Z|fa}}: {{W+|fa|چایخانه|tr=čây-xâne}}
* {{Z|fi}}: {{W+|fi|teehuone}}
* {{Z|fr}}: {{W+|fr|salon de thé|n}}, {{W+|fr|maison de thé|m}}
* {{Z|gag}}: {{W-|gag|çay evi}}
* {{Z|ka}}: {{W-|ka|ჩაიხანა|sc=Geor}}
* {{Z|hi}}: {{W-|hi|चायख़ाना|n}}, {{W-|hi|चाय की दुकान|m}}
* {{Z|nl}}: {{W+|nl|theehuis|nt}}, {{W+|nl|tearoom}}, {{W-|nl|theesalon}}
* {{Z|he}}: {{W-|he|בֵּית תֵּה|n|tr=báyit te}}
* {{Z|io}}: {{W-|io|teerio}}
* {{Z|en}}: {{W+|en|teahouse}}, {{W+|en|tearoom}}
* {{Z|ga}}: {{W-|ga|teach tae|n}}
* {{Z|it}}: {{W-|it|sala da tè|m}}, {{W-|it|casa da tè|m}}
* {{Z|is}}: {{W-|is|tehús|nt}}
* {{Z|ja}}: {{W+|ja|喫茶店|tr=きっさてん, kissaten}}, {{W+|ja|茶店|tr=ちゃみせ, chamise, さてん, saten}}, {{W+|ja|茶室|tr=ちゃしつ, chashitsu}}, {{W+|ja|茶屋|tr=ちゃや, chaya}}, {{W+|ja|茶房|tr=さぼ, sabo}}
* {{Z|ky}}: {{W+|ky|чайкана|sc=Cyrl}}
* {{Z|ko}}: {{W+|ko|찻집}}, {{W+|ko|다방}}
* {{Z|lv}}: {{W-|lv|tējnīca|m}}
* {{Z|lo}}: {{W-|lo|ຮ້ານຊາ}}
* {{Z|lt}}: {{W-|lt|arbatinė|m}}
* {{Z|mk}}: {{W-|mk|чајџилница|m}}
* {{Z|mnc}}: {{W-|mnc|ᠴᠠᡳ ᡦᡠᠰᡝᠯᡳ}}
* {{Z|hu}}: {{W+|hu|teaház}}
* {{Z|mn}}:
*: Kirîlî: {{W-|mn|цайны газар}}
*: Mongolî: {{W-|mn|ᠴᠠᠢ ᠶᠢᠨ ᠭᠠᠵᠠᠷ}}
* {{Z|no}}:
*: {{Z|nb}}: {{W-|nb|tehus|nt}}
* {{Z|ota}}: {{W-|ota|چایخانه|tr=çayhane}}
* {{Z|ug}}: {{W-|ug|چايخانا|sc=ug-Arab}}
* {{Z|uz}}: {{W+|uz|choyxona}}, {{W-|uz|чойхона|sc=Cyrl}}
* {{Z|ps}}: {{W-|ps|چايخانه|m|tr=čâyxāná}}
* {{Z|pl}}: {{W+|pl|herbaciarnia|m}}
* {{Z|pt}}: {{W-|pt|casa de chá|m}}, {{W-|pt|casa de chás|m}}
* {{Z|pa}}: {{W-|pa|ਚਾਹਖ਼ਾਨਾ}}
* {{Z|kk}}: {{W-|kk|шайхана}}
* {{Z|ro}}: {{W+|ro|ceainărie|m}}, {{W-|ro|ceainărie-cofetărie|m}}
* {{Z|ru}}: {{W+|ru|ча́йная|m}}, {{W+|ru|чайхана́|m}}
* {{Z|sh}}:
*: Kirîlî: {{W-|sh|чајџиница|m}}
*: Latînî: {{W-|sh|čajdžinica|m}}
* {{Z|sk}}: {{W+|sk|čajovňa|m}}
* {{Z|sl}}: {{W-|sl|čajnica|m}}
* {{Z|es}}: {{W-|es|casa de té|m}}, {{W-|es|salón de té|n}}
* {{Z|sv}}: {{W-|sv|tehus|nt}}
* {{Z|tg}}: {{W+|tg|чойхона|sc=Cyrl}}
* {{Z|th}}: {{W-|th|โรงน้ำชา}}
* {{Z|tt}}: {{W+|tt|чәйханә}}
* {{Z|tr}}: {{W+|tr|çayhane}}, {{W+|tr|çayevi}}
* {{Z|tk}}: {{W-|tk|çayhana}}
* {{Z|bo}}: {{W-|bo|ཇ་ཁང}}
* {{Z|ur}}: {{W-|ur|چایخانہ|n|tr=cāyxāna}}
* {{Z|uk}}: {{W-|uk|ча́йна|m|sc=Cyrl}}, {{W-|uk|чайхана́|m|sc=Cyrl}}
* {{Z|uum}}: {{W-|uum|чайхане|tr=çayhane}}
* {{Z|vi}}: {{W-|vi|phòng trà}}, {{W-|vi|quán trà}}
* {{Z|cy}}: {{W-|cy|tŷ te|n}}
* {{Z|km}}: {{W-|km|ហាងតែ}}
* {{Z|yi}}: {{W-|yi|טיי-הויז|nt}}
* {{Z|el}}: {{W-|el|τσαγερία|m}}
{{werger-bin}}
:{{werger çavkanî|en|teahouse|beş=Ev qismê Wergerê|oldid=73508214}}
h7gvl8h2vah246dpqlv310e9cr9xo42
elektron
0
39094
6532003
6458327
2026-09-17T20:33:58Z
Wikihez
7344
Kurmancî/Navdêr: beşa «Etîmolojî» hate nûkirin, beşa «Hevmane» hate rakirin (DefSideBoxes)
6532003
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Navdêr ===
{{navdêr|ku|z=m}}
# {{f|ku|fizîk}} [[pirt|Pirtên]] [[atom]]ê yên [[negatîv]] yên ku li dor [[navok]]ê [[zivirîn|dizivirin]].
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Formên alternatîv ====
* [[elektiron]]
* [[êlêktron]]
* [[êlêktiron]]
==== Jê ====
* [[elektronî]]
* [[elektronîk]]
* [[elektronîkî]]
==== Têkildar ====
* [[elektrîk]]
==== Etîmolojî ====
{{etîmon|ku|:deyn|en:electron|dar=1|metn=++}}
==== Bide ber ====
* [[proton]]
==== Werger ====
{{werger-ser}}
* {{Z|af}}: {{W+|af|elektron}}
* {{Z|de}}: {{W|de|Elektron|n}}
* {{Z|bg}}: {{W|bg|електрон|m}}
* {{Z|zh}}: {{W+|zh|電子}}, {{W+|zh|电子}}
* {{Z|ar}}: {{W|ar|إلكترون }}
* {{Z|eo}}: {{W+|eo|elektrono}}
* {{Z|fa}}: {{W+|fa|الکترون}}
* {{Z|fi}}: {{W+|fi|elektroni}}
* {{Z|fr}}: {{W+|fr|électron}}
* {{Z|ka}}: {{W+|ka|ელექტრონი}}
* {{Z|nl}}: {{W+|nl|elektron}}
* {{Z|en}}: {{W+|en|electron}}
* {{Z|it}}: {{W+|it|elettrone}}
* {{Z|is}}: {{W+|is|rafeind}}
* {{Z|ja}}: {{W|ja|電子|tr=[[でんし]], denshi}}
* {{Z|ko}}: {{W+|ko|전자}}
* {{Z|lv}}: {{W+|lv|elektrons}}
* {{Z|pl}}: {{W+|pl|elektron}}
* {{Z|pt}}: {{W+|pt|eléctron}}, {{W+|pt|electrônio}}
* {{Z|ru}}: {{W+|ru|электрон}}
* {{Z|es}}: {{W+|es|electrón}}
* {{Z|sv}}: {{W+|sv|elektron}}
* {{Z|tr}}: {{W+|tr|elektron}}
* {{Z|el}}: {{W+|el|ηλεκτρόνιο}}
{{werger-bin}}
== {{ziman|tr}} ==
=== Bilêvkirin ===
* {{deng|tr|LL-Q256 (tur)-ToprakM-elektron.wav|Deng|dever=}}
=== Navdêr ===
{{navdêr|tr}}
# {{f|tr|fizîk}} {{g|ku|elektron}}
oa9uu9llthi8fg16f2gfnphlzdplxmt
xêrnexwaz
0
41737
6531979
6462220
2026-09-17T16:20:32Z
Wikihez
7344
Kurmancî/Rengdêr: mane, hevmane hatin guhartin, beşa «Etîmolojî» hate nûkirin (DefSideBoxes)
6531979
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Rengdêr ===
{{ku-tewîn-rd|xêrnexwaz}}
{{rengdêr|ku}}
# Kesê ku [[xwestin|dixwaze]] tiştên [[xirab]] bike.
#: {{hevmane|ku|xirabîxwaz|bedniyet|niyetxirab|belakir}}
==== Formên alternatîv ====
* [[nexêrxwaz]]
* [[nexêrxaz]]
* [[xêrnexaz]]
==== Jê ====
* [[xêrnexwazî]]
* [[xêrnexwazîtî]]
==== Etîmolojî ====
{{etîmon|ku|:qertaf|xêr|ne|-xwaz|dar=1|metn=++}}.
==== Werger ====
{{werger-ser}}
* {{Z|de}}: {{W+|de|böswillig}}, {{W-|de|übel gesinnt}}, {{W-|de|Übelwollender|guman=erê}}, {{W+|de|Neider|guman=erê}}
* {{Z|fa}}: {{W+|fa|دشمن}}, {{W+|fa|رقیب}}
* {{Z|en}}: {{W+|en|unrighteous}}, {{W+|en|rotter}}
* {{Z|tr}}: {{W+|tr|fena kalpli}}, {{W-|tr|gönlü kara}}, {{W+|tr|kepaze}}, {{W+|tr|kötücül}}, {{W+|tr|kötülükçü}}, {{W+|tr|mürüvvetsiz}}, {{W+|tr|bedhah}}
{{werger-bin}}
==== Anagram ====
* {{anagram|ku|a=aeênrwxxz|nexêrxwaz}}
jtytdv47sri867g2eqqsdgx6r63xzgt
6531980
6531979
2026-09-17T16:20:42Z
Wikihez
7344
/* Etîmolojî */ (bi [[MediaWiki:Gadget-AjaxEdit.js|AjaxEdit]]ê re)
6531980
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Rengdêr ===
{{ku-tewîn-rd|xêrnexwaz}}
{{rengdêr|ku}}
# Kesê ku [[xwestin|dixwaze]] tiştên [[xirab]] bike.
#: {{hevmane|ku|xirabîxwaz|bedniyet|niyetxirab|belakir}}
==== Formên alternatîv ====
* [[nexêrxwaz]]
* [[nexêrxaz]]
* [[xêrnexaz]]
==== Jê ====
* [[xêrnexwazî]]
* [[xêrnexwazîtî]]
==== Etîmolojî ====
{{etîmon|ku|:qertaf|xêr|ne|-xwaz|dar=1|metn=++}}
==== Werger ====
{{werger-ser}}
* {{Z|de}}: {{W+|de|böswillig}}, {{W-|de|übel gesinnt}}, {{W-|de|Übelwollender|guman=erê}}, {{W+|de|Neider|guman=erê}}
* {{Z|fa}}: {{W+|fa|دشمن}}, {{W+|fa|رقیب}}
* {{Z|en}}: {{W+|en|unrighteous}}, {{W+|en|rotter}}
* {{Z|tr}}: {{W+|tr|fena kalpli}}, {{W-|tr|gönlü kara}}, {{W+|tr|kepaze}}, {{W+|tr|kötücül}}, {{W+|tr|kötülükçü}}, {{W+|tr|mürüvvetsiz}}, {{W+|tr|bedhah}}
{{werger-bin}}
==== Anagram ====
* {{anagram|ku|a=aeênrwxxz|nexêrxwaz}}
b38ppw3pl1u7hgiqdosp7h4tj1amm6f
Şablon:tewandinên rêzimanî/ku
10
42954
6531969
5883951
2026-09-17T16:00:44Z
Wikihez
7344
6531969
wikitext
text/x-wiki
{{kol3|ku|sernav=tewandinên rêzimanî
|[[abesîv]]
|[[ablatîv]]
|[[absolutîv]]
|[[akûzatîv]]
|[[adîrektîv]]
|[[adelatîv]]
|[[adesîv]]
|[[hoker]]
|[[alatîv]]
|[[antesîv]]
|[[apûdesîv]]
|[[aversîv]]
|[[benefaktîv]]
|[[karîtîv]], [[kirde]]
|[[bireser]]
|[[komîtatîv]]
|[[datîv]]
|[[delatîv]]
|[[dîstantitîv]]
|[[dîstrîbûtîv]]
|[[dûbîtîv]]
|[[elatîv]]
|[[ergatîv]]
|[[esîv]]
|[[ekwatîv]]
|[[aversîv]]
|[[evîtatîv]]
|[[eksesîv]]
|[[genetîv]]
|[[îlatîv]]
|[[înelatîv]]
|[[înesîv]]
|[[înstruktîv]]
|[[enstrûmental]]
|[[întransîtîv]]
|[[komparatîv]]
|[[latîv]]
|[[lokatîv]]
|[[mediyal]]
|[[multîplîkatîv]]
|[[nomînatîv]]
|[[partîtîv]]
|[[pegatîv]]
|[[perlatîv]]
|[[posesîv]]
|[[postelatîv]]
|[[postdîrektîv]]
|[[postesîv]]
|[[postpozîsyonal]]
|[[prepozîsyonal]]
|[[prîvatîv]]
|[[prolatîv]]
|[[prozekûtîv]]
|[[proksîmatîv]]
|[[separatîv]]
|[[sosyatîv]]
|[[sûbdîrektîv]]
|[[sûbesîv]]
|[[sûpelatîv]]
|[[sûblatîv]]
|[[sûperdîrektîv]]
|[[sûperesîv]]
|[[sûperlatîv]]
|[[sûpresîv]]
|[[temporal]]
|[[termînatîv]]
|[[translatîv]]
|[[viyalîs]]
|[[vokatîv]]
}}<noinclude>
[[Kategorî:Şablonên lîsteyan bi {{navê zimanî|ku}}]]
</noinclude>
chy3r3m8ruewaa7s3mh6pjjpqolk3zx
6531970
6531969
2026-09-17T16:00:52Z
Wikihez
7344
6531970
wikitext
text/x-wiki
{{kol3|ku|sernav=Tewandinên rêzimanî
|[[abesîv]]
|[[ablatîv]]
|[[absolutîv]]
|[[akûzatîv]]
|[[adîrektîv]]
|[[adelatîv]]
|[[adesîv]]
|[[hoker]]
|[[alatîv]]
|[[antesîv]]
|[[apûdesîv]]
|[[aversîv]]
|[[benefaktîv]]
|[[karîtîv]], [[kirde]]
|[[bireser]]
|[[komîtatîv]]
|[[datîv]]
|[[delatîv]]
|[[dîstantitîv]]
|[[dîstrîbûtîv]]
|[[dûbîtîv]]
|[[elatîv]]
|[[ergatîv]]
|[[esîv]]
|[[ekwatîv]]
|[[aversîv]]
|[[evîtatîv]]
|[[eksesîv]]
|[[genetîv]]
|[[îlatîv]]
|[[înelatîv]]
|[[înesîv]]
|[[înstruktîv]]
|[[enstrûmental]]
|[[întransîtîv]]
|[[komparatîv]]
|[[latîv]]
|[[lokatîv]]
|[[mediyal]]
|[[multîplîkatîv]]
|[[nomînatîv]]
|[[partîtîv]]
|[[pegatîv]]
|[[perlatîv]]
|[[posesîv]]
|[[postelatîv]]
|[[postdîrektîv]]
|[[postesîv]]
|[[postpozîsyonal]]
|[[prepozîsyonal]]
|[[prîvatîv]]
|[[prolatîv]]
|[[prozekûtîv]]
|[[proksîmatîv]]
|[[separatîv]]
|[[sosyatîv]]
|[[sûbdîrektîv]]
|[[sûbesîv]]
|[[sûpelatîv]]
|[[sûblatîv]]
|[[sûperdîrektîv]]
|[[sûperesîv]]
|[[sûperlatîv]]
|[[sûpresîv]]
|[[temporal]]
|[[termînatîv]]
|[[translatîv]]
|[[viyalîs]]
|[[vokatîv]]
}}<noinclude>
[[Kategorî:Şablonên lîsteyan bi {{navê zimanî|ku}}]]
</noinclude>
26i3iixs11v8cht2uuhjsnllch9r6hb
nomînatîv
0
43444
6531972
6242931
2026-09-17T16:03:16Z
Wikihez
7344
Kurmancî/Navdêr: ±nimûneya bikaranînê bi amûra « [[Mediawiki:Gadget-wikt.add-examples|Nimûne zêde bike]] » (v2.0), mane, hevmane hatin guhartin (DefSideBoxes)
6531972
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Navdêr ===
{{navdêr|ku|z=}}
# {{f|ku|rêzimanî}} [[form|Form]]a [[navdêr]]an yan [[cînav]]an ya [[netewandî]].
#: {{hevmane|ku|navkî}}
#: {{nimûne|ku|"Gul" '''nomînatîv''' e lê "gulê /gula / gulan / gulên" na.}}
==== Formên alternatîv ====
* [[nomînatîf]]
==== Têkildar ====
{{tewandinên rêzimanî/ku}}
==== Etîmolojî ====
Deyn ji {{deyn|en|ku|nominative}} yan {{deyn|fr|ku|nominatif}} ji {{ji|la|ku|nominativus}} ji {{q|la|nōmen|w=[[nav]]}}.
==== Werger ====
{{werger-ser}}
* {{Z|de}}: {{W+|de|Nominativ|m}}
* {{Z|be}}: [[назоўны склон]] (nazowny sklon)
* {{Z|zh}}: [[主格]] (zhǔgé)
* {{Z|ar}}: {{W-|ar|حالة الرفع|cuda=حَالةُ الرَّفْع|tr=ħaletu r-ref'u}}
* {{Z|fo}}: {{W+|fo|hvørfall|n}}
* {{Z|fi}}: {{W+|fi|nominatiivi}}
* {{Z|fr}}: {{W+|fr|nominatif|m}}
* {{Z|nl}}: {{W+|nl|eerste naamval|m}}, {{W+|nl|nominatief|m}}
* {{Z|he}}: [[נושא]] (nosé)
* {{Z|ang}}: {{W-|ang|nemniendlic cásus}}
* {{Z|en}}: {{W+|en|nominative}}, {{W+|en|nominative case}}
* {{Z|it}}: {{W-|it|caso nominativo|m}}
* {{Z|is}}: {{W+|is|nefnifall|n}}
* {{Z|ja}}: {{W+|ja|主格|tr=[[しゅかく]], shukaku}}
* {{Z|la}}: [[casus]] [[nominativus]]
* {{Z|lt}}: {{W+|lt|vardininkas|m}}
* {{Z|pl}}: {{W-|pl|nominacyjny|m}}, {{W+|pl|mianownik|m}}
* {{Z|pt}}: {{W-|pt|caso nominativo|m}}
* {{Z|ro}}: {{W-|ro|cazul nominativ}}
* {{Z|ru}}: [[именительный падеж]] (imenítel’nyj padéž)
* {{Z|sh}}: {{W+|sh|nominativ|m}}
* {{Z|sk}}: {{W-|sk|prvý pád|m}}, {{W+|sk|nominatív|m}}
* {{Z|es}}: {{W-|es|caso nominativo|m}}
* {{Z|sv}}: {{W+|sv|nominativ}}
* {{Z|tr}}: {{W-|tr|ismin yalın hali}}
* [[volapûkî]]: [[kimfal]]
* {{Z|vi}}: {{W-|vi|nguyên cách}}
* {{Z|el}}: {{IPAchar|[[ὀνομαστική]]}} (onomastikí)
{{werger-bin}}
ffvj5sb94fhwewgea9ygbcbellthq91
6531992
6531972
2026-09-17T17:12:06Z
Balyozbot
7867
Bot: Paqijiyên kozmetîk (Lînk paqij kir.)
6531992
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Navdêr ===
{{navdêr|ku|z=}}
# {{f|ku|rêzimanî}} [[form|Forma]] [[navdêr]]an yan [[cînav]]an ya [[netewandî]].
#: {{hevmane|ku|navkî}}
#: {{nimûne|ku|"Gul" '''nomînatîv''' e lê "gulê /gula / gulan / gulên" na.}}
==== Formên alternatîv ====
* [[nomînatîf]]
==== Têkildar ====
{{tewandinên rêzimanî/ku}}
==== Etîmolojî ====
Deyn ji {{deyn|en|ku|nominative}} yan {{deyn|fr|ku|nominatif}} ji {{ji|la|ku|nominativus}} ji {{q|la|nōmen|w=[[nav]]}}.
==== Werger ====
{{werger-ser}}
* {{Z|de}}: {{W+|de|Nominativ|m}}
* {{Z|be}}: [[назоўны склон]] (nazowny sklon)
* {{Z|zh}}: [[主格]] (zhǔgé)
* {{Z|ar}}: {{W-|ar|حالة الرفع|cuda=حَالةُ الرَّفْع|tr=ħaletu r-ref'u}}
* {{Z|fo}}: {{W+|fo|hvørfall|n}}
* {{Z|fi}}: {{W+|fi|nominatiivi}}
* {{Z|fr}}: {{W+|fr|nominatif|m}}
* {{Z|nl}}: {{W+|nl|eerste naamval|m}}, {{W+|nl|nominatief|m}}
* {{Z|he}}: [[נושא]] (nosé)
* {{Z|ang}}: {{W-|ang|nemniendlic cásus}}
* {{Z|en}}: {{W+|en|nominative}}, {{W+|en|nominative case}}
* {{Z|it}}: {{W-|it|caso nominativo|m}}
* {{Z|is}}: {{W+|is|nefnifall|n}}
* {{Z|ja}}: {{W+|ja|主格|tr=[[しゅかく]], shukaku}}
* {{Z|la}}: [[casus]] [[nominativus]]
* {{Z|lt}}: {{W+|lt|vardininkas|m}}
* {{Z|pl}}: {{W-|pl|nominacyjny|m}}, {{W+|pl|mianownik|m}}
* {{Z|pt}}: {{W-|pt|caso nominativo|m}}
* {{Z|ro}}: {{W-|ro|cazul nominativ}}
* {{Z|ru}}: [[именительный падеж]] (imenítel’nyj padéž)
* {{Z|sh}}: {{W+|sh|nominativ|m}}
* {{Z|sk}}: {{W-|sk|prvý pád|m}}, {{W+|sk|nominatív|m}}
* {{Z|es}}: {{W-|es|caso nominativo|m}}
* {{Z|sv}}: {{W+|sv|nominativ}}
* {{Z|tr}}: {{W-|tr|ismin yalın hali}}
* [[volapûkî]]: [[kimfal]]
* {{Z|vi}}: {{W-|vi|nguyên cách}}
* {{Z|el}}: {{IPAchar|[[ὀνομαστική]]}} (onomastikí)
{{werger-bin}}
cziofnde44lsr79cqe6w8hcrpttxi1j
-hane
0
44037
6531946
4637743
2026-09-17T14:19:25Z
Wikihez
7344
/* Etîmolojî */
6531946
wikitext
text/x-wiki
== {{ziman|tr}} ==
=== Paşgir ===
{{paşgir|tr}}
# [[-xane]], [[-geh]]
==== Etîmolojî ====
{{etîmon|tr|:mîr|ota:ـخانه<tr:-hane><etimon:deyn<fa:خانه<tr:-xane>>>|dar=1|metn=+}}
ogx67c0u9700oiggxzeiuwqse1il6ni
xirar
0
50522
6531999
6349631
2026-09-17T19:59:41Z
Wikihez
7344
/* Werger */ (bi [[MediaWiki:Gadget-AjaxEdit.js|AjaxEdit]]ê re)
6531999
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-IPA}}
* {{ku-kîte}}
=== Navdêr ===
[[Wêne:St Aspais Melun - cilice.jpg|thumb|xirar]]
{{navdêr|ku|z=n}}
# [[telîs|Telîsê]] mezin yê ku ji [[pembo]] hatiye [[çêkirin]] û [[piranî]] [[pembo]] û [[ka]] tên xistin.
#* {{jêder-websîte|ku|jêgirtin=Serê şêst salên rebeq e karekê bê rawestiyan dihête kirin da soranî bibe zimanê serekî yê Kurdistanê û dergehên herêmê li himber 50 milyon kurdên Kurdistana mezin bihêne girtin. Ev xale jî bêguman mistek e ji '''xirarekî'''.|nivîskar=Îsmayîl Tahir|sernav=Kurmanciya jorî li ber metirsiyên mezin û giran e|websîte=gelavej.com|sal=7/2008}}
==== Tewandin ====
{{ku-tewîn-nav|nêr}}
==== Bi alfabeyên din ====
* {{ku-ar|خرار}}
==== Hevmane ====
{{kol3|ku
|torbe
|telîs
|tûr
|çiwal
|tûrik
|gînîk
}}
==== Etîmolojî ====
Deyn ji {{deyn|ar|ku|غرارة|cuda=غِرَارَة|w=[[tûrik]]}}
{{kontrolWerger}}
==== Werger ====
{{werger-ser}}
* {{Z|ar}}: {{W-|ar|خيش}}, {{W-|ar|غرارة}}, {{W-|ar|الغرارة|guman=erê}}, {{W-|ar|وطیئة|guman=erê}}
* {{Z|en}}: {{W+|en|sackcloth}}
* {{Z|tr}}: {{W+|tr|çuval}}, {{W+|tr|harar}}, {{W-|tr|[[büyük]] [[saman]] [[çuval]]ı}}
{{werger-bin}}
== {{ziman|gl}} ==
=== Lêker ===
{{lêker|gl}}
# {{g|ku|zivirîn}}
=== Çavkanî ===
{{rûpela wergerê|gl|zivirîn}}
lge3jo3etrq0lzkevynfqe6kvlz4f5e
mamiz
0
51983
6531974
6473855
2026-09-17T16:11:12Z
Wikihez
7344
Kurmancî/Jê: beşa «Jê» hate nûkirin (DefSideBoxes)
6531974
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Navdêr ===
[[Wêne:Marsh deer.jpg|thumb|mamiz]]
{{navdêr|ku|z=m}}
# [[ask]], [[xezal]], [[ahû]]
==== Bi alfabeyên din ====
* {{ku-ar|مامز}}
=== Çavkanî ===
* Mela Xelîl Mişextî: ''Ferhenga Merg û Jî'', Aras, Hewlêr 2006
==== Jê ====
{{kol3|ku|cure=Jê
|mamizgir
|mamizgirî
|mamizkuj
|mamizkujî
|mamizvan
|mamizvanî
|şivir
|pezkûvî
|gakûvî
|bizkûvî
|meşin
|meşkûvî
}}
==== Etîmolojî ====
{{bêetîmolojî|ku}}
==== Werger ====
{{werger-ser}}
* {{Z|ar}}: {{W|ar|الغزال|guman=erê}}, {{W|ar|الیعفور|guman=erê}}, {{W|ar|خولة|guman=erê}}
* {{Z|fa}}: {{W+|fa|غزال}}
* {{Z|en}}: {{W+|en|roe}}, {{W|en|deer}}
* {{Z|tr}}: {{W+|tr|ahu}}, {{W|tr|karaca|guman=erê}}
{{werger-bin}}
== {{ziman|ckb}} ==
=== Navdêr ===
{{navdêr|ckb|sc=Latn}}
# [[xezal]], [[ask]]
5ejj3lp2z0bfpuxctuy6j2in8gz7qtx
6531975
6531974
2026-09-17T16:11:43Z
Wikihez
7344
/* Kurmancî */ (bi [[MediaWiki:Gadget-AjaxEdit.js|AjaxEdit]]ê re)
6531975
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Navdêr ===
[[Wêne:Marsh deer.jpg|thumb|mamiz]]
{{navdêr|ku|z=m}}
# [[ask]], [[xezal]], [[ahû]]
==== Bi alfabeyên din ====
* {{ku-ar|مامز}}
==== Jê ====
{{kol3|ku|cure=Jê
|mamizgir
|mamizgirî
|mamizkuj
|mamizkujî
|mamizvan
|mamizvanî
|şivir
|pezkûvî
|gakûvî
|bizkûvî
|meşin
|meşkûvî
}}
==== Etîmolojî ====
{{bêetîmolojî|ku}}
==== Werger ====
{{werger-ser}}
* {{Z|ar}}: {{W|ar|الغزال|guman=erê}}, {{W|ar|الیعفور|guman=erê}}, {{W|ar|خولة|guman=erê}}
* {{Z|fa}}: {{W+|fa|غزال}}
* {{Z|en}}: {{W+|en|roe}}, {{W|en|deer}}
* {{Z|tr}}: {{W+|tr|ahu}}, {{W|tr|karaca|guman=erê}}
{{werger-bin}}
=== Çavkanî ===
* Mela Xelîl Mişextî: ''Ferhenga Merg û Jî'', Aras, Hewlêr 2006
== {{ziman|ckb}} ==
=== Navdêr ===
{{navdêr|ckb|sc=Latn}}
# [[xezal]], [[ask]]
bs8gjnvsdkxue5xx4n8iw5u8l7wxox4
îz
0
53118
6531948
6474778
2026-09-17T14:26:27Z
Wikihez
7344
Kurmancî/Navdêr: mane, hevmane hatin guhartin, mane hate guhartin, beşa «Jê» hate nûkirin, beşa «Etîmolojî» hate nûkirin (DefSideBoxes)
6531948
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Navdêr ===
[[Wêne:Traces.JPG|thumb|250px|şopên kesekî]]
{{navdêr|ku|z=m}}
# [[nîşan|Nîşan]]ên ku ji [[meş]]a kesekî yan [[heywan]]ekî mane.
#: {{hevmane|ku|şop|rêç|şûnpî|pêger|şûnpê|şoperêç|dews}}
# {{f|ku|mecazî}} [[berhem|Berhem]]ên yan [[nîşan]]ên ku kesek pê tê [[bi bîr anîn|bibîranîn]].
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Jê ====
{{kol3|ku|cure=Jê
|îzî
}}
==== Etîmolojî ====
{{etîmon|ku|:deyn|tr:iz|metn=++|dar=1}}
==== Werger ====
{{werger-ser}}
* {{Z|fi}}: {{W+|fi|jälki}}
* {{Z|fr}}: {{W+|fr|trace}}
* {{Z|en}}: {{W+|en|trace}}, {{W+|en|track}}
* {{Z|tr}}: {{W+|tr|iz}}
{{werger-bin}}
9vjkz5o71k9duvfsfpwkt06dxme4dz3
6531977
6531948
2026-09-17T16:12:10Z
Balyozbot
7867
Bot: Paqijiyên kozmetîk (Lînk paqij kir.)
6531977
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Navdêr ===
[[Wêne:Traces.JPG|thumb|250px|şopên kesekî]]
{{navdêr|ku|z=m}}
# [[nîşan|Nîşanên]] ku ji [[meş]]a kesekî yan [[heywan]]ekî mane.
#: {{hevmane|ku|şop|rêç|şûnpî|pêger|şûnpê|şoperêç|dews}}
# {{f|ku|mecazî}} [[berhem|Berhemên]] yan [[nîşan]]ên ku kesek pê tê [[bi bîr anîn|bibîranîn]].
==== Tewandin ====
{{ku-tewîn-nav|mê}}
==== Jê ====
{{kol3|ku|cure=Jê
|îzî
}}
==== Etîmolojî ====
{{etîmon|ku|:deyn|tr:iz|metn=++|dar=1}}
==== Werger ====
{{werger-ser}}
* {{Z|fi}}: {{W+|fi|jälki}}
* {{Z|fr}}: {{W+|fr|trace}}
* {{Z|en}}: {{W+|en|trace}}, {{W+|en|track}}
* {{Z|tr}}: {{W+|tr|iz}}
{{werger-bin}}
3t3k4q4b59opuw1ig75tyrxtgxw4ncp
electron
0
55588
6532004
6477309
2026-09-17T21:01:11Z
Wikihez
7344
/* Îngilîzî */ [[Bikarhêner:Wikihez/templateScript2.js|TS]]: Ji [[en:electron|en.wikt]]
6532004
wikitext
text/x-wiki
== {{ziman|en}} ==
=== Bilêvkirin ===
:{{ku-tîp|elektron}}
* {{IPA|en|/ɪˈlɛk.tɹɒn/|dever=QY}}
* {{IPA|en|/ɪˈlɛk.tɹɑn/|dever=DYA}}
* {{deng|en|en-us-electron.ogg|dever=DYA}}
=== Navdêr ===
{{wp|ziman=en}}
{{en-navdêr}}
# [[elektron]]
#: {{hevmane|en|e<id:electron><qq:sembol>|;|negatron<qq:kevnbûyî>}}
#: {{jornav|en|subatomic particle|<|particle}}
#: {{koordîne|en|proton|p<id:proton>|neutron|n<id:neutron>|;|nucleon|baryon|;|positron|;|holon|chargon|orbiton|spinon}}<!--types of subatomic particle-->
==== Formên alternatîv ====
* {{g|en|electrion}} {{mj|kevnbûyî}}
==== Jê ====
{{col|en|cryoelectron|electronize|electronogram|immunoelectron|thermoelectron
|electron avalanche|electron paramagnetic resonance
|antielectron
|coelectron|Auger electron|Auger electron spectroscopy|electron beam
|cryo-electron microscopy
|cryo-transmission electron microscopy
|dielectron|electron gap
|electride
|electrino|transferred electron device
|electron affinity
|electron capture
|electron capture detector
|electron carrier
|electron cloud|electron multiplier
|electron configuration
|electron cryomicroscopy
|electron density
|electron donor
|electron gun
|electron hole
|electron micrograph
|electron microscope
|electron microscopy
|electron neutrino|electron transfer
|electron number
|electron pair
|electron shell
|electron spin resonance
|electron transfer reaction
|electron tube
|electron volt|electron crystal
|electron wave function
|electronation
|electronlike| Bloch electron
|free electron
|immunogold electron microscopy
|interelectron
|monochromated scanning transmission electron microscope
|multielectron
|photoelectron
|quasielectron
|scanning electron microscope
|scanning transmission electron microscope
|selectron
|surface-conduction electron-emitter display
|three-center two-electron bond
|transmission electron diffraction
|transmission electron microscope
|transmission electron microscopy
|unpaired electron
|valence electron}}
==== Têkildar ====
* {{g|en|electronic}}
* {{g|en|electronics}}
==== Etîmolojî ====
{{etymon|en|id=partîkel|:têkilkirin|en:electric<id:elektrîk>|en:ion<id:partîkel>}}
{{blend|en|electric|ion}}, {{coinage|en|nobycat=1|nocap=1|w=George Johnstone Stoney|George Stoney|occ=alimê|in=1891}}.
6tuhnhzyb424za9n9uu68hjxy5qnxv9
6532005
6532004
2026-09-17T21:01:42Z
Wikihez
7344
/* Etîmolojî */ (bi [[MediaWiki:Gadget-AjaxEdit.js|AjaxEdit]]ê re)
6532005
wikitext
text/x-wiki
== {{ziman|en}} ==
=== Bilêvkirin ===
:{{ku-tîp|elektron}}
* {{IPA|en|/ɪˈlɛk.tɹɒn/|dever=QY}}
* {{IPA|en|/ɪˈlɛk.tɹɑn/|dever=DYA}}
* {{deng|en|en-us-electron.ogg|dever=DYA}}
=== Navdêr ===
{{wp|ziman=en}}
{{en-navdêr}}
# [[elektron]]
#: {{hevmane|en|e<id:electron><qq:sembol>|;|negatron<qq:kevnbûyî>}}
#: {{jornav|en|subatomic particle|<|particle}}
#: {{koordîne|en|proton|p<id:proton>|neutron|n<id:neutron>|;|nucleon|baryon|;|positron|;|holon|chargon|orbiton|spinon}}<!--types of subatomic particle-->
==== Formên alternatîv ====
* {{g|en|electrion}} {{mj|kevnbûyî}}
==== Jê ====
{{col|en|cryoelectron|electronize|electronogram|immunoelectron|thermoelectron
|electron avalanche|electron paramagnetic resonance
|antielectron
|coelectron|Auger electron|Auger electron spectroscopy|electron beam
|cryo-electron microscopy
|cryo-transmission electron microscopy
|dielectron|electron gap
|electride
|electrino|transferred electron device
|electron affinity
|electron capture
|electron capture detector
|electron carrier
|electron cloud|electron multiplier
|electron configuration
|electron cryomicroscopy
|electron density
|electron donor
|electron gun
|electron hole
|electron micrograph
|electron microscope
|electron microscopy
|electron neutrino|electron transfer
|electron number
|electron pair
|electron shell
|electron spin resonance
|electron transfer reaction
|electron tube
|electron volt|electron crystal
|electron wave function
|electronation
|electronlike| Bloch electron
|free electron
|immunogold electron microscopy
|interelectron
|monochromated scanning transmission electron microscope
|multielectron
|photoelectron
|quasielectron
|scanning electron microscope
|scanning transmission electron microscope
|selectron
|surface-conduction electron-emitter display
|three-center two-electron bond
|transmission electron diffraction
|transmission electron microscope
|transmission electron microscopy
|unpaired electron
|valence electron}}
==== Têkildar ====
* {{g|en|electronic}}
* {{g|en|electronics}}
==== Etîmolojî ====
{{etymon|en|id=partîkel|:têkilkirin|en:electric<id:elektrîk>|en:ion<id:partîkel>}}
{{blend|en|electric|ion}}, {{coinage|en|nobycat=1|nocap=1|w=George Johnstone Stoney|George Stoney|occ=alimê brîtanî|in=1891}}.
rw89zvi45us26xsgqjpj2onqgus9cis
hidrobiyoloji
0
141366
6531917
5798093
2026-09-17T12:40:17Z
Wikihez
7344
Tirkî/Mane: mane hate guhartin, mane hate guhartin; Tirkî: beşa «Mane» bû «Navdêr» (DefSideBoxes)
6531917
wikitext
text/x-wiki
== {{ziman|tr}} ==
=== Navdêr ===
{{navdêr|tr}}
# [[hîdrobiyolojî|hîdrobiyolojî]] {{îzah|[[zanist|zanista]] ku [[li ser]] [[candar|candarên]] di [[av|avê]] de [[dijîn|dijîn]] [[xebat|xebatê]] [[kirin|dike]]}}
# [[hîdrobiyolojî|hîdrobiyolojî]] {{îzah|[[tişt|tiştê]] bi vê [[zanist|zanistê]] re [[têkildar|têkildar]] e}}
awfggwmi8ykaxrhqiyiievudfh1a0m9
6531941
6531917
2026-09-17T14:12:08Z
Balyozbot
7867
Bot: Paqijiyên kozmetîk (Lînk paqij kir.)
6531941
wikitext
text/x-wiki
== {{ziman|tr}} ==
=== Navdêr ===
{{navdêr|tr}}
# [[hîdrobiyolojî]] {{îzah|[[zanist]]a ku [[li ser]] [[candar]]ên di [[av]]ê de [[dijîn]] [[xebat]]ê [[kirin|dike]]}}
# [[hîdrobiyolojî]] {{îzah|[[tişt]]ê bi vê [[zanist]]ê re [[têkildar]] e}}
0fbfeeayh5gl24x9ij3lwhsqoy047x9
sivri kuyruk
0
163499
6531947
5800954
2026-09-17T14:20:25Z
Wikihez
7344
Tirkî/Mane: mane hate guhartin; Tirkî: beşa «Mane» bû «Navdêr» (DefSideBoxes)
6531947
wikitext
text/x-wiki
== {{ziman|tr}} ==
=== Navdêr ===
{{navdêr|tr}}
# [[malik]] {{îzah|[[cure|cureyekî]] [[kurm|kurmek]] e}}
d5gaeesx2ecchmnu9svr2b8uh6baq6i
6531978
6531947
2026-09-17T16:12:14Z
Balyozbot
7867
Bot: Paqijiyên kozmetîk (Lînk paqij kir.)
6531978
wikitext
text/x-wiki
== {{ziman|tr}} ==
=== Navdêr ===
{{navdêr|tr}}
# [[malik]] {{îzah|[[cure]]yekî [[kurm]]ek e}}
cw2pdqtlaxnbumcho0857ezkj8pdjc6
şart dilek kipi
0
177381
6531914
4645197
2026-09-17T12:35:16Z
Wikihez
7344
Tirkî: beşa «Biwêj» bû «Navdêr» (DefSideBoxes)
6531914
wikitext
text/x-wiki
== {{ziman|tr}} ==
=== Navdêr ===
{{navdêr|tr}}
# raweya şert daxwaz
ccyohiduc8nqokthmk98fbjjvcbu4hg
girêba
0
179559
6532002
6387281
2026-09-17T20:27:36Z
Wikihez
7344
Kurmancî/Navdêr: mane hate guhartin, mane hate guhartin (DefSideBoxes)
6532002
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Navdêr ===
{{navdêr|ku}}
# {{f|ku|nexweşî}} [[nexweşî|Nexweşiyeke]] [[bakî|bakî]] û [[jandar|jandar]] a ku [[ketin|dikeve]] [[masûlke|masûlke]] û [[geh|gehên]] [[mirovî|mirovî]].
# [[kînor|Kînora]] ku di [[rehçikên|rehçikên]] [[xwîn|xwînê]] de [[derdikevin|derdikevin]].
==== Jê ====
{{kol3|ku
|girêba bûn<pos:lêker>
|girêba kirin<pos:lêker>
|girêbabûn<pos:navdêr>
|girêbakirin<pos:navdêr>
|girêbabûyî<pos:rengdêr>
|girêbakirî<pos:rengdêr>
}}
==== Etîmolojî ====
{{bêetîmolojî|ku}}
==== Werger ====
{{werger-ser}}
* {{Z|tr}}: {{W+|tr|kanser}}, {{W+|tr|ur}}, {{W+|tr|tümör}}
{{werger-bin}}
==== Anagram ====
* {{anagram|ku|a=abêgir|bêagir}}
iadnbw76duti3zbgx59evr0tdygwr1z
6532010
6532002
2026-09-17T22:12:08Z
Balyozbot
7867
Bot: Paqijiyên kozmetîk (Lînk paqij kir.)
6532010
wikitext
text/x-wiki
== {{ziman|ku}} ==
=== Bilêvkirin ===
* {{ku-kîte}}
=== Navdêr ===
{{navdêr|ku}}
# {{f|ku|nexweşî}} [[nexweşî|Nexweşiyeke]] [[bakî]] û [[jandar]] a ku [[ketin|dikeve]] [[masûlke]] û [[geh]]ên [[mirovî]].
# [[kînor|Kînora]] ku di [[rehçikên]] [[xwîn]]ê de [[derdikevin]].
==== Jê ====
{{kol3|ku
|girêba bûn<pos:lêker>
|girêba kirin<pos:lêker>
|girêbabûn<pos:navdêr>
|girêbakirin<pos:navdêr>
|girêbabûyî<pos:rengdêr>
|girêbakirî<pos:rengdêr>
}}
==== Etîmolojî ====
{{bêetîmolojî|ku}}
==== Werger ====
{{werger-ser}}
* {{Z|tr}}: {{W+|tr|kanser}}, {{W+|tr|ur}}, {{W+|tr|tümör}}
{{werger-bin}}
==== Anagram ====
* {{anagram|ku|a=abêgir|bêagir}}
t4nc0c24molqaba1d3eb08u6uorvzyi
خانه
0
256920
6531921
6382006
2026-09-17T12:57:57Z
Wikihez
7344
Farisî/Navdêr: beşa «Etîmolojî» hate zêdekirin (DefSideBoxes)
6531921
wikitext
text/x-wiki
{{h-bnr|خانة|حانة|خانہ|ـخانة|ـخانه|جانه|چانه}}
== {{ziman|fa}} ==
=== Bilêvkirin ===
* {{deng|fa|Fa-خانه.ogg}}
* {{deng|fa|LL-Q9168 (fas)-Afsham23-خانه.wav|Deng|dever=London,QY}}
* {{deng|fa|LL-Q9168 (fas)-Darafsh-خانه.wav|Deng|dever=Îran}}
=== Navdêr ===
{{navdêr|fa|tr=xâne}}
# [[xanî]], [[mal]]
==== Têkildar ====
* {{g|fa|آپارتمان|tr=âpârtemân}}
==== Pêkhateyên malê ====
* {{g|fa|آشپزخانه|tr=âšpaz-xâne}}
* {{g|fa|اتاق نشیمن|tr=otâq nešiman}} an jî {{g|fa|هال|tr=}}
* {{g|fa|حمام|tr=hammâm}}
* {{g|fa|اتاق خواب|tr=otâq xâb}}
* {{g|fa|دستشویی|tr=dastšuyi}}
==== Etîmolojî ====
{{etîmon|fa|id=xanî|:mîr|pal:𐭡𐭩𐭲𐭠<ts:xānag>|dar=1|metn=++}}
fo4rvqwk87ywptzxslylwf0v5qov675
6531922
6531921
2026-09-17T13:01:38Z
Wikihez
7344
[[Bikarhêner:Wikihez/templateScript2.js|TS]]: Ji [[en:خانه|en.wikt]]
6531922
wikitext
text/x-wiki
{{h-bnr|خانة|حانة|خانہ|ـخانة|ـخانه|جانه|چانه}}
== {{ziman|fa}} ==
=== Bilêvkirin ===
* {{deng|fa|Fa-خانه.ogg}}
* {{deng|fa|LL-Q9168 (fas)-Afsham23-خانه.wav|Deng|dever=London,QY}}
* {{deng|fa|LL-Q9168 (fas)-Darafsh-خانه.wav|Deng|dever=Îran}}
=== Navdêr ===
{{navdêr|fa|tr=xâne}}
# [[xanî]], [[mal]]
==== Têkildar ====
* {{g|fa|آپارتمان|tr=âpârtemân}}
==== Pêkhateyên malê ====
* {{g|fa|آشپزخانه|tr=âšpaz-xâne}}
* {{g|fa|اتاق نشیمن|tr=otâq nešiman}} an jî {{g|fa|هال|tr=}}
* {{g|fa|حمام|tr=hammâm}}
* {{g|fa|اتاق خواب|tr=otâq xâb}}
* {{g|fa|دستشویی|tr=dastšuyi}}
==== Etîmolojî ====
{{etîmon|fa|id=xanî|:mîr|pal:𐭡𐭩𐭲𐭠<ts:xānag>|dar=1|metn=++}}
==== Dûnde ====
{{stûn|
* {{dû|prs|خَانَه}}
* {{dû|fa-ira|خَانِه|خونِه<e:Tehran,kolankî>}}
* {{dû|tg|хона}}
* {{dû|jdt|xunə}}
* {{dû|deyn=1|hy|-խանա}}
* {{dû|deyn=1|az|-xana}}
* {{dû|deyn=1|ba|-хана|sc=Cyrl}}
* {{dû|deyn=1|bn|-খানা}}
* {{dû|inc-hnd|-|deyn=1}}
*: {{dû|hi|ख़ाना}}
*: {{dû|ur|خَانَہ}}
* {{dû|deyn=1|ka|ხანა|-ხანა}}
* {{dû|deyn=1|kk|-хана}}
* {{dû|deyn=1|ky|-кана}}
* {{dû|deyn=1|lrc|هۆنںە|tr=honə}}
* {{dardû|ota|خانه|tr=hane|deyn=1}}
* {{dû|ota|ـخانه|tr=-hane|deyn=1}}
** {{dû|tr|-hane}}
** {{dû|sh|-àna//-а̀на|deyn=1}}
* {{dû|deyn=1|pa|-|tr=khanna}} {{qal|pa|خانہ|tr=xāna, khāna}} / {{qal|pa|ਖਾਨਾ}}
* {{dû|deyn=1|sd|خانو|tr=xāno}}
* {{dû|deyn=1|sgh|хона̄|ху̊на̄}}
* {{dû|deyn=1|uz|-xona}}
* {{dû|deyn=1|ug|-خانا}}
}}
c9hmlf5sur35fvv930oki5j0m2p3tge
6531939
6531922
2026-09-17T13:41:12Z
Balyozbot
7867
[[Special:PermanentLink/6298023|Bot]]: rêzkirina beşan
6531939
wikitext
text/x-wiki
{{h-bnr|خانة|حانة|خانہ|ـخانة|ـخانه|جانه|چانه}}
== {{ziman|fa}} ==
=== Bilêvkirin ===
* {{deng|fa|Fa-خانه.ogg}}
* {{deng|fa|LL-Q9168 (fas)-Afsham23-خانه.wav|Deng|dever=London,QY}}
* {{deng|fa|LL-Q9168 (fas)-Darafsh-خانه.wav|Deng|dever=Îran}}
=== Navdêr ===
{{navdêr|fa|tr=xâne}}
# [[xanî]], [[mal]]
==== Têkildar ====
* {{g|fa|آپارتمان|tr=âpârtemân}}
==== Etîmolojî ====
{{etîmon|fa|id=xanî|:mîr|pal:𐭡𐭩𐭲𐭠<ts:xānag>|dar=1|metn=++}}
==== Dûnde ====
{{stûn|
* {{dû|prs|خَانَه}}
* {{dû|fa-ira|خَانِه|خونِه<e:Tehran,kolankî>}}
* {{dû|tg|хона}}
* {{dû|jdt|xunə}}
* {{dû|deyn=1|hy|-խանա}}
* {{dû|deyn=1|az|-xana}}
* {{dû|deyn=1|ba|-хана|sc=Cyrl}}
* {{dû|deyn=1|bn|-খানা}}
* {{dû|inc-hnd|-|deyn=1}}
*: {{dû|hi|ख़ाना}}
*: {{dû|ur|خَانَہ}}
* {{dû|deyn=1|ka|ხანა|-ხანა}}
* {{dû|deyn=1|kk|-хана}}
* {{dû|deyn=1|ky|-кана}}
* {{dû|deyn=1|lrc|هۆنںە|tr=honə}}
* {{dardû|ota|خانه|tr=hane|deyn=1}}
* {{dû|ota|ـخانه|tr=-hane|deyn=1}}
** {{dû|tr|-hane}}
** {{dû|sh|-àna//-а̀на|deyn=1}}
* {{dû|deyn=1|pa|-|tr=khanna}} {{qal|pa|خانہ|tr=xāna, khāna}} / {{qal|pa|ਖਾਨਾ}}
* {{dû|deyn=1|sd|خانو|tr=xāno}}
* {{dû|deyn=1|sgh|хона̄|ху̊на̄}}
* {{dû|deyn=1|uz|-xona}}
* {{dû|deyn=1|ug|-خانا}}
}}
==== Pêkhateyên malê ====
* {{g|fa|آشپزخانه|tr=âšpaz-xâne}}
* {{g|fa|اتاق نشیمن|tr=otâq nešiman}} an jî {{g|fa|هال|tr=}}
* {{g|fa|حمام|tr=hammâm}}
* {{g|fa|اتاق خواب|tr=otâq xâb}}
* {{g|fa|دستشویی|tr=dastšuyi}}
me37vimnj7knr61wi9vv05l73p9waxg
خطر
0
295968
6531971
6264180
2026-09-17T16:01:53Z
Wikihez
7344
Erebî/Navdêr: beşa «Dûnde» hate nûkirin (DefSideBoxes)
6531971
wikitext
text/x-wiki
== {{ziman|arz}} ==
=== Navdêr ===
{{navdêr|arz|tr=ḵaṭar|z=n}}
# {{g|ku|xeter}}, {{g|ku|talûke}}, {{g|ku|metirsî}}
=== Çavkanî ===
{{rûpela wergerê|arz|talûke}}
== {{ziman|ar}} ==
=== Bilêvkirin ===
: {{ku-tîp|xeter}}
* {{deng|ar|Ar-خطر.ogg|Deng}}
=== Navdêr ===
{{navdêr|ar|خَطَر|z=n}}
# [[xeter]], [[talûke]], [[metirsdarî]]
==== Etîmolojî ====
Ji rehê {{ar-reh|خ ط ر}}.
==== Formên alternatîv ====
* {{g|ar|خطير}}
==== Dûnde ====
* {{dû|fa|خطر|tr=ḵaṭar|deyn=1}}
* {{dû|ku|xeter|deyn=1}}
== {{ziman|fa}} ==
=== Bilêvkirin ===
: {{ku-tîp|xeter}}
=== Navdêr ===
{{navdêr|fa|tr=ḵaṭar}}
# [[xeter]]
==== Etîmolojî ====
Ji {{deyn|ar|fa|خَطَر}}.
==== Jê ====
* {{g|fa|خطرناک|tr=xatarnâk}}
== {{ziman|ps}} ==
=== Navdêr ===
{{navdêr|ps|tr=xatár|z=n}}
# {{g|ku|xeter}}, {{g|ku|talûke}}, {{g|ku|metirsî}}
=== Çavkanî ===
{{rûpela wergerê|ps|talûke}}
skuay0i5kcvfr1fmxhbrqpnnqda8qcl
6531981
6531971
2026-09-17T16:29:07Z
Balyozbot
7867
[[Bikarhêner:Wikihez/skrîpt/py/dengbot.py|Bot]]: Formata parametreya "dever" hat guhartin
6531981
wikitext
text/x-wiki
== {{ziman|arz}} ==
=== Navdêr ===
{{navdêr|arz|tr=ḵaṭar|z=n}}
# {{g|ku|xeter}}, {{g|ku|talûke}}, {{g|ku|metirsî}}
=== Çavkanî ===
{{rûpela wergerê|arz|talûke}}
== {{ziman|ar}} ==
=== Bilêvkirin ===
: {{ku-tîp|xeter}}
* {{deng|ar|Ar-خطر.ogg}}
=== Navdêr ===
{{navdêr|ar|خَطَر|z=n}}
# [[xeter]], [[talûke]], [[metirsdarî]]
==== Etîmolojî ====
Ji rehê {{ar-reh|خ ط ر}}.
==== Formên alternatîv ====
* {{g|ar|خطير}}
==== Dûnde ====
* {{dû|fa|خطر|tr=ḵaṭar|deyn=1}}
* {{dû|ku|xeter|deyn=1}}
== {{ziman|fa}} ==
=== Bilêvkirin ===
: {{ku-tîp|xeter}}
=== Navdêr ===
{{navdêr|fa|tr=ḵaṭar}}
# [[xeter]]
==== Etîmolojî ====
Ji {{deyn|ar|fa|خَطَر}}.
==== Jê ====
* {{g|fa|خطرناک|tr=xatarnâk}}
== {{ziman|ps}} ==
=== Navdêr ===
{{navdêr|ps|tr=xatár|z=n}}
# {{g|ku|xeter}}, {{g|ku|talûke}}, {{g|ku|metirsî}}
=== Çavkanî ===
{{rûpela wergerê|ps|talûke}}
8kd8bec2aa9a3yorc1usb9oswepjbyn
6531982
6531981
2026-09-17T16:41:06Z
Balyozbot
7867
[[Special:PermanentLink/6298023|Bot]]: rêzkirina beşan
6531982
wikitext
text/x-wiki
== {{ziman|arz}} ==
=== Navdêr ===
{{navdêr|arz|tr=ḵaṭar|z=n}}
# {{g|ku|xeter}}, {{g|ku|talûke}}, {{g|ku|metirsî}}
=== Çavkanî ===
{{rûpela wergerê|arz|talûke}}
== {{ziman|ar}} ==
=== Bilêvkirin ===
: {{ku-tîp|xeter}}
* {{deng|ar|Ar-خطر.ogg}}
=== Navdêr ===
{{navdêr|ar|خَطَر|z=n}}
# [[xeter]], [[talûke]], [[metirsdarî]]
==== Formên alternatîv ====
* {{g|ar|خطير}}
==== Etîmolojî ====
Ji rehê {{ar-reh|خ ط ر}}.
==== Dûnde ====
* {{dû|fa|خطر|tr=ḵaṭar|deyn=1}}
* {{dû|ku|xeter|deyn=1}}
== {{ziman|fa}} ==
=== Bilêvkirin ===
: {{ku-tîp|xeter}}
=== Navdêr ===
{{navdêr|fa|tr=ḵaṭar}}
# [[xeter]]
==== Jê ====
* {{g|fa|خطرناک|tr=xatarnâk}}
==== Etîmolojî ====
Ji {{deyn|ar|fa|خَطَر}}.
== {{ziman|ps}} ==
=== Navdêr ===
{{navdêr|ps|tr=xatár|z=n}}
# {{g|ku|xeter}}, {{g|ku|talûke}}, {{g|ku|metirsî}}
=== Çavkanî ===
{{rûpela wergerê|ps|talûke}}
gzzpgr9mifooku223w0xo919nrm1tyd
rhododendron
0
315957
6531998
6051291
2026-09-17T19:58:18Z
Wikihez
7344
Îngilîzî/Navdêr: mane hate guhartin (DefSideBoxes)
6531998
wikitext
text/x-wiki
== {{ziman|en}} ==
=== Bilêvkirin ===
* {{deng|en|LL-Q1860 (eng)-Vealhurl-rhododendron.wav|Deng|dever=Başûrê Îngilistanê, QY}}
=== Navdêr ===
{{navdêr|en}}
# [[rododendron]] {{îzah|[[riwekek|riwekeka]] [[wekê|wekê]] [[açalya|açalyayê]]}}
775duvpa0vcdddtcd7laphsi5qammxp
6532006
6531998
2026-09-17T21:12:09Z
Balyozbot
7867
Bot: Paqijiyên kozmetîk (Lînk paqij kir.)
6532006
wikitext
text/x-wiki
== {{ziman|en}} ==
=== Bilêvkirin ===
* {{deng|en|LL-Q1860 (eng)-Vealhurl-rhododendron.wav|Deng|dever=Başûrê Îngilistanê, QY}}
=== Navdêr ===
{{navdêr|en}}
# [[rododendron]] {{îzah|[[riwekek]]a [[wekê]] [[açalya]]yê}}
f54gldjqkdz8ufjb7hnxgf4j8v0af0x
loslegen
0
454831
6531915
4647055
2026-09-17T12:36:11Z
Wikihez
7344
Almanî/Lêker: mane hate guhartin (DefSideBoxes)
6531915
wikitext
text/x-wiki
== {{ziman|de}} ==
=== Lêker ===
{{lêker|de}}
# [[dest|dest]] [[bi kar]] [[kirin|kirin]], [[li ser]] îş [[bûn|bûn]], bi [[îş|îşekî]] de [[ketin|ketin]]
7vpf054kadrii2p4rpo6ual6lvagewm
6531942
6531915
2026-09-17T14:12:11Z
Balyozbot
7867
Bot: Paqijiyên kozmetîk (Lînk paqij kir.)
6531942
wikitext
text/x-wiki
== {{ziman|de}} ==
=== Lêker ===
{{lêker|de}}
# [[dest]] [[bi kar]] [[kirin]], [[li ser]] îş [[bûn]], bi [[îş]]ekî de [[ketin]]
b8iayyru9p9rnkavmf7u6l9p8gj1xqp
gorila
0
638413
6531919
6512705
2026-09-17T12:53:09Z
Wikihez
7344
/* Îngilîzî */ [[Bikarhêner:Wikihez/templateScript2.js|TS]]: Ji [[en:gorila|en.wikt]]
6531919
wikitext
text/x-wiki
== {{ziman|pt}} ==
=== Bilêvkirin ===
* {{deng|pt|LL-Q5146 (por)-MedK1-gorila.wav|a=<<Brazîl>> (<<Caipira>>)}}
* {{kîte|pt|go|ri|la}}
=== Navdêr ===
{{wp|ziman=pt}}
[[Wêne:N'Tami, Western Gorilla.jpg|thumb|250px|Gorila]]
{{navdêr|pt|c=n}}
# [[gorîl]]
==== Jê ====
{{col|pt|gorila-das-montanhas|gorilão|gorilismo|gorila-de-grauer|gorila-do-ocidente|gorila-do-oriente|gorila-do-rio-cross}}
==== Etîmolojî ====
{{etîmon|pt|:deyn|la-new:gorilla|dar=1|metn=+}}
==== Dûnde ====
* {{dû|tet|gorila|deyn=1}}
l0au078wr4te6ub8d60pccsabiszl5q
قهوة
0
935811
6531949
5235021
2026-09-17T14:32:52Z
Wikihez
7344
Erebî/Navdêr: beşa «Etîmolojî» hate zêdekirin (DefSideBoxes)
6531949
wikitext
text/x-wiki
== {{ziman|ary}} ==
=== Navdêr ===
{{navdêr|ary|tr=qahwa|z=m}}
# {{f|ary|vexwarin}} [[qehwe#ku|qehwe]]
== {{ziman|afb}} ==
=== Navdêr ===
{{navdêr|afb|tr=gəhwə}}
# {{f|afb|vexwarin}} [[qehwe#ku|qehwe]]
== {{ziman|arz}} ==
=== Navdêr ===
{{navdêr|arz|قَهْوَة|tr=ʾahwa|z=m}}
# {{f|arz|vexwarin}} [[qehwe#ku|qehwe]]
== {{ziman|acw}} ==
=== Navdêr ===
{{navdêr|acw|قَهْوَة|tr=gahwa|z=m}}
# {{f|acw|vexwarin}} [[qehwe#ku|qehwe]]
== {{ziman|ar}} ==
=== Navdêr ===
{{navdêr|ar|قَهْوَة|z=m|p=قَهَوَات|p2=قَهَاوٍ}}
# {{f|ar|vexwarin}} [[qehwe]]
# {{f|ar|nefermî}} [[kafê]]
# {{f|ar|kevnbûyî}} [[şerab]], [[mey]]
==== Jê ====
* {{g|ar|مَقْهًى}}
==== Têkildar ====
* {{g|ar|أَقْهَى}}
==== Etîmolojî ====
{{etîmon|ar|id=qehwe|:reh|ق ه ي<t:yê ku eleqedarî têrbûnê>|metn=+}}
==== Bide ber ====
* {{g|ar|بُنّ}}
* {{g|ar|شَاي}}
* {{g|ar|نَبِيذ}}}
=== Çavkanî ===
{{werger çavkanî|en|قهوة|oldid=61677321}}
ho0yhx7ph14u2uegh7hq1iqqu58wvf8
6531950
6531949
2026-09-17T14:34:28Z
Wikihez
7344
/* Etîmolojî */
6531950
wikitext
text/x-wiki
== {{ziman|ary}} ==
=== Navdêr ===
{{navdêr|ary|tr=qahwa|z=m}}
# {{f|ary|vexwarin}} [[qehwe#ku|qehwe]]
== {{ziman|afb}} ==
=== Navdêr ===
{{navdêr|afb|tr=gəhwə}}
# {{f|afb|vexwarin}} [[qehwe#ku|qehwe]]
== {{ziman|arz}} ==
=== Navdêr ===
{{navdêr|arz|قَهْوَة|tr=ʾahwa|z=m}}
# {{f|arz|vexwarin}} [[qehwe#ku|qehwe]]
== {{ziman|acw}} ==
=== Navdêr ===
{{navdêr|acw|قَهْوَة|tr=gahwa|z=m}}
# {{f|acw|vexwarin}} [[qehwe#ku|qehwe]]
== {{ziman|ar}} ==
=== Navdêr ===
{{navdêr|ar|قَهْوَة|z=m|p=قَهَوَات|p2=قَهَاوٍ}}
# {{f|ar|vexwarin}} [[qehwe]]
# {{f|ar|nefermî}} [[kafê]]
# {{f|ar|kevnbûyî}} [[şerab]], [[mey]]
==== Jê ====
* {{g|ar|مَقْهًى}}
==== Têkildar ====
* {{g|ar|أَقْهَى}}
==== Etîmolojî ====
{{etîmon|ar|id=qehwe|:reh|ق ه ي<t:yê ku eleqedarî têrbûnê><nediyar>|metn=+}}
Ji rehê {{ar-reh|ق ه ي|nekat=1}}
==== Bide ber ====
* {{g|ar|بُنّ}}
* {{g|ar|شَاي}}
* {{g|ar|نَبِيذ}}}
=== Çavkanî ===
{{werger çavkanî|en|قهوة|oldid=61677321}}
7i4i2pbfilrzlc5xkedf7ptgif77g3g
Gotûbêja bikarhêner:SHB
3
971537
6532011
5038251
2026-09-18T02:37:02Z
Max
11795
Maxî/ê navê [[Gotûbêja bikarhêner:SHB2000]] weke [[Gotûbêja bikarhêner:SHB]] guhart: Automatically moved page while renaming the user "[[Special:CentralAuth/SHB2000|SHB2000]]" to "[[Special:CentralAuth/SHB|SHB]]"
5038251
wikitext
text/x-wiki
<div style="border: 1rem #dd0000 ridge; padding: 0.5em;">Hello! Please message me on [[m:User talk:SHB2000|meta.wikimedia.org]] instead. I may not notice messages on this page.</div>
ojy2vkqs676esjrcfi79dwyt0tjr5xc
ordnungsmäßig
0
973644
6531916
5765809
2026-09-17T12:36:36Z
Wikihez
7344
/* Almanî */ (bi [[MediaWiki:Gadget-AjaxEdit.js|AjaxEdit]]ê re)
6531916
wikitext
text/x-wiki
== {{ziman|de}} ==
=== Bilêvkirin===
* {{deng|de|De-ordnungsmäßig2.ogg}}
* {{deng|de|De-ordnungsmäßig.ogg}}
=== Rengdêr ===
{{rengdêr|de}}
#[[bilayiqî]], [[guncan]], têra xwe
hzmny0nj0t2eex9p7too4yabs98ahgv
Jinûvesazî:proto-hindûewropî/HéHḱus
108
1326182
6532013
6369928
2026-09-18T06:44:19Z
Wikihez
7344
/* Dûnde */ (bi [[MediaWiki:Gadget-AjaxEdit.js|AjaxEdit]]ê re)
6532013
wikitext
text/x-wiki
{{jinûvesazkirî}}
== {{ziman|ine-pro}} ==
=== Rengdêr ===
{{etîmon|ine-pro|id=bilez}}
{{rengdêr|ine-pro}}<ref name=Zair>{{R:cel:Zair:2012|page=25}}</ref><ref name="Schrijver H">{{R:la:Schrijver|page=77}}</ref>
# [[bilez]]
==== Jinûvesazkirinên alternatîv====
* {{herwiha|ine-pro|*h₁óh₁ḱus}}<ref>{{R:grc:Beekes|pages=1677-1678}}</ref>
* {{herwiha|ine-pro|*h₁ōḱus}}<ref>{{R:cel:EDPC|pages=97-98|cuda=*dī-ākV-}}</ref>
==== Dûnde ====
* {{dû|cel-pro|*ākus}}
** {{dû|drş=1|cel-pro|*dīākus}}
* {{dû|grk-pro|-}}
** {{dû|grc|ὠκύς}}
* {{dardû|iir-pro|*HaHćúš}}
* {{dû|itc-pro|*ōkus}}
** {{dû|la|ōcior|ōcissimus}}
=== Çavkanî ===
{{çavkanî}}
ocuwu0bxbjs4ft56oikbaubo34fj1y6
Bikarhêner:Wikihez/Gadget-test/loader.js
2
1358398
6531966
6427489
2026-09-17T15:59:19Z
Wikihez
7344
Test loader lists only gadgets with undeployed changes; italic example with a bracketed explanation reads as {{nimûne}}; no îzah button for Kurmancî
6531966
javascript
text/javascript
// <nowiki>
// Test harness for the gadgets. Loaded from [[MediaWiki:Gadget-test.js]], which is one line:
// mw.loader.load('//ku.wiktionary.org/w/index.php?title=Bikarhêner:Wikihez/Gadget-test/loader.js&action=raw&ctype=text/javascript');
//
// Runs the userspace copies in place of the site's gadgets, under the same module names. A gadget the
// wiki has registered but not yet loaded is implemented here from the test page instead, so its
// dependencies are the real ones from [[MediaWiki:Gadgets-definition]]. One already loading from the wiki
// cannot be replaced: turn the default gadgets that load it (start and wikt.dsb load wikt.core) off in
// Special:Preferences while testing, and this names any that are still on.
//
// MODULES lists only the gadgets with changes not yet live; everything else runs the site's copy. A
// gadget is added back when it is being changed, and taken out once that change is deployed.
(function () {
"use strict";
// Set synchronously, before add-examples' DOM-ready pass, so its own inline link is never built.
window.wikt = window.wikt || {};
wikt.addExamples = wikt.addExamples || {};
wikt.addExamples.suppressLinks = true;
// Subpage segments are case-sensitive, so BASE must match the pages exactly.
var BASE = "Bikarhêner:Wikihez/Gadget-test";
// A value carrying a namespace is used as the whole title; anything else is appended to BASE.
function pageTitle(suffix) {
return suffix.indexOf(":") !== -1 ? suffix : BASE + suffix;
}
// module name -> package ({main, files}) or plain script ({script}), plus styles.
// deferred held back until something asks for it, as production fetches it only then
// start asked for on page load, as production loads a default gadget
// deps only for a gadget not yet in Gadgets-definition; a registered one keeps the wiki's
// actions with start, only on these wgAction values, as a definition's "actions=" does
var MODULES = {
"ext.gadget.wikt.core": {
main: "wikt.core.js",
files: {
// Keyed exactly as Gadgets-definition names them, so a require that would fail in
// production fails here first.
"wikt.core.js": "/wikt-core.js",
"wikt.core/sections.js": "/sections.js",
"wikt.core/prefs.js": "/prefs.js",
"wikt.core/data-sections.json": "/data-sections.json"
},
styles: ["/tokens.css"]
},
"ext.gadget.wikt.edit": {
main: "wikt.edit.js",
files: {
"wikt.edit.js": "/wikt-edit.js",
"wikt.edit/util.js": "/util.js",
"wikt.edit/wikitext.js": "/wikitext.js",
"wikt.edit/langs.js": "/langs.js",
"wikt.edit/labels.js": "/labels.js",
"wikt.edit/posparams.js": "/posparams.js",
"wikt.edit/data-main.json": "/data-main.json",
"wikt.edit/ziman-core.json": "/ziman-core.json"
},
styles: ["/wikt-edit.css"]
},
"ext.gadget.wikt.cite": {
main: "wikt.cite.js",
files: { "wikt.cite.js": "/cite.js" },
styles: ["/cite.css"]
},
// Unchanged, but it is what loads the modules above and below, and it is off in Preferences while
// they are tested.
"ext.gadget.wikt.dsb": {
start: true,
main: "wikt.dsb.js",
files: { "wikt.dsb.js": "/dsb.js" },
styles: ["/dsb.css"]
},
"ext.gadget.wikt.dsb-panels": {
deferred: true,
main: "wikt.dsb-panels.js",
files: { "wikt.dsb-panels.js": "/dsb-panels.js" },
styles: ["/dsb-panels.css"]
}
};
// Force dataType "text": otherwise jQuery sees action=raw's JavaScript Content-Type and runs it.
function raw(suffix) {
return $.get({
url: mw.util.getUrl(pageTitle(suffix), { action: "raw" }),
dataType: "text"
});
}
function run() {
var names = Object.keys(MODULES);
// A module the wiki has started loading cannot be implemented a second time.
var busy = names.filter(function (name) {
var state = mw.loader.getState(name);
return state !== null && state !== "registered";
});
if (busy.length) {
console.warn("loader.js: already loaded from the wiki, so the site's copy runs: " + busy.join(", "));
mw.notify("loader.js: van amûran di Tercîhan de bigire, da ku kopiya ceribandinê bixebite: " +
busy.map(function (n) { return n.replace("ext.gadget.", ""); }).join(", "), { type: "warn" });
}
var ours = names.filter(function (name) {
return busy.indexOf(name) === -1;
});
// Every distinct page this harness needs, fetched once each.
var suffixes = [];
function need(suffix) {
if (suffixes.indexOf(suffix) === -1) {
suffixes.push(suffix);
}
}
ours.forEach(function (name) {
var m = MODULES[name];
if (m.script) {
need(m.script);
} else {
Object.keys(m.files).forEach(function (key) { need(m.files[key]); });
}
m.styles.forEach(need);
});
if (!suffixes.length) {
return;
}
$.when.apply($, suffixes.map(raw)).done(function () {
var args = suffixes.length === 1 ? [arguments] : [].slice.call(arguments);
var text = {};
suffixes.forEach(function (s, i) {
text[s] = args[i][0];
});
// Styles go in directly: a stylesheet passed to implement() that never fires its load event
// leaves the module stuck in "executing", and everything queued behind it with it.
function addStyles(styles) {
(styles || []).forEach(function (suffix) {
if (text[suffix]) {
mw.util.addCSS(text[suffix]);
}
});
}
var bad = null;
var payloads = {};
ours.forEach(function (name) {
var m = MODULES[name];
if (m.script) {
payloads[name] = new Function("$", "jQuery", "require", "module", text[m.script]);
return;
}
var files = {};
Object.keys(m.files).forEach(function (key) {
var src = text[m.files[key]];
if (key.slice(-5) === ".json") {
try {
files[key] = JSON.parse(src);
} catch (e) {
bad = m.files[key] + " nederbasdar e (JSON parse ket): " + e.message;
}
return;
}
files[key] = new Function("require", "module", "exports", src);
});
payloads[name] = { main: m.main, files: files };
});
if (bad) {
mw.notify(bad, { type: "error" });
return;
}
// implement() runs a module as soon as its dependencies are ready, so a deferred one is
// handed over only when something asks for it.
function implement(name) {
if (!payloads[name]) {
return;
}
var payload = payloads[name];
delete payloads[name];
if (mw.loader.getState(name) === null) {
mw.loader.register(name, "0", MODULES[name].deps || []);
} else if (mw.loader.getState(name) !== "registered") {
console.warn("loader.js: " + name + " is already loading from the wiki; its copy runs.");
return;
}
mw.loader.implement(name, payload);
addStyles(MODULES[name].styles);
}
// A deferred module reached as another's dependency is handed over too, or ResourceLoader
// fetches the site's copy of it instead.
function withDeps(list) {
var out = [];
(function walk(items) {
(items || []).forEach(function (n) {
if (out.indexOf(n) !== -1) {
return;
}
out.push(n);
var entry = mw.loader.moduleRegistry[n];
walk(entry ? entry.dependencies : MODULES[n] && MODULES[n].deps);
});
}(list));
return out;
}
var origUsing = mw.loader.using;
mw.loader.using = function (modules) {
withDeps([].concat(modules || [])).forEach(implement);
return origUsing.apply(this, arguments);
};
ours.forEach(function (name) {
if (!MODULES[name].deferred) {
implement(name);
}
});
var action = mw.config.get("wgAction");
var starting = ours.filter(function (name) {
var m = MODULES[name];
return m.start && (!m.actions || m.actions.indexOf(action) !== -1);
});
var settled = false;
mw.loader.using(starting).then(function () {
settled = true;
console.log("loader.js: started " + starting.join(", "));
}, function (err) {
settled = true;
mw.notify("Xeta barkirinê: " + err, { type: "error" });
});
// A dependency stuck half-loaded leaves using() pending forever, with no error at all.
setTimeout(function () {
if (settled) {
return;
}
var stuck = withDeps(starting)
.filter(function (m) { return mw.loader.getState(m) !== "ready"; })
.map(function (m) { return m + "=" + mw.loader.getState(m); });
mw.notify("Barkirin asê ma: " + stuck.join(", "), { type: "warn" });
}, 10000);
}).fail(function () {
mw.notify("Nikare hemû rûpelên " + BASE + " bixwîne: " + suffixes.join(", "), { type: "error" });
});
}
// The test gadget declares no dependencies, so mw.util is not there yet when this runs.
mw.loader.using("mediawiki.util").then(run);
})();
// </nowiki>
jn6wse86lukvbpik28qkvn80n3jd0al
6531989
6531966
2026-09-17T16:50:13Z
Wikihez
7344
Test loader: EtytreeZoom back in, for the zoom-button icon change
6531989
javascript
text/javascript
// <nowiki>
// Test harness for the gadgets. Loaded from [[MediaWiki:Gadget-test.js]], which is one line:
// mw.loader.load('//ku.wiktionary.org/w/index.php?title=Bikarhêner:Wikihez/Gadget-test/loader.js&action=raw&ctype=text/javascript');
//
// Runs the userspace copies in place of the site's gadgets, under the same module names. A gadget the
// wiki has registered but not yet loaded is implemented here from the test page instead, so its
// dependencies are the real ones from [[MediaWiki:Gadgets-definition]]. One already loading from the wiki
// cannot be replaced: turn the default gadgets that load it (start and wikt.dsb load wikt.core) off in
// Special:Preferences while testing, and this names any that are still on.
//
// MODULES lists only the gadgets with changes not yet live; everything else runs the site's copy. A
// gadget is added back when it is being changed, and taken out once that change is deployed.
(function () {
"use strict";
// Set synchronously, before add-examples' DOM-ready pass, so its own inline link is never built.
window.wikt = window.wikt || {};
wikt.addExamples = wikt.addExamples || {};
wikt.addExamples.suppressLinks = true;
// Subpage segments are case-sensitive, so BASE must match the pages exactly.
var BASE = "Bikarhêner:Wikihez/Gadget-test";
// A value carrying a namespace is used as the whole title; anything else is appended to BASE.
function pageTitle(suffix) {
return suffix.indexOf(":") !== -1 ? suffix : BASE + suffix;
}
// module name -> package ({main, files}) or plain script ({script}), plus styles.
// deferred held back until something asks for it, as production fetches it only then
// start asked for on page load, as production loads a default gadget
// deps only for a gadget not yet in Gadgets-definition; a registered one keeps the wiki's
// actions with start, only on these wgAction values, as a definition's "actions=" does
var MODULES = {
"ext.gadget.wikt.core": {
main: "wikt.core.js",
files: {
// Keyed exactly as Gadgets-definition names them, so a require that would fail in
// production fails here first.
"wikt.core.js": "/wikt-core.js",
"wikt.core/sections.js": "/sections.js",
"wikt.core/prefs.js": "/prefs.js",
"wikt.core/data-sections.json": "/data-sections.json"
},
styles: ["/tokens.css"]
},
"ext.gadget.wikt.edit": {
main: "wikt.edit.js",
files: {
"wikt.edit.js": "/wikt-edit.js",
"wikt.edit/util.js": "/util.js",
"wikt.edit/wikitext.js": "/wikitext.js",
"wikt.edit/langs.js": "/langs.js",
"wikt.edit/labels.js": "/labels.js",
"wikt.edit/posparams.js": "/posparams.js",
"wikt.edit/data-main.json": "/data-main.json",
"wikt.edit/ziman-core.json": "/ziman-core.json"
},
styles: ["/wikt-edit.css"]
},
"ext.gadget.wikt.cite": {
main: "wikt.cite.js",
files: { "wikt.cite.js": "/cite.js" },
styles: ["/cite.css"]
},
// Unchanged, but it is what loads the modules above and below, and it is off in Preferences while
// they are tested.
"ext.gadget.wikt.dsb": {
start: true,
main: "wikt.dsb.js",
files: { "wikt.dsb.js": "/dsb.js" },
styles: ["/dsb.css"]
},
"ext.gadget.wikt.dsb-panels": {
deferred: true,
main: "wikt.dsb-panels.js",
files: { "wikt.dsb-panels.js": "/dsb-panels.js" },
styles: ["/dsb-panels.css"]
},
// A default gadget: off in Preferences while its copy is tested.
"ext.gadget.EtytreeZoom": {
start: true,
script: "Bikarhêner:Wikihez/Gadget-EtytreeZoom.js",
styles: []
}
};
// Force dataType "text": otherwise jQuery sees action=raw's JavaScript Content-Type and runs it.
function raw(suffix) {
return $.get({
url: mw.util.getUrl(pageTitle(suffix), { action: "raw" }),
dataType: "text"
});
}
function run() {
var names = Object.keys(MODULES);
// A module the wiki has started loading cannot be implemented a second time.
var busy = names.filter(function (name) {
var state = mw.loader.getState(name);
return state !== null && state !== "registered";
});
if (busy.length) {
console.warn("loader.js: already loaded from the wiki, so the site's copy runs: " + busy.join(", "));
mw.notify("loader.js: van amûran di Tercîhan de bigire, da ku kopiya ceribandinê bixebite: " +
busy.map(function (n) { return n.replace("ext.gadget.", ""); }).join(", "), { type: "warn" });
}
var ours = names.filter(function (name) {
return busy.indexOf(name) === -1;
});
// Every distinct page this harness needs, fetched once each.
var suffixes = [];
function need(suffix) {
if (suffixes.indexOf(suffix) === -1) {
suffixes.push(suffix);
}
}
ours.forEach(function (name) {
var m = MODULES[name];
if (m.script) {
need(m.script);
} else {
Object.keys(m.files).forEach(function (key) { need(m.files[key]); });
}
m.styles.forEach(need);
});
if (!suffixes.length) {
return;
}
$.when.apply($, suffixes.map(raw)).done(function () {
var args = suffixes.length === 1 ? [arguments] : [].slice.call(arguments);
var text = {};
suffixes.forEach(function (s, i) {
text[s] = args[i][0];
});
// Styles go in directly: a stylesheet passed to implement() that never fires its load event
// leaves the module stuck in "executing", and everything queued behind it with it.
function addStyles(styles) {
(styles || []).forEach(function (suffix) {
if (text[suffix]) {
mw.util.addCSS(text[suffix]);
}
});
}
var bad = null;
var payloads = {};
ours.forEach(function (name) {
var m = MODULES[name];
if (m.script) {
payloads[name] = new Function("$", "jQuery", "require", "module", text[m.script]);
return;
}
var files = {};
Object.keys(m.files).forEach(function (key) {
var src = text[m.files[key]];
if (key.slice(-5) === ".json") {
try {
files[key] = JSON.parse(src);
} catch (e) {
bad = m.files[key] + " nederbasdar e (JSON parse ket): " + e.message;
}
return;
}
files[key] = new Function("require", "module", "exports", src);
});
payloads[name] = { main: m.main, files: files };
});
if (bad) {
mw.notify(bad, { type: "error" });
return;
}
// implement() runs a module as soon as its dependencies are ready, so a deferred one is
// handed over only when something asks for it.
function implement(name) {
if (!payloads[name]) {
return;
}
var payload = payloads[name];
delete payloads[name];
if (mw.loader.getState(name) === null) {
mw.loader.register(name, "0", MODULES[name].deps || []);
} else if (mw.loader.getState(name) !== "registered") {
console.warn("loader.js: " + name + " is already loading from the wiki; its copy runs.");
return;
}
mw.loader.implement(name, payload);
addStyles(MODULES[name].styles);
}
// A deferred module reached as another's dependency is handed over too, or ResourceLoader
// fetches the site's copy of it instead.
function withDeps(list) {
var out = [];
(function walk(items) {
(items || []).forEach(function (n) {
if (out.indexOf(n) !== -1) {
return;
}
out.push(n);
var entry = mw.loader.moduleRegistry[n];
walk(entry ? entry.dependencies : MODULES[n] && MODULES[n].deps);
});
}(list));
return out;
}
var origUsing = mw.loader.using;
mw.loader.using = function (modules) {
withDeps([].concat(modules || [])).forEach(implement);
return origUsing.apply(this, arguments);
};
ours.forEach(function (name) {
if (!MODULES[name].deferred) {
implement(name);
}
});
var action = mw.config.get("wgAction");
var starting = ours.filter(function (name) {
var m = MODULES[name];
return m.start && (!m.actions || m.actions.indexOf(action) !== -1);
});
var settled = false;
mw.loader.using(starting).then(function () {
settled = true;
console.log("loader.js: started " + starting.join(", "));
}, function (err) {
settled = true;
mw.notify("Xeta barkirinê: " + err, { type: "error" });
});
// A dependency stuck half-loaded leaves using() pending forever, with no error at all.
setTimeout(function () {
if (settled) {
return;
}
var stuck = withDeps(starting)
.filter(function (m) { return mw.loader.getState(m) !== "ready"; })
.map(function (m) { return m + "=" + mw.loader.getState(m); });
mw.notify("Barkirin asê ma: " + stuck.join(", "), { type: "warn" });
}, 10000);
}).fail(function () {
mw.notify("Nikare hemû rûpelên " + BASE + " bixwîne: " + suffixes.join(", "), { type: "error" });
});
}
// The test gadget declares no dependencies, so mw.util is not there yet when this runs.
mw.loader.using("mediawiki.util").then(run);
})();
// </nowiki>
52t4dg3qadt0b37w3msltauqihrtnd1
Bikarhêner:Wikihez/Gadget-test/wikitext.js
2
1358896
6531944
6425755
2026-09-17T14:17:13Z
Wikihez
7344
POS conversion keeps the heading depth and replaces a numbered sense-group heading; numbered headings tagged with {{cure?}} count as Mane sections; + Îzah for Kurmancî senses
6531944
javascript
text/javascript
// <nowiki>
/*
* Page: [[MediaWiki:Gadget-wikt.edit/wikitext.js]]
*
*/
// Wikitext parsing and serialisation: template calls, kol/col word lists, template redirects and
// TemplateData lookups. No DOM, no wizard state.
(function () {
"use strict";
var util = require("./util.js");
var sections = require("ext.gadget.wikt.core").sections;
var startData = require("./data-main.json");
var escapeRegExp = util.escapeRegExp;
var SubmitAbort = util.SubmitAbort;
var MANE_SECTION = sections.MANE_SECTION;
// The "needs a POS" maintenance tag under a placeholder "Mane" heading, under every name it is called by
// on real pages (data-main.json's maneTags). A space inside a tag name matches any run of whitespace.
var MANE_TAG_REGEX = new RegExp(
"^\\{\\{\\s*(?:" + startData.maneTags.map(function (tag) {
return escapeRegExp(tag).replace(/ +/g, "\\s+");
}).join("|") + ")\\s*(?:\\|[^{}]*)?\\}\\}\\s*$", "i");
var HEADING_LINE = /^(={3,6})[ ]*(.+?)[ ]*\1\s*$/;
// A numbered group heading ("=== 3. roj ===") with the maintenance tag before its first definition is a
// "Mane" section under another name, as sections.isManeHeading() reads it on the page. Its depth, or 0.
function numberedManeDepth(lines, index) {
var m = HEADING_LINE.exec(lines[index] || "");
if (!m || !sections.isNumberedHeadingText(m[2])) {
return 0;
}
for (var i = index + 1; i < lines.length && !/^[#=]/.test(lines[i]); i++) {
if (MANE_TAG_REGEX.test(lines[i])) {
return m[1].length;
}
}
return 0;
}
// The numbered group heading one level up that a section sits straight under, blank lines aside
// ("=== 2. dîn ===" then "==== Mane ===="), as a line index; -1 when there is none.
function numberedGroupAbove(lines, index, depth) {
var at = index - 1;
while (at >= 0 && !$.trim(lines[at])) {
at--;
}
var m = at >= 0 && HEADING_LINE.exec(lines[at]);
return m && m[1].length === depth - 1 && sections.isNumberedHeadingText(m[2]) ? at : -1;
}
var templateDataApi = new mw.Api();
// Templates that mean the same thing. The first entry of each row is what gets written; every entry is
// recognised when reading, so an existing call keeps the spelling the page already uses.
// Matched differently, so listed separately: WORDLIST_TEMPLATES and MANE_TAG_REGEX.
var TEMPLATE_ALIASES = startData.templateAliases;
// Every alias -> the name this gadget uses for it.
var TEMPLATE_CANONICAL = {};
Object.keys(TEMPLATE_ALIASES).forEach(function (key) {
TEMPLATE_ALIASES[key].forEach(function (alias) {
TEMPLATE_CANONICAL[alias] = key;
});
});
function canonicalTemplateKey(name) {
var trimmed = $.trim(name || "");
return TEMPLATE_CANONICAL[trimmed] || trimmed;
}
// Regex alternation matching any spelling of one key. Callers wrap it in their own group.
function aliasPattern(key) {
return (TEMPLATE_ALIASES[key] || [key]).map(escapeRegExp).join("|");
}
// {{kol}}, {{kol2}}, {{kol3}} and their col* spellings are the same column word-list template. New sections
// are written as kol3; any of the six is recognized when parsing an existing one.
var WORDLIST_TEMPLATES = startData.wordlistTemplates;
// The leading run of "{{...}}" calls on a definition line; "rest" is the gloss text after them.
function splitLeadingCalls(afterMarker) {
var calls = [];
var rest = afterMarker;
var leadingRegex = /^\{\{\s*([^{}|]+?)\s*\|([^{}]*)\}\}\s*/;
var m;
while ((m = leadingRegex.exec(rest)) !== null) {
calls.push({ key: m[1], params: m[2].split("|") });
rest = rest.slice(m[0].length);
}
return { calls: calls, rest: rest };
}
// Splits a template call's inner text on its top-level "|", ignoring pipes nested in {{...}} or [[...]].
function splitTemplateParams(inner) {
var parts = [];
var buf = "";
var tpl = 0;
var link = 0;
for (var i = 0; i < inner.length; i++) {
var two = inner.substr(i, 2);
if (two === "{{" || two === "}}" || two === "[[" || two === "]]") {
if (two === "{{") { tpl++; } else if (two === "}}") { tpl--; } else if (two === "[[") { link++; } else { link--; }
buf += two;
i++;
continue;
}
if (inner.charAt(i) === "|" && tpl === 0 && link === 0) {
parts.push(buf);
buf = "";
continue;
}
buf += inner.charAt(i);
}
parts.push(buf);
return parts;
}
// Every top-level "{{...}}" call in a string, and whether anything else (bar whitespace) sits between them.
function parseTemplateCalls(text) {
var calls = [];
var otherContent = false;
var plainFrom = 0;
var i = 0;
while (i < text.length) {
if (text.substr(i, 2) !== "{{") {
i++;
continue;
}
if ($.trim(text.slice(plainFrom, i))) {
otherContent = true;
}
var depth = 0;
var close = -1;
for (var j = i; j < text.length - 1; j++) {
var two = text.substr(j, 2);
if (two === "{{") {
depth++;
j++;
} else if (two === "}}") {
depth--;
j++;
if (depth === 0) {
close = j + 1;
break;
}
}
}
if (close === -1) {
otherContent = true; // unbalanced braces: not something to offer a form for
break;
}
var parts = splitTemplateParams(text.slice(i + 2, close - 2));
calls.push({ name: $.trim(parts[0]), params: parts.slice(1), start: i, end: close });
i = close;
plainFrom = close;
}
if ($.trim(text.slice(plainFrom))) {
otherContent = true;
}
return { calls: calls, otherContent: otherContent };
}
// "ku", "îngilîzî", "id=ziman" -> positional values in order plus a named map, keeping the keys the call
// actually uses (a positional value is keyed by its 1-based position, matching TemplateData).
function splitCallParams(params) {
var values = {};
var used = [];
var positional = 0;
params.forEach(function (raw) {
// Anything but | = { } can be a parameter name; Kurdish ones carry diacritics ("rêz=", "jêder=").
var m = /^\s*([^|={}\n]+?)\s*=([\s\S]*)$/.exec(raw);
var key;
if (m) {
key = m[1];
values[key] = $.trim(m[2]);
} else {
positional++;
key = String(positional);
values[key] = $.trim(raw);
}
used.push(key);
});
return { values: values, used: used };
}
// Rebuilds a call from the form's values. Positional parameters keep their slots even when blanked --
// dropping "1" would silently promote "2" into its place.
function buildTemplateCall(name, values, keys) {
var numeric = keys.filter(function (k) {
return /^\d+$/.test(k);
}).sort(function (a, b) {
return a - b;
});
var lastFilled = 0;
numeric.forEach(function (k) {
if ($.trim(values[k] || "")) {
lastFilled = Math.max(lastFilled, parseInt(k, 10));
}
});
var out = "{{" + name;
numeric.forEach(function (k) {
if (parseInt(k, 10) <= lastFilled) {
out += "|" + $.trim(values[k] || "");
}
});
keys.forEach(function (k) {
if (/^\d+$/.test(k)) {
return;
}
var v = $.trim(values[k] || "");
if (v) {
out += "|" + k + "=" + v;
}
});
return out + "}}";
}
// TemplateData labels and descriptions are {langCode: text} objects; prefer Kurdish, then the wiki's own
// content language, then whatever is present.
function tdText(value) {
if (!value) {
return "";
}
if (typeof value === "string") {
return value;
}
return value.ku || value[mw.config.get("wgContentLanguage")] || value[Object.keys(value)[0]] || "";
}
// Cached per template name. A template with no TemplateData resolves to null, not an error -- the form
// still renders, just with raw parameter names for labels.
var templateDataCache = {};
// Fetches TemplateData for many templates in as few requests as the API's 50-title cap allows, filling
// the same cache fetchTemplateData() reads -- so a picker with hundreds of entries (Kategorî:Çavkanî,
// say) costs a handful of requests instead of one per template. Every requested name is cached (a miss
// as null) by the time this resolves; a name already cached, pending or not, is left untouched.
function fetchTemplateDataBatch(names) {
var missing = [];
(names || []).forEach(function (name) {
if (!name || templateDataCache[name] || missing.indexOf(name) !== -1) {
return;
}
// Baked-in entries answer here too, so a picker row for one costs nothing.
var baked = bakedTemplateData(name);
if (baked) {
templateDataCache[name] = $.Deferred().resolve(baked).promise();
return;
}
missing.push(name);
});
if (!missing.length) {
return $.Deferred().resolve().promise();
}
var chunks = [];
for (var i = 0; i < missing.length; i += 50) {
chunks.push(missing.slice(i, i + 50));
}
function fetchChunk(chunk) {
return templateDataApi.get({
action: "templatedata",
titles: chunk.map(function (n) { return "Şablon:" + n; }).join("|"),
redirects: 1,
formatversion: 2
}).then(function (data) {
// "pages" is an object keyed by page id even under formatversion=2 -- this endpoint (a
// third-party extension, not a core action) doesn't follow the "pages as array" convention
// core actions do -- and a title with no TemplateData at all is simply absent, not listed
// with an empty entry.
var byTitle = {};
$.each((data && data.pages) || {}, function (_, page) {
if (page && page.title) {
byTitle[page.title] = page;
}
});
// A redirect's/normalization's own target may itself need one more hop.
((data && data.redirects) || []).forEach(function (r) {
if (byTitle[r.to] && !byTitle[r.from]) {
byTitle[r.from] = byTitle[r.to];
}
});
((data && data.normalized) || []).forEach(function (n) {
if (byTitle[n.to] && !byTitle[n.from]) {
byTitle[n.from] = byTitle[n.to];
}
});
chunk.forEach(function (name) {
templateDataCache[name] = $.Deferred().resolve(byTitle["Şablon:" + name] || null).promise();
});
}, function () {
chunk.forEach(function (name) {
templateDataCache[name] = $.Deferred().resolve(null).promise();
});
});
}
return $.when.apply($, chunks.map(fetchChunk));
}
// data-main.json's own copy of the parameter lists for the templates whose dialogs open constantly, so
// those need no request at all. Shaped like the API's own answer, so nothing downstream can tell the
// difference; a template that is not in the table is fetched as before.
function bakedTemplateData(name) {
var baked = (startData.templateParams || {})[name];
if (!baked) {
return null;
}
var params = {};
(baked.order || []).forEach(function (key) {
params[key] = { label: (baked.labels || {})[key] || null };
});
return { title: "Şablon:" + name, params: params, paramOrder: baked.order || [] };
}
function fetchTemplateData(name) {
if (!templateDataCache[name]) {
var baked = bakedTemplateData(name);
if (baked) {
templateDataCache[name] = $.Deferred().resolve(baked).promise();
return templateDataCache[name];
}
}
if (!templateDataCache[name]) {
templateDataCache[name] = templateDataApi.get({
action: "templatedata",
titles: "Şablon:" + name,
redirects: 1,
formatversion: 2
}).then(function (data) {
var pages = data && data.pages;
var key = pages && Object.keys(pages)[0];
return key ? pages[key] : null;
}, function () {
return null;
});
}
return templateDataCache[name];
}
// One line of a TemplateData description, capped so a picker row stays a picker row rather than
// wrapping. Shared so every picker that shows descriptions trims them the same way.
// Numbers repeated part-of-speech headings inside one language section: two "=== Rengdêr ===" are
// indistinguishable on the page, in the table of contents and to anything linking at one. Every
// repeated heading is numbered 1..n, so an existing lone "Rengdêr" becomes "Rengdêr 1" the moment a
// second is added, and a heading that stops being repeated loses its number again.
//
// Numbers already on a heading are stripped before counting, so the result depends only on what is
// there now -- removing the middle one of three closes the gap instead of leaving 1, 3.
// Only "===" headings are touched: "====" subsections and the "==" language header are left alone.
function numberRepeatedPosHeadings(sectionText) {
var HEADING = /^(===[ \t]*)([^=\n]+?)([ \t]*===)[ \t]*$/gm;
function base(name) {
return name.replace(/\s+\d+$/, "");
}
var counts = {};
var m;
HEADING.lastIndex = 0;
while ((m = HEADING.exec(sectionText)) !== null) {
var b = base(m[2]);
counts[b] = (counts[b] || 0) + 1;
}
var seen = {};
HEADING.lastIndex = 0;
return sectionText.replace(HEADING, function (all, open, name, close) {
var key = base(name);
if (counts[key] < 2) {
return open + key + close;
}
seen[key] = (seen[key] || 0) + 1;
return open + key + " " + seen[key] + close;
});
}
function shortDesc(text, maxLen) {
maxLen = maxLen || 90;
var line = $.trim(String(text || "").split("\n")[0]);
return line.length > maxLen ? line.slice(0, maxLen) + "…" : line;
}
// Pulls a {{kol3|ku|cure=Jê|word|word}}-style call apart into the bits an edit has to preserve (template
// name, language, named params) plus the positional word list. Null if the body has no such call.
// Every word-list call in a section body, in order with absolute offsets. A section commonly holds
// several: one per "sernav=" group, often alongside an unheaded one.
function parseWordListCalls(text) {
var calls = [];
var offset = 0;
for (;;) {
var call = parseWordListCall(text.slice(offset));
if (!call) {
return calls;
}
call.start += offset;
call.end += offset;
calls.push(call);
offset = call.end;
}
}
function parseWordListCall(text) {
var m = new RegExp("\\{\\{\\s*(" + WORDLIST_TEMPLATES.join("|") + ")\\s*[|}]", "i").exec(text);
if (!m) {
return null;
}
var open = m.index;
var depth = 0;
var close = -1;
for (var i = open; i < text.length - 1; i++) {
var two = text.substr(i, 2);
if (two === "{{") {
depth++;
i++;
} else if (two === "}}") {
depth--;
i++;
if (depth === 0) {
close = i + 1;
break;
}
}
}
if (close === -1) {
return null;
}
var parts = splitTemplateParams(text.slice(open + 2, close - 2));
var named = [];
var words = [];
for (var pi = 2; pi < parts.length; pi++) {
var val = $.trim(parts[pi]);
if (!val) {
continue;
}
if (/^[A-Za-z][A-Za-z0-9_-]*\s*=/.test(val)) {
named.push(val);
} else {
words.push(val);
}
}
return {
name: $.trim(parts[0]),
lang: parts.length > 1 ? $.trim(parts[1]) : "",
named: named,
words: words,
start: open,
end: close
};
}
// Named parameters of a kol/col call are carried around as raw "key=value" strings ("cure=Jê").
// Modul:columns declares sernav alongside cure, so it round-trips like any other named parameter.
function namedParam(named, key) {
var prefix = new RegExp("^" + escapeRegExp(key) + "\s*=");
for (var i = 0; i < named.length; i++) {
if (prefix.test(named[i])) {
return $.trim(named[i].replace(prefix, ""));
}
}
return "";
}
// A copy of `named` with key set, or with it removed when the value is blank.
function withNamedParam(named, key, value) {
var prefix = new RegExp("^" + escapeRegExp(key) + "\s*=");
var out = named.filter(function (param) {
return !prefix.test(param);
});
if ($.trim(value || "")) {
out.push(key + "=" + $.trim(value));
}
return out;
}
// Canonical multi-line layout, matching how existing ku.wiktionary lists are written (one word per line).
function buildWordListCall(name, lang, named, words) {
var head = "{{" + name + "|" + lang;
named.forEach(function (n) {
head += "|" + n;
});
return words.length ? head + "\n|" + words.join("\n|") + "\n}}" : head + "\n}}";
}
// A word-list section body written as plain bullets -- "* [[kelîme]]" or "* {{g|ku|kelîme}}" -- read as a word
// list so it can be edited in the normal widget and saved back as a real {{kol3}}. Null for anything
// richer (a piped link, a {{g}} carrying extra parameters, prose between the bullets), which is left as
// raw wikitext rather than guessed at.
// A "{{mj|a|b}}" label at one end of a bullet. Several labels become one comma-joined modifier value,
// which is how the word-list templates read "<e:...>" anyway.
var MJ_LEADING = /^\{\{\s*mj\s*\|([^{}]*)\}\}\s*/;
var MJ_TRAILING = /\s*\{\{\s*mj\s*\|([^{}]*)\}\}$/;
function mjValue(params) {
return params.split("|").map(function (part) {
return $.trim(part);
}).filter(Boolean).join(", ");
}
/**
* One bullet's word, or null if the line is carrying something this cannot express.
*
* "[[kelîme]]" and "{{g|ku|kelîme}}" are the plain forms. A "{{mj|...}}" label in front of the word becomes
* an "<e:...>" modifier and one after it "<ee:...>" -- the same distinction the label templates make
* between a label that reads before the word and one that reads after it.
*/
function parseBulletWord(item) {
var lead = "";
var tail = "";
var m = MJ_LEADING.exec(item);
if (m) {
lead = mjValue(m[1]);
item = $.trim(item.slice(m[0].length));
}
m = MJ_TRAILING.exec(item);
if (m) {
tail = mjValue(m[1]);
item = $.trim(item.slice(0, item.length - m[0].length));
}
var word = null;
var link = /^\[\[([^\[\]|]+)\]\]$/.exec(item);
if (link) {
word = $.trim(link[1]);
} else {
var parsed = parseTemplateCalls(item);
if (parsed.otherContent || parsed.calls.length !== 1) {
return null;
}
var call = parsed.calls[0];
// {{g|lang|kelîme}} exactly: a third parameter means it is carrying something this cannot express.
if (canonicalTemplateKey(call.name) !== "g" || call.params.length !== 2) {
return null;
}
word = $.trim(call.params[1]);
}
if (!word) {
return null;
}
return word + (lead ? "<e:" + lead + ">" : "") + (tail ? "<ee:" + tail + ">" : "");
}
function parseBulletWordList(body) {
var lines = (body || "").split("\n");
var words = [];
for (var i = 0; i < lines.length; i++) {
var line = $.trim(lines[i]);
if (!line) {
continue;
}
var bullet = /^\*\s*(.+)$/.exec(line);
if (!bullet) {
return null;
}
var word = parseBulletWord($.trim(bullet[1]));
if (word === null) {
return null;
}
words.push(word);
}
return words.length ? words : null;
}
// "{{h-bnr|a|b}}" — the "herwiha binêre" hatnote that sits above the first language heading. Empty
// entries are dropped; an empty list has no call at all. name keeps whichever of the seven spellings
// the page already uses, so editing one never renames it.
function buildHbnrCall(pages, name) {
var kept = $.grep(pages || [], function (p) { return !!$.trim(p); });
return kept.length ? "{{" + (name || "h-bnr") + "|" + $.map(kept, $.trim).join("|") + "}}" : "";
}
// The "{{herwiha binêre|...}}" call on a page, under any of its names, or null. subpage is set when
// the call names one -- "{{h-bnr/-ane}}" -- because those hold a list shared by every page that
// transcludes them, which lives on the template rather than here.
function findHbnrCall(pageContent) {
var lines = (pageContent || "").split("\n");
var pattern = new RegExp("^\\{\\{\\s*(" + aliasPattern("h-bnr") + ")(/[^|}]*)?\\s*([|}])", "i");
for (var i = 0; i < lines.length; i++) {
if (/^==[^=]/.test(lines[i])) {
break; // the hatnote sits above the first language heading
}
var m = pattern.exec($.trim(lines[i]));
if (m) {
var call = parseTemplateCalls($.trim(lines[i])).calls[0];
return {
lineIndex: i,
name: m[1] + (m[2] || ""),
subpage: m[2] ? m[1] + m[2] : null,
words: (call && call.params) || []
};
}
}
return null;
}
// One "[[Wêne:...|thumb|...]]" line. alt is left out when it is empty rather than written blank.
// extra is the line's own display keywords ("thumb", "left", "frameless", ...) in the order it had them,
// kept so editing a caption never drops them; a new image gets "thumb". size ("250px", "upright=1.2")
// has its own box, so it is written after them.
function buildImageLine(image) {
var file = $.trim((image && image.file) || "");
if (!file) {
return "";
}
var extra = (image.extra && image.extra.length) ? image.extra : ["thumb"];
var size = $.trim(image.size || "");
var alt = $.trim(image.alt || "");
return "[[Wêne:" + file + "|" + extra.join("|") + (size ? "|" + size : "") +
(alt ? "|alt=" + alt : "") + "|" + $.trim(image.caption || "") + "]]";
}
var IMAGE_SIZE = /^\s*(?:\d+\s*px|x\s*\d+\s*px|\d+\s*x\s*\d+\s*px|upright(?:\s*=\s*[\d.]+)?)\s*$/i;
var IMAGE_KEYWORD = /^\s*(?:thumb|thumbnail|frame|framed|frameless|border|right|left|center|centre|none|baseline|middle|top|bottom)\s*$/i;
// The {file, caption, alt} behind one "[[Wêne:...|thumb|...]]" line, or null for anything else. The
// caption is the last parameter; "alt=" and the sizing/placement keywords are read off the ones before
// it, so a hand-written line with "|left|200px" still comes back editable.
//
// Split on top-level pipes only: a caption routinely holds "{{w|Riya Teze (rojname)|Riya Teze}}" or
// "[[kaxiz|kaxizên]]", and a plain split would take their pipes for the image's own.
function parseImageLine(line) {
var m = /^\[\[\s*(?:Wêne|File|Image|Dosya)\s*:([\s\S]*)\]\]$/i.exec($.trim(line || ""));
if (!m || !closesOnlyAtEnd(m[1])) {
return null;
}
var parts = splitTemplateParams(m[1]);
var file = $.trim(parts.shift());
if (!file) {
return null;
}
var alt = "";
var caption = "";
var size = "";
var extra = [];
$.each(parts, function (_, part) {
var altMatch = /^\s*alt\s*=\s*([\s\S]*)$/i.exec(part);
if (altMatch) {
alt = $.trim(altMatch[1]);
return;
}
if (IMAGE_SIZE.test(part)) {
size = $.trim(part);
return;
}
if (IMAGE_KEYWORD.test(part)) {
extra.push($.trim(part));
return;
}
caption = $.trim(part);
});
return { file: file, caption: caption, alt: alt, size: size, extra: extra };
}
// Whether a link's inner text never closes the link early: "a.jpg|x]] [[Wêne:b.jpg|y" would otherwise
// pass for one image holding the text of two.
function closesOnlyAtEnd(inner) {
var depth = 0;
for (var i = 0; i < inner.length; i++) {
var two = inner.substr(i, 2);
if (two === "[[") {
depth++;
i++;
} else if (two === "]]") {
depth--;
i++;
if (depth < 0) {
return false;
}
}
}
return depth === 0;
}
// A caption on a non-Kurdish entry is written in that language, so it is wrapped for the reader --
// unless whoever typed it already wrapped it themselves.
function captionForLang(caption, lang) {
var text = $.trim(caption || "");
if (!text || !lang || lang === "ku" || /^\{\{\s*zim\s*\|/i.test(text)) {
return text;
}
return "{{zim|" + lang + "|" + text + "}}";
}
// One "* {{dû|lang|kelîme|kelîme}}" line of a Dûnde list.
function buildDundeLine(lang, words) {
var kept = $.grep(words || [], function (w) { return !!$.trim(w); });
return kept.length ? "* {{dû|" + lang + "|" + $.map(kept, $.trim).join("|") + "}}" : "";
}
// Regex source for a "== {{ziman|CODE}} ==" heading; codePattern is a regex fragment. No leading
// newline: callers that want one prepend it, and callers that index into the page must not have it.
function langHeaderSource(codePattern) {
return "==\\s*\\{\\{\\s*ziman\\s*\\|\\s*(" + codePattern + ")\\s*\\}\\}\\s*==";
}
/**
* One TemplateData page in the shape the pickers actually want: the description, and one entry per
* documented parameter in the template's own paramOrder. Labels and descriptions can be language maps,
* so tdText picks the right one; null when the template has no TemplateData at all, which is common.
*
* Shaped once and kept, so the wizard's ⓘ popup, the citation form and DefSideBoxes' template dialog
* share one request and one copy per template.
*/
var shapedCache = {};
function shapeTemplateDoc(doc) {
if (!doc) {
return null;
}
var params = [];
(doc.paramOrder || Object.keys(doc.params || {})).forEach(function (key) {
var p = (doc.params || {})[key];
if (!p) {
return;
}
params.push({
key: key,
label: tdText(p.label) || "",
required: !!p.required,
deprecated: !!p.deprecated
});
});
return { desc: tdText(doc.description) || "", params: params };
}
function templateDoc(name) {
if (Object.prototype.hasOwnProperty.call(shapedCache, name)) {
return $.Deferred().resolve(shapedCache[name]).promise();
}
return fetchTemplateData(name).then(function (doc) {
shapedCache[name] = shapeTemplateDoc(doc);
return shapedCache[name];
});
}
// Many at once, so a picker full of rows costs one request. Resolves once every name is shaped and in
// the cache, which is what a caller that then reads templateDocNow() depends on.
function templateDocBatch(names) {
var wanted = $.grep(names || [], function (n) {
return n && !Object.prototype.hasOwnProperty.call(shapedCache, n);
});
if (!wanted.length) {
return $.Deferred().resolve().promise();
}
return fetchTemplateDataBatch(wanted).then(function () {
return $.when.apply($, wanted.map(function (n) {
return templateDoc(n);
}));
});
}
// What is already known, without asking: null for a template nobody has fetched yet. For drawing a
// list that must not wait, after templateDocBatch() has filled the cache.
function templateDocNow(name) {
return Object.prototype.hasOwnProperty.call(shapedCache, name) ? shapedCache[name] : null;
}
// ---- locating a section, and a definition inside it ----
//
// One copy, shared: DefSideBoxes and add-examples both write into a numbered sense of a numbered
// section, and both find it here.
// Start and end offsets of one language section within the page. Throws when the page has no such
// section, naming the code that was looked for.
function langSectionRange(pageContent, language) {
var langSectionIndex = pageContent.search(new RegExp(langHeaderSource(escapeRegExp(language))));
if (langSectionIndex === -1) {
// Quoted: a stray space or wrong case in the code is invisible otherwise.
throw new SubmitAbort("Xetayeke bi koda zimanî heye. Koda zimanî: " + JSON.stringify(language));
}
var afterLangHeading = pageContent.indexOf("\n", langSectionIndex);
afterLangHeading = afterLangHeading === -1 ? pageContent.length : afterLangHeading + 1;
var nextLangSectionMatch = /\n==[^=\n][^\n]*==/.exec(pageContent.slice(afterLangHeading));
return {
start: langSectionIndex,
end: nextLangSectionMatch ? afterLangHeading + nextLangSectionMatch.index : pageContent.length
};
}
/**
* The line index of one "=== ... ===" heading, and the lines to read it from.
*
* The heading is found by section key + ordinal, never by a DOM id number: MediaWiki's page-wide
* anchor uniquification can attach a number to a heading that has nothing to do with its position.
* The closing run is anchored to the opening one, or "=== Navdêra pirjimar ===" would count as a
* "Navdêr" here while sectionTypeOf() rejects it, and the two ordinals would drift apart.
*
* A part of speech is usually "===", but an entry that groups its senses under numbered etymologies
* writes it one level deeper ("=== Etîmolojî 1 ===" then "==== Lêker ===="). The depth is read off
* the heading rather than assumed, and comes back with it: a subsection of that section is one
* deeper again.
*
* lines covers the whole rest of the page from the language heading onward, not just this language's
* own section: callers rebuild the saved page out of it, and truncating here would drop every later
* language. Only the heading search itself is bounded, so a same-named heading in the next language
* can never be matched by mistake.
*/
function locateSection(pageContent, language, sectionType, sectionOrdinal) {
var headingNames = sections.headingNamesFor(sectionType);
if (!headingNames) {
throw new SubmitAbort("Navê beşê xelet e: " + JSON.stringify(sectionType));
}
var langRange = langSectionRange(pageContent, language);
var langSectionIndex = langRange.start;
var lines = pageContent.slice(langSectionIndex).split("\n");
var searchEndLine = pageContent.slice(langSectionIndex, langRange.end).split("\n").length;
// Every recognised heading name, so the ordinal counts headings of this kind and no others.
var namePattern = Object.keys(sections.sectionNames).concat([MANE_SECTION])
.map(escapeRegExp).join("|");
var headingRegex = new RegExp("^(={3,6})[ ]*(" + namePattern + ")\\s*(\\d+)?[ ]*\\1");
var posLineIndex;
var occurrence = 0;
var depth = 3;
for (posLineIndex = 0; posLineIndex < searchEndLine; posLineIndex++) {
var m = headingRegex.exec(lines[posLineIndex]);
var numbered = m ? 0 : numberedManeDepth(lines, posLineIndex);
var name = m ? m[2] : (numbered ? MANE_SECTION : null);
if (name && headingNames.indexOf(name) !== -1) {
occurrence++;
if (occurrence === sectionOrdinal) {
depth = m ? m[1].length : numbered;
break;
}
}
}
if (posLineIndex === searchEndLine) {
throw new SubmitAbort("Di navê beşê de xetayek heye. Mihtemelen çend beş bi vî navî hene.");
}
return { langSectionIndex: langSectionIndex, lines: lines, posLineIndex: posLineIndex, depth: depth };
}
// The "=" run for a heading depth, and a matcher for any heading at or above one (a smaller run is a
// bigger section, so "at or above" means "this deep or shallower").
function headingMarks(depth) {
return new Array(depth + 1).join("=");
}
function headingAtOrAbove(depth) {
return new RegExp("^={2," + depth + "}[^=]");
}
function headingAtDepth(depth) {
var marks = headingMarks(depth);
return new RegExp("^" + marks + "\\s*([^=]+?)\\s*" + marks + "\\s*$");
}
/**
* One heading found by its own text, and the lines around it.
*
* The section keys locateSection() knows cover the parts of speech; a heading like
* "=== Etîmolojî ===" that sits beside them rather than under one is matched by its own text
* instead, at any depth from "===" down. The ordinal counts headings carrying that same text within
* the language, which is what the DOM side counts too.
*
* bodyEnd is the next heading of any depth: a section's own text is what lies between its heading and
* the next one, so editing "=== Etîmolojî 1 ===" never reaches the parts of speech under it.
*/
function locateHeadingByText(pageContent, language, header, ordinal) {
var langRange = langSectionRange(pageContent, language);
var langSectionIndex = langRange.start;
var lines = pageContent.slice(langSectionIndex).split("\n");
var searchEndLine = pageContent.slice(langSectionIndex, langRange.end).split("\n").length;
var headingRegex = /^(={3,6})[ ]*(.+?)[ ]*\1\s*$/;
var nextHeading = headingAtOrAbove(6);
var occurrence = 0;
for (var i = 0; i < searchEndLine; i++) {
var m = headingRegex.exec(lines[i]);
if (!m || m[2] !== header) {
continue;
}
occurrence++;
if (occurrence !== (ordinal || 1)) {
continue;
}
var end = lines.length;
for (var j = i + 1; j < lines.length; j++) {
if (nextHeading.test(lines[j])) {
end = j;
break;
}
}
return {
langSectionIndex: langSectionIndex,
lines: lines,
headingLineIndex: i,
depth: m[1].length,
bodyStart: i + 1,
bodyEnd: end
};
}
throw new SubmitAbort(util.msg.sectionMissing + header);
}
/**
* The line index of one definition under a section heading.
*
* definitionLevel is the shape both gadgets already pass around: element 0 is the "type|ordinal"
* string locateSection() has resolved, and the rest are 0-based indices, one per nesting level, so
* ["Navdêr|1", 1, 0] is the first sub-sense of the second sense. Only "#" lines count -- "#:", "#*"
* and "##" belong to a sense rather than starting one.
*/
function findDefinitionLine(lines, posLineIndex, definitionLevel) {
var defIndex = -1;
var level = 1;
for (var i = posLineIndex; i < lines.length; i++) {
var m = /^(#+)[^*#:]/.exec(lines[i]);
if (m && m[1].length === level) {
defIndex++;
if (definitionLevel[level] === defIndex) {
if (level === definitionLevel.length - 1) {
return i;
}
level++;
defIndex = -1;
}
}
}
throw new SubmitAbort("Nikare rêza vê maneyê di metnê de bibîne.");
}
// Parameter names a headword template reads as another: Modul:rêziman declares "z" alias_of "c", so
// "z=m" and "c=m" are one gender. A numbered one maps the same way ("z2" is "c2").
var PARAM_ALIASES = { z: "c" };
function canonicalParamName(name) {
var trimmed = $.trim(name || "");
var m = /^([a-z]+)(\d*)$/i.exec(trimmed);
return m && Object.prototype.hasOwnProperty.call(PARAM_ALIASES, m[1])
? PARAM_ALIASES[m[1]] + m[2]
: trimmed;
}
// A named parameter of the first call in a line, read with the parser above rather than a pattern, so
// a "c=" inside a nested template is never taken for the call's own. Names match canonically, so
// asking for "c" also finds "z=". Empty when the line holds no closed call or the parameter is unset.
function callParamValue(text, name) {
var call = parseTemplateCalls($.trim(text || "")).calls[0];
if (!call) {
return "";
}
var values = splitCallParams(call.params).values;
var want = canonicalParamName(name);
var found = "";
Object.keys(values).forEach(function (key) {
if (!found && values[key] && canonicalParamName(key) === want) {
found = values[key];
}
});
return found;
}
module.exports = {
canonicalParamName: canonicalParamName,
callParamValue: callParamValue,
langHeaderSource: langHeaderSource,
langSectionRange: langSectionRange,
MANE_TAG_REGEX: MANE_TAG_REGEX,
locateSection: locateSection,
numberedGroupAbove: numberedGroupAbove,
locateHeadingByText: locateHeadingByText,
headingMarks: headingMarks,
headingAtOrAbove: headingAtOrAbove,
headingAtDepth: headingAtDepth,
findDefinitionLine: findDefinitionLine,
canonicalTemplateKey: canonicalTemplateKey,
aliasPattern: aliasPattern,
splitLeadingCalls: splitLeadingCalls,
splitTemplateParams: splitTemplateParams,
parseTemplateCalls: parseTemplateCalls,
splitCallParams: splitCallParams,
buildTemplateCall: buildTemplateCall,
buildHbnrCall: buildHbnrCall,
findHbnrCall: findHbnrCall,
buildImageLine: buildImageLine,
parseImageLine: parseImageLine,
captionForLang: captionForLang,
buildDundeLine: buildDundeLine,
tdText: tdText,
fetchTemplateData: fetchTemplateData,
templateDoc: templateDoc,
templateDocBatch: templateDocBatch,
templateDocNow: templateDocNow,
fetchTemplateDataBatch: fetchTemplateDataBatch,
shortDesc: shortDesc,
numberRepeatedPosHeadings: numberRepeatedPosHeadings,
parseWordListCall: parseWordListCall,
parseWordListCalls: parseWordListCalls,
namedParam: namedParam,
withNamedParam: withNamedParam,
buildWordListCall: buildWordListCall,
parseBulletWordList: parseBulletWordList
};
// </nowiki>
}());
92ug9k8y6u2czgpy61dhv68mtao7eni
Bikarhêner:Wikihez/Gadget-test/sections.js
2
1358904
6531943
6513958
2026-09-17T14:17:03Z
Wikihez
7344
POS conversion keeps the heading depth and replaces a numbered sense-group heading; numbered headings tagged with {{cure?}} count as Mane sections; + Îzah for Kurmancî senses
6531943
javascript
text/javascript
// <nowiki>
/*
* Page: [[MediaWiki:Gadget-wikt.core/sections.js]]
*/
(function () {
"use strict";
var startData = require("./data-sections.json");
// Inlined so the view path does not pull in util.js.
function capitalize(str) {
return str ? str[0].toUpperCase() + str.substr(1) : str;
}
// Recognized part-of-speech section headings, exported for the wizard, DefSideBoxes and add-examples.
// Section titles: posChoices' keys capitalised, plus a "Formeke Xê" for each POS that has forms, plus Biwêj.
var POS_KEYS = Object.keys(startData.posChoices);
var NO_FORM_HEADING = ["gotineke pêşiyan", "îfade"];
function hasFormHeading(pos) {
return NO_FORM_HEADING.indexOf(pos) === -1;
}
var sectionNames = {};
POS_KEYS.forEach(function (key) {
var title = capitalize(key);
sectionNames[title] = [title];
if (hasFormHeading(key)) {
sectionNames["Formeke " + key + "ê"] = ["Formeke " + key + "ê"];
}
});
sectionNames["Biwêj"] = ["Biwêj"];
// Page-level (per part-of-speech, "====" heading) subsections start.js can also build from scratch.
// "kind: wordlist" ones use {{kol3|lang|cure=Header|word1|word2}}; "kind: text" ones are a plain paragraph.
// Only steps with a confirmed wikitext format are included; insertion rank comes from SUBSECTION_ORDER below.
var PAGE_SUBSECTIONS = startData.pageSubsections;
// ---- section/language helpers (document-order based, immune to mobile's per-section DOM wrapping and
// MediaWiki's page-wide heading-id collisions) ----
// MediaWiki wraps each heading in a "mw-heading2/3/.../" div in current skin output.
function headingOuter($h) {
var $wrap = $h.closest(".mw-heading2, .mw-heading3, .mw-heading4, .mw-heading5, .mw-heading6");
return $wrap.length ? $wrap : $h;
}
// Section's language code: nearest preceding .besazimani[id] by document order, not sibling-walking.
function languageCodeForOl($ol) {
var $marks = $(".besazimani[id]").add($ol);
var ol = $ol.get(0);
var idx = $marks.index(ol);
for (var i = idx - 1; i >= 0; i--) {
var el = $marks.get(i);
if (el.id) {
return el.id;
}
}
return null;
}
// Pseudo section type for start.js's unclassified "=== Mane ===" / "=== Mane 2 ===" placeholder heading, so
// locateSection() (and everything built on it) can target one before a real POS has been chosen.
var MANE_SECTION = "Mane";
// A heading's depth, from its own tag or the wrapper the skin puts around it. 0 for anything that is
// not a heading.
function headingDepth($el) {
var m = /(?:^|\s)mw-heading([2-6])(?:\s|$)/.exec($el.attr("class") || "");
if (m) {
return Number(m[1]);
}
var tag = ($el.prop("tagName") || "").toLowerCase();
return /^h[2-6]$/.test(tag) ? Number(tag.slice(1)) : 0;
}
// Every part-of-speech heading on the page, at either depth it is written at: "===" on most entries,
// "====" where the senses are grouped under numbered etymologies. Called back with the heading, its
// section key and its depth.
function eachPosHeading(callback) {
$("h3, h4").each(function () {
var $h = $(this);
var key = sectionKeyOf($h);
if (key) {
callback($h, key, headingDepth($h) || 3);
}
});
}
// A heading's own text, with the pencil/convert-trigger this gadget injects into it filtered out --
// otherwise attaching one would change what sectionTypeOf() and the ordinal counter see.
function headingText($h) {
return $h.clone().find(".dsb-toolbar").remove().end().text().trim();
}
// An older layout heads each sense group with a number and a short gloss: "=== 2. dîn, bêhiş ===".
function isNumberedHeadingText(text) {
return /^\d+\.\s+\S/.test(text);
}
// "Mane", or a numbered group heading with {{cure?}} straight under it -- the same unsorted section under
// another name. {{cure?}} and its aliases render an anchor with data-id="bekatrez". Read once per heading
// and kept: a queued conversion's preview replaces that line, and the heading must not change kind.
function isManeHeading($h) {
var text = headingText($h);
if (/^Mane(\s+\d+)?$/.test(text)) {
return true;
}
if (!isNumberedHeadingText(text)) {
return false;
}
var known = $h.data("dsbNumberedMane");
if (typeof known === "boolean") {
return known;
}
var found = false;
for (var $cursor = headingOuter($h).next(); $cursor.length; $cursor = $cursor.next()) {
if (headingDepth($cursor) || $cursor.is("ol")) {
break;
}
if ($cursor.is("[data-id='bekatrez']") || $cursor.find("[data-id='bekatrez']").length) {
found = true;
break;
}
}
$h.data("dsbNumberedMane", found);
return found;
}
// Canonical section-type key for a heading, stripping a page author's own disambiguating trailing number.
function sectionTypeOf($h3) {
var text = headingText($h3);
if (sectionNames[text]) {
return text;
}
var stripped = text.replace(/\s*\d+$/, ""); // \s* not \s+: locateSection() also accepts "Navdêr2"
return sectionNames[stripped] ? stripped : null;
}
// A heading's section key: its part of speech, or the wizard's "Mane" placeholder, which is a real
// section holding real definitions but not a part of speech. Everything that locates a section by
// heading goes through here, so the placeholder is never half-supported.
function sectionKeyOf($h3) {
return sectionTypeOf($h3) || (isManeHeading($h3) ? MANE_SECTION : null);
}
// The heading texts a section key accepts, or null when the key names no section at all. What the
// wikitext locator matches "=== ... ===" against.
function headingNamesFor(sectionType) {
return sectionNames[sectionType] ||
(sectionType === MANE_SECTION ? [MANE_SECTION] : null);
}
// This heading's 1-based position among same-type headings within its own language block.
// How many sections of one type this language has. A summary only needs to number a section when
// there is more than one of its kind -- "Navdêr" alone is unambiguous, "Navdêr" out of two is not.
// Same walk as sectionOrdinalWithinLanguage below: language marks reset the count, headings add to it.
function sectionTypeCountWithinLanguage($h3) {
var h3El = $h3.get(0);
var wanted = sectionKeyOf($h3) || headingText($h3);
var $all = $(".besazimani[id]").add($("h3, h4"));
var count = 0;
var seen = false;
$all.each(function () {
if ($(this).is(".besazimani[id]")) {
// A new language starts: whatever was counted belonged to the previous one.
if (seen) {
return false;
}
count = 0;
return;
}
var type = sectionKeyOf($(this)) || headingText($(this));
if (type === wanted) {
count++;
}
if (this === h3El) {
seen = true;
}
});
return count;
}
function sectionOrdinalWithinLanguage($h3) {
var h3El = $h3.get(0);
var $all = $(".besazimani[id]").add($("h3, h4"));
var counts = {};
var result = 1;
$all.each(function () {
if ($(this).is(".besazimani[id]")) {
counts = {};
return;
}
var type = sectionKeyOf($(this)) || headingText($(this));
counts[type] = (counts[type] || 0) + 1;
if (this === h3El) {
result = counts[type];
return false;
}
});
return result;
}
// This heading's 1-based position among headings carrying the same text in its own language block,
// at any depth. What locateHeadingByText() counts in the wikitext, so the two agree.
function headingTextOrdinalWithinLanguage($h) {
var el = $h.get(0);
var wanted = headingText($h);
var $all = $(".besazimani[id]").add($("h3, h4, h5, h6"));
var count = 0;
var result = 1;
$all.each(function () {
if ($(this).is(".besazimani[id]")) {
count = 0;
return;
}
if (headingText($(this)) === wanted) {
count++;
}
if (this === el) {
result = count;
return false;
}
});
return result;
}
// A "form of"-template-generated <li> wraps its own content; its nested <ol> isn't real "##" senses.
function isFormOfWrapper($li) {
return $li.children(".form-of-definition").length > 0;
}
// Recurse into a <li>'s nested <ol> unless it's a form-of wrapper.
function eachLeafDefinition($ol, fn) {
$ol.children("li").each(function () {
var $li = $(this);
var $childOl = $li.children("ol");
if ($childOl.length && !isFormOfWrapper($li)) {
$childOl.each(function () {
eachLeafDefinition($(this), fn);
});
} else {
fn($li);
}
});
}
/**
* Whether the gadgets should run on this page at all: an entry, a reconstruction (ns 108), or a
* sandbox -- a user's own /ceribandin or /test subpage, and the project's own [[WF:ceribandin]].
*
* Shared because dsb.js and start-launch.js each gate on it, and a sandbox added to one but not the
* other is a gadget that half-works there.
*/
function isEntryPage() {
var ns = mw.config.get("wgNamespaceNumber");
var page = mw.config.get("wgPageName") || "";
if (!ns || ns === 108) {
return true;
}
// A sandbox and anything under it: [[Bikarhêner:X/ceribandin]] and its own subpages.
if (ns === 2 && /\/(ceribandin|test)(\/|$)/.test(page)) {
return true;
}
// The project namespace has one sandbox, not one per page, so it is matched by name.
return ns === 4 && /^[^:]+:(ceribandin|test)(\/|$)/i.test(page);
}
// The examples and quotations drawn under one definition, in page order: a "#:" line is a <dd> and a "#*"
// line an <li>. A <dd> counts when it is a usage example or a plain italic sentence, never a word list;
// the "no example yet" placeholder and add-examples' own form row never count. While DefSideBoxes shows a
// queued change, the sense is drawn inside its preview, and the examples are read from there.
function exampleItemsOf($li) {
var $root = $li.children(".dsb-sense-preview").last();
if (!$root.length) {
$root = $li;
}
return $root.children("dl").children("dd").add($root.children("ul").children("li")).filter(function () {
var $item = $(this);
if (this.style.display === "none" || $item.hasClass("mw-empty-elt")) {
return false;
}
if ($item.is("li")) {
return !$item.find(".benimune").length && !$item.children(".add-example-form-title").length;
}
if ($item.children(".h-usage-example").length) {
return true;
}
var first = this.firstChild;
while (first && first.nodeType === 3 && !$.trim(first.nodeValue)) {
first = first.nextSibling;
}
return !!first && first.nodeType === 1 && first.tagName === "I";
});
}
module.exports = {
// posChoices and pageSubsections: wikt.edit hands these on as part of its own data.
data: startData,
isEntryPage: isEntryPage,
hasFormHeading: hasFormHeading,
sectionNames: sectionNames,
MANE_SECTION: MANE_SECTION,
PAGE_SUBSECTIONS: PAGE_SUBSECTIONS,
headingOuter: headingOuter,
headingText: headingText,
isManeHeading: isManeHeading,
isNumberedHeadingText: isNumberedHeadingText,
sectionTypeOf: sectionTypeOf,
sectionKeyOf: sectionKeyOf,
headingDepth: headingDepth,
eachPosHeading: eachPosHeading,
headingNamesFor: headingNamesFor,
sectionOrdinalWithinLanguage: sectionOrdinalWithinLanguage,
headingTextOrdinalWithinLanguage: headingTextOrdinalWithinLanguage,
sectionTypeCountWithinLanguage: sectionTypeCountWithinLanguage,
languageCodeForOl: languageCodeForOl,
isFormOfWrapper: isFormOfWrapper,
exampleItemsOf: exampleItemsOf,
eachLeafDefinition: eachLeafDefinition
};
}());
// </nowiki>
1fcj0v4xpgwr3aukbgh0j7ruoob57sb
Bikarhêner:Wikihez/Gadget-test/dsb-panels.js
2
1358909
6531918
6513964
2026-09-17T12:46:53Z
Wikihez
7344
Sense previews ignore queued section renames when locating the sense; a queued POS conversion renders its new headword line
6531918
javascript
text/javascript
// <nowiki>
/*
* Page: [[MediaWiki:Gadget-wikt.dsb-panels.js]]
*
* Everything DefSideBoxes shows once you click: the per-sense advanced panel, the subsection editors,
* the part-of-speech picker, the template dialogs, the preview and the queued save.
*
* Loaded on demand by wikt.dsb, never on a plain page view. It exports only what that half calls.
*/
(function () {
"use strict";
var sections = require("ext.gadget.wikt.core").sections;
var edit = require("ext.gadget.wikt.edit");
var cite = require("ext.gadget.wikt.cite");
var util = edit.util;
var posparams = edit.posparams;
var wt = edit.wt;
var startData = edit.data;
var langs = edit.langs;
var escapeRegExp = util.escapeRegExp;
// Local names for the shared wikitext layer, so every call site below reads as it always did.
var langHeaderSource = wt.langHeaderSource;
var canonicalTemplateKey = wt.canonicalTemplateKey;
var aliasPattern = wt.aliasPattern;
var splitLeadingCalls = wt.splitLeadingCalls;
var splitTemplateParams = wt.splitTemplateParams;
var parseTemplateCalls = wt.parseTemplateCalls;
var splitCallParams = wt.splitCallParams;
var buildTemplateCall = wt.buildTemplateCall;
var tdText = wt.tdText;
var fetchTemplateData = wt.fetchTemplateData;
var parseWordListCall = wt.parseWordListCall;
var parseWordListCalls = wt.parseWordListCalls;
var namedParam = wt.namedParam;
var withNamedParam = wt.withNamedParam;
var buildWordListCall = wt.buildWordListCall;
var parseBulletWordList = wt.parseBulletWordList;
var sectionNames = sections.sectionNames;
var MANE_SECTION = sections.MANE_SECTION;
var PAGE_SUBSECTIONS = sections.PAGE_SUBSECTIONS;
var headingOuter = sections.headingOuter;
var headingText = sections.headingText;
var isManeHeading = sections.isManeHeading;
var sectionTypeOf = sections.sectionTypeOf;
var sectionOrdinalWithinLanguage = sections.sectionOrdinalWithinLanguage;
var isFormOfWrapper = sections.isFormOfWrapper;
var eachLeafDefinition = sections.eachLeafDefinition;
var api = new mw.Api();
// metadata[lang].g, minus the grammatical-number codes start.js filters out with isNumberCode(), rendered
// the way its genderLabel() does ("m-p" -> "mê (pirjimar)").
// start.js asks transitivity and structural form as two questions and joins them as "<form>-<base>", so
// "gerguhêz" alone is "gh" and "xwerû" + "gerguhêz" is "x-gh".
// Template call convertSectionToPos() drops under a newly-chosen POS heading, e.g. "Hoker" -> "{{hoker|ku}}".
// A "Formeke X" (form-of) heading uses the unchanged base pos template. Doubles as the list of template
// calls a conversion may find under the OLD heading and replace, rather than leave stacked beside the new one.
// A section's template call. "Formeke Xê" uses X's own template with form=erê. Biwêj is recognised so a
// conversion can replace its {{biwêj}}, but never written: no new ones.
function posTemplateFor(header) {
var form = /^Formeke (.+)ê$/.exec(header || "");
if (form && startData.posChoices[form[1]] && sections.hasFormHeading(form[1])) {
return { name: form[1], params: ["form=erê"] };
}
var key = util.decapitalize(header || "");
return startData.posChoices[key] ? { name: key, params: [] } : null;
}
// The "needs a POS" maintenance tag under a placeholder "Mane" heading, and every known redirect/alias name
// it's also called under on real pages -- convertSectionToPos() drops whichever one is actually present.
// Built from data-main.json's maneTags, so the alias list sits with every other table. A space inside a tag
// name matches any run of whitespace, as the hand-written pattern did.
var MANE_TAG_REGEX = new RegExp(
"^\\{\\{\\s*(?:" + startData.maneTags.map(function (tag) {
return escapeRegExp(tag).replace(/ +/g, "\\s+");
}).join("|") + ")\\s*(?:\\|[^{}]*)?\\}\\}\\s*$", "i");
// The six per-sense groups start.js writes as "#: {{tpl|lang|word1|word2}}" sub-lines under a "#" definition.
// Array order controls where a new group line lands relative to existing ones. Every group lives behind the
// quick row's "Hevmane û Bêhtir" dropdown; only the gloss and Nimûne/jêgirtin links sit in the row itself.
var SENSE_GROUPS = startData.senseGroups;
// The column word-list template new sections are written with; the rest of wordlistTemplates are
// spellings the parser also accepts.
var WORDLIST_TEMPLATE = startData.wordlistTemplates[0];
// ---- {{werger-ser}} / {{werger-bin}}: one translation box ----
// The pair brackets the rows; the translation editor gadget writes the rows themselves onto the
// rendered page, so this only ever builds (and reads back) the opening call.
// Parameters, from [[Şablon:werger-ser]]: 1 = the sense shown in the box head, 2 = a note in
// parentheses after it, id = anchor override (defaults to 1), stûn = column count, firehî = column
// width (default 26em), bgcolor, valign (default top), align (default left).
var WERGER_NAMED = ["id", "stûn", "firehî", "bgcolor", "valign", "align"];
function buildWergerBlock(params) {
var parts = ["werger-ser"];
// Positional slots: 2 can only be reached past 1, so an empty 1 still has to be written.
if (params["1"] || params["2"]) {
parts.push(params["1"] || "");
}
if (params["2"]) {
parts.push(params["2"]);
}
WERGER_NAMED.forEach(function (key) {
if (params[key]) {
parts.push(key + "=" + params[key]);
}
});
return "{{" + parts.join("|") + "}}\n{{werger-bin}}";
}
// The opening call of the first block in `text`, as the same {1, 2, id, ...} object. Empty object when
// there is none, so a half-typed draft restores as an empty form rather than throwing.
function parseWergerBlock(text) {
var out = {};
var body = wergerSerBody(String(text || ""));
if (!body) {
return out;
}
var positional = 0;
splitTemplateArgs(body).forEach(function (arg) {
var eq = arg.indexOf("=");
var name = eq === -1 ? "" : $.trim(arg.slice(0, eq));
if (eq !== -1 && WERGER_NAMED.indexOf(name) !== -1) {
out[name] = $.trim(arg.slice(eq + 1));
} else {
out[String(++positional)] = $.trim(arg);
}
});
return out;
}
// Everything after "{{werger-ser|" up to the "}}" that closes it, or "" when there is no call or it
// takes no parameters. Scans for the matching close rather than the first one: a gloss can hold a
// template of its own, and a lazy regex would stop inside it.
function wergerSerBody(text) {
var m = /\{\{\s*werger-ser\s*(?=[|}])/.exec(text);
if (!m) {
return "";
}
var depth = 1;
var start = m.index + m[0].length;
for (var i = start; i < text.length - 1; i++) {
var two = text.substr(i, 2);
if (two === "{{") {
depth++; i++;
} else if (two === "}}") {
depth--; i++;
if (!depth) {
return text.charAt(start) === "|" ? text.slice(start + 1, i - 1) : "";
}
}
}
return "";
}
// Splits on "|" at brace/bracket depth 0, so a value containing a template or a link stays whole.
function splitTemplateArgs(body) {
var args = [];
var depth = 0;
var current = "";
for (var i = 0; i < body.length; i++) {
var two = body.substr(i, 2);
if (two === "{{" || two === "[[") {
depth++; current += two; i++; continue;
}
if (two === "}}" || two === "]]") {
depth--; current += two; i++; continue;
}
if (body[i] === "|" && depth <= 0) {
args.push(current); current = ""; continue;
}
current += body[i];
}
args.push(current);
return args;
}
// Canonical ku.wiktionary "====" subsection order, used only as insertPageSubsection()'s insertion rank
// (see subsectionRankForHeader()). Wider than PAGE_SUBSECTIONS: it also covers subsections the gadget
// cannot create but still has to order around.
var SUBSECTION_ORDER = startData.subsectionOrder;
// ---- searchable <select> ----------------------------------------------------------------------------
// Same widget start.js gets from its util.js wrapNativeSelect(): the native <select> stays as the value
// holder (so .val() and "change" handlers keep working) but is hidden behind a text input that filters an
// overlay list. Ported rather than required() because util.js is a file inside start.js's package module,
// which another gadget cannot reach; the template-row decoration start.js uses is left out.
// util.js's wrapNativeSelect(): the native <select> stays the value holder behind a filtering text input.
// It reads its own placeholder off data-placeholder, and its wiktku-* styles ship in start.css, so both
// gadgets get the identical widget instead of two lookalikes.
function makeSearchable($select, placeholder) {
$select.attr("data-placeholder", placeholder || "Lê bigere…");
return util.wrapNativeSelect($select);
}
// ---- small shared utilities ----
// The readable half of an mw.Api rejection.
var apiErrorMessage = util.apiErrorMessage;
// Thrown from inside an edit() transform to bail out with a specific message.
var SubmitAbort = util.SubmitAbort;
// A gloss is one field on the page: the sense, then {{îzah|…}} explaining it when there is one.
function joinGloss(mane, izah) {
return izah ? $.trim(mane + " {{îzah|" + izah + "}}") : $.trim(mane);
}
// The two halves are linked separately, never joined first: linkify leaves template calls alone by
// design, so anything inside {{îzah|…}} would never be looked at.
//
// A half is skipped when there is nothing in it worth looking up, and -- when onlyUnlinked is set --
// when it already carries a link. Someone who has linked a word by hand has made that half theirs, and
// a save must not go on linking the rest of it behind their back; "Oto-lînk bike" leaves the flag off
// and still links on demand.
function linkifyGlossParts(mane, izah, done, onlyUnlinked) {
function needsLinking(text) {
if (!$.trim(text) || !util.hasLinkableText(text)) {
return false;
}
return !(onlyUnlinked && text.indexOf("[[") !== -1);
}
function step(text, next) {
if (!needsLinking(text)) {
next(text, null);
return;
}
util.linkifyText(text, function (linked, error) {
// An empty answer would wipe what the user typed, so only real text is taken.
next(typeof linked === "string" && $.trim(linked) ? $.trim(linked) : text, error || null);
});
}
step(mane, function (linkedMane, maneError) {
step(izah, function (linkedIzah, izahError) {
done(linkedMane, linkedIzah, maneError || izahError);
});
});
}
// Runs transformFn(content) inside mw.Api().edit()'s own transform callback so one fetch covers both the
// edit-conflict check and the content, preventing a stale overwrite of someone else's edit.
//
// No badtoken retry here on purpose: edit() posts through postWithEditToken -> postWithToken, which
// already throws the cached token away and repeats the request once when the wiki says "badtoken".
// A second retry of our own would only double it.
function editPage(summary, transformFn, onSuccess, onError) {
api.edit(mw.config.get("wgPageName"), function (revision) {
return { text: transformFn(revision.content), summary: summary };
}).then(onSuccess, function (codeOrAbort, result) {
if (codeOrAbort instanceof SubmitAbort) {
onError(codeOrAbort.message);
} else {
onError(util.msg.saveFailed + apiErrorMessage(codeOrAbort, result));
}
});
}
// ---- rendered preview: wikitext through the parse API, so a preview shows what the templates
// actually produce rather than a plain list standing in for them ----
// Same text is usually asked for twice (open the preview, close it, open it again), so the promise is
// kept rather than the call repeated.
var renderCache = {};
/**
* Renders wikitext as this page would render it. Resolves with an HTML string.
*
* pst: 1 applies pre-save transform, so ~~~~ and subst: behave as they will once saved. The
* modulestyles/modules the parse reports are loaded before resolving: without them TemplateStyles is
* missing and the preview would show the right markup in the wrong clothes.
*/
function renderWikitext(wikitext) {
if (renderCache[wikitext]) {
return renderCache[wikitext];
}
renderCache[wikitext] = api.post({
action: "parse",
format: "json",
formatversion: 2,
title: mw.config.get("wgPageName"),
text: wikitext,
contentmodel: "wikitext",
prop: "text|modules|jsconfigvars",
pst: 1,
disablelimitreport: 1,
disableeditsection: 1,
disabletoc: 1
}).then(function (data) {
var parsed = (data && data.parse) || {};
if (parsed.modulestyles && parsed.modulestyles.length) {
mw.loader.load(parsed.modulestyles);
}
if (parsed.modules && parsed.modules.length) {
mw.loader.load(parsed.modules);
}
return parsed.text || "";
}, function () {
// A failed render must not be remembered, or the next attempt gets the rejection back.
delete renderCache[wikitext];
return $.Deferred().reject().promise();
});
return renderCache[wikitext];
}
/**
* Puts rendered wikitext into $target, replacing whatever placeholder is there.
*
* The site's own gadgets are told about it through the "wikipage.content" hook, the same way
* MediaWiki announces real content, so collapsible lists and the like behave in the preview too.
*/
// MediaWiki warns when the hook is fired on content that is not in the document, and a gadget that
// listens to it can do nothing useful with nodes nobody can see.
function announceContent($el) {
if ($el.closest("html").length) {
mw.hook("wikipage.content").fire($el);
}
}
function fillWithRendered($target, wikitext, onFail) {
return renderWikitext(wikitext).then(function (html) {
if (!$target.closest("body").length) {
return; // the preview was dropped while the parse was in flight
}
$target.empty().append($("<div class='mw-parser-output'></div>").html(html));
announceContent($target);
}, function () {
if (onFail) onFail();
});
}
/**
* Puts a rendered sense in place of the one on the page, keeping this gadget's own furniture (the
* pencil's toolbar, the quick row, any open panel) where it is. The page's own nodes are detached
* rather than dropped, so the returned function can put them straight back when the change is
* cancelled or superseded.
*
* Nothing labels the result: the change is shown by the sense simply reading the way it will read,
* and the queue box is what says nothing is saved yet.
*/
function replaceSenseInPlace($li, item) {
var html = item.html;
var $anchor = $li.children(".dsb-toolbar").last();
// class/id/data-* off the parsed <li>, so a queued maneid shows its pill here exactly as it will
// once saved. The old values are kept for the restore below.
var node = $li.get(0);
var before = {};
$.each(node.attributes, function (_, attr) {
before[attr.name] = attr.value;
});
$.each(item.attrs, function (name, value) {
if (name !== "class") {
$li.attr(name, value);
}
});
var addedClasses = (item.attrs["class"] || "").split(/\s+/).filter(function (c) {
return c && !$li.hasClass(c);
});
$li.addClass(addedClasses.join(" "));
// add-examples hangs its own <ul> off the sense and opens its form as that <ul>'s next sibling.
// Detaching those leaves the form to open into a tree nobody can see -- "+ Nimune/jegirtin" takes
// the click and shows nothing -- so they stay where they are. Only the rows already in that <ul>
// are hidden, since the preview draws the examples itself; the tool's own title line stays.
var $keep = $li.children("ul, .add-example-form");
var $hiddenRows = $keep.filter("ul").children("li").filter(function () {
return !$(this).find(".add-example-form-title").length;
});
$hiddenRows.addClass("dsb-preview-hidden");
var originals = $li.contents().filter(function () {
return !$(this).is(".dsb-toolbar, .dsb-quick-row, .dsb-subsection-panel, .dsb-preview-block, .dsb-sense-preview");
}).not($keep).get();
// Where the sense's own nodes sat, so cancelling puts them back there rather than above the quick
// row -- the row belongs between the sense and its examples.
var backAfter = originals.length ? originals[0].previousSibling : null;
var $rendered = $("<span class='dsb-sense-preview'></span>").html(html);
if ($anchor.length) {
$anchor.after($rendered);
} else {
$li.prepend($rendered);
}
$(originals).detach();
announceContent($rendered);
// The id pill is drawn by the same code a page load uses, so a queued maneid looks like a saved
// one rather than like something this gadget invented.
if (window.wikt && wikt.showIDs && wikt.showIDs.process) {
wikt.showIDs.process($li);
}
// dsb.js puts the example pencils back on the redrawn examples. The originals keep theirs.
$li.triggerHandler("dsbSenseRedrawn");
return function () {
$hiddenRows.removeClass("dsb-preview-hidden");
$li.removeClass(addedClasses.join(" "));
$.each(item.attrs, function (name) {
if (name === "class") {
return;
}
if (Object.prototype.hasOwnProperty.call(before, name)) {
$li.attr(name, before[name]);
} else {
$li.removeAttr(name);
}
});
$li.children(".senseid-displayed, .etymid-displayed, .etymonid-displayed").remove();
if (backAfter && backAfter.parentNode === node) {
$(backAfter).after(originals);
} else if ($anchor.length) {
$anchor.after(originals);
} else {
$li.prepend(originals);
}
$rendered.remove();
};
}
/**
* The <li> out of a parsed "#" block, and the attributes it carries.
*
* The parse wraps it in its own <ol>, which must not reach the page: the sense already has a number
* from the list it lives in. A leading {{maneid}} also makes the parser emit an empty
* "li.mw-empty-elt" ahead of the real one, so the first <li> is not necessarily the sense.
*
* The attributes matter as much as the content: {{maneid|ku|x}} renders as class="senseid"
* data-id="x" on the <li> itself, which is what wikt.showids draws its pill from.
*/
function parsedSenseItem(html, depth) {
var $item = $("<div></div>").html(html).find("ol > li").filter(function () {
return !$(this).hasClass("mw-empty-elt");
}).first();
// A sub-sense parsed on its own comes back one <ol> deeper per "#": "##" is a list inside a list.
// Descend as far as the sense's own depth, or its number would be drawn inside a second list --
// "2." with a "1." under it.
for (var level = 1; level < (depth || 1) && $item.length; level++) {
var $inner = $item.children("ol").children("li").filter(function () {
return !$(this).hasClass("mw-empty-elt");
}).first();
if (!$inner.length) {
break;
}
$item = $inner;
}
if (!$item.length) {
return { html: html, attrs: {} };
}
var attrs = {};
$.each($item.get(0).attributes, function (_, attr) {
attrs[attr.name] = attr.value;
});
return { html: $item.html(), attrs: attrs };
}
// This gadget's own save queue + floating widget, separate from the shared Gadget-Editor queue.
// Each entry keeps its own transformFn so saveQueueNow() can re-run all of them against one fresh fetch.
var dsbQueue = {
// [{key, summary, transformFn, runLast, removePreview}]; transformFn: baseContent -> {content, warnings}.
// removePreview lives on the entry so replacing one entry takes its own live DOM preview with it.
entries: []
};
var $queueBox = null;
function dropPreview(entry) {
if (typeof entry.removePreview === "function") {
try {
entry.removePreview();
} catch (e) { /* a preview failing to clean up should never block the queue itself */ }
entry.removePreview = null;
}
}
function clearAllPreviews() {
dsbQueue.entries.forEach(dropPreview);
}
function clearQueue() {
clearAllPreviews();
dsbQueue.entries = [];
if ($queueBox) {
$queueBox.$list.empty();
$queueBox.$box.hide();
}
}
// Closes every editor UI on the page. Each open box's own "x" clears its getter, restores its picker
// link and updates the counter, so those are triggered rather than repeating that work here; hiding a
// row alone would leave its fields populated and the next pencil click would reopen them half-filled.
function closeAllPanels() {
$(".dsb-open-boxes .dsb-box-remove").trigger("click");
$(".dsb-quick-note").hide().text("");
$(".dsb-more-menu").hide();
$(".dsb-subsection-panel, .dsb-convert-pos-panel").hide();
// dsb.js owns each pencil's pinned state; tell it rather than hiding behind its back, or the next
// click on the pencil would only toggle "pinned" off and appear to do nothing.
$(".dsb-quick-row").trigger("dsbCancel");
}
function ensureQueueBox() {
if ($queueBox) {
return $queueBox;
}
var $box = $("<div class='dsb-queue-box'></div>").hide();
var $close = $("<a href='#' class='dsb-queue-close' title='Veşêre'>×</a>");
var $title = $("<div class='dsb-queue-title'></div>").text("Guhartinên li benda qeydkirinê");
var $list = $("<ul class='dsb-queue-list'></ul>");
var $empty = $("<div class='dsb-queue-empty'></div>").text(
"Guhartinên te dê li vir werin rêzkirin. Heta ku tu pêl «Guhartinan qeyd bike» nekî, "
+ "tiştek li ser rûpelê nayê weşandin."
);
var $status = $("<div class='dsb-queue-status'></div>");
var $save = $("<button type='button' class='dsb-queue-save'>Guhartinan qeyd bike</button>");
var $cancel = $("<button type='button' class='dsb-queue-cancel'></button>").text(util.msg.cancel);
var $actions = $("<div class='dsb-queue-actions'></div>").append($save, $cancel);
$close.on("click", function (e) {
e.preventDefault();
$box.hide();
});
$cancel.on("click", function () {
clearQueue();
closeAllPanels();
});
$save.on("click", function () {
saveQueueNow();
});
$box.append($close, $title, $empty, $list, $actions, $status);
$(document.body).append($box);
$queueBox = { $box: $box, $list: $list, $empty: $empty, $status: $status, $save: $save, $cancel: $cancel };
return $queueBox;
}
// Takes one change out of the queue and puts the page back as it was before it, its in-place preview
// included. The queue is the only record of what "Guhartinan qeyd bike" will write, so every line in it
// has to be removable on its own -- closing the panel that made a change does not unmake it.
// Each preview draws the page the whole queue would save, so one entry leaving changes what all the
// others should show. They are taken down and drawn again from what is left.
function redrawPreviews() {
dsbQueue.entries.forEach(function (entry) {
if (typeof entry.previewFn !== "function") {
return;
}
dropPreview(entry);
try {
var removePreview = entry.previewFn();
if (typeof removePreview === "function") {
entry.removePreview = removePreview;
}
} catch (e) { /* a broken preview never stops the queue */ }
});
}
function dropQueueEntry(entry) {
var at = dsbQueue.entries.indexOf(entry);
if (at === -1) {
return;
}
dropPreview(entry);
dsbQueue.entries.splice(at, 1);
redrawPreviews();
renderQueueBox();
hideQueueIfIdle();
}
function renderQueueBox() {
var box = ensureQueueBox();
box.$list.empty();
dsbQueue.entries.forEach(function (entry) {
var $drop = $("<button type='button' class='dsb-queue-drop'>×</button>")
.attr("title", "Vê guhartinê ji rêzê derxe");
$drop.on("click", function () {
dropQueueEntry(entry);
});
box.$list.append($("<li></li>").text(entry.summary).append($drop));
});
box.$empty.toggle(!dsbQueue.entries.length);
box.$status.removeClass("dsb-status-ok dsb-status-error").text("");
box.$save.prop("disabled", !dsbQueue.entries.length);
box.$box.show();
}
// Shown the moment a panel opens, empty, so it is clear before anything is typed that a change waits
// for "Guhartinan qeyd bike" rather than publishing itself.
function openQueueBox() {
renderQueueBox();
}
// Nothing queued and no form left open means nothing to save, so the box goes away rather than
// lingering on an empty state.
function hideQueueIfIdle() {
if (dsbQueue.entries.length || !$queueBox) {
return;
}
if ($(".dsb-quick-row:visible, .dsb-subsection-panel:visible, .dsb-convert-pos-panel:visible").length) {
return;
}
$queueBox.$box.hide();
}
// Asks core's postEdit module to show its toast on the next load (dsb.js loads it there), then reloads.
// Loaded here rather than declared as a dependency, so a reload happens even if it fails.
function reloadWithPostEditNotice() {
mw.loader.using("mediawiki.action.view.postEdit").then(function (require) {
require("mediawiki.action.view.postEdit").fireHookOnPageReload("saved");
}).always(function () {
location.reload();
});
}
// A transform returns either the new page text or {content, warnings}; both shapes are accepted.
// One that returns neither stops the save, named, rather than writing a broken page.
function transformedContent(entry, pageContent) {
var result = entry.transformFn(pageContent);
var next = typeof result === "string" ? result : (result && result.content);
if (typeof next !== "string") {
throw new SubmitAbort("Guhartin nehate sepandin: " + entry.summary);
}
return next;
}
// Queue order, except that runLast entries move to the end: converting "=== Mane ===" into a real POS
// heading renames the very heading every other queued change locates itself by.
function orderedEntries() {
var normal = [];
var last = [];
dsbQueue.entries.forEach(function (entry) {
(entry.runLast ? last : normal).push(entry);
});
return normal.concat(last);
}
/**
* The pending page as previews and example lookups need it: every queued transform, in save order,
* except the runLast section renames. Those callers find their section under the heading it has on
* the page now, which a queued "Mane" -> "Navdêr" would take away, and a rename leaves the senses as
* they are. An entry that cannot apply is skipped -- the save reports that properly, and a preview
* must not shout.
*/
function queuedContent(pageContent) {
var content = pageContent;
orderedEntries().filter(function (entry) {
return !entry.runLast;
}).forEach(function (entry) {
try {
content = transformedContent(entry, content);
} catch (e) {
// Left out of the preview; the save reports it properly. Named, so a change that fails to
// draw is distinguishable from one with nothing to draw.
mw.log.warn("wikt.dsb: pêşdîtin bêyî vê guhartinê hate xêzkirin — " + entry.summary, e);
}
});
return content;
}
/**
* Redraws one sense where it sits, from the page the queue would save -- so a queued gloss edit, a
* queued example and everything else pending on that sense are shown together, rendered by the parser
* rather than described in a box. Returns the queue's own "take the preview down" function.
*
* One preview per sense: a second queued change takes the first drawing down and draws the queue.
*/
function previewSense($li, language, sectionType, sectionOrdinal, definitionLevel) {
var cancelled = false;
var restore = null;
function undo() {
cancelled = true;
if (restore) {
restore();
restore = null;
}
if ($li.data("dsbPreview") === undo) {
$li.removeData("dsbPreview");
}
}
var previous = $li.data("dsbPreview");
if (typeof previous === "function") {
previous();
}
$li.data("dsbPreview", undo);
fetchPageContent().then(function (pageContent) {
if (cancelled) {
return;
}
var wikitext;
try {
wikitext = senseBlockWikitext(queuedContent(pageContent), language, sectionType,
sectionOrdinal, definitionLevel);
} catch (e) {
return;
}
renderWikitext(wikitext).then(function (html) {
if (cancelled || $li.data("dsbPreview") !== undo || !$li.closest("body").length) {
return;
}
restore = replaceSenseInPlace($li, parsedSenseItem(html, definitionLevel.length - 1));
});
});
return undo;
}
// The real save: re-runs every queued transformFn, in order, against one freshly-fetched revision and
// posts the result as a single edit with a combined summary.
function saveQueueNow() {
if (!dsbQueue.entries.length) {
return;
}
var box = ensureQueueBox();
box.$save.prop("disabled", true);
box.$cancel.prop("disabled", true);
box.$status.removeClass("dsb-status-ok dsb-status-error").text("Tê qeydkirin…");
var entries = orderedEntries();
// Grouped by place, in the order the places were first queued: several changes to one section read
// as "Kurmancî/Navdêr 2: a, b, c" rather than repeating the section name in front of each of them.
var order = [];
var byPlace = {};
entries.forEach(function (entry) {
if (!Object.prototype.hasOwnProperty.call(byPlace, entry.place)) {
byPlace[entry.place] = [];
order.push(entry.place);
}
byPlace[entry.place].push(entry.action);
});
var combinedSummary = order.map(function (place) {
var actions = byPlace[place].join(", ");
return place ? place + ": " + actions : actions;
}).join("; ") + " (DefSideBoxes)";
editPage(
combinedSummary,
function (pageContent) {
var content = pageContent;
entries.forEach(function (entry) {
content = transformedContent(entry, content);
});
return content;
},
function (data) {
forgetPageContent();
clearAllPreviews();
dsbQueue.entries = [];
box.$list.empty();
box.$status
.removeClass("dsb-status-error")
.addClass("dsb-status-ok")
.text("✓ Hemû guhartin hatin qeydkirin.");
box.$save.prop("disabled", true);
box.$cancel.prop("disabled", false);
// A real edit reloads the page and gets MediaWiki's own "Guhartina te hat weşandin." toast, the
// one a normal save shows. A null edit changed nothing, so the page stays as it is.
if (data && data.newrevid) {
reloadWithPostEditNotice();
}
},
function (message) {
box.$save.prop("disabled", false);
box.$cancel.prop("disabled", false);
box.$status.removeClass("dsb-status-ok").addClass("dsb-status-error").text(message);
}
);
}
// "Kurmancî/Navdêr" -- where a queued change lands, for its summary. The language name comes from the
// shipped list; an unknown code is left as the code itself.
// "Kurmancî/Navdêr", or "Kurmancî/Navdêr 2" where the language has more than one section of that
// type. Without the number a summary on a page with two Navdêr sections named neither of them.
// Numbered only when it disambiguates: on the usual page "Navdêr 1" would just be noise.
function placeLabel(language, sectionType, $h3) {
return langLabel(language) + "/" + sectionLabel(sectionType, $h3);
}
// Bilêvkirin sits above the parts of speech and belongs to the whole language, so naming the section
// it was opened from would say something untrue about what the edit touches.
function subsectionPlaceLabel(subsection, language, sectionType, $h3) {
if (subsection && subsection.free) {
return langLabel(language) + "/" + subsection.header;
}
return subsection && subsection.place === "pron"
? langLabel(language)
: placeLabel(language, sectionType, $h3);
}
function sectionLabel(sectionType, $h3) {
if ($h3 && $h3.length && sections.sectionTypeCountWithinLanguage($h3) > 1) {
return sectionType + " " + sections.sectionOrdinalWithinLanguage($h3);
}
return sectionType;
}
// Capitalised because it opens the summary; Kurdish otherwise writes language names lower-case.
function langLabel(language) {
var names = langs.names();
return util.capitalize(names[language] || language);
}
// The parts of a sense the form actually changed, so the summary lists them instead of saying nothing.
function senseChangeLabels(changes) {
var parts = [];
var changed = changes.changed || {};
if (changes.gloss) {
parts.push("mane");
}
if (changes.newSense) {
parts.push("maneyeke nû");
}
SENSE_GROUPS.forEach(function (group) {
if (changed["word:" + group.key]) {
parts.push(group.label.toLowerCase());
}
});
if (changed.maneid) {
parts.push("id");
}
if (changed.etiket) {
parts.push("etîket");
}
if (changed.kategori) {
parts.push("kategorî");
}
return parts;
}
// Queues transformFn(baseWikitext) -> {content, warnings} for the real save to run later against fresh
// content (see saveQueueNow()); nothing here touches the page yet. Also runs previewFn(), which drops a
// small "not saved yet" preview into the live DOM at the change's own spot on the page. Shared by every
// panel that queues a wikitext change: buildAdvancedPanel, buildSubsectionPanel, buildConvertPosPanel.
// place is what the change is being made to ("Kurmancî/Navdêr 2"), action is what is being done to it.
// Kept apart rather than pre-joined so saveQueueNow() can name a section once and list everything
// queued against it after it, instead of repeating the place before every item.
function queueChange(place, action, transformFn, $save, $status, previewFn, opts) {
opts = opts || {};
var entry = {
key: opts.key || null,
place: place || "",
action: action,
summary: place ? place + ": " + action : action,
transformFn: transformFn,
runLast: !!opts.runLast,
// Kept, not just run: redrawPreviews() calls it again when another entry leaves the queue.
previewFn: typeof previewFn === "function" ? previewFn : null,
removePreview: null
};
// Queuing the same target twice replaces the pending change rather than stacking a second one --
// two "+Têkildar" entries would both run, and the second would abort on "Ev beş jixwe heye".
var superseded = false;
if (entry.key) {
for (var i = dsbQueue.entries.length - 1; i >= 0; i--) {
if (dsbQueue.entries[i].key === entry.key) {
dropPreview(dsbQueue.entries[i]);
dsbQueue.entries.splice(i, 1);
superseded = true;
}
}
}
dsbQueue.entries.push(entry);
$status.removeClass("dsb-status-error").addClass("dsb-status-ok").text(
"✓ li rêza guhartinan hate zêdekirin — pêl «Guhartinan qeyd bike» bike da ku bi cih bibe."
);
$save.prop("disabled", false);
if (previewFn) {
try {
var removePreview = previewFn();
if (typeof removePreview === "function") {
entry.removePreview = removePreview;
}
} catch (e) { /* a broken preview should never stop the change from being queued */ }
}
// What the superseded entry drew is gone from the queue, so whatever else is drawn from it is stale.
if (superseded) {
dsbQueue.entries.forEach(function (other) {
if (other !== entry) {
dropPreview(other);
}
});
redrawPreviews();
}
renderQueueBox();
}
// Runs text through the ext.gadget.linkify gadget ($.kuLinkify) and calls back with the linkified result.
// Passes the text through unchanged if the gadget can't be loaded, so it never blocks a save.
var linkifyText = util.linkifyText;
// ---- wikitext location ----
//
// One copy for both editing gadgets, in wikt.edit: add-examples writes into the same places.
var langSectionRange = wt.langSectionRange;
var locateSection = wt.locateSection;
var findDefinitionLine = wt.findDefinitionLine;
/**
* The wikitext of one sense's own block: its "#" line plus everything nested under it -- the "#:" word
* lines, the "#*" citations and any sub-senses -- stopping at the next line of the same depth that
* starts a new sense. Parsed on its own that block renders as exactly the <li> the page shows.
*/
function senseBlockWikitext(pageContent, language, sectionType, sectionOrdinal, definitionLevel) {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var defLine = findDefinitionLine(lines, loc.posLineIndex, definitionLevel);
var depth = definitionLevel.length - 1;
var out = [lines[defLine]];
for (var i = defLine + 1; i < lines.length; i++) {
var m = /^(#+)([:*]?)/.exec(lines[i]);
if (!m || m[1].length < depth) {
break;
}
// Same depth and no ":" or "*" after the hashes is the next sense, not part of this one.
if (m[1].length === depth && !m[2]) {
break;
}
out.push(lines[i]);
}
return out.join("\n");
}
// Inserts or extends a "#: {{tplKey|lang|word1|word2}}" sub-line for one sense (start.js's wordLine()
// format). If a line for this tplKey already exists under the definition, missing words are appended to
// it; otherwise a new line lands after the last existing group line, always before any "#*" citation lines.
function insertSenseWords(pageContent, language, sectionType, sectionOrdinal, definitionLevel, tplKey, words, opts) {
opts = opts || {};
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var defLine = findDefinitionLine(lines, loc.posLineIndex, definitionLevel);
var marker = new Array(definitionLevel.length).join("#"); // definitionLevel.length - 1 "#" characters
// Group 1 is the spelling the page uses, group 2 the parameters: an existing {{hevwate}} stays
// {{hevwate}} rather than being silently renamed to {{hevmane}}.
var tplLineRegex = new RegExp("^" + marker + ":\\s*\\{\\{\\s*(" + aliasPattern(tplKey) + ")\\s*\\|([^}]*)\\}\\}\\s*$");
var groupKeysPattern = SENSE_GROUPS.map(function (g) { return aliasPattern(g.key); }).join("|");
var groupLineRegex = new RegExp("^" + marker + ":\\s*\\{\\{\\s*(" + groupKeysPattern + ")\\s*\\|");
var anyBlockLineRegex = new RegExp("^" + marker + "[:*]");
var lastGroupLine = -1;
for (var i = defLine + 1; i < lines.length; i++) {
if (!anyBlockLineRegex.test(lines[i])) {
break;
}
var tplMatch = tplLineRegex.exec(lines[i]);
if (tplMatch) {
var foundKey = tplMatch[1];
if (opts.replace) {
// Editing an existing list: an empty result means the user cleared it, so drop the line.
if (words.length) {
lines[i] = marker + ": {{" + foundKey + "|" + language + "|" + words.join("|") + "}}";
} else {
lines.splice(i, 1);
}
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
var params = tplMatch[2].split("|"); // [lang, word1, word2, ...]
var existingWords = params.slice(1);
words.forEach(function (w) {
if (existingWords.indexOf(w) === -1) {
existingWords.push(w);
}
});
lines[i] = marker + ": {{" + foundKey + "|" + language + "|" + existingWords.join("|") + "}}";
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
if (groupLineRegex.test(lines[i])) {
lastGroupLine = i;
}
}
var insertAt = lastGroupLine >= 0 ? lastGroupLine + 1 : defLine + 1;
lines.splice(insertAt, 0, marker + ": {{" + tplKey + "|" + language + "|" + words.join("|") + "}}");
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
// Canonical left-to-right order of the small templates that can prefix a definition line's gloss text:
// maneid, then kategori (K), then etîket (e).
var LEADING_TPL_ORDER = startData.leadingTplOrder;
// True for a leading call the advanced panel edits in a box of its own -- those are kept out of the
// gloss box so the same template is never editable in two places at once. Redirect spellings count:
// {{f}} and {{etîket}} are {{e}}.
function hasOwnFieldBox(key) {
return LEADING_TPL_ORDER.indexOf(canonicalTemplateKey(key)) !== -1;
}
// Inserts, extends, or (with opts.replace) overwrites one of the small templates that prefix a definition
// line's gloss text -- {{maneid|lang|id}}, {{K|lang|kat1|...}} or {{e|lang|etîket1|...}}. Under
// opts.replace an empty values list removes the call rather than writing an empty one.
function insertLeadingSenseTpl(pageContent, language, sectionType, sectionOrdinal, definitionLevel, tplKey, values, opts) {
opts = opts || {};
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var defLine = findDefinitionLine(lines, loc.posLineIndex, definitionLevel);
var marker = /^#+/.exec(lines[defLine])[0];
var afterMarker = lines[defLine].slice(marker.length).replace(/^\s+/, "");
// Collecting the whole leading-call run means an existing tplKey call is found and merged wherever it
// already sits, not just when it happens to be first.
var split = splitLeadingCalls(afterMarker);
var calls = split.calls;
var rest = split.rest;
var existing = null;
calls.forEach(function (c) {
if (canonicalTemplateKey(c.key) === tplKey) {
existing = c;
}
});
if (existing) {
if (opts.replace) {
// Editing an existing call: an empty result means the user cleared it, so drop the call.
if (values.length) {
existing.params = [language].concat(values);
} else {
calls = calls.filter(function (c) { return c !== existing; });
}
} else {
var existingVals = existing.params.slice(1);
values.forEach(function (v) {
if (existingVals.indexOf(v) === -1) {
existingVals.push(v);
}
});
existing.params = [language].concat(existingVals);
}
} else if (!values.length) {
return pageContent; // nothing to add, and nothing there to clear
} else {
// Not present yet -- insert right before the first existing call that outranks tplKey in
// LEADING_TPL_ORDER (an unrecognized call always outranks everything), or at the end of the run.
var myRank = LEADING_TPL_ORDER.indexOf(tplKey);
var insertAt = calls.length;
for (var i = 0; i < calls.length; i++) {
var otherRank = LEADING_TPL_ORDER.indexOf(canonicalTemplateKey(calls[i].key));
if (otherRank === -1 || otherRank > myRank) {
insertAt = i;
break;
}
}
calls.splice(insertAt, 0, { key: tplKey, params: [language].concat(values) });
}
var rebuilt = calls.map(function (c) {
return "{{" + c.key + "|" + c.params.join("|") + "}}";
}).join(" ");
lines[defLine] = marker + " " + (rebuilt ? rebuilt + " " : "") + rest;
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
// Replaces just the gloss text of a definition line, leaving any leading template calls untouched --
// the save half of "Maneyê biguhêre" (edit sense).
function updateSenseGloss(pageContent, language, sectionType, sectionOrdinal, definitionLevel, newGloss) {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var defLine = findDefinitionLine(lines, loc.posLineIndex, definitionLevel);
var marker = /^#+/.exec(lines[defLine])[0];
var afterMarker = lines[defLine].slice(marker.length).replace(/^\s+/, "");
// The box holds everything except the calls with their own field, so those are put back in front.
// Dropping "{{mane?}}" here is what clears start.js's placeholder once a real gloss is written.
var kept = splitLeadingCalls(afterMarker).calls.filter(function (c) {
return hasOwnFieldBox(c.key);
});
var leadingText = kept.map(function (c) {
return "{{" + c.key + "|" + c.params.join("|") + "}}";
}).join(" ");
lines[defLine] = marker + " " + (leadingText ? leadingText + " " : "") + newGloss;
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
// Deletes one sense: its definition line plus everything indented under it (its "#:" groups, "#*"
// citations, any sub-senses). Refuses to empty a section, since a POS heading with no senses is worse
// than the sense the user wanted gone.
// A new sense goes directly below the one whose pencil asked for it -- after the lines that sense owns
// (its "#:" examples, "#*" citations and "##" sub-senses), so it reads as the next sense rather than
// landing at the foot of the section. It takes the same marker depth as its neighbour, so asking from
// a "##" sub-sense adds a sibling sub-sense.
function insertNewSense(pageContent, language, sectionType, sectionOrdinal, definitionLevel, text) {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var defLine = findDefinitionLine(lines, loc.posLineIndex, definitionLevel);
var marker = /^#+/.exec(lines[defLine])[0];
var end = defLine + 1;
var ownedRegex = new RegExp("^" + marker + "[#:*]");
while (end < lines.length && ownedRegex.test(lines[end])) {
end++;
}
lines.splice(end, 0, marker + " " + text);
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
function removeSense(pageContent, language, sectionType, sectionOrdinal, definitionLevel) {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var defLine = findDefinitionLine(lines, loc.posLineIndex, definitionLevel);
var marker = /^#+/.exec(lines[defLine])[0];
var end = defLine + 1;
var ownedRegex = new RegExp("^" + marker + "[#:*]");
while (end < lines.length && ownedRegex.test(lines[end])) {
end++;
}
// Top-level senses of this section only -- "##" sub-senses go with their parent either way.
if (definitionLevel.length === 2) {
var siblings = 0;
for (var i = loc.posLineIndex + 1; i < lines.length && !/^==/.test(lines[i]); i++) {
if (/^#[^*#:]/.test(lines[i])) {
siblings++;
}
}
if (siblings < 2) {
throw new SubmitAbort("Ev maneya dawî ya vê beşê ye. Ji bo rakirinê divê tu tevahiya beşê rakî.");
}
}
lines.splice(defLine, end - defLine);
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
// Fetches the page's current wikitext and pulls out just this sense's gloss text to prefill the "Maneyê
// biguhêre" form. A plain read, not part of the hardened save path -- the actual save still re-fetches
// fresh content, so only the prefilled text itself could be a little stale.
// Current contents of one existing subsection, for prefilling the edit form.
// This page's current wikitext.
// One fetch for the whole queue: every preview reads the same revision, and asking per preview meant
// downloading the page again for each queued change. Dropped after a save, when the page has moved on.
var pageContentPromise = null;
function forgetPageContent() {
pageContentPromise = null;
}
function fetchPageContent() {
if (pageContentPromise) {
return pageContentPromise;
}
pageContentPromise = rawPageContent();
// A failed read must not be remembered as the answer.
pageContentPromise.fail(forgetPageContent);
return pageContentPromise;
}
function rawPageContent() {
return api.get({
action: "query",
prop: "revisions",
rvprop: "content",
titles: mw.config.get("wgPageName"),
formatversion: 2
}).then(function (data) {
var page = data.query && data.query.pages && data.query.pages[0];
var content = page && !page.missing && page.revisions && page.revisions[0] && page.revisions[0].content;
if (!content) {
throw new SubmitAbort("Nikare muhtewaya rûpelê bixwîne.");
}
return content;
});
}
function fetchPageSubsection(language, sectionType, sectionOrdinal, subsection) {
return fetchPageContent().then(function (content) {
return readPageSubsection(content, language, sectionType, sectionOrdinal, subsection);
});
}
function fetchSenseFields(language, sectionType, sectionOrdinal, definitionLevel) {
return fetchPageContent().then(function (content) {
return readSenseFields(content, language, sectionType, sectionOrdinal, definitionLevel);
});
}
// Everything the advanced panel can edit about one sense, read off its definition line and the "#:" lines
// under it: the raw gloss (templates included), the leading maneid/K/e values, and each word-list group.
function readSenseFields(pageContent, language, sectionType, sectionOrdinal, definitionLevel) {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var defLine = findDefinitionLine(lines, loc.posLineIndex, definitionLevel);
var defMarker = /^#+/.exec(lines[defLine])[0];
var afterMarker = lines[defLine].slice(defMarker.length).replace(/^\s+/, "");
var leading = {};
splitLeadingCalls(afterMarker).calls.forEach(function (c) {
leading[canonicalTemplateKey(c.key)] = c.params.slice(1);
});
// Only start.js's "{{mane?|lang}}" placeholder is hidden; every other template stays visible so it can
// be edited by hand in the gloss box.
// The gloss box shows the line minus the calls that have their own box ({{maneid}}, {{K}}, {{e}} and
// their redirects) and minus start.js's "{{mane?}}" placeholder. Everything else -- a {{g}} link, a
// {{guharto}} -- stays visible and editable there.
var split = splitLeadingCalls(afterMarker);
var glossCalls = split.calls.filter(function (c) {
return !hasOwnFieldBox(c.key) && canonicalTemplateKey(c.key) !== "mane?";
});
var glossLead = glossCalls.map(function (c) {
return "{{" + c.key + "|" + c.params.join("|") + "}}";
}).join(" ");
var gloss = $.trim((glossLead ? glossLead + " " : "") + split.rest);
var marker = new Array(definitionLevel.length).join("#");
var groupKeysPattern = SENSE_GROUPS.map(function (g) { return aliasPattern(g.key); }).join("|");
var groupLineRegex = new RegExp("^" + marker + ":\\s*\\{\\{\\s*(" + groupKeysPattern + ")\\s*\\|([^}]*)\\}\\}\\s*$");
var anyBlockLineRegex = new RegExp("^" + marker + "[:*]");
var words = {};
for (var i = defLine + 1; i < lines.length; i++) {
if (!anyBlockLineRegex.test(lines[i])) {
break;
}
var m = groupLineRegex.exec(lines[i]);
if (m) {
words[canonicalTemplateKey(m[1])] = m[2].split("|").slice(1);
}
}
return {
gloss: gloss,
maneid: leading.maneid ? leading.maneid[0] : "",
etiket: leading.e || [],
kategori: leading.K || [],
words: words
};
}
// Finds a POS block's body range (up to, but not including, the next heading of level <= 3), and the
// header text + line index of any "====" subsections it already contains.
// depth is the part-of-speech heading's own ("===" on most entries, "====" where the senses are
// grouped under numbered etymologies). Its block runs to the next heading at that depth or shallower,
// and its subsections are the headings one deeper.
function posBlockBounds(lines, posLineIndex, depth) {
depth = depth || 3;
var blockEnd = wt.headingAtOrAbove(depth);
var subsectionHeading = wt.headingAtDepth(depth + 1);
var end = lines.length;
var existingHeaders = [];
for (var i = posLineIndex + 1; i < lines.length; i++) {
if (blockEnd.test(lines[i])) {
end = i;
break;
}
var sub = subsectionHeading.exec(lines[i]);
if (sub) {
existingHeaders.push({ header: $.trim(sub[1]), lineIndex: i });
}
}
return { end: end, existingHeaders: existingHeaders, depth: depth };
}
// Looks up a header's rank in SUBSECTION_ORDER. -1 for a header that isn't one of ours; such a header is
// never used as an insertion anchor.
function subsectionRankForHeader(header) {
return SUBSECTION_ORDER.indexOf(header);
}
// Body line range of one "====" subsection inside a POS block: everything after its heading up to the next
// "====" heading, or the end of the block. Null if the block has no such subsection.
function subsectionBodyRange(bounds, header) {
var headers = bounds.existingHeaders;
for (var i = 0; i < headers.length; i++) {
if (headers[i].header === header) {
return {
start: headers[i].lineIndex + 1,
end: i + 1 < headers.length ? headers[i + 1].lineIndex : bounds.end
};
}
}
return null;
}
// ---- template-parameter editing -------------------------------------------------------------------
// A gloss that is nothing but one template call gets a labelled box per parameter instead of a raw
// wikitext line, the way VisualEditor's template dialog works.
// The single template call a gloss is made of, or null when the line is anything less tidy. {{g|...}} is a
// link inside the gloss rather than the gloss itself, so it neither disqualifies the line nor gets a form.
function glossTemplateTarget(gloss) {
var parsed = parseTemplateCalls(gloss || "");
if (parsed.otherContent) {
return null;
}
var editable = parsed.calls.filter(function (c) {
return canonicalTemplateKey(c.name) !== "g";
});
return editable.length === 1 ? editable[0] : null;
}
// ---- the three places that carry no "====" heading of their own ----------------------------------
// Bilêvkirin is a "===" section above the POS blocks, images sit at the top of a POS block, and the
// h-bnr hatnote goes above the first language heading. Each is located here and then read, written or
// inserted through the same three lines of splice, so nothing below needs to know about them.
var IMAGE_LINE = /^\[\[\s*(?:Wêne|File|Image|Dosya)\s*:/i;
/**
* {lines, langSectionIndex, exists, start, end, insertAt} for a subsection carrying a "place", or null
* for an ordinary one. start..end is the body as it stands; insertAt is where a missing one goes.
*/
function locateLoosePlace(pageContent, language, sectionType, sectionOrdinal, subsection) {
var place = subsection.place;
if (!place) {
return null;
}
var i, end;
if (place === "page-top") {
// Above every language section, so the whole page is the search space. The call is found under
// any of its names, and its own name is carried back so a save never renames it.
var pageLines = pageContent.split("\n");
var call = wt.findHbnrCall(pageContent);
if (call) {
return { lines: pageLines, langSectionIndex: 0, exists: true, call: call,
start: call.lineIndex, end: call.lineIndex + 1, insertAt: call.lineIndex };
}
return { lines: pageLines, langSectionIndex: 0, exists: false, start: 0, end: 0, insertAt: 0 };
}
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
if (place === "pron") {
// One per language, not one per part of speech, so it is looked for above the POS heading.
for (i = 0; i < loc.posLineIndex; i++) {
var h3 = /^===\s*([^=]+?)\s*===/.exec(lines[i]);
if (h3 && $.trim(h3[1]) === subsection.header) {
end = i + 1;
while (end < lines.length && !/^==/.test(lines[end])) {
end++;
}
return { lines: lines, langSectionIndex: loc.langSectionIndex, exists: true,
start: i + 1, end: end, insertAt: i };
}
}
// Straight after the language heading, which puts it before every POS block.
return { lines: lines, langSectionIndex: loc.langSectionIndex, exists: false,
start: 1, end: 1, insertAt: 1 };
}
if (place === "pos-top") {
// Between the conjugation line and the headword template -- where the wizard writes them.
var at = loc.posLineIndex + 1;
while (at < lines.length && /^\{\{\s*ku-tewîn/.test($.trim(lines[at]))) {
at++;
}
var from = at;
while (at < lines.length && IMAGE_LINE.test($.trim(lines[at]))) {
at++;
}
return { lines: lines, langSectionIndex: loc.langSectionIndex, exists: at > from,
start: from, end: at, insertAt: from };
}
return null;
}
// The wikitext a "place" section's content becomes. A word list there is one template call, not a
// {{kol3}} block, so the words are joined into it -- and any ⊕ modifier a row picked up is dropped,
// since a page name is all the call can carry.
function loosePlaceBody(subsection, content, language) {
if (subsection.kind === "images") {
return $.grep($.map(content || [], function (image) {
return wt.buildImageLine({
file: image.file,
caption: wt.captionForLang(image.caption, language),
alt: image.alt,
size: image.size,
extra: image.extra
});
}), Boolean).join("\n");
}
return content;
}
// The words a "place" word list holds, flattened out of its single group. Any ⊕ modifier a row picked
// up is dropped: a page name is all the call can carry.
function loosePlaceWords(content) {
var words = [];
$.each(content || [], function (_, group) {
$.each(group.words || [], function (_, raw) {
var plain = $.trim(util.parseWordValue(raw).word);
if (plain && $.inArray(plain, words) === -1) {
words.push(plain);
}
});
});
return words;
}
// Writes one of those places, inserting it when it is not there yet and replacing it when it is. Only
// Bilêvkirin carries a heading; the other two are bare lines.
function writeLoosePlace(pageContent, language, sectionType, sectionOrdinal, subsection, content) {
var at = locateLoosePlace(pageContent, language, sectionType, sectionOrdinal, subsection);
if (!at) {
throw new SubmitAbort("Cih nehate dîtin: " + subsection.header);
}
// A call naming a subpage -- "{{h-bnr/-ane}}" -- holds a list shared by every page transcluding
// it. Writing here would change this page alone and desync the rest, so the edit is refused and
// the template that actually owns the list is named instead.
if (at.call && at.call.subpage) {
throw new SubmitAbort("Ev rûpel «{{" + at.call.name + "}}» bi kar tîne — lîste li " +
"[[Şablon:" + at.call.subpage + "]] ye, lê wê zêde bike, ne li vê rûpelê.");
}
// A word list here is one template call, so it is built where the existing call's own name is
// known -- editing "{{bnr-h|...}}" must not turn it into "{{h-bnr|...}}".
var body = subsection.kind === "wordlist"
? wt.buildHbnrCall(loosePlaceWords(content), at.call && at.call.name)
: $.trim(content || "");
// A "===" section is separated from what is above and below it by a blank line, the way the rest
// of an entry is written. The other two places are bare lines inside a block and take none.
var heading = subsection.place === "pron";
function blank(index) {
return $.trim(at.lines[index] || "") === "";
}
if (at.exists) {
if (body) {
var replacement = [body];
// end is the next "==" heading; the blank line before it is inside the range being replaced.
if (heading && /^==/.test(at.lines[at.end] || "")) {
replacement.push("");
}
at.lines.splice.apply(at.lines, [at.start, at.end - at.start].concat(replacement));
} else {
// Emptying the box removes the section rather than leaving an empty heading behind.
at.lines.splice(at.start, at.end - at.start);
if (heading) {
at.lines.splice(at.insertAt, 1);
// The blank line that separated it goes with it, rather than doubling up.
if (blank(at.insertAt) && blank(at.insertAt - 1)) {
at.lines.splice(at.insertAt, 1);
}
}
}
} else {
if (!body) {
throw new SubmitAbort("Tiştek nehate nivîsandin.");
}
var block = heading ? ["=== " + subsection.header + " ===", body] : [body];
if (heading) {
if (!blank(at.insertAt - 1)) {
block.unshift("");
}
if (!blank(at.insertAt)) {
block.push("");
}
}
at.lines.splice.apply(at.lines, [at.insertAt, 0].concat(block));
}
return pageContent.slice(0, at.langSectionIndex) + at.lines.join("\n");
}
// What a section body holds: its {{kol3}} groups, the plain bullet list that stands in for them, or
// the raw wikitext when it is neither. A free section is read this way before its kind is known.
function readSubsectionBody(body, subsection) {
if (subsection.kind === "wordlist" || subsection.free) {
var calls = parseWordListCalls(body);
if (calls.length) {
return {
kind: "wordlist",
groups: calls.map(function (call) {
return { sernav: namedParam(call.named, "sernav"), words: call.words };
})
};
}
var bullets = parseBulletWordList(body);
if (bullets) {
// Editable as a word list; saving rewrites the bullets as {{kol3}}.
return { kind: "wordlist", groups: [{ sernav: "", words: bullets }], fromBullets: true };
}
}
return { kind: "text", text: $.trim(body) };
}
// The wikitext a section body becomes after an edit. A wordlist keeps its own template name, language
// and named params ("cure=Jê", ...) and only has its positional words replaced; a text one is
// replaced whole.
function rewriteSubsectionBody(body, language, subsection, content) {
if (subsection.kind !== "wordlist") {
// Replacing the body wholesale drops that separator, so put it back.
return content + (/\n\s*$/.test(body) ? "\n" : "");
}
var calls = parseWordListCalls(body);
if (calls.length) {
// Each call is rewritten where it stands, so text between and around them survives untouched.
// Last to first, so the offsets of the earlier ones stay valid as we go.
var replacement = body;
for (var gi = calls.length - 1; gi >= 0; gi--) {
var call = calls[gi];
var group = content[gi];
// A group emptied in the form drops its call entirely rather than leaving "{{kol3|ku}}".
var rebuilt = group && group.words.length
? buildWordListCall(call.name, call.lang, withNamedParam(call.named, "sernav", group.sernav), group.words)
: "";
replacement = replacement.slice(0, call.start) + rebuilt + replacement.slice(call.end);
}
// Groups the form added beyond the ones already there go after the last of them.
var added = content.slice(calls.length).filter(function (group) {
return group.words.length;
}).map(function (group) {
return buildWordListCall(WORDLIST_TEMPLATE, language,
withNamedParam(["cure=" + subsection.header], "sernav", group.sernav), group.words);
});
if (added.length) {
replacement = replacement.replace(/\s*$/, "") + "\n\n" + added.join("\n\n") + "\n";
}
return replacement.replace(/\n{3,}/g, "\n\n"); // gaps left by any dropped call
}
if (parseBulletWordList(body)) {
// A plain bullet list is replaced wholesale by a real {{kol3}} on its first edit.
return content.map(function (group) {
return buildWordListCall(WORDLIST_TEMPLATE, language,
withNamedParam(["cure=" + subsection.header], "sernav", group.sernav), group.words);
}).join("\n\n") + (/\n\s*$/.test(body) ? "\n" : "");
}
throw new SubmitAbort("Nikare lîsteya vê beşê bixwîne: " + subsection.header);
}
// ---- sections that are not subsections of a part of speech ----
// A "=== Etîmolojî ===" that the parts of speech hang under, a "=== Çavkanî ===" at the foot of a
// language, a heading nobody planned for. Located by its own text, and edited as the wikitext under
// it -- or as a {{kol3}} list when that is what it holds.
function freeSectionBody(pageContent, language, subsection) {
var loc = wt.locateHeadingByText(pageContent, language, subsection.header, subsection.free.ordinal);
return { loc: loc, body: loc.lines.slice(loc.bodyStart, loc.bodyEnd).join("\n") };
}
function readFreeSection(pageContent, language, subsection) {
var at = freeSectionBody(pageContent, language, subsection);
return readSubsectionBody(at.body, subsection);
}
function updateFreeSection(pageContent, language, subsection, content) {
var at = freeSectionBody(pageContent, language, subsection);
at.loc.lines.splice(at.loc.bodyStart, at.loc.bodyEnd - at.loc.bodyStart,
rewriteSubsectionBody(at.body, language, subsection, content));
return pageContent.slice(0, at.loc.langSectionIndex) + at.loc.lines.join("\n");
}
// Current contents of an existing "====" subsection. A wordlist one whose template can't be parsed comes
// back as {kind: "text"} so the caller can still edit it as raw wikitext instead of refusing outright.
function readPageSubsection(pageContent, language, sectionType, sectionOrdinal, subsection) {
if (subsection.free) {
return readFreeSection(pageContent, language, subsection);
}
var loose = locateLoosePlace(pageContent, language, sectionType, sectionOrdinal, subsection);
if (loose) {
if (!loose.exists) {
return null;
}
if (loose.call) {
return { kind: "wordlist", groups: [{ sernav: "", words: loose.call.words }] };
}
var looseBody = $.trim(loose.lines.slice(loose.start, loose.end).join("\n"));
if (subsection.kind === "images") {
return {
kind: "images",
images: $.grep($.map(looseBody.split("\n"), wt.parseImageLine), Boolean)
};
}
if (subsection.kind !== "wordlist") {
return { kind: "text", text: looseBody };
}
// One template call rather than a {{kol3}} block: its positional parameters are the words.
var looseCall = parseTemplateCalls(looseBody).calls[0];
return { kind: "wordlist", groups: [{ sernav: "", words: (looseCall && looseCall.params) || [] }] };
}
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var bounds = posBlockBounds(loc.lines, loc.posLineIndex, loc.depth);
var range = subsectionBodyRange(bounds, subsection.header);
if (!range) {
return null;
}
return readSubsectionBody(loc.lines.slice(range.start, range.end).join("\n"), subsection);
}
// Rewrites an existing "====" subsection in place. A wordlist one keeps its own template name, language and
// named params ("cure=Jê", ...) and only has its positional words replaced; a text one is replaced whole.
function updatePageSubsection(pageContent, language, sectionType, sectionOrdinal, subsection, content) {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var bounds = posBlockBounds(lines, loc.posLineIndex, loc.depth);
var range = subsectionBodyRange(bounds, subsection.header);
if (!range) {
throw new SubmitAbort(util.msg.sectionMissing + subsection.header);
}
var body = lines.slice(range.start, range.end).join("\n");
lines.splice(range.start, range.end - range.start,
rewriteSubsectionBody(body, language, subsection, content));
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
// Inserts a new "==== Header ====" subsection into its SUBSECTION_ORDER position within a POS block, right
// before the first ranked subsection that outranks it, or at the end if none does. Either a
// {{kol3|...}} word list or a plain paragraph, matching start.js's own subsection formats.
function insertPageSubsection(pageContent, language, sectionType, sectionOrdinal, subsection, content) {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var bounds = posBlockBounds(lines, loc.posLineIndex, loc.depth);
var alreadyThere = false;
bounds.existingHeaders.forEach(function (h) {
if (h.header === subsection.header) {
alreadyThere = true;
}
});
if (alreadyThere) {
throw new SubmitAbort("Ev beş jixwe heye: " + subsection.header);
}
var body = subsection.kind === "wordlist"
? content.map(function (group) {
return buildWordListCall(WORDLIST_TEMPLATE, language,
withNamedParam(["cure=" + subsection.header], "sernav", group.sernav), group.words);
}).join("\n\n")
: content;
var myRank = subsectionRankForHeader(subsection.header);
var headers = bounds.existingHeaders;
var insertAt = bounds.end;
for (var i = 0; i < headers.length; i++) {
var otherRank = subsectionRankForHeader(headers[i].header);
if (otherRank !== -1 && otherRank > myRank) {
insertAt = headers[i].lineIndex;
break;
}
}
// Exactly one blank line on each side: the line before the insertion point is normally already blank
// (it separated the previous subsection from the next heading), so only add one where there isn't one.
var block = [];
if (insertAt > 0 && $.trim(lines[insertAt - 1]) !== "") {
block.push("");
}
var marks = wt.headingMarks(bounds.depth + 1);
block.push(marks + " " + subsection.header + " " + marks, body);
if (insertAt < lines.length) {
block.push("");
}
lines.splice.apply(lines, [insertAt, 0].concat(block));
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
// ---- converting an unclassified "=== Mane ===" placeholder heading into a real POS heading ----
// Some pages carry a heading literally named "Mane" (optionally "Mane 2" etc.) instead of a real
// part-of-speech name, tagged right below with {{cure?|<lang>}} (ku.wiktionary's "part of speech not yet
// determined" maintenance flag). It is a section key of its own (sections.MANE_SECTION), so the shared
// locator finds it like any other heading; only the rewriting below is particular to it.
// Renames the "=== Mane ===" placeholder to the chosen POS heading, drops the maintenance tag line right
// after it if present (MANE_TAG_REGEX -- cure? or any of its known aliases), and inserts the matching
// "{{<pos>|<lang>}}" call in its place (skipped when the section has no template of its own).
// Other content under the heading is left untouched.
/**
* Where a section's own headword template sits, as {index, text}, or {index: -1} when it has none.
*
* Not always the line under the heading: a "{{ku-tewîn-lk}}" line and any number of "[[Wêne:...]]"
* images can come first. Everything down to the first definition line is searched, and index doubles
* as the slot a new call belongs in when there is no old one.
*/
function findPosTemplateLine(lines, headingLine, sectionType) {
var oldSpec = posTemplateFor(sectionType);
var oldTemplate = oldSpec ? oldSpec.name : (sectionType === "Biwêj" ? "biwêj" : null);
var oldTemplateRegex = oldTemplate &&
new RegExp("^\\{\\{\\s*" + escapeRegExp(oldTemplate) + "\\s*[|}]");
var at = headingLine + 1;
while (at < lines.length && !/^#/.test(lines[at]) && !/^==/.test(lines[at])) {
if (MANE_TAG_REGEX.test(lines[at]) ||
(oldTemplateRegex && oldTemplateRegex.test(lines[at]))) {
return { index: at, text: lines[at], end: at };
}
at++;
}
return { index: -1, text: "", end: at };
}
// Named parameters merged in order, each written once: a later value replaces an earlier one of the
// same name. Positional values are kept as they come.
function mergeCallParams() {
var out = [];
var seen = {};
$.each(Array.prototype.slice.call(arguments), function (_, list) {
$.each(list || [], function (__, param) {
var m = /^\s*([^=|]+?)\s*=([\s\S]*)$/.exec(param);
if (!m) {
out.push(param);
return;
}
// "z" and "c" are one name to Modul:rêziman, so the form's "c=m" meets the page's "z=".
var key = wt.canonicalParamName(m[1]);
if (!Object.prototype.hasOwnProperty.call(seen, key)) {
seen[key] = out.length;
out.push(param);
return;
}
// The page's own spelling stays when it carried a value; an empty one gives way entirely.
var earlier = /^\s*([^=|]+?)\s*=([\s\S]*)$/.exec(out[seen[key]]);
// "?" asks for a value, so it never replaces one the page already has.
if ($.trim(earlier[2]) && $.trim(m[2]) === "?") {
return;
}
out[seen[key]] =$.trim(earlier[2]) ? earlier[1] + "=" + m[2] : param;
});
});
return out;
}
// What the call on the page already says, minus its language. Carried over only when the new section
// uses that same template -- "z=m" survives Navdêr becoming Formeke navdêrê, while a verb's "c=h-gh"
// has no meaning on a noun and is left behind.
function keptCallParams(line, templateName) {
var call = parseTemplateCalls($.trim(line || "")).calls[0];
if (!call || canonicalTemplateKey(call.name) !== canonicalTemplateKey(templateName)) {
return [];
}
return call.params.slice(1).filter(function (param) {
return /=/.test(param);
});
}
// The headword call a converted section gets, or "" when the new section has none. What the old call
// (found, from findPosTemplateLine) said keeps its place, then the section's own defaults, then what the
// form was filled in with -- each name written once, the last value winning, so a converted call reads
// as small a change from the old one as it can.
function posCallLine(found, language, newSectionType, posParams) {
var spec = posTemplateFor(newSectionType);
if (!spec) {
return "";
}
var kept = found.index !== -1 ? keptCallParams(found.text, spec.name) : [];
// A lemma is not an inflected form, so the old section's form=erê stays behind.
if (spec.params.indexOf("form=erê") === -1) {
kept = kept.filter(function (param) {
return !/^\s*form\s*=\s*erê\s*$/.test(param);
});
}
var params = mergeCallParams(kept, spec.params, posParams ? posParams.split("|") : []);
return "{{" + spec.name + "|" + language + (params.length ? "|" + params.join("|") : "") + "}}";
}
function convertSectionToPos(pageContent, language, sectionType, sectionOrdinal, newSectionType, posParams) {
if (!sectionNames[newSectionType]) {
throw new SubmitAbort("Navê cureyê rêzimanî yê hilbijartî nediyar e.");
}
// locateSection() already handles the "Mane" placeholder as a section type of its own.
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var headingLine = loc.posLineIndex;
// Written without a number: the renumbering pass below decides every number in this language
// section at once, so one carried over from the old name would just be overwritten.
lines[headingLine] = "=== " + newSectionType + " ===";
// The line under the heading is either the old section's own template call ("{{biwêj|ku}}") or, for a
// Mane placeholder, the "needs a POS" maintenance tag. Both are replaced by the new POS's call.
// The new call takes the old one's own place -- or, when there was none, the slot just above the
// definitions, which is where the wizard writes it.
var found = findPosTemplateLine(lines, headingLine, sectionType);
var insertAt = found.index !== -1 ? found.index : found.end;
if (found.index !== -1) {
lines.splice(found.index, 1);
}
var call = posCallLine(found, language, newSectionType, posParams);
if (call) {
lines.splice(insertAt, 0, call);
}
// Renaming a section can make it repeat one the page already had, and can equally leave a name that
// is no longer repeated. Both are settled by renumbering the whole language section: a "Rengdêr"
// beside a new "Rengdêr" becomes "Rengdêr 1" and "Rengdêr 2", and a number left behind by a name
// that is now unique is dropped.
var body = lines.join("\n");
var nextLang = body.indexOf("\n== ", 1);
var end = nextLang === -1 ? body.length : nextLang;
body = wt.numberRepeatedPosHeadings(body.slice(0, end)) + body.slice(end);
return pageContent.slice(0, loc.langSectionIndex) + body;
}
// ---- "guhartina pêşketî" (advanced editing): combine several independent per-sense edits into ONE save ----
// Threads pageContent through the same pure functions every single-field form uses, applying only the
// pieces the caller actually filled in. A piece that conflicts with the page (maneid already set, a
// subsection that's already there) is skipped with a warning rather than failing the whole save. Throws
// SubmitAbort if nothing was filled in, so this can never turn into a no-op save.
function applyAdvancedChanges(pageContent, language, sectionType, sectionOrdinal, definitionLevel, changes) {
var content = pageContent;
var warnings = [];
var appliedAnything = false;
if (changes.gloss) {
content = updateSenseGloss(content, language, sectionType, sectionOrdinal, definitionLevel, changes.gloss);
appliedAnything = true;
}
if (changes.newSense) {
content = insertNewSense(content, language, sectionType, sectionOrdinal, definitionLevel, changes.newSense);
appliedAnything = true;
}
// Every box below is prefilled with what the sense already has, so a save replaces that value rather
// than merging into it -- and clearing a box removes the template. changes.changed marks the boxes
// whose contents actually differ from the page, so an opened-but-untouched box writes nothing.
var replace = { replace: true };
if (changes.changed.maneid) {
content = insertLeadingSenseTpl(content, language, sectionType, sectionOrdinal, definitionLevel, "maneid", changes.maneid ? [changes.maneid] : [], replace);
appliedAnything = true;
}
if (changes.changed.etiket) {
content = insertLeadingSenseTpl(content, language, sectionType, sectionOrdinal, definitionLevel, "e", changes.etiket, replace);
appliedAnything = true;
}
if (changes.changed.kategori) {
content = insertLeadingSenseTpl(content, language, sectionType, sectionOrdinal, definitionLevel, "K", changes.kategori, replace);
appliedAnything = true;
}
SENSE_GROUPS.forEach(function (group) {
if (changes.changed["word:" + group.key]) {
content = insertSenseWords(content, language, sectionType, sectionOrdinal, definitionLevel, group.key, changes.words[group.key], replace);
appliedAnything = true;
}
});
if (!appliedAnything) {
throw new SubmitAbort("Tiştek nehat guhartin.");
}
return { content: content, warnings: warnings };
}
// ---- per-definition UI: a small always-visible "+" toggle (no hover, so it works on mobile/touch too) ----
// A growing list of word rows. The row itself is util.js's, shared with the wizard; this owns the list:
// typing in the last row appends a fresh empty one, so there is always somewhere to type.
var WORDLIST_COLUMN_THRESHOLD = 6;
function buildWordListField(placeholder, opts) {
opts = opts || {};
var $list = $("<ol class='dsb-wordlist'></ol>");
// One column is a long scroll once a list gets past a handful of words. The class only asks for
// two; the stylesheet still gives one on a narrow screen. Two columns inside the panel's usual
// 520px would leave each word about 250px, so the panel drops its width cap for as long as any
// list in it is in two columns -- checked across the whole panel, since a "Herwiha binêre" holds
// several groups and one of them going wide settles it for all.
function updateColumns() {
$list.toggleClass("dsb-wordlist-2col",
$list.children("li").length > WORDLIST_COLUMN_THRESHOLD);
var $panel = $list.closest(".dsb-subsection-panel");
$panel.toggleClass("dsb-panel-wide", !!$panel.find(".dsb-wordlist-2col").length);
}
function makeRow() {
var row = util.makeModifierRow({
prefix: "dsb-",
placeholder: placeholder,
modTitle: util.msg.addModifier,
removeTitle: util.msg.removeWord,
removeModTitle: util.msg.removeModifier,
modifiers: opts.modifiers,
// "bixe qutiyan" puts each comma-separated word in a row of its own, right after the row it
// was typed in, and the caret follows to the last of them. One word per row is also one
// word per template parameter, which both {{hevmane}} and {{kol3}} read the same way --
// a comma left inside a parameter would not be.
onSplit: function (rest) {
var $after = row.$li;
var last = null;
rest.forEach(function (raw) {
var extra = makeRow();
$after.after(extra.$li);
$after = extra.$li;
if (raw) extra.setRaw(raw);
last = extra;
});
// Always an empty row at the end to type the next word into.
var $tail = $list.children("li").last();
if ($.trim($tail.data("row").getRaw())) {
last = makeRow();
$list.append(last.$li);
}
updateColumns();
if (last) last.focus();
},
// raw === null means the row removed itself, so the count has changed either way.
onChange: function (raw) {
if (raw !== null) {
var isLast = row.$li.is($list.children("li").last());
if (isLast && raw) $list.append(makeRow().$li);
}
updateColumns();
}
});
row.$li.data("row", row);
return row;
}
$list.append(makeRow().$li);
return {
$element: $list,
values: function () {
var vals = [];
$list.children("li").each(function () {
var raw = $(this).data("row").getRaw();
if (raw) vals.push(raw);
});
return vals;
},
// Replaces the whole list, keeping one empty trailing row to type into.
setValues: function (raws) {
$list.empty();
raws.forEach(function (raw) {
var row = makeRow();
$list.append(row.$li);
row.setRaw(raw);
});
$list.append(makeRow().$li);
updateColumns();
},
focus: function () {
$list.children("li").first().data("row").focus();
}
};
}
// Plain removable-tag field, used for Etîket and Kategorî (simple value lists with no per-word modifiers).
// Enter or "," commits the current text as a tag; Backspace on an empty input removes the last tag;
// values() also commits whatever is still typed but not yet confirmed.
function buildTagField(placeholder) {
var $input = $("<input type='text' class='dsb-input dsb-tag-input'>").attr("placeholder", placeholder);
var $element = $("<span class='dsb-tagfield'></span>").append($input);
function currentValues() {
var vals = [];
$element.children(".dsb-tag").each(function () {
vals.push($(this).data("value"));
});
return vals;
}
function addTag(val) {
val = $.trim(val || "");
if (!val || currentValues().indexOf(val) !== -1) {
return;
}
var $tag = $("<span class='dsb-tag'></span>").text(val).data("value", val);
var $remove = $("<button type='button' class='dsb-tag-remove' title='Jê bibe'>×</button>");
$remove.on("click", function () {
$tag.remove();
$input.trigger("focus");
});
$tag.append($remove);
$tag.insertBefore($input);
}
$input.on("keydown", function (e) {
if (e.which === 13 || e.which === 188) { // Enter or comma
e.preventDefault();
addTag($input.val());
$input.val("");
} else if (e.which === 8 && !$input.val()) {
$element.children(".dsb-tag").last().remove();
}
});
$input.on("blur", function () {
addTag($input.val());
$input.val("");
});
return {
$element: $element,
// Replaces every tag, for prefilling from what the sense already has.
setValues: function (vals) {
$element.children(".dsb-tag").remove();
vals.forEach(function (v) {
addTag(v);
});
$input.val("");
},
values: function () {
addTag($input.val());
$input.val("");
return currentValues();
},
focus: function () {
$input.trigger("focus");
}
};
}
// The wizard's own etymology templates, straight out of data-main.json's "etymTpls" -- the same list start.js's
// insert menu offers, so the two can never drift apart. CODE/SOURCELANG are its placeholders.
// The wizard's own template lists, straight out of data-main.json, so both gadgets offer the same
// templates in the same order. A pronunciation template names the languages it is for; one that names
// none is offered everywhere.
function templateChoices(pool, language) {
var defs = startData[pool] || {};
return Object.keys(defs).filter(function (name) {
var langs = defs[name].langs;
return !langs || !langs.length || $.inArray(language, langs) !== -1;
}).map(function (name) {
return { name: name, desc: defs[name].desc, snippet: defs[name].snippet };
});
}
// One template call inside the surface: shown as its own wikitext, but atomic -- contenteditable="false"
// keeps the caret out of it, so it can only be changed through the dialog.
function makeTemplateChip(wikitext) {
// The × is its own element rather than the chip's only way out: on a phone there is no reliable
// backspace onto a contenteditable="false" span, so without it the dialog is the only way to take
// a call off the line. readChipSurface() reads chips from data-wikitext, so the extra child does
// not reach the saved text.
var $remove = $("<span class='dsb-tpl-chip-remove' contenteditable='false'>×</span>")
.attr("title", "Vê şablonê jê bibe");
return $("<span class='dsb-tpl-chip' contenteditable='false' role='button' tabindex='0'></span>")
.attr("data-wikitext", wikitext)
.attr("title", "Bitikîne da ku parametreyan biguhêrî")
.text(wikitext)
.append($remove);
}
// wikitext -> surface DOM: template calls become chips, everything else stays plain editable text.
function renderChipSurface($surface, wikitext) {
var text = wikitext || "";
var calls = parseTemplateCalls(text).calls;
var pos = 0;
$surface.empty();
calls.forEach(function (call) {
if (call.start > pos) {
$surface.append(document.createTextNode(text.slice(pos, call.start)));
}
$surface.append(makeTemplateChip(text.slice(call.start, call.end)));
pos = call.end;
});
if (pos < text.length) {
$surface.append(document.createTextNode(text.slice(pos)));
}
}
// surface DOM -> wikitext. Chips give back their stored call; anything the browser inserted while typing
// (a <br>, a wrapper <div>) degrades to its text, so the result is always plain wikitext.
function readChipSurface(node) {
var out = "";
$(node).contents().each(function () {
if (this.nodeType === 3) {
out += this.nodeValue;
return;
}
var $el = $(this);
if ($el.hasClass("dsb-tpl-chip")) {
out += $el.attr("data-wikitext");
return;
}
if (this.nodeName === "BR") {
out += "\n";
return;
}
if (this.nodeName === "DIV" || this.nodeName === "P") {
out += "\n" + readChipSurface(this);
return;
}
out += readChipSurface(this);
});
return out;
}
// Drops a node in at the caret, or at the end when the caret is not inside the surface.
function insertAtCaret($surface, node) {
var sel = window.getSelection();
if (sel && sel.rangeCount && $.contains($surface.get(0), sel.getRangeAt(0).commonAncestorContainer)) {
var range = sel.getRangeAt(0);
range.deleteContents();
range.insertNode(node);
range.setStartAfter(node);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
} else {
$surface.append(node);
}
}
// The template-param dialog (openTemplateDialog/buildTemplateParamForm) moved to wikt.cite, shared with
// start.js's own Etîmolojî and Çavkanî pickers: cite.openTemplateDialog / cite.buildTemplateParamForm.
// Each new example queued is its own entry, so several can be waiting at once.
var exampleSeq = 0;
// Takes the save over from add-examples while its form is open for one sense: its "Qeyd bike" becomes
// "Lê zêde bike" and the edit joins the pending queue instead of publishing on its own, so examples and
// whatever else is queued go up as one edit. Installed on every open, so the handler always closes over
// the sense whose form is actually open. ctx: {$li, $h3, language, sectionType, sectionOrdinal,
// definitionLevel, $save, $status, afterQueue}.
function takeOverExampleSave(ctx) {
if (!(wikt.addExamples && wikt.addExamples.setSubmitHandler)) {
return; // older add-examples: it publishes on its own, as before
}
// Lets the form read an example as the queue has already changed it.
wikt.addExamples.pendingContent = queuedContent;
wikt.addExamples.setSubmitHandler(function (job) {
exampleSeq++;
queueChange(
placeLabel(ctx.language, ctx.sectionType, ctx.$h3),
// add-examples' own summary, so the combined edit still credits the tool and names its
// version. The trailing stop goes: this is one item in a list, not a sentence of its own.
(job.summary || "nimûne/jêgirtin hate zêdekirin").replace(/\.\s*$/, ""),
function (pageContent) {
// add-examples builds against a revision object, and returns {text, summary}.
var built = job.transform({ content: pageContent });
return { content: built.text, warnings: [] };
},
ctx.$save,
ctx.$status,
// Drawn like any other queued change, in the sense it belongs to.
function () {
return previewSense(ctx.$li, ctx.language, ctx.sectionType, ctx.sectionOrdinal, ctx.definitionLevel);
},
// An edit to an example already on the page brings its own key, so editing it again replaces it.
{ key: job.key || ("example:" + ctx.sectionType + "|" + ctx.sectionOrdinal + "|" +
ctx.definitionLevel.join(".") + "|" + exampleSeq) }
);
job.done();
if (ctx.afterQueue) {
ctx.afterQueue();
}
});
}
// Opens add-examples' form on one example already under a sense -- dsb.js's pencil on it. Its save joins
// the queue like any other change.
function editExample($li, $h3, language, sectionType, sectionOrdinal, definitionLevel, itemEl) {
takeOverExampleSave({ $li: $li, $h3: $h3, language: language, sectionType: sectionType,
sectionOrdinal: sectionOrdinal, definitionLevel: definitionLevel, $save: $(), $status: $(),
afterQueue: openQueueBox });
if (!(wikt.addExamples && wikt.addExamples.editFor && wikt.addExamples.editFor($li.get(0), itemEl))) {
mw.notify("Amûra nimûneyan li vê maneyê nehat dîtin.");
}
}
// Builds the whole "guhartina pêşketî" panel into $quickRow: gloss and the two most-used word lists
// (Hevmane/Dijmane) sit directly in the picker row; the remaining word lists, maneid, etîket, kategorî
// and Nimûne/jêgirtin live behind a "Hevmane û Bêhtir" dropdown; one shared "Lê zêde bike" queues everything the
// user filled in as a single edit via applyAdvancedChanges() and queueChange(). Nimûne/jêgirtin is an
// immediate proxy-click into a different gadget's own form, not part of this combined save.
function buildAdvancedPanel($quickRow, $li, $h3, language, sectionType, sectionOrdinal, definitionLevel, isLastSense) {
// "+" marks the actions that add something, matching the dropdown's own "+ Hevmane" labels.
var $nimune = $("<a href='#' class='dsb-quick-link'></a>").text("+ Nimûne/jêgirtin");
$nimune.on("click", function (e) {
e.preventDefault();
takeOverExampleSave({ $li: $li, $h3: $h3, language: language, sectionType: sectionType,
sectionOrdinal: sectionOrdinal, definitionLevel: definitionLevel, $save: $save, $status: $status });
// Toggle, so clicking this a second time closes the form again. add-examples builds nothing
// on the page of its own, so this is the only way its form opens.
var drive = wikt.addExamples.toggleFor || wikt.addExamples.openFor;
if (!(drive && drive($li.get(0)))) {
mw.notify("Amûra nimûneyan li vê maneyê nehat dîtin.");
}
$moreMenu.slideUp();
});
// getters holds one function per currently-open box (key -> () => value); collectChanges() only ever
// reads from a box the user actually clicked to open.
var getters = {};
// What the sense currently holds, fetched once and shared by every box that prefills from it.
// collectChanges() also diffs against it, so an untouched box never queues a change.
var original = null;
var sensePromise = null;
function loadSense() {
if (!sensePromise) {
sensePromise = fetchSenseFields(language, sectionType, sectionOrdinal, definitionLevel).then(function (fields) {
original = fields;
return fields;
});
}
return sensePromise;
}
function sameList(a, b) {
return (a || []).join("\u0000") === (b || []).join("\u0000");
}
// -- gloss ("Maneyê biguhêre" -- the one box that EDITS the sense's wording; everything else here only ADDS) --
function addGlossField() {
var $fields = $("<div class='dsb-gloss-fields'></div>");
var $textarea = $("<textarea class='dsb-input dsb-edit-gloss' rows='2'></textarea>")
.attr("placeholder", util.msg.loading).prop("disabled", true);
var $autoLink = $("<button type='button' class='dsb-autolink'>Oto-lînk bike</button>").prop("disabled", true);
var $toRaw = $("<a href='#' class='dsb-quick-link dsb-gloss-raw'>Wekî wîkîtekst biguhêre</a>").hide();
var $status = $("<span class='dsb-status'></span>");
var originalGloss = null;
var tplForm = null; // set while the parameter boxes are showing instead of the raw line
var tplCall = null;
// The gloss of a non-Kurdish entry is a translation followed by {{îzah|…}} explaining it. That
// is one field on the page, so it stays one box here; the explanation opens beside it only when
// asked for, and the two are joined back into a single gloss on save. A Kurdish sense is its
// own definition, so it is never offered one -- though a ku gloss that somehow already carries
// an {{îzah}} still opens its box below, rather than being edited blind.
var offerIzah = language !== "ku";
var $izah = $("<textarea class='dsb-input dsb-edit-izah' rows='2'></textarea>")
.attr("placeholder", "Îzaha maneyî… ji bo school-a îngilîzî 'cihê tehsîlgirtinê'");
// A non-Kurdish gloss is a translation and its explanation, so both boxes open together. A Kurdish
// sense is its own definition and gets no îzah box, unless its gloss already carries one.
var $izahWrap = $("<div class='dsb-izah-wrap'></div>")
.append($("<label class='dsb-adv-label'>Îzaha maneyî</label>").append($izah))
.toggle(offerIzah);
$fields.append($textarea);
// The template form edits one call inside the line, so its result is spliced back into the rest.
function currentGloss() {
if (tplForm) {
return $.trim(originalGloss.slice(0, tplCall.start) + tplForm.values() + originalGloss.slice(tplCall.end));
}
return joinGloss($.trim($textarea.val()), $.trim($izah.val()));
}
function showRaw() {
tplForm = null;
tplCall = null;
$fields.empty().append($textarea);
$autoLink.show();
$toRaw.hide();
}
loadSense().then(
function (fields) {
originalGloss = fields.gloss;
// An existing gloss that already ends in {{îzah|…}} comes apart into the two boxes, so
// editing it does not mean editing template syntax by hand.
var parts = util.splitTercimeIzah(fields.gloss);
$textarea.val(parts ? $.trim(parts.tercime) : fields.gloss)
.prop("disabled", false)
// A non-Kurdish gloss is a translation; the box under it takes the explanation.
.attr("placeholder", offerIzah ? util.msg.tercimePrompt : "Maneyê binivîse…");
if (parts) {
$izah.val($.trim(parts.izah));
$izahWrap.show();
}
$autoLink.prop("disabled", false);
var target = glossTemplateTarget(fields.gloss);
if (!target) {
return;
}
fetchTemplateData(target.name).then(function (td) {
// The fetch is async; leave the raw box alone if it has already been typed into.
if ($.trim($textarea.val()) !== $.trim(originalGloss)) {
return;
}
tplCall = target;
tplForm = cite.buildTemplateParamForm(target, td);
$fields.empty().append(tplForm.$element);
$autoLink.hide(); // linkifying a bare template call makes no sense
// A gloss that is one template call carries no separate explanation.
$izahWrap.hide();
$toRaw.show();
});
},
function (codeOrAbort, result) {
$textarea.attr("placeholder", "");
var message = codeOrAbort instanceof SubmitAbort ? codeOrAbort.message : "Nikare maneya heyî bar bike: " + apiErrorMessage(codeOrAbort, result);
$status.removeClass("dsb-status-ok").addClass("dsb-status-error").text(message);
}
);
$autoLink.on("click", function () {
var mane = $.trim($textarea.val());
var izah = $.trim($izah.val());
if (!mane && !izah) {
return;
}
$autoLink.prop("disabled", true);
$status.removeClass("dsb-status-ok dsb-status-error").text("Tê lînkkirin…");
linkifyGlossParts(mane, izah, function (linkedMane, linkedIzah, error) {
$textarea.val(linkedMane);
$izah.val(linkedIzah);
$autoLink.prop("disabled", false);
if (error) {
$status.addClass("dsb-status-error").text(error);
} else {
$status.text("");
}
});
});
$toRaw.on("click", function (e) {
e.preventDefault();
$textarea.val(currentGloss()); // read the form before showRaw() discards it
showRaw();
});
getters.gloss = function () {
var v = currentGloss();
return (v && v !== originalGloss) ? v : null;
};
return $("<div class='dsb-adv-group'></div>").append(
$("<div class='dsb-adv-label'></div>").text("Mane"),
$fields,
$izahWrap,
$("<div class='dsb-form-actions'></div>").append($autoLink, $toRaw, $status)
);
}
// -- one SENSE_GROUPS word-list box (Hevmane/Dijmane/etc.) --
function addWordField(group) {
var field = buildWordListField(group.label + "…");
loadSense().then(function (fields) {
var current = fields.words[group.key];
if (current && current.length) {
field.setValues(current);
}
});
getters["word:" + group.key] = function () {
return field.values();
};
// The hint is senseGroups' own, so the box carries the same example sentence the wizard puts
// beside its copy of this list.
var $label = $("<div class='dsb-adv-label'></div>").text(group.label);
if (group.hint) {
$label.append($("<span class='dsb-adv-hint'></span>").text(group.hint));
}
return $("<div class='dsb-adv-group'></div>").append($label, field.$element);
}
// -- maneid (single value; re-adding one that already exists is rejected -- see applyAdvancedChanges) --
function addNewSenseField() {
var $textarea = $("<textarea class='dsb-input' rows='2'></textarea>").attr("placeholder", "Maneya nû binivîse…");
getters.newSense = function () {
return $.trim($textarea.val());
};
return $("<div class='dsb-adv-group'></div>").append(
$("<div class='dsb-adv-label'></div>").text(util.msg.anotherSense),
$textarea
);
}
function addManeidField() {
var $input = $("<input type='text' class='dsb-input' placeholder='id…'>");
loadSense().then(function (fields) {
$input.val(fields.maneid);
});
getters.maneid = function () {
return $.trim($input.val());
};
return $("<div class='dsb-adv-group'></div>").append($("<div class='dsb-adv-label'></div>").text("Maneid"), $input);
}
// -- etîket / kategorî --
function addEtiketField() {
var field = buildTagField("Etîket…");
loadSense().then(function (fields) {
field.setValues(fields.etiket);
});
getters.etiket = function () {
return field.values();
};
return $("<div class='dsb-adv-group'></div>").append($("<div class='dsb-adv-label'></div>").text("Etîket"), field.$element);
}
function addKategoriField() {
var field = buildTagField("Kategorî…");
loadSense().then(function (fields) {
field.setValues(fields.kategori);
});
getters.kategori = function () {
return field.values();
};
return $("<div class='dsb-adv-group'></div>").append($("<div class='dsb-adv-label'></div>").text("Kategorî"), field.$element);
}
// -- the picker row: the gloss and Nimûne/jêgirtin links sit directly in the row; every word group and
// the rest live behind a "Hevmane û Bêhtir" dropdown. Clicking any link builds and appends its box
// to $openBoxes and hides that link; the box's own "×" removes it, clears its getter, and un-hides the
// link -- several boxes can be open at once. No "Beşek (====)" entry here: buildSubsectionPanel()'s
// own "+ Beşek lê zêde bike" trigger already covers page-level subsections. ----
var FIELD_DEFS = [{ key: "gloss", label: "Maneyê biguhêre", build: addGlossField, primary: true }]
.concat(SENSE_GROUPS.map(function (group) {
return { key: "word:" + group.key, label: group.label, build: function () { return addWordField(group); }, primary: false };
}))
.concat([
{ key: "maneid", label: "Maneid", build: addManeidField, primary: false },
{ key: "etiket", label: "Etîket", build: addEtiketField, primary: false },
{ key: "kategori", label: "Kategorî", build: addKategoriField, primary: false }
]);
// Every sense offers this: the new one lands directly below whichever sense asked for it (see
// insertNewSense), so it is no longer only the last one that can add to the end.
FIELD_DEFS.push({ key: "newSense", label: util.msg.addAnotherSense, build: addNewSenseField, primary: false });
var $picker = $("<div class='dsb-field-picker'></div>");
var $moreMenu = $("<div class='dsb-more-menu'></div>").hide();
var $openBoxes = $("<div class='dsb-open-boxes'></div>");
// $actions (built below) stays hidden until at least one field box is actually open -- otherwise
// "Lê zêde bike" sits there enabled with nothing to queue, which just produces an empty no-op change.
var openBoxCount = 0;
function updateActionsVisibility() {
$actions.toggle(openBoxCount > 0);
}
function makeFieldLink(def) {
var $link = $("<a href='#' class='dsb-quick-link dsb-field-pick'></a>").text(def.key === "gloss" ? def.label : "+ " + def.label);
$link.on("click", function (e) {
e.preventDefault();
openQueueBox();
var $box = def.build();
var $remove = $("<a href='#' class='dsb-box-remove' title='Rake'>×</a>");
$remove.on("click", function (e2) {
e2.preventDefault();
delete getters[def.key];
$box.remove();
$link.show();
openBoxCount--;
updateActionsVisibility();
});
$box.prepend($remove);
$openBoxes.append($box);
util.initManualResize($box);
$link.hide();
$moreMenu.slideUp();
openBoxCount++;
updateActionsVisibility();
});
return $link;
}
// Destructive, so it lives at the bottom of the dropdown behind a confirmation, and shares the
// sense's queue key: deleting supersedes any edit still pending on the same sense.
function makeDeleteLink() {
var $link = $("<a href='#' class='dsb-quick-link dsb-field-delete'>Maneyê rake</a>");
$link.on("click", function (e) {
e.preventDefault();
$moreMenu.slideUp();
OO.ui.confirm("Ev mane û hemû rêzên bin wê dê werin rakirin. Dewam bike?").done(function (ok) {
if (!ok) {
return;
}
queueChange(
placeLabel(language, sectionType, $h3),
"maneyek hate rakirin",
function (baseContent) {
return removeSense(baseContent, language, sectionType, sectionOrdinal, definitionLevel);
},
$save,
$note,
function () {
$li.addClass("dsb-sense-doomed");
return function () {
$li.removeClass("dsb-sense-doomed");
};
},
{ key: senseQueueKey() }
);
$note.show();
});
});
return $link;
}
// Only the links the row actually shows are built now; the dropdown's nine are 43% of the work and
// nobody sees them until "Hevmane û Bêhtir" is clicked, so buildMenuLinks() waits for that.
FIELD_DEFS.forEach(function (def) {
if (!def.primary) {
return;
}
$picker.append(makeFieldLink(def));
if (def.key === "gloss") {
$picker.append($nimune); // directly after "Maneyê biguhêre"
}
});
// Three sections, ruled apart: what labels this sense, the word lists that hang off it, and adding
// another sense. "Maneyê rake" is appended after these by makeDeleteLink() and so closes the last
// one. The word lists follow senseGroups' "order" -- that array's own order is the order the "#:"
// lines are written to the page in, which is not a reading order; the wizard's boxes read the same
// field, so both gadgets list them alike. Anything not named here still appears, after these, so a
// field added later is never silently dropped from the menu.
var MENU_GROUPS = [
["etiket", "kategori", "maneid"],
SENSE_GROUPS.slice().sort(function (a, b) {
return (a.order || 0) - (b.order || 0);
}).map(function (g) {
return "word:" + g.key;
}),
["newSense"]
];
var menuBuilt = false;
function buildMenuLinks() {
if (menuBuilt) {
return;
}
menuBuilt = true;
var byKey = {};
FIELD_DEFS.forEach(function (def) {
if (!def.primary) {
byKey[def.key] = def;
}
});
var placed = {};
MENU_GROUPS.forEach(function (group, groupIndex) {
var defs = group.map(function (key) {
return byKey[key];
}).filter(Boolean);
defs.forEach(function (def, i) {
placed[def.key] = true;
var $link = makeFieldLink(def);
// The rule goes on the first entry of every group after the first.
if (groupIndex > 0 && i === 0) {
$link.addClass("dsb-menu-group-start");
}
$moreMenu.append($link);
});
});
FIELD_DEFS.forEach(function (def) {
if (!def.primary && !placed[def.key]) {
$moreMenu.append(makeFieldLink(def));
}
});
$moreMenu.append(makeDeleteLink());
}
// Lives inside $picker (not appended straight to $quickRow) so .dsb-more-menu's CSS can position it as
// an overlay anchored to $picker, instead of pushing $openBoxes/the rest of the page down when it opens.
var $moreArrow = $("<span class='dsb-more-arrow'></span>").text("▸");
var $moreToggle = $("<a href='#' class='dsb-quick-link dsb-more-toggle'></a>")
.text(util.msg.moreMenu + " ").append($moreArrow);
$moreToggle.on("click", function (e) {
e.preventDefault();
buildMenuLinks();
$moreMenu.slideToggle(function () {
$moreArrow.text($moreMenu.is(":visible") ? "▾" : "▸");
});
});
$picker.append($moreToggle, $moreMenu);
// Outside clicks are handled by one shared document listener (see closeMoreMenus), not one per panel:
// hovering a sense builds its panel, so a long entry would otherwise accumulate dozens of them.
$moreMenu.data("dsbArrow", $moreArrow);
// -- combined save: whatever boxes are open right now, queued together via queueChange(); the actual
// save happens when the user clicks "Guhartinan qeyd bike" on the floating queue box. ----
// Small "not saved yet" preview of everything this queued change will do, appended right under the
// sense's gloss (plain text, not a real render of {{hevmane}}/etc.).
function collectChanges() {
var base = original || { maneid: "", etiket: [], kategori: [], words: {} };
var changed = {};
var words = {};
SENSE_GROUPS.forEach(function (group) {
var getter = getters["word:" + group.key];
words[group.key] = getter ? getter() : [];
if (getter && !sameList(words[group.key], base.words[group.key])) {
changed["word:" + group.key] = true;
}
});
var maneid = getters.maneid ? getters.maneid() : "";
var etiket = getters.etiket ? getters.etiket() : [];
var kategori = getters.kategori ? getters.kategori() : [];
if (getters.maneid && maneid !== (base.maneid || "")) {
changed.maneid = true;
}
if (getters.etiket && !sameList(etiket, base.etiket)) {
changed.etiket = true;
}
if (getters.kategori && !sameList(kategori, base.kategori)) {
changed.kategori = true;
}
return {
gloss: getters.gloss ? getters.gloss() : null,
newSense: getters.newSense ? getters.newSense() : null,
maneid: maneid,
etiket: etiket,
kategori: kategori,
words: words,
changed: changed
};
}
// Both the field save and "Maneyê rake" queue under this, so the later one supersedes the earlier.
function senseQueueKey() {
return "sense:" + sectionType + "|" + sectionOrdinal + "|" + definitionLevel.join(".");
}
var $save = $("<button type='button' class='dsb-save'></button>").text(util.msg.add);
// Where queueChange() writes its confirmation for actions that have no box of their own.
var $note = $("<div class='dsb-status dsb-quick-note'></div>").hide();
var $cancel = $("<button type='button' class='dsb-cancel'></button>").text(util.msg.cancel);
var $status = $("<span class='dsb-status'></span>");
var $actions = $("<div class='dsb-form-actions'></div>").append($save, $cancel, $status).hide();
$cancel.on("click", function () {
// Close the boxes too, not just the row. Hiding alone left every field still open with its
// text in it, so the next click on the pencil reopened the half-filled panel that had just
// been cancelled. Each box's own "x" already clears its getter, restores its link and updates
// the counter, so those are triggered rather than repeating the teardown here.
$openBoxes.find(".dsb-box-remove").trigger("click");
$note.hide().text("");
// dsb.js owns the pencil's pinned state; tell it rather than hiding behind its back, or the
// next click on the pencil would only toggle "pinned" off and appear to do nothing.
$quickRow.trigger("dsbCancel");
hideQueueIfIdle();
});
$save.on("click", function () {
var changes = collectChanges();
function afterLinkify() {
var parts = senseChangeLabels(changes);
queueChange(
placeLabel(language, sectionType, $h3),
parts.length
? parts.join(", ") + (parts.length > 1 ? " hatin guhartin" : " hate guhartin")
: "mane hate guhartin",
function (baseContent) {
return applyAdvancedChanges(baseContent, language, sectionType, sectionOrdinal, definitionLevel, changes);
},
$save,
$status,
function () {
var undoPreview = previewSense($li, language, sectionType, sectionOrdinal,
definitionLevel);
// A new sense has no place on the page yet, so it gets a row of its own directly
// after this one -- the list numbers it, so where it will land is visible.
var $newSense = null;
if (changes.newSense) {
$newSense = $("<li class='dsb-sense-preview'></li>");
$li.after($newSense);
fillWithRendered($newSense, changes.newSense, function () {
$newSense.text(changes.newSense);
});
}
return function () {
undoPreview();
if ($newSense) {
$newSense.remove();
}
};
},
// Re-saving this same sense supersedes its pending change rather than queuing another.
{ key: senseQueueKey() }
);
}
$save.prop("disabled", true);
// Auto-linkify the gloss as part of this save, but only a half nobody has linked by hand.
// Split back into its two halves first, so the explanation is linked on its own rather than
// sitting unseen inside {{îzah|…}}.
if (changes.gloss) {
$status.removeClass("dsb-status-ok dsb-status-error").text("Tê lînkkirin…");
var glossParts = util.splitTercimeIzah(changes.gloss);
linkifyGlossParts(
glossParts ? $.trim(glossParts.tercime) : changes.gloss,
glossParts ? $.trim(glossParts.izah) : "",
function (linkedMane, linkedIzah, error) {
// The change is queued either way, so a gloss that came back empty is not written
// back -- that would save the sense with its wording gone.
var joined = joinGloss(linkedMane, linkedIzah);
if ($.trim(joined)) {
changes.gloss = joined;
}
if (error) {
$status.removeClass("dsb-status-ok").addClass("dsb-status-error").text(error);
}
afterLinkify();
},
true
);
} else {
afterLinkify();
}
});
// The links sit under the buttons, so each open box pushes them down rather than leaving them far above.
$quickRow.empty().append($openBoxes, $actions, $picker, $note);
}
// Attaches the pencil toggle to one leaf definition <li> -- clicking it reveals the "guhartina pêşketî"
// (advanced editing) panel built by buildAdvancedPanel() above.
// ---- per-section UI: "+ Beşek lê zêde bike" after a POS block's own content ----
// Reads the header text of "====" subsections already rendered under this POS heading, straight from the DOM.
// The subsections of a part of speech are the headings one deeper than it is.
function subsectionDepthFor($h3) {
return (sections.headingDepth($h3) || 3) + 1;
}
function isSubsectionHeading($el, depth) {
return sections.headingDepth($el) === depth;
}
function existingSubsectionHeadersInDom($h3) {
var depth = subsectionDepthFor($h3);
var headers = [];
for (var $cursor = headingOuter($h3).next(); $cursor.length; $cursor = $cursor.next()) {
var here = sections.headingDepth($cursor);
if (here && here < depth) {
break; // out of this part of speech
}
if (isSubsectionHeading($cursor, depth)) {
var $hh = $cursor.is("h2, h3, h4, h5, h6") ? $cursor : $cursor.find("h2, h3, h4, h5, h6").first();
headers.push(headingText($hh));
}
}
return headers;
}
// Finds where a new "====" subsection preview belongs in the live DOM, using the same SUBSECTION_ORDER rank
// insertPageSubsection() uses. Returns {insertBefore: $el} (a ranked subsection that outranks this one) or
// {insertAfter: $el} (goes at the end of the block).
function domSubsectionInsertAnchor($h3, header) {
var depth = subsectionDepthFor($h3);
var myRank = subsectionRankForHeader(header);
var $last = headingOuter($h3);
for (var $cursor = headingOuter($h3).next(); $cursor.length; $cursor = $cursor.next()) {
var here = sections.headingDepth($cursor);
if (here && here < depth) {
break; // out of this part of speech
}
if (isSubsectionHeading($cursor, depth)) {
var $hh = $cursor.is("h2, h3, h4, h5, h6") ? $cursor : $cursor.find("h2, h3, h4, h5, h6").first();
var otherRank = subsectionRankForHeader(headingText($hh));
if (otherRank !== -1 && otherRank > myRank) {
return { insertBefore: $cursor };
}
}
$last = $cursor;
}
return { insertAfter: $last };
}
// The wikitext one subsection's content becomes, exactly as insertPageSubsection() writes it -- so the
// preview parses the same string the save will produce.
function subsectionBodyWikitext(language, s, content) {
if (s.kind !== "wordlist") {
return content;
}
return content.filter(function (group) {
return group.words.length;
}).map(function (group) {
return buildWordListCall(WORDLIST_TEMPLATE, language,
withNamedParam(["cure=" + s.header], "sernav", group.sernav), group.words);
}).join("\n\n");
}
// The plain word list this preview falls back to when the parse fails: the values that were typed,
// without the template around them. Right, but not what the page will look like.
function subsectionPlainDom($wrap, s, content) {
if (s.kind !== "wordlist") {
$wrap.append($("<p class='dsb-preview-text'></p>").text(content));
return;
}
content.forEach(function (group) {
if (group.sernav) {
$wrap.append($("<div class='dsb-preview-subheading'></div>").text(group.sernav));
}
var $list = $("<ul class='dsb-preview-list'></ul>");
group.words.forEach(function (w) {
$list.append($("<li></li>").text(w.replace(/<[^<>]*>/g, "")));
});
$wrap.append($list);
});
}
// A "not saved yet" preview of a page-level "====" subsection, dropped into the live page at the
// position the save will put it, with its body rendered the way the page renders it -- real {{kol3}}
// columns and real links, not a list of the words that went into them. Returns the inserted element so
// the caller can remove it again.
function buildSubsectionPreviewDom($h3, s, content, language) {
var $wrap = $("<div class='dsb-preview-block'></div>");
// A styled <div>, not a real <h4>, so existingSubsectionHeadersInDom() never mistakes this live-only
// preview for an actual subsection already on the page.
$wrap.append($("<div class='dsb-preview-heading' role='heading' aria-level='4'></div>").text(s.header));
$wrap.append($("<span class='dsb-preview-badge'></span>").text("nû — hîn nehatiye qeydkirin"));
// Placed before it is filled: a cached render resolves almost at once, and filling a wrap that is
// still detached would announce content nobody can see.
var anchor = domSubsectionInsertAnchor($h3, s.header);
if (anchor.insertBefore) {
anchor.insertBefore.before($wrap);
} else {
anchor.insertAfter.after($wrap);
}
var wikitext = language ? subsectionBodyWikitext(language, s, content) : "";
if ($.trim(wikitext)) {
var $body = $("<div class='dsb-preview-rendered'></div>").appendTo($wrap);
fillWithRendered($body, wikitext, function () {
$body.remove();
subsectionPlainDom($wrap, s, content);
});
} else {
subsectionPlainDom($wrap, s, content);
}
return $wrap;
}
// A free section owns what lies between its own heading and the next heading of any depth, matching
// the range locateHeadingByText() edits. Returns a function that puts the page back, or null.
function replaceFreeSectionInPlace($heading, wikitext) {
var $start = headingOuter($heading);
var body = [];
for (var $cursor = $start.next(); $cursor.length; $cursor = $cursor.next()) {
if (sections.headingDepth($cursor)) {
break;
}
if (!$cursor.is(".dsb-subsection-panel, .dsb-convert-pos-panel, .dsb-preview-block, .dsb-sense-preview")) {
body.push($cursor.get(0));
}
}
if (!body.length) {
return null;
}
var $slot = $("<div class='dsb-sense-preview'></div>").insertAfter($start);
var $body = $(body).detach();
fillWithRendered($slot, wikitext, function () {
$slot.remove();
$slot = null;
$body.insertAfter($start);
});
return function () {
if ($slot) {
$body.insertAfter($start);
$slot.remove();
$slot = null;
}
};
}
/**
* Redraws a subsection that is already on the page with the content this edit would give it.
*
* The section's own nodes -- everything between its heading and the next heading of the same rank or
* higher -- are detached rather than dropped, so the returned function puts the page back exactly as it
* was when the change is cancelled or superseded.
*/
function replaceSubsectionInPlace($h3, header, wikitext) {
var depth = subsectionDepthFor($h3);
// Walked the same way existingSubsectionHeadersInDom() walks it, so "which nodes are this
// subsection" is decided once and the same way everywhere.
var $start = null;
var body = [];
for (var $cursor = headingOuter($h3).next(); $cursor.length; $cursor = $cursor.next()) {
var here = sections.headingDepth($cursor);
if (here && here < depth) {
break; // out of this part of speech
}
if (isSubsectionHeading($cursor, depth)) {
if ($start) {
break; // the next subsection starts here
}
var $hh = $cursor.is("h2, h3, h4, h5, h6") ? $cursor : $cursor.find("h2, h3, h4, h5, h6").first();
if (headingText($hh) === header) {
$start = $cursor;
}
continue;
}
// This gadget's own furniture stays where it is: the subsection's editing panel is the sibling
// right after its heading, so sweeping it into the body would detach the very panel the change
// was made in and leave the pencil toggling an element that is no longer on the page.
if ($start && !$cursor.is(".dsb-subsection-panel, .dsb-convert-pos-panel, .dsb-preview-block, .dsb-sense-preview")) {
body.push($cursor.get(0));
}
}
if (!$start || !body.length) {
return null;
}
var $slot = $("<div class='dsb-sense-preview'></div>").insertAfter($start);
var $body = $(body).detach();
fillWithRendered($slot, wikitext, function () {
$slot.remove();
$slot = null;
$body.insertAfter($start);
});
return function () {
if ($slot) {
$body.insertAfter($start);
$slot.remove();
$slot = null;
}
};
}
/**
* The live DOM one "place" section occupies: {nodes, insert}. nodes are what a render replaces --
* empty when the section is not on the page yet -- and insert() puts an element where the section
* belongs, so a new one appears exactly where the save will put it.
*/
// This gadget's own panels sit between a heading and the page's content, so every walk steps over
// them rather than counting one as content.
function isOwnFurniture($el) {
return $el.is(".dsb-toolbar, .dsb-quick-row, .dsb-subsection-panel, .dsb-convert-pos-panel," +
" .dsb-preview-block, .dsb-sense-preview");
}
function loosePlaceDom($h3, subsection) {
var place = subsection.place;
var nodes = [];
var $cursor;
if (place === "page-top") {
// Şablon:herwiha binêre renders as <div class="rellink binere"> at the head of the content.
var $out = $("#mw-content-text .mw-parser-output").first();
var $hat = $out.children(".binere, .rellink").first();
if ($hat.length) {
return { nodes: $hat.get(), insert: function ($el) { $hat.before($el); } };
}
return { nodes: [], insert: function ($el) { $out.prepend($el); } };
}
var $start = headingOuter($h3);
if (place === "pron") {
// A "===" heading above this POS block, under the language one.
for (var $back = $start.prev(); $back.length; $back = $back.prev()) {
if ($back.is(".mw-heading2, h2")) {
break;
}
if ($back.is(".mw-heading3, h3")) {
var $hh = $back.is("h3") ? $back : $back.find("h3").first();
if (headingText($hh) === subsection.header) {
for ($cursor = $back.next();
$cursor.length && !$cursor.is(".mw-heading2, .mw-heading3, h2, h3");
$cursor = $cursor.next()) {
if (!isOwnFurniture($cursor)) {
nodes.push($cursor.get(0));
}
}
return { nodes: nodes, insert: function ($el) { $back.after($el); } };
}
}
}
return { nodes: [], insert: function ($el) { $start.before($el); } };
}
if (place === "pos-top") {
// The thumbnails between the heading and the headword line.
for ($cursor = $start.next(); $cursor.length; $cursor = $cursor.next()) {
if (isOwnFurniture($cursor)) {
continue;
}
if (!$cursor.is(".thumb, figure, .floatright, .floatleft")) {
break;
}
nodes.push($cursor.get(0));
}
return { nodes: nodes, insert: function ($el) { $start.after($el); } };
}
return null;
}
// The wikitext a "place" section's pending content renders as. A word list is its one call; everything
// else is already the body that will be written.
function loosePlacePreviewWikitext(subsection, content, language) {
return subsection.kind === "wordlist"
? wt.buildHbnrCall(loosePlaceWords(content))
: loosePlaceBody(subsection, content, language);
}
// Draws a section's headword line from wikitext where the page's own line stands, or above the
// definitions when the section has none. The page's line is detached rather than dropped, so the
// returned function puts it back.
function replaceHeadwordInPlace($h3, wikitext) {
if (!$.trim(wikitext || "")) {
return null;
}
var $old = $();
var $at = $();
for (var $cursor = headingOuter($h3).next(); $cursor.length; $cursor = $cursor.next()) {
if ($cursor.is("p")) {
$old = $cursor;
break;
}
if (sections.headingDepth($cursor) || $cursor.is("ol")) {
$at = $cursor;
break;
}
}
var $slot = $("<div class='dsb-sense-preview'></div>");
if ($old.length) {
$old.before($slot);
} else if ($at.length) {
$at.before($slot);
} else {
return null;
}
var $body = $old.detach();
function restore() {
if (!$slot) {
return;
}
$body.insertBefore($slot);
$slot.remove();
$slot = null;
}
fillWithRendered($slot, wikitext, restore);
return restore;
}
// Redraws one of those places where it stands, the same way replaceSubsectionInPlace() redraws a
// "====" one. The page's own nodes are detached rather than dropped, so cancelling puts them back.
function replaceLoosePlaceInPlace($h3, subsection, wikitext) {
if (!$.trim(wikitext || "")) {
return null;
}
var at = loosePlaceDom($h3, subsection);
if (!at) {
return null;
}
var $slot = $("<div class='dsb-sense-preview'></div>");
at.insert($slot);
var $body = $(at.nodes).detach();
function restore() {
if (!$slot) {
return;
}
$body.insertBefore($slot);
$slot.remove();
$slot = null;
}
fillWithRendered($slot, wikitext, restore);
return restore;
}
// Builds the subsection picker + its word-list/text field. A subsection that isn't on the page yet is saved
// via insertPageSubsection(); one that already is gets prefilled and saved via updatePageSubsection().
// onlyHeader pins the panel to a single subsection (the per-heading pencil); omit it for the full picker.
// free pins the panel to a heading that is not a subsection of a part of speech, located by its own
// text: {header, ordinal}.
function buildSubsectionPanel($panel, $h3, language, sectionType, sectionOrdinal, onlyHeader, free) {
var existing = existingSubsectionHeadersInDom($h3);
// A heading the data does not name is still editable. It is declared a word list so the reader
// tries, in order, its {{kol3}} calls and then a plain bullet list; anything else falls through to
// raw wikitext, which is what prefill() does with a list it cannot read.
// Hidden counts as unknown here: the picker does not offer to build a Werger box, but one already
// on the page still opens, as the wikitext it is.
var unknown = onlyHeader && !PAGE_SUBSECTIONS.some(function (s) {
return s.header === onlyHeader && !s.hidden;
});
// A free section starts as wikitext: what it holds is only known once it has been read, and
// prefill() swaps in the {{kol3}} form there if it turns out to be a list.
var available = free ? [{
header: onlyHeader,
id: "free",
kind: "text",
free: free,
existing: true
}] : unknown ? [{
header: onlyHeader,
id: "unknown",
kind: "wordlist",
unknown: true,
existing: true
}] : PAGE_SUBSECTIONS.filter(function (s) {
// "hidden" takes a section out of the picker without removing it: Werger waits for the
// translation tools it belongs to, and offering a box those cannot yet fill misleads.
// Werger also carries lang: "ku" -- it is a Kurdish-entry section, so it is not offered
// elsewhere either.
return !s.hidden && (!s.lang || s.lang === language) &&
(!onlyHeader || s.header === onlyHeader);
}).map(function (s) {
return $.extend({}, s, { existing: existing.indexOf(s.header) !== -1 });
});
if (!available.length) {
$panel.text("Ji bo vê beşê guhartin tune.");
return;
}
var $select = $("<select class='dsb-subsection-select'></select>");
if (!onlyHeader) {
// Label-less on purpose: it holds the "nothing chosen yet" value without becoming a row in the
// list (makeSearchable() skips a blank option that has no label), so the prompt lives in the
// input's placeholder instead.
$select.append($("<option value=''></option>"));
}
available.forEach(function (s) {
// A "place" entry has no "====" heading of its own, so it is marked rather than left looking
// like the subsections around it.
var label = s.place ? s.header + " (ne binebeş e)"
: (s.existing ? s.header + " (biguhêre)" : s.header);
$select.append($("<option></option>").val(s.id).text(label));
});
var $selectWrap = makeSearchable($select, "Binebeşek hilbijêre");
if (onlyHeader) {
$selectWrap.hide(); // pinned to one subsection: nothing to choose
}
var $fieldWrap = $("<div class='dsb-subsection-field'></div>");
var $save = $("<button type='button' class='dsb-save'></button>").text(util.msg.add);
var $cancel = $("<button type='button' class='dsb-cancel'></button>").text(util.msg.cancel);
var $status = $("<span class='dsb-status'></span>");
var $actions = $("<div class='dsb-form-actions'></div>").append($save, $cancel, $status);
$cancel.on("click", function () {
resetForm();
$panel.hide();
hideQueueIfIdle();
});
// Undefined while the placeholder option is selected.
function currentSubsection() {
var id = $select.val();
return id ? available.filter(function (s) {
return s.id === id;
})[0] : undefined;
}
// One entry per "{{kol3}}" group in the current wordlist section; empty for any other kind.
var groupFields = [];
var $groups = null;
var editAsText = false; // set when an existing wordlist section turns out to be unparseable
// Whatever has been typed into each subsection's box, so switching the picker away and back restores it
// instead of rebuilding an empty field. Keyed by subsection id; lastId is whichever box is on screen.
var drafts = {};
var lastId = null;
// Called before renderField() rebuilds, while the group fields still hold the outgoing box.
function captureDraft() {
if (!lastId) {
return;
}
drafts[lastId] = imagesField
? { asText: false, images: imagesField.values() }
: (groupFields.length
? { asText: false, groups: readGroups() }
: { asText: editAsText, text: fieldText() });
}
function restoreDraft(s, draft) {
// An existing wordlist we previously fell back to raw wikitext for stays in that mode.
if (draft.asText && s.kind === "wordlist") {
editAsText = true;
groupFields = [];
renderTextArea(s, 6);
}
if (imagesField) {
imagesField.setValues(draft.images);
} else if (groupFields.length) {
setGroups(s, draft.groups);
} else {
setFieldText(draft.text);
}
}
// Etîmolojî is edited on a live surface (see renderChipSurface) rather than in a textarea, so every
// read and write of the field goes through these two instead of touching .dsb-subsection-text.
var chipSurface = null;
function fieldText() {
if (wergerForm) {
return buildWergerBlock(wergerForm.read());
}
return chipSurface
? $.trim(readChipSurface(chipSurface.get(0)))
: $.trim($fieldWrap.find(".dsb-subsection-text").val() || "");
}
function setFieldText(value) {
if (wergerForm) {
wergerForm.set(parseWergerBlock(value));
return;
}
if (chipSurface) {
renderChipSurface(chipSurface, value || "");
var schedule = chipSurface.data("dsbSchedulePreview");
if (schedule) {
schedule(); // show what the loaded etymology renders as
}
} else {
$fieldWrap.find(".dsb-subsection-text").val(value || "");
}
}
// The contenteditable surface: text typed directly, template calls as chips that open the dialog,
// plus start.js's etymology templates in a searchable insert menu.
// A live surface where a template shows as a chip, with a menu that inserts one. Etîmolojî and
// Bilêvkirin are both written this way; they differ only in which templates the menu offers.
function renderTemplateEditor(s, templates) {
$fieldWrap.empty();
var $select = $("<select class='dsb-subsection-select'></select>");
$select.append($("<option value=''></option>"));
templates.forEach(function (t) {
$select.append($("<option></option>").val(t.snippet).text(t.name + " — " + t.desc));
});
var $surface = $("<div class='dsb-tpl-surface' contenteditable='true'></div>")
.attr("role", "textbox")
.attr("aria-multiline", "true");
chipSurface = $surface;
// Clicking the insert menu moves focus out of the surface and the browser forgets the caret, so
// the last caret position inside the surface is kept and restored before inserting -- otherwise
// focus() drops it back to the very start and every template lands at the beginning of the line.
var savedRange = null;
function rememberCaret() {
var sel = window.getSelection();
if (!sel || !sel.rangeCount) {
return;
}
var range = sel.getRangeAt(0);
var root = $surface.get(0);
if (range.commonAncestorContainer === root || $.contains(root, range.commonAncestorContainer)) {
savedRange = range.cloneRange();
}
}
$surface.on("keyup mouseup focusout", rememberCaret);
function restoreCaret() {
if (!savedRange) {
return;
}
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(savedRange);
}
$select.on("change", function () {
var snippet = $select.val();
if (!snippet) {
return;
}
// Same substitution start.js makes: CODE is this section's language, SOURCELANG is left for
// the user to fill in.
snippet = snippet.replace(/CODE/g, language).replace(/SOURCELANG/g, "?");
// A pronunciation template is a bullet of its own ("* {{ku-IPA}}"), so it starts a line
// rather than landing in the middle of one.
var ownLine = /^\*\s/.test(snippet);
if (ownLine) {
snippet = $.trim(snippet.replace(/^\*\s*/, ""));
}
$surface.focus();
restoreCaret();
var $chip = makeTemplateChip(snippet);
var piece = document.createDocumentFragment();
if (ownLine) {
// The bullet, and a line of its own unless the caret already starts one.
var at = caretOffset();
var before = at >= 0 ? readChipSurface($surface.get(0)).slice(0, at) : "";
piece.appendChild(document.createTextNode(!before || /\n$/.test(before) ? "* " : "\n* "));
}
piece.appendChild($chip.get(0));
insertAtCaret($surface, piece);
rememberCaret(); // so a second insert lands after the first, not back at the old spot
$select.val("");
// Straight into the dialog: the snippet is placeholders ("?", "…") until someone fills it in.
cite.openTemplateDialog(snippet).then(function (edited) {
applyChipEdit($chip, edited);
syncPanelWidth();
});
});
// Paste as plain text: pasted markup would otherwise become real nodes in the surface.
$surface.on("paste", function (e) {
var clip = e.originalEvent && e.originalEvent.clipboardData;
if (!clip) {
return;
}
e.preventDefault();
document.execCommand("insertText", false, clip.getData("text/plain"));
});
// The chip the dialog was opened for, or the one that has taken its place. renderChipSurface()
// rebuilds the surface from its wikitext whenever a call is typed in as text, which throws away
// the element this closure is holding; without this, "Jê bibe" would remove a node that is no
// longer on screen and appear to do nothing.
function liveChip($chip, wikitext) {
if ($.contains($surface.get(0), $chip.get(0))) {
return $chip;
}
return $surface.find(".dsb-tpl-chip").filter(function () {
return $(this).attr("data-wikitext") === wikitext;
}).first();
}
function applyChipEdit($chip, edited) {
if (edited === null || !$chip.length) {
return;
}
if (!$.trim(edited)) {
$chip.remove();
return;
}
// Edited into something that is no longer one template call: let it fall back to plain text
// rather than keeping a chip that lies about its contents.
var call = parseTemplateCalls(edited);
if (call.otherContent || call.calls.length !== 1) {
$chip.replaceWith(document.createTextNode(edited));
return;
}
$chip.replaceWith(makeTemplateChip(edited));
}
// Double-click edits the call's wikitext in place; single click opens the parameter dialog. The
// click is held back briefly so the first of a double-click does not open the dialog underneath.
var chipClickTimer = null;
var lastChipTap = 0;
var lastChipNode = null;
var DOUBLE_TAP_MS = 300;
// Double-tap drops the chip back to plain wikitext in the surface, rather than making the chip
// itself editable: a contenteditable inside a contenteditable gives Android a selection handle and
// no usable caret. As plain text the browser's own editing works, and chipifyTyped() puts the chip
// back once the caret leaves the call.
// A collapsed caret inside the text just inserted: at the character tapped where the browser can
// say which that is, otherwise just inside the closing braces so the caret stays within the call
// (which is what stops chipifyTyped putting the chip straight back).
function caretRangeIn(node, event) {
var range;
var x = event && event.clientX;
var y = event && event.clientY;
if (x !== undefined && document.caretRangeFromPoint) {
range = document.caretRangeFromPoint(x, y);
} else if (x !== undefined && document.caretPositionFromPoint) {
var pos = document.caretPositionFromPoint(x, y);
if (pos) {
range = document.createRange();
range.setStart(pos.offsetNode, pos.offset);
}
}
if (range && range.startContainer === node) {
range.collapse(true);
return range;
}
var text = node.nodeValue;
range = document.createRange();
range.setStart(node, /\}\}$/.test(text) ? text.length - 2 : text.length);
range.collapse(true);
return range;
}
function startInlineEdit($chip, event) {
if (!$.contains($surface.get(0), $chip.get(0))) {
return; // the tap timer and dblclick can both fire; only the first one has a chip left
}
var node = document.createTextNode($chip.attr("data-wikitext"));
$chip.replaceWith(node);
$surface.get(0).focus();
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(caretRangeIn(node, event));
}
// Bound before the chip's own handler and stopping there, so removing a call never opens the
// dialog for it on the way out.
$surface.on("click", ".dsb-tpl-chip-remove", function (e) {
e.preventDefault();
e.stopImmediatePropagation();
clearTimeout(chipClickTimer);
lastChipNode = null;
$(this).closest(".dsb-tpl-chip").remove();
syncPanelWidth();
});
$surface.on("click keydown", ".dsb-tpl-chip", function (e) {
if (e.type === "keydown" && e.which !== 13 && e.which !== 32) {
return;
}
e.preventDefault();
var $chip = $(this);
clearTimeout(chipClickTimer);
// A second tap on the same chip edits it inline. Timed here rather than left to "dblclick",
// which mobile browsers fire unreliably or not at all for a double-tap.
var now = new Date().getTime();
if (lastChipNode === this && now - lastChipTap < DOUBLE_TAP_MS) {
lastChipNode = null;
startInlineEdit($chip, e);
return;
}
lastChipTap = now;
lastChipNode = this;
var wikitext = $chip.attr("data-wikitext");
chipClickTimer = setTimeout(function () {
cite.openTemplateDialog(wikitext).then(function (edited) {
applyChipEdit(liveChip($chip, wikitext), edited);
syncPanelWidth();
});
}, DOUBLE_TAP_MS);
});
$surface.on("dblclick", ".dsb-tpl-chip", function (e) {
e.preventDefault();
clearTimeout(chipClickTimer);
startInlineEdit($(this), e);
});
// A long etymology cannot be read in the panel's usual 520px column, so the panel drops its
// width cap once there is enough text to need it. What it will look like is shown by the
// in-place render on the page itself, not by a second copy inside the form.
var WIDE_AT = 240; // characters
function syncPanelWidth() {
var wikitext = readChipSurface($surface.get(0));
if (wikitext.length > WIDE_AT || wikitext.indexOf("\n") !== -1) {
$surface.closest(".dsb-subsection-panel").addClass("dsb-panel-wide");
}
}
// A template typed by hand becomes a chip once typing settles, so it behaves like an inserted one:
// click for the parameter dialog, double-click for its raw wikitext.
var composing = false;
// The caret as an offset into the surface's wikitext, which is what survives rebuilding the DOM.
function caretOffset() {
var sel = window.getSelection();
var root = $surface.get(0);
if (!sel || !sel.rangeCount) {
return -1;
}
var range = sel.getRangeAt(0);
if (range.startContainer !== root && !$.contains(root, range.startContainer)) {
return -1; // caret is somewhere else on the page
}
var pre = document.createRange();
pre.selectNodeContents(root);
pre.setEnd(range.startContainer, range.startOffset);
var holder = document.createElement("div");
holder.appendChild(pre.cloneContents());
return readChipSurface(holder).length;
}
// Puts the caret back at that wikitext offset. Only runs straight after a rebuild, so the surface
// holds nothing but text nodes and chips.
function placeCaret(offset) {
var root = $surface.get(0);
var range = document.createRange();
var nodes = root.childNodes;
root.focus();
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i];
var len = node.nodeType === 3
? node.nodeValue.length
: ($(node).attr("data-wikitext") || $(node).text()).length;
if (offset <= len) {
if (node.nodeType === 3) {
range.setStart(node, offset);
} else {
range.setStartAfter(node); // a chip is atomic; the caret goes after it
}
break;
}
offset -= len;
}
if (!range.startContainer || range.startContainer === document) {
range.selectNodeContents(root);
}
range.collapse(true);
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
// True when the selection overlaps the inside of a template call. A caret resting exactly at the
// closing "}}" does not count -- that is someone who has just finished typing one.
function selectionTouchesCall(wikitext) {
var sel = window.getSelection();
if (!sel || !sel.rangeCount) {
return false;
}
var range = sel.getRangeAt(0);
var root = $surface.get(0);
if (range.startContainer !== root && !$.contains(root, range.startContainer)) {
return false;
}
var from = caretOffset();
var to = from;
if (!range.collapsed) {
var pre = document.createRange();
pre.selectNodeContents(root);
pre.setEnd(range.endContainer, range.endOffset);
var holder = document.createElement("div");
holder.appendChild(pre.cloneContents());
to = readChipSurface(holder).length;
}
return parseTemplateCalls(wikitext).calls.some(function (call) {
return from < call.end && to > call.start;
});
}
function chipifyTyped() {
if (composing) {
return; // never rebuild under an IME
}
var wikitext = readChipSurface($surface.get(0));
// Chips already hold one call each, so a higher call count means one was typed in as text.
if (parseTemplateCalls(wikitext).calls.length <= $surface.find(".dsb-tpl-chip").length) {
return;
}
var offset = caretOffset();
if (selectionTouchesCall(wikitext)) {
return; // still inside a call: leave it as text until the caret moves out
}
renderChipSurface($surface, wikitext);
savedRange = null; // the old range points at nodes that no longer exist
if (offset >= 0) {
placeCaret(offset);
rememberCaret();
}
}
$surface.on("compositionstart", function () {
composing = true;
});
$surface.on("compositionend", function () {
composing = false;
schedulePreview();
});
var previewTimer = null;
function schedulePreview() {
clearTimeout(previewTimer);
previewTimer = setTimeout(function () {
chipifyTyped();
syncPanelWidth();
}, 700);
}
$surface.on("input", schedulePreview);
$surface.data("dsbSchedulePreview", schedulePreview);
$fieldWrap.append(makeSearchable($select, util.msg.addTemplate), $surface);
// Bilêvkirin is written in IPA, so it gets the same character palette the wizard shows. Its
// characters go in as text and its two template buttons as chips, which is what the rest of
// this surface is made of.
if (s.place === "pron") {
$fieldWrap.append(util.buildIpaPalette({
insertText: function (text) {
$surface.focus();
restoreCaret();
insertAtCaret($surface, document.createTextNode(text));
rememberCaret();
schedulePreview();
},
insertTemplate: function (snippet) {
$surface.focus();
restoreCaret();
var $chip = makeTemplateChip(snippet.replace(/\|\s*\}\}$/, "}}"));
insertAtCaret($surface, $chip.get(0));
rememberCaret();
cite.openTemplateDialog($chip.attr("data-wikitext")).then(function (edited) {
applyChipEdit($chip, edited);
syncPanelWidth();
});
}
}));
}
}
// The three "place" sections are wikitext with a shape worth spelling out, since nothing on screen
// says what belongs in the box.
var PLACE_PLACEHOLDERS = {
"page-top": "{{h-bnr|Kelîme|Kelîmeyeke din}} an jî {{herwiha binêre/ane}}",
"pos-top": "[[Wêne:Navê pelî.jpg|thumb|Binnivîs]]",
"pron": "* {{ku-IPA}}"
};
function renderTextArea(s, rows) {
$fieldWrap.empty().append(
$("<textarea class='dsb-input dsb-subsection-text'></textarea>")
.attr("rows", rows)
.attr("placeholder", PLACE_PLACEHOLDERS[s.place] || (s.header + "…"))
);
util.initManualResize($fieldWrap);
}
// A translation box: the {{werger-ser}}/{{werger-bin}} pair, with the sense it covers and the
// layout parameters that template accepts. The rows in between are left to the translation editor
// gadget, which is what edits them on the rendered page.
// Reads and writes through fieldText()/setFieldText() as wikitext, so drafts, save and the queue
// need no special case beyond the append below.
var wergerForm = null;
var imagesField = null;
function renderWergerField(s) {
var inputs = {};
// Its own row class rather than .dsb-tpl-param-row: that one is styled in cite.css, which is
// only on the page when wikt.cite happens to be loaded, and without it the label falls back to
// sitting inline and a short one ("id") is overlapped by its own box.
function row(key, label, placeholder) {
inputs[key] = $("<input type='text' class='dsb-input'>").attr("placeholder", placeholder || "");
return $("<div class='dsb-werger-row'></div>").append(
$("<label class='dsb-adv-label'></label>").text(label).append(inputs[key])
);
}
// {{werger-ser}} also takes stûn, firehî, bgcolor, valign and align, but they only restyle the
// box and its defaults are what these pages use, so the form does not offer them. They are
// still in WERGER_NAMED, so a block that already sets one is read back correctly.
var $main = $("<div class='dsb-werger-main'></div>").append(
row("1", "Mane", "ava vexwarinê"),
row("2", "Mane no", "ne mecbûrî — di parantezê de xuya dike"),
row("id", "id", "ne mecbûrî — IDya tercimeyê")
);
var $note = $("<p class='dsb-werger-note'></p>").text(
"Qutiyeke Wergerê ya vala lê zêde dikî. Tercimeyan bi amûra wergerê dûre binivîse."
);
wergerForm = {
read: function () {
var out = {};
$.each(inputs, function (key, $input) {
var value = $.trim($input.val() || "");
if (value) {
out[key] = value;
}
});
return out;
},
set: function (params) {
$.each(inputs, function (key, $input) {
$input.val((params && params[key]) || "");
});
}
};
$fieldWrap.empty().append($note, $main);
}
// Same [[Kategorî:Çavkanî]] search widget start.js's own picker uses: choosing an entry inserts
// "* {{Target}}" at the cursor. The field itself stays the plain ".dsb-subsection-text" textarea
// every other "kind: text" subsection already uses, so save/prefill/drafts need no changes.
function renderCavkaniField(s) {
var $ta = $("<textarea class='dsb-input dsb-subsection-text'></textarea>")
.attr("rows", 4)
.attr("placeholder", util.msg.pickOrTypeTemplate);
var picker = cite.buildCategoryTemplateSearch({
placeholder: util.msg.addTemplate,
describe: true,
// Same param dialog Etîmolojî opens on a fresh insert: filled from the picked template's own
// TemplateData, so the user adds "rûpel=", "sernav=", ... before anything lands in the field
// rather than typing a bare {{Target}} and going back to fill params in by hand.
onPick: function (entry) {
cite.openTemplateDialog("{{" + entry.target + "}}").then(function (edited) {
if (!edited) {
return; // cancelled, or "Jê bibe" on a call that was never inserted
}
util.insertAtCursor($ta.get(0), "* " + edited + "\n");
$ta.trigger("focus");
});
}
});
var $picker = $("<div class='dsb-cavkani-picker'></div>").append(picker.$element);
$fieldWrap.empty().append($picker, $ta);
}
// One "{{kol3}}" group: its "sernav=" heading and its words. Several of these make up one section --
// "Herwiha binêre" typically carries an unheaded list plus a few headed ones.
function addGroup(s, group) {
var $sernav = $("<input type='text' class='dsb-input'>").attr("placeholder", "ne mecbûrî…");
// {{h-bnr}} takes bare page names, so its rows carry no ⊕: there is nowhere in the call for a
// modifier to go.
var words = buildWordListField(s.header + "…", { modifiers: !s.single });
var $remove = $("<a href='#' class='dsb-quick-link dsb-group-remove'>Komê rake</a>");
var $element = $("<div class='dsb-wordgroup'></div>").append(
// Label beside the box, not above it: stacked, "Sernav" read as a heading for the whole
// group rather than as the name of the field under it. The input is nested in the label so
// clicking the word focuses the box, without needing an id per group.
// A single-call list has no "sernav=" to carry, so the row is built (readGroups still reads
// it) but never shown. Hidden here rather than in renderField: setGroups() rebuilds every
// group on prefill, which would put a visible one back.
$("<div class='dsb-tpl-param-row dsb-param-row-inline'></div>").append(
$("<label class='dsb-adv-label'></label>").text("Sernav").append($sernav)
).toggle(!s.single),
words.$element,
$remove
);
var field = {
$element: $element,
read: function () {
return { sernav: $.trim($sernav.val()), words: words.values() };
},
set: function (g) {
$sernav.val((g && g.sernav) || "");
words.setValues((g && g.words) || []);
}
};
$remove.on("click", function (e) {
e.preventDefault();
var at = $.inArray(field, groupFields);
if (at !== -1) {
groupFields.splice(at, 1);
}
$element.remove();
updateGroupChrome();
});
field.set(group);
groupFields.push(field);
$groups.append($element);
updateGroupChrome();
return field;
}
// With a single group there is nothing to tell apart, so it needs neither a remove link nor a label.
function updateGroupChrome() {
var many = groupFields.length > 1;
$groups.toggleClass("dsb-wordgroups-many", many);
$groups.find(".dsb-group-remove").toggle(many);
}
function readGroups() {
return groupFields.map(function (field) {
return field.read();
});
}
// Rebuilds the blocks to match what the page (or the draft) holds, always leaving at least one.
function setGroups(s, groups) {
groupFields = [];
$groups.empty();
(groups && groups.length ? groups : [null]).forEach(function (group) {
addGroup(s, group);
});
}
// Loads what the page already has into the form, so an edit starts from the current list rather than
// from an empty one. A wordlist section we can't parse degrades to raw wikitext editing.
function prefill(s) {
$status.removeClass("dsb-status-error dsb-status-ok").text("Tê barkirin…");
$save.prop("disabled", true);
fetchPageSubsection(language, sectionType, sectionOrdinal, s).then(function (current) {
$save.prop("disabled", false);
$status.text("");
if (!current) {
return;
}
// A "place" section has no heading of its own, so whether the page already has one cannot
// be seen in the DOM the way a "====" subsection can -- it is known only once its content
// has been read. That is what decides "nûkirin" or "zêdekirin" in the edit summary.
if (s.place) {
s.existing = !!(current.images ? current.images.length : $.trim(current.text || ""));
// The label was set before this content arrived, when nothing yet said one was there.
$save.text(s.existing && s.kind !== "werger" ? util.msg.edit : util.msg.add);
}
if (current.kind === "images" && imagesField) {
imagesField.setValues(current.images);
return;
}
// A free section's field was built before its content was known: a list turns it into the
// {{kol3}} form, anything else leaves it the wikitext box it already is.
if (s.free && current.kind === "wordlist" && !groupFields.length) {
s.kind = "wordlist";
renderWordListField(s);
setGroups(s, current.groups);
if (current.fromBullets) {
$status.text("Ev lîste made bi made hatiye nivîsîn — bi qeydkirinê dibe {{kol3}}.");
}
return;
}
if (current.kind === "wordlist" && groupFields.length) {
setGroups(s, current.groups);
if (current.fromBullets) {
$status.text("Ev lîste made bi made hatiye nivîsîn — bi qeydkirinê dibe {{kol3}}.");
}
return;
}
if (s.kind === "wordlist") {
editAsText = true;
groupFields = [];
renderTextArea(s, 6);
$status.text(s.unknown
? "Ev beş nayê naskirin — wekî wîkîtekst tê guhartin."
: "Şablona vê lîsteyê nehate naskirin — wekî wîkîtekst biguhêre.");
}
setFieldText(current.text);
}, function () {
$save.prop("disabled", false);
$status.addClass("dsb-status-error").text("Nikare muhtewaya heyî bixwîne.");
});
}
// One "{{kol3}}" group per block, plus a way out to raw wikitext for a list no row can express --
// a shared subpage list like {{herwiha binêre/ane}}, say.
function renderWordListField(s) {
$fieldWrap.empty();
chipSurface = null;
editAsText = false;
var $addGroup = $("<a href='#' class='dsb-quick-link'>+ Komek lê zêde bike</a>");
$addGroup.on("click", function (e) {
e.preventDefault();
addGroup(s, null);
});
$groups = $("<div class='dsb-wordgroups'></div>");
$fieldWrap.append($groups, $addGroup);
addGroup(s, null);
// One call takes one flat list of names, so there is no second group to add.
if (s.single) {
$addGroup.hide();
}
var $toRaw = $("<a href='#' class='dsb-quick-link'>Wekî wîkîtext biguhêre</a>");
$toRaw.on("click", function (e) {
e.preventDefault();
var current = s.place
? wt.buildHbnrCall(loosePlaceWords(readGroups()))
: subsectionBodyWikitext(language, s, readGroups());
editAsText = true;
groupFields = [];
$groups = null;
renderTextArea(s, 3);
setFieldText(current);
util.initManualResize($fieldWrap);
});
$fieldWrap.append($toRaw);
util.initManualResize($fieldWrap);
}
function renderField() {
var s = currentSubsection();
$fieldWrap.empty();
groupFields = [];
$groups = null;
editAsText = false;
chipSurface = null;
wergerForm = null;
imagesField = null;
$status.removeClass("dsb-status-error dsb-status-ok").text("");
// Save stays disabled (but "Betal bike" usable) until something is actually chosen.
$save.prop("disabled", !s);
if (!s) {
$save.text(util.msg.add);
lastId = null;
return; // still on "Binebeşekê hilbijêre"
}
if (s.kind === "wordlist") {
renderWordListField(s);
} else if (s.kind === "images") {
imagesField = util.buildImageListField({
prefix: "dsb-",
rowClass: "dsb-tpl-param-row dsb-param-row-inline",
inputClass: "dsb-input",
addClass: "dsb-quick-link",
pageName: mw.config.get("wgTitle")
});
$fieldWrap.append(imagesField.$element);
} else if (s.header === "Etîmolojî") {
renderTemplateEditor(s, templateChoices("etymTpls", language));
} else if (s.place === "pron") {
renderTemplateEditor(s, templateChoices("pronTpls", language));
} else if (s.kind === "werger") {
renderWergerField(s);
} else if (s.header === "Çavkanî") {
renderCavkaniField(s);
} else {
renderTextArea(s, 3);
}
// Werger always adds a box, even to a section that is already there, so its button never says
// "Biguhêre".
$save.text(s.existing && s.kind !== "werger" ? util.msg.edit : util.msg.add);
// Covers whichever branch above built the field. Idempotent, so a textarea already carrying a
// grip is skipped.
util.initManualResize($fieldWrap);
lastId = s.id;
// A draft always wins over re-reading the page: it is the newer, unsaved state.
if (drafts[s.id]) {
restoreDraft(s, drafts[s.id]);
} else if ((s.existing || s.place) && s.kind !== "werger") {
prefill(s);
}
}
$select.on("change", function () {
captureDraft();
renderField();
});
renderField();
// Back to the state a fresh open would show: no draft, no picked subsection. Called directly
// rather than through the "change" handler above, so the discarded draft is never captured first.
function resetForm() {
drafts = {};
if (!onlyHeader) {
$select.val("").trigger("wiktku:refresh");
}
renderField();
}
$save.on("click", function () {
var s = currentSubsection();
if (!s) {
return;
}
var asText = editAsText || (s.kind !== "wordlist" && s.kind !== "images");
var effective = asText ? $.extend({}, s, { kind: "text" }) : s;
var content;
if (asText) {
content = fieldText();
if (!content) {
return;
}
} else if (s.kind === "images") {
content = imagesField ? imagesField.values() : [];
if (!content.length) {
return;
}
} else {
content = readGroups();
if (!content.some(function (group) { return group.words.length; })) {
return;
}
}
$save.prop("disabled", true);
// A Werger section holds any number of boxes, so an existing one is appended to rather than
// replaced; every other kind edits the single body it owns.
var appending = s.kind === "werger" && s.existing;
queueChange(
subsectionPlaceLabel(s, language, sectionType, $h3),
s.kind === "werger"
? "qutiyeke wergerê hate zêdekirin"
: "beşa «" + s.header + "» hate " + (s.existing ? "nûkirin" : "zêdekirin"),
function (pageContent) {
var body = content;
if (appending) {
var current = readPageSubsection(pageContent, language, sectionType, sectionOrdinal, effective);
var existingText = current ? $.trim(current.text || "") : "";
body = existingText ? existingText + "\n\n" + content : content;
}
return {
content: s.free
? updateFreeSection(pageContent, language, effective, body)
: (s.place
? writeLoosePlace(pageContent, language, sectionType, sectionOrdinal, effective,
loosePlaceBody(effective, body, language))
: (s.existing
? updatePageSubsection(pageContent, language, sectionType, sectionOrdinal, effective, body)
: insertPageSubsection(pageContent, language, sectionType, sectionOrdinal, effective, body))),
warnings: []
};
},
$save,
$status,
// Werger is a box the translation tools fill in afterwards, so there is nothing to draw for
// it. A new subsection gets a block of its own at the position it will land in; one already
// on the page is redrawn where it stands.
s.kind === "werger" ? null : function () {
// A free section is redrawn under its own heading, which is the element the panel hangs
// on.
if (s.free) {
var restoreFree = replaceFreeSectionInPlace($h3,
subsectionBodyWikitext(language, effective, content));
return function () {
if (restoreFree) {
restoreFree();
restoreFree = null;
}
};
}
// A "place" section has no heading to redraw between, so it is found in the DOM by what
// it renders as and replaced there.
if (s.place) {
var restoreLoose = replaceLoosePlaceInPlace($h3, effective,
loosePlacePreviewWikitext(effective, content, language));
return function () {
if (restoreLoose) {
restoreLoose();
restoreLoose = null;
}
};
}
if (!s.existing) {
var $preview = buildSubsectionPreviewDom($h3, s, content, language);
return function () {
$preview.remove();
};
}
var restore = replaceSubsectionInPlace($h3, effective.header,
subsectionBodyWikitext(language, effective, content));
return function () {
if (restore) {
restore();
restore = null;
}
};
},
{ key: s.free
? "section:" + s.header + "|" + s.free.ordinal
: "subsection:" + sectionType + "|" + sectionOrdinal + "|" + s.header }
);
});
$panel.empty().append($selectWrap, $fieldWrap, $actions);
}
// Builds the "choose a part of speech" picker that reclassifies a section -- an unclassified
// "=== Mane ===" placeholder, or a "=== Biwêj ===" that turns out to be a real part of speech. Saved via
// convertSectionToPos(); lists every name from sectionNames except the one the section already is.
function buildConvertPosPanel($panel, $h3, language, sectionType, sectionOrdinal) {
var $select = $("<select class='dsb-convert-pos-select'></select>");
// Label-less, like the subsection picker's: it holds the "nothing chosen yet" value without showing
// up as a row, so the prompt lives in the input's placeholder instead.
$select.append($("<option value=''></option>"));
Object.keys(sectionNames).forEach(function (name) {
// The section's own part of speech is listed too, marked: picking it changes nothing about the
// heading and leaves the parameters below to be edited on their own.
$select.append($("<option></option>").val(name)
.text(name === sectionType ? name + " (ya niha)" : name));
});
var $selectWrap = makeSearchable($select, "Li cureyekî rêzimanî bigere");
// Opens on the section's own part of speech, so editing only its parameters needs no pick.
if (sectionNames[sectionType]) {
$select.val(sectionType).trigger("wiktku:refresh");
}
// The same card the entry wizard shows under its part-of-speech question -- gender, transitivity,
// the transliteration boxes and everything behind "Tercîhên pêşketî". Shared from
// wikt.edit/posparams.js, so reclassifying a section here offers exactly what creating one in the
// wizard does.
var card = posparams.build({ lang: language, pos: "" });
// Opened on the call the section already has, so this edits the current template rather than
// starting from a blank line. setContext() rebuilds the line whenever the part of speech moves,
// so the prefill is only the starting point.
var spec0 = posTemplateFor(sectionType);
card.setContext(language, spec0 ? spec0.name : "", /^Formeke /.test(sectionType));
fetchPageContent().then(function (pageContent) {
try {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var found = findPosTemplateLine(loc.lines, loc.posLineIndex, sectionType);
if (found.index !== -1 && ($select.val() || sectionType) === sectionType) {
card.setWikitext(found.text);
}
} catch (e) { /* an unreadable section just leaves the card as it was */ }
});
$select.on("change", function () {
var spec = posTemplateFor($select.val());
card.setContext(language, spec ? spec.name : "", /^Formeke /.test($select.val() || ""));
});
// "gh" on its own, or "<form>-<base>" once a structural form is picked -- start.js's own composition.
// convertSectionToPos() writes the template itself, so only what follows "{{pos|lang" is wanted
// from the card's line.
function posParams() {
var m = /^\{\{[^|}]*\|[^|}]*\|?([\s\S]*)\}\}$/.exec(card.wikitext());
return m ? m[1] : "";
}
var $save = $("<button type='button' class='dsb-save'></button>").text(util.msg.add);
var $cancel = $("<button type='button' class='dsb-cancel'></button>").text(util.msg.cancel);
var $status = $("<span class='dsb-status'></span>");
var $actions = $("<div class='dsb-form-actions'></div>").append($save, $cancel, $status);
// dsb.js owns the pencil and the panel's built state, so it does the closing.
$cancel.on("click", function () {
$panel.trigger("dsbCancel");
hideQueueIfIdle();
});
$save.on("click", function () {
var chosen = $select.val();
if (!chosen) {
$status.removeClass("dsb-status-ok").addClass("dsb-status-error").text("Pêşî cureyekî rêzimanî hilbijêre.");
return;
}
var params = posParams();
$save.prop("disabled", true);
$status.removeClass("dsb-status-error").text("");
queueChange(
langLabel(language),
"beşa «" + sectionLabel(sectionType, $h3) + "» bû «" + chosen + "»",
function (pageContent) {
return { content: convertSectionToPos(pageContent, language, sectionType, sectionOrdinal, chosen, params), warnings: [] };
},
$save,
$status,
function () {
// The heading keeps its text, which every other queued change finds the section by; a
// note under it names the new one. The headword line is drawn as the save will write it.
var $note = $("<div class='dsb-preview-inline dsb-preview-heading-note'></div>")
.text("→ " + chosen + (params ? " (" + params + ")" : "") + " — nû, hîn nehatiye qeydkirin");
headingOuter($h3).after($note);
var dropped = false;
var restoreHeadword = null;
fetchPageContent().then(function (pageContent) {
if (dropped) {
return;
}
var call;
try {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
call = posCallLine(findPosTemplateLine(loc.lines, loc.posLineIndex, sectionType),
language, chosen, params);
} catch (e) {
return;
}
restoreHeadword = replaceHeadwordInPlace($h3, call);
});
return function () {
dropped = true;
$note.remove();
if (restoreHeadword) {
restoreHeadword();
restoreHeadword = null;
}
};
},
// Runs after every other queued change: it renames the "Mane" heading those changes locate
// themselves by, so anything edited on this section has to be applied first.
{ runLast: true, key: "convert:" + sectionType + "|" + sectionOrdinal }
);
});
$panel.empty().append($selectWrap, card.$element, $actions);
}
// Attaches the "+ Beşek lê zêde bike" trigger right after a POS block's own last element.
// Puts the same pencil the definitions use on every existing "====" subsection heading the gadget knows how
// to edit, opening buildSubsectionPanel() pinned to that one subsection.
module.exports = {
buildAdvancedPanel: buildAdvancedPanel,
buildSubsectionPanel: buildSubsectionPanel,
buildConvertPosPanel: buildConvertPosPanel,
editExample: editExample,
clearQueue: clearQueue,
closeAllPanels: closeAllPanels,
openQueueBox: openQueueBox
};
}());
// </nowiki>
nck8lcdikdv8nrmuwpewxehs5j9h8xt
6531945
6531918
2026-09-17T14:17:23Z
Wikihez
7344
POS conversion keeps the heading depth and replaces a numbered sense-group heading; numbered headings tagged with {{cure?}} count as Mane sections; + Îzah for Kurmancî senses
6531945
javascript
text/javascript
// <nowiki>
/*
* Page: [[MediaWiki:Gadget-wikt.dsb-panels.js]]
*
* Everything DefSideBoxes shows once you click: the per-sense advanced panel, the subsection editors,
* the part-of-speech picker, the template dialogs, the preview and the queued save.
*
* Loaded on demand by wikt.dsb, never on a plain page view. It exports only what that half calls.
*/
(function () {
"use strict";
var sections = require("ext.gadget.wikt.core").sections;
var edit = require("ext.gadget.wikt.edit");
var cite = require("ext.gadget.wikt.cite");
var util = edit.util;
var posparams = edit.posparams;
var wt = edit.wt;
var startData = edit.data;
var langs = edit.langs;
var escapeRegExp = util.escapeRegExp;
// Local names for the shared wikitext layer, so every call site below reads as it always did.
var langHeaderSource = wt.langHeaderSource;
var canonicalTemplateKey = wt.canonicalTemplateKey;
var aliasPattern = wt.aliasPattern;
var splitLeadingCalls = wt.splitLeadingCalls;
var splitTemplateParams = wt.splitTemplateParams;
var parseTemplateCalls = wt.parseTemplateCalls;
var splitCallParams = wt.splitCallParams;
var buildTemplateCall = wt.buildTemplateCall;
var tdText = wt.tdText;
var fetchTemplateData = wt.fetchTemplateData;
var parseWordListCall = wt.parseWordListCall;
var parseWordListCalls = wt.parseWordListCalls;
var namedParam = wt.namedParam;
var withNamedParam = wt.withNamedParam;
var buildWordListCall = wt.buildWordListCall;
var parseBulletWordList = wt.parseBulletWordList;
var sectionNames = sections.sectionNames;
var MANE_SECTION = sections.MANE_SECTION;
var PAGE_SUBSECTIONS = sections.PAGE_SUBSECTIONS;
var headingOuter = sections.headingOuter;
var headingText = sections.headingText;
var isManeHeading = sections.isManeHeading;
var sectionTypeOf = sections.sectionTypeOf;
var sectionOrdinalWithinLanguage = sections.sectionOrdinalWithinLanguage;
var isFormOfWrapper = sections.isFormOfWrapper;
var eachLeafDefinition = sections.eachLeafDefinition;
var api = new mw.Api();
// metadata[lang].g, minus the grammatical-number codes start.js filters out with isNumberCode(), rendered
// the way its genderLabel() does ("m-p" -> "mê (pirjimar)").
// start.js asks transitivity and structural form as two questions and joins them as "<form>-<base>", so
// "gerguhêz" alone is "gh" and "xwerû" + "gerguhêz" is "x-gh".
// Template call convertSectionToPos() drops under a newly-chosen POS heading, e.g. "Hoker" -> "{{hoker|ku}}".
// A "Formeke X" (form-of) heading uses the unchanged base pos template. Doubles as the list of template
// calls a conversion may find under the OLD heading and replace, rather than leave stacked beside the new one.
// A section's template call. "Formeke Xê" uses X's own template with form=erê. Biwêj is recognised so a
// conversion can replace its {{biwêj}}, but never written: no new ones.
function posTemplateFor(header) {
var form = /^Formeke (.+)ê$/.exec(header || "");
if (form && startData.posChoices[form[1]] && sections.hasFormHeading(form[1])) {
return { name: form[1], params: ["form=erê"] };
}
var key = util.decapitalize(header || "");
return startData.posChoices[key] ? { name: key, params: [] } : null;
}
// convertSectionToPos() drops whichever of the tag's names is actually present.
var MANE_TAG_REGEX = wt.MANE_TAG_REGEX;
// The six per-sense groups start.js writes as "#: {{tpl|lang|word1|word2}}" sub-lines under a "#" definition.
// Array order controls where a new group line lands relative to existing ones. Every group lives behind the
// quick row's "Hevmane û Bêhtir" dropdown; only the gloss and Nimûne/jêgirtin links sit in the row itself.
var SENSE_GROUPS = startData.senseGroups;
// The column word-list template new sections are written with; the rest of wordlistTemplates are
// spellings the parser also accepts.
var WORDLIST_TEMPLATE = startData.wordlistTemplates[0];
// ---- {{werger-ser}} / {{werger-bin}}: one translation box ----
// The pair brackets the rows; the translation editor gadget writes the rows themselves onto the
// rendered page, so this only ever builds (and reads back) the opening call.
// Parameters, from [[Şablon:werger-ser]]: 1 = the sense shown in the box head, 2 = a note in
// parentheses after it, id = anchor override (defaults to 1), stûn = column count, firehî = column
// width (default 26em), bgcolor, valign (default top), align (default left).
var WERGER_NAMED = ["id", "stûn", "firehî", "bgcolor", "valign", "align"];
function buildWergerBlock(params) {
var parts = ["werger-ser"];
// Positional slots: 2 can only be reached past 1, so an empty 1 still has to be written.
if (params["1"] || params["2"]) {
parts.push(params["1"] || "");
}
if (params["2"]) {
parts.push(params["2"]);
}
WERGER_NAMED.forEach(function (key) {
if (params[key]) {
parts.push(key + "=" + params[key]);
}
});
return "{{" + parts.join("|") + "}}\n{{werger-bin}}";
}
// The opening call of the first block in `text`, as the same {1, 2, id, ...} object. Empty object when
// there is none, so a half-typed draft restores as an empty form rather than throwing.
function parseWergerBlock(text) {
var out = {};
var body = wergerSerBody(String(text || ""));
if (!body) {
return out;
}
var positional = 0;
splitTemplateArgs(body).forEach(function (arg) {
var eq = arg.indexOf("=");
var name = eq === -1 ? "" : $.trim(arg.slice(0, eq));
if (eq !== -1 && WERGER_NAMED.indexOf(name) !== -1) {
out[name] = $.trim(arg.slice(eq + 1));
} else {
out[String(++positional)] = $.trim(arg);
}
});
return out;
}
// Everything after "{{werger-ser|" up to the "}}" that closes it, or "" when there is no call or it
// takes no parameters. Scans for the matching close rather than the first one: a gloss can hold a
// template of its own, and a lazy regex would stop inside it.
function wergerSerBody(text) {
var m = /\{\{\s*werger-ser\s*(?=[|}])/.exec(text);
if (!m) {
return "";
}
var depth = 1;
var start = m.index + m[0].length;
for (var i = start; i < text.length - 1; i++) {
var two = text.substr(i, 2);
if (two === "{{") {
depth++; i++;
} else if (two === "}}") {
depth--; i++;
if (!depth) {
return text.charAt(start) === "|" ? text.slice(start + 1, i - 1) : "";
}
}
}
return "";
}
// Splits on "|" at brace/bracket depth 0, so a value containing a template or a link stays whole.
function splitTemplateArgs(body) {
var args = [];
var depth = 0;
var current = "";
for (var i = 0; i < body.length; i++) {
var two = body.substr(i, 2);
if (two === "{{" || two === "[[") {
depth++; current += two; i++; continue;
}
if (two === "}}" || two === "]]") {
depth--; current += two; i++; continue;
}
if (body[i] === "|" && depth <= 0) {
args.push(current); current = ""; continue;
}
current += body[i];
}
args.push(current);
return args;
}
// Canonical ku.wiktionary "====" subsection order, used only as insertPageSubsection()'s insertion rank
// (see subsectionRankForHeader()). Wider than PAGE_SUBSECTIONS: it also covers subsections the gadget
// cannot create but still has to order around.
var SUBSECTION_ORDER = startData.subsectionOrder;
// ---- searchable <select> ----------------------------------------------------------------------------
// Same widget start.js gets from its util.js wrapNativeSelect(): the native <select> stays as the value
// holder (so .val() and "change" handlers keep working) but is hidden behind a text input that filters an
// overlay list. Ported rather than required() because util.js is a file inside start.js's package module,
// which another gadget cannot reach; the template-row decoration start.js uses is left out.
// util.js's wrapNativeSelect(): the native <select> stays the value holder behind a filtering text input.
// It reads its own placeholder off data-placeholder, and its wiktku-* styles ship in start.css, so both
// gadgets get the identical widget instead of two lookalikes.
function makeSearchable($select, placeholder) {
$select.attr("data-placeholder", placeholder || "Lê bigere…");
return util.wrapNativeSelect($select);
}
// ---- small shared utilities ----
// The readable half of an mw.Api rejection.
var apiErrorMessage = util.apiErrorMessage;
// Thrown from inside an edit() transform to bail out with a specific message.
var SubmitAbort = util.SubmitAbort;
// A gloss is one field on the page: the sense, then {{îzah|…}} explaining it when there is one.
function joinGloss(mane, izah) {
return izah ? $.trim(mane + " {{îzah|" + izah + "}}") : $.trim(mane);
}
// The two halves are linked separately, never joined first: linkify leaves template calls alone by
// design, so anything inside {{îzah|…}} would never be looked at.
//
// A half is skipped when there is nothing in it worth looking up, and -- when onlyUnlinked is set --
// when it already carries a link. Someone who has linked a word by hand has made that half theirs, and
// a save must not go on linking the rest of it behind their back; "Oto-lînk bike" leaves the flag off
// and still links on demand.
function linkifyGlossParts(mane, izah, done, onlyUnlinked) {
function needsLinking(text) {
if (!$.trim(text) || !util.hasLinkableText(text)) {
return false;
}
return !(onlyUnlinked && text.indexOf("[[") !== -1);
}
function step(text, next) {
if (!needsLinking(text)) {
next(text, null);
return;
}
util.linkifyText(text, function (linked, error) {
// An empty answer would wipe what the user typed, so only real text is taken.
next(typeof linked === "string" && $.trim(linked) ? $.trim(linked) : text, error || null);
});
}
step(mane, function (linkedMane, maneError) {
step(izah, function (linkedIzah, izahError) {
done(linkedMane, linkedIzah, maneError || izahError);
});
});
}
// Runs transformFn(content) inside mw.Api().edit()'s own transform callback so one fetch covers both the
// edit-conflict check and the content, preventing a stale overwrite of someone else's edit.
//
// No badtoken retry here on purpose: edit() posts through postWithEditToken -> postWithToken, which
// already throws the cached token away and repeats the request once when the wiki says "badtoken".
// A second retry of our own would only double it.
function editPage(summary, transformFn, onSuccess, onError) {
api.edit(mw.config.get("wgPageName"), function (revision) {
return { text: transformFn(revision.content), summary: summary };
}).then(onSuccess, function (codeOrAbort, result) {
if (codeOrAbort instanceof SubmitAbort) {
onError(codeOrAbort.message);
} else {
onError(util.msg.saveFailed + apiErrorMessage(codeOrAbort, result));
}
});
}
// ---- rendered preview: wikitext through the parse API, so a preview shows what the templates
// actually produce rather than a plain list standing in for them ----
// Same text is usually asked for twice (open the preview, close it, open it again), so the promise is
// kept rather than the call repeated.
var renderCache = {};
/**
* Renders wikitext as this page would render it. Resolves with an HTML string.
*
* pst: 1 applies pre-save transform, so ~~~~ and subst: behave as they will once saved. The
* modulestyles/modules the parse reports are loaded before resolving: without them TemplateStyles is
* missing and the preview would show the right markup in the wrong clothes.
*/
function renderWikitext(wikitext) {
if (renderCache[wikitext]) {
return renderCache[wikitext];
}
renderCache[wikitext] = api.post({
action: "parse",
format: "json",
formatversion: 2,
title: mw.config.get("wgPageName"),
text: wikitext,
contentmodel: "wikitext",
prop: "text|modules|jsconfigvars",
pst: 1,
disablelimitreport: 1,
disableeditsection: 1,
disabletoc: 1
}).then(function (data) {
var parsed = (data && data.parse) || {};
if (parsed.modulestyles && parsed.modulestyles.length) {
mw.loader.load(parsed.modulestyles);
}
if (parsed.modules && parsed.modules.length) {
mw.loader.load(parsed.modules);
}
return parsed.text || "";
}, function () {
// A failed render must not be remembered, or the next attempt gets the rejection back.
delete renderCache[wikitext];
return $.Deferred().reject().promise();
});
return renderCache[wikitext];
}
/**
* Puts rendered wikitext into $target, replacing whatever placeholder is there.
*
* The site's own gadgets are told about it through the "wikipage.content" hook, the same way
* MediaWiki announces real content, so collapsible lists and the like behave in the preview too.
*/
// MediaWiki warns when the hook is fired on content that is not in the document, and a gadget that
// listens to it can do nothing useful with nodes nobody can see.
function announceContent($el) {
if ($el.closest("html").length) {
mw.hook("wikipage.content").fire($el);
}
}
function fillWithRendered($target, wikitext, onFail) {
return renderWikitext(wikitext).then(function (html) {
if (!$target.closest("body").length) {
return; // the preview was dropped while the parse was in flight
}
$target.empty().append($("<div class='mw-parser-output'></div>").html(html));
announceContent($target);
}, function () {
if (onFail) onFail();
});
}
/**
* Puts a rendered sense in place of the one on the page, keeping this gadget's own furniture (the
* pencil's toolbar, the quick row, any open panel) where it is. The page's own nodes are detached
* rather than dropped, so the returned function can put them straight back when the change is
* cancelled or superseded.
*
* Nothing labels the result: the change is shown by the sense simply reading the way it will read,
* and the queue box is what says nothing is saved yet.
*/
function replaceSenseInPlace($li, item) {
var html = item.html;
var $anchor = $li.children(".dsb-toolbar").last();
// class/id/data-* off the parsed <li>, so a queued maneid shows its pill here exactly as it will
// once saved. The old values are kept for the restore below.
var node = $li.get(0);
var before = {};
$.each(node.attributes, function (_, attr) {
before[attr.name] = attr.value;
});
$.each(item.attrs, function (name, value) {
if (name !== "class") {
$li.attr(name, value);
}
});
var addedClasses = (item.attrs["class"] || "").split(/\s+/).filter(function (c) {
return c && !$li.hasClass(c);
});
$li.addClass(addedClasses.join(" "));
// add-examples hangs its own <ul> off the sense and opens its form as that <ul>'s next sibling.
// Detaching those leaves the form to open into a tree nobody can see -- "+ Nimune/jegirtin" takes
// the click and shows nothing -- so they stay where they are. Only the rows already in that <ul>
// are hidden, since the preview draws the examples itself; the tool's own title line stays.
var $keep = $li.children("ul, .add-example-form");
var $hiddenRows = $keep.filter("ul").children("li").filter(function () {
return !$(this).find(".add-example-form-title").length;
});
$hiddenRows.addClass("dsb-preview-hidden");
var originals = $li.contents().filter(function () {
return !$(this).is(".dsb-toolbar, .dsb-quick-row, .dsb-subsection-panel, .dsb-preview-block, .dsb-sense-preview");
}).not($keep).get();
// Where the sense's own nodes sat, so cancelling puts them back there rather than above the quick
// row -- the row belongs between the sense and its examples.
var backAfter = originals.length ? originals[0].previousSibling : null;
var $rendered = $("<span class='dsb-sense-preview'></span>").html(html);
if ($anchor.length) {
$anchor.after($rendered);
} else {
$li.prepend($rendered);
}
$(originals).detach();
announceContent($rendered);
// The id pill is drawn by the same code a page load uses, so a queued maneid looks like a saved
// one rather than like something this gadget invented.
if (window.wikt && wikt.showIDs && wikt.showIDs.process) {
wikt.showIDs.process($li);
}
// dsb.js puts the example pencils back on the redrawn examples. The originals keep theirs.
$li.triggerHandler("dsbSenseRedrawn");
return function () {
$hiddenRows.removeClass("dsb-preview-hidden");
$li.removeClass(addedClasses.join(" "));
$.each(item.attrs, function (name) {
if (name === "class") {
return;
}
if (Object.prototype.hasOwnProperty.call(before, name)) {
$li.attr(name, before[name]);
} else {
$li.removeAttr(name);
}
});
$li.children(".senseid-displayed, .etymid-displayed, .etymonid-displayed").remove();
if (backAfter && backAfter.parentNode === node) {
$(backAfter).after(originals);
} else if ($anchor.length) {
$anchor.after(originals);
} else {
$li.prepend(originals);
}
$rendered.remove();
};
}
/**
* The <li> out of a parsed "#" block, and the attributes it carries.
*
* The parse wraps it in its own <ol>, which must not reach the page: the sense already has a number
* from the list it lives in. A leading {{maneid}} also makes the parser emit an empty
* "li.mw-empty-elt" ahead of the real one, so the first <li> is not necessarily the sense.
*
* The attributes matter as much as the content: {{maneid|ku|x}} renders as class="senseid"
* data-id="x" on the <li> itself, which is what wikt.showids draws its pill from.
*/
function parsedSenseItem(html, depth) {
var $item = $("<div></div>").html(html).find("ol > li").filter(function () {
return !$(this).hasClass("mw-empty-elt");
}).first();
// A sub-sense parsed on its own comes back one <ol> deeper per "#": "##" is a list inside a list.
// Descend as far as the sense's own depth, or its number would be drawn inside a second list --
// "2." with a "1." under it.
for (var level = 1; level < (depth || 1) && $item.length; level++) {
var $inner = $item.children("ol").children("li").filter(function () {
return !$(this).hasClass("mw-empty-elt");
}).first();
if (!$inner.length) {
break;
}
$item = $inner;
}
if (!$item.length) {
return { html: html, attrs: {} };
}
var attrs = {};
$.each($item.get(0).attributes, function (_, attr) {
attrs[attr.name] = attr.value;
});
return { html: $item.html(), attrs: attrs };
}
// This gadget's own save queue + floating widget, separate from the shared Gadget-Editor queue.
// Each entry keeps its own transformFn so saveQueueNow() can re-run all of them against one fresh fetch.
var dsbQueue = {
// [{key, summary, transformFn, runLast, removePreview}]; transformFn: baseContent -> {content, warnings}.
// removePreview lives on the entry so replacing one entry takes its own live DOM preview with it.
entries: []
};
var $queueBox = null;
function dropPreview(entry) {
if (typeof entry.removePreview === "function") {
try {
entry.removePreview();
} catch (e) { /* a preview failing to clean up should never block the queue itself */ }
entry.removePreview = null;
}
}
function clearAllPreviews() {
dsbQueue.entries.forEach(dropPreview);
}
function clearQueue() {
clearAllPreviews();
dsbQueue.entries = [];
if ($queueBox) {
$queueBox.$list.empty();
$queueBox.$box.hide();
}
}
// Closes every editor UI on the page. Each open box's own "x" clears its getter, restores its picker
// link and updates the counter, so those are triggered rather than repeating that work here; hiding a
// row alone would leave its fields populated and the next pencil click would reopen them half-filled.
function closeAllPanels() {
$(".dsb-open-boxes .dsb-box-remove").trigger("click");
$(".dsb-quick-note").hide().text("");
$(".dsb-more-menu").hide();
$(".dsb-subsection-panel, .dsb-convert-pos-panel").hide();
// dsb.js owns each pencil's pinned state; tell it rather than hiding behind its back, or the next
// click on the pencil would only toggle "pinned" off and appear to do nothing.
$(".dsb-quick-row").trigger("dsbCancel");
}
function ensureQueueBox() {
if ($queueBox) {
return $queueBox;
}
var $box = $("<div class='dsb-queue-box'></div>").hide();
var $close = $("<a href='#' class='dsb-queue-close' title='Veşêre'>×</a>");
var $title = $("<div class='dsb-queue-title'></div>").text("Guhartinên li benda qeydkirinê");
var $list = $("<ul class='dsb-queue-list'></ul>");
var $empty = $("<div class='dsb-queue-empty'></div>").text(
"Guhartinên te dê li vir werin rêzkirin. Heta ku tu pêl «Guhartinan qeyd bike» nekî, "
+ "tiştek li ser rûpelê nayê weşandin."
);
var $status = $("<div class='dsb-queue-status'></div>");
var $save = $("<button type='button' class='dsb-queue-save'>Guhartinan qeyd bike</button>");
var $cancel = $("<button type='button' class='dsb-queue-cancel'></button>").text(util.msg.cancel);
var $actions = $("<div class='dsb-queue-actions'></div>").append($save, $cancel);
$close.on("click", function (e) {
e.preventDefault();
$box.hide();
});
$cancel.on("click", function () {
clearQueue();
closeAllPanels();
});
$save.on("click", function () {
saveQueueNow();
});
$box.append($close, $title, $empty, $list, $actions, $status);
$(document.body).append($box);
$queueBox = { $box: $box, $list: $list, $empty: $empty, $status: $status, $save: $save, $cancel: $cancel };
return $queueBox;
}
// Takes one change out of the queue and puts the page back as it was before it, its in-place preview
// included. The queue is the only record of what "Guhartinan qeyd bike" will write, so every line in it
// has to be removable on its own -- closing the panel that made a change does not unmake it.
// Each preview draws the page the whole queue would save, so one entry leaving changes what all the
// others should show. They are taken down and drawn again from what is left.
function redrawPreviews() {
dsbQueue.entries.forEach(function (entry) {
if (typeof entry.previewFn !== "function") {
return;
}
dropPreview(entry);
try {
var removePreview = entry.previewFn();
if (typeof removePreview === "function") {
entry.removePreview = removePreview;
}
} catch (e) { /* a broken preview never stops the queue */ }
});
}
function dropQueueEntry(entry) {
var at = dsbQueue.entries.indexOf(entry);
if (at === -1) {
return;
}
dropPreview(entry);
dsbQueue.entries.splice(at, 1);
redrawPreviews();
renderQueueBox();
hideQueueIfIdle();
}
function renderQueueBox() {
var box = ensureQueueBox();
box.$list.empty();
dsbQueue.entries.forEach(function (entry) {
var $drop = $("<button type='button' class='dsb-queue-drop'>×</button>")
.attr("title", "Vê guhartinê ji rêzê derxe");
$drop.on("click", function () {
dropQueueEntry(entry);
});
box.$list.append($("<li></li>").text(entry.summary).append($drop));
});
box.$empty.toggle(!dsbQueue.entries.length);
box.$status.removeClass("dsb-status-ok dsb-status-error").text("");
box.$save.prop("disabled", !dsbQueue.entries.length);
box.$box.show();
}
// Shown the moment a panel opens, empty, so it is clear before anything is typed that a change waits
// for "Guhartinan qeyd bike" rather than publishing itself.
function openQueueBox() {
renderQueueBox();
}
// Nothing queued and no form left open means nothing to save, so the box goes away rather than
// lingering on an empty state.
function hideQueueIfIdle() {
if (dsbQueue.entries.length || !$queueBox) {
return;
}
if ($(".dsb-quick-row:visible, .dsb-subsection-panel:visible, .dsb-convert-pos-panel:visible").length) {
return;
}
$queueBox.$box.hide();
}
// Asks core's postEdit module to show its toast on the next load (dsb.js loads it there), then reloads.
// Loaded here rather than declared as a dependency, so a reload happens even if it fails.
function reloadWithPostEditNotice() {
mw.loader.using("mediawiki.action.view.postEdit").then(function (require) {
require("mediawiki.action.view.postEdit").fireHookOnPageReload("saved");
}).always(function () {
location.reload();
});
}
// A transform returns either the new page text or {content, warnings}; both shapes are accepted.
// One that returns neither stops the save, named, rather than writing a broken page.
function transformedContent(entry, pageContent) {
var result = entry.transformFn(pageContent);
var next = typeof result === "string" ? result : (result && result.content);
if (typeof next !== "string") {
throw new SubmitAbort("Guhartin nehate sepandin: " + entry.summary);
}
return next;
}
// Queue order, except that runLast entries move to the end: converting "=== Mane ===" into a real POS
// heading renames the very heading every other queued change locates itself by.
function orderedEntries() {
var normal = [];
var last = [];
dsbQueue.entries.forEach(function (entry) {
(entry.runLast ? last : normal).push(entry);
});
return normal.concat(last);
}
/**
* The pending page as previews and example lookups need it: every queued transform, in save order,
* except the runLast section renames. Those callers find their section under the heading it has on
* the page now, which a queued "Mane" -> "Navdêr" would take away, and a rename leaves the senses as
* they are. An entry that cannot apply is skipped -- the save reports that properly, and a preview
* must not shout.
*/
function queuedContent(pageContent) {
var content = pageContent;
orderedEntries().filter(function (entry) {
return !entry.runLast;
}).forEach(function (entry) {
try {
content = transformedContent(entry, content);
} catch (e) {
// Left out of the preview; the save reports it properly. Named, so a change that fails to
// draw is distinguishable from one with nothing to draw.
mw.log.warn("wikt.dsb: pêşdîtin bêyî vê guhartinê hate xêzkirin — " + entry.summary, e);
}
});
return content;
}
/**
* Redraws one sense where it sits, from the page the queue would save -- so a queued gloss edit, a
* queued example and everything else pending on that sense are shown together, rendered by the parser
* rather than described in a box. Returns the queue's own "take the preview down" function.
*
* One preview per sense: a second queued change takes the first drawing down and draws the queue.
*/
function previewSense($li, language, sectionType, sectionOrdinal, definitionLevel) {
var cancelled = false;
var restore = null;
function undo() {
cancelled = true;
if (restore) {
restore();
restore = null;
}
if ($li.data("dsbPreview") === undo) {
$li.removeData("dsbPreview");
}
}
var previous = $li.data("dsbPreview");
if (typeof previous === "function") {
previous();
}
$li.data("dsbPreview", undo);
fetchPageContent().then(function (pageContent) {
if (cancelled) {
return;
}
var wikitext;
try {
wikitext = senseBlockWikitext(queuedContent(pageContent), language, sectionType,
sectionOrdinal, definitionLevel);
} catch (e) {
return;
}
renderWikitext(wikitext).then(function (html) {
if (cancelled || $li.data("dsbPreview") !== undo || !$li.closest("body").length) {
return;
}
restore = replaceSenseInPlace($li, parsedSenseItem(html, definitionLevel.length - 1));
});
});
return undo;
}
// The real save: re-runs every queued transformFn, in order, against one freshly-fetched revision and
// posts the result as a single edit with a combined summary.
function saveQueueNow() {
if (!dsbQueue.entries.length) {
return;
}
var box = ensureQueueBox();
box.$save.prop("disabled", true);
box.$cancel.prop("disabled", true);
box.$status.removeClass("dsb-status-ok dsb-status-error").text("Tê qeydkirin…");
var entries = orderedEntries();
// Grouped by place, in the order the places were first queued: several changes to one section read
// as "Kurmancî/Navdêr 2: a, b, c" rather than repeating the section name in front of each of them.
var order = [];
var byPlace = {};
entries.forEach(function (entry) {
if (!Object.prototype.hasOwnProperty.call(byPlace, entry.place)) {
byPlace[entry.place] = [];
order.push(entry.place);
}
byPlace[entry.place].push(entry.action);
});
var combinedSummary = order.map(function (place) {
var actions = byPlace[place].join(", ");
return place ? place + ": " + actions : actions;
}).join("; ") + " (DefSideBoxes)";
editPage(
combinedSummary,
function (pageContent) {
var content = pageContent;
entries.forEach(function (entry) {
content = transformedContent(entry, content);
});
return content;
},
function (data) {
forgetPageContent();
clearAllPreviews();
dsbQueue.entries = [];
box.$list.empty();
box.$status
.removeClass("dsb-status-error")
.addClass("dsb-status-ok")
.text("✓ Hemû guhartin hatin qeydkirin.");
box.$save.prop("disabled", true);
box.$cancel.prop("disabled", false);
// A real edit reloads the page and gets MediaWiki's own "Guhartina te hat weşandin." toast, the
// one a normal save shows. A null edit changed nothing, so the page stays as it is.
if (data && data.newrevid) {
reloadWithPostEditNotice();
}
},
function (message) {
box.$save.prop("disabled", false);
box.$cancel.prop("disabled", false);
box.$status.removeClass("dsb-status-ok").addClass("dsb-status-error").text(message);
}
);
}
// "Kurmancî/Navdêr" -- where a queued change lands, for its summary. The language name comes from the
// shipped list; an unknown code is left as the code itself.
// "Kurmancî/Navdêr", or "Kurmancî/Navdêr 2" where the language has more than one section of that
// type. Without the number a summary on a page with two Navdêr sections named neither of them.
// Numbered only when it disambiguates: on the usual page "Navdêr 1" would just be noise.
function placeLabel(language, sectionType, $h3) {
return langLabel(language) + "/" + sectionLabel(sectionType, $h3);
}
// Bilêvkirin sits above the parts of speech and belongs to the whole language, so naming the section
// it was opened from would say something untrue about what the edit touches.
function subsectionPlaceLabel(subsection, language, sectionType, $h3) {
if (subsection && subsection.free) {
return langLabel(language) + "/" + subsection.header;
}
return subsection && subsection.place === "pron"
? langLabel(language)
: placeLabel(language, sectionType, $h3);
}
function sectionLabel(sectionType, $h3) {
if ($h3 && $h3.length && sections.sectionTypeCountWithinLanguage($h3) > 1) {
return sectionType + " " + sections.sectionOrdinalWithinLanguage($h3);
}
return sectionType;
}
// Capitalised because it opens the summary; Kurdish otherwise writes language names lower-case.
function langLabel(language) {
var names = langs.names();
return util.capitalize(names[language] || language);
}
// The parts of a sense the form actually changed, so the summary lists them instead of saying nothing.
function senseChangeLabels(changes) {
var parts = [];
var changed = changes.changed || {};
if (changes.gloss) {
parts.push("mane");
}
if (changes.newSense) {
parts.push("maneyeke nû");
}
SENSE_GROUPS.forEach(function (group) {
if (changed["word:" + group.key]) {
parts.push(group.label.toLowerCase());
}
});
if (changed.maneid) {
parts.push("id");
}
if (changed.etiket) {
parts.push("etîket");
}
if (changed.kategori) {
parts.push("kategorî");
}
return parts;
}
// Queues transformFn(baseWikitext) -> {content, warnings} for the real save to run later against fresh
// content (see saveQueueNow()); nothing here touches the page yet. Also runs previewFn(), which drops a
// small "not saved yet" preview into the live DOM at the change's own spot on the page. Shared by every
// panel that queues a wikitext change: buildAdvancedPanel, buildSubsectionPanel, buildConvertPosPanel.
// place is what the change is being made to ("Kurmancî/Navdêr 2"), action is what is being done to it.
// Kept apart rather than pre-joined so saveQueueNow() can name a section once and list everything
// queued against it after it, instead of repeating the place before every item.
function queueChange(place, action, transformFn, $save, $status, previewFn, opts) {
opts = opts || {};
var entry = {
key: opts.key || null,
place: place || "",
action: action,
summary: place ? place + ": " + action : action,
transformFn: transformFn,
runLast: !!opts.runLast,
// Kept, not just run: redrawPreviews() calls it again when another entry leaves the queue.
previewFn: typeof previewFn === "function" ? previewFn : null,
removePreview: null
};
// Queuing the same target twice replaces the pending change rather than stacking a second one --
// two "+Têkildar" entries would both run, and the second would abort on "Ev beş jixwe heye".
var superseded = false;
if (entry.key) {
for (var i = dsbQueue.entries.length - 1; i >= 0; i--) {
if (dsbQueue.entries[i].key === entry.key) {
dropPreview(dsbQueue.entries[i]);
dsbQueue.entries.splice(i, 1);
superseded = true;
}
}
}
dsbQueue.entries.push(entry);
$status.removeClass("dsb-status-error").addClass("dsb-status-ok").text(
"✓ li rêza guhartinan hate zêdekirin — pêl «Guhartinan qeyd bike» bike da ku bi cih bibe."
);
$save.prop("disabled", false);
if (previewFn) {
try {
var removePreview = previewFn();
if (typeof removePreview === "function") {
entry.removePreview = removePreview;
}
} catch (e) { /* a broken preview should never stop the change from being queued */ }
}
// What the superseded entry drew is gone from the queue, so whatever else is drawn from it is stale.
if (superseded) {
dsbQueue.entries.forEach(function (other) {
if (other !== entry) {
dropPreview(other);
}
});
redrawPreviews();
}
renderQueueBox();
}
// Runs text through the ext.gadget.linkify gadget ($.kuLinkify) and calls back with the linkified result.
// Passes the text through unchanged if the gadget can't be loaded, so it never blocks a save.
var linkifyText = util.linkifyText;
// ---- wikitext location ----
//
// One copy for both editing gadgets, in wikt.edit: add-examples writes into the same places.
var langSectionRange = wt.langSectionRange;
var locateSection = wt.locateSection;
var findDefinitionLine = wt.findDefinitionLine;
/**
* The wikitext of one sense's own block: its "#" line plus everything nested under it -- the "#:" word
* lines, the "#*" citations and any sub-senses -- stopping at the next line of the same depth that
* starts a new sense. Parsed on its own that block renders as exactly the <li> the page shows.
*/
function senseBlockWikitext(pageContent, language, sectionType, sectionOrdinal, definitionLevel) {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var defLine = findDefinitionLine(lines, loc.posLineIndex, definitionLevel);
var depth = definitionLevel.length - 1;
var out = [lines[defLine]];
for (var i = defLine + 1; i < lines.length; i++) {
var m = /^(#+)([:*]?)/.exec(lines[i]);
if (!m || m[1].length < depth) {
break;
}
// Same depth and no ":" or "*" after the hashes is the next sense, not part of this one.
if (m[1].length === depth && !m[2]) {
break;
}
out.push(lines[i]);
}
return out.join("\n");
}
// Inserts or extends a "#: {{tplKey|lang|word1|word2}}" sub-line for one sense (start.js's wordLine()
// format). If a line for this tplKey already exists under the definition, missing words are appended to
// it; otherwise a new line lands after the last existing group line, always before any "#*" citation lines.
function insertSenseWords(pageContent, language, sectionType, sectionOrdinal, definitionLevel, tplKey, words, opts) {
opts = opts || {};
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var defLine = findDefinitionLine(lines, loc.posLineIndex, definitionLevel);
var marker = new Array(definitionLevel.length).join("#"); // definitionLevel.length - 1 "#" characters
// Group 1 is the spelling the page uses, group 2 the parameters: an existing {{hevwate}} stays
// {{hevwate}} rather than being silently renamed to {{hevmane}}.
var tplLineRegex = new RegExp("^" + marker + ":\\s*\\{\\{\\s*(" + aliasPattern(tplKey) + ")\\s*\\|([^}]*)\\}\\}\\s*$");
var groupKeysPattern = SENSE_GROUPS.map(function (g) { return aliasPattern(g.key); }).join("|");
var groupLineRegex = new RegExp("^" + marker + ":\\s*\\{\\{\\s*(" + groupKeysPattern + ")\\s*\\|");
var anyBlockLineRegex = new RegExp("^" + marker + "[:*]");
var lastGroupLine = -1;
for (var i = defLine + 1; i < lines.length; i++) {
if (!anyBlockLineRegex.test(lines[i])) {
break;
}
var tplMatch = tplLineRegex.exec(lines[i]);
if (tplMatch) {
var foundKey = tplMatch[1];
if (opts.replace) {
// Editing an existing list: an empty result means the user cleared it, so drop the line.
if (words.length) {
lines[i] = marker + ": {{" + foundKey + "|" + language + "|" + words.join("|") + "}}";
} else {
lines.splice(i, 1);
}
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
var params = tplMatch[2].split("|"); // [lang, word1, word2, ...]
var existingWords = params.slice(1);
words.forEach(function (w) {
if (existingWords.indexOf(w) === -1) {
existingWords.push(w);
}
});
lines[i] = marker + ": {{" + foundKey + "|" + language + "|" + existingWords.join("|") + "}}";
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
if (groupLineRegex.test(lines[i])) {
lastGroupLine = i;
}
}
var insertAt = lastGroupLine >= 0 ? lastGroupLine + 1 : defLine + 1;
lines.splice(insertAt, 0, marker + ": {{" + tplKey + "|" + language + "|" + words.join("|") + "}}");
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
// Canonical left-to-right order of the small templates that can prefix a definition line's gloss text:
// maneid, then kategori (K), then etîket (e).
var LEADING_TPL_ORDER = startData.leadingTplOrder;
// True for a leading call the advanced panel edits in a box of its own -- those are kept out of the
// gloss box so the same template is never editable in two places at once. Redirect spellings count:
// {{f}} and {{etîket}} are {{e}}.
function hasOwnFieldBox(key) {
return LEADING_TPL_ORDER.indexOf(canonicalTemplateKey(key)) !== -1;
}
// Inserts, extends, or (with opts.replace) overwrites one of the small templates that prefix a definition
// line's gloss text -- {{maneid|lang|id}}, {{K|lang|kat1|...}} or {{e|lang|etîket1|...}}. Under
// opts.replace an empty values list removes the call rather than writing an empty one.
function insertLeadingSenseTpl(pageContent, language, sectionType, sectionOrdinal, definitionLevel, tplKey, values, opts) {
opts = opts || {};
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var defLine = findDefinitionLine(lines, loc.posLineIndex, definitionLevel);
var marker = /^#+/.exec(lines[defLine])[0];
var afterMarker = lines[defLine].slice(marker.length).replace(/^\s+/, "");
// Collecting the whole leading-call run means an existing tplKey call is found and merged wherever it
// already sits, not just when it happens to be first.
var split = splitLeadingCalls(afterMarker);
var calls = split.calls;
var rest = split.rest;
var existing = null;
calls.forEach(function (c) {
if (canonicalTemplateKey(c.key) === tplKey) {
existing = c;
}
});
if (existing) {
if (opts.replace) {
// Editing an existing call: an empty result means the user cleared it, so drop the call.
if (values.length) {
existing.params = [language].concat(values);
} else {
calls = calls.filter(function (c) { return c !== existing; });
}
} else {
var existingVals = existing.params.slice(1);
values.forEach(function (v) {
if (existingVals.indexOf(v) === -1) {
existingVals.push(v);
}
});
existing.params = [language].concat(existingVals);
}
} else if (!values.length) {
return pageContent; // nothing to add, and nothing there to clear
} else {
// Not present yet -- insert right before the first existing call that outranks tplKey in
// LEADING_TPL_ORDER (an unrecognized call always outranks everything), or at the end of the run.
var myRank = LEADING_TPL_ORDER.indexOf(tplKey);
var insertAt = calls.length;
for (var i = 0; i < calls.length; i++) {
var otherRank = LEADING_TPL_ORDER.indexOf(canonicalTemplateKey(calls[i].key));
if (otherRank === -1 || otherRank > myRank) {
insertAt = i;
break;
}
}
calls.splice(insertAt, 0, { key: tplKey, params: [language].concat(values) });
}
var rebuilt = calls.map(function (c) {
return "{{" + c.key + "|" + c.params.join("|") + "}}";
}).join(" ");
lines[defLine] = marker + " " + (rebuilt ? rebuilt + " " : "") + rest;
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
// Replaces just the gloss text of a definition line, leaving any leading template calls untouched --
// the save half of "Maneyê biguhêre" (edit sense).
function updateSenseGloss(pageContent, language, sectionType, sectionOrdinal, definitionLevel, newGloss) {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var defLine = findDefinitionLine(lines, loc.posLineIndex, definitionLevel);
var marker = /^#+/.exec(lines[defLine])[0];
var afterMarker = lines[defLine].slice(marker.length).replace(/^\s+/, "");
// The box holds everything except the calls with their own field, so those are put back in front.
// Dropping "{{mane?}}" here is what clears start.js's placeholder once a real gloss is written.
var kept = splitLeadingCalls(afterMarker).calls.filter(function (c) {
return hasOwnFieldBox(c.key);
});
var leadingText = kept.map(function (c) {
return "{{" + c.key + "|" + c.params.join("|") + "}}";
}).join(" ");
lines[defLine] = marker + " " + (leadingText ? leadingText + " " : "") + newGloss;
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
// Deletes one sense: its definition line plus everything indented under it (its "#:" groups, "#*"
// citations, any sub-senses). Refuses to empty a section, since a POS heading with no senses is worse
// than the sense the user wanted gone.
// A new sense goes directly below the one whose pencil asked for it -- after the lines that sense owns
// (its "#:" examples, "#*" citations and "##" sub-senses), so it reads as the next sense rather than
// landing at the foot of the section. It takes the same marker depth as its neighbour, so asking from
// a "##" sub-sense adds a sibling sub-sense.
function insertNewSense(pageContent, language, sectionType, sectionOrdinal, definitionLevel, text) {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var defLine = findDefinitionLine(lines, loc.posLineIndex, definitionLevel);
var marker = /^#+/.exec(lines[defLine])[0];
var end = defLine + 1;
var ownedRegex = new RegExp("^" + marker + "[#:*]");
while (end < lines.length && ownedRegex.test(lines[end])) {
end++;
}
lines.splice(end, 0, marker + " " + text);
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
function removeSense(pageContent, language, sectionType, sectionOrdinal, definitionLevel) {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var defLine = findDefinitionLine(lines, loc.posLineIndex, definitionLevel);
var marker = /^#+/.exec(lines[defLine])[0];
var end = defLine + 1;
var ownedRegex = new RegExp("^" + marker + "[#:*]");
while (end < lines.length && ownedRegex.test(lines[end])) {
end++;
}
// Top-level senses of this section only -- "##" sub-senses go with their parent either way.
if (definitionLevel.length === 2) {
var siblings = 0;
for (var i = loc.posLineIndex + 1; i < lines.length && !/^==/.test(lines[i]); i++) {
if (/^#[^*#:]/.test(lines[i])) {
siblings++;
}
}
if (siblings < 2) {
throw new SubmitAbort("Ev maneya dawî ya vê beşê ye. Ji bo rakirinê divê tu tevahiya beşê rakî.");
}
}
lines.splice(defLine, end - defLine);
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
// Fetches the page's current wikitext and pulls out just this sense's gloss text to prefill the "Maneyê
// biguhêre" form. A plain read, not part of the hardened save path -- the actual save still re-fetches
// fresh content, so only the prefilled text itself could be a little stale.
// Current contents of one existing subsection, for prefilling the edit form.
// This page's current wikitext.
// One fetch for the whole queue: every preview reads the same revision, and asking per preview meant
// downloading the page again for each queued change. Dropped after a save, when the page has moved on.
var pageContentPromise = null;
function forgetPageContent() {
pageContentPromise = null;
}
function fetchPageContent() {
if (pageContentPromise) {
return pageContentPromise;
}
pageContentPromise = rawPageContent();
// A failed read must not be remembered as the answer.
pageContentPromise.fail(forgetPageContent);
return pageContentPromise;
}
function rawPageContent() {
return api.get({
action: "query",
prop: "revisions",
rvprop: "content",
titles: mw.config.get("wgPageName"),
formatversion: 2
}).then(function (data) {
var page = data.query && data.query.pages && data.query.pages[0];
var content = page && !page.missing && page.revisions && page.revisions[0] && page.revisions[0].content;
if (!content) {
throw new SubmitAbort("Nikare muhtewaya rûpelê bixwîne.");
}
return content;
});
}
function fetchPageSubsection(language, sectionType, sectionOrdinal, subsection) {
return fetchPageContent().then(function (content) {
return readPageSubsection(content, language, sectionType, sectionOrdinal, subsection);
});
}
function fetchSenseFields(language, sectionType, sectionOrdinal, definitionLevel) {
return fetchPageContent().then(function (content) {
return readSenseFields(content, language, sectionType, sectionOrdinal, definitionLevel);
});
}
// Everything the advanced panel can edit about one sense, read off its definition line and the "#:" lines
// under it: the raw gloss (templates included), the leading maneid/K/e values, and each word-list group.
function readSenseFields(pageContent, language, sectionType, sectionOrdinal, definitionLevel) {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var defLine = findDefinitionLine(lines, loc.posLineIndex, definitionLevel);
var defMarker = /^#+/.exec(lines[defLine])[0];
var afterMarker = lines[defLine].slice(defMarker.length).replace(/^\s+/, "");
var leading = {};
splitLeadingCalls(afterMarker).calls.forEach(function (c) {
leading[canonicalTemplateKey(c.key)] = c.params.slice(1);
});
// Only start.js's "{{mane?|lang}}" placeholder is hidden; every other template stays visible so it can
// be edited by hand in the gloss box.
// The gloss box shows the line minus the calls that have their own box ({{maneid}}, {{K}}, {{e}} and
// their redirects) and minus start.js's "{{mane?}}" placeholder. Everything else -- a {{g}} link, a
// {{guharto}} -- stays visible and editable there.
var split = splitLeadingCalls(afterMarker);
var glossCalls = split.calls.filter(function (c) {
return !hasOwnFieldBox(c.key) && canonicalTemplateKey(c.key) !== "mane?";
});
var glossLead = glossCalls.map(function (c) {
return "{{" + c.key + "|" + c.params.join("|") + "}}";
}).join(" ");
var gloss = $.trim((glossLead ? glossLead + " " : "") + split.rest);
var marker = new Array(definitionLevel.length).join("#");
var groupKeysPattern = SENSE_GROUPS.map(function (g) { return aliasPattern(g.key); }).join("|");
var groupLineRegex = new RegExp("^" + marker + ":\\s*\\{\\{\\s*(" + groupKeysPattern + ")\\s*\\|([^}]*)\\}\\}\\s*$");
var anyBlockLineRegex = new RegExp("^" + marker + "[:*]");
var words = {};
for (var i = defLine + 1; i < lines.length; i++) {
if (!anyBlockLineRegex.test(lines[i])) {
break;
}
var m = groupLineRegex.exec(lines[i]);
if (m) {
words[canonicalTemplateKey(m[1])] = m[2].split("|").slice(1);
}
}
return {
gloss: gloss,
maneid: leading.maneid ? leading.maneid[0] : "",
etiket: leading.e || [],
kategori: leading.K || [],
words: words
};
}
// Finds a POS block's body range (up to, but not including, the next heading of level <= 3), and the
// header text + line index of any "====" subsections it already contains.
// depth is the part-of-speech heading's own ("===" on most entries, "====" where the senses are
// grouped under numbered etymologies). Its block runs to the next heading at that depth or shallower,
// and its subsections are the headings one deeper.
function posBlockBounds(lines, posLineIndex, depth) {
depth = depth || 3;
var blockEnd = wt.headingAtOrAbove(depth);
var subsectionHeading = wt.headingAtDepth(depth + 1);
var end = lines.length;
var existingHeaders = [];
for (var i = posLineIndex + 1; i < lines.length; i++) {
if (blockEnd.test(lines[i])) {
end = i;
break;
}
var sub = subsectionHeading.exec(lines[i]);
if (sub) {
existingHeaders.push({ header: $.trim(sub[1]), lineIndex: i });
}
}
return { end: end, existingHeaders: existingHeaders, depth: depth };
}
// Looks up a header's rank in SUBSECTION_ORDER. -1 for a header that isn't one of ours; such a header is
// never used as an insertion anchor.
function subsectionRankForHeader(header) {
return SUBSECTION_ORDER.indexOf(header);
}
// Body line range of one "====" subsection inside a POS block: everything after its heading up to the next
// "====" heading, or the end of the block. Null if the block has no such subsection.
function subsectionBodyRange(bounds, header) {
var headers = bounds.existingHeaders;
for (var i = 0; i < headers.length; i++) {
if (headers[i].header === header) {
return {
start: headers[i].lineIndex + 1,
end: i + 1 < headers.length ? headers[i + 1].lineIndex : bounds.end
};
}
}
return null;
}
// ---- template-parameter editing -------------------------------------------------------------------
// A gloss that is nothing but one template call gets a labelled box per parameter instead of a raw
// wikitext line, the way VisualEditor's template dialog works.
// The single template call a gloss is made of, or null when the line is anything less tidy. {{g|...}} is a
// link inside the gloss rather than the gloss itself, so it neither disqualifies the line nor gets a form.
function glossTemplateTarget(gloss) {
var parsed = parseTemplateCalls(gloss || "");
if (parsed.otherContent) {
return null;
}
var editable = parsed.calls.filter(function (c) {
return canonicalTemplateKey(c.name) !== "g";
});
return editable.length === 1 ? editable[0] : null;
}
// ---- the three places that carry no "====" heading of their own ----------------------------------
// Bilêvkirin is a "===" section above the POS blocks, images sit at the top of a POS block, and the
// h-bnr hatnote goes above the first language heading. Each is located here and then read, written or
// inserted through the same three lines of splice, so nothing below needs to know about them.
var IMAGE_LINE = /^\[\[\s*(?:Wêne|File|Image|Dosya)\s*:/i;
/**
* {lines, langSectionIndex, exists, start, end, insertAt} for a subsection carrying a "place", or null
* for an ordinary one. start..end is the body as it stands; insertAt is where a missing one goes.
*/
function locateLoosePlace(pageContent, language, sectionType, sectionOrdinal, subsection) {
var place = subsection.place;
if (!place) {
return null;
}
var i, end;
if (place === "page-top") {
// Above every language section, so the whole page is the search space. The call is found under
// any of its names, and its own name is carried back so a save never renames it.
var pageLines = pageContent.split("\n");
var call = wt.findHbnrCall(pageContent);
if (call) {
return { lines: pageLines, langSectionIndex: 0, exists: true, call: call,
start: call.lineIndex, end: call.lineIndex + 1, insertAt: call.lineIndex };
}
return { lines: pageLines, langSectionIndex: 0, exists: false, start: 0, end: 0, insertAt: 0 };
}
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
if (place === "pron") {
// One per language, not one per part of speech, so it is looked for above the POS heading.
for (i = 0; i < loc.posLineIndex; i++) {
var h3 = /^===\s*([^=]+?)\s*===/.exec(lines[i]);
if (h3 && $.trim(h3[1]) === subsection.header) {
end = i + 1;
while (end < lines.length && !/^==/.test(lines[end])) {
end++;
}
return { lines: lines, langSectionIndex: loc.langSectionIndex, exists: true,
start: i + 1, end: end, insertAt: i };
}
}
// Straight after the language heading, which puts it before every POS block.
return { lines: lines, langSectionIndex: loc.langSectionIndex, exists: false,
start: 1, end: 1, insertAt: 1 };
}
if (place === "pos-top") {
// Between the conjugation line and the headword template -- where the wizard writes them.
var at = loc.posLineIndex + 1;
while (at < lines.length && /^\{\{\s*ku-tewîn/.test($.trim(lines[at]))) {
at++;
}
var from = at;
while (at < lines.length && IMAGE_LINE.test($.trim(lines[at]))) {
at++;
}
return { lines: lines, langSectionIndex: loc.langSectionIndex, exists: at > from,
start: from, end: at, insertAt: from };
}
return null;
}
// The wikitext a "place" section's content becomes. A word list there is one template call, not a
// {{kol3}} block, so the words are joined into it -- and any ⊕ modifier a row picked up is dropped,
// since a page name is all the call can carry.
function loosePlaceBody(subsection, content, language) {
if (subsection.kind === "images") {
return $.grep($.map(content || [], function (image) {
return wt.buildImageLine({
file: image.file,
caption: wt.captionForLang(image.caption, language),
alt: image.alt,
size: image.size,
extra: image.extra
});
}), Boolean).join("\n");
}
return content;
}
// The words a "place" word list holds, flattened out of its single group. Any ⊕ modifier a row picked
// up is dropped: a page name is all the call can carry.
function loosePlaceWords(content) {
var words = [];
$.each(content || [], function (_, group) {
$.each(group.words || [], function (_, raw) {
var plain = $.trim(util.parseWordValue(raw).word);
if (plain && $.inArray(plain, words) === -1) {
words.push(plain);
}
});
});
return words;
}
// Writes one of those places, inserting it when it is not there yet and replacing it when it is. Only
// Bilêvkirin carries a heading; the other two are bare lines.
function writeLoosePlace(pageContent, language, sectionType, sectionOrdinal, subsection, content) {
var at = locateLoosePlace(pageContent, language, sectionType, sectionOrdinal, subsection);
if (!at) {
throw new SubmitAbort("Cih nehate dîtin: " + subsection.header);
}
// A call naming a subpage -- "{{h-bnr/-ane}}" -- holds a list shared by every page transcluding
// it. Writing here would change this page alone and desync the rest, so the edit is refused and
// the template that actually owns the list is named instead.
if (at.call && at.call.subpage) {
throw new SubmitAbort("Ev rûpel «{{" + at.call.name + "}}» bi kar tîne — lîste li " +
"[[Şablon:" + at.call.subpage + "]] ye, lê wê zêde bike, ne li vê rûpelê.");
}
// A word list here is one template call, so it is built where the existing call's own name is
// known -- editing "{{bnr-h|...}}" must not turn it into "{{h-bnr|...}}".
var body = subsection.kind === "wordlist"
? wt.buildHbnrCall(loosePlaceWords(content), at.call && at.call.name)
: $.trim(content || "");
// A "===" section is separated from what is above and below it by a blank line, the way the rest
// of an entry is written. The other two places are bare lines inside a block and take none.
var heading = subsection.place === "pron";
function blank(index) {
return $.trim(at.lines[index] || "") === "";
}
if (at.exists) {
if (body) {
var replacement = [body];
// end is the next "==" heading; the blank line before it is inside the range being replaced.
if (heading && /^==/.test(at.lines[at.end] || "")) {
replacement.push("");
}
at.lines.splice.apply(at.lines, [at.start, at.end - at.start].concat(replacement));
} else {
// Emptying the box removes the section rather than leaving an empty heading behind.
at.lines.splice(at.start, at.end - at.start);
if (heading) {
at.lines.splice(at.insertAt, 1);
// The blank line that separated it goes with it, rather than doubling up.
if (blank(at.insertAt) && blank(at.insertAt - 1)) {
at.lines.splice(at.insertAt, 1);
}
}
}
} else {
if (!body) {
throw new SubmitAbort("Tiştek nehate nivîsandin.");
}
var block = heading ? ["=== " + subsection.header + " ===", body] : [body];
if (heading) {
if (!blank(at.insertAt - 1)) {
block.unshift("");
}
if (!blank(at.insertAt)) {
block.push("");
}
}
at.lines.splice.apply(at.lines, [at.insertAt, 0].concat(block));
}
return pageContent.slice(0, at.langSectionIndex) + at.lines.join("\n");
}
// What a section body holds: its {{kol3}} groups, the plain bullet list that stands in for them, or
// the raw wikitext when it is neither. A free section is read this way before its kind is known.
function readSubsectionBody(body, subsection) {
if (subsection.kind === "wordlist" || subsection.free) {
var calls = parseWordListCalls(body);
if (calls.length) {
return {
kind: "wordlist",
groups: calls.map(function (call) {
return { sernav: namedParam(call.named, "sernav"), words: call.words };
})
};
}
var bullets = parseBulletWordList(body);
if (bullets) {
// Editable as a word list; saving rewrites the bullets as {{kol3}}.
return { kind: "wordlist", groups: [{ sernav: "", words: bullets }], fromBullets: true };
}
}
return { kind: "text", text: $.trim(body) };
}
// The wikitext a section body becomes after an edit. A wordlist keeps its own template name, language
// and named params ("cure=Jê", ...) and only has its positional words replaced; a text one is
// replaced whole.
function rewriteSubsectionBody(body, language, subsection, content) {
if (subsection.kind !== "wordlist") {
// Replacing the body wholesale drops that separator, so put it back.
return content + (/\n\s*$/.test(body) ? "\n" : "");
}
var calls = parseWordListCalls(body);
if (calls.length) {
// Each call is rewritten where it stands, so text between and around them survives untouched.
// Last to first, so the offsets of the earlier ones stay valid as we go.
var replacement = body;
for (var gi = calls.length - 1; gi >= 0; gi--) {
var call = calls[gi];
var group = content[gi];
// A group emptied in the form drops its call entirely rather than leaving "{{kol3|ku}}".
var rebuilt = group && group.words.length
? buildWordListCall(call.name, call.lang, withNamedParam(call.named, "sernav", group.sernav), group.words)
: "";
replacement = replacement.slice(0, call.start) + rebuilt + replacement.slice(call.end);
}
// Groups the form added beyond the ones already there go after the last of them.
var added = content.slice(calls.length).filter(function (group) {
return group.words.length;
}).map(function (group) {
return buildWordListCall(WORDLIST_TEMPLATE, language,
withNamedParam(["cure=" + subsection.header], "sernav", group.sernav), group.words);
});
if (added.length) {
replacement = replacement.replace(/\s*$/, "") + "\n\n" + added.join("\n\n") + "\n";
}
return replacement.replace(/\n{3,}/g, "\n\n"); // gaps left by any dropped call
}
if (parseBulletWordList(body)) {
// A plain bullet list is replaced wholesale by a real {{kol3}} on its first edit.
return content.map(function (group) {
return buildWordListCall(WORDLIST_TEMPLATE, language,
withNamedParam(["cure=" + subsection.header], "sernav", group.sernav), group.words);
}).join("\n\n") + (/\n\s*$/.test(body) ? "\n" : "");
}
throw new SubmitAbort("Nikare lîsteya vê beşê bixwîne: " + subsection.header);
}
// ---- sections that are not subsections of a part of speech ----
// A "=== Etîmolojî ===" that the parts of speech hang under, a "=== Çavkanî ===" at the foot of a
// language, a heading nobody planned for. Located by its own text, and edited as the wikitext under
// it -- or as a {{kol3}} list when that is what it holds.
function freeSectionBody(pageContent, language, subsection) {
var loc = wt.locateHeadingByText(pageContent, language, subsection.header, subsection.free.ordinal);
return { loc: loc, body: loc.lines.slice(loc.bodyStart, loc.bodyEnd).join("\n") };
}
function readFreeSection(pageContent, language, subsection) {
var at = freeSectionBody(pageContent, language, subsection);
return readSubsectionBody(at.body, subsection);
}
function updateFreeSection(pageContent, language, subsection, content) {
var at = freeSectionBody(pageContent, language, subsection);
at.loc.lines.splice(at.loc.bodyStart, at.loc.bodyEnd - at.loc.bodyStart,
rewriteSubsectionBody(at.body, language, subsection, content));
return pageContent.slice(0, at.loc.langSectionIndex) + at.loc.lines.join("\n");
}
// Current contents of an existing "====" subsection. A wordlist one whose template can't be parsed comes
// back as {kind: "text"} so the caller can still edit it as raw wikitext instead of refusing outright.
function readPageSubsection(pageContent, language, sectionType, sectionOrdinal, subsection) {
if (subsection.free) {
return readFreeSection(pageContent, language, subsection);
}
var loose = locateLoosePlace(pageContent, language, sectionType, sectionOrdinal, subsection);
if (loose) {
if (!loose.exists) {
return null;
}
if (loose.call) {
return { kind: "wordlist", groups: [{ sernav: "", words: loose.call.words }] };
}
var looseBody = $.trim(loose.lines.slice(loose.start, loose.end).join("\n"));
if (subsection.kind === "images") {
return {
kind: "images",
images: $.grep($.map(looseBody.split("\n"), wt.parseImageLine), Boolean)
};
}
if (subsection.kind !== "wordlist") {
return { kind: "text", text: looseBody };
}
// One template call rather than a {{kol3}} block: its positional parameters are the words.
var looseCall = parseTemplateCalls(looseBody).calls[0];
return { kind: "wordlist", groups: [{ sernav: "", words: (looseCall && looseCall.params) || [] }] };
}
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var bounds = posBlockBounds(loc.lines, loc.posLineIndex, loc.depth);
var range = subsectionBodyRange(bounds, subsection.header);
if (!range) {
return null;
}
return readSubsectionBody(loc.lines.slice(range.start, range.end).join("\n"), subsection);
}
// Rewrites an existing "====" subsection in place. A wordlist one keeps its own template name, language and
// named params ("cure=Jê", ...) and only has its positional words replaced; a text one is replaced whole.
function updatePageSubsection(pageContent, language, sectionType, sectionOrdinal, subsection, content) {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var bounds = posBlockBounds(lines, loc.posLineIndex, loc.depth);
var range = subsectionBodyRange(bounds, subsection.header);
if (!range) {
throw new SubmitAbort(util.msg.sectionMissing + subsection.header);
}
var body = lines.slice(range.start, range.end).join("\n");
lines.splice(range.start, range.end - range.start,
rewriteSubsectionBody(body, language, subsection, content));
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
// Inserts a new "==== Header ====" subsection into its SUBSECTION_ORDER position within a POS block, right
// before the first ranked subsection that outranks it, or at the end if none does. Either a
// {{kol3|...}} word list or a plain paragraph, matching start.js's own subsection formats.
function insertPageSubsection(pageContent, language, sectionType, sectionOrdinal, subsection, content) {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var bounds = posBlockBounds(lines, loc.posLineIndex, loc.depth);
var alreadyThere = false;
bounds.existingHeaders.forEach(function (h) {
if (h.header === subsection.header) {
alreadyThere = true;
}
});
if (alreadyThere) {
throw new SubmitAbort("Ev beş jixwe heye: " + subsection.header);
}
var body = subsection.kind === "wordlist"
? content.map(function (group) {
return buildWordListCall(WORDLIST_TEMPLATE, language,
withNamedParam(["cure=" + subsection.header], "sernav", group.sernav), group.words);
}).join("\n\n")
: content;
var myRank = subsectionRankForHeader(subsection.header);
var headers = bounds.existingHeaders;
var insertAt = bounds.end;
for (var i = 0; i < headers.length; i++) {
var otherRank = subsectionRankForHeader(headers[i].header);
if (otherRank !== -1 && otherRank > myRank) {
insertAt = headers[i].lineIndex;
break;
}
}
// Exactly one blank line on each side: the line before the insertion point is normally already blank
// (it separated the previous subsection from the next heading), so only add one where there isn't one.
var block = [];
if (insertAt > 0 && $.trim(lines[insertAt - 1]) !== "") {
block.push("");
}
var marks = wt.headingMarks(bounds.depth + 1);
block.push(marks + " " + subsection.header + " " + marks, body);
if (insertAt < lines.length) {
block.push("");
}
lines.splice.apply(lines, [insertAt, 0].concat(block));
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
// ---- converting an unclassified "=== Mane ===" placeholder heading into a real POS heading ----
// Some pages carry a heading literally named "Mane" (optionally "Mane 2" etc.) instead of a real
// part-of-speech name, tagged right below with {{cure?|<lang>}} (ku.wiktionary's "part of speech not yet
// determined" maintenance flag). It is a section key of its own (sections.MANE_SECTION), so the shared
// locator finds it like any other heading; only the rewriting below is particular to it.
// Renames the "=== Mane ===" placeholder to the chosen POS heading, drops the maintenance tag line right
// after it if present (MANE_TAG_REGEX -- cure? or any of its known aliases), and inserts the matching
// "{{<pos>|<lang>}}" call in its place (skipped when the section has no template of its own).
// Other content under the heading is left untouched.
/**
* Where a section's own headword template sits, as {index, text}, or {index: -1} when it has none.
*
* Not always the line under the heading: a "{{ku-tewîn-lk}}" line and any number of "[[Wêne:...]]"
* images can come first. Everything down to the first definition line is searched, and index doubles
* as the slot a new call belongs in when there is no old one.
*/
function findPosTemplateLine(lines, headingLine, sectionType) {
var oldSpec = posTemplateFor(sectionType);
var oldTemplate = oldSpec ? oldSpec.name : (sectionType === "Biwêj" ? "biwêj" : null);
var oldTemplateRegex = oldTemplate &&
new RegExp("^\\{\\{\\s*" + escapeRegExp(oldTemplate) + "\\s*[|}]");
var at = headingLine + 1;
while (at < lines.length && !/^#/.test(lines[at]) && !/^==/.test(lines[at])) {
if (MANE_TAG_REGEX.test(lines[at]) ||
(oldTemplateRegex && oldTemplateRegex.test(lines[at]))) {
return { index: at, text: lines[at], end: at };
}
at++;
}
return { index: -1, text: "", end: at };
}
// Named parameters merged in order, each written once: a later value replaces an earlier one of the
// same name. Positional values are kept as they come.
function mergeCallParams() {
var out = [];
var seen = {};
$.each(Array.prototype.slice.call(arguments), function (_, list) {
$.each(list || [], function (__, param) {
var m = /^\s*([^=|]+?)\s*=([\s\S]*)$/.exec(param);
if (!m) {
out.push(param);
return;
}
// "z" and "c" are one name to Modul:rêziman, so the form's "c=m" meets the page's "z=".
var key = wt.canonicalParamName(m[1]);
if (!Object.prototype.hasOwnProperty.call(seen, key)) {
seen[key] = out.length;
out.push(param);
return;
}
// The page's own spelling stays when it carried a value; an empty one gives way entirely.
var earlier = /^\s*([^=|]+?)\s*=([\s\S]*)$/.exec(out[seen[key]]);
// "?" asks for a value, so it never replaces one the page already has.
if ($.trim(earlier[2]) && $.trim(m[2]) === "?") {
return;
}
out[seen[key]] =$.trim(earlier[2]) ? earlier[1] + "=" + m[2] : param;
});
});
return out;
}
// What the call on the page already says, minus its language. Carried over only when the new section
// uses that same template -- "z=m" survives Navdêr becoming Formeke navdêrê, while a verb's "c=h-gh"
// has no meaning on a noun and is left behind.
function keptCallParams(line, templateName) {
var call = parseTemplateCalls($.trim(line || "")).calls[0];
if (!call || canonicalTemplateKey(call.name) !== canonicalTemplateKey(templateName)) {
return [];
}
return call.params.slice(1).filter(function (param) {
return /=/.test(param);
});
}
// The headword call a converted section gets, or "" when the new section has none. What the old call
// (found, from findPosTemplateLine) said keeps its place, then the section's own defaults, then what the
// form was filled in with -- each name written once, the last value winning, so a converted call reads
// as small a change from the old one as it can.
function posCallLine(found, language, newSectionType, posParams) {
var spec = posTemplateFor(newSectionType);
if (!spec) {
return "";
}
var kept = found.index !== -1 ? keptCallParams(found.text, spec.name) : [];
// A lemma is not an inflected form, so the old section's form=erê stays behind.
if (spec.params.indexOf("form=erê") === -1) {
kept = kept.filter(function (param) {
return !/^\s*form\s*=\s*erê\s*$/.test(param);
});
}
var params = mergeCallParams(kept, spec.params, posParams ? posParams.split("|") : []);
return "{{" + spec.name + "|" + language + (params.length ? "|" + params.join("|") : "") + "}}";
}
function convertSectionToPos(pageContent, language, sectionType, sectionOrdinal, newSectionType, posParams) {
if (!sectionNames[newSectionType]) {
throw new SubmitAbort("Navê cureyê rêzimanî yê hilbijartî nediyar e.");
}
// locateSection() already handles the "Mane" placeholder as a section type of its own.
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var headingLine = loc.posLineIndex;
var depth = loc.depth;
// A section straight under a numbered group heading takes that heading's place: the group heading
// held nothing but its gloss.
var group = depth > 3 ? wt.numberedGroupAbove(lines, headingLine, depth) : -1;
if (group !== -1) {
lines.splice(group, headingLine - group);
headingLine = group;
depth--;
}
// At the heading's own depth. Written without a number: the renumbering pass below decides every
// number in this language section at once, so one carried over from the old name would just be
// overwritten.
var marks = wt.headingMarks(depth);
lines[headingLine] = marks + " " + newSectionType + " " + marks;
// The line under the heading is either the old section's own template call ("{{biwêj|ku}}") or, for a
// Mane placeholder, the "needs a POS" maintenance tag. Both are replaced by the new POS's call.
// The new call takes the old one's own place -- or, when there was none, the slot just above the
// definitions, which is where the wizard writes it.
var found = findPosTemplateLine(lines, headingLine, sectionType);
var insertAt = found.index !== -1 ? found.index : found.end;
if (found.index !== -1) {
lines.splice(found.index, 1);
}
var call = posCallLine(found, language, newSectionType, posParams);
if (call) {
lines.splice(insertAt, 0, call);
}
// Renaming a section can make it repeat one the page already had, and can equally leave a name that
// is no longer repeated. Both are settled by renumbering the whole language section: a "Rengdêr"
// beside a new "Rengdêr" becomes "Rengdêr 1" and "Rengdêr 2", and a number left behind by a name
// that is now unique is dropped.
var body = lines.join("\n");
var nextLang = body.indexOf("\n== ", 1);
var end = nextLang === -1 ? body.length : nextLang;
body = wt.numberRepeatedPosHeadings(body.slice(0, end)) + body.slice(end);
return pageContent.slice(0, loc.langSectionIndex) + body;
}
// ---- "guhartina pêşketî" (advanced editing): combine several independent per-sense edits into ONE save ----
// Threads pageContent through the same pure functions every single-field form uses, applying only the
// pieces the caller actually filled in. A piece that conflicts with the page (maneid already set, a
// subsection that's already there) is skipped with a warning rather than failing the whole save. Throws
// SubmitAbort if nothing was filled in, so this can never turn into a no-op save.
function applyAdvancedChanges(pageContent, language, sectionType, sectionOrdinal, definitionLevel, changes) {
var content = pageContent;
var warnings = [];
var appliedAnything = false;
if (changes.gloss) {
content = updateSenseGloss(content, language, sectionType, sectionOrdinal, definitionLevel, changes.gloss);
appliedAnything = true;
}
if (changes.newSense) {
content = insertNewSense(content, language, sectionType, sectionOrdinal, definitionLevel, changes.newSense);
appliedAnything = true;
}
// Every box below is prefilled with what the sense already has, so a save replaces that value rather
// than merging into it -- and clearing a box removes the template. changes.changed marks the boxes
// whose contents actually differ from the page, so an opened-but-untouched box writes nothing.
var replace = { replace: true };
if (changes.changed.maneid) {
content = insertLeadingSenseTpl(content, language, sectionType, sectionOrdinal, definitionLevel, "maneid", changes.maneid ? [changes.maneid] : [], replace);
appliedAnything = true;
}
if (changes.changed.etiket) {
content = insertLeadingSenseTpl(content, language, sectionType, sectionOrdinal, definitionLevel, "e", changes.etiket, replace);
appliedAnything = true;
}
if (changes.changed.kategori) {
content = insertLeadingSenseTpl(content, language, sectionType, sectionOrdinal, definitionLevel, "K", changes.kategori, replace);
appliedAnything = true;
}
SENSE_GROUPS.forEach(function (group) {
if (changes.changed["word:" + group.key]) {
content = insertSenseWords(content, language, sectionType, sectionOrdinal, definitionLevel, group.key, changes.words[group.key], replace);
appliedAnything = true;
}
});
if (!appliedAnything) {
throw new SubmitAbort("Tiştek nehat guhartin.");
}
return { content: content, warnings: warnings };
}
// ---- per-definition UI: a small always-visible "+" toggle (no hover, so it works on mobile/touch too) ----
// A growing list of word rows. The row itself is util.js's, shared with the wizard; this owns the list:
// typing in the last row appends a fresh empty one, so there is always somewhere to type.
var WORDLIST_COLUMN_THRESHOLD = 6;
function buildWordListField(placeholder, opts) {
opts = opts || {};
var $list = $("<ol class='dsb-wordlist'></ol>");
// One column is a long scroll once a list gets past a handful of words. The class only asks for
// two; the stylesheet still gives one on a narrow screen. Two columns inside the panel's usual
// 520px would leave each word about 250px, so the panel drops its width cap for as long as any
// list in it is in two columns -- checked across the whole panel, since a "Herwiha binêre" holds
// several groups and one of them going wide settles it for all.
function updateColumns() {
$list.toggleClass("dsb-wordlist-2col",
$list.children("li").length > WORDLIST_COLUMN_THRESHOLD);
var $panel = $list.closest(".dsb-subsection-panel");
$panel.toggleClass("dsb-panel-wide", !!$panel.find(".dsb-wordlist-2col").length);
}
function makeRow() {
var row = util.makeModifierRow({
prefix: "dsb-",
placeholder: placeholder,
modTitle: util.msg.addModifier,
removeTitle: util.msg.removeWord,
removeModTitle: util.msg.removeModifier,
modifiers: opts.modifiers,
// "bixe qutiyan" puts each comma-separated word in a row of its own, right after the row it
// was typed in, and the caret follows to the last of them. One word per row is also one
// word per template parameter, which both {{hevmane}} and {{kol3}} read the same way --
// a comma left inside a parameter would not be.
onSplit: function (rest) {
var $after = row.$li;
var last = null;
rest.forEach(function (raw) {
var extra = makeRow();
$after.after(extra.$li);
$after = extra.$li;
if (raw) extra.setRaw(raw);
last = extra;
});
// Always an empty row at the end to type the next word into.
var $tail = $list.children("li").last();
if ($.trim($tail.data("row").getRaw())) {
last = makeRow();
$list.append(last.$li);
}
updateColumns();
if (last) last.focus();
},
// raw === null means the row removed itself, so the count has changed either way.
onChange: function (raw) {
if (raw !== null) {
var isLast = row.$li.is($list.children("li").last());
if (isLast && raw) $list.append(makeRow().$li);
}
updateColumns();
}
});
row.$li.data("row", row);
return row;
}
$list.append(makeRow().$li);
return {
$element: $list,
values: function () {
var vals = [];
$list.children("li").each(function () {
var raw = $(this).data("row").getRaw();
if (raw) vals.push(raw);
});
return vals;
},
// Replaces the whole list, keeping one empty trailing row to type into.
setValues: function (raws) {
$list.empty();
raws.forEach(function (raw) {
var row = makeRow();
$list.append(row.$li);
row.setRaw(raw);
});
$list.append(makeRow().$li);
updateColumns();
},
focus: function () {
$list.children("li").first().data("row").focus();
}
};
}
// Plain removable-tag field, used for Etîket and Kategorî (simple value lists with no per-word modifiers).
// Enter or "," commits the current text as a tag; Backspace on an empty input removes the last tag;
// values() also commits whatever is still typed but not yet confirmed.
function buildTagField(placeholder) {
var $input = $("<input type='text' class='dsb-input dsb-tag-input'>").attr("placeholder", placeholder);
var $element = $("<span class='dsb-tagfield'></span>").append($input);
function currentValues() {
var vals = [];
$element.children(".dsb-tag").each(function () {
vals.push($(this).data("value"));
});
return vals;
}
function addTag(val) {
val = $.trim(val || "");
if (!val || currentValues().indexOf(val) !== -1) {
return;
}
var $tag = $("<span class='dsb-tag'></span>").text(val).data("value", val);
var $remove = $("<button type='button' class='dsb-tag-remove' title='Jê bibe'>×</button>");
$remove.on("click", function () {
$tag.remove();
$input.trigger("focus");
});
$tag.append($remove);
$tag.insertBefore($input);
}
$input.on("keydown", function (e) {
if (e.which === 13 || e.which === 188) { // Enter or comma
e.preventDefault();
addTag($input.val());
$input.val("");
} else if (e.which === 8 && !$input.val()) {
$element.children(".dsb-tag").last().remove();
}
});
$input.on("blur", function () {
addTag($input.val());
$input.val("");
});
return {
$element: $element,
// Replaces every tag, for prefilling from what the sense already has.
setValues: function (vals) {
$element.children(".dsb-tag").remove();
vals.forEach(function (v) {
addTag(v);
});
$input.val("");
},
values: function () {
addTag($input.val());
$input.val("");
return currentValues();
},
focus: function () {
$input.trigger("focus");
}
};
}
// The wizard's own etymology templates, straight out of data-main.json's "etymTpls" -- the same list start.js's
// insert menu offers, so the two can never drift apart. CODE/SOURCELANG are its placeholders.
// The wizard's own template lists, straight out of data-main.json, so both gadgets offer the same
// templates in the same order. A pronunciation template names the languages it is for; one that names
// none is offered everywhere.
function templateChoices(pool, language) {
var defs = startData[pool] || {};
return Object.keys(defs).filter(function (name) {
var langs = defs[name].langs;
return !langs || !langs.length || $.inArray(language, langs) !== -1;
}).map(function (name) {
return { name: name, desc: defs[name].desc, snippet: defs[name].snippet };
});
}
// One template call inside the surface: shown as its own wikitext, but atomic -- contenteditable="false"
// keeps the caret out of it, so it can only be changed through the dialog.
function makeTemplateChip(wikitext) {
// The × is its own element rather than the chip's only way out: on a phone there is no reliable
// backspace onto a contenteditable="false" span, so without it the dialog is the only way to take
// a call off the line. readChipSurface() reads chips from data-wikitext, so the extra child does
// not reach the saved text.
var $remove = $("<span class='dsb-tpl-chip-remove' contenteditable='false'>×</span>")
.attr("title", "Vê şablonê jê bibe");
return $("<span class='dsb-tpl-chip' contenteditable='false' role='button' tabindex='0'></span>")
.attr("data-wikitext", wikitext)
.attr("title", "Bitikîne da ku parametreyan biguhêrî")
.text(wikitext)
.append($remove);
}
// wikitext -> surface DOM: template calls become chips, everything else stays plain editable text.
function renderChipSurface($surface, wikitext) {
var text = wikitext || "";
var calls = parseTemplateCalls(text).calls;
var pos = 0;
$surface.empty();
calls.forEach(function (call) {
if (call.start > pos) {
$surface.append(document.createTextNode(text.slice(pos, call.start)));
}
$surface.append(makeTemplateChip(text.slice(call.start, call.end)));
pos = call.end;
});
if (pos < text.length) {
$surface.append(document.createTextNode(text.slice(pos)));
}
}
// surface DOM -> wikitext. Chips give back their stored call; anything the browser inserted while typing
// (a <br>, a wrapper <div>) degrades to its text, so the result is always plain wikitext.
function readChipSurface(node) {
var out = "";
$(node).contents().each(function () {
if (this.nodeType === 3) {
out += this.nodeValue;
return;
}
var $el = $(this);
if ($el.hasClass("dsb-tpl-chip")) {
out += $el.attr("data-wikitext");
return;
}
if (this.nodeName === "BR") {
out += "\n";
return;
}
if (this.nodeName === "DIV" || this.nodeName === "P") {
out += "\n" + readChipSurface(this);
return;
}
out += readChipSurface(this);
});
return out;
}
// Drops a node in at the caret, or at the end when the caret is not inside the surface.
function insertAtCaret($surface, node) {
var sel = window.getSelection();
if (sel && sel.rangeCount && $.contains($surface.get(0), sel.getRangeAt(0).commonAncestorContainer)) {
var range = sel.getRangeAt(0);
range.deleteContents();
range.insertNode(node);
range.setStartAfter(node);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
} else {
$surface.append(node);
}
}
// The template-param dialog (openTemplateDialog/buildTemplateParamForm) moved to wikt.cite, shared with
// start.js's own Etîmolojî and Çavkanî pickers: cite.openTemplateDialog / cite.buildTemplateParamForm.
// Each new example queued is its own entry, so several can be waiting at once.
var exampleSeq = 0;
// Takes the save over from add-examples while its form is open for one sense: its "Qeyd bike" becomes
// "Lê zêde bike" and the edit joins the pending queue instead of publishing on its own, so examples and
// whatever else is queued go up as one edit. Installed on every open, so the handler always closes over
// the sense whose form is actually open. ctx: {$li, $h3, language, sectionType, sectionOrdinal,
// definitionLevel, $save, $status, afterQueue}.
function takeOverExampleSave(ctx) {
if (!(wikt.addExamples && wikt.addExamples.setSubmitHandler)) {
return; // older add-examples: it publishes on its own, as before
}
// Lets the form read an example as the queue has already changed it.
wikt.addExamples.pendingContent = queuedContent;
wikt.addExamples.setSubmitHandler(function (job) {
exampleSeq++;
queueChange(
placeLabel(ctx.language, ctx.sectionType, ctx.$h3),
// add-examples' own summary, so the combined edit still credits the tool and names its
// version. The trailing stop goes: this is one item in a list, not a sentence of its own.
(job.summary || "nimûne/jêgirtin hate zêdekirin").replace(/\.\s*$/, ""),
function (pageContent) {
// add-examples builds against a revision object, and returns {text, summary}.
var built = job.transform({ content: pageContent });
return { content: built.text, warnings: [] };
},
ctx.$save,
ctx.$status,
// Drawn like any other queued change, in the sense it belongs to.
function () {
return previewSense(ctx.$li, ctx.language, ctx.sectionType, ctx.sectionOrdinal, ctx.definitionLevel);
},
// An edit to an example already on the page brings its own key, so editing it again replaces it.
{ key: job.key || ("example:" + ctx.sectionType + "|" + ctx.sectionOrdinal + "|" +
ctx.definitionLevel.join(".") + "|" + exampleSeq) }
);
job.done();
if (ctx.afterQueue) {
ctx.afterQueue();
}
});
}
// Opens add-examples' form on one example already under a sense -- dsb.js's pencil on it. Its save joins
// the queue like any other change.
function editExample($li, $h3, language, sectionType, sectionOrdinal, definitionLevel, itemEl) {
takeOverExampleSave({ $li: $li, $h3: $h3, language: language, sectionType: sectionType,
sectionOrdinal: sectionOrdinal, definitionLevel: definitionLevel, $save: $(), $status: $(),
afterQueue: openQueueBox });
if (!(wikt.addExamples && wikt.addExamples.editFor && wikt.addExamples.editFor($li.get(0), itemEl))) {
mw.notify("Amûra nimûneyan li vê maneyê nehat dîtin.");
}
}
// Builds the whole "guhartina pêşketî" panel into $quickRow: gloss and the two most-used word lists
// (Hevmane/Dijmane) sit directly in the picker row; the remaining word lists, maneid, etîket, kategorî
// and Nimûne/jêgirtin live behind a "Hevmane û Bêhtir" dropdown; one shared "Lê zêde bike" queues everything the
// user filled in as a single edit via applyAdvancedChanges() and queueChange(). Nimûne/jêgirtin is an
// immediate proxy-click into a different gadget's own form, not part of this combined save.
function buildAdvancedPanel($quickRow, $li, $h3, language, sectionType, sectionOrdinal, definitionLevel, isLastSense) {
// "+" marks the actions that add something, matching the dropdown's own "+ Hevmane" labels.
var $nimune = $("<a href='#' class='dsb-quick-link'></a>").text("+ Nimûne/jêgirtin");
$nimune.on("click", function (e) {
e.preventDefault();
takeOverExampleSave({ $li: $li, $h3: $h3, language: language, sectionType: sectionType,
sectionOrdinal: sectionOrdinal, definitionLevel: definitionLevel, $save: $save, $status: $status });
// Toggle, so clicking this a second time closes the form again. add-examples builds nothing
// on the page of its own, so this is the only way its form opens.
var drive = wikt.addExamples.toggleFor || wikt.addExamples.openFor;
if (!(drive && drive($li.get(0)))) {
mw.notify("Amûra nimûneyan li vê maneyê nehat dîtin.");
}
$moreMenu.slideUp();
});
// getters holds one function per currently-open box (key -> () => value); collectChanges() only ever
// reads from a box the user actually clicked to open.
var getters = {};
// What the sense currently holds, fetched once and shared by every box that prefills from it.
// collectChanges() also diffs against it, so an untouched box never queues a change.
var original = null;
var sensePromise = null;
function loadSense() {
if (!sensePromise) {
sensePromise = fetchSenseFields(language, sectionType, sectionOrdinal, definitionLevel).then(function (fields) {
original = fields;
return fields;
});
}
return sensePromise;
}
function sameList(a, b) {
return (a || []).join("\u0000") === (b || []).join("\u0000");
}
// -- gloss ("Maneyê biguhêre" -- the one box that EDITS the sense's wording; everything else here only ADDS) --
function addGlossField() {
var $fields = $("<div class='dsb-gloss-fields'></div>");
var $textarea = $("<textarea class='dsb-input dsb-edit-gloss' rows='2'></textarea>")
.attr("placeholder", util.msg.loading).prop("disabled", true);
var $autoLink = $("<button type='button' class='dsb-autolink'>Oto-lînk bike</button>").prop("disabled", true);
var $toRaw = $("<a href='#' class='dsb-quick-link dsb-gloss-raw'>Wekî wîkîtekst biguhêre</a>").hide();
var $status = $("<span class='dsb-status'></span>");
var originalGloss = null;
var tplForm = null; // set while the parameter boxes are showing instead of the raw line
var tplCall = null;
// The gloss is the definition followed by {{îzah|…}}. That is one field on the page, so it stays
// one box here, and the two are joined back into a single gloss on save. A non-Kurdish gloss is a
// translation and its explanation, so both boxes open together. A Kurdish sense is its own
// definition: its îzah box opens when the gloss already carries one, or from "+ Îzah".
var offerIzah = language !== "ku";
var $izah = $("<textarea class='dsb-input dsb-edit-izah' rows='2'></textarea>")
.attr("placeholder", offerIzah ? "Îzaha maneyî… ji bo school-a îngilîzî 'cihê tehsîlgirtinê'" : "Îzaha maneyî…");
var $izahWrap = $("<div class='dsb-izah-wrap'></div>")
.append($("<label class='dsb-adv-label'>Îzaha maneyî</label>").append($izah))
.toggle(offerIzah);
var $addIzah = $("<a href='#' class='dsb-quick-link dsb-gloss-add-izah'>+ Îzah</a>").toggle(!offerIzah);
$addIzah.on("click", function (e) {
e.preventDefault();
$addIzah.hide();
$izahWrap.show();
$izah.trigger("focus");
});
$fields.append($textarea);
// The template form edits one call inside the line, so its result is spliced back into the rest.
function currentGloss() {
if (tplForm) {
return $.trim(originalGloss.slice(0, tplCall.start) + tplForm.values() + originalGloss.slice(tplCall.end));
}
return joinGloss($.trim($textarea.val()), $.trim($izah.val()));
}
function showRaw() {
tplForm = null;
tplCall = null;
$fields.empty().append($textarea);
$autoLink.show();
$toRaw.hide();
$addIzah.toggle($izahWrap.css("display") === "none");
}
loadSense().then(
function (fields) {
originalGloss = fields.gloss;
// An existing gloss that already ends in {{îzah|…}} comes apart into the two boxes, so
// editing it does not mean editing template syntax by hand.
var parts = util.splitTercimeIzah(fields.gloss);
$textarea.val(parts ? $.trim(parts.tercime) : fields.gloss)
.prop("disabled", false)
// A non-Kurdish gloss is a translation; the box under it takes the explanation.
.attr("placeholder", offerIzah ? util.msg.tercimePrompt : "Maneyê binivîse…");
if (parts) {
$izah.val($.trim(parts.izah));
$izahWrap.show();
$addIzah.hide();
}
$autoLink.prop("disabled", false);
var target = glossTemplateTarget(fields.gloss);
if (!target) {
return;
}
fetchTemplateData(target.name).then(function (td) {
// The fetch is async; leave the raw box alone if it has already been typed into.
if ($.trim($textarea.val()) !== $.trim(originalGloss)) {
return;
}
tplCall = target;
tplForm = cite.buildTemplateParamForm(target, td);
$fields.empty().append(tplForm.$element);
$autoLink.hide(); // linkifying a bare template call makes no sense
// A gloss that is one template call carries no separate explanation.
$izahWrap.hide();
$addIzah.hide();
$toRaw.show();
});
},
function (codeOrAbort, result) {
$textarea.attr("placeholder", "");
var message = codeOrAbort instanceof SubmitAbort ? codeOrAbort.message : "Nikare maneya heyî bar bike: " + apiErrorMessage(codeOrAbort, result);
$status.removeClass("dsb-status-ok").addClass("dsb-status-error").text(message);
}
);
$autoLink.on("click", function () {
var mane = $.trim($textarea.val());
var izah = $.trim($izah.val());
if (!mane && !izah) {
return;
}
$autoLink.prop("disabled", true);
$status.removeClass("dsb-status-ok dsb-status-error").text("Tê lînkkirin…");
linkifyGlossParts(mane, izah, function (linkedMane, linkedIzah, error) {
$textarea.val(linkedMane);
$izah.val(linkedIzah);
$autoLink.prop("disabled", false);
if (error) {
$status.addClass("dsb-status-error").text(error);
} else {
$status.text("");
}
});
});
$toRaw.on("click", function (e) {
e.preventDefault();
$textarea.val(currentGloss()); // read the form before showRaw() discards it
showRaw();
});
getters.gloss = function () {
var v = currentGloss();
return (v && v !== originalGloss) ? v : null;
};
return $("<div class='dsb-adv-group'></div>").append(
$("<div class='dsb-adv-label'></div>").text("Mane"),
$fields,
$izahWrap,
$("<div class='dsb-form-actions'></div>").append($autoLink, $addIzah, $toRaw, $status)
);
}
// -- one SENSE_GROUPS word-list box (Hevmane/Dijmane/etc.) --
function addWordField(group) {
var field = buildWordListField(group.label + "…");
loadSense().then(function (fields) {
var current = fields.words[group.key];
if (current && current.length) {
field.setValues(current);
}
});
getters["word:" + group.key] = function () {
return field.values();
};
// The hint is senseGroups' own, so the box carries the same example sentence the wizard puts
// beside its copy of this list.
var $label = $("<div class='dsb-adv-label'></div>").text(group.label);
if (group.hint) {
$label.append($("<span class='dsb-adv-hint'></span>").text(group.hint));
}
return $("<div class='dsb-adv-group'></div>").append($label, field.$element);
}
// -- maneid (single value; re-adding one that already exists is rejected -- see applyAdvancedChanges) --
function addNewSenseField() {
var $textarea = $("<textarea class='dsb-input' rows='2'></textarea>").attr("placeholder", "Maneya nû binivîse…");
getters.newSense = function () {
return $.trim($textarea.val());
};
return $("<div class='dsb-adv-group'></div>").append(
$("<div class='dsb-adv-label'></div>").text(util.msg.anotherSense),
$textarea
);
}
function addManeidField() {
var $input = $("<input type='text' class='dsb-input' placeholder='id…'>");
loadSense().then(function (fields) {
$input.val(fields.maneid);
});
getters.maneid = function () {
return $.trim($input.val());
};
return $("<div class='dsb-adv-group'></div>").append($("<div class='dsb-adv-label'></div>").text("Maneid"), $input);
}
// -- etîket / kategorî --
function addEtiketField() {
var field = buildTagField("Etîket…");
loadSense().then(function (fields) {
field.setValues(fields.etiket);
});
getters.etiket = function () {
return field.values();
};
return $("<div class='dsb-adv-group'></div>").append($("<div class='dsb-adv-label'></div>").text("Etîket"), field.$element);
}
function addKategoriField() {
var field = buildTagField("Kategorî…");
loadSense().then(function (fields) {
field.setValues(fields.kategori);
});
getters.kategori = function () {
return field.values();
};
return $("<div class='dsb-adv-group'></div>").append($("<div class='dsb-adv-label'></div>").text("Kategorî"), field.$element);
}
// -- the picker row: the gloss and Nimûne/jêgirtin links sit directly in the row; every word group and
// the rest live behind a "Hevmane û Bêhtir" dropdown. Clicking any link builds and appends its box
// to $openBoxes and hides that link; the box's own "×" removes it, clears its getter, and un-hides the
// link -- several boxes can be open at once. No "Beşek (====)" entry here: buildSubsectionPanel()'s
// own "+ Beşek lê zêde bike" trigger already covers page-level subsections. ----
var FIELD_DEFS = [{ key: "gloss", label: "Maneyê biguhêre", build: addGlossField, primary: true }]
.concat(SENSE_GROUPS.map(function (group) {
return { key: "word:" + group.key, label: group.label, build: function () { return addWordField(group); }, primary: false };
}))
.concat([
{ key: "maneid", label: "Maneid", build: addManeidField, primary: false },
{ key: "etiket", label: "Etîket", build: addEtiketField, primary: false },
{ key: "kategori", label: "Kategorî", build: addKategoriField, primary: false }
]);
// Every sense offers this: the new one lands directly below whichever sense asked for it (see
// insertNewSense), so it is no longer only the last one that can add to the end.
FIELD_DEFS.push({ key: "newSense", label: util.msg.addAnotherSense, build: addNewSenseField, primary: false });
var $picker = $("<div class='dsb-field-picker'></div>");
var $moreMenu = $("<div class='dsb-more-menu'></div>").hide();
var $openBoxes = $("<div class='dsb-open-boxes'></div>");
// $actions (built below) stays hidden until at least one field box is actually open -- otherwise
// "Lê zêde bike" sits there enabled with nothing to queue, which just produces an empty no-op change.
var openBoxCount = 0;
function updateActionsVisibility() {
$actions.toggle(openBoxCount > 0);
}
function makeFieldLink(def) {
var $link = $("<a href='#' class='dsb-quick-link dsb-field-pick'></a>").text(def.key === "gloss" ? def.label : "+ " + def.label);
$link.on("click", function (e) {
e.preventDefault();
openQueueBox();
var $box = def.build();
var $remove = $("<a href='#' class='dsb-box-remove' title='Rake'>×</a>");
$remove.on("click", function (e2) {
e2.preventDefault();
delete getters[def.key];
$box.remove();
$link.show();
openBoxCount--;
updateActionsVisibility();
});
$box.prepend($remove);
$openBoxes.append($box);
util.initManualResize($box);
$link.hide();
$moreMenu.slideUp();
openBoxCount++;
updateActionsVisibility();
});
return $link;
}
// Destructive, so it lives at the bottom of the dropdown behind a confirmation, and shares the
// sense's queue key: deleting supersedes any edit still pending on the same sense.
function makeDeleteLink() {
var $link = $("<a href='#' class='dsb-quick-link dsb-field-delete'>Maneyê rake</a>");
$link.on("click", function (e) {
e.preventDefault();
$moreMenu.slideUp();
OO.ui.confirm("Ev mane û hemû rêzên bin wê dê werin rakirin. Dewam bike?").done(function (ok) {
if (!ok) {
return;
}
queueChange(
placeLabel(language, sectionType, $h3),
"maneyek hate rakirin",
function (baseContent) {
return removeSense(baseContent, language, sectionType, sectionOrdinal, definitionLevel);
},
$save,
$note,
function () {
$li.addClass("dsb-sense-doomed");
return function () {
$li.removeClass("dsb-sense-doomed");
};
},
{ key: senseQueueKey() }
);
$note.show();
});
});
return $link;
}
// Only the links the row actually shows are built now; the dropdown's nine are 43% of the work and
// nobody sees them until "Hevmane û Bêhtir" is clicked, so buildMenuLinks() waits for that.
FIELD_DEFS.forEach(function (def) {
if (!def.primary) {
return;
}
$picker.append(makeFieldLink(def));
if (def.key === "gloss") {
$picker.append($nimune); // directly after "Maneyê biguhêre"
}
});
// Three sections, ruled apart: what labels this sense, the word lists that hang off it, and adding
// another sense. "Maneyê rake" is appended after these by makeDeleteLink() and so closes the last
// one. The word lists follow senseGroups' "order" -- that array's own order is the order the "#:"
// lines are written to the page in, which is not a reading order; the wizard's boxes read the same
// field, so both gadgets list them alike. Anything not named here still appears, after these, so a
// field added later is never silently dropped from the menu.
var MENU_GROUPS = [
["etiket", "kategori", "maneid"],
SENSE_GROUPS.slice().sort(function (a, b) {
return (a.order || 0) - (b.order || 0);
}).map(function (g) {
return "word:" + g.key;
}),
["newSense"]
];
var menuBuilt = false;
function buildMenuLinks() {
if (menuBuilt) {
return;
}
menuBuilt = true;
var byKey = {};
FIELD_DEFS.forEach(function (def) {
if (!def.primary) {
byKey[def.key] = def;
}
});
var placed = {};
MENU_GROUPS.forEach(function (group, groupIndex) {
var defs = group.map(function (key) {
return byKey[key];
}).filter(Boolean);
defs.forEach(function (def, i) {
placed[def.key] = true;
var $link = makeFieldLink(def);
// The rule goes on the first entry of every group after the first.
if (groupIndex > 0 && i === 0) {
$link.addClass("dsb-menu-group-start");
}
$moreMenu.append($link);
});
});
FIELD_DEFS.forEach(function (def) {
if (!def.primary && !placed[def.key]) {
$moreMenu.append(makeFieldLink(def));
}
});
$moreMenu.append(makeDeleteLink());
}
// Lives inside $picker (not appended straight to $quickRow) so .dsb-more-menu's CSS can position it as
// an overlay anchored to $picker, instead of pushing $openBoxes/the rest of the page down when it opens.
var $moreArrow = $("<span class='dsb-more-arrow'></span>").text("▸");
var $moreToggle = $("<a href='#' class='dsb-quick-link dsb-more-toggle'></a>")
.text(util.msg.moreMenu + " ").append($moreArrow);
$moreToggle.on("click", function (e) {
e.preventDefault();
buildMenuLinks();
$moreMenu.slideToggle(function () {
$moreArrow.text($moreMenu.is(":visible") ? "▾" : "▸");
});
});
$picker.append($moreToggle, $moreMenu);
// Outside clicks are handled by one shared document listener (see closeMoreMenus), not one per panel:
// hovering a sense builds its panel, so a long entry would otherwise accumulate dozens of them.
$moreMenu.data("dsbArrow", $moreArrow);
// -- combined save: whatever boxes are open right now, queued together via queueChange(); the actual
// save happens when the user clicks "Guhartinan qeyd bike" on the floating queue box. ----
// Small "not saved yet" preview of everything this queued change will do, appended right under the
// sense's gloss (plain text, not a real render of {{hevmane}}/etc.).
function collectChanges() {
var base = original || { maneid: "", etiket: [], kategori: [], words: {} };
var changed = {};
var words = {};
SENSE_GROUPS.forEach(function (group) {
var getter = getters["word:" + group.key];
words[group.key] = getter ? getter() : [];
if (getter && !sameList(words[group.key], base.words[group.key])) {
changed["word:" + group.key] = true;
}
});
var maneid = getters.maneid ? getters.maneid() : "";
var etiket = getters.etiket ? getters.etiket() : [];
var kategori = getters.kategori ? getters.kategori() : [];
if (getters.maneid && maneid !== (base.maneid || "")) {
changed.maneid = true;
}
if (getters.etiket && !sameList(etiket, base.etiket)) {
changed.etiket = true;
}
if (getters.kategori && !sameList(kategori, base.kategori)) {
changed.kategori = true;
}
return {
gloss: getters.gloss ? getters.gloss() : null,
newSense: getters.newSense ? getters.newSense() : null,
maneid: maneid,
etiket: etiket,
kategori: kategori,
words: words,
changed: changed
};
}
// Both the field save and "Maneyê rake" queue under this, so the later one supersedes the earlier.
function senseQueueKey() {
return "sense:" + sectionType + "|" + sectionOrdinal + "|" + definitionLevel.join(".");
}
var $save = $("<button type='button' class='dsb-save'></button>").text(util.msg.add);
// Where queueChange() writes its confirmation for actions that have no box of their own.
var $note = $("<div class='dsb-status dsb-quick-note'></div>").hide();
var $cancel = $("<button type='button' class='dsb-cancel'></button>").text(util.msg.cancel);
var $status = $("<span class='dsb-status'></span>");
var $actions = $("<div class='dsb-form-actions'></div>").append($save, $cancel, $status).hide();
$cancel.on("click", function () {
// Close the boxes too, not just the row. Hiding alone left every field still open with its
// text in it, so the next click on the pencil reopened the half-filled panel that had just
// been cancelled. Each box's own "x" already clears its getter, restores its link and updates
// the counter, so those are triggered rather than repeating the teardown here.
$openBoxes.find(".dsb-box-remove").trigger("click");
$note.hide().text("");
// dsb.js owns the pencil's pinned state; tell it rather than hiding behind its back, or the
// next click on the pencil would only toggle "pinned" off and appear to do nothing.
$quickRow.trigger("dsbCancel");
hideQueueIfIdle();
});
$save.on("click", function () {
var changes = collectChanges();
function afterLinkify() {
var parts = senseChangeLabels(changes);
queueChange(
placeLabel(language, sectionType, $h3),
parts.length
? parts.join(", ") + (parts.length > 1 ? " hatin guhartin" : " hate guhartin")
: "mane hate guhartin",
function (baseContent) {
return applyAdvancedChanges(baseContent, language, sectionType, sectionOrdinal, definitionLevel, changes);
},
$save,
$status,
function () {
var undoPreview = previewSense($li, language, sectionType, sectionOrdinal,
definitionLevel);
// A new sense has no place on the page yet, so it gets a row of its own directly
// after this one -- the list numbers it, so where it will land is visible.
var $newSense = null;
if (changes.newSense) {
$newSense = $("<li class='dsb-sense-preview'></li>");
$li.after($newSense);
fillWithRendered($newSense, changes.newSense, function () {
$newSense.text(changes.newSense);
});
}
return function () {
undoPreview();
if ($newSense) {
$newSense.remove();
}
};
},
// Re-saving this same sense supersedes its pending change rather than queuing another.
{ key: senseQueueKey() }
);
}
$save.prop("disabled", true);
// Auto-linkify the gloss as part of this save, but only a half nobody has linked by hand.
// Split back into its two halves first, so the explanation is linked on its own rather than
// sitting unseen inside {{îzah|…}}.
if (changes.gloss) {
$status.removeClass("dsb-status-ok dsb-status-error").text("Tê lînkkirin…");
var glossParts = util.splitTercimeIzah(changes.gloss);
linkifyGlossParts(
glossParts ? $.trim(glossParts.tercime) : changes.gloss,
glossParts ? $.trim(glossParts.izah) : "",
function (linkedMane, linkedIzah, error) {
// The change is queued either way, so a gloss that came back empty is not written
// back -- that would save the sense with its wording gone.
var joined = joinGloss(linkedMane, linkedIzah);
if ($.trim(joined)) {
changes.gloss = joined;
}
if (error) {
$status.removeClass("dsb-status-ok").addClass("dsb-status-error").text(error);
}
afterLinkify();
},
true
);
} else {
afterLinkify();
}
});
// The links sit under the buttons, so each open box pushes them down rather than leaving them far above.
$quickRow.empty().append($openBoxes, $actions, $picker, $note);
}
// Attaches the pencil toggle to one leaf definition <li> -- clicking it reveals the "guhartina pêşketî"
// (advanced editing) panel built by buildAdvancedPanel() above.
// ---- per-section UI: "+ Beşek lê zêde bike" after a POS block's own content ----
// Reads the header text of "====" subsections already rendered under this POS heading, straight from the DOM.
// The subsections of a part of speech are the headings one deeper than it is.
function subsectionDepthFor($h3) {
return (sections.headingDepth($h3) || 3) + 1;
}
function isSubsectionHeading($el, depth) {
return sections.headingDepth($el) === depth;
}
function existingSubsectionHeadersInDom($h3) {
var depth = subsectionDepthFor($h3);
var headers = [];
for (var $cursor = headingOuter($h3).next(); $cursor.length; $cursor = $cursor.next()) {
var here = sections.headingDepth($cursor);
if (here && here < depth) {
break; // out of this part of speech
}
if (isSubsectionHeading($cursor, depth)) {
var $hh = $cursor.is("h2, h3, h4, h5, h6") ? $cursor : $cursor.find("h2, h3, h4, h5, h6").first();
headers.push(headingText($hh));
}
}
return headers;
}
// Finds where a new "====" subsection preview belongs in the live DOM, using the same SUBSECTION_ORDER rank
// insertPageSubsection() uses. Returns {insertBefore: $el} (a ranked subsection that outranks this one) or
// {insertAfter: $el} (goes at the end of the block).
function domSubsectionInsertAnchor($h3, header) {
var depth = subsectionDepthFor($h3);
var myRank = subsectionRankForHeader(header);
var $last = headingOuter($h3);
for (var $cursor = headingOuter($h3).next(); $cursor.length; $cursor = $cursor.next()) {
var here = sections.headingDepth($cursor);
if (here && here < depth) {
break; // out of this part of speech
}
if (isSubsectionHeading($cursor, depth)) {
var $hh = $cursor.is("h2, h3, h4, h5, h6") ? $cursor : $cursor.find("h2, h3, h4, h5, h6").first();
var otherRank = subsectionRankForHeader(headingText($hh));
if (otherRank !== -1 && otherRank > myRank) {
return { insertBefore: $cursor };
}
}
$last = $cursor;
}
return { insertAfter: $last };
}
// The wikitext one subsection's content becomes, exactly as insertPageSubsection() writes it -- so the
// preview parses the same string the save will produce.
function subsectionBodyWikitext(language, s, content) {
if (s.kind !== "wordlist") {
return content;
}
return content.filter(function (group) {
return group.words.length;
}).map(function (group) {
return buildWordListCall(WORDLIST_TEMPLATE, language,
withNamedParam(["cure=" + s.header], "sernav", group.sernav), group.words);
}).join("\n\n");
}
// The plain word list this preview falls back to when the parse fails: the values that were typed,
// without the template around them. Right, but not what the page will look like.
function subsectionPlainDom($wrap, s, content) {
if (s.kind !== "wordlist") {
$wrap.append($("<p class='dsb-preview-text'></p>").text(content));
return;
}
content.forEach(function (group) {
if (group.sernav) {
$wrap.append($("<div class='dsb-preview-subheading'></div>").text(group.sernav));
}
var $list = $("<ul class='dsb-preview-list'></ul>");
group.words.forEach(function (w) {
$list.append($("<li></li>").text(w.replace(/<[^<>]*>/g, "")));
});
$wrap.append($list);
});
}
// A "not saved yet" preview of a page-level "====" subsection, dropped into the live page at the
// position the save will put it, with its body rendered the way the page renders it -- real {{kol3}}
// columns and real links, not a list of the words that went into them. Returns the inserted element so
// the caller can remove it again.
function buildSubsectionPreviewDom($h3, s, content, language) {
var $wrap = $("<div class='dsb-preview-block'></div>");
// A styled <div>, not a real <h4>, so existingSubsectionHeadersInDom() never mistakes this live-only
// preview for an actual subsection already on the page.
$wrap.append($("<div class='dsb-preview-heading' role='heading' aria-level='4'></div>").text(s.header));
$wrap.append($("<span class='dsb-preview-badge'></span>").text("nû — hîn nehatiye qeydkirin"));
// Placed before it is filled: a cached render resolves almost at once, and filling a wrap that is
// still detached would announce content nobody can see.
var anchor = domSubsectionInsertAnchor($h3, s.header);
if (anchor.insertBefore) {
anchor.insertBefore.before($wrap);
} else {
anchor.insertAfter.after($wrap);
}
var wikitext = language ? subsectionBodyWikitext(language, s, content) : "";
if ($.trim(wikitext)) {
var $body = $("<div class='dsb-preview-rendered'></div>").appendTo($wrap);
fillWithRendered($body, wikitext, function () {
$body.remove();
subsectionPlainDom($wrap, s, content);
});
} else {
subsectionPlainDom($wrap, s, content);
}
return $wrap;
}
// A free section owns what lies between its own heading and the next heading of any depth, matching
// the range locateHeadingByText() edits. Returns a function that puts the page back, or null.
function replaceFreeSectionInPlace($heading, wikitext) {
var $start = headingOuter($heading);
var body = [];
for (var $cursor = $start.next(); $cursor.length; $cursor = $cursor.next()) {
if (sections.headingDepth($cursor)) {
break;
}
if (!$cursor.is(".dsb-subsection-panel, .dsb-convert-pos-panel, .dsb-preview-block, .dsb-sense-preview")) {
body.push($cursor.get(0));
}
}
if (!body.length) {
return null;
}
var $slot = $("<div class='dsb-sense-preview'></div>").insertAfter($start);
var $body = $(body).detach();
fillWithRendered($slot, wikitext, function () {
$slot.remove();
$slot = null;
$body.insertAfter($start);
});
return function () {
if ($slot) {
$body.insertAfter($start);
$slot.remove();
$slot = null;
}
};
}
/**
* Redraws a subsection that is already on the page with the content this edit would give it.
*
* The section's own nodes -- everything between its heading and the next heading of the same rank or
* higher -- are detached rather than dropped, so the returned function puts the page back exactly as it
* was when the change is cancelled or superseded.
*/
function replaceSubsectionInPlace($h3, header, wikitext) {
var depth = subsectionDepthFor($h3);
// Walked the same way existingSubsectionHeadersInDom() walks it, so "which nodes are this
// subsection" is decided once and the same way everywhere.
var $start = null;
var body = [];
for (var $cursor = headingOuter($h3).next(); $cursor.length; $cursor = $cursor.next()) {
var here = sections.headingDepth($cursor);
if (here && here < depth) {
break; // out of this part of speech
}
if (isSubsectionHeading($cursor, depth)) {
if ($start) {
break; // the next subsection starts here
}
var $hh = $cursor.is("h2, h3, h4, h5, h6") ? $cursor : $cursor.find("h2, h3, h4, h5, h6").first();
if (headingText($hh) === header) {
$start = $cursor;
}
continue;
}
// This gadget's own furniture stays where it is: the subsection's editing panel is the sibling
// right after its heading, so sweeping it into the body would detach the very panel the change
// was made in and leave the pencil toggling an element that is no longer on the page.
if ($start && !$cursor.is(".dsb-subsection-panel, .dsb-convert-pos-panel, .dsb-preview-block, .dsb-sense-preview")) {
body.push($cursor.get(0));
}
}
if (!$start || !body.length) {
return null;
}
var $slot = $("<div class='dsb-sense-preview'></div>").insertAfter($start);
var $body = $(body).detach();
fillWithRendered($slot, wikitext, function () {
$slot.remove();
$slot = null;
$body.insertAfter($start);
});
return function () {
if ($slot) {
$body.insertAfter($start);
$slot.remove();
$slot = null;
}
};
}
/**
* The live DOM one "place" section occupies: {nodes, insert}. nodes are what a render replaces --
* empty when the section is not on the page yet -- and insert() puts an element where the section
* belongs, so a new one appears exactly where the save will put it.
*/
// This gadget's own panels sit between a heading and the page's content, so every walk steps over
// them rather than counting one as content.
function isOwnFurniture($el) {
return $el.is(".dsb-toolbar, .dsb-quick-row, .dsb-subsection-panel, .dsb-convert-pos-panel," +
" .dsb-preview-block, .dsb-sense-preview");
}
function loosePlaceDom($h3, subsection) {
var place = subsection.place;
var nodes = [];
var $cursor;
if (place === "page-top") {
// Şablon:herwiha binêre renders as <div class="rellink binere"> at the head of the content.
var $out = $("#mw-content-text .mw-parser-output").first();
var $hat = $out.children(".binere, .rellink").first();
if ($hat.length) {
return { nodes: $hat.get(), insert: function ($el) { $hat.before($el); } };
}
return { nodes: [], insert: function ($el) { $out.prepend($el); } };
}
var $start = headingOuter($h3);
if (place === "pron") {
// A "===" heading above this POS block, under the language one.
for (var $back = $start.prev(); $back.length; $back = $back.prev()) {
if ($back.is(".mw-heading2, h2")) {
break;
}
if ($back.is(".mw-heading3, h3")) {
var $hh = $back.is("h3") ? $back : $back.find("h3").first();
if (headingText($hh) === subsection.header) {
for ($cursor = $back.next();
$cursor.length && !$cursor.is(".mw-heading2, .mw-heading3, h2, h3");
$cursor = $cursor.next()) {
if (!isOwnFurniture($cursor)) {
nodes.push($cursor.get(0));
}
}
return { nodes: nodes, insert: function ($el) { $back.after($el); } };
}
}
}
return { nodes: [], insert: function ($el) { $start.before($el); } };
}
if (place === "pos-top") {
// The thumbnails between the heading and the headword line.
for ($cursor = $start.next(); $cursor.length; $cursor = $cursor.next()) {
if (isOwnFurniture($cursor)) {
continue;
}
if (!$cursor.is(".thumb, figure, .floatright, .floatleft")) {
break;
}
nodes.push($cursor.get(0));
}
return { nodes: nodes, insert: function ($el) { $start.after($el); } };
}
return null;
}
// The wikitext a "place" section's pending content renders as. A word list is its one call; everything
// else is already the body that will be written.
function loosePlacePreviewWikitext(subsection, content, language) {
return subsection.kind === "wordlist"
? wt.buildHbnrCall(loosePlaceWords(content))
: loosePlaceBody(subsection, content, language);
}
// Draws a section's headword line from wikitext where the page's own line stands, or above the
// definitions when the section has none. The page's line is detached rather than dropped, so the
// returned function puts it back.
function replaceHeadwordInPlace($h3, wikitext) {
if (!$.trim(wikitext || "")) {
return null;
}
var $old = $();
var $at = $();
for (var $cursor = headingOuter($h3).next(); $cursor.length; $cursor = $cursor.next()) {
if ($cursor.is("p")) {
$old = $cursor;
break;
}
if (sections.headingDepth($cursor) || $cursor.is("ol")) {
$at = $cursor;
break;
}
}
var $slot = $("<div class='dsb-sense-preview'></div>");
if ($old.length) {
$old.before($slot);
} else if ($at.length) {
$at.before($slot);
} else {
return null;
}
var $body = $old.detach();
function restore() {
if (!$slot) {
return;
}
$body.insertBefore($slot);
$slot.remove();
$slot = null;
}
fillWithRendered($slot, wikitext, restore);
return restore;
}
// Redraws one of those places where it stands, the same way replaceSubsectionInPlace() redraws a
// "====" one. The page's own nodes are detached rather than dropped, so cancelling puts them back.
function replaceLoosePlaceInPlace($h3, subsection, wikitext) {
if (!$.trim(wikitext || "")) {
return null;
}
var at = loosePlaceDom($h3, subsection);
if (!at) {
return null;
}
var $slot = $("<div class='dsb-sense-preview'></div>");
at.insert($slot);
var $body = $(at.nodes).detach();
function restore() {
if (!$slot) {
return;
}
$body.insertBefore($slot);
$slot.remove();
$slot = null;
}
fillWithRendered($slot, wikitext, restore);
return restore;
}
// Builds the subsection picker + its word-list/text field. A subsection that isn't on the page yet is saved
// via insertPageSubsection(); one that already is gets prefilled and saved via updatePageSubsection().
// onlyHeader pins the panel to a single subsection (the per-heading pencil); omit it for the full picker.
// free pins the panel to a heading that is not a subsection of a part of speech, located by its own
// text: {header, ordinal}.
function buildSubsectionPanel($panel, $h3, language, sectionType, sectionOrdinal, onlyHeader, free) {
var existing = existingSubsectionHeadersInDom($h3);
// A heading the data does not name is still editable. It is declared a word list so the reader
// tries, in order, its {{kol3}} calls and then a plain bullet list; anything else falls through to
// raw wikitext, which is what prefill() does with a list it cannot read.
// Hidden counts as unknown here: the picker does not offer to build a Werger box, but one already
// on the page still opens, as the wikitext it is.
var unknown = onlyHeader && !PAGE_SUBSECTIONS.some(function (s) {
return s.header === onlyHeader && !s.hidden;
});
// A free section starts as wikitext: what it holds is only known once it has been read, and
// prefill() swaps in the {{kol3}} form there if it turns out to be a list.
var available = free ? [{
header: onlyHeader,
id: "free",
kind: "text",
free: free,
existing: true
}] : unknown ? [{
header: onlyHeader,
id: "unknown",
kind: "wordlist",
unknown: true,
existing: true
}] : PAGE_SUBSECTIONS.filter(function (s) {
// "hidden" takes a section out of the picker without removing it: Werger waits for the
// translation tools it belongs to, and offering a box those cannot yet fill misleads.
// Werger also carries lang: "ku" -- it is a Kurdish-entry section, so it is not offered
// elsewhere either.
return !s.hidden && (!s.lang || s.lang === language) &&
(!onlyHeader || s.header === onlyHeader);
}).map(function (s) {
return $.extend({}, s, { existing: existing.indexOf(s.header) !== -1 });
});
if (!available.length) {
$panel.text("Ji bo vê beşê guhartin tune.");
return;
}
var $select = $("<select class='dsb-subsection-select'></select>");
if (!onlyHeader) {
// Label-less on purpose: it holds the "nothing chosen yet" value without becoming a row in the
// list (makeSearchable() skips a blank option that has no label), so the prompt lives in the
// input's placeholder instead.
$select.append($("<option value=''></option>"));
}
available.forEach(function (s) {
// A "place" entry has no "====" heading of its own, so it is marked rather than left looking
// like the subsections around it.
var label = s.place ? s.header + " (ne binebeş e)"
: (s.existing ? s.header + " (biguhêre)" : s.header);
$select.append($("<option></option>").val(s.id).text(label));
});
var $selectWrap = makeSearchable($select, "Binebeşek hilbijêre");
if (onlyHeader) {
$selectWrap.hide(); // pinned to one subsection: nothing to choose
}
var $fieldWrap = $("<div class='dsb-subsection-field'></div>");
var $save = $("<button type='button' class='dsb-save'></button>").text(util.msg.add);
var $cancel = $("<button type='button' class='dsb-cancel'></button>").text(util.msg.cancel);
var $status = $("<span class='dsb-status'></span>");
var $actions = $("<div class='dsb-form-actions'></div>").append($save, $cancel, $status);
$cancel.on("click", function () {
resetForm();
$panel.hide();
hideQueueIfIdle();
});
// Undefined while the placeholder option is selected.
function currentSubsection() {
var id = $select.val();
return id ? available.filter(function (s) {
return s.id === id;
})[0] : undefined;
}
// One entry per "{{kol3}}" group in the current wordlist section; empty for any other kind.
var groupFields = [];
var $groups = null;
var editAsText = false; // set when an existing wordlist section turns out to be unparseable
// Whatever has been typed into each subsection's box, so switching the picker away and back restores it
// instead of rebuilding an empty field. Keyed by subsection id; lastId is whichever box is on screen.
var drafts = {};
var lastId = null;
// Called before renderField() rebuilds, while the group fields still hold the outgoing box.
function captureDraft() {
if (!lastId) {
return;
}
drafts[lastId] = imagesField
? { asText: false, images: imagesField.values() }
: (groupFields.length
? { asText: false, groups: readGroups() }
: { asText: editAsText, text: fieldText() });
}
function restoreDraft(s, draft) {
// An existing wordlist we previously fell back to raw wikitext for stays in that mode.
if (draft.asText && s.kind === "wordlist") {
editAsText = true;
groupFields = [];
renderTextArea(s, 6);
}
if (imagesField) {
imagesField.setValues(draft.images);
} else if (groupFields.length) {
setGroups(s, draft.groups);
} else {
setFieldText(draft.text);
}
}
// Etîmolojî is edited on a live surface (see renderChipSurface) rather than in a textarea, so every
// read and write of the field goes through these two instead of touching .dsb-subsection-text.
var chipSurface = null;
function fieldText() {
if (wergerForm) {
return buildWergerBlock(wergerForm.read());
}
return chipSurface
? $.trim(readChipSurface(chipSurface.get(0)))
: $.trim($fieldWrap.find(".dsb-subsection-text").val() || "");
}
function setFieldText(value) {
if (wergerForm) {
wergerForm.set(parseWergerBlock(value));
return;
}
if (chipSurface) {
renderChipSurface(chipSurface, value || "");
var schedule = chipSurface.data("dsbSchedulePreview");
if (schedule) {
schedule(); // show what the loaded etymology renders as
}
} else {
$fieldWrap.find(".dsb-subsection-text").val(value || "");
}
}
// The contenteditable surface: text typed directly, template calls as chips that open the dialog,
// plus start.js's etymology templates in a searchable insert menu.
// A live surface where a template shows as a chip, with a menu that inserts one. Etîmolojî and
// Bilêvkirin are both written this way; they differ only in which templates the menu offers.
function renderTemplateEditor(s, templates) {
$fieldWrap.empty();
var $select = $("<select class='dsb-subsection-select'></select>");
$select.append($("<option value=''></option>"));
templates.forEach(function (t) {
$select.append($("<option></option>").val(t.snippet).text(t.name + " — " + t.desc));
});
var $surface = $("<div class='dsb-tpl-surface' contenteditable='true'></div>")
.attr("role", "textbox")
.attr("aria-multiline", "true");
chipSurface = $surface;
// Clicking the insert menu moves focus out of the surface and the browser forgets the caret, so
// the last caret position inside the surface is kept and restored before inserting -- otherwise
// focus() drops it back to the very start and every template lands at the beginning of the line.
var savedRange = null;
function rememberCaret() {
var sel = window.getSelection();
if (!sel || !sel.rangeCount) {
return;
}
var range = sel.getRangeAt(0);
var root = $surface.get(0);
if (range.commonAncestorContainer === root || $.contains(root, range.commonAncestorContainer)) {
savedRange = range.cloneRange();
}
}
$surface.on("keyup mouseup focusout", rememberCaret);
function restoreCaret() {
if (!savedRange) {
return;
}
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(savedRange);
}
$select.on("change", function () {
var snippet = $select.val();
if (!snippet) {
return;
}
// Same substitution start.js makes: CODE is this section's language, SOURCELANG is left for
// the user to fill in.
snippet = snippet.replace(/CODE/g, language).replace(/SOURCELANG/g, "?");
// A pronunciation template is a bullet of its own ("* {{ku-IPA}}"), so it starts a line
// rather than landing in the middle of one.
var ownLine = /^\*\s/.test(snippet);
if (ownLine) {
snippet = $.trim(snippet.replace(/^\*\s*/, ""));
}
$surface.focus();
restoreCaret();
var $chip = makeTemplateChip(snippet);
var piece = document.createDocumentFragment();
if (ownLine) {
// The bullet, and a line of its own unless the caret already starts one.
var at = caretOffset();
var before = at >= 0 ? readChipSurface($surface.get(0)).slice(0, at) : "";
piece.appendChild(document.createTextNode(!before || /\n$/.test(before) ? "* " : "\n* "));
}
piece.appendChild($chip.get(0));
insertAtCaret($surface, piece);
rememberCaret(); // so a second insert lands after the first, not back at the old spot
$select.val("");
// Straight into the dialog: the snippet is placeholders ("?", "…") until someone fills it in.
cite.openTemplateDialog(snippet).then(function (edited) {
applyChipEdit($chip, edited);
syncPanelWidth();
});
});
// Paste as plain text: pasted markup would otherwise become real nodes in the surface.
$surface.on("paste", function (e) {
var clip = e.originalEvent && e.originalEvent.clipboardData;
if (!clip) {
return;
}
e.preventDefault();
document.execCommand("insertText", false, clip.getData("text/plain"));
});
// The chip the dialog was opened for, or the one that has taken its place. renderChipSurface()
// rebuilds the surface from its wikitext whenever a call is typed in as text, which throws away
// the element this closure is holding; without this, "Jê bibe" would remove a node that is no
// longer on screen and appear to do nothing.
function liveChip($chip, wikitext) {
if ($.contains($surface.get(0), $chip.get(0))) {
return $chip;
}
return $surface.find(".dsb-tpl-chip").filter(function () {
return $(this).attr("data-wikitext") === wikitext;
}).first();
}
function applyChipEdit($chip, edited) {
if (edited === null || !$chip.length) {
return;
}
if (!$.trim(edited)) {
$chip.remove();
return;
}
// Edited into something that is no longer one template call: let it fall back to plain text
// rather than keeping a chip that lies about its contents.
var call = parseTemplateCalls(edited);
if (call.otherContent || call.calls.length !== 1) {
$chip.replaceWith(document.createTextNode(edited));
return;
}
$chip.replaceWith(makeTemplateChip(edited));
}
// Double-click edits the call's wikitext in place; single click opens the parameter dialog. The
// click is held back briefly so the first of a double-click does not open the dialog underneath.
var chipClickTimer = null;
var lastChipTap = 0;
var lastChipNode = null;
var DOUBLE_TAP_MS = 300;
// Double-tap drops the chip back to plain wikitext in the surface, rather than making the chip
// itself editable: a contenteditable inside a contenteditable gives Android a selection handle and
// no usable caret. As plain text the browser's own editing works, and chipifyTyped() puts the chip
// back once the caret leaves the call.
// A collapsed caret inside the text just inserted: at the character tapped where the browser can
// say which that is, otherwise just inside the closing braces so the caret stays within the call
// (which is what stops chipifyTyped putting the chip straight back).
function caretRangeIn(node, event) {
var range;
var x = event && event.clientX;
var y = event && event.clientY;
if (x !== undefined && document.caretRangeFromPoint) {
range = document.caretRangeFromPoint(x, y);
} else if (x !== undefined && document.caretPositionFromPoint) {
var pos = document.caretPositionFromPoint(x, y);
if (pos) {
range = document.createRange();
range.setStart(pos.offsetNode, pos.offset);
}
}
if (range && range.startContainer === node) {
range.collapse(true);
return range;
}
var text = node.nodeValue;
range = document.createRange();
range.setStart(node, /\}\}$/.test(text) ? text.length - 2 : text.length);
range.collapse(true);
return range;
}
function startInlineEdit($chip, event) {
if (!$.contains($surface.get(0), $chip.get(0))) {
return; // the tap timer and dblclick can both fire; only the first one has a chip left
}
var node = document.createTextNode($chip.attr("data-wikitext"));
$chip.replaceWith(node);
$surface.get(0).focus();
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(caretRangeIn(node, event));
}
// Bound before the chip's own handler and stopping there, so removing a call never opens the
// dialog for it on the way out.
$surface.on("click", ".dsb-tpl-chip-remove", function (e) {
e.preventDefault();
e.stopImmediatePropagation();
clearTimeout(chipClickTimer);
lastChipNode = null;
$(this).closest(".dsb-tpl-chip").remove();
syncPanelWidth();
});
$surface.on("click keydown", ".dsb-tpl-chip", function (e) {
if (e.type === "keydown" && e.which !== 13 && e.which !== 32) {
return;
}
e.preventDefault();
var $chip = $(this);
clearTimeout(chipClickTimer);
// A second tap on the same chip edits it inline. Timed here rather than left to "dblclick",
// which mobile browsers fire unreliably or not at all for a double-tap.
var now = new Date().getTime();
if (lastChipNode === this && now - lastChipTap < DOUBLE_TAP_MS) {
lastChipNode = null;
startInlineEdit($chip, e);
return;
}
lastChipTap = now;
lastChipNode = this;
var wikitext = $chip.attr("data-wikitext");
chipClickTimer = setTimeout(function () {
cite.openTemplateDialog(wikitext).then(function (edited) {
applyChipEdit(liveChip($chip, wikitext), edited);
syncPanelWidth();
});
}, DOUBLE_TAP_MS);
});
$surface.on("dblclick", ".dsb-tpl-chip", function (e) {
e.preventDefault();
clearTimeout(chipClickTimer);
startInlineEdit($(this), e);
});
// A long etymology cannot be read in the panel's usual 520px column, so the panel drops its
// width cap once there is enough text to need it. What it will look like is shown by the
// in-place render on the page itself, not by a second copy inside the form.
var WIDE_AT = 240; // characters
function syncPanelWidth() {
var wikitext = readChipSurface($surface.get(0));
if (wikitext.length > WIDE_AT || wikitext.indexOf("\n") !== -1) {
$surface.closest(".dsb-subsection-panel").addClass("dsb-panel-wide");
}
}
// A template typed by hand becomes a chip once typing settles, so it behaves like an inserted one:
// click for the parameter dialog, double-click for its raw wikitext.
var composing = false;
// The caret as an offset into the surface's wikitext, which is what survives rebuilding the DOM.
function caretOffset() {
var sel = window.getSelection();
var root = $surface.get(0);
if (!sel || !sel.rangeCount) {
return -1;
}
var range = sel.getRangeAt(0);
if (range.startContainer !== root && !$.contains(root, range.startContainer)) {
return -1; // caret is somewhere else on the page
}
var pre = document.createRange();
pre.selectNodeContents(root);
pre.setEnd(range.startContainer, range.startOffset);
var holder = document.createElement("div");
holder.appendChild(pre.cloneContents());
return readChipSurface(holder).length;
}
// Puts the caret back at that wikitext offset. Only runs straight after a rebuild, so the surface
// holds nothing but text nodes and chips.
function placeCaret(offset) {
var root = $surface.get(0);
var range = document.createRange();
var nodes = root.childNodes;
root.focus();
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i];
var len = node.nodeType === 3
? node.nodeValue.length
: ($(node).attr("data-wikitext") || $(node).text()).length;
if (offset <= len) {
if (node.nodeType === 3) {
range.setStart(node, offset);
} else {
range.setStartAfter(node); // a chip is atomic; the caret goes after it
}
break;
}
offset -= len;
}
if (!range.startContainer || range.startContainer === document) {
range.selectNodeContents(root);
}
range.collapse(true);
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
// True when the selection overlaps the inside of a template call. A caret resting exactly at the
// closing "}}" does not count -- that is someone who has just finished typing one.
function selectionTouchesCall(wikitext) {
var sel = window.getSelection();
if (!sel || !sel.rangeCount) {
return false;
}
var range = sel.getRangeAt(0);
var root = $surface.get(0);
if (range.startContainer !== root && !$.contains(root, range.startContainer)) {
return false;
}
var from = caretOffset();
var to = from;
if (!range.collapsed) {
var pre = document.createRange();
pre.selectNodeContents(root);
pre.setEnd(range.endContainer, range.endOffset);
var holder = document.createElement("div");
holder.appendChild(pre.cloneContents());
to = readChipSurface(holder).length;
}
return parseTemplateCalls(wikitext).calls.some(function (call) {
return from < call.end && to > call.start;
});
}
function chipifyTyped() {
if (composing) {
return; // never rebuild under an IME
}
var wikitext = readChipSurface($surface.get(0));
// Chips already hold one call each, so a higher call count means one was typed in as text.
if (parseTemplateCalls(wikitext).calls.length <= $surface.find(".dsb-tpl-chip").length) {
return;
}
var offset = caretOffset();
if (selectionTouchesCall(wikitext)) {
return; // still inside a call: leave it as text until the caret moves out
}
renderChipSurface($surface, wikitext);
savedRange = null; // the old range points at nodes that no longer exist
if (offset >= 0) {
placeCaret(offset);
rememberCaret();
}
}
$surface.on("compositionstart", function () {
composing = true;
});
$surface.on("compositionend", function () {
composing = false;
schedulePreview();
});
var previewTimer = null;
function schedulePreview() {
clearTimeout(previewTimer);
previewTimer = setTimeout(function () {
chipifyTyped();
syncPanelWidth();
}, 700);
}
$surface.on("input", schedulePreview);
$surface.data("dsbSchedulePreview", schedulePreview);
$fieldWrap.append(makeSearchable($select, util.msg.addTemplate), $surface);
// Bilêvkirin is written in IPA, so it gets the same character palette the wizard shows. Its
// characters go in as text and its two template buttons as chips, which is what the rest of
// this surface is made of.
if (s.place === "pron") {
$fieldWrap.append(util.buildIpaPalette({
insertText: function (text) {
$surface.focus();
restoreCaret();
insertAtCaret($surface, document.createTextNode(text));
rememberCaret();
schedulePreview();
},
insertTemplate: function (snippet) {
$surface.focus();
restoreCaret();
var $chip = makeTemplateChip(snippet.replace(/\|\s*\}\}$/, "}}"));
insertAtCaret($surface, $chip.get(0));
rememberCaret();
cite.openTemplateDialog($chip.attr("data-wikitext")).then(function (edited) {
applyChipEdit($chip, edited);
syncPanelWidth();
});
}
}));
}
}
// The three "place" sections are wikitext with a shape worth spelling out, since nothing on screen
// says what belongs in the box.
var PLACE_PLACEHOLDERS = {
"page-top": "{{h-bnr|Kelîme|Kelîmeyeke din}} an jî {{herwiha binêre/ane}}",
"pos-top": "[[Wêne:Navê pelî.jpg|thumb|Binnivîs]]",
"pron": "* {{ku-IPA}}"
};
function renderTextArea(s, rows) {
$fieldWrap.empty().append(
$("<textarea class='dsb-input dsb-subsection-text'></textarea>")
.attr("rows", rows)
.attr("placeholder", PLACE_PLACEHOLDERS[s.place] || (s.header + "…"))
);
util.initManualResize($fieldWrap);
}
// A translation box: the {{werger-ser}}/{{werger-bin}} pair, with the sense it covers and the
// layout parameters that template accepts. The rows in between are left to the translation editor
// gadget, which is what edits them on the rendered page.
// Reads and writes through fieldText()/setFieldText() as wikitext, so drafts, save and the queue
// need no special case beyond the append below.
var wergerForm = null;
var imagesField = null;
function renderWergerField(s) {
var inputs = {};
// Its own row class rather than .dsb-tpl-param-row: that one is styled in cite.css, which is
// only on the page when wikt.cite happens to be loaded, and without it the label falls back to
// sitting inline and a short one ("id") is overlapped by its own box.
function row(key, label, placeholder) {
inputs[key] = $("<input type='text' class='dsb-input'>").attr("placeholder", placeholder || "");
return $("<div class='dsb-werger-row'></div>").append(
$("<label class='dsb-adv-label'></label>").text(label).append(inputs[key])
);
}
// {{werger-ser}} also takes stûn, firehî, bgcolor, valign and align, but they only restyle the
// box and its defaults are what these pages use, so the form does not offer them. They are
// still in WERGER_NAMED, so a block that already sets one is read back correctly.
var $main = $("<div class='dsb-werger-main'></div>").append(
row("1", "Mane", "ava vexwarinê"),
row("2", "Mane no", "ne mecbûrî — di parantezê de xuya dike"),
row("id", "id", "ne mecbûrî — IDya tercimeyê")
);
var $note = $("<p class='dsb-werger-note'></p>").text(
"Qutiyeke Wergerê ya vala lê zêde dikî. Tercimeyan bi amûra wergerê dûre binivîse."
);
wergerForm = {
read: function () {
var out = {};
$.each(inputs, function (key, $input) {
var value = $.trim($input.val() || "");
if (value) {
out[key] = value;
}
});
return out;
},
set: function (params) {
$.each(inputs, function (key, $input) {
$input.val((params && params[key]) || "");
});
}
};
$fieldWrap.empty().append($note, $main);
}
// Same [[Kategorî:Çavkanî]] search widget start.js's own picker uses: choosing an entry inserts
// "* {{Target}}" at the cursor. The field itself stays the plain ".dsb-subsection-text" textarea
// every other "kind: text" subsection already uses, so save/prefill/drafts need no changes.
function renderCavkaniField(s) {
var $ta = $("<textarea class='dsb-input dsb-subsection-text'></textarea>")
.attr("rows", 4)
.attr("placeholder", util.msg.pickOrTypeTemplate);
var picker = cite.buildCategoryTemplateSearch({
placeholder: util.msg.addTemplate,
describe: true,
// Same param dialog Etîmolojî opens on a fresh insert: filled from the picked template's own
// TemplateData, so the user adds "rûpel=", "sernav=", ... before anything lands in the field
// rather than typing a bare {{Target}} and going back to fill params in by hand.
onPick: function (entry) {
cite.openTemplateDialog("{{" + entry.target + "}}").then(function (edited) {
if (!edited) {
return; // cancelled, or "Jê bibe" on a call that was never inserted
}
util.insertAtCursor($ta.get(0), "* " + edited + "\n");
$ta.trigger("focus");
});
}
});
var $picker = $("<div class='dsb-cavkani-picker'></div>").append(picker.$element);
$fieldWrap.empty().append($picker, $ta);
}
// One "{{kol3}}" group: its "sernav=" heading and its words. Several of these make up one section --
// "Herwiha binêre" typically carries an unheaded list plus a few headed ones.
function addGroup(s, group) {
var $sernav = $("<input type='text' class='dsb-input'>").attr("placeholder", "ne mecbûrî…");
// {{h-bnr}} takes bare page names, so its rows carry no ⊕: there is nowhere in the call for a
// modifier to go.
var words = buildWordListField(s.header + "…", { modifiers: !s.single });
var $remove = $("<a href='#' class='dsb-quick-link dsb-group-remove'>Komê rake</a>");
var $element = $("<div class='dsb-wordgroup'></div>").append(
// Label beside the box, not above it: stacked, "Sernav" read as a heading for the whole
// group rather than as the name of the field under it. The input is nested in the label so
// clicking the word focuses the box, without needing an id per group.
// A single-call list has no "sernav=" to carry, so the row is built (readGroups still reads
// it) but never shown. Hidden here rather than in renderField: setGroups() rebuilds every
// group on prefill, which would put a visible one back.
$("<div class='dsb-tpl-param-row dsb-param-row-inline'></div>").append(
$("<label class='dsb-adv-label'></label>").text("Sernav").append($sernav)
).toggle(!s.single),
words.$element,
$remove
);
var field = {
$element: $element,
read: function () {
return { sernav: $.trim($sernav.val()), words: words.values() };
},
set: function (g) {
$sernav.val((g && g.sernav) || "");
words.setValues((g && g.words) || []);
}
};
$remove.on("click", function (e) {
e.preventDefault();
var at = $.inArray(field, groupFields);
if (at !== -1) {
groupFields.splice(at, 1);
}
$element.remove();
updateGroupChrome();
});
field.set(group);
groupFields.push(field);
$groups.append($element);
updateGroupChrome();
return field;
}
// With a single group there is nothing to tell apart, so it needs neither a remove link nor a label.
function updateGroupChrome() {
var many = groupFields.length > 1;
$groups.toggleClass("dsb-wordgroups-many", many);
$groups.find(".dsb-group-remove").toggle(many);
}
function readGroups() {
return groupFields.map(function (field) {
return field.read();
});
}
// Rebuilds the blocks to match what the page (or the draft) holds, always leaving at least one.
function setGroups(s, groups) {
groupFields = [];
$groups.empty();
(groups && groups.length ? groups : [null]).forEach(function (group) {
addGroup(s, group);
});
}
// Loads what the page already has into the form, so an edit starts from the current list rather than
// from an empty one. A wordlist section we can't parse degrades to raw wikitext editing.
function prefill(s) {
$status.removeClass("dsb-status-error dsb-status-ok").text("Tê barkirin…");
$save.prop("disabled", true);
fetchPageSubsection(language, sectionType, sectionOrdinal, s).then(function (current) {
$save.prop("disabled", false);
$status.text("");
if (!current) {
return;
}
// A "place" section has no heading of its own, so whether the page already has one cannot
// be seen in the DOM the way a "====" subsection can -- it is known only once its content
// has been read. That is what decides "nûkirin" or "zêdekirin" in the edit summary.
if (s.place) {
s.existing = !!(current.images ? current.images.length : $.trim(current.text || ""));
// The label was set before this content arrived, when nothing yet said one was there.
$save.text(s.existing && s.kind !== "werger" ? util.msg.edit : util.msg.add);
}
if (current.kind === "images" && imagesField) {
imagesField.setValues(current.images);
return;
}
// A free section's field was built before its content was known: a list turns it into the
// {{kol3}} form, anything else leaves it the wikitext box it already is.
if (s.free && current.kind === "wordlist" && !groupFields.length) {
s.kind = "wordlist";
renderWordListField(s);
setGroups(s, current.groups);
if (current.fromBullets) {
$status.text("Ev lîste made bi made hatiye nivîsîn — bi qeydkirinê dibe {{kol3}}.");
}
return;
}
if (current.kind === "wordlist" && groupFields.length) {
setGroups(s, current.groups);
if (current.fromBullets) {
$status.text("Ev lîste made bi made hatiye nivîsîn — bi qeydkirinê dibe {{kol3}}.");
}
return;
}
if (s.kind === "wordlist") {
editAsText = true;
groupFields = [];
renderTextArea(s, 6);
$status.text(s.unknown
? "Ev beş nayê naskirin — wekî wîkîtekst tê guhartin."
: "Şablona vê lîsteyê nehate naskirin — wekî wîkîtekst biguhêre.");
}
setFieldText(current.text);
}, function () {
$save.prop("disabled", false);
$status.addClass("dsb-status-error").text("Nikare muhtewaya heyî bixwîne.");
});
}
// One "{{kol3}}" group per block, plus a way out to raw wikitext for a list no row can express --
// a shared subpage list like {{herwiha binêre/ane}}, say.
function renderWordListField(s) {
$fieldWrap.empty();
chipSurface = null;
editAsText = false;
var $addGroup = $("<a href='#' class='dsb-quick-link'>+ Komek lê zêde bike</a>");
$addGroup.on("click", function (e) {
e.preventDefault();
addGroup(s, null);
});
$groups = $("<div class='dsb-wordgroups'></div>");
$fieldWrap.append($groups, $addGroup);
addGroup(s, null);
// One call takes one flat list of names, so there is no second group to add.
if (s.single) {
$addGroup.hide();
}
var $toRaw = $("<a href='#' class='dsb-quick-link'>Wekî wîkîtext biguhêre</a>");
$toRaw.on("click", function (e) {
e.preventDefault();
var current = s.place
? wt.buildHbnrCall(loosePlaceWords(readGroups()))
: subsectionBodyWikitext(language, s, readGroups());
editAsText = true;
groupFields = [];
$groups = null;
renderTextArea(s, 3);
setFieldText(current);
util.initManualResize($fieldWrap);
});
$fieldWrap.append($toRaw);
util.initManualResize($fieldWrap);
}
function renderField() {
var s = currentSubsection();
$fieldWrap.empty();
groupFields = [];
$groups = null;
editAsText = false;
chipSurface = null;
wergerForm = null;
imagesField = null;
$status.removeClass("dsb-status-error dsb-status-ok").text("");
// Save stays disabled (but "Betal bike" usable) until something is actually chosen.
$save.prop("disabled", !s);
if (!s) {
$save.text(util.msg.add);
lastId = null;
return; // still on "Binebeşekê hilbijêre"
}
if (s.kind === "wordlist") {
renderWordListField(s);
} else if (s.kind === "images") {
imagesField = util.buildImageListField({
prefix: "dsb-",
rowClass: "dsb-tpl-param-row dsb-param-row-inline",
inputClass: "dsb-input",
addClass: "dsb-quick-link",
pageName: mw.config.get("wgTitle")
});
$fieldWrap.append(imagesField.$element);
} else if (s.header === "Etîmolojî") {
renderTemplateEditor(s, templateChoices("etymTpls", language));
} else if (s.place === "pron") {
renderTemplateEditor(s, templateChoices("pronTpls", language));
} else if (s.kind === "werger") {
renderWergerField(s);
} else if (s.header === "Çavkanî") {
renderCavkaniField(s);
} else {
renderTextArea(s, 3);
}
// Werger always adds a box, even to a section that is already there, so its button never says
// "Biguhêre".
$save.text(s.existing && s.kind !== "werger" ? util.msg.edit : util.msg.add);
// Covers whichever branch above built the field. Idempotent, so a textarea already carrying a
// grip is skipped.
util.initManualResize($fieldWrap);
lastId = s.id;
// A draft always wins over re-reading the page: it is the newer, unsaved state.
if (drafts[s.id]) {
restoreDraft(s, drafts[s.id]);
} else if ((s.existing || s.place) && s.kind !== "werger") {
prefill(s);
}
}
$select.on("change", function () {
captureDraft();
renderField();
});
renderField();
// Back to the state a fresh open would show: no draft, no picked subsection. Called directly
// rather than through the "change" handler above, so the discarded draft is never captured first.
function resetForm() {
drafts = {};
if (!onlyHeader) {
$select.val("").trigger("wiktku:refresh");
}
renderField();
}
$save.on("click", function () {
var s = currentSubsection();
if (!s) {
return;
}
var asText = editAsText || (s.kind !== "wordlist" && s.kind !== "images");
var effective = asText ? $.extend({}, s, { kind: "text" }) : s;
var content;
if (asText) {
content = fieldText();
if (!content) {
return;
}
} else if (s.kind === "images") {
content = imagesField ? imagesField.values() : [];
if (!content.length) {
return;
}
} else {
content = readGroups();
if (!content.some(function (group) { return group.words.length; })) {
return;
}
}
$save.prop("disabled", true);
// A Werger section holds any number of boxes, so an existing one is appended to rather than
// replaced; every other kind edits the single body it owns.
var appending = s.kind === "werger" && s.existing;
queueChange(
subsectionPlaceLabel(s, language, sectionType, $h3),
s.kind === "werger"
? "qutiyeke wergerê hate zêdekirin"
: "beşa «" + s.header + "» hate " + (s.existing ? "nûkirin" : "zêdekirin"),
function (pageContent) {
var body = content;
if (appending) {
var current = readPageSubsection(pageContent, language, sectionType, sectionOrdinal, effective);
var existingText = current ? $.trim(current.text || "") : "";
body = existingText ? existingText + "\n\n" + content : content;
}
return {
content: s.free
? updateFreeSection(pageContent, language, effective, body)
: (s.place
? writeLoosePlace(pageContent, language, sectionType, sectionOrdinal, effective,
loosePlaceBody(effective, body, language))
: (s.existing
? updatePageSubsection(pageContent, language, sectionType, sectionOrdinal, effective, body)
: insertPageSubsection(pageContent, language, sectionType, sectionOrdinal, effective, body))),
warnings: []
};
},
$save,
$status,
// Werger is a box the translation tools fill in afterwards, so there is nothing to draw for
// it. A new subsection gets a block of its own at the position it will land in; one already
// on the page is redrawn where it stands.
s.kind === "werger" ? null : function () {
// A free section is redrawn under its own heading, which is the element the panel hangs
// on.
if (s.free) {
var restoreFree = replaceFreeSectionInPlace($h3,
subsectionBodyWikitext(language, effective, content));
return function () {
if (restoreFree) {
restoreFree();
restoreFree = null;
}
};
}
// A "place" section has no heading to redraw between, so it is found in the DOM by what
// it renders as and replaced there.
if (s.place) {
var restoreLoose = replaceLoosePlaceInPlace($h3, effective,
loosePlacePreviewWikitext(effective, content, language));
return function () {
if (restoreLoose) {
restoreLoose();
restoreLoose = null;
}
};
}
if (!s.existing) {
var $preview = buildSubsectionPreviewDom($h3, s, content, language);
return function () {
$preview.remove();
};
}
var restore = replaceSubsectionInPlace($h3, effective.header,
subsectionBodyWikitext(language, effective, content));
return function () {
if (restore) {
restore();
restore = null;
}
};
},
{ key: s.free
? "section:" + s.header + "|" + s.free.ordinal
: "subsection:" + sectionType + "|" + sectionOrdinal + "|" + s.header }
);
});
$panel.empty().append($selectWrap, $fieldWrap, $actions);
}
// Builds the "choose a part of speech" picker that reclassifies a section -- an unclassified
// "=== Mane ===" placeholder, or a "=== Biwêj ===" that turns out to be a real part of speech. Saved via
// convertSectionToPos(); lists every name from sectionNames except the one the section already is.
function buildConvertPosPanel($panel, $h3, language, sectionType, sectionOrdinal) {
var $select = $("<select class='dsb-convert-pos-select'></select>");
// Label-less, like the subsection picker's: it holds the "nothing chosen yet" value without showing
// up as a row, so the prompt lives in the input's placeholder instead.
$select.append($("<option value=''></option>"));
Object.keys(sectionNames).forEach(function (name) {
// The section's own part of speech is listed too, marked: picking it changes nothing about the
// heading and leaves the parameters below to be edited on their own.
$select.append($("<option></option>").val(name)
.text(name === sectionType ? name + " (ya niha)" : name));
});
var $selectWrap = makeSearchable($select, "Li cureyekî rêzimanî bigere");
// Opens on the section's own part of speech, so editing only its parameters needs no pick.
if (sectionNames[sectionType]) {
$select.val(sectionType).trigger("wiktku:refresh");
}
// The same card the entry wizard shows under its part-of-speech question -- gender, transitivity,
// the transliteration boxes and everything behind "Tercîhên pêşketî". Shared from
// wikt.edit/posparams.js, so reclassifying a section here offers exactly what creating one in the
// wizard does.
var card = posparams.build({ lang: language, pos: "" });
// Opened on the call the section already has, so this edits the current template rather than
// starting from a blank line. setContext() rebuilds the line whenever the part of speech moves,
// so the prefill is only the starting point.
var spec0 = posTemplateFor(sectionType);
card.setContext(language, spec0 ? spec0.name : "", /^Formeke /.test(sectionType));
fetchPageContent().then(function (pageContent) {
try {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var found = findPosTemplateLine(loc.lines, loc.posLineIndex, sectionType);
if (found.index !== -1 && ($select.val() || sectionType) === sectionType) {
card.setWikitext(found.text);
}
} catch (e) { /* an unreadable section just leaves the card as it was */ }
});
$select.on("change", function () {
var spec = posTemplateFor($select.val());
card.setContext(language, spec ? spec.name : "", /^Formeke /.test($select.val() || ""));
});
// "gh" on its own, or "<form>-<base>" once a structural form is picked -- start.js's own composition.
// convertSectionToPos() writes the template itself, so only what follows "{{pos|lang" is wanted
// from the card's line.
function posParams() {
var m = /^\{\{[^|}]*\|[^|}]*\|?([\s\S]*)\}\}$/.exec(card.wikitext());
return m ? m[1] : "";
}
var $save = $("<button type='button' class='dsb-save'></button>").text(util.msg.add);
var $cancel = $("<button type='button' class='dsb-cancel'></button>").text(util.msg.cancel);
var $status = $("<span class='dsb-status'></span>");
var $actions = $("<div class='dsb-form-actions'></div>").append($save, $cancel, $status);
// dsb.js owns the pencil and the panel's built state, so it does the closing.
$cancel.on("click", function () {
$panel.trigger("dsbCancel");
hideQueueIfIdle();
});
$save.on("click", function () {
var chosen = $select.val();
if (!chosen) {
$status.removeClass("dsb-status-ok").addClass("dsb-status-error").text("Pêşî cureyekî rêzimanî hilbijêre.");
return;
}
var params = posParams();
$save.prop("disabled", true);
$status.removeClass("dsb-status-error").text("");
queueChange(
langLabel(language),
"beşa «" + sectionLabel(sectionType, $h3) + "» bû «" + chosen + "»",
function (pageContent) {
return { content: convertSectionToPos(pageContent, language, sectionType, sectionOrdinal, chosen, params), warnings: [] };
},
$save,
$status,
function () {
// The heading keeps its text, which every other queued change finds the section by; a
// note under it names the new one. The headword line is drawn as the save will write it.
var $note = $("<div class='dsb-preview-inline dsb-preview-heading-note'></div>")
.text("→ " + chosen + (params ? " (" + params + ")" : "") + " — nû, hîn nehatiye qeydkirin");
headingOuter($h3).after($note);
var dropped = false;
var restoreHeadword = null;
fetchPageContent().then(function (pageContent) {
if (dropped) {
return;
}
var call;
try {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
call = posCallLine(findPosTemplateLine(loc.lines, loc.posLineIndex, sectionType),
language, chosen, params);
} catch (e) {
return;
}
restoreHeadword = replaceHeadwordInPlace($h3, call);
});
return function () {
dropped = true;
$note.remove();
if (restoreHeadword) {
restoreHeadword();
restoreHeadword = null;
}
};
},
// Runs after every other queued change: it renames the "Mane" heading those changes locate
// themselves by, so anything edited on this section has to be applied first.
{ runLast: true, key: "convert:" + sectionType + "|" + sectionOrdinal }
);
});
$panel.empty().append($selectWrap, card.$element, $actions);
}
// Attaches the "+ Beşek lê zêde bike" trigger right after a POS block's own last element.
// Puts the same pencil the definitions use on every existing "====" subsection heading the gadget knows how
// to edit, opening buildSubsectionPanel() pinned to that one subsection.
module.exports = {
buildAdvancedPanel: buildAdvancedPanel,
buildSubsectionPanel: buildSubsectionPanel,
buildConvertPosPanel: buildConvertPosPanel,
editExample: editExample,
clearQueue: clearQueue,
closeAllPanels: closeAllPanels,
openQueueBox: openQueueBox
};
}());
// </nowiki>
mg8zg7j4koggadsd9mfpa6nsc8cb8vo
6531954
6531945
2026-09-17T14:39:12Z
Wikihez
7344
Template dialog opens one at a time; queued section conversions apply from the bottom of the page up
6531954
javascript
text/javascript
// <nowiki>
/*
* Page: [[MediaWiki:Gadget-wikt.dsb-panels.js]]
*
* Everything DefSideBoxes shows once you click: the per-sense advanced panel, the subsection editors,
* the part-of-speech picker, the template dialogs, the preview and the queued save.
*
* Loaded on demand by wikt.dsb, never on a plain page view. It exports only what that half calls.
*/
(function () {
"use strict";
var sections = require("ext.gadget.wikt.core").sections;
var edit = require("ext.gadget.wikt.edit");
var cite = require("ext.gadget.wikt.cite");
var util = edit.util;
var posparams = edit.posparams;
var wt = edit.wt;
var startData = edit.data;
var langs = edit.langs;
var escapeRegExp = util.escapeRegExp;
// Local names for the shared wikitext layer, so every call site below reads as it always did.
var langHeaderSource = wt.langHeaderSource;
var canonicalTemplateKey = wt.canonicalTemplateKey;
var aliasPattern = wt.aliasPattern;
var splitLeadingCalls = wt.splitLeadingCalls;
var splitTemplateParams = wt.splitTemplateParams;
var parseTemplateCalls = wt.parseTemplateCalls;
var splitCallParams = wt.splitCallParams;
var buildTemplateCall = wt.buildTemplateCall;
var tdText = wt.tdText;
var fetchTemplateData = wt.fetchTemplateData;
var parseWordListCall = wt.parseWordListCall;
var parseWordListCalls = wt.parseWordListCalls;
var namedParam = wt.namedParam;
var withNamedParam = wt.withNamedParam;
var buildWordListCall = wt.buildWordListCall;
var parseBulletWordList = wt.parseBulletWordList;
var sectionNames = sections.sectionNames;
var MANE_SECTION = sections.MANE_SECTION;
var PAGE_SUBSECTIONS = sections.PAGE_SUBSECTIONS;
var headingOuter = sections.headingOuter;
var headingText = sections.headingText;
var isManeHeading = sections.isManeHeading;
var sectionTypeOf = sections.sectionTypeOf;
var sectionOrdinalWithinLanguage = sections.sectionOrdinalWithinLanguage;
var isFormOfWrapper = sections.isFormOfWrapper;
var eachLeafDefinition = sections.eachLeafDefinition;
var api = new mw.Api();
// metadata[lang].g, minus the grammatical-number codes start.js filters out with isNumberCode(), rendered
// the way its genderLabel() does ("m-p" -> "mê (pirjimar)").
// start.js asks transitivity and structural form as two questions and joins them as "<form>-<base>", so
// "gerguhêz" alone is "gh" and "xwerû" + "gerguhêz" is "x-gh".
// Template call convertSectionToPos() drops under a newly-chosen POS heading, e.g. "Hoker" -> "{{hoker|ku}}".
// A "Formeke X" (form-of) heading uses the unchanged base pos template. Doubles as the list of template
// calls a conversion may find under the OLD heading and replace, rather than leave stacked beside the new one.
// A section's template call. "Formeke Xê" uses X's own template with form=erê. Biwêj is recognised so a
// conversion can replace its {{biwêj}}, but never written: no new ones.
function posTemplateFor(header) {
var form = /^Formeke (.+)ê$/.exec(header || "");
if (form && startData.posChoices[form[1]] && sections.hasFormHeading(form[1])) {
return { name: form[1], params: ["form=erê"] };
}
var key = util.decapitalize(header || "");
return startData.posChoices[key] ? { name: key, params: [] } : null;
}
// convertSectionToPos() drops whichever of the tag's names is actually present.
var MANE_TAG_REGEX = wt.MANE_TAG_REGEX;
// The six per-sense groups start.js writes as "#: {{tpl|lang|word1|word2}}" sub-lines under a "#" definition.
// Array order controls where a new group line lands relative to existing ones. Every group lives behind the
// quick row's "Hevmane û Bêhtir" dropdown; only the gloss and Nimûne/jêgirtin links sit in the row itself.
var SENSE_GROUPS = startData.senseGroups;
// The column word-list template new sections are written with; the rest of wordlistTemplates are
// spellings the parser also accepts.
var WORDLIST_TEMPLATE = startData.wordlistTemplates[0];
// ---- {{werger-ser}} / {{werger-bin}}: one translation box ----
// The pair brackets the rows; the translation editor gadget writes the rows themselves onto the
// rendered page, so this only ever builds (and reads back) the opening call.
// Parameters, from [[Şablon:werger-ser]]: 1 = the sense shown in the box head, 2 = a note in
// parentheses after it, id = anchor override (defaults to 1), stûn = column count, firehî = column
// width (default 26em), bgcolor, valign (default top), align (default left).
var WERGER_NAMED = ["id", "stûn", "firehî", "bgcolor", "valign", "align"];
function buildWergerBlock(params) {
var parts = ["werger-ser"];
// Positional slots: 2 can only be reached past 1, so an empty 1 still has to be written.
if (params["1"] || params["2"]) {
parts.push(params["1"] || "");
}
if (params["2"]) {
parts.push(params["2"]);
}
WERGER_NAMED.forEach(function (key) {
if (params[key]) {
parts.push(key + "=" + params[key]);
}
});
return "{{" + parts.join("|") + "}}\n{{werger-bin}}";
}
// The opening call of the first block in `text`, as the same {1, 2, id, ...} object. Empty object when
// there is none, so a half-typed draft restores as an empty form rather than throwing.
function parseWergerBlock(text) {
var out = {};
var body = wergerSerBody(String(text || ""));
if (!body) {
return out;
}
var positional = 0;
splitTemplateArgs(body).forEach(function (arg) {
var eq = arg.indexOf("=");
var name = eq === -1 ? "" : $.trim(arg.slice(0, eq));
if (eq !== -1 && WERGER_NAMED.indexOf(name) !== -1) {
out[name] = $.trim(arg.slice(eq + 1));
} else {
out[String(++positional)] = $.trim(arg);
}
});
return out;
}
// Everything after "{{werger-ser|" up to the "}}" that closes it, or "" when there is no call or it
// takes no parameters. Scans for the matching close rather than the first one: a gloss can hold a
// template of its own, and a lazy regex would stop inside it.
function wergerSerBody(text) {
var m = /\{\{\s*werger-ser\s*(?=[|}])/.exec(text);
if (!m) {
return "";
}
var depth = 1;
var start = m.index + m[0].length;
for (var i = start; i < text.length - 1; i++) {
var two = text.substr(i, 2);
if (two === "{{") {
depth++; i++;
} else if (two === "}}") {
depth--; i++;
if (!depth) {
return text.charAt(start) === "|" ? text.slice(start + 1, i - 1) : "";
}
}
}
return "";
}
// Splits on "|" at brace/bracket depth 0, so a value containing a template or a link stays whole.
function splitTemplateArgs(body) {
var args = [];
var depth = 0;
var current = "";
for (var i = 0; i < body.length; i++) {
var two = body.substr(i, 2);
if (two === "{{" || two === "[[") {
depth++; current += two; i++; continue;
}
if (two === "}}" || two === "]]") {
depth--; current += two; i++; continue;
}
if (body[i] === "|" && depth <= 0) {
args.push(current); current = ""; continue;
}
current += body[i];
}
args.push(current);
return args;
}
// Canonical ku.wiktionary "====" subsection order, used only as insertPageSubsection()'s insertion rank
// (see subsectionRankForHeader()). Wider than PAGE_SUBSECTIONS: it also covers subsections the gadget
// cannot create but still has to order around.
var SUBSECTION_ORDER = startData.subsectionOrder;
// ---- searchable <select> ----------------------------------------------------------------------------
// Same widget start.js gets from its util.js wrapNativeSelect(): the native <select> stays as the value
// holder (so .val() and "change" handlers keep working) but is hidden behind a text input that filters an
// overlay list. Ported rather than required() because util.js is a file inside start.js's package module,
// which another gadget cannot reach; the template-row decoration start.js uses is left out.
// util.js's wrapNativeSelect(): the native <select> stays the value holder behind a filtering text input.
// It reads its own placeholder off data-placeholder, and its wiktku-* styles ship in start.css, so both
// gadgets get the identical widget instead of two lookalikes.
function makeSearchable($select, placeholder) {
$select.attr("data-placeholder", placeholder || "Lê bigere…");
return util.wrapNativeSelect($select);
}
// ---- small shared utilities ----
// The readable half of an mw.Api rejection.
var apiErrorMessage = util.apiErrorMessage;
// Thrown from inside an edit() transform to bail out with a specific message.
var SubmitAbort = util.SubmitAbort;
// A gloss is one field on the page: the sense, then {{îzah|…}} explaining it when there is one.
function joinGloss(mane, izah) {
return izah ? $.trim(mane + " {{îzah|" + izah + "}}") : $.trim(mane);
}
// The two halves are linked separately, never joined first: linkify leaves template calls alone by
// design, so anything inside {{îzah|…}} would never be looked at.
//
// A half is skipped when there is nothing in it worth looking up, and -- when onlyUnlinked is set --
// when it already carries a link. Someone who has linked a word by hand has made that half theirs, and
// a save must not go on linking the rest of it behind their back; "Oto-lînk bike" leaves the flag off
// and still links on demand.
function linkifyGlossParts(mane, izah, done, onlyUnlinked) {
function needsLinking(text) {
if (!$.trim(text) || !util.hasLinkableText(text)) {
return false;
}
return !(onlyUnlinked && text.indexOf("[[") !== -1);
}
function step(text, next) {
if (!needsLinking(text)) {
next(text, null);
return;
}
util.linkifyText(text, function (linked, error) {
// An empty answer would wipe what the user typed, so only real text is taken.
next(typeof linked === "string" && $.trim(linked) ? $.trim(linked) : text, error || null);
});
}
step(mane, function (linkedMane, maneError) {
step(izah, function (linkedIzah, izahError) {
done(linkedMane, linkedIzah, maneError || izahError);
});
});
}
// Runs transformFn(content) inside mw.Api().edit()'s own transform callback so one fetch covers both the
// edit-conflict check and the content, preventing a stale overwrite of someone else's edit.
//
// No badtoken retry here on purpose: edit() posts through postWithEditToken -> postWithToken, which
// already throws the cached token away and repeats the request once when the wiki says "badtoken".
// A second retry of our own would only double it.
function editPage(summary, transformFn, onSuccess, onError) {
api.edit(mw.config.get("wgPageName"), function (revision) {
return { text: transformFn(revision.content), summary: summary };
}).then(onSuccess, function (codeOrAbort, result) {
if (codeOrAbort instanceof SubmitAbort) {
onError(codeOrAbort.message);
} else {
onError(util.msg.saveFailed + apiErrorMessage(codeOrAbort, result));
}
});
}
// ---- rendered preview: wikitext through the parse API, so a preview shows what the templates
// actually produce rather than a plain list standing in for them ----
// Same text is usually asked for twice (open the preview, close it, open it again), so the promise is
// kept rather than the call repeated.
var renderCache = {};
/**
* Renders wikitext as this page would render it. Resolves with an HTML string.
*
* pst: 1 applies pre-save transform, so ~~~~ and subst: behave as they will once saved. The
* modulestyles/modules the parse reports are loaded before resolving: without them TemplateStyles is
* missing and the preview would show the right markup in the wrong clothes.
*/
function renderWikitext(wikitext) {
if (renderCache[wikitext]) {
return renderCache[wikitext];
}
renderCache[wikitext] = api.post({
action: "parse",
format: "json",
formatversion: 2,
title: mw.config.get("wgPageName"),
text: wikitext,
contentmodel: "wikitext",
prop: "text|modules|jsconfigvars",
pst: 1,
disablelimitreport: 1,
disableeditsection: 1,
disabletoc: 1
}).then(function (data) {
var parsed = (data && data.parse) || {};
if (parsed.modulestyles && parsed.modulestyles.length) {
mw.loader.load(parsed.modulestyles);
}
if (parsed.modules && parsed.modules.length) {
mw.loader.load(parsed.modules);
}
return parsed.text || "";
}, function () {
// A failed render must not be remembered, or the next attempt gets the rejection back.
delete renderCache[wikitext];
return $.Deferred().reject().promise();
});
return renderCache[wikitext];
}
/**
* Puts rendered wikitext into $target, replacing whatever placeholder is there.
*
* The site's own gadgets are told about it through the "wikipage.content" hook, the same way
* MediaWiki announces real content, so collapsible lists and the like behave in the preview too.
*/
// MediaWiki warns when the hook is fired on content that is not in the document, and a gadget that
// listens to it can do nothing useful with nodes nobody can see.
function announceContent($el) {
if ($el.closest("html").length) {
mw.hook("wikipage.content").fire($el);
}
}
function fillWithRendered($target, wikitext, onFail) {
return renderWikitext(wikitext).then(function (html) {
if (!$target.closest("body").length) {
return; // the preview was dropped while the parse was in flight
}
$target.empty().append($("<div class='mw-parser-output'></div>").html(html));
announceContent($target);
}, function () {
if (onFail) onFail();
});
}
/**
* Puts a rendered sense in place of the one on the page, keeping this gadget's own furniture (the
* pencil's toolbar, the quick row, any open panel) where it is. The page's own nodes are detached
* rather than dropped, so the returned function can put them straight back when the change is
* cancelled or superseded.
*
* Nothing labels the result: the change is shown by the sense simply reading the way it will read,
* and the queue box is what says nothing is saved yet.
*/
function replaceSenseInPlace($li, item) {
var html = item.html;
var $anchor = $li.children(".dsb-toolbar").last();
// class/id/data-* off the parsed <li>, so a queued maneid shows its pill here exactly as it will
// once saved. The old values are kept for the restore below.
var node = $li.get(0);
var before = {};
$.each(node.attributes, function (_, attr) {
before[attr.name] = attr.value;
});
$.each(item.attrs, function (name, value) {
if (name !== "class") {
$li.attr(name, value);
}
});
var addedClasses = (item.attrs["class"] || "").split(/\s+/).filter(function (c) {
return c && !$li.hasClass(c);
});
$li.addClass(addedClasses.join(" "));
// add-examples hangs its own <ul> off the sense and opens its form as that <ul>'s next sibling.
// Detaching those leaves the form to open into a tree nobody can see -- "+ Nimune/jegirtin" takes
// the click and shows nothing -- so they stay where they are. Only the rows already in that <ul>
// are hidden, since the preview draws the examples itself; the tool's own title line stays.
var $keep = $li.children("ul, .add-example-form");
var $hiddenRows = $keep.filter("ul").children("li").filter(function () {
return !$(this).find(".add-example-form-title").length;
});
$hiddenRows.addClass("dsb-preview-hidden");
var originals = $li.contents().filter(function () {
return !$(this).is(".dsb-toolbar, .dsb-quick-row, .dsb-subsection-panel, .dsb-preview-block, .dsb-sense-preview");
}).not($keep).get();
// Where the sense's own nodes sat, so cancelling puts them back there rather than above the quick
// row -- the row belongs between the sense and its examples.
var backAfter = originals.length ? originals[0].previousSibling : null;
var $rendered = $("<span class='dsb-sense-preview'></span>").html(html);
if ($anchor.length) {
$anchor.after($rendered);
} else {
$li.prepend($rendered);
}
$(originals).detach();
announceContent($rendered);
// The id pill is drawn by the same code a page load uses, so a queued maneid looks like a saved
// one rather than like something this gadget invented.
if (window.wikt && wikt.showIDs && wikt.showIDs.process) {
wikt.showIDs.process($li);
}
// dsb.js puts the example pencils back on the redrawn examples. The originals keep theirs.
$li.triggerHandler("dsbSenseRedrawn");
return function () {
$hiddenRows.removeClass("dsb-preview-hidden");
$li.removeClass(addedClasses.join(" "));
$.each(item.attrs, function (name) {
if (name === "class") {
return;
}
if (Object.prototype.hasOwnProperty.call(before, name)) {
$li.attr(name, before[name]);
} else {
$li.removeAttr(name);
}
});
$li.children(".senseid-displayed, .etymid-displayed, .etymonid-displayed").remove();
if (backAfter && backAfter.parentNode === node) {
$(backAfter).after(originals);
} else if ($anchor.length) {
$anchor.after(originals);
} else {
$li.prepend(originals);
}
$rendered.remove();
};
}
/**
* The <li> out of a parsed "#" block, and the attributes it carries.
*
* The parse wraps it in its own <ol>, which must not reach the page: the sense already has a number
* from the list it lives in. A leading {{maneid}} also makes the parser emit an empty
* "li.mw-empty-elt" ahead of the real one, so the first <li> is not necessarily the sense.
*
* The attributes matter as much as the content: {{maneid|ku|x}} renders as class="senseid"
* data-id="x" on the <li> itself, which is what wikt.showids draws its pill from.
*/
function parsedSenseItem(html, depth) {
var $item = $("<div></div>").html(html).find("ol > li").filter(function () {
return !$(this).hasClass("mw-empty-elt");
}).first();
// A sub-sense parsed on its own comes back one <ol> deeper per "#": "##" is a list inside a list.
// Descend as far as the sense's own depth, or its number would be drawn inside a second list --
// "2." with a "1." under it.
for (var level = 1; level < (depth || 1) && $item.length; level++) {
var $inner = $item.children("ol").children("li").filter(function () {
return !$(this).hasClass("mw-empty-elt");
}).first();
if (!$inner.length) {
break;
}
$item = $inner;
}
if (!$item.length) {
return { html: html, attrs: {} };
}
var attrs = {};
$.each($item.get(0).attributes, function (_, attr) {
attrs[attr.name] = attr.value;
});
return { html: $item.html(), attrs: attrs };
}
// This gadget's own save queue + floating widget, separate from the shared Gadget-Editor queue.
// Each entry keeps its own transformFn so saveQueueNow() can re-run all of them against one fresh fetch.
var dsbQueue = {
// [{key, summary, transformFn, runLast, removePreview}]; transformFn: baseContent -> {content, warnings}.
// removePreview lives on the entry so replacing one entry takes its own live DOM preview with it.
entries: []
};
var $queueBox = null;
function dropPreview(entry) {
if (typeof entry.removePreview === "function") {
try {
entry.removePreview();
} catch (e) { /* a preview failing to clean up should never block the queue itself */ }
entry.removePreview = null;
}
}
function clearAllPreviews() {
dsbQueue.entries.forEach(dropPreview);
}
function clearQueue() {
clearAllPreviews();
dsbQueue.entries = [];
if ($queueBox) {
$queueBox.$list.empty();
$queueBox.$box.hide();
}
}
// Closes every editor UI on the page. Each open box's own "x" clears its getter, restores its picker
// link and updates the counter, so those are triggered rather than repeating that work here; hiding a
// row alone would leave its fields populated and the next pencil click would reopen them half-filled.
function closeAllPanels() {
$(".dsb-open-boxes .dsb-box-remove").trigger("click");
$(".dsb-quick-note").hide().text("");
$(".dsb-more-menu").hide();
$(".dsb-subsection-panel, .dsb-convert-pos-panel").hide();
// dsb.js owns each pencil's pinned state; tell it rather than hiding behind its back, or the next
// click on the pencil would only toggle "pinned" off and appear to do nothing.
$(".dsb-quick-row").trigger("dsbCancel");
}
function ensureQueueBox() {
if ($queueBox) {
return $queueBox;
}
var $box = $("<div class='dsb-queue-box'></div>").hide();
var $close = $("<a href='#' class='dsb-queue-close' title='Veşêre'>×</a>");
var $title = $("<div class='dsb-queue-title'></div>").text("Guhartinên li benda qeydkirinê");
var $list = $("<ul class='dsb-queue-list'></ul>");
var $empty = $("<div class='dsb-queue-empty'></div>").text(
"Guhartinên te dê li vir werin rêzkirin. Heta ku tu pêl «Guhartinan qeyd bike» nekî, "
+ "tiştek li ser rûpelê nayê weşandin."
);
var $status = $("<div class='dsb-queue-status'></div>");
var $save = $("<button type='button' class='dsb-queue-save'>Guhartinan qeyd bike</button>");
var $cancel = $("<button type='button' class='dsb-queue-cancel'></button>").text(util.msg.cancel);
var $actions = $("<div class='dsb-queue-actions'></div>").append($save, $cancel);
$close.on("click", function (e) {
e.preventDefault();
$box.hide();
});
$cancel.on("click", function () {
clearQueue();
closeAllPanels();
});
$save.on("click", function () {
saveQueueNow();
});
$box.append($close, $title, $empty, $list, $actions, $status);
$(document.body).append($box);
$queueBox = { $box: $box, $list: $list, $empty: $empty, $status: $status, $save: $save, $cancel: $cancel };
return $queueBox;
}
// Takes one change out of the queue and puts the page back as it was before it, its in-place preview
// included. The queue is the only record of what "Guhartinan qeyd bike" will write, so every line in it
// has to be removable on its own -- closing the panel that made a change does not unmake it.
// Each preview draws the page the whole queue would save, so one entry leaving changes what all the
// others should show. They are taken down and drawn again from what is left.
function redrawPreviews() {
dsbQueue.entries.forEach(function (entry) {
if (typeof entry.previewFn !== "function") {
return;
}
dropPreview(entry);
try {
var removePreview = entry.previewFn();
if (typeof removePreview === "function") {
entry.removePreview = removePreview;
}
} catch (e) { /* a broken preview never stops the queue */ }
});
}
function dropQueueEntry(entry) {
var at = dsbQueue.entries.indexOf(entry);
if (at === -1) {
return;
}
dropPreview(entry);
dsbQueue.entries.splice(at, 1);
redrawPreviews();
renderQueueBox();
hideQueueIfIdle();
}
function renderQueueBox() {
var box = ensureQueueBox();
box.$list.empty();
dsbQueue.entries.forEach(function (entry) {
var $drop = $("<button type='button' class='dsb-queue-drop'>×</button>")
.attr("title", "Vê guhartinê ji rêzê derxe");
$drop.on("click", function () {
dropQueueEntry(entry);
});
box.$list.append($("<li></li>").text(entry.summary).append($drop));
});
box.$empty.toggle(!dsbQueue.entries.length);
box.$status.removeClass("dsb-status-ok dsb-status-error").text("");
box.$save.prop("disabled", !dsbQueue.entries.length);
box.$box.show();
}
// Shown the moment a panel opens, empty, so it is clear before anything is typed that a change waits
// for "Guhartinan qeyd bike" rather than publishing itself.
function openQueueBox() {
renderQueueBox();
}
// Nothing queued and no form left open means nothing to save, so the box goes away rather than
// lingering on an empty state.
function hideQueueIfIdle() {
if (dsbQueue.entries.length || !$queueBox) {
return;
}
if ($(".dsb-quick-row:visible, .dsb-subsection-panel:visible, .dsb-convert-pos-panel:visible").length) {
return;
}
$queueBox.$box.hide();
}
// Asks core's postEdit module to show its toast on the next load (dsb.js loads it there), then reloads.
// Loaded here rather than declared as a dependency, so a reload happens even if it fails.
function reloadWithPostEditNotice() {
mw.loader.using("mediawiki.action.view.postEdit").then(function (require) {
require("mediawiki.action.view.postEdit").fireHookOnPageReload("saved");
}).always(function () {
location.reload();
});
}
// A transform returns either the new page text or {content, warnings}; both shapes are accepted.
// One that returns neither stops the save, named, rather than writing a broken page.
function transformedContent(entry, pageContent) {
var result = entry.transformFn(pageContent);
var next = typeof result === "string" ? result : (result && result.content);
if (typeof next !== "string") {
throw new SubmitAbort("Guhartin nehate sepandin: " + entry.summary);
}
return next;
}
// Queue order, except that runLast entries move to the end: converting "=== Mane ===" into a real POS
// heading renames the very heading every other queued change locates itself by.
// Those run from the bottom of the page up: a rename changes the ordinals of the sections after it
// ("Mane 2" becomes "Mane 1"), never of the ones before.
function orderedEntries() {
var normal = [];
var last = [];
dsbQueue.entries.forEach(function (entry) {
(entry.runLast ? last : normal).push(entry);
});
last.sort(function (a, b) {
return b.pagePosition - a.pagePosition;
});
return normal.concat(last);
}
/**
* The pending page as previews and example lookups need it: every queued transform, in save order,
* except the runLast section renames. Those callers find their section under the heading it has on
* the page now, which a queued "Mane" -> "Navdêr" would take away, and a rename leaves the senses as
* they are. An entry that cannot apply is skipped -- the save reports that properly, and a preview
* must not shout.
*/
function queuedContent(pageContent) {
var content = pageContent;
orderedEntries().filter(function (entry) {
return !entry.runLast;
}).forEach(function (entry) {
try {
content = transformedContent(entry, content);
} catch (e) {
// Left out of the preview; the save reports it properly. Named, so a change that fails to
// draw is distinguishable from one with nothing to draw.
mw.log.warn("wikt.dsb: pêşdîtin bêyî vê guhartinê hate xêzkirin — " + entry.summary, e);
}
});
return content;
}
/**
* Redraws one sense where it sits, from the page the queue would save -- so a queued gloss edit, a
* queued example and everything else pending on that sense are shown together, rendered by the parser
* rather than described in a box. Returns the queue's own "take the preview down" function.
*
* One preview per sense: a second queued change takes the first drawing down and draws the queue.
*/
function previewSense($li, language, sectionType, sectionOrdinal, definitionLevel) {
var cancelled = false;
var restore = null;
function undo() {
cancelled = true;
if (restore) {
restore();
restore = null;
}
if ($li.data("dsbPreview") === undo) {
$li.removeData("dsbPreview");
}
}
var previous = $li.data("dsbPreview");
if (typeof previous === "function") {
previous();
}
$li.data("dsbPreview", undo);
fetchPageContent().then(function (pageContent) {
if (cancelled) {
return;
}
var wikitext;
try {
wikitext = senseBlockWikitext(queuedContent(pageContent), language, sectionType,
sectionOrdinal, definitionLevel);
} catch (e) {
return;
}
renderWikitext(wikitext).then(function (html) {
if (cancelled || $li.data("dsbPreview") !== undo || !$li.closest("body").length) {
return;
}
restore = replaceSenseInPlace($li, parsedSenseItem(html, definitionLevel.length - 1));
});
});
return undo;
}
// The real save: re-runs every queued transformFn, in order, against one freshly-fetched revision and
// posts the result as a single edit with a combined summary.
function saveQueueNow() {
if (!dsbQueue.entries.length) {
return;
}
var box = ensureQueueBox();
box.$save.prop("disabled", true);
box.$cancel.prop("disabled", true);
box.$status.removeClass("dsb-status-ok dsb-status-error").text("Tê qeydkirin…");
var entries = orderedEntries();
// Grouped by place, in the order the places were first queued: several changes to one section read
// as "Kurmancî/Navdêr 2: a, b, c" rather than repeating the section name in front of each of them.
var order = [];
var byPlace = {};
entries.forEach(function (entry) {
if (!Object.prototype.hasOwnProperty.call(byPlace, entry.place)) {
byPlace[entry.place] = [];
order.push(entry.place);
}
byPlace[entry.place].push(entry.action);
});
var combinedSummary = order.map(function (place) {
var actions = byPlace[place].join(", ");
return place ? place + ": " + actions : actions;
}).join("; ") + " (DefSideBoxes)";
editPage(
combinedSummary,
function (pageContent) {
var content = pageContent;
entries.forEach(function (entry) {
content = transformedContent(entry, content);
});
return content;
},
function (data) {
forgetPageContent();
clearAllPreviews();
dsbQueue.entries = [];
box.$list.empty();
box.$status
.removeClass("dsb-status-error")
.addClass("dsb-status-ok")
.text("✓ Hemû guhartin hatin qeydkirin.");
box.$save.prop("disabled", true);
box.$cancel.prop("disabled", false);
// A real edit reloads the page and gets MediaWiki's own "Guhartina te hat weşandin." toast, the
// one a normal save shows. A null edit changed nothing, so the page stays as it is.
if (data && data.newrevid) {
reloadWithPostEditNotice();
}
},
function (message) {
box.$save.prop("disabled", false);
box.$cancel.prop("disabled", false);
box.$status.removeClass("dsb-status-ok").addClass("dsb-status-error").text(message);
}
);
}
// "Kurmancî/Navdêr" -- where a queued change lands, for its summary. The language name comes from the
// shipped list; an unknown code is left as the code itself.
// "Kurmancî/Navdêr", or "Kurmancî/Navdêr 2" where the language has more than one section of that
// type. Without the number a summary on a page with two Navdêr sections named neither of them.
// Numbered only when it disambiguates: on the usual page "Navdêr 1" would just be noise.
function placeLabel(language, sectionType, $h3) {
return langLabel(language) + "/" + sectionLabel(sectionType, $h3);
}
// Bilêvkirin sits above the parts of speech and belongs to the whole language, so naming the section
// it was opened from would say something untrue about what the edit touches.
function subsectionPlaceLabel(subsection, language, sectionType, $h3) {
if (subsection && subsection.free) {
return langLabel(language) + "/" + subsection.header;
}
return subsection && subsection.place === "pron"
? langLabel(language)
: placeLabel(language, sectionType, $h3);
}
function sectionLabel(sectionType, $h3) {
if ($h3 && $h3.length && sections.sectionTypeCountWithinLanguage($h3) > 1) {
return sectionType + " " + sections.sectionOrdinalWithinLanguage($h3);
}
return sectionType;
}
// Capitalised because it opens the summary; Kurdish otherwise writes language names lower-case.
function langLabel(language) {
var names = langs.names();
return util.capitalize(names[language] || language);
}
// The parts of a sense the form actually changed, so the summary lists them instead of saying nothing.
function senseChangeLabels(changes) {
var parts = [];
var changed = changes.changed || {};
if (changes.gloss) {
parts.push("mane");
}
if (changes.newSense) {
parts.push("maneyeke nû");
}
SENSE_GROUPS.forEach(function (group) {
if (changed["word:" + group.key]) {
parts.push(group.label.toLowerCase());
}
});
if (changed.maneid) {
parts.push("id");
}
if (changed.etiket) {
parts.push("etîket");
}
if (changed.kategori) {
parts.push("kategorî");
}
return parts;
}
// Queues transformFn(baseWikitext) -> {content, warnings} for the real save to run later against fresh
// content (see saveQueueNow()); nothing here touches the page yet. Also runs previewFn(), which drops a
// small "not saved yet" preview into the live DOM at the change's own spot on the page. Shared by every
// panel that queues a wikitext change: buildAdvancedPanel, buildSubsectionPanel, buildConvertPosPanel.
// place is what the change is being made to ("Kurmancî/Navdêr 2"), action is what is being done to it.
// Kept apart rather than pre-joined so saveQueueNow() can name a section once and list everything
// queued against it after it, instead of repeating the place before every item.
function queueChange(place, action, transformFn, $save, $status, previewFn, opts) {
opts = opts || {};
var entry = {
key: opts.key || null,
place: place || "",
action: action,
summary: place ? place + ": " + action : action,
transformFn: transformFn,
runLast: !!opts.runLast,
pagePosition: opts.pagePosition || 0,
// Kept, not just run: redrawPreviews() calls it again when another entry leaves the queue.
previewFn: typeof previewFn === "function" ? previewFn : null,
removePreview: null
};
// Queuing the same target twice replaces the pending change rather than stacking a second one --
// two "+Têkildar" entries would both run, and the second would abort on "Ev beş jixwe heye".
var superseded = false;
if (entry.key) {
for (var i = dsbQueue.entries.length - 1; i >= 0; i--) {
if (dsbQueue.entries[i].key === entry.key) {
dropPreview(dsbQueue.entries[i]);
dsbQueue.entries.splice(i, 1);
superseded = true;
}
}
}
dsbQueue.entries.push(entry);
$status.removeClass("dsb-status-error").addClass("dsb-status-ok").text(
"✓ li rêza guhartinan hate zêdekirin — pêl «Guhartinan qeyd bike» bike da ku bi cih bibe."
);
$save.prop("disabled", false);
if (previewFn) {
try {
var removePreview = previewFn();
if (typeof removePreview === "function") {
entry.removePreview = removePreview;
}
} catch (e) { /* a broken preview should never stop the change from being queued */ }
}
// What the superseded entry drew is gone from the queue, so whatever else is drawn from it is stale.
if (superseded) {
dsbQueue.entries.forEach(function (other) {
if (other !== entry) {
dropPreview(other);
}
});
redrawPreviews();
}
renderQueueBox();
}
// Runs text through the ext.gadget.linkify gadget ($.kuLinkify) and calls back with the linkified result.
// Passes the text through unchanged if the gadget can't be loaded, so it never blocks a save.
var linkifyText = util.linkifyText;
// ---- wikitext location ----
//
// One copy for both editing gadgets, in wikt.edit: add-examples writes into the same places.
var langSectionRange = wt.langSectionRange;
var locateSection = wt.locateSection;
var findDefinitionLine = wt.findDefinitionLine;
/**
* The wikitext of one sense's own block: its "#" line plus everything nested under it -- the "#:" word
* lines, the "#*" citations and any sub-senses -- stopping at the next line of the same depth that
* starts a new sense. Parsed on its own that block renders as exactly the <li> the page shows.
*/
function senseBlockWikitext(pageContent, language, sectionType, sectionOrdinal, definitionLevel) {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var defLine = findDefinitionLine(lines, loc.posLineIndex, definitionLevel);
var depth = definitionLevel.length - 1;
var out = [lines[defLine]];
for (var i = defLine + 1; i < lines.length; i++) {
var m = /^(#+)([:*]?)/.exec(lines[i]);
if (!m || m[1].length < depth) {
break;
}
// Same depth and no ":" or "*" after the hashes is the next sense, not part of this one.
if (m[1].length === depth && !m[2]) {
break;
}
out.push(lines[i]);
}
return out.join("\n");
}
// Inserts or extends a "#: {{tplKey|lang|word1|word2}}" sub-line for one sense (start.js's wordLine()
// format). If a line for this tplKey already exists under the definition, missing words are appended to
// it; otherwise a new line lands after the last existing group line, always before any "#*" citation lines.
function insertSenseWords(pageContent, language, sectionType, sectionOrdinal, definitionLevel, tplKey, words, opts) {
opts = opts || {};
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var defLine = findDefinitionLine(lines, loc.posLineIndex, definitionLevel);
var marker = new Array(definitionLevel.length).join("#"); // definitionLevel.length - 1 "#" characters
// Group 1 is the spelling the page uses, group 2 the parameters: an existing {{hevwate}} stays
// {{hevwate}} rather than being silently renamed to {{hevmane}}.
var tplLineRegex = new RegExp("^" + marker + ":\\s*\\{\\{\\s*(" + aliasPattern(tplKey) + ")\\s*\\|([^}]*)\\}\\}\\s*$");
var groupKeysPattern = SENSE_GROUPS.map(function (g) { return aliasPattern(g.key); }).join("|");
var groupLineRegex = new RegExp("^" + marker + ":\\s*\\{\\{\\s*(" + groupKeysPattern + ")\\s*\\|");
var anyBlockLineRegex = new RegExp("^" + marker + "[:*]");
var lastGroupLine = -1;
for (var i = defLine + 1; i < lines.length; i++) {
if (!anyBlockLineRegex.test(lines[i])) {
break;
}
var tplMatch = tplLineRegex.exec(lines[i]);
if (tplMatch) {
var foundKey = tplMatch[1];
if (opts.replace) {
// Editing an existing list: an empty result means the user cleared it, so drop the line.
if (words.length) {
lines[i] = marker + ": {{" + foundKey + "|" + language + "|" + words.join("|") + "}}";
} else {
lines.splice(i, 1);
}
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
var params = tplMatch[2].split("|"); // [lang, word1, word2, ...]
var existingWords = params.slice(1);
words.forEach(function (w) {
if (existingWords.indexOf(w) === -1) {
existingWords.push(w);
}
});
lines[i] = marker + ": {{" + foundKey + "|" + language + "|" + existingWords.join("|") + "}}";
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
if (groupLineRegex.test(lines[i])) {
lastGroupLine = i;
}
}
var insertAt = lastGroupLine >= 0 ? lastGroupLine + 1 : defLine + 1;
lines.splice(insertAt, 0, marker + ": {{" + tplKey + "|" + language + "|" + words.join("|") + "}}");
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
// Canonical left-to-right order of the small templates that can prefix a definition line's gloss text:
// maneid, then kategori (K), then etîket (e).
var LEADING_TPL_ORDER = startData.leadingTplOrder;
// True for a leading call the advanced panel edits in a box of its own -- those are kept out of the
// gloss box so the same template is never editable in two places at once. Redirect spellings count:
// {{f}} and {{etîket}} are {{e}}.
function hasOwnFieldBox(key) {
return LEADING_TPL_ORDER.indexOf(canonicalTemplateKey(key)) !== -1;
}
// Inserts, extends, or (with opts.replace) overwrites one of the small templates that prefix a definition
// line's gloss text -- {{maneid|lang|id}}, {{K|lang|kat1|...}} or {{e|lang|etîket1|...}}. Under
// opts.replace an empty values list removes the call rather than writing an empty one.
function insertLeadingSenseTpl(pageContent, language, sectionType, sectionOrdinal, definitionLevel, tplKey, values, opts) {
opts = opts || {};
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var defLine = findDefinitionLine(lines, loc.posLineIndex, definitionLevel);
var marker = /^#+/.exec(lines[defLine])[0];
var afterMarker = lines[defLine].slice(marker.length).replace(/^\s+/, "");
// Collecting the whole leading-call run means an existing tplKey call is found and merged wherever it
// already sits, not just when it happens to be first.
var split = splitLeadingCalls(afterMarker);
var calls = split.calls;
var rest = split.rest;
var existing = null;
calls.forEach(function (c) {
if (canonicalTemplateKey(c.key) === tplKey) {
existing = c;
}
});
if (existing) {
if (opts.replace) {
// Editing an existing call: an empty result means the user cleared it, so drop the call.
if (values.length) {
existing.params = [language].concat(values);
} else {
calls = calls.filter(function (c) { return c !== existing; });
}
} else {
var existingVals = existing.params.slice(1);
values.forEach(function (v) {
if (existingVals.indexOf(v) === -1) {
existingVals.push(v);
}
});
existing.params = [language].concat(existingVals);
}
} else if (!values.length) {
return pageContent; // nothing to add, and nothing there to clear
} else {
// Not present yet -- insert right before the first existing call that outranks tplKey in
// LEADING_TPL_ORDER (an unrecognized call always outranks everything), or at the end of the run.
var myRank = LEADING_TPL_ORDER.indexOf(tplKey);
var insertAt = calls.length;
for (var i = 0; i < calls.length; i++) {
var otherRank = LEADING_TPL_ORDER.indexOf(canonicalTemplateKey(calls[i].key));
if (otherRank === -1 || otherRank > myRank) {
insertAt = i;
break;
}
}
calls.splice(insertAt, 0, { key: tplKey, params: [language].concat(values) });
}
var rebuilt = calls.map(function (c) {
return "{{" + c.key + "|" + c.params.join("|") + "}}";
}).join(" ");
lines[defLine] = marker + " " + (rebuilt ? rebuilt + " " : "") + rest;
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
// Replaces just the gloss text of a definition line, leaving any leading template calls untouched --
// the save half of "Maneyê biguhêre" (edit sense).
function updateSenseGloss(pageContent, language, sectionType, sectionOrdinal, definitionLevel, newGloss) {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var defLine = findDefinitionLine(lines, loc.posLineIndex, definitionLevel);
var marker = /^#+/.exec(lines[defLine])[0];
var afterMarker = lines[defLine].slice(marker.length).replace(/^\s+/, "");
// The box holds everything except the calls with their own field, so those are put back in front.
// Dropping "{{mane?}}" here is what clears start.js's placeholder once a real gloss is written.
var kept = splitLeadingCalls(afterMarker).calls.filter(function (c) {
return hasOwnFieldBox(c.key);
});
var leadingText = kept.map(function (c) {
return "{{" + c.key + "|" + c.params.join("|") + "}}";
}).join(" ");
lines[defLine] = marker + " " + (leadingText ? leadingText + " " : "") + newGloss;
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
// Deletes one sense: its definition line plus everything indented under it (its "#:" groups, "#*"
// citations, any sub-senses). Refuses to empty a section, since a POS heading with no senses is worse
// than the sense the user wanted gone.
// A new sense goes directly below the one whose pencil asked for it -- after the lines that sense owns
// (its "#:" examples, "#*" citations and "##" sub-senses), so it reads as the next sense rather than
// landing at the foot of the section. It takes the same marker depth as its neighbour, so asking from
// a "##" sub-sense adds a sibling sub-sense.
function insertNewSense(pageContent, language, sectionType, sectionOrdinal, definitionLevel, text) {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var defLine = findDefinitionLine(lines, loc.posLineIndex, definitionLevel);
var marker = /^#+/.exec(lines[defLine])[0];
var end = defLine + 1;
var ownedRegex = new RegExp("^" + marker + "[#:*]");
while (end < lines.length && ownedRegex.test(lines[end])) {
end++;
}
lines.splice(end, 0, marker + " " + text);
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
function removeSense(pageContent, language, sectionType, sectionOrdinal, definitionLevel) {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var defLine = findDefinitionLine(lines, loc.posLineIndex, definitionLevel);
var marker = /^#+/.exec(lines[defLine])[0];
var end = defLine + 1;
var ownedRegex = new RegExp("^" + marker + "[#:*]");
while (end < lines.length && ownedRegex.test(lines[end])) {
end++;
}
// Top-level senses of this section only -- "##" sub-senses go with their parent either way.
if (definitionLevel.length === 2) {
var siblings = 0;
for (var i = loc.posLineIndex + 1; i < lines.length && !/^==/.test(lines[i]); i++) {
if (/^#[^*#:]/.test(lines[i])) {
siblings++;
}
}
if (siblings < 2) {
throw new SubmitAbort("Ev maneya dawî ya vê beşê ye. Ji bo rakirinê divê tu tevahiya beşê rakî.");
}
}
lines.splice(defLine, end - defLine);
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
// Fetches the page's current wikitext and pulls out just this sense's gloss text to prefill the "Maneyê
// biguhêre" form. A plain read, not part of the hardened save path -- the actual save still re-fetches
// fresh content, so only the prefilled text itself could be a little stale.
// Current contents of one existing subsection, for prefilling the edit form.
// This page's current wikitext.
// One fetch for the whole queue: every preview reads the same revision, and asking per preview meant
// downloading the page again for each queued change. Dropped after a save, when the page has moved on.
var pageContentPromise = null;
function forgetPageContent() {
pageContentPromise = null;
}
function fetchPageContent() {
if (pageContentPromise) {
return pageContentPromise;
}
pageContentPromise = rawPageContent();
// A failed read must not be remembered as the answer.
pageContentPromise.fail(forgetPageContent);
return pageContentPromise;
}
function rawPageContent() {
return api.get({
action: "query",
prop: "revisions",
rvprop: "content",
titles: mw.config.get("wgPageName"),
formatversion: 2
}).then(function (data) {
var page = data.query && data.query.pages && data.query.pages[0];
var content = page && !page.missing && page.revisions && page.revisions[0] && page.revisions[0].content;
if (!content) {
throw new SubmitAbort("Nikare muhtewaya rûpelê bixwîne.");
}
return content;
});
}
function fetchPageSubsection(language, sectionType, sectionOrdinal, subsection) {
return fetchPageContent().then(function (content) {
return readPageSubsection(content, language, sectionType, sectionOrdinal, subsection);
});
}
function fetchSenseFields(language, sectionType, sectionOrdinal, definitionLevel) {
return fetchPageContent().then(function (content) {
return readSenseFields(content, language, sectionType, sectionOrdinal, definitionLevel);
});
}
// Everything the advanced panel can edit about one sense, read off its definition line and the "#:" lines
// under it: the raw gloss (templates included), the leading maneid/K/e values, and each word-list group.
function readSenseFields(pageContent, language, sectionType, sectionOrdinal, definitionLevel) {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var defLine = findDefinitionLine(lines, loc.posLineIndex, definitionLevel);
var defMarker = /^#+/.exec(lines[defLine])[0];
var afterMarker = lines[defLine].slice(defMarker.length).replace(/^\s+/, "");
var leading = {};
splitLeadingCalls(afterMarker).calls.forEach(function (c) {
leading[canonicalTemplateKey(c.key)] = c.params.slice(1);
});
// Only start.js's "{{mane?|lang}}" placeholder is hidden; every other template stays visible so it can
// be edited by hand in the gloss box.
// The gloss box shows the line minus the calls that have their own box ({{maneid}}, {{K}}, {{e}} and
// their redirects) and minus start.js's "{{mane?}}" placeholder. Everything else -- a {{g}} link, a
// {{guharto}} -- stays visible and editable there.
var split = splitLeadingCalls(afterMarker);
var glossCalls = split.calls.filter(function (c) {
return !hasOwnFieldBox(c.key) && canonicalTemplateKey(c.key) !== "mane?";
});
var glossLead = glossCalls.map(function (c) {
return "{{" + c.key + "|" + c.params.join("|") + "}}";
}).join(" ");
var gloss = $.trim((glossLead ? glossLead + " " : "") + split.rest);
var marker = new Array(definitionLevel.length).join("#");
var groupKeysPattern = SENSE_GROUPS.map(function (g) { return aliasPattern(g.key); }).join("|");
var groupLineRegex = new RegExp("^" + marker + ":\\s*\\{\\{\\s*(" + groupKeysPattern + ")\\s*\\|([^}]*)\\}\\}\\s*$");
var anyBlockLineRegex = new RegExp("^" + marker + "[:*]");
var words = {};
for (var i = defLine + 1; i < lines.length; i++) {
if (!anyBlockLineRegex.test(lines[i])) {
break;
}
var m = groupLineRegex.exec(lines[i]);
if (m) {
words[canonicalTemplateKey(m[1])] = m[2].split("|").slice(1);
}
}
return {
gloss: gloss,
maneid: leading.maneid ? leading.maneid[0] : "",
etiket: leading.e || [],
kategori: leading.K || [],
words: words
};
}
// Finds a POS block's body range (up to, but not including, the next heading of level <= 3), and the
// header text + line index of any "====" subsections it already contains.
// depth is the part-of-speech heading's own ("===" on most entries, "====" where the senses are
// grouped under numbered etymologies). Its block runs to the next heading at that depth or shallower,
// and its subsections are the headings one deeper.
function posBlockBounds(lines, posLineIndex, depth) {
depth = depth || 3;
var blockEnd = wt.headingAtOrAbove(depth);
var subsectionHeading = wt.headingAtDepth(depth + 1);
var end = lines.length;
var existingHeaders = [];
for (var i = posLineIndex + 1; i < lines.length; i++) {
if (blockEnd.test(lines[i])) {
end = i;
break;
}
var sub = subsectionHeading.exec(lines[i]);
if (sub) {
existingHeaders.push({ header: $.trim(sub[1]), lineIndex: i });
}
}
return { end: end, existingHeaders: existingHeaders, depth: depth };
}
// Looks up a header's rank in SUBSECTION_ORDER. -1 for a header that isn't one of ours; such a header is
// never used as an insertion anchor.
function subsectionRankForHeader(header) {
return SUBSECTION_ORDER.indexOf(header);
}
// Body line range of one "====" subsection inside a POS block: everything after its heading up to the next
// "====" heading, or the end of the block. Null if the block has no such subsection.
function subsectionBodyRange(bounds, header) {
var headers = bounds.existingHeaders;
for (var i = 0; i < headers.length; i++) {
if (headers[i].header === header) {
return {
start: headers[i].lineIndex + 1,
end: i + 1 < headers.length ? headers[i + 1].lineIndex : bounds.end
};
}
}
return null;
}
// ---- template-parameter editing -------------------------------------------------------------------
// A gloss that is nothing but one template call gets a labelled box per parameter instead of a raw
// wikitext line, the way VisualEditor's template dialog works.
// The single template call a gloss is made of, or null when the line is anything less tidy. {{g|...}} is a
// link inside the gloss rather than the gloss itself, so it neither disqualifies the line nor gets a form.
function glossTemplateTarget(gloss) {
var parsed = parseTemplateCalls(gloss || "");
if (parsed.otherContent) {
return null;
}
var editable = parsed.calls.filter(function (c) {
return canonicalTemplateKey(c.name) !== "g";
});
return editable.length === 1 ? editable[0] : null;
}
// ---- the three places that carry no "====" heading of their own ----------------------------------
// Bilêvkirin is a "===" section above the POS blocks, images sit at the top of a POS block, and the
// h-bnr hatnote goes above the first language heading. Each is located here and then read, written or
// inserted through the same three lines of splice, so nothing below needs to know about them.
var IMAGE_LINE = /^\[\[\s*(?:Wêne|File|Image|Dosya)\s*:/i;
/**
* {lines, langSectionIndex, exists, start, end, insertAt} for a subsection carrying a "place", or null
* for an ordinary one. start..end is the body as it stands; insertAt is where a missing one goes.
*/
function locateLoosePlace(pageContent, language, sectionType, sectionOrdinal, subsection) {
var place = subsection.place;
if (!place) {
return null;
}
var i, end;
if (place === "page-top") {
// Above every language section, so the whole page is the search space. The call is found under
// any of its names, and its own name is carried back so a save never renames it.
var pageLines = pageContent.split("\n");
var call = wt.findHbnrCall(pageContent);
if (call) {
return { lines: pageLines, langSectionIndex: 0, exists: true, call: call,
start: call.lineIndex, end: call.lineIndex + 1, insertAt: call.lineIndex };
}
return { lines: pageLines, langSectionIndex: 0, exists: false, start: 0, end: 0, insertAt: 0 };
}
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
if (place === "pron") {
// One per language, not one per part of speech, so it is looked for above the POS heading.
for (i = 0; i < loc.posLineIndex; i++) {
var h3 = /^===\s*([^=]+?)\s*===/.exec(lines[i]);
if (h3 && $.trim(h3[1]) === subsection.header) {
end = i + 1;
while (end < lines.length && !/^==/.test(lines[end])) {
end++;
}
return { lines: lines, langSectionIndex: loc.langSectionIndex, exists: true,
start: i + 1, end: end, insertAt: i };
}
}
// Straight after the language heading, which puts it before every POS block.
return { lines: lines, langSectionIndex: loc.langSectionIndex, exists: false,
start: 1, end: 1, insertAt: 1 };
}
if (place === "pos-top") {
// Between the conjugation line and the headword template -- where the wizard writes them.
var at = loc.posLineIndex + 1;
while (at < lines.length && /^\{\{\s*ku-tewîn/.test($.trim(lines[at]))) {
at++;
}
var from = at;
while (at < lines.length && IMAGE_LINE.test($.trim(lines[at]))) {
at++;
}
return { lines: lines, langSectionIndex: loc.langSectionIndex, exists: at > from,
start: from, end: at, insertAt: from };
}
return null;
}
// The wikitext a "place" section's content becomes. A word list there is one template call, not a
// {{kol3}} block, so the words are joined into it -- and any ⊕ modifier a row picked up is dropped,
// since a page name is all the call can carry.
function loosePlaceBody(subsection, content, language) {
if (subsection.kind === "images") {
return $.grep($.map(content || [], function (image) {
return wt.buildImageLine({
file: image.file,
caption: wt.captionForLang(image.caption, language),
alt: image.alt,
size: image.size,
extra: image.extra
});
}), Boolean).join("\n");
}
return content;
}
// The words a "place" word list holds, flattened out of its single group. Any ⊕ modifier a row picked
// up is dropped: a page name is all the call can carry.
function loosePlaceWords(content) {
var words = [];
$.each(content || [], function (_, group) {
$.each(group.words || [], function (_, raw) {
var plain = $.trim(util.parseWordValue(raw).word);
if (plain && $.inArray(plain, words) === -1) {
words.push(plain);
}
});
});
return words;
}
// Writes one of those places, inserting it when it is not there yet and replacing it when it is. Only
// Bilêvkirin carries a heading; the other two are bare lines.
function writeLoosePlace(pageContent, language, sectionType, sectionOrdinal, subsection, content) {
var at = locateLoosePlace(pageContent, language, sectionType, sectionOrdinal, subsection);
if (!at) {
throw new SubmitAbort("Cih nehate dîtin: " + subsection.header);
}
// A call naming a subpage -- "{{h-bnr/-ane}}" -- holds a list shared by every page transcluding
// it. Writing here would change this page alone and desync the rest, so the edit is refused and
// the template that actually owns the list is named instead.
if (at.call && at.call.subpage) {
throw new SubmitAbort("Ev rûpel «{{" + at.call.name + "}}» bi kar tîne — lîste li " +
"[[Şablon:" + at.call.subpage + "]] ye, lê wê zêde bike, ne li vê rûpelê.");
}
// A word list here is one template call, so it is built where the existing call's own name is
// known -- editing "{{bnr-h|...}}" must not turn it into "{{h-bnr|...}}".
var body = subsection.kind === "wordlist"
? wt.buildHbnrCall(loosePlaceWords(content), at.call && at.call.name)
: $.trim(content || "");
// A "===" section is separated from what is above and below it by a blank line, the way the rest
// of an entry is written. The other two places are bare lines inside a block and take none.
var heading = subsection.place === "pron";
function blank(index) {
return $.trim(at.lines[index] || "") === "";
}
if (at.exists) {
if (body) {
var replacement = [body];
// end is the next "==" heading; the blank line before it is inside the range being replaced.
if (heading && /^==/.test(at.lines[at.end] || "")) {
replacement.push("");
}
at.lines.splice.apply(at.lines, [at.start, at.end - at.start].concat(replacement));
} else {
// Emptying the box removes the section rather than leaving an empty heading behind.
at.lines.splice(at.start, at.end - at.start);
if (heading) {
at.lines.splice(at.insertAt, 1);
// The blank line that separated it goes with it, rather than doubling up.
if (blank(at.insertAt) && blank(at.insertAt - 1)) {
at.lines.splice(at.insertAt, 1);
}
}
}
} else {
if (!body) {
throw new SubmitAbort("Tiştek nehate nivîsandin.");
}
var block = heading ? ["=== " + subsection.header + " ===", body] : [body];
if (heading) {
if (!blank(at.insertAt - 1)) {
block.unshift("");
}
if (!blank(at.insertAt)) {
block.push("");
}
}
at.lines.splice.apply(at.lines, [at.insertAt, 0].concat(block));
}
return pageContent.slice(0, at.langSectionIndex) + at.lines.join("\n");
}
// What a section body holds: its {{kol3}} groups, the plain bullet list that stands in for them, or
// the raw wikitext when it is neither. A free section is read this way before its kind is known.
function readSubsectionBody(body, subsection) {
if (subsection.kind === "wordlist" || subsection.free) {
var calls = parseWordListCalls(body);
if (calls.length) {
return {
kind: "wordlist",
groups: calls.map(function (call) {
return { sernav: namedParam(call.named, "sernav"), words: call.words };
})
};
}
var bullets = parseBulletWordList(body);
if (bullets) {
// Editable as a word list; saving rewrites the bullets as {{kol3}}.
return { kind: "wordlist", groups: [{ sernav: "", words: bullets }], fromBullets: true };
}
}
return { kind: "text", text: $.trim(body) };
}
// The wikitext a section body becomes after an edit. A wordlist keeps its own template name, language
// and named params ("cure=Jê", ...) and only has its positional words replaced; a text one is
// replaced whole.
function rewriteSubsectionBody(body, language, subsection, content) {
if (subsection.kind !== "wordlist") {
// Replacing the body wholesale drops that separator, so put it back.
return content + (/\n\s*$/.test(body) ? "\n" : "");
}
var calls = parseWordListCalls(body);
if (calls.length) {
// Each call is rewritten where it stands, so text between and around them survives untouched.
// Last to first, so the offsets of the earlier ones stay valid as we go.
var replacement = body;
for (var gi = calls.length - 1; gi >= 0; gi--) {
var call = calls[gi];
var group = content[gi];
// A group emptied in the form drops its call entirely rather than leaving "{{kol3|ku}}".
var rebuilt = group && group.words.length
? buildWordListCall(call.name, call.lang, withNamedParam(call.named, "sernav", group.sernav), group.words)
: "";
replacement = replacement.slice(0, call.start) + rebuilt + replacement.slice(call.end);
}
// Groups the form added beyond the ones already there go after the last of them.
var added = content.slice(calls.length).filter(function (group) {
return group.words.length;
}).map(function (group) {
return buildWordListCall(WORDLIST_TEMPLATE, language,
withNamedParam(["cure=" + subsection.header], "sernav", group.sernav), group.words);
});
if (added.length) {
replacement = replacement.replace(/\s*$/, "") + "\n\n" + added.join("\n\n") + "\n";
}
return replacement.replace(/\n{3,}/g, "\n\n"); // gaps left by any dropped call
}
if (parseBulletWordList(body)) {
// A plain bullet list is replaced wholesale by a real {{kol3}} on its first edit.
return content.map(function (group) {
return buildWordListCall(WORDLIST_TEMPLATE, language,
withNamedParam(["cure=" + subsection.header], "sernav", group.sernav), group.words);
}).join("\n\n") + (/\n\s*$/.test(body) ? "\n" : "");
}
throw new SubmitAbort("Nikare lîsteya vê beşê bixwîne: " + subsection.header);
}
// ---- sections that are not subsections of a part of speech ----
// A "=== Etîmolojî ===" that the parts of speech hang under, a "=== Çavkanî ===" at the foot of a
// language, a heading nobody planned for. Located by its own text, and edited as the wikitext under
// it -- or as a {{kol3}} list when that is what it holds.
function freeSectionBody(pageContent, language, subsection) {
var loc = wt.locateHeadingByText(pageContent, language, subsection.header, subsection.free.ordinal);
return { loc: loc, body: loc.lines.slice(loc.bodyStart, loc.bodyEnd).join("\n") };
}
function readFreeSection(pageContent, language, subsection) {
var at = freeSectionBody(pageContent, language, subsection);
return readSubsectionBody(at.body, subsection);
}
function updateFreeSection(pageContent, language, subsection, content) {
var at = freeSectionBody(pageContent, language, subsection);
at.loc.lines.splice(at.loc.bodyStart, at.loc.bodyEnd - at.loc.bodyStart,
rewriteSubsectionBody(at.body, language, subsection, content));
return pageContent.slice(0, at.loc.langSectionIndex) + at.loc.lines.join("\n");
}
// Current contents of an existing "====" subsection. A wordlist one whose template can't be parsed comes
// back as {kind: "text"} so the caller can still edit it as raw wikitext instead of refusing outright.
function readPageSubsection(pageContent, language, sectionType, sectionOrdinal, subsection) {
if (subsection.free) {
return readFreeSection(pageContent, language, subsection);
}
var loose = locateLoosePlace(pageContent, language, sectionType, sectionOrdinal, subsection);
if (loose) {
if (!loose.exists) {
return null;
}
if (loose.call) {
return { kind: "wordlist", groups: [{ sernav: "", words: loose.call.words }] };
}
var looseBody = $.trim(loose.lines.slice(loose.start, loose.end).join("\n"));
if (subsection.kind === "images") {
return {
kind: "images",
images: $.grep($.map(looseBody.split("\n"), wt.parseImageLine), Boolean)
};
}
if (subsection.kind !== "wordlist") {
return { kind: "text", text: looseBody };
}
// One template call rather than a {{kol3}} block: its positional parameters are the words.
var looseCall = parseTemplateCalls(looseBody).calls[0];
return { kind: "wordlist", groups: [{ sernav: "", words: (looseCall && looseCall.params) || [] }] };
}
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var bounds = posBlockBounds(loc.lines, loc.posLineIndex, loc.depth);
var range = subsectionBodyRange(bounds, subsection.header);
if (!range) {
return null;
}
return readSubsectionBody(loc.lines.slice(range.start, range.end).join("\n"), subsection);
}
// Rewrites an existing "====" subsection in place. A wordlist one keeps its own template name, language and
// named params ("cure=Jê", ...) and only has its positional words replaced; a text one is replaced whole.
function updatePageSubsection(pageContent, language, sectionType, sectionOrdinal, subsection, content) {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var bounds = posBlockBounds(lines, loc.posLineIndex, loc.depth);
var range = subsectionBodyRange(bounds, subsection.header);
if (!range) {
throw new SubmitAbort(util.msg.sectionMissing + subsection.header);
}
var body = lines.slice(range.start, range.end).join("\n");
lines.splice(range.start, range.end - range.start,
rewriteSubsectionBody(body, language, subsection, content));
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
// Inserts a new "==== Header ====" subsection into its SUBSECTION_ORDER position within a POS block, right
// before the first ranked subsection that outranks it, or at the end if none does. Either a
// {{kol3|...}} word list or a plain paragraph, matching start.js's own subsection formats.
function insertPageSubsection(pageContent, language, sectionType, sectionOrdinal, subsection, content) {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var bounds = posBlockBounds(lines, loc.posLineIndex, loc.depth);
var alreadyThere = false;
bounds.existingHeaders.forEach(function (h) {
if (h.header === subsection.header) {
alreadyThere = true;
}
});
if (alreadyThere) {
throw new SubmitAbort("Ev beş jixwe heye: " + subsection.header);
}
var body = subsection.kind === "wordlist"
? content.map(function (group) {
return buildWordListCall(WORDLIST_TEMPLATE, language,
withNamedParam(["cure=" + subsection.header], "sernav", group.sernav), group.words);
}).join("\n\n")
: content;
var myRank = subsectionRankForHeader(subsection.header);
var headers = bounds.existingHeaders;
var insertAt = bounds.end;
for (var i = 0; i < headers.length; i++) {
var otherRank = subsectionRankForHeader(headers[i].header);
if (otherRank !== -1 && otherRank > myRank) {
insertAt = headers[i].lineIndex;
break;
}
}
// Exactly one blank line on each side: the line before the insertion point is normally already blank
// (it separated the previous subsection from the next heading), so only add one where there isn't one.
var block = [];
if (insertAt > 0 && $.trim(lines[insertAt - 1]) !== "") {
block.push("");
}
var marks = wt.headingMarks(bounds.depth + 1);
block.push(marks + " " + subsection.header + " " + marks, body);
if (insertAt < lines.length) {
block.push("");
}
lines.splice.apply(lines, [insertAt, 0].concat(block));
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
// ---- converting an unclassified "=== Mane ===" placeholder heading into a real POS heading ----
// Some pages carry a heading literally named "Mane" (optionally "Mane 2" etc.) instead of a real
// part-of-speech name, tagged right below with {{cure?|<lang>}} (ku.wiktionary's "part of speech not yet
// determined" maintenance flag). It is a section key of its own (sections.MANE_SECTION), so the shared
// locator finds it like any other heading; only the rewriting below is particular to it.
// Renames the "=== Mane ===" placeholder to the chosen POS heading, drops the maintenance tag line right
// after it if present (MANE_TAG_REGEX -- cure? or any of its known aliases), and inserts the matching
// "{{<pos>|<lang>}}" call in its place (skipped when the section has no template of its own).
// Other content under the heading is left untouched.
/**
* Where a section's own headword template sits, as {index, text}, or {index: -1} when it has none.
*
* Not always the line under the heading: a "{{ku-tewîn-lk}}" line and any number of "[[Wêne:...]]"
* images can come first. Everything down to the first definition line is searched, and index doubles
* as the slot a new call belongs in when there is no old one.
*/
function findPosTemplateLine(lines, headingLine, sectionType) {
var oldSpec = posTemplateFor(sectionType);
var oldTemplate = oldSpec ? oldSpec.name : (sectionType === "Biwêj" ? "biwêj" : null);
var oldTemplateRegex = oldTemplate &&
new RegExp("^\\{\\{\\s*" + escapeRegExp(oldTemplate) + "\\s*[|}]");
var at = headingLine + 1;
while (at < lines.length && !/^#/.test(lines[at]) && !/^==/.test(lines[at])) {
if (MANE_TAG_REGEX.test(lines[at]) ||
(oldTemplateRegex && oldTemplateRegex.test(lines[at]))) {
return { index: at, text: lines[at], end: at };
}
at++;
}
return { index: -1, text: "", end: at };
}
// Named parameters merged in order, each written once: a later value replaces an earlier one of the
// same name. Positional values are kept as they come.
function mergeCallParams() {
var out = [];
var seen = {};
$.each(Array.prototype.slice.call(arguments), function (_, list) {
$.each(list || [], function (__, param) {
var m = /^\s*([^=|]+?)\s*=([\s\S]*)$/.exec(param);
if (!m) {
out.push(param);
return;
}
// "z" and "c" are one name to Modul:rêziman, so the form's "c=m" meets the page's "z=".
var key = wt.canonicalParamName(m[1]);
if (!Object.prototype.hasOwnProperty.call(seen, key)) {
seen[key] = out.length;
out.push(param);
return;
}
// The page's own spelling stays when it carried a value; an empty one gives way entirely.
var earlier = /^\s*([^=|]+?)\s*=([\s\S]*)$/.exec(out[seen[key]]);
// "?" asks for a value, so it never replaces one the page already has.
if ($.trim(earlier[2]) && $.trim(m[2]) === "?") {
return;
}
out[seen[key]] =$.trim(earlier[2]) ? earlier[1] + "=" + m[2] : param;
});
});
return out;
}
// What the call on the page already says, minus its language. Carried over only when the new section
// uses that same template -- "z=m" survives Navdêr becoming Formeke navdêrê, while a verb's "c=h-gh"
// has no meaning on a noun and is left behind.
function keptCallParams(line, templateName) {
var call = parseTemplateCalls($.trim(line || "")).calls[0];
if (!call || canonicalTemplateKey(call.name) !== canonicalTemplateKey(templateName)) {
return [];
}
return call.params.slice(1).filter(function (param) {
return /=/.test(param);
});
}
// The headword call a converted section gets, or "" when the new section has none. What the old call
// (found, from findPosTemplateLine) said keeps its place, then the section's own defaults, then what the
// form was filled in with -- each name written once, the last value winning, so a converted call reads
// as small a change from the old one as it can.
function posCallLine(found, language, newSectionType, posParams) {
var spec = posTemplateFor(newSectionType);
if (!spec) {
return "";
}
var kept = found.index !== -1 ? keptCallParams(found.text, spec.name) : [];
// A lemma is not an inflected form, so the old section's form=erê stays behind.
if (spec.params.indexOf("form=erê") === -1) {
kept = kept.filter(function (param) {
return !/^\s*form\s*=\s*erê\s*$/.test(param);
});
}
var params = mergeCallParams(kept, spec.params, posParams ? posParams.split("|") : []);
return "{{" + spec.name + "|" + language + (params.length ? "|" + params.join("|") : "") + "}}";
}
function convertSectionToPos(pageContent, language, sectionType, sectionOrdinal, newSectionType, posParams) {
if (!sectionNames[newSectionType]) {
throw new SubmitAbort("Navê cureyê rêzimanî yê hilbijartî nediyar e.");
}
// locateSection() already handles the "Mane" placeholder as a section type of its own.
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var headingLine = loc.posLineIndex;
var depth = loc.depth;
// A section straight under a numbered group heading takes that heading's place: the group heading
// held nothing but its gloss.
var group = depth > 3 ? wt.numberedGroupAbove(lines, headingLine, depth) : -1;
if (group !== -1) {
lines.splice(group, headingLine - group);
headingLine = group;
depth--;
}
// At the heading's own depth. Written without a number: the renumbering pass below decides every
// number in this language section at once, so one carried over from the old name would just be
// overwritten.
var marks = wt.headingMarks(depth);
lines[headingLine] = marks + " " + newSectionType + " " + marks;
// The line under the heading is either the old section's own template call ("{{biwêj|ku}}") or, for a
// Mane placeholder, the "needs a POS" maintenance tag. Both are replaced by the new POS's call.
// The new call takes the old one's own place -- or, when there was none, the slot just above the
// definitions, which is where the wizard writes it.
var found = findPosTemplateLine(lines, headingLine, sectionType);
var insertAt = found.index !== -1 ? found.index : found.end;
if (found.index !== -1) {
lines.splice(found.index, 1);
}
var call = posCallLine(found, language, newSectionType, posParams);
if (call) {
lines.splice(insertAt, 0, call);
}
// Renaming a section can make it repeat one the page already had, and can equally leave a name that
// is no longer repeated. Both are settled by renumbering the whole language section: a "Rengdêr"
// beside a new "Rengdêr" becomes "Rengdêr 1" and "Rengdêr 2", and a number left behind by a name
// that is now unique is dropped.
var body = lines.join("\n");
var nextLang = body.indexOf("\n== ", 1);
var end = nextLang === -1 ? body.length : nextLang;
body = wt.numberRepeatedPosHeadings(body.slice(0, end)) + body.slice(end);
return pageContent.slice(0, loc.langSectionIndex) + body;
}
// ---- "guhartina pêşketî" (advanced editing): combine several independent per-sense edits into ONE save ----
// Threads pageContent through the same pure functions every single-field form uses, applying only the
// pieces the caller actually filled in. A piece that conflicts with the page (maneid already set, a
// subsection that's already there) is skipped with a warning rather than failing the whole save. Throws
// SubmitAbort if nothing was filled in, so this can never turn into a no-op save.
function applyAdvancedChanges(pageContent, language, sectionType, sectionOrdinal, definitionLevel, changes) {
var content = pageContent;
var warnings = [];
var appliedAnything = false;
if (changes.gloss) {
content = updateSenseGloss(content, language, sectionType, sectionOrdinal, definitionLevel, changes.gloss);
appliedAnything = true;
}
if (changes.newSense) {
content = insertNewSense(content, language, sectionType, sectionOrdinal, definitionLevel, changes.newSense);
appliedAnything = true;
}
// Every box below is prefilled with what the sense already has, so a save replaces that value rather
// than merging into it -- and clearing a box removes the template. changes.changed marks the boxes
// whose contents actually differ from the page, so an opened-but-untouched box writes nothing.
var replace = { replace: true };
if (changes.changed.maneid) {
content = insertLeadingSenseTpl(content, language, sectionType, sectionOrdinal, definitionLevel, "maneid", changes.maneid ? [changes.maneid] : [], replace);
appliedAnything = true;
}
if (changes.changed.etiket) {
content = insertLeadingSenseTpl(content, language, sectionType, sectionOrdinal, definitionLevel, "e", changes.etiket, replace);
appliedAnything = true;
}
if (changes.changed.kategori) {
content = insertLeadingSenseTpl(content, language, sectionType, sectionOrdinal, definitionLevel, "K", changes.kategori, replace);
appliedAnything = true;
}
SENSE_GROUPS.forEach(function (group) {
if (changes.changed["word:" + group.key]) {
content = insertSenseWords(content, language, sectionType, sectionOrdinal, definitionLevel, group.key, changes.words[group.key], replace);
appliedAnything = true;
}
});
if (!appliedAnything) {
throw new SubmitAbort("Tiştek nehat guhartin.");
}
return { content: content, warnings: warnings };
}
// ---- per-definition UI: a small always-visible "+" toggle (no hover, so it works on mobile/touch too) ----
// A growing list of word rows. The row itself is util.js's, shared with the wizard; this owns the list:
// typing in the last row appends a fresh empty one, so there is always somewhere to type.
var WORDLIST_COLUMN_THRESHOLD = 6;
function buildWordListField(placeholder, opts) {
opts = opts || {};
var $list = $("<ol class='dsb-wordlist'></ol>");
// One column is a long scroll once a list gets past a handful of words. The class only asks for
// two; the stylesheet still gives one on a narrow screen. Two columns inside the panel's usual
// 520px would leave each word about 250px, so the panel drops its width cap for as long as any
// list in it is in two columns -- checked across the whole panel, since a "Herwiha binêre" holds
// several groups and one of them going wide settles it for all.
function updateColumns() {
$list.toggleClass("dsb-wordlist-2col",
$list.children("li").length > WORDLIST_COLUMN_THRESHOLD);
var $panel = $list.closest(".dsb-subsection-panel");
$panel.toggleClass("dsb-panel-wide", !!$panel.find(".dsb-wordlist-2col").length);
}
function makeRow() {
var row = util.makeModifierRow({
prefix: "dsb-",
placeholder: placeholder,
modTitle: util.msg.addModifier,
removeTitle: util.msg.removeWord,
removeModTitle: util.msg.removeModifier,
modifiers: opts.modifiers,
// "bixe qutiyan" puts each comma-separated word in a row of its own, right after the row it
// was typed in, and the caret follows to the last of them. One word per row is also one
// word per template parameter, which both {{hevmane}} and {{kol3}} read the same way --
// a comma left inside a parameter would not be.
onSplit: function (rest) {
var $after = row.$li;
var last = null;
rest.forEach(function (raw) {
var extra = makeRow();
$after.after(extra.$li);
$after = extra.$li;
if (raw) extra.setRaw(raw);
last = extra;
});
// Always an empty row at the end to type the next word into.
var $tail = $list.children("li").last();
if ($.trim($tail.data("row").getRaw())) {
last = makeRow();
$list.append(last.$li);
}
updateColumns();
if (last) last.focus();
},
// raw === null means the row removed itself, so the count has changed either way.
onChange: function (raw) {
if (raw !== null) {
var isLast = row.$li.is($list.children("li").last());
if (isLast && raw) $list.append(makeRow().$li);
}
updateColumns();
}
});
row.$li.data("row", row);
return row;
}
$list.append(makeRow().$li);
return {
$element: $list,
values: function () {
var vals = [];
$list.children("li").each(function () {
var raw = $(this).data("row").getRaw();
if (raw) vals.push(raw);
});
return vals;
},
// Replaces the whole list, keeping one empty trailing row to type into.
setValues: function (raws) {
$list.empty();
raws.forEach(function (raw) {
var row = makeRow();
$list.append(row.$li);
row.setRaw(raw);
});
$list.append(makeRow().$li);
updateColumns();
},
focus: function () {
$list.children("li").first().data("row").focus();
}
};
}
// Plain removable-tag field, used for Etîket and Kategorî (simple value lists with no per-word modifiers).
// Enter or "," commits the current text as a tag; Backspace on an empty input removes the last tag;
// values() also commits whatever is still typed but not yet confirmed.
function buildTagField(placeholder) {
var $input = $("<input type='text' class='dsb-input dsb-tag-input'>").attr("placeholder", placeholder);
var $element = $("<span class='dsb-tagfield'></span>").append($input);
function currentValues() {
var vals = [];
$element.children(".dsb-tag").each(function () {
vals.push($(this).data("value"));
});
return vals;
}
function addTag(val) {
val = $.trim(val || "");
if (!val || currentValues().indexOf(val) !== -1) {
return;
}
var $tag = $("<span class='dsb-tag'></span>").text(val).data("value", val);
var $remove = $("<button type='button' class='dsb-tag-remove' title='Jê bibe'>×</button>");
$remove.on("click", function () {
$tag.remove();
$input.trigger("focus");
});
$tag.append($remove);
$tag.insertBefore($input);
}
$input.on("keydown", function (e) {
if (e.which === 13 || e.which === 188) { // Enter or comma
e.preventDefault();
addTag($input.val());
$input.val("");
} else if (e.which === 8 && !$input.val()) {
$element.children(".dsb-tag").last().remove();
}
});
$input.on("blur", function () {
addTag($input.val());
$input.val("");
});
return {
$element: $element,
// Replaces every tag, for prefilling from what the sense already has.
setValues: function (vals) {
$element.children(".dsb-tag").remove();
vals.forEach(function (v) {
addTag(v);
});
$input.val("");
},
values: function () {
addTag($input.val());
$input.val("");
return currentValues();
},
focus: function () {
$input.trigger("focus");
}
};
}
// The wizard's own etymology templates, straight out of data-main.json's "etymTpls" -- the same list start.js's
// insert menu offers, so the two can never drift apart. CODE/SOURCELANG are its placeholders.
// The wizard's own template lists, straight out of data-main.json, so both gadgets offer the same
// templates in the same order. A pronunciation template names the languages it is for; one that names
// none is offered everywhere.
function templateChoices(pool, language) {
var defs = startData[pool] || {};
return Object.keys(defs).filter(function (name) {
var langs = defs[name].langs;
return !langs || !langs.length || $.inArray(language, langs) !== -1;
}).map(function (name) {
return { name: name, desc: defs[name].desc, snippet: defs[name].snippet };
});
}
// One template call inside the surface: shown as its own wikitext, but atomic -- contenteditable="false"
// keeps the caret out of it, so it can only be changed through the dialog.
function makeTemplateChip(wikitext) {
// The × is its own element rather than the chip's only way out: on a phone there is no reliable
// backspace onto a contenteditable="false" span, so without it the dialog is the only way to take
// a call off the line. readChipSurface() reads chips from data-wikitext, so the extra child does
// not reach the saved text.
var $remove = $("<span class='dsb-tpl-chip-remove' contenteditable='false'>×</span>")
.attr("title", "Vê şablonê jê bibe");
return $("<span class='dsb-tpl-chip' contenteditable='false' role='button' tabindex='0'></span>")
.attr("data-wikitext", wikitext)
.attr("title", "Bitikîne da ku parametreyan biguhêrî")
.text(wikitext)
.append($remove);
}
// wikitext -> surface DOM: template calls become chips, everything else stays plain editable text.
function renderChipSurface($surface, wikitext) {
var text = wikitext || "";
var calls = parseTemplateCalls(text).calls;
var pos = 0;
$surface.empty();
calls.forEach(function (call) {
if (call.start > pos) {
$surface.append(document.createTextNode(text.slice(pos, call.start)));
}
$surface.append(makeTemplateChip(text.slice(call.start, call.end)));
pos = call.end;
});
if (pos < text.length) {
$surface.append(document.createTextNode(text.slice(pos)));
}
}
// surface DOM -> wikitext. Chips give back their stored call; anything the browser inserted while typing
// (a <br>, a wrapper <div>) degrades to its text, so the result is always plain wikitext.
function readChipSurface(node) {
var out = "";
$(node).contents().each(function () {
if (this.nodeType === 3) {
out += this.nodeValue;
return;
}
var $el = $(this);
if ($el.hasClass("dsb-tpl-chip")) {
out += $el.attr("data-wikitext");
return;
}
if (this.nodeName === "BR") {
out += "\n";
return;
}
if (this.nodeName === "DIV" || this.nodeName === "P") {
out += "\n" + readChipSurface(this);
return;
}
out += readChipSurface(this);
});
return out;
}
// Drops a node in at the caret, or at the end when the caret is not inside the surface.
function insertAtCaret($surface, node) {
var sel = window.getSelection();
if (sel && sel.rangeCount && $.contains($surface.get(0), sel.getRangeAt(0).commonAncestorContainer)) {
var range = sel.getRangeAt(0);
range.deleteContents();
range.insertNode(node);
range.setStartAfter(node);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
} else {
$surface.append(node);
}
}
// The template-param dialog (openTemplateDialog/buildTemplateParamForm) moved to wikt.cite, shared with
// start.js's own Etîmolojî and Çavkanî pickers: cite.openTemplateDialog / cite.buildTemplateParamForm.
// Each new example queued is its own entry, so several can be waiting at once.
var exampleSeq = 0;
// Takes the save over from add-examples while its form is open for one sense: its "Qeyd bike" becomes
// "Lê zêde bike" and the edit joins the pending queue instead of publishing on its own, so examples and
// whatever else is queued go up as one edit. Installed on every open, so the handler always closes over
// the sense whose form is actually open. ctx: {$li, $h3, language, sectionType, sectionOrdinal,
// definitionLevel, $save, $status, afterQueue}.
function takeOverExampleSave(ctx) {
if (!(wikt.addExamples && wikt.addExamples.setSubmitHandler)) {
return; // older add-examples: it publishes on its own, as before
}
// Lets the form read an example as the queue has already changed it.
wikt.addExamples.pendingContent = queuedContent;
wikt.addExamples.setSubmitHandler(function (job) {
exampleSeq++;
queueChange(
placeLabel(ctx.language, ctx.sectionType, ctx.$h3),
// add-examples' own summary, so the combined edit still credits the tool and names its
// version. The trailing stop goes: this is one item in a list, not a sentence of its own.
(job.summary || "nimûne/jêgirtin hate zêdekirin").replace(/\.\s*$/, ""),
function (pageContent) {
// add-examples builds against a revision object, and returns {text, summary}.
var built = job.transform({ content: pageContent });
return { content: built.text, warnings: [] };
},
ctx.$save,
ctx.$status,
// Drawn like any other queued change, in the sense it belongs to.
function () {
return previewSense(ctx.$li, ctx.language, ctx.sectionType, ctx.sectionOrdinal, ctx.definitionLevel);
},
// An edit to an example already on the page brings its own key, so editing it again replaces it.
{ key: job.key || ("example:" + ctx.sectionType + "|" + ctx.sectionOrdinal + "|" +
ctx.definitionLevel.join(".") + "|" + exampleSeq) }
);
job.done();
if (ctx.afterQueue) {
ctx.afterQueue();
}
});
}
// Opens add-examples' form on one example already under a sense -- dsb.js's pencil on it. Its save joins
// the queue like any other change.
function editExample($li, $h3, language, sectionType, sectionOrdinal, definitionLevel, itemEl) {
takeOverExampleSave({ $li: $li, $h3: $h3, language: language, sectionType: sectionType,
sectionOrdinal: sectionOrdinal, definitionLevel: definitionLevel, $save: $(), $status: $(),
afterQueue: openQueueBox });
if (!(wikt.addExamples && wikt.addExamples.editFor && wikt.addExamples.editFor($li.get(0), itemEl))) {
mw.notify("Amûra nimûneyan li vê maneyê nehat dîtin.");
}
}
// Builds the whole "guhartina pêşketî" panel into $quickRow: gloss and the two most-used word lists
// (Hevmane/Dijmane) sit directly in the picker row; the remaining word lists, maneid, etîket, kategorî
// and Nimûne/jêgirtin live behind a "Hevmane û Bêhtir" dropdown; one shared "Lê zêde bike" queues everything the
// user filled in as a single edit via applyAdvancedChanges() and queueChange(). Nimûne/jêgirtin is an
// immediate proxy-click into a different gadget's own form, not part of this combined save.
function buildAdvancedPanel($quickRow, $li, $h3, language, sectionType, sectionOrdinal, definitionLevel, isLastSense) {
// "+" marks the actions that add something, matching the dropdown's own "+ Hevmane" labels.
var $nimune = $("<a href='#' class='dsb-quick-link'></a>").text("+ Nimûne/jêgirtin");
$nimune.on("click", function (e) {
e.preventDefault();
takeOverExampleSave({ $li: $li, $h3: $h3, language: language, sectionType: sectionType,
sectionOrdinal: sectionOrdinal, definitionLevel: definitionLevel, $save: $save, $status: $status });
// Toggle, so clicking this a second time closes the form again. add-examples builds nothing
// on the page of its own, so this is the only way its form opens.
var drive = wikt.addExamples.toggleFor || wikt.addExamples.openFor;
if (!(drive && drive($li.get(0)))) {
mw.notify("Amûra nimûneyan li vê maneyê nehat dîtin.");
}
$moreMenu.slideUp();
});
// getters holds one function per currently-open box (key -> () => value); collectChanges() only ever
// reads from a box the user actually clicked to open.
var getters = {};
// What the sense currently holds, fetched once and shared by every box that prefills from it.
// collectChanges() also diffs against it, so an untouched box never queues a change.
var original = null;
var sensePromise = null;
function loadSense() {
if (!sensePromise) {
sensePromise = fetchSenseFields(language, sectionType, sectionOrdinal, definitionLevel).then(function (fields) {
original = fields;
return fields;
});
}
return sensePromise;
}
function sameList(a, b) {
return (a || []).join("\u0000") === (b || []).join("\u0000");
}
// -- gloss ("Maneyê biguhêre" -- the one box that EDITS the sense's wording; everything else here only ADDS) --
function addGlossField() {
var $fields = $("<div class='dsb-gloss-fields'></div>");
var $textarea = $("<textarea class='dsb-input dsb-edit-gloss' rows='2'></textarea>")
.attr("placeholder", util.msg.loading).prop("disabled", true);
var $autoLink = $("<button type='button' class='dsb-autolink'>Oto-lînk bike</button>").prop("disabled", true);
var $toRaw = $("<a href='#' class='dsb-quick-link dsb-gloss-raw'>Wekî wîkîtekst biguhêre</a>").hide();
var $status = $("<span class='dsb-status'></span>");
var originalGloss = null;
var tplForm = null; // set while the parameter boxes are showing instead of the raw line
var tplCall = null;
// The gloss is the definition followed by {{îzah|…}}. That is one field on the page, so it stays
// one box here, and the two are joined back into a single gloss on save. A non-Kurdish gloss is a
// translation and its explanation, so both boxes open together. A Kurdish sense is its own
// definition: its îzah box opens when the gloss already carries one, or from "+ Îzah".
var offerIzah = language !== "ku";
var $izah = $("<textarea class='dsb-input dsb-edit-izah' rows='2'></textarea>")
.attr("placeholder", offerIzah ? "Îzaha maneyî… ji bo school-a îngilîzî 'cihê tehsîlgirtinê'" : "Îzaha maneyî…");
var $izahWrap = $("<div class='dsb-izah-wrap'></div>")
.append($("<label class='dsb-adv-label'>Îzaha maneyî</label>").append($izah))
.toggle(offerIzah);
var $addIzah = $("<a href='#' class='dsb-quick-link dsb-gloss-add-izah'>+ Îzah</a>").toggle(!offerIzah);
$addIzah.on("click", function (e) {
e.preventDefault();
$addIzah.hide();
$izahWrap.show();
$izah.trigger("focus");
});
$fields.append($textarea);
// The template form edits one call inside the line, so its result is spliced back into the rest.
function currentGloss() {
if (tplForm) {
return $.trim(originalGloss.slice(0, tplCall.start) + tplForm.values() + originalGloss.slice(tplCall.end));
}
return joinGloss($.trim($textarea.val()), $.trim($izah.val()));
}
function showRaw() {
tplForm = null;
tplCall = null;
$fields.empty().append($textarea);
$autoLink.show();
$toRaw.hide();
$addIzah.toggle($izahWrap.css("display") === "none");
}
loadSense().then(
function (fields) {
originalGloss = fields.gloss;
// An existing gloss that already ends in {{îzah|…}} comes apart into the two boxes, so
// editing it does not mean editing template syntax by hand.
var parts = util.splitTercimeIzah(fields.gloss);
$textarea.val(parts ? $.trim(parts.tercime) : fields.gloss)
.prop("disabled", false)
// A non-Kurdish gloss is a translation; the box under it takes the explanation.
.attr("placeholder", offerIzah ? util.msg.tercimePrompt : "Maneyê binivîse…");
if (parts) {
$izah.val($.trim(parts.izah));
$izahWrap.show();
$addIzah.hide();
}
$autoLink.prop("disabled", false);
var target = glossTemplateTarget(fields.gloss);
if (!target) {
return;
}
fetchTemplateData(target.name).then(function (td) {
// The fetch is async; leave the raw box alone if it has already been typed into.
if ($.trim($textarea.val()) !== $.trim(originalGloss)) {
return;
}
tplCall = target;
tplForm = cite.buildTemplateParamForm(target, td);
$fields.empty().append(tplForm.$element);
$autoLink.hide(); // linkifying a bare template call makes no sense
// A gloss that is one template call carries no separate explanation.
$izahWrap.hide();
$addIzah.hide();
$toRaw.show();
});
},
function (codeOrAbort, result) {
$textarea.attr("placeholder", "");
var message = codeOrAbort instanceof SubmitAbort ? codeOrAbort.message : "Nikare maneya heyî bar bike: " + apiErrorMessage(codeOrAbort, result);
$status.removeClass("dsb-status-ok").addClass("dsb-status-error").text(message);
}
);
$autoLink.on("click", function () {
var mane = $.trim($textarea.val());
var izah = $.trim($izah.val());
if (!mane && !izah) {
return;
}
$autoLink.prop("disabled", true);
$status.removeClass("dsb-status-ok dsb-status-error").text("Tê lînkkirin…");
linkifyGlossParts(mane, izah, function (linkedMane, linkedIzah, error) {
$textarea.val(linkedMane);
$izah.val(linkedIzah);
$autoLink.prop("disabled", false);
if (error) {
$status.addClass("dsb-status-error").text(error);
} else {
$status.text("");
}
});
});
$toRaw.on("click", function (e) {
e.preventDefault();
$textarea.val(currentGloss()); // read the form before showRaw() discards it
showRaw();
});
getters.gloss = function () {
var v = currentGloss();
return (v && v !== originalGloss) ? v : null;
};
return $("<div class='dsb-adv-group'></div>").append(
$("<div class='dsb-adv-label'></div>").text("Mane"),
$fields,
$izahWrap,
$("<div class='dsb-form-actions'></div>").append($autoLink, $addIzah, $toRaw, $status)
);
}
// -- one SENSE_GROUPS word-list box (Hevmane/Dijmane/etc.) --
function addWordField(group) {
var field = buildWordListField(group.label + "…");
loadSense().then(function (fields) {
var current = fields.words[group.key];
if (current && current.length) {
field.setValues(current);
}
});
getters["word:" + group.key] = function () {
return field.values();
};
// The hint is senseGroups' own, so the box carries the same example sentence the wizard puts
// beside its copy of this list.
var $label = $("<div class='dsb-adv-label'></div>").text(group.label);
if (group.hint) {
$label.append($("<span class='dsb-adv-hint'></span>").text(group.hint));
}
return $("<div class='dsb-adv-group'></div>").append($label, field.$element);
}
// -- maneid (single value; re-adding one that already exists is rejected -- see applyAdvancedChanges) --
function addNewSenseField() {
var $textarea = $("<textarea class='dsb-input' rows='2'></textarea>").attr("placeholder", "Maneya nû binivîse…");
getters.newSense = function () {
return $.trim($textarea.val());
};
return $("<div class='dsb-adv-group'></div>").append(
$("<div class='dsb-adv-label'></div>").text(util.msg.anotherSense),
$textarea
);
}
function addManeidField() {
var $input = $("<input type='text' class='dsb-input' placeholder='id…'>");
loadSense().then(function (fields) {
$input.val(fields.maneid);
});
getters.maneid = function () {
return $.trim($input.val());
};
return $("<div class='dsb-adv-group'></div>").append($("<div class='dsb-adv-label'></div>").text("Maneid"), $input);
}
// -- etîket / kategorî --
function addEtiketField() {
var field = buildTagField("Etîket…");
loadSense().then(function (fields) {
field.setValues(fields.etiket);
});
getters.etiket = function () {
return field.values();
};
return $("<div class='dsb-adv-group'></div>").append($("<div class='dsb-adv-label'></div>").text("Etîket"), field.$element);
}
function addKategoriField() {
var field = buildTagField("Kategorî…");
loadSense().then(function (fields) {
field.setValues(fields.kategori);
});
getters.kategori = function () {
return field.values();
};
return $("<div class='dsb-adv-group'></div>").append($("<div class='dsb-adv-label'></div>").text("Kategorî"), field.$element);
}
// -- the picker row: the gloss and Nimûne/jêgirtin links sit directly in the row; every word group and
// the rest live behind a "Hevmane û Bêhtir" dropdown. Clicking any link builds and appends its box
// to $openBoxes and hides that link; the box's own "×" removes it, clears its getter, and un-hides the
// link -- several boxes can be open at once. No "Beşek (====)" entry here: buildSubsectionPanel()'s
// own "+ Beşek lê zêde bike" trigger already covers page-level subsections. ----
var FIELD_DEFS = [{ key: "gloss", label: "Maneyê biguhêre", build: addGlossField, primary: true }]
.concat(SENSE_GROUPS.map(function (group) {
return { key: "word:" + group.key, label: group.label, build: function () { return addWordField(group); }, primary: false };
}))
.concat([
{ key: "maneid", label: "Maneid", build: addManeidField, primary: false },
{ key: "etiket", label: "Etîket", build: addEtiketField, primary: false },
{ key: "kategori", label: "Kategorî", build: addKategoriField, primary: false }
]);
// Every sense offers this: the new one lands directly below whichever sense asked for it (see
// insertNewSense), so it is no longer only the last one that can add to the end.
FIELD_DEFS.push({ key: "newSense", label: util.msg.addAnotherSense, build: addNewSenseField, primary: false });
var $picker = $("<div class='dsb-field-picker'></div>");
var $moreMenu = $("<div class='dsb-more-menu'></div>").hide();
var $openBoxes = $("<div class='dsb-open-boxes'></div>");
// $actions (built below) stays hidden until at least one field box is actually open -- otherwise
// "Lê zêde bike" sits there enabled with nothing to queue, which just produces an empty no-op change.
var openBoxCount = 0;
function updateActionsVisibility() {
$actions.toggle(openBoxCount > 0);
}
function makeFieldLink(def) {
var $link = $("<a href='#' class='dsb-quick-link dsb-field-pick'></a>").text(def.key === "gloss" ? def.label : "+ " + def.label);
$link.on("click", function (e) {
e.preventDefault();
openQueueBox();
var $box = def.build();
var $remove = $("<a href='#' class='dsb-box-remove' title='Rake'>×</a>");
$remove.on("click", function (e2) {
e2.preventDefault();
delete getters[def.key];
$box.remove();
$link.show();
openBoxCount--;
updateActionsVisibility();
});
$box.prepend($remove);
$openBoxes.append($box);
util.initManualResize($box);
$link.hide();
$moreMenu.slideUp();
openBoxCount++;
updateActionsVisibility();
});
return $link;
}
// Destructive, so it lives at the bottom of the dropdown behind a confirmation, and shares the
// sense's queue key: deleting supersedes any edit still pending on the same sense.
function makeDeleteLink() {
var $link = $("<a href='#' class='dsb-quick-link dsb-field-delete'>Maneyê rake</a>");
$link.on("click", function (e) {
e.preventDefault();
$moreMenu.slideUp();
OO.ui.confirm("Ev mane û hemû rêzên bin wê dê werin rakirin. Dewam bike?").done(function (ok) {
if (!ok) {
return;
}
queueChange(
placeLabel(language, sectionType, $h3),
"maneyek hate rakirin",
function (baseContent) {
return removeSense(baseContent, language, sectionType, sectionOrdinal, definitionLevel);
},
$save,
$note,
function () {
$li.addClass("dsb-sense-doomed");
return function () {
$li.removeClass("dsb-sense-doomed");
};
},
{ key: senseQueueKey() }
);
$note.show();
});
});
return $link;
}
// Only the links the row actually shows are built now; the dropdown's nine are 43% of the work and
// nobody sees them until "Hevmane û Bêhtir" is clicked, so buildMenuLinks() waits for that.
FIELD_DEFS.forEach(function (def) {
if (!def.primary) {
return;
}
$picker.append(makeFieldLink(def));
if (def.key === "gloss") {
$picker.append($nimune); // directly after "Maneyê biguhêre"
}
});
// Three sections, ruled apart: what labels this sense, the word lists that hang off it, and adding
// another sense. "Maneyê rake" is appended after these by makeDeleteLink() and so closes the last
// one. The word lists follow senseGroups' "order" -- that array's own order is the order the "#:"
// lines are written to the page in, which is not a reading order; the wizard's boxes read the same
// field, so both gadgets list them alike. Anything not named here still appears, after these, so a
// field added later is never silently dropped from the menu.
var MENU_GROUPS = [
["etiket", "kategori", "maneid"],
SENSE_GROUPS.slice().sort(function (a, b) {
return (a.order || 0) - (b.order || 0);
}).map(function (g) {
return "word:" + g.key;
}),
["newSense"]
];
var menuBuilt = false;
function buildMenuLinks() {
if (menuBuilt) {
return;
}
menuBuilt = true;
var byKey = {};
FIELD_DEFS.forEach(function (def) {
if (!def.primary) {
byKey[def.key] = def;
}
});
var placed = {};
MENU_GROUPS.forEach(function (group, groupIndex) {
var defs = group.map(function (key) {
return byKey[key];
}).filter(Boolean);
defs.forEach(function (def, i) {
placed[def.key] = true;
var $link = makeFieldLink(def);
// The rule goes on the first entry of every group after the first.
if (groupIndex > 0 && i === 0) {
$link.addClass("dsb-menu-group-start");
}
$moreMenu.append($link);
});
});
FIELD_DEFS.forEach(function (def) {
if (!def.primary && !placed[def.key]) {
$moreMenu.append(makeFieldLink(def));
}
});
$moreMenu.append(makeDeleteLink());
}
// Lives inside $picker (not appended straight to $quickRow) so .dsb-more-menu's CSS can position it as
// an overlay anchored to $picker, instead of pushing $openBoxes/the rest of the page down when it opens.
var $moreArrow = $("<span class='dsb-more-arrow'></span>").text("▸");
var $moreToggle = $("<a href='#' class='dsb-quick-link dsb-more-toggle'></a>")
.text(util.msg.moreMenu + " ").append($moreArrow);
$moreToggle.on("click", function (e) {
e.preventDefault();
buildMenuLinks();
$moreMenu.slideToggle(function () {
$moreArrow.text($moreMenu.is(":visible") ? "▾" : "▸");
});
});
$picker.append($moreToggle, $moreMenu);
// Outside clicks are handled by one shared document listener (see closeMoreMenus), not one per panel:
// hovering a sense builds its panel, so a long entry would otherwise accumulate dozens of them.
$moreMenu.data("dsbArrow", $moreArrow);
// -- combined save: whatever boxes are open right now, queued together via queueChange(); the actual
// save happens when the user clicks "Guhartinan qeyd bike" on the floating queue box. ----
// Small "not saved yet" preview of everything this queued change will do, appended right under the
// sense's gloss (plain text, not a real render of {{hevmane}}/etc.).
function collectChanges() {
var base = original || { maneid: "", etiket: [], kategori: [], words: {} };
var changed = {};
var words = {};
SENSE_GROUPS.forEach(function (group) {
var getter = getters["word:" + group.key];
words[group.key] = getter ? getter() : [];
if (getter && !sameList(words[group.key], base.words[group.key])) {
changed["word:" + group.key] = true;
}
});
var maneid = getters.maneid ? getters.maneid() : "";
var etiket = getters.etiket ? getters.etiket() : [];
var kategori = getters.kategori ? getters.kategori() : [];
if (getters.maneid && maneid !== (base.maneid || "")) {
changed.maneid = true;
}
if (getters.etiket && !sameList(etiket, base.etiket)) {
changed.etiket = true;
}
if (getters.kategori && !sameList(kategori, base.kategori)) {
changed.kategori = true;
}
return {
gloss: getters.gloss ? getters.gloss() : null,
newSense: getters.newSense ? getters.newSense() : null,
maneid: maneid,
etiket: etiket,
kategori: kategori,
words: words,
changed: changed
};
}
// Both the field save and "Maneyê rake" queue under this, so the later one supersedes the earlier.
function senseQueueKey() {
return "sense:" + sectionType + "|" + sectionOrdinal + "|" + definitionLevel.join(".");
}
var $save = $("<button type='button' class='dsb-save'></button>").text(util.msg.add);
// Where queueChange() writes its confirmation for actions that have no box of their own.
var $note = $("<div class='dsb-status dsb-quick-note'></div>").hide();
var $cancel = $("<button type='button' class='dsb-cancel'></button>").text(util.msg.cancel);
var $status = $("<span class='dsb-status'></span>");
var $actions = $("<div class='dsb-form-actions'></div>").append($save, $cancel, $status).hide();
$cancel.on("click", function () {
// Close the boxes too, not just the row. Hiding alone left every field still open with its
// text in it, so the next click on the pencil reopened the half-filled panel that had just
// been cancelled. Each box's own "x" already clears its getter, restores its link and updates
// the counter, so those are triggered rather than repeating the teardown here.
$openBoxes.find(".dsb-box-remove").trigger("click");
$note.hide().text("");
// dsb.js owns the pencil's pinned state; tell it rather than hiding behind its back, or the
// next click on the pencil would only toggle "pinned" off and appear to do nothing.
$quickRow.trigger("dsbCancel");
hideQueueIfIdle();
});
$save.on("click", function () {
var changes = collectChanges();
function afterLinkify() {
var parts = senseChangeLabels(changes);
queueChange(
placeLabel(language, sectionType, $h3),
parts.length
? parts.join(", ") + (parts.length > 1 ? " hatin guhartin" : " hate guhartin")
: "mane hate guhartin",
function (baseContent) {
return applyAdvancedChanges(baseContent, language, sectionType, sectionOrdinal, definitionLevel, changes);
},
$save,
$status,
function () {
var undoPreview = previewSense($li, language, sectionType, sectionOrdinal,
definitionLevel);
// A new sense has no place on the page yet, so it gets a row of its own directly
// after this one -- the list numbers it, so where it will land is visible.
var $newSense = null;
if (changes.newSense) {
$newSense = $("<li class='dsb-sense-preview'></li>");
$li.after($newSense);
fillWithRendered($newSense, changes.newSense, function () {
$newSense.text(changes.newSense);
});
}
return function () {
undoPreview();
if ($newSense) {
$newSense.remove();
}
};
},
// Re-saving this same sense supersedes its pending change rather than queuing another.
{ key: senseQueueKey() }
);
}
$save.prop("disabled", true);
// Auto-linkify the gloss as part of this save, but only a half nobody has linked by hand.
// Split back into its two halves first, so the explanation is linked on its own rather than
// sitting unseen inside {{îzah|…}}.
if (changes.gloss) {
$status.removeClass("dsb-status-ok dsb-status-error").text("Tê lînkkirin…");
var glossParts = util.splitTercimeIzah(changes.gloss);
linkifyGlossParts(
glossParts ? $.trim(glossParts.tercime) : changes.gloss,
glossParts ? $.trim(glossParts.izah) : "",
function (linkedMane, linkedIzah, error) {
// The change is queued either way, so a gloss that came back empty is not written
// back -- that would save the sense with its wording gone.
var joined = joinGloss(linkedMane, linkedIzah);
if ($.trim(joined)) {
changes.gloss = joined;
}
if (error) {
$status.removeClass("dsb-status-ok").addClass("dsb-status-error").text(error);
}
afterLinkify();
},
true
);
} else {
afterLinkify();
}
});
// The links sit under the buttons, so each open box pushes them down rather than leaving them far above.
$quickRow.empty().append($openBoxes, $actions, $picker, $note);
}
// Attaches the pencil toggle to one leaf definition <li> -- clicking it reveals the "guhartina pêşketî"
// (advanced editing) panel built by buildAdvancedPanel() above.
// ---- per-section UI: "+ Beşek lê zêde bike" after a POS block's own content ----
// Reads the header text of "====" subsections already rendered under this POS heading, straight from the DOM.
// The subsections of a part of speech are the headings one deeper than it is.
function subsectionDepthFor($h3) {
return (sections.headingDepth($h3) || 3) + 1;
}
function isSubsectionHeading($el, depth) {
return sections.headingDepth($el) === depth;
}
function existingSubsectionHeadersInDom($h3) {
var depth = subsectionDepthFor($h3);
var headers = [];
for (var $cursor = headingOuter($h3).next(); $cursor.length; $cursor = $cursor.next()) {
var here = sections.headingDepth($cursor);
if (here && here < depth) {
break; // out of this part of speech
}
if (isSubsectionHeading($cursor, depth)) {
var $hh = $cursor.is("h2, h3, h4, h5, h6") ? $cursor : $cursor.find("h2, h3, h4, h5, h6").first();
headers.push(headingText($hh));
}
}
return headers;
}
// Finds where a new "====" subsection preview belongs in the live DOM, using the same SUBSECTION_ORDER rank
// insertPageSubsection() uses. Returns {insertBefore: $el} (a ranked subsection that outranks this one) or
// {insertAfter: $el} (goes at the end of the block).
function domSubsectionInsertAnchor($h3, header) {
var depth = subsectionDepthFor($h3);
var myRank = subsectionRankForHeader(header);
var $last = headingOuter($h3);
for (var $cursor = headingOuter($h3).next(); $cursor.length; $cursor = $cursor.next()) {
var here = sections.headingDepth($cursor);
if (here && here < depth) {
break; // out of this part of speech
}
if (isSubsectionHeading($cursor, depth)) {
var $hh = $cursor.is("h2, h3, h4, h5, h6") ? $cursor : $cursor.find("h2, h3, h4, h5, h6").first();
var otherRank = subsectionRankForHeader(headingText($hh));
if (otherRank !== -1 && otherRank > myRank) {
return { insertBefore: $cursor };
}
}
$last = $cursor;
}
return { insertAfter: $last };
}
// The wikitext one subsection's content becomes, exactly as insertPageSubsection() writes it -- so the
// preview parses the same string the save will produce.
function subsectionBodyWikitext(language, s, content) {
if (s.kind !== "wordlist") {
return content;
}
return content.filter(function (group) {
return group.words.length;
}).map(function (group) {
return buildWordListCall(WORDLIST_TEMPLATE, language,
withNamedParam(["cure=" + s.header], "sernav", group.sernav), group.words);
}).join("\n\n");
}
// The plain word list this preview falls back to when the parse fails: the values that were typed,
// without the template around them. Right, but not what the page will look like.
function subsectionPlainDom($wrap, s, content) {
if (s.kind !== "wordlist") {
$wrap.append($("<p class='dsb-preview-text'></p>").text(content));
return;
}
content.forEach(function (group) {
if (group.sernav) {
$wrap.append($("<div class='dsb-preview-subheading'></div>").text(group.sernav));
}
var $list = $("<ul class='dsb-preview-list'></ul>");
group.words.forEach(function (w) {
$list.append($("<li></li>").text(w.replace(/<[^<>]*>/g, "")));
});
$wrap.append($list);
});
}
// A "not saved yet" preview of a page-level "====" subsection, dropped into the live page at the
// position the save will put it, with its body rendered the way the page renders it -- real {{kol3}}
// columns and real links, not a list of the words that went into them. Returns the inserted element so
// the caller can remove it again.
function buildSubsectionPreviewDom($h3, s, content, language) {
var $wrap = $("<div class='dsb-preview-block'></div>");
// A styled <div>, not a real <h4>, so existingSubsectionHeadersInDom() never mistakes this live-only
// preview for an actual subsection already on the page.
$wrap.append($("<div class='dsb-preview-heading' role='heading' aria-level='4'></div>").text(s.header));
$wrap.append($("<span class='dsb-preview-badge'></span>").text("nû — hîn nehatiye qeydkirin"));
// Placed before it is filled: a cached render resolves almost at once, and filling a wrap that is
// still detached would announce content nobody can see.
var anchor = domSubsectionInsertAnchor($h3, s.header);
if (anchor.insertBefore) {
anchor.insertBefore.before($wrap);
} else {
anchor.insertAfter.after($wrap);
}
var wikitext = language ? subsectionBodyWikitext(language, s, content) : "";
if ($.trim(wikitext)) {
var $body = $("<div class='dsb-preview-rendered'></div>").appendTo($wrap);
fillWithRendered($body, wikitext, function () {
$body.remove();
subsectionPlainDom($wrap, s, content);
});
} else {
subsectionPlainDom($wrap, s, content);
}
return $wrap;
}
// A free section owns what lies between its own heading and the next heading of any depth, matching
// the range locateHeadingByText() edits. Returns a function that puts the page back, or null.
function replaceFreeSectionInPlace($heading, wikitext) {
var $start = headingOuter($heading);
var body = [];
for (var $cursor = $start.next(); $cursor.length; $cursor = $cursor.next()) {
if (sections.headingDepth($cursor)) {
break;
}
if (!$cursor.is(".dsb-subsection-panel, .dsb-convert-pos-panel, .dsb-preview-block, .dsb-sense-preview")) {
body.push($cursor.get(0));
}
}
if (!body.length) {
return null;
}
var $slot = $("<div class='dsb-sense-preview'></div>").insertAfter($start);
var $body = $(body).detach();
fillWithRendered($slot, wikitext, function () {
$slot.remove();
$slot = null;
$body.insertAfter($start);
});
return function () {
if ($slot) {
$body.insertAfter($start);
$slot.remove();
$slot = null;
}
};
}
/**
* Redraws a subsection that is already on the page with the content this edit would give it.
*
* The section's own nodes -- everything between its heading and the next heading of the same rank or
* higher -- are detached rather than dropped, so the returned function puts the page back exactly as it
* was when the change is cancelled or superseded.
*/
function replaceSubsectionInPlace($h3, header, wikitext) {
var depth = subsectionDepthFor($h3);
// Walked the same way existingSubsectionHeadersInDom() walks it, so "which nodes are this
// subsection" is decided once and the same way everywhere.
var $start = null;
var body = [];
for (var $cursor = headingOuter($h3).next(); $cursor.length; $cursor = $cursor.next()) {
var here = sections.headingDepth($cursor);
if (here && here < depth) {
break; // out of this part of speech
}
if (isSubsectionHeading($cursor, depth)) {
if ($start) {
break; // the next subsection starts here
}
var $hh = $cursor.is("h2, h3, h4, h5, h6") ? $cursor : $cursor.find("h2, h3, h4, h5, h6").first();
if (headingText($hh) === header) {
$start = $cursor;
}
continue;
}
// This gadget's own furniture stays where it is: the subsection's editing panel is the sibling
// right after its heading, so sweeping it into the body would detach the very panel the change
// was made in and leave the pencil toggling an element that is no longer on the page.
if ($start && !$cursor.is(".dsb-subsection-panel, .dsb-convert-pos-panel, .dsb-preview-block, .dsb-sense-preview")) {
body.push($cursor.get(0));
}
}
if (!$start || !body.length) {
return null;
}
var $slot = $("<div class='dsb-sense-preview'></div>").insertAfter($start);
var $body = $(body).detach();
fillWithRendered($slot, wikitext, function () {
$slot.remove();
$slot = null;
$body.insertAfter($start);
});
return function () {
if ($slot) {
$body.insertAfter($start);
$slot.remove();
$slot = null;
}
};
}
/**
* The live DOM one "place" section occupies: {nodes, insert}. nodes are what a render replaces --
* empty when the section is not on the page yet -- and insert() puts an element where the section
* belongs, so a new one appears exactly where the save will put it.
*/
// This gadget's own panels sit between a heading and the page's content, so every walk steps over
// them rather than counting one as content.
function isOwnFurniture($el) {
return $el.is(".dsb-toolbar, .dsb-quick-row, .dsb-subsection-panel, .dsb-convert-pos-panel," +
" .dsb-preview-block, .dsb-sense-preview");
}
function loosePlaceDom($h3, subsection) {
var place = subsection.place;
var nodes = [];
var $cursor;
if (place === "page-top") {
// Şablon:herwiha binêre renders as <div class="rellink binere"> at the head of the content.
var $out = $("#mw-content-text .mw-parser-output").first();
var $hat = $out.children(".binere, .rellink").first();
if ($hat.length) {
return { nodes: $hat.get(), insert: function ($el) { $hat.before($el); } };
}
return { nodes: [], insert: function ($el) { $out.prepend($el); } };
}
var $start = headingOuter($h3);
if (place === "pron") {
// A "===" heading above this POS block, under the language one.
for (var $back = $start.prev(); $back.length; $back = $back.prev()) {
if ($back.is(".mw-heading2, h2")) {
break;
}
if ($back.is(".mw-heading3, h3")) {
var $hh = $back.is("h3") ? $back : $back.find("h3").first();
if (headingText($hh) === subsection.header) {
for ($cursor = $back.next();
$cursor.length && !$cursor.is(".mw-heading2, .mw-heading3, h2, h3");
$cursor = $cursor.next()) {
if (!isOwnFurniture($cursor)) {
nodes.push($cursor.get(0));
}
}
return { nodes: nodes, insert: function ($el) { $back.after($el); } };
}
}
}
return { nodes: [], insert: function ($el) { $start.before($el); } };
}
if (place === "pos-top") {
// The thumbnails between the heading and the headword line.
for ($cursor = $start.next(); $cursor.length; $cursor = $cursor.next()) {
if (isOwnFurniture($cursor)) {
continue;
}
if (!$cursor.is(".thumb, figure, .floatright, .floatleft")) {
break;
}
nodes.push($cursor.get(0));
}
return { nodes: nodes, insert: function ($el) { $start.after($el); } };
}
return null;
}
// The wikitext a "place" section's pending content renders as. A word list is its one call; everything
// else is already the body that will be written.
function loosePlacePreviewWikitext(subsection, content, language) {
return subsection.kind === "wordlist"
? wt.buildHbnrCall(loosePlaceWords(content))
: loosePlaceBody(subsection, content, language);
}
// Draws a section's headword line from wikitext where the page's own line stands, or above the
// definitions when the section has none. The page's line is detached rather than dropped, so the
// returned function puts it back.
function replaceHeadwordInPlace($h3, wikitext) {
if (!$.trim(wikitext || "")) {
return null;
}
var $old = $();
var $at = $();
for (var $cursor = headingOuter($h3).next(); $cursor.length; $cursor = $cursor.next()) {
if ($cursor.is("p")) {
$old = $cursor;
break;
}
if (sections.headingDepth($cursor) || $cursor.is("ol")) {
$at = $cursor;
break;
}
}
var $slot = $("<div class='dsb-sense-preview'></div>");
if ($old.length) {
$old.before($slot);
} else if ($at.length) {
$at.before($slot);
} else {
return null;
}
var $body = $old.detach();
function restore() {
if (!$slot) {
return;
}
$body.insertBefore($slot);
$slot.remove();
$slot = null;
}
fillWithRendered($slot, wikitext, restore);
return restore;
}
// Redraws one of those places where it stands, the same way replaceSubsectionInPlace() redraws a
// "====" one. The page's own nodes are detached rather than dropped, so cancelling puts them back.
function replaceLoosePlaceInPlace($h3, subsection, wikitext) {
if (!$.trim(wikitext || "")) {
return null;
}
var at = loosePlaceDom($h3, subsection);
if (!at) {
return null;
}
var $slot = $("<div class='dsb-sense-preview'></div>");
at.insert($slot);
var $body = $(at.nodes).detach();
function restore() {
if (!$slot) {
return;
}
$body.insertBefore($slot);
$slot.remove();
$slot = null;
}
fillWithRendered($slot, wikitext, restore);
return restore;
}
// Builds the subsection picker + its word-list/text field. A subsection that isn't on the page yet is saved
// via insertPageSubsection(); one that already is gets prefilled and saved via updatePageSubsection().
// onlyHeader pins the panel to a single subsection (the per-heading pencil); omit it for the full picker.
// free pins the panel to a heading that is not a subsection of a part of speech, located by its own
// text: {header, ordinal}.
function buildSubsectionPanel($panel, $h3, language, sectionType, sectionOrdinal, onlyHeader, free) {
var existing = existingSubsectionHeadersInDom($h3);
// A heading the data does not name is still editable. It is declared a word list so the reader
// tries, in order, its {{kol3}} calls and then a plain bullet list; anything else falls through to
// raw wikitext, which is what prefill() does with a list it cannot read.
// Hidden counts as unknown here: the picker does not offer to build a Werger box, but one already
// on the page still opens, as the wikitext it is.
var unknown = onlyHeader && !PAGE_SUBSECTIONS.some(function (s) {
return s.header === onlyHeader && !s.hidden;
});
// A free section starts as wikitext: what it holds is only known once it has been read, and
// prefill() swaps in the {{kol3}} form there if it turns out to be a list.
var available = free ? [{
header: onlyHeader,
id: "free",
kind: "text",
free: free,
existing: true
}] : unknown ? [{
header: onlyHeader,
id: "unknown",
kind: "wordlist",
unknown: true,
existing: true
}] : PAGE_SUBSECTIONS.filter(function (s) {
// "hidden" takes a section out of the picker without removing it: Werger waits for the
// translation tools it belongs to, and offering a box those cannot yet fill misleads.
// Werger also carries lang: "ku" -- it is a Kurdish-entry section, so it is not offered
// elsewhere either.
return !s.hidden && (!s.lang || s.lang === language) &&
(!onlyHeader || s.header === onlyHeader);
}).map(function (s) {
return $.extend({}, s, { existing: existing.indexOf(s.header) !== -1 });
});
if (!available.length) {
$panel.text("Ji bo vê beşê guhartin tune.");
return;
}
var $select = $("<select class='dsb-subsection-select'></select>");
if (!onlyHeader) {
// Label-less on purpose: it holds the "nothing chosen yet" value without becoming a row in the
// list (makeSearchable() skips a blank option that has no label), so the prompt lives in the
// input's placeholder instead.
$select.append($("<option value=''></option>"));
}
available.forEach(function (s) {
// A "place" entry has no "====" heading of its own, so it is marked rather than left looking
// like the subsections around it.
var label = s.place ? s.header + " (ne binebeş e)"
: (s.existing ? s.header + " (biguhêre)" : s.header);
$select.append($("<option></option>").val(s.id).text(label));
});
var $selectWrap = makeSearchable($select, "Binebeşek hilbijêre");
if (onlyHeader) {
$selectWrap.hide(); // pinned to one subsection: nothing to choose
}
var $fieldWrap = $("<div class='dsb-subsection-field'></div>");
var $save = $("<button type='button' class='dsb-save'></button>").text(util.msg.add);
var $cancel = $("<button type='button' class='dsb-cancel'></button>").text(util.msg.cancel);
var $status = $("<span class='dsb-status'></span>");
var $actions = $("<div class='dsb-form-actions'></div>").append($save, $cancel, $status);
$cancel.on("click", function () {
resetForm();
$panel.hide();
hideQueueIfIdle();
});
// Undefined while the placeholder option is selected.
function currentSubsection() {
var id = $select.val();
return id ? available.filter(function (s) {
return s.id === id;
})[0] : undefined;
}
// One entry per "{{kol3}}" group in the current wordlist section; empty for any other kind.
var groupFields = [];
var $groups = null;
var editAsText = false; // set when an existing wordlist section turns out to be unparseable
// Whatever has been typed into each subsection's box, so switching the picker away and back restores it
// instead of rebuilding an empty field. Keyed by subsection id; lastId is whichever box is on screen.
var drafts = {};
var lastId = null;
// Called before renderField() rebuilds, while the group fields still hold the outgoing box.
function captureDraft() {
if (!lastId) {
return;
}
drafts[lastId] = imagesField
? { asText: false, images: imagesField.values() }
: (groupFields.length
? { asText: false, groups: readGroups() }
: { asText: editAsText, text: fieldText() });
}
function restoreDraft(s, draft) {
// An existing wordlist we previously fell back to raw wikitext for stays in that mode.
if (draft.asText && s.kind === "wordlist") {
editAsText = true;
groupFields = [];
renderTextArea(s, 6);
}
if (imagesField) {
imagesField.setValues(draft.images);
} else if (groupFields.length) {
setGroups(s, draft.groups);
} else {
setFieldText(draft.text);
}
}
// Etîmolojî is edited on a live surface (see renderChipSurface) rather than in a textarea, so every
// read and write of the field goes through these two instead of touching .dsb-subsection-text.
var chipSurface = null;
function fieldText() {
if (wergerForm) {
return buildWergerBlock(wergerForm.read());
}
return chipSurface
? $.trim(readChipSurface(chipSurface.get(0)))
: $.trim($fieldWrap.find(".dsb-subsection-text").val() || "");
}
function setFieldText(value) {
if (wergerForm) {
wergerForm.set(parseWergerBlock(value));
return;
}
if (chipSurface) {
renderChipSurface(chipSurface, value || "");
var schedule = chipSurface.data("dsbSchedulePreview");
if (schedule) {
schedule(); // show what the loaded etymology renders as
}
} else {
$fieldWrap.find(".dsb-subsection-text").val(value || "");
}
}
// The contenteditable surface: text typed directly, template calls as chips that open the dialog,
// plus start.js's etymology templates in a searchable insert menu.
// A live surface where a template shows as a chip, with a menu that inserts one. Etîmolojî and
// Bilêvkirin are both written this way; they differ only in which templates the menu offers.
function renderTemplateEditor(s, templates) {
$fieldWrap.empty();
var $select = $("<select class='dsb-subsection-select'></select>");
$select.append($("<option value=''></option>"));
templates.forEach(function (t) {
$select.append($("<option></option>").val(t.snippet).text(t.name + " — " + t.desc));
});
var $surface = $("<div class='dsb-tpl-surface' contenteditable='true'></div>")
.attr("role", "textbox")
.attr("aria-multiline", "true");
chipSurface = $surface;
// Clicking the insert menu moves focus out of the surface and the browser forgets the caret, so
// the last caret position inside the surface is kept and restored before inserting -- otherwise
// focus() drops it back to the very start and every template lands at the beginning of the line.
var savedRange = null;
function rememberCaret() {
var sel = window.getSelection();
if (!sel || !sel.rangeCount) {
return;
}
var range = sel.getRangeAt(0);
var root = $surface.get(0);
if (range.commonAncestorContainer === root || $.contains(root, range.commonAncestorContainer)) {
savedRange = range.cloneRange();
}
}
$surface.on("keyup mouseup focusout", rememberCaret);
function restoreCaret() {
if (!savedRange) {
return;
}
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(savedRange);
}
$select.on("change", function () {
var snippet = $select.val();
if (!snippet) {
return;
}
// Same substitution start.js makes: CODE is this section's language, SOURCELANG is left for
// the user to fill in.
snippet = snippet.replace(/CODE/g, language).replace(/SOURCELANG/g, "?");
// A pronunciation template is a bullet of its own ("* {{ku-IPA}}"), so it starts a line
// rather than landing in the middle of one.
var ownLine = /^\*\s/.test(snippet);
if (ownLine) {
snippet = $.trim(snippet.replace(/^\*\s*/, ""));
}
$surface.focus();
restoreCaret();
var $chip = makeTemplateChip(snippet);
var piece = document.createDocumentFragment();
if (ownLine) {
// The bullet, and a line of its own unless the caret already starts one.
var at = caretOffset();
var before = at >= 0 ? readChipSurface($surface.get(0)).slice(0, at) : "";
piece.appendChild(document.createTextNode(!before || /\n$/.test(before) ? "* " : "\n* "));
}
piece.appendChild($chip.get(0));
insertAtCaret($surface, piece);
rememberCaret(); // so a second insert lands after the first, not back at the old spot
$select.val("");
// Straight into the dialog: the snippet is placeholders ("?", "…") until someone fills it in.
cite.openTemplateDialog(snippet).then(function (edited) {
applyChipEdit($chip, edited);
syncPanelWidth();
});
});
// Paste as plain text: pasted markup would otherwise become real nodes in the surface.
$surface.on("paste", function (e) {
var clip = e.originalEvent && e.originalEvent.clipboardData;
if (!clip) {
return;
}
e.preventDefault();
document.execCommand("insertText", false, clip.getData("text/plain"));
});
// The chip the dialog was opened for, or the one that has taken its place. renderChipSurface()
// rebuilds the surface from its wikitext whenever a call is typed in as text, which throws away
// the element this closure is holding; without this, "Jê bibe" would remove a node that is no
// longer on screen and appear to do nothing.
function liveChip($chip, wikitext) {
if ($.contains($surface.get(0), $chip.get(0))) {
return $chip;
}
return $surface.find(".dsb-tpl-chip").filter(function () {
return $(this).attr("data-wikitext") === wikitext;
}).first();
}
function applyChipEdit($chip, edited) {
if (edited === null || !$chip.length) {
return;
}
if (!$.trim(edited)) {
$chip.remove();
return;
}
// Edited into something that is no longer one template call: let it fall back to plain text
// rather than keeping a chip that lies about its contents.
var call = parseTemplateCalls(edited);
if (call.otherContent || call.calls.length !== 1) {
$chip.replaceWith(document.createTextNode(edited));
return;
}
$chip.replaceWith(makeTemplateChip(edited));
}
// Double-click edits the call's wikitext in place; single click opens the parameter dialog. The
// click is held back briefly so the first of a double-click does not open the dialog underneath.
var chipClickTimer = null;
var lastChipTap = 0;
var lastChipNode = null;
var DOUBLE_TAP_MS = 300;
// Double-tap drops the chip back to plain wikitext in the surface, rather than making the chip
// itself editable: a contenteditable inside a contenteditable gives Android a selection handle and
// no usable caret. As plain text the browser's own editing works, and chipifyTyped() puts the chip
// back once the caret leaves the call.
// A collapsed caret inside the text just inserted: at the character tapped where the browser can
// say which that is, otherwise just inside the closing braces so the caret stays within the call
// (which is what stops chipifyTyped putting the chip straight back).
function caretRangeIn(node, event) {
var range;
var x = event && event.clientX;
var y = event && event.clientY;
if (x !== undefined && document.caretRangeFromPoint) {
range = document.caretRangeFromPoint(x, y);
} else if (x !== undefined && document.caretPositionFromPoint) {
var pos = document.caretPositionFromPoint(x, y);
if (pos) {
range = document.createRange();
range.setStart(pos.offsetNode, pos.offset);
}
}
if (range && range.startContainer === node) {
range.collapse(true);
return range;
}
var text = node.nodeValue;
range = document.createRange();
range.setStart(node, /\}\}$/.test(text) ? text.length - 2 : text.length);
range.collapse(true);
return range;
}
function startInlineEdit($chip, event) {
if (!$.contains($surface.get(0), $chip.get(0))) {
return; // the tap timer and dblclick can both fire; only the first one has a chip left
}
var node = document.createTextNode($chip.attr("data-wikitext"));
$chip.replaceWith(node);
$surface.get(0).focus();
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(caretRangeIn(node, event));
}
// Bound before the chip's own handler and stopping there, so removing a call never opens the
// dialog for it on the way out.
$surface.on("click", ".dsb-tpl-chip-remove", function (e) {
e.preventDefault();
e.stopImmediatePropagation();
clearTimeout(chipClickTimer);
lastChipNode = null;
$(this).closest(".dsb-tpl-chip").remove();
syncPanelWidth();
});
$surface.on("click keydown", ".dsb-tpl-chip", function (e) {
if (e.type === "keydown" && e.which !== 13 && e.which !== 32) {
return;
}
e.preventDefault();
var $chip = $(this);
clearTimeout(chipClickTimer);
// A second tap on the same chip edits it inline. Timed here rather than left to "dblclick",
// which mobile browsers fire unreliably or not at all for a double-tap.
var now = new Date().getTime();
if (lastChipNode === this && now - lastChipTap < DOUBLE_TAP_MS) {
lastChipNode = null;
startInlineEdit($chip, e);
return;
}
lastChipTap = now;
lastChipNode = this;
var wikitext = $chip.attr("data-wikitext");
chipClickTimer = setTimeout(function () {
cite.openTemplateDialog(wikitext).then(function (edited) {
applyChipEdit(liveChip($chip, wikitext), edited);
syncPanelWidth();
});
}, DOUBLE_TAP_MS);
});
$surface.on("dblclick", ".dsb-tpl-chip", function (e) {
e.preventDefault();
clearTimeout(chipClickTimer);
startInlineEdit($(this), e);
});
// A long etymology cannot be read in the panel's usual 520px column, so the panel drops its
// width cap once there is enough text to need it. What it will look like is shown by the
// in-place render on the page itself, not by a second copy inside the form.
var WIDE_AT = 240; // characters
function syncPanelWidth() {
var wikitext = readChipSurface($surface.get(0));
if (wikitext.length > WIDE_AT || wikitext.indexOf("\n") !== -1) {
$surface.closest(".dsb-subsection-panel").addClass("dsb-panel-wide");
}
}
// A template typed by hand becomes a chip once typing settles, so it behaves like an inserted one:
// click for the parameter dialog, double-click for its raw wikitext.
var composing = false;
// The caret as an offset into the surface's wikitext, which is what survives rebuilding the DOM.
function caretOffset() {
var sel = window.getSelection();
var root = $surface.get(0);
if (!sel || !sel.rangeCount) {
return -1;
}
var range = sel.getRangeAt(0);
if (range.startContainer !== root && !$.contains(root, range.startContainer)) {
return -1; // caret is somewhere else on the page
}
var pre = document.createRange();
pre.selectNodeContents(root);
pre.setEnd(range.startContainer, range.startOffset);
var holder = document.createElement("div");
holder.appendChild(pre.cloneContents());
return readChipSurface(holder).length;
}
// Puts the caret back at that wikitext offset. Only runs straight after a rebuild, so the surface
// holds nothing but text nodes and chips.
function placeCaret(offset) {
var root = $surface.get(0);
var range = document.createRange();
var nodes = root.childNodes;
root.focus();
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i];
var len = node.nodeType === 3
? node.nodeValue.length
: ($(node).attr("data-wikitext") || $(node).text()).length;
if (offset <= len) {
if (node.nodeType === 3) {
range.setStart(node, offset);
} else {
range.setStartAfter(node); // a chip is atomic; the caret goes after it
}
break;
}
offset -= len;
}
if (!range.startContainer || range.startContainer === document) {
range.selectNodeContents(root);
}
range.collapse(true);
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
// True when the selection overlaps the inside of a template call. A caret resting exactly at the
// closing "}}" does not count -- that is someone who has just finished typing one.
function selectionTouchesCall(wikitext) {
var sel = window.getSelection();
if (!sel || !sel.rangeCount) {
return false;
}
var range = sel.getRangeAt(0);
var root = $surface.get(0);
if (range.startContainer !== root && !$.contains(root, range.startContainer)) {
return false;
}
var from = caretOffset();
var to = from;
if (!range.collapsed) {
var pre = document.createRange();
pre.selectNodeContents(root);
pre.setEnd(range.endContainer, range.endOffset);
var holder = document.createElement("div");
holder.appendChild(pre.cloneContents());
to = readChipSurface(holder).length;
}
return parseTemplateCalls(wikitext).calls.some(function (call) {
return from < call.end && to > call.start;
});
}
function chipifyTyped() {
if (composing) {
return; // never rebuild under an IME
}
var wikitext = readChipSurface($surface.get(0));
// Chips already hold one call each, so a higher call count means one was typed in as text.
if (parseTemplateCalls(wikitext).calls.length <= $surface.find(".dsb-tpl-chip").length) {
return;
}
var offset = caretOffset();
if (selectionTouchesCall(wikitext)) {
return; // still inside a call: leave it as text until the caret moves out
}
renderChipSurface($surface, wikitext);
savedRange = null; // the old range points at nodes that no longer exist
if (offset >= 0) {
placeCaret(offset);
rememberCaret();
}
}
$surface.on("compositionstart", function () {
composing = true;
});
$surface.on("compositionend", function () {
composing = false;
schedulePreview();
});
var previewTimer = null;
function schedulePreview() {
clearTimeout(previewTimer);
previewTimer = setTimeout(function () {
chipifyTyped();
syncPanelWidth();
}, 700);
}
$surface.on("input", schedulePreview);
$surface.data("dsbSchedulePreview", schedulePreview);
$fieldWrap.append(makeSearchable($select, util.msg.addTemplate), $surface);
// Bilêvkirin is written in IPA, so it gets the same character palette the wizard shows. Its
// characters go in as text and its two template buttons as chips, which is what the rest of
// this surface is made of.
if (s.place === "pron") {
$fieldWrap.append(util.buildIpaPalette({
insertText: function (text) {
$surface.focus();
restoreCaret();
insertAtCaret($surface, document.createTextNode(text));
rememberCaret();
schedulePreview();
},
insertTemplate: function (snippet) {
$surface.focus();
restoreCaret();
var $chip = makeTemplateChip(snippet.replace(/\|\s*\}\}$/, "}}"));
insertAtCaret($surface, $chip.get(0));
rememberCaret();
cite.openTemplateDialog($chip.attr("data-wikitext")).then(function (edited) {
applyChipEdit($chip, edited);
syncPanelWidth();
});
}
}));
}
}
// The three "place" sections are wikitext with a shape worth spelling out, since nothing on screen
// says what belongs in the box.
var PLACE_PLACEHOLDERS = {
"page-top": "{{h-bnr|Kelîme|Kelîmeyeke din}} an jî {{herwiha binêre/ane}}",
"pos-top": "[[Wêne:Navê pelî.jpg|thumb|Binnivîs]]",
"pron": "* {{ku-IPA}}"
};
function renderTextArea(s, rows) {
$fieldWrap.empty().append(
$("<textarea class='dsb-input dsb-subsection-text'></textarea>")
.attr("rows", rows)
.attr("placeholder", PLACE_PLACEHOLDERS[s.place] || (s.header + "…"))
);
util.initManualResize($fieldWrap);
}
// A translation box: the {{werger-ser}}/{{werger-bin}} pair, with the sense it covers and the
// layout parameters that template accepts. The rows in between are left to the translation editor
// gadget, which is what edits them on the rendered page.
// Reads and writes through fieldText()/setFieldText() as wikitext, so drafts, save and the queue
// need no special case beyond the append below.
var wergerForm = null;
var imagesField = null;
function renderWergerField(s) {
var inputs = {};
// Its own row class rather than .dsb-tpl-param-row: that one is styled in cite.css, which is
// only on the page when wikt.cite happens to be loaded, and without it the label falls back to
// sitting inline and a short one ("id") is overlapped by its own box.
function row(key, label, placeholder) {
inputs[key] = $("<input type='text' class='dsb-input'>").attr("placeholder", placeholder || "");
return $("<div class='dsb-werger-row'></div>").append(
$("<label class='dsb-adv-label'></label>").text(label).append(inputs[key])
);
}
// {{werger-ser}} also takes stûn, firehî, bgcolor, valign and align, but they only restyle the
// box and its defaults are what these pages use, so the form does not offer them. They are
// still in WERGER_NAMED, so a block that already sets one is read back correctly.
var $main = $("<div class='dsb-werger-main'></div>").append(
row("1", "Mane", "ava vexwarinê"),
row("2", "Mane no", "ne mecbûrî — di parantezê de xuya dike"),
row("id", "id", "ne mecbûrî — IDya tercimeyê")
);
var $note = $("<p class='dsb-werger-note'></p>").text(
"Qutiyeke Wergerê ya vala lê zêde dikî. Tercimeyan bi amûra wergerê dûre binivîse."
);
wergerForm = {
read: function () {
var out = {};
$.each(inputs, function (key, $input) {
var value = $.trim($input.val() || "");
if (value) {
out[key] = value;
}
});
return out;
},
set: function (params) {
$.each(inputs, function (key, $input) {
$input.val((params && params[key]) || "");
});
}
};
$fieldWrap.empty().append($note, $main);
}
// Same [[Kategorî:Çavkanî]] search widget start.js's own picker uses: choosing an entry inserts
// "* {{Target}}" at the cursor. The field itself stays the plain ".dsb-subsection-text" textarea
// every other "kind: text" subsection already uses, so save/prefill/drafts need no changes.
function renderCavkaniField(s) {
var $ta = $("<textarea class='dsb-input dsb-subsection-text'></textarea>")
.attr("rows", 4)
.attr("placeholder", util.msg.pickOrTypeTemplate);
var picker = cite.buildCategoryTemplateSearch({
placeholder: util.msg.addTemplate,
describe: true,
// Same param dialog Etîmolojî opens on a fresh insert: filled from the picked template's own
// TemplateData, so the user adds "rûpel=", "sernav=", ... before anything lands in the field
// rather than typing a bare {{Target}} and going back to fill params in by hand.
onPick: function (entry) {
cite.openTemplateDialog("{{" + entry.target + "}}").then(function (edited) {
if (!edited) {
return; // cancelled, or "Jê bibe" on a call that was never inserted
}
util.insertAtCursor($ta.get(0), "* " + edited + "\n");
$ta.trigger("focus");
});
}
});
var $picker = $("<div class='dsb-cavkani-picker'></div>").append(picker.$element);
$fieldWrap.empty().append($picker, $ta);
}
// One "{{kol3}}" group: its "sernav=" heading and its words. Several of these make up one section --
// "Herwiha binêre" typically carries an unheaded list plus a few headed ones.
function addGroup(s, group) {
var $sernav = $("<input type='text' class='dsb-input'>").attr("placeholder", "ne mecbûrî…");
// {{h-bnr}} takes bare page names, so its rows carry no ⊕: there is nowhere in the call for a
// modifier to go.
var words = buildWordListField(s.header + "…", { modifiers: !s.single });
var $remove = $("<a href='#' class='dsb-quick-link dsb-group-remove'>Komê rake</a>");
var $element = $("<div class='dsb-wordgroup'></div>").append(
// Label beside the box, not above it: stacked, "Sernav" read as a heading for the whole
// group rather than as the name of the field under it. The input is nested in the label so
// clicking the word focuses the box, without needing an id per group.
// A single-call list has no "sernav=" to carry, so the row is built (readGroups still reads
// it) but never shown. Hidden here rather than in renderField: setGroups() rebuilds every
// group on prefill, which would put a visible one back.
$("<div class='dsb-tpl-param-row dsb-param-row-inline'></div>").append(
$("<label class='dsb-adv-label'></label>").text("Sernav").append($sernav)
).toggle(!s.single),
words.$element,
$remove
);
var field = {
$element: $element,
read: function () {
return { sernav: $.trim($sernav.val()), words: words.values() };
},
set: function (g) {
$sernav.val((g && g.sernav) || "");
words.setValues((g && g.words) || []);
}
};
$remove.on("click", function (e) {
e.preventDefault();
var at = $.inArray(field, groupFields);
if (at !== -1) {
groupFields.splice(at, 1);
}
$element.remove();
updateGroupChrome();
});
field.set(group);
groupFields.push(field);
$groups.append($element);
updateGroupChrome();
return field;
}
// With a single group there is nothing to tell apart, so it needs neither a remove link nor a label.
function updateGroupChrome() {
var many = groupFields.length > 1;
$groups.toggleClass("dsb-wordgroups-many", many);
$groups.find(".dsb-group-remove").toggle(many);
}
function readGroups() {
return groupFields.map(function (field) {
return field.read();
});
}
// Rebuilds the blocks to match what the page (or the draft) holds, always leaving at least one.
function setGroups(s, groups) {
groupFields = [];
$groups.empty();
(groups && groups.length ? groups : [null]).forEach(function (group) {
addGroup(s, group);
});
}
// Loads what the page already has into the form, so an edit starts from the current list rather than
// from an empty one. A wordlist section we can't parse degrades to raw wikitext editing.
function prefill(s) {
$status.removeClass("dsb-status-error dsb-status-ok").text("Tê barkirin…");
$save.prop("disabled", true);
fetchPageSubsection(language, sectionType, sectionOrdinal, s).then(function (current) {
$save.prop("disabled", false);
$status.text("");
if (!current) {
return;
}
// A "place" section has no heading of its own, so whether the page already has one cannot
// be seen in the DOM the way a "====" subsection can -- it is known only once its content
// has been read. That is what decides "nûkirin" or "zêdekirin" in the edit summary.
if (s.place) {
s.existing = !!(current.images ? current.images.length : $.trim(current.text || ""));
// The label was set before this content arrived, when nothing yet said one was there.
$save.text(s.existing && s.kind !== "werger" ? util.msg.edit : util.msg.add);
}
if (current.kind === "images" && imagesField) {
imagesField.setValues(current.images);
return;
}
// A free section's field was built before its content was known: a list turns it into the
// {{kol3}} form, anything else leaves it the wikitext box it already is.
if (s.free && current.kind === "wordlist" && !groupFields.length) {
s.kind = "wordlist";
renderWordListField(s);
setGroups(s, current.groups);
if (current.fromBullets) {
$status.text("Ev lîste made bi made hatiye nivîsîn — bi qeydkirinê dibe {{kol3}}.");
}
return;
}
if (current.kind === "wordlist" && groupFields.length) {
setGroups(s, current.groups);
if (current.fromBullets) {
$status.text("Ev lîste made bi made hatiye nivîsîn — bi qeydkirinê dibe {{kol3}}.");
}
return;
}
if (s.kind === "wordlist") {
editAsText = true;
groupFields = [];
renderTextArea(s, 6);
$status.text(s.unknown
? "Ev beş nayê naskirin — wekî wîkîtekst tê guhartin."
: "Şablona vê lîsteyê nehate naskirin — wekî wîkîtekst biguhêre.");
}
setFieldText(current.text);
}, function () {
$save.prop("disabled", false);
$status.addClass("dsb-status-error").text("Nikare muhtewaya heyî bixwîne.");
});
}
// One "{{kol3}}" group per block, plus a way out to raw wikitext for a list no row can express --
// a shared subpage list like {{herwiha binêre/ane}}, say.
function renderWordListField(s) {
$fieldWrap.empty();
chipSurface = null;
editAsText = false;
var $addGroup = $("<a href='#' class='dsb-quick-link'>+ Komek lê zêde bike</a>");
$addGroup.on("click", function (e) {
e.preventDefault();
addGroup(s, null);
});
$groups = $("<div class='dsb-wordgroups'></div>");
$fieldWrap.append($groups, $addGroup);
addGroup(s, null);
// One call takes one flat list of names, so there is no second group to add.
if (s.single) {
$addGroup.hide();
}
var $toRaw = $("<a href='#' class='dsb-quick-link'>Wekî wîkîtext biguhêre</a>");
$toRaw.on("click", function (e) {
e.preventDefault();
var current = s.place
? wt.buildHbnrCall(loosePlaceWords(readGroups()))
: subsectionBodyWikitext(language, s, readGroups());
editAsText = true;
groupFields = [];
$groups = null;
renderTextArea(s, 3);
setFieldText(current);
util.initManualResize($fieldWrap);
});
$fieldWrap.append($toRaw);
util.initManualResize($fieldWrap);
}
function renderField() {
var s = currentSubsection();
$fieldWrap.empty();
groupFields = [];
$groups = null;
editAsText = false;
chipSurface = null;
wergerForm = null;
imagesField = null;
$status.removeClass("dsb-status-error dsb-status-ok").text("");
// Save stays disabled (but "Betal bike" usable) until something is actually chosen.
$save.prop("disabled", !s);
if (!s) {
$save.text(util.msg.add);
lastId = null;
return; // still on "Binebeşekê hilbijêre"
}
if (s.kind === "wordlist") {
renderWordListField(s);
} else if (s.kind === "images") {
imagesField = util.buildImageListField({
prefix: "dsb-",
rowClass: "dsb-tpl-param-row dsb-param-row-inline",
inputClass: "dsb-input",
addClass: "dsb-quick-link",
pageName: mw.config.get("wgTitle")
});
$fieldWrap.append(imagesField.$element);
} else if (s.header === "Etîmolojî") {
renderTemplateEditor(s, templateChoices("etymTpls", language));
} else if (s.place === "pron") {
renderTemplateEditor(s, templateChoices("pronTpls", language));
} else if (s.kind === "werger") {
renderWergerField(s);
} else if (s.header === "Çavkanî") {
renderCavkaniField(s);
} else {
renderTextArea(s, 3);
}
// Werger always adds a box, even to a section that is already there, so its button never says
// "Biguhêre".
$save.text(s.existing && s.kind !== "werger" ? util.msg.edit : util.msg.add);
// Covers whichever branch above built the field. Idempotent, so a textarea already carrying a
// grip is skipped.
util.initManualResize($fieldWrap);
lastId = s.id;
// A draft always wins over re-reading the page: it is the newer, unsaved state.
if (drafts[s.id]) {
restoreDraft(s, drafts[s.id]);
} else if ((s.existing || s.place) && s.kind !== "werger") {
prefill(s);
}
}
$select.on("change", function () {
captureDraft();
renderField();
});
renderField();
// Back to the state a fresh open would show: no draft, no picked subsection. Called directly
// rather than through the "change" handler above, so the discarded draft is never captured first.
function resetForm() {
drafts = {};
if (!onlyHeader) {
$select.val("").trigger("wiktku:refresh");
}
renderField();
}
$save.on("click", function () {
var s = currentSubsection();
if (!s) {
return;
}
var asText = editAsText || (s.kind !== "wordlist" && s.kind !== "images");
var effective = asText ? $.extend({}, s, { kind: "text" }) : s;
var content;
if (asText) {
content = fieldText();
if (!content) {
return;
}
} else if (s.kind === "images") {
content = imagesField ? imagesField.values() : [];
if (!content.length) {
return;
}
} else {
content = readGroups();
if (!content.some(function (group) { return group.words.length; })) {
return;
}
}
$save.prop("disabled", true);
// A Werger section holds any number of boxes, so an existing one is appended to rather than
// replaced; every other kind edits the single body it owns.
var appending = s.kind === "werger" && s.existing;
queueChange(
subsectionPlaceLabel(s, language, sectionType, $h3),
s.kind === "werger"
? "qutiyeke wergerê hate zêdekirin"
: "beşa «" + s.header + "» hate " + (s.existing ? "nûkirin" : "zêdekirin"),
function (pageContent) {
var body = content;
if (appending) {
var current = readPageSubsection(pageContent, language, sectionType, sectionOrdinal, effective);
var existingText = current ? $.trim(current.text || "") : "";
body = existingText ? existingText + "\n\n" + content : content;
}
return {
content: s.free
? updateFreeSection(pageContent, language, effective, body)
: (s.place
? writeLoosePlace(pageContent, language, sectionType, sectionOrdinal, effective,
loosePlaceBody(effective, body, language))
: (s.existing
? updatePageSubsection(pageContent, language, sectionType, sectionOrdinal, effective, body)
: insertPageSubsection(pageContent, language, sectionType, sectionOrdinal, effective, body))),
warnings: []
};
},
$save,
$status,
// Werger is a box the translation tools fill in afterwards, so there is nothing to draw for
// it. A new subsection gets a block of its own at the position it will land in; one already
// on the page is redrawn where it stands.
s.kind === "werger" ? null : function () {
// A free section is redrawn under its own heading, which is the element the panel hangs
// on.
if (s.free) {
var restoreFree = replaceFreeSectionInPlace($h3,
subsectionBodyWikitext(language, effective, content));
return function () {
if (restoreFree) {
restoreFree();
restoreFree = null;
}
};
}
// A "place" section has no heading to redraw between, so it is found in the DOM by what
// it renders as and replaced there.
if (s.place) {
var restoreLoose = replaceLoosePlaceInPlace($h3, effective,
loosePlacePreviewWikitext(effective, content, language));
return function () {
if (restoreLoose) {
restoreLoose();
restoreLoose = null;
}
};
}
if (!s.existing) {
var $preview = buildSubsectionPreviewDom($h3, s, content, language);
return function () {
$preview.remove();
};
}
var restore = replaceSubsectionInPlace($h3, effective.header,
subsectionBodyWikitext(language, effective, content));
return function () {
if (restore) {
restore();
restore = null;
}
};
},
{ key: s.free
? "section:" + s.header + "|" + s.free.ordinal
: "subsection:" + sectionType + "|" + sectionOrdinal + "|" + s.header }
);
});
$panel.empty().append($selectWrap, $fieldWrap, $actions);
}
// Builds the "choose a part of speech" picker that reclassifies a section -- an unclassified
// "=== Mane ===" placeholder, or a "=== Biwêj ===" that turns out to be a real part of speech. Saved via
// convertSectionToPos(); lists every name from sectionNames except the one the section already is.
function buildConvertPosPanel($panel, $h3, language, sectionType, sectionOrdinal) {
var $select = $("<select class='dsb-convert-pos-select'></select>");
// Label-less, like the subsection picker's: it holds the "nothing chosen yet" value without showing
// up as a row, so the prompt lives in the input's placeholder instead.
$select.append($("<option value=''></option>"));
Object.keys(sectionNames).forEach(function (name) {
// The section's own part of speech is listed too, marked: picking it changes nothing about the
// heading and leaves the parameters below to be edited on their own.
$select.append($("<option></option>").val(name)
.text(name === sectionType ? name + " (ya niha)" : name));
});
var $selectWrap = makeSearchable($select, "Li cureyekî rêzimanî bigere");
// Opens on the section's own part of speech, so editing only its parameters needs no pick.
if (sectionNames[sectionType]) {
$select.val(sectionType).trigger("wiktku:refresh");
}
// The same card the entry wizard shows under its part-of-speech question -- gender, transitivity,
// the transliteration boxes and everything behind "Tercîhên pêşketî". Shared from
// wikt.edit/posparams.js, so reclassifying a section here offers exactly what creating one in the
// wizard does.
var card = posparams.build({ lang: language, pos: "" });
// Opened on the call the section already has, so this edits the current template rather than
// starting from a blank line. setContext() rebuilds the line whenever the part of speech moves,
// so the prefill is only the starting point.
var spec0 = posTemplateFor(sectionType);
card.setContext(language, spec0 ? spec0.name : "", /^Formeke /.test(sectionType));
fetchPageContent().then(function (pageContent) {
try {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var found = findPosTemplateLine(loc.lines, loc.posLineIndex, sectionType);
if (found.index !== -1 && ($select.val() || sectionType) === sectionType) {
card.setWikitext(found.text);
}
} catch (e) { /* an unreadable section just leaves the card as it was */ }
});
$select.on("change", function () {
var spec = posTemplateFor($select.val());
card.setContext(language, spec ? spec.name : "", /^Formeke /.test($select.val() || ""));
});
// "gh" on its own, or "<form>-<base>" once a structural form is picked -- start.js's own composition.
// convertSectionToPos() writes the template itself, so only what follows "{{pos|lang" is wanted
// from the card's line.
function posParams() {
var m = /^\{\{[^|}]*\|[^|}]*\|?([\s\S]*)\}\}$/.exec(card.wikitext());
return m ? m[1] : "";
}
var $save = $("<button type='button' class='dsb-save'></button>").text(util.msg.add);
var $cancel = $("<button type='button' class='dsb-cancel'></button>").text(util.msg.cancel);
var $status = $("<span class='dsb-status'></span>");
var $actions = $("<div class='dsb-form-actions'></div>").append($save, $cancel, $status);
// dsb.js owns the pencil and the panel's built state, so it does the closing.
$cancel.on("click", function () {
$panel.trigger("dsbCancel");
hideQueueIfIdle();
});
$save.on("click", function () {
var chosen = $select.val();
if (!chosen) {
$status.removeClass("dsb-status-ok").addClass("dsb-status-error").text("Pêşî cureyekî rêzimanî hilbijêre.");
return;
}
var params = posParams();
$save.prop("disabled", true);
$status.removeClass("dsb-status-error").text("");
queueChange(
langLabel(language),
"beşa «" + sectionLabel(sectionType, $h3) + "» bû «" + chosen + "»",
function (pageContent) {
return { content: convertSectionToPos(pageContent, language, sectionType, sectionOrdinal, chosen, params), warnings: [] };
},
$save,
$status,
function () {
// The heading keeps its text, which every other queued change finds the section by; a
// note under it names the new one. The headword line is drawn as the save will write it.
var $note = $("<div class='dsb-preview-inline dsb-preview-heading-note'></div>")
.text("→ " + chosen + (params ? " (" + params + ")" : "") + " — nû, hîn nehatiye qeydkirin");
headingOuter($h3).after($note);
var dropped = false;
var restoreHeadword = null;
fetchPageContent().then(function (pageContent) {
if (dropped) {
return;
}
var call;
try {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
call = posCallLine(findPosTemplateLine(loc.lines, loc.posLineIndex, sectionType),
language, chosen, params);
} catch (e) {
return;
}
restoreHeadword = replaceHeadwordInPlace($h3, call);
});
return function () {
dropped = true;
$note.remove();
if (restoreHeadword) {
restoreHeadword();
restoreHeadword = null;
}
};
},
// Runs after every other queued change: it renames the "Mane" heading those changes locate
// themselves by, so anything edited on this section has to be applied first.
{ runLast: true, key: "convert:" + sectionType + "|" + sectionOrdinal, pagePosition: $("h3, h4").index($h3) }
);
});
$panel.empty().append($selectWrap, card.$element, $actions);
}
// Attaches the "+ Beşek lê zêde bike" trigger right after a POS block's own last element.
// Puts the same pencil the definitions use on every existing "====" subsection heading the gadget knows how
// to edit, opening buildSubsectionPanel() pinned to that one subsection.
module.exports = {
buildAdvancedPanel: buildAdvancedPanel,
buildSubsectionPanel: buildSubsectionPanel,
buildConvertPosPanel: buildConvertPosPanel,
editExample: editExample,
clearQueue: clearQueue,
closeAllPanels: closeAllPanels,
openQueueBox: openQueueBox
};
}());
// </nowiki>
hhizmc896kk236jfoj6e92tft1ytex7
6531968
6531954
2026-09-17T15:59:39Z
Wikihez
7344
Test loader lists only gadgets with undeployed changes; italic example with a bracketed explanation reads as {{nimûne}}; no îzah button for Kurmancî
6531968
javascript
text/javascript
// <nowiki>
/*
* Page: [[MediaWiki:Gadget-wikt.dsb-panels.js]]
*
* Everything DefSideBoxes shows once you click: the per-sense advanced panel, the subsection editors,
* the part-of-speech picker, the template dialogs, the preview and the queued save.
*
* Loaded on demand by wikt.dsb, never on a plain page view. It exports only what that half calls.
*/
(function () {
"use strict";
var sections = require("ext.gadget.wikt.core").sections;
var edit = require("ext.gadget.wikt.edit");
var cite = require("ext.gadget.wikt.cite");
var util = edit.util;
var posparams = edit.posparams;
var wt = edit.wt;
var startData = edit.data;
var langs = edit.langs;
var escapeRegExp = util.escapeRegExp;
// Local names for the shared wikitext layer, so every call site below reads as it always did.
var langHeaderSource = wt.langHeaderSource;
var canonicalTemplateKey = wt.canonicalTemplateKey;
var aliasPattern = wt.aliasPattern;
var splitLeadingCalls = wt.splitLeadingCalls;
var splitTemplateParams = wt.splitTemplateParams;
var parseTemplateCalls = wt.parseTemplateCalls;
var splitCallParams = wt.splitCallParams;
var buildTemplateCall = wt.buildTemplateCall;
var tdText = wt.tdText;
var fetchTemplateData = wt.fetchTemplateData;
var parseWordListCall = wt.parseWordListCall;
var parseWordListCalls = wt.parseWordListCalls;
var namedParam = wt.namedParam;
var withNamedParam = wt.withNamedParam;
var buildWordListCall = wt.buildWordListCall;
var parseBulletWordList = wt.parseBulletWordList;
var sectionNames = sections.sectionNames;
var MANE_SECTION = sections.MANE_SECTION;
var PAGE_SUBSECTIONS = sections.PAGE_SUBSECTIONS;
var headingOuter = sections.headingOuter;
var headingText = sections.headingText;
var isManeHeading = sections.isManeHeading;
var sectionTypeOf = sections.sectionTypeOf;
var sectionOrdinalWithinLanguage = sections.sectionOrdinalWithinLanguage;
var isFormOfWrapper = sections.isFormOfWrapper;
var eachLeafDefinition = sections.eachLeafDefinition;
var api = new mw.Api();
// metadata[lang].g, minus the grammatical-number codes start.js filters out with isNumberCode(), rendered
// the way its genderLabel() does ("m-p" -> "mê (pirjimar)").
// start.js asks transitivity and structural form as two questions and joins them as "<form>-<base>", so
// "gerguhêz" alone is "gh" and "xwerû" + "gerguhêz" is "x-gh".
// Template call convertSectionToPos() drops under a newly-chosen POS heading, e.g. "Hoker" -> "{{hoker|ku}}".
// A "Formeke X" (form-of) heading uses the unchanged base pos template. Doubles as the list of template
// calls a conversion may find under the OLD heading and replace, rather than leave stacked beside the new one.
// A section's template call. "Formeke Xê" uses X's own template with form=erê. Biwêj is recognised so a
// conversion can replace its {{biwêj}}, but never written: no new ones.
function posTemplateFor(header) {
var form = /^Formeke (.+)ê$/.exec(header || "");
if (form && startData.posChoices[form[1]] && sections.hasFormHeading(form[1])) {
return { name: form[1], params: ["form=erê"] };
}
var key = util.decapitalize(header || "");
return startData.posChoices[key] ? { name: key, params: [] } : null;
}
// convertSectionToPos() drops whichever of the tag's names is actually present.
var MANE_TAG_REGEX = wt.MANE_TAG_REGEX;
// The six per-sense groups start.js writes as "#: {{tpl|lang|word1|word2}}" sub-lines under a "#" definition.
// Array order controls where a new group line lands relative to existing ones. Every group lives behind the
// quick row's "Hevmane û Bêhtir" dropdown; only the gloss and Nimûne/jêgirtin links sit in the row itself.
var SENSE_GROUPS = startData.senseGroups;
// The column word-list template new sections are written with; the rest of wordlistTemplates are
// spellings the parser also accepts.
var WORDLIST_TEMPLATE = startData.wordlistTemplates[0];
// ---- {{werger-ser}} / {{werger-bin}}: one translation box ----
// The pair brackets the rows; the translation editor gadget writes the rows themselves onto the
// rendered page, so this only ever builds (and reads back) the opening call.
// Parameters, from [[Şablon:werger-ser]]: 1 = the sense shown in the box head, 2 = a note in
// parentheses after it, id = anchor override (defaults to 1), stûn = column count, firehî = column
// width (default 26em), bgcolor, valign (default top), align (default left).
var WERGER_NAMED = ["id", "stûn", "firehî", "bgcolor", "valign", "align"];
function buildWergerBlock(params) {
var parts = ["werger-ser"];
// Positional slots: 2 can only be reached past 1, so an empty 1 still has to be written.
if (params["1"] || params["2"]) {
parts.push(params["1"] || "");
}
if (params["2"]) {
parts.push(params["2"]);
}
WERGER_NAMED.forEach(function (key) {
if (params[key]) {
parts.push(key + "=" + params[key]);
}
});
return "{{" + parts.join("|") + "}}\n{{werger-bin}}";
}
// The opening call of the first block in `text`, as the same {1, 2, id, ...} object. Empty object when
// there is none, so a half-typed draft restores as an empty form rather than throwing.
function parseWergerBlock(text) {
var out = {};
var body = wergerSerBody(String(text || ""));
if (!body) {
return out;
}
var positional = 0;
splitTemplateArgs(body).forEach(function (arg) {
var eq = arg.indexOf("=");
var name = eq === -1 ? "" : $.trim(arg.slice(0, eq));
if (eq !== -1 && WERGER_NAMED.indexOf(name) !== -1) {
out[name] = $.trim(arg.slice(eq + 1));
} else {
out[String(++positional)] = $.trim(arg);
}
});
return out;
}
// Everything after "{{werger-ser|" up to the "}}" that closes it, or "" when there is no call or it
// takes no parameters. Scans for the matching close rather than the first one: a gloss can hold a
// template of its own, and a lazy regex would stop inside it.
function wergerSerBody(text) {
var m = /\{\{\s*werger-ser\s*(?=[|}])/.exec(text);
if (!m) {
return "";
}
var depth = 1;
var start = m.index + m[0].length;
for (var i = start; i < text.length - 1; i++) {
var two = text.substr(i, 2);
if (two === "{{") {
depth++; i++;
} else if (two === "}}") {
depth--; i++;
if (!depth) {
return text.charAt(start) === "|" ? text.slice(start + 1, i - 1) : "";
}
}
}
return "";
}
// Splits on "|" at brace/bracket depth 0, so a value containing a template or a link stays whole.
function splitTemplateArgs(body) {
var args = [];
var depth = 0;
var current = "";
for (var i = 0; i < body.length; i++) {
var two = body.substr(i, 2);
if (two === "{{" || two === "[[") {
depth++; current += two; i++; continue;
}
if (two === "}}" || two === "]]") {
depth--; current += two; i++; continue;
}
if (body[i] === "|" && depth <= 0) {
args.push(current); current = ""; continue;
}
current += body[i];
}
args.push(current);
return args;
}
// Canonical ku.wiktionary "====" subsection order, used only as insertPageSubsection()'s insertion rank
// (see subsectionRankForHeader()). Wider than PAGE_SUBSECTIONS: it also covers subsections the gadget
// cannot create but still has to order around.
var SUBSECTION_ORDER = startData.subsectionOrder;
// ---- searchable <select> ----------------------------------------------------------------------------
// Same widget start.js gets from its util.js wrapNativeSelect(): the native <select> stays as the value
// holder (so .val() and "change" handlers keep working) but is hidden behind a text input that filters an
// overlay list. Ported rather than required() because util.js is a file inside start.js's package module,
// which another gadget cannot reach; the template-row decoration start.js uses is left out.
// util.js's wrapNativeSelect(): the native <select> stays the value holder behind a filtering text input.
// It reads its own placeholder off data-placeholder, and its wiktku-* styles ship in start.css, so both
// gadgets get the identical widget instead of two lookalikes.
function makeSearchable($select, placeholder) {
$select.attr("data-placeholder", placeholder || "Lê bigere…");
return util.wrapNativeSelect($select);
}
// ---- small shared utilities ----
// The readable half of an mw.Api rejection.
var apiErrorMessage = util.apiErrorMessage;
// Thrown from inside an edit() transform to bail out with a specific message.
var SubmitAbort = util.SubmitAbort;
// A gloss is one field on the page: the sense, then {{îzah|…}} explaining it when there is one.
function joinGloss(mane, izah) {
return izah ? $.trim(mane + " {{îzah|" + izah + "}}") : $.trim(mane);
}
// The two halves are linked separately, never joined first: linkify leaves template calls alone by
// design, so anything inside {{îzah|…}} would never be looked at.
//
// A half is skipped when there is nothing in it worth looking up, and -- when onlyUnlinked is set --
// when it already carries a link. Someone who has linked a word by hand has made that half theirs, and
// a save must not go on linking the rest of it behind their back; "Oto-lînk bike" leaves the flag off
// and still links on demand.
function linkifyGlossParts(mane, izah, done, onlyUnlinked) {
function needsLinking(text) {
if (!$.trim(text) || !util.hasLinkableText(text)) {
return false;
}
return !(onlyUnlinked && text.indexOf("[[") !== -1);
}
function step(text, next) {
if (!needsLinking(text)) {
next(text, null);
return;
}
util.linkifyText(text, function (linked, error) {
// An empty answer would wipe what the user typed, so only real text is taken.
next(typeof linked === "string" && $.trim(linked) ? $.trim(linked) : text, error || null);
});
}
step(mane, function (linkedMane, maneError) {
step(izah, function (linkedIzah, izahError) {
done(linkedMane, linkedIzah, maneError || izahError);
});
});
}
// Runs transformFn(content) inside mw.Api().edit()'s own transform callback so one fetch covers both the
// edit-conflict check and the content, preventing a stale overwrite of someone else's edit.
//
// No badtoken retry here on purpose: edit() posts through postWithEditToken -> postWithToken, which
// already throws the cached token away and repeats the request once when the wiki says "badtoken".
// A second retry of our own would only double it.
function editPage(summary, transformFn, onSuccess, onError) {
api.edit(mw.config.get("wgPageName"), function (revision) {
return { text: transformFn(revision.content), summary: summary };
}).then(onSuccess, function (codeOrAbort, result) {
if (codeOrAbort instanceof SubmitAbort) {
onError(codeOrAbort.message);
} else {
onError(util.msg.saveFailed + apiErrorMessage(codeOrAbort, result));
}
});
}
// ---- rendered preview: wikitext through the parse API, so a preview shows what the templates
// actually produce rather than a plain list standing in for them ----
// Same text is usually asked for twice (open the preview, close it, open it again), so the promise is
// kept rather than the call repeated.
var renderCache = {};
/**
* Renders wikitext as this page would render it. Resolves with an HTML string.
*
* pst: 1 applies pre-save transform, so ~~~~ and subst: behave as they will once saved. The
* modulestyles/modules the parse reports are loaded before resolving: without them TemplateStyles is
* missing and the preview would show the right markup in the wrong clothes.
*/
function renderWikitext(wikitext) {
if (renderCache[wikitext]) {
return renderCache[wikitext];
}
renderCache[wikitext] = api.post({
action: "parse",
format: "json",
formatversion: 2,
title: mw.config.get("wgPageName"),
text: wikitext,
contentmodel: "wikitext",
prop: "text|modules|jsconfigvars",
pst: 1,
disablelimitreport: 1,
disableeditsection: 1,
disabletoc: 1
}).then(function (data) {
var parsed = (data && data.parse) || {};
if (parsed.modulestyles && parsed.modulestyles.length) {
mw.loader.load(parsed.modulestyles);
}
if (parsed.modules && parsed.modules.length) {
mw.loader.load(parsed.modules);
}
return parsed.text || "";
}, function () {
// A failed render must not be remembered, or the next attempt gets the rejection back.
delete renderCache[wikitext];
return $.Deferred().reject().promise();
});
return renderCache[wikitext];
}
/**
* Puts rendered wikitext into $target, replacing whatever placeholder is there.
*
* The site's own gadgets are told about it through the "wikipage.content" hook, the same way
* MediaWiki announces real content, so collapsible lists and the like behave in the preview too.
*/
// MediaWiki warns when the hook is fired on content that is not in the document, and a gadget that
// listens to it can do nothing useful with nodes nobody can see.
function announceContent($el) {
if ($el.closest("html").length) {
mw.hook("wikipage.content").fire($el);
}
}
function fillWithRendered($target, wikitext, onFail) {
return renderWikitext(wikitext).then(function (html) {
if (!$target.closest("body").length) {
return; // the preview was dropped while the parse was in flight
}
$target.empty().append($("<div class='mw-parser-output'></div>").html(html));
announceContent($target);
}, function () {
if (onFail) onFail();
});
}
/**
* Puts a rendered sense in place of the one on the page, keeping this gadget's own furniture (the
* pencil's toolbar, the quick row, any open panel) where it is. The page's own nodes are detached
* rather than dropped, so the returned function can put them straight back when the change is
* cancelled or superseded.
*
* Nothing labels the result: the change is shown by the sense simply reading the way it will read,
* and the queue box is what says nothing is saved yet.
*/
function replaceSenseInPlace($li, item) {
var html = item.html;
var $anchor = $li.children(".dsb-toolbar").last();
// class/id/data-* off the parsed <li>, so a queued maneid shows its pill here exactly as it will
// once saved. The old values are kept for the restore below.
var node = $li.get(0);
var before = {};
$.each(node.attributes, function (_, attr) {
before[attr.name] = attr.value;
});
$.each(item.attrs, function (name, value) {
if (name !== "class") {
$li.attr(name, value);
}
});
var addedClasses = (item.attrs["class"] || "").split(/\s+/).filter(function (c) {
return c && !$li.hasClass(c);
});
$li.addClass(addedClasses.join(" "));
// add-examples hangs its own <ul> off the sense and opens its form as that <ul>'s next sibling.
// Detaching those leaves the form to open into a tree nobody can see -- "+ Nimune/jegirtin" takes
// the click and shows nothing -- so they stay where they are. Only the rows already in that <ul>
// are hidden, since the preview draws the examples itself; the tool's own title line stays.
var $keep = $li.children("ul, .add-example-form");
var $hiddenRows = $keep.filter("ul").children("li").filter(function () {
return !$(this).find(".add-example-form-title").length;
});
$hiddenRows.addClass("dsb-preview-hidden");
var originals = $li.contents().filter(function () {
return !$(this).is(".dsb-toolbar, .dsb-quick-row, .dsb-subsection-panel, .dsb-preview-block, .dsb-sense-preview");
}).not($keep).get();
// Where the sense's own nodes sat, so cancelling puts them back there rather than above the quick
// row -- the row belongs between the sense and its examples.
var backAfter = originals.length ? originals[0].previousSibling : null;
var $rendered = $("<span class='dsb-sense-preview'></span>").html(html);
if ($anchor.length) {
$anchor.after($rendered);
} else {
$li.prepend($rendered);
}
$(originals).detach();
announceContent($rendered);
// The id pill is drawn by the same code a page load uses, so a queued maneid looks like a saved
// one rather than like something this gadget invented.
if (window.wikt && wikt.showIDs && wikt.showIDs.process) {
wikt.showIDs.process($li);
}
// dsb.js puts the example pencils back on the redrawn examples. The originals keep theirs.
$li.triggerHandler("dsbSenseRedrawn");
return function () {
$hiddenRows.removeClass("dsb-preview-hidden");
$li.removeClass(addedClasses.join(" "));
$.each(item.attrs, function (name) {
if (name === "class") {
return;
}
if (Object.prototype.hasOwnProperty.call(before, name)) {
$li.attr(name, before[name]);
} else {
$li.removeAttr(name);
}
});
$li.children(".senseid-displayed, .etymid-displayed, .etymonid-displayed").remove();
if (backAfter && backAfter.parentNode === node) {
$(backAfter).after(originals);
} else if ($anchor.length) {
$anchor.after(originals);
} else {
$li.prepend(originals);
}
$rendered.remove();
};
}
/**
* The <li> out of a parsed "#" block, and the attributes it carries.
*
* The parse wraps it in its own <ol>, which must not reach the page: the sense already has a number
* from the list it lives in. A leading {{maneid}} also makes the parser emit an empty
* "li.mw-empty-elt" ahead of the real one, so the first <li> is not necessarily the sense.
*
* The attributes matter as much as the content: {{maneid|ku|x}} renders as class="senseid"
* data-id="x" on the <li> itself, which is what wikt.showids draws its pill from.
*/
function parsedSenseItem(html, depth) {
var $item = $("<div></div>").html(html).find("ol > li").filter(function () {
return !$(this).hasClass("mw-empty-elt");
}).first();
// A sub-sense parsed on its own comes back one <ol> deeper per "#": "##" is a list inside a list.
// Descend as far as the sense's own depth, or its number would be drawn inside a second list --
// "2." with a "1." under it.
for (var level = 1; level < (depth || 1) && $item.length; level++) {
var $inner = $item.children("ol").children("li").filter(function () {
return !$(this).hasClass("mw-empty-elt");
}).first();
if (!$inner.length) {
break;
}
$item = $inner;
}
if (!$item.length) {
return { html: html, attrs: {} };
}
var attrs = {};
$.each($item.get(0).attributes, function (_, attr) {
attrs[attr.name] = attr.value;
});
return { html: $item.html(), attrs: attrs };
}
// This gadget's own save queue + floating widget, separate from the shared Gadget-Editor queue.
// Each entry keeps its own transformFn so saveQueueNow() can re-run all of them against one fresh fetch.
var dsbQueue = {
// [{key, summary, transformFn, runLast, removePreview}]; transformFn: baseContent -> {content, warnings}.
// removePreview lives on the entry so replacing one entry takes its own live DOM preview with it.
entries: []
};
var $queueBox = null;
function dropPreview(entry) {
if (typeof entry.removePreview === "function") {
try {
entry.removePreview();
} catch (e) { /* a preview failing to clean up should never block the queue itself */ }
entry.removePreview = null;
}
}
function clearAllPreviews() {
dsbQueue.entries.forEach(dropPreview);
}
function clearQueue() {
clearAllPreviews();
dsbQueue.entries = [];
if ($queueBox) {
$queueBox.$list.empty();
$queueBox.$box.hide();
}
}
// Closes every editor UI on the page. Each open box's own "x" clears its getter, restores its picker
// link and updates the counter, so those are triggered rather than repeating that work here; hiding a
// row alone would leave its fields populated and the next pencil click would reopen them half-filled.
function closeAllPanels() {
$(".dsb-open-boxes .dsb-box-remove").trigger("click");
$(".dsb-quick-note").hide().text("");
$(".dsb-more-menu").hide();
$(".dsb-subsection-panel, .dsb-convert-pos-panel").hide();
// dsb.js owns each pencil's pinned state; tell it rather than hiding behind its back, or the next
// click on the pencil would only toggle "pinned" off and appear to do nothing.
$(".dsb-quick-row").trigger("dsbCancel");
}
function ensureQueueBox() {
if ($queueBox) {
return $queueBox;
}
var $box = $("<div class='dsb-queue-box'></div>").hide();
var $close = $("<a href='#' class='dsb-queue-close' title='Veşêre'>×</a>");
var $title = $("<div class='dsb-queue-title'></div>").text("Guhartinên li benda qeydkirinê");
var $list = $("<ul class='dsb-queue-list'></ul>");
var $empty = $("<div class='dsb-queue-empty'></div>").text(
"Guhartinên te dê li vir werin rêzkirin. Heta ku tu pêl «Guhartinan qeyd bike» nekî, "
+ "tiştek li ser rûpelê nayê weşandin."
);
var $status = $("<div class='dsb-queue-status'></div>");
var $save = $("<button type='button' class='dsb-queue-save'>Guhartinan qeyd bike</button>");
var $cancel = $("<button type='button' class='dsb-queue-cancel'></button>").text(util.msg.cancel);
var $actions = $("<div class='dsb-queue-actions'></div>").append($save, $cancel);
$close.on("click", function (e) {
e.preventDefault();
$box.hide();
});
$cancel.on("click", function () {
clearQueue();
closeAllPanels();
});
$save.on("click", function () {
saveQueueNow();
});
$box.append($close, $title, $empty, $list, $actions, $status);
$(document.body).append($box);
$queueBox = { $box: $box, $list: $list, $empty: $empty, $status: $status, $save: $save, $cancel: $cancel };
return $queueBox;
}
// Takes one change out of the queue and puts the page back as it was before it, its in-place preview
// included. The queue is the only record of what "Guhartinan qeyd bike" will write, so every line in it
// has to be removable on its own -- closing the panel that made a change does not unmake it.
// Each preview draws the page the whole queue would save, so one entry leaving changes what all the
// others should show. They are taken down and drawn again from what is left.
function redrawPreviews() {
dsbQueue.entries.forEach(function (entry) {
if (typeof entry.previewFn !== "function") {
return;
}
dropPreview(entry);
try {
var removePreview = entry.previewFn();
if (typeof removePreview === "function") {
entry.removePreview = removePreview;
}
} catch (e) { /* a broken preview never stops the queue */ }
});
}
function dropQueueEntry(entry) {
var at = dsbQueue.entries.indexOf(entry);
if (at === -1) {
return;
}
dropPreview(entry);
dsbQueue.entries.splice(at, 1);
redrawPreviews();
renderQueueBox();
hideQueueIfIdle();
}
function renderQueueBox() {
var box = ensureQueueBox();
box.$list.empty();
dsbQueue.entries.forEach(function (entry) {
var $drop = $("<button type='button' class='dsb-queue-drop'>×</button>")
.attr("title", "Vê guhartinê ji rêzê derxe");
$drop.on("click", function () {
dropQueueEntry(entry);
});
box.$list.append($("<li></li>").text(entry.summary).append($drop));
});
box.$empty.toggle(!dsbQueue.entries.length);
box.$status.removeClass("dsb-status-ok dsb-status-error").text("");
box.$save.prop("disabled", !dsbQueue.entries.length);
box.$box.show();
}
// Shown the moment a panel opens, empty, so it is clear before anything is typed that a change waits
// for "Guhartinan qeyd bike" rather than publishing itself.
function openQueueBox() {
renderQueueBox();
}
// Nothing queued and no form left open means nothing to save, so the box goes away rather than
// lingering on an empty state.
function hideQueueIfIdle() {
if (dsbQueue.entries.length || !$queueBox) {
return;
}
if ($(".dsb-quick-row:visible, .dsb-subsection-panel:visible, .dsb-convert-pos-panel:visible").length) {
return;
}
$queueBox.$box.hide();
}
// Asks core's postEdit module to show its toast on the next load (dsb.js loads it there), then reloads.
// Loaded here rather than declared as a dependency, so a reload happens even if it fails.
function reloadWithPostEditNotice() {
mw.loader.using("mediawiki.action.view.postEdit").then(function (require) {
require("mediawiki.action.view.postEdit").fireHookOnPageReload("saved");
}).always(function () {
location.reload();
});
}
// A transform returns either the new page text or {content, warnings}; both shapes are accepted.
// One that returns neither stops the save, named, rather than writing a broken page.
function transformedContent(entry, pageContent) {
var result = entry.transformFn(pageContent);
var next = typeof result === "string" ? result : (result && result.content);
if (typeof next !== "string") {
throw new SubmitAbort("Guhartin nehate sepandin: " + entry.summary);
}
return next;
}
// Queue order, except that runLast entries move to the end: converting "=== Mane ===" into a real POS
// heading renames the very heading every other queued change locates itself by.
// Those run from the bottom of the page up: a rename changes the ordinals of the sections after it
// ("Mane 2" becomes "Mane 1"), never of the ones before.
function orderedEntries() {
var normal = [];
var last = [];
dsbQueue.entries.forEach(function (entry) {
(entry.runLast ? last : normal).push(entry);
});
last.sort(function (a, b) {
return b.pagePosition - a.pagePosition;
});
return normal.concat(last);
}
/**
* The pending page as previews and example lookups need it: every queued transform, in save order,
* except the runLast section renames. Those callers find their section under the heading it has on
* the page now, which a queued "Mane" -> "Navdêr" would take away, and a rename leaves the senses as
* they are. An entry that cannot apply is skipped -- the save reports that properly, and a preview
* must not shout.
*/
function queuedContent(pageContent) {
var content = pageContent;
orderedEntries().filter(function (entry) {
return !entry.runLast;
}).forEach(function (entry) {
try {
content = transformedContent(entry, content);
} catch (e) {
// Left out of the preview; the save reports it properly. Named, so a change that fails to
// draw is distinguishable from one with nothing to draw.
mw.log.warn("wikt.dsb: pêşdîtin bêyî vê guhartinê hate xêzkirin — " + entry.summary, e);
}
});
return content;
}
/**
* Redraws one sense where it sits, from the page the queue would save -- so a queued gloss edit, a
* queued example and everything else pending on that sense are shown together, rendered by the parser
* rather than described in a box. Returns the queue's own "take the preview down" function.
*
* One preview per sense: a second queued change takes the first drawing down and draws the queue.
*/
function previewSense($li, language, sectionType, sectionOrdinal, definitionLevel) {
var cancelled = false;
var restore = null;
function undo() {
cancelled = true;
if (restore) {
restore();
restore = null;
}
if ($li.data("dsbPreview") === undo) {
$li.removeData("dsbPreview");
}
}
var previous = $li.data("dsbPreview");
if (typeof previous === "function") {
previous();
}
$li.data("dsbPreview", undo);
fetchPageContent().then(function (pageContent) {
if (cancelled) {
return;
}
var wikitext;
try {
wikitext = senseBlockWikitext(queuedContent(pageContent), language, sectionType,
sectionOrdinal, definitionLevel);
} catch (e) {
return;
}
renderWikitext(wikitext).then(function (html) {
if (cancelled || $li.data("dsbPreview") !== undo || !$li.closest("body").length) {
return;
}
restore = replaceSenseInPlace($li, parsedSenseItem(html, definitionLevel.length - 1));
});
});
return undo;
}
// The real save: re-runs every queued transformFn, in order, against one freshly-fetched revision and
// posts the result as a single edit with a combined summary.
function saveQueueNow() {
if (!dsbQueue.entries.length) {
return;
}
var box = ensureQueueBox();
box.$save.prop("disabled", true);
box.$cancel.prop("disabled", true);
box.$status.removeClass("dsb-status-ok dsb-status-error").text("Tê qeydkirin…");
var entries = orderedEntries();
// Grouped by place, in the order the places were first queued: several changes to one section read
// as "Kurmancî/Navdêr 2: a, b, c" rather than repeating the section name in front of each of them.
var order = [];
var byPlace = {};
entries.forEach(function (entry) {
if (!Object.prototype.hasOwnProperty.call(byPlace, entry.place)) {
byPlace[entry.place] = [];
order.push(entry.place);
}
byPlace[entry.place].push(entry.action);
});
var combinedSummary = order.map(function (place) {
var actions = byPlace[place].join(", ");
return place ? place + ": " + actions : actions;
}).join("; ") + " (DefSideBoxes)";
editPage(
combinedSummary,
function (pageContent) {
var content = pageContent;
entries.forEach(function (entry) {
content = transformedContent(entry, content);
});
return content;
},
function (data) {
forgetPageContent();
clearAllPreviews();
dsbQueue.entries = [];
box.$list.empty();
box.$status
.removeClass("dsb-status-error")
.addClass("dsb-status-ok")
.text("✓ Hemû guhartin hatin qeydkirin.");
box.$save.prop("disabled", true);
box.$cancel.prop("disabled", false);
// A real edit reloads the page and gets MediaWiki's own "Guhartina te hat weşandin." toast, the
// one a normal save shows. A null edit changed nothing, so the page stays as it is.
if (data && data.newrevid) {
reloadWithPostEditNotice();
}
},
function (message) {
box.$save.prop("disabled", false);
box.$cancel.prop("disabled", false);
box.$status.removeClass("dsb-status-ok").addClass("dsb-status-error").text(message);
}
);
}
// "Kurmancî/Navdêr" -- where a queued change lands, for its summary. The language name comes from the
// shipped list; an unknown code is left as the code itself.
// "Kurmancî/Navdêr", or "Kurmancî/Navdêr 2" where the language has more than one section of that
// type. Without the number a summary on a page with two Navdêr sections named neither of them.
// Numbered only when it disambiguates: on the usual page "Navdêr 1" would just be noise.
function placeLabel(language, sectionType, $h3) {
return langLabel(language) + "/" + sectionLabel(sectionType, $h3);
}
// Bilêvkirin sits above the parts of speech and belongs to the whole language, so naming the section
// it was opened from would say something untrue about what the edit touches.
function subsectionPlaceLabel(subsection, language, sectionType, $h3) {
if (subsection && subsection.free) {
return langLabel(language) + "/" + subsection.header;
}
return subsection && subsection.place === "pron"
? langLabel(language)
: placeLabel(language, sectionType, $h3);
}
function sectionLabel(sectionType, $h3) {
if ($h3 && $h3.length && sections.sectionTypeCountWithinLanguage($h3) > 1) {
return sectionType + " " + sections.sectionOrdinalWithinLanguage($h3);
}
return sectionType;
}
// Capitalised because it opens the summary; Kurdish otherwise writes language names lower-case.
function langLabel(language) {
var names = langs.names();
return util.capitalize(names[language] || language);
}
// The parts of a sense the form actually changed, so the summary lists them instead of saying nothing.
function senseChangeLabels(changes) {
var parts = [];
var changed = changes.changed || {};
if (changes.gloss) {
parts.push("mane");
}
if (changes.newSense) {
parts.push("maneyeke nû");
}
SENSE_GROUPS.forEach(function (group) {
if (changed["word:" + group.key]) {
parts.push(group.label.toLowerCase());
}
});
if (changed.maneid) {
parts.push("id");
}
if (changed.etiket) {
parts.push("etîket");
}
if (changed.kategori) {
parts.push("kategorî");
}
return parts;
}
// Queues transformFn(baseWikitext) -> {content, warnings} for the real save to run later against fresh
// content (see saveQueueNow()); nothing here touches the page yet. Also runs previewFn(), which drops a
// small "not saved yet" preview into the live DOM at the change's own spot on the page. Shared by every
// panel that queues a wikitext change: buildAdvancedPanel, buildSubsectionPanel, buildConvertPosPanel.
// place is what the change is being made to ("Kurmancî/Navdêr 2"), action is what is being done to it.
// Kept apart rather than pre-joined so saveQueueNow() can name a section once and list everything
// queued against it after it, instead of repeating the place before every item.
function queueChange(place, action, transformFn, $save, $status, previewFn, opts) {
opts = opts || {};
var entry = {
key: opts.key || null,
place: place || "",
action: action,
summary: place ? place + ": " + action : action,
transformFn: transformFn,
runLast: !!opts.runLast,
pagePosition: opts.pagePosition || 0,
// Kept, not just run: redrawPreviews() calls it again when another entry leaves the queue.
previewFn: typeof previewFn === "function" ? previewFn : null,
removePreview: null
};
// Queuing the same target twice replaces the pending change rather than stacking a second one --
// two "+Têkildar" entries would both run, and the second would abort on "Ev beş jixwe heye".
var superseded = false;
if (entry.key) {
for (var i = dsbQueue.entries.length - 1; i >= 0; i--) {
if (dsbQueue.entries[i].key === entry.key) {
dropPreview(dsbQueue.entries[i]);
dsbQueue.entries.splice(i, 1);
superseded = true;
}
}
}
dsbQueue.entries.push(entry);
$status.removeClass("dsb-status-error").addClass("dsb-status-ok").text(
"✓ li rêza guhartinan hate zêdekirin — pêl «Guhartinan qeyd bike» bike da ku bi cih bibe."
);
$save.prop("disabled", false);
if (previewFn) {
try {
var removePreview = previewFn();
if (typeof removePreview === "function") {
entry.removePreview = removePreview;
}
} catch (e) { /* a broken preview should never stop the change from being queued */ }
}
// What the superseded entry drew is gone from the queue, so whatever else is drawn from it is stale.
if (superseded) {
dsbQueue.entries.forEach(function (other) {
if (other !== entry) {
dropPreview(other);
}
});
redrawPreviews();
}
renderQueueBox();
}
// Runs text through the ext.gadget.linkify gadget ($.kuLinkify) and calls back with the linkified result.
// Passes the text through unchanged if the gadget can't be loaded, so it never blocks a save.
var linkifyText = util.linkifyText;
// ---- wikitext location ----
//
// One copy for both editing gadgets, in wikt.edit: add-examples writes into the same places.
var langSectionRange = wt.langSectionRange;
var locateSection = wt.locateSection;
var findDefinitionLine = wt.findDefinitionLine;
/**
* The wikitext of one sense's own block: its "#" line plus everything nested under it -- the "#:" word
* lines, the "#*" citations and any sub-senses -- stopping at the next line of the same depth that
* starts a new sense. Parsed on its own that block renders as exactly the <li> the page shows.
*/
function senseBlockWikitext(pageContent, language, sectionType, sectionOrdinal, definitionLevel) {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var defLine = findDefinitionLine(lines, loc.posLineIndex, definitionLevel);
var depth = definitionLevel.length - 1;
var out = [lines[defLine]];
for (var i = defLine + 1; i < lines.length; i++) {
var m = /^(#+)([:*]?)/.exec(lines[i]);
if (!m || m[1].length < depth) {
break;
}
// Same depth and no ":" or "*" after the hashes is the next sense, not part of this one.
if (m[1].length === depth && !m[2]) {
break;
}
out.push(lines[i]);
}
return out.join("\n");
}
// Inserts or extends a "#: {{tplKey|lang|word1|word2}}" sub-line for one sense (start.js's wordLine()
// format). If a line for this tplKey already exists under the definition, missing words are appended to
// it; otherwise a new line lands after the last existing group line, always before any "#*" citation lines.
function insertSenseWords(pageContent, language, sectionType, sectionOrdinal, definitionLevel, tplKey, words, opts) {
opts = opts || {};
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var defLine = findDefinitionLine(lines, loc.posLineIndex, definitionLevel);
var marker = new Array(definitionLevel.length).join("#"); // definitionLevel.length - 1 "#" characters
// Group 1 is the spelling the page uses, group 2 the parameters: an existing {{hevwate}} stays
// {{hevwate}} rather than being silently renamed to {{hevmane}}.
var tplLineRegex = new RegExp("^" + marker + ":\\s*\\{\\{\\s*(" + aliasPattern(tplKey) + ")\\s*\\|([^}]*)\\}\\}\\s*$");
var groupKeysPattern = SENSE_GROUPS.map(function (g) { return aliasPattern(g.key); }).join("|");
var groupLineRegex = new RegExp("^" + marker + ":\\s*\\{\\{\\s*(" + groupKeysPattern + ")\\s*\\|");
var anyBlockLineRegex = new RegExp("^" + marker + "[:*]");
var lastGroupLine = -1;
for (var i = defLine + 1; i < lines.length; i++) {
if (!anyBlockLineRegex.test(lines[i])) {
break;
}
var tplMatch = tplLineRegex.exec(lines[i]);
if (tplMatch) {
var foundKey = tplMatch[1];
if (opts.replace) {
// Editing an existing list: an empty result means the user cleared it, so drop the line.
if (words.length) {
lines[i] = marker + ": {{" + foundKey + "|" + language + "|" + words.join("|") + "}}";
} else {
lines.splice(i, 1);
}
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
var params = tplMatch[2].split("|"); // [lang, word1, word2, ...]
var existingWords = params.slice(1);
words.forEach(function (w) {
if (existingWords.indexOf(w) === -1) {
existingWords.push(w);
}
});
lines[i] = marker + ": {{" + foundKey + "|" + language + "|" + existingWords.join("|") + "}}";
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
if (groupLineRegex.test(lines[i])) {
lastGroupLine = i;
}
}
var insertAt = lastGroupLine >= 0 ? lastGroupLine + 1 : defLine + 1;
lines.splice(insertAt, 0, marker + ": {{" + tplKey + "|" + language + "|" + words.join("|") + "}}");
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
// Canonical left-to-right order of the small templates that can prefix a definition line's gloss text:
// maneid, then kategori (K), then etîket (e).
var LEADING_TPL_ORDER = startData.leadingTplOrder;
// True for a leading call the advanced panel edits in a box of its own -- those are kept out of the
// gloss box so the same template is never editable in two places at once. Redirect spellings count:
// {{f}} and {{etîket}} are {{e}}.
function hasOwnFieldBox(key) {
return LEADING_TPL_ORDER.indexOf(canonicalTemplateKey(key)) !== -1;
}
// Inserts, extends, or (with opts.replace) overwrites one of the small templates that prefix a definition
// line's gloss text -- {{maneid|lang|id}}, {{K|lang|kat1|...}} or {{e|lang|etîket1|...}}. Under
// opts.replace an empty values list removes the call rather than writing an empty one.
function insertLeadingSenseTpl(pageContent, language, sectionType, sectionOrdinal, definitionLevel, tplKey, values, opts) {
opts = opts || {};
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var defLine = findDefinitionLine(lines, loc.posLineIndex, definitionLevel);
var marker = /^#+/.exec(lines[defLine])[0];
var afterMarker = lines[defLine].slice(marker.length).replace(/^\s+/, "");
// Collecting the whole leading-call run means an existing tplKey call is found and merged wherever it
// already sits, not just when it happens to be first.
var split = splitLeadingCalls(afterMarker);
var calls = split.calls;
var rest = split.rest;
var existing = null;
calls.forEach(function (c) {
if (canonicalTemplateKey(c.key) === tplKey) {
existing = c;
}
});
if (existing) {
if (opts.replace) {
// Editing an existing call: an empty result means the user cleared it, so drop the call.
if (values.length) {
existing.params = [language].concat(values);
} else {
calls = calls.filter(function (c) { return c !== existing; });
}
} else {
var existingVals = existing.params.slice(1);
values.forEach(function (v) {
if (existingVals.indexOf(v) === -1) {
existingVals.push(v);
}
});
existing.params = [language].concat(existingVals);
}
} else if (!values.length) {
return pageContent; // nothing to add, and nothing there to clear
} else {
// Not present yet -- insert right before the first existing call that outranks tplKey in
// LEADING_TPL_ORDER (an unrecognized call always outranks everything), or at the end of the run.
var myRank = LEADING_TPL_ORDER.indexOf(tplKey);
var insertAt = calls.length;
for (var i = 0; i < calls.length; i++) {
var otherRank = LEADING_TPL_ORDER.indexOf(canonicalTemplateKey(calls[i].key));
if (otherRank === -1 || otherRank > myRank) {
insertAt = i;
break;
}
}
calls.splice(insertAt, 0, { key: tplKey, params: [language].concat(values) });
}
var rebuilt = calls.map(function (c) {
return "{{" + c.key + "|" + c.params.join("|") + "}}";
}).join(" ");
lines[defLine] = marker + " " + (rebuilt ? rebuilt + " " : "") + rest;
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
// Replaces just the gloss text of a definition line, leaving any leading template calls untouched --
// the save half of "Maneyê biguhêre" (edit sense).
function updateSenseGloss(pageContent, language, sectionType, sectionOrdinal, definitionLevel, newGloss) {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var defLine = findDefinitionLine(lines, loc.posLineIndex, definitionLevel);
var marker = /^#+/.exec(lines[defLine])[0];
var afterMarker = lines[defLine].slice(marker.length).replace(/^\s+/, "");
// The box holds everything except the calls with their own field, so those are put back in front.
// Dropping "{{mane?}}" here is what clears start.js's placeholder once a real gloss is written.
var kept = splitLeadingCalls(afterMarker).calls.filter(function (c) {
return hasOwnFieldBox(c.key);
});
var leadingText = kept.map(function (c) {
return "{{" + c.key + "|" + c.params.join("|") + "}}";
}).join(" ");
lines[defLine] = marker + " " + (leadingText ? leadingText + " " : "") + newGloss;
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
// Deletes one sense: its definition line plus everything indented under it (its "#:" groups, "#*"
// citations, any sub-senses). Refuses to empty a section, since a POS heading with no senses is worse
// than the sense the user wanted gone.
// A new sense goes directly below the one whose pencil asked for it -- after the lines that sense owns
// (its "#:" examples, "#*" citations and "##" sub-senses), so it reads as the next sense rather than
// landing at the foot of the section. It takes the same marker depth as its neighbour, so asking from
// a "##" sub-sense adds a sibling sub-sense.
function insertNewSense(pageContent, language, sectionType, sectionOrdinal, definitionLevel, text) {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var defLine = findDefinitionLine(lines, loc.posLineIndex, definitionLevel);
var marker = /^#+/.exec(lines[defLine])[0];
var end = defLine + 1;
var ownedRegex = new RegExp("^" + marker + "[#:*]");
while (end < lines.length && ownedRegex.test(lines[end])) {
end++;
}
lines.splice(end, 0, marker + " " + text);
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
function removeSense(pageContent, language, sectionType, sectionOrdinal, definitionLevel) {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var defLine = findDefinitionLine(lines, loc.posLineIndex, definitionLevel);
var marker = /^#+/.exec(lines[defLine])[0];
var end = defLine + 1;
var ownedRegex = new RegExp("^" + marker + "[#:*]");
while (end < lines.length && ownedRegex.test(lines[end])) {
end++;
}
// Top-level senses of this section only -- "##" sub-senses go with their parent either way.
if (definitionLevel.length === 2) {
var siblings = 0;
for (var i = loc.posLineIndex + 1; i < lines.length && !/^==/.test(lines[i]); i++) {
if (/^#[^*#:]/.test(lines[i])) {
siblings++;
}
}
if (siblings < 2) {
throw new SubmitAbort("Ev maneya dawî ya vê beşê ye. Ji bo rakirinê divê tu tevahiya beşê rakî.");
}
}
lines.splice(defLine, end - defLine);
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
// Fetches the page's current wikitext and pulls out just this sense's gloss text to prefill the "Maneyê
// biguhêre" form. A plain read, not part of the hardened save path -- the actual save still re-fetches
// fresh content, so only the prefilled text itself could be a little stale.
// Current contents of one existing subsection, for prefilling the edit form.
// This page's current wikitext.
// One fetch for the whole queue: every preview reads the same revision, and asking per preview meant
// downloading the page again for each queued change. Dropped after a save, when the page has moved on.
var pageContentPromise = null;
function forgetPageContent() {
pageContentPromise = null;
}
function fetchPageContent() {
if (pageContentPromise) {
return pageContentPromise;
}
pageContentPromise = rawPageContent();
// A failed read must not be remembered as the answer.
pageContentPromise.fail(forgetPageContent);
return pageContentPromise;
}
function rawPageContent() {
return api.get({
action: "query",
prop: "revisions",
rvprop: "content",
titles: mw.config.get("wgPageName"),
formatversion: 2
}).then(function (data) {
var page = data.query && data.query.pages && data.query.pages[0];
var content = page && !page.missing && page.revisions && page.revisions[0] && page.revisions[0].content;
if (!content) {
throw new SubmitAbort("Nikare muhtewaya rûpelê bixwîne.");
}
return content;
});
}
function fetchPageSubsection(language, sectionType, sectionOrdinal, subsection) {
return fetchPageContent().then(function (content) {
return readPageSubsection(content, language, sectionType, sectionOrdinal, subsection);
});
}
function fetchSenseFields(language, sectionType, sectionOrdinal, definitionLevel) {
return fetchPageContent().then(function (content) {
return readSenseFields(content, language, sectionType, sectionOrdinal, definitionLevel);
});
}
// Everything the advanced panel can edit about one sense, read off its definition line and the "#:" lines
// under it: the raw gloss (templates included), the leading maneid/K/e values, and each word-list group.
function readSenseFields(pageContent, language, sectionType, sectionOrdinal, definitionLevel) {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var defLine = findDefinitionLine(lines, loc.posLineIndex, definitionLevel);
var defMarker = /^#+/.exec(lines[defLine])[0];
var afterMarker = lines[defLine].slice(defMarker.length).replace(/^\s+/, "");
var leading = {};
splitLeadingCalls(afterMarker).calls.forEach(function (c) {
leading[canonicalTemplateKey(c.key)] = c.params.slice(1);
});
// Only start.js's "{{mane?|lang}}" placeholder is hidden; every other template stays visible so it can
// be edited by hand in the gloss box.
// The gloss box shows the line minus the calls that have their own box ({{maneid}}, {{K}}, {{e}} and
// their redirects) and minus start.js's "{{mane?}}" placeholder. Everything else -- a {{g}} link, a
// {{guharto}} -- stays visible and editable there.
var split = splitLeadingCalls(afterMarker);
var glossCalls = split.calls.filter(function (c) {
return !hasOwnFieldBox(c.key) && canonicalTemplateKey(c.key) !== "mane?";
});
var glossLead = glossCalls.map(function (c) {
return "{{" + c.key + "|" + c.params.join("|") + "}}";
}).join(" ");
var gloss = $.trim((glossLead ? glossLead + " " : "") + split.rest);
var marker = new Array(definitionLevel.length).join("#");
var groupKeysPattern = SENSE_GROUPS.map(function (g) { return aliasPattern(g.key); }).join("|");
var groupLineRegex = new RegExp("^" + marker + ":\\s*\\{\\{\\s*(" + groupKeysPattern + ")\\s*\\|([^}]*)\\}\\}\\s*$");
var anyBlockLineRegex = new RegExp("^" + marker + "[:*]");
var words = {};
for (var i = defLine + 1; i < lines.length; i++) {
if (!anyBlockLineRegex.test(lines[i])) {
break;
}
var m = groupLineRegex.exec(lines[i]);
if (m) {
words[canonicalTemplateKey(m[1])] = m[2].split("|").slice(1);
}
}
return {
gloss: gloss,
maneid: leading.maneid ? leading.maneid[0] : "",
etiket: leading.e || [],
kategori: leading.K || [],
words: words
};
}
// Finds a POS block's body range (up to, but not including, the next heading of level <= 3), and the
// header text + line index of any "====" subsections it already contains.
// depth is the part-of-speech heading's own ("===" on most entries, "====" where the senses are
// grouped under numbered etymologies). Its block runs to the next heading at that depth or shallower,
// and its subsections are the headings one deeper.
function posBlockBounds(lines, posLineIndex, depth) {
depth = depth || 3;
var blockEnd = wt.headingAtOrAbove(depth);
var subsectionHeading = wt.headingAtDepth(depth + 1);
var end = lines.length;
var existingHeaders = [];
for (var i = posLineIndex + 1; i < lines.length; i++) {
if (blockEnd.test(lines[i])) {
end = i;
break;
}
var sub = subsectionHeading.exec(lines[i]);
if (sub) {
existingHeaders.push({ header: $.trim(sub[1]), lineIndex: i });
}
}
return { end: end, existingHeaders: existingHeaders, depth: depth };
}
// Looks up a header's rank in SUBSECTION_ORDER. -1 for a header that isn't one of ours; such a header is
// never used as an insertion anchor.
function subsectionRankForHeader(header) {
return SUBSECTION_ORDER.indexOf(header);
}
// Body line range of one "====" subsection inside a POS block: everything after its heading up to the next
// "====" heading, or the end of the block. Null if the block has no such subsection.
function subsectionBodyRange(bounds, header) {
var headers = bounds.existingHeaders;
for (var i = 0; i < headers.length; i++) {
if (headers[i].header === header) {
return {
start: headers[i].lineIndex + 1,
end: i + 1 < headers.length ? headers[i + 1].lineIndex : bounds.end
};
}
}
return null;
}
// ---- template-parameter editing -------------------------------------------------------------------
// A gloss that is nothing but one template call gets a labelled box per parameter instead of a raw
// wikitext line, the way VisualEditor's template dialog works.
// The single template call a gloss is made of, or null when the line is anything less tidy. {{g|...}} is a
// link inside the gloss rather than the gloss itself, so it neither disqualifies the line nor gets a form.
function glossTemplateTarget(gloss) {
var parsed = parseTemplateCalls(gloss || "");
if (parsed.otherContent) {
return null;
}
var editable = parsed.calls.filter(function (c) {
return canonicalTemplateKey(c.name) !== "g";
});
return editable.length === 1 ? editable[0] : null;
}
// ---- the three places that carry no "====" heading of their own ----------------------------------
// Bilêvkirin is a "===" section above the POS blocks, images sit at the top of a POS block, and the
// h-bnr hatnote goes above the first language heading. Each is located here and then read, written or
// inserted through the same three lines of splice, so nothing below needs to know about them.
var IMAGE_LINE = /^\[\[\s*(?:Wêne|File|Image|Dosya)\s*:/i;
/**
* {lines, langSectionIndex, exists, start, end, insertAt} for a subsection carrying a "place", or null
* for an ordinary one. start..end is the body as it stands; insertAt is where a missing one goes.
*/
function locateLoosePlace(pageContent, language, sectionType, sectionOrdinal, subsection) {
var place = subsection.place;
if (!place) {
return null;
}
var i, end;
if (place === "page-top") {
// Above every language section, so the whole page is the search space. The call is found under
// any of its names, and its own name is carried back so a save never renames it.
var pageLines = pageContent.split("\n");
var call = wt.findHbnrCall(pageContent);
if (call) {
return { lines: pageLines, langSectionIndex: 0, exists: true, call: call,
start: call.lineIndex, end: call.lineIndex + 1, insertAt: call.lineIndex };
}
return { lines: pageLines, langSectionIndex: 0, exists: false, start: 0, end: 0, insertAt: 0 };
}
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
if (place === "pron") {
// One per language, not one per part of speech, so it is looked for above the POS heading.
for (i = 0; i < loc.posLineIndex; i++) {
var h3 = /^===\s*([^=]+?)\s*===/.exec(lines[i]);
if (h3 && $.trim(h3[1]) === subsection.header) {
end = i + 1;
while (end < lines.length && !/^==/.test(lines[end])) {
end++;
}
return { lines: lines, langSectionIndex: loc.langSectionIndex, exists: true,
start: i + 1, end: end, insertAt: i };
}
}
// Straight after the language heading, which puts it before every POS block.
return { lines: lines, langSectionIndex: loc.langSectionIndex, exists: false,
start: 1, end: 1, insertAt: 1 };
}
if (place === "pos-top") {
// Between the conjugation line and the headword template -- where the wizard writes them.
var at = loc.posLineIndex + 1;
while (at < lines.length && /^\{\{\s*ku-tewîn/.test($.trim(lines[at]))) {
at++;
}
var from = at;
while (at < lines.length && IMAGE_LINE.test($.trim(lines[at]))) {
at++;
}
return { lines: lines, langSectionIndex: loc.langSectionIndex, exists: at > from,
start: from, end: at, insertAt: from };
}
return null;
}
// The wikitext a "place" section's content becomes. A word list there is one template call, not a
// {{kol3}} block, so the words are joined into it -- and any ⊕ modifier a row picked up is dropped,
// since a page name is all the call can carry.
function loosePlaceBody(subsection, content, language) {
if (subsection.kind === "images") {
return $.grep($.map(content || [], function (image) {
return wt.buildImageLine({
file: image.file,
caption: wt.captionForLang(image.caption, language),
alt: image.alt,
size: image.size,
extra: image.extra
});
}), Boolean).join("\n");
}
return content;
}
// The words a "place" word list holds, flattened out of its single group. Any ⊕ modifier a row picked
// up is dropped: a page name is all the call can carry.
function loosePlaceWords(content) {
var words = [];
$.each(content || [], function (_, group) {
$.each(group.words || [], function (_, raw) {
var plain = $.trim(util.parseWordValue(raw).word);
if (plain && $.inArray(plain, words) === -1) {
words.push(plain);
}
});
});
return words;
}
// Writes one of those places, inserting it when it is not there yet and replacing it when it is. Only
// Bilêvkirin carries a heading; the other two are bare lines.
function writeLoosePlace(pageContent, language, sectionType, sectionOrdinal, subsection, content) {
var at = locateLoosePlace(pageContent, language, sectionType, sectionOrdinal, subsection);
if (!at) {
throw new SubmitAbort("Cih nehate dîtin: " + subsection.header);
}
// A call naming a subpage -- "{{h-bnr/-ane}}" -- holds a list shared by every page transcluding
// it. Writing here would change this page alone and desync the rest, so the edit is refused and
// the template that actually owns the list is named instead.
if (at.call && at.call.subpage) {
throw new SubmitAbort("Ev rûpel «{{" + at.call.name + "}}» bi kar tîne — lîste li " +
"[[Şablon:" + at.call.subpage + "]] ye, lê wê zêde bike, ne li vê rûpelê.");
}
// A word list here is one template call, so it is built where the existing call's own name is
// known -- editing "{{bnr-h|...}}" must not turn it into "{{h-bnr|...}}".
var body = subsection.kind === "wordlist"
? wt.buildHbnrCall(loosePlaceWords(content), at.call && at.call.name)
: $.trim(content || "");
// A "===" section is separated from what is above and below it by a blank line, the way the rest
// of an entry is written. The other two places are bare lines inside a block and take none.
var heading = subsection.place === "pron";
function blank(index) {
return $.trim(at.lines[index] || "") === "";
}
if (at.exists) {
if (body) {
var replacement = [body];
// end is the next "==" heading; the blank line before it is inside the range being replaced.
if (heading && /^==/.test(at.lines[at.end] || "")) {
replacement.push("");
}
at.lines.splice.apply(at.lines, [at.start, at.end - at.start].concat(replacement));
} else {
// Emptying the box removes the section rather than leaving an empty heading behind.
at.lines.splice(at.start, at.end - at.start);
if (heading) {
at.lines.splice(at.insertAt, 1);
// The blank line that separated it goes with it, rather than doubling up.
if (blank(at.insertAt) && blank(at.insertAt - 1)) {
at.lines.splice(at.insertAt, 1);
}
}
}
} else {
if (!body) {
throw new SubmitAbort("Tiştek nehate nivîsandin.");
}
var block = heading ? ["=== " + subsection.header + " ===", body] : [body];
if (heading) {
if (!blank(at.insertAt - 1)) {
block.unshift("");
}
if (!blank(at.insertAt)) {
block.push("");
}
}
at.lines.splice.apply(at.lines, [at.insertAt, 0].concat(block));
}
return pageContent.slice(0, at.langSectionIndex) + at.lines.join("\n");
}
// What a section body holds: its {{kol3}} groups, the plain bullet list that stands in for them, or
// the raw wikitext when it is neither. A free section is read this way before its kind is known.
function readSubsectionBody(body, subsection) {
if (subsection.kind === "wordlist" || subsection.free) {
var calls = parseWordListCalls(body);
if (calls.length) {
return {
kind: "wordlist",
groups: calls.map(function (call) {
return { sernav: namedParam(call.named, "sernav"), words: call.words };
})
};
}
var bullets = parseBulletWordList(body);
if (bullets) {
// Editable as a word list; saving rewrites the bullets as {{kol3}}.
return { kind: "wordlist", groups: [{ sernav: "", words: bullets }], fromBullets: true };
}
}
return { kind: "text", text: $.trim(body) };
}
// The wikitext a section body becomes after an edit. A wordlist keeps its own template name, language
// and named params ("cure=Jê", ...) and only has its positional words replaced; a text one is
// replaced whole.
function rewriteSubsectionBody(body, language, subsection, content) {
if (subsection.kind !== "wordlist") {
// Replacing the body wholesale drops that separator, so put it back.
return content + (/\n\s*$/.test(body) ? "\n" : "");
}
var calls = parseWordListCalls(body);
if (calls.length) {
// Each call is rewritten where it stands, so text between and around them survives untouched.
// Last to first, so the offsets of the earlier ones stay valid as we go.
var replacement = body;
for (var gi = calls.length - 1; gi >= 0; gi--) {
var call = calls[gi];
var group = content[gi];
// A group emptied in the form drops its call entirely rather than leaving "{{kol3|ku}}".
var rebuilt = group && group.words.length
? buildWordListCall(call.name, call.lang, withNamedParam(call.named, "sernav", group.sernav), group.words)
: "";
replacement = replacement.slice(0, call.start) + rebuilt + replacement.slice(call.end);
}
// Groups the form added beyond the ones already there go after the last of them.
var added = content.slice(calls.length).filter(function (group) {
return group.words.length;
}).map(function (group) {
return buildWordListCall(WORDLIST_TEMPLATE, language,
withNamedParam(["cure=" + subsection.header], "sernav", group.sernav), group.words);
});
if (added.length) {
replacement = replacement.replace(/\s*$/, "") + "\n\n" + added.join("\n\n") + "\n";
}
return replacement.replace(/\n{3,}/g, "\n\n"); // gaps left by any dropped call
}
if (parseBulletWordList(body)) {
// A plain bullet list is replaced wholesale by a real {{kol3}} on its first edit.
return content.map(function (group) {
return buildWordListCall(WORDLIST_TEMPLATE, language,
withNamedParam(["cure=" + subsection.header], "sernav", group.sernav), group.words);
}).join("\n\n") + (/\n\s*$/.test(body) ? "\n" : "");
}
throw new SubmitAbort("Nikare lîsteya vê beşê bixwîne: " + subsection.header);
}
// ---- sections that are not subsections of a part of speech ----
// A "=== Etîmolojî ===" that the parts of speech hang under, a "=== Çavkanî ===" at the foot of a
// language, a heading nobody planned for. Located by its own text, and edited as the wikitext under
// it -- or as a {{kol3}} list when that is what it holds.
function freeSectionBody(pageContent, language, subsection) {
var loc = wt.locateHeadingByText(pageContent, language, subsection.header, subsection.free.ordinal);
return { loc: loc, body: loc.lines.slice(loc.bodyStart, loc.bodyEnd).join("\n") };
}
function readFreeSection(pageContent, language, subsection) {
var at = freeSectionBody(pageContent, language, subsection);
return readSubsectionBody(at.body, subsection);
}
function updateFreeSection(pageContent, language, subsection, content) {
var at = freeSectionBody(pageContent, language, subsection);
at.loc.lines.splice(at.loc.bodyStart, at.loc.bodyEnd - at.loc.bodyStart,
rewriteSubsectionBody(at.body, language, subsection, content));
return pageContent.slice(0, at.loc.langSectionIndex) + at.loc.lines.join("\n");
}
// Current contents of an existing "====" subsection. A wordlist one whose template can't be parsed comes
// back as {kind: "text"} so the caller can still edit it as raw wikitext instead of refusing outright.
function readPageSubsection(pageContent, language, sectionType, sectionOrdinal, subsection) {
if (subsection.free) {
return readFreeSection(pageContent, language, subsection);
}
var loose = locateLoosePlace(pageContent, language, sectionType, sectionOrdinal, subsection);
if (loose) {
if (!loose.exists) {
return null;
}
if (loose.call) {
return { kind: "wordlist", groups: [{ sernav: "", words: loose.call.words }] };
}
var looseBody = $.trim(loose.lines.slice(loose.start, loose.end).join("\n"));
if (subsection.kind === "images") {
return {
kind: "images",
images: $.grep($.map(looseBody.split("\n"), wt.parseImageLine), Boolean)
};
}
if (subsection.kind !== "wordlist") {
return { kind: "text", text: looseBody };
}
// One template call rather than a {{kol3}} block: its positional parameters are the words.
var looseCall = parseTemplateCalls(looseBody).calls[0];
return { kind: "wordlist", groups: [{ sernav: "", words: (looseCall && looseCall.params) || [] }] };
}
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var bounds = posBlockBounds(loc.lines, loc.posLineIndex, loc.depth);
var range = subsectionBodyRange(bounds, subsection.header);
if (!range) {
return null;
}
return readSubsectionBody(loc.lines.slice(range.start, range.end).join("\n"), subsection);
}
// Rewrites an existing "====" subsection in place. A wordlist one keeps its own template name, language and
// named params ("cure=Jê", ...) and only has its positional words replaced; a text one is replaced whole.
function updatePageSubsection(pageContent, language, sectionType, sectionOrdinal, subsection, content) {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var bounds = posBlockBounds(lines, loc.posLineIndex, loc.depth);
var range = subsectionBodyRange(bounds, subsection.header);
if (!range) {
throw new SubmitAbort(util.msg.sectionMissing + subsection.header);
}
var body = lines.slice(range.start, range.end).join("\n");
lines.splice(range.start, range.end - range.start,
rewriteSubsectionBody(body, language, subsection, content));
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
// Inserts a new "==== Header ====" subsection into its SUBSECTION_ORDER position within a POS block, right
// before the first ranked subsection that outranks it, or at the end if none does. Either a
// {{kol3|...}} word list or a plain paragraph, matching start.js's own subsection formats.
function insertPageSubsection(pageContent, language, sectionType, sectionOrdinal, subsection, content) {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var bounds = posBlockBounds(lines, loc.posLineIndex, loc.depth);
var alreadyThere = false;
bounds.existingHeaders.forEach(function (h) {
if (h.header === subsection.header) {
alreadyThere = true;
}
});
if (alreadyThere) {
throw new SubmitAbort("Ev beş jixwe heye: " + subsection.header);
}
var body = subsection.kind === "wordlist"
? content.map(function (group) {
return buildWordListCall(WORDLIST_TEMPLATE, language,
withNamedParam(["cure=" + subsection.header], "sernav", group.sernav), group.words);
}).join("\n\n")
: content;
var myRank = subsectionRankForHeader(subsection.header);
var headers = bounds.existingHeaders;
var insertAt = bounds.end;
for (var i = 0; i < headers.length; i++) {
var otherRank = subsectionRankForHeader(headers[i].header);
if (otherRank !== -1 && otherRank > myRank) {
insertAt = headers[i].lineIndex;
break;
}
}
// Exactly one blank line on each side: the line before the insertion point is normally already blank
// (it separated the previous subsection from the next heading), so only add one where there isn't one.
var block = [];
if (insertAt > 0 && $.trim(lines[insertAt - 1]) !== "") {
block.push("");
}
var marks = wt.headingMarks(bounds.depth + 1);
block.push(marks + " " + subsection.header + " " + marks, body);
if (insertAt < lines.length) {
block.push("");
}
lines.splice.apply(lines, [insertAt, 0].concat(block));
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
// ---- converting an unclassified "=== Mane ===" placeholder heading into a real POS heading ----
// Some pages carry a heading literally named "Mane" (optionally "Mane 2" etc.) instead of a real
// part-of-speech name, tagged right below with {{cure?|<lang>}} (ku.wiktionary's "part of speech not yet
// determined" maintenance flag). It is a section key of its own (sections.MANE_SECTION), so the shared
// locator finds it like any other heading; only the rewriting below is particular to it.
// Renames the "=== Mane ===" placeholder to the chosen POS heading, drops the maintenance tag line right
// after it if present (MANE_TAG_REGEX -- cure? or any of its known aliases), and inserts the matching
// "{{<pos>|<lang>}}" call in its place (skipped when the section has no template of its own).
// Other content under the heading is left untouched.
/**
* Where a section's own headword template sits, as {index, text}, or {index: -1} when it has none.
*
* Not always the line under the heading: a "{{ku-tewîn-lk}}" line and any number of "[[Wêne:...]]"
* images can come first. Everything down to the first definition line is searched, and index doubles
* as the slot a new call belongs in when there is no old one.
*/
function findPosTemplateLine(lines, headingLine, sectionType) {
var oldSpec = posTemplateFor(sectionType);
var oldTemplate = oldSpec ? oldSpec.name : (sectionType === "Biwêj" ? "biwêj" : null);
var oldTemplateRegex = oldTemplate &&
new RegExp("^\\{\\{\\s*" + escapeRegExp(oldTemplate) + "\\s*[|}]");
var at = headingLine + 1;
while (at < lines.length && !/^#/.test(lines[at]) && !/^==/.test(lines[at])) {
if (MANE_TAG_REGEX.test(lines[at]) ||
(oldTemplateRegex && oldTemplateRegex.test(lines[at]))) {
return { index: at, text: lines[at], end: at };
}
at++;
}
return { index: -1, text: "", end: at };
}
// Named parameters merged in order, each written once: a later value replaces an earlier one of the
// same name. Positional values are kept as they come.
function mergeCallParams() {
var out = [];
var seen = {};
$.each(Array.prototype.slice.call(arguments), function (_, list) {
$.each(list || [], function (__, param) {
var m = /^\s*([^=|]+?)\s*=([\s\S]*)$/.exec(param);
if (!m) {
out.push(param);
return;
}
// "z" and "c" are one name to Modul:rêziman, so the form's "c=m" meets the page's "z=".
var key = wt.canonicalParamName(m[1]);
if (!Object.prototype.hasOwnProperty.call(seen, key)) {
seen[key] = out.length;
out.push(param);
return;
}
// The page's own spelling stays when it carried a value; an empty one gives way entirely.
var earlier = /^\s*([^=|]+?)\s*=([\s\S]*)$/.exec(out[seen[key]]);
// "?" asks for a value, so it never replaces one the page already has.
if ($.trim(earlier[2]) && $.trim(m[2]) === "?") {
return;
}
out[seen[key]] =$.trim(earlier[2]) ? earlier[1] + "=" + m[2] : param;
});
});
return out;
}
// What the call on the page already says, minus its language. Carried over only when the new section
// uses that same template -- "z=m" survives Navdêr becoming Formeke navdêrê, while a verb's "c=h-gh"
// has no meaning on a noun and is left behind.
function keptCallParams(line, templateName) {
var call = parseTemplateCalls($.trim(line || "")).calls[0];
if (!call || canonicalTemplateKey(call.name) !== canonicalTemplateKey(templateName)) {
return [];
}
return call.params.slice(1).filter(function (param) {
return /=/.test(param);
});
}
// The headword call a converted section gets, or "" when the new section has none. What the old call
// (found, from findPosTemplateLine) said keeps its place, then the section's own defaults, then what the
// form was filled in with -- each name written once, the last value winning, so a converted call reads
// as small a change from the old one as it can.
function posCallLine(found, language, newSectionType, posParams) {
var spec = posTemplateFor(newSectionType);
if (!spec) {
return "";
}
var kept = found.index !== -1 ? keptCallParams(found.text, spec.name) : [];
// A lemma is not an inflected form, so the old section's form=erê stays behind.
if (spec.params.indexOf("form=erê") === -1) {
kept = kept.filter(function (param) {
return !/^\s*form\s*=\s*erê\s*$/.test(param);
});
}
var params = mergeCallParams(kept, spec.params, posParams ? posParams.split("|") : []);
return "{{" + spec.name + "|" + language + (params.length ? "|" + params.join("|") : "") + "}}";
}
function convertSectionToPos(pageContent, language, sectionType, sectionOrdinal, newSectionType, posParams) {
if (!sectionNames[newSectionType]) {
throw new SubmitAbort("Navê cureyê rêzimanî yê hilbijartî nediyar e.");
}
// locateSection() already handles the "Mane" placeholder as a section type of its own.
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var headingLine = loc.posLineIndex;
var depth = loc.depth;
// A section straight under a numbered group heading takes that heading's place: the group heading
// held nothing but its gloss.
var group = depth > 3 ? wt.numberedGroupAbove(lines, headingLine, depth) : -1;
if (group !== -1) {
lines.splice(group, headingLine - group);
headingLine = group;
depth--;
}
// At the heading's own depth. Written without a number: the renumbering pass below decides every
// number in this language section at once, so one carried over from the old name would just be
// overwritten.
var marks = wt.headingMarks(depth);
lines[headingLine] = marks + " " + newSectionType + " " + marks;
// The line under the heading is either the old section's own template call ("{{biwêj|ku}}") or, for a
// Mane placeholder, the "needs a POS" maintenance tag. Both are replaced by the new POS's call.
// The new call takes the old one's own place -- or, when there was none, the slot just above the
// definitions, which is where the wizard writes it.
var found = findPosTemplateLine(lines, headingLine, sectionType);
var insertAt = found.index !== -1 ? found.index : found.end;
if (found.index !== -1) {
lines.splice(found.index, 1);
}
var call = posCallLine(found, language, newSectionType, posParams);
if (call) {
lines.splice(insertAt, 0, call);
}
// Renaming a section can make it repeat one the page already had, and can equally leave a name that
// is no longer repeated. Both are settled by renumbering the whole language section: a "Rengdêr"
// beside a new "Rengdêr" becomes "Rengdêr 1" and "Rengdêr 2", and a number left behind by a name
// that is now unique is dropped.
var body = lines.join("\n");
var nextLang = body.indexOf("\n== ", 1);
var end = nextLang === -1 ? body.length : nextLang;
body = wt.numberRepeatedPosHeadings(body.slice(0, end)) + body.slice(end);
return pageContent.slice(0, loc.langSectionIndex) + body;
}
// ---- "guhartina pêşketî" (advanced editing): combine several independent per-sense edits into ONE save ----
// Threads pageContent through the same pure functions every single-field form uses, applying only the
// pieces the caller actually filled in. A piece that conflicts with the page (maneid already set, a
// subsection that's already there) is skipped with a warning rather than failing the whole save. Throws
// SubmitAbort if nothing was filled in, so this can never turn into a no-op save.
function applyAdvancedChanges(pageContent, language, sectionType, sectionOrdinal, definitionLevel, changes) {
var content = pageContent;
var warnings = [];
var appliedAnything = false;
if (changes.gloss) {
content = updateSenseGloss(content, language, sectionType, sectionOrdinal, definitionLevel, changes.gloss);
appliedAnything = true;
}
if (changes.newSense) {
content = insertNewSense(content, language, sectionType, sectionOrdinal, definitionLevel, changes.newSense);
appliedAnything = true;
}
// Every box below is prefilled with what the sense already has, so a save replaces that value rather
// than merging into it -- and clearing a box removes the template. changes.changed marks the boxes
// whose contents actually differ from the page, so an opened-but-untouched box writes nothing.
var replace = { replace: true };
if (changes.changed.maneid) {
content = insertLeadingSenseTpl(content, language, sectionType, sectionOrdinal, definitionLevel, "maneid", changes.maneid ? [changes.maneid] : [], replace);
appliedAnything = true;
}
if (changes.changed.etiket) {
content = insertLeadingSenseTpl(content, language, sectionType, sectionOrdinal, definitionLevel, "e", changes.etiket, replace);
appliedAnything = true;
}
if (changes.changed.kategori) {
content = insertLeadingSenseTpl(content, language, sectionType, sectionOrdinal, definitionLevel, "K", changes.kategori, replace);
appliedAnything = true;
}
SENSE_GROUPS.forEach(function (group) {
if (changes.changed["word:" + group.key]) {
content = insertSenseWords(content, language, sectionType, sectionOrdinal, definitionLevel, group.key, changes.words[group.key], replace);
appliedAnything = true;
}
});
if (!appliedAnything) {
throw new SubmitAbort("Tiştek nehat guhartin.");
}
return { content: content, warnings: warnings };
}
// ---- per-definition UI: a small always-visible "+" toggle (no hover, so it works on mobile/touch too) ----
// A growing list of word rows. The row itself is util.js's, shared with the wizard; this owns the list:
// typing in the last row appends a fresh empty one, so there is always somewhere to type.
var WORDLIST_COLUMN_THRESHOLD = 6;
function buildWordListField(placeholder, opts) {
opts = opts || {};
var $list = $("<ol class='dsb-wordlist'></ol>");
// One column is a long scroll once a list gets past a handful of words. The class only asks for
// two; the stylesheet still gives one on a narrow screen. Two columns inside the panel's usual
// 520px would leave each word about 250px, so the panel drops its width cap for as long as any
// list in it is in two columns -- checked across the whole panel, since a "Herwiha binêre" holds
// several groups and one of them going wide settles it for all.
function updateColumns() {
$list.toggleClass("dsb-wordlist-2col",
$list.children("li").length > WORDLIST_COLUMN_THRESHOLD);
var $panel = $list.closest(".dsb-subsection-panel");
$panel.toggleClass("dsb-panel-wide", !!$panel.find(".dsb-wordlist-2col").length);
}
function makeRow() {
var row = util.makeModifierRow({
prefix: "dsb-",
placeholder: placeholder,
modTitle: util.msg.addModifier,
removeTitle: util.msg.removeWord,
removeModTitle: util.msg.removeModifier,
modifiers: opts.modifiers,
// "bixe qutiyan" puts each comma-separated word in a row of its own, right after the row it
// was typed in, and the caret follows to the last of them. One word per row is also one
// word per template parameter, which both {{hevmane}} and {{kol3}} read the same way --
// a comma left inside a parameter would not be.
onSplit: function (rest) {
var $after = row.$li;
var last = null;
rest.forEach(function (raw) {
var extra = makeRow();
$after.after(extra.$li);
$after = extra.$li;
if (raw) extra.setRaw(raw);
last = extra;
});
// Always an empty row at the end to type the next word into.
var $tail = $list.children("li").last();
if ($.trim($tail.data("row").getRaw())) {
last = makeRow();
$list.append(last.$li);
}
updateColumns();
if (last) last.focus();
},
// raw === null means the row removed itself, so the count has changed either way.
onChange: function (raw) {
if (raw !== null) {
var isLast = row.$li.is($list.children("li").last());
if (isLast && raw) $list.append(makeRow().$li);
}
updateColumns();
}
});
row.$li.data("row", row);
return row;
}
$list.append(makeRow().$li);
return {
$element: $list,
values: function () {
var vals = [];
$list.children("li").each(function () {
var raw = $(this).data("row").getRaw();
if (raw) vals.push(raw);
});
return vals;
},
// Replaces the whole list, keeping one empty trailing row to type into.
setValues: function (raws) {
$list.empty();
raws.forEach(function (raw) {
var row = makeRow();
$list.append(row.$li);
row.setRaw(raw);
});
$list.append(makeRow().$li);
updateColumns();
},
focus: function () {
$list.children("li").first().data("row").focus();
}
};
}
// Plain removable-tag field, used for Etîket and Kategorî (simple value lists with no per-word modifiers).
// Enter or "," commits the current text as a tag; Backspace on an empty input removes the last tag;
// values() also commits whatever is still typed but not yet confirmed.
function buildTagField(placeholder) {
var $input = $("<input type='text' class='dsb-input dsb-tag-input'>").attr("placeholder", placeholder);
var $element = $("<span class='dsb-tagfield'></span>").append($input);
function currentValues() {
var vals = [];
$element.children(".dsb-tag").each(function () {
vals.push($(this).data("value"));
});
return vals;
}
function addTag(val) {
val = $.trim(val || "");
if (!val || currentValues().indexOf(val) !== -1) {
return;
}
var $tag = $("<span class='dsb-tag'></span>").text(val).data("value", val);
var $remove = $("<button type='button' class='dsb-tag-remove' title='Jê bibe'>×</button>");
$remove.on("click", function () {
$tag.remove();
$input.trigger("focus");
});
$tag.append($remove);
$tag.insertBefore($input);
}
$input.on("keydown", function (e) {
if (e.which === 13 || e.which === 188) { // Enter or comma
e.preventDefault();
addTag($input.val());
$input.val("");
} else if (e.which === 8 && !$input.val()) {
$element.children(".dsb-tag").last().remove();
}
});
$input.on("blur", function () {
addTag($input.val());
$input.val("");
});
return {
$element: $element,
// Replaces every tag, for prefilling from what the sense already has.
setValues: function (vals) {
$element.children(".dsb-tag").remove();
vals.forEach(function (v) {
addTag(v);
});
$input.val("");
},
values: function () {
addTag($input.val());
$input.val("");
return currentValues();
},
focus: function () {
$input.trigger("focus");
}
};
}
// The wizard's own etymology templates, straight out of data-main.json's "etymTpls" -- the same list start.js's
// insert menu offers, so the two can never drift apart. CODE/SOURCELANG are its placeholders.
// The wizard's own template lists, straight out of data-main.json, so both gadgets offer the same
// templates in the same order. A pronunciation template names the languages it is for; one that names
// none is offered everywhere.
function templateChoices(pool, language) {
var defs = startData[pool] || {};
return Object.keys(defs).filter(function (name) {
var langs = defs[name].langs;
return !langs || !langs.length || $.inArray(language, langs) !== -1;
}).map(function (name) {
return { name: name, desc: defs[name].desc, snippet: defs[name].snippet };
});
}
// One template call inside the surface: shown as its own wikitext, but atomic -- contenteditable="false"
// keeps the caret out of it, so it can only be changed through the dialog.
function makeTemplateChip(wikitext) {
// The × is its own element rather than the chip's only way out: on a phone there is no reliable
// backspace onto a contenteditable="false" span, so without it the dialog is the only way to take
// a call off the line. readChipSurface() reads chips from data-wikitext, so the extra child does
// not reach the saved text.
var $remove = $("<span class='dsb-tpl-chip-remove' contenteditable='false'>×</span>")
.attr("title", "Vê şablonê jê bibe");
return $("<span class='dsb-tpl-chip' contenteditable='false' role='button' tabindex='0'></span>")
.attr("data-wikitext", wikitext)
.attr("title", "Bitikîne da ku parametreyan biguhêrî")
.text(wikitext)
.append($remove);
}
// wikitext -> surface DOM: template calls become chips, everything else stays plain editable text.
function renderChipSurface($surface, wikitext) {
var text = wikitext || "";
var calls = parseTemplateCalls(text).calls;
var pos = 0;
$surface.empty();
calls.forEach(function (call) {
if (call.start > pos) {
$surface.append(document.createTextNode(text.slice(pos, call.start)));
}
$surface.append(makeTemplateChip(text.slice(call.start, call.end)));
pos = call.end;
});
if (pos < text.length) {
$surface.append(document.createTextNode(text.slice(pos)));
}
}
// surface DOM -> wikitext. Chips give back their stored call; anything the browser inserted while typing
// (a <br>, a wrapper <div>) degrades to its text, so the result is always plain wikitext.
function readChipSurface(node) {
var out = "";
$(node).contents().each(function () {
if (this.nodeType === 3) {
out += this.nodeValue;
return;
}
var $el = $(this);
if ($el.hasClass("dsb-tpl-chip")) {
out += $el.attr("data-wikitext");
return;
}
if (this.nodeName === "BR") {
out += "\n";
return;
}
if (this.nodeName === "DIV" || this.nodeName === "P") {
out += "\n" + readChipSurface(this);
return;
}
out += readChipSurface(this);
});
return out;
}
// Drops a node in at the caret, or at the end when the caret is not inside the surface.
function insertAtCaret($surface, node) {
var sel = window.getSelection();
if (sel && sel.rangeCount && $.contains($surface.get(0), sel.getRangeAt(0).commonAncestorContainer)) {
var range = sel.getRangeAt(0);
range.deleteContents();
range.insertNode(node);
range.setStartAfter(node);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
} else {
$surface.append(node);
}
}
// The template-param dialog (openTemplateDialog/buildTemplateParamForm) moved to wikt.cite, shared with
// start.js's own Etîmolojî and Çavkanî pickers: cite.openTemplateDialog / cite.buildTemplateParamForm.
// Each new example queued is its own entry, so several can be waiting at once.
var exampleSeq = 0;
// Takes the save over from add-examples while its form is open for one sense: its "Qeyd bike" becomes
// "Lê zêde bike" and the edit joins the pending queue instead of publishing on its own, so examples and
// whatever else is queued go up as one edit. Installed on every open, so the handler always closes over
// the sense whose form is actually open. ctx: {$li, $h3, language, sectionType, sectionOrdinal,
// definitionLevel, $save, $status, afterQueue}.
function takeOverExampleSave(ctx) {
if (!(wikt.addExamples && wikt.addExamples.setSubmitHandler)) {
return; // older add-examples: it publishes on its own, as before
}
// Lets the form read an example as the queue has already changed it.
wikt.addExamples.pendingContent = queuedContent;
wikt.addExamples.setSubmitHandler(function (job) {
exampleSeq++;
queueChange(
placeLabel(ctx.language, ctx.sectionType, ctx.$h3),
// add-examples' own summary, so the combined edit still credits the tool and names its
// version. The trailing stop goes: this is one item in a list, not a sentence of its own.
(job.summary || "nimûne/jêgirtin hate zêdekirin").replace(/\.\s*$/, ""),
function (pageContent) {
// add-examples builds against a revision object, and returns {text, summary}.
var built = job.transform({ content: pageContent });
return { content: built.text, warnings: [] };
},
ctx.$save,
ctx.$status,
// Drawn like any other queued change, in the sense it belongs to.
function () {
return previewSense(ctx.$li, ctx.language, ctx.sectionType, ctx.sectionOrdinal, ctx.definitionLevel);
},
// An edit to an example already on the page brings its own key, so editing it again replaces it.
{ key: job.key || ("example:" + ctx.sectionType + "|" + ctx.sectionOrdinal + "|" +
ctx.definitionLevel.join(".") + "|" + exampleSeq) }
);
job.done();
if (ctx.afterQueue) {
ctx.afterQueue();
}
});
}
// Opens add-examples' form on one example already under a sense -- dsb.js's pencil on it. Its save joins
// the queue like any other change.
function editExample($li, $h3, language, sectionType, sectionOrdinal, definitionLevel, itemEl) {
takeOverExampleSave({ $li: $li, $h3: $h3, language: language, sectionType: sectionType,
sectionOrdinal: sectionOrdinal, definitionLevel: definitionLevel, $save: $(), $status: $(),
afterQueue: openQueueBox });
if (!(wikt.addExamples && wikt.addExamples.editFor && wikt.addExamples.editFor($li.get(0), itemEl))) {
mw.notify("Amûra nimûneyan li vê maneyê nehat dîtin.");
}
}
// Builds the whole "guhartina pêşketî" panel into $quickRow: gloss and the two most-used word lists
// (Hevmane/Dijmane) sit directly in the picker row; the remaining word lists, maneid, etîket, kategorî
// and Nimûne/jêgirtin live behind a "Hevmane û Bêhtir" dropdown; one shared "Lê zêde bike" queues everything the
// user filled in as a single edit via applyAdvancedChanges() and queueChange(). Nimûne/jêgirtin is an
// immediate proxy-click into a different gadget's own form, not part of this combined save.
function buildAdvancedPanel($quickRow, $li, $h3, language, sectionType, sectionOrdinal, definitionLevel, isLastSense) {
// "+" marks the actions that add something, matching the dropdown's own "+ Hevmane" labels.
var $nimune = $("<a href='#' class='dsb-quick-link'></a>").text("+ Nimûne/jêgirtin");
$nimune.on("click", function (e) {
e.preventDefault();
takeOverExampleSave({ $li: $li, $h3: $h3, language: language, sectionType: sectionType,
sectionOrdinal: sectionOrdinal, definitionLevel: definitionLevel, $save: $save, $status: $status });
// Toggle, so clicking this a second time closes the form again. add-examples builds nothing
// on the page of its own, so this is the only way its form opens.
var drive = wikt.addExamples.toggleFor || wikt.addExamples.openFor;
if (!(drive && drive($li.get(0)))) {
mw.notify("Amûra nimûneyan li vê maneyê nehat dîtin.");
}
$moreMenu.slideUp();
});
// getters holds one function per currently-open box (key -> () => value); collectChanges() only ever
// reads from a box the user actually clicked to open.
var getters = {};
// What the sense currently holds, fetched once and shared by every box that prefills from it.
// collectChanges() also diffs against it, so an untouched box never queues a change.
var original = null;
var sensePromise = null;
function loadSense() {
if (!sensePromise) {
sensePromise = fetchSenseFields(language, sectionType, sectionOrdinal, definitionLevel).then(function (fields) {
original = fields;
return fields;
});
}
return sensePromise;
}
function sameList(a, b) {
return (a || []).join("\u0000") === (b || []).join("\u0000");
}
// -- gloss ("Maneyê biguhêre" -- the one box that EDITS the sense's wording; everything else here only ADDS) --
function addGlossField() {
var $fields = $("<div class='dsb-gloss-fields'></div>");
var $textarea = $("<textarea class='dsb-input dsb-edit-gloss' rows='2'></textarea>")
.attr("placeholder", util.msg.loading).prop("disabled", true);
var $autoLink = $("<button type='button' class='dsb-autolink'>Oto-lînk bike</button>").prop("disabled", true);
var $toRaw = $("<a href='#' class='dsb-quick-link dsb-gloss-raw'>Wekî wîkîtekst biguhêre</a>").hide();
var $status = $("<span class='dsb-status'></span>");
var originalGloss = null;
var tplForm = null; // set while the parameter boxes are showing instead of the raw line
var tplCall = null;
// The gloss of a non-Kurdish entry is a translation followed by {{îzah|…}} explaining it. That
// is one field on the page, so it stays one box here; the explanation opens beside it only when
// asked for, and the two are joined back into a single gloss on save. A Kurdish sense is its
// own definition, so it is never offered one -- though a ku gloss that somehow already carries
// an {{îzah}} still opens its box below, rather than being edited blind.
var offerIzah = language !== "ku";
var $izah = $("<textarea class='dsb-input dsb-edit-izah' rows='2'></textarea>")
.attr("placeholder", "Îzaha maneyî… ji bo school-a îngilîzî 'cihê tehsîlgirtinê'");
// A non-Kurdish gloss is a translation and its explanation, so both boxes open together. A Kurdish
// sense is its own definition and gets no îzah box, unless its gloss already carries one.
var $izahWrap = $("<div class='dsb-izah-wrap'></div>")
.append($("<label class='dsb-adv-label'>Îzaha maneyî</label>").append($izah))
.toggle(offerIzah);
$fields.append($textarea);
// The template form edits one call inside the line, so its result is spliced back into the rest.
function currentGloss() {
if (tplForm) {
return $.trim(originalGloss.slice(0, tplCall.start) + tplForm.values() + originalGloss.slice(tplCall.end));
}
return joinGloss($.trim($textarea.val()), $.trim($izah.val()));
}
function showRaw() {
tplForm = null;
tplCall = null;
$fields.empty().append($textarea);
$autoLink.show();
$toRaw.hide();
}
loadSense().then(
function (fields) {
originalGloss = fields.gloss;
// An existing gloss that already ends in {{îzah|…}} comes apart into the two boxes, so
// editing it does not mean editing template syntax by hand.
var parts = util.splitTercimeIzah(fields.gloss);
$textarea.val(parts ? $.trim(parts.tercime) : fields.gloss)
.prop("disabled", false)
// A non-Kurdish gloss is a translation; the box under it takes the explanation.
.attr("placeholder", offerIzah ? util.msg.tercimePrompt : "Maneyê binivîse…");
if (parts) {
$izah.val($.trim(parts.izah));
$izahWrap.show();
}
$autoLink.prop("disabled", false);
var target = glossTemplateTarget(fields.gloss);
if (!target) {
return;
}
fetchTemplateData(target.name).then(function (td) {
// The fetch is async; leave the raw box alone if it has already been typed into.
if ($.trim($textarea.val()) !== $.trim(originalGloss)) {
return;
}
tplCall = target;
tplForm = cite.buildTemplateParamForm(target, td);
$fields.empty().append(tplForm.$element);
$autoLink.hide(); // linkifying a bare template call makes no sense
// A gloss that is one template call carries no separate explanation.
$izahWrap.hide();
$toRaw.show();
});
},
function (codeOrAbort, result) {
$textarea.attr("placeholder", "");
var message = codeOrAbort instanceof SubmitAbort ? codeOrAbort.message : "Nikare maneya heyî bar bike: " + apiErrorMessage(codeOrAbort, result);
$status.removeClass("dsb-status-ok").addClass("dsb-status-error").text(message);
}
);
$autoLink.on("click", function () {
var mane = $.trim($textarea.val());
var izah = $.trim($izah.val());
if (!mane && !izah) {
return;
}
$autoLink.prop("disabled", true);
$status.removeClass("dsb-status-ok dsb-status-error").text("Tê lînkkirin…");
linkifyGlossParts(mane, izah, function (linkedMane, linkedIzah, error) {
$textarea.val(linkedMane);
$izah.val(linkedIzah);
$autoLink.prop("disabled", false);
if (error) {
$status.addClass("dsb-status-error").text(error);
} else {
$status.text("");
}
});
});
$toRaw.on("click", function (e) {
e.preventDefault();
$textarea.val(currentGloss()); // read the form before showRaw() discards it
showRaw();
});
getters.gloss = function () {
var v = currentGloss();
return (v && v !== originalGloss) ? v : null;
};
return $("<div class='dsb-adv-group'></div>").append(
$("<div class='dsb-adv-label'></div>").text("Mane"),
$fields,
$izahWrap,
$("<div class='dsb-form-actions'></div>").append($autoLink, $toRaw, $status)
);
}
// -- one SENSE_GROUPS word-list box (Hevmane/Dijmane/etc.) --
function addWordField(group) {
var field = buildWordListField(group.label + "…");
loadSense().then(function (fields) {
var current = fields.words[group.key];
if (current && current.length) {
field.setValues(current);
}
});
getters["word:" + group.key] = function () {
return field.values();
};
// The hint is senseGroups' own, so the box carries the same example sentence the wizard puts
// beside its copy of this list.
var $label = $("<div class='dsb-adv-label'></div>").text(group.label);
if (group.hint) {
$label.append($("<span class='dsb-adv-hint'></span>").text(group.hint));
}
return $("<div class='dsb-adv-group'></div>").append($label, field.$element);
}
// -- maneid (single value; re-adding one that already exists is rejected -- see applyAdvancedChanges) --
function addNewSenseField() {
var $textarea = $("<textarea class='dsb-input' rows='2'></textarea>").attr("placeholder", "Maneya nû binivîse…");
getters.newSense = function () {
return $.trim($textarea.val());
};
return $("<div class='dsb-adv-group'></div>").append(
$("<div class='dsb-adv-label'></div>").text(util.msg.anotherSense),
$textarea
);
}
function addManeidField() {
var $input = $("<input type='text' class='dsb-input' placeholder='id…'>");
loadSense().then(function (fields) {
$input.val(fields.maneid);
});
getters.maneid = function () {
return $.trim($input.val());
};
return $("<div class='dsb-adv-group'></div>").append($("<div class='dsb-adv-label'></div>").text("Maneid"), $input);
}
// -- etîket / kategorî --
function addEtiketField() {
var field = buildTagField("Etîket…");
loadSense().then(function (fields) {
field.setValues(fields.etiket);
});
getters.etiket = function () {
return field.values();
};
return $("<div class='dsb-adv-group'></div>").append($("<div class='dsb-adv-label'></div>").text("Etîket"), field.$element);
}
function addKategoriField() {
var field = buildTagField("Kategorî…");
loadSense().then(function (fields) {
field.setValues(fields.kategori);
});
getters.kategori = function () {
return field.values();
};
return $("<div class='dsb-adv-group'></div>").append($("<div class='dsb-adv-label'></div>").text("Kategorî"), field.$element);
}
// -- the picker row: the gloss and Nimûne/jêgirtin links sit directly in the row; every word group and
// the rest live behind a "Hevmane û Bêhtir" dropdown. Clicking any link builds and appends its box
// to $openBoxes and hides that link; the box's own "×" removes it, clears its getter, and un-hides the
// link -- several boxes can be open at once. No "Beşek (====)" entry here: buildSubsectionPanel()'s
// own "+ Beşek lê zêde bike" trigger already covers page-level subsections. ----
var FIELD_DEFS = [{ key: "gloss", label: "Maneyê biguhêre", build: addGlossField, primary: true }]
.concat(SENSE_GROUPS.map(function (group) {
return { key: "word:" + group.key, label: group.label, build: function () { return addWordField(group); }, primary: false };
}))
.concat([
{ key: "maneid", label: "Maneid", build: addManeidField, primary: false },
{ key: "etiket", label: "Etîket", build: addEtiketField, primary: false },
{ key: "kategori", label: "Kategorî", build: addKategoriField, primary: false }
]);
// Every sense offers this: the new one lands directly below whichever sense asked for it (see
// insertNewSense), so it is no longer only the last one that can add to the end.
FIELD_DEFS.push({ key: "newSense", label: util.msg.addAnotherSense, build: addNewSenseField, primary: false });
var $picker = $("<div class='dsb-field-picker'></div>");
var $moreMenu = $("<div class='dsb-more-menu'></div>").hide();
var $openBoxes = $("<div class='dsb-open-boxes'></div>");
// $actions (built below) stays hidden until at least one field box is actually open -- otherwise
// "Lê zêde bike" sits there enabled with nothing to queue, which just produces an empty no-op change.
var openBoxCount = 0;
function updateActionsVisibility() {
$actions.toggle(openBoxCount > 0);
}
function makeFieldLink(def) {
var $link = $("<a href='#' class='dsb-quick-link dsb-field-pick'></a>").text(def.key === "gloss" ? def.label : "+ " + def.label);
$link.on("click", function (e) {
e.preventDefault();
openQueueBox();
var $box = def.build();
var $remove = $("<a href='#' class='dsb-box-remove' title='Rake'>×</a>");
$remove.on("click", function (e2) {
e2.preventDefault();
delete getters[def.key];
$box.remove();
$link.show();
openBoxCount--;
updateActionsVisibility();
});
$box.prepend($remove);
$openBoxes.append($box);
util.initManualResize($box);
$link.hide();
$moreMenu.slideUp();
openBoxCount++;
updateActionsVisibility();
});
return $link;
}
// Destructive, so it lives at the bottom of the dropdown behind a confirmation, and shares the
// sense's queue key: deleting supersedes any edit still pending on the same sense.
function makeDeleteLink() {
var $link = $("<a href='#' class='dsb-quick-link dsb-field-delete'>Maneyê rake</a>");
$link.on("click", function (e) {
e.preventDefault();
$moreMenu.slideUp();
OO.ui.confirm("Ev mane û hemû rêzên bin wê dê werin rakirin. Dewam bike?").done(function (ok) {
if (!ok) {
return;
}
queueChange(
placeLabel(language, sectionType, $h3),
"maneyek hate rakirin",
function (baseContent) {
return removeSense(baseContent, language, sectionType, sectionOrdinal, definitionLevel);
},
$save,
$note,
function () {
$li.addClass("dsb-sense-doomed");
return function () {
$li.removeClass("dsb-sense-doomed");
};
},
{ key: senseQueueKey() }
);
$note.show();
});
});
return $link;
}
// Only the links the row actually shows are built now; the dropdown's nine are 43% of the work and
// nobody sees them until "Hevmane û Bêhtir" is clicked, so buildMenuLinks() waits for that.
FIELD_DEFS.forEach(function (def) {
if (!def.primary) {
return;
}
$picker.append(makeFieldLink(def));
if (def.key === "gloss") {
$picker.append($nimune); // directly after "Maneyê biguhêre"
}
});
// Three sections, ruled apart: what labels this sense, the word lists that hang off it, and adding
// another sense. "Maneyê rake" is appended after these by makeDeleteLink() and so closes the last
// one. The word lists follow senseGroups' "order" -- that array's own order is the order the "#:"
// lines are written to the page in, which is not a reading order; the wizard's boxes read the same
// field, so both gadgets list them alike. Anything not named here still appears, after these, so a
// field added later is never silently dropped from the menu.
var MENU_GROUPS = [
["etiket", "kategori", "maneid"],
SENSE_GROUPS.slice().sort(function (a, b) {
return (a.order || 0) - (b.order || 0);
}).map(function (g) {
return "word:" + g.key;
}),
["newSense"]
];
var menuBuilt = false;
function buildMenuLinks() {
if (menuBuilt) {
return;
}
menuBuilt = true;
var byKey = {};
FIELD_DEFS.forEach(function (def) {
if (!def.primary) {
byKey[def.key] = def;
}
});
var placed = {};
MENU_GROUPS.forEach(function (group, groupIndex) {
var defs = group.map(function (key) {
return byKey[key];
}).filter(Boolean);
defs.forEach(function (def, i) {
placed[def.key] = true;
var $link = makeFieldLink(def);
// The rule goes on the first entry of every group after the first.
if (groupIndex > 0 && i === 0) {
$link.addClass("dsb-menu-group-start");
}
$moreMenu.append($link);
});
});
FIELD_DEFS.forEach(function (def) {
if (!def.primary && !placed[def.key]) {
$moreMenu.append(makeFieldLink(def));
}
});
$moreMenu.append(makeDeleteLink());
}
// Lives inside $picker (not appended straight to $quickRow) so .dsb-more-menu's CSS can position it as
// an overlay anchored to $picker, instead of pushing $openBoxes/the rest of the page down when it opens.
var $moreArrow = $("<span class='dsb-more-arrow'></span>").text("▸");
var $moreToggle = $("<a href='#' class='dsb-quick-link dsb-more-toggle'></a>")
.text(util.msg.moreMenu + " ").append($moreArrow);
$moreToggle.on("click", function (e) {
e.preventDefault();
buildMenuLinks();
$moreMenu.slideToggle(function () {
$moreArrow.text($moreMenu.is(":visible") ? "▾" : "▸");
});
});
$picker.append($moreToggle, $moreMenu);
// Outside clicks are handled by one shared document listener (see closeMoreMenus), not one per panel:
// hovering a sense builds its panel, so a long entry would otherwise accumulate dozens of them.
$moreMenu.data("dsbArrow", $moreArrow);
// -- combined save: whatever boxes are open right now, queued together via queueChange(); the actual
// save happens when the user clicks "Guhartinan qeyd bike" on the floating queue box. ----
// Small "not saved yet" preview of everything this queued change will do, appended right under the
// sense's gloss (plain text, not a real render of {{hevmane}}/etc.).
function collectChanges() {
var base = original || { maneid: "", etiket: [], kategori: [], words: {} };
var changed = {};
var words = {};
SENSE_GROUPS.forEach(function (group) {
var getter = getters["word:" + group.key];
words[group.key] = getter ? getter() : [];
if (getter && !sameList(words[group.key], base.words[group.key])) {
changed["word:" + group.key] = true;
}
});
var maneid = getters.maneid ? getters.maneid() : "";
var etiket = getters.etiket ? getters.etiket() : [];
var kategori = getters.kategori ? getters.kategori() : [];
if (getters.maneid && maneid !== (base.maneid || "")) {
changed.maneid = true;
}
if (getters.etiket && !sameList(etiket, base.etiket)) {
changed.etiket = true;
}
if (getters.kategori && !sameList(kategori, base.kategori)) {
changed.kategori = true;
}
return {
gloss: getters.gloss ? getters.gloss() : null,
newSense: getters.newSense ? getters.newSense() : null,
maneid: maneid,
etiket: etiket,
kategori: kategori,
words: words,
changed: changed
};
}
// Both the field save and "Maneyê rake" queue under this, so the later one supersedes the earlier.
function senseQueueKey() {
return "sense:" + sectionType + "|" + sectionOrdinal + "|" + definitionLevel.join(".");
}
var $save = $("<button type='button' class='dsb-save'></button>").text(util.msg.add);
// Where queueChange() writes its confirmation for actions that have no box of their own.
var $note = $("<div class='dsb-status dsb-quick-note'></div>").hide();
var $cancel = $("<button type='button' class='dsb-cancel'></button>").text(util.msg.cancel);
var $status = $("<span class='dsb-status'></span>");
var $actions = $("<div class='dsb-form-actions'></div>").append($save, $cancel, $status).hide();
$cancel.on("click", function () {
// Close the boxes too, not just the row. Hiding alone left every field still open with its
// text in it, so the next click on the pencil reopened the half-filled panel that had just
// been cancelled. Each box's own "x" already clears its getter, restores its link and updates
// the counter, so those are triggered rather than repeating the teardown here.
$openBoxes.find(".dsb-box-remove").trigger("click");
$note.hide().text("");
// dsb.js owns the pencil's pinned state; tell it rather than hiding behind its back, or the
// next click on the pencil would only toggle "pinned" off and appear to do nothing.
$quickRow.trigger("dsbCancel");
hideQueueIfIdle();
});
$save.on("click", function () {
var changes = collectChanges();
function afterLinkify() {
var parts = senseChangeLabels(changes);
queueChange(
placeLabel(language, sectionType, $h3),
parts.length
? parts.join(", ") + (parts.length > 1 ? " hatin guhartin" : " hate guhartin")
: "mane hate guhartin",
function (baseContent) {
return applyAdvancedChanges(baseContent, language, sectionType, sectionOrdinal, definitionLevel, changes);
},
$save,
$status,
function () {
var undoPreview = previewSense($li, language, sectionType, sectionOrdinal,
definitionLevel);
// A new sense has no place on the page yet, so it gets a row of its own directly
// after this one -- the list numbers it, so where it will land is visible.
var $newSense = null;
if (changes.newSense) {
$newSense = $("<li class='dsb-sense-preview'></li>");
$li.after($newSense);
fillWithRendered($newSense, changes.newSense, function () {
$newSense.text(changes.newSense);
});
}
return function () {
undoPreview();
if ($newSense) {
$newSense.remove();
}
};
},
// Re-saving this same sense supersedes its pending change rather than queuing another.
{ key: senseQueueKey() }
);
}
$save.prop("disabled", true);
// Auto-linkify the gloss as part of this save, but only a half nobody has linked by hand.
// Split back into its two halves first, so the explanation is linked on its own rather than
// sitting unseen inside {{îzah|…}}.
if (changes.gloss) {
$status.removeClass("dsb-status-ok dsb-status-error").text("Tê lînkkirin…");
var glossParts = util.splitTercimeIzah(changes.gloss);
linkifyGlossParts(
glossParts ? $.trim(glossParts.tercime) : changes.gloss,
glossParts ? $.trim(glossParts.izah) : "",
function (linkedMane, linkedIzah, error) {
// The change is queued either way, so a gloss that came back empty is not written
// back -- that would save the sense with its wording gone.
var joined = joinGloss(linkedMane, linkedIzah);
if ($.trim(joined)) {
changes.gloss = joined;
}
if (error) {
$status.removeClass("dsb-status-ok").addClass("dsb-status-error").text(error);
}
afterLinkify();
},
true
);
} else {
afterLinkify();
}
});
// The links sit under the buttons, so each open box pushes them down rather than leaving them far above.
$quickRow.empty().append($openBoxes, $actions, $picker, $note);
}
// Attaches the pencil toggle to one leaf definition <li> -- clicking it reveals the "guhartina pêşketî"
// (advanced editing) panel built by buildAdvancedPanel() above.
// ---- per-section UI: "+ Beşek lê zêde bike" after a POS block's own content ----
// Reads the header text of "====" subsections already rendered under this POS heading, straight from the DOM.
// The subsections of a part of speech are the headings one deeper than it is.
function subsectionDepthFor($h3) {
return (sections.headingDepth($h3) || 3) + 1;
}
function isSubsectionHeading($el, depth) {
return sections.headingDepth($el) === depth;
}
function existingSubsectionHeadersInDom($h3) {
var depth = subsectionDepthFor($h3);
var headers = [];
for (var $cursor = headingOuter($h3).next(); $cursor.length; $cursor = $cursor.next()) {
var here = sections.headingDepth($cursor);
if (here && here < depth) {
break; // out of this part of speech
}
if (isSubsectionHeading($cursor, depth)) {
var $hh = $cursor.is("h2, h3, h4, h5, h6") ? $cursor : $cursor.find("h2, h3, h4, h5, h6").first();
headers.push(headingText($hh));
}
}
return headers;
}
// Finds where a new "====" subsection preview belongs in the live DOM, using the same SUBSECTION_ORDER rank
// insertPageSubsection() uses. Returns {insertBefore: $el} (a ranked subsection that outranks this one) or
// {insertAfter: $el} (goes at the end of the block).
function domSubsectionInsertAnchor($h3, header) {
var depth = subsectionDepthFor($h3);
var myRank = subsectionRankForHeader(header);
var $last = headingOuter($h3);
for (var $cursor = headingOuter($h3).next(); $cursor.length; $cursor = $cursor.next()) {
var here = sections.headingDepth($cursor);
if (here && here < depth) {
break; // out of this part of speech
}
if (isSubsectionHeading($cursor, depth)) {
var $hh = $cursor.is("h2, h3, h4, h5, h6") ? $cursor : $cursor.find("h2, h3, h4, h5, h6").first();
var otherRank = subsectionRankForHeader(headingText($hh));
if (otherRank !== -1 && otherRank > myRank) {
return { insertBefore: $cursor };
}
}
$last = $cursor;
}
return { insertAfter: $last };
}
// The wikitext one subsection's content becomes, exactly as insertPageSubsection() writes it -- so the
// preview parses the same string the save will produce.
function subsectionBodyWikitext(language, s, content) {
if (s.kind !== "wordlist") {
return content;
}
return content.filter(function (group) {
return group.words.length;
}).map(function (group) {
return buildWordListCall(WORDLIST_TEMPLATE, language,
withNamedParam(["cure=" + s.header], "sernav", group.sernav), group.words);
}).join("\n\n");
}
// The plain word list this preview falls back to when the parse fails: the values that were typed,
// without the template around them. Right, but not what the page will look like.
function subsectionPlainDom($wrap, s, content) {
if (s.kind !== "wordlist") {
$wrap.append($("<p class='dsb-preview-text'></p>").text(content));
return;
}
content.forEach(function (group) {
if (group.sernav) {
$wrap.append($("<div class='dsb-preview-subheading'></div>").text(group.sernav));
}
var $list = $("<ul class='dsb-preview-list'></ul>");
group.words.forEach(function (w) {
$list.append($("<li></li>").text(w.replace(/<[^<>]*>/g, "")));
});
$wrap.append($list);
});
}
// A "not saved yet" preview of a page-level "====" subsection, dropped into the live page at the
// position the save will put it, with its body rendered the way the page renders it -- real {{kol3}}
// columns and real links, not a list of the words that went into them. Returns the inserted element so
// the caller can remove it again.
function buildSubsectionPreviewDom($h3, s, content, language) {
var $wrap = $("<div class='dsb-preview-block'></div>");
// A styled <div>, not a real <h4>, so existingSubsectionHeadersInDom() never mistakes this live-only
// preview for an actual subsection already on the page.
$wrap.append($("<div class='dsb-preview-heading' role='heading' aria-level='4'></div>").text(s.header));
$wrap.append($("<span class='dsb-preview-badge'></span>").text("nû — hîn nehatiye qeydkirin"));
// Placed before it is filled: a cached render resolves almost at once, and filling a wrap that is
// still detached would announce content nobody can see.
var anchor = domSubsectionInsertAnchor($h3, s.header);
if (anchor.insertBefore) {
anchor.insertBefore.before($wrap);
} else {
anchor.insertAfter.after($wrap);
}
var wikitext = language ? subsectionBodyWikitext(language, s, content) : "";
if ($.trim(wikitext)) {
var $body = $("<div class='dsb-preview-rendered'></div>").appendTo($wrap);
fillWithRendered($body, wikitext, function () {
$body.remove();
subsectionPlainDom($wrap, s, content);
});
} else {
subsectionPlainDom($wrap, s, content);
}
return $wrap;
}
// A free section owns what lies between its own heading and the next heading of any depth, matching
// the range locateHeadingByText() edits. Returns a function that puts the page back, or null.
function replaceFreeSectionInPlace($heading, wikitext) {
var $start = headingOuter($heading);
var body = [];
for (var $cursor = $start.next(); $cursor.length; $cursor = $cursor.next()) {
if (sections.headingDepth($cursor)) {
break;
}
if (!$cursor.is(".dsb-subsection-panel, .dsb-convert-pos-panel, .dsb-preview-block, .dsb-sense-preview")) {
body.push($cursor.get(0));
}
}
if (!body.length) {
return null;
}
var $slot = $("<div class='dsb-sense-preview'></div>").insertAfter($start);
var $body = $(body).detach();
fillWithRendered($slot, wikitext, function () {
$slot.remove();
$slot = null;
$body.insertAfter($start);
});
return function () {
if ($slot) {
$body.insertAfter($start);
$slot.remove();
$slot = null;
}
};
}
/**
* Redraws a subsection that is already on the page with the content this edit would give it.
*
* The section's own nodes -- everything between its heading and the next heading of the same rank or
* higher -- are detached rather than dropped, so the returned function puts the page back exactly as it
* was when the change is cancelled or superseded.
*/
function replaceSubsectionInPlace($h3, header, wikitext) {
var depth = subsectionDepthFor($h3);
// Walked the same way existingSubsectionHeadersInDom() walks it, so "which nodes are this
// subsection" is decided once and the same way everywhere.
var $start = null;
var body = [];
for (var $cursor = headingOuter($h3).next(); $cursor.length; $cursor = $cursor.next()) {
var here = sections.headingDepth($cursor);
if (here && here < depth) {
break; // out of this part of speech
}
if (isSubsectionHeading($cursor, depth)) {
if ($start) {
break; // the next subsection starts here
}
var $hh = $cursor.is("h2, h3, h4, h5, h6") ? $cursor : $cursor.find("h2, h3, h4, h5, h6").first();
if (headingText($hh) === header) {
$start = $cursor;
}
continue;
}
// This gadget's own furniture stays where it is: the subsection's editing panel is the sibling
// right after its heading, so sweeping it into the body would detach the very panel the change
// was made in and leave the pencil toggling an element that is no longer on the page.
if ($start && !$cursor.is(".dsb-subsection-panel, .dsb-convert-pos-panel, .dsb-preview-block, .dsb-sense-preview")) {
body.push($cursor.get(0));
}
}
if (!$start || !body.length) {
return null;
}
var $slot = $("<div class='dsb-sense-preview'></div>").insertAfter($start);
var $body = $(body).detach();
fillWithRendered($slot, wikitext, function () {
$slot.remove();
$slot = null;
$body.insertAfter($start);
});
return function () {
if ($slot) {
$body.insertAfter($start);
$slot.remove();
$slot = null;
}
};
}
/**
* The live DOM one "place" section occupies: {nodes, insert}. nodes are what a render replaces --
* empty when the section is not on the page yet -- and insert() puts an element where the section
* belongs, so a new one appears exactly where the save will put it.
*/
// This gadget's own panels sit between a heading and the page's content, so every walk steps over
// them rather than counting one as content.
function isOwnFurniture($el) {
return $el.is(".dsb-toolbar, .dsb-quick-row, .dsb-subsection-panel, .dsb-convert-pos-panel," +
" .dsb-preview-block, .dsb-sense-preview");
}
function loosePlaceDom($h3, subsection) {
var place = subsection.place;
var nodes = [];
var $cursor;
if (place === "page-top") {
// Şablon:herwiha binêre renders as <div class="rellink binere"> at the head of the content.
var $out = $("#mw-content-text .mw-parser-output").first();
var $hat = $out.children(".binere, .rellink").first();
if ($hat.length) {
return { nodes: $hat.get(), insert: function ($el) { $hat.before($el); } };
}
return { nodes: [], insert: function ($el) { $out.prepend($el); } };
}
var $start = headingOuter($h3);
if (place === "pron") {
// A "===" heading above this POS block, under the language one.
for (var $back = $start.prev(); $back.length; $back = $back.prev()) {
if ($back.is(".mw-heading2, h2")) {
break;
}
if ($back.is(".mw-heading3, h3")) {
var $hh = $back.is("h3") ? $back : $back.find("h3").first();
if (headingText($hh) === subsection.header) {
for ($cursor = $back.next();
$cursor.length && !$cursor.is(".mw-heading2, .mw-heading3, h2, h3");
$cursor = $cursor.next()) {
if (!isOwnFurniture($cursor)) {
nodes.push($cursor.get(0));
}
}
return { nodes: nodes, insert: function ($el) { $back.after($el); } };
}
}
}
return { nodes: [], insert: function ($el) { $start.before($el); } };
}
if (place === "pos-top") {
// The thumbnails between the heading and the headword line.
for ($cursor = $start.next(); $cursor.length; $cursor = $cursor.next()) {
if (isOwnFurniture($cursor)) {
continue;
}
if (!$cursor.is(".thumb, figure, .floatright, .floatleft")) {
break;
}
nodes.push($cursor.get(0));
}
return { nodes: nodes, insert: function ($el) { $start.after($el); } };
}
return null;
}
// The wikitext a "place" section's pending content renders as. A word list is its one call; everything
// else is already the body that will be written.
function loosePlacePreviewWikitext(subsection, content, language) {
return subsection.kind === "wordlist"
? wt.buildHbnrCall(loosePlaceWords(content))
: loosePlaceBody(subsection, content, language);
}
// Draws a section's headword line from wikitext where the page's own line stands, or above the
// definitions when the section has none. The page's line is detached rather than dropped, so the
// returned function puts it back.
function replaceHeadwordInPlace($h3, wikitext) {
if (!$.trim(wikitext || "")) {
return null;
}
var $old = $();
var $at = $();
for (var $cursor = headingOuter($h3).next(); $cursor.length; $cursor = $cursor.next()) {
if ($cursor.is("p")) {
$old = $cursor;
break;
}
if (sections.headingDepth($cursor) || $cursor.is("ol")) {
$at = $cursor;
break;
}
}
var $slot = $("<div class='dsb-sense-preview'></div>");
if ($old.length) {
$old.before($slot);
} else if ($at.length) {
$at.before($slot);
} else {
return null;
}
var $body = $old.detach();
function restore() {
if (!$slot) {
return;
}
$body.insertBefore($slot);
$slot.remove();
$slot = null;
}
fillWithRendered($slot, wikitext, restore);
return restore;
}
// Redraws one of those places where it stands, the same way replaceSubsectionInPlace() redraws a
// "====" one. The page's own nodes are detached rather than dropped, so cancelling puts them back.
function replaceLoosePlaceInPlace($h3, subsection, wikitext) {
if (!$.trim(wikitext || "")) {
return null;
}
var at = loosePlaceDom($h3, subsection);
if (!at) {
return null;
}
var $slot = $("<div class='dsb-sense-preview'></div>");
at.insert($slot);
var $body = $(at.nodes).detach();
function restore() {
if (!$slot) {
return;
}
$body.insertBefore($slot);
$slot.remove();
$slot = null;
}
fillWithRendered($slot, wikitext, restore);
return restore;
}
// Builds the subsection picker + its word-list/text field. A subsection that isn't on the page yet is saved
// via insertPageSubsection(); one that already is gets prefilled and saved via updatePageSubsection().
// onlyHeader pins the panel to a single subsection (the per-heading pencil); omit it for the full picker.
// free pins the panel to a heading that is not a subsection of a part of speech, located by its own
// text: {header, ordinal}.
function buildSubsectionPanel($panel, $h3, language, sectionType, sectionOrdinal, onlyHeader, free) {
var existing = existingSubsectionHeadersInDom($h3);
// A heading the data does not name is still editable. It is declared a word list so the reader
// tries, in order, its {{kol3}} calls and then a plain bullet list; anything else falls through to
// raw wikitext, which is what prefill() does with a list it cannot read.
// Hidden counts as unknown here: the picker does not offer to build a Werger box, but one already
// on the page still opens, as the wikitext it is.
var unknown = onlyHeader && !PAGE_SUBSECTIONS.some(function (s) {
return s.header === onlyHeader && !s.hidden;
});
// A free section starts as wikitext: what it holds is only known once it has been read, and
// prefill() swaps in the {{kol3}} form there if it turns out to be a list.
var available = free ? [{
header: onlyHeader,
id: "free",
kind: "text",
free: free,
existing: true
}] : unknown ? [{
header: onlyHeader,
id: "unknown",
kind: "wordlist",
unknown: true,
existing: true
}] : PAGE_SUBSECTIONS.filter(function (s) {
// "hidden" takes a section out of the picker without removing it: Werger waits for the
// translation tools it belongs to, and offering a box those cannot yet fill misleads.
// Werger also carries lang: "ku" -- it is a Kurdish-entry section, so it is not offered
// elsewhere either.
return !s.hidden && (!s.lang || s.lang === language) &&
(!onlyHeader || s.header === onlyHeader);
}).map(function (s) {
return $.extend({}, s, { existing: existing.indexOf(s.header) !== -1 });
});
if (!available.length) {
$panel.text("Ji bo vê beşê guhartin tune.");
return;
}
var $select = $("<select class='dsb-subsection-select'></select>");
if (!onlyHeader) {
// Label-less on purpose: it holds the "nothing chosen yet" value without becoming a row in the
// list (makeSearchable() skips a blank option that has no label), so the prompt lives in the
// input's placeholder instead.
$select.append($("<option value=''></option>"));
}
available.forEach(function (s) {
// A "place" entry has no "====" heading of its own, so it is marked rather than left looking
// like the subsections around it.
var label = s.place ? s.header + " (ne binebeş e)"
: (s.existing ? s.header + " (biguhêre)" : s.header);
$select.append($("<option></option>").val(s.id).text(label));
});
var $selectWrap = makeSearchable($select, "Binebeşek hilbijêre");
if (onlyHeader) {
$selectWrap.hide(); // pinned to one subsection: nothing to choose
}
var $fieldWrap = $("<div class='dsb-subsection-field'></div>");
var $save = $("<button type='button' class='dsb-save'></button>").text(util.msg.add);
var $cancel = $("<button type='button' class='dsb-cancel'></button>").text(util.msg.cancel);
var $status = $("<span class='dsb-status'></span>");
var $actions = $("<div class='dsb-form-actions'></div>").append($save, $cancel, $status);
$cancel.on("click", function () {
resetForm();
$panel.hide();
hideQueueIfIdle();
});
// Undefined while the placeholder option is selected.
function currentSubsection() {
var id = $select.val();
return id ? available.filter(function (s) {
return s.id === id;
})[0] : undefined;
}
// One entry per "{{kol3}}" group in the current wordlist section; empty for any other kind.
var groupFields = [];
var $groups = null;
var editAsText = false; // set when an existing wordlist section turns out to be unparseable
// Whatever has been typed into each subsection's box, so switching the picker away and back restores it
// instead of rebuilding an empty field. Keyed by subsection id; lastId is whichever box is on screen.
var drafts = {};
var lastId = null;
// Called before renderField() rebuilds, while the group fields still hold the outgoing box.
function captureDraft() {
if (!lastId) {
return;
}
drafts[lastId] = imagesField
? { asText: false, images: imagesField.values() }
: (groupFields.length
? { asText: false, groups: readGroups() }
: { asText: editAsText, text: fieldText() });
}
function restoreDraft(s, draft) {
// An existing wordlist we previously fell back to raw wikitext for stays in that mode.
if (draft.asText && s.kind === "wordlist") {
editAsText = true;
groupFields = [];
renderTextArea(s, 6);
}
if (imagesField) {
imagesField.setValues(draft.images);
} else if (groupFields.length) {
setGroups(s, draft.groups);
} else {
setFieldText(draft.text);
}
}
// Etîmolojî is edited on a live surface (see renderChipSurface) rather than in a textarea, so every
// read and write of the field goes through these two instead of touching .dsb-subsection-text.
var chipSurface = null;
function fieldText() {
if (wergerForm) {
return buildWergerBlock(wergerForm.read());
}
return chipSurface
? $.trim(readChipSurface(chipSurface.get(0)))
: $.trim($fieldWrap.find(".dsb-subsection-text").val() || "");
}
function setFieldText(value) {
if (wergerForm) {
wergerForm.set(parseWergerBlock(value));
return;
}
if (chipSurface) {
renderChipSurface(chipSurface, value || "");
var schedule = chipSurface.data("dsbSchedulePreview");
if (schedule) {
schedule(); // show what the loaded etymology renders as
}
} else {
$fieldWrap.find(".dsb-subsection-text").val(value || "");
}
}
// The contenteditable surface: text typed directly, template calls as chips that open the dialog,
// plus start.js's etymology templates in a searchable insert menu.
// A live surface where a template shows as a chip, with a menu that inserts one. Etîmolojî and
// Bilêvkirin are both written this way; they differ only in which templates the menu offers.
function renderTemplateEditor(s, templates) {
$fieldWrap.empty();
var $select = $("<select class='dsb-subsection-select'></select>");
$select.append($("<option value=''></option>"));
templates.forEach(function (t) {
$select.append($("<option></option>").val(t.snippet).text(t.name + " — " + t.desc));
});
var $surface = $("<div class='dsb-tpl-surface' contenteditable='true'></div>")
.attr("role", "textbox")
.attr("aria-multiline", "true");
chipSurface = $surface;
// Clicking the insert menu moves focus out of the surface and the browser forgets the caret, so
// the last caret position inside the surface is kept and restored before inserting -- otherwise
// focus() drops it back to the very start and every template lands at the beginning of the line.
var savedRange = null;
function rememberCaret() {
var sel = window.getSelection();
if (!sel || !sel.rangeCount) {
return;
}
var range = sel.getRangeAt(0);
var root = $surface.get(0);
if (range.commonAncestorContainer === root || $.contains(root, range.commonAncestorContainer)) {
savedRange = range.cloneRange();
}
}
$surface.on("keyup mouseup focusout", rememberCaret);
function restoreCaret() {
if (!savedRange) {
return;
}
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(savedRange);
}
$select.on("change", function () {
var snippet = $select.val();
if (!snippet) {
return;
}
// Same substitution start.js makes: CODE is this section's language, SOURCELANG is left for
// the user to fill in.
snippet = snippet.replace(/CODE/g, language).replace(/SOURCELANG/g, "?");
// A pronunciation template is a bullet of its own ("* {{ku-IPA}}"), so it starts a line
// rather than landing in the middle of one.
var ownLine = /^\*\s/.test(snippet);
if (ownLine) {
snippet = $.trim(snippet.replace(/^\*\s*/, ""));
}
$surface.focus();
restoreCaret();
var $chip = makeTemplateChip(snippet);
var piece = document.createDocumentFragment();
if (ownLine) {
// The bullet, and a line of its own unless the caret already starts one.
var at = caretOffset();
var before = at >= 0 ? readChipSurface($surface.get(0)).slice(0, at) : "";
piece.appendChild(document.createTextNode(!before || /\n$/.test(before) ? "* " : "\n* "));
}
piece.appendChild($chip.get(0));
insertAtCaret($surface, piece);
rememberCaret(); // so a second insert lands after the first, not back at the old spot
$select.val("");
// Straight into the dialog: the snippet is placeholders ("?", "…") until someone fills it in.
cite.openTemplateDialog(snippet).then(function (edited) {
applyChipEdit($chip, edited);
syncPanelWidth();
});
});
// Paste as plain text: pasted markup would otherwise become real nodes in the surface.
$surface.on("paste", function (e) {
var clip = e.originalEvent && e.originalEvent.clipboardData;
if (!clip) {
return;
}
e.preventDefault();
document.execCommand("insertText", false, clip.getData("text/plain"));
});
// The chip the dialog was opened for, or the one that has taken its place. renderChipSurface()
// rebuilds the surface from its wikitext whenever a call is typed in as text, which throws away
// the element this closure is holding; without this, "Jê bibe" would remove a node that is no
// longer on screen and appear to do nothing.
function liveChip($chip, wikitext) {
if ($.contains($surface.get(0), $chip.get(0))) {
return $chip;
}
return $surface.find(".dsb-tpl-chip").filter(function () {
return $(this).attr("data-wikitext") === wikitext;
}).first();
}
function applyChipEdit($chip, edited) {
if (edited === null || !$chip.length) {
return;
}
if (!$.trim(edited)) {
$chip.remove();
return;
}
// Edited into something that is no longer one template call: let it fall back to plain text
// rather than keeping a chip that lies about its contents.
var call = parseTemplateCalls(edited);
if (call.otherContent || call.calls.length !== 1) {
$chip.replaceWith(document.createTextNode(edited));
return;
}
$chip.replaceWith(makeTemplateChip(edited));
}
// Double-click edits the call's wikitext in place; single click opens the parameter dialog. The
// click is held back briefly so the first of a double-click does not open the dialog underneath.
var chipClickTimer = null;
var lastChipTap = 0;
var lastChipNode = null;
var DOUBLE_TAP_MS = 300;
// Double-tap drops the chip back to plain wikitext in the surface, rather than making the chip
// itself editable: a contenteditable inside a contenteditable gives Android a selection handle and
// no usable caret. As plain text the browser's own editing works, and chipifyTyped() puts the chip
// back once the caret leaves the call.
// A collapsed caret inside the text just inserted: at the character tapped where the browser can
// say which that is, otherwise just inside the closing braces so the caret stays within the call
// (which is what stops chipifyTyped putting the chip straight back).
function caretRangeIn(node, event) {
var range;
var x = event && event.clientX;
var y = event && event.clientY;
if (x !== undefined && document.caretRangeFromPoint) {
range = document.caretRangeFromPoint(x, y);
} else if (x !== undefined && document.caretPositionFromPoint) {
var pos = document.caretPositionFromPoint(x, y);
if (pos) {
range = document.createRange();
range.setStart(pos.offsetNode, pos.offset);
}
}
if (range && range.startContainer === node) {
range.collapse(true);
return range;
}
var text = node.nodeValue;
range = document.createRange();
range.setStart(node, /\}\}$/.test(text) ? text.length - 2 : text.length);
range.collapse(true);
return range;
}
function startInlineEdit($chip, event) {
if (!$.contains($surface.get(0), $chip.get(0))) {
return; // the tap timer and dblclick can both fire; only the first one has a chip left
}
var node = document.createTextNode($chip.attr("data-wikitext"));
$chip.replaceWith(node);
$surface.get(0).focus();
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(caretRangeIn(node, event));
}
// Bound before the chip's own handler and stopping there, so removing a call never opens the
// dialog for it on the way out.
$surface.on("click", ".dsb-tpl-chip-remove", function (e) {
e.preventDefault();
e.stopImmediatePropagation();
clearTimeout(chipClickTimer);
lastChipNode = null;
$(this).closest(".dsb-tpl-chip").remove();
syncPanelWidth();
});
$surface.on("click keydown", ".dsb-tpl-chip", function (e) {
if (e.type === "keydown" && e.which !== 13 && e.which !== 32) {
return;
}
e.preventDefault();
var $chip = $(this);
clearTimeout(chipClickTimer);
// A second tap on the same chip edits it inline. Timed here rather than left to "dblclick",
// which mobile browsers fire unreliably or not at all for a double-tap.
var now = new Date().getTime();
if (lastChipNode === this && now - lastChipTap < DOUBLE_TAP_MS) {
lastChipNode = null;
startInlineEdit($chip, e);
return;
}
lastChipTap = now;
lastChipNode = this;
var wikitext = $chip.attr("data-wikitext");
chipClickTimer = setTimeout(function () {
cite.openTemplateDialog(wikitext).then(function (edited) {
applyChipEdit(liveChip($chip, wikitext), edited);
syncPanelWidth();
});
}, DOUBLE_TAP_MS);
});
$surface.on("dblclick", ".dsb-tpl-chip", function (e) {
e.preventDefault();
clearTimeout(chipClickTimer);
startInlineEdit($(this), e);
});
// A long etymology cannot be read in the panel's usual 520px column, so the panel drops its
// width cap once there is enough text to need it. What it will look like is shown by the
// in-place render on the page itself, not by a second copy inside the form.
var WIDE_AT = 240; // characters
function syncPanelWidth() {
var wikitext = readChipSurface($surface.get(0));
if (wikitext.length > WIDE_AT || wikitext.indexOf("\n") !== -1) {
$surface.closest(".dsb-subsection-panel").addClass("dsb-panel-wide");
}
}
// A template typed by hand becomes a chip once typing settles, so it behaves like an inserted one:
// click for the parameter dialog, double-click for its raw wikitext.
var composing = false;
// The caret as an offset into the surface's wikitext, which is what survives rebuilding the DOM.
function caretOffset() {
var sel = window.getSelection();
var root = $surface.get(0);
if (!sel || !sel.rangeCount) {
return -1;
}
var range = sel.getRangeAt(0);
if (range.startContainer !== root && !$.contains(root, range.startContainer)) {
return -1; // caret is somewhere else on the page
}
var pre = document.createRange();
pre.selectNodeContents(root);
pre.setEnd(range.startContainer, range.startOffset);
var holder = document.createElement("div");
holder.appendChild(pre.cloneContents());
return readChipSurface(holder).length;
}
// Puts the caret back at that wikitext offset. Only runs straight after a rebuild, so the surface
// holds nothing but text nodes and chips.
function placeCaret(offset) {
var root = $surface.get(0);
var range = document.createRange();
var nodes = root.childNodes;
root.focus();
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i];
var len = node.nodeType === 3
? node.nodeValue.length
: ($(node).attr("data-wikitext") || $(node).text()).length;
if (offset <= len) {
if (node.nodeType === 3) {
range.setStart(node, offset);
} else {
range.setStartAfter(node); // a chip is atomic; the caret goes after it
}
break;
}
offset -= len;
}
if (!range.startContainer || range.startContainer === document) {
range.selectNodeContents(root);
}
range.collapse(true);
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
// True when the selection overlaps the inside of a template call. A caret resting exactly at the
// closing "}}" does not count -- that is someone who has just finished typing one.
function selectionTouchesCall(wikitext) {
var sel = window.getSelection();
if (!sel || !sel.rangeCount) {
return false;
}
var range = sel.getRangeAt(0);
var root = $surface.get(0);
if (range.startContainer !== root && !$.contains(root, range.startContainer)) {
return false;
}
var from = caretOffset();
var to = from;
if (!range.collapsed) {
var pre = document.createRange();
pre.selectNodeContents(root);
pre.setEnd(range.endContainer, range.endOffset);
var holder = document.createElement("div");
holder.appendChild(pre.cloneContents());
to = readChipSurface(holder).length;
}
return parseTemplateCalls(wikitext).calls.some(function (call) {
return from < call.end && to > call.start;
});
}
function chipifyTyped() {
if (composing) {
return; // never rebuild under an IME
}
var wikitext = readChipSurface($surface.get(0));
// Chips already hold one call each, so a higher call count means one was typed in as text.
if (parseTemplateCalls(wikitext).calls.length <= $surface.find(".dsb-tpl-chip").length) {
return;
}
var offset = caretOffset();
if (selectionTouchesCall(wikitext)) {
return; // still inside a call: leave it as text until the caret moves out
}
renderChipSurface($surface, wikitext);
savedRange = null; // the old range points at nodes that no longer exist
if (offset >= 0) {
placeCaret(offset);
rememberCaret();
}
}
$surface.on("compositionstart", function () {
composing = true;
});
$surface.on("compositionend", function () {
composing = false;
schedulePreview();
});
var previewTimer = null;
function schedulePreview() {
clearTimeout(previewTimer);
previewTimer = setTimeout(function () {
chipifyTyped();
syncPanelWidth();
}, 700);
}
$surface.on("input", schedulePreview);
$surface.data("dsbSchedulePreview", schedulePreview);
$fieldWrap.append(makeSearchable($select, util.msg.addTemplate), $surface);
// Bilêvkirin is written in IPA, so it gets the same character palette the wizard shows. Its
// characters go in as text and its two template buttons as chips, which is what the rest of
// this surface is made of.
if (s.place === "pron") {
$fieldWrap.append(util.buildIpaPalette({
insertText: function (text) {
$surface.focus();
restoreCaret();
insertAtCaret($surface, document.createTextNode(text));
rememberCaret();
schedulePreview();
},
insertTemplate: function (snippet) {
$surface.focus();
restoreCaret();
var $chip = makeTemplateChip(snippet.replace(/\|\s*\}\}$/, "}}"));
insertAtCaret($surface, $chip.get(0));
rememberCaret();
cite.openTemplateDialog($chip.attr("data-wikitext")).then(function (edited) {
applyChipEdit($chip, edited);
syncPanelWidth();
});
}
}));
}
}
// The three "place" sections are wikitext with a shape worth spelling out, since nothing on screen
// says what belongs in the box.
var PLACE_PLACEHOLDERS = {
"page-top": "{{h-bnr|Kelîme|Kelîmeyeke din}} an jî {{herwiha binêre/ane}}",
"pos-top": "[[Wêne:Navê pelî.jpg|thumb|Binnivîs]]",
"pron": "* {{ku-IPA}}"
};
function renderTextArea(s, rows) {
$fieldWrap.empty().append(
$("<textarea class='dsb-input dsb-subsection-text'></textarea>")
.attr("rows", rows)
.attr("placeholder", PLACE_PLACEHOLDERS[s.place] || (s.header + "…"))
);
util.initManualResize($fieldWrap);
}
// A translation box: the {{werger-ser}}/{{werger-bin}} pair, with the sense it covers and the
// layout parameters that template accepts. The rows in between are left to the translation editor
// gadget, which is what edits them on the rendered page.
// Reads and writes through fieldText()/setFieldText() as wikitext, so drafts, save and the queue
// need no special case beyond the append below.
var wergerForm = null;
var imagesField = null;
function renderWergerField(s) {
var inputs = {};
// Its own row class rather than .dsb-tpl-param-row: that one is styled in cite.css, which is
// only on the page when wikt.cite happens to be loaded, and without it the label falls back to
// sitting inline and a short one ("id") is overlapped by its own box.
function row(key, label, placeholder) {
inputs[key] = $("<input type='text' class='dsb-input'>").attr("placeholder", placeholder || "");
return $("<div class='dsb-werger-row'></div>").append(
$("<label class='dsb-adv-label'></label>").text(label).append(inputs[key])
);
}
// {{werger-ser}} also takes stûn, firehî, bgcolor, valign and align, but they only restyle the
// box and its defaults are what these pages use, so the form does not offer them. They are
// still in WERGER_NAMED, so a block that already sets one is read back correctly.
var $main = $("<div class='dsb-werger-main'></div>").append(
row("1", "Mane", "ava vexwarinê"),
row("2", "Mane no", "ne mecbûrî — di parantezê de xuya dike"),
row("id", "id", "ne mecbûrî — IDya tercimeyê")
);
var $note = $("<p class='dsb-werger-note'></p>").text(
"Qutiyeke Wergerê ya vala lê zêde dikî. Tercimeyan bi amûra wergerê dûre binivîse."
);
wergerForm = {
read: function () {
var out = {};
$.each(inputs, function (key, $input) {
var value = $.trim($input.val() || "");
if (value) {
out[key] = value;
}
});
return out;
},
set: function (params) {
$.each(inputs, function (key, $input) {
$input.val((params && params[key]) || "");
});
}
};
$fieldWrap.empty().append($note, $main);
}
// Same [[Kategorî:Çavkanî]] search widget start.js's own picker uses: choosing an entry inserts
// "* {{Target}}" at the cursor. The field itself stays the plain ".dsb-subsection-text" textarea
// every other "kind: text" subsection already uses, so save/prefill/drafts need no changes.
function renderCavkaniField(s) {
var $ta = $("<textarea class='dsb-input dsb-subsection-text'></textarea>")
.attr("rows", 4)
.attr("placeholder", util.msg.pickOrTypeTemplate);
var picker = cite.buildCategoryTemplateSearch({
placeholder: util.msg.addTemplate,
describe: true,
// Same param dialog Etîmolojî opens on a fresh insert: filled from the picked template's own
// TemplateData, so the user adds "rûpel=", "sernav=", ... before anything lands in the field
// rather than typing a bare {{Target}} and going back to fill params in by hand.
onPick: function (entry) {
cite.openTemplateDialog("{{" + entry.target + "}}").then(function (edited) {
if (!edited) {
return; // cancelled, or "Jê bibe" on a call that was never inserted
}
util.insertAtCursor($ta.get(0), "* " + edited + "\n");
$ta.trigger("focus");
});
}
});
var $picker = $("<div class='dsb-cavkani-picker'></div>").append(picker.$element);
$fieldWrap.empty().append($picker, $ta);
}
// One "{{kol3}}" group: its "sernav=" heading and its words. Several of these make up one section --
// "Herwiha binêre" typically carries an unheaded list plus a few headed ones.
function addGroup(s, group) {
var $sernav = $("<input type='text' class='dsb-input'>").attr("placeholder", "ne mecbûrî…");
// {{h-bnr}} takes bare page names, so its rows carry no ⊕: there is nowhere in the call for a
// modifier to go.
var words = buildWordListField(s.header + "…", { modifiers: !s.single });
var $remove = $("<a href='#' class='dsb-quick-link dsb-group-remove'>Komê rake</a>");
var $element = $("<div class='dsb-wordgroup'></div>").append(
// Label beside the box, not above it: stacked, "Sernav" read as a heading for the whole
// group rather than as the name of the field under it. The input is nested in the label so
// clicking the word focuses the box, without needing an id per group.
// A single-call list has no "sernav=" to carry, so the row is built (readGroups still reads
// it) but never shown. Hidden here rather than in renderField: setGroups() rebuilds every
// group on prefill, which would put a visible one back.
$("<div class='dsb-tpl-param-row dsb-param-row-inline'></div>").append(
$("<label class='dsb-adv-label'></label>").text("Sernav").append($sernav)
).toggle(!s.single),
words.$element,
$remove
);
var field = {
$element: $element,
read: function () {
return { sernav: $.trim($sernav.val()), words: words.values() };
},
set: function (g) {
$sernav.val((g && g.sernav) || "");
words.setValues((g && g.words) || []);
}
};
$remove.on("click", function (e) {
e.preventDefault();
var at = $.inArray(field, groupFields);
if (at !== -1) {
groupFields.splice(at, 1);
}
$element.remove();
updateGroupChrome();
});
field.set(group);
groupFields.push(field);
$groups.append($element);
updateGroupChrome();
return field;
}
// With a single group there is nothing to tell apart, so it needs neither a remove link nor a label.
function updateGroupChrome() {
var many = groupFields.length > 1;
$groups.toggleClass("dsb-wordgroups-many", many);
$groups.find(".dsb-group-remove").toggle(many);
}
function readGroups() {
return groupFields.map(function (field) {
return field.read();
});
}
// Rebuilds the blocks to match what the page (or the draft) holds, always leaving at least one.
function setGroups(s, groups) {
groupFields = [];
$groups.empty();
(groups && groups.length ? groups : [null]).forEach(function (group) {
addGroup(s, group);
});
}
// Loads what the page already has into the form, so an edit starts from the current list rather than
// from an empty one. A wordlist section we can't parse degrades to raw wikitext editing.
function prefill(s) {
$status.removeClass("dsb-status-error dsb-status-ok").text("Tê barkirin…");
$save.prop("disabled", true);
fetchPageSubsection(language, sectionType, sectionOrdinal, s).then(function (current) {
$save.prop("disabled", false);
$status.text("");
if (!current) {
return;
}
// A "place" section has no heading of its own, so whether the page already has one cannot
// be seen in the DOM the way a "====" subsection can -- it is known only once its content
// has been read. That is what decides "nûkirin" or "zêdekirin" in the edit summary.
if (s.place) {
s.existing = !!(current.images ? current.images.length : $.trim(current.text || ""));
// The label was set before this content arrived, when nothing yet said one was there.
$save.text(s.existing && s.kind !== "werger" ? util.msg.edit : util.msg.add);
}
if (current.kind === "images" && imagesField) {
imagesField.setValues(current.images);
return;
}
// A free section's field was built before its content was known: a list turns it into the
// {{kol3}} form, anything else leaves it the wikitext box it already is.
if (s.free && current.kind === "wordlist" && !groupFields.length) {
s.kind = "wordlist";
renderWordListField(s);
setGroups(s, current.groups);
if (current.fromBullets) {
$status.text("Ev lîste made bi made hatiye nivîsîn — bi qeydkirinê dibe {{kol3}}.");
}
return;
}
if (current.kind === "wordlist" && groupFields.length) {
setGroups(s, current.groups);
if (current.fromBullets) {
$status.text("Ev lîste made bi made hatiye nivîsîn — bi qeydkirinê dibe {{kol3}}.");
}
return;
}
if (s.kind === "wordlist") {
editAsText = true;
groupFields = [];
renderTextArea(s, 6);
$status.text(s.unknown
? "Ev beş nayê naskirin — wekî wîkîtekst tê guhartin."
: "Şablona vê lîsteyê nehate naskirin — wekî wîkîtekst biguhêre.");
}
setFieldText(current.text);
}, function () {
$save.prop("disabled", false);
$status.addClass("dsb-status-error").text("Nikare muhtewaya heyî bixwîne.");
});
}
// One "{{kol3}}" group per block, plus a way out to raw wikitext for a list no row can express --
// a shared subpage list like {{herwiha binêre/ane}}, say.
function renderWordListField(s) {
$fieldWrap.empty();
chipSurface = null;
editAsText = false;
var $addGroup = $("<a href='#' class='dsb-quick-link'>+ Komek lê zêde bike</a>");
$addGroup.on("click", function (e) {
e.preventDefault();
addGroup(s, null);
});
$groups = $("<div class='dsb-wordgroups'></div>");
$fieldWrap.append($groups, $addGroup);
addGroup(s, null);
// One call takes one flat list of names, so there is no second group to add.
if (s.single) {
$addGroup.hide();
}
var $toRaw = $("<a href='#' class='dsb-quick-link'>Wekî wîkîtext biguhêre</a>");
$toRaw.on("click", function (e) {
e.preventDefault();
var current = s.place
? wt.buildHbnrCall(loosePlaceWords(readGroups()))
: subsectionBodyWikitext(language, s, readGroups());
editAsText = true;
groupFields = [];
$groups = null;
renderTextArea(s, 3);
setFieldText(current);
util.initManualResize($fieldWrap);
});
$fieldWrap.append($toRaw);
util.initManualResize($fieldWrap);
}
function renderField() {
var s = currentSubsection();
$fieldWrap.empty();
groupFields = [];
$groups = null;
editAsText = false;
chipSurface = null;
wergerForm = null;
imagesField = null;
$status.removeClass("dsb-status-error dsb-status-ok").text("");
// Save stays disabled (but "Betal bike" usable) until something is actually chosen.
$save.prop("disabled", !s);
if (!s) {
$save.text(util.msg.add);
lastId = null;
return; // still on "Binebeşekê hilbijêre"
}
if (s.kind === "wordlist") {
renderWordListField(s);
} else if (s.kind === "images") {
imagesField = util.buildImageListField({
prefix: "dsb-",
rowClass: "dsb-tpl-param-row dsb-param-row-inline",
inputClass: "dsb-input",
addClass: "dsb-quick-link",
pageName: mw.config.get("wgTitle")
});
$fieldWrap.append(imagesField.$element);
} else if (s.header === "Etîmolojî") {
renderTemplateEditor(s, templateChoices("etymTpls", language));
} else if (s.place === "pron") {
renderTemplateEditor(s, templateChoices("pronTpls", language));
} else if (s.kind === "werger") {
renderWergerField(s);
} else if (s.header === "Çavkanî") {
renderCavkaniField(s);
} else {
renderTextArea(s, 3);
}
// Werger always adds a box, even to a section that is already there, so its button never says
// "Biguhêre".
$save.text(s.existing && s.kind !== "werger" ? util.msg.edit : util.msg.add);
// Covers whichever branch above built the field. Idempotent, so a textarea already carrying a
// grip is skipped.
util.initManualResize($fieldWrap);
lastId = s.id;
// A draft always wins over re-reading the page: it is the newer, unsaved state.
if (drafts[s.id]) {
restoreDraft(s, drafts[s.id]);
} else if ((s.existing || s.place) && s.kind !== "werger") {
prefill(s);
}
}
$select.on("change", function () {
captureDraft();
renderField();
});
renderField();
// Back to the state a fresh open would show: no draft, no picked subsection. Called directly
// rather than through the "change" handler above, so the discarded draft is never captured first.
function resetForm() {
drafts = {};
if (!onlyHeader) {
$select.val("").trigger("wiktku:refresh");
}
renderField();
}
$save.on("click", function () {
var s = currentSubsection();
if (!s) {
return;
}
var asText = editAsText || (s.kind !== "wordlist" && s.kind !== "images");
var effective = asText ? $.extend({}, s, { kind: "text" }) : s;
var content;
if (asText) {
content = fieldText();
if (!content) {
return;
}
} else if (s.kind === "images") {
content = imagesField ? imagesField.values() : [];
if (!content.length) {
return;
}
} else {
content = readGroups();
if (!content.some(function (group) { return group.words.length; })) {
return;
}
}
$save.prop("disabled", true);
// A Werger section holds any number of boxes, so an existing one is appended to rather than
// replaced; every other kind edits the single body it owns.
var appending = s.kind === "werger" && s.existing;
queueChange(
subsectionPlaceLabel(s, language, sectionType, $h3),
s.kind === "werger"
? "qutiyeke wergerê hate zêdekirin"
: "beşa «" + s.header + "» hate " + (s.existing ? "nûkirin" : "zêdekirin"),
function (pageContent) {
var body = content;
if (appending) {
var current = readPageSubsection(pageContent, language, sectionType, sectionOrdinal, effective);
var existingText = current ? $.trim(current.text || "") : "";
body = existingText ? existingText + "\n\n" + content : content;
}
return {
content: s.free
? updateFreeSection(pageContent, language, effective, body)
: (s.place
? writeLoosePlace(pageContent, language, sectionType, sectionOrdinal, effective,
loosePlaceBody(effective, body, language))
: (s.existing
? updatePageSubsection(pageContent, language, sectionType, sectionOrdinal, effective, body)
: insertPageSubsection(pageContent, language, sectionType, sectionOrdinal, effective, body))),
warnings: []
};
},
$save,
$status,
// Werger is a box the translation tools fill in afterwards, so there is nothing to draw for
// it. A new subsection gets a block of its own at the position it will land in; one already
// on the page is redrawn where it stands.
s.kind === "werger" ? null : function () {
// A free section is redrawn under its own heading, which is the element the panel hangs
// on.
if (s.free) {
var restoreFree = replaceFreeSectionInPlace($h3,
subsectionBodyWikitext(language, effective, content));
return function () {
if (restoreFree) {
restoreFree();
restoreFree = null;
}
};
}
// A "place" section has no heading to redraw between, so it is found in the DOM by what
// it renders as and replaced there.
if (s.place) {
var restoreLoose = replaceLoosePlaceInPlace($h3, effective,
loosePlacePreviewWikitext(effective, content, language));
return function () {
if (restoreLoose) {
restoreLoose();
restoreLoose = null;
}
};
}
if (!s.existing) {
var $preview = buildSubsectionPreviewDom($h3, s, content, language);
return function () {
$preview.remove();
};
}
var restore = replaceSubsectionInPlace($h3, effective.header,
subsectionBodyWikitext(language, effective, content));
return function () {
if (restore) {
restore();
restore = null;
}
};
},
{ key: s.free
? "section:" + s.header + "|" + s.free.ordinal
: "subsection:" + sectionType + "|" + sectionOrdinal + "|" + s.header }
);
});
$panel.empty().append($selectWrap, $fieldWrap, $actions);
}
// Builds the "choose a part of speech" picker that reclassifies a section -- an unclassified
// "=== Mane ===" placeholder, or a "=== Biwêj ===" that turns out to be a real part of speech. Saved via
// convertSectionToPos(); lists every name from sectionNames except the one the section already is.
function buildConvertPosPanel($panel, $h3, language, sectionType, sectionOrdinal) {
var $select = $("<select class='dsb-convert-pos-select'></select>");
// Label-less, like the subsection picker's: it holds the "nothing chosen yet" value without showing
// up as a row, so the prompt lives in the input's placeholder instead.
$select.append($("<option value=''></option>"));
Object.keys(sectionNames).forEach(function (name) {
// The section's own part of speech is listed too, marked: picking it changes nothing about the
// heading and leaves the parameters below to be edited on their own.
$select.append($("<option></option>").val(name)
.text(name === sectionType ? name + " (ya niha)" : name));
});
var $selectWrap = makeSearchable($select, "Li cureyekî rêzimanî bigere");
// Opens on the section's own part of speech, so editing only its parameters needs no pick.
if (sectionNames[sectionType]) {
$select.val(sectionType).trigger("wiktku:refresh");
}
// The same card the entry wizard shows under its part-of-speech question -- gender, transitivity,
// the transliteration boxes and everything behind "Tercîhên pêşketî". Shared from
// wikt.edit/posparams.js, so reclassifying a section here offers exactly what creating one in the
// wizard does.
var card = posparams.build({ lang: language, pos: "" });
// Opened on the call the section already has, so this edits the current template rather than
// starting from a blank line. setContext() rebuilds the line whenever the part of speech moves,
// so the prefill is only the starting point.
var spec0 = posTemplateFor(sectionType);
card.setContext(language, spec0 ? spec0.name : "", /^Formeke /.test(sectionType));
fetchPageContent().then(function (pageContent) {
try {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var found = findPosTemplateLine(loc.lines, loc.posLineIndex, sectionType);
if (found.index !== -1 && ($select.val() || sectionType) === sectionType) {
card.setWikitext(found.text);
}
} catch (e) { /* an unreadable section just leaves the card as it was */ }
});
$select.on("change", function () {
var spec = posTemplateFor($select.val());
card.setContext(language, spec ? spec.name : "", /^Formeke /.test($select.val() || ""));
});
// "gh" on its own, or "<form>-<base>" once a structural form is picked -- start.js's own composition.
// convertSectionToPos() writes the template itself, so only what follows "{{pos|lang" is wanted
// from the card's line.
function posParams() {
var m = /^\{\{[^|}]*\|[^|}]*\|?([\s\S]*)\}\}$/.exec(card.wikitext());
return m ? m[1] : "";
}
var $save = $("<button type='button' class='dsb-save'></button>").text(util.msg.add);
var $cancel = $("<button type='button' class='dsb-cancel'></button>").text(util.msg.cancel);
var $status = $("<span class='dsb-status'></span>");
var $actions = $("<div class='dsb-form-actions'></div>").append($save, $cancel, $status);
// dsb.js owns the pencil and the panel's built state, so it does the closing.
$cancel.on("click", function () {
$panel.trigger("dsbCancel");
hideQueueIfIdle();
});
$save.on("click", function () {
var chosen = $select.val();
if (!chosen) {
$status.removeClass("dsb-status-ok").addClass("dsb-status-error").text("Pêşî cureyekî rêzimanî hilbijêre.");
return;
}
var params = posParams();
$save.prop("disabled", true);
$status.removeClass("dsb-status-error").text("");
queueChange(
langLabel(language),
"beşa «" + sectionLabel(sectionType, $h3) + "» bû «" + chosen + "»",
function (pageContent) {
return { content: convertSectionToPos(pageContent, language, sectionType, sectionOrdinal, chosen, params), warnings: [] };
},
$save,
$status,
function () {
// The heading keeps its text, which every other queued change finds the section by; a
// note under it names the new one. The headword line is drawn as the save will write it.
var $note = $("<div class='dsb-preview-inline dsb-preview-heading-note'></div>")
.text("→ " + chosen + (params ? " (" + params + ")" : "") + " — nû, hîn nehatiye qeydkirin");
headingOuter($h3).after($note);
var dropped = false;
var restoreHeadword = null;
fetchPageContent().then(function (pageContent) {
if (dropped) {
return;
}
var call;
try {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
call = posCallLine(findPosTemplateLine(loc.lines, loc.posLineIndex, sectionType),
language, chosen, params);
} catch (e) {
return;
}
restoreHeadword = replaceHeadwordInPlace($h3, call);
});
return function () {
dropped = true;
$note.remove();
if (restoreHeadword) {
restoreHeadword();
restoreHeadword = null;
}
};
},
// Runs after every other queued change: it renames the "Mane" heading those changes locate
// themselves by, so anything edited on this section has to be applied first.
{ runLast: true, key: "convert:" + sectionType + "|" + sectionOrdinal, pagePosition: $("h3, h4").index($h3) }
);
});
$panel.empty().append($selectWrap, card.$element, $actions);
}
// Attaches the "+ Beşek lê zêde bike" trigger right after a POS block's own last element.
// Puts the same pencil the definitions use on every existing "====" subsection heading the gadget knows how
// to edit, opening buildSubsectionPanel() pinned to that one subsection.
module.exports = {
buildAdvancedPanel: buildAdvancedPanel,
buildSubsectionPanel: buildSubsectionPanel,
buildConvertPosPanel: buildConvertPosPanel,
editExample: editExample,
clearQueue: clearQueue,
closeAllPanels: closeAllPanels,
openQueueBox: openQueueBox
};
}());
// </nowiki>
g9wrik17o4k4vtlfkwy1okf9a04rrly
6531985
6531968
2026-09-17T16:47:24Z
Wikihez
7344
Subsection panel: rename a heading (Nav) and remove it (Beşê jê bibe), queued last and bottom-up
6531985
javascript
text/javascript
// <nowiki>
/*
* Page: [[MediaWiki:Gadget-wikt.dsb-panels.js]]
*
* Everything DefSideBoxes shows once you click: the per-sense advanced panel, the subsection editors,
* the part-of-speech picker, the template dialogs, the preview and the queued save.
*
* Loaded on demand by wikt.dsb, never on a plain page view. It exports only what that half calls.
*/
(function () {
"use strict";
var sections = require("ext.gadget.wikt.core").sections;
var edit = require("ext.gadget.wikt.edit");
var cite = require("ext.gadget.wikt.cite");
var util = edit.util;
var posparams = edit.posparams;
var wt = edit.wt;
var startData = edit.data;
var langs = edit.langs;
var escapeRegExp = util.escapeRegExp;
// Local names for the shared wikitext layer, so every call site below reads as it always did.
var langHeaderSource = wt.langHeaderSource;
var canonicalTemplateKey = wt.canonicalTemplateKey;
var aliasPattern = wt.aliasPattern;
var splitLeadingCalls = wt.splitLeadingCalls;
var splitTemplateParams = wt.splitTemplateParams;
var parseTemplateCalls = wt.parseTemplateCalls;
var splitCallParams = wt.splitCallParams;
var buildTemplateCall = wt.buildTemplateCall;
var tdText = wt.tdText;
var fetchTemplateData = wt.fetchTemplateData;
var parseWordListCall = wt.parseWordListCall;
var parseWordListCalls = wt.parseWordListCalls;
var namedParam = wt.namedParam;
var withNamedParam = wt.withNamedParam;
var buildWordListCall = wt.buildWordListCall;
var parseBulletWordList = wt.parseBulletWordList;
var sectionNames = sections.sectionNames;
var MANE_SECTION = sections.MANE_SECTION;
var PAGE_SUBSECTIONS = sections.PAGE_SUBSECTIONS;
var headingOuter = sections.headingOuter;
var headingText = sections.headingText;
var isManeHeading = sections.isManeHeading;
var sectionTypeOf = sections.sectionTypeOf;
var sectionOrdinalWithinLanguage = sections.sectionOrdinalWithinLanguage;
var isFormOfWrapper = sections.isFormOfWrapper;
var eachLeafDefinition = sections.eachLeafDefinition;
var api = new mw.Api();
// metadata[lang].g, minus the grammatical-number codes start.js filters out with isNumberCode(), rendered
// the way its genderLabel() does ("m-p" -> "mê (pirjimar)").
// start.js asks transitivity and structural form as two questions and joins them as "<form>-<base>", so
// "gerguhêz" alone is "gh" and "xwerû" + "gerguhêz" is "x-gh".
// Template call convertSectionToPos() drops under a newly-chosen POS heading, e.g. "Hoker" -> "{{hoker|ku}}".
// A "Formeke X" (form-of) heading uses the unchanged base pos template. Doubles as the list of template
// calls a conversion may find under the OLD heading and replace, rather than leave stacked beside the new one.
// A section's template call. "Formeke Xê" uses X's own template with form=erê. Biwêj is recognised so a
// conversion can replace its {{biwêj}}, but never written: no new ones.
function posTemplateFor(header) {
var form = /^Formeke (.+)ê$/.exec(header || "");
if (form && startData.posChoices[form[1]] && sections.hasFormHeading(form[1])) {
return { name: form[1], params: ["form=erê"] };
}
var key = util.decapitalize(header || "");
return startData.posChoices[key] ? { name: key, params: [] } : null;
}
// convertSectionToPos() drops whichever of the tag's names is actually present.
var MANE_TAG_REGEX = wt.MANE_TAG_REGEX;
// The six per-sense groups start.js writes as "#: {{tpl|lang|word1|word2}}" sub-lines under a "#" definition.
// Array order controls where a new group line lands relative to existing ones. Every group lives behind the
// quick row's "Hevmane û Bêhtir" dropdown; only the gloss and Nimûne/jêgirtin links sit in the row itself.
var SENSE_GROUPS = startData.senseGroups;
// The column word-list template new sections are written with; the rest of wordlistTemplates are
// spellings the parser also accepts.
var WORDLIST_TEMPLATE = startData.wordlistTemplates[0];
// ---- {{werger-ser}} / {{werger-bin}}: one translation box ----
// The pair brackets the rows; the translation editor gadget writes the rows themselves onto the
// rendered page, so this only ever builds (and reads back) the opening call.
// Parameters, from [[Şablon:werger-ser]]: 1 = the sense shown in the box head, 2 = a note in
// parentheses after it, id = anchor override (defaults to 1), stûn = column count, firehî = column
// width (default 26em), bgcolor, valign (default top), align (default left).
var WERGER_NAMED = ["id", "stûn", "firehî", "bgcolor", "valign", "align"];
function buildWergerBlock(params) {
var parts = ["werger-ser"];
// Positional slots: 2 can only be reached past 1, so an empty 1 still has to be written.
if (params["1"] || params["2"]) {
parts.push(params["1"] || "");
}
if (params["2"]) {
parts.push(params["2"]);
}
WERGER_NAMED.forEach(function (key) {
if (params[key]) {
parts.push(key + "=" + params[key]);
}
});
return "{{" + parts.join("|") + "}}\n{{werger-bin}}";
}
// The opening call of the first block in `text`, as the same {1, 2, id, ...} object. Empty object when
// there is none, so a half-typed draft restores as an empty form rather than throwing.
function parseWergerBlock(text) {
var out = {};
var body = wergerSerBody(String(text || ""));
if (!body) {
return out;
}
var positional = 0;
splitTemplateArgs(body).forEach(function (arg) {
var eq = arg.indexOf("=");
var name = eq === -1 ? "" : $.trim(arg.slice(0, eq));
if (eq !== -1 && WERGER_NAMED.indexOf(name) !== -1) {
out[name] = $.trim(arg.slice(eq + 1));
} else {
out[String(++positional)] = $.trim(arg);
}
});
return out;
}
// Everything after "{{werger-ser|" up to the "}}" that closes it, or "" when there is no call or it
// takes no parameters. Scans for the matching close rather than the first one: a gloss can hold a
// template of its own, and a lazy regex would stop inside it.
function wergerSerBody(text) {
var m = /\{\{\s*werger-ser\s*(?=[|}])/.exec(text);
if (!m) {
return "";
}
var depth = 1;
var start = m.index + m[0].length;
for (var i = start; i < text.length - 1; i++) {
var two = text.substr(i, 2);
if (two === "{{") {
depth++; i++;
} else if (two === "}}") {
depth--; i++;
if (!depth) {
return text.charAt(start) === "|" ? text.slice(start + 1, i - 1) : "";
}
}
}
return "";
}
// Splits on "|" at brace/bracket depth 0, so a value containing a template or a link stays whole.
function splitTemplateArgs(body) {
var args = [];
var depth = 0;
var current = "";
for (var i = 0; i < body.length; i++) {
var two = body.substr(i, 2);
if (two === "{{" || two === "[[") {
depth++; current += two; i++; continue;
}
if (two === "}}" || two === "]]") {
depth--; current += two; i++; continue;
}
if (body[i] === "|" && depth <= 0) {
args.push(current); current = ""; continue;
}
current += body[i];
}
args.push(current);
return args;
}
// Canonical ku.wiktionary "====" subsection order, used only as insertPageSubsection()'s insertion rank
// (see subsectionRankForHeader()). Wider than PAGE_SUBSECTIONS: it also covers subsections the gadget
// cannot create but still has to order around.
var SUBSECTION_ORDER = startData.subsectionOrder;
// ---- searchable <select> ----------------------------------------------------------------------------
// Same widget start.js gets from its util.js wrapNativeSelect(): the native <select> stays as the value
// holder (so .val() and "change" handlers keep working) but is hidden behind a text input that filters an
// overlay list. Ported rather than required() because util.js is a file inside start.js's package module,
// which another gadget cannot reach; the template-row decoration start.js uses is left out.
// util.js's wrapNativeSelect(): the native <select> stays the value holder behind a filtering text input.
// It reads its own placeholder off data-placeholder, and its wiktku-* styles ship in start.css, so both
// gadgets get the identical widget instead of two lookalikes.
function makeSearchable($select, placeholder) {
$select.attr("data-placeholder", placeholder || "Lê bigere…");
return util.wrapNativeSelect($select);
}
// ---- small shared utilities ----
// The readable half of an mw.Api rejection.
var apiErrorMessage = util.apiErrorMessage;
// Thrown from inside an edit() transform to bail out with a specific message.
var SubmitAbort = util.SubmitAbort;
// A gloss is one field on the page: the sense, then {{îzah|…}} explaining it when there is one.
function joinGloss(mane, izah) {
return izah ? $.trim(mane + " {{îzah|" + izah + "}}") : $.trim(mane);
}
// The two halves are linked separately, never joined first: linkify leaves template calls alone by
// design, so anything inside {{îzah|…}} would never be looked at.
//
// A half is skipped when there is nothing in it worth looking up, and -- when onlyUnlinked is set --
// when it already carries a link. Someone who has linked a word by hand has made that half theirs, and
// a save must not go on linking the rest of it behind their back; "Oto-lînk bike" leaves the flag off
// and still links on demand.
function linkifyGlossParts(mane, izah, done, onlyUnlinked) {
function needsLinking(text) {
if (!$.trim(text) || !util.hasLinkableText(text)) {
return false;
}
return !(onlyUnlinked && text.indexOf("[[") !== -1);
}
function step(text, next) {
if (!needsLinking(text)) {
next(text, null);
return;
}
util.linkifyText(text, function (linked, error) {
// An empty answer would wipe what the user typed, so only real text is taken.
next(typeof linked === "string" && $.trim(linked) ? $.trim(linked) : text, error || null);
});
}
step(mane, function (linkedMane, maneError) {
step(izah, function (linkedIzah, izahError) {
done(linkedMane, linkedIzah, maneError || izahError);
});
});
}
// Runs transformFn(content) inside mw.Api().edit()'s own transform callback so one fetch covers both the
// edit-conflict check and the content, preventing a stale overwrite of someone else's edit.
//
// No badtoken retry here on purpose: edit() posts through postWithEditToken -> postWithToken, which
// already throws the cached token away and repeats the request once when the wiki says "badtoken".
// A second retry of our own would only double it.
function editPage(summary, transformFn, onSuccess, onError) {
api.edit(mw.config.get("wgPageName"), function (revision) {
return { text: transformFn(revision.content), summary: summary };
}).then(onSuccess, function (codeOrAbort, result) {
if (codeOrAbort instanceof SubmitAbort) {
onError(codeOrAbort.message);
} else {
onError(util.msg.saveFailed + apiErrorMessage(codeOrAbort, result));
}
});
}
// ---- rendered preview: wikitext through the parse API, so a preview shows what the templates
// actually produce rather than a plain list standing in for them ----
// Same text is usually asked for twice (open the preview, close it, open it again), so the promise is
// kept rather than the call repeated.
var renderCache = {};
/**
* Renders wikitext as this page would render it. Resolves with an HTML string.
*
* pst: 1 applies pre-save transform, so ~~~~ and subst: behave as they will once saved. The
* modulestyles/modules the parse reports are loaded before resolving: without them TemplateStyles is
* missing and the preview would show the right markup in the wrong clothes.
*/
function renderWikitext(wikitext) {
if (renderCache[wikitext]) {
return renderCache[wikitext];
}
renderCache[wikitext] = api.post({
action: "parse",
format: "json",
formatversion: 2,
title: mw.config.get("wgPageName"),
text: wikitext,
contentmodel: "wikitext",
prop: "text|modules|jsconfigvars",
pst: 1,
disablelimitreport: 1,
disableeditsection: 1,
disabletoc: 1
}).then(function (data) {
var parsed = (data && data.parse) || {};
if (parsed.modulestyles && parsed.modulestyles.length) {
mw.loader.load(parsed.modulestyles);
}
if (parsed.modules && parsed.modules.length) {
mw.loader.load(parsed.modules);
}
return parsed.text || "";
}, function () {
// A failed render must not be remembered, or the next attempt gets the rejection back.
delete renderCache[wikitext];
return $.Deferred().reject().promise();
});
return renderCache[wikitext];
}
/**
* Puts rendered wikitext into $target, replacing whatever placeholder is there.
*
* The site's own gadgets are told about it through the "wikipage.content" hook, the same way
* MediaWiki announces real content, so collapsible lists and the like behave in the preview too.
*/
// MediaWiki warns when the hook is fired on content that is not in the document, and a gadget that
// listens to it can do nothing useful with nodes nobody can see.
function announceContent($el) {
if ($el.closest("html").length) {
mw.hook("wikipage.content").fire($el);
}
}
function fillWithRendered($target, wikitext, onFail) {
return renderWikitext(wikitext).then(function (html) {
if (!$target.closest("body").length) {
return; // the preview was dropped while the parse was in flight
}
$target.empty().append($("<div class='mw-parser-output'></div>").html(html));
announceContent($target);
}, function () {
if (onFail) onFail();
});
}
/**
* Puts a rendered sense in place of the one on the page, keeping this gadget's own furniture (the
* pencil's toolbar, the quick row, any open panel) where it is. The page's own nodes are detached
* rather than dropped, so the returned function can put them straight back when the change is
* cancelled or superseded.
*
* Nothing labels the result: the change is shown by the sense simply reading the way it will read,
* and the queue box is what says nothing is saved yet.
*/
function replaceSenseInPlace($li, item) {
var html = item.html;
var $anchor = $li.children(".dsb-toolbar").last();
// class/id/data-* off the parsed <li>, so a queued maneid shows its pill here exactly as it will
// once saved. The old values are kept for the restore below.
var node = $li.get(0);
var before = {};
$.each(node.attributes, function (_, attr) {
before[attr.name] = attr.value;
});
$.each(item.attrs, function (name, value) {
if (name !== "class") {
$li.attr(name, value);
}
});
var addedClasses = (item.attrs["class"] || "").split(/\s+/).filter(function (c) {
return c && !$li.hasClass(c);
});
$li.addClass(addedClasses.join(" "));
// add-examples hangs its own <ul> off the sense and opens its form as that <ul>'s next sibling.
// Detaching those leaves the form to open into a tree nobody can see -- "+ Nimune/jegirtin" takes
// the click and shows nothing -- so they stay where they are. Only the rows already in that <ul>
// are hidden, since the preview draws the examples itself; the tool's own title line stays.
var $keep = $li.children("ul, .add-example-form");
var $hiddenRows = $keep.filter("ul").children("li").filter(function () {
return !$(this).find(".add-example-form-title").length;
});
$hiddenRows.addClass("dsb-preview-hidden");
var originals = $li.contents().filter(function () {
return !$(this).is(".dsb-toolbar, .dsb-quick-row, .dsb-subsection-panel, .dsb-preview-block, .dsb-sense-preview");
}).not($keep).get();
// Where the sense's own nodes sat, so cancelling puts them back there rather than above the quick
// row -- the row belongs between the sense and its examples.
var backAfter = originals.length ? originals[0].previousSibling : null;
var $rendered = $("<span class='dsb-sense-preview'></span>").html(html);
if ($anchor.length) {
$anchor.after($rendered);
} else {
$li.prepend($rendered);
}
$(originals).detach();
announceContent($rendered);
// The id pill is drawn by the same code a page load uses, so a queued maneid looks like a saved
// one rather than like something this gadget invented.
if (window.wikt && wikt.showIDs && wikt.showIDs.process) {
wikt.showIDs.process($li);
}
// dsb.js puts the example pencils back on the redrawn examples. The originals keep theirs.
$li.triggerHandler("dsbSenseRedrawn");
return function () {
$hiddenRows.removeClass("dsb-preview-hidden");
$li.removeClass(addedClasses.join(" "));
$.each(item.attrs, function (name) {
if (name === "class") {
return;
}
if (Object.prototype.hasOwnProperty.call(before, name)) {
$li.attr(name, before[name]);
} else {
$li.removeAttr(name);
}
});
$li.children(".senseid-displayed, .etymid-displayed, .etymonid-displayed").remove();
if (backAfter && backAfter.parentNode === node) {
$(backAfter).after(originals);
} else if ($anchor.length) {
$anchor.after(originals);
} else {
$li.prepend(originals);
}
$rendered.remove();
};
}
/**
* The <li> out of a parsed "#" block, and the attributes it carries.
*
* The parse wraps it in its own <ol>, which must not reach the page: the sense already has a number
* from the list it lives in. A leading {{maneid}} also makes the parser emit an empty
* "li.mw-empty-elt" ahead of the real one, so the first <li> is not necessarily the sense.
*
* The attributes matter as much as the content: {{maneid|ku|x}} renders as class="senseid"
* data-id="x" on the <li> itself, which is what wikt.showids draws its pill from.
*/
function parsedSenseItem(html, depth) {
var $item = $("<div></div>").html(html).find("ol > li").filter(function () {
return !$(this).hasClass("mw-empty-elt");
}).first();
// A sub-sense parsed on its own comes back one <ol> deeper per "#": "##" is a list inside a list.
// Descend as far as the sense's own depth, or its number would be drawn inside a second list --
// "2." with a "1." under it.
for (var level = 1; level < (depth || 1) && $item.length; level++) {
var $inner = $item.children("ol").children("li").filter(function () {
return !$(this).hasClass("mw-empty-elt");
}).first();
if (!$inner.length) {
break;
}
$item = $inner;
}
if (!$item.length) {
return { html: html, attrs: {} };
}
var attrs = {};
$.each($item.get(0).attributes, function (_, attr) {
attrs[attr.name] = attr.value;
});
return { html: $item.html(), attrs: attrs };
}
// This gadget's own save queue + floating widget, separate from the shared Gadget-Editor queue.
// Each entry keeps its own transformFn so saveQueueNow() can re-run all of them against one fresh fetch.
var dsbQueue = {
// [{key, summary, transformFn, runLast, removePreview}]; transformFn: baseContent -> {content, warnings}.
// removePreview lives on the entry so replacing one entry takes its own live DOM preview with it.
entries: []
};
var $queueBox = null;
function dropPreview(entry) {
if (typeof entry.removePreview === "function") {
try {
entry.removePreview();
} catch (e) { /* a preview failing to clean up should never block the queue itself */ }
entry.removePreview = null;
}
}
function clearAllPreviews() {
dsbQueue.entries.forEach(dropPreview);
}
function clearQueue() {
clearAllPreviews();
dsbQueue.entries = [];
if ($queueBox) {
$queueBox.$list.empty();
$queueBox.$box.hide();
}
}
// Closes every editor UI on the page. Each open box's own "x" clears its getter, restores its picker
// link and updates the counter, so those are triggered rather than repeating that work here; hiding a
// row alone would leave its fields populated and the next pencil click would reopen them half-filled.
function closeAllPanels() {
$(".dsb-open-boxes .dsb-box-remove").trigger("click");
$(".dsb-quick-note").hide().text("");
$(".dsb-more-menu").hide();
$(".dsb-subsection-panel, .dsb-convert-pos-panel").hide();
// dsb.js owns each pencil's pinned state; tell it rather than hiding behind its back, or the next
// click on the pencil would only toggle "pinned" off and appear to do nothing.
$(".dsb-quick-row").trigger("dsbCancel");
}
function ensureQueueBox() {
if ($queueBox) {
return $queueBox;
}
var $box = $("<div class='dsb-queue-box'></div>").hide();
var $close = $("<a href='#' class='dsb-queue-close' title='Veşêre'>×</a>");
var $title = $("<div class='dsb-queue-title'></div>").text("Guhartinên li benda qeydkirinê");
var $list = $("<ul class='dsb-queue-list'></ul>");
var $empty = $("<div class='dsb-queue-empty'></div>").text(
"Guhartinên te dê li vir werin rêzkirin. Heta ku tu pêl «Guhartinan qeyd bike» nekî, "
+ "tiştek li ser rûpelê nayê weşandin."
);
var $status = $("<div class='dsb-queue-status'></div>");
var $save = $("<button type='button' class='dsb-queue-save'>Guhartinan qeyd bike</button>");
var $cancel = $("<button type='button' class='dsb-queue-cancel'></button>").text(util.msg.cancel);
var $actions = $("<div class='dsb-queue-actions'></div>").append($save, $cancel);
$close.on("click", function (e) {
e.preventDefault();
$box.hide();
});
$cancel.on("click", function () {
clearQueue();
closeAllPanels();
});
$save.on("click", function () {
saveQueueNow();
});
$box.append($close, $title, $empty, $list, $actions, $status);
$(document.body).append($box);
$queueBox = { $box: $box, $list: $list, $empty: $empty, $status: $status, $save: $save, $cancel: $cancel };
return $queueBox;
}
// Takes one change out of the queue and puts the page back as it was before it, its in-place preview
// included. The queue is the only record of what "Guhartinan qeyd bike" will write, so every line in it
// has to be removable on its own -- closing the panel that made a change does not unmake it.
// Each preview draws the page the whole queue would save, so one entry leaving changes what all the
// others should show. They are taken down and drawn again from what is left.
function redrawPreviews() {
dsbQueue.entries.forEach(function (entry) {
if (typeof entry.previewFn !== "function") {
return;
}
dropPreview(entry);
try {
var removePreview = entry.previewFn();
if (typeof removePreview === "function") {
entry.removePreview = removePreview;
}
} catch (e) { /* a broken preview never stops the queue */ }
});
}
function dropQueueEntry(entry) {
var at = dsbQueue.entries.indexOf(entry);
if (at === -1) {
return;
}
dropPreview(entry);
dsbQueue.entries.splice(at, 1);
redrawPreviews();
renderQueueBox();
hideQueueIfIdle();
}
function renderQueueBox() {
var box = ensureQueueBox();
box.$list.empty();
dsbQueue.entries.forEach(function (entry) {
var $drop = $("<button type='button' class='dsb-queue-drop'>×</button>")
.attr("title", "Vê guhartinê ji rêzê derxe");
$drop.on("click", function () {
dropQueueEntry(entry);
});
box.$list.append($("<li></li>").text(entry.summary).append($drop));
});
box.$empty.toggle(!dsbQueue.entries.length);
box.$status.removeClass("dsb-status-ok dsb-status-error").text("");
box.$save.prop("disabled", !dsbQueue.entries.length);
box.$box.show();
}
// Shown the moment a panel opens, empty, so it is clear before anything is typed that a change waits
// for "Guhartinan qeyd bike" rather than publishing itself.
function openQueueBox() {
renderQueueBox();
}
// Nothing queued and no form left open means nothing to save, so the box goes away rather than
// lingering on an empty state.
function hideQueueIfIdle() {
if (dsbQueue.entries.length || !$queueBox) {
return;
}
if ($(".dsb-quick-row:visible, .dsb-subsection-panel:visible, .dsb-convert-pos-panel:visible").length) {
return;
}
$queueBox.$box.hide();
}
// Asks core's postEdit module to show its toast on the next load (dsb.js loads it there), then reloads.
// Loaded here rather than declared as a dependency, so a reload happens even if it fails.
function reloadWithPostEditNotice() {
mw.loader.using("mediawiki.action.view.postEdit").then(function (require) {
require("mediawiki.action.view.postEdit").fireHookOnPageReload("saved");
}).always(function () {
location.reload();
});
}
// A transform returns either the new page text or {content, warnings}; both shapes are accepted.
// One that returns neither stops the save, named, rather than writing a broken page.
function transformedContent(entry, pageContent) {
var result = entry.transformFn(pageContent);
var next = typeof result === "string" ? result : (result && result.content);
if (typeof next !== "string") {
throw new SubmitAbort("Guhartin nehate sepandin: " + entry.summary);
}
return next;
}
// Queue order, except that runLast entries move to the end: converting "=== Mane ===" into a real POS
// heading renames the very heading every other queued change locates itself by.
// Those run from the bottom of the page up: a rename changes the ordinals of the sections after it
// ("Mane 2" becomes "Mane 1"), never of the ones before.
function orderedEntries() {
var normal = [];
var last = [];
dsbQueue.entries.forEach(function (entry) {
(entry.runLast ? last : normal).push(entry);
});
last.sort(function (a, b) {
return b.pagePosition - a.pagePosition;
});
return normal.concat(last);
}
/**
* The pending page as previews and example lookups need it: every queued transform, in save order,
* except the runLast section renames. Those callers find their section under the heading it has on
* the page now, which a queued "Mane" -> "Navdêr" would take away, and a rename leaves the senses as
* they are. An entry that cannot apply is skipped -- the save reports that properly, and a preview
* must not shout.
*/
function queuedContent(pageContent) {
var content = pageContent;
orderedEntries().filter(function (entry) {
return !entry.runLast;
}).forEach(function (entry) {
try {
content = transformedContent(entry, content);
} catch (e) {
// Left out of the preview; the save reports it properly. Named, so a change that fails to
// draw is distinguishable from one with nothing to draw.
mw.log.warn("wikt.dsb: pêşdîtin bêyî vê guhartinê hate xêzkirin — " + entry.summary, e);
}
});
return content;
}
/**
* Redraws one sense where it sits, from the page the queue would save -- so a queued gloss edit, a
* queued example and everything else pending on that sense are shown together, rendered by the parser
* rather than described in a box. Returns the queue's own "take the preview down" function.
*
* One preview per sense: a second queued change takes the first drawing down and draws the queue.
*/
function previewSense($li, language, sectionType, sectionOrdinal, definitionLevel) {
var cancelled = false;
var restore = null;
function undo() {
cancelled = true;
if (restore) {
restore();
restore = null;
}
if ($li.data("dsbPreview") === undo) {
$li.removeData("dsbPreview");
}
}
var previous = $li.data("dsbPreview");
if (typeof previous === "function") {
previous();
}
$li.data("dsbPreview", undo);
fetchPageContent().then(function (pageContent) {
if (cancelled) {
return;
}
var wikitext;
try {
wikitext = senseBlockWikitext(queuedContent(pageContent), language, sectionType,
sectionOrdinal, definitionLevel);
} catch (e) {
return;
}
renderWikitext(wikitext).then(function (html) {
if (cancelled || $li.data("dsbPreview") !== undo || !$li.closest("body").length) {
return;
}
restore = replaceSenseInPlace($li, parsedSenseItem(html, definitionLevel.length - 1));
});
});
return undo;
}
// The real save: re-runs every queued transformFn, in order, against one freshly-fetched revision and
// posts the result as a single edit with a combined summary.
function saveQueueNow() {
if (!dsbQueue.entries.length) {
return;
}
var box = ensureQueueBox();
box.$save.prop("disabled", true);
box.$cancel.prop("disabled", true);
box.$status.removeClass("dsb-status-ok dsb-status-error").text("Tê qeydkirin…");
var entries = orderedEntries();
// Grouped by place, in the order the places were first queued: several changes to one section read
// as "Kurmancî/Navdêr 2: a, b, c" rather than repeating the section name in front of each of them.
var order = [];
var byPlace = {};
entries.forEach(function (entry) {
if (!Object.prototype.hasOwnProperty.call(byPlace, entry.place)) {
byPlace[entry.place] = [];
order.push(entry.place);
}
byPlace[entry.place].push(entry.action);
});
var combinedSummary = order.map(function (place) {
var actions = byPlace[place].join(", ");
return place ? place + ": " + actions : actions;
}).join("; ") + " (DefSideBoxes)";
editPage(
combinedSummary,
function (pageContent) {
var content = pageContent;
entries.forEach(function (entry) {
content = transformedContent(entry, content);
});
return content;
},
function (data) {
forgetPageContent();
clearAllPreviews();
dsbQueue.entries = [];
box.$list.empty();
box.$status
.removeClass("dsb-status-error")
.addClass("dsb-status-ok")
.text("✓ Hemû guhartin hatin qeydkirin.");
box.$save.prop("disabled", true);
box.$cancel.prop("disabled", false);
// A real edit reloads the page and gets MediaWiki's own "Guhartina te hat weşandin." toast, the
// one a normal save shows. A null edit changed nothing, so the page stays as it is.
if (data && data.newrevid) {
reloadWithPostEditNotice();
}
},
function (message) {
box.$save.prop("disabled", false);
box.$cancel.prop("disabled", false);
box.$status.removeClass("dsb-status-ok").addClass("dsb-status-error").text(message);
}
);
}
// "Kurmancî/Navdêr" -- where a queued change lands, for its summary. The language name comes from the
// shipped list; an unknown code is left as the code itself.
// "Kurmancî/Navdêr", or "Kurmancî/Navdêr 2" where the language has more than one section of that
// type. Without the number a summary on a page with two Navdêr sections named neither of them.
// Numbered only when it disambiguates: on the usual page "Navdêr 1" would just be noise.
function placeLabel(language, sectionType, $h3) {
return langLabel(language) + "/" + sectionLabel(sectionType, $h3);
}
// Bilêvkirin sits above the parts of speech and belongs to the whole language, so naming the section
// it was opened from would say something untrue about what the edit touches.
function subsectionPlaceLabel(subsection, language, sectionType, $h3) {
if (subsection && subsection.free) {
return langLabel(language) + "/" + subsection.header;
}
return subsection && subsection.place === "pron"
? langLabel(language)
: placeLabel(language, sectionType, $h3);
}
function sectionLabel(sectionType, $h3) {
if ($h3 && $h3.length && sections.sectionTypeCountWithinLanguage($h3) > 1) {
return sectionType + " " + sections.sectionOrdinalWithinLanguage($h3);
}
return sectionType;
}
// Capitalised because it opens the summary; Kurdish otherwise writes language names lower-case.
function langLabel(language) {
var names = langs.names();
return util.capitalize(names[language] || language);
}
// The parts of a sense the form actually changed, so the summary lists them instead of saying nothing.
function senseChangeLabels(changes) {
var parts = [];
var changed = changes.changed || {};
if (changes.gloss) {
parts.push("mane");
}
if (changes.newSense) {
parts.push("maneyeke nû");
}
SENSE_GROUPS.forEach(function (group) {
if (changed["word:" + group.key]) {
parts.push(group.label.toLowerCase());
}
});
if (changed.maneid) {
parts.push("id");
}
if (changed.etiket) {
parts.push("etîket");
}
if (changed.kategori) {
parts.push("kategorî");
}
return parts;
}
// Queues transformFn(baseWikitext) -> {content, warnings} for the real save to run later against fresh
// content (see saveQueueNow()); nothing here touches the page yet. Also runs previewFn(), which drops a
// small "not saved yet" preview into the live DOM at the change's own spot on the page. Shared by every
// panel that queues a wikitext change: buildAdvancedPanel, buildSubsectionPanel, buildConvertPosPanel.
// place is what the change is being made to ("Kurmancî/Navdêr 2"), action is what is being done to it.
// Kept apart rather than pre-joined so saveQueueNow() can name a section once and list everything
// queued against it after it, instead of repeating the place before every item.
function queueChange(place, action, transformFn, $save, $status, previewFn, opts) {
opts = opts || {};
var entry = {
key: opts.key || null,
place: place || "",
action: action,
summary: place ? place + ": " + action : action,
transformFn: transformFn,
runLast: !!opts.runLast,
pagePosition: opts.pagePosition || 0,
// Kept, not just run: redrawPreviews() calls it again when another entry leaves the queue.
previewFn: typeof previewFn === "function" ? previewFn : null,
removePreview: null
};
// Queuing the same target twice replaces the pending change rather than stacking a second one --
// two "+Têkildar" entries would both run, and the second would abort on "Ev beş jixwe heye".
var superseded = false;
if (entry.key) {
for (var i = dsbQueue.entries.length - 1; i >= 0; i--) {
if (dsbQueue.entries[i].key === entry.key) {
dropPreview(dsbQueue.entries[i]);
dsbQueue.entries.splice(i, 1);
superseded = true;
}
}
}
dsbQueue.entries.push(entry);
$status.removeClass("dsb-status-error").addClass("dsb-status-ok").text(
"✓ li rêza guhartinan hate zêdekirin — pêl «Guhartinan qeyd bike» bike da ku bi cih bibe."
);
$save.prop("disabled", false);
if (previewFn) {
try {
var removePreview = previewFn();
if (typeof removePreview === "function") {
entry.removePreview = removePreview;
}
} catch (e) { /* a broken preview should never stop the change from being queued */ }
}
// What the superseded entry drew is gone from the queue, so whatever else is drawn from it is stale.
if (superseded) {
dsbQueue.entries.forEach(function (other) {
if (other !== entry) {
dropPreview(other);
}
});
redrawPreviews();
}
renderQueueBox();
}
// Runs text through the ext.gadget.linkify gadget ($.kuLinkify) and calls back with the linkified result.
// Passes the text through unchanged if the gadget can't be loaded, so it never blocks a save.
var linkifyText = util.linkifyText;
// ---- wikitext location ----
//
// One copy for both editing gadgets, in wikt.edit: add-examples writes into the same places.
var langSectionRange = wt.langSectionRange;
var locateSection = wt.locateSection;
var findDefinitionLine = wt.findDefinitionLine;
/**
* The wikitext of one sense's own block: its "#" line plus everything nested under it -- the "#:" word
* lines, the "#*" citations and any sub-senses -- stopping at the next line of the same depth that
* starts a new sense. Parsed on its own that block renders as exactly the <li> the page shows.
*/
function senseBlockWikitext(pageContent, language, sectionType, sectionOrdinal, definitionLevel) {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var defLine = findDefinitionLine(lines, loc.posLineIndex, definitionLevel);
var depth = definitionLevel.length - 1;
var out = [lines[defLine]];
for (var i = defLine + 1; i < lines.length; i++) {
var m = /^(#+)([:*]?)/.exec(lines[i]);
if (!m || m[1].length < depth) {
break;
}
// Same depth and no ":" or "*" after the hashes is the next sense, not part of this one.
if (m[1].length === depth && !m[2]) {
break;
}
out.push(lines[i]);
}
return out.join("\n");
}
// Inserts or extends a "#: {{tplKey|lang|word1|word2}}" sub-line for one sense (start.js's wordLine()
// format). If a line for this tplKey already exists under the definition, missing words are appended to
// it; otherwise a new line lands after the last existing group line, always before any "#*" citation lines.
function insertSenseWords(pageContent, language, sectionType, sectionOrdinal, definitionLevel, tplKey, words, opts) {
opts = opts || {};
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var defLine = findDefinitionLine(lines, loc.posLineIndex, definitionLevel);
var marker = new Array(definitionLevel.length).join("#"); // definitionLevel.length - 1 "#" characters
// Group 1 is the spelling the page uses, group 2 the parameters: an existing {{hevwate}} stays
// {{hevwate}} rather than being silently renamed to {{hevmane}}.
var tplLineRegex = new RegExp("^" + marker + ":\\s*\\{\\{\\s*(" + aliasPattern(tplKey) + ")\\s*\\|([^}]*)\\}\\}\\s*$");
var groupKeysPattern = SENSE_GROUPS.map(function (g) { return aliasPattern(g.key); }).join("|");
var groupLineRegex = new RegExp("^" + marker + ":\\s*\\{\\{\\s*(" + groupKeysPattern + ")\\s*\\|");
var anyBlockLineRegex = new RegExp("^" + marker + "[:*]");
var lastGroupLine = -1;
for (var i = defLine + 1; i < lines.length; i++) {
if (!anyBlockLineRegex.test(lines[i])) {
break;
}
var tplMatch = tplLineRegex.exec(lines[i]);
if (tplMatch) {
var foundKey = tplMatch[1];
if (opts.replace) {
// Editing an existing list: an empty result means the user cleared it, so drop the line.
if (words.length) {
lines[i] = marker + ": {{" + foundKey + "|" + language + "|" + words.join("|") + "}}";
} else {
lines.splice(i, 1);
}
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
var params = tplMatch[2].split("|"); // [lang, word1, word2, ...]
var existingWords = params.slice(1);
words.forEach(function (w) {
if (existingWords.indexOf(w) === -1) {
existingWords.push(w);
}
});
lines[i] = marker + ": {{" + foundKey + "|" + language + "|" + existingWords.join("|") + "}}";
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
if (groupLineRegex.test(lines[i])) {
lastGroupLine = i;
}
}
var insertAt = lastGroupLine >= 0 ? lastGroupLine + 1 : defLine + 1;
lines.splice(insertAt, 0, marker + ": {{" + tplKey + "|" + language + "|" + words.join("|") + "}}");
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
// Canonical left-to-right order of the small templates that can prefix a definition line's gloss text:
// maneid, then kategori (K), then etîket (e).
var LEADING_TPL_ORDER = startData.leadingTplOrder;
// True for a leading call the advanced panel edits in a box of its own -- those are kept out of the
// gloss box so the same template is never editable in two places at once. Redirect spellings count:
// {{f}} and {{etîket}} are {{e}}.
function hasOwnFieldBox(key) {
return LEADING_TPL_ORDER.indexOf(canonicalTemplateKey(key)) !== -1;
}
// Inserts, extends, or (with opts.replace) overwrites one of the small templates that prefix a definition
// line's gloss text -- {{maneid|lang|id}}, {{K|lang|kat1|...}} or {{e|lang|etîket1|...}}. Under
// opts.replace an empty values list removes the call rather than writing an empty one.
function insertLeadingSenseTpl(pageContent, language, sectionType, sectionOrdinal, definitionLevel, tplKey, values, opts) {
opts = opts || {};
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var defLine = findDefinitionLine(lines, loc.posLineIndex, definitionLevel);
var marker = /^#+/.exec(lines[defLine])[0];
var afterMarker = lines[defLine].slice(marker.length).replace(/^\s+/, "");
// Collecting the whole leading-call run means an existing tplKey call is found and merged wherever it
// already sits, not just when it happens to be first.
var split = splitLeadingCalls(afterMarker);
var calls = split.calls;
var rest = split.rest;
var existing = null;
calls.forEach(function (c) {
if (canonicalTemplateKey(c.key) === tplKey) {
existing = c;
}
});
if (existing) {
if (opts.replace) {
// Editing an existing call: an empty result means the user cleared it, so drop the call.
if (values.length) {
existing.params = [language].concat(values);
} else {
calls = calls.filter(function (c) { return c !== existing; });
}
} else {
var existingVals = existing.params.slice(1);
values.forEach(function (v) {
if (existingVals.indexOf(v) === -1) {
existingVals.push(v);
}
});
existing.params = [language].concat(existingVals);
}
} else if (!values.length) {
return pageContent; // nothing to add, and nothing there to clear
} else {
// Not present yet -- insert right before the first existing call that outranks tplKey in
// LEADING_TPL_ORDER (an unrecognized call always outranks everything), or at the end of the run.
var myRank = LEADING_TPL_ORDER.indexOf(tplKey);
var insertAt = calls.length;
for (var i = 0; i < calls.length; i++) {
var otherRank = LEADING_TPL_ORDER.indexOf(canonicalTemplateKey(calls[i].key));
if (otherRank === -1 || otherRank > myRank) {
insertAt = i;
break;
}
}
calls.splice(insertAt, 0, { key: tplKey, params: [language].concat(values) });
}
var rebuilt = calls.map(function (c) {
return "{{" + c.key + "|" + c.params.join("|") + "}}";
}).join(" ");
lines[defLine] = marker + " " + (rebuilt ? rebuilt + " " : "") + rest;
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
// Replaces just the gloss text of a definition line, leaving any leading template calls untouched --
// the save half of "Maneyê biguhêre" (edit sense).
function updateSenseGloss(pageContent, language, sectionType, sectionOrdinal, definitionLevel, newGloss) {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var defLine = findDefinitionLine(lines, loc.posLineIndex, definitionLevel);
var marker = /^#+/.exec(lines[defLine])[0];
var afterMarker = lines[defLine].slice(marker.length).replace(/^\s+/, "");
// The box holds everything except the calls with their own field, so those are put back in front.
// Dropping "{{mane?}}" here is what clears start.js's placeholder once a real gloss is written.
var kept = splitLeadingCalls(afterMarker).calls.filter(function (c) {
return hasOwnFieldBox(c.key);
});
var leadingText = kept.map(function (c) {
return "{{" + c.key + "|" + c.params.join("|") + "}}";
}).join(" ");
lines[defLine] = marker + " " + (leadingText ? leadingText + " " : "") + newGloss;
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
// Deletes one sense: its definition line plus everything indented under it (its "#:" groups, "#*"
// citations, any sub-senses). Refuses to empty a section, since a POS heading with no senses is worse
// than the sense the user wanted gone.
// A new sense goes directly below the one whose pencil asked for it -- after the lines that sense owns
// (its "#:" examples, "#*" citations and "##" sub-senses), so it reads as the next sense rather than
// landing at the foot of the section. It takes the same marker depth as its neighbour, so asking from
// a "##" sub-sense adds a sibling sub-sense.
function insertNewSense(pageContent, language, sectionType, sectionOrdinal, definitionLevel, text) {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var defLine = findDefinitionLine(lines, loc.posLineIndex, definitionLevel);
var marker = /^#+/.exec(lines[defLine])[0];
var end = defLine + 1;
var ownedRegex = new RegExp("^" + marker + "[#:*]");
while (end < lines.length && ownedRegex.test(lines[end])) {
end++;
}
lines.splice(end, 0, marker + " " + text);
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
function removeSense(pageContent, language, sectionType, sectionOrdinal, definitionLevel) {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var defLine = findDefinitionLine(lines, loc.posLineIndex, definitionLevel);
var marker = /^#+/.exec(lines[defLine])[0];
var end = defLine + 1;
var ownedRegex = new RegExp("^" + marker + "[#:*]");
while (end < lines.length && ownedRegex.test(lines[end])) {
end++;
}
// Top-level senses of this section only -- "##" sub-senses go with their parent either way.
if (definitionLevel.length === 2) {
var siblings = 0;
for (var i = loc.posLineIndex + 1; i < lines.length && !/^==/.test(lines[i]); i++) {
if (/^#[^*#:]/.test(lines[i])) {
siblings++;
}
}
if (siblings < 2) {
throw new SubmitAbort("Ev maneya dawî ya vê beşê ye. Ji bo rakirinê divê tu tevahiya beşê rakî.");
}
}
lines.splice(defLine, end - defLine);
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
// Fetches the page's current wikitext and pulls out just this sense's gloss text to prefill the "Maneyê
// biguhêre" form. A plain read, not part of the hardened save path -- the actual save still re-fetches
// fresh content, so only the prefilled text itself could be a little stale.
// Current contents of one existing subsection, for prefilling the edit form.
// This page's current wikitext.
// One fetch for the whole queue: every preview reads the same revision, and asking per preview meant
// downloading the page again for each queued change. Dropped after a save, when the page has moved on.
var pageContentPromise = null;
function forgetPageContent() {
pageContentPromise = null;
}
function fetchPageContent() {
if (pageContentPromise) {
return pageContentPromise;
}
pageContentPromise = rawPageContent();
// A failed read must not be remembered as the answer.
pageContentPromise.fail(forgetPageContent);
return pageContentPromise;
}
function rawPageContent() {
return api.get({
action: "query",
prop: "revisions",
rvprop: "content",
titles: mw.config.get("wgPageName"),
formatversion: 2
}).then(function (data) {
var page = data.query && data.query.pages && data.query.pages[0];
var content = page && !page.missing && page.revisions && page.revisions[0] && page.revisions[0].content;
if (!content) {
throw new SubmitAbort("Nikare muhtewaya rûpelê bixwîne.");
}
return content;
});
}
function fetchPageSubsection(language, sectionType, sectionOrdinal, subsection) {
return fetchPageContent().then(function (content) {
return readPageSubsection(content, language, sectionType, sectionOrdinal, subsection);
});
}
function fetchSenseFields(language, sectionType, sectionOrdinal, definitionLevel) {
return fetchPageContent().then(function (content) {
return readSenseFields(content, language, sectionType, sectionOrdinal, definitionLevel);
});
}
// Everything the advanced panel can edit about one sense, read off its definition line and the "#:" lines
// under it: the raw gloss (templates included), the leading maneid/K/e values, and each word-list group.
function readSenseFields(pageContent, language, sectionType, sectionOrdinal, definitionLevel) {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var defLine = findDefinitionLine(lines, loc.posLineIndex, definitionLevel);
var defMarker = /^#+/.exec(lines[defLine])[0];
var afterMarker = lines[defLine].slice(defMarker.length).replace(/^\s+/, "");
var leading = {};
splitLeadingCalls(afterMarker).calls.forEach(function (c) {
leading[canonicalTemplateKey(c.key)] = c.params.slice(1);
});
// Only start.js's "{{mane?|lang}}" placeholder is hidden; every other template stays visible so it can
// be edited by hand in the gloss box.
// The gloss box shows the line minus the calls that have their own box ({{maneid}}, {{K}}, {{e}} and
// their redirects) and minus start.js's "{{mane?}}" placeholder. Everything else -- a {{g}} link, a
// {{guharto}} -- stays visible and editable there.
var split = splitLeadingCalls(afterMarker);
var glossCalls = split.calls.filter(function (c) {
return !hasOwnFieldBox(c.key) && canonicalTemplateKey(c.key) !== "mane?";
});
var glossLead = glossCalls.map(function (c) {
return "{{" + c.key + "|" + c.params.join("|") + "}}";
}).join(" ");
var gloss = $.trim((glossLead ? glossLead + " " : "") + split.rest);
var marker = new Array(definitionLevel.length).join("#");
var groupKeysPattern = SENSE_GROUPS.map(function (g) { return aliasPattern(g.key); }).join("|");
var groupLineRegex = new RegExp("^" + marker + ":\\s*\\{\\{\\s*(" + groupKeysPattern + ")\\s*\\|([^}]*)\\}\\}\\s*$");
var anyBlockLineRegex = new RegExp("^" + marker + "[:*]");
var words = {};
for (var i = defLine + 1; i < lines.length; i++) {
if (!anyBlockLineRegex.test(lines[i])) {
break;
}
var m = groupLineRegex.exec(lines[i]);
if (m) {
words[canonicalTemplateKey(m[1])] = m[2].split("|").slice(1);
}
}
return {
gloss: gloss,
maneid: leading.maneid ? leading.maneid[0] : "",
etiket: leading.e || [],
kategori: leading.K || [],
words: words
};
}
// Finds a POS block's body range (up to, but not including, the next heading of level <= 3), and the
// header text + line index of any "====" subsections it already contains.
// depth is the part-of-speech heading's own ("===" on most entries, "====" where the senses are
// grouped under numbered etymologies). Its block runs to the next heading at that depth or shallower,
// and its subsections are the headings one deeper.
function posBlockBounds(lines, posLineIndex, depth) {
depth = depth || 3;
var blockEnd = wt.headingAtOrAbove(depth);
var subsectionHeading = wt.headingAtDepth(depth + 1);
var end = lines.length;
var existingHeaders = [];
for (var i = posLineIndex + 1; i < lines.length; i++) {
if (blockEnd.test(lines[i])) {
end = i;
break;
}
var sub = subsectionHeading.exec(lines[i]);
if (sub) {
existingHeaders.push({ header: $.trim(sub[1]), lineIndex: i });
}
}
return { end: end, existingHeaders: existingHeaders, depth: depth };
}
// Looks up a header's rank in SUBSECTION_ORDER. -1 for a header that isn't one of ours; such a header is
// never used as an insertion anchor.
function subsectionRankForHeader(header) {
return SUBSECTION_ORDER.indexOf(header);
}
// Body line range of one "====" subsection inside a POS block: everything after its heading up to the next
// "====" heading, or the end of the block. Null if the block has no such subsection.
function subsectionBodyRange(bounds, header) {
var headers = bounds.existingHeaders;
for (var i = 0; i < headers.length; i++) {
if (headers[i].header === header) {
return {
start: headers[i].lineIndex + 1,
end: i + 1 < headers.length ? headers[i + 1].lineIndex : bounds.end
};
}
}
return null;
}
// ---- template-parameter editing -------------------------------------------------------------------
// A gloss that is nothing but one template call gets a labelled box per parameter instead of a raw
// wikitext line, the way VisualEditor's template dialog works.
// The single template call a gloss is made of, or null when the line is anything less tidy. {{g|...}} is a
// link inside the gloss rather than the gloss itself, so it neither disqualifies the line nor gets a form.
function glossTemplateTarget(gloss) {
var parsed = parseTemplateCalls(gloss || "");
if (parsed.otherContent) {
return null;
}
var editable = parsed.calls.filter(function (c) {
return canonicalTemplateKey(c.name) !== "g";
});
return editable.length === 1 ? editable[0] : null;
}
// ---- the three places that carry no "====" heading of their own ----------------------------------
// Bilêvkirin is a "===" section above the POS blocks, images sit at the top of a POS block, and the
// h-bnr hatnote goes above the first language heading. Each is located here and then read, written or
// inserted through the same three lines of splice, so nothing below needs to know about them.
var IMAGE_LINE = /^\[\[\s*(?:Wêne|File|Image|Dosya)\s*:/i;
/**
* {lines, langSectionIndex, exists, start, end, insertAt} for a subsection carrying a "place", or null
* for an ordinary one. start..end is the body as it stands; insertAt is where a missing one goes.
*/
function locateLoosePlace(pageContent, language, sectionType, sectionOrdinal, subsection) {
var place = subsection.place;
if (!place) {
return null;
}
var i, end;
if (place === "page-top") {
// Above every language section, so the whole page is the search space. The call is found under
// any of its names, and its own name is carried back so a save never renames it.
var pageLines = pageContent.split("\n");
var call = wt.findHbnrCall(pageContent);
if (call) {
return { lines: pageLines, langSectionIndex: 0, exists: true, call: call,
start: call.lineIndex, end: call.lineIndex + 1, insertAt: call.lineIndex };
}
return { lines: pageLines, langSectionIndex: 0, exists: false, start: 0, end: 0, insertAt: 0 };
}
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
if (place === "pron") {
// One per language, not one per part of speech, so it is looked for above the POS heading.
for (i = 0; i < loc.posLineIndex; i++) {
var h3 = /^===\s*([^=]+?)\s*===/.exec(lines[i]);
if (h3 && $.trim(h3[1]) === subsection.header) {
end = i + 1;
while (end < lines.length && !/^==/.test(lines[end])) {
end++;
}
return { lines: lines, langSectionIndex: loc.langSectionIndex, exists: true,
start: i + 1, end: end, insertAt: i };
}
}
// Straight after the language heading, which puts it before every POS block.
return { lines: lines, langSectionIndex: loc.langSectionIndex, exists: false,
start: 1, end: 1, insertAt: 1 };
}
if (place === "pos-top") {
// Between the conjugation line and the headword template -- where the wizard writes them.
var at = loc.posLineIndex + 1;
while (at < lines.length && /^\{\{\s*ku-tewîn/.test($.trim(lines[at]))) {
at++;
}
var from = at;
while (at < lines.length && IMAGE_LINE.test($.trim(lines[at]))) {
at++;
}
return { lines: lines, langSectionIndex: loc.langSectionIndex, exists: at > from,
start: from, end: at, insertAt: from };
}
return null;
}
// The wikitext a "place" section's content becomes. A word list there is one template call, not a
// {{kol3}} block, so the words are joined into it -- and any ⊕ modifier a row picked up is dropped,
// since a page name is all the call can carry.
function loosePlaceBody(subsection, content, language) {
if (subsection.kind === "images") {
return $.grep($.map(content || [], function (image) {
return wt.buildImageLine({
file: image.file,
caption: wt.captionForLang(image.caption, language),
alt: image.alt,
size: image.size,
extra: image.extra
});
}), Boolean).join("\n");
}
return content;
}
// The words a "place" word list holds, flattened out of its single group. Any ⊕ modifier a row picked
// up is dropped: a page name is all the call can carry.
function loosePlaceWords(content) {
var words = [];
$.each(content || [], function (_, group) {
$.each(group.words || [], function (_, raw) {
var plain = $.trim(util.parseWordValue(raw).word);
if (plain && $.inArray(plain, words) === -1) {
words.push(plain);
}
});
});
return words;
}
// Writes one of those places, inserting it when it is not there yet and replacing it when it is. Only
// Bilêvkirin carries a heading; the other two are bare lines.
function writeLoosePlace(pageContent, language, sectionType, sectionOrdinal, subsection, content) {
var at = locateLoosePlace(pageContent, language, sectionType, sectionOrdinal, subsection);
if (!at) {
throw new SubmitAbort("Cih nehate dîtin: " + subsection.header);
}
// A call naming a subpage -- "{{h-bnr/-ane}}" -- holds a list shared by every page transcluding
// it. Writing here would change this page alone and desync the rest, so the edit is refused and
// the template that actually owns the list is named instead.
if (at.call && at.call.subpage) {
throw new SubmitAbort("Ev rûpel «{{" + at.call.name + "}}» bi kar tîne — lîste li " +
"[[Şablon:" + at.call.subpage + "]] ye, lê wê zêde bike, ne li vê rûpelê.");
}
// A word list here is one template call, so it is built where the existing call's own name is
// known -- editing "{{bnr-h|...}}" must not turn it into "{{h-bnr|...}}".
var body = subsection.kind === "wordlist"
? wt.buildHbnrCall(loosePlaceWords(content), at.call && at.call.name)
: $.trim(content || "");
// A "===" section is separated from what is above and below it by a blank line, the way the rest
// of an entry is written. The other two places are bare lines inside a block and take none.
var heading = subsection.place === "pron";
function blank(index) {
return $.trim(at.lines[index] || "") === "";
}
if (at.exists) {
if (body) {
var replacement = [body];
// end is the next "==" heading; the blank line before it is inside the range being replaced.
if (heading && /^==/.test(at.lines[at.end] || "")) {
replacement.push("");
}
at.lines.splice.apply(at.lines, [at.start, at.end - at.start].concat(replacement));
} else {
// Emptying the box removes the section rather than leaving an empty heading behind.
at.lines.splice(at.start, at.end - at.start);
if (heading) {
at.lines.splice(at.insertAt, 1);
// The blank line that separated it goes with it, rather than doubling up.
if (blank(at.insertAt) && blank(at.insertAt - 1)) {
at.lines.splice(at.insertAt, 1);
}
}
}
} else {
if (!body) {
throw new SubmitAbort("Tiştek nehate nivîsandin.");
}
var block = heading ? ["=== " + subsection.header + " ===", body] : [body];
if (heading) {
if (!blank(at.insertAt - 1)) {
block.unshift("");
}
if (!blank(at.insertAt)) {
block.push("");
}
}
at.lines.splice.apply(at.lines, [at.insertAt, 0].concat(block));
}
return pageContent.slice(0, at.langSectionIndex) + at.lines.join("\n");
}
// What a section body holds: its {{kol3}} groups, the plain bullet list that stands in for them, or
// the raw wikitext when it is neither. A free section is read this way before its kind is known.
function readSubsectionBody(body, subsection) {
if (subsection.kind === "wordlist" || subsection.free) {
var calls = parseWordListCalls(body);
if (calls.length) {
return {
kind: "wordlist",
groups: calls.map(function (call) {
return { sernav: namedParam(call.named, "sernav"), words: call.words };
})
};
}
var bullets = parseBulletWordList(body);
if (bullets) {
// Editable as a word list; saving rewrites the bullets as {{kol3}}.
return { kind: "wordlist", groups: [{ sernav: "", words: bullets }], fromBullets: true };
}
}
return { kind: "text", text: $.trim(body) };
}
// The wikitext a section body becomes after an edit. A wordlist keeps its own template name, language
// and named params ("cure=Jê", ...) and only has its positional words replaced; a text one is
// replaced whole.
function rewriteSubsectionBody(body, language, subsection, content) {
if (subsection.kind !== "wordlist") {
// Replacing the body wholesale drops that separator, so put it back.
return content + (/\n\s*$/.test(body) ? "\n" : "");
}
var calls = parseWordListCalls(body);
if (calls.length) {
// Each call is rewritten where it stands, so text between and around them survives untouched.
// Last to first, so the offsets of the earlier ones stay valid as we go.
var replacement = body;
for (var gi = calls.length - 1; gi >= 0; gi--) {
var call = calls[gi];
var group = content[gi];
// A group emptied in the form drops its call entirely rather than leaving "{{kol3|ku}}".
var rebuilt = group && group.words.length
? buildWordListCall(call.name, call.lang, withNamedParam(call.named, "sernav", group.sernav), group.words)
: "";
replacement = replacement.slice(0, call.start) + rebuilt + replacement.slice(call.end);
}
// Groups the form added beyond the ones already there go after the last of them.
var added = content.slice(calls.length).filter(function (group) {
return group.words.length;
}).map(function (group) {
return buildWordListCall(WORDLIST_TEMPLATE, language,
withNamedParam(["cure=" + subsection.header], "sernav", group.sernav), group.words);
});
if (added.length) {
replacement = replacement.replace(/\s*$/, "") + "\n\n" + added.join("\n\n") + "\n";
}
return replacement.replace(/\n{3,}/g, "\n\n"); // gaps left by any dropped call
}
if (parseBulletWordList(body)) {
// A plain bullet list is replaced wholesale by a real {{kol3}} on its first edit.
return content.map(function (group) {
return buildWordListCall(WORDLIST_TEMPLATE, language,
withNamedParam(["cure=" + subsection.header], "sernav", group.sernav), group.words);
}).join("\n\n") + (/\n\s*$/.test(body) ? "\n" : "");
}
throw new SubmitAbort("Nikare lîsteya vê beşê bixwîne: " + subsection.header);
}
// ---- sections that are not subsections of a part of speech ----
// A "=== Etîmolojî ===" that the parts of speech hang under, a "=== Çavkanî ===" at the foot of a
// language, a heading nobody planned for. Located by its own text, and edited as the wikitext under
// it -- or as a {{kol3}} list when that is what it holds.
function freeSectionBody(pageContent, language, subsection) {
var loc = wt.locateHeadingByText(pageContent, language, subsection.header, subsection.free.ordinal);
return { loc: loc, body: loc.lines.slice(loc.bodyStart, loc.bodyEnd).join("\n") };
}
function readFreeSection(pageContent, language, subsection) {
var at = freeSectionBody(pageContent, language, subsection);
return readSubsectionBody(at.body, subsection);
}
function updateFreeSection(pageContent, language, subsection, content) {
var at = freeSectionBody(pageContent, language, subsection);
at.loc.lines.splice(at.loc.bodyStart, at.loc.bodyEnd - at.loc.bodyStart,
rewriteSubsectionBody(at.body, language, subsection, content));
return pageContent.slice(0, at.loc.langSectionIndex) + at.loc.lines.join("\n");
}
// ---- removing and renaming a heading ----
// A subsection's heading line and the end of what it owns, as {loc, heading, end, depth, bounds}. A
// "====" subsection runs to the next one, taking deeper headings with it. A free section owns only its
// own text: what sits under it belongs to the parts of speech (see removeSubsection).
function headingSpan(pageContent, language, sectionType, sectionOrdinal, subsection) {
if (subsection.free) {
var free = wt.locateHeadingByText(pageContent, language, subsection.header, subsection.free.ordinal);
return { loc: free, heading: free.headingLineIndex, end: free.bodyEnd, depth: free.depth };
}
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var bounds = posBlockBounds(loc.lines, loc.posLineIndex, loc.depth);
var range = subsectionBodyRange(bounds, subsection.header);
if (!range) {
throw new SubmitAbort(util.msg.sectionMissing + subsection.header);
}
return { loc: loc, heading: range.start - 1, end: range.end, depth: bounds.depth + 1, bounds: bounds };
}
// A free section with deeper headings under it is refused: removing it would leave them under the
// section before, and taking them along would remove whole parts of speech.
function hasDeeperHeadingAfter(lines, index, depth) {
var m = /^(=+)[^=]/.exec(lines[index] || "");
return !!m && m[1].length > depth;
}
function removeSubsection(pageContent, language, sectionType, sectionOrdinal, subsection) {
var span = headingSpan(pageContent, language, sectionType, sectionOrdinal, subsection);
var lines = span.loc.lines;
if (subsection.free && hasDeeperHeadingAfter(lines, span.end, span.depth)) {
throw new SubmitAbort("Di bin «" + subsection.header + "» de beşên din hene; pêşî wan jê bibe.");
}
lines.splice(span.heading, span.end - span.heading);
// The blank line that separated it goes with it, rather than doubling up.
if (span.heading > 0 && !$.trim(lines[span.heading - 1]) && !$.trim(lines[span.heading] || "x")) {
lines.splice(span.heading, 1);
}
return pageContent.slice(0, span.loc.langSectionIndex) + lines.join("\n");
}
function renameSubsection(pageContent, language, sectionType, sectionOrdinal, subsection, newHeader) {
var span = headingSpan(pageContent, language, sectionType, sectionOrdinal, subsection);
var lines = span.loc.lines;
if (span.bounds && span.bounds.existingHeaders.some(function (h) {
return h.header === newHeader;
})) {
throw new SubmitAbort("Ev beş jixwe heye: " + newHeader);
}
var marks = wt.headingMarks(span.depth);
lines[span.heading] = marks + " " + newHeader + " " + marks;
// A {{kol3}} names its section in cure=, which follows the heading.
var cure = new RegExp("(\\|\\s*cure\\s*=\\s*)" + escapeRegExp(subsection.header) + "(\\s*[|}])", "g");
for (var i = span.heading + 1; i < span.end; i++) {
lines[i] = lines[i].replace(cure, "$1" + newHeader + "$2");
}
return pageContent.slice(0, span.loc.langSectionIndex) + lines.join("\n");
}
// Current contents of an existing "====" subsection. A wordlist one whose template can't be parsed comes
// back as {kind: "text"} so the caller can still edit it as raw wikitext instead of refusing outright.
function readPageSubsection(pageContent, language, sectionType, sectionOrdinal, subsection) {
if (subsection.free) {
return readFreeSection(pageContent, language, subsection);
}
var loose = locateLoosePlace(pageContent, language, sectionType, sectionOrdinal, subsection);
if (loose) {
if (!loose.exists) {
return null;
}
if (loose.call) {
return { kind: "wordlist", groups: [{ sernav: "", words: loose.call.words }] };
}
var looseBody = $.trim(loose.lines.slice(loose.start, loose.end).join("\n"));
if (subsection.kind === "images") {
return {
kind: "images",
images: $.grep($.map(looseBody.split("\n"), wt.parseImageLine), Boolean)
};
}
if (subsection.kind !== "wordlist") {
return { kind: "text", text: looseBody };
}
// One template call rather than a {{kol3}} block: its positional parameters are the words.
var looseCall = parseTemplateCalls(looseBody).calls[0];
return { kind: "wordlist", groups: [{ sernav: "", words: (looseCall && looseCall.params) || [] }] };
}
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var bounds = posBlockBounds(loc.lines, loc.posLineIndex, loc.depth);
var range = subsectionBodyRange(bounds, subsection.header);
if (!range) {
return null;
}
return readSubsectionBody(loc.lines.slice(range.start, range.end).join("\n"), subsection);
}
// Rewrites an existing "====" subsection in place. A wordlist one keeps its own template name, language and
// named params ("cure=Jê", ...) and only has its positional words replaced; a text one is replaced whole.
function updatePageSubsection(pageContent, language, sectionType, sectionOrdinal, subsection, content) {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var bounds = posBlockBounds(lines, loc.posLineIndex, loc.depth);
var range = subsectionBodyRange(bounds, subsection.header);
if (!range) {
throw new SubmitAbort(util.msg.sectionMissing + subsection.header);
}
var body = lines.slice(range.start, range.end).join("\n");
lines.splice(range.start, range.end - range.start,
rewriteSubsectionBody(body, language, subsection, content));
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
// Inserts a new "==== Header ====" subsection into its SUBSECTION_ORDER position within a POS block, right
// before the first ranked subsection that outranks it, or at the end if none does. Either a
// {{kol3|...}} word list or a plain paragraph, matching start.js's own subsection formats.
function insertPageSubsection(pageContent, language, sectionType, sectionOrdinal, subsection, content) {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var bounds = posBlockBounds(lines, loc.posLineIndex, loc.depth);
var alreadyThere = false;
bounds.existingHeaders.forEach(function (h) {
if (h.header === subsection.header) {
alreadyThere = true;
}
});
if (alreadyThere) {
throw new SubmitAbort("Ev beş jixwe heye: " + subsection.header);
}
var body = subsection.kind === "wordlist"
? content.map(function (group) {
return buildWordListCall(WORDLIST_TEMPLATE, language,
withNamedParam(["cure=" + subsection.header], "sernav", group.sernav), group.words);
}).join("\n\n")
: content;
var myRank = subsectionRankForHeader(subsection.header);
var headers = bounds.existingHeaders;
var insertAt = bounds.end;
for (var i = 0; i < headers.length; i++) {
var otherRank = subsectionRankForHeader(headers[i].header);
if (otherRank !== -1 && otherRank > myRank) {
insertAt = headers[i].lineIndex;
break;
}
}
// Exactly one blank line on each side: the line before the insertion point is normally already blank
// (it separated the previous subsection from the next heading), so only add one where there isn't one.
var block = [];
if (insertAt > 0 && $.trim(lines[insertAt - 1]) !== "") {
block.push("");
}
var marks = wt.headingMarks(bounds.depth + 1);
block.push(marks + " " + subsection.header + " " + marks, body);
if (insertAt < lines.length) {
block.push("");
}
lines.splice.apply(lines, [insertAt, 0].concat(block));
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
// ---- converting an unclassified "=== Mane ===" placeholder heading into a real POS heading ----
// Some pages carry a heading literally named "Mane" (optionally "Mane 2" etc.) instead of a real
// part-of-speech name, tagged right below with {{cure?|<lang>}} (ku.wiktionary's "part of speech not yet
// determined" maintenance flag). It is a section key of its own (sections.MANE_SECTION), so the shared
// locator finds it like any other heading; only the rewriting below is particular to it.
// Renames the "=== Mane ===" placeholder to the chosen POS heading, drops the maintenance tag line right
// after it if present (MANE_TAG_REGEX -- cure? or any of its known aliases), and inserts the matching
// "{{<pos>|<lang>}}" call in its place (skipped when the section has no template of its own).
// Other content under the heading is left untouched.
/**
* Where a section's own headword template sits, as {index, text}, or {index: -1} when it has none.
*
* Not always the line under the heading: a "{{ku-tewîn-lk}}" line and any number of "[[Wêne:...]]"
* images can come first. Everything down to the first definition line is searched, and index doubles
* as the slot a new call belongs in when there is no old one.
*/
function findPosTemplateLine(lines, headingLine, sectionType) {
var oldSpec = posTemplateFor(sectionType);
var oldTemplate = oldSpec ? oldSpec.name : (sectionType === "Biwêj" ? "biwêj" : null);
var oldTemplateRegex = oldTemplate &&
new RegExp("^\\{\\{\\s*" + escapeRegExp(oldTemplate) + "\\s*[|}]");
var at = headingLine + 1;
while (at < lines.length && !/^#/.test(lines[at]) && !/^==/.test(lines[at])) {
if (MANE_TAG_REGEX.test(lines[at]) ||
(oldTemplateRegex && oldTemplateRegex.test(lines[at]))) {
return { index: at, text: lines[at], end: at };
}
at++;
}
return { index: -1, text: "", end: at };
}
// Named parameters merged in order, each written once: a later value replaces an earlier one of the
// same name. Positional values are kept as they come.
function mergeCallParams() {
var out = [];
var seen = {};
$.each(Array.prototype.slice.call(arguments), function (_, list) {
$.each(list || [], function (__, param) {
var m = /^\s*([^=|]+?)\s*=([\s\S]*)$/.exec(param);
if (!m) {
out.push(param);
return;
}
// "z" and "c" are one name to Modul:rêziman, so the form's "c=m" meets the page's "z=".
var key = wt.canonicalParamName(m[1]);
if (!Object.prototype.hasOwnProperty.call(seen, key)) {
seen[key] = out.length;
out.push(param);
return;
}
// The page's own spelling stays when it carried a value; an empty one gives way entirely.
var earlier = /^\s*([^=|]+?)\s*=([\s\S]*)$/.exec(out[seen[key]]);
// "?" asks for a value, so it never replaces one the page already has.
if ($.trim(earlier[2]) && $.trim(m[2]) === "?") {
return;
}
out[seen[key]] =$.trim(earlier[2]) ? earlier[1] + "=" + m[2] : param;
});
});
return out;
}
// What the call on the page already says, minus its language. Carried over only when the new section
// uses that same template -- "z=m" survives Navdêr becoming Formeke navdêrê, while a verb's "c=h-gh"
// has no meaning on a noun and is left behind.
function keptCallParams(line, templateName) {
var call = parseTemplateCalls($.trim(line || "")).calls[0];
if (!call || canonicalTemplateKey(call.name) !== canonicalTemplateKey(templateName)) {
return [];
}
return call.params.slice(1).filter(function (param) {
return /=/.test(param);
});
}
// The headword call a converted section gets, or "" when the new section has none. What the old call
// (found, from findPosTemplateLine) said keeps its place, then the section's own defaults, then what the
// form was filled in with -- each name written once, the last value winning, so a converted call reads
// as small a change from the old one as it can.
function posCallLine(found, language, newSectionType, posParams) {
var spec = posTemplateFor(newSectionType);
if (!spec) {
return "";
}
var kept = found.index !== -1 ? keptCallParams(found.text, spec.name) : [];
// A lemma is not an inflected form, so the old section's form=erê stays behind.
if (spec.params.indexOf("form=erê") === -1) {
kept = kept.filter(function (param) {
return !/^\s*form\s*=\s*erê\s*$/.test(param);
});
}
var params = mergeCallParams(kept, spec.params, posParams ? posParams.split("|") : []);
return "{{" + spec.name + "|" + language + (params.length ? "|" + params.join("|") : "") + "}}";
}
function convertSectionToPos(pageContent, language, sectionType, sectionOrdinal, newSectionType, posParams) {
if (!sectionNames[newSectionType]) {
throw new SubmitAbort("Navê cureyê rêzimanî yê hilbijartî nediyar e.");
}
// locateSection() already handles the "Mane" placeholder as a section type of its own.
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var headingLine = loc.posLineIndex;
var depth = loc.depth;
// A section straight under a numbered group heading takes that heading's place: the group heading
// held nothing but its gloss.
var group = depth > 3 ? wt.numberedGroupAbove(lines, headingLine, depth) : -1;
if (group !== -1) {
lines.splice(group, headingLine - group);
headingLine = group;
depth--;
}
// At the heading's own depth. Written without a number: the renumbering pass below decides every
// number in this language section at once, so one carried over from the old name would just be
// overwritten.
var marks = wt.headingMarks(depth);
lines[headingLine] = marks + " " + newSectionType + " " + marks;
// The line under the heading is either the old section's own template call ("{{biwêj|ku}}") or, for a
// Mane placeholder, the "needs a POS" maintenance tag. Both are replaced by the new POS's call.
// The new call takes the old one's own place -- or, when there was none, the slot just above the
// definitions, which is where the wizard writes it.
var found = findPosTemplateLine(lines, headingLine, sectionType);
var insertAt = found.index !== -1 ? found.index : found.end;
if (found.index !== -1) {
lines.splice(found.index, 1);
}
var call = posCallLine(found, language, newSectionType, posParams);
if (call) {
lines.splice(insertAt, 0, call);
}
// Renaming a section can make it repeat one the page already had, and can equally leave a name that
// is no longer repeated. Both are settled by renumbering the whole language section: a "Rengdêr"
// beside a new "Rengdêr" becomes "Rengdêr 1" and "Rengdêr 2", and a number left behind by a name
// that is now unique is dropped.
var body = lines.join("\n");
var nextLang = body.indexOf("\n== ", 1);
var end = nextLang === -1 ? body.length : nextLang;
body = wt.numberRepeatedPosHeadings(body.slice(0, end)) + body.slice(end);
return pageContent.slice(0, loc.langSectionIndex) + body;
}
// ---- "guhartina pêşketî" (advanced editing): combine several independent per-sense edits into ONE save ----
// Threads pageContent through the same pure functions every single-field form uses, applying only the
// pieces the caller actually filled in. A piece that conflicts with the page (maneid already set, a
// subsection that's already there) is skipped with a warning rather than failing the whole save. Throws
// SubmitAbort if nothing was filled in, so this can never turn into a no-op save.
function applyAdvancedChanges(pageContent, language, sectionType, sectionOrdinal, definitionLevel, changes) {
var content = pageContent;
var warnings = [];
var appliedAnything = false;
if (changes.gloss) {
content = updateSenseGloss(content, language, sectionType, sectionOrdinal, definitionLevel, changes.gloss);
appliedAnything = true;
}
if (changes.newSense) {
content = insertNewSense(content, language, sectionType, sectionOrdinal, definitionLevel, changes.newSense);
appliedAnything = true;
}
// Every box below is prefilled with what the sense already has, so a save replaces that value rather
// than merging into it -- and clearing a box removes the template. changes.changed marks the boxes
// whose contents actually differ from the page, so an opened-but-untouched box writes nothing.
var replace = { replace: true };
if (changes.changed.maneid) {
content = insertLeadingSenseTpl(content, language, sectionType, sectionOrdinal, definitionLevel, "maneid", changes.maneid ? [changes.maneid] : [], replace);
appliedAnything = true;
}
if (changes.changed.etiket) {
content = insertLeadingSenseTpl(content, language, sectionType, sectionOrdinal, definitionLevel, "e", changes.etiket, replace);
appliedAnything = true;
}
if (changes.changed.kategori) {
content = insertLeadingSenseTpl(content, language, sectionType, sectionOrdinal, definitionLevel, "K", changes.kategori, replace);
appliedAnything = true;
}
SENSE_GROUPS.forEach(function (group) {
if (changes.changed["word:" + group.key]) {
content = insertSenseWords(content, language, sectionType, sectionOrdinal, definitionLevel, group.key, changes.words[group.key], replace);
appliedAnything = true;
}
});
if (!appliedAnything) {
throw new SubmitAbort("Tiştek nehat guhartin.");
}
return { content: content, warnings: warnings };
}
// ---- per-definition UI: a small always-visible "+" toggle (no hover, so it works on mobile/touch too) ----
// A growing list of word rows. The row itself is util.js's, shared with the wizard; this owns the list:
// typing in the last row appends a fresh empty one, so there is always somewhere to type.
var WORDLIST_COLUMN_THRESHOLD = 6;
function buildWordListField(placeholder, opts) {
opts = opts || {};
var $list = $("<ol class='dsb-wordlist'></ol>");
// One column is a long scroll once a list gets past a handful of words. The class only asks for
// two; the stylesheet still gives one on a narrow screen. Two columns inside the panel's usual
// 520px would leave each word about 250px, so the panel drops its width cap for as long as any
// list in it is in two columns -- checked across the whole panel, since a "Herwiha binêre" holds
// several groups and one of them going wide settles it for all.
function updateColumns() {
$list.toggleClass("dsb-wordlist-2col",
$list.children("li").length > WORDLIST_COLUMN_THRESHOLD);
var $panel = $list.closest(".dsb-subsection-panel");
$panel.toggleClass("dsb-panel-wide", !!$panel.find(".dsb-wordlist-2col").length);
}
function makeRow() {
var row = util.makeModifierRow({
prefix: "dsb-",
placeholder: placeholder,
modTitle: util.msg.addModifier,
removeTitle: util.msg.removeWord,
removeModTitle: util.msg.removeModifier,
modifiers: opts.modifiers,
// "bixe qutiyan" puts each comma-separated word in a row of its own, right after the row it
// was typed in, and the caret follows to the last of them. One word per row is also one
// word per template parameter, which both {{hevmane}} and {{kol3}} read the same way --
// a comma left inside a parameter would not be.
onSplit: function (rest) {
var $after = row.$li;
var last = null;
rest.forEach(function (raw) {
var extra = makeRow();
$after.after(extra.$li);
$after = extra.$li;
if (raw) extra.setRaw(raw);
last = extra;
});
// Always an empty row at the end to type the next word into.
var $tail = $list.children("li").last();
if ($.trim($tail.data("row").getRaw())) {
last = makeRow();
$list.append(last.$li);
}
updateColumns();
if (last) last.focus();
},
// raw === null means the row removed itself, so the count has changed either way.
onChange: function (raw) {
if (raw !== null) {
var isLast = row.$li.is($list.children("li").last());
if (isLast && raw) $list.append(makeRow().$li);
}
updateColumns();
}
});
row.$li.data("row", row);
return row;
}
$list.append(makeRow().$li);
return {
$element: $list,
values: function () {
var vals = [];
$list.children("li").each(function () {
var raw = $(this).data("row").getRaw();
if (raw) vals.push(raw);
});
return vals;
},
// Replaces the whole list, keeping one empty trailing row to type into.
setValues: function (raws) {
$list.empty();
raws.forEach(function (raw) {
var row = makeRow();
$list.append(row.$li);
row.setRaw(raw);
});
$list.append(makeRow().$li);
updateColumns();
},
focus: function () {
$list.children("li").first().data("row").focus();
}
};
}
// Plain removable-tag field, used for Etîket and Kategorî (simple value lists with no per-word modifiers).
// Enter or "," commits the current text as a tag; Backspace on an empty input removes the last tag;
// values() also commits whatever is still typed but not yet confirmed.
function buildTagField(placeholder) {
var $input = $("<input type='text' class='dsb-input dsb-tag-input'>").attr("placeholder", placeholder);
var $element = $("<span class='dsb-tagfield'></span>").append($input);
function currentValues() {
var vals = [];
$element.children(".dsb-tag").each(function () {
vals.push($(this).data("value"));
});
return vals;
}
function addTag(val) {
val = $.trim(val || "");
if (!val || currentValues().indexOf(val) !== -1) {
return;
}
var $tag = $("<span class='dsb-tag'></span>").text(val).data("value", val);
var $remove = $("<button type='button' class='dsb-tag-remove' title='Jê bibe'>×</button>");
$remove.on("click", function () {
$tag.remove();
$input.trigger("focus");
});
$tag.append($remove);
$tag.insertBefore($input);
}
$input.on("keydown", function (e) {
if (e.which === 13 || e.which === 188) { // Enter or comma
e.preventDefault();
addTag($input.val());
$input.val("");
} else if (e.which === 8 && !$input.val()) {
$element.children(".dsb-tag").last().remove();
}
});
$input.on("blur", function () {
addTag($input.val());
$input.val("");
});
return {
$element: $element,
// Replaces every tag, for prefilling from what the sense already has.
setValues: function (vals) {
$element.children(".dsb-tag").remove();
vals.forEach(function (v) {
addTag(v);
});
$input.val("");
},
values: function () {
addTag($input.val());
$input.val("");
return currentValues();
},
focus: function () {
$input.trigger("focus");
}
};
}
// The wizard's own etymology templates, straight out of data-main.json's "etymTpls" -- the same list start.js's
// insert menu offers, so the two can never drift apart. CODE/SOURCELANG are its placeholders.
// The wizard's own template lists, straight out of data-main.json, so both gadgets offer the same
// templates in the same order. A pronunciation template names the languages it is for; one that names
// none is offered everywhere.
function templateChoices(pool, language) {
var defs = startData[pool] || {};
return Object.keys(defs).filter(function (name) {
var langs = defs[name].langs;
return !langs || !langs.length || $.inArray(language, langs) !== -1;
}).map(function (name) {
return { name: name, desc: defs[name].desc, snippet: defs[name].snippet };
});
}
// One template call inside the surface: shown as its own wikitext, but atomic -- contenteditable="false"
// keeps the caret out of it, so it can only be changed through the dialog.
function makeTemplateChip(wikitext) {
// The × is its own element rather than the chip's only way out: on a phone there is no reliable
// backspace onto a contenteditable="false" span, so without it the dialog is the only way to take
// a call off the line. readChipSurface() reads chips from data-wikitext, so the extra child does
// not reach the saved text.
var $remove = $("<span class='dsb-tpl-chip-remove' contenteditable='false'>×</span>")
.attr("title", "Vê şablonê jê bibe");
return $("<span class='dsb-tpl-chip' contenteditable='false' role='button' tabindex='0'></span>")
.attr("data-wikitext", wikitext)
.attr("title", "Bitikîne da ku parametreyan biguhêrî")
.text(wikitext)
.append($remove);
}
// wikitext -> surface DOM: template calls become chips, everything else stays plain editable text.
function renderChipSurface($surface, wikitext) {
var text = wikitext || "";
var calls = parseTemplateCalls(text).calls;
var pos = 0;
$surface.empty();
calls.forEach(function (call) {
if (call.start > pos) {
$surface.append(document.createTextNode(text.slice(pos, call.start)));
}
$surface.append(makeTemplateChip(text.slice(call.start, call.end)));
pos = call.end;
});
if (pos < text.length) {
$surface.append(document.createTextNode(text.slice(pos)));
}
}
// surface DOM -> wikitext. Chips give back their stored call; anything the browser inserted while typing
// (a <br>, a wrapper <div>) degrades to its text, so the result is always plain wikitext.
function readChipSurface(node) {
var out = "";
$(node).contents().each(function () {
if (this.nodeType === 3) {
out += this.nodeValue;
return;
}
var $el = $(this);
if ($el.hasClass("dsb-tpl-chip")) {
out += $el.attr("data-wikitext");
return;
}
if (this.nodeName === "BR") {
out += "\n";
return;
}
if (this.nodeName === "DIV" || this.nodeName === "P") {
out += "\n" + readChipSurface(this);
return;
}
out += readChipSurface(this);
});
return out;
}
// Drops a node in at the caret, or at the end when the caret is not inside the surface.
function insertAtCaret($surface, node) {
var sel = window.getSelection();
if (sel && sel.rangeCount && $.contains($surface.get(0), sel.getRangeAt(0).commonAncestorContainer)) {
var range = sel.getRangeAt(0);
range.deleteContents();
range.insertNode(node);
range.setStartAfter(node);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
} else {
$surface.append(node);
}
}
// The template-param dialog (openTemplateDialog/buildTemplateParamForm) moved to wikt.cite, shared with
// start.js's own Etîmolojî and Çavkanî pickers: cite.openTemplateDialog / cite.buildTemplateParamForm.
// Each new example queued is its own entry, so several can be waiting at once.
var exampleSeq = 0;
// Takes the save over from add-examples while its form is open for one sense: its "Qeyd bike" becomes
// "Lê zêde bike" and the edit joins the pending queue instead of publishing on its own, so examples and
// whatever else is queued go up as one edit. Installed on every open, so the handler always closes over
// the sense whose form is actually open. ctx: {$li, $h3, language, sectionType, sectionOrdinal,
// definitionLevel, $save, $status, afterQueue}.
function takeOverExampleSave(ctx) {
if (!(wikt.addExamples && wikt.addExamples.setSubmitHandler)) {
return; // older add-examples: it publishes on its own, as before
}
// Lets the form read an example as the queue has already changed it.
wikt.addExamples.pendingContent = queuedContent;
wikt.addExamples.setSubmitHandler(function (job) {
exampleSeq++;
queueChange(
placeLabel(ctx.language, ctx.sectionType, ctx.$h3),
// add-examples' own summary, so the combined edit still credits the tool and names its
// version. The trailing stop goes: this is one item in a list, not a sentence of its own.
(job.summary || "nimûne/jêgirtin hate zêdekirin").replace(/\.\s*$/, ""),
function (pageContent) {
// add-examples builds against a revision object, and returns {text, summary}.
var built = job.transform({ content: pageContent });
return { content: built.text, warnings: [] };
},
ctx.$save,
ctx.$status,
// Drawn like any other queued change, in the sense it belongs to.
function () {
return previewSense(ctx.$li, ctx.language, ctx.sectionType, ctx.sectionOrdinal, ctx.definitionLevel);
},
// An edit to an example already on the page brings its own key, so editing it again replaces it.
{ key: job.key || ("example:" + ctx.sectionType + "|" + ctx.sectionOrdinal + "|" +
ctx.definitionLevel.join(".") + "|" + exampleSeq) }
);
job.done();
if (ctx.afterQueue) {
ctx.afterQueue();
}
});
}
// Opens add-examples' form on one example already under a sense -- dsb.js's pencil on it. Its save joins
// the queue like any other change.
function editExample($li, $h3, language, sectionType, sectionOrdinal, definitionLevel, itemEl) {
takeOverExampleSave({ $li: $li, $h3: $h3, language: language, sectionType: sectionType,
sectionOrdinal: sectionOrdinal, definitionLevel: definitionLevel, $save: $(), $status: $(),
afterQueue: openQueueBox });
if (!(wikt.addExamples && wikt.addExamples.editFor && wikt.addExamples.editFor($li.get(0), itemEl))) {
mw.notify("Amûra nimûneyan li vê maneyê nehat dîtin.");
}
}
// Builds the whole "guhartina pêşketî" panel into $quickRow: gloss and the two most-used word lists
// (Hevmane/Dijmane) sit directly in the picker row; the remaining word lists, maneid, etîket, kategorî
// and Nimûne/jêgirtin live behind a "Hevmane û Bêhtir" dropdown; one shared "Lê zêde bike" queues everything the
// user filled in as a single edit via applyAdvancedChanges() and queueChange(). Nimûne/jêgirtin is an
// immediate proxy-click into a different gadget's own form, not part of this combined save.
function buildAdvancedPanel($quickRow, $li, $h3, language, sectionType, sectionOrdinal, definitionLevel, isLastSense) {
// "+" marks the actions that add something, matching the dropdown's own "+ Hevmane" labels.
var $nimune = $("<a href='#' class='dsb-quick-link'></a>").text("+ Nimûne/jêgirtin");
$nimune.on("click", function (e) {
e.preventDefault();
takeOverExampleSave({ $li: $li, $h3: $h3, language: language, sectionType: sectionType,
sectionOrdinal: sectionOrdinal, definitionLevel: definitionLevel, $save: $save, $status: $status });
// Toggle, so clicking this a second time closes the form again. add-examples builds nothing
// on the page of its own, so this is the only way its form opens.
var drive = wikt.addExamples.toggleFor || wikt.addExamples.openFor;
if (!(drive && drive($li.get(0)))) {
mw.notify("Amûra nimûneyan li vê maneyê nehat dîtin.");
}
$moreMenu.slideUp();
});
// getters holds one function per currently-open box (key -> () => value); collectChanges() only ever
// reads from a box the user actually clicked to open.
var getters = {};
// What the sense currently holds, fetched once and shared by every box that prefills from it.
// collectChanges() also diffs against it, so an untouched box never queues a change.
var original = null;
var sensePromise = null;
function loadSense() {
if (!sensePromise) {
sensePromise = fetchSenseFields(language, sectionType, sectionOrdinal, definitionLevel).then(function (fields) {
original = fields;
return fields;
});
}
return sensePromise;
}
function sameList(a, b) {
return (a || []).join("\u0000") === (b || []).join("\u0000");
}
// -- gloss ("Maneyê biguhêre" -- the one box that EDITS the sense's wording; everything else here only ADDS) --
function addGlossField() {
var $fields = $("<div class='dsb-gloss-fields'></div>");
var $textarea = $("<textarea class='dsb-input dsb-edit-gloss' rows='2'></textarea>")
.attr("placeholder", util.msg.loading).prop("disabled", true);
var $autoLink = $("<button type='button' class='dsb-autolink'>Oto-lînk bike</button>").prop("disabled", true);
var $toRaw = $("<a href='#' class='dsb-quick-link dsb-gloss-raw'>Wekî wîkîtekst biguhêre</a>").hide();
var $status = $("<span class='dsb-status'></span>");
var originalGloss = null;
var tplForm = null; // set while the parameter boxes are showing instead of the raw line
var tplCall = null;
// The gloss of a non-Kurdish entry is a translation followed by {{îzah|…}} explaining it. That
// is one field on the page, so it stays one box here; the explanation opens beside it only when
// asked for, and the two are joined back into a single gloss on save. A Kurdish sense is its
// own definition, so it is never offered one -- though a ku gloss that somehow already carries
// an {{îzah}} still opens its box below, rather than being edited blind.
var offerIzah = language !== "ku";
var $izah = $("<textarea class='dsb-input dsb-edit-izah' rows='2'></textarea>")
.attr("placeholder", "Îzaha maneyî… ji bo school-a îngilîzî 'cihê tehsîlgirtinê'");
// A non-Kurdish gloss is a translation and its explanation, so both boxes open together. A Kurdish
// sense is its own definition and gets no îzah box, unless its gloss already carries one.
var $izahWrap = $("<div class='dsb-izah-wrap'></div>")
.append($("<label class='dsb-adv-label'>Îzaha maneyî</label>").append($izah))
.toggle(offerIzah);
$fields.append($textarea);
// The template form edits one call inside the line, so its result is spliced back into the rest.
function currentGloss() {
if (tplForm) {
return $.trim(originalGloss.slice(0, tplCall.start) + tplForm.values() + originalGloss.slice(tplCall.end));
}
return joinGloss($.trim($textarea.val()), $.trim($izah.val()));
}
function showRaw() {
tplForm = null;
tplCall = null;
$fields.empty().append($textarea);
$autoLink.show();
$toRaw.hide();
}
loadSense().then(
function (fields) {
originalGloss = fields.gloss;
// An existing gloss that already ends in {{îzah|…}} comes apart into the two boxes, so
// editing it does not mean editing template syntax by hand.
var parts = util.splitTercimeIzah(fields.gloss);
$textarea.val(parts ? $.trim(parts.tercime) : fields.gloss)
.prop("disabled", false)
// A non-Kurdish gloss is a translation; the box under it takes the explanation.
.attr("placeholder", offerIzah ? util.msg.tercimePrompt : "Maneyê binivîse…");
if (parts) {
$izah.val($.trim(parts.izah));
$izahWrap.show();
}
$autoLink.prop("disabled", false);
var target = glossTemplateTarget(fields.gloss);
if (!target) {
return;
}
fetchTemplateData(target.name).then(function (td) {
// The fetch is async; leave the raw box alone if it has already been typed into.
if ($.trim($textarea.val()) !== $.trim(originalGloss)) {
return;
}
tplCall = target;
tplForm = cite.buildTemplateParamForm(target, td);
$fields.empty().append(tplForm.$element);
$autoLink.hide(); // linkifying a bare template call makes no sense
// A gloss that is one template call carries no separate explanation.
$izahWrap.hide();
$toRaw.show();
});
},
function (codeOrAbort, result) {
$textarea.attr("placeholder", "");
var message = codeOrAbort instanceof SubmitAbort ? codeOrAbort.message : "Nikare maneya heyî bar bike: " + apiErrorMessage(codeOrAbort, result);
$status.removeClass("dsb-status-ok").addClass("dsb-status-error").text(message);
}
);
$autoLink.on("click", function () {
var mane = $.trim($textarea.val());
var izah = $.trim($izah.val());
if (!mane && !izah) {
return;
}
$autoLink.prop("disabled", true);
$status.removeClass("dsb-status-ok dsb-status-error").text("Tê lînkkirin…");
linkifyGlossParts(mane, izah, function (linkedMane, linkedIzah, error) {
$textarea.val(linkedMane);
$izah.val(linkedIzah);
$autoLink.prop("disabled", false);
if (error) {
$status.addClass("dsb-status-error").text(error);
} else {
$status.text("");
}
});
});
$toRaw.on("click", function (e) {
e.preventDefault();
$textarea.val(currentGloss()); // read the form before showRaw() discards it
showRaw();
});
getters.gloss = function () {
var v = currentGloss();
return (v && v !== originalGloss) ? v : null;
};
return $("<div class='dsb-adv-group'></div>").append(
$("<div class='dsb-adv-label'></div>").text("Mane"),
$fields,
$izahWrap,
$("<div class='dsb-form-actions'></div>").append($autoLink, $toRaw, $status)
);
}
// -- one SENSE_GROUPS word-list box (Hevmane/Dijmane/etc.) --
function addWordField(group) {
var field = buildWordListField(group.label + "…");
loadSense().then(function (fields) {
var current = fields.words[group.key];
if (current && current.length) {
field.setValues(current);
}
});
getters["word:" + group.key] = function () {
return field.values();
};
// The hint is senseGroups' own, so the box carries the same example sentence the wizard puts
// beside its copy of this list.
var $label = $("<div class='dsb-adv-label'></div>").text(group.label);
if (group.hint) {
$label.append($("<span class='dsb-adv-hint'></span>").text(group.hint));
}
return $("<div class='dsb-adv-group'></div>").append($label, field.$element);
}
// -- maneid (single value; re-adding one that already exists is rejected -- see applyAdvancedChanges) --
function addNewSenseField() {
var $textarea = $("<textarea class='dsb-input' rows='2'></textarea>").attr("placeholder", "Maneya nû binivîse…");
getters.newSense = function () {
return $.trim($textarea.val());
};
return $("<div class='dsb-adv-group'></div>").append(
$("<div class='dsb-adv-label'></div>").text(util.msg.anotherSense),
$textarea
);
}
function addManeidField() {
var $input = $("<input type='text' class='dsb-input' placeholder='id…'>");
loadSense().then(function (fields) {
$input.val(fields.maneid);
});
getters.maneid = function () {
return $.trim($input.val());
};
return $("<div class='dsb-adv-group'></div>").append($("<div class='dsb-adv-label'></div>").text("Maneid"), $input);
}
// -- etîket / kategorî --
function addEtiketField() {
var field = buildTagField("Etîket…");
loadSense().then(function (fields) {
field.setValues(fields.etiket);
});
getters.etiket = function () {
return field.values();
};
return $("<div class='dsb-adv-group'></div>").append($("<div class='dsb-adv-label'></div>").text("Etîket"), field.$element);
}
function addKategoriField() {
var field = buildTagField("Kategorî…");
loadSense().then(function (fields) {
field.setValues(fields.kategori);
});
getters.kategori = function () {
return field.values();
};
return $("<div class='dsb-adv-group'></div>").append($("<div class='dsb-adv-label'></div>").text("Kategorî"), field.$element);
}
// -- the picker row: the gloss and Nimûne/jêgirtin links sit directly in the row; every word group and
// the rest live behind a "Hevmane û Bêhtir" dropdown. Clicking any link builds and appends its box
// to $openBoxes and hides that link; the box's own "×" removes it, clears its getter, and un-hides the
// link -- several boxes can be open at once. No "Beşek (====)" entry here: buildSubsectionPanel()'s
// own "+ Beşek lê zêde bike" trigger already covers page-level subsections. ----
var FIELD_DEFS = [{ key: "gloss", label: "Maneyê biguhêre", build: addGlossField, primary: true }]
.concat(SENSE_GROUPS.map(function (group) {
return { key: "word:" + group.key, label: group.label, build: function () { return addWordField(group); }, primary: false };
}))
.concat([
{ key: "maneid", label: "Maneid", build: addManeidField, primary: false },
{ key: "etiket", label: "Etîket", build: addEtiketField, primary: false },
{ key: "kategori", label: "Kategorî", build: addKategoriField, primary: false }
]);
// Every sense offers this: the new one lands directly below whichever sense asked for it (see
// insertNewSense), so it is no longer only the last one that can add to the end.
FIELD_DEFS.push({ key: "newSense", label: util.msg.addAnotherSense, build: addNewSenseField, primary: false });
var $picker = $("<div class='dsb-field-picker'></div>");
var $moreMenu = $("<div class='dsb-more-menu'></div>").hide();
var $openBoxes = $("<div class='dsb-open-boxes'></div>");
// $actions (built below) stays hidden until at least one field box is actually open -- otherwise
// "Lê zêde bike" sits there enabled with nothing to queue, which just produces an empty no-op change.
var openBoxCount = 0;
function updateActionsVisibility() {
$actions.toggle(openBoxCount > 0);
}
function makeFieldLink(def) {
var $link = $("<a href='#' class='dsb-quick-link dsb-field-pick'></a>").text(def.key === "gloss" ? def.label : "+ " + def.label);
$link.on("click", function (e) {
e.preventDefault();
openQueueBox();
var $box = def.build();
var $remove = $("<a href='#' class='dsb-box-remove' title='Rake'>×</a>");
$remove.on("click", function (e2) {
e2.preventDefault();
delete getters[def.key];
$box.remove();
$link.show();
openBoxCount--;
updateActionsVisibility();
});
$box.prepend($remove);
$openBoxes.append($box);
util.initManualResize($box);
$link.hide();
$moreMenu.slideUp();
openBoxCount++;
updateActionsVisibility();
});
return $link;
}
// Destructive, so it lives at the bottom of the dropdown behind a confirmation, and shares the
// sense's queue key: deleting supersedes any edit still pending on the same sense.
function makeDeleteLink() {
var $link = $("<a href='#' class='dsb-quick-link dsb-field-delete'>Maneyê rake</a>");
$link.on("click", function (e) {
e.preventDefault();
$moreMenu.slideUp();
OO.ui.confirm("Ev mane û hemû rêzên bin wê dê werin rakirin. Dewam bike?").done(function (ok) {
if (!ok) {
return;
}
queueChange(
placeLabel(language, sectionType, $h3),
"maneyek hate rakirin",
function (baseContent) {
return removeSense(baseContent, language, sectionType, sectionOrdinal, definitionLevel);
},
$save,
$note,
function () {
$li.addClass("dsb-sense-doomed");
return function () {
$li.removeClass("dsb-sense-doomed");
};
},
{ key: senseQueueKey() }
);
$note.show();
});
});
return $link;
}
// Only the links the row actually shows are built now; the dropdown's nine are 43% of the work and
// nobody sees them until "Hevmane û Bêhtir" is clicked, so buildMenuLinks() waits for that.
FIELD_DEFS.forEach(function (def) {
if (!def.primary) {
return;
}
$picker.append(makeFieldLink(def));
if (def.key === "gloss") {
$picker.append($nimune); // directly after "Maneyê biguhêre"
}
});
// Three sections, ruled apart: what labels this sense, the word lists that hang off it, and adding
// another sense. "Maneyê rake" is appended after these by makeDeleteLink() and so closes the last
// one. The word lists follow senseGroups' "order" -- that array's own order is the order the "#:"
// lines are written to the page in, which is not a reading order; the wizard's boxes read the same
// field, so both gadgets list them alike. Anything not named here still appears, after these, so a
// field added later is never silently dropped from the menu.
var MENU_GROUPS = [
["etiket", "kategori", "maneid"],
SENSE_GROUPS.slice().sort(function (a, b) {
return (a.order || 0) - (b.order || 0);
}).map(function (g) {
return "word:" + g.key;
}),
["newSense"]
];
var menuBuilt = false;
function buildMenuLinks() {
if (menuBuilt) {
return;
}
menuBuilt = true;
var byKey = {};
FIELD_DEFS.forEach(function (def) {
if (!def.primary) {
byKey[def.key] = def;
}
});
var placed = {};
MENU_GROUPS.forEach(function (group, groupIndex) {
var defs = group.map(function (key) {
return byKey[key];
}).filter(Boolean);
defs.forEach(function (def, i) {
placed[def.key] = true;
var $link = makeFieldLink(def);
// The rule goes on the first entry of every group after the first.
if (groupIndex > 0 && i === 0) {
$link.addClass("dsb-menu-group-start");
}
$moreMenu.append($link);
});
});
FIELD_DEFS.forEach(function (def) {
if (!def.primary && !placed[def.key]) {
$moreMenu.append(makeFieldLink(def));
}
});
$moreMenu.append(makeDeleteLink());
}
// Lives inside $picker (not appended straight to $quickRow) so .dsb-more-menu's CSS can position it as
// an overlay anchored to $picker, instead of pushing $openBoxes/the rest of the page down when it opens.
var $moreArrow = $("<span class='dsb-more-arrow'></span>").text("▸");
var $moreToggle = $("<a href='#' class='dsb-quick-link dsb-more-toggle'></a>")
.text(util.msg.moreMenu + " ").append($moreArrow);
$moreToggle.on("click", function (e) {
e.preventDefault();
buildMenuLinks();
$moreMenu.slideToggle(function () {
$moreArrow.text($moreMenu.is(":visible") ? "▾" : "▸");
});
});
$picker.append($moreToggle, $moreMenu);
// Outside clicks are handled by one shared document listener (see closeMoreMenus), not one per panel:
// hovering a sense builds its panel, so a long entry would otherwise accumulate dozens of them.
$moreMenu.data("dsbArrow", $moreArrow);
// -- combined save: whatever boxes are open right now, queued together via queueChange(); the actual
// save happens when the user clicks "Guhartinan qeyd bike" on the floating queue box. ----
// Small "not saved yet" preview of everything this queued change will do, appended right under the
// sense's gloss (plain text, not a real render of {{hevmane}}/etc.).
function collectChanges() {
var base = original || { maneid: "", etiket: [], kategori: [], words: {} };
var changed = {};
var words = {};
SENSE_GROUPS.forEach(function (group) {
var getter = getters["word:" + group.key];
words[group.key] = getter ? getter() : [];
if (getter && !sameList(words[group.key], base.words[group.key])) {
changed["word:" + group.key] = true;
}
});
var maneid = getters.maneid ? getters.maneid() : "";
var etiket = getters.etiket ? getters.etiket() : [];
var kategori = getters.kategori ? getters.kategori() : [];
if (getters.maneid && maneid !== (base.maneid || "")) {
changed.maneid = true;
}
if (getters.etiket && !sameList(etiket, base.etiket)) {
changed.etiket = true;
}
if (getters.kategori && !sameList(kategori, base.kategori)) {
changed.kategori = true;
}
return {
gloss: getters.gloss ? getters.gloss() : null,
newSense: getters.newSense ? getters.newSense() : null,
maneid: maneid,
etiket: etiket,
kategori: kategori,
words: words,
changed: changed
};
}
// Both the field save and "Maneyê rake" queue under this, so the later one supersedes the earlier.
function senseQueueKey() {
return "sense:" + sectionType + "|" + sectionOrdinal + "|" + definitionLevel.join(".");
}
var $save = $("<button type='button' class='dsb-save'></button>").text(util.msg.add);
// Where queueChange() writes its confirmation for actions that have no box of their own.
var $note = $("<div class='dsb-status dsb-quick-note'></div>").hide();
var $cancel = $("<button type='button' class='dsb-cancel'></button>").text(util.msg.cancel);
var $status = $("<span class='dsb-status'></span>");
var $actions = $("<div class='dsb-form-actions'></div>").append($save, $cancel, $status).hide();
$cancel.on("click", function () {
// Close the boxes too, not just the row. Hiding alone left every field still open with its
// text in it, so the next click on the pencil reopened the half-filled panel that had just
// been cancelled. Each box's own "x" already clears its getter, restores its link and updates
// the counter, so those are triggered rather than repeating the teardown here.
$openBoxes.find(".dsb-box-remove").trigger("click");
$note.hide().text("");
// dsb.js owns the pencil's pinned state; tell it rather than hiding behind its back, or the
// next click on the pencil would only toggle "pinned" off and appear to do nothing.
$quickRow.trigger("dsbCancel");
hideQueueIfIdle();
});
$save.on("click", function () {
var changes = collectChanges();
function afterLinkify() {
var parts = senseChangeLabels(changes);
queueChange(
placeLabel(language, sectionType, $h3),
parts.length
? parts.join(", ") + (parts.length > 1 ? " hatin guhartin" : " hate guhartin")
: "mane hate guhartin",
function (baseContent) {
return applyAdvancedChanges(baseContent, language, sectionType, sectionOrdinal, definitionLevel, changes);
},
$save,
$status,
function () {
var undoPreview = previewSense($li, language, sectionType, sectionOrdinal,
definitionLevel);
// A new sense has no place on the page yet, so it gets a row of its own directly
// after this one -- the list numbers it, so where it will land is visible.
var $newSense = null;
if (changes.newSense) {
$newSense = $("<li class='dsb-sense-preview'></li>");
$li.after($newSense);
fillWithRendered($newSense, changes.newSense, function () {
$newSense.text(changes.newSense);
});
}
return function () {
undoPreview();
if ($newSense) {
$newSense.remove();
}
};
},
// Re-saving this same sense supersedes its pending change rather than queuing another.
{ key: senseQueueKey() }
);
}
$save.prop("disabled", true);
// Auto-linkify the gloss as part of this save, but only a half nobody has linked by hand.
// Split back into its two halves first, so the explanation is linked on its own rather than
// sitting unseen inside {{îzah|…}}.
if (changes.gloss) {
$status.removeClass("dsb-status-ok dsb-status-error").text("Tê lînkkirin…");
var glossParts = util.splitTercimeIzah(changes.gloss);
linkifyGlossParts(
glossParts ? $.trim(glossParts.tercime) : changes.gloss,
glossParts ? $.trim(glossParts.izah) : "",
function (linkedMane, linkedIzah, error) {
// The change is queued either way, so a gloss that came back empty is not written
// back -- that would save the sense with its wording gone.
var joined = joinGloss(linkedMane, linkedIzah);
if ($.trim(joined)) {
changes.gloss = joined;
}
if (error) {
$status.removeClass("dsb-status-ok").addClass("dsb-status-error").text(error);
}
afterLinkify();
},
true
);
} else {
afterLinkify();
}
});
// The links sit under the buttons, so each open box pushes them down rather than leaving them far above.
$quickRow.empty().append($openBoxes, $actions, $picker, $note);
}
// Attaches the pencil toggle to one leaf definition <li> -- clicking it reveals the "guhartina pêşketî"
// (advanced editing) panel built by buildAdvancedPanel() above.
// ---- per-section UI: "+ Beşek lê zêde bike" after a POS block's own content ----
// Reads the header text of "====" subsections already rendered under this POS heading, straight from the DOM.
// The subsections of a part of speech are the headings one deeper than it is.
function subsectionDepthFor($h3) {
return (sections.headingDepth($h3) || 3) + 1;
}
function isSubsectionHeading($el, depth) {
return sections.headingDepth($el) === depth;
}
function existingSubsectionHeadersInDom($h3) {
var depth = subsectionDepthFor($h3);
var headers = [];
for (var $cursor = headingOuter($h3).next(); $cursor.length; $cursor = $cursor.next()) {
var here = sections.headingDepth($cursor);
if (here && here < depth) {
break; // out of this part of speech
}
if (isSubsectionHeading($cursor, depth)) {
var $hh = $cursor.is("h2, h3, h4, h5, h6") ? $cursor : $cursor.find("h2, h3, h4, h5, h6").first();
headers.push(headingText($hh));
}
}
return headers;
}
// Finds where a new "====" subsection preview belongs in the live DOM, using the same SUBSECTION_ORDER rank
// insertPageSubsection() uses. Returns {insertBefore: $el} (a ranked subsection that outranks this one) or
// {insertAfter: $el} (goes at the end of the block).
function domSubsectionInsertAnchor($h3, header) {
var depth = subsectionDepthFor($h3);
var myRank = subsectionRankForHeader(header);
var $last = headingOuter($h3);
for (var $cursor = headingOuter($h3).next(); $cursor.length; $cursor = $cursor.next()) {
var here = sections.headingDepth($cursor);
if (here && here < depth) {
break; // out of this part of speech
}
if (isSubsectionHeading($cursor, depth)) {
var $hh = $cursor.is("h2, h3, h4, h5, h6") ? $cursor : $cursor.find("h2, h3, h4, h5, h6").first();
var otherRank = subsectionRankForHeader(headingText($hh));
if (otherRank !== -1 && otherRank > myRank) {
return { insertBefore: $cursor };
}
}
$last = $cursor;
}
return { insertAfter: $last };
}
// The wikitext one subsection's content becomes, exactly as insertPageSubsection() writes it -- so the
// preview parses the same string the save will produce.
function subsectionBodyWikitext(language, s, content) {
if (s.kind !== "wordlist") {
return content;
}
return content.filter(function (group) {
return group.words.length;
}).map(function (group) {
return buildWordListCall(WORDLIST_TEMPLATE, language,
withNamedParam(["cure=" + s.header], "sernav", group.sernav), group.words);
}).join("\n\n");
}
// The plain word list this preview falls back to when the parse fails: the values that were typed,
// without the template around them. Right, but not what the page will look like.
function subsectionPlainDom($wrap, s, content) {
if (s.kind !== "wordlist") {
$wrap.append($("<p class='dsb-preview-text'></p>").text(content));
return;
}
content.forEach(function (group) {
if (group.sernav) {
$wrap.append($("<div class='dsb-preview-subheading'></div>").text(group.sernav));
}
var $list = $("<ul class='dsb-preview-list'></ul>");
group.words.forEach(function (w) {
$list.append($("<li></li>").text(w.replace(/<[^<>]*>/g, "")));
});
$wrap.append($list);
});
}
// A "not saved yet" preview of a page-level "====" subsection, dropped into the live page at the
// position the save will put it, with its body rendered the way the page renders it -- real {{kol3}}
// columns and real links, not a list of the words that went into them. Returns the inserted element so
// the caller can remove it again.
function buildSubsectionPreviewDom($h3, s, content, language) {
var $wrap = $("<div class='dsb-preview-block'></div>");
// A styled <div>, not a real <h4>, so existingSubsectionHeadersInDom() never mistakes this live-only
// preview for an actual subsection already on the page.
$wrap.append($("<div class='dsb-preview-heading' role='heading' aria-level='4'></div>").text(s.header));
$wrap.append($("<span class='dsb-preview-badge'></span>").text("nû — hîn nehatiye qeydkirin"));
// Placed before it is filled: a cached render resolves almost at once, and filling a wrap that is
// still detached would announce content nobody can see.
var anchor = domSubsectionInsertAnchor($h3, s.header);
if (anchor.insertBefore) {
anchor.insertBefore.before($wrap);
} else {
anchor.insertAfter.after($wrap);
}
var wikitext = language ? subsectionBodyWikitext(language, s, content) : "";
if ($.trim(wikitext)) {
var $body = $("<div class='dsb-preview-rendered'></div>").appendTo($wrap);
fillWithRendered($body, wikitext, function () {
$body.remove();
subsectionPlainDom($wrap, s, content);
});
} else {
subsectionPlainDom($wrap, s, content);
}
return $wrap;
}
// A free section owns what lies between its own heading and the next heading of any depth, matching
// the range locateHeadingByText() edits. Returns a function that puts the page back, or null.
function replaceFreeSectionInPlace($heading, wikitext) {
var $start = headingOuter($heading);
var body = [];
for (var $cursor = $start.next(); $cursor.length; $cursor = $cursor.next()) {
if (sections.headingDepth($cursor)) {
break;
}
if (!isOwnFurniture($cursor)) {
body.push($cursor.get(0));
}
}
if (!body.length) {
return null;
}
var $slot = $("<div class='dsb-sense-preview'></div>").insertAfter($start);
var $body = $(body).detach();
fillWithRendered($slot, wikitext, function () {
$slot.remove();
$slot = null;
$body.insertAfter($start);
});
return function () {
if ($slot) {
$body.insertAfter($start);
$slot.remove();
$slot = null;
}
};
}
/**
* Redraws a subsection that is already on the page with the content this edit would give it.
*
* The section's own nodes -- everything between its heading and the next heading of the same rank or
* higher -- are detached rather than dropped, so the returned function puts the page back exactly as it
* was when the change is cancelled or superseded.
*/
function replaceSubsectionInPlace($h3, header, wikitext) {
var depth = subsectionDepthFor($h3);
// Walked the same way existingSubsectionHeadersInDom() walks it, so "which nodes are this
// subsection" is decided once and the same way everywhere.
var $start = null;
var body = [];
for (var $cursor = headingOuter($h3).next(); $cursor.length; $cursor = $cursor.next()) {
var here = sections.headingDepth($cursor);
if (here && here < depth) {
break; // out of this part of speech
}
if (isSubsectionHeading($cursor, depth)) {
if ($start) {
break; // the next subsection starts here
}
var $hh = $cursor.is("h2, h3, h4, h5, h6") ? $cursor : $cursor.find("h2, h3, h4, h5, h6").first();
if (headingText($hh) === header) {
$start = $cursor;
}
continue;
}
// This gadget's own furniture stays where it is: the subsection's editing panel is the sibling
// right after its heading, so sweeping it into the body would detach the very panel the change
// was made in and leave the pencil toggling an element that is no longer on the page.
if ($start && !isOwnFurniture($cursor)) {
body.push($cursor.get(0));
}
}
if (!$start || !body.length) {
return null;
}
var $slot = $("<div class='dsb-sense-preview'></div>").insertAfter($start);
var $body = $(body).detach();
fillWithRendered($slot, wikitext, function () {
$slot.remove();
$slot = null;
$body.insertAfter($start);
});
return function () {
if ($slot) {
$body.insertAfter($start);
$slot.remove();
$slot = null;
}
};
}
/**
* The live DOM one "place" section occupies: {nodes, insert}. nodes are what a render replaces --
* empty when the section is not on the page yet -- and insert() puts an element where the section
* belongs, so a new one appears exactly where the save will put it.
*/
// This gadget's own panels sit between a heading and the page's content, so every walk steps over
// them rather than counting one as content.
function isOwnFurniture($el) {
return $el.is(".dsb-toolbar, .dsb-quick-row, .dsb-subsection-panel, .dsb-convert-pos-panel," +
" .dsb-preview-block, .dsb-sense-preview, .dsb-preview-inline");
}
// A heading's place in the page, which orders queued renames and removals bottom-up.
function headingPosition($el) {
var $h = $el.is("h2, h3, h4, h5, h6") ? $el : $el.find("h2, h3, h4, h5, h6").first();
return $("h2, h3, h4, h5, h6").index($h);
}
// One subsection's own nodes on the page, read the way headingSpan() reads the wikitext: a "===="
// subsection down to the next heading at its depth or shallower, a free section ($h3 is its heading)
// down to the next heading of any depth. hasDeeper says a free section has headings under it.
function subsectionDom($h3, s) {
var $start = null;
var depth;
if (s.free) {
$start = headingOuter($h3);
depth = sections.headingDepth($start) || sections.headingDepth($h3);
} else {
depth = subsectionDepthFor($h3);
for (var $c = headingOuter($h3).next(); $c.length; $c = $c.next()) {
var d = sections.headingDepth($c);
if (d && d < depth) {
break;
}
if (d === depth) {
var $hh = $c.is("h2, h3, h4, h5, h6") ? $c : $c.find("h2, h3, h4, h5, h6").first();
if (headingText($hh) === s.header) {
$start = $c;
break;
}
}
}
}
if (!$start) {
return null;
}
var body = [];
var hasDeeper = false;
for (var $cursor = $start.next(); $cursor.length; $cursor = $cursor.next()) {
var here = sections.headingDepth($cursor);
if (here && (s.free || here <= depth)) {
hasDeeper = !!s.free && here > depth;
break;
}
if (!isOwnFurniture($cursor)) {
body.push($cursor.get(0));
}
}
return { $heading: $start, $body: $(body), hasDeeper: hasDeeper };
}
function loosePlaceDom($h3, subsection) {
var place = subsection.place;
var nodes = [];
var $cursor;
if (place === "page-top") {
// Şablon:herwiha binêre renders as <div class="rellink binere"> at the head of the content.
var $out = $("#mw-content-text .mw-parser-output").first();
var $hat = $out.children(".binere, .rellink").first();
if ($hat.length) {
return { nodes: $hat.get(), insert: function ($el) { $hat.before($el); } };
}
return { nodes: [], insert: function ($el) { $out.prepend($el); } };
}
var $start = headingOuter($h3);
if (place === "pron") {
// A "===" heading above this POS block, under the language one.
for (var $back = $start.prev(); $back.length; $back = $back.prev()) {
if ($back.is(".mw-heading2, h2")) {
break;
}
if ($back.is(".mw-heading3, h3")) {
var $hh = $back.is("h3") ? $back : $back.find("h3").first();
if (headingText($hh) === subsection.header) {
for ($cursor = $back.next();
$cursor.length && !$cursor.is(".mw-heading2, .mw-heading3, h2, h3");
$cursor = $cursor.next()) {
if (!isOwnFurniture($cursor)) {
nodes.push($cursor.get(0));
}
}
return { nodes: nodes, insert: function ($el) { $back.after($el); } };
}
}
}
return { nodes: [], insert: function ($el) { $start.before($el); } };
}
if (place === "pos-top") {
// The thumbnails between the heading and the headword line.
for ($cursor = $start.next(); $cursor.length; $cursor = $cursor.next()) {
if (isOwnFurniture($cursor)) {
continue;
}
if (!$cursor.is(".thumb, figure, .floatright, .floatleft")) {
break;
}
nodes.push($cursor.get(0));
}
return { nodes: nodes, insert: function ($el) { $start.after($el); } };
}
return null;
}
// The wikitext a "place" section's pending content renders as. A word list is its one call; everything
// else is already the body that will be written.
function loosePlacePreviewWikitext(subsection, content, language) {
return subsection.kind === "wordlist"
? wt.buildHbnrCall(loosePlaceWords(content))
: loosePlaceBody(subsection, content, language);
}
// Draws a section's headword line from wikitext where the page's own line stands, or above the
// definitions when the section has none. The page's line is detached rather than dropped, so the
// returned function puts it back.
function replaceHeadwordInPlace($h3, wikitext) {
if (!$.trim(wikitext || "")) {
return null;
}
var $old = $();
var $at = $();
for (var $cursor = headingOuter($h3).next(); $cursor.length; $cursor = $cursor.next()) {
if ($cursor.is("p")) {
$old = $cursor;
break;
}
if (sections.headingDepth($cursor) || $cursor.is("ol")) {
$at = $cursor;
break;
}
}
var $slot = $("<div class='dsb-sense-preview'></div>");
if ($old.length) {
$old.before($slot);
} else if ($at.length) {
$at.before($slot);
} else {
return null;
}
var $body = $old.detach();
function restore() {
if (!$slot) {
return;
}
$body.insertBefore($slot);
$slot.remove();
$slot = null;
}
fillWithRendered($slot, wikitext, restore);
return restore;
}
// Redraws one of those places where it stands, the same way replaceSubsectionInPlace() redraws a
// "====" one. The page's own nodes are detached rather than dropped, so cancelling puts them back.
function replaceLoosePlaceInPlace($h3, subsection, wikitext) {
if (!$.trim(wikitext || "")) {
return null;
}
var at = loosePlaceDom($h3, subsection);
if (!at) {
return null;
}
var $slot = $("<div class='dsb-sense-preview'></div>");
at.insert($slot);
var $body = $(at.nodes).detach();
function restore() {
if (!$slot) {
return;
}
$body.insertBefore($slot);
$slot.remove();
$slot = null;
}
fillWithRendered($slot, wikitext, restore);
return restore;
}
// Builds the subsection picker + its word-list/text field. A subsection that isn't on the page yet is saved
// via insertPageSubsection(); one that already is gets prefilled and saved via updatePageSubsection().
// onlyHeader pins the panel to a single subsection (the per-heading pencil); omit it for the full picker.
// free pins the panel to a heading that is not a subsection of a part of speech, located by its own
// text: {header, ordinal}.
function buildSubsectionPanel($panel, $h3, language, sectionType, sectionOrdinal, onlyHeader, free) {
var existing = existingSubsectionHeadersInDom($h3);
// A heading the data does not name is still editable. It is declared a word list so the reader
// tries, in order, its {{kol3}} calls and then a plain bullet list; anything else falls through to
// raw wikitext, which is what prefill() does with a list it cannot read.
// Hidden counts as unknown here: the picker does not offer to build a Werger box, but one already
// on the page still opens, as the wikitext it is.
var unknown = onlyHeader && !PAGE_SUBSECTIONS.some(function (s) {
return s.header === onlyHeader && !s.hidden;
});
// A free section starts as wikitext: what it holds is only known once it has been read, and
// prefill() swaps in the {{kol3}} form there if it turns out to be a list.
var available = free ? [{
header: onlyHeader,
id: "free",
kind: "text",
free: free,
existing: true
}] : unknown ? [{
header: onlyHeader,
id: "unknown",
kind: "wordlist",
unknown: true,
existing: true
}] : PAGE_SUBSECTIONS.filter(function (s) {
// "hidden" takes a section out of the picker without removing it: Werger waits for the
// translation tools it belongs to, and offering a box those cannot yet fill misleads.
// Werger also carries lang: "ku" -- it is a Kurdish-entry section, so it is not offered
// elsewhere either.
return !s.hidden && (!s.lang || s.lang === language) &&
(!onlyHeader || s.header === onlyHeader);
}).map(function (s) {
return $.extend({}, s, { existing: existing.indexOf(s.header) !== -1 });
});
if (!available.length) {
$panel.text("Ji bo vê beşê guhartin tune.");
return;
}
var $select = $("<select class='dsb-subsection-select'></select>");
if (!onlyHeader) {
// Label-less on purpose: it holds the "nothing chosen yet" value without becoming a row in the
// list (makeSearchable() skips a blank option that has no label), so the prompt lives in the
// input's placeholder instead.
$select.append($("<option value=''></option>"));
}
available.forEach(function (s) {
// A "place" entry has no "====" heading of its own, so it is marked rather than left looking
// like the subsections around it.
var label = s.place ? s.header + " (ne binebeş e)"
: (s.existing ? s.header + " (biguhêre)" : s.header);
$select.append($("<option></option>").val(s.id).text(label));
});
var $selectWrap = makeSearchable($select, "Binebeşek hilbijêre");
if (onlyHeader) {
$selectWrap.hide(); // pinned to one subsection: nothing to choose
}
var $fieldWrap = $("<div class='dsb-subsection-field'></div>");
var $save = $("<button type='button' class='dsb-save'></button>").text(util.msg.add);
var $cancel = $("<button type='button' class='dsb-cancel'></button>").text(util.msg.cancel);
var $status = $("<span class='dsb-status'></span>");
var $actions = $("<div class='dsb-form-actions'></div>").append($save, $cancel, $status);
$cancel.on("click", function () {
resetForm();
$panel.hide();
hideQueueIfIdle();
});
// A panel pinned to a heading on the page can rename or remove it. Bilêvkirin and the other places
// have no heading of their own to change.
var headingEditable = !!onlyHeader && available.length === 1 && available[0].existing && !available[0].place;
var $nameWrap = null;
var readName = function () {
return onlyHeader;
};
if (headingEditable) {
var subject = available[0];
var $nameLabel = $("<div class='dsb-adv-label'>Nav</div>");
if (subject.free) {
var $nameInput = $("<input type='text' class='dsb-input dsb-heading-name'>").val(onlyHeader);
readName = function () {
return $.trim($nameInput.val());
};
$nameWrap = $("<div class='dsb-heading-name-wrap'></div>").append($nameLabel, $nameInput);
} else {
// The names the gadget knows, for a subsection of a part of speech; one this part of speech
// already has is left out.
var $nameSelect = $("<select class='dsb-heading-name'></select>");
var names = [onlyHeader];
PAGE_SUBSECTIONS.forEach(function (s) {
if (!s.place && !s.hidden && (!s.lang || s.lang === language) &&
names.indexOf(s.header) === -1 && existing.indexOf(s.header) === -1) {
names.push(s.header);
}
});
names.forEach(function (name) {
$nameSelect.append($("<option></option>").val(name).text(name));
});
var $nameSearch = makeSearchable($nameSelect, "Nav");
$nameSelect.val(onlyHeader).trigger("wiktku:refresh");
readName = function () {
return $nameSelect.val() || onlyHeader;
};
$nameWrap = $("<div class='dsb-heading-name-wrap'></div>").append($nameLabel, $nameSearch);
}
var subjectDom = subsectionDom($h3, subject);
if (!(subjectDom && subjectDom.hasDeeper)) {
var $delete = $("<button type='button' class='dsb-delete'>Beşê jê bibe</button>");
$cancel.after($delete);
$delete.on("click", function () {
OO.ui.confirm("Beşa «" + subject.header + "» dê were rakirin. Dewam bike?").done(function (ok) {
if (!ok) {
return;
}
queueHeadingChange(subject, "beşa «" + subject.header + "» hate rakirin",
function (pageContent) {
return removeSubsection(pageContent, language, sectionType, sectionOrdinal, subject);
},
function () {
var dom = subsectionDom($h3, subject);
if (!dom) {
return null;
}
var $doomed = dom.$heading.add(dom.$body).addClass("dsb-preview-removed");
return function () {
$doomed.removeClass("dsb-preview-removed");
};
});
});
});
}
}
// Renaming and removing share one queue key, so the later choice replaces the earlier one. Both run
// last and bottom-up, since other queued changes find the section by the heading they change.
function queueHeadingChange(s, action, transform, previewFn) {
var dom = subsectionDom($h3, s);
queueChange(subsectionPlaceLabel(s, language, sectionType, $h3), action,
function (pageContent) {
return { content: transform(pageContent), warnings: [] };
},
$save, $status, previewFn,
{
runLast: true,
key: "heading:" + (s.free ? s.header + "|" + s.free.ordinal
: sectionType + "|" + sectionOrdinal + "|" + s.header),
pagePosition: dom ? headingPosition(dom.$heading) : 0
});
}
function queueRename(s, newName) {
queueHeadingChange(s, "beşa «" + s.header + "» bû «" + newName + "»",
function (pageContent) {
return renameSubsection(pageContent, language, sectionType, sectionOrdinal, s, newName);
},
function () {
var dom = subsectionDom($h3, s);
if (!dom) {
return null;
}
var $note = $("<div class='dsb-preview-inline dsb-preview-heading-note'></div>")
.text("→ " + newName + " — nû, hîn nehatiye qeydkirin");
dom.$heading.after($note);
return function () {
$note.remove();
};
});
}
// Undefined while the placeholder option is selected.
function currentSubsection() {
var id = $select.val();
return id ? available.filter(function (s) {
return s.id === id;
})[0] : undefined;
}
// One entry per "{{kol3}}" group in the current wordlist section; empty for any other kind.
var groupFields = [];
var $groups = null;
var editAsText = false; // set when an existing wordlist section turns out to be unparseable
// Whatever has been typed into each subsection's box, so switching the picker away and back restores it
// instead of rebuilding an empty field. Keyed by subsection id; lastId is whichever box is on screen.
var drafts = {};
var lastId = null;
// Called before renderField() rebuilds, while the group fields still hold the outgoing box.
function captureDraft() {
if (!lastId) {
return;
}
drafts[lastId] = imagesField
? { asText: false, images: imagesField.values() }
: (groupFields.length
? { asText: false, groups: readGroups() }
: { asText: editAsText, text: fieldText() });
}
function restoreDraft(s, draft) {
// An existing wordlist we previously fell back to raw wikitext for stays in that mode.
if (draft.asText && s.kind === "wordlist") {
editAsText = true;
groupFields = [];
renderTextArea(s, 6);
}
if (imagesField) {
imagesField.setValues(draft.images);
} else if (groupFields.length) {
setGroups(s, draft.groups);
} else {
setFieldText(draft.text);
}
}
// Etîmolojî is edited on a live surface (see renderChipSurface) rather than in a textarea, so every
// read and write of the field goes through these two instead of touching .dsb-subsection-text.
var chipSurface = null;
function fieldText() {
if (wergerForm) {
return buildWergerBlock(wergerForm.read());
}
return chipSurface
? $.trim(readChipSurface(chipSurface.get(0)))
: $.trim($fieldWrap.find(".dsb-subsection-text").val() || "");
}
function setFieldText(value) {
if (wergerForm) {
wergerForm.set(parseWergerBlock(value));
return;
}
if (chipSurface) {
renderChipSurface(chipSurface, value || "");
var schedule = chipSurface.data("dsbSchedulePreview");
if (schedule) {
schedule(); // show what the loaded etymology renders as
}
} else {
$fieldWrap.find(".dsb-subsection-text").val(value || "");
}
}
// The contenteditable surface: text typed directly, template calls as chips that open the dialog,
// plus start.js's etymology templates in a searchable insert menu.
// A live surface where a template shows as a chip, with a menu that inserts one. Etîmolojî and
// Bilêvkirin are both written this way; they differ only in which templates the menu offers.
function renderTemplateEditor(s, templates) {
$fieldWrap.empty();
var $select = $("<select class='dsb-subsection-select'></select>");
$select.append($("<option value=''></option>"));
templates.forEach(function (t) {
$select.append($("<option></option>").val(t.snippet).text(t.name + " — " + t.desc));
});
var $surface = $("<div class='dsb-tpl-surface' contenteditable='true'></div>")
.attr("role", "textbox")
.attr("aria-multiline", "true");
chipSurface = $surface;
// Clicking the insert menu moves focus out of the surface and the browser forgets the caret, so
// the last caret position inside the surface is kept and restored before inserting -- otherwise
// focus() drops it back to the very start and every template lands at the beginning of the line.
var savedRange = null;
function rememberCaret() {
var sel = window.getSelection();
if (!sel || !sel.rangeCount) {
return;
}
var range = sel.getRangeAt(0);
var root = $surface.get(0);
if (range.commonAncestorContainer === root || $.contains(root, range.commonAncestorContainer)) {
savedRange = range.cloneRange();
}
}
$surface.on("keyup mouseup focusout", rememberCaret);
function restoreCaret() {
if (!savedRange) {
return;
}
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(savedRange);
}
$select.on("change", function () {
var snippet = $select.val();
if (!snippet) {
return;
}
// Same substitution start.js makes: CODE is this section's language, SOURCELANG is left for
// the user to fill in.
snippet = snippet.replace(/CODE/g, language).replace(/SOURCELANG/g, "?");
// A pronunciation template is a bullet of its own ("* {{ku-IPA}}"), so it starts a line
// rather than landing in the middle of one.
var ownLine = /^\*\s/.test(snippet);
if (ownLine) {
snippet = $.trim(snippet.replace(/^\*\s*/, ""));
}
$surface.focus();
restoreCaret();
var $chip = makeTemplateChip(snippet);
var piece = document.createDocumentFragment();
if (ownLine) {
// The bullet, and a line of its own unless the caret already starts one.
var at = caretOffset();
var before = at >= 0 ? readChipSurface($surface.get(0)).slice(0, at) : "";
piece.appendChild(document.createTextNode(!before || /\n$/.test(before) ? "* " : "\n* "));
}
piece.appendChild($chip.get(0));
insertAtCaret($surface, piece);
rememberCaret(); // so a second insert lands after the first, not back at the old spot
$select.val("");
// Straight into the dialog: the snippet is placeholders ("?", "…") until someone fills it in.
cite.openTemplateDialog(snippet).then(function (edited) {
applyChipEdit($chip, edited);
syncPanelWidth();
});
});
// Paste as plain text: pasted markup would otherwise become real nodes in the surface.
$surface.on("paste", function (e) {
var clip = e.originalEvent && e.originalEvent.clipboardData;
if (!clip) {
return;
}
e.preventDefault();
document.execCommand("insertText", false, clip.getData("text/plain"));
});
// The chip the dialog was opened for, or the one that has taken its place. renderChipSurface()
// rebuilds the surface from its wikitext whenever a call is typed in as text, which throws away
// the element this closure is holding; without this, "Jê bibe" would remove a node that is no
// longer on screen and appear to do nothing.
function liveChip($chip, wikitext) {
if ($.contains($surface.get(0), $chip.get(0))) {
return $chip;
}
return $surface.find(".dsb-tpl-chip").filter(function () {
return $(this).attr("data-wikitext") === wikitext;
}).first();
}
function applyChipEdit($chip, edited) {
if (edited === null || !$chip.length) {
return;
}
if (!$.trim(edited)) {
$chip.remove();
return;
}
// Edited into something that is no longer one template call: let it fall back to plain text
// rather than keeping a chip that lies about its contents.
var call = parseTemplateCalls(edited);
if (call.otherContent || call.calls.length !== 1) {
$chip.replaceWith(document.createTextNode(edited));
return;
}
$chip.replaceWith(makeTemplateChip(edited));
}
// Double-click edits the call's wikitext in place; single click opens the parameter dialog. The
// click is held back briefly so the first of a double-click does not open the dialog underneath.
var chipClickTimer = null;
var lastChipTap = 0;
var lastChipNode = null;
var DOUBLE_TAP_MS = 300;
// Double-tap drops the chip back to plain wikitext in the surface, rather than making the chip
// itself editable: a contenteditable inside a contenteditable gives Android a selection handle and
// no usable caret. As plain text the browser's own editing works, and chipifyTyped() puts the chip
// back once the caret leaves the call.
// A collapsed caret inside the text just inserted: at the character tapped where the browser can
// say which that is, otherwise just inside the closing braces so the caret stays within the call
// (which is what stops chipifyTyped putting the chip straight back).
function caretRangeIn(node, event) {
var range;
var x = event && event.clientX;
var y = event && event.clientY;
if (x !== undefined && document.caretRangeFromPoint) {
range = document.caretRangeFromPoint(x, y);
} else if (x !== undefined && document.caretPositionFromPoint) {
var pos = document.caretPositionFromPoint(x, y);
if (pos) {
range = document.createRange();
range.setStart(pos.offsetNode, pos.offset);
}
}
if (range && range.startContainer === node) {
range.collapse(true);
return range;
}
var text = node.nodeValue;
range = document.createRange();
range.setStart(node, /\}\}$/.test(text) ? text.length - 2 : text.length);
range.collapse(true);
return range;
}
function startInlineEdit($chip, event) {
if (!$.contains($surface.get(0), $chip.get(0))) {
return; // the tap timer and dblclick can both fire; only the first one has a chip left
}
var node = document.createTextNode($chip.attr("data-wikitext"));
$chip.replaceWith(node);
$surface.get(0).focus();
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(caretRangeIn(node, event));
}
// Bound before the chip's own handler and stopping there, so removing a call never opens the
// dialog for it on the way out.
$surface.on("click", ".dsb-tpl-chip-remove", function (e) {
e.preventDefault();
e.stopImmediatePropagation();
clearTimeout(chipClickTimer);
lastChipNode = null;
$(this).closest(".dsb-tpl-chip").remove();
syncPanelWidth();
});
$surface.on("click keydown", ".dsb-tpl-chip", function (e) {
if (e.type === "keydown" && e.which !== 13 && e.which !== 32) {
return;
}
e.preventDefault();
var $chip = $(this);
clearTimeout(chipClickTimer);
// A second tap on the same chip edits it inline. Timed here rather than left to "dblclick",
// which mobile browsers fire unreliably or not at all for a double-tap.
var now = new Date().getTime();
if (lastChipNode === this && now - lastChipTap < DOUBLE_TAP_MS) {
lastChipNode = null;
startInlineEdit($chip, e);
return;
}
lastChipTap = now;
lastChipNode = this;
var wikitext = $chip.attr("data-wikitext");
chipClickTimer = setTimeout(function () {
cite.openTemplateDialog(wikitext).then(function (edited) {
applyChipEdit(liveChip($chip, wikitext), edited);
syncPanelWidth();
});
}, DOUBLE_TAP_MS);
});
$surface.on("dblclick", ".dsb-tpl-chip", function (e) {
e.preventDefault();
clearTimeout(chipClickTimer);
startInlineEdit($(this), e);
});
// A long etymology cannot be read in the panel's usual 520px column, so the panel drops its
// width cap once there is enough text to need it. What it will look like is shown by the
// in-place render on the page itself, not by a second copy inside the form.
var WIDE_AT = 240; // characters
function syncPanelWidth() {
var wikitext = readChipSurface($surface.get(0));
if (wikitext.length > WIDE_AT || wikitext.indexOf("\n") !== -1) {
$surface.closest(".dsb-subsection-panel").addClass("dsb-panel-wide");
}
}
// A template typed by hand becomes a chip once typing settles, so it behaves like an inserted one:
// click for the parameter dialog, double-click for its raw wikitext.
var composing = false;
// The caret as an offset into the surface's wikitext, which is what survives rebuilding the DOM.
function caretOffset() {
var sel = window.getSelection();
var root = $surface.get(0);
if (!sel || !sel.rangeCount) {
return -1;
}
var range = sel.getRangeAt(0);
if (range.startContainer !== root && !$.contains(root, range.startContainer)) {
return -1; // caret is somewhere else on the page
}
var pre = document.createRange();
pre.selectNodeContents(root);
pre.setEnd(range.startContainer, range.startOffset);
var holder = document.createElement("div");
holder.appendChild(pre.cloneContents());
return readChipSurface(holder).length;
}
// Puts the caret back at that wikitext offset. Only runs straight after a rebuild, so the surface
// holds nothing but text nodes and chips.
function placeCaret(offset) {
var root = $surface.get(0);
var range = document.createRange();
var nodes = root.childNodes;
root.focus();
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i];
var len = node.nodeType === 3
? node.nodeValue.length
: ($(node).attr("data-wikitext") || $(node).text()).length;
if (offset <= len) {
if (node.nodeType === 3) {
range.setStart(node, offset);
} else {
range.setStartAfter(node); // a chip is atomic; the caret goes after it
}
break;
}
offset -= len;
}
if (!range.startContainer || range.startContainer === document) {
range.selectNodeContents(root);
}
range.collapse(true);
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
// True when the selection overlaps the inside of a template call. A caret resting exactly at the
// closing "}}" does not count -- that is someone who has just finished typing one.
function selectionTouchesCall(wikitext) {
var sel = window.getSelection();
if (!sel || !sel.rangeCount) {
return false;
}
var range = sel.getRangeAt(0);
var root = $surface.get(0);
if (range.startContainer !== root && !$.contains(root, range.startContainer)) {
return false;
}
var from = caretOffset();
var to = from;
if (!range.collapsed) {
var pre = document.createRange();
pre.selectNodeContents(root);
pre.setEnd(range.endContainer, range.endOffset);
var holder = document.createElement("div");
holder.appendChild(pre.cloneContents());
to = readChipSurface(holder).length;
}
return parseTemplateCalls(wikitext).calls.some(function (call) {
return from < call.end && to > call.start;
});
}
function chipifyTyped() {
if (composing) {
return; // never rebuild under an IME
}
var wikitext = readChipSurface($surface.get(0));
// Chips already hold one call each, so a higher call count means one was typed in as text.
if (parseTemplateCalls(wikitext).calls.length <= $surface.find(".dsb-tpl-chip").length) {
return;
}
var offset = caretOffset();
if (selectionTouchesCall(wikitext)) {
return; // still inside a call: leave it as text until the caret moves out
}
renderChipSurface($surface, wikitext);
savedRange = null; // the old range points at nodes that no longer exist
if (offset >= 0) {
placeCaret(offset);
rememberCaret();
}
}
$surface.on("compositionstart", function () {
composing = true;
});
$surface.on("compositionend", function () {
composing = false;
schedulePreview();
});
var previewTimer = null;
function schedulePreview() {
clearTimeout(previewTimer);
previewTimer = setTimeout(function () {
chipifyTyped();
syncPanelWidth();
}, 700);
}
$surface.on("input", schedulePreview);
$surface.data("dsbSchedulePreview", schedulePreview);
$fieldWrap.append(makeSearchable($select, util.msg.addTemplate), $surface);
// Bilêvkirin is written in IPA, so it gets the same character palette the wizard shows. Its
// characters go in as text and its two template buttons as chips, which is what the rest of
// this surface is made of.
if (s.place === "pron") {
$fieldWrap.append(util.buildIpaPalette({
insertText: function (text) {
$surface.focus();
restoreCaret();
insertAtCaret($surface, document.createTextNode(text));
rememberCaret();
schedulePreview();
},
insertTemplate: function (snippet) {
$surface.focus();
restoreCaret();
var $chip = makeTemplateChip(snippet.replace(/\|\s*\}\}$/, "}}"));
insertAtCaret($surface, $chip.get(0));
rememberCaret();
cite.openTemplateDialog($chip.attr("data-wikitext")).then(function (edited) {
applyChipEdit($chip, edited);
syncPanelWidth();
});
}
}));
}
}
// The three "place" sections are wikitext with a shape worth spelling out, since nothing on screen
// says what belongs in the box.
var PLACE_PLACEHOLDERS = {
"page-top": "{{h-bnr|Kelîme|Kelîmeyeke din}} an jî {{herwiha binêre/ane}}",
"pos-top": "[[Wêne:Navê pelî.jpg|thumb|Binnivîs]]",
"pron": "* {{ku-IPA}}"
};
function renderTextArea(s, rows) {
$fieldWrap.empty().append(
$("<textarea class='dsb-input dsb-subsection-text'></textarea>")
.attr("rows", rows)
.attr("placeholder", PLACE_PLACEHOLDERS[s.place] || (s.header + "…"))
);
util.initManualResize($fieldWrap);
}
// A translation box: the {{werger-ser}}/{{werger-bin}} pair, with the sense it covers and the
// layout parameters that template accepts. The rows in between are left to the translation editor
// gadget, which is what edits them on the rendered page.
// Reads and writes through fieldText()/setFieldText() as wikitext, so drafts, save and the queue
// need no special case beyond the append below.
var wergerForm = null;
var imagesField = null;
function renderWergerField(s) {
var inputs = {};
// Its own row class rather than .dsb-tpl-param-row: that one is styled in cite.css, which is
// only on the page when wikt.cite happens to be loaded, and without it the label falls back to
// sitting inline and a short one ("id") is overlapped by its own box.
function row(key, label, placeholder) {
inputs[key] = $("<input type='text' class='dsb-input'>").attr("placeholder", placeholder || "");
return $("<div class='dsb-werger-row'></div>").append(
$("<label class='dsb-adv-label'></label>").text(label).append(inputs[key])
);
}
// {{werger-ser}} also takes stûn, firehî, bgcolor, valign and align, but they only restyle the
// box and its defaults are what these pages use, so the form does not offer them. They are
// still in WERGER_NAMED, so a block that already sets one is read back correctly.
var $main = $("<div class='dsb-werger-main'></div>").append(
row("1", "Mane", "ava vexwarinê"),
row("2", "Mane no", "ne mecbûrî — di parantezê de xuya dike"),
row("id", "id", "ne mecbûrî — IDya tercimeyê")
);
var $note = $("<p class='dsb-werger-note'></p>").text(
"Qutiyeke Wergerê ya vala lê zêde dikî. Tercimeyan bi amûra wergerê dûre binivîse."
);
wergerForm = {
read: function () {
var out = {};
$.each(inputs, function (key, $input) {
var value = $.trim($input.val() || "");
if (value) {
out[key] = value;
}
});
return out;
},
set: function (params) {
$.each(inputs, function (key, $input) {
$input.val((params && params[key]) || "");
});
}
};
$fieldWrap.empty().append($note, $main);
}
// Same [[Kategorî:Çavkanî]] search widget start.js's own picker uses: choosing an entry inserts
// "* {{Target}}" at the cursor. The field itself stays the plain ".dsb-subsection-text" textarea
// every other "kind: text" subsection already uses, so save/prefill/drafts need no changes.
function renderCavkaniField(s) {
var $ta = $("<textarea class='dsb-input dsb-subsection-text'></textarea>")
.attr("rows", 4)
.attr("placeholder", util.msg.pickOrTypeTemplate);
var picker = cite.buildCategoryTemplateSearch({
placeholder: util.msg.addTemplate,
describe: true,
// Same param dialog Etîmolojî opens on a fresh insert: filled from the picked template's own
// TemplateData, so the user adds "rûpel=", "sernav=", ... before anything lands in the field
// rather than typing a bare {{Target}} and going back to fill params in by hand.
onPick: function (entry) {
cite.openTemplateDialog("{{" + entry.target + "}}").then(function (edited) {
if (!edited) {
return; // cancelled, or "Jê bibe" on a call that was never inserted
}
util.insertAtCursor($ta.get(0), "* " + edited + "\n");
$ta.trigger("focus");
});
}
});
var $picker = $("<div class='dsb-cavkani-picker'></div>").append(picker.$element);
$fieldWrap.empty().append($picker, $ta);
}
// One "{{kol3}}" group: its "sernav=" heading and its words. Several of these make up one section --
// "Herwiha binêre" typically carries an unheaded list plus a few headed ones.
function addGroup(s, group) {
var $sernav = $("<input type='text' class='dsb-input'>").attr("placeholder", "ne mecbûrî…");
// {{h-bnr}} takes bare page names, so its rows carry no ⊕: there is nowhere in the call for a
// modifier to go.
var words = buildWordListField(s.header + "…", { modifiers: !s.single });
var $remove = $("<a href='#' class='dsb-quick-link dsb-group-remove'>Komê rake</a>");
var $element = $("<div class='dsb-wordgroup'></div>").append(
// Label beside the box, not above it: stacked, "Sernav" read as a heading for the whole
// group rather than as the name of the field under it. The input is nested in the label so
// clicking the word focuses the box, without needing an id per group.
// A single-call list has no "sernav=" to carry, so the row is built (readGroups still reads
// it) but never shown. Hidden here rather than in renderField: setGroups() rebuilds every
// group on prefill, which would put a visible one back.
$("<div class='dsb-tpl-param-row dsb-param-row-inline'></div>").append(
$("<label class='dsb-adv-label'></label>").text("Sernav").append($sernav)
).toggle(!s.single),
words.$element,
$remove
);
var field = {
$element: $element,
read: function () {
return { sernav: $.trim($sernav.val()), words: words.values() };
},
set: function (g) {
$sernav.val((g && g.sernav) || "");
words.setValues((g && g.words) || []);
}
};
$remove.on("click", function (e) {
e.preventDefault();
var at = $.inArray(field, groupFields);
if (at !== -1) {
groupFields.splice(at, 1);
}
$element.remove();
updateGroupChrome();
});
field.set(group);
groupFields.push(field);
$groups.append($element);
updateGroupChrome();
return field;
}
// With a single group there is nothing to tell apart, so it needs neither a remove link nor a label.
function updateGroupChrome() {
var many = groupFields.length > 1;
$groups.toggleClass("dsb-wordgroups-many", many);
$groups.find(".dsb-group-remove").toggle(many);
}
function readGroups() {
return groupFields.map(function (field) {
return field.read();
});
}
// Rebuilds the blocks to match what the page (or the draft) holds, always leaving at least one.
function setGroups(s, groups) {
groupFields = [];
$groups.empty();
(groups && groups.length ? groups : [null]).forEach(function (group) {
addGroup(s, group);
});
}
// Loads what the page already has into the form, so an edit starts from the current list rather than
// from an empty one. A wordlist section we can't parse degrades to raw wikitext editing.
function prefill(s) {
$status.removeClass("dsb-status-error dsb-status-ok").text("Tê barkirin…");
$save.prop("disabled", true);
fetchPageSubsection(language, sectionType, sectionOrdinal, s).then(function (current) {
$save.prop("disabled", false);
$status.text("");
if (!current) {
return;
}
// A "place" section has no heading of its own, so whether the page already has one cannot
// be seen in the DOM the way a "====" subsection can -- it is known only once its content
// has been read. That is what decides "nûkirin" or "zêdekirin" in the edit summary.
if (s.place) {
s.existing = !!(current.images ? current.images.length : $.trim(current.text || ""));
// The label was set before this content arrived, when nothing yet said one was there.
$save.text(s.existing && s.kind !== "werger" ? util.msg.edit : util.msg.add);
}
if (current.kind === "images" && imagesField) {
imagesField.setValues(current.images);
return;
}
// A free section's field was built before its content was known: a list turns it into the
// {{kol3}} form, anything else leaves it the wikitext box it already is.
if (s.free && current.kind === "wordlist" && !groupFields.length) {
s.kind = "wordlist";
renderWordListField(s);
setGroups(s, current.groups);
if (current.fromBullets) {
$status.text("Ev lîste made bi made hatiye nivîsîn — bi qeydkirinê dibe {{kol3}}.");
}
return;
}
if (current.kind === "wordlist" && groupFields.length) {
setGroups(s, current.groups);
if (current.fromBullets) {
$status.text("Ev lîste made bi made hatiye nivîsîn — bi qeydkirinê dibe {{kol3}}.");
}
return;
}
if (s.kind === "wordlist") {
editAsText = true;
groupFields = [];
renderTextArea(s, 6);
$status.text(s.unknown
? "Ev beş nayê naskirin — wekî wîkîtekst tê guhartin."
: "Şablona vê lîsteyê nehate naskirin — wekî wîkîtekst biguhêre.");
}
setFieldText(current.text);
}, function () {
$save.prop("disabled", false);
$status.addClass("dsb-status-error").text("Nikare muhtewaya heyî bixwîne.");
});
}
// One "{{kol3}}" group per block, plus a way out to raw wikitext for a list no row can express --
// a shared subpage list like {{herwiha binêre/ane}}, say.
function renderWordListField(s) {
$fieldWrap.empty();
chipSurface = null;
editAsText = false;
var $addGroup = $("<a href='#' class='dsb-quick-link'>+ Komek lê zêde bike</a>");
$addGroup.on("click", function (e) {
e.preventDefault();
addGroup(s, null);
});
$groups = $("<div class='dsb-wordgroups'></div>");
$fieldWrap.append($groups, $addGroup);
addGroup(s, null);
// One call takes one flat list of names, so there is no second group to add.
if (s.single) {
$addGroup.hide();
}
var $toRaw = $("<a href='#' class='dsb-quick-link'>Wekî wîkîtext biguhêre</a>");
$toRaw.on("click", function (e) {
e.preventDefault();
var current = s.place
? wt.buildHbnrCall(loosePlaceWords(readGroups()))
: subsectionBodyWikitext(language, s, readGroups());
editAsText = true;
groupFields = [];
$groups = null;
renderTextArea(s, 3);
setFieldText(current);
util.initManualResize($fieldWrap);
});
$fieldWrap.append($toRaw);
util.initManualResize($fieldWrap);
}
function renderField() {
var s = currentSubsection();
$fieldWrap.empty();
groupFields = [];
$groups = null;
editAsText = false;
chipSurface = null;
wergerForm = null;
imagesField = null;
$status.removeClass("dsb-status-error dsb-status-ok").text("");
// Save stays disabled (but "Betal bike" usable) until something is actually chosen.
$save.prop("disabled", !s);
if (!s) {
$save.text(util.msg.add);
lastId = null;
return; // still on "Binebeşekê hilbijêre"
}
if (s.kind === "wordlist") {
renderWordListField(s);
} else if (s.kind === "images") {
imagesField = util.buildImageListField({
prefix: "dsb-",
rowClass: "dsb-tpl-param-row dsb-param-row-inline",
inputClass: "dsb-input",
addClass: "dsb-quick-link",
pageName: mw.config.get("wgTitle")
});
$fieldWrap.append(imagesField.$element);
} else if (s.header === "Etîmolojî") {
renderTemplateEditor(s, templateChoices("etymTpls", language));
} else if (s.place === "pron") {
renderTemplateEditor(s, templateChoices("pronTpls", language));
} else if (s.kind === "werger") {
renderWergerField(s);
} else if (s.header === "Çavkanî") {
renderCavkaniField(s);
} else {
renderTextArea(s, 3);
}
// Werger always adds a box, even to a section that is already there, so its button never says
// "Biguhêre".
$save.text(s.existing && s.kind !== "werger" ? util.msg.edit : util.msg.add);
// Covers whichever branch above built the field. Idempotent, so a textarea already carrying a
// grip is skipped.
util.initManualResize($fieldWrap);
lastId = s.id;
// A draft always wins over re-reading the page: it is the newer, unsaved state.
if (drafts[s.id]) {
restoreDraft(s, drafts[s.id]);
} else if ((s.existing || s.place) && s.kind !== "werger") {
prefill(s);
}
}
$select.on("change", function () {
captureDraft();
renderField();
});
renderField();
// Back to the state a fresh open would show: no draft, no picked subsection. Called directly
// rather than through the "change" handler above, so the discarded draft is never captured first.
function resetForm() {
drafts = {};
if (!onlyHeader) {
$select.val("").trigger("wiktku:refresh");
}
renderField();
}
$save.on("click", function () {
var s = currentSubsection();
if (!s) {
return;
}
var newName = readName();
if (!newName) {
$status.removeClass("dsb-status-ok").addClass("dsb-status-error").text("Navê beşê vala ye.");
return;
}
var asText = editAsText || (s.kind !== "wordlist" && s.kind !== "images");
var effective = asText ? $.extend({}, s, { kind: "text" }) : s;
var content;
var hasContent;
if (asText) {
content = fieldText();
hasContent = !!content;
} else if (s.kind === "images") {
content = imagesField ? imagesField.values() : [];
hasContent = content.length > 0;
} else {
content = readGroups();
hasContent = content.some(function (group) { return group.words.length; });
}
if (newName !== s.header) {
queueRename(s, newName);
}
if (!hasContent) {
return;
}
$save.prop("disabled", true);
// A Werger section holds any number of boxes, so an existing one is appended to rather than
// replaced; every other kind edits the single body it owns.
var appending = s.kind === "werger" && s.existing;
queueChange(
subsectionPlaceLabel(s, language, sectionType, $h3),
s.kind === "werger"
? "qutiyeke wergerê hate zêdekirin"
: "beşa «" + s.header + "» hate " + (s.existing ? "nûkirin" : "zêdekirin"),
function (pageContent) {
var body = content;
if (appending) {
var current = readPageSubsection(pageContent, language, sectionType, sectionOrdinal, effective);
var existingText = current ? $.trim(current.text || "") : "";
body = existingText ? existingText + "\n\n" + content : content;
}
return {
content: s.free
? updateFreeSection(pageContent, language, effective, body)
: (s.place
? writeLoosePlace(pageContent, language, sectionType, sectionOrdinal, effective,
loosePlaceBody(effective, body, language))
: (s.existing
? updatePageSubsection(pageContent, language, sectionType, sectionOrdinal, effective, body)
: insertPageSubsection(pageContent, language, sectionType, sectionOrdinal, effective, body))),
warnings: []
};
},
$save,
$status,
// Werger is a box the translation tools fill in afterwards, so there is nothing to draw for
// it. A new subsection gets a block of its own at the position it will land in; one already
// on the page is redrawn where it stands.
s.kind === "werger" ? null : function () {
// A free section is redrawn under its own heading, which is the element the panel hangs
// on.
if (s.free) {
var restoreFree = replaceFreeSectionInPlace($h3,
subsectionBodyWikitext(language, effective, content));
return function () {
if (restoreFree) {
restoreFree();
restoreFree = null;
}
};
}
// A "place" section has no heading to redraw between, so it is found in the DOM by what
// it renders as and replaced there.
if (s.place) {
var restoreLoose = replaceLoosePlaceInPlace($h3, effective,
loosePlacePreviewWikitext(effective, content, language));
return function () {
if (restoreLoose) {
restoreLoose();
restoreLoose = null;
}
};
}
if (!s.existing) {
var $preview = buildSubsectionPreviewDom($h3, s, content, language);
return function () {
$preview.remove();
};
}
var restore = replaceSubsectionInPlace($h3, effective.header,
subsectionBodyWikitext(language, effective, content));
return function () {
if (restore) {
restore();
restore = null;
}
};
},
{ key: s.free
? "section:" + s.header + "|" + s.free.ordinal
: "subsection:" + sectionType + "|" + sectionOrdinal + "|" + s.header }
);
});
$panel.empty().append($selectWrap, $nameWrap, $fieldWrap, $actions);
}
// Builds the "choose a part of speech" picker that reclassifies a section -- an unclassified
// "=== Mane ===" placeholder, or a "=== Biwêj ===" that turns out to be a real part of speech. Saved via
// convertSectionToPos(); lists every name from sectionNames except the one the section already is.
function buildConvertPosPanel($panel, $h3, language, sectionType, sectionOrdinal) {
var $select = $("<select class='dsb-convert-pos-select'></select>");
// Label-less, like the subsection picker's: it holds the "nothing chosen yet" value without showing
// up as a row, so the prompt lives in the input's placeholder instead.
$select.append($("<option value=''></option>"));
Object.keys(sectionNames).forEach(function (name) {
// The section's own part of speech is listed too, marked: picking it changes nothing about the
// heading and leaves the parameters below to be edited on their own.
$select.append($("<option></option>").val(name)
.text(name === sectionType ? name + " (ya niha)" : name));
});
var $selectWrap = makeSearchable($select, "Li cureyekî rêzimanî bigere");
// Opens on the section's own part of speech, so editing only its parameters needs no pick.
if (sectionNames[sectionType]) {
$select.val(sectionType).trigger("wiktku:refresh");
}
// The same card the entry wizard shows under its part-of-speech question -- gender, transitivity,
// the transliteration boxes and everything behind "Tercîhên pêşketî". Shared from
// wikt.edit/posparams.js, so reclassifying a section here offers exactly what creating one in the
// wizard does.
var card = posparams.build({ lang: language, pos: "" });
// Opened on the call the section already has, so this edits the current template rather than
// starting from a blank line. setContext() rebuilds the line whenever the part of speech moves,
// so the prefill is only the starting point.
var spec0 = posTemplateFor(sectionType);
card.setContext(language, spec0 ? spec0.name : "", /^Formeke /.test(sectionType));
fetchPageContent().then(function (pageContent) {
try {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var found = findPosTemplateLine(loc.lines, loc.posLineIndex, sectionType);
if (found.index !== -1 && ($select.val() || sectionType) === sectionType) {
card.setWikitext(found.text);
}
} catch (e) { /* an unreadable section just leaves the card as it was */ }
});
$select.on("change", function () {
var spec = posTemplateFor($select.val());
card.setContext(language, spec ? spec.name : "", /^Formeke /.test($select.val() || ""));
});
// "gh" on its own, or "<form>-<base>" once a structural form is picked -- start.js's own composition.
// convertSectionToPos() writes the template itself, so only what follows "{{pos|lang" is wanted
// from the card's line.
function posParams() {
var m = /^\{\{[^|}]*\|[^|}]*\|?([\s\S]*)\}\}$/.exec(card.wikitext());
return m ? m[1] : "";
}
var $save = $("<button type='button' class='dsb-save'></button>").text(util.msg.add);
var $cancel = $("<button type='button' class='dsb-cancel'></button>").text(util.msg.cancel);
var $status = $("<span class='dsb-status'></span>");
var $actions = $("<div class='dsb-form-actions'></div>").append($save, $cancel, $status);
// dsb.js owns the pencil and the panel's built state, so it does the closing.
$cancel.on("click", function () {
$panel.trigger("dsbCancel");
hideQueueIfIdle();
});
$save.on("click", function () {
var chosen = $select.val();
if (!chosen) {
$status.removeClass("dsb-status-ok").addClass("dsb-status-error").text("Pêşî cureyekî rêzimanî hilbijêre.");
return;
}
var params = posParams();
$save.prop("disabled", true);
$status.removeClass("dsb-status-error").text("");
queueChange(
langLabel(language),
"beşa «" + sectionLabel(sectionType, $h3) + "» bû «" + chosen + "»",
function (pageContent) {
return { content: convertSectionToPos(pageContent, language, sectionType, sectionOrdinal, chosen, params), warnings: [] };
},
$save,
$status,
function () {
// The heading keeps its text, which every other queued change finds the section by; a
// note under it names the new one. The headword line is drawn as the save will write it.
var $note = $("<div class='dsb-preview-inline dsb-preview-heading-note'></div>")
.text("→ " + chosen + (params ? " (" + params + ")" : "") + " — nû, hîn nehatiye qeydkirin");
headingOuter($h3).after($note);
var dropped = false;
var restoreHeadword = null;
fetchPageContent().then(function (pageContent) {
if (dropped) {
return;
}
var call;
try {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
call = posCallLine(findPosTemplateLine(loc.lines, loc.posLineIndex, sectionType),
language, chosen, params);
} catch (e) {
return;
}
restoreHeadword = replaceHeadwordInPlace($h3, call);
});
return function () {
dropped = true;
$note.remove();
if (restoreHeadword) {
restoreHeadword();
restoreHeadword = null;
}
};
},
// Runs after every other queued change: it renames the "Mane" heading those changes locate
// themselves by, so anything edited on this section has to be applied first.
{ runLast: true, key: "convert:" + sectionType + "|" + sectionOrdinal, pagePosition: headingPosition($h3) }
);
});
$panel.empty().append($selectWrap, card.$element, $actions);
}
// Attaches the "+ Beşek lê zêde bike" trigger right after a POS block's own last element.
// Puts the same pencil the definitions use on every existing "====" subsection heading the gadget knows how
// to edit, opening buildSubsectionPanel() pinned to that one subsection.
module.exports = {
buildAdvancedPanel: buildAdvancedPanel,
buildSubsectionPanel: buildSubsectionPanel,
buildConvertPosPanel: buildConvertPosPanel,
editExample: editExample,
clearQueue: clearQueue,
closeAllPanels: closeAllPanels,
openQueueBox: openQueueBox
};
}());
// </nowiki>
3alr8livjqzuf77yzd8o1k2lfqnvx5d
6531987
6531985
2026-09-17T16:49:48Z
Wikihez
7344
Renaming follows a multi-line {{kol3}} cure=; an untouched box queues nothing
6531987
javascript
text/javascript
// <nowiki>
/*
* Page: [[MediaWiki:Gadget-wikt.dsb-panels.js]]
*
* Everything DefSideBoxes shows once you click: the per-sense advanced panel, the subsection editors,
* the part-of-speech picker, the template dialogs, the preview and the queued save.
*
* Loaded on demand by wikt.dsb, never on a plain page view. It exports only what that half calls.
*/
(function () {
"use strict";
var sections = require("ext.gadget.wikt.core").sections;
var edit = require("ext.gadget.wikt.edit");
var cite = require("ext.gadget.wikt.cite");
var util = edit.util;
var posparams = edit.posparams;
var wt = edit.wt;
var startData = edit.data;
var langs = edit.langs;
var escapeRegExp = util.escapeRegExp;
// Local names for the shared wikitext layer, so every call site below reads as it always did.
var langHeaderSource = wt.langHeaderSource;
var canonicalTemplateKey = wt.canonicalTemplateKey;
var aliasPattern = wt.aliasPattern;
var splitLeadingCalls = wt.splitLeadingCalls;
var splitTemplateParams = wt.splitTemplateParams;
var parseTemplateCalls = wt.parseTemplateCalls;
var splitCallParams = wt.splitCallParams;
var buildTemplateCall = wt.buildTemplateCall;
var tdText = wt.tdText;
var fetchTemplateData = wt.fetchTemplateData;
var parseWordListCall = wt.parseWordListCall;
var parseWordListCalls = wt.parseWordListCalls;
var namedParam = wt.namedParam;
var withNamedParam = wt.withNamedParam;
var buildWordListCall = wt.buildWordListCall;
var parseBulletWordList = wt.parseBulletWordList;
var sectionNames = sections.sectionNames;
var MANE_SECTION = sections.MANE_SECTION;
var PAGE_SUBSECTIONS = sections.PAGE_SUBSECTIONS;
var headingOuter = sections.headingOuter;
var headingText = sections.headingText;
var isManeHeading = sections.isManeHeading;
var sectionTypeOf = sections.sectionTypeOf;
var sectionOrdinalWithinLanguage = sections.sectionOrdinalWithinLanguage;
var isFormOfWrapper = sections.isFormOfWrapper;
var eachLeafDefinition = sections.eachLeafDefinition;
var api = new mw.Api();
// metadata[lang].g, minus the grammatical-number codes start.js filters out with isNumberCode(), rendered
// the way its genderLabel() does ("m-p" -> "mê (pirjimar)").
// start.js asks transitivity and structural form as two questions and joins them as "<form>-<base>", so
// "gerguhêz" alone is "gh" and "xwerû" + "gerguhêz" is "x-gh".
// Template call convertSectionToPos() drops under a newly-chosen POS heading, e.g. "Hoker" -> "{{hoker|ku}}".
// A "Formeke X" (form-of) heading uses the unchanged base pos template. Doubles as the list of template
// calls a conversion may find under the OLD heading and replace, rather than leave stacked beside the new one.
// A section's template call. "Formeke Xê" uses X's own template with form=erê. Biwêj is recognised so a
// conversion can replace its {{biwêj}}, but never written: no new ones.
function posTemplateFor(header) {
var form = /^Formeke (.+)ê$/.exec(header || "");
if (form && startData.posChoices[form[1]] && sections.hasFormHeading(form[1])) {
return { name: form[1], params: ["form=erê"] };
}
var key = util.decapitalize(header || "");
return startData.posChoices[key] ? { name: key, params: [] } : null;
}
// convertSectionToPos() drops whichever of the tag's names is actually present.
var MANE_TAG_REGEX = wt.MANE_TAG_REGEX;
// The six per-sense groups start.js writes as "#: {{tpl|lang|word1|word2}}" sub-lines under a "#" definition.
// Array order controls where a new group line lands relative to existing ones. Every group lives behind the
// quick row's "Hevmane û Bêhtir" dropdown; only the gloss and Nimûne/jêgirtin links sit in the row itself.
var SENSE_GROUPS = startData.senseGroups;
// The column word-list template new sections are written with; the rest of wordlistTemplates are
// spellings the parser also accepts.
var WORDLIST_TEMPLATE = startData.wordlistTemplates[0];
// ---- {{werger-ser}} / {{werger-bin}}: one translation box ----
// The pair brackets the rows; the translation editor gadget writes the rows themselves onto the
// rendered page, so this only ever builds (and reads back) the opening call.
// Parameters, from [[Şablon:werger-ser]]: 1 = the sense shown in the box head, 2 = a note in
// parentheses after it, id = anchor override (defaults to 1), stûn = column count, firehî = column
// width (default 26em), bgcolor, valign (default top), align (default left).
var WERGER_NAMED = ["id", "stûn", "firehî", "bgcolor", "valign", "align"];
function buildWergerBlock(params) {
var parts = ["werger-ser"];
// Positional slots: 2 can only be reached past 1, so an empty 1 still has to be written.
if (params["1"] || params["2"]) {
parts.push(params["1"] || "");
}
if (params["2"]) {
parts.push(params["2"]);
}
WERGER_NAMED.forEach(function (key) {
if (params[key]) {
parts.push(key + "=" + params[key]);
}
});
return "{{" + parts.join("|") + "}}\n{{werger-bin}}";
}
// The opening call of the first block in `text`, as the same {1, 2, id, ...} object. Empty object when
// there is none, so a half-typed draft restores as an empty form rather than throwing.
function parseWergerBlock(text) {
var out = {};
var body = wergerSerBody(String(text || ""));
if (!body) {
return out;
}
var positional = 0;
splitTemplateArgs(body).forEach(function (arg) {
var eq = arg.indexOf("=");
var name = eq === -1 ? "" : $.trim(arg.slice(0, eq));
if (eq !== -1 && WERGER_NAMED.indexOf(name) !== -1) {
out[name] = $.trim(arg.slice(eq + 1));
} else {
out[String(++positional)] = $.trim(arg);
}
});
return out;
}
// Everything after "{{werger-ser|" up to the "}}" that closes it, or "" when there is no call or it
// takes no parameters. Scans for the matching close rather than the first one: a gloss can hold a
// template of its own, and a lazy regex would stop inside it.
function wergerSerBody(text) {
var m = /\{\{\s*werger-ser\s*(?=[|}])/.exec(text);
if (!m) {
return "";
}
var depth = 1;
var start = m.index + m[0].length;
for (var i = start; i < text.length - 1; i++) {
var two = text.substr(i, 2);
if (two === "{{") {
depth++; i++;
} else if (two === "}}") {
depth--; i++;
if (!depth) {
return text.charAt(start) === "|" ? text.slice(start + 1, i - 1) : "";
}
}
}
return "";
}
// Splits on "|" at brace/bracket depth 0, so a value containing a template or a link stays whole.
function splitTemplateArgs(body) {
var args = [];
var depth = 0;
var current = "";
for (var i = 0; i < body.length; i++) {
var two = body.substr(i, 2);
if (two === "{{" || two === "[[") {
depth++; current += two; i++; continue;
}
if (two === "}}" || two === "]]") {
depth--; current += two; i++; continue;
}
if (body[i] === "|" && depth <= 0) {
args.push(current); current = ""; continue;
}
current += body[i];
}
args.push(current);
return args;
}
// Canonical ku.wiktionary "====" subsection order, used only as insertPageSubsection()'s insertion rank
// (see subsectionRankForHeader()). Wider than PAGE_SUBSECTIONS: it also covers subsections the gadget
// cannot create but still has to order around.
var SUBSECTION_ORDER = startData.subsectionOrder;
// ---- searchable <select> ----------------------------------------------------------------------------
// Same widget start.js gets from its util.js wrapNativeSelect(): the native <select> stays as the value
// holder (so .val() and "change" handlers keep working) but is hidden behind a text input that filters an
// overlay list. Ported rather than required() because util.js is a file inside start.js's package module,
// which another gadget cannot reach; the template-row decoration start.js uses is left out.
// util.js's wrapNativeSelect(): the native <select> stays the value holder behind a filtering text input.
// It reads its own placeholder off data-placeholder, and its wiktku-* styles ship in start.css, so both
// gadgets get the identical widget instead of two lookalikes.
function makeSearchable($select, placeholder) {
$select.attr("data-placeholder", placeholder || "Lê bigere…");
return util.wrapNativeSelect($select);
}
// ---- small shared utilities ----
// The readable half of an mw.Api rejection.
var apiErrorMessage = util.apiErrorMessage;
// Thrown from inside an edit() transform to bail out with a specific message.
var SubmitAbort = util.SubmitAbort;
// A gloss is one field on the page: the sense, then {{îzah|…}} explaining it when there is one.
function joinGloss(mane, izah) {
return izah ? $.trim(mane + " {{îzah|" + izah + "}}") : $.trim(mane);
}
// The two halves are linked separately, never joined first: linkify leaves template calls alone by
// design, so anything inside {{îzah|…}} would never be looked at.
//
// A half is skipped when there is nothing in it worth looking up, and -- when onlyUnlinked is set --
// when it already carries a link. Someone who has linked a word by hand has made that half theirs, and
// a save must not go on linking the rest of it behind their back; "Oto-lînk bike" leaves the flag off
// and still links on demand.
function linkifyGlossParts(mane, izah, done, onlyUnlinked) {
function needsLinking(text) {
if (!$.trim(text) || !util.hasLinkableText(text)) {
return false;
}
return !(onlyUnlinked && text.indexOf("[[") !== -1);
}
function step(text, next) {
if (!needsLinking(text)) {
next(text, null);
return;
}
util.linkifyText(text, function (linked, error) {
// An empty answer would wipe what the user typed, so only real text is taken.
next(typeof linked === "string" && $.trim(linked) ? $.trim(linked) : text, error || null);
});
}
step(mane, function (linkedMane, maneError) {
step(izah, function (linkedIzah, izahError) {
done(linkedMane, linkedIzah, maneError || izahError);
});
});
}
// Runs transformFn(content) inside mw.Api().edit()'s own transform callback so one fetch covers both the
// edit-conflict check and the content, preventing a stale overwrite of someone else's edit.
//
// No badtoken retry here on purpose: edit() posts through postWithEditToken -> postWithToken, which
// already throws the cached token away and repeats the request once when the wiki says "badtoken".
// A second retry of our own would only double it.
function editPage(summary, transformFn, onSuccess, onError) {
api.edit(mw.config.get("wgPageName"), function (revision) {
return { text: transformFn(revision.content), summary: summary };
}).then(onSuccess, function (codeOrAbort, result) {
if (codeOrAbort instanceof SubmitAbort) {
onError(codeOrAbort.message);
} else {
onError(util.msg.saveFailed + apiErrorMessage(codeOrAbort, result));
}
});
}
// ---- rendered preview: wikitext through the parse API, so a preview shows what the templates
// actually produce rather than a plain list standing in for them ----
// Same text is usually asked for twice (open the preview, close it, open it again), so the promise is
// kept rather than the call repeated.
var renderCache = {};
/**
* Renders wikitext as this page would render it. Resolves with an HTML string.
*
* pst: 1 applies pre-save transform, so ~~~~ and subst: behave as they will once saved. The
* modulestyles/modules the parse reports are loaded before resolving: without them TemplateStyles is
* missing and the preview would show the right markup in the wrong clothes.
*/
function renderWikitext(wikitext) {
if (renderCache[wikitext]) {
return renderCache[wikitext];
}
renderCache[wikitext] = api.post({
action: "parse",
format: "json",
formatversion: 2,
title: mw.config.get("wgPageName"),
text: wikitext,
contentmodel: "wikitext",
prop: "text|modules|jsconfigvars",
pst: 1,
disablelimitreport: 1,
disableeditsection: 1,
disabletoc: 1
}).then(function (data) {
var parsed = (data && data.parse) || {};
if (parsed.modulestyles && parsed.modulestyles.length) {
mw.loader.load(parsed.modulestyles);
}
if (parsed.modules && parsed.modules.length) {
mw.loader.load(parsed.modules);
}
return parsed.text || "";
}, function () {
// A failed render must not be remembered, or the next attempt gets the rejection back.
delete renderCache[wikitext];
return $.Deferred().reject().promise();
});
return renderCache[wikitext];
}
/**
* Puts rendered wikitext into $target, replacing whatever placeholder is there.
*
* The site's own gadgets are told about it through the "wikipage.content" hook, the same way
* MediaWiki announces real content, so collapsible lists and the like behave in the preview too.
*/
// MediaWiki warns when the hook is fired on content that is not in the document, and a gadget that
// listens to it can do nothing useful with nodes nobody can see.
function announceContent($el) {
if ($el.closest("html").length) {
mw.hook("wikipage.content").fire($el);
}
}
function fillWithRendered($target, wikitext, onFail) {
return renderWikitext(wikitext).then(function (html) {
if (!$target.closest("body").length) {
return; // the preview was dropped while the parse was in flight
}
$target.empty().append($("<div class='mw-parser-output'></div>").html(html));
announceContent($target);
}, function () {
if (onFail) onFail();
});
}
/**
* Puts a rendered sense in place of the one on the page, keeping this gadget's own furniture (the
* pencil's toolbar, the quick row, any open panel) where it is. The page's own nodes are detached
* rather than dropped, so the returned function can put them straight back when the change is
* cancelled or superseded.
*
* Nothing labels the result: the change is shown by the sense simply reading the way it will read,
* and the queue box is what says nothing is saved yet.
*/
function replaceSenseInPlace($li, item) {
var html = item.html;
var $anchor = $li.children(".dsb-toolbar").last();
// class/id/data-* off the parsed <li>, so a queued maneid shows its pill here exactly as it will
// once saved. The old values are kept for the restore below.
var node = $li.get(0);
var before = {};
$.each(node.attributes, function (_, attr) {
before[attr.name] = attr.value;
});
$.each(item.attrs, function (name, value) {
if (name !== "class") {
$li.attr(name, value);
}
});
var addedClasses = (item.attrs["class"] || "").split(/\s+/).filter(function (c) {
return c && !$li.hasClass(c);
});
$li.addClass(addedClasses.join(" "));
// add-examples hangs its own <ul> off the sense and opens its form as that <ul>'s next sibling.
// Detaching those leaves the form to open into a tree nobody can see -- "+ Nimune/jegirtin" takes
// the click and shows nothing -- so they stay where they are. Only the rows already in that <ul>
// are hidden, since the preview draws the examples itself; the tool's own title line stays.
var $keep = $li.children("ul, .add-example-form");
var $hiddenRows = $keep.filter("ul").children("li").filter(function () {
return !$(this).find(".add-example-form-title").length;
});
$hiddenRows.addClass("dsb-preview-hidden");
var originals = $li.contents().filter(function () {
return !$(this).is(".dsb-toolbar, .dsb-quick-row, .dsb-subsection-panel, .dsb-preview-block, .dsb-sense-preview");
}).not($keep).get();
// Where the sense's own nodes sat, so cancelling puts them back there rather than above the quick
// row -- the row belongs between the sense and its examples.
var backAfter = originals.length ? originals[0].previousSibling : null;
var $rendered = $("<span class='dsb-sense-preview'></span>").html(html);
if ($anchor.length) {
$anchor.after($rendered);
} else {
$li.prepend($rendered);
}
$(originals).detach();
announceContent($rendered);
// The id pill is drawn by the same code a page load uses, so a queued maneid looks like a saved
// one rather than like something this gadget invented.
if (window.wikt && wikt.showIDs && wikt.showIDs.process) {
wikt.showIDs.process($li);
}
// dsb.js puts the example pencils back on the redrawn examples. The originals keep theirs.
$li.triggerHandler("dsbSenseRedrawn");
return function () {
$hiddenRows.removeClass("dsb-preview-hidden");
$li.removeClass(addedClasses.join(" "));
$.each(item.attrs, function (name) {
if (name === "class") {
return;
}
if (Object.prototype.hasOwnProperty.call(before, name)) {
$li.attr(name, before[name]);
} else {
$li.removeAttr(name);
}
});
$li.children(".senseid-displayed, .etymid-displayed, .etymonid-displayed").remove();
if (backAfter && backAfter.parentNode === node) {
$(backAfter).after(originals);
} else if ($anchor.length) {
$anchor.after(originals);
} else {
$li.prepend(originals);
}
$rendered.remove();
};
}
/**
* The <li> out of a parsed "#" block, and the attributes it carries.
*
* The parse wraps it in its own <ol>, which must not reach the page: the sense already has a number
* from the list it lives in. A leading {{maneid}} also makes the parser emit an empty
* "li.mw-empty-elt" ahead of the real one, so the first <li> is not necessarily the sense.
*
* The attributes matter as much as the content: {{maneid|ku|x}} renders as class="senseid"
* data-id="x" on the <li> itself, which is what wikt.showids draws its pill from.
*/
function parsedSenseItem(html, depth) {
var $item = $("<div></div>").html(html).find("ol > li").filter(function () {
return !$(this).hasClass("mw-empty-elt");
}).first();
// A sub-sense parsed on its own comes back one <ol> deeper per "#": "##" is a list inside a list.
// Descend as far as the sense's own depth, or its number would be drawn inside a second list --
// "2." with a "1." under it.
for (var level = 1; level < (depth || 1) && $item.length; level++) {
var $inner = $item.children("ol").children("li").filter(function () {
return !$(this).hasClass("mw-empty-elt");
}).first();
if (!$inner.length) {
break;
}
$item = $inner;
}
if (!$item.length) {
return { html: html, attrs: {} };
}
var attrs = {};
$.each($item.get(0).attributes, function (_, attr) {
attrs[attr.name] = attr.value;
});
return { html: $item.html(), attrs: attrs };
}
// This gadget's own save queue + floating widget, separate from the shared Gadget-Editor queue.
// Each entry keeps its own transformFn so saveQueueNow() can re-run all of them against one fresh fetch.
var dsbQueue = {
// [{key, summary, transformFn, runLast, removePreview}]; transformFn: baseContent -> {content, warnings}.
// removePreview lives on the entry so replacing one entry takes its own live DOM preview with it.
entries: []
};
var $queueBox = null;
function dropPreview(entry) {
if (typeof entry.removePreview === "function") {
try {
entry.removePreview();
} catch (e) { /* a preview failing to clean up should never block the queue itself */ }
entry.removePreview = null;
}
}
function clearAllPreviews() {
dsbQueue.entries.forEach(dropPreview);
}
function clearQueue() {
clearAllPreviews();
dsbQueue.entries = [];
if ($queueBox) {
$queueBox.$list.empty();
$queueBox.$box.hide();
}
}
// Closes every editor UI on the page. Each open box's own "x" clears its getter, restores its picker
// link and updates the counter, so those are triggered rather than repeating that work here; hiding a
// row alone would leave its fields populated and the next pencil click would reopen them half-filled.
function closeAllPanels() {
$(".dsb-open-boxes .dsb-box-remove").trigger("click");
$(".dsb-quick-note").hide().text("");
$(".dsb-more-menu").hide();
$(".dsb-subsection-panel, .dsb-convert-pos-panel").hide();
// dsb.js owns each pencil's pinned state; tell it rather than hiding behind its back, or the next
// click on the pencil would only toggle "pinned" off and appear to do nothing.
$(".dsb-quick-row").trigger("dsbCancel");
}
function ensureQueueBox() {
if ($queueBox) {
return $queueBox;
}
var $box = $("<div class='dsb-queue-box'></div>").hide();
var $close = $("<a href='#' class='dsb-queue-close' title='Veşêre'>×</a>");
var $title = $("<div class='dsb-queue-title'></div>").text("Guhartinên li benda qeydkirinê");
var $list = $("<ul class='dsb-queue-list'></ul>");
var $empty = $("<div class='dsb-queue-empty'></div>").text(
"Guhartinên te dê li vir werin rêzkirin. Heta ku tu pêl «Guhartinan qeyd bike» nekî, "
+ "tiştek li ser rûpelê nayê weşandin."
);
var $status = $("<div class='dsb-queue-status'></div>");
var $save = $("<button type='button' class='dsb-queue-save'>Guhartinan qeyd bike</button>");
var $cancel = $("<button type='button' class='dsb-queue-cancel'></button>").text(util.msg.cancel);
var $actions = $("<div class='dsb-queue-actions'></div>").append($save, $cancel);
$close.on("click", function (e) {
e.preventDefault();
$box.hide();
});
$cancel.on("click", function () {
clearQueue();
closeAllPanels();
});
$save.on("click", function () {
saveQueueNow();
});
$box.append($close, $title, $empty, $list, $actions, $status);
$(document.body).append($box);
$queueBox = { $box: $box, $list: $list, $empty: $empty, $status: $status, $save: $save, $cancel: $cancel };
return $queueBox;
}
// Takes one change out of the queue and puts the page back as it was before it, its in-place preview
// included. The queue is the only record of what "Guhartinan qeyd bike" will write, so every line in it
// has to be removable on its own -- closing the panel that made a change does not unmake it.
// Each preview draws the page the whole queue would save, so one entry leaving changes what all the
// others should show. They are taken down and drawn again from what is left.
function redrawPreviews() {
dsbQueue.entries.forEach(function (entry) {
if (typeof entry.previewFn !== "function") {
return;
}
dropPreview(entry);
try {
var removePreview = entry.previewFn();
if (typeof removePreview === "function") {
entry.removePreview = removePreview;
}
} catch (e) { /* a broken preview never stops the queue */ }
});
}
function dropQueueEntry(entry) {
var at = dsbQueue.entries.indexOf(entry);
if (at === -1) {
return;
}
dropPreview(entry);
dsbQueue.entries.splice(at, 1);
redrawPreviews();
renderQueueBox();
hideQueueIfIdle();
}
function renderQueueBox() {
var box = ensureQueueBox();
box.$list.empty();
dsbQueue.entries.forEach(function (entry) {
var $drop = $("<button type='button' class='dsb-queue-drop'>×</button>")
.attr("title", "Vê guhartinê ji rêzê derxe");
$drop.on("click", function () {
dropQueueEntry(entry);
});
box.$list.append($("<li></li>").text(entry.summary).append($drop));
});
box.$empty.toggle(!dsbQueue.entries.length);
box.$status.removeClass("dsb-status-ok dsb-status-error").text("");
box.$save.prop("disabled", !dsbQueue.entries.length);
box.$box.show();
}
// Shown the moment a panel opens, empty, so it is clear before anything is typed that a change waits
// for "Guhartinan qeyd bike" rather than publishing itself.
function openQueueBox() {
renderQueueBox();
}
// Nothing queued and no form left open means nothing to save, so the box goes away rather than
// lingering on an empty state.
function hideQueueIfIdle() {
if (dsbQueue.entries.length || !$queueBox) {
return;
}
if ($(".dsb-quick-row:visible, .dsb-subsection-panel:visible, .dsb-convert-pos-panel:visible").length) {
return;
}
$queueBox.$box.hide();
}
// Asks core's postEdit module to show its toast on the next load (dsb.js loads it there), then reloads.
// Loaded here rather than declared as a dependency, so a reload happens even if it fails.
function reloadWithPostEditNotice() {
mw.loader.using("mediawiki.action.view.postEdit").then(function (require) {
require("mediawiki.action.view.postEdit").fireHookOnPageReload("saved");
}).always(function () {
location.reload();
});
}
// A transform returns either the new page text or {content, warnings}; both shapes are accepted.
// One that returns neither stops the save, named, rather than writing a broken page.
function transformedContent(entry, pageContent) {
var result = entry.transformFn(pageContent);
var next = typeof result === "string" ? result : (result && result.content);
if (typeof next !== "string") {
throw new SubmitAbort("Guhartin nehate sepandin: " + entry.summary);
}
return next;
}
// Queue order, except that runLast entries move to the end: converting "=== Mane ===" into a real POS
// heading renames the very heading every other queued change locates itself by.
// Those run from the bottom of the page up: a rename changes the ordinals of the sections after it
// ("Mane 2" becomes "Mane 1"), never of the ones before.
function orderedEntries() {
var normal = [];
var last = [];
dsbQueue.entries.forEach(function (entry) {
(entry.runLast ? last : normal).push(entry);
});
last.sort(function (a, b) {
return b.pagePosition - a.pagePosition;
});
return normal.concat(last);
}
/**
* The pending page as previews and example lookups need it: every queued transform, in save order,
* except the runLast section renames. Those callers find their section under the heading it has on
* the page now, which a queued "Mane" -> "Navdêr" would take away, and a rename leaves the senses as
* they are. An entry that cannot apply is skipped -- the save reports that properly, and a preview
* must not shout.
*/
function queuedContent(pageContent) {
var content = pageContent;
orderedEntries().filter(function (entry) {
return !entry.runLast;
}).forEach(function (entry) {
try {
content = transformedContent(entry, content);
} catch (e) {
// Left out of the preview; the save reports it properly. Named, so a change that fails to
// draw is distinguishable from one with nothing to draw.
mw.log.warn("wikt.dsb: pêşdîtin bêyî vê guhartinê hate xêzkirin — " + entry.summary, e);
}
});
return content;
}
/**
* Redraws one sense where it sits, from the page the queue would save -- so a queued gloss edit, a
* queued example and everything else pending on that sense are shown together, rendered by the parser
* rather than described in a box. Returns the queue's own "take the preview down" function.
*
* One preview per sense: a second queued change takes the first drawing down and draws the queue.
*/
function previewSense($li, language, sectionType, sectionOrdinal, definitionLevel) {
var cancelled = false;
var restore = null;
function undo() {
cancelled = true;
if (restore) {
restore();
restore = null;
}
if ($li.data("dsbPreview") === undo) {
$li.removeData("dsbPreview");
}
}
var previous = $li.data("dsbPreview");
if (typeof previous === "function") {
previous();
}
$li.data("dsbPreview", undo);
fetchPageContent().then(function (pageContent) {
if (cancelled) {
return;
}
var wikitext;
try {
wikitext = senseBlockWikitext(queuedContent(pageContent), language, sectionType,
sectionOrdinal, definitionLevel);
} catch (e) {
return;
}
renderWikitext(wikitext).then(function (html) {
if (cancelled || $li.data("dsbPreview") !== undo || !$li.closest("body").length) {
return;
}
restore = replaceSenseInPlace($li, parsedSenseItem(html, definitionLevel.length - 1));
});
});
return undo;
}
// The real save: re-runs every queued transformFn, in order, against one freshly-fetched revision and
// posts the result as a single edit with a combined summary.
function saveQueueNow() {
if (!dsbQueue.entries.length) {
return;
}
var box = ensureQueueBox();
box.$save.prop("disabled", true);
box.$cancel.prop("disabled", true);
box.$status.removeClass("dsb-status-ok dsb-status-error").text("Tê qeydkirin…");
var entries = orderedEntries();
// Grouped by place, in the order the places were first queued: several changes to one section read
// as "Kurmancî/Navdêr 2: a, b, c" rather than repeating the section name in front of each of them.
var order = [];
var byPlace = {};
entries.forEach(function (entry) {
if (!Object.prototype.hasOwnProperty.call(byPlace, entry.place)) {
byPlace[entry.place] = [];
order.push(entry.place);
}
byPlace[entry.place].push(entry.action);
});
var combinedSummary = order.map(function (place) {
var actions = byPlace[place].join(", ");
return place ? place + ": " + actions : actions;
}).join("; ") + " (DefSideBoxes)";
editPage(
combinedSummary,
function (pageContent) {
var content = pageContent;
entries.forEach(function (entry) {
content = transformedContent(entry, content);
});
return content;
},
function (data) {
forgetPageContent();
clearAllPreviews();
dsbQueue.entries = [];
box.$list.empty();
box.$status
.removeClass("dsb-status-error")
.addClass("dsb-status-ok")
.text("✓ Hemû guhartin hatin qeydkirin.");
box.$save.prop("disabled", true);
box.$cancel.prop("disabled", false);
// A real edit reloads the page and gets MediaWiki's own "Guhartina te hat weşandin." toast, the
// one a normal save shows. A null edit changed nothing, so the page stays as it is.
if (data && data.newrevid) {
reloadWithPostEditNotice();
}
},
function (message) {
box.$save.prop("disabled", false);
box.$cancel.prop("disabled", false);
box.$status.removeClass("dsb-status-ok").addClass("dsb-status-error").text(message);
}
);
}
// "Kurmancî/Navdêr" -- where a queued change lands, for its summary. The language name comes from the
// shipped list; an unknown code is left as the code itself.
// "Kurmancî/Navdêr", or "Kurmancî/Navdêr 2" where the language has more than one section of that
// type. Without the number a summary on a page with two Navdêr sections named neither of them.
// Numbered only when it disambiguates: on the usual page "Navdêr 1" would just be noise.
function placeLabel(language, sectionType, $h3) {
return langLabel(language) + "/" + sectionLabel(sectionType, $h3);
}
// Bilêvkirin sits above the parts of speech and belongs to the whole language, so naming the section
// it was opened from would say something untrue about what the edit touches.
function subsectionPlaceLabel(subsection, language, sectionType, $h3) {
if (subsection && subsection.free) {
return langLabel(language) + "/" + subsection.header;
}
return subsection && subsection.place === "pron"
? langLabel(language)
: placeLabel(language, sectionType, $h3);
}
function sectionLabel(sectionType, $h3) {
if ($h3 && $h3.length && sections.sectionTypeCountWithinLanguage($h3) > 1) {
return sectionType + " " + sections.sectionOrdinalWithinLanguage($h3);
}
return sectionType;
}
// Capitalised because it opens the summary; Kurdish otherwise writes language names lower-case.
function langLabel(language) {
var names = langs.names();
return util.capitalize(names[language] || language);
}
// The parts of a sense the form actually changed, so the summary lists them instead of saying nothing.
function senseChangeLabels(changes) {
var parts = [];
var changed = changes.changed || {};
if (changes.gloss) {
parts.push("mane");
}
if (changes.newSense) {
parts.push("maneyeke nû");
}
SENSE_GROUPS.forEach(function (group) {
if (changed["word:" + group.key]) {
parts.push(group.label.toLowerCase());
}
});
if (changed.maneid) {
parts.push("id");
}
if (changed.etiket) {
parts.push("etîket");
}
if (changed.kategori) {
parts.push("kategorî");
}
return parts;
}
// Queues transformFn(baseWikitext) -> {content, warnings} for the real save to run later against fresh
// content (see saveQueueNow()); nothing here touches the page yet. Also runs previewFn(), which drops a
// small "not saved yet" preview into the live DOM at the change's own spot on the page. Shared by every
// panel that queues a wikitext change: buildAdvancedPanel, buildSubsectionPanel, buildConvertPosPanel.
// place is what the change is being made to ("Kurmancî/Navdêr 2"), action is what is being done to it.
// Kept apart rather than pre-joined so saveQueueNow() can name a section once and list everything
// queued against it after it, instead of repeating the place before every item.
function queueChange(place, action, transformFn, $save, $status, previewFn, opts) {
opts = opts || {};
var entry = {
key: opts.key || null,
place: place || "",
action: action,
summary: place ? place + ": " + action : action,
transformFn: transformFn,
runLast: !!opts.runLast,
pagePosition: opts.pagePosition || 0,
// Kept, not just run: redrawPreviews() calls it again when another entry leaves the queue.
previewFn: typeof previewFn === "function" ? previewFn : null,
removePreview: null
};
// Queuing the same target twice replaces the pending change rather than stacking a second one --
// two "+Têkildar" entries would both run, and the second would abort on "Ev beş jixwe heye".
var superseded = false;
if (entry.key) {
for (var i = dsbQueue.entries.length - 1; i >= 0; i--) {
if (dsbQueue.entries[i].key === entry.key) {
dropPreview(dsbQueue.entries[i]);
dsbQueue.entries.splice(i, 1);
superseded = true;
}
}
}
dsbQueue.entries.push(entry);
$status.removeClass("dsb-status-error").addClass("dsb-status-ok").text(
"✓ li rêza guhartinan hate zêdekirin — pêl «Guhartinan qeyd bike» bike da ku bi cih bibe."
);
$save.prop("disabled", false);
if (previewFn) {
try {
var removePreview = previewFn();
if (typeof removePreview === "function") {
entry.removePreview = removePreview;
}
} catch (e) { /* a broken preview should never stop the change from being queued */ }
}
// What the superseded entry drew is gone from the queue, so whatever else is drawn from it is stale.
if (superseded) {
dsbQueue.entries.forEach(function (other) {
if (other !== entry) {
dropPreview(other);
}
});
redrawPreviews();
}
renderQueueBox();
}
// Runs text through the ext.gadget.linkify gadget ($.kuLinkify) and calls back with the linkified result.
// Passes the text through unchanged if the gadget can't be loaded, so it never blocks a save.
var linkifyText = util.linkifyText;
// ---- wikitext location ----
//
// One copy for both editing gadgets, in wikt.edit: add-examples writes into the same places.
var langSectionRange = wt.langSectionRange;
var locateSection = wt.locateSection;
var findDefinitionLine = wt.findDefinitionLine;
/**
* The wikitext of one sense's own block: its "#" line plus everything nested under it -- the "#:" word
* lines, the "#*" citations and any sub-senses -- stopping at the next line of the same depth that
* starts a new sense. Parsed on its own that block renders as exactly the <li> the page shows.
*/
function senseBlockWikitext(pageContent, language, sectionType, sectionOrdinal, definitionLevel) {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var defLine = findDefinitionLine(lines, loc.posLineIndex, definitionLevel);
var depth = definitionLevel.length - 1;
var out = [lines[defLine]];
for (var i = defLine + 1; i < lines.length; i++) {
var m = /^(#+)([:*]?)/.exec(lines[i]);
if (!m || m[1].length < depth) {
break;
}
// Same depth and no ":" or "*" after the hashes is the next sense, not part of this one.
if (m[1].length === depth && !m[2]) {
break;
}
out.push(lines[i]);
}
return out.join("\n");
}
// Inserts or extends a "#: {{tplKey|lang|word1|word2}}" sub-line for one sense (start.js's wordLine()
// format). If a line for this tplKey already exists under the definition, missing words are appended to
// it; otherwise a new line lands after the last existing group line, always before any "#*" citation lines.
function insertSenseWords(pageContent, language, sectionType, sectionOrdinal, definitionLevel, tplKey, words, opts) {
opts = opts || {};
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var defLine = findDefinitionLine(lines, loc.posLineIndex, definitionLevel);
var marker = new Array(definitionLevel.length).join("#"); // definitionLevel.length - 1 "#" characters
// Group 1 is the spelling the page uses, group 2 the parameters: an existing {{hevwate}} stays
// {{hevwate}} rather than being silently renamed to {{hevmane}}.
var tplLineRegex = new RegExp("^" + marker + ":\\s*\\{\\{\\s*(" + aliasPattern(tplKey) + ")\\s*\\|([^}]*)\\}\\}\\s*$");
var groupKeysPattern = SENSE_GROUPS.map(function (g) { return aliasPattern(g.key); }).join("|");
var groupLineRegex = new RegExp("^" + marker + ":\\s*\\{\\{\\s*(" + groupKeysPattern + ")\\s*\\|");
var anyBlockLineRegex = new RegExp("^" + marker + "[:*]");
var lastGroupLine = -1;
for (var i = defLine + 1; i < lines.length; i++) {
if (!anyBlockLineRegex.test(lines[i])) {
break;
}
var tplMatch = tplLineRegex.exec(lines[i]);
if (tplMatch) {
var foundKey = tplMatch[1];
if (opts.replace) {
// Editing an existing list: an empty result means the user cleared it, so drop the line.
if (words.length) {
lines[i] = marker + ": {{" + foundKey + "|" + language + "|" + words.join("|") + "}}";
} else {
lines.splice(i, 1);
}
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
var params = tplMatch[2].split("|"); // [lang, word1, word2, ...]
var existingWords = params.slice(1);
words.forEach(function (w) {
if (existingWords.indexOf(w) === -1) {
existingWords.push(w);
}
});
lines[i] = marker + ": {{" + foundKey + "|" + language + "|" + existingWords.join("|") + "}}";
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
if (groupLineRegex.test(lines[i])) {
lastGroupLine = i;
}
}
var insertAt = lastGroupLine >= 0 ? lastGroupLine + 1 : defLine + 1;
lines.splice(insertAt, 0, marker + ": {{" + tplKey + "|" + language + "|" + words.join("|") + "}}");
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
// Canonical left-to-right order of the small templates that can prefix a definition line's gloss text:
// maneid, then kategori (K), then etîket (e).
var LEADING_TPL_ORDER = startData.leadingTplOrder;
// True for a leading call the advanced panel edits in a box of its own -- those are kept out of the
// gloss box so the same template is never editable in two places at once. Redirect spellings count:
// {{f}} and {{etîket}} are {{e}}.
function hasOwnFieldBox(key) {
return LEADING_TPL_ORDER.indexOf(canonicalTemplateKey(key)) !== -1;
}
// Inserts, extends, or (with opts.replace) overwrites one of the small templates that prefix a definition
// line's gloss text -- {{maneid|lang|id}}, {{K|lang|kat1|...}} or {{e|lang|etîket1|...}}. Under
// opts.replace an empty values list removes the call rather than writing an empty one.
function insertLeadingSenseTpl(pageContent, language, sectionType, sectionOrdinal, definitionLevel, tplKey, values, opts) {
opts = opts || {};
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var defLine = findDefinitionLine(lines, loc.posLineIndex, definitionLevel);
var marker = /^#+/.exec(lines[defLine])[0];
var afterMarker = lines[defLine].slice(marker.length).replace(/^\s+/, "");
// Collecting the whole leading-call run means an existing tplKey call is found and merged wherever it
// already sits, not just when it happens to be first.
var split = splitLeadingCalls(afterMarker);
var calls = split.calls;
var rest = split.rest;
var existing = null;
calls.forEach(function (c) {
if (canonicalTemplateKey(c.key) === tplKey) {
existing = c;
}
});
if (existing) {
if (opts.replace) {
// Editing an existing call: an empty result means the user cleared it, so drop the call.
if (values.length) {
existing.params = [language].concat(values);
} else {
calls = calls.filter(function (c) { return c !== existing; });
}
} else {
var existingVals = existing.params.slice(1);
values.forEach(function (v) {
if (existingVals.indexOf(v) === -1) {
existingVals.push(v);
}
});
existing.params = [language].concat(existingVals);
}
} else if (!values.length) {
return pageContent; // nothing to add, and nothing there to clear
} else {
// Not present yet -- insert right before the first existing call that outranks tplKey in
// LEADING_TPL_ORDER (an unrecognized call always outranks everything), or at the end of the run.
var myRank = LEADING_TPL_ORDER.indexOf(tplKey);
var insertAt = calls.length;
for (var i = 0; i < calls.length; i++) {
var otherRank = LEADING_TPL_ORDER.indexOf(canonicalTemplateKey(calls[i].key));
if (otherRank === -1 || otherRank > myRank) {
insertAt = i;
break;
}
}
calls.splice(insertAt, 0, { key: tplKey, params: [language].concat(values) });
}
var rebuilt = calls.map(function (c) {
return "{{" + c.key + "|" + c.params.join("|") + "}}";
}).join(" ");
lines[defLine] = marker + " " + (rebuilt ? rebuilt + " " : "") + rest;
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
// Replaces just the gloss text of a definition line, leaving any leading template calls untouched --
// the save half of "Maneyê biguhêre" (edit sense).
function updateSenseGloss(pageContent, language, sectionType, sectionOrdinal, definitionLevel, newGloss) {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var defLine = findDefinitionLine(lines, loc.posLineIndex, definitionLevel);
var marker = /^#+/.exec(lines[defLine])[0];
var afterMarker = lines[defLine].slice(marker.length).replace(/^\s+/, "");
// The box holds everything except the calls with their own field, so those are put back in front.
// Dropping "{{mane?}}" here is what clears start.js's placeholder once a real gloss is written.
var kept = splitLeadingCalls(afterMarker).calls.filter(function (c) {
return hasOwnFieldBox(c.key);
});
var leadingText = kept.map(function (c) {
return "{{" + c.key + "|" + c.params.join("|") + "}}";
}).join(" ");
lines[defLine] = marker + " " + (leadingText ? leadingText + " " : "") + newGloss;
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
// Deletes one sense: its definition line plus everything indented under it (its "#:" groups, "#*"
// citations, any sub-senses). Refuses to empty a section, since a POS heading with no senses is worse
// than the sense the user wanted gone.
// A new sense goes directly below the one whose pencil asked for it -- after the lines that sense owns
// (its "#:" examples, "#*" citations and "##" sub-senses), so it reads as the next sense rather than
// landing at the foot of the section. It takes the same marker depth as its neighbour, so asking from
// a "##" sub-sense adds a sibling sub-sense.
function insertNewSense(pageContent, language, sectionType, sectionOrdinal, definitionLevel, text) {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var defLine = findDefinitionLine(lines, loc.posLineIndex, definitionLevel);
var marker = /^#+/.exec(lines[defLine])[0];
var end = defLine + 1;
var ownedRegex = new RegExp("^" + marker + "[#:*]");
while (end < lines.length && ownedRegex.test(lines[end])) {
end++;
}
lines.splice(end, 0, marker + " " + text);
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
function removeSense(pageContent, language, sectionType, sectionOrdinal, definitionLevel) {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var defLine = findDefinitionLine(lines, loc.posLineIndex, definitionLevel);
var marker = /^#+/.exec(lines[defLine])[0];
var end = defLine + 1;
var ownedRegex = new RegExp("^" + marker + "[#:*]");
while (end < lines.length && ownedRegex.test(lines[end])) {
end++;
}
// Top-level senses of this section only -- "##" sub-senses go with their parent either way.
if (definitionLevel.length === 2) {
var siblings = 0;
for (var i = loc.posLineIndex + 1; i < lines.length && !/^==/.test(lines[i]); i++) {
if (/^#[^*#:]/.test(lines[i])) {
siblings++;
}
}
if (siblings < 2) {
throw new SubmitAbort("Ev maneya dawî ya vê beşê ye. Ji bo rakirinê divê tu tevahiya beşê rakî.");
}
}
lines.splice(defLine, end - defLine);
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
// Fetches the page's current wikitext and pulls out just this sense's gloss text to prefill the "Maneyê
// biguhêre" form. A plain read, not part of the hardened save path -- the actual save still re-fetches
// fresh content, so only the prefilled text itself could be a little stale.
// Current contents of one existing subsection, for prefilling the edit form.
// This page's current wikitext.
// One fetch for the whole queue: every preview reads the same revision, and asking per preview meant
// downloading the page again for each queued change. Dropped after a save, when the page has moved on.
var pageContentPromise = null;
function forgetPageContent() {
pageContentPromise = null;
}
function fetchPageContent() {
if (pageContentPromise) {
return pageContentPromise;
}
pageContentPromise = rawPageContent();
// A failed read must not be remembered as the answer.
pageContentPromise.fail(forgetPageContent);
return pageContentPromise;
}
function rawPageContent() {
return api.get({
action: "query",
prop: "revisions",
rvprop: "content",
titles: mw.config.get("wgPageName"),
formatversion: 2
}).then(function (data) {
var page = data.query && data.query.pages && data.query.pages[0];
var content = page && !page.missing && page.revisions && page.revisions[0] && page.revisions[0].content;
if (!content) {
throw new SubmitAbort("Nikare muhtewaya rûpelê bixwîne.");
}
return content;
});
}
function fetchPageSubsection(language, sectionType, sectionOrdinal, subsection) {
return fetchPageContent().then(function (content) {
return readPageSubsection(content, language, sectionType, sectionOrdinal, subsection);
});
}
function fetchSenseFields(language, sectionType, sectionOrdinal, definitionLevel) {
return fetchPageContent().then(function (content) {
return readSenseFields(content, language, sectionType, sectionOrdinal, definitionLevel);
});
}
// Everything the advanced panel can edit about one sense, read off its definition line and the "#:" lines
// under it: the raw gloss (templates included), the leading maneid/K/e values, and each word-list group.
function readSenseFields(pageContent, language, sectionType, sectionOrdinal, definitionLevel) {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var defLine = findDefinitionLine(lines, loc.posLineIndex, definitionLevel);
var defMarker = /^#+/.exec(lines[defLine])[0];
var afterMarker = lines[defLine].slice(defMarker.length).replace(/^\s+/, "");
var leading = {};
splitLeadingCalls(afterMarker).calls.forEach(function (c) {
leading[canonicalTemplateKey(c.key)] = c.params.slice(1);
});
// Only start.js's "{{mane?|lang}}" placeholder is hidden; every other template stays visible so it can
// be edited by hand in the gloss box.
// The gloss box shows the line minus the calls that have their own box ({{maneid}}, {{K}}, {{e}} and
// their redirects) and minus start.js's "{{mane?}}" placeholder. Everything else -- a {{g}} link, a
// {{guharto}} -- stays visible and editable there.
var split = splitLeadingCalls(afterMarker);
var glossCalls = split.calls.filter(function (c) {
return !hasOwnFieldBox(c.key) && canonicalTemplateKey(c.key) !== "mane?";
});
var glossLead = glossCalls.map(function (c) {
return "{{" + c.key + "|" + c.params.join("|") + "}}";
}).join(" ");
var gloss = $.trim((glossLead ? glossLead + " " : "") + split.rest);
var marker = new Array(definitionLevel.length).join("#");
var groupKeysPattern = SENSE_GROUPS.map(function (g) { return aliasPattern(g.key); }).join("|");
var groupLineRegex = new RegExp("^" + marker + ":\\s*\\{\\{\\s*(" + groupKeysPattern + ")\\s*\\|([^}]*)\\}\\}\\s*$");
var anyBlockLineRegex = new RegExp("^" + marker + "[:*]");
var words = {};
for (var i = defLine + 1; i < lines.length; i++) {
if (!anyBlockLineRegex.test(lines[i])) {
break;
}
var m = groupLineRegex.exec(lines[i]);
if (m) {
words[canonicalTemplateKey(m[1])] = m[2].split("|").slice(1);
}
}
return {
gloss: gloss,
maneid: leading.maneid ? leading.maneid[0] : "",
etiket: leading.e || [],
kategori: leading.K || [],
words: words
};
}
// Finds a POS block's body range (up to, but not including, the next heading of level <= 3), and the
// header text + line index of any "====" subsections it already contains.
// depth is the part-of-speech heading's own ("===" on most entries, "====" where the senses are
// grouped under numbered etymologies). Its block runs to the next heading at that depth or shallower,
// and its subsections are the headings one deeper.
function posBlockBounds(lines, posLineIndex, depth) {
depth = depth || 3;
var blockEnd = wt.headingAtOrAbove(depth);
var subsectionHeading = wt.headingAtDepth(depth + 1);
var end = lines.length;
var existingHeaders = [];
for (var i = posLineIndex + 1; i < lines.length; i++) {
if (blockEnd.test(lines[i])) {
end = i;
break;
}
var sub = subsectionHeading.exec(lines[i]);
if (sub) {
existingHeaders.push({ header: $.trim(sub[1]), lineIndex: i });
}
}
return { end: end, existingHeaders: existingHeaders, depth: depth };
}
// Looks up a header's rank in SUBSECTION_ORDER. -1 for a header that isn't one of ours; such a header is
// never used as an insertion anchor.
function subsectionRankForHeader(header) {
return SUBSECTION_ORDER.indexOf(header);
}
// Body line range of one "====" subsection inside a POS block: everything after its heading up to the next
// "====" heading, or the end of the block. Null if the block has no such subsection.
function subsectionBodyRange(bounds, header) {
var headers = bounds.existingHeaders;
for (var i = 0; i < headers.length; i++) {
if (headers[i].header === header) {
return {
start: headers[i].lineIndex + 1,
end: i + 1 < headers.length ? headers[i + 1].lineIndex : bounds.end
};
}
}
return null;
}
// ---- template-parameter editing -------------------------------------------------------------------
// A gloss that is nothing but one template call gets a labelled box per parameter instead of a raw
// wikitext line, the way VisualEditor's template dialog works.
// The single template call a gloss is made of, or null when the line is anything less tidy. {{g|...}} is a
// link inside the gloss rather than the gloss itself, so it neither disqualifies the line nor gets a form.
function glossTemplateTarget(gloss) {
var parsed = parseTemplateCalls(gloss || "");
if (parsed.otherContent) {
return null;
}
var editable = parsed.calls.filter(function (c) {
return canonicalTemplateKey(c.name) !== "g";
});
return editable.length === 1 ? editable[0] : null;
}
// ---- the three places that carry no "====" heading of their own ----------------------------------
// Bilêvkirin is a "===" section above the POS blocks, images sit at the top of a POS block, and the
// h-bnr hatnote goes above the first language heading. Each is located here and then read, written or
// inserted through the same three lines of splice, so nothing below needs to know about them.
var IMAGE_LINE = /^\[\[\s*(?:Wêne|File|Image|Dosya)\s*:/i;
/**
* {lines, langSectionIndex, exists, start, end, insertAt} for a subsection carrying a "place", or null
* for an ordinary one. start..end is the body as it stands; insertAt is where a missing one goes.
*/
function locateLoosePlace(pageContent, language, sectionType, sectionOrdinal, subsection) {
var place = subsection.place;
if (!place) {
return null;
}
var i, end;
if (place === "page-top") {
// Above every language section, so the whole page is the search space. The call is found under
// any of its names, and its own name is carried back so a save never renames it.
var pageLines = pageContent.split("\n");
var call = wt.findHbnrCall(pageContent);
if (call) {
return { lines: pageLines, langSectionIndex: 0, exists: true, call: call,
start: call.lineIndex, end: call.lineIndex + 1, insertAt: call.lineIndex };
}
return { lines: pageLines, langSectionIndex: 0, exists: false, start: 0, end: 0, insertAt: 0 };
}
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
if (place === "pron") {
// One per language, not one per part of speech, so it is looked for above the POS heading.
for (i = 0; i < loc.posLineIndex; i++) {
var h3 = /^===\s*([^=]+?)\s*===/.exec(lines[i]);
if (h3 && $.trim(h3[1]) === subsection.header) {
end = i + 1;
while (end < lines.length && !/^==/.test(lines[end])) {
end++;
}
return { lines: lines, langSectionIndex: loc.langSectionIndex, exists: true,
start: i + 1, end: end, insertAt: i };
}
}
// Straight after the language heading, which puts it before every POS block.
return { lines: lines, langSectionIndex: loc.langSectionIndex, exists: false,
start: 1, end: 1, insertAt: 1 };
}
if (place === "pos-top") {
// Between the conjugation line and the headword template -- where the wizard writes them.
var at = loc.posLineIndex + 1;
while (at < lines.length && /^\{\{\s*ku-tewîn/.test($.trim(lines[at]))) {
at++;
}
var from = at;
while (at < lines.length && IMAGE_LINE.test($.trim(lines[at]))) {
at++;
}
return { lines: lines, langSectionIndex: loc.langSectionIndex, exists: at > from,
start: from, end: at, insertAt: from };
}
return null;
}
// The wikitext a "place" section's content becomes. A word list there is one template call, not a
// {{kol3}} block, so the words are joined into it -- and any ⊕ modifier a row picked up is dropped,
// since a page name is all the call can carry.
function loosePlaceBody(subsection, content, language) {
if (subsection.kind === "images") {
return $.grep($.map(content || [], function (image) {
return wt.buildImageLine({
file: image.file,
caption: wt.captionForLang(image.caption, language),
alt: image.alt,
size: image.size,
extra: image.extra
});
}), Boolean).join("\n");
}
return content;
}
// The words a "place" word list holds, flattened out of its single group. Any ⊕ modifier a row picked
// up is dropped: a page name is all the call can carry.
function loosePlaceWords(content) {
var words = [];
$.each(content || [], function (_, group) {
$.each(group.words || [], function (_, raw) {
var plain = $.trim(util.parseWordValue(raw).word);
if (plain && $.inArray(plain, words) === -1) {
words.push(plain);
}
});
});
return words;
}
// Writes one of those places, inserting it when it is not there yet and replacing it when it is. Only
// Bilêvkirin carries a heading; the other two are bare lines.
function writeLoosePlace(pageContent, language, sectionType, sectionOrdinal, subsection, content) {
var at = locateLoosePlace(pageContent, language, sectionType, sectionOrdinal, subsection);
if (!at) {
throw new SubmitAbort("Cih nehate dîtin: " + subsection.header);
}
// A call naming a subpage -- "{{h-bnr/-ane}}" -- holds a list shared by every page transcluding
// it. Writing here would change this page alone and desync the rest, so the edit is refused and
// the template that actually owns the list is named instead.
if (at.call && at.call.subpage) {
throw new SubmitAbort("Ev rûpel «{{" + at.call.name + "}}» bi kar tîne — lîste li " +
"[[Şablon:" + at.call.subpage + "]] ye, lê wê zêde bike, ne li vê rûpelê.");
}
// A word list here is one template call, so it is built where the existing call's own name is
// known -- editing "{{bnr-h|...}}" must not turn it into "{{h-bnr|...}}".
var body = subsection.kind === "wordlist"
? wt.buildHbnrCall(loosePlaceWords(content), at.call && at.call.name)
: $.trim(content || "");
// A "===" section is separated from what is above and below it by a blank line, the way the rest
// of an entry is written. The other two places are bare lines inside a block and take none.
var heading = subsection.place === "pron";
function blank(index) {
return $.trim(at.lines[index] || "") === "";
}
if (at.exists) {
if (body) {
var replacement = [body];
// end is the next "==" heading; the blank line before it is inside the range being replaced.
if (heading && /^==/.test(at.lines[at.end] || "")) {
replacement.push("");
}
at.lines.splice.apply(at.lines, [at.start, at.end - at.start].concat(replacement));
} else {
// Emptying the box removes the section rather than leaving an empty heading behind.
at.lines.splice(at.start, at.end - at.start);
if (heading) {
at.lines.splice(at.insertAt, 1);
// The blank line that separated it goes with it, rather than doubling up.
if (blank(at.insertAt) && blank(at.insertAt - 1)) {
at.lines.splice(at.insertAt, 1);
}
}
}
} else {
if (!body) {
throw new SubmitAbort("Tiştek nehate nivîsandin.");
}
var block = heading ? ["=== " + subsection.header + " ===", body] : [body];
if (heading) {
if (!blank(at.insertAt - 1)) {
block.unshift("");
}
if (!blank(at.insertAt)) {
block.push("");
}
}
at.lines.splice.apply(at.lines, [at.insertAt, 0].concat(block));
}
return pageContent.slice(0, at.langSectionIndex) + at.lines.join("\n");
}
// What a section body holds: its {{kol3}} groups, the plain bullet list that stands in for them, or
// the raw wikitext when it is neither. A free section is read this way before its kind is known.
function readSubsectionBody(body, subsection) {
if (subsection.kind === "wordlist" || subsection.free) {
var calls = parseWordListCalls(body);
if (calls.length) {
return {
kind: "wordlist",
groups: calls.map(function (call) {
return { sernav: namedParam(call.named, "sernav"), words: call.words };
})
};
}
var bullets = parseBulletWordList(body);
if (bullets) {
// Editable as a word list; saving rewrites the bullets as {{kol3}}.
return { kind: "wordlist", groups: [{ sernav: "", words: bullets }], fromBullets: true };
}
}
return { kind: "text", text: $.trim(body) };
}
// The wikitext a section body becomes after an edit. A wordlist keeps its own template name, language
// and named params ("cure=Jê", ...) and only has its positional words replaced; a text one is
// replaced whole.
function rewriteSubsectionBody(body, language, subsection, content) {
if (subsection.kind !== "wordlist") {
// Replacing the body wholesale drops that separator, so put it back.
return content + (/\n\s*$/.test(body) ? "\n" : "");
}
var calls = parseWordListCalls(body);
if (calls.length) {
// Each call is rewritten where it stands, so text between and around them survives untouched.
// Last to first, so the offsets of the earlier ones stay valid as we go.
var replacement = body;
for (var gi = calls.length - 1; gi >= 0; gi--) {
var call = calls[gi];
var group = content[gi];
// A group emptied in the form drops its call entirely rather than leaving "{{kol3|ku}}".
var rebuilt = group && group.words.length
? buildWordListCall(call.name, call.lang, withNamedParam(call.named, "sernav", group.sernav), group.words)
: "";
replacement = replacement.slice(0, call.start) + rebuilt + replacement.slice(call.end);
}
// Groups the form added beyond the ones already there go after the last of them.
var added = content.slice(calls.length).filter(function (group) {
return group.words.length;
}).map(function (group) {
return buildWordListCall(WORDLIST_TEMPLATE, language,
withNamedParam(["cure=" + subsection.header], "sernav", group.sernav), group.words);
});
if (added.length) {
replacement = replacement.replace(/\s*$/, "") + "\n\n" + added.join("\n\n") + "\n";
}
return replacement.replace(/\n{3,}/g, "\n\n"); // gaps left by any dropped call
}
if (parseBulletWordList(body)) {
// A plain bullet list is replaced wholesale by a real {{kol3}} on its first edit.
return content.map(function (group) {
return buildWordListCall(WORDLIST_TEMPLATE, language,
withNamedParam(["cure=" + subsection.header], "sernav", group.sernav), group.words);
}).join("\n\n") + (/\n\s*$/.test(body) ? "\n" : "");
}
throw new SubmitAbort("Nikare lîsteya vê beşê bixwîne: " + subsection.header);
}
// ---- sections that are not subsections of a part of speech ----
// A "=== Etîmolojî ===" that the parts of speech hang under, a "=== Çavkanî ===" at the foot of a
// language, a heading nobody planned for. Located by its own text, and edited as the wikitext under
// it -- or as a {{kol3}} list when that is what it holds.
function freeSectionBody(pageContent, language, subsection) {
var loc = wt.locateHeadingByText(pageContent, language, subsection.header, subsection.free.ordinal);
return { loc: loc, body: loc.lines.slice(loc.bodyStart, loc.bodyEnd).join("\n") };
}
function readFreeSection(pageContent, language, subsection) {
var at = freeSectionBody(pageContent, language, subsection);
return readSubsectionBody(at.body, subsection);
}
function updateFreeSection(pageContent, language, subsection, content) {
var at = freeSectionBody(pageContent, language, subsection);
at.loc.lines.splice(at.loc.bodyStart, at.loc.bodyEnd - at.loc.bodyStart,
rewriteSubsectionBody(at.body, language, subsection, content));
return pageContent.slice(0, at.loc.langSectionIndex) + at.loc.lines.join("\n");
}
// ---- removing and renaming a heading ----
// A subsection's heading line and the end of what it owns, as {loc, heading, end, depth, bounds}. A
// "====" subsection runs to the next one, taking deeper headings with it. A free section owns only its
// own text: what sits under it belongs to the parts of speech (see removeSubsection).
function headingSpan(pageContent, language, sectionType, sectionOrdinal, subsection) {
if (subsection.free) {
var free = wt.locateHeadingByText(pageContent, language, subsection.header, subsection.free.ordinal);
return { loc: free, heading: free.headingLineIndex, end: free.bodyEnd, depth: free.depth };
}
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var bounds = posBlockBounds(loc.lines, loc.posLineIndex, loc.depth);
var range = subsectionBodyRange(bounds, subsection.header);
if (!range) {
throw new SubmitAbort(util.msg.sectionMissing + subsection.header);
}
return { loc: loc, heading: range.start - 1, end: range.end, depth: bounds.depth + 1, bounds: bounds };
}
// A free section with deeper headings under it is refused: removing it would leave them under the
// section before, and taking them along would remove whole parts of speech.
function hasDeeperHeadingAfter(lines, index, depth) {
var m = /^(=+)[^=]/.exec(lines[index] || "");
return !!m && m[1].length > depth;
}
function removeSubsection(pageContent, language, sectionType, sectionOrdinal, subsection) {
var span = headingSpan(pageContent, language, sectionType, sectionOrdinal, subsection);
var lines = span.loc.lines;
if (subsection.free && hasDeeperHeadingAfter(lines, span.end, span.depth)) {
throw new SubmitAbort("Di bin «" + subsection.header + "» de beşên din hene; pêşî wan jê bibe.");
}
lines.splice(span.heading, span.end - span.heading);
// The blank line that separated it goes with it, rather than doubling up.
if (span.heading > 0 && !$.trim(lines[span.heading - 1]) && !$.trim(lines[span.heading] || "x")) {
lines.splice(span.heading, 1);
}
return pageContent.slice(0, span.loc.langSectionIndex) + lines.join("\n");
}
function renameSubsection(pageContent, language, sectionType, sectionOrdinal, subsection, newHeader) {
var span = headingSpan(pageContent, language, sectionType, sectionOrdinal, subsection);
var lines = span.loc.lines;
if (span.bounds && span.bounds.existingHeaders.some(function (h) {
return h.header === newHeader;
})) {
throw new SubmitAbort("Ev beş jixwe heye: " + newHeader);
}
var marks = wt.headingMarks(span.depth);
lines[span.heading] = marks + " " + newHeader + " " + marks;
// A {{kol3}} names its section in cure=, which follows the heading. Read across lines: a call is
// often written one parameter per line.
var cure = new RegExp("(\\|\\s*cure\\s*=\\s*)" + escapeRegExp(subsection.header) + "(\\s*[|}])", "g");
var body = lines.slice(span.heading + 1, span.end).join("\n").replace(cure, "$1" + newHeader + "$2");
lines.splice.apply(lines, [span.heading + 1, span.end - span.heading - 1].concat(body.split("\n")));
return pageContent.slice(0, span.loc.langSectionIndex) + lines.join("\n");
}
// Current contents of an existing "====" subsection. A wordlist one whose template can't be parsed comes
// back as {kind: "text"} so the caller can still edit it as raw wikitext instead of refusing outright.
function readPageSubsection(pageContent, language, sectionType, sectionOrdinal, subsection) {
if (subsection.free) {
return readFreeSection(pageContent, language, subsection);
}
var loose = locateLoosePlace(pageContent, language, sectionType, sectionOrdinal, subsection);
if (loose) {
if (!loose.exists) {
return null;
}
if (loose.call) {
return { kind: "wordlist", groups: [{ sernav: "", words: loose.call.words }] };
}
var looseBody = $.trim(loose.lines.slice(loose.start, loose.end).join("\n"));
if (subsection.kind === "images") {
return {
kind: "images",
images: $.grep($.map(looseBody.split("\n"), wt.parseImageLine), Boolean)
};
}
if (subsection.kind !== "wordlist") {
return { kind: "text", text: looseBody };
}
// One template call rather than a {{kol3}} block: its positional parameters are the words.
var looseCall = parseTemplateCalls(looseBody).calls[0];
return { kind: "wordlist", groups: [{ sernav: "", words: (looseCall && looseCall.params) || [] }] };
}
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var bounds = posBlockBounds(loc.lines, loc.posLineIndex, loc.depth);
var range = subsectionBodyRange(bounds, subsection.header);
if (!range) {
return null;
}
return readSubsectionBody(loc.lines.slice(range.start, range.end).join("\n"), subsection);
}
// Rewrites an existing "====" subsection in place. A wordlist one keeps its own template name, language and
// named params ("cure=Jê", ...) and only has its positional words replaced; a text one is replaced whole.
function updatePageSubsection(pageContent, language, sectionType, sectionOrdinal, subsection, content) {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var bounds = posBlockBounds(lines, loc.posLineIndex, loc.depth);
var range = subsectionBodyRange(bounds, subsection.header);
if (!range) {
throw new SubmitAbort(util.msg.sectionMissing + subsection.header);
}
var body = lines.slice(range.start, range.end).join("\n");
lines.splice(range.start, range.end - range.start,
rewriteSubsectionBody(body, language, subsection, content));
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
// Inserts a new "==== Header ====" subsection into its SUBSECTION_ORDER position within a POS block, right
// before the first ranked subsection that outranks it, or at the end if none does. Either a
// {{kol3|...}} word list or a plain paragraph, matching start.js's own subsection formats.
function insertPageSubsection(pageContent, language, sectionType, sectionOrdinal, subsection, content) {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var bounds = posBlockBounds(lines, loc.posLineIndex, loc.depth);
var alreadyThere = false;
bounds.existingHeaders.forEach(function (h) {
if (h.header === subsection.header) {
alreadyThere = true;
}
});
if (alreadyThere) {
throw new SubmitAbort("Ev beş jixwe heye: " + subsection.header);
}
var body = subsection.kind === "wordlist"
? content.map(function (group) {
return buildWordListCall(WORDLIST_TEMPLATE, language,
withNamedParam(["cure=" + subsection.header], "sernav", group.sernav), group.words);
}).join("\n\n")
: content;
var myRank = subsectionRankForHeader(subsection.header);
var headers = bounds.existingHeaders;
var insertAt = bounds.end;
for (var i = 0; i < headers.length; i++) {
var otherRank = subsectionRankForHeader(headers[i].header);
if (otherRank !== -1 && otherRank > myRank) {
insertAt = headers[i].lineIndex;
break;
}
}
// Exactly one blank line on each side: the line before the insertion point is normally already blank
// (it separated the previous subsection from the next heading), so only add one where there isn't one.
var block = [];
if (insertAt > 0 && $.trim(lines[insertAt - 1]) !== "") {
block.push("");
}
var marks = wt.headingMarks(bounds.depth + 1);
block.push(marks + " " + subsection.header + " " + marks, body);
if (insertAt < lines.length) {
block.push("");
}
lines.splice.apply(lines, [insertAt, 0].concat(block));
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
// ---- converting an unclassified "=== Mane ===" placeholder heading into a real POS heading ----
// Some pages carry a heading literally named "Mane" (optionally "Mane 2" etc.) instead of a real
// part-of-speech name, tagged right below with {{cure?|<lang>}} (ku.wiktionary's "part of speech not yet
// determined" maintenance flag). It is a section key of its own (sections.MANE_SECTION), so the shared
// locator finds it like any other heading; only the rewriting below is particular to it.
// Renames the "=== Mane ===" placeholder to the chosen POS heading, drops the maintenance tag line right
// after it if present (MANE_TAG_REGEX -- cure? or any of its known aliases), and inserts the matching
// "{{<pos>|<lang>}}" call in its place (skipped when the section has no template of its own).
// Other content under the heading is left untouched.
/**
* Where a section's own headword template sits, as {index, text}, or {index: -1} when it has none.
*
* Not always the line under the heading: a "{{ku-tewîn-lk}}" line and any number of "[[Wêne:...]]"
* images can come first. Everything down to the first definition line is searched, and index doubles
* as the slot a new call belongs in when there is no old one.
*/
function findPosTemplateLine(lines, headingLine, sectionType) {
var oldSpec = posTemplateFor(sectionType);
var oldTemplate = oldSpec ? oldSpec.name : (sectionType === "Biwêj" ? "biwêj" : null);
var oldTemplateRegex = oldTemplate &&
new RegExp("^\\{\\{\\s*" + escapeRegExp(oldTemplate) + "\\s*[|}]");
var at = headingLine + 1;
while (at < lines.length && !/^#/.test(lines[at]) && !/^==/.test(lines[at])) {
if (MANE_TAG_REGEX.test(lines[at]) ||
(oldTemplateRegex && oldTemplateRegex.test(lines[at]))) {
return { index: at, text: lines[at], end: at };
}
at++;
}
return { index: -1, text: "", end: at };
}
// Named parameters merged in order, each written once: a later value replaces an earlier one of the
// same name. Positional values are kept as they come.
function mergeCallParams() {
var out = [];
var seen = {};
$.each(Array.prototype.slice.call(arguments), function (_, list) {
$.each(list || [], function (__, param) {
var m = /^\s*([^=|]+?)\s*=([\s\S]*)$/.exec(param);
if (!m) {
out.push(param);
return;
}
// "z" and "c" are one name to Modul:rêziman, so the form's "c=m" meets the page's "z=".
var key = wt.canonicalParamName(m[1]);
if (!Object.prototype.hasOwnProperty.call(seen, key)) {
seen[key] = out.length;
out.push(param);
return;
}
// The page's own spelling stays when it carried a value; an empty one gives way entirely.
var earlier = /^\s*([^=|]+?)\s*=([\s\S]*)$/.exec(out[seen[key]]);
// "?" asks for a value, so it never replaces one the page already has.
if ($.trim(earlier[2]) && $.trim(m[2]) === "?") {
return;
}
out[seen[key]] =$.trim(earlier[2]) ? earlier[1] + "=" + m[2] : param;
});
});
return out;
}
// What the call on the page already says, minus its language. Carried over only when the new section
// uses that same template -- "z=m" survives Navdêr becoming Formeke navdêrê, while a verb's "c=h-gh"
// has no meaning on a noun and is left behind.
function keptCallParams(line, templateName) {
var call = parseTemplateCalls($.trim(line || "")).calls[0];
if (!call || canonicalTemplateKey(call.name) !== canonicalTemplateKey(templateName)) {
return [];
}
return call.params.slice(1).filter(function (param) {
return /=/.test(param);
});
}
// The headword call a converted section gets, or "" when the new section has none. What the old call
// (found, from findPosTemplateLine) said keeps its place, then the section's own defaults, then what the
// form was filled in with -- each name written once, the last value winning, so a converted call reads
// as small a change from the old one as it can.
function posCallLine(found, language, newSectionType, posParams) {
var spec = posTemplateFor(newSectionType);
if (!spec) {
return "";
}
var kept = found.index !== -1 ? keptCallParams(found.text, spec.name) : [];
// A lemma is not an inflected form, so the old section's form=erê stays behind.
if (spec.params.indexOf("form=erê") === -1) {
kept = kept.filter(function (param) {
return !/^\s*form\s*=\s*erê\s*$/.test(param);
});
}
var params = mergeCallParams(kept, spec.params, posParams ? posParams.split("|") : []);
return "{{" + spec.name + "|" + language + (params.length ? "|" + params.join("|") : "") + "}}";
}
function convertSectionToPos(pageContent, language, sectionType, sectionOrdinal, newSectionType, posParams) {
if (!sectionNames[newSectionType]) {
throw new SubmitAbort("Navê cureyê rêzimanî yê hilbijartî nediyar e.");
}
// locateSection() already handles the "Mane" placeholder as a section type of its own.
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var headingLine = loc.posLineIndex;
var depth = loc.depth;
// A section straight under a numbered group heading takes that heading's place: the group heading
// held nothing but its gloss.
var group = depth > 3 ? wt.numberedGroupAbove(lines, headingLine, depth) : -1;
if (group !== -1) {
lines.splice(group, headingLine - group);
headingLine = group;
depth--;
}
// At the heading's own depth. Written without a number: the renumbering pass below decides every
// number in this language section at once, so one carried over from the old name would just be
// overwritten.
var marks = wt.headingMarks(depth);
lines[headingLine] = marks + " " + newSectionType + " " + marks;
// The line under the heading is either the old section's own template call ("{{biwêj|ku}}") or, for a
// Mane placeholder, the "needs a POS" maintenance tag. Both are replaced by the new POS's call.
// The new call takes the old one's own place -- or, when there was none, the slot just above the
// definitions, which is where the wizard writes it.
var found = findPosTemplateLine(lines, headingLine, sectionType);
var insertAt = found.index !== -1 ? found.index : found.end;
if (found.index !== -1) {
lines.splice(found.index, 1);
}
var call = posCallLine(found, language, newSectionType, posParams);
if (call) {
lines.splice(insertAt, 0, call);
}
// Renaming a section can make it repeat one the page already had, and can equally leave a name that
// is no longer repeated. Both are settled by renumbering the whole language section: a "Rengdêr"
// beside a new "Rengdêr" becomes "Rengdêr 1" and "Rengdêr 2", and a number left behind by a name
// that is now unique is dropped.
var body = lines.join("\n");
var nextLang = body.indexOf("\n== ", 1);
var end = nextLang === -1 ? body.length : nextLang;
body = wt.numberRepeatedPosHeadings(body.slice(0, end)) + body.slice(end);
return pageContent.slice(0, loc.langSectionIndex) + body;
}
// ---- "guhartina pêşketî" (advanced editing): combine several independent per-sense edits into ONE save ----
// Threads pageContent through the same pure functions every single-field form uses, applying only the
// pieces the caller actually filled in. A piece that conflicts with the page (maneid already set, a
// subsection that's already there) is skipped with a warning rather than failing the whole save. Throws
// SubmitAbort if nothing was filled in, so this can never turn into a no-op save.
function applyAdvancedChanges(pageContent, language, sectionType, sectionOrdinal, definitionLevel, changes) {
var content = pageContent;
var warnings = [];
var appliedAnything = false;
if (changes.gloss) {
content = updateSenseGloss(content, language, sectionType, sectionOrdinal, definitionLevel, changes.gloss);
appliedAnything = true;
}
if (changes.newSense) {
content = insertNewSense(content, language, sectionType, sectionOrdinal, definitionLevel, changes.newSense);
appliedAnything = true;
}
// Every box below is prefilled with what the sense already has, so a save replaces that value rather
// than merging into it -- and clearing a box removes the template. changes.changed marks the boxes
// whose contents actually differ from the page, so an opened-but-untouched box writes nothing.
var replace = { replace: true };
if (changes.changed.maneid) {
content = insertLeadingSenseTpl(content, language, sectionType, sectionOrdinal, definitionLevel, "maneid", changes.maneid ? [changes.maneid] : [], replace);
appliedAnything = true;
}
if (changes.changed.etiket) {
content = insertLeadingSenseTpl(content, language, sectionType, sectionOrdinal, definitionLevel, "e", changes.etiket, replace);
appliedAnything = true;
}
if (changes.changed.kategori) {
content = insertLeadingSenseTpl(content, language, sectionType, sectionOrdinal, definitionLevel, "K", changes.kategori, replace);
appliedAnything = true;
}
SENSE_GROUPS.forEach(function (group) {
if (changes.changed["word:" + group.key]) {
content = insertSenseWords(content, language, sectionType, sectionOrdinal, definitionLevel, group.key, changes.words[group.key], replace);
appliedAnything = true;
}
});
if (!appliedAnything) {
throw new SubmitAbort("Tiştek nehat guhartin.");
}
return { content: content, warnings: warnings };
}
// ---- per-definition UI: a small always-visible "+" toggle (no hover, so it works on mobile/touch too) ----
// A growing list of word rows. The row itself is util.js's, shared with the wizard; this owns the list:
// typing in the last row appends a fresh empty one, so there is always somewhere to type.
var WORDLIST_COLUMN_THRESHOLD = 6;
function buildWordListField(placeholder, opts) {
opts = opts || {};
var $list = $("<ol class='dsb-wordlist'></ol>");
// One column is a long scroll once a list gets past a handful of words. The class only asks for
// two; the stylesheet still gives one on a narrow screen. Two columns inside the panel's usual
// 520px would leave each word about 250px, so the panel drops its width cap for as long as any
// list in it is in two columns -- checked across the whole panel, since a "Herwiha binêre" holds
// several groups and one of them going wide settles it for all.
function updateColumns() {
$list.toggleClass("dsb-wordlist-2col",
$list.children("li").length > WORDLIST_COLUMN_THRESHOLD);
var $panel = $list.closest(".dsb-subsection-panel");
$panel.toggleClass("dsb-panel-wide", !!$panel.find(".dsb-wordlist-2col").length);
}
function makeRow() {
var row = util.makeModifierRow({
prefix: "dsb-",
placeholder: placeholder,
modTitle: util.msg.addModifier,
removeTitle: util.msg.removeWord,
removeModTitle: util.msg.removeModifier,
modifiers: opts.modifiers,
// "bixe qutiyan" puts each comma-separated word in a row of its own, right after the row it
// was typed in, and the caret follows to the last of them. One word per row is also one
// word per template parameter, which both {{hevmane}} and {{kol3}} read the same way --
// a comma left inside a parameter would not be.
onSplit: function (rest) {
var $after = row.$li;
var last = null;
rest.forEach(function (raw) {
var extra = makeRow();
$after.after(extra.$li);
$after = extra.$li;
if (raw) extra.setRaw(raw);
last = extra;
});
// Always an empty row at the end to type the next word into.
var $tail = $list.children("li").last();
if ($.trim($tail.data("row").getRaw())) {
last = makeRow();
$list.append(last.$li);
}
updateColumns();
if (last) last.focus();
},
// raw === null means the row removed itself, so the count has changed either way.
onChange: function (raw) {
if (raw !== null) {
var isLast = row.$li.is($list.children("li").last());
if (isLast && raw) $list.append(makeRow().$li);
}
updateColumns();
}
});
row.$li.data("row", row);
return row;
}
$list.append(makeRow().$li);
return {
$element: $list,
values: function () {
var vals = [];
$list.children("li").each(function () {
var raw = $(this).data("row").getRaw();
if (raw) vals.push(raw);
});
return vals;
},
// Replaces the whole list, keeping one empty trailing row to type into.
setValues: function (raws) {
$list.empty();
raws.forEach(function (raw) {
var row = makeRow();
$list.append(row.$li);
row.setRaw(raw);
});
$list.append(makeRow().$li);
updateColumns();
},
focus: function () {
$list.children("li").first().data("row").focus();
}
};
}
// Plain removable-tag field, used for Etîket and Kategorî (simple value lists with no per-word modifiers).
// Enter or "," commits the current text as a tag; Backspace on an empty input removes the last tag;
// values() also commits whatever is still typed but not yet confirmed.
function buildTagField(placeholder) {
var $input = $("<input type='text' class='dsb-input dsb-tag-input'>").attr("placeholder", placeholder);
var $element = $("<span class='dsb-tagfield'></span>").append($input);
function currentValues() {
var vals = [];
$element.children(".dsb-tag").each(function () {
vals.push($(this).data("value"));
});
return vals;
}
function addTag(val) {
val = $.trim(val || "");
if (!val || currentValues().indexOf(val) !== -1) {
return;
}
var $tag = $("<span class='dsb-tag'></span>").text(val).data("value", val);
var $remove = $("<button type='button' class='dsb-tag-remove' title='Jê bibe'>×</button>");
$remove.on("click", function () {
$tag.remove();
$input.trigger("focus");
});
$tag.append($remove);
$tag.insertBefore($input);
}
$input.on("keydown", function (e) {
if (e.which === 13 || e.which === 188) { // Enter or comma
e.preventDefault();
addTag($input.val());
$input.val("");
} else if (e.which === 8 && !$input.val()) {
$element.children(".dsb-tag").last().remove();
}
});
$input.on("blur", function () {
addTag($input.val());
$input.val("");
});
return {
$element: $element,
// Replaces every tag, for prefilling from what the sense already has.
setValues: function (vals) {
$element.children(".dsb-tag").remove();
vals.forEach(function (v) {
addTag(v);
});
$input.val("");
},
values: function () {
addTag($input.val());
$input.val("");
return currentValues();
},
focus: function () {
$input.trigger("focus");
}
};
}
// The wizard's own etymology templates, straight out of data-main.json's "etymTpls" -- the same list start.js's
// insert menu offers, so the two can never drift apart. CODE/SOURCELANG are its placeholders.
// The wizard's own template lists, straight out of data-main.json, so both gadgets offer the same
// templates in the same order. A pronunciation template names the languages it is for; one that names
// none is offered everywhere.
function templateChoices(pool, language) {
var defs = startData[pool] || {};
return Object.keys(defs).filter(function (name) {
var langs = defs[name].langs;
return !langs || !langs.length || $.inArray(language, langs) !== -1;
}).map(function (name) {
return { name: name, desc: defs[name].desc, snippet: defs[name].snippet };
});
}
// One template call inside the surface: shown as its own wikitext, but atomic -- contenteditable="false"
// keeps the caret out of it, so it can only be changed through the dialog.
function makeTemplateChip(wikitext) {
// The × is its own element rather than the chip's only way out: on a phone there is no reliable
// backspace onto a contenteditable="false" span, so without it the dialog is the only way to take
// a call off the line. readChipSurface() reads chips from data-wikitext, so the extra child does
// not reach the saved text.
var $remove = $("<span class='dsb-tpl-chip-remove' contenteditable='false'>×</span>")
.attr("title", "Vê şablonê jê bibe");
return $("<span class='dsb-tpl-chip' contenteditable='false' role='button' tabindex='0'></span>")
.attr("data-wikitext", wikitext)
.attr("title", "Bitikîne da ku parametreyan biguhêrî")
.text(wikitext)
.append($remove);
}
// wikitext -> surface DOM: template calls become chips, everything else stays plain editable text.
function renderChipSurface($surface, wikitext) {
var text = wikitext || "";
var calls = parseTemplateCalls(text).calls;
var pos = 0;
$surface.empty();
calls.forEach(function (call) {
if (call.start > pos) {
$surface.append(document.createTextNode(text.slice(pos, call.start)));
}
$surface.append(makeTemplateChip(text.slice(call.start, call.end)));
pos = call.end;
});
if (pos < text.length) {
$surface.append(document.createTextNode(text.slice(pos)));
}
}
// surface DOM -> wikitext. Chips give back their stored call; anything the browser inserted while typing
// (a <br>, a wrapper <div>) degrades to its text, so the result is always plain wikitext.
function readChipSurface(node) {
var out = "";
$(node).contents().each(function () {
if (this.nodeType === 3) {
out += this.nodeValue;
return;
}
var $el = $(this);
if ($el.hasClass("dsb-tpl-chip")) {
out += $el.attr("data-wikitext");
return;
}
if (this.nodeName === "BR") {
out += "\n";
return;
}
if (this.nodeName === "DIV" || this.nodeName === "P") {
out += "\n" + readChipSurface(this);
return;
}
out += readChipSurface(this);
});
return out;
}
// Drops a node in at the caret, or at the end when the caret is not inside the surface.
function insertAtCaret($surface, node) {
var sel = window.getSelection();
if (sel && sel.rangeCount && $.contains($surface.get(0), sel.getRangeAt(0).commonAncestorContainer)) {
var range = sel.getRangeAt(0);
range.deleteContents();
range.insertNode(node);
range.setStartAfter(node);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
} else {
$surface.append(node);
}
}
// The template-param dialog (openTemplateDialog/buildTemplateParamForm) moved to wikt.cite, shared with
// start.js's own Etîmolojî and Çavkanî pickers: cite.openTemplateDialog / cite.buildTemplateParamForm.
// Each new example queued is its own entry, so several can be waiting at once.
var exampleSeq = 0;
// Takes the save over from add-examples while its form is open for one sense: its "Qeyd bike" becomes
// "Lê zêde bike" and the edit joins the pending queue instead of publishing on its own, so examples and
// whatever else is queued go up as one edit. Installed on every open, so the handler always closes over
// the sense whose form is actually open. ctx: {$li, $h3, language, sectionType, sectionOrdinal,
// definitionLevel, $save, $status, afterQueue}.
function takeOverExampleSave(ctx) {
if (!(wikt.addExamples && wikt.addExamples.setSubmitHandler)) {
return; // older add-examples: it publishes on its own, as before
}
// Lets the form read an example as the queue has already changed it.
wikt.addExamples.pendingContent = queuedContent;
wikt.addExamples.setSubmitHandler(function (job) {
exampleSeq++;
queueChange(
placeLabel(ctx.language, ctx.sectionType, ctx.$h3),
// add-examples' own summary, so the combined edit still credits the tool and names its
// version. The trailing stop goes: this is one item in a list, not a sentence of its own.
(job.summary || "nimûne/jêgirtin hate zêdekirin").replace(/\.\s*$/, ""),
function (pageContent) {
// add-examples builds against a revision object, and returns {text, summary}.
var built = job.transform({ content: pageContent });
return { content: built.text, warnings: [] };
},
ctx.$save,
ctx.$status,
// Drawn like any other queued change, in the sense it belongs to.
function () {
return previewSense(ctx.$li, ctx.language, ctx.sectionType, ctx.sectionOrdinal, ctx.definitionLevel);
},
// An edit to an example already on the page brings its own key, so editing it again replaces it.
{ key: job.key || ("example:" + ctx.sectionType + "|" + ctx.sectionOrdinal + "|" +
ctx.definitionLevel.join(".") + "|" + exampleSeq) }
);
job.done();
if (ctx.afterQueue) {
ctx.afterQueue();
}
});
}
// Opens add-examples' form on one example already under a sense -- dsb.js's pencil on it. Its save joins
// the queue like any other change.
function editExample($li, $h3, language, sectionType, sectionOrdinal, definitionLevel, itemEl) {
takeOverExampleSave({ $li: $li, $h3: $h3, language: language, sectionType: sectionType,
sectionOrdinal: sectionOrdinal, definitionLevel: definitionLevel, $save: $(), $status: $(),
afterQueue: openQueueBox });
if (!(wikt.addExamples && wikt.addExamples.editFor && wikt.addExamples.editFor($li.get(0), itemEl))) {
mw.notify("Amûra nimûneyan li vê maneyê nehat dîtin.");
}
}
// Builds the whole "guhartina pêşketî" panel into $quickRow: gloss and the two most-used word lists
// (Hevmane/Dijmane) sit directly in the picker row; the remaining word lists, maneid, etîket, kategorî
// and Nimûne/jêgirtin live behind a "Hevmane û Bêhtir" dropdown; one shared "Lê zêde bike" queues everything the
// user filled in as a single edit via applyAdvancedChanges() and queueChange(). Nimûne/jêgirtin is an
// immediate proxy-click into a different gadget's own form, not part of this combined save.
function buildAdvancedPanel($quickRow, $li, $h3, language, sectionType, sectionOrdinal, definitionLevel, isLastSense) {
// "+" marks the actions that add something, matching the dropdown's own "+ Hevmane" labels.
var $nimune = $("<a href='#' class='dsb-quick-link'></a>").text("+ Nimûne/jêgirtin");
$nimune.on("click", function (e) {
e.preventDefault();
takeOverExampleSave({ $li: $li, $h3: $h3, language: language, sectionType: sectionType,
sectionOrdinal: sectionOrdinal, definitionLevel: definitionLevel, $save: $save, $status: $status });
// Toggle, so clicking this a second time closes the form again. add-examples builds nothing
// on the page of its own, so this is the only way its form opens.
var drive = wikt.addExamples.toggleFor || wikt.addExamples.openFor;
if (!(drive && drive($li.get(0)))) {
mw.notify("Amûra nimûneyan li vê maneyê nehat dîtin.");
}
$moreMenu.slideUp();
});
// getters holds one function per currently-open box (key -> () => value); collectChanges() only ever
// reads from a box the user actually clicked to open.
var getters = {};
// What the sense currently holds, fetched once and shared by every box that prefills from it.
// collectChanges() also diffs against it, so an untouched box never queues a change.
var original = null;
var sensePromise = null;
function loadSense() {
if (!sensePromise) {
sensePromise = fetchSenseFields(language, sectionType, sectionOrdinal, definitionLevel).then(function (fields) {
original = fields;
return fields;
});
}
return sensePromise;
}
function sameList(a, b) {
return (a || []).join("\u0000") === (b || []).join("\u0000");
}
// -- gloss ("Maneyê biguhêre" -- the one box that EDITS the sense's wording; everything else here only ADDS) --
function addGlossField() {
var $fields = $("<div class='dsb-gloss-fields'></div>");
var $textarea = $("<textarea class='dsb-input dsb-edit-gloss' rows='2'></textarea>")
.attr("placeholder", util.msg.loading).prop("disabled", true);
var $autoLink = $("<button type='button' class='dsb-autolink'>Oto-lînk bike</button>").prop("disabled", true);
var $toRaw = $("<a href='#' class='dsb-quick-link dsb-gloss-raw'>Wekî wîkîtekst biguhêre</a>").hide();
var $status = $("<span class='dsb-status'></span>");
var originalGloss = null;
var tplForm = null; // set while the parameter boxes are showing instead of the raw line
var tplCall = null;
// The gloss of a non-Kurdish entry is a translation followed by {{îzah|…}} explaining it. That
// is one field on the page, so it stays one box here; the explanation opens beside it only when
// asked for, and the two are joined back into a single gloss on save. A Kurdish sense is its
// own definition, so it is never offered one -- though a ku gloss that somehow already carries
// an {{îzah}} still opens its box below, rather than being edited blind.
var offerIzah = language !== "ku";
var $izah = $("<textarea class='dsb-input dsb-edit-izah' rows='2'></textarea>")
.attr("placeholder", "Îzaha maneyî… ji bo school-a îngilîzî 'cihê tehsîlgirtinê'");
// A non-Kurdish gloss is a translation and its explanation, so both boxes open together. A Kurdish
// sense is its own definition and gets no îzah box, unless its gloss already carries one.
var $izahWrap = $("<div class='dsb-izah-wrap'></div>")
.append($("<label class='dsb-adv-label'>Îzaha maneyî</label>").append($izah))
.toggle(offerIzah);
$fields.append($textarea);
// The template form edits one call inside the line, so its result is spliced back into the rest.
function currentGloss() {
if (tplForm) {
return $.trim(originalGloss.slice(0, tplCall.start) + tplForm.values() + originalGloss.slice(tplCall.end));
}
return joinGloss($.trim($textarea.val()), $.trim($izah.val()));
}
function showRaw() {
tplForm = null;
tplCall = null;
$fields.empty().append($textarea);
$autoLink.show();
$toRaw.hide();
}
loadSense().then(
function (fields) {
originalGloss = fields.gloss;
// An existing gloss that already ends in {{îzah|…}} comes apart into the two boxes, so
// editing it does not mean editing template syntax by hand.
var parts = util.splitTercimeIzah(fields.gloss);
$textarea.val(parts ? $.trim(parts.tercime) : fields.gloss)
.prop("disabled", false)
// A non-Kurdish gloss is a translation; the box under it takes the explanation.
.attr("placeholder", offerIzah ? util.msg.tercimePrompt : "Maneyê binivîse…");
if (parts) {
$izah.val($.trim(parts.izah));
$izahWrap.show();
}
$autoLink.prop("disabled", false);
var target = glossTemplateTarget(fields.gloss);
if (!target) {
return;
}
fetchTemplateData(target.name).then(function (td) {
// The fetch is async; leave the raw box alone if it has already been typed into.
if ($.trim($textarea.val()) !== $.trim(originalGloss)) {
return;
}
tplCall = target;
tplForm = cite.buildTemplateParamForm(target, td);
$fields.empty().append(tplForm.$element);
$autoLink.hide(); // linkifying a bare template call makes no sense
// A gloss that is one template call carries no separate explanation.
$izahWrap.hide();
$toRaw.show();
});
},
function (codeOrAbort, result) {
$textarea.attr("placeholder", "");
var message = codeOrAbort instanceof SubmitAbort ? codeOrAbort.message : "Nikare maneya heyî bar bike: " + apiErrorMessage(codeOrAbort, result);
$status.removeClass("dsb-status-ok").addClass("dsb-status-error").text(message);
}
);
$autoLink.on("click", function () {
var mane = $.trim($textarea.val());
var izah = $.trim($izah.val());
if (!mane && !izah) {
return;
}
$autoLink.prop("disabled", true);
$status.removeClass("dsb-status-ok dsb-status-error").text("Tê lînkkirin…");
linkifyGlossParts(mane, izah, function (linkedMane, linkedIzah, error) {
$textarea.val(linkedMane);
$izah.val(linkedIzah);
$autoLink.prop("disabled", false);
if (error) {
$status.addClass("dsb-status-error").text(error);
} else {
$status.text("");
}
});
});
$toRaw.on("click", function (e) {
e.preventDefault();
$textarea.val(currentGloss()); // read the form before showRaw() discards it
showRaw();
});
getters.gloss = function () {
var v = currentGloss();
return (v && v !== originalGloss) ? v : null;
};
return $("<div class='dsb-adv-group'></div>").append(
$("<div class='dsb-adv-label'></div>").text("Mane"),
$fields,
$izahWrap,
$("<div class='dsb-form-actions'></div>").append($autoLink, $toRaw, $status)
);
}
// -- one SENSE_GROUPS word-list box (Hevmane/Dijmane/etc.) --
function addWordField(group) {
var field = buildWordListField(group.label + "…");
loadSense().then(function (fields) {
var current = fields.words[group.key];
if (current && current.length) {
field.setValues(current);
}
});
getters["word:" + group.key] = function () {
return field.values();
};
// The hint is senseGroups' own, so the box carries the same example sentence the wizard puts
// beside its copy of this list.
var $label = $("<div class='dsb-adv-label'></div>").text(group.label);
if (group.hint) {
$label.append($("<span class='dsb-adv-hint'></span>").text(group.hint));
}
return $("<div class='dsb-adv-group'></div>").append($label, field.$element);
}
// -- maneid (single value; re-adding one that already exists is rejected -- see applyAdvancedChanges) --
function addNewSenseField() {
var $textarea = $("<textarea class='dsb-input' rows='2'></textarea>").attr("placeholder", "Maneya nû binivîse…");
getters.newSense = function () {
return $.trim($textarea.val());
};
return $("<div class='dsb-adv-group'></div>").append(
$("<div class='dsb-adv-label'></div>").text(util.msg.anotherSense),
$textarea
);
}
function addManeidField() {
var $input = $("<input type='text' class='dsb-input' placeholder='id…'>");
loadSense().then(function (fields) {
$input.val(fields.maneid);
});
getters.maneid = function () {
return $.trim($input.val());
};
return $("<div class='dsb-adv-group'></div>").append($("<div class='dsb-adv-label'></div>").text("Maneid"), $input);
}
// -- etîket / kategorî --
function addEtiketField() {
var field = buildTagField("Etîket…");
loadSense().then(function (fields) {
field.setValues(fields.etiket);
});
getters.etiket = function () {
return field.values();
};
return $("<div class='dsb-adv-group'></div>").append($("<div class='dsb-adv-label'></div>").text("Etîket"), field.$element);
}
function addKategoriField() {
var field = buildTagField("Kategorî…");
loadSense().then(function (fields) {
field.setValues(fields.kategori);
});
getters.kategori = function () {
return field.values();
};
return $("<div class='dsb-adv-group'></div>").append($("<div class='dsb-adv-label'></div>").text("Kategorî"), field.$element);
}
// -- the picker row: the gloss and Nimûne/jêgirtin links sit directly in the row; every word group and
// the rest live behind a "Hevmane û Bêhtir" dropdown. Clicking any link builds and appends its box
// to $openBoxes and hides that link; the box's own "×" removes it, clears its getter, and un-hides the
// link -- several boxes can be open at once. No "Beşek (====)" entry here: buildSubsectionPanel()'s
// own "+ Beşek lê zêde bike" trigger already covers page-level subsections. ----
var FIELD_DEFS = [{ key: "gloss", label: "Maneyê biguhêre", build: addGlossField, primary: true }]
.concat(SENSE_GROUPS.map(function (group) {
return { key: "word:" + group.key, label: group.label, build: function () { return addWordField(group); }, primary: false };
}))
.concat([
{ key: "maneid", label: "Maneid", build: addManeidField, primary: false },
{ key: "etiket", label: "Etîket", build: addEtiketField, primary: false },
{ key: "kategori", label: "Kategorî", build: addKategoriField, primary: false }
]);
// Every sense offers this: the new one lands directly below whichever sense asked for it (see
// insertNewSense), so it is no longer only the last one that can add to the end.
FIELD_DEFS.push({ key: "newSense", label: util.msg.addAnotherSense, build: addNewSenseField, primary: false });
var $picker = $("<div class='dsb-field-picker'></div>");
var $moreMenu = $("<div class='dsb-more-menu'></div>").hide();
var $openBoxes = $("<div class='dsb-open-boxes'></div>");
// $actions (built below) stays hidden until at least one field box is actually open -- otherwise
// "Lê zêde bike" sits there enabled with nothing to queue, which just produces an empty no-op change.
var openBoxCount = 0;
function updateActionsVisibility() {
$actions.toggle(openBoxCount > 0);
}
function makeFieldLink(def) {
var $link = $("<a href='#' class='dsb-quick-link dsb-field-pick'></a>").text(def.key === "gloss" ? def.label : "+ " + def.label);
$link.on("click", function (e) {
e.preventDefault();
openQueueBox();
var $box = def.build();
var $remove = $("<a href='#' class='dsb-box-remove' title='Rake'>×</a>");
$remove.on("click", function (e2) {
e2.preventDefault();
delete getters[def.key];
$box.remove();
$link.show();
openBoxCount--;
updateActionsVisibility();
});
$box.prepend($remove);
$openBoxes.append($box);
util.initManualResize($box);
$link.hide();
$moreMenu.slideUp();
openBoxCount++;
updateActionsVisibility();
});
return $link;
}
// Destructive, so it lives at the bottom of the dropdown behind a confirmation, and shares the
// sense's queue key: deleting supersedes any edit still pending on the same sense.
function makeDeleteLink() {
var $link = $("<a href='#' class='dsb-quick-link dsb-field-delete'>Maneyê rake</a>");
$link.on("click", function (e) {
e.preventDefault();
$moreMenu.slideUp();
OO.ui.confirm("Ev mane û hemû rêzên bin wê dê werin rakirin. Dewam bike?").done(function (ok) {
if (!ok) {
return;
}
queueChange(
placeLabel(language, sectionType, $h3),
"maneyek hate rakirin",
function (baseContent) {
return removeSense(baseContent, language, sectionType, sectionOrdinal, definitionLevel);
},
$save,
$note,
function () {
$li.addClass("dsb-sense-doomed");
return function () {
$li.removeClass("dsb-sense-doomed");
};
},
{ key: senseQueueKey() }
);
$note.show();
});
});
return $link;
}
// Only the links the row actually shows are built now; the dropdown's nine are 43% of the work and
// nobody sees them until "Hevmane û Bêhtir" is clicked, so buildMenuLinks() waits for that.
FIELD_DEFS.forEach(function (def) {
if (!def.primary) {
return;
}
$picker.append(makeFieldLink(def));
if (def.key === "gloss") {
$picker.append($nimune); // directly after "Maneyê biguhêre"
}
});
// Three sections, ruled apart: what labels this sense, the word lists that hang off it, and adding
// another sense. "Maneyê rake" is appended after these by makeDeleteLink() and so closes the last
// one. The word lists follow senseGroups' "order" -- that array's own order is the order the "#:"
// lines are written to the page in, which is not a reading order; the wizard's boxes read the same
// field, so both gadgets list them alike. Anything not named here still appears, after these, so a
// field added later is never silently dropped from the menu.
var MENU_GROUPS = [
["etiket", "kategori", "maneid"],
SENSE_GROUPS.slice().sort(function (a, b) {
return (a.order || 0) - (b.order || 0);
}).map(function (g) {
return "word:" + g.key;
}),
["newSense"]
];
var menuBuilt = false;
function buildMenuLinks() {
if (menuBuilt) {
return;
}
menuBuilt = true;
var byKey = {};
FIELD_DEFS.forEach(function (def) {
if (!def.primary) {
byKey[def.key] = def;
}
});
var placed = {};
MENU_GROUPS.forEach(function (group, groupIndex) {
var defs = group.map(function (key) {
return byKey[key];
}).filter(Boolean);
defs.forEach(function (def, i) {
placed[def.key] = true;
var $link = makeFieldLink(def);
// The rule goes on the first entry of every group after the first.
if (groupIndex > 0 && i === 0) {
$link.addClass("dsb-menu-group-start");
}
$moreMenu.append($link);
});
});
FIELD_DEFS.forEach(function (def) {
if (!def.primary && !placed[def.key]) {
$moreMenu.append(makeFieldLink(def));
}
});
$moreMenu.append(makeDeleteLink());
}
// Lives inside $picker (not appended straight to $quickRow) so .dsb-more-menu's CSS can position it as
// an overlay anchored to $picker, instead of pushing $openBoxes/the rest of the page down when it opens.
var $moreArrow = $("<span class='dsb-more-arrow'></span>").text("▸");
var $moreToggle = $("<a href='#' class='dsb-quick-link dsb-more-toggle'></a>")
.text(util.msg.moreMenu + " ").append($moreArrow);
$moreToggle.on("click", function (e) {
e.preventDefault();
buildMenuLinks();
$moreMenu.slideToggle(function () {
$moreArrow.text($moreMenu.is(":visible") ? "▾" : "▸");
});
});
$picker.append($moreToggle, $moreMenu);
// Outside clicks are handled by one shared document listener (see closeMoreMenus), not one per panel:
// hovering a sense builds its panel, so a long entry would otherwise accumulate dozens of them.
$moreMenu.data("dsbArrow", $moreArrow);
// -- combined save: whatever boxes are open right now, queued together via queueChange(); the actual
// save happens when the user clicks "Guhartinan qeyd bike" on the floating queue box. ----
// Small "not saved yet" preview of everything this queued change will do, appended right under the
// sense's gloss (plain text, not a real render of {{hevmane}}/etc.).
function collectChanges() {
var base = original || { maneid: "", etiket: [], kategori: [], words: {} };
var changed = {};
var words = {};
SENSE_GROUPS.forEach(function (group) {
var getter = getters["word:" + group.key];
words[group.key] = getter ? getter() : [];
if (getter && !sameList(words[group.key], base.words[group.key])) {
changed["word:" + group.key] = true;
}
});
var maneid = getters.maneid ? getters.maneid() : "";
var etiket = getters.etiket ? getters.etiket() : [];
var kategori = getters.kategori ? getters.kategori() : [];
if (getters.maneid && maneid !== (base.maneid || "")) {
changed.maneid = true;
}
if (getters.etiket && !sameList(etiket, base.etiket)) {
changed.etiket = true;
}
if (getters.kategori && !sameList(kategori, base.kategori)) {
changed.kategori = true;
}
return {
gloss: getters.gloss ? getters.gloss() : null,
newSense: getters.newSense ? getters.newSense() : null,
maneid: maneid,
etiket: etiket,
kategori: kategori,
words: words,
changed: changed
};
}
// Both the field save and "Maneyê rake" queue under this, so the later one supersedes the earlier.
function senseQueueKey() {
return "sense:" + sectionType + "|" + sectionOrdinal + "|" + definitionLevel.join(".");
}
var $save = $("<button type='button' class='dsb-save'></button>").text(util.msg.add);
// Where queueChange() writes its confirmation for actions that have no box of their own.
var $note = $("<div class='dsb-status dsb-quick-note'></div>").hide();
var $cancel = $("<button type='button' class='dsb-cancel'></button>").text(util.msg.cancel);
var $status = $("<span class='dsb-status'></span>");
var $actions = $("<div class='dsb-form-actions'></div>").append($save, $cancel, $status).hide();
$cancel.on("click", function () {
// Close the boxes too, not just the row. Hiding alone left every field still open with its
// text in it, so the next click on the pencil reopened the half-filled panel that had just
// been cancelled. Each box's own "x" already clears its getter, restores its link and updates
// the counter, so those are triggered rather than repeating the teardown here.
$openBoxes.find(".dsb-box-remove").trigger("click");
$note.hide().text("");
// dsb.js owns the pencil's pinned state; tell it rather than hiding behind its back, or the
// next click on the pencil would only toggle "pinned" off and appear to do nothing.
$quickRow.trigger("dsbCancel");
hideQueueIfIdle();
});
$save.on("click", function () {
var changes = collectChanges();
function afterLinkify() {
var parts = senseChangeLabels(changes);
queueChange(
placeLabel(language, sectionType, $h3),
parts.length
? parts.join(", ") + (parts.length > 1 ? " hatin guhartin" : " hate guhartin")
: "mane hate guhartin",
function (baseContent) {
return applyAdvancedChanges(baseContent, language, sectionType, sectionOrdinal, definitionLevel, changes);
},
$save,
$status,
function () {
var undoPreview = previewSense($li, language, sectionType, sectionOrdinal,
definitionLevel);
// A new sense has no place on the page yet, so it gets a row of its own directly
// after this one -- the list numbers it, so where it will land is visible.
var $newSense = null;
if (changes.newSense) {
$newSense = $("<li class='dsb-sense-preview'></li>");
$li.after($newSense);
fillWithRendered($newSense, changes.newSense, function () {
$newSense.text(changes.newSense);
});
}
return function () {
undoPreview();
if ($newSense) {
$newSense.remove();
}
};
},
// Re-saving this same sense supersedes its pending change rather than queuing another.
{ key: senseQueueKey() }
);
}
$save.prop("disabled", true);
// Auto-linkify the gloss as part of this save, but only a half nobody has linked by hand.
// Split back into its two halves first, so the explanation is linked on its own rather than
// sitting unseen inside {{îzah|…}}.
if (changes.gloss) {
$status.removeClass("dsb-status-ok dsb-status-error").text("Tê lînkkirin…");
var glossParts = util.splitTercimeIzah(changes.gloss);
linkifyGlossParts(
glossParts ? $.trim(glossParts.tercime) : changes.gloss,
glossParts ? $.trim(glossParts.izah) : "",
function (linkedMane, linkedIzah, error) {
// The change is queued either way, so a gloss that came back empty is not written
// back -- that would save the sense with its wording gone.
var joined = joinGloss(linkedMane, linkedIzah);
if ($.trim(joined)) {
changes.gloss = joined;
}
if (error) {
$status.removeClass("dsb-status-ok").addClass("dsb-status-error").text(error);
}
afterLinkify();
},
true
);
} else {
afterLinkify();
}
});
// The links sit under the buttons, so each open box pushes them down rather than leaving them far above.
$quickRow.empty().append($openBoxes, $actions, $picker, $note);
}
// Attaches the pencil toggle to one leaf definition <li> -- clicking it reveals the "guhartina pêşketî"
// (advanced editing) panel built by buildAdvancedPanel() above.
// ---- per-section UI: "+ Beşek lê zêde bike" after a POS block's own content ----
// Reads the header text of "====" subsections already rendered under this POS heading, straight from the DOM.
// The subsections of a part of speech are the headings one deeper than it is.
function subsectionDepthFor($h3) {
return (sections.headingDepth($h3) || 3) + 1;
}
function isSubsectionHeading($el, depth) {
return sections.headingDepth($el) === depth;
}
function existingSubsectionHeadersInDom($h3) {
var depth = subsectionDepthFor($h3);
var headers = [];
for (var $cursor = headingOuter($h3).next(); $cursor.length; $cursor = $cursor.next()) {
var here = sections.headingDepth($cursor);
if (here && here < depth) {
break; // out of this part of speech
}
if (isSubsectionHeading($cursor, depth)) {
var $hh = $cursor.is("h2, h3, h4, h5, h6") ? $cursor : $cursor.find("h2, h3, h4, h5, h6").first();
headers.push(headingText($hh));
}
}
return headers;
}
// Finds where a new "====" subsection preview belongs in the live DOM, using the same SUBSECTION_ORDER rank
// insertPageSubsection() uses. Returns {insertBefore: $el} (a ranked subsection that outranks this one) or
// {insertAfter: $el} (goes at the end of the block).
function domSubsectionInsertAnchor($h3, header) {
var depth = subsectionDepthFor($h3);
var myRank = subsectionRankForHeader(header);
var $last = headingOuter($h3);
for (var $cursor = headingOuter($h3).next(); $cursor.length; $cursor = $cursor.next()) {
var here = sections.headingDepth($cursor);
if (here && here < depth) {
break; // out of this part of speech
}
if (isSubsectionHeading($cursor, depth)) {
var $hh = $cursor.is("h2, h3, h4, h5, h6") ? $cursor : $cursor.find("h2, h3, h4, h5, h6").first();
var otherRank = subsectionRankForHeader(headingText($hh));
if (otherRank !== -1 && otherRank > myRank) {
return { insertBefore: $cursor };
}
}
$last = $cursor;
}
return { insertAfter: $last };
}
// The wikitext one subsection's content becomes, exactly as insertPageSubsection() writes it -- so the
// preview parses the same string the save will produce.
function subsectionBodyWikitext(language, s, content) {
if (s.kind !== "wordlist") {
return content;
}
return content.filter(function (group) {
return group.words.length;
}).map(function (group) {
return buildWordListCall(WORDLIST_TEMPLATE, language,
withNamedParam(["cure=" + s.header], "sernav", group.sernav), group.words);
}).join("\n\n");
}
// The plain word list this preview falls back to when the parse fails: the values that were typed,
// without the template around them. Right, but not what the page will look like.
function subsectionPlainDom($wrap, s, content) {
if (s.kind !== "wordlist") {
$wrap.append($("<p class='dsb-preview-text'></p>").text(content));
return;
}
content.forEach(function (group) {
if (group.sernav) {
$wrap.append($("<div class='dsb-preview-subheading'></div>").text(group.sernav));
}
var $list = $("<ul class='dsb-preview-list'></ul>");
group.words.forEach(function (w) {
$list.append($("<li></li>").text(w.replace(/<[^<>]*>/g, "")));
});
$wrap.append($list);
});
}
// A "not saved yet" preview of a page-level "====" subsection, dropped into the live page at the
// position the save will put it, with its body rendered the way the page renders it -- real {{kol3}}
// columns and real links, not a list of the words that went into them. Returns the inserted element so
// the caller can remove it again.
function buildSubsectionPreviewDom($h3, s, content, language) {
var $wrap = $("<div class='dsb-preview-block'></div>");
// A styled <div>, not a real <h4>, so existingSubsectionHeadersInDom() never mistakes this live-only
// preview for an actual subsection already on the page.
$wrap.append($("<div class='dsb-preview-heading' role='heading' aria-level='4'></div>").text(s.header));
$wrap.append($("<span class='dsb-preview-badge'></span>").text("nû — hîn nehatiye qeydkirin"));
// Placed before it is filled: a cached render resolves almost at once, and filling a wrap that is
// still detached would announce content nobody can see.
var anchor = domSubsectionInsertAnchor($h3, s.header);
if (anchor.insertBefore) {
anchor.insertBefore.before($wrap);
} else {
anchor.insertAfter.after($wrap);
}
var wikitext = language ? subsectionBodyWikitext(language, s, content) : "";
if ($.trim(wikitext)) {
var $body = $("<div class='dsb-preview-rendered'></div>").appendTo($wrap);
fillWithRendered($body, wikitext, function () {
$body.remove();
subsectionPlainDom($wrap, s, content);
});
} else {
subsectionPlainDom($wrap, s, content);
}
return $wrap;
}
// A free section owns what lies between its own heading and the next heading of any depth, matching
// the range locateHeadingByText() edits. Returns a function that puts the page back, or null.
function replaceFreeSectionInPlace($heading, wikitext) {
var $start = headingOuter($heading);
var body = [];
for (var $cursor = $start.next(); $cursor.length; $cursor = $cursor.next()) {
if (sections.headingDepth($cursor)) {
break;
}
if (!isOwnFurniture($cursor)) {
body.push($cursor.get(0));
}
}
if (!body.length) {
return null;
}
var $slot = $("<div class='dsb-sense-preview'></div>").insertAfter($start);
var $body = $(body).detach();
fillWithRendered($slot, wikitext, function () {
$slot.remove();
$slot = null;
$body.insertAfter($start);
});
return function () {
if ($slot) {
$body.insertAfter($start);
$slot.remove();
$slot = null;
}
};
}
/**
* Redraws a subsection that is already on the page with the content this edit would give it.
*
* The section's own nodes -- everything between its heading and the next heading of the same rank or
* higher -- are detached rather than dropped, so the returned function puts the page back exactly as it
* was when the change is cancelled or superseded.
*/
function replaceSubsectionInPlace($h3, header, wikitext) {
var depth = subsectionDepthFor($h3);
// Walked the same way existingSubsectionHeadersInDom() walks it, so "which nodes are this
// subsection" is decided once and the same way everywhere.
var $start = null;
var body = [];
for (var $cursor = headingOuter($h3).next(); $cursor.length; $cursor = $cursor.next()) {
var here = sections.headingDepth($cursor);
if (here && here < depth) {
break; // out of this part of speech
}
if (isSubsectionHeading($cursor, depth)) {
if ($start) {
break; // the next subsection starts here
}
var $hh = $cursor.is("h2, h3, h4, h5, h6") ? $cursor : $cursor.find("h2, h3, h4, h5, h6").first();
if (headingText($hh) === header) {
$start = $cursor;
}
continue;
}
// This gadget's own furniture stays where it is: the subsection's editing panel is the sibling
// right after its heading, so sweeping it into the body would detach the very panel the change
// was made in and leave the pencil toggling an element that is no longer on the page.
if ($start && !isOwnFurniture($cursor)) {
body.push($cursor.get(0));
}
}
if (!$start || !body.length) {
return null;
}
var $slot = $("<div class='dsb-sense-preview'></div>").insertAfter($start);
var $body = $(body).detach();
fillWithRendered($slot, wikitext, function () {
$slot.remove();
$slot = null;
$body.insertAfter($start);
});
return function () {
if ($slot) {
$body.insertAfter($start);
$slot.remove();
$slot = null;
}
};
}
/**
* The live DOM one "place" section occupies: {nodes, insert}. nodes are what a render replaces --
* empty when the section is not on the page yet -- and insert() puts an element where the section
* belongs, so a new one appears exactly where the save will put it.
*/
// This gadget's own panels sit between a heading and the page's content, so every walk steps over
// them rather than counting one as content.
function isOwnFurniture($el) {
return $el.is(".dsb-toolbar, .dsb-quick-row, .dsb-subsection-panel, .dsb-convert-pos-panel," +
" .dsb-preview-block, .dsb-sense-preview, .dsb-preview-inline");
}
// A heading's place in the page, which orders queued renames and removals bottom-up.
function headingPosition($el) {
var $h = $el.is("h2, h3, h4, h5, h6") ? $el : $el.find("h2, h3, h4, h5, h6").first();
return $("h2, h3, h4, h5, h6").index($h);
}
// One subsection's own nodes on the page, read the way headingSpan() reads the wikitext: a "===="
// subsection down to the next heading at its depth or shallower, a free section ($h3 is its heading)
// down to the next heading of any depth. hasDeeper says a free section has headings under it.
function subsectionDom($h3, s) {
var $start = null;
var depth;
if (s.free) {
$start = headingOuter($h3);
depth = sections.headingDepth($start) || sections.headingDepth($h3);
} else {
depth = subsectionDepthFor($h3);
for (var $c = headingOuter($h3).next(); $c.length; $c = $c.next()) {
var d = sections.headingDepth($c);
if (d && d < depth) {
break;
}
if (d === depth) {
var $hh = $c.is("h2, h3, h4, h5, h6") ? $c : $c.find("h2, h3, h4, h5, h6").first();
if (headingText($hh) === s.header) {
$start = $c;
break;
}
}
}
}
if (!$start) {
return null;
}
var body = [];
var hasDeeper = false;
for (var $cursor = $start.next(); $cursor.length; $cursor = $cursor.next()) {
var here = sections.headingDepth($cursor);
if (here && (s.free || here <= depth)) {
hasDeeper = !!s.free && here > depth;
break;
}
if (!isOwnFurniture($cursor)) {
body.push($cursor.get(0));
}
}
return { $heading: $start, $body: $(body), hasDeeper: hasDeeper };
}
function loosePlaceDom($h3, subsection) {
var place = subsection.place;
var nodes = [];
var $cursor;
if (place === "page-top") {
// Şablon:herwiha binêre renders as <div class="rellink binere"> at the head of the content.
var $out = $("#mw-content-text .mw-parser-output").first();
var $hat = $out.children(".binere, .rellink").first();
if ($hat.length) {
return { nodes: $hat.get(), insert: function ($el) { $hat.before($el); } };
}
return { nodes: [], insert: function ($el) { $out.prepend($el); } };
}
var $start = headingOuter($h3);
if (place === "pron") {
// A "===" heading above this POS block, under the language one.
for (var $back = $start.prev(); $back.length; $back = $back.prev()) {
if ($back.is(".mw-heading2, h2")) {
break;
}
if ($back.is(".mw-heading3, h3")) {
var $hh = $back.is("h3") ? $back : $back.find("h3").first();
if (headingText($hh) === subsection.header) {
for ($cursor = $back.next();
$cursor.length && !$cursor.is(".mw-heading2, .mw-heading3, h2, h3");
$cursor = $cursor.next()) {
if (!isOwnFurniture($cursor)) {
nodes.push($cursor.get(0));
}
}
return { nodes: nodes, insert: function ($el) { $back.after($el); } };
}
}
}
return { nodes: [], insert: function ($el) { $start.before($el); } };
}
if (place === "pos-top") {
// The thumbnails between the heading and the headword line.
for ($cursor = $start.next(); $cursor.length; $cursor = $cursor.next()) {
if (isOwnFurniture($cursor)) {
continue;
}
if (!$cursor.is(".thumb, figure, .floatright, .floatleft")) {
break;
}
nodes.push($cursor.get(0));
}
return { nodes: nodes, insert: function ($el) { $start.after($el); } };
}
return null;
}
// The wikitext a "place" section's pending content renders as. A word list is its one call; everything
// else is already the body that will be written.
function loosePlacePreviewWikitext(subsection, content, language) {
return subsection.kind === "wordlist"
? wt.buildHbnrCall(loosePlaceWords(content))
: loosePlaceBody(subsection, content, language);
}
// Draws a section's headword line from wikitext where the page's own line stands, or above the
// definitions when the section has none. The page's line is detached rather than dropped, so the
// returned function puts it back.
function replaceHeadwordInPlace($h3, wikitext) {
if (!$.trim(wikitext || "")) {
return null;
}
var $old = $();
var $at = $();
for (var $cursor = headingOuter($h3).next(); $cursor.length; $cursor = $cursor.next()) {
if ($cursor.is("p")) {
$old = $cursor;
break;
}
if (sections.headingDepth($cursor) || $cursor.is("ol")) {
$at = $cursor;
break;
}
}
var $slot = $("<div class='dsb-sense-preview'></div>");
if ($old.length) {
$old.before($slot);
} else if ($at.length) {
$at.before($slot);
} else {
return null;
}
var $body = $old.detach();
function restore() {
if (!$slot) {
return;
}
$body.insertBefore($slot);
$slot.remove();
$slot = null;
}
fillWithRendered($slot, wikitext, restore);
return restore;
}
// Redraws one of those places where it stands, the same way replaceSubsectionInPlace() redraws a
// "====" one. The page's own nodes are detached rather than dropped, so cancelling puts them back.
function replaceLoosePlaceInPlace($h3, subsection, wikitext) {
if (!$.trim(wikitext || "")) {
return null;
}
var at = loosePlaceDom($h3, subsection);
if (!at) {
return null;
}
var $slot = $("<div class='dsb-sense-preview'></div>");
at.insert($slot);
var $body = $(at.nodes).detach();
function restore() {
if (!$slot) {
return;
}
$body.insertBefore($slot);
$slot.remove();
$slot = null;
}
fillWithRendered($slot, wikitext, restore);
return restore;
}
// Builds the subsection picker + its word-list/text field. A subsection that isn't on the page yet is saved
// via insertPageSubsection(); one that already is gets prefilled and saved via updatePageSubsection().
// onlyHeader pins the panel to a single subsection (the per-heading pencil); omit it for the full picker.
// free pins the panel to a heading that is not a subsection of a part of speech, located by its own
// text: {header, ordinal}.
function buildSubsectionPanel($panel, $h3, language, sectionType, sectionOrdinal, onlyHeader, free) {
var existing = existingSubsectionHeadersInDom($h3);
// A heading the data does not name is still editable. It is declared a word list so the reader
// tries, in order, its {{kol3}} calls and then a plain bullet list; anything else falls through to
// raw wikitext, which is what prefill() does with a list it cannot read.
// Hidden counts as unknown here: the picker does not offer to build a Werger box, but one already
// on the page still opens, as the wikitext it is.
var unknown = onlyHeader && !PAGE_SUBSECTIONS.some(function (s) {
return s.header === onlyHeader && !s.hidden;
});
// A free section starts as wikitext: what it holds is only known once it has been read, and
// prefill() swaps in the {{kol3}} form there if it turns out to be a list.
var available = free ? [{
header: onlyHeader,
id: "free",
kind: "text",
free: free,
existing: true
}] : unknown ? [{
header: onlyHeader,
id: "unknown",
kind: "wordlist",
unknown: true,
existing: true
}] : PAGE_SUBSECTIONS.filter(function (s) {
// "hidden" takes a section out of the picker without removing it: Werger waits for the
// translation tools it belongs to, and offering a box those cannot yet fill misleads.
// Werger also carries lang: "ku" -- it is a Kurdish-entry section, so it is not offered
// elsewhere either.
return !s.hidden && (!s.lang || s.lang === language) &&
(!onlyHeader || s.header === onlyHeader);
}).map(function (s) {
return $.extend({}, s, { existing: existing.indexOf(s.header) !== -1 });
});
if (!available.length) {
$panel.text("Ji bo vê beşê guhartin tune.");
return;
}
var $select = $("<select class='dsb-subsection-select'></select>");
if (!onlyHeader) {
// Label-less on purpose: it holds the "nothing chosen yet" value without becoming a row in the
// list (makeSearchable() skips a blank option that has no label), so the prompt lives in the
// input's placeholder instead.
$select.append($("<option value=''></option>"));
}
available.forEach(function (s) {
// A "place" entry has no "====" heading of its own, so it is marked rather than left looking
// like the subsections around it.
var label = s.place ? s.header + " (ne binebeş e)"
: (s.existing ? s.header + " (biguhêre)" : s.header);
$select.append($("<option></option>").val(s.id).text(label));
});
var $selectWrap = makeSearchable($select, "Binebeşek hilbijêre");
if (onlyHeader) {
$selectWrap.hide(); // pinned to one subsection: nothing to choose
}
var $fieldWrap = $("<div class='dsb-subsection-field'></div>");
var $save = $("<button type='button' class='dsb-save'></button>").text(util.msg.add);
var $cancel = $("<button type='button' class='dsb-cancel'></button>").text(util.msg.cancel);
var $status = $("<span class='dsb-status'></span>");
var $actions = $("<div class='dsb-form-actions'></div>").append($save, $cancel, $status);
$cancel.on("click", function () {
resetForm();
$panel.hide();
hideQueueIfIdle();
});
// A panel pinned to a heading on the page can rename or remove it. Bilêvkirin and the other places
// have no heading of their own to change.
var headingEditable = !!onlyHeader && available.length === 1 && available[0].existing && !available[0].place;
var $nameWrap = null;
var readName = function () {
return onlyHeader;
};
if (headingEditable) {
var subject = available[0];
var $nameLabel = $("<div class='dsb-adv-label'>Nav</div>");
if (subject.free) {
var $nameInput = $("<input type='text' class='dsb-input dsb-heading-name'>").val(onlyHeader);
readName = function () {
return $.trim($nameInput.val());
};
$nameWrap = $("<div class='dsb-heading-name-wrap'></div>").append($nameLabel, $nameInput);
} else {
// The names the gadget knows, for a subsection of a part of speech; one this part of speech
// already has is left out.
var $nameSelect = $("<select class='dsb-heading-name'></select>");
var names = [onlyHeader];
PAGE_SUBSECTIONS.forEach(function (s) {
if (!s.place && !s.hidden && (!s.lang || s.lang === language) &&
names.indexOf(s.header) === -1 && existing.indexOf(s.header) === -1) {
names.push(s.header);
}
});
names.forEach(function (name) {
$nameSelect.append($("<option></option>").val(name).text(name));
});
var $nameSearch = makeSearchable($nameSelect, "Nav");
$nameSelect.val(onlyHeader).trigger("wiktku:refresh");
readName = function () {
return $nameSelect.val() || onlyHeader;
};
$nameWrap = $("<div class='dsb-heading-name-wrap'></div>").append($nameLabel, $nameSearch);
}
var subjectDom = subsectionDom($h3, subject);
if (!(subjectDom && subjectDom.hasDeeper)) {
var $delete = $("<button type='button' class='dsb-delete'>Beşê jê bibe</button>");
$cancel.after($delete);
$delete.on("click", function () {
OO.ui.confirm("Beşa «" + subject.header + "» dê were rakirin. Dewam bike?").done(function (ok) {
if (!ok) {
return;
}
queueHeadingChange(subject, "beşa «" + subject.header + "» hate rakirin",
function (pageContent) {
return removeSubsection(pageContent, language, sectionType, sectionOrdinal, subject);
},
function () {
var dom = subsectionDom($h3, subject);
if (!dom) {
return null;
}
var $doomed = dom.$heading.add(dom.$body).addClass("dsb-preview-removed");
return function () {
$doomed.removeClass("dsb-preview-removed");
};
});
});
});
}
}
// Renaming and removing share one queue key, so the later choice replaces the earlier one. Both run
// last and bottom-up, since other queued changes find the section by the heading they change.
function queueHeadingChange(s, action, transform, previewFn) {
var dom = subsectionDom($h3, s);
queueChange(subsectionPlaceLabel(s, language, sectionType, $h3), action,
function (pageContent) {
return { content: transform(pageContent), warnings: [] };
},
$save, $status, previewFn,
{
runLast: true,
key: "heading:" + (s.free ? s.header + "|" + s.free.ordinal
: sectionType + "|" + sectionOrdinal + "|" + s.header),
pagePosition: dom ? headingPosition(dom.$heading) : 0
});
}
function queueRename(s, newName) {
queueHeadingChange(s, "beşa «" + s.header + "» bû «" + newName + "»",
function (pageContent) {
return renameSubsection(pageContent, language, sectionType, sectionOrdinal, s, newName);
},
function () {
var dom = subsectionDom($h3, s);
if (!dom) {
return null;
}
var $note = $("<div class='dsb-preview-inline dsb-preview-heading-note'></div>")
.text("→ " + newName + " — nû, hîn nehatiye qeydkirin");
dom.$heading.after($note);
return function () {
$note.remove();
};
});
}
// Undefined while the placeholder option is selected.
function currentSubsection() {
var id = $select.val();
return id ? available.filter(function (s) {
return s.id === id;
})[0] : undefined;
}
// One entry per "{{kol3}}" group in the current wordlist section; empty for any other kind.
var groupFields = [];
var $groups = null;
var editAsText = false; // set when an existing wordlist section turns out to be unparseable
// Whatever has been typed into each subsection's box, so switching the picker away and back restores it
// instead of rebuilding an empty field. Keyed by subsection id; lastId is whichever box is on screen.
var drafts = {};
var lastId = null;
// Called before renderField() rebuilds, while the group fields still hold the outgoing box.
function captureDraft() {
if (!lastId) {
return;
}
drafts[lastId] = imagesField
? { asText: false, images: imagesField.values() }
: (groupFields.length
? { asText: false, groups: readGroups() }
: { asText: editAsText, text: fieldText() });
}
function restoreDraft(s, draft) {
// An existing wordlist we previously fell back to raw wikitext for stays in that mode.
if (draft.asText && s.kind === "wordlist") {
editAsText = true;
groupFields = [];
renderTextArea(s, 6);
}
if (imagesField) {
imagesField.setValues(draft.images);
} else if (groupFields.length) {
setGroups(s, draft.groups);
} else {
setFieldText(draft.text);
}
}
// Etîmolojî is edited on a live surface (see renderChipSurface) rather than in a textarea, so every
// read and write of the field goes through these two instead of touching .dsb-subsection-text.
var chipSurface = null;
function fieldText() {
if (wergerForm) {
return buildWergerBlock(wergerForm.read());
}
return chipSurface
? $.trim(readChipSurface(chipSurface.get(0)))
: $.trim($fieldWrap.find(".dsb-subsection-text").val() || "");
}
function setFieldText(value) {
if (wergerForm) {
wergerForm.set(parseWergerBlock(value));
return;
}
if (chipSurface) {
renderChipSurface(chipSurface, value || "");
var schedule = chipSurface.data("dsbSchedulePreview");
if (schedule) {
schedule(); // show what the loaded etymology renders as
}
} else {
$fieldWrap.find(".dsb-subsection-text").val(value || "");
}
}
// The contenteditable surface: text typed directly, template calls as chips that open the dialog,
// plus start.js's etymology templates in a searchable insert menu.
// A live surface where a template shows as a chip, with a menu that inserts one. Etîmolojî and
// Bilêvkirin are both written this way; they differ only in which templates the menu offers.
function renderTemplateEditor(s, templates) {
$fieldWrap.empty();
var $select = $("<select class='dsb-subsection-select'></select>");
$select.append($("<option value=''></option>"));
templates.forEach(function (t) {
$select.append($("<option></option>").val(t.snippet).text(t.name + " — " + t.desc));
});
var $surface = $("<div class='dsb-tpl-surface' contenteditable='true'></div>")
.attr("role", "textbox")
.attr("aria-multiline", "true");
chipSurface = $surface;
// Clicking the insert menu moves focus out of the surface and the browser forgets the caret, so
// the last caret position inside the surface is kept and restored before inserting -- otherwise
// focus() drops it back to the very start and every template lands at the beginning of the line.
var savedRange = null;
function rememberCaret() {
var sel = window.getSelection();
if (!sel || !sel.rangeCount) {
return;
}
var range = sel.getRangeAt(0);
var root = $surface.get(0);
if (range.commonAncestorContainer === root || $.contains(root, range.commonAncestorContainer)) {
savedRange = range.cloneRange();
}
}
$surface.on("keyup mouseup focusout", rememberCaret);
function restoreCaret() {
if (!savedRange) {
return;
}
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(savedRange);
}
$select.on("change", function () {
var snippet = $select.val();
if (!snippet) {
return;
}
// Same substitution start.js makes: CODE is this section's language, SOURCELANG is left for
// the user to fill in.
snippet = snippet.replace(/CODE/g, language).replace(/SOURCELANG/g, "?");
// A pronunciation template is a bullet of its own ("* {{ku-IPA}}"), so it starts a line
// rather than landing in the middle of one.
var ownLine = /^\*\s/.test(snippet);
if (ownLine) {
snippet = $.trim(snippet.replace(/^\*\s*/, ""));
}
$surface.focus();
restoreCaret();
var $chip = makeTemplateChip(snippet);
var piece = document.createDocumentFragment();
if (ownLine) {
// The bullet, and a line of its own unless the caret already starts one.
var at = caretOffset();
var before = at >= 0 ? readChipSurface($surface.get(0)).slice(0, at) : "";
piece.appendChild(document.createTextNode(!before || /\n$/.test(before) ? "* " : "\n* "));
}
piece.appendChild($chip.get(0));
insertAtCaret($surface, piece);
rememberCaret(); // so a second insert lands after the first, not back at the old spot
$select.val("");
// Straight into the dialog: the snippet is placeholders ("?", "…") until someone fills it in.
cite.openTemplateDialog(snippet).then(function (edited) {
applyChipEdit($chip, edited);
syncPanelWidth();
});
});
// Paste as plain text: pasted markup would otherwise become real nodes in the surface.
$surface.on("paste", function (e) {
var clip = e.originalEvent && e.originalEvent.clipboardData;
if (!clip) {
return;
}
e.preventDefault();
document.execCommand("insertText", false, clip.getData("text/plain"));
});
// The chip the dialog was opened for, or the one that has taken its place. renderChipSurface()
// rebuilds the surface from its wikitext whenever a call is typed in as text, which throws away
// the element this closure is holding; without this, "Jê bibe" would remove a node that is no
// longer on screen and appear to do nothing.
function liveChip($chip, wikitext) {
if ($.contains($surface.get(0), $chip.get(0))) {
return $chip;
}
return $surface.find(".dsb-tpl-chip").filter(function () {
return $(this).attr("data-wikitext") === wikitext;
}).first();
}
function applyChipEdit($chip, edited) {
if (edited === null || !$chip.length) {
return;
}
if (!$.trim(edited)) {
$chip.remove();
return;
}
// Edited into something that is no longer one template call: let it fall back to plain text
// rather than keeping a chip that lies about its contents.
var call = parseTemplateCalls(edited);
if (call.otherContent || call.calls.length !== 1) {
$chip.replaceWith(document.createTextNode(edited));
return;
}
$chip.replaceWith(makeTemplateChip(edited));
}
// Double-click edits the call's wikitext in place; single click opens the parameter dialog. The
// click is held back briefly so the first of a double-click does not open the dialog underneath.
var chipClickTimer = null;
var lastChipTap = 0;
var lastChipNode = null;
var DOUBLE_TAP_MS = 300;
// Double-tap drops the chip back to plain wikitext in the surface, rather than making the chip
// itself editable: a contenteditable inside a contenteditable gives Android a selection handle and
// no usable caret. As plain text the browser's own editing works, and chipifyTyped() puts the chip
// back once the caret leaves the call.
// A collapsed caret inside the text just inserted: at the character tapped where the browser can
// say which that is, otherwise just inside the closing braces so the caret stays within the call
// (which is what stops chipifyTyped putting the chip straight back).
function caretRangeIn(node, event) {
var range;
var x = event && event.clientX;
var y = event && event.clientY;
if (x !== undefined && document.caretRangeFromPoint) {
range = document.caretRangeFromPoint(x, y);
} else if (x !== undefined && document.caretPositionFromPoint) {
var pos = document.caretPositionFromPoint(x, y);
if (pos) {
range = document.createRange();
range.setStart(pos.offsetNode, pos.offset);
}
}
if (range && range.startContainer === node) {
range.collapse(true);
return range;
}
var text = node.nodeValue;
range = document.createRange();
range.setStart(node, /\}\}$/.test(text) ? text.length - 2 : text.length);
range.collapse(true);
return range;
}
function startInlineEdit($chip, event) {
if (!$.contains($surface.get(0), $chip.get(0))) {
return; // the tap timer and dblclick can both fire; only the first one has a chip left
}
var node = document.createTextNode($chip.attr("data-wikitext"));
$chip.replaceWith(node);
$surface.get(0).focus();
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(caretRangeIn(node, event));
}
// Bound before the chip's own handler and stopping there, so removing a call never opens the
// dialog for it on the way out.
$surface.on("click", ".dsb-tpl-chip-remove", function (e) {
e.preventDefault();
e.stopImmediatePropagation();
clearTimeout(chipClickTimer);
lastChipNode = null;
$(this).closest(".dsb-tpl-chip").remove();
syncPanelWidth();
});
$surface.on("click keydown", ".dsb-tpl-chip", function (e) {
if (e.type === "keydown" && e.which !== 13 && e.which !== 32) {
return;
}
e.preventDefault();
var $chip = $(this);
clearTimeout(chipClickTimer);
// A second tap on the same chip edits it inline. Timed here rather than left to "dblclick",
// which mobile browsers fire unreliably or not at all for a double-tap.
var now = new Date().getTime();
if (lastChipNode === this && now - lastChipTap < DOUBLE_TAP_MS) {
lastChipNode = null;
startInlineEdit($chip, e);
return;
}
lastChipTap = now;
lastChipNode = this;
var wikitext = $chip.attr("data-wikitext");
chipClickTimer = setTimeout(function () {
cite.openTemplateDialog(wikitext).then(function (edited) {
applyChipEdit(liveChip($chip, wikitext), edited);
syncPanelWidth();
});
}, DOUBLE_TAP_MS);
});
$surface.on("dblclick", ".dsb-tpl-chip", function (e) {
e.preventDefault();
clearTimeout(chipClickTimer);
startInlineEdit($(this), e);
});
// A long etymology cannot be read in the panel's usual 520px column, so the panel drops its
// width cap once there is enough text to need it. What it will look like is shown by the
// in-place render on the page itself, not by a second copy inside the form.
var WIDE_AT = 240; // characters
function syncPanelWidth() {
var wikitext = readChipSurface($surface.get(0));
if (wikitext.length > WIDE_AT || wikitext.indexOf("\n") !== -1) {
$surface.closest(".dsb-subsection-panel").addClass("dsb-panel-wide");
}
}
// A template typed by hand becomes a chip once typing settles, so it behaves like an inserted one:
// click for the parameter dialog, double-click for its raw wikitext.
var composing = false;
// The caret as an offset into the surface's wikitext, which is what survives rebuilding the DOM.
function caretOffset() {
var sel = window.getSelection();
var root = $surface.get(0);
if (!sel || !sel.rangeCount) {
return -1;
}
var range = sel.getRangeAt(0);
if (range.startContainer !== root && !$.contains(root, range.startContainer)) {
return -1; // caret is somewhere else on the page
}
var pre = document.createRange();
pre.selectNodeContents(root);
pre.setEnd(range.startContainer, range.startOffset);
var holder = document.createElement("div");
holder.appendChild(pre.cloneContents());
return readChipSurface(holder).length;
}
// Puts the caret back at that wikitext offset. Only runs straight after a rebuild, so the surface
// holds nothing but text nodes and chips.
function placeCaret(offset) {
var root = $surface.get(0);
var range = document.createRange();
var nodes = root.childNodes;
root.focus();
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i];
var len = node.nodeType === 3
? node.nodeValue.length
: ($(node).attr("data-wikitext") || $(node).text()).length;
if (offset <= len) {
if (node.nodeType === 3) {
range.setStart(node, offset);
} else {
range.setStartAfter(node); // a chip is atomic; the caret goes after it
}
break;
}
offset -= len;
}
if (!range.startContainer || range.startContainer === document) {
range.selectNodeContents(root);
}
range.collapse(true);
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
// True when the selection overlaps the inside of a template call. A caret resting exactly at the
// closing "}}" does not count -- that is someone who has just finished typing one.
function selectionTouchesCall(wikitext) {
var sel = window.getSelection();
if (!sel || !sel.rangeCount) {
return false;
}
var range = sel.getRangeAt(0);
var root = $surface.get(0);
if (range.startContainer !== root && !$.contains(root, range.startContainer)) {
return false;
}
var from = caretOffset();
var to = from;
if (!range.collapsed) {
var pre = document.createRange();
pre.selectNodeContents(root);
pre.setEnd(range.endContainer, range.endOffset);
var holder = document.createElement("div");
holder.appendChild(pre.cloneContents());
to = readChipSurface(holder).length;
}
return parseTemplateCalls(wikitext).calls.some(function (call) {
return from < call.end && to > call.start;
});
}
function chipifyTyped() {
if (composing) {
return; // never rebuild under an IME
}
var wikitext = readChipSurface($surface.get(0));
// Chips already hold one call each, so a higher call count means one was typed in as text.
if (parseTemplateCalls(wikitext).calls.length <= $surface.find(".dsb-tpl-chip").length) {
return;
}
var offset = caretOffset();
if (selectionTouchesCall(wikitext)) {
return; // still inside a call: leave it as text until the caret moves out
}
renderChipSurface($surface, wikitext);
savedRange = null; // the old range points at nodes that no longer exist
if (offset >= 0) {
placeCaret(offset);
rememberCaret();
}
}
$surface.on("compositionstart", function () {
composing = true;
});
$surface.on("compositionend", function () {
composing = false;
schedulePreview();
});
var previewTimer = null;
function schedulePreview() {
clearTimeout(previewTimer);
previewTimer = setTimeout(function () {
chipifyTyped();
syncPanelWidth();
}, 700);
}
$surface.on("input", schedulePreview);
$surface.data("dsbSchedulePreview", schedulePreview);
$fieldWrap.append(makeSearchable($select, util.msg.addTemplate), $surface);
// Bilêvkirin is written in IPA, so it gets the same character palette the wizard shows. Its
// characters go in as text and its two template buttons as chips, which is what the rest of
// this surface is made of.
if (s.place === "pron") {
$fieldWrap.append(util.buildIpaPalette({
insertText: function (text) {
$surface.focus();
restoreCaret();
insertAtCaret($surface, document.createTextNode(text));
rememberCaret();
schedulePreview();
},
insertTemplate: function (snippet) {
$surface.focus();
restoreCaret();
var $chip = makeTemplateChip(snippet.replace(/\|\s*\}\}$/, "}}"));
insertAtCaret($surface, $chip.get(0));
rememberCaret();
cite.openTemplateDialog($chip.attr("data-wikitext")).then(function (edited) {
applyChipEdit($chip, edited);
syncPanelWidth();
});
}
}));
}
}
// The three "place" sections are wikitext with a shape worth spelling out, since nothing on screen
// says what belongs in the box.
var PLACE_PLACEHOLDERS = {
"page-top": "{{h-bnr|Kelîme|Kelîmeyeke din}} an jî {{herwiha binêre/ane}}",
"pos-top": "[[Wêne:Navê pelî.jpg|thumb|Binnivîs]]",
"pron": "* {{ku-IPA}}"
};
function renderTextArea(s, rows) {
$fieldWrap.empty().append(
$("<textarea class='dsb-input dsb-subsection-text'></textarea>")
.attr("rows", rows)
.attr("placeholder", PLACE_PLACEHOLDERS[s.place] || (s.header + "…"))
);
util.initManualResize($fieldWrap);
}
// A translation box: the {{werger-ser}}/{{werger-bin}} pair, with the sense it covers and the
// layout parameters that template accepts. The rows in between are left to the translation editor
// gadget, which is what edits them on the rendered page.
// Reads and writes through fieldText()/setFieldText() as wikitext, so drafts, save and the queue
// need no special case beyond the append below.
var wergerForm = null;
var imagesField = null;
function renderWergerField(s) {
var inputs = {};
// Its own row class rather than .dsb-tpl-param-row: that one is styled in cite.css, which is
// only on the page when wikt.cite happens to be loaded, and without it the label falls back to
// sitting inline and a short one ("id") is overlapped by its own box.
function row(key, label, placeholder) {
inputs[key] = $("<input type='text' class='dsb-input'>").attr("placeholder", placeholder || "");
return $("<div class='dsb-werger-row'></div>").append(
$("<label class='dsb-adv-label'></label>").text(label).append(inputs[key])
);
}
// {{werger-ser}} also takes stûn, firehî, bgcolor, valign and align, but they only restyle the
// box and its defaults are what these pages use, so the form does not offer them. They are
// still in WERGER_NAMED, so a block that already sets one is read back correctly.
var $main = $("<div class='dsb-werger-main'></div>").append(
row("1", "Mane", "ava vexwarinê"),
row("2", "Mane no", "ne mecbûrî — di parantezê de xuya dike"),
row("id", "id", "ne mecbûrî — IDya tercimeyê")
);
var $note = $("<p class='dsb-werger-note'></p>").text(
"Qutiyeke Wergerê ya vala lê zêde dikî. Tercimeyan bi amûra wergerê dûre binivîse."
);
wergerForm = {
read: function () {
var out = {};
$.each(inputs, function (key, $input) {
var value = $.trim($input.val() || "");
if (value) {
out[key] = value;
}
});
return out;
},
set: function (params) {
$.each(inputs, function (key, $input) {
$input.val((params && params[key]) || "");
});
}
};
$fieldWrap.empty().append($note, $main);
}
// Same [[Kategorî:Çavkanî]] search widget start.js's own picker uses: choosing an entry inserts
// "* {{Target}}" at the cursor. The field itself stays the plain ".dsb-subsection-text" textarea
// every other "kind: text" subsection already uses, so save/prefill/drafts need no changes.
function renderCavkaniField(s) {
var $ta = $("<textarea class='dsb-input dsb-subsection-text'></textarea>")
.attr("rows", 4)
.attr("placeholder", util.msg.pickOrTypeTemplate);
var picker = cite.buildCategoryTemplateSearch({
placeholder: util.msg.addTemplate,
describe: true,
// Same param dialog Etîmolojî opens on a fresh insert: filled from the picked template's own
// TemplateData, so the user adds "rûpel=", "sernav=", ... before anything lands in the field
// rather than typing a bare {{Target}} and going back to fill params in by hand.
onPick: function (entry) {
cite.openTemplateDialog("{{" + entry.target + "}}").then(function (edited) {
if (!edited) {
return; // cancelled, or "Jê bibe" on a call that was never inserted
}
util.insertAtCursor($ta.get(0), "* " + edited + "\n");
$ta.trigger("focus");
});
}
});
var $picker = $("<div class='dsb-cavkani-picker'></div>").append(picker.$element);
$fieldWrap.empty().append($picker, $ta);
}
// One "{{kol3}}" group: its "sernav=" heading and its words. Several of these make up one section --
// "Herwiha binêre" typically carries an unheaded list plus a few headed ones.
function addGroup(s, group) {
var $sernav = $("<input type='text' class='dsb-input'>").attr("placeholder", "ne mecbûrî…");
// {{h-bnr}} takes bare page names, so its rows carry no ⊕: there is nowhere in the call for a
// modifier to go.
var words = buildWordListField(s.header + "…", { modifiers: !s.single });
var $remove = $("<a href='#' class='dsb-quick-link dsb-group-remove'>Komê rake</a>");
var $element = $("<div class='dsb-wordgroup'></div>").append(
// Label beside the box, not above it: stacked, "Sernav" read as a heading for the whole
// group rather than as the name of the field under it. The input is nested in the label so
// clicking the word focuses the box, without needing an id per group.
// A single-call list has no "sernav=" to carry, so the row is built (readGroups still reads
// it) but never shown. Hidden here rather than in renderField: setGroups() rebuilds every
// group on prefill, which would put a visible one back.
$("<div class='dsb-tpl-param-row dsb-param-row-inline'></div>").append(
$("<label class='dsb-adv-label'></label>").text("Sernav").append($sernav)
).toggle(!s.single),
words.$element,
$remove
);
var field = {
$element: $element,
read: function () {
return { sernav: $.trim($sernav.val()), words: words.values() };
},
set: function (g) {
$sernav.val((g && g.sernav) || "");
words.setValues((g && g.words) || []);
}
};
$remove.on("click", function (e) {
e.preventDefault();
var at = $.inArray(field, groupFields);
if (at !== -1) {
groupFields.splice(at, 1);
}
$element.remove();
updateGroupChrome();
});
field.set(group);
groupFields.push(field);
$groups.append($element);
updateGroupChrome();
return field;
}
// With a single group there is nothing to tell apart, so it needs neither a remove link nor a label.
function updateGroupChrome() {
var many = groupFields.length > 1;
$groups.toggleClass("dsb-wordgroups-many", many);
$groups.find(".dsb-group-remove").toggle(many);
}
function readGroups() {
return groupFields.map(function (field) {
return field.read();
});
}
// Rebuilds the blocks to match what the page (or the draft) holds, always leaving at least one.
function setGroups(s, groups) {
groupFields = [];
$groups.empty();
(groups && groups.length ? groups : [null]).forEach(function (group) {
addGroup(s, group);
});
}
// Loads what the page already has into the form, so an edit starts from the current list rather than
// from an empty one. A wordlist section we can't parse degrades to raw wikitext editing.
function prefill(s) {
$status.removeClass("dsb-status-error dsb-status-ok").text("Tê barkirin…");
$save.prop("disabled", true);
fetchPageSubsection(language, sectionType, sectionOrdinal, s).then(function (current) {
$save.prop("disabled", false);
$status.text("");
if (!current) {
return;
}
// A "place" section has no heading of its own, so whether the page already has one cannot
// be seen in the DOM the way a "====" subsection can -- it is known only once its content
// has been read. That is what decides "nûkirin" or "zêdekirin" in the edit summary.
if (s.place) {
s.existing = !!(current.images ? current.images.length : $.trim(current.text || ""));
// The label was set before this content arrived, when nothing yet said one was there.
$save.text(s.existing && s.kind !== "werger" ? util.msg.edit : util.msg.add);
}
if (current.kind === "images" && imagesField) {
imagesField.setValues(current.images);
return;
}
// A free section's field was built before its content was known: a list turns it into the
// {{kol3}} form, anything else leaves it the wikitext box it already is.
if (s.free && current.kind === "wordlist" && !groupFields.length) {
s.kind = "wordlist";
renderWordListField(s);
setGroups(s, current.groups);
if (current.fromBullets) {
$status.text("Ev lîste made bi made hatiye nivîsîn — bi qeydkirinê dibe {{kol3}}.");
}
return;
}
if (current.kind === "wordlist" && groupFields.length) {
setGroups(s, current.groups);
if (current.fromBullets) {
$status.text("Ev lîste made bi made hatiye nivîsîn — bi qeydkirinê dibe {{kol3}}.");
}
return;
}
if (s.kind === "wordlist") {
editAsText = true;
groupFields = [];
renderTextArea(s, 6);
$status.text(s.unknown
? "Ev beş nayê naskirin — wekî wîkîtekst tê guhartin."
: "Şablona vê lîsteyê nehate naskirin — wekî wîkîtekst biguhêre.");
}
setFieldText(current.text);
}, function () {
$save.prop("disabled", false);
$status.addClass("dsb-status-error").text("Nikare muhtewaya heyî bixwîne.");
}).then(function () {
if (currentSubsection() === s) {
loadedSnapshot = contentSnapshot();
}
});
}
// What the box held when the page's content was put in it, so a save that changed nothing queues
// nothing -- renaming a heading does not also rewrite its body.
var loadedSnapshot = null;
function contentSnapshot() {
if (imagesField) {
return JSON.stringify(imagesField.values());
}
return JSON.stringify(groupFields.length ? readGroups() : fieldText());
}
// One "{{kol3}}" group per block, plus a way out to raw wikitext for a list no row can express --
// a shared subpage list like {{herwiha binêre/ane}}, say.
function renderWordListField(s) {
$fieldWrap.empty();
chipSurface = null;
editAsText = false;
var $addGroup = $("<a href='#' class='dsb-quick-link'>+ Komek lê zêde bike</a>");
$addGroup.on("click", function (e) {
e.preventDefault();
addGroup(s, null);
});
$groups = $("<div class='dsb-wordgroups'></div>");
$fieldWrap.append($groups, $addGroup);
addGroup(s, null);
// One call takes one flat list of names, so there is no second group to add.
if (s.single) {
$addGroup.hide();
}
var $toRaw = $("<a href='#' class='dsb-quick-link'>Wekî wîkîtext biguhêre</a>");
$toRaw.on("click", function (e) {
e.preventDefault();
var current = s.place
? wt.buildHbnrCall(loosePlaceWords(readGroups()))
: subsectionBodyWikitext(language, s, readGroups());
editAsText = true;
groupFields = [];
$groups = null;
renderTextArea(s, 3);
setFieldText(current);
util.initManualResize($fieldWrap);
});
$fieldWrap.append($toRaw);
util.initManualResize($fieldWrap);
}
function renderField() {
var s = currentSubsection();
loadedSnapshot = null;
$fieldWrap.empty();
groupFields = [];
$groups = null;
editAsText = false;
chipSurface = null;
wergerForm = null;
imagesField = null;
$status.removeClass("dsb-status-error dsb-status-ok").text("");
// Save stays disabled (but "Betal bike" usable) until something is actually chosen.
$save.prop("disabled", !s);
if (!s) {
$save.text(util.msg.add);
lastId = null;
return; // still on "Binebeşekê hilbijêre"
}
if (s.kind === "wordlist") {
renderWordListField(s);
} else if (s.kind === "images") {
imagesField = util.buildImageListField({
prefix: "dsb-",
rowClass: "dsb-tpl-param-row dsb-param-row-inline",
inputClass: "dsb-input",
addClass: "dsb-quick-link",
pageName: mw.config.get("wgTitle")
});
$fieldWrap.append(imagesField.$element);
} else if (s.header === "Etîmolojî") {
renderTemplateEditor(s, templateChoices("etymTpls", language));
} else if (s.place === "pron") {
renderTemplateEditor(s, templateChoices("pronTpls", language));
} else if (s.kind === "werger") {
renderWergerField(s);
} else if (s.header === "Çavkanî") {
renderCavkaniField(s);
} else {
renderTextArea(s, 3);
}
// Werger always adds a box, even to a section that is already there, so its button never says
// "Biguhêre".
$save.text(s.existing && s.kind !== "werger" ? util.msg.edit : util.msg.add);
// Covers whichever branch above built the field. Idempotent, so a textarea already carrying a
// grip is skipped.
util.initManualResize($fieldWrap);
lastId = s.id;
// A draft always wins over re-reading the page: it is the newer, unsaved state.
if (drafts[s.id]) {
restoreDraft(s, drafts[s.id]);
} else if ((s.existing || s.place) && s.kind !== "werger") {
prefill(s);
}
}
$select.on("change", function () {
captureDraft();
renderField();
});
renderField();
// Back to the state a fresh open would show: no draft, no picked subsection. Called directly
// rather than through the "change" handler above, so the discarded draft is never captured first.
function resetForm() {
drafts = {};
if (!onlyHeader) {
$select.val("").trigger("wiktku:refresh");
}
renderField();
}
$save.on("click", function () {
var s = currentSubsection();
if (!s) {
return;
}
var newName = readName();
if (!newName) {
$status.removeClass("dsb-status-ok").addClass("dsb-status-error").text("Navê beşê vala ye.");
return;
}
var asText = editAsText || (s.kind !== "wordlist" && s.kind !== "images");
var effective = asText ? $.extend({}, s, { kind: "text" }) : s;
var content;
var hasContent;
if (asText) {
content = fieldText();
hasContent = !!content;
} else if (s.kind === "images") {
content = imagesField ? imagesField.values() : [];
hasContent = content.length > 0;
} else {
content = readGroups();
hasContent = content.some(function (group) { return group.words.length; });
}
var renaming = newName !== s.header;
if (renaming) {
queueRename(s, newName);
}
var unchanged = loadedSnapshot !== null && contentSnapshot() === loadedSnapshot;
if (!hasContent || unchanged) {
if (unchanged && !renaming) {
$status.removeClass("dsb-status-ok").addClass("dsb-status-error").text("Tiştek nehatiye guhartin.");
}
return;
}
$save.prop("disabled", true);
// A Werger section holds any number of boxes, so an existing one is appended to rather than
// replaced; every other kind edits the single body it owns.
var appending = s.kind === "werger" && s.existing;
queueChange(
subsectionPlaceLabel(s, language, sectionType, $h3),
s.kind === "werger"
? "qutiyeke wergerê hate zêdekirin"
: "beşa «" + s.header + "» hate " + (s.existing ? "nûkirin" : "zêdekirin"),
function (pageContent) {
var body = content;
if (appending) {
var current = readPageSubsection(pageContent, language, sectionType, sectionOrdinal, effective);
var existingText = current ? $.trim(current.text || "") : "";
body = existingText ? existingText + "\n\n" + content : content;
}
return {
content: s.free
? updateFreeSection(pageContent, language, effective, body)
: (s.place
? writeLoosePlace(pageContent, language, sectionType, sectionOrdinal, effective,
loosePlaceBody(effective, body, language))
: (s.existing
? updatePageSubsection(pageContent, language, sectionType, sectionOrdinal, effective, body)
: insertPageSubsection(pageContent, language, sectionType, sectionOrdinal, effective, body))),
warnings: []
};
},
$save,
$status,
// Werger is a box the translation tools fill in afterwards, so there is nothing to draw for
// it. A new subsection gets a block of its own at the position it will land in; one already
// on the page is redrawn where it stands.
s.kind === "werger" ? null : function () {
// A free section is redrawn under its own heading, which is the element the panel hangs
// on.
if (s.free) {
var restoreFree = replaceFreeSectionInPlace($h3,
subsectionBodyWikitext(language, effective, content));
return function () {
if (restoreFree) {
restoreFree();
restoreFree = null;
}
};
}
// A "place" section has no heading to redraw between, so it is found in the DOM by what
// it renders as and replaced there.
if (s.place) {
var restoreLoose = replaceLoosePlaceInPlace($h3, effective,
loosePlacePreviewWikitext(effective, content, language));
return function () {
if (restoreLoose) {
restoreLoose();
restoreLoose = null;
}
};
}
if (!s.existing) {
var $preview = buildSubsectionPreviewDom($h3, s, content, language);
return function () {
$preview.remove();
};
}
var restore = replaceSubsectionInPlace($h3, effective.header,
subsectionBodyWikitext(language, effective, content));
return function () {
if (restore) {
restore();
restore = null;
}
};
},
{ key: s.free
? "section:" + s.header + "|" + s.free.ordinal
: "subsection:" + sectionType + "|" + sectionOrdinal + "|" + s.header }
);
});
$panel.empty().append($selectWrap, $nameWrap, $fieldWrap, $actions);
}
// Builds the "choose a part of speech" picker that reclassifies a section -- an unclassified
// "=== Mane ===" placeholder, or a "=== Biwêj ===" that turns out to be a real part of speech. Saved via
// convertSectionToPos(); lists every name from sectionNames except the one the section already is.
function buildConvertPosPanel($panel, $h3, language, sectionType, sectionOrdinal) {
var $select = $("<select class='dsb-convert-pos-select'></select>");
// Label-less, like the subsection picker's: it holds the "nothing chosen yet" value without showing
// up as a row, so the prompt lives in the input's placeholder instead.
$select.append($("<option value=''></option>"));
Object.keys(sectionNames).forEach(function (name) {
// The section's own part of speech is listed too, marked: picking it changes nothing about the
// heading and leaves the parameters below to be edited on their own.
$select.append($("<option></option>").val(name)
.text(name === sectionType ? name + " (ya niha)" : name));
});
var $selectWrap = makeSearchable($select, "Li cureyekî rêzimanî bigere");
// Opens on the section's own part of speech, so editing only its parameters needs no pick.
if (sectionNames[sectionType]) {
$select.val(sectionType).trigger("wiktku:refresh");
}
// The same card the entry wizard shows under its part-of-speech question -- gender, transitivity,
// the transliteration boxes and everything behind "Tercîhên pêşketî". Shared from
// wikt.edit/posparams.js, so reclassifying a section here offers exactly what creating one in the
// wizard does.
var card = posparams.build({ lang: language, pos: "" });
// Opened on the call the section already has, so this edits the current template rather than
// starting from a blank line. setContext() rebuilds the line whenever the part of speech moves,
// so the prefill is only the starting point.
var spec0 = posTemplateFor(sectionType);
card.setContext(language, spec0 ? spec0.name : "", /^Formeke /.test(sectionType));
fetchPageContent().then(function (pageContent) {
try {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
var found = findPosTemplateLine(loc.lines, loc.posLineIndex, sectionType);
if (found.index !== -1 && ($select.val() || sectionType) === sectionType) {
card.setWikitext(found.text);
}
} catch (e) { /* an unreadable section just leaves the card as it was */ }
});
$select.on("change", function () {
var spec = posTemplateFor($select.val());
card.setContext(language, spec ? spec.name : "", /^Formeke /.test($select.val() || ""));
});
// "gh" on its own, or "<form>-<base>" once a structural form is picked -- start.js's own composition.
// convertSectionToPos() writes the template itself, so only what follows "{{pos|lang" is wanted
// from the card's line.
function posParams() {
var m = /^\{\{[^|}]*\|[^|}]*\|?([\s\S]*)\}\}$/.exec(card.wikitext());
return m ? m[1] : "";
}
var $save = $("<button type='button' class='dsb-save'></button>").text(util.msg.add);
var $cancel = $("<button type='button' class='dsb-cancel'></button>").text(util.msg.cancel);
var $status = $("<span class='dsb-status'></span>");
var $actions = $("<div class='dsb-form-actions'></div>").append($save, $cancel, $status);
// dsb.js owns the pencil and the panel's built state, so it does the closing.
$cancel.on("click", function () {
$panel.trigger("dsbCancel");
hideQueueIfIdle();
});
$save.on("click", function () {
var chosen = $select.val();
if (!chosen) {
$status.removeClass("dsb-status-ok").addClass("dsb-status-error").text("Pêşî cureyekî rêzimanî hilbijêre.");
return;
}
var params = posParams();
$save.prop("disabled", true);
$status.removeClass("dsb-status-error").text("");
queueChange(
langLabel(language),
"beşa «" + sectionLabel(sectionType, $h3) + "» bû «" + chosen + "»",
function (pageContent) {
return { content: convertSectionToPos(pageContent, language, sectionType, sectionOrdinal, chosen, params), warnings: [] };
},
$save,
$status,
function () {
// The heading keeps its text, which every other queued change finds the section by; a
// note under it names the new one. The headword line is drawn as the save will write it.
var $note = $("<div class='dsb-preview-inline dsb-preview-heading-note'></div>")
.text("→ " + chosen + (params ? " (" + params + ")" : "") + " — nû, hîn nehatiye qeydkirin");
headingOuter($h3).after($note);
var dropped = false;
var restoreHeadword = null;
fetchPageContent().then(function (pageContent) {
if (dropped) {
return;
}
var call;
try {
var loc = locateSection(pageContent, language, sectionType, sectionOrdinal);
call = posCallLine(findPosTemplateLine(loc.lines, loc.posLineIndex, sectionType),
language, chosen, params);
} catch (e) {
return;
}
restoreHeadword = replaceHeadwordInPlace($h3, call);
});
return function () {
dropped = true;
$note.remove();
if (restoreHeadword) {
restoreHeadword();
restoreHeadword = null;
}
};
},
// Runs after every other queued change: it renames the "Mane" heading those changes locate
// themselves by, so anything edited on this section has to be applied first.
{ runLast: true, key: "convert:" + sectionType + "|" + sectionOrdinal, pagePosition: headingPosition($h3) }
);
});
$panel.empty().append($selectWrap, card.$element, $actions);
}
// Attaches the "+ Beşek lê zêde bike" trigger right after a POS block's own last element.
// Puts the same pencil the definitions use on every existing "====" subsection heading the gadget knows how
// to edit, opening buildSubsectionPanel() pinned to that one subsection.
module.exports = {
buildAdvancedPanel: buildAdvancedPanel,
buildSubsectionPanel: buildSubsectionPanel,
buildConvertPosPanel: buildConvertPosPanel,
editExample: editExample,
clearQueue: clearQueue,
closeAllPanels: closeAllPanels,
openQueueBox: openQueueBox
};
}());
// </nowiki>
ol1e26blp535n4gqwta3yeo6sl0vgfr
Bikarhêner:Wikihez/Gadget-test/dsb-panels.css
2
1358910
6531986
6425085
2026-09-17T16:47:32Z
Wikihez
7344
Subsection panel: rename a heading (Nav) and remove it (Beşê jê bibe), queued last and bottom-up
6531986
css
text/css
/* <nowiki> */
/*
* Page: [[MediaWiki:Gadget-wikt.dsb-panels.css]]
*/
/* ── Searchable select ───────────────────────────────────────────────
The native <select> is hidden inside the wrapper and still holds the value; the input filters the list. */
.dsb-ac-wrap {
position: relative;
display: block;
}
.dsb-ac-input {
width: 100%;
box-sizing: border-box;
}
.dsb-ac-results {
position: absolute;
z-index: 30;
top: 100%;
left: 0;
right: 0;
max-height: 260px;
margin: 2px 0 0;
padding: 4px;
overflow-y: auto;
list-style: none;
font-family: var(--wikt-sans);
font-size: 13px;
background: var(--wikt-field-bg);
border: 1px solid var(--wikt-field-edge);
border-radius: var(--wikt-r-field);
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.12);
}
.dsb-ac-results li {
padding: 6px 8px;
border-radius: 6px;
cursor: pointer;
}
.dsb-ac-results li:hover,
.dsb-ac-results li.dsb-ac-active {
background: var(--wikt-accent-tint);
}
/* ── Etîmolojî editing surface ───────────────────────────────────────
A contenteditable line of wikitext: text is typed straight into it, template calls sit in it as chips. */
.dsb-tpl-surface {
min-height: 72px;
margin-top: 6px;
padding: 8px 10px;
font-family: var(--wikt-sans);
font-size: 14px;
line-height: 1.6;
color: var(--wikt-ink);
white-space: pre-wrap;
word-wrap: break-word;
background: var(--wikt-field-bg);
border: 1px solid var(--wikt-field-edge);
border-radius: var(--wikt-r-field);
}
.dsb-tpl-surface:focus {
outline: none;
border-color: var(--wikt-accent);
box-shadow: 0 0 0 2px var(--wikt-accent-tint);
}
/* The chips: visibly not prose, and atomic to the caret. */
/* Takes the call off the line without going through the parameter dialog -- on a phone there is no
reliable backspace onto a contenteditable="false" chip. */
.dsb-tpl-chip-remove {
position: relative;
margin-left: 5px;
padding: 0 2px;
font-family: var(--wikt-sans);
font-size: 13px;
line-height: 1;
color: var(--wikt-accent-deep);
opacity: 0.6;
cursor: pointer;
}
.dsb-tpl-chip-remove:hover {
color: var(--wikt-danger);
opacity: 1;
}
/* The glyph stays small; the tap target grows invisibly around it. */
@media (max-width: 720px), (pointer: coarse) {
.dsb-tpl-chip-remove {
opacity: 1;
}
.dsb-tpl-chip-remove::after {
content: "";
position: absolute;
inset: -10px -8px;
}
}
.dsb-tpl-chip {
display: inline-block;
margin: 0 1px;
padding: 1px 6px;
font-family: var(--wikt-mono);
font-size: 12.5px;
color: var(--wikt-accent-deep);
max-width: 100%;
white-space: normal;
overflow-wrap: anywhere;
vertical-align: baseline;
background: var(--wikt-accent-tint);
border: 1px solid var(--wikt-accent-edge);
border-radius: var(--wikt-r-pill);
touch-action: manipulation;
cursor: pointer;
user-select: none;
}
.dsb-tpl-chip:hover,
.dsb-tpl-chip:focus {
background: var(--wikt-field-bg);
border-color: var(--wikt-accent);
outline: none;
}
/* ── Template parameter form ─────────────────────────────────────────
.dsb-tpl-name/-desc/-param-row/-help/-undocumented/-modifier-list and the deprecated-label override now
live in cite.css (wikt.cite's own stylesheet): buildTemplateParamForm() moved there so start.js's own
Etîmolojî/Çavkanî pickers can open the same dialog. */
/* One-line variant: label beside its box rather than above it, for a single field whose name would
otherwise read as a heading for everything below it (the word groups' "Sernav"). */
.dsb-param-row-inline > .dsb-adv-label {
display: flex;
gap: 8px;
align-items: center;
}
/* Sernav is optional and names the group; the word boxes under it are what the form is actually for.
Letting it stretch to the full row made the one thing you may leave blank the widest box on the
card, so it takes only the space its own content needs. */
.dsb-param-row-inline .dsb-input {
flex: 0 1 auto;
width: 12em;
max-width: 40%;
/* .dsb-input's own 16em floor would win over the width above and make this the widest box on the
card, which is the opposite of the point. */
min-width: 0;
max-width: 100%;
min-width: 0;
}
.dsb-gloss-raw {
font-size: 12px;
}
/* Disclosure arrow on "Hevmane û Bêhtir", flipped when the menu is open. */
.dsb-more-arrow {
font-size: 10px;
vertical-align: 1px;
}
/* The POS panel's follow-up questions (cinsiyet / lektur): label above its select, like the field boxes. */
.dsb-pos-param-row {
display: flex;
flex-direction: column;
gap: 4px;
margin-top: 8px;
}
/* ── "Guhartina pêşketî" (advanced editing) panel ──────────────────────
Revealed by the pencil toggle above: every per-sense action this gadget offers, queued together with one
"Lê zêde bike". A plain inline row, not a bordered/background card. */
.dsb-quick-row {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 4px;
box-sizing: border-box;
max-width: 520px;
margin: 4px 0 0 16px;
font-family: var(--wikt-sans);
font-size: 12px;
color: var(--wikt-ink-2);
}
.dsb-quick-link {
align-self: flex-start;
margin-bottom: 10px;
font-size: 12px;
color: var(--wikt-accent);
text-decoration: none;
white-space: nowrap;
cursor: pointer;
}
/* The links under a word list ("+ Komek lê zêde bike", "Wekî wîkîtext biguhêre") sit in the field wrap
rather than in a flex column, so they need their own spacing or they run together. */
.dsb-subsection-field > .dsb-quick-link {
display: inline-block;
margin-right: 14px;
}
.dsb-quick-link:hover,
.dsb-quick-link:focus {
color: var(--wikt-accent-deep);
text-decoration: underline;
}
/* The picker row: gloss + the two primary word groups as direct "+ <field>" links, plus a "Hevmane û Bêhtir" toggle
for the rest -- clicking a link opens its box above the buttons and hides the link; .dsb-box-remove un-hides it.
position: relative anchors .dsb-more-menu's overlay below (see there). */
.dsb-field-picker {
position: relative;
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 4px 14px;
}
.dsb-field-picker .dsb-quick-link {
margin-bottom: 0;
}
/* Under the buttons once a box is open: a little space so the links do not read as part of the buttons. */
.dsb-open-boxes:not(:empty) ~ .dsb-field-picker {
margin-top: 8px;
}
/* The "Hevmane û Bêhtir" dropdown: every non-primary field link (plus Nimûne/jêgirtin, which sits first) stacked as a
plain vertical list. Floats over the page as an overlay (positioned against .dsb-field-picker) instead of
pushing $openBoxes/the rest of the definition down when it opens. */
.dsb-more-menu {
position: absolute;
top: 100%;
left: 0;
z-index: 10;
display: flex;
flex-direction: column;
align-items: flex-start;
width: max-content;
max-width: min(320px, calc(100vw - 40px));
gap: 6px;
margin: 2px 0 0 4px;
padding: 8px 12px;
background: var(--wikt-card);
border: 1px solid var(--wikt-rule);
border-radius: var(--wikt-r-field);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.06);
}
.dsb-more-menu .dsb-quick-link {
margin-bottom: 0;
}
/* Boxes the user has actually opened, stacked in the order they were added -- several can be open at once. */
.dsb-open-boxes {
display: flex;
flex-direction: column;
}
/* Nothing open: take it out of the flow so the links do not sit above an empty gap. */
.dsb-open-boxes:empty {
display: none;
}
/* One field (its label + input) within the panel -- always a currently-open, removable box. */
.dsb-adv-group {
position: relative;
margin-bottom: 14px;
padding-right: 20px;
}
.dsb-adv-label {
margin-bottom: 4px;
font-size: 12px;
font-weight: 600;
color: var(--wikt-ink-3);
}
/* The example sentence beside a word list's name: same text the wizard shows, so it reads as the
quieter half of the label rather than a second heading. */
.dsb-adv-hint {
margin-left: 6px;
font-weight: 400;
font-style: italic;
color: var(--wikt-muted);
}
/* The shared image rows (util.js's buildImageListField): one label + box per line, like the parameter
rows above them. Three identical boxes twice over is unreadable without a break between them, so each
image is its own block with its own name. */
.dsb-resim-list {
margin: 0;
padding-left: 0;
}
.dsb-resim-item {
list-style: none;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--wikt-rule-soft);
}
.dsb-resim-item:first-child {
margin-top: 0;
padding-top: 0;
border-top: 0;
}
.dsb-resim-index {
margin-bottom: 6px;
font-size: 12px;
font-weight: 600;
color: var(--wikt-ink-3);
}
.dsb-box-remove {
position: absolute;
top: 0;
right: 0;
color: var(--wikt-ink-3);
text-decoration: none;
font-size: 14px;
line-height: 1;
cursor: pointer;
}
.dsb-box-remove:hover,
.dsb-box-remove:focus {
color: var(--wikt-accent-deep);
}
.dsb-form-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
}
.dsb-input {
box-sizing: border-box;
min-width: 16em;
padding: 6px 10px;
font-family: var(--wikt-sans);
font-size: 13px;
color: var(--wikt-ink);
background: var(--wikt-field-bg);
border: 1px solid var(--wikt-field-edge);
border-radius: var(--wikt-r-field);
transition: border-color 0.12s ease, box-shadow 0.12s ease;
}
/* The native corner is replaced by util.js's grip (see wikt-edit.css), which drags vertically only and
remembers the height. */
.dsb-subsection-panel textarea.dsb-input,
.dsb-adv-group textarea.dsb-input {
resize: none;
}
.dsb-input::placeholder {
color: var(--wikt-placeholder);
opacity: 1;
}
.dsb-input:focus {
border-color: var(--wikt-accent);
box-shadow: 0 0 0 3px var(--wikt-accent-tint);
outline: 0;
}
/* ── Plain tag field (Etîket, Kategorî) ────────────────────────────────
Plain type-and-commit tags with no suggestion panel; unlike Hevmane/Dijmane/etc., these carry no modifiers. */
.dsb-tagfield {
box-sizing: border-box;
display: inline-flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
min-width: 16em;
padding: 4px 6px;
background: var(--wikt-field-bg);
border: 1px solid var(--wikt-field-edge);
border-radius: var(--wikt-r-field);
transition: border-color 0.12s ease, box-shadow 0.12s ease;
}
.dsb-tagfield:focus-within {
border-color: var(--wikt-accent);
box-shadow: 0 0 0 3px var(--wikt-accent-tint);
}
.dsb-tag {
display: inline-flex;
align-items: center;
gap: 6px;
background: var(--wikt-accent-tint);
color: var(--wikt-accent-deep);
border: 1px solid var(--wikt-accent-edge);
border-radius: var(--wikt-r-pill);
font-size: 12px;
padding: 3px 6px 3px 10px;
}
.dsb-tag-remove {
border: 0;
background: transparent;
color: var(--wikt-muted);
font-size: 13px;
line-height: 1;
cursor: pointer;
padding: 0 2px;
}
.dsb-tag-remove:hover {
color: var(--wikt-danger);
}
.dsb-tagfield .dsb-input {
flex: 1 1 8em;
min-width: 8em;
border: 0;
padding: 3px 2px;
background: transparent;
box-shadow: none;
}
.dsb-tagfield .dsb-input:focus {
box-shadow: none;
}
.dsb-save,
.dsb-cancel,
.dsb-delete,
.dsb-autolink {
font-family: var(--wikt-sans);
font-size: 13px;
font-weight: 500;
padding: 6px 14px;
border-radius: var(--wikt-r-pill);
cursor: pointer;
transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease;
}
/* One filled pill per row — the same rule the form's footer follows. */
.dsb-save {
background: var(--wikt-accent);
border: 1px solid var(--wikt-accent);
color: #fff;
}
.dsb-save:hover {
background: var(--wikt-accent-deep);
border-color: var(--wikt-accent-deep);
}
.dsb-save:disabled {
opacity: 0.55;
cursor: progress;
}
.dsb-cancel {
background: transparent;
border: 1px solid transparent;
color: var(--wikt-ink-3);
}
.dsb-cancel:hover {
border-color: var(--wikt-field-edge);
color: var(--wikt-ink);
}
/* Removing a heading: plain like Betal bike, in the danger colour, pushed to the far end of the row. */
.dsb-delete {
margin-left: auto;
background: transparent;
border: 1px solid transparent;
color: var(--wikt-danger);
}
.dsb-delete:hover {
border-color: var(--wikt-danger);
}
/* The heading's name, above the section's own box. */
.dsb-heading-name-wrap {
margin-bottom: 10px;
}
/* A heading queued for removal, with everything it owns, until the queue is saved or the change dropped. */
.dsb-preview-removed {
opacity: 0.5;
text-decoration: line-through;
text-decoration-color: var(--wikt-danger);
}
/* An outline pill, distinct from both the filled .dsb-save and the plain-text .dsb-cancel -- a secondary
utility action, not the primary save nor a dismissal. */
.dsb-autolink {
background: transparent;
border: 1px solid var(--wikt-accent-edge);
color: var(--wikt-accent);
}
.dsb-autolink:hover {
border-color: var(--wikt-accent);
color: var(--wikt-accent-deep);
}
.dsb-autolink:disabled {
opacity: 0.55;
cursor: progress;
}
/* Status line. Color comes from .dsb-status-ok/.dsb-status-error (added by the .js on success/failure)
rather than an inline style, so these tokens (and dark mode) apply. */
.dsb-status {
font-family: var(--wikt-sans);
font-size: 12.5px;
color: var(--wikt-muted);
}
.dsb-status-ok { color: var(--wikt-ok); }
.dsb-status-error { color: var(--wikt-danger); }
/* ── "+ Beşek lê zêde bike" ──────────────────────────────────────────
An accent-coloured action link, like the form's .wiktku-adv-toggle:
it opens something, so it is not muted grey. */
/* iOS Safari zooms the page in whenever a field it focuses has text under 16px, and everything here is
12-13px for density. On touch the form controls go up to the size that stops it; the pointer query
leaves the desktop layout alone. */
@media (pointer: coarse) {
.dsb-input,
.dsb-tag-input,
.dsb-subsection-select,
.dsb-convert-pos-select {
font-size: 16px;
}
/* Full width, whatever the viewport reports: a phone asked to render the desktop site says ~980px and
would otherwise leave these at their desktop size. */
.dsb-subsection-panel .dsb-input,
.dsb-subsection-panel .dsb-subsection-select,
.dsb-convert-pos-panel .dsb-convert-pos-select {
width: 100%;
max-width: 100%;
}
/* Sernav is the exception: it names the group, so it stays visibly smaller than the boxes it heads.
The cap matters more than the em width -- at 16px, 12em came out the same size as a word box. */
.dsb-subsection-panel .dsb-param-row-inline .dsb-input {
width: 8em;
max-width: 40%;
}
}
/* The image rows reuse the inline row, but nothing here is optional the way a group's "Sernav" is:
a file name and a caption want the whole row. */
.dsb-resim-list .dsb-param-row-inline > .dsb-adv-label,
.dsb-resim-list .dsb-tpl-param-row > label {
display: flex;
gap: 8px;
align-items: center;
}
.dsb-resim-list .dsb-param-row-inline .dsb-input,
.dsb-resim-list .dsb-tpl-param-row .dsb-input {
flex: 1 1 auto;
width: auto;
max-width: 100%;
}
/* ── Subsection panel ────────────────────────────────────────────────
A sunken card rather than a grey utility box; fields stack so the
picker and its input are not competing for one narrow row. */
.dsb-subsection-select,
.dsb-convert-pos-select {
box-sizing: border-box;
width: 100%;
max-width: 100%;
padding: 6px 10px;
font-family: var(--wikt-sans);
font-size: 13px;
color: var(--wikt-ink);
background: var(--wikt-field-bg);
border: 1px solid var(--wikt-field-edge);
border-radius: var(--wikt-r-field);
}
.dsb-subsection-select:focus,
.dsb-convert-pos-select:focus {
border-color: var(--wikt-accent);
box-shadow: 0 0 0 3px var(--wikt-accent-tint);
outline: 0;
}
/* The template-insert picker sits above the Çavkanî textarea, not in place of it -- a little breathing
room so the two don't read as one control. */
.dsb-cavkani-picker {
margin-bottom: 6px;
}
.dsb-subsection-field {
min-width: 0;
}
.dsb-subsection-words,
.dsb-subsection-text {
box-sizing: border-box;
width: 100%;
min-width: 0;
}
.dsb-subsection-text {
line-height: 1.6;
resize: vertical;
}
/* ── Gloss field inside the advanced-editing panel ────────────────────
Same shape as .dsb-subsection-text. */
.dsb-edit-gloss {
width: 100%;
line-height: 1.6;
resize: vertical;
}
/* ── Live "not saved yet" previews ─────────────────────────────────────
Dropped into the rendered page at the change's own spot. Dashed edge + a small badge mark them as
pending, not real content; they disappear on "Betal bike" or once the real save lands. */
.dsb-preview-block,
.dsb-preview-inline {
margin: 8px 0;
padding: 8px 12px;
font-family: var(--wikt-sans);
font-size: 13px;
color: var(--wikt-ink-2);
background: var(--wikt-accent-tint);
border: 1px dashed var(--wikt-accent-edge);
border-radius: var(--wikt-r-field);
}
.dsb-preview-inline {
margin-left: 26px;
max-width: 520px;
}
.dsb-preview-heading {
margin: 0 0 4px;
font-size: 15px;
font-weight: 600;
color: var(--wikt-ink);
}
.dsb-preview-badge {
display: inline-block;
margin-bottom: 6px;
padding: 1px 8px;
font-size: 11px;
font-weight: 500;
color: var(--wikt-accent-deep);
background: var(--wikt-card);
border: 1px solid var(--wikt-accent-edge);
border-radius: var(--wikt-r-pill);
}
/* Parser output goes in here, so it drops the panel's own font and colour and takes the page's \u2014 the
point of the box is that what is inside it looks exactly like what will be saved. */
.dsb-preview-rendered,
.dsb-preview-rendered-inline {
font-family: inherit;
font-size: 14px;
color: var(--wikt-ink);
}
.dsb-preview-rendered-inline {
display: inline;
}
.dsb-preview-rendered-inline .mw-parser-output,
.dsb-preview-rendered-inline .mw-parser-output > p {
display: inline;
}
.dsb-preview-list,
.dsb-preview-text {
margin: 0;
padding-left: 18px;
}
.dsb-preview-text {
padding-left: 0;
}
/* ── Floating save-queue widget ─────────────────────────────────────
This gadget's own small floating box. One "Lê zêde bike" click anywhere on the page adds a line here;
"Guhertinan qeyd bike" makes the one real save; "Betal bike" drops everything queued without touching the page. */
.dsb-queue-box {
position: fixed;
top: 60px;
right: 16px;
z-index: 1000;
box-sizing: border-box;
width: 280px;
max-width: calc(100vw - 32px);
max-height: calc(100vh - 90px);
overflow-y: auto;
padding: 12px 14px;
font-family: var(--wikt-sans);
font-size: 13px;
color: var(--wikt-ink-2);
background: var(--wikt-card);
border: 1px solid var(--wikt-accent);
border-radius: var(--wikt-r-card);
box-shadow: 0 4px 18px rgba(0, 0, 0, 0.16);
}
.dsb-queue-title {
margin-right: 20px;
font-size: 13px;
font-weight: 600;
color: var(--wikt-ink);
}
.dsb-queue-close {
float: right;
color: var(--wikt-ink-3);
text-decoration: none;
font-size: 14px;
line-height: 1;
cursor: pointer;
}
.dsb-queue-close:hover,
.dsb-queue-close:focus {
color: var(--wikt-accent-deep);
}
/* Stands in for the list before anything is queued, saying what the save button is for. */
.dsb-queue-empty {
margin: 8px 0;
font-size: 12px;
line-height: 1.5;
color: var(--wikt-ink-3);
}
.dsb-queue-list {
margin: 8px 0;
padding-left: 18px;
max-height: 160px;
overflow-y: auto;
}
.dsb-queue-list li {
margin-bottom: 4px;
}
/* Drops one queued change. Dim until its line is hovered, so the list stays a list and not a row of
buttons. */
.dsb-queue-drop {
margin-left: 6px;
padding: 0 3px;
border: 0;
background: transparent;
color: var(--wikt-muted);
font-size: 13px;
line-height: 1;
cursor: pointer;
opacity: 0;
transition: opacity 0.12s ease, color 0.12s ease;
}
.dsb-queue-list li:hover .dsb-queue-drop,
.dsb-queue-drop:focus {
opacity: 1;
}
.dsb-queue-drop:hover {
color: var(--wikt-danger);
opacity: 1;
}
/* No hover on touch, so the button is simply there. */
@media (hover: none), (pointer: coarse) {
.dsb-queue-drop {
opacity: 1;
}
}
.dsb-queue-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 4px;
}
.dsb-queue-save,
.dsb-queue-cancel {
font-family: var(--wikt-sans);
font-size: 13px;
font-weight: 500;
padding: 6px 14px;
border-radius: var(--wikt-r-pill);
cursor: pointer;
transition: background 0.12s ease, border-color 0.12s ease, color 0.12s ease;
}
.dsb-queue-save {
background: var(--wikt-accent);
border: 1px solid var(--wikt-accent);
color: #fff;
}
.dsb-queue-save:hover {
background: var(--wikt-accent-deep);
border-color: var(--wikt-accent-deep);
}
.dsb-queue-save:disabled {
opacity: 0.55;
cursor: progress;
}
.dsb-queue-cancel {
background: transparent;
border: 1px solid var(--wikt-field-edge);
color: var(--wikt-ink-3);
}
.dsb-queue-cancel:hover {
border-color: var(--wikt-ink-3);
color: var(--wikt-ink);
}
.dsb-queue-status {
margin-top: 6px;
font-size: 12.5px;
color: var(--wikt-muted);
}
.dsb-queue-status.dsb-status-ok { color: var(--wikt-ok); }
.dsb-queue-status.dsb-status-error { color: var(--wikt-danger); }
/* ── Minerva (mobile) ────────────────────────────────────────────────
Narrower viewport: keep the panel on screen and give every tap
target the form's 44px minimum. */
@media (max-width: 720px), (pointer: coarse) {
.dsb-quick-row {
font-size: 13px;
margin-left: 20px;
}
.dsb-input,
.dsb-subsection-select,
.dsb-convert-pos-select {
min-height: 40px;
/* >=16px stops iOS Safari zooming the page on focus. */
font-size: 16px;
}
.dsb-tagfield .dsb-input {
min-height: 32px;
}
.dsb-tag-remove {
padding: 0 6px;
}
.dsb-save,
.dsb-cancel,
.dsb-autolink,
.dsb-queue-save,
.dsb-queue-cancel {
min-height: 40px;
padding: 8px 16px;
}
.dsb-queue-box {
top: auto;
bottom: 8px;
right: 8px;
left: 8px;
width: auto;
}
}
/* ── Dark mode ───────────────────────────────────────────────────────
Same warm dark palette and same explicit-signal-only rule as the
form's stylesheet: no bare prefers-color-scheme fallback, since
legacy Vector sets no theme class and would go dark on its own. */
html.skin-theme-clientpref-night .dsb-save,
html.skin-nightmode .dsb-save,
html.skin-theme-clientpref-night .dsb-queue-save,
html.skin-nightmode .dsb-queue-save {
color: #14213a;
}
@media screen and (prefers-color-scheme: dark) {
html.skin-theme-clientpref-os .dsb-save,
html.skin-theme-clientpref-os .dsb-queue-save {
color: #14213a;
}
}
/* Rules the menu's three sections apart (see MENU_GROUPS). */
.dsb-more-menu .dsb-menu-group-start {
margin-top: 7px;
padding-top: 7px;
border-top: 1px solid var(--wikt-rule-soft);
}
/* Deleting a sense is destructive, so it reads in the danger colour -- but it shares the last section
with "Maneyeke din zêde bike" (both act on the sense itself), so it carries no rule of its own. */
.dsb-more-menu .dsb-field-delete {
color: var(--wikt-danger);
}
.dsb-more-menu .dsb-field-delete:hover,
.dsb-more-menu .dsb-field-delete:focus {
color: var(--wikt-danger);
text-decoration: underline;
}
/* A sense queued for deletion, struck through until the queue is saved or the change dropped. */
li.dsb-sense-doomed {
text-decoration: line-through;
text-decoration-color: var(--wikt-danger);
}
li.dsb-sense-doomed .dsb-quick-row {
text-decoration: none;
}
/* Queue confirmation for row actions that have no field box of their own. */
.dsb-quick-note {
display: block;
margin-top: 6px;
}
/* Dropped by anything that cannot live in a 520px column: a long etymology, and a word list wide
enough to have gone to two columns. */
.dsb-subsection-panel.dsb-panel-wide {
max-width: none;
}
/* Word-list groups: one "{{kol3}}" per block, told apart only when there is more than one. */
.dsb-wordgroups-many .dsb-wordgroup {
padding-top: 10px;
border-top: 1px solid var(--wikt-rule-soft);
}
.dsb-wordgroups-many .dsb-wordgroup:first-child {
padding-top: 0;
border-top: 0;
}
.dsb-group-remove {
color: var(--wikt-danger);
}
/* A sernav-headed group inside an unsaved-section preview. */
.dsb-preview-subheading {
margin-top: 6px;
font-weight: 600;
}
/* ── Werger box form ──
The {{werger-ser}} parameters. Reuses .dsb-input / .dsb-adv-label from the other panels; only the
note and the disclosure body need anything of their own. */
.dsb-werger-note {
margin: 0 0 10px;
color: var(--wikt-muted);
font-size: 12px;
line-height: 1.5;
}
.dsb-werger-main {
display: flex;
flex-direction: column;
gap: 10px;
}
/* The box is nested inside its label, so it is made a block to sit under the label text instead of
beside it -- inline, a short label like "id" ends up behind its own box. */
.dsb-werger-row .dsb-adv-label {
display: block;
margin-bottom: 0;
}
.dsb-werger-row .dsb-input {
display: block;
width: 100%;
margin-top: 4px;
}
/* ── Îzaha maneyî ──
Opens under the Mane box; the two are one gloss, so it sits tight against it. */
.dsb-izah-wrap {
margin-top: 6px;
}
.dsb-izah-wrap .dsb-adv-label {
display: block;
}
.dsb-izah-wrap .dsb-input {
display: block;
width: 100%;
margin-top: 4px;
}
/* A sense redrawn from the change that is queued for it, or a whole new sense waiting in the list's own
numbering. No badge and no frame: the point is that it reads the way it will read once saved. The tint
is the only sign, and the queue box is what actually says nothing is published yet. */
.dsb-sense-preview {
background: var(--wikt-accent-tint);
box-shadow: -3px 0 0 var(--wikt-accent-edge);
}
/* Kept in the tree so another gadget's form still has somewhere to open, but out of sight while the
preview above draws the same content. */
.dsb-preview-hidden {
display: none !important;
}
li.dsb-sense-preview {
display: list-item;
padding-left: 4px;
}
/* </nowiki> */
fekfatcdtyhisr91y2eya3rm9fkepzy
Bikarhêner:Wikihez/Gadget-test/cite.js
2
1358913
6531953
6531662
2026-09-17T14:39:03Z
Wikihez
7344
Template dialog opens one at a time; queued section conversions apply from the bottom of the page up
6531953
javascript
text/javascript
// <nowiki>
/*
* Page: [[MediaWiki:Gadget-wikt.cite.js]]
*
* Citations for both add-examples and the wizard: jêder-* templates, [[Kategorî:Çavkanî]] templates,
* Citoid, and the "#*" line they become. fieldSpecs() returns plain field descriptions because the two
* callers draw them differently (OOUI vs the wizard's own grid); everything else is shared. Also the
* template-param dialog (openTemplateDialog), used the same way by DefSideBoxes and start.js.
*
* The only caller of window.wikt.citoid, which the citoid-helper gadget owns.
*/
(function () {
"use strict";
var edit = require("ext.gadget.wikt.edit");
var util = edit.util;
var wt = edit.wt;
var langs = edit.langs;
var escapeRegExp = util.escapeRegExp;
var tdText = wt.tdText;
var parseTemplateCalls = wt.parseTemplateCalls;
var splitCallParams = wt.splitCallParams;
var buildTemplateCall = wt.buildTemplateCall;
var jederDefs = edit.data.jederDefs || {};
var api = new mw.Api();
// Sentinel _currentJederKey / dropdown value for "pick a template from Kategorî:Çavkanî". Not a real
// jederDefs key. Both gadgets use this same string, so a draft saved by one is read by the other.
var CATEGORY_KEY = "__category__";
// Sentinel for "the example box holds a finished template; write it out untouched".
var RAW_KEY = "__raw__";
// Covered by the form's own dedicated boxes, so they are never drawn again as generic template fields.
var COVERED_PARAMS = ["jêgirtin", "jg", "t", "tr"];
// A Kategorî:Çavkanî template whose TemplateData labels a parameter exactly this is fed the language
// from the language dropdown instead of showing a box for it. Unlike jêder-*, an arbitrary template has
// no "parameter 1 is the language" convention, so nothing is assumed positionally.
var LANG_LABEL = "koda zimanî";
// Field key -> {span, multiline}, taken from whichever jêder-* template defines it first. Most
// Kategorî:Çavkanî templates reuse the same bibliographic parameter names, so "sernav" comes out
// full-width and multiline there too rather than as a generic one-line box.
var SIZE_BY_KEY = {};
Object.keys(jederDefs).forEach(function (jederKey) {
(jederDefs[jederKey].fields || []).forEach(function (field) {
if (!SIZE_BY_KEY[field.key]) {
SIZE_BY_KEY[field.key] = {
span: field.span || 4,
multiline: !!field.multiline,
width: field.width || ""
};
}
});
});
// Field pairs Modul:quote refuses together ("Divê tenê yek ji |tarîx= an |sal= were dayîn"): a full
// date and a bare year say the same thing twice. Both are offered because either may be all a source
// gives, so the forms clear one when the other is filled rather than letting the citation break.
var EXCLUSIVE_PAIRS = [["sal", "tarîx"]];
function exclusivePartner(key) {
var found = null;
EXCLUSIVE_PAIRS.forEach(function (pair) {
if (pair[0] === key) {
found = pair[1];
} else if (pair[1] === key) {
found = pair[0];
}
});
return found;
}
function fieldSize(key) {
return SIZE_BY_KEY[key] || { span: 4, multiline: false, width: "" };
}
// ---- text helpers -------------------------------------------------------------------------------
// The entry name as a pattern. A Kurdish word ending in -î loses the circumflex once something is
// attached to it ("safî" is written "safiye"), so a title ending that way matches either spelling --
// otherwise the name is simply never found in the quotation.
function titlePattern(title) {
var escaped = escapeRegExp(String(title || ""));
return /î$/i.test(escaped) ? escaped.slice(0, -1) + "[îi]" : escaped;
}
// Wraps the entry's own name in bold wherever it appears in the quotation, which is how quotations are
// written here, and reports whether the name ends up bold so a caller can say so.
//
// The name may carry an attached suffix ("celbê"), so the match runs on to the end of the word. A word
// ends at whitespace, punctuation or markup: excluding markup is what keeps "celb<br>me’şûq" from
// bolding as one lump. The typographic apostrophe stays a word character -- it is one inside Kurdish
// words like "me’şûq".
function boldenPageTitle(text, title) {
var source = String(text || "");
var escapedTitle = titlePattern(title);
var suffixPattern = "([^\\s.,;:?!()'\"<>\\[\\]{}|=/\\\\«»„“”–—]+)?";
var titleRe = new RegExp("(" + escapedTitle + suffixPattern + ")", "ig");
var titleAnywhere = new RegExp(escapedTitle, "i");
// Whatever already sits between ''' marks is copied through untouched, so a quotation somebody
// bolded by hand -- or one an earlier run bolded -- never collects a second pair.
var boldSpan = /'''[\s\S]*?'''/g;
var out = "";
var last = 0;
var alreadyBold = false;
var span;
while ((span = boldSpan.exec(source)) !== null) {
out += source.slice(last, span.index).replace(titleRe, "'''$1'''");
if (titleAnywhere.test(span[0])) {
alreadyBold = true;
}
out += span[0];
last = span.index + span[0].length;
}
out += source.slice(last).replace(titleRe, "'''$1'''");
return { text: out, bolded: out !== source || alreadyBold };
}
// A literal newline would end the single-line "#*" wikitext, so display text keeps its break as <br>...
function nlToBr(text) {
return String(text || "").replace(/\r\n|\r|\n/g, "<br>");
}
// ...while a plain parameter value (an author, a title) just loses it.
function nlToSpace(text) {
return String(text || "").replace(/\s*(?:\r\n|\r|\n)\s*/g, " ");
}
// Bolds the entry name inside a pasted citation's jêgirtin=/jg= value only, so a book title or author
// containing it is left alone. With no such parameter nothing is bolded: running the plain-text bolder
// over wikitext would wrap the braces too.
function boldQuoteParam(text, title) {
var match = /(\|\s*(?:jêgirtin|jg)\s*=)([\s\S]*?)(?=\|[^|=]*=|\}\}\s*$)/.exec(text);
if (!match) {
return text;
}
var bolded = boldenPageTitle(match[2], title).text;
return text.slice(0, match.index) + match[1] + bolded + text.slice(match.index + match[0].length);
}
// "name — description" for each Kategorî:Çavkanî option in a <select>, from the description half of
// the same TemplateData every citation picker already fetches -- so a search box built over these
// options can also match on what a template documents itself as doing, not only its name. An
// undescribed option, or a failed fetch, just keeps its plain label; never blocks on this.
// A self-contained "search Kategorî:Çavkanî" combo: a text input plus its own results list, matching
// add-examples' own jêgirtin picker -- each row shows its label, a " → target" arrow when a redirect's
// label differs from its target, and a 🔗 link to the canonical template's own page. opts.onPick(entry)
// fires when a row is chosen; the input is then left holding the plain label, not "label → target", so
// re-focusing it to search again doesn't start from that arrow text. opts.describe: true also fills in
// each visible row's own TemplateData description once it loads (batched, so scrolling through matches
// costs a handful of requests, not one per row).
function buildCategoryTemplateSearch(opts) {
opts = opts || {};
var shell = util.buildSearchList({
placeholder: opts.placeholder || util.msg.searchTemplate
});
var $input = shell.$input;
var $results = shell.$results;
var $status = $("<span class='wiktku-cat-tpl-status'></span>").text(util.msg.templatesLoading);
var list = [];
function decorate($text, target, wanted) {
if (!wanted) {
return;
}
edit.wt.fetchTemplateData(target).then(function (doc) {
var desc = doc && edit.wt.tdText(doc.description);
if (desc && $text.closest("body").length) {
$text.append(document.createTextNode(" — " + edit.wt.shortDesc(desc)));
}
});
}
function showResults(query) {
var q = $.trim(query || "").toLowerCase();
$results.empty();
var matched = q ? list.filter(function (entry) {
return entry.label.toLowerCase().indexOf(q) !== -1;
}) : list;
if (!matched.length) {
shell.close();
return;
}
var shown = matched.slice(0, 50);
// Only what a search has narrowed to: an empty box is the whole category, and describing its
// first fifty would ask the wiki about templates nobody has looked for.
var describing = !!opts.describe && !!q;
if (describing) {
edit.wt.fetchTemplateDataBatch(shown.map(function (entry) { return entry.target; }));
}
shown.forEach(function (entry) {
var $li = $("<li class='wiktku-cat-tpl-row'></li>");
var $text = $("<span class='wiktku-cat-tpl-text'></span>").text(entry.label);
if (entry.label !== entry.target) {
$text.append($("<span class='wiktku-cat-tpl-arrow'></span>").text(" → " + entry.target));
}
decorate($text, entry.target, describing);
var $link = $("<a class='wiktku-cat-tpl-link' target='_blank' rel='noopener'>🔗</a>")
.attr("title", util.msg.openTemplatePage)
.attr("href", util.tplPageUrl(entry.target));
// stopPropagation only -- preventDefault would stop the link from actually navigating.
$link.on("mousedown click", function (e) { e.stopPropagation(); });
$li.append($text, $link);
shell.bindPick($li, function () {
shell.close();
$input.val(entry.label);
opts.onPick(entry);
});
$results.append($li);
});
shell.open();
}
$input.on("input focus", function () {
showResults($(this).val());
});
$input.on("blur", function () {
setTimeout(shell.close, 200); // lets the pick handler fire first
});
fetchCategoryTemplates().then(function (l) {
list = l;
$status.text("");
}, function () {
$status.text(util.msg.templatesFailed);
});
return {
$element: $("<span class='wiktku-native-select-wrap wiktku-cat-tpl-picker'></span>").append($input, $results, $status),
$input: $input,
$status: $status
};
}
// ---- Kategorî:Çavkanî -----------------------------------------------------------------------------
// The category's template members plus their redirects, each redirect its own searchable entry pointing
// at the same canonical target, so a template is findable by any of its names. Cached for the session.
var catTemplatesPromise = null;
function fetchCategoryTemplates() {
if (catTemplatesPromise) {
return catTemplatesPromise;
}
catTemplatesPromise = api.get({
action: "query",
generator: "categorymembers",
gcmtitle: "Kategorî:Çavkanî",
gcmnamespace: 10,
gcmlimit: "max",
prop: "redirects",
rdnamespace: 10,
rdlimit: "max",
formatversion: 2
}).then(function (data) {
var list = [];
((data.query && data.query.pages) || []).forEach(function (page) {
var target = page.title.replace(/^[^:]+:/, "");
list.push({ label: target, target: target });
(page.redirects || []).forEach(function (redirect) {
list.push({ label: redirect.title.replace(/^[^:]+:/, ""), target: target });
});
});
list.sort(function (a, b) {
return a.label.localeCompare(b.label, mw.config.get("wgUserLanguage"));
});
return list;
}, function () {
return [];
});
return catTemplatesPromise;
}
// ---- template-param dialog: fills a {{Name|...}} call from its own TemplateData -------------------
// Shared by DefSideBoxes (a fresh insert from Etîmolojî/Çavkanî, or double-clicking an existing chip
// to edit it) and start.js's own Etîmolojî/Çavkanî pickers -- one dialog, so all of them read the same
// TemplateData the same way.
// OOUI's own dialog stack -- the same widgetry VisualEditor's template dialog is built on.
var citeWindowManager = null;
function templateDialogManager() {
if (!citeWindowManager) {
citeWindowManager = new OO.ui.WindowManager();
$(document.body).append(citeWindowManager.$element);
}
return citeWindowManager;
}
// Opens one template call in a modal built from its TemplateData. Resolves with the edited wikitext, or
// null when the user cancels.
// One at a time: a second open while one is loading or showing (a chip clicked again before its dialog
// appeared) stacks a window the manager does not track, and that window's buttons do nothing. The
// second call resolves null, as a cancel does.
var templateDialogBusy = false;
function openTemplateDialog(callWikitext) {
var done = $.Deferred();
var call = parseTemplateCalls(callWikitext).calls[0];
if (!call || templateDialogBusy) {
return done.resolve(null).promise();
}
templateDialogBusy = true;
done.always(function () {
templateDialogBusy = false;
});
$.when(
mw.loader.using(["oojs-ui-core", "oojs-ui-windows"]),
edit.wt.fetchTemplateData(call.name)
).then(function (_loaded, td) {
var result = null;
// The parent is captured rather than reached through TplDialog.super: "super" is a reserved word,
// and OOjs has moved between .super and .parent for it over the years.
var ParentDialog = OO.ui.ProcessDialog;
function TplDialog(config) {
ParentDialog.call(this, config);
}
OO.inheritClass(TplDialog, ParentDialog);
TplDialog.static.name = "citeTemplateDialog";
TplDialog.static.title = "{{" + call.name + "}}";
TplDialog.static.actions = [
{ action: "save", label: "Biguhêre", flags: ["primary", "progressive"] },
{ action: "remove", label: "Jê bibe", flags: "destructive" },
{ label: util.msg.cancel, flags: "safe" }
];
TplDialog.prototype.initialize = function () {
ParentDialog.prototype.initialize.apply(this, arguments);
this.panel = new OO.ui.PanelLayout({ padded: true, expanded: false });
this.form = buildTemplateParamForm(call, td);
this.panel.$element.append(this.form.$element);
this.$body.append(this.panel.$element);
};
TplDialog.prototype.getBodyHeight = function () {
return this.panel.$element.outerHeight(true);
};
TplDialog.prototype.getActionProcess = function (action) {
var dialog = this;
if (action === "save" || action === "remove") {
return new OO.ui.Process(function () {
result = action === "remove" ? "" : dialog.form.values();
dialog.close();
});
}
return ParentDialog.prototype.getActionProcess.call(this, action);
};
var manager = templateDialogManager();
var dialog = new TplDialog({ size: "medium" });
manager.addWindows([dialog]);
var instance = manager.openWindow(dialog);
instance.closed.then(function () {
manager.clearWindows();
done.resolve(result);
});
instance.opening.fail(function () {
manager.clearWindows();
done.resolve(null);
});
}, function () {
done.resolve(null);
});
return done.promise();
}
// TemplateData descriptions are plain text, but authors write wikilinks in them; render those as links.
function tdDescription(text) {
var $out = $("<span></span>");
var re = /\[\[([^\[\]|]+)(?:\|([^\[\]]*))?\]\]/g;
var last = 0;
var m;
while ((m = re.exec(text)) !== null) {
if (m.index > last) {
$out.append(document.createTextNode(text.slice(last, m.index)));
}
$out.append($("<a></a>").attr("href", mw.util.getUrl(m[1])).text(m[2] || m[1]));
last = re.lastIndex;
}
if (last < text.length) {
$out.append(document.createTextNode(text.slice(last)));
}
return $out;
}
// A parameter whose documentation says so takes "kelîme<t:...>" inline modifiers, so it gets the pill
// widget instead of a plain box. TemplateData has no field for this, hence the marker in the description.
var INLINE_MODIFIER_MARKER = "WF:Guherkerên rêzê";
function acceptsInlineModifiers(doc) {
return tdText(doc && doc.description).indexOf(INLINE_MODIFIER_MARKER) !== -1;
}
// A language-code parameter, per its own label.
function isLanguageParam(doc) {
return /ziman/i.test(tdText(doc && doc.label));
}
// 3693 languages is far too many to rebuild per dialog (~190ms), and a datalist can be shared by id, so
// this is built once and reused by every language box on the page.
var LANG_DATALIST_ID = "dsb-language-list";
function fillLanguageDatalist($list) {
var names = langs.names();
$list.empty();
Object.keys(names).forEach(function (code) {
$list.append($("<option></option>").val(code).attr("label", names[code]));
});
}
function languageDatalistId() {
if (!document.getElementById(LANG_DATALIST_ID)) {
var $list = $("<datalist></datalist>").attr("id", LANG_DATALIST_ID);
fillLanguageDatalist($list);
$(document.body).append($list);
// Refilled in place if the long list ever arrives, so every box using it grows at once.
langs.onFull(function () {
fillLanguageDatalist($("#" + LANG_DATALIST_ID));
});
}
return LANG_DATALIST_ID;
}
function buildTemplateParamForm(call, td) {
var parsed = splitCallParams(call.params);
var docParams = (td && td.params) || {};
var keys = (td && td.paramOrder) ? td.paramOrder.slice() : Object.keys(docParams);
parsed.used.forEach(function (k) {
if (keys.indexOf(k) === -1) {
keys.push(k);
}
});
var $element = $("<div class='dsb-tpl-form'></div>");
var readers = {}; // key -> () => the parameter's current text
$element.append($("<div class='dsb-tpl-name'></div>").append(
$("<a></a>").attr("href", mw.util.getUrl("Şablon:" + call.name))
.attr("target", "_blank").text("{{" + call.name + "}}")
));
var desc = tdText(td && td.description);
if (desc) {
$element.append($("<div class='dsb-tpl-desc'></div>").append(tdDescription(desc)));
}
keys.forEach(function (key) {
var doc = docParams[key] || {};
var value = parsed.values[key] || "";
var $field;
if (acceptsInlineModifiers(doc)) {
// The same word row the sense lists use: plain text plus removable "<t:...>" pills.
var row = util.makeModifierRow({
prefix: "dsb-",
placeholder: tdText(doc.label) || key,
modTitle: util.msg.addModifier,
removeTitle: "Vê nirxê jê bibe",
removeModTitle: util.msg.removeModifier
});
row.setRaw(value);
// A <ul> so the row's own "close other pickers" lookup has something to scope to.
$field = $("<ul class='dsb-wordlist dsb-tpl-modifier-list'></ul>").append(row.$li);
readers[key] = row.getRaw;
} else {
var $input = $("<input type='text' class='dsb-input'>").val(value);
if (isLanguageParam(doc)) {
$input.attr("list", languageDatalistId()).attr("placeholder", "koda zimanî…");
// Typing a language the short list does not have is the cue to fetch the rest.
$input.on("input", function () {
if (langs.missingFromCore($input.val())) {
langs.ensureFull();
}
});
} else {
var suggested = doc.suggestedvalues || [];
if (suggested.length) {
var listId = "dsb-sv-" + call.name.replace(/[^A-Za-z0-9]/g, "") + "-" + key;
var $list = $("<datalist></datalist>").attr("id", listId);
suggested.forEach(function (v) {
$list.append($("<option></option>").val(v));
});
$input.attr("list", listId);
$element.append($list);
}
}
$field = $input;
readers[key] = function () {
return $input.val();
};
}
var $label = $("<label class='dsb-adv-label'></label>").text(tdText(doc.label) || key);
if (!docParams[key]) {
$label.append($("<span class='dsb-tpl-undocumented'></span>").text(" (ne di belgeyê de)"));
}
var $row = $("<div class='dsb-tpl-param-row'></div>").append($label, $field);
var help = tdText(doc.description);
if (help) {
$row.append($("<div class='dsb-tpl-help'></div>").append(tdDescription(help)));
}
if (doc.deprecated) {
$row.addClass("dsb-tpl-deprecated");
}
$element.append($row);
});
return {
$element: $element,
values: function () {
var vals = {};
keys.forEach(function (k) {
vals[k] = $.trim(readers[k]() || "");
});
return buildTemplateCall(call.name, vals, keys);
}
};
}
// The documented parameters of one template (name without the "Şablon:" prefix), in its own
// paramOrder; [] when it has no TemplateData. The fetch, the shaping and the cache all live in
// wikt.edit, so the wizard, DefSideBoxes and this module share one request and one copy per template.
//
// The key stands in for a missing label, because these become form fields and a field needs something
// written on it.
// A template's parameters: its TemplateData, or -- for one with none, like most Kategorî:Çavkanî
// templates -- every {{{name}}} its own source reads.
function templateParams(name) {
return edit.wt.templateDoc(name).then(function (td) {
var params = ((td && td.params) || []).map(function (p) {
return {
key: p.key,
label: p.label || p.key,
required: p.required,
deprecated: p.deprecated
};
});
return params.length ? params : sourceParams(name);
});
}
var sourceParamsCache = {};
function sourceParams(name) {
if (!sourceParamsCache[name]) {
sourceParamsCache[name] = new mw.Api().get({
action: "query", prop: "revisions", titles: "Template:" + name, rvprop: "content", rvslots: "main",
redirects: 1, formatversion: 2
}).then(function (data) {
var page = data.query.pages[0];
var source = page.revisions ? page.revisions[0].slots.main.content : "";
var keys = [];
var re = /\{\{\{\s*([^{}|]+?)\s*[|}]/g;
var m;
while ((m = re.exec(source)) !== null) {
if (!/^\d+$/.test(m[1]) && keys.indexOf(m[1]) === -1) {
keys.push(m[1]);
}
}
return keys.map(function (key) {
return { key: key, label: key, required: false, deprecated: false };
});
}, function () {
return [];
});
}
return sourceParamsCache[name];
}
// alias -> the name a template's TemplateData documents it under ({r: "rûpel"} for jêder-kitêb). Asked
// of the wiki itself: the parameter lists baked into data-main.json carry no aliases.
var aliasCache = {};
function templateAliases(name) {
if (!aliasCache[name]) {
aliasCache[name] = new mw.Api().get({
action: "templatedata", titles: "Template:" + name, redirects: 1, formatversion: 2
}).then(function (data) {
var map = {};
$.each(data.pages || {}, function (_, page) {
$.each(page.params || {}, function (key, p) {
(p.aliases || []).forEach(function (alias) {
map[alias] = key;
});
});
});
return map;
}, function () {
return {};
});
}
return aliasCache[name];
}
// What to draw for one Kategorî:Çavkanî template: a field per documented parameter, minus the ones the
// form already has boxes for and minus the language parameter, which is filled from the dropdown.
// A template with no TemplateData at all falls back to a lone "Rûpel" box, which is what such citations
// almost always need ({{Şablon|rûpel=12|jêgirtin=…}}).
function fieldSpecs(name) {
return templateParams(name).then(function (params) {
var langParamKey = null;
(params || []).forEach(function (p) {
if ((p.label || "").trim().toLowerCase() === LANG_LABEL) {
langParamKey = p.key;
}
});
var filtered = (params || []).filter(function (p) {
return COVERED_PARAMS.indexOf(p.key) === -1 && p.key !== langParamKey;
});
var list = filtered.length ? filtered : [{ key: "rûpel", label: "Rûpel", required: false }];
return {
langParamKey: langParamKey,
fields: list.map(function (p) {
var size = fieldSize(p.key);
return {
key: p.key,
label: p.label,
required: p.required,
span: size.span,
width: size.width,
multiline: size.multiline
};
})
};
});
}
// The same list for a jêder-* template, straight out of the shared table.
function jederFieldSpecs(jederKey) {
var def = jederDefs[jederKey];
return def ? (def.fields || []).slice() : [];
}
// ---- Citoid --------------------------------------------------------------------------------------
// Today, as Modul:quote wants it (it renders "2024-05-05" as "5 gulan 2024").
function today() {
var d = new Date();
function pad(n) { return (n < 10 ? "0" : "") + n; }
return d.getFullYear() + "-" + pad(d.getMonth() + 1) + "-" + pad(d.getDate());
}
// Carries across the parts of a Citoid result that citoid-helper's own mapping drops: the access date
// and the identifiers for the fields the forms have only just gained. Applied here rather than in
// citoid-helper so it reaches both gadgets without an edit to the MediaWiki: gadget page.
function enrichCitoidFields(fields, raw) {
fields = fields || {};
if (fields.url && !fields["tarîxa-gihiştinê"]) {
// Citoid reports when it actually fetched the page ("accessDate": "2026-09-03"), so use that
// rather than the clock -- they differ whenever the answer came from a cache. today() is only
// the fallback for a result that carries no accessDate.
var accessed = raw && (raw.accessDate || raw.accessdate);
fields["tarîxa-gihiştinê"] = (accessed && String(accessed).slice(0, 10)) || today();
}
if (raw) {
var issn = Array.isArray(raw.ISSN) ? raw.ISSN[0] : raw.ISSN;
if (issn && !fields.issn) {
fields.issn = issn;
}
var doi = Array.isArray(raw.DOI) ? raw.DOI[0] : raw.DOI;
if (doi && !fields.doi) {
fields.doi = doi;
}
if (raw.volume && !fields.cild) {
fields.cild = raw.volume;
}
}
return fields;
}
// One URL/DOI/ISBN -> {jederKey, fields}. The lookup itself lives in the citoid-helper gadget; this
// wrapper exists so neither caller has to know that, and so a missing gadget fails in one place.
function fetchFromCitoid(query) {
if (!(window.wikt && window.wikt.citoid && window.wikt.citoid.fetchJederFields)) {
return $.Deferred().reject("Amûra Citoidê ne barkirî ye.").promise();
}
return window.wikt.citoid.fetchJederFields(query).then(function (result) {
result.fields = enrichCitoidFields(result.fields, result.raw);
return result;
});
}
// Re-derives the fields for a different jêder-* template than the one citoid detected, enriched the
// same way -- used when the person had already chosen a template and we keep their choice.
// The form's link box stands in for a jêder-* template's own url= when that is left empty.
function withLinkAsUrl(jederKey, fields, link) {
var def = jederDefs[jederKey];
var hasUrl = !!def && (def.fields || []).some(function (field) {
return field.key === "url";
});
if (hasUrl && !fields.url && /^https?:\/\//.test(link || "")) {
fields.url = link;
}
return fields;
}
function citoidFieldsFor(jederKey, raw, query) {
if (!(window.wikt && window.wikt.citoid && window.wikt.citoid.dataToJederFields)) {
return {};
}
return enrichCitoidFields(window.wikt.citoid.dataToJederFields(jederKey, raw, query), raw);
}
// ---- the "#*" line ------------------------------------------------------------------------------
// One finished citation line. Modes:
// "jeder" {{jêder-kitêb|ku|sernav=…|jêgirtin=…}}
// "category" the Kategorî:Çavkanî template as the citation template: {{Ç:Kitêb|rûpel=12|…}}
// "plain" free-text source, or none: {{jêder|ku|2=…|jêgirtin=…}} / {{nimûne|ku|…}}
// "raw" a finished template, passed through with only the entry name bolded
// opts: {mode, template, language, fields, langParamKey, quote, translation, transcription,
// source, sourceURL, title, marker, keepTranslation, extra}
function buildCitation(opts) {
var marker = opts.marker || "#*";
var title = opts.title || mw.config.get("wgTitle");
var quote = boldenPageTitle(String(opts.quote || ""), title).text;
if (opts.mode === "raw") {
// Written as given: the point is to paste a citation built elsewhere and have it survive intact.
// A line copied off another entry usually still carries its own "#*"/"#:" -- the marker is added
// back below, so strip it rather than emitting "#* #* {{...}}".
var pasted = String(opts.quote || "").replace(LIST_MARKER, "");
return marker + " " + nlToBr(boldQuoteParam(pasted, title)).trim();
}
var isCategory = opts.mode === "category";
var isTemplate = isCategory || opts.mode === "jeder";
var name = isTemplate ? opts.template : (opts.source || opts.sourceURL ? "jêder" : "nimûne");
var line = marker + " {{" + name;
// jêder-*, jêder and nimûne all take the language as parameter 1. A Kategorî:Çavkanî template only
// gets one when its own TemplateData names it, under its own key -- never assumed positionally.
if (!isCategory) {
line += "|" + opts.language;
}
if (isTemplate) {
var fields = opts.fields || {};
Object.keys(fields).forEach(function (key) {
var value = nlToSpace(String(fields[key] || "").trim());
if (value) {
line += "|" + key + "=" + value;
}
});
if (isCategory && opts.langParamKey) {
line += "|" + opts.langParamKey + "=" + opts.language;
}
// "Ç:" templates are quotation-equivalents rather than usage examples (see Modul:quote).
if (isCategory && /^Ç:/.test(name)) {
line += "|wekjêgirtin=1";
}
} else {
if (opts.source) {
// A positional value containing "=" would be read as its own key=value pair, so name it.
line += (String(opts.source).indexOf("=") !== -1 ? "|2=" : "|") + opts.source;
}
if (opts.sourceURL) {
line += "|url=" + opts.sourceURL;
}
}
var isUsex = name === "nimûne";
var translation = nlToBr(String(opts.translation || "").trim());
// {{nimûne}} always takes one, a Kurmancî example's being its explanation; a quotation's is left out
// for Kurmancî unless the edited line already had one (keepTranslation).
var writeTranslation = !!translation && (isUsex || opts.language !== "ku" || opts.keepTranslation);
// {{nimûne}}'s example is parameter 2 and its translation parameter 3, both positional. A value
// containing "=" would be read as key=value, so then both are named: naming only the example would
// slide the translation into slot 2.
var namedSlots = isUsex && (quote.indexOf("=") !== -1 || (writeTranslation && translation.indexOf("=") !== -1));
line += "|" + (isUsex ? (namedSlots ? "2=" : "") : "jêgirtin=") + nlToBr(quote);
if (writeTranslation) {
// Kategorî:Çavkanî templates route through Modul:quote and take its "t="; jêder-* uses "tercime=".
line += "|" + (isUsex ? (namedSlots ? "3=" : "") : isCategory ? "t=" : "tercime=") + translation;
}
var transcription = nlToBr(String(opts.transcription || "").trim());
if (transcription) {
line += "|tr=" + transcription;
}
// Parameters the form has no box for, carried through from the line being edited.
(opts.extra || []).forEach(function (param) {
line += "|" + param;
});
// A short example and its translation read best on one line, which {{nimûne}} does only when told.
// Short is Modul:usex's own measure for "rêz=auto": at most 100 characters shown between them.
if (isUsex && writeTranslation && !hasInlineParam(opts.extra) &&
shownLength(quote) + shownLength(translation) <= 100) {
line += "|rêz=1";
}
return line + "}}";
}
// True when the parameters already say inline or not: {{nimûne}}'s rêz=, under any of its names.
function hasInlineParam(params) {
return (params || []).some(function (param) {
return /^\s*(?:rêz|birêz|inline)\s*=/.test(param);
});
}
// How many characters a bit of wikitext shows: link targets, bold/italic marks and tags don't count.
function shownLength(text) {
return String(text || "")
.replace(/\[\[(?:[^\]|]*\|)?([^\]]*)\]\]/g, "$1")
.replace(/'{2,}/g, "")
.replace(/<[^>]*>/g, " ")
.length;
}
// A leading "#*" / "#:" / "##*" ... list marker.
var LIST_MARKER = /^\s*#+[*:]\s*/;
// True when the text is a whole citation line copied from somewhere else: its own list marker followed
// by a template call. That is the shape of every line this family writes -- {{jêder}}, {{jêder-kitêb}},
// {{nm}}, {{nimûne}}, {{mk}}, any Kategorî:Çavkanî template -- so the template name is not checked
// against a list, which could never cover every citation template on the wiki.
//
// Used to switch the form into raw mode by itself: pasting a finished line and forgetting to change the
// dropdown would otherwise wrap it in a second {{nimûne}}.
function looksLikeCitationLine(text) {
var trimmed = String(text || "");
if (!LIST_MARKER.test(trimmed)) {
return false;
}
return trimmed.replace(LIST_MARKER, "").slice(0, 2) === "{{";
}
// True when the example box holds nothing but one finished template call -- the cue to offer, or to
// switch to, the "write it out as-is" mode.
function looksLikeTemplate(text) {
var trimmed = String(text || "").trim();
if (trimmed.slice(0, 2) !== "{{" || trimmed.slice(-2) !== "}}") {
return false;
}
var parsed = edit.wt.parseTemplateCalls(trimmed);
return parsed.calls.length === 1 && !parsed.otherContent;
}
// ---- reading a finished line back --------------------------------------------------------------
// [[Şablon:nimûne]] and every redirect to it.
var USEX_NAMES = ["nimûne", "mk", "ux", "mînak", "nim", "nm"];
var USEX_TRANSLATION = ["3", "t", "tercime", "translation", "w", "werger"];
var USEX_TRANSCRIPTION = ["tr", "transliteration"];
// A quotation template's translation, under the name Modul:quote documents and its aliases.
var QUOTE_TRANSLATION = ["t", "tercime", "werger", "w"];
// [[Şablon:bênimûne]] and its redirects: the "no example yet" placeholder, with or without a language.
var EMPTY_EXAMPLE_MARKER = /^#+\*\s*\{\{\s*(?:bênimûne|bêmînak|mînak\?|nimûne\?)\s*(?:\|[^{}]*)?\}\}\s*$/i;
// The text of a line written as one italic run ("''…''", bold inside allowed -- "'''''X''' …''" opens
// with both), or null.
function italicRun(text) {
var t = $.trim(text || "");
var opening = /^'*/.exec(t)[0].length;
var m = /^''([\s\S]+)''$/.exec(t);
if ((opening !== 2 && opening !== 5) || !m || /''/.test(m[1].replace(/'''/g, ""))) {
return null;
}
return m[1];
}
function brToNl(text) {
return String(text || "").replace(/<br\s*\/?>/gi, "\n");
}
/**
* One "#:"/"#*" block -- its line plus any deeper "#::"/"#*:" lines -- as the form's values, or null when
* it is not an example: a word list, the "no example yet" placeholder, a plain note.
* {mode: "nimune"|"jeder"|"source"|"raw", marker, template, text, translation, transcription,
* source, sourceURL, fields, extra}
* extra is every parameter the form has no box for, to be written back as it was. A "raw" block is
* edited as written: text is the whole block, list markers included.
*/
function parseCitationLine(block) {
var all = String(block || "").split("\n");
var k = 1;
while (k < all.length && !/^#/.test(all[k])) {
k++;
}
var head = /^#+([:*])\s*([\s\S]*)$/.exec(all.slice(0, k).join("\n"));
var sub = all.slice(k);
if (!head || EMPTY_EXAMPLE_MARKER.test(all[0])) {
return null;
}
var out = { mode: "raw", marker: head[1], template: "", text: "", translation: "", transcription: "",
source: "", sourceURL: "", fields: {}, extra: [] };
var body = $.trim(head[2]);
var parsed = edit.wt.parseTemplateCalls(body);
var call = parsed.calls.length === 1 && !parsed.otherContent ? parsed.calls[0] : null;
var name = call ? edit.wt.canonicalTemplateKey(call.name) : "";
name = name.charAt(0).toLowerCase() + name.slice(1);
var isUsex = USEX_NAMES.indexOf(name) !== -1;
var def = call && jederDefs[name];
if (call && !sub.length && (isUsex || def || name === "jêder")) {
var split = edit.wt.splitCallParams(call.params);
var fieldKeys = def ? def.fields.map(function (f) { return f.key; }) : [];
out.mode = isUsex ? "nimune" : def ? "jeder" : "source";
out.template = def ? name : "";
split.used.forEach(function (key, i) {
var value = brToNl(split.values[key]);
if (key === "1") {
return; // the language, which the section already says
}
if (isUsex ? key === "2" : (key === "jêgirtin" || key === "jg")) {
out.text = value;
} else if ((isUsex ? USEX_TRANSLATION : QUOTE_TRANSLATION).indexOf(key) !== -1) {
out.translation = value;
} else if (USEX_TRANSCRIPTION.indexOf(key) !== -1) {
out.transcription = value;
} else if (def && fieldKeys.indexOf(key) !== -1) {
out.fields[key] = split.values[key];
} else if (name === "jêder" && key === "2") {
out.source = split.values[key];
} else if (name === "jêder" && key === "url") {
out.sourceURL = split.values[key];
} else {
// Named when it was positional, so it cannot shift into a slot the form now fills.
out.extra.push(/^\d+$/.test(key) && !/=/.test(call.params[i]) ? key + "=" + split.values[key] : $.trim(call.params[i]));
}
});
return out;
}
// Any other citation template on a "#*" line -- the Kategorî:Çavkanî ones, {{J:…}} and {{Ç:…}} --
// reads into the category mode: quote, translation and transcription in their boxes, every other
// named parameter as a field. Which of those get a box is known once the template's parameters
// are fetched (see applyAliases).
if (call && !sub.length && out.marker === "*" &&
(/^[JÇ]:/.test($.trim(call.name)) || /(?:^|\|)\s*(?:jêgirtin|jg)\s*=/.test(call.params.join("|")))) {
var catSplit = edit.wt.splitCallParams(call.params);
out.mode = "category";
out.template = $.trim(call.name);
catSplit.used.forEach(function (key, i) {
var value = catSplit.values[key];
if (key === "jêgirtin" || key === "jg") {
out.text = brToNl(value);
} else if (QUOTE_TRANSLATION.indexOf(key) !== -1) {
out.translation = brToNl(value);
} else if (USEX_TRANSCRIPTION.indexOf(key) !== -1) {
out.transcription = brToNl(value);
} else if (key === "wekjêgirtin" && /^Ç:/.test(out.template)) {
return; // buildCitation() writes it for every "Ç:" template
} else if (/^\d+$/.test(key)) {
out.extra.push(!/=/.test(call.params[i]) ? key + "=" + value : $.trim(call.params[i]));
} else {
out.fields[key] = value;
}
});
return out;
}
if (out.marker === ":") {
// The hand-written kind: "#: ''…''", its translation, if any, on a "#::" line under it.
var italic = italicRun(body);
var translationLine = sub.length === 1 ? /^#+::\s*([\s\S]*)$/.exec(sub[0]) : null;
if (italic !== null && (!sub.length || translationLine)) {
out.mode = "nimune";
out.text = brToNl(italic);
out.translation = translationLine ? brToNl(italicRun(translationLine[1]) || $.trim(translationLine[1])) : "";
return out;
}
if (!isUsex) {
return null;
}
}
out.text = all.join("\n");
return out;
}
/**
* Settles which parameters of a read line get a box. aliases maps an alias to its documented name
* (templateAliases); fieldKeys are the boxes the form drew. A parameter written under an alias -- "r="
* for "rûpel=" -- goes to its box; one with no box goes to extra, written back as it was.
*/
function applyAliases(example, aliases, fieldKeys) {
var pending = example.extra.slice();
Object.keys(example.fields).forEach(function (key) {
if (fieldKeys.indexOf(key) === -1) {
pending.push(key + "=" + example.fields[key]);
delete example.fields[key];
}
});
example.extra = [];
pending.forEach(function (param) {
var m = /^\s*([^=]+?)\s*=([\s\S]*)$/.exec(param);
var key = m ? (aliases[m[1]] || m[1]) : null;
var value = m ? $.trim(m[2]) : "";
if (m && fieldKeys.indexOf(key) !== -1 && !example.fields[key]) {
example.fields[key] = value;
} else if (m && key === "jêgirtin" && !example.text) {
example.text = brToNl(value);
} else if (m && QUOTE_TRANSLATION.indexOf(key) !== -1 && !example.translation) {
example.translation = brToNl(value);
} else {
example.extra.push(param);
}
});
return example;
}
/**
* newLine with its parameters in the order originalLine had them, so an edit's diff shows only what
* changed. Parameters match by name -- an alias counting as the name it stands for (aliases, from
* templateAliases) -- and positional ones by position; any the original lacked follow in newLine's own
* order. A line that is not one template call on either side comes back as it was.
*/
function keepOrder(newLine, originalLine, aliases) {
aliases = aliases || {};
var marker = /^#+[:*]\s*/.exec(newLine || "");
var oldBody = /^#+[:*]\s*([\s\S]*)$/.exec(originalLine || "");
if (!marker || !oldBody) {
return newLine;
}
var fresh = edit.wt.parseTemplateCalls(newLine.slice(marker[0].length));
var old = edit.wt.parseTemplateCalls($.trim(oldBody[1]));
if (fresh.calls.length !== 1 || fresh.otherContent || old.calls.length !== 1 || old.otherContent) {
return newLine;
}
function keyed(params) {
var positional = 0;
return params.map(function (raw) {
var m = /^\s*([^|={}\n]+?)\s*=/.exec(raw);
var key = m ? m[1] : String(++positional);
return { key: aliases[key] || key, raw: raw };
});
}
var rest = keyed(fresh.calls[0].params);
var ordered = [];
keyed(old.calls[0].params).forEach(function (o) {
for (var i = 0; i < rest.length; i++) {
if (rest[i].key === o.key) {
ordered.push(rest.splice(i, 1)[0]);
return;
}
}
});
return marker[0] + "{{" + fresh.calls[0].name + ordered.concat(rest).map(function (p) {
return "|" + p.raw;
}).join("") + "}}";
}
// Where the {{ … }} opened on lines[start] closes, so a template written over several lines stays whole.
function braceEnd(lines, start) {
var depth = 0;
for (var i = start; i < lines.length; i++) {
depth += (lines[i].match(/\{\{/g) || []).length - (lines[i].match(/\}\}/g) || []).length;
if (depth <= 0) {
return i;
}
}
return lines.length - 1;
}
// The "#:"/"#*" blocks right under the definition on lines[defIndex], each {start, end, text, example}:
// example is parseCitationLine's reading, or null for a block that is not an example.
function exampleBlocks(lines, defIndex) {
var depth = /^#+/.exec(lines[defIndex])[0].length;
var own = new RegExp("^#{" + depth + "}[:*]");
var deeper = new RegExp("^#{" + depth + "}[:*][:*]");
var blocks = [];
var i = defIndex + 1;
while (i < lines.length && own.test(lines[i])) {
var start = i;
var end = braceEnd(lines, i);
while (end + 1 < lines.length && deeper.test(lines[end + 1])) {
end = braceEnd(lines, end + 1);
}
var text = lines.slice(start, end + 1).join("\n");
blocks.push({ start: start, end: end, text: text, example: parseCitationLine(text) });
i = end + 1;
}
return blocks;
}
/**
* Replaces the index-th example under a definition with code, or removes it when code is null.
* original is the block as it read when the form opened. When the index no longer holds it (an earlier
* queued change moved it) the one block still reading exactly that is used; otherwise nothing is touched.
*/
function replaceExample(pageContent, language, sectionType, sectionOrdinal, definitionLevel, index, original, code) {
var loc = edit.wt.locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var defIndex = edit.wt.findDefinitionLine(lines, loc.posLineIndex, definitionLevel);
var examples = exampleBlocks(lines, defIndex).filter(function (b) {
return b.example;
});
var block = examples[index] && examples[index].text === original ? examples[index] : null;
if (!block) {
var same = examples.filter(function (b) {
return b.text === original;
});
block = same.length === 1 ? same[0] : null;
}
if (!block) {
throw new edit.util.SubmitAbort("Ev nimûne ji dema vekirina formê ve hatiye guhartin — rûpelê nû bike û dîsa biceribîne.");
}
var replacement = code === null ? [] : String(code).split("\n");
lines.splice.apply(lines, [block.start, block.end - block.start + 1].concat(replacement));
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
// Words only, lower-cased: links, templates, tags and quote marks out, so the page's drawn text and the
// line's wikitext can be compared.
function normalizeWords(text) {
return String(text || "")
.replace(/\[\[(?:[^\]|]*\|)?([^\]]*)\]\]/g, "$1")
.replace(/\{\{[^{}]*\}\}/g, " ")
.replace(/<[^>]*>/g, " ")
.replace(/'{2,}/g, "")
.toLowerCase()
.replace(/[\s.,;:!?"'“”‘’«»()\[\]\-–—…\/]+/g, "");
}
// True when the text a page draws for an example comes from this line.
function exampleMatches(drawnText, example) {
var source = example.text;
if (example.mode === "raw") {
var quote = /\|\s*(?:jêgirtin|jg)\s*=([^|}]*)/.exec(source);
source = quote ? quote[1] : source.replace(/^#+[:*]\s*/, "");
}
var drawn = normalizeWords(drawnText);
var written = normalizeWords(source);
if (!drawn || !written) {
return false;
}
return drawn === written || written.indexOf(drawn.slice(0, 24)) !== -1;
}
module.exports = {
CATEGORY_KEY: CATEGORY_KEY,
RAW_KEY: RAW_KEY,
COVERED_PARAMS: COVERED_PARAMS,
LANG_LABEL: LANG_LABEL,
jederDefs: jederDefs,
fieldSize: fieldSize,
exclusivePartner: exclusivePartner,
boldenPageTitle: boldenPageTitle,
nlToBr: nlToBr,
nlToSpace: nlToSpace,
fetchCategoryTemplates: fetchCategoryTemplates,
buildCategoryTemplateSearch: buildCategoryTemplateSearch,
openTemplateDialog: openTemplateDialog,
buildTemplateParamForm: buildTemplateParamForm,
templateParams: templateParams,
fieldSpecs: fieldSpecs,
jederFieldSpecs: jederFieldSpecs,
fetchFromCitoid: fetchFromCitoid,
citoidFieldsFor: citoidFieldsFor,
withLinkAsUrl: withLinkAsUrl,
today: today,
buildCitation: buildCitation,
looksLikeTemplate: looksLikeTemplate,
looksLikeCitationLine: looksLikeCitationLine,
USEX_NAMES: USEX_NAMES,
EMPTY_EXAMPLE_MARKER: EMPTY_EXAMPLE_MARKER,
parseCitationLine: parseCitationLine,
applyAliases: applyAliases,
keepOrder: keepOrder,
templateAliases: templateAliases,
exampleBlocks: exampleBlocks,
replaceExample: replaceExample,
exampleMatches: exampleMatches
};
}());
// </nowiki>
5fc0rel4ey1cy17f5ttg370zvxamytu
6531967
6531953
2026-09-17T15:59:29Z
Wikihez
7344
Test loader lists only gadgets with undeployed changes; italic example with a bracketed explanation reads as {{nimûne}}; no îzah button for Kurmancî
6531967
javascript
text/javascript
// <nowiki>
/*
* Page: [[MediaWiki:Gadget-wikt.cite.js]]
*
* Citations for both add-examples and the wizard: jêder-* templates, [[Kategorî:Çavkanî]] templates,
* Citoid, and the "#*" line they become. fieldSpecs() returns plain field descriptions because the two
* callers draw them differently (OOUI vs the wizard's own grid); everything else is shared. Also the
* template-param dialog (openTemplateDialog), used the same way by DefSideBoxes and start.js.
*
* The only caller of window.wikt.citoid, which the citoid-helper gadget owns.
*/
(function () {
"use strict";
var edit = require("ext.gadget.wikt.edit");
var util = edit.util;
var wt = edit.wt;
var langs = edit.langs;
var escapeRegExp = util.escapeRegExp;
var tdText = wt.tdText;
var parseTemplateCalls = wt.parseTemplateCalls;
var splitCallParams = wt.splitCallParams;
var buildTemplateCall = wt.buildTemplateCall;
var jederDefs = edit.data.jederDefs || {};
var api = new mw.Api();
// Sentinel _currentJederKey / dropdown value for "pick a template from Kategorî:Çavkanî". Not a real
// jederDefs key. Both gadgets use this same string, so a draft saved by one is read by the other.
var CATEGORY_KEY = "__category__";
// Sentinel for "the example box holds a finished template; write it out untouched".
var RAW_KEY = "__raw__";
// Covered by the form's own dedicated boxes, so they are never drawn again as generic template fields.
var COVERED_PARAMS = ["jêgirtin", "jg", "t", "tr"];
// A Kategorî:Çavkanî template whose TemplateData labels a parameter exactly this is fed the language
// from the language dropdown instead of showing a box for it. Unlike jêder-*, an arbitrary template has
// no "parameter 1 is the language" convention, so nothing is assumed positionally.
var LANG_LABEL = "koda zimanî";
// Field key -> {span, multiline}, taken from whichever jêder-* template defines it first. Most
// Kategorî:Çavkanî templates reuse the same bibliographic parameter names, so "sernav" comes out
// full-width and multiline there too rather than as a generic one-line box.
var SIZE_BY_KEY = {};
Object.keys(jederDefs).forEach(function (jederKey) {
(jederDefs[jederKey].fields || []).forEach(function (field) {
if (!SIZE_BY_KEY[field.key]) {
SIZE_BY_KEY[field.key] = {
span: field.span || 4,
multiline: !!field.multiline,
width: field.width || ""
};
}
});
});
// Field pairs Modul:quote refuses together ("Divê tenê yek ji |tarîx= an |sal= were dayîn"): a full
// date and a bare year say the same thing twice. Both are offered because either may be all a source
// gives, so the forms clear one when the other is filled rather than letting the citation break.
var EXCLUSIVE_PAIRS = [["sal", "tarîx"]];
function exclusivePartner(key) {
var found = null;
EXCLUSIVE_PAIRS.forEach(function (pair) {
if (pair[0] === key) {
found = pair[1];
} else if (pair[1] === key) {
found = pair[0];
}
});
return found;
}
function fieldSize(key) {
return SIZE_BY_KEY[key] || { span: 4, multiline: false, width: "" };
}
// ---- text helpers -------------------------------------------------------------------------------
// The entry name as a pattern. A Kurdish word ending in -î loses the circumflex once something is
// attached to it ("safî" is written "safiye"), so a title ending that way matches either spelling --
// otherwise the name is simply never found in the quotation.
function titlePattern(title) {
var escaped = escapeRegExp(String(title || ""));
return /î$/i.test(escaped) ? escaped.slice(0, -1) + "[îi]" : escaped;
}
// Wraps the entry's own name in bold wherever it appears in the quotation, which is how quotations are
// written here, and reports whether the name ends up bold so a caller can say so.
//
// The name may carry an attached suffix ("celbê"), so the match runs on to the end of the word. A word
// ends at whitespace, punctuation or markup: excluding markup is what keeps "celb<br>me’şûq" from
// bolding as one lump. The typographic apostrophe stays a word character -- it is one inside Kurdish
// words like "me’şûq".
function boldenPageTitle(text, title) {
var source = String(text || "");
var escapedTitle = titlePattern(title);
var suffixPattern = "([^\\s.,;:?!()'\"<>\\[\\]{}|=/\\\\«»„“”–—]+)?";
var titleRe = new RegExp("(" + escapedTitle + suffixPattern + ")", "ig");
var titleAnywhere = new RegExp(escapedTitle, "i");
// Whatever already sits between ''' marks is copied through untouched, so a quotation somebody
// bolded by hand -- or one an earlier run bolded -- never collects a second pair.
var boldSpan = /'''[\s\S]*?'''/g;
var out = "";
var last = 0;
var alreadyBold = false;
var span;
while ((span = boldSpan.exec(source)) !== null) {
out += source.slice(last, span.index).replace(titleRe, "'''$1'''");
if (titleAnywhere.test(span[0])) {
alreadyBold = true;
}
out += span[0];
last = span.index + span[0].length;
}
out += source.slice(last).replace(titleRe, "'''$1'''");
return { text: out, bolded: out !== source || alreadyBold };
}
// A literal newline would end the single-line "#*" wikitext, so display text keeps its break as <br>...
function nlToBr(text) {
return String(text || "").replace(/\r\n|\r|\n/g, "<br>");
}
// ...while a plain parameter value (an author, a title) just loses it.
function nlToSpace(text) {
return String(text || "").replace(/\s*(?:\r\n|\r|\n)\s*/g, " ");
}
// Bolds the entry name inside a pasted citation's jêgirtin=/jg= value only, so a book title or author
// containing it is left alone. With no such parameter nothing is bolded: running the plain-text bolder
// over wikitext would wrap the braces too.
function boldQuoteParam(text, title) {
var match = /(\|\s*(?:jêgirtin|jg)\s*=)([\s\S]*?)(?=\|[^|=]*=|\}\}\s*$)/.exec(text);
if (!match) {
return text;
}
var bolded = boldenPageTitle(match[2], title).text;
return text.slice(0, match.index) + match[1] + bolded + text.slice(match.index + match[0].length);
}
// "name — description" for each Kategorî:Çavkanî option in a <select>, from the description half of
// the same TemplateData every citation picker already fetches -- so a search box built over these
// options can also match on what a template documents itself as doing, not only its name. An
// undescribed option, or a failed fetch, just keeps its plain label; never blocks on this.
// A self-contained "search Kategorî:Çavkanî" combo: a text input plus its own results list, matching
// add-examples' own jêgirtin picker -- each row shows its label, a " → target" arrow when a redirect's
// label differs from its target, and a 🔗 link to the canonical template's own page. opts.onPick(entry)
// fires when a row is chosen; the input is then left holding the plain label, not "label → target", so
// re-focusing it to search again doesn't start from that arrow text. opts.describe: true also fills in
// each visible row's own TemplateData description once it loads (batched, so scrolling through matches
// costs a handful of requests, not one per row).
function buildCategoryTemplateSearch(opts) {
opts = opts || {};
var shell = util.buildSearchList({
placeholder: opts.placeholder || util.msg.searchTemplate
});
var $input = shell.$input;
var $results = shell.$results;
var $status = $("<span class='wiktku-cat-tpl-status'></span>").text(util.msg.templatesLoading);
var list = [];
function decorate($text, target, wanted) {
if (!wanted) {
return;
}
edit.wt.fetchTemplateData(target).then(function (doc) {
var desc = doc && edit.wt.tdText(doc.description);
if (desc && $text.closest("body").length) {
$text.append(document.createTextNode(" — " + edit.wt.shortDesc(desc)));
}
});
}
function showResults(query) {
var q = $.trim(query || "").toLowerCase();
$results.empty();
var matched = q ? list.filter(function (entry) {
return entry.label.toLowerCase().indexOf(q) !== -1;
}) : list;
if (!matched.length) {
shell.close();
return;
}
var shown = matched.slice(0, 50);
// Only what a search has narrowed to: an empty box is the whole category, and describing its
// first fifty would ask the wiki about templates nobody has looked for.
var describing = !!opts.describe && !!q;
if (describing) {
edit.wt.fetchTemplateDataBatch(shown.map(function (entry) { return entry.target; }));
}
shown.forEach(function (entry) {
var $li = $("<li class='wiktku-cat-tpl-row'></li>");
var $text = $("<span class='wiktku-cat-tpl-text'></span>").text(entry.label);
if (entry.label !== entry.target) {
$text.append($("<span class='wiktku-cat-tpl-arrow'></span>").text(" → " + entry.target));
}
decorate($text, entry.target, describing);
var $link = $("<a class='wiktku-cat-tpl-link' target='_blank' rel='noopener'>🔗</a>")
.attr("title", util.msg.openTemplatePage)
.attr("href", util.tplPageUrl(entry.target));
// stopPropagation only -- preventDefault would stop the link from actually navigating.
$link.on("mousedown click", function (e) { e.stopPropagation(); });
$li.append($text, $link);
shell.bindPick($li, function () {
shell.close();
$input.val(entry.label);
opts.onPick(entry);
});
$results.append($li);
});
shell.open();
}
$input.on("input focus", function () {
showResults($(this).val());
});
$input.on("blur", function () {
setTimeout(shell.close, 200); // lets the pick handler fire first
});
fetchCategoryTemplates().then(function (l) {
list = l;
$status.text("");
}, function () {
$status.text(util.msg.templatesFailed);
});
return {
$element: $("<span class='wiktku-native-select-wrap wiktku-cat-tpl-picker'></span>").append($input, $results, $status),
$input: $input,
$status: $status
};
}
// ---- Kategorî:Çavkanî -----------------------------------------------------------------------------
// The category's template members plus their redirects, each redirect its own searchable entry pointing
// at the same canonical target, so a template is findable by any of its names. Cached for the session.
var catTemplatesPromise = null;
function fetchCategoryTemplates() {
if (catTemplatesPromise) {
return catTemplatesPromise;
}
catTemplatesPromise = api.get({
action: "query",
generator: "categorymembers",
gcmtitle: "Kategorî:Çavkanî",
gcmnamespace: 10,
gcmlimit: "max",
prop: "redirects",
rdnamespace: 10,
rdlimit: "max",
formatversion: 2
}).then(function (data) {
var list = [];
((data.query && data.query.pages) || []).forEach(function (page) {
var target = page.title.replace(/^[^:]+:/, "");
list.push({ label: target, target: target });
(page.redirects || []).forEach(function (redirect) {
list.push({ label: redirect.title.replace(/^[^:]+:/, ""), target: target });
});
});
list.sort(function (a, b) {
return a.label.localeCompare(b.label, mw.config.get("wgUserLanguage"));
});
return list;
}, function () {
return [];
});
return catTemplatesPromise;
}
// ---- template-param dialog: fills a {{Name|...}} call from its own TemplateData -------------------
// Shared by DefSideBoxes (a fresh insert from Etîmolojî/Çavkanî, or double-clicking an existing chip
// to edit it) and start.js's own Etîmolojî/Çavkanî pickers -- one dialog, so all of them read the same
// TemplateData the same way.
// OOUI's own dialog stack -- the same widgetry VisualEditor's template dialog is built on.
var citeWindowManager = null;
function templateDialogManager() {
if (!citeWindowManager) {
citeWindowManager = new OO.ui.WindowManager();
$(document.body).append(citeWindowManager.$element);
}
return citeWindowManager;
}
// Opens one template call in a modal built from its TemplateData. Resolves with the edited wikitext, or
// null when the user cancels.
// One at a time: a second open while one is loading or showing (a chip clicked again before its dialog
// appeared) stacks a window the manager does not track, and that window's buttons do nothing. The
// second call resolves null, as a cancel does.
var templateDialogBusy = false;
function openTemplateDialog(callWikitext) {
var done = $.Deferred();
var call = parseTemplateCalls(callWikitext).calls[0];
if (!call || templateDialogBusy) {
return done.resolve(null).promise();
}
templateDialogBusy = true;
done.always(function () {
templateDialogBusy = false;
});
$.when(
mw.loader.using(["oojs-ui-core", "oojs-ui-windows"]),
edit.wt.fetchTemplateData(call.name)
).then(function (_loaded, td) {
var result = null;
// The parent is captured rather than reached through TplDialog.super: "super" is a reserved word,
// and OOjs has moved between .super and .parent for it over the years.
var ParentDialog = OO.ui.ProcessDialog;
function TplDialog(config) {
ParentDialog.call(this, config);
}
OO.inheritClass(TplDialog, ParentDialog);
TplDialog.static.name = "citeTemplateDialog";
TplDialog.static.title = "{{" + call.name + "}}";
TplDialog.static.actions = [
{ action: "save", label: "Biguhêre", flags: ["primary", "progressive"] },
{ action: "remove", label: "Jê bibe", flags: "destructive" },
{ label: util.msg.cancel, flags: "safe" }
];
TplDialog.prototype.initialize = function () {
ParentDialog.prototype.initialize.apply(this, arguments);
this.panel = new OO.ui.PanelLayout({ padded: true, expanded: false });
this.form = buildTemplateParamForm(call, td);
this.panel.$element.append(this.form.$element);
this.$body.append(this.panel.$element);
};
TplDialog.prototype.getBodyHeight = function () {
return this.panel.$element.outerHeight(true);
};
TplDialog.prototype.getActionProcess = function (action) {
var dialog = this;
if (action === "save" || action === "remove") {
return new OO.ui.Process(function () {
result = action === "remove" ? "" : dialog.form.values();
dialog.close();
});
}
return ParentDialog.prototype.getActionProcess.call(this, action);
};
var manager = templateDialogManager();
var dialog = new TplDialog({ size: "medium" });
manager.addWindows([dialog]);
var instance = manager.openWindow(dialog);
instance.closed.then(function () {
manager.clearWindows();
done.resolve(result);
});
instance.opening.fail(function () {
manager.clearWindows();
done.resolve(null);
});
}, function () {
done.resolve(null);
});
return done.promise();
}
// TemplateData descriptions are plain text, but authors write wikilinks in them; render those as links.
function tdDescription(text) {
var $out = $("<span></span>");
var re = /\[\[([^\[\]|]+)(?:\|([^\[\]]*))?\]\]/g;
var last = 0;
var m;
while ((m = re.exec(text)) !== null) {
if (m.index > last) {
$out.append(document.createTextNode(text.slice(last, m.index)));
}
$out.append($("<a></a>").attr("href", mw.util.getUrl(m[1])).text(m[2] || m[1]));
last = re.lastIndex;
}
if (last < text.length) {
$out.append(document.createTextNode(text.slice(last)));
}
return $out;
}
// A parameter whose documentation says so takes "kelîme<t:...>" inline modifiers, so it gets the pill
// widget instead of a plain box. TemplateData has no field for this, hence the marker in the description.
var INLINE_MODIFIER_MARKER = "WF:Guherkerên rêzê";
function acceptsInlineModifiers(doc) {
return tdText(doc && doc.description).indexOf(INLINE_MODIFIER_MARKER) !== -1;
}
// A language-code parameter, per its own label.
function isLanguageParam(doc) {
return /ziman/i.test(tdText(doc && doc.label));
}
// 3693 languages is far too many to rebuild per dialog (~190ms), and a datalist can be shared by id, so
// this is built once and reused by every language box on the page.
var LANG_DATALIST_ID = "dsb-language-list";
function fillLanguageDatalist($list) {
var names = langs.names();
$list.empty();
Object.keys(names).forEach(function (code) {
$list.append($("<option></option>").val(code).attr("label", names[code]));
});
}
function languageDatalistId() {
if (!document.getElementById(LANG_DATALIST_ID)) {
var $list = $("<datalist></datalist>").attr("id", LANG_DATALIST_ID);
fillLanguageDatalist($list);
$(document.body).append($list);
// Refilled in place if the long list ever arrives, so every box using it grows at once.
langs.onFull(function () {
fillLanguageDatalist($("#" + LANG_DATALIST_ID));
});
}
return LANG_DATALIST_ID;
}
function buildTemplateParamForm(call, td) {
var parsed = splitCallParams(call.params);
var docParams = (td && td.params) || {};
var keys = (td && td.paramOrder) ? td.paramOrder.slice() : Object.keys(docParams);
parsed.used.forEach(function (k) {
if (keys.indexOf(k) === -1) {
keys.push(k);
}
});
var $element = $("<div class='dsb-tpl-form'></div>");
var readers = {}; // key -> () => the parameter's current text
$element.append($("<div class='dsb-tpl-name'></div>").append(
$("<a></a>").attr("href", mw.util.getUrl("Şablon:" + call.name))
.attr("target", "_blank").text("{{" + call.name + "}}")
));
var desc = tdText(td && td.description);
if (desc) {
$element.append($("<div class='dsb-tpl-desc'></div>").append(tdDescription(desc)));
}
keys.forEach(function (key) {
var doc = docParams[key] || {};
var value = parsed.values[key] || "";
var $field;
if (acceptsInlineModifiers(doc)) {
// The same word row the sense lists use: plain text plus removable "<t:...>" pills.
var row = util.makeModifierRow({
prefix: "dsb-",
placeholder: tdText(doc.label) || key,
modTitle: util.msg.addModifier,
removeTitle: "Vê nirxê jê bibe",
removeModTitle: util.msg.removeModifier
});
row.setRaw(value);
// A <ul> so the row's own "close other pickers" lookup has something to scope to.
$field = $("<ul class='dsb-wordlist dsb-tpl-modifier-list'></ul>").append(row.$li);
readers[key] = row.getRaw;
} else {
var $input = $("<input type='text' class='dsb-input'>").val(value);
if (isLanguageParam(doc)) {
$input.attr("list", languageDatalistId()).attr("placeholder", "koda zimanî…");
// Typing a language the short list does not have is the cue to fetch the rest.
$input.on("input", function () {
if (langs.missingFromCore($input.val())) {
langs.ensureFull();
}
});
} else {
var suggested = doc.suggestedvalues || [];
if (suggested.length) {
var listId = "dsb-sv-" + call.name.replace(/[^A-Za-z0-9]/g, "") + "-" + key;
var $list = $("<datalist></datalist>").attr("id", listId);
suggested.forEach(function (v) {
$list.append($("<option></option>").val(v));
});
$input.attr("list", listId);
$element.append($list);
}
}
$field = $input;
readers[key] = function () {
return $input.val();
};
}
var $label = $("<label class='dsb-adv-label'></label>").text(tdText(doc.label) || key);
if (!docParams[key]) {
$label.append($("<span class='dsb-tpl-undocumented'></span>").text(" (ne di belgeyê de)"));
}
var $row = $("<div class='dsb-tpl-param-row'></div>").append($label, $field);
var help = tdText(doc.description);
if (help) {
$row.append($("<div class='dsb-tpl-help'></div>").append(tdDescription(help)));
}
if (doc.deprecated) {
$row.addClass("dsb-tpl-deprecated");
}
$element.append($row);
});
return {
$element: $element,
values: function () {
var vals = {};
keys.forEach(function (k) {
vals[k] = $.trim(readers[k]() || "");
});
return buildTemplateCall(call.name, vals, keys);
}
};
}
// The documented parameters of one template (name without the "Şablon:" prefix), in its own
// paramOrder; [] when it has no TemplateData. The fetch, the shaping and the cache all live in
// wikt.edit, so the wizard, DefSideBoxes and this module share one request and one copy per template.
//
// The key stands in for a missing label, because these become form fields and a field needs something
// written on it.
// A template's parameters: its TemplateData, or -- for one with none, like most Kategorî:Çavkanî
// templates -- every {{{name}}} its own source reads.
function templateParams(name) {
return edit.wt.templateDoc(name).then(function (td) {
var params = ((td && td.params) || []).map(function (p) {
return {
key: p.key,
label: p.label || p.key,
required: p.required,
deprecated: p.deprecated
};
});
return params.length ? params : sourceParams(name);
});
}
var sourceParamsCache = {};
function sourceParams(name) {
if (!sourceParamsCache[name]) {
sourceParamsCache[name] = new mw.Api().get({
action: "query", prop: "revisions", titles: "Template:" + name, rvprop: "content", rvslots: "main",
redirects: 1, formatversion: 2
}).then(function (data) {
var page = data.query.pages[0];
var source = page.revisions ? page.revisions[0].slots.main.content : "";
var keys = [];
var re = /\{\{\{\s*([^{}|]+?)\s*[|}]/g;
var m;
while ((m = re.exec(source)) !== null) {
if (!/^\d+$/.test(m[1]) && keys.indexOf(m[1]) === -1) {
keys.push(m[1]);
}
}
return keys.map(function (key) {
return { key: key, label: key, required: false, deprecated: false };
});
}, function () {
return [];
});
}
return sourceParamsCache[name];
}
// alias -> the name a template's TemplateData documents it under ({r: "rûpel"} for jêder-kitêb). Asked
// of the wiki itself: the parameter lists baked into data-main.json carry no aliases.
var aliasCache = {};
function templateAliases(name) {
if (!aliasCache[name]) {
aliasCache[name] = new mw.Api().get({
action: "templatedata", titles: "Template:" + name, redirects: 1, formatversion: 2
}).then(function (data) {
var map = {};
$.each(data.pages || {}, function (_, page) {
$.each(page.params || {}, function (key, p) {
(p.aliases || []).forEach(function (alias) {
map[alias] = key;
});
});
});
return map;
}, function () {
return {};
});
}
return aliasCache[name];
}
// What to draw for one Kategorî:Çavkanî template: a field per documented parameter, minus the ones the
// form already has boxes for and minus the language parameter, which is filled from the dropdown.
// A template with no TemplateData at all falls back to a lone "Rûpel" box, which is what such citations
// almost always need ({{Şablon|rûpel=12|jêgirtin=…}}).
function fieldSpecs(name) {
return templateParams(name).then(function (params) {
var langParamKey = null;
(params || []).forEach(function (p) {
if ((p.label || "").trim().toLowerCase() === LANG_LABEL) {
langParamKey = p.key;
}
});
var filtered = (params || []).filter(function (p) {
return COVERED_PARAMS.indexOf(p.key) === -1 && p.key !== langParamKey;
});
var list = filtered.length ? filtered : [{ key: "rûpel", label: "Rûpel", required: false }];
return {
langParamKey: langParamKey,
fields: list.map(function (p) {
var size = fieldSize(p.key);
return {
key: p.key,
label: p.label,
required: p.required,
span: size.span,
width: size.width,
multiline: size.multiline
};
})
};
});
}
// The same list for a jêder-* template, straight out of the shared table.
function jederFieldSpecs(jederKey) {
var def = jederDefs[jederKey];
return def ? (def.fields || []).slice() : [];
}
// ---- Citoid --------------------------------------------------------------------------------------
// Today, as Modul:quote wants it (it renders "2024-05-05" as "5 gulan 2024").
function today() {
var d = new Date();
function pad(n) { return (n < 10 ? "0" : "") + n; }
return d.getFullYear() + "-" + pad(d.getMonth() + 1) + "-" + pad(d.getDate());
}
// Carries across the parts of a Citoid result that citoid-helper's own mapping drops: the access date
// and the identifiers for the fields the forms have only just gained. Applied here rather than in
// citoid-helper so it reaches both gadgets without an edit to the MediaWiki: gadget page.
function enrichCitoidFields(fields, raw) {
fields = fields || {};
if (fields.url && !fields["tarîxa-gihiştinê"]) {
// Citoid reports when it actually fetched the page ("accessDate": "2026-09-03"), so use that
// rather than the clock -- they differ whenever the answer came from a cache. today() is only
// the fallback for a result that carries no accessDate.
var accessed = raw && (raw.accessDate || raw.accessdate);
fields["tarîxa-gihiştinê"] = (accessed && String(accessed).slice(0, 10)) || today();
}
if (raw) {
var issn = Array.isArray(raw.ISSN) ? raw.ISSN[0] : raw.ISSN;
if (issn && !fields.issn) {
fields.issn = issn;
}
var doi = Array.isArray(raw.DOI) ? raw.DOI[0] : raw.DOI;
if (doi && !fields.doi) {
fields.doi = doi;
}
if (raw.volume && !fields.cild) {
fields.cild = raw.volume;
}
}
return fields;
}
// One URL/DOI/ISBN -> {jederKey, fields}. The lookup itself lives in the citoid-helper gadget; this
// wrapper exists so neither caller has to know that, and so a missing gadget fails in one place.
function fetchFromCitoid(query) {
if (!(window.wikt && window.wikt.citoid && window.wikt.citoid.fetchJederFields)) {
return $.Deferred().reject("Amûra Citoidê ne barkirî ye.").promise();
}
return window.wikt.citoid.fetchJederFields(query).then(function (result) {
result.fields = enrichCitoidFields(result.fields, result.raw);
return result;
});
}
// Re-derives the fields for a different jêder-* template than the one citoid detected, enriched the
// same way -- used when the person had already chosen a template and we keep their choice.
// The form's link box stands in for a jêder-* template's own url= when that is left empty.
function withLinkAsUrl(jederKey, fields, link) {
var def = jederDefs[jederKey];
var hasUrl = !!def && (def.fields || []).some(function (field) {
return field.key === "url";
});
if (hasUrl && !fields.url && /^https?:\/\//.test(link || "")) {
fields.url = link;
}
return fields;
}
function citoidFieldsFor(jederKey, raw, query) {
if (!(window.wikt && window.wikt.citoid && window.wikt.citoid.dataToJederFields)) {
return {};
}
return enrichCitoidFields(window.wikt.citoid.dataToJederFields(jederKey, raw, query), raw);
}
// ---- the "#*" line ------------------------------------------------------------------------------
// One finished citation line. Modes:
// "jeder" {{jêder-kitêb|ku|sernav=…|jêgirtin=…}}
// "category" the Kategorî:Çavkanî template as the citation template: {{Ç:Kitêb|rûpel=12|…}}
// "plain" free-text source, or none: {{jêder|ku|2=…|jêgirtin=…}} / {{nimûne|ku|…}}
// "raw" a finished template, passed through with only the entry name bolded
// opts: {mode, template, language, fields, langParamKey, quote, translation, transcription,
// source, sourceURL, title, marker, keepTranslation, extra}
function buildCitation(opts) {
var marker = opts.marker || "#*";
var title = opts.title || mw.config.get("wgTitle");
var quote = boldenPageTitle(String(opts.quote || ""), title).text;
if (opts.mode === "raw") {
// Written as given: the point is to paste a citation built elsewhere and have it survive intact.
// A line copied off another entry usually still carries its own "#*"/"#:" -- the marker is added
// back below, so strip it rather than emitting "#* #* {{...}}".
var pasted = String(opts.quote || "").replace(LIST_MARKER, "");
return marker + " " + nlToBr(boldQuoteParam(pasted, title)).trim();
}
var isCategory = opts.mode === "category";
var isTemplate = isCategory || opts.mode === "jeder";
var name = isTemplate ? opts.template : (opts.source || opts.sourceURL ? "jêder" : "nimûne");
var line = marker + " {{" + name;
// jêder-*, jêder and nimûne all take the language as parameter 1. A Kategorî:Çavkanî template only
// gets one when its own TemplateData names it, under its own key -- never assumed positionally.
if (!isCategory) {
line += "|" + opts.language;
}
if (isTemplate) {
var fields = opts.fields || {};
Object.keys(fields).forEach(function (key) {
var value = nlToSpace(String(fields[key] || "").trim());
if (value) {
line += "|" + key + "=" + value;
}
});
if (isCategory && opts.langParamKey) {
line += "|" + opts.langParamKey + "=" + opts.language;
}
// "Ç:" templates are quotation-equivalents rather than usage examples (see Modul:quote).
if (isCategory && /^Ç:/.test(name)) {
line += "|wekjêgirtin=1";
}
} else {
if (opts.source) {
// A positional value containing "=" would be read as its own key=value pair, so name it.
line += (String(opts.source).indexOf("=") !== -1 ? "|2=" : "|") + opts.source;
}
if (opts.sourceURL) {
line += "|url=" + opts.sourceURL;
}
}
var isUsex = name === "nimûne";
var translation = nlToBr(String(opts.translation || "").trim());
// {{nimûne}} always takes one, a Kurmancî example's being its explanation; a quotation's is left out
// for Kurmancî unless the edited line already had one (keepTranslation).
var writeTranslation = !!translation && (isUsex || opts.language !== "ku" || opts.keepTranslation);
// {{nimûne}}'s example is parameter 2 and its translation parameter 3, both positional. A value
// containing "=" would be read as key=value, so then both are named: naming only the example would
// slide the translation into slot 2.
var namedSlots = isUsex && (quote.indexOf("=") !== -1 || (writeTranslation && translation.indexOf("=") !== -1));
line += "|" + (isUsex ? (namedSlots ? "2=" : "") : "jêgirtin=") + nlToBr(quote);
if (writeTranslation) {
// Kategorî:Çavkanî templates route through Modul:quote and take its "t="; jêder-* uses "tercime=".
line += "|" + (isUsex ? (namedSlots ? "3=" : "") : isCategory ? "t=" : "tercime=") + translation;
}
var transcription = nlToBr(String(opts.transcription || "").trim());
if (transcription) {
line += "|tr=" + transcription;
}
// Parameters the form has no box for, carried through from the line being edited.
(opts.extra || []).forEach(function (param) {
line += "|" + param;
});
// A short example and its translation read best on one line, which {{nimûne}} does only when told.
// Short is Modul:usex's own measure for "rêz=auto": at most 100 characters shown between them.
if (isUsex && writeTranslation && !hasInlineParam(opts.extra) &&
shownLength(quote) + shownLength(translation) <= 100) {
line += "|rêz=1";
}
return line + "}}";
}
// True when the parameters already say inline or not: {{nimûne}}'s rêz=, under any of its names.
function hasInlineParam(params) {
return (params || []).some(function (param) {
return /^\s*(?:rêz|birêz|inline)\s*=/.test(param);
});
}
// How many characters a bit of wikitext shows: link targets, bold/italic marks and tags don't count.
function shownLength(text) {
return String(text || "")
.replace(/\[\[(?:[^\]|]*\|)?([^\]]*)\]\]/g, "$1")
.replace(/'{2,}/g, "")
.replace(/<[^>]*>/g, " ")
.length;
}
// A leading "#*" / "#:" / "##*" ... list marker.
var LIST_MARKER = /^\s*#+[*:]\s*/;
// True when the text is a whole citation line copied from somewhere else: its own list marker followed
// by a template call. That is the shape of every line this family writes -- {{jêder}}, {{jêder-kitêb}},
// {{nm}}, {{nimûne}}, {{mk}}, any Kategorî:Çavkanî template -- so the template name is not checked
// against a list, which could never cover every citation template on the wiki.
//
// Used to switch the form into raw mode by itself: pasting a finished line and forgetting to change the
// dropdown would otherwise wrap it in a second {{nimûne}}.
function looksLikeCitationLine(text) {
var trimmed = String(text || "");
if (!LIST_MARKER.test(trimmed)) {
return false;
}
return trimmed.replace(LIST_MARKER, "").slice(0, 2) === "{{";
}
// True when the example box holds nothing but one finished template call -- the cue to offer, or to
// switch to, the "write it out as-is" mode.
function looksLikeTemplate(text) {
var trimmed = String(text || "").trim();
if (trimmed.slice(0, 2) !== "{{" || trimmed.slice(-2) !== "}}") {
return false;
}
var parsed = edit.wt.parseTemplateCalls(trimmed);
return parsed.calls.length === 1 && !parsed.otherContent;
}
// ---- reading a finished line back --------------------------------------------------------------
// [[Şablon:nimûne]] and every redirect to it.
var USEX_NAMES = ["nimûne", "mk", "ux", "mînak", "nim", "nm"];
var USEX_TRANSLATION = ["3", "t", "tercime", "translation", "w", "werger"];
var USEX_TRANSCRIPTION = ["tr", "transliteration"];
// A quotation template's translation, under the name Modul:quote documents and its aliases.
var QUOTE_TRANSLATION = ["t", "tercime", "werger", "w"];
// [[Şablon:bênimûne]] and its redirects: the "no example yet" placeholder, with or without a language.
var EMPTY_EXAMPLE_MARKER = /^#+\*\s*\{\{\s*(?:bênimûne|bêmînak|mînak\?|nimûne\?)\s*(?:\|[^{}]*)?\}\}\s*$/i;
// The text of a line written as one italic run ("''…''", bold inside allowed -- "'''''X''' …''" opens
// with both), or null.
function italicRun(text) {
var t = $.trim(text || "");
var opening = /^'*/.exec(t)[0].length;
var m = /^''([\s\S]+)''$/.exec(t);
if ((opening !== 2 && opening !== 5) || !m || /''/.test(m[1].replace(/'''/g, ""))) {
return null;
}
return m[1];
}
// "''…'' (…)": an italic example with its explanation in brackets on the same line, as
// {text, translation}, or null. One level of brackets inside the explanation is allowed.
function italicWithGloss(text) {
var m = /^([\s\S]*'')\s*\(([^()]*(?:\([^()]*\)[^()]*)*)\)$/.exec($.trim(text || ""));
var italic = m && italicRun(m[1]);
if (italic === null || !m) {
return null;
}
var gloss = $.trim(m[2]);
return { text: italic, translation: italicRun(gloss) || gloss };
}
function brToNl(text) {
return String(text || "").replace(/<br\s*\/?>/gi, "\n");
}
/**
* One "#:"/"#*" block -- its line plus any deeper "#::"/"#*:" lines -- as the form's values, or null when
* it is not an example: a word list, the "no example yet" placeholder, a plain note.
* {mode: "nimune"|"jeder"|"source"|"raw", marker, template, text, translation, transcription,
* source, sourceURL, fields, extra}
* extra is every parameter the form has no box for, to be written back as it was. A "raw" block is
* edited as written: text is the whole block, list markers included.
*/
function parseCitationLine(block) {
var all = String(block || "").split("\n");
var k = 1;
while (k < all.length && !/^#/.test(all[k])) {
k++;
}
var head = /^#+([:*])\s*([\s\S]*)$/.exec(all.slice(0, k).join("\n"));
var sub = all.slice(k);
if (!head || EMPTY_EXAMPLE_MARKER.test(all[0])) {
return null;
}
var out = { mode: "raw", marker: head[1], template: "", text: "", translation: "", transcription: "",
source: "", sourceURL: "", fields: {}, extra: [] };
var body = $.trim(head[2]);
var parsed = edit.wt.parseTemplateCalls(body);
var call = parsed.calls.length === 1 && !parsed.otherContent ? parsed.calls[0] : null;
var name = call ? edit.wt.canonicalTemplateKey(call.name) : "";
name = name.charAt(0).toLowerCase() + name.slice(1);
var isUsex = USEX_NAMES.indexOf(name) !== -1;
var def = call && jederDefs[name];
if (call && !sub.length && (isUsex || def || name === "jêder")) {
var split = edit.wt.splitCallParams(call.params);
var fieldKeys = def ? def.fields.map(function (f) { return f.key; }) : [];
out.mode = isUsex ? "nimune" : def ? "jeder" : "source";
out.template = def ? name : "";
split.used.forEach(function (key, i) {
var value = brToNl(split.values[key]);
if (key === "1") {
return; // the language, which the section already says
}
if (isUsex ? key === "2" : (key === "jêgirtin" || key === "jg")) {
out.text = value;
} else if ((isUsex ? USEX_TRANSLATION : QUOTE_TRANSLATION).indexOf(key) !== -1) {
out.translation = value;
} else if (USEX_TRANSCRIPTION.indexOf(key) !== -1) {
out.transcription = value;
} else if (def && fieldKeys.indexOf(key) !== -1) {
out.fields[key] = split.values[key];
} else if (name === "jêder" && key === "2") {
out.source = split.values[key];
} else if (name === "jêder" && key === "url") {
out.sourceURL = split.values[key];
} else {
// Named when it was positional, so it cannot shift into a slot the form now fills.
out.extra.push(/^\d+$/.test(key) && !/=/.test(call.params[i]) ? key + "=" + split.values[key] : $.trim(call.params[i]));
}
});
return out;
}
// Any other citation template on a "#*" line -- the Kategorî:Çavkanî ones, {{J:…}} and {{Ç:…}} --
// reads into the category mode: quote, translation and transcription in their boxes, every other
// named parameter as a field. Which of those get a box is known once the template's parameters
// are fetched (see applyAliases).
if (call && !sub.length && out.marker === "*" &&
(/^[JÇ]:/.test($.trim(call.name)) || /(?:^|\|)\s*(?:jêgirtin|jg)\s*=/.test(call.params.join("|")))) {
var catSplit = edit.wt.splitCallParams(call.params);
out.mode = "category";
out.template = $.trim(call.name);
catSplit.used.forEach(function (key, i) {
var value = catSplit.values[key];
if (key === "jêgirtin" || key === "jg") {
out.text = brToNl(value);
} else if (QUOTE_TRANSLATION.indexOf(key) !== -1) {
out.translation = brToNl(value);
} else if (USEX_TRANSCRIPTION.indexOf(key) !== -1) {
out.transcription = brToNl(value);
} else if (key === "wekjêgirtin" && /^Ç:/.test(out.template)) {
return; // buildCitation() writes it for every "Ç:" template
} else if (/^\d+$/.test(key)) {
out.extra.push(!/=/.test(call.params[i]) ? key + "=" + value : $.trim(call.params[i]));
} else {
out.fields[key] = value;
}
});
return out;
}
if (out.marker === ":") {
// The hand-written kind: "#: ''…''", its translation, if any, on a "#::" line under it.
var italic = italicRun(body);
var translationLine = sub.length === 1 ? /^#+::\s*([\s\S]*)$/.exec(sub[0]) : null;
if (italic !== null && (!sub.length || translationLine)) {
out.mode = "nimune";
out.text = brToNl(italic);
out.translation = translationLine ? brToNl(italicRun(translationLine[1]) || $.trim(translationLine[1])) : "";
return out;
}
var withGloss = sub.length ? null : italicWithGloss(body);
if (withGloss) {
out.mode = "nimune";
out.text = brToNl(withGloss.text);
out.translation = brToNl(withGloss.translation);
return out;
}
if (!isUsex) {
return null;
}
}
out.text = all.join("\n");
return out;
}
/**
* Settles which parameters of a read line get a box. aliases maps an alias to its documented name
* (templateAliases); fieldKeys are the boxes the form drew. A parameter written under an alias -- "r="
* for "rûpel=" -- goes to its box; one with no box goes to extra, written back as it was.
*/
function applyAliases(example, aliases, fieldKeys) {
var pending = example.extra.slice();
Object.keys(example.fields).forEach(function (key) {
if (fieldKeys.indexOf(key) === -1) {
pending.push(key + "=" + example.fields[key]);
delete example.fields[key];
}
});
example.extra = [];
pending.forEach(function (param) {
var m = /^\s*([^=]+?)\s*=([\s\S]*)$/.exec(param);
var key = m ? (aliases[m[1]] || m[1]) : null;
var value = m ? $.trim(m[2]) : "";
if (m && fieldKeys.indexOf(key) !== -1 && !example.fields[key]) {
example.fields[key] = value;
} else if (m && key === "jêgirtin" && !example.text) {
example.text = brToNl(value);
} else if (m && QUOTE_TRANSLATION.indexOf(key) !== -1 && !example.translation) {
example.translation = brToNl(value);
} else {
example.extra.push(param);
}
});
return example;
}
/**
* newLine with its parameters in the order originalLine had them, so an edit's diff shows only what
* changed. Parameters match by name -- an alias counting as the name it stands for (aliases, from
* templateAliases) -- and positional ones by position; any the original lacked follow in newLine's own
* order. A line that is not one template call on either side comes back as it was.
*/
function keepOrder(newLine, originalLine, aliases) {
aliases = aliases || {};
var marker = /^#+[:*]\s*/.exec(newLine || "");
var oldBody = /^#+[:*]\s*([\s\S]*)$/.exec(originalLine || "");
if (!marker || !oldBody) {
return newLine;
}
var fresh = edit.wt.parseTemplateCalls(newLine.slice(marker[0].length));
var old = edit.wt.parseTemplateCalls($.trim(oldBody[1]));
if (fresh.calls.length !== 1 || fresh.otherContent || old.calls.length !== 1 || old.otherContent) {
return newLine;
}
function keyed(params) {
var positional = 0;
return params.map(function (raw) {
var m = /^\s*([^|={}\n]+?)\s*=/.exec(raw);
var key = m ? m[1] : String(++positional);
return { key: aliases[key] || key, raw: raw };
});
}
var rest = keyed(fresh.calls[0].params);
var ordered = [];
keyed(old.calls[0].params).forEach(function (o) {
for (var i = 0; i < rest.length; i++) {
if (rest[i].key === o.key) {
ordered.push(rest.splice(i, 1)[0]);
return;
}
}
});
return marker[0] + "{{" + fresh.calls[0].name + ordered.concat(rest).map(function (p) {
return "|" + p.raw;
}).join("") + "}}";
}
// Where the {{ … }} opened on lines[start] closes, so a template written over several lines stays whole.
function braceEnd(lines, start) {
var depth = 0;
for (var i = start; i < lines.length; i++) {
depth += (lines[i].match(/\{\{/g) || []).length - (lines[i].match(/\}\}/g) || []).length;
if (depth <= 0) {
return i;
}
}
return lines.length - 1;
}
// The "#:"/"#*" blocks right under the definition on lines[defIndex], each {start, end, text, example}:
// example is parseCitationLine's reading, or null for a block that is not an example.
function exampleBlocks(lines, defIndex) {
var depth = /^#+/.exec(lines[defIndex])[0].length;
var own = new RegExp("^#{" + depth + "}[:*]");
var deeper = new RegExp("^#{" + depth + "}[:*][:*]");
var blocks = [];
var i = defIndex + 1;
while (i < lines.length && own.test(lines[i])) {
var start = i;
var end = braceEnd(lines, i);
while (end + 1 < lines.length && deeper.test(lines[end + 1])) {
end = braceEnd(lines, end + 1);
}
var text = lines.slice(start, end + 1).join("\n");
blocks.push({ start: start, end: end, text: text, example: parseCitationLine(text) });
i = end + 1;
}
return blocks;
}
/**
* Replaces the index-th example under a definition with code, or removes it when code is null.
* original is the block as it read when the form opened. When the index no longer holds it (an earlier
* queued change moved it) the one block still reading exactly that is used; otherwise nothing is touched.
*/
function replaceExample(pageContent, language, sectionType, sectionOrdinal, definitionLevel, index, original, code) {
var loc = edit.wt.locateSection(pageContent, language, sectionType, sectionOrdinal);
var lines = loc.lines;
var defIndex = edit.wt.findDefinitionLine(lines, loc.posLineIndex, definitionLevel);
var examples = exampleBlocks(lines, defIndex).filter(function (b) {
return b.example;
});
var block = examples[index] && examples[index].text === original ? examples[index] : null;
if (!block) {
var same = examples.filter(function (b) {
return b.text === original;
});
block = same.length === 1 ? same[0] : null;
}
if (!block) {
throw new edit.util.SubmitAbort("Ev nimûne ji dema vekirina formê ve hatiye guhartin — rûpelê nû bike û dîsa biceribîne.");
}
var replacement = code === null ? [] : String(code).split("\n");
lines.splice.apply(lines, [block.start, block.end - block.start + 1].concat(replacement));
return pageContent.slice(0, loc.langSectionIndex) + lines.join("\n");
}
// Words only, lower-cased: links, templates, tags and quote marks out, so the page's drawn text and the
// line's wikitext can be compared.
function normalizeWords(text) {
return String(text || "")
.replace(/\[\[(?:[^\]|]*\|)?([^\]]*)\]\]/g, "$1")
.replace(/\{\{[^{}]*\}\}/g, " ")
.replace(/<[^>]*>/g, " ")
.replace(/'{2,}/g, "")
.toLowerCase()
.replace(/[\s.,;:!?"'“”‘’«»()\[\]\-–—…\/]+/g, "");
}
// True when the text a page draws for an example comes from this line.
function exampleMatches(drawnText, example) {
var source = example.text;
if (example.mode === "raw") {
var quote = /\|\s*(?:jêgirtin|jg)\s*=([^|}]*)/.exec(source);
source = quote ? quote[1] : source.replace(/^#+[:*]\s*/, "");
}
var drawn = normalizeWords(drawnText);
var written = normalizeWords(source);
if (!drawn || !written) {
return false;
}
return drawn === written || written.indexOf(drawn.slice(0, 24)) !== -1;
}
module.exports = {
CATEGORY_KEY: CATEGORY_KEY,
RAW_KEY: RAW_KEY,
COVERED_PARAMS: COVERED_PARAMS,
LANG_LABEL: LANG_LABEL,
jederDefs: jederDefs,
fieldSize: fieldSize,
exclusivePartner: exclusivePartner,
boldenPageTitle: boldenPageTitle,
nlToBr: nlToBr,
nlToSpace: nlToSpace,
fetchCategoryTemplates: fetchCategoryTemplates,
buildCategoryTemplateSearch: buildCategoryTemplateSearch,
openTemplateDialog: openTemplateDialog,
buildTemplateParamForm: buildTemplateParamForm,
templateParams: templateParams,
fieldSpecs: fieldSpecs,
jederFieldSpecs: jederFieldSpecs,
fetchFromCitoid: fetchFromCitoid,
citoidFieldsFor: citoidFieldsFor,
withLinkAsUrl: withLinkAsUrl,
today: today,
buildCitation: buildCitation,
looksLikeTemplate: looksLikeTemplate,
looksLikeCitationLine: looksLikeCitationLine,
USEX_NAMES: USEX_NAMES,
EMPTY_EXAMPLE_MARKER: EMPTY_EXAMPLE_MARKER,
parseCitationLine: parseCitationLine,
applyAliases: applyAliases,
keepOrder: keepOrder,
templateAliases: templateAliases,
exampleBlocks: exampleBlocks,
replaceExample: replaceExample,
exampleMatches: exampleMatches
};
}());
// </nowiki>
sotok8w2s60zo5ki3bm3jogsgegyor6
Bikarhêner:Wikihez/Gadget-EtytreeZoom.js
2
1359077
6531984
6370285
2026-09-17T16:44:17Z
Wikihez
7344
test: icon instead of "Mezin bike" on the zoom button
6531984
javascript
text/javascript
// <nowiki>
/*
* Page: [[MediaWiki:Gadget-EtytreeZoom.js]]
*
* Ji [[:en:MediaWiki:Gadget-EtytreeZoom.js]] hatiye wergirtin (guhartoya 88543908, 2025-12-11).
*
* The etymology tree box has a fixed height (Module:etymon/styles.css) and scrolls. This gadget:
* - opens the box on the entry itself (the bottom of the tree, centred);
* - lets the tree be dragged with the mouse (touch screens already move it by swiping);
* - adds a button above the box, when the tree doesn't fit, that scales the whole tree down to the box's width and
* lets the box grow to the tree's height, and back.
* It works on the ".etytree" markup the tree template renders; without that on the page it does nothing.
*/
const elem = s => document.createRange().createContextualFragment(s).firstChild; // helper
const ZOOM_OUT = "Biçûk bike";
const ZOOM_IN = "Mezin bike";
// Codex "search" icon, shown instead of the ZOOM_IN text.
const ZOOM_IN_ICON = '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" aria-hidden="true"><path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M8 1C11.866 1 15 4.13401 15 8C15 9.57231 14.4807 11.0228 13.6055 12.1914L18.707 17.293L17.293 18.707L12.1914 13.6055C11.0228 14.4807 9.57231 15 8 15C4.13401 15 1 11.866 1 8C1 4.13401 4.13401 1 8 1ZM8 3C5.23858 3 3 5.23858 3 8C3 10.7614 5.23858 13 8 13C10.7614 13 13 10.7614 13 8C13 5.23858 10.7614 3 8 3Z"/></svg>';
mw.util.addCSS(`
.etytree-scale {
text-align: right;
padding: 0.2em 0.3em;
font-size: 85%;
}
.etytree-scale-toggle svg {
display: block;
width: 1.2em;
height: 1.2em;
}
.etytree-body {
transform-origin: top left;
}
.etytree .NavContent.etytree-pannable {
cursor: grab;
}
.etytree .NavContent.etytree-panning {
cursor: grabbing;
user-select: none;
}
`);
function overflows(navContent) {
return navContent.scrollWidth > navContent.clientWidth || navContent.scrollHeight > navContent.clientHeight;
}
// Scroll to the entry: the last block, at the bottom of the tree.
function showEntry(navContent, tree) {
navContent.scrollTop = navContent.scrollHeight;
const blocks = tree.querySelectorAll(".etytree-block");
if (blocks.length) {
const b = blocks[blocks.length - 1].getBoundingClientRect();
const n = navContent.getBoundingClientRect();
navContent.scrollLeft += (b.left + b.width / 2) - (n.left + n.width / 2);
}
}
function zoomOut(tree) {
const navContent = tree.parentElement;
if (!navContent.clientWidth) // collapsed
return;
tree.style.transform = "";
navContent.style.overflow = "hidden"; // hide scrollbars
navContent.style.maxHeight = "none"; // let the box grow to the whole tree
const scale = Math.min(1, navContent.clientWidth / tree.scrollWidth);
tree.style.transform = `scale(${scale})`;
navContent.scrollLeft = 0;
navContent.scrollTop = 0;
navContent.style.height = `${tree.scrollHeight * scale}px`;
}
function resetZoom(tree) {
const navContent = tree.parentElement;
tree.style.transform = "";
navContent.style.overflow = "";
navContent.style.maxHeight = "";
navContent.style.height = "";
showEntry(navContent, tree);
}
// Drag to move around the tree. A drag shorter than a few pixels stays a click, so links still work.
function makeDraggable(navContent) {
let startX = 0, startY = 0, left = 0, top = 0, down = false, moved = false;
navContent.addEventListener("mousedown", e => {
if (e.button !== 0 || !navContent.classList.contains("etytree-pannable"))
return;
down = true;
moved = false;
startX = e.clientX;
startY = e.clientY;
left = navContent.scrollLeft;
top = navContent.scrollTop;
});
window.addEventListener("mousemove", e => {
if (!down)
return;
const dx = e.clientX - startX, dy = e.clientY - startY;
if (!moved && Math.abs(dx) + Math.abs(dy) < 5)
return;
moved = true;
navContent.classList.add("etytree-panning");
navContent.scrollLeft = left - dx;
navContent.scrollTop = top - dy;
e.preventDefault();
});
window.addEventListener("mouseup", () => {
if (!down)
return;
down = false;
navContent.classList.remove("etytree-panning");
// The click that ends a drag fires before this timer; after it, clicks count again. Without
// this, a drag released outside the box would swallow the next real click in it.
setTimeout(() => { moved = false; }, 0);
});
// A drag that ends on a link must not follow it.
navContent.addEventListener("click", e => {
if (moved) {
e.preventDefault();
e.stopPropagation();
moved = false;
}
}, true);
}
mw.hook("wikipage.content").add(function($content) {
for (let etytree of $(".etytree", $content).toArray()) {
if (etytree.dataset.etytreeZoom) // already set up
continue;
etytree.dataset.etytreeZoom = "1";
let navContent = etytree.querySelector(".NavContent");
let tree = navContent.querySelector(".etytree-body");
let etytreeScale = elem(`<div class="etytree-scale"><button class="etytree-scale-toggle cdx-button">${ZOOM_OUT}</button></div>`.trim());
let toggle = etytreeScale.querySelector(".etytree-scale-toggle");
let shown = false;
function setLabel(zoomedOut) {
if (zoomedOut)
toggle.innerHTML = ZOOM_IN_ICON;
else
toggle.textContent = ZOOM_OUT;
toggle.title = zoomedOut ? ZOOM_IN : "";
toggle.setAttribute("aria-label", zoomedOut ? ZOOM_IN : ZOOM_OUT);
toggle.classList.toggle("cdx-button--icon-only", zoomedOut);
}
navContent.before(etytreeScale); // outside the box, so it never covers the tree
etytree.treeZoomedOut = false;
function update() {
// Zoomed out, nothing overflows, but the button is still needed to zoom back in. Collapsed: no button.
const open = navContent.clientHeight > 0;
etytreeScale.style.display = open && (etytree.treeZoomedOut || overflows(navContent)) ? "" : "none";
navContent.classList.toggle("etytree-pannable", !etytree.treeZoomedOut && overflows(navContent));
}
toggle.addEventListener("click", () => {
etytree.treeZoomedOut = !etytree.treeZoomedOut;
if (etytree.treeZoomedOut)
zoomOut(tree);
else
resetZoom(tree);
setLabel(etytree.treeZoomedOut);
update();
});
makeDraggable(navContent);
(new ResizeObserver(() => {
if (etytree.treeZoomedOut) // redo the zoom out
zoomOut(tree);
// The box may start collapsed; open on the entry the first time it has a size.
if (!shown && navContent.clientHeight > 0) {
shown = true;
showEntry(navContent, tree);
}
update();
})).observe(navContent);
}
});
// </nowiki>
9wwrxxyl2tkx711p42kuo5rnbsfbwie
6531988
6531984
2026-09-17T16:49:58Z
Wikihez
7344
EtytreeZoom test copy: Commons Codex search icon (CSS mask) instead of "Mezin bike"
6531988
javascript
text/javascript
// <nowiki>
/*
* Page: [[MediaWiki:Gadget-EtytreeZoom.js]]
*
* Ji [[:en:MediaWiki:Gadget-EtytreeZoom.js]] hatiye wergirtin (guhartoya 88543908, 2025-12-11).
*
* The etymology tree box has a fixed height (Module:etymon/styles.css) and scrolls. This gadget:
* - opens the box on the entry itself (the bottom of the tree, centred);
* - lets the tree be dragged with the mouse (touch screens already move it by swiping);
* - adds a button above the box, when the tree doesn't fit, that scales the whole tree down to the box's width and
* lets the box grow to the tree's height, and back.
* It works on the ".etytree" markup the tree template renders; without that on the page it does nothing.
*/
const elem = s => document.createRange().createContextualFragment(s).firstChild; // helper
const ZOOM_OUT = "Biçûk bike";
const ZOOM_IN = "Mezin bike";
// Codex "search" icon, shown instead of the ZOOM_IN text. Drawn as a mask in the text colour, so it
// follows light and dark mode.
const ZOOM_IN_ICON = "https://upload.wikimedia.org/wikipedia/commons/0/0e/Codex_icon_search_%28v2.6%29.svg";
mw.util.addCSS(`
.etytree-scale {
text-align: right;
padding: 0.2em 0.3em;
font-size: 85%;
}
.etytree-zoom-icon {
display: block;
width: 1.2em;
height: 1.2em;
background-color: currentColor;
-webkit-mask: url("${ZOOM_IN_ICON}") center / contain no-repeat;
mask: url("${ZOOM_IN_ICON}") center / contain no-repeat;
}
.etytree-body {
transform-origin: top left;
}
.etytree .NavContent.etytree-pannable {
cursor: grab;
}
.etytree .NavContent.etytree-panning {
cursor: grabbing;
user-select: none;
}
`);
function overflows(navContent) {
return navContent.scrollWidth > navContent.clientWidth || navContent.scrollHeight > navContent.clientHeight;
}
// Scroll to the entry: the last block, at the bottom of the tree.
function showEntry(navContent, tree) {
navContent.scrollTop = navContent.scrollHeight;
const blocks = tree.querySelectorAll(".etytree-block");
if (blocks.length) {
const b = blocks[blocks.length - 1].getBoundingClientRect();
const n = navContent.getBoundingClientRect();
navContent.scrollLeft += (b.left + b.width / 2) - (n.left + n.width / 2);
}
}
function zoomOut(tree) {
const navContent = tree.parentElement;
if (!navContent.clientWidth) // collapsed
return;
tree.style.transform = "";
navContent.style.overflow = "hidden"; // hide scrollbars
navContent.style.maxHeight = "none"; // let the box grow to the whole tree
const scale = Math.min(1, navContent.clientWidth / tree.scrollWidth);
tree.style.transform = `scale(${scale})`;
navContent.scrollLeft = 0;
navContent.scrollTop = 0;
navContent.style.height = `${tree.scrollHeight * scale}px`;
}
function resetZoom(tree) {
const navContent = tree.parentElement;
tree.style.transform = "";
navContent.style.overflow = "";
navContent.style.maxHeight = "";
navContent.style.height = "";
showEntry(navContent, tree);
}
// Drag to move around the tree. A drag shorter than a few pixels stays a click, so links still work.
function makeDraggable(navContent) {
let startX = 0, startY = 0, left = 0, top = 0, down = false, moved = false;
navContent.addEventListener("mousedown", e => {
if (e.button !== 0 || !navContent.classList.contains("etytree-pannable"))
return;
down = true;
moved = false;
startX = e.clientX;
startY = e.clientY;
left = navContent.scrollLeft;
top = navContent.scrollTop;
});
window.addEventListener("mousemove", e => {
if (!down)
return;
const dx = e.clientX - startX, dy = e.clientY - startY;
if (!moved && Math.abs(dx) + Math.abs(dy) < 5)
return;
moved = true;
navContent.classList.add("etytree-panning");
navContent.scrollLeft = left - dx;
navContent.scrollTop = top - dy;
e.preventDefault();
});
window.addEventListener("mouseup", () => {
if (!down)
return;
down = false;
navContent.classList.remove("etytree-panning");
// The click that ends a drag fires before this timer; after it, clicks count again. Without
// this, a drag released outside the box would swallow the next real click in it.
setTimeout(() => { moved = false; }, 0);
});
// A drag that ends on a link must not follow it.
navContent.addEventListener("click", e => {
if (moved) {
e.preventDefault();
e.stopPropagation();
moved = false;
}
}, true);
}
mw.hook("wikipage.content").add(function($content) {
for (let etytree of $(".etytree", $content).toArray()) {
if (etytree.dataset.etytreeZoom) // already set up
continue;
etytree.dataset.etytreeZoom = "1";
let navContent = etytree.querySelector(".NavContent");
let tree = navContent.querySelector(".etytree-body");
let etytreeScale = elem(`<div class="etytree-scale"><button class="etytree-scale-toggle cdx-button">${ZOOM_OUT}</button></div>`.trim());
let toggle = etytreeScale.querySelector(".etytree-scale-toggle");
let shown = false;
function setLabel(zoomedOut) {
if (zoomedOut)
toggle.replaceChildren(elem('<span class="etytree-zoom-icon"></span>'));
else
toggle.textContent = ZOOM_OUT;
toggle.title = zoomedOut ? ZOOM_IN : "";
toggle.setAttribute("aria-label", zoomedOut ? ZOOM_IN : ZOOM_OUT);
toggle.classList.toggle("cdx-button--icon-only", zoomedOut);
}
navContent.before(etytreeScale); // outside the box, so it never covers the tree
etytree.treeZoomedOut = false;
function update() {
// Zoomed out, nothing overflows, but the button is still needed to zoom back in. Collapsed: no button.
const open = navContent.clientHeight > 0;
etytreeScale.style.display = open && (etytree.treeZoomedOut || overflows(navContent)) ? "" : "none";
navContent.classList.toggle("etytree-pannable", !etytree.treeZoomedOut && overflows(navContent));
}
toggle.addEventListener("click", () => {
etytree.treeZoomedOut = !etytree.treeZoomedOut;
if (etytree.treeZoomedOut)
zoomOut(tree);
else
resetZoom(tree);
setLabel(etytree.treeZoomedOut);
update();
});
makeDraggable(navContent);
(new ResizeObserver(() => {
if (etytree.treeZoomedOut) // redo the zoom out
zoomOut(tree);
// The box may start collapsed; open on the entry the first time it has a size.
if (!shown && navContent.clientHeight > 0) {
shown = true;
showEntry(navContent, tree);
}
update();
})).observe(navContent);
}
});
// </nowiki>
ek7zvook6c04s6huqywygzwh40f6zpz
6531990
6531988
2026-09-17T16:55:42Z
Wikihez
7344
EtytreeZoom test copy: icon button over the box's top right corner; trees that don't fit start zoomed out; Codex zoomOut icon when zoomed in
6531990
javascript
text/javascript
// <nowiki>
/*
* Page: [[MediaWiki:Gadget-EtytreeZoom.js]]
*
* Ji [[:en:MediaWiki:Gadget-EtytreeZoom.js]] hatiye wergirtin (guhartoya 88543908, 2025-12-11).
*
* The etymology tree box has a fixed height (Module:etymon/styles.css) and scrolls. For a tree that doesn't
* fit, this gadget:
* - starts zoomed out: the whole tree scaled down to the box's width, with the box as tall as the tree;
* - adds an icon button in the box's top right corner that zooms in to the full-size, scrolling box
* (opened on the entry, the bottom of the tree) and back;
* - lets the zoomed-in tree be dragged with the mouse (touch screens already move it by swiping).
* It works on the ".etytree" markup the tree template renders; without that on the page it does nothing.
*/
const elem = s => document.createRange().createContextualFragment(s).firstChild; // helper
const ZOOM_IN = "Mezin bike";
const ZOOM_OUT = "Biçûk bike";
// Codex icons from Commons, drawn as masks in the text colour so they follow light and dark mode.
const ZOOM_IN_ICON = "https://upload.wikimedia.org/wikipedia/commons/0/0e/Codex_icon_search_%28v2.6%29.svg";
const ZOOM_OUT_ICON = "https://upload.wikimedia.org/wikipedia/commons/2/26/Codex_icon_zoomOut_%28v2.6%29.svg";
mw.util.addCSS(`
.etytree {
position: relative;
}
.etytree-scale {
position: absolute;
z-index: 1;
padding: 0.3em;
}
.etytree-scale-toggle.cdx-button {
min-width: 0;
min-height: 0;
padding: 0.3em;
opacity: 0.85;
}
.etytree-scale-toggle.cdx-button:hover {
opacity: 1;
}
.etytree-zoom-icon {
display: block;
width: 1.1em;
height: 1.1em;
background-color: currentColor;
-webkit-mask: var(--etytree-icon) center / contain no-repeat;
mask: var(--etytree-icon) center / contain no-repeat;
}
.etytree-body {
transform-origin: top left;
}
.etytree .NavContent.etytree-pannable {
cursor: grab;
}
.etytree .NavContent.etytree-panning {
cursor: grabbing;
user-select: none;
}
`);
// Scroll to the entry: the last block, at the bottom of the tree.
function showEntry(navContent, tree) {
navContent.scrollTop = navContent.scrollHeight;
const blocks = tree.querySelectorAll(".etytree-block");
if (blocks.length) {
const b = blocks[blocks.length - 1].getBoundingClientRect();
const n = navContent.getBoundingClientRect();
navContent.scrollLeft += (b.left + b.width / 2) - (n.left + n.width / 2);
}
}
function zoomOut(tree) {
const navContent = tree.parentElement;
if (!navContent.clientWidth) // collapsed
return;
tree.style.transform = "";
navContent.style.overflow = "hidden"; // hide scrollbars
navContent.style.maxHeight = "none"; // let the box grow to the whole tree
const scale = Math.min(1, navContent.clientWidth / tree.scrollWidth);
tree.style.transform = `scale(${scale})`;
navContent.scrollLeft = 0;
navContent.scrollTop = 0;
navContent.style.height = `${tree.scrollHeight * scale}px`;
}
function zoomIn(tree) {
const navContent = tree.parentElement;
tree.style.transform = "";
navContent.style.overflow = "";
navContent.style.maxHeight = "";
navContent.style.height = "";
showEntry(navContent, tree);
}
// Drag to move around the tree. A drag shorter than a few pixels stays a click, so links still work.
function makeDraggable(navContent) {
let startX = 0, startY = 0, left = 0, top = 0, down = false, moved = false;
navContent.addEventListener("mousedown", e => {
if (e.button !== 0 || !navContent.classList.contains("etytree-pannable"))
return;
down = true;
moved = false;
startX = e.clientX;
startY = e.clientY;
left = navContent.scrollLeft;
top = navContent.scrollTop;
});
window.addEventListener("mousemove", e => {
if (!down)
return;
const dx = e.clientX - startX, dy = e.clientY - startY;
if (!moved && Math.abs(dx) + Math.abs(dy) < 5)
return;
moved = true;
navContent.classList.add("etytree-panning");
navContent.scrollLeft = left - dx;
navContent.scrollTop = top - dy;
e.preventDefault();
});
window.addEventListener("mouseup", () => {
if (!down)
return;
down = false;
navContent.classList.remove("etytree-panning");
// The click that ends a drag fires before this timer; after it, clicks count again. Without
// this, a drag released outside the box would swallow the next real click in it.
setTimeout(() => { moved = false; }, 0);
});
// A drag that ends on a link must not follow it.
navContent.addEventListener("click", e => {
if (moved) {
e.preventDefault();
e.stopPropagation();
moved = false;
}
}, true);
}
mw.hook("wikipage.content").add(function($content) {
for (let etytree of $(".etytree", $content).toArray()) {
if (etytree.dataset.etytreeZoom) // already set up
continue;
etytree.dataset.etytreeZoom = "1";
let navContent = etytree.querySelector(".NavContent");
let tree = navContent.querySelector(".etytree-body");
let etytreeScale = elem(`<div class="etytree-scale"><button class="etytree-scale-toggle cdx-button cdx-button--icon-only"><span class="etytree-zoom-icon"></span></button></div>`);
let toggle = etytreeScale.querySelector(".etytree-scale-toggle");
let maxHeight = parseFloat(getComputedStyle(navContent).maxHeight); // the fixed height, from the stylesheet
let started = false;
// A sibling of the box, placed over its top right corner, so it doesn't scroll with the tree.
navContent.before(etytreeScale);
etytree.treeZoomedOut = false;
// Whether the full-size tree is wider or taller than the box.
function tooBig() {
return tree.scrollWidth > navContent.clientWidth + 1 || tree.scrollHeight > maxHeight + 1;
}
function setIcon() {
const label = etytree.treeZoomedOut ? ZOOM_IN : ZOOM_OUT;
const icon = etytree.treeZoomedOut ? ZOOM_IN_ICON : ZOOM_OUT_ICON;
toggle.title = label;
toggle.setAttribute("aria-label", label);
toggle.firstElementChild.style.setProperty("--etytree-icon", `url("${icon}")`);
}
function update() {
const open = navContent.clientHeight > 0;
etytreeScale.style.display = open && tooBig() ? "" : "none";
if (open) {
// Inside the box's border and clear of its scrollbar.
etytreeScale.style.top = `${navContent.offsetTop + navContent.clientTop}px`;
etytreeScale.style.right = `${etytree.clientWidth - navContent.offsetLeft - navContent.clientLeft - navContent.clientWidth}px`;
}
navContent.classList.toggle("etytree-pannable", !etytree.treeZoomedOut && open && tooBig());
}
toggle.addEventListener("click", () => {
etytree.treeZoomedOut = !etytree.treeZoomedOut;
if (etytree.treeZoomedOut)
zoomOut(tree);
else
zoomIn(tree);
setIcon();
update();
});
makeDraggable(navContent);
setIcon();
(new ResizeObserver(() => {
// The box may start collapsed; zoom out the first time it has a size.
if (!started && navContent.clientHeight > 0) {
started = true;
etytree.treeZoomedOut = tooBig();
setIcon();
}
if (etytree.treeZoomedOut) // redo the zoom out
zoomOut(tree);
update();
})).observe(navContent);
}
});
// </nowiki>
h4liio8z6phpfghkv0ox3p24u69o5w7
6531991
6531990
2026-09-17T16:56:40Z
Wikihez
7344
EtytreeZoom test copy: first zoom check runs at setup too, not only on resize
6531991
javascript
text/javascript
// <nowiki>
/*
* Page: [[MediaWiki:Gadget-EtytreeZoom.js]]
*
* Ji [[:en:MediaWiki:Gadget-EtytreeZoom.js]] hatiye wergirtin (guhartoya 88543908, 2025-12-11).
*
* The etymology tree box has a fixed height (Module:etymon/styles.css) and scrolls. For a tree that doesn't
* fit, this gadget:
* - starts zoomed out: the whole tree scaled down to the box's width, with the box as tall as the tree;
* - adds an icon button in the box's top right corner that zooms in to the full-size, scrolling box
* (opened on the entry, the bottom of the tree) and back;
* - lets the zoomed-in tree be dragged with the mouse (touch screens already move it by swiping).
* It works on the ".etytree" markup the tree template renders; without that on the page it does nothing.
*/
const elem = s => document.createRange().createContextualFragment(s).firstChild; // helper
const ZOOM_IN = "Mezin bike";
const ZOOM_OUT = "Biçûk bike";
// Codex icons from Commons, drawn as masks in the text colour so they follow light and dark mode.
const ZOOM_IN_ICON = "https://upload.wikimedia.org/wikipedia/commons/0/0e/Codex_icon_search_%28v2.6%29.svg";
const ZOOM_OUT_ICON = "https://upload.wikimedia.org/wikipedia/commons/2/26/Codex_icon_zoomOut_%28v2.6%29.svg";
mw.util.addCSS(`
.etytree {
position: relative;
}
.etytree-scale {
position: absolute;
z-index: 1;
padding: 0.3em;
}
.etytree-scale-toggle.cdx-button {
min-width: 0;
min-height: 0;
padding: 0.3em;
opacity: 0.85;
}
.etytree-scale-toggle.cdx-button:hover {
opacity: 1;
}
.etytree-zoom-icon {
display: block;
width: 1.1em;
height: 1.1em;
background-color: currentColor;
-webkit-mask: var(--etytree-icon) center / contain no-repeat;
mask: var(--etytree-icon) center / contain no-repeat;
}
.etytree-body {
transform-origin: top left;
}
.etytree .NavContent.etytree-pannable {
cursor: grab;
}
.etytree .NavContent.etytree-panning {
cursor: grabbing;
user-select: none;
}
`);
// Scroll to the entry: the last block, at the bottom of the tree.
function showEntry(navContent, tree) {
navContent.scrollTop = navContent.scrollHeight;
const blocks = tree.querySelectorAll(".etytree-block");
if (blocks.length) {
const b = blocks[blocks.length - 1].getBoundingClientRect();
const n = navContent.getBoundingClientRect();
navContent.scrollLeft += (b.left + b.width / 2) - (n.left + n.width / 2);
}
}
function zoomOut(tree) {
const navContent = tree.parentElement;
if (!navContent.clientWidth) // collapsed
return;
tree.style.transform = "";
navContent.style.overflow = "hidden"; // hide scrollbars
navContent.style.maxHeight = "none"; // let the box grow to the whole tree
const scale = Math.min(1, navContent.clientWidth / tree.scrollWidth);
tree.style.transform = `scale(${scale})`;
navContent.scrollLeft = 0;
navContent.scrollTop = 0;
navContent.style.height = `${tree.scrollHeight * scale}px`;
}
function zoomIn(tree) {
const navContent = tree.parentElement;
tree.style.transform = "";
navContent.style.overflow = "";
navContent.style.maxHeight = "";
navContent.style.height = "";
showEntry(navContent, tree);
}
// Drag to move around the tree. A drag shorter than a few pixels stays a click, so links still work.
function makeDraggable(navContent) {
let startX = 0, startY = 0, left = 0, top = 0, down = false, moved = false;
navContent.addEventListener("mousedown", e => {
if (e.button !== 0 || !navContent.classList.contains("etytree-pannable"))
return;
down = true;
moved = false;
startX = e.clientX;
startY = e.clientY;
left = navContent.scrollLeft;
top = navContent.scrollTop;
});
window.addEventListener("mousemove", e => {
if (!down)
return;
const dx = e.clientX - startX, dy = e.clientY - startY;
if (!moved && Math.abs(dx) + Math.abs(dy) < 5)
return;
moved = true;
navContent.classList.add("etytree-panning");
navContent.scrollLeft = left - dx;
navContent.scrollTop = top - dy;
e.preventDefault();
});
window.addEventListener("mouseup", () => {
if (!down)
return;
down = false;
navContent.classList.remove("etytree-panning");
// The click that ends a drag fires before this timer; after it, clicks count again. Without
// this, a drag released outside the box would swallow the next real click in it.
setTimeout(() => { moved = false; }, 0);
});
// A drag that ends on a link must not follow it.
navContent.addEventListener("click", e => {
if (moved) {
e.preventDefault();
e.stopPropagation();
moved = false;
}
}, true);
}
mw.hook("wikipage.content").add(function($content) {
for (let etytree of $(".etytree", $content).toArray()) {
if (etytree.dataset.etytreeZoom) // already set up
continue;
etytree.dataset.etytreeZoom = "1";
let navContent = etytree.querySelector(".NavContent");
let tree = navContent.querySelector(".etytree-body");
let etytreeScale = elem(`<div class="etytree-scale"><button class="etytree-scale-toggle cdx-button cdx-button--icon-only"><span class="etytree-zoom-icon"></span></button></div>`);
let toggle = etytreeScale.querySelector(".etytree-scale-toggle");
let maxHeight = parseFloat(getComputedStyle(navContent).maxHeight); // the fixed height, from the stylesheet
let started = false;
// A sibling of the box, placed over its top right corner, so it doesn't scroll with the tree.
navContent.before(etytreeScale);
etytree.treeZoomedOut = false;
// Whether the full-size tree is wider or taller than the box.
function tooBig() {
return tree.scrollWidth > navContent.clientWidth + 1 || tree.scrollHeight > maxHeight + 1;
}
function setIcon() {
const label = etytree.treeZoomedOut ? ZOOM_IN : ZOOM_OUT;
const icon = etytree.treeZoomedOut ? ZOOM_IN_ICON : ZOOM_OUT_ICON;
toggle.title = label;
toggle.setAttribute("aria-label", label);
toggle.firstElementChild.style.setProperty("--etytree-icon", `url("${icon}")`);
}
function update() {
const open = navContent.clientHeight > 0;
etytreeScale.style.display = open && tooBig() ? "" : "none";
if (open) {
// Inside the box's border and clear of its scrollbar.
etytreeScale.style.top = `${navContent.offsetTop + navContent.clientTop}px`;
etytreeScale.style.right = `${etytree.clientWidth - navContent.offsetLeft - navContent.clientLeft - navContent.clientWidth}px`;
}
navContent.classList.toggle("etytree-pannable", !etytree.treeZoomedOut && open && tooBig());
}
toggle.addEventListener("click", () => {
etytree.treeZoomedOut = !etytree.treeZoomedOut;
if (etytree.treeZoomedOut)
zoomOut(tree);
else
zoomIn(tree);
setIcon();
update();
});
// The box may start collapsed; zoom out the first time it has a size.
function refresh() {
if (!started && navContent.clientHeight > 0) {
started = true;
etytree.treeZoomedOut = tooBig();
setIcon();
}
if (etytree.treeZoomedOut) // redo the zoom out
zoomOut(tree);
update();
}
makeDraggable(navContent);
setIcon();
refresh();
(new ResizeObserver(refresh)).observe(navContent);
}
});
// </nowiki>
1njws4q4xtrk1ktifr5ico5cqcxvmto
6531997
6531991
2026-09-17T19:57:06Z
Wikihez
7344
EtytreeZoom test copy: Codex zoomIn icon instead of the search icon
6531997
javascript
text/javascript
// <nowiki>
/*
* Page: [[MediaWiki:Gadget-EtytreeZoom.js]]
*
* Ji [[:en:MediaWiki:Gadget-EtytreeZoom.js]] hatiye wergirtin (guhartoya 88543908, 2025-12-11).
*
* The etymology tree box has a fixed height (Module:etymon/styles.css) and scrolls. For a tree that doesn't
* fit, this gadget:
* - starts zoomed out: the whole tree scaled down to the box's width, with the box as tall as the tree;
* - adds an icon button in the box's top right corner that zooms in to the full-size, scrolling box
* (opened on the entry, the bottom of the tree) and back;
* - lets the zoomed-in tree be dragged with the mouse (touch screens already move it by swiping).
* It works on the ".etytree" markup the tree template renders; without that on the page it does nothing.
*/
const elem = s => document.createRange().createContextualFragment(s).firstChild; // helper
const ZOOM_IN = "Mezin bike";
const ZOOM_OUT = "Biçûk bike";
// Codex icons from Commons, drawn as masks in the text colour so they follow light and dark mode.
const ZOOM_IN_ICON = "https://upload.wikimedia.org/wikipedia/commons/0/0d/Codex_icon_zoomIn_%28v2.6%29.svg";
const ZOOM_OUT_ICON = "https://upload.wikimedia.org/wikipedia/commons/2/26/Codex_icon_zoomOut_%28v2.6%29.svg";
mw.util.addCSS(`
.etytree {
position: relative;
}
.etytree-scale {
position: absolute;
z-index: 1;
padding: 0.3em;
}
.etytree-scale-toggle.cdx-button {
min-width: 0;
min-height: 0;
padding: 0.3em;
opacity: 0.85;
}
.etytree-scale-toggle.cdx-button:hover {
opacity: 1;
}
.etytree-zoom-icon {
display: block;
width: 1.1em;
height: 1.1em;
background-color: currentColor;
-webkit-mask: var(--etytree-icon) center / contain no-repeat;
mask: var(--etytree-icon) center / contain no-repeat;
}
.etytree-body {
transform-origin: top left;
}
.etytree .NavContent.etytree-pannable {
cursor: grab;
}
.etytree .NavContent.etytree-panning {
cursor: grabbing;
user-select: none;
}
`);
// Scroll to the entry: the last block, at the bottom of the tree.
function showEntry(navContent, tree) {
navContent.scrollTop = navContent.scrollHeight;
const blocks = tree.querySelectorAll(".etytree-block");
if (blocks.length) {
const b = blocks[blocks.length - 1].getBoundingClientRect();
const n = navContent.getBoundingClientRect();
navContent.scrollLeft += (b.left + b.width / 2) - (n.left + n.width / 2);
}
}
function zoomOut(tree) {
const navContent = tree.parentElement;
if (!navContent.clientWidth) // collapsed
return;
tree.style.transform = "";
navContent.style.overflow = "hidden"; // hide scrollbars
navContent.style.maxHeight = "none"; // let the box grow to the whole tree
const scale = Math.min(1, navContent.clientWidth / tree.scrollWidth);
tree.style.transform = `scale(${scale})`;
navContent.scrollLeft = 0;
navContent.scrollTop = 0;
navContent.style.height = `${tree.scrollHeight * scale}px`;
}
function zoomIn(tree) {
const navContent = tree.parentElement;
tree.style.transform = "";
navContent.style.overflow = "";
navContent.style.maxHeight = "";
navContent.style.height = "";
showEntry(navContent, tree);
}
// Drag to move around the tree. A drag shorter than a few pixels stays a click, so links still work.
function makeDraggable(navContent) {
let startX = 0, startY = 0, left = 0, top = 0, down = false, moved = false;
navContent.addEventListener("mousedown", e => {
if (e.button !== 0 || !navContent.classList.contains("etytree-pannable"))
return;
down = true;
moved = false;
startX = e.clientX;
startY = e.clientY;
left = navContent.scrollLeft;
top = navContent.scrollTop;
});
window.addEventListener("mousemove", e => {
if (!down)
return;
const dx = e.clientX - startX, dy = e.clientY - startY;
if (!moved && Math.abs(dx) + Math.abs(dy) < 5)
return;
moved = true;
navContent.classList.add("etytree-panning");
navContent.scrollLeft = left - dx;
navContent.scrollTop = top - dy;
e.preventDefault();
});
window.addEventListener("mouseup", () => {
if (!down)
return;
down = false;
navContent.classList.remove("etytree-panning");
// The click that ends a drag fires before this timer; after it, clicks count again. Without
// this, a drag released outside the box would swallow the next real click in it.
setTimeout(() => { moved = false; }, 0);
});
// A drag that ends on a link must not follow it.
navContent.addEventListener("click", e => {
if (moved) {
e.preventDefault();
e.stopPropagation();
moved = false;
}
}, true);
}
mw.hook("wikipage.content").add(function($content) {
for (let etytree of $(".etytree", $content).toArray()) {
if (etytree.dataset.etytreeZoom) // already set up
continue;
etytree.dataset.etytreeZoom = "1";
let navContent = etytree.querySelector(".NavContent");
let tree = navContent.querySelector(".etytree-body");
let etytreeScale = elem(`<div class="etytree-scale"><button class="etytree-scale-toggle cdx-button cdx-button--icon-only"><span class="etytree-zoom-icon"></span></button></div>`);
let toggle = etytreeScale.querySelector(".etytree-scale-toggle");
let maxHeight = parseFloat(getComputedStyle(navContent).maxHeight); // the fixed height, from the stylesheet
let started = false;
// A sibling of the box, placed over its top right corner, so it doesn't scroll with the tree.
navContent.before(etytreeScale);
etytree.treeZoomedOut = false;
// Whether the full-size tree is wider or taller than the box.
function tooBig() {
return tree.scrollWidth > navContent.clientWidth + 1 || tree.scrollHeight > maxHeight + 1;
}
function setIcon() {
const label = etytree.treeZoomedOut ? ZOOM_IN : ZOOM_OUT;
const icon = etytree.treeZoomedOut ? ZOOM_IN_ICON : ZOOM_OUT_ICON;
toggle.title = label;
toggle.setAttribute("aria-label", label);
toggle.firstElementChild.style.setProperty("--etytree-icon", `url("${icon}")`);
}
function update() {
const open = navContent.clientHeight > 0;
etytreeScale.style.display = open && tooBig() ? "" : "none";
if (open) {
// Inside the box's border and clear of its scrollbar.
etytreeScale.style.top = `${navContent.offsetTop + navContent.clientTop}px`;
etytreeScale.style.right = `${etytree.clientWidth - navContent.offsetLeft - navContent.clientLeft - navContent.clientWidth}px`;
}
navContent.classList.toggle("etytree-pannable", !etytree.treeZoomedOut && open && tooBig());
}
toggle.addEventListener("click", () => {
etytree.treeZoomedOut = !etytree.treeZoomedOut;
if (etytree.treeZoomedOut)
zoomOut(tree);
else
zoomIn(tree);
setIcon();
update();
});
// The box may start collapsed; zoom out the first time it has a size.
function refresh() {
if (!started && navContent.clientHeight > 0) {
started = true;
etytree.treeZoomedOut = tooBig();
setIcon();
}
if (etytree.treeZoomedOut) // redo the zoom out
zoomOut(tree);
update();
}
makeDraggable(navContent);
setIcon();
refresh();
(new ResizeObserver(refresh)).observe(navContent);
}
});
// </nowiki>
lbhh96sp2d69uj6lwi4gm4aatteby3n
MediaWiki:Gadget-EtytreeZoom.js
8
1359170
6531936
6435075
2026-09-17T13:26:32Z
Wikihez
7344
zoom button on its own row above the tree box instead of over the tree; hidden when collapsed; no double setup
6531936
javascript
text/javascript
// <nowiki>
/*
* Page: [[MediaWiki:Gadget-EtytreeZoom.js]]
*
* Ji [[:en:MediaWiki:Gadget-EtytreeZoom.js]] hatiye wergirtin (guhartoya 88543908, 2025-12-11).
*
* The etymology tree box has a fixed height (Module:etymon/styles.css) and scrolls. This gadget:
* - opens the box on the entry itself (the bottom of the tree, centred);
* - lets the tree be dragged with the mouse (touch screens already move it by swiping);
* - adds a button above the box, when the tree doesn't fit, that scales the whole tree down to the box's width and
* lets the box grow to the tree's height, and back.
* It works on the ".etytree" markup the tree template renders; without that on the page it does nothing.
*/
const elem = s => document.createRange().createContextualFragment(s).firstChild; // helper
const ZOOM_OUT = "Biçûk bike";
const ZOOM_IN = "Mezin bike";
mw.util.addCSS(`
.etytree-scale {
text-align: right;
padding: 0.2em 0.3em;
font-size: 85%;
}
.etytree-body {
transform-origin: top left;
}
.etytree .NavContent.etytree-pannable {
cursor: grab;
}
.etytree .NavContent.etytree-panning {
cursor: grabbing;
user-select: none;
}
`);
function overflows(navContent) {
return navContent.scrollWidth > navContent.clientWidth || navContent.scrollHeight > navContent.clientHeight;
}
// Scroll to the entry: the last block, at the bottom of the tree.
function showEntry(navContent, tree) {
navContent.scrollTop = navContent.scrollHeight;
const blocks = tree.querySelectorAll(".etytree-block");
if (blocks.length) {
const b = blocks[blocks.length - 1].getBoundingClientRect();
const n = navContent.getBoundingClientRect();
navContent.scrollLeft += (b.left + b.width / 2) - (n.left + n.width / 2);
}
}
function zoomOut(tree) {
const navContent = tree.parentElement;
if (!navContent.clientWidth) // collapsed
return;
tree.style.transform = "";
navContent.style.overflow = "hidden"; // hide scrollbars
navContent.style.maxHeight = "none"; // let the box grow to the whole tree
const scale = Math.min(1, navContent.clientWidth / tree.scrollWidth);
tree.style.transform = `scale(${scale})`;
navContent.scrollLeft = 0;
navContent.scrollTop = 0;
navContent.style.height = `${tree.scrollHeight * scale}px`;
}
function resetZoom(tree) {
const navContent = tree.parentElement;
tree.style.transform = "";
navContent.style.overflow = "";
navContent.style.maxHeight = "";
navContent.style.height = "";
showEntry(navContent, tree);
}
// Drag to move around the tree. A drag shorter than a few pixels stays a click, so links still work.
function makeDraggable(navContent) {
let startX = 0, startY = 0, left = 0, top = 0, down = false, moved = false;
navContent.addEventListener("mousedown", e => {
if (e.button !== 0 || !navContent.classList.contains("etytree-pannable"))
return;
down = true;
moved = false;
startX = e.clientX;
startY = e.clientY;
left = navContent.scrollLeft;
top = navContent.scrollTop;
});
window.addEventListener("mousemove", e => {
if (!down)
return;
const dx = e.clientX - startX, dy = e.clientY - startY;
if (!moved && Math.abs(dx) + Math.abs(dy) < 5)
return;
moved = true;
navContent.classList.add("etytree-panning");
navContent.scrollLeft = left - dx;
navContent.scrollTop = top - dy;
e.preventDefault();
});
window.addEventListener("mouseup", () => {
if (!down)
return;
down = false;
navContent.classList.remove("etytree-panning");
// The click that ends a drag fires before this timer; after it, clicks count again. Without
// this, a drag released outside the box would swallow the next real click in it.
setTimeout(() => { moved = false; }, 0);
});
// A drag that ends on a link must not follow it.
navContent.addEventListener("click", e => {
if (moved) {
e.preventDefault();
e.stopPropagation();
moved = false;
}
}, true);
}
mw.hook("wikipage.content").add(function($content) {
for (let etytree of $(".etytree", $content).toArray()) {
if (etytree.dataset.etytreeZoom) // already set up
continue;
etytree.dataset.etytreeZoom = "1";
let navContent = etytree.querySelector(".NavContent");
let tree = navContent.querySelector(".etytree-body");
let etytreeScale = elem(`<div class="etytree-scale"><button class="etytree-scale-toggle cdx-button">${ZOOM_OUT}</button></div>`.trim());
let toggle = etytreeScale.querySelector(".etytree-scale-toggle");
let shown = false;
navContent.before(etytreeScale); // outside the box, so it never covers the tree
etytree.treeZoomedOut = false;
function update() {
// Zoomed out, nothing overflows, but the button is still needed to zoom back in. Collapsed: no button.
const open = navContent.clientHeight > 0;
etytreeScale.style.display = open && (etytree.treeZoomedOut || overflows(navContent)) ? "" : "none";
navContent.classList.toggle("etytree-pannable", !etytree.treeZoomedOut && overflows(navContent));
}
toggle.addEventListener("click", () => {
etytree.treeZoomedOut = !etytree.treeZoomedOut;
if (etytree.treeZoomedOut)
zoomOut(tree);
else
resetZoom(tree);
toggle.textContent = etytree.treeZoomedOut ? ZOOM_IN : ZOOM_OUT;
update();
});
makeDraggable(navContent);
(new ResizeObserver(() => {
if (etytree.treeZoomedOut) // redo the zoom out
zoomOut(tree);
// The box may start collapsed; open on the entry the first time it has a size.
if (!shown && navContent.clientHeight > 0) {
shown = true;
showEntry(navContent, tree);
}
update();
})).observe(navContent);
}
});
// </nowiki>
q7aq1ilrs57mnv4iv328go423kwjpd8
𐭡𐭩𐭲𐭠
0
1359251
6531923
2026-09-17T13:04:22Z
Wikihez
7344
[[Bikarhêner:Wikihez/templateScript2.js|TS]]: Ji [[en:𐭡𐭩𐭲𐭠|en.wikt]]
6531923
wikitext
text/x-wiki
== {{ziman|pal}} ==
==== Etîmolojî ====
{{etîmon|pal|:mîr|peo:*vahanam<t:xanî><etimon:mîr<ira-pro:*wāhana<t:habîtasyon><etimon:ji<*Hwah->>>>|dar=1|metn=++}}
=== Navdêr ===
{{head|pal|noun|tr=BYTA|ts=xānag|sc=Phli}}
# [[xanî]]
==== Dûnde ====
* {{dardû|fa-cls|خَانَه}}
d5lkdax70rew6jvy0vmppk72azcyhvl
6531924
6531923
2026-09-17T13:05:54Z
Wikihez
7344
/* Navdêr */ (bi [[MediaWiki:Gadget-AjaxEdit.js|AjaxEdit]]ê re)
6531924
wikitext
text/x-wiki
== {{ziman|pal}} ==
==== Etîmolojî ====
{{etîmon|pal|:mîr|peo:*vahanam<t:xanî><etimon:mîr<ira-pro:*wāhana<t:habîtasyon><etimon:ji<*Hwah->>>>|dar=1|metn=++}}
=== Navdêr ===
{{navdêr|pal|tr=BYTA|ts=xānag|sc=Phli}}
# [[xanî]]
==== Dûnde ====
* {{dardû|fa-cls|خَانَه}}
myewan0qfaoy4isk5n7tftpjaf7ucd2
6531925
6531924
2026-09-17T13:12:46Z
Wikihez
7344
/* Farisiya navîn */ (bi [[MediaWiki:Gadget-AjaxEdit.js|AjaxEdit]]ê re)
6531925
wikitext
text/x-wiki
== {{ziman|pal}} ==
=== Navdêr ===
{{navdêr|pal|tr=BYTA|ts=xānag|sc=Phli}}
# [[xanî]]
==== Etîmolojî ====
{{etîmon|pal|:mîr|peo:*vahanam<t:xanî><etimon:mîr<ira-pro:*wāhana<t:habîtasyon><etimon:ji<*Hwah->>>>|dar=1|metn=++}}
==== Dûnde ====
* {{dardû|fa-cls|خَانَه}}
a0jhfcte1io19ljz352qowuc910odov
Gotûbêja bikarhêner:SHB2000
3
1359253
6532012
2026-09-18T02:37:02Z
Max
11795
Maxî/ê navê [[Gotûbêja bikarhêner:SHB2000]] weke [[Gotûbêja bikarhêner:SHB]] guhart: Automatically moved page while renaming the user "[[Special:CentralAuth/SHB2000|SHB2000]]" to "[[Special:CentralAuth/SHB|SHB]]"
6532012
wikitext
text/x-wiki
#BERALÎKIRIN [[Gotûbêja bikarhêner:SHB]]
73b6wsrok2q32yzvoq7e8avbn8kpafk