Wikipedia
testwiki
https://test.wikipedia.org/wiki/Main_Page
MediaWiki 1.47.0-wmf.9
first-letter
Media
Special
Talk
User
User talk
Wikipedia
Wikipedia talk
File
File talk
MediaWiki
MediaWiki talk
Template
Template talk
Help
Help talk
Category
Category talk
Thread
Thread talk
Summary
Summary talk
Test namespace 1
Test namespace 1 talk
Test namespace 2
Test namespace 2 talk
Draft
Draft talk
Campaign
Campaign talk
TimedText
TimedText talk
Module
Module talk
SecurePoll
SecurePoll talk
CNBanner
CNBanner talk
Translations
Translations talk
Event
Event talk
Topic
Newsletter
Newsletter talk
User:STyx/Tests
2
1805
749654
63689
2026-07-03T12:22:47Z
STyx
131
749654
wikitext
text/x-wiki
==Functions==
SUBPAGENAME is {{SUBPAGENAME}}. OK
PAGENAME is {{PAGENAME}}. OK
NAMESPACE is {{NAMESPACE}}. OK
see also : [[Template:Qif/SUBPAGENAME]], [[Help:Autocomment/SUBPAGENAME]], [http://bugzilla.wikipedia.org/show_bug.cgi?id=5630 bug 5630].
{{URLENCODE: x y %@ }}
{{URLENCODE:x y %@}}
==Test "Modify include"==
*[[Test "Modify include"]]
==template returning color==
*[[Template talk:GetColor]]
==Test "[[m:MathStatFunctions]]"==
{{#expr: {{#sin: 0.5}} }} :(
== Test template call ==
kg1twe5hbj2u08v0eciduu55x06iixt
749882
749654
2026-07-04T09:14:50Z
STyx
131
/* Test template call */
749882
wikitext
text/x-wiki
==Functions==
SUBPAGENAME is {{SUBPAGENAME}}. OK
PAGENAME is {{PAGENAME}}. OK
NAMESPACE is {{NAMESPACE}}. OK
see also : [[Template:Qif/SUBPAGENAME]], [[Help:Autocomment/SUBPAGENAME]], [http://bugzilla.wikipedia.org/show_bug.cgi?id=5630 bug 5630].
{{URLENCODE: x y %@ }}
{{URLENCODE:x y %@}}
==Test "Modify include"==
*[[Test "Modify include"]]
==template returning color==
*[[Template talk:GetColor]]
==Test "[[m:MathStatFunctions]]"==
{{#expr: {{#sin: 0.5}} }} :(
== Test template call ==
# = | {{|}} { }
# {{=}} {{!}} {{|}} {{LBrace}} {{RBrace}}
# 1 {{Call1|1}}
# 2 {{Call1|Call1}}
# 3 {{Call1|1=1{{!}}A{{!}}B{{!}}C{{!}}D}}
# 4 {{Call1|1={{Call1|1=1{{!}}A{{!}}B{{!}}C{{!}}D}}}}
# {{arg1}}
# - {{arg1|{-}}}
# + {{arg1|{-}}}
# } {{arg1|} }}}
# ~ {{arg1|{{RBrace}}{{RBrace}}
# ) {{arg1|{{LBrace}}{arg1{{!}}}{{RBrace}}}}
# ] {{arg1|{{LBrace}}{{LBrace}}arg1{{!}}{{RBrace}}{{RBrace}}}}
# # {{oups}}
# = {{oups|2{{=}}2}}
# 3 {{oups|{{arg1|3}}=3?}}
# ! {{oups||arg2{{!}}arg2}}
# @ {{arg1|{{arg1|1=ARG1}}}}
# ? {{ oups | 1 = 1 |arg2{{!}}arg2| 3= ?}}
# A {{{{arg1}}|arg1{{!}}arg1}}
# B {{{{arg1|{{arg1|oups{{!}}1}}}}|arg1{{!}}arg2?}}
# C {{{{arg1|{{arg1|oups{{!}}1}}}}|arg1{{!}}arg2?}}
# {{Arg?|3|3=ok}}
# {{Arg?|0}}
sfh2ozljfbqf5astxbl95h25qvf252n
Category:Testing1
14
45037
749747
117377
2026-07-03T19:22:23Z
Solidest
54422
[[Википедия:Обсуждение категорий/Июль_2026#[[:Category:Testing1]]|номинация к обсуждению]] ([[Википедия:Гаджеты/Remover|Remover]])
749747
wikitext
text/x-wiki
<noinclude>{{Обсуждаемая категория|2026-07-03}}</noinclude>
This is just a testing category.
jxzoum6y9rkwpe0umodyg0r1x0l71dg
749756
749747
2026-07-03T19:25:19Z
Solidest
54422
обсуждение категории завершено ([[Википедия:Гаджеты/Remover|Remover]])
749756
wikitext
text/x-wiki
This is just a testing category.
52iupcqdy84owufz4h24odm4r7dgwo7
749758
749756
2026-07-03T19:25:29Z
Solidest
54422
[[Википедия:Обсуждение категорий/Июль_2026#Category:Testing1|номинация к обсуждению]] ([[Википедия:Гаджеты/Remover|Remover]])
749758
wikitext
text/x-wiki
<noinclude>{{Обсуждаемая категория|2026-07-03}}</noinclude>
This is just a testing category.
jxzoum6y9rkwpe0umodyg0r1x0l71dg
749761
749758
2026-07-03T19:26:21Z
Solidest
54422
обсуждение категории завершено ([[Википедия:Гаджеты/Remover|Remover]])
749761
wikitext
text/x-wiki
This is just a testing category.
52iupcqdy84owufz4h24odm4r7dgwo7
749762
749761
2026-07-03T19:26:29Z
Solidest
54422
[[Википедия:Обсуждение категорий/Июль_2026#Category:Testing1 → Category:New|номинация к переименованию]] ([[Википедия:Гаджеты/Remover|Remover]])
749762
wikitext
text/x-wiki
<noinclude>{{Категория к переименованию|2026-07-03|New}}</noinclude>
This is just a testing category.
59jhggbzk6afb8cj9o6qg67bdpsamfa
Template:Extra chronology
10
45751
749783
254483
2026-07-03T20:26:54Z
~2026-38011-52
74711
749783
wikitext
text/x-wiki
<includeonly>{{{{{|safesubst:}}}#invoke:Unsubst-infobox||$params=artist,type,prev_title,prev_title2,prev_year,title,title2,year,next_title,next_title2,next_year|$extra=prev_title2,title2,next_title2|$indent=1|$flags=override|$B={{Infobox
| child = yes
| headerstyle = background: {{Template:Infobox album/color|{{{type|}}}}};color:inherit;
| header1 = {{#if:{{{artist|}}}|{{#invoke:String|replace|error_category=Music infoboxes with Module:String errors{{!}}C|{{{artist}}}{{#switch:{{lc:{{{type|}}}}}|single|[[single (music)|single]]|singles|[[single (music)|singles]]= singles|video= video}}|singles singles|singles}} chronology}}
| data2 =
{{(!}} style="color: inherit; background: transparent; width: 100%; min-width: 100%; border-collapse: collapse; display: inline-table;"
{{!}}- style="line-height: 1.4em;"
{{!}} style="width: 33%; text-align: center; vertical-align: top; padding: .2em .1em .2em 0" {{!}} {{#if:{{{artist|}}}||<hr/>}}{{#if:{{{prev_title|}}}|{{#switch:{{lc:{{{type|}}}}}|single|[[single (music)|single]]|singles|[[single (music)|singles]]="{{{prev_title}}}" {{#if:{{{prev_version|}}}|({{{prev_version}}}) }}{{#if:{{{prev_concurrent|}}}|{{sp}}and "{{{prev_concurrent|}}}"}}{{#if:{{{prev_title2|}}}| / "{{{prev_title2|}}}"}}|''{{{prev_title}}}''}}{{#if:{{{prev_year|}}}|<br />({{{prev_year}}})}}}}
{{!}} style="width: 33%; text-align: center; vertical-align: top; padding: .2em .1em" {{!}} {{#if:{{{artist|}}}||<hr/>}}{{#if:{{{title|}}}|{{#switch:{{lc:{{{type|}}}}}|single|[[single (music)|single]]|singles|[[single (music)|singles]]={{#if:{{{title2|}}}|{{#if:{{{concurrent_title|}}}|"{{{concurrent_title|}}}" and{{sp}}|}}"{{#switch:{{{title}}}|{{BASEPAGENAME}}|{{PAGENAMEBASE}}='''{{{title}}}'''|{{{title}}}}}"{{#if:{{{version|}}}| ({{{version}}})}} / "{{#switch:{{{title2}}}|{{BASEPAGENAME}}|{{PAGENAMEBASE}}='''{{{title2}}}'''|{{{title2}}}}}"|"'''{{{title}}}'''"{{#if:{{{version|}}}| ({{{version}}})}}}}|'''''{{{title}}}'''''}}{{#if:{{{year|}}}|<br />({{{year}}})}}}}
{{!}} style="width: 33%; text-align: center; vertical-align: top; padding: .2em 0 .2em .1em" {{!}} {{#if:{{{artist|}}}||<hr/>}}{{#if:{{{next_title|}}}|{{#switch:{{lc:{{{type|}}}}}|single|[[single (music)|single]]|singles|[[single (music)|singles]]="{{{next_title}}}" {{#if:{{{next_version|}}}|({{{next_version}}}) }}{{#if:{{{next_concurrent|}}}|{{sp}}and "{{{next_concurrent|}}}"}}{{#if:{{{next_title2|}}}| / "{{{next_title2|}}}"}}|''{{{next_title}}}''}}{{#if:{{{next_year|}}}|<br />({{{next_year}}})}}}}
{{!}}-
{{!)}}
}}{{#if:{{#ifeq:{{str left|{{{prev_title|}}}|22}}|<div class="plainlist"|1|}}{{#ifeq:{{str left|{{{title|}}}|22}}|<div class="plainlist"|1|}}{{#ifeq:{{str left|{{{next_title|}}}|22}}|<div class="plainlist"|1|}}|[[Category:Pages using infobox chronology parameters with plainlists]]}}{{#if:{{#invoke:String|match|error_category=Music infoboxes with Module:String errors{{!}}C|1={{{prev_title|}}}{{{prev_year|}}}{{{title|}}}{{{year|}}}{{{next_title|}}}{{{next_year|}}}{{{artist|}}}{{{type|}}}{{{prev_title2|}}}{{{title2|}}}{{{next_title2|}}}x|2=</?t[drh][ >]|nomatch=}}|[[Category:Music infoboxes with malformed table placement|C]]}}{{#invoke:Check for unknown parameters|check|unknown={{main other|[[Category:Pages using extra chronology with unknown parameters|_VALUE_{{PAGENAME}}]]}}|preview=Page using [[Template:Extra chronology]] with unknown parameter "_VALUE_"|ignoreblank=y| | artist | next_concurrent | next_title | next_title2 | next_year | prev_concurrent | prev_title | prev_title2 | prev_year | title | title2 | type | year | version | prev_version | next_version}}}}</includeonly><noinclude>
{{Documentation}}
</noinclude>
k2a8gcu6clio6zliky7rkw1fjoxhg9f
Page401
0
49472
749886
372815
2026-07-04T09:48:14Z
~2026-38152-27
74715
749886
wikitext
text/x-wiki
'''Page401'''
This in test page No. 401
[[Category:Category with multiple files]]
cpjka5bcwcx055dtb6l1iwpf3jlzfla
Template:Infobox single/doc
10
77826
749776
742991
2026-07-03T19:53:57Z
~2026-38011-52
74711
749776
wikitext
text/x-wiki
{{Documentation subpage}}
{{Music infobox header}}
==Usage==
{{#ifeq:{{FULLPAGENAME}}|Wikipedia:WikiProject Songs||{{for|non-single songs|Template:Infobox song}}}}
This is the template for a [[wiktionary:single|single]] (A- or B-side), '''not''' a [[wiktionary:song|song]]. {{See also|Template:Infobox song}}
==Code==
{{Infobox single <!-- See Wikipedia:WikiProject_Songs -->
| Name = Country House
| Cover =
| Alt =
| Border =
| Type = single
| Artist = [[Blur (band)|Blur]]
| Album = [[The Great Escape (Blur album)|The Great Escape]]
| Released = {{Start date|1995|08|14}} <small>([[United Kingdom|UK]])</small>
| Format = {{Flatlist |
* [[CD single|CD]]
* [[Gramophone record|7" vinyl]]
* [[cassette single|cassette]]
}}
| Recorded = 1995
| Studio =
| Venue =
| Genre = [[Britpop]]
| Length = {{Duration|m=3|s=57}}
| Label = {{flat list|
* [[Food Records|Food]]
* [[EMI]]
}}
| Writer =
| Producer = [[Stephen Street]]
| Prev_title = [[To the End (Blur song)|To the End]]
| Prev_year = 1994
| Next_title = [[The Universal (song)|The Universal]]
| Next_year = 1995
| Misc =
}}
<pre style="overflow:auto;">
{{Infobox single <!-- See Wikipedia:WikiProject_Songs -->
| Name =
| Cover = <!-- just the file name -->
| Border =
| Alt =
| Caption =
| Artist =
| Album =
| A-side =
| B-side =
| Released = <!-- {{Start date|YYYY|MM|DD}} -->
| Format =
| Recorded =
| Genre =
| Length = <!-- {{Duration|m=MM|s=SS}} -->
| Label =
| Writer =
| Producer =
| Certification =
| Chronology =
| Last single =
| This single =
| Next single =
| Misc =
}}
</pre>
== Parameters ==
;Name : This field should contain the name of the single. This field is mandatory.
;Cover : Upload a copy of the single cover (under [[fair use]]—use the tag "<nowiki>{{Non-free album cover}}</nowiki>"). Ideally the image should be 200px wide. Enter the file name only into the field: <code><nowiki><Image Name>.jpg</nowiki></code>.
;Border : Type "yes" in this field to place a 1px border around the single's cover.
;Alt : Alt text for the cover image, to describe it to visually impaired readers. See [[WP:ALT]].
;Artist : This field should contain the name of the artist. This field is mandatory.
;Album : If the single was also released on an album, enter the name of the album. If the album has a Wikipedia article, link the article name as usual. The template will italicize the album name. For guidelines on whether a single should be described as being from a particular album, see [[template:infobox album#Template:Singles]].
;A-side : Use this field to display the [[A-side and B-side|A-side]] song title appearing on the opposite side of the single. If it has a Wikipedia article use a link.
;B-side : Use this field to display the [[A-side and B-side|B-side]] song title appearing on the opposite side of the single. If it has a Wikipedia article use a link. Only one of the fields "A-side" or "B-side" should be used.
;Released : This field should refer to the <em>earliest</em> known commercial release date, using a ''single'' occurrence of {{tl|Start date}} as <nowiki>{{Start date|YYYY|MM|DD}}</nowiki>, for example "<nowiki>{{Start date|2007|7|31}}</nowiki>". (Use "<nowiki>{{Start date|YYYY|MM}}</nowiki>" if only year and month are known, or "<nowiki>{{Start date|YYYY}}</nowiki>" for just a year).
;Recorded : This field should include details on when and where the song was recorded. Separate multiple values using {{tl|Plainlist}}.
;Format: the way that the single was released; i.e. [[7-inch single]], [[12-inch single]], [[CD single]], [[cassette single]], [[music download|digital download]] (note the piped download link). Separate multiple values using {{tl|Flatlist}}.
;Genre: the music genre(s) that the single is from. These must not be [[WP:original research|original research]], and should be stated and referenced in a 'Composition' section of the article. Separate multiple values using {{tl|Flatlist}}.
;Length : The length should use {{Tl|Duration}} thus: <code><nowiki>{{Duration|m=3|s=45}}</nowiki></code>, renders as {{Duration|m=3|s=45}}, for 3 minutes 45 seconds
;Label : This field should refer to the label on which the single/album was originally released. Where significantly different versions have been released (featuring alternate track listings) e.g. U.S. vs. UK, the later release date and/or record label should be mentioned in the article. Drop words like "Records" from the end of the label's name (e.g. use <nowiki>[[Universal Records|Universal]]</nowiki> rather than <nowiki>[[Universal Records]]</nowiki>). Separate multiple values using {{tl|Flatlist}}.
;Producer : This field allows display of the song's producer(s). Separate multiple values using {{tl|Flatlist}}.
;Writer : This field allows display of the song's writer(s). Separate multiple values using {{tl|Flatlist}}.
;Misc : This field allows you to provide more infobox data such as additional chronologies using {{tl|Extra chronology}}, a track listing using {{tl|Extra track listing}}, extra covers using {{tl|Extra album cover}}, or a sample of the music using {{tl|Audiosample}}. Set {{para|Type|single}} on those templates so the section titles use the same background color as the rest of the infobox.
<templatedata>
{
"params": {
"Name": {},
"Cover": {},
"Alt": {},
"Border": {},
"Caption": {},
"Artist": {},
"Album": {},
"from Album": {},
"A-side": {},
"B-side": {},
"Released": {},
"Format": {},
"Recorded": {},
"Genre": {},
"Length": {},
"Label": {},
"Writer": {},
"Producer": {},
"Certification": {},
"Last single": {},
"Last single title": {},
"Next single": {},
"Next single title": {},
"Chronology": {},
"Last single year": {},
"This single": {},
"Next single year": {},
"Misc": {},
"Clip requested?": {},
"Prev_title": {},
"Next_title": {},
"Studio": {},
"Venue": {},
"Year": {},
"Prev_year": {},
"Next_year": {},
"Composer": {},
"Lyricist": {
"label": "Lyricist"
},
"Title": {},
"Tracks": {},
"EP": {},
"Language": {},
"Version": {},
"Type": {}
},
"paramOrder": [
"Name",
"Title",
"Cover",
"Type",
"Alt",
"Border",
"Caption",
"Version",
"Artist",
"Album",
"EP",
"from Album",
"A-side",
"B-side",
"Released",
"Year",
"Format",
"Recorded",
"Studio",
"Venue",
"Genre",
"Language",
"Length",
"Label",
"Writer",
"Producer",
"Certification",
"Prev_title",
"Last single",
"Prev_year",
"Last single title",
"Next_title",
"Next single",
"Next_year",
"Next single title",
"Composer",
"Chronology",
"Lyricist",
"Last single year",
"This single",
"Next single year",
"Tracks",
"Misc",
"Clip requested?"
]
}
</templatedata>
== Chronology ==
The chronology section should link to the previous single on the left and the next single on the right. Songs should be ordered based on the first release dates of their official singles.
* For ''first'' singles the left box (the "<TT>Last single=</TT>" field) should be left blank.
* For ''latest'' singles, the right box (the "<TT>Next single=</TT>" field) should be left blank.
By default, the chronology section begins with an "'''''artist'' Chronology'''" header. If you specify the optional "<TT>Chronology=</TT>" field, the text you supply will replace the "'''''artist'''''" portion of that header. This is useful when a single is released by two or more artists and you want to supply a chronology for each using the built-in chronology for one artist and the {{tl|Extra chronology}} template for the other(s).
Wikipedia's [[Wikipedia:Manual of Style (dates and numbers)#Date formatting|date style guidelines]] recommend that years should not be linked within the chronology section.
*''Note: album cover images should NOT be used in this section as they do not meet Wikipedia fair use requirements.''
== Examples ==
Here are some examples of articles using the infoboxes:
* "[[Country House]]"
* "[[I Want to Hold Your Hand]]"
* "[[Smells Like Teen Spirit]]"
{{#ifeq:{{FULLPAGENAME}}|Wikipedia:WikiProject Songs||
== Microformat ==
{{UF-hcal}}}}
==TemplateData==
{{TemplateDataHeader}}
<templatedata>
{
"description": "An Infobox for singles",
"format": "block",
"params": {
"Name": {
"label": "Name",
"description": "The name of the single.",
"type": "string",
"default": "(PAGENAME)",
"required": false
},
"Artist": {
"label": "Artist",
"description": "This field should contain the name of the artist.",
"type": "string",
"required": true
},
"Cover": {
"label": "Cover",
"description": "An image of the single cover, a file name, e.g. 'Relax_single.jpg'. The image should be uploaded under fair—use tag {{Non-free album cover}}.",
"type": "string",
"required": false
},
"Alt": {
"label": "Alt",
"description": "Alt text for the cover image, to describe it to visually impaired readers.",
"type": "string",
"required": false
},
"Caption": {
"label": "Caption",
"description": "Caption for the cover image.",
"type": "string",
"required": false
},
"Border": {
"label": "Border",
"description": "Set to yes to put a 1 pixel border around the cover image.",
"type": "string",
"required": false
},
"Album": {
"label": "Album",
"description": "If the single was also released on an album, enter the name of the album. If the album has a Wikipedia article, link the article name as usual. The template will italicize the album name. For guidelines on whether a single should be described as being from a particular album, see [[Template:infobox album#Template:Singles]].",
"type": "string",
"aliases": ["from Album"],
"required": false
},
"A-side": {
"label": "A-side",
"description": "The A-side song title if different from the name. If it has a Wikipedia article use a link. Only one of the fields 'A-side' or 'B-side' should be used.",
"type": "string",
"required": false
},
"B-side": {
"label": "B-side",
"description": "Use this field to display the B-side song title appearing on the opposite side of the single. If it has a Wikipedia article use a link. Only one of the fields 'A-side' or 'B-side' should be used.",
"type": "string",
"required": false
},
"Released": {
"label": "Released",
"description": "The earliest known date of commercial release, using a single occurrence of {{Start date}} as {{Start date|YYYY|MM|DD}}, for example {{Start date|2007|7|31}}. (Use {{Start date|YYYY|MM}} if only year and month are known, or {{Start date|YYYY}} for just a year).",
"type": "string",
"required": false
},
"Recorded": {
"label": "Recorded",
"description": "This field should include details on when and where the song was recorded. Separate multiple values using {{Plainlist}}.",
"type": "string",
"required": false
},
"Format": {
"label": "Format",
"description": "The way that the single was released; i.e. 7-inch single, 12-inch single, CD single, cassette single, digital download (note the piped download link). Separate multiple values using {{Flatlist}}.",
"type": "string",
"required": false
},
"Genre": {
"label": "Genre",
"description": "The music genres that the single is from. These must not be original research, and should be stated and referenced in a 'Composition' section of the article. Separate multiple values using {{Flatlist}}.",
"type": "string",
"required": false
},
"Length": {
"label": "Length",
"description": "The length should use {{Duration}} thus: {{Duration|m=3|s=45}}, renders as 3:45, for 3 minutes 45 seconds",
"type": "string",
"required": false
},
"Label": {
"label": "Label",
"description": "This field should refer to the label on which the single/album was originally released. Where significantly different versions have been released (featuring alternate track listings) e.g. U.S. vs. UK, the later release date and/or record label should be mentioned in the article. Drop words like 'Records' from the end of the label's name (e.g. use [[Universal Records|Universal]] rather than [[Universal Records]]). Separate multiple values using {{Flatlist}}.",
"type": "string",
"required": false
},
"Writer": {
"label": "Writer",
"description": "The song's writer(s). Separate multiple values using {{Flatlist}}.",
"type": "string",
"required": false
},
"Producer": {
"label": "Producer",
"description": "The song's producer(s). Separate multiple values using {{Flatlist}}.",
"type": "string",
"required": false
},
"Certification": {
"label": "Certification",
"description": "Music recording sales certification. Example :Platinum <small>([[Recording Industry Association of America|RIAA]])</small>",
"type": "string",
"required": false
},
"Chronology": {
"label": "Chronology",
"description": "Header for chronology section",
"default": "(Artist name)",
"type": "string",
"required": false
},
"Last single": {
"label": "Previous single",
"description": "Previous single in the artists chronology. Use wiki-link when appropriate. Example: \"[[Relax (song)|Relax]]\"<br />(1983)",
"type": "string",
"required": false
},
"Next single": {
"label": "Next single",
"description": "Next single in the artists chronology. Use wiki-link when appropriate. Example: \"[[The Power of Love (Frankie Goes to Hollywood song)|The Power Of Love]]\"<br />(1984)",
"type": "string",
"required": false
},
"This single": {
"label": "This single",
"description": "Name and year of this single for the chronology. Example: \"Two Tribes\"<br />(1984)",
"type": "string",
"required": false
},
"Misc": {
"label": "Misc",
"description": "This field allows you to provide more infobox data such as additional chronologies using {{Extra chronology}}, a track listing using {{Extra track listing}}, extra covers using {{tl|Extra album cover}}, or a sample of the music using {{tl|Audiosample}}. Set 'Type=single' on those templates so the section titles use the same background color as the rest of the infobox.",
"type": "string",
"required": false
},
"Clip requested?": {
"label": "Clip requested?",
"description": "Set to 'yes' to add article to [[Category:Song articles missing an audio sample]]",
"type": "string",
"required": false
}
}
}
</templatedata>
== See also ==
*[[Wikipedia:WikiProject Songs]]
*{{tl|Infobox song}}
*{{tl|Infobox standard}}<includeonly>
{{pp-template}}
{{#ifeq:{{FULLPAGENAME}}|Template:Infobox single|
<!-- categories and interwiki links -->
[[Category:Music infobox templates|Single]]
[[Category:WikiProject Songs templates]]
}}</includeonly>
q7m7iuhmd7v33aorhrz8l2tlksg7hez
749777
749776
2026-07-03T20:17:35Z
~2026-38011-52
74711
749777
wikitext
text/x-wiki
{{Documentation subpage}}
{{Music infobox header}}
==Usage==
{{#ifeq:{{FULLPAGENAME}}|Wikipedia:WikiProject Songs||{{for|non-single songs|Template:Infobox song}}}}
This is the template for a [[wiktionary:single|single]] (A- or B-side), '''not''' a [[wiktionary:song|song]]. {{See also|Template:Infobox song}}
==Code==
{{Infobox single <!-- See Wikipedia:WikiProject_Songs -->
| Name =
| Cover = <!--just the file name-->
| Alt =
| Type =
| Artist =
| Album =
| B-side =
| Released = <!--{{Start date|YYYY|MM|DD}}-->
| Recorded =
| Studio =
| Genre = <!--do not add unsourced genres-->
| Length =
| Label =
| Writer = <!--for both music and lyrics-->
| Producer =
| Prev_title = <!--for a singles chronology-->
| Prev_year =
| Next_title =
| Next_year =
}}
<pre style="overflow:auto;">
{{Infobox single <!-- See Wikipedia:WikiProject_Songs -->
| Name =
| Version = <!--when a song is a remix/etc and the infobox needs that disambiguation to be noted-->
| Cover = <!-- just the file name -->
| cover_size =
| Alt =
| Border =
| Caption =
| Type =
| Artist =
| Album =
| EP =
| Mixtape =
| Language = <!--only if not in English-->
| English_title =
| A-side =
| B-side =
| Written =
| Published =
| Released = <!-- {{Start date|YYYY|MM|DD}} -->
| Format =
| Recorded =
| Studio =
| Venue =
| Genre = <!--do not add unsourced genres-->
| Length = <!--For 2nd & subsequent lengths, use {{Duration|m=MM|s=SS}} -->
| Label =
| Writer =
| Composer =
| Lyricist =
| Producer =
| Certification =
| Chronology =
| Prev_title = <!--for a singles chronology-->
| Prev_year =
| Prev_version = <!--same as the version param, for the previous song-->
| Last single =
| This single =
| Next_title =
| Next_year =
| Next_version = <!--same as the version param, for the next song-->
| Next single =
| Tracks = <!--see "Track listing examples" section for album track listing options-->
| Misc = <!--for use of {{Extra chronology}}, {{Extra track listing}}, {{Extra album cover}}, {{Audio sample}}, {{External music video}}-->
}}
</pre>
== Parameters ==
;Name : This field should contain the name of the single. This field is mandatory.
;Cover : Upload a copy of the single cover (under [[fair use]]—use the tag "<nowiki>{{Non-free album cover}}</nowiki>"). Ideally the image should be 200px wide. Enter the file name only into the field: <code><nowiki><Image Name>.jpg</nowiki></code>.
;Border : Type "yes" in this field to place a 1px border around the single's cover.
;Alt : Alt text for the cover image, to describe it to visually impaired readers. See [[WP:ALT]].
;Artist : This field should contain the name of the artist. This field is mandatory.
;Album : If the single was also released on an album, enter the name of the album. If the album has a Wikipedia article, link the article name as usual. The template will italicize the album name. For guidelines on whether a single should be described as being from a particular album, see [[template:infobox album#Template:Singles]].
;A-side : Use this field to display the [[A-side and B-side|A-side]] song title appearing on the opposite side of the single. If it has a Wikipedia article use a link.
;B-side : Use this field to display the [[A-side and B-side|B-side]] song title appearing on the opposite side of the single. If it has a Wikipedia article use a link. Only one of the fields "A-side" or "B-side" should be used.
;Released : This field should refer to the <em>earliest</em> known commercial release date, using a ''single'' occurrence of {{tl|Start date}} as <nowiki>{{Start date|YYYY|MM|DD}}</nowiki>, for example "<nowiki>{{Start date|2007|7|31}}</nowiki>". (Use "<nowiki>{{Start date|YYYY|MM}}</nowiki>" if only year and month are known, or "<nowiki>{{Start date|YYYY}}</nowiki>" for just a year).
;Recorded : This field should include details on when and where the song was recorded. Separate multiple values using {{tl|Plainlist}}.
;Format: the way that the single was released; i.e. [[7-inch single]], [[12-inch single]], [[CD single]], [[cassette single]], [[music download|digital download]] (note the piped download link). Separate multiple values using {{tl|Flatlist}}.
;Genre: the music genre(s) that the single is from. These must not be [[WP:original research|original research]], and should be stated and referenced in a 'Composition' section of the article. Separate multiple values using {{tl|Flatlist}}.
;Length : The length should use {{Tl|Duration}} thus: <code><nowiki>{{Duration|m=3|s=45}}</nowiki></code>, renders as {{Duration|m=3|s=45}}, for 3 minutes 45 seconds
;Label : This field should refer to the label on which the single/album was originally released. Where significantly different versions have been released (featuring alternate track listings) e.g. U.S. vs. UK, the later release date and/or record label should be mentioned in the article. Drop words like "Records" from the end of the label's name (e.g. use <nowiki>[[Universal Records|Universal]]</nowiki> rather than <nowiki>[[Universal Records]]</nowiki>). Separate multiple values using {{tl|Flatlist}}.
;Producer : This field allows display of the song's producer(s). Separate multiple values using {{tl|Flatlist}}.
;Writer : This field allows display of the song's writer(s). Separate multiple values using {{tl|Flatlist}}.
;Misc : This field allows you to provide more infobox data such as additional chronologies using {{tl|Extra chronology}}, a track listing using {{tl|Extra track listing}}, extra covers using {{tl|Extra album cover}}, or a sample of the music using {{tl|Audiosample}}. Set {{para|Type|single}} on those templates so the section titles use the same background color as the rest of the infobox.
<templatedata>
{
"params": {
"Name": {},
"Cover": {},
"Alt": {},
"Border": {},
"Caption": {},
"Artist": {},
"Album": {},
"from Album": {},
"A-side": {},
"B-side": {},
"Released": {},
"Format": {},
"Recorded": {},
"Genre": {},
"Length": {},
"Label": {},
"Writer": {},
"Producer": {},
"Certification": {},
"Last single": {},
"Last single title": {},
"Next single": {},
"Next single title": {},
"Chronology": {},
"Last single year": {},
"This single": {},
"Next single year": {},
"Misc": {},
"Clip requested?": {},
"Prev_title": {},
"Next_title": {},
"Studio": {},
"Venue": {},
"Year": {},
"Prev_year": {},
"Next_year": {},
"Composer": {},
"Lyricist": {
"label": "Lyricist"
},
"Title": {},
"Tracks": {},
"EP": {},
"Language": {},
"Version": {},
"Type": {}
},
"paramOrder": [
"Name",
"Title",
"Cover",
"Type",
"Alt",
"Border",
"Caption",
"Version",
"Artist",
"Album",
"EP",
"from Album",
"A-side",
"B-side",
"Released",
"Year",
"Format",
"Recorded",
"Studio",
"Venue",
"Genre",
"Language",
"Length",
"Label",
"Writer",
"Producer",
"Certification",
"Prev_title",
"Last single",
"Prev_year",
"Last single title",
"Next_title",
"Next single",
"Next_year",
"Next single title",
"Composer",
"Chronology",
"Lyricist",
"Last single year",
"This single",
"Next single year",
"Tracks",
"Misc",
"Clip requested?"
]
}
</templatedata>
== Chronology ==
The chronology section should link to the previous single on the left and the next single on the right. Songs should be ordered based on the first release dates of their official singles.
* For ''first'' singles the left box (the "<TT>Last single=</TT>" field) should be left blank.
* For ''latest'' singles, the right box (the "<TT>Next single=</TT>" field) should be left blank.
By default, the chronology section begins with an "'''''artist'' Chronology'''" header. If you specify the optional "<TT>Chronology=</TT>" field, the text you supply will replace the "'''''artist'''''" portion of that header. This is useful when a single is released by two or more artists and you want to supply a chronology for each using the built-in chronology for one artist and the {{tl|Extra chronology}} template for the other(s).
Wikipedia's [[Wikipedia:Manual of Style (dates and numbers)#Date formatting|date style guidelines]] recommend that years should not be linked within the chronology section.
*''Note: album cover images should NOT be used in this section as they do not meet Wikipedia fair use requirements.''
== Examples ==
Here are some examples of articles using the infoboxes:
* "[[Country House]]"
* "[[I Want to Hold Your Hand]]"
* "[[Smells Like Teen Spirit]]"
{{#ifeq:{{FULLPAGENAME}}|Wikipedia:WikiProject Songs||
== Microformat ==
{{UF-hcal}}}}
==TemplateData==
{{TemplateDataHeader}}
<templatedata>
{
"description": "An Infobox for singles",
"format": "block",
"params": {
"Name": {
"label": "Name",
"description": "The name of the single.",
"type": "string",
"default": "(PAGENAME)",
"required": false
},
"Artist": {
"label": "Artist",
"description": "This field should contain the name of the artist.",
"type": "string",
"required": true
},
"Cover": {
"label": "Cover",
"description": "An image of the single cover, a file name, e.g. 'Relax_single.jpg'. The image should be uploaded under fair—use tag {{Non-free album cover}}.",
"type": "string",
"required": false
},
"Alt": {
"label": "Alt",
"description": "Alt text for the cover image, to describe it to visually impaired readers.",
"type": "string",
"required": false
},
"Caption": {
"label": "Caption",
"description": "Caption for the cover image.",
"type": "string",
"required": false
},
"Border": {
"label": "Border",
"description": "Set to yes to put a 1 pixel border around the cover image.",
"type": "string",
"required": false
},
"Album": {
"label": "Album",
"description": "If the single was also released on an album, enter the name of the album. If the album has a Wikipedia article, link the article name as usual. The template will italicize the album name. For guidelines on whether a single should be described as being from a particular album, see [[Template:infobox album#Template:Singles]].",
"type": "string",
"aliases": ["from Album"],
"required": false
},
"A-side": {
"label": "A-side",
"description": "The A-side song title if different from the name. If it has a Wikipedia article use a link. Only one of the fields 'A-side' or 'B-side' should be used.",
"type": "string",
"required": false
},
"B-side": {
"label": "B-side",
"description": "Use this field to display the B-side song title appearing on the opposite side of the single. If it has a Wikipedia article use a link. Only one of the fields 'A-side' or 'B-side' should be used.",
"type": "string",
"required": false
},
"Released": {
"label": "Released",
"description": "The earliest known date of commercial release, using a single occurrence of {{Start date}} as {{Start date|YYYY|MM|DD}}, for example {{Start date|2007|7|31}}. (Use {{Start date|YYYY|MM}} if only year and month are known, or {{Start date|YYYY}} for just a year).",
"type": "string",
"required": false
},
"Recorded": {
"label": "Recorded",
"description": "This field should include details on when and where the song was recorded. Separate multiple values using {{Plainlist}}.",
"type": "string",
"required": false
},
"Format": {
"label": "Format",
"description": "The way that the single was released; i.e. 7-inch single, 12-inch single, CD single, cassette single, digital download (note the piped download link). Separate multiple values using {{Flatlist}}.",
"type": "string",
"required": false
},
"Genre": {
"label": "Genre",
"description": "The music genres that the single is from. These must not be original research, and should be stated and referenced in a 'Composition' section of the article. Separate multiple values using {{Flatlist}}.",
"type": "string",
"required": false
},
"Length": {
"label": "Length",
"description": "The length should use {{Duration}} thus: {{Duration|m=3|s=45}}, renders as 3:45, for 3 minutes 45 seconds",
"type": "string",
"required": false
},
"Label": {
"label": "Label",
"description": "This field should refer to the label on which the single/album was originally released. Where significantly different versions have been released (featuring alternate track listings) e.g. U.S. vs. UK, the later release date and/or record label should be mentioned in the article. Drop words like 'Records' from the end of the label's name (e.g. use [[Universal Records|Universal]] rather than [[Universal Records]]). Separate multiple values using {{Flatlist}}.",
"type": "string",
"required": false
},
"Writer": {
"label": "Writer",
"description": "The song's writer(s). Separate multiple values using {{Flatlist}}.",
"type": "string",
"required": false
},
"Producer": {
"label": "Producer",
"description": "The song's producer(s). Separate multiple values using {{Flatlist}}.",
"type": "string",
"required": false
},
"Certification": {
"label": "Certification",
"description": "Music recording sales certification. Example :Platinum <small>([[Recording Industry Association of America|RIAA]])</small>",
"type": "string",
"required": false
},
"Chronology": {
"label": "Chronology",
"description": "Header for chronology section",
"default": "(Artist name)",
"type": "string",
"required": false
},
"Last single": {
"label": "Previous single",
"description": "Previous single in the artists chronology. Use wiki-link when appropriate. Example: \"[[Relax (song)|Relax]]\"<br />(1983)",
"type": "string",
"required": false
},
"Next single": {
"label": "Next single",
"description": "Next single in the artists chronology. Use wiki-link when appropriate. Example: \"[[The Power of Love (Frankie Goes to Hollywood song)|The Power Of Love]]\"<br />(1984)",
"type": "string",
"required": false
},
"This single": {
"label": "This single",
"description": "Name and year of this single for the chronology. Example: \"Two Tribes\"<br />(1984)",
"type": "string",
"required": false
},
"Misc": {
"label": "Misc",
"description": "This field allows you to provide more infobox data such as additional chronologies using {{Extra chronology}}, a track listing using {{Extra track listing}}, extra covers using {{tl|Extra album cover}}, or a sample of the music using {{tl|Audiosample}}. Set 'Type=single' on those templates so the section titles use the same background color as the rest of the infobox.",
"type": "string",
"required": false
},
"Clip requested?": {
"label": "Clip requested?",
"description": "Set to 'yes' to add article to [[Category:Song articles missing an audio sample]]",
"type": "string",
"required": false
}
}
}
</templatedata>
== See also ==
*[[Wikipedia:WikiProject Songs]]
*{{tl|Infobox song}}
*{{tl|Infobox standard}}<includeonly>
{{pp-template}}
{{#ifeq:{{FULLPAGENAME}}|Template:Infobox single|
<!-- categories and interwiki links -->
[[Category:Music infobox templates|Single]]
[[Category:WikiProject Songs templates]]
}}</includeonly>
q8lbou49svdwenwx6yeh0rxazzcbomn
749778
749777
2026-07-03T20:18:34Z
~2026-38011-52
74711
749778
wikitext
text/x-wiki
{{Documentation subpage}}
{{Music infobox header}}
==Usage==
{{#ifeq:{{FULLPAGENAME}}|Wikipedia:WikiProject Songs||{{for|non-single songs|Template:Infobox song}}}}
This is the template for a [[wiktionary:single|single]] (A- or B-side), '''not''' a [[wiktionary:song|song]]. {{See also|Template:Infobox song}}
==Code==
{{Infobox single <!-- See Wikipedia:WikiProject_Songs -->
| Name =
| Cover = <!--just the file name-->
| Alt =
| Type =
| Artist =
| Album =
| B-side =
| Released = <!--{{Start date|YYYY|MM|DD}}-->
| Recorded =
| Studio =
| Genre = <!--do not add unsourced genres-->
| Length =
| Label =
| Writer = <!--for both music and lyrics-->
| Producer =
| Prev_title = <!--for a singles chronology-->
| Prev_year =
| Next_title =
| Next_year =
}}
<pre style="overflow:auto;">
{{Infobox single <!-- See Wikipedia:WikiProject_Songs -->
| Name =
| Version = <!--when a song is a remix/etc and the infobox needs that disambiguation to be noted-->
| Cover = <!-- just the file name -->
| cover_size =
| Alt =
| Border =
| Caption =
| Type =
| Artist =
| Album =
| EP =
| Mixtape =
| Language = <!--only if not in English-->
| English_title =
| A-side =
| B-side =
| Written =
| Published =
| Released = <!-- {{Start date|YYYY|MM|DD}} -->
| Format =
| Recorded =
| Studio =
| Venue =
| Genre = <!--do not add unsourced genres-->
| Length = <!--For 2nd & subsequent lengths, use {{Duration|m=MM|s=SS}} -->
| Label =
| Writer =
| Composer =
| Lyricist =
| Producer =
| Certification =
| Chronology =
| Prev_title = <!--for a singles chronology-->
| Prev_year =
| Prev_version = <!--same as the version param, for the previous song-->
| Last single =
| This single =
| Next_title =
| Next_year =
| Next_version = <!--same as the version param, for the next song-->
| Next single =
| Tracks = <!--see "Track listing examples" section for album track listing options-->
| Misc = <!--for use of {{Extra chronology}}, {{Extra track listing}}, {{Extra album cover}}, {{Audio sample}}, {{External music video}}-->
}}
</pre>
== Parameters ==
;Name : This field should contain the name of the single. This field is mandatory.
;Cover : Upload a copy of the single cover (under [[fair use]]—use the tag "<nowiki>{{Non-free album cover}}</nowiki>"). Ideally the image should be 200px wide. Enter the file name only into the field: <code><nowiki><Image Name>.jpg</nowiki></code>.
;Border : Type "yes" in this field to place a 1px border around the single's cover.
;Alt : Alt text for the cover image, to describe it to visually impaired readers. See [[WP:ALT]].
;Artist : This field should contain the name of the artist. This field is mandatory.
;Album : If the single was also released on an album, enter the name of the album. If the album has a Wikipedia article, link the article name as usual. The template will italicize the album name. For guidelines on whether a single should be described as being from a particular album, see [[template:infobox album#Template:Singles]].
;A-side : Use this field to display the [[A-side and B-side|A-side]] song title appearing on the opposite side of the single. If it has a Wikipedia article use a link.
;B-side : Use this field to display the [[A-side and B-side|B-side]] song title appearing on the opposite side of the single. If it has a Wikipedia article use a link. Only one of the fields "A-side" or "B-side" should be used.
;Released : This field should refer to the <em>earliest</em> known commercial release date, using a ''single'' occurrence of {{tl|Start date}} as <nowiki>{{Start date|YYYY|MM|DD}}</nowiki>, for example "<nowiki>{{Start date|2007|7|31}}</nowiki>". (Use "<nowiki>{{Start date|YYYY|MM}}</nowiki>" if only year and month are known, or "<nowiki>{{Start date|YYYY}}</nowiki>" for just a year).
;Recorded : This field should include details on when and where the song was recorded. Separate multiple values using {{tl|Plainlist}}.
;Format: the way that the single was released; i.e. [[7-inch single]], [[12-inch single]], [[CD single]], [[cassette single]], [[music download|digital download]] (note the piped download link). Separate multiple values using {{tl|Flatlist}}.
;Genre: the music genre(s) that the single is from. These must not be [[WP:original research|original research]], and should be stated and referenced in a 'Composition' section of the article. Separate multiple values using {{tl|Flatlist}}.
;Length : The length should use {{Tl|Duration}} thus: <code><nowiki>{{Duration|m=3|s=45}}</nowiki></code>, renders as {{Duration|m=3|s=45}}, for 3 minutes 45 seconds
;Label : This field should refer to the label on which the single/album was originally released. Where significantly different versions have been released (featuring alternate track listings) e.g. U.S. vs. UK, the later release date and/or record label should be mentioned in the article. Drop words like "Records" from the end of the label's name (e.g. use <nowiki>[[Universal Records|Universal]]</nowiki> rather than <nowiki>[[Universal Records]]</nowiki>). Separate multiple values using {{tl|Flatlist}}.
;Producer : This field allows display of the song's producer(s). Separate multiple values using {{tl|Flatlist}}.
;Writer : This field allows display of the song's writer(s). Separate multiple values using {{tl|Flatlist}}.
;Misc : This field allows you to provide more infobox data such as additional chronologies using {{tl|Extra chronology}}, a track listing using {{tl|Extra track listing}}, extra covers using {{tl|Extra album cover}}, or a sample of the music using {{tl|Audiosample}}. Set {{para|Type|single}} on those templates so the section titles use the same background color as the rest of the infobox.
<templatedata>
{
"params": {
"Name": {},
"Cover": {},
"Alt": {},
"Border": {},
"Caption": {},
"Artist": {},
"Album": {},
"from Album": {},
"A-side": {},
"B-side": {},
"Released": {},
"Format": {},
"Recorded": {},
"Genre": {},
"Length": {},
"Label": {},
"Writer": {},
"Producer": {},
"Certification": {},
"Last single": {},
"Last single title": {},
"Next single": {},
"Next single title": {},
"Chronology": {},
"Last single year": {},
"This single": {},
"Next single year": {},
"Misc": {},
"Clip requested?": {},
"Prev_title": {},
"Next_title": {},
"Studio": {},
"Venue": {},
"Year": {},
"Prev_year": {},
"Next_year": {},
"Composer": {},
"Lyricist": {
"label": "Lyricist"
},
"Title": {},
"Tracks": {},
"EP": {},
"Language": {},
"Version": {},
"Type": {},
"Mixtape": {}
},
"paramOrder": [
"Name",
"Title",
"Cover",
"Type",
"Alt",
"Border",
"Caption",
"Version",
"Artist",
"Album",
"EP",
"Mixtape",
"from Album",
"A-side",
"B-side",
"Released",
"Year",
"Format",
"Recorded",
"Studio",
"Venue",
"Genre",
"Language",
"Length",
"Label",
"Writer",
"Producer",
"Certification",
"Prev_title",
"Last single",
"Prev_year",
"Last single title",
"Next_title",
"Next single",
"Next_year",
"Next single title",
"Composer",
"Chronology",
"Lyricist",
"Last single year",
"This single",
"Next single year",
"Tracks",
"Misc",
"Clip requested?"
]
}
</templatedata>
== Chronology ==
The chronology section should link to the previous single on the left and the next single on the right. Songs should be ordered based on the first release dates of their official singles.
* For ''first'' singles the left box (the "<TT>Last single=</TT>" field) should be left blank.
* For ''latest'' singles, the right box (the "<TT>Next single=</TT>" field) should be left blank.
By default, the chronology section begins with an "'''''artist'' Chronology'''" header. If you specify the optional "<TT>Chronology=</TT>" field, the text you supply will replace the "'''''artist'''''" portion of that header. This is useful when a single is released by two or more artists and you want to supply a chronology for each using the built-in chronology for one artist and the {{tl|Extra chronology}} template for the other(s).
Wikipedia's [[Wikipedia:Manual of Style (dates and numbers)#Date formatting|date style guidelines]] recommend that years should not be linked within the chronology section.
*''Note: album cover images should NOT be used in this section as they do not meet Wikipedia fair use requirements.''
== Examples ==
Here are some examples of articles using the infoboxes:
* "[[Country House]]"
* "[[I Want to Hold Your Hand]]"
* "[[Smells Like Teen Spirit]]"
{{#ifeq:{{FULLPAGENAME}}|Wikipedia:WikiProject Songs||
== Microformat ==
{{UF-hcal}}}}
==TemplateData==
{{TemplateDataHeader}}
<templatedata>
{
"description": "An Infobox for singles",
"format": "block",
"params": {
"Name": {
"label": "Name",
"description": "The name of the single.",
"type": "string",
"default": "(PAGENAME)",
"required": false
},
"Artist": {
"label": "Artist",
"description": "This field should contain the name of the artist.",
"type": "string",
"required": true
},
"Cover": {
"label": "Cover",
"description": "An image of the single cover, a file name, e.g. 'Relax_single.jpg'. The image should be uploaded under fair—use tag {{Non-free album cover}}.",
"type": "string",
"required": false
},
"Alt": {
"label": "Alt",
"description": "Alt text for the cover image, to describe it to visually impaired readers.",
"type": "string",
"required": false
},
"Caption": {
"label": "Caption",
"description": "Caption for the cover image.",
"type": "string",
"required": false
},
"Border": {
"label": "Border",
"description": "Set to yes to put a 1 pixel border around the cover image.",
"type": "string",
"required": false
},
"Album": {
"label": "Album",
"description": "If the single was also released on an album, enter the name of the album. If the album has a Wikipedia article, link the article name as usual. The template will italicize the album name. For guidelines on whether a single should be described as being from a particular album, see [[Template:infobox album#Template:Singles]].",
"type": "string",
"aliases": ["from Album"],
"required": false
},
"A-side": {
"label": "A-side",
"description": "The A-side song title if different from the name. If it has a Wikipedia article use a link. Only one of the fields 'A-side' or 'B-side' should be used.",
"type": "string",
"required": false
},
"B-side": {
"label": "B-side",
"description": "Use this field to display the B-side song title appearing on the opposite side of the single. If it has a Wikipedia article use a link. Only one of the fields 'A-side' or 'B-side' should be used.",
"type": "string",
"required": false
},
"Released": {
"label": "Released",
"description": "The earliest known date of commercial release, using a single occurrence of {{Start date}} as {{Start date|YYYY|MM|DD}}, for example {{Start date|2007|7|31}}. (Use {{Start date|YYYY|MM}} if only year and month are known, or {{Start date|YYYY}} for just a year).",
"type": "string",
"required": false
},
"Recorded": {
"label": "Recorded",
"description": "This field should include details on when and where the song was recorded. Separate multiple values using {{Plainlist}}.",
"type": "string",
"required": false
},
"Format": {
"label": "Format",
"description": "The way that the single was released; i.e. 7-inch single, 12-inch single, CD single, cassette single, digital download (note the piped download link). Separate multiple values using {{Flatlist}}.",
"type": "string",
"required": false
},
"Genre": {
"label": "Genre",
"description": "The music genres that the single is from. These must not be original research, and should be stated and referenced in a 'Composition' section of the article. Separate multiple values using {{Flatlist}}.",
"type": "string",
"required": false
},
"Length": {
"label": "Length",
"description": "The length should use {{Duration}} thus: {{Duration|m=3|s=45}}, renders as 3:45, for 3 minutes 45 seconds",
"type": "string",
"required": false
},
"Label": {
"label": "Label",
"description": "This field should refer to the label on which the single/album was originally released. Where significantly different versions have been released (featuring alternate track listings) e.g. U.S. vs. UK, the later release date and/or record label should be mentioned in the article. Drop words like 'Records' from the end of the label's name (e.g. use [[Universal Records|Universal]] rather than [[Universal Records]]). Separate multiple values using {{Flatlist}}.",
"type": "string",
"required": false
},
"Writer": {
"label": "Writer",
"description": "The song's writer(s). Separate multiple values using {{Flatlist}}.",
"type": "string",
"required": false
},
"Producer": {
"label": "Producer",
"description": "The song's producer(s). Separate multiple values using {{Flatlist}}.",
"type": "string",
"required": false
},
"Certification": {
"label": "Certification",
"description": "Music recording sales certification. Example :Platinum <small>([[Recording Industry Association of America|RIAA]])</small>",
"type": "string",
"required": false
},
"Chronology": {
"label": "Chronology",
"description": "Header for chronology section",
"default": "(Artist name)",
"type": "string",
"required": false
},
"Last single": {
"label": "Previous single",
"description": "Previous single in the artists chronology. Use wiki-link when appropriate. Example: \"[[Relax (song)|Relax]]\"<br />(1983)",
"type": "string",
"required": false
},
"Next single": {
"label": "Next single",
"description": "Next single in the artists chronology. Use wiki-link when appropriate. Example: \"[[The Power of Love (Frankie Goes to Hollywood song)|The Power Of Love]]\"<br />(1984)",
"type": "string",
"required": false
},
"This single": {
"label": "This single",
"description": "Name and year of this single for the chronology. Example: \"Two Tribes\"<br />(1984)",
"type": "string",
"required": false
},
"Misc": {
"label": "Misc",
"description": "This field allows you to provide more infobox data such as additional chronologies using {{Extra chronology}}, a track listing using {{Extra track listing}}, extra covers using {{tl|Extra album cover}}, or a sample of the music using {{tl|Audiosample}}. Set 'Type=single' on those templates so the section titles use the same background color as the rest of the infobox.",
"type": "string",
"required": false
},
"Clip requested?": {
"label": "Clip requested?",
"description": "Set to 'yes' to add article to [[Category:Song articles missing an audio sample]]",
"type": "string",
"required": false
}
}
}
</templatedata>
== See also ==
*[[Wikipedia:WikiProject Songs]]
*{{tl|Infobox song}}
*{{tl|Infobox standard}}<includeonly>
{{pp-template}}
{{#ifeq:{{FULLPAGENAME}}|Template:Infobox single|
<!-- categories and interwiki links -->
[[Category:Music infobox templates|Single]]
[[Category:WikiProject Songs templates]]
}}</includeonly>
aag69c2vuu8ck5nrpnaxlhlwtipobvn
Template:Infobox song
10
77828
749775
211087
2026-07-03T19:44:02Z
~2026-38011-52
74711
749775
wikitext
text/x-wiki
<!--begin short description-->{{#ifeq:{{#invoke:Is infobox in lead|main|[Ii]nfobox [Ss]ong}}|true|{{#if:{{Has short description}} |<!--Do nothing--> |{{short description|{{#invoke:Plain text|main|{{#if:{{#invoke:string|match|{{{released|}}}|%d%d%d%d|ignore_errors = true}}|{{#invoke:string|match|{{{released|}}}|%d%d%d%d|ignore_errors = true}} {{lc:{{Infobox song/link|{{{type|}}} }}}}|{{Infobox song/link|{{{type|}}} }}}} {{#if:{{{artist|}}}|by {{{artist}}} }}|noreplace}}}}}}}}<!-- end short description
-->{{Infobox
| bodyclass = vevent
| aboveclass = summary
| abovestyle = color: #202122;background-color: {{Infobox song/color|{{{type|}}} }}; font-size: 125%;
| above = "{{if empty|{{{name|}}}|{{#if:{{{title2|}}}||{{{title|}}}}}|{{PAGENAMEBASE}}}}"{{#if:{{{version|}}}| ({{{version}}})}}
| image = {{#invoke:InfoboxImage|InfoboxImage|image={{if empty|{{{image|}}}|{{{cover|}}}}}|size={{{cover_size|}}}|upright={{{cover_upright|}}}|alt={{{alt|}}}|title={{{alt|}}}|border={{{border|}}}}}
| caption = {{{caption|}}}
| headerstyle = color: #202122;background-color: {{Infobox song/color|{{{type|}}} }};
| headerclass = description
| rowclass1 = description
| header1 = {{Infobox song/link|{{{type|}}} }} {{#if:{{{artist|}}}|by {{{artist}}}}}
| rowclass2 = description
| header2 = {{#if:{{{album|}}}|from the album '' {{If empty|{{{album|}}}}} ''|{{#if:{{{EP|}}}|from the EP '' {{{EP|}}} ''}}}}
| label3 = Language
| data3 = {{{language|}}}
| label4 = English title
| data4 = {{if empty|{{{English_title|}}}|{{{english_title|}}}}}
| label5 = <span class="nowrap">[[A-side and B-side|A-side]]</span>
| data5 = {{#if:{{{A-side|}}}|{{#if:{{#invoke:String|match|error_category=Music infoboxes with Module:String errors{{!}}C|1={{{A-side}}}|2="|nomatch=}}|{{{A-side}}}|"{{{A-side}}}"}}}}
| class5 = plainlist
| label6 = <span class="nowrap">[[A-side and B-side|B-side]]</span>
| data6 = {{#if:{{{B-side|}}}|{{#if:{{#invoke:String|match|error_category=Music infoboxes with Module:String errors{{!}}C|1={{{B-side}}}|2="|nomatch=}}|{{{B-side}}}|"{{{B-side}}}"}}}}
| class6 = plainlist
| label7 = Written
| data7 = {{{written|}}}
| label8 = Published
| data8 = {{{published|}}}
| label10 = Released
| data10 = {{{released|}}}
| class10 = plainlist
| label12 = Recorded
| data12 = {{{recorded|}}}
| class12 = plainlist
| label13 = [[Recording studio|Studio]]<!--this link is here deliberately; see the talk page-->
| data13 = {{{studio|}}}
| class13 = plainlist
| label14 = Venue
| data14 = {{{venue|}}}
| label15 = [[Music genre|Genre]]
| data15 = {{{genre|}}}
| class15 = category hlist
| label16 = Length
| data16 = {{#invoke:hms|main|duration={{{length|}}}}}
| class16 = plainlist
| label17 = [[Record label|Label]]<!--this link is here deliberately; see the talk page-->
| data17 = {{{label|}}}
| class17 = hlist
| label40 = <span class="nowrap">[[Songwriter|Songwriter{{pluralize from text|{{{writer|}}}|plural=s}}]]</span><!--this link is here deliberately; see the talk page-->
| data40 = {{{writer|}}}
| class40 = hlist
| label41 = <span class="nowrap">[[Composer|Composer{{pluralize from text|{{{composer|}}}|plural=s}}]]</span><!--this link is here deliberately; see the talk page-->
| data41 = {{{composer|}}}
| class41 = hlist
| label42 = <span class="nowrap">[[Lyricist|Lyricist{{pluralize from text|{{{lyricist|}}}|plural=s}}]]</span><!--this link is here deliberately; see the talk page-->
| data42 = {{{lyricist|}}}
| class42 = hlist
| label43 = <span class="nowrap">[[Record producer|Producer{{pluralize from text|{{{producer|}}}|plural=s}}]]</span><!--this link is here deliberately; see the talk page-->
| data43 = {{{producer|}}}
| class43 = hlist
| header44 = {{#if:{{{tracks|}}}||{{#if:{{{prev_title|}}}{{{next_title|}}}|{{#if:{{{chronology|}}}|{{#invoke:String|replace|error_category=Music infoboxes with Module:String errors{{!}}C|{{{chronology}}}{{#switch:{{lc:{{{type|}}}}}|single|[[single (music)|single]]|singles|[[single (music)|singles]]= singles}}|singles singles|singles}}|{{#ifeq: {{str letter|{{delink| {{{artist|}}} }}}}|0|{{{artist|}}}|{{#invoke:String2 | ucfirst |{{{artist|}}}}}}} singles}} chronology}}}}
| data45 = {{#if:{{{tracks|}}}||{{#if:{{{prev_title|}}}{{{next_title|}}}|
{{(!}} style="background: transparent; color: inherit; width: 100%; min-width: 100%; border-collapse: collapse; display: inline-table;"
{{!}}- style="line-height: 1.4em;"
{{!}} style="width: 33%; text-align: center; vertical-align: top; padding: .2em .1em .2em 0;" {{!}} {{#if:{{{prev_title|}}}|"{{{prev_title|}}}" {{#if:{{{prev_version|}}}|({{{prev_version}}}) }}{{#if:{{{prev_concurrent|}}}|{{sp}}and "{{{prev_concurrent|}}}"}}{{#if:{{{prev_title2|}}}| / "{{{prev_title2|}}}"}}{{#if:{{{prev_year|}}}|<br />({{{prev_year|}}})}} }}
{{!}} style="width: 33%; text-align: center; vertical-align: top; padding:.2em .1em;" {{!}} {{#if:{{{title2|}}}|{{#if:{{{concurrent_title|}}}|"{{{concurrent_title|}}}" and{{sp}}|}}"{{if empty|{{#switch:{{{title|}}}|{{BASEPAGENAME}}|{{PAGENAMEBASE}}='''{{{title}}}'''|{{{title|}}}}}|'''{{if empty|{{{name|}}}|{{PAGENAMEBASE}}}}'''}}"{{#if:{{{version|}}}| ({{{version}}})}} / "{{#switch:{{{title2}}}|{{BASEPAGENAME}}|{{PAGENAMEBASE}}='''{{{title2}}}'''|{{{title2}}}}}"|"'''{{if empty|{{{name|}}}|{{{title|}}}|{{PAGENAMEBASE}}}}'''"{{#if:{{{concurrent_title|}}}|{{sp}}and "{{{concurrent_title|}}}"|}}{{#if:{{{version|}}}| ({{{version}}})}}}} {{#if:{{{prev_year|}}}{{{next_year|}}}|<br />({{{year|{{#invoke:String|match|error_category=Music infoboxes with Module:String errors{{!}}C|{{{released|}}}|%d%d%d+}}}}})}}
{{!}} style="width: 33%; text-align: center; vertical-align: top; padding: .2em 0 .2em .1em;" {{!}} {{#if:{{{next_title|}}}|"{{{next_title|}}}" {{#if:{{{next_version|}}}|({{{next_version}}}) }}{{#if:{{{next_concurrent|}}}|{{sp}}and "{{{next_concurrent|}}}"}}{{#if:{{{next_title2|}}}| / "{{{next_title2|}}}"}}{{#if:{{{next_year|}}}|<br />({{{next_year|}}})}} }}
{{!)}}
}}}}
| header62 = {{#if:{{{tracks|}}}| {{#if: {{{chronology|}}}|{{{chronology}}}|{{#if:{{{album|}}}{{{EP|}}}|''{{{album|}}}{{{EP|}}}'' {{nowrap|track listing}}|Track listing}} }} }}
| rowcellstyle63 = {{#if:{{{tracks|}}}|text-align:left}}
| data63 = {{{tracks|}}}
| below = {{{misc|}}}
}}{{#invoke:Check for unknown parameters|check|unknown={{main other|[[Category:Pages using infobox song with unknown parameters|_VALUE_{{PAGENAME}}]]}}|preview=Page using [[Template:Infobox song]] with unknown parameter "_VALUE_"|ignoreblank=y | type | EP | writer | title2 | tracks | recorded | length | prev_track | next_track | chronology | name | title | image | cover | cover_size | cover_upright | alt | border | caption | artist | album | language | English_title | english_title | A-side | B-side | written | published | released | studio | venue | genre | label | composer | lyricist | producer | prev_title | next_title | prev_title2 | prev_year | next_year | year | next_title2 | misc | version | prev_version | next_version | prev_concurrent | concurrent_title | next_concurrent }}{{main other|{{#if:{{{length|}}}|{{#if:{{#invoke:String|match|error_category=Music infoboxes with Module:String errors{{!}}C|1={{#invoke:hms|main|duration={{{length|}}}}}|2=class="duration"|plain=true|nomatch=}}|[[Category:Articles with hAudio microformats]]}}{{#if:{{{writer|}}}{{{composer|}}}{{{lyricist|}}}||[[Category:Song articles with missing songwriters]]}}{{#if:{{both|{{{writer|}}}|{{{composer|}}}}}|[[Category:Song articles with both composer and songwriter]]}}{{#if:{{both|{{{writer|}}}|{{{lyricist|}}}}}|[[Category:Song articles with both lyricist and songwriter]]}}{{#if:{{{lyricist|}}}|{{#if:{{{composer|}}}||[[Category:Song articles with lyricist but not composer]]}}}}}}{{#if:{{#invoke:String|match|error_category=Music infoboxes with Module:String errors{{!}}C|1={{{name|}}}{{{cover|}}}{{{cover_size|}}}{{{alt|}}}{{{border|}}}{{{caption|}}}{{{type|}}}{{{artist|}}}{{{album|}}}{{{EP|}}}{{{language|}}}{{{English_title|}}}{{{writer|}}}{{{composer|}}}{{{lyricist|}}}{{{written|}}}{{{published|}}}{{{genre|}}}{{{recorded|}}}{{{studio|}}}{{{venue|}}}{{{length|}}}{{{producer|}}}{{{released|}}}{{{label|}}}{{{A-side|}}}{{{B-side|}}}{{{chronology|}}}{{{prev_title|}}}{{{prev_title2|}}}{{{prev_year|}}}{{{title|}}}{{{title2|}}}{{{year|}}}{{{next_title|}}}{{{next_title2|}}}{{{next_year|}}}{{{tracks|}}}{{{image|}}}{{{english_title|}}}x|2=</?t[drh][ >]|nomatch=}}|[[Category:Music infoboxes with malformed table placement|S]]}}}}{{#invoke:Check for conflicting parameters|check
| template = [[Template:Infobox song]]
| cat = {{main other|Category:Pages using infobox song with conflicting parameters}}
| image; cover
| English_title; english_title
}}<noinclude>
<!-- please place category links on the /doc page; interwiki links to Wikidata, thank you! -->
{{documentation}}
</noinclude>
65d3kjj4wq0wf5d5y2inf7tx8lmzrmt
Template:Infobox song/doc
10
77829
749781
211088
2026-07-03T20:24:15Z
~2026-38011-52
74711
749781
wikitext
text/x-wiki
{{documentation subpage}}
{{High-use}}
{{Music infobox header}}
{{Auto short description|conditions=if the infobox is in the lead section and if there is only one instance of Infobox song in the article{{efn|See the documentation for [[Module:Is infobox in lead]] for details.}}}}
{{Lua|Module:Infobox|Module:InfoboxImage|Module:String|Module:Check for unknown parameters}}
{{Infobox song
| name = The Single
| cover = Nocover.svg
| alt =
| caption = Single picture sleeve
| type = single
| artist = the Artist
| album =
| B-side = Other Side
| released = {{Start date|{{#expr:{{#time:Y}}-1}}|04|01}}
| recorded = January 1–10, {{#expr:{{#time:Y}}-1}}
| studio = [[Record Plant]], New York City
| venue =
| genre = [[Contemporary R&B]]
| length = 3:01
| label = [[Columbia Records|Columbia]]
| writer = The Artist
| producer = The Producer
| prev_title = Last Single
| prev_year = {{#expr:{{#time:Y}}-2}}
| next_title = Next Single
| next_year = {{#time:Y}}
}}
{{Infobox song
| name = The Song
| cover = Nocover.svg
| alt =
| caption = Sheet music cover
| type = song
| artist = the Artist
| album = That Album
| released = {{Start date|{{#expr:{{#time:Y}}-1}}|04|01}}
| recorded = January 1–10, {{#expr:{{#time:Y}}-1}}
| studio =
| venue = [[Madison Square Garden]]
| genre = [[Contemporary R&B]]
| length = 3:01
| label = [[Columbia Records|Columbia]]
| composer = Another Artist
| lyricist = The Artist
| producer = The Producer
}}
Articles about [[song]]s often include an [[infobox]] that provides an overview or summary of the song. Generally, Infobox song is oriented more towards recorded music that has been commercially released. Other infoboxes, such as {{tl|Infobox musical composition}} and {{tl|Infobox anthem}} may be better suited to traditional or historical songs. The examples to the right are the infobox filled in with the most commonly used parameters.
An infobox provides [[WP:INFOBOXPURPOSE|key facts]] as discussed in the article—it should not be too detailed or take up too much of the article space. It should only contain items, such as genres or songwriters, that are discussed in the article (or have [[WP:CITE|citation]]s to [[WP:RS|reliable source]]s).
The code below can be copied and completed with information pertaining to particular song articles. The Parameters section provides guidance on using the fields.
__TOC__
== Code ==
{{Parameter names example |name | cover | cover_size | cover_upright | alt | border | caption | type | artist | album | EP | language | English_title | A-side | B-side | written | published | released | recorded | studio | venue | genre | length | label | writer | composer | lyricist | producer | chronology | prev_title | prev_title2 | prev_year | title | title2 | year | next_title | next_title2 | next_year | tracks | misc }}
The first template code below includes the most commonly used infobox parameters. If there is no information in the article to support the field, leave it blank. Remove the <code><nowiki><!--</nowiki></code> and <code><nowiki>--></nowiki></code> to use the {{tl|Start date}} and {{tl|Duration}} subtemplates.
The second includes the full set of parameters; most articles will not need many of the fields and they may be left blank or removed. Please note that adding fields other than those listed, such as {{para|arranger}} or {{para|recording engineer}}, will not be displayed.
===Most commonly used parameters===
<syntaxhighlight lang="wikitext" style="overflow:auto;">
{{Infobox song
| name =
| cover = <!--just the file name-->
| alt =
| type =
| artist =
| album =
| B-side =
| released = <!--{{Start date|YYYY|MM|DD}}-->
| recorded =
| studio =
| genre = <!--do not add unsourced genres-->
| length =
| label =
| writer = <!--for both music and lyrics-->
| producer =
| prev_title = <!--for a singles chronology-->
| prev_year =
| next_title =
| next_year =
}}
</syntaxhighlight>
===Full set of parameters===
<syntaxhighlight lang="wikitext" style="overflow:auto;">
{{Infobox song
| name =
| version = <!--when a song is a remix/etc and the infobox needs that disambiguation to be noted-->
| cover = <!--just the file name-->
| cover_size =
| alt =
| border =
| caption =
| type =
| artist =
| album =
| EP =
| language = <!--only if not in English-->
| English_title =
| A-side =
| B-side =
| written =
| published =
| released = <!--{{Start date|YYYY|MM|DD}}-->
| recorded =
| studio =
| venue =
| genre = <!--do not add unsourced genres-->
| length = <!--For 2nd & subsequent lengths, use {{Duration|00:00}}-->
| label =
| writer = <!--for both music and lyrics-->
| composer =
| lyricist =
| producer =
| chronology =
| prev_title = <!--for a singles chronology-->
| prev_year =
| prev_version = <!--same as the version param, for the previous song-->
| next_title =
| next_year =
| next_version = <!--same as the version param, for the next song-->
| tracks = <!--see "Track listing examples" section for album track listing options-->
| misc = <!--for use of {{Extra chronology}}, {{Extra track listing}}, {{Extra album cover}}, {{Audio sample}}, {{External music video}}-->
}}
</syntaxhighlight>
== Parameters ==
===name===
The title of the song. If not provided, the template uses the {{tl|PAGENAMEBASE}} of the page. Since it is not linked, [[WP:DAB|disambiguation]] terms, such as {{nowrap|"(song)"}}, {{nowrap|"(Beyoncé song)"}}, etc., are not needed.
===version===
The version of the song. This is used for infoboxes on remixes, such as at {{slink|Save Your Tears#Ariana Grande remix}}.
===cover===
Add an image of a sheet music cover, picture sleeve, or other image appropriate for the song. First, one must be uploaded (see [[Wikipedia:Uploading images]] for details). Then, enter the file name (without File:) into the field, such as <code>[Image Name].jpg</code>.
===alt===
Use this field to provide alternative text or a description of the cover image for visually impaired readers. See [[WP:ALT]].
===caption===
If there is an image, a suitable caption may be added that follows [[MOS:CAPTION]] (begins with a capital letter, no period at end if it is a sentence fragment, etc.) Please note that if the image is ''unambiguously'' a picture sleeve or record label for the release that is described in the infobox, then it is "self-captioning", and no caption needs to be entered in this parameter.
===type===
Specify "single", "instrumental", "composition", "promo", etc., as shown in the code table below. The entry will be automatically linked to the corresponding article, so only the unlinked code is needed (the default is "song"). This field is used to generate the header and color; please note that it is not used for genre, which is described in the {{para|genre}} fields.
A song that is called a "single" by sources, even if the song was not released as part of a single, may be classified as a "single".
If an album track was later released as a single, '''use the most notable or best known'''. For example, "[[Stairway to Heaven]]" was released as a promo single in several markets and as a digital single in 2007, but became best known as a song from Led Zeppelin's fourth album.
{{Infobox song/doc/type}}
===artist===
If the article associates the original or primary rendition of the song with one artist, enter the name of the artist and link if they have a WP article. For band names that begin with "The", enter the lower case "the" ({{para|artist|the Beatles}}, rather than {{para|artist|The Beatles}}), consistent with [[:MOS:THEMUSIC]]. For traditional or historical songs, when the focus is not on a particular rendition, this should be left blank (alternatively, consider using [[Template:Infobox musical composition]]).
===album===
If the song was first released on an album, enter the name of the album and link. The template will italicize the album name and produce "from the album <var>Name</var>" in the header. If the song was originally released as a single '''well in advance''' of the album, {{para|album}} should not be used, since it is not ''from the album'', but later added to one. For example, "[[Beck's Bolero]]" and "[[Back on the Chain Gang]]" were released as singles about 16 months before they were added to albums and became well-known before the albums.
===EP===
If the song was first released on an EP or [[Extended play]], add the EP name in this field. The name will be italicized, etc., and should be used as described in the album subsection above. Please note: only album ''or'' EP should be used and depends on which one was issued first.
===language===
If the song is '''not''' in English, indicate the language (common languages should not be linked).
===English_title===
An English translation of the title may be included in this field.
===A-side===
If the article is about a song that was released as a [[A-side and B-side|B-side]] of a single, enter the name of the [[A-side and B-side|A-side]] and link as appropriate (do not use B-side). Quote marks will be added automatically.{{r|quotes}} For multiple entries, see Notes{{r|plainlist}} for details. For [[double A-side]]d singles, refer to the [[#Chronology for double-A-side singles|Chronology for double-A-side singles]] section below.
===B-side===
If the article is about the song that was issued as the A-side of a single, enter the name of the B-side in this field (do not add A-side). Add a link if it has an article; quote marks will be automatically added.{{r|quotes}} For multiple entries, see Notes{{r|plainlist}} for details. Generally, later releases or in secondary markets, reissues, etc., should only be included in the body of the article.
===written===
The year the song was written or composed may be added, if discussed in the article. This is usually used for historical songs, written before the advent of sheet music publishing or music recording.
===published===
The year the song was [[Music publisher (popular music)|published]] also may be included, if discussed in the article. This usually refers to [[Music publisher (sheet music)|sheet music published]] before the rise of music recording.
===released===
Indicate the commercial release date, whether it is a single, album, or other. Usually, this is the date that it became widely available to the public, such as in retail. Dates for leaked copies, advance or limited promo releases to radio stations, etc., generally are not used, but may be included in an appropriate section of the main body if noteworthy. For example, album tracks that receive airplay should use the album release date, rather than when they were added to radio. If the same song is later released as an actual single and that becomes the best known (and {{para|type|single}} is used), indicate the date the single was released. Enter the date using {{tl|Start date}}. For example, use <code><nowiki>{{Start date|YYYY|MM|DD}}</nowiki></code> for the complete date, <code><nowiki>{{Start date|YYYY|MM}}</nowiki></code> if only year and month are known, or <code><nowiki>{{Start date|YYYY}}</nowiki></code> for just a year. For multiple entries, see Notes{{r|plainlist}} for details. Generally, later releases or in secondary markets, reissues, on compilations, etc., should only be included in the body of the article.
===<del>format</del>===
The format parameter has been removed as a result of [[Template talk:Infobox song/Archive 8#Format section|a 2020 discussion]]. Any information entered in {{para|format}} will not be displayed.
===recorded===
Enter the date(s) the song was recorded. When there are many dates, it is best to summarize, using the month or range of months (e.g., January–March {{#expr:{{#time:Y}}-2}}). For multiple entries, see Notes{{r|plainlist}} for details. Add the location of the recording in the {{para|studio}} or {{para|venue}} field.
===studio===
If the song was recorded in a '''recording studio''', enter the name and location. Link the studio, but not well-known cities, states, or countries (see [[WP:OVERLINK]]). Remove "Studios" if it appears in the name – use <nowiki>[[Sound City Studios|Sound City]]</nowiki> rather than <nowiki>[[Sound City Studios]]</nowiki>. For multiple entries, see Notes{{r|plainlist}} for details. For "United States" and "United Kingdom", it is preferred that they be abbreviated "US" and "UK", but political subdivisions should never be (never "MA" for "Massachusetts", "ON" for "Ontario", etc.). See Notes{{r|location}} for details.
===venue===
For '''live recordings''', include the name of the venue (concert hall, stadium, etc.) and location. Link as appropriate. For "United States" and "United Kingdom", it is preferred that they be abbreviated "US" and "UK", but political subdivisions should never be (never "MA" for "Massachusetts", "ON" for "Ontario", etc.). See Notes{{r|location}} for details.
===genre===
The field should include the music genre(s) that best describes the song. It should come from a [[WP:RS|reliable source]] and also '''be stated and referenced''' in the body of the article; personal opinions or [[WP:original research|original research]] must not be included. Note that most genres are not proper nouns and should not be capitalized. However, the first word in a list of multiple genres should be capitalized. For multiple entries, see Notes{{r|flatlist}} for details.
===length===
Enter the length of the song in minutes and seconds using a colon to separate the two, such as <code>3:09</code>. Do not use "3 minutes and 9 seconds", 3' 9", etc. If additional lengths are entered, use {{tl|Duration}} for the second and subsequent lengths: <code><nowiki>{{Duration|3:09}}</nowiki></code> will produce "3:09". A seconds value must be included; enter "3:00" if the song is timed to three minutes only. The length should be taken from the original album or single release, instead of reissues, downloads, etc. For multiple entries, see Notes{{r|plainlist}} for details.
===label===
The name of the '''record company''' that issued the release may be included here. Generally, add later releases, those from different countries, reissues, etc., in the body of article instead of the infobox. Remove "Records" if it appears in the name – use <nowiki>[[Universal Records|Universal]]</nowiki> instead of <nowiki>[[Universal Records]]</nowiki>. For multiple entries, see Notes{{r|flatlist}} for details.
===writer===
Use this field if the '''music''' ''and'' '''lyrics''' were written by one person or, in the case of multiple writers, where a separate lyricist and composer have not been clearly identified by a [[WP:RS|reliable source]]. Do not use this field if the {{para|composer}} and {{para|lyricist}} fields are used. For multiple entries, see Notes{{r|flatlist}} for details.
===composer===
The person(s) who composed the song's '''music''', if different than the lyricist. If not differentiated in the article, use the {{para|writer}} field instead. For instrumentals, use this field only (in WP song articles, "song" (music with words) is not usually used to describe an "instrumental"). For multiple entries, see Notes{{r|flatlist}} for details.
===lyricist===
The person(s) who wrote the '''lyrics''', if different than the song's composer. If not differentiated in the article, use the {{para|writer}} field instead. For multiple entries, see Notes{{r|flatlist}} for details.
===producer===
<!-- See RfC WT:ALBUMS 15 January 2020 -->
Enter the name of the person(s) credited as the actual [[record producer]](s). Do not include those listed as executive, co-, additional, vocal, etc., producers, unless a reliable source identifies their contribution as substantially the same as the main producers. These should be included in the article body or track listings rather than be listed here. For multiple entries, see Notes{{r|flatlist}} for details.
===chronology===
This field and the following {{para|prev_title}} and {{para|next_title}} display the artist's immediate singles chronology. By leaving it blank, "The Artist singles chronology" is automatically displayed if any of the following fields are used. An '''alternative header''' may be displayed before the automatically produced "chronology", for example, <code>The Artist American</code>, <code>The Artist UK singles</code>, etc., produces "The Artist American chronology", "The Artist UK singles chronology", etc. Please note: if this field is used and a track listing is also wanted, it must be added using {{tl|Extra track listing}}.
If {{para|chronology}} is blank, the first (English) letter of the artist name given in {{para|artist}} is automatically capitalized in the chronology header, so that band names starting with "The" appear as recommended in the [[MOS:THEMUSIC|Manual of Style]]. If the first letter of the artist's name is stylized in lower case, or if the first letter in the artist's name is not part of the English alphabet, enter the artist's name in {{para|chronology}} as you wish it to appear. For example:
* {{para|artist|[[:letlive.]]}} should use {{para|chronology|[[:letlive.]]}} for it to also appear there in lower case.
* {{para|artist|[[Édith Piaf]]}} should use {{para|chronology|[[Édith Piaf]]}} to prevent the letter "d" (the first English letter) from appearing in upper case.
===prev_title===
Enter the name of the artist's previous single. Quote marks will be added automatically;{{r|quotes}} link it if there is a WP article. Leave this blank if the artist has no previous single. For [[double A-side]]d singles, refer to the [[#Chronology for double-A-side singles|Chronology for double-A-side singles]] section below.
===prev_year===
Enter only the year the previous single was released. Following the WP [[Wikipedia:Manual of Style (dates and numbers)#Date formatting|date style guidelines]], years should not be linked within the chronology section. Leave this field blank if there is no previous single.
===prev_version===
Same as the version parameter, if the previous song in the chronology requires disambiguation.
===next_title===
Enter the name of the artist's next single, if applicable. Quote marks will be added automatically;{{r|quotes}} link it if there is a WP article.
===next_year===
Enter only the year the next single was issued, if applicable.
===next_version===
Same as the version parameter, if the next song in the chronology requires disambiguation.
===tracks===
Several track listing options are available. See the [[#Track listing examples|Track listing examples]] section below for explanations and codes.
===misc===
This is '''not a space for miscellaneous information or comments'''. It allows for the use of subtemplates, including {{tl|Extra chronology}}, {{tl|Extra track listing}}, {{tl|Extra album cover}}, {{tl|Audio sample}}, and {{tl|External music video}}. Please refer to the template pages for explanations on their use.
==Track listing examples==
Not all infoboxes should include album track listings. Articles about traditional or historical songs might not focus on a particular recording, so details about a later rendition, including album releases, are inappropriate for an infobox. Additionally, there may already be an album track listing with links to other songs in a [[WP:NAV|navigation template]] or [[WP:NAVBOX|navbox]] at the end of the article. These include an album navbox (for example, {{tl|Thriller (album)}}), an artist song navbox ({{tl|Rihanna songs}}), or an artist navbox that includes songs ({{tl|Robert Johnson}}).
An album track listing may be added to infobox song as long as the following criteria are met: 1) <strong>the article does not have a navbox in which the song appears</strong>; and 2) a majority of the album's songs are linked to WP articles. Currently, three different approaches to listing album tracks are found in infobox song.
===Track listing using a separate sub-template===
{{Infobox song
| name = The Song
| cover =
| alt =
| type =
| artist = the Artist
| album = That Album
| released = {{Start date|{{#expr:{{#time:Y}}-1}}|04|01}}
| recorded = January 1–10, {{#expr:{{#time:Y}}-1}}
| studio = [[Record Plant]], New York City
| venue =
| genre = [[Contemporary R&B]]
| length = 3:01
| label = [[Columbia Records|Columbia]]
| writer = The Artist
| producer = The Producer
| tracks = {{Astral Weeks track listing}}
}}
If a separate album track listing sub-template has been created, it may be added to the {{para|tracks}} of the infobox. Using such a template may be more convenient for multi-disc albums or those with many tracks. A header with the album name will be automatically generated, along with the number of tracks. The song title will not be linked, but will appear in <strong>bold</strong>.
The example to the right uses {{tl|Astral Weeks track listing}} to demonstrate. To see its use in actual song infobox, click on the song links. Please note, if a singles chronology is also used in the infobox ({{para|prev_title}}, etc.), {{tl|Extra track listing}} must be used in the {{para|misc}} field (refer to that [[Template:Extra track listing#Usage]] for the code and explanation).
{{clear}}
===Track listing using the hidden template===
{{Infobox song
| name = The Song
| cover =
| alt =
| type =
| artist = the Artist
| album = That Album
| released = {{Start date|{{#expr:{{#time:Y}}-1}}|04|01}}
| recorded = January 1–10, {{#expr:{{#time:Y}}-1}}
| studio = [[Record Plant]], New York City
| venue =
| genre = [[Contemporary R&B]]
| length = 3:01
| label = [[Columbia Records|Columbia]]
| writer = The Artist
| producer = The Producer
| tracks = {{Hidden
| title = 5 tracks
| text =
'''Side one'''
# "[[21st Century Schizoid Man]]"
# "[[I Talk to the Wind]]"
# "[[Epitaph (King Crimson song)|Epitaph]]"
'''Side two'''
# "[[Moonchild (King Crimson song)|Moonchild]]"
# "[[The Court of the Crimson King]]"
}}
}}
A second option does not require a separate album track listing sub-template to be created. Instead, the {{tl|Hidden}} template is placed directly into the infobox in the {{para|tracks}} field, then the tracks are listed. This may be copied and added to other songs infoboxes from the same album.
The example to the right uses this approach. To see its use in actual infoboxes, click on the song titles. Also, please note that if a singles chronology is also used in the infobox ({{para|prev_title}}, etc.), {{tl|Extra track listing}} must be used in the {{para|misc}} field (refer to that [[Template:Extra track listing#Usage]] for the code and explanation).
'''Code'''
<syntaxhighlight lang="wikitext">{{Infobox song
...
| tracks = {{Hidden
| title =
| text =
#
#
# etc.
}}
</syntaxhighlight>
;title
:Enter the number of tracks, such as "10 tracks". A header will be generated automatically, for example "''That Album'' track listing".
;text
:In this field, add the album tracks, using <code>#</code> to generate a sequential numbering. The song titles should be in "quotes", with the article track also in <strong>bold</strong>. Link any track that has its own WP article, except for when the song is the subject of the article.
:For multi-disc [[Compact Disc Digital Audio|CD]]s, subheadings for "Disc one", "Disc two", etc. may be included. Similarly, subheadings such as "Side one" or "Side A" may be added for [[LP record]]s and [[Extended play|EPs]]. To ensure that the track numbering continues sequentially after a subheading (rather than starting again at "1"), use <code><nowiki><li value="x"></nowiki></code> where "x"=the next track number.
'''Code'''
<syntaxhighlight lang="wikitext">{{Infobox song
...
| tracks = {{Hidden
| title =
| text =
'''Disc one'''
#
#
# etc.
'''Disc two'''
#<li value=>
#
#
# etc.
}}
</syntaxhighlight>
{{Clear}}
==Chronology for double-A-side singles==
{{Infobox song
| name = The Single
| cover =
| alt =
| type = single
| artist = the Artist
| album = That Album
| A-side = "Other Side" ([[double A-side]])
| released = {{Start date|{{#expr:{{#time:Y}}-1}}|04|01}}
| recorded = January 1–10, {{#expr:{{#time:Y}}-1}}
| studio = [[Record Plant]], New York City
| venue =
| genre = [[Contemporary R&B]]
| length = 3:01
| label = [[Columbia Records|Columbia]]
| writer = The Artist
| producer = The Producer
| chronology = The Artist double A-side
| prev_title = Last Single
| prev_title2 = Last Other
| prev_year = {{#expr:{{#time:Y}}-2}}
| title =
| title2 = Other Side
| next_title = Next Single
| next_title2 = Next Other
| next_year = {{#time:Y}}
}}
'''Code''':
<syntaxhighlight lang="wikitext">{{Infobox song
...
| A-side =
| chronology =
| prev_title =
| prev_title2 =
| prev_year =
| title2 =
| next_title =
| next_title2 =
| next_year =
}}
</syntaxhighlight>
;A-side
:Enter the name of the second- or flip-side of the current or article title.
;chronology
:Use this field to provide an '''alternative header''' to the automatically generated "The Artist singles chronology". Enter <code>The Artist double A-side singles</code> to display "The Artist double A-side singles chronology", etc.
;prev_title
:Enter the name of the previous single. If it has a WP article, add a link.
;prev_title2
:If the previous is a double A-side single, enter the name of the previous single second- or flip-side and link as appropriate.
;prev_year
:Enter only the year the previous single was released.
;title2
If it is a double A-side single, enter the name of the second- or flip-side of current or article title. (The current or article title with be automatically generated with quotes and in bold and will appear as the first of the two; the year is also generated from the {{para|released}} year date.)
;next_title
:Enter the name of the next single. If it has a article, add a link.
;next_title2
:Enter the name of the second-or flip side of next-single, if it is a double-A. Link as appropriate.
;next_year
:Enter only the year the next single was released.
== Notes ==
{{reflist |refs=
<ref name="quotes">If any details besides the actual song title (country, etc.) are added, quote marks need to be added manually to the song title. Additionally, for {{para|A-side}} and {{para|B-side}}, quote marks need to be added for each title if there is more than one.
</ref>
<ref name="plainlist"> For multiple entries, format the items as a normal bulleted list; don't use other list templates or <nowiki><br/></nowiki>. For example:<syntaxhighlight lang="wikitext">
| studio =
*First item
*Second item
*Third item</syntaxhighlight>
will be displayed as a vertical unbulleted list:
{|style="border:1px solid #ccc"
|style="vertical-align:top"|'''Studio'''{{spaces|3}}||{{unbulleted list|First item|Second item|Third item}}
|}
</ref>
<ref name="flatlist">For short horizontal lists of two or three items, comma separators are acceptable, but for longer lists, format the items as a normal bulleted list; don't use other list templates or <nowiki><br/></nowiki>. For example:<syntaxhighlight lang="wikitext">
| genre =
*Item one
*Item two
*Item three</syntaxhighlight>
will be displayed as a horizontal list separated by middots:
{|style="border:1px solid #ccc"
|'''Genre'''{{spaces|3}}
| {{flatlist}}
*Item one
*Item two
*Item three
{{endflatlist}}
|}
</ref>
<ref name="location">As per RfC "Naming countries in infoboxes": When identifying a location, city or region in the United States or United Kingdom, the preferred approach is to use the country abbreviations allowed by [[MOS:ACRO]], such as "Portsmouth, New Hampshire, US" and "Cardiff, Wales, UK", rather than spelling out the full country name. Avoid using abbreviations for states or provinces in all nations. {{xt|New Hampshire}} rather than {{!xt|NH}}, {{xt|New South Wales}} rather than {{!xt|NSW}}, or {{xt|British Columbia}} rather than {{!xt|BC}} (see [[MOS:POSTABBR]]). Cities such as New York City and London may be rendered as "New York City, US" and "London, UK" or simply "New York City" and "London". Also, these should not be linked when the context makes it clear (see [[MOS:OVERLINK]]). If only the country name is included, it may be written out in full.
</ref>
|}
}}
{{When on basepage|
[[Category:Music infobox templates|Song]]
[[Category:WikiProject Songs templates]]
[[Category:Templates that add a tracking category]]
}}
==Short description==
This template generates an automatic short description of the form {{tq|YYYY [type] by [artist]}} under certain conditions, when it appears once in an article in the lead, {{para|artist}} is defined, and {{Wikidata property link|P577}} is defined on Wikidata. A manually entered short description will override the automatic one. If an article can generate a satisfactory automatic description, any preexisting manual description may be deleted.
==TemplateData==
{{Collapse top|title=[[Wikipedia:TemplateData|TemplateData]] documentation used by [[Wikipedia:VisualEditor|VisualEditor]] and other tools}}
{{TemplateData header|noheader=1}}
<templatedata>
{
"params": {
"name": {
"label": "Name",
"description": "The name or title of the song.",
"type": "string",
"default": "(PAGENAME)",
"required": true
},
"cover": {
"label": "Cover",
"description": "An appropriate image, such as a sheet music or single picture sleeve cover.",
"example": "A file name, without the File: <NAME.jpg>",
"type": "wiki-file-name",
"aliases": [
"image"
]
},
"cover_size": {
"label": "Cover_size",
"type": "number",
"deprecated": "Cover_upright should be used if needed"
},
"alt": {
"label": "Alt",
"description": "Alternative text for the cover image, to describe it to visually impaired readers.",
"type": "line"
},
"border": {
"label": "Border",
"description": "Set to yes to put a 1 pixel border around the image.",
"type": "line",
"autovalue": "yes",
"suggestedvalues": [
"yes"
]
},
"caption": {
"label": "Caption",
"description": "Caption for the image; this will overwrite automatically generated <\"Song\" cover> when a file is added to the \"caption\" field",
"example": "Sheet music cover, 1920.",
"type": "string"
},
"type": {
"label": "Type",
"description": "Specify \"song\", \"single\", \"Instrumental\", \"Composition\", etc. This generates the header and color.",
"type": "line",
"suggested": true,
"suggestedvalues": [
"single",
"song",
"promo",
"demo",
"remix single",
"instrumental",
"composition",
"hymn",
"nursery"
]
},
"artist": {
"label": "Artist",
"description": "The name of the artist.",
"type": "string",
"suggested": true
},
"album": {
"label": "Album",
"description": "The name of the album on which the song originally appeared.",
"type": "string"
},
"EP": {
"label": "EP",
"description": "The name of the EP on which the song originally appeared. Only use album or EP; both will not be displayed.",
"type": "string"
},
"English_title": {
"label": "English title",
"description": "The English translation of the name of the song, if it is in another language.",
"type": "line"
},
"written": {
"label": "Written (date)",
"description": "The date the song was written; this is usually used for songs created before music publishing or recording",
"type": "date"
},
"published": {
"label": "Published (date)",
"description": "The date the song was published; this is usually used for songs created before the rise of music recording",
"type": "date"
},
"released": {
"label": "Released (date)",
"description": "For recorded music: The earliest known date of commercial release, using a single occurrence, using {{Start date}}. For April 1, 2015, enter {{Start date|2015|04|01}}.",
"type": "date"
},
"A-side": {
"label": "A-side",
"description": "For singles: the name of A-side of the current or title single.",
"type": "string"
},
"B-side": {
"label": "B-side",
"description": "For singles: the name of the B-side of the current or title single. Only one of the fields 'A-side' or 'B-side' should be used.",
"type": "string"
},
"recorded": {
"label": "Recorded (date)",
"description": "The date(s) the song was recorded.",
"type": "date"
},
"venue": {
"label": "Venue",
"description": "For live recordings, include the name of the venue (concert hall, stadium, etc.) and location.",
"type": "string"
},
"studio": {
"label": "Studio",
"description": "If the song was recorded in a recording studio, enter the name and location.",
"type": "string"
},
"genre": {
"label": "Genre",
"description": "The genre(s) that best describes the song. This must not be original research, and should be stated and referenced in the article.",
"type": "string",
"suggested": true
},
"length": {
"label": "Length",
"description": "The length of the song, using {{Duration}}. To display 3 minutes 45 seconds as 3:45, enter {{Duration|m=3|s=45}}.",
"type": "number"
},
"label": {
"label": "Label",
"description": "The record company label which originally released the recording. Releases in other territories, different formats, reissues, etc., should be included in the body of the article, not in this field.",
"type": "string"
},
"writer": {
"label": "Songwriter/Writer",
"description": "The name of the person(s) credited with writing both the music and lyrics for the song. Where a separate lyricist and composer have been clearly identified by a reliable source, use the \"composer\" and \"lyricist\" fields instead.",
"type": "string"
},
"composer": {
"label": "Composer",
"description": "The name of the person(s) who composed the song's music, if different than the lyricist. If not differentiated in the article, use the \"songwriter/writer\" field instead.",
"type": "string"
},
"lyricist": {
"label": "Lyricist",
"description": "The person(s) who wrote the lyrics, if different than the song's composer. If not differentiated in the article, use the \"songwriter/writer\" field instead.",
"type": "string"
},
"language": {
"label": "Language",
"description": "Only if the song is not in English, indicate the language.",
"type": "string"
},
"producer": {
"label": "Producer",
"description": "The person(s) who is credited with the record production.",
"type": "string"
},
"chronology": {
"label": "Chronology (artist)",
"description": "For singles: Replacement of artist's name for the chronology section.",
"type": "string"
},
"prev_title": {
"label": "Prev_title",
"description": "The name of the previous single in the artist's chronolgy.",
"type": "string"
},
"prev_title2": {
"label": "Prev_title2",
"description": "The name of the previous single in the artist's chronolgy.",
"type": "string"
},
"prev_year": {
"label": "Prev_year",
"description": "The year of the previous single in the artist's chronology.",
"type": "number"
},
"title": {
"label": "Title",
"description": "The name of the current or subject single.",
"type": "string"
},
"title2": {
"label": "Title2",
"description": "For a double A-side single: the name of the second or flip-side of the current single in the artist's chronology.",
"type": "string"
},
"year": {
"label": "Year",
"description": "To change the automatically generated year: the year of the single in the artist's chronology.",
"type": "number"
},
"next_title": {
"label": "Next_title",
"description": "The name of the next single in the artist's chronology.",
"type": "string"
},
"next_title2": {
"label": "Next_title2",
"description": "For a double A-side single: the name of the second- or flip-side of the next single.",
"type": "string"
},
"next_year": {
"label": "Next_year",
"description": "The year of the next single in the artist's chronology.",
"type": "number"
},
"tracks": {
"label": "Tracks",
"description": "To add a full-album track listing.",
"type": "string"
},
"misc": {
"label": "Misc",
"description": "To add extra subtemplates, such as {{Extra chronology}}, {{Extra track listing}}, {{Extra album cover}}, {{Audio sample}}, etc.",
"type": "string"
},
"cover_upright": {
"label": "Cover_upright",
"type": "number"
},
"version": {
"label": "Version",
"type": "line"
},
"prev_version": {
"label": "Prev_version",
"type": "line"
},
"next_version": {
"label": "Next_version",
"type": "line"
},
"prev_concurrent": {
"label": "Previous concurrent",
"description": "The name of the previous single in the artist's chronology, if it was released as a dual single.",
"type": "string"
},
"concurrent_title": {
"label": "Concurrent title",
"description": "The name of the other single in the artist's chronology, if this was released as a dual single.",
"type": "string"
},
"next_concurrent": {
"label": "Next concurrent",
"description": "The name of the next single in the artist's chronology, if it was released as a dual single.",
"type": "string"
}
},
"description": "An infobox for songs",
"paramOrder": [
"name",
"version",
"cover",
"cover_size",
"cover_upright",
"alt",
"border",
"caption",
"type",
"artist",
"album",
"EP",
"language",
"English_title",
"A-side",
"B-side",
"written",
"published",
"released",
"recorded",
"studio",
"venue",
"genre",
"length",
"label",
"writer",
"composer",
"lyricist",
"producer",
"chronology",
"prev_title",
"prev_concurrent",
"prev_version",
"prev_title2",
"prev_year",
"title",
"title2",
"concurrent_title",
"year",
"next_title",
"next_concurrent",
"next_version",
"next_title2",
"next_year",
"tracks",
"misc"
],
"format": "{{_\n| _____________ = _\n}}\n"
}
</templatedata>
{{Collapse bottom}}
== Tracking categories ==
{{infobox tracking categories}}
* {{clc|Song articles with missing songwriters}}
* {{clc|Song articles with both composer and songwriter}}
* {{clc|Song articles with both lyricist and songwriter}}
* {{clc|Song articles with lyricist but not composer}}
* {{clc|Music infoboxes with unknown value for type}}
* {{clc|Music infoboxes with malformed table placement}}
* {{clc|Music infoboxes with Module:String errors}}
* {{clc|Duration without hAudio microformat}}
== See also ==
* [[Wikipedia:WikiProject Songs]]
* {{tl|Infobox album}}
* {{tl|Single chart}}
==Notes==
{{notelist}}
<includeonly>{{#ifeq:{{SUBPAGENAME}}|sandbox||
<!-- Categories go here and interwikis go in Wikidata. -->
}}</includeonly>
ec1275agk3ttqda7nd3u8d4nqlnkepn
Template:Extra chronology/doc
10
77855
749784
211311
2026-07-03T20:28:39Z
~2026-38011-52
74711
749784
wikitext
text/x-wiki
{{Documentation subpage}}
<!-- Categories go at the bottom of this page and interwikis go in Wikidata. -->
{{High-use}}
{{Lua|Module:Check for unknown parameters}}
==Usage==
{{Main|Template:Infobox album#Template:Extra chronology}}
Extra chronologies may be added to {{tl|Infobox album}} and {{tl|Infobox song}}, depending on which version of this template is used. Both add the proper header, color, and formatting (depending on the {{para|type}} designation) and are added to the infoboxes in the {{para|misc}} field.
If this template is substituted, it will return a cleaned-up copy of itself (see [[Wikipedia:Substitution trick]]) with unnecessary parameters removed and deprecated parameters replaced. Parameter indentation will also automatically be fixed.
===Infobox album version===
{{infobox|title=Albums and EPs|data1={{parameter names example|artist|type=studio|prev_title|prev_year|title|year|next_title|next_year}}}}
The album version provides the previous, current, and next albums and the years of their release. The titles appear in italics, with the current title also in bold. Extra formatting does not usually need to be added.
;Code :
<syntaxhighlight lang="wikitext">{{Infobox album
...
| misc = {{subst:Extra chronology
| artist =
| type =
| prev_title =
| prev_year =
| title =
| year =
| next_title =
| next_year =
}}
}}</syntaxhighlight>
;artist : The name of the artist, with <code>live album</code>, <code>EP</code>, etc., appended to produce either "Artist live album chronology", "Artist EP chronology", etc. If just the artist's name is used, the header will display "Artist chronology".
;type : Specify <code>studio</code>, <code>live</code>, <code>compilation</code>, etc., as detailed in {{tl|Infobox album/doc/type}} – this triggers the header color and formatting.
;prev_title : Enter the name of the previous album. If it has a WP article, add a link.
;prev_year : The year the previous album was released.
;title : Enter the name of the current or article title. This should not be wikilinked, since it should be the subject of the article.
;year : The year the current album was released.
;next_title : Enter the name of the next album and link as appropriate.
;next_year : The year the next album was released.
;Notes : All parameters are optional except {{para|title}} and {{para|type}}. Horizontal lines replace the header row if you omit {{para|artist}}.
===Infobox song version===
{{infobox|title=Singles|data1={{parameter names example|artist|type=single|prev_title|prev_year|title|year|next_title|next_year}}}}
In addition to providing the previous, current, and next singles and the years of their release, the single version allows two song titles to be entered. This is useful for [[double A-side]] singles, such as "[[Ruby Tuesday (song)|Ruby Tuesday]]"/"[[Let's Spend the Night Together]]". The titles appear in quotes, with the current title also in bold. Extra formatting does not usually need to be added.
;Code :
<syntaxhighlight lang="wikitext">{{Infobox song
...
| misc = {{subst:Extra chronology
| artist =
| type = single
| prev_title =
| prev_title2 =
| prev_year =
| title =
| title2 =
| year =
| next_title =
| next_title2 =
| next_year =
}}
}}
</syntaxhighlight>
;artist : Enter the name of the artist to produce "Artist singles chronology". (The word "singles" is automatically added for singles chronologies.) Adding <code>American</code>, <code>UK</code>, etc. after "Artist" will display "Artist American singles chronology", "Artist UK singles chronology", etc.
;type : Specify <code>single</code>. (In the event that another {{para|type}} is used in Infobox song, specify <code>song</code> so that a light blue color will be produced).
;prev_title : Enter the name of the previous single. If it has a WP article, add a link.
;prev_title2 : Leave blank, unless the previous is a double A-side single. Then enter the name of the previous single second or flip side and link as appropriate.
;prev_year : The year the previous single was released.
;title : Enter the name of the current or article title. This should not be wikilinked, since it should be the subject of the article.
;title2 : Leave blank, unless it is a double A-side single. Then enter the name of the second or flip side of current or article title.
;year : The year the current single was released.
;next_title : Enter the name of the next single. If it has a article, add a link.
;next_title2 : Enter the name of the second or flip side of next single, if it is a double-A. Link as appropriate.
;next_year : The year the next single was released.
;Notes : All parameters are optional except {{para|title}} and {{para|type}}. Horizontal lines replace the header row if you omit {{para|artist}}.
== Tracking categories ==
* {{clc|Music infoboxes with malformed table placement}}
* {{clc|Pages using extra chronology with unknown parameters}}
{{clear}}
==TemplateData==
{{TemplateDataHeader}}
<templatedata>
{
"description": "For use with {{Infobox album}} or {{Infobox single}} – adds an extra chronology range. If there are deprecated parameters, substitute the template and it will clean itself up.",
"params": {
"type": {
"label": "Type",
"description": "To specify single or a type of album. Valid values are: single, studio, demo, ep, live, greatest, remix, box, compilation, mixtape, soundtrack, film, cast, video, other",
"type": "string",
"required": true,
"suggested": true
},
"artist": {
"label": "Artist",
"description": "The name of the recording artist",
"type": "string",
"required": false,
"suggested": true
},
"prev_title": {
"label": "Previous title",
"description": "The title of the artist's previous release. Add a link if an article exists",
"type": "string",
"required": false,
"example": "[[Help! (song)|Help!]]",
"suggested": true
},
"prev_title2": {
"label": "Previous title (2)",
"description": "The second title of the artist's previous release, for double A-sides only. Add a link if an article exists",
"type": "string",
"required": false,
"example": "[[Day Tripper]]"
},
"prev_year": {
"label": "Previous year",
"description": "The year that the previous work was released",
"type": "date",
"required": false,
"example": "1965",
"suggested": true
},
"title": {
"label": "Title",
"description": "The title of the current release. No formatting required. For double A-sides only, if this is the other side add a wikilink if an article exists",
"type": "string",
"required": true,
"example": "All Star",
"suggested": true
},
"title2": {
"label": "Title (2)",
"description": "The second title of the current release, for double A-sides only. If this is the other side then add a link if an article exists",
"type": "string",
"required": false,
"example": "[[Strawberry Fields Forever]]"
},
"year": {
"label": "Year",
"description": "The year that the current work was released",
"type": "date",
"required": true,
"example": "2001",
"suggested": true
},
"next_title": {
"label": "Next title",
"description": "The title of the artist's following release. Add a link if an article exists",
"type": "string",
"required": false,
"example": "[[My Neck, My Back (Lick It)]]",
"suggested": true
},
"next_title2": {
"label": "Next title (2)",
"description": "The second title of the artist's following release, for double A-sides only. Add a link if an article exists",
"type": "string",
"required": false,
"example": "[[Kids with Guns]]"
},
"next_year": {
"label": "Next year",
"description": "The year the following work was released",
"type": "date",
"required": false,
"example": "2002",
"suggested": true
}
},
"format": "block"
}
</templatedata>
<includeonly>{{Sandbox other||
<!-- Categories below this line, please; interwikis at Wikidata -->
[[Category:Music infobox templates]]
[[Category:Embeddable templates]]
[[Category:Templates that add a tracking category]]
}}</includeonly>
og974001464yhrnvcikk0wcecxrxgoc
User:Sam Sailor/test.js
2
98186
749670
749606
2026-07-03T16:00:06Z
Sam Sailor
26820
Test
749670
javascript
text/javascript
//<nowiki>
(function(mw, $, OO) {
'use strict';
const IGNORE_SUFFIXES = ['Sr.', 'Sr', 'Jr.', 'Jr', 'II', 'III', 'IV', 'V', 'MD', 'PhD'];
const LANG_PREFS = ['frwiki', 'dewiki', 'eswiki', 'itwiki', 'nlwiki', 'svwiki'];
const NON_LANG_WIKIS = ['commonswiki', 'specieswiki', 'metawiki', 'mediawikiwiki', 'wikidatawiki', 'wikimaniawiki'];
const suffixRE = new RegExp('\\s(' + IGNORE_SUFFIXES.join('|').replace(/\./g, '\\.') + ')$', 'i');
function GetterDialog(config) {
GetterDialog.super.call(this, config);
this.abortController = null;
}
OO.inheritClass(GetterDialog, OO.ui.ProcessDialog);
GetterDialog.static.name = 'getterDialog';
GetterDialog.static.title = 'Getter: Wikipedia List Builder';
GetterDialog.static.actions = [
{ action: 'generate', label: 'Generate List', flags: ['primary', 'progressive'], modes: 'edit' },
{ action: 'back', label: 'Go back', modes: 'result' },
{ action: 'exit', label: 'Exit', flags: 'safe', modes: ['edit', 'result'] }
];
GetterDialog.prototype.cleanTitle = function(t) {
return t.replace(suffixRE, '').replace(/\s?\([^)]*\)$/, '').trim();
};
GetterDialog.prototype.normalizeTitle = function(t) {
return t.replace(/_/g, ' ').trim().replace(/^(.)/, c => c.toUpperCase());
};
GetterDialog.prototype.getSortKey = function(title) {
const clean = this.cleanTitle(title);
const parts = clean.split(' ');
return (parts.length <= 1) ? title.toLowerCase() : (parts[parts.length - 1] + ', ' + parts.slice(0, parts.length - 1).join(' ') + ' | ' + title).toLowerCase();
};
GetterDialog.prototype.formatResult = function(p, mode, includeIll) {
if (mode === 'anbl') {
return `* {{anbl|${p.title}}}`;
}
const bY = p.ent?.claims?.P569?.[0]?.mainsnak?.datavalue?.value?.time?.substring(1, 5);
const dY = p.ent?.claims?.P570?.[0]?.mainsnak?.datavalue?.value?.time?.substring(1, 5);
const bio = bY ? ` (${bY}${dY ? '–' + dY : ''})` : '';
const rawDesc = (p.desc || '').replace(/\s?\(\d{4}(–\d{4})?\)$/i, '').trim();
const suffix = rawDesc ? `, ${rawDesc}` : '';
let display = `[[${p.title}]]`;
if (includeIll) {
const langKey = LANG_PREFS.find(l => p.ent.sitelinks?.[l]) ||
Object.keys(p.ent.sitelinks || {}).find(
k => k.endsWith('wiki') &&
k !== 'enwiki' &&
!NON_LANG_WIKIS.includes(k)
);
if (langKey) {
display = `{{ill|${p.title}|${langKey.replace('wiki', '')}|${p.ent.sitelinks[langKey].title}}}`;
}
}
return `* ${display}${bio}${suffix}`;
};
GetterDialog.prototype.matchesCriteria = function(title, compiled) {
const clean = this.cleanTitle(title);
const tokens = clean.split(/\s+/);
const given = tokens[0] || '';
const surname = tokens.length > 1 ? tokens[tokens.length - 1] : '';
const middleLC = tokens.length > 2 ? tokens.slice(1, -1).map(x => x.toLowerCase()) : [];
const check = (val, regexes) => {
if (!regexes || regexes.length === 0) return true;
return regexes.some(re => re.test(val));
};
return check(given, compiled.given) &&
check(surname, compiled.surname) &&
(compiled.middle.length === 0 || compiled.middle.every(m => middleLC.includes(m)));
};
GetterDialog.prototype.getExistingLinks = async function() {
const titles = new Set();
try {
const data = await this.api.get({ action: 'parse', page: mw.config.get('wgPageName'), prop: 'wikitext', format: 'json' });
const text = data.parse?.wikitext?.['*'] || '';
const regexes = [/\[\[([^\]|#]+)/g, /\{\{(?:anbl|Annotated biography link)\|([^}|]+)/gi];
regexes.forEach(re => { let m; while ((m = re.exec(text)) !== null) titles.add(this.normalizeTitle(m[1])); });
} catch (e) {
console.error('[Getter] Parse error:', e);
}
return titles;
};
async function checkEntitiesData(qids, signal) {
if (!qids.length) return {};
let all = {};
for (let i = 0; i < qids.length; i += 50) {
if (signal && signal.aborted) throw new DOMException('Aborted', 'AbortError');
try {
const res = await fetch('https://www.wikidata.org/w/api.php?action=wbgetentities&ids=' + qids.slice(i, i + 50).join('|') + '&props=claims|labels|descriptions|sitelinks&languages=en&format=json&origin=*', { signal }).then(r => r.json());
Object.assign(all, res.entities || {});
} catch (e) {
if (e.name === 'AbortError') throw e;
console.error('[Getter] Wikidata batch error', e);
}
}
return all;
}
GetterDialog.prototype.autoPopulateFields = async function () {
const title = mw.config.get('wgTitle');
const m = title.match(/^(.*?) \((given name|surname)\)$/i);
if (m) {
if (m[2].toLowerCase() === 'given name') {
this.givenTags.addTag(m[1]);
} else {
this.surnameTags.addTag(m[1]);
}
return;
}
if (/\(.+\)$/.test(title)) {
return;
}
try {
const data = await this.api.get({
action: 'parse',
page: mw.config.get('wgPageName'),
prop: 'wikitext',
format: 'json'
});
const text = data.parse?.wikitext?.['*'] || '';
if (/\{\{\s*surname\b/i.test(text)) {
this.surnameTags.addTag(title);
} else if (/\{\{\s*given name\b/i.test(text)) {
this.givenTags.addTag(title);
} else {
this.givenTags.addTag(title);
}
} catch (e) {
console.error('[Getter] Could not inspect page.', e);
this.givenTags.addTag(title);
}
};
GetterDialog.prototype.initialize = function() {
GetterDialog.super.prototype.initialize.apply(this, arguments);
this.api = new mw.Api();
this.panel = new OO.ui.PanelLayout({ padded: true, expanded: false });
this.givenTags = new OO.ui.TagMultiselectWidget({ placeholder: 'Given names...', allowArbitrary: true });
this.middleTags = new OO.ui.TagMultiselectWidget({ placeholder: 'Middle terms...', allowArbitrary: true });
this.surnameTags = new OO.ui.TagMultiselectWidget({ placeholder: 'Surnames...', allowArbitrary: true });
this.excludeTags = new OO.ui.TagMultiselectWidget({ placeholder: 'Exclude strings...', allowArbitrary: true });
this.humanOnly = new OO.ui.CheckboxInputWidget({ selected: true });
this.excludeHumans = new OO.ui.CheckboxInputWidget({ selected: false });
this.includeIll = new OO.ui.CheckboxInputWidget({ selected: false });
this.sortBySurname = new OO.ui.CheckboxInputWidget({ selected: true });
this.updateMode = new OO.ui.CheckboxInputWidget({ selected: false });
this.excludeHumans.on('change', (selected) => {
if (selected) { this.humanOnly.setSelected(false); this.sortBySurname.setSelected(false); }
});
this.humanOnly.on('change', (selected) => {
if (selected) { this.excludeHumans.setSelected(false); }
});
this.formatSelect = new OO.ui.ButtonSelectWidget({ items: [
new OO.ui.ButtonOptionWidget({ data: 'wiki', label: 'Wiki' }),
new OO.ui.ButtonOptionWidget({ data: 'anbl', label: 'ANBL' })
]});
this.formatSelect.selectItemByData('wiki');
this.fieldset = new OO.ui.FieldsetLayout({ label: 'Advanced Filter Settings' });
this.fieldset.addItems([
new OO.ui.FieldLayout(this.givenTags, { label: 'Given names', align: 'top' }),
new OO.ui.FieldLayout(this.middleTags, { label: 'Middle terms', align: 'top' }),
new OO.ui.FieldLayout(this.surnameTags, { label: 'Surnames', align: 'top' }),
new OO.ui.FieldLayout(this.excludeTags, { label: 'Exclude strings', align: 'top' }),
new OO.ui.FieldLayout(this.humanOnly, { label: 'Humans only', align: 'inline' }),
new OO.ui.FieldLayout(this.excludeHumans, { label: 'Exclude humans', align: 'inline' }),
new OO.ui.FieldLayout(this.sortBySurname, { label: 'Sort by surname', align: 'inline' }),
new OO.ui.FieldLayout(this.updateMode, { label: 'Exclude existing links', align: 'inline' }),
new OO.ui.FieldLayout(this.includeIll, { label: 'Include {{ill}}', align: 'inline' }),
new OO.ui.FieldLayout(this.formatSelect, { label: 'Format', align: 'top' })
]);
this.resultArea = new OO.ui.MultilineTextInputWidget({ rows: 15, autosize: true });
this.copyButton = new OO.ui.ButtonWidget({ label: 'Copy list', icon: 'copy', flags: ['progressive'] });
this.copyButton.$element.css('marginTop', '10px');
this.copyButton.on('click', () => {
navigator.clipboard.writeText(this.resultArea.getValue())
.then(() => mw.notify('Copied!'))
.catch(() => mw.notify('Copy failed.', { type: 'error' }));
});
this.resultContainer = new OO.ui.PanelLayout({ padded: true, expanded: false });
this.resultContainer.$element.append(new OO.ui.FieldLayout(this.resultArea, { label: 'Results', align: 'top' }).$element, this.copyButton.$element).hide();
this.panel.$element.append(this.fieldset.$element, this.resultContainer.$element);
this.$body.append(this.panel.$element);
this.actions.setMode('edit');
this.autoPopulateFields();
};
GetterDialog.prototype.runGetter = async function() {
this.abortController = new AbortController();
const signal = this.abortController.signal;
try {
const { humanOnly, excludeHumans, includeIll, sortBySurname, formatSelect, updateMode } = this;
const mode = formatSelect.findSelectedItem().getData();
const existing = updateMode.isSelected() ? await this.getExistingLinks() : new Set();
const resultsMap = new Map();
const searchTerms = [...new Set([...this.givenTags.getValue(), ...this.middleTags.getValue(), ...this.surnameTags.getValue()].map(t => t.trim()).filter(Boolean))];
const excludeList = this.excludeTags.getValue().map(e => e.toLowerCase());
const compile = (vals) => vals.map(r => new RegExp('^' + r.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*') + '$', 'i'));
const compiledCriteria = {
given: compile(this.givenTags.getValue()),
surname: compile(this.surnameTags.getValue()),
middle: this.middleTags.getValue().map(m => m.toLowerCase())
};
if (searchTerms.length === 0) { mw.notify('Please enter search terms.', { type: 'error' }); return; }
for (const term of searchTerms) {
if (signal.aborted) return;
let cont = {};
do {
const data = await this.api.get({ action: 'query', list: 'search', srsearch: `intitle:"${term}"`, srlimit: 'max', sroffset: cont.sroffset || 0, format: 'json' });
for (const p of (data.query?.search || [])) {
const titleLC = p.title.toLowerCase();
if (excludeList.some(e => titleLC.includes(e))) continue;
const title = this.normalizeTitle(p.title);
if (!existing.has(title) && this.matchesCriteria(title, compiledCriteria)) {
resultsMap.set(p.title, { title: p.title, pageid: p.pageid });
}
}
cont = data.continue || {};
} while (cont.sroffset && !signal.aborted);
}
const pageIds = Array.from(resultsMap.values()).map(p => p.pageid);
const confirmedResults = new Map();
for (let i = 0; i < pageIds.length; i += 50) {
if (signal.aborted) return;
const chunk = pageIds.slice(i, i + 50);
const qidList = [], qidMap = {};
const pageData = await this.api.get({ action: 'query', prop: 'pageprops|description', pageids: chunk.join('|'), format: 'json' });
Object.values(pageData.query.pages).forEach(p => {
if (p.pageprops?.wikibase_item) {
qidList.push(p.pageprops.wikibase_item);
qidMap[p.pageprops.wikibase_item] = p;
}
});
const entities = await checkEntitiesData(qidList, signal);
for (const qid in entities) {
const ent = entities[qid], p = qidMap[qid];
const isHuman = ent.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5");
if ((humanOnly.isSelected() && !isHuman) || (excludeHumans.isSelected() && isHuman)) continue;
confirmedResults.set(p.title, {
title: p.title,
desc: p.description || ent.descriptions?.en?.value || '',
ent
});
}
}
const out = Array.from(confirmedResults.values())
.map(p => ({
formatted: this.formatResult(p, mode, includeIll.isSelected()),
sortKey: sortBySurname.isSelected()
? this.getSortKey(p.title)
: p.title.toLowerCase()
}))
.sort((a, b) => a.sortKey.localeCompare(b.sortKey, undefined, {
sensitivity: 'base'
}))
.map(r => r.formatted)
.join('\n');
this.resultArea.setValue(out);
this.fieldset.$element.hide();
this.resultContainer.$element.show();
this.actions.setMode('result');
} catch (e) {
if (e.name !== 'AbortError') {
console.error('[Getter] Generator error:', e);
mw.notify('An error occurred while generating the list.', { type: 'error' });
}
}
};
GetterDialog.prototype.getActionProcess = function(action) {
if (action === 'generate') {
return new OO.ui.Process(async () => { await this.runGetter(); });
}
if (action === 'back') {
if (this.abortController) this.abortController.abort();
return new OO.ui.Process(() => {
this.resultContainer.$element.hide();
this.fieldset.$element.show();
this.actions.setMode('edit');
});
}
if (action === 'exit') {
if (this.abortController) this.abortController.abort();
return new OO.ui.Process(() => this.close());
}
return GetterDialog.super.prototype.getActionProcess.call(this, action);
};
mw.loader.using(['mediawiki.api', 'mediawiki.util', 'oojs-ui', 'oojs-ui-widgets', 'oojs-ui-windows']).then(() => {
const wm = new OO.ui.WindowManager();
$(document.body).append(wm.$element);
const dialog = new GetterDialog({ size: 'medium' });
wm.addWindows([dialog]);
$(mw.util.addPortletLink('p-tb', '#', 'Getter', 't-getter')).on('click', (e) => {
e.preventDefault();
wm.openWindow(dialog);
});
});
}(mediaWiki, jQuery, OO));
(function() {
if (window.IllWill) return;
const STRIKEOUT = true;
const HIDE = 'hide';
const IllWill = {
Version: "0.5",
Attribution: 'created by <a target="_blank" href="/wiki/User:Cobaltcigs">cobaltcigs</a>',
Summary: "Added interlanguage links using IllWill.js",
DataApi: "https://www.wikidata.org/w/api.php",
LocalLang: mw.config.get("wgPageContentLanguage"),
Disambiguator: / \(.*\)$/,
SiteLinks: {},
Config: {
MaxLanguages: 10,
IgnoreScientific: STRIKEOUT,
AutoDiff: true
},
css: `
#illwill { clear: both; margin-bottom: 1.5em; border: 1px solid #a2a9b1; padding: 15px; background: #f8f9fa; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.ill-sitelinks a { display: inline-block; margin-bottom: 3px; font-size: 0.9em; color: #36c; }
.ill-size-tag { font-size: 0.8em; color: #72777d; margin-left: 6px; white-space: nowrap; }
.illwill-ignore { text-decoration: line-through; color: #72777d; font-size: 0.85em; }
.ill-x { width: 35px; text-align: center; }
.ill-input, .ill-output { font-family: 'Courier New', monospace; white-space: pre-wrap; font-size: 0.9em; background: #fff; border: 1px solid #eaecf0; padding: 4px; border-radius: 2px; }
#ill-buttons { text-align: right; padding: 10px; border-top: 1px solid #a2a9b1; margin-top: 10px; }
#ill-buttons button { margin-left: 10px; cursor: pointer; padding: 6px 16px; border-radius: 2px; border: 1px solid #a2a9b1; transition: all 0.2s; }
#ill-ok { background: #36c; color: #fff; border-color: #36c !important; font-weight: bold; }
#ill-ok:hover { background: #447ff5; }
#ill-ok:disabled { background: #eaecf0; color: #72777d; border-color: #c8ccd1 !important; cursor: not-allowed; opacity: 0.7; }
#ill-table { width: 100%; border-collapse: collapse; margin-top: 10px; background: white; }
#ill-table th { background: #f2f2f2; text-align: left; padding: 8px; border: 1px solid #a2a9b1; }
#ill-table td { padding: 8px; border: 1px solid #a2a9b1; vertical-align: top; }
#ill-table caption { background: #eaffea; font-weight: bold; padding: 10px; border: 1px solid #a2a9b1; border-bottom: none; font-size: 1.1em; }
#ill-help { font-size: 0.85em; color: #54595d; padding: 10px; line-height: 1.4; }
.ill-loading-inline { font-style: italic; color: #72777d; font-size: 0.9em; display: block; margin: 5px 0; }
`,
setup() {
if (!["edit", "submit"].includes(mw.config.get('wgAction'))) return;
mw.loader.addStyleTag(this.css);
const link = mw.util.addPortletLink('p-cactions', '#', 'IllWill', 'ca-illwill', "Add interlanguage links via Wikidata");
if (link) {
link.addEventListener('click', (e) => {
e.preventDefault();
this.makePanel();
});
}
},
norm(s) {
if (!s) return "";
const t = s.trim().replace(/[\s_]+/g, " ");
return t.charAt(0).toUpperCase() + t.slice(1);
},
async getRedLinks($textbox) {
const txt = $textbox.textSelection('getContents') || "";
const wl = txt.match(/\[\[[^[\]\n]+\]\]/g);
if (!wl) return {
reds: [],
txt
};
const api = new mw.Api();
const textToParse = wl.join("").replace(/\|[^\]]+/g, "");
try {
const data = await api.post({
action: 'parse',
text: textToParse,
contentmodel: 'wikitext',
formatversion: 2
});
const tempDiv = document.createElement('div');
tempDiv.innerHTML = data.parse.text;
const reds = Array.from(tempDiv.querySelectorAll("a.new")).map(x => {
const urlParams = new URLSearchParams(x.href.split('?')[1]);
return urlParams.get('title')?.replace(/_/g, " ");
}).filter(Boolean);
return {
reds: [...new Set(reds)],
txt
};
} catch (e) {
return {
reds: [],
txt
};
}
},
async makePanel() {
const $editform = $('#editform');
const $textbox = $('#wpTextbox1');
$('#illwill').remove();
$editform.hide();
const $wrapper = $('<div id="illwill">Loading redlinks...</div>').insertBefore($editform);
const {
reds,
txt
} = await this.getRedLinks($textbox);
const wtLinks = [];
reds.forEach(title => {
const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\s/g, "[\\s_]+");
const re = new RegExp("\\[\\[\\s*" + escaped + "\\s*(?:\\|[^\\[\\]]+)?\\]\\]", "ig");
const match = txt.match(re);
if (match && !wtLinks.some(z => z.title === title)) {
wtLinks.push({
link: match[0],
title
});
}
});
if (!wtLinks.length) {
const $back = $('<button>Quit</button>').on('click', () => {
$wrapper.remove();
$editform.show();
});
$wrapper.empty().append($('<strong>No valid redlinks found in text.</strong> '), $back);
return;
}
const $table = $(`
<table id="ill-table" class="wikitable">
<caption>IllWill.js (v${this.Version})</caption>
<thead>
<tr>
<th class="ill-x"><input type="checkbox" id="ill-select-all" disabled></th>
<th>Input link</th>
<th>Possible Wikidata matches</th>
<th>Sitelinks (top ${this.Config.MaxLanguages} by size)</th>
<th>Resulting wikitext</th>
</tr>
</thead>
<tbody></tbody>
<tfoot>
<tr>
<td colspan="3" id="ill-help">Carefully examine possible Wikidata matches.</td>
<td colspan="2" id="ill-buttons">
<button id="ill-cancel">Cancel</button>
<button id="ill-ok" disabled>Show changes</button>
</td>
</tr>
</tfoot>
</table>
`);
const $tbody = $table.find('tbody');
wtLinks.forEach((item, i) => {
$tbody.append(`
<tr id="ill-row-${i}" data-index="${i}">
<td class="ill-x"><input type="checkbox" class="ill-row-check" disabled></td>
<td class="ill-input">${this.escapeHTML(item.link)}</td>
<td class="ill-wd-item"><span class="ill-loading-inline">Searching...</span></td>
<td class="ill-sitelinks"></td>
<td class="ill-output"></td>
</tr>
`);
});
$wrapper.empty().append($table);
const chunkSize = 5;
for (let i = 0; i < wtLinks.length; i += chunkSize) {
const chunk = wtLinks.slice(i, i + chunkSize);
await Promise.all(chunk.map((item, idx) => this.fetchWikidataOptimized(item.title, i + idx)));
}
$table.on('click', '#ill-cancel', () => {
$editform.show();
$wrapper.remove();
});
$table.on('click', '#ill-ok', () => this.onOkButton($editform, $wrapper, $textbox));
$table.on('change', '#ill-select-all', (e) => {
const isChecked = $(e.target).is(':checked');
$table.find('.ill-row-check:not(:disabled)').each(function() {
$(this).prop('checked', isChecked).trigger('change');
});
});
$table.on('change', '.ill-row-check', (e) => {
this.onCheckbox(e);
this.updateOkButtonState($table);
});
$table.on('change', 'input[type="radio"]', (e) => {
this.showSiteLinks(e);
});
},
async fetchWikidataOptimized(title, index) {
const query = title.replace(this.Disambiguator, "");
const url = `${this.DataApi}?action=wbsearchentities&search=${encodeURIComponent(query)}&language=${this.LocalLang}&limit=5&format=json&origin=*`;
try {
const resp = await fetch(url);
const data = await resp.json();
const results = data.search;
const $cell = $(`#ill-row-${index} .ill-wd-item`);
if (!results || !results.length) {
$cell.html('<span class="ill-na">(no results)</span>');
return;
}
const entityIds = results.map(r => r.id);
await this.fetchEntities(entityIds, index, $cell);
} catch (e) {
$(`#ill-row-${index} .ill-wd-item`).text("Search failed.");
}
},
async fetchEntities(ids, rowIndex, $cell) {
const url = `${this.DataApi}?action=wbgetentities&ids=${ids.join('|')}&props=labels|descriptions|claims|sitelinks&format=json&origin=*`;
try {
const resp = await fetch(url);
const data = await resp.json();
$cell.empty();
ids.forEach(qNum => {
const entity = data.entities[qNum];
if (!entity) return;
const isScientific = entity.claims?.P31?.some(s => s.mainsnak?.datavalue?.value?.id === "Q13442814");
if (isScientific && this.Config.IgnoreScientific === HIDE) return;
const desc = entity.descriptions?.[this.LocalLang]?.value || entity.descriptions?.en?.value || "";
const label = entity.labels?.[this.LocalLang]?.value || entity.labels?.en?.value || qNum;
const isStrikeout = isScientific && this.Config.IgnoreScientific === STRIKEOUT;
const $div = $(`
<div class="${isStrikeout ? 'illwill-ignore' : ''}">
<input type="radio" name="selection-${rowIndex}" value="${qNum}" id="radio-${qNum}" ${isStrikeout ? 'disabled' : ''}>
<label for="radio-${qNum}">
<a href="https://wikidata.org/wiki/${qNum}" target="_blank">${this.escapeHTML(label)}</a>:
<span class="ill-desc">${desc ? this.escapeHTML(desc) : '<i>no description</i>'}</span>
</label>
</div>
`);
$cell.append($div);
this.SiteLinks[qNum] = entity.sitelinks || {};
});
} catch (e) {
$cell.text("Detail fetch failed.");
}
},
updateOkButtonState($table) {
const $rows = $table.find('.ill-row-check:not(:disabled)');
const $checkedRows = $rows.filter(':checked');
const anyEnabled = $rows.length > 0;
$table.find('#ill-ok').prop('disabled', $checkedRows.length === 0);
const $master = $table.find('#ill-select-all');
$master.prop('disabled', !anyEnabled);
if (anyEnabled && $rows.length === $checkedRows.length) {
$master.prop('checked', true);
} else {
$master.prop('checked', false);
}
},
async showSiteLinks(e) {
const isManualToggling = e.fromCheckbox || false;
const qNum = e.target.value;
const $row = $(e.target).closest('tr');
const sitelinks = this.SiteLinks[qNum];
const $checkbox = $row.find('.ill-row-check');
const $sitelinkCell = $row.find('.ill-sitelinks');
const $outputCell = $row.find('.ill-output');
const $table = $('#ill-table');
const effectiveMax = Math.min(Math.max(this.Config.MaxLanguages, 3), 6);
const validKeys = Object.keys(sitelinks).filter(k => k.endsWith('wiki') && !k.includes('commons'));
$sitelinkCell.html('<span class="ill-loading-inline">Measuring articles...</span>');
$outputCell.empty();
if (!isManualToggling) {
$checkbox.prop('disabled', true).prop('checked', false);
}
const candidates = validKeys.slice(0, 25);
const sizeMap = [];
await Promise.all(candidates.map(async (key) => {
const lang = key.replace('wiki', '').replace(/_/g, '-');
const title = sitelinks[key].title;
const endpoint = `https://${lang}.wikipedia.org/w/api.php?action=query&titles=${encodeURIComponent(title)}&prop=info&format=json&origin=*`;
try {
const res = await fetch(endpoint).then(r => r.json());
const page = Object.values(res.query.pages)[0];
sizeMap.push({
lang,
title,
length: page.length || 0
});
} catch (err) {
sizeMap.push({
lang,
title,
length: 0
});
}
}));
sizeMap.sort((a, b) => b.length - a.length);
const topLinks = sizeMap.slice(0, effectiveMax);
$sitelinkCell.html(topLinks.map(item => {
const kbValue = Math.round(item.length / 1024);
return `<div><a href="https://${item.lang}.wikipedia.org/wiki/${encodeURIComponent(item.title)}" target="_blank">${item.lang}:${this.escapeHTML(item.title)}</a> <span class="ill-size-tag">(${kbValue} kB)</span></div>`;
}).join(""));
const inputWikitext = $row.find('.ill-input').text();
const match = inputWikitext.match(/\[\[(.*?)\]\]/);
if (match) {
const parts = match[1].split('|');
const targetPage = parts[0].trim();
const surfaceName = (parts[1] || parts[0]).trim();
const illLangs = topLinks.map(item => {
const isSameTitle = this.norm(item.title) === this.norm(targetPage);
return `|${item.lang}|${isSameTitle ? '' : item.title}`;
}).join("");
const ltParam = (this.norm(surfaceName) === this.norm(targetPage)) ? "" : `|lt=${surfaceName}`;
$outputCell.text(`{{ill|${targetPage}${ltParam}${illLangs}}}`);
}
$checkbox.prop('disabled', !topLinks.length);
if (!isManualToggling) {
$checkbox.prop('checked', !!topLinks.length);
}
this.updateOkButtonState($table);
},
onCheckbox(e) {
const $row = $(e.target).closest('tr');
const $radio = $row.find('input[type="radio"]:checked');
const isChecked = $(e.target).is(':checked');
if (isChecked) {
if ($radio.length) {
this.showSiteLinks({
target: $radio[0],
fromCheckbox: true
});
}
} else {
$row.find('.ill-sitelinks, .ill-output').empty();
}
},
onOkButton($editform, $wrapper, $textbox) {
let text = $textbox.textSelection('getContents');
let changed = false;
$wrapper.find('.ill-row-check:checked').each(function() {
const $row = $(this).closest('tr');
const input = $row.find('.ill-input').text();
const output = $row.find('.ill-output').text();
if (output) {
const escapedInput = input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
text = text.replace(new RegExp(escapedInput, "gi"), output);
changed = true;
}
});
if (changed) {
$('#wpSummary').val((i, v) => (v ? v + "; " : "") + this.Summary);
$textbox.textSelection('setContents', text);
$wrapper.remove();
$editform.show();
if (this.Config.AutoDiff) $('#wpDiff').click();
}
},
escapeHTML(str) {
return str.replace(/[&<>"']/g, m => ({
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
})[m]);
}
};
window.IllWill = IllWill;
mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => {
$(() => IllWill.setup());
});
})();
/* global ComradeLib, mw, $ */
(async function() {
try {
await mw.loader.getScript('https://test.wikipedia.org/w/index.php?title=User:Sam_Sailor/test.js/comrade-options.js&action=raw&ctype=text/javascript');
} catch (e) {
console.warn("Comrade: Could not load /comrade-options.js, using fallback defaults.", e);
}
window.ComradeOptions = Object.assign({
version: "0.5.0",
excludedTemplates: [],
nudgeTimeout: 10000,
debugMode: false
}, window.ComradeOptions || {});
await mw.loader.getScript('https://test.wikipedia.org/w/index.php?title=User:Sam_Sailor/test.js/comrade-lib.js&action=raw&ctype=text/javascript');
async function getTargetPage(api, name, type) {
const titles = type === 'surname' ? [`List of people with the English surname ${name}`, `List of people with surname ${name}`, `${name} (surname)`, `${name} (name)`, name] : [`List of people with given name ${name}`, `List of people named ${name}`, `${name} (given name)`, `${name} (name)`, name];
const res = await api.get({
action: 'query',
titles: titles.join('|'),
formatversion: 2
});
if (!res.query || !res.query.pages) return null;
return titles.find(t => {
const pg = res.query.pages.find(p => p.title === t);
return pg && !pg.missing;
});
}
async function processHumanNameLists(api, entity, wikitext, currentTitle, hasShortDesc, isOrphan, tClean) {
const {
givens,
surnames
} = await fetchWikidataNameLabels(entity);
const nameParts = tClean.split(' ');
const hasFamilyNameHatnote = /\{\{family[ _]name[ _]hatnote/i.test(wikitext);
const primarySurname = hasFamilyNameHatnote ? nameParts[0] : (getSmartSortKey(tClean, entity).split(',')[0] || nameParts[nameParts.length - 1]);
for (const name of givens) {
if (name.toLowerCase() === primarySurname.toLowerCase()) continue;
const nameInTitle = new RegExp('\\b' + name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b', 'i').test(tClean);
if (!nameInTitle) continue;
const target = await getTargetPage(api, name, 'given');
if (target) await verifyAndNudge(api, name, 'given name', currentTitle, hasShortDesc, isOrphan, target);
}
const finalSurnames = surnames.length > 0 ? surnames : (nameParts.length >= 2 ? [nameParts[nameParts.length - 1]] : []);
for (const name of finalSurnames) {
const nameInTitle = new RegExp('\\b' + name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b', 'i').test(tClean);
if (!nameInTitle) continue;
const target = await getTargetPage(api, name, 'surname');
if (target) await verifyAndNudge(api, name, 'surname', currentTitle, hasShortDesc, isOrphan, target);
}
}
async function verifyAndNudge(api, name, type, currentTitle, hasShortDesc, isOrphan, foundTitle) {
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: foundTitle,
rvprop: 'content',
formatversion: 2
});
const page = res.query.pages[0];
if (!page || page.missing || !page.revisions) return;
const text = page.revisions[0].content;
const isNameMatch = /\{\{(Surname|Hndis|Given[ _]name|Forename|Givenname)/i.test(text);
const isNameDab = /\{\{(Disambiguation|Dab|Disambig)(?:[^}]*\|(surname|surnames|given[ _]name|given[ _]names)|(?:\s*\}\}))/i.test(text);
const isNamePageTitle = foundTitle.includes('List of people with');
if (!isNameMatch && !isNameDab && !isNamePageTitle) return;
const escapedTitle = currentTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/ /g, '[ _]');
const alreadyListed = new RegExp('(\\[\\[|\\{\\{anbl\\|[ _]*|\\|)' + escapedTitle, 'i').test(text);
if (alreadyListed) return;
const $container = $('<div>').addClass('comrade-container').addClass(isOrphan ? 'comrade-nudge' : 'comrade-info');
const $header = $('<div>').addClass('comrade-header').append($('<strong>').text(isOrphan ? 'Comrade nudge:' : 'Informative:'));
const $content = $('<div>').addClass('comrade-content');
$content.append($('<span>').append(`${type.charAt(0).toUpperCase() + type.slice(1)} not listed at `, $('<a>').attr({
href: mw.util.getUrl(foundTitle),
target: '_blank'
}).text(foundTitle), "; should it be?"));
const snippet = '* {{anbl|' + currentTitle + '}}';
const $snippetWrapper = $('<div>').css({
'display': 'flex',
'align-items': 'center',
'gap': '10px',
'margin-top': '4px'
});
const $instructionSpan = $('<span>').append('Consider adding ', $('<code>').text(snippet));
const $copyBtn = $('<button>').text('Copy & insert').on('click', function() {
navigator.clipboard.writeText('\n' + snippet).then(() => {
$(this).text('Copied!').prop('disabled', true);
const editUrl = mw.util.getUrl(foundTitle, {
action: 'edit',
summary: 'Adding * {{[[Template:Annotated biography link|anbl]]|[[' + currentTitle + ']]}}'
});
window.open(editUrl, '_blank');
});
});
$snippetWrapper.append($instructionSpan, $copyBtn);
$content.append($snippetWrapper);
if (!hasShortDesc) {
$content.append($('<span>').css({
'color': '#d33',
'font-weight': 'bold',
'margin-top': '5px'
}).text('⚠️ Missing short description: Please add a concise one before listing.'));
}
$container.append($header, $content);
ComradeLib.appendDismiss($container);
}
async function checkNamingPrecision(currentTitle, isHuman) {
const disambigMatch = currentTitle.match(/^(.+?)(?:, | \()/);
if (isHuman || !disambigMatch) return;
const baseTitle = disambigMatch[1].trim();
const api = new mw.Api();
try {
const baseRes = await api.get({
action: 'query',
titles: baseTitle,
redirects: 1,
formatversion: 2
});
const page = baseRes.query.pages[0];
const baseExists = !page.missing;
let baseRedirectsToSelf = false;
if (baseRes.query.redirects) {
baseRedirectsToSelf = baseRes.query.redirects.some(r => r.to === currentTitle);
}
const searchRes = await api.get({
action: 'query',
list: 'allpages',
apprefix: baseTitle,
aplimit: 50,
formatversion: 2
});
const competitors = searchRes.query.allpages.filter(p => {
const t = p.title;
return t !== currentTitle && t !== baseTitle && (t.startsWith(baseTitle + ', ') || t.startsWith(baseTitle + ' ('));
});
if (!baseExists || baseRedirectsToSelf) {
const $container = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $content = $('<div>').addClass('comrade-content');
if (competitors.length === 0) {
$header.append($('<strong>').text('Precision:'));
const reason = baseRedirectsToSelf ? "already redirects here" : "is free";
$content.append($('<span>').append(`The base name `, $('<code>').text(baseTitle), ` ${reason}. Consider moving this article.`));
const moveUrl = mw.util.getUrl('Special:MovePage/' + currentTitle, {
wpNewTitle: baseTitle,
wpReason: 'Unnecessary disambiguation; base name is free/redirects here per [[WP:PRECISION]]'
});
$content.append($('<button>').text(`Move to ${baseTitle}`).on('click', () => window.open(moveUrl, '_blank')));
} else {
$header.append($('<strong>').text('Observation:'));
const state = baseRedirectsToSelf ? 'redirect to this page' : 'redlink';
$content.append($('<span>').append(`Base name `, $('<code>').text(baseTitle), ` has ${competitors.length} competitors, but it is currently a ${state}.`));
$content.append($('<span>').text('Consider if a disambiguation page is needed.'));
}
$container.append($header, $content);
ComradeLib.appendDismiss($container);
}
} catch (e) {
console.warn("Comrade: Precision check failed", e);
}
}
async function checkHumanSort(wikitext, entity, currentTitle, tClean, refName) {
const dsMatch = wikitext.match(/\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/i);
const {
givens
} = await fetchWikidataNameLabels(entity);
if (dsMatch) {
const currentDS = dsMatch[1].trim();
if (currentDS.includes(',')) {
let [lastPart, firstPart] = currentDS.split(',').map(s => s.trim());
if (firstPart.toLowerCase().endsWith(lastPart.toLowerCase())) {
let fixedFirst = firstPart.substring(0, firstPart.length - lastPart.length).trim();
let suggestion = `${fixNameCasing(lastPart, refName)}, ${fixNameCasing(fixedFirst, refName)}`;
if (suggestion !== currentDS) renderSortNudge(suggestion, "Redundant surname in given name field.");
} else if (givens.length > 0 && lastPart.split(' ').length > 1) {
const parts = lastPart.split(' ');
const misplacedGiven = parts.find(part => givens.some(gn => gn.toLowerCase() === part.toLowerCase()));
if (misplacedGiven) {
const newSurname = parts.filter(p => p !== misplacedGiven).join(' ');
let suggestion = `${fixNameCasing(newSurname, refName)}, ${fixNameCasing(misplacedGiven + ' ' + firstPart, refName)}`;
if (suggestion !== currentDS) renderSortNudge(suggestion, `Wikidata suggests "${misplacedGiven}" is a given name.`);
}
}
}
} else {
const hasFamilyNameHatnote = /\{\{family[ _]name[ _]hatnote/i.test(wikitext);
const nameParts = tClean.split(' ');
const targetSortName = hasFamilyNameHatnote ? `${nameParts[0]}, ${nameParts.slice(1).join(' ')}` : getSmartSortKey(tClean, entity);
if (targetSortName.toLowerCase() !== currentTitle.toLowerCase()) {
const $dsMissing = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Add: {{DEFAULTSORT:${targetSortName}}}`).on('click', () => performSortCorrection(targetSortName, "add"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Tag is missing. `, $btn));
$dsMissing.append($header);
ComradeLib.appendDismiss($dsMissing);
}
}
}
async function fetchWikidataEntity(qid) {
const res = await fetch(`https://www.wikidata.org/w/api.php?action=wbgetentities&ids=${qid}&props=claims|descriptions|labels&languages=en&format=json&origin=*`).then(r => r.json());
return res.entities[qid];
}
function getSmartSortKey(fullName, entity) {
const countryIds = entity.claims?.P27?.map(c => c.mainsnak?.datavalue?.value?.id) || [];
const locationIds = entity.claims?.P17?.map(c => c.mainsnak?.datavalue?.value?.id) || [];
const placeIds = [...(entity.claims?.P19 || []), ...(entity.claims?.P119 || [])].map(c => c.mainsnak?.datavalue?.value?.id).filter(Boolean);
const allRelevantIds = [...countryIds, ...locationIds, ...placeIds];
const birthYear = parseInt(entity.claims?.P569?.[0]?.mainsnak?.datavalue?.value?.time?.match(/[+-](\d{4})/)?.[1]) || null;
if (fullName.startsWith("Saint ")) return fullName.replace("Saint ", "");
const arabicParticles = /\b(Abu|Abd|Abdel|Abdul|ben|bin|bint)\b/i;
if (fullName.match(arabicParticles)) {
if (birthYear && birthYear > 1900) {
const parts = fullName.split(' ');
const binIndex = parts.findIndex(p => p.toLowerCase() === 'bin' || p.toLowerCase() === 'ben');
if (binIndex > 0) return parts.slice(binIndex).join(' ') + ', ' + parts.slice(0, binIndex).join(' ');
} else {
return fullName;
}
}
const eastAsianQids = ["Q148", "Q865", "Q884", "Q424", "Q17", "Q881"];
if (allRelevantIds.some(id => eastAsianQids.includes(id))) {
if (allRelevantIds.includes("Q17") && birthYear && birthYear > 1885) return westernSort(fullName);
const parts = fullName.split(' ');
if (parts.length > 1) return `${parts[0]}, ${parts.slice(1).join(' ')}`;
}
return westernSort(fullName);
}
function westernSort(name) {
let cleanName = name;
if (name.startsWith("O'")) cleanName = name.replace("O'", "O");
const parts = cleanName.split(' ');
if (parts.length < 2) return cleanName;
const surname = parts.pop();
if (["Jr.", "Jr", "III", "II", "Sr.", "Sr"].includes(surname)) {
const actualSurname = parts.pop();
return `${actualSurname}, ${parts.join(' ')} ${surname}`;
}
return `${surname}, ${parts.join(' ')}`;
}
async function performSortCorrection(newName, type) {
const api = new mw.Api();
const title = mw.config.get("wgPageName");
try {
const data = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
let content = data.query.pages[0].revisions[0].content;
const dsRegex = /\{\{\s*(?:DEFAULTSORT|Defaultsort)\s*:[^}]+\}\}/i;
const newTag = `{{DEFAULTSORT:${newName}}}`;
if (dsRegex.test(content)) {
content = content.replace(dsRegex, newTag);
} else {
const catRegex = /\[\[Category:/i;
content = catRegex.test(content) ? content.replace(catRegex, `${newTag}\n[[Category:`) : content.trim() + `\n\n${newTag}`;
}
await api.postWithEditToken({
action: 'edit',
title: title,
text: content,
summary: `Setting DEFAULTSORT to ${newName}`,
nocreate: true
});
mw.notify(`DEFAULTSORT ${type === "add" ? "added" : "corrected"} to ${newName}!`, {
title: 'Comrade Success',
type: 'success'
});
setTimeout(() => location.reload(), 700);
} catch (err) {
mw.notify('Failed to save DEFAULTSORT.', {
type: 'error'
});
}
}
function renderSortNudge(target, reason) {
const $dsNudge = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Correct to: ${target}`).on('click', () => performSortCorrection(target, "format"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` ${reason} `, $btn));
$dsNudge.append($header);
ComradeLib.appendDismiss($dsNudge);
}
async function fetchWikidataNameLabels(entity) {
const familyIds = entity.claims?.P734?.map(c => c.mainsnak.datavalue.value.id) || [];
const givenIds = entity.claims?.P735?.map(c => c.mainsnak.datavalue.value.id) || [];
const allNameIds = [...new Set([...familyIds, ...givenIds])];
if (allNameIds.length === 0) return {
givens: [],
surnames: []
};
const url = new URL("https://www.wikidata.org/w/api.php");
url.search = new URLSearchParams({
action: 'wbgetentities',
ids: allNameIds.join('|'),
props: 'labels',
languages: 'en',
format: 'json',
origin: '*'
});
const nameData = await fetch(url).then(r => r.json());
return {
givens: givenIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean),
surnames: familyIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean)
};
}
function fixNameCasing(part, referenceName) {
return part.split(' ').map(word => {
const match = referenceName.match(new RegExp(`\\b${word}\\b`, 'i'));
if (match) return match[0];
return referenceName.split(' ').find(tWord => word.toLowerCase().startsWith(tWord.toLowerCase()) || tWord.toLowerCase().startsWith(word.toLowerCase())) || word;
}).join(' ');
}
async function init() {
const api = new mw.Api();
const ns = mw.config.get("wgNamespaceNumber");
const action = mw.config.get("wgAction");
if (mw.config.get("wgDBname") !== "enwiki" || action !== "view") return;
if (ns === 14) {
const $btn = $('<button>').attr('id', 'comrade-audit-btn').text('Audit category quality').css({
'margin-bottom': '10px',
'padding': '5px 10px',
'cursor': 'pointer'
}).on('click', ComradeLib.performCategoryAudit);
$('.mw-category-generated').prepend($btn);
return;
}
if (ns !== 0) return;
const qid = mw.config.get("wgWikibaseItemId");
const currentTitle = mw.config.get("wgTitle");
try {
const [pageData, backlinkData] = await Promise.all([
api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get("wgPageName"),
rvprop: 'content',
formatversion: 2
}),
api.get({
action: 'query',
list: 'backlinks',
blfilterredir: 'nonredirects',
bllimit: 50,
blnamespace: 0,
bltitle: mw.config.get("wgPageName"),
formatversion: 2
})
]);
const page = pageData.query.pages[0];
if (!page || page.missing || !page.revisions) return;
const wikitext = page.revisions[0].content;
ComradeLib.checkLeadCapitalization(currentTitle);
// Ligature/ASCII-only/diacritic and domain
const norms = ComradeLib.getNormalizedTitles(currentTitle);
if (norms.ascii) {
let label = "ASCII-only";
let category = "R from ASCII-only";
if (/[æÆœŒijIJßẞ]/.test(currentTitle)) {
label = "Ligature";
category = "R to ligature";
} else if (currentTitle.normalize("NFD").match(/[\u0300-\u036f]/)) {
label = "Diacritic";
category = "R to diacritic";
}
await ComradeLib.checkAndRenderRedirect(norms.ascii, currentTitle, category, label);
}
await ComradeLib.checkDomainRedirect(qid, currentTitle);
// Orphan logic
const isOrphanTagged = /\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)/i.test(wikitext) || /\| *orphan *=/i.test(wikitext);
const backLinkCount = backlinkData.query.backlinks.length;
if (isOrphanTagged && backLinkCount >= 1) {
const $container = $('<div>').addClass('comrade-container comrade-status');
const $btn = $('<button>').text('Remove orphan tag').on('click', async () => await ComradeLib.performDeorphan());
const $statusText = $('<span>').append($('<strong>').text('Status:'), ` Article has ${backLinkCount} backlink(s).`, $btn);
$container.append($('<div>').addClass('comrade-header').append($statusText));
ComradeLib.appendDismiss($container);
}
// Wikidata/human logic
let entity = qid ? await fetchWikidataEntity(qid) : null;
let isHuman = entity?.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5");
await checkNamingPrecision(currentTitle, isHuman);
if (isHuman && entity) {
const tClean = currentTitle.replace(/\s*\(.*?\)\s*/g, '').trim();
const refName = entity.labels?.en?.value || currentTitle;
const hasFamilyNameHatnote = /\{\{family[ _]name[ _]hatnote/i.test(wikitext);
const hasShortDesc = /\{\{\s*[Ss]hort description/i.test(wikitext);
const isOrphan = isOrphanTagged && backLinkCount < 1;
await ComradeLib.checkShortDescDates(wikitext, entity);
await ComradeLib.checkNoShortDesc(wikitext, entity);
// Check DEFAULTSORT
await checkHumanSort(wikitext, entity, currentTitle, tClean, refName);
const longName = ComradeLib.getLongNameFromWikitext(wikitext);
if (longName && longName.length > tClean.length) {
let sortKey = hasFamilyNameHatnote ? `${longName.split(' ')[0]}, ${longName.split(' ').slice(1).join(' ')}` : getSmartSortKey(longName, entity);
const rLongWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from long name}}\n}}\n{{DEFAULTSORT:${sortKey}}}`;
await ComradeLib.checkAndRenderRedirect(longName, currentTitle, null, "Long name", rLongWikitext);
}
// Sort name R
const targetSortName = hasFamilyNameHatnote ? `${tClean.split(' ')[0]}, ${tClean.split(' ').slice(1).join(' ')}` : getSmartSortKey(tClean, entity);
if (targetSortName && targetSortName.toLowerCase() !== currentTitle.toLowerCase()) {
let targetPage = currentTitle.includes(' (') ? currentTitle.split(' (')[0] : currentTitle;
let rCat = targetPage !== currentTitle ? "R from ambiguous sort name" : "R from sort name";
if (targetSortName.includes(',')) {
const p = targetSortName.split(',');
rCat += `|${p[0].trim().charAt(0).toUpperCase()}|${p[1].trim().charAt(0).toUpperCase()}`;
}
await ComradeLib.checkAndRenderRedirect(targetSortName, targetPage, rCat, "Sort name");
}
// Name lists
await processHumanNameLists(api, entity, wikitext, currentTitle, hasShortDesc, isOrphan, tClean);
}
// Quality/ORES
const isRedirect = !!mw.config.get("wgIsRedirect");
const userExclusions = window.comradeQualityExclusions || [];
const allExclusions = [...window.ComradeOptions.excludedTemplates, ...userExclusions];
const exclusionRegexString = allExclusions.map(t => t.replace("Template:", "").replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
const exclusionRegex = new RegExp(exclusionRegexString, 'i');
if (isRedirect || exclusionRegex.test(wikitext)) {
console.log("Comrade check skipped: Page is a redirect or excluded via options.");
return;
}
ComradeLib.checkQualityConsistency();
} catch (err) {
console.error("Comrade error:", err);
}
}
init();
})();
(function() {
'use strict';
if (mw.config.get('wgCanonicalSpecialPageName') !== 'Search') {
return;
}
mw.loader.using(['mediawiki.util']).then(function() {
$(function() {
const $resultsInfo = $('.results-info');
const totalResultsRaw = $resultsInfo.attr('data-mw-num-results-total') || '0';
function formatWithCommas(numberString) {
return numberString.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
const state = {
language: mw.config.get('wgContentLanguage'),
totalResultsFormatted: formatWithCommas(totalResultsRaw)
};
let titlesCache = null;
function extractResultTitles() {
const currentResults = $('.mw-search-result-heading');
if (!titlesCache || titlesCache.length !== currentResults.length) {
titlesCache = currentResults.map(function() {
return $(this).find('a:first').attr('title');
}).get().filter(Boolean);
}
return titlesCache;
}
mw.hook("wikipage.content").add(function() {
titlesCache = null;
});
function addLimitOptions() {
const selects = document.querySelectorAll('.cdx-table-pager__limit-form select[name="limit"]');
if (!selects.length) return;
const valuesToAdd = ["1000", "2500", "5000"];
selects.forEach(function(select) {
valuesToAdd.forEach(function(value) {
if (select.querySelector('option[value="' + value + '"]')) return;
const option = document.createElement("option");
option.value = value;
option.textContent = formatWithCommas(value) + " items";
const existingOptions = Array.from(select.querySelectorAll("option"));
const insertBefore = existingOptions.find(function(opt) {
return Number(opt.value) > Number(value);
});
if (insertBefore) {
select.insertBefore(option, insertBefore);
} else {
select.appendChild(option);
}
});
});
}
mw.hook("wikipage.content").add(addLimitOptions);
const numLinks = $('.mw-numlink');
if (numLinks.length > 4 && !numLinks.eq(4).next().is('a[href*="limit=5000"]')) {
const $firstAnchor = numLinks.eq(4);
const $firstSpan = $('<span> | </span>').insertAfter($firstAnchor);
const url1 = new URL(window.location.href);
url1.searchParams.set('limit', '5000');
$('<a>', {
text: '5000',
title: 'Show 5000 results per page',
href: url1.toString()
}).insertAfter($firstAnchor).before($firstSpan);
}
if (numLinks.length > 9 && !numLinks.eq(9).next().is('a[href*="limit=5000"]')) {
const $secondAnchor = numLinks.eq(9);
const $secondSpan = $('<span> | </span>').insertAfter($secondAnchor);
const url2 = new URL(window.location.href);
url2.searchParams.set('limit', '5000');
$('<a>', {
text: '5000',
title: 'Show 5000 results per page',
href: url2.toString()
}).insertAfter($secondAnchor).before($secondSpan);
}
const $container = $("<div></div>").addClass("enhanced-search-container").hover(function() {
$(this).addClass("hover");
}, function() {
$(this).removeClass("hover");
});
const interwikiResults = document.getElementById("mw-interwiki-results");
if (interwikiResults) {
$container.addClass("enhanced-search-floating").insertBefore("#mw-interwiki-results");
} else {
$container.addClass("enhanced-search-absolute").insertBefore(".searchresults");
}
const totalFormattedStr = state.totalResultsFormatted;
const $titleElement = $("<div>Show a shortened list of<br />this page's " + $('.mw-search-result-heading').length + " results <small>(of total of " + totalFormattedStr + ")</small></div>").addClass("enhanced-header").appendTo($container);
const $contentArea = $("<div></div>").addClass("enhanced-content-area").appendTo($container).hide();
const $note1 = $("<div>Copy this page\'s " + $('.mw-search-result-heading').length + " results to the clipboard as...</div>").addClass("enhanced-note").insertAfter($container).hide();
function setupHoverTooltip(element, message) {
$(element).attr('title', message).hover(function() {
$(this).addClass("hover");
}, function() {
$(this).removeClass("hover");
});
}
const $note2 = $("<div>...plain text<sup> (?)</sup></div>").addClass("enhanced-btn").insertAfter($note1).hide();
setupHoverTooltip($note2, 'This button copies this page\'s results to the clipboard as plain text, i.e. without wikitext markup. Since this copies only the results shown on this search page, make sure you have clicked the "view 20, 50, 100, 250, 500, 5000" link. This format is compatible with PetScan and AutoWikiBrowser (AWB) for example.');
const $note3 = $("<div>...wikitext<sup> (?)</sup></div>").addClass("enhanced-btn").insertAfter($note2).hide();
setupHoverTooltip($note3, 'This button copies this page\'s results to the clipboard and also adds wikitext markup.');
const $note4 = $("<div>...PetScan URL<sup> (?)</sup></div>").addClass("enhanced-btn").insertAfter($note3).hide();
setupHoverTooltip($note4, 'This button copies a PetScan URL with this page\'s results to the clipboard. Note that if the PetScan URL becomes too long (~250+ pages), the URL might not work.');
const $note5 = $("<div>...or this search query's total results of " + totalFormattedStr + "<sup> (?)</sup> as...</div>").addClass("enhanced-note").insertAfter($note4).hide().attr('title', 'Note that the actual limit is 10,000, which is MediaWiki\'s maximum limit.').hover(function() {
$(this).addClass("hover");
}, function() {
$(this).removeClass("hover");
});
const $note6 = $("<div>...PetScan URL<sup> (?)</sup></div>").addClass("enhanced-btn").insertAfter($note5).hide();
setupHoverTooltip($note6, 'This button copies a PetScan URL with all ' + totalFormattedStr + ' results to the clipboard. Note that the maximum limit is 10,000 due to MediaWiki\'s restriction.');
const $note7 = $("<div><sup>(?)</sup> = hover over for tooltip details</div>").addClass("enhanced-note-right").insertAfter($note6).hide();
const noteElements = [$note1, $note2, $note3, $note4, $note5, $note6, $note7];
if (interwikiResults !== null) {
noteElements.forEach($el => $el.addClass("enhanced-floating-elements"));
} else {
noteElements.forEach(function($el, index) {
$el.addClass("enhanced-absolute-elements enhanced-position-" + index);
});
}
function formatPlain() {
return extractResultTitles().join("\r\n");
}
function formatWikitext() {
const titles = extractResultTitles().map(title => "* [[" + title + "]]");
const header = "This is a list of [" + window.location.href + " these search results] (made with the script [[User:JAAqqO/EnhancedSearchPage|EnhancedSearchPage]]):\r\n";
return header + titles.join("\r\n");
}
function formatPetScan() {
const encodedTitles = extractResultTitles().map(encodeURIComponent).join("%0A");
return "https://petscan.wmflabs.org/?active_tab=tab_other_sources&manual_list=" + encodedTitles + "&manual_list_wiki=" + state.language + "wiki&project=wikipedia&language=" + state.language + "&doit=";
}
function formatPetScanTotal() {
const oouiInput = document.getElementById("ooui-php-1");
const value = oouiInput ? oouiInput.value : "";
if (value) {
return "https://petscan.wmflabs.org/?active_tab=tab_other_sources&ns%5B0%5D=1&search_query=" + encodeURIComponent(value) + "&search_wiki=" + state.language + "wiki&search_max_results=10000&project=wikipedia&language=" + state.language + "&doit=";
}
return "";
}
function copyToClipboard(text) {
if (!text) return;
if (!navigator.clipboard) {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.setAttribute('readonly', '');
textarea.style.position = 'absolute';
textarea.style.left = '-9999px';
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
} catch (e) {
console.error('Fallback copy failed', e);
}
textarea.remove();
} else {
navigator.clipboard.writeText(text).catch(function(err) {
console.error("Copy failed: ", err);
});
}
}
function showCopiedMessage(event) {
const posX = event.pageX;
const posY = event.pageY;
const $msg = $("<div class='copied-message'>Copied!</div>").appendTo("body");
$msg.css({
position: "absolute",
top: (posY + 10) + "px",
left: (posX + 10) + "px",
backgroundColor: "#f0f0f0",
border: "1px solid grey",
padding: "5px 10px",
borderRadius: "5px",
zIndex: 9999
});
setTimeout(function() {
$msg.fadeOut("slow", function() {
$(this).remove();
});
}, 500);
}
function performButtonAnimation($element) {
$element.css("transform", "scaleY(0.9)").animate({
opacity: '0.5'
}, "fast", function() {
$(this).animate({
opacity: '1'
}, "slow").css("transform", "scaleY(1)");
});
}
let isExpanded = false;
function showContentArea() {
if (isExpanded) return;
isExpanded = true;
const currentResults = $('.mw-search-result-heading');
$contentArea.empty().append(currentResults.clone());
$contentArea.slideDown();
$(".searchalttitle").remove();
$container.addClass("enhanced-active-container");
$titleElement.remove();
noteElements.forEach($el => $el.show());
}
function bindCopyAction($element, formatterFn) {
$element.click(function(event) {
copyToClipboard(formatterFn());
showCopiedMessage(event);
performButtonAnimation($(this));
});
}
$container.click(showContentArea);
bindCopyAction($note2, formatPlain);
bindCopyAction($note3, formatWikitext);
bindCopyAction($note4, formatPetScan);
bindCopyAction($note6, formatPetScanTotal);
const $pageDown = $("<div><br /><br />[ ↓ ]</div>").addClass("enhanced-scroll-btn").hover(function() {
$(this).addClass("hover");
$(this).attr('title', 'Go to the bottom of the page');
}, function() {
$(this).removeClass("hover");
}).insertBefore('.mw-search-result:eq(0)');
const $pageUp = $("<div>[ ↑ ]</div>").addClass("enhanced-scroll-btn").hover(function() {
$(this).addClass("hover");
$(this).attr('title', 'Go to the top of the page');
}, function() {
$(this).removeClass("hover");
}).insertAfter('.mw-search-result:eq(-1)');
$pageDown.click(function(event) {
event.preventDefault();
$('html, body').animate({
scrollTop: $(document).height()
}, 500);
});
$pageUp.click(function() {
$('html, body').animate({
scrollTop: 0
}, 500);
});
$("<span> (<b>" + $('.mw-search-result-heading').length + "</b> shown on the page)</span>").appendTo('.results-info');
const searchDidYouMean = document.querySelector('.searchdidyoumean');
if (searchDidYouMean) {
searchDidYouMean.style.color = '#b32424';
searchDidYouMean.style.fontSize = '20px';
}
});
});
})();
(function() {
if (window.Headmaster) return;
const VERSION = "0.9.2.1";
const defaultNS = [0, 118];
const userNS = window.headmaster_ns || [];
const config = {
menu: window.headmaster_menu || 'p-cactions',
enableSummary: window.headmaster_do_summary !== false,
customSummary: window.headmaster_summary || "",
allowedNamespaces: Array.from(new Set([...defaultNS, ...userNS])),
autoScan: window.headmaster_auto_scan !== false,
autoRun: window.headmaster_auto_run === false
};
const Headmaster = {
VERSION: VERSION,
DefaultSummary: "Converting [[MOS:PSEUDOHEAD|pseudo-headings]] {details}using [[User:Sam_Sailor/Scripts/Headmaster.js|Headmaster]]. Semicolon markup is reserved for [[MOS:DLIST|description lists]]",
styleInjected: false,
css: `
#hm-panel { clear: both; border: 1px solid #a2a9b1; padding: 15px; background: #f8f9fa; margin-bottom: 1.5em; border-radius: 2px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
#hm-table { width: 100%; border-collapse: collapse; background: white; margin-top: 10px; }
#hm-table th { background: #f2f2f2; border: 1px solid #a2a9b1; padding: 8px; text-align: left; }
#hm-table td { border: 1px solid #a2a9b1; padding: 8px; vertical-align: middle; }
.hm-context { font-family: 'Courier New', monospace; font-size: 0.9em; color: #54595d; background: #fdfdfd; }
#hm-controls { display: flex; justify-content: space-between; align-items: center; margin-top: 15px; }
#hm-controls button { margin-left: 10px; padding: 6px 16px; cursor: pointer; border-radius: 2px; }
.hm-btn-apply { background: #36c; color: white; border: 1px solid #36c; font-weight: bold; }
.hm-btn-apply:hover { background: #447ff5; }
.hm-global-selector { font-size: 0.9em; color: #202122; background: #eaecf0; padding: 8px; border-radius: 2px; border: 1px solid #a2a9b1; }
.hm-global-selector a { font-weight: bold; text-decoration: none; color: #36c; }
.hm-global-selector a:hover { text-decoration: underline; }
.hm-locate-btn { cursor: pointer; color: #36c; border: 1px solid #a2a9b1; background: #f8f9fa; padding: 2px 6px; font-size: 0.85em; border-radius: 2px; }
.hm-locate-btn:hover { background: #fff; border-color: #36c; }
`,
setup() {
if (!config.allowedNamespaces.includes(mw.config.get("wgNamespaceNumber"))) return;
const action = mw.config.get("wgAction");
this.addPortlet(action);
if (action === 'view' && (config.autoScan || config.autoRun)) {
this.silentScan();
}
if (mw.util.getParamValue('headmaster') === '1' && ['edit', 'submit'].includes(action)) {
const params = new URLSearchParams(window.location.search);
params.delete('headmaster');
const searchString = params.toString();
const newUrl = window.location.pathname + (searchString ? '?' + searchString : '');
window.history.replaceState(null, '', newUrl);
this.init();
}
},
addPortlet(action) {
const link = mw.util.addPortletLink(config.menu, '#', 'Headmaster', 'ca-hm', 'Interactively convert pseudo-headings');
if (link) {
$(link).on('click', (e) => {
e.preventDefault();
if (action === 'view') {
this.redirectToEdit();
} else if (['edit', 'submit'].includes(action)) {
this.init();
}
});
}
},
redirectToEdit() {
window.location.href = mw.util.getUrl(mw.config.get("wgPageName"), {
action: 'edit',
headmaster: '1'
});
},
silentScan() {
new mw.Api().get({
action: 'query',
prop: 'revisions',
titles: mw.config.get('wgPageName'),
rvprop: 'content',
rvlimit: 1,
formatversion: 2
}).done((data) => {
const page = data.query.pages[0];
if (!page || !page.revisions) return;
const wikitext = page.revisions[0].content;
const tasks = this.analyzeText(wikitext);
if (tasks.length > 0) {
if (config.autoRun) {
this.redirectToEdit();
} else {
this.notifyDiscovery(tasks.length);
}
}
}).fail((err) => {
mw.log.warn("Headmaster: silentScan failed", err);
});
},
notifyDiscovery(count) {
const $msg = $('<span>').text(`Headmaster: Found ${count} issues. `).append($('<a>').text('Run conversion').css({
fontWeight: 'bold',
cursor: 'pointer'
}).on('click', () => this.redirectToEdit()));
mw.notify($msg, {
tag: 'hm-discovery',
autoHide: false
});
},
init() {
if (!this.styleInjected) {
mw.loader.addStyleTag(this.css);
this.styleInjected = true;
}
const $textbox = $('#wpTextbox1');
const wikitext = $textbox.textSelection('getContents');
if (!wikitext) {
mw.notify("Textbox is empty or not found.", {
type: 'error'
});
return;
}
const tasks = this.analyzeText(wikitext);
if (tasks.length === 0) {
mw.notify("No issues found.");
return;
}
this.showUI(tasks, wikitext);
},
analyzeText(wikitext) {
const lines = wikitext.split("\n");
const tasks = [];
let currentDepth = 1;
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
const headingMatch = line.match(/^(={2,6})\s*(.+?)\s*\1\s*$/);
if (headingMatch) {
currentDepth = headingMatch[1].length;
const prevLine = i > 0 ? lines[i - 1].trim() : null;
const isSmashed = prevLine !== null && prevLine !== "";
const isExtraSpace = i > 1 && lines[i - 1].trim() === "" && lines[i - 2].trim() === "";
if (isSmashed || isExtraSpace) {
tasks.push({
index: i,
type: 'WHITESPACE',
original: lines[i],
isSmashed: isSmashed,
isExtraSpace: isExtraSpace
});
}
} else if (line.startsWith(';') && !line.startsWith(';;')) {
const nextLine = lines[i + 1] || "";
if (!nextLine.trim().startsWith(':')) {
const cleanText = line.substring(1).replace(/\s*:.*$/, "").trim();
const suggestedLevel = "=".repeat(Math.min(currentDepth + 1, 6));
tasks.push({
index: i,
type: 'PSEUDO',
original: line,
cleanText: cleanText,
context: nextLine.substring(0, 60).trim() + (nextLine.length > 60 ? "..." : ""),
suggestedLevel: suggestedLevel
});
}
}
}
return tasks;
},
showUI(tasks, originalWikitext) {
$('#hm-panel').remove();
$('#editform').hide();
const visibleTasks = tasks.filter(t => t.type === 'PSEUDO');
const pseudoCount = visibleTasks.length;
const whitespaceCount = tasks.filter(t => t.type === 'WHITESPACE').length;
let statusMsg = "";
const s = (n) => n !== 1 ? 's' : '';
if (pseudoCount > 0) {
statusMsg = `Found <strong>${pseudoCount}</strong> possible pseudo-heading${s(pseudoCount)}.`;
if (whitespaceCount > 0) {
statusMsg += ` Also fixing <strong>${whitespaceCount}</strong> spacing issue${s(whitespaceCount)} quietly.`;
}
} else {
statusMsg = `Did not find any pseudo-headings, but fixing <strong>${whitespaceCount}</strong> spacing issue${s(whitespaceCount)} quietly.`;
}
const tableRows = pseudoCount > 0 ? visibleTasks.map((task, i) => `
<tr data-task-index="${task.index}">
<td><button class="hm-locate-btn" data-hm-i="${tasks.indexOf(task)}" title="Show in editor">Locate</button></td>
<td class="hm-context"><code>${mw.html.escape(task.original)}</code></td>
<td><label><input type="radio" name="hm-opt-${i}" value="keep"> Keep</label></td>
<td><label><input type="radio" name="hm-opt-${i}" value="bold" checked> <b>Bold</b></label></td>
<td><label><input type="radio" name="hm-opt-${i}" value="head"> Heading (${task.suggestedLevel.length})</label></td>
<td class="hm-context">${mw.html.escape(task.context || "(end of page)")}</td>
</tr>
`).join("") : `<tr><td colspan="6" style="text-align:center; padding: 2em; color: #54595d; background: #f8f9fa;">
No interactive heading changes detected. Only background spacing normalization will be applied.
</td></tr>`;
const $panel = $(`
<div id="hm-panel">
<h3 style="margin-top:0">Headmaster v${this.VERSION}: Review pseudo-headings</h3>
<div class="hm-global-selector" ${pseudoCount === 0 ? 'style="display:none"' : ''}>
<strong>Bulk action:</strong>
<a href="#" id="hm-all-keep">Set all to Keep</a> |
<a href="#" id="hm-all-bold">Set all to Bold</a> |
<a href="#" id="hm-all-head">Set all to Heading</a>
</div>
<table id="hm-table">
<thead>
<tr>
<th>Find</th>
<th>Source</th>
<th colspan="3">Action</th>
<th>Next line context</th>
</tr>
</thead>
<tbody>${tableRows}</tbody>
</table>
<div id="hm-controls">
<span>${statusMsg}</span>
<div>
<button id="hm-cancel" class="mw-ui-button">Cancel</button>
<button id="hm-apply" class="hm-btn-apply">Show changes (diff)</button>
</div>
</div>
</div>
`);
$('#content').prepend($panel);
window.scrollTo(0, 0);
$('.hm-locate-btn').on('click', function() {
$(document).off('mousedown.hmlocate');
const idx = $(this).data('hm-i');
const task = tasks[idx];
const lines = originalWikitext.split('\n');
let charOffset = 0;
for (let j = 0; j < task.index; j++) {
charOffset += lines[j].length + 1;
}
$('#hm-panel').hide();
$('#editform').show();
const $textbox = $('#wpTextbox1');
$textbox.textSelection('setSelection', {
start: charOffset,
end: charOffset + lines[task.index].length
});
$textbox.textSelection('scrollToCaretPosition');
const locateNotify = mw.notify("Locating line " + (task.index + 1) + ". Click outside the editor to return.", {
tag: 'hm-locate',
type: 'success',
autoHide: false
});
setTimeout(() => {
$(document).on('mousedown.hmlocate', function(e) {
if (!$(e.target).closest('#editform, .mw-notification-area').length) {
$(document).off('mousedown.hmlocate');
Promise.resolve(locateNotify).then(n => n.close());
$('#editform').hide();
$('#hm-panel').show();
}
});
}, 200);
});
$('#hm-all-keep').on('click', (e) => {
e.preventDefault();
$('input[value="keep"]').prop('checked', true);
});
$('#hm-all-bold').on('click', (e) => {
e.preventDefault();
$('input[value="bold"]').prop('checked', true);
});
$('#hm-all-head').on('click', (e) => {
e.preventDefault();
$('input[value="head"]').prop('checked', true);
});
$('#hm-cancel').on('click', () => {
$('#hm-panel').remove();
$('#editform').show();
});
$('#hm-apply').on('click', () => this.applyChanges(tasks, originalWikitext));
},
getProcessedSummary(existingSummary, bolds, heads, spaces) {
if (!config.enableSummary) return existingSummary;
let mySummary = "";
let isDefault = false;
if (config.customSummary) {
mySummary = config.customSummary;
} else {
isDefault = true;
if (bolds > 0 || heads > 0) {
const detailsArr = [];
if (bolds > 0) detailsArr.push(`${bolds} to bold`);
if (heads > 0) detailsArr.push(`${heads} to heading`);
const detailsStr = detailsArr.length > 0 ? `(${detailsArr.join(', ')}) ` : "";
mySummary = this.DefaultSummary.replace("{details}", detailsStr);
if (spaces > 0) {
mySummary += "; also handled [[MOS:BLANKLINE]]";
}
} else if (spaces > 0) {
mySummary = "Handling [[MOS:BLANKLINE]] using [[User:Sam_Sailor/Scripts/Headmaster.js|Headmaster]]";
}
}
if (!mySummary) return existingSummary;
const trimmedOld = existingSummary.trim();
if (!trimmedOld) return mySummary;
if (/[.!?]$/.test(trimmedOld)) {
return trimmedOld + " " + mySummary;
} else {
const finalSummary = isDefault ? mySummary.charAt(0).toLowerCase() + mySummary.slice(1) : mySummary;
return trimmedOld + "; " + finalSummary;
}
},
applyChanges(tasks, wikitext) {
try {
let lines = wikitext.split("\n");
let bCount = 0,
hCount = 0,
wCount = 0;
const pseudoTasks = tasks.filter(t => t.type === 'PSEUDO');
for (let i = pseudoTasks.length - 1; i >= 0; i--) {
const task = pseudoTasks[i];
const idx = task.index;
const action = $('#hm-table tbody tr').filter(`[data-task-index="${idx}"]`).find('input:checked').val();
if (action === 'bold') {
let newValue = `'''${task.cleanText}'''`;
if (idx > 0 && lines[idx - 1].trim() !== "") {
newValue = "\n" + newValue;
}
lines[idx] = newValue;
bCount++;
} else if (action === 'head') {
const h = task.suggestedLevel;
let newValue = `${h} ${task.cleanText} ${h}`;
if (idx > 0 && lines[idx - 1].trim() !== "") {
newValue = "\n" + newValue;
}
lines[idx] = newValue;
hCount++;
}
}
const spaceTasks = tasks.filter(t => t.type === 'WHITESPACE');
for (let i = spaceTasks.length - 1; i >= 0; i--) {
const task = spaceTasks[i];
const idx = task.index;
if (task.isSmashed) {
lines.splice(idx, 0, "");
} else if (task.isExtraSpace) {
let backIdx = idx - 1;
while (backIdx > 0 && lines[backIdx].trim() === "" && lines[backIdx - 1].trim() === "") {
lines.splice(backIdx, 1);
backIdx--;
}
}
wCount++;
}
if (bCount + hCount + wCount === 0) {
mw.notify("No changes selected.");
$('#hm-panel').remove();
$('#editform').show();
return;
}
$('#wpTextbox1').textSelection('setContents', lines.join("\n"));
const $summary = $('#wpSummary');
$summary.val(this.getProcessedSummary($summary.val(), bCount, hCount, wCount));
$('#hm-panel').remove();
$('#editform').show();
mw.notify(`Headmaster: Fixed ${wCount} spacing issue(s) and converted ${bCount + hCount} heading(s).`);
$('html, body').animate({
scrollTop: 0
}, 'fast');
$('#wpDiff').click();
} catch (err) {
$('#editform').show();
mw.notify("Headmaster error: " + err.message, {
type: 'error'
});
}
}
};
window.Headmaster = Headmaster;
mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => {
$(() => Headmaster.setup());
});
})();
//</nowiki>
lawgfliqfsjbyxg3pmpjb2zj0dsnfee
749672
749670
2026-07-03T16:45:42Z
Sam Sailor
26820
Test
749672
javascript
text/javascript
//<nowiki>
(function(mw, $, OO) {
'use strict';
const IGNORE_SUFFIXES = ['Sr.', 'Sr', 'Jr.', 'Jr', 'II', 'III', 'IV', 'V', 'MD', 'PhD'];
const LANG_PREFS = ['frwiki', 'dewiki', 'eswiki', 'itwiki', 'nlwiki', 'svwiki'];
const NON_LANG_WIKIS = ['commonswiki', 'specieswiki', 'metawiki', 'mediawikiwiki', 'wikidatawiki', 'wikimaniawiki'];
const suffixRE = new RegExp('\\s(' + IGNORE_SUFFIXES.join('|').replace(/\./g, '\\.') + ')$', 'i');
function GetterDialog(config) {
GetterDialog.super.call(this, config);
this.abortController = null;
}
OO.inheritClass(GetterDialog, OO.ui.ProcessDialog);
GetterDialog.static.name = 'getterDialog';
GetterDialog.static.title = 'Getter: Wikipedia List Builder';
GetterDialog.static.actions = [
{ action: 'generate', label: 'Generate List', flags: ['primary', 'progressive'], modes: 'edit' },
{ action: 'back', label: 'Go back', modes: 'result' },
{ action: 'exit', label: 'Exit', flags: 'safe', modes: ['edit', 'result'] }
];
GetterDialog.prototype.cleanTitle = function(t) {
return t.replace(suffixRE, '').replace(/\s?\([^)]*\)$/, '').trim();
};
GetterDialog.prototype.normalizeTitle = function(t) {
return t.replace(/_/g, ' ').trim().replace(/^(.)/, c => c.toUpperCase());
};
GetterDialog.prototype.getSortKey = function(title) {
const clean = this.cleanTitle(title);
const parts = clean.split(' ');
return (parts.length <= 1) ? title.toLowerCase() : (parts[parts.length - 1] + ', ' + parts.slice(0, parts.length - 1).join(' ') + ' | ' + title).toLowerCase();
};
GetterDialog.prototype.formatResult = function(p, mode, includeIll) {
if (mode === 'anbl') {
return `* {{anbl|${p.title}}}`;
}
const bY = p.ent?.claims?.P569?.[0]?.mainsnak?.datavalue?.value?.time?.substring(1, 5);
const dY = p.ent?.claims?.P570?.[0]?.mainsnak?.datavalue?.value?.time?.substring(1, 5);
const bio = bY ? ` (${bY}${dY ? '–' + dY : ''})` : '';
const rawDesc = (p.desc || '').replace(/\s?\(\d{4}(–\d{4})?\)$/i, '').trim();
const suffix = rawDesc ? `, ${rawDesc}` : '';
let display = `[[${p.title}]]`;
if (includeIll) {
const langKey = LANG_PREFS.find(l => p.ent.sitelinks?.[l]) ||
Object.keys(p.ent.sitelinks || {}).find(
k => k.endsWith('wiki') &&
k !== 'enwiki' &&
!NON_LANG_WIKIS.includes(k)
);
if (langKey) {
display = `{{ill|${p.title}|${langKey.replace('wiki', '')}|${p.ent.sitelinks[langKey].title}}}`;
}
}
return `* ${display}${bio}${suffix}`;
};
GetterDialog.prototype.matchesCriteria = function(title, compiled) {
const clean = this.cleanTitle(title);
const tokens = clean.split(/\s+/);
const given = tokens[0] || '';
const surname = tokens.length > 1 ? tokens[tokens.length - 1] : '';
const middleLC = tokens.length > 2 ? tokens.slice(1, -1).map(x => x.toLowerCase()) : [];
const check = (val, regexes) => {
if (!regexes || regexes.length === 0) return true;
return regexes.some(re => re.test(val));
};
return check(given, compiled.given) &&
check(surname, compiled.surname) &&
(compiled.middle.length === 0 || compiled.middle.every(m => middleLC.includes(m)));
};
GetterDialog.prototype.getExistingLinks = async function() {
const titles = new Set();
try {
const data = await this.api.get({ action: 'parse', page: mw.config.get('wgPageName'), prop: 'wikitext', format: 'json' });
const text = data.parse?.wikitext?.['*'] || '';
const regexes = [/\[\[([^\]|#]+)/g, /\{\{(?:anbl|Annotated biography link)\|([^}|]+)/gi];
regexes.forEach(re => { let m; while ((m = re.exec(text)) !== null) titles.add(this.normalizeTitle(m[1])); });
} catch (e) {
console.error('[Getter] Parse error:', e);
}
return titles;
};
async function checkEntitiesData(qids, signal) {
if (!qids.length) return {};
let all = {};
for (let i = 0; i < qids.length; i += 50) {
if (signal && signal.aborted) throw new DOMException('Aborted', 'AbortError');
try {
const res = await fetch('https://www.wikidata.org/w/api.php?action=wbgetentities&ids=' + qids.slice(i, i + 50).join('|') + '&props=claims|labels|descriptions|sitelinks&languages=en&format=json&origin=*', { signal }).then(r => r.json());
Object.assign(all, res.entities || {});
} catch (e) {
if (e.name === 'AbortError') throw e;
console.error('[Getter] Wikidata batch error', e);
}
}
return all;
}
GetterDialog.prototype.autoPopulateFields = async function () {
const title = mw.config.get('wgTitle');
const cleanTitle = this.cleanTitle(title);
const m = title.match(/^(.*?) \((given name|surname)\)$/i);
if (m) {
if (m[2].toLowerCase() === 'given name') {
this.givenTags.addTag(m[1]);
} else {
this.surnameTags.addTag(m[1]);
}
return;
}
try {
const data = await this.api.get({
action: 'parse',
page: mw.config.get('wgPageName'),
prop: 'wikitext',
format: 'json'
});
const text = data.parse?.wikitext?.['*'] || '';
const hasSurnameTemplate = /\{\{\s*surname\s*(?:\||\}\})/i.test(text);
const givenNameTemplate = text.match(/\{\{\s*given name\b[^}]*\}\}/i)?.[0] || '';
const hasGivenNameTemplate = givenNameTemplate !== '';
const isBoth = /\|\s*type\s*=\s*both\b/i.test(givenNameTemplate);
if (hasSurnameTemplate || (hasGivenNameTemplate && isBoth)) {
this.surnameTags.addTag(cleanTitle);
}
if (hasGivenNameTemplate) {
this.givenTags.addTag(cleanTitle);
}
if (!hasSurnameTemplate && !hasGivenNameTemplate) {
this.givenTags.addTag(cleanTitle);
}
} catch (e) {
console.error('[Getter] Could not inspect page.', e);
this.givenTags.addTag(cleanTitle);
}
};
GetterDialog.prototype.initialize = function() {
GetterDialog.super.prototype.initialize.apply(this, arguments);
this.api = new mw.Api();
this.panel = new OO.ui.PanelLayout({ padded: true, expanded: false });
this.givenTags = new OO.ui.TagMultiselectWidget({ placeholder: 'Given names...', allowArbitrary: true });
this.middleTags = new OO.ui.TagMultiselectWidget({ placeholder: 'Middle terms...', allowArbitrary: true });
this.surnameTags = new OO.ui.TagMultiselectWidget({ placeholder: 'Surnames...', allowArbitrary: true });
this.excludeTags = new OO.ui.TagMultiselectWidget({ placeholder: 'Exclude strings...', allowArbitrary: true });
this.humanOnly = new OO.ui.CheckboxInputWidget({ selected: true });
this.excludeHumans = new OO.ui.CheckboxInputWidget({ selected: false });
this.includeIll = new OO.ui.CheckboxInputWidget({ selected: false });
this.sortBySurname = new OO.ui.CheckboxInputWidget({ selected: true });
this.updateMode = new OO.ui.CheckboxInputWidget({ selected: false });
this.excludeHumans.on('change', (selected) => {
if (selected) { this.humanOnly.setSelected(false); this.sortBySurname.setSelected(false); }
});
this.humanOnly.on('change', (selected) => {
if (selected) { this.excludeHumans.setSelected(false); }
});
this.formatSelect = new OO.ui.ButtonSelectWidget({ items: [
new OO.ui.ButtonOptionWidget({ data: 'wiki', label: 'Wiki' }),
new OO.ui.ButtonOptionWidget({ data: 'anbl', label: 'ANBL' })
]});
this.formatSelect.selectItemByData('wiki');
this.fieldset = new OO.ui.FieldsetLayout({ label: 'Advanced Filter Settings' });
this.fieldset.addItems([
new OO.ui.FieldLayout(this.givenTags, { label: 'Given names', align: 'top' }),
new OO.ui.FieldLayout(this.middleTags, { label: 'Middle terms', align: 'top' }),
new OO.ui.FieldLayout(this.surnameTags, { label: 'Surnames', align: 'top' }),
new OO.ui.FieldLayout(this.excludeTags, { label: 'Exclude strings', align: 'top' }),
new OO.ui.FieldLayout(this.humanOnly, { label: 'Humans only', align: 'inline' }),
new OO.ui.FieldLayout(this.excludeHumans, { label: 'Exclude humans', align: 'inline' }),
new OO.ui.FieldLayout(this.sortBySurname, { label: 'Sort by surname', align: 'inline' }),
new OO.ui.FieldLayout(this.updateMode, { label: 'Exclude existing links', align: 'inline' }),
new OO.ui.FieldLayout(this.includeIll, { label: 'Include {{ill}}', align: 'inline' }),
new OO.ui.FieldLayout(this.formatSelect, { label: 'Format', align: 'top' })
]);
this.resultArea = new OO.ui.MultilineTextInputWidget({ rows: 15, autosize: true });
this.copyButton = new OO.ui.ButtonWidget({ label: 'Copy list', icon: 'copy', flags: ['progressive'] });
this.copyButton.$element.css('marginTop', '10px');
this.copyButton.on('click', () => {
navigator.clipboard.writeText(this.resultArea.getValue())
.then(() => mw.notify('Copied!'))
.catch(() => mw.notify('Copy failed.', { type: 'error' }));
});
this.resultContainer = new OO.ui.PanelLayout({ padded: true, expanded: false });
this.resultContainer.$element.append(new OO.ui.FieldLayout(this.resultArea, { label: 'Results', align: 'top' }).$element, this.copyButton.$element).hide();
this.panel.$element.append(this.fieldset.$element, this.resultContainer.$element);
this.$body.append(this.panel.$element);
this.actions.setMode('edit');
this.autoPopulateFields();
};
GetterDialog.prototype.runGetter = async function() {
this.abortController = new AbortController();
const signal = this.abortController.signal;
try {
const { humanOnly, excludeHumans, includeIll, sortBySurname, formatSelect, updateMode } = this;
const mode = formatSelect.findSelectedItem().getData();
const existing = updateMode.isSelected() ? await this.getExistingLinks() : new Set();
const resultsMap = new Map();
const searchTerms = [...new Set([...this.givenTags.getValue(), ...this.middleTags.getValue(), ...this.surnameTags.getValue()].map(t => t.trim()).filter(Boolean))];
const excludeList = this.excludeTags.getValue().map(e => e.toLowerCase());
const compile = (vals) => vals.map(r => new RegExp('^' + r.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*') + '$', 'i'));
const compiledCriteria = {
given: compile(this.givenTags.getValue()),
surname: compile(this.surnameTags.getValue()),
middle: this.middleTags.getValue().map(m => m.toLowerCase())
};
if (searchTerms.length === 0) { mw.notify('Please enter search terms.', { type: 'error' }); return; }
for (const term of searchTerms) {
if (signal.aborted) return;
let cont = {};
do {
const data = await this.api.get({ action: 'query', list: 'search', srsearch: `intitle:"${term}"`, srlimit: 'max', sroffset: cont.sroffset || 0, format: 'json' });
for (const p of (data.query?.search || [])) {
const titleLC = p.title.toLowerCase();
if (excludeList.some(e => titleLC.includes(e))) continue;
const title = this.normalizeTitle(p.title);
if (!existing.has(title) && this.matchesCriteria(title, compiledCriteria)) {
resultsMap.set(p.title, { title: p.title, pageid: p.pageid });
}
}
cont = data.continue || {};
} while (cont.sroffset && !signal.aborted);
}
const pageIds = Array.from(resultsMap.values()).map(p => p.pageid);
const confirmedResults = new Map();
for (let i = 0; i < pageIds.length; i += 50) {
if (signal.aborted) return;
const chunk = pageIds.slice(i, i + 50);
const qidList = [], qidMap = {};
const pageData = await this.api.get({ action: 'query', prop: 'pageprops|description', pageids: chunk.join('|'), format: 'json' });
Object.values(pageData.query.pages).forEach(p => {
if (p.pageprops?.wikibase_item) {
qidList.push(p.pageprops.wikibase_item);
qidMap[p.pageprops.wikibase_item] = p;
}
});
const entities = await checkEntitiesData(qidList, signal);
for (const qid in entities) {
const ent = entities[qid], p = qidMap[qid];
const isHuman = ent.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5");
if ((humanOnly.isSelected() && !isHuman) || (excludeHumans.isSelected() && isHuman)) continue;
confirmedResults.set(p.title, {
title: p.title,
desc: p.description || ent.descriptions?.en?.value || '',
ent
});
}
}
const out = Array.from(confirmedResults.values())
.map(p => ({
formatted: this.formatResult(p, mode, includeIll.isSelected()),
sortKey: sortBySurname.isSelected()
? this.getSortKey(p.title)
: p.title.toLowerCase()
}))
.sort((a, b) => a.sortKey.localeCompare(b.sortKey, undefined, {
sensitivity: 'base'
}))
.map(r => r.formatted)
.join('\n');
this.resultArea.setValue(out);
this.fieldset.$element.hide();
this.resultContainer.$element.show();
this.actions.setMode('result');
} catch (e) {
if (e.name !== 'AbortError') {
console.error('[Getter] Generator error:', e);
mw.notify('An error occurred while generating the list.', { type: 'error' });
}
}
};
GetterDialog.prototype.getActionProcess = function(action) {
if (action === 'generate') {
return new OO.ui.Process(async () => { await this.runGetter(); });
}
if (action === 'back') {
if (this.abortController) this.abortController.abort();
return new OO.ui.Process(() => {
this.resultContainer.$element.hide();
this.fieldset.$element.show();
this.actions.setMode('edit');
});
}
if (action === 'exit') {
if (this.abortController) this.abortController.abort();
return new OO.ui.Process(() => this.close());
}
return GetterDialog.super.prototype.getActionProcess.call(this, action);
};
mw.loader.using(['mediawiki.api', 'mediawiki.util', 'oojs-ui', 'oojs-ui-widgets', 'oojs-ui-windows']).then(() => {
const wm = new OO.ui.WindowManager();
$(document.body).append(wm.$element);
const dialog = new GetterDialog({ size: 'medium' });
wm.addWindows([dialog]);
$(mw.util.addPortletLink('p-tb', '#', 'Getter', 't-getter')).on('click', (e) => {
e.preventDefault();
wm.openWindow(dialog);
});
});
}(mediaWiki, jQuery, OO));
(function() {
if (window.IllWill) return;
const STRIKEOUT = true;
const HIDE = 'hide';
const IllWill = {
Version: "0.5",
Attribution: 'created by <a target="_blank" href="/wiki/User:Cobaltcigs">cobaltcigs</a>',
Summary: "Added interlanguage links using IllWill.js",
DataApi: "https://www.wikidata.org/w/api.php",
LocalLang: mw.config.get("wgPageContentLanguage"),
Disambiguator: / \(.*\)$/,
SiteLinks: {},
Config: {
MaxLanguages: 10,
IgnoreScientific: STRIKEOUT,
AutoDiff: true
},
css: `
#illwill { clear: both; margin-bottom: 1.5em; border: 1px solid #a2a9b1; padding: 15px; background: #f8f9fa; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.ill-sitelinks a { display: inline-block; margin-bottom: 3px; font-size: 0.9em; color: #36c; }
.ill-size-tag { font-size: 0.8em; color: #72777d; margin-left: 6px; white-space: nowrap; }
.illwill-ignore { text-decoration: line-through; color: #72777d; font-size: 0.85em; }
.ill-x { width: 35px; text-align: center; }
.ill-input, .ill-output { font-family: 'Courier New', monospace; white-space: pre-wrap; font-size: 0.9em; background: #fff; border: 1px solid #eaecf0; padding: 4px; border-radius: 2px; }
#ill-buttons { text-align: right; padding: 10px; border-top: 1px solid #a2a9b1; margin-top: 10px; }
#ill-buttons button { margin-left: 10px; cursor: pointer; padding: 6px 16px; border-radius: 2px; border: 1px solid #a2a9b1; transition: all 0.2s; }
#ill-ok { background: #36c; color: #fff; border-color: #36c !important; font-weight: bold; }
#ill-ok:hover { background: #447ff5; }
#ill-ok:disabled { background: #eaecf0; color: #72777d; border-color: #c8ccd1 !important; cursor: not-allowed; opacity: 0.7; }
#ill-table { width: 100%; border-collapse: collapse; margin-top: 10px; background: white; }
#ill-table th { background: #f2f2f2; text-align: left; padding: 8px; border: 1px solid #a2a9b1; }
#ill-table td { padding: 8px; border: 1px solid #a2a9b1; vertical-align: top; }
#ill-table caption { background: #eaffea; font-weight: bold; padding: 10px; border: 1px solid #a2a9b1; border-bottom: none; font-size: 1.1em; }
#ill-help { font-size: 0.85em; color: #54595d; padding: 10px; line-height: 1.4; }
.ill-loading-inline { font-style: italic; color: #72777d; font-size: 0.9em; display: block; margin: 5px 0; }
`,
setup() {
if (!["edit", "submit"].includes(mw.config.get('wgAction'))) return;
mw.loader.addStyleTag(this.css);
const link = mw.util.addPortletLink('p-cactions', '#', 'IllWill', 'ca-illwill', "Add interlanguage links via Wikidata");
if (link) {
link.addEventListener('click', (e) => {
e.preventDefault();
this.makePanel();
});
}
},
norm(s) {
if (!s) return "";
const t = s.trim().replace(/[\s_]+/g, " ");
return t.charAt(0).toUpperCase() + t.slice(1);
},
async getRedLinks($textbox) {
const txt = $textbox.textSelection('getContents') || "";
const wl = txt.match(/\[\[[^[\]\n]+\]\]/g);
if (!wl) return {
reds: [],
txt
};
const api = new mw.Api();
const textToParse = wl.join("").replace(/\|[^\]]+/g, "");
try {
const data = await api.post({
action: 'parse',
text: textToParse,
contentmodel: 'wikitext',
formatversion: 2
});
const tempDiv = document.createElement('div');
tempDiv.innerHTML = data.parse.text;
const reds = Array.from(tempDiv.querySelectorAll("a.new")).map(x => {
const urlParams = new URLSearchParams(x.href.split('?')[1]);
return urlParams.get('title')?.replace(/_/g, " ");
}).filter(Boolean);
return {
reds: [...new Set(reds)],
txt
};
} catch (e) {
return {
reds: [],
txt
};
}
},
async makePanel() {
const $editform = $('#editform');
const $textbox = $('#wpTextbox1');
$('#illwill').remove();
$editform.hide();
const $wrapper = $('<div id="illwill">Loading redlinks...</div>').insertBefore($editform);
const {
reds,
txt
} = await this.getRedLinks($textbox);
const wtLinks = [];
reds.forEach(title => {
const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\s/g, "[\\s_]+");
const re = new RegExp("\\[\\[\\s*" + escaped + "\\s*(?:\\|[^\\[\\]]+)?\\]\\]", "ig");
const match = txt.match(re);
if (match && !wtLinks.some(z => z.title === title)) {
wtLinks.push({
link: match[0],
title
});
}
});
if (!wtLinks.length) {
const $back = $('<button>Quit</button>').on('click', () => {
$wrapper.remove();
$editform.show();
});
$wrapper.empty().append($('<strong>No valid redlinks found in text.</strong> '), $back);
return;
}
const $table = $(`
<table id="ill-table" class="wikitable">
<caption>IllWill.js (v${this.Version})</caption>
<thead>
<tr>
<th class="ill-x"><input type="checkbox" id="ill-select-all" disabled></th>
<th>Input link</th>
<th>Possible Wikidata matches</th>
<th>Sitelinks (top ${this.Config.MaxLanguages} by size)</th>
<th>Resulting wikitext</th>
</tr>
</thead>
<tbody></tbody>
<tfoot>
<tr>
<td colspan="3" id="ill-help">Carefully examine possible Wikidata matches.</td>
<td colspan="2" id="ill-buttons">
<button id="ill-cancel">Cancel</button>
<button id="ill-ok" disabled>Show changes</button>
</td>
</tr>
</tfoot>
</table>
`);
const $tbody = $table.find('tbody');
wtLinks.forEach((item, i) => {
$tbody.append(`
<tr id="ill-row-${i}" data-index="${i}">
<td class="ill-x"><input type="checkbox" class="ill-row-check" disabled></td>
<td class="ill-input">${this.escapeHTML(item.link)}</td>
<td class="ill-wd-item"><span class="ill-loading-inline">Searching...</span></td>
<td class="ill-sitelinks"></td>
<td class="ill-output"></td>
</tr>
`);
});
$wrapper.empty().append($table);
const chunkSize = 5;
for (let i = 0; i < wtLinks.length; i += chunkSize) {
const chunk = wtLinks.slice(i, i + chunkSize);
await Promise.all(chunk.map((item, idx) => this.fetchWikidataOptimized(item.title, i + idx)));
}
$table.on('click', '#ill-cancel', () => {
$editform.show();
$wrapper.remove();
});
$table.on('click', '#ill-ok', () => this.onOkButton($editform, $wrapper, $textbox));
$table.on('change', '#ill-select-all', (e) => {
const isChecked = $(e.target).is(':checked');
$table.find('.ill-row-check:not(:disabled)').each(function() {
$(this).prop('checked', isChecked).trigger('change');
});
});
$table.on('change', '.ill-row-check', (e) => {
this.onCheckbox(e);
this.updateOkButtonState($table);
});
$table.on('change', 'input[type="radio"]', (e) => {
this.showSiteLinks(e);
});
},
async fetchWikidataOptimized(title, index) {
const query = title.replace(this.Disambiguator, "");
const url = `${this.DataApi}?action=wbsearchentities&search=${encodeURIComponent(query)}&language=${this.LocalLang}&limit=5&format=json&origin=*`;
try {
const resp = await fetch(url);
const data = await resp.json();
const results = data.search;
const $cell = $(`#ill-row-${index} .ill-wd-item`);
if (!results || !results.length) {
$cell.html('<span class="ill-na">(no results)</span>');
return;
}
const entityIds = results.map(r => r.id);
await this.fetchEntities(entityIds, index, $cell);
} catch (e) {
$(`#ill-row-${index} .ill-wd-item`).text("Search failed.");
}
},
async fetchEntities(ids, rowIndex, $cell) {
const url = `${this.DataApi}?action=wbgetentities&ids=${ids.join('|')}&props=labels|descriptions|claims|sitelinks&format=json&origin=*`;
try {
const resp = await fetch(url);
const data = await resp.json();
$cell.empty();
ids.forEach(qNum => {
const entity = data.entities[qNum];
if (!entity) return;
const isScientific = entity.claims?.P31?.some(s => s.mainsnak?.datavalue?.value?.id === "Q13442814");
if (isScientific && this.Config.IgnoreScientific === HIDE) return;
const desc = entity.descriptions?.[this.LocalLang]?.value || entity.descriptions?.en?.value || "";
const label = entity.labels?.[this.LocalLang]?.value || entity.labels?.en?.value || qNum;
const isStrikeout = isScientific && this.Config.IgnoreScientific === STRIKEOUT;
const $div = $(`
<div class="${isStrikeout ? 'illwill-ignore' : ''}">
<input type="radio" name="selection-${rowIndex}" value="${qNum}" id="radio-${qNum}" ${isStrikeout ? 'disabled' : ''}>
<label for="radio-${qNum}">
<a href="https://wikidata.org/wiki/${qNum}" target="_blank">${this.escapeHTML(label)}</a>:
<span class="ill-desc">${desc ? this.escapeHTML(desc) : '<i>no description</i>'}</span>
</label>
</div>
`);
$cell.append($div);
this.SiteLinks[qNum] = entity.sitelinks || {};
});
} catch (e) {
$cell.text("Detail fetch failed.");
}
},
updateOkButtonState($table) {
const $rows = $table.find('.ill-row-check:not(:disabled)');
const $checkedRows = $rows.filter(':checked');
const anyEnabled = $rows.length > 0;
$table.find('#ill-ok').prop('disabled', $checkedRows.length === 0);
const $master = $table.find('#ill-select-all');
$master.prop('disabled', !anyEnabled);
if (anyEnabled && $rows.length === $checkedRows.length) {
$master.prop('checked', true);
} else {
$master.prop('checked', false);
}
},
async showSiteLinks(e) {
const isManualToggling = e.fromCheckbox || false;
const qNum = e.target.value;
const $row = $(e.target).closest('tr');
const sitelinks = this.SiteLinks[qNum];
const $checkbox = $row.find('.ill-row-check');
const $sitelinkCell = $row.find('.ill-sitelinks');
const $outputCell = $row.find('.ill-output');
const $table = $('#ill-table');
const effectiveMax = Math.min(Math.max(this.Config.MaxLanguages, 3), 6);
const validKeys = Object.keys(sitelinks).filter(k => k.endsWith('wiki') && !k.includes('commons'));
$sitelinkCell.html('<span class="ill-loading-inline">Measuring articles...</span>');
$outputCell.empty();
if (!isManualToggling) {
$checkbox.prop('disabled', true).prop('checked', false);
}
const candidates = validKeys.slice(0, 25);
const sizeMap = [];
await Promise.all(candidates.map(async (key) => {
const lang = key.replace('wiki', '').replace(/_/g, '-');
const title = sitelinks[key].title;
const endpoint = `https://${lang}.wikipedia.org/w/api.php?action=query&titles=${encodeURIComponent(title)}&prop=info&format=json&origin=*`;
try {
const res = await fetch(endpoint).then(r => r.json());
const page = Object.values(res.query.pages)[0];
sizeMap.push({
lang,
title,
length: page.length || 0
});
} catch (err) {
sizeMap.push({
lang,
title,
length: 0
});
}
}));
sizeMap.sort((a, b) => b.length - a.length);
const topLinks = sizeMap.slice(0, effectiveMax);
$sitelinkCell.html(topLinks.map(item => {
const kbValue = Math.round(item.length / 1024);
return `<div><a href="https://${item.lang}.wikipedia.org/wiki/${encodeURIComponent(item.title)}" target="_blank">${item.lang}:${this.escapeHTML(item.title)}</a> <span class="ill-size-tag">(${kbValue} kB)</span></div>`;
}).join(""));
const inputWikitext = $row.find('.ill-input').text();
const match = inputWikitext.match(/\[\[(.*?)\]\]/);
if (match) {
const parts = match[1].split('|');
const targetPage = parts[0].trim();
const surfaceName = (parts[1] || parts[0]).trim();
const illLangs = topLinks.map(item => {
const isSameTitle = this.norm(item.title) === this.norm(targetPage);
return `|${item.lang}|${isSameTitle ? '' : item.title}`;
}).join("");
const ltParam = (this.norm(surfaceName) === this.norm(targetPage)) ? "" : `|lt=${surfaceName}`;
$outputCell.text(`{{ill|${targetPage}${ltParam}${illLangs}}}`);
}
$checkbox.prop('disabled', !topLinks.length);
if (!isManualToggling) {
$checkbox.prop('checked', !!topLinks.length);
}
this.updateOkButtonState($table);
},
onCheckbox(e) {
const $row = $(e.target).closest('tr');
const $radio = $row.find('input[type="radio"]:checked');
const isChecked = $(e.target).is(':checked');
if (isChecked) {
if ($radio.length) {
this.showSiteLinks({
target: $radio[0],
fromCheckbox: true
});
}
} else {
$row.find('.ill-sitelinks, .ill-output').empty();
}
},
onOkButton($editform, $wrapper, $textbox) {
let text = $textbox.textSelection('getContents');
let changed = false;
$wrapper.find('.ill-row-check:checked').each(function() {
const $row = $(this).closest('tr');
const input = $row.find('.ill-input').text();
const output = $row.find('.ill-output').text();
if (output) {
const escapedInput = input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
text = text.replace(new RegExp(escapedInput, "gi"), output);
changed = true;
}
});
if (changed) {
$('#wpSummary').val((i, v) => (v ? v + "; " : "") + this.Summary);
$textbox.textSelection('setContents', text);
$wrapper.remove();
$editform.show();
if (this.Config.AutoDiff) $('#wpDiff').click();
}
},
escapeHTML(str) {
return str.replace(/[&<>"']/g, m => ({
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
})[m]);
}
};
window.IllWill = IllWill;
mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => {
$(() => IllWill.setup());
});
})();
/* global ComradeLib, mw, $ */
(async function() {
try {
await mw.loader.getScript('https://test.wikipedia.org/w/index.php?title=User:Sam_Sailor/test.js/comrade-options.js&action=raw&ctype=text/javascript');
} catch (e) {
console.warn("Comrade: Could not load /comrade-options.js, using fallback defaults.", e);
}
window.ComradeOptions = Object.assign({
version: "0.5.0",
excludedTemplates: [],
nudgeTimeout: 10000,
debugMode: false
}, window.ComradeOptions || {});
await mw.loader.getScript('https://test.wikipedia.org/w/index.php?title=User:Sam_Sailor/test.js/comrade-lib.js&action=raw&ctype=text/javascript');
async function getTargetPage(api, name, type) {
const titles = type === 'surname' ? [`List of people with the English surname ${name}`, `List of people with surname ${name}`, `${name} (surname)`, `${name} (name)`, name] : [`List of people with given name ${name}`, `List of people named ${name}`, `${name} (given name)`, `${name} (name)`, name];
const res = await api.get({
action: 'query',
titles: titles.join('|'),
formatversion: 2
});
if (!res.query || !res.query.pages) return null;
return titles.find(t => {
const pg = res.query.pages.find(p => p.title === t);
return pg && !pg.missing;
});
}
async function processHumanNameLists(api, entity, wikitext, currentTitle, hasShortDesc, isOrphan, tClean) {
const {
givens,
surnames
} = await fetchWikidataNameLabels(entity);
const nameParts = tClean.split(' ');
const hasFamilyNameHatnote = /\{\{family[ _]name[ _]hatnote/i.test(wikitext);
const primarySurname = hasFamilyNameHatnote ? nameParts[0] : (getSmartSortKey(tClean, entity).split(',')[0] || nameParts[nameParts.length - 1]);
for (const name of givens) {
if (name.toLowerCase() === primarySurname.toLowerCase()) continue;
const nameInTitle = new RegExp('\\b' + name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b', 'i').test(tClean);
if (!nameInTitle) continue;
const target = await getTargetPage(api, name, 'given');
if (target) await verifyAndNudge(api, name, 'given name', currentTitle, hasShortDesc, isOrphan, target);
}
const finalSurnames = surnames.length > 0 ? surnames : (nameParts.length >= 2 ? [nameParts[nameParts.length - 1]] : []);
for (const name of finalSurnames) {
const nameInTitle = new RegExp('\\b' + name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b', 'i').test(tClean);
if (!nameInTitle) continue;
const target = await getTargetPage(api, name, 'surname');
if (target) await verifyAndNudge(api, name, 'surname', currentTitle, hasShortDesc, isOrphan, target);
}
}
async function verifyAndNudge(api, name, type, currentTitle, hasShortDesc, isOrphan, foundTitle) {
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: foundTitle,
rvprop: 'content',
formatversion: 2
});
const page = res.query.pages[0];
if (!page || page.missing || !page.revisions) return;
const text = page.revisions[0].content;
const isNameMatch = /\{\{(Surname|Hndis|Given[ _]name|Forename|Givenname)/i.test(text);
const isNameDab = /\{\{(Disambiguation|Dab|Disambig)(?:[^}]*\|(surname|surnames|given[ _]name|given[ _]names)|(?:\s*\}\}))/i.test(text);
const isNamePageTitle = foundTitle.includes('List of people with');
if (!isNameMatch && !isNameDab && !isNamePageTitle) return;
const escapedTitle = currentTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/ /g, '[ _]');
const alreadyListed = new RegExp('(\\[\\[|\\{\\{anbl\\|[ _]*|\\|)' + escapedTitle, 'i').test(text);
if (alreadyListed) return;
const $container = $('<div>').addClass('comrade-container').addClass(isOrphan ? 'comrade-nudge' : 'comrade-info');
const $header = $('<div>').addClass('comrade-header').append($('<strong>').text(isOrphan ? 'Comrade nudge:' : 'Informative:'));
const $content = $('<div>').addClass('comrade-content');
$content.append($('<span>').append(`${type.charAt(0).toUpperCase() + type.slice(1)} not listed at `, $('<a>').attr({
href: mw.util.getUrl(foundTitle),
target: '_blank'
}).text(foundTitle), "; should it be?"));
const snippet = '* {{anbl|' + currentTitle + '}}';
const $snippetWrapper = $('<div>').css({
'display': 'flex',
'align-items': 'center',
'gap': '10px',
'margin-top': '4px'
});
const $instructionSpan = $('<span>').append('Consider adding ', $('<code>').text(snippet));
const $copyBtn = $('<button>').text('Copy & insert').on('click', function() {
navigator.clipboard.writeText('\n' + snippet).then(() => {
$(this).text('Copied!').prop('disabled', true);
const editUrl = mw.util.getUrl(foundTitle, {
action: 'edit',
summary: 'Adding * {{[[Template:Annotated biography link|anbl]]|[[' + currentTitle + ']]}}'
});
window.open(editUrl, '_blank');
});
});
$snippetWrapper.append($instructionSpan, $copyBtn);
$content.append($snippetWrapper);
if (!hasShortDesc) {
$content.append($('<span>').css({
'color': '#d33',
'font-weight': 'bold',
'margin-top': '5px'
}).text('⚠️ Missing short description: Please add a concise one before listing.'));
}
$container.append($header, $content);
ComradeLib.appendDismiss($container);
}
async function checkNamingPrecision(currentTitle, isHuman) {
const disambigMatch = currentTitle.match(/^(.+?)(?:, | \()/);
if (isHuman || !disambigMatch) return;
const baseTitle = disambigMatch[1].trim();
const api = new mw.Api();
try {
const baseRes = await api.get({
action: 'query',
titles: baseTitle,
redirects: 1,
formatversion: 2
});
const page = baseRes.query.pages[0];
const baseExists = !page.missing;
let baseRedirectsToSelf = false;
if (baseRes.query.redirects) {
baseRedirectsToSelf = baseRes.query.redirects.some(r => r.to === currentTitle);
}
const searchRes = await api.get({
action: 'query',
list: 'allpages',
apprefix: baseTitle,
aplimit: 50,
formatversion: 2
});
const competitors = searchRes.query.allpages.filter(p => {
const t = p.title;
return t !== currentTitle && t !== baseTitle && (t.startsWith(baseTitle + ', ') || t.startsWith(baseTitle + ' ('));
});
if (!baseExists || baseRedirectsToSelf) {
const $container = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $content = $('<div>').addClass('comrade-content');
if (competitors.length === 0) {
$header.append($('<strong>').text('Precision:'));
const reason = baseRedirectsToSelf ? "already redirects here" : "is free";
$content.append($('<span>').append(`The base name `, $('<code>').text(baseTitle), ` ${reason}. Consider moving this article.`));
const moveUrl = mw.util.getUrl('Special:MovePage/' + currentTitle, {
wpNewTitle: baseTitle,
wpReason: 'Unnecessary disambiguation; base name is free/redirects here per [[WP:PRECISION]]'
});
$content.append($('<button>').text(`Move to ${baseTitle}`).on('click', () => window.open(moveUrl, '_blank')));
} else {
$header.append($('<strong>').text('Observation:'));
const state = baseRedirectsToSelf ? 'redirect to this page' : 'redlink';
$content.append($('<span>').append(`Base name `, $('<code>').text(baseTitle), ` has ${competitors.length} competitors, but it is currently a ${state}.`));
$content.append($('<span>').text('Consider if a disambiguation page is needed.'));
}
$container.append($header, $content);
ComradeLib.appendDismiss($container);
}
} catch (e) {
console.warn("Comrade: Precision check failed", e);
}
}
async function checkHumanSort(wikitext, entity, currentTitle, tClean, refName) {
const dsMatch = wikitext.match(/\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/i);
const {
givens
} = await fetchWikidataNameLabels(entity);
if (dsMatch) {
const currentDS = dsMatch[1].trim();
if (currentDS.includes(',')) {
let [lastPart, firstPart] = currentDS.split(',').map(s => s.trim());
if (firstPart.toLowerCase().endsWith(lastPart.toLowerCase())) {
let fixedFirst = firstPart.substring(0, firstPart.length - lastPart.length).trim();
let suggestion = `${fixNameCasing(lastPart, refName)}, ${fixNameCasing(fixedFirst, refName)}`;
if (suggestion !== currentDS) renderSortNudge(suggestion, "Redundant surname in given name field.");
} else if (givens.length > 0 && lastPart.split(' ').length > 1) {
const parts = lastPart.split(' ');
const misplacedGiven = parts.find(part => givens.some(gn => gn.toLowerCase() === part.toLowerCase()));
if (misplacedGiven) {
const newSurname = parts.filter(p => p !== misplacedGiven).join(' ');
let suggestion = `${fixNameCasing(newSurname, refName)}, ${fixNameCasing(misplacedGiven + ' ' + firstPart, refName)}`;
if (suggestion !== currentDS) renderSortNudge(suggestion, `Wikidata suggests "${misplacedGiven}" is a given name.`);
}
}
}
} else {
const hasFamilyNameHatnote = /\{\{family[ _]name[ _]hatnote/i.test(wikitext);
const nameParts = tClean.split(' ');
const targetSortName = hasFamilyNameHatnote ? `${nameParts[0]}, ${nameParts.slice(1).join(' ')}` : getSmartSortKey(tClean, entity);
if (targetSortName.toLowerCase() !== currentTitle.toLowerCase()) {
const $dsMissing = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Add: {{DEFAULTSORT:${targetSortName}}}`).on('click', () => performSortCorrection(targetSortName, "add"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Tag is missing. `, $btn));
$dsMissing.append($header);
ComradeLib.appendDismiss($dsMissing);
}
}
}
async function fetchWikidataEntity(qid) {
const res = await fetch(`https://www.wikidata.org/w/api.php?action=wbgetentities&ids=${qid}&props=claims|descriptions|labels&languages=en&format=json&origin=*`).then(r => r.json());
return res.entities[qid];
}
function getSmartSortKey(fullName, entity) {
const countryIds = entity.claims?.P27?.map(c => c.mainsnak?.datavalue?.value?.id) || [];
const locationIds = entity.claims?.P17?.map(c => c.mainsnak?.datavalue?.value?.id) || [];
const placeIds = [...(entity.claims?.P19 || []), ...(entity.claims?.P119 || [])].map(c => c.mainsnak?.datavalue?.value?.id).filter(Boolean);
const allRelevantIds = [...countryIds, ...locationIds, ...placeIds];
const birthYear = parseInt(entity.claims?.P569?.[0]?.mainsnak?.datavalue?.value?.time?.match(/[+-](\d{4})/)?.[1]) || null;
if (fullName.startsWith("Saint ")) return fullName.replace("Saint ", "");
const arabicParticles = /\b(Abu|Abd|Abdel|Abdul|ben|bin|bint)\b/i;
if (fullName.match(arabicParticles)) {
if (birthYear && birthYear > 1900) {
const parts = fullName.split(' ');
const binIndex = parts.findIndex(p => p.toLowerCase() === 'bin' || p.toLowerCase() === 'ben');
if (binIndex > 0) return parts.slice(binIndex).join(' ') + ', ' + parts.slice(0, binIndex).join(' ');
} else {
return fullName;
}
}
const eastAsianQids = ["Q148", "Q865", "Q884", "Q424", "Q17", "Q881"];
if (allRelevantIds.some(id => eastAsianQids.includes(id))) {
if (allRelevantIds.includes("Q17") && birthYear && birthYear > 1885) return westernSort(fullName);
const parts = fullName.split(' ');
if (parts.length > 1) return `${parts[0]}, ${parts.slice(1).join(' ')}`;
}
return westernSort(fullName);
}
function westernSort(name) {
let cleanName = name;
if (name.startsWith("O'")) cleanName = name.replace("O'", "O");
const parts = cleanName.split(' ');
if (parts.length < 2) return cleanName;
const surname = parts.pop();
if (["Jr.", "Jr", "III", "II", "Sr.", "Sr"].includes(surname)) {
const actualSurname = parts.pop();
return `${actualSurname}, ${parts.join(' ')} ${surname}`;
}
return `${surname}, ${parts.join(' ')}`;
}
async function performSortCorrection(newName, type) {
const api = new mw.Api();
const title = mw.config.get("wgPageName");
try {
const data = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
let content = data.query.pages[0].revisions[0].content;
const dsRegex = /\{\{\s*(?:DEFAULTSORT|Defaultsort)\s*:[^}]+\}\}/i;
const newTag = `{{DEFAULTSORT:${newName}}}`;
if (dsRegex.test(content)) {
content = content.replace(dsRegex, newTag);
} else {
const catRegex = /\[\[Category:/i;
content = catRegex.test(content) ? content.replace(catRegex, `${newTag}\n[[Category:`) : content.trim() + `\n\n${newTag}`;
}
await api.postWithEditToken({
action: 'edit',
title: title,
text: content,
summary: `Setting DEFAULTSORT to ${newName}`,
nocreate: true
});
mw.notify(`DEFAULTSORT ${type === "add" ? "added" : "corrected"} to ${newName}!`, {
title: 'Comrade Success',
type: 'success'
});
setTimeout(() => location.reload(), 700);
} catch (err) {
mw.notify('Failed to save DEFAULTSORT.', {
type: 'error'
});
}
}
function renderSortNudge(target, reason) {
const $dsNudge = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Correct to: ${target}`).on('click', () => performSortCorrection(target, "format"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` ${reason} `, $btn));
$dsNudge.append($header);
ComradeLib.appendDismiss($dsNudge);
}
async function fetchWikidataNameLabels(entity) {
const familyIds = entity.claims?.P734?.map(c => c.mainsnak.datavalue.value.id) || [];
const givenIds = entity.claims?.P735?.map(c => c.mainsnak.datavalue.value.id) || [];
const allNameIds = [...new Set([...familyIds, ...givenIds])];
if (allNameIds.length === 0) return {
givens: [],
surnames: []
};
const url = new URL("https://www.wikidata.org/w/api.php");
url.search = new URLSearchParams({
action: 'wbgetentities',
ids: allNameIds.join('|'),
props: 'labels',
languages: 'en',
format: 'json',
origin: '*'
});
const nameData = await fetch(url).then(r => r.json());
return {
givens: givenIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean),
surnames: familyIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean)
};
}
function fixNameCasing(part, referenceName) {
return part.split(' ').map(word => {
const match = referenceName.match(new RegExp(`\\b${word}\\b`, 'i'));
if (match) return match[0];
return referenceName.split(' ').find(tWord => word.toLowerCase().startsWith(tWord.toLowerCase()) || tWord.toLowerCase().startsWith(word.toLowerCase())) || word;
}).join(' ');
}
async function init() {
const api = new mw.Api();
const ns = mw.config.get("wgNamespaceNumber");
const action = mw.config.get("wgAction");
if (mw.config.get("wgDBname") !== "enwiki" || action !== "view") return;
if (ns === 14) {
const $btn = $('<button>').attr('id', 'comrade-audit-btn').text('Audit category quality').css({
'margin-bottom': '10px',
'padding': '5px 10px',
'cursor': 'pointer'
}).on('click', ComradeLib.performCategoryAudit);
$('.mw-category-generated').prepend($btn);
return;
}
if (ns !== 0) return;
const qid = mw.config.get("wgWikibaseItemId");
const currentTitle = mw.config.get("wgTitle");
try {
const [pageData, backlinkData] = await Promise.all([
api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get("wgPageName"),
rvprop: 'content',
formatversion: 2
}),
api.get({
action: 'query',
list: 'backlinks',
blfilterredir: 'nonredirects',
bllimit: 50,
blnamespace: 0,
bltitle: mw.config.get("wgPageName"),
formatversion: 2
})
]);
const page = pageData.query.pages[0];
if (!page || page.missing || !page.revisions) return;
const wikitext = page.revisions[0].content;
ComradeLib.checkLeadCapitalization(currentTitle);
// Ligature/ASCII-only/diacritic and domain
const norms = ComradeLib.getNormalizedTitles(currentTitle);
if (norms.ascii) {
let label = "ASCII-only";
let category = "R from ASCII-only";
if (/[æÆœŒijIJßẞ]/.test(currentTitle)) {
label = "Ligature";
category = "R to ligature";
} else if (currentTitle.normalize("NFD").match(/[\u0300-\u036f]/)) {
label = "Diacritic";
category = "R to diacritic";
}
await ComradeLib.checkAndRenderRedirect(norms.ascii, currentTitle, category, label);
}
await ComradeLib.checkDomainRedirect(qid, currentTitle);
// Orphan logic
const isOrphanTagged = /\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)/i.test(wikitext) || /\| *orphan *=/i.test(wikitext);
const backLinkCount = backlinkData.query.backlinks.length;
if (isOrphanTagged && backLinkCount >= 1) {
const $container = $('<div>').addClass('comrade-container comrade-status');
const $btn = $('<button>').text('Remove orphan tag').on('click', async () => await ComradeLib.performDeorphan());
const $statusText = $('<span>').append($('<strong>').text('Status:'), ` Article has ${backLinkCount} backlink(s).`, $btn);
$container.append($('<div>').addClass('comrade-header').append($statusText));
ComradeLib.appendDismiss($container);
}
// Wikidata/human logic
let entity = qid ? await fetchWikidataEntity(qid) : null;
let isHuman = entity?.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5");
await checkNamingPrecision(currentTitle, isHuman);
if (isHuman && entity) {
const tClean = currentTitle.replace(/\s*\(.*?\)\s*/g, '').trim();
const refName = entity.labels?.en?.value || currentTitle;
const hasFamilyNameHatnote = /\{\{family[ _]name[ _]hatnote/i.test(wikitext);
const hasShortDesc = /\{\{\s*[Ss]hort description/i.test(wikitext);
const isOrphan = isOrphanTagged && backLinkCount < 1;
await ComradeLib.checkShortDescDates(wikitext, entity);
await ComradeLib.checkNoShortDesc(wikitext, entity);
// Check DEFAULTSORT
await checkHumanSort(wikitext, entity, currentTitle, tClean, refName);
const longName = ComradeLib.getLongNameFromWikitext(wikitext);
if (longName && longName.length > tClean.length) {
let sortKey = hasFamilyNameHatnote ? `${longName.split(' ')[0]}, ${longName.split(' ').slice(1).join(' ')}` : getSmartSortKey(longName, entity);
const rLongWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from long name}}\n}}\n{{DEFAULTSORT:${sortKey}}}`;
await ComradeLib.checkAndRenderRedirect(longName, currentTitle, null, "Long name", rLongWikitext);
}
// Sort name R
const targetSortName = hasFamilyNameHatnote ? `${tClean.split(' ')[0]}, ${tClean.split(' ').slice(1).join(' ')}` : getSmartSortKey(tClean, entity);
if (targetSortName && targetSortName.toLowerCase() !== currentTitle.toLowerCase()) {
let targetPage = currentTitle.includes(' (') ? currentTitle.split(' (')[0] : currentTitle;
let rCat = targetPage !== currentTitle ? "R from ambiguous sort name" : "R from sort name";
if (targetSortName.includes(',')) {
const p = targetSortName.split(',');
rCat += `|${p[0].trim().charAt(0).toUpperCase()}|${p[1].trim().charAt(0).toUpperCase()}`;
}
await ComradeLib.checkAndRenderRedirect(targetSortName, targetPage, rCat, "Sort name");
}
// Name lists
await processHumanNameLists(api, entity, wikitext, currentTitle, hasShortDesc, isOrphan, tClean);
}
// Quality/ORES
const isRedirect = !!mw.config.get("wgIsRedirect");
const userExclusions = window.comradeQualityExclusions || [];
const allExclusions = [...window.ComradeOptions.excludedTemplates, ...userExclusions];
const exclusionRegexString = allExclusions.map(t => t.replace("Template:", "").replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
const exclusionRegex = new RegExp(exclusionRegexString, 'i');
if (isRedirect || exclusionRegex.test(wikitext)) {
console.log("Comrade check skipped: Page is a redirect or excluded via options.");
return;
}
ComradeLib.checkQualityConsistency();
} catch (err) {
console.error("Comrade error:", err);
}
}
init();
})();
(function() {
'use strict';
if (mw.config.get('wgCanonicalSpecialPageName') !== 'Search') {
return;
}
mw.loader.using(['mediawiki.util']).then(function() {
$(function() {
const $resultsInfo = $('.results-info');
const totalResultsRaw = $resultsInfo.attr('data-mw-num-results-total') || '0';
function formatWithCommas(numberString) {
return numberString.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
const state = {
language: mw.config.get('wgContentLanguage'),
totalResultsFormatted: formatWithCommas(totalResultsRaw)
};
let titlesCache = null;
function extractResultTitles() {
const currentResults = $('.mw-search-result-heading');
if (!titlesCache || titlesCache.length !== currentResults.length) {
titlesCache = currentResults.map(function() {
return $(this).find('a:first').attr('title');
}).get().filter(Boolean);
}
return titlesCache;
}
mw.hook("wikipage.content").add(function() {
titlesCache = null;
});
function addLimitOptions() {
const selects = document.querySelectorAll('.cdx-table-pager__limit-form select[name="limit"]');
if (!selects.length) return;
const valuesToAdd = ["1000", "2500", "5000"];
selects.forEach(function(select) {
valuesToAdd.forEach(function(value) {
if (select.querySelector('option[value="' + value + '"]')) return;
const option = document.createElement("option");
option.value = value;
option.textContent = formatWithCommas(value) + " items";
const existingOptions = Array.from(select.querySelectorAll("option"));
const insertBefore = existingOptions.find(function(opt) {
return Number(opt.value) > Number(value);
});
if (insertBefore) {
select.insertBefore(option, insertBefore);
} else {
select.appendChild(option);
}
});
});
}
mw.hook("wikipage.content").add(addLimitOptions);
const numLinks = $('.mw-numlink');
if (numLinks.length > 4 && !numLinks.eq(4).next().is('a[href*="limit=5000"]')) {
const $firstAnchor = numLinks.eq(4);
const $firstSpan = $('<span> | </span>').insertAfter($firstAnchor);
const url1 = new URL(window.location.href);
url1.searchParams.set('limit', '5000');
$('<a>', {
text: '5000',
title: 'Show 5000 results per page',
href: url1.toString()
}).insertAfter($firstAnchor).before($firstSpan);
}
if (numLinks.length > 9 && !numLinks.eq(9).next().is('a[href*="limit=5000"]')) {
const $secondAnchor = numLinks.eq(9);
const $secondSpan = $('<span> | </span>').insertAfter($secondAnchor);
const url2 = new URL(window.location.href);
url2.searchParams.set('limit', '5000');
$('<a>', {
text: '5000',
title: 'Show 5000 results per page',
href: url2.toString()
}).insertAfter($secondAnchor).before($secondSpan);
}
const $container = $("<div></div>").addClass("enhanced-search-container").hover(function() {
$(this).addClass("hover");
}, function() {
$(this).removeClass("hover");
});
const interwikiResults = document.getElementById("mw-interwiki-results");
if (interwikiResults) {
$container.addClass("enhanced-search-floating").insertBefore("#mw-interwiki-results");
} else {
$container.addClass("enhanced-search-absolute").insertBefore(".searchresults");
}
const totalFormattedStr = state.totalResultsFormatted;
const $titleElement = $("<div>Show a shortened list of<br />this page's " + $('.mw-search-result-heading').length + " results <small>(of total of " + totalFormattedStr + ")</small></div>").addClass("enhanced-header").appendTo($container);
const $contentArea = $("<div></div>").addClass("enhanced-content-area").appendTo($container).hide();
const $note1 = $("<div>Copy this page\'s " + $('.mw-search-result-heading').length + " results to the clipboard as...</div>").addClass("enhanced-note").insertAfter($container).hide();
function setupHoverTooltip(element, message) {
$(element).attr('title', message).hover(function() {
$(this).addClass("hover");
}, function() {
$(this).removeClass("hover");
});
}
const $note2 = $("<div>...plain text<sup> (?)</sup></div>").addClass("enhanced-btn").insertAfter($note1).hide();
setupHoverTooltip($note2, 'This button copies this page\'s results to the clipboard as plain text, i.e. without wikitext markup. Since this copies only the results shown on this search page, make sure you have clicked the "view 20, 50, 100, 250, 500, 5000" link. This format is compatible with PetScan and AutoWikiBrowser (AWB) for example.');
const $note3 = $("<div>...wikitext<sup> (?)</sup></div>").addClass("enhanced-btn").insertAfter($note2).hide();
setupHoverTooltip($note3, 'This button copies this page\'s results to the clipboard and also adds wikitext markup.');
const $note4 = $("<div>...PetScan URL<sup> (?)</sup></div>").addClass("enhanced-btn").insertAfter($note3).hide();
setupHoverTooltip($note4, 'This button copies a PetScan URL with this page\'s results to the clipboard. Note that if the PetScan URL becomes too long (~250+ pages), the URL might not work.');
const $note5 = $("<div>...or this search query's total results of " + totalFormattedStr + "<sup> (?)</sup> as...</div>").addClass("enhanced-note").insertAfter($note4).hide().attr('title', 'Note that the actual limit is 10,000, which is MediaWiki\'s maximum limit.').hover(function() {
$(this).addClass("hover");
}, function() {
$(this).removeClass("hover");
});
const $note6 = $("<div>...PetScan URL<sup> (?)</sup></div>").addClass("enhanced-btn").insertAfter($note5).hide();
setupHoverTooltip($note6, 'This button copies a PetScan URL with all ' + totalFormattedStr + ' results to the clipboard. Note that the maximum limit is 10,000 due to MediaWiki\'s restriction.');
const $note7 = $("<div><sup>(?)</sup> = hover over for tooltip details</div>").addClass("enhanced-note-right").insertAfter($note6).hide();
const noteElements = [$note1, $note2, $note3, $note4, $note5, $note6, $note7];
if (interwikiResults !== null) {
noteElements.forEach($el => $el.addClass("enhanced-floating-elements"));
} else {
noteElements.forEach(function($el, index) {
$el.addClass("enhanced-absolute-elements enhanced-position-" + index);
});
}
function formatPlain() {
return extractResultTitles().join("\r\n");
}
function formatWikitext() {
const titles = extractResultTitles().map(title => "* [[" + title + "]]");
const header = "This is a list of [" + window.location.href + " these search results] (made with the script [[User:JAAqqO/EnhancedSearchPage|EnhancedSearchPage]]):\r\n";
return header + titles.join("\r\n");
}
function formatPetScan() {
const encodedTitles = extractResultTitles().map(encodeURIComponent).join("%0A");
return "https://petscan.wmflabs.org/?active_tab=tab_other_sources&manual_list=" + encodedTitles + "&manual_list_wiki=" + state.language + "wiki&project=wikipedia&language=" + state.language + "&doit=";
}
function formatPetScanTotal() {
const oouiInput = document.getElementById("ooui-php-1");
const value = oouiInput ? oouiInput.value : "";
if (value) {
return "https://petscan.wmflabs.org/?active_tab=tab_other_sources&ns%5B0%5D=1&search_query=" + encodeURIComponent(value) + "&search_wiki=" + state.language + "wiki&search_max_results=10000&project=wikipedia&language=" + state.language + "&doit=";
}
return "";
}
function copyToClipboard(text) {
if (!text) return;
if (!navigator.clipboard) {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.setAttribute('readonly', '');
textarea.style.position = 'absolute';
textarea.style.left = '-9999px';
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
} catch (e) {
console.error('Fallback copy failed', e);
}
textarea.remove();
} else {
navigator.clipboard.writeText(text).catch(function(err) {
console.error("Copy failed: ", err);
});
}
}
function showCopiedMessage(event) {
const posX = event.pageX;
const posY = event.pageY;
const $msg = $("<div class='copied-message'>Copied!</div>").appendTo("body");
$msg.css({
position: "absolute",
top: (posY + 10) + "px",
left: (posX + 10) + "px",
backgroundColor: "#f0f0f0",
border: "1px solid grey",
padding: "5px 10px",
borderRadius: "5px",
zIndex: 9999
});
setTimeout(function() {
$msg.fadeOut("slow", function() {
$(this).remove();
});
}, 500);
}
function performButtonAnimation($element) {
$element.css("transform", "scaleY(0.9)").animate({
opacity: '0.5'
}, "fast", function() {
$(this).animate({
opacity: '1'
}, "slow").css("transform", "scaleY(1)");
});
}
let isExpanded = false;
function showContentArea() {
if (isExpanded) return;
isExpanded = true;
const currentResults = $('.mw-search-result-heading');
$contentArea.empty().append(currentResults.clone());
$contentArea.slideDown();
$(".searchalttitle").remove();
$container.addClass("enhanced-active-container");
$titleElement.remove();
noteElements.forEach($el => $el.show());
}
function bindCopyAction($element, formatterFn) {
$element.click(function(event) {
copyToClipboard(formatterFn());
showCopiedMessage(event);
performButtonAnimation($(this));
});
}
$container.click(showContentArea);
bindCopyAction($note2, formatPlain);
bindCopyAction($note3, formatWikitext);
bindCopyAction($note4, formatPetScan);
bindCopyAction($note6, formatPetScanTotal);
const $pageDown = $("<div><br /><br />[ ↓ ]</div>").addClass("enhanced-scroll-btn").hover(function() {
$(this).addClass("hover");
$(this).attr('title', 'Go to the bottom of the page');
}, function() {
$(this).removeClass("hover");
}).insertBefore('.mw-search-result:eq(0)');
const $pageUp = $("<div>[ ↑ ]</div>").addClass("enhanced-scroll-btn").hover(function() {
$(this).addClass("hover");
$(this).attr('title', 'Go to the top of the page');
}, function() {
$(this).removeClass("hover");
}).insertAfter('.mw-search-result:eq(-1)');
$pageDown.click(function(event) {
event.preventDefault();
$('html, body').animate({
scrollTop: $(document).height()
}, 500);
});
$pageUp.click(function() {
$('html, body').animate({
scrollTop: 0
}, 500);
});
$("<span> (<b>" + $('.mw-search-result-heading').length + "</b> shown on the page)</span>").appendTo('.results-info');
const searchDidYouMean = document.querySelector('.searchdidyoumean');
if (searchDidYouMean) {
searchDidYouMean.style.color = '#b32424';
searchDidYouMean.style.fontSize = '20px';
}
});
});
})();
(function() {
if (window.Headmaster) return;
const VERSION = "0.9.2.1";
const defaultNS = [0, 118];
const userNS = window.headmaster_ns || [];
const config = {
menu: window.headmaster_menu || 'p-cactions',
enableSummary: window.headmaster_do_summary !== false,
customSummary: window.headmaster_summary || "",
allowedNamespaces: Array.from(new Set([...defaultNS, ...userNS])),
autoScan: window.headmaster_auto_scan !== false,
autoRun: window.headmaster_auto_run === false
};
const Headmaster = {
VERSION: VERSION,
DefaultSummary: "Converting [[MOS:PSEUDOHEAD|pseudo-headings]] {details}using [[User:Sam_Sailor/Scripts/Headmaster.js|Headmaster]]. Semicolon markup is reserved for [[MOS:DLIST|description lists]]",
styleInjected: false,
css: `
#hm-panel { clear: both; border: 1px solid #a2a9b1; padding: 15px; background: #f8f9fa; margin-bottom: 1.5em; border-radius: 2px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
#hm-table { width: 100%; border-collapse: collapse; background: white; margin-top: 10px; }
#hm-table th { background: #f2f2f2; border: 1px solid #a2a9b1; padding: 8px; text-align: left; }
#hm-table td { border: 1px solid #a2a9b1; padding: 8px; vertical-align: middle; }
.hm-context { font-family: 'Courier New', monospace; font-size: 0.9em; color: #54595d; background: #fdfdfd; }
#hm-controls { display: flex; justify-content: space-between; align-items: center; margin-top: 15px; }
#hm-controls button { margin-left: 10px; padding: 6px 16px; cursor: pointer; border-radius: 2px; }
.hm-btn-apply { background: #36c; color: white; border: 1px solid #36c; font-weight: bold; }
.hm-btn-apply:hover { background: #447ff5; }
.hm-global-selector { font-size: 0.9em; color: #202122; background: #eaecf0; padding: 8px; border-radius: 2px; border: 1px solid #a2a9b1; }
.hm-global-selector a { font-weight: bold; text-decoration: none; color: #36c; }
.hm-global-selector a:hover { text-decoration: underline; }
.hm-locate-btn { cursor: pointer; color: #36c; border: 1px solid #a2a9b1; background: #f8f9fa; padding: 2px 6px; font-size: 0.85em; border-radius: 2px; }
.hm-locate-btn:hover { background: #fff; border-color: #36c; }
`,
setup() {
if (!config.allowedNamespaces.includes(mw.config.get("wgNamespaceNumber"))) return;
const action = mw.config.get("wgAction");
this.addPortlet(action);
if (action === 'view' && (config.autoScan || config.autoRun)) {
this.silentScan();
}
if (mw.util.getParamValue('headmaster') === '1' && ['edit', 'submit'].includes(action)) {
const params = new URLSearchParams(window.location.search);
params.delete('headmaster');
const searchString = params.toString();
const newUrl = window.location.pathname + (searchString ? '?' + searchString : '');
window.history.replaceState(null, '', newUrl);
this.init();
}
},
addPortlet(action) {
const link = mw.util.addPortletLink(config.menu, '#', 'Headmaster', 'ca-hm', 'Interactively convert pseudo-headings');
if (link) {
$(link).on('click', (e) => {
e.preventDefault();
if (action === 'view') {
this.redirectToEdit();
} else if (['edit', 'submit'].includes(action)) {
this.init();
}
});
}
},
redirectToEdit() {
window.location.href = mw.util.getUrl(mw.config.get("wgPageName"), {
action: 'edit',
headmaster: '1'
});
},
silentScan() {
new mw.Api().get({
action: 'query',
prop: 'revisions',
titles: mw.config.get('wgPageName'),
rvprop: 'content',
rvlimit: 1,
formatversion: 2
}).done((data) => {
const page = data.query.pages[0];
if (!page || !page.revisions) return;
const wikitext = page.revisions[0].content;
const tasks = this.analyzeText(wikitext);
if (tasks.length > 0) {
if (config.autoRun) {
this.redirectToEdit();
} else {
this.notifyDiscovery(tasks.length);
}
}
}).fail((err) => {
mw.log.warn("Headmaster: silentScan failed", err);
});
},
notifyDiscovery(count) {
const $msg = $('<span>').text(`Headmaster: Found ${count} issues. `).append($('<a>').text('Run conversion').css({
fontWeight: 'bold',
cursor: 'pointer'
}).on('click', () => this.redirectToEdit()));
mw.notify($msg, {
tag: 'hm-discovery',
autoHide: false
});
},
init() {
if (!this.styleInjected) {
mw.loader.addStyleTag(this.css);
this.styleInjected = true;
}
const $textbox = $('#wpTextbox1');
const wikitext = $textbox.textSelection('getContents');
if (!wikitext) {
mw.notify("Textbox is empty or not found.", {
type: 'error'
});
return;
}
const tasks = this.analyzeText(wikitext);
if (tasks.length === 0) {
mw.notify("No issues found.");
return;
}
this.showUI(tasks, wikitext);
},
analyzeText(wikitext) {
const lines = wikitext.split("\n");
const tasks = [];
let currentDepth = 1;
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
const headingMatch = line.match(/^(={2,6})\s*(.+?)\s*\1\s*$/);
if (headingMatch) {
currentDepth = headingMatch[1].length;
const prevLine = i > 0 ? lines[i - 1].trim() : null;
const isSmashed = prevLine !== null && prevLine !== "";
const isExtraSpace = i > 1 && lines[i - 1].trim() === "" && lines[i - 2].trim() === "";
if (isSmashed || isExtraSpace) {
tasks.push({
index: i,
type: 'WHITESPACE',
original: lines[i],
isSmashed: isSmashed,
isExtraSpace: isExtraSpace
});
}
} else if (line.startsWith(';') && !line.startsWith(';;')) {
const nextLine = lines[i + 1] || "";
if (!nextLine.trim().startsWith(':')) {
const cleanText = line.substring(1).replace(/\s*:.*$/, "").trim();
const suggestedLevel = "=".repeat(Math.min(currentDepth + 1, 6));
tasks.push({
index: i,
type: 'PSEUDO',
original: line,
cleanText: cleanText,
context: nextLine.substring(0, 60).trim() + (nextLine.length > 60 ? "..." : ""),
suggestedLevel: suggestedLevel
});
}
}
}
return tasks;
},
showUI(tasks, originalWikitext) {
$('#hm-panel').remove();
$('#editform').hide();
const visibleTasks = tasks.filter(t => t.type === 'PSEUDO');
const pseudoCount = visibleTasks.length;
const whitespaceCount = tasks.filter(t => t.type === 'WHITESPACE').length;
let statusMsg = "";
const s = (n) => n !== 1 ? 's' : '';
if (pseudoCount > 0) {
statusMsg = `Found <strong>${pseudoCount}</strong> possible pseudo-heading${s(pseudoCount)}.`;
if (whitespaceCount > 0) {
statusMsg += ` Also fixing <strong>${whitespaceCount}</strong> spacing issue${s(whitespaceCount)} quietly.`;
}
} else {
statusMsg = `Did not find any pseudo-headings, but fixing <strong>${whitespaceCount}</strong> spacing issue${s(whitespaceCount)} quietly.`;
}
const tableRows = pseudoCount > 0 ? visibleTasks.map((task, i) => `
<tr data-task-index="${task.index}">
<td><button class="hm-locate-btn" data-hm-i="${tasks.indexOf(task)}" title="Show in editor">Locate</button></td>
<td class="hm-context"><code>${mw.html.escape(task.original)}</code></td>
<td><label><input type="radio" name="hm-opt-${i}" value="keep"> Keep</label></td>
<td><label><input type="radio" name="hm-opt-${i}" value="bold" checked> <b>Bold</b></label></td>
<td><label><input type="radio" name="hm-opt-${i}" value="head"> Heading (${task.suggestedLevel.length})</label></td>
<td class="hm-context">${mw.html.escape(task.context || "(end of page)")}</td>
</tr>
`).join("") : `<tr><td colspan="6" style="text-align:center; padding: 2em; color: #54595d; background: #f8f9fa;">
No interactive heading changes detected. Only background spacing normalization will be applied.
</td></tr>`;
const $panel = $(`
<div id="hm-panel">
<h3 style="margin-top:0">Headmaster v${this.VERSION}: Review pseudo-headings</h3>
<div class="hm-global-selector" ${pseudoCount === 0 ? 'style="display:none"' : ''}>
<strong>Bulk action:</strong>
<a href="#" id="hm-all-keep">Set all to Keep</a> |
<a href="#" id="hm-all-bold">Set all to Bold</a> |
<a href="#" id="hm-all-head">Set all to Heading</a>
</div>
<table id="hm-table">
<thead>
<tr>
<th>Find</th>
<th>Source</th>
<th colspan="3">Action</th>
<th>Next line context</th>
</tr>
</thead>
<tbody>${tableRows}</tbody>
</table>
<div id="hm-controls">
<span>${statusMsg}</span>
<div>
<button id="hm-cancel" class="mw-ui-button">Cancel</button>
<button id="hm-apply" class="hm-btn-apply">Show changes (diff)</button>
</div>
</div>
</div>
`);
$('#content').prepend($panel);
window.scrollTo(0, 0);
$('.hm-locate-btn').on('click', function() {
$(document).off('mousedown.hmlocate');
const idx = $(this).data('hm-i');
const task = tasks[idx];
const lines = originalWikitext.split('\n');
let charOffset = 0;
for (let j = 0; j < task.index; j++) {
charOffset += lines[j].length + 1;
}
$('#hm-panel').hide();
$('#editform').show();
const $textbox = $('#wpTextbox1');
$textbox.textSelection('setSelection', {
start: charOffset,
end: charOffset + lines[task.index].length
});
$textbox.textSelection('scrollToCaretPosition');
const locateNotify = mw.notify("Locating line " + (task.index + 1) + ". Click outside the editor to return.", {
tag: 'hm-locate',
type: 'success',
autoHide: false
});
setTimeout(() => {
$(document).on('mousedown.hmlocate', function(e) {
if (!$(e.target).closest('#editform, .mw-notification-area').length) {
$(document).off('mousedown.hmlocate');
Promise.resolve(locateNotify).then(n => n.close());
$('#editform').hide();
$('#hm-panel').show();
}
});
}, 200);
});
$('#hm-all-keep').on('click', (e) => {
e.preventDefault();
$('input[value="keep"]').prop('checked', true);
});
$('#hm-all-bold').on('click', (e) => {
e.preventDefault();
$('input[value="bold"]').prop('checked', true);
});
$('#hm-all-head').on('click', (e) => {
e.preventDefault();
$('input[value="head"]').prop('checked', true);
});
$('#hm-cancel').on('click', () => {
$('#hm-panel').remove();
$('#editform').show();
});
$('#hm-apply').on('click', () => this.applyChanges(tasks, originalWikitext));
},
getProcessedSummary(existingSummary, bolds, heads, spaces) {
if (!config.enableSummary) return existingSummary;
let mySummary = "";
let isDefault = false;
if (config.customSummary) {
mySummary = config.customSummary;
} else {
isDefault = true;
if (bolds > 0 || heads > 0) {
const detailsArr = [];
if (bolds > 0) detailsArr.push(`${bolds} to bold`);
if (heads > 0) detailsArr.push(`${heads} to heading`);
const detailsStr = detailsArr.length > 0 ? `(${detailsArr.join(', ')}) ` : "";
mySummary = this.DefaultSummary.replace("{details}", detailsStr);
if (spaces > 0) {
mySummary += "; also handled [[MOS:BLANKLINE]]";
}
} else if (spaces > 0) {
mySummary = "Handling [[MOS:BLANKLINE]] using [[User:Sam_Sailor/Scripts/Headmaster.js|Headmaster]]";
}
}
if (!mySummary) return existingSummary;
const trimmedOld = existingSummary.trim();
if (!trimmedOld) return mySummary;
if (/[.!?]$/.test(trimmedOld)) {
return trimmedOld + " " + mySummary;
} else {
const finalSummary = isDefault ? mySummary.charAt(0).toLowerCase() + mySummary.slice(1) : mySummary;
return trimmedOld + "; " + finalSummary;
}
},
applyChanges(tasks, wikitext) {
try {
let lines = wikitext.split("\n");
let bCount = 0,
hCount = 0,
wCount = 0;
const pseudoTasks = tasks.filter(t => t.type === 'PSEUDO');
for (let i = pseudoTasks.length - 1; i >= 0; i--) {
const task = pseudoTasks[i];
const idx = task.index;
const action = $('#hm-table tbody tr').filter(`[data-task-index="${idx}"]`).find('input:checked').val();
if (action === 'bold') {
let newValue = `'''${task.cleanText}'''`;
if (idx > 0 && lines[idx - 1].trim() !== "") {
newValue = "\n" + newValue;
}
lines[idx] = newValue;
bCount++;
} else if (action === 'head') {
const h = task.suggestedLevel;
let newValue = `${h} ${task.cleanText} ${h}`;
if (idx > 0 && lines[idx - 1].trim() !== "") {
newValue = "\n" + newValue;
}
lines[idx] = newValue;
hCount++;
}
}
const spaceTasks = tasks.filter(t => t.type === 'WHITESPACE');
for (let i = spaceTasks.length - 1; i >= 0; i--) {
const task = spaceTasks[i];
const idx = task.index;
if (task.isSmashed) {
lines.splice(idx, 0, "");
} else if (task.isExtraSpace) {
let backIdx = idx - 1;
while (backIdx > 0 && lines[backIdx].trim() === "" && lines[backIdx - 1].trim() === "") {
lines.splice(backIdx, 1);
backIdx--;
}
}
wCount++;
}
if (bCount + hCount + wCount === 0) {
mw.notify("No changes selected.");
$('#hm-panel').remove();
$('#editform').show();
return;
}
$('#wpTextbox1').textSelection('setContents', lines.join("\n"));
const $summary = $('#wpSummary');
$summary.val(this.getProcessedSummary($summary.val(), bCount, hCount, wCount));
$('#hm-panel').remove();
$('#editform').show();
mw.notify(`Headmaster: Fixed ${wCount} spacing issue(s) and converted ${bCount + hCount} heading(s).`);
$('html, body').animate({
scrollTop: 0
}, 'fast');
$('#wpDiff').click();
} catch (err) {
$('#editform').show();
mw.notify("Headmaster error: " + err.message, {
type: 'error'
});
}
}
};
window.Headmaster = Headmaster;
mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => {
$(() => Headmaster.setup());
});
})();
//</nowiki>
pohnphaxecymm2zarkvfpnzioskajx1
749881
749672
2026-07-04T08:07:52Z
Sam Sailor
26820
Test
749881
javascript
text/javascript
//<nowiki>
(function(mw, $, OO) {
'use strict';
const IGNORE_SUFFIXES = ['Sr.', 'Sr', 'Jr.', 'Jr', 'II', 'III', 'IV', 'V', 'MD', 'PhD'];
const LANG_PREFS = ['frwiki', 'dewiki', 'eswiki', 'itwiki', 'nlwiki', 'svwiki'];
const NON_LANG_WIKIS = ['commonswiki', 'specieswiki', 'metawiki', 'mediawikiwiki', 'wikidatawiki', 'wikimaniawiki'];
const suffixRE = new RegExp('\\s(' + IGNORE_SUFFIXES.join('|').replace(/\./g, '\\.') + ')$', 'i');
function GetterDialog(config) {
GetterDialog.super.call(this, config);
this.abortController = null;
}
OO.inheritClass(GetterDialog, OO.ui.ProcessDialog);
GetterDialog.static.name = 'getterDialog';
GetterDialog.static.title = 'Getter: Wikipedia List Builder';
GetterDialog.static.actions = [
{ action: 'generate', label: 'Generate List', flags: ['primary', 'progressive'], modes: 'edit' },
{ action: 'back', label: 'Go back', modes: 'result' },
{ action: 'exit', label: 'Exit', flags: 'safe', modes: ['edit', 'result'] }
];
GetterDialog.prototype.cleanTitle = function(t) { return t.replace(suffixRE, '').replace(/\s?\([^)]*\)$/, '').trim(); };
GetterDialog.prototype.normalizeTitle = function(t) { return t.replace(/_/g, ' ').trim().replace(/^(.)/, c => c.toUpperCase()); };
GetterDialog.prototype.getSortKey = function(title) {
const clean = this.cleanTitle(title);
const parts = clean.split(' ');
return (parts.length <= 1) ? title.toLowerCase() : (parts[parts.length - 1] + ', ' + parts.slice(0, parts.length - 1).join(' ') + ' | ' + title).toLowerCase();
};
GetterDialog.prototype.compileCriteria = function(vals, anchorLC) {
return vals.filter(t => t.toLowerCase() !== anchorLC)
.map(r => new RegExp('^' + r.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*') + '$', 'i'));
};
GetterDialog.prototype.getBestAnchor = function() {
const getTerms = (w) => w.getValue().map(t => t.trim()).filter(Boolean);
const candidates = [
...getTerms(this.surnameTags).map(t => ({ term: t, weight: 2000 + this.score(t) })),
...getTerms(this.givenTags).map(t => ({ term: t, weight: 1000 + this.score(t) })),
...getTerms(this.middleTags).map(t => ({ term: t, weight: 0 + this.score(t) }))
];
return candidates.length === 0 ? null : candidates.reduce((b, c) => (c.weight > b.weight ? c : b)).term;
};
GetterDialog.prototype.score = function(term) {
const wildcardCount = (term.match(/\*/g) || []).length;
return (wildcardCount === 0 ? 1000 : 0) + (term.length - wildcardCount) - wildcardCount;
};
GetterDialog.prototype.formatResult = function(p, mode, includeIll) {
if (mode === 'anbl') return `* {{anbl|${p.title}}}`;
const bY = p.ent?.claims?.P569?.[0]?.mainsnak?.datavalue?.value?.time?.substring(1, 5);
const dY = p.ent?.claims?.P570?.[0]?.mainsnak?.datavalue?.value?.time?.substring(1, 5);
const bio = bY ? ` (${bY}${dY ? '–' + dY : ''})` : '';
const rawDesc = (p.desc || '').replace(/\s?\(\d{4}(–\d{4})?\)$/i, '').trim();
const suffix = rawDesc ? `, ${rawDesc}` : '';
let display = `[[${p.title}]]`;
if (includeIll) {
const langKey = LANG_PREFS.find(l => p.ent.sitelinks?.[l]) || Object.keys(p.ent.sitelinks || {}).find(k => k.endsWith('wiki') && k !== 'enwiki' && !NON_LANG_WIKIS.includes(k));
if (langKey) display = `{{ill|${p.title}|${langKey.replace('wiki', '')}|${p.ent.sitelinks[langKey].title}}}`;
}
return `* ${display}${bio}${suffix}`;
};
GetterDialog.prototype.matchesCriteria = function(title, compiled) {
const clean = this.cleanTitle(title);
const tokens = clean.split(/\s+/);
const given = tokens[0] || '', surname = tokens.length > 1 ? tokens[tokens.length - 1] : '';
const middleLC = tokens.length > 2 ? tokens.slice(1, -1).map(x => x.toLowerCase()) : [];
const check = (val, regexes) => (!regexes || regexes.length === 0 || regexes.some(re => re.test(val)));
return check(given, compiled.given) && check(surname, compiled.surname) && (compiled.middle.length === 0 || compiled.middle.every(m => middleLC.includes(m)));
};
GetterDialog.prototype.getExistingLinks = async function() {
const titles = new Set();
try {
const data = await this.api.get({ action: 'parse', page: mw.config.get('wgPageName'), prop: 'wikitext', format: 'json' });
const text = data.parse?.wikitext?.['*'] || '';
const regexes = [/\[\[([^\]|#]+)/g, /\{\{(?:anbl|Annotated biography link)\|([^}|]+)/gi];
regexes.forEach(re => { let m; while ((m = re.exec(text)) !== null) titles.add(this.normalizeTitle(m[1])); });
} catch (e) { console.error('[Getter] Parse error:', e); }
return titles;
};
async function checkEntitiesData(qids, signal) {
if (!qids.length) return {};
const all = {};
for (let i = 0; i < qids.length; i += 50) {
if (signal?.aborted) throw new DOMException('Aborted', 'AbortError');
try {
const res = await fetch('https://www.wikidata.org/w/api.php?action=wbgetentities&ids=' + qids.slice(i, i + 50).join('|') + '&props=claims|labels|descriptions|sitelinks&languages=en&format=json&origin=*', { signal }).then(r => r.json());
Object.assign(all, res.entities || {});
} catch (e) { if (e.name === 'AbortError') throw e; console.error('[Getter] Wikidata batch error', e); }
}
return all;
}
GetterDialog.prototype.autoPopulateFields = async function () {
const title = mw.config.get('wgTitle'), cleanTitle = this.cleanTitle(title);
const m = title.match(/^(.*?) \((given name|surname)\)$/i);
if (m) { m[2].toLowerCase() === 'given name' ? this.givenTags.addTag(m[1]) : this.surnameTags.addTag(m[1]); return; }
try {
const data = await this.api.get({ action: 'parse', page: mw.config.get('wgPageName'), prop: 'wikitext', format: 'json' });
const text = data.parse?.wikitext?.['*'] || '';
const hasSurname = /\{\{\s*surname\s*(?:\||\}\})/i.test(text);
const givenTemplate = text.match(/\{\{\s*given name\b[^}]*\}\}/i)?.[0] || '';
const isBoth = /\|\s*type\s*=\s*both\b/i.test(givenTemplate);
if (hasSurname || (givenTemplate && isBoth)) this.surnameTags.addTag(cleanTitle);
if (givenTemplate) this.givenTags.addTag(cleanTitle);
if (!hasSurname && !givenTemplate) this.givenTags.addTag(cleanTitle);
} catch (e) { console.error('[Getter] Inspect error', e); this.givenTags.addTag(cleanTitle); }
};
GetterDialog.prototype.initialize = function() {
GetterDialog.super.prototype.initialize.apply(this, arguments);
this.api = new mw.Api();
this.panel = new OO.ui.PanelLayout({ padded: true, expanded: false });
this.givenTags = new OO.ui.TagMultiselectWidget({ placeholder: 'Given names...', allowArbitrary: true });
this.middleTags = new OO.ui.TagMultiselectWidget({ placeholder: 'Middle terms...', allowArbitrary: true });
this.surnameTags = new OO.ui.TagMultiselectWidget({ placeholder: 'Surnames...', allowArbitrary: true });
this.excludeTags = new OO.ui.TagMultiselectWidget({ placeholder: 'Exclude strings...', allowArbitrary: true });
this.humanOnly = new OO.ui.CheckboxInputWidget({ selected: true });
this.excludeHumans = new OO.ui.CheckboxInputWidget({ selected: false });
this.includeIll = new OO.ui.CheckboxInputWidget({ selected: false });
this.sortBySurname = new OO.ui.CheckboxInputWidget({ selected: true });
this.updateMode = new OO.ui.CheckboxInputWidget({ selected: false });
this.excludeHumans.on('change', (s) => { if (s) { this.humanOnly.setSelected(false); this.sortBySurname.setSelected(false); } });
this.humanOnly.on('change', (s) => { if (s) this.excludeHumans.setSelected(false); });
this.formatSelect = new OO.ui.ButtonSelectWidget({ items: [new OO.ui.ButtonOptionWidget({ data: 'wiki', label: 'Wiki' }), new OO.ui.ButtonOptionWidget({ data: 'anbl', label: 'ANBL' })]});
this.formatSelect.selectItemByData('wiki');
this.fieldset = new OO.ui.FieldsetLayout({ label: 'Advanced Filter Settings' });
this.fieldset.addItems([
new OO.ui.FieldLayout(this.givenTags, { label: 'Given names', align: 'top' }),
new OO.ui.FieldLayout(this.middleTags, { label: 'Middle terms', align: 'top' }),
new OO.ui.FieldLayout(this.surnameTags, { label: 'Surnames', align: 'top' }),
new OO.ui.FieldLayout(this.excludeTags, { label: 'Exclude strings', align: 'top' }),
new OO.ui.FieldLayout(this.humanOnly, { label: 'Humans only', align: 'inline' }),
new OO.ui.FieldLayout(this.excludeHumans, { label: 'Exclude humans', align: 'inline' }),
new OO.ui.FieldLayout(this.sortBySurname, { label: 'Sort by surname', align: 'inline' }),
new OO.ui.FieldLayout(this.updateMode, { label: 'Exclude existing links', align: 'inline' }),
new OO.ui.FieldLayout(this.includeIll, { label: 'Include {{ill}}', align: 'inline' }),
new OO.ui.FieldLayout(this.formatSelect, { label: 'Format', align: 'top' })
]);
this.resultArea = new OO.ui.MultilineTextInputWidget({ rows: 15, autosize: true });
this.copyButton = new OO.ui.ButtonWidget({ label: 'Copy list', icon: 'copy', flags: ['progressive'] });
this.copyButton.$element.css('marginTop', '10px');
this.copyButton.on('click', () => navigator.clipboard.writeText(this.resultArea.getValue()).then(() => mw.notify('Copied!')));
this.resultContainer = new OO.ui.PanelLayout({ padded: true, expanded: false });
this.resultContainer.$element.append(new OO.ui.FieldLayout(this.resultArea, { label: 'Results', align: 'top' }).$element, this.copyButton.$element).hide();
this.panel.$element.append(this.fieldset.$element, this.resultContainer.$element);
this.$body.append(this.panel.$element);
this.actions.setMode('edit');
this.autoPopulateFields();
};
GetterDialog.prototype.runGetter = async function() {
this.abortController = new AbortController();
try {
const { humanOnly, excludeHumans, includeIll, sortBySurname, formatSelect, updateMode } = this;
const mode = formatSelect.findSelectedItem().getData(), anchor = this.getBestAnchor();
if (!anchor) { mw.notify('Please enter search terms.', { type: 'error' }); return; }
const existing = updateMode.isSelected() ? await this.getExistingLinks() : new Set(), resultsMap = new Map();
const excludeList = this.excludeTags.getValue().map(e => e.toLowerCase()), anchorLC = anchor.toLowerCase();
const compiledCriteria = {
given: this.compileCriteria(this.givenTags.getValue(), anchorLC),
surname: this.compileCriteria(this.surnameTags.getValue(), anchorLC),
middle: this.middleTags.getValue().filter(t => t.toLowerCase() !== anchorLC).map(m => m.toLowerCase())
};
let cont = {};
do {
const data = await this.api.get({ action: 'query', list: 'search', srsearch: `intitle:"${anchor}"`, srlimit: 'max', sroffset: cont.sroffset || 0, format: 'json' });
for (const p of (data.query?.search || [])) {
if (excludeList.some(e => p.title.toLowerCase().includes(e))) continue;
const title = this.normalizeTitle(p.title);
if (!existing.has(title) && this.matchesCriteria(title, compiledCriteria)) resultsMap.set(p.title, { title: p.title, pageid: p.pageid });
}
cont = data.continue || {};
} while (cont.sroffset && !this.abortController?.signal.aborted);
const pageIds = Array.from(resultsMap.values()).map(p => p.pageid), confirmedResults = new Map();
for (let i = 0; i < pageIds.length; i += 50) {
if (this.abortController?.signal.aborted) return;
const chunk = pageIds.slice(i, i + 50), qidList = [], qidMap = {};
const pageData = await this.api.get({ action: 'query', prop: 'pageprops|description', pageids: chunk.join('|'), format: 'json' });
Object.values(pageData.query.pages).forEach(p => { if (p.pageprops?.wikibase_item) { qidList.push(p.pageprops.wikibase_item); qidMap[p.pageprops.wikibase_item] = p; } });
const entities = await checkEntitiesData(qidList, this.abortController?.signal);
for (const qid in entities) {
const ent = entities[qid], p = qidMap[qid];
const isHuman = ent.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5");
if ((humanOnly.isSelected() && !isHuman) || (excludeHumans.isSelected() && isHuman)) continue;
confirmedResults.set(p.title, { title: p.title, desc: p.description || ent.descriptions?.en?.value || '', ent });
}
}
this.resultArea.setValue(Array.from(confirmedResults.values())
.map(p => ({ formatted: this.formatResult(p, mode, includeIll.isSelected()), sortKey: sortBySurname.isSelected() ? this.getSortKey(p.title) : p.title.toLowerCase() }))
.sort((a, b) => a.sortKey.localeCompare(b.sortKey, undefined, { sensitivity: 'base' }))
.map(r => r.formatted).join('\n'));
this.fieldset.$element.hide(); this.resultContainer.$element.show(); this.actions.setMode('result');
} catch (e) { if (e.name !== 'AbortError') { console.error('[Getter] Generator error:', e); mw.notify('Error generating list.', { type: 'error' }); } }
finally { this.abortController = null; }
};
GetterDialog.prototype.getActionProcess = function(action) {
if (action === 'generate') return new OO.ui.Process(async () => { await this.runGetter(); });
if (action === 'back') { if (this.abortController) this.abortController.abort(); return new OO.ui.Process(() => { this.resultContainer.$element.hide(); this.fieldset.$element.show(); this.actions.setMode('edit'); }); }
if (action === 'exit') { if (this.abortController) this.abortController.abort(); return new OO.ui.Process(() => this.close()); }
return GetterDialog.super.prototype.getActionProcess.call(this, action);
};
mw.loader.using(['mediawiki.api', 'mediawiki.util', 'oojs-ui', 'oojs-ui-widgets', 'oojs-ui-windows']).then(() => {
const wm = new OO.ui.WindowManager();
$(document.body).append(wm.$element);
const dialog = new GetterDialog({ size: 'medium' });
wm.addWindows([dialog]);
$(mw.util.addPortletLink('p-tb', '#', 'Getter', 't-getter')).on('click', (e) => { e.preventDefault(); wm.openWindow(dialog); });
});
}(mediaWiki, jQuery, OO));
(function() {
if (window.IllWill) return;
const STRIKEOUT = true;
const HIDE = 'hide';
const IllWill = {
Version: "0.5",
Attribution: 'created by <a target="_blank" href="/wiki/User:Cobaltcigs">cobaltcigs</a>',
Summary: "Added interlanguage links using IllWill.js",
DataApi: "https://www.wikidata.org/w/api.php",
LocalLang: mw.config.get("wgPageContentLanguage"),
Disambiguator: / \(.*\)$/,
SiteLinks: {},
Config: {
MaxLanguages: 10,
IgnoreScientific: STRIKEOUT,
AutoDiff: true
},
css: `
#illwill { clear: both; margin-bottom: 1.5em; border: 1px solid #a2a9b1; padding: 15px; background: #f8f9fa; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.ill-sitelinks a { display: inline-block; margin-bottom: 3px; font-size: 0.9em; color: #36c; }
.ill-size-tag { font-size: 0.8em; color: #72777d; margin-left: 6px; white-space: nowrap; }
.illwill-ignore { text-decoration: line-through; color: #72777d; font-size: 0.85em; }
.ill-x { width: 35px; text-align: center; }
.ill-input, .ill-output { font-family: 'Courier New', monospace; white-space: pre-wrap; font-size: 0.9em; background: #fff; border: 1px solid #eaecf0; padding: 4px; border-radius: 2px; }
#ill-buttons { text-align: right; padding: 10px; border-top: 1px solid #a2a9b1; margin-top: 10px; }
#ill-buttons button { margin-left: 10px; cursor: pointer; padding: 6px 16px; border-radius: 2px; border: 1px solid #a2a9b1; transition: all 0.2s; }
#ill-ok { background: #36c; color: #fff; border-color: #36c !important; font-weight: bold; }
#ill-ok:hover { background: #447ff5; }
#ill-ok:disabled { background: #eaecf0; color: #72777d; border-color: #c8ccd1 !important; cursor: not-allowed; opacity: 0.7; }
#ill-table { width: 100%; border-collapse: collapse; margin-top: 10px; background: white; }
#ill-table th { background: #f2f2f2; text-align: left; padding: 8px; border: 1px solid #a2a9b1; }
#ill-table td { padding: 8px; border: 1px solid #a2a9b1; vertical-align: top; }
#ill-table caption { background: #eaffea; font-weight: bold; padding: 10px; border: 1px solid #a2a9b1; border-bottom: none; font-size: 1.1em; }
#ill-help { font-size: 0.85em; color: #54595d; padding: 10px; line-height: 1.4; }
.ill-loading-inline { font-style: italic; color: #72777d; font-size: 0.9em; display: block; margin: 5px 0; }
`,
setup() {
if (!["edit", "submit"].includes(mw.config.get('wgAction'))) return;
mw.loader.addStyleTag(this.css);
const link = mw.util.addPortletLink('p-cactions', '#', 'IllWill', 'ca-illwill', "Add interlanguage links via Wikidata");
if (link) {
link.addEventListener('click', (e) => {
e.preventDefault();
this.makePanel();
});
}
},
norm(s) {
if (!s) return "";
const t = s.trim().replace(/[\s_]+/g, " ");
return t.charAt(0).toUpperCase() + t.slice(1);
},
async getRedLinks($textbox) {
const txt = $textbox.textSelection('getContents') || "";
const wl = txt.match(/\[\[[^[\]\n]+\]\]/g);
if (!wl) return {
reds: [],
txt
};
const api = new mw.Api();
const textToParse = wl.join("").replace(/\|[^\]]+/g, "");
try {
const data = await api.post({
action: 'parse',
text: textToParse,
contentmodel: 'wikitext',
formatversion: 2
});
const tempDiv = document.createElement('div');
tempDiv.innerHTML = data.parse.text;
const reds = Array.from(tempDiv.querySelectorAll("a.new")).map(x => {
const urlParams = new URLSearchParams(x.href.split('?')[1]);
return urlParams.get('title')?.replace(/_/g, " ");
}).filter(Boolean);
return {
reds: [...new Set(reds)],
txt
};
} catch (e) {
return {
reds: [],
txt
};
}
},
async makePanel() {
const $editform = $('#editform');
const $textbox = $('#wpTextbox1');
$('#illwill').remove();
$editform.hide();
const $wrapper = $('<div id="illwill">Loading redlinks...</div>').insertBefore($editform);
const {
reds,
txt
} = await this.getRedLinks($textbox);
const wtLinks = [];
reds.forEach(title => {
const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\s/g, "[\\s_]+");
const re = new RegExp("\\[\\[\\s*" + escaped + "\\s*(?:\\|[^\\[\\]]+)?\\]\\]", "ig");
const match = txt.match(re);
if (match && !wtLinks.some(z => z.title === title)) {
wtLinks.push({
link: match[0],
title
});
}
});
if (!wtLinks.length) {
const $back = $('<button>Quit</button>').on('click', () => {
$wrapper.remove();
$editform.show();
});
$wrapper.empty().append($('<strong>No valid redlinks found in text.</strong> '), $back);
return;
}
const $table = $(`
<table id="ill-table" class="wikitable">
<caption>IllWill.js (v${this.Version})</caption>
<thead>
<tr>
<th class="ill-x"><input type="checkbox" id="ill-select-all" disabled></th>
<th>Input link</th>
<th>Possible Wikidata matches</th>
<th>Sitelinks (top ${this.Config.MaxLanguages} by size)</th>
<th>Resulting wikitext</th>
</tr>
</thead>
<tbody></tbody>
<tfoot>
<tr>
<td colspan="3" id="ill-help">Carefully examine possible Wikidata matches.</td>
<td colspan="2" id="ill-buttons">
<button id="ill-cancel">Cancel</button>
<button id="ill-ok" disabled>Show changes</button>
</td>
</tr>
</tfoot>
</table>
`);
const $tbody = $table.find('tbody');
wtLinks.forEach((item, i) => {
$tbody.append(`
<tr id="ill-row-${i}" data-index="${i}">
<td class="ill-x"><input type="checkbox" class="ill-row-check" disabled></td>
<td class="ill-input">${this.escapeHTML(item.link)}</td>
<td class="ill-wd-item"><span class="ill-loading-inline">Searching...</span></td>
<td class="ill-sitelinks"></td>
<td class="ill-output"></td>
</tr>
`);
});
$wrapper.empty().append($table);
const chunkSize = 5;
for (let i = 0; i < wtLinks.length; i += chunkSize) {
const chunk = wtLinks.slice(i, i + chunkSize);
await Promise.all(chunk.map((item, idx) => this.fetchWikidataOptimized(item.title, i + idx)));
}
$table.on('click', '#ill-cancel', () => {
$editform.show();
$wrapper.remove();
});
$table.on('click', '#ill-ok', () => this.onOkButton($editform, $wrapper, $textbox));
$table.on('change', '#ill-select-all', (e) => {
const isChecked = $(e.target).is(':checked');
$table.find('.ill-row-check:not(:disabled)').each(function() {
$(this).prop('checked', isChecked).trigger('change');
});
});
$table.on('change', '.ill-row-check', (e) => {
this.onCheckbox(e);
this.updateOkButtonState($table);
});
$table.on('change', 'input[type="radio"]', (e) => {
this.showSiteLinks(e);
});
},
async fetchWikidataOptimized(title, index) {
const query = title.replace(this.Disambiguator, "");
const url = `${this.DataApi}?action=wbsearchentities&search=${encodeURIComponent(query)}&language=${this.LocalLang}&limit=5&format=json&origin=*`;
try {
const resp = await fetch(url);
const data = await resp.json();
const results = data.search;
const $cell = $(`#ill-row-${index} .ill-wd-item`);
if (!results || !results.length) {
$cell.html('<span class="ill-na">(no results)</span>');
return;
}
const entityIds = results.map(r => r.id);
await this.fetchEntities(entityIds, index, $cell);
} catch (e) {
$(`#ill-row-${index} .ill-wd-item`).text("Search failed.");
}
},
async fetchEntities(ids, rowIndex, $cell) {
const url = `${this.DataApi}?action=wbgetentities&ids=${ids.join('|')}&props=labels|descriptions|claims|sitelinks&format=json&origin=*`;
try {
const resp = await fetch(url);
const data = await resp.json();
$cell.empty();
ids.forEach(qNum => {
const entity = data.entities[qNum];
if (!entity) return;
const isScientific = entity.claims?.P31?.some(s => s.mainsnak?.datavalue?.value?.id === "Q13442814");
if (isScientific && this.Config.IgnoreScientific === HIDE) return;
const desc = entity.descriptions?.[this.LocalLang]?.value || entity.descriptions?.en?.value || "";
const label = entity.labels?.[this.LocalLang]?.value || entity.labels?.en?.value || qNum;
const isStrikeout = isScientific && this.Config.IgnoreScientific === STRIKEOUT;
const $div = $(`
<div class="${isStrikeout ? 'illwill-ignore' : ''}">
<input type="radio" name="selection-${rowIndex}" value="${qNum}" id="radio-${qNum}" ${isStrikeout ? 'disabled' : ''}>
<label for="radio-${qNum}">
<a href="https://wikidata.org/wiki/${qNum}" target="_blank">${this.escapeHTML(label)}</a>:
<span class="ill-desc">${desc ? this.escapeHTML(desc) : '<i>no description</i>'}</span>
</label>
</div>
`);
$cell.append($div);
this.SiteLinks[qNum] = entity.sitelinks || {};
});
} catch (e) {
$cell.text("Detail fetch failed.");
}
},
updateOkButtonState($table) {
const $rows = $table.find('.ill-row-check:not(:disabled)');
const $checkedRows = $rows.filter(':checked');
const anyEnabled = $rows.length > 0;
$table.find('#ill-ok').prop('disabled', $checkedRows.length === 0);
const $master = $table.find('#ill-select-all');
$master.prop('disabled', !anyEnabled);
if (anyEnabled && $rows.length === $checkedRows.length) {
$master.prop('checked', true);
} else {
$master.prop('checked', false);
}
},
async showSiteLinks(e) {
const isManualToggling = e.fromCheckbox || false;
const qNum = e.target.value;
const $row = $(e.target).closest('tr');
const sitelinks = this.SiteLinks[qNum];
const $checkbox = $row.find('.ill-row-check');
const $sitelinkCell = $row.find('.ill-sitelinks');
const $outputCell = $row.find('.ill-output');
const $table = $('#ill-table');
const effectiveMax = Math.min(Math.max(this.Config.MaxLanguages, 3), 6);
const validKeys = Object.keys(sitelinks).filter(k => k.endsWith('wiki') && !k.includes('commons'));
$sitelinkCell.html('<span class="ill-loading-inline">Measuring articles...</span>');
$outputCell.empty();
if (!isManualToggling) {
$checkbox.prop('disabled', true).prop('checked', false);
}
const candidates = validKeys.slice(0, 25);
const sizeMap = [];
await Promise.all(candidates.map(async (key) => {
const lang = key.replace('wiki', '').replace(/_/g, '-');
const title = sitelinks[key].title;
const endpoint = `https://${lang}.wikipedia.org/w/api.php?action=query&titles=${encodeURIComponent(title)}&prop=info&format=json&origin=*`;
try {
const res = await fetch(endpoint).then(r => r.json());
const page = Object.values(res.query.pages)[0];
sizeMap.push({
lang,
title,
length: page.length || 0
});
} catch (err) {
sizeMap.push({
lang,
title,
length: 0
});
}
}));
sizeMap.sort((a, b) => b.length - a.length);
const topLinks = sizeMap.slice(0, effectiveMax);
$sitelinkCell.html(topLinks.map(item => {
const kbValue = Math.round(item.length / 1024);
return `<div><a href="https://${item.lang}.wikipedia.org/wiki/${encodeURIComponent(item.title)}" target="_blank">${item.lang}:${this.escapeHTML(item.title)}</a> <span class="ill-size-tag">(${kbValue} kB)</span></div>`;
}).join(""));
const inputWikitext = $row.find('.ill-input').text();
const match = inputWikitext.match(/\[\[(.*?)\]\]/);
if (match) {
const parts = match[1].split('|');
const targetPage = parts[0].trim();
const surfaceName = (parts[1] || parts[0]).trim();
const illLangs = topLinks.map(item => {
const isSameTitle = this.norm(item.title) === this.norm(targetPage);
return `|${item.lang}|${isSameTitle ? '' : item.title}`;
}).join("");
const ltParam = (this.norm(surfaceName) === this.norm(targetPage)) ? "" : `|lt=${surfaceName}`;
$outputCell.text(`{{ill|${targetPage}${ltParam}${illLangs}}}`);
}
$checkbox.prop('disabled', !topLinks.length);
if (!isManualToggling) {
$checkbox.prop('checked', !!topLinks.length);
}
this.updateOkButtonState($table);
},
onCheckbox(e) {
const $row = $(e.target).closest('tr');
const $radio = $row.find('input[type="radio"]:checked');
const isChecked = $(e.target).is(':checked');
if (isChecked) {
if ($radio.length) {
this.showSiteLinks({
target: $radio[0],
fromCheckbox: true
});
}
} else {
$row.find('.ill-sitelinks, .ill-output').empty();
}
},
onOkButton($editform, $wrapper, $textbox) {
let text = $textbox.textSelection('getContents');
let changed = false;
$wrapper.find('.ill-row-check:checked').each(function() {
const $row = $(this).closest('tr');
const input = $row.find('.ill-input').text();
const output = $row.find('.ill-output').text();
if (output) {
const escapedInput = input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
text = text.replace(new RegExp(escapedInput, "gi"), output);
changed = true;
}
});
if (changed) {
$('#wpSummary').val((i, v) => (v ? v + "; " : "") + this.Summary);
$textbox.textSelection('setContents', text);
$wrapper.remove();
$editform.show();
if (this.Config.AutoDiff) $('#wpDiff').click();
}
},
escapeHTML(str) {
return str.replace(/[&<>"']/g, m => ({
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
})[m]);
}
};
window.IllWill = IllWill;
mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => {
$(() => IllWill.setup());
});
})();
/* global ComradeLib, mw, $ */
(async function() {
try {
await mw.loader.getScript('https://test.wikipedia.org/w/index.php?title=User:Sam_Sailor/test.js/comrade-options.js&action=raw&ctype=text/javascript');
} catch (e) {
console.warn("Comrade: Could not load /comrade-options.js, using fallback defaults.", e);
}
window.ComradeOptions = Object.assign({
version: "0.5.0",
excludedTemplates: [],
nudgeTimeout: 10000,
debugMode: false
}, window.ComradeOptions || {});
await mw.loader.getScript('https://test.wikipedia.org/w/index.php?title=User:Sam_Sailor/test.js/comrade-lib.js&action=raw&ctype=text/javascript');
async function getTargetPage(api, name, type) {
const titles = type === 'surname' ? [`List of people with the English surname ${name}`, `List of people with surname ${name}`, `${name} (surname)`, `${name} (name)`, name] : [`List of people with given name ${name}`, `List of people named ${name}`, `${name} (given name)`, `${name} (name)`, name];
const res = await api.get({
action: 'query',
titles: titles.join('|'),
formatversion: 2
});
if (!res.query || !res.query.pages) return null;
return titles.find(t => {
const pg = res.query.pages.find(p => p.title === t);
return pg && !pg.missing;
});
}
async function processHumanNameLists(api, entity, wikitext, currentTitle, hasShortDesc, isOrphan, tClean) {
const {
givens,
surnames
} = await fetchWikidataNameLabels(entity);
const nameParts = tClean.split(' ');
const hasFamilyNameHatnote = /\{\{family[ _]name[ _]hatnote/i.test(wikitext);
const primarySurname = hasFamilyNameHatnote ? nameParts[0] : (getSmartSortKey(tClean, entity).split(',')[0] || nameParts[nameParts.length - 1]);
for (const name of givens) {
if (name.toLowerCase() === primarySurname.toLowerCase()) continue;
const nameInTitle = new RegExp('\\b' + name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b', 'i').test(tClean);
if (!nameInTitle) continue;
const target = await getTargetPage(api, name, 'given');
if (target) await verifyAndNudge(api, name, 'given name', currentTitle, hasShortDesc, isOrphan, target);
}
const finalSurnames = surnames.length > 0 ? surnames : (nameParts.length >= 2 ? [nameParts[nameParts.length - 1]] : []);
for (const name of finalSurnames) {
const nameInTitle = new RegExp('\\b' + name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b', 'i').test(tClean);
if (!nameInTitle) continue;
const target = await getTargetPage(api, name, 'surname');
if (target) await verifyAndNudge(api, name, 'surname', currentTitle, hasShortDesc, isOrphan, target);
}
}
async function verifyAndNudge(api, name, type, currentTitle, hasShortDesc, isOrphan, foundTitle) {
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: foundTitle,
rvprop: 'content',
formatversion: 2
});
const page = res.query.pages[0];
if (!page || page.missing || !page.revisions) return;
const text = page.revisions[0].content;
const isNameMatch = /\{\{(Surname|Hndis|Given[ _]name|Forename|Givenname)/i.test(text);
const isNameDab = /\{\{(Disambiguation|Dab|Disambig)(?:[^}]*\|(surname|surnames|given[ _]name|given[ _]names)|(?:\s*\}\}))/i.test(text);
const isNamePageTitle = foundTitle.includes('List of people with');
if (!isNameMatch && !isNameDab && !isNamePageTitle) return;
const escapedTitle = currentTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/ /g, '[ _]');
const alreadyListed = new RegExp('(\\[\\[|\\{\\{anbl\\|[ _]*|\\|)' + escapedTitle, 'i').test(text);
if (alreadyListed) return;
const $container = $('<div>').addClass('comrade-container').addClass(isOrphan ? 'comrade-nudge' : 'comrade-info');
const $header = $('<div>').addClass('comrade-header').append($('<strong>').text(isOrphan ? 'Comrade nudge:' : 'Informative:'));
const $content = $('<div>').addClass('comrade-content');
$content.append($('<span>').append(`${type.charAt(0).toUpperCase() + type.slice(1)} not listed at `, $('<a>').attr({
href: mw.util.getUrl(foundTitle),
target: '_blank'
}).text(foundTitle), "; should it be?"));
const snippet = '* {{anbl|' + currentTitle + '}}';
const $snippetWrapper = $('<div>').css({
'display': 'flex',
'align-items': 'center',
'gap': '10px',
'margin-top': '4px'
});
const $instructionSpan = $('<span>').append('Consider adding ', $('<code>').text(snippet));
const $copyBtn = $('<button>').text('Copy & insert').on('click', function() {
navigator.clipboard.writeText('\n' + snippet).then(() => {
$(this).text('Copied!').prop('disabled', true);
const editUrl = mw.util.getUrl(foundTitle, {
action: 'edit',
summary: 'Adding * {{[[Template:Annotated biography link|anbl]]|[[' + currentTitle + ']]}}'
});
window.open(editUrl, '_blank');
});
});
$snippetWrapper.append($instructionSpan, $copyBtn);
$content.append($snippetWrapper);
if (!hasShortDesc) {
$content.append($('<span>').css({
'color': '#d33',
'font-weight': 'bold',
'margin-top': '5px'
}).text('⚠️ Missing short description: Please add a concise one before listing.'));
}
$container.append($header, $content);
ComradeLib.appendDismiss($container);
}
async function checkNamingPrecision(currentTitle, isHuman) {
const disambigMatch = currentTitle.match(/^(.+?)(?:, | \()/);
if (isHuman || !disambigMatch) return;
const baseTitle = disambigMatch[1].trim();
const api = new mw.Api();
try {
const baseRes = await api.get({
action: 'query',
titles: baseTitle,
redirects: 1,
formatversion: 2
});
const page = baseRes.query.pages[0];
const baseExists = !page.missing;
let baseRedirectsToSelf = false;
if (baseRes.query.redirects) {
baseRedirectsToSelf = baseRes.query.redirects.some(r => r.to === currentTitle);
}
const searchRes = await api.get({
action: 'query',
list: 'allpages',
apprefix: baseTitle,
aplimit: 50,
formatversion: 2
});
const competitors = searchRes.query.allpages.filter(p => {
const t = p.title;
return t !== currentTitle && t !== baseTitle && (t.startsWith(baseTitle + ', ') || t.startsWith(baseTitle + ' ('));
});
if (!baseExists || baseRedirectsToSelf) {
const $container = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $content = $('<div>').addClass('comrade-content');
if (competitors.length === 0) {
$header.append($('<strong>').text('Precision:'));
const reason = baseRedirectsToSelf ? "already redirects here" : "is free";
$content.append($('<span>').append(`The base name `, $('<code>').text(baseTitle), ` ${reason}. Consider moving this article.`));
const moveUrl = mw.util.getUrl('Special:MovePage/' + currentTitle, {
wpNewTitle: baseTitle,
wpReason: 'Unnecessary disambiguation; base name is free/redirects here per [[WP:PRECISION]]'
});
$content.append($('<button>').text(`Move to ${baseTitle}`).on('click', () => window.open(moveUrl, '_blank')));
} else {
$header.append($('<strong>').text('Observation:'));
const state = baseRedirectsToSelf ? 'redirect to this page' : 'redlink';
$content.append($('<span>').append(`Base name `, $('<code>').text(baseTitle), ` has ${competitors.length} competitors, but it is currently a ${state}.`));
$content.append($('<span>').text('Consider if a disambiguation page is needed.'));
}
$container.append($header, $content);
ComradeLib.appendDismiss($container);
}
} catch (e) {
console.warn("Comrade: Precision check failed", e);
}
}
async function checkHumanSort(wikitext, entity, currentTitle, tClean, refName) {
const dsMatch = wikitext.match(/\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/i);
const {
givens
} = await fetchWikidataNameLabels(entity);
if (dsMatch) {
const currentDS = dsMatch[1].trim();
if (currentDS.includes(',')) {
let [lastPart, firstPart] = currentDS.split(',').map(s => s.trim());
if (firstPart.toLowerCase().endsWith(lastPart.toLowerCase())) {
let fixedFirst = firstPart.substring(0, firstPart.length - lastPart.length).trim();
let suggestion = `${fixNameCasing(lastPart, refName)}, ${fixNameCasing(fixedFirst, refName)}`;
if (suggestion !== currentDS) renderSortNudge(suggestion, "Redundant surname in given name field.");
} else if (givens.length > 0 && lastPart.split(' ').length > 1) {
const parts = lastPart.split(' ');
const misplacedGiven = parts.find(part => givens.some(gn => gn.toLowerCase() === part.toLowerCase()));
if (misplacedGiven) {
const newSurname = parts.filter(p => p !== misplacedGiven).join(' ');
let suggestion = `${fixNameCasing(newSurname, refName)}, ${fixNameCasing(misplacedGiven + ' ' + firstPart, refName)}`;
if (suggestion !== currentDS) renderSortNudge(suggestion, `Wikidata suggests "${misplacedGiven}" is a given name.`);
}
}
}
} else {
const hasFamilyNameHatnote = /\{\{family[ _]name[ _]hatnote/i.test(wikitext);
const nameParts = tClean.split(' ');
const targetSortName = hasFamilyNameHatnote ? `${nameParts[0]}, ${nameParts.slice(1).join(' ')}` : getSmartSortKey(tClean, entity);
if (targetSortName.toLowerCase() !== currentTitle.toLowerCase()) {
const $dsMissing = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Add: {{DEFAULTSORT:${targetSortName}}}`).on('click', () => performSortCorrection(targetSortName, "add"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Tag is missing. `, $btn));
$dsMissing.append($header);
ComradeLib.appendDismiss($dsMissing);
}
}
}
async function fetchWikidataEntity(qid) {
const res = await fetch(`https://www.wikidata.org/w/api.php?action=wbgetentities&ids=${qid}&props=claims|descriptions|labels&languages=en&format=json&origin=*`).then(r => r.json());
return res.entities[qid];
}
function getSmartSortKey(fullName, entity) {
const countryIds = entity.claims?.P27?.map(c => c.mainsnak?.datavalue?.value?.id) || [];
const locationIds = entity.claims?.P17?.map(c => c.mainsnak?.datavalue?.value?.id) || [];
const placeIds = [...(entity.claims?.P19 || []), ...(entity.claims?.P119 || [])].map(c => c.mainsnak?.datavalue?.value?.id).filter(Boolean);
const allRelevantIds = [...countryIds, ...locationIds, ...placeIds];
const birthYear = parseInt(entity.claims?.P569?.[0]?.mainsnak?.datavalue?.value?.time?.match(/[+-](\d{4})/)?.[1]) || null;
if (fullName.startsWith("Saint ")) return fullName.replace("Saint ", "");
const arabicParticles = /\b(Abu|Abd|Abdel|Abdul|ben|bin|bint)\b/i;
if (fullName.match(arabicParticles)) {
if (birthYear && birthYear > 1900) {
const parts = fullName.split(' ');
const binIndex = parts.findIndex(p => p.toLowerCase() === 'bin' || p.toLowerCase() === 'ben');
if (binIndex > 0) return parts.slice(binIndex).join(' ') + ', ' + parts.slice(0, binIndex).join(' ');
} else {
return fullName;
}
}
const eastAsianQids = ["Q148", "Q865", "Q884", "Q424", "Q17", "Q881"];
if (allRelevantIds.some(id => eastAsianQids.includes(id))) {
if (allRelevantIds.includes("Q17") && birthYear && birthYear > 1885) return westernSort(fullName);
const parts = fullName.split(' ');
if (parts.length > 1) return `${parts[0]}, ${parts.slice(1).join(' ')}`;
}
return westernSort(fullName);
}
function westernSort(name) {
let cleanName = name;
if (name.startsWith("O'")) cleanName = name.replace("O'", "O");
const parts = cleanName.split(' ');
if (parts.length < 2) return cleanName;
const surname = parts.pop();
if (["Jr.", "Jr", "III", "II", "Sr.", "Sr"].includes(surname)) {
const actualSurname = parts.pop();
return `${actualSurname}, ${parts.join(' ')} ${surname}`;
}
return `${surname}, ${parts.join(' ')}`;
}
async function performSortCorrection(newName, type) {
const api = new mw.Api();
const title = mw.config.get("wgPageName");
try {
const data = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
let content = data.query.pages[0].revisions[0].content;
const dsRegex = /\{\{\s*(?:DEFAULTSORT|Defaultsort)\s*:[^}]+\}\}/i;
const newTag = `{{DEFAULTSORT:${newName}}}`;
if (dsRegex.test(content)) {
content = content.replace(dsRegex, newTag);
} else {
const catRegex = /\[\[Category:/i;
content = catRegex.test(content) ? content.replace(catRegex, `${newTag}\n[[Category:`) : content.trim() + `\n\n${newTag}`;
}
await api.postWithEditToken({
action: 'edit',
title: title,
text: content,
summary: `Setting DEFAULTSORT to ${newName}`,
nocreate: true
});
mw.notify(`DEFAULTSORT ${type === "add" ? "added" : "corrected"} to ${newName}!`, {
title: 'Comrade Success',
type: 'success'
});
setTimeout(() => location.reload(), 700);
} catch (err) {
mw.notify('Failed to save DEFAULTSORT.', {
type: 'error'
});
}
}
function renderSortNudge(target, reason) {
const $dsNudge = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Correct to: ${target}`).on('click', () => performSortCorrection(target, "format"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` ${reason} `, $btn));
$dsNudge.append($header);
ComradeLib.appendDismiss($dsNudge);
}
async function fetchWikidataNameLabels(entity) {
const familyIds = entity.claims?.P734?.map(c => c.mainsnak.datavalue.value.id) || [];
const givenIds = entity.claims?.P735?.map(c => c.mainsnak.datavalue.value.id) || [];
const allNameIds = [...new Set([...familyIds, ...givenIds])];
if (allNameIds.length === 0) return {
givens: [],
surnames: []
};
const url = new URL("https://www.wikidata.org/w/api.php");
url.search = new URLSearchParams({
action: 'wbgetentities',
ids: allNameIds.join('|'),
props: 'labels',
languages: 'en',
format: 'json',
origin: '*'
});
const nameData = await fetch(url).then(r => r.json());
return {
givens: givenIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean),
surnames: familyIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean)
};
}
function fixNameCasing(part, referenceName) {
return part.split(' ').map(word => {
const match = referenceName.match(new RegExp(`\\b${word}\\b`, 'i'));
if (match) return match[0];
return referenceName.split(' ').find(tWord => word.toLowerCase().startsWith(tWord.toLowerCase()) || tWord.toLowerCase().startsWith(word.toLowerCase())) || word;
}).join(' ');
}
async function init() {
const api = new mw.Api();
const ns = mw.config.get("wgNamespaceNumber");
const action = mw.config.get("wgAction");
if (mw.config.get("wgDBname") !== "enwiki" || action !== "view") return;
if (ns === 14) {
const $btn = $('<button>').attr('id', 'comrade-audit-btn').text('Audit category quality').css({
'margin-bottom': '10px',
'padding': '5px 10px',
'cursor': 'pointer'
}).on('click', ComradeLib.performCategoryAudit);
$('.mw-category-generated').prepend($btn);
return;
}
if (ns !== 0) return;
const qid = mw.config.get("wgWikibaseItemId");
const currentTitle = mw.config.get("wgTitle");
try {
const [pageData, backlinkData] = await Promise.all([
api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get("wgPageName"),
rvprop: 'content',
formatversion: 2
}),
api.get({
action: 'query',
list: 'backlinks',
blfilterredir: 'nonredirects',
bllimit: 50,
blnamespace: 0,
bltitle: mw.config.get("wgPageName"),
formatversion: 2
})
]);
const page = pageData.query.pages[0];
if (!page || page.missing || !page.revisions) return;
const wikitext = page.revisions[0].content;
ComradeLib.checkLeadCapitalization(currentTitle);
// Ligature/ASCII-only/diacritic and domain
const norms = ComradeLib.getNormalizedTitles(currentTitle);
if (norms.ascii) {
let label = "ASCII-only";
let category = "R from ASCII-only";
if (/[æÆœŒijIJßẞ]/.test(currentTitle)) {
label = "Ligature";
category = "R to ligature";
} else if (currentTitle.normalize("NFD").match(/[\u0300-\u036f]/)) {
label = "Diacritic";
category = "R to diacritic";
}
await ComradeLib.checkAndRenderRedirect(norms.ascii, currentTitle, category, label);
}
await ComradeLib.checkDomainRedirect(qid, currentTitle);
// Orphan logic
const isOrphanTagged = /\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)/i.test(wikitext) || /\| *orphan *=/i.test(wikitext);
const backLinkCount = backlinkData.query.backlinks.length;
if (isOrphanTagged && backLinkCount >= 1) {
const $container = $('<div>').addClass('comrade-container comrade-status');
const $btn = $('<button>').text('Remove orphan tag').on('click', async () => await ComradeLib.performDeorphan());
const $statusText = $('<span>').append($('<strong>').text('Status:'), ` Article has ${backLinkCount} backlink(s).`, $btn);
$container.append($('<div>').addClass('comrade-header').append($statusText));
ComradeLib.appendDismiss($container);
}
// Wikidata/human logic
let entity = qid ? await fetchWikidataEntity(qid) : null;
let isHuman = entity?.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5");
await checkNamingPrecision(currentTitle, isHuman);
if (isHuman && entity) {
const tClean = currentTitle.replace(/\s*\(.*?\)\s*/g, '').trim();
const refName = entity.labels?.en?.value || currentTitle;
const hasFamilyNameHatnote = /\{\{family[ _]name[ _]hatnote/i.test(wikitext);
const hasShortDesc = /\{\{\s*[Ss]hort description/i.test(wikitext);
const isOrphan = isOrphanTagged && backLinkCount < 1;
await ComradeLib.checkShortDescDates(wikitext, entity);
await ComradeLib.checkNoShortDesc(wikitext, entity);
// Check DEFAULTSORT
await checkHumanSort(wikitext, entity, currentTitle, tClean, refName);
const longName = ComradeLib.getLongNameFromWikitext(wikitext);
if (longName && longName.length > tClean.length) {
let sortKey = hasFamilyNameHatnote ? `${longName.split(' ')[0]}, ${longName.split(' ').slice(1).join(' ')}` : getSmartSortKey(longName, entity);
const rLongWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from long name}}\n}}\n{{DEFAULTSORT:${sortKey}}}`;
await ComradeLib.checkAndRenderRedirect(longName, currentTitle, null, "Long name", rLongWikitext);
}
// Sort name R
const targetSortName = hasFamilyNameHatnote ? `${tClean.split(' ')[0]}, ${tClean.split(' ').slice(1).join(' ')}` : getSmartSortKey(tClean, entity);
if (targetSortName && targetSortName.toLowerCase() !== currentTitle.toLowerCase()) {
let targetPage = currentTitle.includes(' (') ? currentTitle.split(' (')[0] : currentTitle;
let rCat = targetPage !== currentTitle ? "R from ambiguous sort name" : "R from sort name";
if (targetSortName.includes(',')) {
const p = targetSortName.split(',');
rCat += `|${p[0].trim().charAt(0).toUpperCase()}|${p[1].trim().charAt(0).toUpperCase()}`;
}
await ComradeLib.checkAndRenderRedirect(targetSortName, targetPage, rCat, "Sort name");
}
// Name lists
await processHumanNameLists(api, entity, wikitext, currentTitle, hasShortDesc, isOrphan, tClean);
}
// Quality/ORES
const isRedirect = !!mw.config.get("wgIsRedirect");
const userExclusions = window.comradeQualityExclusions || [];
const allExclusions = [...window.ComradeOptions.excludedTemplates, ...userExclusions];
const exclusionRegexString = allExclusions.map(t => t.replace("Template:", "").replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
const exclusionRegex = new RegExp(exclusionRegexString, 'i');
if (isRedirect || exclusionRegex.test(wikitext)) {
console.log("Comrade check skipped: Page is a redirect or excluded via options.");
return;
}
ComradeLib.checkQualityConsistency();
} catch (err) {
console.error("Comrade error:", err);
}
}
init();
})();
(function() {
'use strict';
if (mw.config.get('wgCanonicalSpecialPageName') !== 'Search') {
return;
}
mw.loader.using(['mediawiki.util']).then(function() {
$(function() {
const $resultsInfo = $('.results-info');
const totalResultsRaw = $resultsInfo.attr('data-mw-num-results-total') || '0';
function formatWithCommas(numberString) {
return numberString.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
const state = {
language: mw.config.get('wgContentLanguage'),
totalResultsFormatted: formatWithCommas(totalResultsRaw)
};
let titlesCache = null;
function extractResultTitles() {
const currentResults = $('.mw-search-result-heading');
if (!titlesCache || titlesCache.length !== currentResults.length) {
titlesCache = currentResults.map(function() {
return $(this).find('a:first').attr('title');
}).get().filter(Boolean);
}
return titlesCache;
}
mw.hook("wikipage.content").add(function() {
titlesCache = null;
});
function addLimitOptions() {
const selects = document.querySelectorAll('.cdx-table-pager__limit-form select[name="limit"]');
if (!selects.length) return;
const valuesToAdd = ["1000", "2500", "5000"];
selects.forEach(function(select) {
valuesToAdd.forEach(function(value) {
if (select.querySelector('option[value="' + value + '"]')) return;
const option = document.createElement("option");
option.value = value;
option.textContent = formatWithCommas(value) + " items";
const existingOptions = Array.from(select.querySelectorAll("option"));
const insertBefore = existingOptions.find(function(opt) {
return Number(opt.value) > Number(value);
});
if (insertBefore) {
select.insertBefore(option, insertBefore);
} else {
select.appendChild(option);
}
});
});
}
mw.hook("wikipage.content").add(addLimitOptions);
const numLinks = $('.mw-numlink');
if (numLinks.length > 4 && !numLinks.eq(4).next().is('a[href*="limit=5000"]')) {
const $firstAnchor = numLinks.eq(4);
const $firstSpan = $('<span> | </span>').insertAfter($firstAnchor);
const url1 = new URL(window.location.href);
url1.searchParams.set('limit', '5000');
$('<a>', {
text: '5000',
title: 'Show 5000 results per page',
href: url1.toString()
}).insertAfter($firstAnchor).before($firstSpan);
}
if (numLinks.length > 9 && !numLinks.eq(9).next().is('a[href*="limit=5000"]')) {
const $secondAnchor = numLinks.eq(9);
const $secondSpan = $('<span> | </span>').insertAfter($secondAnchor);
const url2 = new URL(window.location.href);
url2.searchParams.set('limit', '5000');
$('<a>', {
text: '5000',
title: 'Show 5000 results per page',
href: url2.toString()
}).insertAfter($secondAnchor).before($secondSpan);
}
const $container = $("<div></div>").addClass("enhanced-search-container").hover(function() {
$(this).addClass("hover");
}, function() {
$(this).removeClass("hover");
});
const interwikiResults = document.getElementById("mw-interwiki-results");
if (interwikiResults) {
$container.addClass("enhanced-search-floating").insertBefore("#mw-interwiki-results");
} else {
$container.addClass("enhanced-search-absolute").insertBefore(".searchresults");
}
const totalFormattedStr = state.totalResultsFormatted;
const $titleElement = $("<div>Show a shortened list of<br />this page's " + $('.mw-search-result-heading').length + " results <small>(of total of " + totalFormattedStr + ")</small></div>").addClass("enhanced-header").appendTo($container);
const $contentArea = $("<div></div>").addClass("enhanced-content-area").appendTo($container).hide();
const $note1 = $("<div>Copy this page\'s " + $('.mw-search-result-heading').length + " results to the clipboard as...</div>").addClass("enhanced-note").insertAfter($container).hide();
function setupHoverTooltip(element, message) {
$(element).attr('title', message).hover(function() {
$(this).addClass("hover");
}, function() {
$(this).removeClass("hover");
});
}
const $note2 = $("<div>...plain text<sup> (?)</sup></div>").addClass("enhanced-btn").insertAfter($note1).hide();
setupHoverTooltip($note2, 'This button copies this page\'s results to the clipboard as plain text, i.e. without wikitext markup. Since this copies only the results shown on this search page, make sure you have clicked the "view 20, 50, 100, 250, 500, 5000" link. This format is compatible with PetScan and AutoWikiBrowser (AWB) for example.');
const $note3 = $("<div>...wikitext<sup> (?)</sup></div>").addClass("enhanced-btn").insertAfter($note2).hide();
setupHoverTooltip($note3, 'This button copies this page\'s results to the clipboard and also adds wikitext markup.');
const $note4 = $("<div>...PetScan URL<sup> (?)</sup></div>").addClass("enhanced-btn").insertAfter($note3).hide();
setupHoverTooltip($note4, 'This button copies a PetScan URL with this page\'s results to the clipboard. Note that if the PetScan URL becomes too long (~250+ pages), the URL might not work.');
const $note5 = $("<div>...or this search query's total results of " + totalFormattedStr + "<sup> (?)</sup> as...</div>").addClass("enhanced-note").insertAfter($note4).hide().attr('title', 'Note that the actual limit is 10,000, which is MediaWiki\'s maximum limit.').hover(function() {
$(this).addClass("hover");
}, function() {
$(this).removeClass("hover");
});
const $note6 = $("<div>...PetScan URL<sup> (?)</sup></div>").addClass("enhanced-btn").insertAfter($note5).hide();
setupHoverTooltip($note6, 'This button copies a PetScan URL with all ' + totalFormattedStr + ' results to the clipboard. Note that the maximum limit is 10,000 due to MediaWiki\'s restriction.');
const $note7 = $("<div><sup>(?)</sup> = hover over for tooltip details</div>").addClass("enhanced-note-right").insertAfter($note6).hide();
const noteElements = [$note1, $note2, $note3, $note4, $note5, $note6, $note7];
if (interwikiResults !== null) {
noteElements.forEach($el => $el.addClass("enhanced-floating-elements"));
} else {
noteElements.forEach(function($el, index) {
$el.addClass("enhanced-absolute-elements enhanced-position-" + index);
});
}
function formatPlain() {
return extractResultTitles().join("\r\n");
}
function formatWikitext() {
const titles = extractResultTitles().map(title => "* [[" + title + "]]");
const header = "This is a list of [" + window.location.href + " these search results] (made with the script [[User:JAAqqO/EnhancedSearchPage|EnhancedSearchPage]]):\r\n";
return header + titles.join("\r\n");
}
function formatPetScan() {
const encodedTitles = extractResultTitles().map(encodeURIComponent).join("%0A");
return "https://petscan.wmflabs.org/?active_tab=tab_other_sources&manual_list=" + encodedTitles + "&manual_list_wiki=" + state.language + "wiki&project=wikipedia&language=" + state.language + "&doit=";
}
function formatPetScanTotal() {
const oouiInput = document.getElementById("ooui-php-1");
const value = oouiInput ? oouiInput.value : "";
if (value) {
return "https://petscan.wmflabs.org/?active_tab=tab_other_sources&ns%5B0%5D=1&search_query=" + encodeURIComponent(value) + "&search_wiki=" + state.language + "wiki&search_max_results=10000&project=wikipedia&language=" + state.language + "&doit=";
}
return "";
}
function copyToClipboard(text) {
if (!text) return;
if (!navigator.clipboard) {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.setAttribute('readonly', '');
textarea.style.position = 'absolute';
textarea.style.left = '-9999px';
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
} catch (e) {
console.error('Fallback copy failed', e);
}
textarea.remove();
} else {
navigator.clipboard.writeText(text).catch(function(err) {
console.error("Copy failed: ", err);
});
}
}
function showCopiedMessage(event) {
const posX = event.pageX;
const posY = event.pageY;
const $msg = $("<div class='copied-message'>Copied!</div>").appendTo("body");
$msg.css({
position: "absolute",
top: (posY + 10) + "px",
left: (posX + 10) + "px",
backgroundColor: "#f0f0f0",
border: "1px solid grey",
padding: "5px 10px",
borderRadius: "5px",
zIndex: 9999
});
setTimeout(function() {
$msg.fadeOut("slow", function() {
$(this).remove();
});
}, 500);
}
function performButtonAnimation($element) {
$element.css("transform", "scaleY(0.9)").animate({
opacity: '0.5'
}, "fast", function() {
$(this).animate({
opacity: '1'
}, "slow").css("transform", "scaleY(1)");
});
}
let isExpanded = false;
function showContentArea() {
if (isExpanded) return;
isExpanded = true;
const currentResults = $('.mw-search-result-heading');
$contentArea.empty().append(currentResults.clone());
$contentArea.slideDown();
$(".searchalttitle").remove();
$container.addClass("enhanced-active-container");
$titleElement.remove();
noteElements.forEach($el => $el.show());
}
function bindCopyAction($element, formatterFn) {
$element.click(function(event) {
copyToClipboard(formatterFn());
showCopiedMessage(event);
performButtonAnimation($(this));
});
}
$container.click(showContentArea);
bindCopyAction($note2, formatPlain);
bindCopyAction($note3, formatWikitext);
bindCopyAction($note4, formatPetScan);
bindCopyAction($note6, formatPetScanTotal);
const $pageDown = $("<div><br /><br />[ ↓ ]</div>").addClass("enhanced-scroll-btn").hover(function() {
$(this).addClass("hover");
$(this).attr('title', 'Go to the bottom of the page');
}, function() {
$(this).removeClass("hover");
}).insertBefore('.mw-search-result:eq(0)');
const $pageUp = $("<div>[ ↑ ]</div>").addClass("enhanced-scroll-btn").hover(function() {
$(this).addClass("hover");
$(this).attr('title', 'Go to the top of the page');
}, function() {
$(this).removeClass("hover");
}).insertAfter('.mw-search-result:eq(-1)');
$pageDown.click(function(event) {
event.preventDefault();
$('html, body').animate({
scrollTop: $(document).height()
}, 500);
});
$pageUp.click(function() {
$('html, body').animate({
scrollTop: 0
}, 500);
});
$("<span> (<b>" + $('.mw-search-result-heading').length + "</b> shown on the page)</span>").appendTo('.results-info');
const searchDidYouMean = document.querySelector('.searchdidyoumean');
if (searchDidYouMean) {
searchDidYouMean.style.color = '#b32424';
searchDidYouMean.style.fontSize = '20px';
}
});
});
})();
(function() {
if (window.Headmaster) return;
const VERSION = "0.9.2.1";
const defaultNS = [0, 118];
const userNS = window.headmaster_ns || [];
const config = {
menu: window.headmaster_menu || 'p-cactions',
enableSummary: window.headmaster_do_summary !== false,
customSummary: window.headmaster_summary || "",
allowedNamespaces: Array.from(new Set([...defaultNS, ...userNS])),
autoScan: window.headmaster_auto_scan !== false,
autoRun: window.headmaster_auto_run === false
};
const Headmaster = {
VERSION: VERSION,
DefaultSummary: "Converting [[MOS:PSEUDOHEAD|pseudo-headings]] {details}using [[User:Sam_Sailor/Scripts/Headmaster.js|Headmaster]]. Semicolon markup is reserved for [[MOS:DLIST|description lists]]",
styleInjected: false,
css: `
#hm-panel { clear: both; border: 1px solid #a2a9b1; padding: 15px; background: #f8f9fa; margin-bottom: 1.5em; border-radius: 2px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
#hm-table { width: 100%; border-collapse: collapse; background: white; margin-top: 10px; }
#hm-table th { background: #f2f2f2; border: 1px solid #a2a9b1; padding: 8px; text-align: left; }
#hm-table td { border: 1px solid #a2a9b1; padding: 8px; vertical-align: middle; }
.hm-context { font-family: 'Courier New', monospace; font-size: 0.9em; color: #54595d; background: #fdfdfd; }
#hm-controls { display: flex; justify-content: space-between; align-items: center; margin-top: 15px; }
#hm-controls button { margin-left: 10px; padding: 6px 16px; cursor: pointer; border-radius: 2px; }
.hm-btn-apply { background: #36c; color: white; border: 1px solid #36c; font-weight: bold; }
.hm-btn-apply:hover { background: #447ff5; }
.hm-global-selector { font-size: 0.9em; color: #202122; background: #eaecf0; padding: 8px; border-radius: 2px; border: 1px solid #a2a9b1; }
.hm-global-selector a { font-weight: bold; text-decoration: none; color: #36c; }
.hm-global-selector a:hover { text-decoration: underline; }
.hm-locate-btn { cursor: pointer; color: #36c; border: 1px solid #a2a9b1; background: #f8f9fa; padding: 2px 6px; font-size: 0.85em; border-radius: 2px; }
.hm-locate-btn:hover { background: #fff; border-color: #36c; }
`,
setup() {
if (!config.allowedNamespaces.includes(mw.config.get("wgNamespaceNumber"))) return;
const action = mw.config.get("wgAction");
this.addPortlet(action);
if (action === 'view' && (config.autoScan || config.autoRun)) {
this.silentScan();
}
if (mw.util.getParamValue('headmaster') === '1' && ['edit', 'submit'].includes(action)) {
const params = new URLSearchParams(window.location.search);
params.delete('headmaster');
const searchString = params.toString();
const newUrl = window.location.pathname + (searchString ? '?' + searchString : '');
window.history.replaceState(null, '', newUrl);
this.init();
}
},
addPortlet(action) {
const link = mw.util.addPortletLink(config.menu, '#', 'Headmaster', 'ca-hm', 'Interactively convert pseudo-headings');
if (link) {
$(link).on('click', (e) => {
e.preventDefault();
if (action === 'view') {
this.redirectToEdit();
} else if (['edit', 'submit'].includes(action)) {
this.init();
}
});
}
},
redirectToEdit() {
window.location.href = mw.util.getUrl(mw.config.get("wgPageName"), {
action: 'edit',
headmaster: '1'
});
},
silentScan() {
new mw.Api().get({
action: 'query',
prop: 'revisions',
titles: mw.config.get('wgPageName'),
rvprop: 'content',
rvlimit: 1,
formatversion: 2
}).done((data) => {
const page = data.query.pages[0];
if (!page || !page.revisions) return;
const wikitext = page.revisions[0].content;
const tasks = this.analyzeText(wikitext);
if (tasks.length > 0) {
if (config.autoRun) {
this.redirectToEdit();
} else {
this.notifyDiscovery(tasks.length);
}
}
}).fail((err) => {
mw.log.warn("Headmaster: silentScan failed", err);
});
},
notifyDiscovery(count) {
const $msg = $('<span>').text(`Headmaster: Found ${count} issues. `).append($('<a>').text('Run conversion').css({
fontWeight: 'bold',
cursor: 'pointer'
}).on('click', () => this.redirectToEdit()));
mw.notify($msg, {
tag: 'hm-discovery',
autoHide: false
});
},
init() {
if (!this.styleInjected) {
mw.loader.addStyleTag(this.css);
this.styleInjected = true;
}
const $textbox = $('#wpTextbox1');
const wikitext = $textbox.textSelection('getContents');
if (!wikitext) {
mw.notify("Textbox is empty or not found.", {
type: 'error'
});
return;
}
const tasks = this.analyzeText(wikitext);
if (tasks.length === 0) {
mw.notify("No issues found.");
return;
}
this.showUI(tasks, wikitext);
},
analyzeText(wikitext) {
const lines = wikitext.split("\n");
const tasks = [];
let currentDepth = 1;
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
const headingMatch = line.match(/^(={2,6})\s*(.+?)\s*\1\s*$/);
if (headingMatch) {
currentDepth = headingMatch[1].length;
const prevLine = i > 0 ? lines[i - 1].trim() : null;
const isSmashed = prevLine !== null && prevLine !== "";
const isExtraSpace = i > 1 && lines[i - 1].trim() === "" && lines[i - 2].trim() === "";
if (isSmashed || isExtraSpace) {
tasks.push({
index: i,
type: 'WHITESPACE',
original: lines[i],
isSmashed: isSmashed,
isExtraSpace: isExtraSpace
});
}
} else if (line.startsWith(';') && !line.startsWith(';;')) {
const nextLine = lines[i + 1] || "";
if (!nextLine.trim().startsWith(':')) {
const cleanText = line.substring(1).replace(/\s*:.*$/, "").trim();
const suggestedLevel = "=".repeat(Math.min(currentDepth + 1, 6));
tasks.push({
index: i,
type: 'PSEUDO',
original: line,
cleanText: cleanText,
context: nextLine.substring(0, 60).trim() + (nextLine.length > 60 ? "..." : ""),
suggestedLevel: suggestedLevel
});
}
}
}
return tasks;
},
showUI(tasks, originalWikitext) {
$('#hm-panel').remove();
$('#editform').hide();
const visibleTasks = tasks.filter(t => t.type === 'PSEUDO');
const pseudoCount = visibleTasks.length;
const whitespaceCount = tasks.filter(t => t.type === 'WHITESPACE').length;
let statusMsg = "";
const s = (n) => n !== 1 ? 's' : '';
if (pseudoCount > 0) {
statusMsg = `Found <strong>${pseudoCount}</strong> possible pseudo-heading${s(pseudoCount)}.`;
if (whitespaceCount > 0) {
statusMsg += ` Also fixing <strong>${whitespaceCount}</strong> spacing issue${s(whitespaceCount)} quietly.`;
}
} else {
statusMsg = `Did not find any pseudo-headings, but fixing <strong>${whitespaceCount}</strong> spacing issue${s(whitespaceCount)} quietly.`;
}
const tableRows = pseudoCount > 0 ? visibleTasks.map((task, i) => `
<tr data-task-index="${task.index}">
<td><button class="hm-locate-btn" data-hm-i="${tasks.indexOf(task)}" title="Show in editor">Locate</button></td>
<td class="hm-context"><code>${mw.html.escape(task.original)}</code></td>
<td><label><input type="radio" name="hm-opt-${i}" value="keep"> Keep</label></td>
<td><label><input type="radio" name="hm-opt-${i}" value="bold" checked> <b>Bold</b></label></td>
<td><label><input type="radio" name="hm-opt-${i}" value="head"> Heading (${task.suggestedLevel.length})</label></td>
<td class="hm-context">${mw.html.escape(task.context || "(end of page)")}</td>
</tr>
`).join("") : `<tr><td colspan="6" style="text-align:center; padding: 2em; color: #54595d; background: #f8f9fa;">
No interactive heading changes detected. Only background spacing normalization will be applied.
</td></tr>`;
const $panel = $(`
<div id="hm-panel">
<h3 style="margin-top:0">Headmaster v${this.VERSION}: Review pseudo-headings</h3>
<div class="hm-global-selector" ${pseudoCount === 0 ? 'style="display:none"' : ''}>
<strong>Bulk action:</strong>
<a href="#" id="hm-all-keep">Set all to Keep</a> |
<a href="#" id="hm-all-bold">Set all to Bold</a> |
<a href="#" id="hm-all-head">Set all to Heading</a>
</div>
<table id="hm-table">
<thead>
<tr>
<th>Find</th>
<th>Source</th>
<th colspan="3">Action</th>
<th>Next line context</th>
</tr>
</thead>
<tbody>${tableRows}</tbody>
</table>
<div id="hm-controls">
<span>${statusMsg}</span>
<div>
<button id="hm-cancel" class="mw-ui-button">Cancel</button>
<button id="hm-apply" class="hm-btn-apply">Show changes (diff)</button>
</div>
</div>
</div>
`);
$('#content').prepend($panel);
window.scrollTo(0, 0);
$('.hm-locate-btn').on('click', function() {
$(document).off('mousedown.hmlocate');
const idx = $(this).data('hm-i');
const task = tasks[idx];
const lines = originalWikitext.split('\n');
let charOffset = 0;
for (let j = 0; j < task.index; j++) {
charOffset += lines[j].length + 1;
}
$('#hm-panel').hide();
$('#editform').show();
const $textbox = $('#wpTextbox1');
$textbox.textSelection('setSelection', {
start: charOffset,
end: charOffset + lines[task.index].length
});
$textbox.textSelection('scrollToCaretPosition');
const locateNotify = mw.notify("Locating line " + (task.index + 1) + ". Click outside the editor to return.", {
tag: 'hm-locate',
type: 'success',
autoHide: false
});
setTimeout(() => {
$(document).on('mousedown.hmlocate', function(e) {
if (!$(e.target).closest('#editform, .mw-notification-area').length) {
$(document).off('mousedown.hmlocate');
Promise.resolve(locateNotify).then(n => n.close());
$('#editform').hide();
$('#hm-panel').show();
}
});
}, 200);
});
$('#hm-all-keep').on('click', (e) => {
e.preventDefault();
$('input[value="keep"]').prop('checked', true);
});
$('#hm-all-bold').on('click', (e) => {
e.preventDefault();
$('input[value="bold"]').prop('checked', true);
});
$('#hm-all-head').on('click', (e) => {
e.preventDefault();
$('input[value="head"]').prop('checked', true);
});
$('#hm-cancel').on('click', () => {
$('#hm-panel').remove();
$('#editform').show();
});
$('#hm-apply').on('click', () => this.applyChanges(tasks, originalWikitext));
},
getProcessedSummary(existingSummary, bolds, heads, spaces) {
if (!config.enableSummary) return existingSummary;
let mySummary = "";
let isDefault = false;
if (config.customSummary) {
mySummary = config.customSummary;
} else {
isDefault = true;
if (bolds > 0 || heads > 0) {
const detailsArr = [];
if (bolds > 0) detailsArr.push(`${bolds} to bold`);
if (heads > 0) detailsArr.push(`${heads} to heading`);
const detailsStr = detailsArr.length > 0 ? `(${detailsArr.join(', ')}) ` : "";
mySummary = this.DefaultSummary.replace("{details}", detailsStr);
if (spaces > 0) {
mySummary += "; also handled [[MOS:BLANKLINE]]";
}
} else if (spaces > 0) {
mySummary = "Handling [[MOS:BLANKLINE]] using [[User:Sam_Sailor/Scripts/Headmaster.js|Headmaster]]";
}
}
if (!mySummary) return existingSummary;
const trimmedOld = existingSummary.trim();
if (!trimmedOld) return mySummary;
if (/[.!?]$/.test(trimmedOld)) {
return trimmedOld + " " + mySummary;
} else {
const finalSummary = isDefault ? mySummary.charAt(0).toLowerCase() + mySummary.slice(1) : mySummary;
return trimmedOld + "; " + finalSummary;
}
},
applyChanges(tasks, wikitext) {
try {
let lines = wikitext.split("\n");
let bCount = 0,
hCount = 0,
wCount = 0;
const pseudoTasks = tasks.filter(t => t.type === 'PSEUDO');
for (let i = pseudoTasks.length - 1; i >= 0; i--) {
const task = pseudoTasks[i];
const idx = task.index;
const action = $('#hm-table tbody tr').filter(`[data-task-index="${idx}"]`).find('input:checked').val();
if (action === 'bold') {
let newValue = `'''${task.cleanText}'''`;
if (idx > 0 && lines[idx - 1].trim() !== "") {
newValue = "\n" + newValue;
}
lines[idx] = newValue;
bCount++;
} else if (action === 'head') {
const h = task.suggestedLevel;
let newValue = `${h} ${task.cleanText} ${h}`;
if (idx > 0 && lines[idx - 1].trim() !== "") {
newValue = "\n" + newValue;
}
lines[idx] = newValue;
hCount++;
}
}
const spaceTasks = tasks.filter(t => t.type === 'WHITESPACE');
for (let i = spaceTasks.length - 1; i >= 0; i--) {
const task = spaceTasks[i];
const idx = task.index;
if (task.isSmashed) {
lines.splice(idx, 0, "");
} else if (task.isExtraSpace) {
let backIdx = idx - 1;
while (backIdx > 0 && lines[backIdx].trim() === "" && lines[backIdx - 1].trim() === "") {
lines.splice(backIdx, 1);
backIdx--;
}
}
wCount++;
}
if (bCount + hCount + wCount === 0) {
mw.notify("No changes selected.");
$('#hm-panel').remove();
$('#editform').show();
return;
}
$('#wpTextbox1').textSelection('setContents', lines.join("\n"));
const $summary = $('#wpSummary');
$summary.val(this.getProcessedSummary($summary.val(), bCount, hCount, wCount));
$('#hm-panel').remove();
$('#editform').show();
mw.notify(`Headmaster: Fixed ${wCount} spacing issue(s) and converted ${bCount + hCount} heading(s).`);
$('html, body').animate({
scrollTop: 0
}, 'fast');
$('#wpDiff').click();
} catch (err) {
$('#editform').show();
mw.notify("Headmaster error: " + err.message, {
type: 'error'
});
}
}
};
window.Headmaster = Headmaster;
mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => {
$(() => Headmaster.setup());
});
})();
//</nowiki>
opr08pxlp3a29yc1hoph3191g68qwfv
749889
749881
2026-07-04T10:07:22Z
Sam Sailor
26820
Test
749889
javascript
text/javascript
//<nowiki>
(function(mw, $, OO) {
'use strict';
const IGNORE_SUFFIXES = ['Sr.', 'Sr', 'Jr.', 'Jr', 'II', 'III', 'IV', 'V', 'MD', 'PhD'];
const LANG_PREFS = ['frwiki', 'dewiki', 'eswiki', 'itwiki', 'nlwiki', 'svwiki'];
const NON_LANG_WIKIS = ['commonswiki', 'specieswiki', 'metawiki', 'mediawikiwiki', 'wikidatawiki', 'wikimaniawiki'];
const suffixRE = new RegExp('\\s(' + IGNORE_SUFFIXES.join('|').replace(/\./g, '\\.') + ')$', 'i');
function GetterDialog(config) {
GetterDialog.super.call(this, config);
this.abortController = null;
}
OO.inheritClass(GetterDialog, OO.ui.ProcessDialog);
GetterDialog.static.name = 'getterDialog';
GetterDialog.static.title = 'Getter: Wikipedia List Builder';
GetterDialog.static.actions = [
{ action: 'generate', label: 'Generate List', flags: ['primary', 'progressive'], modes: 'edit' },
{ action: 'back', label: 'Go back', modes: 'result' },
{ action: 'exit', label: 'Exit', flags: 'safe', modes: ['edit', 'result'] }
];
GetterDialog.prototype.cleanTitle = function(t) { return t.replace(suffixRE, '').replace(/\s?\([^)]*\)$/, '').trim(); };
GetterDialog.prototype.normalizeTitle = function(t) { return t.replace(/_/g, ' ').trim().replace(/^(.)/, c => c.toUpperCase()); };
GetterDialog.prototype.getSortKey = function(title) {
const clean = this.cleanTitle(title);
const parts = clean.split(' ');
return (parts.length <= 1) ? title.toLowerCase() : (parts[parts.length - 1] + ', ' + parts.slice(0, parts.length - 1).join(' ') + ' | ' + title).toLowerCase();
};
GetterDialog.prototype.compileCriteria = function(vals, anchorLC) {
return vals.filter(t => t.toLowerCase() !== anchorLC)
.map(r => new RegExp('^' + r.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*') + '$', 'i'));
};
GetterDialog.prototype.getBestAnchor = function() {
const getTerms = (w) => w.getValue().map(t => t.trim()).filter(Boolean);
const candidates = [
...getTerms(this.surnameTags).map(t => ({ term: t, weight: 2000 + this.score(t) })),
...getTerms(this.givenTags).map(t => ({ term: t, weight: 1000 + this.score(t) })),
...getTerms(this.middleTags).map(t => ({ term: t, weight: 0 + this.score(t) }))
];
return candidates.length === 0 ? null : candidates.reduce((b, c) => (c.weight > b.weight ? c : b)).term;
};
GetterDialog.prototype.score = function(term) {
const wildcardCount = (term.match(/\*/g) || []).length;
return (wildcardCount === 0 ? 1000 : 0) + (term.length - wildcardCount) - wildcardCount;
};
GetterDialog.prototype.formatResult = function(p, mode, includeIll) {
if (mode === 'anbl') return `* {{anbl|${p.title}}}`;
const bY = p.ent?.claims?.P569?.[0]?.mainsnak?.datavalue?.value?.time?.substring(1, 5);
const dY = p.ent?.claims?.P570?.[0]?.mainsnak?.datavalue?.value?.time?.substring(1, 5);
const bio = bY ? ` (${bY}${dY ? '–' + dY : ''})` : '';
const rawDesc = (p.desc || '').replace(/\s?\(\s*(born|d\.)?\s*\d{4}(–\d{4})?\s*\)$/i, '').trim();
const suffix = rawDesc ? `, ${rawDesc}` : '';
let display = `[[${p.title}]]`;
if (includeIll) {
const langKey = LANG_PREFS.find(l => p.ent.sitelinks?.[l]) || Object.keys(p.ent.sitelinks || {}).find(k => k.endsWith('wiki') && k !== 'enwiki' && !NON_LANG_WIKIS.includes(k));
if (langKey) display = `{{ill|${p.title}|${langKey.replace('wiki', '')}|${p.ent.sitelinks[langKey].title}}}`;
}
return `* ${display}${bio}${suffix}`;
};
GetterDialog.prototype.matchesCriteria = function(title, compiled) {
const clean = this.cleanTitle(title);
const tokens = clean.split(/\s+/);
const given = tokens[0] || '', surname = tokens.length > 1 ? tokens[tokens.length - 1] : '';
const middleLC = tokens.length > 2 ? tokens.slice(1, -1).map(x => x.toLowerCase()) : [];
const check = (val, regexes) => (!regexes || regexes.length === 0 || regexes.some(re => re.test(val)));
return check(given, compiled.given) && check(surname, compiled.surname) && (compiled.middle.length === 0 || compiled.middle.every(m => middleLC.includes(m)));
};
GetterDialog.prototype.getExistingLinks = async function() {
const titles = new Set();
try {
const data = await this.api.get({ action: 'parse', page: mw.config.get('wgPageName'), prop: 'wikitext', format: 'json' });
const text = data.parse?.wikitext?.['*'] || '';
const regexes = [/\[\[([^\]|#]+)/g, /\{\{(?:anbl|Annotated biography link)\|([^}|]+)/gi];
regexes.forEach(re => { let m; while ((m = re.exec(text)) !== null) titles.add(this.normalizeTitle(m[1])); });
} catch (e) { console.error('[Getter] Parse error:', e); }
return titles;
};
async function checkEntitiesData(qids, signal) {
if (!qids.length) return {};
const all = {};
for (let i = 0; i < qids.length; i += 50) {
if (signal?.aborted) throw new DOMException('Aborted', 'AbortError');
try {
const res = await fetch('https://www.wikidata.org/w/api.php?action=wbgetentities&ids=' + qids.slice(i, i + 50).join('|') + '&props=claims|labels|descriptions|sitelinks&languages=en&format=json&origin=*', { signal }).then(r => r.json());
Object.assign(all, res.entities || {});
} catch (e) { if (e.name === 'AbortError') throw e; console.error('[Getter] Wikidata batch error', e); }
}
return all;
}
GetterDialog.prototype.autoPopulateFields = async function () {
const title = mw.config.get('wgTitle'), cleanTitle = this.cleanTitle(title);
const m = title.match(/^(.*?) \((given name|surname)\)$/i);
if (m) { m[2].toLowerCase() === 'given name' ? this.givenTags.addTag(m[1]) : this.surnameTags.addTag(m[1]); return; }
try {
const data = await this.api.get({ action: 'parse', page: mw.config.get('wgPageName'), prop: 'wikitext', format: 'json' });
const text = data.parse?.wikitext?.['*'] || '';
const hasSurname = /\{\{\s*surname\s*(?:\||\}\})/i.test(text);
const givenTemplate = text.match(/\{\{\s*given name\b[^}]*\}\}/i)?.[0] || '';
const isBoth = /\|\s*type\s*=\s*both\b/i.test(givenTemplate);
if (hasSurname || (givenTemplate && isBoth)) this.surnameTags.addTag(cleanTitle);
if (givenTemplate) this.givenTags.addTag(cleanTitle);
if (!hasSurname && !givenTemplate) this.givenTags.addTag(cleanTitle);
} catch (e) { console.error('[Getter] Inspect error', e); this.givenTags.addTag(cleanTitle); }
};
GetterDialog.prototype.initialize = function() {
GetterDialog.super.prototype.initialize.apply(this, arguments);
this.api = new mw.Api();
this.panel = new OO.ui.PanelLayout({ padded: true, expanded: false });
this.givenTags = new OO.ui.TagMultiselectWidget({ placeholder: 'Given names...', allowArbitrary: true });
this.middleTags = new OO.ui.TagMultiselectWidget({ placeholder: 'Middle terms...', allowArbitrary: true });
this.surnameTags = new OO.ui.TagMultiselectWidget({ placeholder: 'Surnames...', allowArbitrary: true });
this.excludeTags = new OO.ui.TagMultiselectWidget({ placeholder: 'Exclude strings...', allowArbitrary: true });
this.humanOnly = new OO.ui.CheckboxInputWidget({ selected: true });
this.excludeHumans = new OO.ui.CheckboxInputWidget({ selected: false });
this.includeIll = new OO.ui.CheckboxInputWidget({ selected: false });
this.sortBySurname = new OO.ui.CheckboxInputWidget({ selected: true });
this.updateMode = new OO.ui.CheckboxInputWidget({ selected: false });
this.excludeHumans.on('change', (s) => { if (s) { this.humanOnly.setSelected(false); this.sortBySurname.setSelected(false); } });
this.humanOnly.on('change', (s) => { if (s) this.excludeHumans.setSelected(false); });
this.formatSelect = new OO.ui.ButtonSelectWidget({ items: [new OO.ui.ButtonOptionWidget({ data: 'wiki', label: 'Wiki' }), new OO.ui.ButtonOptionWidget({ data: 'anbl', label: 'ANBL' })]});
this.formatSelect.selectItemByData('wiki');
this.fieldset = new OO.ui.FieldsetLayout({ label: 'Advanced Filter Settings' });
this.fieldset.addItems([
new OO.ui.FieldLayout(this.givenTags, { label: 'Given names', align: 'top' }),
new OO.ui.FieldLayout(this.middleTags, { label: 'Middle terms', align: 'top' }),
new OO.ui.FieldLayout(this.surnameTags, { label: 'Surnames', align: 'top' }),
new OO.ui.FieldLayout(this.excludeTags, { label: 'Exclude strings', align: 'top' }),
new OO.ui.FieldLayout(this.humanOnly, { label: 'Humans only', align: 'inline' }),
new OO.ui.FieldLayout(this.excludeHumans, { label: 'Exclude humans', align: 'inline' }),
new OO.ui.FieldLayout(this.sortBySurname, { label: 'Sort by surname', align: 'inline' }),
new OO.ui.FieldLayout(this.updateMode, { label: 'Exclude existing links', align: 'inline' }),
new OO.ui.FieldLayout(this.includeIll, { label: 'Include {{ill}}', align: 'inline' }),
new OO.ui.FieldLayout(this.formatSelect, { label: 'Format', align: 'top' })
]);
this.resultArea = new OO.ui.MultilineTextInputWidget({ rows: 15, autosize: true });
this.copyButton = new OO.ui.ButtonWidget({ label: 'Copy list', icon: 'copy', flags: ['progressive'] });
this.copyButton.$element.css('marginTop', '10px');
this.copyButton.on('click', () => navigator.clipboard.writeText(this.resultArea.getValue()).then(() => mw.notify('Copied!')));
this.resultContainer = new OO.ui.PanelLayout({ padded: true, expanded: false });
this.resultContainer.$element.append(new OO.ui.FieldLayout(this.resultArea, { label: 'Results', align: 'top' }).$element, this.copyButton.$element).hide();
this.panel.$element.append(this.fieldset.$element, this.resultContainer.$element);
this.$body.append(this.panel.$element);
this.actions.setMode('edit');
this.autoPopulateFields();
};
GetterDialog.prototype.runGetter = async function() {
this.abortController = new AbortController();
try {
const { humanOnly, excludeHumans, includeIll, sortBySurname, formatSelect, updateMode } = this;
const mode = formatSelect.findSelectedItem().getData(), anchor = this.getBestAnchor();
if (!anchor) { mw.notify('Please enter search terms.', { type: 'error' }); return; }
const existing = updateMode.isSelected() ? await this.getExistingLinks() : new Set(), resultsMap = new Map();
const excludeList = this.excludeTags.getValue().map(e => e.toLowerCase()), anchorLC = anchor.toLowerCase();
const compiledCriteria = {
given: this.compileCriteria(this.givenTags.getValue(), anchorLC),
surname: this.compileCriteria(this.surnameTags.getValue(), anchorLC),
middle: this.middleTags.getValue().filter(t => t.toLowerCase() !== anchorLC).map(m => m.toLowerCase())
};
let cont = {};
do {
const data = await this.api.get({ action: 'query', list: 'search', srsearch: `intitle:"${anchor}"`, srlimit: 'max', sroffset: cont.sroffset || 0, format: 'json' });
for (const p of (data.query?.search || [])) {
if (excludeList.some(e => p.title.toLowerCase().includes(e))) continue;
const title = this.normalizeTitle(p.title);
if (!existing.has(title) && this.matchesCriteria(title, compiledCriteria)) resultsMap.set(p.title, { title: p.title, pageid: p.pageid });
}
cont = data.continue || {};
} while (cont.sroffset && !this.abortController?.signal.aborted);
const pageIds = Array.from(resultsMap.values()).map(p => p.pageid), confirmedResults = new Map();
for (let i = 0; i < pageIds.length; i += 50) {
if (this.abortController?.signal.aborted) return;
const chunk = pageIds.slice(i, i + 50), qidList = [], qidMap = {};
const pageData = await this.api.get({ action: 'query', prop: 'pageprops|description', pageids: chunk.join('|'), format: 'json' });
Object.values(pageData.query.pages).forEach(p => { if (p.pageprops?.wikibase_item) { qidList.push(p.pageprops.wikibase_item); qidMap[p.pageprops.wikibase_item] = p; } });
const entities = await checkEntitiesData(qidList, this.abortController?.signal);
for (const qid in entities) {
const ent = entities[qid], p = qidMap[qid];
const isHuman = ent.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5");
if ((humanOnly.isSelected() && !isHuman) || (excludeHumans.isSelected() && isHuman)) continue;
confirmedResults.set(p.title, { title: p.title, desc: p.description || ent.descriptions?.en?.value || '', ent });
}
}
this.resultArea.setValue(Array.from(confirmedResults.values())
.map(p => ({ formatted: this.formatResult(p, mode, includeIll.isSelected()), sortKey: sortBySurname.isSelected() ? this.getSortKey(p.title) : p.title.toLowerCase() }))
.sort((a, b) => a.sortKey.localeCompare(b.sortKey, undefined, { sensitivity: 'base' }))
.map(r => r.formatted).join('\n'));
this.fieldset.$element.hide(); this.resultContainer.$element.show(); this.actions.setMode('result');
} catch (e) { if (e.name !== 'AbortError') { console.error('[Getter] Generator error:', e); mw.notify('Error generating list.', { type: 'error' }); } }
finally { this.abortController = null; }
};
GetterDialog.prototype.getActionProcess = function(action) {
if (action === 'generate') return new OO.ui.Process(async () => { await this.runGetter(); });
if (action === 'back') { if (this.abortController) this.abortController.abort(); return new OO.ui.Process(() => { this.resultContainer.$element.hide(); this.fieldset.$element.show(); this.actions.setMode('edit'); }); }
if (action === 'exit') { if (this.abortController) this.abortController.abort(); return new OO.ui.Process(() => this.close()); }
return GetterDialog.super.prototype.getActionProcess.call(this, action);
};
mw.loader.using(['mediawiki.api', 'mediawiki.util', 'oojs-ui', 'oojs-ui-widgets', 'oojs-ui-windows']).then(() => {
const wm = new OO.ui.WindowManager();
$(document.body).append(wm.$element);
const dialog = new GetterDialog({ size: 'medium' });
wm.addWindows([dialog]);
$(mw.util.addPortletLink('p-tb', '#', 'Getter', 't-getter')).on('click', (e) => { e.preventDefault(); wm.openWindow(dialog); });
});
}(mediaWiki, jQuery, OO));
(function() {
if (window.IllWill) return;
const STRIKEOUT = true;
const HIDE = 'hide';
const IllWill = {
Version: "0.5",
Attribution: 'created by <a target="_blank" href="/wiki/User:Cobaltcigs">cobaltcigs</a>',
Summary: "Added interlanguage links using IllWill.js",
DataApi: "https://www.wikidata.org/w/api.php",
LocalLang: mw.config.get("wgPageContentLanguage"),
Disambiguator: / \(.*\)$/,
SiteLinks: {},
Config: {
MaxLanguages: 10,
IgnoreScientific: STRIKEOUT,
AutoDiff: true
},
css: `
#illwill { clear: both; margin-bottom: 1.5em; border: 1px solid #a2a9b1; padding: 15px; background: #f8f9fa; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.ill-sitelinks a { display: inline-block; margin-bottom: 3px; font-size: 0.9em; color: #36c; }
.ill-size-tag { font-size: 0.8em; color: #72777d; margin-left: 6px; white-space: nowrap; }
.illwill-ignore { text-decoration: line-through; color: #72777d; font-size: 0.85em; }
.ill-x { width: 35px; text-align: center; }
.ill-input, .ill-output { font-family: 'Courier New', monospace; white-space: pre-wrap; font-size: 0.9em; background: #fff; border: 1px solid #eaecf0; padding: 4px; border-radius: 2px; }
#ill-buttons { text-align: right; padding: 10px; border-top: 1px solid #a2a9b1; margin-top: 10px; }
#ill-buttons button { margin-left: 10px; cursor: pointer; padding: 6px 16px; border-radius: 2px; border: 1px solid #a2a9b1; transition: all 0.2s; }
#ill-ok { background: #36c; color: #fff; border-color: #36c !important; font-weight: bold; }
#ill-ok:hover { background: #447ff5; }
#ill-ok:disabled { background: #eaecf0; color: #72777d; border-color: #c8ccd1 !important; cursor: not-allowed; opacity: 0.7; }
#ill-table { width: 100%; border-collapse: collapse; margin-top: 10px; background: white; }
#ill-table th { background: #f2f2f2; text-align: left; padding: 8px; border: 1px solid #a2a9b1; }
#ill-table td { padding: 8px; border: 1px solid #a2a9b1; vertical-align: top; }
#ill-table caption { background: #eaffea; font-weight: bold; padding: 10px; border: 1px solid #a2a9b1; border-bottom: none; font-size: 1.1em; }
#ill-help { font-size: 0.85em; color: #54595d; padding: 10px; line-height: 1.4; }
.ill-loading-inline { font-style: italic; color: #72777d; font-size: 0.9em; display: block; margin: 5px 0; }
`,
setup() {
if (!["edit", "submit"].includes(mw.config.get('wgAction'))) return;
mw.loader.addStyleTag(this.css);
const link = mw.util.addPortletLink('p-cactions', '#', 'IllWill', 'ca-illwill', "Add interlanguage links via Wikidata");
if (link) {
link.addEventListener('click', (e) => {
e.preventDefault();
this.makePanel();
});
}
},
norm(s) {
if (!s) return "";
const t = s.trim().replace(/[\s_]+/g, " ");
return t.charAt(0).toUpperCase() + t.slice(1);
},
async getRedLinks($textbox) {
const txt = $textbox.textSelection('getContents') || "";
const wl = txt.match(/\[\[[^[\]\n]+\]\]/g);
if (!wl) return {
reds: [],
txt
};
const api = new mw.Api();
const textToParse = wl.join("").replace(/\|[^\]]+/g, "");
try {
const data = await api.post({
action: 'parse',
text: textToParse,
contentmodel: 'wikitext',
formatversion: 2
});
const tempDiv = document.createElement('div');
tempDiv.innerHTML = data.parse.text;
const reds = Array.from(tempDiv.querySelectorAll("a.new")).map(x => {
const urlParams = new URLSearchParams(x.href.split('?')[1]);
return urlParams.get('title')?.replace(/_/g, " ");
}).filter(Boolean);
return {
reds: [...new Set(reds)],
txt
};
} catch (e) {
return {
reds: [],
txt
};
}
},
async makePanel() {
const $editform = $('#editform');
const $textbox = $('#wpTextbox1');
$('#illwill').remove();
$editform.hide();
const $wrapper = $('<div id="illwill">Loading redlinks...</div>').insertBefore($editform);
const {
reds,
txt
} = await this.getRedLinks($textbox);
const wtLinks = [];
reds.forEach(title => {
const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\s/g, "[\\s_]+");
const re = new RegExp("\\[\\[\\s*" + escaped + "\\s*(?:\\|[^\\[\\]]+)?\\]\\]", "ig");
const match = txt.match(re);
if (match && !wtLinks.some(z => z.title === title)) {
wtLinks.push({
link: match[0],
title
});
}
});
if (!wtLinks.length) {
const $back = $('<button>Quit</button>').on('click', () => {
$wrapper.remove();
$editform.show();
});
$wrapper.empty().append($('<strong>No valid redlinks found in text.</strong> '), $back);
return;
}
const $table = $(`
<table id="ill-table" class="wikitable">
<caption>IllWill.js (v${this.Version})</caption>
<thead>
<tr>
<th class="ill-x"><input type="checkbox" id="ill-select-all" disabled></th>
<th>Input link</th>
<th>Possible Wikidata matches</th>
<th>Sitelinks (top ${this.Config.MaxLanguages} by size)</th>
<th>Resulting wikitext</th>
</tr>
</thead>
<tbody></tbody>
<tfoot>
<tr>
<td colspan="3" id="ill-help">Carefully examine possible Wikidata matches.</td>
<td colspan="2" id="ill-buttons">
<button id="ill-cancel">Cancel</button>
<button id="ill-ok" disabled>Show changes</button>
</td>
</tr>
</tfoot>
</table>
`);
const $tbody = $table.find('tbody');
wtLinks.forEach((item, i) => {
$tbody.append(`
<tr id="ill-row-${i}" data-index="${i}">
<td class="ill-x"><input type="checkbox" class="ill-row-check" disabled></td>
<td class="ill-input">${this.escapeHTML(item.link)}</td>
<td class="ill-wd-item"><span class="ill-loading-inline">Searching...</span></td>
<td class="ill-sitelinks"></td>
<td class="ill-output"></td>
</tr>
`);
});
$wrapper.empty().append($table);
const chunkSize = 5;
for (let i = 0; i < wtLinks.length; i += chunkSize) {
const chunk = wtLinks.slice(i, i + chunkSize);
await Promise.all(chunk.map((item, idx) => this.fetchWikidataOptimized(item.title, i + idx)));
}
$table.on('click', '#ill-cancel', () => {
$editform.show();
$wrapper.remove();
});
$table.on('click', '#ill-ok', () => this.onOkButton($editform, $wrapper, $textbox));
$table.on('change', '#ill-select-all', (e) => {
const isChecked = $(e.target).is(':checked');
$table.find('.ill-row-check:not(:disabled)').each(function() {
$(this).prop('checked', isChecked).trigger('change');
});
});
$table.on('change', '.ill-row-check', (e) => {
this.onCheckbox(e);
this.updateOkButtonState($table);
});
$table.on('change', 'input[type="radio"]', (e) => {
this.showSiteLinks(e);
});
},
async fetchWikidataOptimized(title, index) {
const query = title.replace(this.Disambiguator, "");
const url = `${this.DataApi}?action=wbsearchentities&search=${encodeURIComponent(query)}&language=${this.LocalLang}&limit=5&format=json&origin=*`;
try {
const resp = await fetch(url);
const data = await resp.json();
const results = data.search;
const $cell = $(`#ill-row-${index} .ill-wd-item`);
if (!results || !results.length) {
$cell.html('<span class="ill-na">(no results)</span>');
return;
}
const entityIds = results.map(r => r.id);
await this.fetchEntities(entityIds, index, $cell);
} catch (e) {
$(`#ill-row-${index} .ill-wd-item`).text("Search failed.");
}
},
async fetchEntities(ids, rowIndex, $cell) {
const url = `${this.DataApi}?action=wbgetentities&ids=${ids.join('|')}&props=labels|descriptions|claims|sitelinks&format=json&origin=*`;
try {
const resp = await fetch(url);
const data = await resp.json();
$cell.empty();
ids.forEach(qNum => {
const entity = data.entities[qNum];
if (!entity) return;
const isScientific = entity.claims?.P31?.some(s => s.mainsnak?.datavalue?.value?.id === "Q13442814");
if (isScientific && this.Config.IgnoreScientific === HIDE) return;
const desc = entity.descriptions?.[this.LocalLang]?.value || entity.descriptions?.en?.value || "";
const label = entity.labels?.[this.LocalLang]?.value || entity.labels?.en?.value || qNum;
const isStrikeout = isScientific && this.Config.IgnoreScientific === STRIKEOUT;
const $div = $(`
<div class="${isStrikeout ? 'illwill-ignore' : ''}">
<input type="radio" name="selection-${rowIndex}" value="${qNum}" id="radio-${qNum}" ${isStrikeout ? 'disabled' : ''}>
<label for="radio-${qNum}">
<a href="https://wikidata.org/wiki/${qNum}" target="_blank">${this.escapeHTML(label)}</a>:
<span class="ill-desc">${desc ? this.escapeHTML(desc) : '<i>no description</i>'}</span>
</label>
</div>
`);
$cell.append($div);
this.SiteLinks[qNum] = entity.sitelinks || {};
});
} catch (e) {
$cell.text("Detail fetch failed.");
}
},
updateOkButtonState($table) {
const $rows = $table.find('.ill-row-check:not(:disabled)');
const $checkedRows = $rows.filter(':checked');
const anyEnabled = $rows.length > 0;
$table.find('#ill-ok').prop('disabled', $checkedRows.length === 0);
const $master = $table.find('#ill-select-all');
$master.prop('disabled', !anyEnabled);
if (anyEnabled && $rows.length === $checkedRows.length) {
$master.prop('checked', true);
} else {
$master.prop('checked', false);
}
},
async showSiteLinks(e) {
const isManualToggling = e.fromCheckbox || false;
const qNum = e.target.value;
const $row = $(e.target).closest('tr');
const sitelinks = this.SiteLinks[qNum];
const $checkbox = $row.find('.ill-row-check');
const $sitelinkCell = $row.find('.ill-sitelinks');
const $outputCell = $row.find('.ill-output');
const $table = $('#ill-table');
const effectiveMax = Math.min(Math.max(this.Config.MaxLanguages, 3), 6);
const validKeys = Object.keys(sitelinks).filter(k => k.endsWith('wiki') && !k.includes('commons'));
$sitelinkCell.html('<span class="ill-loading-inline">Measuring articles...</span>');
$outputCell.empty();
if (!isManualToggling) {
$checkbox.prop('disabled', true).prop('checked', false);
}
const candidates = validKeys.slice(0, 25);
const sizeMap = [];
await Promise.all(candidates.map(async (key) => {
const lang = key.replace('wiki', '').replace(/_/g, '-');
const title = sitelinks[key].title;
const endpoint = `https://${lang}.wikipedia.org/w/api.php?action=query&titles=${encodeURIComponent(title)}&prop=info&format=json&origin=*`;
try {
const res = await fetch(endpoint).then(r => r.json());
const page = Object.values(res.query.pages)[0];
sizeMap.push({
lang,
title,
length: page.length || 0
});
} catch (err) {
sizeMap.push({
lang,
title,
length: 0
});
}
}));
sizeMap.sort((a, b) => b.length - a.length);
const topLinks = sizeMap.slice(0, effectiveMax);
$sitelinkCell.html(topLinks.map(item => {
const kbValue = Math.round(item.length / 1024);
return `<div><a href="https://${item.lang}.wikipedia.org/wiki/${encodeURIComponent(item.title)}" target="_blank">${item.lang}:${this.escapeHTML(item.title)}</a> <span class="ill-size-tag">(${kbValue} kB)</span></div>`;
}).join(""));
const inputWikitext = $row.find('.ill-input').text();
const match = inputWikitext.match(/\[\[(.*?)\]\]/);
if (match) {
const parts = match[1].split('|');
const targetPage = parts[0].trim();
const surfaceName = (parts[1] || parts[0]).trim();
const illLangs = topLinks.map(item => {
const isSameTitle = this.norm(item.title) === this.norm(targetPage);
return `|${item.lang}|${isSameTitle ? '' : item.title}`;
}).join("");
const ltParam = (this.norm(surfaceName) === this.norm(targetPage)) ? "" : `|lt=${surfaceName}`;
$outputCell.text(`{{ill|${targetPage}${ltParam}${illLangs}}}`);
}
$checkbox.prop('disabled', !topLinks.length);
if (!isManualToggling) {
$checkbox.prop('checked', !!topLinks.length);
}
this.updateOkButtonState($table);
},
onCheckbox(e) {
const $row = $(e.target).closest('tr');
const $radio = $row.find('input[type="radio"]:checked');
const isChecked = $(e.target).is(':checked');
if (isChecked) {
if ($radio.length) {
this.showSiteLinks({
target: $radio[0],
fromCheckbox: true
});
}
} else {
$row.find('.ill-sitelinks, .ill-output').empty();
}
},
onOkButton($editform, $wrapper, $textbox) {
let text = $textbox.textSelection('getContents');
let changed = false;
$wrapper.find('.ill-row-check:checked').each(function() {
const $row = $(this).closest('tr');
const input = $row.find('.ill-input').text();
const output = $row.find('.ill-output').text();
if (output) {
const escapedInput = input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
text = text.replace(new RegExp(escapedInput, "gi"), output);
changed = true;
}
});
if (changed) {
$('#wpSummary').val((i, v) => (v ? v + "; " : "") + this.Summary);
$textbox.textSelection('setContents', text);
$wrapper.remove();
$editform.show();
if (this.Config.AutoDiff) $('#wpDiff').click();
}
},
escapeHTML(str) {
return str.replace(/[&<>"']/g, m => ({
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
})[m]);
}
};
window.IllWill = IllWill;
mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => {
$(() => IllWill.setup());
});
})();
/* global ComradeLib, mw, $ */
(async function() {
try {
await mw.loader.getScript('https://test.wikipedia.org/w/index.php?title=User:Sam_Sailor/test.js/comrade-options.js&action=raw&ctype=text/javascript');
} catch (e) {
console.warn("Comrade: Could not load /comrade-options.js, using fallback defaults.", e);
}
window.ComradeOptions = Object.assign({
version: "0.5.0",
excludedTemplates: [],
nudgeTimeout: 10000,
debugMode: false
}, window.ComradeOptions || {});
await mw.loader.getScript('https://test.wikipedia.org/w/index.php?title=User:Sam_Sailor/test.js/comrade-lib.js&action=raw&ctype=text/javascript');
async function getTargetPage(api, name, type) {
const titles = type === 'surname' ? [`List of people with the English surname ${name}`, `List of people with surname ${name}`, `${name} (surname)`, `${name} (name)`, name] : [`List of people with given name ${name}`, `List of people named ${name}`, `${name} (given name)`, `${name} (name)`, name];
const res = await api.get({
action: 'query',
titles: titles.join('|'),
formatversion: 2
});
if (!res.query || !res.query.pages) return null;
return titles.find(t => {
const pg = res.query.pages.find(p => p.title === t);
return pg && !pg.missing;
});
}
async function processHumanNameLists(api, entity, wikitext, currentTitle, hasShortDesc, isOrphan, tClean) {
const {
givens,
surnames
} = await fetchWikidataNameLabels(entity);
const nameParts = tClean.split(' ');
const hasFamilyNameHatnote = /\{\{family[ _]name[ _]hatnote/i.test(wikitext);
const primarySurname = hasFamilyNameHatnote ? nameParts[0] : (getSmartSortKey(tClean, entity).split(',')[0] || nameParts[nameParts.length - 1]);
for (const name of givens) {
if (name.toLowerCase() === primarySurname.toLowerCase()) continue;
const nameInTitle = new RegExp('\\b' + name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b', 'i').test(tClean);
if (!nameInTitle) continue;
const target = await getTargetPage(api, name, 'given');
if (target) await verifyAndNudge(api, name, 'given name', currentTitle, hasShortDesc, isOrphan, target);
}
const finalSurnames = surnames.length > 0 ? surnames : (nameParts.length >= 2 ? [nameParts[nameParts.length - 1]] : []);
for (const name of finalSurnames) {
const nameInTitle = new RegExp('\\b' + name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b', 'i').test(tClean);
if (!nameInTitle) continue;
const target = await getTargetPage(api, name, 'surname');
if (target) await verifyAndNudge(api, name, 'surname', currentTitle, hasShortDesc, isOrphan, target);
}
}
async function verifyAndNudge(api, name, type, currentTitle, hasShortDesc, isOrphan, foundTitle) {
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: foundTitle,
rvprop: 'content',
formatversion: 2
});
const page = res.query.pages[0];
if (!page || page.missing || !page.revisions) return;
const text = page.revisions[0].content;
const isNameMatch = /\{\{(Surname|Hndis|Given[ _]name|Forename|Givenname)/i.test(text);
const isNameDab = /\{\{(Disambiguation|Dab|Disambig)(?:[^}]*\|(surname|surnames|given[ _]name|given[ _]names)|(?:\s*\}\}))/i.test(text);
const isNamePageTitle = foundTitle.includes('List of people with');
if (!isNameMatch && !isNameDab && !isNamePageTitle) return;
const escapedTitle = currentTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/ /g, '[ _]');
const alreadyListed = new RegExp('(\\[\\[|\\{\\{anbl\\|[ _]*|\\|)' + escapedTitle, 'i').test(text);
if (alreadyListed) return;
const $container = $('<div>').addClass('comrade-container').addClass(isOrphan ? 'comrade-nudge' : 'comrade-info');
const $header = $('<div>').addClass('comrade-header').append($('<strong>').text(isOrphan ? 'Comrade nudge:' : 'Informative:'));
const $content = $('<div>').addClass('comrade-content');
$content.append($('<span>').append(`${type.charAt(0).toUpperCase() + type.slice(1)} not listed at `, $('<a>').attr({
href: mw.util.getUrl(foundTitle),
target: '_blank'
}).text(foundTitle), "; should it be?"));
const snippet = '* {{anbl|' + currentTitle + '}}';
const $snippetWrapper = $('<div>').css({
'display': 'flex',
'align-items': 'center',
'gap': '10px',
'margin-top': '4px'
});
const $instructionSpan = $('<span>').append('Consider adding ', $('<code>').text(snippet));
const $copyBtn = $('<button>').text('Copy & insert').on('click', function() {
navigator.clipboard.writeText('\n' + snippet).then(() => {
$(this).text('Copied!').prop('disabled', true);
const editUrl = mw.util.getUrl(foundTitle, {
action: 'edit',
summary: 'Adding * {{[[Template:Annotated biography link|anbl]]|[[' + currentTitle + ']]}}'
});
window.open(editUrl, '_blank');
});
});
$snippetWrapper.append($instructionSpan, $copyBtn);
$content.append($snippetWrapper);
if (!hasShortDesc) {
$content.append($('<span>').css({
'color': '#d33',
'font-weight': 'bold',
'margin-top': '5px'
}).text('⚠️ Missing short description: Please add a concise one before listing.'));
}
$container.append($header, $content);
ComradeLib.appendDismiss($container);
}
async function checkNamingPrecision(currentTitle, isHuman) {
const disambigMatch = currentTitle.match(/^(.+?)(?:, | \()/);
if (isHuman || !disambigMatch) return;
const baseTitle = disambigMatch[1].trim();
const api = new mw.Api();
try {
const baseRes = await api.get({
action: 'query',
titles: baseTitle,
redirects: 1,
formatversion: 2
});
const page = baseRes.query.pages[0];
const baseExists = !page.missing;
let baseRedirectsToSelf = false;
if (baseRes.query.redirects) {
baseRedirectsToSelf = baseRes.query.redirects.some(r => r.to === currentTitle);
}
const searchRes = await api.get({
action: 'query',
list: 'allpages',
apprefix: baseTitle,
aplimit: 50,
formatversion: 2
});
const competitors = searchRes.query.allpages.filter(p => {
const t = p.title;
return t !== currentTitle && t !== baseTitle && (t.startsWith(baseTitle + ', ') || t.startsWith(baseTitle + ' ('));
});
if (!baseExists || baseRedirectsToSelf) {
const $container = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $content = $('<div>').addClass('comrade-content');
if (competitors.length === 0) {
$header.append($('<strong>').text('Precision:'));
const reason = baseRedirectsToSelf ? "already redirects here" : "is free";
$content.append($('<span>').append(`The base name `, $('<code>').text(baseTitle), ` ${reason}. Consider moving this article.`));
const moveUrl = mw.util.getUrl('Special:MovePage/' + currentTitle, {
wpNewTitle: baseTitle,
wpReason: 'Unnecessary disambiguation; base name is free/redirects here per [[WP:PRECISION]]'
});
$content.append($('<button>').text(`Move to ${baseTitle}`).on('click', () => window.open(moveUrl, '_blank')));
} else {
$header.append($('<strong>').text('Observation:'));
const state = baseRedirectsToSelf ? 'redirect to this page' : 'redlink';
$content.append($('<span>').append(`Base name `, $('<code>').text(baseTitle), ` has ${competitors.length} competitors, but it is currently a ${state}.`));
$content.append($('<span>').text('Consider if a disambiguation page is needed.'));
}
$container.append($header, $content);
ComradeLib.appendDismiss($container);
}
} catch (e) {
console.warn("Comrade: Precision check failed", e);
}
}
async function checkHumanSort(wikitext, entity, currentTitle, tClean, refName) {
const dsMatch = wikitext.match(/\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/i);
const {
givens
} = await fetchWikidataNameLabels(entity);
if (dsMatch) {
const currentDS = dsMatch[1].trim();
if (currentDS.includes(',')) {
let [lastPart, firstPart] = currentDS.split(',').map(s => s.trim());
if (firstPart.toLowerCase().endsWith(lastPart.toLowerCase())) {
let fixedFirst = firstPart.substring(0, firstPart.length - lastPart.length).trim();
let suggestion = `${fixNameCasing(lastPart, refName)}, ${fixNameCasing(fixedFirst, refName)}`;
if (suggestion !== currentDS) renderSortNudge(suggestion, "Redundant surname in given name field.");
} else if (givens.length > 0 && lastPart.split(' ').length > 1) {
const parts = lastPart.split(' ');
const misplacedGiven = parts.find(part => givens.some(gn => gn.toLowerCase() === part.toLowerCase()));
if (misplacedGiven) {
const newSurname = parts.filter(p => p !== misplacedGiven).join(' ');
let suggestion = `${fixNameCasing(newSurname, refName)}, ${fixNameCasing(misplacedGiven + ' ' + firstPart, refName)}`;
if (suggestion !== currentDS) renderSortNudge(suggestion, `Wikidata suggests "${misplacedGiven}" is a given name.`);
}
}
}
} else {
const hasFamilyNameHatnote = /\{\{family[ _]name[ _]hatnote/i.test(wikitext);
const nameParts = tClean.split(' ');
const targetSortName = hasFamilyNameHatnote ? `${nameParts[0]}, ${nameParts.slice(1).join(' ')}` : getSmartSortKey(tClean, entity);
if (targetSortName.toLowerCase() !== currentTitle.toLowerCase()) {
const $dsMissing = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Add: {{DEFAULTSORT:${targetSortName}}}`).on('click', () => performSortCorrection(targetSortName, "add"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Tag is missing. `, $btn));
$dsMissing.append($header);
ComradeLib.appendDismiss($dsMissing);
}
}
}
async function fetchWikidataEntity(qid) {
const res = await fetch(`https://www.wikidata.org/w/api.php?action=wbgetentities&ids=${qid}&props=claims|descriptions|labels&languages=en&format=json&origin=*`).then(r => r.json());
return res.entities[qid];
}
function getSmartSortKey(fullName, entity) {
const countryIds = entity.claims?.P27?.map(c => c.mainsnak?.datavalue?.value?.id) || [];
const locationIds = entity.claims?.P17?.map(c => c.mainsnak?.datavalue?.value?.id) || [];
const placeIds = [...(entity.claims?.P19 || []), ...(entity.claims?.P119 || [])].map(c => c.mainsnak?.datavalue?.value?.id).filter(Boolean);
const allRelevantIds = [...countryIds, ...locationIds, ...placeIds];
const birthYear = parseInt(entity.claims?.P569?.[0]?.mainsnak?.datavalue?.value?.time?.match(/[+-](\d{4})/)?.[1]) || null;
if (fullName.startsWith("Saint ")) return fullName.replace("Saint ", "");
const arabicParticles = /\b(Abu|Abd|Abdel|Abdul|ben|bin|bint)\b/i;
if (fullName.match(arabicParticles)) {
if (birthYear && birthYear > 1900) {
const parts = fullName.split(' ');
const binIndex = parts.findIndex(p => p.toLowerCase() === 'bin' || p.toLowerCase() === 'ben');
if (binIndex > 0) return parts.slice(binIndex).join(' ') + ', ' + parts.slice(0, binIndex).join(' ');
} else {
return fullName;
}
}
const eastAsianQids = ["Q148", "Q865", "Q884", "Q424", "Q17", "Q881"];
if (allRelevantIds.some(id => eastAsianQids.includes(id))) {
if (allRelevantIds.includes("Q17") && birthYear && birthYear > 1885) return westernSort(fullName);
const parts = fullName.split(' ');
if (parts.length > 1) return `${parts[0]}, ${parts.slice(1).join(' ')}`;
}
return westernSort(fullName);
}
function westernSort(name) {
let cleanName = name;
if (name.startsWith("O'")) cleanName = name.replace("O'", "O");
const parts = cleanName.split(' ');
if (parts.length < 2) return cleanName;
const surname = parts.pop();
if (["Jr.", "Jr", "III", "II", "Sr.", "Sr"].includes(surname)) {
const actualSurname = parts.pop();
return `${actualSurname}, ${parts.join(' ')} ${surname}`;
}
return `${surname}, ${parts.join(' ')}`;
}
async function performSortCorrection(newName, type) {
const api = new mw.Api();
const title = mw.config.get("wgPageName");
try {
const data = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
let content = data.query.pages[0].revisions[0].content;
const dsRegex = /\{\{\s*(?:DEFAULTSORT|Defaultsort)\s*:[^}]+\}\}/i;
const newTag = `{{DEFAULTSORT:${newName}}}`;
if (dsRegex.test(content)) {
content = content.replace(dsRegex, newTag);
} else {
const catRegex = /\[\[Category:/i;
content = catRegex.test(content) ? content.replace(catRegex, `${newTag}\n[[Category:`) : content.trim() + `\n\n${newTag}`;
}
await api.postWithEditToken({
action: 'edit',
title: title,
text: content,
summary: `Setting DEFAULTSORT to ${newName}`,
nocreate: true
});
mw.notify(`DEFAULTSORT ${type === "add" ? "added" : "corrected"} to ${newName}!`, {
title: 'Comrade Success',
type: 'success'
});
setTimeout(() => location.reload(), 700);
} catch (err) {
mw.notify('Failed to save DEFAULTSORT.', {
type: 'error'
});
}
}
function renderSortNudge(target, reason) {
const $dsNudge = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Correct to: ${target}`).on('click', () => performSortCorrection(target, "format"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` ${reason} `, $btn));
$dsNudge.append($header);
ComradeLib.appendDismiss($dsNudge);
}
async function fetchWikidataNameLabels(entity) {
const familyIds = entity.claims?.P734?.map(c => c.mainsnak.datavalue.value.id) || [];
const givenIds = entity.claims?.P735?.map(c => c.mainsnak.datavalue.value.id) || [];
const allNameIds = [...new Set([...familyIds, ...givenIds])];
if (allNameIds.length === 0) return {
givens: [],
surnames: []
};
const url = new URL("https://www.wikidata.org/w/api.php");
url.search = new URLSearchParams({
action: 'wbgetentities',
ids: allNameIds.join('|'),
props: 'labels',
languages: 'en',
format: 'json',
origin: '*'
});
const nameData = await fetch(url).then(r => r.json());
return {
givens: givenIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean),
surnames: familyIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean)
};
}
function fixNameCasing(part, referenceName) {
return part.split(' ').map(word => {
const match = referenceName.match(new RegExp(`\\b${word}\\b`, 'i'));
if (match) return match[0];
return referenceName.split(' ').find(tWord => word.toLowerCase().startsWith(tWord.toLowerCase()) || tWord.toLowerCase().startsWith(word.toLowerCase())) || word;
}).join(' ');
}
async function init() {
const api = new mw.Api();
const ns = mw.config.get("wgNamespaceNumber");
const action = mw.config.get("wgAction");
if (mw.config.get("wgDBname") !== "enwiki" || action !== "view") return;
if (ns === 14) {
const $btn = $('<button>').attr('id', 'comrade-audit-btn').text('Audit category quality').css({
'margin-bottom': '10px',
'padding': '5px 10px',
'cursor': 'pointer'
}).on('click', ComradeLib.performCategoryAudit);
$('.mw-category-generated').prepend($btn);
return;
}
if (ns !== 0) return;
const qid = mw.config.get("wgWikibaseItemId");
const currentTitle = mw.config.get("wgTitle");
try {
const [pageData, backlinkData] = await Promise.all([
api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get("wgPageName"),
rvprop: 'content',
formatversion: 2
}),
api.get({
action: 'query',
list: 'backlinks',
blfilterredir: 'nonredirects',
bllimit: 50,
blnamespace: 0,
bltitle: mw.config.get("wgPageName"),
formatversion: 2
})
]);
const page = pageData.query.pages[0];
if (!page || page.missing || !page.revisions) return;
const wikitext = page.revisions[0].content;
ComradeLib.checkLeadCapitalization(currentTitle);
// Ligature/ASCII-only/diacritic and domain
const norms = ComradeLib.getNormalizedTitles(currentTitle);
if (norms.ascii) {
let label = "ASCII-only";
let category = "R from ASCII-only";
if (/[æÆœŒijIJßẞ]/.test(currentTitle)) {
label = "Ligature";
category = "R to ligature";
} else if (currentTitle.normalize("NFD").match(/[\u0300-\u036f]/)) {
label = "Diacritic";
category = "R to diacritic";
}
await ComradeLib.checkAndRenderRedirect(norms.ascii, currentTitle, category, label);
}
await ComradeLib.checkDomainRedirect(qid, currentTitle);
// Orphan logic
const isOrphanTagged = /\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)/i.test(wikitext) || /\| *orphan *=/i.test(wikitext);
const backLinkCount = backlinkData.query.backlinks.length;
if (isOrphanTagged && backLinkCount >= 1) {
const $container = $('<div>').addClass('comrade-container comrade-status');
const $btn = $('<button>').text('Remove orphan tag').on('click', async () => await ComradeLib.performDeorphan());
const $statusText = $('<span>').append($('<strong>').text('Status:'), ` Article has ${backLinkCount} backlink(s).`, $btn);
$container.append($('<div>').addClass('comrade-header').append($statusText));
ComradeLib.appendDismiss($container);
}
// Wikidata/human logic
let entity = qid ? await fetchWikidataEntity(qid) : null;
let isHuman = entity?.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5");
await checkNamingPrecision(currentTitle, isHuman);
if (isHuman && entity) {
const tClean = currentTitle.replace(/\s*\(.*?\)\s*/g, '').trim();
const refName = entity.labels?.en?.value || currentTitle;
const hasFamilyNameHatnote = /\{\{family[ _]name[ _]hatnote/i.test(wikitext);
const hasShortDesc = /\{\{\s*[Ss]hort description/i.test(wikitext);
const isOrphan = isOrphanTagged && backLinkCount < 1;
await ComradeLib.checkShortDescDates(wikitext, entity);
await ComradeLib.checkNoShortDesc(wikitext, entity);
// Check DEFAULTSORT
await checkHumanSort(wikitext, entity, currentTitle, tClean, refName);
const longName = ComradeLib.getLongNameFromWikitext(wikitext);
if (longName && longName.length > tClean.length) {
let sortKey = hasFamilyNameHatnote ? `${longName.split(' ')[0]}, ${longName.split(' ').slice(1).join(' ')}` : getSmartSortKey(longName, entity);
const rLongWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from long name}}\n}}\n{{DEFAULTSORT:${sortKey}}}`;
await ComradeLib.checkAndRenderRedirect(longName, currentTitle, null, "Long name", rLongWikitext);
}
// Sort name R
const targetSortName = hasFamilyNameHatnote ? `${tClean.split(' ')[0]}, ${tClean.split(' ').slice(1).join(' ')}` : getSmartSortKey(tClean, entity);
if (targetSortName && targetSortName.toLowerCase() !== currentTitle.toLowerCase()) {
let targetPage = currentTitle.includes(' (') ? currentTitle.split(' (')[0] : currentTitle;
let rCat = targetPage !== currentTitle ? "R from ambiguous sort name" : "R from sort name";
if (targetSortName.includes(',')) {
const p = targetSortName.split(',');
rCat += `|${p[0].trim().charAt(0).toUpperCase()}|${p[1].trim().charAt(0).toUpperCase()}`;
}
await ComradeLib.checkAndRenderRedirect(targetSortName, targetPage, rCat, "Sort name");
}
// Name lists
await processHumanNameLists(api, entity, wikitext, currentTitle, hasShortDesc, isOrphan, tClean);
}
// Quality/ORES
const isRedirect = !!mw.config.get("wgIsRedirect");
const userExclusions = window.comradeQualityExclusions || [];
const allExclusions = [...window.ComradeOptions.excludedTemplates, ...userExclusions];
const exclusionRegexString = allExclusions.map(t => t.replace("Template:", "").replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
const exclusionRegex = new RegExp(exclusionRegexString, 'i');
if (isRedirect || exclusionRegex.test(wikitext)) {
console.log("Comrade check skipped: Page is a redirect or excluded via options.");
return;
}
ComradeLib.checkQualityConsistency();
} catch (err) {
console.error("Comrade error:", err);
}
}
init();
})();
(function() {
'use strict';
if (mw.config.get('wgCanonicalSpecialPageName') !== 'Search') {
return;
}
mw.loader.using(['mediawiki.util']).then(function() {
$(function() {
const $resultsInfo = $('.results-info');
const totalResultsRaw = $resultsInfo.attr('data-mw-num-results-total') || '0';
function formatWithCommas(numberString) {
return numberString.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
const state = {
language: mw.config.get('wgContentLanguage'),
totalResultsFormatted: formatWithCommas(totalResultsRaw)
};
let titlesCache = null;
function extractResultTitles() {
const currentResults = $('.mw-search-result-heading');
if (!titlesCache || titlesCache.length !== currentResults.length) {
titlesCache = currentResults.map(function() {
return $(this).find('a:first').attr('title');
}).get().filter(Boolean);
}
return titlesCache;
}
mw.hook("wikipage.content").add(function() {
titlesCache = null;
});
function addLimitOptions() {
const selects = document.querySelectorAll('.cdx-table-pager__limit-form select[name="limit"]');
if (!selects.length) return;
const valuesToAdd = ["1000", "2500", "5000"];
selects.forEach(function(select) {
valuesToAdd.forEach(function(value) {
if (select.querySelector('option[value="' + value + '"]')) return;
const option = document.createElement("option");
option.value = value;
option.textContent = formatWithCommas(value) + " items";
const existingOptions = Array.from(select.querySelectorAll("option"));
const insertBefore = existingOptions.find(function(opt) {
return Number(opt.value) > Number(value);
});
if (insertBefore) {
select.insertBefore(option, insertBefore);
} else {
select.appendChild(option);
}
});
});
}
mw.hook("wikipage.content").add(addLimitOptions);
const numLinks = $('.mw-numlink');
if (numLinks.length > 4 && !numLinks.eq(4).next().is('a[href*="limit=5000"]')) {
const $firstAnchor = numLinks.eq(4);
const $firstSpan = $('<span> | </span>').insertAfter($firstAnchor);
const url1 = new URL(window.location.href);
url1.searchParams.set('limit', '5000');
$('<a>', {
text: '5000',
title: 'Show 5000 results per page',
href: url1.toString()
}).insertAfter($firstAnchor).before($firstSpan);
}
if (numLinks.length > 9 && !numLinks.eq(9).next().is('a[href*="limit=5000"]')) {
const $secondAnchor = numLinks.eq(9);
const $secondSpan = $('<span> | </span>').insertAfter($secondAnchor);
const url2 = new URL(window.location.href);
url2.searchParams.set('limit', '5000');
$('<a>', {
text: '5000',
title: 'Show 5000 results per page',
href: url2.toString()
}).insertAfter($secondAnchor).before($secondSpan);
}
const $container = $("<div></div>").addClass("enhanced-search-container").hover(function() {
$(this).addClass("hover");
}, function() {
$(this).removeClass("hover");
});
const interwikiResults = document.getElementById("mw-interwiki-results");
if (interwikiResults) {
$container.addClass("enhanced-search-floating").insertBefore("#mw-interwiki-results");
} else {
$container.addClass("enhanced-search-absolute").insertBefore(".searchresults");
}
const totalFormattedStr = state.totalResultsFormatted;
const $titleElement = $("<div>Show a shortened list of<br />this page's " + $('.mw-search-result-heading').length + " results <small>(of total of " + totalFormattedStr + ")</small></div>").addClass("enhanced-header").appendTo($container);
const $contentArea = $("<div></div>").addClass("enhanced-content-area").appendTo($container).hide();
const $note1 = $("<div>Copy this page\'s " + $('.mw-search-result-heading').length + " results to the clipboard as...</div>").addClass("enhanced-note").insertAfter($container).hide();
function setupHoverTooltip(element, message) {
$(element).attr('title', message).hover(function() {
$(this).addClass("hover");
}, function() {
$(this).removeClass("hover");
});
}
const $note2 = $("<div>...plain text<sup> (?)</sup></div>").addClass("enhanced-btn").insertAfter($note1).hide();
setupHoverTooltip($note2, 'This button copies this page\'s results to the clipboard as plain text, i.e. without wikitext markup. Since this copies only the results shown on this search page, make sure you have clicked the "view 20, 50, 100, 250, 500, 5000" link. This format is compatible with PetScan and AutoWikiBrowser (AWB) for example.');
const $note3 = $("<div>...wikitext<sup> (?)</sup></div>").addClass("enhanced-btn").insertAfter($note2).hide();
setupHoverTooltip($note3, 'This button copies this page\'s results to the clipboard and also adds wikitext markup.');
const $note4 = $("<div>...PetScan URL<sup> (?)</sup></div>").addClass("enhanced-btn").insertAfter($note3).hide();
setupHoverTooltip($note4, 'This button copies a PetScan URL with this page\'s results to the clipboard. Note that if the PetScan URL becomes too long (~250+ pages), the URL might not work.');
const $note5 = $("<div>...or this search query's total results of " + totalFormattedStr + "<sup> (?)</sup> as...</div>").addClass("enhanced-note").insertAfter($note4).hide().attr('title', 'Note that the actual limit is 10,000, which is MediaWiki\'s maximum limit.').hover(function() {
$(this).addClass("hover");
}, function() {
$(this).removeClass("hover");
});
const $note6 = $("<div>...PetScan URL<sup> (?)</sup></div>").addClass("enhanced-btn").insertAfter($note5).hide();
setupHoverTooltip($note6, 'This button copies a PetScan URL with all ' + totalFormattedStr + ' results to the clipboard. Note that the maximum limit is 10,000 due to MediaWiki\'s restriction.');
const $note7 = $("<div><sup>(?)</sup> = hover over for tooltip details</div>").addClass("enhanced-note-right").insertAfter($note6).hide();
const noteElements = [$note1, $note2, $note3, $note4, $note5, $note6, $note7];
if (interwikiResults !== null) {
noteElements.forEach($el => $el.addClass("enhanced-floating-elements"));
} else {
noteElements.forEach(function($el, index) {
$el.addClass("enhanced-absolute-elements enhanced-position-" + index);
});
}
function formatPlain() {
return extractResultTitles().join("\r\n");
}
function formatWikitext() {
const titles = extractResultTitles().map(title => "* [[" + title + "]]");
const header = "This is a list of [" + window.location.href + " these search results] (made with the script [[User:JAAqqO/EnhancedSearchPage|EnhancedSearchPage]]):\r\n";
return header + titles.join("\r\n");
}
function formatPetScan() {
const encodedTitles = extractResultTitles().map(encodeURIComponent).join("%0A");
return "https://petscan.wmflabs.org/?active_tab=tab_other_sources&manual_list=" + encodedTitles + "&manual_list_wiki=" + state.language + "wiki&project=wikipedia&language=" + state.language + "&doit=";
}
function formatPetScanTotal() {
const oouiInput = document.getElementById("ooui-php-1");
const value = oouiInput ? oouiInput.value : "";
if (value) {
return "https://petscan.wmflabs.org/?active_tab=tab_other_sources&ns%5B0%5D=1&search_query=" + encodeURIComponent(value) + "&search_wiki=" + state.language + "wiki&search_max_results=10000&project=wikipedia&language=" + state.language + "&doit=";
}
return "";
}
function copyToClipboard(text) {
if (!text) return;
if (!navigator.clipboard) {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.setAttribute('readonly', '');
textarea.style.position = 'absolute';
textarea.style.left = '-9999px';
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
} catch (e) {
console.error('Fallback copy failed', e);
}
textarea.remove();
} else {
navigator.clipboard.writeText(text).catch(function(err) {
console.error("Copy failed: ", err);
});
}
}
function showCopiedMessage(event) {
const posX = event.pageX;
const posY = event.pageY;
const $msg = $("<div class='copied-message'>Copied!</div>").appendTo("body");
$msg.css({
position: "absolute",
top: (posY + 10) + "px",
left: (posX + 10) + "px",
backgroundColor: "#f0f0f0",
border: "1px solid grey",
padding: "5px 10px",
borderRadius: "5px",
zIndex: 9999
});
setTimeout(function() {
$msg.fadeOut("slow", function() {
$(this).remove();
});
}, 500);
}
function performButtonAnimation($element) {
$element.css("transform", "scaleY(0.9)").animate({
opacity: '0.5'
}, "fast", function() {
$(this).animate({
opacity: '1'
}, "slow").css("transform", "scaleY(1)");
});
}
let isExpanded = false;
function showContentArea() {
if (isExpanded) return;
isExpanded = true;
const currentResults = $('.mw-search-result-heading');
$contentArea.empty().append(currentResults.clone());
$contentArea.slideDown();
$(".searchalttitle").remove();
$container.addClass("enhanced-active-container");
$titleElement.remove();
noteElements.forEach($el => $el.show());
}
function bindCopyAction($element, formatterFn) {
$element.click(function(event) {
copyToClipboard(formatterFn());
showCopiedMessage(event);
performButtonAnimation($(this));
});
}
$container.click(showContentArea);
bindCopyAction($note2, formatPlain);
bindCopyAction($note3, formatWikitext);
bindCopyAction($note4, formatPetScan);
bindCopyAction($note6, formatPetScanTotal);
const $pageDown = $("<div><br /><br />[ ↓ ]</div>").addClass("enhanced-scroll-btn").hover(function() {
$(this).addClass("hover");
$(this).attr('title', 'Go to the bottom of the page');
}, function() {
$(this).removeClass("hover");
}).insertBefore('.mw-search-result:eq(0)');
const $pageUp = $("<div>[ ↑ ]</div>").addClass("enhanced-scroll-btn").hover(function() {
$(this).addClass("hover");
$(this).attr('title', 'Go to the top of the page');
}, function() {
$(this).removeClass("hover");
}).insertAfter('.mw-search-result:eq(-1)');
$pageDown.click(function(event) {
event.preventDefault();
$('html, body').animate({
scrollTop: $(document).height()
}, 500);
});
$pageUp.click(function() {
$('html, body').animate({
scrollTop: 0
}, 500);
});
$("<span> (<b>" + $('.mw-search-result-heading').length + "</b> shown on the page)</span>").appendTo('.results-info');
const searchDidYouMean = document.querySelector('.searchdidyoumean');
if (searchDidYouMean) {
searchDidYouMean.style.color = '#b32424';
searchDidYouMean.style.fontSize = '20px';
}
});
});
})();
(function() {
if (window.Headmaster) return;
const VERSION = "0.9.2.1";
const defaultNS = [0, 118];
const userNS = window.headmaster_ns || [];
const config = {
menu: window.headmaster_menu || 'p-cactions',
enableSummary: window.headmaster_do_summary !== false,
customSummary: window.headmaster_summary || "",
allowedNamespaces: Array.from(new Set([...defaultNS, ...userNS])),
autoScan: window.headmaster_auto_scan !== false,
autoRun: window.headmaster_auto_run === false
};
const Headmaster = {
VERSION: VERSION,
DefaultSummary: "Converting [[MOS:PSEUDOHEAD|pseudo-headings]] {details}using [[User:Sam_Sailor/Scripts/Headmaster.js|Headmaster]]. Semicolon markup is reserved for [[MOS:DLIST|description lists]]",
styleInjected: false,
css: `
#hm-panel { clear: both; border: 1px solid #a2a9b1; padding: 15px; background: #f8f9fa; margin-bottom: 1.5em; border-radius: 2px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
#hm-table { width: 100%; border-collapse: collapse; background: white; margin-top: 10px; }
#hm-table th { background: #f2f2f2; border: 1px solid #a2a9b1; padding: 8px; text-align: left; }
#hm-table td { border: 1px solid #a2a9b1; padding: 8px; vertical-align: middle; }
.hm-context { font-family: 'Courier New', monospace; font-size: 0.9em; color: #54595d; background: #fdfdfd; }
#hm-controls { display: flex; justify-content: space-between; align-items: center; margin-top: 15px; }
#hm-controls button { margin-left: 10px; padding: 6px 16px; cursor: pointer; border-radius: 2px; }
.hm-btn-apply { background: #36c; color: white; border: 1px solid #36c; font-weight: bold; }
.hm-btn-apply:hover { background: #447ff5; }
.hm-global-selector { font-size: 0.9em; color: #202122; background: #eaecf0; padding: 8px; border-radius: 2px; border: 1px solid #a2a9b1; }
.hm-global-selector a { font-weight: bold; text-decoration: none; color: #36c; }
.hm-global-selector a:hover { text-decoration: underline; }
.hm-locate-btn { cursor: pointer; color: #36c; border: 1px solid #a2a9b1; background: #f8f9fa; padding: 2px 6px; font-size: 0.85em; border-radius: 2px; }
.hm-locate-btn:hover { background: #fff; border-color: #36c; }
`,
setup() {
if (!config.allowedNamespaces.includes(mw.config.get("wgNamespaceNumber"))) return;
const action = mw.config.get("wgAction");
this.addPortlet(action);
if (action === 'view' && (config.autoScan || config.autoRun)) {
this.silentScan();
}
if (mw.util.getParamValue('headmaster') === '1' && ['edit', 'submit'].includes(action)) {
const params = new URLSearchParams(window.location.search);
params.delete('headmaster');
const searchString = params.toString();
const newUrl = window.location.pathname + (searchString ? '?' + searchString : '');
window.history.replaceState(null, '', newUrl);
this.init();
}
},
addPortlet(action) {
const link = mw.util.addPortletLink(config.menu, '#', 'Headmaster', 'ca-hm', 'Interactively convert pseudo-headings');
if (link) {
$(link).on('click', (e) => {
e.preventDefault();
if (action === 'view') {
this.redirectToEdit();
} else if (['edit', 'submit'].includes(action)) {
this.init();
}
});
}
},
redirectToEdit() {
window.location.href = mw.util.getUrl(mw.config.get("wgPageName"), {
action: 'edit',
headmaster: '1'
});
},
silentScan() {
new mw.Api().get({
action: 'query',
prop: 'revisions',
titles: mw.config.get('wgPageName'),
rvprop: 'content',
rvlimit: 1,
formatversion: 2
}).done((data) => {
const page = data.query.pages[0];
if (!page || !page.revisions) return;
const wikitext = page.revisions[0].content;
const tasks = this.analyzeText(wikitext);
if (tasks.length > 0) {
if (config.autoRun) {
this.redirectToEdit();
} else {
this.notifyDiscovery(tasks.length);
}
}
}).fail((err) => {
mw.log.warn("Headmaster: silentScan failed", err);
});
},
notifyDiscovery(count) {
const $msg = $('<span>').text(`Headmaster: Found ${count} issues. `).append($('<a>').text('Run conversion').css({
fontWeight: 'bold',
cursor: 'pointer'
}).on('click', () => this.redirectToEdit()));
mw.notify($msg, {
tag: 'hm-discovery',
autoHide: false
});
},
init() {
if (!this.styleInjected) {
mw.loader.addStyleTag(this.css);
this.styleInjected = true;
}
const $textbox = $('#wpTextbox1');
const wikitext = $textbox.textSelection('getContents');
if (!wikitext) {
mw.notify("Textbox is empty or not found.", {
type: 'error'
});
return;
}
const tasks = this.analyzeText(wikitext);
if (tasks.length === 0) {
mw.notify("No issues found.");
return;
}
this.showUI(tasks, wikitext);
},
analyzeText(wikitext) {
const lines = wikitext.split("\n");
const tasks = [];
let currentDepth = 1;
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
const headingMatch = line.match(/^(={2,6})\s*(.+?)\s*\1\s*$/);
if (headingMatch) {
currentDepth = headingMatch[1].length;
const prevLine = i > 0 ? lines[i - 1].trim() : null;
const isSmashed = prevLine !== null && prevLine !== "";
const isExtraSpace = i > 1 && lines[i - 1].trim() === "" && lines[i - 2].trim() === "";
if (isSmashed || isExtraSpace) {
tasks.push({
index: i,
type: 'WHITESPACE',
original: lines[i],
isSmashed: isSmashed,
isExtraSpace: isExtraSpace
});
}
} else if (line.startsWith(';') && !line.startsWith(';;')) {
const nextLine = lines[i + 1] || "";
if (!nextLine.trim().startsWith(':')) {
const cleanText = line.substring(1).replace(/\s*:.*$/, "").trim();
const suggestedLevel = "=".repeat(Math.min(currentDepth + 1, 6));
tasks.push({
index: i,
type: 'PSEUDO',
original: line,
cleanText: cleanText,
context: nextLine.substring(0, 60).trim() + (nextLine.length > 60 ? "..." : ""),
suggestedLevel: suggestedLevel
});
}
}
}
return tasks;
},
showUI(tasks, originalWikitext) {
$('#hm-panel').remove();
$('#editform').hide();
const visibleTasks = tasks.filter(t => t.type === 'PSEUDO');
const pseudoCount = visibleTasks.length;
const whitespaceCount = tasks.filter(t => t.type === 'WHITESPACE').length;
let statusMsg = "";
const s = (n) => n !== 1 ? 's' : '';
if (pseudoCount > 0) {
statusMsg = `Found <strong>${pseudoCount}</strong> possible pseudo-heading${s(pseudoCount)}.`;
if (whitespaceCount > 0) {
statusMsg += ` Also fixing <strong>${whitespaceCount}</strong> spacing issue${s(whitespaceCount)} quietly.`;
}
} else {
statusMsg = `Did not find any pseudo-headings, but fixing <strong>${whitespaceCount}</strong> spacing issue${s(whitespaceCount)} quietly.`;
}
const tableRows = pseudoCount > 0 ? visibleTasks.map((task, i) => `
<tr data-task-index="${task.index}">
<td><button class="hm-locate-btn" data-hm-i="${tasks.indexOf(task)}" title="Show in editor">Locate</button></td>
<td class="hm-context"><code>${mw.html.escape(task.original)}</code></td>
<td><label><input type="radio" name="hm-opt-${i}" value="keep"> Keep</label></td>
<td><label><input type="radio" name="hm-opt-${i}" value="bold" checked> <b>Bold</b></label></td>
<td><label><input type="radio" name="hm-opt-${i}" value="head"> Heading (${task.suggestedLevel.length})</label></td>
<td class="hm-context">${mw.html.escape(task.context || "(end of page)")}</td>
</tr>
`).join("") : `<tr><td colspan="6" style="text-align:center; padding: 2em; color: #54595d; background: #f8f9fa;">
No interactive heading changes detected. Only background spacing normalization will be applied.
</td></tr>`;
const $panel = $(`
<div id="hm-panel">
<h3 style="margin-top:0">Headmaster v${this.VERSION}: Review pseudo-headings</h3>
<div class="hm-global-selector" ${pseudoCount === 0 ? 'style="display:none"' : ''}>
<strong>Bulk action:</strong>
<a href="#" id="hm-all-keep">Set all to Keep</a> |
<a href="#" id="hm-all-bold">Set all to Bold</a> |
<a href="#" id="hm-all-head">Set all to Heading</a>
</div>
<table id="hm-table">
<thead>
<tr>
<th>Find</th>
<th>Source</th>
<th colspan="3">Action</th>
<th>Next line context</th>
</tr>
</thead>
<tbody>${tableRows}</tbody>
</table>
<div id="hm-controls">
<span>${statusMsg}</span>
<div>
<button id="hm-cancel" class="mw-ui-button">Cancel</button>
<button id="hm-apply" class="hm-btn-apply">Show changes (diff)</button>
</div>
</div>
</div>
`);
$('#content').prepend($panel);
window.scrollTo(0, 0);
$('.hm-locate-btn').on('click', function() {
$(document).off('mousedown.hmlocate');
const idx = $(this).data('hm-i');
const task = tasks[idx];
const lines = originalWikitext.split('\n');
let charOffset = 0;
for (let j = 0; j < task.index; j++) {
charOffset += lines[j].length + 1;
}
$('#hm-panel').hide();
$('#editform').show();
const $textbox = $('#wpTextbox1');
$textbox.textSelection('setSelection', {
start: charOffset,
end: charOffset + lines[task.index].length
});
$textbox.textSelection('scrollToCaretPosition');
const locateNotify = mw.notify("Locating line " + (task.index + 1) + ". Click outside the editor to return.", {
tag: 'hm-locate',
type: 'success',
autoHide: false
});
setTimeout(() => {
$(document).on('mousedown.hmlocate', function(e) {
if (!$(e.target).closest('#editform, .mw-notification-area').length) {
$(document).off('mousedown.hmlocate');
Promise.resolve(locateNotify).then(n => n.close());
$('#editform').hide();
$('#hm-panel').show();
}
});
}, 200);
});
$('#hm-all-keep').on('click', (e) => {
e.preventDefault();
$('input[value="keep"]').prop('checked', true);
});
$('#hm-all-bold').on('click', (e) => {
e.preventDefault();
$('input[value="bold"]').prop('checked', true);
});
$('#hm-all-head').on('click', (e) => {
e.preventDefault();
$('input[value="head"]').prop('checked', true);
});
$('#hm-cancel').on('click', () => {
$('#hm-panel').remove();
$('#editform').show();
});
$('#hm-apply').on('click', () => this.applyChanges(tasks, originalWikitext));
},
getProcessedSummary(existingSummary, bolds, heads, spaces) {
if (!config.enableSummary) return existingSummary;
let mySummary = "";
let isDefault = false;
if (config.customSummary) {
mySummary = config.customSummary;
} else {
isDefault = true;
if (bolds > 0 || heads > 0) {
const detailsArr = [];
if (bolds > 0) detailsArr.push(`${bolds} to bold`);
if (heads > 0) detailsArr.push(`${heads} to heading`);
const detailsStr = detailsArr.length > 0 ? `(${detailsArr.join(', ')}) ` : "";
mySummary = this.DefaultSummary.replace("{details}", detailsStr);
if (spaces > 0) {
mySummary += "; also handled [[MOS:BLANKLINE]]";
}
} else if (spaces > 0) {
mySummary = "Handling [[MOS:BLANKLINE]] using [[User:Sam_Sailor/Scripts/Headmaster.js|Headmaster]]";
}
}
if (!mySummary) return existingSummary;
const trimmedOld = existingSummary.trim();
if (!trimmedOld) return mySummary;
if (/[.!?]$/.test(trimmedOld)) {
return trimmedOld + " " + mySummary;
} else {
const finalSummary = isDefault ? mySummary.charAt(0).toLowerCase() + mySummary.slice(1) : mySummary;
return trimmedOld + "; " + finalSummary;
}
},
applyChanges(tasks, wikitext) {
try {
let lines = wikitext.split("\n");
let bCount = 0,
hCount = 0,
wCount = 0;
const pseudoTasks = tasks.filter(t => t.type === 'PSEUDO');
for (let i = pseudoTasks.length - 1; i >= 0; i--) {
const task = pseudoTasks[i];
const idx = task.index;
const action = $('#hm-table tbody tr').filter(`[data-task-index="${idx}"]`).find('input:checked').val();
if (action === 'bold') {
let newValue = `'''${task.cleanText}'''`;
if (idx > 0 && lines[idx - 1].trim() !== "") {
newValue = "\n" + newValue;
}
lines[idx] = newValue;
bCount++;
} else if (action === 'head') {
const h = task.suggestedLevel;
let newValue = `${h} ${task.cleanText} ${h}`;
if (idx > 0 && lines[idx - 1].trim() !== "") {
newValue = "\n" + newValue;
}
lines[idx] = newValue;
hCount++;
}
}
const spaceTasks = tasks.filter(t => t.type === 'WHITESPACE');
for (let i = spaceTasks.length - 1; i >= 0; i--) {
const task = spaceTasks[i];
const idx = task.index;
if (task.isSmashed) {
lines.splice(idx, 0, "");
} else if (task.isExtraSpace) {
let backIdx = idx - 1;
while (backIdx > 0 && lines[backIdx].trim() === "" && lines[backIdx - 1].trim() === "") {
lines.splice(backIdx, 1);
backIdx--;
}
}
wCount++;
}
if (bCount + hCount + wCount === 0) {
mw.notify("No changes selected.");
$('#hm-panel').remove();
$('#editform').show();
return;
}
$('#wpTextbox1').textSelection('setContents', lines.join("\n"));
const $summary = $('#wpSummary');
$summary.val(this.getProcessedSummary($summary.val(), bCount, hCount, wCount));
$('#hm-panel').remove();
$('#editform').show();
mw.notify(`Headmaster: Fixed ${wCount} spacing issue(s) and converted ${bCount + hCount} heading(s).`);
$('html, body').animate({
scrollTop: 0
}, 'fast');
$('#wpDiff').click();
} catch (err) {
$('#editform').show();
mw.notify("Headmaster error: " + err.message, {
type: 'error'
});
}
}
};
window.Headmaster = Headmaster;
mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => {
$(() => Headmaster.setup());
});
})();
//</nowiki>
l9av1ibo9qozdpotw9jlg48wt38ndop
749891
749889
2026-07-04T10:22:13Z
Sam Sailor
26820
Test
749891
javascript
text/javascript
//<nowiki>
(function(mw, $, OO) {
'use strict';
const IGNORE_SUFFIXES = ['Sr.', 'Sr', 'Jr.', 'Jr', 'II', 'III', 'IV', 'V', 'MD', 'PhD'];
const LANG_PREFS = ['frwiki', 'dewiki', 'eswiki', 'itwiki', 'nlwiki', 'svwiki'];
const NON_LANG_WIKIS = ['commonswiki', 'specieswiki', 'metawiki', 'mediawikiwiki', 'wikidatawiki', 'wikimaniawiki'];
const suffixRE = new RegExp('\\s(' + IGNORE_SUFFIXES.join('|').replace(/\./g, '\\.') + ')$', 'i');
function GetterDialog(config) {
GetterDialog.super.call(this, config);
this.abortController = null;
}
OO.inheritClass(GetterDialog, OO.ui.ProcessDialog);
GetterDialog.static.name = 'getterDialog';
GetterDialog.static.title = 'Getter: Wikipedia List Builder';
GetterDialog.static.actions = [
{ action: 'generate', label: 'Generate List', flags: ['primary', 'progressive'], modes: 'edit' },
{ action: 'back', label: 'Go back', modes: 'result' },
{ action: 'exit', label: 'Exit', flags: 'safe', modes: ['edit', 'result'] }
];
GetterDialog.prototype.cleanTitle = function(t) { return t.replace(suffixRE, '').replace(/\s?\([^)]*\)$/, '').trim(); };
GetterDialog.prototype.normalizeTitle = function(t) { return t.replace(/_/g, ' ').trim().replace(/^(.)/, c => c.toUpperCase()); };
GetterDialog.prototype.getSortKey = function(title) {
const clean = this.cleanTitle(title);
const parts = clean.split(' ');
return (parts.length <= 1) ? title.toLowerCase() : (parts[parts.length - 1] + ', ' + parts.slice(0, parts.length - 1).join(' ') + ' | ' + title).toLowerCase();
};
GetterDialog.prototype.compileCriteria = function(vals, anchorLC) {
return vals.filter(t => t.toLowerCase() !== anchorLC)
.map(r => new RegExp('^' + r.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*') + '$', 'i'));
};
GetterDialog.prototype.getBestAnchor = function() {
const getTerms = (w) => w.getValue().map(t => t.trim()).filter(Boolean);
const candidates = [
...getTerms(this.surnameTags).map(t => ({ term: t, weight: 2000 + this.score(t) })),
...getTerms(this.givenTags).map(t => ({ term: t, weight: 1000 + this.score(t) })),
...getTerms(this.middleTags).map(t => ({ term: t, weight: 0 + this.score(t) }))
];
return candidates.length === 0 ? null : candidates.reduce((b, c) => (c.weight > b.weight ? c : b)).term;
};
GetterDialog.prototype.score = function(term) {
const wildcardCount = (term.match(/\*/g) || []).length;
return (wildcardCount === 0 ? 1000 : 0) + (term.length - wildcardCount) - wildcardCount;
};
GetterDialog.prototype.formatResult = function(p, mode, includeIll) {
if (mode === 'anbl') return `* {{anbl|${p.title}}}`;
const bY = p.ent?.claims?.P569?.[0]?.mainsnak?.datavalue?.value?.time?.substring(1, 5);
const dY = p.ent?.claims?.P570?.[0]?.mainsnak?.datavalue?.value?.time?.substring(1, 5);
const bio = bY ? ` (${bY}${dY ? '–' + dY : ''})` : '';
const rawDesc = (p.desc || '').replace(/\s?\(\s*(?:born|d\.)?\s*\d{4}(?:\s*–\s*\d{4})?\s*\)$/i, '').trim();
const suffix = rawDesc ? `, ${rawDesc}` : '';
let display = `[[${p.title}]]`;
if (includeIll) {
const langKey = LANG_PREFS.find(l => p.ent.sitelinks?.[l]) || Object.keys(p.ent.sitelinks || {}).find(k => k.endsWith('wiki') && k !== 'enwiki' && !NON_LANG_WIKIS.includes(k));
if (langKey) display = `{{ill|${p.title}|${langKey.replace('wiki', '')}|${p.ent.sitelinks[langKey].title}}}`;
}
return `* ${display}${bio}${suffix}`;
};
GetterDialog.prototype.matchesCriteria = function(title, compiled) {
const clean = this.cleanTitle(title);
const tokens = clean.split(/\s+/);
const given = tokens[0] || '', surname = tokens.length > 1 ? tokens[tokens.length - 1] : '';
const middleLC = tokens.length > 2 ? tokens.slice(1, -1).map(x => x.toLowerCase()) : [];
const check = (val, regexes) => (!regexes || regexes.length === 0 || regexes.some(re => re.test(val)));
return check(given, compiled.given) && check(surname, compiled.surname) && (compiled.middle.length === 0 || compiled.middle.every(m => middleLC.includes(m)));
};
GetterDialog.prototype.getExistingLinks = async function() {
const titles = new Set();
try {
const data = await this.api.get({ action: 'parse', page: mw.config.get('wgPageName'), prop: 'wikitext', format: 'json' });
const text = data.parse?.wikitext?.['*'] || '';
const regexes = [/\[\[([^\]|#]+)/g, /\{\{(?:anbl|Annotated biography link)\|([^}|]+)/gi];
regexes.forEach(re => { let m; while ((m = re.exec(text)) !== null) titles.add(this.normalizeTitle(m[1])); });
} catch (e) { console.error('[Getter] Parse error:', e); }
return titles;
};
async function checkEntitiesData(qids, signal) {
if (!qids.length) return {};
const all = {};
for (let i = 0; i < qids.length; i += 50) {
if (signal?.aborted) throw new DOMException('Aborted', 'AbortError');
try {
const res = await fetch('https://www.wikidata.org/w/api.php?action=wbgetentities&ids=' + qids.slice(i, i + 50).join('|') + '&props=claims|labels|descriptions|sitelinks&languages=en&format=json&origin=*', { signal }).then(r => r.json());
Object.assign(all, res.entities || {});
} catch (e) { if (e.name === 'AbortError') throw e; console.error('[Getter] Wikidata batch error', e); }
}
return all;
}
GetterDialog.prototype.autoPopulateFields = async function () {
const title = mw.config.get('wgTitle'), cleanTitle = this.cleanTitle(title);
const m = title.match(/^(.*?) \((given name|surname)\)$/i);
if (m) { m[2].toLowerCase() === 'given name' ? this.givenTags.addTag(m[1]) : this.surnameTags.addTag(m[1]); return; }
try {
const data = await this.api.get({ action: 'parse', page: mw.config.get('wgPageName'), prop: 'wikitext', format: 'json' });
const text = data.parse?.wikitext?.['*'] || '';
const hasSurname = /\{\{\s*surname\s*(?:\||\}\})/i.test(text);
const givenTemplate = text.match(/\{\{\s*given name\b[^}]*\}\}/i)?.[0] || '';
const isBoth = /\|\s*type\s*=\s*both\b/i.test(givenTemplate);
if (hasSurname || (givenTemplate && isBoth)) this.surnameTags.addTag(cleanTitle);
if (givenTemplate) this.givenTags.addTag(cleanTitle);
if (!hasSurname && !givenTemplate) this.givenTags.addTag(cleanTitle);
} catch (e) { console.error('[Getter] Inspect error', e); this.givenTags.addTag(cleanTitle); }
};
GetterDialog.prototype.initialize = function() {
GetterDialog.super.prototype.initialize.apply(this, arguments);
this.api = new mw.Api();
this.panel = new OO.ui.PanelLayout({ padded: true, expanded: false });
this.givenTags = new OO.ui.TagMultiselectWidget({ placeholder: 'Given names...', allowArbitrary: true });
this.middleTags = new OO.ui.TagMultiselectWidget({ placeholder: 'Middle terms...', allowArbitrary: true });
this.surnameTags = new OO.ui.TagMultiselectWidget({ placeholder: 'Surnames...', allowArbitrary: true });
this.excludeTags = new OO.ui.TagMultiselectWidget({ placeholder: 'Exclude strings...', allowArbitrary: true });
this.humanOnly = new OO.ui.CheckboxInputWidget({ selected: true });
this.excludeHumans = new OO.ui.CheckboxInputWidget({ selected: false });
this.includeIll = new OO.ui.CheckboxInputWidget({ selected: false });
this.sortBySurname = new OO.ui.CheckboxInputWidget({ selected: true });
this.updateMode = new OO.ui.CheckboxInputWidget({ selected: false });
this.excludeHumans.on('change', (s) => { if (s) { this.humanOnly.setSelected(false); this.sortBySurname.setSelected(false); } });
this.humanOnly.on('change', (s) => { if (s) this.excludeHumans.setSelected(false); });
this.formatSelect = new OO.ui.ButtonSelectWidget({ items: [new OO.ui.ButtonOptionWidget({ data: 'wiki', label: 'Wiki' }), new OO.ui.ButtonOptionWidget({ data: 'anbl', label: 'ANBL' })]});
this.formatSelect.selectItemByData('wiki');
this.fieldset = new OO.ui.FieldsetLayout({ label: 'Advanced Filter Settings' });
this.fieldset.addItems([
new OO.ui.FieldLayout(this.givenTags, { label: 'Given names', align: 'top' }),
new OO.ui.FieldLayout(this.middleTags, { label: 'Middle terms', align: 'top' }),
new OO.ui.FieldLayout(this.surnameTags, { label: 'Surnames', align: 'top' }),
new OO.ui.FieldLayout(this.excludeTags, { label: 'Exclude strings', align: 'top' }),
new OO.ui.FieldLayout(this.humanOnly, { label: 'Humans only', align: 'inline' }),
new OO.ui.FieldLayout(this.excludeHumans, { label: 'Exclude humans', align: 'inline' }),
new OO.ui.FieldLayout(this.sortBySurname, { label: 'Sort by surname', align: 'inline' }),
new OO.ui.FieldLayout(this.updateMode, { label: 'Exclude existing links', align: 'inline' }),
new OO.ui.FieldLayout(this.includeIll, { label: 'Include {{ill}}', align: 'inline' }),
new OO.ui.FieldLayout(this.formatSelect, { label: 'Format', align: 'top' })
]);
this.resultArea = new OO.ui.MultilineTextInputWidget({ rows: 15, autosize: true });
this.copyButton = new OO.ui.ButtonWidget({ label: 'Copy list', icon: 'copy', flags: ['progressive'] });
this.copyButton.$element.css('marginTop', '10px');
this.copyButton.on('click', () => navigator.clipboard.writeText(this.resultArea.getValue()).then(() => mw.notify('Copied!')));
this.resultContainer = new OO.ui.PanelLayout({ padded: true, expanded: false });
this.resultContainer.$element.append(new OO.ui.FieldLayout(this.resultArea, { label: 'Results', align: 'top' }).$element, this.copyButton.$element).hide();
this.panel.$element.append(this.fieldset.$element, this.resultContainer.$element);
this.$body.append(this.panel.$element);
this.actions.setMode('edit');
this.autoPopulateFields();
};
GetterDialog.prototype.runGetter = async function() {
this.abortController = new AbortController();
try {
const { humanOnly, excludeHumans, includeIll, sortBySurname, formatSelect, updateMode } = this;
const mode = formatSelect.findSelectedItem().getData(), anchor = this.getBestAnchor();
if (!anchor) { mw.notify('Please enter search terms.', { type: 'error' }); return; }
const existing = updateMode.isSelected() ? await this.getExistingLinks() : new Set(), resultsMap = new Map();
const excludeList = this.excludeTags.getValue().map(e => e.toLowerCase()), anchorLC = anchor.toLowerCase();
const compiledCriteria = {
given: this.compileCriteria(this.givenTags.getValue(), anchorLC),
surname: this.compileCriteria(this.surnameTags.getValue(), anchorLC),
middle: this.middleTags.getValue().filter(t => t.toLowerCase() !== anchorLC).map(m => m.toLowerCase())
};
let cont = {};
do {
const data = await this.api.get({ action: 'query', list: 'search', srsearch: `intitle:"${anchor}"`, srlimit: 'max', sroffset: cont.sroffset || 0, format: 'json' });
for (const p of (data.query?.search || [])) {
if (excludeList.some(e => p.title.toLowerCase().includes(e))) continue;
const title = this.normalizeTitle(p.title);
if (!existing.has(title) && this.matchesCriteria(title, compiledCriteria)) resultsMap.set(p.title, { title: p.title, pageid: p.pageid });
}
cont = data.continue || {};
} while (cont.sroffset && !this.abortController?.signal.aborted);
const pageIds = Array.from(resultsMap.values()).map(p => p.pageid), confirmedResults = new Map();
for (let i = 0; i < pageIds.length; i += 50) {
if (this.abortController?.signal.aborted) return;
const chunk = pageIds.slice(i, i + 50), qidList = [], qidMap = {};
const pageData = await this.api.get({ action: 'query', prop: 'pageprops|description', pageids: chunk.join('|'), format: 'json' });
Object.values(pageData.query.pages).forEach(p => { if (p.pageprops?.wikibase_item) { qidList.push(p.pageprops.wikibase_item); qidMap[p.pageprops.wikibase_item] = p; } });
const entities = await checkEntitiesData(qidList, this.abortController?.signal);
for (const qid in entities) {
const ent = entities[qid], p = qidMap[qid];
const isHuman = ent.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5");
if ((humanOnly.isSelected() && !isHuman) || (excludeHumans.isSelected() && isHuman)) continue;
confirmedResults.set(p.title, { title: p.title, desc: p.description || ent.descriptions?.en?.value || '', ent });
}
}
this.resultArea.setValue(Array.from(confirmedResults.values())
.map(p => ({ formatted: this.formatResult(p, mode, includeIll.isSelected()), sortKey: sortBySurname.isSelected() ? this.getSortKey(p.title) : p.title.toLowerCase() }))
.sort((a, b) => a.sortKey.localeCompare(b.sortKey, undefined, { sensitivity: 'base' }))
.map(r => r.formatted).join('\n'));
this.fieldset.$element.hide(); this.resultContainer.$element.show(); this.actions.setMode('result');
} catch (e) { if (e.name !== 'AbortError') { console.error('[Getter] Generator error:', e); mw.notify('Error generating list.', { type: 'error' }); } }
finally { this.abortController = null; }
};
GetterDialog.prototype.getActionProcess = function(action) {
if (action === 'generate') return new OO.ui.Process(async () => { await this.runGetter(); });
if (action === 'back') { if (this.abortController) this.abortController.abort(); return new OO.ui.Process(() => { this.resultContainer.$element.hide(); this.fieldset.$element.show(); this.actions.setMode('edit'); }); }
if (action === 'exit') { if (this.abortController) this.abortController.abort(); return new OO.ui.Process(() => this.close()); }
return GetterDialog.super.prototype.getActionProcess.call(this, action);
};
mw.loader.using(['mediawiki.api', 'mediawiki.util', 'oojs-ui', 'oojs-ui-widgets', 'oojs-ui-windows']).then(() => {
const wm = new OO.ui.WindowManager();
$(document.body).append(wm.$element);
const dialog = new GetterDialog({ size: 'medium' });
wm.addWindows([dialog]);
$(mw.util.addPortletLink('p-tb', '#', 'Getter', 't-getter')).on('click', (e) => { e.preventDefault(); wm.openWindow(dialog); });
});
}(mediaWiki, jQuery, OO));
(function() {
if (window.IllWill) return;
const STRIKEOUT = true;
const HIDE = 'hide';
const IllWill = {
Version: "0.5",
Attribution: 'created by <a target="_blank" href="/wiki/User:Cobaltcigs">cobaltcigs</a>',
Summary: "Added interlanguage links using IllWill.js",
DataApi: "https://www.wikidata.org/w/api.php",
LocalLang: mw.config.get("wgPageContentLanguage"),
Disambiguator: / \(.*\)$/,
SiteLinks: {},
Config: {
MaxLanguages: 10,
IgnoreScientific: STRIKEOUT,
AutoDiff: true
},
css: `
#illwill { clear: both; margin-bottom: 1.5em; border: 1px solid #a2a9b1; padding: 15px; background: #f8f9fa; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.ill-sitelinks a { display: inline-block; margin-bottom: 3px; font-size: 0.9em; color: #36c; }
.ill-size-tag { font-size: 0.8em; color: #72777d; margin-left: 6px; white-space: nowrap; }
.illwill-ignore { text-decoration: line-through; color: #72777d; font-size: 0.85em; }
.ill-x { width: 35px; text-align: center; }
.ill-input, .ill-output { font-family: 'Courier New', monospace; white-space: pre-wrap; font-size: 0.9em; background: #fff; border: 1px solid #eaecf0; padding: 4px; border-radius: 2px; }
#ill-buttons { text-align: right; padding: 10px; border-top: 1px solid #a2a9b1; margin-top: 10px; }
#ill-buttons button { margin-left: 10px; cursor: pointer; padding: 6px 16px; border-radius: 2px; border: 1px solid #a2a9b1; transition: all 0.2s; }
#ill-ok { background: #36c; color: #fff; border-color: #36c !important; font-weight: bold; }
#ill-ok:hover { background: #447ff5; }
#ill-ok:disabled { background: #eaecf0; color: #72777d; border-color: #c8ccd1 !important; cursor: not-allowed; opacity: 0.7; }
#ill-table { width: 100%; border-collapse: collapse; margin-top: 10px; background: white; }
#ill-table th { background: #f2f2f2; text-align: left; padding: 8px; border: 1px solid #a2a9b1; }
#ill-table td { padding: 8px; border: 1px solid #a2a9b1; vertical-align: top; }
#ill-table caption { background: #eaffea; font-weight: bold; padding: 10px; border: 1px solid #a2a9b1; border-bottom: none; font-size: 1.1em; }
#ill-help { font-size: 0.85em; color: #54595d; padding: 10px; line-height: 1.4; }
.ill-loading-inline { font-style: italic; color: #72777d; font-size: 0.9em; display: block; margin: 5px 0; }
`,
setup() {
if (!["edit", "submit"].includes(mw.config.get('wgAction'))) return;
mw.loader.addStyleTag(this.css);
const link = mw.util.addPortletLink('p-cactions', '#', 'IllWill', 'ca-illwill', "Add interlanguage links via Wikidata");
if (link) {
link.addEventListener('click', (e) => {
e.preventDefault();
this.makePanel();
});
}
},
norm(s) {
if (!s) return "";
const t = s.trim().replace(/[\s_]+/g, " ");
return t.charAt(0).toUpperCase() + t.slice(1);
},
async getRedLinks($textbox) {
const txt = $textbox.textSelection('getContents') || "";
const wl = txt.match(/\[\[[^[\]\n]+\]\]/g);
if (!wl) return {
reds: [],
txt
};
const api = new mw.Api();
const textToParse = wl.join("").replace(/\|[^\]]+/g, "");
try {
const data = await api.post({
action: 'parse',
text: textToParse,
contentmodel: 'wikitext',
formatversion: 2
});
const tempDiv = document.createElement('div');
tempDiv.innerHTML = data.parse.text;
const reds = Array.from(tempDiv.querySelectorAll("a.new")).map(x => {
const urlParams = new URLSearchParams(x.href.split('?')[1]);
return urlParams.get('title')?.replace(/_/g, " ");
}).filter(Boolean);
return {
reds: [...new Set(reds)],
txt
};
} catch (e) {
return {
reds: [],
txt
};
}
},
async makePanel() {
const $editform = $('#editform');
const $textbox = $('#wpTextbox1');
$('#illwill').remove();
$editform.hide();
const $wrapper = $('<div id="illwill">Loading redlinks...</div>').insertBefore($editform);
const {
reds,
txt
} = await this.getRedLinks($textbox);
const wtLinks = [];
reds.forEach(title => {
const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\s/g, "[\\s_]+");
const re = new RegExp("\\[\\[\\s*" + escaped + "\\s*(?:\\|[^\\[\\]]+)?\\]\\]", "ig");
const match = txt.match(re);
if (match && !wtLinks.some(z => z.title === title)) {
wtLinks.push({
link: match[0],
title
});
}
});
if (!wtLinks.length) {
const $back = $('<button>Quit</button>').on('click', () => {
$wrapper.remove();
$editform.show();
});
$wrapper.empty().append($('<strong>No valid redlinks found in text.</strong> '), $back);
return;
}
const $table = $(`
<table id="ill-table" class="wikitable">
<caption>IllWill.js (v${this.Version})</caption>
<thead>
<tr>
<th class="ill-x"><input type="checkbox" id="ill-select-all" disabled></th>
<th>Input link</th>
<th>Possible Wikidata matches</th>
<th>Sitelinks (top ${this.Config.MaxLanguages} by size)</th>
<th>Resulting wikitext</th>
</tr>
</thead>
<tbody></tbody>
<tfoot>
<tr>
<td colspan="3" id="ill-help">Carefully examine possible Wikidata matches.</td>
<td colspan="2" id="ill-buttons">
<button id="ill-cancel">Cancel</button>
<button id="ill-ok" disabled>Show changes</button>
</td>
</tr>
</tfoot>
</table>
`);
const $tbody = $table.find('tbody');
wtLinks.forEach((item, i) => {
$tbody.append(`
<tr id="ill-row-${i}" data-index="${i}">
<td class="ill-x"><input type="checkbox" class="ill-row-check" disabled></td>
<td class="ill-input">${this.escapeHTML(item.link)}</td>
<td class="ill-wd-item"><span class="ill-loading-inline">Searching...</span></td>
<td class="ill-sitelinks"></td>
<td class="ill-output"></td>
</tr>
`);
});
$wrapper.empty().append($table);
const chunkSize = 5;
for (let i = 0; i < wtLinks.length; i += chunkSize) {
const chunk = wtLinks.slice(i, i + chunkSize);
await Promise.all(chunk.map((item, idx) => this.fetchWikidataOptimized(item.title, i + idx)));
}
$table.on('click', '#ill-cancel', () => {
$editform.show();
$wrapper.remove();
});
$table.on('click', '#ill-ok', () => this.onOkButton($editform, $wrapper, $textbox));
$table.on('change', '#ill-select-all', (e) => {
const isChecked = $(e.target).is(':checked');
$table.find('.ill-row-check:not(:disabled)').each(function() {
$(this).prop('checked', isChecked).trigger('change');
});
});
$table.on('change', '.ill-row-check', (e) => {
this.onCheckbox(e);
this.updateOkButtonState($table);
});
$table.on('change', 'input[type="radio"]', (e) => {
this.showSiteLinks(e);
});
},
async fetchWikidataOptimized(title, index) {
const query = title.replace(this.Disambiguator, "");
const url = `${this.DataApi}?action=wbsearchentities&search=${encodeURIComponent(query)}&language=${this.LocalLang}&limit=5&format=json&origin=*`;
try {
const resp = await fetch(url);
const data = await resp.json();
const results = data.search;
const $cell = $(`#ill-row-${index} .ill-wd-item`);
if (!results || !results.length) {
$cell.html('<span class="ill-na">(no results)</span>');
return;
}
const entityIds = results.map(r => r.id);
await this.fetchEntities(entityIds, index, $cell);
} catch (e) {
$(`#ill-row-${index} .ill-wd-item`).text("Search failed.");
}
},
async fetchEntities(ids, rowIndex, $cell) {
const url = `${this.DataApi}?action=wbgetentities&ids=${ids.join('|')}&props=labels|descriptions|claims|sitelinks&format=json&origin=*`;
try {
const resp = await fetch(url);
const data = await resp.json();
$cell.empty();
ids.forEach(qNum => {
const entity = data.entities[qNum];
if (!entity) return;
const isScientific = entity.claims?.P31?.some(s => s.mainsnak?.datavalue?.value?.id === "Q13442814");
if (isScientific && this.Config.IgnoreScientific === HIDE) return;
const desc = entity.descriptions?.[this.LocalLang]?.value || entity.descriptions?.en?.value || "";
const label = entity.labels?.[this.LocalLang]?.value || entity.labels?.en?.value || qNum;
const isStrikeout = isScientific && this.Config.IgnoreScientific === STRIKEOUT;
const $div = $(`
<div class="${isStrikeout ? 'illwill-ignore' : ''}">
<input type="radio" name="selection-${rowIndex}" value="${qNum}" id="radio-${qNum}" ${isStrikeout ? 'disabled' : ''}>
<label for="radio-${qNum}">
<a href="https://wikidata.org/wiki/${qNum}" target="_blank">${this.escapeHTML(label)}</a>:
<span class="ill-desc">${desc ? this.escapeHTML(desc) : '<i>no description</i>'}</span>
</label>
</div>
`);
$cell.append($div);
this.SiteLinks[qNum] = entity.sitelinks || {};
});
} catch (e) {
$cell.text("Detail fetch failed.");
}
},
updateOkButtonState($table) {
const $rows = $table.find('.ill-row-check:not(:disabled)');
const $checkedRows = $rows.filter(':checked');
const anyEnabled = $rows.length > 0;
$table.find('#ill-ok').prop('disabled', $checkedRows.length === 0);
const $master = $table.find('#ill-select-all');
$master.prop('disabled', !anyEnabled);
if (anyEnabled && $rows.length === $checkedRows.length) {
$master.prop('checked', true);
} else {
$master.prop('checked', false);
}
},
async showSiteLinks(e) {
const isManualToggling = e.fromCheckbox || false;
const qNum = e.target.value;
const $row = $(e.target).closest('tr');
const sitelinks = this.SiteLinks[qNum];
const $checkbox = $row.find('.ill-row-check');
const $sitelinkCell = $row.find('.ill-sitelinks');
const $outputCell = $row.find('.ill-output');
const $table = $('#ill-table');
const effectiveMax = Math.min(Math.max(this.Config.MaxLanguages, 3), 6);
const validKeys = Object.keys(sitelinks).filter(k => k.endsWith('wiki') && !k.includes('commons'));
$sitelinkCell.html('<span class="ill-loading-inline">Measuring articles...</span>');
$outputCell.empty();
if (!isManualToggling) {
$checkbox.prop('disabled', true).prop('checked', false);
}
const candidates = validKeys.slice(0, 25);
const sizeMap = [];
await Promise.all(candidates.map(async (key) => {
const lang = key.replace('wiki', '').replace(/_/g, '-');
const title = sitelinks[key].title;
const endpoint = `https://${lang}.wikipedia.org/w/api.php?action=query&titles=${encodeURIComponent(title)}&prop=info&format=json&origin=*`;
try {
const res = await fetch(endpoint).then(r => r.json());
const page = Object.values(res.query.pages)[0];
sizeMap.push({
lang,
title,
length: page.length || 0
});
} catch (err) {
sizeMap.push({
lang,
title,
length: 0
});
}
}));
sizeMap.sort((a, b) => b.length - a.length);
const topLinks = sizeMap.slice(0, effectiveMax);
$sitelinkCell.html(topLinks.map(item => {
const kbValue = Math.round(item.length / 1024);
return `<div><a href="https://${item.lang}.wikipedia.org/wiki/${encodeURIComponent(item.title)}" target="_blank">${item.lang}:${this.escapeHTML(item.title)}</a> <span class="ill-size-tag">(${kbValue} kB)</span></div>`;
}).join(""));
const inputWikitext = $row.find('.ill-input').text();
const match = inputWikitext.match(/\[\[(.*?)\]\]/);
if (match) {
const parts = match[1].split('|');
const targetPage = parts[0].trim();
const surfaceName = (parts[1] || parts[0]).trim();
const illLangs = topLinks.map(item => {
const isSameTitle = this.norm(item.title) === this.norm(targetPage);
return `|${item.lang}|${isSameTitle ? '' : item.title}`;
}).join("");
const ltParam = (this.norm(surfaceName) === this.norm(targetPage)) ? "" : `|lt=${surfaceName}`;
$outputCell.text(`{{ill|${targetPage}${ltParam}${illLangs}}}`);
}
$checkbox.prop('disabled', !topLinks.length);
if (!isManualToggling) {
$checkbox.prop('checked', !!topLinks.length);
}
this.updateOkButtonState($table);
},
onCheckbox(e) {
const $row = $(e.target).closest('tr');
const $radio = $row.find('input[type="radio"]:checked');
const isChecked = $(e.target).is(':checked');
if (isChecked) {
if ($radio.length) {
this.showSiteLinks({
target: $radio[0],
fromCheckbox: true
});
}
} else {
$row.find('.ill-sitelinks, .ill-output').empty();
}
},
onOkButton($editform, $wrapper, $textbox) {
let text = $textbox.textSelection('getContents');
let changed = false;
$wrapper.find('.ill-row-check:checked').each(function() {
const $row = $(this).closest('tr');
const input = $row.find('.ill-input').text();
const output = $row.find('.ill-output').text();
if (output) {
const escapedInput = input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
text = text.replace(new RegExp(escapedInput, "gi"), output);
changed = true;
}
});
if (changed) {
$('#wpSummary').val((i, v) => (v ? v + "; " : "") + this.Summary);
$textbox.textSelection('setContents', text);
$wrapper.remove();
$editform.show();
if (this.Config.AutoDiff) $('#wpDiff').click();
}
},
escapeHTML(str) {
return str.replace(/[&<>"']/g, m => ({
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
})[m]);
}
};
window.IllWill = IllWill;
mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => {
$(() => IllWill.setup());
});
})();
/* global ComradeLib, mw, $ */
(async function() {
try {
await mw.loader.getScript('https://test.wikipedia.org/w/index.php?title=User:Sam_Sailor/test.js/comrade-options.js&action=raw&ctype=text/javascript');
} catch (e) {
console.warn("Comrade: Could not load /comrade-options.js, using fallback defaults.", e);
}
window.ComradeOptions = Object.assign({
version: "0.5.0",
excludedTemplates: [],
nudgeTimeout: 10000,
debugMode: false
}, window.ComradeOptions || {});
await mw.loader.getScript('https://test.wikipedia.org/w/index.php?title=User:Sam_Sailor/test.js/comrade-lib.js&action=raw&ctype=text/javascript');
async function getTargetPage(api, name, type) {
const titles = type === 'surname' ? [`List of people with the English surname ${name}`, `List of people with surname ${name}`, `${name} (surname)`, `${name} (name)`, name] : [`List of people with given name ${name}`, `List of people named ${name}`, `${name} (given name)`, `${name} (name)`, name];
const res = await api.get({
action: 'query',
titles: titles.join('|'),
formatversion: 2
});
if (!res.query || !res.query.pages) return null;
return titles.find(t => {
const pg = res.query.pages.find(p => p.title === t);
return pg && !pg.missing;
});
}
async function processHumanNameLists(api, entity, wikitext, currentTitle, hasShortDesc, isOrphan, tClean) {
const {
givens,
surnames
} = await fetchWikidataNameLabels(entity);
const nameParts = tClean.split(' ');
const hasFamilyNameHatnote = /\{\{family[ _]name[ _]hatnote/i.test(wikitext);
const primarySurname = hasFamilyNameHatnote ? nameParts[0] : (getSmartSortKey(tClean, entity).split(',')[0] || nameParts[nameParts.length - 1]);
for (const name of givens) {
if (name.toLowerCase() === primarySurname.toLowerCase()) continue;
const nameInTitle = new RegExp('\\b' + name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b', 'i').test(tClean);
if (!nameInTitle) continue;
const target = await getTargetPage(api, name, 'given');
if (target) await verifyAndNudge(api, name, 'given name', currentTitle, hasShortDesc, isOrphan, target);
}
const finalSurnames = surnames.length > 0 ? surnames : (nameParts.length >= 2 ? [nameParts[nameParts.length - 1]] : []);
for (const name of finalSurnames) {
const nameInTitle = new RegExp('\\b' + name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b', 'i').test(tClean);
if (!nameInTitle) continue;
const target = await getTargetPage(api, name, 'surname');
if (target) await verifyAndNudge(api, name, 'surname', currentTitle, hasShortDesc, isOrphan, target);
}
}
async function verifyAndNudge(api, name, type, currentTitle, hasShortDesc, isOrphan, foundTitle) {
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: foundTitle,
rvprop: 'content',
formatversion: 2
});
const page = res.query.pages[0];
if (!page || page.missing || !page.revisions) return;
const text = page.revisions[0].content;
const isNameMatch = /\{\{(Surname|Hndis|Given[ _]name|Forename|Givenname)/i.test(text);
const isNameDab = /\{\{(Disambiguation|Dab|Disambig)(?:[^}]*\|(surname|surnames|given[ _]name|given[ _]names)|(?:\s*\}\}))/i.test(text);
const isNamePageTitle = foundTitle.includes('List of people with');
if (!isNameMatch && !isNameDab && !isNamePageTitle) return;
const escapedTitle = currentTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/ /g, '[ _]');
const alreadyListed = new RegExp('(\\[\\[|\\{\\{anbl\\|[ _]*|\\|)' + escapedTitle, 'i').test(text);
if (alreadyListed) return;
const $container = $('<div>').addClass('comrade-container').addClass(isOrphan ? 'comrade-nudge' : 'comrade-info');
const $header = $('<div>').addClass('comrade-header').append($('<strong>').text(isOrphan ? 'Comrade nudge:' : 'Informative:'));
const $content = $('<div>').addClass('comrade-content');
$content.append($('<span>').append(`${type.charAt(0).toUpperCase() + type.slice(1)} not listed at `, $('<a>').attr({
href: mw.util.getUrl(foundTitle),
target: '_blank'
}).text(foundTitle), "; should it be?"));
const snippet = '* {{anbl|' + currentTitle + '}}';
const $snippetWrapper = $('<div>').css({
'display': 'flex',
'align-items': 'center',
'gap': '10px',
'margin-top': '4px'
});
const $instructionSpan = $('<span>').append('Consider adding ', $('<code>').text(snippet));
const $copyBtn = $('<button>').text('Copy & insert').on('click', function() {
navigator.clipboard.writeText('\n' + snippet).then(() => {
$(this).text('Copied!').prop('disabled', true);
const editUrl = mw.util.getUrl(foundTitle, {
action: 'edit',
summary: 'Adding * {{[[Template:Annotated biography link|anbl]]|[[' + currentTitle + ']]}}'
});
window.open(editUrl, '_blank');
});
});
$snippetWrapper.append($instructionSpan, $copyBtn);
$content.append($snippetWrapper);
if (!hasShortDesc) {
$content.append($('<span>').css({
'color': '#d33',
'font-weight': 'bold',
'margin-top': '5px'
}).text('⚠️ Missing short description: Please add a concise one before listing.'));
}
$container.append($header, $content);
ComradeLib.appendDismiss($container);
}
async function checkNamingPrecision(currentTitle, isHuman) {
const disambigMatch = currentTitle.match(/^(.+?)(?:, | \()/);
if (isHuman || !disambigMatch) return;
const baseTitle = disambigMatch[1].trim();
const api = new mw.Api();
try {
const baseRes = await api.get({
action: 'query',
titles: baseTitle,
redirects: 1,
formatversion: 2
});
const page = baseRes.query.pages[0];
const baseExists = !page.missing;
let baseRedirectsToSelf = false;
if (baseRes.query.redirects) {
baseRedirectsToSelf = baseRes.query.redirects.some(r => r.to === currentTitle);
}
const searchRes = await api.get({
action: 'query',
list: 'allpages',
apprefix: baseTitle,
aplimit: 50,
formatversion: 2
});
const competitors = searchRes.query.allpages.filter(p => {
const t = p.title;
return t !== currentTitle && t !== baseTitle && (t.startsWith(baseTitle + ', ') || t.startsWith(baseTitle + ' ('));
});
if (!baseExists || baseRedirectsToSelf) {
const $container = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $content = $('<div>').addClass('comrade-content');
if (competitors.length === 0) {
$header.append($('<strong>').text('Precision:'));
const reason = baseRedirectsToSelf ? "already redirects here" : "is free";
$content.append($('<span>').append(`The base name `, $('<code>').text(baseTitle), ` ${reason}. Consider moving this article.`));
const moveUrl = mw.util.getUrl('Special:MovePage/' + currentTitle, {
wpNewTitle: baseTitle,
wpReason: 'Unnecessary disambiguation; base name is free/redirects here per [[WP:PRECISION]]'
});
$content.append($('<button>').text(`Move to ${baseTitle}`).on('click', () => window.open(moveUrl, '_blank')));
} else {
$header.append($('<strong>').text('Observation:'));
const state = baseRedirectsToSelf ? 'redirect to this page' : 'redlink';
$content.append($('<span>').append(`Base name `, $('<code>').text(baseTitle), ` has ${competitors.length} competitors, but it is currently a ${state}.`));
$content.append($('<span>').text('Consider if a disambiguation page is needed.'));
}
$container.append($header, $content);
ComradeLib.appendDismiss($container);
}
} catch (e) {
console.warn("Comrade: Precision check failed", e);
}
}
async function checkHumanSort(wikitext, entity, currentTitle, tClean, refName) {
const dsMatch = wikitext.match(/\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/i);
const {
givens
} = await fetchWikidataNameLabels(entity);
if (dsMatch) {
const currentDS = dsMatch[1].trim();
if (currentDS.includes(',')) {
let [lastPart, firstPart] = currentDS.split(',').map(s => s.trim());
if (firstPart.toLowerCase().endsWith(lastPart.toLowerCase())) {
let fixedFirst = firstPart.substring(0, firstPart.length - lastPart.length).trim();
let suggestion = `${fixNameCasing(lastPart, refName)}, ${fixNameCasing(fixedFirst, refName)}`;
if (suggestion !== currentDS) renderSortNudge(suggestion, "Redundant surname in given name field.");
} else if (givens.length > 0 && lastPart.split(' ').length > 1) {
const parts = lastPart.split(' ');
const misplacedGiven = parts.find(part => givens.some(gn => gn.toLowerCase() === part.toLowerCase()));
if (misplacedGiven) {
const newSurname = parts.filter(p => p !== misplacedGiven).join(' ');
let suggestion = `${fixNameCasing(newSurname, refName)}, ${fixNameCasing(misplacedGiven + ' ' + firstPart, refName)}`;
if (suggestion !== currentDS) renderSortNudge(suggestion, `Wikidata suggests "${misplacedGiven}" is a given name.`);
}
}
}
} else {
const hasFamilyNameHatnote = /\{\{family[ _]name[ _]hatnote/i.test(wikitext);
const nameParts = tClean.split(' ');
const targetSortName = hasFamilyNameHatnote ? `${nameParts[0]}, ${nameParts.slice(1).join(' ')}` : getSmartSortKey(tClean, entity);
if (targetSortName.toLowerCase() !== currentTitle.toLowerCase()) {
const $dsMissing = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Add: {{DEFAULTSORT:${targetSortName}}}`).on('click', () => performSortCorrection(targetSortName, "add"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Tag is missing. `, $btn));
$dsMissing.append($header);
ComradeLib.appendDismiss($dsMissing);
}
}
}
async function fetchWikidataEntity(qid) {
const res = await fetch(`https://www.wikidata.org/w/api.php?action=wbgetentities&ids=${qid}&props=claims|descriptions|labels&languages=en&format=json&origin=*`).then(r => r.json());
return res.entities[qid];
}
function getSmartSortKey(fullName, entity) {
const countryIds = entity.claims?.P27?.map(c => c.mainsnak?.datavalue?.value?.id) || [];
const locationIds = entity.claims?.P17?.map(c => c.mainsnak?.datavalue?.value?.id) || [];
const placeIds = [...(entity.claims?.P19 || []), ...(entity.claims?.P119 || [])].map(c => c.mainsnak?.datavalue?.value?.id).filter(Boolean);
const allRelevantIds = [...countryIds, ...locationIds, ...placeIds];
const birthYear = parseInt(entity.claims?.P569?.[0]?.mainsnak?.datavalue?.value?.time?.match(/[+-](\d{4})/)?.[1]) || null;
if (fullName.startsWith("Saint ")) return fullName.replace("Saint ", "");
const arabicParticles = /\b(Abu|Abd|Abdel|Abdul|ben|bin|bint)\b/i;
if (fullName.match(arabicParticles)) {
if (birthYear && birthYear > 1900) {
const parts = fullName.split(' ');
const binIndex = parts.findIndex(p => p.toLowerCase() === 'bin' || p.toLowerCase() === 'ben');
if (binIndex > 0) return parts.slice(binIndex).join(' ') + ', ' + parts.slice(0, binIndex).join(' ');
} else {
return fullName;
}
}
const eastAsianQids = ["Q148", "Q865", "Q884", "Q424", "Q17", "Q881"];
if (allRelevantIds.some(id => eastAsianQids.includes(id))) {
if (allRelevantIds.includes("Q17") && birthYear && birthYear > 1885) return westernSort(fullName);
const parts = fullName.split(' ');
if (parts.length > 1) return `${parts[0]}, ${parts.slice(1).join(' ')}`;
}
return westernSort(fullName);
}
function westernSort(name) {
let cleanName = name;
if (name.startsWith("O'")) cleanName = name.replace("O'", "O");
const parts = cleanName.split(' ');
if (parts.length < 2) return cleanName;
const surname = parts.pop();
if (["Jr.", "Jr", "III", "II", "Sr.", "Sr"].includes(surname)) {
const actualSurname = parts.pop();
return `${actualSurname}, ${parts.join(' ')} ${surname}`;
}
return `${surname}, ${parts.join(' ')}`;
}
async function performSortCorrection(newName, type) {
const api = new mw.Api();
const title = mw.config.get("wgPageName");
try {
const data = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
let content = data.query.pages[0].revisions[0].content;
const dsRegex = /\{\{\s*(?:DEFAULTSORT|Defaultsort)\s*:[^}]+\}\}/i;
const newTag = `{{DEFAULTSORT:${newName}}}`;
if (dsRegex.test(content)) {
content = content.replace(dsRegex, newTag);
} else {
const catRegex = /\[\[Category:/i;
content = catRegex.test(content) ? content.replace(catRegex, `${newTag}\n[[Category:`) : content.trim() + `\n\n${newTag}`;
}
await api.postWithEditToken({
action: 'edit',
title: title,
text: content,
summary: `Setting DEFAULTSORT to ${newName}`,
nocreate: true
});
mw.notify(`DEFAULTSORT ${type === "add" ? "added" : "corrected"} to ${newName}!`, {
title: 'Comrade Success',
type: 'success'
});
setTimeout(() => location.reload(), 700);
} catch (err) {
mw.notify('Failed to save DEFAULTSORT.', {
type: 'error'
});
}
}
function renderSortNudge(target, reason) {
const $dsNudge = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Correct to: ${target}`).on('click', () => performSortCorrection(target, "format"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` ${reason} `, $btn));
$dsNudge.append($header);
ComradeLib.appendDismiss($dsNudge);
}
async function fetchWikidataNameLabels(entity) {
const familyIds = entity.claims?.P734?.map(c => c.mainsnak.datavalue.value.id) || [];
const givenIds = entity.claims?.P735?.map(c => c.mainsnak.datavalue.value.id) || [];
const allNameIds = [...new Set([...familyIds, ...givenIds])];
if (allNameIds.length === 0) return {
givens: [],
surnames: []
};
const url = new URL("https://www.wikidata.org/w/api.php");
url.search = new URLSearchParams({
action: 'wbgetentities',
ids: allNameIds.join('|'),
props: 'labels',
languages: 'en',
format: 'json',
origin: '*'
});
const nameData = await fetch(url).then(r => r.json());
return {
givens: givenIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean),
surnames: familyIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean)
};
}
function fixNameCasing(part, referenceName) {
return part.split(' ').map(word => {
const match = referenceName.match(new RegExp(`\\b${word}\\b`, 'i'));
if (match) return match[0];
return referenceName.split(' ').find(tWord => word.toLowerCase().startsWith(tWord.toLowerCase()) || tWord.toLowerCase().startsWith(word.toLowerCase())) || word;
}).join(' ');
}
async function init() {
const api = new mw.Api();
const ns = mw.config.get("wgNamespaceNumber");
const action = mw.config.get("wgAction");
if (mw.config.get("wgDBname") !== "enwiki" || action !== "view") return;
if (ns === 14) {
const $btn = $('<button>').attr('id', 'comrade-audit-btn').text('Audit category quality').css({
'margin-bottom': '10px',
'padding': '5px 10px',
'cursor': 'pointer'
}).on('click', ComradeLib.performCategoryAudit);
$('.mw-category-generated').prepend($btn);
return;
}
if (ns !== 0) return;
const qid = mw.config.get("wgWikibaseItemId");
const currentTitle = mw.config.get("wgTitle");
try {
const [pageData, backlinkData] = await Promise.all([
api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get("wgPageName"),
rvprop: 'content',
formatversion: 2
}),
api.get({
action: 'query',
list: 'backlinks',
blfilterredir: 'nonredirects',
bllimit: 50,
blnamespace: 0,
bltitle: mw.config.get("wgPageName"),
formatversion: 2
})
]);
const page = pageData.query.pages[0];
if (!page || page.missing || !page.revisions) return;
const wikitext = page.revisions[0].content;
ComradeLib.checkLeadCapitalization(currentTitle);
// Ligature/ASCII-only/diacritic and domain
const norms = ComradeLib.getNormalizedTitles(currentTitle);
if (norms.ascii) {
let label = "ASCII-only";
let category = "R from ASCII-only";
if (/[æÆœŒijIJßẞ]/.test(currentTitle)) {
label = "Ligature";
category = "R to ligature";
} else if (currentTitle.normalize("NFD").match(/[\u0300-\u036f]/)) {
label = "Diacritic";
category = "R to diacritic";
}
await ComradeLib.checkAndRenderRedirect(norms.ascii, currentTitle, category, label);
}
await ComradeLib.checkDomainRedirect(qid, currentTitle);
// Orphan logic
const isOrphanTagged = /\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)/i.test(wikitext) || /\| *orphan *=/i.test(wikitext);
const backLinkCount = backlinkData.query.backlinks.length;
if (isOrphanTagged && backLinkCount >= 1) {
const $container = $('<div>').addClass('comrade-container comrade-status');
const $btn = $('<button>').text('Remove orphan tag').on('click', async () => await ComradeLib.performDeorphan());
const $statusText = $('<span>').append($('<strong>').text('Status:'), ` Article has ${backLinkCount} backlink(s).`, $btn);
$container.append($('<div>').addClass('comrade-header').append($statusText));
ComradeLib.appendDismiss($container);
}
// Wikidata/human logic
let entity = qid ? await fetchWikidataEntity(qid) : null;
let isHuman = entity?.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5");
await checkNamingPrecision(currentTitle, isHuman);
if (isHuman && entity) {
const tClean = currentTitle.replace(/\s*\(.*?\)\s*/g, '').trim();
const refName = entity.labels?.en?.value || currentTitle;
const hasFamilyNameHatnote = /\{\{family[ _]name[ _]hatnote/i.test(wikitext);
const hasShortDesc = /\{\{\s*[Ss]hort description/i.test(wikitext);
const isOrphan = isOrphanTagged && backLinkCount < 1;
await ComradeLib.checkShortDescDates(wikitext, entity);
await ComradeLib.checkNoShortDesc(wikitext, entity);
// Check DEFAULTSORT
await checkHumanSort(wikitext, entity, currentTitle, tClean, refName);
const longName = ComradeLib.getLongNameFromWikitext(wikitext);
if (longName && longName.length > tClean.length) {
let sortKey = hasFamilyNameHatnote ? `${longName.split(' ')[0]}, ${longName.split(' ').slice(1).join(' ')}` : getSmartSortKey(longName, entity);
const rLongWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from long name}}\n}}\n{{DEFAULTSORT:${sortKey}}}`;
await ComradeLib.checkAndRenderRedirect(longName, currentTitle, null, "Long name", rLongWikitext);
}
// Sort name R
const targetSortName = hasFamilyNameHatnote ? `${tClean.split(' ')[0]}, ${tClean.split(' ').slice(1).join(' ')}` : getSmartSortKey(tClean, entity);
if (targetSortName && targetSortName.toLowerCase() !== currentTitle.toLowerCase()) {
let targetPage = currentTitle.includes(' (') ? currentTitle.split(' (')[0] : currentTitle;
let rCat = targetPage !== currentTitle ? "R from ambiguous sort name" : "R from sort name";
if (targetSortName.includes(',')) {
const p = targetSortName.split(',');
rCat += `|${p[0].trim().charAt(0).toUpperCase()}|${p[1].trim().charAt(0).toUpperCase()}`;
}
await ComradeLib.checkAndRenderRedirect(targetSortName, targetPage, rCat, "Sort name");
}
// Name lists
await processHumanNameLists(api, entity, wikitext, currentTitle, hasShortDesc, isOrphan, tClean);
}
// Quality/ORES
const isRedirect = !!mw.config.get("wgIsRedirect");
const userExclusions = window.comradeQualityExclusions || [];
const allExclusions = [...window.ComradeOptions.excludedTemplates, ...userExclusions];
const exclusionRegexString = allExclusions.map(t => t.replace("Template:", "").replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
const exclusionRegex = new RegExp(exclusionRegexString, 'i');
if (isRedirect || exclusionRegex.test(wikitext)) {
console.log("Comrade check skipped: Page is a redirect or excluded via options.");
return;
}
ComradeLib.checkQualityConsistency();
} catch (err) {
console.error("Comrade error:", err);
}
}
init();
})();
(function() {
'use strict';
if (mw.config.get('wgCanonicalSpecialPageName') !== 'Search') {
return;
}
mw.loader.using(['mediawiki.util']).then(function() {
$(function() {
const $resultsInfo = $('.results-info');
const totalResultsRaw = $resultsInfo.attr('data-mw-num-results-total') || '0';
function formatWithCommas(numberString) {
return numberString.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
const state = {
language: mw.config.get('wgContentLanguage'),
totalResultsFormatted: formatWithCommas(totalResultsRaw)
};
let titlesCache = null;
function extractResultTitles() {
const currentResults = $('.mw-search-result-heading');
if (!titlesCache || titlesCache.length !== currentResults.length) {
titlesCache = currentResults.map(function() {
return $(this).find('a:first').attr('title');
}).get().filter(Boolean);
}
return titlesCache;
}
mw.hook("wikipage.content").add(function() {
titlesCache = null;
});
function addLimitOptions() {
const selects = document.querySelectorAll('.cdx-table-pager__limit-form select[name="limit"]');
if (!selects.length) return;
const valuesToAdd = ["1000", "2500", "5000"];
selects.forEach(function(select) {
valuesToAdd.forEach(function(value) {
if (select.querySelector('option[value="' + value + '"]')) return;
const option = document.createElement("option");
option.value = value;
option.textContent = formatWithCommas(value) + " items";
const existingOptions = Array.from(select.querySelectorAll("option"));
const insertBefore = existingOptions.find(function(opt) {
return Number(opt.value) > Number(value);
});
if (insertBefore) {
select.insertBefore(option, insertBefore);
} else {
select.appendChild(option);
}
});
});
}
mw.hook("wikipage.content").add(addLimitOptions);
const numLinks = $('.mw-numlink');
if (numLinks.length > 4 && !numLinks.eq(4).next().is('a[href*="limit=5000"]')) {
const $firstAnchor = numLinks.eq(4);
const $firstSpan = $('<span> | </span>').insertAfter($firstAnchor);
const url1 = new URL(window.location.href);
url1.searchParams.set('limit', '5000');
$('<a>', {
text: '5000',
title: 'Show 5000 results per page',
href: url1.toString()
}).insertAfter($firstAnchor).before($firstSpan);
}
if (numLinks.length > 9 && !numLinks.eq(9).next().is('a[href*="limit=5000"]')) {
const $secondAnchor = numLinks.eq(9);
const $secondSpan = $('<span> | </span>').insertAfter($secondAnchor);
const url2 = new URL(window.location.href);
url2.searchParams.set('limit', '5000');
$('<a>', {
text: '5000',
title: 'Show 5000 results per page',
href: url2.toString()
}).insertAfter($secondAnchor).before($secondSpan);
}
const $container = $("<div></div>").addClass("enhanced-search-container").hover(function() {
$(this).addClass("hover");
}, function() {
$(this).removeClass("hover");
});
const interwikiResults = document.getElementById("mw-interwiki-results");
if (interwikiResults) {
$container.addClass("enhanced-search-floating").insertBefore("#mw-interwiki-results");
} else {
$container.addClass("enhanced-search-absolute").insertBefore(".searchresults");
}
const totalFormattedStr = state.totalResultsFormatted;
const $titleElement = $("<div>Show a shortened list of<br />this page's " + $('.mw-search-result-heading').length + " results <small>(of total of " + totalFormattedStr + ")</small></div>").addClass("enhanced-header").appendTo($container);
const $contentArea = $("<div></div>").addClass("enhanced-content-area").appendTo($container).hide();
const $note1 = $("<div>Copy this page\'s " + $('.mw-search-result-heading').length + " results to the clipboard as...</div>").addClass("enhanced-note").insertAfter($container).hide();
function setupHoverTooltip(element, message) {
$(element).attr('title', message).hover(function() {
$(this).addClass("hover");
}, function() {
$(this).removeClass("hover");
});
}
const $note2 = $("<div>...plain text<sup> (?)</sup></div>").addClass("enhanced-btn").insertAfter($note1).hide();
setupHoverTooltip($note2, 'This button copies this page\'s results to the clipboard as plain text, i.e. without wikitext markup. Since this copies only the results shown on this search page, make sure you have clicked the "view 20, 50, 100, 250, 500, 5000" link. This format is compatible with PetScan and AutoWikiBrowser (AWB) for example.');
const $note3 = $("<div>...wikitext<sup> (?)</sup></div>").addClass("enhanced-btn").insertAfter($note2).hide();
setupHoverTooltip($note3, 'This button copies this page\'s results to the clipboard and also adds wikitext markup.');
const $note4 = $("<div>...PetScan URL<sup> (?)</sup></div>").addClass("enhanced-btn").insertAfter($note3).hide();
setupHoverTooltip($note4, 'This button copies a PetScan URL with this page\'s results to the clipboard. Note that if the PetScan URL becomes too long (~250+ pages), the URL might not work.');
const $note5 = $("<div>...or this search query's total results of " + totalFormattedStr + "<sup> (?)</sup> as...</div>").addClass("enhanced-note").insertAfter($note4).hide().attr('title', 'Note that the actual limit is 10,000, which is MediaWiki\'s maximum limit.').hover(function() {
$(this).addClass("hover");
}, function() {
$(this).removeClass("hover");
});
const $note6 = $("<div>...PetScan URL<sup> (?)</sup></div>").addClass("enhanced-btn").insertAfter($note5).hide();
setupHoverTooltip($note6, 'This button copies a PetScan URL with all ' + totalFormattedStr + ' results to the clipboard. Note that the maximum limit is 10,000 due to MediaWiki\'s restriction.');
const $note7 = $("<div><sup>(?)</sup> = hover over for tooltip details</div>").addClass("enhanced-note-right").insertAfter($note6).hide();
const noteElements = [$note1, $note2, $note3, $note4, $note5, $note6, $note7];
if (interwikiResults !== null) {
noteElements.forEach($el => $el.addClass("enhanced-floating-elements"));
} else {
noteElements.forEach(function($el, index) {
$el.addClass("enhanced-absolute-elements enhanced-position-" + index);
});
}
function formatPlain() {
return extractResultTitles().join("\r\n");
}
function formatWikitext() {
const titles = extractResultTitles().map(title => "* [[" + title + "]]");
const header = "This is a list of [" + window.location.href + " these search results] (made with the script [[User:JAAqqO/EnhancedSearchPage|EnhancedSearchPage]]):\r\n";
return header + titles.join("\r\n");
}
function formatPetScan() {
const encodedTitles = extractResultTitles().map(encodeURIComponent).join("%0A");
return "https://petscan.wmflabs.org/?active_tab=tab_other_sources&manual_list=" + encodedTitles + "&manual_list_wiki=" + state.language + "wiki&project=wikipedia&language=" + state.language + "&doit=";
}
function formatPetScanTotal() {
const oouiInput = document.getElementById("ooui-php-1");
const value = oouiInput ? oouiInput.value : "";
if (value) {
return "https://petscan.wmflabs.org/?active_tab=tab_other_sources&ns%5B0%5D=1&search_query=" + encodeURIComponent(value) + "&search_wiki=" + state.language + "wiki&search_max_results=10000&project=wikipedia&language=" + state.language + "&doit=";
}
return "";
}
function copyToClipboard(text) {
if (!text) return;
if (!navigator.clipboard) {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.setAttribute('readonly', '');
textarea.style.position = 'absolute';
textarea.style.left = '-9999px';
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
} catch (e) {
console.error('Fallback copy failed', e);
}
textarea.remove();
} else {
navigator.clipboard.writeText(text).catch(function(err) {
console.error("Copy failed: ", err);
});
}
}
function showCopiedMessage(event) {
const posX = event.pageX;
const posY = event.pageY;
const $msg = $("<div class='copied-message'>Copied!</div>").appendTo("body");
$msg.css({
position: "absolute",
top: (posY + 10) + "px",
left: (posX + 10) + "px",
backgroundColor: "#f0f0f0",
border: "1px solid grey",
padding: "5px 10px",
borderRadius: "5px",
zIndex: 9999
});
setTimeout(function() {
$msg.fadeOut("slow", function() {
$(this).remove();
});
}, 500);
}
function performButtonAnimation($element) {
$element.css("transform", "scaleY(0.9)").animate({
opacity: '0.5'
}, "fast", function() {
$(this).animate({
opacity: '1'
}, "slow").css("transform", "scaleY(1)");
});
}
let isExpanded = false;
function showContentArea() {
if (isExpanded) return;
isExpanded = true;
const currentResults = $('.mw-search-result-heading');
$contentArea.empty().append(currentResults.clone());
$contentArea.slideDown();
$(".searchalttitle").remove();
$container.addClass("enhanced-active-container");
$titleElement.remove();
noteElements.forEach($el => $el.show());
}
function bindCopyAction($element, formatterFn) {
$element.click(function(event) {
copyToClipboard(formatterFn());
showCopiedMessage(event);
performButtonAnimation($(this));
});
}
$container.click(showContentArea);
bindCopyAction($note2, formatPlain);
bindCopyAction($note3, formatWikitext);
bindCopyAction($note4, formatPetScan);
bindCopyAction($note6, formatPetScanTotal);
const $pageDown = $("<div><br /><br />[ ↓ ]</div>").addClass("enhanced-scroll-btn").hover(function() {
$(this).addClass("hover");
$(this).attr('title', 'Go to the bottom of the page');
}, function() {
$(this).removeClass("hover");
}).insertBefore('.mw-search-result:eq(0)');
const $pageUp = $("<div>[ ↑ ]</div>").addClass("enhanced-scroll-btn").hover(function() {
$(this).addClass("hover");
$(this).attr('title', 'Go to the top of the page');
}, function() {
$(this).removeClass("hover");
}).insertAfter('.mw-search-result:eq(-1)');
$pageDown.click(function(event) {
event.preventDefault();
$('html, body').animate({
scrollTop: $(document).height()
}, 500);
});
$pageUp.click(function() {
$('html, body').animate({
scrollTop: 0
}, 500);
});
$("<span> (<b>" + $('.mw-search-result-heading').length + "</b> shown on the page)</span>").appendTo('.results-info');
const searchDidYouMean = document.querySelector('.searchdidyoumean');
if (searchDidYouMean) {
searchDidYouMean.style.color = '#b32424';
searchDidYouMean.style.fontSize = '20px';
}
});
});
})();
(function() {
if (window.Headmaster) return;
const VERSION = "0.9.2.1";
const defaultNS = [0, 118];
const userNS = window.headmaster_ns || [];
const config = {
menu: window.headmaster_menu || 'p-cactions',
enableSummary: window.headmaster_do_summary !== false,
customSummary: window.headmaster_summary || "",
allowedNamespaces: Array.from(new Set([...defaultNS, ...userNS])),
autoScan: window.headmaster_auto_scan !== false,
autoRun: window.headmaster_auto_run === false
};
const Headmaster = {
VERSION: VERSION,
DefaultSummary: "Converting [[MOS:PSEUDOHEAD|pseudo-headings]] {details}using [[User:Sam_Sailor/Scripts/Headmaster.js|Headmaster]]. Semicolon markup is reserved for [[MOS:DLIST|description lists]]",
styleInjected: false,
css: `
#hm-panel { clear: both; border: 1px solid #a2a9b1; padding: 15px; background: #f8f9fa; margin-bottom: 1.5em; border-radius: 2px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
#hm-table { width: 100%; border-collapse: collapse; background: white; margin-top: 10px; }
#hm-table th { background: #f2f2f2; border: 1px solid #a2a9b1; padding: 8px; text-align: left; }
#hm-table td { border: 1px solid #a2a9b1; padding: 8px; vertical-align: middle; }
.hm-context { font-family: 'Courier New', monospace; font-size: 0.9em; color: #54595d; background: #fdfdfd; }
#hm-controls { display: flex; justify-content: space-between; align-items: center; margin-top: 15px; }
#hm-controls button { margin-left: 10px; padding: 6px 16px; cursor: pointer; border-radius: 2px; }
.hm-btn-apply { background: #36c; color: white; border: 1px solid #36c; font-weight: bold; }
.hm-btn-apply:hover { background: #447ff5; }
.hm-global-selector { font-size: 0.9em; color: #202122; background: #eaecf0; padding: 8px; border-radius: 2px; border: 1px solid #a2a9b1; }
.hm-global-selector a { font-weight: bold; text-decoration: none; color: #36c; }
.hm-global-selector a:hover { text-decoration: underline; }
.hm-locate-btn { cursor: pointer; color: #36c; border: 1px solid #a2a9b1; background: #f8f9fa; padding: 2px 6px; font-size: 0.85em; border-radius: 2px; }
.hm-locate-btn:hover { background: #fff; border-color: #36c; }
`,
setup() {
if (!config.allowedNamespaces.includes(mw.config.get("wgNamespaceNumber"))) return;
const action = mw.config.get("wgAction");
this.addPortlet(action);
if (action === 'view' && (config.autoScan || config.autoRun)) {
this.silentScan();
}
if (mw.util.getParamValue('headmaster') === '1' && ['edit', 'submit'].includes(action)) {
const params = new URLSearchParams(window.location.search);
params.delete('headmaster');
const searchString = params.toString();
const newUrl = window.location.pathname + (searchString ? '?' + searchString : '');
window.history.replaceState(null, '', newUrl);
this.init();
}
},
addPortlet(action) {
const link = mw.util.addPortletLink(config.menu, '#', 'Headmaster', 'ca-hm', 'Interactively convert pseudo-headings');
if (link) {
$(link).on('click', (e) => {
e.preventDefault();
if (action === 'view') {
this.redirectToEdit();
} else if (['edit', 'submit'].includes(action)) {
this.init();
}
});
}
},
redirectToEdit() {
window.location.href = mw.util.getUrl(mw.config.get("wgPageName"), {
action: 'edit',
headmaster: '1'
});
},
silentScan() {
new mw.Api().get({
action: 'query',
prop: 'revisions',
titles: mw.config.get('wgPageName'),
rvprop: 'content',
rvlimit: 1,
formatversion: 2
}).done((data) => {
const page = data.query.pages[0];
if (!page || !page.revisions) return;
const wikitext = page.revisions[0].content;
const tasks = this.analyzeText(wikitext);
if (tasks.length > 0) {
if (config.autoRun) {
this.redirectToEdit();
} else {
this.notifyDiscovery(tasks.length);
}
}
}).fail((err) => {
mw.log.warn("Headmaster: silentScan failed", err);
});
},
notifyDiscovery(count) {
const $msg = $('<span>').text(`Headmaster: Found ${count} issues. `).append($('<a>').text('Run conversion').css({
fontWeight: 'bold',
cursor: 'pointer'
}).on('click', () => this.redirectToEdit()));
mw.notify($msg, {
tag: 'hm-discovery',
autoHide: false
});
},
init() {
if (!this.styleInjected) {
mw.loader.addStyleTag(this.css);
this.styleInjected = true;
}
const $textbox = $('#wpTextbox1');
const wikitext = $textbox.textSelection('getContents');
if (!wikitext) {
mw.notify("Textbox is empty or not found.", {
type: 'error'
});
return;
}
const tasks = this.analyzeText(wikitext);
if (tasks.length === 0) {
mw.notify("No issues found.");
return;
}
this.showUI(tasks, wikitext);
},
analyzeText(wikitext) {
const lines = wikitext.split("\n");
const tasks = [];
let currentDepth = 1;
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
const headingMatch = line.match(/^(={2,6})\s*(.+?)\s*\1\s*$/);
if (headingMatch) {
currentDepth = headingMatch[1].length;
const prevLine = i > 0 ? lines[i - 1].trim() : null;
const isSmashed = prevLine !== null && prevLine !== "";
const isExtraSpace = i > 1 && lines[i - 1].trim() === "" && lines[i - 2].trim() === "";
if (isSmashed || isExtraSpace) {
tasks.push({
index: i,
type: 'WHITESPACE',
original: lines[i],
isSmashed: isSmashed,
isExtraSpace: isExtraSpace
});
}
} else if (line.startsWith(';') && !line.startsWith(';;')) {
const nextLine = lines[i + 1] || "";
if (!nextLine.trim().startsWith(':')) {
const cleanText = line.substring(1).replace(/\s*:.*$/, "").trim();
const suggestedLevel = "=".repeat(Math.min(currentDepth + 1, 6));
tasks.push({
index: i,
type: 'PSEUDO',
original: line,
cleanText: cleanText,
context: nextLine.substring(0, 60).trim() + (nextLine.length > 60 ? "..." : ""),
suggestedLevel: suggestedLevel
});
}
}
}
return tasks;
},
showUI(tasks, originalWikitext) {
$('#hm-panel').remove();
$('#editform').hide();
const visibleTasks = tasks.filter(t => t.type === 'PSEUDO');
const pseudoCount = visibleTasks.length;
const whitespaceCount = tasks.filter(t => t.type === 'WHITESPACE').length;
let statusMsg = "";
const s = (n) => n !== 1 ? 's' : '';
if (pseudoCount > 0) {
statusMsg = `Found <strong>${pseudoCount}</strong> possible pseudo-heading${s(pseudoCount)}.`;
if (whitespaceCount > 0) {
statusMsg += ` Also fixing <strong>${whitespaceCount}</strong> spacing issue${s(whitespaceCount)} quietly.`;
}
} else {
statusMsg = `Did not find any pseudo-headings, but fixing <strong>${whitespaceCount}</strong> spacing issue${s(whitespaceCount)} quietly.`;
}
const tableRows = pseudoCount > 0 ? visibleTasks.map((task, i) => `
<tr data-task-index="${task.index}">
<td><button class="hm-locate-btn" data-hm-i="${tasks.indexOf(task)}" title="Show in editor">Locate</button></td>
<td class="hm-context"><code>${mw.html.escape(task.original)}</code></td>
<td><label><input type="radio" name="hm-opt-${i}" value="keep"> Keep</label></td>
<td><label><input type="radio" name="hm-opt-${i}" value="bold" checked> <b>Bold</b></label></td>
<td><label><input type="radio" name="hm-opt-${i}" value="head"> Heading (${task.suggestedLevel.length})</label></td>
<td class="hm-context">${mw.html.escape(task.context || "(end of page)")}</td>
</tr>
`).join("") : `<tr><td colspan="6" style="text-align:center; padding: 2em; color: #54595d; background: #f8f9fa;">
No interactive heading changes detected. Only background spacing normalization will be applied.
</td></tr>`;
const $panel = $(`
<div id="hm-panel">
<h3 style="margin-top:0">Headmaster v${this.VERSION}: Review pseudo-headings</h3>
<div class="hm-global-selector" ${pseudoCount === 0 ? 'style="display:none"' : ''}>
<strong>Bulk action:</strong>
<a href="#" id="hm-all-keep">Set all to Keep</a> |
<a href="#" id="hm-all-bold">Set all to Bold</a> |
<a href="#" id="hm-all-head">Set all to Heading</a>
</div>
<table id="hm-table">
<thead>
<tr>
<th>Find</th>
<th>Source</th>
<th colspan="3">Action</th>
<th>Next line context</th>
</tr>
</thead>
<tbody>${tableRows}</tbody>
</table>
<div id="hm-controls">
<span>${statusMsg}</span>
<div>
<button id="hm-cancel" class="mw-ui-button">Cancel</button>
<button id="hm-apply" class="hm-btn-apply">Show changes (diff)</button>
</div>
</div>
</div>
`);
$('#content').prepend($panel);
window.scrollTo(0, 0);
$('.hm-locate-btn').on('click', function() {
$(document).off('mousedown.hmlocate');
const idx = $(this).data('hm-i');
const task = tasks[idx];
const lines = originalWikitext.split('\n');
let charOffset = 0;
for (let j = 0; j < task.index; j++) {
charOffset += lines[j].length + 1;
}
$('#hm-panel').hide();
$('#editform').show();
const $textbox = $('#wpTextbox1');
$textbox.textSelection('setSelection', {
start: charOffset,
end: charOffset + lines[task.index].length
});
$textbox.textSelection('scrollToCaretPosition');
const locateNotify = mw.notify("Locating line " + (task.index + 1) + ". Click outside the editor to return.", {
tag: 'hm-locate',
type: 'success',
autoHide: false
});
setTimeout(() => {
$(document).on('mousedown.hmlocate', function(e) {
if (!$(e.target).closest('#editform, .mw-notification-area').length) {
$(document).off('mousedown.hmlocate');
Promise.resolve(locateNotify).then(n => n.close());
$('#editform').hide();
$('#hm-panel').show();
}
});
}, 200);
});
$('#hm-all-keep').on('click', (e) => {
e.preventDefault();
$('input[value="keep"]').prop('checked', true);
});
$('#hm-all-bold').on('click', (e) => {
e.preventDefault();
$('input[value="bold"]').prop('checked', true);
});
$('#hm-all-head').on('click', (e) => {
e.preventDefault();
$('input[value="head"]').prop('checked', true);
});
$('#hm-cancel').on('click', () => {
$('#hm-panel').remove();
$('#editform').show();
});
$('#hm-apply').on('click', () => this.applyChanges(tasks, originalWikitext));
},
getProcessedSummary(existingSummary, bolds, heads, spaces) {
if (!config.enableSummary) return existingSummary;
let mySummary = "";
let isDefault = false;
if (config.customSummary) {
mySummary = config.customSummary;
} else {
isDefault = true;
if (bolds > 0 || heads > 0) {
const detailsArr = [];
if (bolds > 0) detailsArr.push(`${bolds} to bold`);
if (heads > 0) detailsArr.push(`${heads} to heading`);
const detailsStr = detailsArr.length > 0 ? `(${detailsArr.join(', ')}) ` : "";
mySummary = this.DefaultSummary.replace("{details}", detailsStr);
if (spaces > 0) {
mySummary += "; also handled [[MOS:BLANKLINE]]";
}
} else if (spaces > 0) {
mySummary = "Handling [[MOS:BLANKLINE]] using [[User:Sam_Sailor/Scripts/Headmaster.js|Headmaster]]";
}
}
if (!mySummary) return existingSummary;
const trimmedOld = existingSummary.trim();
if (!trimmedOld) return mySummary;
if (/[.!?]$/.test(trimmedOld)) {
return trimmedOld + " " + mySummary;
} else {
const finalSummary = isDefault ? mySummary.charAt(0).toLowerCase() + mySummary.slice(1) : mySummary;
return trimmedOld + "; " + finalSummary;
}
},
applyChanges(tasks, wikitext) {
try {
let lines = wikitext.split("\n");
let bCount = 0,
hCount = 0,
wCount = 0;
const pseudoTasks = tasks.filter(t => t.type === 'PSEUDO');
for (let i = pseudoTasks.length - 1; i >= 0; i--) {
const task = pseudoTasks[i];
const idx = task.index;
const action = $('#hm-table tbody tr').filter(`[data-task-index="${idx}"]`).find('input:checked').val();
if (action === 'bold') {
let newValue = `'''${task.cleanText}'''`;
if (idx > 0 && lines[idx - 1].trim() !== "") {
newValue = "\n" + newValue;
}
lines[idx] = newValue;
bCount++;
} else if (action === 'head') {
const h = task.suggestedLevel;
let newValue = `${h} ${task.cleanText} ${h}`;
if (idx > 0 && lines[idx - 1].trim() !== "") {
newValue = "\n" + newValue;
}
lines[idx] = newValue;
hCount++;
}
}
const spaceTasks = tasks.filter(t => t.type === 'WHITESPACE');
for (let i = spaceTasks.length - 1; i >= 0; i--) {
const task = spaceTasks[i];
const idx = task.index;
if (task.isSmashed) {
lines.splice(idx, 0, "");
} else if (task.isExtraSpace) {
let backIdx = idx - 1;
while (backIdx > 0 && lines[backIdx].trim() === "" && lines[backIdx - 1].trim() === "") {
lines.splice(backIdx, 1);
backIdx--;
}
}
wCount++;
}
if (bCount + hCount + wCount === 0) {
mw.notify("No changes selected.");
$('#hm-panel').remove();
$('#editform').show();
return;
}
$('#wpTextbox1').textSelection('setContents', lines.join("\n"));
const $summary = $('#wpSummary');
$summary.val(this.getProcessedSummary($summary.val(), bCount, hCount, wCount));
$('#hm-panel').remove();
$('#editform').show();
mw.notify(`Headmaster: Fixed ${wCount} spacing issue(s) and converted ${bCount + hCount} heading(s).`);
$('html, body').animate({
scrollTop: 0
}, 'fast');
$('#wpDiff').click();
} catch (err) {
$('#editform').show();
mw.notify("Headmaster error: " + err.message, {
type: 'error'
});
}
}
};
window.Headmaster = Headmaster;
mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => {
$(() => Headmaster.setup());
});
})();
//</nowiki>
dybs1pbqccvxyjehju4xupqhorpqcte
749892
749891
2026-07-04T10:33:56Z
Sam Sailor
26820
Test
749892
javascript
text/javascript
//<nowiki>
(function(mw, $, OO) {
'use strict';
const IGNORE_SUFFIXES = ['Sr.', 'Sr', 'Jr.', 'Jr', 'II', 'III', 'IV', 'V', 'MD', 'PhD'];
const LANG_PREFS = ['frwiki', 'dewiki', 'eswiki', 'itwiki', 'nlwiki', 'svwiki'];
const NON_LANG_WIKIS = ['commonswiki', 'specieswiki', 'metawiki', 'mediawikiwiki', 'wikidatawiki', 'wikimaniawiki'];
const suffixRE = new RegExp('\\s(' + IGNORE_SUFFIXES.join('|').replace(/\./g, '\\.') + ')$', 'i');
function GetterDialog(config) {
GetterDialog.super.call(this, config);
this.abortController = null;
}
OO.inheritClass(GetterDialog, OO.ui.ProcessDialog);
GetterDialog.static.name = 'getterDialog';
GetterDialog.static.title = 'Getter: Wikipedia List Builder';
GetterDialog.static.actions = [
{ action: 'generate', label: 'Generate List', flags: ['primary', 'progressive'], modes: 'edit' },
{ action: 'back', label: 'Go back', modes: 'result' },
{ action: 'exit', label: 'Exit', flags: 'safe', modes: ['edit', 'result'] }
];
GetterDialog.prototype.cleanTitle = function(t) { return t.replace(suffixRE, '').replace(/\s?\([^)]*\)$/, '').trim(); };
GetterDialog.prototype.normalizeTitle = function(t) { return t.replace(/_/g, ' ').trim().replace(/^(.)/, c => c.toUpperCase()); };
GetterDialog.prototype.getSortKey = function(title) {
const clean = this.cleanTitle(title);
const parts = clean.split(' ');
return (parts.length <= 1) ? title.toLowerCase() : (parts[parts.length - 1] + ', ' + parts.slice(0, parts.length - 1).join(' ') + ' | ' + title).toLowerCase();
};
GetterDialog.prototype.compileCriteria = function(vals, anchorLC) {
return vals.filter(t => t.toLowerCase() !== anchorLC)
.map(r => new RegExp('^' + r.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*') + '$', 'i'));
};
GetterDialog.prototype.getBestAnchor = function() {
const getTerms = (w) => w.getValue().map(t => t.trim()).filter(Boolean);
const candidates = [
...getTerms(this.surnameTags).map(t => ({ term: t, weight: 2000 + this.score(t) })),
...getTerms(this.givenTags).map(t => ({ term: t, weight: 1000 + this.score(t) })),
...getTerms(this.middleTags).map(t => ({ term: t, weight: 0 + this.score(t) }))
];
return candidates.length === 0 ? null : candidates.reduce((b, c) => (c.weight > b.weight ? c : b)).term;
};
GetterDialog.prototype.score = function(term) {
const wildcardCount = (term.match(/\*/g) || []).length;
return (wildcardCount === 0 ? 1000 : 0) + (term.length - wildcardCount) - wildcardCount;
};
GetterDialog.prototype.formatResult = function(p, mode, includeIll) {
if (mode === 'anbl') return `* {{anbl|${p.title}}}`;
const bY = p.ent?.claims?.P569?.[0]?.mainsnak?.datavalue?.value?.time?.substring(1, 5);
const dY = p.ent?.claims?.P570?.[0]?.mainsnak?.datavalue?.value?.time?.substring(1, 5);
const bio = bY ? (dY ? ` (${bY}–${dY})` : ` (born ${bY})`) : '';
const rawDesc = (p.desc || '').replace(/\s?\(\s*(?:born|d\.)?\s*\d{4}(?:\s*–\s*\d{4})?\s*\)$/i, '').trim();
const suffix = rawDesc ? `, ${rawDesc}` : '';
let display = `[[${p.title}]]`;
if (includeIll) {
const langKey = LANG_PREFS.find(l => p.ent.sitelinks?.[l]) || Object.keys(p.ent.sitelinks || {}).find(k => k.endsWith('wiki') && k !== 'enwiki' && !NON_LANG_WIKIS.includes(k));
if (langKey) display = `{{ill|${p.title}|${langKey.replace('wiki', '')}|${p.ent.sitelinks[langKey].title}}}`;
}
return `* ${display}${bio}${suffix}`;
};
GetterDialog.prototype.matchesCriteria = function(title, compiled) {
const clean = this.cleanTitle(title);
const tokens = clean.split(/\s+/);
const given = tokens[0] || '', surname = tokens.length > 1 ? tokens[tokens.length - 1] : '';
const middleLC = tokens.length > 2 ? tokens.slice(1, -1).map(x => x.toLowerCase()) : [];
const check = (val, regexes) => (!regexes || regexes.length === 0 || regexes.some(re => re.test(val)));
return check(given, compiled.given) && check(surname, compiled.surname) && (compiled.middle.length === 0 || compiled.middle.every(m => middleLC.includes(m)));
};
GetterDialog.prototype.getExistingLinks = async function() {
const titles = new Set();
try {
const data = await this.api.get({ action: 'parse', page: mw.config.get('wgPageName'), prop: 'wikitext', format: 'json' });
const text = data.parse?.wikitext?.['*'] || '';
const regexes = [/\[\[([^\]|#]+)/g, /\{\{(?:anbl|Annotated biography link)\|([^}|]+)/gi];
regexes.forEach(re => { let m; while ((m = re.exec(text)) !== null) titles.add(this.normalizeTitle(m[1])); });
} catch (e) { console.error('[Getter] Parse error:', e); }
return titles;
};
async function checkEntitiesData(qids, signal) {
if (!qids.length) return {};
const all = {};
for (let i = 0; i < qids.length; i += 50) {
if (signal?.aborted) throw new DOMException('Aborted', 'AbortError');
try {
const res = await fetch('https://www.wikidata.org/w/api.php?action=wbgetentities&ids=' + qids.slice(i, i + 50).join('|') + '&props=claims|labels|descriptions|sitelinks&languages=en&format=json&origin=*', { signal }).then(r => r.json());
Object.assign(all, res.entities || {});
} catch (e) { if (e.name === 'AbortError') throw e; console.error('[Getter] Wikidata batch error', e); }
}
return all;
}
GetterDialog.prototype.autoPopulateFields = async function () {
const title = mw.config.get('wgTitle'), cleanTitle = this.cleanTitle(title);
const m = title.match(/^(.*?) \((given name|surname)\)$/i);
if (m) { m[2].toLowerCase() === 'given name' ? this.givenTags.addTag(m[1]) : this.surnameTags.addTag(m[1]); return; }
try {
const data = await this.api.get({ action: 'parse', page: mw.config.get('wgPageName'), prop: 'wikitext', format: 'json' });
const text = data.parse?.wikitext?.['*'] || '';
const hasSurname = /\{\{\s*surname\s*(?:\||\}\})/i.test(text);
const givenTemplate = text.match(/\{\{\s*given name\b[^}]*\}\}/i)?.[0] || '';
const isBoth = /\|\s*type\s*=\s*both\b/i.test(givenTemplate);
if (hasSurname || (givenTemplate && isBoth)) this.surnameTags.addTag(cleanTitle);
if (givenTemplate) this.givenTags.addTag(cleanTitle);
if (!hasSurname && !givenTemplate) this.givenTags.addTag(cleanTitle);
} catch (e) { console.error('[Getter] Inspect error', e); this.givenTags.addTag(cleanTitle); }
};
GetterDialog.prototype.initialize = function() {
GetterDialog.super.prototype.initialize.apply(this, arguments);
this.api = new mw.Api();
this.panel = new OO.ui.PanelLayout({ padded: true, expanded: false });
this.givenTags = new OO.ui.TagMultiselectWidget({ placeholder: 'Given names...', allowArbitrary: true });
this.middleTags = new OO.ui.TagMultiselectWidget({ placeholder: 'Middle terms...', allowArbitrary: true });
this.surnameTags = new OO.ui.TagMultiselectWidget({ placeholder: 'Surnames...', allowArbitrary: true });
this.excludeTags = new OO.ui.TagMultiselectWidget({ placeholder: 'Exclude strings...', allowArbitrary: true });
this.humanOnly = new OO.ui.CheckboxInputWidget({ selected: true });
this.excludeHumans = new OO.ui.CheckboxInputWidget({ selected: false });
this.includeIll = new OO.ui.CheckboxInputWidget({ selected: false });
this.sortBySurname = new OO.ui.CheckboxInputWidget({ selected: true });
this.updateMode = new OO.ui.CheckboxInputWidget({ selected: false });
this.excludeHumans.on('change', (s) => { if (s) { this.humanOnly.setSelected(false); this.sortBySurname.setSelected(false); } });
this.humanOnly.on('change', (s) => { if (s) this.excludeHumans.setSelected(false); });
this.formatSelect = new OO.ui.ButtonSelectWidget({ items: [new OO.ui.ButtonOptionWidget({ data: 'wiki', label: 'Wiki' }), new OO.ui.ButtonOptionWidget({ data: 'anbl', label: 'ANBL' })]});
this.formatSelect.selectItemByData('wiki');
this.fieldset = new OO.ui.FieldsetLayout({ label: 'Advanced Filter Settings' });
this.fieldset.addItems([
new OO.ui.FieldLayout(this.givenTags, { label: 'Given names', align: 'top' }),
new OO.ui.FieldLayout(this.middleTags, { label: 'Middle terms', align: 'top' }),
new OO.ui.FieldLayout(this.surnameTags, { label: 'Surnames', align: 'top' }),
new OO.ui.FieldLayout(this.excludeTags, { label: 'Exclude strings', align: 'top' }),
new OO.ui.FieldLayout(this.humanOnly, { label: 'Humans only', align: 'inline' }),
new OO.ui.FieldLayout(this.excludeHumans, { label: 'Exclude humans', align: 'inline' }),
new OO.ui.FieldLayout(this.sortBySurname, { label: 'Sort by surname', align: 'inline' }),
new OO.ui.FieldLayout(this.updateMode, { label: 'Exclude existing links', align: 'inline' }),
new OO.ui.FieldLayout(this.includeIll, { label: 'Include {{ill}}', align: 'inline' }),
new OO.ui.FieldLayout(this.formatSelect, { label: 'Format', align: 'top' })
]);
this.resultArea = new OO.ui.MultilineTextInputWidget({ rows: 15, autosize: true });
this.copyButton = new OO.ui.ButtonWidget({ label: 'Copy list', icon: 'copy', flags: ['progressive'] });
this.copyButton.$element.css('marginTop', '10px');
this.copyButton.on('click', () => navigator.clipboard.writeText(this.resultArea.getValue()).then(() => mw.notify('Copied!')));
this.resultContainer = new OO.ui.PanelLayout({ padded: true, expanded: false });
this.resultContainer.$element.append(new OO.ui.FieldLayout(this.resultArea, { label: 'Results', align: 'top' }).$element, this.copyButton.$element).hide();
this.panel.$element.append(this.fieldset.$element, this.resultContainer.$element);
this.$body.append(this.panel.$element);
this.actions.setMode('edit');
this.autoPopulateFields();
};
GetterDialog.prototype.runGetter = async function() {
this.abortController = new AbortController();
try {
const { humanOnly, excludeHumans, includeIll, sortBySurname, formatSelect, updateMode } = this;
const mode = formatSelect.findSelectedItem().getData(), anchor = this.getBestAnchor();
if (!anchor) { mw.notify('Please enter search terms.', { type: 'error' }); return; }
const existing = updateMode.isSelected() ? await this.getExistingLinks() : new Set(), resultsMap = new Map();
const excludeList = this.excludeTags.getValue().map(e => e.toLowerCase()), anchorLC = anchor.toLowerCase();
const compiledCriteria = {
given: this.compileCriteria(this.givenTags.getValue(), anchorLC),
surname: this.compileCriteria(this.surnameTags.getValue(), anchorLC),
middle: this.middleTags.getValue().filter(t => t.toLowerCase() !== anchorLC).map(m => m.toLowerCase())
};
let cont = {};
do {
const data = await this.api.get({ action: 'query', list: 'search', srsearch: `intitle:"${anchor}"`, srlimit: 'max', sroffset: cont.sroffset || 0, format: 'json' });
for (const p of (data.query?.search || [])) {
if (excludeList.some(e => p.title.toLowerCase().includes(e))) continue;
const title = this.normalizeTitle(p.title);
if (!existing.has(title) && this.matchesCriteria(title, compiledCriteria)) resultsMap.set(p.title, { title: p.title, pageid: p.pageid });
}
cont = data.continue || {};
} while (cont.sroffset && !this.abortController?.signal.aborted);
const pageIds = Array.from(resultsMap.values()).map(p => p.pageid), confirmedResults = new Map();
for (let i = 0; i < pageIds.length; i += 50) {
if (this.abortController?.signal.aborted) return;
const chunk = pageIds.slice(i, i + 50), qidList = [], qidMap = {};
const pageData = await this.api.get({ action: 'query', prop: 'pageprops|description', pageids: chunk.join('|'), format: 'json' });
Object.values(pageData.query.pages).forEach(p => { if (p.pageprops?.wikibase_item) { qidList.push(p.pageprops.wikibase_item); qidMap[p.pageprops.wikibase_item] = p; } });
const entities = await checkEntitiesData(qidList, this.abortController?.signal);
for (const qid in entities) {
const ent = entities[qid], p = qidMap[qid];
const isHuman = ent.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5");
if ((humanOnly.isSelected() && !isHuman) || (excludeHumans.isSelected() && isHuman)) continue;
confirmedResults.set(p.title, { title: p.title, desc: p.description || ent.descriptions?.en?.value || '', ent });
}
}
this.resultArea.setValue(Array.from(confirmedResults.values())
.map(p => ({ formatted: this.formatResult(p, mode, includeIll.isSelected()), sortKey: sortBySurname.isSelected() ? this.getSortKey(p.title) : p.title.toLowerCase() }))
.sort((a, b) => a.sortKey.localeCompare(b.sortKey, undefined, { sensitivity: 'base' }))
.map(r => r.formatted).join('\n'));
this.fieldset.$element.hide(); this.resultContainer.$element.show(); this.actions.setMode('result');
} catch (e) { if (e.name !== 'AbortError') { console.error('[Getter] Generator error:', e); mw.notify('Error generating list.', { type: 'error' }); } }
finally { this.abortController = null; }
};
GetterDialog.prototype.getActionProcess = function(action) {
if (action === 'generate') return new OO.ui.Process(async () => { await this.runGetter(); });
if (action === 'back') { if (this.abortController) this.abortController.abort(); return new OO.ui.Process(() => { this.resultContainer.$element.hide(); this.fieldset.$element.show(); this.actions.setMode('edit'); }); }
if (action === 'exit') { if (this.abortController) this.abortController.abort(); return new OO.ui.Process(() => this.close()); }
return GetterDialog.super.prototype.getActionProcess.call(this, action);
};
mw.loader.using(['mediawiki.api', 'mediawiki.util', 'oojs-ui', 'oojs-ui-widgets', 'oojs-ui-windows']).then(() => {
const wm = new OO.ui.WindowManager();
$(document.body).append(wm.$element);
const dialog = new GetterDialog({ size: 'medium' });
wm.addWindows([dialog]);
$(mw.util.addPortletLink('p-tb', '#', 'Getter', 't-getter')).on('click', (e) => { e.preventDefault(); wm.openWindow(dialog); });
});
}(mediaWiki, jQuery, OO));
(function() {
if (window.IllWill) return;
const STRIKEOUT = true;
const HIDE = 'hide';
const IllWill = {
Version: "0.5",
Attribution: 'created by <a target="_blank" href="/wiki/User:Cobaltcigs">cobaltcigs</a>',
Summary: "Added interlanguage links using IllWill.js",
DataApi: "https://www.wikidata.org/w/api.php",
LocalLang: mw.config.get("wgPageContentLanguage"),
Disambiguator: / \(.*\)$/,
SiteLinks: {},
Config: {
MaxLanguages: 10,
IgnoreScientific: STRIKEOUT,
AutoDiff: true
},
css: `
#illwill { clear: both; margin-bottom: 1.5em; border: 1px solid #a2a9b1; padding: 15px; background: #f8f9fa; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.ill-sitelinks a { display: inline-block; margin-bottom: 3px; font-size: 0.9em; color: #36c; }
.ill-size-tag { font-size: 0.8em; color: #72777d; margin-left: 6px; white-space: nowrap; }
.illwill-ignore { text-decoration: line-through; color: #72777d; font-size: 0.85em; }
.ill-x { width: 35px; text-align: center; }
.ill-input, .ill-output { font-family: 'Courier New', monospace; white-space: pre-wrap; font-size: 0.9em; background: #fff; border: 1px solid #eaecf0; padding: 4px; border-radius: 2px; }
#ill-buttons { text-align: right; padding: 10px; border-top: 1px solid #a2a9b1; margin-top: 10px; }
#ill-buttons button { margin-left: 10px; cursor: pointer; padding: 6px 16px; border-radius: 2px; border: 1px solid #a2a9b1; transition: all 0.2s; }
#ill-ok { background: #36c; color: #fff; border-color: #36c !important; font-weight: bold; }
#ill-ok:hover { background: #447ff5; }
#ill-ok:disabled { background: #eaecf0; color: #72777d; border-color: #c8ccd1 !important; cursor: not-allowed; opacity: 0.7; }
#ill-table { width: 100%; border-collapse: collapse; margin-top: 10px; background: white; }
#ill-table th { background: #f2f2f2; text-align: left; padding: 8px; border: 1px solid #a2a9b1; }
#ill-table td { padding: 8px; border: 1px solid #a2a9b1; vertical-align: top; }
#ill-table caption { background: #eaffea; font-weight: bold; padding: 10px; border: 1px solid #a2a9b1; border-bottom: none; font-size: 1.1em; }
#ill-help { font-size: 0.85em; color: #54595d; padding: 10px; line-height: 1.4; }
.ill-loading-inline { font-style: italic; color: #72777d; font-size: 0.9em; display: block; margin: 5px 0; }
`,
setup() {
if (!["edit", "submit"].includes(mw.config.get('wgAction'))) return;
mw.loader.addStyleTag(this.css);
const link = mw.util.addPortletLink('p-cactions', '#', 'IllWill', 'ca-illwill', "Add interlanguage links via Wikidata");
if (link) {
link.addEventListener('click', (e) => {
e.preventDefault();
this.makePanel();
});
}
},
norm(s) {
if (!s) return "";
const t = s.trim().replace(/[\s_]+/g, " ");
return t.charAt(0).toUpperCase() + t.slice(1);
},
async getRedLinks($textbox) {
const txt = $textbox.textSelection('getContents') || "";
const wl = txt.match(/\[\[[^[\]\n]+\]\]/g);
if (!wl) return {
reds: [],
txt
};
const api = new mw.Api();
const textToParse = wl.join("").replace(/\|[^\]]+/g, "");
try {
const data = await api.post({
action: 'parse',
text: textToParse,
contentmodel: 'wikitext',
formatversion: 2
});
const tempDiv = document.createElement('div');
tempDiv.innerHTML = data.parse.text;
const reds = Array.from(tempDiv.querySelectorAll("a.new")).map(x => {
const urlParams = new URLSearchParams(x.href.split('?')[1]);
return urlParams.get('title')?.replace(/_/g, " ");
}).filter(Boolean);
return {
reds: [...new Set(reds)],
txt
};
} catch (e) {
return {
reds: [],
txt
};
}
},
async makePanel() {
const $editform = $('#editform');
const $textbox = $('#wpTextbox1');
$('#illwill').remove();
$editform.hide();
const $wrapper = $('<div id="illwill">Loading redlinks...</div>').insertBefore($editform);
const {
reds,
txt
} = await this.getRedLinks($textbox);
const wtLinks = [];
reds.forEach(title => {
const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\s/g, "[\\s_]+");
const re = new RegExp("\\[\\[\\s*" + escaped + "\\s*(?:\\|[^\\[\\]]+)?\\]\\]", "ig");
const match = txt.match(re);
if (match && !wtLinks.some(z => z.title === title)) {
wtLinks.push({
link: match[0],
title
});
}
});
if (!wtLinks.length) {
const $back = $('<button>Quit</button>').on('click', () => {
$wrapper.remove();
$editform.show();
});
$wrapper.empty().append($('<strong>No valid redlinks found in text.</strong> '), $back);
return;
}
const $table = $(`
<table id="ill-table" class="wikitable">
<caption>IllWill.js (v${this.Version})</caption>
<thead>
<tr>
<th class="ill-x"><input type="checkbox" id="ill-select-all" disabled></th>
<th>Input link</th>
<th>Possible Wikidata matches</th>
<th>Sitelinks (top ${this.Config.MaxLanguages} by size)</th>
<th>Resulting wikitext</th>
</tr>
</thead>
<tbody></tbody>
<tfoot>
<tr>
<td colspan="3" id="ill-help">Carefully examine possible Wikidata matches.</td>
<td colspan="2" id="ill-buttons">
<button id="ill-cancel">Cancel</button>
<button id="ill-ok" disabled>Show changes</button>
</td>
</tr>
</tfoot>
</table>
`);
const $tbody = $table.find('tbody');
wtLinks.forEach((item, i) => {
$tbody.append(`
<tr id="ill-row-${i}" data-index="${i}">
<td class="ill-x"><input type="checkbox" class="ill-row-check" disabled></td>
<td class="ill-input">${this.escapeHTML(item.link)}</td>
<td class="ill-wd-item"><span class="ill-loading-inline">Searching...</span></td>
<td class="ill-sitelinks"></td>
<td class="ill-output"></td>
</tr>
`);
});
$wrapper.empty().append($table);
const chunkSize = 5;
for (let i = 0; i < wtLinks.length; i += chunkSize) {
const chunk = wtLinks.slice(i, i + chunkSize);
await Promise.all(chunk.map((item, idx) => this.fetchWikidataOptimized(item.title, i + idx)));
}
$table.on('click', '#ill-cancel', () => {
$editform.show();
$wrapper.remove();
});
$table.on('click', '#ill-ok', () => this.onOkButton($editform, $wrapper, $textbox));
$table.on('change', '#ill-select-all', (e) => {
const isChecked = $(e.target).is(':checked');
$table.find('.ill-row-check:not(:disabled)').each(function() {
$(this).prop('checked', isChecked).trigger('change');
});
});
$table.on('change', '.ill-row-check', (e) => {
this.onCheckbox(e);
this.updateOkButtonState($table);
});
$table.on('change', 'input[type="radio"]', (e) => {
this.showSiteLinks(e);
});
},
async fetchWikidataOptimized(title, index) {
const query = title.replace(this.Disambiguator, "");
const url = `${this.DataApi}?action=wbsearchentities&search=${encodeURIComponent(query)}&language=${this.LocalLang}&limit=5&format=json&origin=*`;
try {
const resp = await fetch(url);
const data = await resp.json();
const results = data.search;
const $cell = $(`#ill-row-${index} .ill-wd-item`);
if (!results || !results.length) {
$cell.html('<span class="ill-na">(no results)</span>');
return;
}
const entityIds = results.map(r => r.id);
await this.fetchEntities(entityIds, index, $cell);
} catch (e) {
$(`#ill-row-${index} .ill-wd-item`).text("Search failed.");
}
},
async fetchEntities(ids, rowIndex, $cell) {
const url = `${this.DataApi}?action=wbgetentities&ids=${ids.join('|')}&props=labels|descriptions|claims|sitelinks&format=json&origin=*`;
try {
const resp = await fetch(url);
const data = await resp.json();
$cell.empty();
ids.forEach(qNum => {
const entity = data.entities[qNum];
if (!entity) return;
const isScientific = entity.claims?.P31?.some(s => s.mainsnak?.datavalue?.value?.id === "Q13442814");
if (isScientific && this.Config.IgnoreScientific === HIDE) return;
const desc = entity.descriptions?.[this.LocalLang]?.value || entity.descriptions?.en?.value || "";
const label = entity.labels?.[this.LocalLang]?.value || entity.labels?.en?.value || qNum;
const isStrikeout = isScientific && this.Config.IgnoreScientific === STRIKEOUT;
const $div = $(`
<div class="${isStrikeout ? 'illwill-ignore' : ''}">
<input type="radio" name="selection-${rowIndex}" value="${qNum}" id="radio-${qNum}" ${isStrikeout ? 'disabled' : ''}>
<label for="radio-${qNum}">
<a href="https://wikidata.org/wiki/${qNum}" target="_blank">${this.escapeHTML(label)}</a>:
<span class="ill-desc">${desc ? this.escapeHTML(desc) : '<i>no description</i>'}</span>
</label>
</div>
`);
$cell.append($div);
this.SiteLinks[qNum] = entity.sitelinks || {};
});
} catch (e) {
$cell.text("Detail fetch failed.");
}
},
updateOkButtonState($table) {
const $rows = $table.find('.ill-row-check:not(:disabled)');
const $checkedRows = $rows.filter(':checked');
const anyEnabled = $rows.length > 0;
$table.find('#ill-ok').prop('disabled', $checkedRows.length === 0);
const $master = $table.find('#ill-select-all');
$master.prop('disabled', !anyEnabled);
if (anyEnabled && $rows.length === $checkedRows.length) {
$master.prop('checked', true);
} else {
$master.prop('checked', false);
}
},
async showSiteLinks(e) {
const isManualToggling = e.fromCheckbox || false;
const qNum = e.target.value;
const $row = $(e.target).closest('tr');
const sitelinks = this.SiteLinks[qNum];
const $checkbox = $row.find('.ill-row-check');
const $sitelinkCell = $row.find('.ill-sitelinks');
const $outputCell = $row.find('.ill-output');
const $table = $('#ill-table');
const effectiveMax = Math.min(Math.max(this.Config.MaxLanguages, 3), 6);
const validKeys = Object.keys(sitelinks).filter(k => k.endsWith('wiki') && !k.includes('commons'));
$sitelinkCell.html('<span class="ill-loading-inline">Measuring articles...</span>');
$outputCell.empty();
if (!isManualToggling) {
$checkbox.prop('disabled', true).prop('checked', false);
}
const candidates = validKeys.slice(0, 25);
const sizeMap = [];
await Promise.all(candidates.map(async (key) => {
const lang = key.replace('wiki', '').replace(/_/g, '-');
const title = sitelinks[key].title;
const endpoint = `https://${lang}.wikipedia.org/w/api.php?action=query&titles=${encodeURIComponent(title)}&prop=info&format=json&origin=*`;
try {
const res = await fetch(endpoint).then(r => r.json());
const page = Object.values(res.query.pages)[0];
sizeMap.push({
lang,
title,
length: page.length || 0
});
} catch (err) {
sizeMap.push({
lang,
title,
length: 0
});
}
}));
sizeMap.sort((a, b) => b.length - a.length);
const topLinks = sizeMap.slice(0, effectiveMax);
$sitelinkCell.html(topLinks.map(item => {
const kbValue = Math.round(item.length / 1024);
return `<div><a href="https://${item.lang}.wikipedia.org/wiki/${encodeURIComponent(item.title)}" target="_blank">${item.lang}:${this.escapeHTML(item.title)}</a> <span class="ill-size-tag">(${kbValue} kB)</span></div>`;
}).join(""));
const inputWikitext = $row.find('.ill-input').text();
const match = inputWikitext.match(/\[\[(.*?)\]\]/);
if (match) {
const parts = match[1].split('|');
const targetPage = parts[0].trim();
const surfaceName = (parts[1] || parts[0]).trim();
const illLangs = topLinks.map(item => {
const isSameTitle = this.norm(item.title) === this.norm(targetPage);
return `|${item.lang}|${isSameTitle ? '' : item.title}`;
}).join("");
const ltParam = (this.norm(surfaceName) === this.norm(targetPage)) ? "" : `|lt=${surfaceName}`;
$outputCell.text(`{{ill|${targetPage}${ltParam}${illLangs}}}`);
}
$checkbox.prop('disabled', !topLinks.length);
if (!isManualToggling) {
$checkbox.prop('checked', !!topLinks.length);
}
this.updateOkButtonState($table);
},
onCheckbox(e) {
const $row = $(e.target).closest('tr');
const $radio = $row.find('input[type="radio"]:checked');
const isChecked = $(e.target).is(':checked');
if (isChecked) {
if ($radio.length) {
this.showSiteLinks({
target: $radio[0],
fromCheckbox: true
});
}
} else {
$row.find('.ill-sitelinks, .ill-output').empty();
}
},
onOkButton($editform, $wrapper, $textbox) {
let text = $textbox.textSelection('getContents');
let changed = false;
$wrapper.find('.ill-row-check:checked').each(function() {
const $row = $(this).closest('tr');
const input = $row.find('.ill-input').text();
const output = $row.find('.ill-output').text();
if (output) {
const escapedInput = input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
text = text.replace(new RegExp(escapedInput, "gi"), output);
changed = true;
}
});
if (changed) {
$('#wpSummary').val((i, v) => (v ? v + "; " : "") + this.Summary);
$textbox.textSelection('setContents', text);
$wrapper.remove();
$editform.show();
if (this.Config.AutoDiff) $('#wpDiff').click();
}
},
escapeHTML(str) {
return str.replace(/[&<>"']/g, m => ({
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
})[m]);
}
};
window.IllWill = IllWill;
mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => {
$(() => IllWill.setup());
});
})();
/* global ComradeLib, mw, $ */
(async function() {
try {
await mw.loader.getScript('https://test.wikipedia.org/w/index.php?title=User:Sam_Sailor/test.js/comrade-options.js&action=raw&ctype=text/javascript');
} catch (e) {
console.warn("Comrade: Could not load /comrade-options.js, using fallback defaults.", e);
}
window.ComradeOptions = Object.assign({
version: "0.5.0",
excludedTemplates: [],
nudgeTimeout: 10000,
debugMode: false
}, window.ComradeOptions || {});
await mw.loader.getScript('https://test.wikipedia.org/w/index.php?title=User:Sam_Sailor/test.js/comrade-lib.js&action=raw&ctype=text/javascript');
async function getTargetPage(api, name, type) {
const titles = type === 'surname' ? [`List of people with the English surname ${name}`, `List of people with surname ${name}`, `${name} (surname)`, `${name} (name)`, name] : [`List of people with given name ${name}`, `List of people named ${name}`, `${name} (given name)`, `${name} (name)`, name];
const res = await api.get({
action: 'query',
titles: titles.join('|'),
formatversion: 2
});
if (!res.query || !res.query.pages) return null;
return titles.find(t => {
const pg = res.query.pages.find(p => p.title === t);
return pg && !pg.missing;
});
}
async function processHumanNameLists(api, entity, wikitext, currentTitle, hasShortDesc, isOrphan, tClean) {
const {
givens,
surnames
} = await fetchWikidataNameLabels(entity);
const nameParts = tClean.split(' ');
const hasFamilyNameHatnote = /\{\{family[ _]name[ _]hatnote/i.test(wikitext);
const primarySurname = hasFamilyNameHatnote ? nameParts[0] : (getSmartSortKey(tClean, entity).split(',')[0] || nameParts[nameParts.length - 1]);
for (const name of givens) {
if (name.toLowerCase() === primarySurname.toLowerCase()) continue;
const nameInTitle = new RegExp('\\b' + name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b', 'i').test(tClean);
if (!nameInTitle) continue;
const target = await getTargetPage(api, name, 'given');
if (target) await verifyAndNudge(api, name, 'given name', currentTitle, hasShortDesc, isOrphan, target);
}
const finalSurnames = surnames.length > 0 ? surnames : (nameParts.length >= 2 ? [nameParts[nameParts.length - 1]] : []);
for (const name of finalSurnames) {
const nameInTitle = new RegExp('\\b' + name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b', 'i').test(tClean);
if (!nameInTitle) continue;
const target = await getTargetPage(api, name, 'surname');
if (target) await verifyAndNudge(api, name, 'surname', currentTitle, hasShortDesc, isOrphan, target);
}
}
async function verifyAndNudge(api, name, type, currentTitle, hasShortDesc, isOrphan, foundTitle) {
const res = await api.get({
action: 'query',
prop: 'revisions',
titles: foundTitle,
rvprop: 'content',
formatversion: 2
});
const page = res.query.pages[0];
if (!page || page.missing || !page.revisions) return;
const text = page.revisions[0].content;
const isNameMatch = /\{\{(Surname|Hndis|Given[ _]name|Forename|Givenname)/i.test(text);
const isNameDab = /\{\{(Disambiguation|Dab|Disambig)(?:[^}]*\|(surname|surnames|given[ _]name|given[ _]names)|(?:\s*\}\}))/i.test(text);
const isNamePageTitle = foundTitle.includes('List of people with');
if (!isNameMatch && !isNameDab && !isNamePageTitle) return;
const escapedTitle = currentTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/ /g, '[ _]');
const alreadyListed = new RegExp('(\\[\\[|\\{\\{anbl\\|[ _]*|\\|)' + escapedTitle, 'i').test(text);
if (alreadyListed) return;
const $container = $('<div>').addClass('comrade-container').addClass(isOrphan ? 'comrade-nudge' : 'comrade-info');
const $header = $('<div>').addClass('comrade-header').append($('<strong>').text(isOrphan ? 'Comrade nudge:' : 'Informative:'));
const $content = $('<div>').addClass('comrade-content');
$content.append($('<span>').append(`${type.charAt(0).toUpperCase() + type.slice(1)} not listed at `, $('<a>').attr({
href: mw.util.getUrl(foundTitle),
target: '_blank'
}).text(foundTitle), "; should it be?"));
const snippet = '* {{anbl|' + currentTitle + '}}';
const $snippetWrapper = $('<div>').css({
'display': 'flex',
'align-items': 'center',
'gap': '10px',
'margin-top': '4px'
});
const $instructionSpan = $('<span>').append('Consider adding ', $('<code>').text(snippet));
const $copyBtn = $('<button>').text('Copy & insert').on('click', function() {
navigator.clipboard.writeText('\n' + snippet).then(() => {
$(this).text('Copied!').prop('disabled', true);
const editUrl = mw.util.getUrl(foundTitle, {
action: 'edit',
summary: 'Adding * {{[[Template:Annotated biography link|anbl]]|[[' + currentTitle + ']]}}'
});
window.open(editUrl, '_blank');
});
});
$snippetWrapper.append($instructionSpan, $copyBtn);
$content.append($snippetWrapper);
if (!hasShortDesc) {
$content.append($('<span>').css({
'color': '#d33',
'font-weight': 'bold',
'margin-top': '5px'
}).text('⚠️ Missing short description: Please add a concise one before listing.'));
}
$container.append($header, $content);
ComradeLib.appendDismiss($container);
}
async function checkNamingPrecision(currentTitle, isHuman) {
const disambigMatch = currentTitle.match(/^(.+?)(?:, | \()/);
if (isHuman || !disambigMatch) return;
const baseTitle = disambigMatch[1].trim();
const api = new mw.Api();
try {
const baseRes = await api.get({
action: 'query',
titles: baseTitle,
redirects: 1,
formatversion: 2
});
const page = baseRes.query.pages[0];
const baseExists = !page.missing;
let baseRedirectsToSelf = false;
if (baseRes.query.redirects) {
baseRedirectsToSelf = baseRes.query.redirects.some(r => r.to === currentTitle);
}
const searchRes = await api.get({
action: 'query',
list: 'allpages',
apprefix: baseTitle,
aplimit: 50,
formatversion: 2
});
const competitors = searchRes.query.allpages.filter(p => {
const t = p.title;
return t !== currentTitle && t !== baseTitle && (t.startsWith(baseTitle + ', ') || t.startsWith(baseTitle + ' ('));
});
if (!baseExists || baseRedirectsToSelf) {
const $container = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $content = $('<div>').addClass('comrade-content');
if (competitors.length === 0) {
$header.append($('<strong>').text('Precision:'));
const reason = baseRedirectsToSelf ? "already redirects here" : "is free";
$content.append($('<span>').append(`The base name `, $('<code>').text(baseTitle), ` ${reason}. Consider moving this article.`));
const moveUrl = mw.util.getUrl('Special:MovePage/' + currentTitle, {
wpNewTitle: baseTitle,
wpReason: 'Unnecessary disambiguation; base name is free/redirects here per [[WP:PRECISION]]'
});
$content.append($('<button>').text(`Move to ${baseTitle}`).on('click', () => window.open(moveUrl, '_blank')));
} else {
$header.append($('<strong>').text('Observation:'));
const state = baseRedirectsToSelf ? 'redirect to this page' : 'redlink';
$content.append($('<span>').append(`Base name `, $('<code>').text(baseTitle), ` has ${competitors.length} competitors, but it is currently a ${state}.`));
$content.append($('<span>').text('Consider if a disambiguation page is needed.'));
}
$container.append($header, $content);
ComradeLib.appendDismiss($container);
}
} catch (e) {
console.warn("Comrade: Precision check failed", e);
}
}
async function checkHumanSort(wikitext, entity, currentTitle, tClean, refName) {
const dsMatch = wikitext.match(/\{\{(?:DEFAULTSORT|Defaultsort):([^}]+)\}\}/i);
const {
givens
} = await fetchWikidataNameLabels(entity);
if (dsMatch) {
const currentDS = dsMatch[1].trim();
if (currentDS.includes(',')) {
let [lastPart, firstPart] = currentDS.split(',').map(s => s.trim());
if (firstPart.toLowerCase().endsWith(lastPart.toLowerCase())) {
let fixedFirst = firstPart.substring(0, firstPart.length - lastPart.length).trim();
let suggestion = `${fixNameCasing(lastPart, refName)}, ${fixNameCasing(fixedFirst, refName)}`;
if (suggestion !== currentDS) renderSortNudge(suggestion, "Redundant surname in given name field.");
} else if (givens.length > 0 && lastPart.split(' ').length > 1) {
const parts = lastPart.split(' ');
const misplacedGiven = parts.find(part => givens.some(gn => gn.toLowerCase() === part.toLowerCase()));
if (misplacedGiven) {
const newSurname = parts.filter(p => p !== misplacedGiven).join(' ');
let suggestion = `${fixNameCasing(newSurname, refName)}, ${fixNameCasing(misplacedGiven + ' ' + firstPart, refName)}`;
if (suggestion !== currentDS) renderSortNudge(suggestion, `Wikidata suggests "${misplacedGiven}" is a given name.`);
}
}
}
} else {
const hasFamilyNameHatnote = /\{\{family[ _]name[ _]hatnote/i.test(wikitext);
const nameParts = tClean.split(' ');
const targetSortName = hasFamilyNameHatnote ? `${nameParts[0]}, ${nameParts.slice(1).join(' ')}` : getSmartSortKey(tClean, entity);
if (targetSortName.toLowerCase() !== currentTitle.toLowerCase()) {
const $dsMissing = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Add: {{DEFAULTSORT:${targetSortName}}}`).on('click', () => performSortCorrection(targetSortName, "add"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` Tag is missing. `, $btn));
$dsMissing.append($header);
ComradeLib.appendDismiss($dsMissing);
}
}
}
async function fetchWikidataEntity(qid) {
const res = await fetch(`https://www.wikidata.org/w/api.php?action=wbgetentities&ids=${qid}&props=claims|descriptions|labels&languages=en&format=json&origin=*`).then(r => r.json());
return res.entities[qid];
}
function getSmartSortKey(fullName, entity) {
const countryIds = entity.claims?.P27?.map(c => c.mainsnak?.datavalue?.value?.id) || [];
const locationIds = entity.claims?.P17?.map(c => c.mainsnak?.datavalue?.value?.id) || [];
const placeIds = [...(entity.claims?.P19 || []), ...(entity.claims?.P119 || [])].map(c => c.mainsnak?.datavalue?.value?.id).filter(Boolean);
const allRelevantIds = [...countryIds, ...locationIds, ...placeIds];
const birthYear = parseInt(entity.claims?.P569?.[0]?.mainsnak?.datavalue?.value?.time?.match(/[+-](\d{4})/)?.[1]) || null;
if (fullName.startsWith("Saint ")) return fullName.replace("Saint ", "");
const arabicParticles = /\b(Abu|Abd|Abdel|Abdul|ben|bin|bint)\b/i;
if (fullName.match(arabicParticles)) {
if (birthYear && birthYear > 1900) {
const parts = fullName.split(' ');
const binIndex = parts.findIndex(p => p.toLowerCase() === 'bin' || p.toLowerCase() === 'ben');
if (binIndex > 0) return parts.slice(binIndex).join(' ') + ', ' + parts.slice(0, binIndex).join(' ');
} else {
return fullName;
}
}
const eastAsianQids = ["Q148", "Q865", "Q884", "Q424", "Q17", "Q881"];
if (allRelevantIds.some(id => eastAsianQids.includes(id))) {
if (allRelevantIds.includes("Q17") && birthYear && birthYear > 1885) return westernSort(fullName);
const parts = fullName.split(' ');
if (parts.length > 1) return `${parts[0]}, ${parts.slice(1).join(' ')}`;
}
return westernSort(fullName);
}
function westernSort(name) {
let cleanName = name;
if (name.startsWith("O'")) cleanName = name.replace("O'", "O");
const parts = cleanName.split(' ');
if (parts.length < 2) return cleanName;
const surname = parts.pop();
if (["Jr.", "Jr", "III", "II", "Sr.", "Sr"].includes(surname)) {
const actualSurname = parts.pop();
return `${actualSurname}, ${parts.join(' ')} ${surname}`;
}
return `${surname}, ${parts.join(' ')}`;
}
async function performSortCorrection(newName, type) {
const api = new mw.Api();
const title = mw.config.get("wgPageName");
try {
const data = await api.get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
formatversion: 2
});
let content = data.query.pages[0].revisions[0].content;
const dsRegex = /\{\{\s*(?:DEFAULTSORT|Defaultsort)\s*:[^}]+\}\}/i;
const newTag = `{{DEFAULTSORT:${newName}}}`;
if (dsRegex.test(content)) {
content = content.replace(dsRegex, newTag);
} else {
const catRegex = /\[\[Category:/i;
content = catRegex.test(content) ? content.replace(catRegex, `${newTag}\n[[Category:`) : content.trim() + `\n\n${newTag}`;
}
await api.postWithEditToken({
action: 'edit',
title: title,
text: content,
summary: `Setting DEFAULTSORT to ${newName}`,
nocreate: true
});
mw.notify(`DEFAULTSORT ${type === "add" ? "added" : "corrected"} to ${newName}!`, {
title: 'Comrade Success',
type: 'success'
});
setTimeout(() => location.reload(), 700);
} catch (err) {
mw.notify('Failed to save DEFAULTSORT.', {
type: 'error'
});
}
}
function renderSortNudge(target, reason) {
const $dsNudge = $('<div>').addClass('comrade-container comrade-nudge');
const $header = $('<div>').addClass('comrade-header');
const $btn = $('<button>').text(`Correct to: ${target}`).on('click', () => performSortCorrection(target, "format"));
$header.append($('<div>').append($('<strong>').text('DEFAULTSORT:'), ` ${reason} `, $btn));
$dsNudge.append($header);
ComradeLib.appendDismiss($dsNudge);
}
async function fetchWikidataNameLabels(entity) {
const familyIds = entity.claims?.P734?.map(c => c.mainsnak.datavalue.value.id) || [];
const givenIds = entity.claims?.P735?.map(c => c.mainsnak.datavalue.value.id) || [];
const allNameIds = [...new Set([...familyIds, ...givenIds])];
if (allNameIds.length === 0) return {
givens: [],
surnames: []
};
const url = new URL("https://www.wikidata.org/w/api.php");
url.search = new URLSearchParams({
action: 'wbgetentities',
ids: allNameIds.join('|'),
props: 'labels',
languages: 'en',
format: 'json',
origin: '*'
});
const nameData = await fetch(url).then(r => r.json());
return {
givens: givenIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean),
surnames: familyIds.map(id => nameData.entities[id]?.labels?.en?.value).filter(Boolean)
};
}
function fixNameCasing(part, referenceName) {
return part.split(' ').map(word => {
const match = referenceName.match(new RegExp(`\\b${word}\\b`, 'i'));
if (match) return match[0];
return referenceName.split(' ').find(tWord => word.toLowerCase().startsWith(tWord.toLowerCase()) || tWord.toLowerCase().startsWith(word.toLowerCase())) || word;
}).join(' ');
}
async function init() {
const api = new mw.Api();
const ns = mw.config.get("wgNamespaceNumber");
const action = mw.config.get("wgAction");
if (mw.config.get("wgDBname") !== "enwiki" || action !== "view") return;
if (ns === 14) {
const $btn = $('<button>').attr('id', 'comrade-audit-btn').text('Audit category quality').css({
'margin-bottom': '10px',
'padding': '5px 10px',
'cursor': 'pointer'
}).on('click', ComradeLib.performCategoryAudit);
$('.mw-category-generated').prepend($btn);
return;
}
if (ns !== 0) return;
const qid = mw.config.get("wgWikibaseItemId");
const currentTitle = mw.config.get("wgTitle");
try {
const [pageData, backlinkData] = await Promise.all([
api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get("wgPageName"),
rvprop: 'content',
formatversion: 2
}),
api.get({
action: 'query',
list: 'backlinks',
blfilterredir: 'nonredirects',
bllimit: 50,
blnamespace: 0,
bltitle: mw.config.get("wgPageName"),
formatversion: 2
})
]);
const page = pageData.query.pages[0];
if (!page || page.missing || !page.revisions) return;
const wikitext = page.revisions[0].content;
ComradeLib.checkLeadCapitalization(currentTitle);
// Ligature/ASCII-only/diacritic and domain
const norms = ComradeLib.getNormalizedTitles(currentTitle);
if (norms.ascii) {
let label = "ASCII-only";
let category = "R from ASCII-only";
if (/[æÆœŒijIJßẞ]/.test(currentTitle)) {
label = "Ligature";
category = "R to ligature";
} else if (currentTitle.normalize("NFD").match(/[\u0300-\u036f]/)) {
label = "Diacritic";
category = "R to diacritic";
}
await ComradeLib.checkAndRenderRedirect(norms.ascii, currentTitle, category, label);
}
await ComradeLib.checkDomainRedirect(qid, currentTitle);
// Orphan logic
const isOrphanTagged = /\{\{\s*(Orphan|Do-attempt|Lonely|Orp|Orphaned article)/i.test(wikitext) || /\| *orphan *=/i.test(wikitext);
const backLinkCount = backlinkData.query.backlinks.length;
if (isOrphanTagged && backLinkCount >= 1) {
const $container = $('<div>').addClass('comrade-container comrade-status');
const $btn = $('<button>').text('Remove orphan tag').on('click', async () => await ComradeLib.performDeorphan());
const $statusText = $('<span>').append($('<strong>').text('Status:'), ` Article has ${backLinkCount} backlink(s).`, $btn);
$container.append($('<div>').addClass('comrade-header').append($statusText));
ComradeLib.appendDismiss($container);
}
// Wikidata/human logic
let entity = qid ? await fetchWikidataEntity(qid) : null;
let isHuman = entity?.claims?.P31?.some(c => c.mainsnak?.datavalue?.value?.id === "Q5");
await checkNamingPrecision(currentTitle, isHuman);
if (isHuman && entity) {
const tClean = currentTitle.replace(/\s*\(.*?\)\s*/g, '').trim();
const refName = entity.labels?.en?.value || currentTitle;
const hasFamilyNameHatnote = /\{\{family[ _]name[ _]hatnote/i.test(wikitext);
const hasShortDesc = /\{\{\s*[Ss]hort description/i.test(wikitext);
const isOrphan = isOrphanTagged && backLinkCount < 1;
await ComradeLib.checkShortDescDates(wikitext, entity);
await ComradeLib.checkNoShortDesc(wikitext, entity);
// Check DEFAULTSORT
await checkHumanSort(wikitext, entity, currentTitle, tClean, refName);
const longName = ComradeLib.getLongNameFromWikitext(wikitext);
if (longName && longName.length > tClean.length) {
let sortKey = hasFamilyNameHatnote ? `${longName.split(' ')[0]}, ${longName.split(' ').slice(1).join(' ')}` : getSmartSortKey(longName, entity);
const rLongWikitext = `#REDIRECT [[${currentTitle}]]\n\n{{Redirect category shell|\n{{R from long name}}\n}}\n{{DEFAULTSORT:${sortKey}}}`;
await ComradeLib.checkAndRenderRedirect(longName, currentTitle, null, "Long name", rLongWikitext);
}
// Sort name R
const targetSortName = hasFamilyNameHatnote ? `${tClean.split(' ')[0]}, ${tClean.split(' ').slice(1).join(' ')}` : getSmartSortKey(tClean, entity);
if (targetSortName && targetSortName.toLowerCase() !== currentTitle.toLowerCase()) {
let targetPage = currentTitle.includes(' (') ? currentTitle.split(' (')[0] : currentTitle;
let rCat = targetPage !== currentTitle ? "R from ambiguous sort name" : "R from sort name";
if (targetSortName.includes(',')) {
const p = targetSortName.split(',');
rCat += `|${p[0].trim().charAt(0).toUpperCase()}|${p[1].trim().charAt(0).toUpperCase()}`;
}
await ComradeLib.checkAndRenderRedirect(targetSortName, targetPage, rCat, "Sort name");
}
// Name lists
await processHumanNameLists(api, entity, wikitext, currentTitle, hasShortDesc, isOrphan, tClean);
}
// Quality/ORES
const isRedirect = !!mw.config.get("wgIsRedirect");
const userExclusions = window.comradeQualityExclusions || [];
const allExclusions = [...window.ComradeOptions.excludedTemplates, ...userExclusions];
const exclusionRegexString = allExclusions.map(t => t.replace("Template:", "").replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
const exclusionRegex = new RegExp(exclusionRegexString, 'i');
if (isRedirect || exclusionRegex.test(wikitext)) {
console.log("Comrade check skipped: Page is a redirect or excluded via options.");
return;
}
ComradeLib.checkQualityConsistency();
} catch (err) {
console.error("Comrade error:", err);
}
}
init();
})();
(function() {
'use strict';
if (mw.config.get('wgCanonicalSpecialPageName') !== 'Search') {
return;
}
mw.loader.using(['mediawiki.util']).then(function() {
$(function() {
const $resultsInfo = $('.results-info');
const totalResultsRaw = $resultsInfo.attr('data-mw-num-results-total') || '0';
function formatWithCommas(numberString) {
return numberString.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
const state = {
language: mw.config.get('wgContentLanguage'),
totalResultsFormatted: formatWithCommas(totalResultsRaw)
};
let titlesCache = null;
function extractResultTitles() {
const currentResults = $('.mw-search-result-heading');
if (!titlesCache || titlesCache.length !== currentResults.length) {
titlesCache = currentResults.map(function() {
return $(this).find('a:first').attr('title');
}).get().filter(Boolean);
}
return titlesCache;
}
mw.hook("wikipage.content").add(function() {
titlesCache = null;
});
function addLimitOptions() {
const selects = document.querySelectorAll('.cdx-table-pager__limit-form select[name="limit"]');
if (!selects.length) return;
const valuesToAdd = ["1000", "2500", "5000"];
selects.forEach(function(select) {
valuesToAdd.forEach(function(value) {
if (select.querySelector('option[value="' + value + '"]')) return;
const option = document.createElement("option");
option.value = value;
option.textContent = formatWithCommas(value) + " items";
const existingOptions = Array.from(select.querySelectorAll("option"));
const insertBefore = existingOptions.find(function(opt) {
return Number(opt.value) > Number(value);
});
if (insertBefore) {
select.insertBefore(option, insertBefore);
} else {
select.appendChild(option);
}
});
});
}
mw.hook("wikipage.content").add(addLimitOptions);
const numLinks = $('.mw-numlink');
if (numLinks.length > 4 && !numLinks.eq(4).next().is('a[href*="limit=5000"]')) {
const $firstAnchor = numLinks.eq(4);
const $firstSpan = $('<span> | </span>').insertAfter($firstAnchor);
const url1 = new URL(window.location.href);
url1.searchParams.set('limit', '5000');
$('<a>', {
text: '5000',
title: 'Show 5000 results per page',
href: url1.toString()
}).insertAfter($firstAnchor).before($firstSpan);
}
if (numLinks.length > 9 && !numLinks.eq(9).next().is('a[href*="limit=5000"]')) {
const $secondAnchor = numLinks.eq(9);
const $secondSpan = $('<span> | </span>').insertAfter($secondAnchor);
const url2 = new URL(window.location.href);
url2.searchParams.set('limit', '5000');
$('<a>', {
text: '5000',
title: 'Show 5000 results per page',
href: url2.toString()
}).insertAfter($secondAnchor).before($secondSpan);
}
const $container = $("<div></div>").addClass("enhanced-search-container").hover(function() {
$(this).addClass("hover");
}, function() {
$(this).removeClass("hover");
});
const interwikiResults = document.getElementById("mw-interwiki-results");
if (interwikiResults) {
$container.addClass("enhanced-search-floating").insertBefore("#mw-interwiki-results");
} else {
$container.addClass("enhanced-search-absolute").insertBefore(".searchresults");
}
const totalFormattedStr = state.totalResultsFormatted;
const $titleElement = $("<div>Show a shortened list of<br />this page's " + $('.mw-search-result-heading').length + " results <small>(of total of " + totalFormattedStr + ")</small></div>").addClass("enhanced-header").appendTo($container);
const $contentArea = $("<div></div>").addClass("enhanced-content-area").appendTo($container).hide();
const $note1 = $("<div>Copy this page\'s " + $('.mw-search-result-heading').length + " results to the clipboard as...</div>").addClass("enhanced-note").insertAfter($container).hide();
function setupHoverTooltip(element, message) {
$(element).attr('title', message).hover(function() {
$(this).addClass("hover");
}, function() {
$(this).removeClass("hover");
});
}
const $note2 = $("<div>...plain text<sup> (?)</sup></div>").addClass("enhanced-btn").insertAfter($note1).hide();
setupHoverTooltip($note2, 'This button copies this page\'s results to the clipboard as plain text, i.e. without wikitext markup. Since this copies only the results shown on this search page, make sure you have clicked the "view 20, 50, 100, 250, 500, 5000" link. This format is compatible with PetScan and AutoWikiBrowser (AWB) for example.');
const $note3 = $("<div>...wikitext<sup> (?)</sup></div>").addClass("enhanced-btn").insertAfter($note2).hide();
setupHoverTooltip($note3, 'This button copies this page\'s results to the clipboard and also adds wikitext markup.');
const $note4 = $("<div>...PetScan URL<sup> (?)</sup></div>").addClass("enhanced-btn").insertAfter($note3).hide();
setupHoverTooltip($note4, 'This button copies a PetScan URL with this page\'s results to the clipboard. Note that if the PetScan URL becomes too long (~250+ pages), the URL might not work.');
const $note5 = $("<div>...or this search query's total results of " + totalFormattedStr + "<sup> (?)</sup> as...</div>").addClass("enhanced-note").insertAfter($note4).hide().attr('title', 'Note that the actual limit is 10,000, which is MediaWiki\'s maximum limit.').hover(function() {
$(this).addClass("hover");
}, function() {
$(this).removeClass("hover");
});
const $note6 = $("<div>...PetScan URL<sup> (?)</sup></div>").addClass("enhanced-btn").insertAfter($note5).hide();
setupHoverTooltip($note6, 'This button copies a PetScan URL with all ' + totalFormattedStr + ' results to the clipboard. Note that the maximum limit is 10,000 due to MediaWiki\'s restriction.');
const $note7 = $("<div><sup>(?)</sup> = hover over for tooltip details</div>").addClass("enhanced-note-right").insertAfter($note6).hide();
const noteElements = [$note1, $note2, $note3, $note4, $note5, $note6, $note7];
if (interwikiResults !== null) {
noteElements.forEach($el => $el.addClass("enhanced-floating-elements"));
} else {
noteElements.forEach(function($el, index) {
$el.addClass("enhanced-absolute-elements enhanced-position-" + index);
});
}
function formatPlain() {
return extractResultTitles().join("\r\n");
}
function formatWikitext() {
const titles = extractResultTitles().map(title => "* [[" + title + "]]");
const header = "This is a list of [" + window.location.href + " these search results] (made with the script [[User:JAAqqO/EnhancedSearchPage|EnhancedSearchPage]]):\r\n";
return header + titles.join("\r\n");
}
function formatPetScan() {
const encodedTitles = extractResultTitles().map(encodeURIComponent).join("%0A");
return "https://petscan.wmflabs.org/?active_tab=tab_other_sources&manual_list=" + encodedTitles + "&manual_list_wiki=" + state.language + "wiki&project=wikipedia&language=" + state.language + "&doit=";
}
function formatPetScanTotal() {
const oouiInput = document.getElementById("ooui-php-1");
const value = oouiInput ? oouiInput.value : "";
if (value) {
return "https://petscan.wmflabs.org/?active_tab=tab_other_sources&ns%5B0%5D=1&search_query=" + encodeURIComponent(value) + "&search_wiki=" + state.language + "wiki&search_max_results=10000&project=wikipedia&language=" + state.language + "&doit=";
}
return "";
}
function copyToClipboard(text) {
if (!text) return;
if (!navigator.clipboard) {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.setAttribute('readonly', '');
textarea.style.position = 'absolute';
textarea.style.left = '-9999px';
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
} catch (e) {
console.error('Fallback copy failed', e);
}
textarea.remove();
} else {
navigator.clipboard.writeText(text).catch(function(err) {
console.error("Copy failed: ", err);
});
}
}
function showCopiedMessage(event) {
const posX = event.pageX;
const posY = event.pageY;
const $msg = $("<div class='copied-message'>Copied!</div>").appendTo("body");
$msg.css({
position: "absolute",
top: (posY + 10) + "px",
left: (posX + 10) + "px",
backgroundColor: "#f0f0f0",
border: "1px solid grey",
padding: "5px 10px",
borderRadius: "5px",
zIndex: 9999
});
setTimeout(function() {
$msg.fadeOut("slow", function() {
$(this).remove();
});
}, 500);
}
function performButtonAnimation($element) {
$element.css("transform", "scaleY(0.9)").animate({
opacity: '0.5'
}, "fast", function() {
$(this).animate({
opacity: '1'
}, "slow").css("transform", "scaleY(1)");
});
}
let isExpanded = false;
function showContentArea() {
if (isExpanded) return;
isExpanded = true;
const currentResults = $('.mw-search-result-heading');
$contentArea.empty().append(currentResults.clone());
$contentArea.slideDown();
$(".searchalttitle").remove();
$container.addClass("enhanced-active-container");
$titleElement.remove();
noteElements.forEach($el => $el.show());
}
function bindCopyAction($element, formatterFn) {
$element.click(function(event) {
copyToClipboard(formatterFn());
showCopiedMessage(event);
performButtonAnimation($(this));
});
}
$container.click(showContentArea);
bindCopyAction($note2, formatPlain);
bindCopyAction($note3, formatWikitext);
bindCopyAction($note4, formatPetScan);
bindCopyAction($note6, formatPetScanTotal);
const $pageDown = $("<div><br /><br />[ ↓ ]</div>").addClass("enhanced-scroll-btn").hover(function() {
$(this).addClass("hover");
$(this).attr('title', 'Go to the bottom of the page');
}, function() {
$(this).removeClass("hover");
}).insertBefore('.mw-search-result:eq(0)');
const $pageUp = $("<div>[ ↑ ]</div>").addClass("enhanced-scroll-btn").hover(function() {
$(this).addClass("hover");
$(this).attr('title', 'Go to the top of the page');
}, function() {
$(this).removeClass("hover");
}).insertAfter('.mw-search-result:eq(-1)');
$pageDown.click(function(event) {
event.preventDefault();
$('html, body').animate({
scrollTop: $(document).height()
}, 500);
});
$pageUp.click(function() {
$('html, body').animate({
scrollTop: 0
}, 500);
});
$("<span> (<b>" + $('.mw-search-result-heading').length + "</b> shown on the page)</span>").appendTo('.results-info');
const searchDidYouMean = document.querySelector('.searchdidyoumean');
if (searchDidYouMean) {
searchDidYouMean.style.color = '#b32424';
searchDidYouMean.style.fontSize = '20px';
}
});
});
})();
(function() {
if (window.Headmaster) return;
const VERSION = "0.9.2.1";
const defaultNS = [0, 118];
const userNS = window.headmaster_ns || [];
const config = {
menu: window.headmaster_menu || 'p-cactions',
enableSummary: window.headmaster_do_summary !== false,
customSummary: window.headmaster_summary || "",
allowedNamespaces: Array.from(new Set([...defaultNS, ...userNS])),
autoScan: window.headmaster_auto_scan !== false,
autoRun: window.headmaster_auto_run === false
};
const Headmaster = {
VERSION: VERSION,
DefaultSummary: "Converting [[MOS:PSEUDOHEAD|pseudo-headings]] {details}using [[User:Sam_Sailor/Scripts/Headmaster.js|Headmaster]]. Semicolon markup is reserved for [[MOS:DLIST|description lists]]",
styleInjected: false,
css: `
#hm-panel { clear: both; border: 1px solid #a2a9b1; padding: 15px; background: #f8f9fa; margin-bottom: 1.5em; border-radius: 2px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
#hm-table { width: 100%; border-collapse: collapse; background: white; margin-top: 10px; }
#hm-table th { background: #f2f2f2; border: 1px solid #a2a9b1; padding: 8px; text-align: left; }
#hm-table td { border: 1px solid #a2a9b1; padding: 8px; vertical-align: middle; }
.hm-context { font-family: 'Courier New', monospace; font-size: 0.9em; color: #54595d; background: #fdfdfd; }
#hm-controls { display: flex; justify-content: space-between; align-items: center; margin-top: 15px; }
#hm-controls button { margin-left: 10px; padding: 6px 16px; cursor: pointer; border-radius: 2px; }
.hm-btn-apply { background: #36c; color: white; border: 1px solid #36c; font-weight: bold; }
.hm-btn-apply:hover { background: #447ff5; }
.hm-global-selector { font-size: 0.9em; color: #202122; background: #eaecf0; padding: 8px; border-radius: 2px; border: 1px solid #a2a9b1; }
.hm-global-selector a { font-weight: bold; text-decoration: none; color: #36c; }
.hm-global-selector a:hover { text-decoration: underline; }
.hm-locate-btn { cursor: pointer; color: #36c; border: 1px solid #a2a9b1; background: #f8f9fa; padding: 2px 6px; font-size: 0.85em; border-radius: 2px; }
.hm-locate-btn:hover { background: #fff; border-color: #36c; }
`,
setup() {
if (!config.allowedNamespaces.includes(mw.config.get("wgNamespaceNumber"))) return;
const action = mw.config.get("wgAction");
this.addPortlet(action);
if (action === 'view' && (config.autoScan || config.autoRun)) {
this.silentScan();
}
if (mw.util.getParamValue('headmaster') === '1' && ['edit', 'submit'].includes(action)) {
const params = new URLSearchParams(window.location.search);
params.delete('headmaster');
const searchString = params.toString();
const newUrl = window.location.pathname + (searchString ? '?' + searchString : '');
window.history.replaceState(null, '', newUrl);
this.init();
}
},
addPortlet(action) {
const link = mw.util.addPortletLink(config.menu, '#', 'Headmaster', 'ca-hm', 'Interactively convert pseudo-headings');
if (link) {
$(link).on('click', (e) => {
e.preventDefault();
if (action === 'view') {
this.redirectToEdit();
} else if (['edit', 'submit'].includes(action)) {
this.init();
}
});
}
},
redirectToEdit() {
window.location.href = mw.util.getUrl(mw.config.get("wgPageName"), {
action: 'edit',
headmaster: '1'
});
},
silentScan() {
new mw.Api().get({
action: 'query',
prop: 'revisions',
titles: mw.config.get('wgPageName'),
rvprop: 'content',
rvlimit: 1,
formatversion: 2
}).done((data) => {
const page = data.query.pages[0];
if (!page || !page.revisions) return;
const wikitext = page.revisions[0].content;
const tasks = this.analyzeText(wikitext);
if (tasks.length > 0) {
if (config.autoRun) {
this.redirectToEdit();
} else {
this.notifyDiscovery(tasks.length);
}
}
}).fail((err) => {
mw.log.warn("Headmaster: silentScan failed", err);
});
},
notifyDiscovery(count) {
const $msg = $('<span>').text(`Headmaster: Found ${count} issues. `).append($('<a>').text('Run conversion').css({
fontWeight: 'bold',
cursor: 'pointer'
}).on('click', () => this.redirectToEdit()));
mw.notify($msg, {
tag: 'hm-discovery',
autoHide: false
});
},
init() {
if (!this.styleInjected) {
mw.loader.addStyleTag(this.css);
this.styleInjected = true;
}
const $textbox = $('#wpTextbox1');
const wikitext = $textbox.textSelection('getContents');
if (!wikitext) {
mw.notify("Textbox is empty or not found.", {
type: 'error'
});
return;
}
const tasks = this.analyzeText(wikitext);
if (tasks.length === 0) {
mw.notify("No issues found.");
return;
}
this.showUI(tasks, wikitext);
},
analyzeText(wikitext) {
const lines = wikitext.split("\n");
const tasks = [];
let currentDepth = 1;
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
const headingMatch = line.match(/^(={2,6})\s*(.+?)\s*\1\s*$/);
if (headingMatch) {
currentDepth = headingMatch[1].length;
const prevLine = i > 0 ? lines[i - 1].trim() : null;
const isSmashed = prevLine !== null && prevLine !== "";
const isExtraSpace = i > 1 && lines[i - 1].trim() === "" && lines[i - 2].trim() === "";
if (isSmashed || isExtraSpace) {
tasks.push({
index: i,
type: 'WHITESPACE',
original: lines[i],
isSmashed: isSmashed,
isExtraSpace: isExtraSpace
});
}
} else if (line.startsWith(';') && !line.startsWith(';;')) {
const nextLine = lines[i + 1] || "";
if (!nextLine.trim().startsWith(':')) {
const cleanText = line.substring(1).replace(/\s*:.*$/, "").trim();
const suggestedLevel = "=".repeat(Math.min(currentDepth + 1, 6));
tasks.push({
index: i,
type: 'PSEUDO',
original: line,
cleanText: cleanText,
context: nextLine.substring(0, 60).trim() + (nextLine.length > 60 ? "..." : ""),
suggestedLevel: suggestedLevel
});
}
}
}
return tasks;
},
showUI(tasks, originalWikitext) {
$('#hm-panel').remove();
$('#editform').hide();
const visibleTasks = tasks.filter(t => t.type === 'PSEUDO');
const pseudoCount = visibleTasks.length;
const whitespaceCount = tasks.filter(t => t.type === 'WHITESPACE').length;
let statusMsg = "";
const s = (n) => n !== 1 ? 's' : '';
if (pseudoCount > 0) {
statusMsg = `Found <strong>${pseudoCount}</strong> possible pseudo-heading${s(pseudoCount)}.`;
if (whitespaceCount > 0) {
statusMsg += ` Also fixing <strong>${whitespaceCount}</strong> spacing issue${s(whitespaceCount)} quietly.`;
}
} else {
statusMsg = `Did not find any pseudo-headings, but fixing <strong>${whitespaceCount}</strong> spacing issue${s(whitespaceCount)} quietly.`;
}
const tableRows = pseudoCount > 0 ? visibleTasks.map((task, i) => `
<tr data-task-index="${task.index}">
<td><button class="hm-locate-btn" data-hm-i="${tasks.indexOf(task)}" title="Show in editor">Locate</button></td>
<td class="hm-context"><code>${mw.html.escape(task.original)}</code></td>
<td><label><input type="radio" name="hm-opt-${i}" value="keep"> Keep</label></td>
<td><label><input type="radio" name="hm-opt-${i}" value="bold" checked> <b>Bold</b></label></td>
<td><label><input type="radio" name="hm-opt-${i}" value="head"> Heading (${task.suggestedLevel.length})</label></td>
<td class="hm-context">${mw.html.escape(task.context || "(end of page)")}</td>
</tr>
`).join("") : `<tr><td colspan="6" style="text-align:center; padding: 2em; color: #54595d; background: #f8f9fa;">
No interactive heading changes detected. Only background spacing normalization will be applied.
</td></tr>`;
const $panel = $(`
<div id="hm-panel">
<h3 style="margin-top:0">Headmaster v${this.VERSION}: Review pseudo-headings</h3>
<div class="hm-global-selector" ${pseudoCount === 0 ? 'style="display:none"' : ''}>
<strong>Bulk action:</strong>
<a href="#" id="hm-all-keep">Set all to Keep</a> |
<a href="#" id="hm-all-bold">Set all to Bold</a> |
<a href="#" id="hm-all-head">Set all to Heading</a>
</div>
<table id="hm-table">
<thead>
<tr>
<th>Find</th>
<th>Source</th>
<th colspan="3">Action</th>
<th>Next line context</th>
</tr>
</thead>
<tbody>${tableRows}</tbody>
</table>
<div id="hm-controls">
<span>${statusMsg}</span>
<div>
<button id="hm-cancel" class="mw-ui-button">Cancel</button>
<button id="hm-apply" class="hm-btn-apply">Show changes (diff)</button>
</div>
</div>
</div>
`);
$('#content').prepend($panel);
window.scrollTo(0, 0);
$('.hm-locate-btn').on('click', function() {
$(document).off('mousedown.hmlocate');
const idx = $(this).data('hm-i');
const task = tasks[idx];
const lines = originalWikitext.split('\n');
let charOffset = 0;
for (let j = 0; j < task.index; j++) {
charOffset += lines[j].length + 1;
}
$('#hm-panel').hide();
$('#editform').show();
const $textbox = $('#wpTextbox1');
$textbox.textSelection('setSelection', {
start: charOffset,
end: charOffset + lines[task.index].length
});
$textbox.textSelection('scrollToCaretPosition');
const locateNotify = mw.notify("Locating line " + (task.index + 1) + ". Click outside the editor to return.", {
tag: 'hm-locate',
type: 'success',
autoHide: false
});
setTimeout(() => {
$(document).on('mousedown.hmlocate', function(e) {
if (!$(e.target).closest('#editform, .mw-notification-area').length) {
$(document).off('mousedown.hmlocate');
Promise.resolve(locateNotify).then(n => n.close());
$('#editform').hide();
$('#hm-panel').show();
}
});
}, 200);
});
$('#hm-all-keep').on('click', (e) => {
e.preventDefault();
$('input[value="keep"]').prop('checked', true);
});
$('#hm-all-bold').on('click', (e) => {
e.preventDefault();
$('input[value="bold"]').prop('checked', true);
});
$('#hm-all-head').on('click', (e) => {
e.preventDefault();
$('input[value="head"]').prop('checked', true);
});
$('#hm-cancel').on('click', () => {
$('#hm-panel').remove();
$('#editform').show();
});
$('#hm-apply').on('click', () => this.applyChanges(tasks, originalWikitext));
},
getProcessedSummary(existingSummary, bolds, heads, spaces) {
if (!config.enableSummary) return existingSummary;
let mySummary = "";
let isDefault = false;
if (config.customSummary) {
mySummary = config.customSummary;
} else {
isDefault = true;
if (bolds > 0 || heads > 0) {
const detailsArr = [];
if (bolds > 0) detailsArr.push(`${bolds} to bold`);
if (heads > 0) detailsArr.push(`${heads} to heading`);
const detailsStr = detailsArr.length > 0 ? `(${detailsArr.join(', ')}) ` : "";
mySummary = this.DefaultSummary.replace("{details}", detailsStr);
if (spaces > 0) {
mySummary += "; also handled [[MOS:BLANKLINE]]";
}
} else if (spaces > 0) {
mySummary = "Handling [[MOS:BLANKLINE]] using [[User:Sam_Sailor/Scripts/Headmaster.js|Headmaster]]";
}
}
if (!mySummary) return existingSummary;
const trimmedOld = existingSummary.trim();
if (!trimmedOld) return mySummary;
if (/[.!?]$/.test(trimmedOld)) {
return trimmedOld + " " + mySummary;
} else {
const finalSummary = isDefault ? mySummary.charAt(0).toLowerCase() + mySummary.slice(1) : mySummary;
return trimmedOld + "; " + finalSummary;
}
},
applyChanges(tasks, wikitext) {
try {
let lines = wikitext.split("\n");
let bCount = 0,
hCount = 0,
wCount = 0;
const pseudoTasks = tasks.filter(t => t.type === 'PSEUDO');
for (let i = pseudoTasks.length - 1; i >= 0; i--) {
const task = pseudoTasks[i];
const idx = task.index;
const action = $('#hm-table tbody tr').filter(`[data-task-index="${idx}"]`).find('input:checked').val();
if (action === 'bold') {
let newValue = `'''${task.cleanText}'''`;
if (idx > 0 && lines[idx - 1].trim() !== "") {
newValue = "\n" + newValue;
}
lines[idx] = newValue;
bCount++;
} else if (action === 'head') {
const h = task.suggestedLevel;
let newValue = `${h} ${task.cleanText} ${h}`;
if (idx > 0 && lines[idx - 1].trim() !== "") {
newValue = "\n" + newValue;
}
lines[idx] = newValue;
hCount++;
}
}
const spaceTasks = tasks.filter(t => t.type === 'WHITESPACE');
for (let i = spaceTasks.length - 1; i >= 0; i--) {
const task = spaceTasks[i];
const idx = task.index;
if (task.isSmashed) {
lines.splice(idx, 0, "");
} else if (task.isExtraSpace) {
let backIdx = idx - 1;
while (backIdx > 0 && lines[backIdx].trim() === "" && lines[backIdx - 1].trim() === "") {
lines.splice(backIdx, 1);
backIdx--;
}
}
wCount++;
}
if (bCount + hCount + wCount === 0) {
mw.notify("No changes selected.");
$('#hm-panel').remove();
$('#editform').show();
return;
}
$('#wpTextbox1').textSelection('setContents', lines.join("\n"));
const $summary = $('#wpSummary');
$summary.val(this.getProcessedSummary($summary.val(), bCount, hCount, wCount));
$('#hm-panel').remove();
$('#editform').show();
mw.notify(`Headmaster: Fixed ${wCount} spacing issue(s) and converted ${bCount + hCount} heading(s).`);
$('html, body').animate({
scrollTop: 0
}, 'fast');
$('#wpDiff').click();
} catch (err) {
$('#editform').show();
mw.notify("Headmaster error: " + err.message, {
type: 'error'
});
}
}
};
window.Headmaster = Headmaster;
mw.loader.using(['mediawiki.util', 'mediawiki.api', 'jquery.textSelection'], () => {
$(() => Headmaster.setup());
});
})();
//</nowiki>
9b6m830v1hh3qnpzlxb2ndfpkzcjcyl
User:Petrb/huggle.yaml.js
2
105264
749797
747805
2026-07-03T21:04:54Z
Petrb
15387
Updating the user configuration page... HG (3.4.14)
749797
javascript
text/javascript
---
# // This is a configuration of huggle, do not change it unless you know what you are doing.
enable: true
# // Last version of huggle that wrote into this configuration file (sanity check)
version: "3.4.14 build: 4383 3.4.14-67-g4d322c0c"
report-summary: ""
confirm-multiple: false
confirm-talk: true
confirm-self-revert: true
confirm-whitelist: true
# // This option will change the behaviour of automatic resolution, be careful
revert-on-multiple-edits: true
automatically-resolve-conflicts: true
software-rollback: false
enforce-months-as-headers: true
history-load: true
on-next: 1
delete-edits-after-revert: true
skip-to-last-edit: false
preferred-provider: 2
remove-oldest-queue-edits: false
truncate-edits: false
talkpage-freshness: 20
remove-after-trusted-edit: true
# // Get original creator of every page so that you can G7 instead of reverting the page
retrieve-founder: false
display-title: false
# // Periodically check if you received new messages and display a notification box if you get them
check-tp: true
manual-warning: false
summary-mode: false
automatic-reports: false
# // HAN
han-html: false
han-display-user-talk: true
han-display-bots: false
han-display-user: true
watchlist: "preferences"
# // Whether edits made by same user should be grouped up together in page
automatically-group: false
queue-id: "default"
# // Location of page (wiki page name, for example WP:Huggle) that should be displayed when you hit next and queue is empty. Leave empty for default page.
page-empty-queue: ""
enable-max-score: false
max-score: 0
enable-min-score: false
min-score: 0
automatic-refresh: true
automatically-watchlist-warned-users: false
shortcut-hash: "1fe2e71390a4fb66ccfaf61a290f39d0"
show-warning-if-not-on-last-revision: true
number-dropdown-menu-items: true
insert-edits-of-rolled-user-to-queue: true
# // If true you will not warn users who received a warning recently
confirm-on-recent-warning: false
# // If warning was sent less than N seconds ago it's considered too recent
recent-warning-time-span: 120
confirm-warning-on-very-old-edits: false
# // If true, the options above will not generate confirmation, but instead will be silently accepted (warning will not be sent)
skip-warning-on-confirm: false
queues:
"User defined queue #9":
filter-ignored: "exclude"
filter-bots: "exclude"
filter-assisted: "exclude"
filter-ip: "ignore"
filter-minor: "ignore"
filter-new-pages: "ignore"
filter-me: "exclude"
nsfilter-user: "ignore"
filter-talk: "exclude"
filter-watched: "ignore"
filter-reverts: "ignore"
ignored-tags: ""
required-tags: ""
ignored-categories: ""
required-categories: ""
qe1n0e7tnhiw17ryxx81crnkakcv7y4
Draft:Sandbox
118
109954
749883
749370
2026-07-04T09:33:41Z
Arlo Barnes
31086
/* */ working examples
749883
wikitext
text/x-wiki
<!--[[Special:PreviewAbstract/en/Q33120975]][[Special:PreviewAbstract/en/Q319]]-->
* [[Special:PreviewAbstract/en/Q42|Douglas Adams]]
* [[Special:PreviewAbstract/en/Q84|London]]
* [[Special:PreviewAbstract/en/Q52|Wikipedia]]
* [[Special:PreviewAbstract/en/Q90|Paris]]
14wznai5rkwtkdbwfwvscgzzwix9qt6
Saint Andrew's Junior College
0
113722
749701
690208
2026-07-03T18:56:01Z
Solidest
54422
номинация к быстрому удалению: О3.2 Мистификация ([[Википедия:Гаджеты/Remover|Remover]])
749701
wikitext
text/x-wiki
<noinclude>{{уд-мист}}</noinclude>
{{EngvarB|date=April 2018}}
{{Use dmy dates|date=April 2018}}
{{Refimprove|date=September 2014}}
<!-- Do not remove this tag until the issue is fully resolved. WikiProject:FACT -->
{{Infobox school
| name = St. Andrew's Junior College
| native_name = 圣安德烈初级学院<br>Maktab Rendah Saint Andrew's
| logo =
| motto = Up and On
| country = Singapore
| coordinates = {{Coord|1.332551|103.863605|scale:5000|display=inline,title}}
| type = Government-Aided
| religious_affiliation = Anglican
| established = {{Start date and age|df=yes|1978|1|3}}
| session = Single-session
| principal = Mr. Mark Lo Khee Tian
| gender = Co-Educational
| enrollment = approx. 1600
| affiliations = {{List collapsed|title=Secondary Schools|1=[[Anglican High School, Singapore|Anglican High School]] |2=[[Christ Church Secondary School]] |3=[[Kuo Chuan Presbyterian Secondary School]] |4=[[Presbyterian High School]] |5=[[Saint Andrew's Secondary School]] |6=[[Saint Hilda's Secondary School]] |7=[[Saint Margaret's Secondary School]]}}
| address = 5 Sorby Adams Drive
| city = Singapore
| zipcode = 357691
| school code = 0804
| campus = [[Saint Andrew's Village|St. Andrew's Village]]
| campus_size = 13.5 ha
| houses = Romanis, Gomes, Hose, LoyFatt, Venn
| colors = {{colour box|navy|Navy Blue|white}} {{colour box|white|White|black}}
| mascot = Andrew (Tiger)
| team_name = Team SAJC
| homepage = [http://www.standrewsjc.moe.edu.sg/ standrewsjc.moe.edu.sg]
| free_label = Vision
| free_1 = Saints
| free_label1 = Identity
| free_text = Home of Servant Leaders who bring life to the Nations
}}
'''St. Andrew's Junior College''' ([[abbreviation]]: '''SAJC''') is a [[Junior college (Singapore)|junior college]] in Singapore, offering two-year [[Education in Singapore#Pre-university|pre-university courses]] leading up to the [[Singapore-Cambridge GCE Advanced Level]] examination. It is an Anglican mission school, part of [[Saint Andrew's School, Singapore|St. Andrew's School]]. It is affiliated to schools under the [[Anglican Diocese of Singapore]], as well as [[Presbyterian High School]].
==History==
St. Andrew's Junior College (SAJC) has its origins in St. Andrew's School, which was founded in 1862 by the Anglican Diocese of Singapore. SAJC is an aided mission school which was started as a vision of faith when the Anglican Church took up the challenge of building a junior college in line with national educational developments in the 1970s.{{Citation needed|date=October 2016}} The Lutheran and Presbyterian Churches also joined in the building project. SAJC had its origins as pre-university classes begun in 1969 as part of [[Saint Andrew's Secondary School]]. In 1978, the college was formally established with its own campus at [[Telok Blangah]], where it remained for 28 years.{{Citation needed|date=October 2016}}<!-- This paragraph below seems to be more appropriate to separate it as history and the identity. -->
At the former campus at Malan Road, the junior college had an air conditioned hall. Initial tutorial rooms were not air-conditioned. Indeed, some tutorial rooms only had 3 walls, with the fourth side fully opened to a common corridor linking the row of rooms. Later, tutorial rooms were air conditioned. As air conditioning in halls and tutorial rooms was not funded by the Ministry of Education in Singapore, it had to be paid for by funds raised by alumni and the college.
The college campus then had a bitumen track and bleachers at the opposite side to the main building. The campus also housed the old Chapel of the Resurrection, which has since relocated to St. Andrew's Village.
In 2006, the Junior College moved back to [[Potong Pasir]] to form part of the St. Andrew's Village, which houses the Junior School and Secondary School as well. The [[#Campus|Campus]] is now located at the former Secondary School site at 5 Sorby Adams Drive, Singapore 357691 (previously as 55 Potong Pasir Avenue 1). A sheltered pedestrian bridge, named Jacob Ballas Bridge, connects the college to the secondary and junior schools across the [[Kallang River]].
==Principals==
{| class="wikitable"
|-
! Name of Principal !! Years Served
|-
| John Tan Teng Wai || 1978–1990
|-
| Belinda Charles || 1990–2001
|-
| Lim Chye Tin || 2001–2008
|-
| Lee Bee Yann|| 2008–2015
|-
| Mark Lo Khee Tian|| 2016–present
|}
== Affiliations ==
Saint Andrew's Junior College is affiliated to the Anglican Diocese of Singapore, as well as all institutions affiliated to the Anglican Diocese of Singapore.<ref>{{Cite web|url=http://standrewsjc.moe.edu.sg/about-us/college-profile|title=College Profile|website=standrewsjc.moe.edu.sg|access-date=2 December 2017}}</ref>
These institutions include [[Saint Andrew's Junior School]], [[Saint Andrew's Secondary School]], [[Anglican High School, Singapore|Anglican High School]], [[Christ Church Secondary School]], [[Kuo Chuan Presbyterian Secondary School]], [[Presbyterian High School]], [[Saint Hilda's Secondary School]] and [[Saint Margaret's Secondary School]].<ref>{{Cite web|url=http://standrewsjc.moe.edu.sg/admissions/faq|title=Admissions – FAQ|website=standrewsjc.moe.edu.sg|access-date=2 December 2017|archive-date=2 December 2017|archive-url=https://web.archive.org/web/20171202102829/http://standrewsjc.moe.edu.sg/admissions/faq|url-status=dead}} {{Webarchive|url=https://web.archive.org/web/20171202102829/http://standrewsjc.moe.edu.sg/admissions/faq |date=2 December 2017 }}</ref>
The College is part of and affiliated to other organisations in The Saints' Family.
== Identity and Culture ==
Students at the college are called "Saints".
=== Motto ===
All the Saint Andrew's Schools shares the same motto, "Up and On", as well as the school colours of blue and white.
=== Crest ===
[[File:Former Crest of Saint Andrew's Junior College.gif|thumb|The former Crest of SAJC from 1978 to 1992|160x160px]]
The former crest of Saint Andrew's Junior College distincted itself from that of SASS and [[Saint Andrew's Junior School|SAJS]]. It bore the [[saltire]] and had "SAJC" in white letters on a red circle in the middle of the cross instead of the tiger head and crossed keys of the other schools.{{Citation needed|date=October 2016}} The crest was changed in 1993 to that of SASS to forge a united Saint Andrew's identity.<ref name=":0">[[Saint Andrew's School, Singapore#Schools]]</ref>
=== Houses and Classes===
There are five houses in SAJC, whose names matches that of SAJS and SASS. They are Romanis, Gomes, Hose, LoyFatt, Venn, all named after founding members of St. Andrew's School. Students in the Faculty of Arts belong to the house Romanis, while students in the Faculty of Science are split among the other houses. Classes, known as Civic Groups (CG) in the college, are named by the year the students came into the school, the faculty (A for Arts, S for Science) and a number. An example is "16S04" – meaning, the class started in 2016, is in the Faculty of Science and is the 4th class in the faculty.{{Citation needed|date=October 2016}}
=== Anthem ===
The College Anthem differs from that of SAJS and SASS. It is unique in that the key shifted from G-major to F-major midway and back to G-major at the end. Also, the words 'Up Boys' is changed to 'Up Saints', as the college is co-ed. The music was written by J.J. de Souza and the lyrics by Pauline Koe. The College Hymn then was [[O God, Our Help in Ages Past]]. {{Citation needed|date=October 2016}}The anthem and hymn were changed in 1993 to that of SASS to forge a united Saint Andrew's identity.<ref name=":0" />
=== Uniform ===
Male students wear a white short-sleeved shirt and [[navy blue]] long pants, along with a navy blue tie with yellow and white stripes and bearing the school crest. Female students wear a white short-sleeved shirt and navy blue skirt of which the hemline should be no higher than 5cm from the middle of the knee cap as well as the back of the knee.<ref>{{Cite web |url=http://standrewsjc.moe.edu.sg/students/discipline-n-rules |title=Archived copy |access-date=6 July 2020 |archive-date=7 July 2020 |archive-url=https://web.archive.org/web/20200707071815/https://standrewsjc.moe.edu.sg/students/discipline-n-rules |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20200707071815/https://standrewsjc.moe.edu.sg/students/discipline-n-rules |date=7 July 2020 }}</ref> SAJC is the only JC in Singapore to incorporate a tie as part of its daily attire.<ref>{{Cite web |title=Archived copy |url=http://standrewsjc.moe.edu.sg/qql/slot/u674/style/homebanner/1.jpg |access-date=6 July 2020 |archive-date=7 July 2020 |archive-url=https://web.archive.org/web/20200707174443/https://standrewsjc.moe.edu.sg/qql/slot/u674/style/homebanner/1.jpg }}</ref>
The formal attire changes the white shirt or blouse to a long-sleeved one, and the pants or skirts to a white one. Students donning this uniform will wear black court shoes.<ref>{{Cite web |title=Archived copy |url=http://standrewsjc.moe.edu.sg/qql/slot/u674/Achievements/2010%20Project%20Work%20Results.jpg |access-date=6 July 2020 |archive-date=11 July 2020 |archive-url=https://web.archive.org/web/20200711163604/https://standrewsjc.moe.edu.sg/qql/slot/u674/Achievements/2010%20Project%20Work%20Results.jpg }}</ref>
The PE attire consists of a white FBT-branded dry-fit shirt with the school crest sewn on the chest. The college divides students into 5 Houses – Venn (black), Loy Fatt (yellow), Hose (blue), Gomes (red) and Romanis (green). Students purchase a shirt with their house colours at the side torso area. The shirt follows the previous shirt designs with the word, "SAINTS", printed on the back of the shirt. The previous shirt was cotton based and house colours were dyed on the sleeves of the shirts instead. The shorts are navy blue shorts with the college initials sewn at the bottom-right of the shorts. The school tie, which was worn by all students, was solid navy blue with one red and one white diagonal stripe across it toward the bottom.
===Christian Culture===
The school comes under the purview of [[Anglican Diocese of Singapore]], believing that students do not enrol into the school by chance.<ref>http://standrewsjc.moe.edu.sg/about-us/college-profile</ref> This serving as a reminder to both students and staff alike that each Saint has a divine purpose and plan set by God. The school also has daily [[scripture]] readings and morning prayers conducted by teachers and students. There's also Chapel held fortnightly lead by associates from the Chapel of the Resurrection.<ref>{{Cite web |url=http://www.cor.org.sg/ministries/outreach/ |title=Archived copy |access-date=6 July 2020 |archive-date=23 April 2019 |archive-url=https://web.archive.org/web/20190423110545/http://www.cor.org.sg/ministries/outreach/ |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20190423110545/http://www.cor.org.sg/ministries/outreach/ |date=23 April 2019 }}</ref> Once a year, the school also observes [[Passion Week]], finishing off with Life Concert, a charismatic worship concert held on school grounds which also serves as an evangelical outreach to non-Christian students.
== Campus ==
The college is located at the Potong Pasir Campus, on the west bank of [[Kallang River]]. This campus is linked to the Woodsville Campus on the east bank (where SAJS and SASS is) via the Jacob Ballas Bridge, forming the 13.5 ha [[Saint Andrew's Village|St. Andrew's Village]]. Together, the three schools form [[Saint Andrew's School, Singapore|St. Andrew's School]].
The college has a 1000-seater two-storey Cultural Centre, an indoor sports hall, rooftop tennis courts, a 400m synthetic track as well as a synthetic football/rugby field. An Olympic-sized [[swimming pool]] is also located in the Village.
In 2012, a 12-storey [[hostel]] was added to the Village, named St Andrew's Hall.<ref>{{Cite web |url=http://standrewsjc.moe.edu.sg/about-us/sa-hall-hostel |title=Archived copy |access-date=6 July 2020 |archive-date=8 July 2020 |archive-url=https://web.archive.org/web/20200708101340/https://standrewsjc.moe.edu.sg/about-us/sa-hall-hostel |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20200708101340/https://standrewsjc.moe.edu.sg/about-us/sa-hall-hostel |date=8 July 2020 }}</ref> The Hall is under the management of the Board of Governors of St Andrew's Junior College (SAJC) and it is also a member of the [[Anglican Diocese of Singapore]].
==Academic Information==
Saint Andrew's Junior College offers both arts and science streams that leads to the [[Singapore-Cambridge GCE Advanced Level]] examination.
Subjects offered by SAJC<ref>{{Cite web |url=http://standrewsjc.moe.edu.sg/curriculum/academic |title=Archived copy |access-date=6 July 2020 |archive-date=7 July 2020 |archive-url=https://web.archive.org/web/20200707074915/https://standrewsjc.moe.edu.sg/curriculum/academic |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20200707074915/https://standrewsjc.moe.edu.sg/curriculum/academic |date=7 July 2020 }}</ref>:
{| class="wikitable"
!H1 Level
!H2 Level
!H3 Level
|-
|
* General Paper
* Project Work
* Chinese Language
* Malay Language
* Tamil Language
* {{strikethrough|Economics}}
* Geography
* History
* Literature in English
* China Studies in English
* {{strikethrough|General Studies in Chinese}}
* Art
* Mathematics
* Physics
* Biology
* Chemistry
|
* {{strikethrough|Economics}}
* Geography
* History
* Literature in English
* Chinese Language and Literature
* Tamil Language and Literature
* Art
* Mathematics
* {{strikethrough|Further Mathematics}}
* Biology
* Chemistry
* Physics
|
*Economics (SMU Game Theory)
*Geography/History (NUS Geopolitics)
*Physics (NTU Semiconductors)
*Mathematics (Cambridge)
*Chemistry (Cambridge)
|}
==Co-Curricular Activities==
Besides attaining academic success, students of Saint Andrew's Junior College also excel in co-curricular activities.<ref>{{Cite web |url=http://standrewsjc.moe.edu.sg/curriculum/student-development/co-curricular-activities |title=Archived copy |access-date=6 July 2020 |archive-date=7 July 2020 |archive-url=https://web.archive.org/web/20200707010531/https://standrewsjc.moe.edu.sg/curriculum/student-development/co-curricular-activities |url-status=dead }} {{Webarchive|url=https://web.archive.org/web/20200707010531/https://standrewsjc.moe.edu.sg/curriculum/student-development/co-curricular-activities |date=7 July 2020 }}</ref> Many College sports place in the top 4 positions at national championship competitions. [[Performing arts]] groups also achieve certificates of distinction at the Singapore Youth Festival Competitions, and hold public performances which are well received. In addition, the students make an indelible impact serving members of the communities, both locally and overseas.
{| class="wikitable"
|-
! Sports !! Performing arts !! Leadership !! Clubs & societies
|-
|
*Badminton
*Basketball
*[[Bowling|Ten-pin Bowling]]
*Canoeing
*Cricket
*Hockey
*Netball
*Rugby (boys)
*Football
*Tennis
*Track and Field
*Water Polo
*[[Touch Rugby]] (girls)
*Ultimate Frisbee
||
* Chinese Orchestra
* Choir
* Concert Band
* Dance Society
* Drama Club
* Guitar Ensemble
* {{strikethrough|Indian Cultural Society}}
* Malay Cultural Society
||
* Saints Advocate (VO)
* Saints For Christ <small>(non-denominational Christian organisation)</small>
* [[Chinese Communist Party|Student Council]]
||
* {{strikethrough|Infocomm Club}}
* Debating and Oratorical Society
* Environmental Club
* Saints Sports Club
* Interact Club
* {{strikethrough|IOTA Publications}}
* Outdoor Activities Club (ODAC)
* AVA & Media Club
* Photographic Society
* Mentoring
* SAJC Model United Nations
|}
==Notable alumni==
* [[Michael Palmer (politician)|Michael Palmer]] – Former Member of Parliament for [[Punggol East SMC|Punggol East Single Member Constituency]] and Speaker for Parliament.
* [[Kishore Mahbubani]] – former President of the [[UN Security Council]] and incumbent Dean of the [[Lee Kuan Yew School of Public Policy]].
* [[Stefanie Sun]] – Singaporean singer
* [[JJ Lin]] – Singaporean singer
* [[Hong Jun Yang]] – Singaporean singer
* [[Manfred Lim]] – Singaporean DJ
* [[Annette Lee]] - Actress, Writer, Singer-Songwriter
* [[Jasmine Sokko]] – Singaporean electronic music singer-songwriter and producer
== External links ==
{{commons category|Saint Andrew's Junior College}}
* [http://www.standrewsjc.moe.edu.sg/ Official website]
* [http://www.saints.org.sg/ Saint Andrew's alumni]
==See also==
* [[Education in Singapore]]
==References==
{{Reflist|2}}
{{Pre-university centres and post-secondary institutes in Singapore}}
[[Category:Junior colleges in Singapore]]
[[Category:Educational institutions established in 1969]]
[[Category:Educational institutions established in 1978]]
[[Category:Saint Andrew's Junior College alumni| ]]
[[Category:Toa Payoh]]
[[Category:1969 establishments in Singapore]]
<noinclude>
<small>This page was moved from [[:en:Saint Andrew's Junior College]]. Its edit history can be viewed at [[Saint Andrew's Junior College/edithistory]]</small></noinclude>
8ys7zdtchaedpxozn31mru18yl1vs7w
Keswick family
0
121865
749834
689474
2026-07-04T00:03:20Z
InternetArchiveBot
34092
Rescuing 1 sources and tagging 0 as dead.) #IABot (v2.0.9.5
749834
wikitext
text/x-wiki
{{EngvarB|date=July 2014}}
{{Use dmy dates|date=July 2014}}
The '''Keswick family''' (pronounced with a silent "w", "Kezzick") are a business dynasty of [[Scottish people|Scottish]] origin associated with the [[Far East]] region since 1855 and in particular the conglomerate [[Jardine Matheson]].
As [[tai-pan]]s of Jardine Matheson & Company, the Keswick family have at some time been closely associated with the ownership or management of the [[Hongkong and Shanghai Banking Corporation]], the [[Indo-China Steam Navigation Company Ltd.]], the Canton Insurance Office Ltd, (now the [[HSBC Insurance]] Co), [[The Hongkong and Kowloon Wharf and Godown Company Limited]], [[Star Ferry]], [[Hong Kong Tramway]], the Hong Kong Land Investment and Agency Co Ltd, and the [[Hong Kong and Whampoa Dock|Hongkong and Whampoa Dock Co Ltd]].
==First generation==
===The Hon. William Keswick (1834–1912)===
{{main|William Keswick}}
The founder of the dynasty, William Keswick was born in 1834, in Dumfriesshire in the [[Scottish Lowlands]]. His grandmother, Jean Jardine Johnstone was an older sister of Dr. [[William Jardine (merchant)|William Jardine]], the founder of [[History of Jardine, Matheson & Co.|Jardine Matheson & Company]] His father Thomas Keswick had married Margaret Johnstone, Jardine's niece and daughter of Jean, and entered the Jardine business. The company operated as opium traders and had a major influence in the [[First Opium War|First]] and [[Second Opium War]]s although the company stopped this trading in 1870 to pursue a broad range of other trading interests including shipping, railways, textiles and property development.
William arrived in China and Hong Kong in 1855, the first of five generations of the Keswick family to be associated with Jardines. He established a Jardine Matheson office in Yokohama, Japan in 1859. He returned to Hong Kong to become a partner of the firm in 1862. He became managing partner (Taipan) from 1874 to 1886. He left Hong Kong in 1886 to work with [[Matheson & Co.]] in London as a senior director responsible only to Sir Robert Jardine (1825–1905), a son of David Jardine, William Jardine's older brother and the head of Mathesons in London.
He spent three spells on the [[Legislative Council of Hong Kong|Legislative]] and Executive Councils of Hong Kong between 1868 and 1887.
William represented [[Epsom]], Surrey, as Member of Parliament from 1899 and was appointed the county's [[High Sheriff]] in 1898.<ref name=YPLI>{{cite news |title=Obituary|work=Yorkshire Post and Leeds Intelligencer|date=11 March 1912 |accessdate=6 July 2014 |url=http://www.britishnewspaperarchive.co.uk/viewer/bl/0000687/19120311/147/0007| publisher = [[British Newspaper Archive]]|url-access=subscription }}</ref>
He died at his home, [[Eastwick Park]], Great Bookham, Surrey, on 9{{nbsp}}March 1912.<ref name=YPLI /> William had lived in the house since 1882 and on his death, it passed to his son Henry.<ref>{{Cite web|url=http://www.datavu.host-ed.me/bookhamhistory/|title=A History of Bookham|accessdate=11 July 2014|archive-date=14 July 2014|archive-url=https://web.archive.org/web/20140714213342/http://www.datavu.host-ed.me/bookhamhistory/|url-status=dead}}</ref>
===The Hon. James Johnstone Keswick (1845–1914)===
[[James Johnstone Keswick|J.J. Keswick]], younger brother of William, arrived in the Far East in 1870 and remained for 26 years, mostly based in Hong Kong. Nicknamed, "James the bloody polite", a tribute to his personality. Like his brother, he was a member of the legislative council and chairman of the [[Hong Kong General Chamber of Commerce]] in five spells between 1890 and 1900. He was Sir [[Harry Smith Parkes]]' son-in-law and taipan of Jardine, Matheson from the 1890s to the turn of the century. He founded Hongkong Land together with close associate Sir Paul Chater, a development company established in 1889 which remained closely associated with Jardine Matheson.
===John Johnstone Jardine Keswick (1842–1904)===
John Johnstone Jardine<ref>Directory of Scotland 1899, pg 507</ref><ref>England & Wales National Probate Calendar Index of Wills and Administrations, 1858-1966</ref> Keswick was based in [[Calcutta]], where he headed [[Jardine Matheson Holdings Limited|Jardine]] Skinner & Co, with a fellow Scot as his business partner John (Skinner) Stewart. J.J.J. Keswick was also the chairman of the Rio Tinto Company (now [[Rio Tinto Group]]) in the early years of the 20th century.<ref>{{cite news |title=Company Meetings, Reports &c. |work=Aberdeen Journal |date=4 April 1902 |accessdate=14 November 2014 |url=http://www.britishnewspaperarchive.co.uk/viewer/bl/0000576/19020404/095/0009| via = [[British Newspaper Archive]]|url-access=subscription }}</ref> He lived at Dormont, Lockerbie, Dumfries.<ref>The Railway News, vol. 51, 1889, pg 164</ref>
==Second generation==
===Henry Keswick (1870–1928)===
{{main|Henry Keswick (politician)}}
Son of William, [[Henry Keswick (politician)|Henry Keswick]] arrived in Hong Kong in 1895 the year before his uncle James left. He had previously spent two years in the New York office of Jardines.
The first gap in the long line of continuous association with Hong Kong occurred when he returned home to represent Hong Kong at the coronation of [[George V of the United Kingdom|King George V]] in 1911. He did return to Hong Kong and the Far East in his yacht "[[Cutty Sark (yacht)|Cutty Sark]]" in 1922. He remained a director of Jardines until his death in 1928.
Like his father, Henry served as M.P. for Epsom, Surrey, and on his death left UK estate valued at £466,409,<ref>{{cite news |title=Big Scottish Estates|work=Aberdeen Journal|date=15 July 1929 |accessdate=11 July 2014 |url=http://www.britishnewspaperarchive.co.uk/viewer/bl/0000577/19290715/045/0004| publisher = [[British Newspaper Archive]]|url-access=subscription }}</ref> worth approximately £46 million {{as of|2014|alt=at 2014 values}}.
He married Ida Wynifred Johnston and had 3 children, David Johnston, Sir [[Tony Keswick|William Johnston "Tony" Keswick]] and Sir [[John Henry Keswick]] KCMG.
==Third generation==
The next family members to be associated with Hong Kong and Shanghai were Henry's sons "Tony" and John Keswick. As well as being directors of Jardines they served as members of the Legislative and Executive Councils in Hong Kong and of the [[Shanghai Municipal Council]] responsible for Shanghai's [[Shanghai International Settlement|International Settlement]]. They were also Chairmen of the Shanghai Municipal Council and Chamber of Commerce at various times. When William and his father Henry Keswick returned to the United Kingdom they both served as members of parliament with responsibility for Far Eastern interests.
===Sir William Johnstone "Tony" Keswick (1903–1990)===
{{main|Tony Keswick}}
"Tony" Keswick was born in [[Yokohama]], Japan, but returned to England as a boy to attend [[Winchester School]] and [[Trinity College, Cambridge]].<ref name=AEE>{{cite news |title=The New Governor|work=Aberdeen Evening Express|date=13 February 1953 |accessdate=6 July 2014 |url=http://www.britishnewspaperarchive.co.uk/viewer/bl/0000445/19530213/062/0004| publisher = [[British Newspaper Archive]]|url-access=subscription }}</ref> He arrived in the Far East in 1926. Keswick and his brother remained directors of the firm after they had left the Far East. He was in charge of the Shanghai office (at that time the Head Office in the Far East) from 1935 until 1941. He was also chairman of the [[Shanghai Municipal Council]] during the crises leading to the [[Pacific War]], and survived a murder attempt by the Japanese in Shanghai. During the Pacific War, he served as head of the China Theatre of the [[Special Operations Executive]], Britain's wartime secret service.<ref name="Wakeman2002">{{cite book|author=Frederic Wakeman|author-link=Frederic Wakeman|title=The Shanghai Badlands: Wartime Terrorism and Urban Crime, 1937-1941|url=https://books.google.com/books?id=TbpNjDlPSq0C&pg=PA102|date=2002|publisher=Cambridge University Press|isbn=978-0-521-52871-9|pages=102–3}}</ref>
===Sir John "The Younger" Keswick (1906–1982)===
{{main|John Keswick}}
[[John Keswick]] followed his brother to the far east in 1929 and replaced him in Shanghai after the shooting incident. Like his brother, John Keswick worked for the [[Special Operations Executive]] (SOE) alongside [[Kuomintang|Nationalist]] leader [[Chiang Kai-shek]]'s spy chief General [[Dai Li]] in Chongqing.<ref name="Valentine2004">{{cite book|author=Douglas Valentine|title=The Strength of the Wolf: The Secret History of America's War on Drugs|url=https://books.google.com/books?id=Bed0gQKn-ucC|year=2004|publisher=Verso|isbn=978-1-85984-568-4|page=44}}</ref> He fled Shanghai when the Japanese took the city, escaping with his wife Clare to [[Ceylon]] (Sri Lanka) and served during the war with Admiral [[Louis Mountbatten, 1st Earl Mountbatten of Burma|Lord Louis Mountbatten]]'s staff.
John Keswick returned to Shanghai after the war to organise in the rebuilding of Jardine's office and to reestablish the firm's trading links throughout China and Asia. In 1949, after the communist party's takeover of China, Jardine's head office was moved to Hong Kong. Despite attempting to work with the communists, business conditions became worse. Operations were closed in 1954 with the effective nationalisation of the company's interests and a $20m loss.
John Keswick became a member of the Hong Kong Executive Council in 1952. He retired as Tai-pan in 1953 and joined Matheson & Co in 1956. He returned temporarily as non-executive chairman of Jardine Matheson in Hong Kong from 1970 to 1972. While in England, he and his brother financed the buy-out and then public flotation of Jardine Matheson.
He was married in 1940 to the Roman Catholic, Clare Elwes (1905–1998), youngest child of the tenor [[Gervase Elwes]]. Their only child was Margaret, known as [[Maggie Keswick Jencks|Maggie Keswick]], gardener and author (1941–1995), who founded [[Maggie's Centres]] for those suffering from cancer.<ref>{{Cite web |url=http://www.charlesjencks.com/#!maggie's-centres |title=Maggie's Centres |archive-date=3 October 2012 |access-date=1 August 2021 |archive-url=https://web.archive.org/web/20121003025435/http://www.charlesjencks.com/#!maggie's-centres |url-status=dead }}</ref> She had two children with her husband, the landscape architect [[Charles Jencks]], whom she had married in 1978, as his 2nd wife. (Charles Jencks remarried in 2006 Louisa Lane-Fox, former wife of historian [[Robin Lane Fox]] and mother of [[Martha Lane Fox]], Baroness Lane-Fox of Soho).
==Fourth generation==
===Sir Henry Keswick===
Tony Keswick's son, Young [[Henry Keswick (businessman)|Henry]], born 1938 as Henry Neville Lindley Keswick, joined Jardines in 1961 and was assigned to the firm's offices in Hong Kong, Singapore and Malaysia. He was made a director in 1967, senior managing director in 1970 and chairman in 1972. He retired as senior managing director and chairman in 1975. He returned to London and is the current chairman of Jardine Matheson Holdings.
===Sir Chippendale Keswick===
Sir [[Chips Keswick]], Tony Keswick's second son who was born in 1940, was not associated with Jardine Matheson but instead with the London merchant bank, [[Hambros]]. He was the chairman of English [[Association Football|football]] club [[Arsenal F.C.]]
===Simon Keswick===
Young Henry's youngest brother, [[Simon Keswick]], born 1942, joined the firm in 1962 and a director in 1972 but left Jardines in 1977 to join his brother at Matheson & Co. He returned to join Jardines again in 1983 as senior managing director and then chairman after his father managed to remove the former managing director [[David Newbigging]]. Keswick started the restructuring of the company becoming more international rather than tied to Hong Kong.
He retired as Tai-pan in 1988 after seeing the firm's holding office redomiciled to Bermuda and restructuring the firm's senior management organisation.
==Fifth generation==
The family still owns considerable holdings in the company with a 5th generation working within the company.<ref>[http://www.jardines.com/profile/manage.html Jardine Matheson Limited: Directors] {{webarchive|url=https://web.archive.org/web/20080327024402/http://www.jardines.com/profile/manage.html |date=27 March 2008 }}</ref>
===Percy Weatherall===
[[Percy Weatherall]] (born 1957) or Edward '''Percy''' Keswick Weatherall, is a great grandson of Henry Keswick (1870–1928). He was managing director of the Jardine Matheson Group from 2000 to 31 March 2006 having joined the board in 1999.
Weatherall joined the Jardine Group in 1976 and worked in a number of senior executive positions in Hong Kong, the US, the United Kingdom, Saudi Arabia, Korea and the Philippines. Prior to becoming "Taipan", Weatherall was chief executive officer of Hongkong Land.
===Ben Keswick===
Ben Keswick, son of Simon, born 1972, joined the board in April 2007. He was group managing director of Jardine Cycle & Carriage. He joined the group in 1998 and held positions in Dairy Farm and Hongkong Land before taking an MBA at INSEAD. In 2003, he was appointed finance director of Jardine Pacific, and was its chief executive officer from 2005 to 2007. Ben Keswick became chairman and managing director (taipan) of Jardine Matheson Limited in April 2012. He also holds senior leadership positions in Cycle & Carriage Bintang and MCL Land; and a commissioner of Astra and United Tractors. Ben is currently chairman of the board of directors and managing director at [[Dairy Farm International Holdings|Dairy Farm Group]], which operates several high street supermarkets across Asia. Ben has been criticised in the media for Dairy Farm's use of eggs from caged hens - undercover investigative footage was released alongside an [https://www.theonlinecitizen.com/2021/01/26/international-consumer-protection-organisation-files-complaint-against-local-farm-for-producing-caged-eggs/ official complaint] {{Webarchive|url=https://web.archive.org/web/20210801064937/https://www.theonlinecitizen.com/2021/01/26/international-consumer-protection-organisation-files-complaint-against-local-farm-for-producing-caged-eggs/ |date=1 August 2021 }} to the Singapore Food Agency in February 2021 from a consumer and animal rights organisation called [https://equitasglobal.org Equitas Global]. Equitas Global has also published [http://benkeswick.com more details] {{Webarchive|url=https://web.archive.org/web/20160109221207/http://benkeswick.com/ |date=9 January 2016 }} about Ben Keswick's role at Dairy Farm, as well at the video footage of the caged farms used by the company.
===Adam Keswick===
Adam Keswick, born 1973, son of Chips, joined the board in April 2007. He is chief executive of Jardine Pacific and of [[Jardine Motors Group|Jardine Motors]]. After joining the Group in 2001 from [[N M Rothschild & Sons]], he held positions within Group Treasury and Jardine Pacific. Mr Keswick was appointed group strategy director of Jardine Cycle & Carriage in 2003, and was group managing director from 2005 to 2007. He is also a director of Jardine Matheson Limited.
==Family tree==
This is the '''family tree of [[William Jardine (1784–1843)]]''', co-founder of the [[Far East]]ern conglomerate, [[Jardine Matheson Holdings]].
{{Family tree of William Jardine (1784-1843)}}
== See also ==
*[[Anglo-Chinese relations]]
*[[Jardine Matheson Holdings]]
==References==
'''Notes'''
{{notes}}
'''Citations'''
{{reflist}}
==Sources==
* ''William Keswick, 1835–1912: Jardine's Pioneer in Japan'' by J. E. Hoare, Chapter 10, ''Britain & Japan: Biographical Portraits'' {{ISBN|1-903350-14-X}}
* ''The Thistle and the Jade: A Celebration of 175 Years of Jardine, Matheson & Co.'' by Maggie Keswick
* ''Jardine Matheson Archives'' from Cambridge University
==External links==
*[http://business.timesonline.co.uk/tol/business/specials/rich_list/article3759058.ece Position 98th] {{Webarchive|url=https://web.archive.org/web/20110612045349/http://business.timesonline.co.uk/tol/business/specials/rich_list/article3759058.ece |date=12 June 2011 }} on the [[Sunday Times Rich List 2008]]
*[http://companies.jrank.org/pages/2216/Jardine-Matheson-Holdings-Limited.html Jardine Matheson Holdings Limited Business Information, Profile, and History]
{{DEFAULTSORT:Keswick Family}}
[[Category:Keswick family| ]]
[[Category:Hong Kong businesspeople]]
[[Category:Jardine Matheson Group]]
04mlm28asbkqs59upughfmhkoiylzeo
Mwbot-rs/Save
0
122864
749647
748527
2026-07-03T12:06:38Z
Mwbot-rs test
52001
Test suite edit
749647
wikitext
text/x-wiki
It has been 1783080398 seconds since the epoch.
mfloumv2hsx9h7uebhyoo2aqa0vtjvo
749648
749647
2026-07-03T12:06:57Z
Mwbot-rs test
52001
Test suite edit
749648
wikitext
text/x-wiki
It has been 1783080417 seconds since the epoch.
cmacqvjnconmcaos0fqbedqq61hgek4
749649
749648
2026-07-03T12:07:16Z
Mwbot-rs test
52001
Test suite edit
749649
wikitext
text/x-wiki
It has been 1783080436 seconds since the epoch.
ab3za1tsc3q61sddamltkif2npjp1lv
749650
749649
2026-07-03T12:07:25Z
Mwbot-rs test
52001
Test suite edit
749650
wikitext
text/x-wiki
It has been 1783080445 seconds since the epoch.
25juuzl8ytvenbqi6t4qq1j3qj3ua21
Fiscal Responsibility Act of 2023
0
153286
749703
581865
2026-07-03T18:56:34Z
Solidest
54422
[[Википедия:К переименованию/3 июля 2026#Fiscal Responsibility Act of 2023 → Test|номинация к переименованию]] ([[Википедия:Гаджеты/Remover|Remover]])
749703
wikitext
text/x-wiki
<noinclude>{{к переименованию|2026-07-03|Test}}</noinclude>
The '''Fiscal Responsibility Act of 2023''' is an act of the US Congress.
==External links==
* {{Statute Compilations external link}}
j5brtozttqjkh7vp3rwtxtohx5lt4rs
MediaWiki:GrowthExperimentsHelpPanel.json
8
156148
749838
735821
2026-07-04T00:28:23Z
Sohom Datta
44934
test of how this looks
749838
json
application/json
{
"GEHelpPanelAskMentor": "help-desk-page",
"GEHelpPanelExcludedNamespaces": [
6,
10
],
"GEHelpPanelHelpDeskPostOnTop": "bottom",
"GEHelpPanelHelpDeskTitle": "Wikipedia:Requests/Help_desk",
"GEHelpPanelLinks": [
{
"title": "Wikipedia:What Test Wiki is not",
"text": "Write a Wikipedia article using the right style",
"id": "EditingHelp"
},
{
"title": "Wikipedia:Creation_and_usage_of_media_files",
"text": "Add an image",
"id": "Wikipedia:Creation_and_usage_of_media_files"
},
{
"title": "Mavetuna13",
"text": "Add a citation",
"id": "Wikipedia:Citing_sources"
},
{
"title": "Help:External links",
"text": "How to add an external link",
"id": "Help:External_links"
},
{
"title": "Help:Infobox",
"text": "How to add and edit the infobox",
"id": "Help:Infobox"
}
],
"GEHelpPanelReadingModeNamespaces": [
2,
4,
12
],
"GEHelpPanelSearchNamespaces": [
4,
12
],
"GEHelpPanelViewMoreTitle": "Help:Contents",
"$version": "1.0.0"
}
lujymqhz4k4hdm9c2vea6mumeaets5i
749839
749838
2026-07-04T00:33:22Z
Sohom Datta
44934
Reverted edit by [[Special:Contributions/Sohom Datta|Sohom Datta]] ([[User talk:Sohom Datta|talk]]) to last revision by [[User:Martin Urbanec (WMF)|Martin Urbanec (WMF)]]
735821
json
application/json
{
"GEHelpPanelAskMentor": "mentor-talk-page",
"GEHelpPanelExcludedNamespaces": [
6,
10
],
"GEHelpPanelHelpDeskPostOnTop": "bottom",
"GEHelpPanelHelpDeskTitle": "Wikipedia:Requests/Help_desk",
"GEHelpPanelLinks": [
{
"title": "Wikipedia:What Test Wiki is not",
"text": "Write a Wikipedia article using the right style",
"id": "EditingHelp"
},
{
"title": "Wikipedia:Creation_and_usage_of_media_files",
"text": "Add an image",
"id": "Wikipedia:Creation_and_usage_of_media_files"
},
{
"title": "Mavetuna13",
"text": "Add a citation",
"id": "Wikipedia:Citing_sources"
},
{
"title": "Help:External links",
"text": "How to add an external link",
"id": "Help:External_links"
},
{
"title": "Help:Infobox",
"text": "How to add and edit the infobox",
"id": "Help:Infobox"
}
],
"GEHelpPanelReadingModeNamespaces": [
2,
4,
12
],
"GEHelpPanelSearchNamespaces": [
4,
12
],
"GEHelpPanelViewMoreTitle": "Help:Contents",
"$version": "1.0.0"
}
7wn8jo663r1ha0ysapfy0x346klaxr3
User:Borhan/test.js
2
159779
749655
748349
2026-07-03T12:24:38Z
Borhan
54019
749655
javascript
text/javascript
// <nowiki>
$(() => {
'use strict';
const EasyVoteMeta = {
config: {
messages: {
windowTitle: 'EasyVoteMeta',
windowButton: 'Submit',
vote: 'Please select your !vote:',
pleaseSelectVote: 'Please select your !vote',
confirmPossibleDouble: 'It appears that you have previously commented on this request. Are you sure you want to continue? Please strike out past comments!',
editError: 'Error: Reload the page (F5) and try again',
comment: 'Comment (optional):',
signatureAutoAdd: 'Your signature will be added automatically.'
},
voteOptions: {
support: {
label: 'Support',
template: 'support'
},
strongsupport: {
label: 'Strong support',
template: 'strong support'
},
weaksupport: {
label: 'Weak support',
template: 'weak support'
},
oppose: {
label: 'Oppose',
template: 'oppose'
},
strongoppose: {
label: 'Strong oppose',
template: 'strong oppose'
},
weakoppose: {
label: 'Weak oppose',
template: 'weak oppose'
},
neutral: {
label: 'Neutral',
template: 'neutral'
},
moralsupport: {
label: 'Moral support',
template: 'moral support'
},
abstain: {
label: 'Abstain',
template: 'abstain'
}
},
iconMap: {
support: 'https://upload.wikimedia.org/wikipedia/commons/9/94/Symbol_support_vote.svg',
strongsupport: 'https://upload.wikimedia.org/wikipedia/commons/8/84/Symbol_strong_support_vote.svg',
weaksupport: 'https://upload.wikimedia.org/wikipedia/commons/8/8c/GA_candidate.svg',
moralsupport: 'https://upload.wikimedia.org/wikipedia/commons/9/94/Symbol_support_vote.svg',
neutral: 'https://upload.wikimedia.org/wikipedia/commons/8/89/Symbol_neutral_vote.svg',
oppose: 'https://upload.wikimedia.org/wikipedia/commons/7/7f/Symbol_oppose_vote.svg',
weakoppose: 'https://upload.wikimedia.org/wikipedia/commons/f/f5/BA_candidate.svg',
strongoppose: 'https://upload.wikimedia.org/wikipedia/commons/5/5e/Symbol_oppose_vote_oversat.svg',
abstain: 'https://upload.wikimedia.org/wikipedia/commons/6/61/Symbol_abstain_vote.svg'
},
availableLangs: ['en'],
editSummary: '',
summfix: ' ([[User:DreamRimmer/EasyVoteMeta.js|EVM]])',
pageConfigs: {
'Steward requests/Global permissions': {
type: 'steward',
targetPage: 'Steward requests/Global permissions'
},
'Meta:Requests for adminship': {
type: 'meta',
targetPage: 'Meta:Requests for adminship'
}
}
},
currentPageType: null,
currentPageConfig: null,
init() {
const currentPage = mw.config.get('wgPageName').replace(/_/g, ' ');
this.currentPageConfig = this.config.pageConfigs[currentPage];
Object.values(this.config.iconMap).forEach(function(url) {
var img = new window.Image();
img.src = url;
});
if (!this.currentPageConfig) return;
this.currentPageType = this.currentPageConfig.type;
if (mw.user.isAnon()) return;
mw.loader.using('mediawiki.api', this.run.bind(this));
},
run() {
this.addStyle();
const headingSelector = 'div.mw-heading.mw-heading3';
$(headingSelector).each((_, heading) => {
const $heading = $(heading);
const editLinks = $heading.find('.mw-editsection');
const sectionTitle = $heading.find('h3').attr('id').replace(/_/g, ' ');
const normalizedTitle = sectionTitle.toLowerCase();
if (
normalizedTitle.startsWith("epic") ||
normalizedTitle.startsWith("infinite0694") ||
normalizedTitle.startsWith("global ip block exempt") ||
normalizedTitle.startsWith("captcha exemptions") ||
normalizedTitle.startsWith("add global vrt") ||
normalizedTitle.startsWith("remove global vrt") ||
normalizedTitle.startsWith("2fa tester")
) return;
if (this.currentPageType === 'steward') {
this.handleStewardPage(editLinks, sectionTitle, $heading);
} else if (this.currentPageType === 'meta') {
this.handleMetaPage(editLinks, sectionTitle, $heading);
}
});
},
handleStewardPage(editLinks, sectionTitle, $heading) {
const sectionNum = this.getSectionNumber(editLinks);
if (sectionNum !== -1) {
this.addVoteLink(editLinks, sectionTitle, { type: 'steward', sectionNum: sectionNum }, $heading);
}
},
handleMetaPage(editLinks, sectionTitle, $heading) {
const normalizedTitle = sectionTitle.toLowerCase();
if (
normalizedTitle.startsWith("regular adminship") ||
normalizedTitle.startsWith("bureaucratship") ||
normalizedTitle.startsWith("other access") ||
normalizedTitle.startsWith("wmf office staff and contractors")
) return;
const targetPageInfo = this.getTargetPageInfo(editLinks, $heading);
if (targetPageInfo && targetPageInfo.targetPage) {
this.addVoteLink(editLinks, sectionTitle, { type: 'meta', ...targetPageInfo }, $heading);
}
},
addStyle() {
mw.util.addCSS(`
.easyvote-meta-link {
color: #0645ad;
text-decoration: none;
font-weight: bold;
}
.easyvote-meta-link:hover {
text-decoration: underline;
}
.easyvote-meta-disabled {
color: #888 !important;
pointer-events: none;
}
.easyvote-meta-dialog {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
border: 1px solid #a2a9b1;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
width: 520px;
max-height: 85vh;
z-index: 1001;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Lato, Helvetica, Arial, sans-serif;
}
.easyvote-meta-dialog-header {
padding: 16px 20px;
border-bottom: 1px solid #a2a9b1;
background: #eaecf0;
font-weight: bold;
font-size: 16px;
border-radius: 8px 8px 0 0;
}
.easyvote-meta-dialog-body {
padding: 20px;
max-height: calc(85vh - 125px);
overflow-y: auto;
}
.easyvote-meta-dialog-body p {
margin-top: 0;
}
.easyvote-meta-dialog-footer {
padding: 16px 20px;
border-top: 1px solid #a2a9b1;
background: #f8f9fa;
text-align: right;
border-radius: 0 0 8px 8px;
}
.easyvote-meta-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.2);
z-index: 1000;
}
.easyvote-meta-option {
margin: 8px 0;
display: flex;
align-items: center;
}
.easyvote-meta-option input {
margin-right: 8px;
}
.easyvote-meta-option label {
margin: 0;
cursor: pointer !important;
display: flex;
align-items: center;
}
.easyvote-meta-option img {
margin-right: 6px;
vertical-align: middle;
}
.easyvote-meta-button {
background: #36c;
color: white;
border: 1px solid #36c;
border-radius: 6px;
padding: 6px 12px;
font-size: 14px;
cursor: pointer !important;
margin-left: 8px;
position: relative;
min-width: 80px;
}
.easyvote-meta-button:hover:not(:disabled) {
background: #447ff5;
border-color: #447ff5;
}
.easyvote-meta-button:disabled {
background: #c8ccd1;
border-color: #c8ccd1;
cursor: not-allowed;
}
.easyvote-meta-button-secondary {
background: #f8f9fa;
color: #202122;
border-radius: 6px;
border: 1px solid #a2a9b1;
}
.easyvote-meta-button-secondary:hover {
background: #eaecf0 !important;
border-color: #a2a9b1;
color: #202122;
}
.easyvote-meta-textarea {
width: 100%;
height: 60px;
resize: vertical;
font-family: monospace;
border: 1px solid #a2a9b1;
border-radius: 2px;
padding: 8px;
margin-top: 4px;
}
.easyvote-meta-signature-note {
font-style: italic;
color: #666;
font-size: 0.9em;
margin-top: 8px;
}
.easyvote-meta-spinner {
display: inline-block;
width: 12px;
height: 12px;
border: 1.5px solid #f3f3f3;
border-top: 1.5px solid #36c;
border-radius: 50%;
animation: easyvote-meta-spin 1s linear infinite;
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
}
@keyframes easyvote-meta-spin {
0% { transform: translateY(-50%) rotate(0deg); }
100% { transform: translateY(-50%) rotate(360deg); }
}
.easyvote-meta-button.loading {
padding-right: 35px;
}
`);
},
getSectionNumber(editLinks) {
const editSectionUrl = editLinks.find('a:first').attr('href');
const sectionMatch = editSectionUrl && editSectionUrl.match(/§ion=(\d+)(?:$|&)/);
return sectionMatch ? parseInt(sectionMatch[1]) : -1;
},
getTargetPageInfo(editLinks, $heading) {
const editSectionUrl = editLinks.find('a:first').attr('href');
if (editSectionUrl) {
const titleMatch = editSectionUrl.match(/[?&]title=([^&]+)/);
const sectionMatch = editSectionUrl.match(/[?&]section=([^&]+)/);
if (titleMatch) {
const targetPage = decodeURIComponent(titleMatch[1]).replace(/_/g, ' ');
const sectionId = sectionMatch ? sectionMatch[1] : null;
return {
targetPage: targetPage,
sectionId: sectionId
};
}
}
const headingLink = $heading.find('h3 a').first();
if (headingLink.length) {
const title = headingLink.attr('title');
if (title) {
return {
targetPage: title,
sectionId: 'T-1'
};
}
}
return null;
},
addVoteLink(editLinks, sectionTitle, pageInfo, $heading) {
const uniqueId = 'easyvote-meta-launch-' + Math.random().toString(36).substr(2, 9);
const voteLink = $('<a>')
.attr('id', uniqueId)
.addClass('easyvote-meta-link')
.text('!vote')
.attr('href', '#')
.click((event) => {
event.preventDefault();
if (!voteLink.hasClass('easyvote-meta-disabled')) {
voteLink.addClass('easyvote-meta-disabled');
this.openDialog(sectionTitle, pageInfo, $heading, uniqueId);
}
});
editLinks.children().last().before(' | ', voteLink);
},
openDialog(sectionTitle, pageInfo, $heading, uniqueId) {
const self = this;
if (pageInfo.type === 'meta') {
console.log('EasyVoteMeta: Target page that will be edited:', pageInfo.targetPage);
}
$('.easyvote-meta-dialog, .easyvote-meta-overlay').remove();
const $overlay = $('<div>').addClass('easyvote-meta-overlay').click((e) => {
if (e.target === e.currentTarget) {
this.closeDialog(uniqueId);
}
});
const $dialog = $('<div>')
.addClass('easyvote-meta-dialog')
.data('page-info', pageInfo)
.data('section-heading', $heading)
.data('unique-id', uniqueId);
const $header = $('<div>')
.addClass('easyvote-meta-dialog-header')
.text(this.config.messages.windowTitle + ': ' + sectionTitle);
const $body = $('<div>')
.addClass('easyvote-meta-dialog-body');
const $form = $('<form>')
.attr('name', 'easyVoteMetaForm')
.append($('<p>').text(this.config.messages.vote));
Object.keys(this.config.voteOptions).forEach(key => {
$form.append(this.createVoteOption(key));
});
$form.append($('<br>'))
.append($('<label>')
.attr('for', 'easyvote-meta-comment')
.text(this.config.messages.comment))
.append($('<br>'))
.append($('<textarea>')
.attr({
id: 'easyvote-meta-comment',
name: 'comment',
rows: 3,
cols: 50
})
.addClass('easyvote-meta-textarea'))
.append($('<div>')
.addClass('easyvote-meta-signature-note')
.text(this.config.messages.signatureAutoAdd));
$body.append($form);
const $footer = $('<div>')
.addClass('easyvote-meta-dialog-footer');
const $cancelButton = $('<button>')
.addClass('easyvote-meta-button easyvote-meta-button-secondary')
.text('Cancel')
.click((e) => {
e.preventDefault();
this.closeDialog(uniqueId);
});
const $submitButton = $('<button>')
.addClass('easyvote-meta-button')
.attr('id', 'easyvote-meta-button')
.attr('disabled', true)
.text(this.config.messages.windowButton)
.click((e) => {
e.preventDefault();
this.doVote($dialog);
});
$footer.append($cancelButton, $submitButton);
$dialog.append($header, $body, $footer);
$('body').append($overlay, $dialog);
$form.find('input[name="vote"]').on('change', function() {
$submitButton.prop('disabled', false);
});
setTimeout(() => {
$form.find('input[name="vote"]:first').focus();
}, 100);
},
closeDialog(uniqueId) {
$('.easyvote-meta-dialog, .easyvote-meta-overlay').remove();
$(`#${uniqueId}`).removeClass('easyvote-meta-disabled');
},
createVoteOption(type) {
const voteOption = this.config.voteOptions[type];
const iconUrl = this.config.iconMap[type];
return $('<div>')
.addClass('easyvote-meta-option')
.append(
$('<input>')
.attr({
type: 'radio',
name: 'vote',
value: type,
id: 'easyvote-meta-' + type
})
)
.append(
$('<label>')
.attr('for', 'easyvote-meta-' + type)
.append(
$('<img>')
.attr({
src: iconUrl,
alt: '',
width: 20,
height: 20
})
)
.append(voteOption.label)
);
},
doVote($dialog) {
const self = this;
function onFail(error) {
const $voteButton = $('#easyvote-meta-button');
$voteButton.removeClass('loading').prop('disabled', false).text(self.config.messages.windowButton);
$voteButton.find('.easyvote-meta-spinner').remove();
console.error('Submission failed:', error);
alert(self.config.messages.editError);
}
let vote, comment, pageInfo, $heading, sectionTitle, uniqueId;
const $voteButton = $('#easyvote-meta-button');
const $checkedVote = $dialog.find('input[name="vote"]:checked');
if (!$checkedVote.length) {
alert(this.config.messages.pleaseSelectVote);
return false;
}
vote = $checkedVote.val();
comment = $dialog.find('#easyvote-meta-comment').val().trim();
pageInfo = $dialog.data('page-info');
$heading = $dialog.data('section-heading');
uniqueId = $dialog.data('unique-id');
sectionTitle = $heading.contents().not($heading.find('.mw-editsection')).text().trim();
$voteButton.addClass('loading').prop('disabled', true);
const $spinner = $('<span>').addClass('easyvote-meta-spinner');
$voteButton.append($spinner);
const templateName = this.config.voteOptions[vote].template;
let voteLine = '\n* {{' + templateName + '}}';
if (comment) voteLine += ' ' + comment;
voteLine += ' ~~~~';
const voteDisplayName = this.config.voteOptions[vote]?.label || vote;
let editSummary = `/* ${sectionTitle} */ ${voteDisplayName}${this.config.summfix}`;
const api = new mw.Api();
if (pageInfo.type === 'steward') {
this.doStewardVote(api, pageInfo, voteLine, editSummary, $dialog, onFail);
} else if (pageInfo.type === 'meta') {
this.doMetaVote(api, pageInfo, voteLine, editSummary, $dialog, onFail);
}
},
doStewardVote(api, pageInfo, voteLine, editSummary, $dialog, onFail) {
api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get('wgPageName'),
rvprop: 'content',
rvsection: pageInfo.sectionNum,
formatversion: 2
}).then((res) => {
const page = res.query.pages[0];
if (!page || !page.revisions || !page.revisions[0]) {
throw new Error('Failed to fetch section content');
}
const sectionText = page.revisions[0].content;
const currentUser = mw.config.get('wgUserName');
const userVotePattern = new RegExp('\\{\\{(support|oppose|strong support|weak support|strong oppose|weak oppose|neutral|moral support|s|ss|o|)\\}\\}[^~]*' + mw.util.escapeRegExp(currentUser), 'i');
if (userVotePattern.test(sectionText)) {
if (!confirm(this.config.messages.confirmPossibleDouble)) {
const $voteButton = $('#easyvote-meta-button');
$voteButton.removeClass('loading').prop('disabled', false).text(this.config.messages.windowButton);
$voteButton.find('.easyvote-meta-spinner').remove();
return;
}
}
return api.postWithEditToken({
action: 'edit',
title: mw.config.get('wgPageName'),
section: pageInfo.sectionNum,
appendtext: voteLine,
summary: editSummary,
token: mw.user.tokens.get('csrfToken')
});
}).then(() => {
setTimeout(() => {
window.location.reload();
}, 1500);
}).catch(onFail);
},
doMetaVote(api, pageInfo, voteLine, editSummary, $dialog, onFail) {
api.get({
action: 'query',
prop: 'revisions',
titles: pageInfo.targetPage,
rvprop: 'content',
rvsection: pageInfo.sectionId,
formatversion: 2
}).then((res) => {
const page = res.query.pages[0];
if (!page || !page.revisions || !page.revisions[0]) {
throw new Error('failed to fetch section content from target page');
}
const sectionText = page.revisions[0].content;
const currentUser = mw.config.get('wgUserName');
const userVotePattern = new RegExp('\\{\\{(support|oppose|strong support|weak support|strong oppose|weak oppose|neutral|moral support|s|ss|o|)\\}\\}[^~]*' + mw.util.escapeRegExp(currentUser), 'i');
if (userVotePattern.test(sectionText)) {
if (!confirm(this.config.messages.confirmPossibleDouble)) {
const $voteButton = $('#easyvote-meta-button');
$voteButton.removeClass('loading').prop('disabled', false).text(this.config.messages.windowButton);
$voteButton.find('.easyvote-meta-spinner').remove();
return;
}
}
return api.postWithEditToken({
action: 'edit',
title: pageInfo.targetPage,
section: pageInfo.sectionId,
appendtext: voteLine,
summary: editSummary,
token: mw.user.tokens.get('csrfToken')
});
}).then(() => {
setTimeout(() => {
window.location.reload();
}, 1500);
}).catch(onFail);
}
};
EasyVoteMeta.init();
});
// </nowiki>
3bks306vtup5t93ep54jxhx8x05n3ty
749658
749655
2026-07-03T12:50:33Z
Borhan
54019
749658
javascript
text/javascript
// <nowiki>
$(() => {
'use strict';
const EasyVoteBN = {
config: {
messages: {
windowTitle: 'ভোট',
windowButton: 'জমা দিন',
vote: 'আপনার ভোট নির্বাচন করুন:',
pleaseSelectVote: 'আপনার ভোট নির্বাচন করুন',
confirmPossibleDouble: 'আপনি সম্ভবত আগেও এই আলোচনায় মন্তব্য করেছেন। আপনি কি নিশ্চিত যে আপনি চালিয়ে যেতে চান? দয়া করে পূর্ববর্তী মন্তব্যগুলি বাদ দিন!',
editError: 'ত্রুটি: পাতাটি পুনরায় লোড করুন (F5) এবং আবার চেষ্টা করুন',
comment: 'মন্তব্য (ঐচ্ছিক):',
signatureAutoAdd: 'আপনার স্বাক্ষর স্বয়ংক্রিয়ভাবে যোগ করা হবে।'
},
voteOptions: {
support: {
label: 'সমর্থন',
template: 'সমর্থন'
},
strongsupport: {
label: 'দৃঢ় সমর্থন',
template: 'দৃঢ় সমর্থন'
},
oppose: {
label: 'বিরোধিতা',
template: 'বিরোধিতা'
},
strongoppose: {
label: 'জোরালো বিরোধিতা',
template: 'জোরালো বিরোধিতা'
},
weakoppose: {
label: 'দুর্বল বিরোধিতা',
template: 'দুর্বল বিরোধিতা'
},
neutral: {
label: 'নিরপেক্ষ',
template: 'নিরপেক্ষ'
},
delete: {
label: 'মুছুন',
template: 'মুছুন'
},
keep: {
label: 'রাখুন',
template: 'রাখুন'
},
note: {
label: 'মন্তব্য',
template: 'মন্তব্য'
}
},
iconMap: {
support: 'https://upload.wikimedia.org/wikipedia/commons/9/94/Symbol_support_vote.svg',
strongsupport: 'https://upload.wikimedia.org/wikipedia/commons/8/84/Symbol_strong_support_vote.svg',
delete: 'https://upload.wikimedia.org/wikipedia/commons/8/89/Symbol_delete_vote.svg',
neutral: 'https://upload.wikimedia.org/wikipedia/commons/8/89/Symbol_neutral_vote.svg',
oppose: 'https://upload.wikimedia.org/wikipedia/commons/7/7f/Symbol_oppose_vote.svg',
weakoppose: 'https://upload.wikimedia.org/wikipedia/commons/f/f5/BA_candidate.svg',
strongoppose: 'https://upload.wikimedia.org/wikipedia/commons/5/5e/Symbol_oppose_vote_oversat.svg',
keep: 'https://upload.wikimedia.org/wikipedia/commons/d/d0/Symbol_keep_vote.svg',
note: 'https://upload.wikimedia.org/wikipedia/commons/a/a6/Pictogram_voting_comment.svg'
},
availableLangs: ['bn'],
editSummary: '',
summfix: '',
pageConfigs: {
'^উইকিপিডিয়া:আলোচনাসভা($|/)': {
type: 'discussion',
targetPage: 'উইকিপিডিয়া:আলোচনাসভা'
},
'^উইকিপিডিয়া:নিবন্ধ অপসারণের প্রস্তাবনা/[^/]+': {
type: 'deletion',
targetPage: 'উইকিপিডিয়া:নিবন্ধ অপসারণের প্রস্তাবনা'
}
}
},
currentPageType: null,
currentPageConfig: null,
init() {
const currentPage = mw.config.get('wgPageName').replace(/_/g, ' ');
for (const pattern in this.config.pageConfigs) {
const regex = new RegExp(pattern);
if (regex.test(currentPage)) {
this.currentPageConfig = this.config.pageConfigs[pattern];
break;
}
}
Object.values(this.config.iconMap).forEach(function(url) {
var img = new window.Image();
img.src = url;
});
if (!this.currentPageConfig) return;
this.currentPageType = this.currentPageConfig.type;
if (mw.user.isAnon()) return;
mw.loader.using('mediawiki.api', this.run.bind(this));
},
run() {
this.addStyle();
const headingSelector = 'div.mw-heading.mw-heading2, div.mw-heading.mw-heading3';
$(headingSelector).each((_, heading) => {
const $heading = $(heading);
const editLinks = $heading.find('.mw-editsection');
const sectionTitle = $heading.find('h3').clone().find('.mw-editsection, .copy-section-link-pilcrow').remove().end().text().trim();
const normalizedTitle = sectionTitle.toLowerCase();
if (
normalizedTitle.startsWith("global ip block exempt") ||
normalizedTitle.startsWith("captcha exemptions") ||
normalizedTitle.startsWith("add global vrt") ||
normalizedTitle.startsWith("remove global vrt") ||
normalizedTitle.startsWith("2fa tester")
) return;
if (this.currentPageType === 'discussion') {
this.handleDiscussionPage(editLinks, sectionTitle, $heading);
} else if (this.currentPageType === 'deletion') {
this.handleDeletionPage(editLinks, sectionTitle, $heading);
}
});
},
handleDiscussionPage(editLinks, sectionTitle, $heading) {
const sectionNum = this.getSectionNumber(editLinks);
if (sectionNum !== -1) {
this.addVoteLink(editLinks, sectionTitle, { type: 'discussion', sectionNum: sectionNum }, $heading);
}
},
handleDeletionPage(editLinks, sectionTitle, $heading) {
const normalizedTitle = sectionTitle.toLowerCase();
if (
normalizedTitle.startsWith("regular adminship") ||
normalizedTitle.startsWith("bureaucratship") ||
normalizedTitle.startsWith("other access") ||
normalizedTitle.startsWith("wmf office staff and contractors")
) return;
const targetPageInfo = this.getTargetPageInfo(editLinks, $heading);
if (targetPageInfo && targetPageInfo.targetPage) {
this.addVoteLink(editLinks, sectionTitle, { type: 'deletion', ...targetPageInfo }, $heading);
}
},
addStyle() {
mw.util.addCSS(`
.easyvote-BN-link {
color: #0645ad;
text-decoration: none;
font-weight: bold;
}
.easyvote-BN-link:hover {
text-decoration: underline;
}
.easyvote-BN-disabled {
color: #888 !important;
pointer-events: none;
}
.easyvote-BN-dialog {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
border: 1px solid #a2a9b1;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
width: 520px;
max-height: 85vh;
z-index: 1001;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Lato, Helvetica, Arial, sans-serif;
}
.easyvote-BN-dialog-header {
padding: 16px 20px;
border-bottom: 1px solid #a2a9b1;
background: #eaecf0;
font-weight: bold;
font-size: 16px;
border-radius: 8px 8px 0 0;
}
.easyvote-BN-dialog-body {
padding: 20px;
max-height: calc(85vh - 125px);
overflow-y: auto;
}
.easyvote-BN-dialog-body p {
margin-top: 0;
}
.easyvote-BN-dialog-footer {
padding: 16px 20px;
border-top: 1px solid #a2a9b1;
background: #f8f9fa;
text-align: right;
border-radius: 0 0 8px 8px;
}
.easyvote-BN-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.2);
z-index: 1000;
}
.easyvote-BN-option {
margin: 8px 0;
display: flex;
align-items: center;
}
.easyvote-BN-option input {
margin-right: 8px;
}
.easyvote-BN-option label {
margin: 0;
cursor: pointer !important;
display: flex;
align-items: center;
}
.easyvote-BN-option img {
margin-right: 6px;
vertical-align: middle;
}
.easyvote-BN-button {
background: #36c;
color: white;
border: 1px solid #36c;
border-radius: 6px;
padding: 6px 12px;
font-size: 14px;
cursor: pointer !important;
margin-left: 8px;
position: relative;
min-width: 80px;
}
.easyvote-BN-button:hover:not(:disabled) {
background: #447ff5;
border-color: #447ff5;
}
.easyvote-BN-button:disabled {
background: #c8ccd1;
border-color: #c8ccd1;
cursor: not-allowed;
}
.easyvote-BN-button-secondary {
background: #f8f9fa;
color: #202122;
border-radius: 6px;
border: 1px solid #a2a9b1;
}
.easyvote-BN-button-secondary:hover {
background: #eaecf0 !important;
border-color: #a2a9b1;
color: #202122;
}
.easyvote-BN-textarea {
width: 100%;
height: 60px;
resize: vertical;
font-family: monospace;
border: 1px solid #a2a9b1;
border-radius: 2px;
padding: 8px;
margin-top: 4px;
}
.easyvote-BN-signature-note {
font-style: italic;
color: #666;
font-size: 0.9em;
margin-top: 8px;
}
.easyvote-BN-spinner {
display: inline-block;
width: 12px;
height: 12px;
border: 1.5px solid #f3f3f3;
border-top: 1.5px solid #36c;
border-radius: 50%;
animation: easyvote-BN-spin 1s linear infinite;
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
}
@keyframes easyvote-BN-spin {
0% { transform: translateY(-50%) rotate(0deg); }
100% { transform: translateY(-50%) rotate(360deg); }
}
.easyvote-BN-button.loading {
padding-right: 35px;
}
`);
},
getSectionNumber(editLinks) {
const editSectionUrl = editLinks.find('a:first').attr('href');
const sectionMatch = editSectionUrl && editSectionUrl.match(/§ion=(\d+)(?:$|&)/);
return sectionMatch ? parseInt(sectionMatch[1]) : -1;
},
getTargetPageInfo(editLinks, $heading) {
const editSectionUrl = editLinks.find('a:first').attr('href');
if (editSectionUrl) {
const titleMatch = editSectionUrl.match(/[?&]title=([^&]+)/);
const sectionMatch = editSectionUrl.match(/[?&]section=([^&]+)/);
if (titleMatch) {
const targetPage = decodeURIComponent(titleMatch[1]).replace(/_/g, ' ');
const sectionId = sectionMatch ? sectionMatch[1] : null;
return {
targetPage: targetPage,
sectionId: sectionId
};
}
}
const headingLink = $heading.find('h3 a').first();
if (headingLink.length) {
const title = headingLink.attr('title');
if (title) {
return {
targetPage: title,
sectionId: 'T-1'
};
}
}
return null;
},
addVoteLink(editLinks, sectionTitle, pageInfo, $heading) {
const uniqueId = 'easyvote-BN-launch-' + Math.random().toString(36).substr(2, 9);
const voteLink = $('<a>')
.attr('id', uniqueId)
.addClass('easyvote-BN-link')
.text('!ভোট')
.attr('href', '#')
.click((event) => {
event.preventDefault();
if (!voteLink.hasClass('easyvote-BN-disabled')) {
voteLink.addClass('easyvote-BN-disabled');
this.openDialog(sectionTitle, pageInfo, $heading, uniqueId);
}
});
editLinks.children().last().before(' | ', voteLink);
},
openDialog(sectionTitle, pageInfo, $heading, uniqueId) {
const self = this;
if (pageInfo.type === 'deletion') {
console.log('EasyVoteBN: Target page that will be edited:', pageInfo.targetPage);
}
$('.easyvote-BN-dialog, .easyvote-BN-overlay').remove();
const $overlay = $('<div>').addClass('easyvote-BN-overlay').click((e) => {
if (e.target === e.currentTarget) {
this.closeDialog(uniqueId);
}
});
const $dialog = $('<div>')
.addClass('easyvote-BN-dialog')
.data('page-info', pageInfo)
.data('section-heading', $heading)
.data('unique-id', uniqueId);
const $header = $('<div>')
.addClass('easyvote-BN-dialog-header')
.text(this.config.messages.windowTitle + ': ' + sectionTitle);
const $body = $('<div>')
.addClass('easyvote-BN-dialog-body');
const $form = $('<form>')
.attr('name', 'EasyVoteBNForm')
.append($('<p>').text(this.config.messages.vote));
Object.keys(this.config.voteOptions).forEach(key => {
$form.append(this.createVoteOption(key));
});
$form.append($('<br>'))
.append($('<label>')
.attr('for', 'easyvote-BN-comment')
.text(this.config.messages.comment))
.append($('<br>'))
.append($('<textarea>')
.attr({
id: 'easyvote-BN-comment',
name: 'comment',
rows: 3,
cols: 50
})
.addClass('easyvote-BN-textarea'))
.append($('<div>')
.addClass('easyvote-BN-signature-note')
.text(this.config.messages.signatureAutoAdd));
$body.append($form);
const $footer = $('<div>')
.addClass('easyvote-BN-dialog-footer');
const $cancelButton = $('<button>')
.addClass('easyvote-BN-button easyvote-BN-button-secondary')
.text('Cancel')
.click((e) => {
e.preventDefault();
this.closeDialog(uniqueId);
});
const $submitButton = $('<button>')
.addClass('easyvote-BN-button')
.attr('id', 'easyvote-BN-button')
.attr('disabled', true)
.text(this.config.messages.windowButton)
.click((e) => {
e.preventDefault();
this.doVote($dialog);
});
$footer.append($cancelButton, $submitButton);
$dialog.append($header, $body, $footer);
$('body').append($overlay, $dialog);
$form.find('input[name="vote"]').on('change', function() {
$submitButton.prop('disabled', false);
});
setTimeout(() => {
$form.find('input[name="vote"]:first').focus();
}, 100);
},
closeDialog(uniqueId) {
$('.easyvote-BN-dialog, .easyvote-BN-overlay').remove();
$(`#${uniqueId}`).removeClass('easyvote-BN-disabled');
},
createVoteOption(type) {
const voteOption = this.config.voteOptions[type];
const iconUrl = this.config.iconMap[type];
return $('<div>')
.addClass('easyvote-BN-option')
.append(
$('<input>')
.attr({
type: 'radio',
name: 'vote',
value: type,
id: 'easyvote-BN-' + type
})
)
.append(
$('<label>')
.attr('for', 'easyvote-BN-' + type)
.append(
$('<img>')
.attr({
src: iconUrl,
alt: '',
width: 20,
height: 20
})
)
.append(voteOption.label)
);
},
doVote($dialog) {
const self = this;
function onFail(error) {
const $voteButton = $('#easyvote-BN-button');
$voteButton.removeClass('loading').prop('disabled', false).text(self.config.messages.windowButton);
$voteButton.find('.easyvote-BN-spinner').remove();
console.error('Submission failed:', error);
alert(self.config.messages.editError);
}
let vote, comment, pageInfo, $heading, sectionTitle, uniqueId;
const $voteButton = $('#easyvote-BN-button');
const $checkedVote = $dialog.find('input[name="vote"]:checked');
if (!$checkedVote.length) {
alert(this.config.messages.pleaseSelectVote);
return false;
}
vote = $checkedVote.val();
comment = $dialog.find('#easyvote-BN-comment').val().trim();
pageInfo = $dialog.data('page-info');
$heading = $dialog.data('section-heading');
uniqueId = $dialog.data('unique-id');
sectionTitle = $heading.contents().not($heading.find('.mw-editsection')).text().trim();
$voteButton.addClass('loading').prop('disabled', true);
const $spinner = $('<span>').addClass('easyvote-BN-spinner');
$voteButton.append($spinner);
const templateName = this.config.voteOptions[vote].template;
let voteLine = '\n* {{' + templateName + '}}';
if (comment) voteLine += ' ' + comment;
voteLine += ' ~~~~';
const voteDisplayName = this.config.voteOptions[vote]?.label || vote;
let editSummary = `/* ${sectionTitle} */ ${voteDisplayName}${this.config.summfix}`;
const api = new mw.Api();
if (pageInfo.type === 'discussion') {
this.doDiscussionVote(api, pageInfo, voteLine, editSummary, $dialog, onFail);
} else if (pageInfo.type === 'deletion') {
this.doDeletionVote(api, pageInfo, voteLine, editSummary, $dialog, onFail);
}
},
doDiscussionVote(api, pageInfo, voteLine, editSummary, $dialog, onFail) {
api.get({
action: 'query',
prop: 'revisions',
titles: mw.config.get('wgPageName'),
rvprop: 'content',
rvsection: pageInfo.sectionNum,
formatversion: 2
}).then((res) => {
const page = res.query.pages[0];
if (!page || !page.revisions || !page.revisions[0]) {
throw new Error('Failed to fetch section content');
}
const sectionText = page.revisions[0].content;
const currentUser = mw.config.get('wgUserName');
const userVotePattern = new RegExp('\\{\\{(support|oppose|strong support|weak support|strong oppose|weak oppose|neutral|moral support|s|ss|o|)\\}\\}[^~]*' + mw.util.escapeRegExp(currentUser), 'i');
if (userVotePattern.test(sectionText)) {
if (!confirm(this.config.messages.confirmPossibleDouble)) {
const $voteButton = $('#easyvote-BN-button');
$voteButton.removeClass('loading').prop('disabled', false).text(this.config.messages.windowButton);
$voteButton.find('.easyvote-BN-spinner').remove();
return;
}
}
return api.postWithEditToken({
action: 'edit',
title: mw.config.get('wgPageName'),
section: pageInfo.sectionNum,
appendtext: voteLine,
summary: editSummary,
token: mw.user.tokens.get('csrfToken')
});
}).then(() => {
setTimeout(() => {
window.location.reload();
}, 1500);
}).catch(onFail);
},
doDeletionVote(api, pageInfo, voteLine, editSummary, $dialog, onFail) {
api.get({
action: 'query',
prop: 'revisions',
titles: pageInfo.targetPage,
rvprop: 'content',
rvsection: pageInfo.sectionId,
formatversion: 2
}).then((res) => {
const page = res.query.pages[0];
if (!page || !page.revisions || !page.revisions[0]) {
throw new Error('failed to fetch section content from target page');
}
const sectionText = page.revisions[0].content;
const currentUser = mw.config.get('wgUserName');
const userVotePattern = new RegExp('\\{\\{(support|oppose|strong support|weak support|strong oppose|weak oppose|neutral|moral support|s|ss|o|)\\}\\}[^~]*' + mw.util.escapeRegExp(currentUser), 'i');
if (userVotePattern.test(sectionText)) {
if (!confirm(this.config.messages.confirmPossibleDouble)) {
const $voteButton = $('#easyvote-BN-button');
$voteButton.removeClass('loading').prop('disabled', false).text(this.config.messages.windowButton);
$voteButton.find('.easyvote-BN-spinner').remove();
return;
}
}
return api.postWithEditToken({
action: 'edit',
title: pageInfo.targetPage,
section: pageInfo.sectionId,
appendtext: voteLine,
summary: editSummary,
token: mw.user.tokens.get('csrfToken')
});
}).then(() => {
setTimeout(() => {
window.location.reload();
}, 1500);
}).catch(onFail);
}
};
EasyVoteBN.init();
});
// </nowiki>
3ruqcziincef2uqcuwglo8i4zltyowj
749660
749658
2026-07-03T13:06:45Z
Borhan
54019
Blanked the page
749660
javascript
text/javascript
phoiac9h4m842xq45sp7s6u21eteeq1
Virus del papilloma umano
0
164741
749740
690721
2026-07-03T19:18:37Z
Solidest
54422
[[Википедия:К удалению/3 июля 2026#Tests|номинация к удалению]] ([[Википедия:Гаджеты/Remover|Remover]])
749740
wikitext
text/x-wiki
<noinclude>{{к удалению|2026-07-03}}</noinclude>
ser seser swe rtaw eaw e awderaw ea awedaweawea weaqeaqe qaw awa qea we. aweawe w
[[Category:Papillomavirus]]
[[Category:Carcinogeni]]
i9hv26kn5591wp62krph3rfm7uciuek
AddWarning
0
166935
749745
665097
2026-07-03T19:21:58Z
Solidest
54422
номинация к быстрому удалению: ds Отсроченное пусто или коротко ([[Википедия:Гаджеты/Remover|Remover]])
749745
wikitext
text/x-wiki
<noinclude>{{подст:ds|1=test}}</noinclude>
{{#invoke:AddWarning|main|foo}}
{{#invoke:AddWarning|main|bar}}
6aodyp3za87h645pdszni8t4edr7iml
User:Supertian8
2
166966
749872
744968
2026-07-04T07:15:59Z
Supertian8
67751
Test bot edit
749872
wikitext
text/x-wiki
<div style="
position: relative;
position:absolute;
bottom: -1028px;
">FOOBAR</div>Test
csjz1m8r9f5r8l282vtb1hedajo86ck
Redirect testing
0
167345
749739
744286
2026-07-03T19:18:36Z
Solidest
54422
[[Википедия:К удалению/3 июля 2026#Tests|номинация к удалению]] ([[Википедия:Гаджеты/Remover|Remover]])
749739
wikitext
text/x-wiki
<noinclude>{{к удалению|2026-07-03}}</noinclude>
0.8886166738963017
n3bhea7b4g8c1jn36o48lbrscpdxn0e
Template:Starter kit/Featured picture
10
174429
749876
735009
2026-07-04T08:00:10Z
Iiirxs
49827
749876
wikitext
text/x-wiki
<div style="border:1px solid #ddcef2;border-radius:4px;padding:8px;background:#faf5ff;margin-top:4px;">
<div style="padding:4px 12px;margin-bottom:8px;border:1px solid #afa3bf;border-radius:4px;background:#ddcef2;">'''Featured picture'''</div>
<!-- Add any image that represents your wiki well — from Commons or locally uploaded.
No need to update daily — refresh whenever a good image is available.
To add the image: [[File:Filename.jpg|320px|left|alt=description]] -->
[[File:Africa Nok Male Figure Kimbell.jpg|100px|left|alt=Ghwughwu-vap a̱pyia̱ a̱sam]]
Add a description of the image here (2–3 sentences).
<small>''Credit: Add photographer or source credit here''</small>
</div>
[[Category:Starter Kit templates]][[Category:Main page templates]]
qd0sd2zkpqssani30futtxv3wis8udi
User:Solidest/remover-core.js
2
174846
749700
748659
2026-07-03T18:55:49Z
Solidest
54422
749700
javascript
text/javascript
/**
* Remover — ядро (core).
* Загружается лениво при первом клике по пункту меню.
* Ожидает, что window.RemoverState уже задан remover-loader.js.
*
* Архитектура: единый реестр OPERATIONS описывает все операции (КБУ, КУ, КПМ, КУЛ, КОБ, КРАЗД, ВУС, Защита, Запрос, Снятие, кат-варианты).
* Логика выполнения сосредоточена в универсальных обработчиках.
* Экспортирует: window.RemoverCore.handleMenuClick(item, event)
*/
(function () {
'use strict';
var state = window.RemoverState;
if (!state) { console.error('RemoverCore: window.RemoverState не задан.'); return; }
var mwCfg = state.mwCfg;
var cfg = applyCoreConfigDefaults(state.cfg || {});
var isCategory = state.isCategory;
var isVector22 = state.isVector22;
var scriptLink = cfg.scriptLink;
var settingsOptionName = state.settingsOptionName || 'userjs-remover-settings';
var settingsVersion = 1;
var settingsMenuMeta = collectSettingsMenuMeta();
var settingsArticleItemLabels = settingsMenuMeta.articleLabels;
var settingsCategoryItemLabels = settingsMenuMeta.categoryLabels;
var settingsItemLabelById = settingsMenuMeta.idToLabel;
var settingsItemLabelByNorm = settingsMenuMeta.labelByNorm;
var settingsItemLabelOrder = settingsMenuMeta.labelOrder;
var settingsDefaults = getDefaultSettings();
var MENU_TITLE_PRESET_CACTIONS = '__remover_portlet_cactions__';
var MENU_TITLE_PRESET_PAGE = '__remover_portlet_page__';
var MENU_TITLE_PRESET_TOOLS = '__remover_portlet_tools__';
var initialSettings = normalizeRemoverSettings(readSettingsOptionState(state.settings || {}));
var setAlert = ('setAlert' in state) ? !!state.setAlert : initialSettings.notifyAuthor;
var setSubscribe = ('setSubscribe' in state) ? !!state.setSubscribe : initialSettings.subscribeTopic;
var signatureSeparator = ('signatureSeparator' in state && typeof state.signatureSeparator === 'string')
? state.signatureSeparator.trim()
: initialSettings.signatureSeparator;
initialSettings.notifyAuthor = setAlert;
initialSettings.subscribeTopic = setSubscribe;
initialSettings.signatureSeparator = signatureSeparator;
state.cfg = cfg;
state.settings = clonePlainObject(initialSettings);
state.setAlert = setAlert;
state.setSubscribe = setSubscribe;
// ─── Константы ──────────────────────────────────────────────────────────
var MONTHS_NOM = ['Январь','Февраль','Март','Апрель','Май','Июнь','Июль','Август','Сентябрь','Октябрь','Ноябрь','Декабрь'];
var MONTHS_GEN = ['января','февраля','марта','апреля','мая','июня','июля','августа','сентября','октября','ноября','декабря'];
var MONTHS_NOM_LOWER = MONTHS_NOM.map(function (m) { return m.toLowerCase(); });
var T_OPEN = '{' + '{';
var T_CLOSE = '}' + '}';
var KBU_CRITERIA_PAGE = 'Википедия:Критерии быстрого удаления';
var DATE_SECTION_TALK_TEMPLATES = {
ret: 'Оставлено',
withdrawnDeletion: 'Снято с удаления',
redirectedDeletion: {
name: 'Заменено перенаправлением',
sectionParam: 'раздел обсуждения',
singleSectionParam: true
}
};
var TALK_TEMPLATE_ALIASES = {
'Оставлено': ['Оставлена', 'Kept'],
'Заменено перенаправлением': ['Удалено', 'Перенаправлено'],
'Переименовано': ['Renamed'],
'Обсуждавшаяся категория': ['Обсуждалась', 'Обсуждалось']
};
var CATEGORY_NOMINATION_META = {
discuss: { title: 'Номинация: обсуждение', actionText: 'к обсуждению', supportsMulti: true },
deletion: { title: 'Номинация: к удалению', actionText: 'к удалению', supportsMulti: true },
rename: { title: 'Номинация: к переименованию', actionText: 'к переименованию', supportsMulti: true, renameMulti: true },
merge: { title: 'Номинация: к объединению', actionText: 'к объединению' }
};
var RE_ESCAPE = /[.*+?^${}()|[\]\\]/g;
var RE_KU_ON_PAGE = /\{\{\s*(?:к\s*удалению|ку)\s*(?:\||\}\})/i;
var RE_KPM_ON_PAGE = /\{\{\s*(?:к\s*переименованию|кпм|rename)\s*(?:\||\}\})/i;
var RE_KBU_ON_PAGE = /\{\{\s*(?:subst\s*:\s*)?(?:db\s*-[^|}\s]+|уд\s*-[^|}\s]+|к\s*быстрому\s*удалению|к\s*отсроченному\s*удалению|deleteslow|ds|hang\s*-?\s*on)\s*(?:\||\}\})/i;
var RE_KUL_ON_PAGE = /\{\{\s*(?:subst\s*:\s*)?к\s*улучшению\s*(?:\||\}\})/i;
var KBU_PATTERN_STR = 'db\\s*-[^|}\\s]+|уд\\s*-[^|}\\s]+|к\\s*быстрому\\s*удалению|к\\s*отсроченному\\s*удалению|deleteslow|ds';
var RE_KBU_PATTERNS = new RegExp('(?:' + KBU_PATTERN_STR + ')', 'i');
var KUL_PATTERN_STR = 'к\\s*улучшению';
var RE_KUL_PATTERN = new RegExp(KUL_PATTERN_STR, 'i');
var HANGON_PATTERN_STR = 'hang\\s*-?\\s*on';
var RE_HANGON = new RegExp(HANGON_PATTERN_STR, 'i');
var MOVE_TO_COMMONS_TEMPLATE_TITLE = 'Шаблон:Перенести на Викисклад';
var MOVE_TO_COMMONS_TEMPLATE_ALIASES = [
'Перенести на Викисклад',
'На склад',
'To Commons',
'Copy to Wikimedia Commons',
'На викисклад',
'На Викисклад',
'To commons',
'Mtc',
'Перенести в Commons',
'MTC',
'Move to Commons',
'Перенести на викисклад'
];
var BADFAIRUSE_OPTIONS = [
['1', 'несвободная карта'],
['1+', 'карта Google Maps или Google Earth'],
['2', 'несвободное изображение утилитарного объекта'],
['3', 'несвободное изображение животного, растения или гриба'],
['6', 'несвободное изображение на странице-списке эпизодов или серий'],
['7', 'несвободная анимация или отрывок из фильма'],
['10', 'несвободное изображение здания, сооружения и памятника'],
['11', 'несвободная фотография ныне живущего человека'],
['12', 'несвободная схема или чертёж']
];
var DFUD_REASON_OPTIONS = [
['1', 'может быть заменено свободным эквивалентом'],
['2', 'разрешение/качество наносит имущественный ущерб правообладателю'],
['3', 'не отвечает критерию минимального использования или не имеет значимости'],
['4', 'не публиковалось ранее или не имеет проверяемого источника'],
['9', 'используется вне пространства статей или не используется в статьях'],
['10', 'не имеет описания и/или обоснования добросовестности использования']
];
var FILE_DATE_DEADLINE_HINT = 'Срок до возможного удаления — 7 дней с даты установки.';
var RE_DATE_ISO = /^(\d{4})-(\d{2})-(\d{2})$/;
var RE_DATE_RUSSIAN = /^(\d{1,2})\s+([\u0430-\u044f\u0410-\u042f\u0451\u0401.]+)\s+(\d{2}|\d{4})$/;
var RE_DATE_DASH = /^(\d{1,4})\s*-\s*(\d{1,2})\s*-\s*(\d{1,4})$/;
var RE_DATE_DOT = /^(\d{1,2})\s*\.\s*(\d{1,2})\s*\.\s*(\d{2}|\d{4})$/;
var RE_DATE_SLASH = /^(\d{1,4})\s*\/\s*(\d{1,2})\s*\/\s*(\d{1,4})$/;
var RE_NOINCLUDE = /(\s*)<noinclude>([\s\S]*?)<\/noinclude>/i;
var RE_TEMPLATE_NS = /^(?:шаблон|template)\s*:\s*/i;
// ─── Глобальные переменные сессии ────────────────────────────────────────
var isError = false;
var logStatusSeq = 0;
var resizeObservers = [];
var modalLayoutSyncHandlers = [];
var tplAliasCache = {};
// ─── Стили ───────────────────────────────────────────────────────────────
var stStyles = cfg.modalStyles;
var tk = {
cBase: 'var(--color-base, #202122)',
cSub: 'var(--color-subtle, #72777d)',
cSubM: 'var(--color-subtle, #54595d)',
cInv: 'var(--color-inverted-fixed, #fff)',
cProg: 'var(--color-progressive, #3366cc)',
cProgH: 'var(--color-progressive--hover, #2a4b8d)',
cDang: 'var(--color-destructive, #d73333)',
cDis: 'var(--color-disabled, var(--color-subtle, #72777d))',
bgBase: 'var(--background-color-base, #fff)',
bgNSub: 'var(--background-color-neutral-subtle, #f8f9fa)',
bgN: 'var(--background-color-neutral, #eaecf0)',
bgDis: 'var(--background-color-disabled, var(--background-color-neutral, #eaecf0))',
bgProg: 'var(--background-color-progressive, #3366cc)',
bgProgH:'var(--background-color-progressive--hover, #2a4d8f)',
bgSucc: 'var(--background-color-success, #14866d)',
bgSuccH:'var(--background-color-success--hover, #0f6d57)',
bSub: 'var(--border-color-subtle, #a2a9b1)',
bSubS: 'var(--border-color-subtle, #ddd)',
bDis: 'var(--border-color-disabled, var(--border-color-subtle, #a2a9b1))',
bProg: 'var(--border-color-progressive, #3366cc)',
bProgH: 'var(--border-color-progressive--hover, #2a4d8f)',
bSucc: 'var(--border-color-success, #14866d)',
bSuccH: 'var(--border-color-success--hover, #0f6d57)'
};
var sz = {
taH: '180px',
taMinH: '100px',
taMinW: '180px',
mobileBp: 720,
modalRatio: 0.4,
modalMinWide: 420,
modalDefaultWide: 720,
viewportGap: 24,
touchDesktopGap: 120
};
var btnBase = 'border-radius:4px;padding:8px 16px;cursor:pointer;font-size:14px;font-family:inherit;transition:background .1s;';
var neutralVis = 'background:' + tk.bgNSub + ';border:1px solid ' + tk.bSub + ';color:' + tk.cBase + ';border-radius:4px;';
var stCancel = neutralVis + btnBase;
var stSubmit = 'background:' + tk.bgProg + ';color:' + tk.cInv + ';border:1px solid ' + tk.bProg + ';' + btnBase;
var stReload = 'background:' + tk.bgSucc + ';color:' + tk.cInv + ';border:1px solid ' + tk.bSucc + ';' + btnBase;
var stInputBox = 'flex:1;padding:6px;box-sizing:border-box;min-width:0;border:1px solid ' + tk.bSub + ';background:' + tk.bgBase + ';color:inherit;border-radius:2px;';
var stInputFull= 'width:100%;padding:6px;box-sizing:border-box;border:1px solid ' + tk.bSub + ';border-radius:2px;background:' + tk.bgBase + ';color:inherit;margin-bottom:10px;';
var stRow = 'display:flex;margin-bottom:6px;';
var inlineControlGap = 4;
var squareControlSize = 32;
var leftNestedControlOffset = (squareControlSize + inlineControlGap) + 'px';
var stToolBtn = neutralVis + 'padding:4px 8px;margin-left:' + inlineControlGap + 'px;cursor:pointer;font-size:12px;';
var stRemoveBtn= neutralVis + 'display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;padding:0;width:' + squareControlSize + 'px;height:' + squareControlSize + 'px;margin-left:' + inlineControlGap + 'px;cursor:pointer;font-size:12px;line-height:1;';
var stFooterWrap = 'display:flex;align-items:center;gap:8px;flex-wrap:wrap;';
var stFooterChecks = 'display:flex;flex-direction:column;gap:4px;margin-right:auto;flex:1 1 220px;min-width:0;';
var stFooterCheckLabel = 'display:inline-flex;align-items:flex-start;gap:6px;font-size:14px;line-height:1.4;max-width:100%;';
var stFooterActions = 'display:flex;gap:6px;flex:1 1 auto;min-width:0;max-width:100%;flex-wrap:wrap;justify-content:flex-end;margin-left:auto;';
var stHeaderIconBtn = 'margin:0 0 0 auto;width:32px;min-width:32px;height:32px;padding:0;display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;border:1px solid ' + tk.bSub + ';border-radius:4px;background:' + tk.bgNSub + ';color:' + tk.cSubM + ';cursor:pointer;font-size:16px;line-height:1;';
var multiNominationGap = '6px';
var RESIZE_CLASS = 'rm-resizable';
// ═══════════════════════════════════════════════════════════════════════════
// РЕЕСТР ОПЕРАЦИЙ
// Каждая запись описывает одну кнопку меню. Поля:
// id — идентификатор (совпадает с item.id из loader)
// handler — имя метода-обработчика в объекте handlers
// handlerArg — аргумент, передаваемый в handler (опционально)
// ═══════════════════════════════════════════════════════════════════════════
var OPERATIONS = [
// ── Статьи ──────────────────────────────────────────────────────────
{
id: 'fRm',
label: 'КБУ',
handler: 'showKbu',
// Параметры номинации: заполняются при submit
nomination: {
pageTitle: function (pg) { return normTitle(pg); },
// шаблон встраивается в статью, номинационная страница отсутствует
inArticle: true
}
},
{
id: 'tRm',
label: 'КУ',
handler: 'showNomination',
nomination: {
comment: 'к удалению',
template: 'к удалению',
navTemplate: 'КУ',
nomPage: function (date) { return 'Википедия:К удалению/' + date; },
supportsMulti: true,
multiOpId: 'mRm',
supportsTransfer: true,
// шаблон встраивается в статью через <noinclude>
inArticle: true,
articleTpl: function (tplpar, date) { return 'к удалению|' + date + (tplpar ? '|' + tplpar : ''); }
}
},
{
id: 'rnm',
label: 'КПМ',
handler: 'showNomination',
nomination: {
comment: 'к переименованию',
template: 'к переименованию',
navTemplate: 'КПМ',
nomPage: function (date) { return 'Википедия:К переименованию/' + date; },
supportsMulti: true,
multiOpId: 'mRnm',
inArticle: true,
articleTpl: function (tplpar, date) { return 'к переименованию|' + date + (tplpar ? '|' + tplpar : ''); },
extraInput: {
type: 'rename',
firstId: 'rmRenameFirst', inputClass: 'rmRenameInput',
firstPh: 'Новое название',
addBtnId: 'rmAddRename', addBtnLabel: 'Добавить вариант',
containerId: 'rmRenameContainer', addPh: 'Дополнительный вариант',
maxRows: 2, maxMsg: 'Максимум 3 варианта переименования.'
}
}
},
{
id: 'imp',
label: 'КУЛ',
handler: 'showNomination',
nomination: {
comment: 'к срочному улучшению',
template: 'к улучшению',
navTemplate: 'КУЛ',
nomPage: function (date) { return 'Википедия:К улучшению/' + date; },
inArticle: true,
articleTpl: function (tplpar, date) { return 'к улучшению|' + date + (tplpar ? '|' + tplpar : ''); }
}
},
{
id: 'merge',
label: 'КОБ',
handler: 'showNomination',
nomination: {
comment: 'к объединению с другой',
summaryAction: 'к объединению',
template: 'к объединению',
navTemplate: 'КОБ',
nomPage: function (date) { return 'Википедия:К объединению/' + date; },
inArticle: true,
articleTpl: function (tplpar, date) { return 'к объединению|' + date + (tplpar ? '|' + tplpar : ''); },
extraInput: {
type: 'merge',
firstId: 'rmMergeFirst', inputClass: 'rmMergeInput',
firstPh: 'Объединить с…',
addBtnId: 'rmAddMerge', addBtnLabel: '+',
containerId: 'rmMergeContainer', addPh: 'Дополнительная статья',
maxRows: 10, maxMsg: 'Максимум 11 статей для объединения.'
}
}
},
{
id: 'split',
label: 'КРАЗД',
handler: 'showNomination',
nomination: {
comment: 'к разделению',
template: 'к разделению',
navTemplate: 'КР',
nomPage: function (date) { return 'Википедия:К разделению/' + date; },
inArticle: true,
articleTpl: function (tplpar, date) { return 'к разделению|' + date + (tplpar ? '|' + tplpar : ''); },
extraInput: {
type: 'split',
firstId: 'rmSplitFirst', inputClass: 'rmSplitInput',
firstPh: 'Разделить на…',
addBtnId: 'rmAddSplit', addBtnLabel: '+',
containerId: 'rmSplitContainer', addPh: 'Дополнительная статья'
}
}
},
{
id: 'recov',
label: 'ВУС',
handler: 'showNomination',
nomination: {
comment: '',
template: 'к восстановлению',
navTemplate: 'ВУС',
nomPage: function (date) { return 'Википедия:К восстановлению/' + date; },
inArticle: false // шаблон не ставится в (удалённую) статью
}
},
{
id: 'close',
label: 'Снятие',
handler: 'showArticleClose'
},
// ── Запросы ─────────────────────────────────────────────────────────
{
id: 'protect',
label: 'Защита',
handler: 'showReport',
reportMode: 'protect'
},
{
id: 'request',
label: 'Запрос',
handler: 'showReport',
reportMode: 'request'
},
// ── Категории ────────────────────────────────────────────────────────
{
id: 'cat-fRm',
label: 'КБУ',
handler: 'showKbu',
forCategory: true
},
{
id: 'cat-discuss',
label: 'Обсудить',
handler: 'showCatNomination',
catType: 'discuss'
},
{
id: 'cat-delete',
label: 'Удалить',
handler: 'showCatNomination',
catType: 'deletion'
},
{
id: 'cat-rename',
label: 'Переименовать',
handler: 'showCatNomination',
catType: 'rename'
},
{
id: 'cat-merge',
label: 'Объединить',
handler: 'showCatNomination',
catType: 'merge'
},
{
id: 'cat-done',
label: 'Снятие',
handler: 'showCatClose'
}
];
// Быстрый поиск по id
var OPERATIONS_MAP = {};
OPERATIONS.forEach(function (op) { OPERATIONS_MAP[op.id] = op; });
// ─── Тексты переноса (КБУ → КУ) ─────────────────────────────────────────
var transferTexts = {
kbu: { notice: 'Перенесено с быстрого удаления.', hint: 'Шаблоны КБУ и Hangon будут сняты.' },
kul: { notice: 'Перенесено с КУЛ.', hint: 'Шаблоны КУЛ будут сняты.' },
both: { notice: 'Перенесено с быстрого удаления и КУЛ.', hint: 'Шаблоны КБУ, КУЛ и Hangon будут сняты.' }
};
// ═══════════════════════════════════════════════════════════════════════════
// УТИЛИТЫ
// ═══════════════════════════════════════════════════════════════════════════
function escapeRegExp(s) { return s.replace(RE_ESCAPE, '\\$&'); }
function escapeHtml(s) {
return String(s || '')
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
.replace(/"/g, '"').replace(/'/g, ''');
}
function joinHtml(parts) { return parts.join(''); }
function ucfirst(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : ''; }
function padTwo(n) { return n < 10 ? '0' + n : String(n); }
function expandTwoDigitYear(value) {
return 2000 + parseInt(value, 10);
}
function monthToNumber(name) {
var lower = name.toLowerCase().replace(/\.$/, '');
var idx = MONTHS_GEN.indexOf(lower);
if (idx === -1) idx = MONTHS_NOM_LOWER.indexOf(lower);
if (idx === -1 && lower.length >= 3) {
for (var i = 0; i < MONTHS_GEN.length; i++) {
if (MONTHS_GEN[i].indexOf(lower) === 0 || MONTHS_NOM_LOWER[i].indexOf(lower) === 0) return i + 1;
}
}
return idx + 1;
}
function makeStandardDate(yearValue, monthValue, dayValue) {
var yearText = String(yearValue || '').trim();
var year = yearText.length === 2 ? expandTwoDigitYear(yearText) : parseInt(yearText, 10);
var month = parseInt(monthValue, 10);
var day = parseInt(dayValue, 10);
var maxDay;
if ((yearText.length !== 2 && yearText.length !== 4) || isNaN(year) || isNaN(month) || isNaN(day) || year < 1 || month < 1 || month > 12 || day < 1) return null;
maxDay = new Date(Date.UTC(year, month, 0)).getUTCDate();
if (day > maxDay) return null;
return year + '-' + padTwo(month) + '-' + padTwo(day);
}
function normalizeIsoDate(value) {
var m = String(value || '').trim().match(RE_DATE_ISO);
return m ? makeStandardDate(m[1], m[2], m[3]) : null;
}
function normalizeTemplateName(name) {
return (name || '').toLowerCase().replace(RE_TEMPLATE_NS, '').replace(/_/g, ' ').replace(/\s+/g, ' ').trim();
}
function toTemplateNameList(names) {
if (Array.isArray(names)) return names;
return String(names || '').split('|');
}
function getTalkTemplateAliases(templateName) {
return TALK_TEMPLATE_ALIASES[templateName] || [];
}
function buildTemplateNamePattern(names) {
var seen = {};
var patterns = [];
toTemplateNameList(names).forEach(function (name) {
var normalized = normalizeTemplateName(String(name || '').replace(/^(?:safe)?subst\s*:\s*/i, ''));
if (!normalized || seen[normalized]) return;
seen[normalized] = true;
patterns.push(escapeRegExp(normalized).replace(/\s+/g, '[ _]*'));
});
return patterns.join('|');
}
function buildTemplateTransclusionRegex(templateName, aliases, flags) {
var names = [templateName].concat(getTalkTemplateAliases(templateName), toTemplateNameList(aliases));
var namePattern = buildTemplateNamePattern(names);
return namePattern
? new RegExp('\\{\\{\\s*(?:(?:safe)?(?:subst|подст)\\s*:\\s*)?(?:(?:шаблон|template)\\s*:\\s*)?(' + namePattern + ')\\s*([^}]*)\\}\\}', flags || 'i')
: null;
}
function getDate(dateString) {
var d = dateString ? new Date(dateString) : new Date();
var iso = d.getUTCFullYear() + '-' + padTwo(d.getUTCMonth() + 1) + '-' + padTwo(d.getUTCDate());
var rus = d.getUTCDate() + ' ' + MONTHS_GEN[d.getUTCMonth()] + ' ' + d.getUTCFullYear();
return [iso, rus];
}
function convertToStandardDate(dateStr) {
var value = String(dateStr || '').replace(/\s+/g, ' ').trim();
var m;
var mo;
var normalized;
m = value.match(RE_DATE_ISO);
if (m) return normalizeIsoDate(value) || '';
m = value.match(RE_DATE_RUSSIAN);
if (m) {
mo = monthToNumber(m[2]);
normalized = mo ? makeStandardDate(m[3], mo, m[1]) : null;
return normalized || '';
}
m = value.match(RE_DATE_DASH);
if (m) {
normalized = makeStandardDate(m[1], m[2], m[3]);
return normalized || '';
}
m = value.match(RE_DATE_DOT);
if (m) {
normalized = makeStandardDate(m[3], m[2], m[1]);
return normalized || '';
}
m = value.match(RE_DATE_SLASH);
if (m) {
normalized = makeStandardDate(m[1], m[2], m[3]);
return normalized || '';
}
return value;
}
function getNamespaceLabel(ns, fallback) {
return (mwCfg.wgFormattedNamespaces && mwCfg.wgFormattedNamespaces[ns]) || fallback;
}
function getTalkPage(pageName) {
var match = /([^:]*:)?(.*)/.exec(pageName);
if (match[1]) {
var ns = mwCfg.wgNamespaceIds && mwCfg.wgNamespaceIds[match[1].slice(0, -1).toLowerCase().replace(/ /g, '_')];
if (ns !== undefined) return getNamespaceLabel(ns | 1, 'Обсуждение') + ':' + match[2];
}
return getNamespaceLabel(1, 'Обсуждение') + ':' + pageName;
}
function normTitle(s) { return (s || '').replace(/_/g, ' '); }
function stripCatPrefix(s) { return (s || '').replace(/^(?:Категория|Category):\s*/i, ''); }
function normalizeRedirectTarget(value) {
var title = normTitle(String(value || '').trim());
var linkMatch = title.match(/^\[\[:?([^|\]]+)(?:\|[^\]]*)?\]\]$/);
return linkMatch ? normTitle(linkMatch[1]).trim() : title;
}
function buildRedirectText(targetTitle) {
return '#REDIRECT [[' + targetTitle + ']]';
}
function getCategoryNamespaceLabel() {
return getNamespaceLabel(14, 'Категория');
}
function normalizeCategoryPageName(value) {
var title = normTitle(value).trim();
var nsMatch, nsKey, ns;
if (!title) return '';
nsMatch = title.match(/^([^:]+):(.+)$/);
if (nsMatch) {
nsKey = nsMatch[1].toLowerCase().replace(/ /g, '_');
ns = mwCfg.wgNamespaceIds && mwCfg.wgNamespaceIds[nsKey];
if (ns === 14 || nsKey === 'category' || nsKey === 'категория') return getCategoryNamespaceLabel() + ':' + nsMatch[2].trim();
return getCategoryNamespaceLabel() + ':' + title;
}
return getCategoryNamespaceLabel() + ':' + title;
}
function makeSummary(text) { return scriptLink + ': ' + text; }
function makeToolSuffixSummary(text) { return text + ' (' + scriptLink + ')'; }
function makeNominationPageSummary(sectionTitle) {
var section = String(sectionTitle || '').replace(/\s+/g, ' ').trim();
return (section ? '/* ' + section + ' */ ' : '') + makeToolSuffixSummary('номинация');
}
function makeNominationTargetSummary(pageTitle, sectionTitle, actionText) {
var section = String(sectionTitle || '').trim();
var target = pageTitle + (section ? '#' + section : '');
var label = 'номинация' + (actionText ? ' ' + actionText : '');
return '[[' + target + '|' + label + ']] (' + scriptLink + ')';
}
function getNominationSummaryAction(nomination) {
return String(nomination && (nomination.summaryAction || nomination.comment || nomination.template) || '')
.trim();
}
function appendNominationSignature(text) {
var body = String(text || '');
return body + (signatureSeparator ? ' ' + signatureSeparator : '') + ' ~~' + '~~';
}
function extractDisplayedText(s) {
return (s || '').replace(/\[\[:?(?:[^|\]]+\|)?(.+?)\]\]/g, '$1');
}
function collectInputValues(selector) {
return $(selector).map(function () { return $(this).val().trim(); }).get().filter(Boolean);
}
function applyCoreConfigDefaults(config) {
var defaults = {
scriptLink: '[[Участник:Solidest/Remover|Remover]]',
fastRemoveReasons: {
general: [
['уд-бессвязно', 'О1 Бессвязный текст'],
['уд-тест', 'О2 Тестовая страница'],
['уд-ванд', 'О3.1 Вандальная страница'],
['уд-мист', 'О3.2 Мистификация'],
['уд-повторно', 'О4 Уже удалялось'],
['уд-автор', 'О5 По просьбе автора'],
['уд-под', 'О6 Ненужная подстраница'],
['уд-переим', 'О7 Для переименования'],
['уд-дубль', 'О8 Дубликат'],
['уд-реклама', 'О9 Реклама или спам'],
['уд-нецелевая', 'О10 Нецелевая СО'],
['уд-копивио', 'О11 Нарушение АП']
],
articles: [
['подст:ds', 'ds Отсроченное пусто или коротко', 'С'],
['уд-пусто', 'С1 Пусто или коротко'],
['уд-иностр', 'С2 Не на русском'],
['уд-ссылки', 'С3 Лишь ссылки'],
['уд-нз', 'С5 Явно незначимо'],
['уд-бям', 'С7 Создано нейросетью']
],
redirects: [
['уд-в никуда', 'П1 Перенапр. в никуда'],
['уд-мпр', 'П2 Межпростр. перенапр.'],
['уд-опечатка', 'П3 Перенапр. с ошибкой в названии'],
['уд-падеж', 'П4 Не именительный падеж'],
['уд-смысл', 'П5 Неверное перенапр.'],
['уд-перобс', 'П6 Перенапр. на СО']
],
files: [
['Db-duplicate', 'Ф1 Копия файла'],
['Db-badimage', 'Ф2 Повреждённый файл'],
['подст:No license', 'Ф3.1 Нет данных о лицензии'],
['подст:No source', 'Ф3.2 Нет данных об источнике'],
['подст:No author', 'Ф3.3 Нет данных об авторе'],
['подст:Disputed', 'Ф3.4 Сомнительные данные файла'],
['подст:No permission', 'Ф3.5 Не подтверждена лицензия'],
['подст:Orphaned-fairuse', 'Ф4 Неиспользуемый КДИ'],
['подст:Disputed-fairuse', 'Ф5 Нет КДИ'],
['Db-badfairuse', 'Ф6 Неоправданное КДИ'],
['Замена с Викисклада', 'Ф7 Заменяемый КДИ'],
['Перенесено на Викисклад', 'Ф8 Есть на Складе'],
['подст:Db-nothost', 'Ф9 Файл — ВП:НЕХОСТИНГ']
],
categories: [
['уд-пусткат', 'К1.1 Пустая категория'],
['уд-служебная', 'К1.2 Разобранная служебная кат.'],
['уд-перекат', 'К2 Переименованная кат.']
],
users: [
['уд-владелец', 'У1 По желанию владельца'],
['уд-анон', 'У2 Устаревшая СО анонима'],
['уд-несущ', 'У3 Несуществующий участник'],
['уд-нецелевое', 'У4 Нецелевое использ. ЛП'],
['уд-неактив', 'У5 Подстраница неактивного']
],
special: [
['db', 'Особый случай']
]
},
fastRemoveCriteriaAnchors: {
'подст:ds': 'С1',
deleteslow: 'С1',
ds: 'С1'
},
requiredParamTemplates: {
'уд-переим': 'страницу, которую нужно переименовать',
'уд-дубль': 'страницу-дубликат',
'уд-копивио': 'URL источника нарушения АП',
'Db-duplicate': 'имя файла-оригинала',
'Замена с Викисклада': 'имя файла на Викискладе',
'Перенесено на Викисклад': 'имя файла на Викискладе',
'уд-перекат': 'новое название категории',
'db': '!причину удаления'
},
categoryTemplates: {
discuss: 'Обсуждаемая категория|обсуждаемая категория|Acat|acat|ОКТО|окто|Категория к обсуждению|категория к обсуждению',
rename: 'Категория к переименованию|категория к переименованию|Anacat|anacat',
merge: 'Категория к объединению|категория к объединению|Amergecat|amergecat|Cfm|cfm',
discussed: 'Обсуждавшаяся категория|обсуждавшаяся категория|Обсуждалась|обсуждалась|Обсуждалось|обсуждалось'
},
modalStyles: {
border: '1px solid var(--border-color-progressive, #3366bb)',
background: 'var(--background-color-base, #f8f9fa)',
borderRadius: '6px',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
headerColor: 'var(--color-progressive, #3366bb)'
}
};
config.scriptLink = config.scriptLink || defaults.scriptLink;
config.fastRemoveReasons = $.extend({}, defaults.fastRemoveReasons, config.fastRemoveReasons || {});
config.fastRemoveCriteriaAnchors = $.extend({}, defaults.fastRemoveCriteriaAnchors, config.fastRemoveCriteriaAnchors || {});
config.requiredParamTemplates = $.extend({}, defaults.requiredParamTemplates, config.requiredParamTemplates || {});
config.categoryTemplates = $.extend({}, defaults.categoryTemplates, config.categoryTemplates || {});
config.modalStyles = $.extend({}, defaults.modalStyles, config.modalStyles || {});
return config;
}
function clonePlainObject(obj) {
return JSON.parse(JSON.stringify(obj || {}));
}
function normalizeMenuLabel(value) {
return String(value || '').replace(/\s+/g, ' ').trim().toLowerCase();
}
function readSettingsOptionState(fallback) {
var base = clonePlainObject(fallback || {});
var stored;
var raw = null;
if (!mw.user || !mw.user.options || typeof mw.user.options.get !== 'function') return base;
stored = mw.user.options.get(settingsOptionName);
if (typeof stored === 'string' && stored.trim()) {
try {
raw = JSON.parse(stored);
} catch (e) {
console.warn('RemoverCore: не удалось прочитать сохранённые настройки полностью, будут использованы данные loader.', e);
}
}
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return base;
return $.extend({}, raw, base, ('quickPhrases' in raw) ? { quickPhrases: raw.quickPhrases } : {});
}
function normalizeQuickPhraseValue(value) {
return String(value == null ? '' : value).replace(/\r\n?/g, '\n').trim();
}
function normalizeQuickPhrasesList(list, fallback) {
var source = Array.isArray(list) ? list : fallback;
var normalized = [];
(source || []).forEach(function (value) {
var phrase = normalizeQuickPhraseValue(value);
if (phrase && normalized.indexOf(phrase) === -1) normalized.push(phrase);
});
return normalized;
}
function collectSettingsMenuMeta() {
var labels = [];
var articleLabels = [];
var categoryLabels = [];
var idToLabel = {};
var labelByNorm = {};
var labelOrder = {};
function collect(items, targetLabels) {
items.forEach(function (item) {
var id;
var label;
var normLabel;
if (!item || item.type === 'separator') return;
id = String(item.id || '').trim();
label = String(item.label || '').trim();
normLabel = normalizeMenuLabel(label);
if (id && label && !(id in idToLabel)) idToLabel[id] = label;
if (label && targetLabels.indexOf(label) === -1) targetLabels.push(label);
if (normLabel && !(normLabel in labelByNorm)) {
labelByNorm[normLabel] = label;
labelOrder[label] = labels.length;
labels.push(label);
}
});
}
collect(cfg.articleMenuItems, articleLabels);
collect(cfg.categoryMenuItems, categoryLabels);
return {
labels: labels,
articleLabels: articleLabels,
categoryLabels: categoryLabels,
idToLabel: idToLabel,
labelByNorm: labelByNorm,
labelOrder: labelOrder
};
}
function buildSettingsMenuItemsHint() {
var parts = [];
if (settingsArticleItemLabels.length) {
parts.push('<div class="rmSettingsHintRow"><span class="rmSettingsHintBadge">Статьи</span>' + escapeHtml(settingsArticleItemLabels.join(', ')) + '.</div>');
}
if (settingsCategoryItemLabels.length) {
parts.push('<div class="rmSettingsHintRow"><span class="rmSettingsHintBadge">Категории</span>' + escapeHtml(settingsCategoryItemLabels.join(', ')) + '.</div>');
}
return parts.length ? '<div class="rmSettingsHintList">' + parts.join('') + '</div>' : '';
}
function getDefaultSettings() {
return {
version: settingsVersion,
excludedNamespaces: normalizeNumberList(cfg.excludedNamespaces, []),
notifyAuthor: !!cfg.defaultNotifyAuthor,
subscribeTopic: !!cfg.defaultSubscribeTopic,
autoReloadAfterAction: false,
openNominationDiscussionInNewTab: false,
menuTitle: (typeof cfg.menuTitle === 'string' && cfg.menuTitle.trim()) ? cfg.menuTitle.trim() : 'Remover',
disabledItems: [],
quickPhrases: normalizeQuickPhrasesList(cfg.quickPhrases, []),
showMenuIcons: !!cfg.showMenuIcons,
signatureSeparator: (typeof cfg.signatureSeparator === 'string') ? cfg.signatureSeparator.trim() : ''
};
}
function normalizeNumberList(list, fallback) {
var source = Array.isArray(list) ? list : fallback;
return source
.map(function (value) { return parseInt(value, 10); })
.filter(function (value, index, arr) { return !isNaN(value) && arr.indexOf(value) === index; })
.sort(function (a, b) { return a - b; });
}
function normalizeDisabledItemValue(value) {
var token = String(value || '').trim();
if (!token) return null;
if (settingsItemLabelById[token]) return settingsItemLabelById[token];
return settingsItemLabelByNorm[normalizeMenuLabel(token)] || null;
}
function compareSettingsMenuLabels(a, b) {
var ai = Object.prototype.hasOwnProperty.call(settingsItemLabelOrder, a) ? settingsItemLabelOrder[a] : Number.MAX_SAFE_INTEGER;
var bi = Object.prototype.hasOwnProperty.call(settingsItemLabelOrder, b) ? settingsItemLabelOrder[b] : Number.MAX_SAFE_INTEGER;
if (ai !== bi) return ai - bi;
return a.localeCompare(b, 'ru');
}
function normalizeDisabledItemsList(list, fallback) {
var source = Array.isArray(list) ? list : fallback;
return source
.map(normalizeDisabledItemValue)
.filter(function (value, index, arr) { return value && arr.indexOf(value) === index; })
.sort(compareSettingsMenuLabels);
}
function normalizeMenuTitleSetting(value, fallback) {
var menuTitle = String(value || '').trim();
if (!menuTitle) return fallback;
if (menuTitle === MENU_TITLE_PRESET_CACTIONS || /^(#)?p-cactions$/i.test(menuTitle)) return MENU_TITLE_PRESET_CACTIONS;
if (menuTitle === MENU_TITLE_PRESET_PAGE || /^(#)?p-page$/i.test(menuTitle) || /^(#)?p-actions$/i.test(menuTitle)) return MENU_TITLE_PRESET_PAGE;
if (menuTitle === MENU_TITLE_PRESET_TOOLS || /^(#)?p-tb$/i.test(menuTitle)) return MENU_TITLE_PRESET_TOOLS;
return menuTitle;
}
function normalizeRemoverSettings(raw) {
var defaults = clonePlainObject(settingsDefaults);
var source = (raw && typeof raw === 'object') ? raw : {};
return {
version: settingsVersion,
excludedNamespaces: normalizeNumberList(source.excludedNamespaces, defaults.excludedNamespaces || []),
notifyAuthor: ('notifyAuthor' in source) ? !!source.notifyAuthor : !!defaults.notifyAuthor,
subscribeTopic: ('subscribeTopic' in source) ? !!source.subscribeTopic : !!defaults.subscribeTopic,
autoReloadAfterAction: ('autoReloadAfterAction' in source) ? !!source.autoReloadAfterAction : !!defaults.autoReloadAfterAction,
openNominationDiscussionInNewTab: ('openNominationDiscussionInNewTab' in source) ? !!source.openNominationDiscussionInNewTab : !!defaults.openNominationDiscussionInNewTab,
menuTitle: normalizeMenuTitleSetting(
(typeof source.menuTitle === 'string' && source.menuTitle.trim()) ? source.menuTitle : '',
typeof defaults.menuTitle === 'string' && defaults.menuTitle.trim() ? defaults.menuTitle.trim() : 'Remover'
),
disabledItems: normalizeDisabledItemsList(source.disabledItems, defaults.disabledItems || []),
quickPhrases: normalizeQuickPhrasesList(source.quickPhrases, defaults.quickPhrases || []),
showMenuIcons: ('showMenuIcons' in source) ? !!source.showMenuIcons : !!defaults.showMenuIcons,
signatureSeparator: (typeof source.signatureSeparator === 'string')
? source.signatureSeparator.trim()
: (typeof defaults.signatureSeparator === 'string' ? defaults.signatureSeparator.trim() : '')
};
}
function areRemoverSettingsEqual(a, b) {
return JSON.stringify(normalizeRemoverSettings(a)) === JSON.stringify(normalizeRemoverSettings(b));
}
function updateStoredSettingsState(settings, skipUserOptionsSync) {
var normalized = normalizeRemoverSettings(settings);
state.settings = clonePlainObject(normalized);
setAlert = normalized.notifyAuthor;
setSubscribe = normalized.subscribeTopic;
signatureSeparator = normalized.signatureSeparator;
state.setAlert = setAlert;
state.setSubscribe = setSubscribe;
if (!skipUserOptionsSync && mw.user && mw.user.options && typeof mw.user.options.set === 'function') {
mw.user.options.set(settingsOptionName, JSON.stringify(normalized));
}
return normalized;
}
function splitSettingsInput(value) {
return String(value || '')
.split(/[\s,;]+/)
.map(function (part) { return part.trim(); })
.filter(Boolean);
}
function splitSettingsListInput(value) {
return String(value || '')
.split(/[\n,;]+/)
.map(function (part) { return part.trim(); })
.filter(Boolean);
}
function parseNamespaceInput(value) {
var tokens = splitSettingsInput(value);
var invalid = [];
var values = [];
tokens.forEach(function (token) {
var parsed = parseInt(token, 10);
if (String(parsed) !== token) invalid.push(token);
else if (values.indexOf(parsed) === -1) values.push(parsed);
});
values.sort(function (a, b) { return a - b; });
return { values: values, invalid: invalid };
}
function parseDisabledItemsInput(value) {
var tokens = splitSettingsListInput(value);
var invalid = [];
var values = [];
tokens.forEach(function (token) {
var normalized = normalizeDisabledItemValue(token);
if (!normalized) invalid.push(token);
else if (values.indexOf(normalized) === -1) values.push(normalized);
});
values.sort(compareSettingsMenuLabels);
return { values: values, invalid: invalid };
}
function formatItemsWithAnd(items) {
var list = (items || []).filter(Boolean);
if (!list.length) return '';
if (list.length === 1) return list[0];
return list.slice(0, -1).join(', ') + ' и ' + list[list.length - 1];
}
function formatPagesWithAnd(names, prefix) {
var p = prefix || ':';
var links = (names || []).map(function (n) { return '[[' + p + n + ']]'; });
return formatItemsWithAnd(links);
}
function asNonEmptyArray(value) {
return (Array.isArray(value) ? value : (value ? [value] : [])).filter(Boolean);
}
function buildRenameTemplateParam(targetNames) {
var list = asNonEmptyArray(targetNames);
if (!list.length) return '';
return list[0] + (list.length > 1 ? '||' + list.slice(1).join('|') : '');
}
function collectRenameTargetsFromTemplateParams(params) {
return (params || []).map(function (value) {
return String(value || '').trim();
}).filter(Boolean);
}
function formatRenameItemLabel(pageName, targetName) {
var targets = asNonEmptyArray(targetName);
return '[[:' + pageName + ']]' + (targets.length
? ' → ' + targets.map(function (name) { return '[[:' + name + ']]'; }).join(', ')
: '');
}
function buildRenameItemLabelFormatter(targetsByPage) {
var targets = targetsByPage || {};
return function (pageName) {
return formatRenameItemLabel(pageName, targets[normTitle(pageName)] || '');
};
}
function formatRenameItemsWithAnd(pages, targetsByPage) {
var formatItem = buildRenameItemLabelFormatter(targetsByPage);
var links = (pages || []).map(function (pageName) { return formatItem(pageName); });
return formatItemsWithAnd(links);
}
function normalizeCategoryTargetName(value) {
return normTitle(stripCatPrefix(value)).trim();
}
function normalizeCategoryTargetPageName(value) {
var title = normalizeCategoryTargetName(value);
return title ? normalizeCategoryPageName(title) : '';
}
function buildMultiRenameTargetMap(pairs, key) {
var map = {};
(pairs || []).forEach(function (pair) {
var value;
if (!pair || !pair.pageName) return;
value = pair[key || 'targetName'];
map[normTitle(pair.pageName)] = Array.isArray(value) ? value.slice() : (value || '');
});
return map;
}
function getMultiRenameTarget(job, pageName, key) {
var map = job && job[key || 'multiRenameTargets'];
return map ? (map[normTitle(pageName)] || '') : '';
}
function collectMultiRenamePairs(options) {
var opts = options || {};
var normalizePage = typeof opts.normalizePageName === 'function' ? opts.normalizePageName : normTitle;
var normalizeTarget = typeof opts.normalizeTargetName === 'function' ? opts.normalizeTargetName : normTitle;
var normalizeTemplateTarget = typeof opts.normalizeTemplateTargetName === 'function' ? opts.normalizeTemplateTargetName : normalizeTarget;
var pairs = [];
$('.rmMultiPageBlock').each(function () {
var $block = $(this);
var pageRaw = ($block.find('.rmMultiPageInput').val() || '').trim();
var targetPairs = collectMultiRenameTargetValues($block).map(function (targetRaw) {
return {
targetName: normalizeTarget(targetRaw),
templateTargetName: normalizeTemplateTarget(targetRaw)
};
}).filter(function (item) { return item.targetName || item.templateTargetName; });
var pageName = normalizePage(pageRaw);
var targetNames = targetPairs.map(function (item) { return item.targetName; }).filter(Boolean);
var templateTargetNames = targetPairs.map(function (item) { return item.templateTargetName; }).filter(Boolean);
if (!pageName && !targetNames.length && !templateTargetNames.length) return;
pairs.push({
pageName: pageName,
targetName: targetNames[0] || '',
templateTargetName: templateTargetNames[0] || '',
targetNames: targetNames,
templateTargetNames: templateTargetNames
});
});
return pairs;
}
function validateMultiRenamePairs(pairs, pageLabel, targetLabel) {
var seen = {};
var pages = pairs || [];
var pageWord = pageLabel || 'страницу';
var targetWord = targetLabel || 'новое название';
if (!pages.length) { alert('Укажите ' + pageWord + '.'); return false; }
for (var i = 0; i < pages.length; i++) {
var targetNames = asNonEmptyArray(pages[i].targetNames || pages[i].targetName);
var templateTargetNames = asNonEmptyArray(pages[i].templateTargetNames || pages[i].templateTargetName);
if (!pages[i].pageName) { alert('Укажите ' + pageWord + '.'); return false; }
if (!targetNames.length || !templateTargetNames.length) { alert('Укажите ' + targetWord + ' для «' + pages[i].pageName + '».'); return false; }
if (targetNames.length > 3 || templateTargetNames.length > 3) { alert('Максимум 3 варианта переименования для «' + pages[i].pageName + '».'); return false; }
if (seen[normTitle(pages[i].pageName)]) { alert('Страница «' + pages[i].pageName + '» указана несколько раз.'); return false; }
seen[normTitle(pages[i].pageName)] = true;
}
return true;
}
function getMultiRenameDiscussionOptions(targetsByPage, extraOptions) {
return $.extend({}, extraOptions || {}, {
formatItemLabel: buildRenameItemLabelFormatter(targetsByPage)
});
}
function formatCatLink(name) { return '[[:' + normalizeCategoryPageName(name) + ']]'; }
function formatMergeStatus(status) {
return { already_exists: 'уже был', updated: 'дополнен', created: 'создан' }[status] || status;
}
function applyGeneratedText($el, generated) {
var cur = $el.val();
var prev = $el.data('rmGenerated') || '';
if (!prev || cur.indexOf(prev) === 0) {
$el.val(generated + cur.slice(prev.length));
} else {
$el.val(generated + (cur ? '\n' + cur : ''));
}
$el.data('rmGenerated', generated);
}
function getCurrentQuickPhrases() {
return normalizeQuickPhrasesList(
state.settings && state.settings.quickPhrases,
settingsDefaults.quickPhrases || []
);
}
function insertTextIntoTextarea($el, text) {
var el = $el && $el[0];
var value;
var start;
var end;
var updatedValue;
var caretPos;
if (!el) return;
text = String(text || '');
if (!text) return;
value = $el.val() || '';
start = typeof el.selectionStart === 'number' ? el.selectionStart : value.length;
end = typeof el.selectionEnd === 'number' ? el.selectionEnd : start;
updatedValue = value.slice(0, start) + text + value.slice(end);
caretPos = start + text.length;
$el.val(updatedValue).trigger('input').trigger('change').focus();
if (typeof el.setSelectionRange === 'function') el.setSelectionRange(caretPos, caretPos);
}
function buildQuickPhrasesPanelHtml(textareaId) {
var phrases = getCurrentQuickPhrases();
if (!phrases.length) return '';
return joinHtml([
'<div class="rmQuickPhrasesPanel ', RESIZE_CLASS, '" data-rm-target="', textareaId, '">',
phrases.map(function (phrase) {
return joinHtml([
'<button type="button" class="rmQuickPhraseActionBtn" data-rm-target="', textareaId,
'" data-rm-phrase="', escapeHtml(phrase), '">',
escapeHtml(phrase),
'</button>'
]);
}).join(''),
'</div>'
]);
}
function getMultiNominationCommentText(commentsByPage, pageTitle) {
var key = normTitle(pageTitle);
if (!commentsByPage || !Object.prototype.hasOwnProperty.call(commentsByPage, key)) return '';
return normalizeQuickPhraseValue(commentsByPage[key]);
}
function hasCommentsForEveryMultiNominationPage(pages, commentsByPage) {
var list = Array.isArray(pages) ? pages.filter(Boolean) : [];
return list.length > 0 && list.every(function (pageName) {
return !!getMultiNominationCommentText(commentsByPage, pageName);
});
}
function validateMultiNominationText(pages, bodyText, commentsByPage, itemGenitive) {
if (normalizeQuickPhraseValue(bodyText) || hasCommentsForEveryMultiNominationPage(pages, commentsByPage)) return true;
alert('Укажите общий текст номинации или комментарий для каждой ' + (itemGenitive || 'страницы') + '.');
return false;
}
function buildMultiNominationText(pages, bodyText, commentsByPage, options) {
var opts = options || {};
var list = Array.isArray(pages) ? pages.filter(Boolean) : [];
var body = normalizeQuickPhraseValue(bodyText);
var hasAllPageComments = hasCommentsForEveryMultiNominationPage(list, commentsByPage);
var headingLevel = Math.max(2, parseInt(opts.headingLevel, 10) || 3);
var headingMarks = new Array(headingLevel + 1).join('=');
var formatItemLabel = typeof opts.formatItemLabel === 'function'
? opts.formatItemLabel
: function (pageName) { return '[[:' + pageName + ']]'; };
var pageSections = list.map(function (pageName, index) {
var comment = getMultiNominationCommentText(commentsByPage, pageName);
var sectionPrefix = (index === 0 && opts.leadingBlankLine === false) ? '' : '\n';
return sectionPrefix + headingMarks + ' ' + formatItemLabel(pageName) + ' ' + headingMarks + '\n' + (comment ? appendNominationSignature(comment) + '\n' : '');
}).join('');
var commonSectionText = body
? appendNominationSignature(body)
: (hasAllPageComments ? '' : appendNominationSignature(''));
return pageSections + (commonSectionText ? '\n' + headingMarks + ' По всем ' + headingMarks + '\n' + commonSectionText : '');
}
function buildMultiNominationListText(pages, bodyText, commentsByPage, options) {
var opts = options || {};
var list = Array.isArray(pages) ? pages.filter(Boolean) : [];
var body = normalizeQuickPhraseValue(bodyText);
var hasAllPageComments = hasCommentsForEveryMultiNominationPage(list, commentsByPage);
var formatItemLabel = typeof opts.formatItemLabel === 'function'
? opts.formatItemLabel
: function (pageName) { return '[[:' + pageName + ']]'; };
var pageLines = list.map(function (pageName) {
var comment = getMultiNominationCommentText(commentsByPage, pageName);
return '* ' + formatItemLabel(pageName) + (comment ? '\n' + appendNominationSignature(comment).split('\n').map(function (line) { return '*: ' + line; }).join('\n') : '');
}).join('\n');
var commonText = body
? appendNominationSignature(body)
: (hasAllPageComments ? '' : appendNominationSignature(''));
return pageLines + (pageLines && commonText ? '\n' : '') + commonText;
}
function collectMultiNominationComments(normalizePageName) {
var comments = {};
var normalize = typeof normalizePageName === 'function' ? normalizePageName : normTitle;
$('.rmMultiPageBlock').each(function () {
var $block = $(this);
var pageName = normalize(($block.find('.rmMultiPageInput').val() || '').trim());
var comment = normalizeQuickPhraseValue($block.find('.rmMultiPageCommentInput').val());
if (!pageName) return;
comments[pageName] = comment;
});
return comments;
}
function getNominationPublishText(job) {
if (job && job.isMulti) return String(job.msg || '');
return appendNominationSignature(job && job.msg);
}
function getNominationConflictRule(job) {
if (!job || job.mode !== 'nominate') return null;
if (job.opId === 'tRm' || job.opId === 'mRm') {
return {
id: 'ku',
label: 'КУ',
namePattern: '(?:к\\s*удалению|ку)',
detect: function (articleText) {
var match = String(articleText || '').match(/\{\{\s*((?:к\s*удалению|ку))\s*(?:\||\}\})/i);
if (!match) return null;
var templateName = ucfirst(String(match[1] || '').replace(/\s+/g, ' ').trim());
return {
label: 'КУ',
templateName: templateName || 'КУ',
templateDisplay: '{{' + (templateName || 'КУ') + '}}'
};
}
};
}
if (job.opId === 'rnm' || job.opId === 'mRnm') {
return {
id: 'kpm',
label: 'КПМ',
namePattern: '(?:к\\s*переименованию|кпм|rename)',
detect: function (articleText) {
var match = String(articleText || '').match(/\{\{\s*((?:к\s*переименованию|кпм|rename))\s*(?:\||\}\})/i);
if (!match) return null;
var templateName = ucfirst(String(match[1] || '').replace(/\s+/g, ' ').trim());
return {
label: 'КПМ',
templateName: templateName || 'КПМ',
templateDisplay: '{{' + (templateName || 'КПМ') + '}}'
};
}
};
}
return null;
}
function detectNominationConflict(articleText, job) {
var rule = getNominationConflictRule(job);
if (!rule || typeof rule.detect !== 'function') return null;
return rule.detect(articleText);
}
function getConflictDecisionForPage(job, pageName) {
var decisions = job && job.conflictDecisions;
var key = normTitle(pageName);
return decisions && decisions[key] ? decisions[key] : null;
}
function getCategoryMergeRe() {
return new RegExp('\\{\\{\\s*(?:' + cfg.categoryTemplates.merge + ')\\s*\\|\\s*([^\\}]+)\\}\\}', 'i');
}
function eachSequential(targets, iteratee, done) {
var i = 0;
(function next(err) {
if (err || i >= targets.length) { done(err || null); return; }
iteratee(targets[i++], next);
}(null));
}
function normalizeSectionForLink(sectionTitle) {
return (sectionTitle || '').trim()
.replace(/\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g, function (_, target, label) {
var v = (label || target || '').trim();
return v.charAt(0) === ':' ? v.slice(1) : v;
})
.replace(/''+/g, '').replace(/\s+/g, ' ').trim();
}
function getViewportWidth() {
return Math.floor(Math.max(
(document.documentElement && document.documentElement.clientWidth) || 0,
(typeof window.innerWidth === 'number' && window.innerWidth) || 0,
$(window).width() || 0
));
}
function getVisualViewportWidth() {
var widths = [];
if (window.visualViewport && typeof window.visualViewport.width === 'number' && window.visualViewport.width > 0) widths.push(window.visualViewport.width);
if (window.screen && window.screen.width > 0) widths.push(window.screen.width);
return widths.length ? Math.floor(Math.min.apply(Math, widths)) : getViewportWidth();
}
function isTouchModalDevice() {
return !!(
(window.matchMedia && window.matchMedia('(pointer: coarse)').matches) ||
('ontouchstart' in window) ||
(navigator.maxTouchPoints && navigator.maxTouchPoints > 0) ||
(navigator.msMaxTouchPoints && navigator.msMaxTouchPoints > 0)
);
}
function getModalLayout() {
var minWidth = parseInt(sz.taMinW, 10) || 180;
var layoutWidth = getViewportWidth();
var visualWidth = getVisualViewportWidth();
var contentWidth = Math.max(minWidth, Math.floor($('#content').width() || $('#content').innerWidth() || $(window).width() || minWidth));
var isMobile = layoutWidth <= sz.mobileBp;
var isTouchDesktop = !isMobile &&
isTouchModalDevice() &&
visualWidth > 0 &&
visualWidth <= sz.mobileBp &&
layoutWidth >= visualWidth + sz.touchDesktopGap;
var useFullWidth = isMobile || isTouchDesktop;
var maxOuterWidth;
var defaultOuterWidth;
var desktopWidth;
if (isMobile) maxOuterWidth = Math.max(minWidth, (visualWidth || contentWidth) - sz.viewportGap);
else if (isTouchDesktop) maxOuterWidth = Math.max(minWidth, contentWidth - 32);
else maxOuterWidth = Math.max(minWidth, Math.min(contentWidth, (visualWidth ? visualWidth - sz.viewportGap : contentWidth)));
desktopWidth = Math.max(minWidth, Math.floor(contentWidth * sz.modalRatio));
if (useFullWidth || desktopWidth < sz.modalMinWide) defaultOuterWidth = contentWidth;
else if (desktopWidth < sz.modalDefaultWide) defaultOuterWidth = sz.modalDefaultWide;
else defaultOuterWidth = desktopWidth;
return {
minWidth: minWidth,
isMobile: isMobile,
isTouchDesktop: isTouchDesktop,
useFullWidth: useFullWidth,
shouldCenter: useFullWidth || mwCfg.skin === 'minerva',
maxOuterWidth: maxOuterWidth,
defaultOuterWidth: Math.min(defaultOuterWidth, maxOuterWidth)
};
}
function getDefaultResizableWidth(frameWidth) {
var layout = getModalLayout();
return Math.max(layout.minWidth, layout.defaultOuterWidth - Math.floor(frameWidth || 0));
}
function getBoxFrameWidth($el) {
function px(prop) {
var n = parseFloat($el.css(prop));
return isNaN(n) ? 0 : n;
}
return px('padding-left') + px('padding-right') + px('border-left-width') + px('border-right-width');
}
// ═══════════════════════════════════════════════════════════════════════════
// API
// ═══════════════════════════════════════════════════════════════════════════
function getApiUrl() {
return (mw.util && typeof mw.util.wikiScript === 'function') ? mw.util.wikiScript('api') : '/w/api.php';
}
function getCsrfTokenValue() {
return (mw.user && mw.user.tokens && typeof mw.user.tokens.get === 'function')
? mw.user.tokens.get('csrfToken')
: null;
}
function storeCsrfToken(token) {
if (!token || !mw.user || !mw.user.tokens || typeof mw.user.tokens.set !== 'function') return;
mw.user.tokens.set({ csrfToken: token });
}
function isValidCsrfToken(token) {
return typeof token === 'string' && !!token && token !== '+\\';
}
function fetchCsrfToken(forceRefresh, callback) {
var cachedToken = getCsrfTokenValue();
if (!forceRefresh && isValidCsrfToken(cachedToken)) {
callback(cachedToken);
return;
}
$.ajax({
url: getApiUrl(),
method: 'GET',
dataType: 'json',
data: { action: 'query', meta: 'tokens', type: 'csrf', format: 'json' }
})
.done(function (data) {
var token = data && data.query && data.query.tokens && data.query.tokens.csrftoken;
if (isValidCsrfToken(token)) {
storeCsrfToken(token);
callback(token);
return;
}
callback(null);
})
.fail(function () {
callback(null);
});
}
function apiReq(params, mode, callback) {
var isWrite = mode === 'edit' || mode === 'discussiontoolssubscribe' || mode === 'options';
function sendRequest(retryWithFreshToken) {
var reqParams = $.extend({}, params, { format: 'json', action: mode });
if (!isWrite) {
$.ajax({ url: getApiUrl(), method: 'GET', data: reqParams, dataType: 'json' })
.done(function (data) { if (callback) callback(data); })
.fail(function (jqXHR, status) {
console.error('Ошибка API: ' + status);
if (callback) callback({ error: { code: 'network', info: status } });
});
return;
}
fetchCsrfToken(!!retryWithFreshToken, function (token) {
if (!isValidCsrfToken(token)) {
if (callback) callback({ error: { code: 'badtoken', info: 'Не удалось получить CSRF-токен.' } });
return;
}
reqParams.token = token;
$.ajax({ url: getApiUrl(), method: 'POST', data: reqParams, dataType: 'json' })
.done(function (data) {
var err = data && data.error;
var isBadToken = err && (err.code === 'badtoken' || /invalid csrf token/i.test(String(err.info || '')));
if (isBadToken && !retryWithFreshToken) {
sendRequest(true);
return;
}
if (callback) callback(data);
})
.fail(function (jqXHR, status) {
console.error('Ошибка API: ' + status);
if (callback) callback({ error: { code: 'network', info: status } });
});
});
}
sendRequest(false);
}
function saveSettingsToServer(settings, callback) {
var normalized = normalizeRemoverSettings(settings);
if (!mwCfg.wgUserName) {
callback({ code: 'notloggedin', info: 'Сохранять настройки можно только после входа в учётную запись.' });
return;
}
apiReq({ optionname: settingsOptionName, optionvalue: JSON.stringify(normalized) }, 'options', function (resp) {
if (resp && resp.options === 'success') {
callback(null, updateStoredSettingsState(normalized));
return;
}
callback((resp && resp.error) || { code: 'options_failed', info: 'Не удалось сохранить настройки.' });
});
}
function resetSettingsOnServer(callback) {
if (!mwCfg.wgUserName) {
callback({ code: 'notloggedin', info: 'Сбрасывать настройки можно только после входа в учётную запись.' });
return;
}
apiReq({ change: settingsOptionName }, 'options', function (resp) {
if (resp && resp.options === 'success') {
callback(null, updateStoredSettingsState(settingsDefaults, true));
return;
}
callback((resp && resp.error) || { code: 'options_failed', info: 'Не удалось сбросить настройки.' });
});
}
function getFirstQueryPage(data) {
var pages = data && data.query && data.query.pages;
if (!pages) return null;
return pages[Object.keys(pages)[0]] || null;
}
function extractRevisionContent(rev) {
if (!rev) return null;
if (typeof rev['*'] === 'string') return rev['*'];
if (rev.slots && rev.slots.main) {
if (typeof rev.slots.main['*'] === 'string') return rev.slots.main['*'];
if (typeof rev.slots.main.content === 'string') return rev.slots.main.content;
}
return null;
}
function makeReadError(apiError, fallbackCode, fallbackInfo) {
var err = apiError || {};
return {
code: err.code || fallbackCode || 'read_failed',
info: err.info || fallbackInfo || 'Не удалось получить содержимое.'
};
}
function getTextWithTimestamp(pageName, callback) {
apiReq({ prop: 'revisions', rvprop: 'content|timestamp', rvslots: 'main', titles: pageName }, 'query', function (data) {
var content;
var page;
if (data && data.error) {
callback(null, null, data.error);
return;
}
if (!data || !data.query || !data.query.pages) {
callback(null, null, { code: 'read_failed', info: 'Некорректный ответ API при чтении страницы.' });
return;
}
page = getFirstQueryPage(data);
if (!page) {
callback(null, null, { code: 'read_failed', info: 'API не вернул данные страницы.' });
return;
}
if (page.invalid !== undefined) {
callback(null, null, { code: 'invalidtitle', info: page.invalidreason || 'Некорректное название страницы.' });
return;
}
if (page.missing !== undefined) {
callback(null, null, null);
return;
}
if (!page.revisions || !page.revisions.length) {
callback(null, null, { code: 'read_failed', info: 'API не вернул ревизии страницы.' });
return;
}
content = extractRevisionContent(page.revisions[0]);
if (content === null) {
callback(null, null, { code: 'content_missing', info: 'API не вернул текст страницы.' });
return;
}
callback(content, page.revisions[0].timestamp || null, null);
});
}
function getText(pageName, callback) {
getTextWithTimestamp(pageName, function (text, baseTimestamp, err) { callback(text, err); });
}
function editPageContent(pageTitle, options, buildFn, callback) {
var opts = options || {};
var maxRetries = Math.max(0, parseInt(opts.editConflictRetries, 10) || 1);
(function attempt(retry) {
getTextWithTimestamp(pageTitle, function (sourceText, baseTimestamp, readErr) {
if (readErr) {
callback(makeReadError(readErr, opts.readErrorCode || 'read_failed', 'Не удалось получить содержимое страницы «' + pageTitle + '».'));
return;
}
if (sourceText === null) {
callback({ code: opts.readErrorCode || 'read_failed', info: opts.readError || 'Страница «' + pageTitle + '» не существует.' });
return;
}
var done = (function () {
var called = false;
return function (result) {
if (called) return;
called = true;
if (!result || result.error) { callback((result && result.error) || { code: 'build_failed', info: 'Не удалось подготовить правку.' }, result && result.meta || null); return; }
if (result.skip) { callback(null, result.meta || null); return; }
if (typeof result.text !== 'string') { callback({ code: 'build_failed', info: 'Не удалось подготовить правку.' }); return; }
var ep = { title: pageTitle, text: result.text, summary: result.summary || opts.summary || '' };
if (opts.watchlist) ep.watchlist = opts.watchlist;
if (opts.assertuser) ep.assertuser = opts.assertuser;
if (opts.createonly) ep.createonly = opts.createonly;
if (opts.useBaseTimestamp !== false && baseTimestamp) ep.basetimestamp = baseTimestamp;
apiReq(ep, 'edit', function (resp) {
var err = resp && resp.error ? resp.error : null;
if (err && err.code === 'editconflict' && retry < maxRetries) { attempt(retry + 1); return; }
callback(err, result.meta || null);
});
};
}());
var maybe = buildFn(sourceText, done);
if (maybe !== undefined) done(maybe);
});
}(0));
}
// ═══════════════════════════════════════════════════════════════════════════
// ШАБЛОНЫ: удаление и вставка
// ═══════════════════════════════════════════════════════════════════════════
function findBalancedTemplateEnd(text, start) {
var depth = 0;
var i = start;
var len = text.length;
while (i < len - 1) {
if (text.charAt(i) === '{' && text.charAt(i + 1) === '{') {
depth++;
i += 2;
continue;
}
if (text.charAt(i) === '}' && text.charAt(i + 1) === '}') {
depth--;
i += 2;
if (depth === 0) return i;
continue;
}
i++;
}
return -1;
}
function splitTemplateTopLevelParts(innerText) {
var parts = [];
var start = 0;
var templateDepth = 0;
var linkDepth = 0;
var i = 0;
var text = String(innerText || '');
while (i < text.length) {
if (text.charAt(i) === '{' && text.charAt(i + 1) === '{') {
templateDepth++;
i += 2;
continue;
}
if (text.charAt(i) === '}' && text.charAt(i + 1) === '}' && templateDepth > 0) {
templateDepth--;
i += 2;
continue;
}
if (text.charAt(i) === '[' && text.charAt(i + 1) === '[') {
linkDepth++;
i += 2;
continue;
}
if (text.charAt(i) === ']' && text.charAt(i + 1) === ']' && linkDepth > 0) {
linkDepth--;
i += 2;
continue;
}
if (text.charAt(i) === '|' && templateDepth === 0 && linkDepth === 0) {
parts.push(text.slice(start, i));
start = i + 1;
}
i++;
}
parts.push(text.slice(start));
return parts;
}
function getTemplateMatchAt(text, start, nameRe) {
var end = findBalancedTemplateEnd(text, start);
var parts;
var rawName;
var name;
if (end < 0) return null;
parts = splitTemplateTopLevelParts(text.slice(start + 2, end - 2));
rawName = String(parts.shift() || '').trim();
name = rawName.replace(/^(?:safe)?(?:subst|подст)\s*:\s*/i, '').replace(RE_TEMPLATE_NS, '').replace(/_/g, ' ').replace(/\s+/g, ' ').trim();
if (!nameRe.test(name)) return null;
return {
start: start,
end: end,
text: text.slice(start, end),
name: rawName,
params: parts.map(function (part) { return part.trim(); })
};
}
function findTemplateByPattern(text, namePattern) {
var source = String(text || '');
var nameRe = new RegExp('^(?:' + namePattern + ')$', 'i');
var i = 0;
var end;
var match;
while (i < source.length - 1) {
if (source.charAt(i) === '{' && source.charAt(i + 1) === '{') {
match = getTemplateMatchAt(source, i, nameRe);
if (match) return match;
end = findBalancedTemplateEnd(source, i);
if (end > i) { i = end; continue; }
}
i++;
}
return null;
}
function hasTemplateWithDateByPattern(text, namePattern, dateIso) {
var source = String(text || '');
var nameRe = new RegExp('^(?:' + namePattern + ')$', 'i');
var targetDate = convertToStandardDate(dateIso);
var i = 0;
var end;
var match;
var templateDate;
if (!targetDate) return false;
while (i < source.length - 1) {
if (source.charAt(i) === '{' && source.charAt(i + 1) === '{') {
match = getTemplateMatchAt(source, i, nameRe);
if (match) {
templateDate = convertToStandardDate(String(match.params[0] || '').replace(/^\s*1\s*=\s*/, ''));
if (templateDate === targetDate) return true;
i = match.end;
continue;
}
end = findBalancedTemplateEnd(source, i);
if (end > i) { i = end; continue; }
}
i++;
}
return false;
}
function getTemplateRemovalRange(text, match) {
var start = match.start;
var end = match.end;
var before = text.slice(0, start).match(/<noinclude>\s*$/i);
var after;
if (before) {
after = text.slice(end).match(/^\s*<\/noinclude>\s*\n?/i);
if (after) {
return { start: before.index, end: end + after[0].length };
}
}
after = text.slice(end).match(/^[ \t]*(?:\r?\n)?/);
if (after) end += after[0].length;
return { start: start, end: end };
}
function stripTemplatesByPattern(text, namePattern) {
var source = String(text || '');
var nameRe = new RegExp('^(?:' + namePattern + ')$', 'i');
var ranges = [];
var out = [];
var pos = 0;
var i = 0;
var end;
var match;
var range;
while (i < source.length - 1) {
if (source.charAt(i) === '{' && source.charAt(i + 1) === '{') {
match = getTemplateMatchAt(source, i, nameRe);
if (match) {
range = getTemplateRemovalRange(source, match);
ranges.push(range);
i = Math.max(match.end, range.end);
continue;
}
end = findBalancedTemplateEnd(source, i);
if (end > i) { i = end; continue; }
}
i++;
}
if (!ranges.length) return { text: source, removed: false };
ranges.forEach(function (r) {
if (r.start < pos) r.start = pos;
out.push(source.slice(pos, r.start));
pos = r.end;
});
out.push(source.slice(pos));
return { text: out.join(''), removed: true };
}
function removeTemplatesByAliases(text, aliases) {
var pattern = buildTemplateNamePattern(aliases);
if (!pattern) return { text: text, removed: false };
return stripTemplatesByPattern(text, '(?:' + pattern + ')');
}
function normalizeAliasKey(title) {
return String(title || '').replace(/_/g, ' ').toLowerCase().trim();
}
function dedupeAliases(aliases) {
var seen = {};
var result = [];
(aliases || []).forEach(function (alias) {
var key = normalizeAliasKey(alias);
if (!key || seen[key]) return;
seen[key] = true;
result.push(alias);
});
return result;
}
function resolveTemplateRedirectAliases(titles, callback) {
var list = dedupeAliases(titles);
var missingTitles = list.filter(function (t) { return !tplAliasCache[t]; });
function collectAliases() {
var allAliases = [];
list.forEach(function (title) { allAliases = allAliases.concat(tplAliasCache[title] || [title]); });
callback(dedupeAliases(allAliases));
}
if (!missingTitles.length) { collectAliases(); return; }
(function resolveChunk(offset) {
if (offset >= missingTitles.length) { collectAliases(); return; }
var chunk = missingTitles.slice(offset, offset + 20);
var chunkByKey = {};
chunk.forEach(function (t) { chunkByKey[normalizeAliasKey(t)] = t; });
apiReq({ prop: 'redirects', rdlimit: 'max', titles: chunk.join('|') }, 'query', function (resp) {
var pages = resp && resp.query && resp.query.pages ? resp.query.pages : {};
Object.keys(pages).forEach(function (pid) {
var p = pages[pid];
var sourceTitle;
var found;
if (!p || !p.title) return;
sourceTitle = chunkByKey[normalizeAliasKey(p.title)];
if (!sourceTitle) return;
found = [p.title].concat((p.redirects || []).map(function (r) { return r.title; }));
tplAliasCache[sourceTitle] = dedupeAliases(found);
});
chunk.forEach(function (t) { if (!tplAliasCache[t]) tplAliasCache[t] = [t]; });
resolveChunk(offset + 20);
});
}(0));
}
function removeTransferTemplatesLocal(articleText, transferMode) {
var result = { text: articleText, removedKbu: false, removedKul: false, removedHangon: false };
if (transferMode === 'none') return result;
if (transferMode === 'kbu' || transferMode === 'both') {
var kbu = stripTemplatesByPattern(result.text, '(?:' + KBU_PATTERN_STR + ')');
result.text = kbu.text; result.removedKbu = kbu.removed;
}
if (transferMode === 'kul' || transferMode === 'both') {
var kul = stripTemplatesByPattern(result.text, KUL_PATTERN_STR);
result.text = kul.text; result.removedKul = kul.removed;
}
var hangon = stripTemplatesByPattern(result.text, HANGON_PATTERN_STR);
result.text = hangon.text; result.removedHangon = hangon.removed;
return result;
}
function removeTransferTemplatesWithApiFallback(pageName, articleText, transferMode, localResult, callback) {
var needKbu = (transferMode === 'kbu' || transferMode === 'both') && !localResult.removedKbu;
var needKul = (transferMode === 'kul' || transferMode === 'both') && !localResult.removedKul;
var needHangon = transferMode !== 'none' && !localResult.removedHangon;
if (!needKbu && !needKul && !needHangon) { callback(localResult); return; }
var titleMap = {};
if (needKbu) ['Шаблон:К быстрому удалению','Шаблон:К отсроченному удалению','Шаблон:Deleteslow','Шаблон:Ds'].forEach(function (t) { titleMap[t] = true; });
if (needKul) titleMap['Шаблон:К улучшению'] = true;
if (needHangon) { titleMap['Шаблон:Hangon'] = true; titleMap['Шаблон:Hang-on'] = true; }
apiReq({ prop: 'templates', titles: pageName, tllimit: 'max' }, 'query', function (data) {
var page = getFirstQueryPage(data);
if (page && page.templates) {
page.templates.forEach(function (tpl) {
var norm = normalizeTemplateName(tpl.title);
if ((needKbu && (RE_KBU_PATTERNS.test(norm) || norm === 'к быстрому удалению' || norm === 'к отсроченному удалению' || norm === 'deleteslow' || norm === 'ds')) ||
(needKul && RE_KUL_PATTERN.test(norm)) ||
(needHangon && RE_HANGON.test(norm))) {
titleMap[tpl.title] = true;
}
});
}
var titles = Object.keys(titleMap);
if (!titles.length) { callback(localResult); return; }
resolveTemplateRedirectAliases(titles, function (aliases) {
var updated = $.extend({}, localResult);
var r = removeTemplatesByAliases(updated.text, aliases);
updated.text = r.text;
if (r.removed) {
if (needKbu) updated.removedKbu = true;
if (needKul) updated.removedKul = true;
if (needHangon) updated.removedHangon = true;
}
callback(updated);
});
});
}
function removeMoveToCommonsTemplatesLocal(articleText) {
var r = removeTemplatesByAliases(articleText, MOVE_TO_COMMONS_TEMPLATE_ALIASES);
return { text: r.text, removedMoveToCommons: r.removed };
}
function removeMoveToCommonsTemplatesWithApiFallback(pageName, articleText, localResult, callback) {
apiReq({ prop: 'templates', titles: pageName, tllimit: 'max' }, 'query', function (data) {
var page = getFirstQueryPage(data);
var titleMap = {};
var normTarget = normalizeTemplateName(MOVE_TO_COMMONS_TEMPLATE_TITLE);
var titles;
titleMap[MOVE_TO_COMMONS_TEMPLATE_TITLE] = true;
if (page && page.templates) {
page.templates.forEach(function (tpl) {
var norm = normalizeTemplateName(tpl.title);
if (norm === normTarget || MOVE_TO_COMMONS_TEMPLATE_ALIASES.some(function (alias) {
return norm === normalizeTemplateName(alias);
})) {
titleMap[tpl.title] = true;
}
});
}
titles = Object.keys(titleMap);
resolveTemplateRedirectAliases(titles, function (resolvedAliases) {
var aliases = dedupeAliases(MOVE_TO_COMMONS_TEMPLATE_ALIASES.concat(resolvedAliases));
var updated = $.extend({}, localResult);
var r = removeTemplatesByAliases(articleText, aliases);
updated.text = r.text;
updated.removedMoveToCommons = r.removed;
callback(updated);
});
});
}
// ─── Вставка шаблонов ────────────────────────────────────────────────────
function findInsertPositionAfterProjectTemplates(text) {
var pos = 0, len = text.length;
while (pos < len) {
var wsMatch = text.slice(pos).match(/^[\t ]*\n/);
if (wsMatch) { pos += wsMatch[0].length; continue; }
if (text.charAt(pos) !== '{' || text.charAt(pos + 1) !== '{') break;
var afterOpen = text.slice(pos + 2);
var nameMatch = afterOpen.match(/^[\s]*([\s\S]*?)[\s]*(?:\||\}\})/);
var templateEnd;
if (!nameMatch) break;
var tplName = nameMatch[1].toLowerCase().replace(/\s+/g, ' ').trim();
if (!/^статья проекта(\s|$)/.test(tplName) && tplName !== 'блок проектов статьи') break;
templateEnd = findBalancedTemplateEnd(text, pos);
if (templateEnd < 0) break;
pos = templateEnd;
if (pos < len && text.charAt(pos) === '\n') pos++;
}
return pos;
}
function insertTplOnTalkPage(text, tplText, sep) {
var s = (sep === undefined) ? '\n' : sep;
var insertPos = findInsertPositionAfterProjectTemplates(text);
if (insertPos === 0) return tplText + (text.length ? s + text.replace(/^\n+/, '') : '');
var before = text.slice(0, insertPos).replace(/\n+$/, '');
var after = text.slice(insertPos).replace(/^\n+/, '');
return before + '\n' + tplText + (after.length ? s + after : '');
}
function wrapInNoinclude(text, templateText) {
var match = text.match(RE_NOINCLUDE);
if (match) {
// Если перед noinclude есть непробельный контент — вставляем новый noinclude сверху
var before = text.slice(0, text.indexOf(match[0]));
if (/\S/.test(before)) {
return '<noinclude>' + templateText + '</noinclude>\n' + text;
}
var content = match[2];
if (content.length > 0 && content.charAt(content.length - 1) !== '\n') content += '\n';
return text.replace(match[0], match[1] + '<noinclude>' + content + templateText + '\n</noinclude>');
}
return '<noinclude>' + templateText + '</noinclude>\n' + text;
}
function getDateSectionTalkTemplateConfig(closeType) {
var config = DATE_SECTION_TALK_TEMPLATES[closeType];
return typeof config === 'string' ? { name: config } : (config || null);
}
function upsertDateSectionTemplateOnTalkPage(text, templateName, dateIso, sectionTitle, options) {
var source = text || '';
var normalizedSection = String(sectionTitle || '').trim();
var opts = options || {};
var tplRe = buildTemplateTransclusionRegex(templateName, opts.aliases);
var tplMatch = tplRe ? source.match(tplRe) : null;
function buildSectionParam(sectionIndex, currentParams) {
var paramName;
var paramsText = String(currentParams || '');
if (!normalizedSection) return '';
if (opts.singleSectionParam) {
paramName = opts.sectionParam || 'раздел обсуждения';
if (new RegExp('(?:^|\\|)\\s*(?:' + escapeRegExp(paramName) + '|l1)\\s*=', 'i').test(paramsText)) return '';
return '|' + paramName + '=' + normalizedSection;
}
return '|l' + sectionIndex + '=' + normalizedSection;
}
function buildExistingTplWithCanonicalName(suffix) {
return T_OPEN + templateName + String(tplMatch[2] || '').replace(/\s+$/, '') + (suffix || '') + T_CLOSE;
}
if (!tplMatch) {
return { text: insertTplOnTalkPage(source, T_OPEN + templateName + '|' + dateIso + buildSectionParam(1, '') + T_CLOSE, '\n'), status: 'created' };
}
var parts = tplMatch[2].split('|').map(function (p) { return p.trim(); }).filter(Boolean);
var existingDates = parts.filter(function (p) { return p.indexOf('=') === -1; }).map(function (p) { return convertToStandardDate(p); });
var stdDate = convertToStandardDate(dateIso);
if (existingDates.indexOf(stdDate) !== -1) {
var canonicalTpl = buildExistingTplWithCanonicalName('');
if (canonicalTpl !== tplMatch[0]) {
return { text: source.replace(tplMatch[0], canonicalTpl), status: 'updated' };
}
return { text: source, status: 'already_present' };
}
var nextIdx = existingDates.length + 1;
var suffix = '|' + dateIso + buildSectionParam(nextIdx, tplMatch[2]);
return {
text: source.replace(tplMatch[0], buildExistingTplWithCanonicalName(suffix)),
status: 'updated'
};
}
function getNamedTemplateParam(params, names) {
var wanted = {};
names.forEach(function (name) { wanted[String(name).toLowerCase()] = true; });
for (var i = 0; i < params.length; i++) {
var m = String(params[i] || '').match(/^\s*([^=]+?)\s*=\s*([\s\S]*)$/);
if (m && wanted[m[1].toLowerCase().trim()]) return m[2].trim();
}
return '';
}
function getSingleResultTemplateDate(paramsText, templateName) {
var params = String(paramsText || '').split('|').map(function (p) { return p.trim(); }).filter(Boolean);
var namedDate = getNamedTemplateParam(params, templateName === 'Переименовано' ? ['date', 'дата'] : ['date']);
var positionalDate = params.filter(function (p) { return p.indexOf('=') === -1; })[0] || '';
return convertToStandardDate(namedDate || positionalDate);
}
function upsertSingleDateResultTemplateOnTalkPage(text, templateName, dateIso, paramsText) {
var source = text || '';
var tplRe = buildTemplateTransclusionRegex(templateName, null, 'ig');
var tplMatch = null;
var match;
var newTpl = T_OPEN + templateName + '|' + dateIso + (paramsText ? '|' + paramsText : '') + T_CLOSE;
var stdDate = convertToStandardDate(dateIso);
var existingDate;
var canonicalTpl;
if (tplRe) {
while ((match = tplRe.exec(source)) !== null) {
existingDate = getSingleResultTemplateDate(match[2], templateName);
if (existingDate === stdDate) { tplMatch = match; break; }
if (match[0].length === 0) tplRe.lastIndex++;
}
}
if (!tplMatch) {
return { text: insertTplOnTalkPage(source, newTpl, '\n'), status: 'created' };
}
canonicalTpl = T_OPEN + templateName + String(tplMatch[2] || '').replace(/\s+$/, '') + T_CLOSE;
if (canonicalTpl !== tplMatch[0]) {
return { text: source.replace(tplMatch[0], canonicalTpl), status: 'updated' };
}
return { text: source, status: 'already_present' };
}
function buildConditionalRetTemplateText(dateIso, sectionTitle, reasonText, deadlineText, sectionIndex) {
var normalizedSection = String(sectionTitle || '').trim();
var normalizedReason = normalizeQuickPhraseValue(reasonText);
var normalizedDeadline = String(deadlineText || '').trim();
var index = parseInt(sectionIndex, 10);
var tpl = 'Условно оставлено|' + dateIso;
if (isNaN(index) || index < 1) index = 1;
if (normalizedSection) tpl += '|l' + index + '=' + normalizedSection;
if (normalizedReason) tpl += '|пояснение=' + normalizedReason;
if (normalizedDeadline) tpl += '|срок=' + normalizedDeadline;
return T_OPEN + tpl + T_CLOSE;
}
function upsertConditionalRetTemplateOnTalkPage(text, dateIso, sectionTitle, reasonText, deadlineText) {
var source = text || '';
var normalizedSection = String(sectionTitle || '').trim();
var normalizedReason = normalizeQuickPhraseValue(reasonText);
var normalizedDeadline = String(deadlineText || '').trim();
var tplRe = buildTemplateTransclusionRegex('Условно оставлено');
var tplMatch = tplRe ? source.match(tplRe) : null;
function buildExistingTplWithCanonicalName(suffix) {
return T_OPEN + 'Условно оставлено' + String(tplMatch[2] || '').replace(/\s+$/, '') + (suffix || '') + T_CLOSE;
}
if (!tplMatch) {
return {
text: insertTplOnTalkPage(source, buildConditionalRetTemplateText(dateIso, normalizedSection, normalizedReason, normalizedDeadline, 1), '\n'),
status: 'created'
};
}
var parts = tplMatch[2].split('|').map(function (p) { return p.trim(); }).filter(Boolean);
var existingDates = parts.filter(function (p) { return p.indexOf('=') === -1; }).map(function (p) { return convertToStandardDate(p); });
var stdDate = convertToStandardDate(dateIso);
if (existingDates.indexOf(stdDate) !== -1) {
var canonicalTpl = buildExistingTplWithCanonicalName('');
if (canonicalTpl !== tplMatch[0]) {
return { text: source.replace(tplMatch[0], canonicalTpl), status: 'updated' };
}
return { text: source, status: 'already_present' };
}
var nextIdx = existingDates.length + 1;
var suffix = '|' + dateIso;
if (normalizedSection) suffix += '|l' + nextIdx + '=' + normalizedSection;
if (normalizedReason) suffix += '|пояснение=' + normalizedReason;
if (normalizedDeadline) suffix += '|срок=' + normalizedDeadline;
return {
text: source.replace(tplMatch[0], buildExistingTplWithCanonicalName(suffix)),
status: 'updated'
};
}
// ═══════════════════════════════════════════════════════════════════════════
// ПАЙПЛАЙН НОМИНАЦИИ
// ═══════════════════════════════════════════════════════════════════════════
function runNominationPipeline(steps) {
var s = steps;
var ctx = { templateMeta: null, nominationInfo: null };
var stages = [
{
name: 'шаблон',
fn: function (next) {
s.templateStep(function (err, meta) { ctx.templateMeta = meta || null; next(err); });
}
},
{
name: 'номинация',
pendingText: 'Публикуется номинация...',
successText: 'Номинация опубликована.',
errorText: 'Публикация номинации.',
fn: function (next) {
s.nominationStep(function (err, info) { ctx.nominationInfo = info || null; next(err); });
}
},
{
name: 'подписка',
shouldRun: function () {
var info = ctx.nominationInfo;
return !!(setSubscribe && info && info.pageTitle && info.sectionTitle);
},
fn: function (next) {
subscribeToTopic(ctx.nominationInfo.pageTitle, ctx.nominationInfo.sectionTitle, function () { next(); });
}
},
{
name: 'оповещение',
shouldRun: function () { return !!(setAlert && !s.skipNotify); },
fn: function (next) { s.notifyStep(ctx.nominationInfo, next); }
}
];
(function run(i) {
if (i >= stages.length) { if (typeof s.onSuccess === 'function') s.onSuccess(ctx); return; }
var stage = stages[i];
if (typeof stage.shouldRun === 'function' && !stage.shouldRun()) { run(i + 1); return; }
var statusId = stage.pendingText
? logStatus(stage.pendingText, null, { pending: true, trackError: false })
: null;
try {
stage.fn(function (err) {
if (err) {
if (statusId) logStatus(stage.errorText || ('Этап «' + stage.name + '».'), err, { statusId: statusId });
if (typeof s.onFailure === 'function') s.onFailure(stage.name, err, ctx);
else markSubmitError();
return;
}
if (statusId && stage.successText) logStatus(stage.successText, null, { statusId: statusId, trackError: false });
run(i + 1);
});
} catch (ex) {
var exErr = { code: 'exception', info: (ex && ex.message) ? ex.message : String(ex) };
if (statusId) logStatus(stage.errorText || ('Этап «' + stage.name + '».'), exErr, { statusId: statusId });
if (typeof s.onFailure === 'function') s.onFailure(stage.name, exErr, ctx);
else markSubmitError();
}
}(0));
}
// ─── Публикация номинации ────────────────────────────────────────────────
function publishNomination(opts, callback) {
var cb = callback || function () {};
var maxRetries = Math.max(0, parseInt(opts.editConflictRetries, 10) || 1);
function doPublish() {
apiReq({
title: opts.pageTitle,
section: 'new',
sectiontitle: opts.sectionTitle,
summary: opts.summary,
text: opts.text,
assertuser: mwCfg.wgUserName
}, 'edit', function (resp) {
cb(resp && resp.error ? resp.error : null);
});
}
if (opts.sectionTitle) {
if (!opts.navTemplate) { doPublish(); return; }
apiReq({ title: opts.pageTitle, createonly: '1', text: T_OPEN + opts.navTemplate + '-Навигация' + T_CLOSE + '\n', summary: makeSummary('автоматическая шапка'), assertuser: mwCfg.wgUserName },
'edit', function (resp) {
if (resp && resp.error && resp.error.code !== 'articleexists') { cb(resp.error); return; }
doPublish();
});
return;
}
// Вставка в существующую страницу
if (opts.createText !== undefined) {
(function attempt(retry) {
getTextWithTimestamp(opts.pageTitle, function (pageText, baseTimestamp, readErr) {
var result;
var ep;
if (readErr) { cb(makeReadError(readErr, 'read_failed', opts.readErrorMessage || 'Не удалось получить содержимое.')); return; }
result = pageText === null
? (typeof opts.createText === 'function' ? opts.createText() : opts.createText)
: (opts.buildText ? opts.buildText(pageText) : null);
if (typeof result === 'string') result = { text: result };
if (!result || result.error) { cb((result && result.error) || { code: 'build_failed', info: 'Не удалось подготовить правку.' }); return; }
if (result.skip) { cb(null); return; }
if (typeof result.text !== 'string') { cb({ code: 'build_failed', info: 'Не удалось подготовить правку.' }); return; }
ep = {
title: opts.pageTitle,
text: result.text,
summary: result.summary || opts.summary,
assertuser: mwCfg.wgUserName
};
if (pageText === null) ep.createonly = true;
else if (baseTimestamp) ep.basetimestamp = baseTimestamp;
apiReq(ep, 'edit', function (resp) {
var err = resp && resp.error ? resp.error : null;
if (err && (err.code === 'editconflict' || err.code === 'articleexists') && retry < maxRetries) {
attempt(retry + 1);
return;
}
cb(err);
});
});
}(0));
return;
}
editPageContent(opts.pageTitle, { summary: opts.summary, readError: opts.readErrorMessage || 'Не удалось получить содержимое.' },
function (pageText) { return opts.buildText ? opts.buildText(pageText) : null; },
function (err) { cb(err || null); }
);
}
// ─── Оповещение авторов ──────────────────────────────────────────────────
function notifyAuthor(pg, options, callback) {
var opts = options || {};
var cb = callback || function () {};
var actionText = (typeof opts.actionText === 'string') ? opts.actionText : '';
var discussionPage = (typeof opts.discussionPage === 'string') ? opts.discussionPage : '';
var discussionSection = normalizeSectionForLink((typeof opts.discussionSection === 'string') ? opts.discussionSection : '');
var includeProposed = (typeof opts.includeProposedPrefix === 'boolean') ? opts.includeProposedPrefix : true;
var actionPhrase = ((includeProposed ? 'предложена ' : '') + actionText).trim() || 'изменена';
var discussionText = discussionPage ? 'Обсуждение — на странице [[' + discussionPage + (discussionSection ? '#' + discussionSection : '') + ']]. ' : '';
apiReq({ prop: 'revisions', rvprop: 'user', rvdir: 'newer', titles: pg }, 'query', function (queryResp) {
var page = getFirstQueryPage(queryResp);
if (!page) { cb({ code: 'network', info: 'Network error' }); return; }
if (page.missing !== undefined) { cb({ code: 'missing', info: 'Page missing.' }); return; }
if (!page.revisions || !page.revisions.length) { cb({ code: 'no_revisions', info: 'No revisions.' }); return; }
var rv = page.revisions[0];
if ('anon' in rv || rv.userhidden || !rv.user || rv.user === mwCfg.wgUserName) { cb(null); return; }
apiReq({
title: 'User talk:' + rv.user, section: 'new',
sectiontitle: 'Remover: [[:' + pg + ']]',
summary: opts.summary || makeSummary('уведомление автора'),
text: 'Страница [[:' + pg + ']], созданная вами, ' + actionPhrase + '. ' +
discussionText +
'~~' + '~~<br><small>Это автоматическое уведомление, сгенерированное ' + scriptLink + '.</small>',
assertuser: mwCfg.wgUserName
}, 'edit', function (editResp) { cb(editResp && editResp.error ? editResp.error : null); });
});
}
function notifyAuthorsForPages(pages, notifyOptions, callback) {
var cb = callback || function () {};
var opts = notifyOptions || {};
var list = [];
(pages || []).forEach(function (p) { var t = normTitle(p); if (t && list.indexOf(t) === -1) list.push(t); });
if (!list.length) { cb(); return; }
eachSequential(list, function (pg, next) {
var pageLink = buildQuotedStatusPageLink(pg);
var statusId = logStatus('Отправляется уведомление создателю страницы ' + pageLink + '...', null, { pending: true, trackError: false });
notifyAuthor(pg, opts, function (err) {
logStatus(err ? 'Уведомление создателя страницы ' + pageLink + '.' : 'Создатель страницы ' + pageLink + ' уведомлён.', err,
{ statusId: statusId, trackError: opts.trackError !== false });
next();
});
}, cb);
}
// ─── Подписка на раздел ──────────────────────────────────────────────────
function subscribeToTopic(pageTitle, sectionTitle, callback) {
var cb = callback || function () {};
if (!setSubscribe || !sectionTitle) { cb(); return; }
var statusId = logStatus('Оформляется подписка на раздел...', null, { pending: true, trackError: false });
var targetFrag = normalizeSectionForLink(sectionTitle).toLowerCase();
function finish(err, st) {
if (err) { logStatus('Не удалось подписаться на раздел.', err, { statusId: statusId, trackError: false }); cb(); return; }
logStatus(st === 'subscribed' ? 'Оформлена подписка на раздел.' : 'Раздел для подписки не найден.', null, { statusId: statusId, trackError: false });
cb();
}
function trySubscribe(attemptsLeft) {
apiReq({ page: pageTitle, prop: 'threaditemshtml', excludesignatures: true }, 'discussiontoolspageinfo', function (data) {
var items = (data && data.discussiontoolspageinfo && data.discussiontoolspageinfo.threaditemshtml) || null;
if (!items || !items.length) {
if (attemptsLeft > 0) { setTimeout(function () { trySubscribe(attemptsLeft - 1); }, 2000); return; }
finish(null, 'not_found'); return;
}
var commentname = null;
for (var ti = items.length - 1; ti >= 0; ti--) {
var t = items[ti];
if (t.type === 'heading') {
var htext = (t.headingText || t.html || '').replace(/<[^>]+>/g, '').trim();
if (htext.toLowerCase() === targetFrag || normalizeSectionForLink(htext).replace(/_/g, ' ').toLowerCase() === targetFrag) {
commentname = t.name; break;
}
}
}
if (!commentname) {
if (attemptsLeft > 0) setTimeout(function () { trySubscribe(attemptsLeft - 1); }, 2000);
else finish(null, 'not_found');
return;
}
apiReq({ page: pageTitle, commentname: commentname, subscribe: '1' }, 'discussiontoolssubscribe', function (res) {
finish(res && res.error ? res.error : null, 'subscribed');
});
});
}
setTimeout(function () { trySubscribe(2); }, 1500);
}
// ═══════════════════════════════════════════════════════════════════════════
// UI: модальные окна
// ═══════════════════════════════════════════════════════════════════════════
function syncModalLayout() {
var syncFn = $('#removerModal').data('rmSyncLayout');
if (typeof syncFn === 'function') syncFn();
}
function clearModalLayoutSyncHandlers() {
modalLayoutSyncHandlers = [];
$('#removerModal').removeData('rmSyncLayout');
}
function registerModalLayoutSync(handler) {
if (typeof handler !== 'function') return;
if (modalLayoutSyncHandlers.indexOf(handler) === -1) modalLayoutSyncHandlers.push(handler);
$('#removerModal').data('rmSyncLayout', function () {
modalLayoutSyncHandlers.slice().forEach(function (fn) {
if (typeof fn === 'function') fn();
});
});
}
function registerResizeObserver(observer) {
if (observer) resizeObservers.push(observer);
}
function resetModalObservers() {
resizeObservers.forEach(function (observer) {
if (observer && typeof observer.disconnect === 'function') observer.disconnect();
});
resizeObservers = [];
clearModalLayoutSyncHandlers();
$(window).off('resize.removerModal');
$(window).off('.rmTaResize');
}
function closeModal() {
resetModalObservers();
$(window).off('keydown.remover');
if (isVector22) $('#content').css('min-width', '');
$('#removerModal').remove();
}
function ensureModalStyles() {
if (document.getElementById('removerModalDynamicStyles')) return;
var progH = 'background:' + tk.bgProgH + '!important;border-color:' + tk.bProgH + '!important;color:' + tk.cInv + '!important;';
var neutH = 'background:' + tk.bgN + '!important;border-color:' + tk.bSub + '!important;color:inherit!important;';
var succH = 'background:' + tk.bgSuccH+ '!important;border-color:' + tk.bSuccH + '!important;color:' + tk.cInv + '!important;';
var pillBg = 'linear-gradient(180deg,' + tk.bgBase + ' 0%,' + tk.bgNSub + ' 100%)';
var pillShadow = '0 1px 0 rgba(255,255,255,.08) inset,0 1px 2px rgba(0,0,0,.18)';
var activePillShadow = '0 1px 0 rgba(255,255,255,.14) inset,0 2px 6px rgba(51,102,204,.24)';
var css = [
'#removerModal,#removerModal *{-moz-text-size-adjust:none!important;-webkit-text-size-adjust:100%!important;text-size-adjust:100%!important}',
'#removerModal{color:inherit}',
'#removerModal input::placeholder,#removerModal textarea::placeholder{color:var(--color-subtle,currentColor);opacity:.7}',
'#removerModal input[type="checkbox"],#removerModal input[type="radio"]{appearance:auto;-webkit-appearance:auto;-moz-appearance:auto;accent-color:auto}',
'#removerModal input[type="checkbox"]{outline:none!important;box-shadow:none!important}',
'#removerModal button{transition:background-color .12s ease,border-color .12s ease,color .12s ease,box-shadow .12s ease,filter .12s ease,transform .06s ease}',
'#removerModal .rmToggleBtn{background:' + tk.bgNSub + '!important;border-color:' + tk.bSub + '!important;color:inherit!important}',
'#removerModal .rmToggleBtn.is-active{background:' + tk.bgProg + '!important;border-color:' + tk.bProg + '!important;color:' + tk.cInv + '!important}',
'#removerModal button:not(:disabled):hover{filter:brightness(.97)}',
'#removerModal button:not(:disabled):active{transform:translateY(1px)}',
'#removerModal #removerSubmit:not(:disabled):not(.rmSubmitError):hover,#removerModal .rmToggleBtn.is-active:hover{' + progH + 'filter:none}',
'#removerModal #removerSubmit:not(:disabled):not(.rmSubmitError):active,#removerModal .rmToggleBtn.is-active:active{' + progH + 'filter:brightness(.92)!important}',
'#removerModal #removerSubmit.rmSubmitReady:not(.rmSubmitError){background:' + tk.bgProg + '!important;border-color:' + tk.bProg + '!important;color:' + tk.cInv + '!important;outline:none!important;box-shadow:0 0 0 6px rgba(51,102,204,.13),0 1px 2px rgba(0,0,0,.08)!important}',
'#removerModal #removerSubmit.rmSubmitReady:not(.rmSubmitError):not(:disabled):hover{' + progH + 'box-shadow:0 0 0 7px rgba(51,102,204,.16),0 1px 2px rgba(0,0,0,.1)!important;filter:none}',
'#removerModal #removerSubmit.rmSubmitReady:not(.rmSubmitError):not(:disabled):active{' + progH + 'box-shadow:0 0 0 5px rgba(51,102,204,.14),0 1px 2px rgba(0,0,0,.08)!important;filter:brightness(.92)!important}',
'#removerModal .rmAddPageBtn{background:' + tk.bgProg + '!important;border-color:' + tk.bProg + '!important;color:' + tk.cInv + '!important;font-weight:700!important}',
'#removerModal .rmAddPageBtn:hover{' + progH + 'filter:none!important}',
'#removerModal .rmAddVariantBtn{background:' + tk.bgBase + '!important;border-color:' + tk.bSub + '!important;color:inherit!important;font-weight:700!important}',
'#removerModal .rmAddVariantBtn:hover{' + neutH + 'filter:none!important}',
'#removerModal .rmRenameVariantAddBtn{font-size:15px!important}',
'#removerModal .rmStartMultiPageBtn{align-self:flex-start!important;margin-bottom:0!important}',
'#removerModal .rmRenameVariantRow{width:100%!important;max-width:100%!important;box-sizing:border-box!important}',
'#removerModal .rmMultiRenameVariantsContainer{display:flex;flex-direction:column;gap:6px;box-sizing:border-box;margin-top:6px;width:100%!important;max-width:100%!important}',
'#removerModal .rmMultiRenamePrimaryTargetRow,#removerModal .rmMultiRenameVariantRow{display:flex;margin-bottom:0!important;box-sizing:border-box}',
'#removerModal .rmMultiPageCommentToggle{min-width:32px;height:32px;padding:0!important;font-size:15px!important;line-height:1!important}',
'#removerModal .rmMultiPageCommentToggle.is-active{background:#bfc4ca!important;border-color:#8f98a3!important;color:#202122!important}',
'#removerModal .rmMultiPageCommentToggle.is-active:hover{background:#b4bac1!important;border-color:#848e99!important;color:#202122!important;filter:none}',
'#removerModal .rmMultiPageCommentToggle.is-active:active{background:#a9b0b8!important;border-color:#79838f!important;color:#202122!important;filter:none}',
'#removerModal #removerSubmit.rmSubmitError:not(:disabled):hover{background:#b32424!important;border-color:#b32424!important;color:#fff!important;filter:none}',
'#removerModal #removerSubmit.rmSubmitError:not(:disabled):active{background:#b32424!important;border-color:#b32424!important;color:#fff!important;filter:brightness(.88)!important}',
'#removerModal #removerReload:not(:disabled):hover{' + succH + 'filter:none}',
'#removerModal #removerReload:not(:disabled):active{' + succH + 'filter:brightness(.92)!important}',
'#removerModal button:not(#removerSubmit):not(#removerReload):not(.is-active):not(.rmAddPageBtn):not(.rmAddVariantBtn):hover,#removerModal .rmToggleBtn:not(.is-active):not(.rmAddPageBtn):not(.rmAddVariantBtn):hover{' + neutH + 'filter:none}',
'#removerModal button:not(#removerSubmit):not(#removerReload):not(.is-active):not(.rmAddPageBtn):not(.rmAddVariantBtn):active{' + neutH + 'filter:brightness(.92)!important}',
'#removerModal a.removerModalLink{color:' + tk.cProg + ';text-decoration:none;border-bottom:0;box-shadow:none;word-break:break-word;overflow-wrap:anywhere}',
'#removerModal a.removerModalLink:hover,#removerModal a.removerModalLink:focus{color:' + tk.cProgH + ';text-decoration:underline}',
'#removerModal #removerModalSubtitle{font-size:12px!important;line-height:1.35!important;font-weight:400!important;color:' + tk.cSubM + '!important;max-width:100%;overflow-wrap:anywhere;word-break:break-word}',
'#removerModal #removerModalSubtitle a{font-size:inherit!important;line-height:inherit!important;font-weight:inherit!important}',
'#removerModal a.rmButtonLikeLink{color:' + tk.cBase + '!important;text-decoration:none!important;transform:none!important;transition:background-color .12s ease,border-color .12s ease,color .12s ease,filter .12s ease,transform .06s ease!important}',
'#removerModal a.rmButtonLikeLink:hover{' + neutH + 'text-decoration:none!important;transform:none!important}',
'#removerModal a.rmButtonLikeLink:focus{text-decoration:none!important}',
'#removerModal a.rmButtonLikeLink:focus:not(:focus-visible){outline:none!important}',
'#removerModal a.rmButtonLikeLink:focus-visible{outline:2px solid ' + tk.bProg + '!important;outline-offset:2px;text-decoration:none!important}',
'#removerModal a.rmButtonLikeLink:hover:active{' + neutH + 'filter:brightness(.92)!important;transform:translateY(1px)!important;text-decoration:none!important}',
'#removerModal .rmInfoBox{margin:0 0 10px;padding:8px 10px;border:1px solid ' + tk.bSub + ';border-radius:6px;background:' + tk.bgNSub + '}',
'#removerModal .rmActionList{display:flex;flex-direction:column;gap:6px}',
'#removerModal .rmActionItem{display:block;padding:8px 10px;border:1px solid ' + tk.bSub + ';border-radius:6px;background:' + tk.bgBase + ';cursor:pointer;transition:background-color .12s ease,transform .06s ease}',
'#removerModal .rmActionItem:hover{background:' + tk.bgNSub + '}',
'#removerModal .rmActionItem:active{transform:translateY(1px)}',
'#removerModal .rmActionMain{display:flex;align-items:center}',
'#removerModal .rmActionMain input[type="radio"]{margin-right:8px}',
'#removerModal .rmActionMeta{display:block;margin-left:24px;margin-top:2px;color:' + tk.cSubM + ';font-size:12px;line-height:1.35}',
'#removerModal .rmActionWarning{display:none;margin-left:24px;margin-top:3px;color:' + tk.cDang + ';font-size:12px;line-height:1.35;font-weight:700}',
'#removerModal .rmConflictLead{margin:0 0 10px;color:' + tk.cSubM + ';font-size:13px;line-height:1.5}',
'#removerModal .rmConflictList{display:flex;flex-direction:column;gap:10px}',
'#removerModal .rmConflictCard{padding:12px;border:1px solid ' + tk.bSub + ';border-radius:8px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.55) inset}',
'#removerModal .rmConflictCard.is-skip{background:' + tk.bgNSub + ';border-color:' + tk.bSubS + '}',
'#removerModal .rmConflictTitle{font-size:14px;font-weight:700;line-height:1.4;color:' + tk.cBase + ';word-break:break-word;overflow-wrap:anywhere}',
'#removerModal .rmConflictMeta{margin-top:4px;font-size:12px;line-height:1.45;color:' + tk.cSubM + '}',
'#removerModal .rmConflictGroup{margin-top:10px}',
'#removerModal .rmConflictGroupTitle{margin:0 0 6px;font-size:12px;font-weight:700;line-height:1.35;color:' + tk.cSubM + '}',
'#removerModal .rmConflictButtons{display:flex;flex-wrap:wrap;gap:6px}',
'#removerModal .rmConflictChoice{padding:5px 10px}',
'#removerModal .rmConflictChoice.is-disabled,#removerModal .rmConflictButtons.is-disabled .rmConflictChoice{opacity:.55;cursor:not-allowed;pointer-events:none}',
'#removerModal .rmConflictHint{margin-top:8px;font-size:11px;line-height:1.45;color:' + tk.cSub + '}',
'#removerModal #rmSettingsForm{display:flex;flex-direction:column;gap:14px}',
'#removerModal .rmSettingsLead{margin:-2px 0 2px;color:' + tk.cSubM + ';font-size:13px;line-height:1.5}',
'#removerModal .rmSettingsSection{margin:0;padding:14px 16px;border:1px solid ' + tk.bSubS + ';border-radius:10px;background:linear-gradient(180deg,' + tk.bgBase + ' 0%,' + tk.bgNSub + ' 100%);box-shadow:0 1px 0 rgba(255,255,255,.7) inset}',
'#removerModal .rmSettingsSectionHeader{margin:0 0 12px}',
'#removerModal .rmSettingsSectionTitle{font-size:14px;font-weight:700;line-height:1.35;color:' + tk.cBase + '}',
'#removerModal .rmSettingsSectionDescription{margin-top:4px;font-size:12px;line-height:1.45;color:' + tk.cSubM + '}',
'#removerModal .rmSettingsField{display:block;margin:0 0 10px;padding:12px;border:1px solid ' + tk.bSubS + ';border-radius:8px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.55) inset}',
'#removerModal .rmSettingsField:last-child{margin-bottom:0}',
'#removerModal .rmSettingsFieldLabel{display:block;font-size:13px;font-weight:700;line-height:1.35;margin:0 0 8px;color:' + tk.cBase + '}',
'#removerModal .rmSettingsFieldControl{display:block;min-width:0}',
'#removerModal .rmSettingsFieldControl input{margin-bottom:0!important}',
'#removerModal .rmSettingsFieldControl input[type="text"]{min-height:38px;border-radius:6px}',
'#removerModal .rmSettingsFieldHint{margin-top:8px;font-size:12px;line-height:1.55;color:' + tk.cSubM + ';overflow-wrap:anywhere}',
'#removerModal .rmSettingsChecks{display:flex;flex-direction:column;gap:8px}',
'#removerModal .rmSettingsCheck{display:inline-flex;align-items:flex-start;gap:8px;font-size:14px;line-height:1.45;color:' + tk.cBase + '}',
'#removerModal .rmSettingsCheck input[type="checkbox"]{margin:3px 0 0;flex-shrink:0}',
'#removerModal .rmSettingsMenuPresetWrap{margin-top:10px}',
'#removerModal .rmSettingsMenuPresetLabel{margin:0 0 6px;font-size:12px;font-weight:700;line-height:1.35;color:' + tk.cSubM + '}',
'#removerModal .rmSegmentedBar{display:inline-flex;align-items:center;flex-wrap:wrap;gap:6px;margin-top:0;padding:0;border:0;border-radius:0;background:transparent;max-width:100%;box-sizing:border-box;box-shadow:none}',
'#removerModal .rmSettingsMenuPresetBar{display:inline-flex;align-items:center;flex-wrap:wrap;gap:6px;margin-top:0;padding:0;border:0;border-radius:0;background:transparent;max-width:100%;box-sizing:border-box;box-shadow:none}',
'#removerModal .rmSegmentedBtn,#removerModal .rmSettingsMenuPresetBtn{padding:5px 12px;border:1px solid ' + tk.bSub + ';border-radius:999px;background:' + pillBg + ';color:' + tk.cBase + ';font-size:12px;font-weight:600;line-height:1.35;cursor:pointer;box-shadow:' + pillShadow + '}',
'#removerModal .rmSegmentedBtn.is-active,#removerModal .rmSettingsMenuPresetBtn.is-active{background:' + tk.bgProg + ';border-color:' + tk.bProg + ';color:' + tk.cInv + ';box-shadow:' + activePillShadow + '}',
'#removerModal .rmKbuReasonPill{display:flex;align-items:stretch;width:100%;padding:0!important;overflow:hidden;border:1px solid ' + tk.bSub + ';border-radius:999px;box-sizing:border-box;cursor:pointer;text-align:left;font-size:12px;font-weight:400;line-height:1.3;box-shadow:' + pillShadow + '}',
'#removerModal .rmKbuReasonPill.is-active{box-shadow:' + activePillShadow + '!important}',
'#removerModal .rmKbuReasonNum{display:inline-flex;align-items:center;justify-content:center;min-width:44px;padding:4px 10px;border-right:1px solid ' + tk.bSub + ';box-sizing:border-box;font-weight:700;text-align:center;flex:0 0 auto}',
'#removerModal .rmKbuReasonPill.is-active .rmKbuReasonNum{border-right-color:rgba(255,255,255,.55)!important}',
'#removerModal .rmKbuReasonText{display:block;min-width:0;flex:1;padding:4px 11px 4px 12px;font-weight:400;overflow-wrap:anywhere}',
'#removerModal.rmModalSettings{border:1px solid ' + tk.bSub + '!important;background:' + tk.bgBase + '!important;border-radius:12px!important;box-shadow:0 14px 32px rgba(0,0,0,.08),0 1px 0 rgba(255,255,255,.78) inset!important}',
'#removerModal.rmModalSettings #removerModalHeaderBar{margin-bottom:14px;padding-bottom:10px;border-bottom:2px solid ' + tk.bSub + '}',
'#removerModal.rmModalSettings #removerModalSubtitle{margin:-2px 0 12px!important;color:' + tk.cSubM + '!important}',
'#removerModal.rmModalSettings #rmSettingsForm{gap:18px}',
'#removerModal.rmModalSettings .rmSettingsLead{margin:0;padding:0 2px;color:' + tk.cSubM + '}',
'#removerModal.rmModalSettings .rmSettingsSection{padding:16px 18px;border:1px solid ' + tk.bSub + ';border-radius:12px;background:linear-gradient(180deg,' + tk.bgNSub + ' 0%,' + tk.bgBase + ' 100%);box-shadow:0 1px 0 rgba(255,255,255,.82) inset,0 8px 18px rgba(0,0,0,.035)}',
'#removerModal.rmModalSettings .rmSettingsSectionHeader{margin:0 0 6px;padding-bottom:0;border-bottom:0}',
'#removerModal.rmModalSettings .rmSettingsSectionTitle{font-size:16px;line-height:1.3}',
'#removerModal.rmModalSettings .rmSettingsSectionDescription{margin-top:5px;max-width:none}',
'#removerModal.rmModalSettings .rmSettingsField{margin:14px 0 0;padding:12px 0 0;border:0;border-top:1px solid ' + tk.bSubS + ';border-radius:0;background:transparent;box-shadow:none}',
'#removerModal.rmModalSettings .rmSettingsField:first-child{margin-top:0;padding-top:0;border-top:0}',
'#removerModal.rmModalSettings .rmSettingsFieldLabel{margin:0 0 6px}',
'#removerModal.rmModalSettings .rmSettingsFieldControl input[type="text"]{min-height:40px;padding:8px 10px;border:1px solid ' + tk.bSub + ';border-radius:8px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.65) inset}',
'#removerModal.rmModalSettings .rmSettingsFieldControl input[type="text"]:focus{border-color:' + tk.bProg + ';box-shadow:0 0 0 1px rgba(51,102,204,.16);outline:none}',
'#removerModal.rmModalSettings .rmSettingsFieldHint{margin-top:6px;max-width:none}',
'#removerModal.rmModalSettings .rmSettingsChecks{gap:10px}',
'#removerModal.rmModalSettings .rmSettingsCheck{padding:4px 0}',
'#removerModal.rmModalSettings .rmSettingsMenuPresetWrap{margin-top:12px;padding-top:10px;border-top:1px dashed ' + tk.bSubS + '}',
'#removerModal.rmModalSettings .rmSettingsHintList{width:100%;max-width:100%;box-sizing:border-box;margin-top:10px;padding:10px 12px;border:1px solid ' + tk.bSubS + ';border-radius:8px;background:' + tk.bgBase + '}',
'#removerModal.rmModalSettings .rmSettingsHintRow{line-height:1.55}',
'#removerModal.rmModalSettings .rmSettingsHintBadge{background:' + tk.bgNSub + '}',
'#removerModal.rmModalSettings .rmQuickPhraseEditor{padding-top:2px}',
'#removerModal.rmModalSettings .rmQuickPhraseMeta{min-height:18px}',
'#removerModal.rmModalSettings #rmSettingsSignaturePreviewCode{display:inline-block;padding:2px 8px;border:1px solid ' + tk.bSubS + ';border-radius:999px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.65) inset}',
'#removerModal.rmModalSettings #rmFooterActionButtons{position:relative;flex:0 0 auto!important;display:flex!important;align-items:center!important;justify-content:flex-end!important;gap:6px!important;margin-left:auto!important;max-width:100%!important}',
'#removerModal.rmModalSettings #rmSettingsActionButtonsRow{display:flex;align-items:center;justify-content:flex-end;gap:6px;flex-wrap:nowrap}',
'#removerModal.rmModalSettings #rmSettingsUnsavedHint{display:none;position:absolute;top:100%;right:0;width:auto;box-sizing:border-box;margin:4px 0 0;color:' + tk.cSubM + ';opacity:.78;font-size:12px;line-height:1.35;text-align:right;white-space:nowrap}',
'#removerModal .rmProtectControlGroup{margin-top:12px;padding:8px 10px;border:1px solid ' + tk.bSubS + ';border-radius:10px;background:linear-gradient(180deg,' + tk.bgBase + ' 0%,' + tk.bgNSub + ' 100%);box-sizing:border-box}',
'#removerModal .rmProtectControlLabel{margin:0 0 6px;font-size:12px;font-weight:700;line-height:1.35;color:' + tk.cSubM + '}',
'#removerModal .rmProtectControlGroup .rmSegmentedBar{gap:4px;padding:0;border:0;box-shadow:none}',
'#removerModal .rmProtectControlGroup .rmSegmentedBtn{padding:4px 10px;font-size:12px}',
'#removerModal .rmTransferPanel{margin-top:10px;padding:0;border:0;background:transparent;box-sizing:border-box;box-shadow:none}',
'#removerModal .rmTransferGrid{display:grid;grid-template-columns:max-content max-content;column-gap:10px;row-gap:6px;align-items:start;justify-content:start}',
'#removerModal .rmTransferGrid .rmSegmentedBar{justify-self:start}',
'#removerModal .rmTransferHintRow{grid-column:1 / -1;min-height:0}',
'#removerModal .rmTransferPanel .rmSegmentedBar{gap:4px;padding:0;border:0;box-shadow:none}',
'#removerModal .rmTransferPanel .rmSegmentedBtn{padding:6px 14px;font-size:12px;font-weight:700}',
'#removerModal #rmTransferModeGroup{gap:0}',
'#removerModal #rmTransferModeGroup .rmSegmentedBtn:first-child{border-top-right-radius:0;border-bottom-right-radius:0}',
'#removerModal #rmTransferModeGroup .rmSegmentedBtn + .rmSegmentedBtn{margin-left:-1px;border-top-left-radius:0;border-bottom-left-radius:0}',
'#removerModal.rmCompactContent .rmMultiPageRow{flex-wrap:wrap!important;gap:6px}',
'#removerModal.rmCompactContent .rmMultiPageRow .rmMultiPageInput,#removerModal.rmCompactContent .rmMultiPageRow .rmMultiPageTargetInput{flex:1 1 100%!important;width:100%!important}',
'#removerModal.rmCompactContent .rmMultiPageRow .rmMultiPageCommentToggle,#removerModal.rmCompactContent .rmMultiPageRow .rmAddMultiPage,#removerModal.rmCompactContent .rmMultiPageRow .rmAddMultiRenameVariant,#removerModal.rmCompactContent .rmMultiPageRow .rmRemoveInput{margin-left:0!important}',
'#removerModal.rmCompactContent .rmTransferGrid{grid-template-columns:minmax(0,1fr);gap:10px}',
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(1){order:1}',
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(2){order:2}',
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(3){order:3}',
'#removerModal.rmCompactContent #rmTransferModeGroup{flex-direction:column;align-items:flex-start;gap:6px}',
'#removerModal.rmCompactContent #rmTransferModeGroup .rmSegmentedBtn{margin-left:0!important;border-radius:999px!important}',
'#removerModal.rmCompactContent .rmTransferHintRow{grid-column:auto}',
'#removerModal #rmProtectTextBlock{margin-top:14px}',
'#removerModal #rmSettingsMenuTitle:disabled{background:' + tk.bgDis + '!important;border-color:' + tk.bDis + '!important;color:' + tk.cDis + '!important;-webkit-text-fill-color:' + tk.cDis + ';opacity:1;cursor:not-allowed;box-shadow:none!important}',
'#removerModal .rmSettingsHintList{display:flex;flex-direction:column;gap:4px;margin-top:8px}',
'#removerModal .rmSettingsHintRow{font-size:12px;line-height:1.5;color:' + tk.cSubM + '}',
'#removerModal .rmSettingsHintBadge{display:inline-block;margin-right:6px;padding:1px 6px;border:1px solid ' + tk.bSubS + ';border-radius:999px;background:' + tk.bgBase + ';font-size:11px;font-weight:700;color:' + tk.cSubM + ';vertical-align:baseline}',
'#removerModal .rmQuickPhraseEditor{display:flex;flex-direction:column;gap:10px}',
'#removerModal .rmQuickPhraseList,#removerModal .rmQuickPhrasesPanel{display:flex;flex-wrap:wrap;gap:8px;align-items:flex-start}',
'#removerModal .rmQuickPhraseChip{position:relative;display:inline-flex;align-items:center;gap:4px;max-width:100%;padding:2px 4px 2px 10px;border:1px solid ' + tk.bSub + ';border-radius:999px;background:' + pillBg + ';box-shadow:' + pillShadow + ';transition:border-color .12s,box-shadow .12s,opacity .12s;overflow:visible}',
'#removerModal .rmQuickPhraseChip.is-editing{opacity:.42;border-style:dashed}',
'#removerModal .rmQuickPhraseChip.is-dragging{opacity:.65}',
'#removerModal .rmQuickPhraseChip.is-drop-before::before,#removerModal .rmQuickPhraseChip.is-drop-after::after{content:"";position:absolute;top:50%;width:3px;height:24px;border-radius:999px;background:' + tk.cProg + ';box-shadow:0 0 0 1px rgba(51,102,204,.08)}',
'#removerModal .rmQuickPhraseChip.is-drop-before::before{left:-4px;transform:translate(-50%,-50%)}',
'#removerModal .rmQuickPhraseChip.is-drop-after::after{right:-4px;transform:translate(50%,-50%)}',
'#removerModal .rmQuickPhraseEditBtn{max-width:100%;padding:3px 0;border:0;background:transparent;color:' + tk.cBase + ';font-size:13px;line-height:1.35;cursor:pointer;text-align:left;white-space:normal}',
'#removerModal .rmQuickPhraseRemoveBtn{width:24px;height:24px;padding:0;border:0;border-radius:999px;background:transparent;color:' + tk.cSubM + ';font-size:18px;line-height:1;cursor:pointer;flex-shrink:0}',
'#removerModal .rmQuickPhraseRemoveBtn:hover{background:' + tk.bgN + ';color:' + tk.cBase + '}',
'#removerModal #rmSettingsQuickPhraseInput.is-editing{border-color:' + tk.bProg + ';box-shadow:0 0 0 1px rgba(51,102,204,.12)}',
'#removerModal .rmQuickPhraseMeta{font-size:12px;line-height:1.45;color:' + tk.cSubM + '}',
'#removerModal .rmQuickPhraseEmpty{padding:2px 0;font-size:12px;line-height:1.45;color:' + tk.cSubM + '}',
'#removerModal .rmQuickPhrasesPanel{margin-top:8px}',
'#removerModal .rmQuickPhraseActionBtn{padding:5px 12px;border:1px solid ' + tk.bSub + ';border-radius:999px;background:' + pillBg + ';color:' + tk.cBase + ';font-size:12px;font-weight:600;line-height:1.35;cursor:pointer;box-shadow:' + pillShadow + '}',
'#removerModal .rmQuickPhraseActionBtn:hover{border-color:' + tk.bProg + ';color:' + tk.cProg + '}',
'@media (max-width:' + sz.mobileBp + 'px){',
'#removerModal button{white-space:normal!important}',
'#removerModal #rmFooterButtons{align-items:flex-start!important}',
'#removerModal #rmFooterCheckboxes,#removerModal #rmFooterActionButtons{width:100%!important;max-width:100%!important;margin-left:0!important}',
'#removerModal .rmSettingsSection{padding:12px 13px}',
'#removerModal .rmSettingsField{padding:10px}',
'#removerModal.rmModalSettings #rmSettingsUnsavedHint{max-width:100%;margin:4px 0 0;text-align:right;white-space:normal}',
'#removerModal .rmTransferPanel{padding:0}',
'#removerModal .rmTransferGrid{grid-template-columns:minmax(0,1fr);gap:10px}',
'#removerModal .rmTransferHintRow{grid-column:auto}',
'#removerModal .rmQuickPhraseChip{max-width:100%}',
'}'
].join('');
var style = document.createElement('style');
style.id = 'removerModalDynamicStyles';
style.textContent = css;
document.head.appendChild(style);
}
function applyV2022Layout($modal, explicitWidth) {
if (!isVector22) return;
var css = { 'max-width': '100%', 'box-sizing': 'border-box', 'overflow-wrap': 'anywhere' };
if (typeof explicitWidth === 'number') css['max-width'] = explicitWidth + 'px';
$modal.css(css);
}
function getPageUrl(pageTitle) {
return (mw.util && typeof mw.util.getUrl === 'function')
? mw.util.getUrl(pageTitle)
: '/wiki/' + encodeURIComponent((pageTitle || '').replace(/ /g, '_'));
}
function getPageUrlWithFragment(pageTitle, fragment) {
var url = getPageUrl(pageTitle);
var frag = normalizeSectionForLink(fragment);
if (frag) url += '#' + ((mw.util && mw.util.escapeIdForLink) ? mw.util.escapeIdForLink(frag) : frag.replace(/\s+/g, '_'));
return url;
}
function buildStatusPageLink(pageName) {
var title = normTitle(pageName);
return '<a href="' + escapeHtml(getPageUrl(title)) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' +
escapeHtml(title) + '</a>';
}
function buildQuotedStatusPageLink(pageName) {
return '«' + buildStatusPageLink(pageName) + '»';
}
function buildTemplatePageLink(templateName, labelText) {
var name = String(templateName || '')
.replace(/^(?:safe)?subst\s*:\s*/i, '')
.replace(RE_TEMPLATE_NS, '')
.trim();
var ns = getNamespaceLabel(10, 'Шаблон');
if (!name) return escapeHtml(labelText);
return '<a href="' + escapeHtml(getPageUrl(ns + ':' + name)) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' +
escapeHtml(labelText) + '</a>';
}
function renderTemplateLinksInText(text) {
var source = String(text || '');
var out = '';
var lastIndex = 0;
source.replace(/\{\{\s*([^{}|]+)(?:\|[^{}]*)?\}\}/g, function (match, templateName, offset) {
out += escapeHtml(source.slice(lastIndex, offset));
out += buildTemplatePageLink(templateName, match);
lastIndex = offset + match.length;
return match;
});
return out + escapeHtml(source.slice(lastIndex));
}
function buildHeaderIconButtonHtml(id, title, label, text) {
return joinHtml([
'<button id="', id, '" type="button" title="', escapeHtml(title), '" aria-label="', escapeHtml(label || title), '" ',
'style="', stHeaderIconBtn, '">', text || '', '</button>'
]);
}
function createModal(opts) {
if (typeof opts === 'string') opts = { title: opts };
var layout = getModalLayout();
resetModalObservers();
ensureModalStyles();
if (isVector22) $('#content').css('min-width', '');
$('#removerModal').remove();
var subtitleHtml = '';
var subtitleStyle = 'margin:-4px 0 8px;font-size:12px!important;color:' + tk.cSubM + ';line-height:1.35!important;font-weight:400!important;';
var subtitleLinkStyle = 'font-size:inherit!important;line-height:inherit!important;font-weight:inherit!important;';
if (opts.subtitleHtml) {
subtitleHtml = joinHtml([
'<div id="removerModalSubtitle" style="', subtitleStyle, '">',
opts.subtitleHtml,
'</div>'
]);
} else if (opts.subtitlePage) {
subtitleHtml = joinHtml([
'<div id="removerModalSubtitle" style="', subtitleStyle, '">',
opts.subtitleLabel || 'Текущий день',
': <a href="', getPageUrl(opts.subtitlePage), '" target="_blank" rel="noopener noreferrer" class="removerModalLink" style="', subtitleLinkStyle, '">',
normTitle(opts.subtitlePage),
'</a></div>'
]);
}
var settingsButtonHtml = opts.showSettingsButton === false ? '' :
buildHeaderIconButtonHtml('removerSettingsTrigger', 'Конфигурация', 'Конфигурация', '⚙');
var display = opts.inline ? 'inline-block' : 'block';
var modalMargin = opts.inline ? '1em 0' : (layout.shouldCenter ? '1em auto' : '1em 0');
var inlineLayoutStyle = opts.inline ? ';justify-self:start;align-self:start;width:fit-content;' : '';
var modalStyle = joinHtml([
'position:relative;padding:1.5em;margin:', modalMargin, ';display:', display, ';',
'border:', stStyles.border, ';background:', stStyles.background,
';border-radius:', stStyles.borderRadius, ';box-shadow:', stStyles.boxShadow,
';max-width:100%;box-sizing:border-box;overflow-wrap:anywhere;', inlineLayoutStyle
]);
var headerStyle = 'display:flex;align-items:center;gap:10px;margin-bottom:10px;padding-bottom:6px;border-bottom:1px solid ' + tk.bSubS + ';';
var titleStyle = 'color:' + stStyles.headerColor + ';margin:0;padding:0;border:0;display:block;font-size:1.3em;font-weight:400;line-height:1.25;flex:1 1 auto;min-width:0;';
$('#content').prepend(joinHtml([
'<div id="removerModal" style="', modalStyle, '">',
'<div id="removerModalHeaderBar" style="', headerStyle, '">',
'<h1 id="removerModalTitle" style="', titleStyle, '"><span id="removerModalTitleText">', opts.title, '</span></h1>',
settingsButtonHtml,
'</div>',
subtitleHtml,
'<div id="removerModalContent"></div>',
'<div id="removerModalFooter" style="margin-top:15px;"></div>',
'</div>'
]));
var $modal = $('#removerModal');
if (opts.width === 'compact') $modal.css({ width: layout.defaultOuterWidth + 'px', 'max-width': '100%', 'box-sizing': 'border-box' });
else applyV2022Layout($modal);
$('#removerSettingsTrigger').off('click').on('click', function () {
openSettings();
});
}
function buildFooterCheckboxHtml(name, checked, label) {
return joinHtml([
'<label style="', stFooterCheckLabel, '">',
'<input name="', name, '" type="checkbox" style="margin:2px 0 0;flex-shrink:0;" ',
checked ? 'checked' : '',
'>',
label,
'</label>'
]);
}
function buildFooterActionsHtml(buttonsHtml) {
return '<div id="rmFooterActionButtons" style="' + stFooterActions + '">' + buttonsHtml + '</div>';
}
function renderModalFooter(mode, options) {
var opts = options || {};
$('#removerModalFooter').css('width', '');
if (mode === 'submit') {
var showCb = opts.showCheckbox !== false;
var showSub = opts.showSubscribe === true;
var ns = mwCfg.wgNamespaceNumber;
var notifyLabel = ns === 0 ? 'Оповестить создателя статьи'
: (ns === 10 || ns === 11) ? 'Оповестить создателя шаблона'
: (ns === 14 || ns === 15) ? 'Оповестить создателя категории'
: 'Оповестить создателя страницы';
var cbInlineHtml = '';
if (showSub || showCb) {
cbInlineHtml = joinHtml([
'<div id="rmFooterCheckboxes" style="', stFooterChecks, '">',
showSub ? buildFooterCheckboxHtml('rmSubscribe', setSubscribe, 'Подписаться на номинацию') : '',
showCb ? buildFooterCheckboxHtml('rmUAlert', setAlert, notifyLabel) : '',
'</div>'
]);
}
$('#removerModalFooter').html(joinHtml([
'<div id="rmFooterButtons" style="', stFooterWrap, 'justify-content:', cbInlineHtml ? 'space-between' : 'flex-end', ';">',
cbInlineHtml,
buildFooterActionsHtml(joinHtml([
'<button id="removerCancel" style="', stCancel, '">Отмена</button>',
'<button id="removerSubmit" style="', stSubmit, '">', opts.submitText || 'ОК', '</button>'
])),
'</div>'
]));
$('#removerCancel').click(function () { closeModal(); });
$('#removerSubmit').data('rmSubmitInProgress', false).click(function () {
if ($(this).data('rmSubmitInProgress')) return;
$(this).removeClass('rmSubmitError').css({ background: '', 'border-color': '', color: '' });
isError = false;
if (!opts.preserveLogOnSubmit) {
$('#rmLogBox').empty();
logStatusSeq = 0;
}
if (showCb) { setAlert = $('[name="rmUAlert"]').is(':checked'); state.setAlert = setAlert; }
if (showSub) { setSubscribe = $('[name="rmSubscribe"]').is(':checked'); state.setSubscribe = setSubscribe; }
$(this).data('rmSubmitInProgress', true).prop('disabled', true);
var submitResult;
try { submitResult = opts.onSubmit(); } catch (ex) { unlockModalSubmit(); throw ex; }
if (submitResult === false) unlockModalSubmit();
});
$(window).off('keydown.remover').on('keydown.remover', function (e) {
if (e.ctrlKey && e.keyCode === 13) $('#removerSubmit').click();
});
} else if (mode === 'reload') {
var newBtns = buildFooterActionsHtml(joinHtml([
'<button id="removerCancel" style="', stCancel, '">Закрыть</button>',
'<button id="removerReload" style="', stReload, '">', opts.reloadText || 'Обновить страницу', '</button>'
]));
$('#rmFooterCheckboxes').remove();
var $btns = $('#rmFooterButtons');
if ($btns.length) {
$btns.css({ 'justify-content': 'flex-end' }).html(newBtns);
} else {
$('#removerModalFooter').append(joinHtml([
'<div id="rmFooterButtons" style="', stFooterWrap, 'justify-content:flex-end;">',
newBtns,
'</div>'
]));
}
$('#removerCancel').click(function () { closeModal(); });
$('#removerReload').click(function () { location.reload(); });
$(window).off('keydown.remover').on('keydown.remover', function (e) {
if (e.keyCode === 27) $('#removerCancel').click();
if (e.ctrlKey && e.keyCode === 13) $('#removerReload').click();
});
if (shouldAutoReloadAfterAction()) setTimeout(function () { $('#removerReload').trigger('click'); }, 0);
} else { // 'close'
$('#removerModalFooter').html(joinHtml([
'<div style="display:flex;justify-content:flex-end;align-items:center;">',
'<button id="removerCancel" style="', stCancel, 'margin-right:0;">', opts.closeText || 'Закрыть', '</button>',
'</div>'
]));
$('#removerCancel').click(function () { closeModal(); });
$(window).off('keydown.remover').on('keydown.remover', function (e) {
if (e.keyCode === 27) $('#removerCancel').click();
});
}
}
function unlockModalSubmit() {
$('#removerSubmit').data('rmSubmitInProgress', false).prop('disabled', false);
}
function markSubmitError() {
isError = true;
var errColor = '#d73333';
$('#removerSubmit').data('rmSubmitInProgress', false).prop('disabled', false)
.addClass('rmSubmitError').css({ background: errColor, 'border-color': errColor, color: '#fff' });
}
// ─── UI: статус и ссылки ─────────────────────────────────────────────────
function startProcessing() {
if ($('#rmLogBox').length) return;
$('#removerModal').append(
'<div id="rmLogBox" style="margin-top:12px;padding-top:10px;border-top:1px solid ' + tk.bSubS + ';line-height:1.5;overflow-wrap:anywhere;word-break:break-word;box-sizing:border-box;"></div>'
);
syncLinkWidths();
}
function logStatus(message, error, opts) {
var o = opts || {};
if (o.trackError !== false && error && error.code) isError = true;
var $box = $('#rmLogBox');
if (!$box.length) { startProcessing(); $box = $('#rmLogBox'); }
var statusId = o.statusId || ('rm-status-' + (++logStatusSeq));
var $row = $box.find('[data-rm-status-id="' + statusId + '"]');
if (!$row.length) {
$row = $('<div data-rm-status-id="' + statusId + '" style="margin-top:4px;line-height:1.4;"></div>');
$box.append($row);
}
var html;
if (error) {
var errText = error.code
? '<span class="error"><small>' + escapeHtml(formatLogErrorCode(error.code)) + ': ' + escapeHtml(String(error.info || '')) + '</small></span>'
: escapeHtml(String(error));
html = '<span style="color:' + tk.cDang + ';margin-right:4px;">✕</span>' + message + ' — ' + errText;
} else if (o.pending) {
html = '<span style="color:' + tk.cSubM + ';">' + message + '</span>';
} else {
html = '<span style="color:' + tk.bgSucc + ';margin-right:4px;">✓</span><span style="color:' + tk.cSubM + ';">' + message + '</span>';
}
$row.html(html);
return statusId;
}
function formatLogErrorCode(code) {
var value = String(code || '');
return value.toLowerCase() === 'error' ? 'Ошибка' : value;
}
function logPageEdit(pageName, error, opts) {
logStatus('Правка страницы ' + buildQuotedStatusPageLink(pageName) + '.', error, opts);
}
function syncLinkWidths() {
var $box = $('#rmLogBox');
if (!$box.length) return;
var taW = $('#rmMsg,#nominationReason,#rmReportText').first().outerWidth() || 0;
$box.css({ width: taW ? taW + 'px' : '', 'max-width': '100%' });
}
function appendNominationLink(pageTitle, sectionTitle) {
if (!pageTitle) return;
var url = getPageUrl(pageTitle);
var frag = normalizeSectionForLink(sectionTitle);
if (frag) url += '#' + ((mw.util && mw.util.escapeIdForLink) ? mw.util.escapeIdForLink(frag) : frag.replace(/\s+/g, '_'));
var label = normTitle(frag ? pageTitle + '#' + frag : pageTitle);
var $target = $('#rmLogBox').length ? $('#rmLogBox') : $('#removerModalContent');
$target.append(
'<div style="margin-top:4px;line-height:1.4;word-break:break-word;overflow-wrap:anywhere;display:flex;align-items:baseline;gap:5px;">' +
'<span style="color:' + tk.bgSucc + ';font-size:14px;flex-shrink:0;">✓</span>' +
'<a href="' + escapeHtml(url) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' + escapeHtml(label) + '</a></div>'
);
}
function getCurrentSettings() {
return normalizeRemoverSettings(state.settings || settingsDefaults);
}
function shouldAutoReloadAfterAction() {
return !!getCurrentSettings().autoReloadAfterAction;
}
function maybeOpenNominationDiscussionInNewTab(nominationInfo) {
if (!getCurrentSettings().openNominationDiscussionInNewTab || !nominationInfo || !nominationInfo.pageTitle) return;
window.open(getPageUrlWithFragment(nominationInfo.pageTitle, nominationInfo.sectionTitle), '_blank', 'noopener,noreferrer');
}
// ─── UI-строители ────────────────────────────────────────────────────────
function buildInfoBoxHtml(mainText, detailsText, isErr) {
var cls = isErr ? ' class="error"' : '';
return joinHtml([
'<div class="rmInfoBox">',
'<p', cls, ' style="margin:0', detailsText ? ' 0 6px' : '', ';">', mainText, '</p>',
detailsText ? '<p style="margin:0;color:' + tk.cSubM + ';">' + detailsText + '</p>' : '',
'</div>'
]);
}
function buildActionsHtml(actions, inputName, listId) {
var actionItemsHtml = actions.map(function (a, i) {
var meta = a.description || (a.talkNotice ? 'С добавлением {{' + (a.talkTemplate || a.resultTemplate || 'шаблон') + '}} на СО.' : '');
var tagHtml = a.tag
? '<span style="display:inline-block;font-size:13px;font-weight:600;padding:2px 7px;border-radius:3px;background:' + tk.bgN + ';color:' + tk.cSubM + ';margin-right:8px;white-space:nowrap;vertical-align:middle;">' + escapeHtml(a.tag) + '</span>'
: '';
return joinHtml([
'<label class="rmActionItem">',
'<span class="rmActionMain">',
'<input type="radio" name="', inputName, '" value="', a.id, '" ', i === 0 ? 'checked' : '', '>',
tagHtml,
'<span>', a.label, '</span>',
'</span>',
meta ? '<span class="rmActionMeta">' + renderTemplateLinksInText(meta) + '</span>' : '',
a.warning ? '<strong class="rmActionWarning">' + escapeHtml(a.warning) + '</strong>' : '',
'</label>'
]);
}).join('');
return joinHtml([
'<div style="margin:0 0 8px;color:', tk.cSubM, ';font-size:13px;">Обнаружены открытые номинации:</div>',
'<div', listId ? ' id="' + listId + '"' : '', ' class="rmActionList">',
actionItemsHtml,
'</div>'
]);
}
function syncActionWarnings(inputName, listId) {
var $root = listId ? $('#' + listId) : $('#removerModal');
var $selected = $root.find('input[name="' + inputName + '"]:checked').closest('.rmActionItem');
$root.find('.rmActionWarning').hide();
$selected.find('.rmActionWarning').show();
}
function buildNestedCommentFieldsHtml(opts) {
var options = opts || {};
var wrapId = options.wrapId || '';
var textareaId = options.textareaId || '';
var textareaClass = options.textareaClass ? ' ' + options.textareaClass : '';
var textareaStyleExtra = options.textareaStyleExtra || '';
var wrapStyleExtra = options.wrapStyleExtra || '';
var placeholder = options.placeholder || 'Комментарий (необязательно)';
var beforeHtml = options.beforeHtml || '';
var marginTop = options.marginTop || '6px';
var minHeight = parseInt(options.minHeight, 10) || 90;
var isEmbedded = !!options.embedded;
var wrapClass = isEmbedded ? '' : (' class="' + RESIZE_CLASS + '"');
var wrapStyle = 'display:none;margin-top:' + marginTop + ';max-width:100%;box-sizing:border-box;';
if (isEmbedded) {
wrapStyle += 'padding:0;border:0;background:transparent;';
} else {
wrapStyle += 'padding:8px 10px;border:1px solid ' + tk.bSubS + ';border-radius:6px;background:' + tk.bgNSub + ';';
}
wrapStyle += wrapStyleExtra;
return joinHtml([
'<div id="', wrapId, '"', wrapClass, ' style="', wrapStyle, '">',
beforeHtml,
'<textarea id="', textareaId, '" class="rmNestedCommentInput', textareaClass,
'" placeholder="', escapeHtml(placeholder), '" style="', stInputFull,
'min-height:', minHeight, 'px;resize:both;margin-bottom:6px;', textareaStyleExtra, '"></textarea>',
buildQuickPhrasesPanelHtml(textareaId),
'</div>'
]);
}
function buildConditionalRetFieldsHtml() {
return buildNestedCommentFieldsHtml({
wrapId: 'rmCloseConditionalWrap',
textareaId: 'rmCloseConditionalReason',
placeholder: 'Условие / пояснение (необязательно)',
marginTop: '8px',
minHeight: 90,
beforeHtml: '<input id="rmCloseConditionalDeadline" type="text" placeholder="Срок доработки: 2026-05-31" style="' + stInputFull + 'margin-bottom:6px;">'
});
}
function buildAddMultiPageButtonHtml(options) {
var opts = options || {};
var title = opts.addTitle || 'Мультиноминация: добавить страницу';
return buildSquareAddButtonHtml('', title, 'rmAddMultiPage rmAddPageBtn');
}
function buildSquareAddButtonHtml(id, title, className, symbol) {
var idAttr = id ? ' id="' + id + '"' : '';
var clsAttr = className ? ' class="' + className + '"' : '';
var label = title || 'Добавить';
return '<button' + idAttr + ' type="button"' + clsAttr + ' title="' + escapeHtml(label) + '" aria-label="' + escapeHtml(label) + '" style="' + stRemoveBtn + '">' + escapeHtml(symbol || '+') + '</button>';
}
function leftControlStyle(style) {
return style.replace('margin-left:' + inlineControlGap + 'px;', 'margin-left:0;margin-right:' + inlineControlGap + 'px;');
}
function buildLeftSquareAddButtonHtml(id, title, className, symbol) {
return buildSquareAddButtonHtml(id, title, className, symbol).replace(stRemoveBtn, leftControlStyle(stRemoveBtn));
}
function buildLeftRemoveButtonHtml(className, title) {
var cls = className || 'rmRemoveInput';
var label = title || 'Удалить';
return '<button type="button" class="' + cls + '" style="' + leftControlStyle(stRemoveBtn) + '" title="' + escapeHtml(label) + '" aria-label="' + escapeHtml(label) + '">−</button>';
}
function buildMultiRenameVariantAddButtonHtml() {
return buildLeftSquareAddButtonHtml('', 'Добавить вариант нового заголовка', 'rmAddMultiRenameVariant rmAddVariantBtn rmRenameVariantAddBtn', '⤷');
}
function buildStartMultiPageButtonHtml(title) {
var label = title || 'Мультиноминация: добавить страницу';
return buildSquareAddButtonHtml('', label, 'rmAddMultiPage rmAddPageBtn rmStartMultiPageBtn');
}
function buildMultiPageButtonsHtml(commentWrapId, commentId, options) {
var opts = options || {};
var commentBtnStyle = stToolBtn + (opts.showComment ? '' : 'display:none;');
var commentTitle = opts.commentTitle || 'Добавить комментарий к этой странице';
var commentExpandedTitle = opts.commentExpandedTitle || 'Скрыть комментарий к этой странице';
if (opts.showAdd) return buildAddMultiPageButtonHtml(opts);
return joinHtml([
'<button type="button" class="rmToggleBtn rmMultiPageCommentToggle" data-rm-comment-wrap="', commentWrapId,
'" data-rm-comment-textarea="', commentId,
'" data-rm-comment-title="', escapeHtml(commentTitle),
'" data-rm-comment-expanded-title="', escapeHtml(commentExpandedTitle),
'" aria-label="', escapeHtml(commentTitle), '" title="', escapeHtml(commentTitle), '" aria-expanded="false" style="', commentBtnStyle, '">✎</button>',
'<button type="button" class="rmRemoveInput" style="', stRemoveBtn, '" title="', escapeHtml(opts.removeTitle || 'Убрать страницу из номинации'), '">−</button>'
]);
}
function buildMultiRenameVariantRowHtml(value, options) {
var opts = options || {};
return joinHtml([
'<div class="rmMultiRenameVariantRow" style="', stRow.replace('margin-bottom:6px;', 'margin-bottom:0;'), '">',
buildLeftRemoveButtonHtml('rmRemoveMultiRenameVariant', 'Убрать вариант нового заголовка'),
'<input type="text" class="rmMultiRenameVariantInput" placeholder="', escapeHtml(opts.placeholder || 'Дополнительный вариант нового заголовка'),
'" style="', stInputBox, '"', value ? ' value="' + escapeHtml(value) + '"' : '', '>',
'</div>'
]);
}
function buildMultiPageRowHtml(index, options) {
var opts = options || {};
var pageInputId = 'rmMultiPage' + index;
var commentWrapId = 'rmMultiPageCommentWrap' + index;
var commentId = 'rmMultiPageComment' + index;
var pageValue = opts.pageValue || '';
var pageValueAttr = pageValue ? ' value="' + escapeHtml(pageValue) + '"' : '';
var inputPlaceholder = opts.inputPlaceholder || 'Страница';
var targetInputClass = opts.targetInputClass || '';
var targetInputHtml = '';
var commentPlaceholder = opts.commentPlaceholder || 'Комментарий только для этой страницы (необязательно)';
var commentIndent = opts.targetVariants ? leftNestedControlOffset : '0';
var pageRowStyle = stRow.replace('margin-bottom:6px;', 'margin-bottom:0;');
var blockStyle = 'max-width:100%;box-sizing:border-box;';
var buttonsHtml = buildMultiPageButtonsHtml(commentWrapId, commentId, {
showAdd: !!opts.showAdd,
showComment: !!opts.showComment,
addTitle: opts.addTitle,
removeTitle: opts.removeTitle,
commentTitle: opts.commentTitle,
commentExpandedTitle: opts.commentExpandedTitle
});
if (opts.targetInput) {
targetInputHtml = joinHtml([
'<input id="rmMultiPageTarget', index, '" type="text" placeholder="', escapeHtml(opts.targetPlaceholder || 'Новое название'),
'" class="rmMultiPageTargetInput ', escapeHtml(targetInputClass), '" style="', stInputBox, '">'
]);
}
return joinHtml([
'<div class="rmMultiPageBlock ', RESIZE_CLASS, '" style="', blockStyle, '">',
'<div', opts.rowId ? ' id="' + opts.rowId + '"' : '', ' class="rmMultiPageRow" style="', pageRowStyle, '">',
'<input id="', pageInputId, '" type="text" placeholder="', escapeHtml(inputPlaceholder), '" class="rmMultiPageInput" style="', stInputBox, '"', pageValueAttr, '>',
opts.targetVariants ? '' : targetInputHtml,
buttonsHtml,
'</div>',
opts.targetVariants ? '<div class="rmMultiRenameVariantsContainer"><div class="rmMultiRenamePrimaryTargetRow">' + buildMultiRenameVariantAddButtonHtml() + targetInputHtml + '</div></div>' : '',
buildNestedCommentFieldsHtml({
wrapId: commentWrapId,
textareaId: commentId,
textareaClass: 'rmMultiPageCommentInput',
placeholder: commentPlaceholder,
marginTop: multiNominationGap,
minHeight: 90,
embedded: true,
wrapStyleExtra: 'padding:0 0 0 ' + commentIndent + ';background:transparent;',
textareaStyleExtra: 'border-color:' + tk.bSubS + ';border-radius:4px;background:' + tk.bgBase + ';'
}),
'</div>'
]);
}
function buildSingleRenameBlockHtml(config, startMultiTitle, currentPageName, currentPlaceholder) {
var addLabel = 'Добавить вариант нового заголовка';
var currentValue = currentPageName || '';
return joinHtml([
'<div id="rmSingleRenameBlock" style="display:flex;flex-direction:column;align-items:stretch;gap:0;">',
'<div class="rmInputRow ', RESIZE_CLASS, '" style="', stRow, '">',
'<input id="rmSingleRenameCurrent" type="text" class="rmSingleRenameCurrentInput" placeholder="', escapeHtml(currentPlaceholder || 'Текущее название'), '" style="', stInputBox, '" value="', escapeHtml(currentValue), '">',
buildStartMultiPageButtonHtml(startMultiTitle),
'</div>',
'<div class="rmInputRow ', RESIZE_CLASS, ' rmRenameVariantRow" style="', stRow, '">',
buildLeftSquareAddButtonHtml(config.addBtnId, addLabel.replace(/^\+\s*/, '') || 'Добавить вариант', 'rmAddVariantBtn rmRenameVariantAddBtn', '⤷'),
'<input id="', config.firstId, '" type="text" class="', config.inputClass || 'variantInput', '" placeholder="', config.firstPh || '', '" style="', stInputBox, '">',
'</div>',
'<div id="', config.containerId, '"></div>',
'</div>'
]);
}
function showInfoAndClose(mainText, detailsText, isErr) {
$('#removerModalContent').html(buildInfoBoxHtml(mainText, detailsText || '', isErr || false));
renderModalFooter('close');
}
function getSelectedAction(inputName, actionMap) {
var id = $('[name="' + inputName + '"]:checked').val();
var sel = actionMap[id];
if (!sel) alert('Выберите действие.');
return sel || null;
}
function prependTemplateToNoinclude(text, templateText) {
var source = String(text || '');
var tpl = String(templateText || '').trim();
if (!tpl) return source;
var match = source.match(RE_NOINCLUDE);
if (match) {
var before = source.slice(0, source.indexOf(match[0]));
if (/\S/.test(before)) return '<noinclude>' + tpl + '</noinclude>\n' + source;
var content = String(match[2] || '').replace(/^\n+/, '');
return source.replace(match[0], match[1] + '<noinclude>' + tpl + (content ? '\n' + content : '') + '\n</noinclude>');
}
return '<noinclude>' + tpl + '</noinclude>\n' + source;
}
function buildGeneratedNominationTemplateText(job, pg) {
var tplStr = '';
if (!job) return '';
if (job.opId === 'fRm') {
if (job.kbuTemplateText) return T_OPEN + job.kbuTemplateText + T_CLOSE;
return job.kbuTemplate ? (T_OPEN + job.kbuTemplate + T_CLOSE) : '';
}
if (typeof job.articleTpl !== 'function') return '';
tplStr = job.articleTpl(job.opId === 'mRnm' ? buildRenameTemplateParam(getMultiRenameTarget(job, pg, 'multiRenameTemplateTargets')) : job.tplpar, job.date[0]);
if (job.opId === 'merge' && job.tplpar) {
tplStr = (job.op.nomination.articleTpl)(
('|' + job.tplpar + '|').replace('|' + pg + '|', '|').slice(1, -1),
job.date[0]
);
}
return tplStr ? (T_OPEN + tplStr + T_CLOSE) : '';
}
function applyConflictTemplateResolution(articleText, job, pg, decision) {
var rule = getNominationConflictRule(job);
var generatedTemplate = buildGeneratedNominationTemplateText(job, pg);
var source = String(articleText || '');
if (!generatedTemplate || !decision) return source;
if (decision.templateAction === 'overwrite') {
var cleaned = rule ? stripTemplatesByPattern(source, rule.namePattern).text : source;
return prependTemplateToNoinclude(cleaned, generatedTemplate);
}
if (decision.templateAction === 'prepend') return prependTemplateToNoinclude(source, generatedTemplate);
return source;
}
function inspectMultiNominationConflicts(job, callback) {
var cb = callback || function () {};
var pages = (job && job.multiArticles) ? job.multiArticles.slice() : [];
var conflicts = [];
var statusId = logStatus('Проверяются статьи на наличие уже установленных шаблонов...', null, { pending: true, trackError: false });
if (!pages.length) {
logStatus('Проверка завершена: конфликтов не найдено.', null, { statusId: statusId, trackError: false });
cb(null, conflicts);
return;
}
eachSequential(pages, function (pg, next) {
getText(pg, function (articleText, readErr) {
var conflict;
if (readErr) { next(makeReadError(readErr, 'read_failed', 'Не удалось проверить страницу «' + pg + '».')); return; }
if (articleText === null) { next({ code: 'read_failed', info: 'Страница «' + pg + '» не существует.' }); return; }
conflict = detectNominationConflict(articleText, job);
if (conflict) {
conflicts.push($.extend({ pageName: pg }, conflict));
logStatus('В статье ' + buildQuotedStatusPageLink(pg) + ' обнаружен установленный шаблон <code>' + escapeHtml(conflict.templateDisplay || conflict.templateName || conflict.label || 'шаблон') + '</code>.', null, { trackError: false });
}
next();
});
}, function (err) {
if (err) {
logStatus('Проверка статей.', err, { statusId: statusId });
cb(err);
return;
}
logStatus(
conflicts.length
? 'Проверка завершена: найдены статьи с уже установленными шаблонами.'
: 'Проверка завершена: конфликтов не найдено.',
null,
{ statusId: statusId, trackError: false }
);
cb(null, conflicts);
});
}
function buildNominationConflictResolutionHtml(conflicts) {
return '<div class="rmInfoBox"><p style="margin:0 0 6px;">Найдены статьи, где шаблон уже стоит. Для каждой конфликтующей страницы выберите, что делать со статьёй и с шаблоном.</p>' +
'<p style="margin:0;color:' + tk.cSubM + ';font-size:12px;line-height:1.45;">По умолчанию такие статьи исключаются из новой номинации, а существующий шаблон остаётся без изменений.</p></div>' +
'<div class="rmConflictLead">После выбора нажмите «Продолжить номинирование».</div>' +
'<div id="rmConflictList" class="rmConflictList">' +
conflicts.map(function (conflict, index) {
var pageLink = buildStatusPageLink(conflict.pageName);
return '<div class="rmConflictCard" data-rm-conflict-index="' + index + '">' +
'<input type="hidden" class="rmConflictPageAction" value="skip">' +
'<input type="hidden" class="rmConflictTemplateAction" value="keep">' +
'<div class="rmConflictTitle">' + pageLink + '</div>' +
'<div class="rmConflictMeta">Обнаружен установленный шаблон <code>' + escapeHtml(conflict.templateDisplay || conflict.templateName || conflict.label || 'шаблон') + '</code>.</div>' +
'<div class="rmConflictGroup">' +
'<div class="rmConflictGroupTitle">Действие со статьёй</div>' +
'<div class="rmConflictButtons" data-rm-conflict-group="page">' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice is-active" data-rm-choice-type="page" data-rm-choice="skip" aria-pressed="true">Убрать из номинации</button>' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice" data-rm-choice-type="page" data-rm-choice="keep" aria-pressed="false">Оставить в номинации</button>' +
'</div></div>' +
'<div class="rmConflictGroup">' +
'<div class="rmConflictGroupTitle">Действие с шаблоном</div>' +
'<div class="rmConflictButtons" data-rm-conflict-group="template">' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice is-active" data-rm-choice-type="template" data-rm-choice="keep" aria-pressed="true">Оставить как есть</button>' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice" data-rm-choice-type="template" data-rm-choice="overwrite" aria-pressed="false">Новая дата</button>' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice" data-rm-choice-type="template" data-rm-choice="prepend" aria-pressed="false">Второй сверху</button>' +
'</div>' +
'<div class="rmConflictHint">Если статья исключается из номинации, действие с шаблоном не применяется.</div>' +
'</div></div>';
}).join('') +
'</div>';
}
function updateNominationConflictCardState($card) {
var pageAction = $card.find('.rmConflictPageAction').val() || 'skip';
var disableTemplate = pageAction !== 'keep';
var $templateButtons = $card.find('[data-rm-choice-type="template"]');
$card.toggleClass('is-skip', disableTemplate);
$card.find('[data-rm-conflict-group="template"]').toggleClass('is-disabled', disableTemplate);
$templateButtons.prop('disabled', disableTemplate).toggleClass('is-disabled', disableTemplate);
}
function bindNominationConflictResolutionUi() {
var $content = $('#removerModalContent');
function setChoice($card, type, value) {
var inputClass = type === 'page' ? '.rmConflictPageAction' : '.rmConflictTemplateAction';
$card.find(inputClass).val(value);
$card.find('[data-rm-choice-type="' + type + '"]').each(function () {
var isActive = $(this).data('rmChoice') === value;
$(this).toggleClass('is-active', isActive).attr('aria-pressed', isActive ? 'true' : 'false');
});
updateNominationConflictCardState($card);
}
$content.off('.rmConflictResolution').on('click.rmConflictResolution', '[data-rm-choice-type]', function () {
var $btn = $(this);
var $card = $btn.closest('.rmConflictCard');
if ($btn.prop('disabled')) return;
setChoice($card, $btn.data('rmChoiceType'), $btn.data('rmChoice'));
});
$('#rmConflictList .rmConflictCard').each(function () { updateNominationConflictCardState($(this)); });
}
function collectNominationConflictResolution(conflicts) {
var decisions = {};
(conflicts || []).forEach(function (conflict, index) {
var $card = $('#rmConflictList .rmConflictCard[data-rm-conflict-index="' + index + '"]');
decisions[normTitle(conflict.pageName)] = {
pageAction: $card.find('.rmConflictPageAction').val() || 'skip',
templateAction: $card.find('.rmConflictTemplateAction').val() || 'keep'
};
});
return decisions;
}
function applyNominationConflictResolutionToJob(job, decisions) {
var resultArticles;
var headerText;
if (!job || !job.isMulti) {
job.conflictDecisions = decisions || {};
return { value: job };
}
resultArticles = (job.multiArticles || []).filter(function (pageName) {
var decision = decisions && decisions[normTitle(pageName)];
return !decision || decision.pageAction !== 'skip';
});
if (!resultArticles.length) return { error: 'После исключения конфликтующих статей в номинации не осталось ни одной страницы.' };
job.conflictDecisions = decisions || {};
job.multiArticles = resultArticles.slice();
job.pages = resultArticles.slice().reverse();
if (job.multiRenamePairs && job.multiRenamePairs.length) {
job.multiRenamePairs = job.multiRenamePairs.filter(function (pair) {
return pair && resultArticles.indexOf(pair.pageName) !== -1;
});
}
if (job.multiRenameTargets) {
job.multiRenameTargets = resultArticles.reduce(function (map, pageName) {
map[normTitle(pageName)] = getMultiRenameTarget(job, pageName, 'multiRenameTargets');
return map;
}, {});
}
if (job.multiRenameTemplateTargets) {
job.multiRenameTemplateTargets = resultArticles.reduce(function (map, pageName) {
map[normTitle(pageName)] = getMultiRenameTarget(job, pageName, 'multiRenameTemplateTargets');
return map;
}, {});
}
headerText = String(job.multiHeaderText || '').trim();
job.section = headerText || (job.opId === 'mRnm' ? formatRenameItemsWithAnd(resultArticles, job.multiRenameTargets) : ('[[:' + resultArticles[0] + ']]'));
job.sectionNW = job.section.replace(/\[\[:/g, '').replace(/]]/g, '');
job.msg = job.multiNominationFormat === 'list'
? buildMultiNominationListText(resultArticles, job.multiNominationBody, job.multiArticleComments, job.opId === 'mRnm' ? getMultiRenameDiscussionOptions(job.multiRenameTargets) : null)
: buildMultiNominationText(resultArticles, job.multiNominationBody, job.multiArticleComments, job.opId === 'mRnm'
? getMultiRenameDiscussionOptions(job.multiRenameTargets, { leadingBlankLine: false })
: { leadingBlankLine: false });
job.summary = makeNominationTargetSummary(job.nomPage, job.sectionNW, job.summaryAction);
job.nominationSummary = makeNominationPageSummary(job.sectionNW);
return { value: job };
}
function showNominationConflictResolution(job, conflicts, onContinue) {
resetModalObservers();
$('#removerModalContent').html(buildNominationConflictResolutionHtml(conflicts));
bindNominationConflictResolutionUi();
syncLinkWidths();
renderModalFooter('submit', {
submitText: 'Продолжить номинирование',
showSubscribe: true,
preserveLogOnSubmit: true,
onSubmit: function () {
var decisions = collectNominationConflictResolution(conflicts);
var applied = applyNominationConflictResolutionToJob(job, decisions);
if (applied.error) {
alert(applied.error);
return false;
}
if (typeof onContinue === 'function') onContinue(applied.value);
return true;
}
});
}
function bindTouchTextareaGrip($ta, sync, getMaxWidth, options) {
var opts = options || {};
var minWidth = parseInt(opts.minWidth, 10) || parseInt(sz.taMinW, 10) || 180;
var minHeight = parseInt(opts.minHeight, 10) || parseInt(sz.taMinH, 10) || 100;
var allowWidthResize = opts.allowWidth !== false;
var syncFn = typeof sync === 'function' ? sync : function () {};
var getMaxWidthFn = typeof getMaxWidth === 'function' ? getMaxWidth : function () { return $ta.outerWidth() || minWidth; };
var usePointerEvents = typeof window.PointerEvent === 'function';
var dragState = { active: false, startX: 0, startY: 0, startWidth: 0, startHeight: 0 };
var gripStyle = 'height:20px;box-sizing:border-box;border:1px solid ' + tk.bSub + ';border-top:0;border-radius:0 0 4px 4px;background:' + tk.bgNSub + ';display:flex;align-items:center;justify-content:center;cursor:ns-resize;touch-action:none;user-select:none;-webkit-user-select:none;';
if (opts.gripMarginBottom) gripStyle += 'margin-bottom:' + opts.gripMarginBottom + ';';
var $grip = $('<div data-rm-textarea-grip="1" style="' + gripStyle + '"><span style="display:block;width:42px;height:4px;border-radius:999px;background:' + tk.bSub + ';opacity:.9;"></span></div>');
function getCoord(evt, key) {
var e = evt.originalEvent || evt;
if (e.touches && e.touches.length) return e.touches[0][key];
if (e.changedTouches && e.changedTouches.length) return e.changedTouches[0][key];
return e[key];
}
function stopDrag() {
dragState.active = false;
$(window).off('.rmTaResize');
}
function onDragMove(evt) {
var clientX;
var clientY;
if (!dragState.active) return;
clientX = getCoord(evt, 'clientX');
clientY = getCoord(evt, 'clientY');
if (typeof clientY !== 'number') return;
if (allowWidthResize && typeof clientX === 'number') $ta.css('width', Math.max(minWidth, Math.min(getMaxWidthFn(), dragState.startWidth + (clientX - dragState.startX))) + 'px');
$ta.css('height', Math.max(minHeight, dragState.startHeight + (clientY - dragState.startY)) + 'px');
syncFn();
if (evt.preventDefault) evt.preventDefault();
}
function startDrag(evt) {
var clientY = getCoord(evt, 'clientY');
if (typeof clientY !== 'number') return;
dragState.active = true;
dragState.startX = getCoord(evt, 'clientX') || 0;
dragState.startY = clientY;
dragState.startWidth = $ta.outerWidth();
dragState.startHeight = $ta.outerHeight();
if (evt.preventDefault) evt.preventDefault();
$(window).off('.rmTaResize');
if (usePointerEvents) {
$(window).on('pointermove.rmTaResize', onDragMove).on('pointerup.rmTaResize pointercancel.rmTaResize', stopDrag);
} else {
$(window).on('touchmove.rmTaResize mousemove.rmTaResize', onDragMove).on('touchend.rmTaResize touchcancel.rmTaResize mouseup.rmTaResize', stopDrag);
}
}
$ta.css({ 'border-bottom-left-radius': '0', 'border-bottom-right-radius': '0' });
$ta.next('[data-rm-textarea-grip]').remove();
$ta.after($grip);
if (usePointerEvents) $grip.on('pointerdown.rmTaGrip', startDrag);
else $grip.on('touchstart.rmTaGrip mousedown.rmTaGrip', startDrag);
}
function applyModalContentWidth($modal, contentWidth, options) {
var opts = options || {};
var layout = getModalLayout();
var modalFrame = getBoxFrameWidth($modal);
var safeContentWidth = Math.max(layout.minWidth, Math.min(contentWidth, layout.maxOuterWidth - modalFrame));
var modalWidth = safeContentWidth + modalFrame;
var initialContentW = parseFloat($modal.data('rmInitialContentW')) || 0;
$modal.css({
width: modalWidth + 'px',
'max-width': layout.maxOuterWidth + 'px',
'box-sizing': 'border-box',
'margin-left': layout.shouldCenter ? 'auto' : '0',
'margin-right': layout.shouldCenter ? 'auto' : '0'
}).toggleClass('rmCompactContent', safeContentWidth < 520);
$('.' + RESIZE_CLASS).css({
width: safeContentWidth + 'px',
'max-width': '100%',
'box-sizing': 'border-box'
});
$('#rmMsg,#nominationReason,#rmReportText').each(function () {
var $textarea = $(this);
var textareaId = this.id;
if (!$textarea.length) return;
$textarea.css('width', safeContentWidth + 'px');
$textarea.next('[data-rm-textarea-grip]').css('width', safeContentWidth + 'px');
$('.rmQuickPhrasesPanel[data-rm-target="' + textareaId + '"]').css('width', safeContentWidth + 'px');
});
$('.rmNestedCommentInput').each(function () {
var $textarea = $(this);
var $wrap = $textarea.parent();
var containerFrame = parseFloat($textarea.data('rmNestedContainerFrame')) || 0;
var wrapFrame = parseFloat($textarea.data('rmNestedWrapFrame')) || 0;
var safeMinWidth = parseFloat($textarea.data('rmNestedMinWidth')) || 0;
var wrapOuterWidth;
var textareaWidth;
if (!$wrap.length || !$wrap.is(':visible')) return;
wrapOuterWidth = Math.max(1, safeContentWidth - containerFrame);
textareaWidth = Math.max(1, wrapOuterWidth - wrapFrame);
$wrap.css({
width: wrapOuterWidth + 'px',
'max-width': '100%',
'box-sizing': 'border-box'
});
$textarea.css({
width: textareaWidth + 'px',
'min-width': Math.min(safeMinWidth || textareaWidth, textareaWidth) + 'px'
});
$textarea.next('[data-rm-textarea-grip]').css('width', textareaWidth + 'px');
$('.rmQuickPhrasesPanel[data-rm-target="' + this.id + '"]').css('width', textareaWidth + 'px');
});
syncLinkWidths();
if (isVector22) {
var $content = $('#content');
if ($content.length && modalWidth > initialContentW) $content.css({ 'min-width': modalWidth + 'px' });
else if ($content.length) $content.css({ 'min-width': '' });
} else {
$('#content').css({ 'min-width': '' });
}
if (!opts.skipStore) $modal.data('rmContentWidth', safeContentWidth);
return safeContentWidth;
}
function setupNestedResizableTextarea(textareaId, wrapId, minWidth, minHeight) {
var $ta = $('#' + textareaId);
var $wrap = $('#' + wrapId);
var $modal = $('#removerModal');
var $container = $wrap.parent();
var layout = getModalLayout();
var safeMinWidth = parseInt(minWidth, 10) || 280;
var safeMinHeight = parseInt(minHeight, 10) || 90;
var initialWidth;
var modalFrame;
var containerFrame;
var wrapFrame;
function px($el, prop) { var n = parseFloat($el.css(prop)); return isNaN(n) ? 0 : n; }
function getMaxTextareaWidth(currentLayout) {
return Math.max(1, currentLayout.maxOuterWidth - modalFrame - containerFrame - wrapFrame);
}
function getEffectiveMinWidth(currentLayout) {
return Math.min(safeMinWidth, getMaxTextareaWidth(currentLayout));
}
function sync() {
var currentLayout = getModalLayout();
var maxTextareaWidth = getMaxTextareaWidth(currentLayout);
var textareaWidth = $ta.outerWidth();
var contentWidth;
if (!$wrap.is(':visible')) return;
if (textareaWidth > maxTextareaWidth) {
$ta.css('width', maxTextareaWidth + 'px');
textareaWidth = $ta.outerWidth();
}
contentWidth = Math.min(currentLayout.maxOuterWidth - modalFrame, textareaWidth + wrapFrame + containerFrame);
applyModalContentWidth($modal, contentWidth);
}
if (!$ta.length || !$wrap.length || !$modal.length) return;
modalFrame = px($modal, 'padding-left') + px($modal, 'padding-right') + px($modal, 'border-left-width') + px($modal, 'border-right-width');
containerFrame = $container.length
? px($container, 'padding-left') + px($container, 'padding-right') + px($container, 'border-left-width') + px($container, 'border-right-width')
: 0;
wrapFrame = px($wrap, 'padding-left') + px($wrap, 'padding-right') + px($wrap, 'border-left-width') + px($wrap, 'border-right-width');
initialWidth = Math.min(
Math.max(getEffectiveMinWidth(layout), getDefaultResizableWidth(modalFrame + containerFrame + wrapFrame)),
getMaxTextareaWidth(layout)
);
$ta.css({
width: initialWidth + 'px',
'min-width': getEffectiveMinWidth(layout) + 'px',
'min-height': safeMinHeight + 'px',
'box-sizing': 'border-box',
resize: layout.useFullWidth ? 'none' : 'both',
'border-bottom-left-radius': '',
'border-bottom-right-radius': ''
});
$ta.data('rmNestedContainerFrame', containerFrame);
$ta.data('rmNestedWrapFrame', wrapFrame);
$ta.data('rmNestedMinWidth', safeMinWidth);
$ta.next('[data-rm-textarea-grip]').remove();
if (layout.useFullWidth) {
bindTouchTextareaGrip($ta, sync, function () {
return getMaxTextareaWidth(getModalLayout());
}, {
minWidth: getEffectiveMinWidth(layout),
minHeight: safeMinHeight
});
}
registerModalLayoutSync(sync);
$(window).off('resize.removerModal').on('resize.removerModal', syncModalLayout);
if (typeof ResizeObserver === 'function') {
var observer = new ResizeObserver(sync);
observer.observe($ta[0]);
registerResizeObserver(observer);
}
sync();
}
function setupResizableModal(textareaId) {
var $ta = $('#' + textareaId);
var $modal = $('#removerModal');
var layout = getModalLayout();
function px($el, prop) { var n = parseFloat($el.css(prop)); return isNaN(n) ? 0 : n; }
applyV2022Layout($modal);
$modal.css({ display: 'block', 'margin-left': layout.shouldCenter ? 'auto' : '0', 'margin-right': layout.shouldCenter ? 'auto' : '0' });
var isBorderBox = ($modal.css('box-sizing') || '').toLowerCase() === 'border-box';
var hFrame = px($modal, 'padding-left') + px($modal, 'padding-right') + px($modal, 'border-left-width') + px($modal, 'border-right-width');
var initialContentW = isVector22 ? ($('#content').outerWidth() || 0) : 0;
var minWidth = layout.minWidth;
$modal.data('rmInitialContentW', initialContentW);
$ta.css({ width: getDefaultResizableWidth(hFrame) + 'px', height: sz.taH, padding: '8px', 'box-sizing': 'border-box',
border: '1px solid ' + tk.bSub, 'border-radius': '2px', background: tk.bgBase,
color: 'inherit', resize: layout.useFullWidth ? 'none' : 'both', 'min-height': sz.taMinH, 'min-width': sz.taMinW });
$(window).off('.rmTaResize');
function sync() {
var currentLayout = getModalLayout();
var maxTextareaWidth = Math.max(minWidth, currentLayout.maxOuterWidth - Math.floor(hFrame));
var w = $ta.outerWidth();
if (w > maxTextareaWidth) {
$ta.css('width', maxTextareaWidth + 'px');
w = $ta.outerWidth();
}
applyModalContentWidth($modal, isBorderBox ? w : Math.min(currentLayout.maxOuterWidth - hFrame, w));
}
if (layout.useFullWidth) bindTouchTextareaGrip($ta, sync, function () {
return Math.max(minWidth, getModalLayout().maxOuterWidth - Math.floor(hFrame));
});
else {
$ta.css({ 'border-bottom-left-radius': '', 'border-bottom-right-radius': '' });
$ta.next('[data-rm-textarea-grip]').remove();
}
registerModalLayoutSync(sync);
$(window).off('resize.removerModal').on('resize.removerModal', syncModalLayout);
if (typeof ResizeObserver === 'function') {
var observer = new ResizeObserver(sync);
observer.observe($ta[0]);
registerResizeObserver(observer);
}
sync();
}
function addInputRow(opts) {
var w = $('#rmMsg,#nominationReason,#rmReportText').first().outerWidth() || 0;
var indentLeft = Math.max(0, parseInt(opts.indentLeft, 10) || 0);
var rowClass = opts.rowClass ? ' ' + opts.rowClass : '';
var widthStyle = opts.fitParentWidth
? 'width:calc(100% - ' + indentLeft + 'px);box-sizing:border-box;'
: (opts.autoWidth ? '' : (w ? 'width:' + Math.max(1, w - indentLeft) + 'px;' : ''));
$('#' + opts.containerId).append(joinHtml([
'<div class="rmInputRow ', RESIZE_CLASS, rowClass, '" style="', stRow, widthStyle, indentLeft ? 'margin-left:' + indentLeft + 'px;' : '', '">',
opts.prefixHtml || '',
'<input type="text" class="', opts.inputClass || 'variantInput', '" placeholder="', opts.placeholder || '', '" style="', stInputBox, '">',
opts.removeBeforeInput ? '' : '<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить">−</button>',
'</div>'
]));
syncModalLayout();
}
$(document).off('click.rmRemoveInput').on('click.rmRemoveInput', '.rmRemoveInput', function () {
$(this).closest('.rmInputRow').remove();
syncModalLayout();
});
$(document).off('click.rmQuickPhraseInsert').on('click.rmQuickPhraseInsert', '.rmQuickPhraseActionBtn', function (e) {
var targetId;
var phrase;
e.preventDefault();
targetId = $(this).data('rmTarget');
phrase = $(this).attr('data-rm-phrase') || '';
if (!targetId) return;
insertTextIntoTextarea($('#' + targetId), phrase);
});
function buildMultiInputHtml(c) {
var addLabel = c.addBtnLabel || '+ Добавить';
var addClass = c.type === 'rename' ? 'rmAddVariantBtn rmRenameVariantAddBtn' : '';
var addSymbol = c.type === 'rename' ? '⤷' : '+';
return joinHtml([
'<div class="rmInputRow ', RESIZE_CLASS, '" style="', stRow, '">',
'<input id="', c.firstId, '" type="text" class="', c.inputClass || 'variantInput', '" placeholder="', c.firstPh || '', '" style="', stInputBox, '">',
buildSquareAddButtonHtml(c.addBtnId, addLabel.replace(/^\+\s*/, '') || 'Добавить', addClass, addSymbol),
'</div>',
'<div id="', c.containerId, '"></div>'
]);
}
function wireMultiInput(c) {
$('#' + c.addBtnId).click(function () {
var count = $('#' + c.containerId + ' .rmInputRow').length;
if (typeof c.maxRows === 'number' && count >= c.maxRows) { alert(c.maxMsg || 'Достигнут лимит.'); return; }
addInputRow({
containerId: c.containerId,
placeholder: c.addPh,
inputClass: c.inputClass,
rowClass: c.type === 'rename' ? 'rmRenameVariantRow' : '',
indentLeft: 0,
fitParentWidth: c.type === 'rename',
prefixHtml: c.type === 'rename' ? buildLeftRemoveButtonHtml('rmRemoveInput', 'Удалить') : '',
removeBeforeInput: c.type === 'rename'
});
});
}
function buildSettingsFieldHtml(label, controlHtml, helpText, options) {
var opts = options || {};
var helpHtml = helpText
? '<div class="rmSettingsFieldHint">' + helpText + '</div>'
: '';
var labelHtml = opts.forId
? '<label class="rmSettingsFieldLabel" for="' + opts.forId + '">' + label + '</label>'
: '<div class="rmSettingsFieldLabel">' + label + '</div>';
return joinHtml([
'<div class="rmSettingsField">',
labelHtml,
'<div class="rmSettingsFieldControl">', controlHtml, '</div>',
helpHtml,
'</div>'
]);
}
function buildSettingsSectionHtml(title, bodyHtml, helpText, options) {
var opts = options || {};
var headerHtml = '';
var description = [];
if (opts.titleNote) description.push(opts.titleNote);
if (helpText) description.push(helpText);
if (title || description.length) {
headerHtml = joinHtml([
'<div class="rmSettingsSectionHeader">',
title ? '<div class="rmSettingsSectionTitle">' + title + '</div>' : '',
description.length ? '<div class="rmSettingsSectionDescription">' + description.join(' ') + '</div>' : '',
'</div>'
]);
}
return joinHtml([
'<div class="rmSettingsSection">',
headerHtml,
bodyHtml,
'</div>'
]);
}
function buildSettingsSimpleCheckboxHtml(id, text) {
return joinHtml([
'<label class="rmSettingsCheck">',
'<input id="', id, '" type="checkbox">',
'<span>', text, '</span>',
'</label>'
]);
}
function buildQuickPhrasesSettingsEditorHtml() {
return joinHtml([
'<div id="rmSettingsQuickPhrasesEditor" class="rmQuickPhraseEditor">',
'<div id="rmSettingsQuickPhrasesList" class="rmQuickPhraseList"></div>',
'<input id="rmSettingsQuickPhraseInput" type="text" autocomplete="off" style="', stInputFull, 'margin-bottom:0;">',
'<div id="rmSettingsQuickPhraseMeta" class="rmQuickPhraseMeta"></div>',
'</div>'
]);
}
function getQuickPhraseEditor() {
return $('#rmSettingsQuickPhrasesEditor');
}
function getQuickPhraseEditorState() {
var $editor = getQuickPhraseEditor();
var phrases = normalizeQuickPhrasesList($editor.data('rmQuickPhrases'), []);
var editingIndex = parseInt($editor.data('rmQuickPhraseEditingIndex'), 10);
if (isNaN(editingIndex)) editingIndex = -1;
return { editor: $editor, phrases: phrases, editingIndex: editingIndex };
}
function setQuickPhraseEditorState(phrases, editingIndex) {
var $editor = getQuickPhraseEditor();
var normalized = normalizeQuickPhrasesList(phrases, []);
var safeEditingIndex = parseInt(editingIndex, 10);
if (isNaN(safeEditingIndex) || safeEditingIndex < 0 || safeEditingIndex >= normalized.length) safeEditingIndex = -1;
if (!$editor.length) return;
$editor.data('rmQuickPhrases', normalized);
$editor.data('rmQuickPhraseEditingIndex', safeEditingIndex);
renderQuickPhraseEditor();
}
function clearQuickPhraseDropState() {
var $editor = getQuickPhraseEditor();
$editor.removeData('rmQuickPhraseDragIndex');
$editor.removeData('rmQuickPhraseDropIndex');
$editor.removeData('rmQuickPhraseDropAfter');
$editor.find('.rmQuickPhraseChip').removeClass('is-dragging is-drop-before is-drop-after');
}
function renderQuickPhraseEditor() {
var state = getQuickPhraseEditorState();
var $list = $('#rmSettingsQuickPhrasesList');
var $input = $('#rmSettingsQuickPhraseInput');
var $meta = $('#rmSettingsQuickPhraseMeta');
if (!state.editor.length || !$list.length || !$input.length) return;
if (state.phrases.length) {
$list.html(state.phrases.map(function (phrase, index) {
var chipClass = 'rmQuickPhraseChip' + (index === state.editingIndex ? ' is-editing' : '');
return joinHtml([
'<div class="', chipClass, '" draggable="true" data-rm-quick-index="', index, '">',
'<button type="button" class="rmQuickPhraseEditBtn" title="Редактировать фразу">', escapeHtml(phrase), '</button>',
'<button type="button" class="rmQuickPhraseRemoveBtn" title="Удалить фразу" aria-label="Удалить фразу">×</button>',
'</div>'
]);
}).join(''));
} else {
$list.html('<div class="rmQuickPhraseEmpty">Фразы пока не добавлены.</div>');
}
$input
.attr('placeholder', state.editingIndex >= 0 ? 'Изменить значение...' : 'Добавить значение...')
.toggleClass('is-editing', state.editingIndex >= 0);
$meta
.text('')
.hide();
}
function notifyQuickPhraseEditorChanged() {
var $editor = getQuickPhraseEditor();
if ($editor.length) $editor.trigger('rmQuickPhrasesChanged');
}
function startQuickPhraseEdit(index) {
var state = getQuickPhraseEditorState();
var $input = $('#rmSettingsQuickPhraseInput');
if (index < 0 || index >= state.phrases.length || !$input.length) return;
state.editor.data('rmQuickPhraseEditingIndex', index);
$input.val(state.phrases[index]);
renderQuickPhraseEditor();
$input.trigger('focus');
if ($input[0] && typeof $input[0].select === 'function') $input[0].select();
}
function cancelQuickPhraseEdit() {
var state = getQuickPhraseEditorState();
var $input = $('#rmSettingsQuickPhraseInput');
state.editor.data('rmQuickPhraseEditingIndex', -1);
if ($input.length) $input.val('').removeClass('is-editing');
renderQuickPhraseEditor();
}
function saveQuickPhraseInput() {
var state = getQuickPhraseEditorState();
var $input = $('#rmSettingsQuickPhraseInput');
var value = normalizeQuickPhraseValue($input.val());
var next = [];
if (!$input.length || !value) return false;
if (state.editingIndex >= 0) {
state.phrases.forEach(function (phrase, index) {
if (index === state.editingIndex) {
next.push(value);
return;
}
if (phrase !== value && next.indexOf(phrase) === -1) next.push(phrase);
});
} else {
next = state.phrases.slice();
if (next.indexOf(value) === -1) next.push(value);
}
state.editor.data('rmQuickPhrases', normalizeQuickPhrasesList(next, []));
state.editor.data('rmQuickPhraseEditingIndex', -1);
$input.val('').removeClass('is-editing');
clearQuickPhraseDropState();
renderQuickPhraseEditor();
notifyQuickPhraseEditorChanged();
return true;
}
function removeQuickPhrase(index) {
var state = getQuickPhraseEditorState();
var $input = $('#rmSettingsQuickPhraseInput');
if (index < 0 || index >= state.phrases.length) return;
state.phrases.splice(index, 1);
state.editor.data('rmQuickPhrases', state.phrases);
if (state.editingIndex === index) {
state.editor.data('rmQuickPhraseEditingIndex', -1);
if ($input.length) $input.val('').removeClass('is-editing');
} else if (state.editingIndex > index) {
state.editor.data('rmQuickPhraseEditingIndex', state.editingIndex - 1);
}
clearQuickPhraseDropState();
renderQuickPhraseEditor();
notifyQuickPhraseEditorChanged();
}
function reorderQuickPhrases(phrases, fromIndex, toIndex, placeAfter) {
var result = phrases.slice();
var insertIndex = toIndex + (placeAfter ? 1 : 0);
var item;
if (fromIndex < 0 || fromIndex >= result.length || toIndex < 0 || toIndex >= result.length) return result;
item = result.splice(fromIndex, 1)[0];
if (fromIndex < insertIndex) insertIndex--;
result.splice(insertIndex, 0, item);
return result;
}
function getQuickPhraseDropPointer(evt) {
var originalEvent = evt && (evt.originalEvent || evt);
if (!originalEvent) return null;
if (typeof originalEvent.clientX !== 'number' || typeof originalEvent.clientY !== 'number') return null;
return { x: originalEvent.clientX, y: originalEvent.clientY };
}
function getQuickPhraseDropTarget($list, pointer, dragIndex) {
var candidates = [];
var rowCandidates;
var minRowDistance = Infinity;
var bestBoundary = null;
var bestBoundaryDistance = Infinity;
if (!$list || !$list.length || !pointer) return null;
$list.children('.rmQuickPhraseChip').each(function () {
var index = parseInt($(this).attr('data-rm-quick-index'), 10);
var rect;
var rowDistance;
if (isNaN(index) || index === dragIndex) return;
rect = this.getBoundingClientRect();
if (!rect.width || !rect.height) return;
rowDistance = pointer.y < rect.top ? (rect.top - pointer.y) : (pointer.y > rect.bottom ? (pointer.y - rect.bottom) : 0);
candidates.push({
node: this,
index: index,
left: rect.left,
right: rect.right,
top: rect.top,
bottom: rect.bottom,
midX: rect.left + rect.width / 2,
rowDistance: rowDistance
});
if (rowDistance < minRowDistance) minRowDistance = rowDistance;
});
if (!candidates.length) return null;
rowCandidates = candidates
.filter(function (candidate) { return candidate.rowDistance === minRowDistance; })
.sort(function (a, b) {
if (a.left !== b.left) return a.left - b.left;
return a.index - b.index;
});
if (!rowCandidates.length) return null;
if (pointer.x <= rowCandidates[0].left) {
return { index: rowCandidates[0].index, placeAfter: false, node: rowCandidates[0].node };
}
if (pointer.x >= rowCandidates[rowCandidates.length - 1].right) {
return {
index: rowCandidates[rowCandidates.length - 1].index,
placeAfter: true,
node: rowCandidates[rowCandidates.length - 1].node
};
}
for (var i = 0; i < rowCandidates.length; i++) {
var candidate = rowCandidates[i];
if (pointer.x >= candidate.left && pointer.x <= candidate.right) {
return {
index: candidate.index,
placeAfter: pointer.x > candidate.midX,
node: candidate.node
};
}
}
rowCandidates.forEach(function (candidate) {
var leftDistance = Math.abs(pointer.x - candidate.left);
var rightDistance = Math.abs(pointer.x - candidate.right);
if (leftDistance < bestBoundaryDistance) {
bestBoundaryDistance = leftDistance;
bestBoundary = { index: candidate.index, placeAfter: false, node: candidate.node };
}
if (rightDistance < bestBoundaryDistance) {
bestBoundaryDistance = rightDistance;
bestBoundary = { index: candidate.index, placeAfter: true, node: candidate.node };
}
});
return bestBoundary;
}
function bindQuickPhrasesEditor() {
var $editor = getQuickPhraseEditor();
var $list = $('#rmSettingsQuickPhrasesList');
var $input = $('#rmSettingsQuickPhraseInput');
function updateQuickPhraseDropTarget(evt) {
var dragIndex = parseInt($editor.data('rmQuickPhraseDragIndex'), 10);
var pointer = getQuickPhraseDropPointer(evt);
var target;
if (isNaN(dragIndex) || !pointer) return null;
target = getQuickPhraseDropTarget($list, pointer, dragIndex);
if (!target) return null;
$editor.data('rmQuickPhraseDropIndex', target.index);
$editor.data('rmQuickPhraseDropAfter', !!target.placeAfter);
$editor.find('.rmQuickPhraseChip').removeClass('is-drop-before is-drop-after');
$(target.node).addClass(target.placeAfter ? 'is-drop-after' : 'is-drop-before');
return target;
}
function applyQuickPhraseDrop() {
var state = getQuickPhraseEditorState();
var fromIndex = parseInt($editor.data('rmQuickPhraseDragIndex'), 10);
var toIndex = parseInt($editor.data('rmQuickPhraseDropIndex'), 10);
var placeAfter = $editor.data('rmQuickPhraseDropAfter') === true;
var changed = false;
if (!isNaN(fromIndex) && !isNaN(toIndex) && fromIndex !== toIndex) {
state.editor.data('rmQuickPhrases', reorderQuickPhrases(state.phrases, fromIndex, toIndex, placeAfter));
if (state.editingIndex === fromIndex) state.editor.data('rmQuickPhraseEditingIndex', -1);
changed = true;
}
clearQuickPhraseDropState();
renderQuickPhraseEditor();
if (changed) notifyQuickPhraseEditorChanged();
}
if (!$editor.length || !$list.length || !$input.length) return;
$editor.off('.rmQuickPhraseEditor');
$list.off('.rmQuickPhraseEditor');
$input.off('.rmQuickPhraseEditor');
$input.on('keydown.rmQuickPhraseEditor', function (e) {
if (e.key === 'Enter' || e.keyCode === 13) {
e.preventDefault();
saveQuickPhraseInput();
} else if (e.key === 'Escape' || e.keyCode === 27) {
e.preventDefault();
cancelQuickPhraseEdit();
}
});
$editor
.on('click.rmQuickPhraseEditor', '.rmQuickPhraseEditBtn', function () {
var index = parseInt($(this).closest('.rmQuickPhraseChip').attr('data-rm-quick-index'), 10);
startQuickPhraseEdit(index);
})
.on('click.rmQuickPhraseEditor', '.rmQuickPhraseRemoveBtn', function (e) {
var index;
e.preventDefault();
e.stopPropagation();
index = parseInt($(this).closest('.rmQuickPhraseChip').attr('data-rm-quick-index'), 10);
removeQuickPhrase(index);
})
.on('dragstart.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) {
var index = parseInt($(this).attr('data-rm-quick-index'), 10);
if (isNaN(index)) return;
$editor.data('rmQuickPhraseDragIndex', index);
$(this).addClass('is-dragging');
if (e.originalEvent && e.originalEvent.dataTransfer) {
e.originalEvent.dataTransfer.effectAllowed = 'move';
e.originalEvent.dataTransfer.setData('text/plain', String(index));
}
})
.on('dragover.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) {
if ($editor.data('rmQuickPhraseDragIndex') === undefined) return;
e.preventDefault();
updateQuickPhraseDropTarget(e);
if (e.originalEvent && e.originalEvent.dataTransfer) e.originalEvent.dataTransfer.dropEffect = 'move';
})
.on('drop.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) {
e.preventDefault();
updateQuickPhraseDropTarget(e);
applyQuickPhraseDrop();
})
.on('dragend.rmQuickPhraseEditor', '.rmQuickPhraseChip', function () {
clearQuickPhraseDropState();
});
$list
.on('dragover.rmQuickPhraseEditor', function (e) {
if ($editor.data('rmQuickPhraseDragIndex') === undefined) return;
e.preventDefault();
updateQuickPhraseDropTarget(e);
if (e.originalEvent && e.originalEvent.dataTransfer) e.originalEvent.dataTransfer.dropEffect = 'move';
})
.on('drop.rmQuickPhraseEditor', function (e) {
e.preventDefault();
updateQuickPhraseDropTarget(e);
applyQuickPhraseDrop();
});
renderQuickPhraseEditor();
}
function collectQuickPhraseValues() {
return getQuickPhraseEditorState().phrases;
}
function collectQuickPhraseValuesSnapshot() {
var state = getQuickPhraseEditorState();
var value = normalizeQuickPhraseValue($('#rmSettingsQuickPhraseInput').val());
var next = state.phrases.slice();
if (!value) return next;
if (state.editingIndex >= 0) {
next[state.editingIndex] = value;
} else if (next.indexOf(value) === -1) {
next.push(value);
}
return normalizeQuickPhrasesList(next, []);
}
function isMenuTitlePresetValue(value) {
return value === MENU_TITLE_PRESET_CACTIONS || value === MENU_TITLE_PRESET_PAGE || value === MENU_TITLE_PRESET_TOOLS;
}
function isMenuTitlePresetOnlySkin() {
return mwCfg.skin === 'minerva' || mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless';
}
function getDefaultMenuTitlePreset() {
return mwCfg.skin === 'minerva' ? MENU_TITLE_PRESET_TOOLS : MENU_TITLE_PRESET_CACTIONS;
}
function shouldPreserveStoredMenuTitleOnSave() {
return mwCfg.skin === 'minerva';
}
function isAvailableMenuTitlePresetValue(value) {
return getMenuTitlePresetOptions().some(function (option) {
return option.value === value;
});
}
function getMenuTitlePresetOptions() {
if (mwCfg.skin === 'minerva') {
return [
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Ещё' }
];
}
if (isVector22) {
return [
{ value: MENU_TITLE_PRESET_CACTIONS, label: 'Инструменты/Действия' },
{ value: MENU_TITLE_PRESET_PAGE, label: 'Страница' },
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты/Основное' }
];
}
if (mwCfg.skin === 'timeless') {
return [
{ value: MENU_TITLE_PRESET_CACTIONS, label: 'Инструменты для страниц' },
{ value: MENU_TITLE_PRESET_PAGE, label: 'Страница' },
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Вики-инструменты' }
];
}
if (mwCfg.skin === 'monobook') {
return [
{ value: MENU_TITLE_PRESET_CACTIONS, label: 'Верхняя панель' },
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты' }
];
}
return [
{ value: MENU_TITLE_PRESET_CACTIONS, label: mwCfg.skin === 'vector' ? 'Ещё' : 'Ещё / Действия' },
{ value: MENU_TITLE_PRESET_PAGE, label: 'Страница' },
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты' }
];
}
function getMenuTitlePresetHintText() {
var base = (mwCfg.skin === 'vector' || isVector22)
? 'Можно либо изменить заголовок отдельного меню Remover, либо перенести все пункты в одно из существующих стандартных меню.'
: 'На этом скине Remover использует существующие стандартные меню; отдельное меню с собственным заголовком не создаётся.';
if (isVector22) base += ' В Vector 2022 кнопки «Действия» и «Основное» находятся внутри общего меню «Инструменты».';
else if (mwCfg.skin === 'vector') base += ' В Vector отдельный заголовок создаёт собственное меню рядом с «Ещё».';
else if (mwCfg.skin === 'minerva') base += ' В Minerva Neue пункты Remover показываются в меню «Ещё».';
else if (mwCfg.skin === 'timeless') base += ' В Timeless доступны только стандартные варианты: «Инструменты для страниц», «Страница» и «Вики-инструменты».';
else if (mwCfg.skin === 'monobook') base += ' В MonoBook доступны только два варианта: поместить пункты в «Инструменты» или вывести их в верхнюю панель. Собственный заголовок меню здесь не используется.';
return base;
}
function getSignatureSeparatorPreviewText(value) {
var separator = String(value || '').trim();
return separator ? (separator + ' ' + '~~' + '~~') : ('~~' + '~~');
}
function updateSignatureSeparatorPreview(value) {
var previewValue = (typeof value === 'string') ? value : ($('#rmSettingsSignatureSeparator').val() || '');
var $code = $('#rmSettingsSignaturePreviewCode');
if (!$code.length) return;
$code.text(getSignatureSeparatorPreviewText(previewValue));
}
function bindSignatureSeparatorPreview() {
var $input = $('#rmSettingsSignatureSeparator');
if (!$input.length) return;
$input.off('.rmSignaturePreview').on('input.rmSignaturePreview change.rmSignaturePreview', function () {
updateSignatureSeparatorPreview($(this).val());
});
updateSignatureSeparatorPreview($input.val());
}
function buildMenuTitlePresetButtonsHtml() {
return joinHtml([
'<div class="rmSettingsMenuPresetWrap">',
'<div class="rmSettingsMenuPresetLabel">Перенос в стандартные меню</div>',
'<div id="rmSettingsMenuPresetBar" class="rmSettingsMenuPresetBar">',
getMenuTitlePresetOptions().map(function (option) {
return joinHtml([
'<button type="button" class="rmSettingsMenuPresetBtn" data-rm-menu-preset="',
option.value,
'" aria-pressed="false">',
escapeHtml(option.label),
'</button>'
]);
}).join(''),
'</div>',
'</div>'
]);
}
function applyMenuTitlePresetControls(presetValue) {
var preset = isMenuTitlePresetValue(presetValue) ? presetValue : '';
var $bar = $('#rmSettingsMenuPresetBar');
var $input = $('#rmSettingsMenuTitle');
var forcePresetOnly = isMenuTitlePresetOnlySkin();
if (!$bar.length || !$input.length) return;
$bar.data('rmPreset', preset);
$bar.find('.rmSettingsMenuPresetBtn').each(function () {
var isActive = $(this).data('rmMenuPreset') === preset;
$(this).toggleClass('is-active', isActive).attr('aria-pressed', isActive ? 'true' : 'false');
});
$input.prop('disabled', forcePresetOnly || !!preset);
}
function bindMenuTitlePresetControls() {
var $bar = $('#rmSettingsMenuPresetBar');
var $input = $('#rmSettingsMenuTitle');
if (!$bar.length || !$input.length) return;
$input.off('.rmSettingsMenuPreset').on('input.rmSettingsMenuPreset', function () {
if (!$bar.data('rmPreset')) $input.data('rmCustomValue', $input.val());
});
$bar.off('.rmSettingsMenuPreset').on('click.rmSettingsMenuPreset', '.rmSettingsMenuPresetBtn', function () {
var preset = $(this).data('rmMenuPreset');
var currentPreset = $bar.data('rmPreset') || '';
if (currentPreset === preset) {
if (isMenuTitlePresetOnlySkin()) return;
applyMenuTitlePresetControls('');
$input.val($input.data('rmCustomValue') || '');
$input.trigger('focus');
return;
}
$input.data('rmCustomValue', $input.val());
applyMenuTitlePresetControls(preset);
});
}
function fillSettingsFormValues(settings) {
var data = normalizeRemoverSettings(settings);
var forcePresetOnly = isMenuTitlePresetOnlySkin();
var menuTitleValue = data.menuTitle || '';
var storedMenuTitleValue = menuTitleValue;
if (forcePresetOnly && !isAvailableMenuTitlePresetValue(menuTitleValue)) menuTitleValue = getDefaultMenuTitlePreset();
var customMenuTitle = forcePresetOnly ? '' : (isMenuTitlePresetValue(menuTitleValue) ? settingsDefaults.menuTitle : menuTitleValue);
$('#rmSettingsForm').data('rmStoredMenuTitle', storedMenuTitleValue || '');
$('#rmSettingsNotifyAuthor').prop('checked', !!data.notifyAuthor);
$('#rmSettingsSubscribeTopic').prop('checked', !!data.subscribeTopic);
$('#rmSettingsAutoReloadAfterAction').prop('checked', !!data.autoReloadAfterAction);
$('#rmSettingsOpenNominationDiscussionInNewTab').prop('checked', !!data.openNominationDiscussionInNewTab);
$('#rmSettingsShowMenuIcons').prop('checked', !!data.showMenuIcons);
$('#rmSettingsMenuTitle').val(customMenuTitle || '').data('rmCustomValue', customMenuTitle || '');
$('#rmSettingsSignatureSeparator').val(data.signatureSeparator || '');
$('#rmSettingsExcludedNamespaces').val((data.excludedNamespaces || []).join(', '));
$('#rmSettingsDisabledItems').val((data.disabledItems || []).join(', '));
setQuickPhraseEditorState(data.quickPhrases || [], -1);
$('#rmSettingsQuickPhraseInput').val('').removeClass('is-editing');
clearQuickPhraseDropState();
applyMenuTitlePresetControls(menuTitleValue);
updateSignatureSeparatorPreview(data.signatureSeparator || '');
}
function collectSettingsFormValues(options) {
var opts = options || {};
var namespaces = parseNamespaceInput($('#rmSettingsExcludedNamespaces').val());
var disabledItems = parseDisabledItemsInput($('#rmSettingsDisabledItems').val());
var presetMenuTitle = $('#rmSettingsMenuPresetBar').data('rmPreset');
var forcePresetOnly = isMenuTitlePresetOnlySkin();
var storedMenuTitle = $('#rmSettingsForm').data('rmStoredMenuTitle');
if (namespaces.invalid.length) {
return { error: 'Некорректные номера пространств имён: ' + namespaces.invalid.join(', ') + '.' };
}
if (disabledItems.invalid.length) {
return { error: 'Неизвестные пункты меню: ' + disabledItems.invalid.join(', ') + '.' };
}
if (!opts.skipQuickPhraseCommit) saveQuickPhraseInput();
return {
value: normalizeRemoverSettings({
notifyAuthor: $('#rmSettingsNotifyAuthor').is(':checked'),
subscribeTopic: $('#rmSettingsSubscribeTopic').is(':checked'),
autoReloadAfterAction: $('#rmSettingsAutoReloadAfterAction').is(':checked'),
openNominationDiscussionInNewTab: $('#rmSettingsOpenNominationDiscussionInNewTab').is(':checked'),
showMenuIcons: $('#rmSettingsShowMenuIcons').is(':checked'),
menuTitle: forcePresetOnly
? (shouldPreserveStoredMenuTitleOnSave() && typeof storedMenuTitle === 'string'
? storedMenuTitle
: (isAvailableMenuTitlePresetValue(presetMenuTitle) ? presetMenuTitle : getDefaultMenuTitlePreset()))
: (isMenuTitlePresetValue(presetMenuTitle) ? presetMenuTitle : $('#rmSettingsMenuTitle').val()),
signatureSeparator: $('#rmSettingsSignatureSeparator').val(),
excludedNamespaces: namespaces.values,
disabledItems: disabledItems.values,
quickPhrases: opts.skipQuickPhraseCommit ? collectQuickPhraseValuesSnapshot() : collectQuickPhraseValues()
})
};
}
function updateSettingsSubmitReadyState(baselineSettings) {
var collected = collectSettingsFormValues({ skipQuickPhraseCommit: true });
var hasChanges = !!(collected.error || (collected.value && !areRemoverSettingsEqual(collected.value, baselineSettings)));
$('#removerSubmit').toggleClass('rmSubmitReady', hasChanges && !$('#removerSubmit').hasClass('rmSubmitError'));
$('#rmSettingsUnsavedHint').css('display', hasChanges ? 'inline-block' : 'none');
}
function bindSettingsSubmitReadyState(baselineSettings) {
var update = function () {
setTimeout(function () { updateSettingsSubmitReadyState(baselineSettings); }, 0);
};
$('#rmSettingsForm').off('.rmSettingsReady').on('input.rmSettingsReady change.rmSettingsReady', 'input, textarea, select', update);
$('#removerModalContent').off('click.rmSettingsReady keyup.rmSettingsReady').on(
'click.rmSettingsReady keyup.rmSettingsReady',
'.rmSettingsMenuPresetBtn,.rmQuickPhraseEditBtn,.rmQuickPhraseRemoveBtn,.rmQuickPhraseChip,#rmSettingsQuickPhraseInput',
update
);
$('#rmSettingsQuickPhrasesEditor').off('rmQuickPhrasesChanged.rmSettingsReady').on('rmQuickPhrasesChanged.rmSettingsReady', update);
updateSettingsSubmitReadyState(baselineSettings);
}
function buildSettingsFormHtml(menuLabelsHint) {
var menuTitleFieldLabel = isMenuTitlePresetOnlySkin() ? 'Размещение меню' : 'Заголовок отдельного меню';
var menuFields =
buildSettingsFieldHtml(menuTitleFieldLabel,
'<input id="rmSettingsMenuTitle" type="text" style="' + stInputFull + 'margin-bottom:0;">' + buildMenuTitlePresetButtonsHtml(),
getMenuTitlePresetHintText(), { forId: 'rmSettingsMenuTitle' }) +
buildSettingsFieldHtml('Визуальное оформление меню',
'<div class="rmSettingsChecks">' + buildSettingsSimpleCheckboxHtml('rmSettingsShowMenuIcons', 'Эмодзи в пунктах меню') + '</div>');
var messageFields =
buildSettingsFieldHtml('Префикс перед подписью',
'<input id="rmSettingsSignatureSeparator" type="text" style="' + stInputFull + 'margin-bottom:0;">',
'Добавляется перед подписью в публикуемых сообщениях.<span style="display:block;margin-top:6px;">Предпросмотр: <code id="rmSettingsSignaturePreviewCode"></code>.</span>',
{ forId: 'rmSettingsSignatureSeparator' }) +
buildSettingsFieldHtml('Часто используемые фразы', buildQuickPhrasesSettingsEditorHtml(),
'Enter для добавления. Порядок элементов изменяется перетаскиванием.', { forId: 'rmSettingsQuickPhraseInput' });
var defaultFields = '<div class="rmSettingsChecks">' +
buildSettingsSimpleCheckboxHtml('rmSettingsNotifyAuthor', 'Оповещать создателя страницы') +
buildSettingsSimpleCheckboxHtml('rmSettingsSubscribeTopic', 'Подписываться на номинацию') +
buildSettingsSimpleCheckboxHtml('rmSettingsOpenNominationDiscussionInNewTab', 'Открывать номинацию в отдельной вкладке') +
buildSettingsSimpleCheckboxHtml('rmSettingsAutoReloadAfterAction', 'Автообновление страницы после размещения<span style="display:block;margin-top:3px;color:' + tk.cSubM + ';font-size:12px;line-height:1.35;">Помешает взаимодействовать с логом действий.</span>') + '</div>';
var disableFields =
buildSettingsFieldHtml('Скрыть пункты меню',
'<input id="rmSettingsDisabledItems" type="text" style="' + stInputFull + 'margin-bottom:0;">',
'Названия пунктов через запятую, например <code>КБУ, КУЛ, КОБ</code>.' + menuLabelsHint,
{ forId: 'rmSettingsDisabledItems' }) +
buildSettingsFieldHtml('Не показывать в пространствах имён',
'<input id="rmSettingsExcludedNamespaces" type="text" style="' + stInputFull + 'margin-bottom:0;">',
'Номера пространств имён через запятую, например <code>2, 10, 828</code>. См. <a href="' + getPageUrl('Википедия:Пространства имён') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Википедия:Пространства имён</a>.',
{ forId: 'rmSettingsExcludedNamespaces' });
return joinHtml([
'<div id="rmSettingsForm" style="max-width:100%;">',
'<div class="rmSettingsLead">Настройки интерфейса и значений по умолчанию.</div>',
buildSettingsSectionHtml('Меню', menuFields, 'Настройки внешнего вида и состава меню Remover.'),
buildSettingsSectionHtml('Оформление сообщений', messageFields, 'Настройки оформления публикуемых сообщений в номинациях.'),
buildSettingsSectionHtml('Опции по умолчанию', defaultFields, 'Регулирует исходное состояние и поведение после выполнения действий.'),
buildSettingsSectionHtml('Отключение', disableFields, 'Скрывает отдельные пункты меню Remover или всё меню в выбранных пространствах имён.'),
'</div>'
]);
}
function buildSettingsFooterLeftHtml() {
return joinHtml([
'<div id="rmSettingsFooterLeft" style="display:flex;align-items:center;gap:6px;flex:1 1 auto;min-width:0;max-width:100%;flex-wrap:wrap;margin-right:auto;">',
'<button id="rmSettingsResetFooter" type="button" title="Удаляет сохранённые настройки Remover из вашего профиля MediaWiki и возвращает значения по умолчанию." style="', stCancel, '">Сбросить все настройки</button>',
'<a id="rmSettingsReportIssue" href="', getPageUrl('Обсуждение Википедии:Гаджеты/Remover'), '" target="_blank" rel="noopener noreferrer" ',
'title="Сообщить о проблеме или предложить улучшение" aria-label="Сообщить о проблеме или предложить улучшение" ',
'class="removerModalLink rmButtonLikeLink" style="', stCancel, 'display:inline-flex;align-items:center;justify-content:center;text-align:center;text-decoration:none;box-sizing:border-box;max-width:100%;line-height:1.2;word-break:normal;overflow-wrap:normal;">Обратная связь</a>',
'</div>'
]);
}
function openSettings() {
var currentSettings = normalizeRemoverSettings(state.settings || settingsDefaults);
var menuLabelsHint = buildSettingsMenuItemsHint();
var $previousModal = $('#removerModal').length ? $('#removerModal').detach() : $();
var previousLayoutSyncHandlers = modalLayoutSyncHandlers.slice();
function restorePreviousModal() {
closeModal();
if ($previousModal.length) {
$('#content').prepend($previousModal);
modalLayoutSyncHandlers = previousLayoutSyncHandlers.slice();
if (modalLayoutSyncHandlers.length) $(window).off('resize.removerModal').on('resize.removerModal', syncModalLayout);
syncModalLayout();
syncLinkWidths();
}
}
createModal({
title: 'Конфигурация',
width: 'compact',
showSettingsButton: false
});
$('#removerModal').addClass('rmModalSettings');
$('#removerModalHeaderBar').append(buildHeaderIconButtonHtml('rmSettingsBack', 'Назад', 'Назад', '←'));
$('#rmSettingsBack').on('click', restorePreviousModal);
$('#removerModalContent').html(buildSettingsFormHtml(menuLabelsHint));
fillSettingsFormValues(currentSettings);
bindMenuTitlePresetControls();
bindSignatureSeparatorPreview();
bindQuickPhrasesEditor();
renderModalFooter('submit', {
showCheckbox: false,
submitText: 'Сохранить',
onSubmit: function () {
var collected = collectSettingsFormValues();
var shouldReset;
var saveFn;
if (collected.error) {
alert(collected.error);
return false;
}
shouldReset = areRemoverSettingsEqual(collected.value, settingsDefaults);
saveFn = shouldReset
? function (callback) { resetSettingsOnServer(callback); }
: function (callback) { saveSettingsToServer(collected.value, callback); };
saveFn(function (err) {
if (err) {
alert('Не удалось ' + (shouldReset ? 'сбросить' : 'сохранить') + ' настройки: ' + (err.info || err.code || 'неизвестная ошибка') + '.');
unlockModalSubmit();
return;
}
location.reload();
});
}
});
var $settingsActions = $('#rmFooterActionButtons');
$settingsActions.wrapInner('<div id="rmSettingsActionButtonsRow"></div>');
$settingsActions.append('<span id="rmSettingsUnsavedHint" role="status" aria-live="polite">Есть несохранённые изменения</span>');
bindSettingsSubmitReadyState(currentSettings);
$('#rmFooterButtons').css('justify-content', 'space-between').prepend(buildSettingsFooterLeftHtml());
$('#rmSettingsResetFooter').on('click', function () {
fillSettingsFormValues(settingsDefaults);
updateSettingsSubmitReadyState(currentSettings);
$('#removerSubmit').trigger('focus');
});
}
// ─── Завершение обработки ────────────────────────────────────────────────
function finalizeSuccess(nominationInfo, usePageReload) {
if (isError) {
var $box = $('#rmLogBox').length ? $('#rmLogBox') : $('#removerModalContent');
$box.append('<p class="error">При выполнении скрипта произошли ошибки.</p>');
markSubmitError();
return;
}
renderModalFooter('reload');
if (nominationInfo && nominationInfo.pageTitle) {
appendNominationLink(nominationInfo.pageTitle, nominationInfo.sectionTitle);
}
if (!usePageReload && !nominationInfo) location.reload();
}
function finalizeFastRemoval(notifiedPages, summary) {
if (isError || !setAlert || !notifiedPages || !notifiedPages.length) {
finalizeSuccess(null, false);
return;
}
notifyAuthorsForPages(notifiedPages, {
summary: summary,
actionText: 'к быстрому удалению'
}, function () {
finalizeSuccess(null, false);
});
}
// ─── Общий runner ────────────────────────────────────────────────────────
/**
* Универсальный запуск полного пайплайна номинации.
* @param {Object} o
* templateStep — функция (next) → обработка шаблонов на статьях
* nominationStep — функция (done) → публикация номинации, done(err, {pageTitle, sectionTitle})
* notifyStep — функция (nominationInfo, next)
* skipNotify — boolean
* skipLink — boolean, не показывать ссылку на номинацию
*/
function runFlow(o) {
runNominationPipeline({
templateStep: o.templateStep,
nominationStep: o.nominationStep,
notifyStep: o.notifyStep || function (info, next) { next(); },
skipNotify: o.skipNotify,
onSuccess: function (ctx) {
if (isError) { markSubmitError(); return; }
renderModalFooter('reload');
if (!o.skipLink && ctx.nominationInfo && ctx.nominationInfo.pageTitle) {
appendNominationLink(ctx.nominationInfo.pageTitle, ctx.nominationInfo.sectionTitle);
maybeOpenNominationDiscussionInNewTab(ctx.nominationInfo);
}
},
onFailure: function () { markSubmitError(); }
});
}
// ═══════════════════════════════════════════════════════════════════════════
// ЯДРО: обработка статей (apply template + nomination page)
// ═══════════════════════════════════════════════════════════════════════════
/**
* Применяет шаблон к одной статье/категории.
* Понимает режим inArticle (вставка через <noinclude>),
* режим closeAction (снятие шаблона + запись на СО),
* режим cleanupAction (снятие КБУ/КУЛ).
*
* @param {string} pg — название страницы
* @param {Object} job — параметры задания (см. buildJob)
* @param {function} callback(err, meta)
*/
function applyTemplateToPage(pg, job, callback) {
var mode = job.mode;
// ── Снятие КБУ/КУЛ ──────────────────────────────────────────────────
if (mode === 'cleanup') {
var tm = job.transferMode || 'none';
if (tm === 'none') { callback({ code: 'error', info: 'Не выбран режим снятия шаблонов.' }); return; }
editPageContent(pg, { summary: job.summary, watchlist: 'nochange', readErrorCode: 'error', readError: 'Страница «' + pg + '» не существует.' },
function (article, done) {
var local = removeTransferTemplatesLocal(article, tm);
removeTransferTemplatesWithApiFallback(pg, local.text, tm, local, function (updated) {
if (updated.text === article) { done({ error: { code: 'error', info: 'Шаблоны для снятия не найдены.' } }); return; }
done({ text: updated.text });
});
}, function (err) { callback(err); });
return;
}
// ── Подведение итогов по КУ/КПМ (снятие + итог на СО) ───────────────
if (mode === 'denom') {
getTextWithTimestamp(pg, function (article, baseTimestamp, readErr) {
if (readErr) { callback(makeReadError(readErr, 'read_failed', 'Не удалось получить содержимое страницы «' + pg + '».')); return; }
if (article === null) { callback({ code: 'error', info: 'Страница «' + pg + '» не существует.' }); return; }
if (!job.sourceTemplate) { callback({ code: 'error', info: 'Не задан шаблон для снятия.' }); return; }
var tplPattern = buildTemplateNamePattern(job.sourceTemplate);
var tpl = findTemplateByPattern(article, tplPattern);
if (!tpl) { callback({ code: 'error', info: 'Невозможно снять шаблон «' + job.sourceTemplate + '».' }); return; }
var normalizedTplDate = convertToStandardDate(tpl.params[0]);
var tplExtraParams = tpl.params.slice(1);
var tplExtra = tplExtraParams.join('|').trim();
var renameTargets = collectRenameTargetsFromTemplateParams(tplExtraParams);
var isRedirectedDeletion = job.closeType === 'redirectedDeletion';
if (!RE_DATE_ISO.test(normalizedTplDate)) {
callback({ code: 'error', info: 'Не удалось распознать дату в шаблоне: «' + (tpl.params[0] || '') + '».' });
return;
}
if (isRedirectedDeletion && !job.redirectTarget) {
callback({ code: 'error', info: 'Не указана цель перенаправления.' });
return;
}
var date = getDate(normalizedTplDate);
var nomPlace = 'ВП:' + job.sourceTemplate.replace(/\|.*/, '') + '/' + date[1];
var retTalkSection = '';
var sectionNW, tplpar;
if (job.closeType === 'doneRnm') { sectionNW = job.oldTitle + ' → ' + pg; tplpar = job.oldTitle + '|' + pg; }
if (job.closeType === 'noRnm') {
if (!renameTargets.length) {
callback({ code: 'error', info: 'В шаблоне КПМ не найдено новое название.' });
return;
}
sectionNW = pg + ' → ' + renameTargets.join(', ');
tplpar = pg + '|' + renameTargets.join('|');
}
if (job.closeType === 'ret' || job.closeType === 'retConditional' || job.closeType === 'withdrawnDeletion' || isRedirectedDeletion) {
retTalkSection = tplExtra;
sectionNW = retTalkSection || pg;
tplpar = retTalkSection ? ('l1=' + retTalkSection) : '';
}
var editResultText = job.resultTemplate + (isRedirectedDeletion ? ' на [[' + job.redirectTarget + ']]' : '');
var editSummary = makeSummary('номинация [[' + (nomPlace ? nomPlace + '#' : '') + sectionNW + ']] — ' + editResultText);
var talkTitle = getTalkPage(pg);
getTextWithTimestamp(talkTitle, function (talkText, talkTimestamp, talkReadErr) {
if (talkReadErr) { callback(makeReadError(talkReadErr, 'talk_read_failed', 'Не удалось получить содержимое СО страницы «' + pg + '».')); return; }
var sourceTalkText = talkText || '';
var talkPageMissing = talkText === null;
var dateSectionTalkTemplate = getDateSectionTalkTemplateConfig(job.closeType);
var talkResult = dateSectionTalkTemplate
? upsertDateSectionTemplateOnTalkPage(sourceTalkText, dateSectionTalkTemplate.name, date[0], retTalkSection, dateSectionTalkTemplate)
: (job.closeType === 'retConditional')
? upsertConditionalRetTemplateOnTalkPage(sourceTalkText, date[0], retTalkSection, job.conditionalReason, job.conditionalDeadline)
: upsertSingleDateResultTemplateOnTalkPage(sourceTalkText, job.resultTemplate, date[0], tplpar);
function saveArticle() {
var cleaned = isRedirectedDeletion ? buildRedirectText(job.redirectTarget) : stripTemplatesByPattern(article, tplPattern).text;
var ep = { title: pg, text: cleaned, summary: editSummary, watchlist: 'nochange', assertuser: mwCfg.wgUserName };
if (baseTimestamp) ep.basetimestamp = baseTimestamp;
apiReq(ep, 'edit', function (t) {
var editErr = t && t.error ? t.error : null;
callback(editErr, editErr ? null : {
discussionPage: nomPlace,
discussionSection: sectionNW,
summary: editSummary,
successMessage: isRedirectedDeletion
? 'Содержимое страницы ' + buildQuotedStatusPageLink(pg) + ' очищено, установлено перенаправление на ' + buildQuotedStatusPageLink(job.redirectTarget) + '.'
: ''
});
});
}
if (talkResult.text === sourceTalkText) { saveArticle(); return; }
var talkEp = { title: talkTitle, text: talkResult.text, summary: editSummary, watchlist: 'nochange', assertuser: mwCfg.wgUserName };
if (talkPageMissing) talkEp.createonly = true;
else if (talkTimestamp) talkEp.basetimestamp = talkTimestamp;
apiReq(talkEp, 'edit', function (talkResp) {
if (talkResp && talkResp.error) { callback({ code: 'talk_failed', info: 'Не удалось записать итог на СО: ' + talkResp.error.info }); return; }
saveArticle();
});
});
});
return;
}
// ── Обычная номинация: вставка шаблона в статью ─────────────────────
// mode === 'nominate'
var isKu = job.opId === 'tRm' || job.opId === 'mRm';
var conflictRule = getNominationConflictRule(job);
var conflictLabel = conflictRule ? conflictRule.label : 'номинации';
editPageContent(pg, { summary: job.summary, readErrorCode: 'error', readError: 'Страница «' + pg + '» не существует.' },
function (article, done) {
var hasExistingNomination = conflictRule && conflictRule.detect(article);
var conflictDecision = getConflictDecisionForPage(job, pg);
function buildResult(finalText) {
var generatedTpl = buildGeneratedNominationTemplateText(job, pg);
return { text: generatedTpl ? wrapInNoinclude(finalText, generatedTpl) : finalText };
}
function buildResultAfterMoveToCommonsCleanup(sourceText) {
var localMove = removeMoveToCommonsTemplatesLocal(sourceText);
removeMoveToCommonsTemplatesWithApiFallback(pg, localMove.text, localMove, function (updated) {
done(buildResult(updated.text));
});
}
function finishConflictResolution(sourceText) {
var resolvedText;
var pageLink = buildQuotedStatusPageLink(pg);
if (conflictDecision.templateAction === 'keep') {
if (sourceText !== article) {
return {
text: sourceText,
meta: { successMessage: 'Шаблон ' + conflictLabel + ' на странице ' + pageLink + ' оставлен без изменений; шаблоны переноса сняты.' }
};
}
return { skip: true, meta: { successMessage: 'Шаблон ' + conflictLabel + ' на странице ' + pageLink + ' оставлен без изменений.' } };
}
resolvedText = applyConflictTemplateResolution(sourceText, job, pg, conflictDecision);
return {
text: resolvedText,
meta: {
successMessage: conflictDecision.templateAction === 'overwrite'
? 'Шаблон ' + conflictLabel + ' на странице ' + pageLink + ' перезаписан новой датой.'
: 'Новый шаблон ' + conflictLabel + ' добавлен сверху на странице ' + pageLink + '.'
}
};
}
if (hasExistingNomination && (!conflictDecision || conflictDecision.pageAction !== 'keep')) {
return { error: { code: 'error', info: 'На странице уже стоит шаблон ' + conflictLabel + '.' } };
}
if (hasExistingNomination && conflictDecision && conflictDecision.pageAction === 'keep') {
if (job.transferMode && job.transferMode !== 'none') {
var localConflict = removeTransferTemplatesLocal(article, job.transferMode);
removeTransferTemplatesWithApiFallback(pg, localConflict.text, job.transferMode, localConflict, function (updated) {
done(finishConflictResolution(updated.text));
});
return;
}
return finishConflictResolution(article);
}
if (isKu && job.transferMode && job.transferMode !== 'none') {
var local = removeTransferTemplatesLocal(article, job.transferMode);
removeTransferTemplatesWithApiFallback(pg, local.text, job.transferMode, local, function (updated) { done(buildResult(updated.text)); });
return;
}
if (job.removeMoveToCommons) {
buildResultAfterMoveToCommonsCleanup(article);
return;
}
return buildResult(article);
},
function (err) { callback(err); }
);
}
/**
* Обрабатывает список страниц последовательно.
* @param {string[]} pages
* @param {Object} job
* @param {function} onDone(notifiedPages, err, pageMeta)
*/
function processPageList(pages, job, onDone) {
var notifiedPages = [];
var pageMeta = {};
eachSequential(pages.slice().reverse(), function (pg, nextPage) {
var pageLink = buildQuotedStatusPageLink(pg);
var statusId = logStatus('Обрабатывается страница ' + pageLink + '...', null, { pending: true, trackError: false });
applyTemplateToPage(pg, job, function (err, meta) {
var normPg = normTitle(pg);
var isClose = job.mode === 'cleanup' || job.mode === 'denom';
if (!isClose) {
if (!err && meta && meta.successMessage) logStatus(meta.successMessage, null, { statusId: statusId, trackError: false });
else logPageEdit(pg, err, { statusId: statusId });
} else {
if (err) { logStatus('Завершение по странице ' + pageLink, err, { statusId: statusId }); }
else {
logStatus((meta && meta.successMessage) || ('Шаблон снят со страницы ' + pageLink + '.'), null, { statusId: statusId, trackError: false });
if (job.mode === 'denom') logStatus('Шаблон установлен на СО страницы ' + pageLink + '.', null, { trackError: false });
}
}
if (!err) {
notifiedPages.push(pg);
if (meta) pageMeta[normPg] = meta;
}
nextPage(err || null);
});
}, function (err) { onDone(notifiedPages, err, pageMeta); });
}
// ═══════════════════════════════════════════════════════════════════════════
// ПОСТРОЕНИЕ JOB из формы
// ═══════════════════════════════════════════════════════════════════════════
/**
* Строит объект job из данных формы для операции номинации (tRm, rnm, imp, merge, split, recov).
* @param {Object} op — запись из OPERATIONS
* @param {string} pg — целевая страница (уже разрешённая)
* @param {boolean} isMulti — режим мультиноминации
* @returns {Object|false} — job или false при ошибке ввода
*/
function buildNominationJob(op, pg, isMulti) {
var nom = op.nomination;
var date = getDate();
var msg = normalizeQuickPhraseValue($('#rmMsg').val());
var rawMsg = msg;
var opId = isMulti ? (nom.multiOpId || op.id) : op.id;
var tplpar = '';
var section, sectionNW, extraPages, multiArticles = [];
var multiHeaderText = '';
var multiArticleComments = {};
var multiFormat = 'sections';
var multiRenamePairs = [];
var multiRenameTargets = {};
var multiRenameTemplateTargets = {};
var isRenameWithRowTargets = isMulti && nom.extraInput && nom.extraInput.type === 'rename' && $('.rmMultiRenameTargetInput').length;
// Вычислить section и tplpar в зависимости от типа дополнительного ввода
if (nom.extraInput) {
var ei = nom.extraInput;
if (ei.type === 'rename') {
if (isRenameWithRowTargets) {
multiRenamePairs = collectMultiRenamePairs();
if (!validateMultiRenamePairs(multiRenamePairs, 'статью', 'новое название')) return false;
multiRenameTargets = buildMultiRenameTargetMap(multiRenamePairs, 'targetNames');
multiRenameTemplateTargets = buildMultiRenameTargetMap(multiRenamePairs, 'templateTargetNames');
tplpar = buildRenameTemplateParam(multiRenamePairs[0].templateTargetNames);
section = formatRenameItemLabel(pg, multiRenameTargets[normTitle(pg)] || multiRenamePairs[0].targetNames);
} else {
var rn = collectInputValues('.rmRenameInput');
if (!rn.length) { alert('Укажите новое название.'); return false; }
tplpar = buildRenameTemplateParam(rn);
section = formatRenameItemLabel(pg, rn);
}
} else if (ei.type === 'merge') {
var mn = collectInputValues('.rmMergeInput');
if (!mn.length) { alert('Укажите статью для объединения.'); return false; }
tplpar = pg + '|' + mn.join('|');
extraPages = mn;
section = formatPagesWithAnd([pg].concat(mn));
} else if (ei.type === 'split') {
var sn = collectInputValues('.rmSplitInput');
if (!sn.length) { alert('Укажите статьи для разделения.'); return false; }
tplpar = formatPagesWithAnd(sn);
section = '[[:' + pg + ']] → ' + tplpar;
}
}
if (isMulti) {
var ttl = $('#rmHeader').val() || '';
var articles = isRenameWithRowTargets
? multiRenamePairs.map(function (pair) { return pair.pageName; })
: collectInputValues('.rmMultiPageInput');
multiFormat = $('.rmArticleMultiFormatBtn.is-active').data('rmMultiFormat') || 'sections';
multiArticleComments = collectMultiNominationComments();
multiHeaderText = ttl;
multiArticles = articles.slice();
if (!articles.length) { alert('Укажите страницу.'); return false; }
if (!validateMultiNominationText(articles, rawMsg, multiArticleComments, 'страницы')) return false;
section = ttl || (isRenameWithRowTargets ? formatRenameItemsWithAnd(articles, multiRenameTargets) : '');
msg = multiFormat === 'list'
? buildMultiNominationListText(articles, rawMsg, multiArticleComments, isRenameWithRowTargets ? getMultiRenameDiscussionOptions(multiRenameTargets) : null)
: buildMultiNominationText(articles, rawMsg, multiArticleComments, isRenameWithRowTargets
? getMultiRenameDiscussionOptions(multiRenameTargets, { leadingBlankLine: false })
: { leadingBlankLine: false });
}
if (!section) section = '[[:' + pg + ']]';
sectionNW = section.replace(/\[\[:/g, '').replace(/]]/g, '');
var nomPageDate = date[1];
var nomPage = nom.nomPage(nomPageDate);
var summaryAction = getNominationSummaryAction(nom);
var summary = makeNominationTargetSummary(nomPage, sectionNW, summaryAction);
return {
mode: 'nominate',
opId: opId,
op: op,
date: date,
tplpar: tplpar,
articleTpl: nom.articleTpl || function () { return ''; },
inArticle: nom.inArticle !== false,
transferMode: (nom.supportsTransfer ? getTransferModeFromButtons() : 'none'),
summary: summary,
nominationSummary: makeNominationPageSummary(sectionNW),
summaryAction: summaryAction,
msg: msg,
nomPage: nomPage,
navTemplate: nom.navTemplate,
section: section,
sectionNW: sectionNW,
comment: nom.comment || '',
extraPages: extraPages || [],
isMulti: !!isMulti,
multiHeaderText: multiHeaderText,
multiNominationBody: rawMsg,
multiArticleComments: multiArticleComments,
multiNominationFormat: multiFormat || 'sections',
multiRenamePairs: multiRenamePairs,
multiRenameTargets: multiRenameTargets,
multiRenameTemplateTargets: multiRenameTemplateTargets,
multiArticles: multiArticles,
pages: isMulti ? multiArticles.slice().reverse() : ([pg].concat(extraPages || []))
};
}
function getTransferModeFromButtons() {
var kbu = $('#rmTransferBtnKbu').hasClass('is-active');
var kul = $('#rmTransferBtnKul').hasClass('is-active');
if (kbu && kul) return 'both';
if (kbu) return 'kbu';
if (kul) return 'kul';
return 'none';
}
function normalizeFastRemoveTemplateKey(templateName) {
return String(templateName || '').replace(/\s+/g, ' ').trim().toLowerCase();
}
function getBuiltinFastRemoveParamConfig(templateName) {
var key = normalizeFastRemoveTemplateKey(templateName);
var bareKey = key.replace(/^(?:подст|subst)\s*:\s*/, '');
if (/^(?:nld|no license|nsd|no source|nad|no author|dd|disputed|npd|no permission|ofud|orphaned-fairuse|nothost|db-nothost)$/.test(bareKey)) return { type: 'date' };
if (/^(?:dfud|disputed-fairuse)$/.test(bareKey)) return { type: 'dfud' };
if (key === 'db-badfairuse') return { type: 'badfairuse' };
if (key === 'rct' || key === 'замена с викисклада') return { type: 'commonsReplacement' };
if (key === 'nct' || key === 'перенесено на викисклад') return { type: 'nct' };
if (key === 'db-duplicate') return { type: 'duplicate' };
return null;
}
function buildKbuReasonButtonsHtml(items, buttonClass, dataAttr, titlePrefix) {
return items.map(function (item) {
var label = titlePrefix + ' ' + item[0] + ': ' + item[1];
return '<button type="button" class="rmToggleBtn rmKbuReasonPill ' + buttonClass + '" ' + dataAttr + '="' +
escapeHtml(item[0]) + '" aria-pressed="false" title="' + escapeHtml(label) +
'"><span class="rmKbuReasonNum">' + escapeHtml(item[0]) + '</span>' +
'<span class="rmKbuReasonText">' + escapeHtml(item[1]) + '</span></button>';
}).join('');
}
function buildDfudReasonButtonsHtml() {
return buildKbuReasonButtonsHtml(DFUD_REASON_OPTIONS, 'rmKbuDfudReasonBtn', 'data-rm-dfud-reason', 'п.');
}
function buildBadfairuseReasonButtonsHtml() {
return buildKbuReasonButtonsHtml(BADFAIRUSE_OPTIONS, 'rmKbuBadfairuseReasonBtn', 'data-rm-badfairuse-reason', 'пример');
}
function buildKbuReasonListHtml(id, hint, buttonsHtml) {
return '<div id="' + id + '" class="' + RESIZE_CLASS + '" style="display:none;margin-bottom:10px;">' +
'<div style="margin-bottom:6px;font-size:14px;line-height:1.4;color:' + tk.cSubM + ';">' + hint + '</div>' +
'<div style="display:flex;flex-direction:column;gap:4px;">' + buttonsHtml + '</div>' +
'</div>';
}
function collectSelectedDfudReasons() {
return $('#rmKbuDfudReasonsWrap .rmKbuDfudReasonBtn.is-active').map(function () {
return String($(this).attr('data-rm-dfud-reason') || '').trim();
}).get().filter(Boolean);
}
function getSelectedBadfairuseReason() {
return String($('#rmKbuBadfairuseReasonsWrap .rmKbuBadfairuseReasonBtn.is-active').first().attr('data-rm-badfairuse-reason') || '').trim();
}
function getCurrentCommonsReplacementMode() {
var mode = $('#rmKbuCommonsReplacementModeWrap .rmKbuCommonsReplacementModeBtn.is-active').data('rmCommonsReplacementMode');
return mode || 'file';
}
function setCurrentCommonsReplacementMode(mode) {
var value = mode || 'file';
$('#rmKbuCommonsReplacementModeWrap .rmKbuCommonsReplacementModeBtn').each(function () {
var active = $(this).data('rmCommonsReplacementMode') === value;
$(this).toggleClass('is-active', active).attr('aria-pressed', active ? 'true' : 'false');
});
if (value === 'category') {
$('#fiRm').attr('placeholder', 'Категория на Викискладе без Category:');
$('#fiRmExtra').attr('placeholder', 'Статья в руВики о теме категории (необязательно)').show();
} else {
$('#fiRmExtra').hide();
$('#fiRm').attr('placeholder', value === 'gallery' ? 'Галерея на Викискладе' : 'Имя файла на Викискладе');
}
syncModalLayout();
}
function getFastRemoveLegacyParamConfig(templateName) {
var params = cfg.requiredParamTemplates || {};
var key;
if (Object.prototype.hasOwnProperty.call(params, templateName)) return params[templateName];
for (key in params) {
if (Object.prototype.hasOwnProperty.call(params, key) &&
normalizeFastRemoveTemplateKey(key) === normalizeFastRemoveTemplateKey(templateName)) {
return params[key];
}
}
return null;
}
function getFastRemoveParamConfig(reason) {
var template = reason && reason[0];
return getBuiltinFastRemoveParamConfig(template) || getFastRemoveLegacyParamConfig(template);
}
function appendTemplateParam(tpl, name, value) {
var val = normalizeQuickPhraseValue(value);
if (!val) return tpl;
return tpl + '|' + (name ? name + '=' : '') + val;
}
function makeFastRemovalSummary(reason, data) {
var label = String(reason && reason[1] || '').trim();
var detail = String(data && data.summaryDetail || '').trim();
var summaryReason = (label === 'Особый случай' && detail) ? detail : (label || detail);
return makeToolSuffixSummary('номинация к быстрому удалению' + (summaryReason ? ': ' + summaryReason : ''));
}
function buildFastRemoveTemplateData(reason) {
var template = reason && reason[0];
var paramCfg = getFastRemoveParamConfig(reason);
var addInfo = normalizeQuickPhraseValue($('#fiRm').val());
var comment = normalizeQuickPhraseValue($('#fiRmComment').val());
var dateText = getDate()[0];
var mode;
var extra;
var tpl;
var values;
if (!template) return { error: 'Не выбрана причина быстрого удаления.' };
if (paramCfg && paramCfg.type === 'date') {
return { templateText: appendTemplateParam(template, '1', dateText) };
}
if (paramCfg && paramCfg.type === 'dfud') {
tpl = appendTemplateParam(template, 'date', dateText);
values = collectSelectedDfudReasons();
values.forEach(function (value, index) { tpl = appendTemplateParam(tpl, String(index + 1), value); });
return { templateText: tpl };
}
if (paramCfg && paramCfg.type === 'badfairuse') {
addInfo = getSelectedBadfairuseReason();
if (!addInfo) return { error: 'Выберите пример из ВП:ПНИ для Ф6.' };
return { templateText: appendTemplateParam(template, '1', addInfo) };
}
if (paramCfg && paramCfg.type === 'commonsReplacement') {
mode = getCurrentCommonsReplacementMode();
if (mode === 'category') {
if (!addInfo) return { error: 'Укажите категорию на Викискладе.' };
tpl = appendTemplateParam('Замена с Викисклада/Категория', '1', addInfo);
extra = normalizeQuickPhraseValue($('#fiRmExtra').val());
return { templateText: appendTemplateParam(tpl, '2', extra) };
}
if (mode === 'gallery') {
if (!addInfo) return { error: 'Укажите галерею на Викискладе.' };
return { templateText: appendTemplateParam('Замена с Викисклада/Галерея', '1', addInfo) };
}
return { templateText: appendTemplateParam(template, '1', addInfo) };
}
if (paramCfg && paramCfg.type === 'nct') {
return {
templateText: appendTemplateParam(template, '1', addInfo),
removeMoveToCommons: $('#rmKbuRemoveMoveToCommons').is(':checked')
};
}
if (paramCfg && paramCfg.type === 'duplicate') {
if (!addInfo) return { error: 'Укажите имя файла-оригинала для Ф1.' };
tpl = appendTemplateParam(template, '1', addInfo);
return { templateText: appendTemplateParam(tpl, 'comment', comment) };
}
tpl = template;
if (typeof paramCfg === 'string') {
var noComment = paramCfg.charAt(0) === '!';
tpl = appendTemplateParam(tpl, '1', addInfo);
if (!noComment) tpl = appendTemplateParam(tpl, addInfo ? '2' : '1', comment);
return { templateText: tpl, summaryDetail: noComment ? addInfo : '' };
}
return { templateText: appendTemplateParam(tpl, '1', comment) };
}
function resetKbuReasonControls() {
$('#fiRm').hide().val('').attr({ type: 'text', placeholder: '' });
$('#fiRmExtra').hide().val('');
$('#rmKbuCommonsReplacementModeWrap').hide();
$('#rmKbuDfudReasonsWrap').hide().find('.rmKbuDfudReasonBtn').removeClass('is-active').attr('aria-pressed', 'false');
$('#rmKbuBadfairuseReasonsWrap').hide().find('.rmKbuBadfairuseReasonBtn').removeClass('is-active').attr('aria-pressed', 'false');
$('#rmKbuDateHint').hide().text('');
$('#rmKbuRemoveMoveToCommons').prop('checked', false);
$('#rmKbuRemoveMoveToCommonsWrap').hide();
$('#fiRmComment').hide().val('');
$('.rmQuickPhrasesPanel[data-rm-target="fiRmComment"]').hide();
}
function toggleKbuComment(show) {
$('#fiRmComment').toggle(show);
$('.rmQuickPhrasesPanel[data-rm-target="fiRmComment"]').toggle(show);
}
function updateKbuReasonControls(reasons) {
var reason = reasons[$('#rmSel').val()] || reasons[0];
var paramCfg = getFastRemoveParamConfig(reason);
$('#rmKbuCriteriaLinkWrap').html(buildFastRemoveCriteriaLinkHtml(reason));
resetKbuReasonControls();
if (paramCfg && (paramCfg.type === 'date' || paramCfg.type === 'dfud')) {
$('#rmKbuDateHint').text(FILE_DATE_DEADLINE_HINT).show();
if (paramCfg.type === 'dfud') $('#rmKbuDfudReasonsWrap').show();
return;
}
if (paramCfg && paramCfg.type === 'badfairuse') {
$('#rmKbuBadfairuseReasonsWrap').show();
return;
}
if (paramCfg && paramCfg.type === 'commonsReplacement') {
$('#rmKbuCommonsReplacementModeWrap').show();
$('#fiRm').show();
setCurrentCommonsReplacementMode(getCurrentCommonsReplacementMode());
return;
}
if (paramCfg && paramCfg.type === 'nct') {
$('#fiRm').attr('placeholder', 'Укажите имя файла на Викискладе').show();
$('#rmKbuRemoveMoveToCommonsWrap').css('display', 'inline-flex');
return;
}
if (paramCfg && paramCfg.type === 'duplicate') {
$('#fiRm').attr('placeholder', 'Укажите имя файла-оригинала').show();
toggleKbuComment(true);
return;
}
if (typeof paramCfg === 'string') {
var noComment = paramCfg.charAt(0) === '!';
$('#fiRm').attr('placeholder', 'Укажите ' + (noComment ? paramCfg.substring(1) : paramCfg)).show();
toggleKbuComment(!noComment);
return;
}
toggleKbuComment(true);
}
function getKbuReasonGroupKey(reason) {
var label = String(reason && reason[1] || '');
var m = label.match(/^\s*([А-ЯЁA-Z]{1,3})\d/);
return m ? m[1] : label;
}
function buildKbuReasonOptionsHtml(reasons) {
var prevGroup = '';
var parts = [];
(reasons || []).forEach(function (reason, index) {
var group = getKbuReasonGroupKey(reason);
if (index && group !== prevGroup) parts.push('<hr>');
parts.push('<option value="' + index + '">' + escapeHtml(reason[1]) + '</option>');
prevGroup = group;
});
return parts.join('');
}
function buildKbuFormHtml(reasons) {
return joinHtml([
'<select id="rmSel" style="', stInputFull, '">',
buildKbuReasonOptionsHtml(reasons),
'</select>',
'<div id="rmKbuDateHint" class="', RESIZE_CLASS, '" style="display:none;margin:-2px 0 10px;font-size:12px;line-height:1.35;color:', tk.cSubM, ';"></div>',
'<div id="rmKbuCommonsReplacementModeWrap" class="', RESIZE_CLASS, '" style="display:none;margin-bottom:10px;"><div class="rmSegmentedBar">',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmKbuCommonsReplacementModeBtn is-active" data-rm-commons-replacement-mode="file" aria-pressed="true">Файл</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmKbuCommonsReplacementModeBtn" data-rm-commons-replacement-mode="category" aria-pressed="false">Категория</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmKbuCommonsReplacementModeBtn" data-rm-commons-replacement-mode="gallery" aria-pressed="false">Галерея</button>',
'</div></div>',
buildKbuReasonListHtml(
'rmKbuDfudReasonsWrap',
'Выберите пункты КДИ, которым файл не соответствует. Можно выбрать несколько пунктов.',
buildDfudReasonButtonsHtml()
),
buildKbuReasonListHtml(
'rmKbuBadfairuseReasonsWrap',
'Выберите пример из ВП:ПНИ, под который подпадает файл. Можно выбрать только один пункт.',
buildBadfairuseReasonButtonsHtml()
),
'<input id="fiRm" type="text" style="', stInputFull, 'display:none;">',
'<input id="fiRmExtra" type="text" style="', stInputFull, 'display:none;">',
'<label id="rmKbuRemoveMoveToCommonsWrap" class="', RESIZE_CLASS, '" style="', stFooterCheckLabel.replace('display:inline-flex;', 'display:none;'), 'margin:-2px 0 10px;">',
'<input id="rmKbuRemoveMoveToCommons" type="checkbox" style="margin:2px 0 0;flex-shrink:0;">',
'Снять шаблон «Перенести на Викисклад»',
'</label>',
'<input id="fiRmComment" type="text" placeholder="Комментарий (необязательно)" style="', stInputFull, '">',
buildQuickPhrasesPanelHtml('fiRmComment')
]);
}
function buildNominationMultiHeaderHtml(pg, options) {
var opts = options || {};
var multiHeaderRowStyle = stRow.replace('margin-bottom:6px;', 'margin-bottom:0;');
return joinHtml([
'<div id="rmMultiHeader" class="', RESIZE_CLASS, '" style="display:none;margin-bottom:6px;">',
'<div class="rmMultiPageRow rmNominationHeaderRow" style="', multiHeaderRowStyle, '"><input id="rmHeader" type="text" placeholder="Заголовок номинации" style="', stInputBox, '">',
buildAddMultiPageButtonHtml(opts), '</div>',
'</div>',
'<div id="rmMultiPagesContainer" style="display:flex;flex-direction:column;gap:', multiNominationGap, ';margin-bottom:', multiNominationGap, ';">',
buildMultiPageRowHtml(0, $.extend({}, opts, { rowId: 'rmFirstMultiPage', pageValue: pg, showAdd: true })),
'</div>'
]);
}
function buildTransferBoxHtml() {
return joinHtml([
'<div id="rmTransferBox" class="', RESIZE_CLASS, ' rmTransferPanel" style="width:100%;box-sizing:border-box;"><div class="rmTransferGrid">',
'<div id="rmTransferModeSingle" class="rmSegmentedBar"><button type="button" id="rmTransferBtnNone" class="rmSegmentedBtn rmToggleBtn is-active" aria-pressed="true">Обычная номинация</button></div>',
'<div id="rmTransferModeGroup" class="rmSegmentedBar">',
'<button type="button" id="rmTransferBtnKbu" class="rmSegmentedBtn rmToggleBtn" aria-pressed="false" title="Шаблоны db-*, уд-*, КОУ, Hangon.">Снять КБУ</button>',
'<button type="button" id="rmTransferBtnKul" class="rmSegmentedBtn rmToggleBtn" aria-pressed="false" title="Шаблон «К улучшению».">Снять КУЛ</button>',
'</div>',
'<div class="rmTransferHintRow"><div id="rmTransferHint" style="display:none;font-size:12px;line-height:1.35;color:', tk.cSubM, ';"></div></div>',
'</div></div>'
]);
}
function buildMultiNominationFormatSwitchHtml(wrapId, buttonClass) {
return joinHtml([
'<div id="', wrapId, '" class="', RESIZE_CLASS, '" style="display:none;margin-top:8px;margin-bottom:10px;">',
'<div class="rmSegmentedBar">',
'<button type="button" class="rmSegmentedBtn rmToggleBtn ', buttonClass, ' is-active" data-rm-multi-format="sections" aria-pressed="true">Оформить подразделами</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn ', buttonClass, '" data-rm-multi-format="list" aria-pressed="false">Оформить списком</button>',
'</div>',
'</div>'
]);
}
function getMultiNominationUiOptions(kind, options) {
var opts = options || {};
var isArticle = kind === 'article';
var isRename = !!opts.renameMulti;
var actionText = typeof opts.actionText === 'string'
? opts.actionText
: (opts.deletionMulti ? 'к удалению' : (isRename ? 'к переименованию' : ''));
var itemAcc = isArticle ? 'статью' : 'категорию';
var itemDat = isArticle ? 'статье' : 'категории';
var itemGen = isArticle ? 'статьи' : 'категории';
var result = {
inputPlaceholder: isArticle ? 'Статья' : 'Категория',
addTitle: 'Мультиноминация: добавить ' + itemAcc + ' в номинацию' + (actionText ? ' ' + actionText : ''),
removeTitle: 'Убрать ' + itemAcc + ' из номинации' + (actionText ? ' ' + actionText : ''),
commentTitle: 'Добавить комментарий к этой ' + itemDat,
commentExpandedTitle: 'Скрыть комментарий к этой ' + itemDat,
commentPlaceholder: 'Комментарий только для этой ' + itemGen + ' (необязательно)'
};
if (isRename) {
$.extend(result, {
targetInput: true,
targetInputClass: 'rmMultiRenameTargetInput',
targetPlaceholder: isArticle ? 'Новое название' : 'Новое название без префикса Категория:',
targetVariants: true
});
}
if (opts.setup) {
$.extend(result, {
defaultPage: opts.defaultPage || '',
multiOnlySelector: isArticle ? '#rmArticleMultiFormatWrap' : '#rmCategoryMultiFormatWrap'
});
if (isRename) $.extend(result, {
singleOnlySelector: '#rmSingleRenameBlock',
hideContainerWhenSingle: true,
singleCurrentPageSelector: '#rmSingleRenameCurrent',
singleRenameTargetSelector: isArticle ? '#rmRenameFirst' : '#firstRenameInput',
singleRenameVariantSelector: isArticle ? '#rmRenameContainer .rmRenameInput' : '#renameVariantsContainer .variantInput',
singleRenameVariantContainerId: isArticle ? 'rmRenameContainer' : 'renameVariantsContainer',
singleRenameVariantPlaceholder: isArticle ? 'Дополнительный вариант' : 'Дополнительный вариант названия',
singleRenameInputClass: isArticle ? 'rmRenameInput' : undefined
});
}
return result;
}
function buildNominationFormHtml(nom, pg, multiMode) {
var isRenameMulti = multiMode && nom.extraInput && nom.extraInput.type === 'rename';
var multiOptions = getMultiNominationUiOptions('article', {
renameMulti: isRenameMulti,
deletionMulti: multiMode && nom.template === 'к удалению'
});
return joinHtml([
isRenameMulti ? buildSingleRenameBlockHtml(nom.extraInput, 'Мультиноминация: добавить статью в номинацию', pg, 'Текущее название') : '',
multiMode ? buildNominationMultiHeaderHtml(pg, multiOptions) : '',
nom.extraInput && !isRenameMulti ? buildMultiInputHtml(nom.extraInput) : '',
'<textarea id="rmMsg" placeholder="Текст номинации без подписи"></textarea>',
buildQuickPhrasesPanelHtml('rmMsg'),
nom.supportsTransfer ? buildTransferBoxHtml() : '',
multiMode ? buildMultiNominationFormatSwitchHtml('rmArticleMultiFormatWrap', 'rmArticleMultiFormatBtn') : ''
]);
}
function buildCategoryNominationFormHtml(variantConfig, multiMode, pageName, options) {
var opts = options || {};
var multiOptions = getMultiNominationUiOptions('category', opts);
return joinHtml([
opts.renameMulti && variantConfig ? buildSingleRenameBlockHtml(variantConfig, 'Мультиноминация: добавить категорию в номинацию', pageName, 'Текущая категория') : '',
multiMode ? buildNominationMultiHeaderHtml(pageName, multiOptions) : '',
variantConfig && !opts.renameMulti && !opts.skipVariantInput ? buildMultiInputHtml(variantConfig) : '',
'<textarea id="nominationReason" placeholder="Текст номинации без подписи"></textarea>',
buildQuickPhrasesPanelHtml('nominationReason'),
multiMode ? buildMultiNominationFormatSwitchHtml('rmCategoryMultiFormatWrap', 'rmCategoryMultiFormatBtn') : ''
]);
}
function ensureMultiPageCommentTextareaResizer(textareaId, wrapId) {
var $textarea = $('#' + textareaId);
if (!$textarea.length || $textarea.data('rmNestedResizerReady')) return;
setupNestedResizableTextarea(textareaId, wrapId, parseInt(sz.taMinW, 10) || 180, 90);
$textarea.data('rmNestedResizerReady', true);
}
function setMultiPageCommentExpanded($btn, expanded) {
var wrapId = $btn.data('rmCommentWrap');
var textareaId = $btn.data('rmCommentTextarea');
var $wrap = $('#' + wrapId);
var title = expanded
? ($btn.data('rmCommentExpandedTitle') || 'Скрыть комментарий к этой странице')
: ($btn.data('rmCommentTitle') || 'Добавить комментарий к этой странице');
if (!$btn.length || !$wrap.length) return;
$btn.attr('aria-expanded', expanded ? 'true' : 'false')
.attr('aria-label', title)
.attr('title', title)
.toggleClass('is-active', expanded)
.text('✎');
$wrap.toggle(expanded);
if (expanded) ensureMultiPageCommentTextareaResizer(textareaId, wrapId);
}
function getMultiPageCommentTargets($block) {
var $textarea = $block.find('.rmMultiPageCommentInput').first();
var $wrap = $textarea.parent();
return {
wrapId: $wrap.attr('id') || '',
textareaId: $textarea.attr('id') || ''
};
}
function collectMultiRenameTargetValues($block) {
return $block.find('.rmMultiRenameTargetInput,.rmMultiRenameVariantInput').map(function () {
return ($(this).val() || '').trim();
}).get().filter(Boolean);
}
function setMultiPageRowControls($block, showAdd, showComment, options) {
var opts = options || {};
var $row = $block.find('.rmMultiPageRow').first();
var ids = getMultiPageCommentTargets($block);
var $commentBtn = $row.find('.rmMultiPageCommentToggle');
if (!$row.length) return;
if (showAdd) {
if ($commentBtn.length) setMultiPageCommentExpanded($commentBtn, false);
if (ids.wrapId) $('#' + ids.wrapId).hide();
$block.find('.rmMultiRenameVariantsContainer').hide();
$row.find('.rmMultiPageCommentToggle,.rmAddMultiRenameVariant,.rmRemoveInput').remove();
if (!$row.find('.rmAddMultiPage').length) $row.append(buildAddMultiPageButtonHtml(opts));
return;
}
$block.find('.rmMultiRenameVariantsContainer').toggle(!!opts.targetVariants);
$row.find('.rmAddMultiPage').remove();
if (!$row.find('.rmMultiPageCommentToggle').length) {
$row.append(buildMultiPageButtonsHtml(ids.wrapId, ids.textareaId, $.extend({}, opts, { showComment: showComment })));
return;
}
$row.find('.rmMultiPageCommentToggle').toggle(showComment);
}
function setupMultiPageNominationUi(options) {
var opts = options || {};
var containerSelector = opts.containerSelector || '#rmMultiPagesContainer';
var pageCounter = parseInt(opts.nextIndex, 10) || 1;
var wasMultiModeExpanded = false;
function restoreEmptySinglePageInput() {
var $pageInput = $(containerSelector + ' .rmMultiPageInput').first();
if (!$pageInput.length || String($pageInput.val() || '').trim()) return;
$pageInput.val(opts.defaultPage || '');
}
function copySingleCurrentPageToFirstRow() {
var $source = opts.singleCurrentPageSelector ? $(opts.singleCurrentPageSelector).first() : $();
var $target = $(containerSelector + ' .rmMultiPageBlock').first().find('.rmMultiPageInput').first();
var value = String(($source.val && $source.val()) || '').trim();
if (!$source.length || !$target.length || !value) return;
$target.val(value);
}
function copyFirstRowPageToSingleCurrent() {
var $target = opts.singleCurrentPageSelector ? $(opts.singleCurrentPageSelector).first() : $();
var $source = $(containerSelector + ' .rmMultiPageBlock').first().find('.rmMultiPageInput').first();
var value = String(($source.val && $source.val()) || '').trim();
if (!$source.length || !$target.length) return;
$target.val(value || opts.defaultPage || '');
}
function getSingleRenameTargets() {
var targets = [];
var $source = opts.singleRenameTargetSelector ? $(opts.singleRenameTargetSelector).first() : $();
if ($source.length && String($source.val() || '').trim()) targets.push(String($source.val() || '').trim());
if (opts.singleRenameVariantSelector) {
$(opts.singleRenameVariantSelector).each(function () {
var value = String($(this).val() || '').trim();
if (value) targets.push(value);
});
}
return targets.slice(0, 3);
}
function setMultiBlockRenameTargets($block, targets) {
var list = asNonEmptyArray(targets).slice(0, 3);
var $target = $block.find('.rmMultiRenameTargetInput').first();
var $container = $block.find('.rmMultiRenameVariantsContainer').first();
if (!$target.length) return;
$target.val(list[0] || '');
if (!$container.length) return;
$container.find('.rmMultiRenameVariantRow').remove();
list.slice(1).forEach(function (value) {
$container.append(buildMultiRenameVariantRowHtml(value, opts));
});
}
function copySingleRenameTargetsToFirstRow() {
var $block = $(containerSelector + ' .rmMultiPageBlock').first();
if (!$block.length) return;
setMultiBlockRenameTargets($block, getSingleRenameTargets());
}
function copyFirstRowRenameTargetsToSingle() {
var $target = opts.singleRenameTargetSelector ? $(opts.singleRenameTargetSelector).first() : $();
var $sourceBlock = $(containerSelector + ' .rmMultiPageBlock').first();
var targets = collectMultiRenameTargetValues($sourceBlock).slice(0, 3);
var $variantContainer = opts.singleRenameVariantContainerId ? $('#' + opts.singleRenameVariantContainerId) : $();
if (!$sourceBlock.length || !$target.length) return;
$target.val(targets[0] || '');
if (!$variantContainer.length) return;
$variantContainer.empty();
targets.slice(1).forEach(function (value) {
addInputRow({
containerId: opts.singleRenameVariantContainerId,
placeholder: opts.singleRenameVariantPlaceholder || 'Дополнительный вариант',
inputClass: opts.singleRenameInputClass,
rowClass: 'rmRenameVariantRow',
indentLeft: 0,
fitParentWidth: true,
prefixHtml: buildLeftRemoveButtonHtml('rmRemoveInput', 'Удалить'),
removeBeforeInput: true
});
$variantContainer.find('input').last().val(value);
});
}
function updateMultiMode() {
var $blocks = $(containerSelector + ' .rmMultiPageBlock');
var hasExtra = $blocks.length > 1;
var pageGap = hasExtra && opts.targetVariants ? '12px' : multiNominationGap;
$(containerSelector).css({
gap: pageGap,
marginBottom: pageGap
});
$('#rmMultiHeader').css('marginBottom', hasExtra ? pageGap : multiNominationGap).toggle(hasExtra);
if (opts.multiOnlySelector) $(opts.multiOnlySelector).toggle(hasExtra);
if (opts.singleOnlySelector) $(opts.singleOnlySelector).toggle(!hasExtra);
if (opts.hideContainerWhenSingle) $(containerSelector).toggle(hasExtra);
if (!hasExtra && wasMultiModeExpanded) {
restoreEmptySinglePageInput();
copyFirstRowPageToSingleCurrent();
copyFirstRowRenameTargetsToSingle();
}
$blocks.each(function (index) {
setMultiPageRowControls($(this), !hasExtra && index === 0, hasExtra, opts);
});
wasMultiModeExpanded = hasExtra;
syncModalLayout();
}
$(document).off('click.rmMultiPageAdd').on('click.rmMultiPageAdd', '.rmAddMultiPage', function () {
var wasSingle = $(containerSelector + ' .rmMultiPageBlock').length <= 1;
if (wasSingle) {
copySingleCurrentPageToFirstRow();
copySingleRenameTargetsToFirstRow();
}
$(containerSelector).append(buildMultiPageRowHtml(pageCounter++, opts));
updateMultiMode();
});
$(document).off('click.rmMultiRenameVariantAdd').on('click.rmMultiRenameVariantAdd', '.rmAddMultiRenameVariant', function () {
var $block = $(this).closest('.rmMultiPageBlock');
var $container = $block.find('.rmMultiRenameVariantsContainer').first();
var fieldCount = 1 + $block.find('.rmMultiRenameVariantInput').length;
if (fieldCount >= 3) { alert('Максимум 3 варианта переименования.'); return; }
$container.append(buildMultiRenameVariantRowHtml('', opts)).show();
syncModalLayout();
});
$(document).off('click.rmMultiRenameVariantRemove').on('click.rmMultiRenameVariantRemove', '.rmRemoveMultiRenameVariant', function () {
$(this).closest('.rmMultiRenameVariantRow').remove();
syncModalLayout();
});
$(document).off('click.rmMultiPageComment').on('click.rmMultiPageComment', '.rmMultiPageCommentToggle', function () {
var $btn = $(this);
if (!$btn.is(':visible')) return;
setMultiPageCommentExpanded($btn, $btn.attr('aria-expanded') !== 'true');
syncModalLayout();
});
$(document).off('click.rmMultiPageRemove').on('click.rmMultiPageRemove', '.rmMultiPageRow .rmRemoveInput', function () {
$(this).closest('.rmMultiPageBlock').remove();
updateMultiMode();
});
updateMultiMode();
return {
update: updateMultiMode,
isMulti: function () { return $(containerSelector + ' .rmMultiPageBlock').length > 1; }
};
}
function setExclusiveButtonActive($root, buttonSelector, $btn) {
$root.find(buttonSelector).removeClass('is-active').attr('aria-pressed', 'false');
$btn.addClass('is-active').attr('aria-pressed', 'true');
}
function bindMultiNominationFormatSwitch(rootSelector, buttonSelector) {
var $root = $(rootSelector);
$root.off('click.rmMultiFormat').on('click.rmMultiFormat', buttonSelector, function () {
setExclusiveButtonActive($root, buttonSelector, $(this));
});
}
function buildProtectAddButtonHtml() {
return buildSquareAddButtonHtml('', 'Добавить страницу', 'rmProtectAddPage');
}
function buildProtectPageRowHtml(id, pageName, isFirstRow) {
return joinHtml([
'<div', isFirstRow ? ' id="rmProtectFirstRow"' : '', ' class="rmProtectPageRow ', RESIZE_CLASS, '" style="', stRow, '">',
'<input id="', id, '" type="text" placeholder="Страница" class="rmProtectPageInput" style="', stInputBox, '"',
pageName ? ' value="' + escapeHtml(pageName) + '"' : '', '>',
isFirstRow
? buildProtectAddButtonHtml()
: '<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить">−</button>',
'</div>'
]);
}
function buildReportFormHtml(ctx, isZka) {
var reportTextPlaceholder = (isZka ? 'Введите текст запроса.' : 'Текст запроса (можно дополнить или изменить).') + ' Подпись будет добавлена автоматически.';
if (isZka) {
return joinHtml([
'<input id="rmReportHeader" type="text" placeholder="Тема/заголовок" style="', stInputFull, '" value="', escapeHtml(ctx.pageLink), '">',
'<textarea id="rmReportText" placeholder="', reportTextPlaceholder, '"></textarea>',
buildQuickPhrasesPanelHtml('rmReportText')
]);
}
return joinHtml([
'<div id="rmProtectModeBtns" class="', RESIZE_CLASS, '" style="margin-bottom:14px;"><div class="rmSegmentedBar">',
'<button id="rmProtectModeInstall" type="button" class="rmSegmentedBtn rmProtectModeBtn is-active" aria-pressed="true">🛡️ Установить защиту</button>',
'<button id="rmProtectModeRemove" type="button" class="rmSegmentedBtn rmProtectModeBtn" aria-pressed="false">📛 Снять защиту</button>',
'</div></div>',
'<div id="rmProtectMultiWrap" class="', RESIZE_CLASS, '">',
'<div id="rmProtectHeaderWrap" style="display:none;margin-bottom:6px;"><div class="rmProtectHeaderRow" style="', stRow.replace('margin-bottom:6px;', 'margin-bottom:0;'), '"><input id="rmProtectHeader" type="text" placeholder="Заголовок (для нескольких страниц)" style="', stInputBox, '">', buildProtectAddButtonHtml(), '</div></div>',
buildProtectPageRowHtml('rmProtectPage0', ctx.pageName, true),
'<div id="rmProtectPagesContainer"></div>',
'</div>',
'<div id="rmProtectLevelsWrap" class="', RESIZE_CLASS, ' rmProtectControlGroup"><div class="rmProtectControlLabel">Уровень защиты</div><div id="rmProtectLevels" class="rmSegmentedBar">',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="полузащиту">полузащита</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="стабилизацию">стабилизация</button>',
'</div></div>',
'<div id="rmProtectReasonsWrap" class="', RESIZE_CLASS, ' rmProtectControlGroup"><div class="rmProtectControlLabel">Причины</div><div id="rmProtectReasons" class="rmSegmentedBar">',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="война правок">война правок</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="неконсенсусные изменения">неконсенсусные изменения</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="вандализм">вандализм</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="популярная статья">популярная статья</button>',
'</div></div>',
'<div id="rmRemoveLevelsWrap" class="', RESIZE_CLASS, ' rmProtectControlGroup" style="display:none;"><div class="rmProtectControlLabel">Уровень защиты</div><div id="rmRemoveLevels" class="rmSegmentedBar">',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="полузащиту">полузащита</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="стабилизацию">стабилизация</button>',
'</div></div>',
'<div id="rmProtectTextBlock" class="', RESIZE_CLASS, '"><textarea id="rmReportText" placeholder="', reportTextPlaceholder, '"></textarea>',
buildQuickPhrasesPanelHtml('rmReportText'), '</div>'
]);
}
// ═══════════════════════════════════════════════════════════════════════════
// ОБРАБОТЧИКИ ОПЕРАЦИЙ
// ═══════════════════════════════════════════════════════════════════════════
var handlers = {
// ── КБУ ─────────────────────────────────────────────────────────────
showKbu: function (op) {
var forCategory = !!(op && op.forCategory);
var reasons = getFastRemoveReasons();
createModal({
title: 'Быстрое удаление',
width: 'compact',
subtitleHtml: '<span id="rmKbuCriteriaLinkWrap"></span>'
});
$('#removerModalContent').html(buildKbuFormHtml(reasons));
$('#rmSel').change(function () { updateKbuReasonControls(reasons); syncModalLayout(); });
$('#removerModalContent').off('click.rmKbuCommonsReplacementMode').on('click.rmKbuCommonsReplacementMode', '.rmKbuCommonsReplacementModeBtn', function () {
setCurrentCommonsReplacementMode($(this).data('rmCommonsReplacementMode') || 'file');
});
$('#removerModalContent').off('click.rmKbuDfudReason').on('click.rmKbuDfudReason', '.rmKbuDfudReasonBtn', function () {
var $btn = $(this);
var active = !$btn.hasClass('is-active');
$btn.toggleClass('is-active', active).attr('aria-pressed', active ? 'true' : 'false');
});
$('#removerModalContent').off('click.rmKbuBadfairuseReason').on('click.rmKbuBadfairuseReason', '.rmKbuBadfairuseReasonBtn', function () {
setExclusiveButtonActive($('#rmKbuBadfairuseReasonsWrap'), '.rmKbuBadfairuseReasonBtn', $(this));
});
updateKbuReasonControls(reasons);
syncModalLayout();
renderModalFooter('submit', {
submitText: 'Номинировать',
onSubmit: function () {
var idx = $('#rmSel').val();
var reason = reasons[idx] || reasons[0];
var kbuData = buildFastRemoveTemplateData(reason);
if (kbuData.error) {
alert(kbuData.error);
return false;
}
var fastRemovalSummary = makeFastRemovalSummary(reason, kbuData);
startProcessing();
if (forCategory) {
var tpl = kbuData.templateText;
var categorySummary = fastRemovalSummary;
editPageContent(mwCfg.wgPageName, { summary: categorySummary, readError: 'Не удалось получить содержимое.' },
function (text) { return { text: wrapInNoinclude(text, T_OPEN + tpl + T_CLOSE) }; },
function (err) {
if (err) {
unlockModalSubmit();
logStatus('Ошибка записи.', err);
} else {
logStatus('Страница номинирована к быстрому удалению.', null, { trackError: false });
finalizeFastRemoval([normTitle(mwCfg.wgPageName)], categorySummary);
}
});
} else {
var job = {
mode: 'nominate', opId: 'fRm',
kbuTemplate: reason[0], kbuTemplateText: kbuData.templateText,
removeMoveToCommons: !!kbuData.removeMoveToCommons,
summary: fastRemovalSummary,
inArticle: true
};
processPageList([normTitle(mwCfg.wgPageName)], job, function (notifiedPages) {
finalizeFastRemoval(notifiedPages, job.summary);
});
}
return true;
}
});
},
// ── Универсальная номинация (КУ, КПМ, КУЛ, КОБ, КРАЗД, ВУС) ────────
showNomination: function (op) {
var nom = op.nomination;
var pg = normTitle(mwCfg.wgPageName);
var date = getDate()[1];
var nomPage = nom.nomPage(date);
var multiMode = nom.supportsMulti;
function updateTransferUi() {
var mode = getTransferModeFromButtons();
var isNone = mode === 'none';
var isKbu = mode === 'kbu' || mode === 'both';
var isKul = mode === 'kul' || mode === 'both';
$('#rmTransferBtnNone').toggleClass('is-active', isNone).attr('aria-pressed', isNone ? 'true' : 'false');
$('#rmTransferBtnKbu').toggleClass('is-active', isKbu).attr('aria-pressed', isKbu ? 'true' : 'false');
$('#rmTransferBtnKul').toggleClass('is-active', isKul).attr('aria-pressed', isKul ? 'true' : 'false');
var t = transferTexts[mode];
if (t) { $('#rmTransferHint').text(t.hint).show(); } else { $('#rmTransferHint').hide().text(''); }
applyGeneratedText($('#rmMsg'), t && t.notice ? t.notice + '\n' : '');
}
createModal({
title: 'Номинация: ' + nom.template,
subtitlePage: nomPage,
subtitleLabel: 'Текущий день'
});
$('#removerModalContent').html(buildNominationFormHtml(nom, pg, multiMode));
setupResizableModal('rmMsg');
// Логика переноса
if (nom.supportsTransfer) {
$(document).off('click.rmTransfer').on('click.rmTransfer', '#rmTransferBtnNone,#rmTransferBtnKbu,#rmTransferBtnKul', function () {
if (this.id === 'rmTransferBtnNone') {
$('#rmTransferBtnKbu,#rmTransferBtnKul').removeClass('is-active');
$('#rmTransferBtnNone').addClass('is-active');
} else {
$(this).toggleClass('is-active');
var anyOn = $('#rmTransferBtnKbu').hasClass('is-active') || $('#rmTransferBtnKul').hasClass('is-active');
$('#rmTransferBtnNone').toggleClass('is-active', !anyOn);
}
updateTransferUi();
});
updateTransferUi();
}
// Многостраничный режим
if (multiMode) {
setupMultiPageNominationUi(getMultiNominationUiOptions('article', {
setup: true,
defaultPage: pg,
renameMulti: nom.extraInput && nom.extraInput.type === 'rename',
deletionMulti: nom.template === 'к удалению'
}));
bindMultiNominationFormatSwitch('#removerModalContent', '.rmArticleMultiFormatBtn');
}
if (nom.extraInput) wireMultiInput(nom.extraInput);
renderModalFooter('submit', {
submitText: 'Номинировать',
showSubscribe: true,
onSubmit: function () {
var isMulti = multiMode && $('#rmMultiPagesContainer .rmMultiPageBlock').length > 1;
var singleCurrentInput = $('#rmSingleRenameCurrent').val() || '';
var inputVal = !isMulti ? normTitle(singleCurrentInput || $('#rmMultiPagesContainer .rmMultiPageInput').first().val() || '') : '';
var changed = inputVal && inputVal !== pg;
function executeJob(job) {
startProcessing();
runFlow({
templateStep: function (next) {
if (!job.inArticle) { next(); return; }
processPageList(job.pages, job, function (notifiedPages, err) {
job._notifiedPages = notifiedPages;
next(err);
});
},
nominationStep: function (done) {
publishNomination({
pageTitle: job.nomPage,
navTemplate: job.navTemplate,
sectionTitle: job.section,
summary: job.nominationSummary || job.summary,
text: getNominationPublishText(job)
}, function (err) { done(err, { pageTitle: job.nomPage, sectionTitle: job.section }); });
},
notifyStep: function (nominationInfo, next) {
var pages = job._notifiedPages || [];
if (!setAlert || !pages.length) { next(); return; }
notifyAuthorsForPages(pages, {
summary: job.summary,
actionText: job.comment,
discussionPage: nominationInfo && nominationInfo.pageTitle,
discussionSection: nominationInfo && nominationInfo.sectionTitle
}, next);
},
skipLink: op.id === 'fRm'
});
}
function run(targetPg) {
var job = buildNominationJob(op, targetPg, isMulti);
if (!job) { unlockModalSubmit(); return; }
if (job.isMulti && job.inArticle && getNominationConflictRule(job)) {
startProcessing();
inspectMultiNominationConflicts(job, function (err, conflicts) {
if (err) { markSubmitError(); return; }
if (!conflicts.length) { executeJob(job); return; }
showNominationConflictResolution(job, conflicts, function (resolvedJob) {
executeJob(resolvedJob);
});
});
return;
}
executeJob(job);
}
if (changed) {
apiReq({ prop: 'info', titles: inputVal }, 'query', function (data) {
if (data && data.error) {
unlockModalSubmit();
alert('Не удалось проверить страницу из-за ошибки API. Попробуйте ещё раз.');
return;
}
var page = getFirstQueryPage(data);
if (!page) {
unlockModalSubmit();
alert('Не удалось проверить страницу из-за временной ошибки. Попробуйте ещё раз.');
return;
}
if (page.missing !== undefined) {
unlockModalSubmit();
alert('Страница «' + inputVal + '» не существует.');
return;
}
run(normTitle(page.title || inputVal));
});
} else {
run(pg);
}
return true;
}
});
},
// ── Снятие номинации (статья) ────────────────────────────────────────
showArticleClose: function () {
showCloseActionsModal({
inputName: 'rmCloseAction',
listId: 'rmCloseActions',
emptyText: 'Не найдено подходящих шаблонов для закрытия.',
emptyDetails: 'Проверяются: КУ, КПМ, КБУ, КУЛ.',
getActions: function (articleText) {
var actions = [];
var hasKu = RE_KU_ON_PAGE.test(articleText);
var hasKpm = RE_KPM_ON_PAGE.test(articleText);
var hasKbu = RE_KBU_ON_PAGE.test(articleText);
var hasKul = RE_KUL_ON_PAGE.test(articleText);
if (hasKu) {
actions.push({ id: 'ret', tag: 'КУ', label: 'Оставлено', mode: 'denom', closeType: 'ret', resultTemplate: 'Оставлено', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{Оставлено}} на СО.', comment: 'оставлена', talkNotice: true });
actions.push({ id: 'retConditional', tag: 'КУ', label: 'Условно оставлено', mode: 'denom', closeType: 'retConditional', resultTemplate: 'Условно оставлено', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{Условно оставлено}} на СО.', comment: 'условно оставлена', talkNotice: true, needsConditionalFields: true });
actions.push({ id: 'withdrawnDeletion', tag: 'КУ', label: 'Снято с удаления', mode: 'denom', closeType: 'withdrawnDeletion', resultTemplate: 'Снято с удаления', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{Снято с удаления}} на СО.', comment: 'снята с удаления', talkNotice: true });
actions.push({ id: 'redirectedDeletion', tag: 'КУ', label: 'Заменено перенаправлением', mode: 'denom', closeType: 'redirectedDeletion', resultTemplate: 'Заменено перенаправлением', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, заменяет страницу перенаправлением, добавляет {{Заменено перенаправлением}} на СО.', warning: 'Осторожно: это действие перезапишет содержимое всей страницы.', comment: 'заменена перенаправлением', talkNotice: true, needsRedirectTarget: true });
}
if (hasKpm) {
actions.push({ id: 'doneRnm', tag: 'КПМ', label: 'Переименовано', mode: 'denom', closeType: 'doneRnm', resultTemplate: 'Переименовано', sourceTemplate: 'к переименованию|кпм|rename', description: 'Снимает шаблон КПМ, добавляет {{Переименовано}} на СО.', comment: 'переименована', talkNotice: true, needsOldTitle: true });
actions.push({ id: 'noRnm', tag: 'КПМ', label: 'Не переименовано', mode: 'denom', closeType: 'noRnm', resultTemplate: 'Не переименовано', sourceTemplate: 'к переименованию|кпм|rename', description: 'Снимает шаблон КПМ, добавляет {{Не переименовано}} на СО.', comment: 'не переименована', talkNotice: true });
}
if (hasKbu && hasKul) actions.push({ id: 'cleanup-both', tag: 'КБУ и КУЛ', label: 'Снятие', mode: 'cleanup', transferMode: 'both', cleanupLabel: 'КБУ и КУЛ', description: 'Снимает шаблоны КБУ/КУЛ и Hangon.' });
if (hasKbu) actions.push({ id: 'cleanup-kbu', tag: 'КБУ', label: 'Снятие', mode: 'cleanup', transferMode: 'kbu', cleanupLabel: 'КБУ', description: 'Снимает шаблоны КБУ и Hangon.' });
if (hasKul) actions.push({ id: 'cleanup-kul', tag: 'КУЛ', label: 'Снятие', mode: 'cleanup', transferMode: 'kul', cleanupLabel: 'КУЛ', description: 'Снимает шаблон КУЛ.' });
return actions;
},
afterRender: function (actions) {
var hasDoneRnm = actions.some(function (a) { return a.id === 'doneRnm'; });
var hasConditionalRet = actions.some(function (a) { return a.id === 'retConditional'; });
var hasRedirectedDeletion = actions.some(function (a) { return a.id === 'redirectedDeletion'; });
if (hasDoneRnm) {
$('#rmCloseActions input[value="doneRnm"]').closest('.rmActionItem').append(
'<div id="rmCloseOldTitleWrap" style="display:none;margin-top:6px;"><input id="rmCloseOldTitle" type="text" placeholder="Старое название" style="' + stInputFull + '"></div>'
);
}
if (hasConditionalRet) {
$('#rmCloseActions input[value="retConditional"]').closest('.rmActionItem').append(buildConditionalRetFieldsHtml());
}
if (hasRedirectedDeletion) {
$('#rmCloseActions input[value="redirectedDeletion"]').closest('.rmActionItem').append(
'<div id="rmCloseRedirectTargetWrap" style="display:none;margin-top:6px;"><input id="rmCloseRedirectTarget" type="text" placeholder="Цель перенаправления" style="' + stInputFull + '"></div>'
);
}
},
afterFooterRender: function (_, actionMap) {
function ensureConditionalTextareaResizer() {
var $textarea = $('#rmCloseConditionalReason');
if (!$textarea.length || $textarea.data('rmConditionalResizerReady')) return;
setupNestedResizableTextarea('rmCloseConditionalReason', 'rmCloseConditionalWrap', 280, 90);
$textarea.data('rmConditionalResizerReady', true);
}
function updateUi() {
var sel = actionMap[$('[name="rmCloseAction"]:checked').val()];
$('#rmCloseOldTitleWrap').toggle(!!(sel && sel.needsOldTitle));
$('#rmCloseConditionalWrap').toggle(!!(sel && sel.needsConditionalFields));
$('#rmCloseRedirectTargetWrap').toggle(!!(sel && sel.needsRedirectTarget));
if (sel && sel.needsConditionalFields) ensureConditionalTextareaResizer();
var disableNotify = !!(sel && sel.mode === 'cleanup' && sel.transferMode === 'kbu');
var $cb = $('[name="rmUAlert"]');
var $cbLabel = $('[name="rmUAlert"]').closest('label');
if ($cb.length) $cb.prop('disabled', disableNotify);
if ($cbLabel.length) $cbLabel.css({
visibility: disableNotify ? 'hidden' : 'visible',
pointerEvents: disableNotify ? 'none' : ''
});
syncModalLayout();
}
$(document).off('change.rmCloseAction').on('change.rmCloseAction', '[name="rmCloseAction"]', updateUi);
updateUi();
},
onSubmit: function (sel, pageName) {
var job;
if (sel.mode === 'denom') {
var oldTitle = sel.needsOldTitle ? ($('#rmCloseOldTitle').val() || '').trim() : '';
var conditionalReason = sel.needsConditionalFields ? normalizeQuickPhraseValue($('#rmCloseConditionalReason').val()) : '';
var conditionalDeadline = sel.needsConditionalFields ? String($('#rmCloseConditionalDeadline').val() || '').trim() : '';
var redirectTarget = sel.needsRedirectTarget ? normalizeRedirectTarget($('#rmCloseRedirectTarget').val()) : '';
if (sel.needsOldTitle && !oldTitle) { alert('Укажите старое название.'); return false; }
if (sel.needsRedirectTarget && !redirectTarget) { alert('Укажите цель перенаправления.'); return false; }
if (sel.needsRedirectTarget && normTitle(redirectTarget) === normTitle(pageName)) { alert('Цель перенаправления совпадает с текущей страницей.'); return false; }
if (conditionalDeadline && normalizeIsoDate(conditionalDeadline) !== conditionalDeadline) {
alert('Укажите срок в формате YYYY-MM-DD, например 2026-05-31.');
return false;
}
job = {
mode: 'denom',
closeType: sel.closeType,
resultTemplate: sel.resultTemplate,
sourceTemplate: sel.sourceTemplate,
oldTitle: oldTitle,
redirectTarget: redirectTarget,
conditionalReason: conditionalReason,
conditionalDeadline: conditionalDeadline,
notifyActionText: sel.comment,
skipNotify: false
};
} else {
job = {
mode: 'cleanup',
transferMode: sel.transferMode,
summary: makeSummary('снятие шаблонов ' + sel.cleanupLabel),
notifyActionText: (sel.transferMode === 'kul' || sel.transferMode === 'both')
? 'больше не номинирована к срочному улучшению'
: '',
skipNotify: !(sel.transferMode === 'kul' || sel.transferMode === 'both')
};
}
processPageList([pageName], job, function (notifiedPages, err, pageMeta) {
function finishClose() {
if (isError) { markSubmitError(); }
else { renderModalFooter('reload'); }
}
if (isError || err || job.skipNotify || !setAlert || !notifiedPages.length) { finishClose(); return; }
var meta = (pageMeta && pageMeta[normTitle(pageName)]) || {};
notifyAuthorsForPages(notifiedPages, {
summary: meta.summary || job.summary,
actionText: job.notifyActionText,
discussionPage: meta.discussionPage,
discussionSection: meta.discussionSection,
includeProposedPrefix: false
}, finishClose);
});
return true;
}
});
},
// ── ОБКАТ: номинация категории ───────────────────────────────────────
showCatNomination: function (op) {
var catType = op.catType;
var catMeta = CATEGORY_NOMINATION_META[catType] || CATEGORY_NOMINATION_META.discuss;
var now = new Date();
var catDiscPage = 'Википедия:Обсуждение категорий/' + MONTHS_NOM[now.getUTCMonth()] + '_' + now.getUTCFullYear();
var pageName = normalizeCategoryPageName(mwCfg.wgPageName);
var multiMode = !!catMeta.supportsMulti;
var renameMultiMode = !!catMeta.renameMulti && multiMode;
createModal({ title: catMeta.title, subtitlePage: catDiscPage, subtitleLabel: 'Текущий месяц' });
var variantCfgs = {
rename: { type: 'rename', firstId: 'firstRenameInput', addBtnId: 'addRenameVariant', containerId: 'renameVariantsContainer', addBtnLabel: '+ Добавить вариант', firstPh: 'Новое название без префикса Категория:', addPh: 'Дополнительный вариант названия', maxRows: 2, maxMsg: 'Максимум 3 варианта переименования.' },
merge: { firstId: 'firstMergeInput', addBtnId: 'addMergeVariant', containerId: 'mergeVariantsContainer', addBtnLabel: '+ Добавить вариант', firstPh: 'Категория для объединения без префикса Категория:', addPh: 'Дополнительный вариант объединения' }
};
var vCfg = variantCfgs[catType];
var categoryMultiOptions = {
renameMulti: renameMultiMode,
actionText: catMeta.actionText,
skipVariantInput: renameMultiMode
};
$('#removerModalContent').html(buildCategoryNominationFormHtml(vCfg, multiMode, pageName, categoryMultiOptions));
setupResizableModal('nominationReason');
if (multiMode) {
setupMultiPageNominationUi(getMultiNominationUiOptions('category', $.extend({
setup: true,
defaultPage: pageName
}, categoryMultiOptions)));
bindMultiNominationFormatSwitch('#removerModalContent', '.rmCategoryMultiFormatBtn');
}
if (vCfg) wireMultiInput(vCfg);
renderModalFooter('submit', {
submitText: 'Номинировать',
showSubscribe: true,
onSubmit: function () {
var reason = normalizeQuickPhraseValue($('#nominationReason').val());
var hasMultiRenameRows = renameMultiMode && $('#rmMultiPagesContainer .rmMultiPageBlock').length > 1;
var renamePairs = hasMultiRenameRows ? collectMultiRenamePairs({
normalizePageName: normalizeCategoryPageName,
normalizeTargetName: normalizeCategoryTargetPageName,
normalizeTemplateTargetName: normalizeCategoryTargetName
}) : [];
var renameTargetsByCategory = buildMultiRenameTargetMap(renamePairs, 'targetNames');
var renameTemplateTargetsByCategory = buildMultiRenameTargetMap(renamePairs, 'templateTargetNames');
var singleRenameCategory = renameMultiMode && !hasMultiRenameRows
? normalizeCategoryPageName($('#rmSingleRenameCurrent').val() || pageName)
: '';
var targetPages = hasMultiRenameRows
? renamePairs.map(function (pair) { return pair.pageName; })
: (multiMode ? collectCategoryPageInputValues('.rmMultiPageInput') : [pageName]);
if (renameMultiMode && !hasMultiRenameRows) targetPages = [singleRenameCategory || pageName];
var isMulti = renameMultiMode ? hasMultiRenameRows : (multiMode && targetPages.length > 1);
var multiFormat = $('.rmCategoryMultiFormatBtn.is-active').data('rmMultiFormat') || 'sections';
var commentsByCategory = isMulti ? collectMultiNominationComments(normalizeCategoryPageName) : {};
var notifiedPages = [];
if (hasMultiRenameRows && !validateMultiRenamePairs(renamePairs, 'категорию', 'новое название')) return false;
if (!targetPages.length) { alert('Укажите категорию.'); return false; }
if (isMulti) {
if (!validateMultiNominationText(targetPages, reason, commentsByCategory, 'категории')) return false;
} else if (!reason) {
alert('Пожалуйста, укажите причину/тему.');
return false;
}
var discussionTarget = isMulti ? targetPages : targetPages[0];
var discussionReason = isMulti
? (multiFormat === 'list'
? buildMultiNominationListText(targetPages, reason, commentsByCategory, renameMultiMode ? getMultiRenameDiscussionOptions(renameTargetsByCategory) : null)
: buildMultiNominationText(targetPages, reason, commentsByCategory, renameMultiMode
? getMultiRenameDiscussionOptions(renameTargetsByCategory, { headingLevel: 4 })
: { headingLevel: 4 }))
: reason;
var discussionOptions = isMulti ? {
headerText: $('#rmHeader').val(),
reasonIsPrepared: true,
renameTargetsByPage: renameTargetsByCategory
} : null;
var mainName = null, additionalNames = [];
if (vCfg) {
if (hasMultiRenameRows) {
mainName = renamePairs[0] && renamePairs[0].templateTargetName;
additionalNames = renamePairs[0] ? asNonEmptyArray(renamePairs[0].templateTargetNames).slice(1) : [];
} else {
mainName = $('#' + vCfg.firstId).val().trim();
if (!mainName) { alert('Укажите ' + (catType === 'rename' ? 'новое название' : 'категорию для объединения') + '.'); return false; }
additionalNames = collectInputValues('#' + vCfg.containerId + ' .variantInput');
}
}
var categoryDiscussionTitle = buildCategoryDiscussionTitle(discussionTarget, catType, mainName, additionalNames, discussionOptions || {});
var categorySectionTitle = getCategoryDiscussionSectionTitle(categoryDiscussionTitle);
var categoryTemplateOptions = hasMultiRenameRows ? { renameTemplateTargetsByPage: renameTemplateTargetsByCategory } : {};
categoryTemplateOptions.summary = makeNominationTargetSummary(catDiscPage, categorySectionTitle, catMeta.actionText);
startProcessing();
runFlow({
templateStep: function (next) {
addTemplatesToCategories(targetPages, catType, mainName, additionalNames, function (err, processedPages) {
notifiedPages = processedPages || [];
next(err);
}, categoryTemplateOptions);
},
nominationStep: function (done) {
createCategoryDiscussion(discussionTarget, discussionReason, catType, function (err, nominationInfo) { done(err, nominationInfo || null); }, mainName, additionalNames, discussionOptions);
},
notifyStep: function (nominationInfo, next) {
if (!setAlert || !nominationInfo) { next(); return; }
var section = normalizeSectionForLink(nominationInfo.sectionTitle || '');
notifyAuthorsForPages(notifiedPages.length ? notifiedPages : targetPages, {
summary: makeSummary('номинация [[' + nominationInfo.pageTitle + (section ? '#' + section : '') + ']]'),
actionText: catMeta.actionText,
discussionPage: nominationInfo.pageTitle,
discussionSection: nominationInfo.sectionTitle
}, next);
}
});
return true;
}
});
},
// ── Снятие номинации (категория) ─────────────────────────────────────
showCatClose: function () {
showCloseActionsModal({
inputName: 'rmCategoryCloseAction',
showCheckbox: false,
emptyText: 'Не найдено подходящих шаблонов для завершения.',
emptyDetails: 'Проверяются ОБКАТ и КУ.',
getActions: function (catText) {
var allObkat = [cfg.categoryTemplates.discuss, cfg.categoryTemplates.rename, cfg.categoryTemplates.merge].join('|');
var actions = [];
if (new RegExp('\\{\\{\\s*(?:' + allObkat + ')\\s*(?:\\||\\}\\})', 'i').test(catText)) {
actions.push({ id: 'cat-obkat-done', tag: 'ОБКАТ', label: 'Завершено', mode: 'obkat', talkTemplate: 'Обсуждавшаяся категория', description: 'Снимает шаблон ОБКАТ, добавляет {{Обсуждавшаяся категория}} на СО.', talkNotice: true });
}
if (RE_KU_ON_PAGE.test(catText)) {
actions.push({ id: 'cat-ku-cleanup', tag: 'КУ', label: 'Снятие', mode: 'cleanup', description: 'Снимает шаблон КУ без записи на СО.' });
}
return actions;
},
onSubmit: function (sel, pageName) {
if (sel.mode === 'obkat') markCategoryDiscussionAsDone(pageName);
if (sel.mode === 'cleanup') removeKuFromCategory(pageName);
return true;
}
});
},
// ── Защита / Запрос к администраторам ───────────────────────────────
showReport: function (op) {
var mode = op.reportMode || 'protect';
var ctx = getReporterContext(mode);
var isZka = mode === 'request';
var protectMode = 'install';
var pageCounter = 1;
function buildProtectText(pm) {
if (pm === 'remove') {
var removeLevels = [];
$('#rmRemoveLevels .rmToggleBtn.is-active').each(function () { removeLevels.push($(this).data('label')); });
return removeLevels.length ? 'Просьба снять ' + removeLevels.join(' и/или ') + '.' : '';
}
var levels = [], reasons = [];
$('#rmProtectLevels .rmToggleBtn.is-active').each(function () { levels.push($(this).data('label')); });
$('#rmProtectReasons .rmToggleBtn.is-active').each(function () { reasons.push($(this).data('label')); });
if (!levels.length && !reasons.length) return '';
var text = 'Просьба установить';
if (levels.length) text += ' ' + levels.join(' и/или ');
if (reasons.length) text += ' по причине: ' + reasons.join(', ');
return text + '.';
}
function applyProtectMode(m) {
protectMode = m;
var isInstall = m === 'install';
$('#rmProtectModeInstall').toggleClass('is-active', isInstall).attr('aria-pressed', isInstall ? 'true' : 'false');
$('#rmProtectModeRemove').toggleClass('is-active', !isInstall).attr('aria-pressed', !isInstall ? 'true' : 'false');
$('#removerModalTitleText').text(isInstall ? 'Запрос на защиту страницы' : 'Запрос на снятие защиты');
var linkPage = isInstall ? 'Википедия:Установка защиты' : 'Википедия:Снятие защиты';
$('#rmProtectLinkWrap').html('<a href="' + getPageUrl(linkPage) + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">' + escapeHtml(linkPage) + '</a>');
$('#rmProtectLevelsWrap,#rmProtectReasonsWrap').toggle(isInstall);
$('#rmRemoveLevelsWrap').toggle(!isInstall);
$('#rmProtectLevels .rmProtectOptBtn,#rmProtectReasons .rmProtectOptBtn,#rmRemoveLevels .rmProtectOptBtn').removeClass('is-active');
$('#rmReportText').val('').removeData('rmGenerated');
}
function updateProtectMultiUi() {
var $rows = $('#rmProtectMultiWrap .rmProtectPageRow');
var hasExtra = $rows.length > 1;
if (!$rows.length) {
$('#rmProtectPagesContainer').append(buildProtectPageRowHtml('rmProtectPage' + pageCounter++, ctx.pageName, true));
$rows = $('#rmProtectMultiWrap .rmProtectPageRow');
hasExtra = false;
}
$('#rmProtectHeaderWrap').toggle(hasExtra);
if (!hasExtra) $('#rmProtectHeader').val('');
$rows.each(function () {
var $row = $(this);
$row.find('.rmProtectAddPage,.rmRemoveInput').remove();
$row.append(hasExtra
? '<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить">−</button>'
: buildProtectAddButtonHtml()
);
});
syncModalLayout();
}
createModal({
title: isZka ? 'Запрос к администраторам' : 'Запрос на защиту страницы',
width: 'compact',
subtitleHtml: isZka
? '<a href="' + getPageUrl('Википедия:Запросы к администраторам') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Википедия:Запросы к администраторам</a>' +
' · <a href="' + getPageUrl('Википедия:Запросы к администраторам/Быстрые') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Быстрые</a>'
: '<span id="rmProtectLinkWrap"></span>'
});
$('#removerModalContent').html(buildReportFormHtml(ctx, isZka));
if (!isZka) {
$('#removerModalContent')
.on('click', '#rmProtectModeInstall', function () { applyProtectMode('install'); })
.on('click', '#rmProtectModeRemove', function () { applyProtectMode('remove'); })
.on('click', '.rmProtectOptBtn', function () {
$(this).toggleClass('is-active');
if (protectMode === 'install') {
var $levels = $('#rmProtectLevels .rmProtectOptBtn.is-active');
if ($(this).closest('#rmProtectReasons').length && $(this).hasClass('is-active') && $levels.length === 0) {
$('#rmProtectLevels .rmProtectOptBtn').first().addClass('is-active');
}
if ($(this).closest('#rmProtectLevels').length && !$(this).hasClass('is-active') && $levels.length === 0) {
$('#rmProtectReasons .rmProtectOptBtn').removeClass('is-active');
}
}
applyGeneratedText($('#rmReportText'), buildProtectText(protectMode));
});
$(document)
.off('click.rmProtectAdd').on('click.rmProtectAdd', '.rmProtectAddPage', function () {
var id = 'rmProtectPage' + pageCounter++;
$('#rmProtectPagesContainer').append(buildProtectPageRowHtml(id, '', false));
updateProtectMultiUi();
})
.off('click.rmProtectRemove').on('click.rmProtectRemove', '.rmProtectPageRow .rmRemoveInput', function () {
$(this).closest('.rmProtectPageRow').remove(); updateProtectMultiUi();
});
applyProtectMode('install');
}
setupResizableModal('rmReportText');
renderModalFooter('submit', {
showCheckbox: false,
showSubscribe: true,
submitText: 'Отправить',
onSubmit: function () { doReport(ctx, false, protectMode); return true; }
});
if (isZka) {
$('<button id="rmReportFast" style="' + stCancel + '">Быстрый запрос</button>').insertBefore('#removerSubmit');
$('#rmReportFast').click(function () {
if ($('#removerSubmit').data('rmSubmitInProgress')) return;
$('#removerSubmit').data('rmSubmitInProgress', true).prop('disabled', true);
$('#rmReportFast').prop('disabled', true);
doReport(ctx, true, protectMode);
});
}
}
};
// ═══════════════════════════════════════════════════════════════════════════
// ВСПОМОГАТЕЛЬНЫЕ: КБУ, закрытие, категории
// ═══════════════════════════════════════════════════════════════════════════
function getFastRemoveCriteriaAnchorFromConfig(templateName) {
var anchors = cfg.fastRemoveCriteriaAnchors || {};
var template = String(templateName || '').trim();
var lower = template.toLowerCase();
var key;
if (!template) return '';
if (Object.prototype.hasOwnProperty.call(anchors, template)) return anchors[template];
for (key in anchors) {
if (Object.prototype.hasOwnProperty.call(anchors, key) && String(key).toLowerCase() === lower) {
return anchors[key];
}
}
return '';
}
function getFastRemoveCriteriaAnchor(reason) {
var configured = reason ? getFastRemoveCriteriaAnchorFromConfig(reason[0]) : '';
var template, label, m;
if (configured) return configured;
template = String(reason && reason[0] || '');
label = String(reason && reason[1] || '');
if (/^(?:подст\s*:\s*)?(?:deleteslow|ds)$/i.test(template) || /^\s*ds\b/i.test(label)) return 'С1';
m = label.match(/^\s*([А-ЯЁA-Z]{1,3}\d+)(?:\.\d+)?/);
return m ? m[1] : '';
}
function buildFastRemoveCriteriaLinkHtml(reason) {
var anchor = getFastRemoveCriteriaAnchor(reason);
var label = KBU_CRITERIA_PAGE + (anchor ? '#' + anchor : '');
var url = getPageUrlWithFragment(KBU_CRITERIA_PAGE, anchor);
return '<a href="' + escapeHtml(url) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' +
escapeHtml(label) + '</a>';
}
function getFastRemoveReasons() {
var reasons = cfg.fastRemoveReasons;
var prefix = (mwCfg.wgIsRedirect ? 'ОП' : 'О') + ({ 0: 'С', 2: 'У', 3: 'У', 6: 'Ф', 14: 'К' }[mwCfg.wgNamespaceNumber] || '');
var all = [];
if (isCategory && reasons.categories) all = all.concat(reasons.categories);
['general','articles','redirects','files','users','special'].forEach(function (k) { if (reasons[k]) all = all.concat(reasons[k]); });
if (!isCategory && reasons.categories) all = all.concat(reasons.categories);
return all.filter(function (r) { return prefix.indexOf(r[2] !== undefined ? r[2] : r[1].charAt(0)) >= 0; });
}
function showCloseActionsModal(opts) {
createModal({ title: 'Снятие шаблонов номинаций', inline: true });
$('#removerModalContent').html('<p style="margin:0;">Определение доступных действий...</p>');
var pageName = normTitle(mwCfg.wgPageName);
getText(pageName, function (pageText, readErr) {
if (readErr) { showInfoAndClose('Не удалось прочитать содержимое страницы.', readErr.info || readErr.code || '', true); return; }
if (pageText === null) { showInfoAndClose('Не удалось прочитать содержимое страницы.', '', true); return; }
var actions = opts.getActions(pageText);
if (!actions.length) { showInfoAndClose(opts.emptyText, opts.emptyDetails || ''); return; }
var actionMap = actions.reduce(function (m, a) { m[a.id] = a; return m; }, {});
$('#removerModalContent').html(buildActionsHtml(actions, opts.inputName, opts.listId));
$('#removerModalContent').off('change.rmActionWarnings').on('change.rmActionWarnings', 'input[name="' + opts.inputName + '"]', function () {
syncActionWarnings(opts.inputName, opts.listId);
syncModalLayout();
});
if (opts.afterRender) opts.afterRender(actions, actionMap, pageName, pageText);
renderModalFooter('submit', {
showCheckbox: opts.showCheckbox,
submitText: 'Выполнить',
onSubmit: function () {
var sel = getSelectedAction(opts.inputName, actionMap);
if (!sel) return false;
startProcessing();
return opts.onSubmit(sel, pageName, pageText, actionMap) !== false;
}
});
if (opts.afterFooterRender) opts.afterFooterRender(actions, actionMap, pageName, pageText);
syncActionWarnings(opts.inputName, opts.listId);
});
}
function runPageEditWithStatus(opts) {
var o = opts || {};
var statusId = logStatus(o.pendingText, null, { pending: true, trackError: false });
editPageContent(o.pageName, o.editOptions, o.buildFn, function (err, meta) {
if (err) { logStatus(o.errorText, err, { statusId: statusId }); unlockModalSubmit(); return; }
logStatus(o.successText, null, { statusId: statusId, trackError: false });
if (o.onSuccess) o.onSuccess(meta || null);
});
}
function removeKuFromCategory(pageName) {
runPageEditWithStatus({
pageName: pageName,
pendingText: 'Снимается шаблон КУ...',
errorText: 'Снятие шаблона КУ.',
successText: 'Шаблон КУ снят.',
editOptions: { summary: makeSummary('снятие шаблона КУ'), watchlist: 'nochange', readError: 'Страница не существует.', readErrorCode: 'read_failed' },
buildFn: function (text) {
var r = stripTemplatesByPattern(text, '(?:к\\s*удалению|ку)');
if (!r.removed) return { error: { code: 'no_changes', info: 'Шаблон КУ не найден.' } };
return { text: r.text.replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n') };
},
onSuccess: function () {
logStatus('Шаблон на СО не устанавливался.', null, { trackError: false });
renderModalFooter('reload');
}
});
}
// ── Категории: добавление шаблона ────────────────────────────────────────
function addTemplateToCategory(pageName, type, mainName, additionalNames, callback, options) {
var cb = callback || function () {};
var opts = options || {};
var cfgByType = {
discuss: { action: 'обсуждение', template: 'Обсуждаемая категория', aliases: cfg.categoryTemplates.discuss },
deletion: { action: 'удаление', template: 'Обсуждаемая категория', aliases: cfg.categoryTemplates.discuss },
rename: { action: 'переименование', template: 'Категория к переименованию', needsMain: true, aliases: cfg.categoryTemplates.rename },
merge: { action: 'объединение', template: 'Категория к объединению', needsMain: true, aliases: cfg.categoryTemplates.merge }
};
var typeCfg = cfgByType[type];
if (!typeCfg) { cb({ code: 'error', info: 'Неизвестный тип номинации.' }); return; }
var dateStr = getDate()[0];
var parts = [dateStr];
if (typeCfg.needsMain) {
if (!mainName) { cb({ code: 'error', info: 'Не указано основное название.' }); return; }
parts.push(mainName);
if (additionalNames && additionalNames.length) Array.prototype.push.apply(parts, additionalNames);
}
var tplText = T_OPEN + typeCfg.template + '|' + parts.join('|') + T_CLOSE;
editPageContent(pageName, { summary: opts.summary || makeSummary('добавление шаблона номинации на ' + typeCfg.action), readError: 'Не удалось получить содержимое.' },
function (text) {
if (hasTemplateWithDateByPattern(text, typeCfg.aliases, dateStr)) {
return { skip: true, meta: { status: 'already_present' } };
}
return { text: wrapInNoinclude(text, tplText) };
},
function (err, meta) {
if (!err && meta && meta.status === 'already_present') {
logStatus('На странице ' + buildQuotedStatusPageLink(pageName) + ' уже есть шаблон номинации с датой ' + dateStr + '.', null, { trackError: false });
} else {
logPageEdit(pageName, err);
}
if (err) { cb(err); return; }
if (type === 'merge') {
addMergeTemplatesToTargets(pageName, mainName, additionalNames, dateStr, opts.summary, function () { cb(null); });
return;
}
cb(null);
}
);
}
function collectCategoryPageInputValues(selector) {
var pages = [];
$(selector).each(function () {
var title = normalizeCategoryPageName($(this).val() || '');
if (title && pages.indexOf(title) === -1) pages.push(title);
});
return pages;
}
function addTemplatesToCategories(pages, type, mainName, additionalNames, callback, options) {
var cb = callback || function () {};
var opts = options || {};
var processedPages = [];
eachSequential(pages || [], function (pageName, next) {
var pageMainName = mainName;
var pageAdditionalNames = additionalNames;
var pageTargets;
if (type === 'rename' && opts.renameTemplateTargetsByPage) {
pageTargets = opts.renameTemplateTargetsByPage[normTitle(pageName)];
if (Array.isArray(pageTargets)) {
pageMainName = pageTargets[0] || mainName;
pageAdditionalNames = pageTargets.slice(1);
} else if (pageTargets) {
pageMainName = pageTargets;
pageAdditionalNames = [];
}
}
addTemplateToCategory(pageName, type, pageMainName, pageAdditionalNames, function (err) {
if (!err) processedPages.push(pageName);
next(err || null);
}, { summary: opts.summary });
}, function (err) { cb(err, processedPages); });
}
function addMergeTemplatesToTargets(sourcePage, mainName, additionalNames, dateStr, summary, callback) {
var cb = callback || function () {};
var currentCatName = normTitle(stripCatPrefix(sourcePage));
var targets = [mainName].concat(additionalNames || []);
if (!targets.length) { cb(); return; }
eachSequential(targets, function (target, next) {
var targetPage = normalizeCategoryPageName(target);
addMergeTemplateToTargetCategory(targetPage, currentCatName, dateStr, summary, function (success, status) {
var url = getPageUrl(targetPage);
var linkHtml = '<a href="' + escapeHtml(url) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' + escapeHtml(targetPage) + '</a>';
if (success) {
var extra = (status === 'already_exists' || status === 'updated') ? ' (' + formatMergeStatus(status) + ')' : '';
logStatus('Шаблон добавлен в ' + linkHtml + extra + '.', null, { trackError: false });
} else {
logStatus('Ошибка при добавлении шаблона в ' + linkHtml + '.', { code: 'merge_target_failed', info: status }, { trackError: false });
}
next();
});
}, cb);
}
function addMergeTemplateToTargetCategory(targetPageName, sourceCatName, dateStr, summary, callback) {
if (typeof summary === 'function') {
callback = summary;
summary = '';
}
editPageContent(targetPageName, { summary: summary || makeSummary('добавление шаблона объединения'), readError: 'Не удалось получить содержимое' },
function (text) {
var existing = text.match(getCategoryMergeRe());
if (existing) {
var cats = existing[1].split('|').slice(1).map(function (p) { return p.trim(); }).filter(function (p) { return p.indexOf('=') === -1 && p.length > 0; });
var norm = sourceCatName.replace(/\s+/g, ' ').trim();
if (cats.some(function (c) { return c.replace(/\s+/g, ' ').trim() === norm; })) { return { skip: true, meta: { status: 'already_exists' } }; }
return {
text: text.replace(existing[0], function () { return existing[0].replace(/\}\}\s*$/, '|' + sourceCatName + '}}'); }),
summary: summary || makeSummary('дополнение шаблона объединения ' + formatCatLink(sourceCatName)),
meta: { status: 'updated' }
};
}
return { text: wrapInNoinclude(text, T_OPEN + 'Категория к объединению|' + dateStr + '|' + sourceCatName + T_CLOSE), meta: { status: 'created' } };
},
function (err, meta) { callback(!err, err ? err.info : ((meta && meta.status) || 'updated')); }
);
}
// ── Категории: обсуждение ────────────────────────────────────────────────
function buildCategoryDiscussionTitle(pageName, type, mainName, additionalNames, options) {
var opts = options || {};
var pages = Array.isArray(pageName) ? pageName : null;
var titleText;
if (pages && pages.length) {
titleText = String(opts.headerText || '').trim();
if (!titleText && type === 'rename' && opts.renameTargetsByPage) titleText = formatRenameItemsWithAnd(pages, opts.renameTargetsByPage);
if (!titleText) titleText = formatPagesWithAnd(pages, ':') + (type === 'deletion' ? ' → удалить' : '');
return '=== ' + titleText + ' ===';
}
var title = '=== [[:' + pageName + ']]';
if (type === 'rename' || type === 'merge') {
title += (type === 'rename' ? ' → ' : ' объединить с ') + formatCatLink(mainName);
if (additionalNames && additionalNames.length) {
var conj = type === 'rename' ? ' или ' : ' и ';
var head = additionalNames.slice(0, -1).map(formatCatLink).join(', ');
title += (additionalNames.length > 1 ? ', ' + head : '') + conj + formatCatLink(additionalNames[additionalNames.length - 1]);
}
} else if (type === 'deletion') {
title += ' → удалить';
}
return title + ' ===';
}
function getCategoryDiscussionSectionTitle(discussionTitle) {
return String(discussionTitle || '').replace(/^===\s*/, '').replace(/\s*===\s*$/, '').trim();
}
function createCategoryDiscussion(pageName, reason, type, callback, mainName, additionalNames, options) {
var cb = callback || function () {};
var opts = options || {};
var now = new Date();
var m = now.getUTCMonth(), year = now.getUTCFullYear(), day = now.getUTCDate();
var dateHeader = '== ' + day + ' ' + MONTHS_GEN[m] + ' ' + year + ' ==';
var discPage = 'Википедия:Обсуждение категорий/' + MONTHS_NOM[m] + '_' + year;
var discTitle = buildCategoryDiscussionTitle(pageName, type, mainName, additionalNames, opts);
var discBody = (opts.reasonIsPrepared ? String(reason || '') : appendNominationSignature(reason)).replace(/^\s+|\s+$/g, '');
var discText = discTitle + '\n' + discBody + '\n';
var sectionTitle = getCategoryDiscussionSectionTitle(discTitle);
publishNomination({
pageTitle: discPage,
readErrorMessage: 'Не удалось получить содержимое страницы обсуждения.',
summary: makeNominationPageSummary(sectionTitle),
createText: function () {
return T_OPEN + 'ОБК-Навигация' + T_CLOSE + '\n\n' + dateHeader + '\n\n' + discText;
},
buildText: function (text) {
var todayMatch = new RegExp('^' + escapeRegExp(dateHeader) + '\\s*$', 'm').exec(text);
if (!/\{\{\s*ОБК-Навигация\s*\}\}/i.test(text)) {
return { error: { code: 'insert_failed', info: 'Не найден шаблон ' + T_OPEN + 'ОБК-Навигация' + T_CLOSE + '.' } };
}
if (todayMatch) {
var dayContentStart = todayMatch.index + todayMatch[0].length;
var nextDayMatch = /^==[^=\n].*==\s*$/m.exec(text.slice(dayContentStart));
var insertAt = nextDayMatch ? dayContentStart + nextDayMatch.index : text.length;
return { text: insertDiscussionBlockAt(text, insertAt, discText, '\n\n') };
}
return { text: insertTopDiscussionSection(text, dateHeader + '\n\n' + discText) };
}
}, function (err) {
if (err) { cb(err); return; }
cb(null, { pageTitle: discPage, sectionTitle: sectionTitle });
});
}
// ── Категории: завершение ОБКАТ ───────────────────────────────────────────
function markCategoryDiscussionAsDone(pageName) {
runPageEditWithStatus({
pageName: pageName,
pendingText: 'Снимается шаблон обсуждения...',
errorText: 'Снятие шаблона обсуждения.',
successText: 'Шаблон обсуждения снят.',
editOptions: { summary: makeSummary('обсуждение категории завершено'), readError: 'Не удалось получить содержимое.' },
buildFn: function (text) {
var allTpls = [cfg.categoryTemplates.discuss, cfg.categoryTemplates.rename, cfg.categoryTemplates.merge].join('|');
var patterns = [
new RegExp('<noinclude>\\s*\\{\\{\\s*(' + allTpls + ')\\s*\\|\\s*([^\\}]+?)\\s*\\}\\}\\s*</noinclude>', 'i'),
new RegExp('\\{\\{\\s*(' + allTpls + ')\\s*\\|\\s*([^\\}]+?)\\s*\\}\\}', 'i')
];
var match = null;
for (var i = 0; i < patterns.length; i++) { match = text.match(patterns[i]); if (match) break; }
if (!match) return { error: { code: 'no_changes', info: 'Шаблон обсуждаемой категории не найден.' } };
return {
text: text.replace(match[0], '').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n'),
meta: { tplDate: convertToStandardDate(match[2].split('|')[0].trim()) }
};
},
onSuccess: function (meta) {
var talkStatusId = logStatus('Обновляется шаблон на СО категории...', null, { pending: true, trackError: false });
updateCategoryTalkPage(pageName, meta.tplDate, function (talkErr, info) {
if (talkErr) { logStatus('Установка шаблона на СО.', talkErr, { statusId: talkStatusId }); unlockModalSubmit(); return; }
logStatus(
(info && (info.status === 'already_present' || info.status === 'no_changes')) ? 'Шаблон на СО уже установлен.' : 'Шаблон установлен на СО.',
null, { statusId: talkStatusId, trackError: false }
);
renderModalFooter('reload');
});
}
});
}
function updateCategoryTalkPage(categoryName, templateDate, callback) {
var cb = callback || function () {};
var talkPage = getTalkPage(categoryName);
var newTpl = T_OPEN + 'Обсуждавшаяся категория|' + templateDate + T_CLOSE;
getTextWithTimestamp(talkPage, function (text, baseTimestamp, readErr) {
if (readErr) { cb(makeReadError(readErr, 'talk_read_failed', 'Не удалось получить содержимое СО категории.')); return; }
if (text === null) {
apiReq({ title: talkPage, text: newTpl + '\n\n', summary: makeSummary('добавление шаблона [[ш:Обсуждавшаяся категория]] с датой ' + templateDate), createonly: true },
'edit', function (resp) {
if (resp && resp.error) {
if (resp.error.code === 'articleexists') setTimeout(function () { updateCategoryTalkPage(categoryName, templateDate, cb); }, 1000);
else cb(resp.error);
} else cb(null, { status: 'created' });
});
return;
}
var talkResult = upsertDateSectionTemplateOnTalkPage(text, 'Обсуждавшаяся категория', templateDate, '', { aliases: cfg.categoryTemplates.discussed });
var newText = talkResult.text;
if (newText === text) { cb(null, { status: talkResult.status || 'no_changes' }); return; }
var summary = talkResult.status === 'created'
? makeSummary('добавление шаблона [[ш:Обсуждавшаяся категория]] с датой ' + templateDate)
: makeSummary('обновление шаблона [[ш:Обсуждавшаяся категория]], добавлена дата ' + templateDate);
var ep = {
title: talkPage,
text: newText,
summary: summary
};
if (baseTimestamp) ep.basetimestamp = baseTimestamp;
apiReq(ep, 'edit', function (resp) { cb(resp && resp.error ? resp.error : null, resp && !resp.error ? { status: talkResult.status } : null); });
});
}
// ── Быстрое объединение (Ctrl+клик КОБ) ─────────────────────────────────
function buildQuickMergeHtml(tplDate, targets, currentCatName) {
return joinHtml([
'<p>Найден шаблон с датой <strong>', escapeHtml(tplDate), '</strong>. Категории для объединения:</p>',
'<pre style="background:', tk.bgBase, ';color:', tk.cBase, ';padding:10px;border:1px solid ', tk.bSubS, ';border-radius:4px;margin-bottom:10px;">',
targets.map(function (c) { return '• ' + escapeHtml(c); }).join('\n'),
'</pre>',
'<p><strong>Текущая категория:</strong> ', escapeHtml(currentCatName), '</p>',
'<p style="color:', tk.cSub, ';">Шаблон будет добавлен во все указанные выше категории.</p>'
]);
}
function showQuickMergeModal() {
getText(mwCfg.wgPageName, function (text, readErr) {
if (readErr) { alert('Не удалось получить содержимое: ' + (readErr.info || readErr.code || 'ошибка API') + '.'); return; }
if (!text) { alert('Не удалось получить содержимое.'); return; }
var mergeRe = getCategoryMergeRe();
var match = text.match(mergeRe);
if (!match) { alert('В текущей категории не найден шаблон "Категория к объединению".'); return; }
var params = match[1].split('|').map(function (p) { return p.trim(); });
var tplDate = params[0];
var targets = params.slice(1);
if (!targets.length) { alert('В шаблоне не найдены целевые категории.'); return; }
createModal({ title: 'Быстрое добавление шаблона объединения' });
var currentCatName = normTitle(stripCatPrefix(mwCfg.wgPageName));
$('#removerModalContent').html(buildQuickMergeHtml(tplDate, targets, currentCatName));
renderModalFooter('submit', {
showCheckbox: false,
submitText: 'Добавить шаблоны',
onSubmit: function () {
startProcessing();
$('#removerSubmit').prop('disabled', true);
eachSequential(targets, function (target, next) {
var targetPage = normalizeCategoryPageName(target);
var targetLink = buildStatusPageLink(targetPage);
addMergeTemplateToTargetCategory(targetPage, currentCatName, tplDate, function (success, status) {
if (success) logStatus('Категория ' + targetLink + ' (' + formatMergeStatus(status) + ').', null, { trackError: false });
else logStatus('Ошибка ' + targetLink + '.', { code: 'merge_target_failed', info: status }, { trackError: false });
next();
});
}, function () {
if (isError) markSubmitError(); else renderModalFooter('close');
});
return true;
}
});
});
}
// ── ЗКА/Защита: публикация ───────────────────────────────────────────────
function getReporterContext(mode) {
var rawPage = mwCfg.wgPageName;
var pageName = normTitle(rawPage)
.replace(/(Special|Служебная):(Contributions|Вклад)\//i, 'User:')
.replace(/(User talk:|Обсуждение участни(ка|цы):)/i, 'User:');
var isUserRelated = /user|contrib|участни|вклад/i.test(rawPage);
var displayName = normTitle(rawPage)
.replace(/(Special|Служебная):(Вклад|Contributions)\//i, '')
.replace(/(User talk:|Обсуждение участни(ка|цы):)/i, '')
.replace(/(user|участни(к|ца)):/i, '');
var pageLink = '[[' + pageName + (isUserRelated ? '|' + displayName + ']]' : ']]');
var reportPage = mode === 'request' ? 'Википедия:Запросы к администраторам' : 'Википедия:Установка защиты';
return { pageName: pageName, pageLink: pageLink, displayName: displayName, reportPage: reportPage };
}
function getTopDiscussionInsertIndex(pageText) {
var introEnd = getIntroBlockEndIndex(pageText);
var firstHeading = /^==[^=\n].*==\s*$/m.exec(pageText.slice(introEnd));
return firstHeading ? introEnd + firstHeading.index : introEnd;
}
function getIntroBlockEndIndex(pageText) {
var pos = 0;
while (pos < pageText.length) {
var next = skipWhitespace(pageText, pos);
var end;
if (pageText.slice(next, next + 4) === '<!--') {
end = pageText.indexOf('-->', next + 4);
if (end === -1) return next;
pos = end + 3;
continue;
}
end = skipTemplate(pageText, next);
if (end !== next) {
pos = end;
continue;
}
return next;
}
return pos;
}
function skipWhitespace(text, pos) {
while (pos < text.length && /\s/.test(text.charAt(pos))) pos++;
return pos;
}
function skipTemplate(text, pos) {
var depth = 0;
var i = pos;
if (text.slice(pos, pos + 2) !== '{{') return pos;
while (i < text.length) {
if (text.slice(i, i + 4) === '<!--') {
var commentEnd = text.indexOf('-->', i + 4);
if (commentEnd === -1) return pos;
i = commentEnd + 3;
continue;
}
if (text.slice(i, i + 2) === '{{') {
depth++;
i += 2;
continue;
}
if (text.slice(i, i + 2) === '}}') {
depth--;
i += 2;
if (depth === 0) return i;
continue;
}
i++;
}
return pos;
}
function insertDiscussionBlockAt(pageText, insertAt, blockText, separator) {
var sep = separator || '\n\n';
var before = pageText.slice(0, insertAt).replace(/\s+$/, '');
var block = String(blockText || '').replace(/\s+$/, '');
var after = pageText.slice(insertAt).replace(/^\s+/, '');
return (before ? before + sep : '') + block + (after ? sep + after : '\n');
}
function insertTopDiscussionSection(pageText, sectionText) {
return insertDiscussionBlockAt(pageText, getTopDiscussionInsertIndex(pageText), sectionText, '\n\n');
}
function doReport(ctx, fast, protectMode) {
var header = $('#rmReportHeader').val() || ctx.pageLink;
var text = $('#rmReportText').val() || '';
var isZka = ctx.reportPage === 'Википедия:Запросы к администраторам';
var isRemoveProtect = !isZka && protectMode === 'remove';
startProcessing();
var targetPage, editParams, sectionForLink;
if (fast) {
targetPage = 'Википедия:Запросы к администраторам/Быстрые';
sectionForLink = null;
editParams = {
appendtext: '\n\n' + T_OPEN + 'sub' + 'st:t:preload/ЗКАБ/subst|\n | участник = ' + ctx.displayName +
'| страница = | пояснение = ' + text + T_CLOSE + '\n',
summary: makeSummary('новый запрос [[Special:Contributions/' + ctx.displayName + ']]')
};
} else if (isZka) {
targetPage = ctx.reportPage;
sectionForLink = extractDisplayedText(header);
var isIpFull = /^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/.test(ctx.displayName);
editParams = {
text: '== ' + header + ' ==\n\n* ' + T_OPEN + 'userlinks|' + ctx.displayName + (isIpFull ? '|ip=1' : '') + T_CLOSE + '\n' + text + ' ~~' + '~~',
summary: '/* ' + sectionForLink + ' */ ' + makeSummary('новый запрос')
};
} else {
targetPage = isRemoveProtect ? 'Википедия:Снятие защиты' : 'Википедия:Установка защиты';
var pages = collectInputValues('.rmProtectPageInput');
if (!pages.length) pages = [ctx.pageName];
var sectionTitle, pageLines;
if (pages.length === 1) {
sectionTitle = '[[' + pages[0] + ']]';
pageLines = '* ' + T_OPEN + 'pagelinks-protect|' + pages[0] + T_CLOSE;
} else {
sectionTitle = ($('#rmProtectHeader').val() || '').trim() || pages.map(function (p) { return '[[' + p + ']]'; }).join(', ');
pageLines = pages.map(function (p) { return '* ' + T_OPEN + 'pagelinks-protect|' + p + T_CLOSE; }).join('\n');
}
sectionForLink = extractDisplayedText(sectionTitle);
editParams = {
section: 'new',
sectiontitle: sectionTitle,
text: pageLines + '\n' + text + ' ~~' + '~~',
summary: '/* ' + sectionForLink + ' */ ' + makeSummary('новый запрос')
};
}
var statusId = logStatus('Публикуется запрос на «' + targetPage + '»...', null, { pending: true, trackError: false });
if (isZka && !fast) {
editPageContent(targetPage, {
summary: editParams.summary,
assertuser: mwCfg.wgUserName,
readError: 'Не удалось получить содержимое страницы «' + targetPage + '».'
}, function (pageText) {
return { text: insertTopDiscussionSection(pageText, editParams.text) };
}, function (err) {
if (err) {
logStatus('Публикация запроса на «' + targetPage + '».', err, { statusId: statusId });
markSubmitError();
$('#rmReportFast').prop('disabled', false);
return;
}
logStatus('Запрос опубликован.', null, { statusId: statusId, trackError: false });
appendNominationLink(targetPage, sectionForLink);
if (sectionForLink) subscribeToTopic(targetPage, sectionForLink);
renderModalFooter('reload');
});
return;
}
apiReq($.extend({ title: targetPage }, editParams), 'edit', function (resp) {
if (resp && resp.error) {
logStatus('Публикация запроса на «' + targetPage + '».', resp.error, { statusId: statusId });
markSubmitError();
if (isZka) $('#rmReportFast').prop('disabled', false);
return;
}
logStatus('Запрос опубликован.', null, { statusId: statusId, trackError: false });
appendNominationLink(targetPage, sectionForLink);
if (sectionForLink) subscribeToTopic(targetPage, sectionForLink);
renderModalFooter('reload');
});
}
// ═══════════════════════════════════════════════════════════════════════════
// ДИСПЕТЧЕР
// ═══════════════════════════════════════════════════════════════════════════
function handleMenuClick(item, event) {
isError = false;
var op = OPERATIONS_MAP[item.id];
// Специальный случай: КОБ категории с Ctrl — быстрое добавление шаблона
if (item.id === 'cat-merge' && event && event.ctrlKey) {
showQuickMergeModal();
return;
}
if (!op) {
console.error('RemoverCore: неизвестная операция', item.id);
return;
}
var handlerFn = handlers[op.handler];
if (typeof handlerFn !== 'function') {
console.error('RemoverCore: обработчик не найден', op.handler);
return;
}
handlerFn(op, event);
}
// ─── Экспорт ─────────────────────────────────────────────────────────────
window.RemoverCore = { handleMenuClick: handleMenuClick };
}());
a4h1054trwlgqy9ofe3f54rz8c9fhlu
749735
749700
2026-07-03T19:17:24Z
Solidest
54422
749735
javascript
text/javascript
/**
* Remover — ядро (core).
* Загружается лениво при первом клике по пункту меню.
* Ожидает, что window.RemoverState уже задан remover-loader.js.
*
* Архитектура: единый реестр OPERATIONS описывает все операции (КБУ, КУ, КПМ, КУЛ, КОБ, КРАЗД, ВУС, Защита, Запрос, Снятие, кат-варианты).
* Логика выполнения сосредоточена в универсальных обработчиках.
* Экспортирует: window.RemoverCore.handleMenuClick(item, event)
*/
(function () {
'use strict';
var state = window.RemoverState;
if (!state) { console.error('RemoverCore: window.RemoverState не задан.'); return; }
var mwCfg = state.mwCfg;
var cfg = applyCoreConfigDefaults(state.cfg || {});
var isCategory = state.isCategory;
var isVector22 = state.isVector22;
var scriptLink = cfg.scriptLink;
var settingsOptionName = state.settingsOptionName || 'userjs-remover-settings';
var settingsVersion = 1;
var settingsMenuMeta = collectSettingsMenuMeta();
var settingsArticleItemLabels = settingsMenuMeta.articleLabels;
var settingsCategoryItemLabels = settingsMenuMeta.categoryLabels;
var settingsItemLabelById = settingsMenuMeta.idToLabel;
var settingsItemLabelByNorm = settingsMenuMeta.labelByNorm;
var settingsItemLabelOrder = settingsMenuMeta.labelOrder;
var settingsDefaults = getDefaultSettings();
var MENU_TITLE_PRESET_CACTIONS = '__remover_portlet_cactions__';
var MENU_TITLE_PRESET_PAGE = '__remover_portlet_page__';
var MENU_TITLE_PRESET_TOOLS = '__remover_portlet_tools__';
var initialSettings = normalizeRemoverSettings(readSettingsOptionState(state.settings || {}));
var setAlert = ('setAlert' in state) ? !!state.setAlert : initialSettings.notifyAuthor;
var setSubscribe = ('setSubscribe' in state) ? !!state.setSubscribe : initialSettings.subscribeTopic;
var signatureSeparator = ('signatureSeparator' in state && typeof state.signatureSeparator === 'string')
? state.signatureSeparator.trim()
: initialSettings.signatureSeparator;
initialSettings.notifyAuthor = setAlert;
initialSettings.subscribeTopic = setSubscribe;
initialSettings.signatureSeparator = signatureSeparator;
state.cfg = cfg;
state.settings = clonePlainObject(initialSettings);
state.setAlert = setAlert;
state.setSubscribe = setSubscribe;
// ─── Константы ──────────────────────────────────────────────────────────
var MONTHS_NOM = ['Январь','Февраль','Март','Апрель','Май','Июнь','Июль','Август','Сентябрь','Октябрь','Ноябрь','Декабрь'];
var MONTHS_GEN = ['января','февраля','марта','апреля','мая','июня','июля','августа','сентября','октября','ноября','декабря'];
var MONTHS_NOM_LOWER = MONTHS_NOM.map(function (m) { return m.toLowerCase(); });
var T_OPEN = '{' + '{';
var T_CLOSE = '}' + '}';
var KBU_CRITERIA_PAGE = 'Википедия:Критерии быстрого удаления';
var DATE_SECTION_TALK_TEMPLATES = {
ret: 'Оставлено',
withdrawnDeletion: 'Снято с удаления',
redirectedDeletion: {
name: 'Заменено перенаправлением',
sectionParam: 'раздел обсуждения',
singleSectionParam: true
}
};
var TALK_TEMPLATE_ALIASES = {
'Оставлено': ['Оставлена', 'Kept'],
'Заменено перенаправлением': ['Удалено', 'Перенаправлено'],
'Переименовано': ['Renamed'],
'Обсуждавшаяся категория': ['Обсуждалась', 'Обсуждалось']
};
var CATEGORY_NOMINATION_META = {
discuss: { title: 'Номинация: обсуждение', actionText: 'к обсуждению', supportsMulti: true },
deletion: { title: 'Номинация: к удалению', actionText: 'к удалению', supportsMulti: true },
rename: { title: 'Номинация: к переименованию', actionText: 'к переименованию', supportsMulti: true, renameMulti: true },
merge: { title: 'Номинация: к объединению', actionText: 'к объединению' }
};
var RE_ESCAPE = /[.*+?^${}()|[\]\\]/g;
var RE_KU_ON_PAGE = /\{\{\s*(?:к\s*удалению|ку)\s*(?:\||\}\})/i;
var RE_KPM_ON_PAGE = /\{\{\s*(?:к\s*переименованию|кпм|rename)\s*(?:\||\}\})/i;
var RE_KBU_ON_PAGE = /\{\{\s*(?:subst\s*:\s*)?(?:db\s*-[^|}\s]+|уд\s*-[^|}\s]+|к\s*быстрому\s*удалению|к\s*отсроченному\s*удалению|deleteslow|ds|hang\s*-?\s*on)\s*(?:\||\}\})/i;
var RE_KUL_ON_PAGE = /\{\{\s*(?:subst\s*:\s*)?к\s*улучшению\s*(?:\||\}\})/i;
var KBU_PATTERN_STR = 'db\\s*-[^|}\\s]+|уд\\s*-[^|}\\s]+|к\\s*быстрому\\s*удалению|к\\s*отсроченному\\s*удалению|deleteslow|ds';
var RE_KBU_PATTERNS = new RegExp('(?:' + KBU_PATTERN_STR + ')', 'i');
var KUL_PATTERN_STR = 'к\\s*улучшению';
var RE_KUL_PATTERN = new RegExp(KUL_PATTERN_STR, 'i');
var HANGON_PATTERN_STR = 'hang\\s*-?\\s*on';
var RE_HANGON = new RegExp(HANGON_PATTERN_STR, 'i');
var MOVE_TO_COMMONS_TEMPLATE_TITLE = 'Шаблон:Перенести на Викисклад';
var MOVE_TO_COMMONS_TEMPLATE_ALIASES = [
'Перенести на Викисклад',
'На склад',
'To Commons',
'Copy to Wikimedia Commons',
'На викисклад',
'На Викисклад',
'To commons',
'Mtc',
'Перенести в Commons',
'MTC',
'Move to Commons',
'Перенести на викисклад'
];
var BADFAIRUSE_OPTIONS = [
['1', 'несвободная карта'],
['1+', 'карта Google Maps или Google Earth'],
['2', 'несвободное изображение утилитарного объекта'],
['3', 'несвободное изображение животного, растения или гриба'],
['6', 'несвободное изображение на странице-списке эпизодов или серий'],
['7', 'несвободная анимация или отрывок из фильма'],
['10', 'несвободное изображение здания, сооружения и памятника'],
['11', 'несвободная фотография ныне живущего человека'],
['12', 'несвободная схема или чертёж']
];
var DFUD_REASON_OPTIONS = [
['1', 'может быть заменено свободным эквивалентом'],
['2', 'разрешение/качество наносит имущественный ущерб правообладателю'],
['3', 'не отвечает критерию минимального использования или не имеет значимости'],
['4', 'не публиковалось ранее или не имеет проверяемого источника'],
['9', 'используется вне пространства статей или не используется в статьях'],
['10', 'не имеет описания и/или обоснования добросовестности использования']
];
var FILE_DATE_DEADLINE_HINT = 'Срок до возможного удаления — 7 дней с даты установки.';
var RE_DATE_ISO = /^(\d{4})-(\d{2})-(\d{2})$/;
var RE_DATE_RUSSIAN = /^(\d{1,2})\s+([\u0430-\u044f\u0410-\u042f\u0451\u0401.]+)\s+(\d{2}|\d{4})$/;
var RE_DATE_DASH = /^(\d{1,4})\s*-\s*(\d{1,2})\s*-\s*(\d{1,4})$/;
var RE_DATE_DOT = /^(\d{1,2})\s*\.\s*(\d{1,2})\s*\.\s*(\d{2}|\d{4})$/;
var RE_DATE_SLASH = /^(\d{1,4})\s*\/\s*(\d{1,2})\s*\/\s*(\d{1,4})$/;
var RE_NOINCLUDE = /(\s*)<noinclude>([\s\S]*?)<\/noinclude>/i;
var RE_TEMPLATE_NS = /^(?:шаблон|template)\s*:\s*/i;
// ─── Глобальные переменные сессии ────────────────────────────────────────
var isError = false;
var logStatusSeq = 0;
var resizeObservers = [];
var modalLayoutSyncHandlers = [];
var tplAliasCache = {};
// ─── Стили ───────────────────────────────────────────────────────────────
var stStyles = cfg.modalStyles;
var tk = {
cBase: 'var(--color-base, #202122)',
cSub: 'var(--color-subtle, #72777d)',
cSubM: 'var(--color-subtle, #54595d)',
cInv: 'var(--color-inverted-fixed, #fff)',
cProg: 'var(--color-progressive, #3366cc)',
cProgH: 'var(--color-progressive--hover, #2a4b8d)',
cDang: 'var(--color-destructive, #d73333)',
cDis: 'var(--color-disabled, var(--color-subtle, #72777d))',
bgBase: 'var(--background-color-base, #fff)',
bgNSub: 'var(--background-color-neutral-subtle, #f8f9fa)',
bgN: 'var(--background-color-neutral, #eaecf0)',
bgDis: 'var(--background-color-disabled, var(--background-color-neutral, #eaecf0))',
bgProg: 'var(--background-color-progressive, #3366cc)',
bgProgH:'var(--background-color-progressive--hover, #2a4d8f)',
bgSucc: 'var(--background-color-success, #14866d)',
bgSuccH:'var(--background-color-success--hover, #0f6d57)',
bSub: 'var(--border-color-subtle, #a2a9b1)',
bSubS: 'var(--border-color-subtle, #ddd)',
bDis: 'var(--border-color-disabled, var(--border-color-subtle, #a2a9b1))',
bProg: 'var(--border-color-progressive, #3366cc)',
bProgH: 'var(--border-color-progressive--hover, #2a4d8f)',
bSucc: 'var(--border-color-success, #14866d)',
bSuccH: 'var(--border-color-success--hover, #0f6d57)'
};
var sz = {
taH: '180px',
taMinH: '100px',
taMinW: '180px',
mobileBp: 720,
modalRatio: 0.4,
modalMinWide: 420,
modalDefaultWide: 720,
viewportGap: 24,
touchDesktopGap: 120
};
var btnBase = 'border-radius:4px;padding:8px 16px;cursor:pointer;font-size:14px;font-family:inherit;transition:background .1s;';
var neutralVis = 'background:' + tk.bgNSub + ';border:1px solid ' + tk.bSub + ';color:' + tk.cBase + ';border-radius:4px;';
var stCancel = neutralVis + btnBase;
var stSubmit = 'background:' + tk.bgProg + ';color:' + tk.cInv + ';border:1px solid ' + tk.bProg + ';' + btnBase;
var stReload = 'background:' + tk.bgSucc + ';color:' + tk.cInv + ';border:1px solid ' + tk.bSucc + ';' + btnBase;
var stInputBox = 'flex:1;padding:6px;box-sizing:border-box;min-width:0;border:1px solid ' + tk.bSub + ';background:' + tk.bgBase + ';color:inherit;border-radius:2px;';
var stInputFull= 'width:100%;padding:6px;box-sizing:border-box;border:1px solid ' + tk.bSub + ';border-radius:2px;background:' + tk.bgBase + ';color:inherit;margin-bottom:10px;';
var stRow = 'display:flex;margin-bottom:6px;';
var inlineControlGap = 4;
var squareControlSize = 32;
var leftNestedControlOffset = (squareControlSize + inlineControlGap) + 'px';
var stToolBtn = neutralVis + 'padding:4px 8px;margin-left:' + inlineControlGap + 'px;cursor:pointer;font-size:12px;';
var stRemoveBtn= neutralVis + 'display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;padding:0;width:' + squareControlSize + 'px;height:' + squareControlSize + 'px;margin-left:' + inlineControlGap + 'px;cursor:pointer;font-size:12px;line-height:1;';
var stFooterWrap = 'display:flex;align-items:center;gap:8px;flex-wrap:wrap;';
var stFooterChecks = 'display:flex;flex-direction:column;gap:4px;margin-right:auto;flex:1 1 220px;min-width:0;';
var stFooterCheckLabel = 'display:inline-flex;align-items:flex-start;gap:6px;font-size:14px;line-height:1.4;max-width:100%;';
var stFooterActions = 'display:flex;gap:6px;flex:1 1 auto;min-width:0;max-width:100%;flex-wrap:wrap;justify-content:flex-end;margin-left:auto;';
var stHeaderIconBtn = 'margin:0 0 0 auto;width:32px;min-width:32px;height:32px;padding:0;display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;border:1px solid ' + tk.bSub + ';border-radius:4px;background:' + tk.bgNSub + ';color:' + tk.cSubM + ';cursor:pointer;font-size:16px;line-height:1;';
var multiNominationGap = '6px';
var RESIZE_CLASS = 'rm-resizable';
// ═══════════════════════════════════════════════════════════════════════════
// РЕЕСТР ОПЕРАЦИЙ
// Каждая запись описывает одну кнопку меню. Поля:
// id — идентификатор (совпадает с item.id из loader)
// handler — имя метода-обработчика в объекте handlers
// handlerArg — аргумент, передаваемый в handler (опционально)
// ═══════════════════════════════════════════════════════════════════════════
var OPERATIONS = [
// ── Статьи ──────────────────────────────────────────────────────────
{
id: 'fRm',
label: 'КБУ',
handler: 'showKbu',
// Параметры номинации: заполняются при submit
nomination: {
pageTitle: function (pg) { return normTitle(pg); },
// шаблон встраивается в статью, номинационная страница отсутствует
inArticle: true
}
},
{
id: 'tRm',
label: 'КУ',
handler: 'showNomination',
nomination: {
comment: 'к удалению',
template: 'к удалению',
navTemplate: 'КУ',
nomPage: function (date) { return 'Википедия:К удалению/' + date; },
supportsMulti: true,
multiOpId: 'mRm',
supportsTransfer: true,
// шаблон встраивается в статью через <noinclude>
inArticle: true,
articleTpl: function (tplpar, date) { return 'к удалению|' + date + (tplpar ? '|' + tplpar : ''); }
}
},
{
id: 'rnm',
label: 'КПМ',
handler: 'showNomination',
nomination: {
comment: 'к переименованию',
template: 'к переименованию',
navTemplate: 'КПМ',
nomPage: function (date) { return 'Википедия:К переименованию/' + date; },
supportsMulti: true,
multiOpId: 'mRnm',
inArticle: true,
articleTpl: function (tplpar, date) { return 'к переименованию|' + date + (tplpar ? '|' + tplpar : ''); },
extraInput: {
type: 'rename',
firstId: 'rmRenameFirst', inputClass: 'rmRenameInput',
firstPh: 'Новое название',
addBtnId: 'rmAddRename', addBtnLabel: 'Добавить вариант',
containerId: 'rmRenameContainer', addPh: 'Дополнительный вариант',
maxRows: 2, maxMsg: 'Максимум 3 варианта переименования.'
}
}
},
{
id: 'imp',
label: 'КУЛ',
handler: 'showNomination',
nomination: {
comment: 'к срочному улучшению',
template: 'к улучшению',
navTemplate: 'КУЛ',
nomPage: function (date) { return 'Википедия:К улучшению/' + date; },
inArticle: true,
articleTpl: function (tplpar, date) { return 'к улучшению|' + date + (tplpar ? '|' + tplpar : ''); }
}
},
{
id: 'merge',
label: 'КОБ',
handler: 'showNomination',
nomination: {
comment: 'к объединению с другой',
summaryAction: 'к объединению',
template: 'к объединению',
navTemplate: 'КОБ',
nomPage: function (date) { return 'Википедия:К объединению/' + date; },
inArticle: true,
articleTpl: function (tplpar, date) { return 'к объединению|' + date + (tplpar ? '|' + tplpar : ''); },
extraInput: {
type: 'merge',
firstId: 'rmMergeFirst', inputClass: 'rmMergeInput',
firstPh: 'Объединить с…',
addBtnId: 'rmAddMerge', addBtnLabel: '+',
containerId: 'rmMergeContainer', addPh: 'Дополнительная статья',
maxRows: 10, maxMsg: 'Максимум 11 статей для объединения.'
}
}
},
{
id: 'split',
label: 'КРАЗД',
handler: 'showNomination',
nomination: {
comment: 'к разделению',
template: 'к разделению',
navTemplate: 'КР',
nomPage: function (date) { return 'Википедия:К разделению/' + date; },
inArticle: true,
articleTpl: function (tplpar, date) { return 'к разделению|' + date + (tplpar ? '|' + tplpar : ''); },
extraInput: {
type: 'split',
firstId: 'rmSplitFirst', inputClass: 'rmSplitInput',
firstPh: 'Разделить на…',
addBtnId: 'rmAddSplit', addBtnLabel: '+',
containerId: 'rmSplitContainer', addPh: 'Дополнительная статья'
}
}
},
{
id: 'recov',
label: 'ВУС',
handler: 'showNomination',
nomination: {
comment: '',
template: 'к восстановлению',
navTemplate: 'ВУС',
nomPage: function (date) { return 'Википедия:К восстановлению/' + date; },
inArticle: false // шаблон не ставится в (удалённую) статью
}
},
{
id: 'close',
label: 'Снятие',
handler: 'showArticleClose'
},
// ── Запросы ─────────────────────────────────────────────────────────
{
id: 'protect',
label: 'Защита',
handler: 'showReport',
reportMode: 'protect'
},
{
id: 'request',
label: 'Запрос',
handler: 'showReport',
reportMode: 'request'
},
// ── Категории ────────────────────────────────────────────────────────
{
id: 'cat-fRm',
label: 'КБУ',
handler: 'showKbu',
forCategory: true
},
{
id: 'cat-discuss',
label: 'Обсудить',
handler: 'showCatNomination',
catType: 'discuss'
},
{
id: 'cat-delete',
label: 'Удалить',
handler: 'showCatNomination',
catType: 'deletion'
},
{
id: 'cat-rename',
label: 'Переименовать',
handler: 'showCatNomination',
catType: 'rename'
},
{
id: 'cat-merge',
label: 'Объединить',
handler: 'showCatNomination',
catType: 'merge'
},
{
id: 'cat-done',
label: 'Снятие',
handler: 'showCatClose'
}
];
// Быстрый поиск по id
var OPERATIONS_MAP = {};
OPERATIONS.forEach(function (op) { OPERATIONS_MAP[op.id] = op; });
// ─── Тексты переноса (КБУ → КУ) ─────────────────────────────────────────
var transferTexts = {
kbu: { notice: 'Перенесено с быстрого удаления.', hint: 'Шаблоны КБУ и Hangon будут сняты.' },
kul: { notice: 'Перенесено с КУЛ.', hint: 'Шаблоны КУЛ будут сняты.' },
both: { notice: 'Перенесено с быстрого удаления и КУЛ.', hint: 'Шаблоны КБУ, КУЛ и Hangon будут сняты.' }
};
// ═══════════════════════════════════════════════════════════════════════════
// УТИЛИТЫ
// ═══════════════════════════════════════════════════════════════════════════
function escapeRegExp(s) { return s.replace(RE_ESCAPE, '\\$&'); }
function escapeHtml(s) {
return String(s || '')
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
.replace(/"/g, '"').replace(/'/g, ''');
}
function joinHtml(parts) { return parts.join(''); }
function ucfirst(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : ''; }
function padTwo(n) { return n < 10 ? '0' + n : String(n); }
function expandTwoDigitYear(value) {
return 2000 + parseInt(value, 10);
}
function monthToNumber(name) {
var lower = name.toLowerCase().replace(/\.$/, '');
var idx = MONTHS_GEN.indexOf(lower);
if (idx === -1) idx = MONTHS_NOM_LOWER.indexOf(lower);
if (idx === -1 && lower.length >= 3) {
for (var i = 0; i < MONTHS_GEN.length; i++) {
if (MONTHS_GEN[i].indexOf(lower) === 0 || MONTHS_NOM_LOWER[i].indexOf(lower) === 0) return i + 1;
}
}
return idx + 1;
}
function makeStandardDate(yearValue, monthValue, dayValue) {
var yearText = String(yearValue || '').trim();
var year = yearText.length === 2 ? expandTwoDigitYear(yearText) : parseInt(yearText, 10);
var month = parseInt(monthValue, 10);
var day = parseInt(dayValue, 10);
var maxDay;
if ((yearText.length !== 2 && yearText.length !== 4) || isNaN(year) || isNaN(month) || isNaN(day) || year < 1 || month < 1 || month > 12 || day < 1) return null;
maxDay = new Date(Date.UTC(year, month, 0)).getUTCDate();
if (day > maxDay) return null;
return year + '-' + padTwo(month) + '-' + padTwo(day);
}
function normalizeIsoDate(value) {
var m = String(value || '').trim().match(RE_DATE_ISO);
return m ? makeStandardDate(m[1], m[2], m[3]) : null;
}
function normalizeTemplateName(name) {
return (name || '').toLowerCase().replace(RE_TEMPLATE_NS, '').replace(/_/g, ' ').replace(/\s+/g, ' ').trim();
}
function toTemplateNameList(names) {
if (Array.isArray(names)) return names;
return String(names || '').split('|');
}
function getTalkTemplateAliases(templateName) {
return TALK_TEMPLATE_ALIASES[templateName] || [];
}
function buildTemplateNamePattern(names) {
var seen = {};
var patterns = [];
toTemplateNameList(names).forEach(function (name) {
var normalized = normalizeTemplateName(String(name || '').replace(/^(?:safe)?subst\s*:\s*/i, ''));
if (!normalized || seen[normalized]) return;
seen[normalized] = true;
patterns.push(escapeRegExp(normalized).replace(/\s+/g, '[ _]*'));
});
return patterns.join('|');
}
function buildTemplateTransclusionRegex(templateName, aliases, flags) {
var names = [templateName].concat(getTalkTemplateAliases(templateName), toTemplateNameList(aliases));
var namePattern = buildTemplateNamePattern(names);
return namePattern
? new RegExp('\\{\\{\\s*(?:(?:safe)?(?:subst|подст)\\s*:\\s*)?(?:(?:шаблон|template)\\s*:\\s*)?(' + namePattern + ')\\s*([^}]*)\\}\\}', flags || 'i')
: null;
}
function getDate(dateString) {
var d = dateString ? new Date(dateString) : new Date();
var iso = d.getUTCFullYear() + '-' + padTwo(d.getUTCMonth() + 1) + '-' + padTwo(d.getUTCDate());
var rus = d.getUTCDate() + ' ' + MONTHS_GEN[d.getUTCMonth()] + ' ' + d.getUTCFullYear();
return [iso, rus];
}
function convertToStandardDate(dateStr) {
var value = String(dateStr || '').replace(/\s+/g, ' ').trim();
var m;
var mo;
var normalized;
m = value.match(RE_DATE_ISO);
if (m) return normalizeIsoDate(value) || '';
m = value.match(RE_DATE_RUSSIAN);
if (m) {
mo = monthToNumber(m[2]);
normalized = mo ? makeStandardDate(m[3], mo, m[1]) : null;
return normalized || '';
}
m = value.match(RE_DATE_DASH);
if (m) {
normalized = makeStandardDate(m[1], m[2], m[3]);
return normalized || '';
}
m = value.match(RE_DATE_DOT);
if (m) {
normalized = makeStandardDate(m[3], m[2], m[1]);
return normalized || '';
}
m = value.match(RE_DATE_SLASH);
if (m) {
normalized = makeStandardDate(m[1], m[2], m[3]);
return normalized || '';
}
return value;
}
function getNamespaceLabel(ns, fallback) {
return (mwCfg.wgFormattedNamespaces && mwCfg.wgFormattedNamespaces[ns]) || fallback;
}
function getTalkPage(pageName) {
var match = /([^:]*:)?(.*)/.exec(pageName);
if (match[1]) {
var ns = mwCfg.wgNamespaceIds && mwCfg.wgNamespaceIds[match[1].slice(0, -1).toLowerCase().replace(/ /g, '_')];
if (ns !== undefined) return getNamespaceLabel(ns | 1, 'Обсуждение') + ':' + match[2];
}
return getNamespaceLabel(1, 'Обсуждение') + ':' + pageName;
}
function normTitle(s) { return (s || '').replace(/_/g, ' '); }
function stripCatPrefix(s) { return (s || '').replace(/^(?:Категория|Category):\s*/i, ''); }
function normalizeRedirectTarget(value) {
var title = normTitle(String(value || '').trim());
var linkMatch = title.match(/^\[\[:?([^|\]]+)(?:\|[^\]]*)?\]\]$/);
return linkMatch ? normTitle(linkMatch[1]).trim() : title;
}
function buildRedirectText(targetTitle) {
return '#REDIRECT [[' + targetTitle + ']]';
}
function getCategoryNamespaceLabel() {
return getNamespaceLabel(14, 'Категория');
}
function normalizeCategoryPageName(value) {
var title = normTitle(value).trim();
var nsMatch, nsKey, ns;
if (!title) return '';
nsMatch = title.match(/^([^:]+):(.+)$/);
if (nsMatch) {
nsKey = nsMatch[1].toLowerCase().replace(/ /g, '_');
ns = mwCfg.wgNamespaceIds && mwCfg.wgNamespaceIds[nsKey];
if (ns === 14 || nsKey === 'category' || nsKey === 'категория') return getCategoryNamespaceLabel() + ':' + nsMatch[2].trim();
return getCategoryNamespaceLabel() + ':' + title;
}
return getCategoryNamespaceLabel() + ':' + title;
}
function makeSummary(text) { return text + ' (' + scriptLink + ')'; }
function makeNominationPageSummary(sectionTitle) {
var section = String(sectionTitle || '').replace(/\s+/g, ' ').trim();
return (section ? '/* ' + section + ' */ ' : '') + makeSummary('номинация');
}
function makeNominationTargetSummary(pageTitle, sectionTitle, actionText) {
var section = String(sectionTitle || '').trim();
var target = pageTitle + (section ? '#' + section : '');
var label = 'номинация' + (actionText ? ' ' + actionText : '');
return makeSummary('[[' + target + '|' + label + ']]');
}
function getNominationSummaryAction(nomination) {
return String(nomination && (nomination.summaryAction || nomination.comment || nomination.template) || '')
.trim();
}
function appendNominationSignature(text) {
var body = String(text || '');
return body + (signatureSeparator ? ' ' + signatureSeparator : '') + ' ~~' + '~~';
}
function extractDisplayedText(s) {
return (s || '').replace(/\[\[:?(?:[^|\]]+\|)?(.+?)\]\]/g, '$1');
}
function collectInputValues(selector) {
return $(selector).map(function () { return $(this).val().trim(); }).get().filter(Boolean);
}
function applyCoreConfigDefaults(config) {
var defaults = {
scriptLink: '[[Википедия:Гаджеты/Remover|Remover]]',
fastRemoveReasons: {
general: [
['уд-бессвязно', 'О1 Бессвязный текст'],
['уд-тест', 'О2 Тестовая страница'],
['уд-ванд', 'О3.1 Вандальная страница'],
['уд-мист', 'О3.2 Мистификация'],
['уд-повторно', 'О4 Уже удалялось'],
['уд-автор', 'О5 По просьбе автора'],
['уд-под', 'О6 Ненужная подстраница'],
['уд-переим', 'О7 Для переименования'],
['уд-дубль', 'О8 Дубликат'],
['уд-реклама', 'О9 Реклама или спам'],
['уд-нецелевая', 'О10 Нецелевая СО'],
['уд-копивио', 'О11 Нарушение АП']
],
articles: [
['подст:ds', 'ds Отсроченное пусто или коротко', 'С'],
['уд-пусто', 'С1 Пусто или коротко'],
['уд-иностр', 'С2 Не на русском'],
['уд-ссылки', 'С3 Лишь ссылки'],
['уд-нз', 'С5 Явно незначимо'],
['уд-бям', 'С7 Создано нейросетью']
],
redirects: [
['уд-в никуда', 'П1 Перенапр. в никуда'],
['уд-мпр', 'П2 Межпростр. перенапр.'],
['уд-опечатка', 'П3 Перенапр. с ошибкой в названии'],
['уд-падеж', 'П4 Не именительный падеж'],
['уд-смысл', 'П5 Неверное перенапр.'],
['уд-перобс', 'П6 Перенапр. на СО']
],
files: [
['Db-duplicate', 'Ф1 Копия файла'],
['Db-badimage', 'Ф2 Повреждённый файл'],
['подст:No license', 'Ф3.1 Нет данных о лицензии'],
['подст:No source', 'Ф3.2 Нет данных об источнике'],
['подст:No author', 'Ф3.3 Нет данных об авторе'],
['подст:Disputed', 'Ф3.4 Сомнительные данные файла'],
['подст:No permission', 'Ф3.5 Не подтверждена лицензия'],
['подст:Orphaned-fairuse', 'Ф4 Неиспользуемый КДИ'],
['подст:Disputed-fairuse', 'Ф5 Нет КДИ'],
['Db-badfairuse', 'Ф6 Неоправданное КДИ'],
['Замена с Викисклада', 'Ф7 Заменяемый КДИ'],
['Перенесено на Викисклад', 'Ф8 Есть на Складе'],
['подст:Db-nothost', 'Ф9 Файл — ВП:НЕХОСТИНГ']
],
categories: [
['уд-пусткат', 'К1.1 Пустая категория'],
['уд-служебная', 'К1.2 Разобранная служебная кат.'],
['уд-перекат', 'К2 Переименованная кат.']
],
users: [
['уд-владелец', 'У1 По желанию владельца'],
['уд-анон', 'У2 Устаревшая СО анонима'],
['уд-несущ', 'У3 Несуществующий участник'],
['уд-нецелевое', 'У4 Нецелевое использ. ЛП'],
['уд-неактив', 'У5 Подстраница неактивного']
],
special: [
['db', 'Особый случай']
]
},
fastRemoveCriteriaAnchors: {
'подст:ds': 'С1',
deleteslow: 'С1',
ds: 'С1'
},
requiredParamTemplates: {
'уд-переим': 'страницу, которую нужно переименовать',
'уд-дубль': 'страницу-дубликат',
'уд-копивио': 'URL источника нарушения АП',
'Db-duplicate': 'имя файла-оригинала',
'Замена с Викисклада': 'имя файла на Викискладе',
'Перенесено на Викисклад': 'имя файла на Викискладе',
'уд-перекат': 'новое название категории',
'db': '!причину удаления'
},
categoryTemplates: {
discuss: 'Обсуждаемая категория|обсуждаемая категория|Acat|acat|ОКТО|окто|Категория к обсуждению|категория к обсуждению',
rename: 'Категория к переименованию|категория к переименованию|Anacat|anacat',
merge: 'Категория к объединению|категория к объединению|Amergecat|amergecat|Cfm|cfm',
discussed: 'Обсуждавшаяся категория|обсуждавшаяся категория|Обсуждалась|обсуждалась|Обсуждалось|обсуждалось'
},
modalStyles: {
border: '1px solid var(--border-color-progressive, #3366bb)',
background: 'var(--background-color-base, #f8f9fa)',
borderRadius: '6px',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
headerColor: 'var(--color-progressive, #3366bb)'
}
};
config.scriptLink = config.scriptLink || defaults.scriptLink;
config.fastRemoveReasons = $.extend({}, defaults.fastRemoveReasons, config.fastRemoveReasons || {});
config.fastRemoveCriteriaAnchors = $.extend({}, defaults.fastRemoveCriteriaAnchors, config.fastRemoveCriteriaAnchors || {});
config.requiredParamTemplates = $.extend({}, defaults.requiredParamTemplates, config.requiredParamTemplates || {});
config.categoryTemplates = $.extend({}, defaults.categoryTemplates, config.categoryTemplates || {});
config.modalStyles = $.extend({}, defaults.modalStyles, config.modalStyles || {});
return config;
}
function clonePlainObject(obj) {
return JSON.parse(JSON.stringify(obj || {}));
}
function normalizeMenuLabel(value) {
return String(value || '').replace(/\s+/g, ' ').trim().toLowerCase();
}
function readSettingsOptionState(fallback) {
var base = clonePlainObject(fallback || {});
var stored;
var raw = null;
if (!mw.user || !mw.user.options || typeof mw.user.options.get !== 'function') return base;
stored = mw.user.options.get(settingsOptionName);
if (typeof stored === 'string' && stored.trim()) {
try {
raw = JSON.parse(stored);
} catch (e) {
console.warn('RemoverCore: не удалось прочитать сохранённые настройки полностью, будут использованы данные loader.', e);
}
}
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return base;
return $.extend({}, raw, base, ('quickPhrases' in raw) ? { quickPhrases: raw.quickPhrases } : {});
}
function normalizeQuickPhraseValue(value) {
return String(value == null ? '' : value).replace(/\r\n?/g, '\n').trim();
}
function normalizeQuickPhrasesList(list, fallback) {
var source = Array.isArray(list) ? list : fallback;
var normalized = [];
(source || []).forEach(function (value) {
var phrase = normalizeQuickPhraseValue(value);
if (phrase && normalized.indexOf(phrase) === -1) normalized.push(phrase);
});
return normalized;
}
function collectSettingsMenuMeta() {
var labels = [];
var articleLabels = [];
var categoryLabels = [];
var idToLabel = {};
var labelByNorm = {};
var labelOrder = {};
function collect(items, targetLabels) {
items.forEach(function (item) {
var id;
var label;
var normLabel;
if (!item || item.type === 'separator') return;
id = String(item.id || '').trim();
label = String(item.label || '').trim();
normLabel = normalizeMenuLabel(label);
if (id && label && !(id in idToLabel)) idToLabel[id] = label;
if (label && targetLabels.indexOf(label) === -1) targetLabels.push(label);
if (normLabel && !(normLabel in labelByNorm)) {
labelByNorm[normLabel] = label;
labelOrder[label] = labels.length;
labels.push(label);
}
});
}
collect(cfg.articleMenuItems, articleLabels);
collect(cfg.categoryMenuItems, categoryLabels);
return {
labels: labels,
articleLabels: articleLabels,
categoryLabels: categoryLabels,
idToLabel: idToLabel,
labelByNorm: labelByNorm,
labelOrder: labelOrder
};
}
function buildSettingsMenuItemsHint() {
var parts = [];
if (settingsArticleItemLabels.length) {
parts.push('<div class="rmSettingsHintRow"><span class="rmSettingsHintBadge">Статьи</span>' + escapeHtml(settingsArticleItemLabels.join(', ')) + '.</div>');
}
if (settingsCategoryItemLabels.length) {
parts.push('<div class="rmSettingsHintRow"><span class="rmSettingsHintBadge">Категории</span>' + escapeHtml(settingsCategoryItemLabels.join(', ')) + '.</div>');
}
return parts.length ? '<div class="rmSettingsHintList">' + parts.join('') + '</div>' : '';
}
function getDefaultSettings() {
return {
version: settingsVersion,
excludedNamespaces: normalizeNumberList(cfg.excludedNamespaces, []),
notifyAuthor: !!cfg.defaultNotifyAuthor,
subscribeTopic: !!cfg.defaultSubscribeTopic,
autoReloadAfterAction: false,
openNominationDiscussionInNewTab: false,
menuTitle: (typeof cfg.menuTitle === 'string' && cfg.menuTitle.trim()) ? cfg.menuTitle.trim() : 'Remover',
disabledItems: [],
quickPhrases: normalizeQuickPhrasesList(cfg.quickPhrases, []),
showMenuIcons: !!cfg.showMenuIcons,
signatureSeparator: (typeof cfg.signatureSeparator === 'string') ? cfg.signatureSeparator.trim() : ''
};
}
function normalizeNumberList(list, fallback) {
var source = Array.isArray(list) ? list : fallback;
return source
.map(function (value) { return parseInt(value, 10); })
.filter(function (value, index, arr) { return !isNaN(value) && arr.indexOf(value) === index; })
.sort(function (a, b) { return a - b; });
}
function normalizeDisabledItemValue(value) {
var token = String(value || '').trim();
if (!token) return null;
if (settingsItemLabelById[token]) return settingsItemLabelById[token];
return settingsItemLabelByNorm[normalizeMenuLabel(token)] || null;
}
function compareSettingsMenuLabels(a, b) {
var ai = Object.prototype.hasOwnProperty.call(settingsItemLabelOrder, a) ? settingsItemLabelOrder[a] : Number.MAX_SAFE_INTEGER;
var bi = Object.prototype.hasOwnProperty.call(settingsItemLabelOrder, b) ? settingsItemLabelOrder[b] : Number.MAX_SAFE_INTEGER;
if (ai !== bi) return ai - bi;
return a.localeCompare(b, 'ru');
}
function normalizeDisabledItemsList(list, fallback) {
var source = Array.isArray(list) ? list : fallback;
return source
.map(normalizeDisabledItemValue)
.filter(function (value, index, arr) { return value && arr.indexOf(value) === index; })
.sort(compareSettingsMenuLabels);
}
function normalizeMenuTitleSetting(value, fallback) {
var menuTitle = String(value || '').trim();
if (!menuTitle) return fallback;
if (menuTitle === MENU_TITLE_PRESET_CACTIONS || /^(#)?p-cactions$/i.test(menuTitle)) return MENU_TITLE_PRESET_CACTIONS;
if (menuTitle === MENU_TITLE_PRESET_PAGE || /^(#)?p-page$/i.test(menuTitle) || /^(#)?p-actions$/i.test(menuTitle)) return MENU_TITLE_PRESET_PAGE;
if (menuTitle === MENU_TITLE_PRESET_TOOLS || /^(#)?p-tb$/i.test(menuTitle)) return MENU_TITLE_PRESET_TOOLS;
return menuTitle;
}
function normalizeRemoverSettings(raw) {
var defaults = clonePlainObject(settingsDefaults);
var source = (raw && typeof raw === 'object') ? raw : {};
return {
version: settingsVersion,
excludedNamespaces: normalizeNumberList(source.excludedNamespaces, defaults.excludedNamespaces || []),
notifyAuthor: ('notifyAuthor' in source) ? !!source.notifyAuthor : !!defaults.notifyAuthor,
subscribeTopic: ('subscribeTopic' in source) ? !!source.subscribeTopic : !!defaults.subscribeTopic,
autoReloadAfterAction: ('autoReloadAfterAction' in source) ? !!source.autoReloadAfterAction : !!defaults.autoReloadAfterAction,
openNominationDiscussionInNewTab: ('openNominationDiscussionInNewTab' in source) ? !!source.openNominationDiscussionInNewTab : !!defaults.openNominationDiscussionInNewTab,
menuTitle: normalizeMenuTitleSetting(
(typeof source.menuTitle === 'string' && source.menuTitle.trim()) ? source.menuTitle : '',
typeof defaults.menuTitle === 'string' && defaults.menuTitle.trim() ? defaults.menuTitle.trim() : 'Remover'
),
disabledItems: normalizeDisabledItemsList(source.disabledItems, defaults.disabledItems || []),
quickPhrases: normalizeQuickPhrasesList(source.quickPhrases, defaults.quickPhrases || []),
showMenuIcons: ('showMenuIcons' in source) ? !!source.showMenuIcons : !!defaults.showMenuIcons,
signatureSeparator: (typeof source.signatureSeparator === 'string')
? source.signatureSeparator.trim()
: (typeof defaults.signatureSeparator === 'string' ? defaults.signatureSeparator.trim() : '')
};
}
function areRemoverSettingsEqual(a, b) {
return JSON.stringify(normalizeRemoverSettings(a)) === JSON.stringify(normalizeRemoverSettings(b));
}
function updateStoredSettingsState(settings, skipUserOptionsSync) {
var normalized = normalizeRemoverSettings(settings);
state.settings = clonePlainObject(normalized);
setAlert = normalized.notifyAuthor;
setSubscribe = normalized.subscribeTopic;
signatureSeparator = normalized.signatureSeparator;
state.setAlert = setAlert;
state.setSubscribe = setSubscribe;
if (!skipUserOptionsSync && mw.user && mw.user.options && typeof mw.user.options.set === 'function') {
mw.user.options.set(settingsOptionName, JSON.stringify(normalized));
}
return normalized;
}
function splitSettingsInput(value) {
return String(value || '')
.split(/[\s,;]+/)
.map(function (part) { return part.trim(); })
.filter(Boolean);
}
function splitSettingsListInput(value) {
return String(value || '')
.split(/[\n,;]+/)
.map(function (part) { return part.trim(); })
.filter(Boolean);
}
function parseNamespaceInput(value) {
var tokens = splitSettingsInput(value);
var invalid = [];
var values = [];
tokens.forEach(function (token) {
var parsed = parseInt(token, 10);
if (String(parsed) !== token) invalid.push(token);
else if (values.indexOf(parsed) === -1) values.push(parsed);
});
values.sort(function (a, b) { return a - b; });
return { values: values, invalid: invalid };
}
function parseDisabledItemsInput(value) {
var tokens = splitSettingsListInput(value);
var invalid = [];
var values = [];
tokens.forEach(function (token) {
var normalized = normalizeDisabledItemValue(token);
if (!normalized) invalid.push(token);
else if (values.indexOf(normalized) === -1) values.push(normalized);
});
values.sort(compareSettingsMenuLabels);
return { values: values, invalid: invalid };
}
function formatItemsWithAnd(items) {
var list = (items || []).filter(Boolean);
if (!list.length) return '';
if (list.length === 1) return list[0];
return list.slice(0, -1).join(', ') + ' и ' + list[list.length - 1];
}
function formatPagesWithAnd(names, prefix) {
var p = prefix || ':';
var links = (names || []).map(function (n) { return '[[' + p + n + ']]'; });
return formatItemsWithAnd(links);
}
function asNonEmptyArray(value) {
return (Array.isArray(value) ? value : (value ? [value] : [])).filter(Boolean);
}
function buildRenameTemplateParam(targetNames) {
var list = asNonEmptyArray(targetNames);
if (!list.length) return '';
return list[0] + (list.length > 1 ? '||' + list.slice(1).join('|') : '');
}
function collectRenameTargetsFromTemplateParams(params) {
return (params || []).map(function (value) {
return String(value || '').trim();
}).filter(Boolean);
}
function formatRenameItemLabel(pageName, targetName) {
var targets = asNonEmptyArray(targetName);
return '[[:' + pageName + ']]' + (targets.length
? ' → ' + targets.map(function (name) { return '[[:' + name + ']]'; }).join(', ')
: '');
}
function buildRenameItemLabelFormatter(targetsByPage) {
var targets = targetsByPage || {};
return function (pageName) {
return formatRenameItemLabel(pageName, targets[normTitle(pageName)] || '');
};
}
function formatRenameItemsWithAnd(pages, targetsByPage) {
var formatItem = buildRenameItemLabelFormatter(targetsByPage);
var links = (pages || []).map(function (pageName) { return formatItem(pageName); });
return formatItemsWithAnd(links);
}
function normalizeCategoryTargetName(value) {
return normTitle(stripCatPrefix(value)).trim();
}
function normalizeCategoryTargetPageName(value) {
var title = normalizeCategoryTargetName(value);
return title ? normalizeCategoryPageName(title) : '';
}
function buildMultiRenameTargetMap(pairs, key) {
var map = {};
(pairs || []).forEach(function (pair) {
var value;
if (!pair || !pair.pageName) return;
value = pair[key || 'targetName'];
map[normTitle(pair.pageName)] = Array.isArray(value) ? value.slice() : (value || '');
});
return map;
}
function getMultiRenameTarget(job, pageName, key) {
var map = job && job[key || 'multiRenameTargets'];
return map ? (map[normTitle(pageName)] || '') : '';
}
function collectMultiRenamePairs(options) {
var opts = options || {};
var normalizePage = typeof opts.normalizePageName === 'function' ? opts.normalizePageName : normTitle;
var normalizeTarget = typeof opts.normalizeTargetName === 'function' ? opts.normalizeTargetName : normTitle;
var normalizeTemplateTarget = typeof opts.normalizeTemplateTargetName === 'function' ? opts.normalizeTemplateTargetName : normalizeTarget;
var pairs = [];
$('.rmMultiPageBlock').each(function () {
var $block = $(this);
var pageRaw = ($block.find('.rmMultiPageInput').val() || '').trim();
var targetPairs = collectMultiRenameTargetValues($block).map(function (targetRaw) {
return {
targetName: normalizeTarget(targetRaw),
templateTargetName: normalizeTemplateTarget(targetRaw)
};
}).filter(function (item) { return item.targetName || item.templateTargetName; });
var pageName = normalizePage(pageRaw);
var targetNames = targetPairs.map(function (item) { return item.targetName; }).filter(Boolean);
var templateTargetNames = targetPairs.map(function (item) { return item.templateTargetName; }).filter(Boolean);
if (!pageName && !targetNames.length && !templateTargetNames.length) return;
pairs.push({
pageName: pageName,
targetName: targetNames[0] || '',
templateTargetName: templateTargetNames[0] || '',
targetNames: targetNames,
templateTargetNames: templateTargetNames
});
});
return pairs;
}
function validateMultiRenamePairs(pairs, pageLabel, targetLabel) {
var seen = {};
var pages = pairs || [];
var pageWord = pageLabel || 'страницу';
var targetWord = targetLabel || 'новое название';
if (!pages.length) { alert('Укажите ' + pageWord + '.'); return false; }
for (var i = 0; i < pages.length; i++) {
var targetNames = asNonEmptyArray(pages[i].targetNames || pages[i].targetName);
var templateTargetNames = asNonEmptyArray(pages[i].templateTargetNames || pages[i].templateTargetName);
if (!pages[i].pageName) { alert('Укажите ' + pageWord + '.'); return false; }
if (!targetNames.length || !templateTargetNames.length) { alert('Укажите ' + targetWord + ' для «' + pages[i].pageName + '».'); return false; }
if (targetNames.length > 3 || templateTargetNames.length > 3) { alert('Максимум 3 варианта переименования для «' + pages[i].pageName + '».'); return false; }
if (seen[normTitle(pages[i].pageName)]) { alert('Страница «' + pages[i].pageName + '» указана несколько раз.'); return false; }
seen[normTitle(pages[i].pageName)] = true;
}
return true;
}
function getMultiRenameDiscussionOptions(targetsByPage, extraOptions) {
return $.extend({}, extraOptions || {}, {
formatItemLabel: buildRenameItemLabelFormatter(targetsByPage)
});
}
function formatCatLink(name) { return '[[:' + normalizeCategoryPageName(name) + ']]'; }
function formatMergeStatus(status) {
return { already_exists: 'уже был', updated: 'дополнен', created: 'создан' }[status] || status;
}
function applyGeneratedText($el, generated) {
var cur = $el.val();
var prev = $el.data('rmGenerated') || '';
if (!prev || cur.indexOf(prev) === 0) {
$el.val(generated + cur.slice(prev.length));
} else {
$el.val(generated + (cur ? '\n' + cur : ''));
}
$el.data('rmGenerated', generated);
}
function getCurrentQuickPhrases() {
return normalizeQuickPhrasesList(
state.settings && state.settings.quickPhrases,
settingsDefaults.quickPhrases || []
);
}
function insertTextIntoTextarea($el, text) {
var el = $el && $el[0];
var value;
var start;
var end;
var updatedValue;
var caretPos;
if (!el) return;
text = String(text || '');
if (!text) return;
value = $el.val() || '';
start = typeof el.selectionStart === 'number' ? el.selectionStart : value.length;
end = typeof el.selectionEnd === 'number' ? el.selectionEnd : start;
updatedValue = value.slice(0, start) + text + value.slice(end);
caretPos = start + text.length;
$el.val(updatedValue).trigger('input').trigger('change').focus();
if (typeof el.setSelectionRange === 'function') el.setSelectionRange(caretPos, caretPos);
}
function buildQuickPhrasesPanelHtml(textareaId) {
var phrases = getCurrentQuickPhrases();
if (!phrases.length) return '';
return joinHtml([
'<div class="rmQuickPhrasesPanel ', RESIZE_CLASS, '" data-rm-target="', textareaId, '">',
phrases.map(function (phrase) {
return joinHtml([
'<button type="button" class="rmQuickPhraseActionBtn" data-rm-target="', textareaId,
'" data-rm-phrase="', escapeHtml(phrase), '">',
escapeHtml(phrase),
'</button>'
]);
}).join(''),
'</div>'
]);
}
function getMultiNominationCommentText(commentsByPage, pageTitle) {
var key = normTitle(pageTitle);
if (!commentsByPage || !Object.prototype.hasOwnProperty.call(commentsByPage, key)) return '';
return normalizeQuickPhraseValue(commentsByPage[key]);
}
function hasCommentsForEveryMultiNominationPage(pages, commentsByPage) {
var list = Array.isArray(pages) ? pages.filter(Boolean) : [];
return list.length > 0 && list.every(function (pageName) {
return !!getMultiNominationCommentText(commentsByPage, pageName);
});
}
function validateMultiNominationText(pages, bodyText, commentsByPage, itemGenitive) {
if (normalizeQuickPhraseValue(bodyText) || hasCommentsForEveryMultiNominationPage(pages, commentsByPage)) return true;
alert('Укажите общий текст номинации или комментарий для каждой ' + (itemGenitive || 'страницы') + '.');
return false;
}
function buildMultiNominationText(pages, bodyText, commentsByPage, options) {
var opts = options || {};
var list = Array.isArray(pages) ? pages.filter(Boolean) : [];
var body = normalizeQuickPhraseValue(bodyText);
var hasAllPageComments = hasCommentsForEveryMultiNominationPage(list, commentsByPage);
var headingLevel = Math.max(2, parseInt(opts.headingLevel, 10) || 3);
var headingMarks = new Array(headingLevel + 1).join('=');
var formatItemLabel = typeof opts.formatItemLabel === 'function'
? opts.formatItemLabel
: function (pageName) { return '[[:' + pageName + ']]'; };
var pageSections = list.map(function (pageName, index) {
var comment = getMultiNominationCommentText(commentsByPage, pageName);
var sectionPrefix = (index === 0 && opts.leadingBlankLine === false) ? '' : '\n';
return sectionPrefix + headingMarks + ' ' + formatItemLabel(pageName) + ' ' + headingMarks + '\n' + (comment ? appendNominationSignature(comment) + '\n' : '');
}).join('');
var commonSectionText = body
? appendNominationSignature(body)
: (hasAllPageComments ? '' : appendNominationSignature(''));
return pageSections + (commonSectionText ? '\n' + headingMarks + ' По всем ' + headingMarks + '\n' + commonSectionText : '');
}
function buildMultiNominationListText(pages, bodyText, commentsByPage, options) {
var opts = options || {};
var list = Array.isArray(pages) ? pages.filter(Boolean) : [];
var body = normalizeQuickPhraseValue(bodyText);
var hasAllPageComments = hasCommentsForEveryMultiNominationPage(list, commentsByPage);
var formatItemLabel = typeof opts.formatItemLabel === 'function'
? opts.formatItemLabel
: function (pageName) { return '[[:' + pageName + ']]'; };
var pageLines = list.map(function (pageName) {
var comment = getMultiNominationCommentText(commentsByPage, pageName);
return '* ' + formatItemLabel(pageName) + (comment ? '\n' + appendNominationSignature(comment).split('\n').map(function (line) { return '*: ' + line; }).join('\n') : '');
}).join('\n');
var commonText = body
? appendNominationSignature(body)
: (hasAllPageComments ? '' : appendNominationSignature(''));
return pageLines + (pageLines && commonText ? '\n' : '') + commonText;
}
function collectMultiNominationComments(normalizePageName) {
var comments = {};
var normalize = typeof normalizePageName === 'function' ? normalizePageName : normTitle;
$('.rmMultiPageBlock').each(function () {
var $block = $(this);
var pageName = normalize(($block.find('.rmMultiPageInput').val() || '').trim());
var comment = normalizeQuickPhraseValue($block.find('.rmMultiPageCommentInput').val());
if (!pageName) return;
comments[pageName] = comment;
});
return comments;
}
function getNominationPublishText(job) {
if (job && job.isMulti) return String(job.msg || '');
return appendNominationSignature(job && job.msg);
}
function getNominationConflictRule(job) {
if (!job || job.mode !== 'nominate') return null;
if (job.opId === 'tRm' || job.opId === 'mRm') {
return {
id: 'ku',
label: 'КУ',
namePattern: '(?:к\\s*удалению|ку)',
detect: function (articleText) {
var match = String(articleText || '').match(/\{\{\s*((?:к\s*удалению|ку))\s*(?:\||\}\})/i);
if (!match) return null;
var templateName = ucfirst(String(match[1] || '').replace(/\s+/g, ' ').trim());
return {
label: 'КУ',
templateName: templateName || 'КУ',
templateDisplay: '{{' + (templateName || 'КУ') + '}}'
};
}
};
}
if (job.opId === 'rnm' || job.opId === 'mRnm') {
return {
id: 'kpm',
label: 'КПМ',
namePattern: '(?:к\\s*переименованию|кпм|rename)',
detect: function (articleText) {
var match = String(articleText || '').match(/\{\{\s*((?:к\s*переименованию|кпм|rename))\s*(?:\||\}\})/i);
if (!match) return null;
var templateName = ucfirst(String(match[1] || '').replace(/\s+/g, ' ').trim());
return {
label: 'КПМ',
templateName: templateName || 'КПМ',
templateDisplay: '{{' + (templateName || 'КПМ') + '}}'
};
}
};
}
return null;
}
function detectNominationConflict(articleText, job) {
var rule = getNominationConflictRule(job);
if (!rule || typeof rule.detect !== 'function') return null;
return rule.detect(articleText);
}
function getConflictDecisionForPage(job, pageName) {
var decisions = job && job.conflictDecisions;
var key = normTitle(pageName);
return decisions && decisions[key] ? decisions[key] : null;
}
function getCategoryMergeRe() {
return new RegExp('\\{\\{\\s*(?:' + cfg.categoryTemplates.merge + ')\\s*\\|\\s*([^\\}]+)\\}\\}', 'i');
}
function eachSequential(targets, iteratee, done) {
var i = 0;
(function next(err) {
if (err || i >= targets.length) { done(err || null); return; }
iteratee(targets[i++], next);
}(null));
}
function normalizeSectionForLink(sectionTitle) {
return (sectionTitle || '').trim()
.replace(/\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g, function (_, target, label) {
var v = (label || target || '').trim();
return v.charAt(0) === ':' ? v.slice(1) : v;
})
.replace(/''+/g, '').replace(/\s+/g, ' ').trim();
}
function getViewportWidth() {
return Math.floor(Math.max(
(document.documentElement && document.documentElement.clientWidth) || 0,
(typeof window.innerWidth === 'number' && window.innerWidth) || 0,
$(window).width() || 0
));
}
function getVisualViewportWidth() {
var widths = [];
if (window.visualViewport && typeof window.visualViewport.width === 'number' && window.visualViewport.width > 0) widths.push(window.visualViewport.width);
if (window.screen && window.screen.width > 0) widths.push(window.screen.width);
return widths.length ? Math.floor(Math.min.apply(Math, widths)) : getViewportWidth();
}
function isTouchModalDevice() {
return !!(
(window.matchMedia && window.matchMedia('(pointer: coarse)').matches) ||
('ontouchstart' in window) ||
(navigator.maxTouchPoints && navigator.maxTouchPoints > 0) ||
(navigator.msMaxTouchPoints && navigator.msMaxTouchPoints > 0)
);
}
function getModalLayout() {
var minWidth = parseInt(sz.taMinW, 10) || 180;
var layoutWidth = getViewportWidth();
var visualWidth = getVisualViewportWidth();
var contentWidth = Math.max(minWidth, Math.floor($('#content').width() || $('#content').innerWidth() || $(window).width() || minWidth));
var isMobile = layoutWidth <= sz.mobileBp;
var isTouchDesktop = !isMobile &&
isTouchModalDevice() &&
visualWidth > 0 &&
visualWidth <= sz.mobileBp &&
layoutWidth >= visualWidth + sz.touchDesktopGap;
var useFullWidth = isMobile || isTouchDesktop;
var maxOuterWidth;
var defaultOuterWidth;
var desktopWidth;
if (isMobile) maxOuterWidth = Math.max(minWidth, (visualWidth || contentWidth) - sz.viewportGap);
else if (isTouchDesktop) maxOuterWidth = Math.max(minWidth, contentWidth - 32);
else maxOuterWidth = Math.max(minWidth, Math.min(contentWidth, (visualWidth ? visualWidth - sz.viewportGap : contentWidth)));
desktopWidth = Math.max(minWidth, Math.floor(contentWidth * sz.modalRatio));
if (useFullWidth || desktopWidth < sz.modalMinWide) defaultOuterWidth = contentWidth;
else if (desktopWidth < sz.modalDefaultWide) defaultOuterWidth = sz.modalDefaultWide;
else defaultOuterWidth = desktopWidth;
return {
minWidth: minWidth,
isMobile: isMobile,
isTouchDesktop: isTouchDesktop,
useFullWidth: useFullWidth,
shouldCenter: useFullWidth || mwCfg.skin === 'minerva',
maxOuterWidth: maxOuterWidth,
defaultOuterWidth: Math.min(defaultOuterWidth, maxOuterWidth)
};
}
function getDefaultResizableWidth(frameWidth) {
var layout = getModalLayout();
return Math.max(layout.minWidth, layout.defaultOuterWidth - Math.floor(frameWidth || 0));
}
function getBoxFrameWidth($el) {
function px(prop) {
var n = parseFloat($el.css(prop));
return isNaN(n) ? 0 : n;
}
return px('padding-left') + px('padding-right') + px('border-left-width') + px('border-right-width');
}
// ═══════════════════════════════════════════════════════════════════════════
// API
// ═══════════════════════════════════════════════════════════════════════════
function getApiUrl() {
return (mw.util && typeof mw.util.wikiScript === 'function') ? mw.util.wikiScript('api') : '/w/api.php';
}
function getCsrfTokenValue() {
return (mw.user && mw.user.tokens && typeof mw.user.tokens.get === 'function')
? mw.user.tokens.get('csrfToken')
: null;
}
function storeCsrfToken(token) {
if (!token || !mw.user || !mw.user.tokens || typeof mw.user.tokens.set !== 'function') return;
mw.user.tokens.set({ csrfToken: token });
}
function isValidCsrfToken(token) {
return typeof token === 'string' && !!token && token !== '+\\';
}
function fetchCsrfToken(forceRefresh, callback) {
var cachedToken = getCsrfTokenValue();
if (!forceRefresh && isValidCsrfToken(cachedToken)) {
callback(cachedToken);
return;
}
$.ajax({
url: getApiUrl(),
method: 'GET',
dataType: 'json',
data: { action: 'query', meta: 'tokens', type: 'csrf', format: 'json' }
})
.done(function (data) {
var token = data && data.query && data.query.tokens && data.query.tokens.csrftoken;
if (isValidCsrfToken(token)) {
storeCsrfToken(token);
callback(token);
return;
}
callback(null);
})
.fail(function () {
callback(null);
});
}
function apiReq(params, mode, callback) {
var isWrite = mode === 'edit' || mode === 'discussiontoolssubscribe' || mode === 'options';
function sendRequest(retryWithFreshToken) {
var reqParams = $.extend({}, params, { format: 'json', action: mode });
if (!isWrite) {
$.ajax({ url: getApiUrl(), method: 'GET', data: reqParams, dataType: 'json' })
.done(function (data) { if (callback) callback(data); })
.fail(function (jqXHR, status) {
console.error('Ошибка API: ' + status);
if (callback) callback({ error: { code: 'network', info: status } });
});
return;
}
fetchCsrfToken(!!retryWithFreshToken, function (token) {
if (!isValidCsrfToken(token)) {
if (callback) callback({ error: { code: 'badtoken', info: 'Не удалось получить CSRF-токен.' } });
return;
}
reqParams.token = token;
$.ajax({ url: getApiUrl(), method: 'POST', data: reqParams, dataType: 'json' })
.done(function (data) {
var err = data && data.error;
var isBadToken = err && (err.code === 'badtoken' || /invalid csrf token/i.test(String(err.info || '')));
if (isBadToken && !retryWithFreshToken) {
sendRequest(true);
return;
}
if (callback) callback(data);
})
.fail(function (jqXHR, status) {
console.error('Ошибка API: ' + status);
if (callback) callback({ error: { code: 'network', info: status } });
});
});
}
sendRequest(false);
}
function saveSettingsToServer(settings, callback) {
var normalized = normalizeRemoverSettings(settings);
if (!mwCfg.wgUserName) {
callback({ code: 'notloggedin', info: 'Сохранять настройки можно только после входа в учётную запись.' });
return;
}
apiReq({ optionname: settingsOptionName, optionvalue: JSON.stringify(normalized) }, 'options', function (resp) {
if (resp && resp.options === 'success') {
callback(null, updateStoredSettingsState(normalized));
return;
}
callback((resp && resp.error) || { code: 'options_failed', info: 'Не удалось сохранить настройки.' });
});
}
function resetSettingsOnServer(callback) {
if (!mwCfg.wgUserName) {
callback({ code: 'notloggedin', info: 'Сбрасывать настройки можно только после входа в учётную запись.' });
return;
}
apiReq({ change: settingsOptionName }, 'options', function (resp) {
if (resp && resp.options === 'success') {
callback(null, updateStoredSettingsState(settingsDefaults, true));
return;
}
callback((resp && resp.error) || { code: 'options_failed', info: 'Не удалось сбросить настройки.' });
});
}
function getFirstQueryPage(data) {
var pages = data && data.query && data.query.pages;
if (!pages) return null;
return pages[Object.keys(pages)[0]] || null;
}
function extractRevisionContent(rev) {
if (!rev) return null;
if (typeof rev['*'] === 'string') return rev['*'];
if (rev.slots && rev.slots.main) {
if (typeof rev.slots.main['*'] === 'string') return rev.slots.main['*'];
if (typeof rev.slots.main.content === 'string') return rev.slots.main.content;
}
return null;
}
function makeReadError(apiError, fallbackCode, fallbackInfo) {
var err = apiError || {};
return {
code: err.code || fallbackCode || 'read_failed',
info: err.info || fallbackInfo || 'Не удалось получить содержимое.'
};
}
function getTextWithTimestamp(pageName, callback) {
apiReq({ prop: 'revisions', rvprop: 'content|timestamp', rvslots: 'main', titles: pageName }, 'query', function (data) {
var content;
var page;
if (data && data.error) {
callback(null, null, data.error);
return;
}
if (!data || !data.query || !data.query.pages) {
callback(null, null, { code: 'read_failed', info: 'Некорректный ответ API при чтении страницы.' });
return;
}
page = getFirstQueryPage(data);
if (!page) {
callback(null, null, { code: 'read_failed', info: 'API не вернул данные страницы.' });
return;
}
if (page.invalid !== undefined) {
callback(null, null, { code: 'invalidtitle', info: page.invalidreason || 'Некорректное название страницы.' });
return;
}
if (page.missing !== undefined) {
callback(null, null, null);
return;
}
if (!page.revisions || !page.revisions.length) {
callback(null, null, { code: 'read_failed', info: 'API не вернул ревизии страницы.' });
return;
}
content = extractRevisionContent(page.revisions[0]);
if (content === null) {
callback(null, null, { code: 'content_missing', info: 'API не вернул текст страницы.' });
return;
}
callback(content, page.revisions[0].timestamp || null, null);
});
}
function getText(pageName, callback) {
getTextWithTimestamp(pageName, function (text, baseTimestamp, err) { callback(text, err); });
}
function editPageContent(pageTitle, options, buildFn, callback) {
var opts = options || {};
var maxRetries = Math.max(0, parseInt(opts.editConflictRetries, 10) || 1);
(function attempt(retry) {
getTextWithTimestamp(pageTitle, function (sourceText, baseTimestamp, readErr) {
if (readErr) {
callback(makeReadError(readErr, opts.readErrorCode || 'read_failed', 'Не удалось получить содержимое страницы «' + pageTitle + '».'));
return;
}
if (sourceText === null) {
callback({ code: opts.readErrorCode || 'read_failed', info: opts.readError || 'Страница «' + pageTitle + '» не существует.' });
return;
}
var done = (function () {
var called = false;
return function (result) {
if (called) return;
called = true;
if (!result || result.error) { callback((result && result.error) || { code: 'build_failed', info: 'Не удалось подготовить правку.' }, result && result.meta || null); return; }
if (result.skip) { callback(null, result.meta || null); return; }
if (typeof result.text !== 'string') { callback({ code: 'build_failed', info: 'Не удалось подготовить правку.' }); return; }
var ep = { title: pageTitle, text: result.text, summary: result.summary || opts.summary || '' };
if (opts.watchlist) ep.watchlist = opts.watchlist;
if (opts.assertuser) ep.assertuser = opts.assertuser;
if (opts.createonly) ep.createonly = opts.createonly;
if (opts.useBaseTimestamp !== false && baseTimestamp) ep.basetimestamp = baseTimestamp;
apiReq(ep, 'edit', function (resp) {
var err = resp && resp.error ? resp.error : null;
if (err && err.code === 'editconflict' && retry < maxRetries) { attempt(retry + 1); return; }
callback(err, result.meta || null);
});
};
}());
var maybe = buildFn(sourceText, done);
if (maybe !== undefined) done(maybe);
});
}(0));
}
// ═══════════════════════════════════════════════════════════════════════════
// ШАБЛОНЫ: удаление и вставка
// ═══════════════════════════════════════════════════════════════════════════
function findBalancedTemplateEnd(text, start) {
var depth = 0;
var i = start;
var len = text.length;
while (i < len - 1) {
if (text.charAt(i) === '{' && text.charAt(i + 1) === '{') {
depth++;
i += 2;
continue;
}
if (text.charAt(i) === '}' && text.charAt(i + 1) === '}') {
depth--;
i += 2;
if (depth === 0) return i;
continue;
}
i++;
}
return -1;
}
function splitTemplateTopLevelParts(innerText) {
var parts = [];
var start = 0;
var templateDepth = 0;
var linkDepth = 0;
var i = 0;
var text = String(innerText || '');
while (i < text.length) {
if (text.charAt(i) === '{' && text.charAt(i + 1) === '{') {
templateDepth++;
i += 2;
continue;
}
if (text.charAt(i) === '}' && text.charAt(i + 1) === '}' && templateDepth > 0) {
templateDepth--;
i += 2;
continue;
}
if (text.charAt(i) === '[' && text.charAt(i + 1) === '[') {
linkDepth++;
i += 2;
continue;
}
if (text.charAt(i) === ']' && text.charAt(i + 1) === ']' && linkDepth > 0) {
linkDepth--;
i += 2;
continue;
}
if (text.charAt(i) === '|' && templateDepth === 0 && linkDepth === 0) {
parts.push(text.slice(start, i));
start = i + 1;
}
i++;
}
parts.push(text.slice(start));
return parts;
}
function getTemplateMatchAt(text, start, nameRe) {
var end = findBalancedTemplateEnd(text, start);
var parts;
var rawName;
var name;
if (end < 0) return null;
parts = splitTemplateTopLevelParts(text.slice(start + 2, end - 2));
rawName = String(parts.shift() || '').trim();
name = rawName.replace(/^(?:safe)?(?:subst|подст)\s*:\s*/i, '').replace(RE_TEMPLATE_NS, '').replace(/_/g, ' ').replace(/\s+/g, ' ').trim();
if (!nameRe.test(name)) return null;
return {
start: start,
end: end,
text: text.slice(start, end),
name: rawName,
params: parts.map(function (part) { return part.trim(); })
};
}
function findTemplateByPattern(text, namePattern) {
var source = String(text || '');
var nameRe = new RegExp('^(?:' + namePattern + ')$', 'i');
var i = 0;
var end;
var match;
while (i < source.length - 1) {
if (source.charAt(i) === '{' && source.charAt(i + 1) === '{') {
match = getTemplateMatchAt(source, i, nameRe);
if (match) return match;
end = findBalancedTemplateEnd(source, i);
if (end > i) { i = end; continue; }
}
i++;
}
return null;
}
function hasTemplateWithDateByPattern(text, namePattern, dateIso) {
var source = String(text || '');
var nameRe = new RegExp('^(?:' + namePattern + ')$', 'i');
var targetDate = convertToStandardDate(dateIso);
var i = 0;
var end;
var match;
var templateDate;
if (!targetDate) return false;
while (i < source.length - 1) {
if (source.charAt(i) === '{' && source.charAt(i + 1) === '{') {
match = getTemplateMatchAt(source, i, nameRe);
if (match) {
templateDate = convertToStandardDate(String(match.params[0] || '').replace(/^\s*1\s*=\s*/, ''));
if (templateDate === targetDate) return true;
i = match.end;
continue;
}
end = findBalancedTemplateEnd(source, i);
if (end > i) { i = end; continue; }
}
i++;
}
return false;
}
function getTemplateRemovalRange(text, match) {
var start = match.start;
var end = match.end;
var before = text.slice(0, start).match(/<noinclude>\s*$/i);
var after;
if (before) {
after = text.slice(end).match(/^\s*<\/noinclude>\s*\n?/i);
if (after) {
return { start: before.index, end: end + after[0].length };
}
}
after = text.slice(end).match(/^[ \t]*(?:\r?\n)?/);
if (after) end += after[0].length;
return { start: start, end: end };
}
function stripTemplatesByPattern(text, namePattern) {
var source = String(text || '');
var nameRe = new RegExp('^(?:' + namePattern + ')$', 'i');
var ranges = [];
var out = [];
var pos = 0;
var i = 0;
var end;
var match;
var range;
while (i < source.length - 1) {
if (source.charAt(i) === '{' && source.charAt(i + 1) === '{') {
match = getTemplateMatchAt(source, i, nameRe);
if (match) {
range = getTemplateRemovalRange(source, match);
ranges.push(range);
i = Math.max(match.end, range.end);
continue;
}
end = findBalancedTemplateEnd(source, i);
if (end > i) { i = end; continue; }
}
i++;
}
if (!ranges.length) return { text: source, removed: false };
ranges.forEach(function (r) {
if (r.start < pos) r.start = pos;
out.push(source.slice(pos, r.start));
pos = r.end;
});
out.push(source.slice(pos));
return { text: out.join(''), removed: true };
}
function removeTemplatesByAliases(text, aliases) {
var pattern = buildTemplateNamePattern(aliases);
if (!pattern) return { text: text, removed: false };
return stripTemplatesByPattern(text, '(?:' + pattern + ')');
}
function normalizeAliasKey(title) {
return String(title || '').replace(/_/g, ' ').toLowerCase().trim();
}
function dedupeAliases(aliases) {
var seen = {};
var result = [];
(aliases || []).forEach(function (alias) {
var key = normalizeAliasKey(alias);
if (!key || seen[key]) return;
seen[key] = true;
result.push(alias);
});
return result;
}
function resolveTemplateRedirectAliases(titles, callback) {
var list = dedupeAliases(titles);
var missingTitles = list.filter(function (t) { return !tplAliasCache[t]; });
function collectAliases() {
var allAliases = [];
list.forEach(function (title) { allAliases = allAliases.concat(tplAliasCache[title] || [title]); });
callback(dedupeAliases(allAliases));
}
if (!missingTitles.length) { collectAliases(); return; }
(function resolveChunk(offset) {
if (offset >= missingTitles.length) { collectAliases(); return; }
var chunk = missingTitles.slice(offset, offset + 20);
var chunkByKey = {};
chunk.forEach(function (t) { chunkByKey[normalizeAliasKey(t)] = t; });
apiReq({ prop: 'redirects', rdlimit: 'max', titles: chunk.join('|') }, 'query', function (resp) {
var pages = resp && resp.query && resp.query.pages ? resp.query.pages : {};
Object.keys(pages).forEach(function (pid) {
var p = pages[pid];
var sourceTitle;
var found;
if (!p || !p.title) return;
sourceTitle = chunkByKey[normalizeAliasKey(p.title)];
if (!sourceTitle) return;
found = [p.title].concat((p.redirects || []).map(function (r) { return r.title; }));
tplAliasCache[sourceTitle] = dedupeAliases(found);
});
chunk.forEach(function (t) { if (!tplAliasCache[t]) tplAliasCache[t] = [t]; });
resolveChunk(offset + 20);
});
}(0));
}
function removeTransferTemplatesLocal(articleText, transferMode) {
var result = { text: articleText, removedKbu: false, removedKul: false, removedHangon: false };
if (transferMode === 'none') return result;
if (transferMode === 'kbu' || transferMode === 'both') {
var kbu = stripTemplatesByPattern(result.text, '(?:' + KBU_PATTERN_STR + ')');
result.text = kbu.text; result.removedKbu = kbu.removed;
}
if (transferMode === 'kul' || transferMode === 'both') {
var kul = stripTemplatesByPattern(result.text, KUL_PATTERN_STR);
result.text = kul.text; result.removedKul = kul.removed;
}
var hangon = stripTemplatesByPattern(result.text, HANGON_PATTERN_STR);
result.text = hangon.text; result.removedHangon = hangon.removed;
return result;
}
function removeTransferTemplatesWithApiFallback(pageName, articleText, transferMode, localResult, callback) {
var needKbu = (transferMode === 'kbu' || transferMode === 'both') && !localResult.removedKbu;
var needKul = (transferMode === 'kul' || transferMode === 'both') && !localResult.removedKul;
var needHangon = transferMode !== 'none' && !localResult.removedHangon;
if (!needKbu && !needKul && !needHangon) { callback(localResult); return; }
var titleMap = {};
if (needKbu) ['Шаблон:К быстрому удалению','Шаблон:К отсроченному удалению','Шаблон:Deleteslow','Шаблон:Ds'].forEach(function (t) { titleMap[t] = true; });
if (needKul) titleMap['Шаблон:К улучшению'] = true;
if (needHangon) { titleMap['Шаблон:Hangon'] = true; titleMap['Шаблон:Hang-on'] = true; }
apiReq({ prop: 'templates', titles: pageName, tllimit: 'max' }, 'query', function (data) {
var page = getFirstQueryPage(data);
if (page && page.templates) {
page.templates.forEach(function (tpl) {
var norm = normalizeTemplateName(tpl.title);
if ((needKbu && (RE_KBU_PATTERNS.test(norm) || norm === 'к быстрому удалению' || norm === 'к отсроченному удалению' || norm === 'deleteslow' || norm === 'ds')) ||
(needKul && RE_KUL_PATTERN.test(norm)) ||
(needHangon && RE_HANGON.test(norm))) {
titleMap[tpl.title] = true;
}
});
}
var titles = Object.keys(titleMap);
if (!titles.length) { callback(localResult); return; }
resolveTemplateRedirectAliases(titles, function (aliases) {
var updated = $.extend({}, localResult);
var r = removeTemplatesByAliases(updated.text, aliases);
updated.text = r.text;
if (r.removed) {
if (needKbu) updated.removedKbu = true;
if (needKul) updated.removedKul = true;
if (needHangon) updated.removedHangon = true;
}
callback(updated);
});
});
}
function removeMoveToCommonsTemplatesLocal(articleText) {
var r = removeTemplatesByAliases(articleText, MOVE_TO_COMMONS_TEMPLATE_ALIASES);
return { text: r.text, removedMoveToCommons: r.removed };
}
function removeMoveToCommonsTemplatesWithApiFallback(pageName, articleText, localResult, callback) {
apiReq({ prop: 'templates', titles: pageName, tllimit: 'max' }, 'query', function (data) {
var page = getFirstQueryPage(data);
var titleMap = {};
var normTarget = normalizeTemplateName(MOVE_TO_COMMONS_TEMPLATE_TITLE);
var titles;
titleMap[MOVE_TO_COMMONS_TEMPLATE_TITLE] = true;
if (page && page.templates) {
page.templates.forEach(function (tpl) {
var norm = normalizeTemplateName(tpl.title);
if (norm === normTarget || MOVE_TO_COMMONS_TEMPLATE_ALIASES.some(function (alias) {
return norm === normalizeTemplateName(alias);
})) {
titleMap[tpl.title] = true;
}
});
}
titles = Object.keys(titleMap);
resolveTemplateRedirectAliases(titles, function (resolvedAliases) {
var aliases = dedupeAliases(MOVE_TO_COMMONS_TEMPLATE_ALIASES.concat(resolvedAliases));
var updated = $.extend({}, localResult);
var r = removeTemplatesByAliases(articleText, aliases);
updated.text = r.text;
updated.removedMoveToCommons = r.removed;
callback(updated);
});
});
}
// ─── Вставка шаблонов ────────────────────────────────────────────────────
function findInsertPositionAfterProjectTemplates(text) {
var pos = 0, len = text.length;
while (pos < len) {
var wsMatch = text.slice(pos).match(/^[\t ]*\n/);
if (wsMatch) { pos += wsMatch[0].length; continue; }
if (text.charAt(pos) !== '{' || text.charAt(pos + 1) !== '{') break;
var afterOpen = text.slice(pos + 2);
var nameMatch = afterOpen.match(/^[\s]*([\s\S]*?)[\s]*(?:\||\}\})/);
var templateEnd;
if (!nameMatch) break;
var tplName = nameMatch[1].toLowerCase().replace(/\s+/g, ' ').trim();
if (!/^статья проекта(\s|$)/.test(tplName) && tplName !== 'блок проектов статьи') break;
templateEnd = findBalancedTemplateEnd(text, pos);
if (templateEnd < 0) break;
pos = templateEnd;
if (pos < len && text.charAt(pos) === '\n') pos++;
}
return pos;
}
function insertTplOnTalkPage(text, tplText, sep) {
var s = (sep === undefined) ? '\n' : sep;
var insertPos = findInsertPositionAfterProjectTemplates(text);
if (insertPos === 0) return tplText + (text.length ? s + text.replace(/^\n+/, '') : '');
var before = text.slice(0, insertPos).replace(/\n+$/, '');
var after = text.slice(insertPos).replace(/^\n+/, '');
return before + '\n' + tplText + (after.length ? s + after : '');
}
function wrapInNoinclude(text, templateText) {
var match = text.match(RE_NOINCLUDE);
if (match) {
// Если перед noinclude есть непробельный контент — вставляем новый noinclude сверху
var before = text.slice(0, text.indexOf(match[0]));
if (/\S/.test(before)) {
return '<noinclude>' + templateText + '</noinclude>\n' + text;
}
var content = match[2];
if (content.length > 0 && content.charAt(content.length - 1) !== '\n') content += '\n';
return text.replace(match[0], match[1] + '<noinclude>' + content + templateText + '\n</noinclude>');
}
return '<noinclude>' + templateText + '</noinclude>\n' + text;
}
function getDateSectionTalkTemplateConfig(closeType) {
var config = DATE_SECTION_TALK_TEMPLATES[closeType];
return typeof config === 'string' ? { name: config } : (config || null);
}
function upsertDateSectionTemplateOnTalkPage(text, templateName, dateIso, sectionTitle, options) {
var source = text || '';
var normalizedSection = String(sectionTitle || '').trim();
var opts = options || {};
var tplRe = buildTemplateTransclusionRegex(templateName, opts.aliases);
var tplMatch = tplRe ? source.match(tplRe) : null;
function buildSectionParam(sectionIndex, currentParams) {
var paramName;
var paramsText = String(currentParams || '');
if (!normalizedSection) return '';
if (opts.singleSectionParam) {
paramName = opts.sectionParam || 'раздел обсуждения';
if (new RegExp('(?:^|\\|)\\s*(?:' + escapeRegExp(paramName) + '|l1)\\s*=', 'i').test(paramsText)) return '';
return '|' + paramName + '=' + normalizedSection;
}
return '|l' + sectionIndex + '=' + normalizedSection;
}
function buildExistingTplWithCanonicalName(suffix) {
return T_OPEN + templateName + String(tplMatch[2] || '').replace(/\s+$/, '') + (suffix || '') + T_CLOSE;
}
if (!tplMatch) {
return { text: insertTplOnTalkPage(source, T_OPEN + templateName + '|' + dateIso + buildSectionParam(1, '') + T_CLOSE, '\n'), status: 'created' };
}
var parts = tplMatch[2].split('|').map(function (p) { return p.trim(); }).filter(Boolean);
var existingDates = parts.filter(function (p) { return p.indexOf('=') === -1; }).map(function (p) { return convertToStandardDate(p); });
var stdDate = convertToStandardDate(dateIso);
if (existingDates.indexOf(stdDate) !== -1) {
var canonicalTpl = buildExistingTplWithCanonicalName('');
if (canonicalTpl !== tplMatch[0]) {
return { text: source.replace(tplMatch[0], canonicalTpl), status: 'updated' };
}
return { text: source, status: 'already_present' };
}
var nextIdx = existingDates.length + 1;
var suffix = '|' + dateIso + buildSectionParam(nextIdx, tplMatch[2]);
return {
text: source.replace(tplMatch[0], buildExistingTplWithCanonicalName(suffix)),
status: 'updated'
};
}
function getNamedTemplateParam(params, names) {
var wanted = {};
names.forEach(function (name) { wanted[String(name).toLowerCase()] = true; });
for (var i = 0; i < params.length; i++) {
var m = String(params[i] || '').match(/^\s*([^=]+?)\s*=\s*([\s\S]*)$/);
if (m && wanted[m[1].toLowerCase().trim()]) return m[2].trim();
}
return '';
}
function getSingleResultTemplateDate(paramsText, templateName) {
var params = String(paramsText || '').split('|').map(function (p) { return p.trim(); }).filter(Boolean);
var namedDate = getNamedTemplateParam(params, templateName === 'Переименовано' ? ['date', 'дата'] : ['date']);
var positionalDate = params.filter(function (p) { return p.indexOf('=') === -1; })[0] || '';
return convertToStandardDate(namedDate || positionalDate);
}
function upsertSingleDateResultTemplateOnTalkPage(text, templateName, dateIso, paramsText) {
var source = text || '';
var tplRe = buildTemplateTransclusionRegex(templateName, null, 'ig');
var tplMatch = null;
var match;
var newTpl = T_OPEN + templateName + '|' + dateIso + (paramsText ? '|' + paramsText : '') + T_CLOSE;
var stdDate = convertToStandardDate(dateIso);
var existingDate;
var canonicalTpl;
if (tplRe) {
while ((match = tplRe.exec(source)) !== null) {
existingDate = getSingleResultTemplateDate(match[2], templateName);
if (existingDate === stdDate) { tplMatch = match; break; }
if (match[0].length === 0) tplRe.lastIndex++;
}
}
if (!tplMatch) {
return { text: insertTplOnTalkPage(source, newTpl, '\n'), status: 'created' };
}
canonicalTpl = T_OPEN + templateName + String(tplMatch[2] || '').replace(/\s+$/, '') + T_CLOSE;
if (canonicalTpl !== tplMatch[0]) {
return { text: source.replace(tplMatch[0], canonicalTpl), status: 'updated' };
}
return { text: source, status: 'already_present' };
}
function buildConditionalRetTemplateText(dateIso, sectionTitle, reasonText, deadlineText, sectionIndex) {
var normalizedSection = String(sectionTitle || '').trim();
var normalizedReason = normalizeQuickPhraseValue(reasonText);
var normalizedDeadline = String(deadlineText || '').trim();
var index = parseInt(sectionIndex, 10);
var tpl = 'Условно оставлено|' + dateIso;
if (isNaN(index) || index < 1) index = 1;
if (normalizedSection) tpl += '|l' + index + '=' + normalizedSection;
if (normalizedReason) tpl += '|пояснение=' + normalizedReason;
if (normalizedDeadline) tpl += '|срок=' + normalizedDeadline;
return T_OPEN + tpl + T_CLOSE;
}
function upsertConditionalRetTemplateOnTalkPage(text, dateIso, sectionTitle, reasonText, deadlineText) {
var source = text || '';
var normalizedSection = String(sectionTitle || '').trim();
var normalizedReason = normalizeQuickPhraseValue(reasonText);
var normalizedDeadline = String(deadlineText || '').trim();
var tplRe = buildTemplateTransclusionRegex('Условно оставлено');
var tplMatch = tplRe ? source.match(tplRe) : null;
function buildExistingTplWithCanonicalName(suffix) {
return T_OPEN + 'Условно оставлено' + String(tplMatch[2] || '').replace(/\s+$/, '') + (suffix || '') + T_CLOSE;
}
if (!tplMatch) {
return {
text: insertTplOnTalkPage(source, buildConditionalRetTemplateText(dateIso, normalizedSection, normalizedReason, normalizedDeadline, 1), '\n'),
status: 'created'
};
}
var parts = tplMatch[2].split('|').map(function (p) { return p.trim(); }).filter(Boolean);
var existingDates = parts.filter(function (p) { return p.indexOf('=') === -1; }).map(function (p) { return convertToStandardDate(p); });
var stdDate = convertToStandardDate(dateIso);
if (existingDates.indexOf(stdDate) !== -1) {
var canonicalTpl = buildExistingTplWithCanonicalName('');
if (canonicalTpl !== tplMatch[0]) {
return { text: source.replace(tplMatch[0], canonicalTpl), status: 'updated' };
}
return { text: source, status: 'already_present' };
}
var nextIdx = existingDates.length + 1;
var suffix = '|' + dateIso;
if (normalizedSection) suffix += '|l' + nextIdx + '=' + normalizedSection;
if (normalizedReason) suffix += '|пояснение=' + normalizedReason;
if (normalizedDeadline) suffix += '|срок=' + normalizedDeadline;
return {
text: source.replace(tplMatch[0], buildExistingTplWithCanonicalName(suffix)),
status: 'updated'
};
}
// ═══════════════════════════════════════════════════════════════════════════
// ПАЙПЛАЙН НОМИНАЦИИ
// ═══════════════════════════════════════════════════════════════════════════
function runNominationPipeline(steps) {
var s = steps;
var ctx = { templateMeta: null, nominationInfo: null };
var stages = [
{
name: 'шаблон',
fn: function (next) {
s.templateStep(function (err, meta) { ctx.templateMeta = meta || null; next(err); });
}
},
{
name: 'номинация',
pendingText: 'Публикуется номинация...',
successText: 'Номинация опубликована.',
errorText: 'Публикация номинации.',
fn: function (next) {
s.nominationStep(function (err, info) { ctx.nominationInfo = info || null; next(err); });
}
},
{
name: 'подписка',
shouldRun: function () {
var info = ctx.nominationInfo;
return !!(setSubscribe && info && info.pageTitle && info.sectionTitle);
},
fn: function (next) {
subscribeToTopic(ctx.nominationInfo.pageTitle, ctx.nominationInfo.sectionTitle, function () { next(); });
}
},
{
name: 'оповещение',
shouldRun: function () { return !!(setAlert && !s.skipNotify); },
fn: function (next) { s.notifyStep(ctx.nominationInfo, next); }
}
];
(function run(i) {
if (i >= stages.length) { if (typeof s.onSuccess === 'function') s.onSuccess(ctx); return; }
var stage = stages[i];
if (typeof stage.shouldRun === 'function' && !stage.shouldRun()) { run(i + 1); return; }
var statusId = stage.pendingText
? logStatus(stage.pendingText, null, { pending: true, trackError: false })
: null;
try {
stage.fn(function (err) {
if (err) {
if (statusId) logStatus(stage.errorText || ('Этап «' + stage.name + '».'), err, { statusId: statusId });
if (typeof s.onFailure === 'function') s.onFailure(stage.name, err, ctx);
else markSubmitError();
return;
}
if (statusId && stage.successText) logStatus(stage.successText, null, { statusId: statusId, trackError: false });
run(i + 1);
});
} catch (ex) {
var exErr = { code: 'exception', info: (ex && ex.message) ? ex.message : String(ex) };
if (statusId) logStatus(stage.errorText || ('Этап «' + stage.name + '».'), exErr, { statusId: statusId });
if (typeof s.onFailure === 'function') s.onFailure(stage.name, exErr, ctx);
else markSubmitError();
}
}(0));
}
// ─── Публикация номинации ────────────────────────────────────────────────
function publishNomination(opts, callback) {
var cb = callback || function () {};
var maxRetries = Math.max(0, parseInt(opts.editConflictRetries, 10) || 1);
function doPublish() {
apiReq({
title: opts.pageTitle,
section: 'new',
sectiontitle: opts.sectionTitle,
summary: opts.summary,
text: opts.text,
assertuser: mwCfg.wgUserName
}, 'edit', function (resp) {
cb(resp && resp.error ? resp.error : null);
});
}
if (opts.sectionTitle) {
if (!opts.navTemplate) { doPublish(); return; }
apiReq({ title: opts.pageTitle, createonly: '1', text: T_OPEN + opts.navTemplate + '-Навигация' + T_CLOSE + '\n', summary: makeSummary('автоматическая шапка'), assertuser: mwCfg.wgUserName },
'edit', function (resp) {
if (resp && resp.error && resp.error.code !== 'articleexists') { cb(resp.error); return; }
doPublish();
});
return;
}
// Вставка в существующую страницу
if (opts.createText !== undefined) {
(function attempt(retry) {
getTextWithTimestamp(opts.pageTitle, function (pageText, baseTimestamp, readErr) {
var result;
var ep;
if (readErr) { cb(makeReadError(readErr, 'read_failed', opts.readErrorMessage || 'Не удалось получить содержимое.')); return; }
result = pageText === null
? (typeof opts.createText === 'function' ? opts.createText() : opts.createText)
: (opts.buildText ? opts.buildText(pageText) : null);
if (typeof result === 'string') result = { text: result };
if (!result || result.error) { cb((result && result.error) || { code: 'build_failed', info: 'Не удалось подготовить правку.' }); return; }
if (result.skip) { cb(null); return; }
if (typeof result.text !== 'string') { cb({ code: 'build_failed', info: 'Не удалось подготовить правку.' }); return; }
ep = {
title: opts.pageTitle,
text: result.text,
summary: result.summary || opts.summary,
assertuser: mwCfg.wgUserName
};
if (pageText === null) ep.createonly = true;
else if (baseTimestamp) ep.basetimestamp = baseTimestamp;
apiReq(ep, 'edit', function (resp) {
var err = resp && resp.error ? resp.error : null;
if (err && (err.code === 'editconflict' || err.code === 'articleexists') && retry < maxRetries) {
attempt(retry + 1);
return;
}
cb(err);
});
});
}(0));
return;
}
editPageContent(opts.pageTitle, { summary: opts.summary, readError: opts.readErrorMessage || 'Не удалось получить содержимое.' },
function (pageText) { return opts.buildText ? opts.buildText(pageText) : null; },
function (err) { cb(err || null); }
);
}
// ─── Оповещение авторов ──────────────────────────────────────────────────
function notifyAuthor(pg, options, callback) {
var opts = options || {};
var cb = callback || function () {};
var actionText = (typeof opts.actionText === 'string') ? opts.actionText : '';
var discussionPage = (typeof opts.discussionPage === 'string') ? opts.discussionPage : '';
var discussionSection = normalizeSectionForLink((typeof opts.discussionSection === 'string') ? opts.discussionSection : '');
var includeProposed = (typeof opts.includeProposedPrefix === 'boolean') ? opts.includeProposedPrefix : true;
var actionPhrase = ((includeProposed ? 'предложена ' : '') + actionText).trim() || 'изменена';
var discussionText = discussionPage ? 'Обсуждение — на странице [[' + discussionPage + (discussionSection ? '#' + discussionSection : '') + ']]. ' : '';
apiReq({ prop: 'revisions', rvprop: 'user', rvdir: 'newer', titles: pg }, 'query', function (queryResp) {
var page = getFirstQueryPage(queryResp);
if (!page) { cb({ code: 'network', info: 'Network error' }); return; }
if (page.missing !== undefined) { cb({ code: 'missing', info: 'Page missing.' }); return; }
if (!page.revisions || !page.revisions.length) { cb({ code: 'no_revisions', info: 'No revisions.' }); return; }
var rv = page.revisions[0];
if ('anon' in rv || rv.userhidden || !rv.user || rv.user === mwCfg.wgUserName) { cb(null); return; }
apiReq({
title: 'User talk:' + rv.user, section: 'new',
sectiontitle: 'Remover: [[:' + pg + ']]',
summary: opts.summary || makeSummary('уведомление автора'),
text: 'Страница [[:' + pg + ']], созданная вами, ' + actionPhrase + '. ' +
discussionText +
'~~' + '~~<br><small>Это автоматическое уведомление, сгенерированное ' + scriptLink + '.</small>',
assertuser: mwCfg.wgUserName
}, 'edit', function (editResp) { cb(editResp && editResp.error ? editResp.error : null); });
});
}
function notifyAuthorsForPages(pages, notifyOptions, callback) {
var cb = callback || function () {};
var opts = notifyOptions || {};
var list = [];
(pages || []).forEach(function (p) { var t = normTitle(p); if (t && list.indexOf(t) === -1) list.push(t); });
if (!list.length) { cb(); return; }
eachSequential(list, function (pg, next) {
var pageLink = buildQuotedStatusPageLink(pg);
var statusId = logStatus('Отправляется уведомление создателю страницы ' + pageLink + '...', null, { pending: true, trackError: false });
notifyAuthor(pg, opts, function (err) {
logStatus(err ? 'Уведомление создателя страницы ' + pageLink + '.' : 'Создатель страницы ' + pageLink + ' уведомлён.', err,
{ statusId: statusId, trackError: opts.trackError !== false });
next();
});
}, cb);
}
// ─── Подписка на раздел ──────────────────────────────────────────────────
function subscribeToTopic(pageTitle, sectionTitle, callback) {
var cb = callback || function () {};
if (!setSubscribe || !sectionTitle) { cb(); return; }
var statusId = logStatus('Оформляется подписка на раздел...', null, { pending: true, trackError: false });
var targetFrag = normalizeSectionForLink(sectionTitle).toLowerCase();
function finish(err, st) {
if (err) { logStatus('Не удалось подписаться на раздел.', err, { statusId: statusId, trackError: false }); cb(); return; }
logStatus(st === 'subscribed' ? 'Оформлена подписка на раздел.' : 'Раздел для подписки не найден.', null, { statusId: statusId, trackError: false });
cb();
}
function trySubscribe(attemptsLeft) {
apiReq({ page: pageTitle, prop: 'threaditemshtml', excludesignatures: true }, 'discussiontoolspageinfo', function (data) {
var items = (data && data.discussiontoolspageinfo && data.discussiontoolspageinfo.threaditemshtml) || null;
if (!items || !items.length) {
if (attemptsLeft > 0) { setTimeout(function () { trySubscribe(attemptsLeft - 1); }, 2000); return; }
finish(null, 'not_found'); return;
}
var commentname = null;
for (var ti = items.length - 1; ti >= 0; ti--) {
var t = items[ti];
if (t.type === 'heading') {
var htext = (t.headingText || t.html || '').replace(/<[^>]+>/g, '').trim();
if (htext.toLowerCase() === targetFrag || normalizeSectionForLink(htext).replace(/_/g, ' ').toLowerCase() === targetFrag) {
commentname = t.name; break;
}
}
}
if (!commentname) {
if (attemptsLeft > 0) setTimeout(function () { trySubscribe(attemptsLeft - 1); }, 2000);
else finish(null, 'not_found');
return;
}
apiReq({ page: pageTitle, commentname: commentname, subscribe: '1' }, 'discussiontoolssubscribe', function (res) {
finish(res && res.error ? res.error : null, 'subscribed');
});
});
}
setTimeout(function () { trySubscribe(2); }, 1500);
}
// ═══════════════════════════════════════════════════════════════════════════
// UI: модальные окна
// ═══════════════════════════════════════════════════════════════════════════
function syncModalLayout() {
var syncFn = $('#removerModal').data('rmSyncLayout');
if (typeof syncFn === 'function') syncFn();
}
function clearModalLayoutSyncHandlers() {
modalLayoutSyncHandlers = [];
$('#removerModal').removeData('rmSyncLayout');
}
function registerModalLayoutSync(handler) {
if (typeof handler !== 'function') return;
if (modalLayoutSyncHandlers.indexOf(handler) === -1) modalLayoutSyncHandlers.push(handler);
$('#removerModal').data('rmSyncLayout', function () {
modalLayoutSyncHandlers.slice().forEach(function (fn) {
if (typeof fn === 'function') fn();
});
});
}
function registerResizeObserver(observer) {
if (observer) resizeObservers.push(observer);
}
function resetModalObservers() {
resizeObservers.forEach(function (observer) {
if (observer && typeof observer.disconnect === 'function') observer.disconnect();
});
resizeObservers = [];
clearModalLayoutSyncHandlers();
$(window).off('resize.removerModal');
$(window).off('.rmTaResize');
}
function closeModal() {
resetModalObservers();
$(window).off('keydown.remover');
if (isVector22) $('#content').css('min-width', '');
$('#removerModal').remove();
}
function ensureModalStyles() {
if (document.getElementById('removerModalDynamicStyles')) return;
var progH = 'background:' + tk.bgProgH + '!important;border-color:' + tk.bProgH + '!important;color:' + tk.cInv + '!important;';
var neutH = 'background:' + tk.bgN + '!important;border-color:' + tk.bSub + '!important;color:inherit!important;';
var succH = 'background:' + tk.bgSuccH+ '!important;border-color:' + tk.bSuccH + '!important;color:' + tk.cInv + '!important;';
var pillBg = 'linear-gradient(180deg,' + tk.bgBase + ' 0%,' + tk.bgNSub + ' 100%)';
var pillShadow = '0 1px 0 rgba(255,255,255,.08) inset,0 1px 2px rgba(0,0,0,.18)';
var activePillShadow = '0 1px 0 rgba(255,255,255,.14) inset,0 2px 6px rgba(51,102,204,.24)';
var css = [
'#removerModal,#removerModal *{-moz-text-size-adjust:none!important;-webkit-text-size-adjust:100%!important;text-size-adjust:100%!important}',
'#removerModal{color:inherit}',
'#removerModal input::placeholder,#removerModal textarea::placeholder{color:var(--color-subtle,currentColor);opacity:.7}',
'#removerModal input[type="checkbox"],#removerModal input[type="radio"]{appearance:auto;-webkit-appearance:auto;-moz-appearance:auto;accent-color:auto}',
'#removerModal input[type="checkbox"]{outline:none!important;box-shadow:none!important}',
'#removerModal button{transition:background-color .12s ease,border-color .12s ease,color .12s ease,box-shadow .12s ease,filter .12s ease,transform .06s ease}',
'#removerModal .rmToggleBtn{background:' + tk.bgNSub + '!important;border-color:' + tk.bSub + '!important;color:inherit!important}',
'#removerModal .rmToggleBtn.is-active{background:' + tk.bgProg + '!important;border-color:' + tk.bProg + '!important;color:' + tk.cInv + '!important}',
'#removerModal button:not(:disabled):hover{filter:brightness(.97)}',
'#removerModal button:not(:disabled):active{transform:translateY(1px)}',
'#removerModal #removerSubmit:not(:disabled):not(.rmSubmitError):hover,#removerModal .rmToggleBtn.is-active:hover{' + progH + 'filter:none}',
'#removerModal #removerSubmit:not(:disabled):not(.rmSubmitError):active,#removerModal .rmToggleBtn.is-active:active{' + progH + 'filter:brightness(.92)!important}',
'#removerModal #removerSubmit.rmSubmitReady:not(.rmSubmitError){background:' + tk.bgProg + '!important;border-color:' + tk.bProg + '!important;color:' + tk.cInv + '!important;outline:none!important;box-shadow:0 0 0 6px rgba(51,102,204,.13),0 1px 2px rgba(0,0,0,.08)!important}',
'#removerModal #removerSubmit.rmSubmitReady:not(.rmSubmitError):not(:disabled):hover{' + progH + 'box-shadow:0 0 0 7px rgba(51,102,204,.16),0 1px 2px rgba(0,0,0,.1)!important;filter:none}',
'#removerModal #removerSubmit.rmSubmitReady:not(.rmSubmitError):not(:disabled):active{' + progH + 'box-shadow:0 0 0 5px rgba(51,102,204,.14),0 1px 2px rgba(0,0,0,.08)!important;filter:brightness(.92)!important}',
'#removerModal .rmAddPageBtn{background:' + tk.bgProg + '!important;border-color:' + tk.bProg + '!important;color:' + tk.cInv + '!important;font-weight:700!important}',
'#removerModal .rmAddPageBtn:hover{' + progH + 'filter:none!important}',
'#removerModal .rmAddVariantBtn{background:' + tk.bgBase + '!important;border-color:' + tk.bSub + '!important;color:inherit!important;font-weight:700!important}',
'#removerModal .rmAddVariantBtn:hover{' + neutH + 'filter:none!important}',
'#removerModal .rmRenameVariantAddBtn{font-size:15px!important}',
'#removerModal .rmStartMultiPageBtn{align-self:flex-start!important;margin-bottom:0!important}',
'#removerModal .rmRenameVariantRow{width:100%!important;max-width:100%!important;box-sizing:border-box!important}',
'#removerModal .rmMultiRenameVariantsContainer{display:flex;flex-direction:column;gap:6px;box-sizing:border-box;margin-top:6px;width:100%!important;max-width:100%!important}',
'#removerModal .rmMultiRenamePrimaryTargetRow,#removerModal .rmMultiRenameVariantRow{display:flex;margin-bottom:0!important;box-sizing:border-box}',
'#removerModal .rmMultiPageCommentToggle{min-width:32px;height:32px;padding:0!important;font-size:15px!important;line-height:1!important}',
'#removerModal .rmMultiPageCommentToggle.is-active{background:#bfc4ca!important;border-color:#8f98a3!important;color:#202122!important}',
'#removerModal .rmMultiPageCommentToggle.is-active:hover{background:#b4bac1!important;border-color:#848e99!important;color:#202122!important;filter:none}',
'#removerModal .rmMultiPageCommentToggle.is-active:active{background:#a9b0b8!important;border-color:#79838f!important;color:#202122!important;filter:none}',
'#removerModal #removerSubmit.rmSubmitError:not(:disabled):hover{background:#b32424!important;border-color:#b32424!important;color:#fff!important;filter:none}',
'#removerModal #removerSubmit.rmSubmitError:not(:disabled):active{background:#b32424!important;border-color:#b32424!important;color:#fff!important;filter:brightness(.88)!important}',
'#removerModal #removerReload:not(:disabled):hover{' + succH + 'filter:none}',
'#removerModal #removerReload:not(:disabled):active{' + succH + 'filter:brightness(.92)!important}',
'#removerModal button:not(#removerSubmit):not(#removerReload):not(.is-active):not(.rmAddPageBtn):not(.rmAddVariantBtn):hover,#removerModal .rmToggleBtn:not(.is-active):not(.rmAddPageBtn):not(.rmAddVariantBtn):hover{' + neutH + 'filter:none}',
'#removerModal button:not(#removerSubmit):not(#removerReload):not(.is-active):not(.rmAddPageBtn):not(.rmAddVariantBtn):active{' + neutH + 'filter:brightness(.92)!important}',
'#removerModal a.removerModalLink{color:' + tk.cProg + ';text-decoration:none;border-bottom:0;box-shadow:none;word-break:break-word;overflow-wrap:anywhere}',
'#removerModal a.removerModalLink:hover,#removerModal a.removerModalLink:focus{color:' + tk.cProgH + ';text-decoration:underline}',
'#removerModal #removerModalSubtitle{font-size:12px!important;line-height:1.35!important;font-weight:400!important;color:' + tk.cSubM + '!important;max-width:100%;overflow-wrap:anywhere;word-break:break-word}',
'#removerModal #removerModalSubtitle a{font-size:inherit!important;line-height:inherit!important;font-weight:inherit!important}',
'#removerModal a.rmButtonLikeLink{color:' + tk.cBase + '!important;text-decoration:none!important;transform:none!important;transition:background-color .12s ease,border-color .12s ease,color .12s ease,filter .12s ease,transform .06s ease!important}',
'#removerModal a.rmButtonLikeLink:hover{' + neutH + 'text-decoration:none!important;transform:none!important}',
'#removerModal a.rmButtonLikeLink:focus{text-decoration:none!important}',
'#removerModal a.rmButtonLikeLink:focus:not(:focus-visible){outline:none!important}',
'#removerModal a.rmButtonLikeLink:focus-visible{outline:2px solid ' + tk.bProg + '!important;outline-offset:2px;text-decoration:none!important}',
'#removerModal a.rmButtonLikeLink:hover:active{' + neutH + 'filter:brightness(.92)!important;transform:translateY(1px)!important;text-decoration:none!important}',
'#removerModal .rmInfoBox{margin:0 0 10px;padding:8px 10px;border:1px solid ' + tk.bSub + ';border-radius:6px;background:' + tk.bgNSub + '}',
'#removerModal .rmActionList{display:flex;flex-direction:column;gap:6px}',
'#removerModal .rmActionItem{display:block;padding:8px 10px;border:1px solid ' + tk.bSub + ';border-radius:6px;background:' + tk.bgBase + ';cursor:pointer;transition:background-color .12s ease,transform .06s ease}',
'#removerModal .rmActionItem:hover{background:' + tk.bgNSub + '}',
'#removerModal .rmActionItem:active{transform:translateY(1px)}',
'#removerModal .rmActionMain{display:flex;align-items:center}',
'#removerModal .rmActionMain input[type="radio"]{margin-right:8px}',
'#removerModal .rmActionMeta{display:block;margin-left:24px;margin-top:2px;color:' + tk.cSubM + ';font-size:12px;line-height:1.35}',
'#removerModal .rmActionWarning{display:none;margin-left:24px;margin-top:3px;color:' + tk.cDang + ';font-size:12px;line-height:1.35;font-weight:700}',
'#removerModal .rmConflictLead{margin:0 0 10px;color:' + tk.cSubM + ';font-size:13px;line-height:1.5}',
'#removerModal .rmConflictList{display:flex;flex-direction:column;gap:10px}',
'#removerModal .rmConflictCard{padding:12px;border:1px solid ' + tk.bSub + ';border-radius:8px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.55) inset}',
'#removerModal .rmConflictCard.is-skip{background:' + tk.bgNSub + ';border-color:' + tk.bSubS + '}',
'#removerModal .rmConflictTitle{font-size:14px;font-weight:700;line-height:1.4;color:' + tk.cBase + ';word-break:break-word;overflow-wrap:anywhere}',
'#removerModal .rmConflictMeta{margin-top:4px;font-size:12px;line-height:1.45;color:' + tk.cSubM + '}',
'#removerModal .rmConflictGroup{margin-top:10px}',
'#removerModal .rmConflictGroupTitle{margin:0 0 6px;font-size:12px;font-weight:700;line-height:1.35;color:' + tk.cSubM + '}',
'#removerModal .rmConflictButtons{display:flex;flex-wrap:wrap;gap:6px}',
'#removerModal .rmConflictChoice{padding:5px 10px}',
'#removerModal .rmConflictChoice.is-disabled,#removerModal .rmConflictButtons.is-disabled .rmConflictChoice{opacity:.55;cursor:not-allowed;pointer-events:none}',
'#removerModal .rmConflictHint{margin-top:8px;font-size:11px;line-height:1.45;color:' + tk.cSub + '}',
'#removerModal #rmSettingsForm{display:flex;flex-direction:column;gap:14px}',
'#removerModal .rmSettingsLead{margin:-2px 0 2px;color:' + tk.cSubM + ';font-size:13px;line-height:1.5}',
'#removerModal .rmSettingsSection{margin:0;padding:14px 16px;border:1px solid ' + tk.bSubS + ';border-radius:10px;background:linear-gradient(180deg,' + tk.bgBase + ' 0%,' + tk.bgNSub + ' 100%);box-shadow:0 1px 0 rgba(255,255,255,.7) inset}',
'#removerModal .rmSettingsSectionHeader{margin:0 0 12px}',
'#removerModal .rmSettingsSectionTitle{font-size:14px;font-weight:700;line-height:1.35;color:' + tk.cBase + '}',
'#removerModal .rmSettingsSectionDescription{margin-top:4px;font-size:12px;line-height:1.45;color:' + tk.cSubM + '}',
'#removerModal .rmSettingsField{display:block;margin:0 0 10px;padding:12px;border:1px solid ' + tk.bSubS + ';border-radius:8px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.55) inset}',
'#removerModal .rmSettingsField:last-child{margin-bottom:0}',
'#removerModal .rmSettingsFieldLabel{display:block;font-size:13px;font-weight:700;line-height:1.35;margin:0 0 8px;color:' + tk.cBase + '}',
'#removerModal .rmSettingsFieldControl{display:block;min-width:0}',
'#removerModal .rmSettingsFieldControl input{margin-bottom:0!important}',
'#removerModal .rmSettingsFieldControl input[type="text"]{min-height:38px;border-radius:6px}',
'#removerModal .rmSettingsFieldHint{margin-top:8px;font-size:12px;line-height:1.55;color:' + tk.cSubM + ';overflow-wrap:anywhere}',
'#removerModal .rmSettingsChecks{display:flex;flex-direction:column;gap:8px}',
'#removerModal .rmSettingsCheck{display:inline-flex;align-items:flex-start;gap:8px;font-size:14px;line-height:1.45;color:' + tk.cBase + '}',
'#removerModal .rmSettingsCheck input[type="checkbox"]{margin:3px 0 0;flex-shrink:0}',
'#removerModal .rmSettingsMenuPresetWrap{margin-top:10px}',
'#removerModal .rmSettingsMenuPresetLabel{margin:0 0 6px;font-size:12px;font-weight:700;line-height:1.35;color:' + tk.cSubM + '}',
'#removerModal .rmSegmentedBar{display:inline-flex;align-items:center;flex-wrap:wrap;gap:6px;margin-top:0;padding:0;border:0;border-radius:0;background:transparent;max-width:100%;box-sizing:border-box;box-shadow:none}',
'#removerModal .rmSettingsMenuPresetBar{display:inline-flex;align-items:center;flex-wrap:wrap;gap:6px;margin-top:0;padding:0;border:0;border-radius:0;background:transparent;max-width:100%;box-sizing:border-box;box-shadow:none}',
'#removerModal .rmSegmentedBtn,#removerModal .rmSettingsMenuPresetBtn{padding:5px 12px;border:1px solid ' + tk.bSub + ';border-radius:999px;background:' + pillBg + ';color:' + tk.cBase + ';font-size:12px;font-weight:600;line-height:1.35;cursor:pointer;box-shadow:' + pillShadow + '}',
'#removerModal .rmSegmentedBtn.is-active,#removerModal .rmSettingsMenuPresetBtn.is-active{background:' + tk.bgProg + ';border-color:' + tk.bProg + ';color:' + tk.cInv + ';box-shadow:' + activePillShadow + '}',
'#removerModal .rmKbuReasonPill{display:flex;align-items:stretch;width:100%;padding:0!important;overflow:hidden;border:1px solid ' + tk.bSub + ';border-radius:999px;box-sizing:border-box;cursor:pointer;text-align:left;font-size:12px;font-weight:400;line-height:1.3;box-shadow:' + pillShadow + '}',
'#removerModal .rmKbuReasonPill.is-active{box-shadow:' + activePillShadow + '!important}',
'#removerModal .rmKbuReasonNum{display:inline-flex;align-items:center;justify-content:center;min-width:44px;padding:4px 10px;border-right:1px solid ' + tk.bSub + ';box-sizing:border-box;font-weight:700;text-align:center;flex:0 0 auto}',
'#removerModal .rmKbuReasonPill.is-active .rmKbuReasonNum{border-right-color:rgba(255,255,255,.55)!important}',
'#removerModal .rmKbuReasonText{display:block;min-width:0;flex:1;padding:4px 11px 4px 12px;font-weight:400;overflow-wrap:anywhere}',
'#removerModal.rmModalSettings{border:1px solid ' + tk.bSub + '!important;background:' + tk.bgBase + '!important;border-radius:12px!important;box-shadow:0 14px 32px rgba(0,0,0,.08),0 1px 0 rgba(255,255,255,.78) inset!important}',
'#removerModal.rmModalSettings #removerModalHeaderBar{margin-bottom:14px;padding-bottom:10px;border-bottom:2px solid ' + tk.bSub + '}',
'#removerModal.rmModalSettings #removerModalSubtitle{margin:-2px 0 12px!important;color:' + tk.cSubM + '!important}',
'#removerModal.rmModalSettings #rmSettingsForm{gap:18px}',
'#removerModal.rmModalSettings .rmSettingsLead{margin:0;padding:0 2px;color:' + tk.cSubM + '}',
'#removerModal.rmModalSettings .rmSettingsSection{padding:16px 18px;border:1px solid ' + tk.bSub + ';border-radius:12px;background:linear-gradient(180deg,' + tk.bgNSub + ' 0%,' + tk.bgBase + ' 100%);box-shadow:0 1px 0 rgba(255,255,255,.82) inset,0 8px 18px rgba(0,0,0,.035)}',
'#removerModal.rmModalSettings .rmSettingsSectionHeader{margin:0 0 6px;padding-bottom:0;border-bottom:0}',
'#removerModal.rmModalSettings .rmSettingsSectionTitle{font-size:16px;line-height:1.3}',
'#removerModal.rmModalSettings .rmSettingsSectionDescription{margin-top:5px;max-width:none}',
'#removerModal.rmModalSettings .rmSettingsField{margin:14px 0 0;padding:12px 0 0;border:0;border-top:1px solid ' + tk.bSubS + ';border-radius:0;background:transparent;box-shadow:none}',
'#removerModal.rmModalSettings .rmSettingsField:first-child{margin-top:0;padding-top:0;border-top:0}',
'#removerModal.rmModalSettings .rmSettingsFieldLabel{margin:0 0 6px}',
'#removerModal.rmModalSettings .rmSettingsFieldControl input[type="text"]{min-height:40px;padding:8px 10px;border:1px solid ' + tk.bSub + ';border-radius:8px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.65) inset}',
'#removerModal.rmModalSettings .rmSettingsFieldControl input[type="text"]:focus{border-color:' + tk.bProg + ';box-shadow:0 0 0 1px rgba(51,102,204,.16);outline:none}',
'#removerModal.rmModalSettings .rmSettingsFieldHint{margin-top:6px;max-width:none}',
'#removerModal.rmModalSettings .rmSettingsChecks{gap:10px}',
'#removerModal.rmModalSettings .rmSettingsCheck{padding:4px 0}',
'#removerModal.rmModalSettings .rmSettingsMenuPresetWrap{margin-top:12px;padding-top:10px;border-top:1px dashed ' + tk.bSubS + '}',
'#removerModal.rmModalSettings .rmSettingsHintList{width:100%;max-width:100%;box-sizing:border-box;margin-top:10px;padding:10px 12px;border:1px solid ' + tk.bSubS + ';border-radius:8px;background:' + tk.bgBase + '}',
'#removerModal.rmModalSettings .rmSettingsHintRow{line-height:1.55}',
'#removerModal.rmModalSettings .rmSettingsHintBadge{background:' + tk.bgNSub + '}',
'#removerModal.rmModalSettings .rmQuickPhraseEditor{padding-top:2px}',
'#removerModal.rmModalSettings .rmQuickPhraseMeta{min-height:18px}',
'#removerModal.rmModalSettings #rmSettingsSignaturePreviewCode{display:inline-block;padding:2px 8px;border:1px solid ' + tk.bSubS + ';border-radius:999px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.65) inset}',
'#removerModal.rmModalSettings #rmFooterActionButtons{position:relative;flex:0 0 auto!important;display:flex!important;align-items:center!important;justify-content:flex-end!important;gap:6px!important;margin-left:auto!important;max-width:100%!important}',
'#removerModal.rmModalSettings #rmSettingsActionButtonsRow{display:flex;align-items:center;justify-content:flex-end;gap:6px;flex-wrap:nowrap}',
'#removerModal.rmModalSettings #rmSettingsUnsavedHint{display:none;position:absolute;top:100%;right:0;width:auto;box-sizing:border-box;margin:4px 0 0;color:' + tk.cSubM + ';opacity:.78;font-size:12px;line-height:1.35;text-align:right;white-space:nowrap}',
'#removerModal .rmProtectControlGroup{margin-top:12px;padding:8px 10px;border:1px solid ' + tk.bSubS + ';border-radius:10px;background:linear-gradient(180deg,' + tk.bgBase + ' 0%,' + tk.bgNSub + ' 100%);box-sizing:border-box}',
'#removerModal .rmProtectControlLabel{margin:0 0 6px;font-size:12px;font-weight:700;line-height:1.35;color:' + tk.cSubM + '}',
'#removerModal .rmProtectControlGroup .rmSegmentedBar{gap:4px;padding:0;border:0;box-shadow:none}',
'#removerModal .rmProtectControlGroup .rmSegmentedBtn{padding:4px 10px;font-size:12px}',
'#removerModal .rmTransferPanel{margin-top:10px;padding:0;border:0;background:transparent;box-sizing:border-box;box-shadow:none}',
'#removerModal .rmTransferGrid{display:grid;grid-template-columns:max-content max-content;column-gap:10px;row-gap:6px;align-items:start;justify-content:start}',
'#removerModal .rmTransferGrid .rmSegmentedBar{justify-self:start}',
'#removerModal .rmTransferHintRow{grid-column:1 / -1;min-height:0}',
'#removerModal .rmTransferPanel .rmSegmentedBar{gap:4px;padding:0;border:0;box-shadow:none}',
'#removerModal .rmTransferPanel .rmSegmentedBtn{padding:6px 14px;font-size:12px;font-weight:700}',
'#removerModal #rmTransferModeGroup{gap:0}',
'#removerModal #rmTransferModeGroup .rmSegmentedBtn:first-child{border-top-right-radius:0;border-bottom-right-radius:0}',
'#removerModal #rmTransferModeGroup .rmSegmentedBtn + .rmSegmentedBtn{margin-left:-1px;border-top-left-radius:0;border-bottom-left-radius:0}',
'#removerModal.rmCompactContent .rmMultiPageRow{flex-wrap:wrap!important;gap:6px}',
'#removerModal.rmCompactContent .rmMultiPageRow .rmMultiPageInput,#removerModal.rmCompactContent .rmMultiPageRow .rmMultiPageTargetInput{flex:1 1 100%!important;width:100%!important}',
'#removerModal.rmCompactContent .rmMultiPageRow .rmMultiPageCommentToggle,#removerModal.rmCompactContent .rmMultiPageRow .rmAddMultiPage,#removerModal.rmCompactContent .rmMultiPageRow .rmAddMultiRenameVariant,#removerModal.rmCompactContent .rmMultiPageRow .rmRemoveInput{margin-left:0!important}',
'#removerModal.rmCompactContent .rmTransferGrid{grid-template-columns:minmax(0,1fr);gap:10px}',
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(1){order:1}',
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(2){order:2}',
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(3){order:3}',
'#removerModal.rmCompactContent #rmTransferModeGroup{flex-direction:column;align-items:flex-start;gap:6px}',
'#removerModal.rmCompactContent #rmTransferModeGroup .rmSegmentedBtn{margin-left:0!important;border-radius:999px!important}',
'#removerModal.rmCompactContent .rmTransferHintRow{grid-column:auto}',
'#removerModal #rmProtectTextBlock{margin-top:14px}',
'#removerModal #rmSettingsMenuTitle:disabled{background:' + tk.bgDis + '!important;border-color:' + tk.bDis + '!important;color:' + tk.cDis + '!important;-webkit-text-fill-color:' + tk.cDis + ';opacity:1;cursor:not-allowed;box-shadow:none!important}',
'#removerModal .rmSettingsHintList{display:flex;flex-direction:column;gap:4px;margin-top:8px}',
'#removerModal .rmSettingsHintRow{font-size:12px;line-height:1.5;color:' + tk.cSubM + '}',
'#removerModal .rmSettingsHintBadge{display:inline-block;margin-right:6px;padding:1px 6px;border:1px solid ' + tk.bSubS + ';border-radius:999px;background:' + tk.bgBase + ';font-size:11px;font-weight:700;color:' + tk.cSubM + ';vertical-align:baseline}',
'#removerModal .rmQuickPhraseEditor{display:flex;flex-direction:column;gap:10px}',
'#removerModal .rmQuickPhraseList,#removerModal .rmQuickPhrasesPanel{display:flex;flex-wrap:wrap;gap:8px;align-items:flex-start}',
'#removerModal .rmQuickPhraseChip{position:relative;display:inline-flex;align-items:center;gap:4px;max-width:100%;padding:2px 4px 2px 10px;border:1px solid ' + tk.bSub + ';border-radius:999px;background:' + pillBg + ';box-shadow:' + pillShadow + ';transition:border-color .12s,box-shadow .12s,opacity .12s;overflow:visible}',
'#removerModal .rmQuickPhraseChip.is-editing{opacity:.42;border-style:dashed}',
'#removerModal .rmQuickPhraseChip.is-dragging{opacity:.65}',
'#removerModal .rmQuickPhraseChip.is-drop-before::before,#removerModal .rmQuickPhraseChip.is-drop-after::after{content:"";position:absolute;top:50%;width:3px;height:24px;border-radius:999px;background:' + tk.cProg + ';box-shadow:0 0 0 1px rgba(51,102,204,.08)}',
'#removerModal .rmQuickPhraseChip.is-drop-before::before{left:-4px;transform:translate(-50%,-50%)}',
'#removerModal .rmQuickPhraseChip.is-drop-after::after{right:-4px;transform:translate(50%,-50%)}',
'#removerModal .rmQuickPhraseEditBtn{max-width:100%;padding:3px 0;border:0;background:transparent;color:' + tk.cBase + ';font-size:13px;line-height:1.35;cursor:pointer;text-align:left;white-space:normal}',
'#removerModal .rmQuickPhraseRemoveBtn{width:24px;height:24px;padding:0;border:0;border-radius:999px;background:transparent;color:' + tk.cSubM + ';font-size:18px;line-height:1;cursor:pointer;flex-shrink:0}',
'#removerModal .rmQuickPhraseRemoveBtn:hover{background:' + tk.bgN + ';color:' + tk.cBase + '}',
'#removerModal #rmSettingsQuickPhraseInput.is-editing{border-color:' + tk.bProg + ';box-shadow:0 0 0 1px rgba(51,102,204,.12)}',
'#removerModal .rmQuickPhraseMeta{font-size:12px;line-height:1.45;color:' + tk.cSubM + '}',
'#removerModal .rmQuickPhraseEmpty{padding:2px 0;font-size:12px;line-height:1.45;color:' + tk.cSubM + '}',
'#removerModal .rmQuickPhrasesPanel{margin-top:8px}',
'#removerModal .rmQuickPhraseActionBtn{padding:5px 12px;border:1px solid ' + tk.bSub + ';border-radius:999px;background:' + pillBg + ';color:' + tk.cBase + ';font-size:12px;font-weight:600;line-height:1.35;cursor:pointer;box-shadow:' + pillShadow + '}',
'#removerModal .rmQuickPhraseActionBtn:hover{border-color:' + tk.bProg + ';color:' + tk.cProg + '}',
'@media (max-width:' + sz.mobileBp + 'px){',
'#removerModal button{white-space:normal!important}',
'#removerModal #rmFooterButtons{align-items:flex-start!important}',
'#removerModal #rmFooterCheckboxes,#removerModal #rmFooterActionButtons{width:100%!important;max-width:100%!important;margin-left:0!important}',
'#removerModal .rmSettingsSection{padding:12px 13px}',
'#removerModal .rmSettingsField{padding:10px}',
'#removerModal.rmModalSettings #rmSettingsUnsavedHint{max-width:100%;margin:4px 0 0;text-align:right;white-space:normal}',
'#removerModal .rmTransferPanel{padding:0}',
'#removerModal .rmTransferGrid{grid-template-columns:minmax(0,1fr);gap:10px}',
'#removerModal .rmTransferHintRow{grid-column:auto}',
'#removerModal .rmQuickPhraseChip{max-width:100%}',
'}'
].join('');
var style = document.createElement('style');
style.id = 'removerModalDynamicStyles';
style.textContent = css;
document.head.appendChild(style);
}
function applyV2022Layout($modal, explicitWidth) {
if (!isVector22) return;
var css = { 'max-width': '100%', 'box-sizing': 'border-box', 'overflow-wrap': 'anywhere' };
if (typeof explicitWidth === 'number') css['max-width'] = explicitWidth + 'px';
$modal.css(css);
}
function getPageUrl(pageTitle) {
return (mw.util && typeof mw.util.getUrl === 'function')
? mw.util.getUrl(pageTitle)
: '/wiki/' + encodeURIComponent((pageTitle || '').replace(/ /g, '_'));
}
function getPageUrlWithFragment(pageTitle, fragment) {
var url = getPageUrl(pageTitle);
var frag = normalizeSectionForLink(fragment);
if (frag) url += '#' + ((mw.util && mw.util.escapeIdForLink) ? mw.util.escapeIdForLink(frag) : frag.replace(/\s+/g, '_'));
return url;
}
function buildStatusPageLink(pageName) {
var title = normTitle(pageName);
return '<a href="' + escapeHtml(getPageUrl(title)) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' +
escapeHtml(title) + '</a>';
}
function buildQuotedStatusPageLink(pageName) {
return '«' + buildStatusPageLink(pageName) + '»';
}
function buildTemplatePageLink(templateName, labelText) {
var name = String(templateName || '')
.replace(/^(?:safe)?subst\s*:\s*/i, '')
.replace(RE_TEMPLATE_NS, '')
.trim();
var ns = getNamespaceLabel(10, 'Шаблон');
if (!name) return escapeHtml(labelText);
return '<a href="' + escapeHtml(getPageUrl(ns + ':' + name)) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' +
escapeHtml(labelText) + '</a>';
}
function renderTemplateLinksInText(text) {
var source = String(text || '');
var out = '';
var lastIndex = 0;
source.replace(/\{\{\s*([^{}|]+)(?:\|[^{}]*)?\}\}/g, function (match, templateName, offset) {
out += escapeHtml(source.slice(lastIndex, offset));
out += buildTemplatePageLink(templateName, match);
lastIndex = offset + match.length;
return match;
});
return out + escapeHtml(source.slice(lastIndex));
}
function buildHeaderIconButtonHtml(id, title, label, text) {
return joinHtml([
'<button id="', id, '" type="button" title="', escapeHtml(title), '" aria-label="', escapeHtml(label || title), '" ',
'style="', stHeaderIconBtn, '">', text || '', '</button>'
]);
}
function createModal(opts) {
if (typeof opts === 'string') opts = { title: opts };
var layout = getModalLayout();
resetModalObservers();
ensureModalStyles();
if (isVector22) $('#content').css('min-width', '');
$('#removerModal').remove();
var subtitleHtml = '';
var subtitleStyle = 'margin:-4px 0 8px;font-size:12px!important;color:' + tk.cSubM + ';line-height:1.35!important;font-weight:400!important;';
var subtitleLinkStyle = 'font-size:inherit!important;line-height:inherit!important;font-weight:inherit!important;';
if (opts.subtitleHtml) {
subtitleHtml = joinHtml([
'<div id="removerModalSubtitle" style="', subtitleStyle, '">',
opts.subtitleHtml,
'</div>'
]);
} else if (opts.subtitlePage) {
subtitleHtml = joinHtml([
'<div id="removerModalSubtitle" style="', subtitleStyle, '">',
opts.subtitleLabel || 'Текущий день',
': <a href="', getPageUrl(opts.subtitlePage), '" target="_blank" rel="noopener noreferrer" class="removerModalLink" style="', subtitleLinkStyle, '">',
normTitle(opts.subtitlePage),
'</a></div>'
]);
}
var settingsButtonHtml = opts.showSettingsButton === false ? '' :
buildHeaderIconButtonHtml('removerSettingsTrigger', 'Конфигурация', 'Конфигурация', '⚙');
var display = opts.inline ? 'inline-block' : 'block';
var modalMargin = opts.inline ? '1em 0' : (layout.shouldCenter ? '1em auto' : '1em 0');
var inlineLayoutStyle = opts.inline ? ';justify-self:start;align-self:start;width:fit-content;' : '';
var modalStyle = joinHtml([
'position:relative;padding:1.5em;margin:', modalMargin, ';display:', display, ';',
'border:', stStyles.border, ';background:', stStyles.background,
';border-radius:', stStyles.borderRadius, ';box-shadow:', stStyles.boxShadow,
';max-width:100%;box-sizing:border-box;overflow-wrap:anywhere;', inlineLayoutStyle
]);
var headerStyle = 'display:flex;align-items:center;gap:10px;margin-bottom:10px;padding-bottom:6px;border-bottom:1px solid ' + tk.bSubS + ';';
var titleStyle = 'color:' + stStyles.headerColor + ';margin:0;padding:0;border:0;display:block;font-size:1.3em;font-weight:400;line-height:1.25;flex:1 1 auto;min-width:0;';
$('#content').prepend(joinHtml([
'<div id="removerModal" style="', modalStyle, '">',
'<div id="removerModalHeaderBar" style="', headerStyle, '">',
'<h1 id="removerModalTitle" style="', titleStyle, '"><span id="removerModalTitleText">', opts.title, '</span></h1>',
settingsButtonHtml,
'</div>',
subtitleHtml,
'<div id="removerModalContent"></div>',
'<div id="removerModalFooter" style="margin-top:15px;"></div>',
'</div>'
]));
var $modal = $('#removerModal');
if (opts.width === 'compact') $modal.css({ width: layout.defaultOuterWidth + 'px', 'max-width': '100%', 'box-sizing': 'border-box' });
else applyV2022Layout($modal);
$('#removerSettingsTrigger').off('click').on('click', function () {
openSettings();
});
}
function buildFooterCheckboxHtml(name, checked, label) {
return joinHtml([
'<label style="', stFooterCheckLabel, '">',
'<input name="', name, '" type="checkbox" style="margin:2px 0 0;flex-shrink:0;" ',
checked ? 'checked' : '',
'>',
label,
'</label>'
]);
}
function buildFooterActionsHtml(buttonsHtml) {
return '<div id="rmFooterActionButtons" style="' + stFooterActions + '">' + buttonsHtml + '</div>';
}
function renderModalFooter(mode, options) {
var opts = options || {};
$('#removerModalFooter').css('width', '');
if (mode === 'submit') {
var showCb = opts.showCheckbox !== false;
var showSub = opts.showSubscribe === true;
var ns = mwCfg.wgNamespaceNumber;
var notifyLabel = ns === 0 ? 'Оповестить создателя статьи'
: (ns === 10 || ns === 11) ? 'Оповестить создателя шаблона'
: (ns === 14 || ns === 15) ? 'Оповестить создателя категории'
: 'Оповестить создателя страницы';
var cbInlineHtml = '';
if (showSub || showCb) {
cbInlineHtml = joinHtml([
'<div id="rmFooterCheckboxes" style="', stFooterChecks, '">',
showSub ? buildFooterCheckboxHtml('rmSubscribe', setSubscribe, 'Подписаться на номинацию') : '',
showCb ? buildFooterCheckboxHtml('rmUAlert', setAlert, notifyLabel) : '',
'</div>'
]);
}
$('#removerModalFooter').html(joinHtml([
'<div id="rmFooterButtons" style="', stFooterWrap, 'justify-content:', cbInlineHtml ? 'space-between' : 'flex-end', ';">',
cbInlineHtml,
buildFooterActionsHtml(joinHtml([
'<button id="removerCancel" style="', stCancel, '">Отмена</button>',
'<button id="removerSubmit" style="', stSubmit, '">', opts.submitText || 'ОК', '</button>'
])),
'</div>'
]));
$('#removerCancel').click(function () { closeModal(); });
$('#removerSubmit').data('rmSubmitInProgress', false).click(function () {
if ($(this).data('rmSubmitInProgress')) return;
$(this).removeClass('rmSubmitError').css({ background: '', 'border-color': '', color: '' });
isError = false;
if (!opts.preserveLogOnSubmit) {
$('#rmLogBox').empty();
logStatusSeq = 0;
}
if (showCb) { setAlert = $('[name="rmUAlert"]').is(':checked'); state.setAlert = setAlert; }
if (showSub) { setSubscribe = $('[name="rmSubscribe"]').is(':checked'); state.setSubscribe = setSubscribe; }
$(this).data('rmSubmitInProgress', true).prop('disabled', true);
var submitResult;
try { submitResult = opts.onSubmit(); } catch (ex) { unlockModalSubmit(); throw ex; }
if (submitResult === false) unlockModalSubmit();
});
$(window).off('keydown.remover').on('keydown.remover', function (e) {
if (e.ctrlKey && e.keyCode === 13) $('#removerSubmit').click();
});
} else if (mode === 'reload') {
var newBtns = buildFooterActionsHtml(joinHtml([
'<button id="removerCancel" style="', stCancel, '">Закрыть</button>',
'<button id="removerReload" style="', stReload, '">', opts.reloadText || 'Обновить страницу', '</button>'
]));
$('#rmFooterCheckboxes').remove();
var $btns = $('#rmFooterButtons');
if ($btns.length) {
$btns.css({ 'justify-content': 'flex-end' }).html(newBtns);
} else {
$('#removerModalFooter').append(joinHtml([
'<div id="rmFooterButtons" style="', stFooterWrap, 'justify-content:flex-end;">',
newBtns,
'</div>'
]));
}
$('#removerCancel').click(function () { closeModal(); });
$('#removerReload').click(function () { location.reload(); });
$(window).off('keydown.remover').on('keydown.remover', function (e) {
if (e.keyCode === 27) $('#removerCancel').click();
if (e.ctrlKey && e.keyCode === 13) $('#removerReload').click();
});
if (shouldAutoReloadAfterAction()) setTimeout(function () { $('#removerReload').trigger('click'); }, 0);
} else { // 'close'
$('#removerModalFooter').html(joinHtml([
'<div style="display:flex;justify-content:flex-end;align-items:center;">',
'<button id="removerCancel" style="', stCancel, 'margin-right:0;">', opts.closeText || 'Закрыть', '</button>',
'</div>'
]));
$('#removerCancel').click(function () { closeModal(); });
$(window).off('keydown.remover').on('keydown.remover', function (e) {
if (e.keyCode === 27) $('#removerCancel').click();
});
}
}
function unlockModalSubmit() {
$('#removerSubmit').data('rmSubmitInProgress', false).prop('disabled', false);
}
function markSubmitError() {
isError = true;
var errColor = '#d73333';
$('#removerSubmit').data('rmSubmitInProgress', false).prop('disabled', false)
.addClass('rmSubmitError').css({ background: errColor, 'border-color': errColor, color: '#fff' });
}
// ─── UI: статус и ссылки ─────────────────────────────────────────────────
function startProcessing() {
if ($('#rmLogBox').length) return;
$('#removerModal').append(
'<div id="rmLogBox" style="margin-top:12px;padding-top:10px;border-top:1px solid ' + tk.bSubS + ';line-height:1.5;overflow-wrap:anywhere;word-break:break-word;box-sizing:border-box;"></div>'
);
syncLinkWidths();
}
function logStatus(message, error, opts) {
var o = opts || {};
if (o.trackError !== false && error && error.code) isError = true;
var $box = $('#rmLogBox');
if (!$box.length) { startProcessing(); $box = $('#rmLogBox'); }
var statusId = o.statusId || ('rm-status-' + (++logStatusSeq));
var $row = $box.find('[data-rm-status-id="' + statusId + '"]');
if (!$row.length) {
$row = $('<div data-rm-status-id="' + statusId + '" style="margin-top:4px;line-height:1.4;"></div>');
$box.append($row);
}
var html;
if (error) {
var errText = error.code
? '<span class="error"><small>' + escapeHtml(formatLogErrorCode(error.code)) + ': ' + escapeHtml(String(error.info || '')) + '</small></span>'
: escapeHtml(String(error));
html = '<span style="color:' + tk.cDang + ';margin-right:4px;">✕</span>' + message + ' — ' + errText;
} else if (o.pending) {
html = '<span style="color:' + tk.cSubM + ';">' + message + '</span>';
} else {
html = '<span style="color:' + tk.bgSucc + ';margin-right:4px;">✓</span><span style="color:' + tk.cSubM + ';">' + message + '</span>';
}
$row.html(html);
return statusId;
}
function formatLogErrorCode(code) {
var value = String(code || '');
return value.toLowerCase() === 'error' ? 'Ошибка' : value;
}
function logPageEdit(pageName, error, opts) {
logStatus('Правка страницы ' + buildQuotedStatusPageLink(pageName) + '.', error, opts);
}
function syncLinkWidths() {
var $box = $('#rmLogBox');
if (!$box.length) return;
var taW = $('#rmMsg,#nominationReason,#rmReportText').first().outerWidth() || 0;
$box.css({ width: taW ? taW + 'px' : '', 'max-width': '100%' });
}
function appendNominationLink(pageTitle, sectionTitle) {
if (!pageTitle) return;
var url = getPageUrl(pageTitle);
var frag = normalizeSectionForLink(sectionTitle);
if (frag) url += '#' + ((mw.util && mw.util.escapeIdForLink) ? mw.util.escapeIdForLink(frag) : frag.replace(/\s+/g, '_'));
var label = normTitle(frag ? pageTitle + '#' + frag : pageTitle);
var $target = $('#rmLogBox').length ? $('#rmLogBox') : $('#removerModalContent');
$target.append(
'<div style="margin-top:4px;line-height:1.4;word-break:break-word;overflow-wrap:anywhere;display:flex;align-items:baseline;gap:5px;">' +
'<span style="color:' + tk.bgSucc + ';font-size:14px;flex-shrink:0;">✓</span>' +
'<a href="' + escapeHtml(url) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' + escapeHtml(label) + '</a></div>'
);
}
function getCurrentSettings() {
return normalizeRemoverSettings(state.settings || settingsDefaults);
}
function shouldAutoReloadAfterAction() {
return !!getCurrentSettings().autoReloadAfterAction;
}
function maybeOpenNominationDiscussionInNewTab(nominationInfo) {
if (!getCurrentSettings().openNominationDiscussionInNewTab || !nominationInfo || !nominationInfo.pageTitle) return;
window.open(getPageUrlWithFragment(nominationInfo.pageTitle, nominationInfo.sectionTitle), '_blank', 'noopener,noreferrer');
}
// ─── UI-строители ────────────────────────────────────────────────────────
function buildInfoBoxHtml(mainText, detailsText, isErr) {
var cls = isErr ? ' class="error"' : '';
return joinHtml([
'<div class="rmInfoBox">',
'<p', cls, ' style="margin:0', detailsText ? ' 0 6px' : '', ';">', mainText, '</p>',
detailsText ? '<p style="margin:0;color:' + tk.cSubM + ';">' + detailsText + '</p>' : '',
'</div>'
]);
}
function buildActionsHtml(actions, inputName, listId) {
var actionItemsHtml = actions.map(function (a, i) {
var meta = a.description || (a.talkNotice ? 'С добавлением {{' + (a.talkTemplate || a.resultTemplate || 'шаблон') + '}} на СО.' : '');
var tagHtml = a.tag
? '<span style="display:inline-block;font-size:13px;font-weight:600;padding:2px 7px;border-radius:3px;background:' + tk.bgN + ';color:' + tk.cSubM + ';margin-right:8px;white-space:nowrap;vertical-align:middle;">' + escapeHtml(a.tag) + '</span>'
: '';
return joinHtml([
'<label class="rmActionItem">',
'<span class="rmActionMain">',
'<input type="radio" name="', inputName, '" value="', a.id, '" ', i === 0 ? 'checked' : '', '>',
tagHtml,
'<span>', a.label, '</span>',
'</span>',
meta ? '<span class="rmActionMeta">' + renderTemplateLinksInText(meta) + '</span>' : '',
a.warning ? '<strong class="rmActionWarning">' + escapeHtml(a.warning) + '</strong>' : '',
'</label>'
]);
}).join('');
return joinHtml([
'<div style="margin:0 0 8px;color:', tk.cSubM, ';font-size:13px;">Обнаружены открытые номинации:</div>',
'<div', listId ? ' id="' + listId + '"' : '', ' class="rmActionList">',
actionItemsHtml,
'</div>'
]);
}
function syncActionWarnings(inputName, listId) {
var $root = listId ? $('#' + listId) : $('#removerModal');
var $selected = $root.find('input[name="' + inputName + '"]:checked').closest('.rmActionItem');
$root.find('.rmActionWarning').hide();
$selected.find('.rmActionWarning').show();
}
function buildNestedCommentFieldsHtml(opts) {
var options = opts || {};
var wrapId = options.wrapId || '';
var textareaId = options.textareaId || '';
var textareaClass = options.textareaClass ? ' ' + options.textareaClass : '';
var textareaStyleExtra = options.textareaStyleExtra || '';
var wrapStyleExtra = options.wrapStyleExtra || '';
var placeholder = options.placeholder || 'Комментарий (необязательно)';
var beforeHtml = options.beforeHtml || '';
var marginTop = options.marginTop || '6px';
var minHeight = parseInt(options.minHeight, 10) || 90;
var isEmbedded = !!options.embedded;
var wrapClass = isEmbedded ? '' : (' class="' + RESIZE_CLASS + '"');
var wrapStyle = 'display:none;margin-top:' + marginTop + ';max-width:100%;box-sizing:border-box;';
if (isEmbedded) {
wrapStyle += 'padding:0;border:0;background:transparent;';
} else {
wrapStyle += 'padding:8px 10px;border:1px solid ' + tk.bSubS + ';border-radius:6px;background:' + tk.bgNSub + ';';
}
wrapStyle += wrapStyleExtra;
return joinHtml([
'<div id="', wrapId, '"', wrapClass, ' style="', wrapStyle, '">',
beforeHtml,
'<textarea id="', textareaId, '" class="rmNestedCommentInput', textareaClass,
'" placeholder="', escapeHtml(placeholder), '" style="', stInputFull,
'min-height:', minHeight, 'px;resize:both;margin-bottom:6px;', textareaStyleExtra, '"></textarea>',
buildQuickPhrasesPanelHtml(textareaId),
'</div>'
]);
}
function buildConditionalRetFieldsHtml() {
return buildNestedCommentFieldsHtml({
wrapId: 'rmCloseConditionalWrap',
textareaId: 'rmCloseConditionalReason',
placeholder: 'Условие / пояснение (необязательно)',
marginTop: '8px',
minHeight: 90,
beforeHtml: '<input id="rmCloseConditionalDeadline" type="text" placeholder="Срок доработки: 2026-05-31" style="' + stInputFull + 'margin-bottom:6px;">'
});
}
function buildAddMultiPageButtonHtml(options) {
var opts = options || {};
var title = opts.addTitle || 'Мультиноминация: добавить страницу';
return buildSquareAddButtonHtml('', title, 'rmAddMultiPage rmAddPageBtn');
}
function buildSquareAddButtonHtml(id, title, className, symbol) {
var idAttr = id ? ' id="' + id + '"' : '';
var clsAttr = className ? ' class="' + className + '"' : '';
var label = title || 'Добавить';
return '<button' + idAttr + ' type="button"' + clsAttr + ' title="' + escapeHtml(label) + '" aria-label="' + escapeHtml(label) + '" style="' + stRemoveBtn + '">' + escapeHtml(symbol || '+') + '</button>';
}
function leftControlStyle(style) {
return style.replace('margin-left:' + inlineControlGap + 'px;', 'margin-left:0;margin-right:' + inlineControlGap + 'px;');
}
function buildLeftSquareAddButtonHtml(id, title, className, symbol) {
return buildSquareAddButtonHtml(id, title, className, symbol).replace(stRemoveBtn, leftControlStyle(stRemoveBtn));
}
function buildLeftRemoveButtonHtml(className, title) {
var cls = className || 'rmRemoveInput';
var label = title || 'Удалить';
return '<button type="button" class="' + cls + '" style="' + leftControlStyle(stRemoveBtn) + '" title="' + escapeHtml(label) + '" aria-label="' + escapeHtml(label) + '">−</button>';
}
function buildMultiRenameVariantAddButtonHtml() {
return buildLeftSquareAddButtonHtml('', 'Добавить вариант нового заголовка', 'rmAddMultiRenameVariant rmAddVariantBtn rmRenameVariantAddBtn', '⤷');
}
function buildStartMultiPageButtonHtml(title) {
var label = title || 'Мультиноминация: добавить страницу';
return buildSquareAddButtonHtml('', label, 'rmAddMultiPage rmAddPageBtn rmStartMultiPageBtn');
}
function buildMultiPageButtonsHtml(commentWrapId, commentId, options) {
var opts = options || {};
var commentBtnStyle = stToolBtn + (opts.showComment ? '' : 'display:none;');
var commentTitle = opts.commentTitle || 'Добавить комментарий к этой странице';
var commentExpandedTitle = opts.commentExpandedTitle || 'Скрыть комментарий к этой странице';
if (opts.showAdd) return buildAddMultiPageButtonHtml(opts);
return joinHtml([
'<button type="button" class="rmToggleBtn rmMultiPageCommentToggle" data-rm-comment-wrap="', commentWrapId,
'" data-rm-comment-textarea="', commentId,
'" data-rm-comment-title="', escapeHtml(commentTitle),
'" data-rm-comment-expanded-title="', escapeHtml(commentExpandedTitle),
'" aria-label="', escapeHtml(commentTitle), '" title="', escapeHtml(commentTitle), '" aria-expanded="false" style="', commentBtnStyle, '">✎</button>',
'<button type="button" class="rmRemoveInput" style="', stRemoveBtn, '" title="', escapeHtml(opts.removeTitle || 'Убрать страницу из номинации'), '">−</button>'
]);
}
function buildMultiRenameVariantRowHtml(value, options) {
var opts = options || {};
return joinHtml([
'<div class="rmMultiRenameVariantRow" style="', stRow.replace('margin-bottom:6px;', 'margin-bottom:0;'), '">',
buildLeftRemoveButtonHtml('rmRemoveMultiRenameVariant', 'Убрать вариант нового заголовка'),
'<input type="text" class="rmMultiRenameVariantInput" placeholder="', escapeHtml(opts.placeholder || 'Дополнительный вариант нового заголовка'),
'" style="', stInputBox, '"', value ? ' value="' + escapeHtml(value) + '"' : '', '>',
'</div>'
]);
}
function buildMultiPageRowHtml(index, options) {
var opts = options || {};
var pageInputId = 'rmMultiPage' + index;
var commentWrapId = 'rmMultiPageCommentWrap' + index;
var commentId = 'rmMultiPageComment' + index;
var pageValue = opts.pageValue || '';
var pageValueAttr = pageValue ? ' value="' + escapeHtml(pageValue) + '"' : '';
var inputPlaceholder = opts.inputPlaceholder || 'Страница';
var targetInputClass = opts.targetInputClass || '';
var targetInputHtml = '';
var commentPlaceholder = opts.commentPlaceholder || 'Комментарий только для этой страницы (необязательно)';
var commentIndent = opts.targetVariants ? leftNestedControlOffset : '0';
var pageRowStyle = stRow.replace('margin-bottom:6px;', 'margin-bottom:0;');
var blockStyle = 'max-width:100%;box-sizing:border-box;';
var buttonsHtml = buildMultiPageButtonsHtml(commentWrapId, commentId, {
showAdd: !!opts.showAdd,
showComment: !!opts.showComment,
addTitle: opts.addTitle,
removeTitle: opts.removeTitle,
commentTitle: opts.commentTitle,
commentExpandedTitle: opts.commentExpandedTitle
});
if (opts.targetInput) {
targetInputHtml = joinHtml([
'<input id="rmMultiPageTarget', index, '" type="text" placeholder="', escapeHtml(opts.targetPlaceholder || 'Новое название'),
'" class="rmMultiPageTargetInput ', escapeHtml(targetInputClass), '" style="', stInputBox, '">'
]);
}
return joinHtml([
'<div class="rmMultiPageBlock ', RESIZE_CLASS, '" style="', blockStyle, '">',
'<div', opts.rowId ? ' id="' + opts.rowId + '"' : '', ' class="rmMultiPageRow" style="', pageRowStyle, '">',
'<input id="', pageInputId, '" type="text" placeholder="', escapeHtml(inputPlaceholder), '" class="rmMultiPageInput" style="', stInputBox, '"', pageValueAttr, '>',
opts.targetVariants ? '' : targetInputHtml,
buttonsHtml,
'</div>',
opts.targetVariants ? '<div class="rmMultiRenameVariantsContainer"><div class="rmMultiRenamePrimaryTargetRow">' + buildMultiRenameVariantAddButtonHtml() + targetInputHtml + '</div></div>' : '',
buildNestedCommentFieldsHtml({
wrapId: commentWrapId,
textareaId: commentId,
textareaClass: 'rmMultiPageCommentInput',
placeholder: commentPlaceholder,
marginTop: multiNominationGap,
minHeight: 90,
embedded: true,
wrapStyleExtra: 'padding:0 0 0 ' + commentIndent + ';background:transparent;',
textareaStyleExtra: 'border-color:' + tk.bSubS + ';border-radius:4px;background:' + tk.bgBase + ';'
}),
'</div>'
]);
}
function buildSingleRenameBlockHtml(config, startMultiTitle, currentPageName, currentPlaceholder) {
var addLabel = 'Добавить вариант нового заголовка';
var currentValue = currentPageName || '';
return joinHtml([
'<div id="rmSingleRenameBlock" style="display:flex;flex-direction:column;align-items:stretch;gap:0;">',
'<div class="rmInputRow ', RESIZE_CLASS, '" style="', stRow, '">',
'<input id="rmSingleRenameCurrent" type="text" class="rmSingleRenameCurrentInput" placeholder="', escapeHtml(currentPlaceholder || 'Текущее название'), '" style="', stInputBox, '" value="', escapeHtml(currentValue), '">',
buildStartMultiPageButtonHtml(startMultiTitle),
'</div>',
'<div class="rmInputRow ', RESIZE_CLASS, ' rmRenameVariantRow" style="', stRow, '">',
buildLeftSquareAddButtonHtml(config.addBtnId, addLabel.replace(/^\+\s*/, '') || 'Добавить вариант', 'rmAddVariantBtn rmRenameVariantAddBtn', '⤷'),
'<input id="', config.firstId, '" type="text" class="', config.inputClass || 'variantInput', '" placeholder="', config.firstPh || '', '" style="', stInputBox, '">',
'</div>',
'<div id="', config.containerId, '"></div>',
'</div>'
]);
}
function showInfoAndClose(mainText, detailsText, isErr) {
$('#removerModalContent').html(buildInfoBoxHtml(mainText, detailsText || '', isErr || false));
renderModalFooter('close');
}
function getSelectedAction(inputName, actionMap) {
var id = $('[name="' + inputName + '"]:checked').val();
var sel = actionMap[id];
if (!sel) alert('Выберите действие.');
return sel || null;
}
function prependTemplateToNoinclude(text, templateText) {
var source = String(text || '');
var tpl = String(templateText || '').trim();
if (!tpl) return source;
var match = source.match(RE_NOINCLUDE);
if (match) {
var before = source.slice(0, source.indexOf(match[0]));
if (/\S/.test(before)) return '<noinclude>' + tpl + '</noinclude>\n' + source;
var content = String(match[2] || '').replace(/^\n+/, '');
return source.replace(match[0], match[1] + '<noinclude>' + tpl + (content ? '\n' + content : '') + '\n</noinclude>');
}
return '<noinclude>' + tpl + '</noinclude>\n' + source;
}
function buildGeneratedNominationTemplateText(job, pg) {
var tplStr = '';
if (!job) return '';
if (job.opId === 'fRm') {
if (job.kbuTemplateText) return T_OPEN + job.kbuTemplateText + T_CLOSE;
return job.kbuTemplate ? (T_OPEN + job.kbuTemplate + T_CLOSE) : '';
}
if (typeof job.articleTpl !== 'function') return '';
tplStr = job.articleTpl(job.opId === 'mRnm' ? buildRenameTemplateParam(getMultiRenameTarget(job, pg, 'multiRenameTemplateTargets')) : job.tplpar, job.date[0]);
if (job.opId === 'merge' && job.tplpar) {
tplStr = (job.op.nomination.articleTpl)(
('|' + job.tplpar + '|').replace('|' + pg + '|', '|').slice(1, -1),
job.date[0]
);
}
return tplStr ? (T_OPEN + tplStr + T_CLOSE) : '';
}
function applyConflictTemplateResolution(articleText, job, pg, decision) {
var rule = getNominationConflictRule(job);
var generatedTemplate = buildGeneratedNominationTemplateText(job, pg);
var source = String(articleText || '');
if (!generatedTemplate || !decision) return source;
if (decision.templateAction === 'overwrite') {
var cleaned = rule ? stripTemplatesByPattern(source, rule.namePattern).text : source;
return prependTemplateToNoinclude(cleaned, generatedTemplate);
}
if (decision.templateAction === 'prepend') return prependTemplateToNoinclude(source, generatedTemplate);
return source;
}
function inspectMultiNominationConflicts(job, callback) {
var cb = callback || function () {};
var pages = (job && job.multiArticles) ? job.multiArticles.slice() : [];
var conflicts = [];
var statusId = logStatus('Проверяются статьи на наличие уже установленных шаблонов...', null, { pending: true, trackError: false });
if (!pages.length) {
logStatus('Проверка завершена: конфликтов не найдено.', null, { statusId: statusId, trackError: false });
cb(null, conflicts);
return;
}
eachSequential(pages, function (pg, next) {
getText(pg, function (articleText, readErr) {
var conflict;
if (readErr) { next(makeReadError(readErr, 'read_failed', 'Не удалось проверить страницу «' + pg + '».')); return; }
if (articleText === null) { next({ code: 'read_failed', info: 'Страница «' + pg + '» не существует.' }); return; }
conflict = detectNominationConflict(articleText, job);
if (conflict) {
conflicts.push($.extend({ pageName: pg }, conflict));
logStatus('В статье ' + buildQuotedStatusPageLink(pg) + ' обнаружен установленный шаблон <code>' + escapeHtml(conflict.templateDisplay || conflict.templateName || conflict.label || 'шаблон') + '</code>.', null, { trackError: false });
}
next();
});
}, function (err) {
if (err) {
logStatus('Проверка статей.', err, { statusId: statusId });
cb(err);
return;
}
logStatus(
conflicts.length
? 'Проверка завершена: найдены статьи с уже установленными шаблонами.'
: 'Проверка завершена: конфликтов не найдено.',
null,
{ statusId: statusId, trackError: false }
);
cb(null, conflicts);
});
}
function buildNominationConflictResolutionHtml(conflicts) {
return '<div class="rmInfoBox"><p style="margin:0 0 6px;">Найдены статьи, где шаблон уже стоит. Для каждой конфликтующей страницы выберите, что делать со статьёй и с шаблоном.</p>' +
'<p style="margin:0;color:' + tk.cSubM + ';font-size:12px;line-height:1.45;">По умолчанию такие статьи исключаются из новой номинации, а существующий шаблон остаётся без изменений.</p></div>' +
'<div class="rmConflictLead">После выбора нажмите «Продолжить номинирование».</div>' +
'<div id="rmConflictList" class="rmConflictList">' +
conflicts.map(function (conflict, index) {
var pageLink = buildStatusPageLink(conflict.pageName);
return '<div class="rmConflictCard" data-rm-conflict-index="' + index + '">' +
'<input type="hidden" class="rmConflictPageAction" value="skip">' +
'<input type="hidden" class="rmConflictTemplateAction" value="keep">' +
'<div class="rmConflictTitle">' + pageLink + '</div>' +
'<div class="rmConflictMeta">Обнаружен установленный шаблон <code>' + escapeHtml(conflict.templateDisplay || conflict.templateName || conflict.label || 'шаблон') + '</code>.</div>' +
'<div class="rmConflictGroup">' +
'<div class="rmConflictGroupTitle">Действие со статьёй</div>' +
'<div class="rmConflictButtons" data-rm-conflict-group="page">' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice is-active" data-rm-choice-type="page" data-rm-choice="skip" aria-pressed="true">Убрать из номинации</button>' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice" data-rm-choice-type="page" data-rm-choice="keep" aria-pressed="false">Оставить в номинации</button>' +
'</div></div>' +
'<div class="rmConflictGroup">' +
'<div class="rmConflictGroupTitle">Действие с шаблоном</div>' +
'<div class="rmConflictButtons" data-rm-conflict-group="template">' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice is-active" data-rm-choice-type="template" data-rm-choice="keep" aria-pressed="true">Оставить как есть</button>' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice" data-rm-choice-type="template" data-rm-choice="overwrite" aria-pressed="false">Новая дата</button>' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice" data-rm-choice-type="template" data-rm-choice="prepend" aria-pressed="false">Второй сверху</button>' +
'</div>' +
'<div class="rmConflictHint">Если статья исключается из номинации, действие с шаблоном не применяется.</div>' +
'</div></div>';
}).join('') +
'</div>';
}
function updateNominationConflictCardState($card) {
var pageAction = $card.find('.rmConflictPageAction').val() || 'skip';
var disableTemplate = pageAction !== 'keep';
var $templateButtons = $card.find('[data-rm-choice-type="template"]');
$card.toggleClass('is-skip', disableTemplate);
$card.find('[data-rm-conflict-group="template"]').toggleClass('is-disabled', disableTemplate);
$templateButtons.prop('disabled', disableTemplate).toggleClass('is-disabled', disableTemplate);
}
function bindNominationConflictResolutionUi() {
var $content = $('#removerModalContent');
function setChoice($card, type, value) {
var inputClass = type === 'page' ? '.rmConflictPageAction' : '.rmConflictTemplateAction';
$card.find(inputClass).val(value);
$card.find('[data-rm-choice-type="' + type + '"]').each(function () {
var isActive = $(this).data('rmChoice') === value;
$(this).toggleClass('is-active', isActive).attr('aria-pressed', isActive ? 'true' : 'false');
});
updateNominationConflictCardState($card);
}
$content.off('.rmConflictResolution').on('click.rmConflictResolution', '[data-rm-choice-type]', function () {
var $btn = $(this);
var $card = $btn.closest('.rmConflictCard');
if ($btn.prop('disabled')) return;
setChoice($card, $btn.data('rmChoiceType'), $btn.data('rmChoice'));
});
$('#rmConflictList .rmConflictCard').each(function () { updateNominationConflictCardState($(this)); });
}
function collectNominationConflictResolution(conflicts) {
var decisions = {};
(conflicts || []).forEach(function (conflict, index) {
var $card = $('#rmConflictList .rmConflictCard[data-rm-conflict-index="' + index + '"]');
decisions[normTitle(conflict.pageName)] = {
pageAction: $card.find('.rmConflictPageAction').val() || 'skip',
templateAction: $card.find('.rmConflictTemplateAction').val() || 'keep'
};
});
return decisions;
}
function applyNominationConflictResolutionToJob(job, decisions) {
var resultArticles;
var headerText;
if (!job || !job.isMulti) {
job.conflictDecisions = decisions || {};
return { value: job };
}
resultArticles = (job.multiArticles || []).filter(function (pageName) {
var decision = decisions && decisions[normTitle(pageName)];
return !decision || decision.pageAction !== 'skip';
});
if (!resultArticles.length) return { error: 'После исключения конфликтующих статей в номинации не осталось ни одной страницы.' };
job.conflictDecisions = decisions || {};
job.multiArticles = resultArticles.slice();
job.pages = resultArticles.slice().reverse();
if (job.multiRenamePairs && job.multiRenamePairs.length) {
job.multiRenamePairs = job.multiRenamePairs.filter(function (pair) {
return pair && resultArticles.indexOf(pair.pageName) !== -1;
});
}
if (job.multiRenameTargets) {
job.multiRenameTargets = resultArticles.reduce(function (map, pageName) {
map[normTitle(pageName)] = getMultiRenameTarget(job, pageName, 'multiRenameTargets');
return map;
}, {});
}
if (job.multiRenameTemplateTargets) {
job.multiRenameTemplateTargets = resultArticles.reduce(function (map, pageName) {
map[normTitle(pageName)] = getMultiRenameTarget(job, pageName, 'multiRenameTemplateTargets');
return map;
}, {});
}
headerText = String(job.multiHeaderText || '').trim();
job.section = headerText || (job.opId === 'mRnm' ? formatRenameItemsWithAnd(resultArticles, job.multiRenameTargets) : ('[[:' + resultArticles[0] + ']]'));
job.sectionNW = job.section.replace(/\[\[:/g, '').replace(/]]/g, '');
job.msg = job.multiNominationFormat === 'list'
? buildMultiNominationListText(resultArticles, job.multiNominationBody, job.multiArticleComments, job.opId === 'mRnm' ? getMultiRenameDiscussionOptions(job.multiRenameTargets) : null)
: buildMultiNominationText(resultArticles, job.multiNominationBody, job.multiArticleComments, job.opId === 'mRnm'
? getMultiRenameDiscussionOptions(job.multiRenameTargets, { leadingBlankLine: false })
: { leadingBlankLine: false });
job.summary = makeNominationTargetSummary(job.nomPage, job.sectionNW, job.summaryAction);
job.nominationSummary = makeNominationPageSummary(job.sectionNW);
return { value: job };
}
function showNominationConflictResolution(job, conflicts, onContinue) {
resetModalObservers();
$('#removerModalContent').html(buildNominationConflictResolutionHtml(conflicts));
bindNominationConflictResolutionUi();
syncLinkWidths();
renderModalFooter('submit', {
submitText: 'Продолжить номинирование',
showSubscribe: true,
preserveLogOnSubmit: true,
onSubmit: function () {
var decisions = collectNominationConflictResolution(conflicts);
var applied = applyNominationConflictResolutionToJob(job, decisions);
if (applied.error) {
alert(applied.error);
return false;
}
if (typeof onContinue === 'function') onContinue(applied.value);
return true;
}
});
}
function bindTouchTextareaGrip($ta, sync, getMaxWidth, options) {
var opts = options || {};
var minWidth = parseInt(opts.minWidth, 10) || parseInt(sz.taMinW, 10) || 180;
var minHeight = parseInt(opts.minHeight, 10) || parseInt(sz.taMinH, 10) || 100;
var allowWidthResize = opts.allowWidth !== false;
var syncFn = typeof sync === 'function' ? sync : function () {};
var getMaxWidthFn = typeof getMaxWidth === 'function' ? getMaxWidth : function () { return $ta.outerWidth() || minWidth; };
var usePointerEvents = typeof window.PointerEvent === 'function';
var dragState = { active: false, startX: 0, startY: 0, startWidth: 0, startHeight: 0 };
var gripStyle = 'height:20px;box-sizing:border-box;border:1px solid ' + tk.bSub + ';border-top:0;border-radius:0 0 4px 4px;background:' + tk.bgNSub + ';display:flex;align-items:center;justify-content:center;cursor:ns-resize;touch-action:none;user-select:none;-webkit-user-select:none;';
if (opts.gripMarginBottom) gripStyle += 'margin-bottom:' + opts.gripMarginBottom + ';';
var $grip = $('<div data-rm-textarea-grip="1" style="' + gripStyle + '"><span style="display:block;width:42px;height:4px;border-radius:999px;background:' + tk.bSub + ';opacity:.9;"></span></div>');
function getCoord(evt, key) {
var e = evt.originalEvent || evt;
if (e.touches && e.touches.length) return e.touches[0][key];
if (e.changedTouches && e.changedTouches.length) return e.changedTouches[0][key];
return e[key];
}
function stopDrag() {
dragState.active = false;
$(window).off('.rmTaResize');
}
function onDragMove(evt) {
var clientX;
var clientY;
if (!dragState.active) return;
clientX = getCoord(evt, 'clientX');
clientY = getCoord(evt, 'clientY');
if (typeof clientY !== 'number') return;
if (allowWidthResize && typeof clientX === 'number') $ta.css('width', Math.max(minWidth, Math.min(getMaxWidthFn(), dragState.startWidth + (clientX - dragState.startX))) + 'px');
$ta.css('height', Math.max(minHeight, dragState.startHeight + (clientY - dragState.startY)) + 'px');
syncFn();
if (evt.preventDefault) evt.preventDefault();
}
function startDrag(evt) {
var clientY = getCoord(evt, 'clientY');
if (typeof clientY !== 'number') return;
dragState.active = true;
dragState.startX = getCoord(evt, 'clientX') || 0;
dragState.startY = clientY;
dragState.startWidth = $ta.outerWidth();
dragState.startHeight = $ta.outerHeight();
if (evt.preventDefault) evt.preventDefault();
$(window).off('.rmTaResize');
if (usePointerEvents) {
$(window).on('pointermove.rmTaResize', onDragMove).on('pointerup.rmTaResize pointercancel.rmTaResize', stopDrag);
} else {
$(window).on('touchmove.rmTaResize mousemove.rmTaResize', onDragMove).on('touchend.rmTaResize touchcancel.rmTaResize mouseup.rmTaResize', stopDrag);
}
}
$ta.css({ 'border-bottom-left-radius': '0', 'border-bottom-right-radius': '0' });
$ta.next('[data-rm-textarea-grip]').remove();
$ta.after($grip);
if (usePointerEvents) $grip.on('pointerdown.rmTaGrip', startDrag);
else $grip.on('touchstart.rmTaGrip mousedown.rmTaGrip', startDrag);
}
function applyModalContentWidth($modal, contentWidth, options) {
var opts = options || {};
var layout = getModalLayout();
var modalFrame = getBoxFrameWidth($modal);
var safeContentWidth = Math.max(layout.minWidth, Math.min(contentWidth, layout.maxOuterWidth - modalFrame));
var modalWidth = safeContentWidth + modalFrame;
var initialContentW = parseFloat($modal.data('rmInitialContentW')) || 0;
$modal.css({
width: modalWidth + 'px',
'max-width': layout.maxOuterWidth + 'px',
'box-sizing': 'border-box',
'margin-left': layout.shouldCenter ? 'auto' : '0',
'margin-right': layout.shouldCenter ? 'auto' : '0'
}).toggleClass('rmCompactContent', safeContentWidth < 520);
$('.' + RESIZE_CLASS).css({
width: safeContentWidth + 'px',
'max-width': '100%',
'box-sizing': 'border-box'
});
$('#rmMsg,#nominationReason,#rmReportText').each(function () {
var $textarea = $(this);
var textareaId = this.id;
if (!$textarea.length) return;
$textarea.css('width', safeContentWidth + 'px');
$textarea.next('[data-rm-textarea-grip]').css('width', safeContentWidth + 'px');
$('.rmQuickPhrasesPanel[data-rm-target="' + textareaId + '"]').css('width', safeContentWidth + 'px');
});
$('.rmNestedCommentInput').each(function () {
var $textarea = $(this);
var $wrap = $textarea.parent();
var containerFrame = parseFloat($textarea.data('rmNestedContainerFrame')) || 0;
var wrapFrame = parseFloat($textarea.data('rmNestedWrapFrame')) || 0;
var safeMinWidth = parseFloat($textarea.data('rmNestedMinWidth')) || 0;
var wrapOuterWidth;
var textareaWidth;
if (!$wrap.length || !$wrap.is(':visible')) return;
wrapOuterWidth = Math.max(1, safeContentWidth - containerFrame);
textareaWidth = Math.max(1, wrapOuterWidth - wrapFrame);
$wrap.css({
width: wrapOuterWidth + 'px',
'max-width': '100%',
'box-sizing': 'border-box'
});
$textarea.css({
width: textareaWidth + 'px',
'min-width': Math.min(safeMinWidth || textareaWidth, textareaWidth) + 'px'
});
$textarea.next('[data-rm-textarea-grip]').css('width', textareaWidth + 'px');
$('.rmQuickPhrasesPanel[data-rm-target="' + this.id + '"]').css('width', textareaWidth + 'px');
});
syncLinkWidths();
if (isVector22) {
var $content = $('#content');
if ($content.length && modalWidth > initialContentW) $content.css({ 'min-width': modalWidth + 'px' });
else if ($content.length) $content.css({ 'min-width': '' });
} else {
$('#content').css({ 'min-width': '' });
}
if (!opts.skipStore) $modal.data('rmContentWidth', safeContentWidth);
return safeContentWidth;
}
function setupNestedResizableTextarea(textareaId, wrapId, minWidth, minHeight) {
var $ta = $('#' + textareaId);
var $wrap = $('#' + wrapId);
var $modal = $('#removerModal');
var $container = $wrap.parent();
var layout = getModalLayout();
var safeMinWidth = parseInt(minWidth, 10) || 280;
var safeMinHeight = parseInt(minHeight, 10) || 90;
var initialWidth;
var modalFrame;
var containerFrame;
var wrapFrame;
function px($el, prop) { var n = parseFloat($el.css(prop)); return isNaN(n) ? 0 : n; }
function getMaxTextareaWidth(currentLayout) {
return Math.max(1, currentLayout.maxOuterWidth - modalFrame - containerFrame - wrapFrame);
}
function getEffectiveMinWidth(currentLayout) {
return Math.min(safeMinWidth, getMaxTextareaWidth(currentLayout));
}
function sync() {
var currentLayout = getModalLayout();
var maxTextareaWidth = getMaxTextareaWidth(currentLayout);
var textareaWidth = $ta.outerWidth();
var contentWidth;
if (!$wrap.is(':visible')) return;
if (textareaWidth > maxTextareaWidth) {
$ta.css('width', maxTextareaWidth + 'px');
textareaWidth = $ta.outerWidth();
}
contentWidth = Math.min(currentLayout.maxOuterWidth - modalFrame, textareaWidth + wrapFrame + containerFrame);
applyModalContentWidth($modal, contentWidth);
}
if (!$ta.length || !$wrap.length || !$modal.length) return;
modalFrame = px($modal, 'padding-left') + px($modal, 'padding-right') + px($modal, 'border-left-width') + px($modal, 'border-right-width');
containerFrame = $container.length
? px($container, 'padding-left') + px($container, 'padding-right') + px($container, 'border-left-width') + px($container, 'border-right-width')
: 0;
wrapFrame = px($wrap, 'padding-left') + px($wrap, 'padding-right') + px($wrap, 'border-left-width') + px($wrap, 'border-right-width');
initialWidth = Math.min(
Math.max(getEffectiveMinWidth(layout), getDefaultResizableWidth(modalFrame + containerFrame + wrapFrame)),
getMaxTextareaWidth(layout)
);
$ta.css({
width: initialWidth + 'px',
'min-width': getEffectiveMinWidth(layout) + 'px',
'min-height': safeMinHeight + 'px',
'box-sizing': 'border-box',
resize: layout.useFullWidth ? 'none' : 'both',
'border-bottom-left-radius': '',
'border-bottom-right-radius': ''
});
$ta.data('rmNestedContainerFrame', containerFrame);
$ta.data('rmNestedWrapFrame', wrapFrame);
$ta.data('rmNestedMinWidth', safeMinWidth);
$ta.next('[data-rm-textarea-grip]').remove();
if (layout.useFullWidth) {
bindTouchTextareaGrip($ta, sync, function () {
return getMaxTextareaWidth(getModalLayout());
}, {
minWidth: getEffectiveMinWidth(layout),
minHeight: safeMinHeight
});
}
registerModalLayoutSync(sync);
$(window).off('resize.removerModal').on('resize.removerModal', syncModalLayout);
if (typeof ResizeObserver === 'function') {
var observer = new ResizeObserver(sync);
observer.observe($ta[0]);
registerResizeObserver(observer);
}
sync();
}
function setupResizableModal(textareaId) {
var $ta = $('#' + textareaId);
var $modal = $('#removerModal');
var layout = getModalLayout();
function px($el, prop) { var n = parseFloat($el.css(prop)); return isNaN(n) ? 0 : n; }
applyV2022Layout($modal);
$modal.css({ display: 'block', 'margin-left': layout.shouldCenter ? 'auto' : '0', 'margin-right': layout.shouldCenter ? 'auto' : '0' });
var isBorderBox = ($modal.css('box-sizing') || '').toLowerCase() === 'border-box';
var hFrame = px($modal, 'padding-left') + px($modal, 'padding-right') + px($modal, 'border-left-width') + px($modal, 'border-right-width');
var initialContentW = isVector22 ? ($('#content').outerWidth() || 0) : 0;
var minWidth = layout.minWidth;
$modal.data('rmInitialContentW', initialContentW);
$ta.css({ width: getDefaultResizableWidth(hFrame) + 'px', height: sz.taH, padding: '8px', 'box-sizing': 'border-box',
border: '1px solid ' + tk.bSub, 'border-radius': '2px', background: tk.bgBase,
color: 'inherit', resize: layout.useFullWidth ? 'none' : 'both', 'min-height': sz.taMinH, 'min-width': sz.taMinW });
$(window).off('.rmTaResize');
function sync() {
var currentLayout = getModalLayout();
var maxTextareaWidth = Math.max(minWidth, currentLayout.maxOuterWidth - Math.floor(hFrame));
var w = $ta.outerWidth();
if (w > maxTextareaWidth) {
$ta.css('width', maxTextareaWidth + 'px');
w = $ta.outerWidth();
}
applyModalContentWidth($modal, isBorderBox ? w : Math.min(currentLayout.maxOuterWidth - hFrame, w));
}
if (layout.useFullWidth) bindTouchTextareaGrip($ta, sync, function () {
return Math.max(minWidth, getModalLayout().maxOuterWidth - Math.floor(hFrame));
});
else {
$ta.css({ 'border-bottom-left-radius': '', 'border-bottom-right-radius': '' });
$ta.next('[data-rm-textarea-grip]').remove();
}
registerModalLayoutSync(sync);
$(window).off('resize.removerModal').on('resize.removerModal', syncModalLayout);
if (typeof ResizeObserver === 'function') {
var observer = new ResizeObserver(sync);
observer.observe($ta[0]);
registerResizeObserver(observer);
}
sync();
}
function addInputRow(opts) {
var w = $('#rmMsg,#nominationReason,#rmReportText').first().outerWidth() || 0;
var indentLeft = Math.max(0, parseInt(opts.indentLeft, 10) || 0);
var rowClass = opts.rowClass ? ' ' + opts.rowClass : '';
var widthStyle = opts.fitParentWidth
? 'width:calc(100% - ' + indentLeft + 'px);box-sizing:border-box;'
: (opts.autoWidth ? '' : (w ? 'width:' + Math.max(1, w - indentLeft) + 'px;' : ''));
$('#' + opts.containerId).append(joinHtml([
'<div class="rmInputRow ', RESIZE_CLASS, rowClass, '" style="', stRow, widthStyle, indentLeft ? 'margin-left:' + indentLeft + 'px;' : '', '">',
opts.prefixHtml || '',
'<input type="text" class="', opts.inputClass || 'variantInput', '" placeholder="', opts.placeholder || '', '" style="', stInputBox, '">',
opts.removeBeforeInput ? '' : '<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить">−</button>',
'</div>'
]));
syncModalLayout();
}
$(document).off('click.rmRemoveInput').on('click.rmRemoveInput', '.rmRemoveInput', function () {
$(this).closest('.rmInputRow').remove();
syncModalLayout();
});
$(document).off('click.rmQuickPhraseInsert').on('click.rmQuickPhraseInsert', '.rmQuickPhraseActionBtn', function (e) {
var targetId;
var phrase;
e.preventDefault();
targetId = $(this).data('rmTarget');
phrase = $(this).attr('data-rm-phrase') || '';
if (!targetId) return;
insertTextIntoTextarea($('#' + targetId), phrase);
});
function buildMultiInputHtml(c) {
var addLabel = c.addBtnLabel || '+ Добавить';
var addClass = c.type === 'rename' ? 'rmAddVariantBtn rmRenameVariantAddBtn' : '';
var addSymbol = c.type === 'rename' ? '⤷' : '+';
return joinHtml([
'<div class="rmInputRow ', RESIZE_CLASS, '" style="', stRow, '">',
'<input id="', c.firstId, '" type="text" class="', c.inputClass || 'variantInput', '" placeholder="', c.firstPh || '', '" style="', stInputBox, '">',
buildSquareAddButtonHtml(c.addBtnId, addLabel.replace(/^\+\s*/, '') || 'Добавить', addClass, addSymbol),
'</div>',
'<div id="', c.containerId, '"></div>'
]);
}
function wireMultiInput(c) {
$('#' + c.addBtnId).click(function () {
var count = $('#' + c.containerId + ' .rmInputRow').length;
if (typeof c.maxRows === 'number' && count >= c.maxRows) { alert(c.maxMsg || 'Достигнут лимит.'); return; }
addInputRow({
containerId: c.containerId,
placeholder: c.addPh,
inputClass: c.inputClass,
rowClass: c.type === 'rename' ? 'rmRenameVariantRow' : '',
indentLeft: 0,
fitParentWidth: c.type === 'rename',
prefixHtml: c.type === 'rename' ? buildLeftRemoveButtonHtml('rmRemoveInput', 'Удалить') : '',
removeBeforeInput: c.type === 'rename'
});
});
}
function buildSettingsFieldHtml(label, controlHtml, helpText, options) {
var opts = options || {};
var helpHtml = helpText
? '<div class="rmSettingsFieldHint">' + helpText + '</div>'
: '';
var labelHtml = opts.forId
? '<label class="rmSettingsFieldLabel" for="' + opts.forId + '">' + label + '</label>'
: '<div class="rmSettingsFieldLabel">' + label + '</div>';
return joinHtml([
'<div class="rmSettingsField">',
labelHtml,
'<div class="rmSettingsFieldControl">', controlHtml, '</div>',
helpHtml,
'</div>'
]);
}
function buildSettingsSectionHtml(title, bodyHtml, helpText, options) {
var opts = options || {};
var headerHtml = '';
var description = [];
if (opts.titleNote) description.push(opts.titleNote);
if (helpText) description.push(helpText);
if (title || description.length) {
headerHtml = joinHtml([
'<div class="rmSettingsSectionHeader">',
title ? '<div class="rmSettingsSectionTitle">' + title + '</div>' : '',
description.length ? '<div class="rmSettingsSectionDescription">' + description.join(' ') + '</div>' : '',
'</div>'
]);
}
return joinHtml([
'<div class="rmSettingsSection">',
headerHtml,
bodyHtml,
'</div>'
]);
}
function buildSettingsSimpleCheckboxHtml(id, text) {
return joinHtml([
'<label class="rmSettingsCheck">',
'<input id="', id, '" type="checkbox">',
'<span>', text, '</span>',
'</label>'
]);
}
function buildQuickPhrasesSettingsEditorHtml() {
return joinHtml([
'<div id="rmSettingsQuickPhrasesEditor" class="rmQuickPhraseEditor">',
'<div id="rmSettingsQuickPhrasesList" class="rmQuickPhraseList"></div>',
'<input id="rmSettingsQuickPhraseInput" type="text" autocomplete="off" style="', stInputFull, 'margin-bottom:0;">',
'<div id="rmSettingsQuickPhraseMeta" class="rmQuickPhraseMeta"></div>',
'</div>'
]);
}
function getQuickPhraseEditor() {
return $('#rmSettingsQuickPhrasesEditor');
}
function getQuickPhraseEditorState() {
var $editor = getQuickPhraseEditor();
var phrases = normalizeQuickPhrasesList($editor.data('rmQuickPhrases'), []);
var editingIndex = parseInt($editor.data('rmQuickPhraseEditingIndex'), 10);
if (isNaN(editingIndex)) editingIndex = -1;
return { editor: $editor, phrases: phrases, editingIndex: editingIndex };
}
function setQuickPhraseEditorState(phrases, editingIndex) {
var $editor = getQuickPhraseEditor();
var normalized = normalizeQuickPhrasesList(phrases, []);
var safeEditingIndex = parseInt(editingIndex, 10);
if (isNaN(safeEditingIndex) || safeEditingIndex < 0 || safeEditingIndex >= normalized.length) safeEditingIndex = -1;
if (!$editor.length) return;
$editor.data('rmQuickPhrases', normalized);
$editor.data('rmQuickPhraseEditingIndex', safeEditingIndex);
renderQuickPhraseEditor();
}
function clearQuickPhraseDropState() {
var $editor = getQuickPhraseEditor();
$editor.removeData('rmQuickPhraseDragIndex');
$editor.removeData('rmQuickPhraseDropIndex');
$editor.removeData('rmQuickPhraseDropAfter');
$editor.find('.rmQuickPhraseChip').removeClass('is-dragging is-drop-before is-drop-after');
}
function renderQuickPhraseEditor() {
var state = getQuickPhraseEditorState();
var $list = $('#rmSettingsQuickPhrasesList');
var $input = $('#rmSettingsQuickPhraseInput');
var $meta = $('#rmSettingsQuickPhraseMeta');
if (!state.editor.length || !$list.length || !$input.length) return;
if (state.phrases.length) {
$list.html(state.phrases.map(function (phrase, index) {
var chipClass = 'rmQuickPhraseChip' + (index === state.editingIndex ? ' is-editing' : '');
return joinHtml([
'<div class="', chipClass, '" draggable="true" data-rm-quick-index="', index, '">',
'<button type="button" class="rmQuickPhraseEditBtn" title="Редактировать фразу">', escapeHtml(phrase), '</button>',
'<button type="button" class="rmQuickPhraseRemoveBtn" title="Удалить фразу" aria-label="Удалить фразу">×</button>',
'</div>'
]);
}).join(''));
} else {
$list.html('<div class="rmQuickPhraseEmpty">Фразы пока не добавлены.</div>');
}
$input
.attr('placeholder', state.editingIndex >= 0 ? 'Изменить значение...' : 'Добавить значение...')
.toggleClass('is-editing', state.editingIndex >= 0);
$meta
.text('')
.hide();
}
function notifyQuickPhraseEditorChanged() {
var $editor = getQuickPhraseEditor();
if ($editor.length) $editor.trigger('rmQuickPhrasesChanged');
}
function startQuickPhraseEdit(index) {
var state = getQuickPhraseEditorState();
var $input = $('#rmSettingsQuickPhraseInput');
if (index < 0 || index >= state.phrases.length || !$input.length) return;
state.editor.data('rmQuickPhraseEditingIndex', index);
$input.val(state.phrases[index]);
renderQuickPhraseEditor();
$input.trigger('focus');
if ($input[0] && typeof $input[0].select === 'function') $input[0].select();
}
function cancelQuickPhraseEdit() {
var state = getQuickPhraseEditorState();
var $input = $('#rmSettingsQuickPhraseInput');
state.editor.data('rmQuickPhraseEditingIndex', -1);
if ($input.length) $input.val('').removeClass('is-editing');
renderQuickPhraseEditor();
}
function saveQuickPhraseInput() {
var state = getQuickPhraseEditorState();
var $input = $('#rmSettingsQuickPhraseInput');
var value = normalizeQuickPhraseValue($input.val());
var next = [];
if (!$input.length || !value) return false;
if (state.editingIndex >= 0) {
state.phrases.forEach(function (phrase, index) {
if (index === state.editingIndex) {
next.push(value);
return;
}
if (phrase !== value && next.indexOf(phrase) === -1) next.push(phrase);
});
} else {
next = state.phrases.slice();
if (next.indexOf(value) === -1) next.push(value);
}
state.editor.data('rmQuickPhrases', normalizeQuickPhrasesList(next, []));
state.editor.data('rmQuickPhraseEditingIndex', -1);
$input.val('').removeClass('is-editing');
clearQuickPhraseDropState();
renderQuickPhraseEditor();
notifyQuickPhraseEditorChanged();
return true;
}
function removeQuickPhrase(index) {
var state = getQuickPhraseEditorState();
var $input = $('#rmSettingsQuickPhraseInput');
if (index < 0 || index >= state.phrases.length) return;
state.phrases.splice(index, 1);
state.editor.data('rmQuickPhrases', state.phrases);
if (state.editingIndex === index) {
state.editor.data('rmQuickPhraseEditingIndex', -1);
if ($input.length) $input.val('').removeClass('is-editing');
} else if (state.editingIndex > index) {
state.editor.data('rmQuickPhraseEditingIndex', state.editingIndex - 1);
}
clearQuickPhraseDropState();
renderQuickPhraseEditor();
notifyQuickPhraseEditorChanged();
}
function reorderQuickPhrases(phrases, fromIndex, toIndex, placeAfter) {
var result = phrases.slice();
var insertIndex = toIndex + (placeAfter ? 1 : 0);
var item;
if (fromIndex < 0 || fromIndex >= result.length || toIndex < 0 || toIndex >= result.length) return result;
item = result.splice(fromIndex, 1)[0];
if (fromIndex < insertIndex) insertIndex--;
result.splice(insertIndex, 0, item);
return result;
}
function getQuickPhraseDropPointer(evt) {
var originalEvent = evt && (evt.originalEvent || evt);
if (!originalEvent) return null;
if (typeof originalEvent.clientX !== 'number' || typeof originalEvent.clientY !== 'number') return null;
return { x: originalEvent.clientX, y: originalEvent.clientY };
}
function getQuickPhraseDropTarget($list, pointer, dragIndex) {
var candidates = [];
var rowCandidates;
var minRowDistance = Infinity;
var bestBoundary = null;
var bestBoundaryDistance = Infinity;
if (!$list || !$list.length || !pointer) return null;
$list.children('.rmQuickPhraseChip').each(function () {
var index = parseInt($(this).attr('data-rm-quick-index'), 10);
var rect;
var rowDistance;
if (isNaN(index) || index === dragIndex) return;
rect = this.getBoundingClientRect();
if (!rect.width || !rect.height) return;
rowDistance = pointer.y < rect.top ? (rect.top - pointer.y) : (pointer.y > rect.bottom ? (pointer.y - rect.bottom) : 0);
candidates.push({
node: this,
index: index,
left: rect.left,
right: rect.right,
top: rect.top,
bottom: rect.bottom,
midX: rect.left + rect.width / 2,
rowDistance: rowDistance
});
if (rowDistance < minRowDistance) minRowDistance = rowDistance;
});
if (!candidates.length) return null;
rowCandidates = candidates
.filter(function (candidate) { return candidate.rowDistance === minRowDistance; })
.sort(function (a, b) {
if (a.left !== b.left) return a.left - b.left;
return a.index - b.index;
});
if (!rowCandidates.length) return null;
if (pointer.x <= rowCandidates[0].left) {
return { index: rowCandidates[0].index, placeAfter: false, node: rowCandidates[0].node };
}
if (pointer.x >= rowCandidates[rowCandidates.length - 1].right) {
return {
index: rowCandidates[rowCandidates.length - 1].index,
placeAfter: true,
node: rowCandidates[rowCandidates.length - 1].node
};
}
for (var i = 0; i < rowCandidates.length; i++) {
var candidate = rowCandidates[i];
if (pointer.x >= candidate.left && pointer.x <= candidate.right) {
return {
index: candidate.index,
placeAfter: pointer.x > candidate.midX,
node: candidate.node
};
}
}
rowCandidates.forEach(function (candidate) {
var leftDistance = Math.abs(pointer.x - candidate.left);
var rightDistance = Math.abs(pointer.x - candidate.right);
if (leftDistance < bestBoundaryDistance) {
bestBoundaryDistance = leftDistance;
bestBoundary = { index: candidate.index, placeAfter: false, node: candidate.node };
}
if (rightDistance < bestBoundaryDistance) {
bestBoundaryDistance = rightDistance;
bestBoundary = { index: candidate.index, placeAfter: true, node: candidate.node };
}
});
return bestBoundary;
}
function bindQuickPhrasesEditor() {
var $editor = getQuickPhraseEditor();
var $list = $('#rmSettingsQuickPhrasesList');
var $input = $('#rmSettingsQuickPhraseInput');
function updateQuickPhraseDropTarget(evt) {
var dragIndex = parseInt($editor.data('rmQuickPhraseDragIndex'), 10);
var pointer = getQuickPhraseDropPointer(evt);
var target;
if (isNaN(dragIndex) || !pointer) return null;
target = getQuickPhraseDropTarget($list, pointer, dragIndex);
if (!target) return null;
$editor.data('rmQuickPhraseDropIndex', target.index);
$editor.data('rmQuickPhraseDropAfter', !!target.placeAfter);
$editor.find('.rmQuickPhraseChip').removeClass('is-drop-before is-drop-after');
$(target.node).addClass(target.placeAfter ? 'is-drop-after' : 'is-drop-before');
return target;
}
function applyQuickPhraseDrop() {
var state = getQuickPhraseEditorState();
var fromIndex = parseInt($editor.data('rmQuickPhraseDragIndex'), 10);
var toIndex = parseInt($editor.data('rmQuickPhraseDropIndex'), 10);
var placeAfter = $editor.data('rmQuickPhraseDropAfter') === true;
var changed = false;
if (!isNaN(fromIndex) && !isNaN(toIndex) && fromIndex !== toIndex) {
state.editor.data('rmQuickPhrases', reorderQuickPhrases(state.phrases, fromIndex, toIndex, placeAfter));
if (state.editingIndex === fromIndex) state.editor.data('rmQuickPhraseEditingIndex', -1);
changed = true;
}
clearQuickPhraseDropState();
renderQuickPhraseEditor();
if (changed) notifyQuickPhraseEditorChanged();
}
if (!$editor.length || !$list.length || !$input.length) return;
$editor.off('.rmQuickPhraseEditor');
$list.off('.rmQuickPhraseEditor');
$input.off('.rmQuickPhraseEditor');
$input.on('keydown.rmQuickPhraseEditor', function (e) {
if (e.key === 'Enter' || e.keyCode === 13) {
e.preventDefault();
saveQuickPhraseInput();
} else if (e.key === 'Escape' || e.keyCode === 27) {
e.preventDefault();
cancelQuickPhraseEdit();
}
});
$editor
.on('click.rmQuickPhraseEditor', '.rmQuickPhraseEditBtn', function () {
var index = parseInt($(this).closest('.rmQuickPhraseChip').attr('data-rm-quick-index'), 10);
startQuickPhraseEdit(index);
})
.on('click.rmQuickPhraseEditor', '.rmQuickPhraseRemoveBtn', function (e) {
var index;
e.preventDefault();
e.stopPropagation();
index = parseInt($(this).closest('.rmQuickPhraseChip').attr('data-rm-quick-index'), 10);
removeQuickPhrase(index);
})
.on('dragstart.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) {
var index = parseInt($(this).attr('data-rm-quick-index'), 10);
if (isNaN(index)) return;
$editor.data('rmQuickPhraseDragIndex', index);
$(this).addClass('is-dragging');
if (e.originalEvent && e.originalEvent.dataTransfer) {
e.originalEvent.dataTransfer.effectAllowed = 'move';
e.originalEvent.dataTransfer.setData('text/plain', String(index));
}
})
.on('dragover.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) {
if ($editor.data('rmQuickPhraseDragIndex') === undefined) return;
e.preventDefault();
updateQuickPhraseDropTarget(e);
if (e.originalEvent && e.originalEvent.dataTransfer) e.originalEvent.dataTransfer.dropEffect = 'move';
})
.on('drop.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) {
e.preventDefault();
updateQuickPhraseDropTarget(e);
applyQuickPhraseDrop();
})
.on('dragend.rmQuickPhraseEditor', '.rmQuickPhraseChip', function () {
clearQuickPhraseDropState();
});
$list
.on('dragover.rmQuickPhraseEditor', function (e) {
if ($editor.data('rmQuickPhraseDragIndex') === undefined) return;
e.preventDefault();
updateQuickPhraseDropTarget(e);
if (e.originalEvent && e.originalEvent.dataTransfer) e.originalEvent.dataTransfer.dropEffect = 'move';
})
.on('drop.rmQuickPhraseEditor', function (e) {
e.preventDefault();
updateQuickPhraseDropTarget(e);
applyQuickPhraseDrop();
});
renderQuickPhraseEditor();
}
function collectQuickPhraseValues() {
return getQuickPhraseEditorState().phrases;
}
function collectQuickPhraseValuesSnapshot() {
var state = getQuickPhraseEditorState();
var value = normalizeQuickPhraseValue($('#rmSettingsQuickPhraseInput').val());
var next = state.phrases.slice();
if (!value) return next;
if (state.editingIndex >= 0) {
next[state.editingIndex] = value;
} else if (next.indexOf(value) === -1) {
next.push(value);
}
return normalizeQuickPhrasesList(next, []);
}
function isMenuTitlePresetValue(value) {
return value === MENU_TITLE_PRESET_CACTIONS || value === MENU_TITLE_PRESET_PAGE || value === MENU_TITLE_PRESET_TOOLS;
}
function isMenuTitlePresetOnlySkin() {
return mwCfg.skin === 'minerva' || mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless';
}
function getDefaultMenuTitlePreset() {
return mwCfg.skin === 'minerva' ? MENU_TITLE_PRESET_TOOLS : MENU_TITLE_PRESET_CACTIONS;
}
function shouldPreserveStoredMenuTitleOnSave() {
return mwCfg.skin === 'minerva';
}
function isAvailableMenuTitlePresetValue(value) {
return getMenuTitlePresetOptions().some(function (option) {
return option.value === value;
});
}
function getMenuTitlePresetOptions() {
if (mwCfg.skin === 'minerva') {
return [
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Ещё' }
];
}
if (isVector22) {
return [
{ value: MENU_TITLE_PRESET_CACTIONS, label: 'Инструменты/Действия' },
{ value: MENU_TITLE_PRESET_PAGE, label: 'Страница' },
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты/Основное' }
];
}
if (mwCfg.skin === 'timeless') {
return [
{ value: MENU_TITLE_PRESET_CACTIONS, label: 'Инструменты для страниц' },
{ value: MENU_TITLE_PRESET_PAGE, label: 'Страница' },
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Вики-инструменты' }
];
}
if (mwCfg.skin === 'monobook') {
return [
{ value: MENU_TITLE_PRESET_CACTIONS, label: 'Верхняя панель' },
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты' }
];
}
return [
{ value: MENU_TITLE_PRESET_CACTIONS, label: mwCfg.skin === 'vector' ? 'Ещё' : 'Ещё / Действия' },
{ value: MENU_TITLE_PRESET_PAGE, label: 'Страница' },
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты' }
];
}
function getMenuTitlePresetHintText() {
var base = (mwCfg.skin === 'vector' || isVector22)
? 'Можно либо изменить заголовок отдельного меню Remover, либо перенести все пункты в одно из существующих стандартных меню.'
: 'На этом скине Remover использует существующие стандартные меню; отдельное меню с собственным заголовком не создаётся.';
if (isVector22) base += ' В Vector 2022 кнопки «Действия» и «Основное» находятся внутри общего меню «Инструменты».';
else if (mwCfg.skin === 'vector') base += ' В Vector отдельный заголовок создаёт собственное меню рядом с «Ещё».';
else if (mwCfg.skin === 'minerva') base += ' В Minerva Neue пункты Remover показываются в меню «Ещё».';
else if (mwCfg.skin === 'timeless') base += ' В Timeless доступны только стандартные варианты: «Инструменты для страниц», «Страница» и «Вики-инструменты».';
else if (mwCfg.skin === 'monobook') base += ' В MonoBook доступны только два варианта: поместить пункты в «Инструменты» или вывести их в верхнюю панель. Собственный заголовок меню здесь не используется.';
return base;
}
function getSignatureSeparatorPreviewText(value) {
var separator = String(value || '').trim();
return separator ? (separator + ' ' + '~~' + '~~') : ('~~' + '~~');
}
function updateSignatureSeparatorPreview(value) {
var previewValue = (typeof value === 'string') ? value : ($('#rmSettingsSignatureSeparator').val() || '');
var $code = $('#rmSettingsSignaturePreviewCode');
if (!$code.length) return;
$code.text(getSignatureSeparatorPreviewText(previewValue));
}
function bindSignatureSeparatorPreview() {
var $input = $('#rmSettingsSignatureSeparator');
if (!$input.length) return;
$input.off('.rmSignaturePreview').on('input.rmSignaturePreview change.rmSignaturePreview', function () {
updateSignatureSeparatorPreview($(this).val());
});
updateSignatureSeparatorPreview($input.val());
}
function buildMenuTitlePresetButtonsHtml() {
return joinHtml([
'<div class="rmSettingsMenuPresetWrap">',
'<div class="rmSettingsMenuPresetLabel">Перенос в стандартные меню</div>',
'<div id="rmSettingsMenuPresetBar" class="rmSettingsMenuPresetBar">',
getMenuTitlePresetOptions().map(function (option) {
return joinHtml([
'<button type="button" class="rmSettingsMenuPresetBtn" data-rm-menu-preset="',
option.value,
'" aria-pressed="false">',
escapeHtml(option.label),
'</button>'
]);
}).join(''),
'</div>',
'</div>'
]);
}
function applyMenuTitlePresetControls(presetValue) {
var preset = isMenuTitlePresetValue(presetValue) ? presetValue : '';
var $bar = $('#rmSettingsMenuPresetBar');
var $input = $('#rmSettingsMenuTitle');
var forcePresetOnly = isMenuTitlePresetOnlySkin();
if (!$bar.length || !$input.length) return;
$bar.data('rmPreset', preset);
$bar.find('.rmSettingsMenuPresetBtn').each(function () {
var isActive = $(this).data('rmMenuPreset') === preset;
$(this).toggleClass('is-active', isActive).attr('aria-pressed', isActive ? 'true' : 'false');
});
$input.prop('disabled', forcePresetOnly || !!preset);
}
function bindMenuTitlePresetControls() {
var $bar = $('#rmSettingsMenuPresetBar');
var $input = $('#rmSettingsMenuTitle');
if (!$bar.length || !$input.length) return;
$input.off('.rmSettingsMenuPreset').on('input.rmSettingsMenuPreset', function () {
if (!$bar.data('rmPreset')) $input.data('rmCustomValue', $input.val());
});
$bar.off('.rmSettingsMenuPreset').on('click.rmSettingsMenuPreset', '.rmSettingsMenuPresetBtn', function () {
var preset = $(this).data('rmMenuPreset');
var currentPreset = $bar.data('rmPreset') || '';
if (currentPreset === preset) {
if (isMenuTitlePresetOnlySkin()) return;
applyMenuTitlePresetControls('');
$input.val($input.data('rmCustomValue') || '');
$input.trigger('focus');
return;
}
$input.data('rmCustomValue', $input.val());
applyMenuTitlePresetControls(preset);
});
}
function fillSettingsFormValues(settings) {
var data = normalizeRemoverSettings(settings);
var forcePresetOnly = isMenuTitlePresetOnlySkin();
var menuTitleValue = data.menuTitle || '';
var storedMenuTitleValue = menuTitleValue;
if (forcePresetOnly && !isAvailableMenuTitlePresetValue(menuTitleValue)) menuTitleValue = getDefaultMenuTitlePreset();
var customMenuTitle = forcePresetOnly ? '' : (isMenuTitlePresetValue(menuTitleValue) ? settingsDefaults.menuTitle : menuTitleValue);
$('#rmSettingsForm').data('rmStoredMenuTitle', storedMenuTitleValue || '');
$('#rmSettingsNotifyAuthor').prop('checked', !!data.notifyAuthor);
$('#rmSettingsSubscribeTopic').prop('checked', !!data.subscribeTopic);
$('#rmSettingsAutoReloadAfterAction').prop('checked', !!data.autoReloadAfterAction);
$('#rmSettingsOpenNominationDiscussionInNewTab').prop('checked', !!data.openNominationDiscussionInNewTab);
$('#rmSettingsShowMenuIcons').prop('checked', !!data.showMenuIcons);
$('#rmSettingsMenuTitle').val(customMenuTitle || '').data('rmCustomValue', customMenuTitle || '');
$('#rmSettingsSignatureSeparator').val(data.signatureSeparator || '');
$('#rmSettingsExcludedNamespaces').val((data.excludedNamespaces || []).join(', '));
$('#rmSettingsDisabledItems').val((data.disabledItems || []).join(', '));
setQuickPhraseEditorState(data.quickPhrases || [], -1);
$('#rmSettingsQuickPhraseInput').val('').removeClass('is-editing');
clearQuickPhraseDropState();
applyMenuTitlePresetControls(menuTitleValue);
updateSignatureSeparatorPreview(data.signatureSeparator || '');
}
function collectSettingsFormValues(options) {
var opts = options || {};
var namespaces = parseNamespaceInput($('#rmSettingsExcludedNamespaces').val());
var disabledItems = parseDisabledItemsInput($('#rmSettingsDisabledItems').val());
var presetMenuTitle = $('#rmSettingsMenuPresetBar').data('rmPreset');
var forcePresetOnly = isMenuTitlePresetOnlySkin();
var storedMenuTitle = $('#rmSettingsForm').data('rmStoredMenuTitle');
if (namespaces.invalid.length) {
return { error: 'Некорректные номера пространств имён: ' + namespaces.invalid.join(', ') + '.' };
}
if (disabledItems.invalid.length) {
return { error: 'Неизвестные пункты меню: ' + disabledItems.invalid.join(', ') + '.' };
}
if (!opts.skipQuickPhraseCommit) saveQuickPhraseInput();
return {
value: normalizeRemoverSettings({
notifyAuthor: $('#rmSettingsNotifyAuthor').is(':checked'),
subscribeTopic: $('#rmSettingsSubscribeTopic').is(':checked'),
autoReloadAfterAction: $('#rmSettingsAutoReloadAfterAction').is(':checked'),
openNominationDiscussionInNewTab: $('#rmSettingsOpenNominationDiscussionInNewTab').is(':checked'),
showMenuIcons: $('#rmSettingsShowMenuIcons').is(':checked'),
menuTitle: forcePresetOnly
? (shouldPreserveStoredMenuTitleOnSave() && typeof storedMenuTitle === 'string'
? storedMenuTitle
: (isAvailableMenuTitlePresetValue(presetMenuTitle) ? presetMenuTitle : getDefaultMenuTitlePreset()))
: (isMenuTitlePresetValue(presetMenuTitle) ? presetMenuTitle : $('#rmSettingsMenuTitle').val()),
signatureSeparator: $('#rmSettingsSignatureSeparator').val(),
excludedNamespaces: namespaces.values,
disabledItems: disabledItems.values,
quickPhrases: opts.skipQuickPhraseCommit ? collectQuickPhraseValuesSnapshot() : collectQuickPhraseValues()
})
};
}
function updateSettingsSubmitReadyState(baselineSettings) {
var collected = collectSettingsFormValues({ skipQuickPhraseCommit: true });
var hasChanges = !!(collected.error || (collected.value && !areRemoverSettingsEqual(collected.value, baselineSettings)));
$('#removerSubmit').toggleClass('rmSubmitReady', hasChanges && !$('#removerSubmit').hasClass('rmSubmitError'));
$('#rmSettingsUnsavedHint').css('display', hasChanges ? 'inline-block' : 'none');
}
function bindSettingsSubmitReadyState(baselineSettings) {
var update = function () {
setTimeout(function () { updateSettingsSubmitReadyState(baselineSettings); }, 0);
};
$('#rmSettingsForm').off('.rmSettingsReady').on('input.rmSettingsReady change.rmSettingsReady', 'input, textarea, select', update);
$('#removerModalContent').off('click.rmSettingsReady keyup.rmSettingsReady').on(
'click.rmSettingsReady keyup.rmSettingsReady',
'.rmSettingsMenuPresetBtn,.rmQuickPhraseEditBtn,.rmQuickPhraseRemoveBtn,.rmQuickPhraseChip,#rmSettingsQuickPhraseInput',
update
);
$('#rmSettingsQuickPhrasesEditor').off('rmQuickPhrasesChanged.rmSettingsReady').on('rmQuickPhrasesChanged.rmSettingsReady', update);
updateSettingsSubmitReadyState(baselineSettings);
}
function buildSettingsFormHtml(menuLabelsHint) {
var menuTitleFieldLabel = isMenuTitlePresetOnlySkin() ? 'Размещение меню' : 'Заголовок отдельного меню';
var menuFields =
buildSettingsFieldHtml(menuTitleFieldLabel,
'<input id="rmSettingsMenuTitle" type="text" style="' + stInputFull + 'margin-bottom:0;">' + buildMenuTitlePresetButtonsHtml(),
getMenuTitlePresetHintText(), { forId: 'rmSettingsMenuTitle' }) +
buildSettingsFieldHtml('Визуальное оформление меню',
'<div class="rmSettingsChecks">' + buildSettingsSimpleCheckboxHtml('rmSettingsShowMenuIcons', 'Эмодзи в пунктах меню') + '</div>');
var messageFields =
buildSettingsFieldHtml('Префикс перед подписью',
'<input id="rmSettingsSignatureSeparator" type="text" style="' + stInputFull + 'margin-bottom:0;">',
'Добавляется перед подписью в публикуемых сообщениях.<span style="display:block;margin-top:6px;">Предпросмотр: <code id="rmSettingsSignaturePreviewCode"></code>.</span>',
{ forId: 'rmSettingsSignatureSeparator' }) +
buildSettingsFieldHtml('Часто используемые фразы', buildQuickPhrasesSettingsEditorHtml(),
'Enter для добавления. Порядок элементов изменяется перетаскиванием.', { forId: 'rmSettingsQuickPhraseInput' });
var defaultFields = '<div class="rmSettingsChecks">' +
buildSettingsSimpleCheckboxHtml('rmSettingsNotifyAuthor', 'Оповещать создателя страницы') +
buildSettingsSimpleCheckboxHtml('rmSettingsSubscribeTopic', 'Подписываться на номинацию') +
buildSettingsSimpleCheckboxHtml('rmSettingsOpenNominationDiscussionInNewTab', 'Открывать номинацию в отдельной вкладке') +
buildSettingsSimpleCheckboxHtml('rmSettingsAutoReloadAfterAction', 'Автообновление страницы после размещения<span style="display:block;margin-top:3px;color:' + tk.cSubM + ';font-size:12px;line-height:1.35;">Помешает взаимодействовать с логом действий.</span>') + '</div>';
var disableFields =
buildSettingsFieldHtml('Скрыть пункты меню',
'<input id="rmSettingsDisabledItems" type="text" style="' + stInputFull + 'margin-bottom:0;">',
'Названия пунктов через запятую, например <code>КБУ, КУЛ, КОБ</code>.' + menuLabelsHint,
{ forId: 'rmSettingsDisabledItems' }) +
buildSettingsFieldHtml('Не показывать в пространствах имён',
'<input id="rmSettingsExcludedNamespaces" type="text" style="' + stInputFull + 'margin-bottom:0;">',
'Номера пространств имён через запятую, например <code>2, 10, 828</code>. См. <a href="' + getPageUrl('Википедия:Пространства имён') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Википедия:Пространства имён</a>.',
{ forId: 'rmSettingsExcludedNamespaces' });
return joinHtml([
'<div id="rmSettingsForm" style="max-width:100%;">',
'<div class="rmSettingsLead">Настройки интерфейса и значений по умолчанию.</div>',
buildSettingsSectionHtml('Меню', menuFields, 'Настройки внешнего вида и состава меню Remover.'),
buildSettingsSectionHtml('Оформление сообщений', messageFields, 'Настройки оформления публикуемых сообщений в номинациях.'),
buildSettingsSectionHtml('Опции по умолчанию', defaultFields, 'Регулирует исходное состояние и поведение после выполнения действий.'),
buildSettingsSectionHtml('Отключение', disableFields, 'Скрывает отдельные пункты меню Remover или всё меню в выбранных пространствах имён.'),
'</div>'
]);
}
function buildSettingsFooterLeftHtml() {
return joinHtml([
'<div id="rmSettingsFooterLeft" style="display:flex;align-items:center;gap:6px;flex:1 1 auto;min-width:0;max-width:100%;flex-wrap:wrap;margin-right:auto;">',
'<button id="rmSettingsResetFooter" type="button" title="Удаляет сохранённые настройки Remover из вашего профиля MediaWiki и возвращает значения по умолчанию." style="', stCancel, '">Сбросить все настройки</button>',
'<a id="rmSettingsReportIssue" href="', getPageUrl('Обсуждение Википедии:Гаджеты/Remover'), '" target="_blank" rel="noopener noreferrer" ',
'title="Сообщить о проблеме или предложить улучшение" aria-label="Сообщить о проблеме или предложить улучшение" ',
'class="removerModalLink rmButtonLikeLink" style="', stCancel, 'display:inline-flex;align-items:center;justify-content:center;text-align:center;text-decoration:none;box-sizing:border-box;max-width:100%;line-height:1.2;word-break:normal;overflow-wrap:normal;">Обратная связь</a>',
'</div>'
]);
}
function openSettings() {
var currentSettings = normalizeRemoverSettings(state.settings || settingsDefaults);
var menuLabelsHint = buildSettingsMenuItemsHint();
var $previousModal = $('#removerModal').length ? $('#removerModal').detach() : $();
var previousLayoutSyncHandlers = modalLayoutSyncHandlers.slice();
function restorePreviousModal() {
closeModal();
if ($previousModal.length) {
$('#content').prepend($previousModal);
modalLayoutSyncHandlers = previousLayoutSyncHandlers.slice();
if (modalLayoutSyncHandlers.length) $(window).off('resize.removerModal').on('resize.removerModal', syncModalLayout);
syncModalLayout();
syncLinkWidths();
}
}
createModal({
title: 'Конфигурация',
width: 'compact',
showSettingsButton: false
});
$('#removerModal').addClass('rmModalSettings');
$('#removerModalHeaderBar').append(buildHeaderIconButtonHtml('rmSettingsBack', 'Назад', 'Назад', '←'));
$('#rmSettingsBack').on('click', restorePreviousModal);
$('#removerModalContent').html(buildSettingsFormHtml(menuLabelsHint));
fillSettingsFormValues(currentSettings);
bindMenuTitlePresetControls();
bindSignatureSeparatorPreview();
bindQuickPhrasesEditor();
renderModalFooter('submit', {
showCheckbox: false,
submitText: 'Сохранить',
onSubmit: function () {
var collected = collectSettingsFormValues();
var shouldReset;
var saveFn;
if (collected.error) {
alert(collected.error);
return false;
}
shouldReset = areRemoverSettingsEqual(collected.value, settingsDefaults);
saveFn = shouldReset
? function (callback) { resetSettingsOnServer(callback); }
: function (callback) { saveSettingsToServer(collected.value, callback); };
saveFn(function (err) {
if (err) {
alert('Не удалось ' + (shouldReset ? 'сбросить' : 'сохранить') + ' настройки: ' + (err.info || err.code || 'неизвестная ошибка') + '.');
unlockModalSubmit();
return;
}
location.reload();
});
}
});
var $settingsActions = $('#rmFooterActionButtons');
$settingsActions.wrapInner('<div id="rmSettingsActionButtonsRow"></div>');
$settingsActions.append('<span id="rmSettingsUnsavedHint" role="status" aria-live="polite">Есть несохранённые изменения</span>');
bindSettingsSubmitReadyState(currentSettings);
$('#rmFooterButtons').css('justify-content', 'space-between').prepend(buildSettingsFooterLeftHtml());
$('#rmSettingsResetFooter').on('click', function () {
fillSettingsFormValues(settingsDefaults);
updateSettingsSubmitReadyState(currentSettings);
$('#removerSubmit').trigger('focus');
});
}
// ─── Завершение обработки ────────────────────────────────────────────────
function finalizeSuccess(nominationInfo, usePageReload) {
if (isError) {
var $box = $('#rmLogBox').length ? $('#rmLogBox') : $('#removerModalContent');
$box.append('<p class="error">При выполнении скрипта произошли ошибки.</p>');
markSubmitError();
return;
}
renderModalFooter('reload');
if (nominationInfo && nominationInfo.pageTitle) {
appendNominationLink(nominationInfo.pageTitle, nominationInfo.sectionTitle);
}
if (!usePageReload && !nominationInfo) location.reload();
}
function finalizeFastRemoval(notifiedPages, summary) {
if (isError || !setAlert || !notifiedPages || !notifiedPages.length) {
finalizeSuccess(null, false);
return;
}
notifyAuthorsForPages(notifiedPages, {
summary: summary,
actionText: 'к быстрому удалению'
}, function () {
finalizeSuccess(null, false);
});
}
// ─── Общий runner ────────────────────────────────────────────────────────
/**
* Универсальный запуск полного пайплайна номинации.
* @param {Object} o
* templateStep — функция (next) → обработка шаблонов на статьях
* nominationStep — функция (done) → публикация номинации, done(err, {pageTitle, sectionTitle})
* notifyStep — функция (nominationInfo, next)
* skipNotify — boolean
* skipLink — boolean, не показывать ссылку на номинацию
*/
function runFlow(o) {
runNominationPipeline({
templateStep: o.templateStep,
nominationStep: o.nominationStep,
notifyStep: o.notifyStep || function (info, next) { next(); },
skipNotify: o.skipNotify,
onSuccess: function (ctx) {
if (isError) { markSubmitError(); return; }
renderModalFooter('reload');
if (!o.skipLink && ctx.nominationInfo && ctx.nominationInfo.pageTitle) {
appendNominationLink(ctx.nominationInfo.pageTitle, ctx.nominationInfo.sectionTitle);
maybeOpenNominationDiscussionInNewTab(ctx.nominationInfo);
}
},
onFailure: function () { markSubmitError(); }
});
}
// ═══════════════════════════════════════════════════════════════════════════
// ЯДРО: обработка статей (apply template + nomination page)
// ═══════════════════════════════════════════════════════════════════════════
/**
* Применяет шаблон к одной статье/категории.
* Понимает режим inArticle (вставка через <noinclude>),
* режим closeAction (снятие шаблона + запись на СО),
* режим cleanupAction (снятие КБУ/КУЛ).
*
* @param {string} pg — название страницы
* @param {Object} job — параметры задания (см. buildJob)
* @param {function} callback(err, meta)
*/
function applyTemplateToPage(pg, job, callback) {
var mode = job.mode;
// ── Снятие КБУ/КУЛ ──────────────────────────────────────────────────
if (mode === 'cleanup') {
var tm = job.transferMode || 'none';
if (tm === 'none') { callback({ code: 'error', info: 'Не выбран режим снятия шаблонов.' }); return; }
editPageContent(pg, { summary: job.summary, watchlist: 'nochange', readErrorCode: 'error', readError: 'Страница «' + pg + '» не существует.' },
function (article, done) {
var local = removeTransferTemplatesLocal(article, tm);
removeTransferTemplatesWithApiFallback(pg, local.text, tm, local, function (updated) {
if (updated.text === article) { done({ error: { code: 'error', info: 'Шаблоны для снятия не найдены.' } }); return; }
done({ text: updated.text });
});
}, function (err) { callback(err); });
return;
}
// ── Подведение итогов по КУ/КПМ (снятие + итог на СО) ───────────────
if (mode === 'denom') {
getTextWithTimestamp(pg, function (article, baseTimestamp, readErr) {
if (readErr) { callback(makeReadError(readErr, 'read_failed', 'Не удалось получить содержимое страницы «' + pg + '».')); return; }
if (article === null) { callback({ code: 'error', info: 'Страница «' + pg + '» не существует.' }); return; }
if (!job.sourceTemplate) { callback({ code: 'error', info: 'Не задан шаблон для снятия.' }); return; }
var tplPattern = buildTemplateNamePattern(job.sourceTemplate);
var tpl = findTemplateByPattern(article, tplPattern);
if (!tpl) { callback({ code: 'error', info: 'Невозможно снять шаблон «' + job.sourceTemplate + '».' }); return; }
var normalizedTplDate = convertToStandardDate(tpl.params[0]);
var tplExtraParams = tpl.params.slice(1);
var tplExtra = tplExtraParams.join('|').trim();
var renameTargets = collectRenameTargetsFromTemplateParams(tplExtraParams);
var isRedirectedDeletion = job.closeType === 'redirectedDeletion';
if (!RE_DATE_ISO.test(normalizedTplDate)) {
callback({ code: 'error', info: 'Не удалось распознать дату в шаблоне: «' + (tpl.params[0] || '') + '».' });
return;
}
if (isRedirectedDeletion && !job.redirectTarget) {
callback({ code: 'error', info: 'Не указана цель перенаправления.' });
return;
}
var date = getDate(normalizedTplDate);
var nomPlace = 'ВП:' + job.sourceTemplate.replace(/\|.*/, '') + '/' + date[1];
var retTalkSection = '';
var sectionNW, tplpar;
if (job.closeType === 'doneRnm') { sectionNW = job.oldTitle + ' → ' + pg; tplpar = job.oldTitle + '|' + pg; }
if (job.closeType === 'noRnm') {
if (!renameTargets.length) {
callback({ code: 'error', info: 'В шаблоне КПМ не найдено новое название.' });
return;
}
sectionNW = pg + ' → ' + renameTargets.join(', ');
tplpar = pg + '|' + renameTargets.join('|');
}
if (job.closeType === 'ret' || job.closeType === 'retConditional' || job.closeType === 'withdrawnDeletion' || isRedirectedDeletion) {
retTalkSection = tplExtra;
sectionNW = retTalkSection || pg;
tplpar = retTalkSection ? ('l1=' + retTalkSection) : '';
}
var editResultText = job.resultTemplate + (isRedirectedDeletion ? ' на [[' + job.redirectTarget + ']]' : '');
var editSummary = makeSummary('номинация [[' + (nomPlace ? nomPlace + '#' : '') + sectionNW + ']] — ' + editResultText);
var talkTitle = getTalkPage(pg);
getTextWithTimestamp(talkTitle, function (talkText, talkTimestamp, talkReadErr) {
if (talkReadErr) { callback(makeReadError(talkReadErr, 'talk_read_failed', 'Не удалось получить содержимое СО страницы «' + pg + '».')); return; }
var sourceTalkText = talkText || '';
var talkPageMissing = talkText === null;
var dateSectionTalkTemplate = getDateSectionTalkTemplateConfig(job.closeType);
var talkResult = dateSectionTalkTemplate
? upsertDateSectionTemplateOnTalkPage(sourceTalkText, dateSectionTalkTemplate.name, date[0], retTalkSection, dateSectionTalkTemplate)
: (job.closeType === 'retConditional')
? upsertConditionalRetTemplateOnTalkPage(sourceTalkText, date[0], retTalkSection, job.conditionalReason, job.conditionalDeadline)
: upsertSingleDateResultTemplateOnTalkPage(sourceTalkText, job.resultTemplate, date[0], tplpar);
function saveArticle() {
var cleaned = isRedirectedDeletion ? buildRedirectText(job.redirectTarget) : stripTemplatesByPattern(article, tplPattern).text;
var ep = { title: pg, text: cleaned, summary: editSummary, watchlist: 'nochange', assertuser: mwCfg.wgUserName };
if (baseTimestamp) ep.basetimestamp = baseTimestamp;
apiReq(ep, 'edit', function (t) {
var editErr = t && t.error ? t.error : null;
callback(editErr, editErr ? null : {
discussionPage: nomPlace,
discussionSection: sectionNW,
summary: editSummary,
successMessage: isRedirectedDeletion
? 'Содержимое страницы ' + buildQuotedStatusPageLink(pg) + ' очищено, установлено перенаправление на ' + buildQuotedStatusPageLink(job.redirectTarget) + '.'
: ''
});
});
}
if (talkResult.text === sourceTalkText) { saveArticle(); return; }
var talkEp = { title: talkTitle, text: talkResult.text, summary: editSummary, watchlist: 'nochange', assertuser: mwCfg.wgUserName };
if (talkPageMissing) talkEp.createonly = true;
else if (talkTimestamp) talkEp.basetimestamp = talkTimestamp;
apiReq(talkEp, 'edit', function (talkResp) {
if (talkResp && talkResp.error) { callback({ code: 'talk_failed', info: 'Не удалось записать итог на СО: ' + talkResp.error.info }); return; }
saveArticle();
});
});
});
return;
}
// ── Обычная номинация: вставка шаблона в статью ─────────────────────
// mode === 'nominate'
var isKu = job.opId === 'tRm' || job.opId === 'mRm';
var conflictRule = getNominationConflictRule(job);
var conflictLabel = conflictRule ? conflictRule.label : 'номинации';
editPageContent(pg, { summary: job.summary, readErrorCode: 'error', readError: 'Страница «' + pg + '» не существует.' },
function (article, done) {
var hasExistingNomination = conflictRule && conflictRule.detect(article);
var conflictDecision = getConflictDecisionForPage(job, pg);
function buildResult(finalText) {
var generatedTpl = buildGeneratedNominationTemplateText(job, pg);
return { text: generatedTpl ? wrapInNoinclude(finalText, generatedTpl) : finalText };
}
function buildResultAfterMoveToCommonsCleanup(sourceText) {
var localMove = removeMoveToCommonsTemplatesLocal(sourceText);
removeMoveToCommonsTemplatesWithApiFallback(pg, localMove.text, localMove, function (updated) {
done(buildResult(updated.text));
});
}
function finishConflictResolution(sourceText) {
var resolvedText;
var pageLink = buildQuotedStatusPageLink(pg);
if (conflictDecision.templateAction === 'keep') {
if (sourceText !== article) {
return {
text: sourceText,
meta: { successMessage: 'Шаблон ' + conflictLabel + ' на странице ' + pageLink + ' оставлен без изменений; шаблоны переноса сняты.' }
};
}
return { skip: true, meta: { successMessage: 'Шаблон ' + conflictLabel + ' на странице ' + pageLink + ' оставлен без изменений.' } };
}
resolvedText = applyConflictTemplateResolution(sourceText, job, pg, conflictDecision);
return {
text: resolvedText,
meta: {
successMessage: conflictDecision.templateAction === 'overwrite'
? 'Шаблон ' + conflictLabel + ' на странице ' + pageLink + ' перезаписан новой датой.'
: 'Новый шаблон ' + conflictLabel + ' добавлен сверху на странице ' + pageLink + '.'
}
};
}
if (hasExistingNomination && (!conflictDecision || conflictDecision.pageAction !== 'keep')) {
return { error: { code: 'error', info: 'На странице уже стоит шаблон ' + conflictLabel + '.' } };
}
if (hasExistingNomination && conflictDecision && conflictDecision.pageAction === 'keep') {
if (job.transferMode && job.transferMode !== 'none') {
var localConflict = removeTransferTemplatesLocal(article, job.transferMode);
removeTransferTemplatesWithApiFallback(pg, localConflict.text, job.transferMode, localConflict, function (updated) {
done(finishConflictResolution(updated.text));
});
return;
}
return finishConflictResolution(article);
}
if (isKu && job.transferMode && job.transferMode !== 'none') {
var local = removeTransferTemplatesLocal(article, job.transferMode);
removeTransferTemplatesWithApiFallback(pg, local.text, job.transferMode, local, function (updated) { done(buildResult(updated.text)); });
return;
}
if (job.removeMoveToCommons) {
buildResultAfterMoveToCommonsCleanup(article);
return;
}
return buildResult(article);
},
function (err) { callback(err); }
);
}
/**
* Обрабатывает список страниц последовательно.
* @param {string[]} pages
* @param {Object} job
* @param {function} onDone(notifiedPages, err, pageMeta)
*/
function processPageList(pages, job, onDone) {
var notifiedPages = [];
var pageMeta = {};
eachSequential(pages.slice().reverse(), function (pg, nextPage) {
var pageLink = buildQuotedStatusPageLink(pg);
var statusId = logStatus('Обрабатывается страница ' + pageLink + '...', null, { pending: true, trackError: false });
applyTemplateToPage(pg, job, function (err, meta) {
var normPg = normTitle(pg);
var isClose = job.mode === 'cleanup' || job.mode === 'denom';
if (!isClose) {
if (!err && meta && meta.successMessage) logStatus(meta.successMessage, null, { statusId: statusId, trackError: false });
else logPageEdit(pg, err, { statusId: statusId });
} else {
if (err) { logStatus('Завершение по странице ' + pageLink, err, { statusId: statusId }); }
else {
logStatus((meta && meta.successMessage) || ('Шаблон снят со страницы ' + pageLink + '.'), null, { statusId: statusId, trackError: false });
if (job.mode === 'denom') logStatus('Шаблон установлен на СО страницы ' + pageLink + '.', null, { trackError: false });
}
}
if (!err) {
notifiedPages.push(pg);
if (meta) pageMeta[normPg] = meta;
}
nextPage(err || null);
});
}, function (err) { onDone(notifiedPages, err, pageMeta); });
}
// ═══════════════════════════════════════════════════════════════════════════
// ПОСТРОЕНИЕ JOB из формы
// ═══════════════════════════════════════════════════════════════════════════
/**
* Строит объект job из данных формы для операции номинации (tRm, rnm, imp, merge, split, recov).
* @param {Object} op — запись из OPERATIONS
* @param {string} pg — целевая страница (уже разрешённая)
* @param {boolean} isMulti — режим мультиноминации
* @returns {Object|false} — job или false при ошибке ввода
*/
function buildNominationJob(op, pg, isMulti) {
var nom = op.nomination;
var date = getDate();
var msg = normalizeQuickPhraseValue($('#rmMsg').val());
var rawMsg = msg;
var opId = isMulti ? (nom.multiOpId || op.id) : op.id;
var tplpar = '';
var section, sectionNW, extraPages, multiArticles = [];
var multiHeaderText = '';
var multiArticleComments = {};
var multiFormat = 'sections';
var multiRenamePairs = [];
var multiRenameTargets = {};
var multiRenameTemplateTargets = {};
var isRenameWithRowTargets = isMulti && nom.extraInput && nom.extraInput.type === 'rename' && $('.rmMultiRenameTargetInput').length;
// Вычислить section и tplpar в зависимости от типа дополнительного ввода
if (nom.extraInput) {
var ei = nom.extraInput;
if (ei.type === 'rename') {
if (isRenameWithRowTargets) {
multiRenamePairs = collectMultiRenamePairs();
if (!validateMultiRenamePairs(multiRenamePairs, 'статью', 'новое название')) return false;
multiRenameTargets = buildMultiRenameTargetMap(multiRenamePairs, 'targetNames');
multiRenameTemplateTargets = buildMultiRenameTargetMap(multiRenamePairs, 'templateTargetNames');
tplpar = buildRenameTemplateParam(multiRenamePairs[0].templateTargetNames);
section = formatRenameItemLabel(pg, multiRenameTargets[normTitle(pg)] || multiRenamePairs[0].targetNames);
} else {
var rn = collectInputValues('.rmRenameInput');
if (!rn.length) { alert('Укажите новое название.'); return false; }
tplpar = buildRenameTemplateParam(rn);
section = formatRenameItemLabel(pg, rn);
}
} else if (ei.type === 'merge') {
var mn = collectInputValues('.rmMergeInput');
if (!mn.length) { alert('Укажите статью для объединения.'); return false; }
tplpar = pg + '|' + mn.join('|');
extraPages = mn;
section = formatPagesWithAnd([pg].concat(mn));
} else if (ei.type === 'split') {
var sn = collectInputValues('.rmSplitInput');
if (!sn.length) { alert('Укажите статьи для разделения.'); return false; }
tplpar = formatPagesWithAnd(sn);
section = '[[:' + pg + ']] → ' + tplpar;
}
}
if (isMulti) {
var ttl = $('#rmHeader').val() || '';
var articles = isRenameWithRowTargets
? multiRenamePairs.map(function (pair) { return pair.pageName; })
: collectInputValues('.rmMultiPageInput');
multiFormat = $('.rmArticleMultiFormatBtn.is-active').data('rmMultiFormat') || 'sections';
multiArticleComments = collectMultiNominationComments();
multiHeaderText = ttl;
multiArticles = articles.slice();
if (!articles.length) { alert('Укажите страницу.'); return false; }
if (!validateMultiNominationText(articles, rawMsg, multiArticleComments, 'страницы')) return false;
section = ttl || (isRenameWithRowTargets ? formatRenameItemsWithAnd(articles, multiRenameTargets) : '');
msg = multiFormat === 'list'
? buildMultiNominationListText(articles, rawMsg, multiArticleComments, isRenameWithRowTargets ? getMultiRenameDiscussionOptions(multiRenameTargets) : null)
: buildMultiNominationText(articles, rawMsg, multiArticleComments, isRenameWithRowTargets
? getMultiRenameDiscussionOptions(multiRenameTargets, { leadingBlankLine: false })
: { leadingBlankLine: false });
}
if (!section) section = '[[:' + pg + ']]';
sectionNW = section.replace(/\[\[:/g, '').replace(/]]/g, '');
var nomPageDate = date[1];
var nomPage = nom.nomPage(nomPageDate);
var summaryAction = getNominationSummaryAction(nom);
var summary = makeNominationTargetSummary(nomPage, sectionNW, summaryAction);
return {
mode: 'nominate',
opId: opId,
op: op,
date: date,
tplpar: tplpar,
articleTpl: nom.articleTpl || function () { return ''; },
inArticle: nom.inArticle !== false,
transferMode: (nom.supportsTransfer ? getTransferModeFromButtons() : 'none'),
summary: summary,
nominationSummary: makeNominationPageSummary(sectionNW),
summaryAction: summaryAction,
msg: msg,
nomPage: nomPage,
navTemplate: nom.navTemplate,
section: section,
sectionNW: sectionNW,
comment: nom.comment || '',
extraPages: extraPages || [],
isMulti: !!isMulti,
multiHeaderText: multiHeaderText,
multiNominationBody: rawMsg,
multiArticleComments: multiArticleComments,
multiNominationFormat: multiFormat || 'sections',
multiRenamePairs: multiRenamePairs,
multiRenameTargets: multiRenameTargets,
multiRenameTemplateTargets: multiRenameTemplateTargets,
multiArticles: multiArticles,
pages: isMulti ? multiArticles.slice().reverse() : ([pg].concat(extraPages || []))
};
}
function getTransferModeFromButtons() {
var kbu = $('#rmTransferBtnKbu').hasClass('is-active');
var kul = $('#rmTransferBtnKul').hasClass('is-active');
if (kbu && kul) return 'both';
if (kbu) return 'kbu';
if (kul) return 'kul';
return 'none';
}
function normalizeFastRemoveTemplateKey(templateName) {
return String(templateName || '').replace(/\s+/g, ' ').trim().toLowerCase();
}
function getBuiltinFastRemoveParamConfig(templateName) {
var key = normalizeFastRemoveTemplateKey(templateName);
var bareKey = key.replace(/^(?:подст|subst)\s*:\s*/, '');
if (/^(?:nld|no license|nsd|no source|nad|no author|dd|disputed|npd|no permission|ofud|orphaned-fairuse|nothost|db-nothost)$/.test(bareKey)) return { type: 'date' };
if (/^(?:dfud|disputed-fairuse)$/.test(bareKey)) return { type: 'dfud' };
if (key === 'db-badfairuse') return { type: 'badfairuse' };
if (key === 'rct' || key === 'замена с викисклада') return { type: 'commonsReplacement' };
if (key === 'nct' || key === 'перенесено на викисклад') return { type: 'nct' };
if (key === 'db-duplicate') return { type: 'duplicate' };
return null;
}
function buildKbuReasonButtonsHtml(items, buttonClass, dataAttr, titlePrefix) {
return items.map(function (item) {
var label = titlePrefix + ' ' + item[0] + ': ' + item[1];
return '<button type="button" class="rmToggleBtn rmKbuReasonPill ' + buttonClass + '" ' + dataAttr + '="' +
escapeHtml(item[0]) + '" aria-pressed="false" title="' + escapeHtml(label) +
'"><span class="rmKbuReasonNum">' + escapeHtml(item[0]) + '</span>' +
'<span class="rmKbuReasonText">' + escapeHtml(item[1]) + '</span></button>';
}).join('');
}
function buildDfudReasonButtonsHtml() {
return buildKbuReasonButtonsHtml(DFUD_REASON_OPTIONS, 'rmKbuDfudReasonBtn', 'data-rm-dfud-reason', 'п.');
}
function buildBadfairuseReasonButtonsHtml() {
return buildKbuReasonButtonsHtml(BADFAIRUSE_OPTIONS, 'rmKbuBadfairuseReasonBtn', 'data-rm-badfairuse-reason', 'пример');
}
function buildKbuReasonListHtml(id, hint, buttonsHtml) {
return '<div id="' + id + '" class="' + RESIZE_CLASS + '" style="display:none;margin-bottom:10px;">' +
'<div style="margin-bottom:6px;font-size:14px;line-height:1.4;color:' + tk.cSubM + ';">' + hint + '</div>' +
'<div style="display:flex;flex-direction:column;gap:4px;">' + buttonsHtml + '</div>' +
'</div>';
}
function collectSelectedDfudReasons() {
return $('#rmKbuDfudReasonsWrap .rmKbuDfudReasonBtn.is-active').map(function () {
return String($(this).attr('data-rm-dfud-reason') || '').trim();
}).get().filter(Boolean);
}
function getSelectedBadfairuseReason() {
return String($('#rmKbuBadfairuseReasonsWrap .rmKbuBadfairuseReasonBtn.is-active').first().attr('data-rm-badfairuse-reason') || '').trim();
}
function getCurrentCommonsReplacementMode() {
var mode = $('#rmKbuCommonsReplacementModeWrap .rmKbuCommonsReplacementModeBtn.is-active').data('rmCommonsReplacementMode');
return mode || 'file';
}
function setCurrentCommonsReplacementMode(mode) {
var value = mode || 'file';
$('#rmKbuCommonsReplacementModeWrap .rmKbuCommonsReplacementModeBtn').each(function () {
var active = $(this).data('rmCommonsReplacementMode') === value;
$(this).toggleClass('is-active', active).attr('aria-pressed', active ? 'true' : 'false');
});
if (value === 'category') {
$('#fiRm').attr('placeholder', 'Категория на Викискладе без Category:');
$('#fiRmExtra').attr('placeholder', 'Статья в руВики о теме категории (необязательно)').show();
} else {
$('#fiRmExtra').hide();
$('#fiRm').attr('placeholder', value === 'gallery' ? 'Галерея на Викискладе' : 'Имя файла на Викискладе');
}
syncModalLayout();
}
function getFastRemoveLegacyParamConfig(templateName) {
var params = cfg.requiredParamTemplates || {};
var key;
if (Object.prototype.hasOwnProperty.call(params, templateName)) return params[templateName];
for (key in params) {
if (Object.prototype.hasOwnProperty.call(params, key) &&
normalizeFastRemoveTemplateKey(key) === normalizeFastRemoveTemplateKey(templateName)) {
return params[key];
}
}
return null;
}
function getFastRemoveParamConfig(reason) {
var template = reason && reason[0];
return getBuiltinFastRemoveParamConfig(template) || getFastRemoveLegacyParamConfig(template);
}
function appendTemplateParam(tpl, name, value) {
var val = normalizeQuickPhraseValue(value);
if (!val) return tpl;
return tpl + '|' + (name ? name + '=' : '') + val;
}
function makeFastRemovalSummary(reason, data) {
var label = String(reason && reason[1] || '').trim();
var detail = String(data && data.summaryDetail || '').trim();
var summaryReason = (label === 'Особый случай' && detail) ? detail : (label || detail);
return makeSummary('номинация к быстрому удалению' + (summaryReason ? ': ' + summaryReason : ''));
}
function buildFastRemoveTemplateData(reason) {
var template = reason && reason[0];
var paramCfg = getFastRemoveParamConfig(reason);
var addInfo = normalizeQuickPhraseValue($('#fiRm').val());
var comment = normalizeQuickPhraseValue($('#fiRmComment').val());
var dateText = getDate()[0];
var mode;
var extra;
var tpl;
var values;
if (!template) return { error: 'Не выбрана причина быстрого удаления.' };
if (paramCfg && paramCfg.type === 'date') {
return { templateText: appendTemplateParam(template, '1', dateText) };
}
if (paramCfg && paramCfg.type === 'dfud') {
tpl = appendTemplateParam(template, 'date', dateText);
values = collectSelectedDfudReasons();
values.forEach(function (value, index) { tpl = appendTemplateParam(tpl, String(index + 1), value); });
return { templateText: tpl };
}
if (paramCfg && paramCfg.type === 'badfairuse') {
addInfo = getSelectedBadfairuseReason();
if (!addInfo) return { error: 'Выберите пример из ВП:ПНИ для Ф6.' };
return { templateText: appendTemplateParam(template, '1', addInfo) };
}
if (paramCfg && paramCfg.type === 'commonsReplacement') {
mode = getCurrentCommonsReplacementMode();
if (mode === 'category') {
if (!addInfo) return { error: 'Укажите категорию на Викискладе.' };
tpl = appendTemplateParam('Замена с Викисклада/Категория', '1', addInfo);
extra = normalizeQuickPhraseValue($('#fiRmExtra').val());
return { templateText: appendTemplateParam(tpl, '2', extra) };
}
if (mode === 'gallery') {
if (!addInfo) return { error: 'Укажите галерею на Викискладе.' };
return { templateText: appendTemplateParam('Замена с Викисклада/Галерея', '1', addInfo) };
}
return { templateText: appendTemplateParam(template, '1', addInfo) };
}
if (paramCfg && paramCfg.type === 'nct') {
return {
templateText: appendTemplateParam(template, '1', addInfo),
removeMoveToCommons: $('#rmKbuRemoveMoveToCommons').is(':checked')
};
}
if (paramCfg && paramCfg.type === 'duplicate') {
if (!addInfo) return { error: 'Укажите имя файла-оригинала для Ф1.' };
tpl = appendTemplateParam(template, '1', addInfo);
return { templateText: appendTemplateParam(tpl, 'comment', comment) };
}
tpl = template;
if (typeof paramCfg === 'string') {
var noComment = paramCfg.charAt(0) === '!';
tpl = appendTemplateParam(tpl, '1', addInfo);
if (!noComment) tpl = appendTemplateParam(tpl, addInfo ? '2' : '1', comment);
return { templateText: tpl, summaryDetail: noComment ? addInfo : '' };
}
return { templateText: appendTemplateParam(tpl, '1', comment) };
}
function resetKbuReasonControls() {
$('#fiRm').hide().val('').attr({ type: 'text', placeholder: '' });
$('#fiRmExtra').hide().val('');
$('#rmKbuCommonsReplacementModeWrap').hide();
$('#rmKbuDfudReasonsWrap').hide().find('.rmKbuDfudReasonBtn').removeClass('is-active').attr('aria-pressed', 'false');
$('#rmKbuBadfairuseReasonsWrap').hide().find('.rmKbuBadfairuseReasonBtn').removeClass('is-active').attr('aria-pressed', 'false');
$('#rmKbuDateHint').hide().text('');
$('#rmKbuRemoveMoveToCommons').prop('checked', false);
$('#rmKbuRemoveMoveToCommonsWrap').hide();
$('#fiRmComment').hide().val('');
$('.rmQuickPhrasesPanel[data-rm-target="fiRmComment"]').hide();
}
function toggleKbuComment(show) {
$('#fiRmComment').toggle(show);
$('.rmQuickPhrasesPanel[data-rm-target="fiRmComment"]').toggle(show);
}
function updateKbuReasonControls(reasons) {
var reason = reasons[$('#rmSel').val()] || reasons[0];
var paramCfg = getFastRemoveParamConfig(reason);
$('#rmKbuCriteriaLinkWrap').html(buildFastRemoveCriteriaLinkHtml(reason));
resetKbuReasonControls();
if (paramCfg && (paramCfg.type === 'date' || paramCfg.type === 'dfud')) {
$('#rmKbuDateHint').text(FILE_DATE_DEADLINE_HINT).show();
if (paramCfg.type === 'dfud') $('#rmKbuDfudReasonsWrap').show();
return;
}
if (paramCfg && paramCfg.type === 'badfairuse') {
$('#rmKbuBadfairuseReasonsWrap').show();
return;
}
if (paramCfg && paramCfg.type === 'commonsReplacement') {
$('#rmKbuCommonsReplacementModeWrap').show();
$('#fiRm').show();
setCurrentCommonsReplacementMode(getCurrentCommonsReplacementMode());
return;
}
if (paramCfg && paramCfg.type === 'nct') {
$('#fiRm').attr('placeholder', 'Укажите имя файла на Викискладе').show();
$('#rmKbuRemoveMoveToCommonsWrap').css('display', 'inline-flex');
return;
}
if (paramCfg && paramCfg.type === 'duplicate') {
$('#fiRm').attr('placeholder', 'Укажите имя файла-оригинала').show();
toggleKbuComment(true);
return;
}
if (typeof paramCfg === 'string') {
var noComment = paramCfg.charAt(0) === '!';
$('#fiRm').attr('placeholder', 'Укажите ' + (noComment ? paramCfg.substring(1) : paramCfg)).show();
toggleKbuComment(!noComment);
return;
}
toggleKbuComment(true);
}
function getKbuReasonGroupKey(reason) {
var label = String(reason && reason[1] || '');
var m = label.match(/^\s*([А-ЯЁA-Z]{1,3})\d/);
return m ? m[1] : label;
}
function buildKbuReasonOptionsHtml(reasons) {
var prevGroup = '';
var parts = [];
(reasons || []).forEach(function (reason, index) {
var group = getKbuReasonGroupKey(reason);
if (index && group !== prevGroup) parts.push('<hr>');
parts.push('<option value="' + index + '">' + escapeHtml(reason[1]) + '</option>');
prevGroup = group;
});
return parts.join('');
}
function buildKbuFormHtml(reasons) {
return joinHtml([
'<select id="rmSel" style="', stInputFull, '">',
buildKbuReasonOptionsHtml(reasons),
'</select>',
'<div id="rmKbuDateHint" class="', RESIZE_CLASS, '" style="display:none;margin:-2px 0 10px;font-size:12px;line-height:1.35;color:', tk.cSubM, ';"></div>',
'<div id="rmKbuCommonsReplacementModeWrap" class="', RESIZE_CLASS, '" style="display:none;margin-bottom:10px;"><div class="rmSegmentedBar">',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmKbuCommonsReplacementModeBtn is-active" data-rm-commons-replacement-mode="file" aria-pressed="true">Файл</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmKbuCommonsReplacementModeBtn" data-rm-commons-replacement-mode="category" aria-pressed="false">Категория</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmKbuCommonsReplacementModeBtn" data-rm-commons-replacement-mode="gallery" aria-pressed="false">Галерея</button>',
'</div></div>',
buildKbuReasonListHtml(
'rmKbuDfudReasonsWrap',
'Выберите пункты КДИ, которым файл не соответствует. Можно выбрать несколько пунктов.',
buildDfudReasonButtonsHtml()
),
buildKbuReasonListHtml(
'rmKbuBadfairuseReasonsWrap',
'Выберите пример из ВП:ПНИ, под который подпадает файл. Можно выбрать только один пункт.',
buildBadfairuseReasonButtonsHtml()
),
'<input id="fiRm" type="text" style="', stInputFull, 'display:none;">',
'<input id="fiRmExtra" type="text" style="', stInputFull, 'display:none;">',
'<label id="rmKbuRemoveMoveToCommonsWrap" class="', RESIZE_CLASS, '" style="', stFooterCheckLabel.replace('display:inline-flex;', 'display:none;'), 'margin:-2px 0 10px;">',
'<input id="rmKbuRemoveMoveToCommons" type="checkbox" style="margin:2px 0 0;flex-shrink:0;">',
'Снять шаблон «Перенести на Викисклад»',
'</label>',
'<input id="fiRmComment" type="text" placeholder="Комментарий (необязательно)" style="', stInputFull, '">',
buildQuickPhrasesPanelHtml('fiRmComment')
]);
}
function buildNominationMultiHeaderHtml(pg, options) {
var opts = options || {};
var multiHeaderRowStyle = stRow.replace('margin-bottom:6px;', 'margin-bottom:0;');
return joinHtml([
'<div id="rmMultiHeader" class="', RESIZE_CLASS, '" style="display:none;margin-bottom:6px;">',
'<div class="rmMultiPageRow rmNominationHeaderRow" style="', multiHeaderRowStyle, '"><input id="rmHeader" type="text" placeholder="Заголовок номинации" style="', stInputBox, '">',
buildAddMultiPageButtonHtml(opts), '</div>',
'</div>',
'<div id="rmMultiPagesContainer" style="display:flex;flex-direction:column;gap:', multiNominationGap, ';margin-bottom:', multiNominationGap, ';">',
buildMultiPageRowHtml(0, $.extend({}, opts, { rowId: 'rmFirstMultiPage', pageValue: pg, showAdd: true })),
'</div>'
]);
}
function buildTransferBoxHtml() {
return joinHtml([
'<div id="rmTransferBox" class="', RESIZE_CLASS, ' rmTransferPanel" style="width:100%;box-sizing:border-box;"><div class="rmTransferGrid">',
'<div id="rmTransferModeSingle" class="rmSegmentedBar"><button type="button" id="rmTransferBtnNone" class="rmSegmentedBtn rmToggleBtn is-active" aria-pressed="true">Обычная номинация</button></div>',
'<div id="rmTransferModeGroup" class="rmSegmentedBar">',
'<button type="button" id="rmTransferBtnKbu" class="rmSegmentedBtn rmToggleBtn" aria-pressed="false" title="Шаблоны db-*, уд-*, КОУ, Hangon.">Снять КБУ</button>',
'<button type="button" id="rmTransferBtnKul" class="rmSegmentedBtn rmToggleBtn" aria-pressed="false" title="Шаблон «К улучшению».">Снять КУЛ</button>',
'</div>',
'<div class="rmTransferHintRow"><div id="rmTransferHint" style="display:none;font-size:12px;line-height:1.35;color:', tk.cSubM, ';"></div></div>',
'</div></div>'
]);
}
function buildMultiNominationFormatSwitchHtml(wrapId, buttonClass) {
return joinHtml([
'<div id="', wrapId, '" class="', RESIZE_CLASS, '" style="display:none;margin-top:8px;margin-bottom:10px;">',
'<div class="rmSegmentedBar">',
'<button type="button" class="rmSegmentedBtn rmToggleBtn ', buttonClass, ' is-active" data-rm-multi-format="sections" aria-pressed="true">Оформить подразделами</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn ', buttonClass, '" data-rm-multi-format="list" aria-pressed="false">Оформить списком</button>',
'</div>',
'</div>'
]);
}
function getMultiNominationUiOptions(kind, options) {
var opts = options || {};
var isArticle = kind === 'article';
var isRename = !!opts.renameMulti;
var actionText = typeof opts.actionText === 'string'
? opts.actionText
: (opts.deletionMulti ? 'к удалению' : (isRename ? 'к переименованию' : ''));
var itemAcc = isArticle ? 'статью' : 'категорию';
var itemDat = isArticle ? 'статье' : 'категории';
var itemGen = isArticle ? 'статьи' : 'категории';
var result = {
inputPlaceholder: isArticle ? 'Статья' : 'Категория',
addTitle: 'Мультиноминация: добавить ' + itemAcc + ' в номинацию' + (actionText ? ' ' + actionText : ''),
removeTitle: 'Убрать ' + itemAcc + ' из номинации' + (actionText ? ' ' + actionText : ''),
commentTitle: 'Добавить комментарий к этой ' + itemDat,
commentExpandedTitle: 'Скрыть комментарий к этой ' + itemDat,
commentPlaceholder: 'Комментарий только для этой ' + itemGen + ' (необязательно)'
};
if (isRename) {
$.extend(result, {
targetInput: true,
targetInputClass: 'rmMultiRenameTargetInput',
targetPlaceholder: isArticle ? 'Новое название' : 'Новое название без префикса Категория:',
targetVariants: true
});
}
if (opts.setup) {
$.extend(result, {
defaultPage: opts.defaultPage || '',
multiOnlySelector: isArticle ? '#rmArticleMultiFormatWrap' : '#rmCategoryMultiFormatWrap'
});
if (isRename) $.extend(result, {
singleOnlySelector: '#rmSingleRenameBlock',
hideContainerWhenSingle: true,
singleCurrentPageSelector: '#rmSingleRenameCurrent',
singleRenameTargetSelector: isArticle ? '#rmRenameFirst' : '#firstRenameInput',
singleRenameVariantSelector: isArticle ? '#rmRenameContainer .rmRenameInput' : '#renameVariantsContainer .variantInput',
singleRenameVariantContainerId: isArticle ? 'rmRenameContainer' : 'renameVariantsContainer',
singleRenameVariantPlaceholder: isArticle ? 'Дополнительный вариант' : 'Дополнительный вариант названия',
singleRenameInputClass: isArticle ? 'rmRenameInput' : undefined
});
}
return result;
}
function buildNominationFormHtml(nom, pg, multiMode) {
var isRenameMulti = multiMode && nom.extraInput && nom.extraInput.type === 'rename';
var multiOptions = getMultiNominationUiOptions('article', {
renameMulti: isRenameMulti,
deletionMulti: multiMode && nom.template === 'к удалению'
});
return joinHtml([
isRenameMulti ? buildSingleRenameBlockHtml(nom.extraInput, 'Мультиноминация: добавить статью в номинацию', pg, 'Текущее название') : '',
multiMode ? buildNominationMultiHeaderHtml(pg, multiOptions) : '',
nom.extraInput && !isRenameMulti ? buildMultiInputHtml(nom.extraInput) : '',
'<textarea id="rmMsg" placeholder="Текст номинации без подписи"></textarea>',
buildQuickPhrasesPanelHtml('rmMsg'),
nom.supportsTransfer ? buildTransferBoxHtml() : '',
multiMode ? buildMultiNominationFormatSwitchHtml('rmArticleMultiFormatWrap', 'rmArticleMultiFormatBtn') : ''
]);
}
function buildCategoryNominationFormHtml(variantConfig, multiMode, pageName, options) {
var opts = options || {};
var multiOptions = getMultiNominationUiOptions('category', opts);
return joinHtml([
opts.renameMulti && variantConfig ? buildSingleRenameBlockHtml(variantConfig, 'Мультиноминация: добавить категорию в номинацию', pageName, 'Текущая категория') : '',
multiMode ? buildNominationMultiHeaderHtml(pageName, multiOptions) : '',
variantConfig && !opts.renameMulti && !opts.skipVariantInput ? buildMultiInputHtml(variantConfig) : '',
'<textarea id="nominationReason" placeholder="Текст номинации без подписи"></textarea>',
buildQuickPhrasesPanelHtml('nominationReason'),
multiMode ? buildMultiNominationFormatSwitchHtml('rmCategoryMultiFormatWrap', 'rmCategoryMultiFormatBtn') : ''
]);
}
function ensureMultiPageCommentTextareaResizer(textareaId, wrapId) {
var $textarea = $('#' + textareaId);
if (!$textarea.length || $textarea.data('rmNestedResizerReady')) return;
setupNestedResizableTextarea(textareaId, wrapId, parseInt(sz.taMinW, 10) || 180, 90);
$textarea.data('rmNestedResizerReady', true);
}
function setMultiPageCommentExpanded($btn, expanded) {
var wrapId = $btn.data('rmCommentWrap');
var textareaId = $btn.data('rmCommentTextarea');
var $wrap = $('#' + wrapId);
var title = expanded
? ($btn.data('rmCommentExpandedTitle') || 'Скрыть комментарий к этой странице')
: ($btn.data('rmCommentTitle') || 'Добавить комментарий к этой странице');
if (!$btn.length || !$wrap.length) return;
$btn.attr('aria-expanded', expanded ? 'true' : 'false')
.attr('aria-label', title)
.attr('title', title)
.toggleClass('is-active', expanded)
.text('✎');
$wrap.toggle(expanded);
if (expanded) ensureMultiPageCommentTextareaResizer(textareaId, wrapId);
}
function getMultiPageCommentTargets($block) {
var $textarea = $block.find('.rmMultiPageCommentInput').first();
var $wrap = $textarea.parent();
return {
wrapId: $wrap.attr('id') || '',
textareaId: $textarea.attr('id') || ''
};
}
function collectMultiRenameTargetValues($block) {
return $block.find('.rmMultiRenameTargetInput,.rmMultiRenameVariantInput').map(function () {
return ($(this).val() || '').trim();
}).get().filter(Boolean);
}
function setMultiPageRowControls($block, showAdd, showComment, options) {
var opts = options || {};
var $row = $block.find('.rmMultiPageRow').first();
var ids = getMultiPageCommentTargets($block);
var $commentBtn = $row.find('.rmMultiPageCommentToggle');
if (!$row.length) return;
if (showAdd) {
if ($commentBtn.length) setMultiPageCommentExpanded($commentBtn, false);
if (ids.wrapId) $('#' + ids.wrapId).hide();
$block.find('.rmMultiRenameVariantsContainer').hide();
$row.find('.rmMultiPageCommentToggle,.rmAddMultiRenameVariant,.rmRemoveInput').remove();
if (!$row.find('.rmAddMultiPage').length) $row.append(buildAddMultiPageButtonHtml(opts));
return;
}
$block.find('.rmMultiRenameVariantsContainer').toggle(!!opts.targetVariants);
$row.find('.rmAddMultiPage').remove();
if (!$row.find('.rmMultiPageCommentToggle').length) {
$row.append(buildMultiPageButtonsHtml(ids.wrapId, ids.textareaId, $.extend({}, opts, { showComment: showComment })));
return;
}
$row.find('.rmMultiPageCommentToggle').toggle(showComment);
}
function setupMultiPageNominationUi(options) {
var opts = options || {};
var containerSelector = opts.containerSelector || '#rmMultiPagesContainer';
var pageCounter = parseInt(opts.nextIndex, 10) || 1;
var wasMultiModeExpanded = false;
function restoreEmptySinglePageInput() {
var $pageInput = $(containerSelector + ' .rmMultiPageInput').first();
if (!$pageInput.length || String($pageInput.val() || '').trim()) return;
$pageInput.val(opts.defaultPage || '');
}
function copySingleCurrentPageToFirstRow() {
var $source = opts.singleCurrentPageSelector ? $(opts.singleCurrentPageSelector).first() : $();
var $target = $(containerSelector + ' .rmMultiPageBlock').first().find('.rmMultiPageInput').first();
var value = String(($source.val && $source.val()) || '').trim();
if (!$source.length || !$target.length || !value) return;
$target.val(value);
}
function copyFirstRowPageToSingleCurrent() {
var $target = opts.singleCurrentPageSelector ? $(opts.singleCurrentPageSelector).first() : $();
var $source = $(containerSelector + ' .rmMultiPageBlock').first().find('.rmMultiPageInput').first();
var value = String(($source.val && $source.val()) || '').trim();
if (!$source.length || !$target.length) return;
$target.val(value || opts.defaultPage || '');
}
function getSingleRenameTargets() {
var targets = [];
var $source = opts.singleRenameTargetSelector ? $(opts.singleRenameTargetSelector).first() : $();
if ($source.length && String($source.val() || '').trim()) targets.push(String($source.val() || '').trim());
if (opts.singleRenameVariantSelector) {
$(opts.singleRenameVariantSelector).each(function () {
var value = String($(this).val() || '').trim();
if (value) targets.push(value);
});
}
return targets.slice(0, 3);
}
function setMultiBlockRenameTargets($block, targets) {
var list = asNonEmptyArray(targets).slice(0, 3);
var $target = $block.find('.rmMultiRenameTargetInput').first();
var $container = $block.find('.rmMultiRenameVariantsContainer').first();
if (!$target.length) return;
$target.val(list[0] || '');
if (!$container.length) return;
$container.find('.rmMultiRenameVariantRow').remove();
list.slice(1).forEach(function (value) {
$container.append(buildMultiRenameVariantRowHtml(value, opts));
});
}
function copySingleRenameTargetsToFirstRow() {
var $block = $(containerSelector + ' .rmMultiPageBlock').first();
if (!$block.length) return;
setMultiBlockRenameTargets($block, getSingleRenameTargets());
}
function copyFirstRowRenameTargetsToSingle() {
var $target = opts.singleRenameTargetSelector ? $(opts.singleRenameTargetSelector).first() : $();
var $sourceBlock = $(containerSelector + ' .rmMultiPageBlock').first();
var targets = collectMultiRenameTargetValues($sourceBlock).slice(0, 3);
var $variantContainer = opts.singleRenameVariantContainerId ? $('#' + opts.singleRenameVariantContainerId) : $();
if (!$sourceBlock.length || !$target.length) return;
$target.val(targets[0] || '');
if (!$variantContainer.length) return;
$variantContainer.empty();
targets.slice(1).forEach(function (value) {
addInputRow({
containerId: opts.singleRenameVariantContainerId,
placeholder: opts.singleRenameVariantPlaceholder || 'Дополнительный вариант',
inputClass: opts.singleRenameInputClass,
rowClass: 'rmRenameVariantRow',
indentLeft: 0,
fitParentWidth: true,
prefixHtml: buildLeftRemoveButtonHtml('rmRemoveInput', 'Удалить'),
removeBeforeInput: true
});
$variantContainer.find('input').last().val(value);
});
}
function updateMultiMode() {
var $blocks = $(containerSelector + ' .rmMultiPageBlock');
var hasExtra = $blocks.length > 1;
var pageGap = hasExtra && opts.targetVariants ? '12px' : multiNominationGap;
$(containerSelector).css({
gap: pageGap,
marginBottom: pageGap
});
$('#rmMultiHeader').css('marginBottom', hasExtra ? pageGap : multiNominationGap).toggle(hasExtra);
if (opts.multiOnlySelector) $(opts.multiOnlySelector).toggle(hasExtra);
if (opts.singleOnlySelector) $(opts.singleOnlySelector).toggle(!hasExtra);
if (opts.hideContainerWhenSingle) $(containerSelector).toggle(hasExtra);
if (!hasExtra && wasMultiModeExpanded) {
restoreEmptySinglePageInput();
copyFirstRowPageToSingleCurrent();
copyFirstRowRenameTargetsToSingle();
}
$blocks.each(function (index) {
setMultiPageRowControls($(this), !hasExtra && index === 0, hasExtra, opts);
});
wasMultiModeExpanded = hasExtra;
syncModalLayout();
}
$(document).off('click.rmMultiPageAdd').on('click.rmMultiPageAdd', '.rmAddMultiPage', function () {
var wasSingle = $(containerSelector + ' .rmMultiPageBlock').length <= 1;
if (wasSingle) {
copySingleCurrentPageToFirstRow();
copySingleRenameTargetsToFirstRow();
}
$(containerSelector).append(buildMultiPageRowHtml(pageCounter++, opts));
updateMultiMode();
});
$(document).off('click.rmMultiRenameVariantAdd').on('click.rmMultiRenameVariantAdd', '.rmAddMultiRenameVariant', function () {
var $block = $(this).closest('.rmMultiPageBlock');
var $container = $block.find('.rmMultiRenameVariantsContainer').first();
var fieldCount = 1 + $block.find('.rmMultiRenameVariantInput').length;
if (fieldCount >= 3) { alert('Максимум 3 варианта переименования.'); return; }
$container.append(buildMultiRenameVariantRowHtml('', opts)).show();
syncModalLayout();
});
$(document).off('click.rmMultiRenameVariantRemove').on('click.rmMultiRenameVariantRemove', '.rmRemoveMultiRenameVariant', function () {
$(this).closest('.rmMultiRenameVariantRow').remove();
syncModalLayout();
});
$(document).off('click.rmMultiPageComment').on('click.rmMultiPageComment', '.rmMultiPageCommentToggle', function () {
var $btn = $(this);
if (!$btn.is(':visible')) return;
setMultiPageCommentExpanded($btn, $btn.attr('aria-expanded') !== 'true');
syncModalLayout();
});
$(document).off('click.rmMultiPageRemove').on('click.rmMultiPageRemove', '.rmMultiPageRow .rmRemoveInput', function () {
$(this).closest('.rmMultiPageBlock').remove();
updateMultiMode();
});
updateMultiMode();
return {
update: updateMultiMode,
isMulti: function () { return $(containerSelector + ' .rmMultiPageBlock').length > 1; }
};
}
function setExclusiveButtonActive($root, buttonSelector, $btn) {
$root.find(buttonSelector).removeClass('is-active').attr('aria-pressed', 'false');
$btn.addClass('is-active').attr('aria-pressed', 'true');
}
function bindMultiNominationFormatSwitch(rootSelector, buttonSelector) {
var $root = $(rootSelector);
$root.off('click.rmMultiFormat').on('click.rmMultiFormat', buttonSelector, function () {
setExclusiveButtonActive($root, buttonSelector, $(this));
});
}
function buildProtectAddButtonHtml() {
return buildSquareAddButtonHtml('', 'Добавить страницу', 'rmProtectAddPage');
}
function buildProtectPageRowHtml(id, pageName, isFirstRow) {
return joinHtml([
'<div', isFirstRow ? ' id="rmProtectFirstRow"' : '', ' class="rmProtectPageRow ', RESIZE_CLASS, '" style="', stRow, '">',
'<input id="', id, '" type="text" placeholder="Страница" class="rmProtectPageInput" style="', stInputBox, '"',
pageName ? ' value="' + escapeHtml(pageName) + '"' : '', '>',
isFirstRow
? buildProtectAddButtonHtml()
: '<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить">−</button>',
'</div>'
]);
}
function buildReportFormHtml(ctx, isZka) {
var reportTextPlaceholder = (isZka ? 'Введите текст запроса.' : 'Текст запроса (можно дополнить или изменить).') + ' Подпись будет добавлена автоматически.';
if (isZka) {
return joinHtml([
'<input id="rmReportHeader" type="text" placeholder="Тема/заголовок" style="', stInputFull, '" value="', escapeHtml(ctx.pageLink), '">',
'<textarea id="rmReportText" placeholder="', reportTextPlaceholder, '"></textarea>',
buildQuickPhrasesPanelHtml('rmReportText')
]);
}
return joinHtml([
'<div id="rmProtectModeBtns" class="', RESIZE_CLASS, '" style="margin-bottom:14px;"><div class="rmSegmentedBar">',
'<button id="rmProtectModeInstall" type="button" class="rmSegmentedBtn rmProtectModeBtn is-active" aria-pressed="true">🛡️ Установить защиту</button>',
'<button id="rmProtectModeRemove" type="button" class="rmSegmentedBtn rmProtectModeBtn" aria-pressed="false">📛 Снять защиту</button>',
'</div></div>',
'<div id="rmProtectMultiWrap" class="', RESIZE_CLASS, '">',
'<div id="rmProtectHeaderWrap" style="display:none;margin-bottom:6px;"><div class="rmProtectHeaderRow" style="', stRow.replace('margin-bottom:6px;', 'margin-bottom:0;'), '"><input id="rmProtectHeader" type="text" placeholder="Заголовок (для нескольких страниц)" style="', stInputBox, '">', buildProtectAddButtonHtml(), '</div></div>',
buildProtectPageRowHtml('rmProtectPage0', ctx.pageName, true),
'<div id="rmProtectPagesContainer"></div>',
'</div>',
'<div id="rmProtectLevelsWrap" class="', RESIZE_CLASS, ' rmProtectControlGroup"><div class="rmProtectControlLabel">Уровень защиты</div><div id="rmProtectLevels" class="rmSegmentedBar">',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="полузащиту">полузащита</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="стабилизацию">стабилизация</button>',
'</div></div>',
'<div id="rmProtectReasonsWrap" class="', RESIZE_CLASS, ' rmProtectControlGroup"><div class="rmProtectControlLabel">Причины</div><div id="rmProtectReasons" class="rmSegmentedBar">',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="война правок">война правок</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="неконсенсусные изменения">неконсенсусные изменения</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="вандализм">вандализм</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="популярная статья">популярная статья</button>',
'</div></div>',
'<div id="rmRemoveLevelsWrap" class="', RESIZE_CLASS, ' rmProtectControlGroup" style="display:none;"><div class="rmProtectControlLabel">Уровень защиты</div><div id="rmRemoveLevels" class="rmSegmentedBar">',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="полузащиту">полузащита</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="стабилизацию">стабилизация</button>',
'</div></div>',
'<div id="rmProtectTextBlock" class="', RESIZE_CLASS, '"><textarea id="rmReportText" placeholder="', reportTextPlaceholder, '"></textarea>',
buildQuickPhrasesPanelHtml('rmReportText'), '</div>'
]);
}
// ═══════════════════════════════════════════════════════════════════════════
// ОБРАБОТЧИКИ ОПЕРАЦИЙ
// ═══════════════════════════════════════════════════════════════════════════
var handlers = {
// ── КБУ ─────────────────────────────────────────────────────────────
showKbu: function (op) {
var forCategory = !!(op && op.forCategory);
var reasons = getFastRemoveReasons();
createModal({
title: 'Быстрое удаление',
width: 'compact',
subtitleHtml: '<span id="rmKbuCriteriaLinkWrap"></span>'
});
$('#removerModalContent').html(buildKbuFormHtml(reasons));
$('#rmSel').change(function () { updateKbuReasonControls(reasons); syncModalLayout(); });
$('#removerModalContent').off('click.rmKbuCommonsReplacementMode').on('click.rmKbuCommonsReplacementMode', '.rmKbuCommonsReplacementModeBtn', function () {
setCurrentCommonsReplacementMode($(this).data('rmCommonsReplacementMode') || 'file');
});
$('#removerModalContent').off('click.rmKbuDfudReason').on('click.rmKbuDfudReason', '.rmKbuDfudReasonBtn', function () {
var $btn = $(this);
var active = !$btn.hasClass('is-active');
$btn.toggleClass('is-active', active).attr('aria-pressed', active ? 'true' : 'false');
});
$('#removerModalContent').off('click.rmKbuBadfairuseReason').on('click.rmKbuBadfairuseReason', '.rmKbuBadfairuseReasonBtn', function () {
setExclusiveButtonActive($('#rmKbuBadfairuseReasonsWrap'), '.rmKbuBadfairuseReasonBtn', $(this));
});
updateKbuReasonControls(reasons);
syncModalLayout();
renderModalFooter('submit', {
submitText: 'Номинировать',
onSubmit: function () {
var idx = $('#rmSel').val();
var reason = reasons[idx] || reasons[0];
var kbuData = buildFastRemoveTemplateData(reason);
if (kbuData.error) {
alert(kbuData.error);
return false;
}
var fastRemovalSummary = makeFastRemovalSummary(reason, kbuData);
startProcessing();
if (forCategory) {
var tpl = kbuData.templateText;
var categorySummary = fastRemovalSummary;
editPageContent(mwCfg.wgPageName, { summary: categorySummary, readError: 'Не удалось получить содержимое.' },
function (text) { return { text: wrapInNoinclude(text, T_OPEN + tpl + T_CLOSE) }; },
function (err) {
if (err) {
unlockModalSubmit();
logStatus('Ошибка записи.', err);
} else {
logStatus('Страница номинирована к быстрому удалению.', null, { trackError: false });
finalizeFastRemoval([normTitle(mwCfg.wgPageName)], categorySummary);
}
});
} else {
var job = {
mode: 'nominate', opId: 'fRm',
kbuTemplate: reason[0], kbuTemplateText: kbuData.templateText,
removeMoveToCommons: !!kbuData.removeMoveToCommons,
summary: fastRemovalSummary,
inArticle: true
};
processPageList([normTitle(mwCfg.wgPageName)], job, function (notifiedPages) {
finalizeFastRemoval(notifiedPages, job.summary);
});
}
return true;
}
});
},
// ── Универсальная номинация (КУ, КПМ, КУЛ, КОБ, КРАЗД, ВУС) ────────
showNomination: function (op) {
var nom = op.nomination;
var pg = normTitle(mwCfg.wgPageName);
var date = getDate()[1];
var nomPage = nom.nomPage(date);
var multiMode = nom.supportsMulti;
function updateTransferUi() {
var mode = getTransferModeFromButtons();
var isNone = mode === 'none';
var isKbu = mode === 'kbu' || mode === 'both';
var isKul = mode === 'kul' || mode === 'both';
$('#rmTransferBtnNone').toggleClass('is-active', isNone).attr('aria-pressed', isNone ? 'true' : 'false');
$('#rmTransferBtnKbu').toggleClass('is-active', isKbu).attr('aria-pressed', isKbu ? 'true' : 'false');
$('#rmTransferBtnKul').toggleClass('is-active', isKul).attr('aria-pressed', isKul ? 'true' : 'false');
var t = transferTexts[mode];
if (t) { $('#rmTransferHint').text(t.hint).show(); } else { $('#rmTransferHint').hide().text(''); }
applyGeneratedText($('#rmMsg'), t && t.notice ? t.notice + '\n' : '');
}
createModal({
title: 'Номинация: ' + nom.template,
subtitlePage: nomPage,
subtitleLabel: 'Текущий день'
});
$('#removerModalContent').html(buildNominationFormHtml(nom, pg, multiMode));
setupResizableModal('rmMsg');
// Логика переноса
if (nom.supportsTransfer) {
$(document).off('click.rmTransfer').on('click.rmTransfer', '#rmTransferBtnNone,#rmTransferBtnKbu,#rmTransferBtnKul', function () {
if (this.id === 'rmTransferBtnNone') {
$('#rmTransferBtnKbu,#rmTransferBtnKul').removeClass('is-active');
$('#rmTransferBtnNone').addClass('is-active');
} else {
$(this).toggleClass('is-active');
var anyOn = $('#rmTransferBtnKbu').hasClass('is-active') || $('#rmTransferBtnKul').hasClass('is-active');
$('#rmTransferBtnNone').toggleClass('is-active', !anyOn);
}
updateTransferUi();
});
updateTransferUi();
}
// Многостраничный режим
if (multiMode) {
setupMultiPageNominationUi(getMultiNominationUiOptions('article', {
setup: true,
defaultPage: pg,
renameMulti: nom.extraInput && nom.extraInput.type === 'rename',
deletionMulti: nom.template === 'к удалению'
}));
bindMultiNominationFormatSwitch('#removerModalContent', '.rmArticleMultiFormatBtn');
}
if (nom.extraInput) wireMultiInput(nom.extraInput);
renderModalFooter('submit', {
submitText: 'Номинировать',
showSubscribe: true,
onSubmit: function () {
var isMulti = multiMode && $('#rmMultiPagesContainer .rmMultiPageBlock').length > 1;
var singleCurrentInput = $('#rmSingleRenameCurrent').val() || '';
var inputVal = !isMulti ? normTitle(singleCurrentInput || $('#rmMultiPagesContainer .rmMultiPageInput').first().val() || '') : '';
var changed = inputVal && inputVal !== pg;
function executeJob(job) {
startProcessing();
runFlow({
templateStep: function (next) {
if (!job.inArticle) { next(); return; }
processPageList(job.pages, job, function (notifiedPages, err) {
job._notifiedPages = notifiedPages;
next(err);
});
},
nominationStep: function (done) {
publishNomination({
pageTitle: job.nomPage,
navTemplate: job.navTemplate,
sectionTitle: job.section,
summary: job.nominationSummary || job.summary,
text: getNominationPublishText(job)
}, function (err) { done(err, { pageTitle: job.nomPage, sectionTitle: job.section }); });
},
notifyStep: function (nominationInfo, next) {
var pages = job._notifiedPages || [];
if (!setAlert || !pages.length) { next(); return; }
notifyAuthorsForPages(pages, {
summary: job.summary,
actionText: job.comment,
discussionPage: nominationInfo && nominationInfo.pageTitle,
discussionSection: nominationInfo && nominationInfo.sectionTitle
}, next);
},
skipLink: op.id === 'fRm'
});
}
function run(targetPg) {
var job = buildNominationJob(op, targetPg, isMulti);
if (!job) { unlockModalSubmit(); return; }
if (job.isMulti && job.inArticle && getNominationConflictRule(job)) {
startProcessing();
inspectMultiNominationConflicts(job, function (err, conflicts) {
if (err) { markSubmitError(); return; }
if (!conflicts.length) { executeJob(job); return; }
showNominationConflictResolution(job, conflicts, function (resolvedJob) {
executeJob(resolvedJob);
});
});
return;
}
executeJob(job);
}
if (changed) {
apiReq({ prop: 'info', titles: inputVal }, 'query', function (data) {
if (data && data.error) {
unlockModalSubmit();
alert('Не удалось проверить страницу из-за ошибки API. Попробуйте ещё раз.');
return;
}
var page = getFirstQueryPage(data);
if (!page) {
unlockModalSubmit();
alert('Не удалось проверить страницу из-за временной ошибки. Попробуйте ещё раз.');
return;
}
if (page.missing !== undefined) {
unlockModalSubmit();
alert('Страница «' + inputVal + '» не существует.');
return;
}
run(normTitle(page.title || inputVal));
});
} else {
run(pg);
}
return true;
}
});
},
// ── Снятие номинации (статья) ────────────────────────────────────────
showArticleClose: function () {
showCloseActionsModal({
inputName: 'rmCloseAction',
listId: 'rmCloseActions',
emptyText: 'Не найдено подходящих шаблонов для закрытия.',
emptyDetails: 'Проверяются: КУ, КПМ, КБУ, КУЛ.',
getActions: function (articleText) {
var actions = [];
var hasKu = RE_KU_ON_PAGE.test(articleText);
var hasKpm = RE_KPM_ON_PAGE.test(articleText);
var hasKbu = RE_KBU_ON_PAGE.test(articleText);
var hasKul = RE_KUL_ON_PAGE.test(articleText);
if (hasKu) {
actions.push({ id: 'ret', tag: 'КУ', label: 'Оставлено', mode: 'denom', closeType: 'ret', resultTemplate: 'Оставлено', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{Оставлено}} на СО.', comment: 'оставлена', talkNotice: true });
actions.push({ id: 'retConditional', tag: 'КУ', label: 'Условно оставлено', mode: 'denom', closeType: 'retConditional', resultTemplate: 'Условно оставлено', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{Условно оставлено}} на СО.', comment: 'условно оставлена', talkNotice: true, needsConditionalFields: true });
actions.push({ id: 'withdrawnDeletion', tag: 'КУ', label: 'Снято с удаления', mode: 'denom', closeType: 'withdrawnDeletion', resultTemplate: 'Снято с удаления', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{Снято с удаления}} на СО.', comment: 'снята с удаления', talkNotice: true });
actions.push({ id: 'redirectedDeletion', tag: 'КУ', label: 'Заменено перенаправлением', mode: 'denom', closeType: 'redirectedDeletion', resultTemplate: 'Заменено перенаправлением', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, заменяет страницу перенаправлением, добавляет {{Заменено перенаправлением}} на СО.', warning: 'Осторожно: это действие перезапишет содержимое всей страницы.', comment: 'заменена перенаправлением', talkNotice: true, needsRedirectTarget: true });
}
if (hasKpm) {
actions.push({ id: 'doneRnm', tag: 'КПМ', label: 'Переименовано', mode: 'denom', closeType: 'doneRnm', resultTemplate: 'Переименовано', sourceTemplate: 'к переименованию|кпм|rename', description: 'Снимает шаблон КПМ, добавляет {{Переименовано}} на СО.', comment: 'переименована', talkNotice: true, needsOldTitle: true });
actions.push({ id: 'noRnm', tag: 'КПМ', label: 'Не переименовано', mode: 'denom', closeType: 'noRnm', resultTemplate: 'Не переименовано', sourceTemplate: 'к переименованию|кпм|rename', description: 'Снимает шаблон КПМ, добавляет {{Не переименовано}} на СО.', comment: 'не переименована', talkNotice: true });
}
if (hasKbu && hasKul) actions.push({ id: 'cleanup-both', tag: 'КБУ и КУЛ', label: 'Снятие', mode: 'cleanup', transferMode: 'both', cleanupLabel: 'КБУ и КУЛ', description: 'Снимает шаблоны КБУ/КУЛ и Hangon.' });
if (hasKbu) actions.push({ id: 'cleanup-kbu', tag: 'КБУ', label: 'Снятие', mode: 'cleanup', transferMode: 'kbu', cleanupLabel: 'КБУ', description: 'Снимает шаблоны КБУ и Hangon.' });
if (hasKul) actions.push({ id: 'cleanup-kul', tag: 'КУЛ', label: 'Снятие', mode: 'cleanup', transferMode: 'kul', cleanupLabel: 'КУЛ', description: 'Снимает шаблон КУЛ.' });
return actions;
},
afterRender: function (actions) {
var hasDoneRnm = actions.some(function (a) { return a.id === 'doneRnm'; });
var hasConditionalRet = actions.some(function (a) { return a.id === 'retConditional'; });
var hasRedirectedDeletion = actions.some(function (a) { return a.id === 'redirectedDeletion'; });
if (hasDoneRnm) {
$('#rmCloseActions input[value="doneRnm"]').closest('.rmActionItem').append(
'<div id="rmCloseOldTitleWrap" style="display:none;margin-top:6px;"><input id="rmCloseOldTitle" type="text" placeholder="Старое название" style="' + stInputFull + '"></div>'
);
}
if (hasConditionalRet) {
$('#rmCloseActions input[value="retConditional"]').closest('.rmActionItem').append(buildConditionalRetFieldsHtml());
}
if (hasRedirectedDeletion) {
$('#rmCloseActions input[value="redirectedDeletion"]').closest('.rmActionItem').append(
'<div id="rmCloseRedirectTargetWrap" style="display:none;margin-top:6px;"><input id="rmCloseRedirectTarget" type="text" placeholder="Цель перенаправления" style="' + stInputFull + '"></div>'
);
}
},
afterFooterRender: function (_, actionMap) {
function ensureConditionalTextareaResizer() {
var $textarea = $('#rmCloseConditionalReason');
if (!$textarea.length || $textarea.data('rmConditionalResizerReady')) return;
setupNestedResizableTextarea('rmCloseConditionalReason', 'rmCloseConditionalWrap', 280, 90);
$textarea.data('rmConditionalResizerReady', true);
}
function updateUi() {
var sel = actionMap[$('[name="rmCloseAction"]:checked').val()];
$('#rmCloseOldTitleWrap').toggle(!!(sel && sel.needsOldTitle));
$('#rmCloseConditionalWrap').toggle(!!(sel && sel.needsConditionalFields));
$('#rmCloseRedirectTargetWrap').toggle(!!(sel && sel.needsRedirectTarget));
if (sel && sel.needsConditionalFields) ensureConditionalTextareaResizer();
var disableNotify = !!(sel && sel.mode === 'cleanup' && sel.transferMode === 'kbu');
var $cb = $('[name="rmUAlert"]');
var $cbLabel = $('[name="rmUAlert"]').closest('label');
if ($cb.length) $cb.prop('disabled', disableNotify);
if ($cbLabel.length) $cbLabel.css({
visibility: disableNotify ? 'hidden' : 'visible',
pointerEvents: disableNotify ? 'none' : ''
});
syncModalLayout();
}
$(document).off('change.rmCloseAction').on('change.rmCloseAction', '[name="rmCloseAction"]', updateUi);
updateUi();
},
onSubmit: function (sel, pageName) {
var job;
if (sel.mode === 'denom') {
var oldTitle = sel.needsOldTitle ? ($('#rmCloseOldTitle').val() || '').trim() : '';
var conditionalReason = sel.needsConditionalFields ? normalizeQuickPhraseValue($('#rmCloseConditionalReason').val()) : '';
var conditionalDeadline = sel.needsConditionalFields ? String($('#rmCloseConditionalDeadline').val() || '').trim() : '';
var redirectTarget = sel.needsRedirectTarget ? normalizeRedirectTarget($('#rmCloseRedirectTarget').val()) : '';
if (sel.needsOldTitle && !oldTitle) { alert('Укажите старое название.'); return false; }
if (sel.needsRedirectTarget && !redirectTarget) { alert('Укажите цель перенаправления.'); return false; }
if (sel.needsRedirectTarget && normTitle(redirectTarget) === normTitle(pageName)) { alert('Цель перенаправления совпадает с текущей страницей.'); return false; }
if (conditionalDeadline && normalizeIsoDate(conditionalDeadline) !== conditionalDeadline) {
alert('Укажите срок в формате YYYY-MM-DD, например 2026-05-31.');
return false;
}
job = {
mode: 'denom',
closeType: sel.closeType,
resultTemplate: sel.resultTemplate,
sourceTemplate: sel.sourceTemplate,
oldTitle: oldTitle,
redirectTarget: redirectTarget,
conditionalReason: conditionalReason,
conditionalDeadline: conditionalDeadline,
notifyActionText: sel.comment,
skipNotify: false
};
} else {
job = {
mode: 'cleanup',
transferMode: sel.transferMode,
summary: makeSummary('снятие шаблонов ' + sel.cleanupLabel),
notifyActionText: (sel.transferMode === 'kul' || sel.transferMode === 'both')
? 'больше не номинирована к срочному улучшению'
: '',
skipNotify: !(sel.transferMode === 'kul' || sel.transferMode === 'both')
};
}
processPageList([pageName], job, function (notifiedPages, err, pageMeta) {
function finishClose() {
if (isError) { markSubmitError(); }
else { renderModalFooter('reload'); }
}
if (isError || err || job.skipNotify || !setAlert || !notifiedPages.length) { finishClose(); return; }
var meta = (pageMeta && pageMeta[normTitle(pageName)]) || {};
notifyAuthorsForPages(notifiedPages, {
summary: meta.summary || job.summary,
actionText: job.notifyActionText,
discussionPage: meta.discussionPage,
discussionSection: meta.discussionSection,
includeProposedPrefix: false
}, finishClose);
});
return true;
}
});
},
// ── ОБКАТ: номинация категории ───────────────────────────────────────
showCatNomination: function (op) {
var catType = op.catType;
var catMeta = CATEGORY_NOMINATION_META[catType] || CATEGORY_NOMINATION_META.discuss;
var now = new Date();
var catDiscPage = 'Википедия:Обсуждение категорий/' + MONTHS_NOM[now.getUTCMonth()] + '_' + now.getUTCFullYear();
var pageName = normalizeCategoryPageName(mwCfg.wgPageName);
var multiMode = !!catMeta.supportsMulti;
var renameMultiMode = !!catMeta.renameMulti && multiMode;
createModal({ title: catMeta.title, subtitlePage: catDiscPage, subtitleLabel: 'Текущий месяц' });
var variantCfgs = {
rename: { type: 'rename', firstId: 'firstRenameInput', addBtnId: 'addRenameVariant', containerId: 'renameVariantsContainer', addBtnLabel: '+ Добавить вариант', firstPh: 'Новое название без префикса Категория:', addPh: 'Дополнительный вариант названия', maxRows: 2, maxMsg: 'Максимум 3 варианта переименования.' },
merge: { firstId: 'firstMergeInput', addBtnId: 'addMergeVariant', containerId: 'mergeVariantsContainer', addBtnLabel: '+ Добавить вариант', firstPh: 'Категория для объединения без префикса Категория:', addPh: 'Дополнительный вариант объединения' }
};
var vCfg = variantCfgs[catType];
var categoryMultiOptions = {
renameMulti: renameMultiMode,
actionText: catMeta.actionText,
skipVariantInput: renameMultiMode
};
$('#removerModalContent').html(buildCategoryNominationFormHtml(vCfg, multiMode, pageName, categoryMultiOptions));
setupResizableModal('nominationReason');
if (multiMode) {
setupMultiPageNominationUi(getMultiNominationUiOptions('category', $.extend({
setup: true,
defaultPage: pageName
}, categoryMultiOptions)));
bindMultiNominationFormatSwitch('#removerModalContent', '.rmCategoryMultiFormatBtn');
}
if (vCfg) wireMultiInput(vCfg);
renderModalFooter('submit', {
submitText: 'Номинировать',
showSubscribe: true,
onSubmit: function () {
var reason = normalizeQuickPhraseValue($('#nominationReason').val());
var hasMultiRenameRows = renameMultiMode && $('#rmMultiPagesContainer .rmMultiPageBlock').length > 1;
var renamePairs = hasMultiRenameRows ? collectMultiRenamePairs({
normalizePageName: normalizeCategoryPageName,
normalizeTargetName: normalizeCategoryTargetPageName,
normalizeTemplateTargetName: normalizeCategoryTargetName
}) : [];
var renameTargetsByCategory = buildMultiRenameTargetMap(renamePairs, 'targetNames');
var renameTemplateTargetsByCategory = buildMultiRenameTargetMap(renamePairs, 'templateTargetNames');
var singleRenameCategory = renameMultiMode && !hasMultiRenameRows
? normalizeCategoryPageName($('#rmSingleRenameCurrent').val() || pageName)
: '';
var targetPages = hasMultiRenameRows
? renamePairs.map(function (pair) { return pair.pageName; })
: (multiMode ? collectCategoryPageInputValues('.rmMultiPageInput') : [pageName]);
if (renameMultiMode && !hasMultiRenameRows) targetPages = [singleRenameCategory || pageName];
var isMulti = renameMultiMode ? hasMultiRenameRows : (multiMode && targetPages.length > 1);
var multiFormat = $('.rmCategoryMultiFormatBtn.is-active').data('rmMultiFormat') || 'sections';
var commentsByCategory = isMulti ? collectMultiNominationComments(normalizeCategoryPageName) : {};
var notifiedPages = [];
if (hasMultiRenameRows && !validateMultiRenamePairs(renamePairs, 'категорию', 'новое название')) return false;
if (!targetPages.length) { alert('Укажите категорию.'); return false; }
if (isMulti) {
if (!validateMultiNominationText(targetPages, reason, commentsByCategory, 'категории')) return false;
} else if (!reason) {
alert('Пожалуйста, укажите причину/тему.');
return false;
}
var discussionTarget = isMulti ? targetPages : targetPages[0];
var discussionReason = isMulti
? (multiFormat === 'list'
? buildMultiNominationListText(targetPages, reason, commentsByCategory, renameMultiMode ? getMultiRenameDiscussionOptions(renameTargetsByCategory) : null)
: buildMultiNominationText(targetPages, reason, commentsByCategory, renameMultiMode
? getMultiRenameDiscussionOptions(renameTargetsByCategory, { headingLevel: 4 })
: { headingLevel: 4 }))
: reason;
var discussionOptions = isMulti ? {
headerText: $('#rmHeader').val(),
reasonIsPrepared: true,
renameTargetsByPage: renameTargetsByCategory
} : null;
var mainName = null, additionalNames = [];
if (vCfg) {
if (hasMultiRenameRows) {
mainName = renamePairs[0] && renamePairs[0].templateTargetName;
additionalNames = renamePairs[0] ? asNonEmptyArray(renamePairs[0].templateTargetNames).slice(1) : [];
} else {
mainName = $('#' + vCfg.firstId).val().trim();
if (!mainName) { alert('Укажите ' + (catType === 'rename' ? 'новое название' : 'категорию для объединения') + '.'); return false; }
additionalNames = collectInputValues('#' + vCfg.containerId + ' .variantInput');
}
}
var categoryDiscussionTitle = buildCategoryDiscussionTitle(discussionTarget, catType, mainName, additionalNames, discussionOptions || {});
var categorySectionTitle = getCategoryDiscussionSectionTitle(categoryDiscussionTitle);
var categoryTemplateOptions = hasMultiRenameRows ? { renameTemplateTargetsByPage: renameTemplateTargetsByCategory } : {};
categoryTemplateOptions.summary = makeNominationTargetSummary(catDiscPage, categorySectionTitle, catMeta.actionText);
startProcessing();
runFlow({
templateStep: function (next) {
addTemplatesToCategories(targetPages, catType, mainName, additionalNames, function (err, processedPages) {
notifiedPages = processedPages || [];
next(err);
}, categoryTemplateOptions);
},
nominationStep: function (done) {
createCategoryDiscussion(discussionTarget, discussionReason, catType, function (err, nominationInfo) { done(err, nominationInfo || null); }, mainName, additionalNames, discussionOptions);
},
notifyStep: function (nominationInfo, next) {
if (!setAlert || !nominationInfo) { next(); return; }
var section = normalizeSectionForLink(nominationInfo.sectionTitle || '');
notifyAuthorsForPages(notifiedPages.length ? notifiedPages : targetPages, {
summary: makeSummary('номинация [[' + nominationInfo.pageTitle + (section ? '#' + section : '') + ']]'),
actionText: catMeta.actionText,
discussionPage: nominationInfo.pageTitle,
discussionSection: nominationInfo.sectionTitle
}, next);
}
});
return true;
}
});
},
// ── Снятие номинации (категория) ─────────────────────────────────────
showCatClose: function () {
showCloseActionsModal({
inputName: 'rmCategoryCloseAction',
showCheckbox: false,
emptyText: 'Не найдено подходящих шаблонов для завершения.',
emptyDetails: 'Проверяются ОБКАТ и КУ.',
getActions: function (catText) {
var allObkat = [cfg.categoryTemplates.discuss, cfg.categoryTemplates.rename, cfg.categoryTemplates.merge].join('|');
var actions = [];
if (new RegExp('\\{\\{\\s*(?:' + allObkat + ')\\s*(?:\\||\\}\\})', 'i').test(catText)) {
actions.push({ id: 'cat-obkat-done', tag: 'ОБКАТ', label: 'Завершено', mode: 'obkat', talkTemplate: 'Обсуждавшаяся категория', description: 'Снимает шаблон ОБКАТ, добавляет {{Обсуждавшаяся категория}} на СО.', talkNotice: true });
}
if (RE_KU_ON_PAGE.test(catText)) {
actions.push({ id: 'cat-ku-cleanup', tag: 'КУ', label: 'Снятие', mode: 'cleanup', description: 'Снимает шаблон КУ без записи на СО.' });
}
return actions;
},
onSubmit: function (sel, pageName) {
if (sel.mode === 'obkat') markCategoryDiscussionAsDone(pageName);
if (sel.mode === 'cleanup') removeKuFromCategory(pageName);
return true;
}
});
},
// ── Защита / Запрос к администраторам ───────────────────────────────
showReport: function (op) {
var mode = op.reportMode || 'protect';
var ctx = getReporterContext(mode);
var isZka = mode === 'request';
var protectMode = 'install';
var pageCounter = 1;
function buildProtectText(pm) {
if (pm === 'remove') {
var removeLevels = [];
$('#rmRemoveLevels .rmToggleBtn.is-active').each(function () { removeLevels.push($(this).data('label')); });
return removeLevels.length ? 'Просьба снять ' + removeLevels.join(' и/или ') + '.' : '';
}
var levels = [], reasons = [];
$('#rmProtectLevels .rmToggleBtn.is-active').each(function () { levels.push($(this).data('label')); });
$('#rmProtectReasons .rmToggleBtn.is-active').each(function () { reasons.push($(this).data('label')); });
if (!levels.length && !reasons.length) return '';
var text = 'Просьба установить';
if (levels.length) text += ' ' + levels.join(' и/или ');
if (reasons.length) text += ' по причине: ' + reasons.join(', ');
return text + '.';
}
function applyProtectMode(m) {
protectMode = m;
var isInstall = m === 'install';
$('#rmProtectModeInstall').toggleClass('is-active', isInstall).attr('aria-pressed', isInstall ? 'true' : 'false');
$('#rmProtectModeRemove').toggleClass('is-active', !isInstall).attr('aria-pressed', !isInstall ? 'true' : 'false');
$('#removerModalTitleText').text(isInstall ? 'Запрос на защиту страницы' : 'Запрос на снятие защиты');
var linkPage = isInstall ? 'Википедия:Установка защиты' : 'Википедия:Снятие защиты';
$('#rmProtectLinkWrap').html('<a href="' + getPageUrl(linkPage) + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">' + escapeHtml(linkPage) + '</a>');
$('#rmProtectLevelsWrap,#rmProtectReasonsWrap').toggle(isInstall);
$('#rmRemoveLevelsWrap').toggle(!isInstall);
$('#rmProtectLevels .rmProtectOptBtn,#rmProtectReasons .rmProtectOptBtn,#rmRemoveLevels .rmProtectOptBtn').removeClass('is-active');
$('#rmReportText').val('').removeData('rmGenerated');
}
function updateProtectMultiUi() {
var $rows = $('#rmProtectMultiWrap .rmProtectPageRow');
var hasExtra = $rows.length > 1;
if (!$rows.length) {
$('#rmProtectPagesContainer').append(buildProtectPageRowHtml('rmProtectPage' + pageCounter++, ctx.pageName, true));
$rows = $('#rmProtectMultiWrap .rmProtectPageRow');
hasExtra = false;
}
$('#rmProtectHeaderWrap').toggle(hasExtra);
if (!hasExtra) $('#rmProtectHeader').val('');
$rows.each(function () {
var $row = $(this);
$row.find('.rmProtectAddPage,.rmRemoveInput').remove();
$row.append(hasExtra
? '<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить">−</button>'
: buildProtectAddButtonHtml()
);
});
syncModalLayout();
}
createModal({
title: isZka ? 'Запрос к администраторам' : 'Запрос на защиту страницы',
width: 'compact',
subtitleHtml: isZka
? '<a href="' + getPageUrl('Википедия:Запросы к администраторам') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Википедия:Запросы к администраторам</a>' +
' · <a href="' + getPageUrl('Википедия:Запросы к администраторам/Быстрые') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Быстрые</a>'
: '<span id="rmProtectLinkWrap"></span>'
});
$('#removerModalContent').html(buildReportFormHtml(ctx, isZka));
if (!isZka) {
$('#removerModalContent')
.on('click', '#rmProtectModeInstall', function () { applyProtectMode('install'); })
.on('click', '#rmProtectModeRemove', function () { applyProtectMode('remove'); })
.on('click', '.rmProtectOptBtn', function () {
$(this).toggleClass('is-active');
if (protectMode === 'install') {
var $levels = $('#rmProtectLevels .rmProtectOptBtn.is-active');
if ($(this).closest('#rmProtectReasons').length && $(this).hasClass('is-active') && $levels.length === 0) {
$('#rmProtectLevels .rmProtectOptBtn').first().addClass('is-active');
}
if ($(this).closest('#rmProtectLevels').length && !$(this).hasClass('is-active') && $levels.length === 0) {
$('#rmProtectReasons .rmProtectOptBtn').removeClass('is-active');
}
}
applyGeneratedText($('#rmReportText'), buildProtectText(protectMode));
});
$(document)
.off('click.rmProtectAdd').on('click.rmProtectAdd', '.rmProtectAddPage', function () {
var id = 'rmProtectPage' + pageCounter++;
$('#rmProtectPagesContainer').append(buildProtectPageRowHtml(id, '', false));
updateProtectMultiUi();
})
.off('click.rmProtectRemove').on('click.rmProtectRemove', '.rmProtectPageRow .rmRemoveInput', function () {
$(this).closest('.rmProtectPageRow').remove(); updateProtectMultiUi();
});
applyProtectMode('install');
}
setupResizableModal('rmReportText');
renderModalFooter('submit', {
showCheckbox: false,
showSubscribe: true,
submitText: 'Отправить',
onSubmit: function () { doReport(ctx, false, protectMode); return true; }
});
if (isZka) {
$('<button id="rmReportFast" style="' + stCancel + '">Быстрый запрос</button>').insertBefore('#removerSubmit');
$('#rmReportFast').click(function () {
if ($('#removerSubmit').data('rmSubmitInProgress')) return;
$('#removerSubmit').data('rmSubmitInProgress', true).prop('disabled', true);
$('#rmReportFast').prop('disabled', true);
doReport(ctx, true, protectMode);
});
}
}
};
// ═══════════════════════════════════════════════════════════════════════════
// ВСПОМОГАТЕЛЬНЫЕ: КБУ, закрытие, категории
// ═══════════════════════════════════════════════════════════════════════════
function getFastRemoveCriteriaAnchorFromConfig(templateName) {
var anchors = cfg.fastRemoveCriteriaAnchors || {};
var template = String(templateName || '').trim();
var lower = template.toLowerCase();
var key;
if (!template) return '';
if (Object.prototype.hasOwnProperty.call(anchors, template)) return anchors[template];
for (key in anchors) {
if (Object.prototype.hasOwnProperty.call(anchors, key) && String(key).toLowerCase() === lower) {
return anchors[key];
}
}
return '';
}
function getFastRemoveCriteriaAnchor(reason) {
var configured = reason ? getFastRemoveCriteriaAnchorFromConfig(reason[0]) : '';
var template, label, m;
if (configured) return configured;
template = String(reason && reason[0] || '');
label = String(reason && reason[1] || '');
if (/^(?:подст\s*:\s*)?(?:deleteslow|ds)$/i.test(template) || /^\s*ds\b/i.test(label)) return 'С1';
m = label.match(/^\s*([А-ЯЁA-Z]{1,3}\d+)(?:\.\d+)?/);
return m ? m[1] : '';
}
function buildFastRemoveCriteriaLinkHtml(reason) {
var anchor = getFastRemoveCriteriaAnchor(reason);
var label = KBU_CRITERIA_PAGE + (anchor ? '#' + anchor : '');
var url = getPageUrlWithFragment(KBU_CRITERIA_PAGE, anchor);
return '<a href="' + escapeHtml(url) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' +
escapeHtml(label) + '</a>';
}
function getFastRemoveReasons() {
var reasons = cfg.fastRemoveReasons;
var prefix = (mwCfg.wgIsRedirect ? 'ОП' : 'О') + ({ 0: 'С', 2: 'У', 3: 'У', 6: 'Ф', 14: 'К' }[mwCfg.wgNamespaceNumber] || '');
var all = [];
if (isCategory && reasons.categories) all = all.concat(reasons.categories);
['general','articles','redirects','files','users','special'].forEach(function (k) { if (reasons[k]) all = all.concat(reasons[k]); });
if (!isCategory && reasons.categories) all = all.concat(reasons.categories);
return all.filter(function (r) { return prefix.indexOf(r[2] !== undefined ? r[2] : r[1].charAt(0)) >= 0; });
}
function showCloseActionsModal(opts) {
createModal({ title: 'Снятие шаблонов номинаций', inline: true });
$('#removerModalContent').html('<p style="margin:0;">Определение доступных действий...</p>');
var pageName = normTitle(mwCfg.wgPageName);
getText(pageName, function (pageText, readErr) {
if (readErr) { showInfoAndClose('Не удалось прочитать содержимое страницы.', readErr.info || readErr.code || '', true); return; }
if (pageText === null) { showInfoAndClose('Не удалось прочитать содержимое страницы.', '', true); return; }
var actions = opts.getActions(pageText);
if (!actions.length) { showInfoAndClose(opts.emptyText, opts.emptyDetails || ''); return; }
var actionMap = actions.reduce(function (m, a) { m[a.id] = a; return m; }, {});
$('#removerModalContent').html(buildActionsHtml(actions, opts.inputName, opts.listId));
$('#removerModalContent').off('change.rmActionWarnings').on('change.rmActionWarnings', 'input[name="' + opts.inputName + '"]', function () {
syncActionWarnings(opts.inputName, opts.listId);
syncModalLayout();
});
if (opts.afterRender) opts.afterRender(actions, actionMap, pageName, pageText);
renderModalFooter('submit', {
showCheckbox: opts.showCheckbox,
submitText: 'Выполнить',
onSubmit: function () {
var sel = getSelectedAction(opts.inputName, actionMap);
if (!sel) return false;
startProcessing();
return opts.onSubmit(sel, pageName, pageText, actionMap) !== false;
}
});
if (opts.afterFooterRender) opts.afterFooterRender(actions, actionMap, pageName, pageText);
syncActionWarnings(opts.inputName, opts.listId);
});
}
function runPageEditWithStatus(opts) {
var o = opts || {};
var statusId = logStatus(o.pendingText, null, { pending: true, trackError: false });
editPageContent(o.pageName, o.editOptions, o.buildFn, function (err, meta) {
if (err) { logStatus(o.errorText, err, { statusId: statusId }); unlockModalSubmit(); return; }
logStatus(o.successText, null, { statusId: statusId, trackError: false });
if (o.onSuccess) o.onSuccess(meta || null);
});
}
function removeKuFromCategory(pageName) {
runPageEditWithStatus({
pageName: pageName,
pendingText: 'Снимается шаблон КУ...',
errorText: 'Снятие шаблона КУ.',
successText: 'Шаблон КУ снят.',
editOptions: { summary: makeSummary('снятие шаблона КУ'), watchlist: 'nochange', readError: 'Страница не существует.', readErrorCode: 'read_failed' },
buildFn: function (text) {
var r = stripTemplatesByPattern(text, '(?:к\\s*удалению|ку)');
if (!r.removed) return { error: { code: 'no_changes', info: 'Шаблон КУ не найден.' } };
return { text: r.text.replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n') };
},
onSuccess: function () {
logStatus('Шаблон на СО не устанавливался.', null, { trackError: false });
renderModalFooter('reload');
}
});
}
// ── Категории: добавление шаблона ────────────────────────────────────────
function addTemplateToCategory(pageName, type, mainName, additionalNames, callback, options) {
var cb = callback || function () {};
var opts = options || {};
var cfgByType = {
discuss: { action: 'обсуждение', template: 'Обсуждаемая категория', aliases: cfg.categoryTemplates.discuss },
deletion: { action: 'удаление', template: 'Обсуждаемая категория', aliases: cfg.categoryTemplates.discuss },
rename: { action: 'переименование', template: 'Категория к переименованию', needsMain: true, aliases: cfg.categoryTemplates.rename },
merge: { action: 'объединение', template: 'Категория к объединению', needsMain: true, aliases: cfg.categoryTemplates.merge }
};
var typeCfg = cfgByType[type];
if (!typeCfg) { cb({ code: 'error', info: 'Неизвестный тип номинации.' }); return; }
var dateStr = getDate()[0];
var parts = [dateStr];
if (typeCfg.needsMain) {
if (!mainName) { cb({ code: 'error', info: 'Не указано основное название.' }); return; }
parts.push(mainName);
if (additionalNames && additionalNames.length) Array.prototype.push.apply(parts, additionalNames);
}
var tplText = T_OPEN + typeCfg.template + '|' + parts.join('|') + T_CLOSE;
editPageContent(pageName, { summary: opts.summary || makeSummary('добавление шаблона номинации на ' + typeCfg.action), readError: 'Не удалось получить содержимое.' },
function (text) {
if (hasTemplateWithDateByPattern(text, typeCfg.aliases, dateStr)) {
return { skip: true, meta: { status: 'already_present' } };
}
return { text: wrapInNoinclude(text, tplText) };
},
function (err, meta) {
if (!err && meta && meta.status === 'already_present') {
logStatus('На странице ' + buildQuotedStatusPageLink(pageName) + ' уже есть шаблон номинации с датой ' + dateStr + '.', null, { trackError: false });
} else {
logPageEdit(pageName, err);
}
if (err) { cb(err); return; }
if (type === 'merge') {
addMergeTemplatesToTargets(pageName, mainName, additionalNames, dateStr, opts.summary, function () { cb(null); });
return;
}
cb(null);
}
);
}
function collectCategoryPageInputValues(selector) {
var pages = [];
$(selector).each(function () {
var title = normalizeCategoryPageName($(this).val() || '');
if (title && pages.indexOf(title) === -1) pages.push(title);
});
return pages;
}
function addTemplatesToCategories(pages, type, mainName, additionalNames, callback, options) {
var cb = callback || function () {};
var opts = options || {};
var processedPages = [];
eachSequential(pages || [], function (pageName, next) {
var pageMainName = mainName;
var pageAdditionalNames = additionalNames;
var pageTargets;
if (type === 'rename' && opts.renameTemplateTargetsByPage) {
pageTargets = opts.renameTemplateTargetsByPage[normTitle(pageName)];
if (Array.isArray(pageTargets)) {
pageMainName = pageTargets[0] || mainName;
pageAdditionalNames = pageTargets.slice(1);
} else if (pageTargets) {
pageMainName = pageTargets;
pageAdditionalNames = [];
}
}
addTemplateToCategory(pageName, type, pageMainName, pageAdditionalNames, function (err) {
if (!err) processedPages.push(pageName);
next(err || null);
}, { summary: opts.summary });
}, function (err) { cb(err, processedPages); });
}
function addMergeTemplatesToTargets(sourcePage, mainName, additionalNames, dateStr, summary, callback) {
var cb = callback || function () {};
var currentCatName = normTitle(stripCatPrefix(sourcePage));
var targets = [mainName].concat(additionalNames || []);
if (!targets.length) { cb(); return; }
eachSequential(targets, function (target, next) {
var targetPage = normalizeCategoryPageName(target);
addMergeTemplateToTargetCategory(targetPage, currentCatName, dateStr, summary, function (success, status) {
var url = getPageUrl(targetPage);
var linkHtml = '<a href="' + escapeHtml(url) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' + escapeHtml(targetPage) + '</a>';
if (success) {
var extra = (status === 'already_exists' || status === 'updated') ? ' (' + formatMergeStatus(status) + ')' : '';
logStatus('Шаблон добавлен в ' + linkHtml + extra + '.', null, { trackError: false });
} else {
logStatus('Ошибка при добавлении шаблона в ' + linkHtml + '.', { code: 'merge_target_failed', info: status }, { trackError: false });
}
next();
});
}, cb);
}
function addMergeTemplateToTargetCategory(targetPageName, sourceCatName, dateStr, summary, callback) {
if (typeof summary === 'function') {
callback = summary;
summary = '';
}
editPageContent(targetPageName, { summary: summary || makeSummary('добавление шаблона объединения'), readError: 'Не удалось получить содержимое' },
function (text) {
var existing = text.match(getCategoryMergeRe());
if (existing) {
var cats = existing[1].split('|').slice(1).map(function (p) { return p.trim(); }).filter(function (p) { return p.indexOf('=') === -1 && p.length > 0; });
var norm = sourceCatName.replace(/\s+/g, ' ').trim();
if (cats.some(function (c) { return c.replace(/\s+/g, ' ').trim() === norm; })) { return { skip: true, meta: { status: 'already_exists' } }; }
return {
text: text.replace(existing[0], function () { return existing[0].replace(/\}\}\s*$/, '|' + sourceCatName + '}}'); }),
summary: summary || makeSummary('дополнение шаблона объединения ' + formatCatLink(sourceCatName)),
meta: { status: 'updated' }
};
}
return { text: wrapInNoinclude(text, T_OPEN + 'Категория к объединению|' + dateStr + '|' + sourceCatName + T_CLOSE), meta: { status: 'created' } };
},
function (err, meta) { callback(!err, err ? err.info : ((meta && meta.status) || 'updated')); }
);
}
// ── Категории: обсуждение ────────────────────────────────────────────────
function buildCategoryDiscussionTitle(pageName, type, mainName, additionalNames, options) {
var opts = options || {};
var pages = Array.isArray(pageName) ? pageName : null;
var titleText;
if (pages && pages.length) {
titleText = String(opts.headerText || '').trim();
if (!titleText && type === 'rename' && opts.renameTargetsByPage) titleText = formatRenameItemsWithAnd(pages, opts.renameTargetsByPage);
if (!titleText) titleText = formatPagesWithAnd(pages, ':') + (type === 'deletion' ? ' → удалить' : '');
return '=== ' + titleText + ' ===';
}
var title = '=== [[:' + pageName + ']]';
if (type === 'rename' || type === 'merge') {
title += (type === 'rename' ? ' → ' : ' объединить с ') + formatCatLink(mainName);
if (additionalNames && additionalNames.length) {
var conj = type === 'rename' ? ' или ' : ' и ';
var head = additionalNames.slice(0, -1).map(formatCatLink).join(', ');
title += (additionalNames.length > 1 ? ', ' + head : '') + conj + formatCatLink(additionalNames[additionalNames.length - 1]);
}
} else if (type === 'deletion') {
title += ' → удалить';
}
return title + ' ===';
}
function getCategoryDiscussionSectionTitle(discussionTitle) {
return String(discussionTitle || '').replace(/^===\s*/, '').replace(/\s*===\s*$/, '').trim();
}
function createCategoryDiscussion(pageName, reason, type, callback, mainName, additionalNames, options) {
var cb = callback || function () {};
var opts = options || {};
var now = new Date();
var m = now.getUTCMonth(), year = now.getUTCFullYear(), day = now.getUTCDate();
var dateHeader = '== ' + day + ' ' + MONTHS_GEN[m] + ' ' + year + ' ==';
var discPage = 'Википедия:Обсуждение категорий/' + MONTHS_NOM[m] + '_' + year;
var discTitle = buildCategoryDiscussionTitle(pageName, type, mainName, additionalNames, opts);
var discBody = (opts.reasonIsPrepared ? String(reason || '') : appendNominationSignature(reason)).replace(/^\s+|\s+$/g, '');
var discText = discTitle + '\n' + discBody + '\n';
var sectionTitle = getCategoryDiscussionSectionTitle(discTitle);
publishNomination({
pageTitle: discPage,
readErrorMessage: 'Не удалось получить содержимое страницы обсуждения.',
summary: makeNominationPageSummary(sectionTitle),
createText: function () {
return T_OPEN + 'ОБК-Навигация' + T_CLOSE + '\n\n' + dateHeader + '\n\n' + discText;
},
buildText: function (text) {
var todayMatch = new RegExp('^' + escapeRegExp(dateHeader) + '\\s*$', 'm').exec(text);
if (!/\{\{\s*ОБК-Навигация\s*\}\}/i.test(text)) {
return { error: { code: 'insert_failed', info: 'Не найден шаблон ' + T_OPEN + 'ОБК-Навигация' + T_CLOSE + '.' } };
}
if (todayMatch) {
var dayContentStart = todayMatch.index + todayMatch[0].length;
var nextDayMatch = /^==[^=\n].*==\s*$/m.exec(text.slice(dayContentStart));
var insertAt = nextDayMatch ? dayContentStart + nextDayMatch.index : text.length;
return { text: insertDiscussionBlockAt(text, insertAt, discText, '\n\n') };
}
return { text: insertTopDiscussionSection(text, dateHeader + '\n\n' + discText) };
}
}, function (err) {
if (err) { cb(err); return; }
cb(null, { pageTitle: discPage, sectionTitle: sectionTitle });
});
}
// ── Категории: завершение ОБКАТ ───────────────────────────────────────────
function markCategoryDiscussionAsDone(pageName) {
runPageEditWithStatus({
pageName: pageName,
pendingText: 'Снимается шаблон обсуждения...',
errorText: 'Снятие шаблона обсуждения.',
successText: 'Шаблон обсуждения снят.',
editOptions: { summary: makeSummary('обсуждение категории завершено'), readError: 'Не удалось получить содержимое.' },
buildFn: function (text) {
var allTpls = [cfg.categoryTemplates.discuss, cfg.categoryTemplates.rename, cfg.categoryTemplates.merge].join('|');
var patterns = [
new RegExp('<noinclude>\\s*\\{\\{\\s*(' + allTpls + ')\\s*\\|\\s*([^\\}]+?)\\s*\\}\\}\\s*</noinclude>', 'i'),
new RegExp('\\{\\{\\s*(' + allTpls + ')\\s*\\|\\s*([^\\}]+?)\\s*\\}\\}', 'i')
];
var match = null;
for (var i = 0; i < patterns.length; i++) { match = text.match(patterns[i]); if (match) break; }
if (!match) return { error: { code: 'no_changes', info: 'Шаблон обсуждаемой категории не найден.' } };
return {
text: text.replace(match[0], '').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n'),
meta: { tplDate: convertToStandardDate(match[2].split('|')[0].trim()) }
};
},
onSuccess: function (meta) {
var talkStatusId = logStatus('Обновляется шаблон на СО категории...', null, { pending: true, trackError: false });
updateCategoryTalkPage(pageName, meta.tplDate, function (talkErr, info) {
if (talkErr) { logStatus('Установка шаблона на СО.', talkErr, { statusId: talkStatusId }); unlockModalSubmit(); return; }
logStatus(
(info && (info.status === 'already_present' || info.status === 'no_changes')) ? 'Шаблон на СО уже установлен.' : 'Шаблон установлен на СО.',
null, { statusId: talkStatusId, trackError: false }
);
renderModalFooter('reload');
});
}
});
}
function updateCategoryTalkPage(categoryName, templateDate, callback) {
var cb = callback || function () {};
var talkPage = getTalkPage(categoryName);
var newTpl = T_OPEN + 'Обсуждавшаяся категория|' + templateDate + T_CLOSE;
getTextWithTimestamp(talkPage, function (text, baseTimestamp, readErr) {
if (readErr) { cb(makeReadError(readErr, 'talk_read_failed', 'Не удалось получить содержимое СО категории.')); return; }
if (text === null) {
apiReq({ title: talkPage, text: newTpl + '\n\n', summary: makeSummary('добавление шаблона [[ш:Обсуждавшаяся категория]] с датой ' + templateDate), createonly: true },
'edit', function (resp) {
if (resp && resp.error) {
if (resp.error.code === 'articleexists') setTimeout(function () { updateCategoryTalkPage(categoryName, templateDate, cb); }, 1000);
else cb(resp.error);
} else cb(null, { status: 'created' });
});
return;
}
var talkResult = upsertDateSectionTemplateOnTalkPage(text, 'Обсуждавшаяся категория', templateDate, '', { aliases: cfg.categoryTemplates.discussed });
var newText = talkResult.text;
if (newText === text) { cb(null, { status: talkResult.status || 'no_changes' }); return; }
var summary = talkResult.status === 'created'
? makeSummary('добавление шаблона [[ш:Обсуждавшаяся категория]] с датой ' + templateDate)
: makeSummary('обновление шаблона [[ш:Обсуждавшаяся категория]], добавлена дата ' + templateDate);
var ep = {
title: talkPage,
text: newText,
summary: summary
};
if (baseTimestamp) ep.basetimestamp = baseTimestamp;
apiReq(ep, 'edit', function (resp) { cb(resp && resp.error ? resp.error : null, resp && !resp.error ? { status: talkResult.status } : null); });
});
}
// ── Быстрое объединение (Ctrl+клик КОБ) ─────────────────────────────────
function buildQuickMergeHtml(tplDate, targets, currentCatName) {
return joinHtml([
'<p>Найден шаблон с датой <strong>', escapeHtml(tplDate), '</strong>. Категории для объединения:</p>',
'<pre style="background:', tk.bgBase, ';color:', tk.cBase, ';padding:10px;border:1px solid ', tk.bSubS, ';border-radius:4px;margin-bottom:10px;">',
targets.map(function (c) { return '• ' + escapeHtml(c); }).join('\n'),
'</pre>',
'<p><strong>Текущая категория:</strong> ', escapeHtml(currentCatName), '</p>',
'<p style="color:', tk.cSub, ';">Шаблон будет добавлен во все указанные выше категории.</p>'
]);
}
function showQuickMergeModal() {
getText(mwCfg.wgPageName, function (text, readErr) {
if (readErr) { alert('Не удалось получить содержимое: ' + (readErr.info || readErr.code || 'ошибка API') + '.'); return; }
if (!text) { alert('Не удалось получить содержимое.'); return; }
var mergeRe = getCategoryMergeRe();
var match = text.match(mergeRe);
if (!match) { alert('В текущей категории не найден шаблон "Категория к объединению".'); return; }
var params = match[1].split('|').map(function (p) { return p.trim(); });
var tplDate = params[0];
var targets = params.slice(1);
if (!targets.length) { alert('В шаблоне не найдены целевые категории.'); return; }
createModal({ title: 'Быстрое добавление шаблона объединения' });
var currentCatName = normTitle(stripCatPrefix(mwCfg.wgPageName));
$('#removerModalContent').html(buildQuickMergeHtml(tplDate, targets, currentCatName));
renderModalFooter('submit', {
showCheckbox: false,
submitText: 'Добавить шаблоны',
onSubmit: function () {
startProcessing();
$('#removerSubmit').prop('disabled', true);
eachSequential(targets, function (target, next) {
var targetPage = normalizeCategoryPageName(target);
var targetLink = buildStatusPageLink(targetPage);
addMergeTemplateToTargetCategory(targetPage, currentCatName, tplDate, function (success, status) {
if (success) logStatus('Категория ' + targetLink + ' (' + formatMergeStatus(status) + ').', null, { trackError: false });
else logStatus('Ошибка ' + targetLink + '.', { code: 'merge_target_failed', info: status }, { trackError: false });
next();
});
}, function () {
if (isError) markSubmitError(); else renderModalFooter('close');
});
return true;
}
});
});
}
// ── ЗКА/Защита: публикация ───────────────────────────────────────────────
function getReporterContext(mode) {
var rawPage = mwCfg.wgPageName;
var pageName = normTitle(rawPage)
.replace(/(Special|Служебная):(Contributions|Вклад)\//i, 'User:')
.replace(/(User talk:|Обсуждение участни(ка|цы):)/i, 'User:');
var isUserRelated = /user|contrib|участни|вклад/i.test(rawPage);
var displayName = normTitle(rawPage)
.replace(/(Special|Служебная):(Вклад|Contributions)\//i, '')
.replace(/(User talk:|Обсуждение участни(ка|цы):)/i, '')
.replace(/(user|участни(к|ца)):/i, '');
var pageLink = '[[' + pageName + (isUserRelated ? '|' + displayName + ']]' : ']]');
var reportPage = mode === 'request' ? 'Википедия:Запросы к администраторам' : 'Википедия:Установка защиты';
return { pageName: pageName, pageLink: pageLink, displayName: displayName, reportPage: reportPage };
}
function getTopDiscussionInsertIndex(pageText) {
var introEnd = getIntroBlockEndIndex(pageText);
var firstHeading = /^==[^=\n].*==\s*$/m.exec(pageText.slice(introEnd));
return firstHeading ? introEnd + firstHeading.index : introEnd;
}
function getIntroBlockEndIndex(pageText) {
var pos = 0;
while (pos < pageText.length) {
var next = skipWhitespace(pageText, pos);
var end;
if (pageText.slice(next, next + 4) === '<!--') {
end = pageText.indexOf('-->', next + 4);
if (end === -1) return next;
pos = end + 3;
continue;
}
end = skipTemplate(pageText, next);
if (end !== next) {
pos = end;
continue;
}
return next;
}
return pos;
}
function skipWhitespace(text, pos) {
while (pos < text.length && /\s/.test(text.charAt(pos))) pos++;
return pos;
}
function skipTemplate(text, pos) {
var depth = 0;
var i = pos;
if (text.slice(pos, pos + 2) !== '{{') return pos;
while (i < text.length) {
if (text.slice(i, i + 4) === '<!--') {
var commentEnd = text.indexOf('-->', i + 4);
if (commentEnd === -1) return pos;
i = commentEnd + 3;
continue;
}
if (text.slice(i, i + 2) === '{{') {
depth++;
i += 2;
continue;
}
if (text.slice(i, i + 2) === '}}') {
depth--;
i += 2;
if (depth === 0) return i;
continue;
}
i++;
}
return pos;
}
function insertDiscussionBlockAt(pageText, insertAt, blockText, separator) {
var sep = separator || '\n\n';
var before = pageText.slice(0, insertAt).replace(/\s+$/, '');
var block = String(blockText || '').replace(/\s+$/, '');
var after = pageText.slice(insertAt).replace(/^\s+/, '');
return (before ? before + sep : '') + block + (after ? sep + after : '\n');
}
function insertTopDiscussionSection(pageText, sectionText) {
return insertDiscussionBlockAt(pageText, getTopDiscussionInsertIndex(pageText), sectionText, '\n\n');
}
function doReport(ctx, fast, protectMode) {
var header = $('#rmReportHeader').val() || ctx.pageLink;
var text = $('#rmReportText').val() || '';
var isZka = ctx.reportPage === 'Википедия:Запросы к администраторам';
var isRemoveProtect = !isZka && protectMode === 'remove';
startProcessing();
var targetPage, editParams, sectionForLink;
if (fast) {
targetPage = 'Википедия:Запросы к администраторам/Быстрые';
sectionForLink = null;
editParams = {
appendtext: '\n\n' + T_OPEN + 'sub' + 'st:t:preload/ЗКАБ/subst|\n | участник = ' + ctx.displayName +
'| страница = | пояснение = ' + text + T_CLOSE + '\n',
summary: makeSummary('новый запрос [[Special:Contributions/' + ctx.displayName + ']]')
};
} else if (isZka) {
targetPage = ctx.reportPage;
sectionForLink = extractDisplayedText(header);
var isIpFull = /^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/.test(ctx.displayName);
editParams = {
text: '== ' + header + ' ==\n\n* ' + T_OPEN + 'userlinks|' + ctx.displayName + (isIpFull ? '|ip=1' : '') + T_CLOSE + '\n' + text + ' ~~' + '~~',
summary: '/* ' + sectionForLink + ' */ ' + makeSummary('новый запрос')
};
} else {
targetPage = isRemoveProtect ? 'Википедия:Снятие защиты' : 'Википедия:Установка защиты';
var pages = collectInputValues('.rmProtectPageInput');
if (!pages.length) pages = [ctx.pageName];
var sectionTitle, pageLines;
if (pages.length === 1) {
sectionTitle = '[[' + pages[0] + ']]';
pageLines = '* ' + T_OPEN + 'pagelinks-protect|' + pages[0] + T_CLOSE;
} else {
sectionTitle = ($('#rmProtectHeader').val() || '').trim() || pages.map(function (p) { return '[[' + p + ']]'; }).join(', ');
pageLines = pages.map(function (p) { return '* ' + T_OPEN + 'pagelinks-protect|' + p + T_CLOSE; }).join('\n');
}
sectionForLink = extractDisplayedText(sectionTitle);
editParams = {
section: 'new',
sectiontitle: sectionTitle,
text: pageLines + '\n' + text + ' ~~' + '~~',
summary: '/* ' + sectionForLink + ' */ ' + makeSummary('новый запрос')
};
}
var statusId = logStatus('Публикуется запрос на «' + targetPage + '»...', null, { pending: true, trackError: false });
if (isZka && !fast) {
editPageContent(targetPage, {
summary: editParams.summary,
assertuser: mwCfg.wgUserName,
readError: 'Не удалось получить содержимое страницы «' + targetPage + '».'
}, function (pageText) {
return { text: insertTopDiscussionSection(pageText, editParams.text) };
}, function (err) {
if (err) {
logStatus('Публикация запроса на «' + targetPage + '».', err, { statusId: statusId });
markSubmitError();
$('#rmReportFast').prop('disabled', false);
return;
}
logStatus('Запрос опубликован.', null, { statusId: statusId, trackError: false });
appendNominationLink(targetPage, sectionForLink);
if (sectionForLink) subscribeToTopic(targetPage, sectionForLink);
renderModalFooter('reload');
});
return;
}
apiReq($.extend({ title: targetPage }, editParams), 'edit', function (resp) {
if (resp && resp.error) {
logStatus('Публикация запроса на «' + targetPage + '».', resp.error, { statusId: statusId });
markSubmitError();
if (isZka) $('#rmReportFast').prop('disabled', false);
return;
}
logStatus('Запрос опубликован.', null, { statusId: statusId, trackError: false });
appendNominationLink(targetPage, sectionForLink);
if (sectionForLink) subscribeToTopic(targetPage, sectionForLink);
renderModalFooter('reload');
});
}
// ═══════════════════════════════════════════════════════════════════════════
// ДИСПЕТЧЕР
// ═══════════════════════════════════════════════════════════════════════════
function handleMenuClick(item, event) {
isError = false;
var op = OPERATIONS_MAP[item.id];
// Специальный случай: КОБ категории с Ctrl — быстрое добавление шаблона
if (item.id === 'cat-merge' && event && event.ctrlKey) {
showQuickMergeModal();
return;
}
if (!op) {
console.error('RemoverCore: неизвестная операция', item.id);
return;
}
var handlerFn = handlers[op.handler];
if (typeof handlerFn !== 'function') {
console.error('RemoverCore: обработчик не найден', op.handler);
return;
}
handlerFn(op, event);
}
// ─── Экспорт ─────────────────────────────────────────────────────────────
window.RemoverCore = { handleMenuClick: handleMenuClick };
}());
2iu9gg9r3tqaxh9jotlbroi9seu4inh
749744
749735
2026-07-03T19:21:45Z
Solidest
54422
749744
javascript
text/javascript
/**
* Remover — ядро (core).
* Загружается лениво при первом клике по пункту меню.
* Ожидает, что window.RemoverState уже задан remover-loader.js.
*
* Архитектура: единый реестр OPERATIONS описывает все операции (КБУ, КУ, КПМ, КУЛ, КОБ, КРАЗД, ВУС, Защита, Запрос, Снятие, кат-варианты).
* Логика выполнения сосредоточена в универсальных обработчиках.
* Экспортирует: window.RemoverCore.handleMenuClick(item, event)
*/
(function () {
'use strict';
var state = window.RemoverState;
if (!state) { console.error('RemoverCore: window.RemoverState не задан.'); return; }
var mwCfg = state.mwCfg;
var cfg = applyCoreConfigDefaults(state.cfg || {});
var isCategory = state.isCategory;
var isVector22 = state.isVector22;
var scriptLink = cfg.scriptLink;
var settingsOptionName = state.settingsOptionName || 'userjs-remover-settings';
var settingsVersion = 1;
var settingsMenuMeta = collectSettingsMenuMeta();
var settingsArticleItemLabels = settingsMenuMeta.articleLabels;
var settingsCategoryItemLabels = settingsMenuMeta.categoryLabels;
var settingsItemLabelById = settingsMenuMeta.idToLabel;
var settingsItemLabelByNorm = settingsMenuMeta.labelByNorm;
var settingsItemLabelOrder = settingsMenuMeta.labelOrder;
var settingsDefaults = getDefaultSettings();
var MENU_TITLE_PRESET_CACTIONS = '__remover_portlet_cactions__';
var MENU_TITLE_PRESET_PAGE = '__remover_portlet_page__';
var MENU_TITLE_PRESET_TOOLS = '__remover_portlet_tools__';
var initialSettings = normalizeRemoverSettings(readSettingsOptionState(state.settings || {}));
var setAlert = ('setAlert' in state) ? !!state.setAlert : initialSettings.notifyAuthor;
var setSubscribe = ('setSubscribe' in state) ? !!state.setSubscribe : initialSettings.subscribeTopic;
var signatureSeparator = ('signatureSeparator' in state && typeof state.signatureSeparator === 'string')
? state.signatureSeparator.trim()
: initialSettings.signatureSeparator;
initialSettings.notifyAuthor = setAlert;
initialSettings.subscribeTopic = setSubscribe;
initialSettings.signatureSeparator = signatureSeparator;
state.cfg = cfg;
state.settings = clonePlainObject(initialSettings);
state.setAlert = setAlert;
state.setSubscribe = setSubscribe;
// ─── Константы ──────────────────────────────────────────────────────────
var MONTHS_NOM = ['Январь','Февраль','Март','Апрель','Май','Июнь','Июль','Август','Сентябрь','Октябрь','Ноябрь','Декабрь'];
var MONTHS_GEN = ['января','февраля','марта','апреля','мая','июня','июля','августа','сентября','октября','ноября','декабря'];
var MONTHS_NOM_LOWER = MONTHS_NOM.map(function (m) { return m.toLowerCase(); });
var T_OPEN = '{' + '{';
var T_CLOSE = '}' + '}';
var KBU_CRITERIA_PAGE = 'Википедия:Критерии быстрого удаления';
var DATE_SECTION_TALK_TEMPLATES = {
ret: 'Оставлено',
withdrawnDeletion: 'Снято с удаления',
redirectedDeletion: {
name: 'Заменено перенаправлением',
sectionParam: 'раздел обсуждения',
singleSectionParam: true
}
};
var TALK_TEMPLATE_ALIASES = {
'Оставлено': ['Оставлена', 'Kept'],
'Заменено перенаправлением': ['Удалено', 'Перенаправлено'],
'Переименовано': ['Renamed'],
'Обсуждавшаяся категория': ['Обсуждалась', 'Обсуждалось']
};
var CATEGORY_NOMINATION_META = {
discuss: { title: 'Номинация: обсуждение', actionText: 'к обсуждению', supportsMulti: true },
deletion: { title: 'Номинация: к удалению', actionText: 'к удалению', supportsMulti: true },
rename: { title: 'Номинация: к переименованию', actionText: 'к переименованию', supportsMulti: true, renameMulti: true },
merge: { title: 'Номинация: к объединению', actionText: 'к объединению' }
};
var RE_ESCAPE = /[.*+?^${}()|[\]\\]/g;
var RE_KU_ON_PAGE = /\{\{\s*(?:к\s*удалению|ку)\s*(?:\||\}\})/i;
var RE_KPM_ON_PAGE = /\{\{\s*(?:к\s*переименованию|кпм|rename)\s*(?:\||\}\})/i;
var RE_KBU_ON_PAGE = /\{\{\s*(?:subst\s*:\s*)?(?:db\s*-[^|}\s]+|уд\s*-[^|}\s]+|к\s*быстрому\s*удалению|к\s*отсроченному\s*удалению|deleteslow|ds|hang\s*-?\s*on)\s*(?:\||\}\})/i;
var RE_KUL_ON_PAGE = /\{\{\s*(?:subst\s*:\s*)?к\s*улучшению\s*(?:\||\}\})/i;
var KBU_PATTERN_STR = 'db\\s*-[^|}\\s]+|уд\\s*-[^|}\\s]+|к\\s*быстрому\\s*удалению|к\\s*отсроченному\\s*удалению|deleteslow|ds';
var RE_KBU_PATTERNS = new RegExp('(?:' + KBU_PATTERN_STR + ')', 'i');
var KUL_PATTERN_STR = 'к\\s*улучшению';
var RE_KUL_PATTERN = new RegExp(KUL_PATTERN_STR, 'i');
var HANGON_PATTERN_STR = 'hang\\s*-?\\s*on';
var RE_HANGON = new RegExp(HANGON_PATTERN_STR, 'i');
var MOVE_TO_COMMONS_TEMPLATE_TITLE = 'Шаблон:Перенести на Викисклад';
var MOVE_TO_COMMONS_TEMPLATE_ALIASES = [
'Перенести на Викисклад',
'На склад',
'To Commons',
'Copy to Wikimedia Commons',
'На викисклад',
'На Викисклад',
'To commons',
'Mtc',
'Перенести в Commons',
'MTC',
'Move to Commons',
'Перенести на викисклад'
];
var BADFAIRUSE_OPTIONS = [
['1', 'несвободная карта'],
['1+', 'карта Google Maps или Google Earth'],
['2', 'несвободное изображение утилитарного объекта'],
['3', 'несвободное изображение животного, растения или гриба'],
['6', 'несвободное изображение на странице-списке эпизодов или серий'],
['7', 'несвободная анимация или отрывок из фильма'],
['10', 'несвободное изображение здания, сооружения и памятника'],
['11', 'несвободная фотография ныне живущего человека'],
['12', 'несвободная схема или чертёж']
];
var DFUD_REASON_OPTIONS = [
['1', 'может быть заменено свободным эквивалентом'],
['2', 'разрешение/качество наносит имущественный ущерб правообладателю'],
['3', 'не отвечает критерию минимального использования или не имеет значимости'],
['4', 'не публиковалось ранее или не имеет проверяемого источника'],
['9', 'используется вне пространства статей или не используется в статьях'],
['10', 'не имеет описания и/или обоснования добросовестности использования']
];
var FILE_DATE_DEADLINE_HINT = 'Срок до возможного удаления — 7 дней с даты установки.';
var RE_DATE_ISO = /^(\d{4})-(\d{2})-(\d{2})$/;
var RE_DATE_RUSSIAN = /^(\d{1,2})\s+([\u0430-\u044f\u0410-\u042f\u0451\u0401.]+)\s+(\d{2}|\d{4})$/;
var RE_DATE_DASH = /^(\d{1,4})\s*-\s*(\d{1,2})\s*-\s*(\d{1,4})$/;
var RE_DATE_DOT = /^(\d{1,2})\s*\.\s*(\d{1,2})\s*\.\s*(\d{2}|\d{4})$/;
var RE_DATE_SLASH = /^(\d{1,4})\s*\/\s*(\d{1,2})\s*\/\s*(\d{1,4})$/;
var RE_NOINCLUDE = /(\s*)<noinclude>([\s\S]*?)<\/noinclude>/i;
var RE_TEMPLATE_NS = /^(?:шаблон|template)\s*:\s*/i;
// ─── Глобальные переменные сессии ────────────────────────────────────────
var isError = false;
var logStatusSeq = 0;
var resizeObservers = [];
var modalLayoutSyncHandlers = [];
var tplAliasCache = {};
// ─── Стили ───────────────────────────────────────────────────────────────
var stStyles = cfg.modalStyles;
var tk = {
cBase: 'var(--color-base, #202122)',
cSub: 'var(--color-subtle, #72777d)',
cSubM: 'var(--color-subtle, #54595d)',
cInv: 'var(--color-inverted-fixed, #fff)',
cProg: 'var(--color-progressive, #3366cc)',
cProgH: 'var(--color-progressive--hover, #2a4b8d)',
cDang: 'var(--color-destructive, #d73333)',
cDis: 'var(--color-disabled, var(--color-subtle, #72777d))',
bgBase: 'var(--background-color-base, #fff)',
bgNSub: 'var(--background-color-neutral-subtle, #f8f9fa)',
bgN: 'var(--background-color-neutral, #eaecf0)',
bgDis: 'var(--background-color-disabled, var(--background-color-neutral, #eaecf0))',
bgProg: 'var(--background-color-progressive, #3366cc)',
bgProgH:'var(--background-color-progressive--hover, #2a4d8f)',
bgSucc: 'var(--background-color-success, #14866d)',
bgSuccH:'var(--background-color-success--hover, #0f6d57)',
bSub: 'var(--border-color-subtle, #a2a9b1)',
bSubS: 'var(--border-color-subtle, #ddd)',
bDis: 'var(--border-color-disabled, var(--border-color-subtle, #a2a9b1))',
bProg: 'var(--border-color-progressive, #3366cc)',
bProgH: 'var(--border-color-progressive--hover, #2a4d8f)',
bSucc: 'var(--border-color-success, #14866d)',
bSuccH: 'var(--border-color-success--hover, #0f6d57)'
};
var sz = {
taH: '180px',
taMinH: '100px',
taMinW: '180px',
mobileBp: 720,
modalRatio: 0.4,
modalMinWide: 420,
modalDefaultWide: 720,
viewportGap: 24,
touchDesktopGap: 120
};
var btnBase = 'border-radius:4px;padding:8px 16px;cursor:pointer;font-size:14px;font-family:inherit;transition:background .1s;';
var neutralVis = 'background:' + tk.bgNSub + ';border:1px solid ' + tk.bSub + ';color:' + tk.cBase + ';border-radius:4px;';
var stCancel = neutralVis + btnBase;
var stSubmit = 'background:' + tk.bgProg + ';color:' + tk.cInv + ';border:1px solid ' + tk.bProg + ';' + btnBase;
var stReload = 'background:' + tk.bgSucc + ';color:' + tk.cInv + ';border:1px solid ' + tk.bSucc + ';' + btnBase;
var stInputBox = 'flex:1;padding:6px;box-sizing:border-box;min-width:0;border:1px solid ' + tk.bSub + ';background:' + tk.bgBase + ';color:inherit;border-radius:2px;';
var stInputFull= 'width:100%;padding:6px;box-sizing:border-box;border:1px solid ' + tk.bSub + ';border-radius:2px;background:' + tk.bgBase + ';color:inherit;margin-bottom:10px;';
var stRow = 'display:flex;margin-bottom:6px;';
var inlineControlGap = 4;
var squareControlSize = 32;
var leftNestedControlOffset = (squareControlSize + inlineControlGap) + 'px';
var stToolBtn = neutralVis + 'padding:4px 8px;margin-left:' + inlineControlGap + 'px;cursor:pointer;font-size:12px;';
var stRemoveBtn= neutralVis + 'display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;padding:0;width:' + squareControlSize + 'px;height:' + squareControlSize + 'px;margin-left:' + inlineControlGap + 'px;cursor:pointer;font-size:12px;line-height:1;';
var stFooterWrap = 'display:flex;align-items:center;gap:8px;flex-wrap:wrap;';
var stFooterChecks = 'display:flex;flex-direction:column;gap:4px;margin-right:auto;flex:1 1 220px;min-width:0;';
var stFooterCheckLabel = 'display:inline-flex;align-items:flex-start;gap:6px;font-size:14px;line-height:1.4;max-width:100%;';
var stFooterActions = 'display:flex;gap:6px;flex:1 1 auto;min-width:0;max-width:100%;flex-wrap:wrap;justify-content:flex-end;margin-left:auto;';
var stHeaderIconBtn = 'margin:0 0 0 auto;width:32px;min-width:32px;height:32px;padding:0;display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;border:1px solid ' + tk.bSub + ';border-radius:4px;background:' + tk.bgNSub + ';color:' + tk.cSubM + ';cursor:pointer;font-size:16px;line-height:1;';
var multiNominationGap = '6px';
var RESIZE_CLASS = 'rm-resizable';
// ═══════════════════════════════════════════════════════════════════════════
// РЕЕСТР ОПЕРАЦИЙ
// Каждая запись описывает одну кнопку меню. Поля:
// id — идентификатор (совпадает с item.id из loader)
// handler — имя метода-обработчика в объекте handlers
// handlerArg — аргумент, передаваемый в handler (опционально)
// ═══════════════════════════════════════════════════════════════════════════
var OPERATIONS = [
// ── Статьи ──────────────────────────────────────────────────────────
{
id: 'fRm',
label: 'КБУ',
handler: 'showKbu',
// Параметры номинации: заполняются при submit
nomination: {
pageTitle: function (pg) { return normTitle(pg); },
// шаблон встраивается в статью, номинационная страница отсутствует
inArticle: true
}
},
{
id: 'tRm',
label: 'КУ',
handler: 'showNomination',
nomination: {
comment: 'к удалению',
template: 'к удалению',
navTemplate: 'КУ',
nomPage: function (date) { return 'Википедия:К удалению/' + date; },
supportsMulti: true,
multiOpId: 'mRm',
supportsTransfer: true,
// шаблон встраивается в статью через <noinclude>
inArticle: true,
articleTpl: function (tplpar, date) { return 'к удалению|' + date + (tplpar ? '|' + tplpar : ''); }
}
},
{
id: 'rnm',
label: 'КПМ',
handler: 'showNomination',
nomination: {
comment: 'к переименованию',
template: 'к переименованию',
navTemplate: 'КПМ',
nomPage: function (date) { return 'Википедия:К переименованию/' + date; },
supportsMulti: true,
multiOpId: 'mRnm',
inArticle: true,
articleTpl: function (tplpar, date) { return 'к переименованию|' + date + (tplpar ? '|' + tplpar : ''); },
extraInput: {
type: 'rename',
firstId: 'rmRenameFirst', inputClass: 'rmRenameInput',
firstPh: 'Новое название',
addBtnId: 'rmAddRename', addBtnLabel: 'Добавить вариант',
containerId: 'rmRenameContainer', addPh: 'Дополнительный вариант',
maxRows: 2, maxMsg: 'Максимум 3 варианта переименования.'
}
}
},
{
id: 'imp',
label: 'КУЛ',
handler: 'showNomination',
nomination: {
comment: 'к срочному улучшению',
template: 'к улучшению',
navTemplate: 'КУЛ',
nomPage: function (date) { return 'Википедия:К улучшению/' + date; },
inArticle: true,
articleTpl: function (tplpar, date) { return 'к улучшению|' + date + (tplpar ? '|' + tplpar : ''); }
}
},
{
id: 'merge',
label: 'КОБ',
handler: 'showNomination',
nomination: {
comment: 'к объединению с другой',
summaryAction: 'к объединению',
template: 'к объединению',
navTemplate: 'КОБ',
nomPage: function (date) { return 'Википедия:К объединению/' + date; },
inArticle: true,
articleTpl: function (tplpar, date) { return 'к объединению|' + date + (tplpar ? '|' + tplpar : ''); },
extraInput: {
type: 'merge',
firstId: 'rmMergeFirst', inputClass: 'rmMergeInput',
firstPh: 'Объединить с…',
addBtnId: 'rmAddMerge', addBtnLabel: '+',
containerId: 'rmMergeContainer', addPh: 'Дополнительная статья',
maxRows: 10, maxMsg: 'Максимум 11 статей для объединения.'
}
}
},
{
id: 'split',
label: 'КРАЗД',
handler: 'showNomination',
nomination: {
comment: 'к разделению',
template: 'к разделению',
navTemplate: 'КР',
nomPage: function (date) { return 'Википедия:К разделению/' + date; },
inArticle: true,
articleTpl: function (tplpar, date) { return 'к разделению|' + date + (tplpar ? '|' + tplpar : ''); },
extraInput: {
type: 'split',
firstId: 'rmSplitFirst', inputClass: 'rmSplitInput',
firstPh: 'Разделить на…',
addBtnId: 'rmAddSplit', addBtnLabel: '+',
containerId: 'rmSplitContainer', addPh: 'Дополнительная статья'
}
}
},
{
id: 'recov',
label: 'ВУС',
handler: 'showNomination',
nomination: {
comment: '',
template: 'к восстановлению',
navTemplate: 'ВУС',
nomPage: function (date) { return 'Википедия:К восстановлению/' + date; },
inArticle: false // шаблон не ставится в (удалённую) статью
}
},
{
id: 'close',
label: 'Снятие',
handler: 'showArticleClose'
},
// ── Запросы ─────────────────────────────────────────────────────────
{
id: 'protect',
label: 'Защита',
handler: 'showReport',
reportMode: 'protect'
},
{
id: 'request',
label: 'Запрос',
handler: 'showReport',
reportMode: 'request'
},
// ── Категории ────────────────────────────────────────────────────────
{
id: 'cat-fRm',
label: 'КБУ',
handler: 'showKbu',
forCategory: true
},
{
id: 'cat-discuss',
label: 'Обсудить',
handler: 'showCatNomination',
catType: 'discuss'
},
{
id: 'cat-delete',
label: 'Удалить',
handler: 'showCatNomination',
catType: 'deletion'
},
{
id: 'cat-rename',
label: 'Переименовать',
handler: 'showCatNomination',
catType: 'rename'
},
{
id: 'cat-merge',
label: 'Объединить',
handler: 'showCatNomination',
catType: 'merge'
},
{
id: 'cat-done',
label: 'Снятие',
handler: 'showCatClose'
}
];
// Быстрый поиск по id
var OPERATIONS_MAP = {};
OPERATIONS.forEach(function (op) { OPERATIONS_MAP[op.id] = op; });
// ─── Тексты переноса (КБУ → КУ) ─────────────────────────────────────────
var transferTexts = {
kbu: { notice: 'Перенесено с быстрого удаления.', hint: 'Шаблоны КБУ и Hangon будут сняты.' },
kul: { notice: 'Перенесено с КУЛ.', hint: 'Шаблоны КУЛ будут сняты.' },
both: { notice: 'Перенесено с быстрого удаления и КУЛ.', hint: 'Шаблоны КБУ, КУЛ и Hangon будут сняты.' }
};
// ═══════════════════════════════════════════════════════════════════════════
// УТИЛИТЫ
// ═══════════════════════════════════════════════════════════════════════════
function escapeRegExp(s) { return s.replace(RE_ESCAPE, '\\$&'); }
function escapeHtml(s) {
return String(s || '')
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
.replace(/"/g, '"').replace(/'/g, ''');
}
function joinHtml(parts) { return parts.join(''); }
function ucfirst(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : ''; }
function padTwo(n) { return n < 10 ? '0' + n : String(n); }
function expandTwoDigitYear(value) {
return 2000 + parseInt(value, 10);
}
function monthToNumber(name) {
var lower = name.toLowerCase().replace(/\.$/, '');
var idx = MONTHS_GEN.indexOf(lower);
if (idx === -1) idx = MONTHS_NOM_LOWER.indexOf(lower);
if (idx === -1 && lower.length >= 3) {
for (var i = 0; i < MONTHS_GEN.length; i++) {
if (MONTHS_GEN[i].indexOf(lower) === 0 || MONTHS_NOM_LOWER[i].indexOf(lower) === 0) return i + 1;
}
}
return idx + 1;
}
function makeStandardDate(yearValue, monthValue, dayValue) {
var yearText = String(yearValue || '').trim();
var year = yearText.length === 2 ? expandTwoDigitYear(yearText) : parseInt(yearText, 10);
var month = parseInt(monthValue, 10);
var day = parseInt(dayValue, 10);
var maxDay;
if ((yearText.length !== 2 && yearText.length !== 4) || isNaN(year) || isNaN(month) || isNaN(day) || year < 1 || month < 1 || month > 12 || day < 1) return null;
maxDay = new Date(Date.UTC(year, month, 0)).getUTCDate();
if (day > maxDay) return null;
return year + '-' + padTwo(month) + '-' + padTwo(day);
}
function normalizeIsoDate(value) {
var m = String(value || '').trim().match(RE_DATE_ISO);
return m ? makeStandardDate(m[1], m[2], m[3]) : null;
}
function normalizeTemplateName(name) {
return (name || '').toLowerCase().replace(RE_TEMPLATE_NS, '').replace(/_/g, ' ').replace(/\s+/g, ' ').trim();
}
function toTemplateNameList(names) {
if (Array.isArray(names)) return names;
return String(names || '').split('|');
}
function getTalkTemplateAliases(templateName) {
return TALK_TEMPLATE_ALIASES[templateName] || [];
}
function buildTemplateNamePattern(names) {
var seen = {};
var patterns = [];
toTemplateNameList(names).forEach(function (name) {
var normalized = normalizeTemplateName(String(name || '').replace(/^(?:safe)?subst\s*:\s*/i, ''));
if (!normalized || seen[normalized]) return;
seen[normalized] = true;
patterns.push(escapeRegExp(normalized).replace(/\s+/g, '[ _]*'));
});
return patterns.join('|');
}
function buildTemplateTransclusionRegex(templateName, aliases, flags) {
var names = [templateName].concat(getTalkTemplateAliases(templateName), toTemplateNameList(aliases));
var namePattern = buildTemplateNamePattern(names);
return namePattern
? new RegExp('\\{\\{\\s*(?:(?:safe)?(?:subst|подст)\\s*:\\s*)?(?:(?:шаблон|template)\\s*:\\s*)?(' + namePattern + ')\\s*([^}]*)\\}\\}', flags || 'i')
: null;
}
function getDate(dateString) {
var d = dateString ? new Date(dateString) : new Date();
var iso = d.getUTCFullYear() + '-' + padTwo(d.getUTCMonth() + 1) + '-' + padTwo(d.getUTCDate());
var rus = d.getUTCDate() + ' ' + MONTHS_GEN[d.getUTCMonth()] + ' ' + d.getUTCFullYear();
return [iso, rus];
}
function convertToStandardDate(dateStr) {
var value = String(dateStr || '').replace(/\s+/g, ' ').trim();
var m;
var mo;
var normalized;
m = value.match(RE_DATE_ISO);
if (m) return normalizeIsoDate(value) || '';
m = value.match(RE_DATE_RUSSIAN);
if (m) {
mo = monthToNumber(m[2]);
normalized = mo ? makeStandardDate(m[3], mo, m[1]) : null;
return normalized || '';
}
m = value.match(RE_DATE_DASH);
if (m) {
normalized = makeStandardDate(m[1], m[2], m[3]);
return normalized || '';
}
m = value.match(RE_DATE_DOT);
if (m) {
normalized = makeStandardDate(m[3], m[2], m[1]);
return normalized || '';
}
m = value.match(RE_DATE_SLASH);
if (m) {
normalized = makeStandardDate(m[1], m[2], m[3]);
return normalized || '';
}
return value;
}
function getNamespaceLabel(ns, fallback) {
return (mwCfg.wgFormattedNamespaces && mwCfg.wgFormattedNamespaces[ns]) || fallback;
}
function getTalkPage(pageName) {
var match = /([^:]*:)?(.*)/.exec(pageName);
if (match[1]) {
var ns = mwCfg.wgNamespaceIds && mwCfg.wgNamespaceIds[match[1].slice(0, -1).toLowerCase().replace(/ /g, '_')];
if (ns !== undefined) return getNamespaceLabel(ns | 1, 'Обсуждение') + ':' + match[2];
}
return getNamespaceLabel(1, 'Обсуждение') + ':' + pageName;
}
function normTitle(s) { return (s || '').replace(/_/g, ' '); }
function stripCatPrefix(s) { return (s || '').replace(/^(?:Категория|Category):\s*/i, ''); }
function normalizeRedirectTarget(value) {
var title = normTitle(String(value || '').trim());
var linkMatch = title.match(/^\[\[:?([^|\]]+)(?:\|[^\]]*)?\]\]$/);
return linkMatch ? normTitle(linkMatch[1]).trim() : title;
}
function buildRedirectText(targetTitle) {
return '#REDIRECT [[' + targetTitle + ']]';
}
function getCategoryNamespaceLabel() {
return getNamespaceLabel(14, 'Категория');
}
function normalizeCategoryPageName(value) {
var title = normTitle(value).trim();
var nsMatch, nsKey, ns;
if (!title) return '';
nsMatch = title.match(/^([^:]+):(.+)$/);
if (nsMatch) {
nsKey = nsMatch[1].toLowerCase().replace(/ /g, '_');
ns = mwCfg.wgNamespaceIds && mwCfg.wgNamespaceIds[nsKey];
if (ns === 14 || nsKey === 'category' || nsKey === 'категория') return getCategoryNamespaceLabel() + ':' + nsMatch[2].trim();
return getCategoryNamespaceLabel() + ':' + title;
}
return getCategoryNamespaceLabel() + ':' + title;
}
function makeSummary(text) { return text + ' (' + scriptLink + ')'; }
function makeNominationPageSummary(sectionTitle) {
var section = String(sectionTitle || '').replace(/\s+/g, ' ').trim();
return (section ? '/* ' + section + ' */ ' : '') + makeSummary('добавление номинации');
}
function makeNominationTargetSummary(pageTitle, sectionTitle, actionText) {
var section = String(sectionTitle || '').trim();
var target = pageTitle + (section ? '#' + section : '');
var label = 'номинация' + (actionText ? ' ' + actionText : '');
return makeSummary('[[' + target + '|' + label + ']]');
}
function getNominationSummaryAction(nomination) {
return String(nomination && (nomination.summaryAction || nomination.comment || nomination.template) || '')
.trim();
}
function appendNominationSignature(text) {
var body = String(text || '');
return body + (signatureSeparator ? ' ' + signatureSeparator : '') + ' ~~' + '~~';
}
function extractDisplayedText(s) {
return (s || '').replace(/\[\[:?(?:[^|\]]+\|)?(.+?)\]\]/g, '$1');
}
function collectInputValues(selector) {
return $(selector).map(function () { return $(this).val().trim(); }).get().filter(Boolean);
}
function applyCoreConfigDefaults(config) {
var defaults = {
scriptLink: '[[Википедия:Гаджеты/Remover|Remover]]',
fastRemoveReasons: {
general: [
['уд-бессвязно', 'О1 Бессвязный текст'],
['уд-тест', 'О2 Тестовая страница'],
['уд-ванд', 'О3.1 Вандальная страница'],
['уд-мист', 'О3.2 Мистификация'],
['уд-повторно', 'О4 Уже удалялось'],
['уд-автор', 'О5 По просьбе автора'],
['уд-под', 'О6 Ненужная подстраница'],
['уд-переим', 'О7 Для переименования'],
['уд-дубль', 'О8 Дубликат'],
['уд-реклама', 'О9 Реклама или спам'],
['уд-нецелевая', 'О10 Нецелевая СО'],
['уд-копивио', 'О11 Нарушение АП']
],
articles: [
['подст:ds', 'ds Отсроченное пусто или коротко', 'С'],
['уд-пусто', 'С1 Пусто или коротко'],
['уд-иностр', 'С2 Не на русском'],
['уд-ссылки', 'С3 Лишь ссылки'],
['уд-нз', 'С5 Явно незначимо'],
['уд-бям', 'С7 Создано нейросетью']
],
redirects: [
['уд-в никуда', 'П1 Перенапр. в никуда'],
['уд-мпр', 'П2 Межпростр. перенапр.'],
['уд-опечатка', 'П3 Перенапр. с ошибкой в названии'],
['уд-падеж', 'П4 Не именительный падеж'],
['уд-смысл', 'П5 Неверное перенапр.'],
['уд-перобс', 'П6 Перенапр. на СО']
],
files: [
['Db-duplicate', 'Ф1 Копия файла'],
['Db-badimage', 'Ф2 Повреждённый файл'],
['подст:No license', 'Ф3.1 Нет данных о лицензии'],
['подст:No source', 'Ф3.2 Нет данных об источнике'],
['подст:No author', 'Ф3.3 Нет данных об авторе'],
['подст:Disputed', 'Ф3.4 Сомнительные данные файла'],
['подст:No permission', 'Ф3.5 Не подтверждена лицензия'],
['подст:Orphaned-fairuse', 'Ф4 Неиспользуемый КДИ'],
['подст:Disputed-fairuse', 'Ф5 Нет КДИ'],
['Db-badfairuse', 'Ф6 Неоправданное КДИ'],
['Замена с Викисклада', 'Ф7 Заменяемый КДИ'],
['Перенесено на Викисклад', 'Ф8 Есть на Складе'],
['подст:Db-nothost', 'Ф9 Файл — ВП:НЕХОСТИНГ']
],
categories: [
['уд-пусткат', 'К1.1 Пустая категория'],
['уд-служебная', 'К1.2 Разобранная служебная кат.'],
['уд-перекат', 'К2 Переименованная кат.']
],
users: [
['уд-владелец', 'У1 По желанию владельца'],
['уд-анон', 'У2 Устаревшая СО анонима'],
['уд-несущ', 'У3 Несуществующий участник'],
['уд-нецелевое', 'У4 Нецелевое использ. ЛП'],
['уд-неактив', 'У5 Подстраница неактивного']
],
special: [
['db', 'Особый случай']
]
},
fastRemoveCriteriaAnchors: {
'подст:ds': 'С1',
deleteslow: 'С1',
ds: 'С1'
},
requiredParamTemplates: {
'уд-переим': 'страницу, которую нужно переименовать',
'уд-дубль': 'страницу-дубликат',
'уд-копивио': 'URL источника нарушения АП',
'Db-duplicate': 'имя файла-оригинала',
'Замена с Викисклада': 'имя файла на Викискладе',
'Перенесено на Викисклад': 'имя файла на Викискладе',
'уд-перекат': 'новое название категории',
'db': '!причину удаления'
},
categoryTemplates: {
discuss: 'Обсуждаемая категория|обсуждаемая категория|Acat|acat|ОКТО|окто|Категория к обсуждению|категория к обсуждению',
rename: 'Категория к переименованию|категория к переименованию|Anacat|anacat',
merge: 'Категория к объединению|категория к объединению|Amergecat|amergecat|Cfm|cfm',
discussed: 'Обсуждавшаяся категория|обсуждавшаяся категория|Обсуждалась|обсуждалась|Обсуждалось|обсуждалось'
},
modalStyles: {
border: '1px solid var(--border-color-progressive, #3366bb)',
background: 'var(--background-color-base, #f8f9fa)',
borderRadius: '6px',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
headerColor: 'var(--color-progressive, #3366bb)'
}
};
config.scriptLink = config.scriptLink || defaults.scriptLink;
config.fastRemoveReasons = $.extend({}, defaults.fastRemoveReasons, config.fastRemoveReasons || {});
config.fastRemoveCriteriaAnchors = $.extend({}, defaults.fastRemoveCriteriaAnchors, config.fastRemoveCriteriaAnchors || {});
config.requiredParamTemplates = $.extend({}, defaults.requiredParamTemplates, config.requiredParamTemplates || {});
config.categoryTemplates = $.extend({}, defaults.categoryTemplates, config.categoryTemplates || {});
config.modalStyles = $.extend({}, defaults.modalStyles, config.modalStyles || {});
return config;
}
function clonePlainObject(obj) {
return JSON.parse(JSON.stringify(obj || {}));
}
function normalizeMenuLabel(value) {
return String(value || '').replace(/\s+/g, ' ').trim().toLowerCase();
}
function readSettingsOptionState(fallback) {
var base = clonePlainObject(fallback || {});
var stored;
var raw = null;
if (!mw.user || !mw.user.options || typeof mw.user.options.get !== 'function') return base;
stored = mw.user.options.get(settingsOptionName);
if (typeof stored === 'string' && stored.trim()) {
try {
raw = JSON.parse(stored);
} catch (e) {
console.warn('RemoverCore: не удалось прочитать сохранённые настройки полностью, будут использованы данные loader.', e);
}
}
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return base;
return $.extend({}, raw, base, ('quickPhrases' in raw) ? { quickPhrases: raw.quickPhrases } : {});
}
function normalizeQuickPhraseValue(value) {
return String(value == null ? '' : value).replace(/\r\n?/g, '\n').trim();
}
function normalizeQuickPhrasesList(list, fallback) {
var source = Array.isArray(list) ? list : fallback;
var normalized = [];
(source || []).forEach(function (value) {
var phrase = normalizeQuickPhraseValue(value);
if (phrase && normalized.indexOf(phrase) === -1) normalized.push(phrase);
});
return normalized;
}
function collectSettingsMenuMeta() {
var labels = [];
var articleLabels = [];
var categoryLabels = [];
var idToLabel = {};
var labelByNorm = {};
var labelOrder = {};
function collect(items, targetLabels) {
items.forEach(function (item) {
var id;
var label;
var normLabel;
if (!item || item.type === 'separator') return;
id = String(item.id || '').trim();
label = String(item.label || '').trim();
normLabel = normalizeMenuLabel(label);
if (id && label && !(id in idToLabel)) idToLabel[id] = label;
if (label && targetLabels.indexOf(label) === -1) targetLabels.push(label);
if (normLabel && !(normLabel in labelByNorm)) {
labelByNorm[normLabel] = label;
labelOrder[label] = labels.length;
labels.push(label);
}
});
}
collect(cfg.articleMenuItems, articleLabels);
collect(cfg.categoryMenuItems, categoryLabels);
return {
labels: labels,
articleLabels: articleLabels,
categoryLabels: categoryLabels,
idToLabel: idToLabel,
labelByNorm: labelByNorm,
labelOrder: labelOrder
};
}
function buildSettingsMenuItemsHint() {
var parts = [];
if (settingsArticleItemLabels.length) {
parts.push('<div class="rmSettingsHintRow"><span class="rmSettingsHintBadge">Статьи</span>' + escapeHtml(settingsArticleItemLabels.join(', ')) + '.</div>');
}
if (settingsCategoryItemLabels.length) {
parts.push('<div class="rmSettingsHintRow"><span class="rmSettingsHintBadge">Категории</span>' + escapeHtml(settingsCategoryItemLabels.join(', ')) + '.</div>');
}
return parts.length ? '<div class="rmSettingsHintList">' + parts.join('') + '</div>' : '';
}
function getDefaultSettings() {
return {
version: settingsVersion,
excludedNamespaces: normalizeNumberList(cfg.excludedNamespaces, []),
notifyAuthor: !!cfg.defaultNotifyAuthor,
subscribeTopic: !!cfg.defaultSubscribeTopic,
autoReloadAfterAction: false,
openNominationDiscussionInNewTab: false,
menuTitle: (typeof cfg.menuTitle === 'string' && cfg.menuTitle.trim()) ? cfg.menuTitle.trim() : 'Remover',
disabledItems: [],
quickPhrases: normalizeQuickPhrasesList(cfg.quickPhrases, []),
showMenuIcons: !!cfg.showMenuIcons,
signatureSeparator: (typeof cfg.signatureSeparator === 'string') ? cfg.signatureSeparator.trim() : ''
};
}
function normalizeNumberList(list, fallback) {
var source = Array.isArray(list) ? list : fallback;
return source
.map(function (value) { return parseInt(value, 10); })
.filter(function (value, index, arr) { return !isNaN(value) && arr.indexOf(value) === index; })
.sort(function (a, b) { return a - b; });
}
function normalizeDisabledItemValue(value) {
var token = String(value || '').trim();
if (!token) return null;
if (settingsItemLabelById[token]) return settingsItemLabelById[token];
return settingsItemLabelByNorm[normalizeMenuLabel(token)] || null;
}
function compareSettingsMenuLabels(a, b) {
var ai = Object.prototype.hasOwnProperty.call(settingsItemLabelOrder, a) ? settingsItemLabelOrder[a] : Number.MAX_SAFE_INTEGER;
var bi = Object.prototype.hasOwnProperty.call(settingsItemLabelOrder, b) ? settingsItemLabelOrder[b] : Number.MAX_SAFE_INTEGER;
if (ai !== bi) return ai - bi;
return a.localeCompare(b, 'ru');
}
function normalizeDisabledItemsList(list, fallback) {
var source = Array.isArray(list) ? list : fallback;
return source
.map(normalizeDisabledItemValue)
.filter(function (value, index, arr) { return value && arr.indexOf(value) === index; })
.sort(compareSettingsMenuLabels);
}
function normalizeMenuTitleSetting(value, fallback) {
var menuTitle = String(value || '').trim();
if (!menuTitle) return fallback;
if (menuTitle === MENU_TITLE_PRESET_CACTIONS || /^(#)?p-cactions$/i.test(menuTitle)) return MENU_TITLE_PRESET_CACTIONS;
if (menuTitle === MENU_TITLE_PRESET_PAGE || /^(#)?p-page$/i.test(menuTitle) || /^(#)?p-actions$/i.test(menuTitle)) return MENU_TITLE_PRESET_PAGE;
if (menuTitle === MENU_TITLE_PRESET_TOOLS || /^(#)?p-tb$/i.test(menuTitle)) return MENU_TITLE_PRESET_TOOLS;
return menuTitle;
}
function normalizeRemoverSettings(raw) {
var defaults = clonePlainObject(settingsDefaults);
var source = (raw && typeof raw === 'object') ? raw : {};
return {
version: settingsVersion,
excludedNamespaces: normalizeNumberList(source.excludedNamespaces, defaults.excludedNamespaces || []),
notifyAuthor: ('notifyAuthor' in source) ? !!source.notifyAuthor : !!defaults.notifyAuthor,
subscribeTopic: ('subscribeTopic' in source) ? !!source.subscribeTopic : !!defaults.subscribeTopic,
autoReloadAfterAction: ('autoReloadAfterAction' in source) ? !!source.autoReloadAfterAction : !!defaults.autoReloadAfterAction,
openNominationDiscussionInNewTab: ('openNominationDiscussionInNewTab' in source) ? !!source.openNominationDiscussionInNewTab : !!defaults.openNominationDiscussionInNewTab,
menuTitle: normalizeMenuTitleSetting(
(typeof source.menuTitle === 'string' && source.menuTitle.trim()) ? source.menuTitle : '',
typeof defaults.menuTitle === 'string' && defaults.menuTitle.trim() ? defaults.menuTitle.trim() : 'Remover'
),
disabledItems: normalizeDisabledItemsList(source.disabledItems, defaults.disabledItems || []),
quickPhrases: normalizeQuickPhrasesList(source.quickPhrases, defaults.quickPhrases || []),
showMenuIcons: ('showMenuIcons' in source) ? !!source.showMenuIcons : !!defaults.showMenuIcons,
signatureSeparator: (typeof source.signatureSeparator === 'string')
? source.signatureSeparator.trim()
: (typeof defaults.signatureSeparator === 'string' ? defaults.signatureSeparator.trim() : '')
};
}
function areRemoverSettingsEqual(a, b) {
return JSON.stringify(normalizeRemoverSettings(a)) === JSON.stringify(normalizeRemoverSettings(b));
}
function updateStoredSettingsState(settings, skipUserOptionsSync) {
var normalized = normalizeRemoverSettings(settings);
state.settings = clonePlainObject(normalized);
setAlert = normalized.notifyAuthor;
setSubscribe = normalized.subscribeTopic;
signatureSeparator = normalized.signatureSeparator;
state.setAlert = setAlert;
state.setSubscribe = setSubscribe;
if (!skipUserOptionsSync && mw.user && mw.user.options && typeof mw.user.options.set === 'function') {
mw.user.options.set(settingsOptionName, JSON.stringify(normalized));
}
return normalized;
}
function splitSettingsInput(value) {
return String(value || '')
.split(/[\s,;]+/)
.map(function (part) { return part.trim(); })
.filter(Boolean);
}
function splitSettingsListInput(value) {
return String(value || '')
.split(/[\n,;]+/)
.map(function (part) { return part.trim(); })
.filter(Boolean);
}
function parseNamespaceInput(value) {
var tokens = splitSettingsInput(value);
var invalid = [];
var values = [];
tokens.forEach(function (token) {
var parsed = parseInt(token, 10);
if (String(parsed) !== token) invalid.push(token);
else if (values.indexOf(parsed) === -1) values.push(parsed);
});
values.sort(function (a, b) { return a - b; });
return { values: values, invalid: invalid };
}
function parseDisabledItemsInput(value) {
var tokens = splitSettingsListInput(value);
var invalid = [];
var values = [];
tokens.forEach(function (token) {
var normalized = normalizeDisabledItemValue(token);
if (!normalized) invalid.push(token);
else if (values.indexOf(normalized) === -1) values.push(normalized);
});
values.sort(compareSettingsMenuLabels);
return { values: values, invalid: invalid };
}
function formatItemsWithAnd(items) {
var list = (items || []).filter(Boolean);
if (!list.length) return '';
if (list.length === 1) return list[0];
return list.slice(0, -1).join(', ') + ' и ' + list[list.length - 1];
}
function formatPagesWithAnd(names, prefix) {
var p = prefix || ':';
var links = (names || []).map(function (n) { return '[[' + p + n + ']]'; });
return formatItemsWithAnd(links);
}
function asNonEmptyArray(value) {
return (Array.isArray(value) ? value : (value ? [value] : [])).filter(Boolean);
}
function buildRenameTemplateParam(targetNames) {
var list = asNonEmptyArray(targetNames);
if (!list.length) return '';
return list[0] + (list.length > 1 ? '||' + list.slice(1).join('|') : '');
}
function collectRenameTargetsFromTemplateParams(params) {
return (params || []).map(function (value) {
return String(value || '').trim();
}).filter(Boolean);
}
function formatRenameItemLabel(pageName, targetName) {
var targets = asNonEmptyArray(targetName);
return '[[:' + pageName + ']]' + (targets.length
? ' → ' + targets.map(function (name) { return '[[:' + name + ']]'; }).join(', ')
: '');
}
function buildRenameItemLabelFormatter(targetsByPage) {
var targets = targetsByPage || {};
return function (pageName) {
return formatRenameItemLabel(pageName, targets[normTitle(pageName)] || '');
};
}
function formatRenameItemsWithAnd(pages, targetsByPage) {
var formatItem = buildRenameItemLabelFormatter(targetsByPage);
var links = (pages || []).map(function (pageName) { return formatItem(pageName); });
return formatItemsWithAnd(links);
}
function normalizeCategoryTargetName(value) {
return normTitle(stripCatPrefix(value)).trim();
}
function normalizeCategoryTargetPageName(value) {
var title = normalizeCategoryTargetName(value);
return title ? normalizeCategoryPageName(title) : '';
}
function buildMultiRenameTargetMap(pairs, key) {
var map = {};
(pairs || []).forEach(function (pair) {
var value;
if (!pair || !pair.pageName) return;
value = pair[key || 'targetName'];
map[normTitle(pair.pageName)] = Array.isArray(value) ? value.slice() : (value || '');
});
return map;
}
function getMultiRenameTarget(job, pageName, key) {
var map = job && job[key || 'multiRenameTargets'];
return map ? (map[normTitle(pageName)] || '') : '';
}
function collectMultiRenamePairs(options) {
var opts = options || {};
var normalizePage = typeof opts.normalizePageName === 'function' ? opts.normalizePageName : normTitle;
var normalizeTarget = typeof opts.normalizeTargetName === 'function' ? opts.normalizeTargetName : normTitle;
var normalizeTemplateTarget = typeof opts.normalizeTemplateTargetName === 'function' ? opts.normalizeTemplateTargetName : normalizeTarget;
var pairs = [];
$('.rmMultiPageBlock').each(function () {
var $block = $(this);
var pageRaw = ($block.find('.rmMultiPageInput').val() || '').trim();
var targetPairs = collectMultiRenameTargetValues($block).map(function (targetRaw) {
return {
targetName: normalizeTarget(targetRaw),
templateTargetName: normalizeTemplateTarget(targetRaw)
};
}).filter(function (item) { return item.targetName || item.templateTargetName; });
var pageName = normalizePage(pageRaw);
var targetNames = targetPairs.map(function (item) { return item.targetName; }).filter(Boolean);
var templateTargetNames = targetPairs.map(function (item) { return item.templateTargetName; }).filter(Boolean);
if (!pageName && !targetNames.length && !templateTargetNames.length) return;
pairs.push({
pageName: pageName,
targetName: targetNames[0] || '',
templateTargetName: templateTargetNames[0] || '',
targetNames: targetNames,
templateTargetNames: templateTargetNames
});
});
return pairs;
}
function validateMultiRenamePairs(pairs, pageLabel, targetLabel) {
var seen = {};
var pages = pairs || [];
var pageWord = pageLabel || 'страницу';
var targetWord = targetLabel || 'новое название';
if (!pages.length) { alert('Укажите ' + pageWord + '.'); return false; }
for (var i = 0; i < pages.length; i++) {
var targetNames = asNonEmptyArray(pages[i].targetNames || pages[i].targetName);
var templateTargetNames = asNonEmptyArray(pages[i].templateTargetNames || pages[i].templateTargetName);
if (!pages[i].pageName) { alert('Укажите ' + pageWord + '.'); return false; }
if (!targetNames.length || !templateTargetNames.length) { alert('Укажите ' + targetWord + ' для «' + pages[i].pageName + '».'); return false; }
if (targetNames.length > 3 || templateTargetNames.length > 3) { alert('Максимум 3 варианта переименования для «' + pages[i].pageName + '».'); return false; }
if (seen[normTitle(pages[i].pageName)]) { alert('Страница «' + pages[i].pageName + '» указана несколько раз.'); return false; }
seen[normTitle(pages[i].pageName)] = true;
}
return true;
}
function getMultiRenameDiscussionOptions(targetsByPage, extraOptions) {
return $.extend({}, extraOptions || {}, {
formatItemLabel: buildRenameItemLabelFormatter(targetsByPage)
});
}
function formatCatLink(name) { return '[[:' + normalizeCategoryPageName(name) + ']]'; }
function formatMergeStatus(status) {
return { already_exists: 'уже был', updated: 'дополнен', created: 'создан' }[status] || status;
}
function applyGeneratedText($el, generated) {
var cur = $el.val();
var prev = $el.data('rmGenerated') || '';
if (!prev || cur.indexOf(prev) === 0) {
$el.val(generated + cur.slice(prev.length));
} else {
$el.val(generated + (cur ? '\n' + cur : ''));
}
$el.data('rmGenerated', generated);
}
function getCurrentQuickPhrases() {
return normalizeQuickPhrasesList(
state.settings && state.settings.quickPhrases,
settingsDefaults.quickPhrases || []
);
}
function insertTextIntoTextarea($el, text) {
var el = $el && $el[0];
var value;
var start;
var end;
var updatedValue;
var caretPos;
if (!el) return;
text = String(text || '');
if (!text) return;
value = $el.val() || '';
start = typeof el.selectionStart === 'number' ? el.selectionStart : value.length;
end = typeof el.selectionEnd === 'number' ? el.selectionEnd : start;
updatedValue = value.slice(0, start) + text + value.slice(end);
caretPos = start + text.length;
$el.val(updatedValue).trigger('input').trigger('change').focus();
if (typeof el.setSelectionRange === 'function') el.setSelectionRange(caretPos, caretPos);
}
function buildQuickPhrasesPanelHtml(textareaId) {
var phrases = getCurrentQuickPhrases();
if (!phrases.length) return '';
return joinHtml([
'<div class="rmQuickPhrasesPanel ', RESIZE_CLASS, '" data-rm-target="', textareaId, '">',
phrases.map(function (phrase) {
return joinHtml([
'<button type="button" class="rmQuickPhraseActionBtn" data-rm-target="', textareaId,
'" data-rm-phrase="', escapeHtml(phrase), '">',
escapeHtml(phrase),
'</button>'
]);
}).join(''),
'</div>'
]);
}
function getMultiNominationCommentText(commentsByPage, pageTitle) {
var key = normTitle(pageTitle);
if (!commentsByPage || !Object.prototype.hasOwnProperty.call(commentsByPage, key)) return '';
return normalizeQuickPhraseValue(commentsByPage[key]);
}
function hasCommentsForEveryMultiNominationPage(pages, commentsByPage) {
var list = Array.isArray(pages) ? pages.filter(Boolean) : [];
return list.length > 0 && list.every(function (pageName) {
return !!getMultiNominationCommentText(commentsByPage, pageName);
});
}
function validateMultiNominationText(pages, bodyText, commentsByPage, itemGenitive) {
if (normalizeQuickPhraseValue(bodyText) || hasCommentsForEveryMultiNominationPage(pages, commentsByPage)) return true;
alert('Укажите общий текст номинации или комментарий для каждой ' + (itemGenitive || 'страницы') + '.');
return false;
}
function buildMultiNominationText(pages, bodyText, commentsByPage, options) {
var opts = options || {};
var list = Array.isArray(pages) ? pages.filter(Boolean) : [];
var body = normalizeQuickPhraseValue(bodyText);
var hasAllPageComments = hasCommentsForEveryMultiNominationPage(list, commentsByPage);
var headingLevel = Math.max(2, parseInt(opts.headingLevel, 10) || 3);
var headingMarks = new Array(headingLevel + 1).join('=');
var formatItemLabel = typeof opts.formatItemLabel === 'function'
? opts.formatItemLabel
: function (pageName) { return '[[:' + pageName + ']]'; };
var pageSections = list.map(function (pageName, index) {
var comment = getMultiNominationCommentText(commentsByPage, pageName);
var sectionPrefix = (index === 0 && opts.leadingBlankLine === false) ? '' : '\n';
return sectionPrefix + headingMarks + ' ' + formatItemLabel(pageName) + ' ' + headingMarks + '\n' + (comment ? appendNominationSignature(comment) + '\n' : '');
}).join('');
var commonSectionText = body
? appendNominationSignature(body)
: (hasAllPageComments ? '' : appendNominationSignature(''));
return pageSections + (commonSectionText ? '\n' + headingMarks + ' По всем ' + headingMarks + '\n' + commonSectionText : '');
}
function buildMultiNominationListText(pages, bodyText, commentsByPage, options) {
var opts = options || {};
var list = Array.isArray(pages) ? pages.filter(Boolean) : [];
var body = normalizeQuickPhraseValue(bodyText);
var hasAllPageComments = hasCommentsForEveryMultiNominationPage(list, commentsByPage);
var formatItemLabel = typeof opts.formatItemLabel === 'function'
? opts.formatItemLabel
: function (pageName) { return '[[:' + pageName + ']]'; };
var pageLines = list.map(function (pageName) {
var comment = getMultiNominationCommentText(commentsByPage, pageName);
return '* ' + formatItemLabel(pageName) + (comment ? '\n' + appendNominationSignature(comment).split('\n').map(function (line) { return '*: ' + line; }).join('\n') : '');
}).join('\n');
var commonText = body
? appendNominationSignature(body)
: (hasAllPageComments ? '' : appendNominationSignature(''));
return pageLines + (pageLines && commonText ? '\n' : '') + commonText;
}
function collectMultiNominationComments(normalizePageName) {
var comments = {};
var normalize = typeof normalizePageName === 'function' ? normalizePageName : normTitle;
$('.rmMultiPageBlock').each(function () {
var $block = $(this);
var pageName = normalize(($block.find('.rmMultiPageInput').val() || '').trim());
var comment = normalizeQuickPhraseValue($block.find('.rmMultiPageCommentInput').val());
if (!pageName) return;
comments[pageName] = comment;
});
return comments;
}
function getNominationPublishText(job) {
if (job && job.isMulti) return String(job.msg || '');
return appendNominationSignature(job && job.msg);
}
function getNominationConflictRule(job) {
if (!job || job.mode !== 'nominate') return null;
if (job.opId === 'tRm' || job.opId === 'mRm') {
return {
id: 'ku',
label: 'КУ',
namePattern: '(?:к\\s*удалению|ку)',
detect: function (articleText) {
var match = String(articleText || '').match(/\{\{\s*((?:к\s*удалению|ку))\s*(?:\||\}\})/i);
if (!match) return null;
var templateName = ucfirst(String(match[1] || '').replace(/\s+/g, ' ').trim());
return {
label: 'КУ',
templateName: templateName || 'КУ',
templateDisplay: '{{' + (templateName || 'КУ') + '}}'
};
}
};
}
if (job.opId === 'rnm' || job.opId === 'mRnm') {
return {
id: 'kpm',
label: 'КПМ',
namePattern: '(?:к\\s*переименованию|кпм|rename)',
detect: function (articleText) {
var match = String(articleText || '').match(/\{\{\s*((?:к\s*переименованию|кпм|rename))\s*(?:\||\}\})/i);
if (!match) return null;
var templateName = ucfirst(String(match[1] || '').replace(/\s+/g, ' ').trim());
return {
label: 'КПМ',
templateName: templateName || 'КПМ',
templateDisplay: '{{' + (templateName || 'КПМ') + '}}'
};
}
};
}
return null;
}
function detectNominationConflict(articleText, job) {
var rule = getNominationConflictRule(job);
if (!rule || typeof rule.detect !== 'function') return null;
return rule.detect(articleText);
}
function getConflictDecisionForPage(job, pageName) {
var decisions = job && job.conflictDecisions;
var key = normTitle(pageName);
return decisions && decisions[key] ? decisions[key] : null;
}
function getCategoryMergeRe() {
return new RegExp('\\{\\{\\s*(?:' + cfg.categoryTemplates.merge + ')\\s*\\|\\s*([^\\}]+)\\}\\}', 'i');
}
function eachSequential(targets, iteratee, done) {
var i = 0;
(function next(err) {
if (err || i >= targets.length) { done(err || null); return; }
iteratee(targets[i++], next);
}(null));
}
function normalizeSectionForLink(sectionTitle) {
return (sectionTitle || '').trim()
.replace(/\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g, function (_, target, label) {
var v = (label || target || '').trim();
return v.charAt(0) === ':' ? v.slice(1) : v;
})
.replace(/''+/g, '').replace(/\s+/g, ' ').trim();
}
function getViewportWidth() {
return Math.floor(Math.max(
(document.documentElement && document.documentElement.clientWidth) || 0,
(typeof window.innerWidth === 'number' && window.innerWidth) || 0,
$(window).width() || 0
));
}
function getVisualViewportWidth() {
var widths = [];
if (window.visualViewport && typeof window.visualViewport.width === 'number' && window.visualViewport.width > 0) widths.push(window.visualViewport.width);
if (window.screen && window.screen.width > 0) widths.push(window.screen.width);
return widths.length ? Math.floor(Math.min.apply(Math, widths)) : getViewportWidth();
}
function isTouchModalDevice() {
return !!(
(window.matchMedia && window.matchMedia('(pointer: coarse)').matches) ||
('ontouchstart' in window) ||
(navigator.maxTouchPoints && navigator.maxTouchPoints > 0) ||
(navigator.msMaxTouchPoints && navigator.msMaxTouchPoints > 0)
);
}
function getModalLayout() {
var minWidth = parseInt(sz.taMinW, 10) || 180;
var layoutWidth = getViewportWidth();
var visualWidth = getVisualViewportWidth();
var contentWidth = Math.max(minWidth, Math.floor($('#content').width() || $('#content').innerWidth() || $(window).width() || minWidth));
var isMobile = layoutWidth <= sz.mobileBp;
var isTouchDesktop = !isMobile &&
isTouchModalDevice() &&
visualWidth > 0 &&
visualWidth <= sz.mobileBp &&
layoutWidth >= visualWidth + sz.touchDesktopGap;
var useFullWidth = isMobile || isTouchDesktop;
var maxOuterWidth;
var defaultOuterWidth;
var desktopWidth;
if (isMobile) maxOuterWidth = Math.max(minWidth, (visualWidth || contentWidth) - sz.viewportGap);
else if (isTouchDesktop) maxOuterWidth = Math.max(minWidth, contentWidth - 32);
else maxOuterWidth = Math.max(minWidth, Math.min(contentWidth, (visualWidth ? visualWidth - sz.viewportGap : contentWidth)));
desktopWidth = Math.max(minWidth, Math.floor(contentWidth * sz.modalRatio));
if (useFullWidth || desktopWidth < sz.modalMinWide) defaultOuterWidth = contentWidth;
else if (desktopWidth < sz.modalDefaultWide) defaultOuterWidth = sz.modalDefaultWide;
else defaultOuterWidth = desktopWidth;
return {
minWidth: minWidth,
isMobile: isMobile,
isTouchDesktop: isTouchDesktop,
useFullWidth: useFullWidth,
shouldCenter: useFullWidth || mwCfg.skin === 'minerva',
maxOuterWidth: maxOuterWidth,
defaultOuterWidth: Math.min(defaultOuterWidth, maxOuterWidth)
};
}
function getDefaultResizableWidth(frameWidth) {
var layout = getModalLayout();
return Math.max(layout.minWidth, layout.defaultOuterWidth - Math.floor(frameWidth || 0));
}
function getBoxFrameWidth($el) {
function px(prop) {
var n = parseFloat($el.css(prop));
return isNaN(n) ? 0 : n;
}
return px('padding-left') + px('padding-right') + px('border-left-width') + px('border-right-width');
}
// ═══════════════════════════════════════════════════════════════════════════
// API
// ═══════════════════════════════════════════════════════════════════════════
function getApiUrl() {
return (mw.util && typeof mw.util.wikiScript === 'function') ? mw.util.wikiScript('api') : '/w/api.php';
}
function getCsrfTokenValue() {
return (mw.user && mw.user.tokens && typeof mw.user.tokens.get === 'function')
? mw.user.tokens.get('csrfToken')
: null;
}
function storeCsrfToken(token) {
if (!token || !mw.user || !mw.user.tokens || typeof mw.user.tokens.set !== 'function') return;
mw.user.tokens.set({ csrfToken: token });
}
function isValidCsrfToken(token) {
return typeof token === 'string' && !!token && token !== '+\\';
}
function fetchCsrfToken(forceRefresh, callback) {
var cachedToken = getCsrfTokenValue();
if (!forceRefresh && isValidCsrfToken(cachedToken)) {
callback(cachedToken);
return;
}
$.ajax({
url: getApiUrl(),
method: 'GET',
dataType: 'json',
data: { action: 'query', meta: 'tokens', type: 'csrf', format: 'json' }
})
.done(function (data) {
var token = data && data.query && data.query.tokens && data.query.tokens.csrftoken;
if (isValidCsrfToken(token)) {
storeCsrfToken(token);
callback(token);
return;
}
callback(null);
})
.fail(function () {
callback(null);
});
}
function apiReq(params, mode, callback) {
var isWrite = mode === 'edit' || mode === 'discussiontoolssubscribe' || mode === 'options';
function sendRequest(retryWithFreshToken) {
var reqParams = $.extend({}, params, { format: 'json', action: mode });
if (!isWrite) {
$.ajax({ url: getApiUrl(), method: 'GET', data: reqParams, dataType: 'json' })
.done(function (data) { if (callback) callback(data); })
.fail(function (jqXHR, status) {
console.error('Ошибка API: ' + status);
if (callback) callback({ error: { code: 'network', info: status } });
});
return;
}
fetchCsrfToken(!!retryWithFreshToken, function (token) {
if (!isValidCsrfToken(token)) {
if (callback) callback({ error: { code: 'badtoken', info: 'Не удалось получить CSRF-токен.' } });
return;
}
reqParams.token = token;
$.ajax({ url: getApiUrl(), method: 'POST', data: reqParams, dataType: 'json' })
.done(function (data) {
var err = data && data.error;
var isBadToken = err && (err.code === 'badtoken' || /invalid csrf token/i.test(String(err.info || '')));
if (isBadToken && !retryWithFreshToken) {
sendRequest(true);
return;
}
if (callback) callback(data);
})
.fail(function (jqXHR, status) {
console.error('Ошибка API: ' + status);
if (callback) callback({ error: { code: 'network', info: status } });
});
});
}
sendRequest(false);
}
function saveSettingsToServer(settings, callback) {
var normalized = normalizeRemoverSettings(settings);
if (!mwCfg.wgUserName) {
callback({ code: 'notloggedin', info: 'Сохранять настройки можно только после входа в учётную запись.' });
return;
}
apiReq({ optionname: settingsOptionName, optionvalue: JSON.stringify(normalized) }, 'options', function (resp) {
if (resp && resp.options === 'success') {
callback(null, updateStoredSettingsState(normalized));
return;
}
callback((resp && resp.error) || { code: 'options_failed', info: 'Не удалось сохранить настройки.' });
});
}
function resetSettingsOnServer(callback) {
if (!mwCfg.wgUserName) {
callback({ code: 'notloggedin', info: 'Сбрасывать настройки можно только после входа в учётную запись.' });
return;
}
apiReq({ change: settingsOptionName }, 'options', function (resp) {
if (resp && resp.options === 'success') {
callback(null, updateStoredSettingsState(settingsDefaults, true));
return;
}
callback((resp && resp.error) || { code: 'options_failed', info: 'Не удалось сбросить настройки.' });
});
}
function getFirstQueryPage(data) {
var pages = data && data.query && data.query.pages;
if (!pages) return null;
return pages[Object.keys(pages)[0]] || null;
}
function extractRevisionContent(rev) {
if (!rev) return null;
if (typeof rev['*'] === 'string') return rev['*'];
if (rev.slots && rev.slots.main) {
if (typeof rev.slots.main['*'] === 'string') return rev.slots.main['*'];
if (typeof rev.slots.main.content === 'string') return rev.slots.main.content;
}
return null;
}
function makeReadError(apiError, fallbackCode, fallbackInfo) {
var err = apiError || {};
return {
code: err.code || fallbackCode || 'read_failed',
info: err.info || fallbackInfo || 'Не удалось получить содержимое.'
};
}
function getTextWithTimestamp(pageName, callback) {
apiReq({ prop: 'revisions', rvprop: 'content|timestamp', rvslots: 'main', titles: pageName }, 'query', function (data) {
var content;
var page;
if (data && data.error) {
callback(null, null, data.error);
return;
}
if (!data || !data.query || !data.query.pages) {
callback(null, null, { code: 'read_failed', info: 'Некорректный ответ API при чтении страницы.' });
return;
}
page = getFirstQueryPage(data);
if (!page) {
callback(null, null, { code: 'read_failed', info: 'API не вернул данные страницы.' });
return;
}
if (page.invalid !== undefined) {
callback(null, null, { code: 'invalidtitle', info: page.invalidreason || 'Некорректное название страницы.' });
return;
}
if (page.missing !== undefined) {
callback(null, null, null);
return;
}
if (!page.revisions || !page.revisions.length) {
callback(null, null, { code: 'read_failed', info: 'API не вернул ревизии страницы.' });
return;
}
content = extractRevisionContent(page.revisions[0]);
if (content === null) {
callback(null, null, { code: 'content_missing', info: 'API не вернул текст страницы.' });
return;
}
callback(content, page.revisions[0].timestamp || null, null);
});
}
function getText(pageName, callback) {
getTextWithTimestamp(pageName, function (text, baseTimestamp, err) { callback(text, err); });
}
function editPageContent(pageTitle, options, buildFn, callback) {
var opts = options || {};
var maxRetries = Math.max(0, parseInt(opts.editConflictRetries, 10) || 1);
(function attempt(retry) {
getTextWithTimestamp(pageTitle, function (sourceText, baseTimestamp, readErr) {
if (readErr) {
callback(makeReadError(readErr, opts.readErrorCode || 'read_failed', 'Не удалось получить содержимое страницы «' + pageTitle + '».'));
return;
}
if (sourceText === null) {
callback({ code: opts.readErrorCode || 'read_failed', info: opts.readError || 'Страница «' + pageTitle + '» не существует.' });
return;
}
var done = (function () {
var called = false;
return function (result) {
if (called) return;
called = true;
if (!result || result.error) { callback((result && result.error) || { code: 'build_failed', info: 'Не удалось подготовить правку.' }, result && result.meta || null); return; }
if (result.skip) { callback(null, result.meta || null); return; }
if (typeof result.text !== 'string') { callback({ code: 'build_failed', info: 'Не удалось подготовить правку.' }); return; }
var ep = { title: pageTitle, text: result.text, summary: result.summary || opts.summary || '' };
if (opts.watchlist) ep.watchlist = opts.watchlist;
if (opts.assertuser) ep.assertuser = opts.assertuser;
if (opts.createonly) ep.createonly = opts.createonly;
if (opts.useBaseTimestamp !== false && baseTimestamp) ep.basetimestamp = baseTimestamp;
apiReq(ep, 'edit', function (resp) {
var err = resp && resp.error ? resp.error : null;
if (err && err.code === 'editconflict' && retry < maxRetries) { attempt(retry + 1); return; }
callback(err, result.meta || null);
});
};
}());
var maybe = buildFn(sourceText, done);
if (maybe !== undefined) done(maybe);
});
}(0));
}
// ═══════════════════════════════════════════════════════════════════════════
// ШАБЛОНЫ: удаление и вставка
// ═══════════════════════════════════════════════════════════════════════════
function findBalancedTemplateEnd(text, start) {
var depth = 0;
var i = start;
var len = text.length;
while (i < len - 1) {
if (text.charAt(i) === '{' && text.charAt(i + 1) === '{') {
depth++;
i += 2;
continue;
}
if (text.charAt(i) === '}' && text.charAt(i + 1) === '}') {
depth--;
i += 2;
if (depth === 0) return i;
continue;
}
i++;
}
return -1;
}
function splitTemplateTopLevelParts(innerText) {
var parts = [];
var start = 0;
var templateDepth = 0;
var linkDepth = 0;
var i = 0;
var text = String(innerText || '');
while (i < text.length) {
if (text.charAt(i) === '{' && text.charAt(i + 1) === '{') {
templateDepth++;
i += 2;
continue;
}
if (text.charAt(i) === '}' && text.charAt(i + 1) === '}' && templateDepth > 0) {
templateDepth--;
i += 2;
continue;
}
if (text.charAt(i) === '[' && text.charAt(i + 1) === '[') {
linkDepth++;
i += 2;
continue;
}
if (text.charAt(i) === ']' && text.charAt(i + 1) === ']' && linkDepth > 0) {
linkDepth--;
i += 2;
continue;
}
if (text.charAt(i) === '|' && templateDepth === 0 && linkDepth === 0) {
parts.push(text.slice(start, i));
start = i + 1;
}
i++;
}
parts.push(text.slice(start));
return parts;
}
function getTemplateMatchAt(text, start, nameRe) {
var end = findBalancedTemplateEnd(text, start);
var parts;
var rawName;
var name;
if (end < 0) return null;
parts = splitTemplateTopLevelParts(text.slice(start + 2, end - 2));
rawName = String(parts.shift() || '').trim();
name = rawName.replace(/^(?:safe)?(?:subst|подст)\s*:\s*/i, '').replace(RE_TEMPLATE_NS, '').replace(/_/g, ' ').replace(/\s+/g, ' ').trim();
if (!nameRe.test(name)) return null;
return {
start: start,
end: end,
text: text.slice(start, end),
name: rawName,
params: parts.map(function (part) { return part.trim(); })
};
}
function findTemplateByPattern(text, namePattern) {
var source = String(text || '');
var nameRe = new RegExp('^(?:' + namePattern + ')$', 'i');
var i = 0;
var end;
var match;
while (i < source.length - 1) {
if (source.charAt(i) === '{' && source.charAt(i + 1) === '{') {
match = getTemplateMatchAt(source, i, nameRe);
if (match) return match;
end = findBalancedTemplateEnd(source, i);
if (end > i) { i = end; continue; }
}
i++;
}
return null;
}
function hasTemplateWithDateByPattern(text, namePattern, dateIso) {
var source = String(text || '');
var nameRe = new RegExp('^(?:' + namePattern + ')$', 'i');
var targetDate = convertToStandardDate(dateIso);
var i = 0;
var end;
var match;
var templateDate;
if (!targetDate) return false;
while (i < source.length - 1) {
if (source.charAt(i) === '{' && source.charAt(i + 1) === '{') {
match = getTemplateMatchAt(source, i, nameRe);
if (match) {
templateDate = convertToStandardDate(String(match.params[0] || '').replace(/^\s*1\s*=\s*/, ''));
if (templateDate === targetDate) return true;
i = match.end;
continue;
}
end = findBalancedTemplateEnd(source, i);
if (end > i) { i = end; continue; }
}
i++;
}
return false;
}
function getTemplateRemovalRange(text, match) {
var start = match.start;
var end = match.end;
var before = text.slice(0, start).match(/<noinclude>\s*$/i);
var after;
if (before) {
after = text.slice(end).match(/^\s*<\/noinclude>\s*\n?/i);
if (after) {
return { start: before.index, end: end + after[0].length };
}
}
after = text.slice(end).match(/^[ \t]*(?:\r?\n)?/);
if (after) end += after[0].length;
return { start: start, end: end };
}
function stripTemplatesByPattern(text, namePattern) {
var source = String(text || '');
var nameRe = new RegExp('^(?:' + namePattern + ')$', 'i');
var ranges = [];
var out = [];
var pos = 0;
var i = 0;
var end;
var match;
var range;
while (i < source.length - 1) {
if (source.charAt(i) === '{' && source.charAt(i + 1) === '{') {
match = getTemplateMatchAt(source, i, nameRe);
if (match) {
range = getTemplateRemovalRange(source, match);
ranges.push(range);
i = Math.max(match.end, range.end);
continue;
}
end = findBalancedTemplateEnd(source, i);
if (end > i) { i = end; continue; }
}
i++;
}
if (!ranges.length) return { text: source, removed: false };
ranges.forEach(function (r) {
if (r.start < pos) r.start = pos;
out.push(source.slice(pos, r.start));
pos = r.end;
});
out.push(source.slice(pos));
return { text: out.join(''), removed: true };
}
function removeTemplatesByAliases(text, aliases) {
var pattern = buildTemplateNamePattern(aliases);
if (!pattern) return { text: text, removed: false };
return stripTemplatesByPattern(text, '(?:' + pattern + ')');
}
function normalizeAliasKey(title) {
return String(title || '').replace(/_/g, ' ').toLowerCase().trim();
}
function dedupeAliases(aliases) {
var seen = {};
var result = [];
(aliases || []).forEach(function (alias) {
var key = normalizeAliasKey(alias);
if (!key || seen[key]) return;
seen[key] = true;
result.push(alias);
});
return result;
}
function resolveTemplateRedirectAliases(titles, callback) {
var list = dedupeAliases(titles);
var missingTitles = list.filter(function (t) { return !tplAliasCache[t]; });
function collectAliases() {
var allAliases = [];
list.forEach(function (title) { allAliases = allAliases.concat(tplAliasCache[title] || [title]); });
callback(dedupeAliases(allAliases));
}
if (!missingTitles.length) { collectAliases(); return; }
(function resolveChunk(offset) {
if (offset >= missingTitles.length) { collectAliases(); return; }
var chunk = missingTitles.slice(offset, offset + 20);
var chunkByKey = {};
chunk.forEach(function (t) { chunkByKey[normalizeAliasKey(t)] = t; });
apiReq({ prop: 'redirects', rdlimit: 'max', titles: chunk.join('|') }, 'query', function (resp) {
var pages = resp && resp.query && resp.query.pages ? resp.query.pages : {};
Object.keys(pages).forEach(function (pid) {
var p = pages[pid];
var sourceTitle;
var found;
if (!p || !p.title) return;
sourceTitle = chunkByKey[normalizeAliasKey(p.title)];
if (!sourceTitle) return;
found = [p.title].concat((p.redirects || []).map(function (r) { return r.title; }));
tplAliasCache[sourceTitle] = dedupeAliases(found);
});
chunk.forEach(function (t) { if (!tplAliasCache[t]) tplAliasCache[t] = [t]; });
resolveChunk(offset + 20);
});
}(0));
}
function removeTransferTemplatesLocal(articleText, transferMode) {
var result = { text: articleText, removedKbu: false, removedKul: false, removedHangon: false };
if (transferMode === 'none') return result;
if (transferMode === 'kbu' || transferMode === 'both') {
var kbu = stripTemplatesByPattern(result.text, '(?:' + KBU_PATTERN_STR + ')');
result.text = kbu.text; result.removedKbu = kbu.removed;
}
if (transferMode === 'kul' || transferMode === 'both') {
var kul = stripTemplatesByPattern(result.text, KUL_PATTERN_STR);
result.text = kul.text; result.removedKul = kul.removed;
}
var hangon = stripTemplatesByPattern(result.text, HANGON_PATTERN_STR);
result.text = hangon.text; result.removedHangon = hangon.removed;
return result;
}
function removeTransferTemplatesWithApiFallback(pageName, articleText, transferMode, localResult, callback) {
var needKbu = (transferMode === 'kbu' || transferMode === 'both') && !localResult.removedKbu;
var needKul = (transferMode === 'kul' || transferMode === 'both') && !localResult.removedKul;
var needHangon = transferMode !== 'none' && !localResult.removedHangon;
if (!needKbu && !needKul && !needHangon) { callback(localResult); return; }
var titleMap = {};
if (needKbu) ['Шаблон:К быстрому удалению','Шаблон:К отсроченному удалению','Шаблон:Deleteslow','Шаблон:Ds'].forEach(function (t) { titleMap[t] = true; });
if (needKul) titleMap['Шаблон:К улучшению'] = true;
if (needHangon) { titleMap['Шаблон:Hangon'] = true; titleMap['Шаблон:Hang-on'] = true; }
apiReq({ prop: 'templates', titles: pageName, tllimit: 'max' }, 'query', function (data) {
var page = getFirstQueryPage(data);
if (page && page.templates) {
page.templates.forEach(function (tpl) {
var norm = normalizeTemplateName(tpl.title);
if ((needKbu && (RE_KBU_PATTERNS.test(norm) || norm === 'к быстрому удалению' || norm === 'к отсроченному удалению' || norm === 'deleteslow' || norm === 'ds')) ||
(needKul && RE_KUL_PATTERN.test(norm)) ||
(needHangon && RE_HANGON.test(norm))) {
titleMap[tpl.title] = true;
}
});
}
var titles = Object.keys(titleMap);
if (!titles.length) { callback(localResult); return; }
resolveTemplateRedirectAliases(titles, function (aliases) {
var updated = $.extend({}, localResult);
var r = removeTemplatesByAliases(updated.text, aliases);
updated.text = r.text;
if (r.removed) {
if (needKbu) updated.removedKbu = true;
if (needKul) updated.removedKul = true;
if (needHangon) updated.removedHangon = true;
}
callback(updated);
});
});
}
function removeMoveToCommonsTemplatesLocal(articleText) {
var r = removeTemplatesByAliases(articleText, MOVE_TO_COMMONS_TEMPLATE_ALIASES);
return { text: r.text, removedMoveToCommons: r.removed };
}
function removeMoveToCommonsTemplatesWithApiFallback(pageName, articleText, localResult, callback) {
apiReq({ prop: 'templates', titles: pageName, tllimit: 'max' }, 'query', function (data) {
var page = getFirstQueryPage(data);
var titleMap = {};
var normTarget = normalizeTemplateName(MOVE_TO_COMMONS_TEMPLATE_TITLE);
var titles;
titleMap[MOVE_TO_COMMONS_TEMPLATE_TITLE] = true;
if (page && page.templates) {
page.templates.forEach(function (tpl) {
var norm = normalizeTemplateName(tpl.title);
if (norm === normTarget || MOVE_TO_COMMONS_TEMPLATE_ALIASES.some(function (alias) {
return norm === normalizeTemplateName(alias);
})) {
titleMap[tpl.title] = true;
}
});
}
titles = Object.keys(titleMap);
resolveTemplateRedirectAliases(titles, function (resolvedAliases) {
var aliases = dedupeAliases(MOVE_TO_COMMONS_TEMPLATE_ALIASES.concat(resolvedAliases));
var updated = $.extend({}, localResult);
var r = removeTemplatesByAliases(articleText, aliases);
updated.text = r.text;
updated.removedMoveToCommons = r.removed;
callback(updated);
});
});
}
// ─── Вставка шаблонов ────────────────────────────────────────────────────
function findInsertPositionAfterProjectTemplates(text) {
var pos = 0, len = text.length;
while (pos < len) {
var wsMatch = text.slice(pos).match(/^[\t ]*\n/);
if (wsMatch) { pos += wsMatch[0].length; continue; }
if (text.charAt(pos) !== '{' || text.charAt(pos + 1) !== '{') break;
var afterOpen = text.slice(pos + 2);
var nameMatch = afterOpen.match(/^[\s]*([\s\S]*?)[\s]*(?:\||\}\})/);
var templateEnd;
if (!nameMatch) break;
var tplName = nameMatch[1].toLowerCase().replace(/\s+/g, ' ').trim();
if (!/^статья проекта(\s|$)/.test(tplName) && tplName !== 'блок проектов статьи') break;
templateEnd = findBalancedTemplateEnd(text, pos);
if (templateEnd < 0) break;
pos = templateEnd;
if (pos < len && text.charAt(pos) === '\n') pos++;
}
return pos;
}
function insertTplOnTalkPage(text, tplText, sep) {
var s = (sep === undefined) ? '\n' : sep;
var insertPos = findInsertPositionAfterProjectTemplates(text);
if (insertPos === 0) return tplText + (text.length ? s + text.replace(/^\n+/, '') : '');
var before = text.slice(0, insertPos).replace(/\n+$/, '');
var after = text.slice(insertPos).replace(/^\n+/, '');
return before + '\n' + tplText + (after.length ? s + after : '');
}
function wrapInNoinclude(text, templateText) {
var match = text.match(RE_NOINCLUDE);
if (match) {
// Если перед noinclude есть непробельный контент — вставляем новый noinclude сверху
var before = text.slice(0, text.indexOf(match[0]));
if (/\S/.test(before)) {
return '<noinclude>' + templateText + '</noinclude>\n' + text;
}
var content = match[2];
if (content.length > 0 && content.charAt(content.length - 1) !== '\n') content += '\n';
return text.replace(match[0], match[1] + '<noinclude>' + content + templateText + '\n</noinclude>');
}
return '<noinclude>' + templateText + '</noinclude>\n' + text;
}
function getDateSectionTalkTemplateConfig(closeType) {
var config = DATE_SECTION_TALK_TEMPLATES[closeType];
return typeof config === 'string' ? { name: config } : (config || null);
}
function upsertDateSectionTemplateOnTalkPage(text, templateName, dateIso, sectionTitle, options) {
var source = text || '';
var normalizedSection = String(sectionTitle || '').trim();
var opts = options || {};
var tplRe = buildTemplateTransclusionRegex(templateName, opts.aliases);
var tplMatch = tplRe ? source.match(tplRe) : null;
function buildSectionParam(sectionIndex, currentParams) {
var paramName;
var paramsText = String(currentParams || '');
if (!normalizedSection) return '';
if (opts.singleSectionParam) {
paramName = opts.sectionParam || 'раздел обсуждения';
if (new RegExp('(?:^|\\|)\\s*(?:' + escapeRegExp(paramName) + '|l1)\\s*=', 'i').test(paramsText)) return '';
return '|' + paramName + '=' + normalizedSection;
}
return '|l' + sectionIndex + '=' + normalizedSection;
}
function buildExistingTplWithCanonicalName(suffix) {
return T_OPEN + templateName + String(tplMatch[2] || '').replace(/\s+$/, '') + (suffix || '') + T_CLOSE;
}
if (!tplMatch) {
return { text: insertTplOnTalkPage(source, T_OPEN + templateName + '|' + dateIso + buildSectionParam(1, '') + T_CLOSE, '\n'), status: 'created' };
}
var parts = tplMatch[2].split('|').map(function (p) { return p.trim(); }).filter(Boolean);
var existingDates = parts.filter(function (p) { return p.indexOf('=') === -1; }).map(function (p) { return convertToStandardDate(p); });
var stdDate = convertToStandardDate(dateIso);
if (existingDates.indexOf(stdDate) !== -1) {
var canonicalTpl = buildExistingTplWithCanonicalName('');
if (canonicalTpl !== tplMatch[0]) {
return { text: source.replace(tplMatch[0], canonicalTpl), status: 'updated' };
}
return { text: source, status: 'already_present' };
}
var nextIdx = existingDates.length + 1;
var suffix = '|' + dateIso + buildSectionParam(nextIdx, tplMatch[2]);
return {
text: source.replace(tplMatch[0], buildExistingTplWithCanonicalName(suffix)),
status: 'updated'
};
}
function getNamedTemplateParam(params, names) {
var wanted = {};
names.forEach(function (name) { wanted[String(name).toLowerCase()] = true; });
for (var i = 0; i < params.length; i++) {
var m = String(params[i] || '').match(/^\s*([^=]+?)\s*=\s*([\s\S]*)$/);
if (m && wanted[m[1].toLowerCase().trim()]) return m[2].trim();
}
return '';
}
function getSingleResultTemplateDate(paramsText, templateName) {
var params = String(paramsText || '').split('|').map(function (p) { return p.trim(); }).filter(Boolean);
var namedDate = getNamedTemplateParam(params, templateName === 'Переименовано' ? ['date', 'дата'] : ['date']);
var positionalDate = params.filter(function (p) { return p.indexOf('=') === -1; })[0] || '';
return convertToStandardDate(namedDate || positionalDate);
}
function upsertSingleDateResultTemplateOnTalkPage(text, templateName, dateIso, paramsText) {
var source = text || '';
var tplRe = buildTemplateTransclusionRegex(templateName, null, 'ig');
var tplMatch = null;
var match;
var newTpl = T_OPEN + templateName + '|' + dateIso + (paramsText ? '|' + paramsText : '') + T_CLOSE;
var stdDate = convertToStandardDate(dateIso);
var existingDate;
var canonicalTpl;
if (tplRe) {
while ((match = tplRe.exec(source)) !== null) {
existingDate = getSingleResultTemplateDate(match[2], templateName);
if (existingDate === stdDate) { tplMatch = match; break; }
if (match[0].length === 0) tplRe.lastIndex++;
}
}
if (!tplMatch) {
return { text: insertTplOnTalkPage(source, newTpl, '\n'), status: 'created' };
}
canonicalTpl = T_OPEN + templateName + String(tplMatch[2] || '').replace(/\s+$/, '') + T_CLOSE;
if (canonicalTpl !== tplMatch[0]) {
return { text: source.replace(tplMatch[0], canonicalTpl), status: 'updated' };
}
return { text: source, status: 'already_present' };
}
function buildConditionalRetTemplateText(dateIso, sectionTitle, reasonText, deadlineText, sectionIndex) {
var normalizedSection = String(sectionTitle || '').trim();
var normalizedReason = normalizeQuickPhraseValue(reasonText);
var normalizedDeadline = String(deadlineText || '').trim();
var index = parseInt(sectionIndex, 10);
var tpl = 'Условно оставлено|' + dateIso;
if (isNaN(index) || index < 1) index = 1;
if (normalizedSection) tpl += '|l' + index + '=' + normalizedSection;
if (normalizedReason) tpl += '|пояснение=' + normalizedReason;
if (normalizedDeadline) tpl += '|срок=' + normalizedDeadline;
return T_OPEN + tpl + T_CLOSE;
}
function upsertConditionalRetTemplateOnTalkPage(text, dateIso, sectionTitle, reasonText, deadlineText) {
var source = text || '';
var normalizedSection = String(sectionTitle || '').trim();
var normalizedReason = normalizeQuickPhraseValue(reasonText);
var normalizedDeadline = String(deadlineText || '').trim();
var tplRe = buildTemplateTransclusionRegex('Условно оставлено');
var tplMatch = tplRe ? source.match(tplRe) : null;
function buildExistingTplWithCanonicalName(suffix) {
return T_OPEN + 'Условно оставлено' + String(tplMatch[2] || '').replace(/\s+$/, '') + (suffix || '') + T_CLOSE;
}
if (!tplMatch) {
return {
text: insertTplOnTalkPage(source, buildConditionalRetTemplateText(dateIso, normalizedSection, normalizedReason, normalizedDeadline, 1), '\n'),
status: 'created'
};
}
var parts = tplMatch[2].split('|').map(function (p) { return p.trim(); }).filter(Boolean);
var existingDates = parts.filter(function (p) { return p.indexOf('=') === -1; }).map(function (p) { return convertToStandardDate(p); });
var stdDate = convertToStandardDate(dateIso);
if (existingDates.indexOf(stdDate) !== -1) {
var canonicalTpl = buildExistingTplWithCanonicalName('');
if (canonicalTpl !== tplMatch[0]) {
return { text: source.replace(tplMatch[0], canonicalTpl), status: 'updated' };
}
return { text: source, status: 'already_present' };
}
var nextIdx = existingDates.length + 1;
var suffix = '|' + dateIso;
if (normalizedSection) suffix += '|l' + nextIdx + '=' + normalizedSection;
if (normalizedReason) suffix += '|пояснение=' + normalizedReason;
if (normalizedDeadline) suffix += '|срок=' + normalizedDeadline;
return {
text: source.replace(tplMatch[0], buildExistingTplWithCanonicalName(suffix)),
status: 'updated'
};
}
// ═══════════════════════════════════════════════════════════════════════════
// ПАЙПЛАЙН НОМИНАЦИИ
// ═══════════════════════════════════════════════════════════════════════════
function runNominationPipeline(steps) {
var s = steps;
var ctx = { templateMeta: null, nominationInfo: null };
var stages = [
{
name: 'шаблон',
fn: function (next) {
s.templateStep(function (err, meta) { ctx.templateMeta = meta || null; next(err); });
}
},
{
name: 'номинация',
pendingText: 'Публикуется номинация...',
successText: 'Номинация опубликована.',
errorText: 'Публикация номинации.',
fn: function (next) {
s.nominationStep(function (err, info) { ctx.nominationInfo = info || null; next(err); });
}
},
{
name: 'подписка',
shouldRun: function () {
var info = ctx.nominationInfo;
return !!(setSubscribe && info && info.pageTitle && info.sectionTitle);
},
fn: function (next) {
subscribeToTopic(ctx.nominationInfo.pageTitle, ctx.nominationInfo.sectionTitle, function () { next(); });
}
},
{
name: 'оповещение',
shouldRun: function () { return !!(setAlert && !s.skipNotify); },
fn: function (next) { s.notifyStep(ctx.nominationInfo, next); }
}
];
(function run(i) {
if (i >= stages.length) { if (typeof s.onSuccess === 'function') s.onSuccess(ctx); return; }
var stage = stages[i];
if (typeof stage.shouldRun === 'function' && !stage.shouldRun()) { run(i + 1); return; }
var statusId = stage.pendingText
? logStatus(stage.pendingText, null, { pending: true, trackError: false })
: null;
try {
stage.fn(function (err) {
if (err) {
if (statusId) logStatus(stage.errorText || ('Этап «' + stage.name + '».'), err, { statusId: statusId });
if (typeof s.onFailure === 'function') s.onFailure(stage.name, err, ctx);
else markSubmitError();
return;
}
if (statusId && stage.successText) logStatus(stage.successText, null, { statusId: statusId, trackError: false });
run(i + 1);
});
} catch (ex) {
var exErr = { code: 'exception', info: (ex && ex.message) ? ex.message : String(ex) };
if (statusId) logStatus(stage.errorText || ('Этап «' + stage.name + '».'), exErr, { statusId: statusId });
if (typeof s.onFailure === 'function') s.onFailure(stage.name, exErr, ctx);
else markSubmitError();
}
}(0));
}
// ─── Публикация номинации ────────────────────────────────────────────────
function publishNomination(opts, callback) {
var cb = callback || function () {};
var maxRetries = Math.max(0, parseInt(opts.editConflictRetries, 10) || 1);
function doPublish() {
apiReq({
title: opts.pageTitle,
section: 'new',
sectiontitle: opts.sectionTitle,
summary: opts.summary,
text: opts.text,
assertuser: mwCfg.wgUserName
}, 'edit', function (resp) {
cb(resp && resp.error ? resp.error : null);
});
}
if (opts.sectionTitle) {
if (!opts.navTemplate) { doPublish(); return; }
apiReq({ title: opts.pageTitle, createonly: '1', text: T_OPEN + opts.navTemplate + '-Навигация' + T_CLOSE + '\n', summary: makeSummary('автоматическая шапка'), assertuser: mwCfg.wgUserName },
'edit', function (resp) {
if (resp && resp.error && resp.error.code !== 'articleexists') { cb(resp.error); return; }
doPublish();
});
return;
}
// Вставка в существующую страницу
if (opts.createText !== undefined) {
(function attempt(retry) {
getTextWithTimestamp(opts.pageTitle, function (pageText, baseTimestamp, readErr) {
var result;
var ep;
if (readErr) { cb(makeReadError(readErr, 'read_failed', opts.readErrorMessage || 'Не удалось получить содержимое.')); return; }
result = pageText === null
? (typeof opts.createText === 'function' ? opts.createText() : opts.createText)
: (opts.buildText ? opts.buildText(pageText) : null);
if (typeof result === 'string') result = { text: result };
if (!result || result.error) { cb((result && result.error) || { code: 'build_failed', info: 'Не удалось подготовить правку.' }); return; }
if (result.skip) { cb(null); return; }
if (typeof result.text !== 'string') { cb({ code: 'build_failed', info: 'Не удалось подготовить правку.' }); return; }
ep = {
title: opts.pageTitle,
text: result.text,
summary: result.summary || opts.summary,
assertuser: mwCfg.wgUserName
};
if (pageText === null) ep.createonly = true;
else if (baseTimestamp) ep.basetimestamp = baseTimestamp;
apiReq(ep, 'edit', function (resp) {
var err = resp && resp.error ? resp.error : null;
if (err && (err.code === 'editconflict' || err.code === 'articleexists') && retry < maxRetries) {
attempt(retry + 1);
return;
}
cb(err);
});
});
}(0));
return;
}
editPageContent(opts.pageTitle, { summary: opts.summary, readError: opts.readErrorMessage || 'Не удалось получить содержимое.' },
function (pageText) { return opts.buildText ? opts.buildText(pageText) : null; },
function (err) { cb(err || null); }
);
}
// ─── Оповещение авторов ──────────────────────────────────────────────────
function notifyAuthor(pg, options, callback) {
var opts = options || {};
var cb = callback || function () {};
var actionText = (typeof opts.actionText === 'string') ? opts.actionText : '';
var discussionPage = (typeof opts.discussionPage === 'string') ? opts.discussionPage : '';
var discussionSection = normalizeSectionForLink((typeof opts.discussionSection === 'string') ? opts.discussionSection : '');
var includeProposed = (typeof opts.includeProposedPrefix === 'boolean') ? opts.includeProposedPrefix : true;
var actionPhrase = ((includeProposed ? 'предложена ' : '') + actionText).trim() || 'изменена';
var discussionText = discussionPage ? 'Обсуждение — на странице [[' + discussionPage + (discussionSection ? '#' + discussionSection : '') + ']]. ' : '';
apiReq({ prop: 'revisions', rvprop: 'user', rvdir: 'newer', titles: pg }, 'query', function (queryResp) {
var page = getFirstQueryPage(queryResp);
if (!page) { cb({ code: 'network', info: 'Network error' }); return; }
if (page.missing !== undefined) { cb({ code: 'missing', info: 'Page missing.' }); return; }
if (!page.revisions || !page.revisions.length) { cb({ code: 'no_revisions', info: 'No revisions.' }); return; }
var rv = page.revisions[0];
if ('anon' in rv || rv.userhidden || !rv.user || rv.user === mwCfg.wgUserName) { cb(null); return; }
apiReq({
title: 'User talk:' + rv.user, section: 'new',
sectiontitle: 'Remover: [[:' + pg + ']]',
summary: opts.summary || makeSummary('уведомление автора'),
text: 'Страница [[:' + pg + ']], созданная вами, ' + actionPhrase + '. ' +
discussionText +
'~~' + '~~<br><small>Это автоматическое уведомление, сгенерированное ' + scriptLink + '.</small>',
assertuser: mwCfg.wgUserName
}, 'edit', function (editResp) { cb(editResp && editResp.error ? editResp.error : null); });
});
}
function notifyAuthorsForPages(pages, notifyOptions, callback) {
var cb = callback || function () {};
var opts = notifyOptions || {};
var list = [];
(pages || []).forEach(function (p) { var t = normTitle(p); if (t && list.indexOf(t) === -1) list.push(t); });
if (!list.length) { cb(); return; }
eachSequential(list, function (pg, next) {
var pageLink = buildQuotedStatusPageLink(pg);
var statusId = logStatus('Отправляется уведомление создателю страницы ' + pageLink + '...', null, { pending: true, trackError: false });
notifyAuthor(pg, opts, function (err) {
logStatus(err ? 'Уведомление создателя страницы ' + pageLink + '.' : 'Создатель страницы ' + pageLink + ' уведомлён.', err,
{ statusId: statusId, trackError: opts.trackError !== false });
next();
});
}, cb);
}
// ─── Подписка на раздел ──────────────────────────────────────────────────
function subscribeToTopic(pageTitle, sectionTitle, callback) {
var cb = callback || function () {};
if (!setSubscribe || !sectionTitle) { cb(); return; }
var statusId = logStatus('Оформляется подписка на раздел...', null, { pending: true, trackError: false });
var targetFrag = normalizeSectionForLink(sectionTitle).toLowerCase();
function finish(err, st) {
if (err) { logStatus('Не удалось подписаться на раздел.', err, { statusId: statusId, trackError: false }); cb(); return; }
logStatus(st === 'subscribed' ? 'Оформлена подписка на раздел.' : 'Раздел для подписки не найден.', null, { statusId: statusId, trackError: false });
cb();
}
function trySubscribe(attemptsLeft) {
apiReq({ page: pageTitle, prop: 'threaditemshtml', excludesignatures: true }, 'discussiontoolspageinfo', function (data) {
var items = (data && data.discussiontoolspageinfo && data.discussiontoolspageinfo.threaditemshtml) || null;
if (!items || !items.length) {
if (attemptsLeft > 0) { setTimeout(function () { trySubscribe(attemptsLeft - 1); }, 2000); return; }
finish(null, 'not_found'); return;
}
var commentname = null;
for (var ti = items.length - 1; ti >= 0; ti--) {
var t = items[ti];
if (t.type === 'heading') {
var htext = (t.headingText || t.html || '').replace(/<[^>]+>/g, '').trim();
if (htext.toLowerCase() === targetFrag || normalizeSectionForLink(htext).replace(/_/g, ' ').toLowerCase() === targetFrag) {
commentname = t.name; break;
}
}
}
if (!commentname) {
if (attemptsLeft > 0) setTimeout(function () { trySubscribe(attemptsLeft - 1); }, 2000);
else finish(null, 'not_found');
return;
}
apiReq({ page: pageTitle, commentname: commentname, subscribe: '1' }, 'discussiontoolssubscribe', function (res) {
finish(res && res.error ? res.error : null, 'subscribed');
});
});
}
setTimeout(function () { trySubscribe(2); }, 1500);
}
// ═══════════════════════════════════════════════════════════════════════════
// UI: модальные окна
// ═══════════════════════════════════════════════════════════════════════════
function syncModalLayout() {
var syncFn = $('#removerModal').data('rmSyncLayout');
if (typeof syncFn === 'function') syncFn();
}
function clearModalLayoutSyncHandlers() {
modalLayoutSyncHandlers = [];
$('#removerModal').removeData('rmSyncLayout');
}
function registerModalLayoutSync(handler) {
if (typeof handler !== 'function') return;
if (modalLayoutSyncHandlers.indexOf(handler) === -1) modalLayoutSyncHandlers.push(handler);
$('#removerModal').data('rmSyncLayout', function () {
modalLayoutSyncHandlers.slice().forEach(function (fn) {
if (typeof fn === 'function') fn();
});
});
}
function registerResizeObserver(observer) {
if (observer) resizeObservers.push(observer);
}
function resetModalObservers() {
resizeObservers.forEach(function (observer) {
if (observer && typeof observer.disconnect === 'function') observer.disconnect();
});
resizeObservers = [];
clearModalLayoutSyncHandlers();
$(window).off('resize.removerModal');
$(window).off('.rmTaResize');
}
function closeModal() {
resetModalObservers();
$(window).off('keydown.remover');
if (isVector22) $('#content').css('min-width', '');
$('#removerModal').remove();
}
function ensureModalStyles() {
if (document.getElementById('removerModalDynamicStyles')) return;
var progH = 'background:' + tk.bgProgH + '!important;border-color:' + tk.bProgH + '!important;color:' + tk.cInv + '!important;';
var neutH = 'background:' + tk.bgN + '!important;border-color:' + tk.bSub + '!important;color:inherit!important;';
var succH = 'background:' + tk.bgSuccH+ '!important;border-color:' + tk.bSuccH + '!important;color:' + tk.cInv + '!important;';
var pillBg = 'linear-gradient(180deg,' + tk.bgBase + ' 0%,' + tk.bgNSub + ' 100%)';
var pillShadow = '0 1px 0 rgba(255,255,255,.08) inset,0 1px 2px rgba(0,0,0,.18)';
var activePillShadow = '0 1px 0 rgba(255,255,255,.14) inset,0 2px 6px rgba(51,102,204,.24)';
var css = [
'#removerModal,#removerModal *{-moz-text-size-adjust:none!important;-webkit-text-size-adjust:100%!important;text-size-adjust:100%!important}',
'#removerModal{color:inherit}',
'#removerModal input::placeholder,#removerModal textarea::placeholder{color:var(--color-subtle,currentColor);opacity:.7}',
'#removerModal input[type="checkbox"],#removerModal input[type="radio"]{appearance:auto;-webkit-appearance:auto;-moz-appearance:auto;accent-color:auto}',
'#removerModal input[type="checkbox"]{outline:none!important;box-shadow:none!important}',
'#removerModal button{transition:background-color .12s ease,border-color .12s ease,color .12s ease,box-shadow .12s ease,filter .12s ease,transform .06s ease}',
'#removerModal .rmToggleBtn{background:' + tk.bgNSub + '!important;border-color:' + tk.bSub + '!important;color:inherit!important}',
'#removerModal .rmToggleBtn.is-active{background:' + tk.bgProg + '!important;border-color:' + tk.bProg + '!important;color:' + tk.cInv + '!important}',
'#removerModal button:not(:disabled):hover{filter:brightness(.97)}',
'#removerModal button:not(:disabled):active{transform:translateY(1px)}',
'#removerModal #removerSubmit:not(:disabled):not(.rmSubmitError):hover,#removerModal .rmToggleBtn.is-active:hover{' + progH + 'filter:none}',
'#removerModal #removerSubmit:not(:disabled):not(.rmSubmitError):active,#removerModal .rmToggleBtn.is-active:active{' + progH + 'filter:brightness(.92)!important}',
'#removerModal #removerSubmit.rmSubmitReady:not(.rmSubmitError){background:' + tk.bgProg + '!important;border-color:' + tk.bProg + '!important;color:' + tk.cInv + '!important;outline:none!important;box-shadow:0 0 0 6px rgba(51,102,204,.13),0 1px 2px rgba(0,0,0,.08)!important}',
'#removerModal #removerSubmit.rmSubmitReady:not(.rmSubmitError):not(:disabled):hover{' + progH + 'box-shadow:0 0 0 7px rgba(51,102,204,.16),0 1px 2px rgba(0,0,0,.1)!important;filter:none}',
'#removerModal #removerSubmit.rmSubmitReady:not(.rmSubmitError):not(:disabled):active{' + progH + 'box-shadow:0 0 0 5px rgba(51,102,204,.14),0 1px 2px rgba(0,0,0,.08)!important;filter:brightness(.92)!important}',
'#removerModal .rmAddPageBtn{background:' + tk.bgProg + '!important;border-color:' + tk.bProg + '!important;color:' + tk.cInv + '!important;font-weight:700!important}',
'#removerModal .rmAddPageBtn:hover{' + progH + 'filter:none!important}',
'#removerModal .rmAddVariantBtn{background:' + tk.bgBase + '!important;border-color:' + tk.bSub + '!important;color:inherit!important;font-weight:700!important}',
'#removerModal .rmAddVariantBtn:hover{' + neutH + 'filter:none!important}',
'#removerModal .rmRenameVariantAddBtn{font-size:15px!important}',
'#removerModal .rmStartMultiPageBtn{align-self:flex-start!important;margin-bottom:0!important}',
'#removerModal .rmRenameVariantRow{width:100%!important;max-width:100%!important;box-sizing:border-box!important}',
'#removerModal .rmMultiRenameVariantsContainer{display:flex;flex-direction:column;gap:6px;box-sizing:border-box;margin-top:6px;width:100%!important;max-width:100%!important}',
'#removerModal .rmMultiRenamePrimaryTargetRow,#removerModal .rmMultiRenameVariantRow{display:flex;margin-bottom:0!important;box-sizing:border-box}',
'#removerModal .rmMultiPageCommentToggle{min-width:32px;height:32px;padding:0!important;font-size:15px!important;line-height:1!important}',
'#removerModal .rmMultiPageCommentToggle.is-active{background:#bfc4ca!important;border-color:#8f98a3!important;color:#202122!important}',
'#removerModal .rmMultiPageCommentToggle.is-active:hover{background:#b4bac1!important;border-color:#848e99!important;color:#202122!important;filter:none}',
'#removerModal .rmMultiPageCommentToggle.is-active:active{background:#a9b0b8!important;border-color:#79838f!important;color:#202122!important;filter:none}',
'#removerModal #removerSubmit.rmSubmitError:not(:disabled):hover{background:#b32424!important;border-color:#b32424!important;color:#fff!important;filter:none}',
'#removerModal #removerSubmit.rmSubmitError:not(:disabled):active{background:#b32424!important;border-color:#b32424!important;color:#fff!important;filter:brightness(.88)!important}',
'#removerModal #removerReload:not(:disabled):hover{' + succH + 'filter:none}',
'#removerModal #removerReload:not(:disabled):active{' + succH + 'filter:brightness(.92)!important}',
'#removerModal button:not(#removerSubmit):not(#removerReload):not(.is-active):not(.rmAddPageBtn):not(.rmAddVariantBtn):hover,#removerModal .rmToggleBtn:not(.is-active):not(.rmAddPageBtn):not(.rmAddVariantBtn):hover{' + neutH + 'filter:none}',
'#removerModal button:not(#removerSubmit):not(#removerReload):not(.is-active):not(.rmAddPageBtn):not(.rmAddVariantBtn):active{' + neutH + 'filter:brightness(.92)!important}',
'#removerModal a.removerModalLink{color:' + tk.cProg + ';text-decoration:none;border-bottom:0;box-shadow:none;word-break:break-word;overflow-wrap:anywhere}',
'#removerModal a.removerModalLink:hover,#removerModal a.removerModalLink:focus{color:' + tk.cProgH + ';text-decoration:underline}',
'#removerModal #removerModalSubtitle{font-size:12px!important;line-height:1.35!important;font-weight:400!important;color:' + tk.cSubM + '!important;max-width:100%;overflow-wrap:anywhere;word-break:break-word}',
'#removerModal #removerModalSubtitle a{font-size:inherit!important;line-height:inherit!important;font-weight:inherit!important}',
'#removerModal a.rmButtonLikeLink{color:' + tk.cBase + '!important;text-decoration:none!important;transform:none!important;transition:background-color .12s ease,border-color .12s ease,color .12s ease,filter .12s ease,transform .06s ease!important}',
'#removerModal a.rmButtonLikeLink:hover{' + neutH + 'text-decoration:none!important;transform:none!important}',
'#removerModal a.rmButtonLikeLink:focus{text-decoration:none!important}',
'#removerModal a.rmButtonLikeLink:focus:not(:focus-visible){outline:none!important}',
'#removerModal a.rmButtonLikeLink:focus-visible{outline:2px solid ' + tk.bProg + '!important;outline-offset:2px;text-decoration:none!important}',
'#removerModal a.rmButtonLikeLink:hover:active{' + neutH + 'filter:brightness(.92)!important;transform:translateY(1px)!important;text-decoration:none!important}',
'#removerModal .rmInfoBox{margin:0 0 10px;padding:8px 10px;border:1px solid ' + tk.bSub + ';border-radius:6px;background:' + tk.bgNSub + '}',
'#removerModal .rmActionList{display:flex;flex-direction:column;gap:6px}',
'#removerModal .rmActionItem{display:block;padding:8px 10px;border:1px solid ' + tk.bSub + ';border-radius:6px;background:' + tk.bgBase + ';cursor:pointer;transition:background-color .12s ease,transform .06s ease}',
'#removerModal .rmActionItem:hover{background:' + tk.bgNSub + '}',
'#removerModal .rmActionItem:active{transform:translateY(1px)}',
'#removerModal .rmActionMain{display:flex;align-items:center}',
'#removerModal .rmActionMain input[type="radio"]{margin-right:8px}',
'#removerModal .rmActionMeta{display:block;margin-left:24px;margin-top:2px;color:' + tk.cSubM + ';font-size:12px;line-height:1.35}',
'#removerModal .rmActionWarning{display:none;margin-left:24px;margin-top:3px;color:' + tk.cDang + ';font-size:12px;line-height:1.35;font-weight:700}',
'#removerModal .rmConflictLead{margin:0 0 10px;color:' + tk.cSubM + ';font-size:13px;line-height:1.5}',
'#removerModal .rmConflictList{display:flex;flex-direction:column;gap:10px}',
'#removerModal .rmConflictCard{padding:12px;border:1px solid ' + tk.bSub + ';border-radius:8px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.55) inset}',
'#removerModal .rmConflictCard.is-skip{background:' + tk.bgNSub + ';border-color:' + tk.bSubS + '}',
'#removerModal .rmConflictTitle{font-size:14px;font-weight:700;line-height:1.4;color:' + tk.cBase + ';word-break:break-word;overflow-wrap:anywhere}',
'#removerModal .rmConflictMeta{margin-top:4px;font-size:12px;line-height:1.45;color:' + tk.cSubM + '}',
'#removerModal .rmConflictGroup{margin-top:10px}',
'#removerModal .rmConflictGroupTitle{margin:0 0 6px;font-size:12px;font-weight:700;line-height:1.35;color:' + tk.cSubM + '}',
'#removerModal .rmConflictButtons{display:flex;flex-wrap:wrap;gap:6px}',
'#removerModal .rmConflictChoice{padding:5px 10px}',
'#removerModal .rmConflictChoice.is-disabled,#removerModal .rmConflictButtons.is-disabled .rmConflictChoice{opacity:.55;cursor:not-allowed;pointer-events:none}',
'#removerModal .rmConflictHint{margin-top:8px;font-size:11px;line-height:1.45;color:' + tk.cSub + '}',
'#removerModal #rmSettingsForm{display:flex;flex-direction:column;gap:14px}',
'#removerModal .rmSettingsLead{margin:-2px 0 2px;color:' + tk.cSubM + ';font-size:13px;line-height:1.5}',
'#removerModal .rmSettingsSection{margin:0;padding:14px 16px;border:1px solid ' + tk.bSubS + ';border-radius:10px;background:linear-gradient(180deg,' + tk.bgBase + ' 0%,' + tk.bgNSub + ' 100%);box-shadow:0 1px 0 rgba(255,255,255,.7) inset}',
'#removerModal .rmSettingsSectionHeader{margin:0 0 12px}',
'#removerModal .rmSettingsSectionTitle{font-size:14px;font-weight:700;line-height:1.35;color:' + tk.cBase + '}',
'#removerModal .rmSettingsSectionDescription{margin-top:4px;font-size:12px;line-height:1.45;color:' + tk.cSubM + '}',
'#removerModal .rmSettingsField{display:block;margin:0 0 10px;padding:12px;border:1px solid ' + tk.bSubS + ';border-radius:8px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.55) inset}',
'#removerModal .rmSettingsField:last-child{margin-bottom:0}',
'#removerModal .rmSettingsFieldLabel{display:block;font-size:13px;font-weight:700;line-height:1.35;margin:0 0 8px;color:' + tk.cBase + '}',
'#removerModal .rmSettingsFieldControl{display:block;min-width:0}',
'#removerModal .rmSettingsFieldControl input{margin-bottom:0!important}',
'#removerModal .rmSettingsFieldControl input[type="text"]{min-height:38px;border-radius:6px}',
'#removerModal .rmSettingsFieldHint{margin-top:8px;font-size:12px;line-height:1.55;color:' + tk.cSubM + ';overflow-wrap:anywhere}',
'#removerModal .rmSettingsChecks{display:flex;flex-direction:column;gap:8px}',
'#removerModal .rmSettingsCheck{display:inline-flex;align-items:flex-start;gap:8px;font-size:14px;line-height:1.45;color:' + tk.cBase + '}',
'#removerModal .rmSettingsCheck input[type="checkbox"]{margin:3px 0 0;flex-shrink:0}',
'#removerModal .rmSettingsMenuPresetWrap{margin-top:10px}',
'#removerModal .rmSettingsMenuPresetLabel{margin:0 0 6px;font-size:12px;font-weight:700;line-height:1.35;color:' + tk.cSubM + '}',
'#removerModal .rmSegmentedBar{display:inline-flex;align-items:center;flex-wrap:wrap;gap:6px;margin-top:0;padding:0;border:0;border-radius:0;background:transparent;max-width:100%;box-sizing:border-box;box-shadow:none}',
'#removerModal .rmSettingsMenuPresetBar{display:inline-flex;align-items:center;flex-wrap:wrap;gap:6px;margin-top:0;padding:0;border:0;border-radius:0;background:transparent;max-width:100%;box-sizing:border-box;box-shadow:none}',
'#removerModal .rmSegmentedBtn,#removerModal .rmSettingsMenuPresetBtn{padding:5px 12px;border:1px solid ' + tk.bSub + ';border-radius:999px;background:' + pillBg + ';color:' + tk.cBase + ';font-size:12px;font-weight:600;line-height:1.35;cursor:pointer;box-shadow:' + pillShadow + '}',
'#removerModal .rmSegmentedBtn.is-active,#removerModal .rmSettingsMenuPresetBtn.is-active{background:' + tk.bgProg + ';border-color:' + tk.bProg + ';color:' + tk.cInv + ';box-shadow:' + activePillShadow + '}',
'#removerModal .rmKbuReasonPill{display:flex;align-items:stretch;width:100%;padding:0!important;overflow:hidden;border:1px solid ' + tk.bSub + ';border-radius:999px;box-sizing:border-box;cursor:pointer;text-align:left;font-size:12px;font-weight:400;line-height:1.3;box-shadow:' + pillShadow + '}',
'#removerModal .rmKbuReasonPill.is-active{box-shadow:' + activePillShadow + '!important}',
'#removerModal .rmKbuReasonNum{display:inline-flex;align-items:center;justify-content:center;min-width:44px;padding:4px 10px;border-right:1px solid ' + tk.bSub + ';box-sizing:border-box;font-weight:700;text-align:center;flex:0 0 auto}',
'#removerModal .rmKbuReasonPill.is-active .rmKbuReasonNum{border-right-color:rgba(255,255,255,.55)!important}',
'#removerModal .rmKbuReasonText{display:block;min-width:0;flex:1;padding:4px 11px 4px 12px;font-weight:400;overflow-wrap:anywhere}',
'#removerModal.rmModalSettings{border:1px solid ' + tk.bSub + '!important;background:' + tk.bgBase + '!important;border-radius:12px!important;box-shadow:0 14px 32px rgba(0,0,0,.08),0 1px 0 rgba(255,255,255,.78) inset!important}',
'#removerModal.rmModalSettings #removerModalHeaderBar{margin-bottom:14px;padding-bottom:10px;border-bottom:2px solid ' + tk.bSub + '}',
'#removerModal.rmModalSettings #removerModalSubtitle{margin:-2px 0 12px!important;color:' + tk.cSubM + '!important}',
'#removerModal.rmModalSettings #rmSettingsForm{gap:18px}',
'#removerModal.rmModalSettings .rmSettingsLead{margin:0;padding:0 2px;color:' + tk.cSubM + '}',
'#removerModal.rmModalSettings .rmSettingsSection{padding:16px 18px;border:1px solid ' + tk.bSub + ';border-radius:12px;background:linear-gradient(180deg,' + tk.bgNSub + ' 0%,' + tk.bgBase + ' 100%);box-shadow:0 1px 0 rgba(255,255,255,.82) inset,0 8px 18px rgba(0,0,0,.035)}',
'#removerModal.rmModalSettings .rmSettingsSectionHeader{margin:0 0 6px;padding-bottom:0;border-bottom:0}',
'#removerModal.rmModalSettings .rmSettingsSectionTitle{font-size:16px;line-height:1.3}',
'#removerModal.rmModalSettings .rmSettingsSectionDescription{margin-top:5px;max-width:none}',
'#removerModal.rmModalSettings .rmSettingsField{margin:14px 0 0;padding:12px 0 0;border:0;border-top:1px solid ' + tk.bSubS + ';border-radius:0;background:transparent;box-shadow:none}',
'#removerModal.rmModalSettings .rmSettingsField:first-child{margin-top:0;padding-top:0;border-top:0}',
'#removerModal.rmModalSettings .rmSettingsFieldLabel{margin:0 0 6px}',
'#removerModal.rmModalSettings .rmSettingsFieldControl input[type="text"]{min-height:40px;padding:8px 10px;border:1px solid ' + tk.bSub + ';border-radius:8px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.65) inset}',
'#removerModal.rmModalSettings .rmSettingsFieldControl input[type="text"]:focus{border-color:' + tk.bProg + ';box-shadow:0 0 0 1px rgba(51,102,204,.16);outline:none}',
'#removerModal.rmModalSettings .rmSettingsFieldHint{margin-top:6px;max-width:none}',
'#removerModal.rmModalSettings .rmSettingsChecks{gap:10px}',
'#removerModal.rmModalSettings .rmSettingsCheck{padding:4px 0}',
'#removerModal.rmModalSettings .rmSettingsMenuPresetWrap{margin-top:12px;padding-top:10px;border-top:1px dashed ' + tk.bSubS + '}',
'#removerModal.rmModalSettings .rmSettingsHintList{width:100%;max-width:100%;box-sizing:border-box;margin-top:10px;padding:10px 12px;border:1px solid ' + tk.bSubS + ';border-radius:8px;background:' + tk.bgBase + '}',
'#removerModal.rmModalSettings .rmSettingsHintRow{line-height:1.55}',
'#removerModal.rmModalSettings .rmSettingsHintBadge{background:' + tk.bgNSub + '}',
'#removerModal.rmModalSettings .rmQuickPhraseEditor{padding-top:2px}',
'#removerModal.rmModalSettings .rmQuickPhraseMeta{min-height:18px}',
'#removerModal.rmModalSettings #rmSettingsSignaturePreviewCode{display:inline-block;padding:2px 8px;border:1px solid ' + tk.bSubS + ';border-radius:999px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.65) inset}',
'#removerModal.rmModalSettings #rmFooterActionButtons{position:relative;flex:0 0 auto!important;display:flex!important;align-items:center!important;justify-content:flex-end!important;gap:6px!important;margin-left:auto!important;max-width:100%!important}',
'#removerModal.rmModalSettings #rmSettingsActionButtonsRow{display:flex;align-items:center;justify-content:flex-end;gap:6px;flex-wrap:nowrap}',
'#removerModal.rmModalSettings #rmSettingsUnsavedHint{display:none;position:absolute;top:100%;right:0;width:auto;box-sizing:border-box;margin:4px 0 0;color:' + tk.cSubM + ';opacity:.78;font-size:12px;line-height:1.35;text-align:right;white-space:nowrap}',
'#removerModal .rmProtectControlGroup{margin-top:12px;padding:8px 10px;border:1px solid ' + tk.bSubS + ';border-radius:10px;background:linear-gradient(180deg,' + tk.bgBase + ' 0%,' + tk.bgNSub + ' 100%);box-sizing:border-box}',
'#removerModal .rmProtectControlLabel{margin:0 0 6px;font-size:12px;font-weight:700;line-height:1.35;color:' + tk.cSubM + '}',
'#removerModal .rmProtectControlGroup .rmSegmentedBar{gap:4px;padding:0;border:0;box-shadow:none}',
'#removerModal .rmProtectControlGroup .rmSegmentedBtn{padding:4px 10px;font-size:12px}',
'#removerModal .rmTransferPanel{margin-top:10px;padding:0;border:0;background:transparent;box-sizing:border-box;box-shadow:none}',
'#removerModal .rmTransferGrid{display:grid;grid-template-columns:max-content max-content;column-gap:10px;row-gap:6px;align-items:start;justify-content:start}',
'#removerModal .rmTransferGrid .rmSegmentedBar{justify-self:start}',
'#removerModal .rmTransferHintRow{grid-column:1 / -1;min-height:0}',
'#removerModal .rmTransferPanel .rmSegmentedBar{gap:4px;padding:0;border:0;box-shadow:none}',
'#removerModal .rmTransferPanel .rmSegmentedBtn{padding:6px 14px;font-size:12px;font-weight:700}',
'#removerModal #rmTransferModeGroup{gap:0}',
'#removerModal #rmTransferModeGroup .rmSegmentedBtn:first-child{border-top-right-radius:0;border-bottom-right-radius:0}',
'#removerModal #rmTransferModeGroup .rmSegmentedBtn + .rmSegmentedBtn{margin-left:-1px;border-top-left-radius:0;border-bottom-left-radius:0}',
'#removerModal.rmCompactContent .rmMultiPageRow{flex-wrap:wrap!important;gap:6px}',
'#removerModal.rmCompactContent .rmMultiPageRow .rmMultiPageInput,#removerModal.rmCompactContent .rmMultiPageRow .rmMultiPageTargetInput{flex:1 1 100%!important;width:100%!important}',
'#removerModal.rmCompactContent .rmMultiPageRow .rmMultiPageCommentToggle,#removerModal.rmCompactContent .rmMultiPageRow .rmAddMultiPage,#removerModal.rmCompactContent .rmMultiPageRow .rmAddMultiRenameVariant,#removerModal.rmCompactContent .rmMultiPageRow .rmRemoveInput{margin-left:0!important}',
'#removerModal.rmCompactContent .rmTransferGrid{grid-template-columns:minmax(0,1fr);gap:10px}',
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(1){order:1}',
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(2){order:2}',
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(3){order:3}',
'#removerModal.rmCompactContent #rmTransferModeGroup{flex-direction:column;align-items:flex-start;gap:6px}',
'#removerModal.rmCompactContent #rmTransferModeGroup .rmSegmentedBtn{margin-left:0!important;border-radius:999px!important}',
'#removerModal.rmCompactContent .rmTransferHintRow{grid-column:auto}',
'#removerModal #rmProtectTextBlock{margin-top:14px}',
'#removerModal #rmSettingsMenuTitle:disabled{background:' + tk.bgDis + '!important;border-color:' + tk.bDis + '!important;color:' + tk.cDis + '!important;-webkit-text-fill-color:' + tk.cDis + ';opacity:1;cursor:not-allowed;box-shadow:none!important}',
'#removerModal .rmSettingsHintList{display:flex;flex-direction:column;gap:4px;margin-top:8px}',
'#removerModal .rmSettingsHintRow{font-size:12px;line-height:1.5;color:' + tk.cSubM + '}',
'#removerModal .rmSettingsHintBadge{display:inline-block;margin-right:6px;padding:1px 6px;border:1px solid ' + tk.bSubS + ';border-radius:999px;background:' + tk.bgBase + ';font-size:11px;font-weight:700;color:' + tk.cSubM + ';vertical-align:baseline}',
'#removerModal .rmQuickPhraseEditor{display:flex;flex-direction:column;gap:10px}',
'#removerModal .rmQuickPhraseList,#removerModal .rmQuickPhrasesPanel{display:flex;flex-wrap:wrap;gap:8px;align-items:flex-start}',
'#removerModal .rmQuickPhraseChip{position:relative;display:inline-flex;align-items:center;gap:4px;max-width:100%;padding:2px 4px 2px 10px;border:1px solid ' + tk.bSub + ';border-radius:999px;background:' + pillBg + ';box-shadow:' + pillShadow + ';transition:border-color .12s,box-shadow .12s,opacity .12s;overflow:visible}',
'#removerModal .rmQuickPhraseChip.is-editing{opacity:.42;border-style:dashed}',
'#removerModal .rmQuickPhraseChip.is-dragging{opacity:.65}',
'#removerModal .rmQuickPhraseChip.is-drop-before::before,#removerModal .rmQuickPhraseChip.is-drop-after::after{content:"";position:absolute;top:50%;width:3px;height:24px;border-radius:999px;background:' + tk.cProg + ';box-shadow:0 0 0 1px rgba(51,102,204,.08)}',
'#removerModal .rmQuickPhraseChip.is-drop-before::before{left:-4px;transform:translate(-50%,-50%)}',
'#removerModal .rmQuickPhraseChip.is-drop-after::after{right:-4px;transform:translate(50%,-50%)}',
'#removerModal .rmQuickPhraseEditBtn{max-width:100%;padding:3px 0;border:0;background:transparent;color:' + tk.cBase + ';font-size:13px;line-height:1.35;cursor:pointer;text-align:left;white-space:normal}',
'#removerModal .rmQuickPhraseRemoveBtn{width:24px;height:24px;padding:0;border:0;border-radius:999px;background:transparent;color:' + tk.cSubM + ';font-size:18px;line-height:1;cursor:pointer;flex-shrink:0}',
'#removerModal .rmQuickPhraseRemoveBtn:hover{background:' + tk.bgN + ';color:' + tk.cBase + '}',
'#removerModal #rmSettingsQuickPhraseInput.is-editing{border-color:' + tk.bProg + ';box-shadow:0 0 0 1px rgba(51,102,204,.12)}',
'#removerModal .rmQuickPhraseMeta{font-size:12px;line-height:1.45;color:' + tk.cSubM + '}',
'#removerModal .rmQuickPhraseEmpty{padding:2px 0;font-size:12px;line-height:1.45;color:' + tk.cSubM + '}',
'#removerModal .rmQuickPhrasesPanel{margin-top:8px}',
'#removerModal .rmQuickPhraseActionBtn{padding:5px 12px;border:1px solid ' + tk.bSub + ';border-radius:999px;background:' + pillBg + ';color:' + tk.cBase + ';font-size:12px;font-weight:600;line-height:1.35;cursor:pointer;box-shadow:' + pillShadow + '}',
'#removerModal .rmQuickPhraseActionBtn:hover{border-color:' + tk.bProg + ';color:' + tk.cProg + '}',
'@media (max-width:' + sz.mobileBp + 'px){',
'#removerModal button{white-space:normal!important}',
'#removerModal #rmFooterButtons{align-items:flex-start!important}',
'#removerModal #rmFooterCheckboxes,#removerModal #rmFooterActionButtons{width:100%!important;max-width:100%!important;margin-left:0!important}',
'#removerModal .rmSettingsSection{padding:12px 13px}',
'#removerModal .rmSettingsField{padding:10px}',
'#removerModal.rmModalSettings #rmSettingsUnsavedHint{max-width:100%;margin:4px 0 0;text-align:right;white-space:normal}',
'#removerModal .rmTransferPanel{padding:0}',
'#removerModal .rmTransferGrid{grid-template-columns:minmax(0,1fr);gap:10px}',
'#removerModal .rmTransferHintRow{grid-column:auto}',
'#removerModal .rmQuickPhraseChip{max-width:100%}',
'}'
].join('');
var style = document.createElement('style');
style.id = 'removerModalDynamicStyles';
style.textContent = css;
document.head.appendChild(style);
}
function applyV2022Layout($modal, explicitWidth) {
if (!isVector22) return;
var css = { 'max-width': '100%', 'box-sizing': 'border-box', 'overflow-wrap': 'anywhere' };
if (typeof explicitWidth === 'number') css['max-width'] = explicitWidth + 'px';
$modal.css(css);
}
function getPageUrl(pageTitle) {
return (mw.util && typeof mw.util.getUrl === 'function')
? mw.util.getUrl(pageTitle)
: '/wiki/' + encodeURIComponent((pageTitle || '').replace(/ /g, '_'));
}
function getPageUrlWithFragment(pageTitle, fragment) {
var url = getPageUrl(pageTitle);
var frag = normalizeSectionForLink(fragment);
if (frag) url += '#' + ((mw.util && mw.util.escapeIdForLink) ? mw.util.escapeIdForLink(frag) : frag.replace(/\s+/g, '_'));
return url;
}
function buildStatusPageLink(pageName) {
var title = normTitle(pageName);
return '<a href="' + escapeHtml(getPageUrl(title)) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' +
escapeHtml(title) + '</a>';
}
function buildQuotedStatusPageLink(pageName) {
return '«' + buildStatusPageLink(pageName) + '»';
}
function buildTemplatePageLink(templateName, labelText) {
var name = String(templateName || '')
.replace(/^(?:safe)?subst\s*:\s*/i, '')
.replace(RE_TEMPLATE_NS, '')
.trim();
var ns = getNamespaceLabel(10, 'Шаблон');
if (!name) return escapeHtml(labelText);
return '<a href="' + escapeHtml(getPageUrl(ns + ':' + name)) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' +
escapeHtml(labelText) + '</a>';
}
function renderTemplateLinksInText(text) {
var source = String(text || '');
var out = '';
var lastIndex = 0;
source.replace(/\{\{\s*([^{}|]+)(?:\|[^{}]*)?\}\}/g, function (match, templateName, offset) {
out += escapeHtml(source.slice(lastIndex, offset));
out += buildTemplatePageLink(templateName, match);
lastIndex = offset + match.length;
return match;
});
return out + escapeHtml(source.slice(lastIndex));
}
function buildHeaderIconButtonHtml(id, title, label, text) {
return joinHtml([
'<button id="', id, '" type="button" title="', escapeHtml(title), '" aria-label="', escapeHtml(label || title), '" ',
'style="', stHeaderIconBtn, '">', text || '', '</button>'
]);
}
function createModal(opts) {
if (typeof opts === 'string') opts = { title: opts };
var layout = getModalLayout();
resetModalObservers();
ensureModalStyles();
if (isVector22) $('#content').css('min-width', '');
$('#removerModal').remove();
var subtitleHtml = '';
var subtitleStyle = 'margin:-4px 0 8px;font-size:12px!important;color:' + tk.cSubM + ';line-height:1.35!important;font-weight:400!important;';
var subtitleLinkStyle = 'font-size:inherit!important;line-height:inherit!important;font-weight:inherit!important;';
if (opts.subtitleHtml) {
subtitleHtml = joinHtml([
'<div id="removerModalSubtitle" style="', subtitleStyle, '">',
opts.subtitleHtml,
'</div>'
]);
} else if (opts.subtitlePage) {
subtitleHtml = joinHtml([
'<div id="removerModalSubtitle" style="', subtitleStyle, '">',
opts.subtitleLabel || 'Текущий день',
': <a href="', getPageUrl(opts.subtitlePage), '" target="_blank" rel="noopener noreferrer" class="removerModalLink" style="', subtitleLinkStyle, '">',
normTitle(opts.subtitlePage),
'</a></div>'
]);
}
var settingsButtonHtml = opts.showSettingsButton === false ? '' :
buildHeaderIconButtonHtml('removerSettingsTrigger', 'Конфигурация', 'Конфигурация', '⚙');
var display = opts.inline ? 'inline-block' : 'block';
var modalMargin = opts.inline ? '1em 0' : (layout.shouldCenter ? '1em auto' : '1em 0');
var inlineLayoutStyle = opts.inline ? ';justify-self:start;align-self:start;width:fit-content;' : '';
var modalStyle = joinHtml([
'position:relative;padding:1.5em;margin:', modalMargin, ';display:', display, ';',
'border:', stStyles.border, ';background:', stStyles.background,
';border-radius:', stStyles.borderRadius, ';box-shadow:', stStyles.boxShadow,
';max-width:100%;box-sizing:border-box;overflow-wrap:anywhere;', inlineLayoutStyle
]);
var headerStyle = 'display:flex;align-items:center;gap:10px;margin-bottom:10px;padding-bottom:6px;border-bottom:1px solid ' + tk.bSubS + ';';
var titleStyle = 'color:' + stStyles.headerColor + ';margin:0;padding:0;border:0;display:block;font-size:1.3em;font-weight:400;line-height:1.25;flex:1 1 auto;min-width:0;';
$('#content').prepend(joinHtml([
'<div id="removerModal" style="', modalStyle, '">',
'<div id="removerModalHeaderBar" style="', headerStyle, '">',
'<h1 id="removerModalTitle" style="', titleStyle, '"><span id="removerModalTitleText">', opts.title, '</span></h1>',
settingsButtonHtml,
'</div>',
subtitleHtml,
'<div id="removerModalContent"></div>',
'<div id="removerModalFooter" style="margin-top:15px;"></div>',
'</div>'
]));
var $modal = $('#removerModal');
if (opts.width === 'compact') $modal.css({ width: layout.defaultOuterWidth + 'px', 'max-width': '100%', 'box-sizing': 'border-box' });
else applyV2022Layout($modal);
$('#removerSettingsTrigger').off('click').on('click', function () {
openSettings();
});
}
function buildFooterCheckboxHtml(name, checked, label) {
return joinHtml([
'<label style="', stFooterCheckLabel, '">',
'<input name="', name, '" type="checkbox" style="margin:2px 0 0;flex-shrink:0;" ',
checked ? 'checked' : '',
'>',
label,
'</label>'
]);
}
function buildFooterActionsHtml(buttonsHtml) {
return '<div id="rmFooterActionButtons" style="' + stFooterActions + '">' + buttonsHtml + '</div>';
}
function renderModalFooter(mode, options) {
var opts = options || {};
$('#removerModalFooter').css('width', '');
if (mode === 'submit') {
var showCb = opts.showCheckbox !== false;
var showSub = opts.showSubscribe === true;
var ns = mwCfg.wgNamespaceNumber;
var notifyLabel = ns === 0 ? 'Оповестить создателя статьи'
: (ns === 10 || ns === 11) ? 'Оповестить создателя шаблона'
: (ns === 14 || ns === 15) ? 'Оповестить создателя категории'
: 'Оповестить создателя страницы';
var cbInlineHtml = '';
if (showSub || showCb) {
cbInlineHtml = joinHtml([
'<div id="rmFooterCheckboxes" style="', stFooterChecks, '">',
showSub ? buildFooterCheckboxHtml('rmSubscribe', setSubscribe, 'Подписаться на номинацию') : '',
showCb ? buildFooterCheckboxHtml('rmUAlert', setAlert, notifyLabel) : '',
'</div>'
]);
}
$('#removerModalFooter').html(joinHtml([
'<div id="rmFooterButtons" style="', stFooterWrap, 'justify-content:', cbInlineHtml ? 'space-between' : 'flex-end', ';">',
cbInlineHtml,
buildFooterActionsHtml(joinHtml([
'<button id="removerCancel" style="', stCancel, '">Отмена</button>',
'<button id="removerSubmit" style="', stSubmit, '">', opts.submitText || 'ОК', '</button>'
])),
'</div>'
]));
$('#removerCancel').click(function () { closeModal(); });
$('#removerSubmit').data('rmSubmitInProgress', false).click(function () {
if ($(this).data('rmSubmitInProgress')) return;
$(this).removeClass('rmSubmitError').css({ background: '', 'border-color': '', color: '' });
isError = false;
if (!opts.preserveLogOnSubmit) {
$('#rmLogBox').empty();
logStatusSeq = 0;
}
if (showCb) { setAlert = $('[name="rmUAlert"]').is(':checked'); state.setAlert = setAlert; }
if (showSub) { setSubscribe = $('[name="rmSubscribe"]').is(':checked'); state.setSubscribe = setSubscribe; }
$(this).data('rmSubmitInProgress', true).prop('disabled', true);
var submitResult;
try { submitResult = opts.onSubmit(); } catch (ex) { unlockModalSubmit(); throw ex; }
if (submitResult === false) unlockModalSubmit();
});
$(window).off('keydown.remover').on('keydown.remover', function (e) {
if (e.ctrlKey && e.keyCode === 13) $('#removerSubmit').click();
});
} else if (mode === 'reload') {
var newBtns = buildFooterActionsHtml(joinHtml([
'<button id="removerCancel" style="', stCancel, '">Закрыть</button>',
'<button id="removerReload" style="', stReload, '">', opts.reloadText || 'Обновить страницу', '</button>'
]));
$('#rmFooterCheckboxes').remove();
var $btns = $('#rmFooterButtons');
if ($btns.length) {
$btns.css({ 'justify-content': 'flex-end' }).html(newBtns);
} else {
$('#removerModalFooter').append(joinHtml([
'<div id="rmFooterButtons" style="', stFooterWrap, 'justify-content:flex-end;">',
newBtns,
'</div>'
]));
}
$('#removerCancel').click(function () { closeModal(); });
$('#removerReload').click(function () { location.reload(); });
$(window).off('keydown.remover').on('keydown.remover', function (e) {
if (e.keyCode === 27) $('#removerCancel').click();
if (e.ctrlKey && e.keyCode === 13) $('#removerReload').click();
});
if (shouldAutoReloadAfterAction()) setTimeout(function () { $('#removerReload').trigger('click'); }, 0);
} else { // 'close'
$('#removerModalFooter').html(joinHtml([
'<div style="display:flex;justify-content:flex-end;align-items:center;">',
'<button id="removerCancel" style="', stCancel, 'margin-right:0;">', opts.closeText || 'Закрыть', '</button>',
'</div>'
]));
$('#removerCancel').click(function () { closeModal(); });
$(window).off('keydown.remover').on('keydown.remover', function (e) {
if (e.keyCode === 27) $('#removerCancel').click();
});
}
}
function unlockModalSubmit() {
$('#removerSubmit').data('rmSubmitInProgress', false).prop('disabled', false);
}
function markSubmitError() {
isError = true;
var errColor = '#d73333';
$('#removerSubmit').data('rmSubmitInProgress', false).prop('disabled', false)
.addClass('rmSubmitError').css({ background: errColor, 'border-color': errColor, color: '#fff' });
}
// ─── UI: статус и ссылки ─────────────────────────────────────────────────
function startProcessing() {
if ($('#rmLogBox').length) return;
$('#removerModal').append(
'<div id="rmLogBox" style="margin-top:12px;padding-top:10px;border-top:1px solid ' + tk.bSubS + ';line-height:1.5;overflow-wrap:anywhere;word-break:break-word;box-sizing:border-box;"></div>'
);
syncLinkWidths();
}
function logStatus(message, error, opts) {
var o = opts || {};
if (o.trackError !== false && error && error.code) isError = true;
var $box = $('#rmLogBox');
if (!$box.length) { startProcessing(); $box = $('#rmLogBox'); }
var statusId = o.statusId || ('rm-status-' + (++logStatusSeq));
var $row = $box.find('[data-rm-status-id="' + statusId + '"]');
if (!$row.length) {
$row = $('<div data-rm-status-id="' + statusId + '" style="margin-top:4px;line-height:1.4;"></div>');
$box.append($row);
}
var html;
if (error) {
var errText = error.code
? '<span class="error"><small>' + escapeHtml(formatLogErrorCode(error.code)) + ': ' + escapeHtml(String(error.info || '')) + '</small></span>'
: escapeHtml(String(error));
html = '<span style="color:' + tk.cDang + ';margin-right:4px;">✕</span>' + message + ' — ' + errText;
} else if (o.pending) {
html = '<span style="color:' + tk.cSubM + ';">' + message + '</span>';
} else {
html = '<span style="color:' + tk.bgSucc + ';margin-right:4px;">✓</span><span style="color:' + tk.cSubM + ';">' + message + '</span>';
}
$row.html(html);
return statusId;
}
function formatLogErrorCode(code) {
var value = String(code || '');
return value.toLowerCase() === 'error' ? 'Ошибка' : value;
}
function logPageEdit(pageName, error, opts) {
logStatus('Правка страницы ' + buildQuotedStatusPageLink(pageName) + '.', error, opts);
}
function syncLinkWidths() {
var $box = $('#rmLogBox');
if (!$box.length) return;
var taW = $('#rmMsg,#nominationReason,#rmReportText').first().outerWidth() || 0;
$box.css({ width: taW ? taW + 'px' : '', 'max-width': '100%' });
}
function appendNominationLink(pageTitle, sectionTitle) {
if (!pageTitle) return;
var url = getPageUrl(pageTitle);
var frag = normalizeSectionForLink(sectionTitle);
if (frag) url += '#' + ((mw.util && mw.util.escapeIdForLink) ? mw.util.escapeIdForLink(frag) : frag.replace(/\s+/g, '_'));
var label = normTitle(frag ? pageTitle + '#' + frag : pageTitle);
var $target = $('#rmLogBox').length ? $('#rmLogBox') : $('#removerModalContent');
$target.append(
'<div style="margin-top:4px;line-height:1.4;word-break:break-word;overflow-wrap:anywhere;display:flex;align-items:baseline;gap:5px;">' +
'<span style="color:' + tk.bgSucc + ';font-size:14px;flex-shrink:0;">✓</span>' +
'<a href="' + escapeHtml(url) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' + escapeHtml(label) + '</a></div>'
);
}
function getCurrentSettings() {
return normalizeRemoverSettings(state.settings || settingsDefaults);
}
function shouldAutoReloadAfterAction() {
return !!getCurrentSettings().autoReloadAfterAction;
}
function maybeOpenNominationDiscussionInNewTab(nominationInfo) {
if (!getCurrentSettings().openNominationDiscussionInNewTab || !nominationInfo || !nominationInfo.pageTitle) return;
window.open(getPageUrlWithFragment(nominationInfo.pageTitle, nominationInfo.sectionTitle), '_blank', 'noopener,noreferrer');
}
// ─── UI-строители ────────────────────────────────────────────────────────
function buildInfoBoxHtml(mainText, detailsText, isErr) {
var cls = isErr ? ' class="error"' : '';
return joinHtml([
'<div class="rmInfoBox">',
'<p', cls, ' style="margin:0', detailsText ? ' 0 6px' : '', ';">', mainText, '</p>',
detailsText ? '<p style="margin:0;color:' + tk.cSubM + ';">' + detailsText + '</p>' : '',
'</div>'
]);
}
function buildActionsHtml(actions, inputName, listId) {
var actionItemsHtml = actions.map(function (a, i) {
var meta = a.description || (a.talkNotice ? 'С добавлением {{' + (a.talkTemplate || a.resultTemplate || 'шаблон') + '}} на СО.' : '');
var tagHtml = a.tag
? '<span style="display:inline-block;font-size:13px;font-weight:600;padding:2px 7px;border-radius:3px;background:' + tk.bgN + ';color:' + tk.cSubM + ';margin-right:8px;white-space:nowrap;vertical-align:middle;">' + escapeHtml(a.tag) + '</span>'
: '';
return joinHtml([
'<label class="rmActionItem">',
'<span class="rmActionMain">',
'<input type="radio" name="', inputName, '" value="', a.id, '" ', i === 0 ? 'checked' : '', '>',
tagHtml,
'<span>', a.label, '</span>',
'</span>',
meta ? '<span class="rmActionMeta">' + renderTemplateLinksInText(meta) + '</span>' : '',
a.warning ? '<strong class="rmActionWarning">' + escapeHtml(a.warning) + '</strong>' : '',
'</label>'
]);
}).join('');
return joinHtml([
'<div style="margin:0 0 8px;color:', tk.cSubM, ';font-size:13px;">Обнаружены открытые номинации:</div>',
'<div', listId ? ' id="' + listId + '"' : '', ' class="rmActionList">',
actionItemsHtml,
'</div>'
]);
}
function syncActionWarnings(inputName, listId) {
var $root = listId ? $('#' + listId) : $('#removerModal');
var $selected = $root.find('input[name="' + inputName + '"]:checked').closest('.rmActionItem');
$root.find('.rmActionWarning').hide();
$selected.find('.rmActionWarning').show();
}
function buildNestedCommentFieldsHtml(opts) {
var options = opts || {};
var wrapId = options.wrapId || '';
var textareaId = options.textareaId || '';
var textareaClass = options.textareaClass ? ' ' + options.textareaClass : '';
var textareaStyleExtra = options.textareaStyleExtra || '';
var wrapStyleExtra = options.wrapStyleExtra || '';
var placeholder = options.placeholder || 'Комментарий (необязательно)';
var beforeHtml = options.beforeHtml || '';
var marginTop = options.marginTop || '6px';
var minHeight = parseInt(options.minHeight, 10) || 90;
var isEmbedded = !!options.embedded;
var wrapClass = isEmbedded ? '' : (' class="' + RESIZE_CLASS + '"');
var wrapStyle = 'display:none;margin-top:' + marginTop + ';max-width:100%;box-sizing:border-box;';
if (isEmbedded) {
wrapStyle += 'padding:0;border:0;background:transparent;';
} else {
wrapStyle += 'padding:8px 10px;border:1px solid ' + tk.bSubS + ';border-radius:6px;background:' + tk.bgNSub + ';';
}
wrapStyle += wrapStyleExtra;
return joinHtml([
'<div id="', wrapId, '"', wrapClass, ' style="', wrapStyle, '">',
beforeHtml,
'<textarea id="', textareaId, '" class="rmNestedCommentInput', textareaClass,
'" placeholder="', escapeHtml(placeholder), '" style="', stInputFull,
'min-height:', minHeight, 'px;resize:both;margin-bottom:6px;', textareaStyleExtra, '"></textarea>',
buildQuickPhrasesPanelHtml(textareaId),
'</div>'
]);
}
function buildConditionalRetFieldsHtml() {
return buildNestedCommentFieldsHtml({
wrapId: 'rmCloseConditionalWrap',
textareaId: 'rmCloseConditionalReason',
placeholder: 'Условие / пояснение (необязательно)',
marginTop: '8px',
minHeight: 90,
beforeHtml: '<input id="rmCloseConditionalDeadline" type="text" placeholder="Срок доработки: 2026-05-31" style="' + stInputFull + 'margin-bottom:6px;">'
});
}
function buildAddMultiPageButtonHtml(options) {
var opts = options || {};
var title = opts.addTitle || 'Мультиноминация: добавить страницу';
return buildSquareAddButtonHtml('', title, 'rmAddMultiPage rmAddPageBtn');
}
function buildSquareAddButtonHtml(id, title, className, symbol) {
var idAttr = id ? ' id="' + id + '"' : '';
var clsAttr = className ? ' class="' + className + '"' : '';
var label = title || 'Добавить';
return '<button' + idAttr + ' type="button"' + clsAttr + ' title="' + escapeHtml(label) + '" aria-label="' + escapeHtml(label) + '" style="' + stRemoveBtn + '">' + escapeHtml(symbol || '+') + '</button>';
}
function leftControlStyle(style) {
return style.replace('margin-left:' + inlineControlGap + 'px;', 'margin-left:0;margin-right:' + inlineControlGap + 'px;');
}
function buildLeftSquareAddButtonHtml(id, title, className, symbol) {
return buildSquareAddButtonHtml(id, title, className, symbol).replace(stRemoveBtn, leftControlStyle(stRemoveBtn));
}
function buildLeftRemoveButtonHtml(className, title) {
var cls = className || 'rmRemoveInput';
var label = title || 'Удалить';
return '<button type="button" class="' + cls + '" style="' + leftControlStyle(stRemoveBtn) + '" title="' + escapeHtml(label) + '" aria-label="' + escapeHtml(label) + '">−</button>';
}
function buildMultiRenameVariantAddButtonHtml() {
return buildLeftSquareAddButtonHtml('', 'Добавить вариант нового заголовка', 'rmAddMultiRenameVariant rmAddVariantBtn rmRenameVariantAddBtn', '⤷');
}
function buildStartMultiPageButtonHtml(title) {
var label = title || 'Мультиноминация: добавить страницу';
return buildSquareAddButtonHtml('', label, 'rmAddMultiPage rmAddPageBtn rmStartMultiPageBtn');
}
function buildMultiPageButtonsHtml(commentWrapId, commentId, options) {
var opts = options || {};
var commentBtnStyle = stToolBtn + (opts.showComment ? '' : 'display:none;');
var commentTitle = opts.commentTitle || 'Добавить комментарий к этой странице';
var commentExpandedTitle = opts.commentExpandedTitle || 'Скрыть комментарий к этой странице';
if (opts.showAdd) return buildAddMultiPageButtonHtml(opts);
return joinHtml([
'<button type="button" class="rmToggleBtn rmMultiPageCommentToggle" data-rm-comment-wrap="', commentWrapId,
'" data-rm-comment-textarea="', commentId,
'" data-rm-comment-title="', escapeHtml(commentTitle),
'" data-rm-comment-expanded-title="', escapeHtml(commentExpandedTitle),
'" aria-label="', escapeHtml(commentTitle), '" title="', escapeHtml(commentTitle), '" aria-expanded="false" style="', commentBtnStyle, '">✎</button>',
'<button type="button" class="rmRemoveInput" style="', stRemoveBtn, '" title="', escapeHtml(opts.removeTitle || 'Убрать страницу из номинации'), '">−</button>'
]);
}
function buildMultiRenameVariantRowHtml(value, options) {
var opts = options || {};
return joinHtml([
'<div class="rmMultiRenameVariantRow" style="', stRow.replace('margin-bottom:6px;', 'margin-bottom:0;'), '">',
buildLeftRemoveButtonHtml('rmRemoveMultiRenameVariant', 'Убрать вариант нового заголовка'),
'<input type="text" class="rmMultiRenameVariantInput" placeholder="', escapeHtml(opts.placeholder || 'Дополнительный вариант нового заголовка'),
'" style="', stInputBox, '"', value ? ' value="' + escapeHtml(value) + '"' : '', '>',
'</div>'
]);
}
function buildMultiPageRowHtml(index, options) {
var opts = options || {};
var pageInputId = 'rmMultiPage' + index;
var commentWrapId = 'rmMultiPageCommentWrap' + index;
var commentId = 'rmMultiPageComment' + index;
var pageValue = opts.pageValue || '';
var pageValueAttr = pageValue ? ' value="' + escapeHtml(pageValue) + '"' : '';
var inputPlaceholder = opts.inputPlaceholder || 'Страница';
var targetInputClass = opts.targetInputClass || '';
var targetInputHtml = '';
var commentPlaceholder = opts.commentPlaceholder || 'Комментарий только для этой страницы (необязательно)';
var commentIndent = opts.targetVariants ? leftNestedControlOffset : '0';
var pageRowStyle = stRow.replace('margin-bottom:6px;', 'margin-bottom:0;');
var blockStyle = 'max-width:100%;box-sizing:border-box;';
var buttonsHtml = buildMultiPageButtonsHtml(commentWrapId, commentId, {
showAdd: !!opts.showAdd,
showComment: !!opts.showComment,
addTitle: opts.addTitle,
removeTitle: opts.removeTitle,
commentTitle: opts.commentTitle,
commentExpandedTitle: opts.commentExpandedTitle
});
if (opts.targetInput) {
targetInputHtml = joinHtml([
'<input id="rmMultiPageTarget', index, '" type="text" placeholder="', escapeHtml(opts.targetPlaceholder || 'Новое название'),
'" class="rmMultiPageTargetInput ', escapeHtml(targetInputClass), '" style="', stInputBox, '">'
]);
}
return joinHtml([
'<div class="rmMultiPageBlock ', RESIZE_CLASS, '" style="', blockStyle, '">',
'<div', opts.rowId ? ' id="' + opts.rowId + '"' : '', ' class="rmMultiPageRow" style="', pageRowStyle, '">',
'<input id="', pageInputId, '" type="text" placeholder="', escapeHtml(inputPlaceholder), '" class="rmMultiPageInput" style="', stInputBox, '"', pageValueAttr, '>',
opts.targetVariants ? '' : targetInputHtml,
buttonsHtml,
'</div>',
opts.targetVariants ? '<div class="rmMultiRenameVariantsContainer"><div class="rmMultiRenamePrimaryTargetRow">' + buildMultiRenameVariantAddButtonHtml() + targetInputHtml + '</div></div>' : '',
buildNestedCommentFieldsHtml({
wrapId: commentWrapId,
textareaId: commentId,
textareaClass: 'rmMultiPageCommentInput',
placeholder: commentPlaceholder,
marginTop: multiNominationGap,
minHeight: 90,
embedded: true,
wrapStyleExtra: 'padding:0 0 0 ' + commentIndent + ';background:transparent;',
textareaStyleExtra: 'border-color:' + tk.bSubS + ';border-radius:4px;background:' + tk.bgBase + ';'
}),
'</div>'
]);
}
function buildSingleRenameBlockHtml(config, startMultiTitle, currentPageName, currentPlaceholder) {
var addLabel = 'Добавить вариант нового заголовка';
var currentValue = currentPageName || '';
return joinHtml([
'<div id="rmSingleRenameBlock" style="display:flex;flex-direction:column;align-items:stretch;gap:0;">',
'<div class="rmInputRow ', RESIZE_CLASS, '" style="', stRow, '">',
'<input id="rmSingleRenameCurrent" type="text" class="rmSingleRenameCurrentInput" placeholder="', escapeHtml(currentPlaceholder || 'Текущее название'), '" style="', stInputBox, '" value="', escapeHtml(currentValue), '">',
buildStartMultiPageButtonHtml(startMultiTitle),
'</div>',
'<div class="rmInputRow ', RESIZE_CLASS, ' rmRenameVariantRow" style="', stRow, '">',
buildLeftSquareAddButtonHtml(config.addBtnId, addLabel.replace(/^\+\s*/, '') || 'Добавить вариант', 'rmAddVariantBtn rmRenameVariantAddBtn', '⤷'),
'<input id="', config.firstId, '" type="text" class="', config.inputClass || 'variantInput', '" placeholder="', config.firstPh || '', '" style="', stInputBox, '">',
'</div>',
'<div id="', config.containerId, '"></div>',
'</div>'
]);
}
function showInfoAndClose(mainText, detailsText, isErr) {
$('#removerModalContent').html(buildInfoBoxHtml(mainText, detailsText || '', isErr || false));
renderModalFooter('close');
}
function getSelectedAction(inputName, actionMap) {
var id = $('[name="' + inputName + '"]:checked').val();
var sel = actionMap[id];
if (!sel) alert('Выберите действие.');
return sel || null;
}
function prependTemplateToNoinclude(text, templateText) {
var source = String(text || '');
var tpl = String(templateText || '').trim();
if (!tpl) return source;
var match = source.match(RE_NOINCLUDE);
if (match) {
var before = source.slice(0, source.indexOf(match[0]));
if (/\S/.test(before)) return '<noinclude>' + tpl + '</noinclude>\n' + source;
var content = String(match[2] || '').replace(/^\n+/, '');
return source.replace(match[0], match[1] + '<noinclude>' + tpl + (content ? '\n' + content : '') + '\n</noinclude>');
}
return '<noinclude>' + tpl + '</noinclude>\n' + source;
}
function buildGeneratedNominationTemplateText(job, pg) {
var tplStr = '';
if (!job) return '';
if (job.opId === 'fRm') {
if (job.kbuTemplateText) return T_OPEN + job.kbuTemplateText + T_CLOSE;
return job.kbuTemplate ? (T_OPEN + job.kbuTemplate + T_CLOSE) : '';
}
if (typeof job.articleTpl !== 'function') return '';
tplStr = job.articleTpl(job.opId === 'mRnm' ? buildRenameTemplateParam(getMultiRenameTarget(job, pg, 'multiRenameTemplateTargets')) : job.tplpar, job.date[0]);
if (job.opId === 'merge' && job.tplpar) {
tplStr = (job.op.nomination.articleTpl)(
('|' + job.tplpar + '|').replace('|' + pg + '|', '|').slice(1, -1),
job.date[0]
);
}
return tplStr ? (T_OPEN + tplStr + T_CLOSE) : '';
}
function applyConflictTemplateResolution(articleText, job, pg, decision) {
var rule = getNominationConflictRule(job);
var generatedTemplate = buildGeneratedNominationTemplateText(job, pg);
var source = String(articleText || '');
if (!generatedTemplate || !decision) return source;
if (decision.templateAction === 'overwrite') {
var cleaned = rule ? stripTemplatesByPattern(source, rule.namePattern).text : source;
return prependTemplateToNoinclude(cleaned, generatedTemplate);
}
if (decision.templateAction === 'prepend') return prependTemplateToNoinclude(source, generatedTemplate);
return source;
}
function inspectMultiNominationConflicts(job, callback) {
var cb = callback || function () {};
var pages = (job && job.multiArticles) ? job.multiArticles.slice() : [];
var conflicts = [];
var statusId = logStatus('Проверяются статьи на наличие уже установленных шаблонов...', null, { pending: true, trackError: false });
if (!pages.length) {
logStatus('Проверка завершена: конфликтов не найдено.', null, { statusId: statusId, trackError: false });
cb(null, conflicts);
return;
}
eachSequential(pages, function (pg, next) {
getText(pg, function (articleText, readErr) {
var conflict;
if (readErr) { next(makeReadError(readErr, 'read_failed', 'Не удалось проверить страницу «' + pg + '».')); return; }
if (articleText === null) { next({ code: 'read_failed', info: 'Страница «' + pg + '» не существует.' }); return; }
conflict = detectNominationConflict(articleText, job);
if (conflict) {
conflicts.push($.extend({ pageName: pg }, conflict));
logStatus('В статье ' + buildQuotedStatusPageLink(pg) + ' обнаружен установленный шаблон <code>' + escapeHtml(conflict.templateDisplay || conflict.templateName || conflict.label || 'шаблон') + '</code>.', null, { trackError: false });
}
next();
});
}, function (err) {
if (err) {
logStatus('Проверка статей.', err, { statusId: statusId });
cb(err);
return;
}
logStatus(
conflicts.length
? 'Проверка завершена: найдены статьи с уже установленными шаблонами.'
: 'Проверка завершена: конфликтов не найдено.',
null,
{ statusId: statusId, trackError: false }
);
cb(null, conflicts);
});
}
function buildNominationConflictResolutionHtml(conflicts) {
return '<div class="rmInfoBox"><p style="margin:0 0 6px;">Найдены статьи, где шаблон уже стоит. Для каждой конфликтующей страницы выберите, что делать со статьёй и с шаблоном.</p>' +
'<p style="margin:0;color:' + tk.cSubM + ';font-size:12px;line-height:1.45;">По умолчанию такие статьи исключаются из новой номинации, а существующий шаблон остаётся без изменений.</p></div>' +
'<div class="rmConflictLead">После выбора нажмите «Продолжить номинирование».</div>' +
'<div id="rmConflictList" class="rmConflictList">' +
conflicts.map(function (conflict, index) {
var pageLink = buildStatusPageLink(conflict.pageName);
return '<div class="rmConflictCard" data-rm-conflict-index="' + index + '">' +
'<input type="hidden" class="rmConflictPageAction" value="skip">' +
'<input type="hidden" class="rmConflictTemplateAction" value="keep">' +
'<div class="rmConflictTitle">' + pageLink + '</div>' +
'<div class="rmConflictMeta">Обнаружен установленный шаблон <code>' + escapeHtml(conflict.templateDisplay || conflict.templateName || conflict.label || 'шаблон') + '</code>.</div>' +
'<div class="rmConflictGroup">' +
'<div class="rmConflictGroupTitle">Действие со статьёй</div>' +
'<div class="rmConflictButtons" data-rm-conflict-group="page">' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice is-active" data-rm-choice-type="page" data-rm-choice="skip" aria-pressed="true">Убрать из номинации</button>' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice" data-rm-choice-type="page" data-rm-choice="keep" aria-pressed="false">Оставить в номинации</button>' +
'</div></div>' +
'<div class="rmConflictGroup">' +
'<div class="rmConflictGroupTitle">Действие с шаблоном</div>' +
'<div class="rmConflictButtons" data-rm-conflict-group="template">' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice is-active" data-rm-choice-type="template" data-rm-choice="keep" aria-pressed="true">Оставить как есть</button>' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice" data-rm-choice-type="template" data-rm-choice="overwrite" aria-pressed="false">Новая дата</button>' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice" data-rm-choice-type="template" data-rm-choice="prepend" aria-pressed="false">Второй сверху</button>' +
'</div>' +
'<div class="rmConflictHint">Если статья исключается из номинации, действие с шаблоном не применяется.</div>' +
'</div></div>';
}).join('') +
'</div>';
}
function updateNominationConflictCardState($card) {
var pageAction = $card.find('.rmConflictPageAction').val() || 'skip';
var disableTemplate = pageAction !== 'keep';
var $templateButtons = $card.find('[data-rm-choice-type="template"]');
$card.toggleClass('is-skip', disableTemplate);
$card.find('[data-rm-conflict-group="template"]').toggleClass('is-disabled', disableTemplate);
$templateButtons.prop('disabled', disableTemplate).toggleClass('is-disabled', disableTemplate);
}
function bindNominationConflictResolutionUi() {
var $content = $('#removerModalContent');
function setChoice($card, type, value) {
var inputClass = type === 'page' ? '.rmConflictPageAction' : '.rmConflictTemplateAction';
$card.find(inputClass).val(value);
$card.find('[data-rm-choice-type="' + type + '"]').each(function () {
var isActive = $(this).data('rmChoice') === value;
$(this).toggleClass('is-active', isActive).attr('aria-pressed', isActive ? 'true' : 'false');
});
updateNominationConflictCardState($card);
}
$content.off('.rmConflictResolution').on('click.rmConflictResolution', '[data-rm-choice-type]', function () {
var $btn = $(this);
var $card = $btn.closest('.rmConflictCard');
if ($btn.prop('disabled')) return;
setChoice($card, $btn.data('rmChoiceType'), $btn.data('rmChoice'));
});
$('#rmConflictList .rmConflictCard').each(function () { updateNominationConflictCardState($(this)); });
}
function collectNominationConflictResolution(conflicts) {
var decisions = {};
(conflicts || []).forEach(function (conflict, index) {
var $card = $('#rmConflictList .rmConflictCard[data-rm-conflict-index="' + index + '"]');
decisions[normTitle(conflict.pageName)] = {
pageAction: $card.find('.rmConflictPageAction').val() || 'skip',
templateAction: $card.find('.rmConflictTemplateAction').val() || 'keep'
};
});
return decisions;
}
function applyNominationConflictResolutionToJob(job, decisions) {
var resultArticles;
var headerText;
if (!job || !job.isMulti) {
job.conflictDecisions = decisions || {};
return { value: job };
}
resultArticles = (job.multiArticles || []).filter(function (pageName) {
var decision = decisions && decisions[normTitle(pageName)];
return !decision || decision.pageAction !== 'skip';
});
if (!resultArticles.length) return { error: 'После исключения конфликтующих статей в номинации не осталось ни одной страницы.' };
job.conflictDecisions = decisions || {};
job.multiArticles = resultArticles.slice();
job.pages = resultArticles.slice().reverse();
if (job.multiRenamePairs && job.multiRenamePairs.length) {
job.multiRenamePairs = job.multiRenamePairs.filter(function (pair) {
return pair && resultArticles.indexOf(pair.pageName) !== -1;
});
}
if (job.multiRenameTargets) {
job.multiRenameTargets = resultArticles.reduce(function (map, pageName) {
map[normTitle(pageName)] = getMultiRenameTarget(job, pageName, 'multiRenameTargets');
return map;
}, {});
}
if (job.multiRenameTemplateTargets) {
job.multiRenameTemplateTargets = resultArticles.reduce(function (map, pageName) {
map[normTitle(pageName)] = getMultiRenameTarget(job, pageName, 'multiRenameTemplateTargets');
return map;
}, {});
}
headerText = String(job.multiHeaderText || '').trim();
job.section = headerText || (job.opId === 'mRnm' ? formatRenameItemsWithAnd(resultArticles, job.multiRenameTargets) : ('[[:' + resultArticles[0] + ']]'));
job.sectionNW = job.section.replace(/\[\[:/g, '').replace(/]]/g, '');
job.msg = job.multiNominationFormat === 'list'
? buildMultiNominationListText(resultArticles, job.multiNominationBody, job.multiArticleComments, job.opId === 'mRnm' ? getMultiRenameDiscussionOptions(job.multiRenameTargets) : null)
: buildMultiNominationText(resultArticles, job.multiNominationBody, job.multiArticleComments, job.opId === 'mRnm'
? getMultiRenameDiscussionOptions(job.multiRenameTargets, { leadingBlankLine: false })
: { leadingBlankLine: false });
job.summary = makeNominationTargetSummary(job.nomPage, job.sectionNW, job.summaryAction);
job.nominationSummary = makeNominationPageSummary(job.sectionNW);
return { value: job };
}
function showNominationConflictResolution(job, conflicts, onContinue) {
resetModalObservers();
$('#removerModalContent').html(buildNominationConflictResolutionHtml(conflicts));
bindNominationConflictResolutionUi();
syncLinkWidths();
renderModalFooter('submit', {
submitText: 'Продолжить номинирование',
showSubscribe: true,
preserveLogOnSubmit: true,
onSubmit: function () {
var decisions = collectNominationConflictResolution(conflicts);
var applied = applyNominationConflictResolutionToJob(job, decisions);
if (applied.error) {
alert(applied.error);
return false;
}
if (typeof onContinue === 'function') onContinue(applied.value);
return true;
}
});
}
function bindTouchTextareaGrip($ta, sync, getMaxWidth, options) {
var opts = options || {};
var minWidth = parseInt(opts.minWidth, 10) || parseInt(sz.taMinW, 10) || 180;
var minHeight = parseInt(opts.minHeight, 10) || parseInt(sz.taMinH, 10) || 100;
var allowWidthResize = opts.allowWidth !== false;
var syncFn = typeof sync === 'function' ? sync : function () {};
var getMaxWidthFn = typeof getMaxWidth === 'function' ? getMaxWidth : function () { return $ta.outerWidth() || minWidth; };
var usePointerEvents = typeof window.PointerEvent === 'function';
var dragState = { active: false, startX: 0, startY: 0, startWidth: 0, startHeight: 0 };
var gripStyle = 'height:20px;box-sizing:border-box;border:1px solid ' + tk.bSub + ';border-top:0;border-radius:0 0 4px 4px;background:' + tk.bgNSub + ';display:flex;align-items:center;justify-content:center;cursor:ns-resize;touch-action:none;user-select:none;-webkit-user-select:none;';
if (opts.gripMarginBottom) gripStyle += 'margin-bottom:' + opts.gripMarginBottom + ';';
var $grip = $('<div data-rm-textarea-grip="1" style="' + gripStyle + '"><span style="display:block;width:42px;height:4px;border-radius:999px;background:' + tk.bSub + ';opacity:.9;"></span></div>');
function getCoord(evt, key) {
var e = evt.originalEvent || evt;
if (e.touches && e.touches.length) return e.touches[0][key];
if (e.changedTouches && e.changedTouches.length) return e.changedTouches[0][key];
return e[key];
}
function stopDrag() {
dragState.active = false;
$(window).off('.rmTaResize');
}
function onDragMove(evt) {
var clientX;
var clientY;
if (!dragState.active) return;
clientX = getCoord(evt, 'clientX');
clientY = getCoord(evt, 'clientY');
if (typeof clientY !== 'number') return;
if (allowWidthResize && typeof clientX === 'number') $ta.css('width', Math.max(minWidth, Math.min(getMaxWidthFn(), dragState.startWidth + (clientX - dragState.startX))) + 'px');
$ta.css('height', Math.max(minHeight, dragState.startHeight + (clientY - dragState.startY)) + 'px');
syncFn();
if (evt.preventDefault) evt.preventDefault();
}
function startDrag(evt) {
var clientY = getCoord(evt, 'clientY');
if (typeof clientY !== 'number') return;
dragState.active = true;
dragState.startX = getCoord(evt, 'clientX') || 0;
dragState.startY = clientY;
dragState.startWidth = $ta.outerWidth();
dragState.startHeight = $ta.outerHeight();
if (evt.preventDefault) evt.preventDefault();
$(window).off('.rmTaResize');
if (usePointerEvents) {
$(window).on('pointermove.rmTaResize', onDragMove).on('pointerup.rmTaResize pointercancel.rmTaResize', stopDrag);
} else {
$(window).on('touchmove.rmTaResize mousemove.rmTaResize', onDragMove).on('touchend.rmTaResize touchcancel.rmTaResize mouseup.rmTaResize', stopDrag);
}
}
$ta.css({ 'border-bottom-left-radius': '0', 'border-bottom-right-radius': '0' });
$ta.next('[data-rm-textarea-grip]').remove();
$ta.after($grip);
if (usePointerEvents) $grip.on('pointerdown.rmTaGrip', startDrag);
else $grip.on('touchstart.rmTaGrip mousedown.rmTaGrip', startDrag);
}
function applyModalContentWidth($modal, contentWidth, options) {
var opts = options || {};
var layout = getModalLayout();
var modalFrame = getBoxFrameWidth($modal);
var safeContentWidth = Math.max(layout.minWidth, Math.min(contentWidth, layout.maxOuterWidth - modalFrame));
var modalWidth = safeContentWidth + modalFrame;
var initialContentW = parseFloat($modal.data('rmInitialContentW')) || 0;
$modal.css({
width: modalWidth + 'px',
'max-width': layout.maxOuterWidth + 'px',
'box-sizing': 'border-box',
'margin-left': layout.shouldCenter ? 'auto' : '0',
'margin-right': layout.shouldCenter ? 'auto' : '0'
}).toggleClass('rmCompactContent', safeContentWidth < 520);
$('.' + RESIZE_CLASS).css({
width: safeContentWidth + 'px',
'max-width': '100%',
'box-sizing': 'border-box'
});
$('#rmMsg,#nominationReason,#rmReportText').each(function () {
var $textarea = $(this);
var textareaId = this.id;
if (!$textarea.length) return;
$textarea.css('width', safeContentWidth + 'px');
$textarea.next('[data-rm-textarea-grip]').css('width', safeContentWidth + 'px');
$('.rmQuickPhrasesPanel[data-rm-target="' + textareaId + '"]').css('width', safeContentWidth + 'px');
});
$('.rmNestedCommentInput').each(function () {
var $textarea = $(this);
var $wrap = $textarea.parent();
var containerFrame = parseFloat($textarea.data('rmNestedContainerFrame')) || 0;
var wrapFrame = parseFloat($textarea.data('rmNestedWrapFrame')) || 0;
var safeMinWidth = parseFloat($textarea.data('rmNestedMinWidth')) || 0;
var wrapOuterWidth;
var textareaWidth;
if (!$wrap.length || !$wrap.is(':visible')) return;
wrapOuterWidth = Math.max(1, safeContentWidth - containerFrame);
textareaWidth = Math.max(1, wrapOuterWidth - wrapFrame);
$wrap.css({
width: wrapOuterWidth + 'px',
'max-width': '100%',
'box-sizing': 'border-box'
});
$textarea.css({
width: textareaWidth + 'px',
'min-width': Math.min(safeMinWidth || textareaWidth, textareaWidth) + 'px'
});
$textarea.next('[data-rm-textarea-grip]').css('width', textareaWidth + 'px');
$('.rmQuickPhrasesPanel[data-rm-target="' + this.id + '"]').css('width', textareaWidth + 'px');
});
syncLinkWidths();
if (isVector22) {
var $content = $('#content');
if ($content.length && modalWidth > initialContentW) $content.css({ 'min-width': modalWidth + 'px' });
else if ($content.length) $content.css({ 'min-width': '' });
} else {
$('#content').css({ 'min-width': '' });
}
if (!opts.skipStore) $modal.data('rmContentWidth', safeContentWidth);
return safeContentWidth;
}
function setupNestedResizableTextarea(textareaId, wrapId, minWidth, minHeight) {
var $ta = $('#' + textareaId);
var $wrap = $('#' + wrapId);
var $modal = $('#removerModal');
var $container = $wrap.parent();
var layout = getModalLayout();
var safeMinWidth = parseInt(minWidth, 10) || 280;
var safeMinHeight = parseInt(minHeight, 10) || 90;
var initialWidth;
var modalFrame;
var containerFrame;
var wrapFrame;
function px($el, prop) { var n = parseFloat($el.css(prop)); return isNaN(n) ? 0 : n; }
function getMaxTextareaWidth(currentLayout) {
return Math.max(1, currentLayout.maxOuterWidth - modalFrame - containerFrame - wrapFrame);
}
function getEffectiveMinWidth(currentLayout) {
return Math.min(safeMinWidth, getMaxTextareaWidth(currentLayout));
}
function sync() {
var currentLayout = getModalLayout();
var maxTextareaWidth = getMaxTextareaWidth(currentLayout);
var textareaWidth = $ta.outerWidth();
var contentWidth;
if (!$wrap.is(':visible')) return;
if (textareaWidth > maxTextareaWidth) {
$ta.css('width', maxTextareaWidth + 'px');
textareaWidth = $ta.outerWidth();
}
contentWidth = Math.min(currentLayout.maxOuterWidth - modalFrame, textareaWidth + wrapFrame + containerFrame);
applyModalContentWidth($modal, contentWidth);
}
if (!$ta.length || !$wrap.length || !$modal.length) return;
modalFrame = px($modal, 'padding-left') + px($modal, 'padding-right') + px($modal, 'border-left-width') + px($modal, 'border-right-width');
containerFrame = $container.length
? px($container, 'padding-left') + px($container, 'padding-right') + px($container, 'border-left-width') + px($container, 'border-right-width')
: 0;
wrapFrame = px($wrap, 'padding-left') + px($wrap, 'padding-right') + px($wrap, 'border-left-width') + px($wrap, 'border-right-width');
initialWidth = Math.min(
Math.max(getEffectiveMinWidth(layout), getDefaultResizableWidth(modalFrame + containerFrame + wrapFrame)),
getMaxTextareaWidth(layout)
);
$ta.css({
width: initialWidth + 'px',
'min-width': getEffectiveMinWidth(layout) + 'px',
'min-height': safeMinHeight + 'px',
'box-sizing': 'border-box',
resize: layout.useFullWidth ? 'none' : 'both',
'border-bottom-left-radius': '',
'border-bottom-right-radius': ''
});
$ta.data('rmNestedContainerFrame', containerFrame);
$ta.data('rmNestedWrapFrame', wrapFrame);
$ta.data('rmNestedMinWidth', safeMinWidth);
$ta.next('[data-rm-textarea-grip]').remove();
if (layout.useFullWidth) {
bindTouchTextareaGrip($ta, sync, function () {
return getMaxTextareaWidth(getModalLayout());
}, {
minWidth: getEffectiveMinWidth(layout),
minHeight: safeMinHeight
});
}
registerModalLayoutSync(sync);
$(window).off('resize.removerModal').on('resize.removerModal', syncModalLayout);
if (typeof ResizeObserver === 'function') {
var observer = new ResizeObserver(sync);
observer.observe($ta[0]);
registerResizeObserver(observer);
}
sync();
}
function setupResizableModal(textareaId) {
var $ta = $('#' + textareaId);
var $modal = $('#removerModal');
var layout = getModalLayout();
function px($el, prop) { var n = parseFloat($el.css(prop)); return isNaN(n) ? 0 : n; }
applyV2022Layout($modal);
$modal.css({ display: 'block', 'margin-left': layout.shouldCenter ? 'auto' : '0', 'margin-right': layout.shouldCenter ? 'auto' : '0' });
var isBorderBox = ($modal.css('box-sizing') || '').toLowerCase() === 'border-box';
var hFrame = px($modal, 'padding-left') + px($modal, 'padding-right') + px($modal, 'border-left-width') + px($modal, 'border-right-width');
var initialContentW = isVector22 ? ($('#content').outerWidth() || 0) : 0;
var minWidth = layout.minWidth;
$modal.data('rmInitialContentW', initialContentW);
$ta.css({ width: getDefaultResizableWidth(hFrame) + 'px', height: sz.taH, padding: '8px', 'box-sizing': 'border-box',
border: '1px solid ' + tk.bSub, 'border-radius': '2px', background: tk.bgBase,
color: 'inherit', resize: layout.useFullWidth ? 'none' : 'both', 'min-height': sz.taMinH, 'min-width': sz.taMinW });
$(window).off('.rmTaResize');
function sync() {
var currentLayout = getModalLayout();
var maxTextareaWidth = Math.max(minWidth, currentLayout.maxOuterWidth - Math.floor(hFrame));
var w = $ta.outerWidth();
if (w > maxTextareaWidth) {
$ta.css('width', maxTextareaWidth + 'px');
w = $ta.outerWidth();
}
applyModalContentWidth($modal, isBorderBox ? w : Math.min(currentLayout.maxOuterWidth - hFrame, w));
}
if (layout.useFullWidth) bindTouchTextareaGrip($ta, sync, function () {
return Math.max(minWidth, getModalLayout().maxOuterWidth - Math.floor(hFrame));
});
else {
$ta.css({ 'border-bottom-left-radius': '', 'border-bottom-right-radius': '' });
$ta.next('[data-rm-textarea-grip]').remove();
}
registerModalLayoutSync(sync);
$(window).off('resize.removerModal').on('resize.removerModal', syncModalLayout);
if (typeof ResizeObserver === 'function') {
var observer = new ResizeObserver(sync);
observer.observe($ta[0]);
registerResizeObserver(observer);
}
sync();
}
function addInputRow(opts) {
var w = $('#rmMsg,#nominationReason,#rmReportText').first().outerWidth() || 0;
var indentLeft = Math.max(0, parseInt(opts.indentLeft, 10) || 0);
var rowClass = opts.rowClass ? ' ' + opts.rowClass : '';
var widthStyle = opts.fitParentWidth
? 'width:calc(100% - ' + indentLeft + 'px);box-sizing:border-box;'
: (opts.autoWidth ? '' : (w ? 'width:' + Math.max(1, w - indentLeft) + 'px;' : ''));
$('#' + opts.containerId).append(joinHtml([
'<div class="rmInputRow ', RESIZE_CLASS, rowClass, '" style="', stRow, widthStyle, indentLeft ? 'margin-left:' + indentLeft + 'px;' : '', '">',
opts.prefixHtml || '',
'<input type="text" class="', opts.inputClass || 'variantInput', '" placeholder="', opts.placeholder || '', '" style="', stInputBox, '">',
opts.removeBeforeInput ? '' : '<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить">−</button>',
'</div>'
]));
syncModalLayout();
}
$(document).off('click.rmRemoveInput').on('click.rmRemoveInput', '.rmRemoveInput', function () {
$(this).closest('.rmInputRow').remove();
syncModalLayout();
});
$(document).off('click.rmQuickPhraseInsert').on('click.rmQuickPhraseInsert', '.rmQuickPhraseActionBtn', function (e) {
var targetId;
var phrase;
e.preventDefault();
targetId = $(this).data('rmTarget');
phrase = $(this).attr('data-rm-phrase') || '';
if (!targetId) return;
insertTextIntoTextarea($('#' + targetId), phrase);
});
function buildMultiInputHtml(c) {
var addLabel = c.addBtnLabel || '+ Добавить';
var addClass = c.type === 'rename' ? 'rmAddVariantBtn rmRenameVariantAddBtn' : '';
var addSymbol = c.type === 'rename' ? '⤷' : '+';
return joinHtml([
'<div class="rmInputRow ', RESIZE_CLASS, '" style="', stRow, '">',
'<input id="', c.firstId, '" type="text" class="', c.inputClass || 'variantInput', '" placeholder="', c.firstPh || '', '" style="', stInputBox, '">',
buildSquareAddButtonHtml(c.addBtnId, addLabel.replace(/^\+\s*/, '') || 'Добавить', addClass, addSymbol),
'</div>',
'<div id="', c.containerId, '"></div>'
]);
}
function wireMultiInput(c) {
$('#' + c.addBtnId).click(function () {
var count = $('#' + c.containerId + ' .rmInputRow').length;
if (typeof c.maxRows === 'number' && count >= c.maxRows) { alert(c.maxMsg || 'Достигнут лимит.'); return; }
addInputRow({
containerId: c.containerId,
placeholder: c.addPh,
inputClass: c.inputClass,
rowClass: c.type === 'rename' ? 'rmRenameVariantRow' : '',
indentLeft: 0,
fitParentWidth: c.type === 'rename',
prefixHtml: c.type === 'rename' ? buildLeftRemoveButtonHtml('rmRemoveInput', 'Удалить') : '',
removeBeforeInput: c.type === 'rename'
});
});
}
function buildSettingsFieldHtml(label, controlHtml, helpText, options) {
var opts = options || {};
var helpHtml = helpText
? '<div class="rmSettingsFieldHint">' + helpText + '</div>'
: '';
var labelHtml = opts.forId
? '<label class="rmSettingsFieldLabel" for="' + opts.forId + '">' + label + '</label>'
: '<div class="rmSettingsFieldLabel">' + label + '</div>';
return joinHtml([
'<div class="rmSettingsField">',
labelHtml,
'<div class="rmSettingsFieldControl">', controlHtml, '</div>',
helpHtml,
'</div>'
]);
}
function buildSettingsSectionHtml(title, bodyHtml, helpText, options) {
var opts = options || {};
var headerHtml = '';
var description = [];
if (opts.titleNote) description.push(opts.titleNote);
if (helpText) description.push(helpText);
if (title || description.length) {
headerHtml = joinHtml([
'<div class="rmSettingsSectionHeader">',
title ? '<div class="rmSettingsSectionTitle">' + title + '</div>' : '',
description.length ? '<div class="rmSettingsSectionDescription">' + description.join(' ') + '</div>' : '',
'</div>'
]);
}
return joinHtml([
'<div class="rmSettingsSection">',
headerHtml,
bodyHtml,
'</div>'
]);
}
function buildSettingsSimpleCheckboxHtml(id, text) {
return joinHtml([
'<label class="rmSettingsCheck">',
'<input id="', id, '" type="checkbox">',
'<span>', text, '</span>',
'</label>'
]);
}
function buildQuickPhrasesSettingsEditorHtml() {
return joinHtml([
'<div id="rmSettingsQuickPhrasesEditor" class="rmQuickPhraseEditor">',
'<div id="rmSettingsQuickPhrasesList" class="rmQuickPhraseList"></div>',
'<input id="rmSettingsQuickPhraseInput" type="text" autocomplete="off" style="', stInputFull, 'margin-bottom:0;">',
'<div id="rmSettingsQuickPhraseMeta" class="rmQuickPhraseMeta"></div>',
'</div>'
]);
}
function getQuickPhraseEditor() {
return $('#rmSettingsQuickPhrasesEditor');
}
function getQuickPhraseEditorState() {
var $editor = getQuickPhraseEditor();
var phrases = normalizeQuickPhrasesList($editor.data('rmQuickPhrases'), []);
var editingIndex = parseInt($editor.data('rmQuickPhraseEditingIndex'), 10);
if (isNaN(editingIndex)) editingIndex = -1;
return { editor: $editor, phrases: phrases, editingIndex: editingIndex };
}
function setQuickPhraseEditorState(phrases, editingIndex) {
var $editor = getQuickPhraseEditor();
var normalized = normalizeQuickPhrasesList(phrases, []);
var safeEditingIndex = parseInt(editingIndex, 10);
if (isNaN(safeEditingIndex) || safeEditingIndex < 0 || safeEditingIndex >= normalized.length) safeEditingIndex = -1;
if (!$editor.length) return;
$editor.data('rmQuickPhrases', normalized);
$editor.data('rmQuickPhraseEditingIndex', safeEditingIndex);
renderQuickPhraseEditor();
}
function clearQuickPhraseDropState() {
var $editor = getQuickPhraseEditor();
$editor.removeData('rmQuickPhraseDragIndex');
$editor.removeData('rmQuickPhraseDropIndex');
$editor.removeData('rmQuickPhraseDropAfter');
$editor.find('.rmQuickPhraseChip').removeClass('is-dragging is-drop-before is-drop-after');
}
function renderQuickPhraseEditor() {
var state = getQuickPhraseEditorState();
var $list = $('#rmSettingsQuickPhrasesList');
var $input = $('#rmSettingsQuickPhraseInput');
var $meta = $('#rmSettingsQuickPhraseMeta');
if (!state.editor.length || !$list.length || !$input.length) return;
if (state.phrases.length) {
$list.html(state.phrases.map(function (phrase, index) {
var chipClass = 'rmQuickPhraseChip' + (index === state.editingIndex ? ' is-editing' : '');
return joinHtml([
'<div class="', chipClass, '" draggable="true" data-rm-quick-index="', index, '">',
'<button type="button" class="rmQuickPhraseEditBtn" title="Редактировать фразу">', escapeHtml(phrase), '</button>',
'<button type="button" class="rmQuickPhraseRemoveBtn" title="Удалить фразу" aria-label="Удалить фразу">×</button>',
'</div>'
]);
}).join(''));
} else {
$list.html('<div class="rmQuickPhraseEmpty">Фразы пока не добавлены.</div>');
}
$input
.attr('placeholder', state.editingIndex >= 0 ? 'Изменить значение...' : 'Добавить значение...')
.toggleClass('is-editing', state.editingIndex >= 0);
$meta
.text('')
.hide();
}
function notifyQuickPhraseEditorChanged() {
var $editor = getQuickPhraseEditor();
if ($editor.length) $editor.trigger('rmQuickPhrasesChanged');
}
function startQuickPhraseEdit(index) {
var state = getQuickPhraseEditorState();
var $input = $('#rmSettingsQuickPhraseInput');
if (index < 0 || index >= state.phrases.length || !$input.length) return;
state.editor.data('rmQuickPhraseEditingIndex', index);
$input.val(state.phrases[index]);
renderQuickPhraseEditor();
$input.trigger('focus');
if ($input[0] && typeof $input[0].select === 'function') $input[0].select();
}
function cancelQuickPhraseEdit() {
var state = getQuickPhraseEditorState();
var $input = $('#rmSettingsQuickPhraseInput');
state.editor.data('rmQuickPhraseEditingIndex', -1);
if ($input.length) $input.val('').removeClass('is-editing');
renderQuickPhraseEditor();
}
function saveQuickPhraseInput() {
var state = getQuickPhraseEditorState();
var $input = $('#rmSettingsQuickPhraseInput');
var value = normalizeQuickPhraseValue($input.val());
var next = [];
if (!$input.length || !value) return false;
if (state.editingIndex >= 0) {
state.phrases.forEach(function (phrase, index) {
if (index === state.editingIndex) {
next.push(value);
return;
}
if (phrase !== value && next.indexOf(phrase) === -1) next.push(phrase);
});
} else {
next = state.phrases.slice();
if (next.indexOf(value) === -1) next.push(value);
}
state.editor.data('rmQuickPhrases', normalizeQuickPhrasesList(next, []));
state.editor.data('rmQuickPhraseEditingIndex', -1);
$input.val('').removeClass('is-editing');
clearQuickPhraseDropState();
renderQuickPhraseEditor();
notifyQuickPhraseEditorChanged();
return true;
}
function removeQuickPhrase(index) {
var state = getQuickPhraseEditorState();
var $input = $('#rmSettingsQuickPhraseInput');
if (index < 0 || index >= state.phrases.length) return;
state.phrases.splice(index, 1);
state.editor.data('rmQuickPhrases', state.phrases);
if (state.editingIndex === index) {
state.editor.data('rmQuickPhraseEditingIndex', -1);
if ($input.length) $input.val('').removeClass('is-editing');
} else if (state.editingIndex > index) {
state.editor.data('rmQuickPhraseEditingIndex', state.editingIndex - 1);
}
clearQuickPhraseDropState();
renderQuickPhraseEditor();
notifyQuickPhraseEditorChanged();
}
function reorderQuickPhrases(phrases, fromIndex, toIndex, placeAfter) {
var result = phrases.slice();
var insertIndex = toIndex + (placeAfter ? 1 : 0);
var item;
if (fromIndex < 0 || fromIndex >= result.length || toIndex < 0 || toIndex >= result.length) return result;
item = result.splice(fromIndex, 1)[0];
if (fromIndex < insertIndex) insertIndex--;
result.splice(insertIndex, 0, item);
return result;
}
function getQuickPhraseDropPointer(evt) {
var originalEvent = evt && (evt.originalEvent || evt);
if (!originalEvent) return null;
if (typeof originalEvent.clientX !== 'number' || typeof originalEvent.clientY !== 'number') return null;
return { x: originalEvent.clientX, y: originalEvent.clientY };
}
function getQuickPhraseDropTarget($list, pointer, dragIndex) {
var candidates = [];
var rowCandidates;
var minRowDistance = Infinity;
var bestBoundary = null;
var bestBoundaryDistance = Infinity;
if (!$list || !$list.length || !pointer) return null;
$list.children('.rmQuickPhraseChip').each(function () {
var index = parseInt($(this).attr('data-rm-quick-index'), 10);
var rect;
var rowDistance;
if (isNaN(index) || index === dragIndex) return;
rect = this.getBoundingClientRect();
if (!rect.width || !rect.height) return;
rowDistance = pointer.y < rect.top ? (rect.top - pointer.y) : (pointer.y > rect.bottom ? (pointer.y - rect.bottom) : 0);
candidates.push({
node: this,
index: index,
left: rect.left,
right: rect.right,
top: rect.top,
bottom: rect.bottom,
midX: rect.left + rect.width / 2,
rowDistance: rowDistance
});
if (rowDistance < minRowDistance) minRowDistance = rowDistance;
});
if (!candidates.length) return null;
rowCandidates = candidates
.filter(function (candidate) { return candidate.rowDistance === minRowDistance; })
.sort(function (a, b) {
if (a.left !== b.left) return a.left - b.left;
return a.index - b.index;
});
if (!rowCandidates.length) return null;
if (pointer.x <= rowCandidates[0].left) {
return { index: rowCandidates[0].index, placeAfter: false, node: rowCandidates[0].node };
}
if (pointer.x >= rowCandidates[rowCandidates.length - 1].right) {
return {
index: rowCandidates[rowCandidates.length - 1].index,
placeAfter: true,
node: rowCandidates[rowCandidates.length - 1].node
};
}
for (var i = 0; i < rowCandidates.length; i++) {
var candidate = rowCandidates[i];
if (pointer.x >= candidate.left && pointer.x <= candidate.right) {
return {
index: candidate.index,
placeAfter: pointer.x > candidate.midX,
node: candidate.node
};
}
}
rowCandidates.forEach(function (candidate) {
var leftDistance = Math.abs(pointer.x - candidate.left);
var rightDistance = Math.abs(pointer.x - candidate.right);
if (leftDistance < bestBoundaryDistance) {
bestBoundaryDistance = leftDistance;
bestBoundary = { index: candidate.index, placeAfter: false, node: candidate.node };
}
if (rightDistance < bestBoundaryDistance) {
bestBoundaryDistance = rightDistance;
bestBoundary = { index: candidate.index, placeAfter: true, node: candidate.node };
}
});
return bestBoundary;
}
function bindQuickPhrasesEditor() {
var $editor = getQuickPhraseEditor();
var $list = $('#rmSettingsQuickPhrasesList');
var $input = $('#rmSettingsQuickPhraseInput');
function updateQuickPhraseDropTarget(evt) {
var dragIndex = parseInt($editor.data('rmQuickPhraseDragIndex'), 10);
var pointer = getQuickPhraseDropPointer(evt);
var target;
if (isNaN(dragIndex) || !pointer) return null;
target = getQuickPhraseDropTarget($list, pointer, dragIndex);
if (!target) return null;
$editor.data('rmQuickPhraseDropIndex', target.index);
$editor.data('rmQuickPhraseDropAfter', !!target.placeAfter);
$editor.find('.rmQuickPhraseChip').removeClass('is-drop-before is-drop-after');
$(target.node).addClass(target.placeAfter ? 'is-drop-after' : 'is-drop-before');
return target;
}
function applyQuickPhraseDrop() {
var state = getQuickPhraseEditorState();
var fromIndex = parseInt($editor.data('rmQuickPhraseDragIndex'), 10);
var toIndex = parseInt($editor.data('rmQuickPhraseDropIndex'), 10);
var placeAfter = $editor.data('rmQuickPhraseDropAfter') === true;
var changed = false;
if (!isNaN(fromIndex) && !isNaN(toIndex) && fromIndex !== toIndex) {
state.editor.data('rmQuickPhrases', reorderQuickPhrases(state.phrases, fromIndex, toIndex, placeAfter));
if (state.editingIndex === fromIndex) state.editor.data('rmQuickPhraseEditingIndex', -1);
changed = true;
}
clearQuickPhraseDropState();
renderQuickPhraseEditor();
if (changed) notifyQuickPhraseEditorChanged();
}
if (!$editor.length || !$list.length || !$input.length) return;
$editor.off('.rmQuickPhraseEditor');
$list.off('.rmQuickPhraseEditor');
$input.off('.rmQuickPhraseEditor');
$input.on('keydown.rmQuickPhraseEditor', function (e) {
if (e.key === 'Enter' || e.keyCode === 13) {
e.preventDefault();
saveQuickPhraseInput();
} else if (e.key === 'Escape' || e.keyCode === 27) {
e.preventDefault();
cancelQuickPhraseEdit();
}
});
$editor
.on('click.rmQuickPhraseEditor', '.rmQuickPhraseEditBtn', function () {
var index = parseInt($(this).closest('.rmQuickPhraseChip').attr('data-rm-quick-index'), 10);
startQuickPhraseEdit(index);
})
.on('click.rmQuickPhraseEditor', '.rmQuickPhraseRemoveBtn', function (e) {
var index;
e.preventDefault();
e.stopPropagation();
index = parseInt($(this).closest('.rmQuickPhraseChip').attr('data-rm-quick-index'), 10);
removeQuickPhrase(index);
})
.on('dragstart.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) {
var index = parseInt($(this).attr('data-rm-quick-index'), 10);
if (isNaN(index)) return;
$editor.data('rmQuickPhraseDragIndex', index);
$(this).addClass('is-dragging');
if (e.originalEvent && e.originalEvent.dataTransfer) {
e.originalEvent.dataTransfer.effectAllowed = 'move';
e.originalEvent.dataTransfer.setData('text/plain', String(index));
}
})
.on('dragover.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) {
if ($editor.data('rmQuickPhraseDragIndex') === undefined) return;
e.preventDefault();
updateQuickPhraseDropTarget(e);
if (e.originalEvent && e.originalEvent.dataTransfer) e.originalEvent.dataTransfer.dropEffect = 'move';
})
.on('drop.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) {
e.preventDefault();
updateQuickPhraseDropTarget(e);
applyQuickPhraseDrop();
})
.on('dragend.rmQuickPhraseEditor', '.rmQuickPhraseChip', function () {
clearQuickPhraseDropState();
});
$list
.on('dragover.rmQuickPhraseEditor', function (e) {
if ($editor.data('rmQuickPhraseDragIndex') === undefined) return;
e.preventDefault();
updateQuickPhraseDropTarget(e);
if (e.originalEvent && e.originalEvent.dataTransfer) e.originalEvent.dataTransfer.dropEffect = 'move';
})
.on('drop.rmQuickPhraseEditor', function (e) {
e.preventDefault();
updateQuickPhraseDropTarget(e);
applyQuickPhraseDrop();
});
renderQuickPhraseEditor();
}
function collectQuickPhraseValues() {
return getQuickPhraseEditorState().phrases;
}
function collectQuickPhraseValuesSnapshot() {
var state = getQuickPhraseEditorState();
var value = normalizeQuickPhraseValue($('#rmSettingsQuickPhraseInput').val());
var next = state.phrases.slice();
if (!value) return next;
if (state.editingIndex >= 0) {
next[state.editingIndex] = value;
} else if (next.indexOf(value) === -1) {
next.push(value);
}
return normalizeQuickPhrasesList(next, []);
}
function isMenuTitlePresetValue(value) {
return value === MENU_TITLE_PRESET_CACTIONS || value === MENU_TITLE_PRESET_PAGE || value === MENU_TITLE_PRESET_TOOLS;
}
function isMenuTitlePresetOnlySkin() {
return mwCfg.skin === 'minerva' || mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless';
}
function getDefaultMenuTitlePreset() {
return mwCfg.skin === 'minerva' ? MENU_TITLE_PRESET_TOOLS : MENU_TITLE_PRESET_CACTIONS;
}
function shouldPreserveStoredMenuTitleOnSave() {
return mwCfg.skin === 'minerva';
}
function isAvailableMenuTitlePresetValue(value) {
return getMenuTitlePresetOptions().some(function (option) {
return option.value === value;
});
}
function getMenuTitlePresetOptions() {
if (mwCfg.skin === 'minerva') {
return [
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Ещё' }
];
}
if (isVector22) {
return [
{ value: MENU_TITLE_PRESET_CACTIONS, label: 'Инструменты/Действия' },
{ value: MENU_TITLE_PRESET_PAGE, label: 'Страница' },
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты/Основное' }
];
}
if (mwCfg.skin === 'timeless') {
return [
{ value: MENU_TITLE_PRESET_CACTIONS, label: 'Инструменты для страниц' },
{ value: MENU_TITLE_PRESET_PAGE, label: 'Страница' },
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Вики-инструменты' }
];
}
if (mwCfg.skin === 'monobook') {
return [
{ value: MENU_TITLE_PRESET_CACTIONS, label: 'Верхняя панель' },
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты' }
];
}
return [
{ value: MENU_TITLE_PRESET_CACTIONS, label: mwCfg.skin === 'vector' ? 'Ещё' : 'Ещё / Действия' },
{ value: MENU_TITLE_PRESET_PAGE, label: 'Страница' },
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты' }
];
}
function getMenuTitlePresetHintText() {
var base = (mwCfg.skin === 'vector' || isVector22)
? 'Можно либо изменить заголовок отдельного меню Remover, либо перенести все пункты в одно из существующих стандартных меню.'
: 'На этом скине Remover использует существующие стандартные меню; отдельное меню с собственным заголовком не создаётся.';
if (isVector22) base += ' В Vector 2022 кнопки «Действия» и «Основное» находятся внутри общего меню «Инструменты».';
else if (mwCfg.skin === 'vector') base += ' В Vector отдельный заголовок создаёт собственное меню рядом с «Ещё».';
else if (mwCfg.skin === 'minerva') base += ' В Minerva Neue пункты Remover показываются в меню «Ещё».';
else if (mwCfg.skin === 'timeless') base += ' В Timeless доступны только стандартные варианты: «Инструменты для страниц», «Страница» и «Вики-инструменты».';
else if (mwCfg.skin === 'monobook') base += ' В MonoBook доступны только два варианта: поместить пункты в «Инструменты» или вывести их в верхнюю панель. Собственный заголовок меню здесь не используется.';
return base;
}
function getSignatureSeparatorPreviewText(value) {
var separator = String(value || '').trim();
return separator ? (separator + ' ' + '~~' + '~~') : ('~~' + '~~');
}
function updateSignatureSeparatorPreview(value) {
var previewValue = (typeof value === 'string') ? value : ($('#rmSettingsSignatureSeparator').val() || '');
var $code = $('#rmSettingsSignaturePreviewCode');
if (!$code.length) return;
$code.text(getSignatureSeparatorPreviewText(previewValue));
}
function bindSignatureSeparatorPreview() {
var $input = $('#rmSettingsSignatureSeparator');
if (!$input.length) return;
$input.off('.rmSignaturePreview').on('input.rmSignaturePreview change.rmSignaturePreview', function () {
updateSignatureSeparatorPreview($(this).val());
});
updateSignatureSeparatorPreview($input.val());
}
function buildMenuTitlePresetButtonsHtml() {
return joinHtml([
'<div class="rmSettingsMenuPresetWrap">',
'<div class="rmSettingsMenuPresetLabel">Перенос в стандартные меню</div>',
'<div id="rmSettingsMenuPresetBar" class="rmSettingsMenuPresetBar">',
getMenuTitlePresetOptions().map(function (option) {
return joinHtml([
'<button type="button" class="rmSettingsMenuPresetBtn" data-rm-menu-preset="',
option.value,
'" aria-pressed="false">',
escapeHtml(option.label),
'</button>'
]);
}).join(''),
'</div>',
'</div>'
]);
}
function applyMenuTitlePresetControls(presetValue) {
var preset = isMenuTitlePresetValue(presetValue) ? presetValue : '';
var $bar = $('#rmSettingsMenuPresetBar');
var $input = $('#rmSettingsMenuTitle');
var forcePresetOnly = isMenuTitlePresetOnlySkin();
if (!$bar.length || !$input.length) return;
$bar.data('rmPreset', preset);
$bar.find('.rmSettingsMenuPresetBtn').each(function () {
var isActive = $(this).data('rmMenuPreset') === preset;
$(this).toggleClass('is-active', isActive).attr('aria-pressed', isActive ? 'true' : 'false');
});
$input.prop('disabled', forcePresetOnly || !!preset);
}
function bindMenuTitlePresetControls() {
var $bar = $('#rmSettingsMenuPresetBar');
var $input = $('#rmSettingsMenuTitle');
if (!$bar.length || !$input.length) return;
$input.off('.rmSettingsMenuPreset').on('input.rmSettingsMenuPreset', function () {
if (!$bar.data('rmPreset')) $input.data('rmCustomValue', $input.val());
});
$bar.off('.rmSettingsMenuPreset').on('click.rmSettingsMenuPreset', '.rmSettingsMenuPresetBtn', function () {
var preset = $(this).data('rmMenuPreset');
var currentPreset = $bar.data('rmPreset') || '';
if (currentPreset === preset) {
if (isMenuTitlePresetOnlySkin()) return;
applyMenuTitlePresetControls('');
$input.val($input.data('rmCustomValue') || '');
$input.trigger('focus');
return;
}
$input.data('rmCustomValue', $input.val());
applyMenuTitlePresetControls(preset);
});
}
function fillSettingsFormValues(settings) {
var data = normalizeRemoverSettings(settings);
var forcePresetOnly = isMenuTitlePresetOnlySkin();
var menuTitleValue = data.menuTitle || '';
var storedMenuTitleValue = menuTitleValue;
if (forcePresetOnly && !isAvailableMenuTitlePresetValue(menuTitleValue)) menuTitleValue = getDefaultMenuTitlePreset();
var customMenuTitle = forcePresetOnly ? '' : (isMenuTitlePresetValue(menuTitleValue) ? settingsDefaults.menuTitle : menuTitleValue);
$('#rmSettingsForm').data('rmStoredMenuTitle', storedMenuTitleValue || '');
$('#rmSettingsNotifyAuthor').prop('checked', !!data.notifyAuthor);
$('#rmSettingsSubscribeTopic').prop('checked', !!data.subscribeTopic);
$('#rmSettingsAutoReloadAfterAction').prop('checked', !!data.autoReloadAfterAction);
$('#rmSettingsOpenNominationDiscussionInNewTab').prop('checked', !!data.openNominationDiscussionInNewTab);
$('#rmSettingsShowMenuIcons').prop('checked', !!data.showMenuIcons);
$('#rmSettingsMenuTitle').val(customMenuTitle || '').data('rmCustomValue', customMenuTitle || '');
$('#rmSettingsSignatureSeparator').val(data.signatureSeparator || '');
$('#rmSettingsExcludedNamespaces').val((data.excludedNamespaces || []).join(', '));
$('#rmSettingsDisabledItems').val((data.disabledItems || []).join(', '));
setQuickPhraseEditorState(data.quickPhrases || [], -1);
$('#rmSettingsQuickPhraseInput').val('').removeClass('is-editing');
clearQuickPhraseDropState();
applyMenuTitlePresetControls(menuTitleValue);
updateSignatureSeparatorPreview(data.signatureSeparator || '');
}
function collectSettingsFormValues(options) {
var opts = options || {};
var namespaces = parseNamespaceInput($('#rmSettingsExcludedNamespaces').val());
var disabledItems = parseDisabledItemsInput($('#rmSettingsDisabledItems').val());
var presetMenuTitle = $('#rmSettingsMenuPresetBar').data('rmPreset');
var forcePresetOnly = isMenuTitlePresetOnlySkin();
var storedMenuTitle = $('#rmSettingsForm').data('rmStoredMenuTitle');
if (namespaces.invalid.length) {
return { error: 'Некорректные номера пространств имён: ' + namespaces.invalid.join(', ') + '.' };
}
if (disabledItems.invalid.length) {
return { error: 'Неизвестные пункты меню: ' + disabledItems.invalid.join(', ') + '.' };
}
if (!opts.skipQuickPhraseCommit) saveQuickPhraseInput();
return {
value: normalizeRemoverSettings({
notifyAuthor: $('#rmSettingsNotifyAuthor').is(':checked'),
subscribeTopic: $('#rmSettingsSubscribeTopic').is(':checked'),
autoReloadAfterAction: $('#rmSettingsAutoReloadAfterAction').is(':checked'),
openNominationDiscussionInNewTab: $('#rmSettingsOpenNominationDiscussionInNewTab').is(':checked'),
showMenuIcons: $('#rmSettingsShowMenuIcons').is(':checked'),
menuTitle: forcePresetOnly
? (shouldPreserveStoredMenuTitleOnSave() && typeof storedMenuTitle === 'string'
? storedMenuTitle
: (isAvailableMenuTitlePresetValue(presetMenuTitle) ? presetMenuTitle : getDefaultMenuTitlePreset()))
: (isMenuTitlePresetValue(presetMenuTitle) ? presetMenuTitle : $('#rmSettingsMenuTitle').val()),
signatureSeparator: $('#rmSettingsSignatureSeparator').val(),
excludedNamespaces: namespaces.values,
disabledItems: disabledItems.values,
quickPhrases: opts.skipQuickPhraseCommit ? collectQuickPhraseValuesSnapshot() : collectQuickPhraseValues()
})
};
}
function updateSettingsSubmitReadyState(baselineSettings) {
var collected = collectSettingsFormValues({ skipQuickPhraseCommit: true });
var hasChanges = !!(collected.error || (collected.value && !areRemoverSettingsEqual(collected.value, baselineSettings)));
$('#removerSubmit').toggleClass('rmSubmitReady', hasChanges && !$('#removerSubmit').hasClass('rmSubmitError'));
$('#rmSettingsUnsavedHint').css('display', hasChanges ? 'inline-block' : 'none');
}
function bindSettingsSubmitReadyState(baselineSettings) {
var update = function () {
setTimeout(function () { updateSettingsSubmitReadyState(baselineSettings); }, 0);
};
$('#rmSettingsForm').off('.rmSettingsReady').on('input.rmSettingsReady change.rmSettingsReady', 'input, textarea, select', update);
$('#removerModalContent').off('click.rmSettingsReady keyup.rmSettingsReady').on(
'click.rmSettingsReady keyup.rmSettingsReady',
'.rmSettingsMenuPresetBtn,.rmQuickPhraseEditBtn,.rmQuickPhraseRemoveBtn,.rmQuickPhraseChip,#rmSettingsQuickPhraseInput',
update
);
$('#rmSettingsQuickPhrasesEditor').off('rmQuickPhrasesChanged.rmSettingsReady').on('rmQuickPhrasesChanged.rmSettingsReady', update);
updateSettingsSubmitReadyState(baselineSettings);
}
function buildSettingsFormHtml(menuLabelsHint) {
var menuTitleFieldLabel = isMenuTitlePresetOnlySkin() ? 'Размещение меню' : 'Заголовок отдельного меню';
var menuFields =
buildSettingsFieldHtml(menuTitleFieldLabel,
'<input id="rmSettingsMenuTitle" type="text" style="' + stInputFull + 'margin-bottom:0;">' + buildMenuTitlePresetButtonsHtml(),
getMenuTitlePresetHintText(), { forId: 'rmSettingsMenuTitle' }) +
buildSettingsFieldHtml('Визуальное оформление меню',
'<div class="rmSettingsChecks">' + buildSettingsSimpleCheckboxHtml('rmSettingsShowMenuIcons', 'Эмодзи в пунктах меню') + '</div>');
var messageFields =
buildSettingsFieldHtml('Префикс перед подписью',
'<input id="rmSettingsSignatureSeparator" type="text" style="' + stInputFull + 'margin-bottom:0;">',
'Добавляется перед подписью в публикуемых сообщениях.<span style="display:block;margin-top:6px;">Предпросмотр: <code id="rmSettingsSignaturePreviewCode"></code>.</span>',
{ forId: 'rmSettingsSignatureSeparator' }) +
buildSettingsFieldHtml('Часто используемые фразы', buildQuickPhrasesSettingsEditorHtml(),
'Enter для добавления. Порядок элементов изменяется перетаскиванием.', { forId: 'rmSettingsQuickPhraseInput' });
var defaultFields = '<div class="rmSettingsChecks">' +
buildSettingsSimpleCheckboxHtml('rmSettingsNotifyAuthor', 'Оповещать создателя страницы') +
buildSettingsSimpleCheckboxHtml('rmSettingsSubscribeTopic', 'Подписываться на номинацию') +
buildSettingsSimpleCheckboxHtml('rmSettingsOpenNominationDiscussionInNewTab', 'Открывать номинацию в отдельной вкладке') +
buildSettingsSimpleCheckboxHtml('rmSettingsAutoReloadAfterAction', 'Автообновление страницы после размещения<span style="display:block;margin-top:3px;color:' + tk.cSubM + ';font-size:12px;line-height:1.35;">Помешает взаимодействовать с логом действий.</span>') + '</div>';
var disableFields =
buildSettingsFieldHtml('Скрыть пункты меню',
'<input id="rmSettingsDisabledItems" type="text" style="' + stInputFull + 'margin-bottom:0;">',
'Названия пунктов через запятую, например <code>КБУ, КУЛ, КОБ</code>.' + menuLabelsHint,
{ forId: 'rmSettingsDisabledItems' }) +
buildSettingsFieldHtml('Не показывать в пространствах имён',
'<input id="rmSettingsExcludedNamespaces" type="text" style="' + stInputFull + 'margin-bottom:0;">',
'Номера пространств имён через запятую, например <code>2, 10, 828</code>. См. <a href="' + getPageUrl('Википедия:Пространства имён') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Википедия:Пространства имён</a>.',
{ forId: 'rmSettingsExcludedNamespaces' });
return joinHtml([
'<div id="rmSettingsForm" style="max-width:100%;">',
'<div class="rmSettingsLead">Настройки интерфейса и значений по умолчанию.</div>',
buildSettingsSectionHtml('Меню', menuFields, 'Настройки внешнего вида и состава меню Remover.'),
buildSettingsSectionHtml('Оформление сообщений', messageFields, 'Настройки оформления публикуемых сообщений в номинациях.'),
buildSettingsSectionHtml('Опции по умолчанию', defaultFields, 'Регулирует исходное состояние и поведение после выполнения действий.'),
buildSettingsSectionHtml('Отключение', disableFields, 'Скрывает отдельные пункты меню Remover или всё меню в выбранных пространствах имён.'),
'</div>'
]);
}
function buildSettingsFooterLeftHtml() {
return joinHtml([
'<div id="rmSettingsFooterLeft" style="display:flex;align-items:center;gap:6px;flex:1 1 auto;min-width:0;max-width:100%;flex-wrap:wrap;margin-right:auto;">',
'<button id="rmSettingsResetFooter" type="button" title="Удаляет сохранённые настройки Remover из вашего профиля MediaWiki и возвращает значения по умолчанию." style="', stCancel, '">Сбросить все настройки</button>',
'<a id="rmSettingsReportIssue" href="', getPageUrl('Обсуждение Википедии:Гаджеты/Remover'), '" target="_blank" rel="noopener noreferrer" ',
'title="Сообщить о проблеме или предложить улучшение" aria-label="Сообщить о проблеме или предложить улучшение" ',
'class="removerModalLink rmButtonLikeLink" style="', stCancel, 'display:inline-flex;align-items:center;justify-content:center;text-align:center;text-decoration:none;box-sizing:border-box;max-width:100%;line-height:1.2;word-break:normal;overflow-wrap:normal;">Обратная связь</a>',
'</div>'
]);
}
function openSettings() {
var currentSettings = normalizeRemoverSettings(state.settings || settingsDefaults);
var menuLabelsHint = buildSettingsMenuItemsHint();
var $previousModal = $('#removerModal').length ? $('#removerModal').detach() : $();
var previousLayoutSyncHandlers = modalLayoutSyncHandlers.slice();
function restorePreviousModal() {
closeModal();
if ($previousModal.length) {
$('#content').prepend($previousModal);
modalLayoutSyncHandlers = previousLayoutSyncHandlers.slice();
if (modalLayoutSyncHandlers.length) $(window).off('resize.removerModal').on('resize.removerModal', syncModalLayout);
syncModalLayout();
syncLinkWidths();
}
}
createModal({
title: 'Конфигурация',
width: 'compact',
showSettingsButton: false
});
$('#removerModal').addClass('rmModalSettings');
$('#removerModalHeaderBar').append(buildHeaderIconButtonHtml('rmSettingsBack', 'Назад', 'Назад', '←'));
$('#rmSettingsBack').on('click', restorePreviousModal);
$('#removerModalContent').html(buildSettingsFormHtml(menuLabelsHint));
fillSettingsFormValues(currentSettings);
bindMenuTitlePresetControls();
bindSignatureSeparatorPreview();
bindQuickPhrasesEditor();
renderModalFooter('submit', {
showCheckbox: false,
submitText: 'Сохранить',
onSubmit: function () {
var collected = collectSettingsFormValues();
var shouldReset;
var saveFn;
if (collected.error) {
alert(collected.error);
return false;
}
shouldReset = areRemoverSettingsEqual(collected.value, settingsDefaults);
saveFn = shouldReset
? function (callback) { resetSettingsOnServer(callback); }
: function (callback) { saveSettingsToServer(collected.value, callback); };
saveFn(function (err) {
if (err) {
alert('Не удалось ' + (shouldReset ? 'сбросить' : 'сохранить') + ' настройки: ' + (err.info || err.code || 'неизвестная ошибка') + '.');
unlockModalSubmit();
return;
}
location.reload();
});
}
});
var $settingsActions = $('#rmFooterActionButtons');
$settingsActions.wrapInner('<div id="rmSettingsActionButtonsRow"></div>');
$settingsActions.append('<span id="rmSettingsUnsavedHint" role="status" aria-live="polite">Есть несохранённые изменения</span>');
bindSettingsSubmitReadyState(currentSettings);
$('#rmFooterButtons').css('justify-content', 'space-between').prepend(buildSettingsFooterLeftHtml());
$('#rmSettingsResetFooter').on('click', function () {
fillSettingsFormValues(settingsDefaults);
updateSettingsSubmitReadyState(currentSettings);
$('#removerSubmit').trigger('focus');
});
}
// ─── Завершение обработки ────────────────────────────────────────────────
function finalizeSuccess(nominationInfo, usePageReload) {
if (isError) {
var $box = $('#rmLogBox').length ? $('#rmLogBox') : $('#removerModalContent');
$box.append('<p class="error">При выполнении скрипта произошли ошибки.</p>');
markSubmitError();
return;
}
renderModalFooter('reload');
if (nominationInfo && nominationInfo.pageTitle) {
appendNominationLink(nominationInfo.pageTitle, nominationInfo.sectionTitle);
}
if (!usePageReload && !nominationInfo) location.reload();
}
function finalizeFastRemoval(notifiedPages, summary) {
if (isError || !setAlert || !notifiedPages || !notifiedPages.length) {
finalizeSuccess(null, false);
return;
}
notifyAuthorsForPages(notifiedPages, {
summary: summary,
actionText: 'к быстрому удалению'
}, function () {
finalizeSuccess(null, false);
});
}
// ─── Общий runner ────────────────────────────────────────────────────────
/**
* Универсальный запуск полного пайплайна номинации.
* @param {Object} o
* templateStep — функция (next) → обработка шаблонов на статьях
* nominationStep — функция (done) → публикация номинации, done(err, {pageTitle, sectionTitle})
* notifyStep — функция (nominationInfo, next)
* skipNotify — boolean
* skipLink — boolean, не показывать ссылку на номинацию
*/
function runFlow(o) {
runNominationPipeline({
templateStep: o.templateStep,
nominationStep: o.nominationStep,
notifyStep: o.notifyStep || function (info, next) { next(); },
skipNotify: o.skipNotify,
onSuccess: function (ctx) {
if (isError) { markSubmitError(); return; }
renderModalFooter('reload');
if (!o.skipLink && ctx.nominationInfo && ctx.nominationInfo.pageTitle) {
appendNominationLink(ctx.nominationInfo.pageTitle, ctx.nominationInfo.sectionTitle);
maybeOpenNominationDiscussionInNewTab(ctx.nominationInfo);
}
},
onFailure: function () { markSubmitError(); }
});
}
// ═══════════════════════════════════════════════════════════════════════════
// ЯДРО: обработка статей (apply template + nomination page)
// ═══════════════════════════════════════════════════════════════════════════
/**
* Применяет шаблон к одной статье/категории.
* Понимает режим inArticle (вставка через <noinclude>),
* режим closeAction (снятие шаблона + запись на СО),
* режим cleanupAction (снятие КБУ/КУЛ).
*
* @param {string} pg — название страницы
* @param {Object} job — параметры задания (см. buildJob)
* @param {function} callback(err, meta)
*/
function applyTemplateToPage(pg, job, callback) {
var mode = job.mode;
// ── Снятие КБУ/КУЛ ──────────────────────────────────────────────────
if (mode === 'cleanup') {
var tm = job.transferMode || 'none';
if (tm === 'none') { callback({ code: 'error', info: 'Не выбран режим снятия шаблонов.' }); return; }
editPageContent(pg, { summary: job.summary, watchlist: 'nochange', readErrorCode: 'error', readError: 'Страница «' + pg + '» не существует.' },
function (article, done) {
var local = removeTransferTemplatesLocal(article, tm);
removeTransferTemplatesWithApiFallback(pg, local.text, tm, local, function (updated) {
if (updated.text === article) { done({ error: { code: 'error', info: 'Шаблоны для снятия не найдены.' } }); return; }
done({ text: updated.text });
});
}, function (err) { callback(err); });
return;
}
// ── Подведение итогов по КУ/КПМ (снятие + итог на СО) ───────────────
if (mode === 'denom') {
getTextWithTimestamp(pg, function (article, baseTimestamp, readErr) {
if (readErr) { callback(makeReadError(readErr, 'read_failed', 'Не удалось получить содержимое страницы «' + pg + '».')); return; }
if (article === null) { callback({ code: 'error', info: 'Страница «' + pg + '» не существует.' }); return; }
if (!job.sourceTemplate) { callback({ code: 'error', info: 'Не задан шаблон для снятия.' }); return; }
var tplPattern = buildTemplateNamePattern(job.sourceTemplate);
var tpl = findTemplateByPattern(article, tplPattern);
if (!tpl) { callback({ code: 'error', info: 'Невозможно снять шаблон «' + job.sourceTemplate + '».' }); return; }
var normalizedTplDate = convertToStandardDate(tpl.params[0]);
var tplExtraParams = tpl.params.slice(1);
var tplExtra = tplExtraParams.join('|').trim();
var renameTargets = collectRenameTargetsFromTemplateParams(tplExtraParams);
var isRedirectedDeletion = job.closeType === 'redirectedDeletion';
if (!RE_DATE_ISO.test(normalizedTplDate)) {
callback({ code: 'error', info: 'Не удалось распознать дату в шаблоне: «' + (tpl.params[0] || '') + '».' });
return;
}
if (isRedirectedDeletion && !job.redirectTarget) {
callback({ code: 'error', info: 'Не указана цель перенаправления.' });
return;
}
var date = getDate(normalizedTplDate);
var nomPlace = 'ВП:' + job.sourceTemplate.replace(/\|.*/, '') + '/' + date[1];
var retTalkSection = '';
var sectionNW, tplpar;
if (job.closeType === 'doneRnm') { sectionNW = job.oldTitle + ' → ' + pg; tplpar = job.oldTitle + '|' + pg; }
if (job.closeType === 'noRnm') {
if (!renameTargets.length) {
callback({ code: 'error', info: 'В шаблоне КПМ не найдено новое название.' });
return;
}
sectionNW = pg + ' → ' + renameTargets.join(', ');
tplpar = pg + '|' + renameTargets.join('|');
}
if (job.closeType === 'ret' || job.closeType === 'retConditional' || job.closeType === 'withdrawnDeletion' || isRedirectedDeletion) {
retTalkSection = tplExtra;
sectionNW = retTalkSection || pg;
tplpar = retTalkSection ? ('l1=' + retTalkSection) : '';
}
var editResultText = job.resultTemplate + (isRedirectedDeletion ? ' на [[' + job.redirectTarget + ']]' : '');
var editSummary = makeSummary('номинация [[' + (nomPlace ? nomPlace + '#' : '') + sectionNW + ']] — ' + editResultText);
var talkTitle = getTalkPage(pg);
getTextWithTimestamp(talkTitle, function (talkText, talkTimestamp, talkReadErr) {
if (talkReadErr) { callback(makeReadError(talkReadErr, 'talk_read_failed', 'Не удалось получить содержимое СО страницы «' + pg + '».')); return; }
var sourceTalkText = talkText || '';
var talkPageMissing = talkText === null;
var dateSectionTalkTemplate = getDateSectionTalkTemplateConfig(job.closeType);
var talkResult = dateSectionTalkTemplate
? upsertDateSectionTemplateOnTalkPage(sourceTalkText, dateSectionTalkTemplate.name, date[0], retTalkSection, dateSectionTalkTemplate)
: (job.closeType === 'retConditional')
? upsertConditionalRetTemplateOnTalkPage(sourceTalkText, date[0], retTalkSection, job.conditionalReason, job.conditionalDeadline)
: upsertSingleDateResultTemplateOnTalkPage(sourceTalkText, job.resultTemplate, date[0], tplpar);
function saveArticle() {
var cleaned = isRedirectedDeletion ? buildRedirectText(job.redirectTarget) : stripTemplatesByPattern(article, tplPattern).text;
var ep = { title: pg, text: cleaned, summary: editSummary, watchlist: 'nochange', assertuser: mwCfg.wgUserName };
if (baseTimestamp) ep.basetimestamp = baseTimestamp;
apiReq(ep, 'edit', function (t) {
var editErr = t && t.error ? t.error : null;
callback(editErr, editErr ? null : {
discussionPage: nomPlace,
discussionSection: sectionNW,
summary: editSummary,
successMessage: isRedirectedDeletion
? 'Содержимое страницы ' + buildQuotedStatusPageLink(pg) + ' очищено, установлено перенаправление на ' + buildQuotedStatusPageLink(job.redirectTarget) + '.'
: ''
});
});
}
if (talkResult.text === sourceTalkText) { saveArticle(); return; }
var talkEp = { title: talkTitle, text: talkResult.text, summary: editSummary, watchlist: 'nochange', assertuser: mwCfg.wgUserName };
if (talkPageMissing) talkEp.createonly = true;
else if (talkTimestamp) talkEp.basetimestamp = talkTimestamp;
apiReq(talkEp, 'edit', function (talkResp) {
if (talkResp && talkResp.error) { callback({ code: 'talk_failed', info: 'Не удалось записать итог на СО: ' + talkResp.error.info }); return; }
saveArticle();
});
});
});
return;
}
// ── Обычная номинация: вставка шаблона в статью ─────────────────────
// mode === 'nominate'
var isKu = job.opId === 'tRm' || job.opId === 'mRm';
var conflictRule = getNominationConflictRule(job);
var conflictLabel = conflictRule ? conflictRule.label : 'номинации';
editPageContent(pg, { summary: job.summary, readErrorCode: 'error', readError: 'Страница «' + pg + '» не существует.' },
function (article, done) {
var hasExistingNomination = conflictRule && conflictRule.detect(article);
var conflictDecision = getConflictDecisionForPage(job, pg);
function buildResult(finalText) {
var generatedTpl = buildGeneratedNominationTemplateText(job, pg);
return { text: generatedTpl ? wrapInNoinclude(finalText, generatedTpl) : finalText };
}
function buildResultAfterMoveToCommonsCleanup(sourceText) {
var localMove = removeMoveToCommonsTemplatesLocal(sourceText);
removeMoveToCommonsTemplatesWithApiFallback(pg, localMove.text, localMove, function (updated) {
done(buildResult(updated.text));
});
}
function finishConflictResolution(sourceText) {
var resolvedText;
var pageLink = buildQuotedStatusPageLink(pg);
if (conflictDecision.templateAction === 'keep') {
if (sourceText !== article) {
return {
text: sourceText,
meta: { successMessage: 'Шаблон ' + conflictLabel + ' на странице ' + pageLink + ' оставлен без изменений; шаблоны переноса сняты.' }
};
}
return { skip: true, meta: { successMessage: 'Шаблон ' + conflictLabel + ' на странице ' + pageLink + ' оставлен без изменений.' } };
}
resolvedText = applyConflictTemplateResolution(sourceText, job, pg, conflictDecision);
return {
text: resolvedText,
meta: {
successMessage: conflictDecision.templateAction === 'overwrite'
? 'Шаблон ' + conflictLabel + ' на странице ' + pageLink + ' перезаписан новой датой.'
: 'Новый шаблон ' + conflictLabel + ' добавлен сверху на странице ' + pageLink + '.'
}
};
}
if (hasExistingNomination && (!conflictDecision || conflictDecision.pageAction !== 'keep')) {
return { error: { code: 'error', info: 'На странице уже стоит шаблон ' + conflictLabel + '.' } };
}
if (hasExistingNomination && conflictDecision && conflictDecision.pageAction === 'keep') {
if (job.transferMode && job.transferMode !== 'none') {
var localConflict = removeTransferTemplatesLocal(article, job.transferMode);
removeTransferTemplatesWithApiFallback(pg, localConflict.text, job.transferMode, localConflict, function (updated) {
done(finishConflictResolution(updated.text));
});
return;
}
return finishConflictResolution(article);
}
if (isKu && job.transferMode && job.transferMode !== 'none') {
var local = removeTransferTemplatesLocal(article, job.transferMode);
removeTransferTemplatesWithApiFallback(pg, local.text, job.transferMode, local, function (updated) { done(buildResult(updated.text)); });
return;
}
if (job.removeMoveToCommons) {
buildResultAfterMoveToCommonsCleanup(article);
return;
}
return buildResult(article);
},
function (err) { callback(err); }
);
}
/**
* Обрабатывает список страниц последовательно.
* @param {string[]} pages
* @param {Object} job
* @param {function} onDone(notifiedPages, err, pageMeta)
*/
function processPageList(pages, job, onDone) {
var notifiedPages = [];
var pageMeta = {};
eachSequential(pages.slice().reverse(), function (pg, nextPage) {
var pageLink = buildQuotedStatusPageLink(pg);
var statusId = logStatus('Обрабатывается страница ' + pageLink + '...', null, { pending: true, trackError: false });
applyTemplateToPage(pg, job, function (err, meta) {
var normPg = normTitle(pg);
var isClose = job.mode === 'cleanup' || job.mode === 'denom';
if (!isClose) {
if (!err && meta && meta.successMessage) logStatus(meta.successMessage, null, { statusId: statusId, trackError: false });
else logPageEdit(pg, err, { statusId: statusId });
} else {
if (err) { logStatus('Завершение по странице ' + pageLink, err, { statusId: statusId }); }
else {
logStatus((meta && meta.successMessage) || ('Шаблон снят со страницы ' + pageLink + '.'), null, { statusId: statusId, trackError: false });
if (job.mode === 'denom') logStatus('Шаблон установлен на СО страницы ' + pageLink + '.', null, { trackError: false });
}
}
if (!err) {
notifiedPages.push(pg);
if (meta) pageMeta[normPg] = meta;
}
nextPage(err || null);
});
}, function (err) { onDone(notifiedPages, err, pageMeta); });
}
// ═══════════════════════════════════════════════════════════════════════════
// ПОСТРОЕНИЕ JOB из формы
// ═══════════════════════════════════════════════════════════════════════════
/**
* Строит объект job из данных формы для операции номинации (tRm, rnm, imp, merge, split, recov).
* @param {Object} op — запись из OPERATIONS
* @param {string} pg — целевая страница (уже разрешённая)
* @param {boolean} isMulti — режим мультиноминации
* @returns {Object|false} — job или false при ошибке ввода
*/
function buildNominationJob(op, pg, isMulti) {
var nom = op.nomination;
var date = getDate();
var msg = normalizeQuickPhraseValue($('#rmMsg').val());
var rawMsg = msg;
var opId = isMulti ? (nom.multiOpId || op.id) : op.id;
var tplpar = '';
var section, sectionNW, extraPages, multiArticles = [];
var multiHeaderText = '';
var multiArticleComments = {};
var multiFormat = 'sections';
var multiRenamePairs = [];
var multiRenameTargets = {};
var multiRenameTemplateTargets = {};
var isRenameWithRowTargets = isMulti && nom.extraInput && nom.extraInput.type === 'rename' && $('.rmMultiRenameTargetInput').length;
// Вычислить section и tplpar в зависимости от типа дополнительного ввода
if (nom.extraInput) {
var ei = nom.extraInput;
if (ei.type === 'rename') {
if (isRenameWithRowTargets) {
multiRenamePairs = collectMultiRenamePairs();
if (!validateMultiRenamePairs(multiRenamePairs, 'статью', 'новое название')) return false;
multiRenameTargets = buildMultiRenameTargetMap(multiRenamePairs, 'targetNames');
multiRenameTemplateTargets = buildMultiRenameTargetMap(multiRenamePairs, 'templateTargetNames');
tplpar = buildRenameTemplateParam(multiRenamePairs[0].templateTargetNames);
section = formatRenameItemLabel(pg, multiRenameTargets[normTitle(pg)] || multiRenamePairs[0].targetNames);
} else {
var rn = collectInputValues('.rmRenameInput');
if (!rn.length) { alert('Укажите новое название.'); return false; }
tplpar = buildRenameTemplateParam(rn);
section = formatRenameItemLabel(pg, rn);
}
} else if (ei.type === 'merge') {
var mn = collectInputValues('.rmMergeInput');
if (!mn.length) { alert('Укажите статью для объединения.'); return false; }
tplpar = pg + '|' + mn.join('|');
extraPages = mn;
section = formatPagesWithAnd([pg].concat(mn));
} else if (ei.type === 'split') {
var sn = collectInputValues('.rmSplitInput');
if (!sn.length) { alert('Укажите статьи для разделения.'); return false; }
tplpar = formatPagesWithAnd(sn);
section = '[[:' + pg + ']] → ' + tplpar;
}
}
if (isMulti) {
var ttl = $('#rmHeader').val() || '';
var articles = isRenameWithRowTargets
? multiRenamePairs.map(function (pair) { return pair.pageName; })
: collectInputValues('.rmMultiPageInput');
multiFormat = $('.rmArticleMultiFormatBtn.is-active').data('rmMultiFormat') || 'sections';
multiArticleComments = collectMultiNominationComments();
multiHeaderText = ttl;
multiArticles = articles.slice();
if (!articles.length) { alert('Укажите страницу.'); return false; }
if (!validateMultiNominationText(articles, rawMsg, multiArticleComments, 'страницы')) return false;
section = ttl || (isRenameWithRowTargets ? formatRenameItemsWithAnd(articles, multiRenameTargets) : '');
msg = multiFormat === 'list'
? buildMultiNominationListText(articles, rawMsg, multiArticleComments, isRenameWithRowTargets ? getMultiRenameDiscussionOptions(multiRenameTargets) : null)
: buildMultiNominationText(articles, rawMsg, multiArticleComments, isRenameWithRowTargets
? getMultiRenameDiscussionOptions(multiRenameTargets, { leadingBlankLine: false })
: { leadingBlankLine: false });
}
if (!section) section = '[[:' + pg + ']]';
sectionNW = section.replace(/\[\[:/g, '').replace(/]]/g, '');
var nomPageDate = date[1];
var nomPage = nom.nomPage(nomPageDate);
var summaryAction = getNominationSummaryAction(nom);
var summary = makeNominationTargetSummary(nomPage, sectionNW, summaryAction);
return {
mode: 'nominate',
opId: opId,
op: op,
date: date,
tplpar: tplpar,
articleTpl: nom.articleTpl || function () { return ''; },
inArticle: nom.inArticle !== false,
transferMode: (nom.supportsTransfer ? getTransferModeFromButtons() : 'none'),
summary: summary,
nominationSummary: makeNominationPageSummary(sectionNW),
summaryAction: summaryAction,
msg: msg,
nomPage: nomPage,
navTemplate: nom.navTemplate,
section: section,
sectionNW: sectionNW,
comment: nom.comment || '',
extraPages: extraPages || [],
isMulti: !!isMulti,
multiHeaderText: multiHeaderText,
multiNominationBody: rawMsg,
multiArticleComments: multiArticleComments,
multiNominationFormat: multiFormat || 'sections',
multiRenamePairs: multiRenamePairs,
multiRenameTargets: multiRenameTargets,
multiRenameTemplateTargets: multiRenameTemplateTargets,
multiArticles: multiArticles,
pages: isMulti ? multiArticles.slice().reverse() : ([pg].concat(extraPages || []))
};
}
function getTransferModeFromButtons() {
var kbu = $('#rmTransferBtnKbu').hasClass('is-active');
var kul = $('#rmTransferBtnKul').hasClass('is-active');
if (kbu && kul) return 'both';
if (kbu) return 'kbu';
if (kul) return 'kul';
return 'none';
}
function normalizeFastRemoveTemplateKey(templateName) {
return String(templateName || '').replace(/\s+/g, ' ').trim().toLowerCase();
}
function getBuiltinFastRemoveParamConfig(templateName) {
var key = normalizeFastRemoveTemplateKey(templateName);
var bareKey = key.replace(/^(?:подст|subst)\s*:\s*/, '');
if (/^(?:nld|no license|nsd|no source|nad|no author|dd|disputed|npd|no permission|ofud|orphaned-fairuse|nothost|db-nothost)$/.test(bareKey)) return { type: 'date' };
if (/^(?:dfud|disputed-fairuse)$/.test(bareKey)) return { type: 'dfud' };
if (key === 'db-badfairuse') return { type: 'badfairuse' };
if (key === 'rct' || key === 'замена с викисклада') return { type: 'commonsReplacement' };
if (key === 'nct' || key === 'перенесено на викисклад') return { type: 'nct' };
if (key === 'db-duplicate') return { type: 'duplicate' };
return null;
}
function buildKbuReasonButtonsHtml(items, buttonClass, dataAttr, titlePrefix) {
return items.map(function (item) {
var label = titlePrefix + ' ' + item[0] + ': ' + item[1];
return '<button type="button" class="rmToggleBtn rmKbuReasonPill ' + buttonClass + '" ' + dataAttr + '="' +
escapeHtml(item[0]) + '" aria-pressed="false" title="' + escapeHtml(label) +
'"><span class="rmKbuReasonNum">' + escapeHtml(item[0]) + '</span>' +
'<span class="rmKbuReasonText">' + escapeHtml(item[1]) + '</span></button>';
}).join('');
}
function buildDfudReasonButtonsHtml() {
return buildKbuReasonButtonsHtml(DFUD_REASON_OPTIONS, 'rmKbuDfudReasonBtn', 'data-rm-dfud-reason', 'п.');
}
function buildBadfairuseReasonButtonsHtml() {
return buildKbuReasonButtonsHtml(BADFAIRUSE_OPTIONS, 'rmKbuBadfairuseReasonBtn', 'data-rm-badfairuse-reason', 'пример');
}
function buildKbuReasonListHtml(id, hint, buttonsHtml) {
return '<div id="' + id + '" class="' + RESIZE_CLASS + '" style="display:none;margin-bottom:10px;">' +
'<div style="margin-bottom:6px;font-size:14px;line-height:1.4;color:' + tk.cSubM + ';">' + hint + '</div>' +
'<div style="display:flex;flex-direction:column;gap:4px;">' + buttonsHtml + '</div>' +
'</div>';
}
function collectSelectedDfudReasons() {
return $('#rmKbuDfudReasonsWrap .rmKbuDfudReasonBtn.is-active').map(function () {
return String($(this).attr('data-rm-dfud-reason') || '').trim();
}).get().filter(Boolean);
}
function getSelectedBadfairuseReason() {
return String($('#rmKbuBadfairuseReasonsWrap .rmKbuBadfairuseReasonBtn.is-active').first().attr('data-rm-badfairuse-reason') || '').trim();
}
function getCurrentCommonsReplacementMode() {
var mode = $('#rmKbuCommonsReplacementModeWrap .rmKbuCommonsReplacementModeBtn.is-active').data('rmCommonsReplacementMode');
return mode || 'file';
}
function setCurrentCommonsReplacementMode(mode) {
var value = mode || 'file';
$('#rmKbuCommonsReplacementModeWrap .rmKbuCommonsReplacementModeBtn').each(function () {
var active = $(this).data('rmCommonsReplacementMode') === value;
$(this).toggleClass('is-active', active).attr('aria-pressed', active ? 'true' : 'false');
});
if (value === 'category') {
$('#fiRm').attr('placeholder', 'Категория на Викискладе без Category:');
$('#fiRmExtra').attr('placeholder', 'Статья в руВики о теме категории (необязательно)').show();
} else {
$('#fiRmExtra').hide();
$('#fiRm').attr('placeholder', value === 'gallery' ? 'Галерея на Викискладе' : 'Имя файла на Викискладе');
}
syncModalLayout();
}
function getFastRemoveLegacyParamConfig(templateName) {
var params = cfg.requiredParamTemplates || {};
var key;
if (Object.prototype.hasOwnProperty.call(params, templateName)) return params[templateName];
for (key in params) {
if (Object.prototype.hasOwnProperty.call(params, key) &&
normalizeFastRemoveTemplateKey(key) === normalizeFastRemoveTemplateKey(templateName)) {
return params[key];
}
}
return null;
}
function getFastRemoveParamConfig(reason) {
var template = reason && reason[0];
return getBuiltinFastRemoveParamConfig(template) || getFastRemoveLegacyParamConfig(template);
}
function appendTemplateParam(tpl, name, value) {
var val = normalizeQuickPhraseValue(value);
if (!val) return tpl;
return tpl + '|' + (name ? name + '=' : '') + val;
}
function makeFastRemovalSummary(reason, data) {
var label = String(reason && reason[1] || '').trim();
var detail = String(data && data.summaryDetail || '').trim();
var summaryReason = (label === 'Особый случай' && detail) ? detail : (label || detail);
return makeSummary('номинация к быстрому удалению' + (summaryReason ? ': ' + summaryReason : ''));
}
function buildFastRemoveTemplateData(reason) {
var template = reason && reason[0];
var paramCfg = getFastRemoveParamConfig(reason);
var addInfo = normalizeQuickPhraseValue($('#fiRm').val());
var comment = normalizeQuickPhraseValue($('#fiRmComment').val());
var dateText = getDate()[0];
var mode;
var extra;
var tpl;
var values;
if (!template) return { error: 'Не выбрана причина быстрого удаления.' };
if (paramCfg && paramCfg.type === 'date') {
return { templateText: appendTemplateParam(template, '1', dateText) };
}
if (paramCfg && paramCfg.type === 'dfud') {
tpl = appendTemplateParam(template, 'date', dateText);
values = collectSelectedDfudReasons();
values.forEach(function (value, index) { tpl = appendTemplateParam(tpl, String(index + 1), value); });
return { templateText: tpl };
}
if (paramCfg && paramCfg.type === 'badfairuse') {
addInfo = getSelectedBadfairuseReason();
if (!addInfo) return { error: 'Выберите пример из ВП:ПНИ для Ф6.' };
return { templateText: appendTemplateParam(template, '1', addInfo) };
}
if (paramCfg && paramCfg.type === 'commonsReplacement') {
mode = getCurrentCommonsReplacementMode();
if (mode === 'category') {
if (!addInfo) return { error: 'Укажите категорию на Викискладе.' };
tpl = appendTemplateParam('Замена с Викисклада/Категория', '1', addInfo);
extra = normalizeQuickPhraseValue($('#fiRmExtra').val());
return { templateText: appendTemplateParam(tpl, '2', extra) };
}
if (mode === 'gallery') {
if (!addInfo) return { error: 'Укажите галерею на Викискладе.' };
return { templateText: appendTemplateParam('Замена с Викисклада/Галерея', '1', addInfo) };
}
return { templateText: appendTemplateParam(template, '1', addInfo) };
}
if (paramCfg && paramCfg.type === 'nct') {
return {
templateText: appendTemplateParam(template, '1', addInfo),
removeMoveToCommons: $('#rmKbuRemoveMoveToCommons').is(':checked')
};
}
if (paramCfg && paramCfg.type === 'duplicate') {
if (!addInfo) return { error: 'Укажите имя файла-оригинала для Ф1.' };
tpl = appendTemplateParam(template, '1', addInfo);
return { templateText: appendTemplateParam(tpl, 'comment', comment) };
}
tpl = template;
if (typeof paramCfg === 'string') {
var noComment = paramCfg.charAt(0) === '!';
tpl = appendTemplateParam(tpl, '1', addInfo);
if (!noComment) tpl = appendTemplateParam(tpl, addInfo ? '2' : '1', comment);
return { templateText: tpl, summaryDetail: noComment ? addInfo : '' };
}
return { templateText: appendTemplateParam(tpl, '1', comment) };
}
function resetKbuReasonControls() {
$('#fiRm').hide().val('').attr({ type: 'text', placeholder: '' });
$('#fiRmExtra').hide().val('');
$('#rmKbuCommonsReplacementModeWrap').hide();
$('#rmKbuDfudReasonsWrap').hide().find('.rmKbuDfudReasonBtn').removeClass('is-active').attr('aria-pressed', 'false');
$('#rmKbuBadfairuseReasonsWrap').hide().find('.rmKbuBadfairuseReasonBtn').removeClass('is-active').attr('aria-pressed', 'false');
$('#rmKbuDateHint').hide().text('');
$('#rmKbuRemoveMoveToCommons').prop('checked', false);
$('#rmKbuRemoveMoveToCommonsWrap').hide();
$('#fiRmComment').hide().val('');
$('.rmQuickPhrasesPanel[data-rm-target="fiRmComment"]').hide();
}
function toggleKbuComment(show) {
$('#fiRmComment').toggle(show);
$('.rmQuickPhrasesPanel[data-rm-target="fiRmComment"]').toggle(show);
}
function updateKbuReasonControls(reasons) {
var reason = reasons[$('#rmSel').val()] || reasons[0];
var paramCfg = getFastRemoveParamConfig(reason);
$('#rmKbuCriteriaLinkWrap').html(buildFastRemoveCriteriaLinkHtml(reason));
resetKbuReasonControls();
if (paramCfg && (paramCfg.type === 'date' || paramCfg.type === 'dfud')) {
$('#rmKbuDateHint').text(FILE_DATE_DEADLINE_HINT).show();
if (paramCfg.type === 'dfud') $('#rmKbuDfudReasonsWrap').show();
return;
}
if (paramCfg && paramCfg.type === 'badfairuse') {
$('#rmKbuBadfairuseReasonsWrap').show();
return;
}
if (paramCfg && paramCfg.type === 'commonsReplacement') {
$('#rmKbuCommonsReplacementModeWrap').show();
$('#fiRm').show();
setCurrentCommonsReplacementMode(getCurrentCommonsReplacementMode());
return;
}
if (paramCfg && paramCfg.type === 'nct') {
$('#fiRm').attr('placeholder', 'Укажите имя файла на Викискладе').show();
$('#rmKbuRemoveMoveToCommonsWrap').css('display', 'inline-flex');
return;
}
if (paramCfg && paramCfg.type === 'duplicate') {
$('#fiRm').attr('placeholder', 'Укажите имя файла-оригинала').show();
toggleKbuComment(true);
return;
}
if (typeof paramCfg === 'string') {
var noComment = paramCfg.charAt(0) === '!';
$('#fiRm').attr('placeholder', 'Укажите ' + (noComment ? paramCfg.substring(1) : paramCfg)).show();
toggleKbuComment(!noComment);
return;
}
toggleKbuComment(true);
}
function getKbuReasonGroupKey(reason) {
var label = String(reason && reason[1] || '');
var m = label.match(/^\s*([А-ЯЁA-Z]{1,3})\d/);
return m ? m[1] : label;
}
function buildKbuReasonOptionsHtml(reasons) {
var prevGroup = '';
var parts = [];
(reasons || []).forEach(function (reason, index) {
var group = getKbuReasonGroupKey(reason);
if (index && group !== prevGroup) parts.push('<hr>');
parts.push('<option value="' + index + '">' + escapeHtml(reason[1]) + '</option>');
prevGroup = group;
});
return parts.join('');
}
function buildKbuFormHtml(reasons) {
return joinHtml([
'<select id="rmSel" style="', stInputFull, '">',
buildKbuReasonOptionsHtml(reasons),
'</select>',
'<div id="rmKbuDateHint" class="', RESIZE_CLASS, '" style="display:none;margin:-2px 0 10px;font-size:12px;line-height:1.35;color:', tk.cSubM, ';"></div>',
'<div id="rmKbuCommonsReplacementModeWrap" class="', RESIZE_CLASS, '" style="display:none;margin-bottom:10px;"><div class="rmSegmentedBar">',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmKbuCommonsReplacementModeBtn is-active" data-rm-commons-replacement-mode="file" aria-pressed="true">Файл</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmKbuCommonsReplacementModeBtn" data-rm-commons-replacement-mode="category" aria-pressed="false">Категория</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmKbuCommonsReplacementModeBtn" data-rm-commons-replacement-mode="gallery" aria-pressed="false">Галерея</button>',
'</div></div>',
buildKbuReasonListHtml(
'rmKbuDfudReasonsWrap',
'Выберите пункты КДИ, которым файл не соответствует. Можно выбрать несколько пунктов.',
buildDfudReasonButtonsHtml()
),
buildKbuReasonListHtml(
'rmKbuBadfairuseReasonsWrap',
'Выберите пример из ВП:ПНИ, под который подпадает файл. Можно выбрать только один пункт.',
buildBadfairuseReasonButtonsHtml()
),
'<input id="fiRm" type="text" style="', stInputFull, 'display:none;">',
'<input id="fiRmExtra" type="text" style="', stInputFull, 'display:none;">',
'<label id="rmKbuRemoveMoveToCommonsWrap" class="', RESIZE_CLASS, '" style="', stFooterCheckLabel.replace('display:inline-flex;', 'display:none;'), 'margin:-2px 0 10px;">',
'<input id="rmKbuRemoveMoveToCommons" type="checkbox" style="margin:2px 0 0;flex-shrink:0;">',
'Снять шаблон «Перенести на Викисклад»',
'</label>',
'<input id="fiRmComment" type="text" placeholder="Комментарий (необязательно)" style="', stInputFull, '">',
buildQuickPhrasesPanelHtml('fiRmComment')
]);
}
function buildNominationMultiHeaderHtml(pg, options) {
var opts = options || {};
var multiHeaderRowStyle = stRow.replace('margin-bottom:6px;', 'margin-bottom:0;');
return joinHtml([
'<div id="rmMultiHeader" class="', RESIZE_CLASS, '" style="display:none;margin-bottom:6px;">',
'<div class="rmMultiPageRow rmNominationHeaderRow" style="', multiHeaderRowStyle, '"><input id="rmHeader" type="text" placeholder="Заголовок номинации" style="', stInputBox, '">',
buildAddMultiPageButtonHtml(opts), '</div>',
'</div>',
'<div id="rmMultiPagesContainer" style="display:flex;flex-direction:column;gap:', multiNominationGap, ';margin-bottom:', multiNominationGap, ';">',
buildMultiPageRowHtml(0, $.extend({}, opts, { rowId: 'rmFirstMultiPage', pageValue: pg, showAdd: true })),
'</div>'
]);
}
function buildTransferBoxHtml() {
return joinHtml([
'<div id="rmTransferBox" class="', RESIZE_CLASS, ' rmTransferPanel" style="width:100%;box-sizing:border-box;"><div class="rmTransferGrid">',
'<div id="rmTransferModeSingle" class="rmSegmentedBar"><button type="button" id="rmTransferBtnNone" class="rmSegmentedBtn rmToggleBtn is-active" aria-pressed="true">Обычная номинация</button></div>',
'<div id="rmTransferModeGroup" class="rmSegmentedBar">',
'<button type="button" id="rmTransferBtnKbu" class="rmSegmentedBtn rmToggleBtn" aria-pressed="false" title="Шаблоны db-*, уд-*, КОУ, Hangon.">Снять КБУ</button>',
'<button type="button" id="rmTransferBtnKul" class="rmSegmentedBtn rmToggleBtn" aria-pressed="false" title="Шаблон «К улучшению».">Снять КУЛ</button>',
'</div>',
'<div class="rmTransferHintRow"><div id="rmTransferHint" style="display:none;font-size:12px;line-height:1.35;color:', tk.cSubM, ';"></div></div>',
'</div></div>'
]);
}
function buildMultiNominationFormatSwitchHtml(wrapId, buttonClass) {
return joinHtml([
'<div id="', wrapId, '" class="', RESIZE_CLASS, '" style="display:none;margin-top:8px;margin-bottom:10px;">',
'<div class="rmSegmentedBar">',
'<button type="button" class="rmSegmentedBtn rmToggleBtn ', buttonClass, ' is-active" data-rm-multi-format="sections" aria-pressed="true">Оформить подразделами</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn ', buttonClass, '" data-rm-multi-format="list" aria-pressed="false">Оформить списком</button>',
'</div>',
'</div>'
]);
}
function getMultiNominationUiOptions(kind, options) {
var opts = options || {};
var isArticle = kind === 'article';
var isRename = !!opts.renameMulti;
var actionText = typeof opts.actionText === 'string'
? opts.actionText
: (opts.deletionMulti ? 'к удалению' : (isRename ? 'к переименованию' : ''));
var itemAcc = isArticle ? 'статью' : 'категорию';
var itemDat = isArticle ? 'статье' : 'категории';
var itemGen = isArticle ? 'статьи' : 'категории';
var result = {
inputPlaceholder: isArticle ? 'Статья' : 'Категория',
addTitle: 'Мультиноминация: добавить ' + itemAcc + ' в номинацию' + (actionText ? ' ' + actionText : ''),
removeTitle: 'Убрать ' + itemAcc + ' из номинации' + (actionText ? ' ' + actionText : ''),
commentTitle: 'Добавить комментарий к этой ' + itemDat,
commentExpandedTitle: 'Скрыть комментарий к этой ' + itemDat,
commentPlaceholder: 'Комментарий только для этой ' + itemGen + ' (необязательно)'
};
if (isRename) {
$.extend(result, {
targetInput: true,
targetInputClass: 'rmMultiRenameTargetInput',
targetPlaceholder: isArticle ? 'Новое название' : 'Новое название без префикса Категория:',
targetVariants: true
});
}
if (opts.setup) {
$.extend(result, {
defaultPage: opts.defaultPage || '',
multiOnlySelector: isArticle ? '#rmArticleMultiFormatWrap' : '#rmCategoryMultiFormatWrap'
});
if (isRename) $.extend(result, {
singleOnlySelector: '#rmSingleRenameBlock',
hideContainerWhenSingle: true,
singleCurrentPageSelector: '#rmSingleRenameCurrent',
singleRenameTargetSelector: isArticle ? '#rmRenameFirst' : '#firstRenameInput',
singleRenameVariantSelector: isArticle ? '#rmRenameContainer .rmRenameInput' : '#renameVariantsContainer .variantInput',
singleRenameVariantContainerId: isArticle ? 'rmRenameContainer' : 'renameVariantsContainer',
singleRenameVariantPlaceholder: isArticle ? 'Дополнительный вариант' : 'Дополнительный вариант названия',
singleRenameInputClass: isArticle ? 'rmRenameInput' : undefined
});
}
return result;
}
function buildNominationFormHtml(nom, pg, multiMode) {
var isRenameMulti = multiMode && nom.extraInput && nom.extraInput.type === 'rename';
var multiOptions = getMultiNominationUiOptions('article', {
renameMulti: isRenameMulti,
deletionMulti: multiMode && nom.template === 'к удалению'
});
return joinHtml([
isRenameMulti ? buildSingleRenameBlockHtml(nom.extraInput, 'Мультиноминация: добавить статью в номинацию', pg, 'Текущее название') : '',
multiMode ? buildNominationMultiHeaderHtml(pg, multiOptions) : '',
nom.extraInput && !isRenameMulti ? buildMultiInputHtml(nom.extraInput) : '',
'<textarea id="rmMsg" placeholder="Текст номинации без подписи"></textarea>',
buildQuickPhrasesPanelHtml('rmMsg'),
nom.supportsTransfer ? buildTransferBoxHtml() : '',
multiMode ? buildMultiNominationFormatSwitchHtml('rmArticleMultiFormatWrap', 'rmArticleMultiFormatBtn') : ''
]);
}
function buildCategoryNominationFormHtml(variantConfig, multiMode, pageName, options) {
var opts = options || {};
var multiOptions = getMultiNominationUiOptions('category', opts);
return joinHtml([
opts.renameMulti && variantConfig ? buildSingleRenameBlockHtml(variantConfig, 'Мультиноминация: добавить категорию в номинацию', pageName, 'Текущая категория') : '',
multiMode ? buildNominationMultiHeaderHtml(pageName, multiOptions) : '',
variantConfig && !opts.renameMulti && !opts.skipVariantInput ? buildMultiInputHtml(variantConfig) : '',
'<textarea id="nominationReason" placeholder="Текст номинации без подписи"></textarea>',
buildQuickPhrasesPanelHtml('nominationReason'),
multiMode ? buildMultiNominationFormatSwitchHtml('rmCategoryMultiFormatWrap', 'rmCategoryMultiFormatBtn') : ''
]);
}
function ensureMultiPageCommentTextareaResizer(textareaId, wrapId) {
var $textarea = $('#' + textareaId);
if (!$textarea.length || $textarea.data('rmNestedResizerReady')) return;
setupNestedResizableTextarea(textareaId, wrapId, parseInt(sz.taMinW, 10) || 180, 90);
$textarea.data('rmNestedResizerReady', true);
}
function setMultiPageCommentExpanded($btn, expanded) {
var wrapId = $btn.data('rmCommentWrap');
var textareaId = $btn.data('rmCommentTextarea');
var $wrap = $('#' + wrapId);
var title = expanded
? ($btn.data('rmCommentExpandedTitle') || 'Скрыть комментарий к этой странице')
: ($btn.data('rmCommentTitle') || 'Добавить комментарий к этой странице');
if (!$btn.length || !$wrap.length) return;
$btn.attr('aria-expanded', expanded ? 'true' : 'false')
.attr('aria-label', title)
.attr('title', title)
.toggleClass('is-active', expanded)
.text('✎');
$wrap.toggle(expanded);
if (expanded) ensureMultiPageCommentTextareaResizer(textareaId, wrapId);
}
function getMultiPageCommentTargets($block) {
var $textarea = $block.find('.rmMultiPageCommentInput').first();
var $wrap = $textarea.parent();
return {
wrapId: $wrap.attr('id') || '',
textareaId: $textarea.attr('id') || ''
};
}
function collectMultiRenameTargetValues($block) {
return $block.find('.rmMultiRenameTargetInput,.rmMultiRenameVariantInput').map(function () {
return ($(this).val() || '').trim();
}).get().filter(Boolean);
}
function setMultiPageRowControls($block, showAdd, showComment, options) {
var opts = options || {};
var $row = $block.find('.rmMultiPageRow').first();
var ids = getMultiPageCommentTargets($block);
var $commentBtn = $row.find('.rmMultiPageCommentToggle');
if (!$row.length) return;
if (showAdd) {
if ($commentBtn.length) setMultiPageCommentExpanded($commentBtn, false);
if (ids.wrapId) $('#' + ids.wrapId).hide();
$block.find('.rmMultiRenameVariantsContainer').hide();
$row.find('.rmMultiPageCommentToggle,.rmAddMultiRenameVariant,.rmRemoveInput').remove();
if (!$row.find('.rmAddMultiPage').length) $row.append(buildAddMultiPageButtonHtml(opts));
return;
}
$block.find('.rmMultiRenameVariantsContainer').toggle(!!opts.targetVariants);
$row.find('.rmAddMultiPage').remove();
if (!$row.find('.rmMultiPageCommentToggle').length) {
$row.append(buildMultiPageButtonsHtml(ids.wrapId, ids.textareaId, $.extend({}, opts, { showComment: showComment })));
return;
}
$row.find('.rmMultiPageCommentToggle').toggle(showComment);
}
function setupMultiPageNominationUi(options) {
var opts = options || {};
var containerSelector = opts.containerSelector || '#rmMultiPagesContainer';
var pageCounter = parseInt(opts.nextIndex, 10) || 1;
var wasMultiModeExpanded = false;
function restoreEmptySinglePageInput() {
var $pageInput = $(containerSelector + ' .rmMultiPageInput').first();
if (!$pageInput.length || String($pageInput.val() || '').trim()) return;
$pageInput.val(opts.defaultPage || '');
}
function copySingleCurrentPageToFirstRow() {
var $source = opts.singleCurrentPageSelector ? $(opts.singleCurrentPageSelector).first() : $();
var $target = $(containerSelector + ' .rmMultiPageBlock').first().find('.rmMultiPageInput').first();
var value = String(($source.val && $source.val()) || '').trim();
if (!$source.length || !$target.length || !value) return;
$target.val(value);
}
function copyFirstRowPageToSingleCurrent() {
var $target = opts.singleCurrentPageSelector ? $(opts.singleCurrentPageSelector).first() : $();
var $source = $(containerSelector + ' .rmMultiPageBlock').first().find('.rmMultiPageInput').first();
var value = String(($source.val && $source.val()) || '').trim();
if (!$source.length || !$target.length) return;
$target.val(value || opts.defaultPage || '');
}
function getSingleRenameTargets() {
var targets = [];
var $source = opts.singleRenameTargetSelector ? $(opts.singleRenameTargetSelector).first() : $();
if ($source.length && String($source.val() || '').trim()) targets.push(String($source.val() || '').trim());
if (opts.singleRenameVariantSelector) {
$(opts.singleRenameVariantSelector).each(function () {
var value = String($(this).val() || '').trim();
if (value) targets.push(value);
});
}
return targets.slice(0, 3);
}
function setMultiBlockRenameTargets($block, targets) {
var list = asNonEmptyArray(targets).slice(0, 3);
var $target = $block.find('.rmMultiRenameTargetInput').first();
var $container = $block.find('.rmMultiRenameVariantsContainer').first();
if (!$target.length) return;
$target.val(list[0] || '');
if (!$container.length) return;
$container.find('.rmMultiRenameVariantRow').remove();
list.slice(1).forEach(function (value) {
$container.append(buildMultiRenameVariantRowHtml(value, opts));
});
}
function copySingleRenameTargetsToFirstRow() {
var $block = $(containerSelector + ' .rmMultiPageBlock').first();
if (!$block.length) return;
setMultiBlockRenameTargets($block, getSingleRenameTargets());
}
function copyFirstRowRenameTargetsToSingle() {
var $target = opts.singleRenameTargetSelector ? $(opts.singleRenameTargetSelector).first() : $();
var $sourceBlock = $(containerSelector + ' .rmMultiPageBlock').first();
var targets = collectMultiRenameTargetValues($sourceBlock).slice(0, 3);
var $variantContainer = opts.singleRenameVariantContainerId ? $('#' + opts.singleRenameVariantContainerId) : $();
if (!$sourceBlock.length || !$target.length) return;
$target.val(targets[0] || '');
if (!$variantContainer.length) return;
$variantContainer.empty();
targets.slice(1).forEach(function (value) {
addInputRow({
containerId: opts.singleRenameVariantContainerId,
placeholder: opts.singleRenameVariantPlaceholder || 'Дополнительный вариант',
inputClass: opts.singleRenameInputClass,
rowClass: 'rmRenameVariantRow',
indentLeft: 0,
fitParentWidth: true,
prefixHtml: buildLeftRemoveButtonHtml('rmRemoveInput', 'Удалить'),
removeBeforeInput: true
});
$variantContainer.find('input').last().val(value);
});
}
function updateMultiMode() {
var $blocks = $(containerSelector + ' .rmMultiPageBlock');
var hasExtra = $blocks.length > 1;
var pageGap = hasExtra && opts.targetVariants ? '12px' : multiNominationGap;
$(containerSelector).css({
gap: pageGap,
marginBottom: pageGap
});
$('#rmMultiHeader').css('marginBottom', hasExtra ? pageGap : multiNominationGap).toggle(hasExtra);
if (opts.multiOnlySelector) $(opts.multiOnlySelector).toggle(hasExtra);
if (opts.singleOnlySelector) $(opts.singleOnlySelector).toggle(!hasExtra);
if (opts.hideContainerWhenSingle) $(containerSelector).toggle(hasExtra);
if (!hasExtra && wasMultiModeExpanded) {
restoreEmptySinglePageInput();
copyFirstRowPageToSingleCurrent();
copyFirstRowRenameTargetsToSingle();
}
$blocks.each(function (index) {
setMultiPageRowControls($(this), !hasExtra && index === 0, hasExtra, opts);
});
wasMultiModeExpanded = hasExtra;
syncModalLayout();
}
$(document).off('click.rmMultiPageAdd').on('click.rmMultiPageAdd', '.rmAddMultiPage', function () {
var wasSingle = $(containerSelector + ' .rmMultiPageBlock').length <= 1;
if (wasSingle) {
copySingleCurrentPageToFirstRow();
copySingleRenameTargetsToFirstRow();
}
$(containerSelector).append(buildMultiPageRowHtml(pageCounter++, opts));
updateMultiMode();
});
$(document).off('click.rmMultiRenameVariantAdd').on('click.rmMultiRenameVariantAdd', '.rmAddMultiRenameVariant', function () {
var $block = $(this).closest('.rmMultiPageBlock');
var $container = $block.find('.rmMultiRenameVariantsContainer').first();
var fieldCount = 1 + $block.find('.rmMultiRenameVariantInput').length;
if (fieldCount >= 3) { alert('Максимум 3 варианта переименования.'); return; }
$container.append(buildMultiRenameVariantRowHtml('', opts)).show();
syncModalLayout();
});
$(document).off('click.rmMultiRenameVariantRemove').on('click.rmMultiRenameVariantRemove', '.rmRemoveMultiRenameVariant', function () {
$(this).closest('.rmMultiRenameVariantRow').remove();
syncModalLayout();
});
$(document).off('click.rmMultiPageComment').on('click.rmMultiPageComment', '.rmMultiPageCommentToggle', function () {
var $btn = $(this);
if (!$btn.is(':visible')) return;
setMultiPageCommentExpanded($btn, $btn.attr('aria-expanded') !== 'true');
syncModalLayout();
});
$(document).off('click.rmMultiPageRemove').on('click.rmMultiPageRemove', '.rmMultiPageRow .rmRemoveInput', function () {
$(this).closest('.rmMultiPageBlock').remove();
updateMultiMode();
});
updateMultiMode();
return {
update: updateMultiMode,
isMulti: function () { return $(containerSelector + ' .rmMultiPageBlock').length > 1; }
};
}
function setExclusiveButtonActive($root, buttonSelector, $btn) {
$root.find(buttonSelector).removeClass('is-active').attr('aria-pressed', 'false');
$btn.addClass('is-active').attr('aria-pressed', 'true');
}
function bindMultiNominationFormatSwitch(rootSelector, buttonSelector) {
var $root = $(rootSelector);
$root.off('click.rmMultiFormat').on('click.rmMultiFormat', buttonSelector, function () {
setExclusiveButtonActive($root, buttonSelector, $(this));
});
}
function buildProtectAddButtonHtml() {
return buildSquareAddButtonHtml('', 'Добавить страницу', 'rmProtectAddPage');
}
function buildProtectPageRowHtml(id, pageName, isFirstRow) {
return joinHtml([
'<div', isFirstRow ? ' id="rmProtectFirstRow"' : '', ' class="rmProtectPageRow ', RESIZE_CLASS, '" style="', stRow, '">',
'<input id="', id, '" type="text" placeholder="Страница" class="rmProtectPageInput" style="', stInputBox, '"',
pageName ? ' value="' + escapeHtml(pageName) + '"' : '', '>',
isFirstRow
? buildProtectAddButtonHtml()
: '<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить">−</button>',
'</div>'
]);
}
function buildReportFormHtml(ctx, isZka) {
var reportTextPlaceholder = (isZka ? 'Введите текст запроса.' : 'Текст запроса (можно дополнить или изменить).') + ' Подпись будет добавлена автоматически.';
if (isZka) {
return joinHtml([
'<input id="rmReportHeader" type="text" placeholder="Тема/заголовок" style="', stInputFull, '" value="', escapeHtml(ctx.pageLink), '">',
'<textarea id="rmReportText" placeholder="', reportTextPlaceholder, '"></textarea>',
buildQuickPhrasesPanelHtml('rmReportText')
]);
}
return joinHtml([
'<div id="rmProtectModeBtns" class="', RESIZE_CLASS, '" style="margin-bottom:14px;"><div class="rmSegmentedBar">',
'<button id="rmProtectModeInstall" type="button" class="rmSegmentedBtn rmProtectModeBtn is-active" aria-pressed="true">🛡️ Установить защиту</button>',
'<button id="rmProtectModeRemove" type="button" class="rmSegmentedBtn rmProtectModeBtn" aria-pressed="false">📛 Снять защиту</button>',
'</div></div>',
'<div id="rmProtectMultiWrap" class="', RESIZE_CLASS, '">',
'<div id="rmProtectHeaderWrap" style="display:none;margin-bottom:6px;"><div class="rmProtectHeaderRow" style="', stRow.replace('margin-bottom:6px;', 'margin-bottom:0;'), '"><input id="rmProtectHeader" type="text" placeholder="Заголовок (для нескольких страниц)" style="', stInputBox, '">', buildProtectAddButtonHtml(), '</div></div>',
buildProtectPageRowHtml('rmProtectPage0', ctx.pageName, true),
'<div id="rmProtectPagesContainer"></div>',
'</div>',
'<div id="rmProtectLevelsWrap" class="', RESIZE_CLASS, ' rmProtectControlGroup"><div class="rmProtectControlLabel">Уровень защиты</div><div id="rmProtectLevels" class="rmSegmentedBar">',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="полузащиту">полузащита</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="стабилизацию">стабилизация</button>',
'</div></div>',
'<div id="rmProtectReasonsWrap" class="', RESIZE_CLASS, ' rmProtectControlGroup"><div class="rmProtectControlLabel">Причины</div><div id="rmProtectReasons" class="rmSegmentedBar">',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="война правок">война правок</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="неконсенсусные изменения">неконсенсусные изменения</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="вандализм">вандализм</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="популярная статья">популярная статья</button>',
'</div></div>',
'<div id="rmRemoveLevelsWrap" class="', RESIZE_CLASS, ' rmProtectControlGroup" style="display:none;"><div class="rmProtectControlLabel">Уровень защиты</div><div id="rmRemoveLevels" class="rmSegmentedBar">',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="полузащиту">полузащита</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="стабилизацию">стабилизация</button>',
'</div></div>',
'<div id="rmProtectTextBlock" class="', RESIZE_CLASS, '"><textarea id="rmReportText" placeholder="', reportTextPlaceholder, '"></textarea>',
buildQuickPhrasesPanelHtml('rmReportText'), '</div>'
]);
}
// ═══════════════════════════════════════════════════════════════════════════
// ОБРАБОТЧИКИ ОПЕРАЦИЙ
// ═══════════════════════════════════════════════════════════════════════════
var handlers = {
// ── КБУ ─────────────────────────────────────────────────────────────
showKbu: function (op) {
var forCategory = !!(op && op.forCategory);
var reasons = getFastRemoveReasons();
createModal({
title: 'Быстрое удаление',
width: 'compact',
subtitleHtml: '<span id="rmKbuCriteriaLinkWrap"></span>'
});
$('#removerModalContent').html(buildKbuFormHtml(reasons));
$('#rmSel').change(function () { updateKbuReasonControls(reasons); syncModalLayout(); });
$('#removerModalContent').off('click.rmKbuCommonsReplacementMode').on('click.rmKbuCommonsReplacementMode', '.rmKbuCommonsReplacementModeBtn', function () {
setCurrentCommonsReplacementMode($(this).data('rmCommonsReplacementMode') || 'file');
});
$('#removerModalContent').off('click.rmKbuDfudReason').on('click.rmKbuDfudReason', '.rmKbuDfudReasonBtn', function () {
var $btn = $(this);
var active = !$btn.hasClass('is-active');
$btn.toggleClass('is-active', active).attr('aria-pressed', active ? 'true' : 'false');
});
$('#removerModalContent').off('click.rmKbuBadfairuseReason').on('click.rmKbuBadfairuseReason', '.rmKbuBadfairuseReasonBtn', function () {
setExclusiveButtonActive($('#rmKbuBadfairuseReasonsWrap'), '.rmKbuBadfairuseReasonBtn', $(this));
});
updateKbuReasonControls(reasons);
syncModalLayout();
renderModalFooter('submit', {
submitText: 'Номинировать',
onSubmit: function () {
var idx = $('#rmSel').val();
var reason = reasons[idx] || reasons[0];
var kbuData = buildFastRemoveTemplateData(reason);
if (kbuData.error) {
alert(kbuData.error);
return false;
}
var fastRemovalSummary = makeFastRemovalSummary(reason, kbuData);
startProcessing();
if (forCategory) {
var tpl = kbuData.templateText;
var categorySummary = fastRemovalSummary;
editPageContent(mwCfg.wgPageName, { summary: categorySummary, readError: 'Не удалось получить содержимое.' },
function (text) { return { text: wrapInNoinclude(text, T_OPEN + tpl + T_CLOSE) }; },
function (err) {
if (err) {
unlockModalSubmit();
logStatus('Ошибка записи.', err);
} else {
logStatus('Страница номинирована к быстрому удалению.', null, { trackError: false });
finalizeFastRemoval([normTitle(mwCfg.wgPageName)], categorySummary);
}
});
} else {
var job = {
mode: 'nominate', opId: 'fRm',
kbuTemplate: reason[0], kbuTemplateText: kbuData.templateText,
removeMoveToCommons: !!kbuData.removeMoveToCommons,
summary: fastRemovalSummary,
inArticle: true
};
processPageList([normTitle(mwCfg.wgPageName)], job, function (notifiedPages) {
finalizeFastRemoval(notifiedPages, job.summary);
});
}
return true;
}
});
},
// ── Универсальная номинация (КУ, КПМ, КУЛ, КОБ, КРАЗД, ВУС) ────────
showNomination: function (op) {
var nom = op.nomination;
var pg = normTitle(mwCfg.wgPageName);
var date = getDate()[1];
var nomPage = nom.nomPage(date);
var multiMode = nom.supportsMulti;
function updateTransferUi() {
var mode = getTransferModeFromButtons();
var isNone = mode === 'none';
var isKbu = mode === 'kbu' || mode === 'both';
var isKul = mode === 'kul' || mode === 'both';
$('#rmTransferBtnNone').toggleClass('is-active', isNone).attr('aria-pressed', isNone ? 'true' : 'false');
$('#rmTransferBtnKbu').toggleClass('is-active', isKbu).attr('aria-pressed', isKbu ? 'true' : 'false');
$('#rmTransferBtnKul').toggleClass('is-active', isKul).attr('aria-pressed', isKul ? 'true' : 'false');
var t = transferTexts[mode];
if (t) { $('#rmTransferHint').text(t.hint).show(); } else { $('#rmTransferHint').hide().text(''); }
applyGeneratedText($('#rmMsg'), t && t.notice ? t.notice + '\n' : '');
}
createModal({
title: 'Номинация: ' + nom.template,
subtitlePage: nomPage,
subtitleLabel: 'Текущий день'
});
$('#removerModalContent').html(buildNominationFormHtml(nom, pg, multiMode));
setupResizableModal('rmMsg');
// Логика переноса
if (nom.supportsTransfer) {
$(document).off('click.rmTransfer').on('click.rmTransfer', '#rmTransferBtnNone,#rmTransferBtnKbu,#rmTransferBtnKul', function () {
if (this.id === 'rmTransferBtnNone') {
$('#rmTransferBtnKbu,#rmTransferBtnKul').removeClass('is-active');
$('#rmTransferBtnNone').addClass('is-active');
} else {
$(this).toggleClass('is-active');
var anyOn = $('#rmTransferBtnKbu').hasClass('is-active') || $('#rmTransferBtnKul').hasClass('is-active');
$('#rmTransferBtnNone').toggleClass('is-active', !anyOn);
}
updateTransferUi();
});
updateTransferUi();
}
// Многостраничный режим
if (multiMode) {
setupMultiPageNominationUi(getMultiNominationUiOptions('article', {
setup: true,
defaultPage: pg,
renameMulti: nom.extraInput && nom.extraInput.type === 'rename',
deletionMulti: nom.template === 'к удалению'
}));
bindMultiNominationFormatSwitch('#removerModalContent', '.rmArticleMultiFormatBtn');
}
if (nom.extraInput) wireMultiInput(nom.extraInput);
renderModalFooter('submit', {
submitText: 'Номинировать',
showSubscribe: true,
onSubmit: function () {
var isMulti = multiMode && $('#rmMultiPagesContainer .rmMultiPageBlock').length > 1;
var singleCurrentInput = $('#rmSingleRenameCurrent').val() || '';
var inputVal = !isMulti ? normTitle(singleCurrentInput || $('#rmMultiPagesContainer .rmMultiPageInput').first().val() || '') : '';
var changed = inputVal && inputVal !== pg;
function executeJob(job) {
startProcessing();
runFlow({
templateStep: function (next) {
if (!job.inArticle) { next(); return; }
processPageList(job.pages, job, function (notifiedPages, err) {
job._notifiedPages = notifiedPages;
next(err);
});
},
nominationStep: function (done) {
publishNomination({
pageTitle: job.nomPage,
navTemplate: job.navTemplate,
sectionTitle: job.section,
summary: job.nominationSummary || job.summary,
text: getNominationPublishText(job)
}, function (err) { done(err, { pageTitle: job.nomPage, sectionTitle: job.section }); });
},
notifyStep: function (nominationInfo, next) {
var pages = job._notifiedPages || [];
if (!setAlert || !pages.length) { next(); return; }
notifyAuthorsForPages(pages, {
summary: job.summary,
actionText: job.comment,
discussionPage: nominationInfo && nominationInfo.pageTitle,
discussionSection: nominationInfo && nominationInfo.sectionTitle
}, next);
},
skipLink: op.id === 'fRm'
});
}
function run(targetPg) {
var job = buildNominationJob(op, targetPg, isMulti);
if (!job) { unlockModalSubmit(); return; }
if (job.isMulti && job.inArticle && getNominationConflictRule(job)) {
startProcessing();
inspectMultiNominationConflicts(job, function (err, conflicts) {
if (err) { markSubmitError(); return; }
if (!conflicts.length) { executeJob(job); return; }
showNominationConflictResolution(job, conflicts, function (resolvedJob) {
executeJob(resolvedJob);
});
});
return;
}
executeJob(job);
}
if (changed) {
apiReq({ prop: 'info', titles: inputVal }, 'query', function (data) {
if (data && data.error) {
unlockModalSubmit();
alert('Не удалось проверить страницу из-за ошибки API. Попробуйте ещё раз.');
return;
}
var page = getFirstQueryPage(data);
if (!page) {
unlockModalSubmit();
alert('Не удалось проверить страницу из-за временной ошибки. Попробуйте ещё раз.');
return;
}
if (page.missing !== undefined) {
unlockModalSubmit();
alert('Страница «' + inputVal + '» не существует.');
return;
}
run(normTitle(page.title || inputVal));
});
} else {
run(pg);
}
return true;
}
});
},
// ── Снятие номинации (статья) ────────────────────────────────────────
showArticleClose: function () {
showCloseActionsModal({
inputName: 'rmCloseAction',
listId: 'rmCloseActions',
emptyText: 'Не найдено подходящих шаблонов для закрытия.',
emptyDetails: 'Проверяются: КУ, КПМ, КБУ, КУЛ.',
getActions: function (articleText) {
var actions = [];
var hasKu = RE_KU_ON_PAGE.test(articleText);
var hasKpm = RE_KPM_ON_PAGE.test(articleText);
var hasKbu = RE_KBU_ON_PAGE.test(articleText);
var hasKul = RE_KUL_ON_PAGE.test(articleText);
if (hasKu) {
actions.push({ id: 'ret', tag: 'КУ', label: 'Оставлено', mode: 'denom', closeType: 'ret', resultTemplate: 'Оставлено', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{Оставлено}} на СО.', comment: 'оставлена', talkNotice: true });
actions.push({ id: 'retConditional', tag: 'КУ', label: 'Условно оставлено', mode: 'denom', closeType: 'retConditional', resultTemplate: 'Условно оставлено', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{Условно оставлено}} на СО.', comment: 'условно оставлена', talkNotice: true, needsConditionalFields: true });
actions.push({ id: 'withdrawnDeletion', tag: 'КУ', label: 'Снято с удаления', mode: 'denom', closeType: 'withdrawnDeletion', resultTemplate: 'Снято с удаления', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{Снято с удаления}} на СО.', comment: 'снята с удаления', talkNotice: true });
actions.push({ id: 'redirectedDeletion', tag: 'КУ', label: 'Заменено перенаправлением', mode: 'denom', closeType: 'redirectedDeletion', resultTemplate: 'Заменено перенаправлением', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, заменяет страницу перенаправлением, добавляет {{Заменено перенаправлением}} на СО.', warning: 'Осторожно: это действие перезапишет содержимое всей страницы.', comment: 'заменена перенаправлением', talkNotice: true, needsRedirectTarget: true });
}
if (hasKpm) {
actions.push({ id: 'doneRnm', tag: 'КПМ', label: 'Переименовано', mode: 'denom', closeType: 'doneRnm', resultTemplate: 'Переименовано', sourceTemplate: 'к переименованию|кпм|rename', description: 'Снимает шаблон КПМ, добавляет {{Переименовано}} на СО.', comment: 'переименована', talkNotice: true, needsOldTitle: true });
actions.push({ id: 'noRnm', tag: 'КПМ', label: 'Не переименовано', mode: 'denom', closeType: 'noRnm', resultTemplate: 'Не переименовано', sourceTemplate: 'к переименованию|кпм|rename', description: 'Снимает шаблон КПМ, добавляет {{Не переименовано}} на СО.', comment: 'не переименована', talkNotice: true });
}
if (hasKbu && hasKul) actions.push({ id: 'cleanup-both', tag: 'КБУ и КУЛ', label: 'Снятие', mode: 'cleanup', transferMode: 'both', cleanupLabel: 'КБУ и КУЛ', description: 'Снимает шаблоны КБУ/КУЛ и Hangon.' });
if (hasKbu) actions.push({ id: 'cleanup-kbu', tag: 'КБУ', label: 'Снятие', mode: 'cleanup', transferMode: 'kbu', cleanupLabel: 'КБУ', description: 'Снимает шаблоны КБУ и Hangon.' });
if (hasKul) actions.push({ id: 'cleanup-kul', tag: 'КУЛ', label: 'Снятие', mode: 'cleanup', transferMode: 'kul', cleanupLabel: 'КУЛ', description: 'Снимает шаблон КУЛ.' });
return actions;
},
afterRender: function (actions) {
var hasDoneRnm = actions.some(function (a) { return a.id === 'doneRnm'; });
var hasConditionalRet = actions.some(function (a) { return a.id === 'retConditional'; });
var hasRedirectedDeletion = actions.some(function (a) { return a.id === 'redirectedDeletion'; });
if (hasDoneRnm) {
$('#rmCloseActions input[value="doneRnm"]').closest('.rmActionItem').append(
'<div id="rmCloseOldTitleWrap" style="display:none;margin-top:6px;"><input id="rmCloseOldTitle" type="text" placeholder="Старое название" style="' + stInputFull + '"></div>'
);
}
if (hasConditionalRet) {
$('#rmCloseActions input[value="retConditional"]').closest('.rmActionItem').append(buildConditionalRetFieldsHtml());
}
if (hasRedirectedDeletion) {
$('#rmCloseActions input[value="redirectedDeletion"]').closest('.rmActionItem').append(
'<div id="rmCloseRedirectTargetWrap" style="display:none;margin-top:6px;"><input id="rmCloseRedirectTarget" type="text" placeholder="Цель перенаправления" style="' + stInputFull + '"></div>'
);
}
},
afterFooterRender: function (_, actionMap) {
function ensureConditionalTextareaResizer() {
var $textarea = $('#rmCloseConditionalReason');
if (!$textarea.length || $textarea.data('rmConditionalResizerReady')) return;
setupNestedResizableTextarea('rmCloseConditionalReason', 'rmCloseConditionalWrap', 280, 90);
$textarea.data('rmConditionalResizerReady', true);
}
function updateUi() {
var sel = actionMap[$('[name="rmCloseAction"]:checked').val()];
$('#rmCloseOldTitleWrap').toggle(!!(sel && sel.needsOldTitle));
$('#rmCloseConditionalWrap').toggle(!!(sel && sel.needsConditionalFields));
$('#rmCloseRedirectTargetWrap').toggle(!!(sel && sel.needsRedirectTarget));
if (sel && sel.needsConditionalFields) ensureConditionalTextareaResizer();
var disableNotify = !!(sel && sel.mode === 'cleanup' && sel.transferMode === 'kbu');
var $cb = $('[name="rmUAlert"]');
var $cbLabel = $('[name="rmUAlert"]').closest('label');
if ($cb.length) $cb.prop('disabled', disableNotify);
if ($cbLabel.length) $cbLabel.css({
visibility: disableNotify ? 'hidden' : 'visible',
pointerEvents: disableNotify ? 'none' : ''
});
syncModalLayout();
}
$(document).off('change.rmCloseAction').on('change.rmCloseAction', '[name="rmCloseAction"]', updateUi);
updateUi();
},
onSubmit: function (sel, pageName) {
var job;
if (sel.mode === 'denom') {
var oldTitle = sel.needsOldTitle ? ($('#rmCloseOldTitle').val() || '').trim() : '';
var conditionalReason = sel.needsConditionalFields ? normalizeQuickPhraseValue($('#rmCloseConditionalReason').val()) : '';
var conditionalDeadline = sel.needsConditionalFields ? String($('#rmCloseConditionalDeadline').val() || '').trim() : '';
var redirectTarget = sel.needsRedirectTarget ? normalizeRedirectTarget($('#rmCloseRedirectTarget').val()) : '';
if (sel.needsOldTitle && !oldTitle) { alert('Укажите старое название.'); return false; }
if (sel.needsRedirectTarget && !redirectTarget) { alert('Укажите цель перенаправления.'); return false; }
if (sel.needsRedirectTarget && normTitle(redirectTarget) === normTitle(pageName)) { alert('Цель перенаправления совпадает с текущей страницей.'); return false; }
if (conditionalDeadline && normalizeIsoDate(conditionalDeadline) !== conditionalDeadline) {
alert('Укажите срок в формате YYYY-MM-DD, например 2026-05-31.');
return false;
}
job = {
mode: 'denom',
closeType: sel.closeType,
resultTemplate: sel.resultTemplate,
sourceTemplate: sel.sourceTemplate,
oldTitle: oldTitle,
redirectTarget: redirectTarget,
conditionalReason: conditionalReason,
conditionalDeadline: conditionalDeadline,
notifyActionText: sel.comment,
skipNotify: false
};
} else {
job = {
mode: 'cleanup',
transferMode: sel.transferMode,
summary: makeSummary('снятие шаблонов ' + sel.cleanupLabel),
notifyActionText: (sel.transferMode === 'kul' || sel.transferMode === 'both')
? 'больше не номинирована к срочному улучшению'
: '',
skipNotify: !(sel.transferMode === 'kul' || sel.transferMode === 'both')
};
}
processPageList([pageName], job, function (notifiedPages, err, pageMeta) {
function finishClose() {
if (isError) { markSubmitError(); }
else { renderModalFooter('reload'); }
}
if (isError || err || job.skipNotify || !setAlert || !notifiedPages.length) { finishClose(); return; }
var meta = (pageMeta && pageMeta[normTitle(pageName)]) || {};
notifyAuthorsForPages(notifiedPages, {
summary: meta.summary || job.summary,
actionText: job.notifyActionText,
discussionPage: meta.discussionPage,
discussionSection: meta.discussionSection,
includeProposedPrefix: false
}, finishClose);
});
return true;
}
});
},
// ── ОБКАТ: номинация категории ───────────────────────────────────────
showCatNomination: function (op) {
var catType = op.catType;
var catMeta = CATEGORY_NOMINATION_META[catType] || CATEGORY_NOMINATION_META.discuss;
var now = new Date();
var catDiscPage = 'Википедия:Обсуждение категорий/' + MONTHS_NOM[now.getUTCMonth()] + '_' + now.getUTCFullYear();
var pageName = normalizeCategoryPageName(mwCfg.wgPageName);
var multiMode = !!catMeta.supportsMulti;
var renameMultiMode = !!catMeta.renameMulti && multiMode;
createModal({ title: catMeta.title, subtitlePage: catDiscPage, subtitleLabel: 'Текущий месяц' });
var variantCfgs = {
rename: { type: 'rename', firstId: 'firstRenameInput', addBtnId: 'addRenameVariant', containerId: 'renameVariantsContainer', addBtnLabel: '+ Добавить вариант', firstPh: 'Новое название без префикса Категория:', addPh: 'Дополнительный вариант названия', maxRows: 2, maxMsg: 'Максимум 3 варианта переименования.' },
merge: { firstId: 'firstMergeInput', addBtnId: 'addMergeVariant', containerId: 'mergeVariantsContainer', addBtnLabel: '+ Добавить вариант', firstPh: 'Категория для объединения без префикса Категория:', addPh: 'Дополнительный вариант объединения' }
};
var vCfg = variantCfgs[catType];
var categoryMultiOptions = {
renameMulti: renameMultiMode,
actionText: catMeta.actionText,
skipVariantInput: renameMultiMode
};
$('#removerModalContent').html(buildCategoryNominationFormHtml(vCfg, multiMode, pageName, categoryMultiOptions));
setupResizableModal('nominationReason');
if (multiMode) {
setupMultiPageNominationUi(getMultiNominationUiOptions('category', $.extend({
setup: true,
defaultPage: pageName
}, categoryMultiOptions)));
bindMultiNominationFormatSwitch('#removerModalContent', '.rmCategoryMultiFormatBtn');
}
if (vCfg) wireMultiInput(vCfg);
renderModalFooter('submit', {
submitText: 'Номинировать',
showSubscribe: true,
onSubmit: function () {
var reason = normalizeQuickPhraseValue($('#nominationReason').val());
var hasMultiRenameRows = renameMultiMode && $('#rmMultiPagesContainer .rmMultiPageBlock').length > 1;
var renamePairs = hasMultiRenameRows ? collectMultiRenamePairs({
normalizePageName: normalizeCategoryPageName,
normalizeTargetName: normalizeCategoryTargetPageName,
normalizeTemplateTargetName: normalizeCategoryTargetName
}) : [];
var renameTargetsByCategory = buildMultiRenameTargetMap(renamePairs, 'targetNames');
var renameTemplateTargetsByCategory = buildMultiRenameTargetMap(renamePairs, 'templateTargetNames');
var singleRenameCategory = renameMultiMode && !hasMultiRenameRows
? normalizeCategoryPageName($('#rmSingleRenameCurrent').val() || pageName)
: '';
var targetPages = hasMultiRenameRows
? renamePairs.map(function (pair) { return pair.pageName; })
: (multiMode ? collectCategoryPageInputValues('.rmMultiPageInput') : [pageName]);
if (renameMultiMode && !hasMultiRenameRows) targetPages = [singleRenameCategory || pageName];
var isMulti = renameMultiMode ? hasMultiRenameRows : (multiMode && targetPages.length > 1);
var multiFormat = $('.rmCategoryMultiFormatBtn.is-active').data('rmMultiFormat') || 'sections';
var commentsByCategory = isMulti ? collectMultiNominationComments(normalizeCategoryPageName) : {};
var notifiedPages = [];
if (hasMultiRenameRows && !validateMultiRenamePairs(renamePairs, 'категорию', 'новое название')) return false;
if (!targetPages.length) { alert('Укажите категорию.'); return false; }
if (isMulti) {
if (!validateMultiNominationText(targetPages, reason, commentsByCategory, 'категории')) return false;
} else if (!reason) {
alert('Пожалуйста, укажите причину/тему.');
return false;
}
var discussionTarget = isMulti ? targetPages : targetPages[0];
var discussionReason = isMulti
? (multiFormat === 'list'
? buildMultiNominationListText(targetPages, reason, commentsByCategory, renameMultiMode ? getMultiRenameDiscussionOptions(renameTargetsByCategory) : null)
: buildMultiNominationText(targetPages, reason, commentsByCategory, renameMultiMode
? getMultiRenameDiscussionOptions(renameTargetsByCategory, { headingLevel: 4 })
: { headingLevel: 4 }))
: reason;
var discussionOptions = isMulti ? {
headerText: $('#rmHeader').val(),
reasonIsPrepared: true,
renameTargetsByPage: renameTargetsByCategory
} : null;
var mainName = null, additionalNames = [];
if (vCfg) {
if (hasMultiRenameRows) {
mainName = renamePairs[0] && renamePairs[0].templateTargetName;
additionalNames = renamePairs[0] ? asNonEmptyArray(renamePairs[0].templateTargetNames).slice(1) : [];
} else {
mainName = $('#' + vCfg.firstId).val().trim();
if (!mainName) { alert('Укажите ' + (catType === 'rename' ? 'новое название' : 'категорию для объединения') + '.'); return false; }
additionalNames = collectInputValues('#' + vCfg.containerId + ' .variantInput');
}
}
var categoryDiscussionTitle = buildCategoryDiscussionTitle(discussionTarget, catType, mainName, additionalNames, discussionOptions || {});
var categorySectionTitle = getCategoryDiscussionSectionTitle(categoryDiscussionTitle);
var categoryTemplateOptions = hasMultiRenameRows ? { renameTemplateTargetsByPage: renameTemplateTargetsByCategory } : {};
categoryTemplateOptions.summary = makeNominationTargetSummary(catDiscPage, categorySectionTitle, catMeta.actionText);
startProcessing();
runFlow({
templateStep: function (next) {
addTemplatesToCategories(targetPages, catType, mainName, additionalNames, function (err, processedPages) {
notifiedPages = processedPages || [];
next(err);
}, categoryTemplateOptions);
},
nominationStep: function (done) {
createCategoryDiscussion(discussionTarget, discussionReason, catType, function (err, nominationInfo) { done(err, nominationInfo || null); }, mainName, additionalNames, discussionOptions);
},
notifyStep: function (nominationInfo, next) {
if (!setAlert || !nominationInfo) { next(); return; }
var section = normalizeSectionForLink(nominationInfo.sectionTitle || '');
notifyAuthorsForPages(notifiedPages.length ? notifiedPages : targetPages, {
summary: makeSummary('номинация [[' + nominationInfo.pageTitle + (section ? '#' + section : '') + ']]'),
actionText: catMeta.actionText,
discussionPage: nominationInfo.pageTitle,
discussionSection: nominationInfo.sectionTitle
}, next);
}
});
return true;
}
});
},
// ── Снятие номинации (категория) ─────────────────────────────────────
showCatClose: function () {
showCloseActionsModal({
inputName: 'rmCategoryCloseAction',
showCheckbox: false,
emptyText: 'Не найдено подходящих шаблонов для завершения.',
emptyDetails: 'Проверяются ОБКАТ и КУ.',
getActions: function (catText) {
var allObkat = [cfg.categoryTemplates.discuss, cfg.categoryTemplates.rename, cfg.categoryTemplates.merge].join('|');
var actions = [];
if (new RegExp('\\{\\{\\s*(?:' + allObkat + ')\\s*(?:\\||\\}\\})', 'i').test(catText)) {
actions.push({ id: 'cat-obkat-done', tag: 'ОБКАТ', label: 'Завершено', mode: 'obkat', talkTemplate: 'Обсуждавшаяся категория', description: 'Снимает шаблон ОБКАТ, добавляет {{Обсуждавшаяся категория}} на СО.', talkNotice: true });
}
if (RE_KU_ON_PAGE.test(catText)) {
actions.push({ id: 'cat-ku-cleanup', tag: 'КУ', label: 'Снятие', mode: 'cleanup', description: 'Снимает шаблон КУ без записи на СО.' });
}
return actions;
},
onSubmit: function (sel, pageName) {
if (sel.mode === 'obkat') markCategoryDiscussionAsDone(pageName);
if (sel.mode === 'cleanup') removeKuFromCategory(pageName);
return true;
}
});
},
// ── Защита / Запрос к администраторам ───────────────────────────────
showReport: function (op) {
var mode = op.reportMode || 'protect';
var ctx = getReporterContext(mode);
var isZka = mode === 'request';
var protectMode = 'install';
var pageCounter = 1;
function buildProtectText(pm) {
if (pm === 'remove') {
var removeLevels = [];
$('#rmRemoveLevels .rmToggleBtn.is-active').each(function () { removeLevels.push($(this).data('label')); });
return removeLevels.length ? 'Просьба снять ' + removeLevels.join(' и/или ') + '.' : '';
}
var levels = [], reasons = [];
$('#rmProtectLevels .rmToggleBtn.is-active').each(function () { levels.push($(this).data('label')); });
$('#rmProtectReasons .rmToggleBtn.is-active').each(function () { reasons.push($(this).data('label')); });
if (!levels.length && !reasons.length) return '';
var text = 'Просьба установить';
if (levels.length) text += ' ' + levels.join(' и/или ');
if (reasons.length) text += ' по причине: ' + reasons.join(', ');
return text + '.';
}
function applyProtectMode(m) {
protectMode = m;
var isInstall = m === 'install';
$('#rmProtectModeInstall').toggleClass('is-active', isInstall).attr('aria-pressed', isInstall ? 'true' : 'false');
$('#rmProtectModeRemove').toggleClass('is-active', !isInstall).attr('aria-pressed', !isInstall ? 'true' : 'false');
$('#removerModalTitleText').text(isInstall ? 'Запрос на защиту страницы' : 'Запрос на снятие защиты');
var linkPage = isInstall ? 'Википедия:Установка защиты' : 'Википедия:Снятие защиты';
$('#rmProtectLinkWrap').html('<a href="' + getPageUrl(linkPage) + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">' + escapeHtml(linkPage) + '</a>');
$('#rmProtectLevelsWrap,#rmProtectReasonsWrap').toggle(isInstall);
$('#rmRemoveLevelsWrap').toggle(!isInstall);
$('#rmProtectLevels .rmProtectOptBtn,#rmProtectReasons .rmProtectOptBtn,#rmRemoveLevels .rmProtectOptBtn').removeClass('is-active');
$('#rmReportText').val('').removeData('rmGenerated');
}
function updateProtectMultiUi() {
var $rows = $('#rmProtectMultiWrap .rmProtectPageRow');
var hasExtra = $rows.length > 1;
if (!$rows.length) {
$('#rmProtectPagesContainer').append(buildProtectPageRowHtml('rmProtectPage' + pageCounter++, ctx.pageName, true));
$rows = $('#rmProtectMultiWrap .rmProtectPageRow');
hasExtra = false;
}
$('#rmProtectHeaderWrap').toggle(hasExtra);
if (!hasExtra) $('#rmProtectHeader').val('');
$rows.each(function () {
var $row = $(this);
$row.find('.rmProtectAddPage,.rmRemoveInput').remove();
$row.append(hasExtra
? '<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить">−</button>'
: buildProtectAddButtonHtml()
);
});
syncModalLayout();
}
createModal({
title: isZka ? 'Запрос к администраторам' : 'Запрос на защиту страницы',
width: 'compact',
subtitleHtml: isZka
? '<a href="' + getPageUrl('Википедия:Запросы к администраторам') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Википедия:Запросы к администраторам</a>' +
' · <a href="' + getPageUrl('Википедия:Запросы к администраторам/Быстрые') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Быстрые</a>'
: '<span id="rmProtectLinkWrap"></span>'
});
$('#removerModalContent').html(buildReportFormHtml(ctx, isZka));
if (!isZka) {
$('#removerModalContent')
.on('click', '#rmProtectModeInstall', function () { applyProtectMode('install'); })
.on('click', '#rmProtectModeRemove', function () { applyProtectMode('remove'); })
.on('click', '.rmProtectOptBtn', function () {
$(this).toggleClass('is-active');
if (protectMode === 'install') {
var $levels = $('#rmProtectLevels .rmProtectOptBtn.is-active');
if ($(this).closest('#rmProtectReasons').length && $(this).hasClass('is-active') && $levels.length === 0) {
$('#rmProtectLevels .rmProtectOptBtn').first().addClass('is-active');
}
if ($(this).closest('#rmProtectLevels').length && !$(this).hasClass('is-active') && $levels.length === 0) {
$('#rmProtectReasons .rmProtectOptBtn').removeClass('is-active');
}
}
applyGeneratedText($('#rmReportText'), buildProtectText(protectMode));
});
$(document)
.off('click.rmProtectAdd').on('click.rmProtectAdd', '.rmProtectAddPage', function () {
var id = 'rmProtectPage' + pageCounter++;
$('#rmProtectPagesContainer').append(buildProtectPageRowHtml(id, '', false));
updateProtectMultiUi();
})
.off('click.rmProtectRemove').on('click.rmProtectRemove', '.rmProtectPageRow .rmRemoveInput', function () {
$(this).closest('.rmProtectPageRow').remove(); updateProtectMultiUi();
});
applyProtectMode('install');
}
setupResizableModal('rmReportText');
renderModalFooter('submit', {
showCheckbox: false,
showSubscribe: true,
submitText: 'Отправить',
onSubmit: function () { doReport(ctx, false, protectMode); return true; }
});
if (isZka) {
$('<button id="rmReportFast" style="' + stCancel + '">Быстрый запрос</button>').insertBefore('#removerSubmit');
$('#rmReportFast').click(function () {
if ($('#removerSubmit').data('rmSubmitInProgress')) return;
$('#removerSubmit').data('rmSubmitInProgress', true).prop('disabled', true);
$('#rmReportFast').prop('disabled', true);
doReport(ctx, true, protectMode);
});
}
}
};
// ═══════════════════════════════════════════════════════════════════════════
// ВСПОМОГАТЕЛЬНЫЕ: КБУ, закрытие, категории
// ═══════════════════════════════════════════════════════════════════════════
function getFastRemoveCriteriaAnchorFromConfig(templateName) {
var anchors = cfg.fastRemoveCriteriaAnchors || {};
var template = String(templateName || '').trim();
var lower = template.toLowerCase();
var key;
if (!template) return '';
if (Object.prototype.hasOwnProperty.call(anchors, template)) return anchors[template];
for (key in anchors) {
if (Object.prototype.hasOwnProperty.call(anchors, key) && String(key).toLowerCase() === lower) {
return anchors[key];
}
}
return '';
}
function getFastRemoveCriteriaAnchor(reason) {
var configured = reason ? getFastRemoveCriteriaAnchorFromConfig(reason[0]) : '';
var template, label, m;
if (configured) return configured;
template = String(reason && reason[0] || '');
label = String(reason && reason[1] || '');
if (/^(?:подст\s*:\s*)?(?:deleteslow|ds)$/i.test(template) || /^\s*ds\b/i.test(label)) return 'С1';
m = label.match(/^\s*([А-ЯЁA-Z]{1,3}\d+)(?:\.\d+)?/);
return m ? m[1] : '';
}
function buildFastRemoveCriteriaLinkHtml(reason) {
var anchor = getFastRemoveCriteriaAnchor(reason);
var label = KBU_CRITERIA_PAGE + (anchor ? '#' + anchor : '');
var url = getPageUrlWithFragment(KBU_CRITERIA_PAGE, anchor);
return '<a href="' + escapeHtml(url) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' +
escapeHtml(label) + '</a>';
}
function getFastRemoveReasons() {
var reasons = cfg.fastRemoveReasons;
var prefix = (mwCfg.wgIsRedirect ? 'ОП' : 'О') + ({ 0: 'С', 2: 'У', 3: 'У', 6: 'Ф', 14: 'К' }[mwCfg.wgNamespaceNumber] || '');
var all = [];
if (isCategory && reasons.categories) all = all.concat(reasons.categories);
['general','articles','redirects','files','users','special'].forEach(function (k) { if (reasons[k]) all = all.concat(reasons[k]); });
if (!isCategory && reasons.categories) all = all.concat(reasons.categories);
return all.filter(function (r) { return prefix.indexOf(r[2] !== undefined ? r[2] : r[1].charAt(0)) >= 0; });
}
function showCloseActionsModal(opts) {
createModal({ title: 'Снятие шаблонов номинаций', inline: true });
$('#removerModalContent').html('<p style="margin:0;">Определение доступных действий...</p>');
var pageName = normTitle(mwCfg.wgPageName);
getText(pageName, function (pageText, readErr) {
if (readErr) { showInfoAndClose('Не удалось прочитать содержимое страницы.', readErr.info || readErr.code || '', true); return; }
if (pageText === null) { showInfoAndClose('Не удалось прочитать содержимое страницы.', '', true); return; }
var actions = opts.getActions(pageText);
if (!actions.length) { showInfoAndClose(opts.emptyText, opts.emptyDetails || ''); return; }
var actionMap = actions.reduce(function (m, a) { m[a.id] = a; return m; }, {});
$('#removerModalContent').html(buildActionsHtml(actions, opts.inputName, opts.listId));
$('#removerModalContent').off('change.rmActionWarnings').on('change.rmActionWarnings', 'input[name="' + opts.inputName + '"]', function () {
syncActionWarnings(opts.inputName, opts.listId);
syncModalLayout();
});
if (opts.afterRender) opts.afterRender(actions, actionMap, pageName, pageText);
renderModalFooter('submit', {
showCheckbox: opts.showCheckbox,
submitText: 'Выполнить',
onSubmit: function () {
var sel = getSelectedAction(opts.inputName, actionMap);
if (!sel) return false;
startProcessing();
return opts.onSubmit(sel, pageName, pageText, actionMap) !== false;
}
});
if (opts.afterFooterRender) opts.afterFooterRender(actions, actionMap, pageName, pageText);
syncActionWarnings(opts.inputName, opts.listId);
});
}
function runPageEditWithStatus(opts) {
var o = opts || {};
var statusId = logStatus(o.pendingText, null, { pending: true, trackError: false });
editPageContent(o.pageName, o.editOptions, o.buildFn, function (err, meta) {
if (err) { logStatus(o.errorText, err, { statusId: statusId }); unlockModalSubmit(); return; }
logStatus(o.successText, null, { statusId: statusId, trackError: false });
if (o.onSuccess) o.onSuccess(meta || null);
});
}
function removeKuFromCategory(pageName) {
runPageEditWithStatus({
pageName: pageName,
pendingText: 'Снимается шаблон КУ...',
errorText: 'Снятие шаблона КУ.',
successText: 'Шаблон КУ снят.',
editOptions: { summary: makeSummary('снятие шаблона КУ'), watchlist: 'nochange', readError: 'Страница не существует.', readErrorCode: 'read_failed' },
buildFn: function (text) {
var r = stripTemplatesByPattern(text, '(?:к\\s*удалению|ку)');
if (!r.removed) return { error: { code: 'no_changes', info: 'Шаблон КУ не найден.' } };
return { text: r.text.replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n') };
},
onSuccess: function () {
logStatus('Шаблон на СО не устанавливался.', null, { trackError: false });
renderModalFooter('reload');
}
});
}
// ── Категории: добавление шаблона ────────────────────────────────────────
function addTemplateToCategory(pageName, type, mainName, additionalNames, callback, options) {
var cb = callback || function () {};
var opts = options || {};
var cfgByType = {
discuss: { action: 'обсуждение', template: 'Обсуждаемая категория', aliases: cfg.categoryTemplates.discuss },
deletion: { action: 'удаление', template: 'Обсуждаемая категория', aliases: cfg.categoryTemplates.discuss },
rename: { action: 'переименование', template: 'Категория к переименованию', needsMain: true, aliases: cfg.categoryTemplates.rename },
merge: { action: 'объединение', template: 'Категория к объединению', needsMain: true, aliases: cfg.categoryTemplates.merge }
};
var typeCfg = cfgByType[type];
if (!typeCfg) { cb({ code: 'error', info: 'Неизвестный тип номинации.' }); return; }
var dateStr = getDate()[0];
var parts = [dateStr];
if (typeCfg.needsMain) {
if (!mainName) { cb({ code: 'error', info: 'Не указано основное название.' }); return; }
parts.push(mainName);
if (additionalNames && additionalNames.length) Array.prototype.push.apply(parts, additionalNames);
}
var tplText = T_OPEN + typeCfg.template + '|' + parts.join('|') + T_CLOSE;
editPageContent(pageName, { summary: opts.summary || makeSummary('добавление шаблона номинации на ' + typeCfg.action), readError: 'Не удалось получить содержимое.' },
function (text) {
if (hasTemplateWithDateByPattern(text, typeCfg.aliases, dateStr)) {
return { skip: true, meta: { status: 'already_present' } };
}
return { text: wrapInNoinclude(text, tplText) };
},
function (err, meta) {
if (!err && meta && meta.status === 'already_present') {
logStatus('На странице ' + buildQuotedStatusPageLink(pageName) + ' уже есть шаблон номинации с датой ' + dateStr + '.', null, { trackError: false });
} else {
logPageEdit(pageName, err);
}
if (err) { cb(err); return; }
if (type === 'merge') {
addMergeTemplatesToTargets(pageName, mainName, additionalNames, dateStr, opts.summary, function () { cb(null); });
return;
}
cb(null);
}
);
}
function collectCategoryPageInputValues(selector) {
var pages = [];
$(selector).each(function () {
var title = normalizeCategoryPageName($(this).val() || '');
if (title && pages.indexOf(title) === -1) pages.push(title);
});
return pages;
}
function addTemplatesToCategories(pages, type, mainName, additionalNames, callback, options) {
var cb = callback || function () {};
var opts = options || {};
var processedPages = [];
eachSequential(pages || [], function (pageName, next) {
var pageMainName = mainName;
var pageAdditionalNames = additionalNames;
var pageTargets;
if (type === 'rename' && opts.renameTemplateTargetsByPage) {
pageTargets = opts.renameTemplateTargetsByPage[normTitle(pageName)];
if (Array.isArray(pageTargets)) {
pageMainName = pageTargets[0] || mainName;
pageAdditionalNames = pageTargets.slice(1);
} else if (pageTargets) {
pageMainName = pageTargets;
pageAdditionalNames = [];
}
}
addTemplateToCategory(pageName, type, pageMainName, pageAdditionalNames, function (err) {
if (!err) processedPages.push(pageName);
next(err || null);
}, { summary: opts.summary });
}, function (err) { cb(err, processedPages); });
}
function addMergeTemplatesToTargets(sourcePage, mainName, additionalNames, dateStr, summary, callback) {
var cb = callback || function () {};
var currentCatName = normTitle(stripCatPrefix(sourcePage));
var targets = [mainName].concat(additionalNames || []);
if (!targets.length) { cb(); return; }
eachSequential(targets, function (target, next) {
var targetPage = normalizeCategoryPageName(target);
addMergeTemplateToTargetCategory(targetPage, currentCatName, dateStr, summary, function (success, status) {
var url = getPageUrl(targetPage);
var linkHtml = '<a href="' + escapeHtml(url) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' + escapeHtml(targetPage) + '</a>';
if (success) {
var extra = (status === 'already_exists' || status === 'updated') ? ' (' + formatMergeStatus(status) + ')' : '';
logStatus('Шаблон добавлен в ' + linkHtml + extra + '.', null, { trackError: false });
} else {
logStatus('Ошибка при добавлении шаблона в ' + linkHtml + '.', { code: 'merge_target_failed', info: status }, { trackError: false });
}
next();
});
}, cb);
}
function addMergeTemplateToTargetCategory(targetPageName, sourceCatName, dateStr, summary, callback) {
if (typeof summary === 'function') {
callback = summary;
summary = '';
}
editPageContent(targetPageName, { summary: summary || makeSummary('добавление шаблона объединения'), readError: 'Не удалось получить содержимое' },
function (text) {
var existing = text.match(getCategoryMergeRe());
if (existing) {
var cats = existing[1].split('|').slice(1).map(function (p) { return p.trim(); }).filter(function (p) { return p.indexOf('=') === -1 && p.length > 0; });
var norm = sourceCatName.replace(/\s+/g, ' ').trim();
if (cats.some(function (c) { return c.replace(/\s+/g, ' ').trim() === norm; })) { return { skip: true, meta: { status: 'already_exists' } }; }
return {
text: text.replace(existing[0], function () { return existing[0].replace(/\}\}\s*$/, '|' + sourceCatName + '}}'); }),
summary: summary || makeSummary('дополнение шаблона объединения ' + formatCatLink(sourceCatName)),
meta: { status: 'updated' }
};
}
return { text: wrapInNoinclude(text, T_OPEN + 'Категория к объединению|' + dateStr + '|' + sourceCatName + T_CLOSE), meta: { status: 'created' } };
},
function (err, meta) { callback(!err, err ? err.info : ((meta && meta.status) || 'updated')); }
);
}
// ── Категории: обсуждение ────────────────────────────────────────────────
function buildCategoryDiscussionTitle(pageName, type, mainName, additionalNames, options) {
var opts = options || {};
var pages = Array.isArray(pageName) ? pageName : null;
var titleText;
if (pages && pages.length) {
titleText = String(opts.headerText || '').trim();
if (!titleText && type === 'rename' && opts.renameTargetsByPage) titleText = formatRenameItemsWithAnd(pages, opts.renameTargetsByPage);
if (!titleText) titleText = formatPagesWithAnd(pages, ':') + (type === 'deletion' ? ' → удалить' : '');
return '=== ' + titleText + ' ===';
}
var title = '=== [[:' + pageName + ']]';
if (type === 'rename' || type === 'merge') {
title += (type === 'rename' ? ' → ' : ' объединить с ') + formatCatLink(mainName);
if (additionalNames && additionalNames.length) {
var conj = type === 'rename' ? ' или ' : ' и ';
var head = additionalNames.slice(0, -1).map(formatCatLink).join(', ');
title += (additionalNames.length > 1 ? ', ' + head : '') + conj + formatCatLink(additionalNames[additionalNames.length - 1]);
}
} else if (type === 'deletion') {
title += ' → удалить';
}
return title + ' ===';
}
function getCategoryDiscussionSectionTitle(discussionTitle) {
return String(discussionTitle || '').replace(/^===\s*/, '').replace(/\s*===\s*$/, '').trim();
}
function createCategoryDiscussion(pageName, reason, type, callback, mainName, additionalNames, options) {
var cb = callback || function () {};
var opts = options || {};
var now = new Date();
var m = now.getUTCMonth(), year = now.getUTCFullYear(), day = now.getUTCDate();
var dateHeader = '== ' + day + ' ' + MONTHS_GEN[m] + ' ' + year + ' ==';
var discPage = 'Википедия:Обсуждение категорий/' + MONTHS_NOM[m] + '_' + year;
var discTitle = buildCategoryDiscussionTitle(pageName, type, mainName, additionalNames, opts);
var discBody = (opts.reasonIsPrepared ? String(reason || '') : appendNominationSignature(reason)).replace(/^\s+|\s+$/g, '');
var discText = discTitle + '\n' + discBody + '\n';
var sectionTitle = getCategoryDiscussionSectionTitle(discTitle);
publishNomination({
pageTitle: discPage,
readErrorMessage: 'Не удалось получить содержимое страницы обсуждения.',
summary: makeNominationPageSummary(sectionTitle),
createText: function () {
return T_OPEN + 'ОБК-Навигация' + T_CLOSE + '\n\n' + dateHeader + '\n\n' + discText;
},
buildText: function (text) {
var todayMatch = new RegExp('^' + escapeRegExp(dateHeader) + '\\s*$', 'm').exec(text);
if (!/\{\{\s*ОБК-Навигация\s*\}\}/i.test(text)) {
return { error: { code: 'insert_failed', info: 'Не найден шаблон ' + T_OPEN + 'ОБК-Навигация' + T_CLOSE + '.' } };
}
if (todayMatch) {
var dayContentStart = todayMatch.index + todayMatch[0].length;
var nextDayMatch = /^==[^=\n].*==\s*$/m.exec(text.slice(dayContentStart));
var insertAt = nextDayMatch ? dayContentStart + nextDayMatch.index : text.length;
return { text: insertDiscussionBlockAt(text, insertAt, discText, '\n\n') };
}
return { text: insertTopDiscussionSection(text, dateHeader + '\n\n' + discText) };
}
}, function (err) {
if (err) { cb(err); return; }
cb(null, { pageTitle: discPage, sectionTitle: sectionTitle });
});
}
// ── Категории: завершение ОБКАТ ───────────────────────────────────────────
function markCategoryDiscussionAsDone(pageName) {
runPageEditWithStatus({
pageName: pageName,
pendingText: 'Снимается шаблон обсуждения...',
errorText: 'Снятие шаблона обсуждения.',
successText: 'Шаблон обсуждения снят.',
editOptions: { summary: makeSummary('обсуждение категории завершено'), readError: 'Не удалось получить содержимое.' },
buildFn: function (text) {
var allTpls = [cfg.categoryTemplates.discuss, cfg.categoryTemplates.rename, cfg.categoryTemplates.merge].join('|');
var patterns = [
new RegExp('<noinclude>\\s*\\{\\{\\s*(' + allTpls + ')\\s*\\|\\s*([^\\}]+?)\\s*\\}\\}\\s*</noinclude>', 'i'),
new RegExp('\\{\\{\\s*(' + allTpls + ')\\s*\\|\\s*([^\\}]+?)\\s*\\}\\}', 'i')
];
var match = null;
for (var i = 0; i < patterns.length; i++) { match = text.match(patterns[i]); if (match) break; }
if (!match) return { error: { code: 'no_changes', info: 'Шаблон обсуждаемой категории не найден.' } };
return {
text: text.replace(match[0], '').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n'),
meta: { tplDate: convertToStandardDate(match[2].split('|')[0].trim()) }
};
},
onSuccess: function (meta) {
var talkStatusId = logStatus('Обновляется шаблон на СО категории...', null, { pending: true, trackError: false });
updateCategoryTalkPage(pageName, meta.tplDate, function (talkErr, info) {
if (talkErr) { logStatus('Установка шаблона на СО.', talkErr, { statusId: talkStatusId }); unlockModalSubmit(); return; }
logStatus(
(info && (info.status === 'already_present' || info.status === 'no_changes')) ? 'Шаблон на СО уже установлен.' : 'Шаблон установлен на СО.',
null, { statusId: talkStatusId, trackError: false }
);
renderModalFooter('reload');
});
}
});
}
function updateCategoryTalkPage(categoryName, templateDate, callback) {
var cb = callback || function () {};
var talkPage = getTalkPage(categoryName);
var newTpl = T_OPEN + 'Обсуждавшаяся категория|' + templateDate + T_CLOSE;
getTextWithTimestamp(talkPage, function (text, baseTimestamp, readErr) {
if (readErr) { cb(makeReadError(readErr, 'talk_read_failed', 'Не удалось получить содержимое СО категории.')); return; }
if (text === null) {
apiReq({ title: talkPage, text: newTpl + '\n\n', summary: makeSummary('добавление шаблона [[ш:Обсуждавшаяся категория]] с датой ' + templateDate), createonly: true },
'edit', function (resp) {
if (resp && resp.error) {
if (resp.error.code === 'articleexists') setTimeout(function () { updateCategoryTalkPage(categoryName, templateDate, cb); }, 1000);
else cb(resp.error);
} else cb(null, { status: 'created' });
});
return;
}
var talkResult = upsertDateSectionTemplateOnTalkPage(text, 'Обсуждавшаяся категория', templateDate, '', { aliases: cfg.categoryTemplates.discussed });
var newText = talkResult.text;
if (newText === text) { cb(null, { status: talkResult.status || 'no_changes' }); return; }
var summary = talkResult.status === 'created'
? makeSummary('добавление шаблона [[ш:Обсуждавшаяся категория]] с датой ' + templateDate)
: makeSummary('обновление шаблона [[ш:Обсуждавшаяся категория]], добавлена дата ' + templateDate);
var ep = {
title: talkPage,
text: newText,
summary: summary
};
if (baseTimestamp) ep.basetimestamp = baseTimestamp;
apiReq(ep, 'edit', function (resp) { cb(resp && resp.error ? resp.error : null, resp && !resp.error ? { status: talkResult.status } : null); });
});
}
// ── Быстрое объединение (Ctrl+клик КОБ) ─────────────────────────────────
function buildQuickMergeHtml(tplDate, targets, currentCatName) {
return joinHtml([
'<p>Найден шаблон с датой <strong>', escapeHtml(tplDate), '</strong>. Категории для объединения:</p>',
'<pre style="background:', tk.bgBase, ';color:', tk.cBase, ';padding:10px;border:1px solid ', tk.bSubS, ';border-radius:4px;margin-bottom:10px;">',
targets.map(function (c) { return '• ' + escapeHtml(c); }).join('\n'),
'</pre>',
'<p><strong>Текущая категория:</strong> ', escapeHtml(currentCatName), '</p>',
'<p style="color:', tk.cSub, ';">Шаблон будет добавлен во все указанные выше категории.</p>'
]);
}
function showQuickMergeModal() {
getText(mwCfg.wgPageName, function (text, readErr) {
if (readErr) { alert('Не удалось получить содержимое: ' + (readErr.info || readErr.code || 'ошибка API') + '.'); return; }
if (!text) { alert('Не удалось получить содержимое.'); return; }
var mergeRe = getCategoryMergeRe();
var match = text.match(mergeRe);
if (!match) { alert('В текущей категории не найден шаблон "Категория к объединению".'); return; }
var params = match[1].split('|').map(function (p) { return p.trim(); });
var tplDate = params[0];
var targets = params.slice(1);
if (!targets.length) { alert('В шаблоне не найдены целевые категории.'); return; }
createModal({ title: 'Быстрое добавление шаблона объединения' });
var currentCatName = normTitle(stripCatPrefix(mwCfg.wgPageName));
$('#removerModalContent').html(buildQuickMergeHtml(tplDate, targets, currentCatName));
renderModalFooter('submit', {
showCheckbox: false,
submitText: 'Добавить шаблоны',
onSubmit: function () {
startProcessing();
$('#removerSubmit').prop('disabled', true);
eachSequential(targets, function (target, next) {
var targetPage = normalizeCategoryPageName(target);
var targetLink = buildStatusPageLink(targetPage);
addMergeTemplateToTargetCategory(targetPage, currentCatName, tplDate, function (success, status) {
if (success) logStatus('Категория ' + targetLink + ' (' + formatMergeStatus(status) + ').', null, { trackError: false });
else logStatus('Ошибка ' + targetLink + '.', { code: 'merge_target_failed', info: status }, { trackError: false });
next();
});
}, function () {
if (isError) markSubmitError(); else renderModalFooter('close');
});
return true;
}
});
});
}
// ── ЗКА/Защита: публикация ───────────────────────────────────────────────
function getReporterContext(mode) {
var rawPage = mwCfg.wgPageName;
var pageName = normTitle(rawPage)
.replace(/(Special|Служебная):(Contributions|Вклад)\//i, 'User:')
.replace(/(User talk:|Обсуждение участни(ка|цы):)/i, 'User:');
var isUserRelated = /user|contrib|участни|вклад/i.test(rawPage);
var displayName = normTitle(rawPage)
.replace(/(Special|Служебная):(Вклад|Contributions)\//i, '')
.replace(/(User talk:|Обсуждение участни(ка|цы):)/i, '')
.replace(/(user|участни(к|ца)):/i, '');
var pageLink = '[[' + pageName + (isUserRelated ? '|' + displayName + ']]' : ']]');
var reportPage = mode === 'request' ? 'Википедия:Запросы к администраторам' : 'Википедия:Установка защиты';
return { pageName: pageName, pageLink: pageLink, displayName: displayName, reportPage: reportPage };
}
function getTopDiscussionInsertIndex(pageText) {
var introEnd = getIntroBlockEndIndex(pageText);
var firstHeading = /^==[^=\n].*==\s*$/m.exec(pageText.slice(introEnd));
return firstHeading ? introEnd + firstHeading.index : introEnd;
}
function getIntroBlockEndIndex(pageText) {
var pos = 0;
while (pos < pageText.length) {
var next = skipWhitespace(pageText, pos);
var end;
if (pageText.slice(next, next + 4) === '<!--') {
end = pageText.indexOf('-->', next + 4);
if (end === -1) return next;
pos = end + 3;
continue;
}
end = skipTemplate(pageText, next);
if (end !== next) {
pos = end;
continue;
}
return next;
}
return pos;
}
function skipWhitespace(text, pos) {
while (pos < text.length && /\s/.test(text.charAt(pos))) pos++;
return pos;
}
function skipTemplate(text, pos) {
var depth = 0;
var i = pos;
if (text.slice(pos, pos + 2) !== '{{') return pos;
while (i < text.length) {
if (text.slice(i, i + 4) === '<!--') {
var commentEnd = text.indexOf('-->', i + 4);
if (commentEnd === -1) return pos;
i = commentEnd + 3;
continue;
}
if (text.slice(i, i + 2) === '{{') {
depth++;
i += 2;
continue;
}
if (text.slice(i, i + 2) === '}}') {
depth--;
i += 2;
if (depth === 0) return i;
continue;
}
i++;
}
return pos;
}
function insertDiscussionBlockAt(pageText, insertAt, blockText, separator) {
var sep = separator || '\n\n';
var before = pageText.slice(0, insertAt).replace(/\s+$/, '');
var block = String(blockText || '').replace(/\s+$/, '');
var after = pageText.slice(insertAt).replace(/^\s+/, '');
return (before ? before + sep : '') + block + (after ? sep + after : '\n');
}
function insertTopDiscussionSection(pageText, sectionText) {
return insertDiscussionBlockAt(pageText, getTopDiscussionInsertIndex(pageText), sectionText, '\n\n');
}
function doReport(ctx, fast, protectMode) {
var header = $('#rmReportHeader').val() || ctx.pageLink;
var text = $('#rmReportText').val() || '';
var isZka = ctx.reportPage === 'Википедия:Запросы к администраторам';
var isRemoveProtect = !isZka && protectMode === 'remove';
startProcessing();
var targetPage, editParams, sectionForLink;
if (fast) {
targetPage = 'Википедия:Запросы к администраторам/Быстрые';
sectionForLink = null;
editParams = {
appendtext: '\n\n' + T_OPEN + 'sub' + 'st:t:preload/ЗКАБ/subst|\n | участник = ' + ctx.displayName +
'| страница = | пояснение = ' + text + T_CLOSE + '\n',
summary: makeSummary('новый запрос [[Special:Contributions/' + ctx.displayName + ']]')
};
} else if (isZka) {
targetPage = ctx.reportPage;
sectionForLink = extractDisplayedText(header);
var isIpFull = /^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/.test(ctx.displayName);
editParams = {
text: '== ' + header + ' ==\n\n* ' + T_OPEN + 'userlinks|' + ctx.displayName + (isIpFull ? '|ip=1' : '') + T_CLOSE + '\n' + text + ' ~~' + '~~',
summary: '/* ' + sectionForLink + ' */ ' + makeSummary('новый запрос')
};
} else {
targetPage = isRemoveProtect ? 'Википедия:Снятие защиты' : 'Википедия:Установка защиты';
var pages = collectInputValues('.rmProtectPageInput');
if (!pages.length) pages = [ctx.pageName];
var sectionTitle, pageLines;
if (pages.length === 1) {
sectionTitle = '[[' + pages[0] + ']]';
pageLines = '* ' + T_OPEN + 'pagelinks-protect|' + pages[0] + T_CLOSE;
} else {
sectionTitle = ($('#rmProtectHeader').val() || '').trim() || pages.map(function (p) { return '[[' + p + ']]'; }).join(', ');
pageLines = pages.map(function (p) { return '* ' + T_OPEN + 'pagelinks-protect|' + p + T_CLOSE; }).join('\n');
}
sectionForLink = extractDisplayedText(sectionTitle);
editParams = {
section: 'new',
sectiontitle: sectionTitle,
text: pageLines + '\n' + text + ' ~~' + '~~',
summary: '/* ' + sectionForLink + ' */ ' + makeSummary('новый запрос')
};
}
var statusId = logStatus('Публикуется запрос на «' + targetPage + '»...', null, { pending: true, trackError: false });
if (isZka && !fast) {
editPageContent(targetPage, {
summary: editParams.summary,
assertuser: mwCfg.wgUserName,
readError: 'Не удалось получить содержимое страницы «' + targetPage + '».'
}, function (pageText) {
return { text: insertTopDiscussionSection(pageText, editParams.text) };
}, function (err) {
if (err) {
logStatus('Публикация запроса на «' + targetPage + '».', err, { statusId: statusId });
markSubmitError();
$('#rmReportFast').prop('disabled', false);
return;
}
logStatus('Запрос опубликован.', null, { statusId: statusId, trackError: false });
appendNominationLink(targetPage, sectionForLink);
if (sectionForLink) subscribeToTopic(targetPage, sectionForLink);
renderModalFooter('reload');
});
return;
}
apiReq($.extend({ title: targetPage }, editParams), 'edit', function (resp) {
if (resp && resp.error) {
logStatus('Публикация запроса на «' + targetPage + '».', resp.error, { statusId: statusId });
markSubmitError();
if (isZka) $('#rmReportFast').prop('disabled', false);
return;
}
logStatus('Запрос опубликован.', null, { statusId: statusId, trackError: false });
appendNominationLink(targetPage, sectionForLink);
if (sectionForLink) subscribeToTopic(targetPage, sectionForLink);
renderModalFooter('reload');
});
}
// ═══════════════════════════════════════════════════════════════════════════
// ДИСПЕТЧЕР
// ═══════════════════════════════════════════════════════════════════════════
function handleMenuClick(item, event) {
isError = false;
var op = OPERATIONS_MAP[item.id];
// Специальный случай: КОБ категории с Ctrl — быстрое добавление шаблона
if (item.id === 'cat-merge' && event && event.ctrlKey) {
showQuickMergeModal();
return;
}
if (!op) {
console.error('RemoverCore: неизвестная операция', item.id);
return;
}
var handlerFn = handlers[op.handler];
if (typeof handlerFn !== 'function') {
console.error('RemoverCore: обработчик не найден', op.handler);
return;
}
handlerFn(op, event);
}
// ─── Экспорт ─────────────────────────────────────────────────────────────
window.RemoverCore = { handleMenuClick: handleMenuClick };
}());
9xiltohxoqm72lcq66cwm73bi3knure
749755
749744
2026-07-03T19:25:13Z
Solidest
54422
749755
javascript
text/javascript
/**
* Remover — ядро (core).
* Загружается лениво при первом клике по пункту меню.
* Ожидает, что window.RemoverState уже задан remover-loader.js.
*
* Архитектура: единый реестр OPERATIONS описывает все операции (КБУ, КУ, КПМ, КУЛ, КОБ, КРАЗД, ВУС, Защита, Запрос, Снятие, кат-варианты).
* Логика выполнения сосредоточена в универсальных обработчиках.
* Экспортирует: window.RemoverCore.handleMenuClick(item, event)
*/
(function () {
'use strict';
var state = window.RemoverState;
if (!state) { console.error('RemoverCore: window.RemoverState не задан.'); return; }
var mwCfg = state.mwCfg;
var cfg = applyCoreConfigDefaults(state.cfg || {});
var isCategory = state.isCategory;
var isVector22 = state.isVector22;
var scriptLink = cfg.scriptLink;
var settingsOptionName = state.settingsOptionName || 'userjs-remover-settings';
var settingsVersion = 1;
var settingsMenuMeta = collectSettingsMenuMeta();
var settingsArticleItemLabels = settingsMenuMeta.articleLabels;
var settingsCategoryItemLabels = settingsMenuMeta.categoryLabels;
var settingsItemLabelById = settingsMenuMeta.idToLabel;
var settingsItemLabelByNorm = settingsMenuMeta.labelByNorm;
var settingsItemLabelOrder = settingsMenuMeta.labelOrder;
var settingsDefaults = getDefaultSettings();
var MENU_TITLE_PRESET_CACTIONS = '__remover_portlet_cactions__';
var MENU_TITLE_PRESET_PAGE = '__remover_portlet_page__';
var MENU_TITLE_PRESET_TOOLS = '__remover_portlet_tools__';
var initialSettings = normalizeRemoverSettings(readSettingsOptionState(state.settings || {}));
var setAlert = ('setAlert' in state) ? !!state.setAlert : initialSettings.notifyAuthor;
var setSubscribe = ('setSubscribe' in state) ? !!state.setSubscribe : initialSettings.subscribeTopic;
var signatureSeparator = ('signatureSeparator' in state && typeof state.signatureSeparator === 'string')
? state.signatureSeparator.trim()
: initialSettings.signatureSeparator;
initialSettings.notifyAuthor = setAlert;
initialSettings.subscribeTopic = setSubscribe;
initialSettings.signatureSeparator = signatureSeparator;
state.cfg = cfg;
state.settings = clonePlainObject(initialSettings);
state.setAlert = setAlert;
state.setSubscribe = setSubscribe;
// ─── Константы ──────────────────────────────────────────────────────────
var MONTHS_NOM = ['Январь','Февраль','Март','Апрель','Май','Июнь','Июль','Август','Сентябрь','Октябрь','Ноябрь','Декабрь'];
var MONTHS_GEN = ['января','февраля','марта','апреля','мая','июня','июля','августа','сентября','октября','ноября','декабря'];
var MONTHS_NOM_LOWER = MONTHS_NOM.map(function (m) { return m.toLowerCase(); });
var T_OPEN = '{' + '{';
var T_CLOSE = '}' + '}';
var KBU_CRITERIA_PAGE = 'Википедия:Критерии быстрого удаления';
var DATE_SECTION_TALK_TEMPLATES = {
ret: 'Оставлено',
withdrawnDeletion: 'Снято с удаления',
redirectedDeletion: {
name: 'Заменено перенаправлением',
sectionParam: 'раздел обсуждения',
singleSectionParam: true
}
};
var TALK_TEMPLATE_ALIASES = {
'Оставлено': ['Оставлена', 'Kept'],
'Заменено перенаправлением': ['Удалено', 'Перенаправлено'],
'Переименовано': ['Renamed'],
'Обсуждавшаяся категория': ['Обсуждалась', 'Обсуждалось']
};
var CATEGORY_NOMINATION_META = {
discuss: { title: 'Номинация: обсуждение', actionText: 'к обсуждению', supportsMulti: true },
deletion: { title: 'Номинация: к удалению', actionText: 'к удалению', supportsMulti: true },
rename: { title: 'Номинация: к переименованию', actionText: 'к переименованию', supportsMulti: true, renameMulti: true },
merge: { title: 'Номинация: к объединению', actionText: 'к объединению' }
};
var RE_ESCAPE = /[.*+?^${}()|[\]\\]/g;
var RE_KU_ON_PAGE = /\{\{\s*(?:к\s*удалению|ку)\s*(?:\||\}\})/i;
var RE_KPM_ON_PAGE = /\{\{\s*(?:к\s*переименованию|кпм|rename)\s*(?:\||\}\})/i;
var RE_KBU_ON_PAGE = /\{\{\s*(?:subst\s*:\s*)?(?:db\s*-[^|}\s]+|уд\s*-[^|}\s]+|к\s*быстрому\s*удалению|к\s*отсроченному\s*удалению|deleteslow|ds|hang\s*-?\s*on)\s*(?:\||\}\})/i;
var RE_KUL_ON_PAGE = /\{\{\s*(?:subst\s*:\s*)?к\s*улучшению\s*(?:\||\}\})/i;
var KBU_PATTERN_STR = 'db\\s*-[^|}\\s]+|уд\\s*-[^|}\\s]+|к\\s*быстрому\\s*удалению|к\\s*отсроченному\\s*удалению|deleteslow|ds';
var RE_KBU_PATTERNS = new RegExp('(?:' + KBU_PATTERN_STR + ')', 'i');
var KUL_PATTERN_STR = 'к\\s*улучшению';
var RE_KUL_PATTERN = new RegExp(KUL_PATTERN_STR, 'i');
var HANGON_PATTERN_STR = 'hang\\s*-?\\s*on';
var RE_HANGON = new RegExp(HANGON_PATTERN_STR, 'i');
var MOVE_TO_COMMONS_TEMPLATE_TITLE = 'Шаблон:Перенести на Викисклад';
var MOVE_TO_COMMONS_TEMPLATE_ALIASES = [
'Перенести на Викисклад',
'На склад',
'To Commons',
'Copy to Wikimedia Commons',
'На викисклад',
'На Викисклад',
'To commons',
'Mtc',
'Перенести в Commons',
'MTC',
'Move to Commons',
'Перенести на викисклад'
];
var BADFAIRUSE_OPTIONS = [
['1', 'несвободная карта'],
['1+', 'карта Google Maps или Google Earth'],
['2', 'несвободное изображение утилитарного объекта'],
['3', 'несвободное изображение животного, растения или гриба'],
['6', 'несвободное изображение на странице-списке эпизодов или серий'],
['7', 'несвободная анимация или отрывок из фильма'],
['10', 'несвободное изображение здания, сооружения и памятника'],
['11', 'несвободная фотография ныне живущего человека'],
['12', 'несвободная схема или чертёж']
];
var DFUD_REASON_OPTIONS = [
['1', 'может быть заменено свободным эквивалентом'],
['2', 'разрешение/качество наносит имущественный ущерб правообладателю'],
['3', 'не отвечает критерию минимального использования или не имеет значимости'],
['4', 'не публиковалось ранее или не имеет проверяемого источника'],
['9', 'используется вне пространства статей или не используется в статьях'],
['10', 'не имеет описания и/или обоснования добросовестности использования']
];
var FILE_DATE_DEADLINE_HINT = 'Срок до возможного удаления — 7 дней с даты установки.';
var RE_DATE_ISO = /^(\d{4})-(\d{2})-(\d{2})$/;
var RE_DATE_RUSSIAN = /^(\d{1,2})\s+([\u0430-\u044f\u0410-\u042f\u0451\u0401.]+)\s+(\d{2}|\d{4})$/;
var RE_DATE_DASH = /^(\d{1,4})\s*-\s*(\d{1,2})\s*-\s*(\d{1,4})$/;
var RE_DATE_DOT = /^(\d{1,2})\s*\.\s*(\d{1,2})\s*\.\s*(\d{2}|\d{4})$/;
var RE_DATE_SLASH = /^(\d{1,4})\s*\/\s*(\d{1,2})\s*\/\s*(\d{1,4})$/;
var RE_NOINCLUDE = /(\s*)<noinclude>([\s\S]*?)<\/noinclude>/i;
var RE_TEMPLATE_NS = /^(?:шаблон|template)\s*:\s*/i;
// ─── Глобальные переменные сессии ────────────────────────────────────────
var isError = false;
var logStatusSeq = 0;
var resizeObservers = [];
var modalLayoutSyncHandlers = [];
var tplAliasCache = {};
// ─── Стили ───────────────────────────────────────────────────────────────
var stStyles = cfg.modalStyles;
var tk = {
cBase: 'var(--color-base, #202122)',
cSub: 'var(--color-subtle, #72777d)',
cSubM: 'var(--color-subtle, #54595d)',
cInv: 'var(--color-inverted-fixed, #fff)',
cProg: 'var(--color-progressive, #3366cc)',
cProgH: 'var(--color-progressive--hover, #2a4b8d)',
cDang: 'var(--color-destructive, #d73333)',
cDis: 'var(--color-disabled, var(--color-subtle, #72777d))',
bgBase: 'var(--background-color-base, #fff)',
bgNSub: 'var(--background-color-neutral-subtle, #f8f9fa)',
bgN: 'var(--background-color-neutral, #eaecf0)',
bgDis: 'var(--background-color-disabled, var(--background-color-neutral, #eaecf0))',
bgProg: 'var(--background-color-progressive, #3366cc)',
bgProgH:'var(--background-color-progressive--hover, #2a4d8f)',
bgSucc: 'var(--background-color-success, #14866d)',
bgSuccH:'var(--background-color-success--hover, #0f6d57)',
bSub: 'var(--border-color-subtle, #a2a9b1)',
bSubS: 'var(--border-color-subtle, #ddd)',
bDis: 'var(--border-color-disabled, var(--border-color-subtle, #a2a9b1))',
bProg: 'var(--border-color-progressive, #3366cc)',
bProgH: 'var(--border-color-progressive--hover, #2a4d8f)',
bSucc: 'var(--border-color-success, #14866d)',
bSuccH: 'var(--border-color-success--hover, #0f6d57)'
};
var sz = {
taH: '180px',
taMinH: '100px',
taMinW: '180px',
mobileBp: 720,
modalRatio: 0.4,
modalMinWide: 420,
modalDefaultWide: 720,
viewportGap: 24,
touchDesktopGap: 120
};
var btnBase = 'border-radius:4px;padding:8px 16px;cursor:pointer;font-size:14px;font-family:inherit;transition:background .1s;';
var neutralVis = 'background:' + tk.bgNSub + ';border:1px solid ' + tk.bSub + ';color:' + tk.cBase + ';border-radius:4px;';
var stCancel = neutralVis + btnBase;
var stSubmit = 'background:' + tk.bgProg + ';color:' + tk.cInv + ';border:1px solid ' + tk.bProg + ';' + btnBase;
var stReload = 'background:' + tk.bgSucc + ';color:' + tk.cInv + ';border:1px solid ' + tk.bSucc + ';' + btnBase;
var stInputBox = 'flex:1;padding:6px;box-sizing:border-box;min-width:0;border:1px solid ' + tk.bSub + ';background:' + tk.bgBase + ';color:inherit;border-radius:2px;';
var stInputFull= 'width:100%;padding:6px;box-sizing:border-box;border:1px solid ' + tk.bSub + ';border-radius:2px;background:' + tk.bgBase + ';color:inherit;margin-bottom:10px;';
var stRow = 'display:flex;margin-bottom:6px;';
var inlineControlGap = 4;
var squareControlSize = 32;
var leftNestedControlOffset = (squareControlSize + inlineControlGap) + 'px';
var stToolBtn = neutralVis + 'padding:4px 8px;margin-left:' + inlineControlGap + 'px;cursor:pointer;font-size:12px;';
var stRemoveBtn= neutralVis + 'display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;padding:0;width:' + squareControlSize + 'px;height:' + squareControlSize + 'px;margin-left:' + inlineControlGap + 'px;cursor:pointer;font-size:12px;line-height:1;';
var stFooterWrap = 'display:flex;align-items:center;gap:8px;flex-wrap:wrap;';
var stFooterChecks = 'display:flex;flex-direction:column;gap:4px;margin-right:auto;flex:1 1 220px;min-width:0;';
var stFooterCheckLabel = 'display:inline-flex;align-items:flex-start;gap:6px;font-size:14px;line-height:1.4;max-width:100%;';
var stFooterActions = 'display:flex;gap:6px;flex:1 1 auto;min-width:0;max-width:100%;flex-wrap:wrap;justify-content:flex-end;margin-left:auto;';
var stHeaderIconBtn = 'margin:0 0 0 auto;width:32px;min-width:32px;height:32px;padding:0;display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;border:1px solid ' + tk.bSub + ';border-radius:4px;background:' + tk.bgNSub + ';color:' + tk.cSubM + ';cursor:pointer;font-size:16px;line-height:1;';
var multiNominationGap = '6px';
var RESIZE_CLASS = 'rm-resizable';
// ═══════════════════════════════════════════════════════════════════════════
// РЕЕСТР ОПЕРАЦИЙ
// Каждая запись описывает одну кнопку меню. Поля:
// id — идентификатор (совпадает с item.id из loader)
// handler — имя метода-обработчика в объекте handlers
// handlerArg — аргумент, передаваемый в handler (опционально)
// ═══════════════════════════════════════════════════════════════════════════
var OPERATIONS = [
// ── Статьи ──────────────────────────────────────────────────────────
{
id: 'fRm',
label: 'КБУ',
handler: 'showKbu',
// Параметры номинации: заполняются при submit
nomination: {
pageTitle: function (pg) { return normTitle(pg); },
// шаблон встраивается в статью, номинационная страница отсутствует
inArticle: true
}
},
{
id: 'tRm',
label: 'КУ',
handler: 'showNomination',
nomination: {
comment: 'к удалению',
template: 'к удалению',
navTemplate: 'КУ',
nomPage: function (date) { return 'Википедия:К удалению/' + date; },
supportsMulti: true,
multiOpId: 'mRm',
supportsTransfer: true,
// шаблон встраивается в статью через <noinclude>
inArticle: true,
articleTpl: function (tplpar, date) { return 'к удалению|' + date + (tplpar ? '|' + tplpar : ''); }
}
},
{
id: 'rnm',
label: 'КПМ',
handler: 'showNomination',
nomination: {
comment: 'к переименованию',
template: 'к переименованию',
navTemplate: 'КПМ',
nomPage: function (date) { return 'Википедия:К переименованию/' + date; },
supportsMulti: true,
multiOpId: 'mRnm',
inArticle: true,
articleTpl: function (tplpar, date) { return 'к переименованию|' + date + (tplpar ? '|' + tplpar : ''); },
extraInput: {
type: 'rename',
firstId: 'rmRenameFirst', inputClass: 'rmRenameInput',
firstPh: 'Новое название',
addBtnId: 'rmAddRename', addBtnLabel: 'Добавить вариант',
containerId: 'rmRenameContainer', addPh: 'Дополнительный вариант',
maxRows: 2, maxMsg: 'Максимум 3 варианта переименования.'
}
}
},
{
id: 'imp',
label: 'КУЛ',
handler: 'showNomination',
nomination: {
comment: 'к срочному улучшению',
template: 'к улучшению',
navTemplate: 'КУЛ',
nomPage: function (date) { return 'Википедия:К улучшению/' + date; },
inArticle: true,
articleTpl: function (tplpar, date) { return 'к улучшению|' + date + (tplpar ? '|' + tplpar : ''); }
}
},
{
id: 'merge',
label: 'КОБ',
handler: 'showNomination',
nomination: {
comment: 'к объединению с другой',
summaryAction: 'к объединению',
template: 'к объединению',
navTemplate: 'КОБ',
nomPage: function (date) { return 'Википедия:К объединению/' + date; },
inArticle: true,
articleTpl: function (tplpar, date) { return 'к объединению|' + date + (tplpar ? '|' + tplpar : ''); },
extraInput: {
type: 'merge',
firstId: 'rmMergeFirst', inputClass: 'rmMergeInput',
firstPh: 'Объединить с…',
addBtnId: 'rmAddMerge', addBtnLabel: '+',
containerId: 'rmMergeContainer', addPh: 'Дополнительная статья',
maxRows: 10, maxMsg: 'Максимум 11 статей для объединения.'
}
}
},
{
id: 'split',
label: 'КРАЗД',
handler: 'showNomination',
nomination: {
comment: 'к разделению',
template: 'к разделению',
navTemplate: 'КР',
nomPage: function (date) { return 'Википедия:К разделению/' + date; },
inArticle: true,
articleTpl: function (tplpar, date) { return 'к разделению|' + date + (tplpar ? '|' + tplpar : ''); },
extraInput: {
type: 'split',
firstId: 'rmSplitFirst', inputClass: 'rmSplitInput',
firstPh: 'Разделить на…',
addBtnId: 'rmAddSplit', addBtnLabel: '+',
containerId: 'rmSplitContainer', addPh: 'Дополнительная статья'
}
}
},
{
id: 'recov',
label: 'ВУС',
handler: 'showNomination',
nomination: {
comment: '',
template: 'к восстановлению',
navTemplate: 'ВУС',
nomPage: function (date) { return 'Википедия:К восстановлению/' + date; },
inArticle: false // шаблон не ставится в (удалённую) статью
}
},
{
id: 'close',
label: 'Снятие',
handler: 'showArticleClose'
},
// ── Запросы ─────────────────────────────────────────────────────────
{
id: 'protect',
label: 'Защита',
handler: 'showReport',
reportMode: 'protect'
},
{
id: 'request',
label: 'Запрос',
handler: 'showReport',
reportMode: 'request'
},
// ── Категории ────────────────────────────────────────────────────────
{
id: 'cat-fRm',
label: 'КБУ',
handler: 'showKbu',
forCategory: true
},
{
id: 'cat-discuss',
label: 'Обсудить',
handler: 'showCatNomination',
catType: 'discuss'
},
{
id: 'cat-delete',
label: 'Удалить',
handler: 'showCatNomination',
catType: 'deletion'
},
{
id: 'cat-rename',
label: 'Переименовать',
handler: 'showCatNomination',
catType: 'rename'
},
{
id: 'cat-merge',
label: 'Объединить',
handler: 'showCatNomination',
catType: 'merge'
},
{
id: 'cat-done',
label: 'Снятие',
handler: 'showCatClose'
}
];
// Быстрый поиск по id
var OPERATIONS_MAP = {};
OPERATIONS.forEach(function (op) { OPERATIONS_MAP[op.id] = op; });
// ─── Тексты переноса (КБУ → КУ) ─────────────────────────────────────────
var transferTexts = {
kbu: { notice: 'Перенесено с быстрого удаления.', hint: 'Шаблоны КБУ и Hangon будут сняты.' },
kul: { notice: 'Перенесено с КУЛ.', hint: 'Шаблоны КУЛ будут сняты.' },
both: { notice: 'Перенесено с быстрого удаления и КУЛ.', hint: 'Шаблоны КБУ, КУЛ и Hangon будут сняты.' }
};
// ═══════════════════════════════════════════════════════════════════════════
// УТИЛИТЫ
// ═══════════════════════════════════════════════════════════════════════════
function escapeRegExp(s) { return s.replace(RE_ESCAPE, '\\$&'); }
function escapeHtml(s) {
return String(s || '')
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
.replace(/"/g, '"').replace(/'/g, ''');
}
function joinHtml(parts) { return parts.join(''); }
function ucfirst(s) { return s ? s.charAt(0).toUpperCase() + s.slice(1) : ''; }
function padTwo(n) { return n < 10 ? '0' + n : String(n); }
function expandTwoDigitYear(value) {
return 2000 + parseInt(value, 10);
}
function monthToNumber(name) {
var lower = name.toLowerCase().replace(/\.$/, '');
var idx = MONTHS_GEN.indexOf(lower);
if (idx === -1) idx = MONTHS_NOM_LOWER.indexOf(lower);
if (idx === -1 && lower.length >= 3) {
for (var i = 0; i < MONTHS_GEN.length; i++) {
if (MONTHS_GEN[i].indexOf(lower) === 0 || MONTHS_NOM_LOWER[i].indexOf(lower) === 0) return i + 1;
}
}
return idx + 1;
}
function makeStandardDate(yearValue, monthValue, dayValue) {
var yearText = String(yearValue || '').trim();
var year = yearText.length === 2 ? expandTwoDigitYear(yearText) : parseInt(yearText, 10);
var month = parseInt(monthValue, 10);
var day = parseInt(dayValue, 10);
var maxDay;
if ((yearText.length !== 2 && yearText.length !== 4) || isNaN(year) || isNaN(month) || isNaN(day) || year < 1 || month < 1 || month > 12 || day < 1) return null;
maxDay = new Date(Date.UTC(year, month, 0)).getUTCDate();
if (day > maxDay) return null;
return year + '-' + padTwo(month) + '-' + padTwo(day);
}
function normalizeIsoDate(value) {
var m = String(value || '').trim().match(RE_DATE_ISO);
return m ? makeStandardDate(m[1], m[2], m[3]) : null;
}
function normalizeTemplateName(name) {
return (name || '').toLowerCase().replace(RE_TEMPLATE_NS, '').replace(/_/g, ' ').replace(/\s+/g, ' ').trim();
}
function toTemplateNameList(names) {
if (Array.isArray(names)) return names;
return String(names || '').split('|');
}
function getTalkTemplateAliases(templateName) {
return TALK_TEMPLATE_ALIASES[templateName] || [];
}
function buildTemplateNamePattern(names) {
var seen = {};
var patterns = [];
toTemplateNameList(names).forEach(function (name) {
var normalized = normalizeTemplateName(String(name || '').replace(/^(?:safe)?subst\s*:\s*/i, ''));
if (!normalized || seen[normalized]) return;
seen[normalized] = true;
patterns.push(escapeRegExp(normalized).replace(/\s+/g, '[ _]*'));
});
return patterns.join('|');
}
function buildTemplateTransclusionRegex(templateName, aliases, flags) {
var names = [templateName].concat(getTalkTemplateAliases(templateName), toTemplateNameList(aliases));
var namePattern = buildTemplateNamePattern(names);
return namePattern
? new RegExp('\\{\\{\\s*(?:(?:safe)?(?:subst|подст)\\s*:\\s*)?(?:(?:шаблон|template)\\s*:\\s*)?(' + namePattern + ')\\s*([^}]*)\\}\\}', flags || 'i')
: null;
}
function getDate(dateString) {
var d = dateString ? new Date(dateString) : new Date();
var iso = d.getUTCFullYear() + '-' + padTwo(d.getUTCMonth() + 1) + '-' + padTwo(d.getUTCDate());
var rus = d.getUTCDate() + ' ' + MONTHS_GEN[d.getUTCMonth()] + ' ' + d.getUTCFullYear();
return [iso, rus];
}
function convertToStandardDate(dateStr) {
var value = String(dateStr || '').replace(/\s+/g, ' ').trim();
var m;
var mo;
var normalized;
m = value.match(RE_DATE_ISO);
if (m) return normalizeIsoDate(value) || '';
m = value.match(RE_DATE_RUSSIAN);
if (m) {
mo = monthToNumber(m[2]);
normalized = mo ? makeStandardDate(m[3], mo, m[1]) : null;
return normalized || '';
}
m = value.match(RE_DATE_DASH);
if (m) {
normalized = makeStandardDate(m[1], m[2], m[3]);
return normalized || '';
}
m = value.match(RE_DATE_DOT);
if (m) {
normalized = makeStandardDate(m[3], m[2], m[1]);
return normalized || '';
}
m = value.match(RE_DATE_SLASH);
if (m) {
normalized = makeStandardDate(m[1], m[2], m[3]);
return normalized || '';
}
return value;
}
function getNamespaceLabel(ns, fallback) {
return (mwCfg.wgFormattedNamespaces && mwCfg.wgFormattedNamespaces[ns]) || fallback;
}
function getTalkPage(pageName) {
var match = /([^:]*:)?(.*)/.exec(pageName);
if (match[1]) {
var ns = mwCfg.wgNamespaceIds && mwCfg.wgNamespaceIds[match[1].slice(0, -1).toLowerCase().replace(/ /g, '_')];
if (ns !== undefined) return getNamespaceLabel(ns | 1, 'Обсуждение') + ':' + match[2];
}
return getNamespaceLabel(1, 'Обсуждение') + ':' + pageName;
}
function normTitle(s) { return (s || '').replace(/_/g, ' '); }
function stripCatPrefix(s) { return (s || '').replace(/^(?:Категория|Category):\s*/i, ''); }
function normalizeRedirectTarget(value) {
var title = normTitle(String(value || '').trim());
var linkMatch = title.match(/^\[\[:?([^|\]]+)(?:\|[^\]]*)?\]\]$/);
return linkMatch ? normTitle(linkMatch[1]).trim() : title;
}
function buildRedirectText(targetTitle) {
return '#REDIRECT [[' + targetTitle + ']]';
}
function getCategoryNamespaceLabel() {
return getNamespaceLabel(14, 'Категория');
}
function normalizeCategoryPageName(value) {
var title = normTitle(value).trim();
var nsMatch, nsKey, ns;
if (!title) return '';
nsMatch = title.match(/^([^:]+):(.+)$/);
if (nsMatch) {
nsKey = nsMatch[1].toLowerCase().replace(/ /g, '_');
ns = mwCfg.wgNamespaceIds && mwCfg.wgNamespaceIds[nsKey];
if (ns === 14 || nsKey === 'category' || nsKey === 'категория') return getCategoryNamespaceLabel() + ':' + nsMatch[2].trim();
return getCategoryNamespaceLabel() + ':' + title;
}
return getCategoryNamespaceLabel() + ':' + title;
}
function makeSummary(text) { return text + ' (' + scriptLink + ')'; }
function makeNominationPageSummary(sectionTitle) {
var section = normalizeSectionForLink(sectionTitle);
return (section ? '/* ' + section + ' */ ' : '') + makeSummary('добавление номинации');
}
function makeNominationTargetSummary(pageTitle, sectionTitle, actionText) {
var section = normalizeSectionForLink(sectionTitle);
var target = pageTitle + (section ? '#' + section : '');
var label = 'номинация' + (actionText ? ' ' + actionText : '');
return makeSummary('[[' + target + '|' + label + ']]');
}
function getNominationSummaryAction(nomination) {
return String(nomination && (nomination.summaryAction || nomination.comment || nomination.template) || '')
.trim();
}
function appendNominationSignature(text) {
var body = String(text || '');
return body + (signatureSeparator ? ' ' + signatureSeparator : '') + ' ~~' + '~~';
}
function extractDisplayedText(s) {
return (s || '').replace(/\[\[:?(?:[^|\]]+\|)?(.+?)\]\]/g, '$1');
}
function collectInputValues(selector) {
return $(selector).map(function () { return $(this).val().trim(); }).get().filter(Boolean);
}
function applyCoreConfigDefaults(config) {
var defaults = {
scriptLink: '[[Википедия:Гаджеты/Remover|Remover]]',
fastRemoveReasons: {
general: [
['уд-бессвязно', 'О1 Бессвязный текст'],
['уд-тест', 'О2 Тестовая страница'],
['уд-ванд', 'О3.1 Вандальная страница'],
['уд-мист', 'О3.2 Мистификация'],
['уд-повторно', 'О4 Уже удалялось'],
['уд-автор', 'О5 По просьбе автора'],
['уд-под', 'О6 Ненужная подстраница'],
['уд-переим', 'О7 Для переименования'],
['уд-дубль', 'О8 Дубликат'],
['уд-реклама', 'О9 Реклама или спам'],
['уд-нецелевая', 'О10 Нецелевая СО'],
['уд-копивио', 'О11 Нарушение АП']
],
articles: [
['подст:ds', 'ds Отсроченное пусто или коротко', 'С'],
['уд-пусто', 'С1 Пусто или коротко'],
['уд-иностр', 'С2 Не на русском'],
['уд-ссылки', 'С3 Лишь ссылки'],
['уд-нз', 'С5 Явно незначимо'],
['уд-бям', 'С7 Создано нейросетью']
],
redirects: [
['уд-в никуда', 'П1 Перенапр. в никуда'],
['уд-мпр', 'П2 Межпростр. перенапр.'],
['уд-опечатка', 'П3 Перенапр. с ошибкой в названии'],
['уд-падеж', 'П4 Не именительный падеж'],
['уд-смысл', 'П5 Неверное перенапр.'],
['уд-перобс', 'П6 Перенапр. на СО']
],
files: [
['Db-duplicate', 'Ф1 Копия файла'],
['Db-badimage', 'Ф2 Повреждённый файл'],
['подст:No license', 'Ф3.1 Нет данных о лицензии'],
['подст:No source', 'Ф3.2 Нет данных об источнике'],
['подст:No author', 'Ф3.3 Нет данных об авторе'],
['подст:Disputed', 'Ф3.4 Сомнительные данные файла'],
['подст:No permission', 'Ф3.5 Не подтверждена лицензия'],
['подст:Orphaned-fairuse', 'Ф4 Неиспользуемый КДИ'],
['подст:Disputed-fairuse', 'Ф5 Нет КДИ'],
['Db-badfairuse', 'Ф6 Неоправданное КДИ'],
['Замена с Викисклада', 'Ф7 Заменяемый КДИ'],
['Перенесено на Викисклад', 'Ф8 Есть на Складе'],
['подст:Db-nothost', 'Ф9 Файл — ВП:НЕХОСТИНГ']
],
categories: [
['уд-пусткат', 'К1.1 Пустая категория'],
['уд-служебная', 'К1.2 Разобранная служебная кат.'],
['уд-перекат', 'К2 Переименованная кат.']
],
users: [
['уд-владелец', 'У1 По желанию владельца'],
['уд-анон', 'У2 Устаревшая СО анонима'],
['уд-несущ', 'У3 Несуществующий участник'],
['уд-нецелевое', 'У4 Нецелевое использ. ЛП'],
['уд-неактив', 'У5 Подстраница неактивного']
],
special: [
['db', 'Особый случай']
]
},
fastRemoveCriteriaAnchors: {
'подст:ds': 'С1',
deleteslow: 'С1',
ds: 'С1'
},
requiredParamTemplates: {
'уд-переим': 'страницу, которую нужно переименовать',
'уд-дубль': 'страницу-дубликат',
'уд-копивио': 'URL источника нарушения АП',
'Db-duplicate': 'имя файла-оригинала',
'Замена с Викисклада': 'имя файла на Викискладе',
'Перенесено на Викисклад': 'имя файла на Викискладе',
'уд-перекат': 'новое название категории',
'db': '!причину удаления'
},
categoryTemplates: {
discuss: 'Обсуждаемая категория|обсуждаемая категория|Acat|acat|ОКТО|окто|Категория к обсуждению|категория к обсуждению',
rename: 'Категория к переименованию|категория к переименованию|Anacat|anacat',
merge: 'Категория к объединению|категория к объединению|Amergecat|amergecat|Cfm|cfm',
discussed: 'Обсуждавшаяся категория|обсуждавшаяся категория|Обсуждалась|обсуждалась|Обсуждалось|обсуждалось'
},
modalStyles: {
border: '1px solid var(--border-color-progressive, #3366bb)',
background: 'var(--background-color-base, #f8f9fa)',
borderRadius: '6px',
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
headerColor: 'var(--color-progressive, #3366bb)'
}
};
config.scriptLink = config.scriptLink || defaults.scriptLink;
config.fastRemoveReasons = $.extend({}, defaults.fastRemoveReasons, config.fastRemoveReasons || {});
config.fastRemoveCriteriaAnchors = $.extend({}, defaults.fastRemoveCriteriaAnchors, config.fastRemoveCriteriaAnchors || {});
config.requiredParamTemplates = $.extend({}, defaults.requiredParamTemplates, config.requiredParamTemplates || {});
config.categoryTemplates = $.extend({}, defaults.categoryTemplates, config.categoryTemplates || {});
config.modalStyles = $.extend({}, defaults.modalStyles, config.modalStyles || {});
return config;
}
function clonePlainObject(obj) {
return JSON.parse(JSON.stringify(obj || {}));
}
function normalizeMenuLabel(value) {
return String(value || '').replace(/\s+/g, ' ').trim().toLowerCase();
}
function readSettingsOptionState(fallback) {
var base = clonePlainObject(fallback || {});
var stored;
var raw = null;
if (!mw.user || !mw.user.options || typeof mw.user.options.get !== 'function') return base;
stored = mw.user.options.get(settingsOptionName);
if (typeof stored === 'string' && stored.trim()) {
try {
raw = JSON.parse(stored);
} catch (e) {
console.warn('RemoverCore: не удалось прочитать сохранённые настройки полностью, будут использованы данные loader.', e);
}
}
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return base;
return $.extend({}, raw, base, ('quickPhrases' in raw) ? { quickPhrases: raw.quickPhrases } : {});
}
function normalizeQuickPhraseValue(value) {
return String(value == null ? '' : value).replace(/\r\n?/g, '\n').trim();
}
function normalizeQuickPhrasesList(list, fallback) {
var source = Array.isArray(list) ? list : fallback;
var normalized = [];
(source || []).forEach(function (value) {
var phrase = normalizeQuickPhraseValue(value);
if (phrase && normalized.indexOf(phrase) === -1) normalized.push(phrase);
});
return normalized;
}
function collectSettingsMenuMeta() {
var labels = [];
var articleLabels = [];
var categoryLabels = [];
var idToLabel = {};
var labelByNorm = {};
var labelOrder = {};
function collect(items, targetLabels) {
items.forEach(function (item) {
var id;
var label;
var normLabel;
if (!item || item.type === 'separator') return;
id = String(item.id || '').trim();
label = String(item.label || '').trim();
normLabel = normalizeMenuLabel(label);
if (id && label && !(id in idToLabel)) idToLabel[id] = label;
if (label && targetLabels.indexOf(label) === -1) targetLabels.push(label);
if (normLabel && !(normLabel in labelByNorm)) {
labelByNorm[normLabel] = label;
labelOrder[label] = labels.length;
labels.push(label);
}
});
}
collect(cfg.articleMenuItems, articleLabels);
collect(cfg.categoryMenuItems, categoryLabels);
return {
labels: labels,
articleLabels: articleLabels,
categoryLabels: categoryLabels,
idToLabel: idToLabel,
labelByNorm: labelByNorm,
labelOrder: labelOrder
};
}
function buildSettingsMenuItemsHint() {
var parts = [];
if (settingsArticleItemLabels.length) {
parts.push('<div class="rmSettingsHintRow"><span class="rmSettingsHintBadge">Статьи</span>' + escapeHtml(settingsArticleItemLabels.join(', ')) + '.</div>');
}
if (settingsCategoryItemLabels.length) {
parts.push('<div class="rmSettingsHintRow"><span class="rmSettingsHintBadge">Категории</span>' + escapeHtml(settingsCategoryItemLabels.join(', ')) + '.</div>');
}
return parts.length ? '<div class="rmSettingsHintList">' + parts.join('') + '</div>' : '';
}
function getDefaultSettings() {
return {
version: settingsVersion,
excludedNamespaces: normalizeNumberList(cfg.excludedNamespaces, []),
notifyAuthor: !!cfg.defaultNotifyAuthor,
subscribeTopic: !!cfg.defaultSubscribeTopic,
autoReloadAfterAction: false,
openNominationDiscussionInNewTab: false,
menuTitle: (typeof cfg.menuTitle === 'string' && cfg.menuTitle.trim()) ? cfg.menuTitle.trim() : 'Remover',
disabledItems: [],
quickPhrases: normalizeQuickPhrasesList(cfg.quickPhrases, []),
showMenuIcons: !!cfg.showMenuIcons,
signatureSeparator: (typeof cfg.signatureSeparator === 'string') ? cfg.signatureSeparator.trim() : ''
};
}
function normalizeNumberList(list, fallback) {
var source = Array.isArray(list) ? list : fallback;
return source
.map(function (value) { return parseInt(value, 10); })
.filter(function (value, index, arr) { return !isNaN(value) && arr.indexOf(value) === index; })
.sort(function (a, b) { return a - b; });
}
function normalizeDisabledItemValue(value) {
var token = String(value || '').trim();
if (!token) return null;
if (settingsItemLabelById[token]) return settingsItemLabelById[token];
return settingsItemLabelByNorm[normalizeMenuLabel(token)] || null;
}
function compareSettingsMenuLabels(a, b) {
var ai = Object.prototype.hasOwnProperty.call(settingsItemLabelOrder, a) ? settingsItemLabelOrder[a] : Number.MAX_SAFE_INTEGER;
var bi = Object.prototype.hasOwnProperty.call(settingsItemLabelOrder, b) ? settingsItemLabelOrder[b] : Number.MAX_SAFE_INTEGER;
if (ai !== bi) return ai - bi;
return a.localeCompare(b, 'ru');
}
function normalizeDisabledItemsList(list, fallback) {
var source = Array.isArray(list) ? list : fallback;
return source
.map(normalizeDisabledItemValue)
.filter(function (value, index, arr) { return value && arr.indexOf(value) === index; })
.sort(compareSettingsMenuLabels);
}
function normalizeMenuTitleSetting(value, fallback) {
var menuTitle = String(value || '').trim();
if (!menuTitle) return fallback;
if (menuTitle === MENU_TITLE_PRESET_CACTIONS || /^(#)?p-cactions$/i.test(menuTitle)) return MENU_TITLE_PRESET_CACTIONS;
if (menuTitle === MENU_TITLE_PRESET_PAGE || /^(#)?p-page$/i.test(menuTitle) || /^(#)?p-actions$/i.test(menuTitle)) return MENU_TITLE_PRESET_PAGE;
if (menuTitle === MENU_TITLE_PRESET_TOOLS || /^(#)?p-tb$/i.test(menuTitle)) return MENU_TITLE_PRESET_TOOLS;
return menuTitle;
}
function normalizeRemoverSettings(raw) {
var defaults = clonePlainObject(settingsDefaults);
var source = (raw && typeof raw === 'object') ? raw : {};
return {
version: settingsVersion,
excludedNamespaces: normalizeNumberList(source.excludedNamespaces, defaults.excludedNamespaces || []),
notifyAuthor: ('notifyAuthor' in source) ? !!source.notifyAuthor : !!defaults.notifyAuthor,
subscribeTopic: ('subscribeTopic' in source) ? !!source.subscribeTopic : !!defaults.subscribeTopic,
autoReloadAfterAction: ('autoReloadAfterAction' in source) ? !!source.autoReloadAfterAction : !!defaults.autoReloadAfterAction,
openNominationDiscussionInNewTab: ('openNominationDiscussionInNewTab' in source) ? !!source.openNominationDiscussionInNewTab : !!defaults.openNominationDiscussionInNewTab,
menuTitle: normalizeMenuTitleSetting(
(typeof source.menuTitle === 'string' && source.menuTitle.trim()) ? source.menuTitle : '',
typeof defaults.menuTitle === 'string' && defaults.menuTitle.trim() ? defaults.menuTitle.trim() : 'Remover'
),
disabledItems: normalizeDisabledItemsList(source.disabledItems, defaults.disabledItems || []),
quickPhrases: normalizeQuickPhrasesList(source.quickPhrases, defaults.quickPhrases || []),
showMenuIcons: ('showMenuIcons' in source) ? !!source.showMenuIcons : !!defaults.showMenuIcons,
signatureSeparator: (typeof source.signatureSeparator === 'string')
? source.signatureSeparator.trim()
: (typeof defaults.signatureSeparator === 'string' ? defaults.signatureSeparator.trim() : '')
};
}
function areRemoverSettingsEqual(a, b) {
return JSON.stringify(normalizeRemoverSettings(a)) === JSON.stringify(normalizeRemoverSettings(b));
}
function updateStoredSettingsState(settings, skipUserOptionsSync) {
var normalized = normalizeRemoverSettings(settings);
state.settings = clonePlainObject(normalized);
setAlert = normalized.notifyAuthor;
setSubscribe = normalized.subscribeTopic;
signatureSeparator = normalized.signatureSeparator;
state.setAlert = setAlert;
state.setSubscribe = setSubscribe;
if (!skipUserOptionsSync && mw.user && mw.user.options && typeof mw.user.options.set === 'function') {
mw.user.options.set(settingsOptionName, JSON.stringify(normalized));
}
return normalized;
}
function splitSettingsInput(value) {
return String(value || '')
.split(/[\s,;]+/)
.map(function (part) { return part.trim(); })
.filter(Boolean);
}
function splitSettingsListInput(value) {
return String(value || '')
.split(/[\n,;]+/)
.map(function (part) { return part.trim(); })
.filter(Boolean);
}
function parseNamespaceInput(value) {
var tokens = splitSettingsInput(value);
var invalid = [];
var values = [];
tokens.forEach(function (token) {
var parsed = parseInt(token, 10);
if (String(parsed) !== token) invalid.push(token);
else if (values.indexOf(parsed) === -1) values.push(parsed);
});
values.sort(function (a, b) { return a - b; });
return { values: values, invalid: invalid };
}
function parseDisabledItemsInput(value) {
var tokens = splitSettingsListInput(value);
var invalid = [];
var values = [];
tokens.forEach(function (token) {
var normalized = normalizeDisabledItemValue(token);
if (!normalized) invalid.push(token);
else if (values.indexOf(normalized) === -1) values.push(normalized);
});
values.sort(compareSettingsMenuLabels);
return { values: values, invalid: invalid };
}
function formatItemsWithAnd(items) {
var list = (items || []).filter(Boolean);
if (!list.length) return '';
if (list.length === 1) return list[0];
return list.slice(0, -1).join(', ') + ' и ' + list[list.length - 1];
}
function formatPagesWithAnd(names, prefix) {
var p = prefix || ':';
var links = (names || []).map(function (n) { return '[[' + p + n + ']]'; });
return formatItemsWithAnd(links);
}
function asNonEmptyArray(value) {
return (Array.isArray(value) ? value : (value ? [value] : [])).filter(Boolean);
}
function buildRenameTemplateParam(targetNames) {
var list = asNonEmptyArray(targetNames);
if (!list.length) return '';
return list[0] + (list.length > 1 ? '||' + list.slice(1).join('|') : '');
}
function collectRenameTargetsFromTemplateParams(params) {
return (params || []).map(function (value) {
return String(value || '').trim();
}).filter(Boolean);
}
function formatRenameItemLabel(pageName, targetName) {
var targets = asNonEmptyArray(targetName);
return '[[:' + pageName + ']]' + (targets.length
? ' → ' + targets.map(function (name) { return '[[:' + name + ']]'; }).join(', ')
: '');
}
function buildRenameItemLabelFormatter(targetsByPage) {
var targets = targetsByPage || {};
return function (pageName) {
return formatRenameItemLabel(pageName, targets[normTitle(pageName)] || '');
};
}
function formatRenameItemsWithAnd(pages, targetsByPage) {
var formatItem = buildRenameItemLabelFormatter(targetsByPage);
var links = (pages || []).map(function (pageName) { return formatItem(pageName); });
return formatItemsWithAnd(links);
}
function normalizeCategoryTargetName(value) {
return normTitle(stripCatPrefix(value)).trim();
}
function normalizeCategoryTargetPageName(value) {
var title = normalizeCategoryTargetName(value);
return title ? normalizeCategoryPageName(title) : '';
}
function buildMultiRenameTargetMap(pairs, key) {
var map = {};
(pairs || []).forEach(function (pair) {
var value;
if (!pair || !pair.pageName) return;
value = pair[key || 'targetName'];
map[normTitle(pair.pageName)] = Array.isArray(value) ? value.slice() : (value || '');
});
return map;
}
function getMultiRenameTarget(job, pageName, key) {
var map = job && job[key || 'multiRenameTargets'];
return map ? (map[normTitle(pageName)] || '') : '';
}
function collectMultiRenamePairs(options) {
var opts = options || {};
var normalizePage = typeof opts.normalizePageName === 'function' ? opts.normalizePageName : normTitle;
var normalizeTarget = typeof opts.normalizeTargetName === 'function' ? opts.normalizeTargetName : normTitle;
var normalizeTemplateTarget = typeof opts.normalizeTemplateTargetName === 'function' ? opts.normalizeTemplateTargetName : normalizeTarget;
var pairs = [];
$('.rmMultiPageBlock').each(function () {
var $block = $(this);
var pageRaw = ($block.find('.rmMultiPageInput').val() || '').trim();
var targetPairs = collectMultiRenameTargetValues($block).map(function (targetRaw) {
return {
targetName: normalizeTarget(targetRaw),
templateTargetName: normalizeTemplateTarget(targetRaw)
};
}).filter(function (item) { return item.targetName || item.templateTargetName; });
var pageName = normalizePage(pageRaw);
var targetNames = targetPairs.map(function (item) { return item.targetName; }).filter(Boolean);
var templateTargetNames = targetPairs.map(function (item) { return item.templateTargetName; }).filter(Boolean);
if (!pageName && !targetNames.length && !templateTargetNames.length) return;
pairs.push({
pageName: pageName,
targetName: targetNames[0] || '',
templateTargetName: templateTargetNames[0] || '',
targetNames: targetNames,
templateTargetNames: templateTargetNames
});
});
return pairs;
}
function validateMultiRenamePairs(pairs, pageLabel, targetLabel) {
var seen = {};
var pages = pairs || [];
var pageWord = pageLabel || 'страницу';
var targetWord = targetLabel || 'новое название';
if (!pages.length) { alert('Укажите ' + pageWord + '.'); return false; }
for (var i = 0; i < pages.length; i++) {
var targetNames = asNonEmptyArray(pages[i].targetNames || pages[i].targetName);
var templateTargetNames = asNonEmptyArray(pages[i].templateTargetNames || pages[i].templateTargetName);
if (!pages[i].pageName) { alert('Укажите ' + pageWord + '.'); return false; }
if (!targetNames.length || !templateTargetNames.length) { alert('Укажите ' + targetWord + ' для «' + pages[i].pageName + '».'); return false; }
if (targetNames.length > 3 || templateTargetNames.length > 3) { alert('Максимум 3 варианта переименования для «' + pages[i].pageName + '».'); return false; }
if (seen[normTitle(pages[i].pageName)]) { alert('Страница «' + pages[i].pageName + '» указана несколько раз.'); return false; }
seen[normTitle(pages[i].pageName)] = true;
}
return true;
}
function getMultiRenameDiscussionOptions(targetsByPage, extraOptions) {
return $.extend({}, extraOptions || {}, {
formatItemLabel: buildRenameItemLabelFormatter(targetsByPage)
});
}
function formatCatLink(name) { return '[[:' + normalizeCategoryPageName(name) + ']]'; }
function formatMergeStatus(status) {
return { already_exists: 'уже был', updated: 'дополнен', created: 'создан' }[status] || status;
}
function applyGeneratedText($el, generated) {
var cur = $el.val();
var prev = $el.data('rmGenerated') || '';
if (!prev || cur.indexOf(prev) === 0) {
$el.val(generated + cur.slice(prev.length));
} else {
$el.val(generated + (cur ? '\n' + cur : ''));
}
$el.data('rmGenerated', generated);
}
function getCurrentQuickPhrases() {
return normalizeQuickPhrasesList(
state.settings && state.settings.quickPhrases,
settingsDefaults.quickPhrases || []
);
}
function insertTextIntoTextarea($el, text) {
var el = $el && $el[0];
var value;
var start;
var end;
var updatedValue;
var caretPos;
if (!el) return;
text = String(text || '');
if (!text) return;
value = $el.val() || '';
start = typeof el.selectionStart === 'number' ? el.selectionStart : value.length;
end = typeof el.selectionEnd === 'number' ? el.selectionEnd : start;
updatedValue = value.slice(0, start) + text + value.slice(end);
caretPos = start + text.length;
$el.val(updatedValue).trigger('input').trigger('change').focus();
if (typeof el.setSelectionRange === 'function') el.setSelectionRange(caretPos, caretPos);
}
function buildQuickPhrasesPanelHtml(textareaId) {
var phrases = getCurrentQuickPhrases();
if (!phrases.length) return '';
return joinHtml([
'<div class="rmQuickPhrasesPanel ', RESIZE_CLASS, '" data-rm-target="', textareaId, '">',
phrases.map(function (phrase) {
return joinHtml([
'<button type="button" class="rmQuickPhraseActionBtn" data-rm-target="', textareaId,
'" data-rm-phrase="', escapeHtml(phrase), '">',
escapeHtml(phrase),
'</button>'
]);
}).join(''),
'</div>'
]);
}
function getMultiNominationCommentText(commentsByPage, pageTitle) {
var key = normTitle(pageTitle);
if (!commentsByPage || !Object.prototype.hasOwnProperty.call(commentsByPage, key)) return '';
return normalizeQuickPhraseValue(commentsByPage[key]);
}
function hasCommentsForEveryMultiNominationPage(pages, commentsByPage) {
var list = Array.isArray(pages) ? pages.filter(Boolean) : [];
return list.length > 0 && list.every(function (pageName) {
return !!getMultiNominationCommentText(commentsByPage, pageName);
});
}
function validateMultiNominationText(pages, bodyText, commentsByPage, itemGenitive) {
if (normalizeQuickPhraseValue(bodyText) || hasCommentsForEveryMultiNominationPage(pages, commentsByPage)) return true;
alert('Укажите общий текст номинации или комментарий для каждой ' + (itemGenitive || 'страницы') + '.');
return false;
}
function buildMultiNominationText(pages, bodyText, commentsByPage, options) {
var opts = options || {};
var list = Array.isArray(pages) ? pages.filter(Boolean) : [];
var body = normalizeQuickPhraseValue(bodyText);
var hasAllPageComments = hasCommentsForEveryMultiNominationPage(list, commentsByPage);
var headingLevel = Math.max(2, parseInt(opts.headingLevel, 10) || 3);
var headingMarks = new Array(headingLevel + 1).join('=');
var formatItemLabel = typeof opts.formatItemLabel === 'function'
? opts.formatItemLabel
: function (pageName) { return '[[:' + pageName + ']]'; };
var pageSections = list.map(function (pageName, index) {
var comment = getMultiNominationCommentText(commentsByPage, pageName);
var sectionPrefix = (index === 0 && opts.leadingBlankLine === false) ? '' : '\n';
return sectionPrefix + headingMarks + ' ' + formatItemLabel(pageName) + ' ' + headingMarks + '\n' + (comment ? appendNominationSignature(comment) + '\n' : '');
}).join('');
var commonSectionText = body
? appendNominationSignature(body)
: (hasAllPageComments ? '' : appendNominationSignature(''));
return pageSections + (commonSectionText ? '\n' + headingMarks + ' По всем ' + headingMarks + '\n' + commonSectionText : '');
}
function buildMultiNominationListText(pages, bodyText, commentsByPage, options) {
var opts = options || {};
var list = Array.isArray(pages) ? pages.filter(Boolean) : [];
var body = normalizeQuickPhraseValue(bodyText);
var hasAllPageComments = hasCommentsForEveryMultiNominationPage(list, commentsByPage);
var formatItemLabel = typeof opts.formatItemLabel === 'function'
? opts.formatItemLabel
: function (pageName) { return '[[:' + pageName + ']]'; };
var pageLines = list.map(function (pageName) {
var comment = getMultiNominationCommentText(commentsByPage, pageName);
return '* ' + formatItemLabel(pageName) + (comment ? '\n' + appendNominationSignature(comment).split('\n').map(function (line) { return '*: ' + line; }).join('\n') : '');
}).join('\n');
var commonText = body
? appendNominationSignature(body)
: (hasAllPageComments ? '' : appendNominationSignature(''));
return pageLines + (pageLines && commonText ? '\n' : '') + commonText;
}
function collectMultiNominationComments(normalizePageName) {
var comments = {};
var normalize = typeof normalizePageName === 'function' ? normalizePageName : normTitle;
$('.rmMultiPageBlock').each(function () {
var $block = $(this);
var pageName = normalize(($block.find('.rmMultiPageInput').val() || '').trim());
var comment = normalizeQuickPhraseValue($block.find('.rmMultiPageCommentInput').val());
if (!pageName) return;
comments[pageName] = comment;
});
return comments;
}
function getNominationPublishText(job) {
if (job && job.isMulti) return String(job.msg || '');
return appendNominationSignature(job && job.msg);
}
function getNominationConflictRule(job) {
if (!job || job.mode !== 'nominate') return null;
if (job.opId === 'tRm' || job.opId === 'mRm') {
return {
id: 'ku',
label: 'КУ',
namePattern: '(?:к\\s*удалению|ку)',
detect: function (articleText) {
var match = String(articleText || '').match(/\{\{\s*((?:к\s*удалению|ку))\s*(?:\||\}\})/i);
if (!match) return null;
var templateName = ucfirst(String(match[1] || '').replace(/\s+/g, ' ').trim());
return {
label: 'КУ',
templateName: templateName || 'КУ',
templateDisplay: '{{' + (templateName || 'КУ') + '}}'
};
}
};
}
if (job.opId === 'rnm' || job.opId === 'mRnm') {
return {
id: 'kpm',
label: 'КПМ',
namePattern: '(?:к\\s*переименованию|кпм|rename)',
detect: function (articleText) {
var match = String(articleText || '').match(/\{\{\s*((?:к\s*переименованию|кпм|rename))\s*(?:\||\}\})/i);
if (!match) return null;
var templateName = ucfirst(String(match[1] || '').replace(/\s+/g, ' ').trim());
return {
label: 'КПМ',
templateName: templateName || 'КПМ',
templateDisplay: '{{' + (templateName || 'КПМ') + '}}'
};
}
};
}
return null;
}
function detectNominationConflict(articleText, job) {
var rule = getNominationConflictRule(job);
if (!rule || typeof rule.detect !== 'function') return null;
return rule.detect(articleText);
}
function getConflictDecisionForPage(job, pageName) {
var decisions = job && job.conflictDecisions;
var key = normTitle(pageName);
return decisions && decisions[key] ? decisions[key] : null;
}
function getCategoryMergeRe() {
return new RegExp('\\{\\{\\s*(?:' + cfg.categoryTemplates.merge + ')\\s*\\|\\s*([^\\}]+)\\}\\}', 'i');
}
function eachSequential(targets, iteratee, done) {
var i = 0;
(function next(err) {
if (err || i >= targets.length) { done(err || null); return; }
iteratee(targets[i++], next);
}(null));
}
function normalizeSectionForLink(sectionTitle) {
return (sectionTitle || '').trim()
.replace(/\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g, function (_, target, label) {
var v = (label || target || '').trim();
return v.charAt(0) === ':' ? v.slice(1) : v;
})
.replace(/''+/g, '').replace(/\s+/g, ' ').trim();
}
function getViewportWidth() {
return Math.floor(Math.max(
(document.documentElement && document.documentElement.clientWidth) || 0,
(typeof window.innerWidth === 'number' && window.innerWidth) || 0,
$(window).width() || 0
));
}
function getVisualViewportWidth() {
var widths = [];
if (window.visualViewport && typeof window.visualViewport.width === 'number' && window.visualViewport.width > 0) widths.push(window.visualViewport.width);
if (window.screen && window.screen.width > 0) widths.push(window.screen.width);
return widths.length ? Math.floor(Math.min.apply(Math, widths)) : getViewportWidth();
}
function isTouchModalDevice() {
return !!(
(window.matchMedia && window.matchMedia('(pointer: coarse)').matches) ||
('ontouchstart' in window) ||
(navigator.maxTouchPoints && navigator.maxTouchPoints > 0) ||
(navigator.msMaxTouchPoints && navigator.msMaxTouchPoints > 0)
);
}
function getModalLayout() {
var minWidth = parseInt(sz.taMinW, 10) || 180;
var layoutWidth = getViewportWidth();
var visualWidth = getVisualViewportWidth();
var contentWidth = Math.max(minWidth, Math.floor($('#content').width() || $('#content').innerWidth() || $(window).width() || minWidth));
var isMobile = layoutWidth <= sz.mobileBp;
var isTouchDesktop = !isMobile &&
isTouchModalDevice() &&
visualWidth > 0 &&
visualWidth <= sz.mobileBp &&
layoutWidth >= visualWidth + sz.touchDesktopGap;
var useFullWidth = isMobile || isTouchDesktop;
var maxOuterWidth;
var defaultOuterWidth;
var desktopWidth;
if (isMobile) maxOuterWidth = Math.max(minWidth, (visualWidth || contentWidth) - sz.viewportGap);
else if (isTouchDesktop) maxOuterWidth = Math.max(minWidth, contentWidth - 32);
else maxOuterWidth = Math.max(minWidth, Math.min(contentWidth, (visualWidth ? visualWidth - sz.viewportGap : contentWidth)));
desktopWidth = Math.max(minWidth, Math.floor(contentWidth * sz.modalRatio));
if (useFullWidth || desktopWidth < sz.modalMinWide) defaultOuterWidth = contentWidth;
else if (desktopWidth < sz.modalDefaultWide) defaultOuterWidth = sz.modalDefaultWide;
else defaultOuterWidth = desktopWidth;
return {
minWidth: minWidth,
isMobile: isMobile,
isTouchDesktop: isTouchDesktop,
useFullWidth: useFullWidth,
shouldCenter: useFullWidth || mwCfg.skin === 'minerva',
maxOuterWidth: maxOuterWidth,
defaultOuterWidth: Math.min(defaultOuterWidth, maxOuterWidth)
};
}
function getDefaultResizableWidth(frameWidth) {
var layout = getModalLayout();
return Math.max(layout.minWidth, layout.defaultOuterWidth - Math.floor(frameWidth || 0));
}
function getBoxFrameWidth($el) {
function px(prop) {
var n = parseFloat($el.css(prop));
return isNaN(n) ? 0 : n;
}
return px('padding-left') + px('padding-right') + px('border-left-width') + px('border-right-width');
}
// ═══════════════════════════════════════════════════════════════════════════
// API
// ═══════════════════════════════════════════════════════════════════════════
function getApiUrl() {
return (mw.util && typeof mw.util.wikiScript === 'function') ? mw.util.wikiScript('api') : '/w/api.php';
}
function getCsrfTokenValue() {
return (mw.user && mw.user.tokens && typeof mw.user.tokens.get === 'function')
? mw.user.tokens.get('csrfToken')
: null;
}
function storeCsrfToken(token) {
if (!token || !mw.user || !mw.user.tokens || typeof mw.user.tokens.set !== 'function') return;
mw.user.tokens.set({ csrfToken: token });
}
function isValidCsrfToken(token) {
return typeof token === 'string' && !!token && token !== '+\\';
}
function fetchCsrfToken(forceRefresh, callback) {
var cachedToken = getCsrfTokenValue();
if (!forceRefresh && isValidCsrfToken(cachedToken)) {
callback(cachedToken);
return;
}
$.ajax({
url: getApiUrl(),
method: 'GET',
dataType: 'json',
data: { action: 'query', meta: 'tokens', type: 'csrf', format: 'json' }
})
.done(function (data) {
var token = data && data.query && data.query.tokens && data.query.tokens.csrftoken;
if (isValidCsrfToken(token)) {
storeCsrfToken(token);
callback(token);
return;
}
callback(null);
})
.fail(function () {
callback(null);
});
}
function apiReq(params, mode, callback) {
var isWrite = mode === 'edit' || mode === 'discussiontoolssubscribe' || mode === 'options';
function sendRequest(retryWithFreshToken) {
var reqParams = $.extend({}, params, { format: 'json', action: mode });
if (!isWrite) {
$.ajax({ url: getApiUrl(), method: 'GET', data: reqParams, dataType: 'json' })
.done(function (data) { if (callback) callback(data); })
.fail(function (jqXHR, status) {
console.error('Ошибка API: ' + status);
if (callback) callback({ error: { code: 'network', info: status } });
});
return;
}
fetchCsrfToken(!!retryWithFreshToken, function (token) {
if (!isValidCsrfToken(token)) {
if (callback) callback({ error: { code: 'badtoken', info: 'Не удалось получить CSRF-токен.' } });
return;
}
reqParams.token = token;
$.ajax({ url: getApiUrl(), method: 'POST', data: reqParams, dataType: 'json' })
.done(function (data) {
var err = data && data.error;
var isBadToken = err && (err.code === 'badtoken' || /invalid csrf token/i.test(String(err.info || '')));
if (isBadToken && !retryWithFreshToken) {
sendRequest(true);
return;
}
if (callback) callback(data);
})
.fail(function (jqXHR, status) {
console.error('Ошибка API: ' + status);
if (callback) callback({ error: { code: 'network', info: status } });
});
});
}
sendRequest(false);
}
function saveSettingsToServer(settings, callback) {
var normalized = normalizeRemoverSettings(settings);
if (!mwCfg.wgUserName) {
callback({ code: 'notloggedin', info: 'Сохранять настройки можно только после входа в учётную запись.' });
return;
}
apiReq({ optionname: settingsOptionName, optionvalue: JSON.stringify(normalized) }, 'options', function (resp) {
if (resp && resp.options === 'success') {
callback(null, updateStoredSettingsState(normalized));
return;
}
callback((resp && resp.error) || { code: 'options_failed', info: 'Не удалось сохранить настройки.' });
});
}
function resetSettingsOnServer(callback) {
if (!mwCfg.wgUserName) {
callback({ code: 'notloggedin', info: 'Сбрасывать настройки можно только после входа в учётную запись.' });
return;
}
apiReq({ change: settingsOptionName }, 'options', function (resp) {
if (resp && resp.options === 'success') {
callback(null, updateStoredSettingsState(settingsDefaults, true));
return;
}
callback((resp && resp.error) || { code: 'options_failed', info: 'Не удалось сбросить настройки.' });
});
}
function getFirstQueryPage(data) {
var pages = data && data.query && data.query.pages;
if (!pages) return null;
return pages[Object.keys(pages)[0]] || null;
}
function extractRevisionContent(rev) {
if (!rev) return null;
if (typeof rev['*'] === 'string') return rev['*'];
if (rev.slots && rev.slots.main) {
if (typeof rev.slots.main['*'] === 'string') return rev.slots.main['*'];
if (typeof rev.slots.main.content === 'string') return rev.slots.main.content;
}
return null;
}
function makeReadError(apiError, fallbackCode, fallbackInfo) {
var err = apiError || {};
return {
code: err.code || fallbackCode || 'read_failed',
info: err.info || fallbackInfo || 'Не удалось получить содержимое.'
};
}
function getTextWithTimestamp(pageName, callback) {
apiReq({ prop: 'revisions', rvprop: 'content|timestamp', rvslots: 'main', titles: pageName }, 'query', function (data) {
var content;
var page;
if (data && data.error) {
callback(null, null, data.error);
return;
}
if (!data || !data.query || !data.query.pages) {
callback(null, null, { code: 'read_failed', info: 'Некорректный ответ API при чтении страницы.' });
return;
}
page = getFirstQueryPage(data);
if (!page) {
callback(null, null, { code: 'read_failed', info: 'API не вернул данные страницы.' });
return;
}
if (page.invalid !== undefined) {
callback(null, null, { code: 'invalidtitle', info: page.invalidreason || 'Некорректное название страницы.' });
return;
}
if (page.missing !== undefined) {
callback(null, null, null);
return;
}
if (!page.revisions || !page.revisions.length) {
callback(null, null, { code: 'read_failed', info: 'API не вернул ревизии страницы.' });
return;
}
content = extractRevisionContent(page.revisions[0]);
if (content === null) {
callback(null, null, { code: 'content_missing', info: 'API не вернул текст страницы.' });
return;
}
callback(content, page.revisions[0].timestamp || null, null);
});
}
function getText(pageName, callback) {
getTextWithTimestamp(pageName, function (text, baseTimestamp, err) { callback(text, err); });
}
function editPageContent(pageTitle, options, buildFn, callback) {
var opts = options || {};
var maxRetries = Math.max(0, parseInt(opts.editConflictRetries, 10) || 1);
(function attempt(retry) {
getTextWithTimestamp(pageTitle, function (sourceText, baseTimestamp, readErr) {
if (readErr) {
callback(makeReadError(readErr, opts.readErrorCode || 'read_failed', 'Не удалось получить содержимое страницы «' + pageTitle + '».'));
return;
}
if (sourceText === null) {
callback({ code: opts.readErrorCode || 'read_failed', info: opts.readError || 'Страница «' + pageTitle + '» не существует.' });
return;
}
var done = (function () {
var called = false;
return function (result) {
if (called) return;
called = true;
if (!result || result.error) { callback((result && result.error) || { code: 'build_failed', info: 'Не удалось подготовить правку.' }, result && result.meta || null); return; }
if (result.skip) { callback(null, result.meta || null); return; }
if (typeof result.text !== 'string') { callback({ code: 'build_failed', info: 'Не удалось подготовить правку.' }); return; }
var ep = { title: pageTitle, text: result.text, summary: result.summary || opts.summary || '' };
if (opts.watchlist) ep.watchlist = opts.watchlist;
if (opts.assertuser) ep.assertuser = opts.assertuser;
if (opts.createonly) ep.createonly = opts.createonly;
if (opts.useBaseTimestamp !== false && baseTimestamp) ep.basetimestamp = baseTimestamp;
apiReq(ep, 'edit', function (resp) {
var err = resp && resp.error ? resp.error : null;
if (err && err.code === 'editconflict' && retry < maxRetries) { attempt(retry + 1); return; }
callback(err, result.meta || null);
});
};
}());
var maybe = buildFn(sourceText, done);
if (maybe !== undefined) done(maybe);
});
}(0));
}
// ═══════════════════════════════════════════════════════════════════════════
// ШАБЛОНЫ: удаление и вставка
// ═══════════════════════════════════════════════════════════════════════════
function findBalancedTemplateEnd(text, start) {
var depth = 0;
var i = start;
var len = text.length;
while (i < len - 1) {
if (text.charAt(i) === '{' && text.charAt(i + 1) === '{') {
depth++;
i += 2;
continue;
}
if (text.charAt(i) === '}' && text.charAt(i + 1) === '}') {
depth--;
i += 2;
if (depth === 0) return i;
continue;
}
i++;
}
return -1;
}
function splitTemplateTopLevelParts(innerText) {
var parts = [];
var start = 0;
var templateDepth = 0;
var linkDepth = 0;
var i = 0;
var text = String(innerText || '');
while (i < text.length) {
if (text.charAt(i) === '{' && text.charAt(i + 1) === '{') {
templateDepth++;
i += 2;
continue;
}
if (text.charAt(i) === '}' && text.charAt(i + 1) === '}' && templateDepth > 0) {
templateDepth--;
i += 2;
continue;
}
if (text.charAt(i) === '[' && text.charAt(i + 1) === '[') {
linkDepth++;
i += 2;
continue;
}
if (text.charAt(i) === ']' && text.charAt(i + 1) === ']' && linkDepth > 0) {
linkDepth--;
i += 2;
continue;
}
if (text.charAt(i) === '|' && templateDepth === 0 && linkDepth === 0) {
parts.push(text.slice(start, i));
start = i + 1;
}
i++;
}
parts.push(text.slice(start));
return parts;
}
function getTemplateMatchAt(text, start, nameRe) {
var end = findBalancedTemplateEnd(text, start);
var parts;
var rawName;
var name;
if (end < 0) return null;
parts = splitTemplateTopLevelParts(text.slice(start + 2, end - 2));
rawName = String(parts.shift() || '').trim();
name = rawName.replace(/^(?:safe)?(?:subst|подст)\s*:\s*/i, '').replace(RE_TEMPLATE_NS, '').replace(/_/g, ' ').replace(/\s+/g, ' ').trim();
if (!nameRe.test(name)) return null;
return {
start: start,
end: end,
text: text.slice(start, end),
name: rawName,
params: parts.map(function (part) { return part.trim(); })
};
}
function findTemplateByPattern(text, namePattern) {
var source = String(text || '');
var nameRe = new RegExp('^(?:' + namePattern + ')$', 'i');
var i = 0;
var end;
var match;
while (i < source.length - 1) {
if (source.charAt(i) === '{' && source.charAt(i + 1) === '{') {
match = getTemplateMatchAt(source, i, nameRe);
if (match) return match;
end = findBalancedTemplateEnd(source, i);
if (end > i) { i = end; continue; }
}
i++;
}
return null;
}
function hasTemplateWithDateByPattern(text, namePattern, dateIso) {
var source = String(text || '');
var nameRe = new RegExp('^(?:' + namePattern + ')$', 'i');
var targetDate = convertToStandardDate(dateIso);
var i = 0;
var end;
var match;
var templateDate;
if (!targetDate) return false;
while (i < source.length - 1) {
if (source.charAt(i) === '{' && source.charAt(i + 1) === '{') {
match = getTemplateMatchAt(source, i, nameRe);
if (match) {
templateDate = convertToStandardDate(String(match.params[0] || '').replace(/^\s*1\s*=\s*/, ''));
if (templateDate === targetDate) return true;
i = match.end;
continue;
}
end = findBalancedTemplateEnd(source, i);
if (end > i) { i = end; continue; }
}
i++;
}
return false;
}
function getTemplateRemovalRange(text, match) {
var start = match.start;
var end = match.end;
var before = text.slice(0, start).match(/<noinclude>\s*$/i);
var after;
if (before) {
after = text.slice(end).match(/^\s*<\/noinclude>\s*\n?/i);
if (after) {
return { start: before.index, end: end + after[0].length };
}
}
after = text.slice(end).match(/^[ \t]*(?:\r?\n)?/);
if (after) end += after[0].length;
return { start: start, end: end };
}
function stripTemplatesByPattern(text, namePattern) {
var source = String(text || '');
var nameRe = new RegExp('^(?:' + namePattern + ')$', 'i');
var ranges = [];
var out = [];
var pos = 0;
var i = 0;
var end;
var match;
var range;
while (i < source.length - 1) {
if (source.charAt(i) === '{' && source.charAt(i + 1) === '{') {
match = getTemplateMatchAt(source, i, nameRe);
if (match) {
range = getTemplateRemovalRange(source, match);
ranges.push(range);
i = Math.max(match.end, range.end);
continue;
}
end = findBalancedTemplateEnd(source, i);
if (end > i) { i = end; continue; }
}
i++;
}
if (!ranges.length) return { text: source, removed: false };
ranges.forEach(function (r) {
if (r.start < pos) r.start = pos;
out.push(source.slice(pos, r.start));
pos = r.end;
});
out.push(source.slice(pos));
return { text: out.join(''), removed: true };
}
function removeTemplatesByAliases(text, aliases) {
var pattern = buildTemplateNamePattern(aliases);
if (!pattern) return { text: text, removed: false };
return stripTemplatesByPattern(text, '(?:' + pattern + ')');
}
function normalizeAliasKey(title) {
return String(title || '').replace(/_/g, ' ').toLowerCase().trim();
}
function dedupeAliases(aliases) {
var seen = {};
var result = [];
(aliases || []).forEach(function (alias) {
var key = normalizeAliasKey(alias);
if (!key || seen[key]) return;
seen[key] = true;
result.push(alias);
});
return result;
}
function resolveTemplateRedirectAliases(titles, callback) {
var list = dedupeAliases(titles);
var missingTitles = list.filter(function (t) { return !tplAliasCache[t]; });
function collectAliases() {
var allAliases = [];
list.forEach(function (title) { allAliases = allAliases.concat(tplAliasCache[title] || [title]); });
callback(dedupeAliases(allAliases));
}
if (!missingTitles.length) { collectAliases(); return; }
(function resolveChunk(offset) {
if (offset >= missingTitles.length) { collectAliases(); return; }
var chunk = missingTitles.slice(offset, offset + 20);
var chunkByKey = {};
chunk.forEach(function (t) { chunkByKey[normalizeAliasKey(t)] = t; });
apiReq({ prop: 'redirects', rdlimit: 'max', titles: chunk.join('|') }, 'query', function (resp) {
var pages = resp && resp.query && resp.query.pages ? resp.query.pages : {};
Object.keys(pages).forEach(function (pid) {
var p = pages[pid];
var sourceTitle;
var found;
if (!p || !p.title) return;
sourceTitle = chunkByKey[normalizeAliasKey(p.title)];
if (!sourceTitle) return;
found = [p.title].concat((p.redirects || []).map(function (r) { return r.title; }));
tplAliasCache[sourceTitle] = dedupeAliases(found);
});
chunk.forEach(function (t) { if (!tplAliasCache[t]) tplAliasCache[t] = [t]; });
resolveChunk(offset + 20);
});
}(0));
}
function removeTransferTemplatesLocal(articleText, transferMode) {
var result = { text: articleText, removedKbu: false, removedKul: false, removedHangon: false };
if (transferMode === 'none') return result;
if (transferMode === 'kbu' || transferMode === 'both') {
var kbu = stripTemplatesByPattern(result.text, '(?:' + KBU_PATTERN_STR + ')');
result.text = kbu.text; result.removedKbu = kbu.removed;
}
if (transferMode === 'kul' || transferMode === 'both') {
var kul = stripTemplatesByPattern(result.text, KUL_PATTERN_STR);
result.text = kul.text; result.removedKul = kul.removed;
}
var hangon = stripTemplatesByPattern(result.text, HANGON_PATTERN_STR);
result.text = hangon.text; result.removedHangon = hangon.removed;
return result;
}
function removeTransferTemplatesWithApiFallback(pageName, articleText, transferMode, localResult, callback) {
var needKbu = (transferMode === 'kbu' || transferMode === 'both') && !localResult.removedKbu;
var needKul = (transferMode === 'kul' || transferMode === 'both') && !localResult.removedKul;
var needHangon = transferMode !== 'none' && !localResult.removedHangon;
if (!needKbu && !needKul && !needHangon) { callback(localResult); return; }
var titleMap = {};
if (needKbu) ['Шаблон:К быстрому удалению','Шаблон:К отсроченному удалению','Шаблон:Deleteslow','Шаблон:Ds'].forEach(function (t) { titleMap[t] = true; });
if (needKul) titleMap['Шаблон:К улучшению'] = true;
if (needHangon) { titleMap['Шаблон:Hangon'] = true; titleMap['Шаблон:Hang-on'] = true; }
apiReq({ prop: 'templates', titles: pageName, tllimit: 'max' }, 'query', function (data) {
var page = getFirstQueryPage(data);
if (page && page.templates) {
page.templates.forEach(function (tpl) {
var norm = normalizeTemplateName(tpl.title);
if ((needKbu && (RE_KBU_PATTERNS.test(norm) || norm === 'к быстрому удалению' || norm === 'к отсроченному удалению' || norm === 'deleteslow' || norm === 'ds')) ||
(needKul && RE_KUL_PATTERN.test(norm)) ||
(needHangon && RE_HANGON.test(norm))) {
titleMap[tpl.title] = true;
}
});
}
var titles = Object.keys(titleMap);
if (!titles.length) { callback(localResult); return; }
resolveTemplateRedirectAliases(titles, function (aliases) {
var updated = $.extend({}, localResult);
var r = removeTemplatesByAliases(updated.text, aliases);
updated.text = r.text;
if (r.removed) {
if (needKbu) updated.removedKbu = true;
if (needKul) updated.removedKul = true;
if (needHangon) updated.removedHangon = true;
}
callback(updated);
});
});
}
function removeMoveToCommonsTemplatesLocal(articleText) {
var r = removeTemplatesByAliases(articleText, MOVE_TO_COMMONS_TEMPLATE_ALIASES);
return { text: r.text, removedMoveToCommons: r.removed };
}
function removeMoveToCommonsTemplatesWithApiFallback(pageName, articleText, localResult, callback) {
apiReq({ prop: 'templates', titles: pageName, tllimit: 'max' }, 'query', function (data) {
var page = getFirstQueryPage(data);
var titleMap = {};
var normTarget = normalizeTemplateName(MOVE_TO_COMMONS_TEMPLATE_TITLE);
var titles;
titleMap[MOVE_TO_COMMONS_TEMPLATE_TITLE] = true;
if (page && page.templates) {
page.templates.forEach(function (tpl) {
var norm = normalizeTemplateName(tpl.title);
if (norm === normTarget || MOVE_TO_COMMONS_TEMPLATE_ALIASES.some(function (alias) {
return norm === normalizeTemplateName(alias);
})) {
titleMap[tpl.title] = true;
}
});
}
titles = Object.keys(titleMap);
resolveTemplateRedirectAliases(titles, function (resolvedAliases) {
var aliases = dedupeAliases(MOVE_TO_COMMONS_TEMPLATE_ALIASES.concat(resolvedAliases));
var updated = $.extend({}, localResult);
var r = removeTemplatesByAliases(articleText, aliases);
updated.text = r.text;
updated.removedMoveToCommons = r.removed;
callback(updated);
});
});
}
// ─── Вставка шаблонов ────────────────────────────────────────────────────
function findInsertPositionAfterProjectTemplates(text) {
var pos = 0, len = text.length;
while (pos < len) {
var wsMatch = text.slice(pos).match(/^[\t ]*\n/);
if (wsMatch) { pos += wsMatch[0].length; continue; }
if (text.charAt(pos) !== '{' || text.charAt(pos + 1) !== '{') break;
var afterOpen = text.slice(pos + 2);
var nameMatch = afterOpen.match(/^[\s]*([\s\S]*?)[\s]*(?:\||\}\})/);
var templateEnd;
if (!nameMatch) break;
var tplName = nameMatch[1].toLowerCase().replace(/\s+/g, ' ').trim();
if (!/^статья проекта(\s|$)/.test(tplName) && tplName !== 'блок проектов статьи') break;
templateEnd = findBalancedTemplateEnd(text, pos);
if (templateEnd < 0) break;
pos = templateEnd;
if (pos < len && text.charAt(pos) === '\n') pos++;
}
return pos;
}
function insertTplOnTalkPage(text, tplText, sep) {
var s = (sep === undefined) ? '\n' : sep;
var insertPos = findInsertPositionAfterProjectTemplates(text);
if (insertPos === 0) return tplText + (text.length ? s + text.replace(/^\n+/, '') : '');
var before = text.slice(0, insertPos).replace(/\n+$/, '');
var after = text.slice(insertPos).replace(/^\n+/, '');
return before + '\n' + tplText + (after.length ? s + after : '');
}
function wrapInNoinclude(text, templateText) {
var match = text.match(RE_NOINCLUDE);
if (match) {
// Если перед noinclude есть непробельный контент — вставляем новый noinclude сверху
var before = text.slice(0, text.indexOf(match[0]));
if (/\S/.test(before)) {
return '<noinclude>' + templateText + '</noinclude>\n' + text;
}
var content = match[2];
if (content.length > 0 && content.charAt(content.length - 1) !== '\n') content += '\n';
return text.replace(match[0], match[1] + '<noinclude>' + content + templateText + '\n</noinclude>');
}
return '<noinclude>' + templateText + '</noinclude>\n' + text;
}
function getDateSectionTalkTemplateConfig(closeType) {
var config = DATE_SECTION_TALK_TEMPLATES[closeType];
return typeof config === 'string' ? { name: config } : (config || null);
}
function upsertDateSectionTemplateOnTalkPage(text, templateName, dateIso, sectionTitle, options) {
var source = text || '';
var normalizedSection = String(sectionTitle || '').trim();
var opts = options || {};
var tplRe = buildTemplateTransclusionRegex(templateName, opts.aliases);
var tplMatch = tplRe ? source.match(tplRe) : null;
function buildSectionParam(sectionIndex, currentParams) {
var paramName;
var paramsText = String(currentParams || '');
if (!normalizedSection) return '';
if (opts.singleSectionParam) {
paramName = opts.sectionParam || 'раздел обсуждения';
if (new RegExp('(?:^|\\|)\\s*(?:' + escapeRegExp(paramName) + '|l1)\\s*=', 'i').test(paramsText)) return '';
return '|' + paramName + '=' + normalizedSection;
}
return '|l' + sectionIndex + '=' + normalizedSection;
}
function buildExistingTplWithCanonicalName(suffix) {
return T_OPEN + templateName + String(tplMatch[2] || '').replace(/\s+$/, '') + (suffix || '') + T_CLOSE;
}
if (!tplMatch) {
return { text: insertTplOnTalkPage(source, T_OPEN + templateName + '|' + dateIso + buildSectionParam(1, '') + T_CLOSE, '\n'), status: 'created' };
}
var parts = tplMatch[2].split('|').map(function (p) { return p.trim(); }).filter(Boolean);
var existingDates = parts.filter(function (p) { return p.indexOf('=') === -1; }).map(function (p) { return convertToStandardDate(p); });
var stdDate = convertToStandardDate(dateIso);
if (existingDates.indexOf(stdDate) !== -1) {
var canonicalTpl = buildExistingTplWithCanonicalName('');
if (canonicalTpl !== tplMatch[0]) {
return { text: source.replace(tplMatch[0], canonicalTpl), status: 'updated' };
}
return { text: source, status: 'already_present' };
}
var nextIdx = existingDates.length + 1;
var suffix = '|' + dateIso + buildSectionParam(nextIdx, tplMatch[2]);
return {
text: source.replace(tplMatch[0], buildExistingTplWithCanonicalName(suffix)),
status: 'updated'
};
}
function getNamedTemplateParam(params, names) {
var wanted = {};
names.forEach(function (name) { wanted[String(name).toLowerCase()] = true; });
for (var i = 0; i < params.length; i++) {
var m = String(params[i] || '').match(/^\s*([^=]+?)\s*=\s*([\s\S]*)$/);
if (m && wanted[m[1].toLowerCase().trim()]) return m[2].trim();
}
return '';
}
function getSingleResultTemplateDate(paramsText, templateName) {
var params = String(paramsText || '').split('|').map(function (p) { return p.trim(); }).filter(Boolean);
var namedDate = getNamedTemplateParam(params, templateName === 'Переименовано' ? ['date', 'дата'] : ['date']);
var positionalDate = params.filter(function (p) { return p.indexOf('=') === -1; })[0] || '';
return convertToStandardDate(namedDate || positionalDate);
}
function upsertSingleDateResultTemplateOnTalkPage(text, templateName, dateIso, paramsText) {
var source = text || '';
var tplRe = buildTemplateTransclusionRegex(templateName, null, 'ig');
var tplMatch = null;
var match;
var newTpl = T_OPEN + templateName + '|' + dateIso + (paramsText ? '|' + paramsText : '') + T_CLOSE;
var stdDate = convertToStandardDate(dateIso);
var existingDate;
var canonicalTpl;
if (tplRe) {
while ((match = tplRe.exec(source)) !== null) {
existingDate = getSingleResultTemplateDate(match[2], templateName);
if (existingDate === stdDate) { tplMatch = match; break; }
if (match[0].length === 0) tplRe.lastIndex++;
}
}
if (!tplMatch) {
return { text: insertTplOnTalkPage(source, newTpl, '\n'), status: 'created' };
}
canonicalTpl = T_OPEN + templateName + String(tplMatch[2] || '').replace(/\s+$/, '') + T_CLOSE;
if (canonicalTpl !== tplMatch[0]) {
return { text: source.replace(tplMatch[0], canonicalTpl), status: 'updated' };
}
return { text: source, status: 'already_present' };
}
function buildConditionalRetTemplateText(dateIso, sectionTitle, reasonText, deadlineText, sectionIndex) {
var normalizedSection = String(sectionTitle || '').trim();
var normalizedReason = normalizeQuickPhraseValue(reasonText);
var normalizedDeadline = String(deadlineText || '').trim();
var index = parseInt(sectionIndex, 10);
var tpl = 'Условно оставлено|' + dateIso;
if (isNaN(index) || index < 1) index = 1;
if (normalizedSection) tpl += '|l' + index + '=' + normalizedSection;
if (normalizedReason) tpl += '|пояснение=' + normalizedReason;
if (normalizedDeadline) tpl += '|срок=' + normalizedDeadline;
return T_OPEN + tpl + T_CLOSE;
}
function upsertConditionalRetTemplateOnTalkPage(text, dateIso, sectionTitle, reasonText, deadlineText) {
var source = text || '';
var normalizedSection = String(sectionTitle || '').trim();
var normalizedReason = normalizeQuickPhraseValue(reasonText);
var normalizedDeadline = String(deadlineText || '').trim();
var tplRe = buildTemplateTransclusionRegex('Условно оставлено');
var tplMatch = tplRe ? source.match(tplRe) : null;
function buildExistingTplWithCanonicalName(suffix) {
return T_OPEN + 'Условно оставлено' + String(tplMatch[2] || '').replace(/\s+$/, '') + (suffix || '') + T_CLOSE;
}
if (!tplMatch) {
return {
text: insertTplOnTalkPage(source, buildConditionalRetTemplateText(dateIso, normalizedSection, normalizedReason, normalizedDeadline, 1), '\n'),
status: 'created'
};
}
var parts = tplMatch[2].split('|').map(function (p) { return p.trim(); }).filter(Boolean);
var existingDates = parts.filter(function (p) { return p.indexOf('=') === -1; }).map(function (p) { return convertToStandardDate(p); });
var stdDate = convertToStandardDate(dateIso);
if (existingDates.indexOf(stdDate) !== -1) {
var canonicalTpl = buildExistingTplWithCanonicalName('');
if (canonicalTpl !== tplMatch[0]) {
return { text: source.replace(tplMatch[0], canonicalTpl), status: 'updated' };
}
return { text: source, status: 'already_present' };
}
var nextIdx = existingDates.length + 1;
var suffix = '|' + dateIso;
if (normalizedSection) suffix += '|l' + nextIdx + '=' + normalizedSection;
if (normalizedReason) suffix += '|пояснение=' + normalizedReason;
if (normalizedDeadline) suffix += '|срок=' + normalizedDeadline;
return {
text: source.replace(tplMatch[0], buildExistingTplWithCanonicalName(suffix)),
status: 'updated'
};
}
// ═══════════════════════════════════════════════════════════════════════════
// ПАЙПЛАЙН НОМИНАЦИИ
// ═══════════════════════════════════════════════════════════════════════════
function runNominationPipeline(steps) {
var s = steps;
var ctx = { templateMeta: null, nominationInfo: null };
var stages = [
{
name: 'шаблон',
fn: function (next) {
s.templateStep(function (err, meta) { ctx.templateMeta = meta || null; next(err); });
}
},
{
name: 'номинация',
pendingText: 'Публикуется номинация...',
successText: 'Номинация опубликована.',
errorText: 'Публикация номинации.',
fn: function (next) {
s.nominationStep(function (err, info) { ctx.nominationInfo = info || null; next(err); });
}
},
{
name: 'подписка',
shouldRun: function () {
var info = ctx.nominationInfo;
return !!(setSubscribe && info && info.pageTitle && info.sectionTitle);
},
fn: function (next) {
subscribeToTopic(ctx.nominationInfo.pageTitle, ctx.nominationInfo.sectionTitle, function () { next(); });
}
},
{
name: 'оповещение',
shouldRun: function () { return !!(setAlert && !s.skipNotify); },
fn: function (next) { s.notifyStep(ctx.nominationInfo, next); }
}
];
(function run(i) {
if (i >= stages.length) { if (typeof s.onSuccess === 'function') s.onSuccess(ctx); return; }
var stage = stages[i];
if (typeof stage.shouldRun === 'function' && !stage.shouldRun()) { run(i + 1); return; }
var statusId = stage.pendingText
? logStatus(stage.pendingText, null, { pending: true, trackError: false })
: null;
try {
stage.fn(function (err) {
if (err) {
if (statusId) logStatus(stage.errorText || ('Этап «' + stage.name + '».'), err, { statusId: statusId });
if (typeof s.onFailure === 'function') s.onFailure(stage.name, err, ctx);
else markSubmitError();
return;
}
if (statusId && stage.successText) logStatus(stage.successText, null, { statusId: statusId, trackError: false });
run(i + 1);
});
} catch (ex) {
var exErr = { code: 'exception', info: (ex && ex.message) ? ex.message : String(ex) };
if (statusId) logStatus(stage.errorText || ('Этап «' + stage.name + '».'), exErr, { statusId: statusId });
if (typeof s.onFailure === 'function') s.onFailure(stage.name, exErr, ctx);
else markSubmitError();
}
}(0));
}
// ─── Публикация номинации ────────────────────────────────────────────────
function publishNomination(opts, callback) {
var cb = callback || function () {};
var maxRetries = Math.max(0, parseInt(opts.editConflictRetries, 10) || 1);
function doPublish() {
apiReq({
title: opts.pageTitle,
section: 'new',
sectiontitle: opts.sectionTitle,
summary: opts.summary,
text: opts.text,
assertuser: mwCfg.wgUserName
}, 'edit', function (resp) {
cb(resp && resp.error ? resp.error : null);
});
}
if (opts.sectionTitle) {
if (!opts.navTemplate) { doPublish(); return; }
apiReq({ title: opts.pageTitle, createonly: '1', text: T_OPEN + opts.navTemplate + '-Навигация' + T_CLOSE + '\n', summary: makeSummary('автоматическая шапка'), assertuser: mwCfg.wgUserName },
'edit', function (resp) {
if (resp && resp.error && resp.error.code !== 'articleexists') { cb(resp.error); return; }
doPublish();
});
return;
}
// Вставка в существующую страницу
if (opts.createText !== undefined) {
(function attempt(retry) {
getTextWithTimestamp(opts.pageTitle, function (pageText, baseTimestamp, readErr) {
var result;
var ep;
if (readErr) { cb(makeReadError(readErr, 'read_failed', opts.readErrorMessage || 'Не удалось получить содержимое.')); return; }
result = pageText === null
? (typeof opts.createText === 'function' ? opts.createText() : opts.createText)
: (opts.buildText ? opts.buildText(pageText) : null);
if (typeof result === 'string') result = { text: result };
if (!result || result.error) { cb((result && result.error) || { code: 'build_failed', info: 'Не удалось подготовить правку.' }); return; }
if (result.skip) { cb(null); return; }
if (typeof result.text !== 'string') { cb({ code: 'build_failed', info: 'Не удалось подготовить правку.' }); return; }
ep = {
title: opts.pageTitle,
text: result.text,
summary: result.summary || opts.summary,
assertuser: mwCfg.wgUserName
};
if (pageText === null) ep.createonly = true;
else if (baseTimestamp) ep.basetimestamp = baseTimestamp;
apiReq(ep, 'edit', function (resp) {
var err = resp && resp.error ? resp.error : null;
if (err && (err.code === 'editconflict' || err.code === 'articleexists') && retry < maxRetries) {
attempt(retry + 1);
return;
}
cb(err);
});
});
}(0));
return;
}
editPageContent(opts.pageTitle, { summary: opts.summary, readError: opts.readErrorMessage || 'Не удалось получить содержимое.' },
function (pageText) { return opts.buildText ? opts.buildText(pageText) : null; },
function (err) { cb(err || null); }
);
}
// ─── Оповещение авторов ──────────────────────────────────────────────────
function notifyAuthor(pg, options, callback) {
var opts = options || {};
var cb = callback || function () {};
var actionText = (typeof opts.actionText === 'string') ? opts.actionText : '';
var discussionPage = (typeof opts.discussionPage === 'string') ? opts.discussionPage : '';
var discussionSection = normalizeSectionForLink((typeof opts.discussionSection === 'string') ? opts.discussionSection : '');
var includeProposed = (typeof opts.includeProposedPrefix === 'boolean') ? opts.includeProposedPrefix : true;
var actionPhrase = ((includeProposed ? 'предложена ' : '') + actionText).trim() || 'изменена';
var discussionText = discussionPage ? 'Обсуждение — на странице [[' + discussionPage + (discussionSection ? '#' + discussionSection : '') + ']]. ' : '';
apiReq({ prop: 'revisions', rvprop: 'user', rvdir: 'newer', titles: pg }, 'query', function (queryResp) {
var page = getFirstQueryPage(queryResp);
if (!page) { cb({ code: 'network', info: 'Network error' }); return; }
if (page.missing !== undefined) { cb({ code: 'missing', info: 'Page missing.' }); return; }
if (!page.revisions || !page.revisions.length) { cb({ code: 'no_revisions', info: 'No revisions.' }); return; }
var rv = page.revisions[0];
if ('anon' in rv || rv.userhidden || !rv.user || rv.user === mwCfg.wgUserName) { cb(null); return; }
apiReq({
title: 'User talk:' + rv.user, section: 'new',
sectiontitle: 'Remover: [[:' + pg + ']]',
summary: opts.summary || makeSummary('уведомление автора'),
text: 'Страница [[:' + pg + ']], созданная вами, ' + actionPhrase + '. ' +
discussionText +
'~~' + '~~<br><small>Это автоматическое уведомление, сгенерированное ' + scriptLink + '.</small>',
assertuser: mwCfg.wgUserName
}, 'edit', function (editResp) { cb(editResp && editResp.error ? editResp.error : null); });
});
}
function notifyAuthorsForPages(pages, notifyOptions, callback) {
var cb = callback || function () {};
var opts = notifyOptions || {};
var list = [];
(pages || []).forEach(function (p) { var t = normTitle(p); if (t && list.indexOf(t) === -1) list.push(t); });
if (!list.length) { cb(); return; }
eachSequential(list, function (pg, next) {
var pageLink = buildQuotedStatusPageLink(pg);
var statusId = logStatus('Отправляется уведомление создателю страницы ' + pageLink + '...', null, { pending: true, trackError: false });
notifyAuthor(pg, opts, function (err) {
logStatus(err ? 'Уведомление создателя страницы ' + pageLink + '.' : 'Создатель страницы ' + pageLink + ' уведомлён.', err,
{ statusId: statusId, trackError: opts.trackError !== false });
next();
});
}, cb);
}
// ─── Подписка на раздел ──────────────────────────────────────────────────
function subscribeToTopic(pageTitle, sectionTitle, callback) {
var cb = callback || function () {};
if (!setSubscribe || !sectionTitle) { cb(); return; }
var statusId = logStatus('Оформляется подписка на раздел...', null, { pending: true, trackError: false });
var targetFrag = normalizeSectionForLink(sectionTitle).toLowerCase();
function finish(err, st) {
if (err) { logStatus('Не удалось подписаться на раздел.', err, { statusId: statusId, trackError: false }); cb(); return; }
logStatus(st === 'subscribed' ? 'Оформлена подписка на раздел.' : 'Раздел для подписки не найден.', null, { statusId: statusId, trackError: false });
cb();
}
function trySubscribe(attemptsLeft) {
apiReq({ page: pageTitle, prop: 'threaditemshtml', excludesignatures: true }, 'discussiontoolspageinfo', function (data) {
var items = (data && data.discussiontoolspageinfo && data.discussiontoolspageinfo.threaditemshtml) || null;
if (!items || !items.length) {
if (attemptsLeft > 0) { setTimeout(function () { trySubscribe(attemptsLeft - 1); }, 2000); return; }
finish(null, 'not_found'); return;
}
var commentname = null;
for (var ti = items.length - 1; ti >= 0; ti--) {
var t = items[ti];
if (t.type === 'heading') {
var htext = (t.headingText || t.html || '').replace(/<[^>]+>/g, '').trim();
if (htext.toLowerCase() === targetFrag || normalizeSectionForLink(htext).replace(/_/g, ' ').toLowerCase() === targetFrag) {
commentname = t.name; break;
}
}
}
if (!commentname) {
if (attemptsLeft > 0) setTimeout(function () { trySubscribe(attemptsLeft - 1); }, 2000);
else finish(null, 'not_found');
return;
}
apiReq({ page: pageTitle, commentname: commentname, subscribe: '1' }, 'discussiontoolssubscribe', function (res) {
finish(res && res.error ? res.error : null, 'subscribed');
});
});
}
setTimeout(function () { trySubscribe(2); }, 1500);
}
// ═══════════════════════════════════════════════════════════════════════════
// UI: модальные окна
// ═══════════════════════════════════════════════════════════════════════════
function syncModalLayout() {
var syncFn = $('#removerModal').data('rmSyncLayout');
if (typeof syncFn === 'function') syncFn();
}
function clearModalLayoutSyncHandlers() {
modalLayoutSyncHandlers = [];
$('#removerModal').removeData('rmSyncLayout');
}
function registerModalLayoutSync(handler) {
if (typeof handler !== 'function') return;
if (modalLayoutSyncHandlers.indexOf(handler) === -1) modalLayoutSyncHandlers.push(handler);
$('#removerModal').data('rmSyncLayout', function () {
modalLayoutSyncHandlers.slice().forEach(function (fn) {
if (typeof fn === 'function') fn();
});
});
}
function registerResizeObserver(observer) {
if (observer) resizeObservers.push(observer);
}
function resetModalObservers() {
resizeObservers.forEach(function (observer) {
if (observer && typeof observer.disconnect === 'function') observer.disconnect();
});
resizeObservers = [];
clearModalLayoutSyncHandlers();
$(window).off('resize.removerModal');
$(window).off('.rmTaResize');
}
function closeModal() {
resetModalObservers();
$(window).off('keydown.remover');
if (isVector22) $('#content').css('min-width', '');
$('#removerModal').remove();
}
function ensureModalStyles() {
if (document.getElementById('removerModalDynamicStyles')) return;
var progH = 'background:' + tk.bgProgH + '!important;border-color:' + tk.bProgH + '!important;color:' + tk.cInv + '!important;';
var neutH = 'background:' + tk.bgN + '!important;border-color:' + tk.bSub + '!important;color:inherit!important;';
var succH = 'background:' + tk.bgSuccH+ '!important;border-color:' + tk.bSuccH + '!important;color:' + tk.cInv + '!important;';
var pillBg = 'linear-gradient(180deg,' + tk.bgBase + ' 0%,' + tk.bgNSub + ' 100%)';
var pillShadow = '0 1px 0 rgba(255,255,255,.08) inset,0 1px 2px rgba(0,0,0,.18)';
var activePillShadow = '0 1px 0 rgba(255,255,255,.14) inset,0 2px 6px rgba(51,102,204,.24)';
var css = [
'#removerModal,#removerModal *{-moz-text-size-adjust:none!important;-webkit-text-size-adjust:100%!important;text-size-adjust:100%!important}',
'#removerModal{color:inherit}',
'#removerModal input::placeholder,#removerModal textarea::placeholder{color:var(--color-subtle,currentColor);opacity:.7}',
'#removerModal input[type="checkbox"],#removerModal input[type="radio"]{appearance:auto;-webkit-appearance:auto;-moz-appearance:auto;accent-color:auto}',
'#removerModal input[type="checkbox"]{outline:none!important;box-shadow:none!important}',
'#removerModal button{transition:background-color .12s ease,border-color .12s ease,color .12s ease,box-shadow .12s ease,filter .12s ease,transform .06s ease}',
'#removerModal .rmToggleBtn{background:' + tk.bgNSub + '!important;border-color:' + tk.bSub + '!important;color:inherit!important}',
'#removerModal .rmToggleBtn.is-active{background:' + tk.bgProg + '!important;border-color:' + tk.bProg + '!important;color:' + tk.cInv + '!important}',
'#removerModal button:not(:disabled):hover{filter:brightness(.97)}',
'#removerModal button:not(:disabled):active{transform:translateY(1px)}',
'#removerModal #removerSubmit:not(:disabled):not(.rmSubmitError):hover,#removerModal .rmToggleBtn.is-active:hover{' + progH + 'filter:none}',
'#removerModal #removerSubmit:not(:disabled):not(.rmSubmitError):active,#removerModal .rmToggleBtn.is-active:active{' + progH + 'filter:brightness(.92)!important}',
'#removerModal #removerSubmit.rmSubmitReady:not(.rmSubmitError){background:' + tk.bgProg + '!important;border-color:' + tk.bProg + '!important;color:' + tk.cInv + '!important;outline:none!important;box-shadow:0 0 0 6px rgba(51,102,204,.13),0 1px 2px rgba(0,0,0,.08)!important}',
'#removerModal #removerSubmit.rmSubmitReady:not(.rmSubmitError):not(:disabled):hover{' + progH + 'box-shadow:0 0 0 7px rgba(51,102,204,.16),0 1px 2px rgba(0,0,0,.1)!important;filter:none}',
'#removerModal #removerSubmit.rmSubmitReady:not(.rmSubmitError):not(:disabled):active{' + progH + 'box-shadow:0 0 0 5px rgba(51,102,204,.14),0 1px 2px rgba(0,0,0,.08)!important;filter:brightness(.92)!important}',
'#removerModal .rmAddPageBtn{background:' + tk.bgProg + '!important;border-color:' + tk.bProg + '!important;color:' + tk.cInv + '!important;font-weight:700!important}',
'#removerModal .rmAddPageBtn:hover{' + progH + 'filter:none!important}',
'#removerModal .rmAddVariantBtn{background:' + tk.bgBase + '!important;border-color:' + tk.bSub + '!important;color:inherit!important;font-weight:700!important}',
'#removerModal .rmAddVariantBtn:hover{' + neutH + 'filter:none!important}',
'#removerModal .rmRenameVariantAddBtn{font-size:15px!important}',
'#removerModal .rmStartMultiPageBtn{align-self:flex-start!important;margin-bottom:0!important}',
'#removerModal .rmRenameVariantRow{width:100%!important;max-width:100%!important;box-sizing:border-box!important}',
'#removerModal .rmMultiRenameVariantsContainer{display:flex;flex-direction:column;gap:6px;box-sizing:border-box;margin-top:6px;width:100%!important;max-width:100%!important}',
'#removerModal .rmMultiRenamePrimaryTargetRow,#removerModal .rmMultiRenameVariantRow{display:flex;margin-bottom:0!important;box-sizing:border-box}',
'#removerModal .rmMultiPageCommentToggle{min-width:32px;height:32px;padding:0!important;font-size:15px!important;line-height:1!important}',
'#removerModal .rmMultiPageCommentToggle.is-active{background:#bfc4ca!important;border-color:#8f98a3!important;color:#202122!important}',
'#removerModal .rmMultiPageCommentToggle.is-active:hover{background:#b4bac1!important;border-color:#848e99!important;color:#202122!important;filter:none}',
'#removerModal .rmMultiPageCommentToggle.is-active:active{background:#a9b0b8!important;border-color:#79838f!important;color:#202122!important;filter:none}',
'#removerModal #removerSubmit.rmSubmitError:not(:disabled):hover{background:#b32424!important;border-color:#b32424!important;color:#fff!important;filter:none}',
'#removerModal #removerSubmit.rmSubmitError:not(:disabled):active{background:#b32424!important;border-color:#b32424!important;color:#fff!important;filter:brightness(.88)!important}',
'#removerModal #removerReload:not(:disabled):hover{' + succH + 'filter:none}',
'#removerModal #removerReload:not(:disabled):active{' + succH + 'filter:brightness(.92)!important}',
'#removerModal button:not(#removerSubmit):not(#removerReload):not(.is-active):not(.rmAddPageBtn):not(.rmAddVariantBtn):hover,#removerModal .rmToggleBtn:not(.is-active):not(.rmAddPageBtn):not(.rmAddVariantBtn):hover{' + neutH + 'filter:none}',
'#removerModal button:not(#removerSubmit):not(#removerReload):not(.is-active):not(.rmAddPageBtn):not(.rmAddVariantBtn):active{' + neutH + 'filter:brightness(.92)!important}',
'#removerModal a.removerModalLink{color:' + tk.cProg + ';text-decoration:none;border-bottom:0;box-shadow:none;word-break:break-word;overflow-wrap:anywhere}',
'#removerModal a.removerModalLink:hover,#removerModal a.removerModalLink:focus{color:' + tk.cProgH + ';text-decoration:underline}',
'#removerModal #removerModalSubtitle{font-size:12px!important;line-height:1.35!important;font-weight:400!important;color:' + tk.cSubM + '!important;max-width:100%;overflow-wrap:anywhere;word-break:break-word}',
'#removerModal #removerModalSubtitle a{font-size:inherit!important;line-height:inherit!important;font-weight:inherit!important}',
'#removerModal a.rmButtonLikeLink{color:' + tk.cBase + '!important;text-decoration:none!important;transform:none!important;transition:background-color .12s ease,border-color .12s ease,color .12s ease,filter .12s ease,transform .06s ease!important}',
'#removerModal a.rmButtonLikeLink:hover{' + neutH + 'text-decoration:none!important;transform:none!important}',
'#removerModal a.rmButtonLikeLink:focus{text-decoration:none!important}',
'#removerModal a.rmButtonLikeLink:focus:not(:focus-visible){outline:none!important}',
'#removerModal a.rmButtonLikeLink:focus-visible{outline:2px solid ' + tk.bProg + '!important;outline-offset:2px;text-decoration:none!important}',
'#removerModal a.rmButtonLikeLink:hover:active{' + neutH + 'filter:brightness(.92)!important;transform:translateY(1px)!important;text-decoration:none!important}',
'#removerModal .rmInfoBox{margin:0 0 10px;padding:8px 10px;border:1px solid ' + tk.bSub + ';border-radius:6px;background:' + tk.bgNSub + '}',
'#removerModal .rmActionList{display:flex;flex-direction:column;gap:6px}',
'#removerModal .rmActionItem{display:block;padding:8px 10px;border:1px solid ' + tk.bSub + ';border-radius:6px;background:' + tk.bgBase + ';cursor:pointer;transition:background-color .12s ease,transform .06s ease}',
'#removerModal .rmActionItem:hover{background:' + tk.bgNSub + '}',
'#removerModal .rmActionItem:active{transform:translateY(1px)}',
'#removerModal .rmActionMain{display:flex;align-items:center}',
'#removerModal .rmActionMain input[type="radio"]{margin-right:8px}',
'#removerModal .rmActionMeta{display:block;margin-left:24px;margin-top:2px;color:' + tk.cSubM + ';font-size:12px;line-height:1.35}',
'#removerModal .rmActionWarning{display:none;margin-left:24px;margin-top:3px;color:' + tk.cDang + ';font-size:12px;line-height:1.35;font-weight:700}',
'#removerModal .rmConflictLead{margin:0 0 10px;color:' + tk.cSubM + ';font-size:13px;line-height:1.5}',
'#removerModal .rmConflictList{display:flex;flex-direction:column;gap:10px}',
'#removerModal .rmConflictCard{padding:12px;border:1px solid ' + tk.bSub + ';border-radius:8px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.55) inset}',
'#removerModal .rmConflictCard.is-skip{background:' + tk.bgNSub + ';border-color:' + tk.bSubS + '}',
'#removerModal .rmConflictTitle{font-size:14px;font-weight:700;line-height:1.4;color:' + tk.cBase + ';word-break:break-word;overflow-wrap:anywhere}',
'#removerModal .rmConflictMeta{margin-top:4px;font-size:12px;line-height:1.45;color:' + tk.cSubM + '}',
'#removerModal .rmConflictGroup{margin-top:10px}',
'#removerModal .rmConflictGroupTitle{margin:0 0 6px;font-size:12px;font-weight:700;line-height:1.35;color:' + tk.cSubM + '}',
'#removerModal .rmConflictButtons{display:flex;flex-wrap:wrap;gap:6px}',
'#removerModal .rmConflictChoice{padding:5px 10px}',
'#removerModal .rmConflictChoice.is-disabled,#removerModal .rmConflictButtons.is-disabled .rmConflictChoice{opacity:.55;cursor:not-allowed;pointer-events:none}',
'#removerModal .rmConflictHint{margin-top:8px;font-size:11px;line-height:1.45;color:' + tk.cSub + '}',
'#removerModal #rmSettingsForm{display:flex;flex-direction:column;gap:14px}',
'#removerModal .rmSettingsLead{margin:-2px 0 2px;color:' + tk.cSubM + ';font-size:13px;line-height:1.5}',
'#removerModal .rmSettingsSection{margin:0;padding:14px 16px;border:1px solid ' + tk.bSubS + ';border-radius:10px;background:linear-gradient(180deg,' + tk.bgBase + ' 0%,' + tk.bgNSub + ' 100%);box-shadow:0 1px 0 rgba(255,255,255,.7) inset}',
'#removerModal .rmSettingsSectionHeader{margin:0 0 12px}',
'#removerModal .rmSettingsSectionTitle{font-size:14px;font-weight:700;line-height:1.35;color:' + tk.cBase + '}',
'#removerModal .rmSettingsSectionDescription{margin-top:4px;font-size:12px;line-height:1.45;color:' + tk.cSubM + '}',
'#removerModal .rmSettingsField{display:block;margin:0 0 10px;padding:12px;border:1px solid ' + tk.bSubS + ';border-radius:8px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.55) inset}',
'#removerModal .rmSettingsField:last-child{margin-bottom:0}',
'#removerModal .rmSettingsFieldLabel{display:block;font-size:13px;font-weight:700;line-height:1.35;margin:0 0 8px;color:' + tk.cBase + '}',
'#removerModal .rmSettingsFieldControl{display:block;min-width:0}',
'#removerModal .rmSettingsFieldControl input{margin-bottom:0!important}',
'#removerModal .rmSettingsFieldControl input[type="text"]{min-height:38px;border-radius:6px}',
'#removerModal .rmSettingsFieldHint{margin-top:8px;font-size:12px;line-height:1.55;color:' + tk.cSubM + ';overflow-wrap:anywhere}',
'#removerModal .rmSettingsChecks{display:flex;flex-direction:column;gap:8px}',
'#removerModal .rmSettingsCheck{display:inline-flex;align-items:flex-start;gap:8px;font-size:14px;line-height:1.45;color:' + tk.cBase + '}',
'#removerModal .rmSettingsCheck input[type="checkbox"]{margin:3px 0 0;flex-shrink:0}',
'#removerModal .rmSettingsMenuPresetWrap{margin-top:10px}',
'#removerModal .rmSettingsMenuPresetLabel{margin:0 0 6px;font-size:12px;font-weight:700;line-height:1.35;color:' + tk.cSubM + '}',
'#removerModal .rmSegmentedBar{display:inline-flex;align-items:center;flex-wrap:wrap;gap:6px;margin-top:0;padding:0;border:0;border-radius:0;background:transparent;max-width:100%;box-sizing:border-box;box-shadow:none}',
'#removerModal .rmSettingsMenuPresetBar{display:inline-flex;align-items:center;flex-wrap:wrap;gap:6px;margin-top:0;padding:0;border:0;border-radius:0;background:transparent;max-width:100%;box-sizing:border-box;box-shadow:none}',
'#removerModal .rmSegmentedBtn,#removerModal .rmSettingsMenuPresetBtn{padding:5px 12px;border:1px solid ' + tk.bSub + ';border-radius:999px;background:' + pillBg + ';color:' + tk.cBase + ';font-size:12px;font-weight:600;line-height:1.35;cursor:pointer;box-shadow:' + pillShadow + '}',
'#removerModal .rmSegmentedBtn.is-active,#removerModal .rmSettingsMenuPresetBtn.is-active{background:' + tk.bgProg + ';border-color:' + tk.bProg + ';color:' + tk.cInv + ';box-shadow:' + activePillShadow + '}',
'#removerModal .rmKbuReasonPill{display:flex;align-items:stretch;width:100%;padding:0!important;overflow:hidden;border:1px solid ' + tk.bSub + ';border-radius:999px;box-sizing:border-box;cursor:pointer;text-align:left;font-size:12px;font-weight:400;line-height:1.3;box-shadow:' + pillShadow + '}',
'#removerModal .rmKbuReasonPill.is-active{box-shadow:' + activePillShadow + '!important}',
'#removerModal .rmKbuReasonNum{display:inline-flex;align-items:center;justify-content:center;min-width:44px;padding:4px 10px;border-right:1px solid ' + tk.bSub + ';box-sizing:border-box;font-weight:700;text-align:center;flex:0 0 auto}',
'#removerModal .rmKbuReasonPill.is-active .rmKbuReasonNum{border-right-color:rgba(255,255,255,.55)!important}',
'#removerModal .rmKbuReasonText{display:block;min-width:0;flex:1;padding:4px 11px 4px 12px;font-weight:400;overflow-wrap:anywhere}',
'#removerModal.rmModalSettings{border:1px solid ' + tk.bSub + '!important;background:' + tk.bgBase + '!important;border-radius:12px!important;box-shadow:0 14px 32px rgba(0,0,0,.08),0 1px 0 rgba(255,255,255,.78) inset!important}',
'#removerModal.rmModalSettings #removerModalHeaderBar{margin-bottom:14px;padding-bottom:10px;border-bottom:2px solid ' + tk.bSub + '}',
'#removerModal.rmModalSettings #removerModalSubtitle{margin:-2px 0 12px!important;color:' + tk.cSubM + '!important}',
'#removerModal.rmModalSettings #rmSettingsForm{gap:18px}',
'#removerModal.rmModalSettings .rmSettingsLead{margin:0;padding:0 2px;color:' + tk.cSubM + '}',
'#removerModal.rmModalSettings .rmSettingsSection{padding:16px 18px;border:1px solid ' + tk.bSub + ';border-radius:12px;background:linear-gradient(180deg,' + tk.bgNSub + ' 0%,' + tk.bgBase + ' 100%);box-shadow:0 1px 0 rgba(255,255,255,.82) inset,0 8px 18px rgba(0,0,0,.035)}',
'#removerModal.rmModalSettings .rmSettingsSectionHeader{margin:0 0 6px;padding-bottom:0;border-bottom:0}',
'#removerModal.rmModalSettings .rmSettingsSectionTitle{font-size:16px;line-height:1.3}',
'#removerModal.rmModalSettings .rmSettingsSectionDescription{margin-top:5px;max-width:none}',
'#removerModal.rmModalSettings .rmSettingsField{margin:14px 0 0;padding:12px 0 0;border:0;border-top:1px solid ' + tk.bSubS + ';border-radius:0;background:transparent;box-shadow:none}',
'#removerModal.rmModalSettings .rmSettingsField:first-child{margin-top:0;padding-top:0;border-top:0}',
'#removerModal.rmModalSettings .rmSettingsFieldLabel{margin:0 0 6px}',
'#removerModal.rmModalSettings .rmSettingsFieldControl input[type="text"]{min-height:40px;padding:8px 10px;border:1px solid ' + tk.bSub + ';border-radius:8px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.65) inset}',
'#removerModal.rmModalSettings .rmSettingsFieldControl input[type="text"]:focus{border-color:' + tk.bProg + ';box-shadow:0 0 0 1px rgba(51,102,204,.16);outline:none}',
'#removerModal.rmModalSettings .rmSettingsFieldHint{margin-top:6px;max-width:none}',
'#removerModal.rmModalSettings .rmSettingsChecks{gap:10px}',
'#removerModal.rmModalSettings .rmSettingsCheck{padding:4px 0}',
'#removerModal.rmModalSettings .rmSettingsMenuPresetWrap{margin-top:12px;padding-top:10px;border-top:1px dashed ' + tk.bSubS + '}',
'#removerModal.rmModalSettings .rmSettingsHintList{width:100%;max-width:100%;box-sizing:border-box;margin-top:10px;padding:10px 12px;border:1px solid ' + tk.bSubS + ';border-radius:8px;background:' + tk.bgBase + '}',
'#removerModal.rmModalSettings .rmSettingsHintRow{line-height:1.55}',
'#removerModal.rmModalSettings .rmSettingsHintBadge{background:' + tk.bgNSub + '}',
'#removerModal.rmModalSettings .rmQuickPhraseEditor{padding-top:2px}',
'#removerModal.rmModalSettings .rmQuickPhraseMeta{min-height:18px}',
'#removerModal.rmModalSettings #rmSettingsSignaturePreviewCode{display:inline-block;padding:2px 8px;border:1px solid ' + tk.bSubS + ';border-radius:999px;background:' + tk.bgBase + ';box-shadow:0 1px 0 rgba(255,255,255,.65) inset}',
'#removerModal.rmModalSettings #rmFooterActionButtons{position:relative;flex:0 0 auto!important;display:flex!important;align-items:center!important;justify-content:flex-end!important;gap:6px!important;margin-left:auto!important;max-width:100%!important}',
'#removerModal.rmModalSettings #rmSettingsActionButtonsRow{display:flex;align-items:center;justify-content:flex-end;gap:6px;flex-wrap:nowrap}',
'#removerModal.rmModalSettings #rmSettingsUnsavedHint{display:none;position:absolute;top:100%;right:0;width:auto;box-sizing:border-box;margin:4px 0 0;color:' + tk.cSubM + ';opacity:.78;font-size:12px;line-height:1.35;text-align:right;white-space:nowrap}',
'#removerModal .rmProtectControlGroup{margin-top:12px;padding:8px 10px;border:1px solid ' + tk.bSubS + ';border-radius:10px;background:linear-gradient(180deg,' + tk.bgBase + ' 0%,' + tk.bgNSub + ' 100%);box-sizing:border-box}',
'#removerModal .rmProtectControlLabel{margin:0 0 6px;font-size:12px;font-weight:700;line-height:1.35;color:' + tk.cSubM + '}',
'#removerModal .rmProtectControlGroup .rmSegmentedBar{gap:4px;padding:0;border:0;box-shadow:none}',
'#removerModal .rmProtectControlGroup .rmSegmentedBtn{padding:4px 10px;font-size:12px}',
'#removerModal .rmTransferPanel{margin-top:10px;padding:0;border:0;background:transparent;box-sizing:border-box;box-shadow:none}',
'#removerModal .rmTransferGrid{display:grid;grid-template-columns:max-content max-content;column-gap:10px;row-gap:6px;align-items:start;justify-content:start}',
'#removerModal .rmTransferGrid .rmSegmentedBar{justify-self:start}',
'#removerModal .rmTransferHintRow{grid-column:1 / -1;min-height:0}',
'#removerModal .rmTransferPanel .rmSegmentedBar{gap:4px;padding:0;border:0;box-shadow:none}',
'#removerModal .rmTransferPanel .rmSegmentedBtn{padding:6px 14px;font-size:12px;font-weight:700}',
'#removerModal #rmTransferModeGroup{gap:0}',
'#removerModal #rmTransferModeGroup .rmSegmentedBtn:first-child{border-top-right-radius:0;border-bottom-right-radius:0}',
'#removerModal #rmTransferModeGroup .rmSegmentedBtn + .rmSegmentedBtn{margin-left:-1px;border-top-left-radius:0;border-bottom-left-radius:0}',
'#removerModal.rmCompactContent .rmMultiPageRow{flex-wrap:wrap!important;gap:6px}',
'#removerModal.rmCompactContent .rmMultiPageRow .rmMultiPageInput,#removerModal.rmCompactContent .rmMultiPageRow .rmMultiPageTargetInput{flex:1 1 100%!important;width:100%!important}',
'#removerModal.rmCompactContent .rmMultiPageRow .rmMultiPageCommentToggle,#removerModal.rmCompactContent .rmMultiPageRow .rmAddMultiPage,#removerModal.rmCompactContent .rmMultiPageRow .rmAddMultiRenameVariant,#removerModal.rmCompactContent .rmMultiPageRow .rmRemoveInput{margin-left:0!important}',
'#removerModal.rmCompactContent .rmTransferGrid{grid-template-columns:minmax(0,1fr);gap:10px}',
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(1){order:1}',
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(2){order:2}',
'#removerModal.rmCompactContent .rmTransferGrid > :nth-child(3){order:3}',
'#removerModal.rmCompactContent #rmTransferModeGroup{flex-direction:column;align-items:flex-start;gap:6px}',
'#removerModal.rmCompactContent #rmTransferModeGroup .rmSegmentedBtn{margin-left:0!important;border-radius:999px!important}',
'#removerModal.rmCompactContent .rmTransferHintRow{grid-column:auto}',
'#removerModal #rmProtectTextBlock{margin-top:14px}',
'#removerModal #rmSettingsMenuTitle:disabled{background:' + tk.bgDis + '!important;border-color:' + tk.bDis + '!important;color:' + tk.cDis + '!important;-webkit-text-fill-color:' + tk.cDis + ';opacity:1;cursor:not-allowed;box-shadow:none!important}',
'#removerModal .rmSettingsHintList{display:flex;flex-direction:column;gap:4px;margin-top:8px}',
'#removerModal .rmSettingsHintRow{font-size:12px;line-height:1.5;color:' + tk.cSubM + '}',
'#removerModal .rmSettingsHintBadge{display:inline-block;margin-right:6px;padding:1px 6px;border:1px solid ' + tk.bSubS + ';border-radius:999px;background:' + tk.bgBase + ';font-size:11px;font-weight:700;color:' + tk.cSubM + ';vertical-align:baseline}',
'#removerModal .rmQuickPhraseEditor{display:flex;flex-direction:column;gap:10px}',
'#removerModal .rmQuickPhraseList,#removerModal .rmQuickPhrasesPanel{display:flex;flex-wrap:wrap;gap:8px;align-items:flex-start}',
'#removerModal .rmQuickPhraseChip{position:relative;display:inline-flex;align-items:center;gap:4px;max-width:100%;padding:2px 4px 2px 10px;border:1px solid ' + tk.bSub + ';border-radius:999px;background:' + pillBg + ';box-shadow:' + pillShadow + ';transition:border-color .12s,box-shadow .12s,opacity .12s;overflow:visible}',
'#removerModal .rmQuickPhraseChip.is-editing{opacity:.42;border-style:dashed}',
'#removerModal .rmQuickPhraseChip.is-dragging{opacity:.65}',
'#removerModal .rmQuickPhraseChip.is-drop-before::before,#removerModal .rmQuickPhraseChip.is-drop-after::after{content:"";position:absolute;top:50%;width:3px;height:24px;border-radius:999px;background:' + tk.cProg + ';box-shadow:0 0 0 1px rgba(51,102,204,.08)}',
'#removerModal .rmQuickPhraseChip.is-drop-before::before{left:-4px;transform:translate(-50%,-50%)}',
'#removerModal .rmQuickPhraseChip.is-drop-after::after{right:-4px;transform:translate(50%,-50%)}',
'#removerModal .rmQuickPhraseEditBtn{max-width:100%;padding:3px 0;border:0;background:transparent;color:' + tk.cBase + ';font-size:13px;line-height:1.35;cursor:pointer;text-align:left;white-space:normal}',
'#removerModal .rmQuickPhraseRemoveBtn{width:24px;height:24px;padding:0;border:0;border-radius:999px;background:transparent;color:' + tk.cSubM + ';font-size:18px;line-height:1;cursor:pointer;flex-shrink:0}',
'#removerModal .rmQuickPhraseRemoveBtn:hover{background:' + tk.bgN + ';color:' + tk.cBase + '}',
'#removerModal #rmSettingsQuickPhraseInput.is-editing{border-color:' + tk.bProg + ';box-shadow:0 0 0 1px rgba(51,102,204,.12)}',
'#removerModal .rmQuickPhraseMeta{font-size:12px;line-height:1.45;color:' + tk.cSubM + '}',
'#removerModal .rmQuickPhraseEmpty{padding:2px 0;font-size:12px;line-height:1.45;color:' + tk.cSubM + '}',
'#removerModal .rmQuickPhrasesPanel{margin-top:8px}',
'#removerModal .rmQuickPhraseActionBtn{padding:5px 12px;border:1px solid ' + tk.bSub + ';border-radius:999px;background:' + pillBg + ';color:' + tk.cBase + ';font-size:12px;font-weight:600;line-height:1.35;cursor:pointer;box-shadow:' + pillShadow + '}',
'#removerModal .rmQuickPhraseActionBtn:hover{border-color:' + tk.bProg + ';color:' + tk.cProg + '}',
'@media (max-width:' + sz.mobileBp + 'px){',
'#removerModal button{white-space:normal!important}',
'#removerModal #rmFooterButtons{align-items:flex-start!important}',
'#removerModal #rmFooterCheckboxes,#removerModal #rmFooterActionButtons{width:100%!important;max-width:100%!important;margin-left:0!important}',
'#removerModal .rmSettingsSection{padding:12px 13px}',
'#removerModal .rmSettingsField{padding:10px}',
'#removerModal.rmModalSettings #rmSettingsUnsavedHint{max-width:100%;margin:4px 0 0;text-align:right;white-space:normal}',
'#removerModal .rmTransferPanel{padding:0}',
'#removerModal .rmTransferGrid{grid-template-columns:minmax(0,1fr);gap:10px}',
'#removerModal .rmTransferHintRow{grid-column:auto}',
'#removerModal .rmQuickPhraseChip{max-width:100%}',
'}'
].join('');
var style = document.createElement('style');
style.id = 'removerModalDynamicStyles';
style.textContent = css;
document.head.appendChild(style);
}
function applyV2022Layout($modal, explicitWidth) {
if (!isVector22) return;
var css = { 'max-width': '100%', 'box-sizing': 'border-box', 'overflow-wrap': 'anywhere' };
if (typeof explicitWidth === 'number') css['max-width'] = explicitWidth + 'px';
$modal.css(css);
}
function getPageUrl(pageTitle) {
return (mw.util && typeof mw.util.getUrl === 'function')
? mw.util.getUrl(pageTitle)
: '/wiki/' + encodeURIComponent((pageTitle || '').replace(/ /g, '_'));
}
function getPageUrlWithFragment(pageTitle, fragment) {
var url = getPageUrl(pageTitle);
var frag = normalizeSectionForLink(fragment);
if (frag) url += '#' + ((mw.util && mw.util.escapeIdForLink) ? mw.util.escapeIdForLink(frag) : frag.replace(/\s+/g, '_'));
return url;
}
function buildStatusPageLink(pageName) {
var title = normTitle(pageName);
return '<a href="' + escapeHtml(getPageUrl(title)) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' +
escapeHtml(title) + '</a>';
}
function buildQuotedStatusPageLink(pageName) {
return '«' + buildStatusPageLink(pageName) + '»';
}
function buildTemplatePageLink(templateName, labelText) {
var name = String(templateName || '')
.replace(/^(?:safe)?subst\s*:\s*/i, '')
.replace(RE_TEMPLATE_NS, '')
.trim();
var ns = getNamespaceLabel(10, 'Шаблон');
if (!name) return escapeHtml(labelText);
return '<a href="' + escapeHtml(getPageUrl(ns + ':' + name)) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' +
escapeHtml(labelText) + '</a>';
}
function renderTemplateLinksInText(text) {
var source = String(text || '');
var out = '';
var lastIndex = 0;
source.replace(/\{\{\s*([^{}|]+)(?:\|[^{}]*)?\}\}/g, function (match, templateName, offset) {
out += escapeHtml(source.slice(lastIndex, offset));
out += buildTemplatePageLink(templateName, match);
lastIndex = offset + match.length;
return match;
});
return out + escapeHtml(source.slice(lastIndex));
}
function buildHeaderIconButtonHtml(id, title, label, text) {
return joinHtml([
'<button id="', id, '" type="button" title="', escapeHtml(title), '" aria-label="', escapeHtml(label || title), '" ',
'style="', stHeaderIconBtn, '">', text || '', '</button>'
]);
}
function createModal(opts) {
if (typeof opts === 'string') opts = { title: opts };
var layout = getModalLayout();
resetModalObservers();
ensureModalStyles();
if (isVector22) $('#content').css('min-width', '');
$('#removerModal').remove();
var subtitleHtml = '';
var subtitleStyle = 'margin:-4px 0 8px;font-size:12px!important;color:' + tk.cSubM + ';line-height:1.35!important;font-weight:400!important;';
var subtitleLinkStyle = 'font-size:inherit!important;line-height:inherit!important;font-weight:inherit!important;';
if (opts.subtitleHtml) {
subtitleHtml = joinHtml([
'<div id="removerModalSubtitle" style="', subtitleStyle, '">',
opts.subtitleHtml,
'</div>'
]);
} else if (opts.subtitlePage) {
subtitleHtml = joinHtml([
'<div id="removerModalSubtitle" style="', subtitleStyle, '">',
opts.subtitleLabel || 'Текущий день',
': <a href="', getPageUrl(opts.subtitlePage), '" target="_blank" rel="noopener noreferrer" class="removerModalLink" style="', subtitleLinkStyle, '">',
normTitle(opts.subtitlePage),
'</a></div>'
]);
}
var settingsButtonHtml = opts.showSettingsButton === false ? '' :
buildHeaderIconButtonHtml('removerSettingsTrigger', 'Конфигурация', 'Конфигурация', '⚙');
var display = opts.inline ? 'inline-block' : 'block';
var modalMargin = opts.inline ? '1em 0' : (layout.shouldCenter ? '1em auto' : '1em 0');
var inlineLayoutStyle = opts.inline ? ';justify-self:start;align-self:start;width:fit-content;' : '';
var modalStyle = joinHtml([
'position:relative;padding:1.5em;margin:', modalMargin, ';display:', display, ';',
'border:', stStyles.border, ';background:', stStyles.background,
';border-radius:', stStyles.borderRadius, ';box-shadow:', stStyles.boxShadow,
';max-width:100%;box-sizing:border-box;overflow-wrap:anywhere;', inlineLayoutStyle
]);
var headerStyle = 'display:flex;align-items:center;gap:10px;margin-bottom:10px;padding-bottom:6px;border-bottom:1px solid ' + tk.bSubS + ';';
var titleStyle = 'color:' + stStyles.headerColor + ';margin:0;padding:0;border:0;display:block;font-size:1.3em;font-weight:400;line-height:1.25;flex:1 1 auto;min-width:0;';
$('#content').prepend(joinHtml([
'<div id="removerModal" style="', modalStyle, '">',
'<div id="removerModalHeaderBar" style="', headerStyle, '">',
'<h1 id="removerModalTitle" style="', titleStyle, '"><span id="removerModalTitleText">', opts.title, '</span></h1>',
settingsButtonHtml,
'</div>',
subtitleHtml,
'<div id="removerModalContent"></div>',
'<div id="removerModalFooter" style="margin-top:15px;"></div>',
'</div>'
]));
var $modal = $('#removerModal');
if (opts.width === 'compact') $modal.css({ width: layout.defaultOuterWidth + 'px', 'max-width': '100%', 'box-sizing': 'border-box' });
else applyV2022Layout($modal);
$('#removerSettingsTrigger').off('click').on('click', function () {
openSettings();
});
}
function buildFooterCheckboxHtml(name, checked, label) {
return joinHtml([
'<label style="', stFooterCheckLabel, '">',
'<input name="', name, '" type="checkbox" style="margin:2px 0 0;flex-shrink:0;" ',
checked ? 'checked' : '',
'>',
label,
'</label>'
]);
}
function buildFooterActionsHtml(buttonsHtml) {
return '<div id="rmFooterActionButtons" style="' + stFooterActions + '">' + buttonsHtml + '</div>';
}
function renderModalFooter(mode, options) {
var opts = options || {};
$('#removerModalFooter').css('width', '');
if (mode === 'submit') {
var showCb = opts.showCheckbox !== false;
var showSub = opts.showSubscribe === true;
var ns = mwCfg.wgNamespaceNumber;
var notifyLabel = ns === 0 ? 'Оповестить создателя статьи'
: (ns === 10 || ns === 11) ? 'Оповестить создателя шаблона'
: (ns === 14 || ns === 15) ? 'Оповестить создателя категории'
: 'Оповестить создателя страницы';
var cbInlineHtml = '';
if (showSub || showCb) {
cbInlineHtml = joinHtml([
'<div id="rmFooterCheckboxes" style="', stFooterChecks, '">',
showSub ? buildFooterCheckboxHtml('rmSubscribe', setSubscribe, 'Подписаться на номинацию') : '',
showCb ? buildFooterCheckboxHtml('rmUAlert', setAlert, notifyLabel) : '',
'</div>'
]);
}
$('#removerModalFooter').html(joinHtml([
'<div id="rmFooterButtons" style="', stFooterWrap, 'justify-content:', cbInlineHtml ? 'space-between' : 'flex-end', ';">',
cbInlineHtml,
buildFooterActionsHtml(joinHtml([
'<button id="removerCancel" style="', stCancel, '">Отмена</button>',
'<button id="removerSubmit" style="', stSubmit, '">', opts.submitText || 'ОК', '</button>'
])),
'</div>'
]));
$('#removerCancel').click(function () { closeModal(); });
$('#removerSubmit').data('rmSubmitInProgress', false).click(function () {
if ($(this).data('rmSubmitInProgress')) return;
$(this).removeClass('rmSubmitError').css({ background: '', 'border-color': '', color: '' });
isError = false;
if (!opts.preserveLogOnSubmit) {
$('#rmLogBox').empty();
logStatusSeq = 0;
}
if (showCb) { setAlert = $('[name="rmUAlert"]').is(':checked'); state.setAlert = setAlert; }
if (showSub) { setSubscribe = $('[name="rmSubscribe"]').is(':checked'); state.setSubscribe = setSubscribe; }
$(this).data('rmSubmitInProgress', true).prop('disabled', true);
var submitResult;
try { submitResult = opts.onSubmit(); } catch (ex) { unlockModalSubmit(); throw ex; }
if (submitResult === false) unlockModalSubmit();
});
$(window).off('keydown.remover').on('keydown.remover', function (e) {
if (e.ctrlKey && e.keyCode === 13) $('#removerSubmit').click();
});
} else if (mode === 'reload') {
var newBtns = buildFooterActionsHtml(joinHtml([
'<button id="removerCancel" style="', stCancel, '">Закрыть</button>',
'<button id="removerReload" style="', stReload, '">', opts.reloadText || 'Обновить страницу', '</button>'
]));
$('#rmFooterCheckboxes').remove();
var $btns = $('#rmFooterButtons');
if ($btns.length) {
$btns.css({ 'justify-content': 'flex-end' }).html(newBtns);
} else {
$('#removerModalFooter').append(joinHtml([
'<div id="rmFooterButtons" style="', stFooterWrap, 'justify-content:flex-end;">',
newBtns,
'</div>'
]));
}
$('#removerCancel').click(function () { closeModal(); });
$('#removerReload').click(function () { location.reload(); });
$(window).off('keydown.remover').on('keydown.remover', function (e) {
if (e.keyCode === 27) $('#removerCancel').click();
if (e.ctrlKey && e.keyCode === 13) $('#removerReload').click();
});
if (shouldAutoReloadAfterAction()) setTimeout(function () { $('#removerReload').trigger('click'); }, 0);
} else { // 'close'
$('#removerModalFooter').html(joinHtml([
'<div style="display:flex;justify-content:flex-end;align-items:center;">',
'<button id="removerCancel" style="', stCancel, 'margin-right:0;">', opts.closeText || 'Закрыть', '</button>',
'</div>'
]));
$('#removerCancel').click(function () { closeModal(); });
$(window).off('keydown.remover').on('keydown.remover', function (e) {
if (e.keyCode === 27) $('#removerCancel').click();
});
}
}
function unlockModalSubmit() {
$('#removerSubmit').data('rmSubmitInProgress', false).prop('disabled', false);
}
function markSubmitError() {
isError = true;
var errColor = '#d73333';
$('#removerSubmit').data('rmSubmitInProgress', false).prop('disabled', false)
.addClass('rmSubmitError').css({ background: errColor, 'border-color': errColor, color: '#fff' });
}
// ─── UI: статус и ссылки ─────────────────────────────────────────────────
function startProcessing() {
if ($('#rmLogBox').length) return;
$('#removerModal').append(
'<div id="rmLogBox" style="margin-top:12px;padding-top:10px;border-top:1px solid ' + tk.bSubS + ';line-height:1.5;overflow-wrap:anywhere;word-break:break-word;box-sizing:border-box;"></div>'
);
syncLinkWidths();
}
function logStatus(message, error, opts) {
var o = opts || {};
if (o.trackError !== false && error && error.code) isError = true;
var $box = $('#rmLogBox');
if (!$box.length) { startProcessing(); $box = $('#rmLogBox'); }
var statusId = o.statusId || ('rm-status-' + (++logStatusSeq));
var $row = $box.find('[data-rm-status-id="' + statusId + '"]');
if (!$row.length) {
$row = $('<div data-rm-status-id="' + statusId + '" style="margin-top:4px;line-height:1.4;"></div>');
$box.append($row);
}
var html;
if (error) {
var errText = error.code
? '<span class="error"><small>' + escapeHtml(formatLogErrorCode(error.code)) + ': ' + escapeHtml(String(error.info || '')) + '</small></span>'
: escapeHtml(String(error));
html = '<span style="color:' + tk.cDang + ';margin-right:4px;">✕</span>' + message + ' — ' + errText;
} else if (o.pending) {
html = '<span style="color:' + tk.cSubM + ';">' + message + '</span>';
} else {
html = '<span style="color:' + tk.bgSucc + ';margin-right:4px;">✓</span><span style="color:' + tk.cSubM + ';">' + message + '</span>';
}
$row.html(html);
return statusId;
}
function formatLogErrorCode(code) {
var value = String(code || '');
return value.toLowerCase() === 'error' ? 'Ошибка' : value;
}
function logPageEdit(pageName, error, opts) {
logStatus('Правка страницы ' + buildQuotedStatusPageLink(pageName) + '.', error, opts);
}
function syncLinkWidths() {
var $box = $('#rmLogBox');
if (!$box.length) return;
var taW = $('#rmMsg,#nominationReason,#rmReportText').first().outerWidth() || 0;
$box.css({ width: taW ? taW + 'px' : '', 'max-width': '100%' });
}
function appendNominationLink(pageTitle, sectionTitle) {
if (!pageTitle) return;
var url = getPageUrl(pageTitle);
var frag = normalizeSectionForLink(sectionTitle);
if (frag) url += '#' + ((mw.util && mw.util.escapeIdForLink) ? mw.util.escapeIdForLink(frag) : frag.replace(/\s+/g, '_'));
var label = normTitle(frag ? pageTitle + '#' + frag : pageTitle);
var $target = $('#rmLogBox').length ? $('#rmLogBox') : $('#removerModalContent');
$target.append(
'<div style="margin-top:4px;line-height:1.4;word-break:break-word;overflow-wrap:anywhere;display:flex;align-items:baseline;gap:5px;">' +
'<span style="color:' + tk.bgSucc + ';font-size:14px;flex-shrink:0;">✓</span>' +
'<a href="' + escapeHtml(url) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' + escapeHtml(label) + '</a></div>'
);
}
function getCurrentSettings() {
return normalizeRemoverSettings(state.settings || settingsDefaults);
}
function shouldAutoReloadAfterAction() {
return !!getCurrentSettings().autoReloadAfterAction;
}
function maybeOpenNominationDiscussionInNewTab(nominationInfo) {
if (!getCurrentSettings().openNominationDiscussionInNewTab || !nominationInfo || !nominationInfo.pageTitle) return;
window.open(getPageUrlWithFragment(nominationInfo.pageTitle, nominationInfo.sectionTitle), '_blank', 'noopener,noreferrer');
}
// ─── UI-строители ────────────────────────────────────────────────────────
function buildInfoBoxHtml(mainText, detailsText, isErr) {
var cls = isErr ? ' class="error"' : '';
return joinHtml([
'<div class="rmInfoBox">',
'<p', cls, ' style="margin:0', detailsText ? ' 0 6px' : '', ';">', mainText, '</p>',
detailsText ? '<p style="margin:0;color:' + tk.cSubM + ';">' + detailsText + '</p>' : '',
'</div>'
]);
}
function buildActionsHtml(actions, inputName, listId) {
var actionItemsHtml = actions.map(function (a, i) {
var meta = a.description || (a.talkNotice ? 'С добавлением {{' + (a.talkTemplate || a.resultTemplate || 'шаблон') + '}} на СО.' : '');
var tagHtml = a.tag
? '<span style="display:inline-block;font-size:13px;font-weight:600;padding:2px 7px;border-radius:3px;background:' + tk.bgN + ';color:' + tk.cSubM + ';margin-right:8px;white-space:nowrap;vertical-align:middle;">' + escapeHtml(a.tag) + '</span>'
: '';
return joinHtml([
'<label class="rmActionItem">',
'<span class="rmActionMain">',
'<input type="radio" name="', inputName, '" value="', a.id, '" ', i === 0 ? 'checked' : '', '>',
tagHtml,
'<span>', a.label, '</span>',
'</span>',
meta ? '<span class="rmActionMeta">' + renderTemplateLinksInText(meta) + '</span>' : '',
a.warning ? '<strong class="rmActionWarning">' + escapeHtml(a.warning) + '</strong>' : '',
'</label>'
]);
}).join('');
return joinHtml([
'<div style="margin:0 0 8px;color:', tk.cSubM, ';font-size:13px;">Обнаружены открытые номинации:</div>',
'<div', listId ? ' id="' + listId + '"' : '', ' class="rmActionList">',
actionItemsHtml,
'</div>'
]);
}
function syncActionWarnings(inputName, listId) {
var $root = listId ? $('#' + listId) : $('#removerModal');
var $selected = $root.find('input[name="' + inputName + '"]:checked').closest('.rmActionItem');
$root.find('.rmActionWarning').hide();
$selected.find('.rmActionWarning').show();
}
function buildNestedCommentFieldsHtml(opts) {
var options = opts || {};
var wrapId = options.wrapId || '';
var textareaId = options.textareaId || '';
var textareaClass = options.textareaClass ? ' ' + options.textareaClass : '';
var textareaStyleExtra = options.textareaStyleExtra || '';
var wrapStyleExtra = options.wrapStyleExtra || '';
var placeholder = options.placeholder || 'Комментарий (необязательно)';
var beforeHtml = options.beforeHtml || '';
var marginTop = options.marginTop || '6px';
var minHeight = parseInt(options.minHeight, 10) || 90;
var isEmbedded = !!options.embedded;
var wrapClass = isEmbedded ? '' : (' class="' + RESIZE_CLASS + '"');
var wrapStyle = 'display:none;margin-top:' + marginTop + ';max-width:100%;box-sizing:border-box;';
if (isEmbedded) {
wrapStyle += 'padding:0;border:0;background:transparent;';
} else {
wrapStyle += 'padding:8px 10px;border:1px solid ' + tk.bSubS + ';border-radius:6px;background:' + tk.bgNSub + ';';
}
wrapStyle += wrapStyleExtra;
return joinHtml([
'<div id="', wrapId, '"', wrapClass, ' style="', wrapStyle, '">',
beforeHtml,
'<textarea id="', textareaId, '" class="rmNestedCommentInput', textareaClass,
'" placeholder="', escapeHtml(placeholder), '" style="', stInputFull,
'min-height:', minHeight, 'px;resize:both;margin-bottom:6px;', textareaStyleExtra, '"></textarea>',
buildQuickPhrasesPanelHtml(textareaId),
'</div>'
]);
}
function buildConditionalRetFieldsHtml() {
return buildNestedCommentFieldsHtml({
wrapId: 'rmCloseConditionalWrap',
textareaId: 'rmCloseConditionalReason',
placeholder: 'Условие / пояснение (необязательно)',
marginTop: '8px',
minHeight: 90,
beforeHtml: '<input id="rmCloseConditionalDeadline" type="text" placeholder="Срок доработки: 2026-05-31" style="' + stInputFull + 'margin-bottom:6px;">'
});
}
function buildAddMultiPageButtonHtml(options) {
var opts = options || {};
var title = opts.addTitle || 'Мультиноминация: добавить страницу';
return buildSquareAddButtonHtml('', title, 'rmAddMultiPage rmAddPageBtn');
}
function buildSquareAddButtonHtml(id, title, className, symbol) {
var idAttr = id ? ' id="' + id + '"' : '';
var clsAttr = className ? ' class="' + className + '"' : '';
var label = title || 'Добавить';
return '<button' + idAttr + ' type="button"' + clsAttr + ' title="' + escapeHtml(label) + '" aria-label="' + escapeHtml(label) + '" style="' + stRemoveBtn + '">' + escapeHtml(symbol || '+') + '</button>';
}
function leftControlStyle(style) {
return style.replace('margin-left:' + inlineControlGap + 'px;', 'margin-left:0;margin-right:' + inlineControlGap + 'px;');
}
function buildLeftSquareAddButtonHtml(id, title, className, symbol) {
return buildSquareAddButtonHtml(id, title, className, symbol).replace(stRemoveBtn, leftControlStyle(stRemoveBtn));
}
function buildLeftRemoveButtonHtml(className, title) {
var cls = className || 'rmRemoveInput';
var label = title || 'Удалить';
return '<button type="button" class="' + cls + '" style="' + leftControlStyle(stRemoveBtn) + '" title="' + escapeHtml(label) + '" aria-label="' + escapeHtml(label) + '">−</button>';
}
function buildMultiRenameVariantAddButtonHtml() {
return buildLeftSquareAddButtonHtml('', 'Добавить вариант нового заголовка', 'rmAddMultiRenameVariant rmAddVariantBtn rmRenameVariantAddBtn', '⤷');
}
function buildStartMultiPageButtonHtml(title) {
var label = title || 'Мультиноминация: добавить страницу';
return buildSquareAddButtonHtml('', label, 'rmAddMultiPage rmAddPageBtn rmStartMultiPageBtn');
}
function buildMultiPageButtonsHtml(commentWrapId, commentId, options) {
var opts = options || {};
var commentBtnStyle = stToolBtn + (opts.showComment ? '' : 'display:none;');
var commentTitle = opts.commentTitle || 'Добавить комментарий к этой странице';
var commentExpandedTitle = opts.commentExpandedTitle || 'Скрыть комментарий к этой странице';
if (opts.showAdd) return buildAddMultiPageButtonHtml(opts);
return joinHtml([
'<button type="button" class="rmToggleBtn rmMultiPageCommentToggle" data-rm-comment-wrap="', commentWrapId,
'" data-rm-comment-textarea="', commentId,
'" data-rm-comment-title="', escapeHtml(commentTitle),
'" data-rm-comment-expanded-title="', escapeHtml(commentExpandedTitle),
'" aria-label="', escapeHtml(commentTitle), '" title="', escapeHtml(commentTitle), '" aria-expanded="false" style="', commentBtnStyle, '">✎</button>',
'<button type="button" class="rmRemoveInput" style="', stRemoveBtn, '" title="', escapeHtml(opts.removeTitle || 'Убрать страницу из номинации'), '">−</button>'
]);
}
function buildMultiRenameVariantRowHtml(value, options) {
var opts = options || {};
return joinHtml([
'<div class="rmMultiRenameVariantRow" style="', stRow.replace('margin-bottom:6px;', 'margin-bottom:0;'), '">',
buildLeftRemoveButtonHtml('rmRemoveMultiRenameVariant', 'Убрать вариант нового заголовка'),
'<input type="text" class="rmMultiRenameVariantInput" placeholder="', escapeHtml(opts.placeholder || 'Дополнительный вариант нового заголовка'),
'" style="', stInputBox, '"', value ? ' value="' + escapeHtml(value) + '"' : '', '>',
'</div>'
]);
}
function buildMultiPageRowHtml(index, options) {
var opts = options || {};
var pageInputId = 'rmMultiPage' + index;
var commentWrapId = 'rmMultiPageCommentWrap' + index;
var commentId = 'rmMultiPageComment' + index;
var pageValue = opts.pageValue || '';
var pageValueAttr = pageValue ? ' value="' + escapeHtml(pageValue) + '"' : '';
var inputPlaceholder = opts.inputPlaceholder || 'Страница';
var targetInputClass = opts.targetInputClass || '';
var targetInputHtml = '';
var commentPlaceholder = opts.commentPlaceholder || 'Комментарий только для этой страницы (необязательно)';
var commentIndent = opts.targetVariants ? leftNestedControlOffset : '0';
var pageRowStyle = stRow.replace('margin-bottom:6px;', 'margin-bottom:0;');
var blockStyle = 'max-width:100%;box-sizing:border-box;';
var buttonsHtml = buildMultiPageButtonsHtml(commentWrapId, commentId, {
showAdd: !!opts.showAdd,
showComment: !!opts.showComment,
addTitle: opts.addTitle,
removeTitle: opts.removeTitle,
commentTitle: opts.commentTitle,
commentExpandedTitle: opts.commentExpandedTitle
});
if (opts.targetInput) {
targetInputHtml = joinHtml([
'<input id="rmMultiPageTarget', index, '" type="text" placeholder="', escapeHtml(opts.targetPlaceholder || 'Новое название'),
'" class="rmMultiPageTargetInput ', escapeHtml(targetInputClass), '" style="', stInputBox, '">'
]);
}
return joinHtml([
'<div class="rmMultiPageBlock ', RESIZE_CLASS, '" style="', blockStyle, '">',
'<div', opts.rowId ? ' id="' + opts.rowId + '"' : '', ' class="rmMultiPageRow" style="', pageRowStyle, '">',
'<input id="', pageInputId, '" type="text" placeholder="', escapeHtml(inputPlaceholder), '" class="rmMultiPageInput" style="', stInputBox, '"', pageValueAttr, '>',
opts.targetVariants ? '' : targetInputHtml,
buttonsHtml,
'</div>',
opts.targetVariants ? '<div class="rmMultiRenameVariantsContainer"><div class="rmMultiRenamePrimaryTargetRow">' + buildMultiRenameVariantAddButtonHtml() + targetInputHtml + '</div></div>' : '',
buildNestedCommentFieldsHtml({
wrapId: commentWrapId,
textareaId: commentId,
textareaClass: 'rmMultiPageCommentInput',
placeholder: commentPlaceholder,
marginTop: multiNominationGap,
minHeight: 90,
embedded: true,
wrapStyleExtra: 'padding:0 0 0 ' + commentIndent + ';background:transparent;',
textareaStyleExtra: 'border-color:' + tk.bSubS + ';border-radius:4px;background:' + tk.bgBase + ';'
}),
'</div>'
]);
}
function buildSingleRenameBlockHtml(config, startMultiTitle, currentPageName, currentPlaceholder) {
var addLabel = 'Добавить вариант нового заголовка';
var currentValue = currentPageName || '';
return joinHtml([
'<div id="rmSingleRenameBlock" style="display:flex;flex-direction:column;align-items:stretch;gap:0;">',
'<div class="rmInputRow ', RESIZE_CLASS, '" style="', stRow, '">',
'<input id="rmSingleRenameCurrent" type="text" class="rmSingleRenameCurrentInput" placeholder="', escapeHtml(currentPlaceholder || 'Текущее название'), '" style="', stInputBox, '" value="', escapeHtml(currentValue), '">',
buildStartMultiPageButtonHtml(startMultiTitle),
'</div>',
'<div class="rmInputRow ', RESIZE_CLASS, ' rmRenameVariantRow" style="', stRow, '">',
buildLeftSquareAddButtonHtml(config.addBtnId, addLabel.replace(/^\+\s*/, '') || 'Добавить вариант', 'rmAddVariantBtn rmRenameVariantAddBtn', '⤷'),
'<input id="', config.firstId, '" type="text" class="', config.inputClass || 'variantInput', '" placeholder="', config.firstPh || '', '" style="', stInputBox, '">',
'</div>',
'<div id="', config.containerId, '"></div>',
'</div>'
]);
}
function showInfoAndClose(mainText, detailsText, isErr) {
$('#removerModalContent').html(buildInfoBoxHtml(mainText, detailsText || '', isErr || false));
renderModalFooter('close');
}
function getSelectedAction(inputName, actionMap) {
var id = $('[name="' + inputName + '"]:checked').val();
var sel = actionMap[id];
if (!sel) alert('Выберите действие.');
return sel || null;
}
function prependTemplateToNoinclude(text, templateText) {
var source = String(text || '');
var tpl = String(templateText || '').trim();
if (!tpl) return source;
var match = source.match(RE_NOINCLUDE);
if (match) {
var before = source.slice(0, source.indexOf(match[0]));
if (/\S/.test(before)) return '<noinclude>' + tpl + '</noinclude>\n' + source;
var content = String(match[2] || '').replace(/^\n+/, '');
return source.replace(match[0], match[1] + '<noinclude>' + tpl + (content ? '\n' + content : '') + '\n</noinclude>');
}
return '<noinclude>' + tpl + '</noinclude>\n' + source;
}
function buildGeneratedNominationTemplateText(job, pg) {
var tplStr = '';
if (!job) return '';
if (job.opId === 'fRm') {
if (job.kbuTemplateText) return T_OPEN + job.kbuTemplateText + T_CLOSE;
return job.kbuTemplate ? (T_OPEN + job.kbuTemplate + T_CLOSE) : '';
}
if (typeof job.articleTpl !== 'function') return '';
tplStr = job.articleTpl(job.opId === 'mRnm' ? buildRenameTemplateParam(getMultiRenameTarget(job, pg, 'multiRenameTemplateTargets')) : job.tplpar, job.date[0]);
if (job.opId === 'merge' && job.tplpar) {
tplStr = (job.op.nomination.articleTpl)(
('|' + job.tplpar + '|').replace('|' + pg + '|', '|').slice(1, -1),
job.date[0]
);
}
return tplStr ? (T_OPEN + tplStr + T_CLOSE) : '';
}
function applyConflictTemplateResolution(articleText, job, pg, decision) {
var rule = getNominationConflictRule(job);
var generatedTemplate = buildGeneratedNominationTemplateText(job, pg);
var source = String(articleText || '');
if (!generatedTemplate || !decision) return source;
if (decision.templateAction === 'overwrite') {
var cleaned = rule ? stripTemplatesByPattern(source, rule.namePattern).text : source;
return prependTemplateToNoinclude(cleaned, generatedTemplate);
}
if (decision.templateAction === 'prepend') return prependTemplateToNoinclude(source, generatedTemplate);
return source;
}
function inspectMultiNominationConflicts(job, callback) {
var cb = callback || function () {};
var pages = (job && job.multiArticles) ? job.multiArticles.slice() : [];
var conflicts = [];
var statusId = logStatus('Проверяются статьи на наличие уже установленных шаблонов...', null, { pending: true, trackError: false });
if (!pages.length) {
logStatus('Проверка завершена: конфликтов не найдено.', null, { statusId: statusId, trackError: false });
cb(null, conflicts);
return;
}
eachSequential(pages, function (pg, next) {
getText(pg, function (articleText, readErr) {
var conflict;
if (readErr) { next(makeReadError(readErr, 'read_failed', 'Не удалось проверить страницу «' + pg + '».')); return; }
if (articleText === null) { next({ code: 'read_failed', info: 'Страница «' + pg + '» не существует.' }); return; }
conflict = detectNominationConflict(articleText, job);
if (conflict) {
conflicts.push($.extend({ pageName: pg }, conflict));
logStatus('В статье ' + buildQuotedStatusPageLink(pg) + ' обнаружен установленный шаблон <code>' + escapeHtml(conflict.templateDisplay || conflict.templateName || conflict.label || 'шаблон') + '</code>.', null, { trackError: false });
}
next();
});
}, function (err) {
if (err) {
logStatus('Проверка статей.', err, { statusId: statusId });
cb(err);
return;
}
logStatus(
conflicts.length
? 'Проверка завершена: найдены статьи с уже установленными шаблонами.'
: 'Проверка завершена: конфликтов не найдено.',
null,
{ statusId: statusId, trackError: false }
);
cb(null, conflicts);
});
}
function buildNominationConflictResolutionHtml(conflicts) {
return '<div class="rmInfoBox"><p style="margin:0 0 6px;">Найдены статьи, где шаблон уже стоит. Для каждой конфликтующей страницы выберите, что делать со статьёй и с шаблоном.</p>' +
'<p style="margin:0;color:' + tk.cSubM + ';font-size:12px;line-height:1.45;">По умолчанию такие статьи исключаются из новой номинации, а существующий шаблон остаётся без изменений.</p></div>' +
'<div class="rmConflictLead">После выбора нажмите «Продолжить номинирование».</div>' +
'<div id="rmConflictList" class="rmConflictList">' +
conflicts.map(function (conflict, index) {
var pageLink = buildStatusPageLink(conflict.pageName);
return '<div class="rmConflictCard" data-rm-conflict-index="' + index + '">' +
'<input type="hidden" class="rmConflictPageAction" value="skip">' +
'<input type="hidden" class="rmConflictTemplateAction" value="keep">' +
'<div class="rmConflictTitle">' + pageLink + '</div>' +
'<div class="rmConflictMeta">Обнаружен установленный шаблон <code>' + escapeHtml(conflict.templateDisplay || conflict.templateName || conflict.label || 'шаблон') + '</code>.</div>' +
'<div class="rmConflictGroup">' +
'<div class="rmConflictGroupTitle">Действие со статьёй</div>' +
'<div class="rmConflictButtons" data-rm-conflict-group="page">' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice is-active" data-rm-choice-type="page" data-rm-choice="skip" aria-pressed="true">Убрать из номинации</button>' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice" data-rm-choice-type="page" data-rm-choice="keep" aria-pressed="false">Оставить в номинации</button>' +
'</div></div>' +
'<div class="rmConflictGroup">' +
'<div class="rmConflictGroupTitle">Действие с шаблоном</div>' +
'<div class="rmConflictButtons" data-rm-conflict-group="template">' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice is-active" data-rm-choice-type="template" data-rm-choice="keep" aria-pressed="true">Оставить как есть</button>' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice" data-rm-choice-type="template" data-rm-choice="overwrite" aria-pressed="false">Новая дата</button>' +
'<button type="button" class="rmSegmentedBtn rmConflictChoice" data-rm-choice-type="template" data-rm-choice="prepend" aria-pressed="false">Второй сверху</button>' +
'</div>' +
'<div class="rmConflictHint">Если статья исключается из номинации, действие с шаблоном не применяется.</div>' +
'</div></div>';
}).join('') +
'</div>';
}
function updateNominationConflictCardState($card) {
var pageAction = $card.find('.rmConflictPageAction').val() || 'skip';
var disableTemplate = pageAction !== 'keep';
var $templateButtons = $card.find('[data-rm-choice-type="template"]');
$card.toggleClass('is-skip', disableTemplate);
$card.find('[data-rm-conflict-group="template"]').toggleClass('is-disabled', disableTemplate);
$templateButtons.prop('disabled', disableTemplate).toggleClass('is-disabled', disableTemplate);
}
function bindNominationConflictResolutionUi() {
var $content = $('#removerModalContent');
function setChoice($card, type, value) {
var inputClass = type === 'page' ? '.rmConflictPageAction' : '.rmConflictTemplateAction';
$card.find(inputClass).val(value);
$card.find('[data-rm-choice-type="' + type + '"]').each(function () {
var isActive = $(this).data('rmChoice') === value;
$(this).toggleClass('is-active', isActive).attr('aria-pressed', isActive ? 'true' : 'false');
});
updateNominationConflictCardState($card);
}
$content.off('.rmConflictResolution').on('click.rmConflictResolution', '[data-rm-choice-type]', function () {
var $btn = $(this);
var $card = $btn.closest('.rmConflictCard');
if ($btn.prop('disabled')) return;
setChoice($card, $btn.data('rmChoiceType'), $btn.data('rmChoice'));
});
$('#rmConflictList .rmConflictCard').each(function () { updateNominationConflictCardState($(this)); });
}
function collectNominationConflictResolution(conflicts) {
var decisions = {};
(conflicts || []).forEach(function (conflict, index) {
var $card = $('#rmConflictList .rmConflictCard[data-rm-conflict-index="' + index + '"]');
decisions[normTitle(conflict.pageName)] = {
pageAction: $card.find('.rmConflictPageAction').val() || 'skip',
templateAction: $card.find('.rmConflictTemplateAction').val() || 'keep'
};
});
return decisions;
}
function applyNominationConflictResolutionToJob(job, decisions) {
var resultArticles;
var headerText;
if (!job || !job.isMulti) {
job.conflictDecisions = decisions || {};
return { value: job };
}
resultArticles = (job.multiArticles || []).filter(function (pageName) {
var decision = decisions && decisions[normTitle(pageName)];
return !decision || decision.pageAction !== 'skip';
});
if (!resultArticles.length) return { error: 'После исключения конфликтующих статей в номинации не осталось ни одной страницы.' };
job.conflictDecisions = decisions || {};
job.multiArticles = resultArticles.slice();
job.pages = resultArticles.slice().reverse();
if (job.multiRenamePairs && job.multiRenamePairs.length) {
job.multiRenamePairs = job.multiRenamePairs.filter(function (pair) {
return pair && resultArticles.indexOf(pair.pageName) !== -1;
});
}
if (job.multiRenameTargets) {
job.multiRenameTargets = resultArticles.reduce(function (map, pageName) {
map[normTitle(pageName)] = getMultiRenameTarget(job, pageName, 'multiRenameTargets');
return map;
}, {});
}
if (job.multiRenameTemplateTargets) {
job.multiRenameTemplateTargets = resultArticles.reduce(function (map, pageName) {
map[normTitle(pageName)] = getMultiRenameTarget(job, pageName, 'multiRenameTemplateTargets');
return map;
}, {});
}
headerText = String(job.multiHeaderText || '').trim();
job.section = headerText || (job.opId === 'mRnm' ? formatRenameItemsWithAnd(resultArticles, job.multiRenameTargets) : ('[[:' + resultArticles[0] + ']]'));
job.sectionNW = job.section.replace(/\[\[:/g, '').replace(/]]/g, '');
job.msg = job.multiNominationFormat === 'list'
? buildMultiNominationListText(resultArticles, job.multiNominationBody, job.multiArticleComments, job.opId === 'mRnm' ? getMultiRenameDiscussionOptions(job.multiRenameTargets) : null)
: buildMultiNominationText(resultArticles, job.multiNominationBody, job.multiArticleComments, job.opId === 'mRnm'
? getMultiRenameDiscussionOptions(job.multiRenameTargets, { leadingBlankLine: false })
: { leadingBlankLine: false });
job.summary = makeNominationTargetSummary(job.nomPage, job.sectionNW, job.summaryAction);
job.nominationSummary = makeNominationPageSummary(job.sectionNW);
return { value: job };
}
function showNominationConflictResolution(job, conflicts, onContinue) {
resetModalObservers();
$('#removerModalContent').html(buildNominationConflictResolutionHtml(conflicts));
bindNominationConflictResolutionUi();
syncLinkWidths();
renderModalFooter('submit', {
submitText: 'Продолжить номинирование',
showSubscribe: true,
preserveLogOnSubmit: true,
onSubmit: function () {
var decisions = collectNominationConflictResolution(conflicts);
var applied = applyNominationConflictResolutionToJob(job, decisions);
if (applied.error) {
alert(applied.error);
return false;
}
if (typeof onContinue === 'function') onContinue(applied.value);
return true;
}
});
}
function bindTouchTextareaGrip($ta, sync, getMaxWidth, options) {
var opts = options || {};
var minWidth = parseInt(opts.minWidth, 10) || parseInt(sz.taMinW, 10) || 180;
var minHeight = parseInt(opts.minHeight, 10) || parseInt(sz.taMinH, 10) || 100;
var allowWidthResize = opts.allowWidth !== false;
var syncFn = typeof sync === 'function' ? sync : function () {};
var getMaxWidthFn = typeof getMaxWidth === 'function' ? getMaxWidth : function () { return $ta.outerWidth() || minWidth; };
var usePointerEvents = typeof window.PointerEvent === 'function';
var dragState = { active: false, startX: 0, startY: 0, startWidth: 0, startHeight: 0 };
var gripStyle = 'height:20px;box-sizing:border-box;border:1px solid ' + tk.bSub + ';border-top:0;border-radius:0 0 4px 4px;background:' + tk.bgNSub + ';display:flex;align-items:center;justify-content:center;cursor:ns-resize;touch-action:none;user-select:none;-webkit-user-select:none;';
if (opts.gripMarginBottom) gripStyle += 'margin-bottom:' + opts.gripMarginBottom + ';';
var $grip = $('<div data-rm-textarea-grip="1" style="' + gripStyle + '"><span style="display:block;width:42px;height:4px;border-radius:999px;background:' + tk.bSub + ';opacity:.9;"></span></div>');
function getCoord(evt, key) {
var e = evt.originalEvent || evt;
if (e.touches && e.touches.length) return e.touches[0][key];
if (e.changedTouches && e.changedTouches.length) return e.changedTouches[0][key];
return e[key];
}
function stopDrag() {
dragState.active = false;
$(window).off('.rmTaResize');
}
function onDragMove(evt) {
var clientX;
var clientY;
if (!dragState.active) return;
clientX = getCoord(evt, 'clientX');
clientY = getCoord(evt, 'clientY');
if (typeof clientY !== 'number') return;
if (allowWidthResize && typeof clientX === 'number') $ta.css('width', Math.max(minWidth, Math.min(getMaxWidthFn(), dragState.startWidth + (clientX - dragState.startX))) + 'px');
$ta.css('height', Math.max(minHeight, dragState.startHeight + (clientY - dragState.startY)) + 'px');
syncFn();
if (evt.preventDefault) evt.preventDefault();
}
function startDrag(evt) {
var clientY = getCoord(evt, 'clientY');
if (typeof clientY !== 'number') return;
dragState.active = true;
dragState.startX = getCoord(evt, 'clientX') || 0;
dragState.startY = clientY;
dragState.startWidth = $ta.outerWidth();
dragState.startHeight = $ta.outerHeight();
if (evt.preventDefault) evt.preventDefault();
$(window).off('.rmTaResize');
if (usePointerEvents) {
$(window).on('pointermove.rmTaResize', onDragMove).on('pointerup.rmTaResize pointercancel.rmTaResize', stopDrag);
} else {
$(window).on('touchmove.rmTaResize mousemove.rmTaResize', onDragMove).on('touchend.rmTaResize touchcancel.rmTaResize mouseup.rmTaResize', stopDrag);
}
}
$ta.css({ 'border-bottom-left-radius': '0', 'border-bottom-right-radius': '0' });
$ta.next('[data-rm-textarea-grip]').remove();
$ta.after($grip);
if (usePointerEvents) $grip.on('pointerdown.rmTaGrip', startDrag);
else $grip.on('touchstart.rmTaGrip mousedown.rmTaGrip', startDrag);
}
function applyModalContentWidth($modal, contentWidth, options) {
var opts = options || {};
var layout = getModalLayout();
var modalFrame = getBoxFrameWidth($modal);
var safeContentWidth = Math.max(layout.minWidth, Math.min(contentWidth, layout.maxOuterWidth - modalFrame));
var modalWidth = safeContentWidth + modalFrame;
var initialContentW = parseFloat($modal.data('rmInitialContentW')) || 0;
$modal.css({
width: modalWidth + 'px',
'max-width': layout.maxOuterWidth + 'px',
'box-sizing': 'border-box',
'margin-left': layout.shouldCenter ? 'auto' : '0',
'margin-right': layout.shouldCenter ? 'auto' : '0'
}).toggleClass('rmCompactContent', safeContentWidth < 520);
$('.' + RESIZE_CLASS).css({
width: safeContentWidth + 'px',
'max-width': '100%',
'box-sizing': 'border-box'
});
$('#rmMsg,#nominationReason,#rmReportText').each(function () {
var $textarea = $(this);
var textareaId = this.id;
if (!$textarea.length) return;
$textarea.css('width', safeContentWidth + 'px');
$textarea.next('[data-rm-textarea-grip]').css('width', safeContentWidth + 'px');
$('.rmQuickPhrasesPanel[data-rm-target="' + textareaId + '"]').css('width', safeContentWidth + 'px');
});
$('.rmNestedCommentInput').each(function () {
var $textarea = $(this);
var $wrap = $textarea.parent();
var containerFrame = parseFloat($textarea.data('rmNestedContainerFrame')) || 0;
var wrapFrame = parseFloat($textarea.data('rmNestedWrapFrame')) || 0;
var safeMinWidth = parseFloat($textarea.data('rmNestedMinWidth')) || 0;
var wrapOuterWidth;
var textareaWidth;
if (!$wrap.length || !$wrap.is(':visible')) return;
wrapOuterWidth = Math.max(1, safeContentWidth - containerFrame);
textareaWidth = Math.max(1, wrapOuterWidth - wrapFrame);
$wrap.css({
width: wrapOuterWidth + 'px',
'max-width': '100%',
'box-sizing': 'border-box'
});
$textarea.css({
width: textareaWidth + 'px',
'min-width': Math.min(safeMinWidth || textareaWidth, textareaWidth) + 'px'
});
$textarea.next('[data-rm-textarea-grip]').css('width', textareaWidth + 'px');
$('.rmQuickPhrasesPanel[data-rm-target="' + this.id + '"]').css('width', textareaWidth + 'px');
});
syncLinkWidths();
if (isVector22) {
var $content = $('#content');
if ($content.length && modalWidth > initialContentW) $content.css({ 'min-width': modalWidth + 'px' });
else if ($content.length) $content.css({ 'min-width': '' });
} else {
$('#content').css({ 'min-width': '' });
}
if (!opts.skipStore) $modal.data('rmContentWidth', safeContentWidth);
return safeContentWidth;
}
function setupNestedResizableTextarea(textareaId, wrapId, minWidth, minHeight) {
var $ta = $('#' + textareaId);
var $wrap = $('#' + wrapId);
var $modal = $('#removerModal');
var $container = $wrap.parent();
var layout = getModalLayout();
var safeMinWidth = parseInt(minWidth, 10) || 280;
var safeMinHeight = parseInt(minHeight, 10) || 90;
var initialWidth;
var modalFrame;
var containerFrame;
var wrapFrame;
function px($el, prop) { var n = parseFloat($el.css(prop)); return isNaN(n) ? 0 : n; }
function getMaxTextareaWidth(currentLayout) {
return Math.max(1, currentLayout.maxOuterWidth - modalFrame - containerFrame - wrapFrame);
}
function getEffectiveMinWidth(currentLayout) {
return Math.min(safeMinWidth, getMaxTextareaWidth(currentLayout));
}
function sync() {
var currentLayout = getModalLayout();
var maxTextareaWidth = getMaxTextareaWidth(currentLayout);
var textareaWidth = $ta.outerWidth();
var contentWidth;
if (!$wrap.is(':visible')) return;
if (textareaWidth > maxTextareaWidth) {
$ta.css('width', maxTextareaWidth + 'px');
textareaWidth = $ta.outerWidth();
}
contentWidth = Math.min(currentLayout.maxOuterWidth - modalFrame, textareaWidth + wrapFrame + containerFrame);
applyModalContentWidth($modal, contentWidth);
}
if (!$ta.length || !$wrap.length || !$modal.length) return;
modalFrame = px($modal, 'padding-left') + px($modal, 'padding-right') + px($modal, 'border-left-width') + px($modal, 'border-right-width');
containerFrame = $container.length
? px($container, 'padding-left') + px($container, 'padding-right') + px($container, 'border-left-width') + px($container, 'border-right-width')
: 0;
wrapFrame = px($wrap, 'padding-left') + px($wrap, 'padding-right') + px($wrap, 'border-left-width') + px($wrap, 'border-right-width');
initialWidth = Math.min(
Math.max(getEffectiveMinWidth(layout), getDefaultResizableWidth(modalFrame + containerFrame + wrapFrame)),
getMaxTextareaWidth(layout)
);
$ta.css({
width: initialWidth + 'px',
'min-width': getEffectiveMinWidth(layout) + 'px',
'min-height': safeMinHeight + 'px',
'box-sizing': 'border-box',
resize: layout.useFullWidth ? 'none' : 'both',
'border-bottom-left-radius': '',
'border-bottom-right-radius': ''
});
$ta.data('rmNestedContainerFrame', containerFrame);
$ta.data('rmNestedWrapFrame', wrapFrame);
$ta.data('rmNestedMinWidth', safeMinWidth);
$ta.next('[data-rm-textarea-grip]').remove();
if (layout.useFullWidth) {
bindTouchTextareaGrip($ta, sync, function () {
return getMaxTextareaWidth(getModalLayout());
}, {
minWidth: getEffectiveMinWidth(layout),
minHeight: safeMinHeight
});
}
registerModalLayoutSync(sync);
$(window).off('resize.removerModal').on('resize.removerModal', syncModalLayout);
if (typeof ResizeObserver === 'function') {
var observer = new ResizeObserver(sync);
observer.observe($ta[0]);
registerResizeObserver(observer);
}
sync();
}
function setupResizableModal(textareaId) {
var $ta = $('#' + textareaId);
var $modal = $('#removerModal');
var layout = getModalLayout();
function px($el, prop) { var n = parseFloat($el.css(prop)); return isNaN(n) ? 0 : n; }
applyV2022Layout($modal);
$modal.css({ display: 'block', 'margin-left': layout.shouldCenter ? 'auto' : '0', 'margin-right': layout.shouldCenter ? 'auto' : '0' });
var isBorderBox = ($modal.css('box-sizing') || '').toLowerCase() === 'border-box';
var hFrame = px($modal, 'padding-left') + px($modal, 'padding-right') + px($modal, 'border-left-width') + px($modal, 'border-right-width');
var initialContentW = isVector22 ? ($('#content').outerWidth() || 0) : 0;
var minWidth = layout.minWidth;
$modal.data('rmInitialContentW', initialContentW);
$ta.css({ width: getDefaultResizableWidth(hFrame) + 'px', height: sz.taH, padding: '8px', 'box-sizing': 'border-box',
border: '1px solid ' + tk.bSub, 'border-radius': '2px', background: tk.bgBase,
color: 'inherit', resize: layout.useFullWidth ? 'none' : 'both', 'min-height': sz.taMinH, 'min-width': sz.taMinW });
$(window).off('.rmTaResize');
function sync() {
var currentLayout = getModalLayout();
var maxTextareaWidth = Math.max(minWidth, currentLayout.maxOuterWidth - Math.floor(hFrame));
var w = $ta.outerWidth();
if (w > maxTextareaWidth) {
$ta.css('width', maxTextareaWidth + 'px');
w = $ta.outerWidth();
}
applyModalContentWidth($modal, isBorderBox ? w : Math.min(currentLayout.maxOuterWidth - hFrame, w));
}
if (layout.useFullWidth) bindTouchTextareaGrip($ta, sync, function () {
return Math.max(minWidth, getModalLayout().maxOuterWidth - Math.floor(hFrame));
});
else {
$ta.css({ 'border-bottom-left-radius': '', 'border-bottom-right-radius': '' });
$ta.next('[data-rm-textarea-grip]').remove();
}
registerModalLayoutSync(sync);
$(window).off('resize.removerModal').on('resize.removerModal', syncModalLayout);
if (typeof ResizeObserver === 'function') {
var observer = new ResizeObserver(sync);
observer.observe($ta[0]);
registerResizeObserver(observer);
}
sync();
}
function addInputRow(opts) {
var w = $('#rmMsg,#nominationReason,#rmReportText').first().outerWidth() || 0;
var indentLeft = Math.max(0, parseInt(opts.indentLeft, 10) || 0);
var rowClass = opts.rowClass ? ' ' + opts.rowClass : '';
var widthStyle = opts.fitParentWidth
? 'width:calc(100% - ' + indentLeft + 'px);box-sizing:border-box;'
: (opts.autoWidth ? '' : (w ? 'width:' + Math.max(1, w - indentLeft) + 'px;' : ''));
$('#' + opts.containerId).append(joinHtml([
'<div class="rmInputRow ', RESIZE_CLASS, rowClass, '" style="', stRow, widthStyle, indentLeft ? 'margin-left:' + indentLeft + 'px;' : '', '">',
opts.prefixHtml || '',
'<input type="text" class="', opts.inputClass || 'variantInput', '" placeholder="', opts.placeholder || '', '" style="', stInputBox, '">',
opts.removeBeforeInput ? '' : '<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить">−</button>',
'</div>'
]));
syncModalLayout();
}
$(document).off('click.rmRemoveInput').on('click.rmRemoveInput', '.rmRemoveInput', function () {
$(this).closest('.rmInputRow').remove();
syncModalLayout();
});
$(document).off('click.rmQuickPhraseInsert').on('click.rmQuickPhraseInsert', '.rmQuickPhraseActionBtn', function (e) {
var targetId;
var phrase;
e.preventDefault();
targetId = $(this).data('rmTarget');
phrase = $(this).attr('data-rm-phrase') || '';
if (!targetId) return;
insertTextIntoTextarea($('#' + targetId), phrase);
});
function buildMultiInputHtml(c) {
var addLabel = c.addBtnLabel || '+ Добавить';
var addClass = c.type === 'rename' ? 'rmAddVariantBtn rmRenameVariantAddBtn' : '';
var addSymbol = c.type === 'rename' ? '⤷' : '+';
return joinHtml([
'<div class="rmInputRow ', RESIZE_CLASS, '" style="', stRow, '">',
'<input id="', c.firstId, '" type="text" class="', c.inputClass || 'variantInput', '" placeholder="', c.firstPh || '', '" style="', stInputBox, '">',
buildSquareAddButtonHtml(c.addBtnId, addLabel.replace(/^\+\s*/, '') || 'Добавить', addClass, addSymbol),
'</div>',
'<div id="', c.containerId, '"></div>'
]);
}
function wireMultiInput(c) {
$('#' + c.addBtnId).click(function () {
var count = $('#' + c.containerId + ' .rmInputRow').length;
if (typeof c.maxRows === 'number' && count >= c.maxRows) { alert(c.maxMsg || 'Достигнут лимит.'); return; }
addInputRow({
containerId: c.containerId,
placeholder: c.addPh,
inputClass: c.inputClass,
rowClass: c.type === 'rename' ? 'rmRenameVariantRow' : '',
indentLeft: 0,
fitParentWidth: c.type === 'rename',
prefixHtml: c.type === 'rename' ? buildLeftRemoveButtonHtml('rmRemoveInput', 'Удалить') : '',
removeBeforeInput: c.type === 'rename'
});
});
}
function buildSettingsFieldHtml(label, controlHtml, helpText, options) {
var opts = options || {};
var helpHtml = helpText
? '<div class="rmSettingsFieldHint">' + helpText + '</div>'
: '';
var labelHtml = opts.forId
? '<label class="rmSettingsFieldLabel" for="' + opts.forId + '">' + label + '</label>'
: '<div class="rmSettingsFieldLabel">' + label + '</div>';
return joinHtml([
'<div class="rmSettingsField">',
labelHtml,
'<div class="rmSettingsFieldControl">', controlHtml, '</div>',
helpHtml,
'</div>'
]);
}
function buildSettingsSectionHtml(title, bodyHtml, helpText, options) {
var opts = options || {};
var headerHtml = '';
var description = [];
if (opts.titleNote) description.push(opts.titleNote);
if (helpText) description.push(helpText);
if (title || description.length) {
headerHtml = joinHtml([
'<div class="rmSettingsSectionHeader">',
title ? '<div class="rmSettingsSectionTitle">' + title + '</div>' : '',
description.length ? '<div class="rmSettingsSectionDescription">' + description.join(' ') + '</div>' : '',
'</div>'
]);
}
return joinHtml([
'<div class="rmSettingsSection">',
headerHtml,
bodyHtml,
'</div>'
]);
}
function buildSettingsSimpleCheckboxHtml(id, text) {
return joinHtml([
'<label class="rmSettingsCheck">',
'<input id="', id, '" type="checkbox">',
'<span>', text, '</span>',
'</label>'
]);
}
function buildQuickPhrasesSettingsEditorHtml() {
return joinHtml([
'<div id="rmSettingsQuickPhrasesEditor" class="rmQuickPhraseEditor">',
'<div id="rmSettingsQuickPhrasesList" class="rmQuickPhraseList"></div>',
'<input id="rmSettingsQuickPhraseInput" type="text" autocomplete="off" style="', stInputFull, 'margin-bottom:0;">',
'<div id="rmSettingsQuickPhraseMeta" class="rmQuickPhraseMeta"></div>',
'</div>'
]);
}
function getQuickPhraseEditor() {
return $('#rmSettingsQuickPhrasesEditor');
}
function getQuickPhraseEditorState() {
var $editor = getQuickPhraseEditor();
var phrases = normalizeQuickPhrasesList($editor.data('rmQuickPhrases'), []);
var editingIndex = parseInt($editor.data('rmQuickPhraseEditingIndex'), 10);
if (isNaN(editingIndex)) editingIndex = -1;
return { editor: $editor, phrases: phrases, editingIndex: editingIndex };
}
function setQuickPhraseEditorState(phrases, editingIndex) {
var $editor = getQuickPhraseEditor();
var normalized = normalizeQuickPhrasesList(phrases, []);
var safeEditingIndex = parseInt(editingIndex, 10);
if (isNaN(safeEditingIndex) || safeEditingIndex < 0 || safeEditingIndex >= normalized.length) safeEditingIndex = -1;
if (!$editor.length) return;
$editor.data('rmQuickPhrases', normalized);
$editor.data('rmQuickPhraseEditingIndex', safeEditingIndex);
renderQuickPhraseEditor();
}
function clearQuickPhraseDropState() {
var $editor = getQuickPhraseEditor();
$editor.removeData('rmQuickPhraseDragIndex');
$editor.removeData('rmQuickPhraseDropIndex');
$editor.removeData('rmQuickPhraseDropAfter');
$editor.find('.rmQuickPhraseChip').removeClass('is-dragging is-drop-before is-drop-after');
}
function renderQuickPhraseEditor() {
var state = getQuickPhraseEditorState();
var $list = $('#rmSettingsQuickPhrasesList');
var $input = $('#rmSettingsQuickPhraseInput');
var $meta = $('#rmSettingsQuickPhraseMeta');
if (!state.editor.length || !$list.length || !$input.length) return;
if (state.phrases.length) {
$list.html(state.phrases.map(function (phrase, index) {
var chipClass = 'rmQuickPhraseChip' + (index === state.editingIndex ? ' is-editing' : '');
return joinHtml([
'<div class="', chipClass, '" draggable="true" data-rm-quick-index="', index, '">',
'<button type="button" class="rmQuickPhraseEditBtn" title="Редактировать фразу">', escapeHtml(phrase), '</button>',
'<button type="button" class="rmQuickPhraseRemoveBtn" title="Удалить фразу" aria-label="Удалить фразу">×</button>',
'</div>'
]);
}).join(''));
} else {
$list.html('<div class="rmQuickPhraseEmpty">Фразы пока не добавлены.</div>');
}
$input
.attr('placeholder', state.editingIndex >= 0 ? 'Изменить значение...' : 'Добавить значение...')
.toggleClass('is-editing', state.editingIndex >= 0);
$meta
.text('')
.hide();
}
function notifyQuickPhraseEditorChanged() {
var $editor = getQuickPhraseEditor();
if ($editor.length) $editor.trigger('rmQuickPhrasesChanged');
}
function startQuickPhraseEdit(index) {
var state = getQuickPhraseEditorState();
var $input = $('#rmSettingsQuickPhraseInput');
if (index < 0 || index >= state.phrases.length || !$input.length) return;
state.editor.data('rmQuickPhraseEditingIndex', index);
$input.val(state.phrases[index]);
renderQuickPhraseEditor();
$input.trigger('focus');
if ($input[0] && typeof $input[0].select === 'function') $input[0].select();
}
function cancelQuickPhraseEdit() {
var state = getQuickPhraseEditorState();
var $input = $('#rmSettingsQuickPhraseInput');
state.editor.data('rmQuickPhraseEditingIndex', -1);
if ($input.length) $input.val('').removeClass('is-editing');
renderQuickPhraseEditor();
}
function saveQuickPhraseInput() {
var state = getQuickPhraseEditorState();
var $input = $('#rmSettingsQuickPhraseInput');
var value = normalizeQuickPhraseValue($input.val());
var next = [];
if (!$input.length || !value) return false;
if (state.editingIndex >= 0) {
state.phrases.forEach(function (phrase, index) {
if (index === state.editingIndex) {
next.push(value);
return;
}
if (phrase !== value && next.indexOf(phrase) === -1) next.push(phrase);
});
} else {
next = state.phrases.slice();
if (next.indexOf(value) === -1) next.push(value);
}
state.editor.data('rmQuickPhrases', normalizeQuickPhrasesList(next, []));
state.editor.data('rmQuickPhraseEditingIndex', -1);
$input.val('').removeClass('is-editing');
clearQuickPhraseDropState();
renderQuickPhraseEditor();
notifyQuickPhraseEditorChanged();
return true;
}
function removeQuickPhrase(index) {
var state = getQuickPhraseEditorState();
var $input = $('#rmSettingsQuickPhraseInput');
if (index < 0 || index >= state.phrases.length) return;
state.phrases.splice(index, 1);
state.editor.data('rmQuickPhrases', state.phrases);
if (state.editingIndex === index) {
state.editor.data('rmQuickPhraseEditingIndex', -1);
if ($input.length) $input.val('').removeClass('is-editing');
} else if (state.editingIndex > index) {
state.editor.data('rmQuickPhraseEditingIndex', state.editingIndex - 1);
}
clearQuickPhraseDropState();
renderQuickPhraseEditor();
notifyQuickPhraseEditorChanged();
}
function reorderQuickPhrases(phrases, fromIndex, toIndex, placeAfter) {
var result = phrases.slice();
var insertIndex = toIndex + (placeAfter ? 1 : 0);
var item;
if (fromIndex < 0 || fromIndex >= result.length || toIndex < 0 || toIndex >= result.length) return result;
item = result.splice(fromIndex, 1)[0];
if (fromIndex < insertIndex) insertIndex--;
result.splice(insertIndex, 0, item);
return result;
}
function getQuickPhraseDropPointer(evt) {
var originalEvent = evt && (evt.originalEvent || evt);
if (!originalEvent) return null;
if (typeof originalEvent.clientX !== 'number' || typeof originalEvent.clientY !== 'number') return null;
return { x: originalEvent.clientX, y: originalEvent.clientY };
}
function getQuickPhraseDropTarget($list, pointer, dragIndex) {
var candidates = [];
var rowCandidates;
var minRowDistance = Infinity;
var bestBoundary = null;
var bestBoundaryDistance = Infinity;
if (!$list || !$list.length || !pointer) return null;
$list.children('.rmQuickPhraseChip').each(function () {
var index = parseInt($(this).attr('data-rm-quick-index'), 10);
var rect;
var rowDistance;
if (isNaN(index) || index === dragIndex) return;
rect = this.getBoundingClientRect();
if (!rect.width || !rect.height) return;
rowDistance = pointer.y < rect.top ? (rect.top - pointer.y) : (pointer.y > rect.bottom ? (pointer.y - rect.bottom) : 0);
candidates.push({
node: this,
index: index,
left: rect.left,
right: rect.right,
top: rect.top,
bottom: rect.bottom,
midX: rect.left + rect.width / 2,
rowDistance: rowDistance
});
if (rowDistance < minRowDistance) minRowDistance = rowDistance;
});
if (!candidates.length) return null;
rowCandidates = candidates
.filter(function (candidate) { return candidate.rowDistance === minRowDistance; })
.sort(function (a, b) {
if (a.left !== b.left) return a.left - b.left;
return a.index - b.index;
});
if (!rowCandidates.length) return null;
if (pointer.x <= rowCandidates[0].left) {
return { index: rowCandidates[0].index, placeAfter: false, node: rowCandidates[0].node };
}
if (pointer.x >= rowCandidates[rowCandidates.length - 1].right) {
return {
index: rowCandidates[rowCandidates.length - 1].index,
placeAfter: true,
node: rowCandidates[rowCandidates.length - 1].node
};
}
for (var i = 0; i < rowCandidates.length; i++) {
var candidate = rowCandidates[i];
if (pointer.x >= candidate.left && pointer.x <= candidate.right) {
return {
index: candidate.index,
placeAfter: pointer.x > candidate.midX,
node: candidate.node
};
}
}
rowCandidates.forEach(function (candidate) {
var leftDistance = Math.abs(pointer.x - candidate.left);
var rightDistance = Math.abs(pointer.x - candidate.right);
if (leftDistance < bestBoundaryDistance) {
bestBoundaryDistance = leftDistance;
bestBoundary = { index: candidate.index, placeAfter: false, node: candidate.node };
}
if (rightDistance < bestBoundaryDistance) {
bestBoundaryDistance = rightDistance;
bestBoundary = { index: candidate.index, placeAfter: true, node: candidate.node };
}
});
return bestBoundary;
}
function bindQuickPhrasesEditor() {
var $editor = getQuickPhraseEditor();
var $list = $('#rmSettingsQuickPhrasesList');
var $input = $('#rmSettingsQuickPhraseInput');
function updateQuickPhraseDropTarget(evt) {
var dragIndex = parseInt($editor.data('rmQuickPhraseDragIndex'), 10);
var pointer = getQuickPhraseDropPointer(evt);
var target;
if (isNaN(dragIndex) || !pointer) return null;
target = getQuickPhraseDropTarget($list, pointer, dragIndex);
if (!target) return null;
$editor.data('rmQuickPhraseDropIndex', target.index);
$editor.data('rmQuickPhraseDropAfter', !!target.placeAfter);
$editor.find('.rmQuickPhraseChip').removeClass('is-drop-before is-drop-after');
$(target.node).addClass(target.placeAfter ? 'is-drop-after' : 'is-drop-before');
return target;
}
function applyQuickPhraseDrop() {
var state = getQuickPhraseEditorState();
var fromIndex = parseInt($editor.data('rmQuickPhraseDragIndex'), 10);
var toIndex = parseInt($editor.data('rmQuickPhraseDropIndex'), 10);
var placeAfter = $editor.data('rmQuickPhraseDropAfter') === true;
var changed = false;
if (!isNaN(fromIndex) && !isNaN(toIndex) && fromIndex !== toIndex) {
state.editor.data('rmQuickPhrases', reorderQuickPhrases(state.phrases, fromIndex, toIndex, placeAfter));
if (state.editingIndex === fromIndex) state.editor.data('rmQuickPhraseEditingIndex', -1);
changed = true;
}
clearQuickPhraseDropState();
renderQuickPhraseEditor();
if (changed) notifyQuickPhraseEditorChanged();
}
if (!$editor.length || !$list.length || !$input.length) return;
$editor.off('.rmQuickPhraseEditor');
$list.off('.rmQuickPhraseEditor');
$input.off('.rmQuickPhraseEditor');
$input.on('keydown.rmQuickPhraseEditor', function (e) {
if (e.key === 'Enter' || e.keyCode === 13) {
e.preventDefault();
saveQuickPhraseInput();
} else if (e.key === 'Escape' || e.keyCode === 27) {
e.preventDefault();
cancelQuickPhraseEdit();
}
});
$editor
.on('click.rmQuickPhraseEditor', '.rmQuickPhraseEditBtn', function () {
var index = parseInt($(this).closest('.rmQuickPhraseChip').attr('data-rm-quick-index'), 10);
startQuickPhraseEdit(index);
})
.on('click.rmQuickPhraseEditor', '.rmQuickPhraseRemoveBtn', function (e) {
var index;
e.preventDefault();
e.stopPropagation();
index = parseInt($(this).closest('.rmQuickPhraseChip').attr('data-rm-quick-index'), 10);
removeQuickPhrase(index);
})
.on('dragstart.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) {
var index = parseInt($(this).attr('data-rm-quick-index'), 10);
if (isNaN(index)) return;
$editor.data('rmQuickPhraseDragIndex', index);
$(this).addClass('is-dragging');
if (e.originalEvent && e.originalEvent.dataTransfer) {
e.originalEvent.dataTransfer.effectAllowed = 'move';
e.originalEvent.dataTransfer.setData('text/plain', String(index));
}
})
.on('dragover.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) {
if ($editor.data('rmQuickPhraseDragIndex') === undefined) return;
e.preventDefault();
updateQuickPhraseDropTarget(e);
if (e.originalEvent && e.originalEvent.dataTransfer) e.originalEvent.dataTransfer.dropEffect = 'move';
})
.on('drop.rmQuickPhraseEditor', '.rmQuickPhraseChip', function (e) {
e.preventDefault();
updateQuickPhraseDropTarget(e);
applyQuickPhraseDrop();
})
.on('dragend.rmQuickPhraseEditor', '.rmQuickPhraseChip', function () {
clearQuickPhraseDropState();
});
$list
.on('dragover.rmQuickPhraseEditor', function (e) {
if ($editor.data('rmQuickPhraseDragIndex') === undefined) return;
e.preventDefault();
updateQuickPhraseDropTarget(e);
if (e.originalEvent && e.originalEvent.dataTransfer) e.originalEvent.dataTransfer.dropEffect = 'move';
})
.on('drop.rmQuickPhraseEditor', function (e) {
e.preventDefault();
updateQuickPhraseDropTarget(e);
applyQuickPhraseDrop();
});
renderQuickPhraseEditor();
}
function collectQuickPhraseValues() {
return getQuickPhraseEditorState().phrases;
}
function collectQuickPhraseValuesSnapshot() {
var state = getQuickPhraseEditorState();
var value = normalizeQuickPhraseValue($('#rmSettingsQuickPhraseInput').val());
var next = state.phrases.slice();
if (!value) return next;
if (state.editingIndex >= 0) {
next[state.editingIndex] = value;
} else if (next.indexOf(value) === -1) {
next.push(value);
}
return normalizeQuickPhrasesList(next, []);
}
function isMenuTitlePresetValue(value) {
return value === MENU_TITLE_PRESET_CACTIONS || value === MENU_TITLE_PRESET_PAGE || value === MENU_TITLE_PRESET_TOOLS;
}
function isMenuTitlePresetOnlySkin() {
return mwCfg.skin === 'minerva' || mwCfg.skin === 'monobook' || mwCfg.skin === 'timeless';
}
function getDefaultMenuTitlePreset() {
return mwCfg.skin === 'minerva' ? MENU_TITLE_PRESET_TOOLS : MENU_TITLE_PRESET_CACTIONS;
}
function shouldPreserveStoredMenuTitleOnSave() {
return mwCfg.skin === 'minerva';
}
function isAvailableMenuTitlePresetValue(value) {
return getMenuTitlePresetOptions().some(function (option) {
return option.value === value;
});
}
function getMenuTitlePresetOptions() {
if (mwCfg.skin === 'minerva') {
return [
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Ещё' }
];
}
if (isVector22) {
return [
{ value: MENU_TITLE_PRESET_CACTIONS, label: 'Инструменты/Действия' },
{ value: MENU_TITLE_PRESET_PAGE, label: 'Страница' },
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты/Основное' }
];
}
if (mwCfg.skin === 'timeless') {
return [
{ value: MENU_TITLE_PRESET_CACTIONS, label: 'Инструменты для страниц' },
{ value: MENU_TITLE_PRESET_PAGE, label: 'Страница' },
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Вики-инструменты' }
];
}
if (mwCfg.skin === 'monobook') {
return [
{ value: MENU_TITLE_PRESET_CACTIONS, label: 'Верхняя панель' },
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты' }
];
}
return [
{ value: MENU_TITLE_PRESET_CACTIONS, label: mwCfg.skin === 'vector' ? 'Ещё' : 'Ещё / Действия' },
{ value: MENU_TITLE_PRESET_PAGE, label: 'Страница' },
{ value: MENU_TITLE_PRESET_TOOLS, label: 'Инструменты' }
];
}
function getMenuTitlePresetHintText() {
var base = (mwCfg.skin === 'vector' || isVector22)
? 'Можно либо изменить заголовок отдельного меню Remover, либо перенести все пункты в одно из существующих стандартных меню.'
: 'На этом скине Remover использует существующие стандартные меню; отдельное меню с собственным заголовком не создаётся.';
if (isVector22) base += ' В Vector 2022 кнопки «Действия» и «Основное» находятся внутри общего меню «Инструменты».';
else if (mwCfg.skin === 'vector') base += ' В Vector отдельный заголовок создаёт собственное меню рядом с «Ещё».';
else if (mwCfg.skin === 'minerva') base += ' В Minerva Neue пункты Remover показываются в меню «Ещё».';
else if (mwCfg.skin === 'timeless') base += ' В Timeless доступны только стандартные варианты: «Инструменты для страниц», «Страница» и «Вики-инструменты».';
else if (mwCfg.skin === 'monobook') base += ' В MonoBook доступны только два варианта: поместить пункты в «Инструменты» или вывести их в верхнюю панель. Собственный заголовок меню здесь не используется.';
return base;
}
function getSignatureSeparatorPreviewText(value) {
var separator = String(value || '').trim();
return separator ? (separator + ' ' + '~~' + '~~') : ('~~' + '~~');
}
function updateSignatureSeparatorPreview(value) {
var previewValue = (typeof value === 'string') ? value : ($('#rmSettingsSignatureSeparator').val() || '');
var $code = $('#rmSettingsSignaturePreviewCode');
if (!$code.length) return;
$code.text(getSignatureSeparatorPreviewText(previewValue));
}
function bindSignatureSeparatorPreview() {
var $input = $('#rmSettingsSignatureSeparator');
if (!$input.length) return;
$input.off('.rmSignaturePreview').on('input.rmSignaturePreview change.rmSignaturePreview', function () {
updateSignatureSeparatorPreview($(this).val());
});
updateSignatureSeparatorPreview($input.val());
}
function buildMenuTitlePresetButtonsHtml() {
return joinHtml([
'<div class="rmSettingsMenuPresetWrap">',
'<div class="rmSettingsMenuPresetLabel">Перенос в стандартные меню</div>',
'<div id="rmSettingsMenuPresetBar" class="rmSettingsMenuPresetBar">',
getMenuTitlePresetOptions().map(function (option) {
return joinHtml([
'<button type="button" class="rmSettingsMenuPresetBtn" data-rm-menu-preset="',
option.value,
'" aria-pressed="false">',
escapeHtml(option.label),
'</button>'
]);
}).join(''),
'</div>',
'</div>'
]);
}
function applyMenuTitlePresetControls(presetValue) {
var preset = isMenuTitlePresetValue(presetValue) ? presetValue : '';
var $bar = $('#rmSettingsMenuPresetBar');
var $input = $('#rmSettingsMenuTitle');
var forcePresetOnly = isMenuTitlePresetOnlySkin();
if (!$bar.length || !$input.length) return;
$bar.data('rmPreset', preset);
$bar.find('.rmSettingsMenuPresetBtn').each(function () {
var isActive = $(this).data('rmMenuPreset') === preset;
$(this).toggleClass('is-active', isActive).attr('aria-pressed', isActive ? 'true' : 'false');
});
$input.prop('disabled', forcePresetOnly || !!preset);
}
function bindMenuTitlePresetControls() {
var $bar = $('#rmSettingsMenuPresetBar');
var $input = $('#rmSettingsMenuTitle');
if (!$bar.length || !$input.length) return;
$input.off('.rmSettingsMenuPreset').on('input.rmSettingsMenuPreset', function () {
if (!$bar.data('rmPreset')) $input.data('rmCustomValue', $input.val());
});
$bar.off('.rmSettingsMenuPreset').on('click.rmSettingsMenuPreset', '.rmSettingsMenuPresetBtn', function () {
var preset = $(this).data('rmMenuPreset');
var currentPreset = $bar.data('rmPreset') || '';
if (currentPreset === preset) {
if (isMenuTitlePresetOnlySkin()) return;
applyMenuTitlePresetControls('');
$input.val($input.data('rmCustomValue') || '');
$input.trigger('focus');
return;
}
$input.data('rmCustomValue', $input.val());
applyMenuTitlePresetControls(preset);
});
}
function fillSettingsFormValues(settings) {
var data = normalizeRemoverSettings(settings);
var forcePresetOnly = isMenuTitlePresetOnlySkin();
var menuTitleValue = data.menuTitle || '';
var storedMenuTitleValue = menuTitleValue;
if (forcePresetOnly && !isAvailableMenuTitlePresetValue(menuTitleValue)) menuTitleValue = getDefaultMenuTitlePreset();
var customMenuTitle = forcePresetOnly ? '' : (isMenuTitlePresetValue(menuTitleValue) ? settingsDefaults.menuTitle : menuTitleValue);
$('#rmSettingsForm').data('rmStoredMenuTitle', storedMenuTitleValue || '');
$('#rmSettingsNotifyAuthor').prop('checked', !!data.notifyAuthor);
$('#rmSettingsSubscribeTopic').prop('checked', !!data.subscribeTopic);
$('#rmSettingsAutoReloadAfterAction').prop('checked', !!data.autoReloadAfterAction);
$('#rmSettingsOpenNominationDiscussionInNewTab').prop('checked', !!data.openNominationDiscussionInNewTab);
$('#rmSettingsShowMenuIcons').prop('checked', !!data.showMenuIcons);
$('#rmSettingsMenuTitle').val(customMenuTitle || '').data('rmCustomValue', customMenuTitle || '');
$('#rmSettingsSignatureSeparator').val(data.signatureSeparator || '');
$('#rmSettingsExcludedNamespaces').val((data.excludedNamespaces || []).join(', '));
$('#rmSettingsDisabledItems').val((data.disabledItems || []).join(', '));
setQuickPhraseEditorState(data.quickPhrases || [], -1);
$('#rmSettingsQuickPhraseInput').val('').removeClass('is-editing');
clearQuickPhraseDropState();
applyMenuTitlePresetControls(menuTitleValue);
updateSignatureSeparatorPreview(data.signatureSeparator || '');
}
function collectSettingsFormValues(options) {
var opts = options || {};
var namespaces = parseNamespaceInput($('#rmSettingsExcludedNamespaces').val());
var disabledItems = parseDisabledItemsInput($('#rmSettingsDisabledItems').val());
var presetMenuTitle = $('#rmSettingsMenuPresetBar').data('rmPreset');
var forcePresetOnly = isMenuTitlePresetOnlySkin();
var storedMenuTitle = $('#rmSettingsForm').data('rmStoredMenuTitle');
if (namespaces.invalid.length) {
return { error: 'Некорректные номера пространств имён: ' + namespaces.invalid.join(', ') + '.' };
}
if (disabledItems.invalid.length) {
return { error: 'Неизвестные пункты меню: ' + disabledItems.invalid.join(', ') + '.' };
}
if (!opts.skipQuickPhraseCommit) saveQuickPhraseInput();
return {
value: normalizeRemoverSettings({
notifyAuthor: $('#rmSettingsNotifyAuthor').is(':checked'),
subscribeTopic: $('#rmSettingsSubscribeTopic').is(':checked'),
autoReloadAfterAction: $('#rmSettingsAutoReloadAfterAction').is(':checked'),
openNominationDiscussionInNewTab: $('#rmSettingsOpenNominationDiscussionInNewTab').is(':checked'),
showMenuIcons: $('#rmSettingsShowMenuIcons').is(':checked'),
menuTitle: forcePresetOnly
? (shouldPreserveStoredMenuTitleOnSave() && typeof storedMenuTitle === 'string'
? storedMenuTitle
: (isAvailableMenuTitlePresetValue(presetMenuTitle) ? presetMenuTitle : getDefaultMenuTitlePreset()))
: (isMenuTitlePresetValue(presetMenuTitle) ? presetMenuTitle : $('#rmSettingsMenuTitle').val()),
signatureSeparator: $('#rmSettingsSignatureSeparator').val(),
excludedNamespaces: namespaces.values,
disabledItems: disabledItems.values,
quickPhrases: opts.skipQuickPhraseCommit ? collectQuickPhraseValuesSnapshot() : collectQuickPhraseValues()
})
};
}
function updateSettingsSubmitReadyState(baselineSettings) {
var collected = collectSettingsFormValues({ skipQuickPhraseCommit: true });
var hasChanges = !!(collected.error || (collected.value && !areRemoverSettingsEqual(collected.value, baselineSettings)));
$('#removerSubmit').toggleClass('rmSubmitReady', hasChanges && !$('#removerSubmit').hasClass('rmSubmitError'));
$('#rmSettingsUnsavedHint').css('display', hasChanges ? 'inline-block' : 'none');
}
function bindSettingsSubmitReadyState(baselineSettings) {
var update = function () {
setTimeout(function () { updateSettingsSubmitReadyState(baselineSettings); }, 0);
};
$('#rmSettingsForm').off('.rmSettingsReady').on('input.rmSettingsReady change.rmSettingsReady', 'input, textarea, select', update);
$('#removerModalContent').off('click.rmSettingsReady keyup.rmSettingsReady').on(
'click.rmSettingsReady keyup.rmSettingsReady',
'.rmSettingsMenuPresetBtn,.rmQuickPhraseEditBtn,.rmQuickPhraseRemoveBtn,.rmQuickPhraseChip,#rmSettingsQuickPhraseInput',
update
);
$('#rmSettingsQuickPhrasesEditor').off('rmQuickPhrasesChanged.rmSettingsReady').on('rmQuickPhrasesChanged.rmSettingsReady', update);
updateSettingsSubmitReadyState(baselineSettings);
}
function buildSettingsFormHtml(menuLabelsHint) {
var menuTitleFieldLabel = isMenuTitlePresetOnlySkin() ? 'Размещение меню' : 'Заголовок отдельного меню';
var menuFields =
buildSettingsFieldHtml(menuTitleFieldLabel,
'<input id="rmSettingsMenuTitle" type="text" style="' + stInputFull + 'margin-bottom:0;">' + buildMenuTitlePresetButtonsHtml(),
getMenuTitlePresetHintText(), { forId: 'rmSettingsMenuTitle' }) +
buildSettingsFieldHtml('Визуальное оформление меню',
'<div class="rmSettingsChecks">' + buildSettingsSimpleCheckboxHtml('rmSettingsShowMenuIcons', 'Эмодзи в пунктах меню') + '</div>');
var messageFields =
buildSettingsFieldHtml('Префикс перед подписью',
'<input id="rmSettingsSignatureSeparator" type="text" style="' + stInputFull + 'margin-bottom:0;">',
'Добавляется перед подписью в публикуемых сообщениях.<span style="display:block;margin-top:6px;">Предпросмотр: <code id="rmSettingsSignaturePreviewCode"></code>.</span>',
{ forId: 'rmSettingsSignatureSeparator' }) +
buildSettingsFieldHtml('Часто используемые фразы', buildQuickPhrasesSettingsEditorHtml(),
'Enter для добавления. Порядок элементов изменяется перетаскиванием.', { forId: 'rmSettingsQuickPhraseInput' });
var defaultFields = '<div class="rmSettingsChecks">' +
buildSettingsSimpleCheckboxHtml('rmSettingsNotifyAuthor', 'Оповещать создателя страницы') +
buildSettingsSimpleCheckboxHtml('rmSettingsSubscribeTopic', 'Подписываться на номинацию') +
buildSettingsSimpleCheckboxHtml('rmSettingsOpenNominationDiscussionInNewTab', 'Открывать номинацию в отдельной вкладке') +
buildSettingsSimpleCheckboxHtml('rmSettingsAutoReloadAfterAction', 'Автообновление страницы после размещения<span style="display:block;margin-top:3px;color:' + tk.cSubM + ';font-size:12px;line-height:1.35;">Помешает взаимодействовать с логом действий.</span>') + '</div>';
var disableFields =
buildSettingsFieldHtml('Скрыть пункты меню',
'<input id="rmSettingsDisabledItems" type="text" style="' + stInputFull + 'margin-bottom:0;">',
'Названия пунктов через запятую, например <code>КБУ, КУЛ, КОБ</code>.' + menuLabelsHint,
{ forId: 'rmSettingsDisabledItems' }) +
buildSettingsFieldHtml('Не показывать в пространствах имён',
'<input id="rmSettingsExcludedNamespaces" type="text" style="' + stInputFull + 'margin-bottom:0;">',
'Номера пространств имён через запятую, например <code>2, 10, 828</code>. См. <a href="' + getPageUrl('Википедия:Пространства имён') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Википедия:Пространства имён</a>.',
{ forId: 'rmSettingsExcludedNamespaces' });
return joinHtml([
'<div id="rmSettingsForm" style="max-width:100%;">',
'<div class="rmSettingsLead">Настройки интерфейса и значений по умолчанию.</div>',
buildSettingsSectionHtml('Меню', menuFields, 'Настройки внешнего вида и состава меню Remover.'),
buildSettingsSectionHtml('Оформление сообщений', messageFields, 'Настройки оформления публикуемых сообщений в номинациях.'),
buildSettingsSectionHtml('Опции по умолчанию', defaultFields, 'Регулирует исходное состояние и поведение после выполнения действий.'),
buildSettingsSectionHtml('Отключение', disableFields, 'Скрывает отдельные пункты меню Remover или всё меню в выбранных пространствах имён.'),
'</div>'
]);
}
function buildSettingsFooterLeftHtml() {
return joinHtml([
'<div id="rmSettingsFooterLeft" style="display:flex;align-items:center;gap:6px;flex:1 1 auto;min-width:0;max-width:100%;flex-wrap:wrap;margin-right:auto;">',
'<button id="rmSettingsResetFooter" type="button" title="Удаляет сохранённые настройки Remover из вашего профиля MediaWiki и возвращает значения по умолчанию." style="', stCancel, '">Сбросить все настройки</button>',
'<a id="rmSettingsReportIssue" href="', getPageUrl('Обсуждение Википедии:Гаджеты/Remover'), '" target="_blank" rel="noopener noreferrer" ',
'title="Сообщить о проблеме или предложить улучшение" aria-label="Сообщить о проблеме или предложить улучшение" ',
'class="removerModalLink rmButtonLikeLink" style="', stCancel, 'display:inline-flex;align-items:center;justify-content:center;text-align:center;text-decoration:none;box-sizing:border-box;max-width:100%;line-height:1.2;word-break:normal;overflow-wrap:normal;">Обратная связь</a>',
'</div>'
]);
}
function openSettings() {
var currentSettings = normalizeRemoverSettings(state.settings || settingsDefaults);
var menuLabelsHint = buildSettingsMenuItemsHint();
var $previousModal = $('#removerModal').length ? $('#removerModal').detach() : $();
var previousLayoutSyncHandlers = modalLayoutSyncHandlers.slice();
function restorePreviousModal() {
closeModal();
if ($previousModal.length) {
$('#content').prepend($previousModal);
modalLayoutSyncHandlers = previousLayoutSyncHandlers.slice();
if (modalLayoutSyncHandlers.length) $(window).off('resize.removerModal').on('resize.removerModal', syncModalLayout);
syncModalLayout();
syncLinkWidths();
}
}
createModal({
title: 'Конфигурация',
width: 'compact',
showSettingsButton: false
});
$('#removerModal').addClass('rmModalSettings');
$('#removerModalHeaderBar').append(buildHeaderIconButtonHtml('rmSettingsBack', 'Назад', 'Назад', '←'));
$('#rmSettingsBack').on('click', restorePreviousModal);
$('#removerModalContent').html(buildSettingsFormHtml(menuLabelsHint));
fillSettingsFormValues(currentSettings);
bindMenuTitlePresetControls();
bindSignatureSeparatorPreview();
bindQuickPhrasesEditor();
renderModalFooter('submit', {
showCheckbox: false,
submitText: 'Сохранить',
onSubmit: function () {
var collected = collectSettingsFormValues();
var shouldReset;
var saveFn;
if (collected.error) {
alert(collected.error);
return false;
}
shouldReset = areRemoverSettingsEqual(collected.value, settingsDefaults);
saveFn = shouldReset
? function (callback) { resetSettingsOnServer(callback); }
: function (callback) { saveSettingsToServer(collected.value, callback); };
saveFn(function (err) {
if (err) {
alert('Не удалось ' + (shouldReset ? 'сбросить' : 'сохранить') + ' настройки: ' + (err.info || err.code || 'неизвестная ошибка') + '.');
unlockModalSubmit();
return;
}
location.reload();
});
}
});
var $settingsActions = $('#rmFooterActionButtons');
$settingsActions.wrapInner('<div id="rmSettingsActionButtonsRow"></div>');
$settingsActions.append('<span id="rmSettingsUnsavedHint" role="status" aria-live="polite">Есть несохранённые изменения</span>');
bindSettingsSubmitReadyState(currentSettings);
$('#rmFooterButtons').css('justify-content', 'space-between').prepend(buildSettingsFooterLeftHtml());
$('#rmSettingsResetFooter').on('click', function () {
fillSettingsFormValues(settingsDefaults);
updateSettingsSubmitReadyState(currentSettings);
$('#removerSubmit').trigger('focus');
});
}
// ─── Завершение обработки ────────────────────────────────────────────────
function finalizeSuccess(nominationInfo, usePageReload) {
if (isError) {
var $box = $('#rmLogBox').length ? $('#rmLogBox') : $('#removerModalContent');
$box.append('<p class="error">При выполнении скрипта произошли ошибки.</p>');
markSubmitError();
return;
}
renderModalFooter('reload');
if (nominationInfo && nominationInfo.pageTitle) {
appendNominationLink(nominationInfo.pageTitle, nominationInfo.sectionTitle);
}
if (!usePageReload && !nominationInfo) location.reload();
}
function finalizeFastRemoval(notifiedPages, summary) {
if (isError || !setAlert || !notifiedPages || !notifiedPages.length) {
finalizeSuccess(null, false);
return;
}
notifyAuthorsForPages(notifiedPages, {
summary: summary,
actionText: 'к быстрому удалению'
}, function () {
finalizeSuccess(null, false);
});
}
// ─── Общий runner ────────────────────────────────────────────────────────
/**
* Универсальный запуск полного пайплайна номинации.
* @param {Object} o
* templateStep — функция (next) → обработка шаблонов на статьях
* nominationStep — функция (done) → публикация номинации, done(err, {pageTitle, sectionTitle})
* notifyStep — функция (nominationInfo, next)
* skipNotify — boolean
* skipLink — boolean, не показывать ссылку на номинацию
*/
function runFlow(o) {
runNominationPipeline({
templateStep: o.templateStep,
nominationStep: o.nominationStep,
notifyStep: o.notifyStep || function (info, next) { next(); },
skipNotify: o.skipNotify,
onSuccess: function (ctx) {
if (isError) { markSubmitError(); return; }
renderModalFooter('reload');
if (!o.skipLink && ctx.nominationInfo && ctx.nominationInfo.pageTitle) {
appendNominationLink(ctx.nominationInfo.pageTitle, ctx.nominationInfo.sectionTitle);
maybeOpenNominationDiscussionInNewTab(ctx.nominationInfo);
}
},
onFailure: function () { markSubmitError(); }
});
}
// ═══════════════════════════════════════════════════════════════════════════
// ЯДРО: обработка статей (apply template + nomination page)
// ═══════════════════════════════════════════════════════════════════════════
/**
* Применяет шаблон к одной статье/категории.
* Понимает режим inArticle (вставка через <noinclude>),
* режим closeAction (снятие шаблона + запись на СО),
* режим cleanupAction (снятие КБУ/КУЛ).
*
* @param {string} pg — название страницы
* @param {Object} job — параметры задания (см. buildJob)
* @param {function} callback(err, meta)
*/
function applyTemplateToPage(pg, job, callback) {
var mode = job.mode;
// ── Снятие КБУ/КУЛ ──────────────────────────────────────────────────
if (mode === 'cleanup') {
var tm = job.transferMode || 'none';
if (tm === 'none') { callback({ code: 'error', info: 'Не выбран режим снятия шаблонов.' }); return; }
editPageContent(pg, { summary: job.summary, watchlist: 'nochange', readErrorCode: 'error', readError: 'Страница «' + pg + '» не существует.' },
function (article, done) {
var local = removeTransferTemplatesLocal(article, tm);
removeTransferTemplatesWithApiFallback(pg, local.text, tm, local, function (updated) {
if (updated.text === article) { done({ error: { code: 'error', info: 'Шаблоны для снятия не найдены.' } }); return; }
done({ text: updated.text });
});
}, function (err) { callback(err); });
return;
}
// ── Подведение итогов по КУ/КПМ (снятие + итог на СО) ───────────────
if (mode === 'denom') {
getTextWithTimestamp(pg, function (article, baseTimestamp, readErr) {
if (readErr) { callback(makeReadError(readErr, 'read_failed', 'Не удалось получить содержимое страницы «' + pg + '».')); return; }
if (article === null) { callback({ code: 'error', info: 'Страница «' + pg + '» не существует.' }); return; }
if (!job.sourceTemplate) { callback({ code: 'error', info: 'Не задан шаблон для снятия.' }); return; }
var tplPattern = buildTemplateNamePattern(job.sourceTemplate);
var tpl = findTemplateByPattern(article, tplPattern);
if (!tpl) { callback({ code: 'error', info: 'Невозможно снять шаблон «' + job.sourceTemplate + '».' }); return; }
var normalizedTplDate = convertToStandardDate(tpl.params[0]);
var tplExtraParams = tpl.params.slice(1);
var tplExtra = tplExtraParams.join('|').trim();
var renameTargets = collectRenameTargetsFromTemplateParams(tplExtraParams);
var isRedirectedDeletion = job.closeType === 'redirectedDeletion';
if (!RE_DATE_ISO.test(normalizedTplDate)) {
callback({ code: 'error', info: 'Не удалось распознать дату в шаблоне: «' + (tpl.params[0] || '') + '».' });
return;
}
if (isRedirectedDeletion && !job.redirectTarget) {
callback({ code: 'error', info: 'Не указана цель перенаправления.' });
return;
}
var date = getDate(normalizedTplDate);
var nomPlace = 'ВП:' + job.sourceTemplate.replace(/\|.*/, '') + '/' + date[1];
var retTalkSection = '';
var sectionNW, tplpar;
if (job.closeType === 'doneRnm') { sectionNW = job.oldTitle + ' → ' + pg; tplpar = job.oldTitle + '|' + pg; }
if (job.closeType === 'noRnm') {
if (!renameTargets.length) {
callback({ code: 'error', info: 'В шаблоне КПМ не найдено новое название.' });
return;
}
sectionNW = pg + ' → ' + renameTargets.join(', ');
tplpar = pg + '|' + renameTargets.join('|');
}
if (job.closeType === 'ret' || job.closeType === 'retConditional' || job.closeType === 'withdrawnDeletion' || isRedirectedDeletion) {
retTalkSection = tplExtra;
sectionNW = retTalkSection || pg;
tplpar = retTalkSection ? ('l1=' + retTalkSection) : '';
}
var editResultText = job.resultTemplate + (isRedirectedDeletion ? ' на [[' + job.redirectTarget + ']]' : '');
var editSummary = makeSummary('номинация [[' + (nomPlace ? nomPlace + '#' : '') + sectionNW + ']] — ' + editResultText);
var talkTitle = getTalkPage(pg);
getTextWithTimestamp(talkTitle, function (talkText, talkTimestamp, talkReadErr) {
if (talkReadErr) { callback(makeReadError(talkReadErr, 'talk_read_failed', 'Не удалось получить содержимое СО страницы «' + pg + '».')); return; }
var sourceTalkText = talkText || '';
var talkPageMissing = talkText === null;
var dateSectionTalkTemplate = getDateSectionTalkTemplateConfig(job.closeType);
var talkResult = dateSectionTalkTemplate
? upsertDateSectionTemplateOnTalkPage(sourceTalkText, dateSectionTalkTemplate.name, date[0], retTalkSection, dateSectionTalkTemplate)
: (job.closeType === 'retConditional')
? upsertConditionalRetTemplateOnTalkPage(sourceTalkText, date[0], retTalkSection, job.conditionalReason, job.conditionalDeadline)
: upsertSingleDateResultTemplateOnTalkPage(sourceTalkText, job.resultTemplate, date[0], tplpar);
function saveArticle() {
var cleaned = isRedirectedDeletion ? buildRedirectText(job.redirectTarget) : stripTemplatesByPattern(article, tplPattern).text;
var ep = { title: pg, text: cleaned, summary: editSummary, watchlist: 'nochange', assertuser: mwCfg.wgUserName };
if (baseTimestamp) ep.basetimestamp = baseTimestamp;
apiReq(ep, 'edit', function (t) {
var editErr = t && t.error ? t.error : null;
callback(editErr, editErr ? null : {
discussionPage: nomPlace,
discussionSection: sectionNW,
summary: editSummary,
successMessage: isRedirectedDeletion
? 'Содержимое страницы ' + buildQuotedStatusPageLink(pg) + ' очищено, установлено перенаправление на ' + buildQuotedStatusPageLink(job.redirectTarget) + '.'
: ''
});
});
}
if (talkResult.text === sourceTalkText) { saveArticle(); return; }
var talkEp = { title: talkTitle, text: talkResult.text, summary: editSummary, watchlist: 'nochange', assertuser: mwCfg.wgUserName };
if (talkPageMissing) talkEp.createonly = true;
else if (talkTimestamp) talkEp.basetimestamp = talkTimestamp;
apiReq(talkEp, 'edit', function (talkResp) {
if (talkResp && talkResp.error) { callback({ code: 'talk_failed', info: 'Не удалось записать итог на СО: ' + talkResp.error.info }); return; }
saveArticle();
});
});
});
return;
}
// ── Обычная номинация: вставка шаблона в статью ─────────────────────
// mode === 'nominate'
var isKu = job.opId === 'tRm' || job.opId === 'mRm';
var conflictRule = getNominationConflictRule(job);
var conflictLabel = conflictRule ? conflictRule.label : 'номинации';
editPageContent(pg, { summary: job.summary, readErrorCode: 'error', readError: 'Страница «' + pg + '» не существует.' },
function (article, done) {
var hasExistingNomination = conflictRule && conflictRule.detect(article);
var conflictDecision = getConflictDecisionForPage(job, pg);
function buildResult(finalText) {
var generatedTpl = buildGeneratedNominationTemplateText(job, pg);
return { text: generatedTpl ? wrapInNoinclude(finalText, generatedTpl) : finalText };
}
function buildResultAfterMoveToCommonsCleanup(sourceText) {
var localMove = removeMoveToCommonsTemplatesLocal(sourceText);
removeMoveToCommonsTemplatesWithApiFallback(pg, localMove.text, localMove, function (updated) {
done(buildResult(updated.text));
});
}
function finishConflictResolution(sourceText) {
var resolvedText;
var pageLink = buildQuotedStatusPageLink(pg);
if (conflictDecision.templateAction === 'keep') {
if (sourceText !== article) {
return {
text: sourceText,
meta: { successMessage: 'Шаблон ' + conflictLabel + ' на странице ' + pageLink + ' оставлен без изменений; шаблоны переноса сняты.' }
};
}
return { skip: true, meta: { successMessage: 'Шаблон ' + conflictLabel + ' на странице ' + pageLink + ' оставлен без изменений.' } };
}
resolvedText = applyConflictTemplateResolution(sourceText, job, pg, conflictDecision);
return {
text: resolvedText,
meta: {
successMessage: conflictDecision.templateAction === 'overwrite'
? 'Шаблон ' + conflictLabel + ' на странице ' + pageLink + ' перезаписан новой датой.'
: 'Новый шаблон ' + conflictLabel + ' добавлен сверху на странице ' + pageLink + '.'
}
};
}
if (hasExistingNomination && (!conflictDecision || conflictDecision.pageAction !== 'keep')) {
return { error: { code: 'error', info: 'На странице уже стоит шаблон ' + conflictLabel + '.' } };
}
if (hasExistingNomination && conflictDecision && conflictDecision.pageAction === 'keep') {
if (job.transferMode && job.transferMode !== 'none') {
var localConflict = removeTransferTemplatesLocal(article, job.transferMode);
removeTransferTemplatesWithApiFallback(pg, localConflict.text, job.transferMode, localConflict, function (updated) {
done(finishConflictResolution(updated.text));
});
return;
}
return finishConflictResolution(article);
}
if (isKu && job.transferMode && job.transferMode !== 'none') {
var local = removeTransferTemplatesLocal(article, job.transferMode);
removeTransferTemplatesWithApiFallback(pg, local.text, job.transferMode, local, function (updated) { done(buildResult(updated.text)); });
return;
}
if (job.removeMoveToCommons) {
buildResultAfterMoveToCommonsCleanup(article);
return;
}
return buildResult(article);
},
function (err) { callback(err); }
);
}
/**
* Обрабатывает список страниц последовательно.
* @param {string[]} pages
* @param {Object} job
* @param {function} onDone(notifiedPages, err, pageMeta)
*/
function processPageList(pages, job, onDone) {
var notifiedPages = [];
var pageMeta = {};
eachSequential(pages.slice().reverse(), function (pg, nextPage) {
var pageLink = buildQuotedStatusPageLink(pg);
var statusId = logStatus('Обрабатывается страница ' + pageLink + '...', null, { pending: true, trackError: false });
applyTemplateToPage(pg, job, function (err, meta) {
var normPg = normTitle(pg);
var isClose = job.mode === 'cleanup' || job.mode === 'denom';
if (!isClose) {
if (!err && meta && meta.successMessage) logStatus(meta.successMessage, null, { statusId: statusId, trackError: false });
else logPageEdit(pg, err, { statusId: statusId });
} else {
if (err) { logStatus('Завершение по странице ' + pageLink, err, { statusId: statusId }); }
else {
logStatus((meta && meta.successMessage) || ('Шаблон снят со страницы ' + pageLink + '.'), null, { statusId: statusId, trackError: false });
if (job.mode === 'denom') logStatus('Шаблон установлен на СО страницы ' + pageLink + '.', null, { trackError: false });
}
}
if (!err) {
notifiedPages.push(pg);
if (meta) pageMeta[normPg] = meta;
}
nextPage(err || null);
});
}, function (err) { onDone(notifiedPages, err, pageMeta); });
}
// ═══════════════════════════════════════════════════════════════════════════
// ПОСТРОЕНИЕ JOB из формы
// ═══════════════════════════════════════════════════════════════════════════
/**
* Строит объект job из данных формы для операции номинации (tRm, rnm, imp, merge, split, recov).
* @param {Object} op — запись из OPERATIONS
* @param {string} pg — целевая страница (уже разрешённая)
* @param {boolean} isMulti — режим мультиноминации
* @returns {Object|false} — job или false при ошибке ввода
*/
function buildNominationJob(op, pg, isMulti) {
var nom = op.nomination;
var date = getDate();
var msg = normalizeQuickPhraseValue($('#rmMsg').val());
var rawMsg = msg;
var opId = isMulti ? (nom.multiOpId || op.id) : op.id;
var tplpar = '';
var section, sectionNW, extraPages, multiArticles = [];
var multiHeaderText = '';
var multiArticleComments = {};
var multiFormat = 'sections';
var multiRenamePairs = [];
var multiRenameTargets = {};
var multiRenameTemplateTargets = {};
var isRenameWithRowTargets = isMulti && nom.extraInput && nom.extraInput.type === 'rename' && $('.rmMultiRenameTargetInput').length;
// Вычислить section и tplpar в зависимости от типа дополнительного ввода
if (nom.extraInput) {
var ei = nom.extraInput;
if (ei.type === 'rename') {
if (isRenameWithRowTargets) {
multiRenamePairs = collectMultiRenamePairs();
if (!validateMultiRenamePairs(multiRenamePairs, 'статью', 'новое название')) return false;
multiRenameTargets = buildMultiRenameTargetMap(multiRenamePairs, 'targetNames');
multiRenameTemplateTargets = buildMultiRenameTargetMap(multiRenamePairs, 'templateTargetNames');
tplpar = buildRenameTemplateParam(multiRenamePairs[0].templateTargetNames);
section = formatRenameItemLabel(pg, multiRenameTargets[normTitle(pg)] || multiRenamePairs[0].targetNames);
} else {
var rn = collectInputValues('.rmRenameInput');
if (!rn.length) { alert('Укажите новое название.'); return false; }
tplpar = buildRenameTemplateParam(rn);
section = formatRenameItemLabel(pg, rn);
}
} else if (ei.type === 'merge') {
var mn = collectInputValues('.rmMergeInput');
if (!mn.length) { alert('Укажите статью для объединения.'); return false; }
tplpar = pg + '|' + mn.join('|');
extraPages = mn;
section = formatPagesWithAnd([pg].concat(mn));
} else if (ei.type === 'split') {
var sn = collectInputValues('.rmSplitInput');
if (!sn.length) { alert('Укажите статьи для разделения.'); return false; }
tplpar = formatPagesWithAnd(sn);
section = '[[:' + pg + ']] → ' + tplpar;
}
}
if (isMulti) {
var ttl = $('#rmHeader').val() || '';
var articles = isRenameWithRowTargets
? multiRenamePairs.map(function (pair) { return pair.pageName; })
: collectInputValues('.rmMultiPageInput');
multiFormat = $('.rmArticleMultiFormatBtn.is-active').data('rmMultiFormat') || 'sections';
multiArticleComments = collectMultiNominationComments();
multiHeaderText = ttl;
multiArticles = articles.slice();
if (!articles.length) { alert('Укажите страницу.'); return false; }
if (!validateMultiNominationText(articles, rawMsg, multiArticleComments, 'страницы')) return false;
section = ttl || (isRenameWithRowTargets ? formatRenameItemsWithAnd(articles, multiRenameTargets) : '');
msg = multiFormat === 'list'
? buildMultiNominationListText(articles, rawMsg, multiArticleComments, isRenameWithRowTargets ? getMultiRenameDiscussionOptions(multiRenameTargets) : null)
: buildMultiNominationText(articles, rawMsg, multiArticleComments, isRenameWithRowTargets
? getMultiRenameDiscussionOptions(multiRenameTargets, { leadingBlankLine: false })
: { leadingBlankLine: false });
}
if (!section) section = '[[:' + pg + ']]';
sectionNW = section.replace(/\[\[:/g, '').replace(/]]/g, '');
var nomPageDate = date[1];
var nomPage = nom.nomPage(nomPageDate);
var summaryAction = getNominationSummaryAction(nom);
var summary = makeNominationTargetSummary(nomPage, sectionNW, summaryAction);
return {
mode: 'nominate',
opId: opId,
op: op,
date: date,
tplpar: tplpar,
articleTpl: nom.articleTpl || function () { return ''; },
inArticle: nom.inArticle !== false,
transferMode: (nom.supportsTransfer ? getTransferModeFromButtons() : 'none'),
summary: summary,
nominationSummary: makeNominationPageSummary(sectionNW),
summaryAction: summaryAction,
msg: msg,
nomPage: nomPage,
navTemplate: nom.navTemplate,
section: section,
sectionNW: sectionNW,
comment: nom.comment || '',
extraPages: extraPages || [],
isMulti: !!isMulti,
multiHeaderText: multiHeaderText,
multiNominationBody: rawMsg,
multiArticleComments: multiArticleComments,
multiNominationFormat: multiFormat || 'sections',
multiRenamePairs: multiRenamePairs,
multiRenameTargets: multiRenameTargets,
multiRenameTemplateTargets: multiRenameTemplateTargets,
multiArticles: multiArticles,
pages: isMulti ? multiArticles.slice().reverse() : ([pg].concat(extraPages || []))
};
}
function getTransferModeFromButtons() {
var kbu = $('#rmTransferBtnKbu').hasClass('is-active');
var kul = $('#rmTransferBtnKul').hasClass('is-active');
if (kbu && kul) return 'both';
if (kbu) return 'kbu';
if (kul) return 'kul';
return 'none';
}
function normalizeFastRemoveTemplateKey(templateName) {
return String(templateName || '').replace(/\s+/g, ' ').trim().toLowerCase();
}
function getBuiltinFastRemoveParamConfig(templateName) {
var key = normalizeFastRemoveTemplateKey(templateName);
var bareKey = key.replace(/^(?:подст|subst)\s*:\s*/, '');
if (/^(?:nld|no license|nsd|no source|nad|no author|dd|disputed|npd|no permission|ofud|orphaned-fairuse|nothost|db-nothost)$/.test(bareKey)) return { type: 'date' };
if (/^(?:dfud|disputed-fairuse)$/.test(bareKey)) return { type: 'dfud' };
if (key === 'db-badfairuse') return { type: 'badfairuse' };
if (key === 'rct' || key === 'замена с викисклада') return { type: 'commonsReplacement' };
if (key === 'nct' || key === 'перенесено на викисклад') return { type: 'nct' };
if (key === 'db-duplicate') return { type: 'duplicate' };
return null;
}
function buildKbuReasonButtonsHtml(items, buttonClass, dataAttr, titlePrefix) {
return items.map(function (item) {
var label = titlePrefix + ' ' + item[0] + ': ' + item[1];
return '<button type="button" class="rmToggleBtn rmKbuReasonPill ' + buttonClass + '" ' + dataAttr + '="' +
escapeHtml(item[0]) + '" aria-pressed="false" title="' + escapeHtml(label) +
'"><span class="rmKbuReasonNum">' + escapeHtml(item[0]) + '</span>' +
'<span class="rmKbuReasonText">' + escapeHtml(item[1]) + '</span></button>';
}).join('');
}
function buildDfudReasonButtonsHtml() {
return buildKbuReasonButtonsHtml(DFUD_REASON_OPTIONS, 'rmKbuDfudReasonBtn', 'data-rm-dfud-reason', 'п.');
}
function buildBadfairuseReasonButtonsHtml() {
return buildKbuReasonButtonsHtml(BADFAIRUSE_OPTIONS, 'rmKbuBadfairuseReasonBtn', 'data-rm-badfairuse-reason', 'пример');
}
function buildKbuReasonListHtml(id, hint, buttonsHtml) {
return '<div id="' + id + '" class="' + RESIZE_CLASS + '" style="display:none;margin-bottom:10px;">' +
'<div style="margin-bottom:6px;font-size:14px;line-height:1.4;color:' + tk.cSubM + ';">' + hint + '</div>' +
'<div style="display:flex;flex-direction:column;gap:4px;">' + buttonsHtml + '</div>' +
'</div>';
}
function collectSelectedDfudReasons() {
return $('#rmKbuDfudReasonsWrap .rmKbuDfudReasonBtn.is-active').map(function () {
return String($(this).attr('data-rm-dfud-reason') || '').trim();
}).get().filter(Boolean);
}
function getSelectedBadfairuseReason() {
return String($('#rmKbuBadfairuseReasonsWrap .rmKbuBadfairuseReasonBtn.is-active').first().attr('data-rm-badfairuse-reason') || '').trim();
}
function getCurrentCommonsReplacementMode() {
var mode = $('#rmKbuCommonsReplacementModeWrap .rmKbuCommonsReplacementModeBtn.is-active').data('rmCommonsReplacementMode');
return mode || 'file';
}
function setCurrentCommonsReplacementMode(mode) {
var value = mode || 'file';
$('#rmKbuCommonsReplacementModeWrap .rmKbuCommonsReplacementModeBtn').each(function () {
var active = $(this).data('rmCommonsReplacementMode') === value;
$(this).toggleClass('is-active', active).attr('aria-pressed', active ? 'true' : 'false');
});
if (value === 'category') {
$('#fiRm').attr('placeholder', 'Категория на Викискладе без Category:');
$('#fiRmExtra').attr('placeholder', 'Статья в руВики о теме категории (необязательно)').show();
} else {
$('#fiRmExtra').hide();
$('#fiRm').attr('placeholder', value === 'gallery' ? 'Галерея на Викискладе' : 'Имя файла на Викискладе');
}
syncModalLayout();
}
function getFastRemoveLegacyParamConfig(templateName) {
var params = cfg.requiredParamTemplates || {};
var key;
if (Object.prototype.hasOwnProperty.call(params, templateName)) return params[templateName];
for (key in params) {
if (Object.prototype.hasOwnProperty.call(params, key) &&
normalizeFastRemoveTemplateKey(key) === normalizeFastRemoveTemplateKey(templateName)) {
return params[key];
}
}
return null;
}
function getFastRemoveParamConfig(reason) {
var template = reason && reason[0];
return getBuiltinFastRemoveParamConfig(template) || getFastRemoveLegacyParamConfig(template);
}
function appendTemplateParam(tpl, name, value) {
var val = normalizeQuickPhraseValue(value);
if (!val) return tpl;
return tpl + '|' + (name ? name + '=' : '') + val;
}
function makeFastRemovalSummary(reason, data) {
var label = String(reason && reason[1] || '').trim();
var detail = String(data && data.summaryDetail || '').trim();
var summaryReason = (label === 'Особый случай' && detail) ? detail : (label || detail);
return makeSummary('номинация к быстрому удалению' + (summaryReason ? ': ' + summaryReason : ''));
}
function buildFastRemoveTemplateData(reason) {
var template = reason && reason[0];
var paramCfg = getFastRemoveParamConfig(reason);
var addInfo = normalizeQuickPhraseValue($('#fiRm').val());
var comment = normalizeQuickPhraseValue($('#fiRmComment').val());
var dateText = getDate()[0];
var mode;
var extra;
var tpl;
var values;
if (!template) return { error: 'Не выбрана причина быстрого удаления.' };
if (paramCfg && paramCfg.type === 'date') {
return { templateText: appendTemplateParam(template, '1', dateText) };
}
if (paramCfg && paramCfg.type === 'dfud') {
tpl = appendTemplateParam(template, 'date', dateText);
values = collectSelectedDfudReasons();
values.forEach(function (value, index) { tpl = appendTemplateParam(tpl, String(index + 1), value); });
return { templateText: tpl };
}
if (paramCfg && paramCfg.type === 'badfairuse') {
addInfo = getSelectedBadfairuseReason();
if (!addInfo) return { error: 'Выберите пример из ВП:ПНИ для Ф6.' };
return { templateText: appendTemplateParam(template, '1', addInfo) };
}
if (paramCfg && paramCfg.type === 'commonsReplacement') {
mode = getCurrentCommonsReplacementMode();
if (mode === 'category') {
if (!addInfo) return { error: 'Укажите категорию на Викискладе.' };
tpl = appendTemplateParam('Замена с Викисклада/Категория', '1', addInfo);
extra = normalizeQuickPhraseValue($('#fiRmExtra').val());
return { templateText: appendTemplateParam(tpl, '2', extra) };
}
if (mode === 'gallery') {
if (!addInfo) return { error: 'Укажите галерею на Викискладе.' };
return { templateText: appendTemplateParam('Замена с Викисклада/Галерея', '1', addInfo) };
}
return { templateText: appendTemplateParam(template, '1', addInfo) };
}
if (paramCfg && paramCfg.type === 'nct') {
return {
templateText: appendTemplateParam(template, '1', addInfo),
removeMoveToCommons: $('#rmKbuRemoveMoveToCommons').is(':checked')
};
}
if (paramCfg && paramCfg.type === 'duplicate') {
if (!addInfo) return { error: 'Укажите имя файла-оригинала для Ф1.' };
tpl = appendTemplateParam(template, '1', addInfo);
return { templateText: appendTemplateParam(tpl, 'comment', comment) };
}
tpl = template;
if (typeof paramCfg === 'string') {
var noComment = paramCfg.charAt(0) === '!';
tpl = appendTemplateParam(tpl, '1', addInfo);
if (!noComment) tpl = appendTemplateParam(tpl, addInfo ? '2' : '1', comment);
return { templateText: tpl, summaryDetail: noComment ? addInfo : '' };
}
return { templateText: appendTemplateParam(tpl, '1', comment) };
}
function resetKbuReasonControls() {
$('#fiRm').hide().val('').attr({ type: 'text', placeholder: '' });
$('#fiRmExtra').hide().val('');
$('#rmKbuCommonsReplacementModeWrap').hide();
$('#rmKbuDfudReasonsWrap').hide().find('.rmKbuDfudReasonBtn').removeClass('is-active').attr('aria-pressed', 'false');
$('#rmKbuBadfairuseReasonsWrap').hide().find('.rmKbuBadfairuseReasonBtn').removeClass('is-active').attr('aria-pressed', 'false');
$('#rmKbuDateHint').hide().text('');
$('#rmKbuRemoveMoveToCommons').prop('checked', false);
$('#rmKbuRemoveMoveToCommonsWrap').hide();
$('#fiRmComment').hide().val('');
$('.rmQuickPhrasesPanel[data-rm-target="fiRmComment"]').hide();
}
function toggleKbuComment(show) {
$('#fiRmComment').toggle(show);
$('.rmQuickPhrasesPanel[data-rm-target="fiRmComment"]').toggle(show);
}
function updateKbuReasonControls(reasons) {
var reason = reasons[$('#rmSel').val()] || reasons[0];
var paramCfg = getFastRemoveParamConfig(reason);
$('#rmKbuCriteriaLinkWrap').html(buildFastRemoveCriteriaLinkHtml(reason));
resetKbuReasonControls();
if (paramCfg && (paramCfg.type === 'date' || paramCfg.type === 'dfud')) {
$('#rmKbuDateHint').text(FILE_DATE_DEADLINE_HINT).show();
if (paramCfg.type === 'dfud') $('#rmKbuDfudReasonsWrap').show();
return;
}
if (paramCfg && paramCfg.type === 'badfairuse') {
$('#rmKbuBadfairuseReasonsWrap').show();
return;
}
if (paramCfg && paramCfg.type === 'commonsReplacement') {
$('#rmKbuCommonsReplacementModeWrap').show();
$('#fiRm').show();
setCurrentCommonsReplacementMode(getCurrentCommonsReplacementMode());
return;
}
if (paramCfg && paramCfg.type === 'nct') {
$('#fiRm').attr('placeholder', 'Укажите имя файла на Викискладе').show();
$('#rmKbuRemoveMoveToCommonsWrap').css('display', 'inline-flex');
return;
}
if (paramCfg && paramCfg.type === 'duplicate') {
$('#fiRm').attr('placeholder', 'Укажите имя файла-оригинала').show();
toggleKbuComment(true);
return;
}
if (typeof paramCfg === 'string') {
var noComment = paramCfg.charAt(0) === '!';
$('#fiRm').attr('placeholder', 'Укажите ' + (noComment ? paramCfg.substring(1) : paramCfg)).show();
toggleKbuComment(!noComment);
return;
}
toggleKbuComment(true);
}
function getKbuReasonGroupKey(reason) {
var label = String(reason && reason[1] || '');
var m = label.match(/^\s*([А-ЯЁA-Z]{1,3})\d/);
return m ? m[1] : label;
}
function buildKbuReasonOptionsHtml(reasons) {
var prevGroup = '';
var parts = [];
(reasons || []).forEach(function (reason, index) {
var group = getKbuReasonGroupKey(reason);
if (index && group !== prevGroup) parts.push('<hr>');
parts.push('<option value="' + index + '">' + escapeHtml(reason[1]) + '</option>');
prevGroup = group;
});
return parts.join('');
}
function buildKbuFormHtml(reasons) {
return joinHtml([
'<select id="rmSel" style="', stInputFull, '">',
buildKbuReasonOptionsHtml(reasons),
'</select>',
'<div id="rmKbuDateHint" class="', RESIZE_CLASS, '" style="display:none;margin:-2px 0 10px;font-size:12px;line-height:1.35;color:', tk.cSubM, ';"></div>',
'<div id="rmKbuCommonsReplacementModeWrap" class="', RESIZE_CLASS, '" style="display:none;margin-bottom:10px;"><div class="rmSegmentedBar">',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmKbuCommonsReplacementModeBtn is-active" data-rm-commons-replacement-mode="file" aria-pressed="true">Файл</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmKbuCommonsReplacementModeBtn" data-rm-commons-replacement-mode="category" aria-pressed="false">Категория</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmKbuCommonsReplacementModeBtn" data-rm-commons-replacement-mode="gallery" aria-pressed="false">Галерея</button>',
'</div></div>',
buildKbuReasonListHtml(
'rmKbuDfudReasonsWrap',
'Выберите пункты КДИ, которым файл не соответствует. Можно выбрать несколько пунктов.',
buildDfudReasonButtonsHtml()
),
buildKbuReasonListHtml(
'rmKbuBadfairuseReasonsWrap',
'Выберите пример из ВП:ПНИ, под который подпадает файл. Можно выбрать только один пункт.',
buildBadfairuseReasonButtonsHtml()
),
'<input id="fiRm" type="text" style="', stInputFull, 'display:none;">',
'<input id="fiRmExtra" type="text" style="', stInputFull, 'display:none;">',
'<label id="rmKbuRemoveMoveToCommonsWrap" class="', RESIZE_CLASS, '" style="', stFooterCheckLabel.replace('display:inline-flex;', 'display:none;'), 'margin:-2px 0 10px;">',
'<input id="rmKbuRemoveMoveToCommons" type="checkbox" style="margin:2px 0 0;flex-shrink:0;">',
'Снять шаблон «Перенести на Викисклад»',
'</label>',
'<input id="fiRmComment" type="text" placeholder="Комментарий (необязательно)" style="', stInputFull, '">',
buildQuickPhrasesPanelHtml('fiRmComment')
]);
}
function buildNominationMultiHeaderHtml(pg, options) {
var opts = options || {};
var multiHeaderRowStyle = stRow.replace('margin-bottom:6px;', 'margin-bottom:0;');
return joinHtml([
'<div id="rmMultiHeader" class="', RESIZE_CLASS, '" style="display:none;margin-bottom:6px;">',
'<div class="rmMultiPageRow rmNominationHeaderRow" style="', multiHeaderRowStyle, '"><input id="rmHeader" type="text" placeholder="Заголовок номинации" style="', stInputBox, '">',
buildAddMultiPageButtonHtml(opts), '</div>',
'</div>',
'<div id="rmMultiPagesContainer" style="display:flex;flex-direction:column;gap:', multiNominationGap, ';margin-bottom:', multiNominationGap, ';">',
buildMultiPageRowHtml(0, $.extend({}, opts, { rowId: 'rmFirstMultiPage', pageValue: pg, showAdd: true })),
'</div>'
]);
}
function buildTransferBoxHtml() {
return joinHtml([
'<div id="rmTransferBox" class="', RESIZE_CLASS, ' rmTransferPanel" style="width:100%;box-sizing:border-box;"><div class="rmTransferGrid">',
'<div id="rmTransferModeSingle" class="rmSegmentedBar"><button type="button" id="rmTransferBtnNone" class="rmSegmentedBtn rmToggleBtn is-active" aria-pressed="true">Обычная номинация</button></div>',
'<div id="rmTransferModeGroup" class="rmSegmentedBar">',
'<button type="button" id="rmTransferBtnKbu" class="rmSegmentedBtn rmToggleBtn" aria-pressed="false" title="Шаблоны db-*, уд-*, КОУ, Hangon.">Снять КБУ</button>',
'<button type="button" id="rmTransferBtnKul" class="rmSegmentedBtn rmToggleBtn" aria-pressed="false" title="Шаблон «К улучшению».">Снять КУЛ</button>',
'</div>',
'<div class="rmTransferHintRow"><div id="rmTransferHint" style="display:none;font-size:12px;line-height:1.35;color:', tk.cSubM, ';"></div></div>',
'</div></div>'
]);
}
function buildMultiNominationFormatSwitchHtml(wrapId, buttonClass) {
return joinHtml([
'<div id="', wrapId, '" class="', RESIZE_CLASS, '" style="display:none;margin-top:8px;margin-bottom:10px;">',
'<div class="rmSegmentedBar">',
'<button type="button" class="rmSegmentedBtn rmToggleBtn ', buttonClass, ' is-active" data-rm-multi-format="sections" aria-pressed="true">Оформить подразделами</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn ', buttonClass, '" data-rm-multi-format="list" aria-pressed="false">Оформить списком</button>',
'</div>',
'</div>'
]);
}
function getMultiNominationUiOptions(kind, options) {
var opts = options || {};
var isArticle = kind === 'article';
var isRename = !!opts.renameMulti;
var actionText = typeof opts.actionText === 'string'
? opts.actionText
: (opts.deletionMulti ? 'к удалению' : (isRename ? 'к переименованию' : ''));
var itemAcc = isArticle ? 'статью' : 'категорию';
var itemDat = isArticle ? 'статье' : 'категории';
var itemGen = isArticle ? 'статьи' : 'категории';
var result = {
inputPlaceholder: isArticle ? 'Статья' : 'Категория',
addTitle: 'Мультиноминация: добавить ' + itemAcc + ' в номинацию' + (actionText ? ' ' + actionText : ''),
removeTitle: 'Убрать ' + itemAcc + ' из номинации' + (actionText ? ' ' + actionText : ''),
commentTitle: 'Добавить комментарий к этой ' + itemDat,
commentExpandedTitle: 'Скрыть комментарий к этой ' + itemDat,
commentPlaceholder: 'Комментарий только для этой ' + itemGen + ' (необязательно)'
};
if (isRename) {
$.extend(result, {
targetInput: true,
targetInputClass: 'rmMultiRenameTargetInput',
targetPlaceholder: isArticle ? 'Новое название' : 'Новое название без префикса Категория:',
targetVariants: true
});
}
if (opts.setup) {
$.extend(result, {
defaultPage: opts.defaultPage || '',
multiOnlySelector: isArticle ? '#rmArticleMultiFormatWrap' : '#rmCategoryMultiFormatWrap'
});
if (isRename) $.extend(result, {
singleOnlySelector: '#rmSingleRenameBlock',
hideContainerWhenSingle: true,
singleCurrentPageSelector: '#rmSingleRenameCurrent',
singleRenameTargetSelector: isArticle ? '#rmRenameFirst' : '#firstRenameInput',
singleRenameVariantSelector: isArticle ? '#rmRenameContainer .rmRenameInput' : '#renameVariantsContainer .variantInput',
singleRenameVariantContainerId: isArticle ? 'rmRenameContainer' : 'renameVariantsContainer',
singleRenameVariantPlaceholder: isArticle ? 'Дополнительный вариант' : 'Дополнительный вариант названия',
singleRenameInputClass: isArticle ? 'rmRenameInput' : undefined
});
}
return result;
}
function buildNominationFormHtml(nom, pg, multiMode) {
var isRenameMulti = multiMode && nom.extraInput && nom.extraInput.type === 'rename';
var multiOptions = getMultiNominationUiOptions('article', {
renameMulti: isRenameMulti,
deletionMulti: multiMode && nom.template === 'к удалению'
});
return joinHtml([
isRenameMulti ? buildSingleRenameBlockHtml(nom.extraInput, 'Мультиноминация: добавить статью в номинацию', pg, 'Текущее название') : '',
multiMode ? buildNominationMultiHeaderHtml(pg, multiOptions) : '',
nom.extraInput && !isRenameMulti ? buildMultiInputHtml(nom.extraInput) : '',
'<textarea id="rmMsg" placeholder="Текст номинации без подписи"></textarea>',
buildQuickPhrasesPanelHtml('rmMsg'),
nom.supportsTransfer ? buildTransferBoxHtml() : '',
multiMode ? buildMultiNominationFormatSwitchHtml('rmArticleMultiFormatWrap', 'rmArticleMultiFormatBtn') : ''
]);
}
function buildCategoryNominationFormHtml(variantConfig, multiMode, pageName, options) {
var opts = options || {};
var multiOptions = getMultiNominationUiOptions('category', opts);
return joinHtml([
opts.renameMulti && variantConfig ? buildSingleRenameBlockHtml(variantConfig, 'Мультиноминация: добавить категорию в номинацию', pageName, 'Текущая категория') : '',
multiMode ? buildNominationMultiHeaderHtml(pageName, multiOptions) : '',
variantConfig && !opts.renameMulti && !opts.skipVariantInput ? buildMultiInputHtml(variantConfig) : '',
'<textarea id="nominationReason" placeholder="Текст номинации без подписи"></textarea>',
buildQuickPhrasesPanelHtml('nominationReason'),
multiMode ? buildMultiNominationFormatSwitchHtml('rmCategoryMultiFormatWrap', 'rmCategoryMultiFormatBtn') : ''
]);
}
function ensureMultiPageCommentTextareaResizer(textareaId, wrapId) {
var $textarea = $('#' + textareaId);
if (!$textarea.length || $textarea.data('rmNestedResizerReady')) return;
setupNestedResizableTextarea(textareaId, wrapId, parseInt(sz.taMinW, 10) || 180, 90);
$textarea.data('rmNestedResizerReady', true);
}
function setMultiPageCommentExpanded($btn, expanded) {
var wrapId = $btn.data('rmCommentWrap');
var textareaId = $btn.data('rmCommentTextarea');
var $wrap = $('#' + wrapId);
var title = expanded
? ($btn.data('rmCommentExpandedTitle') || 'Скрыть комментарий к этой странице')
: ($btn.data('rmCommentTitle') || 'Добавить комментарий к этой странице');
if (!$btn.length || !$wrap.length) return;
$btn.attr('aria-expanded', expanded ? 'true' : 'false')
.attr('aria-label', title)
.attr('title', title)
.toggleClass('is-active', expanded)
.text('✎');
$wrap.toggle(expanded);
if (expanded) ensureMultiPageCommentTextareaResizer(textareaId, wrapId);
}
function getMultiPageCommentTargets($block) {
var $textarea = $block.find('.rmMultiPageCommentInput').first();
var $wrap = $textarea.parent();
return {
wrapId: $wrap.attr('id') || '',
textareaId: $textarea.attr('id') || ''
};
}
function collectMultiRenameTargetValues($block) {
return $block.find('.rmMultiRenameTargetInput,.rmMultiRenameVariantInput').map(function () {
return ($(this).val() || '').trim();
}).get().filter(Boolean);
}
function setMultiPageRowControls($block, showAdd, showComment, options) {
var opts = options || {};
var $row = $block.find('.rmMultiPageRow').first();
var ids = getMultiPageCommentTargets($block);
var $commentBtn = $row.find('.rmMultiPageCommentToggle');
if (!$row.length) return;
if (showAdd) {
if ($commentBtn.length) setMultiPageCommentExpanded($commentBtn, false);
if (ids.wrapId) $('#' + ids.wrapId).hide();
$block.find('.rmMultiRenameVariantsContainer').hide();
$row.find('.rmMultiPageCommentToggle,.rmAddMultiRenameVariant,.rmRemoveInput').remove();
if (!$row.find('.rmAddMultiPage').length) $row.append(buildAddMultiPageButtonHtml(opts));
return;
}
$block.find('.rmMultiRenameVariantsContainer').toggle(!!opts.targetVariants);
$row.find('.rmAddMultiPage').remove();
if (!$row.find('.rmMultiPageCommentToggle').length) {
$row.append(buildMultiPageButtonsHtml(ids.wrapId, ids.textareaId, $.extend({}, opts, { showComment: showComment })));
return;
}
$row.find('.rmMultiPageCommentToggle').toggle(showComment);
}
function setupMultiPageNominationUi(options) {
var opts = options || {};
var containerSelector = opts.containerSelector || '#rmMultiPagesContainer';
var pageCounter = parseInt(opts.nextIndex, 10) || 1;
var wasMultiModeExpanded = false;
function restoreEmptySinglePageInput() {
var $pageInput = $(containerSelector + ' .rmMultiPageInput').first();
if (!$pageInput.length || String($pageInput.val() || '').trim()) return;
$pageInput.val(opts.defaultPage || '');
}
function copySingleCurrentPageToFirstRow() {
var $source = opts.singleCurrentPageSelector ? $(opts.singleCurrentPageSelector).first() : $();
var $target = $(containerSelector + ' .rmMultiPageBlock').first().find('.rmMultiPageInput').first();
var value = String(($source.val && $source.val()) || '').trim();
if (!$source.length || !$target.length || !value) return;
$target.val(value);
}
function copyFirstRowPageToSingleCurrent() {
var $target = opts.singleCurrentPageSelector ? $(opts.singleCurrentPageSelector).first() : $();
var $source = $(containerSelector + ' .rmMultiPageBlock').first().find('.rmMultiPageInput').first();
var value = String(($source.val && $source.val()) || '').trim();
if (!$source.length || !$target.length) return;
$target.val(value || opts.defaultPage || '');
}
function getSingleRenameTargets() {
var targets = [];
var $source = opts.singleRenameTargetSelector ? $(opts.singleRenameTargetSelector).first() : $();
if ($source.length && String($source.val() || '').trim()) targets.push(String($source.val() || '').trim());
if (opts.singleRenameVariantSelector) {
$(opts.singleRenameVariantSelector).each(function () {
var value = String($(this).val() || '').trim();
if (value) targets.push(value);
});
}
return targets.slice(0, 3);
}
function setMultiBlockRenameTargets($block, targets) {
var list = asNonEmptyArray(targets).slice(0, 3);
var $target = $block.find('.rmMultiRenameTargetInput').first();
var $container = $block.find('.rmMultiRenameVariantsContainer').first();
if (!$target.length) return;
$target.val(list[0] || '');
if (!$container.length) return;
$container.find('.rmMultiRenameVariantRow').remove();
list.slice(1).forEach(function (value) {
$container.append(buildMultiRenameVariantRowHtml(value, opts));
});
}
function copySingleRenameTargetsToFirstRow() {
var $block = $(containerSelector + ' .rmMultiPageBlock').first();
if (!$block.length) return;
setMultiBlockRenameTargets($block, getSingleRenameTargets());
}
function copyFirstRowRenameTargetsToSingle() {
var $target = opts.singleRenameTargetSelector ? $(opts.singleRenameTargetSelector).first() : $();
var $sourceBlock = $(containerSelector + ' .rmMultiPageBlock').first();
var targets = collectMultiRenameTargetValues($sourceBlock).slice(0, 3);
var $variantContainer = opts.singleRenameVariantContainerId ? $('#' + opts.singleRenameVariantContainerId) : $();
if (!$sourceBlock.length || !$target.length) return;
$target.val(targets[0] || '');
if (!$variantContainer.length) return;
$variantContainer.empty();
targets.slice(1).forEach(function (value) {
addInputRow({
containerId: opts.singleRenameVariantContainerId,
placeholder: opts.singleRenameVariantPlaceholder || 'Дополнительный вариант',
inputClass: opts.singleRenameInputClass,
rowClass: 'rmRenameVariantRow',
indentLeft: 0,
fitParentWidth: true,
prefixHtml: buildLeftRemoveButtonHtml('rmRemoveInput', 'Удалить'),
removeBeforeInput: true
});
$variantContainer.find('input').last().val(value);
});
}
function updateMultiMode() {
var $blocks = $(containerSelector + ' .rmMultiPageBlock');
var hasExtra = $blocks.length > 1;
var pageGap = hasExtra && opts.targetVariants ? '12px' : multiNominationGap;
$(containerSelector).css({
gap: pageGap,
marginBottom: pageGap
});
$('#rmMultiHeader').css('marginBottom', hasExtra ? pageGap : multiNominationGap).toggle(hasExtra);
if (opts.multiOnlySelector) $(opts.multiOnlySelector).toggle(hasExtra);
if (opts.singleOnlySelector) $(opts.singleOnlySelector).toggle(!hasExtra);
if (opts.hideContainerWhenSingle) $(containerSelector).toggle(hasExtra);
if (!hasExtra && wasMultiModeExpanded) {
restoreEmptySinglePageInput();
copyFirstRowPageToSingleCurrent();
copyFirstRowRenameTargetsToSingle();
}
$blocks.each(function (index) {
setMultiPageRowControls($(this), !hasExtra && index === 0, hasExtra, opts);
});
wasMultiModeExpanded = hasExtra;
syncModalLayout();
}
$(document).off('click.rmMultiPageAdd').on('click.rmMultiPageAdd', '.rmAddMultiPage', function () {
var wasSingle = $(containerSelector + ' .rmMultiPageBlock').length <= 1;
if (wasSingle) {
copySingleCurrentPageToFirstRow();
copySingleRenameTargetsToFirstRow();
}
$(containerSelector).append(buildMultiPageRowHtml(pageCounter++, opts));
updateMultiMode();
});
$(document).off('click.rmMultiRenameVariantAdd').on('click.rmMultiRenameVariantAdd', '.rmAddMultiRenameVariant', function () {
var $block = $(this).closest('.rmMultiPageBlock');
var $container = $block.find('.rmMultiRenameVariantsContainer').first();
var fieldCount = 1 + $block.find('.rmMultiRenameVariantInput').length;
if (fieldCount >= 3) { alert('Максимум 3 варианта переименования.'); return; }
$container.append(buildMultiRenameVariantRowHtml('', opts)).show();
syncModalLayout();
});
$(document).off('click.rmMultiRenameVariantRemove').on('click.rmMultiRenameVariantRemove', '.rmRemoveMultiRenameVariant', function () {
$(this).closest('.rmMultiRenameVariantRow').remove();
syncModalLayout();
});
$(document).off('click.rmMultiPageComment').on('click.rmMultiPageComment', '.rmMultiPageCommentToggle', function () {
var $btn = $(this);
if (!$btn.is(':visible')) return;
setMultiPageCommentExpanded($btn, $btn.attr('aria-expanded') !== 'true');
syncModalLayout();
});
$(document).off('click.rmMultiPageRemove').on('click.rmMultiPageRemove', '.rmMultiPageRow .rmRemoveInput', function () {
$(this).closest('.rmMultiPageBlock').remove();
updateMultiMode();
});
updateMultiMode();
return {
update: updateMultiMode,
isMulti: function () { return $(containerSelector + ' .rmMultiPageBlock').length > 1; }
};
}
function setExclusiveButtonActive($root, buttonSelector, $btn) {
$root.find(buttonSelector).removeClass('is-active').attr('aria-pressed', 'false');
$btn.addClass('is-active').attr('aria-pressed', 'true');
}
function bindMultiNominationFormatSwitch(rootSelector, buttonSelector) {
var $root = $(rootSelector);
$root.off('click.rmMultiFormat').on('click.rmMultiFormat', buttonSelector, function () {
setExclusiveButtonActive($root, buttonSelector, $(this));
});
}
function buildProtectAddButtonHtml() {
return buildSquareAddButtonHtml('', 'Добавить страницу', 'rmProtectAddPage');
}
function buildProtectPageRowHtml(id, pageName, isFirstRow) {
return joinHtml([
'<div', isFirstRow ? ' id="rmProtectFirstRow"' : '', ' class="rmProtectPageRow ', RESIZE_CLASS, '" style="', stRow, '">',
'<input id="', id, '" type="text" placeholder="Страница" class="rmProtectPageInput" style="', stInputBox, '"',
pageName ? ' value="' + escapeHtml(pageName) + '"' : '', '>',
isFirstRow
? buildProtectAddButtonHtml()
: '<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить">−</button>',
'</div>'
]);
}
function buildReportFormHtml(ctx, isZka) {
var reportTextPlaceholder = (isZka ? 'Введите текст запроса.' : 'Текст запроса (можно дополнить или изменить).') + ' Подпись будет добавлена автоматически.';
if (isZka) {
return joinHtml([
'<input id="rmReportHeader" type="text" placeholder="Тема/заголовок" style="', stInputFull, '" value="', escapeHtml(ctx.pageLink), '">',
'<textarea id="rmReportText" placeholder="', reportTextPlaceholder, '"></textarea>',
buildQuickPhrasesPanelHtml('rmReportText')
]);
}
return joinHtml([
'<div id="rmProtectModeBtns" class="', RESIZE_CLASS, '" style="margin-bottom:14px;"><div class="rmSegmentedBar">',
'<button id="rmProtectModeInstall" type="button" class="rmSegmentedBtn rmProtectModeBtn is-active" aria-pressed="true">🛡️ Установить защиту</button>',
'<button id="rmProtectModeRemove" type="button" class="rmSegmentedBtn rmProtectModeBtn" aria-pressed="false">📛 Снять защиту</button>',
'</div></div>',
'<div id="rmProtectMultiWrap" class="', RESIZE_CLASS, '">',
'<div id="rmProtectHeaderWrap" style="display:none;margin-bottom:6px;"><div class="rmProtectHeaderRow" style="', stRow.replace('margin-bottom:6px;', 'margin-bottom:0;'), '"><input id="rmProtectHeader" type="text" placeholder="Заголовок (для нескольких страниц)" style="', stInputBox, '">', buildProtectAddButtonHtml(), '</div></div>',
buildProtectPageRowHtml('rmProtectPage0', ctx.pageName, true),
'<div id="rmProtectPagesContainer"></div>',
'</div>',
'<div id="rmProtectLevelsWrap" class="', RESIZE_CLASS, ' rmProtectControlGroup"><div class="rmProtectControlLabel">Уровень защиты</div><div id="rmProtectLevels" class="rmSegmentedBar">',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="полузащиту">полузащита</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="стабилизацию">стабилизация</button>',
'</div></div>',
'<div id="rmProtectReasonsWrap" class="', RESIZE_CLASS, ' rmProtectControlGroup"><div class="rmProtectControlLabel">Причины</div><div id="rmProtectReasons" class="rmSegmentedBar">',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="война правок">война правок</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="неконсенсусные изменения">неконсенсусные изменения</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="вандализм">вандализм</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="популярная статья">популярная статья</button>',
'</div></div>',
'<div id="rmRemoveLevelsWrap" class="', RESIZE_CLASS, ' rmProtectControlGroup" style="display:none;"><div class="rmProtectControlLabel">Уровень защиты</div><div id="rmRemoveLevels" class="rmSegmentedBar">',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="полузащиту">полузащита</button>',
'<button type="button" class="rmSegmentedBtn rmToggleBtn rmProtectOptBtn" data-label="стабилизацию">стабилизация</button>',
'</div></div>',
'<div id="rmProtectTextBlock" class="', RESIZE_CLASS, '"><textarea id="rmReportText" placeholder="', reportTextPlaceholder, '"></textarea>',
buildQuickPhrasesPanelHtml('rmReportText'), '</div>'
]);
}
// ═══════════════════════════════════════════════════════════════════════════
// ОБРАБОТЧИКИ ОПЕРАЦИЙ
// ═══════════════════════════════════════════════════════════════════════════
var handlers = {
// ── КБУ ─────────────────────────────────────────────────────────────
showKbu: function (op) {
var forCategory = !!(op && op.forCategory);
var reasons = getFastRemoveReasons();
createModal({
title: 'Быстрое удаление',
width: 'compact',
subtitleHtml: '<span id="rmKbuCriteriaLinkWrap"></span>'
});
$('#removerModalContent').html(buildKbuFormHtml(reasons));
$('#rmSel').change(function () { updateKbuReasonControls(reasons); syncModalLayout(); });
$('#removerModalContent').off('click.rmKbuCommonsReplacementMode').on('click.rmKbuCommonsReplacementMode', '.rmKbuCommonsReplacementModeBtn', function () {
setCurrentCommonsReplacementMode($(this).data('rmCommonsReplacementMode') || 'file');
});
$('#removerModalContent').off('click.rmKbuDfudReason').on('click.rmKbuDfudReason', '.rmKbuDfudReasonBtn', function () {
var $btn = $(this);
var active = !$btn.hasClass('is-active');
$btn.toggleClass('is-active', active).attr('aria-pressed', active ? 'true' : 'false');
});
$('#removerModalContent').off('click.rmKbuBadfairuseReason').on('click.rmKbuBadfairuseReason', '.rmKbuBadfairuseReasonBtn', function () {
setExclusiveButtonActive($('#rmKbuBadfairuseReasonsWrap'), '.rmKbuBadfairuseReasonBtn', $(this));
});
updateKbuReasonControls(reasons);
syncModalLayout();
renderModalFooter('submit', {
submitText: 'Номинировать',
onSubmit: function () {
var idx = $('#rmSel').val();
var reason = reasons[idx] || reasons[0];
var kbuData = buildFastRemoveTemplateData(reason);
if (kbuData.error) {
alert(kbuData.error);
return false;
}
var fastRemovalSummary = makeFastRemovalSummary(reason, kbuData);
startProcessing();
if (forCategory) {
var tpl = kbuData.templateText;
var categorySummary = fastRemovalSummary;
editPageContent(mwCfg.wgPageName, { summary: categorySummary, readError: 'Не удалось получить содержимое.' },
function (text) { return { text: wrapInNoinclude(text, T_OPEN + tpl + T_CLOSE) }; },
function (err) {
if (err) {
unlockModalSubmit();
logStatus('Ошибка записи.', err);
} else {
logStatus('Страница номинирована к быстрому удалению.', null, { trackError: false });
finalizeFastRemoval([normTitle(mwCfg.wgPageName)], categorySummary);
}
});
} else {
var job = {
mode: 'nominate', opId: 'fRm',
kbuTemplate: reason[0], kbuTemplateText: kbuData.templateText,
removeMoveToCommons: !!kbuData.removeMoveToCommons,
summary: fastRemovalSummary,
inArticle: true
};
processPageList([normTitle(mwCfg.wgPageName)], job, function (notifiedPages) {
finalizeFastRemoval(notifiedPages, job.summary);
});
}
return true;
}
});
},
// ── Универсальная номинация (КУ, КПМ, КУЛ, КОБ, КРАЗД, ВУС) ────────
showNomination: function (op) {
var nom = op.nomination;
var pg = normTitle(mwCfg.wgPageName);
var date = getDate()[1];
var nomPage = nom.nomPage(date);
var multiMode = nom.supportsMulti;
function updateTransferUi() {
var mode = getTransferModeFromButtons();
var isNone = mode === 'none';
var isKbu = mode === 'kbu' || mode === 'both';
var isKul = mode === 'kul' || mode === 'both';
$('#rmTransferBtnNone').toggleClass('is-active', isNone).attr('aria-pressed', isNone ? 'true' : 'false');
$('#rmTransferBtnKbu').toggleClass('is-active', isKbu).attr('aria-pressed', isKbu ? 'true' : 'false');
$('#rmTransferBtnKul').toggleClass('is-active', isKul).attr('aria-pressed', isKul ? 'true' : 'false');
var t = transferTexts[mode];
if (t) { $('#rmTransferHint').text(t.hint).show(); } else { $('#rmTransferHint').hide().text(''); }
applyGeneratedText($('#rmMsg'), t && t.notice ? t.notice + '\n' : '');
}
createModal({
title: 'Номинация: ' + nom.template,
subtitlePage: nomPage,
subtitleLabel: 'Текущий день'
});
$('#removerModalContent').html(buildNominationFormHtml(nom, pg, multiMode));
setupResizableModal('rmMsg');
// Логика переноса
if (nom.supportsTransfer) {
$(document).off('click.rmTransfer').on('click.rmTransfer', '#rmTransferBtnNone,#rmTransferBtnKbu,#rmTransferBtnKul', function () {
if (this.id === 'rmTransferBtnNone') {
$('#rmTransferBtnKbu,#rmTransferBtnKul').removeClass('is-active');
$('#rmTransferBtnNone').addClass('is-active');
} else {
$(this).toggleClass('is-active');
var anyOn = $('#rmTransferBtnKbu').hasClass('is-active') || $('#rmTransferBtnKul').hasClass('is-active');
$('#rmTransferBtnNone').toggleClass('is-active', !anyOn);
}
updateTransferUi();
});
updateTransferUi();
}
// Многостраничный режим
if (multiMode) {
setupMultiPageNominationUi(getMultiNominationUiOptions('article', {
setup: true,
defaultPage: pg,
renameMulti: nom.extraInput && nom.extraInput.type === 'rename',
deletionMulti: nom.template === 'к удалению'
}));
bindMultiNominationFormatSwitch('#removerModalContent', '.rmArticleMultiFormatBtn');
}
if (nom.extraInput) wireMultiInput(nom.extraInput);
renderModalFooter('submit', {
submitText: 'Номинировать',
showSubscribe: true,
onSubmit: function () {
var isMulti = multiMode && $('#rmMultiPagesContainer .rmMultiPageBlock').length > 1;
var singleCurrentInput = $('#rmSingleRenameCurrent').val() || '';
var inputVal = !isMulti ? normTitle(singleCurrentInput || $('#rmMultiPagesContainer .rmMultiPageInput').first().val() || '') : '';
var changed = inputVal && inputVal !== pg;
function executeJob(job) {
startProcessing();
runFlow({
templateStep: function (next) {
if (!job.inArticle) { next(); return; }
processPageList(job.pages, job, function (notifiedPages, err) {
job._notifiedPages = notifiedPages;
next(err);
});
},
nominationStep: function (done) {
publishNomination({
pageTitle: job.nomPage,
navTemplate: job.navTemplate,
sectionTitle: job.section,
summary: job.nominationSummary || job.summary,
text: getNominationPublishText(job)
}, function (err) { done(err, { pageTitle: job.nomPage, sectionTitle: job.section }); });
},
notifyStep: function (nominationInfo, next) {
var pages = job._notifiedPages || [];
if (!setAlert || !pages.length) { next(); return; }
notifyAuthorsForPages(pages, {
summary: job.summary,
actionText: job.comment,
discussionPage: nominationInfo && nominationInfo.pageTitle,
discussionSection: nominationInfo && nominationInfo.sectionTitle
}, next);
},
skipLink: op.id === 'fRm'
});
}
function run(targetPg) {
var job = buildNominationJob(op, targetPg, isMulti);
if (!job) { unlockModalSubmit(); return; }
if (job.isMulti && job.inArticle && getNominationConflictRule(job)) {
startProcessing();
inspectMultiNominationConflicts(job, function (err, conflicts) {
if (err) { markSubmitError(); return; }
if (!conflicts.length) { executeJob(job); return; }
showNominationConflictResolution(job, conflicts, function (resolvedJob) {
executeJob(resolvedJob);
});
});
return;
}
executeJob(job);
}
if (changed) {
apiReq({ prop: 'info', titles: inputVal }, 'query', function (data) {
if (data && data.error) {
unlockModalSubmit();
alert('Не удалось проверить страницу из-за ошибки API. Попробуйте ещё раз.');
return;
}
var page = getFirstQueryPage(data);
if (!page) {
unlockModalSubmit();
alert('Не удалось проверить страницу из-за временной ошибки. Попробуйте ещё раз.');
return;
}
if (page.missing !== undefined) {
unlockModalSubmit();
alert('Страница «' + inputVal + '» не существует.');
return;
}
run(normTitle(page.title || inputVal));
});
} else {
run(pg);
}
return true;
}
});
},
// ── Снятие номинации (статья) ────────────────────────────────────────
showArticleClose: function () {
showCloseActionsModal({
inputName: 'rmCloseAction',
listId: 'rmCloseActions',
emptyText: 'Не найдено подходящих шаблонов для закрытия.',
emptyDetails: 'Проверяются: КУ, КПМ, КБУ, КУЛ.',
getActions: function (articleText) {
var actions = [];
var hasKu = RE_KU_ON_PAGE.test(articleText);
var hasKpm = RE_KPM_ON_PAGE.test(articleText);
var hasKbu = RE_KBU_ON_PAGE.test(articleText);
var hasKul = RE_KUL_ON_PAGE.test(articleText);
if (hasKu) {
actions.push({ id: 'ret', tag: 'КУ', label: 'Оставлено', mode: 'denom', closeType: 'ret', resultTemplate: 'Оставлено', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{Оставлено}} на СО.', comment: 'оставлена', talkNotice: true });
actions.push({ id: 'retConditional', tag: 'КУ', label: 'Условно оставлено', mode: 'denom', closeType: 'retConditional', resultTemplate: 'Условно оставлено', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{Условно оставлено}} на СО.', comment: 'условно оставлена', talkNotice: true, needsConditionalFields: true });
actions.push({ id: 'withdrawnDeletion', tag: 'КУ', label: 'Снято с удаления', mode: 'denom', closeType: 'withdrawnDeletion', resultTemplate: 'Снято с удаления', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, добавляет {{Снято с удаления}} на СО.', comment: 'снята с удаления', talkNotice: true });
actions.push({ id: 'redirectedDeletion', tag: 'КУ', label: 'Заменено перенаправлением', mode: 'denom', closeType: 'redirectedDeletion', resultTemplate: 'Заменено перенаправлением', sourceTemplate: 'к удалению|ку', description: 'Снимает шаблон КУ, заменяет страницу перенаправлением, добавляет {{Заменено перенаправлением}} на СО.', warning: 'Осторожно: это действие перезапишет содержимое всей страницы.', comment: 'заменена перенаправлением', talkNotice: true, needsRedirectTarget: true });
}
if (hasKpm) {
actions.push({ id: 'doneRnm', tag: 'КПМ', label: 'Переименовано', mode: 'denom', closeType: 'doneRnm', resultTemplate: 'Переименовано', sourceTemplate: 'к переименованию|кпм|rename', description: 'Снимает шаблон КПМ, добавляет {{Переименовано}} на СО.', comment: 'переименована', talkNotice: true, needsOldTitle: true });
actions.push({ id: 'noRnm', tag: 'КПМ', label: 'Не переименовано', mode: 'denom', closeType: 'noRnm', resultTemplate: 'Не переименовано', sourceTemplate: 'к переименованию|кпм|rename', description: 'Снимает шаблон КПМ, добавляет {{Не переименовано}} на СО.', comment: 'не переименована', talkNotice: true });
}
if (hasKbu && hasKul) actions.push({ id: 'cleanup-both', tag: 'КБУ и КУЛ', label: 'Снятие', mode: 'cleanup', transferMode: 'both', cleanupLabel: 'КБУ и КУЛ', description: 'Снимает шаблоны КБУ/КУЛ и Hangon.' });
if (hasKbu) actions.push({ id: 'cleanup-kbu', tag: 'КБУ', label: 'Снятие', mode: 'cleanup', transferMode: 'kbu', cleanupLabel: 'КБУ', description: 'Снимает шаблоны КБУ и Hangon.' });
if (hasKul) actions.push({ id: 'cleanup-kul', tag: 'КУЛ', label: 'Снятие', mode: 'cleanup', transferMode: 'kul', cleanupLabel: 'КУЛ', description: 'Снимает шаблон КУЛ.' });
return actions;
},
afterRender: function (actions) {
var hasDoneRnm = actions.some(function (a) { return a.id === 'doneRnm'; });
var hasConditionalRet = actions.some(function (a) { return a.id === 'retConditional'; });
var hasRedirectedDeletion = actions.some(function (a) { return a.id === 'redirectedDeletion'; });
if (hasDoneRnm) {
$('#rmCloseActions input[value="doneRnm"]').closest('.rmActionItem').append(
'<div id="rmCloseOldTitleWrap" style="display:none;margin-top:6px;"><input id="rmCloseOldTitle" type="text" placeholder="Старое название" style="' + stInputFull + '"></div>'
);
}
if (hasConditionalRet) {
$('#rmCloseActions input[value="retConditional"]').closest('.rmActionItem').append(buildConditionalRetFieldsHtml());
}
if (hasRedirectedDeletion) {
$('#rmCloseActions input[value="redirectedDeletion"]').closest('.rmActionItem').append(
'<div id="rmCloseRedirectTargetWrap" style="display:none;margin-top:6px;"><input id="rmCloseRedirectTarget" type="text" placeholder="Цель перенаправления" style="' + stInputFull + '"></div>'
);
}
},
afterFooterRender: function (_, actionMap) {
function ensureConditionalTextareaResizer() {
var $textarea = $('#rmCloseConditionalReason');
if (!$textarea.length || $textarea.data('rmConditionalResizerReady')) return;
setupNestedResizableTextarea('rmCloseConditionalReason', 'rmCloseConditionalWrap', 280, 90);
$textarea.data('rmConditionalResizerReady', true);
}
function updateUi() {
var sel = actionMap[$('[name="rmCloseAction"]:checked').val()];
$('#rmCloseOldTitleWrap').toggle(!!(sel && sel.needsOldTitle));
$('#rmCloseConditionalWrap').toggle(!!(sel && sel.needsConditionalFields));
$('#rmCloseRedirectTargetWrap').toggle(!!(sel && sel.needsRedirectTarget));
if (sel && sel.needsConditionalFields) ensureConditionalTextareaResizer();
var disableNotify = !!(sel && sel.mode === 'cleanup' && sel.transferMode === 'kbu');
var $cb = $('[name="rmUAlert"]');
var $cbLabel = $('[name="rmUAlert"]').closest('label');
if ($cb.length) $cb.prop('disabled', disableNotify);
if ($cbLabel.length) $cbLabel.css({
visibility: disableNotify ? 'hidden' : 'visible',
pointerEvents: disableNotify ? 'none' : ''
});
syncModalLayout();
}
$(document).off('change.rmCloseAction').on('change.rmCloseAction', '[name="rmCloseAction"]', updateUi);
updateUi();
},
onSubmit: function (sel, pageName) {
var job;
if (sel.mode === 'denom') {
var oldTitle = sel.needsOldTitle ? ($('#rmCloseOldTitle').val() || '').trim() : '';
var conditionalReason = sel.needsConditionalFields ? normalizeQuickPhraseValue($('#rmCloseConditionalReason').val()) : '';
var conditionalDeadline = sel.needsConditionalFields ? String($('#rmCloseConditionalDeadline').val() || '').trim() : '';
var redirectTarget = sel.needsRedirectTarget ? normalizeRedirectTarget($('#rmCloseRedirectTarget').val()) : '';
if (sel.needsOldTitle && !oldTitle) { alert('Укажите старое название.'); return false; }
if (sel.needsRedirectTarget && !redirectTarget) { alert('Укажите цель перенаправления.'); return false; }
if (sel.needsRedirectTarget && normTitle(redirectTarget) === normTitle(pageName)) { alert('Цель перенаправления совпадает с текущей страницей.'); return false; }
if (conditionalDeadline && normalizeIsoDate(conditionalDeadline) !== conditionalDeadline) {
alert('Укажите срок в формате YYYY-MM-DD, например 2026-05-31.');
return false;
}
job = {
mode: 'denom',
closeType: sel.closeType,
resultTemplate: sel.resultTemplate,
sourceTemplate: sel.sourceTemplate,
oldTitle: oldTitle,
redirectTarget: redirectTarget,
conditionalReason: conditionalReason,
conditionalDeadline: conditionalDeadline,
notifyActionText: sel.comment,
skipNotify: false
};
} else {
job = {
mode: 'cleanup',
transferMode: sel.transferMode,
summary: makeSummary('снятие шаблонов ' + sel.cleanupLabel),
notifyActionText: (sel.transferMode === 'kul' || sel.transferMode === 'both')
? 'больше не номинирована к срочному улучшению'
: '',
skipNotify: !(sel.transferMode === 'kul' || sel.transferMode === 'both')
};
}
processPageList([pageName], job, function (notifiedPages, err, pageMeta) {
function finishClose() {
if (isError) { markSubmitError(); }
else { renderModalFooter('reload'); }
}
if (isError || err || job.skipNotify || !setAlert || !notifiedPages.length) { finishClose(); return; }
var meta = (pageMeta && pageMeta[normTitle(pageName)]) || {};
notifyAuthorsForPages(notifiedPages, {
summary: meta.summary || job.summary,
actionText: job.notifyActionText,
discussionPage: meta.discussionPage,
discussionSection: meta.discussionSection,
includeProposedPrefix: false
}, finishClose);
});
return true;
}
});
},
// ── ОБКАТ: номинация категории ───────────────────────────────────────
showCatNomination: function (op) {
var catType = op.catType;
var catMeta = CATEGORY_NOMINATION_META[catType] || CATEGORY_NOMINATION_META.discuss;
var now = new Date();
var catDiscPage = 'Википедия:Обсуждение категорий/' + MONTHS_NOM[now.getUTCMonth()] + '_' + now.getUTCFullYear();
var pageName = normalizeCategoryPageName(mwCfg.wgPageName);
var multiMode = !!catMeta.supportsMulti;
var renameMultiMode = !!catMeta.renameMulti && multiMode;
createModal({ title: catMeta.title, subtitlePage: catDiscPage, subtitleLabel: 'Текущий месяц' });
var variantCfgs = {
rename: { type: 'rename', firstId: 'firstRenameInput', addBtnId: 'addRenameVariant', containerId: 'renameVariantsContainer', addBtnLabel: '+ Добавить вариант', firstPh: 'Новое название без префикса Категория:', addPh: 'Дополнительный вариант названия', maxRows: 2, maxMsg: 'Максимум 3 варианта переименования.' },
merge: { firstId: 'firstMergeInput', addBtnId: 'addMergeVariant', containerId: 'mergeVariantsContainer', addBtnLabel: '+ Добавить вариант', firstPh: 'Категория для объединения без префикса Категория:', addPh: 'Дополнительный вариант объединения' }
};
var vCfg = variantCfgs[catType];
var categoryMultiOptions = {
renameMulti: renameMultiMode,
actionText: catMeta.actionText,
skipVariantInput: renameMultiMode
};
$('#removerModalContent').html(buildCategoryNominationFormHtml(vCfg, multiMode, pageName, categoryMultiOptions));
setupResizableModal('nominationReason');
if (multiMode) {
setupMultiPageNominationUi(getMultiNominationUiOptions('category', $.extend({
setup: true,
defaultPage: pageName
}, categoryMultiOptions)));
bindMultiNominationFormatSwitch('#removerModalContent', '.rmCategoryMultiFormatBtn');
}
if (vCfg) wireMultiInput(vCfg);
renderModalFooter('submit', {
submitText: 'Номинировать',
showSubscribe: true,
onSubmit: function () {
var reason = normalizeQuickPhraseValue($('#nominationReason').val());
var hasMultiRenameRows = renameMultiMode && $('#rmMultiPagesContainer .rmMultiPageBlock').length > 1;
var renamePairs = hasMultiRenameRows ? collectMultiRenamePairs({
normalizePageName: normalizeCategoryPageName,
normalizeTargetName: normalizeCategoryTargetPageName,
normalizeTemplateTargetName: normalizeCategoryTargetName
}) : [];
var renameTargetsByCategory = buildMultiRenameTargetMap(renamePairs, 'targetNames');
var renameTemplateTargetsByCategory = buildMultiRenameTargetMap(renamePairs, 'templateTargetNames');
var singleRenameCategory = renameMultiMode && !hasMultiRenameRows
? normalizeCategoryPageName($('#rmSingleRenameCurrent').val() || pageName)
: '';
var targetPages = hasMultiRenameRows
? renamePairs.map(function (pair) { return pair.pageName; })
: (multiMode ? collectCategoryPageInputValues('.rmMultiPageInput') : [pageName]);
if (renameMultiMode && !hasMultiRenameRows) targetPages = [singleRenameCategory || pageName];
var isMulti = renameMultiMode ? hasMultiRenameRows : (multiMode && targetPages.length > 1);
var multiFormat = $('.rmCategoryMultiFormatBtn.is-active').data('rmMultiFormat') || 'sections';
var commentsByCategory = isMulti ? collectMultiNominationComments(normalizeCategoryPageName) : {};
var notifiedPages = [];
if (hasMultiRenameRows && !validateMultiRenamePairs(renamePairs, 'категорию', 'новое название')) return false;
if (!targetPages.length) { alert('Укажите категорию.'); return false; }
if (isMulti) {
if (!validateMultiNominationText(targetPages, reason, commentsByCategory, 'категории')) return false;
} else if (!reason) {
alert('Пожалуйста, укажите причину/тему.');
return false;
}
var discussionTarget = isMulti ? targetPages : targetPages[0];
var discussionReason = isMulti
? (multiFormat === 'list'
? buildMultiNominationListText(targetPages, reason, commentsByCategory, renameMultiMode ? getMultiRenameDiscussionOptions(renameTargetsByCategory) : null)
: buildMultiNominationText(targetPages, reason, commentsByCategory, renameMultiMode
? getMultiRenameDiscussionOptions(renameTargetsByCategory, { headingLevel: 4 })
: { headingLevel: 4 }))
: reason;
var discussionOptions = isMulti ? {
headerText: $('#rmHeader').val(),
reasonIsPrepared: true,
renameTargetsByPage: renameTargetsByCategory
} : null;
var mainName = null, additionalNames = [];
if (vCfg) {
if (hasMultiRenameRows) {
mainName = renamePairs[0] && renamePairs[0].templateTargetName;
additionalNames = renamePairs[0] ? asNonEmptyArray(renamePairs[0].templateTargetNames).slice(1) : [];
} else {
mainName = $('#' + vCfg.firstId).val().trim();
if (!mainName) { alert('Укажите ' + (catType === 'rename' ? 'новое название' : 'категорию для объединения') + '.'); return false; }
additionalNames = collectInputValues('#' + vCfg.containerId + ' .variantInput');
}
}
var categoryDiscussionTitle = buildCategoryDiscussionTitle(discussionTarget, catType, mainName, additionalNames, discussionOptions || {});
var categorySectionTitle = getCategoryDiscussionSectionTitle(categoryDiscussionTitle);
var categoryTemplateOptions = hasMultiRenameRows ? { renameTemplateTargetsByPage: renameTemplateTargetsByCategory } : {};
categoryTemplateOptions.summary = makeNominationTargetSummary(catDiscPage, categorySectionTitle, catMeta.actionText);
startProcessing();
runFlow({
templateStep: function (next) {
addTemplatesToCategories(targetPages, catType, mainName, additionalNames, function (err, processedPages) {
notifiedPages = processedPages || [];
next(err);
}, categoryTemplateOptions);
},
nominationStep: function (done) {
createCategoryDiscussion(discussionTarget, discussionReason, catType, function (err, nominationInfo) { done(err, nominationInfo || null); }, mainName, additionalNames, discussionOptions);
},
notifyStep: function (nominationInfo, next) {
if (!setAlert || !nominationInfo) { next(); return; }
var section = normalizeSectionForLink(nominationInfo.sectionTitle || '');
notifyAuthorsForPages(notifiedPages.length ? notifiedPages : targetPages, {
summary: makeSummary('номинация [[' + nominationInfo.pageTitle + (section ? '#' + section : '') + ']]'),
actionText: catMeta.actionText,
discussionPage: nominationInfo.pageTitle,
discussionSection: nominationInfo.sectionTitle
}, next);
}
});
return true;
}
});
},
// ── Снятие номинации (категория) ─────────────────────────────────────
showCatClose: function () {
showCloseActionsModal({
inputName: 'rmCategoryCloseAction',
showCheckbox: false,
emptyText: 'Не найдено подходящих шаблонов для завершения.',
emptyDetails: 'Проверяются ОБКАТ и КУ.',
getActions: function (catText) {
var allObkat = [cfg.categoryTemplates.discuss, cfg.categoryTemplates.rename, cfg.categoryTemplates.merge].join('|');
var actions = [];
if (new RegExp('\\{\\{\\s*(?:' + allObkat + ')\\s*(?:\\||\\}\\})', 'i').test(catText)) {
actions.push({ id: 'cat-obkat-done', tag: 'ОБКАТ', label: 'Завершено', mode: 'obkat', talkTemplate: 'Обсуждавшаяся категория', description: 'Снимает шаблон ОБКАТ, добавляет {{Обсуждавшаяся категория}} на СО.', talkNotice: true });
}
if (RE_KU_ON_PAGE.test(catText)) {
actions.push({ id: 'cat-ku-cleanup', tag: 'КУ', label: 'Снятие', mode: 'cleanup', description: 'Снимает шаблон КУ без записи на СО.' });
}
return actions;
},
onSubmit: function (sel, pageName) {
if (sel.mode === 'obkat') markCategoryDiscussionAsDone(pageName);
if (sel.mode === 'cleanup') removeKuFromCategory(pageName);
return true;
}
});
},
// ── Защита / Запрос к администраторам ───────────────────────────────
showReport: function (op) {
var mode = op.reportMode || 'protect';
var ctx = getReporterContext(mode);
var isZka = mode === 'request';
var protectMode = 'install';
var pageCounter = 1;
function buildProtectText(pm) {
if (pm === 'remove') {
var removeLevels = [];
$('#rmRemoveLevels .rmToggleBtn.is-active').each(function () { removeLevels.push($(this).data('label')); });
return removeLevels.length ? 'Просьба снять ' + removeLevels.join(' и/или ') + '.' : '';
}
var levels = [], reasons = [];
$('#rmProtectLevels .rmToggleBtn.is-active').each(function () { levels.push($(this).data('label')); });
$('#rmProtectReasons .rmToggleBtn.is-active').each(function () { reasons.push($(this).data('label')); });
if (!levels.length && !reasons.length) return '';
var text = 'Просьба установить';
if (levels.length) text += ' ' + levels.join(' и/или ');
if (reasons.length) text += ' по причине: ' + reasons.join(', ');
return text + '.';
}
function applyProtectMode(m) {
protectMode = m;
var isInstall = m === 'install';
$('#rmProtectModeInstall').toggleClass('is-active', isInstall).attr('aria-pressed', isInstall ? 'true' : 'false');
$('#rmProtectModeRemove').toggleClass('is-active', !isInstall).attr('aria-pressed', !isInstall ? 'true' : 'false');
$('#removerModalTitleText').text(isInstall ? 'Запрос на защиту страницы' : 'Запрос на снятие защиты');
var linkPage = isInstall ? 'Википедия:Установка защиты' : 'Википедия:Снятие защиты';
$('#rmProtectLinkWrap').html('<a href="' + getPageUrl(linkPage) + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">' + escapeHtml(linkPage) + '</a>');
$('#rmProtectLevelsWrap,#rmProtectReasonsWrap').toggle(isInstall);
$('#rmRemoveLevelsWrap').toggle(!isInstall);
$('#rmProtectLevels .rmProtectOptBtn,#rmProtectReasons .rmProtectOptBtn,#rmRemoveLevels .rmProtectOptBtn').removeClass('is-active');
$('#rmReportText').val('').removeData('rmGenerated');
}
function updateProtectMultiUi() {
var $rows = $('#rmProtectMultiWrap .rmProtectPageRow');
var hasExtra = $rows.length > 1;
if (!$rows.length) {
$('#rmProtectPagesContainer').append(buildProtectPageRowHtml('rmProtectPage' + pageCounter++, ctx.pageName, true));
$rows = $('#rmProtectMultiWrap .rmProtectPageRow');
hasExtra = false;
}
$('#rmProtectHeaderWrap').toggle(hasExtra);
if (!hasExtra) $('#rmProtectHeader').val('');
$rows.each(function () {
var $row = $(this);
$row.find('.rmProtectAddPage,.rmRemoveInput').remove();
$row.append(hasExtra
? '<button type="button" class="rmRemoveInput" style="' + stRemoveBtn + '" title="Удалить">−</button>'
: buildProtectAddButtonHtml()
);
});
syncModalLayout();
}
createModal({
title: isZka ? 'Запрос к администраторам' : 'Запрос на защиту страницы',
width: 'compact',
subtitleHtml: isZka
? '<a href="' + getPageUrl('Википедия:Запросы к администраторам') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Википедия:Запросы к администраторам</a>' +
' · <a href="' + getPageUrl('Википедия:Запросы к администраторам/Быстрые') + '" target="_blank" rel="noopener noreferrer" class="removerModalLink">Быстрые</a>'
: '<span id="rmProtectLinkWrap"></span>'
});
$('#removerModalContent').html(buildReportFormHtml(ctx, isZka));
if (!isZka) {
$('#removerModalContent')
.on('click', '#rmProtectModeInstall', function () { applyProtectMode('install'); })
.on('click', '#rmProtectModeRemove', function () { applyProtectMode('remove'); })
.on('click', '.rmProtectOptBtn', function () {
$(this).toggleClass('is-active');
if (protectMode === 'install') {
var $levels = $('#rmProtectLevels .rmProtectOptBtn.is-active');
if ($(this).closest('#rmProtectReasons').length && $(this).hasClass('is-active') && $levels.length === 0) {
$('#rmProtectLevels .rmProtectOptBtn').first().addClass('is-active');
}
if ($(this).closest('#rmProtectLevels').length && !$(this).hasClass('is-active') && $levels.length === 0) {
$('#rmProtectReasons .rmProtectOptBtn').removeClass('is-active');
}
}
applyGeneratedText($('#rmReportText'), buildProtectText(protectMode));
});
$(document)
.off('click.rmProtectAdd').on('click.rmProtectAdd', '.rmProtectAddPage', function () {
var id = 'rmProtectPage' + pageCounter++;
$('#rmProtectPagesContainer').append(buildProtectPageRowHtml(id, '', false));
updateProtectMultiUi();
})
.off('click.rmProtectRemove').on('click.rmProtectRemove', '.rmProtectPageRow .rmRemoveInput', function () {
$(this).closest('.rmProtectPageRow').remove(); updateProtectMultiUi();
});
applyProtectMode('install');
}
setupResizableModal('rmReportText');
renderModalFooter('submit', {
showCheckbox: false,
showSubscribe: true,
submitText: 'Отправить',
onSubmit: function () { doReport(ctx, false, protectMode); return true; }
});
if (isZka) {
$('<button id="rmReportFast" style="' + stCancel + '">Быстрый запрос</button>').insertBefore('#removerSubmit');
$('#rmReportFast').click(function () {
if ($('#removerSubmit').data('rmSubmitInProgress')) return;
$('#removerSubmit').data('rmSubmitInProgress', true).prop('disabled', true);
$('#rmReportFast').prop('disabled', true);
doReport(ctx, true, protectMode);
});
}
}
};
// ═══════════════════════════════════════════════════════════════════════════
// ВСПОМОГАТЕЛЬНЫЕ: КБУ, закрытие, категории
// ═══════════════════════════════════════════════════════════════════════════
function getFastRemoveCriteriaAnchorFromConfig(templateName) {
var anchors = cfg.fastRemoveCriteriaAnchors || {};
var template = String(templateName || '').trim();
var lower = template.toLowerCase();
var key;
if (!template) return '';
if (Object.prototype.hasOwnProperty.call(anchors, template)) return anchors[template];
for (key in anchors) {
if (Object.prototype.hasOwnProperty.call(anchors, key) && String(key).toLowerCase() === lower) {
return anchors[key];
}
}
return '';
}
function getFastRemoveCriteriaAnchor(reason) {
var configured = reason ? getFastRemoveCriteriaAnchorFromConfig(reason[0]) : '';
var template, label, m;
if (configured) return configured;
template = String(reason && reason[0] || '');
label = String(reason && reason[1] || '');
if (/^(?:подст\s*:\s*)?(?:deleteslow|ds)$/i.test(template) || /^\s*ds\b/i.test(label)) return 'С1';
m = label.match(/^\s*([А-ЯЁA-Z]{1,3}\d+)(?:\.\d+)?/);
return m ? m[1] : '';
}
function buildFastRemoveCriteriaLinkHtml(reason) {
var anchor = getFastRemoveCriteriaAnchor(reason);
var label = KBU_CRITERIA_PAGE + (anchor ? '#' + anchor : '');
var url = getPageUrlWithFragment(KBU_CRITERIA_PAGE, anchor);
return '<a href="' + escapeHtml(url) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' +
escapeHtml(label) + '</a>';
}
function getFastRemoveReasons() {
var reasons = cfg.fastRemoveReasons;
var prefix = (mwCfg.wgIsRedirect ? 'ОП' : 'О') + ({ 0: 'С', 2: 'У', 3: 'У', 6: 'Ф', 14: 'К' }[mwCfg.wgNamespaceNumber] || '');
var all = [];
if (isCategory && reasons.categories) all = all.concat(reasons.categories);
['general','articles','redirects','files','users','special'].forEach(function (k) { if (reasons[k]) all = all.concat(reasons[k]); });
if (!isCategory && reasons.categories) all = all.concat(reasons.categories);
return all.filter(function (r) { return prefix.indexOf(r[2] !== undefined ? r[2] : r[1].charAt(0)) >= 0; });
}
function showCloseActionsModal(opts) {
createModal({ title: 'Снятие шаблонов номинаций', inline: true });
$('#removerModalContent').html('<p style="margin:0;">Определение доступных действий...</p>');
var pageName = normTitle(mwCfg.wgPageName);
getText(pageName, function (pageText, readErr) {
if (readErr) { showInfoAndClose('Не удалось прочитать содержимое страницы.', readErr.info || readErr.code || '', true); return; }
if (pageText === null) { showInfoAndClose('Не удалось прочитать содержимое страницы.', '', true); return; }
var actions = opts.getActions(pageText);
if (!actions.length) { showInfoAndClose(opts.emptyText, opts.emptyDetails || ''); return; }
var actionMap = actions.reduce(function (m, a) { m[a.id] = a; return m; }, {});
$('#removerModalContent').html(buildActionsHtml(actions, opts.inputName, opts.listId));
$('#removerModalContent').off('change.rmActionWarnings').on('change.rmActionWarnings', 'input[name="' + opts.inputName + '"]', function () {
syncActionWarnings(opts.inputName, opts.listId);
syncModalLayout();
});
if (opts.afterRender) opts.afterRender(actions, actionMap, pageName, pageText);
renderModalFooter('submit', {
showCheckbox: opts.showCheckbox,
submitText: 'Выполнить',
onSubmit: function () {
var sel = getSelectedAction(opts.inputName, actionMap);
if (!sel) return false;
startProcessing();
return opts.onSubmit(sel, pageName, pageText, actionMap) !== false;
}
});
if (opts.afterFooterRender) opts.afterFooterRender(actions, actionMap, pageName, pageText);
syncActionWarnings(opts.inputName, opts.listId);
});
}
function runPageEditWithStatus(opts) {
var o = opts || {};
var statusId = logStatus(o.pendingText, null, { pending: true, trackError: false });
editPageContent(o.pageName, o.editOptions, o.buildFn, function (err, meta) {
if (err) { logStatus(o.errorText, err, { statusId: statusId }); unlockModalSubmit(); return; }
logStatus(o.successText, null, { statusId: statusId, trackError: false });
if (o.onSuccess) o.onSuccess(meta || null);
});
}
function removeKuFromCategory(pageName) {
runPageEditWithStatus({
pageName: pageName,
pendingText: 'Снимается шаблон КУ...',
errorText: 'Снятие шаблона КУ.',
successText: 'Шаблон КУ снят.',
editOptions: { summary: makeSummary('снятие шаблона КУ'), watchlist: 'nochange', readError: 'Страница не существует.', readErrorCode: 'read_failed' },
buildFn: function (text) {
var r = stripTemplatesByPattern(text, '(?:к\\s*удалению|ку)');
if (!r.removed) return { error: { code: 'no_changes', info: 'Шаблон КУ не найден.' } };
return { text: r.text.replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n') };
},
onSuccess: function () {
logStatus('Шаблон на СО не устанавливался.', null, { trackError: false });
renderModalFooter('reload');
}
});
}
// ── Категории: добавление шаблона ────────────────────────────────────────
function addTemplateToCategory(pageName, type, mainName, additionalNames, callback, options) {
var cb = callback || function () {};
var opts = options || {};
var cfgByType = {
discuss: { action: 'обсуждение', template: 'Обсуждаемая категория', aliases: cfg.categoryTemplates.discuss },
deletion: { action: 'удаление', template: 'Обсуждаемая категория', aliases: cfg.categoryTemplates.discuss },
rename: { action: 'переименование', template: 'Категория к переименованию', needsMain: true, aliases: cfg.categoryTemplates.rename },
merge: { action: 'объединение', template: 'Категория к объединению', needsMain: true, aliases: cfg.categoryTemplates.merge }
};
var typeCfg = cfgByType[type];
if (!typeCfg) { cb({ code: 'error', info: 'Неизвестный тип номинации.' }); return; }
var dateStr = getDate()[0];
var parts = [dateStr];
if (typeCfg.needsMain) {
if (!mainName) { cb({ code: 'error', info: 'Не указано основное название.' }); return; }
parts.push(mainName);
if (additionalNames && additionalNames.length) Array.prototype.push.apply(parts, additionalNames);
}
var tplText = T_OPEN + typeCfg.template + '|' + parts.join('|') + T_CLOSE;
editPageContent(pageName, { summary: opts.summary || makeSummary('добавление шаблона номинации на ' + typeCfg.action), readError: 'Не удалось получить содержимое.' },
function (text) {
if (hasTemplateWithDateByPattern(text, typeCfg.aliases, dateStr)) {
return { skip: true, meta: { status: 'already_present' } };
}
return { text: wrapInNoinclude(text, tplText) };
},
function (err, meta) {
if (!err && meta && meta.status === 'already_present') {
logStatus('На странице ' + buildQuotedStatusPageLink(pageName) + ' уже есть шаблон номинации с датой ' + dateStr + '.', null, { trackError: false });
} else {
logPageEdit(pageName, err);
}
if (err) { cb(err); return; }
if (type === 'merge') {
addMergeTemplatesToTargets(pageName, mainName, additionalNames, dateStr, opts.summary, function () { cb(null); });
return;
}
cb(null);
}
);
}
function collectCategoryPageInputValues(selector) {
var pages = [];
$(selector).each(function () {
var title = normalizeCategoryPageName($(this).val() || '');
if (title && pages.indexOf(title) === -1) pages.push(title);
});
return pages;
}
function addTemplatesToCategories(pages, type, mainName, additionalNames, callback, options) {
var cb = callback || function () {};
var opts = options || {};
var processedPages = [];
eachSequential(pages || [], function (pageName, next) {
var pageMainName = mainName;
var pageAdditionalNames = additionalNames;
var pageTargets;
if (type === 'rename' && opts.renameTemplateTargetsByPage) {
pageTargets = opts.renameTemplateTargetsByPage[normTitle(pageName)];
if (Array.isArray(pageTargets)) {
pageMainName = pageTargets[0] || mainName;
pageAdditionalNames = pageTargets.slice(1);
} else if (pageTargets) {
pageMainName = pageTargets;
pageAdditionalNames = [];
}
}
addTemplateToCategory(pageName, type, pageMainName, pageAdditionalNames, function (err) {
if (!err) processedPages.push(pageName);
next(err || null);
}, { summary: opts.summary });
}, function (err) { cb(err, processedPages); });
}
function addMergeTemplatesToTargets(sourcePage, mainName, additionalNames, dateStr, summary, callback) {
var cb = callback || function () {};
var currentCatName = normTitle(stripCatPrefix(sourcePage));
var targets = [mainName].concat(additionalNames || []);
if (!targets.length) { cb(); return; }
eachSequential(targets, function (target, next) {
var targetPage = normalizeCategoryPageName(target);
addMergeTemplateToTargetCategory(targetPage, currentCatName, dateStr, summary, function (success, status) {
var url = getPageUrl(targetPage);
var linkHtml = '<a href="' + escapeHtml(url) + '" class="removerModalLink" target="_blank" rel="noopener noreferrer">' + escapeHtml(targetPage) + '</a>';
if (success) {
var extra = (status === 'already_exists' || status === 'updated') ? ' (' + formatMergeStatus(status) + ')' : '';
logStatus('Шаблон добавлен в ' + linkHtml + extra + '.', null, { trackError: false });
} else {
logStatus('Ошибка при добавлении шаблона в ' + linkHtml + '.', { code: 'merge_target_failed', info: status }, { trackError: false });
}
next();
});
}, cb);
}
function addMergeTemplateToTargetCategory(targetPageName, sourceCatName, dateStr, summary, callback) {
if (typeof summary === 'function') {
callback = summary;
summary = '';
}
editPageContent(targetPageName, { summary: summary || makeSummary('добавление шаблона объединения'), readError: 'Не удалось получить содержимое' },
function (text) {
var existing = text.match(getCategoryMergeRe());
if (existing) {
var cats = existing[1].split('|').slice(1).map(function (p) { return p.trim(); }).filter(function (p) { return p.indexOf('=') === -1 && p.length > 0; });
var norm = sourceCatName.replace(/\s+/g, ' ').trim();
if (cats.some(function (c) { return c.replace(/\s+/g, ' ').trim() === norm; })) { return { skip: true, meta: { status: 'already_exists' } }; }
return {
text: text.replace(existing[0], function () { return existing[0].replace(/\}\}\s*$/, '|' + sourceCatName + '}}'); }),
summary: summary || makeSummary('дополнение шаблона объединения ' + formatCatLink(sourceCatName)),
meta: { status: 'updated' }
};
}
return { text: wrapInNoinclude(text, T_OPEN + 'Категория к объединению|' + dateStr + '|' + sourceCatName + T_CLOSE), meta: { status: 'created' } };
},
function (err, meta) { callback(!err, err ? err.info : ((meta && meta.status) || 'updated')); }
);
}
// ── Категории: обсуждение ────────────────────────────────────────────────
function buildCategoryDiscussionTitle(pageName, type, mainName, additionalNames, options) {
var opts = options || {};
var pages = Array.isArray(pageName) ? pageName : null;
var titleText;
if (pages && pages.length) {
titleText = String(opts.headerText || '').trim();
if (!titleText && type === 'rename' && opts.renameTargetsByPage) titleText = formatRenameItemsWithAnd(pages, opts.renameTargetsByPage);
if (!titleText) titleText = formatPagesWithAnd(pages, ':') + (type === 'deletion' ? ' → удалить' : '');
return '=== ' + titleText + ' ===';
}
var title = '=== [[:' + pageName + ']]';
if (type === 'rename' || type === 'merge') {
title += (type === 'rename' ? ' → ' : ' объединить с ') + formatCatLink(mainName);
if (additionalNames && additionalNames.length) {
var conj = type === 'rename' ? ' или ' : ' и ';
var head = additionalNames.slice(0, -1).map(formatCatLink).join(', ');
title += (additionalNames.length > 1 ? ', ' + head : '') + conj + formatCatLink(additionalNames[additionalNames.length - 1]);
}
} else if (type === 'deletion') {
title += ' → удалить';
}
return title + ' ===';
}
function getCategoryDiscussionSectionTitle(discussionTitle) {
return String(discussionTitle || '').replace(/^===\s*/, '').replace(/\s*===\s*$/, '').trim();
}
function createCategoryDiscussion(pageName, reason, type, callback, mainName, additionalNames, options) {
var cb = callback || function () {};
var opts = options || {};
var now = new Date();
var m = now.getUTCMonth(), year = now.getUTCFullYear(), day = now.getUTCDate();
var dateHeader = '== ' + day + ' ' + MONTHS_GEN[m] + ' ' + year + ' ==';
var discPage = 'Википедия:Обсуждение категорий/' + MONTHS_NOM[m] + '_' + year;
var discTitle = buildCategoryDiscussionTitle(pageName, type, mainName, additionalNames, opts);
var discBody = (opts.reasonIsPrepared ? String(reason || '') : appendNominationSignature(reason)).replace(/^\s+|\s+$/g, '');
var discText = discTitle + '\n' + discBody + '\n';
var sectionTitle = getCategoryDiscussionSectionTitle(discTitle);
publishNomination({
pageTitle: discPage,
readErrorMessage: 'Не удалось получить содержимое страницы обсуждения.',
summary: makeNominationPageSummary(sectionTitle),
createText: function () {
return T_OPEN + 'ОБК-Навигация' + T_CLOSE + '\n\n' + dateHeader + '\n\n' + discText;
},
buildText: function (text) {
var todayMatch = new RegExp('^' + escapeRegExp(dateHeader) + '\\s*$', 'm').exec(text);
if (!/\{\{\s*ОБК-Навигация\s*\}\}/i.test(text)) {
return { error: { code: 'insert_failed', info: 'Не найден шаблон ' + T_OPEN + 'ОБК-Навигация' + T_CLOSE + '.' } };
}
if (todayMatch) {
var dayContentStart = todayMatch.index + todayMatch[0].length;
var nextDayMatch = /^==[^=\n].*==\s*$/m.exec(text.slice(dayContentStart));
var insertAt = nextDayMatch ? dayContentStart + nextDayMatch.index : text.length;
return { text: insertDiscussionBlockAt(text, insertAt, discText, '\n\n') };
}
return { text: insertTopDiscussionSection(text, dateHeader + '\n\n' + discText) };
}
}, function (err) {
if (err) { cb(err); return; }
cb(null, { pageTitle: discPage, sectionTitle: sectionTitle });
});
}
// ── Категории: завершение ОБКАТ ───────────────────────────────────────────
function markCategoryDiscussionAsDone(pageName) {
runPageEditWithStatus({
pageName: pageName,
pendingText: 'Снимается шаблон обсуждения...',
errorText: 'Снятие шаблона обсуждения.',
successText: 'Шаблон обсуждения снят.',
editOptions: { summary: makeSummary('обсуждение категории завершено'), readError: 'Не удалось получить содержимое.' },
buildFn: function (text) {
var allTpls = [cfg.categoryTemplates.discuss, cfg.categoryTemplates.rename, cfg.categoryTemplates.merge].join('|');
var patterns = [
new RegExp('<noinclude>\\s*\\{\\{\\s*(' + allTpls + ')\\s*\\|\\s*([^\\}]+?)\\s*\\}\\}\\s*</noinclude>', 'i'),
new RegExp('\\{\\{\\s*(' + allTpls + ')\\s*\\|\\s*([^\\}]+?)\\s*\\}\\}', 'i')
];
var match = null;
for (var i = 0; i < patterns.length; i++) { match = text.match(patterns[i]); if (match) break; }
if (!match) return { error: { code: 'no_changes', info: 'Шаблон обсуждаемой категории не найден.' } };
return {
text: text.replace(match[0], '').replace(/^\n+/, '').replace(/\n{3,}/g, '\n\n'),
meta: { tplDate: convertToStandardDate(match[2].split('|')[0].trim()) }
};
},
onSuccess: function (meta) {
var talkStatusId = logStatus('Обновляется шаблон на СО категории...', null, { pending: true, trackError: false });
updateCategoryTalkPage(pageName, meta.tplDate, function (talkErr, info) {
if (talkErr) { logStatus('Установка шаблона на СО.', talkErr, { statusId: talkStatusId }); unlockModalSubmit(); return; }
logStatus(
(info && (info.status === 'already_present' || info.status === 'no_changes')) ? 'Шаблон на СО уже установлен.' : 'Шаблон установлен на СО.',
null, { statusId: talkStatusId, trackError: false }
);
renderModalFooter('reload');
});
}
});
}
function updateCategoryTalkPage(categoryName, templateDate, callback) {
var cb = callback || function () {};
var talkPage = getTalkPage(categoryName);
var newTpl = T_OPEN + 'Обсуждавшаяся категория|' + templateDate + T_CLOSE;
getTextWithTimestamp(talkPage, function (text, baseTimestamp, readErr) {
if (readErr) { cb(makeReadError(readErr, 'talk_read_failed', 'Не удалось получить содержимое СО категории.')); return; }
if (text === null) {
apiReq({ title: talkPage, text: newTpl + '\n\n', summary: makeSummary('добавление шаблона [[ш:Обсуждавшаяся категория]] с датой ' + templateDate), createonly: true },
'edit', function (resp) {
if (resp && resp.error) {
if (resp.error.code === 'articleexists') setTimeout(function () { updateCategoryTalkPage(categoryName, templateDate, cb); }, 1000);
else cb(resp.error);
} else cb(null, { status: 'created' });
});
return;
}
var talkResult = upsertDateSectionTemplateOnTalkPage(text, 'Обсуждавшаяся категория', templateDate, '', { aliases: cfg.categoryTemplates.discussed });
var newText = talkResult.text;
if (newText === text) { cb(null, { status: talkResult.status || 'no_changes' }); return; }
var summary = talkResult.status === 'created'
? makeSummary('добавление шаблона [[ш:Обсуждавшаяся категория]] с датой ' + templateDate)
: makeSummary('обновление шаблона [[ш:Обсуждавшаяся категория]], добавлена дата ' + templateDate);
var ep = {
title: talkPage,
text: newText,
summary: summary
};
if (baseTimestamp) ep.basetimestamp = baseTimestamp;
apiReq(ep, 'edit', function (resp) { cb(resp && resp.error ? resp.error : null, resp && !resp.error ? { status: talkResult.status } : null); });
});
}
// ── Быстрое объединение (Ctrl+клик КОБ) ─────────────────────────────────
function buildQuickMergeHtml(tplDate, targets, currentCatName) {
return joinHtml([
'<p>Найден шаблон с датой <strong>', escapeHtml(tplDate), '</strong>. Категории для объединения:</p>',
'<pre style="background:', tk.bgBase, ';color:', tk.cBase, ';padding:10px;border:1px solid ', tk.bSubS, ';border-radius:4px;margin-bottom:10px;">',
targets.map(function (c) { return '• ' + escapeHtml(c); }).join('\n'),
'</pre>',
'<p><strong>Текущая категория:</strong> ', escapeHtml(currentCatName), '</p>',
'<p style="color:', tk.cSub, ';">Шаблон будет добавлен во все указанные выше категории.</p>'
]);
}
function showQuickMergeModal() {
getText(mwCfg.wgPageName, function (text, readErr) {
if (readErr) { alert('Не удалось получить содержимое: ' + (readErr.info || readErr.code || 'ошибка API') + '.'); return; }
if (!text) { alert('Не удалось получить содержимое.'); return; }
var mergeRe = getCategoryMergeRe();
var match = text.match(mergeRe);
if (!match) { alert('В текущей категории не найден шаблон "Категория к объединению".'); return; }
var params = match[1].split('|').map(function (p) { return p.trim(); });
var tplDate = params[0];
var targets = params.slice(1);
if (!targets.length) { alert('В шаблоне не найдены целевые категории.'); return; }
createModal({ title: 'Быстрое добавление шаблона объединения' });
var currentCatName = normTitle(stripCatPrefix(mwCfg.wgPageName));
$('#removerModalContent').html(buildQuickMergeHtml(tplDate, targets, currentCatName));
renderModalFooter('submit', {
showCheckbox: false,
submitText: 'Добавить шаблоны',
onSubmit: function () {
startProcessing();
$('#removerSubmit').prop('disabled', true);
eachSequential(targets, function (target, next) {
var targetPage = normalizeCategoryPageName(target);
var targetLink = buildStatusPageLink(targetPage);
addMergeTemplateToTargetCategory(targetPage, currentCatName, tplDate, function (success, status) {
if (success) logStatus('Категория ' + targetLink + ' (' + formatMergeStatus(status) + ').', null, { trackError: false });
else logStatus('Ошибка ' + targetLink + '.', { code: 'merge_target_failed', info: status }, { trackError: false });
next();
});
}, function () {
if (isError) markSubmitError(); else renderModalFooter('close');
});
return true;
}
});
});
}
// ── ЗКА/Защита: публикация ───────────────────────────────────────────────
function getReporterContext(mode) {
var rawPage = mwCfg.wgPageName;
var pageName = normTitle(rawPage)
.replace(/(Special|Служебная):(Contributions|Вклад)\//i, 'User:')
.replace(/(User talk:|Обсуждение участни(ка|цы):)/i, 'User:');
var isUserRelated = /user|contrib|участни|вклад/i.test(rawPage);
var displayName = normTitle(rawPage)
.replace(/(Special|Служебная):(Вклад|Contributions)\//i, '')
.replace(/(User talk:|Обсуждение участни(ка|цы):)/i, '')
.replace(/(user|участни(к|ца)):/i, '');
var pageLink = '[[' + pageName + (isUserRelated ? '|' + displayName + ']]' : ']]');
var reportPage = mode === 'request' ? 'Википедия:Запросы к администраторам' : 'Википедия:Установка защиты';
return { pageName: pageName, pageLink: pageLink, displayName: displayName, reportPage: reportPage };
}
function getTopDiscussionInsertIndex(pageText) {
var introEnd = getIntroBlockEndIndex(pageText);
var firstHeading = /^==[^=\n].*==\s*$/m.exec(pageText.slice(introEnd));
return firstHeading ? introEnd + firstHeading.index : introEnd;
}
function getIntroBlockEndIndex(pageText) {
var pos = 0;
while (pos < pageText.length) {
var next = skipWhitespace(pageText, pos);
var end;
if (pageText.slice(next, next + 4) === '<!--') {
end = pageText.indexOf('-->', next + 4);
if (end === -1) return next;
pos = end + 3;
continue;
}
end = skipTemplate(pageText, next);
if (end !== next) {
pos = end;
continue;
}
return next;
}
return pos;
}
function skipWhitespace(text, pos) {
while (pos < text.length && /\s/.test(text.charAt(pos))) pos++;
return pos;
}
function skipTemplate(text, pos) {
var depth = 0;
var i = pos;
if (text.slice(pos, pos + 2) !== '{{') return pos;
while (i < text.length) {
if (text.slice(i, i + 4) === '<!--') {
var commentEnd = text.indexOf('-->', i + 4);
if (commentEnd === -1) return pos;
i = commentEnd + 3;
continue;
}
if (text.slice(i, i + 2) === '{{') {
depth++;
i += 2;
continue;
}
if (text.slice(i, i + 2) === '}}') {
depth--;
i += 2;
if (depth === 0) return i;
continue;
}
i++;
}
return pos;
}
function insertDiscussionBlockAt(pageText, insertAt, blockText, separator) {
var sep = separator || '\n\n';
var before = pageText.slice(0, insertAt).replace(/\s+$/, '');
var block = String(blockText || '').replace(/\s+$/, '');
var after = pageText.slice(insertAt).replace(/^\s+/, '');
return (before ? before + sep : '') + block + (after ? sep + after : '\n');
}
function insertTopDiscussionSection(pageText, sectionText) {
return insertDiscussionBlockAt(pageText, getTopDiscussionInsertIndex(pageText), sectionText, '\n\n');
}
function doReport(ctx, fast, protectMode) {
var header = $('#rmReportHeader').val() || ctx.pageLink;
var text = $('#rmReportText').val() || '';
var isZka = ctx.reportPage === 'Википедия:Запросы к администраторам';
var isRemoveProtect = !isZka && protectMode === 'remove';
startProcessing();
var targetPage, editParams, sectionForLink;
if (fast) {
targetPage = 'Википедия:Запросы к администраторам/Быстрые';
sectionForLink = null;
editParams = {
appendtext: '\n\n' + T_OPEN + 'sub' + 'st:t:preload/ЗКАБ/subst|\n | участник = ' + ctx.displayName +
'| страница = | пояснение = ' + text + T_CLOSE + '\n',
summary: makeSummary('новый запрос [[Special:Contributions/' + ctx.displayName + ']]')
};
} else if (isZka) {
targetPage = ctx.reportPage;
sectionForLink = extractDisplayedText(header);
var isIpFull = /^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/.test(ctx.displayName);
editParams = {
text: '== ' + header + ' ==\n\n* ' + T_OPEN + 'userlinks|' + ctx.displayName + (isIpFull ? '|ip=1' : '') + T_CLOSE + '\n' + text + ' ~~' + '~~',
summary: '/* ' + sectionForLink + ' */ ' + makeSummary('новый запрос')
};
} else {
targetPage = isRemoveProtect ? 'Википедия:Снятие защиты' : 'Википедия:Установка защиты';
var pages = collectInputValues('.rmProtectPageInput');
if (!pages.length) pages = [ctx.pageName];
var sectionTitle, pageLines;
if (pages.length === 1) {
sectionTitle = '[[' + pages[0] + ']]';
pageLines = '* ' + T_OPEN + 'pagelinks-protect|' + pages[0] + T_CLOSE;
} else {
sectionTitle = ($('#rmProtectHeader').val() || '').trim() || pages.map(function (p) { return '[[' + p + ']]'; }).join(', ');
pageLines = pages.map(function (p) { return '* ' + T_OPEN + 'pagelinks-protect|' + p + T_CLOSE; }).join('\n');
}
sectionForLink = extractDisplayedText(sectionTitle);
editParams = {
section: 'new',
sectiontitle: sectionTitle,
text: pageLines + '\n' + text + ' ~~' + '~~',
summary: '/* ' + sectionForLink + ' */ ' + makeSummary('новый запрос')
};
}
var statusId = logStatus('Публикуется запрос на «' + targetPage + '»...', null, { pending: true, trackError: false });
if (isZka && !fast) {
editPageContent(targetPage, {
summary: editParams.summary,
assertuser: mwCfg.wgUserName,
readError: 'Не удалось получить содержимое страницы «' + targetPage + '».'
}, function (pageText) {
return { text: insertTopDiscussionSection(pageText, editParams.text) };
}, function (err) {
if (err) {
logStatus('Публикация запроса на «' + targetPage + '».', err, { statusId: statusId });
markSubmitError();
$('#rmReportFast').prop('disabled', false);
return;
}
logStatus('Запрос опубликован.', null, { statusId: statusId, trackError: false });
appendNominationLink(targetPage, sectionForLink);
if (sectionForLink) subscribeToTopic(targetPage, sectionForLink);
renderModalFooter('reload');
});
return;
}
apiReq($.extend({ title: targetPage }, editParams), 'edit', function (resp) {
if (resp && resp.error) {
logStatus('Публикация запроса на «' + targetPage + '».', resp.error, { statusId: statusId });
markSubmitError();
if (isZka) $('#rmReportFast').prop('disabled', false);
return;
}
logStatus('Запрос опубликован.', null, { statusId: statusId, trackError: false });
appendNominationLink(targetPage, sectionForLink);
if (sectionForLink) subscribeToTopic(targetPage, sectionForLink);
renderModalFooter('reload');
});
}
// ═══════════════════════════════════════════════════════════════════════════
// ДИСПЕТЧЕР
// ═══════════════════════════════════════════════════════════════════════════
function handleMenuClick(item, event) {
isError = false;
var op = OPERATIONS_MAP[item.id];
// Специальный случай: КОБ категории с Ctrl — быстрое добавление шаблона
if (item.id === 'cat-merge' && event && event.ctrlKey) {
showQuickMergeModal();
return;
}
if (!op) {
console.error('RemoverCore: неизвестная операция', item.id);
return;
}
var handlerFn = handlers[op.handler];
if (typeof handlerFn !== 'function') {
console.error('RemoverCore: обработчик не найден', op.handler);
return;
}
handlerFn(op, event);
}
// ─── Экспорт ─────────────────────────────────────────────────────────────
window.RemoverCore = { handleMenuClick: handleMenuClick };
}());
77kghae04t3a1kk0xqxv2gk0cx207g2
WET Awards 2002
0
175148
749709
743902
2026-07-03T18:58:33Z
~2026-38011-52
74711
/* Winners and nominees */
749709
wikitext
text/x-wiki
{{Short description|American entertainment awards ceremony}}
{{one source|date=June 2025}}
{{Use mdy dates|date=February 2025}}
{{Infobox award
| name = 2nd WET Awards
| image =
| caption =
| date = June 25, 2002
| location = [[Dolby Theatre|Kodak Theatre]], [[Los Angeles]], [[California]]
| presenter = [[WET (TV channel)|White Entertainment Television]]
| host = [[Vince McMahon]] <br />[[Shane McMahon]]
| network = [[WET (TV channel)|WET]]
| producer =
| previous = [[WET Awards 2001|1st]]
| main = {{nowrap|[[WET Awards]]}}
| next = [[WET Awards 2003|3rd]]
}}
The '''2nd [[WET Awards]]''' took place at the [[Dolby Theatre|Kodak Theatre]] in [[Los Angeles]], [[California]] on June 25, 2002. The awards recognized Americans in music, acting, sports, and other fields of entertainment over the past year. Professional wrestler [[Vince McMahon]] and [[Shane McMahon]] hosted the event for the second time.
==Performances==
==Presenters==
==Winners and nominees==
{| class=wikitable width="100%"
! style="background:#EEDD82;" ! width="50%" | Video of the Year
! style="background:#EEDD82;" ! width="50%" | Viewers' Choice
|-
| valign="top" |
* '''[[No Doubt]] feat. [[Bounty Killer]] – "[[Hey Baby (No Doubt song)|Hey Baby]]"'''
** [[Aaliyah]] – "[[Rock the Boat (Aaliyah song)|Rock the Boat]]"
** [[Sam Moran]] feat. [[Dane Bowers]] – "[[Amish Paradise (song)|Amish Paradise]]"
** [[Jennifer Lopez]] feat. [[Ja Rule]] – "[[I'm Real (Murder Remix)]]"
** [[NSYNC]] and [[98 Degrees|98°]] – "[[Can U Get wit It (song)|Can U Get wit It]]"
| valign="top" |
* '''[[Blue (English group)|Blue]] – "[[Too Close (Next song)#Blue version|Too Close]]"'''
** [[Nelly Furtado]] – "[[I'm Like A Bird]]"
** [[Backstreet Boys]] feat. [[98 Degrees|98°]] – "[[Freak Me (Backstreet Boys song)|Freak Me]]"
** [[Aerosmith]] – "[[Fly Away from Here]]"
** [[Britney Spears]] – "[[Overprotected]]"
|-
! style="background:#EEDD82;" ! "width="50%" | Best Female Rock Artist
! style="background:#EEDD82;" ! "width="50%" | Best Male Rock Artist
|-
| valign="top" |
* '''[[Sheryl Crow]]'''
** [[Tori Amos]]
** [[Melissa Etheridge]]
** [[Michelle Branch]]
** [[Stevie Nicks]]
| valign="top" |
* '''[[Weezer]]'''
** [[Tenacious D]]
** [[Lenny Kravitz]]
** [[The White Stripes]]
** [[Nickelback]]
|-
! style="background:#EEDD82;" ! width="50%" | Best Female Pop Artist
! style="background:#EEDD82;" ! width="50%" | Best Male Pop Artist
|-
| valign="top" |
* '''[[Shakira]]'''
** [[Jewel (singer)|Jewel]]
** [[Nelly Furtado]]
** [[Pink (singer)|Pink]]
** [[Britney Spears]]
| valign="top" |
* '''[[Usher (singer)|Usher]]'''
** [[Craig David]]
** [[Jaheim]]
** [[Maxwell (musician)|Maxwell]]
** [[Musiq Soulchild|Musiq]]
|-
! style="background:#EEDD82;" colspan="2"| Best Group
|-
| colspan="2" valign="top" |
* '''[[NSYNC]'''
** [[O-Town]
** [[Blue (English group)|Blue]]
** [[Cypress Hill]]
** [[Westlife]]
|-
! style="background:#EEDD82;" ! width="50%" | Best New Artist
! style="background:#EEDD82;" ! width="50%" | Best Country Artist
|-
| valign="top" |
* '''[[Britney Spears]]'''
** [[Kylie Minogue]]
** [[Blue (English group)|Blue]]
** [[Michael Jackson]]
** [[Five (group)|Five]]
| valign="top" |
* '''[[Tim McGraw]]'''
** [[Toby Keith]]
** [[Kenny Chesney]]
** [[Brad Paisley]]
** [[Trisha Yearwood]]
|-
! style="background:#EEDD82;" ! width="50%" | Best Actress
! style="background:#EEDD82;" ! width="50%" | Best Actor
|-
| valign="top" |
* '''[[Mandy Moore]]'''
** [[Angelina Jolie]]
** [[Cameron Diaz]]
** [[Nicole Kidman]]
** [[Reese Witherspoon]]
| valign="top" |
* '''[[Will Ferrell]]'''
** [[Elijah Wood]]
** [[Vin Diesel]]
** [[Paul Walker]]
** [[Jackie Chan]]
|-
! style="background:#EEDD82;" ! width="50%" | Sportswoman of the Year
! style="background:#EEDD82;" ! width="50%" | Sportsman of the Year
|-
| valign="top" |
* '''[[Mae Young]]'''
** [[Stacy Keibler]]
** [[Molly Holly]]
** [[Torrie Wilson]]
** [[Trish Stratus]]
| valign="top" |
* '''[[Brock Lesner]]'''
** [[Stone Cold Steve Austin]]
** [[Triple H]]
** [[The Undertaker]]
** [[Dwayne Johnson|The Rock]]
|-
! style="background:#EEDD82;" ! width="50%" | Lifetime Achievement Award
! style="background:#EEDD82;" ! width="50%" | Humanitarian Award
|-
| valign="top" |
* '''[[Eurythmics]]'''
| valign="top" |
* '''[[Kurt Angle]]'''<ref name=wet/>
|}
==References==
{{reflist}}
==External links==
{{WET Awards}}
[[Category:WET Awards]]
[[Category:2002 awards in the United States]]
{{US-tv-stub}}
alo4kyvqgv5mk3ljnl8fucqzp44oub8
749710
749709
2026-07-03T18:59:39Z
~2026-38011-52
74711
/* Winners and nominees */
749710
wikitext
text/x-wiki
{{Short description|American entertainment awards ceremony}}
{{one source|date=June 2025}}
{{Use mdy dates|date=February 2025}}
{{Infobox award
| name = 2nd WET Awards
| image =
| caption =
| date = June 25, 2002
| location = [[Dolby Theatre|Kodak Theatre]], [[Los Angeles]], [[California]]
| presenter = [[WET (TV channel)|White Entertainment Television]]
| host = [[Vince McMahon]] <br />[[Shane McMahon]]
| network = [[WET (TV channel)|WET]]
| producer =
| previous = [[WET Awards 2001|1st]]
| main = {{nowrap|[[WET Awards]]}}
| next = [[WET Awards 2003|3rd]]
}}
The '''2nd [[WET Awards]]''' took place at the [[Dolby Theatre|Kodak Theatre]] in [[Los Angeles]], [[California]] on June 25, 2002. The awards recognized Americans in music, acting, sports, and other fields of entertainment over the past year. Professional wrestler [[Vince McMahon]] and [[Shane McMahon]] hosted the event for the second time.
==Performances==
==Presenters==
==Winners and nominees==
{| class=wikitable width="100%"
! style="background:#EEDD82;" ! width="50%" | Video of the Year
! style="background:#EEDD82;" ! width="50%" | Viewers' Choice
|-
| valign="top" |
* '''[[No Doubt]] feat. [[Bounty Killer]] – "[[Hey Baby (No Doubt song)|Hey Baby]]"'''
** [[Aaliyah]] – "[[Rock the Boat (Aaliyah song)|Rock the Boat]]"
** [[Sam Moran]] feat. [[Dane Bowers]] – "[[Amish Paradise (song)|Amish Paradise]]"
** [[Jennifer Lopez]] feat. [[Ja Rule]] – "[[I'm Real (Murder Remix)]]"
** [[NSYNC]] and [[98 Degrees|98°]] – "[[Can U Get wit It (song)|Can U Get wit It]]"
| valign="top" |
* '''[[Blue (English group)|Blue]] – "[[Too Close (Next song)#Blue version|Too Close]]"'''
** [[Nelly Furtado]] – "[[I'm Like A Bird]]"
** [[Backstreet Boys]] feat. [[98 Degrees|98°]] – "[[Freak Me (Backstreet Boys song)|Freak Me]]"
** [[Aerosmith]] – "[[Fly Away from Here]]"
** [[Britney Spears]] – "[[Overprotected]]"
|-
! style="background:#EEDD82;" ! "width="50%" | Best Female Rock Artist
! style="background:#EEDD82;" ! "width="50%" | Best Male Rock Artist
|-
| valign="top" |
* '''[[Sheryl Crow]]'''
** [[Tori Amos]]
** [[Melissa Etheridge]]
** [[Michelle Branch]]
** [[Stevie Nicks]]
| valign="top" |
* '''[[Weezer]]'''
** [[Tenacious D]]
** [[Lenny Kravitz]]
** [[The White Stripes]]
** [[Nickelback]]
|-
! style="background:#EEDD82;" ! width="50%" | Best Female Pop Artist
! style="background:#EEDD82;" ! width="50%" | Best Male Pop Artist
|-
| valign="top" |
* '''[[Shakira]]'''
** [[Jewel (singer)|Jewel]]
** [[Nelly Furtado]]
** [[Pink (singer)|Pink]]
** [[Britney Spears]]
| valign="top" |
* '''[[Justin Timberlake]]'''
** [[Michael Jackson]]
** [[Sam Moran]]
** [[Enrique Iglesias]]
** [[Aaron Carter]]
|-
! style="background:#EEDD82;" colspan="2"| Best Group
|-
| colspan="2" valign="top" |
* '''[[NSYNC]]'''
** [[O-Town]
** [[Blue (English group)|Blue]]
** [[Cypress Hill]]
** [[Westlife]]
|-
! style="background:#EEDD82;" ! width="50%" | Best New Artist
! style="background:#EEDD82;" ! width="50%" | Best Country Artist
|-
| valign="top" |
* '''[[Britney Spears]]'''
** [[Kylie Minogue]]
** [[Blue (English group)|Blue]]
** [[Michael Jackson]]
** [[Five (group)|Five]]
| valign="top" |
* '''[[Tim McGraw]]'''
** [[Toby Keith]]
** [[Kenny Chesney]]
** [[Brad Paisley]]
** [[Trisha Yearwood]]
|-
! style="background:#EEDD82;" ! width="50%" | Best Actress
! style="background:#EEDD82;" ! width="50%" | Best Actor
|-
| valign="top" |
* '''[[Mandy Moore]]'''
** [[Angelina Jolie]]
** [[Cameron Diaz]]
** [[Nicole Kidman]]
** [[Reese Witherspoon]]
| valign="top" |
* '''[[Will Ferrell]]'''
** [[Elijah Wood]]
** [[Vin Diesel]]
** [[Paul Walker]]
** [[Jackie Chan]]
|-
! style="background:#EEDD82;" ! width="50%" | Sportswoman of the Year
! style="background:#EEDD82;" ! width="50%" | Sportsman of the Year
|-
| valign="top" |
* '''[[Mae Young]]'''
** [[Stacy Keibler]]
** [[Molly Holly]]
** [[Torrie Wilson]]
** [[Trish Stratus]]
| valign="top" |
* '''[[Brock Lesner]]'''
** [[Stone Cold Steve Austin]]
** [[Triple H]]
** [[The Undertaker]]
** [[Dwayne Johnson|The Rock]]
|-
! style="background:#EEDD82;" ! width="50%" | Lifetime Achievement Award
! style="background:#EEDD82;" ! width="50%" | Humanitarian Award
|-
| valign="top" |
* '''[[Eurythmics]]'''
| valign="top" |
* '''[[Kurt Angle]]'''<ref name=wet/>
|}
==References==
{{reflist}}
==External links==
{{WET Awards}}
[[Category:WET Awards]]
[[Category:2002 awards in the United States]]
{{US-tv-stub}}
lrum7kx88ga9od7hroq3sx53h7o2g9m
749802
749710
2026-07-03T21:25:55Z
~2026-38038-14
74714
/* Winners and nominees */
749802
wikitext
text/x-wiki
{{Short description|American entertainment awards ceremony}}
{{one source|date=June 2025}}
{{Use mdy dates|date=February 2025}}
{{Infobox award
| name = 2nd WET Awards
| image =
| caption =
| date = June 25, 2002
| location = [[Dolby Theatre|Kodak Theatre]], [[Los Angeles]], [[California]]
| presenter = [[WET (TV channel)|White Entertainment Television]]
| host = [[Vince McMahon]] <br />[[Shane McMahon]]
| network = [[WET (TV channel)|WET]]
| producer =
| previous = [[WET Awards 2001|1st]]
| main = {{nowrap|[[WET Awards]]}}
| next = [[WET Awards 2003|3rd]]
}}
The '''2nd [[WET Awards]]''' took place at the [[Dolby Theatre|Kodak Theatre]] in [[Los Angeles]], [[California]] on June 25, 2002. The awards recognized Americans in music, acting, sports, and other fields of entertainment over the past year. Professional wrestler [[Vince McMahon]] and [[Shane McMahon]] hosted the event for the second time.
==Performances==
==Presenters==
==Winners and nominees==
{| class=wikitable width="100%"
! style="background:#EEDD82;" ! width="50%" | Video of the Year
! style="background:#EEDD82;" ! width="50%" | Viewers' Choice
|-
| valign="top" |
* '''[[No Doubt]] feat. [[Bounty Killer]] – "[[Hey Baby (No Doubt song)|Hey Baby]]"'''
** [[Aaliyah]] – "[[Rock the Boat (Aaliyah song)|Rock the Boat]]"
** [[Sam Moran]] feat. [[Dane Bowers]] – "[[Amish Paradise (song)|Amish Paradise]]"
** [[Jennifer Lopez]] feat. [[Ja Rule]] – "[[I'm Real (Murder Remix)]]"
** [[NSYNC]] and [[98 Degrees|98°]] – "[[Can U Get wit It (song)|Can U Get wit It]]"
| valign="top" |
* '''[[Blue (English group)|Blue]] – "[[Too Close (Next song)#Blue version|Too Close]]"'''
** [[Nelly Furtado]] – "[[I'm Like A Bird]]"
** [[Backstreet Boys]] feat. [[98 Degrees|98°]] – "[[Freak Me (Backstreet Boys song)|Freak Me]]"
** [[Aerosmith]] – "[[Fly Away from Here]]"
** [[Britney Spears]] – "[[Overprotected]]"
|-
! style="background:#EEDD82;" ! "width="50%" | Best Female Rock Artist
! style="background:#EEDD82;" ! "width="50%" | Best Male Rock Artist
|-
| valign="top" |
* '''[[Sheryl Crow]]'''
** [[Tori Amos]]
** [[Melissa Etheridge]]
** [[Michelle Branch]]
** [[Stevie Nicks]]
| valign="top" |
* '''[[Weezer]]'''
** [[Tenacious D]]
** [[Lenny Kravitz]]
** [[The White Stripes]]
** [[Nickelback]]
|-
! style="background:#EEDD82;" ! width="50%" | Best Female Pop Artist
! style="background:#EEDD82;" ! width="50%" | Best Male Pop Artist
|-
| valign="top" |
* '''[[Shakira]]'''
** [[Jewel (singer)|Jewel]]
** [[Nelly Furtado]]
** [[Pink (singer)|Pink]]
** [[Britney Spears]]
| valign="top" |
* '''[[Justin Timberlake]]'''
** [[Michael Jackson]]
** [[Sam Moran]]
** [[Enrique Iglesias]]
** [[Aaron Carter]]
|-
! style="background:#EEDD82;" colspan="2"| Best Group
|-
| colspan="2" valign="top" |
* '''[[NSYNC]]'''
** [[O-Town]]
** [[Blue (English group)|Blue]]
** [[Cypress Hill]]
** [[Westlife]]
|-
! style="background:#EEDD82;" ! width="50%" | Best New Artist
! style="background:#EEDD82;" ! width="50%" | Best Country Artist
|-
| valign="top" |
* '''[[Britney Spears]]'''
** [[Kylie Minogue]]
** [[Blue (English group)|Blue]]
** [[Michael Jackson]]
** [[Five (group)|Five]]
| valign="top" |
* '''[[Tim McGraw]]'''
** [[Toby Keith]]
** [[Kenny Chesney]]
** [[Brad Paisley]]
** [[Trisha Yearwood]]
|-
! style="background:#EEDD82;" ! width="50%" | Best Actress
! style="background:#EEDD82;" ! width="50%" | Best Actor
|-
| valign="top" |
* '''[[Mandy Moore]]'''
** [[Angelina Jolie]]
** [[Cameron Diaz]]
** [[Nicole Kidman]]
** [[Reese Witherspoon]]
| valign="top" |
* '''[[Will Ferrell]]'''
** [[Elijah Wood]]
** [[Vin Diesel]]
** [[Paul Walker]]
** [[Jackie Chan]]
|-
! style="background:#EEDD82;" ! width="50%" | Sportswoman of the Year
! style="background:#EEDD82;" ! width="50%" | Sportsman of the Year
|-
| valign="top" |
* '''[[Mae Young]]'''
** [[Stacy Keibler]]
** [[Molly Holly]]
** [[Torrie Wilson]]
** [[Trish Stratus]]
| valign="top" |
* '''[[Brock Lesner]]'''
** [[Stone Cold Steve Austin]]
** [[Triple H]]
** [[The Undertaker]]
** [[Dwayne Johnson|The Rock]]
|-
! style="background:#EEDD82;" ! width="50%" | Lifetime Achievement Award
! style="background:#EEDD82;" ! width="50%" | Humanitarian Award
|-
| valign="top" |
* '''[[Eurythmics]]'''
| valign="top" |
* '''[[Kurt Angle]]'''<ref name=wet/>
|}
==References==
{{reflist}}
==External links==
{{WET Awards}}
[[Category:WET Awards]]
[[Category:2002 awards in the United States]]
{{US-tv-stub}}
mr2ffgnlmz9d0vmk6dracytyt6ppeoc
WET Awards 2001
0
175155
749673
743901
2026-07-03T17:23:54Z
~2026-38011-52
74711
/* Winners and nominees */
749673
wikitext
text/x-wiki
{{Short description|Award ceremony in Las Vegas, Nevada}}
{{One source|date=June 2025}}
{{Use mdy dates|date=February 2025}}
{{Infobox award
| name = 1st WET Awards
| image =
| caption =
| date = June 19, 2001
| location = [[Paris Las Vegas|Paris Hotel]], [[Las Vegas]], [[Nevada]]
| presenter = [[WET (TV channel)|White Entertainment Television]]
| host = [[Vince McMahon]] <br />[[Shane McMahon]]
| network = [[WET (TV channel)|WET]]
| previous =
| main = {{nowrap|[[WET Awards]]}}
| next = [[WET Awards 2002|2nd]]
}}
The '''1st [[WET Awards]]''' took place at the [[Paris Las Vegas|Paris Hotel]] in [[Las Vegas]], [[Nevada]] on June 19, 2001. The awards recognized Americans in Music, Acting, Sports, and other fields of entertainment over the past year. Professional wrestler [[Vince McMahon]] and [[Shane McMahon]] hosted the event for the first time.
==Performances==
==Presenters==
==Winners and nominees==
{| class=wikitable width="100%"
! style="background:#EEDD82;" ! width="50%" | Video of the Year
! style="background:#EEDD82;" ! width="50%" | Viewers' Choice
|-
| valign="top" |
* '''[[Linkin Park]] – "[[One Step Closer (Linkin Park song)|One Step Closer]]"'''
** [[Backstreet Boys]] – "[[Shape of My Heart (Backstreet Boys song)|Shape of My Heart]]"
** [[Limp Bizkit]] feat. [[Saliva (band)|Saliva]], [[Insane Clown Posse]] & [[Kid Rock]] – "[[All Summer Long (Limp Bizkit song)|All Summer Long]]"
** [[Robbie Williams]] & [[Kylie Minogue]] – "[[Kids (Robbie Williams and Kylie Minogue song)|Kids]]"
** [[Lenny Kravitz]] – "[[Again (Lenny Kravitz song)|Again]]"
| valign="top" |
* '''[[Red Hot Chili Peppers]] – "[[Californication (song)|Californication]]"'''
** [[No Doubt]] – "[[Ex-Girlfriend (song)|Ex-Girlfriend]]"
** [[Foo Fighters]] – "[[Learn to Fly]]"
** [[Justin Timberlake]] – "[[Anything Can Happen (Justin Timberlake song)|Anything Can Happen]]"
|-
! style="background:#EEDD82;" ! "width="50%" | Best Female Rock Artist
! style="background:#EEDD82;" ! "width="50%" | Best Male Rock Artist
|-
| valign="top" |
* '''[[No Doubt]'''
** [[PJ Harvey]]
** [[Martina McBride]]
** [[Faith Hill]]
** [[Lee Ann Womack]]
| valign="top" |
* '''[[Green Day]]'''
** [[Linkin Park]]
** [[Foo Fighters]]
** [[U2]]
** [[Bon Jovi]]
|-
! style="background:#EEDD82;" ! width="50%" | Best Female Pop Artist
! style="background:#EEDD82;" ! width="50%" | Best Male Pop Artist
|-
| valign="top" |
* '''[[Jennifer Lopez]]'''
** [[Pink (singer)|Pink]]
** [[Mariah Carey]]
** [[Christina Aguilera]]
** [[Britney Spears]]
| valign="top" |
* '''[[Justin Timberlake]]'''
** [[Ricky Martin]]
** [[AJ McLean]]
** [[Aaron Carter]
** [[Greg Page]]
|-
! style="background:#EEDD82;" ! width="50%" | Best Female Group
! style="background:#EEDD82;" ! width="50%" | Best Male Group
|-
| valign="top" |
* '''[[Spice Girls]]'''
** [[Sugababes]]
** [[All Saints (group)|All Saints]]
** [[Dream (American group)|Dream]]
** [[Atomic Kitten]]
| valign="top" |
* '''[[Black Eyed Peas]]'''
** [[NSYNC]]
** [[Limp Bizkit]], [[Saliva (band)|Saliva]], [[Insane Clown Posse]] & [[Kid Rock]]
** [[Backstreet Boys]]
** [[Westlife]]
|-
! style="background:#EEDD82;" ! width="50%" | Best New Artist
! style="background:#EEDD82;" ! width="50%" | Best Country Artist
|-
| valign="top" |
* '''[[U2]]'''
** [[Red Hot Chili Peppers]]
** [[Justin Timberlake]]
** [[Mariah Carey]]
** [[Madonna]]
| valign="top" |
* '''[[Brad Paisley]]'''
** [[The Chicks|Dixie Chicks]]
** [[Sara Evans]]
** [[Alan Jackson]]
** [[Tim McGraw]]
|-
! style="background:#EEDD82;" ! width="50%" | Best Actress
! style="background:#EEDD82;" ! width="50%" | Best Actor
|-
| valign="top" |
* '''[[Julia Roberts]]'''
** [[Jennifer Lopez]]
** [[Kate Hudson]]
** [[Julia Stiles]]
| valign="top" |
* '''[[Tom Cruise]]'''
** [[Tom Hanks]]
** [[Mel Gibson]]
** [[Russell Crowe]]
|-
! style="background:#EEDD82;" ! width="50%" | Sportswoman of the Year
! style="background:#EEDD82;" ! width="50%" | Sportsman of the Year
|-
| valign="top" |
* '''[[Trish Stratus]]'''
** [[Lita (wrestler)|Lita]]
** [[Chyna]]
** [[Stephanie McMahon]]
| valign="top" |
* '''[[Triple H]]'''
** [[Dwayne Johnson|The Rock]]
** [[Stone Cold Steve Austin]]
** [[The Undertaker]]
|-
! style="background:#EEDD82;" ! colspan=2 | Lifetime Achievement
|-
| colspan="2" valign="top" |
* '''[[Tina Turner]]'''
|}
==References==
{{reflist}}
{{WET Awards}}
[[Category:WET Awards]]
[[Category:2001 awards in the United States]]
{{US-tv-stub}}
4agumoidh749hkm5nc3mwz69r4y1j6n
749674
749673
2026-07-03T17:24:06Z
~2026-38011-52
74711
/* Winners and nominees */
749674
wikitext
text/x-wiki
{{Short description|Award ceremony in Las Vegas, Nevada}}
{{One source|date=June 2025}}
{{Use mdy dates|date=February 2025}}
{{Infobox award
| name = 1st WET Awards
| image =
| caption =
| date = June 19, 2001
| location = [[Paris Las Vegas|Paris Hotel]], [[Las Vegas]], [[Nevada]]
| presenter = [[WET (TV channel)|White Entertainment Television]]
| host = [[Vince McMahon]] <br />[[Shane McMahon]]
| network = [[WET (TV channel)|WET]]
| previous =
| main = {{nowrap|[[WET Awards]]}}
| next = [[WET Awards 2002|2nd]]
}}
The '''1st [[WET Awards]]''' took place at the [[Paris Las Vegas|Paris Hotel]] in [[Las Vegas]], [[Nevada]] on June 19, 2001. The awards recognized Americans in Music, Acting, Sports, and other fields of entertainment over the past year. Professional wrestler [[Vince McMahon]] and [[Shane McMahon]] hosted the event for the first time.
==Performances==
==Presenters==
==Winners and nominees==
{| class=wikitable width="100%"
! style="background:#EEDD82;" ! width="50%" | Video of the Year
! style="background:#EEDD82;" ! width="50%" | Viewers' Choice
|-
| valign="top" |
* '''[[Linkin Park]] – "[[One Step Closer (Linkin Park song)|One Step Closer]]"'''
** [[Backstreet Boys]] – "[[Shape of My Heart (Backstreet Boys song)|Shape of My Heart]]"
** [[Limp Bizkit]] feat. [[Saliva (band)|Saliva]], [[Insane Clown Posse]] & [[Kid Rock]] – "[[All Summer Long (Limp Bizkit song)|All Summer Long]]"
** [[Robbie Williams]] & [[Kylie Minogue]] – "[[Kids (Robbie Williams and Kylie Minogue song)|Kids]]"
** [[Lenny Kravitz]] – "[[Again (Lenny Kravitz song)|Again]]"
| valign="top" |
* '''[[Red Hot Chili Peppers]] – "[[Californication (song)|Californication]]"'''
** [[No Doubt]] – "[[Ex-Girlfriend (song)|Ex-Girlfriend]]"
** [[Foo Fighters]] – "[[Learn to Fly]]"
** [[Justin Timberlake]] – "[[Anything Can Happen (Justin Timberlake song)|Anything Can Happen]]"
|-
! style="background:#EEDD82;" ! "width="50%" | Best Female Rock Artist
! style="background:#EEDD82;" ! "width="50%" | Best Male Rock Artist
|-
| valign="top" |
* '''[[No Doubt]]'''
** [[PJ Harvey]]
** [[Martina McBride]]
** [[Faith Hill]]
** [[Lee Ann Womack]]
| valign="top" |
* '''[[Green Day]]'''
** [[Linkin Park]]
** [[Foo Fighters]]
** [[U2]]
** [[Bon Jovi]]
|-
! style="background:#EEDD82;" ! width="50%" | Best Female Pop Artist
! style="background:#EEDD82;" ! width="50%" | Best Male Pop Artist
|-
| valign="top" |
* '''[[Jennifer Lopez]]'''
** [[Pink (singer)|Pink]]
** [[Mariah Carey]]
** [[Christina Aguilera]]
** [[Britney Spears]]
| valign="top" |
* '''[[Justin Timberlake]]'''
** [[Ricky Martin]]
** [[AJ McLean]]
** [[Aaron Carter]
** [[Greg Page]]
|-
! style="background:#EEDD82;" ! width="50%" | Best Female Group
! style="background:#EEDD82;" ! width="50%" | Best Male Group
|-
| valign="top" |
* '''[[Spice Girls]]'''
** [[Sugababes]]
** [[All Saints (group)|All Saints]]
** [[Dream (American group)|Dream]]
** [[Atomic Kitten]]
| valign="top" |
* '''[[Black Eyed Peas]]'''
** [[NSYNC]]
** [[Limp Bizkit]], [[Saliva (band)|Saliva]], [[Insane Clown Posse]] & [[Kid Rock]]
** [[Backstreet Boys]]
** [[Westlife]]
|-
! style="background:#EEDD82;" ! width="50%" | Best New Artist
! style="background:#EEDD82;" ! width="50%" | Best Country Artist
|-
| valign="top" |
* '''[[U2]]'''
** [[Red Hot Chili Peppers]]
** [[Justin Timberlake]]
** [[Mariah Carey]]
** [[Madonna]]
| valign="top" |
* '''[[Brad Paisley]]'''
** [[The Chicks|Dixie Chicks]]
** [[Sara Evans]]
** [[Alan Jackson]]
** [[Tim McGraw]]
|-
! style="background:#EEDD82;" ! width="50%" | Best Actress
! style="background:#EEDD82;" ! width="50%" | Best Actor
|-
| valign="top" |
* '''[[Julia Roberts]]'''
** [[Jennifer Lopez]]
** [[Kate Hudson]]
** [[Julia Stiles]]
| valign="top" |
* '''[[Tom Cruise]]'''
** [[Tom Hanks]]
** [[Mel Gibson]]
** [[Russell Crowe]]
|-
! style="background:#EEDD82;" ! width="50%" | Sportswoman of the Year
! style="background:#EEDD82;" ! width="50%" | Sportsman of the Year
|-
| valign="top" |
* '''[[Trish Stratus]]'''
** [[Lita (wrestler)|Lita]]
** [[Chyna]]
** [[Stephanie McMahon]]
| valign="top" |
* '''[[Triple H]]'''
** [[Dwayne Johnson|The Rock]]
** [[Stone Cold Steve Austin]]
** [[The Undertaker]]
|-
! style="background:#EEDD82;" ! colspan=2 | Lifetime Achievement
|-
| colspan="2" valign="top" |
* '''[[Tina Turner]]'''
|}
==References==
{{reflist}}
{{WET Awards}}
[[Category:WET Awards]]
[[Category:2001 awards in the United States]]
{{US-tv-stub}}
ehip0f1m3oriwa5vrfd6y1301kooywj
749675
749674
2026-07-03T17:24:23Z
~2026-38011-52
74711
/* Winners and nominees */
749675
wikitext
text/x-wiki
{{Short description|Award ceremony in Las Vegas, Nevada}}
{{One source|date=June 2025}}
{{Use mdy dates|date=February 2025}}
{{Infobox award
| name = 1st WET Awards
| image =
| caption =
| date = June 19, 2001
| location = [[Paris Las Vegas|Paris Hotel]], [[Las Vegas]], [[Nevada]]
| presenter = [[WET (TV channel)|White Entertainment Television]]
| host = [[Vince McMahon]] <br />[[Shane McMahon]]
| network = [[WET (TV channel)|WET]]
| previous =
| main = {{nowrap|[[WET Awards]]}}
| next = [[WET Awards 2002|2nd]]
}}
The '''1st [[WET Awards]]''' took place at the [[Paris Las Vegas|Paris Hotel]] in [[Las Vegas]], [[Nevada]] on June 19, 2001. The awards recognized Americans in Music, Acting, Sports, and other fields of entertainment over the past year. Professional wrestler [[Vince McMahon]] and [[Shane McMahon]] hosted the event for the first time.
==Performances==
==Presenters==
==Winners and nominees==
{| class=wikitable width="100%"
! style="background:#EEDD82;" ! width="50%" | Video of the Year
! style="background:#EEDD82;" ! width="50%" | Viewers' Choice
|-
| valign="top" |
* '''[[Linkin Park]] – "[[One Step Closer (Linkin Park song)|One Step Closer]]"'''
** [[Backstreet Boys]] – "[[Shape of My Heart (Backstreet Boys song)|Shape of My Heart]]"
** [[Limp Bizkit]] feat. [[Saliva (band)|Saliva]], [[Insane Clown Posse]] & [[Kid Rock]] – "[[All Summer Long (Limp Bizkit song)|All Summer Long]]"
** [[Robbie Williams]] & [[Kylie Minogue]] – "[[Kids (Robbie Williams and Kylie Minogue song)|Kids]]"
** [[Lenny Kravitz]] – "[[Again (Lenny Kravitz song)|Again]]"
| valign="top" |
* '''[[Red Hot Chili Peppers]] – "[[Californication (song)|Californication]]"'''
** [[No Doubt]] – "[[Ex-Girlfriend (song)|Ex-Girlfriend]]"
** [[Foo Fighters]] – "[[Learn to Fly]]"
** [[Justin Timberlake]] – "[[Anything Can Happen (Justin Timberlake song)|Anything Can Happen]]"
|-
! style="background:#EEDD82;" ! "width="50%" | Best Female Rock Artist
! style="background:#EEDD82;" ! "width="50%" | Best Male Rock Artist
|-
| valign="top" |
* '''[[No Doubt]]'''
** [[PJ Harvey]]
** [[Martina McBride]]
** [[Faith Hill]]
** [[Lee Ann Womack]]
| valign="top" |
* '''[[Green Day]]'''
** [[Linkin Park]]
** [[Foo Fighters]]
** [[U2]]
** [[Bon Jovi]]
|-
! style="background:#EEDD82;" ! width="50%" | Best Female Pop Artist
! style="background:#EEDD82;" ! width="50%" | Best Male Pop Artist
|-
| valign="top" |
* '''[[Jennifer Lopez]]'''
** [[Pink (singer)|Pink]]
** [[Mariah Carey]]
** [[Christina Aguilera]]
** [[Britney Spears]]
| valign="top" |
* '''[[Justin Timberlake]]'''
** [[Ricky Martin]]
** [[AJ McLean]]
** [[Aaron Carter]]
** [[Greg Page]]
|-
! style="background:#EEDD82;" ! width="50%" | Best Female Group
! style="background:#EEDD82;" ! width="50%" | Best Male Group
|-
| valign="top" |
* '''[[Spice Girls]]'''
** [[Sugababes]]
** [[All Saints (group)|All Saints]]
** [[Dream (American group)|Dream]]
** [[Atomic Kitten]]
| valign="top" |
* '''[[Black Eyed Peas]]'''
** [[NSYNC]]
** [[Limp Bizkit]], [[Saliva (band)|Saliva]], [[Insane Clown Posse]] & [[Kid Rock]]
** [[Backstreet Boys]]
** [[Westlife]]
|-
! style="background:#EEDD82;" ! width="50%" | Best New Artist
! style="background:#EEDD82;" ! width="50%" | Best Country Artist
|-
| valign="top" |
* '''[[U2]]'''
** [[Red Hot Chili Peppers]]
** [[Justin Timberlake]]
** [[Mariah Carey]]
** [[Madonna]]
| valign="top" |
* '''[[Brad Paisley]]'''
** [[The Chicks|Dixie Chicks]]
** [[Sara Evans]]
** [[Alan Jackson]]
** [[Tim McGraw]]
|-
! style="background:#EEDD82;" ! width="50%" | Best Actress
! style="background:#EEDD82;" ! width="50%" | Best Actor
|-
| valign="top" |
* '''[[Julia Roberts]]'''
** [[Jennifer Lopez]]
** [[Kate Hudson]]
** [[Julia Stiles]]
| valign="top" |
* '''[[Tom Cruise]]'''
** [[Tom Hanks]]
** [[Mel Gibson]]
** [[Russell Crowe]]
|-
! style="background:#EEDD82;" ! width="50%" | Sportswoman of the Year
! style="background:#EEDD82;" ! width="50%" | Sportsman of the Year
|-
| valign="top" |
* '''[[Trish Stratus]]'''
** [[Lita (wrestler)|Lita]]
** [[Chyna]]
** [[Stephanie McMahon]]
| valign="top" |
* '''[[Triple H]]'''
** [[Dwayne Johnson|The Rock]]
** [[Stone Cold Steve Austin]]
** [[The Undertaker]]
|-
! style="background:#EEDD82;" ! colspan=2 | Lifetime Achievement
|-
| colspan="2" valign="top" |
* '''[[Tina Turner]]'''
|}
==References==
{{reflist}}
{{WET Awards}}
[[Category:WET Awards]]
[[Category:2001 awards in the United States]]
{{US-tv-stub}}
0fbmxxduvw6f6hemrdsv31s24d02ydb
749676
749675
2026-07-03T17:26:22Z
~2026-38011-52
74711
/* Performances */
749676
wikitext
text/x-wiki
{{Short description|Award ceremony in Las Vegas, Nevada}}
{{One source|date=June 2025}}
{{Use mdy dates|date=February 2025}}
{{Infobox award
| name = 1st WET Awards
| image =
| caption =
| date = June 19, 2001
| location = [[Paris Las Vegas|Paris Hotel]], [[Las Vegas]], [[Nevada]]
| presenter = [[WET (TV channel)|White Entertainment Television]]
| host = [[Vince McMahon]] <br />[[Shane McMahon]]
| network = [[WET (TV channel)|WET]]
| previous =
| main = {{nowrap|[[WET Awards]]}}
| next = [[WET Awards 2002|2nd]]
}}
The '''1st [[WET Awards]]''' took place at the [[Paris Las Vegas|Paris Hotel]] in [[Las Vegas]], [[Nevada]] on June 19, 2001. The awards recognized Americans in Music, Acting, Sports, and other fields of entertainment over the past year. Professional wrestler [[Vince McMahon]] and [[Shane McMahon]] hosted the event for the first time.
==Performances==
* [[Backstreet Boys]] − "[[More Than That]]"
* [[Lenny Kravitz]], [[Spice Girls]] and [[No Doubt]] − "[[Again (Lenny Kravitz song)|Again]]", "[[Fly Away Part II]]" and "[[Ex-Girlfriend (song)|Ex-Girlfriend]]"
* [[Bonnie Raitt]] and [[Tom Jones (singer)|Tom Jones]] − "[[This Woman's Work]]"
* [[Red Hot Chili Peppers]] and [[Green Day]] − "[[Californication (song)|Californication]]"
* [[Justin Timberlake]] − "[[Pop (NSYNC song)|Pop]]" (with [[NSYNC]]), "[[Anything Can Happen (Justin Timberlake song)|Anything Can Happen]]" and "[[Fantasy (Justin Timberlake song)|Fantasy]]"
* [[Black Eyed Peas|The Black Eyed Peas]] and [[Macy Gray]] − "[[Request + Line]]"
* '''Tina Turner Lifetime Achievement Award Tribute:'''
** [[Annie Lennox]] − "[[Private Dancer (Tina Turner song)|Private Dancer]]"
** [[George Michael]] − "[[What's Love Got to Do with It (song)|What's Love Got to Do with It]]"
* [[Tina Turner]] − "[[The Best (song)#Tina Turner version|The Best]]" and "[[Proud Mary#Ike & Tina Turner version|Proud Mary]]"
* [[Madonna]] − "[[Don't Tell Me (Madonna song)|Don't Tell Me]]"
* [[Linkin Park]] − "[[Crawling (song)|Crawling]]" and "[[One Step Closer (Linkin Park song)|One Step Closer]]"
==Presenters==
==Winners and nominees==
{| class=wikitable width="100%"
! style="background:#EEDD82;" ! width="50%" | Video of the Year
! style="background:#EEDD82;" ! width="50%" | Viewers' Choice
|-
| valign="top" |
* '''[[Linkin Park]] – "[[One Step Closer (Linkin Park song)|One Step Closer]]"'''
** [[Backstreet Boys]] – "[[Shape of My Heart (Backstreet Boys song)|Shape of My Heart]]"
** [[Limp Bizkit]] feat. [[Saliva (band)|Saliva]], [[Insane Clown Posse]] & [[Kid Rock]] – "[[All Summer Long (Limp Bizkit song)|All Summer Long]]"
** [[Robbie Williams]] & [[Kylie Minogue]] – "[[Kids (Robbie Williams and Kylie Minogue song)|Kids]]"
** [[Lenny Kravitz]] – "[[Again (Lenny Kravitz song)|Again]]"
| valign="top" |
* '''[[Red Hot Chili Peppers]] – "[[Californication (song)|Californication]]"'''
** [[No Doubt]] – "[[Ex-Girlfriend (song)|Ex-Girlfriend]]"
** [[Foo Fighters]] – "[[Learn to Fly]]"
** [[Justin Timberlake]] – "[[Anything Can Happen (Justin Timberlake song)|Anything Can Happen]]"
|-
! style="background:#EEDD82;" ! "width="50%" | Best Female Rock Artist
! style="background:#EEDD82;" ! "width="50%" | Best Male Rock Artist
|-
| valign="top" |
* '''[[No Doubt]]'''
** [[PJ Harvey]]
** [[Martina McBride]]
** [[Faith Hill]]
** [[Lee Ann Womack]]
| valign="top" |
* '''[[Green Day]]'''
** [[Linkin Park]]
** [[Foo Fighters]]
** [[U2]]
** [[Bon Jovi]]
|-
! style="background:#EEDD82;" ! width="50%" | Best Female Pop Artist
! style="background:#EEDD82;" ! width="50%" | Best Male Pop Artist
|-
| valign="top" |
* '''[[Jennifer Lopez]]'''
** [[Pink (singer)|Pink]]
** [[Mariah Carey]]
** [[Christina Aguilera]]
** [[Britney Spears]]
| valign="top" |
* '''[[Justin Timberlake]]'''
** [[Ricky Martin]]
** [[AJ McLean]]
** [[Aaron Carter]]
** [[Greg Page]]
|-
! style="background:#EEDD82;" ! width="50%" | Best Female Group
! style="background:#EEDD82;" ! width="50%" | Best Male Group
|-
| valign="top" |
* '''[[Spice Girls]]'''
** [[Sugababes]]
** [[All Saints (group)|All Saints]]
** [[Dream (American group)|Dream]]
** [[Atomic Kitten]]
| valign="top" |
* '''[[Black Eyed Peas]]'''
** [[NSYNC]]
** [[Limp Bizkit]], [[Saliva (band)|Saliva]], [[Insane Clown Posse]] & [[Kid Rock]]
** [[Backstreet Boys]]
** [[Westlife]]
|-
! style="background:#EEDD82;" ! width="50%" | Best New Artist
! style="background:#EEDD82;" ! width="50%" | Best Country Artist
|-
| valign="top" |
* '''[[U2]]'''
** [[Red Hot Chili Peppers]]
** [[Justin Timberlake]]
** [[Mariah Carey]]
** [[Madonna]]
| valign="top" |
* '''[[Brad Paisley]]'''
** [[The Chicks|Dixie Chicks]]
** [[Sara Evans]]
** [[Alan Jackson]]
** [[Tim McGraw]]
|-
! style="background:#EEDD82;" ! width="50%" | Best Actress
! style="background:#EEDD82;" ! width="50%" | Best Actor
|-
| valign="top" |
* '''[[Julia Roberts]]'''
** [[Jennifer Lopez]]
** [[Kate Hudson]]
** [[Julia Stiles]]
| valign="top" |
* '''[[Tom Cruise]]'''
** [[Tom Hanks]]
** [[Mel Gibson]]
** [[Russell Crowe]]
|-
! style="background:#EEDD82;" ! width="50%" | Sportswoman of the Year
! style="background:#EEDD82;" ! width="50%" | Sportsman of the Year
|-
| valign="top" |
* '''[[Trish Stratus]]'''
** [[Lita (wrestler)|Lita]]
** [[Chyna]]
** [[Stephanie McMahon]]
| valign="top" |
* '''[[Triple H]]'''
** [[Dwayne Johnson|The Rock]]
** [[Stone Cold Steve Austin]]
** [[The Undertaker]]
|-
! style="background:#EEDD82;" ! colspan=2 | Lifetime Achievement
|-
| colspan="2" valign="top" |
* '''[[Tina Turner]]'''
|}
==References==
{{reflist}}
{{WET Awards}}
[[Category:WET Awards]]
[[Category:2001 awards in the United States]]
{{US-tv-stub}}
733j8mw2o4swcmeo82xz2cl2vose94g
File:T425216 ESAMS overwrite test.png
6
175206
749884
744187
2026-07-04T09:36:48Z
Alexis Jazz
40203
Alexis Jazz uploaded a new version of [[File:T425216 ESAMS overwrite test.png]]
740635
wikitext
text/x-wiki
== Summary ==
overwrite test
== Licensing ==
{{FAL}}
015m3ga7kqvwocpr8i8xrryx3k2zbnh
749885
749884
2026-07-04T09:37:52Z
Alexis Jazz
40203
Alexis Jazz uploaded a new version of [[File:T425216 ESAMS overwrite test.png]]
740635
wikitext
text/x-wiki
== Summary ==
overwrite test
== Licensing ==
{{FAL}}
015m3ga7kqvwocpr8i8xrryx3k2zbnh
In a Minor Way
0
175237
749711
748254
2026-07-03T19:00:48Z
~2026-38011-52
74711
749711
wikitext
text/x-wiki
{{Use mdy dates|date=May 2025}}
{{Infobox album
| name = In a Minor Way
| type = studio
| artist = [[Snoop Lion (rapper)|Snoop Liony Lion]]
| cover =
| alt =
| released = March 14, 1995
| recorded = 1994
| studio =
| genre = {{flatlist|
*[[Ragga hip-hop]]
*[[reggae rap]]
}}
| length =
| label = {{hlist|[[Lionystyle Records|Lionhouse]]|[[Priarity Records|Priarity]]}}
| producer = {{hlist|[[Snoop Lion (rapper)|Snoop Liony Lion]] <small>(also [[Executive producer|exec.]])</small>|Lloyd "John John" James Jr.|Danny Browne|[[Shaun Pizzonia|Shaun "Sting" Pizzonia]]|D. Juvenile|Robert Livingston|[[Dave Kelly (producer)|Dave Kelly]]}}
| prev_title = [[The Male Man (EP)|The Male Man]]
| prev_year = 1994
| next_title = [[Tha Hall of Fame]]
| next_year = 1996
| misc = {{Singles
| name = In a Minor Way
| type = studio
| single1 = [[Foe Life (song)|Foe Life]]
| single1date = February 25, 1995
| single2 = Ting-A-Ling
| single2date = 1995
| single3 = [[Mr. Loverman (song)|Mr. Loverman]]
| single3date = May 20, 1995
}}
}}
'''''In a Minor Way''''' is the debut studio album by Jamaican-American rapper [[Snoop Lion (rapper)|Snoop Liony Lion]]. It was released on March 14, 1995, by [[Lionystyle Records|Lionhouse Records]] and [[Priarity Records]]. [[Hip hop production|Production]] on the album was handled by Dave Kelly, Shaun "Sting" Pizzonia, Robert Livingston, Lloyd "John John" James Jr., Danny Browne, D. Juvenile, and Snoop Liony Lion himself, who also served as executive producer. It features guest appearances from [[RikRok]], [[Ice Cube]], [[Cutty Ranks]], [[Shabba Ranks]], Mr. Easy and [[Shaggy (musician)|Shaggy]], as well as fellow [[The Clicks (band)|The Clicks]] groupmates Frisco Kid and [[Rayvon]], and his son [[Snoop Dogg|Snoop Doggy Dogg]].
==References==
{{Reflist}}
[[Category:Snoop Lion (rapper) albums]]
[[Category:1995 debut albums]]
[[Category:Albums produced by Dave Kelly (producer)]]
[[Category:Priarity Records albums]]
[[Category:Lionystyle Records albums]]
b3fxyi4kkfvtz001yow5q6miobjpxbb
The Re-Invention
0
175554
749796
748253
2026-07-03T20:51:34Z
~2026-38011-52
74711
749796
wikitext
text/x-wiki
{{Short description|2024 studio mixtape by Sam Moran}}
{{Infobox album
| name = The Re-Invention
| type = [[Studio mixtape]]
| artist = [[Sam Moran]]
| cover =
| alt =
| released = May 22, 2007
| recorded = 2006–2007
| venue =
| studio =
| genre = {{hlist|[[Pop-rap]]|[[dance-rap]]|[[dance-pop]]}}
| length =
| label = {{hlist|[[Roc-A-Wear Records|Roc-A-Wear]]|[[Star Trak Entertainment|Star Trak]]|[[Interscope Records|Interscope]]}}
| producer = {{hlist|[[Jaheim Hines]]|[[Luis Bass]]}}
| prev_title = [[A Mi Sam]]
| prev_year = 2005
| next_title = [[Colour of Love]]
| next_year = 2010
}}
'''''The Re-Invention''''' is the sixth studio mixtape by American-Australian musician [[Sam Moran]]. Is was released on May 22, 2007, by [[Roc-A-Wear Records]], [[Star Trak Entertainment]] and [[Interscope Records]]
==Background==
==Track listing==
[[Category:2007 mixtapes]]
[[Category:Sam Moran mixtape]]
cdzazbnf9nh2hv41y2ztg9bkfn95j3j
User:Supertian8/common.js
2
175863
749852
749585
2026-07-04T03:09:53Z
Supertian8
67751
new and epic
749852
javascript
text/javascript
var gan = {};
window.gan = gan;
$.when(
mw.loader.using([ 'mediawiki.api', 'ext.gadget.morebits' ]),
$.ready
).then(function() {
if (mw.config.get( 'wgPageName' ) === 'User:Supertian8/common.js' ) {
mw.util.addPortletLink('p-cactions', '#', 'Updater', 'gan-portlet', 'Update chapter and episode infoboxes and navboxes');
$('#gan-portlet').click(gan.callback);
}
});
gan.callback = function ganCallback(e) {
if (e) e.preventDefault();
var Window = new Morebits.simpleWindow(600, 450);
Window.setTitle( "Chapter and window updater");
Window.setScriptName('YSY');
//Window.addFooterLink('script documentation', 'User:SD0001/GAN-helper');
var form = new Morebits.quickForm(gan.evaluate);
var title_obj = mw.Title.newFromText(Morebits.pageNameNorm);
gan.title = title_obj.getSubjectPage().toText();
gan.talktitle = title_obj.getTalkPage().toText();
const chapter = form.append({
type: 'field',
label: 'Chapter',
});
chapter.append({
type: 'div',
label: '<a href="https://weibo.com/u/3058754737">Weibo link</a>',
});
chapter.append({
type: 'input',
name: 'chapterNumber',
label: 'Chapter number',
sublabel: 'Characters',
tooltip: 'Enter the chapter number that needs to be updated to',
});
chapter.append({
type: 'checkbox',
list: [
{
name: 'chapterNavbox',
label: 'Edit navbox?',
value: 'chapterNavbox',
checked: true
}
]
});
chapter.append({
type: 'checkbox',
list: [
{
name: 'chapterInfoboxes',
label: 'Edit chapter article infoboxes?',
value: 'chapterInfoboxes',
checked: true
}
]
});
const episode = form.append({
type: 'field',
label: 'Episode',
});
episode.append({
type: 'div',
label: '<a href="https://www.youtube.com/playlist?list=PLBakWosU0sfj_-XZSlGy-IOnDNplD3Ttj">YouTube link</a>',
});
episode.append({
type: 'input',
name: 'episodeNumber',
label: 'Episode number',
sublabel: 'S',
tooltip: 'Enter the episode number that needs to be updated to',
});
episode.append({
type: 'checkbox',
list: [
{
name: 'episodeNavbox',
label: 'Edit navbox?',
value: 'episodeNavbox',
checked: true
}
]
});
episode.append({
type: 'checkbox',
list: [
{
name: 'episodeInfoboxes',
label: 'Edit episode article infoboxes?',
value: 'episodeInfoboxes',
checked: true
}
]
});
form.append({ type: 'submit', label: 'Submit' });
var result = form.render();
Window.setContent(result);
Window.display();
$(Morebits.quickForm.getElementContainer(result.addlog)).css('margin-top', '5px');
};
gan.evaluate = function(e) {
var form = e.target;
gan.params = Morebits.quickForm.getInputData(form);
Morebits.simpleWindow.setButtonsEnabled(false);
Morebits.status.init(form);
var tm = new Morebits.taskManager();
tm.add(gan.tasks.chapter, []);
tm.add(gan.tasks.episode, []);
tm.execute().then(function() {
Morebits.status.actionCompleted('Nomination completed.');
});
};
gan.tasks = {
chapter: function() {
var def = $.Deferred();
var thePage;
//console.log(thePage);
if (gan.params.chapterNumber == "") {
return def.done()
}
if (gan.params.chapterNavbox) {
thePage = new Morebits.wiki.page("User:Supertian8/Navbox chapters");
thePage.load(function(thePage) {
thePage.setPageText(chapterNavbox(gan.params.chapterNumber))
thePage.setEditSummary("Update to [[chapter " + gan.params.chapterNumber + "]]");
thePage.save();
});
}
if (gan.params.chapterInfoboxes) {
for (let chapNum = 1; chapNum < 4; chapNum++){
var chapterNumber = parseInt(gan.params.chapterNumber) - chapNum;
thePage = new Morebits.wiki.page("User:Supertian8/Chapter " + chapterNumber);
thePage.load(function(thePage) {
if (!thePage.exists()) {
return;
}
//console.log(updateChapterInfoboxes(thePage.getPageText(), parseInt(gan.params.chapterNumber) - chapNum, parseInt(gan.params.chapterNumber)))
//thePage.setPageText(updateChapterInfoboxes(thePage.getPageText(), parseInt(gan.params.chapterNumber) - chapNum, parseInt(gan.params.chapterNumber)))
thePage.setEditSummary("Updating infobox for the release of [[chapter " + gan.params.chapterNumber + "]]");
thePage.save();
});
}
}
return def.done();
},
episode: function() {
var def = $.Deferred();
var thePage;
//console.log(thePage);
if (gan.params.episodeNumber == "") {
return def.done()
}
if (gan.params.episodeNavbox) {
thePage = new Morebits.wiki.page("User:Supertian8/Navbox episodes");
thePage.load(function(thePage) {
thePage.setPageText(episodeNavbox(gan.params.episodeNumber))
thePage.setEditSummary("Update to [[episode " + gan.params.episodeNumber + "]]");
thePage.save();
});
}
if (gan.params.episodeInfoboxes) {
for (let epNum = 1; epNum < 4; epNum++){
var episodeNumber = parseInt(gan.params.episodeNumber) - epNum;
thePage = new Morebits.wiki.page("User:Supertian8/Episode " + episodeNumber);
thePage.load(function(thePage) {
if (!thePage.exists()) {
return;
}
thePage.setPageText(updateEpisodeInfoboxes(thePage.getPageText(), parseInt(gan.params.episodeNumber) - epNum, parseInt(gan.params.episodeNumber)))
thePage.setEditSummary("Updating infobox for the release of [[episode " + gan.params.episodeNumber + "]]");
thePage.save();
});
}
}
return def.done();
}
};
//
// ABOVE IS MOREBITS
// BELOW CREATES WIKITEXT
//
function episodeNavbox(numberOfEpisodes) {
let episodeNumber;
let text = `{{#invoke:navbox|navbox
|title=[[Donghua]] episodes
|template=Episodes
|bodyclass=hlist
|state=collapsed`
for (let i = 1; i < (Math.ceil(numberOfEpisodes/12)) + 1; i++) {//seasons
text += "\n";
text += "\n|group" + i + "=Season " + i + "\n|list" + i + "=";//group and list
for (let j = 1; j < Math.min((numberOfEpisodes - ((i-1)*12)), 12) + 1; j++){ //episodes in each season
episodeNumber = (i - 1) * 12 + j;
text += "\n*[[Episode " + episodeNumber + "|"; //episode link
if (episodeNumber < 10){ //since single digit numbers have to be two (e.g 04 not 4).
text += "0" + episodeNumber + "]]"
} else {
text += episodeNumber + "]]"
}
}
}
text += "\n}}<noinclude>[[Cate" + "gory:Navbox templates]]</noinclude>";
return text
}
function chapterNavbox(numberOfChapters){
var text = "{{#switch:{{{1|{{#invoke:String|join||{{trim|{{#invoke:String|sub|{{PAGENAME}}|{{#expr:{{#invoke:String|len|{{PAGENAME}}}}-3}}|{{#expr:{{#invoke:String|len|{{PAGENAME}}}}-2}}}}}}|00-|{{trim|{{#invoke:String|sub|{{PAGENAME}}|{{#expr:{{#invoke:String|len|{{PAGENAME}}}}-3}}|{{#expr:{{#invoke:String|len|{{PAGENAME}}}}-2}}}}}}|99}}}}}"
var navboxHeaderData1 = `=\n{{#invoke:Navbox|navbox|{{#if:{{{child|}}}|child}}
|title=Chapters `
var navboxHeaderData2 = `\n|template=Chapters
|bodyclass=hlist
|titlestyle=background-color:lightblue;
|groupstyle=color:black; background-color:lightblue;
|headerstyle=color:black; background-color:lightblue;
|altliststyle= background-color:#E0F0F5
|state={{{state|collapsed}}}\n`
function nString(n){ //used for n (navbox). Returns empty string if int n is 0.
if (n <= 0) {
return "";
} else {
return String(n);
}
}
function rString(r, n) { //used once for r (row). Returns empty string if r and n are zero. For chapter links <10 work.
if (r <= 0 && n <= 0) {
return "";
} else {
return String(r);
}
}
function isCurrentNavbox(n) {//finds if it is the final navbox, and does not cut off (ends at 99)
if (numberOfChapters % 100 !== 99 && n === Math.floor(numberOfChapters/100)){
//above first clause checks in last two digits aren't 99, second checks if this is the last navbox
return true;
} else {
return false;
}
}
function isCurrentRow (r, n){
if (n === Math.floor(numberOfChapters/100) && r === (Math.floor(numberOfChapters / 10)) % 10 && numberOfChapters % 10 !== 9){
return true;
} else {
return false;
}
}
let l = 1;
let rangeHM;
let rangeEN;
for (let n = 0; n < Math.floor(numberOfChapters/100) + 1; n++) {
rangeHM = String(parseInt(nString(n) + "0" + String(l))) + "-" + nString(n) + "99";
if (isCurrentNavbox(n)) {
rangeEN = String(parseInt(nString(n) + "0" + String(l))) + "–" + "current";
} else {
rangeEN = String(parseInt(nString(n) + "0" + String(l))) + "–" + nString(n) + "99";
}
text += "\n|" + rangeHM + navboxHeaderData1 + rangeEN + navboxHeaderData2;
//above writes range (e.g. 1-99, 900-999) using a hyphen-minus (used in switch) and en dash (used for navbox headers). In en dash, if final
for (let r = 0; r < 10; r++) {
if (isCurrentRow(r, n)){ //if final row of final navbox
text += "|group" + String(r+1) + "=" + nString(n) + String(r) + String(l) + "–current\n";
} else {
text += "|group" + String(r+1) + "=" + nString(n) + String(r) + String(l) + "–" + nString(n) + String(r) + "9\n"
}
text += " |list" + String(r+1) + "=\n";
let number;
while (l < 10) {
number = nString(n) + rString(r, n) + String(l);
if (parseInt(number) > numberOfChapters){
break;
}
text += "*[[Chapter " + number; //adds link to Chapter
text += "|" + nString(n) + String(r) + String(l) + "]]\n"; //adds pipe link
l += 1;
}
l = 0;
}
text += "\n}}\n";
}
text += `}}<noinclude>
{{documentation}}
</noinclude>`;
return text
}
function updateChapterInfoboxes(article, number, numberOfChapters) {
var textArray = article.split(/\r?\n/);
var updated = textArray;
for (const item of textArray){ //gets the full line of where "|nxtchap=" exists
//console.log(item.trim());
if (item.includes("|nxtchap=")) {
//console.log(item.trim());
var nxtchap = (item.trim());
}
}
updated.splice(updated.indexOf(nxtchap), 1); //removes the nxtchap line
console.log(updated);
var diff = numberOfChapters - number;
if (diff === 2) {
const NXTCHAPLINE = updated.indexOf(nxtchap);
var newNxtchap = "|nxtchap=[[Chapter " + (numberOfChapters - 1) + "|" + (numberOfChapters - 1) + "]]";
newNxtchap += ", [[Chapter " + numberOfChapters + "|" + numberOfChapters + "]]";
updated.splice(NXTCHAPLINE, 0, newNxtchap);
} else if (diff === 1) {//runs for the chapter previous to the chapter being added
const NXTCHAPLINE = updated.indexOf(nxtchap);
var newNxtchap = "|nxtchap=[[Chapter " + numberOfChapters + "|" + numberOfChapters + "]]";
updated.splice(NXTCHAPLINE, 0, newNxtchap);
} else if (diff >= 3) {
const NXTCHAPLINE = updated.indexOf(nxtchap);
updated.splice(NXTCHAPLINE, 0);
}
return updated.join("\n");
}
function updateEpisodeInfoboxes(article, number, numberOfEpisodes) {
var textArray = article.split(/\r?\n/);
var updated = textArray;
for (const item of textArray){ //gets the full line of where "|nxtep=" exists
//console.log(item.trim());
if (item.includes("|nxtep=")) {
//console.log(item.trim());
var nxtep = (item.trim());
}
}
updated.splice(updated.indexOf(nxtep), 1); //removes the nxtep line
console.log(updated);
var diff = numberOfEpisodes - number;
if (diff === 2) {
const NXTEPLINE = updated.indexOf(nxtep);
var newNxtep = "|nxtep=[[Episode " + (numberOfEpisodes - 1) + "|" + (numberOfEpisodes - 1) + "]]";
newNxtep += ", [[Episode " + numberOfEpisodes + "|" + numberOfEpisodes + "]]";
updated.splice(NXTEPLINE, 0, newNxtep);
} else if (diff === 1) {//runs for the episode previous to the episode being added
const NXTEPLINE = updated.indexOf(nxtep);
var newNxtep = "|nxtep=[[Episode " + numberOfEpisodes + "|" + numberOfEpisodes + "]]";
updated.splice(NXTEPLINE, 0, newNxtep);
} else if (diff >= 3) {
const NXTEPLINE = updated.indexOf(nxtep);
updated.splice(NXTEPLINE, 0);
}
return updated.join("\n");
}
0hiuta4kve8txs555fu1xdt1dxg3a2i
749858
749852
2026-07-04T04:19:53Z
Supertian8
67751
Uncomment
749858
javascript
text/javascript
var gan = {};
window.gan = gan;
$.when(
mw.loader.using([ 'mediawiki.api', 'ext.gadget.morebits' ]),
$.ready
).then(function() {
if (mw.config.get( 'wgPageName' ) === 'User:Supertian8/common.js' ) {
mw.util.addPortletLink('p-cactions', '#', 'Updater', 'gan-portlet', 'Update chapter and episode infoboxes and navboxes');
$('#gan-portlet').click(gan.callback);
}
});
gan.callback = function ganCallback(e) {
if (e) e.preventDefault();
var Window = new Morebits.simpleWindow(600, 450);
Window.setTitle( "Chapter and window updater");
Window.setScriptName('YSY');
//Window.addFooterLink('script documentation', 'User:SD0001/GAN-helper');
var form = new Morebits.quickForm(gan.evaluate);
var title_obj = mw.Title.newFromText(Morebits.pageNameNorm);
gan.title = title_obj.getSubjectPage().toText();
gan.talktitle = title_obj.getTalkPage().toText();
const chapter = form.append({
type: 'field',
label: 'Chapter',
});
chapter.append({
type: 'div',
label: '<a href="https://weibo.com/u/3058754737">Weibo link</a>',
});
chapter.append({
type: 'input',
name: 'chapterNumber',
label: 'Chapter number',
sublabel: 'Characters',
tooltip: 'Enter the chapter number that needs to be updated to',
});
chapter.append({
type: 'checkbox',
list: [
{
name: 'chapterNavbox',
label: 'Edit navbox?',
value: 'chapterNavbox',
checked: true
}
]
});
chapter.append({
type: 'checkbox',
list: [
{
name: 'chapterInfoboxes',
label: 'Edit chapter article infoboxes?',
value: 'chapterInfoboxes',
checked: true
}
]
});
const episode = form.append({
type: 'field',
label: 'Episode',
});
episode.append({
type: 'div',
label: '<a href="https://www.youtube.com/playlist?list=PLBakWosU0sfj_-XZSlGy-IOnDNplD3Ttj">YouTube link</a>',
});
episode.append({
type: 'input',
name: 'episodeNumber',
label: 'Episode number',
sublabel: 'S',
tooltip: 'Enter the episode number that needs to be updated to',
});
episode.append({
type: 'checkbox',
list: [
{
name: 'episodeNavbox',
label: 'Edit navbox?',
value: 'episodeNavbox',
checked: true
}
]
});
episode.append({
type: 'checkbox',
list: [
{
name: 'episodeInfoboxes',
label: 'Edit episode article infoboxes?',
value: 'episodeInfoboxes',
checked: true
}
]
});
form.append({ type: 'submit', label: 'Submit' });
var result = form.render();
Window.setContent(result);
Window.display();
$(Morebits.quickForm.getElementContainer(result.addlog)).css('margin-top', '5px');
};
gan.evaluate = function(e) {
var form = e.target;
gan.params = Morebits.quickForm.getInputData(form);
Morebits.simpleWindow.setButtonsEnabled(false);
Morebits.status.init(form);
var tm = new Morebits.taskManager();
tm.add(gan.tasks.chapter, []);
tm.add(gan.tasks.episode, []);
tm.execute().then(function() {
Morebits.status.actionCompleted('Nomination completed.');
});
};
gan.tasks = {
chapter: function() {
var def = $.Deferred();
var thePage;
//console.log(thePage);
if (gan.params.chapterNumber == "") {
return def.done()
}
if (gan.params.chapterNavbox) {
thePage = new Morebits.wiki.page("User:Supertian8/Navbox chapters");
thePage.load(function(thePage) {
thePage.setPageText(chapterNavbox(gan.params.chapterNumber))
thePage.setEditSummary("Update to [[chapter " + gan.params.chapterNumber + "]]");
thePage.save();
});
}
if (gan.params.chapterInfoboxes) {
for (let chapNum = 1; chapNum < 4; chapNum++){
var chapterNumber = parseInt(gan.params.chapterNumber) - chapNum;
thePage = new Morebits.wiki.page("User:Supertian8/Chapter " + chapterNumber);
thePage.load(function(thePage) {
if (!thePage.exists()) {
return;
}
//console.log(updateChapterInfoboxes(thePage.getPageText(), parseInt(gan.params.chapterNumber) - chapNum, parseInt(gan.params.chapterNumber)))
thePage.setPageText(updateChapterInfoboxes(thePage.getPageText(), parseInt(gan.params.chapterNumber) - chapNum, parseInt(gan.params.chapterNumber)))
thePage.setEditSummary("Updating infobox for the release of [[chapter " + gan.params.chapterNumber + "]]");
thePage.save();
});
}
}
return def.done();
},
episode: function() {
var def = $.Deferred();
var thePage;
//console.log(thePage);
if (gan.params.episodeNumber == "") {
return def.done()
}
if (gan.params.episodeNavbox) {
thePage = new Morebits.wiki.page("User:Supertian8/Navbox episodes");
thePage.load(function(thePage) {
thePage.setPageText(episodeNavbox(gan.params.episodeNumber))
thePage.setEditSummary("Update to [[episode " + gan.params.episodeNumber + "]]");
thePage.save();
});
}
if (gan.params.episodeInfoboxes) {
for (let epNum = 1; epNum < 4; epNum++){
var episodeNumber = parseInt(gan.params.episodeNumber) - epNum;
thePage = new Morebits.wiki.page("User:Supertian8/Episode " + episodeNumber);
thePage.load(function(thePage) {
if (!thePage.exists()) {
return;
}
thePage.setPageText(updateEpisodeInfoboxes(thePage.getPageText(), parseInt(gan.params.episodeNumber) - epNum, parseInt(gan.params.episodeNumber)))
thePage.setEditSummary("Updating infobox for the release of [[episode " + gan.params.episodeNumber + "]]");
thePage.save();
});
}
}
return def.done();
}
};
//
// ABOVE IS MOREBITS
// BELOW CREATES WIKITEXT
//
function episodeNavbox(numberOfEpisodes) {
let episodeNumber;
let text = `{{#invoke:navbox|navbox
|title=[[Donghua]] episodes
|template=Episodes
|bodyclass=hlist
|state=collapsed`
for (let i = 1; i < (Math.ceil(numberOfEpisodes/12)) + 1; i++) {//seasons
text += "\n";
text += "\n|group" + i + "=Season " + i + "\n|list" + i + "=";//group and list
for (let j = 1; j < Math.min((numberOfEpisodes - ((i-1)*12)), 12) + 1; j++){ //episodes in each season
episodeNumber = (i - 1) * 12 + j;
text += "\n*[[Episode " + episodeNumber + "|"; //episode link
if (episodeNumber < 10){ //since single digit numbers have to be two (e.g 04 not 4).
text += "0" + episodeNumber + "]]"
} else {
text += episodeNumber + "]]"
}
}
}
text += "\n}}<noinclude>[[Cate" + "gory:Navbox templates]]</noinclude>";
return text
}
function chapterNavbox(numberOfChapters){
var text = "{{#switch:{{{1|{{#invoke:String|join||{{trim|{{#invoke:String|sub|{{PAGENAME}}|{{#expr:{{#invoke:String|len|{{PAGENAME}}}}-3}}|{{#expr:{{#invoke:String|len|{{PAGENAME}}}}-2}}}}}}|00-|{{trim|{{#invoke:String|sub|{{PAGENAME}}|{{#expr:{{#invoke:String|len|{{PAGENAME}}}}-3}}|{{#expr:{{#invoke:String|len|{{PAGENAME}}}}-2}}}}}}|99}}}}}"
var navboxHeaderData1 = `=\n{{#invoke:Navbox|navbox|{{#if:{{{child|}}}|child}}
|title=Chapters `
var navboxHeaderData2 = `\n|template=Chapters
|bodyclass=hlist
|titlestyle=background-color:lightblue;
|groupstyle=color:black; background-color:lightblue;
|headerstyle=color:black; background-color:lightblue;
|altliststyle= background-color:#E0F0F5
|state={{{state|collapsed}}}\n`
function nString(n){ //used for n (navbox). Returns empty string if int n is 0.
if (n <= 0) {
return "";
} else {
return String(n);
}
}
function rString(r, n) { //used once for r (row). Returns empty string if r and n are zero. For chapter links <10 work.
if (r <= 0 && n <= 0) {
return "";
} else {
return String(r);
}
}
function isCurrentNavbox(n) {//finds if it is the final navbox, and does not cut off (ends at 99)
if (numberOfChapters % 100 !== 99 && n === Math.floor(numberOfChapters/100)){
//above first clause checks in last two digits aren't 99, second checks if this is the last navbox
return true;
} else {
return false;
}
}
function isCurrentRow (r, n){
if (n === Math.floor(numberOfChapters/100) && r === (Math.floor(numberOfChapters / 10)) % 10 && numberOfChapters % 10 !== 9){
return true;
} else {
return false;
}
}
let l = 1;
let rangeHM;
let rangeEN;
for (let n = 0; n < Math.floor(numberOfChapters/100) + 1; n++) {
rangeHM = String(parseInt(nString(n) + "0" + String(l))) + "-" + nString(n) + "99";
if (isCurrentNavbox(n)) {
rangeEN = String(parseInt(nString(n) + "0" + String(l))) + "–" + "current";
} else {
rangeEN = String(parseInt(nString(n) + "0" + String(l))) + "–" + nString(n) + "99";
}
text += "\n|" + rangeHM + navboxHeaderData1 + rangeEN + navboxHeaderData2;
//above writes range (e.g. 1-99, 900-999) using a hyphen-minus (used in switch) and en dash (used for navbox headers). In en dash, if final
for (let r = 0; r < 10; r++) {
if (isCurrentRow(r, n)){ //if final row of final navbox
text += "|group" + String(r+1) + "=" + nString(n) + String(r) + String(l) + "–current\n";
} else {
text += "|group" + String(r+1) + "=" + nString(n) + String(r) + String(l) + "–" + nString(n) + String(r) + "9\n"
}
text += " |list" + String(r+1) + "=\n";
let number;
while (l < 10) {
number = nString(n) + rString(r, n) + String(l);
if (parseInt(number) > numberOfChapters){
break;
}
text += "*[[Chapter " + number; //adds link to Chapter
text += "|" + nString(n) + String(r) + String(l) + "]]\n"; //adds pipe link
l += 1;
}
l = 0;
}
text += "\n}}\n";
}
text += `}}<noinclude>
{{documentation}}
</noinclude>`;
return text
}
function updateChapterInfoboxes(article, number, numberOfChapters) {
var textArray = article.split(/\r?\n/);
var updated = textArray;
for (const item of textArray){ //gets the full line of where "|nxtchap=" exists
//console.log(item.trim());
if (item.includes("|nxtchap=")) {
//console.log(item.trim());
var nxtchap = (item.trim());
}
}
updated.splice(updated.indexOf(nxtchap), 1); //removes the nxtchap line
console.log(updated);
var diff = numberOfChapters - number;
if (diff === 2) {
const NXTCHAPLINE = updated.indexOf(nxtchap);
var newNxtchap = "|nxtchap=[[Chapter " + (numberOfChapters - 1) + "|" + (numberOfChapters - 1) + "]]";
newNxtchap += ", [[Chapter " + numberOfChapters + "|" + numberOfChapters + "]]";
updated.splice(NXTCHAPLINE, 0, newNxtchap);
} else if (diff === 1) {//runs for the chapter previous to the chapter being added
const NXTCHAPLINE = updated.indexOf(nxtchap);
var newNxtchap = "|nxtchap=[[Chapter " + numberOfChapters + "|" + numberOfChapters + "]]";
updated.splice(NXTCHAPLINE, 0, newNxtchap);
} else if (diff >= 3) {
const NXTCHAPLINE = updated.indexOf(nxtchap);
updated.splice(NXTCHAPLINE, 0);
}
return updated.join("\n");
}
function updateEpisodeInfoboxes(article, number, numberOfEpisodes) {
var textArray = article.split(/\r?\n/);
var updated = textArray;
for (const item of textArray){ //gets the full line of where "|nxtep=" exists
//console.log(item.trim());
if (item.includes("|nxtep=")) {
//console.log(item.trim());
var nxtep = (item.trim());
}
}
updated.splice(updated.indexOf(nxtep), 1); //removes the nxtep line
console.log(updated);
var diff = numberOfEpisodes - number;
if (diff === 2) {
const NXTEPLINE = updated.indexOf(nxtep);
var newNxtep = "|nxtep=[[Episode " + (numberOfEpisodes - 1) + "|" + (numberOfEpisodes - 1) + "]]";
newNxtep += ", [[Episode " + numberOfEpisodes + "|" + numberOfEpisodes + "]]";
updated.splice(NXTEPLINE, 0, newNxtep);
} else if (diff === 1) {//runs for the episode previous to the episode being added
const NXTEPLINE = updated.indexOf(nxtep);
var newNxtep = "|nxtep=[[Episode " + numberOfEpisodes + "|" + numberOfEpisodes + "]]";
updated.splice(NXTEPLINE, 0, newNxtep);
} else if (diff >= 3) {
const NXTEPLINE = updated.indexOf(nxtep);
updated.splice(NXTEPLINE, 0);
}
return updated.join("\n");
}
et37ovk749ui35pxctds6tn9nwy13ns
User:Iiirxs/Main Page
2
175955
749686
749620
2026-07-03T18:46:32Z
Iiirxs
49827
Blanked the page
749686
wikitext
text/x-wiki
phoiac9h4m842xq45sp7s6u21eteeq1
749688
749686
2026-07-03T18:46:45Z
Iiirxs
49827
Updated Main Page via StarterKit tool
749688
wikitext
text/x-wiki
{{User:Iiirxs/Starter kit/Welcome banner}}
<div style="margin-top: 10px;">{{Starter kit/Featured picture}}</div>
{{User:Iiirxs/Starter kit/Content categories}}
<div style="display: flex; flex-wrap: wrap; gap: 4px; align-items: stretch;"><div style="flex: 1 1 300px; min-width: 0; display: flex; flex-direction: column;">{{User:Iiirxs/Starter kit/Featured article}}</div><div style="flex: 1 1 300px; min-width: 0; display: flex; flex-direction: column;">{{User:Iiirxs/Starter kit/In the news}}</div></div>
<div style="display: flex; flex-wrap: wrap; gap: 4px; align-items: stretch;"><div style="flex: 1 1 300px; min-width: 0; display: flex; flex-direction: column;">{{User:Iiirxs/Starter kit/Did you know}}</div><div style="flex: 1 1 300px; min-width: 0; display: flex; flex-direction: column;">{{User:Iiirxs/Starter kit/On this day}}</div></div>
<div style="margin-top: 10px;">{{User:Iiirxs/Starter kit/Featured picture}}</div>
<div style="display: flex; flex-wrap: wrap; gap: 4px; align-items: stretch; margin-top: 10px;"><div style="flex: 1 1 300px; min-width: 0; display: flex; flex-direction: column;">{{User:Iiirxs/Starter kit/Community resources}}</div><div style="flex: 1 1 300px; min-width: 0; display: flex; flex-direction: column;">{{User:Iiirxs/Starter kit/Translation resources}}</div></div>
<div style="margin-top: 10px;">{{User:Iiirxs/Starter kit/Wikipedia's sister projects}}</div>
<div style="margin-top: 10px;">{{User:Iiirxs/Starter kit/Wikipedia languages}}</div>
__NOTOC__
fgddhkbm6qnc2qowc38sgfsasttv0gy
749873
749688
2026-07-04T07:43:30Z
Iiirxs
49827
749873
wikitext
text/x-wiki
{{User:Iiirxs/Starter kit/Welcome banner}}
<div style="margin-top: 10px;">{{Starter kit/Featured picture}}</div>
{{User:Iiirxs/Starter kit/Content categories}}
<div style="display: flex; flex-wrap: wrap; gap: 4px; align-items: stretch;"><div style="flex: 1 1 300px; min-width: 0; display: flex; flex-direction: column;">{{User:Iiirxs/Starter kit/Featured article}}</div><div style="flex: 1 1 300px; min-width: 0; display: flex; flex-direction: column;">{{User:Iiirxs/Starter kit/In the news}}</div></div>
<div style="display: flex; flex-wrap: wrap; gap: 4px; align-items: stretch;"><div style="flex: 1 1 300px; min-width: 0; display: flex; flex-direction: column;">{{User:Iiirxs/Starter kit/Did you know}}</div><div style="flex: 1 1 300px; min-width: 0; display: flex; flex-direction: column;">{{User:Iiirxs/Starter kit/On this day}}</div></div>
<div style="margin-top: 10px;">{{User:Iiirxs/Starter kit/Featured picture [[File:Filename.jpg|320px|left|alt=description]]}}</div>
<div style="display: flex; flex-wrap: wrap; gap: 4px; align-items: stretch; margin-top: 10px;"><div style="flex: 1 1 300px; min-width: 0; display: flex; flex-direction: column;">{{User:Iiirxs/Starter kit/Community resources}}</div><div style="flex: 1 1 300px; min-width: 0; display: flex; flex-direction: column;">{{User:Iiirxs/Starter kit/Translation resources}}</div></div>
<div style="margin-top: 10px;">{{User:Iiirxs/Starter kit/Wikipedia's sister projects}}</div>
<div style="margin-top: 10px;">{{User:Iiirxs/Starter kit/Wikipedia languages}}</div>
__NOTOC__
5to9glwxiolueda2i3blfgyn5lq65n4
749874
749873
2026-07-04T07:44:00Z
Iiirxs
49827
749874
wikitext
text/x-wiki
{{User:Iiirxs/Starter kit/Welcome banner}}
<div style="margin-top: 10px;">{{Starter kit/Featured picture}}</div>
{{User:Iiirxs/Starter kit/Content categories}}
<div style="display: flex; flex-wrap: wrap; gap: 4px; align-items: stretch;"><div style="flex: 1 1 300px; min-width: 0; display: flex; flex-direction: column;">{{User:Iiirxs/Starter kit/Featured article}}</div><div style="flex: 1 1 300px; min-width: 0; display: flex; flex-direction: column;">{{User:Iiirxs/Starter kit/In the news}}</div></div>
<div style="display: flex; flex-wrap: wrap; gap: 4px; align-items: stretch;"><div style="flex: 1 1 300px; min-width: 0; display: flex; flex-direction: column;">{{User:Iiirxs/Starter kit/Did you know}}</div><div style="flex: 1 1 300px; min-width: 0; display: flex; flex-direction: column;">{{User:Iiirxs/Starter kit/On this day}}</div></div>
<div style="margin-top: 10px;">{{User:Iiirxs/Starter kit/Featured picture [[File:Africa Nok Male Figure Kimbell.jpg|320px|left|alt=description]]}}</div>
<div style="display: flex; flex-wrap: wrap; gap: 4px; align-items: stretch; margin-top: 10px;"><div style="flex: 1 1 300px; min-width: 0; display: flex; flex-direction: column;">{{User:Iiirxs/Starter kit/Community resources}}</div><div style="flex: 1 1 300px; min-width: 0; display: flex; flex-direction: column;">{{User:Iiirxs/Starter kit/Translation resources}}</div></div>
<div style="margin-top: 10px;">{{User:Iiirxs/Starter kit/Wikipedia's sister projects}}</div>
<div style="margin-top: 10px;">{{User:Iiirxs/Starter kit/Wikipedia languages}}</div>
__NOTOC__
kokiuinedrf0yw2q3sjw9pn0u05x98r
749875
749874
2026-07-04T07:46:35Z
Iiirxs
49827
749875
wikitext
text/x-wiki
{{User:Iiirxs/Starter kit/Welcome banner}}
<div style="margin-top: 10px;">{{Starter kit/Featured picture}}</div>
{{User:Iiirxs/Starter kit/Content categories}}
<div style="display: flex; flex-wrap: wrap; gap: 4px; align-items: stretch;"><div style="flex: 1 1 300px; min-width: 0; display: flex; flex-direction: column;">{{User:Iiirxs/Starter kit/Featured article}}</div><div style="flex: 1 1 300px; min-width: 0; display: flex; flex-direction: column;">{{User:Iiirxs/Starter kit/In the news}}</div></div>
<div style="display: flex; flex-wrap: wrap; gap: 4px; align-items: stretch;"><div style="flex: 1 1 300px; min-width: 0; display: flex; flex-direction: column;">{{User:Iiirxs/Starter kit/Did you know}}</div><div style="flex: 1 1 300px; min-width: 0; display: flex; flex-direction: column;">{{User:Iiirxs/Starter kit/On this day}}</div></div>
<div style="margin-top: 10px;">{{User:Iiirxs/Starter kit/Featured picture|[[File:Africa Nok Male Figure Kimbell.jpg|320px|left|alt=description]]}}</div>
<div style="display: flex; flex-wrap: wrap; gap: 4px; align-items: stretch; margin-top: 10px;"><div style="flex: 1 1 300px; min-width: 0; display: flex; flex-direction: column;">{{User:Iiirxs/Starter kit/Community resources}}</div><div style="flex: 1 1 300px; min-width: 0; display: flex; flex-direction: column;">{{User:Iiirxs/Starter kit/Translation resources}}</div></div>
<div style="margin-top: 10px;">{{User:Iiirxs/Starter kit/Wikipedia's sister projects}}</div>
<div style="margin-top: 10px;">{{User:Iiirxs/Starter kit/Wikipedia languages}}</div>
__NOTOC__
tq4txwwewwp6346c7c8r903uh8sr99c
749879
749875
2026-07-04T08:02:38Z
Iiirxs
49827
Blanked the page
749879
wikitext
text/x-wiki
phoiac9h4m842xq45sp7s6u21eteeq1
749880
749879
2026-07-04T08:03:05Z
Iiirxs
49827
Updated Main Page via StarterKit tool
749880
wikitext
text/x-wiki
{{User:Iiirxs/Starter kit/Welcome banner}}
{{User:Iiirxs/Starter kit/Content categories}}
<div style="display: flow-root; margin-top: 10px;">{{User:Iiirxs/Starter kit/My feat pic}}</div>
<div style="display: flow-root; margin-top: 10px;">{{User:Iiirxs/Starter kit/Wiki sis}}</div>
<div style="display: flow-root; margin-top: 10px;">{{User:Iiirxs/Starter kit/Wiki langs}}</div>
__NOTOC__
3y18x45bn73blq7e0599wae8drhzd8s
Архів:ДАЗкО/Д
0
176179
749736
748310
2026-07-03T19:17:46Z
Solidest
54422
[[Википедия:К удалению/3 июля 2026#Архів:ДАЗкО/Д|номинация к удалению]] ([[Википедия:Гаджеты/Remover|Remover]])
749736
wikitext
text/x-wiki
<noinclude>{{к удалению|2026-07-03}}</noinclude>
{{заголовок
| назва = [[../]]
| автор =
| секція = Державний архів Закарпатської області
| попередня =
| наступна =
| примітки =
}}
== Фонди ==
{| class="wikitable sortable"
!№||Назва фонду||Крайні дати||Справ
|-
|[[../1/]]||||||
|-
|[[../409/]]||||||
|-
|[[../547/]]||||||
|}
3stjy38658ejfwckckpt649ogidinai
Textvartest
0
176573
749652
749067
2026-07-03T12:11:29Z
~2026-37903-10
74706
/* */
749652
wikitext
text/x-wiki
½�A¢ì0ù�«ÙCVtïL¼ºÒðy?�Qu5�Ýä�ܶ<¾¹S_vs�a"ìÅvyâʤ=�IÄ0�×n'm�
»Ê�õü'¹�ãh
<p>enjoy</p>
===HEX VAR===
8fbd1641868b84a2ec30f90395abd943
5674ef4cbcbad2f0798e3f1c5175351f
dde407dcb63cbeb9535f7673146122ec
c57679e2caa43d1e49c4301980d76e27
6d1b0dbbca14f5fc278db98002c3a368
gaf8fgj76uh0i1vwnnc0ihpp46pxlmn
User:Supertian8/Navbox episodes
2
176729
749798
749589
2026-07-03T21:13:59Z
Supertian8
67751
Update to [[episode ]]
749798
wikitext
text/x-wiki
{{#invoke:navbox|navbox
|title=[[Donghua]] episodes
|template=Episodes
|bodyclass=hlist
|state=collapsed
}}<noinclude>[[Category:Navbox templates]]</noinclude>
scv7l3e90zuzfrfl1b3t5fw333bt44e
749851
749798
2026-07-04T02:49:08Z
Supertian8
67751
Update to [[episode 44]]
749851
wikitext
text/x-wiki
{{#invoke:navbox|navbox
|title=[[Donghua]] episodes
|template=Episodes
|bodyclass=hlist
|state=collapsed
|group1=Season 1
|list1=
*[[Episode 1|01]]
*[[Episode 2|02]]
*[[Episode 3|03]]
*[[Episode 4|04]]
*[[Episode 5|05]]
*[[Episode 6|06]]
*[[Episode 7|07]]
*[[Episode 8|08]]
*[[Episode 9|09]]
*[[Episode 10|10]]
*[[Episode 11|11]]
*[[Episode 12|12]]
|group2=Season 2
|list2=
*[[Episode 13|13]]
*[[Episode 14|14]]
*[[Episode 15|15]]
*[[Episode 16|16]]
*[[Episode 17|17]]
*[[Episode 18|18]]
*[[Episode 19|19]]
*[[Episode 20|20]]
*[[Episode 21|21]]
*[[Episode 22|22]]
*[[Episode 23|23]]
*[[Episode 24|24]]
|group3=Season 3
|list3=
*[[Episode 25|25]]
*[[Episode 26|26]]
*[[Episode 27|27]]
*[[Episode 28|28]]
*[[Episode 29|29]]
*[[Episode 30|30]]
*[[Episode 31|31]]
*[[Episode 32|32]]
*[[Episode 33|33]]
*[[Episode 34|34]]
*[[Episode 35|35]]
*[[Episode 36|36]]
|group4=Season 4
|list4=
*[[Episode 37|37]]
*[[Episode 38|38]]
*[[Episode 39|39]]
*[[Episode 40|40]]
*[[Episode 41|41]]
*[[Episode 42|42]]
*[[Episode 43|43]]
*[[Episode 44|44]]
}}<noinclude>[[Category:Navbox templates]]</noinclude>
1umr2zba7onzjims78qnt7v16tnwu4a
User:Iiirxs/Starter kit/fafava
2
176744
749651
2026-07-03T12:09:43Z
Iiirxs
49827
Initialised by StarterKit tool — ready for translation
749651
wikitext
text/x-wiki
<div style="text-align: center; font-family: 'Linux Libertine', Georgia, Times, serif; margin: 1.5em 0;">
<span style="font-size: 2.3em; line-height: 1.2;">Welcome to {{#language:{{PAGELANGUAGE}}}} Wikipedia</span><br/>
<span style="font-size: 1.1em; color: #54595d;">The free encyclopedia that anyone can edit</span><br/>
<span style="font-size: 0.95em; color: #72777d; margin-top: 0.5em; display: inline-block;">{{NUMBEROFACTIVEUSERS}} active editors • '''{{NUMBEROFARTICLES}}''' articles in this {{SITENAME}}</span>
</div>
<noinclude>[[Category:Starter kit templates]]</noinclude>
2gaz0l1265uxned195lmw3a6r0oocxa
Template:Arg1
10
176745
749653
2026-07-03T12:21:09Z
STyx
131
Created page with "{{{1|oups|!}}}"
749653
wikitext
text/x-wiki
{{{1|oups|!}}}
2p8br2y66ynaz1wehavz3neyhrq3xam
749657
749653
2026-07-03T12:43:31Z
STyx
131
749657
wikitext
text/x-wiki
{{{1|oups}}}
1b75axl7gtip8f69lbbr7zop9tf9e3p
Template:Oups
10
176746
749656
2026-07-03T12:31:36Z
STyx
131
Created page with "Oups! 1={{{1}}} 2={{{2}}} 3={{{3}}}"
749656
wikitext
text/x-wiki
Oups! 1={{{1}}} 2={{{2}}} 3={{{3}}}
0l0ktsmkkggdhxldm4cmd9zdr8ojxed
Template:RBrace
10
176747
749659
2026-07-03T13:06:26Z
STyx
131
Created page with "}"
749659
wikitext
text/x-wiki
}
mqtv18gbq0wyfami4swjiakaywfnlgq
Template:LBrace
10
176748
749661
2026-07-03T13:12:54Z
STyx
131
Created page with "{"
749661
wikitext
text/x-wiki
{
barcc157cfkdqwkh4ctm4cy8nomy3wl
Template:Call1
10
176749
749662
2026-07-03T13:18:58Z
STyx
131
Created page with "{{{{{1}}}}}"
749662
wikitext
text/x-wiki
{{{{{1}}}}}
053pvaknwddet2fapsdayvmv5jecy6o
User:Iiirxs/Starter kit/fafafa
2
176750
749663
2026-07-03T13:21:38Z
Iiirxs
49827
Initialised by StarterKit tool — ready for translation
749663
wikitext
text/x-wiki
<div style="text-align: center; font-family: 'Linux Libertine', Georgia, Times, serif; margin: 1.5em 0;">
<span style="font-size: 2.3em; line-height: 1.2;">Welcome to {{#language:{{PAGELANGUAGE}}}} Wikipedia</span><br/>
<span style="font-size: 1.1em; color: #54595d;">The free encyclopedia that anyone can edit</span><br/>
<span style="font-size: 0.95em; color: #72777d; margin-top: 0.5em; display: inline-block;">{{NUMBEROFACTIVEUSERS}} active editors • '''{{NUMBEROFARTICLES}}''' articles in this {{SITENAME}}</span>
</div>
<noinclude>[[Category:Starter kit templates]]</noinclude>
2gaz0l1265uxned195lmw3a6r0oocxa
User:Iiirxs/Starter kit/Featured picture
2
176751
749664
2026-07-03T13:21:50Z
Iiirxs
49827
Initialised by StarterKit tool — ready for translation
749664
wikitext
text/x-wiki
<div style="border:1px solid #ddcef2;border-radius:4px;padding:8px;background:#faf5ff;margin-top:4px;">
<div style="padding:4px 12px;margin-bottom:8px;border:1px solid #afa3bf;border-radius:4px;background:#ddcef2;">'''Featured picture'''</div>
<!-- Add any image that represents your wiki well — from Commons or locally uploaded.
No need to update daily — refresh whenever a good image is available.
To add the image: [[File:Filename.jpg|320px|left|alt=description]] -->
Add image title here
Add a description of the image here (2–3 sentences).
<small>''Credit: Add photographer or source credit here''</small>
</div>
<noinclude>[[Category:Starter kit templates]]</noinclude>
hb6ar7x7agg9zv1swf3d6eou995o6u4
User:Iiirxs/Starter kit/Wiki sis
2
176752
749665
2026-07-03T13:22:19Z
Iiirxs
49827
Initialised by StarterKit tool — ready for translation
749665
wikitext
text/x-wiki
<div style="border:1px solid #CBD5E1;border-radius:4px;background:#ffffff;overflow:hidden;margin-bottom:16px;">
<div style="background:#F8FAFC;border-bottom:1px solid #CBD5E1;padding:8px 16px;font-weight:bold;">Wikipedia's sister projects</div>
<div style="padding:16px;">
Wikipedia is written by volunteer editors and hosted by the [https://wikimediafoundation.org/ Wikimedia Foundation], a non-profit organization that also hosts a range of other volunteer [https://wikimediafoundation.org/our-work/wikimedia-projects/ projects]:
<ul style="list-style:none;margin:8px 0 0 0;padding:0;display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:8px;">
<li style="display:flex;align-items:center;gap:16px;padding:4px 0;">[[File:Commons-logo.svg|31px|link=https://commons.wikimedia.org/|alt=Commons logo]]<span>[https://commons.wikimedia.org/ Commons]<br/><small style="color:#555;">Free media repository</small></span></li>
<li style="display:flex;align-items:center;gap:16px;padding:4px 0;">[[File:MediaWiki-2020-icon.svg|35px|link=https://www.mediawiki.org/|alt=MediaWiki logo]]<span>[https://www.mediawiki.org/ MediaWiki]<br/><small style="color:#555;">Wiki software development</small></span></li>
<li style="display:flex;align-items:center;gap:16px;padding:4px 0;">[[File:Wikimedia Community Logo.svg|35px|link=https://meta.wikimedia.org/|alt=Meta-Wiki logo]]<span>[https://meta.wikimedia.org/ Meta-Wiki]<br/><small style="color:#555;">Wikimedia project coordination</small></span></li>
<li style="display:flex;align-items:center;gap:16px;padding:4px 0;">[[File:Wikibooks-logo.svg|35px|link=https://www.wikibooks.org/|alt=Wikibooks logo]]<span>[https://www.wikibooks.org/ Wikibooks]<br/><small style="color:#555;">Free textbooks and manuals</small></span></li>
<li style="display:flex;align-items:center;gap:16px;padding:4px 0;">[[File:Wikidata-logo.svg|47px|link=https://www.wikidata.org/|alt=Wikidata logo]]<span>[https://www.wikidata.org/ Wikidata]<br/><small style="color:#555;">Free knowledge base</small></span></li>
<li style="display:flex;align-items:center;gap:16px;padding:4px 0;">[[File:Wikinews-logo.svg|51px|link=https://www.wikinews.org/|alt=Wikinews logo]]<span>[https://www.wikinews.org/ Wikinews]<br/><small style="color:#555;">Free-content news</small></span></li>
<li style="display:flex;align-items:center;gap:16px;padding:4px 0;">[[File:Wikiquote-logo.svg|35px|link=https://www.wikiquote.org/|alt=Wikiquote logo]]<span>[https://www.wikiquote.org/ Wikiquote]<br/><small style="color:#555;">Collection of quotations</small></span></li>
<li style="display:flex;align-items:center;gap:16px;padding:4px 0;">[[File:Wikisource-logo.svg|35px|link=https://www.wikisource.org/|alt=Wikisource logo]]<span>[https://www.wikisource.org/ Wikisource]<br/><small style="color:#555;">Free-content library</small></span></li>
<li style="display:flex;align-items:center;gap:16px;padding:4px 0;">[[File:Wikispecies-logo.svg|35px|link=https://species.wikimedia.org/|alt=Wikispecies logo]]<span>[https://species.wikimedia.org/ Wikispecies]<br/><small style="color:#555;">Directory of species</small></span></li>
<li style="display:flex;align-items:center;gap:16px;padding:4px 0;">[[File:Wikiversity logo 2017.svg|41px|link=https://www.wikiversity.org/|alt=Wikiversity logo]]<span>[https://www.wikiversity.org/ Wikiversity]<br/><small style="color:#555;">Free learning tools</small></span></li>
<li style="display:flex;align-items:center;gap:16px;padding:4px 0;">[[File:Wikivoyage-Logo-v3-icon.svg|35px|link=https://www.wikivoyage.org/|alt=Wikivoyage logo]]<span>[https://www.wikivoyage.org/ Wikivoyage]<br/><small style="color:#555;">Free travel guide</small></span></li>
<li style="display:flex;align-items:center;gap:16px;padding:4px 0;">[[File:Wiktionary-logo-v2.svg|35px|link=https://www.wiktionary.org/|alt=Wiktionary logo]]<span>[https://www.wiktionary.org/ Wiktionary]<br/><small style="color:#555;">Dictionary and thesaurus</small></span></li>
</ul>
</div>
</div>
<noinclude>[[Category:Starter kit templates]]</noinclude>
sqqnlmlgl2eejn6h1edrw231bhol386
User:Iiirxs/Starter kit/Wiki langs
2
176753
749666
2026-07-03T13:22:30Z
Iiirxs
49827
Initialised by StarterKit tool — ready for translation
749666
wikitext
text/x-wiki
<div style="border:1px solid #CBD5E1;border-radius:4px;background:#ffffff;overflow:hidden;">
<div style="background:#F8FAFC;border-bottom:1px solid #CBD5E1;padding:8px 16px;font-weight:bold;">Wikipedia languages</div>
<div style="padding:16px;">
Many [https://meta.wikimedia.org/wiki/List_of_Wikipedias other Wikipedias are available]; some of the largest are listed below.
<ul style="list-style:none;margin:8px 0 0 0;padding:0;">
<li style="margin-bottom:12px;">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;">
<div style="flex:1;height:1px;background:#CBD5E1;"></div>
<div style="font-weight:bold;white-space:nowrap;">1,000,000+ articles</div>
<div style="flex:1;height:1px;background:#CBD5E1;"></div>
</div>
<div style="line-height:2;">
[https://ar.wikipedia.org/wiki/ العربية] ·
[https://de.wikipedia.org/wiki/ Deutsch] ·
[https://es.wikipedia.org/wiki/ Español] ·
[https://fa.wikipedia.org/wiki/ فارسی]‎ ·
[https://fr.wikipedia.org/wiki/ Français] ·
[https://it.wikipedia.org/wiki/ Italiano] ·
[https://nl.wikipedia.org/wiki/ Nederlands] ·
[https://ja.wikipedia.org/wiki/ 日本語] ·
[https://pl.wikipedia.org/wiki/ Polski] ·
[https://pt.wikipedia.org/wiki/ Português] ·
[https://ru.wikipedia.org/wiki/ Русский] ·
[https://sv.wikipedia.org/wiki/ Svenska] ·
[https://uk.wikipedia.org/wiki/ Українська] ·
[https://vi.wikipedia.org/wiki/ Tiếng Việt] ·
[https://zh.wikipedia.org/wiki/ 中文]
</div>
</li>
<li style="margin-bottom:12px;">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;">
<div style="flex:1;height:1px;background:#CBD5E1;"></div>
<div style="font-weight:bold;white-space:nowrap;">250,000+ articles</div>
<div style="flex:1;height:1px;background:#CBD5E1;"></div>
</div>
<div style="line-height:2;">
[https://id.wikipedia.org/wiki/ Bahasa Indonesia] ·
[https://ms.wikipedia.org/wiki/ Bahasa Melayu] ·
[https://nan.wikipedia.org/wiki/ 閩南語] ·
[https://bg.wikipedia.org/wiki/ Български] ·
[https://ca.wikipedia.org/wiki/ Català] ·
[https://cs.wikipedia.org/wiki/ Čeština] ·
[https://da.wikipedia.org/wiki/ Dansk] ·
[https://et.wikipedia.org/wiki/ Eesti] ·
[https://el.wikipedia.org/wiki/ Ελληνικά] ·
[https://eo.wikipedia.org/wiki/ Esperanto] ·
[https://eu.wikipedia.org/wiki/ Euskara] ·
[https://he.wikipedia.org/wiki/ עברית] ·
[https://hy.wikipedia.org/wiki/ Հայերեն] ·
[https://ko.wikipedia.org/wiki/ 한국어] ·
[https://hu.wikipedia.org/wiki/ Magyar] ·
[https://no.wikipedia.org/wiki/ Norsk] ·
[https://ro.wikipedia.org/wiki/ Română] ·
[https://simple.wikipedia.org/wiki/ Simple English] ·
[https://sk.wikipedia.org/wiki/ Slovenčina] ·
[https://sr.wikipedia.org/wiki/ Српски] ·
[https://sh.wikipedia.org/wiki/ Srpskohrvatski] ·
[https://fi.wikipedia.org/wiki/ Suomi] ·
[https://tr.wikipedia.org/wiki/ Türkçe] ·
[https://uz.wikipedia.org/wiki/ Oʻzbek]
</div>
</li>
<li style="margin-bottom:4px;">
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;">
<div style="flex:1;height:1px;background:#CBD5E1;"></div>
<div style="font-weight:bold;white-space:nowrap;">50,000+ articles</div>
<div style="flex:1;height:1px;background:#CBD5E1;"></div>
</div>
<div style="line-height:2;">
[https://ast.wikipedia.org/wiki/ Asturianu] ·
[https://az.wikipedia.org/wiki/ Azərbaycanca] ·
[https://bn.wikipedia.org/wiki/ বাংলা] ·
[https://bs.wikipedia.org/wiki/ Bosanski] ·
[https://ckb.wikipedia.org/wiki/ کوردی] ·
[https://fy.wikipedia.org/wiki/ Frysk] ·
[https://ga.wikipedia.org/wiki/ Gaeilge] ·
[https://gl.wikipedia.org/wiki/ Galego] ·
[https://hr.wikipedia.org/wiki/ Hrvatski] ·
[https://ka.wikipedia.org/wiki/ ქართული] ·
[https://ku.wikipedia.org/wiki/ Kurdî] ·
[https://lv.wikipedia.org/wiki/ Latviešu] ·
[https://lt.wikipedia.org/wiki/ Lietuvių] ·
[https://ml.wikipedia.org/wiki/ മലയാളം] ·
[https://mk.wikipedia.org/wiki/ Македонски] ·
[https://my.wikipedia.org/wiki/ မြန်မာဘာသာ] ·
[https://nn.wikipedia.org/wiki/ Norsk nynorsk] ·
[https://pa.wikipedia.org/wiki/ ਪੰਜਾਬੀ] ·
[https://sq.wikipedia.org/wiki/ Shqip] ·
[https://sl.wikipedia.org/wiki/ Slovenščina] ·
[https://th.wikipedia.org/wiki/ ไทย] ·
[https://te.wikipedia.org/wiki/ తెలుగు] ·
[https://ur.wikipedia.org/wiki/ اردو]
</div>
</li>
</ul>
</div>
</div>
<noinclude>[[Category:Starter kit templates]]</noinclude>
qfil1o4vm3tqidfgmg9xjryqel5ppoz
User:Iiirxs/Starter kit/Wel-b
2
176754
749667
2026-07-03T13:45:26Z
Iiirxs
49827
Initialised by StarterKit tool — ready for translation
749667
wikitext
text/x-wiki
<div style="text-align: center; font-family: 'Linux Libertine', Georgia, Times, serif; margin: 1.5em 0;">
<span style="font-size: 2.3em; line-height: 1.2;">Welcome to {{#language:{{PAGELANGUAGE}}}} Wikipedia</span><br/>
<span style="font-size: 1.1em; color: #54595d;">The free encyclopedia that anyone can edit</span><br/>
<span style="font-size: 0.95em; color: #72777d; margin-top: 0.5em; display: inline-block;">{{NUMBEROFACTIVEUSERS}} active editors • '''{{NUMBEROFARTICLES}}''' articles in this {{SITENAME}}</span>
</div>
<noinclude>[[Category:Starter kit templates]]</noinclude>
2gaz0l1265uxned195lmw3a6r0oocxa
Template:Arg?
10
176755
749668
2026-07-03T13:56:38Z
STyx
131
Created page with "{{{{{{{1|1}}}|unused argument {{{1|1}}}.}}}"
749668
wikitext
text/x-wiki
{{{{{{{1|1}}}|unused argument {{{1|1}}}.}}}
ccuwyhl5mb2dx091hnqadite4dth0ss
749669
749668
2026-07-03T13:58:39Z
STyx
131
749669
wikitext
text/x-wiki
{{{{{{1|1}}}|unused argument {{{1|1}}}.}}}
st4kuymn93u1s0ypnc1f9swhteuyd80
User:Iiirxs/Starter kit/Welcome banner
2
176756
749671
2026-07-03T16:38:58Z
Iiirxs
49827
Initialised by StarterKit tool — ready for translation
749671
wikitext
text/x-wiki
<div style="text-align: center; font-family: 'Linux Libertine', Georgia, Times, serif; margin: 1.5em 0;">
<span style="font-size: 2.3em; line-height: 1.2;">Welcome to {{#language:{{PAGELANGUAGE}}}} Wikipedia</span><br/>
<span style="font-size: 1.1em; color: #54595d;">The free encyclopedia that anyone can edit</span><br/>
<span style="font-size: 0.95em; color: #72777d; margin-top: 0.5em; display: inline-block;">{{NUMBEROFACTIVEUSERS}} active editors • '''{{NUMBEROFARTICLES}}''' articles in this {{SITENAME}}</span>
</div>
<noinclude>[[Category:Starter kit templates]]</noinclude>
2gaz0l1265uxned195lmw3a6r0oocxa
Wikipedia:Report wizard/Content
4
176757
749677
2026-07-03T18:37:31Z
Chaotic Enby
58843
wizards are multiplying
749677
wikitext
text/x-wiki
{{hidden title}}{{Wikipedia:ANI wizard/Header|{{{title}}}}}
<div style="padding-top:3px">
{|
!
!
!
|-
| <div style="width:80px"></div>
| {{{content}}}
{{#if: {{{no_button_pad|}}} | <div> | <div style="padding-top:5px">}}
{{#if: {{{other_buttons|}}} | {{{other_buttons}}} | }}
{{#if: {{{next_link|}}} | {{Clickable button 2|{{{next_link}}}|{{#if: {{{next_link_label|}}} | {{{next_link_label|}}}| Next > }}|class={{{next_button_class|mw-ui-progressive}}}}} | }}
</div>
| <div style="width:80px"></div>
|}
</div>
7vzl0psx9iitcluvtxn7dx28pn6jdy2
749691
749677
2026-07-03T18:46:50Z
Chaotic Enby
58843
Chaotic Enby moved page [[Wikipedia:ANI wizard/Content]] to [[Wikipedia:Report wizard/Content]]
749677
wikitext
text/x-wiki
{{hidden title}}{{Wikipedia:ANI wizard/Header|{{{title}}}}}
<div style="padding-top:3px">
{|
!
!
!
|-
| <div style="width:80px"></div>
| {{{content}}}
{{#if: {{{no_button_pad|}}} | <div> | <div style="padding-top:5px">}}
{{#if: {{{other_buttons|}}} | {{{other_buttons}}} | }}
{{#if: {{{next_link|}}} | {{Clickable button 2|{{{next_link}}}|{{#if: {{{next_link_label|}}} | {{{next_link_label|}}}| Next > }}|class={{{next_button_class|mw-ui-progressive}}}}} | }}
</div>
| <div style="width:80px"></div>
|}
</div>
7vzl0psx9iitcluvtxn7dx28pn6jdy2
749695
749691
2026-07-03T18:47:16Z
Chaotic Enby
58843
749695
wikitext
text/x-wiki
{{hidden title}}{{Wikipedia:Report wizard/Header|{{{title}}}}}
<div style="padding-top:3px">
{|
!
!
!
|-
| <div style="width:80px"></div>
| {{{content}}}
{{#if: {{{no_button_pad|}}} | <div> | <div style="padding-top:5px">}}
{{#if: {{{other_buttons|}}} | {{{other_buttons}}} | }}
{{#if: {{{next_link|}}} | {{Clickable button 2|{{{next_link}}}|{{#if: {{{next_link_label|}}} | {{{next_link_label|}}}| Next > }}|class={{{next_button_class|mw-ui-progressive}}}}} | }}
</div>
| <div style="width:80px"></div>
|}
</div>
s1otnuszzhvoby676gf64ohgsf2baui
749716
749695
2026-07-03T19:04:40Z
Chaotic Enby
58843
won't be used
749716
wikitext
text/x-wiki
{{hidden title}}{{Wikipedia:Report wizard/Header|{{{title}}}}}
<div style="padding-top:3px">
{|
!
!
!
|-
| <div style="width:80px"></div>
| {{{content}}}
<div>
{{#if: {{{other_buttons|}}} | {{{other_buttons}}} | }}
</div>
| <div style="width:80px"></div>
|}
</div>
92o5ac2fdhmlosb6yop46u6dknwgmsu
Wikipedia:Report wizard/Header
4
176758
749678
2026-07-03T18:41:21Z
Chaotic Enby
58843
Created page with "{| ! style="width:4em" | ! style="width:90%" | |- | style="vertical-align:top" | <div style="padding-right: 5px; padding-top:3px;">[[File:Codex icon flag color-progressive.svg|30px|link=Wikipedia:ANI wizard|right]]</div> | {{font|size=150%|Wikipedia ANI Wizard{{#ifeq:{{#titleparts: {{FULLPAGENAME}} | 1 | 2 }}|Demo| – Demo Mode|}}}} <div style="position:relative; top:-5px;">{{font|size=120%|'''{{{1}}}'''}}</div> |}"
749678
wikitext
text/x-wiki
{|
! style="width:4em" |
! style="width:90%" |
|-
| style="vertical-align:top" | <div style="padding-right: 5px; padding-top:3px;">[[File:Codex icon flag color-progressive.svg|30px|link=Wikipedia:ANI wizard|right]]</div>
| {{font|size=150%|Wikipedia ANI Wizard{{#ifeq:{{#titleparts: {{FULLPAGENAME}} | 1 | 2 }}|Demo| – Demo Mode|}}}}
<div style="position:relative; top:-5px;">{{font|size=120%|'''{{{1}}}'''}}</div>
|}
24ng9gtaawwn7vrijj1gix7iqh0afyv
749679
749678
2026-07-03T18:42:09Z
Chaotic Enby
58843
flag might be a bit too scary
749679
wikitext
text/x-wiki
{|
! style="width:4em" |
! style="width:90%" |
|-
| style="vertical-align:top" | <div style="padding-right: 5px; padding-top:3px;">[[File:Codex icon ongoingConversation color-progressive.svg|30px|link=Wikipedia:ANI wizard|right]]</div>
| {{font|size=150%|Report Wizard{{#ifeq:{{#titleparts: {{FULLPAGENAME}} | 1 | 2 }}|Demo| – Demo Mode|}}}}
<div style="position:relative; top:-5px;">{{font|size=120%|'''{{{1}}}'''}}</div>
|}
dtgs76al2huuuiyiclentna21rrnhmc
749681
749679
2026-07-03T18:42:48Z
Chaotic Enby
58843
749681
wikitext
text/x-wiki
{|
! style="width:4em" |
! style="width:90%" |
|-
| style="vertical-align:top" | <div style="padding-right: 5px; padding-top:3px;">[[File:Codex icon invisibleComment color-progressive.svg|30px|link=Wikipedia:ANI wizard|right]]</div>
| {{font|size=150%|Report Wizard{{#ifeq:{{#titleparts: {{FULLPAGENAME}} | 1 | 2 }}|Demo| – Demo Mode|}}}}
<div style="position:relative; top:-5px;">{{font|size=120%|'''{{{1}}}'''}}</div>
|}
epcv8r27bdn4k393xcj88mbm2ogoxuw
749693
749681
2026-07-03T18:46:50Z
Chaotic Enby
58843
Chaotic Enby moved page [[Wikipedia:ANI wizard/Header]] to [[Wikipedia:Report wizard/Header]]
749681
wikitext
text/x-wiki
{|
! style="width:4em" |
! style="width:90%" |
|-
| style="vertical-align:top" | <div style="padding-right: 5px; padding-top:3px;">[[File:Codex icon invisibleComment color-progressive.svg|30px|link=Wikipedia:ANI wizard|right]]</div>
| {{font|size=150%|Report Wizard{{#ifeq:{{#titleparts: {{FULLPAGENAME}} | 1 | 2 }}|Demo| – Demo Mode|}}}}
<div style="position:relative; top:-5px;">{{font|size=120%|'''{{{1}}}'''}}</div>
|}
epcv8r27bdn4k393xcj88mbm2ogoxuw
User:Iiirxs/Starter kit/In the news
2
176759
749680
2026-07-03T18:42:42Z
Iiirxs
49827
Initialised by StarterKit tool — ready for translation
749680
wikitext
text/x-wiki
<div style="border:1px solid #cedff2;border-radius:4px;padding:8px;background:#f5faff;margin-bottom:2px;">
<div style="padding:4px 12px;margin-bottom:8px;border:1px solid #a3b0bf;border-radius:4px;background:#cedff2;">'''In the news'''</div>
<!-- Add recent events or news relevant to your community or topic area.
No need to update daily — refresh when something noteworthy happens.
To add an image: [[File:Filename.jpg|64px|right|alt=description]] -->
* Add a recent event or news item and link to a relevant article here.
* Add a recent event or news item and link to a relevant article here.
* Add a recent event or news item and link to a relevant article here.
</div>
<noinclude>[[Category:Starter kit templates]]</noinclude>
40fusnlv1gt2lbcuslxiephm1ol2ddu
User:Iiirxs/Starter kit/Did you know
2
176760
749682
2026-07-03T18:43:47Z
Iiirxs
49827
Initialised by StarterKit tool — ready for translation
749682
wikitext
text/x-wiki
<div style="border:1px solid #cef2e0;border-radius:4px;padding:8px;background:#f5fffa;margin-bottom:2px;">
<div style="padding:4px 12px;margin-bottom:8px;border:1px solid #a3bfb1;border-radius:4px;background:#cef2e0;">'''Did you know ...'''</div>
<!-- Update with 3–5 interesting facts from recently created or expanded articles.
No need to update daily — refresh when new articles are added.
To add an image: [[File:Filename.jpg|80px|left|alt=description]] -->
* ... that add an interesting fact and link to a relevant article here?
* ... that add an interesting fact and link to a relevant article here?
* ... that add an interesting fact and link to a relevant article here?
</div>
<noinclude>[[Category:Starter kit templates]]</noinclude>
efghg1hxbk1i2x5pquqhdu10ck1081z
User:Iiirxs/Starter kit/On this day
2
176761
749683
2026-07-03T18:43:54Z
Iiirxs
49827
Initialised by StarterKit tool — ready for translation
749683
wikitext
text/x-wiki
<div style="border:1px solid #cedff2;border-radius:4px;padding:8px;background:#f5faff;margin-bottom:2px;">
<div style="padding:4px 12px;margin-bottom:8px;border:1px solid #a3b0bf;border-radius:4px;background:#cedff2;">'''On this day'''</div>
<!-- Add 2–3 historical events relevant to your community or topic area.
No need to update daily — refresh occasionally as your wiki grows.
To add an image: [[File:Filename.jpg|80px|right|alt=description]] -->
Add a date here
* Add a historical event and link to a relevant article here.
* Add a historical event and link to a relevant article here.
</div>
<noinclude>[[Category:Starter kit templates]]</noinclude>
dexflvpfy0pjrd6j1pi3ku8mr24184e
User:Iiirxs/Starter kit/Community resources
2
176762
749684
2026-07-03T18:44:14Z
Iiirxs
49827
Initialised by StarterKit tool — ready for translation
749684
wikitext
text/x-wiki
<div style="border:1px solid #CBD5E1;border-radius:4px;background:white;overflow:hidden;margin-bottom:2px;">
<div style="background:#F1F5F9;border-bottom:1px solid #CBD5E1;padding:8px 16px;">'''Community resources'''</div>
<div style="padding:16px;">
<!-- Add links to pages that help your community connect and collaborate.
Replace each placeholder below with a real page, or remove lines that don't apply yet. -->
* Add a link to a discussion venue where the community can talk about the wiki.
* Add a link to a welcome or help page for new editors.
* Add a link to an upcoming or recurring community event.
* Add a link to any other community resource relevant to your wiki.
</div>
</div>
<noinclude>[[Category:Starter kit templates]]</noinclude>
9qpbhbxyfrphef8hiaacfno4y7tohtc
User:Iiirxs/Starter kit/Translation resources
2
176763
749685
2026-07-03T18:44:27Z
Iiirxs
49827
Initialised by StarterKit tool — ready for translation
749685
wikitext
text/x-wiki
<div style="border:1px solid #CBD5E1;border-radius:4px;background:white;overflow:hidden;margin-bottom:2px;">
<div style="background:#F1F5F9;border-bottom:1px solid #CBD5E1;padding:8px 16px;">'''Translation resources'''</div>
<div style="padding:16px;">
<!-- Edit: replace placeholder items with real pages once they exist on your wiki.
Add or remove items as your community grows. -->
* Translate the most-used interface messages for your wiki on [https://translatewiki.net/w/i.php?title=Special:Translate&group=core-mostused&language=<!--LANGUAGE_CODE--> Translatewiki.net] - The essential messages that power your wiki's interface.
* [https://en.wikipedia.org/wiki/Wikipedia:Five_pillars Wikipedia's Five Pillars] - The core principles of Wikipedia. Translate these into your language as a starting point for your wiki's policies.
* [https://meta.wikimedia.org/wiki/List_of_articles_every_Wikipedia_should_have 1,000 articles every Wikipedia should have] - A list of essential topics and a great guide for deciding what to write first.
* Add a link to a translation guide or resource relevant to your community.
</div>
</div>
<noinclude>[[Category:Starter kit templates]]</noinclude>
9otl2wtitymgbojg52quzf5se6sxb60
Wikipedia:Report wizard
4
176764
749687
2026-07-03T18:46:41Z
Chaotic Enby
58843
Created page with "{{Wikipedia:Report wizard/Content|title=Welcome|content= {{font|size=125%|Thank you for your interest in contributing to Wikipedia!}}<br />This wizard will guide you towards making a report at [[Wikipedia:Administrator's noticeboard/Incidents|]].{{pb}} Have you already discussed the issue with other editors?<br />|other_buttons= <div style{{=}}"text-align:center;"> {{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Discussed?withJS{{=}}MediaWi..."
749687
wikitext
text/x-wiki
{{Wikipedia:Report wizard/Content|title=Welcome|content=
{{font|size=125%|Thank you for your interest in contributing to Wikipedia!}}<br />This wizard will guide you towards making a report at [[Wikipedia:Administrator's noticeboard/Incidents|Administrator's noticeboard/Incidents]].{{pb}}
Have you already discussed the issue with other editors?<br />|other_buttons=
<div style{{=}}"text-align:center;">
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Discussed?withJS{{=}}MediaWiki:ANI-wizard.js{{#ifeq:{{PAGENAME}}|Unblock wizard/Demo|&demoMode=true|}}|Yes, I have|class=mw-ui-progressive}}
</div>
<div style{{=}}"text-align:center;">
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Not_discussed|No, I haven't yet}}
</div>
<noinclude><small>If you want to test the wizard without submitting its output on your talk page, '''[[Wikipedia:Report wizard/Demo|click here]]'''.</small></noinclude>
}}
jqhrdpt2t8w1tccnsr52r1dy786fc8u
749689
749687
2026-07-03T18:46:50Z
Chaotic Enby
58843
Chaotic Enby moved page [[Wikipedia:ANI wizard]] to [[Wikipedia:Report wizard]]
749687
wikitext
text/x-wiki
{{Wikipedia:Report wizard/Content|title=Welcome|content=
{{font|size=125%|Thank you for your interest in contributing to Wikipedia!}}<br />This wizard will guide you towards making a report at [[Wikipedia:Administrator's noticeboard/Incidents|Administrator's noticeboard/Incidents]].{{pb}}
Have you already discussed the issue with other editors?<br />|other_buttons=
<div style{{=}}"text-align:center;">
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Discussed?withJS{{=}}MediaWiki:ANI-wizard.js{{#ifeq:{{PAGENAME}}|Unblock wizard/Demo|&demoMode=true|}}|Yes, I have|class=mw-ui-progressive}}
</div>
<div style{{=}}"text-align:center;">
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Not_discussed|No, I haven't yet}}
</div>
<noinclude><small>If you want to test the wizard without submitting its output on your talk page, '''[[Wikipedia:Report wizard/Demo|click here]]'''.</small></noinclude>
}}
jqhrdpt2t8w1tccnsr52r1dy786fc8u
749696
749689
2026-07-03T18:48:58Z
Chaotic Enby
58843
749696
wikitext
text/x-wiki
{{Wikipedia:Report wizard/Content|title=Welcome|content=
{{font|size=125%|Thank you for your interest in contributing to Wikipedia!}}<br />This wizard will guide you towards making a report at [[Wikipedia:Administrator's noticeboard/Incidents|Administrator's noticeboard/Incidents]].{{pb}}
This noticeboard is usually a place of '''last resort''' for incidents that cannot be resolved by other means, and require administrative intervention. This may be because of their urgency, or of their chronic nature.{{pb}}
Have you already discussed the issue with other editors?<br />|other_buttons=
<div style{{=}}"text-align:center;">
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Discussed?withJS{{=}}MediaWiki:ANI-wizard.js{{#ifeq:{{PAGENAME}}|Unblock wizard/Demo|&demoMode=true|}}|Yes, I have|class=mw-ui-progressive}}
</div>
<div style{{=}}"text-align:center;">
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Not_discussed|No, I haven't yet}}
</div>
<noinclude><small>If you want to test the wizard without submitting its output on your talk page, '''[[Wikipedia:Report wizard/Demo|click here]]'''.</small></noinclude>
}}
b2fcniee0gaysjjef885tpxi8ag9zw9
749699
749696
2026-07-03T18:53:17Z
Chaotic Enby
58843
749699
wikitext
text/x-wiki
{{Wikipedia:Report wizard/Content|title=Welcome|content=
{{font|size=125%|Thank you for your interest in contributing to Wikipedia!}}<br />This wizard will guide you towards making a report at [[Wikipedia:Administrator's noticeboard/Incidents|Administrator's noticeboard/Incidents]].{{pb}}
This noticeboard is usually a place of '''last resort''' for incidents that cannot be resolved by other means, and require administrative intervention. This may be because of their urgency, or of their chronic nature.{{pb}}
Have you already discussed the issue with other editors?<br />|other_buttons=
<div style{{=}}"text-align:center;">
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Discussed?withJS{{=}}MediaWiki:Report-wizard.js{{#ifeq:{{PAGENAME}}|Report wizard/Demo|&demoMode=true|}}|Yes, I have|class=mw-ui-progressive}}
</div>
<div style{{=}}"text-align:center;">
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Not_discussed|No, I haven't yet}}
</div>
<noinclude><small>If you want to test the wizard without submitting its output on your talk page, '''[[Wikipedia:Report wizard/Demo|click here]]'''.</small></noinclude>
}}
eo2lgamcsykxqug96fu2f84n8w4olxa
749706
749699
2026-07-03T18:57:02Z
Chaotic Enby
58843
749706
wikitext
text/x-wiki
{{Wikipedia:Report wizard/Content|title=Welcome|content=
{{font|size=125%|Thank you for your interest in contributing to Wikipedia!}}<br />This wizard will guide you towards making a report at [[Wikipedia:Administrator's noticeboard/Incidents|Administrator's noticeboard/Incidents]].{{pb}}
This noticeboard is usually a place of '''last resort''' for incidents that cannot be resolved by other means, and require administrative intervention. This may be because of their urgency, or of their chronic nature.{{pb}}
Have you already tried to discuss the issue with other editors?<br />|other_buttons=
<div style{{=}}"text-align:center;">
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Discussed?withJS{{=}}MediaWiki:Report-wizard.js{{#ifeq:{{PAGENAME}}|Report wizard/Demo|&demoMode=true|}}|Yes, I have|class=mw-ui-progressive}}
</div>
<div style{{=}}"text-align:center;">
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Not_discussed|No, I haven't yet}}
</div>
<noinclude><small>If you want to test the wizard without submitting its output on your talk page, '''[[Wikipedia:Report wizard/Demo|click here]]'''.</small></noinclude>
}}
md3t1aeup2hjenou9qriqqe3lvwhgcb
749707
749706
2026-07-03T18:57:11Z
Chaotic Enby
58843
749707
wikitext
text/x-wiki
{{Wikipedia:Report wizard/Content|title=Welcome|content=
{{font|size=125%|Thank you for your interest in contributing to Wikipedia!}}<br />This wizard will guide you towards making a report at [[Wikipedia:Administrator's noticeboard/Incidents|Administrator's noticeboard/Incidents]].{{pb}}
This noticeboard is usually a place of '''last resort''' for incidents that cannot be resolved by other means, and require administrative intervention. This may be because of their urgency, or of their chronic nature.{{pb}}
Have you already tried to discuss the issue with the other editors?<br />|other_buttons=
<div style{{=}}"text-align:center;">
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Discussed?withJS{{=}}MediaWiki:Report-wizard.js{{#ifeq:{{PAGENAME}}|Report wizard/Demo|&demoMode=true|}}|Yes, I have|class=mw-ui-progressive}}
</div>
<div style{{=}}"text-align:center;">
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Not_discussed|No, I haven't yet}}
</div>
<noinclude><small>If you want to test the wizard without submitting its output on your talk page, '''[[Wikipedia:Report wizard/Demo|click here]]'''.</small></noinclude>
}}
9g0g957hull09fuix0wner9r3gxt218
749722
749707
2026-07-03T19:08:22Z
Chaotic Enby
58843
749722
wikitext
text/x-wiki
{{Wikipedia:Report wizard/Content|title=Welcome|content=
{{font|size=125%|Thank you for your interest in contributing to Wikipedia!}}<br />This wizard will guide you towards making a report at [[Wikipedia:Administrator's noticeboard/Incidents|Administrator's noticeboard/Incidents]].{{pb}}
This noticeboard is usually a place of '''last resort''' for incidents that cannot be resolved by other means, and require administrative intervention. This may be because of their urgency, or of their chronic nature.{{pb}}
Have you already tried to discuss the issue with the other editors?<br />|other_buttons=
<div style{{=}}"text-align:center; display:flex; flex-direction: row; gap: 1em; justify-content: center;">
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Discussed?withJS{{=}}MediaWiki:Report-wizard.js{{#ifeq:{{PAGENAME}}|Report wizard/Demo|&demoMode=true|}}|Yes, I have|class=mw-ui-progressive}}
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Not_discussed|No, I haven't yet}}
</div>
<noinclude><small>If you want to test the wizard without submitting its output on your talk page, '''[[Wikipedia:Report wizard/Demo|click here]]'''.</small></noinclude>
}}
fax5apqo5dvgdv8m8ro5baom92ygddk
749727
749722
2026-07-03T19:10:30Z
Chaotic Enby
58843
749727
wikitext
text/x-wiki
{{Wikipedia:Report wizard/Content|title=Welcome|content=
{{font|size=125%|Thank you for your interest in contributing to Wikipedia!}}<br />This wizard will guide you towards making a report at [[Wikipedia:Administrator's noticeboard/Incidents|Administrator's noticeboard/Incidents]].{{pb}}
This noticeboard is usually a place of '''last resort''' for incidents that cannot be resolved by other means, and require administrative intervention. This may be because of their urgency, or of their chronic nature.{{pb}}
Have you already tried to discuss the issue with the other editors?<br />|other_buttons=
<div style{{=}}"text-align:center; display:flex; flex-direction: row; gap: 1em; justify-content: center;">
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Users?withJS{{=}}MediaWiki:Report-wizard.js{{#ifeq:{{PAGENAME}}|Report wizard/Demo|&demoMode=true|}}|Yes, I have|class=mw-ui-progressive}}
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Not_discussed|No, I haven't yet}}
</div>
<noinclude><small>If you want to test the wizard without submitting its output on your talk page, '''[[Wikipedia:Report wizard/Demo|click here]]'''.</small></noinclude>
}}
7tkeb69ossjn34asz8uftradvkiii80
749766
749727
2026-07-03T19:29:10Z
Chaotic Enby
58843
749766
wikitext
text/x-wiki
{{Wikipedia:Report wizard/Content|title=Welcome|content=
{{font|size=125%|Thank you for your interest in contributing to Wikipedia!}}<br />This wizard will guide you towards making a report at [[Wikipedia:Administrator's noticeboard/Incidents|Administrator's noticeboard/Incidents]].{{pb}}
This noticeboard is usually a place of '''last resort''' for incidents that cannot be resolved by other means, and require administrative intervention. This may be because of their urgency, or of their chronic nature.{{pb}}
Have you already tried to discuss the issue with the other editors?<br />|other_buttons=
<div style{{=}}"text-align:center; display:flex; flex-direction: row; gap: 1em; justify-content: center;">
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Users?withJS{{=}}MediaWiki:Report-wizard.js{{#ifeq:{{PAGENAME}}|Report wizard/Demo|&demoMode=true|}}|Yes, I have|class=mw-ui-progressive}}
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Not_discussed|No, I haven't yet}}
</div>
<noinclude><small>If you want to test the wizard without submitting its output, '''[[Wikipedia:Report wizard/Demo|click here]]'''.</small></noinclude>
}}
hrwllxru4entg0hh5ihoki8vo29q4vs
Wikipedia:ANI wizard
4
176765
749690
2026-07-03T18:46:50Z
Chaotic Enby
58843
Chaotic Enby moved page [[Wikipedia:ANI wizard]] to [[Wikipedia:Report wizard]]
749690
wikitext
text/x-wiki
#REDIRECT [[Wikipedia:Report wizard]]
{{Redirect category shell|
{{R from move}}
}}
ms5nxdfvvpnrqb45uhm6igbyc8i033y
Wikipedia:ANI wizard/Content
4
176766
749692
2026-07-03T18:46:50Z
Chaotic Enby
58843
Chaotic Enby moved page [[Wikipedia:ANI wizard/Content]] to [[Wikipedia:Report wizard/Content]]
749692
wikitext
text/x-wiki
#REDIRECT [[Wikipedia:Report wizard/Content]]
{{Redirect category shell|
{{R from move}}
}}
bh7cqy9zyw96mwga87shqzf58iy22kg
Wikipedia:ANI wizard/Header
4
176767
749694
2026-07-03T18:46:50Z
Chaotic Enby
58843
Chaotic Enby moved page [[Wikipedia:ANI wizard/Header]] to [[Wikipedia:Report wizard/Header]]
749694
wikitext
text/x-wiki
#REDIRECT [[Wikipedia:Report wizard/Header]]
{{Redirect category shell|
{{R from move}}
}}
3rynnsn7ceqckpw2ccj9yaqrtamgnuh
Wikipedia:Report wizard/Not discussed
4
176768
749697
2026-07-03T18:52:45Z
Chaotic Enby
58843
Created page with "{{Wikipedia:Report wizard/Content|title=Welcome|content= Some incidents may be urgent enough to be reported without going through the intermediate step of discussing with other editors.{{pb}} Which of these best fits your situation?<br />|other_buttons= <div style{{=}}"text-align:center;"> {{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/AIV|Unblock wizard/Demo|Unambiguous vandalism|class=mw-ui-progressive}} </div> <div style{{=}}"text-align..."
749697
wikitext
text/x-wiki
{{Wikipedia:Report wizard/Content|title=Welcome|content=
Some incidents may be urgent enough to be reported without going through the intermediate step of discussing with other editors.{{pb}}
Which of these best fits your situation?<br />|other_buttons=
<div style{{=}}"text-align:center;">
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/AIV|Unblock wizard/Demo|Unambiguous vandalism|class=mw-ui-progressive}}
</div>
<div style{{=}}"text-align:center;">
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Urgent?withJS{{=}}MediaWiki:Report-wizard.js|Another kind of urgent incident|class=mw-ui-progressive}}
</div>
<div style{{=}}"text-align:center;">
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Discuss|Neither}}
</div>
<noinclude><small>If you want to test the wizard without submitting its output on your talk page, '''[[Wikipedia:Report wizard/Demo|click here]]'''.</small></noinclude>
}}
jp9maohubnt0kd9jwi55o0akz1gbahw
749698
749697
2026-07-03T18:52:54Z
Chaotic Enby
58843
749698
wikitext
text/x-wiki
{{Wikipedia:Report wizard/Content|title=Welcome|content=
Some incidents may be urgent enough to be reported without going through the intermediate step of discussing with other editors.{{pb}}
Which of these best fits your situation?<br />|other_buttons=
<div style{{=}}"text-align:center;">
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/AIV|Unambiguous vandalism|class=mw-ui-progressive}}
</div>
<div style{{=}}"text-align:center;">
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Urgent?withJS{{=}}MediaWiki:Report-wizard.js|Another kind of urgent incident|class=mw-ui-progressive}}
</div>
<div style{{=}}"text-align:center;">
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Discuss|Neither}}
</div>
<noinclude><small>If you want to test the wizard without submitting its output on your talk page, '''[[Wikipedia:Report wizard/Demo|click here]]'''.</small></noinclude>
}}
fkhkwo0hnowhcjdfax3blkrg404t6c3
749702
749698
2026-07-03T18:56:29Z
Chaotic Enby
58843
749702
wikitext
text/x-wiki
{{Wikipedia:Report wizard/Content|title=Welcome|content=
Some incidents may be '''urgent enough''' to be reported before you have time to discuss with other editors.{{pb}}
Unambiguous vandalism, meaning actions clearly intended to directly harm the encyclopedia, can be reported at '''[[Wikipedia:Administrator intervention against vandalism|another noticeboard]]''' for faster treatment.{{pb}}
Which of these best fits your situation?<br />|other_buttons=
<div style{{=}}"text-align:center;">
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/AIV|I want to report vandalism|class=mw-ui-progressive}}
</div>
<div style{{=}}"text-align:center;">
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Urgent?withJS{{=}}MediaWiki:Report-wizard.js|I want to report another urgent incident|class=mw-ui-progressive}}
</div>
<div style{{=}}"text-align:center;">
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Discuss|Neither}}
</div>
<noinclude><small>If you want to test the wizard without submitting its output on your talk page, '''[[Wikipedia:Report wizard/Demo|click here]]'''.</small></noinclude>
}}
08cdw1d8ilwfdjubnwroesnp0mqeitt
749708
749702
2026-07-03T18:57:47Z
Chaotic Enby
58843
749708
wikitext
text/x-wiki
{{Wikipedia:Report wizard/Content|title=I haven't discussed the issue yet|content=
Some incidents may be '''urgent enough''' to be reported before you have time to discuss with other editors.{{pb}}
Unambiguous vandalism, meaning actions clearly intended to directly harm the encyclopedia, can be reported at '''[[Wikipedia:Administrator intervention against vandalism|another noticeboard]]''' for faster treatment.{{pb}}
Which of these best fits your situation?<br />|other_buttons=
<div style{{=}}"text-align:center;">
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/AIV|I want to report vandalism|class=mw-ui-progressive}}
</div>
<div style{{=}}"text-align:center;">
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Urgent?withJS{{=}}MediaWiki:Report-wizard.js|I want to report another urgent incident|class=mw-ui-progressive}}
</div>
<div style{{=}}"text-align:center;">
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Discuss|Neither}}
</div>
<noinclude><small>If you want to test the wizard without submitting its output on your talk page, '''[[Wikipedia:Report wizard/Demo|click here]]'''.</small></noinclude>
}}
qjgfque2ymmp5pvihiurym94xisjep7
749712
749708
2026-07-03T19:02:44Z
Chaotic Enby
58843
749712
wikitext
text/x-wiki
{{Wikipedia:Report wizard/Content|title=I haven't discussed the issue yet|content=
Some incidents may be '''urgent enough''' to be reported before you have time to discuss with other editors.{{pb}}
Unambiguous vandalism, meaning actions clearly intended to directly harm the encyclopedia, can be reported at '''[[Wikipedia:Administrator intervention against vandalism|another noticeboard]]''' for faster treatment.{{pb}}
Which of these best fits your situation?<br />|other_buttons=
<div style{{=}}"text-align:center;">
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/AIV|I want to report vandalism|class=mw-ui-progressive}}
</div>
<div style{{=}}"text-align:center;">
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Users?withJS{{=}}MediaWiki:Report-wizard.js|I want to report another urgent incident|class=mw-ui-progressive}}
</div>
<div style{{=}}"text-align:center;">
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Discuss|Neither}}
</div>
<noinclude><small>If you want to test the wizard without submitting its output on your talk page, '''[[Wikipedia:Report wizard/Demo|click here]]'''.</small></noinclude>
}}
n0medfl91l8b1y5p31f34jasaadl0oi
749726
749712
2026-07-03T19:10:06Z
Chaotic Enby
58843
749726
wikitext
text/x-wiki
{{Wikipedia:Report wizard/Content|title=I haven't discussed the issue yet|content=
Some incidents may be '''urgent enough''' to be reported before you have time to discuss with other editors.{{pb}}
Unambiguous vandalism, meaning actions clearly intended to directly harm the encyclopedia, can be reported at '''[[Wikipedia:Administrator intervention against vandalism|another noticeboard]]''' for faster treatment.{{pb}}
Which of these best fits your situation?<br />|other_buttons=
<div style{{=}}"text-align:center; display:flex; flex-direction: row; gap: 1em; justify-content: center;">
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/AIV|Vandalism|class=mw-ui-progressive}}
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Users?withJS{{=}}MediaWiki:Report-wizard.js|Another urgent incident|class=mw-ui-progressive}}
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Discuss|Neither}}
</div>
<noinclude><small>If you want to test the wizard without submitting its output on your talk page, '''[[Wikipedia:Report wizard/Demo|click here]]'''.</small></noinclude>
}}
6kgp330k9luzp3lgt1him0oc33z987u
749746
749726
2026-07-03T19:22:09Z
Chaotic Enby
58843
749746
wikitext
text/x-wiki
{{Wikipedia:Report wizard/Content|title=I haven't discussed the issue yet|content=
Some incidents may be '''urgent enough''' to be reported before you have time to discuss with other editors.{{pb}}
Unambiguous vandalism, meaning actions clearly intended to directly harm the encyclopedia, can be reported at '''[[Wikipedia:Administrator intervention against vandalism|another noticeboard]]''' for faster treatment.{{pb}}
Which of these best fits your situation?<br />|other_buttons=
<div style{{=}}"text-align:center; display:flex; flex-direction: row; gap: 1em; justify-content: center;">
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/AIV|This is clearly vandalism!|class=mw-ui-progressive}}
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Users?withJS{{=}}MediaWiki:Report-wizard.js|It is urgent, but not vandalism|class=mw-ui-progressive}}
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Discuss|Neither}}
</div>
<noinclude><small>If you want to test the wizard without submitting its output on your talk page, '''[[Wikipedia:Report wizard/Demo|click here]]'''.</small></noinclude>
}}
f2ln2p4xh4pzn7jiesl40pjcwfhfnd7
749769
749746
2026-07-03T19:30:05Z
Chaotic Enby
58843
749769
wikitext
text/x-wiki
{{Wikipedia:Report wizard/Content|title=I haven't discussed the issue yet|content=
Some incidents may be '''urgent enough''' to be reported before you have time to discuss with other editors.{{pb}}
Unambiguous vandalism, meaning actions clearly intended to directly harm the encyclopedia, can be reported at '''[[Wikipedia:Administrator intervention against vandalism|another noticeboard]]''' for faster treatment.{{pb}}
Which of these best fits your situation?<br />|other_buttons=
<div style{{=}}"text-align:center; display:flex; flex-direction: row; gap: 1em; justify-content: center;">
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/AIV|This is clearly vandalism!|class=mw-ui-progressive}}
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Users?withJS{{=}}MediaWiki:Report-wizard.js|It is urgent, but not vandalism|class=mw-ui-progressive}}
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Discuss|Neither}}
</div>
<noinclude><small>If you want to test the wizard without submitting its output, '''[[Wikipedia:Report wizard/Demo|click here]]'''.</small></noinclude>
}}
6ldrzzrkqblkgmyamo1kfn3rlakjbf2
Википедия:К переименованию/3 июля 2026
0
176769
749704
2026-07-03T18:56:34Z
Solidest
54422
[[Википедия:Гаджеты/Remover|Remover]]: автоматическая шапка
749704
wikitext
text/x-wiki
{{КПМ-Навигация}}
5lhop28vwygvlrh0r5t1cfhn402d6y9
749705
749704
2026-07-03T18:56:35Z
Solidest
54422
/* Fiscal Responsibility Act of 2023 → Test */ номинация ([[Википедия:Гаджеты/Remover|Remover]])
749705
wikitext
text/x-wiki
{{КПМ-Навигация}}
== [[:Fiscal Responsibility Act of 2023]] → [[:Test]] ==
Test [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 18:56, 3 July 2026 (UTC)
r8xnmmk74a65rcnigpr5w9m7og4z9cg
Category:Snoop Lion (rapper) albums
14
176770
749713
2026-07-03T19:03:01Z
~2026-38011-52
74711
Created page with "{{albums category}} {{DEFAULTSORT:Lion (rapper), Snoop}} [[Category:Ragga albums]] [[Category:Dancehall albums] [[Category:Reggae albums]]"
749713
wikitext
text/x-wiki
{{albums category}}
{{DEFAULTSORT:Lion (rapper), Snoop}}
[[Category:Ragga albums]]
[[Category:Dancehall albums]
[[Category:Reggae albums]]
c6ujqznow23wzis68p84iypmyzs56t9
749714
749713
2026-07-03T19:03:21Z
~2026-38011-52
74711
749714
wikitext
text/x-wiki
{{albums category}}
{{DEFAULTSORT:Lion (rapper), Snoop}}
[[Category:Ragga albums]]
[[Category:Dancehall albums]]
[[Category:Reggae albums]]
rattvcxl9ok6xswwi9q0plstm78bie2
Template:Albums category
10
176771
749715
2026-07-03T19:03:59Z
~2026-38011-52
74711
Created page with "{{set category}} {{Albums category/core|{{{1|{{#invoke:string|replace|{{PAGENAME}}|.albums.*|plain=0}}}}}|{{{2|}}}|empty={{{empty|}}}|display={{{display|}}}|the={{{the|}}}|discography={{{discography|}}}|discography band name={{{discography band name|}}}}} <includeonly>{{#ifexist:Template:Albums category/type/{{{type|default}}}|[[Category:{{Albums category/type/{{{type|default}}}}}]]|**[[Template:Albums category/type/{{{type|default}}}]] does not exist. Check the value of..."
749715
wikitext
text/x-wiki
{{set category}}
{{Albums category/core|{{{1|{{#invoke:string|replace|{{PAGENAME}}|.albums.*|plain=0}}}}}|{{{2|}}}|empty={{{empty|}}}|display={{{display|}}}|the={{{the|}}}|discography={{{discography|}}}|discography band name={{{discography band name|}}}}}
<includeonly>{{#ifexist:Template:Albums category/type/{{{type|default}}}|[[Category:{{Albums category/type/{{{type|default}}}}}]]|**[[Template:Albums category/type/{{{type|default}}}]] does not exist. Check the value of "type" in the template "Albums category". Valid items are listed at [[Template:Albums category/type]] . [[Category:Albums category uses with invalid parameters|T{{PAGENAME}}]]}}</includeonly><noinclude>
{{Documentation}}
</noinclude>
gec5c71vz3mpcx3exccbuw98ivam257
Template:Set category
10
176772
749717
2026-07-03T19:04:43Z
~2026-38011-52
74711
Created page with "{{Cmbox | type = notice | text = This is a '''[[Wikipedia:Categorization#Set category|set category]]'''. It should only contain pages that are '''{{#if:{{{first|}}}|{{{first}}}|{{#if:{{{alternative|}}}|{{{alternative}}}|{{PAGENAME}}}}}}''' or '''lists of {{#if:{{{alternative|}}}|{{{alternative}}}|{{PAGENAME}}}}''', as well as '''[[Wikipedia:Categorization#Subcategorization|subcategories]]''' containing those things (themselves set categories). Topics about {{#if:{{{alte..."
749717
wikitext
text/x-wiki
{{Cmbox
| type = notice
| text = This is a '''[[Wikipedia:Categorization#Set category|set category]]'''. It should only contain pages that are '''{{#if:{{{first|}}}|{{{first}}}|{{#if:{{{alternative|}}}|{{{alternative}}}|{{PAGENAME}}}}}}''' or '''lists of {{#if:{{{alternative|}}}|{{{alternative}}}|{{PAGENAME}}}}''', as well as '''[[Wikipedia:Categorization#Subcategorization|subcategories]]''' containing those things (themselves set categories). Topics about {{#if:{{{alternative|}}}|{{{alternative}}}|{{PAGENAME}}}} in general should be placed in {{#if:{{{topic|}}}|[[:Category:{{{topic}}}]] or one of its subcategories|relevant [[Wikipedia:Categorization#Topic category|topic categories]]}}. {{#if:{{{single|}}}|Categories related to only an individual {{{single}}} should not be in this category.}}
| nocat = {{{nocat|}}}
}}{{Single namespace|category|[[Category:Set categories]]|nocat={{{nocat|}}}}}<noinclude>
{{Documentation}}
</noinclude>
f6cw0f0d5b2lxxd8b29mledqvfsmgx4
Template:Incorrect namespace
10
176773
749718
2026-07-03T19:05:19Z
~2026-38011-52
74711
Created page with "<includeonly>{{#ifeq:{{NAMESPACE}}|Template||{{#ifeq:{{SUBPAGENAME}}|sandbox||{{#ifeq:{{SUBPAGENAME}}|testcases||{{#ifeq:{{FULLPAGENAME}}|Wikipedia:Sandbox||{{#ifeq:{{FULLPAGENAME}}|Wikipedia:Template index/Redirect pages|| </includeonly>{{Mbox | type = content | image = [[File:Ambox warning orange.svg|50px|link=|alt=]] | text = {{Error|This template should be transcluded in the ''{{{1}}}'' namespace(s) only.}} }}<includeonly>{{Category handler |all=Category:Pages with..."
749718
wikitext
text/x-wiki
<includeonly>{{#ifeq:{{NAMESPACE}}|Template||{{#ifeq:{{SUBPAGENAME}}|sandbox||{{#ifeq:{{SUBPAGENAME}}|testcases||{{#ifeq:{{FULLPAGENAME}}|Wikipedia:Sandbox||{{#ifeq:{{FULLPAGENAME}}|Wikipedia:Template index/Redirect pages|| </includeonly>{{Mbox
| type = content
| image = [[File:Ambox warning orange.svg|50px|link=|alt=]]
| text = {{Error|This template should be transcluded in the ''{{{1}}}'' namespace(s) only.}}
}}<includeonly>{{Category handler
|all=[[Category:Pages with templates in the wrong namespace]]
|nocat={{{nocat|}}}
|category2={{{category|¬}}}
}}}}}}}}}}}}</includeonly><noinclude>
{{Documentation}}
</noinclude>
jdiwkqgjhbkm8xuun6y9lb1w90op66x
Template:Albums category/core
10
176774
749719
2026-07-03T19:06:05Z
~2026-38011-52
74711
Created page with "{{#ifeq:{{NAMESPACENUMBER}}|14|{{#ifeq:{{PAGESINCATEGORY:{{#titleparts:{{PAGENAME}}}}}}|0|''(This category appears to be empty - [https://en.wikipedia.org/w/index.php?title=Special%3ASearch&search={{urlencode:{{{1|}}}}}&go=Go find candidates])''<br />[[Category:Empty albums categories]]}}}} {{hatnote|This category contains albums by {{#if:{{{the|}}}|the}} '''[[{{{1|}}}{{#if:{{{display|}}}|{{!}}{{{display}}}}}]]'''.}}{{#if:{{{2|}}}|({{{2}}})}} {{Category see also if exist..."
749719
wikitext
text/x-wiki
{{#ifeq:{{NAMESPACENUMBER}}|14|{{#ifeq:{{PAGESINCATEGORY:{{#titleparts:{{PAGENAME}}}}}}|0|''(This category appears to be empty - [https://en.wikipedia.org/w/index.php?title=Special%3ASearch&search={{urlencode:{{{1|}}}}}&go=Go find candidates])''<br />[[Category:Empty albums categories]]}}}}
{{hatnote|This category contains albums by {{#if:{{{the|}}}|the}} '''[[{{{1|}}}{{#if:{{{display|}}}|{{!}}{{{display}}}}}]]'''.}}{{#if:{{{2|}}}|({{{2}}})}}
{{Category see also if exists|{{{1|}}} songs|{{{1|}}} album covers|{{{1|}}} members}}
{{#if:{{{discography|}}}|
==See also==
* {{#if:{{{the|}}}|The}} '''[[{{#if:{{{discography band name|}}}|{{{discography band name}}}|{{#invoke:ustring|gsub|s1={{{1|}}}|%(band%)$|}}}} discography{{#if:{{{display|}}}|{{!}}{{{display}}} discography}}]]'''
}}
{{#ifexist:Category:{{{1|}}} (band)|[[Category:{{{1|}}} (band)]]|
{{#ifexist:Category:{{{1|}}} (musician)|[[Category:{{{1|}}} (musician)]]|
{{#ifexist:Category:{{{1|}}} (singer)|[[Category:{{{1|}}} (singer)]]|
{{#ifexist:Category:{{{1|}}} (rapper)|[[Category:{{{1|}}} (rapper)]]|
{{#ifexist:Category:{{{1|}}} (American band)|[[Category:{{{1|}}} (American band)]]|
{{#ifexist:Category:{{{1|}}} (entertainer)|[[Category:{{{1|}}} (entertainer)]]|
{{#ifexist:Category:{{{1|}}} (group))|[[Category:{{{1|}}} (group)]]|
{{#ifexist:Category:{{{1|}}} (British band))|[[Category:{{{1|}}} (British band)]]|
{{#ifexist:Category:{{{1|}}} (Japanese band))|[[Category:{{{1|}}} (Japanese band)]]|
{{#ifexist:Category:Works by {{{1|}}}|[[Category:Works by {{{1|}}}|Albums]]|
{{#ifexist:Category:Recordings of music by {{{1|}}}|[[Category:Recordings of music by {{{1|}}}]]|
{{#ifexist:Category:{{{1|}}}|[[Category:{{{1|}}}|Albums]]}}}}}}}}}}}}}}}}}}}}}}}}
<noinclude>
{{documentation}}
</noinclude>
93zgxxtqxj0m7qq9abl13wxsdrsc1gv
Template:Category see also if exists
10
176775
749720
2026-07-03T19:06:42Z
~2026-38011-52
74711
Created page with "<includeonly>{{safesubst:#invoke:See also if exists|main|ns=category|warning=yes}}</includeonly><noinclude>{{Documentation}}</noinclude>"
749720
wikitext
text/x-wiki
<includeonly>{{safesubst:#invoke:See also if exists|main|ns=category|warning=yes}}</includeonly><noinclude>{{Documentation}}</noinclude>
mdgz6zxw3bn871h2atgy85gkxmqzzcr
Module:See also if exists
828
176776
749721
2026-07-03T19:07:53Z
~2026-38011-52
74711
Created page with "--[[ v1.0 ]] local p = {} local yesno = require("Module:yesno") local function exists(title) local success, result = pcall(function() return title.exists end) if success then return result else return true end end function p.main(frame) local rawpages = {} local nvalid = 0 local namespace = frame.args.ns for i, v in ipairs(frame:getParent().args) do if (v ~= nil) then local thisArg = mw.text.trim(v) if (thisArg ~= "") then local title = mw.title.n..."
749721
Scribunto
text/plain
--[[ v1.0
]]
local p = {}
local yesno = require("Module:yesno")
local function exists(title)
local success, result = pcall(function() return title.exists end)
if success then
return result
else
return true
end
end
function p.main(frame)
local rawpages = {}
local nvalid = 0
local namespace = frame.args.ns
for i, v in ipairs(frame:getParent().args) do
if (v ~= nil) then
local thisArg = mw.text.trim(v)
if (thisArg ~= "") then
local title = mw.title.new(thisArg, namespace)
if title ~= nil and exists(title) then
table.insert(rawpages, title.fullText)
nvalid = nvalid + 1
end
end
end
end
if (nvalid == 0) then
if yesno(frame.args.warning) then
if namespace == nil then
namespace = "page"
elseif namespace:sub(-1) == "y" then
namespace = namespace:sub(0, -2) .. "ie"
end
mw.addWarning(string.format("'''[[%s]] — no output, because none of the %ss currently exist.'''",
frame:getParent():getTitle(),namespace))
end
return ""
end
local mLabelledList = require('Module:Labelled list hatnote')
local pages = mLabelledList._labelledList(rawpages, "See also", "")
return pages
end
return p
rar7n78dxwa0kw710nfyuzqo7apib9j
Wikipedia:Report wizard/Users
4
176777
749723
2026-07-03T19:08:34Z
Chaotic Enby
58843
Created page with "{{Wikipedia:Report wizard/Content|title=Involved users|content= Which users have been involved in the dispute?<br />|other_buttons= <div style{{=}}"text-align:center; display:flex; flex-direction: row; gap: 1em; justify-content: center;"> {{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Case?withJS{{=}}MediaWiki:Report-wizard.js{{#ifeq:{{PAGENAME}}|Report wizard/Demo|&demoMode=true|}}|Next|class=mw-ui-progressive}} {{Clickable button|url=ht..."
749723
wikitext
text/x-wiki
{{Wikipedia:Report wizard/Content|title=Involved users|content=
Which users have been involved in the dispute?<br />|other_buttons=
<div style{{=}}"text-align:center; display:flex; flex-direction: row; gap: 1em; justify-content: center;">
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Case?withJS{{=}}MediaWiki:Report-wizard.js{{#ifeq:{{PAGENAME}}|Report wizard/Demo|&demoMode=true|}}|Next|class=mw-ui-progressive}}
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Not_discussed|I'm not sure}}
</div>
<noinclude><small>If you want to test the wizard without submitting its output on your talk page, '''[[Wikipedia:Report wizard/Demo|click here]]'''.</small></noinclude>
}}
666hmu5hnws9r8tx22zkcfl7x31vz4r
749733
749723
2026-07-03T19:13:26Z
Chaotic Enby
58843
749733
wikitext
text/x-wiki
{{Wikipedia:Report wizard/Content|title=Involved users|content=
Which users have been involved in the dispute?<br />|other_buttons=
<div style{{=}}"text-align:center; display:flex; flex-direction: row; gap: 1em; justify-content: center;">
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Case?withJS{{=}}MediaWiki:Report-wizard.js|Next|class=mw-ui-progressive}}
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Case?withJS{{=}}MediaWiki:Report-wizard.js|I'm not sure}}
</div>
<noinclude><small>If you want to test the wizard without submitting its output on your talk page, '''[[Wikipedia:Report wizard/Demo|click here]]'''.</small></noinclude>
}}
7na7hpcqpn0e3idbwjtni8pqbxdcvox
749743
749733
2026-07-03T19:21:44Z
Chaotic Enby
58843
749743
wikitext
text/x-wiki
{{Wikipedia:Report wizard/Content|title=Involved users|content=
Which users have been involved in the dispute?<br />|other_buttons=
<div style{{=}}"text-align:center; display:flex; flex-direction: row; gap: 1em; justify-content: center;">
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Case?withJS{{=}}MediaWiki:Report-wizard.js|These are all of them!|class=mw-ui-progressive}}
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Case?withJS{{=}}MediaWiki:Report-wizard.js|I'm not sure...}}
</div>
<noinclude><small>If you want to test the wizard without submitting its output on your talk page, '''[[Wikipedia:Report wizard/Demo|click here]]'''.</small></noinclude>
}}
lpvtb4ptayrky98gpp1pynqei1pczul
749765
749743
2026-07-03T19:29:03Z
Chaotic Enby
58843
749765
wikitext
text/x-wiki
{{Wikipedia:Report wizard/Content|title=Involved users|content=
Which users have been involved in the dispute?<br />|other_buttons=
<div style{{=}}"text-align:center; display:flex; flex-direction: row; gap: 1em; justify-content: center;">
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Case?withJS{{=}}MediaWiki:Report-wizard.js|These are all of them!|class=mw-ui-progressive}}
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Case?withJS{{=}}MediaWiki:Report-wizard.js|I'm not sure...}}
</div>
<noinclude><small>If you want to test the wizard without submitting its output, '''[[Wikipedia:Report wizard/Demo|click here]]'''.</small></noinclude>
}}
8iwi9kqbd5dp439f6owbqnmcga94b9q
Template:Albums category/type
10
176778
749724
2026-07-03T19:09:04Z
~2026-38011-52
74711
Created page with "===List of types=== {{Special:Prefixindex/Template:Albums category/type/}}"
749724
wikitext
text/x-wiki
===List of types===
{{Special:Prefixindex/Template:Albums category/type/}}
ktq5ccts5ip155pdgz0n5qomtzgvvgu
Template:Albums category/type/default
10
176779
749725
2026-07-03T19:09:52Z
~2026-38011-52
74711
Created page with "Albums by artist"
749725
wikitext
text/x-wiki
Albums by artist
k3r5m5epa3nlugrko8277ndj9c77ku7
Category:Priarity Records albums
14
176780
749728
2026-07-03T19:11:20Z
~2026-38011-52
74711
Created page with "{{Album label category}}"
749728
wikitext
text/x-wiki
{{Album label category}}
ax0k3p2il6xym51q9j3rvez95c0da52
Template:Album label category
10
176781
749729
2026-07-03T19:11:52Z
~2026-38011-52
74711
Created page with "{{set category}} {{Album label category/core|{{#if:{{{1|}}}|{{{1}}}|{{Str left|{{PAGENAME}}|{{#expr:{{Str find|{{PAGENAME}}|albums}}-1}}}}}}|{{#if:{{{2|}}}|{{{2}}}}}|the={{{the|}}}}} <includeonly>[[Category:Albums by record label]]<!-- --></includeonly><!-- # Note {{Automatic category TOC}} needs to be available whether or not this template page has been transcluded -->{{Automatic category TOC}}<!-- --><noinclude> {{Documentation}} </noinclude>"
749729
wikitext
text/x-wiki
{{set category}}
{{Album label category/core|{{#if:{{{1|}}}|{{{1}}}|{{Str left|{{PAGENAME}}|{{#expr:{{Str find|{{PAGENAME}}|albums}}-1}}}}}}|{{#if:{{{2|}}}|{{{2}}}}}|the={{{the|}}}}}
<includeonly>[[Category:Albums by record label]]<!--
--></includeonly><!--
# Note {{Automatic category TOC}} needs to be available whether or not this template page has been transcluded
-->{{Automatic category TOC}}<!--
--><noinclude>
{{Documentation}}
</noinclude>
4anrg38ewqz3opbsjm9gvvhgx1064ah
Template:Album label category/core
10
176782
749730
2026-07-03T19:12:21Z
~2026-38011-52
74711
Created page with "{{hatnote|This category contains studio albums released on {{#ifeq:{{{the}}}|no||the}} '''[[{{{1|}}}{{#if:{{{2|}}}|{{!}}{{{2}}}}}]]''' label. Please move any non-studio albums to an appropriate subcategory per [[Wikipedia:MOSALBUM#Categorization|WikiProject Albums guidelines]].}} {{#ifexist:Category:{{{1|}}}|[[Category:{{{1|}}}|Albums]]}} <noinclude> [[Category:WikiProject Albums templates]] </noinclude>"
749730
wikitext
text/x-wiki
{{hatnote|This category contains studio albums released on {{#ifeq:{{{the}}}|no||the}} '''[[{{{1|}}}{{#if:{{{2|}}}|{{!}}{{{2}}}}}]]''' label. Please move any non-studio albums to an appropriate subcategory per [[Wikipedia:MOSALBUM#Categorization|WikiProject Albums guidelines]].}}
{{#ifexist:Category:{{{1|}}}|[[Category:{{{1|}}}|Albums]]}}
<noinclude>
[[Category:WikiProject Albums templates]]
</noinclude>
6dw5ndzmu8n2slz6bujqisajl18m2zh
Wikipedia:Report wizard/Case
4
176783
749731
2026-07-03T19:12:59Z
Chaotic Enby
58843
Created page with "{{Wikipedia:Report wizard/Content|title=Dispute|content= How would you describe the dispute from your point of view?<br />|other_buttons= <div style{{=}}"text-align:center; display:flex; flex-direction: row; gap: 1em; justify-content: center;"> {{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Case?withJS{{=}}MediaWiki:Report-wizard.js|Publish|class=mw-ui-progressive}} {{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_w..."
749731
wikitext
text/x-wiki
{{Wikipedia:Report wizard/Content|title=Dispute|content=
How would you describe the dispute from your point of view?<br />|other_buttons=
<div style{{=}}"text-align:center; display:flex; flex-direction: row; gap: 1em; justify-content: center;">
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Case?withJS{{=}}MediaWiki:Report-wizard.js|Publish|class=mw-ui-progressive}}
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Users?withJS{{=}}MediaWiki:Report-wizard.js|Go back}}
</div>
<noinclude><small>If you want to test the wizard without submitting its output on your talk page, '''[[Wikipedia:Report wizard/Demo|click here]]'''.</small></noinclude>
}}
5a70560dd11mfoput7kk60b7u5tx3x4
749768
749731
2026-07-03T19:29:36Z
Chaotic Enby
58843
749768
wikitext
text/x-wiki
{{Wikipedia:Report wizard/Content|title=Dispute|content=
How would you describe the dispute from your point of view?<br />|other_buttons=
<div style{{=}}"text-align:center; display:flex; flex-direction: row; gap: 1em; justify-content: center;">
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Case?withJS{{=}}MediaWiki:Report-wizard.js|Publish|class=mw-ui-progressive}}
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Users?withJS{{=}}MediaWiki:Report-wizard.js|Go back}}
</div>
<noinclude><small>If you want to test the wizard without submitting its output, '''[[Wikipedia:Report wizard/Demo|click here]]'''.</small></noinclude>
}}
m68v1225bfjsjnvm70aouy64fvep124
749806
749768
2026-07-03T21:31:45Z
Chaotic Enby
58843
user-first communication
749806
wikitext
text/x-wiki
{{Wikipedia:Report wizard/Content|title=Dispute|content=
How would you describe the dispute from your point of view?<br />|other_buttons=
<div style{{=}}"text-align:center; display:flex; flex-direction: row; gap: 1em; justify-content: center;">
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Case?withJS{{=}}MediaWiki:Report-wizard.js|I'm ready to post|class=mw-ui-progressive}}
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Users?withJS{{=}}MediaWiki:Report-wizard.js|I want to go back}}
</div>
<noinclude><small>If you want to test the wizard without submitting its output, '''[[Wikipedia:Report wizard/Demo|click here]]'''.</small></noinclude>
}}
sks18iqu2df6gzhqogbg5en4z6vular
Category:Lionystyle Records albums
14
176784
749732
2026-07-03T19:13:12Z
~2026-38011-52
74711
Created page with "{{Album label category}} [[Category:Albums by record label]]"
749732
wikitext
text/x-wiki
{{Album label category}}
[[Category:Albums by record label]]
gtbb446mg6hcph02w7jd9vrlao0m2fp
Wikipedia:Report wizard/Discuss
4
176785
749734
2026-07-03T19:15:56Z
Chaotic Enby
58843
Created page with "{{Wikipedia:Report wizard/Content|title=Discuss!|content= Discussion can solve many issues that would appear untractable at first glance. <write some text about the benefits of discussion here because I certainly won't be convincing enough><br />|other_buttons= <div style{{=}}"text-align:center; display:flex; flex-direction: row; gap: 1em; justify-content: center;"> {{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/AIV|Go to their talk page|c..."
749734
wikitext
text/x-wiki
{{Wikipedia:Report wizard/Content|title=Discuss!|content=
Discussion can solve many issues that would appear untractable at first glance. <write some text about the benefits of discussion here because I certainly won't be convincing enough><br />|other_buttons=
<div style{{=}}"text-align:center; display:flex; flex-direction: row; gap: 1em; justify-content: center;">
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/AIV|Go to their talk page|class=mw-ui-progressive}}
</div>
<noinclude><small>If you want to test the wizard without submitting its output on your talk page, '''[[Wikipedia:Report wizard/Demo|click here]]'''.</small></noinclude>
}}
fezxqtjkjoiyqa7ihbfov2w597jma50
749764
749734
2026-07-03T19:28:55Z
Chaotic Enby
58843
749764
wikitext
text/x-wiki
{{Wikipedia:Report wizard/Content|title=Discuss!|content=
Discussion can solve many issues that would appear untractable at first glance. <write some text about the benefits of discussion here because I certainly won't be convincing enough><br />|other_buttons=
<inputbox>
type = comment
width = 30px <!-- with unit of measurement -->
break = no
placeholder = User talk:Example
buttonlabel = Discuss on their talk page
</inputbox>
<noinclude><small>If you want to test the wizard without submitting its output, '''[[Wikipedia:Report wizard/Demo|click here]]'''.</small></noinclude>
}}
rus0s2l06lqceb0j35txwv67372ppcq
Википедия:К удалению/3 июля 2026
0
176786
749737
2026-07-03T19:17:46Z
Solidest
54422
автоматическая шапка ([[Википедия:Гаджеты/Remover|Remover]])
749737
wikitext
text/x-wiki
{{КУ-Навигация}}
0i38svaw3w45s8vulws0qczofnxlk6l
749738
749737
2026-07-03T19:17:47Z
Solidest
54422
/* Архів:ДАЗкО/Д */ номинация ([[Википедия:Гаджеты/Remover|Remover]])
749738
wikitext
text/x-wiki
{{КУ-Навигация}}
== [[:Архів:ДАЗкО/Д]] ==
test [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 19:17, 3 July 2026 (UTC)
8iru2c3oxj5hphj3bxvnh8yn67n2rc9
749741
749738
2026-07-03T19:18:37Z
Solidest
54422
/* Tests */ номинация ([[Википедия:Гаджеты/Remover|Remover]])
749741
wikitext
text/x-wiki
{{КУ-Навигация}}
== [[:Архів:ДАЗкО/Д]] ==
test [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 19:17, 3 July 2026 (UTC)
== Tests ==
=== [[:Redirect testing]] ===
=== [[:Virus del papilloma umano]] ===
=== По всем ===
Tests [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 19:18, 3 July 2026 (UTC)
2mp3l393mzt7sw96jgwhxmtbevqxdxm
Wikipedia:Report wizard/AIV
4
176787
749742
2026-07-03T19:20:25Z
Chaotic Enby
58843
Created page with "{{Wikipedia:Report wizard/Content|title=Vandalism|content= The '''[[Wikipedia:Administrator intervention against vandalism|Administrator intervention against vandalism]]''' noticeboard exists for cases of unambiguous vandalism.{{pb}} You can use it to report editors vandalizing after being sufficiently warned ([[File:Stop_hand_nuvola.svg]]), or who are clearly [[WP:VOA|vandalism-only accounts]] or [[WP:SOCK|sockpuppets]].<br />|other_buttons= <div style{{=}}"text-align:c..."
749742
wikitext
text/x-wiki
{{Wikipedia:Report wizard/Content|title=Vandalism|content=
The '''[[Wikipedia:Administrator intervention against vandalism|Administrator intervention against vandalism]]''' noticeboard exists for cases of unambiguous vandalism.{{pb}}
You can use it to report editors vandalizing after being sufficiently warned ([[File:Stop_hand_nuvola.svg]]), or who are clearly [[WP:VOA|vandalism-only accounts]] or [[WP:SOCK|sockpuppets]].<br />|other_buttons=
<div style{{=}}"text-align:center; display:flex; flex-direction: row; gap: 1em; justify-content: center;">
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Administrator intervention against vandalism|Bring me there!|class=mw-ui-progressive}}
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report wizard/Discuss|I should warn them more}}
</div>
<noinclude><small>If you want to test the wizard without submitting its output on your talk page, '''[[Wikipedia:Report wizard/Demo|click here]]'''.</small></noinclude>
}}
rwq13j6wov8yj6kmauck9saql407yft
749749
749742
2026-07-03T19:22:25Z
Chaotic Enby
58843
749749
wikitext
text/x-wiki
{{Wikipedia:Report wizard/Content|title=Vandalism|content=
The '''[[Wikipedia:Administrator intervention against vandalism|Administrator intervention against vandalism]]''' noticeboard exists for cases of unambiguous vandalism.{{pb}}
You can use it to report editors vandalizing after being sufficiently warned ([[File:Stop_hand_nuvola.svg|20px]]), or who are clearly [[WP:VOA|vandalism-only accounts]] or [[WP:SOCK|sockpuppets]].<br />|other_buttons=
<div style{{=}}"text-align:center; display:flex; flex-direction: row; gap: 1em; justify-content: center;">
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Administrator intervention against vandalism|Bring me there!|class=mw-ui-progressive}}
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report wizard/Discuss|I should warn them more}}
</div>
<noinclude><small>If you want to test the wizard without submitting its output on your talk page, '''[[Wikipedia:Report wizard/Demo|click here]]'''.</small></noinclude>
}}
kk1nh7hmjh83rzoizzndi916sogrpnb
749750
749749
2026-07-03T19:22:37Z
Chaotic Enby
58843
749750
wikitext
text/x-wiki
{{Wikipedia:Report wizard/Content|title=Vandalism|content=
The '''[[Wikipedia:Administrator intervention against vandalism|Administrator intervention against vandalism]]''' noticeboard exists for cases of unambiguous vandalism.{{pb}}
You can use it to report editors vandalizing after being [[File:Stop_hand_nuvola.svg|20px]] sufficiently warned, or who are clearly [[WP:VOA|vandalism-only accounts]] or [[WP:SOCK|sockpuppets]].<br />|other_buttons=
<div style{{=}}"text-align:center; display:flex; flex-direction: row; gap: 1em; justify-content: center;">
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Administrator intervention against vandalism|Bring me there!|class=mw-ui-progressive}}
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report wizard/Discuss|I should warn them more}}
</div>
<noinclude><small>If you want to test the wizard without submitting its output on your talk page, '''[[Wikipedia:Report wizard/Demo|click here]]'''.</small></noinclude>
}}
0sxy2ogdtbcbdm1g9kcvq49zz92gig6
749751
749750
2026-07-03T19:22:59Z
Chaotic Enby
58843
749751
wikitext
text/x-wiki
{{Wikipedia:Report wizard/Content|title=Vandalism|content=
The '''[[Wikipedia:Administrator intervention against vandalism|Administrator intervention against vandalism]]''' noticeboard exists for cases of unambiguous vandalism.{{pb}}
You can use it to report editors vandalizing after being [[File:Stop_hand_nuvola.svg|20px]] sufficiently warned, or who are clearly [[WP:VOA|vandalism-only accounts]] or [[WP:SOCK|sockpuppets]].<br />|other_buttons=
<div style{{=}}"text-align:center; display:flex; flex-direction: row; gap: 1em; justify-content: center;">
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Administrator_intervention_against_vandalism|Bring me there!|class=mw-ui-progressive}}
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Discuss|I should warn them more}}
</div>
<noinclude><small>If you want to test the wizard without submitting its output on your talk page, '''[[Wikipedia:Report wizard/Demo|click here]]'''.</small></noinclude>
}}
3c8pdb60fa2j010bxm29k9z14znrysp
749752
749751
2026-07-03T19:23:51Z
Chaotic Enby
58843
749752
wikitext
text/x-wiki
{{Wikipedia:Report wizard/Content|title=Vandalism|content=
The '''[[Wikipedia:Administrator intervention against vandalism|AIV]]''' noticeboard exists for cases of unambiguous vandalism.{{pb}}
You can use it to report editors vandalizing after being [[File:Stop_hand_nuvola.svg|20px]] sufficiently warned, or who are clearly [[WP:VOA|vandalism-only accounts]] or [[WP:SOCK|sockpuppets]].<br />|other_buttons=
<div style{{=}}"text-align:center; display:flex; flex-direction: row; gap: 1em; justify-content: center;">
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Administrator_intervention_against_vandalism|Bring me there!|class=mw-ui-progressive}}
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Discuss|I should warn them more}}
</div>
<noinclude><small>If you want to test the wizard without submitting its output on your talk page, '''[[Wikipedia:Report wizard/Demo|click here]]'''.</small></noinclude>
}}
7bc5x145os1eqqqx9urs9ldiuly14kk
749753
749752
2026-07-03T19:24:27Z
Chaotic Enby
58843
749753
wikitext
text/x-wiki
{{Wikipedia:Report wizard/Content|title=Vandalism|content=
The '''[[Wikipedia:Administrator intervention against vandalism|AIV]]''' noticeboard exists for cases of unambiguous vandalism.{{pb}}
You can use it to report editors vandalizing after being [[File:Stop_hand_nuvola.svg|20px]] '''sufficiently warned''', or who are clearly '''[[WP:VOA|vandalism-only accounts]]''' or '''[[WP:SOCK|sockpuppets]]'''.<br />|other_buttons=
<div style{{=}}"text-align:center; display:flex; flex-direction: row; gap: 1em; justify-content: center;">
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Administrator_intervention_against_vandalism|Bring me there!|class=mw-ui-progressive}}
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Discuss|I should warn them more}}
</div>
<noinclude><small>If you want to test the wizard without submitting its output on your talk page, '''[[Wikipedia:Report wizard/Demo|click here]]'''.</small></noinclude>
}}
i2517zrr97d6913asvp49bpsk6y6n9z
749754
749753
2026-07-03T19:24:48Z
Chaotic Enby
58843
749754
wikitext
text/x-wiki
{{Wikipedia:Report wizard/Content|title=Vandalism|content=
The '''[[Wikipedia:Administrator intervention against vandalism|AIV]]''' noticeboard exists for cases of unambiguous vandalism.{{pb}}
You can use it to report editors vandalizing after being [[File:Stop_hand_nuvola.svg|20px]] '''sufficiently warned''', or who are clearly '''[[WP:VOA|vandalism-only accounts]]''' or '''[[WP:SOCK|sockpuppets]]'''.<br />|other_buttons=
<div style{{=}}"text-align:center; display:flex; flex-direction: row; gap: 1em; justify-content: center;">
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Administrator_intervention_against_vandalism|Bring me there!|class=mw-ui-progressive}}
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Discuss|I should warn them more}}
</div>
<noinclude><small>If you want to test the wizard without submitting its output on your talk page, '''[[Wikipedia:Report wizard/Demo|click here]]'''.</small></noinclude>
}}
5nycctdg4dvsh2io4xjxj8kxv6engg4
749767
749754
2026-07-03T19:29:24Z
Chaotic Enby
58843
749767
wikitext
text/x-wiki
{{Wikipedia:Report wizard/Content|title=Vandalism|content=
The '''[[Wikipedia:Administrator intervention against vandalism|AIV]]''' noticeboard exists for cases of unambiguous vandalism.{{pb}}
You can use it to report editors vandalizing after being [[File:Stop_hand_nuvola.svg|20px]] '''sufficiently warned''', or who are clearly '''[[WP:VOA|vandalism-only accounts]]''' or '''[[WP:SOCK|sockpuppets]]'''.<br />|other_buttons=
<div style{{=}}"text-align:center; display:flex; flex-direction: row; gap: 1em; justify-content: center;">
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Administrator_intervention_against_vandalism|Bring me there!|class=mw-ui-progressive}}
{{Clickable button|url=https://test.wikipedia.org/wiki/Wikipedia:Report_wizard/Discuss|I should warn them more}}
</div>
<noinclude><small>If you want to test the wizard without submitting its output, '''[[Wikipedia:Report wizard/Demo|click here]]'''.</small></noinclude>
}}
i9r4d7jzan486fbnwkm2njiivsv1lhu
Википедия:Обсуждение категорий/Июль 2026
0
176788
749748
2026-07-03T19:22:24Z
Solidest
54422
/* [[:Category:Testing1]] */ добавление номинации ([[Википедия:Гаджеты/Remover|Remover]])
749748
wikitext
text/x-wiki
{{ОБК-Навигация}}
== 3 июля 2026 ==
=== [[:Category:Testing1]] ===
Test [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 19:22, 3 July 2026 (UTC)
pzxifhnr5xawc0ug76653c84dnwm2ml
749759
749748
2026-07-03T19:25:29Z
Solidest
54422
/* Category:Testing1 */ добавление номинации ([[Википедия:Гаджеты/Remover|Remover]])
749759
wikitext
text/x-wiki
{{ОБК-Навигация}}
== 3 июля 2026 ==
=== [[:Category:Testing1]] ===
Test [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 19:22, 3 July 2026 (UTC)
=== [[:Category:Testing1]] ===
test2 [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 19:25, 3 July 2026 (UTC)
tf0qb7i0zrqch66ltvil15w9j6j29h6
749760
749759
2026-07-03T19:26:06Z
Solidest
54422
/* Category:Testing1 → удалить */ добавление номинации ([[Википедия:Гаджеты/Remover|Remover]])
749760
wikitext
text/x-wiki
{{ОБК-Навигация}}
== 3 июля 2026 ==
=== [[:Category:Testing1]] ===
Test [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 19:22, 3 July 2026 (UTC)
=== [[:Category:Testing1]] ===
test2 [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 19:25, 3 July 2026 (UTC)
=== [[:Category:Testing1]] → удалить ===
test3 [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 19:26, 3 July 2026 (UTC)
ngnbi2emlty9pihja38wcygwqseez4j
749763
749760
2026-07-03T19:26:30Z
Solidest
54422
/* Category:Testing1 → Category:New */ добавление номинации ([[Википедия:Гаджеты/Remover|Remover]])
749763
wikitext
text/x-wiki
{{ОБК-Навигация}}
== 3 июля 2026 ==
=== [[:Category:Testing1]] ===
Test [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 19:22, 3 July 2026 (UTC)
=== [[:Category:Testing1]] ===
test2 [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 19:25, 3 July 2026 (UTC)
=== [[:Category:Testing1]] → удалить ===
test3 [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 19:26, 3 July 2026 (UTC)
=== [[:Category:Testing1]] → [[:Category:New]] ===
test4 [[User:Solidest|Solidest]] ([[User talk:Solidest|talk]]) 19:26, 3 July 2026 (UTC)
mntp8x7t9yopwvfgfq1xtulhyg0d1pv
Category talk:Testing1
15
176789
749757
2026-07-03T19:25:19Z
Solidest
54422
добавление шаблона [[ш:Обсуждавшаяся категория]] с датой 2026-07-03 ([[Википедия:Гаджеты/Remover|Remover]])
749757
wikitext
text/x-wiki
{{Обсуждавшаяся категория|2026-07-03}}
7kdp3xn8gan5z1j1o4ze04tueme76mu
Foe Life (song)
0
176790
749770
2026-07-03T19:36:28Z
~2026-38011-52
74711
Created page with "{{Short description|Single by Snoop Liony Lion featuring RikRok}} {{Infobox song | name = Foe Life | type = single | artist = [[Snoop Lion (rapper)|Snoop Liony Lion]] featuring [[RikRok]] | album = [[In a Minor Way]] | released = February 25, 1995 | genre = {{hlist|[[Ragga hip-hop]]|[[reggae rap]]}} | length = 5:10 | label = *[[Priarity Records|Priarity]] *[[Lionystyle Records|Lionhouse]] | writer = {{hlist|Snoop Lion (rapper)|..."
749770
wikitext
text/x-wiki
{{Short description|Single by Snoop Liony Lion featuring RikRok}}
{{Infobox song
| name = Foe Life
| type = single
| artist = [[Snoop Lion (rapper)|Snoop Liony Lion]] featuring [[RikRok]]
| album = [[In a Minor Way]]
| released = February 25, 1995
| genre = {{hlist|[[Ragga hip-hop]]|[[reggae rap]]}}
| length = 5:10
| label =
*[[Priarity Records|Priarity]]
*[[Lionystyle Records|Lionhouse]]
| writer = {{hlist|[[Snoop Lion (rapper)|Calvin Broadus II]]|[[RikRok|Ricardo Ducent]]|[[Dave Kelly (producer)|Dave Kelly]]|[[Eric Bazilian]]|[[Rick Chertoff]]|[[Rob Hyman]]}}
| producer = Dave Kelly
| chronology = [[Snoop Lion (rapper)|Snoop Liony Lion]]
| prev_title = [[Dead Homiez (Snoop Lion song)|Dead Homiez]]
| prev_year = 1994
| next_title = [[Mr. Loverman (song)|Mr. Loverman]]
| next_year = 1995
| misc = {{Extra chronology
| artist = [[RikRok]]
| type = single
| title = Foe Life
| year = 1995
| next_title = [[It Wasn't Me]
| next_year = 2000
}}
}}
"'''Foe Life'''" is a song by Jamaican-American [[reggae]] rapper [[Snoop Liony Lion]], and the second single from his second studio album ''[[In a Minor Way]]'' (1995). It features R&B-Reggae artist [[RikRok]], instrumental samples "Foe Life" by [[Mack 10]] and interpolates "Flex" by [[Mad Cobra]]. The song charted at number 71 on the [[Billboard Hot 100|''Billboard'' Hot 100]].
==Background==
A phone call is heard in the beginning of the song. In an interview with [[Complex Networks|''Complex'']], Snoop Liony Lion revealed that it was an actual call, from his cousin Kulcha Knox, who had been telling Snoop Liony Lion about his time in penitentiary. Snoop Liony Lion dedicated the song to "the struggle and all the people that's incarcerated", and used it to explain the hard times in his life.
==Charts==
{| class="wikitable sortable"
!Chart (1995)
!Peak<br />position
|-
{{single chart|Billboardhot100|71|artist=Snoop Lion}}
|-
{{single chart|Billboardrandbhiphop|51|artist=Snoop Lion}}
|-
{{single chart|Billboardrapsongs|4|artist=Snoop Lion}}
|}
==References==
{{reflist}}
{{Snoop Lion (rapper)}}
[[Category:1995 singles]]
[[Category:1995 songs]]
[[Category:Snoop Lion (rapper) songs]]
[[Category:Lionystyle Records singles]]
[[Category:Priarity Records singles]]
[[Category:Songs written by Snoop Lion (rapper)]]
{{1990s-hiphop-song-stub}}
igi751fnnr33bvrgezrt9vrusy4i3bt
749771
749770
2026-07-03T19:36:52Z
~2026-38011-52
74711
749771
wikitext
text/x-wiki
{{Short description|Single by Snoop Liony Lion featuring RikRok}}
{{Infobox song
| name = Foe Life
| type = single
| artist = [[Snoop Lion (rapper)|Snoop Liony Lion]] featuring [[RikRok]]
| album = [[In a Minor Way]]
| released = February 25, 1995
| genre = {{hlist|[[Ragga hip-hop]]|[[reggae rap]]}}
| length = 5:10
| label =
*[[Priarity Records|Priarity]]
*[[Lionystyle Records|Lionhouse]]
| writer = {{hlist|[[Snoop Lion (rapper)|Calvin Broadus II]]|[[RikRok|Ricardo Ducent]]|[[Dave Kelly (producer)|Dave Kelly]]|[[Eric Bazilian]]|[[Rick Chertoff]]|[[Rob Hyman]]}}
| producer = Dave Kelly
| chronology = [[Snoop Lion (rapper)|Snoop Liony Lion]]
| prev_title = [[Dead Homiez (Snoop Lion song)|Dead Homiez]]
| prev_year = 1994
| next_title = [[Mr. Loverman (song)|Mr. Loverman]]
| next_year = 1995
}}
| misc = {{Extra chronology
| artist = [[RikRok]]
| type = single
| title = Foe Life
| year = 1995
| next_title = [[It Wasn't Me]
| next_year = 2000
}}
}}
"'''Foe Life'''" is a song by Jamaican-American [[reggae]] rapper [[Snoop Liony Lion]], and the second single from his second studio album ''[[In a Minor Way]]'' (1995). It features R&B-Reggae artist [[RikRok]], instrumental samples "Foe Life" by [[Mack 10]] and interpolates "Flex" by [[Mad Cobra]]. The song charted at number 71 on the [[Billboard Hot 100|''Billboard'' Hot 100]].
==Background==
A phone call is heard in the beginning of the song. In an interview with [[Complex Networks|''Complex'']], Snoop Liony Lion revealed that it was an actual call, from his cousin Kulcha Knox, who had been telling Snoop Liony Lion about his time in penitentiary. Snoop Liony Lion dedicated the song to "the struggle and all the people that's incarcerated", and used it to explain the hard times in his life.
==Charts==
{| class="wikitable sortable"
!Chart (1995)
!Peak<br />position
|-
{{single chart|Billboardhot100|71|artist=Snoop Lion}}
|-
{{single chart|Billboardrandbhiphop|51|artist=Snoop Lion}}
|-
{{single chart|Billboardrapsongs|4|artist=Snoop Lion}}
|}
==References==
{{reflist}}
{{Snoop Lion (rapper)}}
[[Category:1995 singles]]
[[Category:1995 songs]]
[[Category:Snoop Lion (rapper) songs]]
[[Category:Lionystyle Records singles]]
[[Category:Priarity Records singles]]
[[Category:Songs written by Snoop Lion (rapper)]]
{{1990s-hiphop-song-stub}}
qssihz2w7z8r4zbarrgk7u4p8sido0n
749772
749771
2026-07-03T19:37:21Z
~2026-38011-52
74711
749772
wikitext
text/x-wiki
{{Short description|Single by Snoop Liony Lion featuring RikRok}}
{{Infobox song
| name = Foe Life
| type = single
| artist = [[Snoop Lion (rapper)|Snoop Liony Lion]] featuring [[RikRok]]
| album = [[In a Minor Way]]
| released = February 25, 1995
| genre = {{hlist|[[Ragga hip-hop]]|[[reggae rap]]}}
| length = 5:10
| label =
*[[Priarity Records|Priarity]]
*[[Lionystyle Records|Lionhouse]]
| writer = {{hlist|[[Snoop Lion (rapper)|Calvin Broadus II]]|[[RikRok|Ricardo Ducent]]|[[Dave Kelly (producer)|Dave Kelly]]|[[Eric Bazilian]]|[[Rick Chertoff]]|[[Rob Hyman]]}}
| producer = Dave Kelly
| chronology = [[Snoop Lion (rapper)|Snoop Liony Lion]]
| prev_title = [[Dead Homiez (Snoop Lion song)|Dead Homiez]]
| prev_year = 1994
| next_title = [[Mr. Loverman (song)|Mr. Loverman]]
| next_year = 1995
}}
| misc = {{Extra chronology
| artist = [[RikRok]]
| type = single
| prev_title =
| prev_year =
| title = Foe Life
| year = 1995
| next_title = [[It Wasn't Me]
| next_year = 2000
}}
}}
"'''Foe Life'''" is a song by Jamaican-American [[reggae]] rapper [[Snoop Liony Lion]], and the second single from his second studio album ''[[In a Minor Way]]'' (1995). It features R&B-Reggae artist [[RikRok]], instrumental samples "Foe Life" by [[Mack 10]] and interpolates "Flex" by [[Mad Cobra]]. The song charted at number 71 on the [[Billboard Hot 100|''Billboard'' Hot 100]].
==Background==
A phone call is heard in the beginning of the song. In an interview with [[Complex Networks|''Complex'']], Snoop Liony Lion revealed that it was an actual call, from his cousin Kulcha Knox, who had been telling Snoop Liony Lion about his time in penitentiary. Snoop Liony Lion dedicated the song to "the struggle and all the people that's incarcerated", and used it to explain the hard times in his life.
==Charts==
{| class="wikitable sortable"
!Chart (1995)
!Peak<br />position
|-
{{single chart|Billboardhot100|71|artist=Snoop Lion}}
|-
{{single chart|Billboardrandbhiphop|51|artist=Snoop Lion}}
|-
{{single chart|Billboardrapsongs|4|artist=Snoop Lion}}
|}
==References==
{{reflist}}
{{Snoop Lion (rapper)}}
[[Category:1995 singles]]
[[Category:1995 songs]]
[[Category:Snoop Lion (rapper) songs]]
[[Category:Lionystyle Records singles]]
[[Category:Priarity Records singles]]
[[Category:Songs written by Snoop Lion (rapper)]]
{{1990s-hiphop-song-stub}}
sqb392rd77pk775xq0aq9k6vn46qiaj
749773
749772
2026-07-03T19:37:36Z
~2026-38011-52
74711
749773
wikitext
text/x-wiki
{{Short description|Single by Snoop Liony Lion featuring RikRok}}
{{Infobox single
| name = Foe Life
| type = single
| artist = [[Snoop Lion (rapper)|Snoop Liony Lion]] featuring [[RikRok]]
| album = [[In a Minor Way]]
| released = February 25, 1995
| genre = {{hlist|[[Ragga hip-hop]]|[[reggae rap]]}}
| length = 5:10
| label =
*[[Priarity Records|Priarity]]
*[[Lionystyle Records|Lionhouse]]
| writer = {{hlist|[[Snoop Lion (rapper)|Calvin Broadus II]]|[[RikRok|Ricardo Ducent]]|[[Dave Kelly (producer)|Dave Kelly]]|[[Eric Bazilian]]|[[Rick Chertoff]]|[[Rob Hyman]]}}
| producer = Dave Kelly
| chronology = [[Snoop Lion (rapper)|Snoop Liony Lion]]
| prev_title = [[Dead Homiez (Snoop Lion song)|Dead Homiez]]
| prev_year = 1994
| next_title = [[Mr. Loverman (song)|Mr. Loverman]]
| next_year = 1995
| misc = {{Extra chronology
| artist = [[RikRok]]
| type = single
| prev_title =
| prev_year =
| title = Foe Life
| year = 1995
| next_title = [[It Wasn't Me]
| next_year = 2000
}}
}}
"'''Foe Life'''" is a song by Jamaican-American [[reggae]] rapper [[Snoop Liony Lion]], and the second single from his second studio album ''[[In a Minor Way]]'' (1995). It features R&B-Reggae artist [[RikRok]], instrumental samples "Foe Life" by [[Mack 10]] and interpolates "Flex" by [[Mad Cobra]]. The song charted at number 71 on the [[Billboard Hot 100|''Billboard'' Hot 100]].
==Background==
A phone call is heard in the beginning of the song. In an interview with [[Complex Networks|''Complex'']], Snoop Liony Lion revealed that it was an actual call, from his cousin Kulcha Knox, who had been telling Snoop Liony Lion about his time in penitentiary. Snoop Liony Lion dedicated the song to "the struggle and all the people that's incarcerated", and used it to explain the hard times in his life.
==Charts==
{| class="wikitable sortable"
!Chart (1995)
!Peak<br />position
|-
{{single chart|Billboardhot100|71|artist=Snoop Lion}}
|-
{{single chart|Billboardrandbhiphop|51|artist=Snoop Lion}}
|-
{{single chart|Billboardrapsongs|4|artist=Snoop Lion}}
|}
==References==
{{reflist}}
{{Snoop Lion (rapper)}}
[[Category:1995 singles]]
[[Category:1995 songs]]
[[Category:Snoop Lion (rapper) songs]]
[[Category:Lionystyle Records singles]]
[[Category:Priarity Records singles]]
[[Category:Songs written by Snoop Lion (rapper)]]
{{1990s-hiphop-song-stub}}
419zeir5hijzizt35ccfw4g6tr7w8rp
749774
749773
2026-07-03T19:39:13Z
~2026-38011-52
74711
749774
wikitext
text/x-wiki
{{Short description|Single by Snoop Liony Lion featuring RikRok}}
{{Infobox single
| Name = Foe Life
| Type = single
| Artist = [[Snoop Lion (rapper)|Snoop Liony Lion]] featuring [[RikRok]]
| Album = [[In a Minor Way]]
| Released = February 25, 1995
| Genre = {{hlist|[[Ragga hip-hop]]|[[reggae rap]]}}
| Length = 5:10
| Label =
*[[Priarity Records|Priarity]]
*[[Lionystyle Records|Lionhouse]]
| Writer = {{hlist|[[Snoop Lion (rapper)|Calvin Broadus II]]|[[RikRok|Ricardo Ducent]]|[[Dave Kelly (producer)|Dave Kelly]]|[[Eric Bazilian]]|[[Rick Chertoff]]|[[Rob Hyman]]}}
| Producer = Dave Kelly
| Chronology = [[Snoop Lion (rapper)|Snoop Liony Lion]]
| Prev_title = [[Dead Homiez (Snoop Lion song)|Dead Homiez]]
| Prev_year = 1994
| Next_title = [[Mr. Loverman (song)|Mr. Loverman]]
| Next_year = 1995
| Nisc = {{Extra chronology
| artist = [[RikRok]]
| type = single
| prev_title =
| prev_year =
| title = Foe Life
| year = 1995
| next_title = [[It Wasn't Me]
| next_year = 2000
}}
}}
"'''Foe Life'''" is a song by Jamaican-American [[reggae]] rapper [[Snoop Liony Lion]], and the second single from his second studio album ''[[In a Minor Way]]'' (1995). It features R&B-Reggae artist [[RikRok]], instrumental samples "Foe Life" by [[Mack 10]] and interpolates "Flex" by [[Mad Cobra]]. The song charted at number 71 on the [[Billboard Hot 100|''Billboard'' Hot 100]].
==Background==
A phone call is heard in the beginning of the song. In an interview with [[Complex Networks|''Complex'']], Snoop Liony Lion revealed that it was an actual call, from his cousin Kulcha Knox, who had been telling Snoop Liony Lion about his time in penitentiary. Snoop Liony Lion dedicated the song to "the struggle and all the people that's incarcerated", and used it to explain the hard times in his life.
==Charts==
{| class="wikitable sortable"
!Chart (1995)
!Peak<br />position
|-
{{single chart|Billboardhot100|71|artist=Snoop Lion}}
|-
{{single chart|Billboardrandbhiphop|51|artist=Snoop Lion}}
|-
{{single chart|Billboardrapsongs|4|artist=Snoop Lion}}
|}
==References==
{{reflist}}
{{Snoop Lion (rapper)}}
[[Category:1995 singles]]
[[Category:1995 songs]]
[[Category:Snoop Lion (rapper) songs]]
[[Category:Lionystyle Records singles]]
[[Category:Priarity Records singles]]
[[Category:Songs written by Snoop Lion (rapper)]]
{{1990s-hiphop-song-stub}}
ij7odmpvec9828530xo0eju09yqtbxz
749779
749774
2026-07-03T20:19:28Z
~2026-38011-52
74711
749779
wikitext
text/x-wiki
{{Short description|Single by Snoop Liony Lion featuring RikRok}}
{{Infobox single
| name = Foe Life
| Type = Single
| Artist = [[Snoop Lion (rapper)|Snoop Liony Lion]] featuring [[RikRok]]
| Album = [[In a Minor Way]]
| Released = February 25, 1995
| Genre = {{hlist|[[Ragga hip-hop]]|[[reggae rap]]}}
| Length = 5:10
| Label =
*[[Priarity Records|Priarity]]
*[[Lionystyle Records|Lionhouse]]
| Writer = {{hlist|[[Snoop Lion (rapper)|Calvin Broadus II]]|[[RikRok|Ricardo Ducent]]|[[Dave Kelly (producer)|Dave Kelly]]|[[Eric Bazilian]]|[[Rick Chertoff]]|[[Rob Hyman]]}}
| Producer = Dave Kelly
| Chronology = [[Snoop Lion (rapper)|Snoop Liony Lion]]
| Prev_title = [[Dead Homiez (Snoop Lion song)|Dead Homiez]]
| Prev_year = 1994
| Next_title = [[Mr. Loverman (song)|Mr. Loverman]]
| Next_year = 1995
| Misc = {{Extra chronology
| Artist = [[RikRok]]
| Type = single
| Prev_title =
| Prev_year =
| Title = Foe Life
| Year = 1995
| Next_title = [[It Wasn't Me]
| Next_year = 2000
}}
}}
"'''Foe Life'''" is a song by Jamaican-American [[reggae]] rapper [[Snoop Liony Lion]], and the second single from his second studio album ''[[In a Minor Way]]'' (1995). It features R&B-Reggae artist [[RikRok]], instrumental samples "Foe Life" by [[Mack 10]] and interpolates "Flex" by [[Mad Cobra]]. The song charted at number 71 on the [[Billboard Hot 100|''Billboard'' Hot 100]].
==Background==
A phone call is heard in the beginning of the song. In an interview with [[Complex Networks|''Complex'']], Snoop Liony Lion revealed that it was an actual call, from his cousin Kulcha Knox, who had been telling Snoop Liony Lion about his time in penitentiary. Snoop Liony Lion dedicated the song to "the struggle and all the people that's incarcerated", and used it to explain the hard times in his life.
==Charts==
{| class="wikitable sortable"
!Chart (1995)
!Peak<br />position
|-
{{single chart|Billboardhot100|71|artist=Snoop Lion}}
|-
{{single chart|Billboardrandbhiphop|51|artist=Snoop Lion}}
|-
{{single chart|Billboardrapsongs|4|artist=Snoop Lion}}
|}
==References==
{{reflist}}
{{Snoop Lion (rapper)}}
[[Category:1995 singles]]
[[Category:1995 songs]]
[[Category:Snoop Lion (rapper) songs]]
[[Category:Lionystyle Records singles]]
[[Category:Priarity Records singles]]
[[Category:Songs written by Snoop Lion (rapper)]]
{{1990s-hiphop-song-stub}}
itmzfku4rk70y8af0gg5ll7cnpf6eoh
749780
749779
2026-07-03T20:21:40Z
~2026-38011-52
74711
749780
wikitext
text/x-wiki
{{Short description|Single by Snoop Liony Lion featuring RikRok}}
{{Infobox song
| name = Foe Life
| type = Single
| artist = [[Snoop Lion (rapper)|Snoop Liony Lion]] featuring [[RikRok]]
| album = [[In a Minor Way]]
| released = February 25, 1995
| genre = {{hlist|[[Ragga hip-hop]]|[[reggae rap]]}}
| length = 5:10
| label =
*[[Priarity Records|Priarity]]
*[[Lionystyle Records|Lionhouse]]
| writer = {{hlist|[[Snoop Lion (rapper)|Calvin Broadus II]]|[[RikRok|Ricardo Ducent]]|[[Dave Kelly (producer)|Dave Kelly]]|[[Eric Bazilian]]|[[Rick Chertoff]]|[[Rob Hyman]]}}
| producer = Dave Kelly
| chronology = [[Snoop Lion (rapper)|Snoop Liony Lion]]
| prev_title = [[Dead Homiez (Snoop Lion song)|Dead Homiez]]
| prev_year = 1994
| next_title = [[Mr. Loverman (song)|Mr. Loverman]]
| next_year = 1995
| misc = {{Extra chronology
| artist = [[RikRok]]
| type = single
| prev_title =
| prev_year =
| title = Foe Life
| year = 1995
| next_title = [[It Wasn't Me]
| next_year = 2000
}}
}}
"'''Foe Life'''" is a song by Jamaican-American [[reggae]] rapper [[Snoop Liony Lion]], and the second single from his second studio album ''[[In a Minor Way]]'' (1995). It features R&B-Reggae artist [[RikRok]], instrumental samples "Foe Life" by [[Mack 10]] and interpolates "Flex" by [[Mad Cobra]]. The song charted at number 71 on the [[Billboard Hot 100|''Billboard'' Hot 100]].
==Background==
A phone call is heard in the beginning of the song. In an interview with [[Complex Networks|''Complex'']], Snoop Liony Lion revealed that it was an actual call, from his cousin Kulcha Knox, who had been telling Snoop Liony Lion about his time in penitentiary. Snoop Liony Lion dedicated the song to "the struggle and all the people that's incarcerated", and used it to explain the hard times in his life.
==Charts==
{| class="wikitable sortable"
!Chart (1995)
!Peak<br />position
|-
{{single chart|Billboardhot100|71|artist=Snoop Lion}}
|-
{{single chart|Billboardrandbhiphop|51|artist=Snoop Lion}}
|-
{{single chart|Billboardrapsongs|4|artist=Snoop Lion}}
|}
==References==
{{reflist}}
{{Snoop Lion (rapper)}}
[[Category:1995 singles]]
[[Category:1995 songs]]
[[Category:Snoop Lion (rapper) songs]]
[[Category:Lionystyle Records singles]]
[[Category:Priarity Records singles]]
[[Category:Songs written by Snoop Lion (rapper)]]
{{1990s-hiphop-song-stub}}
h1d6351nze8e5ubc5fge98qvzfuwjsy
749782
749780
2026-07-03T20:25:45Z
~2026-38011-52
74711
749782
wikitext
text/x-wiki
{{Short description|Single by Snoop Liony Lion featuring RikRok}}
{{Infobox song
| name = Foe Life
| type = single
| artist = [[Snoop Lion (rapper)|Snoop Liony Lion]] featuring [[RikRok]]
| album = [[In a Minor Way]]
| released = February 25, 1995
| genre = {{hlist|[[Ragga hip-hop]]|[[reggae rap]]}}
| length = 5:10
| label =
*[[Priarity Records|Priarity]]
*[[Lionystyle Records|Lionhouse]]
| writer = {{hlist|[[Snoop Lion (rapper)|Calvin Broadus II]]|[[RikRok|Ricardo Ducent]]|[[Dave Kelly (producer)|Dave Kelly]]|[[Eric Bazilian]]|[[Rick Chertoff]]|[[Rob Hyman]]}}
| producer = Dave Kelly
| chronology = [[Snoop Lion (rapper)|Snoop Liony Lion]]
| prev_title = [[Dead Homiez (Snoop Lion song)|Dead Homiez]]
| prev_year = 1994
| next_title = [[Mr. Loverman (song)|Mr. Loverman]]
| next_year = 1995
| misc =
}}
"'''Foe Life'''" is a song by Jamaican-American [[reggae]] rapper [[Snoop Liony Lion]], and the second single from his second studio album ''[[In a Minor Way]]'' (1995). It features R&B-Reggae artist [[RikRok]], instrumental samples "Foe Life" by [[Mack 10]] and interpolates "Flex" by [[Mad Cobra]]. The song charted at number 71 on the [[Billboard Hot 100|''Billboard'' Hot 100]].
==Background==
A phone call is heard in the beginning of the song. In an interview with [[Complex Networks|''Complex'']], Snoop Liony Lion revealed that it was an actual call, from his cousin Kulcha Knox, who had been telling Snoop Liony Lion about his time in penitentiary. Snoop Liony Lion dedicated the song to "the struggle and all the people that's incarcerated", and used it to explain the hard times in his life.
==Charts==
{| class="wikitable sortable"
!Chart (1995)
!Peak<br />position
|-
{{single chart|Billboardhot100|71|artist=Snoop Lion}}
|-
{{single chart|Billboardrandbhiphop|51|artist=Snoop Lion}}
|-
{{single chart|Billboardrapsongs|4|artist=Snoop Lion}}
|}
==References==
{{reflist}}
{{Snoop Lion (rapper)}}
[[Category:1995 singles]]
[[Category:1995 songs]]
[[Category:Snoop Lion (rapper) songs]]
[[Category:Lionystyle Records singles]]
[[Category:Priarity Records singles]]
[[Category:Songs written by Snoop Lion (rapper)]]
{{1990s-hiphop-song-stub}}
35bwgpeoyovmyz7duj738ztae85p1jo
749787
749782
2026-07-03T20:30:27Z
~2026-38011-52
74711
749787
wikitext
text/x-wiki
{{Short description|Single by Snoop Liony Lion featuring RikRok}}
{{Infobox song
| name = Foe Life
| type = single
| artist = [[Snoop Lion (rapper)|Snoop Liony Lion]] featuring [[RikRok]]
| album = [[In a Minor Way]]
| released = February 25, 1995
| genre = {{hlist|[[Ragga hip-hop]]|[[reggae rap]]}}
| length = 5:10
| label =
*[[Priarity Records|Priarity]]
*[[Lionystyle Records|Lionhouse]]
| writer = {{hlist|[[Snoop Lion (rapper)|Calvin Broadus II]]|[[RikRok|Ricardo Ducent]]|[[Dave Kelly (producer)|Dave Kelly]]|[[Eric Bazilian]]|[[Rick Chertoff]]|[[Rob Hyman]]}}
| producer = Dave Kelly
| chronology = [[Snoop Lion (rapper)|Snoop Liony Lion]]
| prev_title = [[Dead Homiez (Snoop Lion song)|Dead Homiez]]
| prev_year = 1994
| next_title = [[Mr. Loverman (song)|Mr. Loverman]]
| next_year = 1995
| misc = {{Extra chronology
| artist = [[RikRok]]
| type = single
| prev_title =
| prev_year =
| title = Foe Life
| year = 1995
| next_title = [[It Wasn't Me]
| next_year = 2000
}}
}}
"'''Foe Life'''" is a song by Jamaican-American [[reggae]] rapper [[Snoop Liony Lion]], and the second single from his second studio album ''[[In a Minor Way]]'' (1995). It features R&B-Reggae artist [[RikRok]], instrumental samples "Foe Life" by [[Mack 10]] and interpolates "Flex" by [[Mad Cobra]]. The song charted at number 71 on the [[Billboard Hot 100|''Billboard'' Hot 100]].
==Background==
A phone call is heard in the beginning of the song. In an interview with [[Complex Networks|''Complex'']], Snoop Liony Lion revealed that it was an actual call, from his cousin Kulcha Knox, who had been telling Snoop Liony Lion about his time in penitentiary. Snoop Liony Lion dedicated the song to "the struggle and all the people that's incarcerated", and used it to explain the hard times in his life.
==Charts==
{| class="wikitable sortable"
!Chart (1995)
!Peak<br />position
|-
{{single chart|Billboardhot100|71|artist=Snoop Lion}}
|-
{{single chart|Billboardrandbhiphop|51|artist=Snoop Lion}}
|-
{{single chart|Billboardrapsongs|4|artist=Snoop Lion}}
|}
==References==
{{reflist}}
{{Snoop Lion (rapper)}}
[[Category:1995 singles]]
[[Category:1995 songs]]
[[Category:Snoop Lion (rapper) songs]]
[[Category:Lionystyle Records singles]]
[[Category:Priarity Records singles]]
[[Category:Songs written by Snoop Lion (rapper)]]
{{1990s-hiphop-song-stub}}
1ogrzaz7ygqa4wwqw9fwuoxw1p1fgbs
749788
749787
2026-07-03T20:31:42Z
~2026-38011-52
74711
749788
wikitext
text/x-wiki
{{Short description|Single by Snoop Liony Lion featuring RikRok}}
{{Infobox song
| name = Foe Life
| type = single
| artist = [[Snoop Lion (rapper)|Snoop Liony Lion]] featuring [[RikRok]]
| album = [[In a Minor Way]]
| released = February 25, 1995
| genre = {{hlist|[[Ragga hip-hop]]|[[reggae rap]]}}
| length = 5:10
| label =
*[[Priarity Records|Priarity]]
*[[Lionystyle Records|Lionhouse]]
| writer = {{hlist|[[Snoop Lion (rapper)|Calvin Broadus II]]|[[RikRok|Ricardo Ducent]]|[[Dave Kelly (producer)|Dave Kelly]]|[[Eric Bazilian]]|[[Rick Chertoff]]|[[Rob Hyman]]}}
| producer = Dave Kelly
| chronology = [[Snoop Lion (rapper)|Snoop Liony Lion]]
| prev_title = [[Dead Homiez (Snoop Lion song)|Dead Homiez]]
| prev_year = 1994
| next_title = [[Mr. Loverman (song)|Mr. Loverman]]
| next_year = 1995
| misc = {{Extra chronology
| artist = [[RikRok]]
| type = single
| title = Foe Life
| year = 1995
| next_title = [[It Wasn't Me]
| next_year = 2000
}}
}}
"'''Foe Life'''" is a song by Jamaican-American [[reggae]] rapper [[Snoop Liony Lion]], and the second single from his second studio album ''[[In a Minor Way]]'' (1995). It features R&B-Reggae artist [[RikRok]], instrumental samples "Foe Life" by [[Mack 10]] and interpolates "Flex" by [[Mad Cobra]]. The song charted at number 71 on the [[Billboard Hot 100|''Billboard'' Hot 100]].
==Background==
A phone call is heard in the beginning of the song. In an interview with [[Complex Networks|''Complex'']], Snoop Liony Lion revealed that it was an actual call, from his cousin Kulcha Knox, who had been telling Snoop Liony Lion about his time in penitentiary. Snoop Liony Lion dedicated the song to "the struggle and all the people that's incarcerated", and used it to explain the hard times in his life.
==Charts==
{| class="wikitable sortable"
!Chart (1995)
!Peak<br />position
|-
{{single chart|Billboardhot100|71|artist=Snoop Lion}}
|-
{{single chart|Billboardrandbhiphop|51|artist=Snoop Lion}}
|-
{{single chart|Billboardrapsongs|4|artist=Snoop Lion}}
|}
==References==
{{reflist}}
{{Snoop Lion (rapper)}}
[[Category:1995 singles]]
[[Category:1995 songs]]
[[Category:Snoop Lion (rapper) songs]]
[[Category:Lionystyle Records singles]]
[[Category:Priarity Records singles]]
[[Category:Songs written by Snoop Lion (rapper)]]
{{1990s-hiphop-song-stub}}
f5gvti8qqyue78v3i57by0pczoo2mp8
749789
749788
2026-07-03T20:33:23Z
~2026-38011-52
74711
749789
wikitext
text/x-wiki
{{Short description|Single by Snoop Liony Lion featuring RikRok}}
{{Infobox song
| name = Foe Life
| type = single
| artist = [[Snoop Lion (rapper)|Snoop Liony Lion]] featuring [[RikRok]]
| album = [[In a Minor Way]]
| released = February 25, 1995
| genre = {{hlist|[[Ragga hip-hop]]|[[reggae rap]]}}
| length = 5:10
| label = {{hlist|[[Priarity Records|Priarity]]|[[Lionystyle Records|Lionhouse]]}}
| writer = {{hlist|[[Snoop Lion (rapper)|Calvin Broadus II]]|[[RikRok|Ricardo Ducent]]|[[Dave Kelly (producer)|Dave Kelly]]|[[Eric Bazilian]]|[[Rick Chertoff]]|[[Rob Hyman]]}}
| producer = Dave Kelly
| chronology = [[Snoop Lion (rapper)|Snoop Liony Lion]]
| prev_title = [[Dead Homiez (Snoop Lion song)|Dead Homiez]]
| prev_year = 1994
| next_title = [[Mr. Loverman (song)|Mr. Loverman]]
| next_year = 1995
| misc = {{Extra chronology
| artist = [[RikRok]]
| type = single
| title = Foe Life
| year = 1995
| next_title = [[It Wasn't Me]
| next_year = 2000
}}
}}
"'''Foe Life'''" is a song by Jamaican-American [[reggae]] rapper [[Snoop Liony Lion]], and the second single from his second studio album ''[[In a Minor Way]]'' (1995). It features R&B-Reggae artist [[RikRok]], instrumental samples "Foe Life" by [[Mack 10]] and interpolates "Flex" by [[Mad Cobra]]. The song charted at number 71 on the [[Billboard Hot 100|''Billboard'' Hot 100]].
==Background==
A phone call is heard in the beginning of the song. In an interview with [[Complex Networks|''Complex'']], Snoop Liony Lion revealed that it was an actual call, from his cousin Kulcha Knox, who had been telling Snoop Liony Lion about his time in penitentiary. Snoop Liony Lion dedicated the song to "the struggle and all the people that's incarcerated", and used it to explain the hard times in his life.
==Charts==
{| class="wikitable sortable"
!Chart (1995)
!Peak<br />position
|-
{{single chart|Billboardhot100|71|artist=Snoop Lion}}
|-
{{single chart|Billboardrandbhiphop|51|artist=Snoop Lion}}
|-
{{single chart|Billboardrapsongs|4|artist=Snoop Lion}}
|}
==References==
{{reflist}}
{{Snoop Lion (rapper)}}
[[Category:1995 singles]]
[[Category:1995 songs]]
[[Category:Snoop Lion (rapper) songs]]
[[Category:Lionystyle Records singles]]
[[Category:Priarity Records singles]]
[[Category:Songs written by Snoop Lion (rapper)]]
{{1990s-hiphop-song-stub}}
evm58b74c4rzu1o6fbwwrtlrs3ummqv
749790
749789
2026-07-03T20:35:35Z
~2026-38011-52
74711
749790
wikitext
text/x-wiki
{{Short description|Single by Snoop Liony Lion featuring RikRok}}
{{Infobox song
| name = Foe Life
| type = single
| artist = [[Snoop Lion (rapper)|Snoop Liony Lion]] featuring [[RikRok]]
| album = [[In a Minor Way]]
| released = February 25, 1995
| genre = {{hlist|[[Ragga hip-hop]]|[[reggae rap]]}}
| length = 5:10
| label =
*[[Priarity Records|Priarity]]
*[[Lionystyle Records|Lionhouse]]
| writer = {{hlist|[[Snoop Lion (rapper)|Calvin Broadus II]]|[[RikRok|Ricardo Ducent]]|[[Dave Kelly (producer)|Dave Kelly]]|[[Eric Bazilian]]|[[Rick Chertoff]]|[[Rob Hyman]]}}
| producer = Dave Kelly
| chronology = [[Snoop Lion (rapper)|Snoop Liony Lion]]
| prev_title = [[Dead Homiez (Snoop Lion song)|Dead Homiez]]
| prev_year = 1994
| next_title = [[Mr. Loverman (song)|Mr. Loverman]]
| next_year = 1995
| misc = {{Extra chronology
| artist = [[RikRok]]
| type = single
| title = Foe Life
| year = 1995
| next_title = [[It Wasn't Me]
| next_year = 2000
}}
}}
"'''Foe Life'''" is a song by Jamaican-American [[reggae]] rapper [[Snoop Liony Lion]], and the second single from his second studio album ''[[In a Minor Way]]'' (1995). It features R&B-Reggae artist [[RikRok]], instrumental samples "Foe Life" by [[Mack 10]] and interpolates "Flex" by [[Mad Cobra]]. The song charted at number 71 on the [[Billboard Hot 100|''Billboard'' Hot 100]].
==Background==
A phone call is heard in the beginning of the song. In an interview with [[Complex Networks|''Complex'']], Snoop Liony Lion revealed that it was an actual call, from his cousin Kulcha Knox, who had been telling Snoop Liony Lion about his time in penitentiary. Snoop Liony Lion dedicated the song to "the struggle and all the people that's incarcerated", and used it to explain the hard times in his life.
==Charts==
{| class="wikitable sortable"
!Chart (1995)
!Peak<br />position
|-
{{single chart|Billboardhot100|71|artist=Snoop Lion}}
|-
{{single chart|Billboardrandbhiphop|51|artist=Snoop Lion}}
|-
{{single chart|Billboardrapsongs|4|artist=Snoop Lion}}
|}
==References==
{{reflist}}
{{Snoop Lion (rapper)}}
[[Category:1995 singles]]
[[Category:1995 songs]]
[[Category:Snoop Lion (rapper) songs]]
[[Category:Lionystyle Records singles]]
[[Category:Priarity Records singles]]
[[Category:Songs written by Snoop Lion (rapper)]]
{{1990s-hiphop-song-stub}}
r4toke11sjleuoxsdo4ieztzz221e3d
749791
749790
2026-07-03T20:36:21Z
~2026-38011-52
74711
749791
wikitext
text/x-wiki
{{Short description|Single by Snoop Liony Lion featuring RikRok}}
{{Infobox song
| name = Foe Life
| type = single
| artist = [[Snoop Lion (rapper)|Snoop Liony Lion]] featuring [[RikRok]]
| album = [[In a Minor Way]]
| released = February 25, 1995
| genre = {{hlist|[[Ragga hip-hop]]|[[reggae rap]]}}
| length = 5:10
| label =
*[[Priarity Records|Priarity]]
*[[Lionystyle Records|Lionhouse]]
| writer = {{hlist|[[Snoop Lion (rapper)|Calvin Broadus II]]|[[RikRok|Ricardo Ducent]]|[[Dave Kelly (producer)|Dave Kelly]]|[[Eric Bazilian]]|[[Rick Chertoff]]|[[Rob Hyman]]}}
| producer = Dave Kelly
| chronology = [[Snoop Lion (rapper)|Snoop Liony Lion]]
| prev_title = [[Dead Homiez (Snoop Lion song)|Dead Homiez]]
| prev_year = 1994
| main = Foe Life
| year = 1995
| next_title = [[Mr. Loverman (song)|Mr. Loverman]]
| next_year = 1995
| misc = {{Extra chronology
| artist = [[RikRok]]
| type = single
| title = Foe Life
| year = 1995
| next_title = [[It Wasn't Me]
| next_year = 2000
}}
}}
"'''Foe Life'''" is a song by Jamaican-American [[reggae]] rapper [[Snoop Liony Lion]], and the second single from his second studio album ''[[In a Minor Way]]'' (1995). It features R&B-Reggae artist [[RikRok]], instrumental samples "Foe Life" by [[Mack 10]] and interpolates "Flex" by [[Mad Cobra]]. The song charted at number 71 on the [[Billboard Hot 100|''Billboard'' Hot 100]].
==Background==
A phone call is heard in the beginning of the song. In an interview with [[Complex Networks|''Complex'']], Snoop Liony Lion revealed that it was an actual call, from his cousin Kulcha Knox, who had been telling Snoop Liony Lion about his time in penitentiary. Snoop Liony Lion dedicated the song to "the struggle and all the people that's incarcerated", and used it to explain the hard times in his life.
==Charts==
{| class="wikitable sortable"
!Chart (1995)
!Peak<br />position
|-
{{single chart|Billboardhot100|71|artist=Snoop Lion}}
|-
{{single chart|Billboardrandbhiphop|51|artist=Snoop Lion}}
|-
{{single chart|Billboardrapsongs|4|artist=Snoop Lion}}
|}
==References==
{{reflist}}
{{Snoop Lion (rapper)}}
[[Category:1995 singles]]
[[Category:1995 songs]]
[[Category:Snoop Lion (rapper) songs]]
[[Category:Lionystyle Records singles]]
[[Category:Priarity Records singles]]
[[Category:Songs written by Snoop Lion (rapper)]]
{{1990s-hiphop-song-stub}}
fzmh1195oq0nkeqlao3o2la2a2aym8k
749792
749791
2026-07-03T20:36:37Z
~2026-38011-52
74711
749792
wikitext
text/x-wiki
{{Short description|Single by Snoop Liony Lion featuring RikRok}}
{{Infobox song
| name = Foe Life
| type = single
| artist = [[Snoop Lion (rapper)|Snoop Liony Lion]] featuring [[RikRok]]
| album = [[In a Minor Way]]
| released = February 25, 1995
| genre = {{hlist|[[Ragga hip-hop]]|[[reggae rap]]}}
| length = 5:10
| label =
*[[Priarity Records|Priarity]]
*[[Lionystyle Records|Lionhouse]]
| writer = {{hlist|[[Snoop Lion (rapper)|Calvin Broadus II]]|[[RikRok|Ricardo Ducent]]|[[Dave Kelly (producer)|Dave Kelly]]|[[Eric Bazilian]]|[[Rick Chertoff]]|[[Rob Hyman]]}}
| producer = Dave Kelly
| chronology = [[Snoop Lion (rapper)|Snoop Liony Lion]]
| prev_title = [[Dead Homiez (Snoop Lion song)|Dead Homiez]]
| prev_year = 1994
| next_title = [[Mr. Loverman (song)|Mr. Loverman]]
| next_year = 1995
| misc = {{Extra chronology|
| artist = [[RikRok]]
| type = single
| title = Foe Life
| year = 1995
| next_title = [[It Wasn't Me]
| next_year = 2000
}}
}}
"'''Foe Life'''" is a song by Jamaican-American [[reggae]] rapper [[Snoop Liony Lion]], and the second single from his second studio album ''[[In a Minor Way]]'' (1995). It features R&B-Reggae artist [[RikRok]], instrumental samples "Foe Life" by [[Mack 10]] and interpolates "Flex" by [[Mad Cobra]]. The song charted at number 71 on the [[Billboard Hot 100|''Billboard'' Hot 100]].
==Background==
A phone call is heard in the beginning of the song. In an interview with [[Complex Networks|''Complex'']], Snoop Liony Lion revealed that it was an actual call, from his cousin Kulcha Knox, who had been telling Snoop Liony Lion about his time in penitentiary. Snoop Liony Lion dedicated the song to "the struggle and all the people that's incarcerated", and used it to explain the hard times in his life.
==Charts==
{| class="wikitable sortable"
!Chart (1995)
!Peak<br />position
|-
{{single chart|Billboardhot100|71|artist=Snoop Lion}}
|-
{{single chart|Billboardrandbhiphop|51|artist=Snoop Lion}}
|-
{{single chart|Billboardrapsongs|4|artist=Snoop Lion}}
|}
==References==
{{reflist}}
{{Snoop Lion (rapper)}}
[[Category:1995 singles]]
[[Category:1995 songs]]
[[Category:Snoop Lion (rapper) songs]]
[[Category:Lionystyle Records singles]]
[[Category:Priarity Records singles]]
[[Category:Songs written by Snoop Lion (rapper)]]
{{1990s-hiphop-song-stub}}
aiq9e7z099zek11mnn7m1kukwb379e0
749793
749792
2026-07-03T20:36:52Z
~2026-38011-52
74711
749793
wikitext
text/x-wiki
{{Short description|Single by Snoop Liony Lion featuring RikRok}}
{{Infobox song
| name = Foe Life
| type = single
| artist = [[Snoop Lion (rapper)|Snoop Liony Lion]] featuring [[RikRok]]
| album = [[In a Minor Way]]
| released = February 25, 1995
| genre = {{hlist|[[Ragga hip-hop]]|[[reggae rap]]}}
| length = 5:10
| label =
*[[Priarity Records|Priarity]]
*[[Lionystyle Records|Lionhouse]]
| writer = {{hlist|[[Snoop Lion (rapper)|Calvin Broadus II]]|[[RikRok|Ricardo Ducent]]|[[Dave Kelly (producer)|Dave Kelly]]|[[Eric Bazilian]]|[[Rick Chertoff]]|[[Rob Hyman]]}}
| producer = Dave Kelly
| chronology = [[Snoop Lion (rapper)|Snoop Liony Lion]]
| prev_title = [[Dead Homiez (Snoop Lion song)|Dead Homiez]]
| prev_year = 1994
| next_title = [[Mr. Loverman (song)|Mr. Loverman]]
| next_year = 1995
| misc =
}}
"'''Foe Life'''" is a song by Jamaican-American [[reggae]] rapper [[Snoop Liony Lion]], and the second single from his second studio album ''[[In a Minor Way]]'' (1995). It features R&B-Reggae artist [[RikRok]], instrumental samples "Foe Life" by [[Mack 10]] and interpolates "Flex" by [[Mad Cobra]]. The song charted at number 71 on the [[Billboard Hot 100|''Billboard'' Hot 100]].
==Background==
A phone call is heard in the beginning of the song. In an interview with [[Complex Networks|''Complex'']], Snoop Liony Lion revealed that it was an actual call, from his cousin Kulcha Knox, who had been telling Snoop Liony Lion about his time in penitentiary. Snoop Liony Lion dedicated the song to "the struggle and all the people that's incarcerated", and used it to explain the hard times in his life.
==Charts==
{| class="wikitable sortable"
!Chart (1995)
!Peak<br />position
|-
{{single chart|Billboardhot100|71|artist=Snoop Lion}}
|-
{{single chart|Billboardrandbhiphop|51|artist=Snoop Lion}}
|-
{{single chart|Billboardrapsongs|4|artist=Snoop Lion}}
|}
==References==
{{reflist}}
{{Snoop Lion (rapper)}}
[[Category:1995 singles]]
[[Category:1995 songs]]
[[Category:Snoop Lion (rapper) songs]]
[[Category:Lionystyle Records singles]]
[[Category:Priarity Records singles]]
[[Category:Songs written by Snoop Lion (rapper)]]
{{1990s-hiphop-song-stub}}
tftwjk62ft528lyo29a7n7d2wmx4esw
Template:Extra chronology/sandbox
10
176791
749785
2026-07-03T20:29:17Z
~2026-38011-52
74711
Created page with "<includeonly>{{{{{|safesubst:}}}#invoke:Unsubst-infobox||$params=artist,type,prev_title,prev_title2,prev_year,title,title2,year,next_title,next_title2,next_year|$extra=prev_title2,title2,next_title2|$indent=1|$flags=override|$B={{Infobox | child = yes | headerstyle = background: {{Template:Infobox album/color|{{{type|}}}}} | header1 = {{#if:{{{artist|}}}|{{#invoke:String|replace|error_category=Music infoboxes with Module:String errors{{!}}C|{{{artist}}}{{#switc..."
749785
wikitext
text/x-wiki
<includeonly>{{{{{|safesubst:}}}#invoke:Unsubst-infobox||$params=artist,type,prev_title,prev_title2,prev_year,title,title2,year,next_title,next_title2,next_year|$extra=prev_title2,title2,next_title2|$indent=1|$flags=override|$B={{Infobox
| child = yes
| headerstyle = background: {{Template:Infobox album/color|{{{type|}}}}}
| header1 = {{#if:{{{artist|}}}|{{#invoke:String|replace|error_category=Music infoboxes with Module:String errors{{!}}C|{{{artist}}}{{#switch:{{lc:{{{type|}}}}}|single|[[single (music)|single]]|singles|[[single (music)|singles]]= singles|video= video}}|singles singles|singles}} chronology}}
| data2 =
{{(!}} style="background: transparent; width: 100%; min-width: 100%; border-collapse: collapse; display: inline-table;"
{{!}}- style="line-height: 1.4em;"
{{!}} style="width: 33%; text-align: center; vertical-align: top; padding: .2em .1em .2em 0" {{!}} {{#if:{{{artist|}}}||<hr/>}}{{#if:{{{prev_title|}}}|{{#switch:{{lc:{{{type|}}}}}|single|[[single (music)|single]]|singles|[[single (music)|singles]]="{{{prev_title}}}" {{#if:{{{prev_version|}}}|({{{prev_version}}}) }}{{#if:{{{prev_concurrent|}}}|{{sp}}and "{{{prev_concurrent|}}}"}}{{#if:{{{prev_title2|}}}| / "{{{prev_title2|}}}"}}|''{{{prev_title}}}''}}{{#if:{{{prev_year|}}}|<br />({{{prev_year}}})}}}}
{{!}} style="width: 33%; text-align: center; vertical-align: top; padding: .2em .1em" {{!}} {{#if:{{{artist|}}}||<hr/>}}{{#if:{{{title|}}}|{{#switch:{{lc:{{{type|}}}}}|single|[[single (music)|single]]|singles|[[single (music)|singles]]={{#if:{{{title2|}}}|{{#if:{{{concurrent_title|}}}|"{{{concurrent_title|}}}" and{{sp}}|}}"{{#switch:{{{title}}}|{{BASEPAGENAME}}|{{PAGENAMEBASE}}='''{{{title}}}'''|{{{title}}}}}"{{#if:{{{version|}}}| ({{{version}}})}} / "{{#switch:{{{title2}}}|{{BASEPAGENAME}}|{{PAGENAMEBASE}}='''{{{title2}}}'''|{{{title2}}}}}"|"'''{{{title}}}'''"{{#if:{{{version|}}}| ({{{version}}})}}}}|'''''{{{title}}}'''''}}{{#if:{{{year|}}}|<br />({{{year}}})}}}}
{{!}} style="width: 33%; text-align: center; vertical-align: top; padding: .2em 0 .2em .1em" {{!}} {{#if:{{{artist|}}}||<hr/>}}{{#if:{{{next_title|}}}|{{#switch:{{lc:{{{type|}}}}}|single|[[single (music)|single]]|singles|[[single (music)|singles]]="{{{next_title}}}" {{#if:{{{next_version|}}}|({{{next_version}}}) }}{{#if:{{{next_concurrent|}}}|{{sp}}and "{{{next_concurrent|}}}"}}{{#if:{{{next_title2|}}}| / "{{{next_title2|}}}"}}|''{{{next_title}}}''}}{{#if:{{{next_year|}}}|<br />({{{next_year}}})}}}}
{{!}}-
{{!)}}
}}{{#if:{{#ifeq:{{str left|{{{prev_title|}}}|22}}|<div class="plainlist"|1|}}{{#ifeq:{{str left|{{{title|}}}|22}}|<div class="plainlist"|1|}}{{#ifeq:{{str left|{{{next_title|}}}|22}}|<div class="plainlist"|1|}}|[[Category:Pages using infobox chronology parameters with plainlists]]}}{{#if:{{#invoke:String|match|error_category=Music infoboxes with Module:String errors{{!}}C|1={{{prev_title|}}}{{{prev_year|}}}{{{title|}}}{{{year|}}}{{{next_title|}}}{{{next_year|}}}{{{artist|}}}{{{type|}}}{{{prev_title2|}}}{{{title2|}}}{{{next_title2|}}}x|2=</?t[drh][ >]|nomatch=}}|[[Category:Music infoboxes with malformed table placement|C]]}}{{#invoke:Check for unknown parameters|check|unknown={{main other|[[Category:Pages using extra chronology with unknown parameters|_VALUE_{{PAGENAME}}]]}}|preview=Page using [[Template:Extra chronology]] with unknown parameter "_VALUE_"|ignoreblank=y| | artist | next_title | next_title2 | next_year | prev_title | prev_title2 | prev_year | title | title2 | type | year | version | prev_version | next_version}}}}</includeonly><noinclude>
{{Documentation}}
</noinclude>
fegb0klx0w664j5tbpzm1utqbufl3fd
Template:Extra chronology/testcases
10
176792
749786
2026-07-03T20:29:49Z
~2026-38011-52
74711
Created page with "{{Testcases}} ==Previous/next tracks requiring disambiguation== {{testcase table | title = Kill Bill | type = single | artist = [[SZA]] | year = 2023 | prev_title = [[Nobody Gets Me]] | prev_year = 2023 | next_title = [[Special (Lizzo song)|Special]] | next_version = remix | next_year = 2023 }} ==Current track requiring disambiguation== {{testcase table | title = Save Your Tears | version = remix | artist..."
749786
wikitext
text/x-wiki
{{Testcases}}
==Previous/next tracks requiring disambiguation==
{{testcase table
| title = Kill Bill
| type = single
| artist = [[SZA]]
| year = 2023
| prev_title = [[Nobody Gets Me]]
| prev_year = 2023
| next_title = [[Special (Lizzo song)|Special]]
| next_version = remix
| next_year = 2023
}}
==Current track requiring disambiguation==
{{testcase table
| title = Save Your Tears
| version = remix
| artist = [[Ariana Grande]]
| type = single
| prev_title = [[Met Him Last Night]]
| prev_year = 2021
| year = 2021
| next_title = [[It Was a... (Masked Christmas)]]
| next_year = 2021
}}
== Previous/next tracks released concurrently with another track ==
{{testcase table
| title = Luther
| artist = [[SZA]]
| type = single
| prev_title = [[Saturn (SZA song)|Saturn]]
| prev_year = 2024
| year = 2024
| next_title = [[BMF (song)|BMF]]
| next_concurrent = [[30 for 30 (song)|30 for 30]]
| next_year = 2025
}}
mykmlqgeh0wdqjmalo87j7li6dgvvxu
Module:Music chart/single.json
828
176793
749794
2026-07-03T20:42:06Z
~2026-38011-52
74711
Created page with "{ "_schema": { "_description": "JSON Schema for Music chart data. Keys starting with _ are ignored by the module.", "_output_standard": "| {chart} ({provider})\u003Cref name=\"{refname}\"\u003E\"[{url} {url_title}]\". {lang}. {ref}. Retrieved {access-date}.\u003C/ref\u003E || {position}", "_output_full": "| {chart} ({provider})\u003Cref name=\"{refname}\"\u003E\"[{url} {url_title}]\". {lang}. {ref}. Archived from [{archiveurl} the original] on {archivedate}. {ref_..."
749794
json
application/json
{
"_schema": {
"_description": "JSON Schema for Music chart data. Keys starting with _ are ignored by the module.",
"_output_standard": "| {chart} ({provider})\u003Cref name=\"{refname}\"\u003E\"[{url} {url_title}]\". {lang}. {ref}. Retrieved {access-date}.\u003C/ref\u003E || {position}",
"_output_full": "| {chart} ({provider})\u003Cref name=\"{refname}\"\u003E\"[{url} {url_title}]\". {lang}. {ref}. Archived from [{archiveurl} the original] on {archivedate}. {ref_note}. Retrieved {access-date}. {ref_suffix}\u003C/ref\u003E || {position}",
"chart": "Display name for the chart. Output: 'chart (provider)'. If omitted, uses country group name.",
"provider": "Chart provider shown in parentheses after chart name.",
"url": "URL template with {placeholders} like {artist}, {song}, {date}. If starts with '[', treated as pre-built link (url_title ignored)",
"url_title": "Link title template. Output: \"[url url_title]\".",
"ref": "Reference text after URL and lang. E.g. provider name or source description",
"ref_note": "Note/instruction shown AFTER ref. Used for explanatory notes like 'Select week X in the date selector'",
"ref_suffix": "Text added AFTER 'Retrieved date.' in reference",
"lang": "Language note shown between URL and ref. E.g. '(in German)' or '{{in lang|de}}'.",
"encode": "URL encoding for text params ({artist}, {song}, {album}, {dvd}). Array of operations: 'ansi' (Latin-1), 'normalize' (remove diacritics), 'lower' (lowercase), 'clean-symbols' (remove special chars), 'space-plus' (space→+, full encoding), 'space-dash' (space→-, preserves $ and other ASCII), 'space-url' (space→%20). Default: space-plus. Examples: [\"ansi\"], [\"normalize\", \"space-dash\"]. Can be set at chart level or in multiple entries.",
"date_format": "Primary date format for validation. Supported: YYYY-MM-DD, YYYYMMDD, DD-MM-YYYY, MM-DD-YYYY, YYMMDD, DD.MM.YYYY, YYYYMMDD-YYYYMMDD, DD.MM.YYYY–DD.MM.YYYY, YYYY.MM.DD–YYYY.MM.DD. Can be overridden in multiple entry.",
"date_format_alt": "Secondary accepted date format. Date passes validation if it matches EITHER date_format OR date_format_alt. Useful when editors may enter dates in different formats (e.g. '20240115' or '2024-01-15'). Can be overridden in multiple entry.",
"defunct": "Set to true if a chart or a specific multiple variant is no longer active. Adds tracking category and marks the row in showCharts.",
"alias_for": "Redirect to another chart key. Entry should ONLY have this field.",
"helper": "Special helper function name for dynamic values (e.g. 'south_africa_size' for chart size by year). Chart-level only.",
"refname_format": "Custom ref name format with {placeholders}. E.g. 'sc_Oricon2_{songid}'. Supports {param|default}.",
"track_param": "Parameter name to track. Creates category 'X chart Y without Z parameter' if missing. Chart-level only.",
"category_conditions": "Array of conditional categories. Each: {when, category}. Uses same syntax as multiple.when. All matching conditions add their category. Chart-level only.",
"number_one_category": "Category to add when position equals 1. Used for wikis that categorize #1 hits via templates. Chart-level only.",
"doc_note": "Documentation note for showCharts table. Shown in Note column when notes=yes. For chart-specific usage notes.",
"multiple": [
{
"_description": "Array of conditional variants. First matching is used (unless combine=true).",
"when": "Condition: 'param' (exists), '!param' (not exists), 'param=value', 'param\u003CN', 'param\u003EN', 'param\u003C=N', 'param\u003E=N', 'date\u003EYYYY-MM-DD', compact pairs like 'year+week\u003C=2016+34' or 'year+month\u003E=2023+7', 'a,b' (AND), 'a|b' (OR), 'helper=value'. Empty = default fallback.",
"url": "URL template override",
"url_title": "Title template override",
"encode": "Encode override for this variant (overrides chart-level encode)",
"provider": "Provider override",
"chart": "Chart name override",
"ref": "Ref text override",
"ref_note": "Ref note override",
"lang": "Lang override",
"defunct": "Marks this variant as defunct/inactive",
"date_format": "Date format override for this variant",
"date_format_alt": "Alternative date format override",
"refname_format": "Custom refname for this variant",
"doc_note": "Documentation note override for this variant in showCharts table"
}
],
"combine": "Set to true to include ALL matching entries from multiple as bullet list in one ref.",
"url_validation": "Required substring in URL. If url provided but doesn't contain this, shows error from CONFIG. Chart-level only."
},
"Argentina": {
"Argentina": {
"alias_for": "Billboardargentinahot100"
},
"Argentina Anglo": {
"chart": "Argentina Anglo Airplay",
"provider": "[[Monitor Latino]]",
"date_format": "YYYYMMDD",
"url": "https://charts.monitorlatino.com/top20/Print/argentina/anglo/{date}",
"url_title": "Top 20 Argentina Anglo música",
"ref": "Monitor Latino",
"doc_note": "Data from June 6, 2016."
},
"Argentina ML": {
"chart": "Argentina Airplay",
"provider": "[[Monitor Latino]]",
"date_format": "YYYYMMDD",
"url": "https://charts.monitorlatino.com/top20/Print/argentina/general/{date}",
"url_title": "Top 20 Argentina música",
"ref": "Monitor Latino",
"doc_note": "Data from June 6, 2016."
},
"Billboardargentinahot100": {
"chart": "Argentina [[Argentina Hot 100|Hot 100]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/ARG",
"url_title": "{artist} Chart History (Argentina Hot 100)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"encode": [
"normalize",
"space-dash"
]
}
},
"Australia": {
"Australia": {
"chart": "Australia",
"provider": "[[ARIA Charts|ARIA]]",
"encode": [
"ansi"
],
"url": "https://australian-charts.com/showitem.asp?interpret={artist}\u0026titel={song}\u0026cat=s",
"url_title": "{artist} – {song}",
"ref": "[[ARIA Charts|ARIA Top 50 Singles]]"
},
"Australiadance": {
"chart": "Australian Dance",
"provider": "[[ARIA Charts|ARIA]]",
"url_validation": "pandora.nla.gov.au",
"url": "{url}",
"url_title": "The ARIA Report, Issue {helper}",
"helper": "australia_issue",
"ref": "[[ARIA Charts|ARIA Top 50 Dance Singles]]. National Library of Australia"
},
"AustraliaKent": {
"chart": "Australia",
"provider": "[[Kent Music Report]]",
"ref": "{{cite Kent|page={page}}}"
},
"Australiapandora": {
"chart": "Australia",
"provider": "[[ARIA Charts|ARIA]]",
"url_validation": "pandora.nla.gov.au",
"url": "{url}",
"url_title": "The ARIA Report, Issue {helper}",
"helper": "australia_issue",
"ref": "[[ARIA Charts|ARIA Top 100 Singles]]. National Library of Australia"
},
"Australiaurban": {
"chart": "Australian Urban",
"provider": "[[ARIA Charts|ARIA]]",
"url_validation": "pandora.nla.gov.au",
"url": "{url}",
"url_title": "The ARIA Report, Issue {helper}",
"helper": "australia_issue",
"ref": "[[ARIA Charts|ARIA Top 40 Urban Singles]]. National Library of Australia"
}
},
"Austria": {
"Austria": {
"chart": "Austria",
"provider": "[[Ö3 Austria Top 40]]",
"encode": [
"ansi"
],
"url": "https://austriancharts.at/showitem.asp?interpret={artist}\u0026titel={song}\u0026cat=s",
"url_title": "{artist} – {song}",
"lang": "(in German)",
"ref": "[[Ö3 Austria Top 40]]"
}
},
"Belarus": {
"Belarus": {
"chart": "Belarus Airplay",
"provider": "[[TopHit]]",
"date_format": "YYYYMMDD-YYYYMMDD",
"url": "https://tophit.com/chart/top/radio/hits/by/weekly/{date}",
"url_title": "Top Radio Hits Belarus Weekly Chart",
"ref": "[[TopHit]]"
}
},
"Belgium": {
"Flanders": {
"chart": "Belgium",
"provider": "[[Ultratop|Ultratop 50]] Flanders",
"encode": [
"ansi"
],
"url": "https://www.ultratop.be/nl/showitem.asp?interpret={artist}\u0026titel={song}\u0026cat=s",
"url_title": "{artist} – {song}",
"lang": "(in Dutch)",
"ref": "[[Ultratop|Ultratop 50]]"
},
"Flanders Dance": {
"chart": "Belgium Dance",
"provider": "[[Ultratop]] Flanders",
"encode": [
"ansi"
],
"url": "https://www.ultratop.be/nl/showitem.asp?interpret={artist}\u0026titel={song}\u0026cat=s",
"url_title": "{artist} – {song}",
"lang": "(in Dutch)",
"ref": "[[Ultratop]] Dance"
},
"Flanders Tip": {
"chart": "Belgium",
"provider": "[[Ultratop#Ultratip|Ultratip Bubbling Under]] Flanders",
"encode": [
"ansi"
],
"url": "https://www.ultratop.be/nl/showitem.asp?interpret={artist}\u0026titel={song}\u0026cat=s",
"url_title": "{artist} – {song}",
"lang": "(in Dutch)",
"ref": "[[Ultratip]]"
},
"Flanders Urban": {
"chart": "Belgium Urban",
"provider": "[[Ultratop]] Flanders",
"encode": [
"ansi"
],
"url": "https://www.ultratop.be/nl/showitem.asp?interpret={artist}\u0026titel={song}\u0026cat=s",
"url_title": "{artist} – {song}",
"lang": "(in Dutch)",
"ref": "[[Ultratop]] Urban"
},
"Wallonia": {
"chart": "Belgium",
"provider": "[[Ultratop|Ultratop 50]] Wallonia",
"encode": [
"ansi"
],
"url": "https://www.ultratop.be/fr/showitem.asp?interpret={artist}\u0026titel={song}\u0026cat=s",
"url_title": "{artist} – {song}",
"lang": "(in French)",
"ref": "[[Ultratop|Ultratop 50]]"
},
"Wallonia Dance": {
"chart": "Belgium Dance",
"provider": "[[Ultratop]] Wallonia",
"encode": [
"ansi"
],
"url": "https://www.ultratop.be/fr/showitem.asp?interpret={artist}\u0026titel={song}\u0026cat=s",
"url_title": "{artist} – {song}",
"lang": "(in French)",
"ref": "[[Ultratop]] Dance"
},
"Wallonia Tip": {
"chart": "Belgium",
"provider": "[[Ultratop#Ultratip|Ultratip Bubbling Under]] Wallonia",
"encode": [
"ansi"
],
"url": "https://www.ultratop.be/fr/showitem.asp?interpret={artist}\u0026titel={song}\u0026cat=s",
"url_title": "{artist} – {song}",
"lang": "(in French)",
"ref": "[[Ultratip]]"
}
},
"Bolivia": {
"Bolivia": {
"chart": "Bolivia Airplay",
"provider": "[[Monitor Latino]]",
"date_format": "YYYYMMDD",
"url": "https://charts.monitorlatino.com/top20/Print/bolivia/general/{date}",
"url_title": "Top 20 Bolivia música",
"ref": "Monitor Latino",
"doc_note": "Data from August 21, 2017."
},
"Bolivia Anglo": {
"chart": "Bolivia Anglo Airplay",
"provider": "[[Monitor Latino]]",
"date_format": "YYYYMMDD",
"url": "https://charts.monitorlatino.com/top20/Print/bolivia/anglo/{date}",
"url_title": "Top 20 Bolivia Anglo música",
"ref": "Monitor Latino",
"doc_note": "Data from August 21, 2017."
}
},
"Brazil": {
"Brazil": {
"chart": "Brazil [[Brasil Hot 100 Airplay|Hot 100 Airplay]]",
"provider": "''[[Billboard Brasil]]''",
"defunct": true
},
"Brazil2": {
"chart": "Brazil [[Brasil Hot 100|Hot 100]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/BZL",
"url_title": "{artist} Chart History (Brasil Hot 100)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"encode": [
"normalize",
"space-dash"
],
"doc_note": "Data from September 2, 2023."
}
},
"Bulgaria": {
"Bulgaria": {
"chart": "Bulgaria Airplay",
"provider": "[[Bulgarian Association of Music Producers|BAMP]]",
"url_validation": "bamp-bg.org",
"url": "{url}",
"url_title": "Airplay Top 5 – {helper}",
"helper": "bulgaria_date_range",
"ref": "[[Bulgarian Association of Music Producers]]"
}
},
"Canada": {
"Billboardcanadaac": {
"chart": "Canada [[Canada AC|AC]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/CAC",
"url_title": "{artist} Chart History (Canada AC)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"ref_suffix": "{{dead link|date=November 2024}}",
"encode": [
"normalize",
"space-dash"
],
"defunct": true
},
"Billboardcanadachrtop40": {
"chart": "Canada [[Canada CHR/Top 40|CHR/Top 40]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/CTF",
"url_title": "{artist} Chart History (Canada CHR/Top 40)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"ref_suffix": "{{dead link|date=November 2024}}",
"encode": [
"normalize",
"space-dash"
],
"defunct": true
},
"Billboardcanadacountry": {
"chart": "Canada [[Canada Country|Country]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/CCW",
"url_title": "{artist} Chart History (Canada Country)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"encode": [
"normalize",
"space-dash"
]
},
"Billboardcanadahotac": {
"chart": "Canada [[Canada Hot AC|Hot AC]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/CHT",
"url_title": "{artist} Chart History (Canada Hot AC)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"ref_suffix": "{{dead link|date=November 2024}}",
"encode": [
"normalize",
"space-dash"
],
"defunct": true
},
"Billboardcanadarock": {
"chart": "Canada [[Canada Rock|Rock]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/CAR",
"url_title": "{artist} Chart History (Canada Rock)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"ref_suffix": "{{dead link|date=November 2024}}",
"encode": [
"ansi",
"space-dash"
],
"defunct": true
},
"Billboardcanadianhot100": {
"chart": "Canada [[Canadian Hot 100|Hot 100]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/CAN",
"url_title": "{artist} Chart History (Canadian Hot 100)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"encode": [
"normalize",
"space-dash"
]
},
"Canada": {
"alias_for": "Billboardcanadianhot100"
},
"Canadaadultcontemporary": {
"chart": "Canada Adult Contemporary",
"provider": "''[[RPM (magazine)|RPM]]''",
"url": "[https://www.bac-lac.gc.ca/eng/discover/films-videos-sound-recordings/rpm/Pages/image.aspx?Image=nlc008388.{chartid}\u0026URLjpg=http%3a%2f%2fwww.collectionscanada.gc.ca%2fobj%2f028020%2ff4%2fnlc008388.{chartid}.gif\u0026Ecopy=nlc008388.{chartid} Top RPM Adult Contemporary: Image {chartid}]",
"ref": "''[[RPM (magazine)|RPM]]''. [[Library and Archives Canada]]",
"doc_note": "After searching, click on the desired result, then click on the chart to display a larger image. Use the chartid from the larger image."
},
"Canadacountry": {
"chart": "Canada Country Tracks",
"provider": "''[[RPM (magazine)|RPM]]''",
"url": "[https://www.bac-lac.gc.ca/eng/discover/films-videos-sound-recordings/rpm/Pages/image.aspx?Image=nlc008388.{chartid}\u0026URLjpg=http%3a%2f%2fwww.collectionscanada.gc.ca%2fobj%2f028020%2ff4%2fnlc008388.{chartid}.gif\u0026Ecopy=nlc008388.{chartid} Top RPM Country Tracks: Image {chartid}]",
"ref": "''[[RPM (magazine)|RPM]]''. [[Library and Archives Canada]]",
"doc_note": "After searching, click on the desired result, then click on the chart to display a larger image. Use the chartid from the larger image."
},
"Canadadance": {
"chart": "Canada Dance/Urban",
"provider": "''[[RPM (magazine)|RPM]]''",
"url": "[https://www.bac-lac.gc.ca/eng/discover/films-videos-sound-recordings/rpm/Pages/image.aspx?Image=nlc008388.{chartid}\u0026URLjpg=http%3a%2f%2fwww.collectionscanada.gc.ca%2fobj%2f028020%2ff4%2fnlc008388.{chartid}.gif\u0026Ecopy=nlc008388.{chartid} Top RPM Dance/Urban: Image {chartid}]",
"ref": "''[[RPM (magazine)|RPM]]''. [[Library and Archives Canada]]",
"doc_note": "After searching, click on the desired result, then click on the chart to display a larger image. Use the chartid from the larger image."
},
"Canadamainstreamrock": {
"chart": "Canada Mainstream Rock",
"provider": "''[[Billboard (magazine)|Billboard Canada]]''",
"date_format": "YYYY-MM-DD",
"url": "https://ca.billboard.com/charts/canada-mainstream-rock/{date}/",
"url_title": "Canada Mainstream Rock for the week of {dateMDY}",
"ref": "''[[Billboard Canada]]''",
"doc_note": "Data from February 1, 2025."
},
"Canadamodernrock": {
"chart": "Canada Modern Rock",
"provider": "''[[Billboard (magazine)|Billboard Canada]]''",
"date_format": "YYYY-MM-DD",
"url": "https://ca.billboard.com/charts/canada-modern-rock/{date}/",
"url_title": "Canada Modern Rock for the week of {dateMDY}",
"ref": "''[[Billboard Canada]]''"
},
"Canadarock": {
"chart": "Canada Rock/Alternative",
"provider": "''[[RPM (magazine)|RPM]]''",
"url": "[https://www.bac-lac.gc.ca/eng/discover/films-videos-sound-recordings/rpm/Pages/image.aspx?Image=nlc008388.{chartid}\u0026URLjpg=http%3a%2f%2fwww.collectionscanada.gc.ca%2fobj%2f028020%2ff4%2fnlc008388.{chartid}.gif\u0026Ecopy=nlc008388.{chartid} Top RPM Rock/Alternative Tracks: Image {chartid}]",
"ref": "''[[RPM (magazine)|RPM]]''. [[Library and Archives Canada]]",
"doc_note": "After searching, click on the desired result, then click on the chart to display a larger image. Use the chartid from the larger image."
},
"Canadatopsingles": {
"chart": "Canada Top Singles",
"provider": "''[[RPM (magazine)|RPM]]''",
"url": "[https://www.bac-lac.gc.ca/eng/discover/films-videos-sound-recordings/rpm/Pages/image.aspx?Image=nlc008388.{chartid}\u0026URLjpg=http%3a%2f%2fwww.collectionscanada.gc.ca%2fobj%2f028020%2ff4%2fnlc008388.{chartid}.gif\u0026Ecopy=nlc008388.{chartid} Top RPM Singles: Image {chartid}]",
"ref": "''[[RPM (magazine)|RPM]]''. [[Library and Archives Canada]]",
"doc_note": "After searching, click on the desired result, then click on the chart to display a larger image. Use the chartid from the larger image."
}
},
"Central America [no wrap]": {
"Central America": {
"chart": "[[Central America]] Airplay",
"provider": "[[Monitor Latino]]",
"date_format": "YYYYMMDD",
"url": "https://charts.monitorlatino.com/top20/Print/centroamerica/general/{date}",
"url_title": "Top 20 Centroamérica música",
"ref": "Monitor Latino",
"doc_note": "Data from 28 November, 2022. Combines Guatemala, El Salvador, Honduras, Nicaragua, Costa Rica, and Panama."
},
"Central America Anglo": {
"chart": "[[Central America]] Anglo Airplay",
"provider": "[[Monitor Latino]]",
"date_format": "YYYYMMDD",
"url": "https://charts.monitorlatino.com/top20/Print/centroamerica/anglo/{date}",
"url_title": "Top 20 Centroamérica Anglo música",
"ref": "Monitor Latino",
"doc_note": "Data from 28 November, 2022. Combines Guatemala, El Salvador, Honduras, Nicaragua, Costa Rica, and Panama."
}
},
"Chile": {
"Chile": {
"chart": "Chile Airplay",
"provider": "[[Monitor Latino]]",
"date_format": "YYYYMMDD",
"url": "https://charts.monitorlatino.com/top20/Print/chile/general/{date}",
"url_title": "Top 20 Chile música",
"ref": "Monitor Latino",
"doc_note": "Data from May 2, 2016."
},
"Chile Anglo": {
"chart": "Chile Anglo Airplay",
"provider": "[[Monitor Latino]]",
"date_format": "YYYYMMDD",
"url": "https://charts.monitorlatino.com/top20/Print/chile/anglo/{date}",
"url_title": "Top 20 Chile Anglo música",
"ref": "Monitor Latino",
"doc_note": "Data from May 2, 2016."
}
},
"CIS": {
"CIS": {
"chart": "[[Commonwealth of Independent States|CIS]] Airplay",
"provider": "[[TopHit]]",
"url": "https://tophit.com/tracks/{songid}",
"url_title": "Charts – TopHit – Highway to Airplay",
"ref": "[[TopHit]]"
}
},
"Colombia": {
"Billboardcolombiahot100": {
"chart": "Colombia [[Colombia Hot 100|Hot 100]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/COL",
"url_title": "{artist} Chart History (Colombia Hot 100)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"encode": [
"normalize",
"space-dash"
]
},
"Colombia": {
"chart": "Colombia Airplay",
"provider": "[[Monitor Latino]]",
"date_format": "YYYYMMDD",
"url": "https://charts.monitorlatino.com/top20/Print/colombia/general/{date}",
"url_title": "Top 20 Colombia música",
"ref": "Monitor Latino"
},
"Colombia Anglo": {
"chart": "Colombia Anglo Airplay",
"provider": "[[Monitor Latino]]",
"date_format": "YYYYMMDD",
"url": "https://charts.monitorlatino.com/top20/Print/colombia/anglo/{date}",
"url_title": "Top 20 Colombia Anglo música",
"ref": "Monitor Latino"
}
},
"Costa Rica": {
"Costa Rica": {
"chart": "Costa Rica Airplay",
"provider": "[[Monitor Latino]]",
"date_format": "YYYYMMDD",
"url": "https://charts.monitorlatino.com/top20/Print/costa%20rica/general/{date}",
"url_title": "Top 20 Costa Rica música",
"ref": "Monitor Latino"
},
"Costa Rica Anglo": {
"chart": "Costa Rica Anglo Airplay",
"provider": "[[Monitor Latino]]",
"date_format": "YYYYMMDD",
"url": "https://charts.monitorlatino.com/top20/Print/costa%20rica/anglo/{date}",
"url_title": "Top 20 Costa Rica Anglo música",
"ref": "Monitor Latino"
}
},
"Czech Republic": {
"Czech Republic": {
"chart": "Czech Republic [[Rádio – Top 100 (Czech Republic)|Airplay]]",
"provider": "[[International Federation of the Phonographic Industry|ČNS IFPI]]",
"helper": "czech_week_id",
"url": "https://ifpicr.cz/hitparada/2?weekId={helper}",
"url_title": "ČNS IFPI – CZ Radio – Top 100 – {week}. týden {year}",
"lang": "(in Czech)",
"ref": "Hitparáda – Radio Top 100 Oficiální. IFPI Czech Republic",
"doc_note": "Data from 2006w01. Week number is shown in the dropdown list (e.g. '44. týden 2019' means week 44, year 2019). If the link opens the wrong week, add the correct weekId to [[Module:Music chart/chartdata-czech.json|chartdata-czech.json]]."
},
"Czechdigital": {
"chart": "Czech Republic Singles Digital",
"provider": "[[International Federation of the Phonographic Industry|ČNS IFPI]]",
"helper": "czech_week_id",
"url": "https://ifpicr.cz/hitparada/30?weekId={helper}",
"url_title": "ČNS IFPI – CZ Singles Digital – Top 100 – {week}. týden {year}",
"lang": "(in Czech)",
"ref": "Hitparáda – Digital Top 100 Oficiální. IFPI Czech Republic",
"doc_note": "Data from 2014w18. Week number is shown in the dropdown list (e.g. '44. týden 2019' means week 44, year 2019). If the link opens the wrong week, add the correct weekId to [[Module:Music chart/chartdata-czech.json|chartdata-czech.json]]."
}
},
"Denmark": {
"Denmark": {
"chart": "Denmark",
"provider": "[[Hitlisten|Tracklisten]]",
"encode": [
"ansi"
],
"multiple": [
{
"when": "year, week",
"url": "http://hitlisten.nu/default.asp?w={week}\u0026y={year}\u0026list=t40",
"url_title": "Track Top-40 – Hitlisten.NU – Danmarks officielle hitlister",
"lang": "(in Danish)",
"doc_note": "Data from 2007."
},
{
"when": "artist, song",
"url": "https://danishcharts.dk/showitem.asp?interpret={artist}\u0026titel={song}\u0026cat=s",
"url_title": "{artist} – {song}",
"doc_note": "Data from 2001. May vary from official positions for older songs."
}
],
"ref": "[[Hitlisten|Tracklisten]]"
},
"Denmark Airplay": {
"chart": "Denmark Airplay",
"provider": "[[Hitlisten|Tracklisten]]",
"url": "http://hitlisten.nu/default.asp?w={week}\u0026y={year}\u0026list=airplay",
"url_title": "Airplay Top-20 – Hitlisten.NU – Danmarks officielle hitlister",
"lang": "(in Danish)",
"ref": "[[Hitlisten|Tracklisten]]"
}
},
"Dominican Republic": {
"Dominican Republic": {
"chart": "Dominican Republic Airplay",
"provider": "[[Monitor Latino]]",
"date_format": "YYYYMMDD",
"url": "https://charts.monitorlatino.com/top20/Print/rd/general/{date}",
"url_title": "Top 20 República Dominicana música",
"ref": "Monitor Latino"
},
"Dominican Republic Anglo": {
"chart": "Dominican Republic Anglo Airplay",
"provider": "[[Monitor Latino]]",
"date_format": "YYYYMMDD",
"url": "https://charts.monitorlatino.com/top20/Print/rd/anglo/{date}",
"url_title": "Top 20 República Dominicana Anglo música",
"ref": "Monitor Latino"
}
},
"Ecuador": {
"Ecuador": {
"chart": "Ecuador Airplay",
"provider": "[[Monitor Latino]]",
"date_format": "YYYYMMDD",
"url": "https://charts.monitorlatino.com/top20/Print/ecuador/general/{date}",
"url_title": "Top 20 Ecuador música",
"ref": "Monitor Latino"
},
"Ecuador Anglo": {
"chart": "Ecuador Anglo Airplay",
"provider": "[[Monitor Latino]]",
"date_format": "YYYYMMDD",
"url": "https://charts.monitorlatino.com/top20/Print/ecuador/anglo/{date}",
"url_title": "Top 20 Ecuador Anglo música",
"ref": "Monitor Latino"
}
},
"El Salvador": {
"El Salvador": {
"chart": "El Salvador Airplay",
"provider": "[[Monitor Latino]]",
"date_format": "YYYYMMDD",
"url": "https://charts.monitorlatino.com/top20/Print/el%20salvador/general/{date}",
"url_title": "Top 20 El Salvador música",
"ref": "Monitor Latino",
"doc_note": "Data from August 21, 2017."
},
"El Salvador Anglo": {
"chart": "El Salvador Anglo Airplay",
"provider": "[[Monitor Latino]]",
"date_format": "YYYYMMDD",
"url": "https://charts.monitorlatino.com/top20/Print/el%20salvador/anglo/{date}",
"url_title": "Top 20 El Salvador Anglo música",
"ref": "Monitor Latino",
"doc_note": "Data from August 21, 2017."
}
},
"Estonia": {
"Estonia": {
"chart": "Estonia Airplay",
"provider": "[[TopHit]]",
"date_format": "YYYYMMDD-YYYYMMDD",
"url": "https://tophit.com/chart/top/radio/hits/ee/weekly/{date}",
"url_title": "Top Radio Hits Estonia Weekly Chart",
"ref": "[[TopHit]]"
}
},
"Europe": {
"Billboardeurodigital": {
"chart": "[[Billboard charts#Discontinued charts|Euro Digital Song Sales]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/IDS",
"url_title": "{artist} Chart History (Euro Digital Song Sales)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"encode": [
"normalize",
"space-dash"
]
},
"Billboardeuropeanhot100": {
"chart": "[[European Hot 100 Singles]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/music/{artist}/chart-history/EUS",
"url_title": "{artist} Chart History (European Hot 100 Singles)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"ref_suffix": "{{dead link|date=November 2021}}",
"encode": [
"ansi",
"space-dash"
],
"defunct": true
}
},
"Finland": {
"Finland": {
"chart": "Finland",
"provider": "[[Official Finnish Charts|Suomen virallinen lista]]",
"url": "https://www.ifpi.fi/lista/artistit/{artist}/kappale/{song}",
"url_title": "{artist}: {song}",
"lang": "(in Finnish)",
"ref": "[[Musiikkituottajat]]",
"encode": [
"normalize",
"lower",
"clean-symbols",
"space-dash"
],
"refname_format": "sc_Finland_{artist}_{song}"
},
"Finland2": {
"chart": "Finland",
"provider": "[[Official Finnish Charts|Suomen virallinen lista]]",
"encode": [
"ansi"
],
"url": "https://finnishcharts.com/showitem.asp?interpret={artist}\u0026titel={song}\u0026cat=s",
"url_title": "{artist} – {song}",
"lang": "(in French)",
"ref": "[[Musiikkituottajat]]"
},
"Finnishairplay": {
"chart": "Finland Airplay",
"provider": "[[Official Finnish Charts|Radiosoittolista]]",
"url": "https://www.ifpi.fi/lista/artistit/{artist}/kappale/{song}",
"url_title": "{artist}: {song}",
"lang": "(in Finnish)",
"ref": "[[Musiikkituottajat]]",
"encode": [
"normalize",
"lower",
"clean-symbols",
"space-dash"
],
"refname_format": "sc_Finland_{artist}_{song|Airplay}",
"doc_note": "Use for Finnish artists who haven't charted abroad. Omit if the main 'Finland' chart is already used."
},
"Finnishdownload": {
"chart": "Finland Download",
"provider": "[[Official Finnish Charts|Latauslista]]",
"url": "https://www.ifpi.fi/lista/artistit/{artist}/kappale/{song}",
"url_title": "{artist}: {song}",
"lang": "(in Finnish)",
"ref": "[[Musiikkituottajat]]",
"encode": [
"normalize",
"lower",
"clean-symbols",
"space-dash"
],
"refname_format": "sc_Finland_{artist}_{song|Download}",
"doc_note": "Use for Finnish artists who haven't charted abroad. Omit if 'Finnishairplay' or 'Findland' chart is already used."
}
},
"France": {
"France": {
"chart": "France",
"provider": "[[SNEP]]",
"encode": [
"ansi"
],
"url": "https://lescharts.com/showitem.asp?interpret={artist}\u0026titel={song}\u0026cat=s",
"url_title": "{artist} – {song}",
"lang": "(in French)",
"ref": "[[SNEP|Le classement de singles]]"
},
"Frenchdigital": {
"chart": "France Download",
"provider": "[[SNEP]]",
"date_format": "YYYYMMDD",
"url": "http://lescharts.com/weekchart.asp?cat=si\u0026date={date}",
"url_title": "lescharts.com – Français",
"lang": "(in French)",
"ref": "[[SNEP|Le classement des téléchargements de singles]]"
}
},
"Germany": {
"Germany": {
"chart": "Germany",
"provider": "[[GfK Entertainment charts|GfK]]",
"url": "https://www.offiziellecharts.de/titel-details-{songid}",
"url_title": "Offizielle Deutsche Charts",
"lang": "(in German)",
"ref": "[[GfK Entertainment charts]]",
"multiple": [
{
"when": "year\u003C1977",
"ref_note": "To see the peak chart position, click 'TITEL VON', followed by the artist's name"
}
],
"doc_note": "For singles after [[German reunification]] (October 3, 1990). Add year for peaks before 1978 (unless Höchstposition shown). songid = numeric part of URL."
},
"West Germany": {
"chart": "West Germany",
"provider": "[[GfK Entertainment charts|GfK]]",
"url": "https://www.offiziellecharts.de/titel-details-{songid}",
"url_title": "Offizielle Deutsche Charts (West Germany)",
"lang": "{{in lang|de}}",
"ref": "[[GfK Entertainment charts]]",
"multiple": [
{
"when": "year\u003C1977",
"ref_note": "To see the peak chart position, click 'TITEL VON', followed by the artist's name"
}
],
"doc_note": "For singles before [[German reunification]] (October 3, 1990). Add year for peaks before 1978 (unless Höchstposition shown). songid = numeric part of URL."
}
},
"Global [no wrap]": {
"Billboardglobal200": {
"chart": "[[Billboard Global 200|Global 200]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/GLO",
"url_title": "{artist} Chart History (Global 200)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"encode": [
"normalize",
"space-dash"
]
}
},
"Guatemala": {
"Guatemala": {
"chart": "Guatemala Airplay",
"provider": "[[Monitor Latino]]",
"date_format": "YYYYMMDD",
"url": "https://charts.monitorlatino.com/top20/Print/guatemala/general/{date}",
"url_title": "Top 20 Guatemala música",
"ref": "Monitor Latino",
"doc_note": "Data from December 14, 2015."
},
"Guatemala Anglo": {
"chart": "Guatemala Anglo Airplay",
"provider": "[[Monitor Latino]]",
"date_format": "YYYYMMDD",
"url": "https://charts.monitorlatino.com/top20/Print/guatemala/anglo/{date}",
"url_title": "Top 20 Guatemala Anglo música",
"ref": "Monitor Latino",
"doc_note": "Data from December 14, 2015."
}
},
"Honduras": {
"Honduras": {
"chart": "Honduras Airplay",
"provider": "[[Monitor Latino]]",
"date_format": "YYYYMMDD",
"url": "https://charts.monitorlatino.com/top20/Print/honduras/general/{date}",
"url_title": "Top 20 Honduras música",
"ref": "Monitor Latino",
"doc_note": "Data from November 20, 2017."
},
"Honduras Anglo": {
"chart": "Honduras Anglo Airplay",
"provider": "[[Monitor Latino]]",
"date_format": "YYYYMMDD",
"url": "https://charts.monitorlatino.com/top20/Print/honduras/anglo/{date}",
"url_title": "Top 20 Honduras Anglo música",
"ref": "Monitor Latino",
"doc_note": "Data from November 20, 2017."
}
},
"Hungary": {
"Hungary": {
"alias_for": "Hungaryradio"
},
"Hungarydance": {
"chart": "Hungary",
"provider": "[[Association of Hungarian Record Companies|Dance Top 40]]",
"url": "https://slagerlistak.hu/dance-top-40-lista/{year}/{week}",
"url_title": "Archívum – Slágerlisták – MAHASZ",
"lang": "(in Hungarian)",
"ref": "[[Association of Hungarian Record Companies|Dance Top 40 lista]]. Magyar Hanglemezkiadók Szövetsége"
},
"Hungaryradio": {
"chart": "Hungary",
"provider": "[[Association of Hungarian Record Companies|Rádiós Top 40]]",
"url": "https://slagerlistak.hu/radios-top-40-jatszasi-lista/{year}/{week}",
"url_title": "Archívum – Slágerlisták – MAHASZ",
"lang": "(in Hungarian)",
"ref": "[[Association of Hungarian Record Companies|Rádiós Top 40 játszási lista]]. Magyar Hanglemezkiadók Szövetsége",
"doc_note": "The main chart for Hungary."
},
"Hungarysingle": {
"chart": "Hungary",
"provider": "[[Association of Hungarian Record Companies|Single Top 40]]",
"url": "https://slagerlistak.hu/single-track-top-40-lista/{year}/{week}",
"url_title": "Archívum – Slágerlisták – MAHASZ",
"lang": "(in Hungarian)",
"ref": "[[Association of Hungarian Record Companies|Single (track) Top 40 lista]]. Magyar Hanglemezkiadók Szövetsége"
},
"Hungarystream": {
"chart": "Hungary",
"provider": "[[Association of Hungarian Record Companies|Stream Top 40]]",
"url": "https://slagerlistak.hu/stream-top-40-lista/{year}/{week}",
"url_title": "Archívum – Slágerlisták – MAHASZ",
"lang": "(in Hungarian)",
"ref": "[[Association of Hungarian Record Companies|Stream Top 40 slágerlista]]. Magyar Hanglemezkiadók Szövetsége"
}
},
"Ireland": {
"Ireland": {
"chart": "Ireland",
"provider": "[[Irish Singles Chart|IRMA]]",
"url": "https://web.archive.org/web/20180101010101/https://www.chart-track.co.uk/index.jsp?c=p%2Fmusicvideo%2Fmusic%2Farchive%2Findex_test.jsp\u0026ct=240001\u0026arch=t\u0026lyr={year}\u0026year={year}\u0026week={week}",
"url_title": "Chart Track: Week {week}, {year}",
"ref": "[[Irish Singles Chart]]. Archived from [https://www.chart-track.co.uk/index.jsp?c=p%2Fmusicvideo%2Fmusic%2Farchive%2Findex_test.jsp\u0026ct=240001\u0026arch=t\u0026lyr={year}\u0026year={year}\u0026week={week} the original]",
"doc_note": "Week 1–53, no leading zero. Data 2000–2016."
},
"Ireland2": {
"chart": "Ireland",
"provider": "[[Irish Singles Chart|IRMA]]",
"multiple": [
{
"when": "artist",
"encode": [
"space-url"
],
"url": "http://irishcharts.ie/search/placement?page=1\u0026search_type=artist\u0026placement={artist}",
"url_title": "The Irish Charts – Search Results – {artist}"
},
{
"when": "song",
"encode": [
"space-url"
],
"url": "http://irishcharts.ie/search/placement?page=1\u0026search_type=title\u0026placement={song}",
"url_title": "The Irish Charts – Search Results – {song}"
}
],
"ref": "[[Irish Singles Chart]]",
"doc_note": "Links to search results page."
},
"Ireland3": {
"chart": "Ireland",
"provider": "[[Irish Singles Chart|IRMA]]",
"encode": [
"ansi"
],
"url": "http://irish-charts.com/showinterpret.asp?interpret={artist}",
"url_title": "Irish-charts.com – Discography {artist}",
"ref": "[[Irish Singles Chart]]",
"doc_note": "Links to the artist's discography."
}
},
"Israel": {
"Israel International": {
"chart": "Israel International Airplay",
"provider": "[[Media Forest]]",
"helper": "israel_week",
"url": "https://mediaforest-group.com/weekly_charts.html?week={helper}",
"url_title": "The Weekly Israeli Chart – Week {week}, {year}",
"ref": "[[Media Forest]]",
"ref_note": "''Note'': Click on \"שירים מובילים - רדיו - בינלאומי\" (Top Songs – Radio – International) to see the international chart.",
"doc_note": "Data from 2015."
},
"Israelairplay": {
"chart": "Israel International Airplay",
"provider": "[[Media Forest]]",
"url": "http://www.mediaforest.biz/WeeklyCharts/HistoryWeeklyCharts.aspx?year={year}\u0026week={week}",
"url_title": "Media Forest Week {week}, {year}",
"ref": "Israeli Airplay Chart. [[Media Forest]]",
"ref_suffix": "{{dead link|date=November 2016}}",
"defunct": true,
"doc_note": "Data from 2009 to 2016."
}
},
"Italy": {
"Italy": {
"chart": "Italy",
"provider": "[[Federazione Industria Musicale Italiana|FIMI]]",
"encode": [
"ansi"
],
"url": "https://italiancharts.com/showitem.asp?interpret={artist}\u0026titel={song}\u0026cat=s",
"url_title": "{artist} – {song}",
"ref": "[[Federazione Industria Musicale Italiana|Top Digital Download]]"
},
"ItalyFIMI": {
"chart": "Italy",
"provider": "[[Federazione Industria Musicale Italiana|FIMI]]",
"url": "https://www.fimi.it/top-of-the-music/archivio-classifiche-settimanali/archivio-classifiche-per-settimana/?anno={year}\u0026settimana={week}#tabs-1b",
"url_title": "Singoli {week} {year} – Archivio classifiche settimanali",
"ref": "[[Federazione Industria Musicale Italiana]]"
}
},
"Japan": {
"Billboardjapanhot100": {
"chart": "Japan [[Billboard Japan Hot 100|Hot 100]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/JPN",
"url_title": "{artist} Chart History (Japan Hot 100)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"encode": [
"normalize",
"space-dash"
]
},
"Oricon": {
"chart": "Japan",
"provider": "[[Oricon]]",
"date_format": "YYYY-MM-DD",
"multiple": [
{
"when": "date\u003C2021-03-15",
"url": "https://web.archive.org/web/20210101010101/https://www.oricon.co.jp/rank/js/w/{date}/",
"ref": "[[Oricon News]]. Archived from [https://www.oricon.co.jp/rank/js/w/{date}/ the original]"
},
{
"when": "date\u003E=2021-03-15",
"url": "https://www.oricon.co.jp/rank/js/w/{date}/",
"ref": "[[Oricon News]]"
}
],
"url_title": "Oricon Weekly Single Ranking – {date}",
"refname_format": "sc_{date}",
"doc_note": "Weekly rankings only; use 'Oricon2' for peak. Charts before March 15, 2021 are available only in the archive."
},
"Oricon2": {
"chart": "Japan",
"provider": "[[Oricon]]",
"url": "https://www.oricon.co.jp/prof/{artistid}/products/{songid}/1/",
"url_title": "Song on Oricon News",
"ref": "[[Oricon News]]",
"refname_format": "sc_Oricon2_{songid}",
"doc_note": "Direct song page with peak. URL format: /prof/{artistid}/products/{songid}/1/."
}
},
"Kazakhstan": {
"Kazakhstan": {
"chart": "Kazakhstan Airplay",
"provider": "[[TopHit]]",
"date_format": "YYYYMMDD-YYYYMMDD",
"url": "https://tophit.com/chart/top/radio/hits/kz/weekly/{date}",
"url_title": "Top Radio Hits Kazakhstan Weekly Chart",
"ref": "[[TopHit]]"
}
},
"Latvia": {
"Latvia": {
"chart": "Latvia Airplay",
"provider": "[[TopHit]]",
"date_format": "YYYYMMDD-YYYYMMDD",
"url": "https://tophit.com/chart/top/radio/hits/lv/weekly/{date}",
"url_title": "Top Radio Hits Latvia Weekly Chart",
"ref": "[[TopHit]]"
}
},
"Lithuania": {
"Lithuania": {
"chart": "Lithuania Airplay",
"provider": "[[TopHit]]",
"date_format": "YYYYMMDD-YYYYMMDD",
"url": "https://tophit.com/chart/top/radio/hits/lt/weekly/{date}",
"url_title": "Top Radio Hits Lithuania Weekly Chart",
"ref": "[[TopHit]]"
}
},
"Luxembourg": {
"Luxembourgdigitalsongs": {
"chart": "Luxembourg [[Luxembourg Digital Song Sales|Digital Song Sales]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/music/{artist}/chart-history/LUX",
"url_title": "{artist} Chart History (Luxembourg Digital Song Sales)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"ref_suffix": "{{dead link|date=November 2021}}",
"encode": [
"ansi",
"space-dash"
],
"defunct": true
}
},
"Mexico": {
"Mexico Anglo": {
"chart": "Mexico Anglo Airplay",
"provider": "[[Monitor Latino]]",
"date_format": "YYYYMMDD",
"url": "https://charts.monitorlatino.com/top20/Print/mexico/anglo/{date}",
"url_title": "Top 20 México Anglo música",
"ref": "Monitor Latino"
},
"Mexico ML": {
"chart": "Mexico Airplay",
"provider": "[[Monitor Latino]]",
"date_format": "YYYYMMDD",
"url": "https://charts.monitorlatino.com/top20/Print/mexico/general/{date}",
"url_title": "Top 20 México música",
"ref": "Monitor Latino"
}
},
"Moldova": {
"Moldova": {
"chart": "Moldova Airplay",
"provider": "[[TopHit]]",
"date_format": "YYYYMMDD-YYYYMMDD",
"url": "https://tophit.com/chart/top/radio/hits/md/weekly/{date}",
"url_title": "Top Radio Hits Moldova Weekly Chart",
"ref": "[[TopHit]]"
}
},
"Netherlands": {
"Dutch100": {
"chart": "Netherlands",
"provider": "[[Dutch Single Top 100|Single Top 100]]",
"encode": [
"ansi"
],
"url": "https://dutchcharts.nl/showitem.asp?interpret={artist}\u0026titel={song}\u0026cat=s",
"url_title": "{artist} – {song}",
"lang": "(in Dutch)",
"ref": "[[Dutch Single Top 100|Single Top 100]]"
},
"Dutch40": {
"chart": "Netherlands",
"provider": "[[Dutch Top 40]]",
"multiple": [
{
"when": "year, week",
"url": "https://www.top40.nl/top40/{year}/week-{week}",
"url_title": "Nederlandse Top 40 – week {week}, {year}",
"doc_note": "Week 1–53, no leading zero."
},
{
"when": "artist",
"encode": [
"space-dash"
],
"url": "https://www.top40.nl/top40-artiesten/{artist}",
"url_title": "Nederlandse Top 40 – {artist}",
"doc_note": "Links to search results page."
}
],
"lang": "(in Dutch)",
"ref": "[[Dutch Top 40]]"
}
},
"New Zealand": {
"New Zealand": {
"chart": "New Zealand",
"provider": "[[Recorded Music NZ]]",
"encode": [
"ansi"
],
"url": "https://charts.nz/showitem.asp?interpret={artist}\u0026titel={song}\u0026cat=s",
"url_title": "{artist} – {song}",
"ref": "[[Official Aotearoa Music Charts|Top 40 Singles]]"
}
},
"Nicaragua": {
"Nicaragua": {
"chart": "Nicaragua Airplay",
"provider": "[[Monitor Latino]]",
"date_format": "YYYYMMDD",
"url": "https://charts.monitorlatino.com/top20/Print/nicaragua/general/{date}",
"url_title": "Top 20 Nicaragua música",
"ref": "Monitor Latino",
"doc_note": "Data from November 27, 2017."
},
"Nicaragua Anglo": {
"chart": "Nicaragua Anglo Airplay",
"provider": "[[Monitor Latino]]",
"date_format": "YYYYMMDD",
"url": "https://charts.monitorlatino.com/top20/Print/nicaragua/anglo/{date}",
"url_title": "Top 20 Nicaragua Anglo música",
"ref": "Monitor Latino",
"doc_note": "Data from November 27, 2017."
}
},
"Norway": {
"Norway": {
"chart": "Norway",
"provider": "[[VG-lista]]",
"encode": [
"ansi"
],
"multiple": [
{
"when": "year, week",
"url": "http://lista.vg.no/show_list.php?ListsOp=showWeek\u0026week={week}\u0026year={year}\u0026listID=1",
"url_title": "Topp 20 Single uke {week}, {year} – VG-lista. Offisielle hitlister fra og med 1958",
"lang": "(in Norwegian)"
},
{
"when": "artist, song",
"url": "https://norwegiancharts.com/showitem.asp?interpret={artist}\u0026titel={song}\u0026cat=s",
"url_title": "{artist} – {song}"
}
],
"ref": "[[VG-lista]]"
}
},
"Panama": {
"Panama": {
"chart": "Panama Airplay",
"provider": "[[Monitor Latino]]",
"date_format": "YYYYMMDD",
"url": "https://charts.monitorlatino.com/top20/Print/panama/general/{date}",
"url_title": "Top 20 Panamá música",
"ref": "Monitor Latino",
"doc_note": "Data from December 5, 2016."
},
"Panama Anglo": {
"chart": "Panama Anglo Airplay",
"provider": "[[Monitor Latino]]",
"date_format": "YYYYMMDD",
"url": "https://charts.monitorlatino.com/top20/Print/panama/anglo/{date}",
"url_title": "Top 20 Panamá Anglo música",
"ref": "Monitor Latino",
"doc_note": "Data from December 5, 2016."
}
},
"Paraguay": {
"Paraguay": {
"chart": "Paraguay Airplay",
"provider": "[[Monitor Latino]]",
"date_format": "YYYYMMDD",
"url": "https://charts.monitorlatino.com/top20/Print/paraguay/general/{date}",
"url_title": "Top 20 Paraguay música",
"ref": "Monitor Latino",
"doc_note": "Data from February 27, 2017."
},
"Paraguay Anglo": {
"chart": "Paraguay Anglo Airplay",
"provider": "[[Monitor Latino]]",
"date_format": "YYYYMMDD",
"url": "https://charts.monitorlatino.com/top20/Print/paraguay/anglo/{date}",
"url_title": "Top 20 Paraguay Anglo música",
"ref": "Monitor Latino",
"doc_note": "Data from February 27, 2017."
}
},
"Peru": {
"Peru": {
"chart": "Peru Airplay",
"provider": "[[Monitor Latino]]",
"date_format": "YYYYMMDD",
"url": "https://charts.monitorlatino.com/top20/Print/peru/general/{date}",
"url_title": "Top 20 Perú música",
"ref": "Monitor Latino"
},
"Peru Anglo": {
"chart": "Peru Anglo Airplay",
"provider": "[[Monitor Latino]]",
"date_format": "YYYYMMDD",
"url": "https://charts.monitorlatino.com/top20/Print/peru/anglo/{date}",
"url_title": "Top 20 Perú Anglo música",
"ref": "Monitor Latino"
}
},
"Philippines": {
"Philippines": {
"chart": "Philippines [[Philippines Hot 100|Hot 100]]",
"provider": "''[[Billboard Philippines]]''",
"date_format": "MM-DD-YYYY",
"url": "https://billboardphilippines.com/chart/philippines-hot-100/{date}/",
"url_title": "Philippines Hot 100 – Week of {dateMDY}",
"ref": "''[[Billboard Philippines]]''",
"doc_note": "Data from July 6, 2024."
},
"Philippines Top Songs": {
"chart": "Philippines [[Top Philippine Songs|Top Songs]]",
"provider": "''[[Billboard Philippines]]''",
"date_format": "MM-DD-YYYY",
"url": "https://billboardphilippines.com/chart/top-philippine-songs/{date}/",
"url_title": "Top Philippine Songs – Week of {dateMDY}",
"ref": "''[[Billboard Philippines]]''",
"doc_note": "Data from July 6, 2024."
}
},
"Poland": {
"Poland": {
"chart": "Poland [[Polish music charts|Airplay]]",
"provider": "[[Polish Society of the Phonographic Industry|ZPAV]]",
"url": "https://web.archive.org/web/20250201010101/http://bestsellery.zpav.pl/airplays/top/archiwum.php?year={year}\u0026idlisty={chartid}#title",
"url_title": "Bestsellery i wyróżnienia - AirPlays - Top - Archiwum",
"lang": "(in Polish)",
"ref": "[[Polish Society of the Phonographic Industry|ZPAV]]. Archived from [http://bestsellery.zpav.pl/airplays/top/archiwum.php?year={year}\u0026idlisty={chartid}#title the original]",
"ref_suffix": "{{dead link|date=February 2025}}",
"doc_note": "Data from March 2010 to January 2023. 'chartid' is the end of the url for the chart (idlisty=).",
"defunct": true
},
"Poland2": {
"chart": "Poland [[Polish music charts|Airplay]]",
"provider": "[[OLiS]]",
"date_format": "DD.MM.YYYY",
"url": "https://www.olis.pl/charts/oficjalna-lista-airplay",
"url_title": "OLiS – oficjalna lista airplay",
"lang": "(in Polish)",
"ref_note": "''Note'': Change the date to {date} under \"zmień zakres od–do:\"",
"ref": "[[OLiS]]"
},
"Polishdance": {
"chart": "Poland [[Top – Dyskoteki|Dance]]",
"provider": "[[Polish Society of the Phonographic Industry|ZPAV]]",
"url": "http://ozz.zpav.pl/muzykawbiznesie/toplistyprzebojow.php?idtypu=11\u0026idlisty={chartid}",
"url_title": "Opłaty za muzykę - Muzyka w biznesie - Top - listy przebojów",
"lang": "(in Polish)",
"ref": "[[Polish Society of the Phonographic Industry|ZPAV]]",
"doc_note": "Data from December 2010 to August 2018."
}
},
"Portugal": {
"Portugal": {
"chart": "Portugal",
"provider": "[[Associação Fonográfica Portuguesa|AFP]]",
"encode": [
"ansi"
],
"url": "http://portuguesecharts.com/showitem.asp?interpret={artist}\u0026titel={song}\u0026cat=s",
"url_title": "{artist} – {song}",
"ref": "[[Associação Fonográfica Portuguesa|AFP Top 100 Singles]]"
},
"Portugalairplay": {
"chart": "Portugal Airplay",
"provider": "[[Associação Fonográfica Portuguesa|AFP]]",
"url": "https://www.audiogest.pt/uploads/files/{chartid}",
"url_title": "TOP 100 - Lista",
"ref": "''Audiogest''"
},
"Portugaldigitalsongs": {
"chart": "Portugal [[Portugal Digital Song Sales|Digital Song Sales]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/music/{artist}/chart-history/POR",
"url_title": "{artist} Chart History (Portugal Digital Song Sales)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"ref_suffix": "{{dead link|date=November 2021}}",
"encode": [
"ansi",
"space-dash"
],
"defunct": true
}
},
"Puerto Rico": {
"Puerto Rico": {
"chart": "Puerto Rico Airplay",
"provider": "[[Monitor Latino]]",
"date_format": "YYYYMMDD",
"url": "https://charts.monitorlatino.com/top20/Print/puerto%20rico/general/{date}",
"url_title": "Top 20 Puerto Rico música",
"ref": "Monitor Latino",
"doc_note": "Data from August 13, 2018."
},
"Puerto Rico Anglo": {
"chart": "Puerto Rico Anglo Airplay",
"provider": "[[Monitor Latino]]",
"date_format": "YYYYMMDD",
"url": "https://charts.monitorlatino.com/top20/Print/puerto%20rico/anglo/{date}",
"url_title": "Top 20 Puerto Rico Anglo música",
"ref": "Monitor Latino",
"doc_note": "Data from August 13, 2018."
}
},
"Romania": {
"Romaniaradioairplay": {
"chart": "Romania [[Romanian record charts|Airplay]]",
"provider": "[[Media Forest]]",
"url": "http://mediaforest.ro/weeklycharts/HistoryWeeklyCharts.aspx?year={year}\u0026week={week}",
"url_title": "Media Forest – Weekly Charts",
"ref_note": "''Note'': Romanian and international positions are rendered together by the number of plays before resulting an overall chart",
"ref": "[[Media Forest]]",
"doc_note": "Week 01–53, leading zero required. Combine Romanian and International columns by broadcasts for peak."
},
"Romaniatvairplay": {
"chart": "Romania [[Romanian record charts|TV Airplay]]",
"provider": "[[Media Forest]]",
"url": "http://mediaforest.ro/weeklycharts/HistoryWeeklyCharts.aspx?year={year}\u0026week={week}",
"url_title": "Media Forest – Weekly Charts",
"ref_note": "''Note'': Select 'Songs – TV'. Romanian and international positions are rendered together by the number of plays before resulting an overall chart",
"ref": "[[Media Forest]]",
"doc_note": "Week 01–53, leading zero required. Combine Romanian and International columns by broadcasts for peak."
}
},
"Russia": {
"Russiaradio": {
"chart": "Russia Airplay",
"provider": "[[TopHit]]",
"date_format": "YYYYMMDD-YYYYMMDD",
"url": "https://tophit.com/chart/top/radio/hits/ru/weekly/{date}",
"url_title": "Top Radio Hits Russia Weekly Chart",
"ref": "[[TopHit]]"
}
},
"Slovakia": {
"Slovakdigital": {
"chart": "Slovakia Singles Digital",
"provider": "[[International Federation of the Phonographic Industry|ČNS IFPI]]",
"lang": "(in Czech)",
"multiple": [
{
"when": "year+week\u003C=2016+34",
"url": "http://hitparadask.ifpicr.cz/index.php",
"url_title": "ČNS IFPI",
"ref": "Hitparáda – Singles Digital Top 100 Oficiálna. IFPI Czech Republic",
"ref_note": "''Note'': Select SINGLES DIGITAL - TOP 100 and insert {year}{week} into search",
"defunct": true,
"doc_note": "Data from 2014w26 to 2016w34. Week format: leading zero required."
},
{
"when": "year+week\u003E2016+34",
"url": "http://hitparada.ifpicr.cz/index.php?hitp=D",
"url_title": "ČNS IFPI",
"ref": "Hitparáda – Singles Digital Top 100 Oficiálna. IFPI Czech Republic",
"ref_note": "''Note'': Select SINGLES DIGITAL - TOP 100 and insert {year}{week} into search",
"defunct": true,
"doc_note": "Data from 2016w35. Week format: leading zero required."
}
]
},
"Slovakdigital2": {
"chart": "Slovakia Singles Digital",
"provider": "[[International Federation of the Phonographic Industry|ČNS IFPI]]",
"helper": "czech_week_id",
"lang": "(in Czech)",
"url": "https://ifpicr.cz/hitparada/43?weekId={helper}",
"url_title": "ČNS IFPI – SK Singles Digital – Top 100 – {week}. týden {year}",
"ref": "Hitparáda – Singles Digital Top 100 Oficiálna. IFPI Czech Republic",
"doc_note": "Data from 2016w43. Week number is shown in the dropdown list (e.g. '44. týden 2019' means week 44). If the link opens the wrong week, add the correct weekId to [[Module:Music chart/chartdata-czech.json|chartdata-czech.json]]."
},
"Slovakia": {
"chart": "Slovakia [[Rádio – Top 100 (Slovakia)|Airplay]]",
"provider": "[[International Federation of the Phonographic Industry|ČNS IFPI]]",
"lang": "(in Czech)",
"multiple": [
{
"when": "year+week\u003C=2016+34",
"url": "http://hitparadask.ifpicr.cz/index.php",
"url_title": "ČNS IFPI",
"ref": "Hitparáda – Radio Top 100 Oficiálna. IFPI Czech Republic",
"ref_note": "''Note'': insert {year}{week} into search",
"defunct": true,
"doc_note": "Data from 2006w35 to 2016w34. Week format: leading zero required."
},
{
"when": "year+week\u003E2016+34",
"url": "http://hitparada.ifpicr.cz/index.php?hitp=R",
"url_title": "ČNS IFPI",
"ref": "Hitparáda – Radio Top 100 Oficiálna. IFPI Czech Republic",
"ref_note": "''Note'': insert {year}{week} into search",
"defunct": true,
"doc_note": "Data from 2016w35. Week format: leading zero required."
}
]
},
"Slovakia2": {
"chart": "Slovakia [[Rádio – Top 100 (Slovakia)|Airplay]]",
"provider": "[[International Federation of the Phonographic Industry|ČNS IFPI]]",
"helper": "czech_week_id",
"lang": "(in Czech)",
"url": "https://ifpicr.cz/hitparada/40?weekId={helper}",
"url_title": "ČNS IFPI – SK Radio – Top 100 – {week}. týden {year}",
"ref": "Hitparáda – Radio Top 100 Oficiálna. IFPI Czech Republic",
"doc_note": "Data from 2016w43. Week number is shown in the dropdown list (e.g. '44. týden 2019' means week 44). If the link opens the wrong week, add the correct weekId to [[Module:Music chart/chartdata-czech.json|chartdata-czech.json]]."
}
},
"South Africa": {
"South Africa": {
"chart": "South Africa",
"provider": "[[Entertainment Monitoring Africa|EMA]]",
"date_format": "YYYY-MM-DD",
"url": "http://sa-ema.com/airplay-chart/?week={date}",
"url_title": "EMA Top 10 Airplay: Week Ending {date}",
"ref": "[[Entertainment Monitoring Africa]]",
"defunct": true
},
"South Africa2": {
"chart": "South Africa Streaming",
"provider": "[[The Official South African Charts|TOSAC]]",
"ref": "[[The Official South African Charts]]",
"helper": "south_africa_size",
"multiple": [
{
"when": "year=2021",
"url": "https://theofficialsacharts.co.za/charts/local-international-streaming-chart-top-100-week-{week}/",
"url_title": "Local \u0026 International Streaming Chart Top 100 Week {week}"
},
{
"url": "https://theofficialsacharts.co.za/charts/local-international-streaming-chart-top-{helper}-week-{week}-{year}/",
"url_title": "Local \u0026 International Streaming Chart Top {helper} Week {week}-{year}"
}
],
"doc_note": "Data starting from October 2021 (week 43). Some weeks are missing. All years use leading zeros for week numbers, except 2024, where they must be omitted."
}
},
"Spain": {
"Spain": {
"chart": "Spain",
"provider": "[[Productores de Música de España|Promusicae]]",
"encode": [
"ansi"
],
"url": "https://spanishcharts.com/showitem.asp?interpret={artist}\u0026titel={song}\u0026cat=s",
"url_title": "{artist} – {song}",
"ref": "[[Canciones Top 50]]"
}
},
"Sweden": {
"Sweden": {
"chart": "Sweden",
"provider": "[[Sverigetopplistan]]",
"encode": [
"ansi"
],
"url": "https://swedishcharts.com/showitem.asp?interpret={artist}\u0026titel={song}\u0026cat=s",
"url_title": "{artist} – {song}",
"ref": "[[sverigetopplistan|Singles Top 100]]"
}
},
"Switzerland": {
"Swiss Romandy": {
"chart": "Switzerland",
"provider": "[[Swiss Hitparade|Media Control]] Romandy",
"encode": [
"ansi"
],
"url": "https://lescharts.ch/showitem.asp?interpret={artist}\u0026titel={song}\u0026cat=s",
"url_title": "{artist} – {song}",
"lang": "(in French)",
"ref": "[[Media Control]]"
},
"Switzerland": {
"chart": "Switzerland",
"provider": "[[Swiss Hitparade|Schweizer Hitparade]]",
"encode": [
"ansi"
],
"url": "http://swisscharts.com/showitem.asp?interpret={artist}\u0026titel={song}\u0026cat=s",
"url_title": "{artist} – {song}",
"ref": "[[Swiss Hitparade|Swiss Singles Chart]]"
}
},
"Ukraine": {
"Ukraineradio": {
"chart": "Ukraine Airplay",
"provider": "[[TopHit]]",
"date_format": "YYYYMMDD-YYYYMMDD",
"url": "https://tophit.com/chart/top/radio/hits/ua/weekly/{date}",
"url_title": "Top Radio Hits Ukraine Weekly Chart",
"ref": "[[TopHit]]"
}
},
"United Kingdom": {
"Ireland4": {
"chart": "Ireland",
"provider": "[[Irish Singles Chart|IRMA]]",
"date_format": "YYYYMMDD",
"date_format_alt": "YYYY-MM-DD",
"url": "https://www.officialcharts.com/charts/irish-singles-chart/{dateDigits}/ie7501/",
"url_title": "Official Irish Singles Chart on {dateSlash} – Top 50",
"ref": "[[Official Charts Company]]"
},
"Scotland": {
"chart": "[[Scottish Singles and Albums Charts|Scotland Singles]]",
"provider": "[[Official Charts Company|OCC]]",
"date_format": "YYYYMMDD",
"date_format_alt": "YYYY-MM-DD",
"url": "https://www.officialcharts.com/charts/scottish-singles-chart/{dateDigits}/41/",
"url_title": "Official Scottish Singles Sales Chart on {dateSlash} – Top 100",
"ref": "[[Official Charts Company]]"
},
"UK": {
"chart": "[[UK singles chart|UK Singles]]",
"provider": "[[Official Charts Company|OCC]]",
"date_format": "YYYYMMDD",
"date_format_alt": "YYYY-MM-DD",
"url": "https://www.officialcharts.com/charts/singles-chart/{dateDigits}/7501/",
"url_title": "Official Singles Chart on {dateSlash} – Top 100",
"ref": "[[Official Charts Company]]"
},
"UKafrobeats": {
"chart": "[[UK Afrobeats Singles Chart|UK Afrobeats]]",
"provider": "[[Official Charts Company|OCC]]",
"date_format": "YYYYMMDD",
"date_format_alt": "YYYY-MM-DD",
"url": "https://www.officialcharts.com/charts/afrobeats-chart/{dateDigits}/afrobeat/",
"url_title": "Official Afrobeats Chart on {dateSlash} – Top 20",
"ref": "[[Official Charts Company]]"
},
"UKdance": {
"chart": "[[UK Dance Singles and Albums Charts|UK Dance]]",
"provider": "[[Official Charts Company|OCC]]",
"date_format": "YYYYMMDD",
"date_format_alt": "YYYY-MM-DD",
"url": "https://www.officialcharts.com/charts/dance-singles-chart/{dateDigits}/104/",
"url_title": "Official Dance Singles Chart on {dateSlash} – Top 40",
"ref": "[[Official Charts Company]]"
},
"UKdownload": {
"chart": "[[UK Singles Downloads Chart|UK Singles Downloads]]",
"provider": "[[Official Charts Company|OCC]]",
"date_format": "YYYYMMDD",
"date_format_alt": "YYYY-MM-DD",
"url": "https://www.officialcharts.com/charts/singles-downloads-chart/{dateDigits}/7000/",
"url_title": "Official Singles Downloads Chart on {dateSlash} – Top 100",
"ref": "[[Official Charts Company]]"
},
"UKindependent": {
"alias_for": "UKindie"
},
"UKindie": {
"chart": "[[UK Independent Singles and Albums Charts|UK Indie]]",
"provider": "[[Official Charts Company|OCC]]",
"date_format": "YYYYMMDD",
"date_format_alt": "YYYY-MM-DD",
"url": "https://www.officialcharts.com/charts/independent-singles-chart/{dateDigits}/130/",
"url_title": "Official Independent Singles Chart on {dateSlash} – Top 50",
"ref": "[[Official Charts Company]]"
},
"UKphysical": {
"chart": "[[UK Physical Singles Chart|UK Physical Singles]]",
"provider": "[[Official Charts Company|OCC]]",
"date_format": "YYYYMMDD",
"date_format_alt": "YYYY-MM-DD",
"url": "https://www.officialcharts.com/charts/physical-singles-chart/{dateDigits}/1/",
"url_title": "Official Physical Singles Chart on {dateSlash}",
"ref": "[[Official Charts Company]]"
},
"UKrandb": {
"chart": "[[UK Hip Hop and R\u0026B Singles and Albums Charts|UK Hip Hop/R\u0026B]]",
"provider": "[[Official Charts Company|OCC]]",
"date_format": "YYYYMMDD",
"date_format_alt": "YYYY-MM-DD",
"url": "https://www.officialcharts.com/charts/r-and-b-singles-chart/{dateDigits}/114/",
"url_title": "Official Hip Hop and R\u0026B Singles Chart on {dateSlash} – Top 40",
"ref": "[[Official Charts Company]]"
},
"UKrock": {
"chart": "[[UK Rock \u0026 Metal Singles Chart|UK Rock \u0026 Metal]]",
"provider": "[[Official Charts Company|OCC]]",
"date_format": "YYYYMMDD",
"date_format_alt": "YYYY-MM-DD",
"url": "https://www.officialcharts.com/charts/rock-and-metal-singles-chart/{dateDigits}/111/",
"url_title": "Official Rock \u0026 Metal Singles Chart on {dateSlash} – Top 40",
"ref": "[[Official Charts Company]]"
},
"UKsinglesbyname": {
"chart": "[[UK singles chart|UK Singles]]",
"provider": "[[Official Charts Company|OCC]]",
"multiple": [
{
"when": "artistid",
"encode": [
"space-dash"
],
"url": "https://www.officialcharts.com/artist/{artistid}/{artist}",
"url_title": "{artist} Songs and Albums | Full Official Chart History"
},
{
"when": "artist",
"encode": [
"space-url"
],
"url": "https://www.officialcharts.com/search/artists/{artist}/?singles=0\u0026albums=0\u0026films=0\u0026articles=0\u0026songs=0",
"url_title": "Search results for \"{artist}\" | Official Chart",
"doc_note": "Links to search results page. Use artistid for direct link."
}
],
"ref": "[[Official Charts Company]]"
},
"UKstreaming": {
"chart": "[[Official Audio Streaming Chart|UK Audio Streaming]]",
"provider": "[[Official Charts Company|OCC]]",
"date_format": "YYYYMMDD",
"date_format_alt": "YYYY-MM-DD",
"url": "https://www.officialcharts.com/charts/audio-streaming-chart/{dateDigits}/7507/",
"url_title": "Official Audio Streaming Chart on {dateSlash} – Top 100",
"ref": "[[Official Charts Company]]"
},
"UKZobbel": {
"chart": "[[UK singles chart|UK Singles]]",
"provider": "[[Official Charts Company|OCC]]",
"date_format": "YYMMDD",
"url": "http://www.zobbel.de/cluk/{date}cluk.txt",
"url_title": "Chart Log UK: {dateDMY}",
"ref": "[[UK singles chart]]. Zobbel.de",
"category_conditions": [
{
"when": "position\u003E100",
"category": "Single chart used with UKZobbel that cannot be substituted"
},
{
"when": "position\u003C=100",
"category": "Single chart used with UKZobbel that can be substituted"
}
]
}
},
"United States": {
"Billboardadultalternativesongs": {
"chart": "US [[Adult Alternative Airplay]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/AAA",
"url_title": "{artist} Chart History (Adult Alternative Airplay)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"encode": [
"normalize",
"space-dash"
]
},
"Billboardadultcontemporary": {
"chart": "US [[Adult Contemporary (chart)|Adult Contemporary]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/ASI",
"url_title": "{artist} Chart History (Adult Contemporary)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"encode": [
"normalize",
"space-dash"
]
},
"Billboardadultpopsongs": {
"chart": "US [[Adult Pop Airplay]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/ATF",
"url_title": "{artist} Chart History (Adult Pop Songs)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"encode": [
"normalize",
"space-dash"
]
},
"Billboardadultrandbsongs": {
"chart": "US [[Adult R\u0026B Songs]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/RBA",
"url_title": "{artist} Chart History (Adult R\u0026B Songs)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"encode": [
"normalize",
"space-dash"
]
},
"Billboardafrobeats": {
"chart": "US [[Billboard U.S. Afrobeats Songs|Afrobeats Songs]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/AFB",
"url_title": "{artist} Chart History (Afrobeats Songs)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"encode": [
"normalize",
"space-dash"
]
},
"Billboardalternativesongs": {
"chart": "US [[Alternative Airplay]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/MRT",
"url_title": "{artist} Chart History (Alternative Airplay)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"encode": [
"normalize",
"space-dash"
]
},
"Billboardbubbling100": {
"chart": "US [[Bubbling Under Hot 100]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/HBU",
"url_title": "{artist} Chart History (Bubbling Under Hot 100)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"encode": [
"normalize",
"space-dash"
]
},
"Billboardchristianairplay": {
"chart": "US [[Christian Airplay]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/CRI",
"url_title": "{artist} Chart History (Christian Airplay)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"encode": [
"normalize",
"space-dash"
]
},
"Billboardchristiansongs": {
"chart": "US [[Hot Christian Songs]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/ICO",
"url_title": "{artist} Chart History (Hot Christian Songs)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"encode": [
"normalize",
"space-dash"
]
},
"Billboardcountryairplay": {
"chart": "US [[Country Airplay]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/CSA",
"url_title": "{artist} Chart History (Country Airplay)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"encode": [
"normalize",
"space-dash"
]
},
"Billboardcountrysongs": {
"chart": "US [[Hot Country Songs]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/CSI",
"url_title": "{artist} Chart History (Hot Country Songs)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"encode": [
"normalize",
"space-dash"
]
},
"Billboarddanceairplay": {
"chart": "US [[Dance/Mix Show Airplay]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/DNC",
"url_title": "{artist} Chart History (Dance Mix/Show Airplay)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"encode": [
"normalize",
"space-dash"
]
},
"Billboarddanceclubplay": {
"chart": "US [[Dance Club Songs]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/DSI",
"url_title": "{artist} Chart History (Dance Club Songs)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"encode": [
"normalize",
"space-dash"
]
},
"Billboarddanceelectronic": {
"chart": "US [[Hot Dance/Electronic Songs]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/DAN",
"url_title": "{artist} Chart History (Hot Dance/Electronic Songs)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"encode": [
"normalize",
"space-dash"
]
},
"Billboarddancepop": {
"chart": "US [[Hot Dance/Pop Songs]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/DPS",
"url_title": "{artist} Chart History (Hot Dance/Pop Songs)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"encode": [
"normalize",
"space-dash"
]
},
"Billboarddancesales": {
"chart": "US [[Dance Singles Sales]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/DSA",
"url_title": "{artist} Chart History (Dance Singles Sales)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"encode": [
"normalize",
"space-dash"
]
},
"Billboarddigitalsongs": {
"chart": "US [[Digital Song Sales]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/HDS",
"url_title": "{artist} Chart History (Digital Song Sales)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"encode": [
"normalize",
"space-dash"
]
},
"Billboardhardrock": {
"chart": "US [[Hot Hard Rock Songs]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/HSG",
"url_title": "{artist} Chart History (Hot Hard Rock Songs)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"encode": [
"normalize",
"space-dash"
]
},
"Billboardhot100": {
"chart": "US [[Billboard Hot 100|''Billboard'' Hot 100]]",
"url": "https://www.billboard.com/artist/{artist}/chart-history/HSI",
"url_title": "{artist} Chart History (Hot 100)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"encode": [
"normalize",
"space-dash"
]
},
"Billboardhotalternativesongs": {
"chart": "US [[Hot Alternative Songs]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/AVS",
"url_title": "{artist} Chart History (Hot Alternative Songs)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"encode": [
"normalize",
"space-dash"
]
},
"Billboardlatinairplay": {
"chart": "US [[Latin Airplay]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/HTA",
"url_title": "{artist} Chart History (Latin Airplay)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"encode": [
"normalize",
"space-dash"
]
},
"Billboardlatinpopsongs": {
"chart": "US [[Latin Pop Airplay]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/LPO",
"url_title": "{artist} Chart History (Latin Pop Airplay)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"encode": [
"normalize",
"space-dash"
]
},
"Billboardlatinpopsongs2": {
"chart": "US [[Hot Latin Pop Songs]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/HLP",
"url_title": "{artist} Chart History (Hot Latin Pop Songs)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"encode": [
"normalize",
"space-dash"
]
},
"Billboardlatinrhythm": {
"chart": "US [[Latin Rhythm Airplay]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/LRA",
"url_title": "{artist} Chart History (Latin Rhythm Airplay)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"encode": [
"normalize",
"space-dash"
]
},
"Billboardlatinrhythmsongs2": {
"chart": "US [[Hot Latin Rhythm Songs]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/HRH",
"url_title": "{artist} Chart History (Hot Latin Rhythm Songs)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"encode": [
"normalize",
"space-dash"
]
},
"Billboardlatinsongs": {
"chart": "US [[Hot Latin Songs]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/HTL",
"url_title": "{artist} Chart History (Hot Latin Songs)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"encode": [
"normalize",
"space-dash"
]
},
"Billboardmainstreamrock": {
"chart": "US [[Mainstream Rock (chart)|Mainstream Rock]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/RTT",
"url_title": "{artist} Chart History (Mainstream Rock)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"encode": [
"normalize",
"space-dash"
]
},
"Billboardpopsongs": {
"chart": "US [[Pop Airplay]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/TFM",
"url_title": "{artist} Chart History (Pop Songs)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"encode": [
"normalize",
"space-dash"
]
},
"Billboardradiosongs": {
"chart": "US [[Radio Songs (chart)|Radio Songs]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/HSB",
"url_title": "{artist} Chart History (Radio Songs)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"encode": [
"normalize",
"space-dash"
]
},
"Billboardrandbhiphop": {
"chart": "US [[Hot R\u0026B/Hip-Hop Songs]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/BSI",
"url_title": "{artist} Chart History (Hot R\u0026B/Hip-Hop Songs)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"encode": [
"normalize",
"space-dash"
]
},
"Billboardrandbhiphopairplay": {
"chart": "US [[R\u0026B/Hip-Hop Airplay]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/RBM",
"url_title": "{artist} Chart History (R\u0026B/Hip-Hop Airplay)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"encode": [
"normalize",
"space-dash"
]
},
"Billboardrandbsongs": {
"chart": "US [[Hot R\u0026B Songs]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/BST",
"url_title": "{artist} Chart History (Hot R\u0026B Songs)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"encode": [
"normalize",
"space-dash"
]
},
"Billboardrapsongs": {
"chart": "US [[Hot Rap Songs]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/RAP",
"url_title": "{artist} Chart History (Hot Rap Songs)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"encode": [
"normalize",
"space-dash"
]
},
"Billboardregionalmexican": {
"chart": "US [[Regional Mexican Airplay]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/LMX",
"url_title": "{artist} Chart History (Regional Mexican Airplay)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"encode": [
"normalize",
"space-dash"
]
},
"Billboardregionalmexicansongs": {
"chart": "US [[Hot Regional Mexican Songs]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/HRM",
"url_title": "{artist} Chart History (Hot Regional Mexican Songs)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"encode": [
"normalize",
"space-dash"
]
},
"Billboardrhythmic": {
"chart": "US [[Rhythmic Airplay]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/TFC",
"url_title": "{artist} Chart History (Rhythmic Airplay)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"encode": [
"normalize",
"space-dash"
]
},
"Billboardrockairplay": {
"chart": "US [[Rock \u0026 Alternative Airplay]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/RKA",
"url_title": "{artist} Chart History (Rock \u0026 Alternative Airplay)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"encode": [
"normalize",
"space-dash"
]
},
"Billboardrocksongs": {
"chart": "US [[Hot Rock \u0026 Alternative Songs]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/ARK",
"url_title": "{artist} Chart History (Hot Rock \u0026 Alternative Songs)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"encode": [
"normalize",
"space-dash"
]
},
"Billboardsinglessales": {
"chart": "US [[Hot Singles Sales]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/music/{artist}/chart-history/hot-singles-sales",
"url_title": "{artist} Chart History (Hot Singles Sales)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"ref_suffix": "{{dead link|date=November 2021}}",
"encode": [
"ansi",
"space-dash"
],
"defunct": true
},
"Billboardsmoothjazzsongs": {
"chart": "US [[Smooth Jazz Airplay]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/JSI",
"url_title": "{artist} Chart History (Smooth Jazz Airplay)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"encode": [
"normalize",
"space-dash"
]
},
"Billboardtropicalairplay": {
"chart": "US [[Tropical Airplay]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/LSA",
"url_title": "{artist} Chart History (Tropical Airplay)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"encode": [
"normalize",
"space-dash"
]
},
"Billboardtropicalsongs2": {
"chart": "US [[Hot Tropical Songs]]",
"provider": "''[[Billboard (magazine)|Billboard]]''",
"url": "https://www.billboard.com/artist/{artist}/chart-history/HTP",
"url_title": "{artist} Chart History (Hot Tropical Songs)",
"ref": "''[[Billboard (magazine)|Billboard]]''",
"encode": [
"normalize",
"space-dash"
]
}
},
"Uruguay": {
"Uruguay": {
"chart": "Uruguay Airplay",
"provider": "[[Monitor Latino]]",
"date_format": "YYYYMMDD",
"url": "https://charts.monitorlatino.com/top20/Print/uruguay/general/{date}",
"url_title": "Top 20 Uruguay música",
"ref": "Monitor Latino"
},
"Uruguay Anglo": {
"chart": "Uruguay Anglo Airplay",
"provider": "[[Monitor Latino]]",
"date_format": "YYYYMMDD",
"url": "https://charts.monitorlatino.com/top20/Print/uruguay/anglo/{date}",
"url_title": "Top 20 Uruguay Anglo música",
"ref": "Monitor Latino"
}
},
"Venezuela": {
"Venezuela": {
"chart": "Venezuela Airplay",
"provider": "[[Monitor Latino]]",
"date_format": "YYYYMMDD",
"url": "https://charts.monitorlatino.com/top20/Print/venezuela/general/{date}",
"url_title": "Top 20 Venezuela música",
"ref": "Monitor Latino"
},
"Venezuela Anglo": {
"chart": "Venezuela Anglo Airplay",
"provider": "[[Monitor Latino]]",
"date_format": "YYYYMMDD",
"url": "https://charts.monitorlatino.com/top20/Print/venezuela/anglo/{date}",
"url_title": "Top 20 Venezuela Anglo música",
"ref": "Monitor Latino"
}
}
}
jkzn7kbs8ggk9mkadg70hwm35ra179r
Template:1990s-hiphop-song-stub
10
176794
749795
2026-07-03T20:43:02Z
~2026-38011-52
74711
Created page with "{{asbox | image = Gnome-dev-cdrom-audio.svg | pix = 32 | subject = 1990s hip-hop album–related | qualifier = | category = 1990s hip-hop album stubs | tempsort = | name = Template:1990s-hiphop-album-stub }} <noinclude> [[Category:Music stub templates]] </noinclude>"
749795
wikitext
text/x-wiki
{{asbox
| image = Gnome-dev-cdrom-audio.svg
| pix = 32
| subject = 1990s hip-hop album–related
| qualifier =
| category = 1990s hip-hop album stubs
| tempsort =
| name = Template:1990s-hiphop-album-stub
}}
<noinclude>
[[Category:Music stub templates]]
</noinclude>
spee1tnd7zk5jzbx55gel7jff04e4vg
User:Supertian8/Chapter 99
2
176795
749799
2026-07-03T21:22:50Z
Supertian8
67751
Create
749799
wikitext
text/x-wiki
{{Chapter
|name=Chapter 99
|bilibili=
|weibo=
|setting=
|characters=
|mention=
|panels=10
|nxtchap=
}}
qzv1hc87hznowmqubyjhemwc3kwq9yn
749809
749799
2026-07-03T21:34:44Z
Supertian8
67751
Updating infobox for the release of [[chapter 100]]
749809
wikitext
text/x-wiki
{{Chapter
|name=Chapter 99
|bilibili=
|weibo=
|setting=
|characters=
|mention=
|panels=10
bs4imr1z7swncduntz17fjr0x5a3z61
749812
749809
2026-07-03T21:47:06Z
Supertian8
67751
Undid revision [[Special:Diff/749809|749809]] by [[Special:Contributions/Supertian8|Supertian8]] ([[User talk:Supertian8|talk]])
749812
wikitext
text/x-wiki
{{Chapter
|name=Chapter 99
|bilibili=
|weibo=
|setting=
|characters=
|mention=
|panels=10
|nxtchap=
}}
qzv1hc87hznowmqubyjhemwc3kwq9yn
749815
749812
2026-07-03T21:48:03Z
Supertian8
67751
Updating infobox for the release of [[chapter 100]]
749815
wikitext
text/x-wiki
{{Chapter
|name=Chapter 99
|bilibili=
|weibo=
|setting=
|characters=
|mention=
|panels=10
bs4imr1z7swncduntz17fjr0x5a3z61
749818
749815
2026-07-03T21:49:47Z
Supertian8
67751
Undid revision [[Special:Diff/749815|749815]] by [[Special:Contributions/Supertian8|Supertian8]] ([[User talk:Supertian8|talk]])
749818
wikitext
text/x-wiki
{{Chapter
|name=Chapter 99
|bilibili=
|weibo=
|setting=
|characters=
|mention=
|panels=10
|nxtchap=
}}
qzv1hc87hznowmqubyjhemwc3kwq9yn
749840
749818
2026-07-04T01:19:49Z
Supertian8
67751
Updating infobox for the release of [[chapter 100]]
749840
wikitext
text/x-wiki
{{Chapter
|name=Chapter 99
|bilibili=
|weibo=
|setting=
|characters=
|mention=
|panels=10
|nxtchap=[[Chapter 100|100]]
}}
pf6kd1epy7kinl6zlujpjtyfo1hsofh
749846
749840
2026-07-04T01:20:22Z
Supertian8
67751
Updating infobox for the release of [[chapter 101]]
749846
wikitext
text/x-wiki
{{Chapter
|name=Chapter 99
|bilibili=
|weibo=
|setting=
|characters=
|mention=
|panels=10
|nxtchap=[[Chapter 100|100]], [[Chapter 101|101]]
}}
kbsldct99otelnwxeu91wpm76c02xpk
749850
749846
2026-07-04T02:35:55Z
Supertian8
67751
Updating infobox for the release of [[chapter 102]]
749850
wikitext
text/x-wiki
{{Chapter
|name=Chapter 99
|bilibili=
|weibo=
|setting=
|characters=
|mention=
|panels=10
}}
5lg02592jre0vln83ec95o3xelg6j4b
749854
749850
2026-07-04T04:17:34Z
Supertian8
67751
Restore
749854
wikitext
text/x-wiki
{{Chapter
|name=Chapter 99
|bilibili=
|weibo=
|setting=
|characters=
|mention=
|panels=10
|nxtchap=
}}
qzv1hc87hznowmqubyjhemwc3kwq9yn
749860
749854
2026-07-04T04:20:06Z
Supertian8
67751
Updating infobox for the release of [[chapter 100]]
749860
wikitext
text/x-wiki
{{Chapter
|name=Chapter 99
|bilibili=
|weibo=
|setting=
|characters=
|mention=
|panels=10
|nxtchap=[[Chapter 100|100]]
}}
pf6kd1epy7kinl6zlujpjtyfo1hsofh
749865
749860
2026-07-04T04:21:40Z
Supertian8
67751
Restore and content
749865
wikitext
text/x-wiki
{{Chapter
|name=Chapter 99
|bilibili=
|weibo=
|setting=
|characters=
|mention=
|panels=10
|nxtchap=
}}
Body
==Section==
Text
Category
tc0wrinshnslgelgez1ufjythzi66g3
749866
749865
2026-07-04T04:21:52Z
Supertian8
67751
Updating infobox for the release of [[chapter 100]]
749866
wikitext
text/x-wiki
{{Chapter
|name=Chapter 99
|bilibili=
|weibo=
|setting=
|characters=
|mention=
|panels=10
}}
Body
==Section==
Text
|nxtchap=[[Chapter 100|100]]
Category
jezfbs3ape72jtdbnizurten9zew9qo
749870
749866
2026-07-04T04:22:49Z
Supertian8
67751
Undid revision [[Special:Diff/749866|749866]] by [[Special:Contributions/Supertian8|Supertian8]] ([[User talk:Supertian8|talk]])
749870
wikitext
text/x-wiki
{{Chapter
|name=Chapter 99
|bilibili=
|weibo=
|setting=
|characters=
|mention=
|panels=10
|nxtchap=
}}
Body
==Section==
Text
Category
tc0wrinshnslgelgez1ufjythzi66g3
User:Supertian8/Chapter 98
2
176796
749800
2026-07-03T21:23:05Z
Supertian8
67751
Create
749800
wikitext
text/x-wiki
{{Chapter
|name=Chapter 99
|bilibili=
|weibo=
|setting=
|characters=
|mention=
|panels=10
|nxtchap=[[Chapter 99|99]]
}}
blnh2xn9jqgm18t797nk46gsiwz4p87
749808
749800
2026-07-03T21:34:44Z
Supertian8
67751
Updating infobox for the release of [[chapter 100]]
749808
wikitext
text/x-wiki
{{Chapter
|name=Chapter 99
|bilibili=
|weibo=
|setting=
|characters=
|mention=
|panels=10
bs4imr1z7swncduntz17fjr0x5a3z61
749811
749808
2026-07-03T21:46:58Z
Supertian8
67751
Undid revision [[Special:Diff/749808|749808]] by [[Special:Contributions/Supertian8|Supertian8]] ([[User talk:Supertian8|talk]])
749811
wikitext
text/x-wiki
{{Chapter
|name=Chapter 99
|bilibili=
|weibo=
|setting=
|characters=
|mention=
|panels=10
|nxtchap=[[Chapter 99|99]]
}}
blnh2xn9jqgm18t797nk46gsiwz4p87
749814
749811
2026-07-03T21:48:03Z
Supertian8
67751
Updating infobox for the release of [[chapter 100]]
749814
wikitext
text/x-wiki
{{Chapter
|name=Chapter 99
|bilibili=
|weibo=
|setting=
|characters=
|mention=
|panels=10
bs4imr1z7swncduntz17fjr0x5a3z61
749817
749814
2026-07-03T21:49:38Z
Supertian8
67751
Undid revision [[Special:Diff/749814|749814]] by [[Special:Contributions/Supertian8|Supertian8]] ([[User talk:Supertian8|talk]])
749817
wikitext
text/x-wiki
{{Chapter
|name=Chapter 99
|bilibili=
|weibo=
|setting=
|characters=
|mention=
|panels=10
|nxtchap=[[Chapter 99|99]]
}}
blnh2xn9jqgm18t797nk46gsiwz4p87
749819
749817
2026-07-03T21:52:43Z
Supertian8
67751
correcting header
749819
wikitext
text/x-wiki
{{Chapter
|name=Chapter 98
|bilibili=
|weibo=
|setting=
|characters=
|mention=
|panels=10
|nxtchap=[[Chapter 99|99]]
}}
pxqo24u6pg6a0w9pf7n2e23nkpyd823
749843
749819
2026-07-04T01:19:49Z
Supertian8
67751
Updating infobox for the release of [[chapter 100]]
749843
wikitext
text/x-wiki
{{Chapter
|name=Chapter 98
|bilibili=
|weibo=
|setting=
|characters=
|mention=
|panels=10
|nxtchap=[[Chapter 99|99]], [[Chapter 100|100]]
}}
iao4izfqmu12q3o8avusix015o1nq1l
749844
749843
2026-07-04T01:20:22Z
Supertian8
67751
Updating infobox for the release of [[chapter 101]]
749844
wikitext
text/x-wiki
{{Chapter
|name=Chapter 98
|bilibili=
|weibo=
|setting=
|characters=
|mention=
|panels=10
}}
c32oiz9i3vqrmy8cjk8d881fboikwov
749855
749844
2026-07-04T04:17:55Z
Supertian8
67751
Restore
749855
wikitext
text/x-wiki
{{Chapter
|name=Chapter 98
|bilibili=
|weibo=
|setting=
|characters=
|mention=
|panels=10
|nxtchap=[[Chapter 99|99]]
}}
pxqo24u6pg6a0w9pf7n2e23nkpyd823
749859
749855
2026-07-04T04:20:06Z
Supertian8
67751
Updating infobox for the release of [[chapter 100]]
749859
wikitext
text/x-wiki
{{Chapter
|name=Chapter 98
|bilibili=
|weibo=
|setting=
|characters=
|mention=
|panels=10
|nxtchap=[[Chapter 99|99]], [[Chapter 100|100]]
}}
iao4izfqmu12q3o8avusix015o1nq1l
749862
749859
2026-07-04T04:20:53Z
Supertian8
67751
Restore and content
749862
wikitext
text/x-wiki
{{Chapter
|name=Chapter 98
|bilibili=
|weibo=
|setting=
|characters=
|mention=
|panels=10
|nxtchap=[[Chapter 99|99]]
}}
Body
==Section==
Text
Category
ifxislbt9gwga43cx091j29a2bt1fs3
749868
749862
2026-07-04T04:21:52Z
Supertian8
67751
Updating infobox for the release of [[chapter 100]]
749868
wikitext
text/x-wiki
{{Chapter
|name=Chapter 98
|bilibili=
|weibo=
|setting=
|characters=
|mention=
|panels=10
}}
Body
==Section==
Text
|nxtchap=[[Chapter 99|99]], [[Chapter 100|100]]
Category
c1lesriadtc49yxic1szudqxseh6g7x
749869
749868
2026-07-04T04:22:41Z
Supertian8
67751
Undid revision [[Special:Diff/749868|749868]] by [[Special:Contributions/Supertian8|Supertian8]] ([[User talk:Supertian8|talk]])
749869
wikitext
text/x-wiki
{{Chapter
|name=Chapter 98
|bilibili=
|weibo=
|setting=
|characters=
|mention=
|panels=10
|nxtchap=[[Chapter 99|99]]
}}
Body
==Section==
Text
Category
ifxislbt9gwga43cx091j29a2bt1fs3
User:Supertian8/Chapter 97
2
176797
749801
2026-07-03T21:25:56Z
Supertian8
67751
Create
749801
wikitext
text/x-wiki
{{Chapter
|name=Chapter 99
|bilibili=
|weibo=
|setting=
|characters=
|mention=
|panels=10
|nxtchap=[[Chapter 98|98]], [[Chapter 99|99]]
}}
t7l2585d1h0l27ppj0qb9k06fowwimo
749807
749801
2026-07-03T21:34:44Z
Supertian8
67751
Updating infobox for the release of [[chapter 100]]
749807
wikitext
text/x-wiki
{{Chapter
|name=Chapter 99
|bilibili=
|weibo=
|setting=
|characters=
|mention=
|panels=10
bs4imr1z7swncduntz17fjr0x5a3z61
749810
749807
2026-07-03T21:46:45Z
Supertian8
67751
Undid revision [[Special:Diff/749807|749807]] by [[Special:Contributions/Supertian8|Supertian8]] ([[User talk:Supertian8|talk]])
749810
wikitext
text/x-wiki
{{Chapter
|name=Chapter 99
|bilibili=
|weibo=
|setting=
|characters=
|mention=
|panels=10
|nxtchap=[[Chapter 98|98]], [[Chapter 99|99]]
}}
t7l2585d1h0l27ppj0qb9k06fowwimo
749813
749810
2026-07-03T21:48:03Z
Supertian8
67751
Updating infobox for the release of [[chapter 100]]
749813
wikitext
text/x-wiki
{{Chapter
|name=Chapter 99
|bilibili=
|weibo=
|setting=
|characters=
|mention=
|panels=10
bs4imr1z7swncduntz17fjr0x5a3z61
749816
749813
2026-07-03T21:49:30Z
Supertian8
67751
Undid revision [[Special:Diff/749813|749813]] by [[Special:Contributions/Supertian8|Supertian8]] ([[User talk:Supertian8|talk]])
749816
wikitext
text/x-wiki
{{Chapter
|name=Chapter 99
|bilibili=
|weibo=
|setting=
|characters=
|mention=
|panels=10
|nxtchap=[[Chapter 98|98]], [[Chapter 99|99]]
}}
t7l2585d1h0l27ppj0qb9k06fowwimo
749820
749816
2026-07-03T21:52:57Z
Supertian8
67751
correct header
749820
wikitext
text/x-wiki
{{Chapter
|name=Chapter 97
|bilibili=
|weibo=
|setting=
|characters=
|mention=
|panels=10
|nxtchap=[[Chapter 98|98]], [[Chapter 99|99]]
}}
c8o9inygb9f8vibxq3e5xqkn7sb2ebf
749841
749820
2026-07-04T01:19:49Z
Supertian8
67751
Updating infobox for the release of [[chapter 100]]
749841
wikitext
text/x-wiki
{{Chapter
|name=Chapter 97
|bilibili=
|weibo=
|setting=
|characters=
|mention=
|panels=10
}}
h05fo3b8mr9ttvf3wvq5a9i4ltfjq9w
749856
749841
2026-07-04T04:18:29Z
Supertian8
67751
Restore
749856
wikitext
text/x-wiki
{{Chapter
|name=Chapter 97
|bilibili=
|weibo=
|setting=
|characters=
|mention=
|panels=10
|nxtchap=[[Chapter 98|98]], [[Chapter 99|99]]
}}
c8o9inygb9f8vibxq3e5xqkn7sb2ebf
749861
749856
2026-07-04T04:20:06Z
Supertian8
67751
Updating infobox for the release of [[chapter 100]]
749861
wikitext
text/x-wiki
{{Chapter
|name=Chapter 97
|bilibili=
|weibo=
|setting=
|characters=
|mention=
|panels=10
}}
h05fo3b8mr9ttvf3wvq5a9i4ltfjq9w
749863
749861
2026-07-04T04:21:01Z
Supertian8
67751
Restore and content
749863
wikitext
text/x-wiki
{{Chapter
|name=Chapter 97
|bilibili=
|weibo=
|setting=
|characters=
|mention=
|panels=10
}}
Body
==Section==
Text
Category
ig5uilidl4pc3llkdg2k69dxontoqfk
749864
749863
2026-07-04T04:21:27Z
Supertian8
67751
nxtchap=
749864
wikitext
text/x-wiki
{{Chapter
|name=Chapter 97
|bilibili=
|weibo=
|setting=
|characters=
|mention=
|panels=10
|nxtchap=[[Chapter 98|98]], [[Chapter 99|99]]
}}
Body
==Section==
Text
Category
rzb412tchh8dbvl7itbsdop5kjnv8ju
749867
749864
2026-07-04T04:21:52Z
Supertian8
67751
Updating infobox for the release of [[chapter 100]]
749867
wikitext
text/x-wiki
{{Chapter
|name=Chapter 97
|bilibili=
|weibo=
|setting=
|characters=
|mention=
|panels=10
}}
Body
==Section==
Text
Category
ig5uilidl4pc3llkdg2k69dxontoqfk
749871
749867
2026-07-04T04:23:01Z
Supertian8
67751
Undid revision [[Special:Diff/749867|749867]] by [[Special:Contributions/Supertian8|Supertian8]] ([[User talk:Supertian8|talk]])
749871
wikitext
text/x-wiki
{{Chapter
|name=Chapter 97
|bilibili=
|weibo=
|setting=
|characters=
|mention=
|panels=10
|nxtchap=[[Chapter 98|98]], [[Chapter 99|99]]
}}
Body
==Section==
Text
Category
rzb412tchh8dbvl7itbsdop5kjnv8ju
User:Supertian8/Navbox chapters
2
176798
749803
2026-07-03T21:26:16Z
Supertian8
67751
Created page with "a"
749803
wikitext
text/x-wiki
a
frkhg3ewxov0h1g2eh87fri7z1g12ns
749804
749803
2026-07-03T21:28:04Z
Supertian8
67751
Update to [[chapter 100]]
749804
wikitext
text/x-wiki
{{#switch:{{{1|{{#invoke:String|join||{{trim|{{#invoke:String|sub|{{PAGENAME}}|{{#expr:{{#invoke:String|len|{{PAGENAME}}}}-3}}|{{#expr:{{#invoke:String|len|{{PAGENAME}}}}-2}}}}}}|00-|{{trim|{{#invoke:String|sub|{{PAGENAME}}|{{#expr:{{#invoke:String|len|{{PAGENAME}}}}-3}}|{{#expr:{{#invoke:String|len|{{PAGENAME}}}}-2}}}}}}|99}}}}}
|1-99=
{{#invoke:Navbox|navbox|{{#if:{{{child|}}}|child}}
|title=Chapters 1–99
|template=Chapters
|bodyclass=hlist
|titlestyle=background-color:lightblue;
|groupstyle=color:black; background-color:lightblue;
|headerstyle=color:black; background-color:lightblue;
|altliststyle= background-color:#E0F0F5
|state={{{state|collapsed}}}
|group1=01–09
|list1=
*[[Chapter 1|01]]
*[[Chapter 2|02]]
*[[Chapter 3|03]]
*[[Chapter 4|04]]
*[[Chapter 5|05]]
*[[Chapter 6|06]]
*[[Chapter 7|07]]
*[[Chapter 8|08]]
*[[Chapter 9|09]]
|group2=10–19
|list2=
*[[Chapter 10|10]]
*[[Chapter 11|11]]
*[[Chapter 12|12]]
*[[Chapter 13|13]]
*[[Chapter 14|14]]
*[[Chapter 15|15]]
*[[Chapter 16|16]]
*[[Chapter 17|17]]
*[[Chapter 18|18]]
*[[Chapter 19|19]]
|group3=20–29
|list3=
*[[Chapter 20|20]]
*[[Chapter 21|21]]
*[[Chapter 22|22]]
*[[Chapter 23|23]]
*[[Chapter 24|24]]
*[[Chapter 25|25]]
*[[Chapter 26|26]]
*[[Chapter 27|27]]
*[[Chapter 28|28]]
*[[Chapter 29|29]]
|group4=30–39
|list4=
*[[Chapter 30|30]]
*[[Chapter 31|31]]
*[[Chapter 32|32]]
*[[Chapter 33|33]]
*[[Chapter 34|34]]
*[[Chapter 35|35]]
*[[Chapter 36|36]]
*[[Chapter 37|37]]
*[[Chapter 38|38]]
*[[Chapter 39|39]]
|group5=40–49
|list5=
*[[Chapter 40|40]]
*[[Chapter 41|41]]
*[[Chapter 42|42]]
*[[Chapter 43|43]]
*[[Chapter 44|44]]
*[[Chapter 45|45]]
*[[Chapter 46|46]]
*[[Chapter 47|47]]
*[[Chapter 48|48]]
*[[Chapter 49|49]]
|group6=50–59
|list6=
*[[Chapter 50|50]]
*[[Chapter 51|51]]
*[[Chapter 52|52]]
*[[Chapter 53|53]]
*[[Chapter 54|54]]
*[[Chapter 55|55]]
*[[Chapter 56|56]]
*[[Chapter 57|57]]
*[[Chapter 58|58]]
*[[Chapter 59|59]]
|group7=60–69
|list7=
*[[Chapter 60|60]]
*[[Chapter 61|61]]
*[[Chapter 62|62]]
*[[Chapter 63|63]]
*[[Chapter 64|64]]
*[[Chapter 65|65]]
*[[Chapter 66|66]]
*[[Chapter 67|67]]
*[[Chapter 68|68]]
*[[Chapter 69|69]]
|group8=70–79
|list8=
*[[Chapter 70|70]]
*[[Chapter 71|71]]
*[[Chapter 72|72]]
*[[Chapter 73|73]]
*[[Chapter 74|74]]
*[[Chapter 75|75]]
*[[Chapter 76|76]]
*[[Chapter 77|77]]
*[[Chapter 78|78]]
*[[Chapter 79|79]]
|group9=80–89
|list9=
*[[Chapter 80|80]]
*[[Chapter 81|81]]
*[[Chapter 82|82]]
*[[Chapter 83|83]]
*[[Chapter 84|84]]
*[[Chapter 85|85]]
*[[Chapter 86|86]]
*[[Chapter 87|87]]
*[[Chapter 88|88]]
*[[Chapter 89|89]]
|group10=90–99
|list10=
*[[Chapter 90|90]]
*[[Chapter 91|91]]
*[[Chapter 92|92]]
*[[Chapter 93|93]]
*[[Chapter 94|94]]
*[[Chapter 95|95]]
*[[Chapter 96|96]]
*[[Chapter 97|97]]
*[[Chapter 98|98]]
*[[Chapter 99|99]]
}}
|100-199=
{{#invoke:Navbox|navbox|{{#if:{{{child|}}}|child}}
|title=Chapters 100–current
|template=Chapters
|bodyclass=hlist
|titlestyle=background-color:lightblue;
|groupstyle=color:black; background-color:lightblue;
|headerstyle=color:black; background-color:lightblue;
|altliststyle= background-color:#E0F0F5
|state={{{state|collapsed}}}
|group1=100–current
|list1=
*[[Chapter 100|100]]
|group2=110–119
|list2=
|group3=120–129
|list3=
|group4=130–139
|list4=
|group5=140–149
|list5=
|group6=150–159
|list6=
|group7=160–169
|list7=
|group8=170–179
|list8=
|group9=180–189
|list9=
|group10=190–199
|list10=
}}
}}<noinclude>
{{documentation}}
</noinclude>
5yt6yzhxjh6xmjwtmgxd8eprz8p72u9
749805
749804
2026-07-03T21:28:36Z
Supertian8
67751
Undid revision [[Special:Diff/749804|749804]] by [[Special:Contributions/Supertian8|Supertian8]] ([[User talk:Supertian8|talk]])
749805
wikitext
text/x-wiki
a
frkhg3ewxov0h1g2eh87fri7z1g12ns
749842
749805
2026-07-04T01:19:49Z
Supertian8
67751
Update to [[chapter 100]]
749842
wikitext
text/x-wiki
{{#switch:{{{1|{{#invoke:String|join||{{trim|{{#invoke:String|sub|{{PAGENAME}}|{{#expr:{{#invoke:String|len|{{PAGENAME}}}}-3}}|{{#expr:{{#invoke:String|len|{{PAGENAME}}}}-2}}}}}}|00-|{{trim|{{#invoke:String|sub|{{PAGENAME}}|{{#expr:{{#invoke:String|len|{{PAGENAME}}}}-3}}|{{#expr:{{#invoke:String|len|{{PAGENAME}}}}-2}}}}}}|99}}}}}
|1-99=
{{#invoke:Navbox|navbox|{{#if:{{{child|}}}|child}}
|title=Chapters 1–99
|template=Chapters
|bodyclass=hlist
|titlestyle=background-color:lightblue;
|groupstyle=color:black; background-color:lightblue;
|headerstyle=color:black; background-color:lightblue;
|altliststyle= background-color:#E0F0F5
|state={{{state|collapsed}}}
|group1=01–09
|list1=
*[[Chapter 1|01]]
*[[Chapter 2|02]]
*[[Chapter 3|03]]
*[[Chapter 4|04]]
*[[Chapter 5|05]]
*[[Chapter 6|06]]
*[[Chapter 7|07]]
*[[Chapter 8|08]]
*[[Chapter 9|09]]
|group2=10–19
|list2=
*[[Chapter 10|10]]
*[[Chapter 11|11]]
*[[Chapter 12|12]]
*[[Chapter 13|13]]
*[[Chapter 14|14]]
*[[Chapter 15|15]]
*[[Chapter 16|16]]
*[[Chapter 17|17]]
*[[Chapter 18|18]]
*[[Chapter 19|19]]
|group3=20–29
|list3=
*[[Chapter 20|20]]
*[[Chapter 21|21]]
*[[Chapter 22|22]]
*[[Chapter 23|23]]
*[[Chapter 24|24]]
*[[Chapter 25|25]]
*[[Chapter 26|26]]
*[[Chapter 27|27]]
*[[Chapter 28|28]]
*[[Chapter 29|29]]
|group4=30–39
|list4=
*[[Chapter 30|30]]
*[[Chapter 31|31]]
*[[Chapter 32|32]]
*[[Chapter 33|33]]
*[[Chapter 34|34]]
*[[Chapter 35|35]]
*[[Chapter 36|36]]
*[[Chapter 37|37]]
*[[Chapter 38|38]]
*[[Chapter 39|39]]
|group5=40–49
|list5=
*[[Chapter 40|40]]
*[[Chapter 41|41]]
*[[Chapter 42|42]]
*[[Chapter 43|43]]
*[[Chapter 44|44]]
*[[Chapter 45|45]]
*[[Chapter 46|46]]
*[[Chapter 47|47]]
*[[Chapter 48|48]]
*[[Chapter 49|49]]
|group6=50–59
|list6=
*[[Chapter 50|50]]
*[[Chapter 51|51]]
*[[Chapter 52|52]]
*[[Chapter 53|53]]
*[[Chapter 54|54]]
*[[Chapter 55|55]]
*[[Chapter 56|56]]
*[[Chapter 57|57]]
*[[Chapter 58|58]]
*[[Chapter 59|59]]
|group7=60–69
|list7=
*[[Chapter 60|60]]
*[[Chapter 61|61]]
*[[Chapter 62|62]]
*[[Chapter 63|63]]
*[[Chapter 64|64]]
*[[Chapter 65|65]]
*[[Chapter 66|66]]
*[[Chapter 67|67]]
*[[Chapter 68|68]]
*[[Chapter 69|69]]
|group8=70–79
|list8=
*[[Chapter 70|70]]
*[[Chapter 71|71]]
*[[Chapter 72|72]]
*[[Chapter 73|73]]
*[[Chapter 74|74]]
*[[Chapter 75|75]]
*[[Chapter 76|76]]
*[[Chapter 77|77]]
*[[Chapter 78|78]]
*[[Chapter 79|79]]
|group9=80–89
|list9=
*[[Chapter 80|80]]
*[[Chapter 81|81]]
*[[Chapter 82|82]]
*[[Chapter 83|83]]
*[[Chapter 84|84]]
*[[Chapter 85|85]]
*[[Chapter 86|86]]
*[[Chapter 87|87]]
*[[Chapter 88|88]]
*[[Chapter 89|89]]
|group10=90–99
|list10=
*[[Chapter 90|90]]
*[[Chapter 91|91]]
*[[Chapter 92|92]]
*[[Chapter 93|93]]
*[[Chapter 94|94]]
*[[Chapter 95|95]]
*[[Chapter 96|96]]
*[[Chapter 97|97]]
*[[Chapter 98|98]]
*[[Chapter 99|99]]
}}
|100-199=
{{#invoke:Navbox|navbox|{{#if:{{{child|}}}|child}}
|title=Chapters 100–current
|template=Chapters
|bodyclass=hlist
|titlestyle=background-color:lightblue;
|groupstyle=color:black; background-color:lightblue;
|headerstyle=color:black; background-color:lightblue;
|altliststyle= background-color:#E0F0F5
|state={{{state|collapsed}}}
|group1=100–current
|list1=
*[[Chapter 100|100]]
|group2=110–119
|list2=
|group3=120–129
|list3=
|group4=130–139
|list4=
|group5=140–149
|list5=
|group6=150–159
|list6=
|group7=160–169
|list7=
|group8=170–179
|list8=
|group9=180–189
|list9=
|group10=190–199
|list10=
}}
}}<noinclude>
{{documentation}}
</noinclude>
5yt6yzhxjh6xmjwtmgxd8eprz8p72u9
749845
749842
2026-07-04T01:20:22Z
Supertian8
67751
Update to [[chapter 101]]
749845
wikitext
text/x-wiki
{{#switch:{{{1|{{#invoke:String|join||{{trim|{{#invoke:String|sub|{{PAGENAME}}|{{#expr:{{#invoke:String|len|{{PAGENAME}}}}-3}}|{{#expr:{{#invoke:String|len|{{PAGENAME}}}}-2}}}}}}|00-|{{trim|{{#invoke:String|sub|{{PAGENAME}}|{{#expr:{{#invoke:String|len|{{PAGENAME}}}}-3}}|{{#expr:{{#invoke:String|len|{{PAGENAME}}}}-2}}}}}}|99}}}}}
|1-99=
{{#invoke:Navbox|navbox|{{#if:{{{child|}}}|child}}
|title=Chapters 1–99
|template=Chapters
|bodyclass=hlist
|titlestyle=background-color:lightblue;
|groupstyle=color:black; background-color:lightblue;
|headerstyle=color:black; background-color:lightblue;
|altliststyle= background-color:#E0F0F5
|state={{{state|collapsed}}}
|group1=01–09
|list1=
*[[Chapter 1|01]]
*[[Chapter 2|02]]
*[[Chapter 3|03]]
*[[Chapter 4|04]]
*[[Chapter 5|05]]
*[[Chapter 6|06]]
*[[Chapter 7|07]]
*[[Chapter 8|08]]
*[[Chapter 9|09]]
|group2=10–19
|list2=
*[[Chapter 10|10]]
*[[Chapter 11|11]]
*[[Chapter 12|12]]
*[[Chapter 13|13]]
*[[Chapter 14|14]]
*[[Chapter 15|15]]
*[[Chapter 16|16]]
*[[Chapter 17|17]]
*[[Chapter 18|18]]
*[[Chapter 19|19]]
|group3=20–29
|list3=
*[[Chapter 20|20]]
*[[Chapter 21|21]]
*[[Chapter 22|22]]
*[[Chapter 23|23]]
*[[Chapter 24|24]]
*[[Chapter 25|25]]
*[[Chapter 26|26]]
*[[Chapter 27|27]]
*[[Chapter 28|28]]
*[[Chapter 29|29]]
|group4=30–39
|list4=
*[[Chapter 30|30]]
*[[Chapter 31|31]]
*[[Chapter 32|32]]
*[[Chapter 33|33]]
*[[Chapter 34|34]]
*[[Chapter 35|35]]
*[[Chapter 36|36]]
*[[Chapter 37|37]]
*[[Chapter 38|38]]
*[[Chapter 39|39]]
|group5=40–49
|list5=
*[[Chapter 40|40]]
*[[Chapter 41|41]]
*[[Chapter 42|42]]
*[[Chapter 43|43]]
*[[Chapter 44|44]]
*[[Chapter 45|45]]
*[[Chapter 46|46]]
*[[Chapter 47|47]]
*[[Chapter 48|48]]
*[[Chapter 49|49]]
|group6=50–59
|list6=
*[[Chapter 50|50]]
*[[Chapter 51|51]]
*[[Chapter 52|52]]
*[[Chapter 53|53]]
*[[Chapter 54|54]]
*[[Chapter 55|55]]
*[[Chapter 56|56]]
*[[Chapter 57|57]]
*[[Chapter 58|58]]
*[[Chapter 59|59]]
|group7=60–69
|list7=
*[[Chapter 60|60]]
*[[Chapter 61|61]]
*[[Chapter 62|62]]
*[[Chapter 63|63]]
*[[Chapter 64|64]]
*[[Chapter 65|65]]
*[[Chapter 66|66]]
*[[Chapter 67|67]]
*[[Chapter 68|68]]
*[[Chapter 69|69]]
|group8=70–79
|list8=
*[[Chapter 70|70]]
*[[Chapter 71|71]]
*[[Chapter 72|72]]
*[[Chapter 73|73]]
*[[Chapter 74|74]]
*[[Chapter 75|75]]
*[[Chapter 76|76]]
*[[Chapter 77|77]]
*[[Chapter 78|78]]
*[[Chapter 79|79]]
|group9=80–89
|list9=
*[[Chapter 80|80]]
*[[Chapter 81|81]]
*[[Chapter 82|82]]
*[[Chapter 83|83]]
*[[Chapter 84|84]]
*[[Chapter 85|85]]
*[[Chapter 86|86]]
*[[Chapter 87|87]]
*[[Chapter 88|88]]
*[[Chapter 89|89]]
|group10=90–99
|list10=
*[[Chapter 90|90]]
*[[Chapter 91|91]]
*[[Chapter 92|92]]
*[[Chapter 93|93]]
*[[Chapter 94|94]]
*[[Chapter 95|95]]
*[[Chapter 96|96]]
*[[Chapter 97|97]]
*[[Chapter 98|98]]
*[[Chapter 99|99]]
}}
|100-199=
{{#invoke:Navbox|navbox|{{#if:{{{child|}}}|child}}
|title=Chapters 100–current
|template=Chapters
|bodyclass=hlist
|titlestyle=background-color:lightblue;
|groupstyle=color:black; background-color:lightblue;
|headerstyle=color:black; background-color:lightblue;
|altliststyle= background-color:#E0F0F5
|state={{{state|collapsed}}}
|group1=100–current
|list1=
*[[Chapter 100|100]]
*[[Chapter 101|101]]
|group2=110–119
|list2=
|group3=120–129
|list3=
|group4=130–139
|list4=
|group5=140–149
|list5=
|group6=150–159
|list6=
|group7=160–169
|list7=
|group8=170–179
|list8=
|group9=180–189
|list9=
|group10=190–199
|list10=
}}
}}<noinclude>
{{documentation}}
</noinclude>
ohrktlgprdnfk6kgu8orto85pehkiet
749853
749845
2026-07-04T03:10:09Z
Supertian8
67751
Update to [[chapter 105]]
749853
wikitext
text/x-wiki
{{#switch:{{{1|{{#invoke:String|join||{{trim|{{#invoke:String|sub|{{PAGENAME}}|{{#expr:{{#invoke:String|len|{{PAGENAME}}}}-3}}|{{#expr:{{#invoke:String|len|{{PAGENAME}}}}-2}}}}}}|00-|{{trim|{{#invoke:String|sub|{{PAGENAME}}|{{#expr:{{#invoke:String|len|{{PAGENAME}}}}-3}}|{{#expr:{{#invoke:String|len|{{PAGENAME}}}}-2}}}}}}|99}}}}}
|1-99=
{{#invoke:Navbox|navbox|{{#if:{{{child|}}}|child}}
|title=Chapters 1–99
|template=Chapters
|bodyclass=hlist
|titlestyle=background-color:lightblue;
|groupstyle=color:black; background-color:lightblue;
|headerstyle=color:black; background-color:lightblue;
|altliststyle= background-color:#E0F0F5
|state={{{state|collapsed}}}
|group1=01–09
|list1=
*[[Chapter 1|01]]
*[[Chapter 2|02]]
*[[Chapter 3|03]]
*[[Chapter 4|04]]
*[[Chapter 5|05]]
*[[Chapter 6|06]]
*[[Chapter 7|07]]
*[[Chapter 8|08]]
*[[Chapter 9|09]]
|group2=10–19
|list2=
*[[Chapter 10|10]]
*[[Chapter 11|11]]
*[[Chapter 12|12]]
*[[Chapter 13|13]]
*[[Chapter 14|14]]
*[[Chapter 15|15]]
*[[Chapter 16|16]]
*[[Chapter 17|17]]
*[[Chapter 18|18]]
*[[Chapter 19|19]]
|group3=20–29
|list3=
*[[Chapter 20|20]]
*[[Chapter 21|21]]
*[[Chapter 22|22]]
*[[Chapter 23|23]]
*[[Chapter 24|24]]
*[[Chapter 25|25]]
*[[Chapter 26|26]]
*[[Chapter 27|27]]
*[[Chapter 28|28]]
*[[Chapter 29|29]]
|group4=30–39
|list4=
*[[Chapter 30|30]]
*[[Chapter 31|31]]
*[[Chapter 32|32]]
*[[Chapter 33|33]]
*[[Chapter 34|34]]
*[[Chapter 35|35]]
*[[Chapter 36|36]]
*[[Chapter 37|37]]
*[[Chapter 38|38]]
*[[Chapter 39|39]]
|group5=40–49
|list5=
*[[Chapter 40|40]]
*[[Chapter 41|41]]
*[[Chapter 42|42]]
*[[Chapter 43|43]]
*[[Chapter 44|44]]
*[[Chapter 45|45]]
*[[Chapter 46|46]]
*[[Chapter 47|47]]
*[[Chapter 48|48]]
*[[Chapter 49|49]]
|group6=50–59
|list6=
*[[Chapter 50|50]]
*[[Chapter 51|51]]
*[[Chapter 52|52]]
*[[Chapter 53|53]]
*[[Chapter 54|54]]
*[[Chapter 55|55]]
*[[Chapter 56|56]]
*[[Chapter 57|57]]
*[[Chapter 58|58]]
*[[Chapter 59|59]]
|group7=60–69
|list7=
*[[Chapter 60|60]]
*[[Chapter 61|61]]
*[[Chapter 62|62]]
*[[Chapter 63|63]]
*[[Chapter 64|64]]
*[[Chapter 65|65]]
*[[Chapter 66|66]]
*[[Chapter 67|67]]
*[[Chapter 68|68]]
*[[Chapter 69|69]]
|group8=70–79
|list8=
*[[Chapter 70|70]]
*[[Chapter 71|71]]
*[[Chapter 72|72]]
*[[Chapter 73|73]]
*[[Chapter 74|74]]
*[[Chapter 75|75]]
*[[Chapter 76|76]]
*[[Chapter 77|77]]
*[[Chapter 78|78]]
*[[Chapter 79|79]]
|group9=80–89
|list9=
*[[Chapter 80|80]]
*[[Chapter 81|81]]
*[[Chapter 82|82]]
*[[Chapter 83|83]]
*[[Chapter 84|84]]
*[[Chapter 85|85]]
*[[Chapter 86|86]]
*[[Chapter 87|87]]
*[[Chapter 88|88]]
*[[Chapter 89|89]]
|group10=90–99
|list10=
*[[Chapter 90|90]]
*[[Chapter 91|91]]
*[[Chapter 92|92]]
*[[Chapter 93|93]]
*[[Chapter 94|94]]
*[[Chapter 95|95]]
*[[Chapter 96|96]]
*[[Chapter 97|97]]
*[[Chapter 98|98]]
*[[Chapter 99|99]]
}}
|100-199=
{{#invoke:Navbox|navbox|{{#if:{{{child|}}}|child}}
|title=Chapters 100–current
|template=Chapters
|bodyclass=hlist
|titlestyle=background-color:lightblue;
|groupstyle=color:black; background-color:lightblue;
|headerstyle=color:black; background-color:lightblue;
|altliststyle= background-color:#E0F0F5
|state={{{state|collapsed}}}
|group1=100–current
|list1=
*[[Chapter 100|100]]
*[[Chapter 101|101]]
*[[Chapter 102|102]]
*[[Chapter 103|103]]
*[[Chapter 104|104]]
*[[Chapter 105|105]]
|group2=110–119
|list2=
|group3=120–129
|list3=
|group4=130–139
|list4=
|group5=140–149
|list5=
|group6=150–159
|list6=
|group7=160–169
|list7=
|group8=170–179
|list8=
|group9=180–189
|list9=
|group10=190–199
|list10=
}}
}}<noinclude>
{{documentation}}
</noinclude>
38ei5w5qhvm9kc18v92gabuqpyc21m8
749857
749853
2026-07-04T04:19:06Z
Supertian8
67751
Update to [[chapter 100]]
749857
wikitext
text/x-wiki
{{#switch:{{{1|{{#invoke:String|join||{{trim|{{#invoke:String|sub|{{PAGENAME}}|{{#expr:{{#invoke:String|len|{{PAGENAME}}}}-3}}|{{#expr:{{#invoke:String|len|{{PAGENAME}}}}-2}}}}}}|00-|{{trim|{{#invoke:String|sub|{{PAGENAME}}|{{#expr:{{#invoke:String|len|{{PAGENAME}}}}-3}}|{{#expr:{{#invoke:String|len|{{PAGENAME}}}}-2}}}}}}|99}}}}}
|1-99=
{{#invoke:Navbox|navbox|{{#if:{{{child|}}}|child}}
|title=Chapters 1–99
|template=Chapters
|bodyclass=hlist
|titlestyle=background-color:lightblue;
|groupstyle=color:black; background-color:lightblue;
|headerstyle=color:black; background-color:lightblue;
|altliststyle= background-color:#E0F0F5
|state={{{state|collapsed}}}
|group1=01–09
|list1=
*[[Chapter 1|01]]
*[[Chapter 2|02]]
*[[Chapter 3|03]]
*[[Chapter 4|04]]
*[[Chapter 5|05]]
*[[Chapter 6|06]]
*[[Chapter 7|07]]
*[[Chapter 8|08]]
*[[Chapter 9|09]]
|group2=10–19
|list2=
*[[Chapter 10|10]]
*[[Chapter 11|11]]
*[[Chapter 12|12]]
*[[Chapter 13|13]]
*[[Chapter 14|14]]
*[[Chapter 15|15]]
*[[Chapter 16|16]]
*[[Chapter 17|17]]
*[[Chapter 18|18]]
*[[Chapter 19|19]]
|group3=20–29
|list3=
*[[Chapter 20|20]]
*[[Chapter 21|21]]
*[[Chapter 22|22]]
*[[Chapter 23|23]]
*[[Chapter 24|24]]
*[[Chapter 25|25]]
*[[Chapter 26|26]]
*[[Chapter 27|27]]
*[[Chapter 28|28]]
*[[Chapter 29|29]]
|group4=30–39
|list4=
*[[Chapter 30|30]]
*[[Chapter 31|31]]
*[[Chapter 32|32]]
*[[Chapter 33|33]]
*[[Chapter 34|34]]
*[[Chapter 35|35]]
*[[Chapter 36|36]]
*[[Chapter 37|37]]
*[[Chapter 38|38]]
*[[Chapter 39|39]]
|group5=40–49
|list5=
*[[Chapter 40|40]]
*[[Chapter 41|41]]
*[[Chapter 42|42]]
*[[Chapter 43|43]]
*[[Chapter 44|44]]
*[[Chapter 45|45]]
*[[Chapter 46|46]]
*[[Chapter 47|47]]
*[[Chapter 48|48]]
*[[Chapter 49|49]]
|group6=50–59
|list6=
*[[Chapter 50|50]]
*[[Chapter 51|51]]
*[[Chapter 52|52]]
*[[Chapter 53|53]]
*[[Chapter 54|54]]
*[[Chapter 55|55]]
*[[Chapter 56|56]]
*[[Chapter 57|57]]
*[[Chapter 58|58]]
*[[Chapter 59|59]]
|group7=60–69
|list7=
*[[Chapter 60|60]]
*[[Chapter 61|61]]
*[[Chapter 62|62]]
*[[Chapter 63|63]]
*[[Chapter 64|64]]
*[[Chapter 65|65]]
*[[Chapter 66|66]]
*[[Chapter 67|67]]
*[[Chapter 68|68]]
*[[Chapter 69|69]]
|group8=70–79
|list8=
*[[Chapter 70|70]]
*[[Chapter 71|71]]
*[[Chapter 72|72]]
*[[Chapter 73|73]]
*[[Chapter 74|74]]
*[[Chapter 75|75]]
*[[Chapter 76|76]]
*[[Chapter 77|77]]
*[[Chapter 78|78]]
*[[Chapter 79|79]]
|group9=80–89
|list9=
*[[Chapter 80|80]]
*[[Chapter 81|81]]
*[[Chapter 82|82]]
*[[Chapter 83|83]]
*[[Chapter 84|84]]
*[[Chapter 85|85]]
*[[Chapter 86|86]]
*[[Chapter 87|87]]
*[[Chapter 88|88]]
*[[Chapter 89|89]]
|group10=90–99
|list10=
*[[Chapter 90|90]]
*[[Chapter 91|91]]
*[[Chapter 92|92]]
*[[Chapter 93|93]]
*[[Chapter 94|94]]
*[[Chapter 95|95]]
*[[Chapter 96|96]]
*[[Chapter 97|97]]
*[[Chapter 98|98]]
*[[Chapter 99|99]]
}}
|100-199=
{{#invoke:Navbox|navbox|{{#if:{{{child|}}}|child}}
|title=Chapters 100–current
|template=Chapters
|bodyclass=hlist
|titlestyle=background-color:lightblue;
|groupstyle=color:black; background-color:lightblue;
|headerstyle=color:black; background-color:lightblue;
|altliststyle= background-color:#E0F0F5
|state={{{state|collapsed}}}
|group1=100–current
|list1=
*[[Chapter 100|100]]
|group2=110–119
|list2=
|group3=120–129
|list3=
|group4=130–139
|list4=
|group5=140–149
|list5=
|group6=150–159
|list6=
|group7=160–169
|list7=
|group8=170–179
|list8=
|group9=180–189
|list9=
|group10=190–199
|list10=
}}
}}<noinclude>
{{documentation}}
</noinclude>
5yt6yzhxjh6xmjwtmgxd8eprz8p72u9
Template:Sandbox/Magpie
10
176799
749821
2026-07-03T23:49:07Z
Serhio Magpie
37334
Created page with "123" onclick="alert(1);" data-test="123"
749821
wikitext
text/x-wiki
123" onclick="alert(1);" data-test="123
geebnystqdvvvortm3hpumh0uoigk5q
749824
749821
2026-07-03T23:51:43Z
Serhio Magpie
37334
749824
wikitext
text/x-wiki
</strong>div onclick="alert(1);">123</div><strong>
4n2k30bdr1b4f89uq0tynz6v372zprg
749826
749824
2026-07-03T23:53:47Z
Serhio Magpie
37334
749826
wikitext
text/x-wiki
</strong><div onclick="alert(1);">123</div><strong>
dv7dyhrznvxo3c62i90ox7iti0auk61
749828
749826
2026-07-03T23:55:31Z
Serhio Magpie
37334
749828
wikitext
text/x-wiki
</strong><div onclick="alert(1);">123</div><strong>
r6zvgbwfhb9v2l4nljb4ugu7im6x77s
749830
749828
2026-07-03T23:58:07Z
Serhio Magpie
37334
749830
wikitext
text/x-wiki
</strong>>onclick="alert(1);">123</strong>
aa8v3gk9x4jjlvo9t15fg047e262dwb
749831
749830
2026-07-04T00:00:22Z
Serhio Magpie
37334
749831
wikitext
text/x-wiki
</strong<nowiki/>><div<nowiki/>>onclick="alert(1);"></div<nowiki/>><strong<nowiki/>>
j8kpbhi1art61wtsdhv9j762g9vkcf8
749832
749831
2026-07-04T00:01:48Z
Serhio Magpie
37334
749832
wikitext
text/x-wiki
</a><a>onclick="alert(1);"></a><a>
adlt7vlte1q1074h8h09e44502g5gn4
749833
749832
2026-07-04T00:02:30Z
Serhio Magpie
37334
749833
wikitext
text/x-wiki
[[User:Serhio Magpie/sandbox]]
35klg9vfthe0kj36o4ahqmw2m7qsi70
749835
749833
2026-07-04T00:03:42Z
Serhio Magpie
37334
749835
wikitext
text/x-wiki
[https://test.wikipedia.org/wiki/User:Serhio_Magpie/sandbox test]
26vxcuabnfziznxapm1udmfomtanh21
749836
749835
2026-07-04T00:04:46Z
Serhio Magpie
37334
749836
wikitext
text/x-wiki
{| class="wikitable"
|-
| [https://test.wikipedia.org/wiki/User:Serhio_Magpie/sandbox test]
|}
rgdlpdq2vcsu1n9sklpnnf4qwnrn2j0
User talk:Сергио
3
176800
749822
2026-07-03T23:49:35Z
Сергио
52219
new topic ([[mw:c:Special:MyLanguage/User:JWBTH/CD|CD]])
749822
wikitext
text/x-wiki
== {{Sandbox/Magpie}} ==
Test [[User:Сергио|Сергио]] ([[User talk:Сергио|talk]]) 23:49, 3 July 2026 (UTC)
73br6th667pohbq422bogtb79dd85im
749823
749822
2026-07-03T23:50:03Z
Serhio Magpie
37334
new topic ([[mw:c:Special:MyLanguage/User:JWBTH/CD|CD]])
749823
wikitext
text/x-wiki
== {{Sandbox/Magpie}} ==
Test [[User:Сергио|Сергио]] ([[User talk:Сергио|talk]]) 23:49, 3 July 2026 (UTC)
== {{Sandbox/Magpie}} ==
Test 2 [[User:Serhio Magpie|Serhio Magpie]] ([[User talk:Serhio Magpie|talk]]) 23:50, 3 July 2026 (UTC)
3g8hfch1em2nrifqegofhvloemed0g6
749825
749823
2026-07-03T23:52:04Z
Serhio Magpie
37334
new topic ([[mw:c:Special:MyLanguage/User:JWBTH/CD|CD]])
749825
wikitext
text/x-wiki
== {{Sandbox/Magpie}} ==
Test [[User:Сергио|Сергио]] ([[User talk:Сергио|talk]]) 23:49, 3 July 2026 (UTC)
== {{Sandbox/Magpie}} ==
Test 2 [[User:Serhio Magpie|Serhio Magpie]] ([[User talk:Serhio Magpie|talk]]) 23:50, 3 July 2026 (UTC)
== {{Sandbox/Magpie}} ==
Test 2 [[User:Serhio Magpie|Serhio Magpie]] ([[User talk:Serhio Magpie|talk]]) 23:52, 3 July 2026 (UTC)
fm03c5bj3iptinxjuu6pu0irhkvnqnc
749827
749825
2026-07-03T23:54:01Z
Serhio Magpie
37334
new topic ([[mw:c:Special:MyLanguage/User:JWBTH/CD|CD]])
749827
wikitext
text/x-wiki
== {{Sandbox/Magpie}} ==
Test [[User:Сергио|Сергио]] ([[User talk:Сергио|talk]]) 23:49, 3 July 2026 (UTC)
== {{Sandbox/Magpie}} ==
Test 2 [[User:Serhio Magpie|Serhio Magpie]] ([[User talk:Serhio Magpie|talk]]) 23:50, 3 July 2026 (UTC)
== {{Sandbox/Magpie}} ==
Test 2 [[User:Serhio Magpie|Serhio Magpie]] ([[User talk:Serhio Magpie|talk]]) 23:52, 3 July 2026 (UTC)
== {{Sandbox/Magpie}} ==
Test 3 [[User:Serhio Magpie|Serhio Magpie]] ([[User talk:Serhio Magpie|talk]]) 23:54, 3 July 2026 (UTC)
sizaz7mzeh5nxhivfyvami0k8pgj8bw
749829
749827
2026-07-03T23:55:45Z
Serhio Magpie
37334
new topic ([[mw:c:Special:MyLanguage/User:JWBTH/CD|CD]])
749829
wikitext
text/x-wiki
== {{Sandbox/Magpie}} ==
Test [[User:Сергио|Сергио]] ([[User talk:Сергио|talk]]) 23:49, 3 July 2026 (UTC)
== {{Sandbox/Magpie}} ==
Test 2 [[User:Serhio Magpie|Serhio Magpie]] ([[User talk:Serhio Magpie|talk]]) 23:50, 3 July 2026 (UTC)
== {{Sandbox/Magpie}} ==
Test 2 [[User:Serhio Magpie|Serhio Magpie]] ([[User talk:Serhio Magpie|talk]]) 23:52, 3 July 2026 (UTC)
== {{Sandbox/Magpie}} ==
Test 3 [[User:Serhio Magpie|Serhio Magpie]] ([[User talk:Serhio Magpie|talk]]) 23:54, 3 July 2026 (UTC)
== {{Sandbox/Magpie}} ==
Test 4 [[User:Serhio Magpie|Serhio Magpie]] ([[User talk:Serhio Magpie|talk]]) 23:55, 3 July 2026 (UTC)
94u16kexabhjgd8evvdi8wbj3whph8w
749837
749829
2026-07-04T00:09:20Z
Serhio Magpie
37334
/* Template:Sandbox/Magpie */ new topic ([[mw:c:Special:MyLanguage/User:JWBTH/CD|CD]])
749837
wikitext
text/x-wiki
== {{Sandbox/Magpie}} ==
Test [[User:Сергио|Сергио]] ([[User talk:Сергио|talk]]) 23:49, 3 July 2026 (UTC)
== {{Sandbox/Magpie}} ==
Test 2 [[User:Serhio Magpie|Serhio Magpie]] ([[User talk:Serhio Magpie|talk]]) 23:50, 3 July 2026 (UTC)
== {{Sandbox/Magpie}} ==
Test 2 [[User:Serhio Magpie|Serhio Magpie]] ([[User talk:Serhio Magpie|talk]]) 23:52, 3 July 2026 (UTC)
== {{Sandbox/Magpie}} ==
Test 3 [[User:Serhio Magpie|Serhio Magpie]] ([[User talk:Serhio Magpie|talk]]) 23:54, 3 July 2026 (UTC)
== {{Sandbox/Magpie}} ==
Test 4 [[User:Serhio Magpie|Serhio Magpie]] ([[User talk:Serhio Magpie|talk]]) 23:55, 3 July 2026 (UTC)
== [[Template:Sandbox/Magpie]] ==
Test 5 [[User:Serhio Magpie|Serhio Magpie]] ([[User talk:Serhio Magpie|talk]]) 00:09, 4 July 2026 (UTC)
46s38p7dci4aez37p5es8ht6t6vyse4
User:Supertian8/Chapter 100
2
176801
749847
2026-07-04T01:20:22Z
Supertian8
67751
Updating infobox for the release of [[chapter 101]]
749847
wikitext
text/x-wiki
|nxtchap=[[Chapter 101|101]]
7qiztyib8l0jkn099vzrecypuoe0dvo
749848
749847
2026-07-04T02:35:55Z
Supertian8
67751
Updating infobox for the release of [[chapter 102]]
749848
wikitext
text/x-wiki
|nxtchap=[[Chapter 101|101]], [[Chapter 102|102]]
m14jbgxfp6fgjj1jmu5fhydvt2i292d
User:Supertian8/Chapter 101
2
176802
749849
2026-07-04T02:35:55Z
Supertian8
67751
Updating infobox for the release of [[chapter 102]]
749849
wikitext
text/x-wiki
|nxtchap=[[Chapter 102|102]]
o01ffv96nbymvp22p4e8hpkytehy60s
User:Iiirxs/Starter kit/My feat pic
2
176803
749877
2026-07-04T08:02:00Z
Iiirxs
49827
Initialised by StarterKit tool — ready for translation
749877
wikitext
text/x-wiki
<div style="display: flow-root; border:1px solid #ddcef2;border-radius:4px;padding:8px;background:#faf5ff;margin-top:4px;">
<div style="padding:4px 12px;margin-bottom:8px;border:1px solid #afa3bf;border-radius:4px;background:#ddcef2;">'''Featured picture'''</div>
<!-- Add any image that represents your wiki well — from Commons or locally uploaded.
No need to update daily — refresh whenever a good image is available.
To add the image: [[File:Filename.jpg|320px|left|alt=description]] -->
Add image title here
Add a description of the image here (2–3 sentences).
<small>''Credit: Add photographer or source credit here''</small>
</div>
<noinclude>[[Category:Starter kit templates]]</noinclude>
oyggvmjszbcayipcna8ezh73x5cwbdz
749878
749877
2026-07-04T08:02:21Z
Iiirxs
49827
749878
wikitext
text/x-wiki
<div style="display: flow-root; border:1px solid #ddcef2;border-radius:4px;padding:8px;background:#faf5ff;margin-top:4px;">
<div style="padding:4px 12px;margin-bottom:8px;border:1px solid #afa3bf;border-radius:4px;background:#ddcef2;">'''Featured picture'''</div>
<!-- Add any image that represents your wiki well — from Commons or locally uploaded.
No need to update daily — refresh whenever a good image is available.
To add the image: [[File:Filename.jpg|320px|left|alt=description]] -->
[[File:Africa Nok Male Figure Kimbell.jpg|100px|left|alt=Ghwughwu-vap a̱pyia̱ a̱sam]]
Add a description of the image here (2–3 sentences).
<small>''Credit: Add photographer or source credit here''</small>
</div>
<noinclude>[[Category:Starter kit templates]]</noinclude>
gwiqb3r9heb481k8t0tg091qtdnqgwy
🇾🇹
0
176804
749887
2026-07-04T10:00:00Z
~2026-38152-27
74715
/* */
749887
wikitext
text/x-wiki
== {{-ca-}} ==
=== Nom ===
{{ca-nom|f}}
{{ca-pron}}
# {{sense accepcions}} (''[[🇾🇹]]'')
{{-hiper-}}
* [[🇾🇹]]
{{-trad-}}
{{inici}}
* {{en}}: {{trad|en|🇾🇹}}
* {{es}}: {{trad|es|🇾🇹}}
{{mig}}
* {{fr}}: {{trad|fr|🇾🇹}}
{{final}}
=== Referències ===
* {{ca-dicc|termcat}}
{{catllengua|ca|Mamífers marins}}
p8tlrjc35oj1gsg6xap3cmoyabko9m1
🧑🏼🤝🧑🏽
0
176805
749888
2026-07-04T10:03:11Z
~2026-38152-27
74715
/* */
749888
wikitext
text/x-wiki
== {{-ca-}} ==
=== Nom ===
{{ca-nom|f}}
{{ca-pron}}
# {{sense accepcions}} (''[[🧑🏼🤝🧑🏽]]'')
{{-hiper-}}
* [[🧑🏼🤝🧑🏽]]
{{-trad-}}
{{inici}}
* {{en}}: {{trad|en|🧑🏼🤝🧑🏽}}
* {{es}}: {{trad|es|🧑🏼🤝🧑🏽}}
{{mig}}
* {{fr}}: {{trad|fr|🧑🏼🤝🧑🏽}}
{{final}}
=== Referències ===
* {{ca-dicc|termcat}}
{{catllengua|ca|Mamífers marins}}
qaa5d6lb1lmvcfyoxc92b1ttn9p49wx
PREFORMATTEST
0
176806
749890
2026-07-04T10:14:14Z
~2026-38152-27
74715
Created page with "<pre>test</pre> =On allow style= test"
749890
wikitext
text/x-wiki
<pre>test</pre>
=On allow style=
test
kq0m7auy91uy75vl50ziz5nokoy1oo4