Wikipedia
testwiki
https://test.wikipedia.org/wiki/Main_Page
MediaWiki 1.48.0-wmf.1
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
Package
Package talk
CNBanner
CNBanner talk
Translations
Translations talk
Event
Event talk
Topic
Newsletter
Newsletter talk
Wikipedia:Requests/Permissions
4
32559
769511
769290
2026-10-21T21:30:36Z
CKlimas-WMF
75493
769511
wikitext
text/x-wiki
<noinclude>{{Shortcut|WP:R/P|WP:RfP|WP:RfA|WP:PERM|WP:RFR|WP:RFPERM}}</noinclude>
{{Wikipedia:Requests/Top}}
== Requests for user rights ==
* Subpages: [[Wikipedia:Requests/Permissions/All|All (current and archived)]]
* Request:
<inputbox>
type=create
prefix=Wikipedia:Requests/Permissions/
preload=Template:PA2
buttonlabel=Requests for user rights
placeholder=Enter your username
</inputbox>
After creating the subpage, come back here and transclude the page below (<code><nowiki>{{Wikipedia:Requests/Permissions/Example}}</nowiki></code>).
<!-- Please transclude your requests below this line, LATEST AT THE TOP, in the form {{Wikipedia:Requests/Permissions/USERNAME}} -->
{{Wikipedia:Requests/Permissions/CKlimas-WMF}}
{{Wikipedia:Requests/Permissions/Ahecht}}
{{Wikipedia:Requests/Permissions/TypeInfo}}
{{Wikipedia:Requests/Permissions/TienMinh-mun6xnChing}}
<!-- NEW ENTRIES AT THE TOP, NOT HERE -->
{{Wikipedia:Requests/Permissions/Làm Việc Thiện}}
6lama3udhd62u90ulvcjvewwg9c5wpn
Wikipedia:Test
4
71460
769513
769335
2026-10-21T22:50:46Z
~2026-56256-37
76319
/* Test */ Start
769513
wikitext
text/x-wiki
= Test =
1021
Candy Crush Saga Wiki: Tasty Sugar Track Edits Is Out Of Control (Mostly 6/23) and until 24 Restriction until no edits made all day, Use Account Sometimes.
Up To Date
2:6 — 10:8, 242 days, (10:9+ to fill out).
*10:9, 10:10, 10:11, 10:12, 10:13 (?)
*10:14 (?)
*10:15 (?)
*10:16 (?)
*10:17 (?)
*10:18 (10:19)
''10:20''
*202 6525, Pot with Fork and Spoon, Bags with Sunglasses.
*202 6526, Baskets, Tobacco Stuff or Socks.
*202 6527, Food Shopping like Cola Bottles, Cans, Cordial and Others.
*202 6528, Socks, Pairs of Shoes, Dressing Gowns.
*202 6529, Towels, Cubes with Things.
*202 6530, Cloths, Flowers Frisun or Soap.
*202 6531, Food Shopping like Party Pies, Waters and Others.
*202 6601, Pairs of Boots, Pillow or Podiatrist.
*202 6602, Plates or Pans, Grated Cheese or Baskets.
*202 6603, Forks, Pans and Other Kitchen Tools.
*202 6604, Cups, Cordial Jug or Strawberry Milk.
*202 6605, Food Shopping like Cans of Coca Cola, Waters, Cordials or Pile of Sauce.
*202 6606, Purses, Lu Papers or Paper Towels.
*202 6607, Candles, Statues or Pizza Boxes.
*202 6608, Cups, Blankets and Others.
*202 6609, Food Shoppibg like Cola Bottles, Sour Cream and Onion Chips or Cans.
*202 6610, Towels, Cloths, Soap or Containers.
*202 6611, Nachos, Apple N Raspberry Codrial or Stationary.
*202 6612, Basket, Electric Bed with Instructions or Paint.
*202 6613, Aerobar, Birthday Cake or Packet of Candles.
*202 6614, Plates, Blankets or even Vacuum Cleaner.
*202 6615, Towels, Cloths or Dressing Gowns.
*202 6616, Smiths Chips, Heater Vents, Stuff for Lasagna or Cloths.
*202 6617, Pans, Table Spoons or Lids.
*202 6618, Food Shopping like Cans of Coca Cola, Waters, Milk and Others.
*202 6619, Chocolate Milk, Cups of Tea or Extra Mints.
*202 6620, Kitkats, Pens or Domino's Pizza.
*202 6621, Blankets and Pillows or Vacuum Cleaner.
*202 6622, Tea Towels, Cans of Coca Cola or Carlton Draught.
*202 6623, Food Shopping like Bottles of Cola, Waters or New Pyjamas.
*202 6624, Staples, Socks, Hats and Others.
*202 6625, Kitchen Bowls, Pans and Others.
*202 6626, Food Shopping like Colas, Milk or Rainbow Paddlepop.
*202 6627, Tablemats, Sunglasses or Garden Plant.
*202 6628, Boots, Sleep Medicine or Pillow.
*202 6629, Sewing Machine, Tobacco Stuff or Pan.
*202 6630, Food Shopping like Cans of Cocacola, Bottles, Kitkat or M&Ms.
*202 6701, BBQ Chips, Biskits or Grated Cheese.
*202 6702, Lighters, Plates or Cloths.
*202 6703, Hot Dogs, Cola Cans or Lasagna.
*202 6704, Mop, Battery Packs or Doorhandle.
*202 6705, Video Cameras, Internet or McDonalds Bag?.
*202 6706, Pans, Freshmint, or Hotdog Roll.
*202 6707, Food Shopping like Cans of Coca Cola, Cola Bottles, Waters, Shopping Bag or Grated Cheese.
*202 6708, Ktimes, Nutrigrain or Mueslibars.
*202 6709, Saladas, Socks, Vegemite or Textures.
*202 6710, Baskets, Favourites or Cordial Jug.
*202 6711, Shoes like Novo, Vacuum or Carlton Draught.
*202 6712, Aldi Stuff like Mints, Airfryer or Towel.
*202 6713, Food Shopping like Cola Bottles, Cheese, Milk or Paddle Pops.
*202 6714, Baskets, Mask Fresheners or Coat.
*202 6715, Towels, Cloths or Lasagna?.
*202 6716, Soaps, Toothpastes, Cotton Buds and Others.
*202 6717, Cans of Coca Cola, Bottles, Waters, Novo Shoes and Others.
*202 6718, Food Shopping like Upngo, Breads or Cans of Coca Cola.
*202 6719, Quichebox, Plate or Cup or Cheese Machine.
*202 6720, Cups, Milkshakes, Jug or Can of Coca Cola?.
*202 6721, Baskets, Coathangers, Chocolate Milk or Frypan?.
*202 6722, Food Shopping like Cola Bottles, Waters, Milk or Nachos?
*202 6723, Slippers, Blanket Sheets, Clothes Pair and Others.
*202 6724, Pans, Plates, Scissors or Tools.
*202 6725, Aldi Stuff, Domino's Pizza or Lynxspray.
*202 6726, Towels, Lighter or Journal Books.
*202 6727, Food Shopping like Bottles of Cola or Water, Cans or Milk.
*202 6728, Baskets, Battery Packs or Vaporizer and Others.
*202 6729, Sleep Medicines, Deep Heat or Pillows.
*202 6730, Schnitzels, Nesquiks, Milos and Others.
*202 6731, Toothpicks and Nuts and Others.
*202 6801, Food Shopping like Bottles, Cans, Waters and Others.
*202 6802, Plates, Bowls or Battery Packs.
*202 6803, Towels, Blankets, Purse or Boots.
*202 6804, M&M's, Baskets, Cloths or Coathangers.
*202 6805, Boots, Leaves, Texture and Others.
*202 6806, Food Shopping like Cans of Coca Cola, Nachos, Cheese or Milk.
*202 6807, Dressing Gowns, Glasses or Pillows.
*202 6808, Grater Cheese, Pizza, Mat or Plate.
*202 6809, Stylus, Mask, Soaps and Others.
*202 6810, Paper Towels, Staples, Lighter and Others.
*202 6811, Baskets, Cans of Coca Cola or Biskit Shapes.
*202 6812, Blackngold Cola, Blankets or Chocolatebox.
*202 6813, Smiths Chips, Cheese Mozarella or Bread.
*202 6814, Textures, Pens or Basket and Others.
*202 6815, Smoothies, Toys, Chopping Board or Box.
*202 6816, Cups like Gold, Cup of Tea, Metal Bottle and Others.
*202 6817, Food Shopping like Water Bottles, Cans of Coca Cola, Cola Bottles or Chocolate Milk.
*202 6818, Baskets, Keys or Cordial Jug and Others.
*202 6819, Aldi Stuff, Pizza or Pumpsoap.
*202 6820, Towels, Blankets Sheets and Others.
*202 6821, Cheese, Breads, Waters or Cans of Coca Cola and Others.
*202 6822, Sleep Medicone, Quichebox, Towels and Others.
*202 6823, Shoes, Hats or Ashtray and Others.
*202 6824, Cups, Milk, Towels or some Aldi and Others.
*202 6825, Battery Pack, Clothes and Others.
*202 6826, Cans of Coca Cola, Lime Cordial, Milk and Others.
*202 6827, Blankets, Pillows, Circle Table and Others.
*202 6828, Grated Cheese, Scissors, Knifes and Forks and Others.
*202 6829, Cans of Coca Cola, Bottles, Waters, Milk and Others.
*202 6830, Plates, Stand, Frypans and Others.
*202 6831, Metal Things, Water Bottles, Cans of Coca Cola and Others.
*202 6901, Aldi Bag with Stuff or Aprons.
*202 6902, Food Shopping like Cola Bottles, Cheese with Nachos and Others.
*202 6903, Mask Perfumes, Soaps and Others.
*202 6904, Baskets, Battery Pack, Pillows or Lasagna and Others
*202 6905, Frypans, Lids, Tablemats and Others.
*202 6906, Fathers Day Presents, Glasses, Bags and Others.
*202 6907, Deep Heat, Scissors, Milo or Container and Others.
*202 6908, Baskets, Coathanger, Blankets and Others.
*202 6909, Container Biscuit, Pastas or Pans.
*202 6910, Food Shopping like Milks, M&Ms, Sauces, Coca Cola Cans and Others.
*202 6911, Pillows, Dressing Gowns, Extra Mint and Others.
*202 6912, Laundry Stuff like boxes, bags or Dad's Wrist and Others.
*202 6913, Grater Stuff, Pans and Others.
*202 6914, Bottles of Cola, Breads, Milk to make Chocolate.
*202 6915, Iced Coffee, Water Bottles, Baskets and Others.
*202 6916, Aldi Bag, Strawberry Mints and Others.
*202 6917, Lighters, Plates, Freshener or Spray and Others.
*202 6918, New Clothes, Basket, Extra Mint and Others.
*202 6919, Face Cloths, Lu Paper, Water and Others.
*202 6920, Domino's Pizza, Pillows, Notes and Others.
*202 6921, Remotes, Pizza and Barbecue Shapes, Square Pillow and Others.
*202 6922, Food Shopping like Waters, Cans of Coca Cola, Cheese, Lasagna and Others.
*202 6923, Cloths, Facecloth, New Pot and Others.
*202 6924, Aldi Stuff, Glass Cups, Candles and Others.
*202 6925, Copy Paper, Glasses, Lasagna Tray Pan and Others.
*202 6926, Tax Boxes, Bags, Lighter, Texter and Others.
*202 6927, Cans of Coca Cola, Bottles, Cordial and Others.
*202 6928, Pairs of Shoes or Slippers, Square Pillow and Others.
*202 6929, Big Spoon and Others.
*202 6930, Cola Bottle and Others.
*202 61001, Yellow Bag and Others.
*202 61002, Coathangers or Towels and Others.
*202 61003, Knives, Forks and Others.
*202 61004, Hats like DNA hat and Others.
*202 61005, Pizza SHAPES and Others.
*202 61006, Old Pillow or Cola and Others.
Current, 202 61007. ($#!+).
*202 61008. (High Speed Railway, Train)
''20261009''
4319ltl25ort9g0u1fj3uc9dbanmfa7
769514
769513
2026-10-21T23:00:38Z
~2026-56256-37
76319
769514
wikitext
text/x-wiki
= Test =
1021 -
Candy Crush Saga Wiki: Tasty Sugar Track Edits Is Out Of Control (Mostly 6/23) and until 24 Restriction until no edits made all day, Use Account Sometimes.
Up To Date
2:6 — 10:8, 242 days, (10:9+ to fill out).
*10:9, 10:10, 10:11, 10:12, 10:13 (?)
*10:14 (?)
*10:15 (?)
*10:16 (?)
*10:17 (?)
*10:18 (10:19)
''10:20''
*202 6525, Pot with Fork and Spoon, Bags with Sunglasses.
*202 6526, Baskets, Tobacco Stuff or Socks.
*202 6527, Food Shopping like Cola Bottles, Cans, Cordial and Others.
*202 6528, Socks, Pairs of Shoes, Dressing Gowns.
*202 6529, Towels, Cubes with Things.
*202 6530, Cloths, Flowers Frisun or Soap.
*202 6531, Food Shopping like Party Pies, Waters and Others.
*202 6601, Pairs of Boots, Pillow or Podiatrist.
*202 6602, Plates or Pans, Grated Cheese or Baskets.
*202 6603, Forks, Pans and Other Kitchen Tools.
*202 6604, Cups, Cordial Jug or Strawberry Milk.
*202 6605, Food Shopping like Cans of Coca Cola, Waters, Cordials or Pile of Sauce.
*202 6606, Purses, Lu Papers or Paper Towels.
*202 6607, Candles, Statues or Pizza Boxes.
*202 6608, Cups, Blankets and Others.
*202 6609, Food Shoppibg like Cola Bottles, Sour Cream and Onion Chips or Cans.
*202 6610, Towels, Cloths, Soap or Containers.
*202 6611, Nachos, Apple N Raspberry Codrial or Stationary.
*202 6612, Basket, Electric Bed with Instructions or Paint.
*202 6613, Aerobar, Birthday Cake or Packet of Candles.
*202 6614, Plates, Blankets or even Vacuum Cleaner.
*202 6615, Towels, Cloths or Dressing Gowns.
*202 6616, Smiths Chips, Heater Vents, Stuff for Lasagna or Cloths.
*202 6617, Pans, Table Spoons or Lids.
*202 6618, Food Shopping like Cans of Coca Cola, Waters, Milk and Others.
*202 6619, Chocolate Milk, Cups of Tea or Extra Mints.
*202 6620, Kitkats, Pens or Domino's Pizza.
*202 6621, Blankets and Pillows or Vacuum Cleaner.
*202 6622, Tea Towels, Cans of Coca Cola or Carlton Draught.
*202 6623, Food Shopping like Bottles of Cola, Waters or New Pyjamas.
*202 6624, Staples, Socks, Hats and Others.
*202 6625, Kitchen Bowls, Pans and Others.
*202 6626, Food Shopping like Colas, Milk or Rainbow Paddlepop.
*202 6627, Tablemats, Sunglasses or Garden Plant.
*202 6628, Boots, Sleep Medicine or Pillow.
*202 6629, Sewing Machine, Tobacco Stuff or Pan.
*202 6630, Food Shopping like Cans of Cocacola, Bottles, Kitkat or M&Ms.
*202 6701, BBQ Chips, Biskits or Grated Cheese.
*202 6702, Lighters, Plates or Cloths.
*202 6703, Hot Dogs, Cola Cans or Lasagna.
*202 6704, Mop, Battery Packs or Doorhandle.
*202 6705, Video Cameras, Internet or McDonalds Bag?.
*202 6706, Pans, Freshmint, or Hotdog Roll.
*202 6707, Food Shopping like Cans of Coca Cola, Cola Bottles, Waters, Shopping Bag or Grated Cheese.
*202 6708, Ktimes, Nutrigrain or Mueslibars.
*202 6709, Saladas, Socks, Vegemite or Textures.
*202 6710, Baskets, Favourites or Cordial Jug.
*202 6711, Shoes like Novo, Vacuum or Carlton Draught.
*202 6712, Aldi Stuff like Mints, Airfryer or Towel.
*202 6713, Food Shopping like Cola Bottles, Cheese, Milk or Paddle Pops.
*202 6714, Baskets, Mask Fresheners or Coat.
*202 6715, Towels, Cloths or Lasagna?.
*202 6716, Soaps, Toothpastes, Cotton Buds and Others.
*202 6717, Cans of Coca Cola, Bottles, Waters, Novo Shoes and Others.
*202 6718, Food Shopping like Upngo, Breads or Cans of Coca Cola.
*202 6719, Quichebox, Plate or Cup or Cheese Machine.
*202 6720, Cups, Milkshakes, Jug or Can of Coca Cola?.
*202 6721, Baskets, Coathangers, Chocolate Milk or Frypan?.
*202 6722, Food Shopping like Cola Bottles, Waters, Milk or Nachos?
*202 6723, Slippers, Blanket Sheets, Clothes Pair and Others.
*202 6724, Pans, Plates, Scissors or Tools.
*202 6725, Aldi Stuff, Domino's Pizza or Lynxspray.
*202 6726, Towels, Lighter or Journal Books.
*202 6727, Food Shopping like Bottles of Cola or Water, Cans or Milk.
*202 6728, Baskets, Battery Packs or Vaporizer and Others.
*202 6729, Sleep Medicines, Deep Heat or Pillows.
*202 6730, Schnitzels, Nesquiks, Milos and Others.
*202 6731, Toothpicks and Nuts and Others.
*202 6801, Food Shopping like Bottles, Cans, Waters and Others.
*202 6802, Plates, Bowls or Battery Packs.
*202 6803, Towels, Blankets, Purse or Boots.
*202 6804, M&M's, Baskets, Cloths or Coathangers.
*202 6805, Boots, Leaves, Texture and Others.
*202 6806, Food Shopping like Cans of Coca Cola, Nachos, Cheese or Milk.
*202 6807, Dressing Gowns, Glasses or Pillows.
*202 6808, Grater Cheese, Pizza, Mat or Plate.
*202 6809, Stylus, Mask, Soaps and Others.
*202 6810, Paper Towels, Staples, Lighter and Others.
*202 6811, Baskets, Cans of Coca Cola or Biskit Shapes.
*202 6812, Blackngold Cola, Blankets or Chocolatebox.
*202 6813, Smiths Chips, Cheese Mozarella or Bread.
*202 6814, Textures, Pens or Basket and Others.
*202 6815, Smoothies, Toys, Chopping Board or Box.
*202 6816, Cups like Gold, Cup of Tea, Metal Bottle and Others.
*202 6817, Food Shopping like Water Bottles, Cans of Coca Cola, Cola Bottles or Chocolate Milk.
*202 6818, Baskets, Keys or Cordial Jug and Others.
*202 6819, Aldi Stuff, Pizza or Pumpsoap.
*202 6820, Towels, Blankets Sheets and Others.
*202 6821, Cheese, Breads, Waters or Cans of Coca Cola and Others.
*202 6822, Sleep Medicone, Quichebox, Towels and Others.
*202 6823, Shoes, Hats or Ashtray and Others.
*202 6824, Cups, Milk, Towels or some Aldi and Others.
*202 6825, Battery Pack, Clothes and Others.
*202 6826, Cans of Coca Cola, Lime Cordial, Milk and Others.
*202 6827, Blankets, Pillows, Circle Table and Others.
*202 6828, Grated Cheese, Scissors, Knifes and Forks and Others.
*202 6829, Cans of Coca Cola, Bottles, Waters, Milk and Others.
*202 6830, Plates, Stand, Frypans and Others.
*202 6831, Metal Things, Water Bottles, Cans of Coca Cola and Others.
*202 6901, Aldi Bag with Stuff or Aprons.
*202 6902, Food Shopping like Cola Bottles, Cheese with Nachos and Others.
*202 6903, Mask Perfumes, Soaps and Others.
*202 6904, Baskets, Battery Pack, Pillows or Lasagna and Others
*202 6905, Frypans, Lids, Tablemats and Others.
*202 6906, Fathers Day Presents, Glasses, Bags and Others.
*202 6907, Deep Heat, Scissors, Milo or Container and Others.
*202 6908, Baskets, Coathanger, Blankets and Others.
*202 6909, Container Biscuit, Pastas or Pans.
*202 6910, Food Shopping like Milks, M&Ms, Sauces, Coca Cola Cans and Others.
*202 6911, Pillows, Dressing Gowns, Extra Mint and Others.
*202 6912, Laundry Stuff like boxes, bags or Dad's Wrist and Others.
*202 6913, Grater Stuff, Pans and Others.
*202 6914, Bottles of Cola, Breads, Milk to make Chocolate.
*202 6915, Iced Coffee, Water Bottles, Baskets and Others.
*202 6916, Aldi Bag, Strawberry Mints and Others.
*202 6917, Lighters, Plates, Freshener or Spray and Others.
*202 6918, New Clothes, Basket, Extra Mint and Others.
*202 6919, Face Cloths, Lu Paper, Water and Others.
*202 6920, Domino's Pizza, Pillows, Notes and Others.
*202 6921, Remotes, Pizza and Barbecue Shapes, Square Pillow and Others.
*202 6922, Food Shopping like Waters, Cans of Coca Cola, Cheese, Lasagna and Others.
*202 6923, Cloths, Facecloth, New Pot and Others.
*202 6924, Aldi Stuff, Glass Cups, Candles and Others.
*202 6925, Copy Paper, Glasses, Lasagna Tray Pan and Others.
*202 6926, Tax Boxes, Bags, Lighter, Texter and Others.
*202 6927, Cans of Coca Cola, Bottles, Cordial and Others.
*202 6928, Pairs of Shoes or Slippers, Square Pillow and Others.
*202 6929, Big Spoon and Others.
*202 6930, Cola Bottle and Others.
*202 61001, Yellow Bag and Others.
*202 61002, Coathangers or Towels and Others.
*202 61003, Knives, Forks and Others.
*202 61004, Hats like DNA hat and Others.
*202 61005, Pizza SHAPES and Others.
*202 61006, Old Pillow or Cola and Others.
Current, 202 61007. ($#!+).
*202 61008. (High Speed Railway, Train)
''20261009''
0qnwq5yyz6qsyavbn48nf86swqnxs20
769515
769514
2026-10-21T23:10:30Z
~2026-56256-37
76319
769515
wikitext
text/x-wiki
= Test =
1021 - ???
Candy Crush Saga Wiki: Tasty Sugar Track Edits Is Out Of Control (Mostly 6/23) and until 24 Restriction until no edits made all day, Use Account Sometimes.
Up To Date
2:6 — 10:8, 242 days, (10:9+ to fill out).
*10:9, 10:10, 10:11, 10:12, 10:13 (?)
*10:14 (?)
*10:15 (?)
*10:16 (?)
*10:17 (?)
*10:18 (10:19)
''10:20''
*202 6525, Pot with Fork and Spoon, Bags with Sunglasses.
*202 6526, Baskets, Tobacco Stuff or Socks.
*202 6527, Food Shopping like Cola Bottles, Cans, Cordial and Others.
*202 6528, Socks, Pairs of Shoes, Dressing Gowns.
*202 6529, Towels, Cubes with Things.
*202 6530, Cloths, Flowers Frisun or Soap.
*202 6531, Food Shopping like Party Pies, Waters and Others.
*202 6601, Pairs of Boots, Pillow or Podiatrist.
*202 6602, Plates or Pans, Grated Cheese or Baskets.
*202 6603, Forks, Pans and Other Kitchen Tools.
*202 6604, Cups, Cordial Jug or Strawberry Milk.
*202 6605, Food Shopping like Cans of Coca Cola, Waters, Cordials or Pile of Sauce.
*202 6606, Purses, Lu Papers or Paper Towels.
*202 6607, Candles, Statues or Pizza Boxes.
*202 6608, Cups, Blankets and Others.
*202 6609, Food Shoppibg like Cola Bottles, Sour Cream and Onion Chips or Cans.
*202 6610, Towels, Cloths, Soap or Containers.
*202 6611, Nachos, Apple N Raspberry Codrial or Stationary.
*202 6612, Basket, Electric Bed with Instructions or Paint.
*202 6613, Aerobar, Birthday Cake or Packet of Candles.
*202 6614, Plates, Blankets or even Vacuum Cleaner.
*202 6615, Towels, Cloths or Dressing Gowns.
*202 6616, Smiths Chips, Heater Vents, Stuff for Lasagna or Cloths.
*202 6617, Pans, Table Spoons or Lids.
*202 6618, Food Shopping like Cans of Coca Cola, Waters, Milk and Others.
*202 6619, Chocolate Milk, Cups of Tea or Extra Mints.
*202 6620, Kitkats, Pens or Domino's Pizza.
*202 6621, Blankets and Pillows or Vacuum Cleaner.
*202 6622, Tea Towels, Cans of Coca Cola or Carlton Draught.
*202 6623, Food Shopping like Bottles of Cola, Waters or New Pyjamas.
*202 6624, Staples, Socks, Hats and Others.
*202 6625, Kitchen Bowls, Pans and Others.
*202 6626, Food Shopping like Colas, Milk or Rainbow Paddlepop.
*202 6627, Tablemats, Sunglasses or Garden Plant.
*202 6628, Boots, Sleep Medicine or Pillow.
*202 6629, Sewing Machine, Tobacco Stuff or Pan.
*202 6630, Food Shopping like Cans of Cocacola, Bottles, Kitkat or M&Ms.
*202 6701, BBQ Chips, Biskits or Grated Cheese.
*202 6702, Lighters, Plates or Cloths.
*202 6703, Hot Dogs, Cola Cans or Lasagna.
*202 6704, Mop, Battery Packs or Doorhandle.
*202 6705, Video Cameras, Internet or McDonalds Bag?.
*202 6706, Pans, Freshmint, or Hotdog Roll.
*202 6707, Food Shopping like Cans of Coca Cola, Cola Bottles, Waters, Shopping Bag or Grated Cheese.
*202 6708, Ktimes, Nutrigrain or Mueslibars.
*202 6709, Saladas, Socks, Vegemite or Textures.
*202 6710, Baskets, Favourites or Cordial Jug.
*202 6711, Shoes like Novo, Vacuum or Carlton Draught.
*202 6712, Aldi Stuff like Mints, Airfryer or Towel.
*202 6713, Food Shopping like Cola Bottles, Cheese, Milk or Paddle Pops.
*202 6714, Baskets, Mask Fresheners or Coat.
*202 6715, Towels, Cloths or Lasagna?.
*202 6716, Soaps, Toothpastes, Cotton Buds and Others.
*202 6717, Cans of Coca Cola, Bottles, Waters, Novo Shoes and Others.
*202 6718, Food Shopping like Upngo, Breads or Cans of Coca Cola.
*202 6719, Quichebox, Plate or Cup or Cheese Machine.
*202 6720, Cups, Milkshakes, Jug or Can of Coca Cola?.
*202 6721, Baskets, Coathangers, Chocolate Milk or Frypan?.
*202 6722, Food Shopping like Cola Bottles, Waters, Milk or Nachos?
*202 6723, Slippers, Blanket Sheets, Clothes Pair and Others.
*202 6724, Pans, Plates, Scissors or Tools.
*202 6725, Aldi Stuff, Domino's Pizza or Lynxspray.
*202 6726, Towels, Lighter or Journal Books.
*202 6727, Food Shopping like Bottles of Cola or Water, Cans or Milk.
*202 6728, Baskets, Battery Packs or Vaporizer and Others.
*202 6729, Sleep Medicines, Deep Heat or Pillows.
*202 6730, Schnitzels, Nesquiks, Milos and Others.
*202 6731, Toothpicks and Nuts and Others.
*202 6801, Food Shopping like Bottles, Cans, Waters and Others.
*202 6802, Plates, Bowls or Battery Packs.
*202 6803, Towels, Blankets, Purse or Boots.
*202 6804, M&M's, Baskets, Cloths or Coathangers.
*202 6805, Boots, Leaves, Texture and Others.
*202 6806, Food Shopping like Cans of Coca Cola, Nachos, Cheese or Milk.
*202 6807, Dressing Gowns, Glasses or Pillows.
*202 6808, Grater Cheese, Pizza, Mat or Plate.
*202 6809, Stylus, Mask, Soaps and Others.
*202 6810, Paper Towels, Staples, Lighter and Others.
*202 6811, Baskets, Cans of Coca Cola or Biskit Shapes.
*202 6812, Blackngold Cola, Blankets or Chocolatebox.
*202 6813, Smiths Chips, Cheese Mozarella or Bread.
*202 6814, Textures, Pens or Basket and Others.
*202 6815, Smoothies, Toys, Chopping Board or Box.
*202 6816, Cups like Gold, Cup of Tea, Metal Bottle and Others.
*202 6817, Food Shopping like Water Bottles, Cans of Coca Cola, Cola Bottles or Chocolate Milk.
*202 6818, Baskets, Keys or Cordial Jug and Others.
*202 6819, Aldi Stuff, Pizza or Pumpsoap.
*202 6820, Towels, Blankets Sheets and Others.
*202 6821, Cheese, Breads, Waters or Cans of Coca Cola and Others.
*202 6822, Sleep Medicone, Quichebox, Towels and Others.
*202 6823, Shoes, Hats or Ashtray and Others.
*202 6824, Cups, Milk, Towels or some Aldi and Others.
*202 6825, Battery Pack, Clothes and Others.
*202 6826, Cans of Coca Cola, Lime Cordial, Milk and Others.
*202 6827, Blankets, Pillows, Circle Table and Others.
*202 6828, Grated Cheese, Scissors, Knifes and Forks and Others.
*202 6829, Cans of Coca Cola, Bottles, Waters, Milk and Others.
*202 6830, Plates, Stand, Frypans and Others.
*202 6831, Metal Things, Water Bottles, Cans of Coca Cola and Others.
*202 6901, Aldi Bag with Stuff or Aprons.
*202 6902, Food Shopping like Cola Bottles, Cheese with Nachos and Others.
*202 6903, Mask Perfumes, Soaps and Others.
*202 6904, Baskets, Battery Pack, Pillows or Lasagna and Others
*202 6905, Frypans, Lids, Tablemats and Others.
*202 6906, Fathers Day Presents, Glasses, Bags and Others.
*202 6907, Deep Heat, Scissors, Milo or Container and Others.
*202 6908, Baskets, Coathanger, Blankets and Others.
*202 6909, Container Biscuit, Pastas or Pans.
*202 6910, Food Shopping like Milks, M&Ms, Sauces, Coca Cola Cans and Others.
*202 6911, Pillows, Dressing Gowns, Extra Mint and Others.
*202 6912, Laundry Stuff like boxes, bags or Dad's Wrist and Others.
*202 6913, Grater Stuff, Pans and Others.
*202 6914, Bottles of Cola, Breads, Milk to make Chocolate.
*202 6915, Iced Coffee, Water Bottles, Baskets and Others.
*202 6916, Aldi Bag, Strawberry Mints and Others.
*202 6917, Lighters, Plates, Freshener or Spray and Others.
*202 6918, New Clothes, Basket, Extra Mint and Others.
*202 6919, Face Cloths, Lu Paper, Water and Others.
*202 6920, Domino's Pizza, Pillows, Notes and Others.
*202 6921, Remotes, Pizza and Barbecue Shapes, Square Pillow and Others.
*202 6922, Food Shopping like Waters, Cans of Coca Cola, Cheese, Lasagna and Others.
*202 6923, Cloths, Facecloth, New Pot and Others.
*202 6924, Aldi Stuff, Glass Cups, Candles and Others.
*202 6925, Copy Paper, Glasses, Lasagna Tray Pan and Others.
*202 6926, Tax Boxes, Bags, Lighter, Texter and Others.
*202 6927, Cans of Coca Cola, Bottles, Cordial and Others.
*202 6928, Pairs of Shoes or Slippers, Square Pillow and Others.
*202 6929, Big Spoon and Others.
*202 6930, Cola Bottle and Others.
*202 61001, Yellow Bag and Others.
*202 61002, Coathangers or Towels and Others.
*202 61003, Knives, Forks and Others.
*202 61004, Hats like DNA hat and Others.
*202 61005, Pizza SHAPES and Others.
*202 61006, Old Pillow or Cola and Others.
Current, 202 61007. ($#!+).
*202 61008. (High Speed Railway, Train)
''20261009''
igg9turrxd3z3h203lwwry5t1u1lel3
769516
769515
2026-10-21T23:20:53Z
~2026-56256-37
76319
769516
wikitext
text/x-wiki
= Test =
1021 - Lol
Candy Crush Saga Wiki: Tasty Sugar Track Edits Is Out Of Control (Mostly 6/23) and until 24 Restriction until no edits made all day, Use Account Sometimes.
Up To Date
2:6 — 10:8, 242 days, (10:9+ to fill out).
*10:9, 10:10, 10:11, 10:12, 10:13 (?)
*10:14 (?)
*10:15 (?)
*10:16 (?)
*10:17 (?)
*10:18 (10:19)
''10:20''
*202 6525, Pot with Fork and Spoon, Bags with Sunglasses.
*202 6526, Baskets, Tobacco Stuff or Socks.
*202 6527, Food Shopping like Cola Bottles, Cans, Cordial and Others.
*202 6528, Socks, Pairs of Shoes, Dressing Gowns.
*202 6529, Towels, Cubes with Things.
*202 6530, Cloths, Flowers Frisun or Soap.
*202 6531, Food Shopping like Party Pies, Waters and Others.
*202 6601, Pairs of Boots, Pillow or Podiatrist.
*202 6602, Plates or Pans, Grated Cheese or Baskets.
*202 6603, Forks, Pans and Other Kitchen Tools.
*202 6604, Cups, Cordial Jug or Strawberry Milk.
*202 6605, Food Shopping like Cans of Coca Cola, Waters, Cordials or Pile of Sauce.
*202 6606, Purses, Lu Papers or Paper Towels.
*202 6607, Candles, Statues or Pizza Boxes.
*202 6608, Cups, Blankets and Others.
*202 6609, Food Shoppibg like Cola Bottles, Sour Cream and Onion Chips or Cans.
*202 6610, Towels, Cloths, Soap or Containers.
*202 6611, Nachos, Apple N Raspberry Codrial or Stationary.
*202 6612, Basket, Electric Bed with Instructions or Paint.
*202 6613, Aerobar, Birthday Cake or Packet of Candles.
*202 6614, Plates, Blankets or even Vacuum Cleaner.
*202 6615, Towels, Cloths or Dressing Gowns.
*202 6616, Smiths Chips, Heater Vents, Stuff for Lasagna or Cloths.
*202 6617, Pans, Table Spoons or Lids.
*202 6618, Food Shopping like Cans of Coca Cola, Waters, Milk and Others.
*202 6619, Chocolate Milk, Cups of Tea or Extra Mints.
*202 6620, Kitkats, Pens or Domino's Pizza.
*202 6621, Blankets and Pillows or Vacuum Cleaner.
*202 6622, Tea Towels, Cans of Coca Cola or Carlton Draught.
*202 6623, Food Shopping like Bottles of Cola, Waters or New Pyjamas.
*202 6624, Staples, Socks, Hats and Others.
*202 6625, Kitchen Bowls, Pans and Others.
*202 6626, Food Shopping like Colas, Milk or Rainbow Paddlepop.
*202 6627, Tablemats, Sunglasses or Garden Plant.
*202 6628, Boots, Sleep Medicine or Pillow.
*202 6629, Sewing Machine, Tobacco Stuff or Pan.
*202 6630, Food Shopping like Cans of Cocacola, Bottles, Kitkat or M&Ms.
*202 6701, BBQ Chips, Biskits or Grated Cheese.
*202 6702, Lighters, Plates or Cloths.
*202 6703, Hot Dogs, Cola Cans or Lasagna.
*202 6704, Mop, Battery Packs or Doorhandle.
*202 6705, Video Cameras, Internet or McDonalds Bag?.
*202 6706, Pans, Freshmint, or Hotdog Roll.
*202 6707, Food Shopping like Cans of Coca Cola, Cola Bottles, Waters, Shopping Bag or Grated Cheese.
*202 6708, Ktimes, Nutrigrain or Mueslibars.
*202 6709, Saladas, Socks, Vegemite or Textures.
*202 6710, Baskets, Favourites or Cordial Jug.
*202 6711, Shoes like Novo, Vacuum or Carlton Draught.
*202 6712, Aldi Stuff like Mints, Airfryer or Towel.
*202 6713, Food Shopping like Cola Bottles, Cheese, Milk or Paddle Pops.
*202 6714, Baskets, Mask Fresheners or Coat.
*202 6715, Towels, Cloths or Lasagna?.
*202 6716, Soaps, Toothpastes, Cotton Buds and Others.
*202 6717, Cans of Coca Cola, Bottles, Waters, Novo Shoes and Others.
*202 6718, Food Shopping like Upngo, Breads or Cans of Coca Cola.
*202 6719, Quichebox, Plate or Cup or Cheese Machine.
*202 6720, Cups, Milkshakes, Jug or Can of Coca Cola?.
*202 6721, Baskets, Coathangers, Chocolate Milk or Frypan?.
*202 6722, Food Shopping like Cola Bottles, Waters, Milk or Nachos?
*202 6723, Slippers, Blanket Sheets, Clothes Pair and Others.
*202 6724, Pans, Plates, Scissors or Tools.
*202 6725, Aldi Stuff, Domino's Pizza or Lynxspray.
*202 6726, Towels, Lighter or Journal Books.
*202 6727, Food Shopping like Bottles of Cola or Water, Cans or Milk.
*202 6728, Baskets, Battery Packs or Vaporizer and Others.
*202 6729, Sleep Medicines, Deep Heat or Pillows.
*202 6730, Schnitzels, Nesquiks, Milos and Others.
*202 6731, Toothpicks and Nuts and Others.
*202 6801, Food Shopping like Bottles, Cans, Waters and Others.
*202 6802, Plates, Bowls or Battery Packs.
*202 6803, Towels, Blankets, Purse or Boots.
*202 6804, M&M's, Baskets, Cloths or Coathangers.
*202 6805, Boots, Leaves, Texture and Others.
*202 6806, Food Shopping like Cans of Coca Cola, Nachos, Cheese or Milk.
*202 6807, Dressing Gowns, Glasses or Pillows.
*202 6808, Grater Cheese, Pizza, Mat or Plate.
*202 6809, Stylus, Mask, Soaps and Others.
*202 6810, Paper Towels, Staples, Lighter and Others.
*202 6811, Baskets, Cans of Coca Cola or Biskit Shapes.
*202 6812, Blackngold Cola, Blankets or Chocolatebox.
*202 6813, Smiths Chips, Cheese Mozarella or Bread.
*202 6814, Textures, Pens or Basket and Others.
*202 6815, Smoothies, Toys, Chopping Board or Box.
*202 6816, Cups like Gold, Cup of Tea, Metal Bottle and Others.
*202 6817, Food Shopping like Water Bottles, Cans of Coca Cola, Cola Bottles or Chocolate Milk.
*202 6818, Baskets, Keys or Cordial Jug and Others.
*202 6819, Aldi Stuff, Pizza or Pumpsoap.
*202 6820, Towels, Blankets Sheets and Others.
*202 6821, Cheese, Breads, Waters or Cans of Coca Cola and Others.
*202 6822, Sleep Medicone, Quichebox, Towels and Others.
*202 6823, Shoes, Hats or Ashtray and Others.
*202 6824, Cups, Milk, Towels or some Aldi and Others.
*202 6825, Battery Pack, Clothes and Others.
*202 6826, Cans of Coca Cola, Lime Cordial, Milk and Others.
*202 6827, Blankets, Pillows, Circle Table and Others.
*202 6828, Grated Cheese, Scissors, Knifes and Forks and Others.
*202 6829, Cans of Coca Cola, Bottles, Waters, Milk and Others.
*202 6830, Plates, Stand, Frypans and Others.
*202 6831, Metal Things, Water Bottles, Cans of Coca Cola and Others.
*202 6901, Aldi Bag with Stuff or Aprons.
*202 6902, Food Shopping like Cola Bottles, Cheese with Nachos and Others.
*202 6903, Mask Perfumes, Soaps and Others.
*202 6904, Baskets, Battery Pack, Pillows or Lasagna and Others
*202 6905, Frypans, Lids, Tablemats and Others.
*202 6906, Fathers Day Presents, Glasses, Bags and Others.
*202 6907, Deep Heat, Scissors, Milo or Container and Others.
*202 6908, Baskets, Coathanger, Blankets and Others.
*202 6909, Container Biscuit, Pastas or Pans.
*202 6910, Food Shopping like Milks, M&Ms, Sauces, Coca Cola Cans and Others.
*202 6911, Pillows, Dressing Gowns, Extra Mint and Others.
*202 6912, Laundry Stuff like boxes, bags or Dad's Wrist and Others.
*202 6913, Grater Stuff, Pans and Others.
*202 6914, Bottles of Cola, Breads, Milk to make Chocolate.
*202 6915, Iced Coffee, Water Bottles, Baskets and Others.
*202 6916, Aldi Bag, Strawberry Mints and Others.
*202 6917, Lighters, Plates, Freshener or Spray and Others.
*202 6918, New Clothes, Basket, Extra Mint and Others.
*202 6919, Face Cloths, Lu Paper, Water and Others.
*202 6920, Domino's Pizza, Pillows, Notes and Others.
*202 6921, Remotes, Pizza and Barbecue Shapes, Square Pillow and Others.
*202 6922, Food Shopping like Waters, Cans of Coca Cola, Cheese, Lasagna and Others.
*202 6923, Cloths, Facecloth, New Pot and Others.
*202 6924, Aldi Stuff, Glass Cups, Candles and Others.
*202 6925, Copy Paper, Glasses, Lasagna Tray Pan and Others.
*202 6926, Tax Boxes, Bags, Lighter, Texter and Others.
*202 6927, Cans of Coca Cola, Bottles, Cordial and Others.
*202 6928, Pairs of Shoes or Slippers, Square Pillow and Others.
*202 6929, Big Spoon and Others.
*202 6930, Cola Bottle and Others.
*202 61001, Yellow Bag and Others.
*202 61002, Coathangers or Towels and Others.
*202 61003, Knives, Forks and Others.
*202 61004, Hats like DNA hat and Others.
*202 61005, Pizza SHAPES and Others.
*202 61006, Old Pillow or Cola and Others.
Current, 202 61007. ($#!+).
*202 61008. (High Speed Railway, Train)
''20261009''
f19dgmgo8bcordtyrh4dyjqud2o9ieh
769517
769516
2026-10-21T23:31:02Z
~2026-56256-37
76319
769517
wikitext
text/x-wiki
= Test =
1021 - Broken
Candy Crush Saga Wiki: Tasty Sugar Track Edits Is Out Of Control (Mostly 6/23) and until 24 Restriction until no edits made all day, Use Account Sometimes.
Up To Date
2:6 — 10:8, 242 days, (10:9+ to fill out).
*10:9, 10:10, 10:11, 10:12, 10:13 (?)
*10:14 (?)
*10:15 (?)
*10:16 (?)
*10:17 (?)
*10:18 (10:19)
''10:20''
*202 6525, Pot with Fork and Spoon, Bags with Sunglasses.
*202 6526, Baskets, Tobacco Stuff or Socks.
*202 6527, Food Shopping like Cola Bottles, Cans, Cordial and Others.
*202 6528, Socks, Pairs of Shoes, Dressing Gowns.
*202 6529, Towels, Cubes with Things.
*202 6530, Cloths, Flowers Frisun or Soap.
*202 6531, Food Shopping like Party Pies, Waters and Others.
*202 6601, Pairs of Boots, Pillow or Podiatrist.
*202 6602, Plates or Pans, Grated Cheese or Baskets.
*202 6603, Forks, Pans and Other Kitchen Tools.
*202 6604, Cups, Cordial Jug or Strawberry Milk.
*202 6605, Food Shopping like Cans of Coca Cola, Waters, Cordials or Pile of Sauce.
*202 6606, Purses, Lu Papers or Paper Towels.
*202 6607, Candles, Statues or Pizza Boxes.
*202 6608, Cups, Blankets and Others.
*202 6609, Food Shoppibg like Cola Bottles, Sour Cream and Onion Chips or Cans.
*202 6610, Towels, Cloths, Soap or Containers.
*202 6611, Nachos, Apple N Raspberry Codrial or Stationary.
*202 6612, Basket, Electric Bed with Instructions or Paint.
*202 6613, Aerobar, Birthday Cake or Packet of Candles.
*202 6614, Plates, Blankets or even Vacuum Cleaner.
*202 6615, Towels, Cloths or Dressing Gowns.
*202 6616, Smiths Chips, Heater Vents, Stuff for Lasagna or Cloths.
*202 6617, Pans, Table Spoons or Lids.
*202 6618, Food Shopping like Cans of Coca Cola, Waters, Milk and Others.
*202 6619, Chocolate Milk, Cups of Tea or Extra Mints.
*202 6620, Kitkats, Pens or Domino's Pizza.
*202 6621, Blankets and Pillows or Vacuum Cleaner.
*202 6622, Tea Towels, Cans of Coca Cola or Carlton Draught.
*202 6623, Food Shopping like Bottles of Cola, Waters or New Pyjamas.
*202 6624, Staples, Socks, Hats and Others.
*202 6625, Kitchen Bowls, Pans and Others.
*202 6626, Food Shopping like Colas, Milk or Rainbow Paddlepop.
*202 6627, Tablemats, Sunglasses or Garden Plant.
*202 6628, Boots, Sleep Medicine or Pillow.
*202 6629, Sewing Machine, Tobacco Stuff or Pan.
*202 6630, Food Shopping like Cans of Cocacola, Bottles, Kitkat or M&Ms.
*202 6701, BBQ Chips, Biskits or Grated Cheese.
*202 6702, Lighters, Plates or Cloths.
*202 6703, Hot Dogs, Cola Cans or Lasagna.
*202 6704, Mop, Battery Packs or Doorhandle.
*202 6705, Video Cameras, Internet or McDonalds Bag?.
*202 6706, Pans, Freshmint, or Hotdog Roll.
*202 6707, Food Shopping like Cans of Coca Cola, Cola Bottles, Waters, Shopping Bag or Grated Cheese.
*202 6708, Ktimes, Nutrigrain or Mueslibars.
*202 6709, Saladas, Socks, Vegemite or Textures.
*202 6710, Baskets, Favourites or Cordial Jug.
*202 6711, Shoes like Novo, Vacuum or Carlton Draught.
*202 6712, Aldi Stuff like Mints, Airfryer or Towel.
*202 6713, Food Shopping like Cola Bottles, Cheese, Milk or Paddle Pops.
*202 6714, Baskets, Mask Fresheners or Coat.
*202 6715, Towels, Cloths or Lasagna?.
*202 6716, Soaps, Toothpastes, Cotton Buds and Others.
*202 6717, Cans of Coca Cola, Bottles, Waters, Novo Shoes and Others.
*202 6718, Food Shopping like Upngo, Breads or Cans of Coca Cola.
*202 6719, Quichebox, Plate or Cup or Cheese Machine.
*202 6720, Cups, Milkshakes, Jug or Can of Coca Cola?.
*202 6721, Baskets, Coathangers, Chocolate Milk or Frypan?.
*202 6722, Food Shopping like Cola Bottles, Waters, Milk or Nachos?
*202 6723, Slippers, Blanket Sheets, Clothes Pair and Others.
*202 6724, Pans, Plates, Scissors or Tools.
*202 6725, Aldi Stuff, Domino's Pizza or Lynxspray.
*202 6726, Towels, Lighter or Journal Books.
*202 6727, Food Shopping like Bottles of Cola or Water, Cans or Milk.
*202 6728, Baskets, Battery Packs or Vaporizer and Others.
*202 6729, Sleep Medicines, Deep Heat or Pillows.
*202 6730, Schnitzels, Nesquiks, Milos and Others.
*202 6731, Toothpicks and Nuts and Others.
*202 6801, Food Shopping like Bottles, Cans, Waters and Others.
*202 6802, Plates, Bowls or Battery Packs.
*202 6803, Towels, Blankets, Purse or Boots.
*202 6804, M&M's, Baskets, Cloths or Coathangers.
*202 6805, Boots, Leaves, Texture and Others.
*202 6806, Food Shopping like Cans of Coca Cola, Nachos, Cheese or Milk.
*202 6807, Dressing Gowns, Glasses or Pillows.
*202 6808, Grater Cheese, Pizza, Mat or Plate.
*202 6809, Stylus, Mask, Soaps and Others.
*202 6810, Paper Towels, Staples, Lighter and Others.
*202 6811, Baskets, Cans of Coca Cola or Biskit Shapes.
*202 6812, Blackngold Cola, Blankets or Chocolatebox.
*202 6813, Smiths Chips, Cheese Mozarella or Bread.
*202 6814, Textures, Pens or Basket and Others.
*202 6815, Smoothies, Toys, Chopping Board or Box.
*202 6816, Cups like Gold, Cup of Tea, Metal Bottle and Others.
*202 6817, Food Shopping like Water Bottles, Cans of Coca Cola, Cola Bottles or Chocolate Milk.
*202 6818, Baskets, Keys or Cordial Jug and Others.
*202 6819, Aldi Stuff, Pizza or Pumpsoap.
*202 6820, Towels, Blankets Sheets and Others.
*202 6821, Cheese, Breads, Waters or Cans of Coca Cola and Others.
*202 6822, Sleep Medicone, Quichebox, Towels and Others.
*202 6823, Shoes, Hats or Ashtray and Others.
*202 6824, Cups, Milk, Towels or some Aldi and Others.
*202 6825, Battery Pack, Clothes and Others.
*202 6826, Cans of Coca Cola, Lime Cordial, Milk and Others.
*202 6827, Blankets, Pillows, Circle Table and Others.
*202 6828, Grated Cheese, Scissors, Knifes and Forks and Others.
*202 6829, Cans of Coca Cola, Bottles, Waters, Milk and Others.
*202 6830, Plates, Stand, Frypans and Others.
*202 6831, Metal Things, Water Bottles, Cans of Coca Cola and Others.
*202 6901, Aldi Bag with Stuff or Aprons.
*202 6902, Food Shopping like Cola Bottles, Cheese with Nachos and Others.
*202 6903, Mask Perfumes, Soaps and Others.
*202 6904, Baskets, Battery Pack, Pillows or Lasagna and Others
*202 6905, Frypans, Lids, Tablemats and Others.
*202 6906, Fathers Day Presents, Glasses, Bags and Others.
*202 6907, Deep Heat, Scissors, Milo or Container and Others.
*202 6908, Baskets, Coathanger, Blankets and Others.
*202 6909, Container Biscuit, Pastas or Pans.
*202 6910, Food Shopping like Milks, M&Ms, Sauces, Coca Cola Cans and Others.
*202 6911, Pillows, Dressing Gowns, Extra Mint and Others.
*202 6912, Laundry Stuff like boxes, bags or Dad's Wrist and Others.
*202 6913, Grater Stuff, Pans and Others.
*202 6914, Bottles of Cola, Breads, Milk to make Chocolate.
*202 6915, Iced Coffee, Water Bottles, Baskets and Others.
*202 6916, Aldi Bag, Strawberry Mints and Others.
*202 6917, Lighters, Plates, Freshener or Spray and Others.
*202 6918, New Clothes, Basket, Extra Mint and Others.
*202 6919, Face Cloths, Lu Paper, Water and Others.
*202 6920, Domino's Pizza, Pillows, Notes and Others.
*202 6921, Remotes, Pizza and Barbecue Shapes, Square Pillow and Others.
*202 6922, Food Shopping like Waters, Cans of Coca Cola, Cheese, Lasagna and Others.
*202 6923, Cloths, Facecloth, New Pot and Others.
*202 6924, Aldi Stuff, Glass Cups, Candles and Others.
*202 6925, Copy Paper, Glasses, Lasagna Tray Pan and Others.
*202 6926, Tax Boxes, Bags, Lighter, Texter and Others.
*202 6927, Cans of Coca Cola, Bottles, Cordial and Others.
*202 6928, Pairs of Shoes or Slippers, Square Pillow and Others.
*202 6929, Big Spoon and Others.
*202 6930, Cola Bottle and Others.
*202 61001, Yellow Bag and Others.
*202 61002, Coathangers or Towels and Others.
*202 61003, Knives, Forks and Others.
*202 61004, Hats like DNA hat and Others.
*202 61005, Pizza SHAPES and Others.
*202 61006, Old Pillow or Cola and Others.
Current, 202 61007. ($#!+).
*202 61008. (High Speed Railway, Train)
''20261009''
r6rif1doqzmmy1mqrh9fumuapycr19n
769518
769517
2026-10-21T23:31:17Z
~2026-56256-37
76319
769518
wikitext
text/x-wiki
= Test =
1021 - Broken Down Cars
Candy Crush Saga Wiki: Tasty Sugar Track Edits Is Out Of Control (Mostly 6/23) and until 24 Restriction until no edits made all day, Use Account Sometimes.
Up To Date
2:6 — 10:8, 242 days, (10:9+ to fill out).
*10:9, 10:10, 10:11, 10:12, 10:13 (?)
*10:14 (?)
*10:15 (?)
*10:16 (?)
*10:17 (?)
*10:18 (10:19)
''10:20''
*202 6525, Pot with Fork and Spoon, Bags with Sunglasses.
*202 6526, Baskets, Tobacco Stuff or Socks.
*202 6527, Food Shopping like Cola Bottles, Cans, Cordial and Others.
*202 6528, Socks, Pairs of Shoes, Dressing Gowns.
*202 6529, Towels, Cubes with Things.
*202 6530, Cloths, Flowers Frisun or Soap.
*202 6531, Food Shopping like Party Pies, Waters and Others.
*202 6601, Pairs of Boots, Pillow or Podiatrist.
*202 6602, Plates or Pans, Grated Cheese or Baskets.
*202 6603, Forks, Pans and Other Kitchen Tools.
*202 6604, Cups, Cordial Jug or Strawberry Milk.
*202 6605, Food Shopping like Cans of Coca Cola, Waters, Cordials or Pile of Sauce.
*202 6606, Purses, Lu Papers or Paper Towels.
*202 6607, Candles, Statues or Pizza Boxes.
*202 6608, Cups, Blankets and Others.
*202 6609, Food Shoppibg like Cola Bottles, Sour Cream and Onion Chips or Cans.
*202 6610, Towels, Cloths, Soap or Containers.
*202 6611, Nachos, Apple N Raspberry Codrial or Stationary.
*202 6612, Basket, Electric Bed with Instructions or Paint.
*202 6613, Aerobar, Birthday Cake or Packet of Candles.
*202 6614, Plates, Blankets or even Vacuum Cleaner.
*202 6615, Towels, Cloths or Dressing Gowns.
*202 6616, Smiths Chips, Heater Vents, Stuff for Lasagna or Cloths.
*202 6617, Pans, Table Spoons or Lids.
*202 6618, Food Shopping like Cans of Coca Cola, Waters, Milk and Others.
*202 6619, Chocolate Milk, Cups of Tea or Extra Mints.
*202 6620, Kitkats, Pens or Domino's Pizza.
*202 6621, Blankets and Pillows or Vacuum Cleaner.
*202 6622, Tea Towels, Cans of Coca Cola or Carlton Draught.
*202 6623, Food Shopping like Bottles of Cola, Waters or New Pyjamas.
*202 6624, Staples, Socks, Hats and Others.
*202 6625, Kitchen Bowls, Pans and Others.
*202 6626, Food Shopping like Colas, Milk or Rainbow Paddlepop.
*202 6627, Tablemats, Sunglasses or Garden Plant.
*202 6628, Boots, Sleep Medicine or Pillow.
*202 6629, Sewing Machine, Tobacco Stuff or Pan.
*202 6630, Food Shopping like Cans of Cocacola, Bottles, Kitkat or M&Ms.
*202 6701, BBQ Chips, Biskits or Grated Cheese.
*202 6702, Lighters, Plates or Cloths.
*202 6703, Hot Dogs, Cola Cans or Lasagna.
*202 6704, Mop, Battery Packs or Doorhandle.
*202 6705, Video Cameras, Internet or McDonalds Bag?.
*202 6706, Pans, Freshmint, or Hotdog Roll.
*202 6707, Food Shopping like Cans of Coca Cola, Cola Bottles, Waters, Shopping Bag or Grated Cheese.
*202 6708, Ktimes, Nutrigrain or Mueslibars.
*202 6709, Saladas, Socks, Vegemite or Textures.
*202 6710, Baskets, Favourites or Cordial Jug.
*202 6711, Shoes like Novo, Vacuum or Carlton Draught.
*202 6712, Aldi Stuff like Mints, Airfryer or Towel.
*202 6713, Food Shopping like Cola Bottles, Cheese, Milk or Paddle Pops.
*202 6714, Baskets, Mask Fresheners or Coat.
*202 6715, Towels, Cloths or Lasagna?.
*202 6716, Soaps, Toothpastes, Cotton Buds and Others.
*202 6717, Cans of Coca Cola, Bottles, Waters, Novo Shoes and Others.
*202 6718, Food Shopping like Upngo, Breads or Cans of Coca Cola.
*202 6719, Quichebox, Plate or Cup or Cheese Machine.
*202 6720, Cups, Milkshakes, Jug or Can of Coca Cola?.
*202 6721, Baskets, Coathangers, Chocolate Milk or Frypan?.
*202 6722, Food Shopping like Cola Bottles, Waters, Milk or Nachos?
*202 6723, Slippers, Blanket Sheets, Clothes Pair and Others.
*202 6724, Pans, Plates, Scissors or Tools.
*202 6725, Aldi Stuff, Domino's Pizza or Lynxspray.
*202 6726, Towels, Lighter or Journal Books.
*202 6727, Food Shopping like Bottles of Cola or Water, Cans or Milk.
*202 6728, Baskets, Battery Packs or Vaporizer and Others.
*202 6729, Sleep Medicines, Deep Heat or Pillows.
*202 6730, Schnitzels, Nesquiks, Milos and Others.
*202 6731, Toothpicks and Nuts and Others.
*202 6801, Food Shopping like Bottles, Cans, Waters and Others.
*202 6802, Plates, Bowls or Battery Packs.
*202 6803, Towels, Blankets, Purse or Boots.
*202 6804, M&M's, Baskets, Cloths or Coathangers.
*202 6805, Boots, Leaves, Texture and Others.
*202 6806, Food Shopping like Cans of Coca Cola, Nachos, Cheese or Milk.
*202 6807, Dressing Gowns, Glasses or Pillows.
*202 6808, Grater Cheese, Pizza, Mat or Plate.
*202 6809, Stylus, Mask, Soaps and Others.
*202 6810, Paper Towels, Staples, Lighter and Others.
*202 6811, Baskets, Cans of Coca Cola or Biskit Shapes.
*202 6812, Blackngold Cola, Blankets or Chocolatebox.
*202 6813, Smiths Chips, Cheese Mozarella or Bread.
*202 6814, Textures, Pens or Basket and Others.
*202 6815, Smoothies, Toys, Chopping Board or Box.
*202 6816, Cups like Gold, Cup of Tea, Metal Bottle and Others.
*202 6817, Food Shopping like Water Bottles, Cans of Coca Cola, Cola Bottles or Chocolate Milk.
*202 6818, Baskets, Keys or Cordial Jug and Others.
*202 6819, Aldi Stuff, Pizza or Pumpsoap.
*202 6820, Towels, Blankets Sheets and Others.
*202 6821, Cheese, Breads, Waters or Cans of Coca Cola and Others.
*202 6822, Sleep Medicone, Quichebox, Towels and Others.
*202 6823, Shoes, Hats or Ashtray and Others.
*202 6824, Cups, Milk, Towels or some Aldi and Others.
*202 6825, Battery Pack, Clothes and Others.
*202 6826, Cans of Coca Cola, Lime Cordial, Milk and Others.
*202 6827, Blankets, Pillows, Circle Table and Others.
*202 6828, Grated Cheese, Scissors, Knifes and Forks and Others.
*202 6829, Cans of Coca Cola, Bottles, Waters, Milk and Others.
*202 6830, Plates, Stand, Frypans and Others.
*202 6831, Metal Things, Water Bottles, Cans of Coca Cola and Others.
*202 6901, Aldi Bag with Stuff or Aprons.
*202 6902, Food Shopping like Cola Bottles, Cheese with Nachos and Others.
*202 6903, Mask Perfumes, Soaps and Others.
*202 6904, Baskets, Battery Pack, Pillows or Lasagna and Others
*202 6905, Frypans, Lids, Tablemats and Others.
*202 6906, Fathers Day Presents, Glasses, Bags and Others.
*202 6907, Deep Heat, Scissors, Milo or Container and Others.
*202 6908, Baskets, Coathanger, Blankets and Others.
*202 6909, Container Biscuit, Pastas or Pans.
*202 6910, Food Shopping like Milks, M&Ms, Sauces, Coca Cola Cans and Others.
*202 6911, Pillows, Dressing Gowns, Extra Mint and Others.
*202 6912, Laundry Stuff like boxes, bags or Dad's Wrist and Others.
*202 6913, Grater Stuff, Pans and Others.
*202 6914, Bottles of Cola, Breads, Milk to make Chocolate.
*202 6915, Iced Coffee, Water Bottles, Baskets and Others.
*202 6916, Aldi Bag, Strawberry Mints and Others.
*202 6917, Lighters, Plates, Freshener or Spray and Others.
*202 6918, New Clothes, Basket, Extra Mint and Others.
*202 6919, Face Cloths, Lu Paper, Water and Others.
*202 6920, Domino's Pizza, Pillows, Notes and Others.
*202 6921, Remotes, Pizza and Barbecue Shapes, Square Pillow and Others.
*202 6922, Food Shopping like Waters, Cans of Coca Cola, Cheese, Lasagna and Others.
*202 6923, Cloths, Facecloth, New Pot and Others.
*202 6924, Aldi Stuff, Glass Cups, Candles and Others.
*202 6925, Copy Paper, Glasses, Lasagna Tray Pan and Others.
*202 6926, Tax Boxes, Bags, Lighter, Texter and Others.
*202 6927, Cans of Coca Cola, Bottles, Cordial and Others.
*202 6928, Pairs of Shoes or Slippers, Square Pillow and Others.
*202 6929, Big Spoon and Others.
*202 6930, Cola Bottle and Others.
*202 61001, Yellow Bag and Others.
*202 61002, Coathangers or Towels and Others.
*202 61003, Knives, Forks and Others.
*202 61004, Hats like DNA hat and Others.
*202 61005, Pizza SHAPES and Others.
*202 61006, Old Pillow or Cola and Others.
Current, 202 61007. ($#!+).
*202 61008. (High Speed Railway, Train)
''20261009''
0nhqswnbuep6udy7ee6za6dkjgdzbkf
769519
769518
2026-10-21T23:31:52Z
~2026-56256-37
76319
/* Test */ All done
769519
wikitext
text/x-wiki
= Test =
1021 - Broken Down Cars
Candy Crush Saga Wiki: Tasty Sugar Track Edits Is Out Of Control (Mostly 6/23) and until 24 Restriction until no edits made all day, Use Account Sometimes.
Up To Date
2:6 — 10:8, 242 days, (10:9+ to fill out).
*10:9, 10:10, 10:11, 10:12, 10:13 (?)
*10:14 (?)
*10:15 (?)
*10:16 (?)
*10:17 (?)
*10:18 (?)
*10:19 (10:20)
''10:21''
*202 6525, Pot with Fork and Spoon, Bags with Sunglasses.
*202 6526, Baskets, Tobacco Stuff or Socks.
*202 6527, Food Shopping like Cola Bottles, Cans, Cordial and Others.
*202 6528, Socks, Pairs of Shoes, Dressing Gowns.
*202 6529, Towels, Cubes with Things.
*202 6530, Cloths, Flowers Frisun or Soap.
*202 6531, Food Shopping like Party Pies, Waters and Others.
*202 6601, Pairs of Boots, Pillow or Podiatrist.
*202 6602, Plates or Pans, Grated Cheese or Baskets.
*202 6603, Forks, Pans and Other Kitchen Tools.
*202 6604, Cups, Cordial Jug or Strawberry Milk.
*202 6605, Food Shopping like Cans of Coca Cola, Waters, Cordials or Pile of Sauce.
*202 6606, Purses, Lu Papers or Paper Towels.
*202 6607, Candles, Statues or Pizza Boxes.
*202 6608, Cups, Blankets and Others.
*202 6609, Food Shoppibg like Cola Bottles, Sour Cream and Onion Chips or Cans.
*202 6610, Towels, Cloths, Soap or Containers.
*202 6611, Nachos, Apple N Raspberry Codrial or Stationary.
*202 6612, Basket, Electric Bed with Instructions or Paint.
*202 6613, Aerobar, Birthday Cake or Packet of Candles.
*202 6614, Plates, Blankets or even Vacuum Cleaner.
*202 6615, Towels, Cloths or Dressing Gowns.
*202 6616, Smiths Chips, Heater Vents, Stuff for Lasagna or Cloths.
*202 6617, Pans, Table Spoons or Lids.
*202 6618, Food Shopping like Cans of Coca Cola, Waters, Milk and Others.
*202 6619, Chocolate Milk, Cups of Tea or Extra Mints.
*202 6620, Kitkats, Pens or Domino's Pizza.
*202 6621, Blankets and Pillows or Vacuum Cleaner.
*202 6622, Tea Towels, Cans of Coca Cola or Carlton Draught.
*202 6623, Food Shopping like Bottles of Cola, Waters or New Pyjamas.
*202 6624, Staples, Socks, Hats and Others.
*202 6625, Kitchen Bowls, Pans and Others.
*202 6626, Food Shopping like Colas, Milk or Rainbow Paddlepop.
*202 6627, Tablemats, Sunglasses or Garden Plant.
*202 6628, Boots, Sleep Medicine or Pillow.
*202 6629, Sewing Machine, Tobacco Stuff or Pan.
*202 6630, Food Shopping like Cans of Cocacola, Bottles, Kitkat or M&Ms.
*202 6701, BBQ Chips, Biskits or Grated Cheese.
*202 6702, Lighters, Plates or Cloths.
*202 6703, Hot Dogs, Cola Cans or Lasagna.
*202 6704, Mop, Battery Packs or Doorhandle.
*202 6705, Video Cameras, Internet or McDonalds Bag?.
*202 6706, Pans, Freshmint, or Hotdog Roll.
*202 6707, Food Shopping like Cans of Coca Cola, Cola Bottles, Waters, Shopping Bag or Grated Cheese.
*202 6708, Ktimes, Nutrigrain or Mueslibars.
*202 6709, Saladas, Socks, Vegemite or Textures.
*202 6710, Baskets, Favourites or Cordial Jug.
*202 6711, Shoes like Novo, Vacuum or Carlton Draught.
*202 6712, Aldi Stuff like Mints, Airfryer or Towel.
*202 6713, Food Shopping like Cola Bottles, Cheese, Milk or Paddle Pops.
*202 6714, Baskets, Mask Fresheners or Coat.
*202 6715, Towels, Cloths or Lasagna?.
*202 6716, Soaps, Toothpastes, Cotton Buds and Others.
*202 6717, Cans of Coca Cola, Bottles, Waters, Novo Shoes and Others.
*202 6718, Food Shopping like Upngo, Breads or Cans of Coca Cola.
*202 6719, Quichebox, Plate or Cup or Cheese Machine.
*202 6720, Cups, Milkshakes, Jug or Can of Coca Cola?.
*202 6721, Baskets, Coathangers, Chocolate Milk or Frypan?.
*202 6722, Food Shopping like Cola Bottles, Waters, Milk or Nachos?
*202 6723, Slippers, Blanket Sheets, Clothes Pair and Others.
*202 6724, Pans, Plates, Scissors or Tools.
*202 6725, Aldi Stuff, Domino's Pizza or Lynxspray.
*202 6726, Towels, Lighter or Journal Books.
*202 6727, Food Shopping like Bottles of Cola or Water, Cans or Milk.
*202 6728, Baskets, Battery Packs or Vaporizer and Others.
*202 6729, Sleep Medicines, Deep Heat or Pillows.
*202 6730, Schnitzels, Nesquiks, Milos and Others.
*202 6731, Toothpicks and Nuts and Others.
*202 6801, Food Shopping like Bottles, Cans, Waters and Others.
*202 6802, Plates, Bowls or Battery Packs.
*202 6803, Towels, Blankets, Purse or Boots.
*202 6804, M&M's, Baskets, Cloths or Coathangers.
*202 6805, Boots, Leaves, Texture and Others.
*202 6806, Food Shopping like Cans of Coca Cola, Nachos, Cheese or Milk.
*202 6807, Dressing Gowns, Glasses or Pillows.
*202 6808, Grater Cheese, Pizza, Mat or Plate.
*202 6809, Stylus, Mask, Soaps and Others.
*202 6810, Paper Towels, Staples, Lighter and Others.
*202 6811, Baskets, Cans of Coca Cola or Biskit Shapes.
*202 6812, Blackngold Cola, Blankets or Chocolatebox.
*202 6813, Smiths Chips, Cheese Mozarella or Bread.
*202 6814, Textures, Pens or Basket and Others.
*202 6815, Smoothies, Toys, Chopping Board or Box.
*202 6816, Cups like Gold, Cup of Tea, Metal Bottle and Others.
*202 6817, Food Shopping like Water Bottles, Cans of Coca Cola, Cola Bottles or Chocolate Milk.
*202 6818, Baskets, Keys or Cordial Jug and Others.
*202 6819, Aldi Stuff, Pizza or Pumpsoap.
*202 6820, Towels, Blankets Sheets and Others.
*202 6821, Cheese, Breads, Waters or Cans of Coca Cola and Others.
*202 6822, Sleep Medicone, Quichebox, Towels and Others.
*202 6823, Shoes, Hats or Ashtray and Others.
*202 6824, Cups, Milk, Towels or some Aldi and Others.
*202 6825, Battery Pack, Clothes and Others.
*202 6826, Cans of Coca Cola, Lime Cordial, Milk and Others.
*202 6827, Blankets, Pillows, Circle Table and Others.
*202 6828, Grated Cheese, Scissors, Knifes and Forks and Others.
*202 6829, Cans of Coca Cola, Bottles, Waters, Milk and Others.
*202 6830, Plates, Stand, Frypans and Others.
*202 6831, Metal Things, Water Bottles, Cans of Coca Cola and Others.
*202 6901, Aldi Bag with Stuff or Aprons.
*202 6902, Food Shopping like Cola Bottles, Cheese with Nachos and Others.
*202 6903, Mask Perfumes, Soaps and Others.
*202 6904, Baskets, Battery Pack, Pillows or Lasagna and Others
*202 6905, Frypans, Lids, Tablemats and Others.
*202 6906, Fathers Day Presents, Glasses, Bags and Others.
*202 6907, Deep Heat, Scissors, Milo or Container and Others.
*202 6908, Baskets, Coathanger, Blankets and Others.
*202 6909, Container Biscuit, Pastas or Pans.
*202 6910, Food Shopping like Milks, M&Ms, Sauces, Coca Cola Cans and Others.
*202 6911, Pillows, Dressing Gowns, Extra Mint and Others.
*202 6912, Laundry Stuff like boxes, bags or Dad's Wrist and Others.
*202 6913, Grater Stuff, Pans and Others.
*202 6914, Bottles of Cola, Breads, Milk to make Chocolate.
*202 6915, Iced Coffee, Water Bottles, Baskets and Others.
*202 6916, Aldi Bag, Strawberry Mints and Others.
*202 6917, Lighters, Plates, Freshener or Spray and Others.
*202 6918, New Clothes, Basket, Extra Mint and Others.
*202 6919, Face Cloths, Lu Paper, Water and Others.
*202 6920, Domino's Pizza, Pillows, Notes and Others.
*202 6921, Remotes, Pizza and Barbecue Shapes, Square Pillow and Others.
*202 6922, Food Shopping like Waters, Cans of Coca Cola, Cheese, Lasagna and Others.
*202 6923, Cloths, Facecloth, New Pot and Others.
*202 6924, Aldi Stuff, Glass Cups, Candles and Others.
*202 6925, Copy Paper, Glasses, Lasagna Tray Pan and Others.
*202 6926, Tax Boxes, Bags, Lighter, Texter and Others.
*202 6927, Cans of Coca Cola, Bottles, Cordial and Others.
*202 6928, Pairs of Shoes or Slippers, Square Pillow and Others.
*202 6929, Big Spoon and Others.
*202 6930, Cola Bottle and Others.
*202 61001, Yellow Bag and Others.
*202 61002, Coathangers or Towels and Others.
*202 61003, Knives, Forks and Others.
*202 61004, Hats like DNA hat and Others.
*202 61005, Pizza SHAPES and Others.
*202 61006, Old Pillow or Cola and Others.
Current, 202 61007. ($#!+).
*202 61008. (High Speed Railway, Train)
''20261009''
q8l4f9io32f5ksooavknmqyfnrcb7cb
Praseodymium
0
119452
769716
676583
2026-10-22T11:30:05Z
Douverill
71727
en:Praseodymium
769716
wikitext
text/x-wiki
''Praseodymium'' is a chemical element with symbol Pr and [[atomic number]] Pr in period 6. It appears as a grayish white. It is a Solid
==Chemical properties==
Praseodymium has an [[atomic mass]] of 140.907662, a boiling point of 3403, a density of 6.77, a [[melting point]] of 1208, a molar heat of 27.2, an electron configuration of 1s2 2s2 2p6 3s2 3p6 3d10 4s2 4p6 4d10 4f3 5s2 5p6 6s2, and an electron affinity of 93.
[[Category:Chemical elements]]
[[en:Praseodymium]]
srzdltrz6u3cg5po41iywn6vdakvs3o
769718
769716
2026-10-22T11:33:48Z
Douverill
71727
en:Neodymium
769718
wikitext
text/x-wiki
''Praseodymium'' is a chemical element with symbol Pr and [[atomic number]] Pr in period 6. It appears as a grayish white. It is a Solid
==Chemical properties==
Praseodymium has an [[atomic mass]] of 140.907662, a boiling point of 3403, a density of 6.77, a [[melting point]] of 1208, a molar heat of 27.2, an electron configuration of 1s2 2s2 2p6 3s2 3p6 3d10 4s2 4p6 4d10 4f3 5s2 5p6 6s2, and an electron affinity of 93.
[[Category:Chemical elements]]
[[en:Praseodymium]]
[[en:Neodymium]]
ldi9i29nd4f9m29tdzb64nxyul66d96
769719
769718
2026-10-22T11:34:26Z
Douverill
71727
en:Praseodymium <=> en:Neodymium
769719
wikitext
text/x-wiki
''Praseodymium'' is a chemical element with symbol Pr and [[atomic number]] Pr in period 6. It appears as a grayish white. It is a Solid
==Chemical properties==
Praseodymium has an [[atomic mass]] of 140.907662, a boiling point of 3403, a density of 6.77, a [[melting point]] of 1208, a molar heat of 27.2, an electron configuration of 1s2 2s2 2p6 3s2 3p6 3d10 4s2 4p6 4d10 4f3 5s2 5p6 6s2, and an electron affinity of 93.
[[Category:Chemical elements]]
[[en:Neodymium]]
[[en:Praseodymium]]
4vamh5ozxon4ornq20uiu3547wxheg0
769722
769719
2026-10-22T11:35:20Z
Douverill
71727
:en:Neodymium
769722
wikitext
text/x-wiki
''Praseodymium'' is a chemical element with symbol Pr and [[atomic number]] Pr in period 6. It appears as a grayish white. It is a Solid
==Chemical properties==
Praseodymium has an [[atomic mass]] of 140.907662, a boiling point of 3403, a density of 6.77, a [[melting point]] of 1208, a molar heat of 27.2, an electron configuration of 1s2 2s2 2p6 3s2 3p6 3d10 4s2 4p6 4d10 4f3 5s2 5p6 6s2, and an electron affinity of 93.
[[Category:Chemical elements]]
[[:en:Neodymium]]
[[en:Praseodymium]]
cnvobknor24wcpf19buks53voa2pxt6
769736
769722
2026-10-22T11:52:48Z
Douverill
71727
en:Praseodymite
769736
wikitext
text/x-wiki
''Praseodymium'' is a chemical element with symbol Pr and [[atomic number]] Pr in period 6. It appears as a grayish white. It is a Solid
==Chemical properties==
Praseodymium has an [[atomic mass]] of 140.907662, a boiling point of 3403, a density of 6.77, a [[melting point]] of 1208, a molar heat of 27.2, an electron configuration of 1s2 2s2 2p6 3s2 3p6 3d10 4s2 4p6 4d10 4f3 5s2 5p6 6s2, and an electron affinity of 93.
[[Category:Chemical elements]]
[[:en:Neodymium]]
[[en:Praseodymite]]
e7zp6abs1htxnnw25k3rta2eugzvdak
769737
769736
2026-10-22T11:55:27Z
Douverill
71727
d:Q1386, m:Praseodymium
769737
wikitext
text/x-wiki
''Praseodymium'' is a chemical element with symbol Pr and [[atomic number]] Pr in period 6. It appears as a grayish white. It is a Solid
==Chemical properties==
Praseodymium has an [[atomic mass]] of 140.907662, a boiling point of 3403, a density of 6.77, a [[melting point]] of 1208, a molar heat of 27.2, an electron configuration of 1s2 2s2 2p6 3s2 3p6 3d10 4s2 4p6 4d10 4f3 5s2 5p6 6s2, and an electron affinity of 93.
[[Category:Chemical elements]]
[[:en:Neodymium]]
[[en:Praseodymite]]
[[d:Q1386]]
[[m:Praseodymium]]
7fuf0vf2y5gucrj5dybw2ukebrz3iii
User:SongVĩ.Bot II
2
124239
769497
769300
2026-10-21T17:00:22Z
SongVĩ.Bot II
52414
[[User:SongVĩ.Bot II|Task 0]]: Đã 1759 ngày...
769497
wikitext
text/x-wiki
Cập nhật lần cuối: 22-10-2026
Đã 1759 ngày...
7ul442u0k7pi80eg8kuvxbg98tn05vs
NodeMW client
0
127112
769495
766779
2026-10-21T15:26:53Z
~2026-56227-67
76352
Testing nodemw client
769495
wikitext
text/x-wiki
Test Content 31135 --[[Special:Contributions/~2026-56227-67|~2026-56227-67]] ([[User talk:~2026-56227-67|talk]]) 15:26, 21 October 2026 (UTC)
510npkyd6f8zdnz3i5lkw6al7a2a27u
Testvinca10 Test5
0
147813
769483
552778
2026-10-21T14:18:41Z
Syunsyunminmin
53741
Protected "[[Testvinca10 Test5]]": IP・新規利用者による問題投稿の繰り返し ([Edit=Allow only autoconfirmed users] (expires 14:18, 28 October 2026 (UTC)))
552778
wikitext
text/x-wiki
Test132
5jg3yrt9p0i9anukkhtcstmkixoy4ug
769484
769483
2026-10-21T14:18:43Z
Syunsyunminmin
53741
{{pp-vandalism}}を追加 ([[Help:Twinkle|Twinkle]]使用)
769484
wikitext
text/x-wiki
{{pp-vandalism|small=yes}}
Test132
bj4v4nrjarnfiwwrffyarjjpcok2hdr
769485
769484
2026-10-21T14:19:19Z
Syunsyunminmin
53741
Removed protection from "[[Testvinca10 Test5]]": done
769484
wikitext
text/x-wiki
{{pp-vandalism|small=yes}}
Test132
bj4v4nrjarnfiwwrffyarjjpcok2hdr
769486
769485
2026-10-21T14:19:20Z
Syunsyunminmin
53741
保護(依頼)テンプレートの除去 ([[Help:Twinkle|Twinkle]]使用)
769486
wikitext
text/x-wiki
Test132
5jg3yrt9p0i9anukkhtcstmkixoy4ug
769487
769486
2026-10-21T14:20:49Z
Syunsyunminmin
53741
{{保護依頼}}を追加 ([[Help:Twinkle|Twinkle]]使用)
769487
wikitext
text/x-wiki
{{保護依頼}}
Test132
a41smwo1grkkr6h4nuxgedow42sejg3
769488
769487
2026-10-21T14:21:06Z
Syunsyunminmin
53741
Reverted edits by [[Special:Contributions/Syunsyunminmin|Syunsyunminmin]] ([[User talk:Syunsyunminmin|talk]]) to last revision by [[User:Testvinca10|Testvinca10]]
552778
wikitext
text/x-wiki
Test132
5jg3yrt9p0i9anukkhtcstmkixoy4ug
769547
769488
2026-10-22T00:43:57Z
Syunsyunminmin
53741
769547
wikitext
text/x-wiki
Test132
a
o23spss9rfeqijrlhoa8ibpqfblripg
769548
769547
2026-10-22T00:44:15Z
Syunsyunminmin
53741
差し戻し ([[Help:Twinkle|Twinkle]]使用)
769548
wikitext
text/x-wiki
Test132
5jg3yrt9p0i9anukkhtcstmkixoy4ug
User:Syunsyunminmin/common.js
2
155126
769478
769386
2026-10-21T14:01:11Z
Syunsyunminmin
53741
769478
javascript
text/javascript
mw.loader.load('http://localhost/scripts/ajaxadmintools/index.js');
mw.loader.load('http://localhost/scripts/loader.js');
importScript('User:Syunsyunminmin/Twinkle-dev.js')
rag9wwfq2gfc7bf59fgrja93rytu1os
769557
769478
2026-10-22T01:50:59Z
Syunsyunminmin
53741
769557
javascript
text/javascript
mw.loader.load('http://localhost/scripts/ajaxadmintools/index.js');
mw.loader.load('http://localhost/scripts/loader.js');
mw.loader.load('http://localhost:5500/');
7po6a169xevcl77mxdxydpn0ulc3u17
769566
769557
2026-10-22T02:20:54Z
Syunsyunminmin
53741
769566
javascript
text/javascript
mw.loader.load('http://localhost/scripts/ajaxadmintools/index.js');
mw.loader.load('http://localhost/scripts/loader.js');
importScript('User:Syunsyunminmin/Twinkle-dev.js');
//mw.loader.load('http://localhost:5500/');
bxouh8meut135x99eqxs3penjy1pc2p
769582
769566
2026-10-22T03:05:08Z
Syunsyunminmin
53741
769582
javascript
text/javascript
mw.loader.load('http://localhost/scripts/ajaxadmintools/index.js');
mw.loader.load('http://localhost/scripts/loader.js');
//importScript('User:Syunsyunminmin/Twinkle-dev.js');
mw.loader.load('http://localhost:5500/');
l8ttup53y6ivz061ww2a4g82m3fideg
User:Syunsyunminmin/twinkleoptions.js
2
155129
769549
596752
2026-10-22T00:48:59Z
Syunsyunminmin
53741
Twinkle個人設定を保存: [[:User:Syunsyunminmin/Twinkle/Preferences]]からの自動編集
769549
javascript
text/javascript
// twinkleoptions.js: Twinkleの個人設定ファイル
//
// 注意: Twinkleの設定を変更する最も簡単な方法は、
// Twinkle設定パネルを使用することです。設定パネルは[[User:Syunsyunminmin/Twinkle/Preferences]]にあります。
//
// このファイルは自動的に生成されます。
// (有効なJavaScriptの方法で構成パラメーターを変更する以外に)行った変更は、
// 次にTwinkle設定パネルで「保存」をクリックしたときに上書きされます。
// このファイルを直接変更する場合は、必ず正しいJavaScriptを使用してください。
// <nowiki>
window.Twinkle.prefs = {
"optionsVersion": 2.1,
"unlinkNamespaces": [
"0",
"10",
"100"
]
};
// </nowiki>
// twinkleoptions.js の終わり
d1c7hpq2mqd3t0wgglsrfx237zwy7sg
769550
769549
2026-10-22T00:55:53Z
Syunsyunminmin
53741
Twinkle個人設定を保存: [[:User:Syunsyunminmin/Twinkle/Preferences]]からの自動編集
769550
javascript
text/javascript
// twinkleoptions.js: Twinkleの個人設定ファイル
//
// 注意: Twinkleの設定を変更する最も簡単な方法は、
// Twinkle設定パネルを使用することです。設定パネルは[[User:Syunsyunminmin/Twinkle/Preferences]]にあります。
//
// このファイルは自動的に生成されます。
// (有効なJavaScriptの方法で構成パラメーターを変更する以外に)行った変更は、
// 次にTwinkle設定パネルで「保存」をクリックしたときに上書きされます。
// このファイルを直接変更する場合は、必ず正しいJavaScriptを使用してください。
// <nowiki>
window.Twinkle.prefs = {
"optionsVersion": 2.1,
"disabledModules": [],
"rollbackInPlace": true,
"showRollbackLinks": [
"diff",
"others",
"mine",
"history"
],
"speedySelectionStyle": "radioClick",
"watchSpeedyPages": [
"g3",
"g4"
],
"logSpeedyNominations": true,
"unlinkNamespaces": [
"0",
"10",
"100",
"2"
]
};
// </nowiki>
// twinkleoptions.js の終わり
iibxvkeqahip89lp6yb5uws6p1g2i0i
769551
769550
2026-10-22T00:58:30Z
Syunsyunminmin
53741
Twinkle個人設定を保存: [[:User:Syunsyunminmin/Twinkle/Preferences]]からの自動編集
769551
javascript
text/javascript
// twinkleoptions.js: Twinkleの個人設定ファイル
//
// 注意: Twinkleの設定を変更する最も簡単な方法は、
// Twinkle設定パネルを使用することです。設定パネルは[[User:Syunsyunminmin/Twinkle/Preferences]]にあります。
//
// このファイルは自動的に生成されます。
// (有効なJavaScriptの方法で構成パラメーターを変更する以外に)行った変更は、
// 次にTwinkle設定パネルで「保存」をクリックしたときに上書きされます。
// このファイルを直接変更する場合は、必ず正しいJavaScriptを使用してください。
// <nowiki>
window.Twinkle.prefs = {
"optionsVersion": 2.1,
"unlinkNamespaces": [
"0",
"10",
"100"
]
};
// </nowiki>
// twinkleoptions.js の終わり
d1c7hpq2mqd3t0wgglsrfx237zwy7sg
769583
769551
2026-10-22T03:47:13Z
Syunsyunminmin
53741
Twinkle個人設定を保存: [[:User:Syunsyunminmin/Twinkle/Preferences]]からの自動編集
769583
javascript
text/javascript
// twinkleoptions.js: Twinkleの個人設定ファイル
//
// 注意: Twinkleの設定を変更する最も簡単な方法は、
// Twinkle設定パネルを使用することです。設定パネルは[[User:Syunsyunminmin/Twinkle/Preferences]]にあります。
//
// このファイルは自動的に生成されます。
// (有効なJavaScriptの方法で構成パラメーターを変更する以外に)行った変更は、
// 次にTwinkle設定パネルで「保存」をクリックしたときに上書きされます。
// このファイルを直接変更する場合は、必ず正しいJavaScriptを使用してください。
// <nowiki>
window.Twinkle.prefs = {
"optionsVersion": 2.1,
"disabledModules": [],
"unlinkNamespaces": [
"0",
"10",
"100"
]
};
// </nowiki>
// twinkleoptions.js の終わり
077eaqaiof1r60zxqqfbhgqwrrutl28
ChiKtest01
0
169377
769599
726109
2026-10-22T07:34:28Z
ChiK
51271
(Tagged {{[[Unreferenced]]}})
769599
wikitext
text/x-wiki
{{Unreferenced|date=ಅಕ್ಟೋಬರ್ 2026}}
<div style="background:#fff0f5;color:#800000;border: 1px solid var(--border-color-subtle, #800000);border-radius: 8px;padding:0.7em 0.8em;
text-align:center;border-left: 4px solid #800000;">
<div style="display:inline-block; vertical-align:middle; margin-right:0.8em;">[[File:ಸ್ತ್ರೀವಾದೊ ಬುಕ್ಕೊ ಜಾನಪದೊ banner.svg|150px|link=ವಿಕಿಪೀಡಿಯ:ಸ್ತ್ರೀವಾದೊ ಬುಕ್ಕೊ ಜಾನಪದೊ 2026]]</div>
<div style="display:inline-block; vertical-align:middle; max-width:620px; text-align:left;"><div style="font-size:1.15em; font-weight:bold;text-align:center;">🌺 ಸ್ತ್ರೀವಾದೊ ಬುಕ್ಕೊ ಜಾನಪದೊ 2026</div>
<div style="font-size:0.95em;text-align:center;">
ಬರವ್ದ ಪಂತೊಡ್ <b>ಗೇನೊನ್ ಪಟ್ಟ್ದ್</b> — [[ವಿಕಿಪೀಡಿಯ:ಸ್ತ್ರೀವಾದೊ ಬುಕ್ಕೊ ಜಾನಪದೊ 2026|<span style="background-color: #fff;padding: 6px 16px;border-radius: 50px;font-weight: 700;font-size: 13px; letter-spacing: 0.5px;width: fit-content;box-shadow: 0 2px 5px rgba(0,0,0,0.2);align-self: center;">ಇನಾಮ್ ಗೆಂದ್ಲೆ!</span>]]</div></div></div>
g24cxrwjy6hli2rv23rwffu6rhmx5u6
769600
769599
2026-10-22T07:35:31Z
ChiK
51271
/* */
769600
wikitext
text/x-wiki
<div style="background:#fff0f5;color:#800000;border: 1px solid var(--border-color-subtle, #800000);border-radius: 8px;padding:0.7em 0.8em;
text-align:center;border-left: 4px solid #800000;">
<div style="display:inline-block; vertical-align:middle; margin-right:0.8em;">[[File:ಸ್ತ್ರೀವಾದೊ ಬುಕ್ಕೊ ಜಾನಪದೊ banner.svg|150px|link=ವಿಕಿಪೀಡಿಯ:ಸ್ತ್ರೀವಾದೊ ಬುಕ್ಕೊ ಜಾನಪದೊ 2026]]</div>
<div style="display:inline-block; vertical-align:middle; max-width:620px; text-align:left;"><div style="font-size:1.15em; font-weight:bold;text-align:center;">🌺 ಸ್ತ್ರೀವಾದೊ ಬುಕ್ಕೊ ಜಾನಪದೊ 2026</div>
<div style="font-size:0.95em;text-align:center;">
ಬರವ್ದ ಪಂತೊಡ್ <b>ಗೇನೊನ್ ಪಟ್ಟ್ದ್</b> — [[ವಿಕಿಪೀಡಿಯ:ಸ್ತ್ರೀವಾದೊ ಬುಕ್ಕೊ ಜಾನಪದೊ 2026|<span style="background-color: #fff;padding: 6px 16px;border-radius: 50px;font-weight: 700;font-size: 13px; letter-spacing: 0.5px;width: fit-content;box-shadow: 0 2px 5px rgba(0,0,0,0.2);align-self: center;">ಇನಾಮ್ ಗೆಂದ್ಲೆ!</span>]]</div></div></div>
q3e698il520lmodp15p056x4sq774a7
User:ChiK/TagTool.js
2
169383
769598
726081
2026-10-22T07:33:25Z
ChiK
51271
remove kn digit to Latin
769598
javascript
text/javascript
/*
*
- MIT Licence
- @ ChiK
*
*/
mw.loader.using(['mediawiki.util', 'mediawiki.api']).then(function () {
// ============================
// TULU LOCALIZATION
// ============================
const tuluMonths = ["ಜನವರಿ","ಪೆಬ್ರವರಿ","ಮಾರ್ಚಿ","ಏಪ್ರಿಲ್","ಮೇ","ಜೂನ್","ಜುಲಾಯಿ","ಆಗೋಸ್ಟು","ಸಪ್ಟೆಂಬರ್","ಅಕ್ಟೋಬರ್","ನವೆಂಬರ್","ದಸಂಬರ್"];
function getTuluDate() {
const today = new Date();
const month = tuluMonths[today.getMonth()];
const year = today.getFullYear();
return `${month} ${year}`;
}
// ============================
// TAG CATEGORIES
// ============================
const tagCategories = {
'Maintenance': ['Orphan', 'Underlinked', 'Unreferenced', 'Deadend', 'Refimprove', 'Notability', 'Expand', 'MOS', 'Stub','Not Tulu'],
'Cleanup': ['Cleanup', 'Cleanup-copyedit', 'Cleanup-reorganize', 'Cleanup-linkrot'],
'General': ['Advert', 'Autobiography', 'COI', 'Disputed', 'NPOV'],
'Inline': ['Citation needed', 'Dubious', 'Verify source', 'Who', 'When', 'Why']
};
// ============================
// INSERT TOOLBOX LINK
// ============================
$(document).ready(function () {
mw.util.addPortletLink(
'p-tb',
'#',
'TagTool',
'miniTwinkleAction',
'Open the Mini Tag Tool'
);
$('#miniTwinkleAction').on('click', function (e) {
e.preventDefault();
$('#miniTwinkleMenu').remove();
// ============================
// POPUP MENU
// ============================
const $menu =$('<div id="miniTwinkleMenu">').css({
position: 'fixed',
top: '50%',
left: '50%',
padding: '0',
background: '#f8f9fa',
border: '1px solid #ccc',
'z-index': 99999,
width: '300px',
'max-height': '80vh',
'box-sizing': 'border-box',
'overflow': 'hidden', // we control scrolling manually
'transform': 'translate(-50%, -50%)',
'display': 'flex',
'flex-direction': 'column'
});
// ============================
// HEADER + SEARCH BAR (frozen)
// ============================
const $header =$('<div>').css({
'font-weight': 'bold',
'padding': '5px',
'background': '#e9ecef',
'border-bottom': '1px solid #ccc',
'cursor': 'move'
}).text('Mini-Tag Tool');
$menu.append($header);
const $search =$('<input type="text" placeholder="Search tags...">').css({
width: 'calc(100% - 10px)',
margin: '5px',
padding: '4px',
'box-sizing': 'border-box'
});
$menu.append($search);
// ============================
// TAG LIST (scrollable)
// ============================
const $scrollContainer =$('<div>').css({
'flex': '1 1 auto',
'overflow-y': 'auto',
'padding': '5px'
});
Object.keys(tagCategories).forEach(cat => {
const $group =$('<div class="tag-group">').css({ marginBottom: '10px' });
$group.append(`<b>${cat}</b><br>`);
tagCategories[cat].forEach(tag => {
$group.append(`
<label style="display:block;">
<input type="checkbox" value="${tag}"> ${tag}
</label>
`);
});
$scrollContainer.append($group);
});
$menu.append($scrollContainer);
// Multiple issues checkbox + Add button at bottom
const $footer =$('<div>').css({ padding: '5px', borderTop: '1px solid #ccc' });
$footer.append(`
<label style="display:block; margin-bottom:5px;">
<input type="checkbox" id="mt-multiple-issues">
Wrap inside {{multiple issues}}
</label>
`);
const $applyButton =$('<button>').text('Add Tag(s)').css({ width: '100%' });
$footer.append($applyButton);
$menu.append($footer);
$('body').append($menu);
// ============================
// DRAGGABLE LOGIC
// ============================
let isDragging = false, startX, startY, startLeft, startTop;
$header.on('mousedown', function (e) {
isDragging = true;
startX = e.clientX;
startY = e.clientY;
const offset = $menu.offset();
startLeft = offset.left;
startTop = offset.top;
$menu.css('transform', 'none');
e.preventDefault();
});
$(document).on('mousemove.miniTwinkleDrag', function (e) {
if (!isDragging) return;
$menu.css({ left: startLeft + (e.clientX - startX), top: startTop + (e.clientY - startY) });
}).on('mouseup.miniTwinkleDrag', function () { isDragging = false; });
// ============================
// SEARCH FILTER
// ============================
$search.on('input', function () {
const q = $(this).val().toLowerCase();$scrollContainer.find('label').each(function () {
$(this).toggle($(this).text().toLowerCase().includes(q));
});
});
// ============================
// ADD TAGS LOGIC
// ============================
$applyButton.on('click', function () {
const selected = [];
$scrollContainer.find('input:checked').each(function () {
selected.push($(this).val());
});
if (!selected.length) return alert('Select at least one tag.');
const api = new mw.Api();
const page = mw.config.get('wgPageName');
const tuluDate = getTuluDate();
const useMI = $('#mt-multiple-issues').is(':checked') && selected.length > 1;
api.get({ action: 'query', prop: 'revisions', rvprop: 'content', titles: page }).done(function (data) {
const pg = data.query.pages;
const pgId = Object.keys(pg)[0];
let text = pg[pgId].revisions[0]['*'] || "";
const filteredTags = selected.filter(t => !new RegExp(`\\{\\{\\s*${t}\\b`, 'i').test(text));
if (!filteredTags.length) return alert("All selected tags already exist.");
const miRegex = /\{\{\s*multiple issues[\s\S]*?\n\}\}/i;
if (miRegex.test(text)) {
text = text.replace(miRegex, function (block) {
const idx = block.lastIndexOf('}}');
let newBlock = block.slice(0, idx);
filteredTags.forEach(t => newBlock += `{{${t}|date=${tuluDate}}}\n`);
newBlock += '}}';
return newBlock;
});
} else if (useMI) {
let miBlock = "{{Multiple issues|\n";
filteredTags.forEach(t => miBlock += `{{${t}|date=${tuluDate}}}\n`);
miBlock += "}}";
text = miBlock + '\n' + text;
} else {
let tagBlock = filteredTags.map(t => `{{${t}|date=${tuluDate}}}`).join("\n") + "\n";
text = tagBlock + text;
}
text = text.replace(/\n{3,}/g, "\n\n");
const summary =(useMI ? "(Tagged [[multiple issues]]: " : "(Tagged ") + filteredTags.map(t => `{{[[${t}]]}})`).join(', ');
api.postWithToken('csrf', { action: 'edit', title: page, text: text, summary: summary })
.done(() => location.reload());
});
$menu.remove();$(document).off('.miniTwinkleDrag');
});
// ============================
// CLICK OUTSIDE TO CLOSE
// ============================
setTimeout(() => {
$(document).on('click.miniTwinkle', function (e) {
if (!$(e.target).closest('#miniTwinkleMenu, #miniTwinkleAction').length) {
$menu.remove();
$(document).off('click.miniTwinkle');$(document).off('.miniTwinkleDrag');
}
});
}, 0);
});
});
});
6yw0yqoncvpg59g3crrdx0ylkec9bb3
User:Nardog/sandbox9.js
2
171161
769611
768165
2026-10-22T09:04:14Z
Nardog
40946
769611
javascript
text/javascript
window.iac?.toggle(false);
$('.insertanychar, .tool[rel="insertanychar"]').remove();
$('head > style').filter(function () {
return this.textContent.includes('.insertanychar');
}).remove();
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
(function () {
let clicked, iac;
class Iac {
constructor(context) {
this.context = context;
this.history = new OO.ui.SelectWidget({
classes: ['insertanychar-history']
}).on('choose', item => {
this.history.unselectItem(item);
this.insert(item.getData());
});
this.historyToggle = new OO.ui.ToggleButtonWidget({
framed: false,
icon: 'history',
invisibleLabel: true,
label: 'History',
value: true
}).toggle().connect(this.history, { change: 'toggle' });
this.chars = {
select: new OO.ui.SelectWidget({
classes: ['insertanychar-select']
}).on('choose', item => {
this.chars.select.unselectItem(item);
if (this.chars.loaders.includes(item)) {
this.renderChars(item);
return;
}
this.insert(item.getData());
}),
filterToggle: new OO.ui.ToggleButtonWidget({
framed: false,
icon: 'funnel',
invisibleLabel: true,
label: 'Filter characters',
value: true
}).connect(this, { change: ['onFilterToggle', 'chars'] }),
filter: new OO.ui.SearchInputWidget({
autocomplete: false,
icon: '',
title: 'Filter characters'
}).on('change', OO.ui.debounce(this.filterChars, 500)),
filterMethod: 'filterChars',
$blockLink: $('<a>').attr({
class: 'external',
rel: 'nofollow',
title: 'Open Unicode PDF',
target: '_blank'
}),
$info: $('<span>').addClass('insertanychar-info'),
reset: new OO.ui.ButtonWidget({
flags: ['destructive'],
framed: false,
label: 'Reset',
title: 'Show all characters'
}).toggle().connect(this, { click: 'resetBlock' })
};
this.compactToggle = new OO.ui.ToggleButtonWidget({
framed: false,
icon: 'viewCompact',
invisibleLabel: true,
label: 'Compact view'
}).on('change', on => {
let items = this.chars.select.getItems();
let firstVisible = this.getFirstVisible('chars', items);
this.chars.select.$element.toggleClass('insertanychar-compact', on);
let lang = this.options.lang.getValue();
items.forEach(item => {
if (!this.chars.loaders.includes(item)) {
this.renderChar(item, on, lang);
}
});
this.scroll('chars', firstVisible, {
alignToTop: true,
animate: false
});
});
this.chars.$notice = $('<div>').addClass('insertanychar-notice').append(
$('<span>').addClass('insertanychar-name').append(
this.chars.$blockLink,
this.chars.$info
),
this.chars.reset.$element
);
this.chars.tab = new OO.ui.TabPanelLayout('chars', {
$content: $([
this.chars.$notice[0],
this.chars.select.$element[0]
]),
label: 'Characters'
}).connect(this.chars.filterToggle, { active: 'toggle' })
.connect(this.chars.filter, { active: 'toggle' })
.connect(this.compactToggle, { active: 'toggle' });
this.blocks = {
select: new OO.ui.SelectWidget({
classes: ['insertanychar-select']
}).on('choose', item => {
this.blocks.select.unselectItem(item);
this.showBlock(item.getData());
this.index.setTabPanel('chars');
}),
filterToggle: new OO.ui.ToggleButtonWidget({
framed: false,
icon: 'funnel',
invisibleLabel: true,
label: 'Filter blocks',
value: true
}).toggle().connect(this, { change: ['onFilterToggle', 'blocks'] }),
filter: new OO.ui.SearchInputWidget({
autocomplete: false,
icon: '',
title: 'Filter blocks'
}).toggle().on('change', OO.ui.debounce(this.filterBlocks, 250)),
filterMethod: 'filterBlocks',
$notice: $('<div>').addClass('insertanychar-notice insertanychar-info')
};
this.blocks.tab = new OO.ui.TabPanelLayout('blocks', {
$content: $([
this.blocks.$notice[0],
this.blocks.select.$element[0]
]),
label: 'Blocks'
}).connect(this.blocks.filterToggle, { active: 'toggle' })
.connect(this.blocks.filter, { active: 'toggle' });
this.index = new OO.ui.IndexLayout({
autoFocus: false,
framed: false
}).addTabPanels([this.chars.tab, this.blocks.tab]).on('set', tab => {
let side = tab.getName();
if (this[side].filterToggle.getValue() && !OO.ui.isMobile()) {
this[side].filter.focus();
}
});
this.options = {
font: new OO.ui.DropdownWidget({
$overlay: true,
menu: {
items: [
new OO.ui.MenuOptionWidget({
data: 0,
label: 'Serif first'
}),
new OO.ui.MenuOptionWidget({
data: 1,
label: 'Sans-serif first'
})
]
}
}),
lang: new OO.ui.ComboBoxInputWidget({
$overlay: true,
options: [
{ data: '', label: '(Default)' },
{ data: 'ja' },
{ data: 'ko' },
{ data: 'vi' },
{ data: 'zh-Hans' },
{ data: 'zh-Hant' },
{ data: 'zh-Hant-HK' },
{ data: 'zh-Hant-TW' }
],
placeholder: '(Default)'
}).on('change', on => {
this.$pane.find('.insertanychar-char').attr('lang', on || null);
}),
vs: new OO.ui.DropdownWidget({
$overlay: true,
menu: {
items: [
new OO.ui.MenuOptionWidget({
data: '',
label: 'None'
})
]
}
})
};
this.options.font.getMenu().selectItemByData(0).on('choose', item => {
this.$pane.toggleClass('insertanychar-sans', !!item.getData());
});
this.options.vs.getMenu().selectItemByData('').on('choose', item => {
let v = item.getData();
this.$pane.css(
'--insertanychar-vs',
v ? `'\\${v.toString(16)}'` : ''
);
});
this.options.panel = new OO.ui.PanelLayout({
$content: $([
new OO.ui.FieldLayout(this.options.font, {
label: 'Font stack:'
}).$element[0],
new OO.ui.FieldLayout(this.options.lang, {
label: 'Language:'
}).$element[0],
new OO.ui.FieldLayout(this.options.vs, {
label: 'Variation selector:'
}).$element[0]
]),
expanded: false,
padded: true
}).toggle();
this.optionsToggle = new OO.ui.ToggleButtonWidget({
framed: false,
icon: 'textStyle',
invisibleLabel: true,
label: 'Preview options'
}).connect(this.options.panel, { change: 'toggle' });
this.chars.loaders = [
new OO.ui.MenuOptionWidget({
classes: ['insertanychar-loader'],
label: 'Load more'
}),
new OO.ui.MenuOptionWidget({
classes: ['insertanychar-loader'],
label: 'Load more'
})
];
this.chars.observer = new IntersectionObserver(entries => {
if (!entries[0].isIntersecting) return;
this.renderChars(this.chars.loaders.find(loader => (
loader.$element.is(entries[0].target))
));
}, { threshold: 0.75 });
this.chars.loaders.forEach(loader => {
this.chars.observer.observe(loader.$element[0]);
});
this.twoPaneLayout = new mw.loader.moduleRegistry['ext.wikiEditor.realtimepreview']
.packageExports['realtimepreview/TwoPaneLayout.js']();
this.$pane = this.twoPaneLayout.getPane2()
.addClass('insertanychar insertanychar-loading')
.text('Loading...');
}
ready(blocks) {
this.chars.count = 0;
this.blocks.blocks = blocks;
this.blocks.count = blocks.length;
let vs = [];
this.blocks.items = blocks.map((block, i) => {
if (block.name.startsWith('Variation Selectors')) {
vs.push(...block.entries);
}
let { sample } = block;
delete block.sample;
let $sample = $('<span>').addClass('insertanychar-char');
if (sample) {
$sample.text(Iac.renderChar(sample.v, sample.combining));
}
block.count = Iac.isRange(block.entries[0])
? block.entries[0].end - block.entries[0].start
: block.entries.length;
this.chars.count += block.count;
return new OO.ui.MenuOptionWidget({
data: block,
disabled: !block.count,
label: $([
$sample[0],
$('<span>').addClass('insertanychar-name').text(
block.name
).append(
$('<span>').addClass('insertanychar-info').text(
`${block.count.toLocaleString('en')} characters`
)
)[0],
$('<span>').addClass('insertanychar-num').text(i + 1)[0]
])
});
});
this.blocks.select.addItems(this.blocks.items);
this.options.vs.getMenu().addItems(vs.map(entry => (
new OO.ui.MenuOptionWidget({
data: entry.v,
label: `${entry.name.slice(19)} (${Iac.getHex(entry.v)})`
})
)));
this.blocks.$notice
.text(`${this.blocks.count.toLocaleString('en')} blocks`);
this.$pane.empty().removeClass('insertanychar-loading').append(
this.history.$element,
this.index.$element,
this.options.panel.$element,
$('<div>').addClass('insertanychar-bar').append(
this.historyToggle.$element,
this.chars.filterToggle.$element,
this.chars.filter.$element,
this.blocks.filterToggle.$element,
this.blocks.filter.$element,
this.optionsToggle.$element,
this.compactToggle.$element
)
).on('keydown', e => {
if (e.ctrlKey || e.shiftKey || e.altKey || e.metaKey) return;
let isEnter = e.which === 13;
let isUp = e.which === 38;
let isDown = e.which === 40;
let isHome = e.which === 36;
let isEnd = e.which === 35;
let isPgUp = e.which === 33;
let isPgDown = e.which === 34;
if (!(isEnter || isUp || isDown || isHome || isEnd || isPgUp || isPgDown)) return;
e.preventDefault();
let side = this.index.getCurrentTabPanelName();
if (isEnter) {
let highlighted = this[side].select.findHighlightedItem();
if (highlighted) {
this[side].select.chooseItem(highlighted);
}
return;
}
let item = (isUp || isDown) && this.getAdjacent(side, isUp) ||
(isPgUp || isPgDown) && this.getPageAway(side, isPgUp) ||
this.getEdge(side, isDown || isPgUp || isHome);
if (item) {
this[side].select.highlightItem(item);
this.scroll(side, item);
}
});
this.filterChars(true);
if (!OO.ui.isMobile()) {
this.chars.filter.focus();
}
}
toggle(visible = !this.visible) {
let $uiText = this.context.$ui
.toggleClass('insertanychar-enabled', visible)
.find('.wikiEditor-ui-text');
if (visible) {
$uiText.before(this.twoPaneLayout.$element);
this.twoPaneLayout.setPane1($uiText);
this.twoPaneLayout.$element.show();
this.twoPaneLayout.$element.css('height', $uiText.height() + 'px');
$uiText.css('height', '100%');
let side = this.index.getCurrentTabPanelName();
if (this[side].filterToggle.getValue() && !OO.ui.isMobile()) {
this[side].filter.focus();
}
} else {
$uiText.css('height', this.twoPaneLayout.$element.height() + 'px');
this.twoPaneLayout.$element.after($uiText).hide();
}
this.visible = visible;
this.context.$ui.find('.tool[rel="insertanychar"]').data('ooui')
.setFlags({ progressive: visible });
return this;
}
insert(data) {
this.context.$textarea.textSelection('encapsulateSelection', {
peri: String.fromCodePoint(data.v),
selectPeri: false
});
let historyItem = this.history.getItems().find(item => (
item.getData().v === data.v
)) || this.renderChar(data, true);
this.history.addItems([historyItem], 0);
this.historyToggle.toggle(true);
}
filterChars = first => {
let prevSearch = this.chars.search;
this.chars.search = new Search(
this.chars.filter.getValue(),
this.chars.filterToggle.getValue()
);
if (!first && this.block === this.prevBlock &&
this.chars.search.compare(prevSearch)
) {
return;
}
this.prevBlock = this.block;
this.chars.entries = this.block
? this.block.entries
: this.blocks.blocks.flatMap(block => block.entries);
if (!this.chars.search.valid) {
this.chars.$info.text(`${
(this.block?.count || this.chars.count).toLocaleString('en')
} characters`);
this.renderChars(null, !this.block);
return;
}
let matches = [[], [], [], [], [], []];
this.chars.entries.forEach(entry => {
if (!Iac.isRange(entry)) {
let result = this.chars.search.test(entry.name);
if (result) {
matches[result - 1].push(entry);
}
return;
}
if (!entry.isHangul && !this.chars.search.testHex(entry.name)) return;
for (let v = entry.start; v <= entry.end; v++) {
let nameToTest = Iac.getCharName(entry, v, true);
let result = this.chars.search.test(nameToTest);
if (result) {
matches[result - 1].push({
v,
name: entry.isHangul ? nameToTest : entry.name
});
}
}
});
this.chars.entries = matches.flat();
this.chars.$info.text(`${
this.chars.entries.length.toLocaleString('en')
} / ${
(this.block?.count || this.chars.count).toLocaleString('en')
} characters`);
this.renderChars();
};
renderChars(loader, startMid) {
let compact = this.compactToggle.getValue();
let lang = this.options.lang.getValue();
let prevItems = this.chars.select.getItems();
let prevCharItems = prevItems
.filter(item => !this.chars.loaders.includes(item));
let i = 0, blockV;
if (startMid) {
let firstVisible = this.getFirstVisible('chars', prevCharItems);
if (firstVisible) {
let data = firstVisible.getData();
this.chars.entries.some((entry, j) => {
if (Iac.isRange(entry)) {
if (Iac.inBlock(data.v, entry)) {
i = j;
blockV = data.v;
return true;
}
} else if (entry.v === data.v) {
i = j;
return true;
}
});
}
}
let up = loader === this.chars.loaders[0];
let increment = up ? -1 : 1;
if (loader) {
this.chars.select.removeItems(this.chars.loaders);
let adjacent = prevCharItems.at(up ? 0 : -1);
if (adjacent) {
let data = adjacent.getData();
if (Iac.isRange(data.entry)) {
i = this.chars.entries.indexOf(data.entry);
blockV = data.v + increment;
} else {
i = this.chars.entries.indexOf(data.entry) + increment;
}
}
} else {
this.chars.select.removeItems(prevItems);
}
let toAdd = [], len = 0;
do {
let entry = this.chars.entries[i];
if (!entry) break;
if (!Iac.isRange(entry)) {
len = toAdd.push(this.renderChar({
v: entry.v,
entry
}, compact, lang));
i += increment;
continue;
}
if (typeof blockV === 'number') {
if (!Iac.inBlock(blockV, entry)) {
blockV = null;
i += increment;
continue;
}
} else {
blockV = entry[up ? 'end' : 'start'];
}
len = toAdd.push(this.renderChar({
v: blockV,
entry
}, compact, lang));
blockV += increment;
} while (len < 128);
if (loader) {
let toRemove = up
? prevCharItems.slice(384 - len)
: prevCharItems.slice(0, -384 + len);
this.chars.select.removeItems(toRemove);
}
if (up) {
this.chars.select.addItems(toAdd.reverse(), 0);
} else {
this.chars.select.addItems(toAdd);
}
let reached = {};
let items = this.chars.select.getItems();
if (items.length) {
[['start', 0], ['end', -1]].forEach(([pos, idx]) => {
let data = items.at(idx).getData();
reached[pos] = data.entry === this.chars.entries.at(idx) && (
!Iac.isRange(data.entry) || data.v === data.entry[pos]
);
});
} else {
reached.start = true;
reached.end = true;
}
if (!reached.start) {
this.chars.select.addItems([this.chars.loaders[0]], 0);
}
if (!reached.end) {
this.chars.select.addItems([this.chars.loaders[1]]);
}
if (loader) {
this.scroll('chars', toAdd.at(up ? -1 : 0), {
alignToTop: up,
animate: false
});
} else {
this.chars.tab.$element[0].scrollTop = 0;
}
}
renderChar(item, compact, lang = this.options.lang.getValue()) {
let data;
if ($.isPlainObject(item)) {
data = item;
item = new OO.ui.MenuOptionWidget({ data });
data.name = Iac.getCharName(data.entry, data.v).replace(
/ (?:AND|AS|AT|BY|FOR|FROM|IN|OF|ON|OR|THE|TO|WITH)(?= )/g,
s => s.toLowerCase()
).replace(
/\b(?!CJK\b)([A-Z])([A-Z]+)\b/g,
($0, $1, $2) => $1 + $2.toLowerCase()
);
} else {
data = item.getData();
}
let block = this.block ||
this.blocks.blocks.find(b => Iac.inBlock(data.v, b));
let hex = Iac.getHex(data.v);
let $label = item.getLabel() || $('<span>').attr({
class: 'insertanychar-char',
lang: lang
}).text(Iac.renderChar(data.v, data.entry.combining));
if (!compact && !$label.children().length) {
$label = $([
$label[0],
$('<span>').addClass('insertanychar-name').text(
data.name
).append(
!this.block && $('<span>').addClass('insertanychar-block').append(
$('<a>').attr({
href: '#',
title: 'Show only this block'
}).text(block.name)
.on('click', block, this.onBlockClick)
.on('mousedown', Iac.stopPropagation)
)
)[0],
$('<span>').addClass('insertanychar-hex').text(hex)[0]
]);
}
return item.setLabel($label).setTitle(
compact ? `${data.name} (${hex})\n${block.name}` : null
);
}
showBlock(block) {
this.prevBlock = this.block;
this.block = block;
this.chars.$blockLink.attr(
'href',
`https://www.unicode.org/charts/PDF/U${Iac.getHex(block.start)}.pdf`
).text(block.name).show();
this.chars.reset.toggle(true);
if (this.chars.filterToggle.getValue()) {
this.chars.filterToggle.setValue(false);
} else {
this.filterChars();
}
}
resetBlock() {
this.prevBlock = this.block;
this.block = null;
this.chars.$blockLink.hide();
this.chars.reset.toggle(false);
this.filterChars();
}
filterBlocks = () => {
let prevSearch = this.blocks.search;
this.blocks.search = new Search(
this.blocks.filter.getValue(),
this.blocks.filterToggle.getValue()
);
if (this.blocks.search.compare(prevSearch)) return;
let items = this.blocks.select.getItems(), firstVisible;
if (prevSearch?.valid && !this.blocks.search.valid) {
firstVisible = this.getFirstVisible('chars', items);
}
let filtered = this.blocks.search?.valid;
let toShow = this.blocks.items.filter(item => !filtered || (
this.blocks.search.test(item.getData().name.toUpperCase())
));
this.blocks.select.removeItems(items).addItems(toShow);
this.blocks.$notice.text(`${
filtered ? `${toShow.length.toLocaleString('en')} / ` : ''
}${this.blocks.count.toLocaleString('en')} blocks`);
if (toShow.includes(firstVisible)) {
this.scroll('blocks', firstVisible, {
alignToTop: true,
animate: false
});
} else {
this.blocks.tab.$element[0].scrollTop = 0;
}
};
getFirstVisible(side, items) {
let y = this[side].tab.$element[0].scrollTop;
return items.find(item => item.$element[0].offsetTop >= y);
}
getEdge(side, up) {
return this[side].select.getItems().at(up ? 0 : -1);
}
getAdjacent(side, up) {
let items = this[side].select.getItems();
let highlighted = this[side].select.findHighlightedItem();
if (highlighted) {
return items[items.indexOf(highlighted) + (up ? -1 : 1)];
}
let y = this[side].tab.$element[0].scrollTop;
if (up) {
return items.findLast(item => item.$element[0].offsetTop < y);
}
y += this[side].tab.$element[0].clientHeight;
return items.find(item => (
item.$element[0].offsetTop + item.$element[0].offsetHeight > y
));
}
getPageAway(side, up) {
let items = this[side].select.getItems();
let y = this[side].tab.$element[0].scrollTop +
this[side].tab.$element[0].clientHeight * (up ? -1 : 2);
if (up) {
return items.find(item => item.$element[0].offsetTop >= y);
}
return items.findLast(item => (
item.$element[0].offsetTop + item.$element[0].offsetHeight < y
));
}
scroll(side, item, options = {}) {
if (!item) return;
options.padding = { top: this[side].$notice[0].offsetHeight };
item.scrollElementIntoView(options);
}
onFilterToggle(side, on) {
this[this[side].filterMethod]();
if (on && !OO.ui.isMobile()) {
setTimeout(() => {
this[side].filter.focus();
});
}
}
onBlockClick = e => {
e.preventDefault();
this.showBlock(e.data);
};
static stopPropagation(e) {
e.stopPropagation();
}
static getHex(v) {
return v.toString(16).toUpperCase().padStart(4, '0');
}
// https://www.unicode.org/versions/latest/core-spec/chapter-3/
static hangul = {
leads: [
'G', 'GG', 'N', 'D', 'DD', 'R', 'M', 'B', 'BB', 'S', 'SS', '',
'J', 'JJ', 'C', 'K', 'T', 'P', 'H'
],
vowels: [
'A', 'AE', 'YA', 'YAE', 'EO', 'E', 'YEO', 'YE', 'O', 'WA',
'WAE', 'OE', 'YO', 'U', 'WEO', 'WE', 'WI', 'YU', 'EU', 'YI', 'I'
],
trails: [
'', 'G', 'GG', 'GS', 'N', 'NJ', 'NH', 'D', 'L', 'LG', 'LM',
'LB', 'LS', 'LT', 'LP', 'LH', 'M', 'B', 'BS', 'S', 'SS', 'NG',
'J', 'C', 'K', 'T', 'P', 'H'
]
};
static getCharName(entry, v, appendHex) {
if (entry.isHangul) {
let i = v - entry.start;
return entry.name + ' ' + Iac.hangul.leads[Math.floor(i / 588)] +
Iac.hangul.vowels[Math.floor((i % 588) / 28)] +
Iac.hangul.trails[i % 28];
}
if (appendHex) {
return entry.name + ' ' + Iac.getHex(v);
}
return entry.name;
}
static renderChar(v, combining) {
return `${combining ? '◌' : ''}${
String.fromCodePoint(v)
}${combining === 2 ? '◌' : ''}`;
}
static inBlock(v, block) {
return v >= block.start && v <= block.end;
}
static isRange(entry) {
return !!entry && Object.hasOwn(entry, 'start');
}
}
class Search {
constructor(query, valid) {
let terms = [], negTerms = new Set();
if (valid) {
query.toUpperCase().replace(/[^-\s\dA-Z]/g, '').split(/\s+/).forEach(s => {
if (s.startsWith('-')) {
if (s !== '-') {
negTerms.add(s.slice(1));
}
} else {
terms.push(...s.split('-').filter(Boolean));
}
});
}
this.negTerms = [...negTerms].sort();
this.terms = terms.filter(t => (
!this.negTerms.some(nt => t.includes(nt))
)).sort();
this.termCount = terms.length;
this.valid = !!(this.termCount || this.negTerms.length);
}
test(s) {
if (this.negTerms.some(term => s.includes(term))) return;
let words = s.split(/[ -]/);
let exactCount = 0, initialCount = 0, partialCount = 0;
this.terms.filter(term => {
let i = words.indexOf(term);
if (i === -1) {
return true;
}
words.splice(i, 1);
exactCount++;
}).filter(term => {
let i = words.findIndex(word => word.startsWith(term));
if (i === -1) {
return true;
}
words.splice(i, 1);
initialCount++;
}).forEach(term => {
let i = words.findIndex(word => word.includes(term));
if (i === -1) return;
words.splice(i, 1);
partialCount++;
});
switch (this.termCount) {
case exactCount:
return 1;
case exactCount && initialCount && exactCount + initialCount + partialCount:
return 2;
case exactCount && exactCount + partialCount:
return 3;
case initialCount:
return 4;
case initialCount && initialCount + partialCount:
return 5;
case partialCount:
return 6;
}
}
testHex(s) {
return !this.negTerms.some(term => s.includes(term)) &&
this.terms.every(term => s.includes(term) || /^[\dA-F]{1,6}$/.test(term));
}
compare(search) {
if (!search) {
return !this.valid;
}
return search.valid === this.valid &&
String(search.terms) === String(this.terms) &&
String(search.negTerms) === String(this.negTerms);
}
}
let css = mw.loader.addStyleTag(`.wikiEditor-ui:not(.insertanychar-available) .tool[rel="insertanychar"],
.wikiEditor-ui.insertanychar-enabled .tool[rel="realtimepreview"] {
display: none;
}`);
let init = async context => {
if (clicked) {
if (iac) {
iac.context = context;
iac.toggle();
}
return;
}
clicked = true;
css.textContent += `
.insertanychar-loading {
padding: 8px !important;
}
.insertanychar-bar {
display: flex;
}
.insertanychar-bar > .oo-ui-widget {
margin: 0 !important;
}
.insertanychar-history {
display: flex;
overflow: auto;
}
.insertanychar-history > .oo-ui-menuOptionWidget {
padding: 0;
margin: 2px;
}
.insertanychar-history > .oo-ui-menuOptionWidget > .oo-ui-labelElement-label,
.insertanychar-select > .oo-ui-menuOptionWidget > .oo-ui-labelElement-label {
overflow: visible;
}
.insertanychar > .oo-ui-indexLayout {
flex-grow: 1;
position: relative;
}
.insertanychar .oo-ui-tabSelectWidget {
display: flex;
justify-content: space-evenly;
}
.insertanychar .oo-ui-tabOptionWidget {
margin-inline: 0;
width: 45%;
text-align: center;
}
.insertanychar .oo-ui-tabPanelLayout {
padding: 0;
}
.insertanychar-select > .oo-ui-menuOptionWidget {
padding-inline: 0;
}
.insertanychar-select > .oo-ui-menuOptionWidget > .oo-ui-labelElement-label {
display: flex;
align-items: center;
}
.insertanychar-char {
display: inline-block;
font-size: 200%;
min-width: 1.5em;
min-height: 1.5em;
line-height: 1.5;
text-align: center;
white-space: pre;
font-family: 'Charis','Charis SIL','Doulos SIL','Gentium','Gentium Plus','Noto Serif','Liberation Serif','Roboto Serif',serif,'Andika','Noto Sans','Liberation Sans','Roboto','Bitstream Cyberbit','Code2000','Lucida Grande','Lucida Sans Unicode',sans-serif;
}
.insertanychar-sans .insertanychar-char {
font-family: 'Andika','Noto Sans','Liberation Sans','Roboto','Bitstream Cyberbit','Code2000','Lucida Grande','Lucida Sans Unicode',sans-serif,'Charis','Charis SIL','Doulos SIL','Gentium','Gentium Plus','Noto Serif','Liberation Serif','Roboto Serif',serif;
}
.insertanychar-char::after {
content: var(--insertanychar-vs);
}
.insertanychar-history .insertanychar-char {
font-size: 150%;
}
.insertanychar-select:not(.insertanychar-compact) .insertanychar-char {
margin: 0 2px;
}
.insertanychar-name {
flex-grow: 1;
white-space: normal !important;
word-break: break-word;
}
.insertanychar-block,
.insertanychar-info {
display: block;
}
.insertanychar-block > a,
.insertanychar-block > a:visited,
.insertanychar-info,
.insertanychar-hex,
.insertanychar-num {
font-size: 85%;
color: var(--color-subtle, #54595d);
}
.insertanychar-hex,
.insertanychar-num {
margin: 0 4px;
}
.insertanychar-compact {
display: flex;
flex-wrap: wrap;
align-content: start;
align-items: center;
}
.insertanychar-compact > .oo-ui-menuOptionWidget {
padding: 0;
margin: 4px 6px;
}
.insertanychar-compact .insertanychar-name,
.insertanychar-compact .insertanychar-hex {
display: none;
}
.insertanychar-select:not(.insertanychar-compact) .insertanychar-loader {
padding-inline: 8px;
}
.insertanychar .oo-ui-toggleButtonWidget:not(.oo-ui-toggleWidget-on) + .oo-ui-textInputWidget-type-search {
visibility: hidden;
}
.insertanychar-notice {
padding: 2px 6px;
display: flex;
align-items: center;
position: sticky;
top: 0;
z-index: 1;
background-color: var(--background-color-base, #fff);
border-bottom: 1px solid var(--border-color-subtle, #c8ccd1);
}
.insertanychar-notice a {
font-weight: bold;
}`;
await mw.loader.using([
'oojs-ui-core', 'jquery.textSelection', 'ext.wikiEditor.realtimepreview',
'oojs-ui.styles.icons-content', 'oojs-ui.styles.icons-interactions',
'oojs-ui.styles.icons-editing-styling', 'oojs-ui.styles.icons-layout'
]);
iac = new Iac(context).toggle(true);
window.iac = iac;
let { source } = await $.get({
url: '//en.wiktionary.org/w/rest.php/v1/revision/92711675',
timeout: 30000
});
let blocks = [...source.matchAll(/^([\dA-F]+)\.\.([\dA-F]+); (.+)$/gm)]
.map(([, start, end, n]) => ({
start: parseInt(start, 16),
end: parseInt(end, 16),
name: n,
entries: []
}));
let blockIdx = 0;
for (let rev of ['92711695', '92711699']) {
// eslint-disable-next-line no-shadow
let { source } = await $.get({
url: `//en.wiktionary.org/w/rest.php/v1/revision/${rev}`,
timeout: 30000
});
for (let [, hex, n, cat, combClass, oldName] of source.matchAll(
/^([^;]+);([^;]*);([^;]*);([^;]*);(?:[^;]*;){6}([^;]*)/gm
)) {
let v = parseInt(hex, 16);
while (v > blocks[blockIdx].end) {
blockIdx++;
}
let block = blocks[blockIdx];
if (n?.startsWith('<')) {
if (n === '<control>') {
n = oldName;
} else {
if (block.entries.length) {
block.entries[0].end = v;
} else {
let blockName = n.match(/[^,<>]+/)?.[0].toUpperCase();
if (!blockName || blockName.endsWith(' SURROGATE')) continue;
let entry = { start: v, name: blockName };
if (blockName === 'HANGUL SYLLABLE') {
entry.isHangul = true;
}
block.entries.push(entry);
block.sample = { v, name: Iac.getCharName(entry, v) };
}
continue;
}
}
if (!n) continue;
let entry = { v, name: n };
if (cat === 'Mn') {
entry.combining = 1 + ['233', '234'].includes(combClass);
}
block.entries.push(entry);
if (!block.sample && !['C', 'Z'].includes(cat[0])) {
block.sample = entry;
}
}
}
iac.ready(blocks);
};
mw.hook('wikiEditor.toolbarReady').add(async $textarea => {
$textarea.closest('.wikiEditor-ui').toggleClass(
'insertanychar-available',
mw.config.get('wgPageContentModel') !== 'wikitext' || (
await mw.loader.using('user.options'),
!Number(mw.user.options.get('wikieditor-realtimepreview'))
)
);
$textarea.wikiEditor('addToToolbar', {
section: 'secondary',
group: 'default',
tools: {
insertanychar: {
label: 'InsertAnyChar',
type: 'toggle',
oouiIcon: 'specialCharacter',
action: { type: 'callback', execute: init }
}
}
});
});
mw.hook('ext.WikiEditor.realtimepreview.disable').add(rtp => {
rtp.context.$ui.addClass('insertanychar-available');
});
mw.hook('ext.WikiEditor.realtimepreview.enable').add(rtp => {
rtp.context.$ui.removeClass('insertanychar-available');
});
}());
d6quk2tfmj0k9cihd7bm2hmojkjlmee
769612
769611
2026-10-22T09:11:26Z
Nardog
40946
769612
javascript
text/javascript
window.iac?.toggle(false);
$('.insertanychar, .tool[rel="insertanychar"]').remove();
$('head > style').filter(function () {
return this.textContent.includes('.insertanychar');
}).remove();
['edit', 'submit'].includes(mw.config.get('wgAction')) &&
(function () {
let clicked, iac;
class Iac {
constructor(context) {
this.context = context;
this.history = new OO.ui.SelectWidget({
classes: ['insertanychar-history']
}).on('choose', item => {
this.history.unselectItem(item);
this.insert(item.getData());
});
this.historyToggle = new OO.ui.ToggleButtonWidget({
framed: false,
icon: 'history',
invisibleLabel: true,
label: 'History',
value: true
}).toggle().connect(this.history, { change: 'toggle' });
this.chars = {
select: new OO.ui.SelectWidget({
classes: ['insertanychar-select']
}).on('choose', item => {
this.chars.select.unselectItem(item);
if (this.chars.loaders.includes(item)) {
this.renderChars(item);
return;
}
this.insert(item.getData());
}),
filterToggle: new OO.ui.ToggleButtonWidget({
framed: false,
icon: 'funnel',
invisibleLabel: true,
label: 'Filter characters',
value: true
}).connect(this, { change: ['onFilterToggle', 'chars'] }),
filter: new OO.ui.SearchInputWidget({
autocomplete: false,
icon: '',
title: 'Filter characters'
}).on('change', OO.ui.debounce(this.filterChars, 500)),
filterMethod: 'filterChars',
$blockLink: $('<a>').attr({
class: 'external',
rel: 'nofollow',
title: 'Open Unicode PDF',
target: '_blank'
}),
$info: $('<span>').addClass('insertanychar-info'),
reset: new OO.ui.ButtonWidget({
flags: ['destructive'],
framed: false,
label: 'Reset',
title: 'Show all characters'
}).toggle().connect(this, { click: 'resetBlock' })
};
this.compactToggle = new OO.ui.ToggleButtonWidget({
framed: false,
icon: 'viewCompact',
invisibleLabel: true,
label: 'Compact view'
}).on('change', on => {
let items = this.chars.select.getItems();
let firstVisible = this.getFirstVisible('chars', items);
this.chars.select.$element.toggleClass('insertanychar-compact', on);
let lang = this.options.lang.getValue();
items.forEach(item => {
if (!this.chars.loaders.includes(item)) {
this.renderChar(item, on, lang);
}
});
this.scroll('chars', firstVisible, {
alignToTop: true,
animate: false
});
});
this.chars.$notice = $('<div>').addClass('insertanychar-notice').append(
$('<span>').addClass('insertanychar-name').append(
this.chars.$blockLink,
this.chars.$info
),
this.chars.reset.$element
);
this.chars.tab = new OO.ui.TabPanelLayout('chars', {
$content: $([
this.chars.$notice[0],
this.chars.select.$element[0]
]),
label: 'Characters'
}).connect(this.chars.filterToggle, { active: 'toggle' })
.connect(this.chars.filter, { active: 'toggle' })
.connect(this.compactToggle, { active: 'toggle' });
this.blocks = {
select: new OO.ui.SelectWidget({
classes: ['insertanychar-select']
}).on('choose', item => {
this.blocks.select.unselectItem(item);
this.showBlock(item.getData());
this.index.setTabPanel('chars');
}),
filterToggle: new OO.ui.ToggleButtonWidget({
framed: false,
icon: 'funnel',
invisibleLabel: true,
label: 'Filter blocks',
value: true
}).toggle().connect(this, { change: ['onFilterToggle', 'blocks'] }),
filter: new OO.ui.SearchInputWidget({
autocomplete: false,
icon: '',
title: 'Filter blocks'
}).toggle().on('change', OO.ui.debounce(this.filterBlocks, 250)),
filterMethod: 'filterBlocks',
$notice: $('<div>').addClass('insertanychar-notice insertanychar-info')
};
this.blocks.tab = new OO.ui.TabPanelLayout('blocks', {
$content: $([
this.blocks.$notice[0],
this.blocks.select.$element[0]
]),
label: 'Blocks'
}).connect(this.blocks.filterToggle, { active: 'toggle' })
.connect(this.blocks.filter, { active: 'toggle' });
this.index = new OO.ui.IndexLayout({
autoFocus: false,
framed: false
}).addTabPanels([this.chars.tab, this.blocks.tab]).on('set', tab => {
let side = tab.getName();
if (this[side].filterToggle.getValue() && !OO.ui.isMobile()) {
this[side].filter.focus();
}
});
this.options = {
font: new OO.ui.DropdownWidget({
$overlay: true,
menu: {
items: [
new OO.ui.MenuOptionWidget({
data: 0,
label: 'Serif first'
}),
new OO.ui.MenuOptionWidget({
data: 1,
label: 'Sans-serif first'
})
]
}
}),
lang: new OO.ui.ComboBoxInputWidget({
$overlay: true,
options: [
{ data: '', label: '(Default)' },
{ data: 'ja' },
{ data: 'ko' },
{ data: 'vi' },
{ data: 'zh-Hans' },
{ data: 'zh-Hant' },
{ data: 'zh-Hant-HK' },
{ data: 'zh-Hant-TW' }
],
placeholder: '(Default)'
}).on('change', on => {
this.$pane.find('.insertanychar-char').attr('lang', on || null);
}),
vs: new OO.ui.DropdownWidget({
$overlay: true,
menu: {
items: [
new OO.ui.MenuOptionWidget({
data: '',
label: 'None'
})
]
}
})
};
this.options.font.getMenu().selectItemByData(0).on('choose', item => {
this.$pane.toggleClass('insertanychar-sans', !!item.getData());
});
this.options.vs.getMenu().selectItemByData('').on('choose', item => {
let v = item.getData();
this.$pane.css(
'--insertanychar-vs',
v ? `'\\${v.toString(16)}'` : ''
);
});
this.options.panel = new OO.ui.PanelLayout({
$content: $([
new OO.ui.FieldLayout(this.options.font, {
label: 'Font stack:'
}).$element[0],
new OO.ui.FieldLayout(this.options.lang, {
label: 'Language:'
}).$element[0],
new OO.ui.FieldLayout(this.options.vs, {
label: 'Variation selector:'
}).$element[0]
]),
expanded: false,
padded: true
}).toggle();
this.optionsToggle = new OO.ui.ToggleButtonWidget({
framed: false,
icon: 'textStyle',
invisibleLabel: true,
label: 'Preview options'
}).connect(this.options.panel, { change: 'toggle' });
this.chars.loaders = [
new OO.ui.MenuOptionWidget({
classes: ['insertanychar-loader'],
label: 'Load more'
}),
new OO.ui.MenuOptionWidget({
classes: ['insertanychar-loader'],
label: 'Load more'
})
];
this.chars.observer = new IntersectionObserver(entries => {
if (!entries[0].isIntersecting) return;
this.renderChars(this.chars.loaders.find(loader => (
loader.$element.is(entries[0].target))
));
}, { threshold: 0.75 });
this.chars.loaders.forEach(loader => {
this.chars.observer.observe(loader.$element[0]);
});
this.twoPaneLayout = new mw.loader.moduleRegistry['ext.wikiEditor.realtimepreview']
.packageExports['realtimepreview/TwoPaneLayout.js']();
this.$pane = this.twoPaneLayout.getPane2()
.addClass('insertanychar insertanychar-loading')
.text('Loading...');
}
ready(blocks) {
this.chars.count = 0;
this.blocks.blocks = blocks;
this.blocks.count = blocks.length;
let vs = [];
this.blocks.items = blocks.map((block, i) => {
if (block.name.startsWith('Variation Selectors')) {
vs.push(...block.entries);
}
let { sample } = block;
delete block.sample;
let $sample = $('<span>').addClass('insertanychar-char');
if (sample) {
$sample.text(Iac.renderChar(sample.v, sample.combining));
}
block.count = Iac.isRange(block.entries[0])
? block.entries[0].end - block.entries[0].start
: block.entries.length;
this.chars.count += block.count;
return new OO.ui.MenuOptionWidget({
data: block,
disabled: !block.count,
label: $([
$sample[0],
$('<span>').addClass('insertanychar-name').text(
block.name
).append(
$('<span>').addClass('insertanychar-info').text(
`${block.count.toLocaleString('en')} characters`
)
)[0],
$('<span>').addClass('insertanychar-num').text(i + 1)[0]
])
});
});
this.blocks.select.addItems(this.blocks.items);
this.options.vs.getMenu().addItems(vs.map(entry => (
new OO.ui.MenuOptionWidget({
data: entry.v,
label: `${entry.name.slice(19)} (${Iac.getHex(entry.v)})`
})
)));
this.blocks.$notice
.text(`${this.blocks.count.toLocaleString('en')} blocks`);
this.$pane.empty().removeClass('insertanychar-loading').append(
this.history.$element,
this.index.$element,
this.options.panel.$element,
$('<div>').addClass('insertanychar-bar').append(
this.historyToggle.$element,
this.chars.filterToggle.$element,
this.chars.filter.$element,
this.blocks.filterToggle.$element,
this.blocks.filter.$element,
this.optionsToggle.$element,
this.compactToggle.$element
)
).on('keydown', e => {
if (e.ctrlKey || e.shiftKey || e.altKey || e.metaKey) return;
let isEnter = e.which === 13;
let isUp = e.which === 38;
let isDown = e.which === 40;
let isHome = e.which === 36;
let isEnd = e.which === 35;
let isPgUp = e.which === 33;
let isPgDown = e.which === 34;
if (!(isEnter || isUp || isDown || isHome || isEnd || isPgUp || isPgDown)) return;
e.preventDefault();
let side = this.index.getCurrentTabPanelName();
if (isEnter) {
let highlighted = this[side].select.findHighlightedItem();
if (highlighted) {
this[side].select.chooseItem(highlighted);
}
return;
}
let item = (isUp || isDown) && this.getAdjacent(side, isUp) ||
(isPgUp || isPgDown) && this.getPageAway(side, isPgUp) ||
this.getEdge(side, isDown || isPgUp || isHome);
if (item) {
this[side].select.highlightItem(item);
this.scroll(side, item);
}
});
this.filterChars(true);
if (!OO.ui.isMobile()) {
this.chars.filter.focus();
}
}
toggle(visible = !this.visible) {
let $uiText = this.context.$ui
.toggleClass('insertanychar-enabled', visible)
.find('.wikiEditor-ui-text');
if (visible) {
$uiText.before(this.twoPaneLayout.$element);
this.twoPaneLayout.setPane1($uiText);
this.twoPaneLayout.$element.show();
this.twoPaneLayout.$element.css('height', $uiText.height() + 'px');
$uiText.css('height', '100%');
let side = this.index.getCurrentTabPanelName();
if (this[side].filterToggle.getValue() && !OO.ui.isMobile()) {
this[side].filter.focus();
}
} else {
$uiText.css('height', this.twoPaneLayout.$element.height() + 'px');
this.twoPaneLayout.$element.after($uiText).hide();
}
this.visible = visible;
this.context.$ui.find('.tool[rel="insertanychar"]').data('ooui')
.setFlags({ progressive: visible });
return this;
}
insert(data) {
this.context.$textarea.textSelection('encapsulateSelection', {
peri: String.fromCodePoint(data.v),
selectPeri: false
});
let historyItem = this.history.getItems().find(item => (
item.getData().v === data.v
)) || this.renderChar(data, true);
this.history.addItems([historyItem], 0);
this.historyToggle.toggle(true);
}
filterChars = first => {
let prevSearch = this.chars.search;
this.chars.search = new Search(
this.chars.filter.getValue(),
this.chars.filterToggle.getValue()
);
if (!first && this.block === this.prevBlock &&
this.chars.search.compare(prevSearch)
) {
return;
}
this.prevBlock = this.block;
this.chars.entries = this.block
? this.block.entries
: this.blocks.blocks.flatMap(block => block.entries);
if (!this.chars.search.valid) {
this.chars.$info.text(`${
(this.block?.count || this.chars.count).toLocaleString('en')
} characters`);
this.renderChars(null, !this.block);
return;
}
let matches = [[], [], [], [], [], []];
this.chars.entries.forEach(entry => {
if (!Iac.isRange(entry)) {
let result = this.chars.search.test(entry.name);
if (result) {
matches[result - 1].push(entry);
}
return;
}
if (!entry.isHangul && !this.chars.search.testHex(entry.name)) return;
for (let v = entry.start; v <= entry.end; v++) {
let nameToTest = Iac.getCharName(entry, v, true);
let result = this.chars.search.test(nameToTest);
if (result) {
matches[result - 1].push({
v,
name: entry.isHangul ? nameToTest : entry.name
});
}
}
});
this.chars.entries = matches.flat();
this.chars.$info.text(`${
this.chars.entries.length.toLocaleString('en')
} / ${
(this.block?.count || this.chars.count).toLocaleString('en')
} characters`);
this.renderChars();
};
renderChars(loader, startMid) {
let compact = this.compactToggle.getValue();
let lang = this.options.lang.getValue();
let prevItems = this.chars.select.getItems();
let prevCharItems = prevItems
.filter(item => !this.chars.loaders.includes(item));
let i = 0, blockV;
if (startMid) {
let firstVisible = this.getFirstVisible('chars', prevCharItems);
if (firstVisible) {
let data = firstVisible.getData();
this.chars.entries.some((entry, j) => {
if (Iac.isRange(entry)) {
if (Iac.inBlock(data.v, entry)) {
i = j;
blockV = data.v;
return true;
}
} else if (entry.v === data.v) {
i = j;
return true;
}
});
}
}
let up = loader === this.chars.loaders[0];
let increment = up ? -1 : 1;
if (loader) {
this.chars.select.removeItems(this.chars.loaders);
let adjacent = prevCharItems.at(up ? 0 : -1);
if (adjacent) {
let data = adjacent.getData();
if (Iac.isRange(data.entry)) {
i = this.chars.entries.indexOf(data.entry);
blockV = data.v + increment;
} else {
i = this.chars.entries.indexOf(data.entry) + increment;
}
}
} else {
this.chars.select.removeItems(prevItems);
}
let toAdd = [], len = 0;
do {
let entry = this.chars.entries[i];
if (!entry) break;
if (!Iac.isRange(entry)) {
len = toAdd.push(this.renderChar({
v: entry.v,
entry
}, compact, lang));
i += increment;
continue;
}
if (typeof blockV === 'number') {
if (!Iac.inBlock(blockV, entry)) {
blockV = null;
i += increment;
continue;
}
} else {
blockV = entry[up ? 'end' : 'start'];
}
len = toAdd.push(this.renderChar({
v: blockV,
entry
}, compact, lang));
blockV += increment;
} while (len < 128);
if (loader) {
let toRemove = up
? prevCharItems.slice(384 - len)
: prevCharItems.slice(0, -384 + len);
this.chars.select.removeItems(toRemove);
}
if (up) {
this.chars.select.addItems(toAdd.reverse(), 0);
} else {
this.chars.select.addItems(toAdd);
}
let reached = {};
let items = this.chars.select.getItems();
if (items.length) {
[['start', 0], ['end', -1]].forEach(([pos, idx]) => {
let data = items.at(idx).getData();
reached[pos] = data.entry === this.chars.entries.at(idx) && (
!Iac.isRange(data.entry) || data.v === data.entry[pos]
);
});
} else {
reached.start = true;
reached.end = true;
}
if (!reached.start) {
this.chars.select.addItems([this.chars.loaders[0]], 0);
}
if (!reached.end) {
this.chars.select.addItems([this.chars.loaders[1]]);
}
if (loader) {
this.scroll('chars', toAdd.at(up ? -1 : 0), {
alignToTop: up,
animate: false
});
} else {
this.chars.tab.$element[0].scrollTop = 0;
}
}
renderChar(item, compact, lang = this.options.lang.getValue()) {
let data;
if ($.isPlainObject(item)) {
data = item;
item = new OO.ui.MenuOptionWidget({ data });
data.name = Iac.getCharName(data.entry, data.v).replace(
/ (?:AND|AS|AT|BY|FOR|FROM|IN|OF|ON|OR|THE|TO|WITH)(?= )/g,
s => s.toLowerCase()
).replace(
/\b(?!CJK\b)([A-Z])([A-Z]+)\b/g,
($0, $1, $2) => $1 + $2.toLowerCase()
);
} else {
data = item.getData();
}
let block = this.block ||
this.blocks.blocks.find(b => Iac.inBlock(data.v, b));
let hex = Iac.getHex(data.v);
let $label = item.getLabel() || $('<span>').attr({
class: 'insertanychar-char',
lang: lang
}).text(Iac.renderChar(data.v, data.entry.combining));
if (!compact && !$label.children().length) {
$label = $([
$label[0],
$('<span>').addClass('insertanychar-name').text(
data.name
).append(
!this.block && $('<span>').addClass('insertanychar-block').append(
$('<a>').attr({
href: '#',
title: 'Show only this block'
}).text(block.name)
.on('click', block, this.onBlockClick)
.on('mousedown', Iac.stopPropagation)
)
)[0],
$('<span>').addClass('insertanychar-hex').text(hex)[0]
]);
}
return item.setLabel($label).setTitle(
compact ? `${data.name} (${hex})\n${block.name}` : null
);
}
showBlock(block) {
this.prevBlock = this.block;
this.block = block;
this.chars.$blockLink.attr(
'href',
`https://www.unicode.org/charts/PDF/U${Iac.getHex(block.start)}.pdf`
).text(block.name).show();
this.chars.reset.toggle(true);
if (this.chars.filterToggle.getValue()) {
this.chars.filterToggle.setValue(false);
} else {
this.filterChars();
}
}
resetBlock() {
this.prevBlock = this.block;
this.block = null;
this.chars.$blockLink.hide();
this.chars.reset.toggle(false);
this.filterChars();
}
filterBlocks = () => {
let prevSearch = this.blocks.search;
this.blocks.search = new Search(
this.blocks.filter.getValue(),
this.blocks.filterToggle.getValue()
);
if (this.blocks.search.compare(prevSearch)) return;
let items = this.blocks.select.getItems(), firstVisible;
if (prevSearch?.valid && !this.blocks.search.valid) {
firstVisible = this.getFirstVisible('chars', items);
}
let filtered = this.blocks.search?.valid;
let toShow = this.blocks.items.filter(item => !filtered || (
this.blocks.search.test(item.getData().name.toUpperCase())
));
this.blocks.select.removeItems(items).addItems(toShow);
this.blocks.$notice.text(`${
filtered ? `${toShow.length.toLocaleString('en')} / ` : ''
}${this.blocks.count.toLocaleString('en')} blocks`);
if (toShow.includes(firstVisible)) {
this.scroll('blocks', firstVisible, {
alignToTop: true,
animate: false
});
} else {
this.blocks.tab.$element[0].scrollTop = 0;
}
};
getFirstVisible(side, items) {
let y = this[side].tab.$element[0].scrollTop;
return items.find(item => item.$element[0].offsetTop >= y);
}
getEdge(side, up) {
return this[side].select.getItems().at(up ? 0 : -1);
}
getAdjacent(side, up) {
let items = this[side].select.getItems();
let highlighted = this[side].select.findHighlightedItem();
if (highlighted) {
return items[items.indexOf(highlighted) + (up ? -1 : 1)];
}
let y = this[side].tab.$element[0].scrollTop;
if (up) {
return items.findLast(item => item.$element[0].offsetTop < y);
}
y += this[side].tab.$element[0].clientHeight;
return items.find(item => (
item.$element[0].offsetTop + item.$element[0].offsetHeight > y
));
}
getPageAway(side, up) {
let items = this[side].select.getItems();
let y = this[side].tab.$element[0].scrollTop +
this[side].tab.$element[0].clientHeight * (up ? -1 : 2);
if (up) {
return items.find(item => item.$element[0].offsetTop >= y);
}
return items.findLast(item => (
item.$element[0].offsetTop + item.$element[0].offsetHeight < y
));
}
scroll(side, item, options = {}) {
if (!item) return;
options.padding = { top: this[side].$notice[0].offsetHeight };
item.scrollElementIntoView(options);
}
onFilterToggle(side, on) {
this[this[side].filterMethod]();
if (on && !OO.ui.isMobile()) {
setTimeout(() => {
this[side].filter.focus();
});
}
}
onBlockClick = e => {
e.preventDefault();
this.showBlock(e.data);
};
static stopPropagation(e) {
e.stopPropagation();
}
static getHex(v) {
return v.toString(16).toUpperCase().padStart(4, '0');
}
// https://www.unicode.org/versions/latest/core-spec/chapter-3/
static hangul = {
leads: [
'G', 'GG', 'N', 'D', 'DD', 'R', 'M', 'B', 'BB', 'S', 'SS', '',
'J', 'JJ', 'C', 'K', 'T', 'P', 'H'
],
vowels: [
'A', 'AE', 'YA', 'YAE', 'EO', 'E', 'YEO', 'YE', 'O', 'WA',
'WAE', 'OE', 'YO', 'U', 'WEO', 'WE', 'WI', 'YU', 'EU', 'YI', 'I'
],
trails: [
'', 'G', 'GG', 'GS', 'N', 'NJ', 'NH', 'D', 'L', 'LG', 'LM',
'LB', 'LS', 'LT', 'LP', 'LH', 'M', 'B', 'BS', 'S', 'SS', 'NG',
'J', 'C', 'K', 'T', 'P', 'H'
]
};
static getCharName(entry, v, appendHex) {
if (entry.isHangul) {
let i = v - entry.start;
return entry.name + ' ' + Iac.hangul.leads[Math.floor(i / 588)] +
Iac.hangul.vowels[Math.floor((i % 588) / 28)] +
Iac.hangul.trails[i % 28];
}
if (appendHex) {
return entry.name + ' ' + Iac.getHex(v);
}
return entry.name;
}
static renderChar(v, combining) {
return `${combining ? '◌' : ''}${
String.fromCodePoint(v)
}${combining === 2 ? '◌' : ''}`;
}
static inBlock(v, block) {
return v >= block.start && v <= block.end;
}
static isRange(entry) {
return !!entry && Object.hasOwn(entry, 'start');
}
}
class Search {
constructor(query, valid) {
let terms = [], negTerms = new Set();
if (valid) {
query.toUpperCase().replace(/[^-\s\dA-Z]/g, '').split(/\s+/).forEach(s => {
if (s.startsWith('-')) {
if (s !== '-') {
negTerms.add(s.slice(1));
}
} else {
terms.push(...s.split('-').filter(Boolean));
}
});
}
this.negTerms = [...negTerms].sort();
this.terms = terms.filter(t => (
!this.negTerms.some(nt => t.includes(nt))
)).sort();
this.termCount = terms.length;
this.valid = !!(this.termCount || this.negTerms.length);
}
test(s) {
if (this.negTerms.some(term => s.includes(term))) return;
let words = s.split(/[ -]/);
let exactCount = 0, initialCount = 0, partialCount = 0;
this.terms.filter(term => {
let i = words.indexOf(term);
if (i === -1) {
return true;
}
words.splice(i, 1);
exactCount++;
}).filter(term => {
let i = words.findIndex(word => word.startsWith(term));
if (i === -1) {
return true;
}
words.splice(i, 1);
initialCount++;
}).forEach(term => {
let i = words.findIndex(word => word.includes(term));
if (i === -1) return;
words.splice(i, 1);
partialCount++;
});
switch (this.termCount) {
case exactCount:
return 1;
case exactCount && initialCount && exactCount + initialCount + partialCount:
return 2;
case exactCount && exactCount + partialCount:
return 3;
case initialCount:
return 4;
case initialCount && initialCount + partialCount:
return 5;
case partialCount:
return 6;
}
}
testHex(s) {
return !this.negTerms.some(term => s.includes(term)) &&
this.terms.every(term => s.includes(term) || /^[\dA-F]{1,6}$/.test(term));
}
compare(search) {
if (!search) {
return !this.valid;
}
return search.valid === this.valid &&
String(search.terms) === String(this.terms) &&
String(search.negTerms) === String(this.negTerms);
}
}
let css = mw.loader.addStyleTag(`.wikiEditor-ui:not(.insertanychar-available) .tool[rel="insertanychar"],
.wikiEditor-ui.insertanychar-enabled .tool[rel="realtimepreview"] {
display: none;
}`);
let init = async context => {
if (clicked) {
if (iac) {
iac.context = context;
iac.toggle();
}
return;
}
clicked = true;
css.textContent += `
.insertanychar-loading {
padding: 8px !important;
}
.insertanychar-bar {
display: flex;
}
.insertanychar-bar > .oo-ui-widget {
margin: 0 !important;
}
.insertanychar-history {
display: flex;
overflow: auto;
}
.insertanychar-history > .oo-ui-menuOptionWidget {
padding: 0;
margin: 2px;
}
.insertanychar-history > .oo-ui-menuOptionWidget > .oo-ui-labelElement-label,
.insertanychar-select > .oo-ui-menuOptionWidget > .oo-ui-labelElement-label {
overflow: visible;
}
.insertanychar > .oo-ui-indexLayout {
flex-grow: 1;
position: relative;
}
.insertanychar .oo-ui-tabSelectWidget {
display: flex;
justify-content: space-evenly;
}
.insertanychar .oo-ui-tabOptionWidget {
margin-inline: 0;
width: 45%;
text-align: center;
}
.insertanychar .oo-ui-tabPanelLayout {
padding: 0;
}
.insertanychar-select > .oo-ui-menuOptionWidget {
padding-inline: 0;
}
.insertanychar-select > .oo-ui-menuOptionWidget > .oo-ui-labelElement-label {
display: flex;
align-items: center;
}
.insertanychar-char {
display: inline-block;
font-size: 200%;
min-width: 1.5em;
min-height: 1.5em;
line-height: 1.5;
text-align: center;
white-space: pre;
font-family: 'Charis','Charis SIL','Doulos SIL','Gentium','Gentium Plus','Noto Serif','Liberation Serif','Roboto Serif',serif,'Andika','Noto Sans','Liberation Sans','Roboto','Bitstream Cyberbit','Code2000','Lucida Grande','Lucida Sans Unicode',sans-serif;
}
.insertanychar-sans .insertanychar-char {
font-family: 'Andika','Noto Sans','Liberation Sans','Roboto','Bitstream Cyberbit','Code2000','Lucida Grande','Lucida Sans Unicode',sans-serif,'Charis','Charis SIL','Doulos SIL','Gentium','Gentium Plus','Noto Serif','Liberation Serif','Roboto Serif',serif;
}
.insertanychar-char::after {
content: var(--insertanychar-vs);
}
.insertanychar-history .insertanychar-char {
font-size: 150%;
}
.insertanychar-select:not(.insertanychar-compact) .insertanychar-char {
margin: 0 2px;
}
.insertanychar-name {
flex-grow: 1;
white-space: normal !important;
word-break: break-word;
}
.insertanychar-block,
.insertanychar-info {
display: block;
}
.insertanychar-block > a,
.insertanychar-block > a:visited,
.insertanychar-info,
.insertanychar-hex,
.insertanychar-num {
font-size: 85%;
color: var(--color-subtle, #54595d);
}
.insertanychar-hex,
.insertanychar-num {
margin: 0 4px;
}
.insertanychar-compact {
display: flex;
flex-wrap: wrap;
align-content: start;
align-items: center;
}
.insertanychar-compact > .oo-ui-menuOptionWidget {
padding: 0;
margin: 4px 6px;
}
.insertanychar-compact .insertanychar-name,
.insertanychar-compact .insertanychar-hex {
display: none;
}
.insertanychar-select:not(.insertanychar-compact) .insertanychar-loader {
padding-inline: 8px;
}
.insertanychar .oo-ui-toggleButtonWidget:not(.oo-ui-toggleWidget-on) + .oo-ui-textInputWidget-type-search {
visibility: hidden;
}
.insertanychar-notice {
padding: 2px 6px;
display: flex;
align-items: center;
position: sticky;
top: 0;
z-index: 1;
background-color: var(--background-color-base, #fff);
border-bottom: 1px solid var(--border-color-subtle, #c8ccd1);
}
.insertanychar-notice a {
font-weight: bold;
}`;
await mw.loader.using([
'oojs-ui-core', 'jquery.textSelection', 'ext.wikiEditor.realtimepreview',
'oojs-ui.styles.icons-content', 'oojs-ui.styles.icons-interactions',
'oojs-ui.styles.icons-editing-styling', 'oojs-ui.styles.icons-layout'
]);
iac = new Iac(context).toggle(true);
window.iac = iac;
let { source } = await $.get({
url: '//en.wiktionary.org/w/rest.php/v1/revision/92711675',
timeout: 30000
});
let blocks = [...source.matchAll(/^([\dA-F]+)\.\.([\dA-F]+); (.+)$/gm)]
.map(([, start, end, name]) => ({
start: parseInt(start, 16),
end: parseInt(end, 16),
name,
entries: []
}));
let blockIdx = 0;
for (let rev of ['92711695', '92711699']) {
// eslint-disable-next-line no-shadow
let { source } = await $.get({
url: `//en.wiktionary.org/w/rest.php/v1/revision/${rev}`,
timeout: 30000
});
for (let [, hex, name, cat, combClass, oldName] of source.matchAll(
/^([^;]+);([^;]*);([^;]*);([^;]*);(?:[^;]*;){6}([^;]*)/gm
)) {
let v = parseInt(hex, 16);
while (v > blocks[blockIdx].end) {
blockIdx++;
}
let block = blocks[blockIdx];
if (name?.startsWith('<')) {
if (name === '<control>') {
name = oldName;
} else {
if (block.entries.length) {
block.entries[0].end = v;
} else {
let blockName = name.match(/[^,<>]+/)?.[0].toUpperCase();
if (!blockName || blockName.endsWith(' SURROGATE')) continue;
let entry = { start: v, name: blockName };
if (blockName === 'HANGUL SYLLABLE') {
entry.isHangul = true;
}
block.entries.push(entry);
block.sample = { v, name: Iac.getCharName(entry, v) };
}
continue;
}
}
if (!name) continue;
let entry = { v, name };
if (cat === 'Mn') {
entry.combining = 1 + ['233', '234'].includes(combClass);
}
block.entries.push(entry);
if (!block.sample && !['C', 'Z'].includes(cat[0])) {
block.sample = entry;
}
}
}
iac.ready(blocks);
};
mw.hook('wikiEditor.toolbarReady').add(async $textarea => {
$textarea.closest('.wikiEditor-ui').toggleClass(
'insertanychar-available',
mw.config.get('wgPageContentModel') !== 'wikitext' || (
await mw.loader.using('user.options'),
!Number(mw.user.options.get('wikieditor-realtimepreview'))
)
);
$textarea.wikiEditor('addToToolbar', {
section: 'secondary',
group: 'default',
tools: {
insertanychar: {
label: 'InsertAnyChar',
type: 'toggle',
oouiIcon: 'specialCharacter',
action: { type: 'callback', execute: init }
}
}
});
});
mw.hook('ext.WikiEditor.realtimepreview.disable').add(rtp => {
rtp.context.$ui.addClass('insertanychar-available');
});
mw.hook('ext.WikiEditor.realtimepreview.enable').add(rtp => {
rtp.context.$ui.removeClass('insertanychar-available');
});
}());
9twa9imgcsccsakoe3sf7lgmkk5a4w8
In a Minor Way
0
175237
769438
769336
2026-10-21T12:47:01Z
~2026-56264-26
76350
/* Track listing */
769438
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]]
*[[jamaican hip-hop]]
}}
| 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 [[Tha Lion Pound]] groupmates Frisco Kid and [[Rayvon]], and his son [[Snoop Dogg|Snoop Doggy Dogg]].
==Track listing==
{{Track listing
| all_writing =
| extra_column = Producer(s)
| title1 = Intro
| writer1 = {{hlist|[[Snoop Lion (rapper)|Calvin Broadus II]]|Robert Livingston|[[Shaun Pizzonia]]}}
| extra1 = {{hlist|Robert Livingston|[[Shaun Pizzonia|Shaun "Sting" Pizzonia]]}}
| length1 = 1:46
| title2 = Chop in da Phone
| writer2 = C. Broadus II
| extra2 = [[Snoop Lion (rapper)|Snoop Liony Lion]]
| length2 = 0:16
| title3 = No Vaseline
| writer3 = {{hlist|C. Broadus II|R. Livingston|S. Pizzonia}}
| extra3 = {{hlist|Robert Livingston|Shaun "Sting Pizzonia}}
| length3 = 4:14
| title4 = My Summer Vacation
| note4 = featuring Frisco Kid and Mr. Easy
| writer4 = {{hlist|C. Broadus II|Steve Wray|Lloyd James Jr.}}
| extra4 = Lloyd "John John" James Jr.
| length4 = 4:28
| title5 = Gangsta Bird
| writer5 = {{hlist|C. Broadus II|[[Dave Kelly (producer)|Dave Kelly]]}}
| extra5 = [[Dave Kelly (producer)|Dave Kelly]]
| length5 = 4:44
| title6 = [[Mr. Loverman (song)|Mr. Loverman]]
| note6 = featuring [[Rayvon]]
| writer6 = {{hlist|C. Broadus II|[[Rayvon|Bruce Brewster]]|Livingston|Pizzonia}}
| extra6 = {{hlist|Robert Livingston|Shaun "Sting" Pizzonia}}
| length6 = 4:12
| title7 = Outta Rounds
| writer7 = C. Broadus II
| extra7 = Snoop Liony Lion
| length7 = 0:44
| title8 = Ting-a-Ling
| note8 = featuring [[Ice Cube]], [[Shabba Ranks]] and [[Shaggy (musician)|Shaggy]]
| writer8 = {{hlist|C. Broadus II|[[Shaggy (musician)|Orville Burrell]]|[[Ice Cube|O'Shea Jackson]]|[[Shabba Ranks|Rexton Gordon]]|R. Livingston|S. Pizzonia}}
| extra8 = {{hlist|Robert Livingston|Shaun "Sting" Pizzonia}}
| length8 = 4:32
| title9 = [[Foe Life (song)|Foe Life]]
| note9 = featuring [[RikRok]]
| writer9 = {{hlist|C. Broadus II|D. Kelly|[[RikRok|Ricardo Ducent]]|[[Rob Hyman]]|[[Eric Bazilian]]|[[Rick Chertoff]]}}
| extra9 = Dave Kelly
| length9 = 5:10
| title10 = Look Who's Burnin'
| writer10 = {{hlist|C. Broadus II|Harold Browne|R. Duncent}}
| extra10 = {{hlist|Danny Browne|D. Juvenile|[[Richie Stephens]] <small>({{abbr|co.|co-producer}})</small>}}
| length10 = 4:20
| title11 = Man's Best Friend
| writer11 = {{hlist|C. Broadus II|D. Kelly}}
| extra11 = Dave Kelly
| length11 = 4:34
| title12 = Enemy
| note12 = featuring Rayvon
| writer12 = {{hlist|C. Broadus II|L. James Jr.|[[Jimmy Jam and Terry Lewis|James Harris III]]|[[Jimmy Jam and Terry Lewis|Terry Lewis]]}}
| extra12 = Lloyd "John John" James Jr.
| length12 = 5:14
| title13 = A Bird in the Hand
| note13 = featuring Frisco Kid and [[Cutty Ranks]]
| writer13 = {{hlist|C. Broadus II|S. Wray|[[Cutty Ranks|Philip Thomas]]|D. Kelly}}
| extra13 = Dave Kelly
| length13 = 4:46
| title14 = Don't Trust 'Em
| writer14 = {{hlist|C. Broadus II|D. Kelly}}
| extra14 = Dave Kelly
| length14 = 4:18
| title15 = It's a Man's World
| writer15 = {{hlist|C. Broadus II|[[Snoop Dogg|Calvin Broadus II]]|L. James Jr.}}
| extra15 = Lloyd "John John" James Jr.
| length15 = 3:30
| title16 = Outro
| writer16 = {{hlist|C. Broadus II|S. Wray|D. Kelly}}
| extra16 = Dave Kelly
| length16 = 2:28
| total_length =
}}
==Personnel==
==Charts==
===Weekly charts===
===Year-end charts===
==Certifications==
==References==
{{Reflist}}
{{Snoop Lion (rapper)}}
{{Authority control}}
[[Category:Snoop Lion (rapper) albums]]
[[Category:1995 debut albums]]
[[Category:Albums produced by Dave Kelly (producer)]]
[[Category:Priarity Records albums]]
[[Category:Lionystyle Records albums]]
nfy3hb165tahj9ma3tkou7tjh1dodop
769440
769438
2026-10-21T12:47:38Z
Jaheim Island
76075
/* Track listing */
769440
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]]
*[[jamaican hip-hop]]
}}
| 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 [[Tha Lion Pound]] groupmates Frisco Kid and [[Rayvon]], and his son [[Snoop Dogg|Snoop Doggy Dogg]].
==Track listing==
{{Track listing
| all_writing =
| extra_column = Producer(s)
| title1 = Intro
| writer1 = {{hlist|[[Snoop Lion (rapper)|Calvin Broadus II]]|Robert Livingston|[[Shaun Pizzonia]]}}
| extra1 = {{hlist|Robert Livingston|[[Shaun Pizzonia|Shaun "Sting" Pizzonia]]}}
| length1 = 1:46
| title2 = Chop in da Phone
| writer2 = C. Broadus II
| extra2 = [[Snoop Lion (rapper)|Snoop Liony Lion]]
| length2 = 0:16
| title3 = No Vaseline
| writer3 = {{hlist|C. Broadus II|R. Livingston|S. Pizzonia}}
| extra3 = {{hlist|Robert Livingston|Shaun "Sting Pizzonia}}
| length3 = 4:14
| title4 = My Summer Vacation
| note4 = featuring Frisco Kid and Mr. Easy
| writer4 = {{hlist|C. Broadus II|Steve Wray|Lloyd James Jr.}}
| extra4 = Lloyd "John John" James Jr.
| length4 = 4:28
| title5 = Gangsta Bird
| writer5 = {{hlist|C. Broadus II|[[Dave Kelly (producer)|Dave Kelly]]}}
| extra5 = [[Dave Kelly (producer)|Dave Kelly]]
| length5 = 4:44
| title6 = [[Mr. Loverman (song)|Mr. Loverman]]
| note6 = featuring [[Rayvon]]
| writer6 = {{hlist|C. Broadus II|[[Rayvon|Bruce Brewster]]|Livingston|Pizzonia}}
| extra6 = {{hlist|Robert Livingston|Shaun "Sting" Pizzonia}}
| length6 = 4:12
| title7 = Outta Rounds
| writer7 = C. Broadus II
| extra7 = Snoop Liony Lion
| length7 = 0:44
| title8 = Ting-a-Ling
| note8 = featuring [[Ice Cube]], [[Shabba Ranks]] and [[Shaggy (musician)|Shaggy]]
| writer8 = {{hlist|C. Broadus II|[[Shaggy (musician)|Orville Burrell]]|[[Ice Cube|O'Shea Jackson]]|[[Shabba Ranks|Rexton Gordon]]|R. Livingston|S. Pizzonia}}
| extra8 = {{hlist|Robert Livingston|Shaun "Sting" Pizzonia}}
| length8 = 4:32
| title9 = [[Foe Life (song)|Foe Life]]
| note9 = featuring [[RikRok]]
| writer9 = {{hlist|C. Broadus II|D. Kelly|[[RikRok|Ricardo Ducent]]|[[Rob Hyman]]|[[Eric Bazilian]]|[[Rick Chertoff]]}}
| extra9 = Dave Kelly
| length9 = 5:10
| title10 = Look Who's Burnin'
| writer10 = {{hlist|C. Broadus II|Harold Browne|R. Duncent}}
| extra10 = {{hlist|Danny Browne|D. Juvenile|[[Richie Stephens]] <small>({{abbr|co.|co-producer}})</small>}}
| length10 = 4:20
| title11 = Man's Best Friend
| writer11 = {{hlist|C. Broadus II|D. Kelly}}
| extra11 = Dave Kelly
| length11 = 4:34
| title12 = Enemy
| note12 = featuring Rayvon
| writer12 = {{hlist|C. Broadus II|L. James Jr.|[[Jimmy Jam and Terry Lewis|James Harris III]]|[[Jimmy Jam and Terry Lewis|Terry Lewis]]}}
| extra12 = Lloyd "John John" James Jr.
| length12 = 5:14
| title13 = A Bird in the Hand
| note13 = featuring Frisco Kid and [[Cutty Ranks]]
| writer13 = {{hlist|C. Broadus II|S. Wray|[[Cutty Ranks|Philip Thomas]]|D. Kelly}}
| extra13 = Dave Kelly
| length13 = 4:46
| title14 = Don't Trust 'Em
| writer14 = {{hlist|C. Broadus II|D. Kelly}}
| extra14 = Dave Kelly
| length14 = 4:18
| title15 = It's a Man's World
| writer15 = {{hlist|C. Broadus II|[[Snoop Dogg|Calvin Broadus]]|L. James Jr.}}
| extra15 = Lloyd "John John" James Jr.
| length15 = 3:30
| title16 = Outro
| writer16 = {{hlist|C. Broadus II|S. Wray|D. Kelly}}
| extra16 = Dave Kelly
| length16 = 2:28
| total_length =
}}
==Personnel==
==Charts==
===Weekly charts===
===Year-end charts===
==Certifications==
==References==
{{Reflist}}
{{Snoop Lion (rapper)}}
{{Authority control}}
[[Category:Snoop Lion (rapper) albums]]
[[Category:1995 debut albums]]
[[Category:Albums produced by Dave Kelly (producer)]]
[[Category:Priarity Records albums]]
[[Category:Lionystyle Records albums]]
qyfwfsgqi120jje45mg1q1s1dzqtkpm
User:OUT is this/common.js
2
175239
769443
768266
2026-10-21T12:59:17Z
OUT is this
73801
769443
javascript
text/javascript
//JSロード高速化 [[:en:User:SD0001/userscript-load-caching.js]]
mw.loader.load( '/w/index.php?title=User:SD0001/userscript-load-caching.js&action=raw&ctype=text/javascript' );
window.scriptManagerList = [
// [[:test:User:OUT_is_this/WikiFontsAddonBigdataTest.js]]
{ name: 'OUT is this/WikiFontsAddonBigdataTest.js', lang: 'test', title: 'User:OUT_is_this/WikiFontsAddonBigdataTest.js' },
// [[:test:User:OUT_is_this/WikiFontsAddon.js]]
{ name: 'WikiFontsAddon', lang: 'test', title: 'User:OUT_is_this/WikiFontsAddon.js' },
// [[:test:User:OUT_is_this/ArticleInfo_(Japanese)_.js]]
{ name: '記事情報を追加', lang: 'test', title: 'User:OUT_is_this/ArticleInfo_(Japanese)_.js' },
// [[:test:User:OUT is this/SimpleBookmark.js]]
{ name: 'OUT is this/SimpleBookmark.js', lang: 'test', title: 'User:OUT is this/SimpleBookmark.js' },
{ name: 'スクリプトサンドボックス', title: '利用者:Nanona15dobato/script/sandbox.js' },
{ name: 'フォント切り替え', lang: 'en', title: 'User:PerryPerson/WikiFonts.js' },
{ name: 'カスタムウォッチリストバッジ', lang: 'test', title: 'User:OUT is this/watchlistCustomBadge.js' },
];
//テスト
//絞り込みおまかせ表示 [[利用者:OUT is this/advancedRandom.js]]
mw.loader.load( '/w/index.php?title=利用者:OUT is this/advancedRandom.js&action=raw&ctype=text/javascript' );
/* --- マネージャー本体の読み込み --- インポート元:[[利用者:OUT_is_this/ScriptManager.js]]*/
mw.loader.load( '//test.wikipedia.org/w/index.php?title=User:OUT_is_this/ScriptManager.js&action=raw&ctype=text/javascript' );
188hwb4vvqpp7s6u2fgvcw45lo89o4r
769444
769443
2026-10-21T13:02:03Z
OUT is this
73801
769444
javascript
text/javascript
//JSロード高速化 [[:en:User:SD0001/userscript-load-caching.js]]
mw.loader.load( '/w/index.php?title=User:SD0001/userscript-load-caching.js&action=raw&ctype=text/javascript' );
window.scriptManagerList = [
// [[:test:User:OUT_is_this/WikiFontsAddonBigdataTest.js]]
{ name: 'OUT is this/WikiFontsAddonBigdataTest.js', lang: 'test', title: 'User:OUT_is_this/WikiFontsAddonBigdataTest.js' },
// [[:test:User:OUT_is_this/WikiFontsAddon.js]]
{ name: 'WikiFontsAddon', lang: 'test', title: 'User:OUT_is_this/WikiFontsAddon.js' },
// [[:test:User:OUT_is_this/ArticleInfo_(Japanese)_.js]]
{ name: '記事情報を追加', lang: 'test', title: 'User:OUT_is_this/ArticleInfo_(Japanese)_.js' },
// [[:test:User:OUT is this/SimpleBookmark.js]]
{ name: 'OUT is this/SimpleBookmark.js', lang: 'test', title: 'User:OUT is this/SimpleBookmark.js' },
{ name: 'スクリプトサンドボックス', title: '利用者:Nanona15dobato/script/sandbox.js' },
{ name: 'フォント切り替え', lang: 'en', title: 'User:PerryPerson/WikiFonts.js' },
{ name: 'カスタムウォッチリストバッジ', lang: 'test', title: 'User:OUT is this/watchlistCustomBadge.js' },
];
//テスト
//絞り込みおまかせ表示 [[利用者:OUT is this/advancedRandom.js]]
mw.loader.load( '/w/index.php?title=利用者:OUT is this/advancedRandom.js&action=raw&ctype=text/javascript' );
/* --- マネージャー本体の読み込み --- インポート元:[[User:OUT_is_this/ScriptManager.js]]*/
mw.loader.load( '//test.wikipedia.org/w/index.php?title=User:OUT_is_this/ScriptManager.js&action=raw&ctype=text/javascript' );
g6ue0a9fyhrevwcjbc6or2yfrqqvulw
Test
0
177681
769423
769224
2026-10-21T12:00:29Z
Mahmoud Abdelsattar (WMDE)
75745
showcaptcha
769423
wikitext
text/x-wiki
Hello worldg
This is a test
More testing
This is now the 3rd test!
Final Test
After final test
4vqzq8vedkjunjf0g76ixj33yu2tvmn
769427
769423
2026-10-21T12:41:11Z
Mahmoud Abdelsattar (WMDE)
75745
769427
wikitext
text/x-wiki
Hello worldg
This is a test
More testing
This is now the 3rd test!
Final Test
After final test!
3chosup3f9cq9gmp5vfqy9dx4w9dcls
769428
769427
2026-10-21T12:41:40Z
Mahmoud Abdelsattar (WMDE)
75745
769428
wikitext
text/x-wiki
Hello worldg
This is a test
More testing
This is now the 3rd test!
Final Test!
After final test!
jbelyq1d2xapulxbknwwvuwhd2j3c09
769429
769428
2026-10-21T12:41:50Z
Mahmoud Abdelsattar (WMDE)
75745
769429
wikitext
text/x-wiki
Hello worldg
This is a test
More testing!
This is now the 3rd test!
Final Test!
After final test!
8tfvzdm3nxfvgpup5httbk77mu08szt
769430
769429
2026-10-21T12:42:01Z
Mahmoud Abdelsattar (WMDE)
75745
769430
wikitext
text/x-wiki
Hello worldg
This is a test!
More testing!
This is now the 3rd test!
Final Test!
After final test!
dwm7041ymz4aken9b65gt6kvd1ws7qw
769431
769430
2026-10-21T12:42:12Z
Mahmoud Abdelsattar (WMDE)
75745
769431
wikitext
text/x-wiki
Hello world!
This is a test!
More testing!
This is now the 3rd test!
Final Test!
After final test!
1axeb1pwk10xlx3rprjo5fl5a09ual7
769432
769431
2026-10-21T12:42:28Z
Mahmoud Abdelsattar (WMDE)
75745
769432
wikitext
text/x-wiki
Hello world!
This is a test!
More testing!
This is now the 3rd test!
Final Test!
After final test.
ek4510rtx0w39up6dtjuysju5o02mam
769433
769432
2026-10-21T12:42:39Z
Mahmoud Abdelsattar (WMDE)
75745
769433
wikitext
text/x-wiki
Hello world!
This is a test!
More testing!
This is now the 3rd test.
Final Test!
After final test.
0z95d129by0lf4cmfynvd0zlr47s91v
769434
769433
2026-10-21T12:42:52Z
Mahmoud Abdelsattar (WMDE)
75745
769434
wikitext
text/x-wiki
Hello world!
This is a test.
More testing!
This is now the 3rd test.
Final Test!
After final test.
lhftc9ilfai05q8d8puf5982c6anwac
769435
769434
2026-10-21T12:43:17Z
Mahmoud Abdelsattar (WMDE)
75745
769435
wikitext
text/x-wiki
Hello world!
This is a test!
More testing!
This is now the 3rd test!
Final Test!
After final test.
ek4510rtx0w39up6dtjuysju5o02mam
769436
769435
2026-10-21T12:43:35Z
Mahmoud Abdelsattar (WMDE)
75745
769436
wikitext
text/x-wiki
Hello world!
This is a test!
More testing!
This is now the 3rd test!
Final Test!
After final test
frlz7hhlw33whmdpwxkcsmznbri4w7c
User:Laerdon/ConvoWizardTesting.js
2
177690
769585
764832
2026-10-22T05:41:24Z
Laerdon
70887
769585
javascript
text/javascript
//<nowiki>
/**
* Filename: wiki-talk-page-codex.js
* ConvoCompass LLM – Newcomer-oriented talk-page assistance (Wikipedia userscript)
*
* Codex-styled ConvoCompass. Renders the server's plain-text llm_response as
* guidance; a JSON array of policy page titles is still accepted and shown as
* nutshells (from the ConvoWizardRouting fork), but JSON is never required.
*/
(async function () {
'use strict';
// ===== ConvoCompass Codex styling (injected) =====
(function injectCodexStyles() {
if (document.getElementById('convocompass-codex-styles')) return;
const css = `
.cc-panel{margin:var(--spacing-50,8px) 0;padding:var(--spacing-75,12px) var(--spacing-100,16px);border-radius:var(--border-radius-base,2px);border:1px solid var(--border-color-base,#a2a9b1);background-color:var(--background-color-neutral-subtle,#f8f9fa);color:var(--color-base,#202122);font-family:var(--font-family-system,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif);font-size:14px;line-height:1.5;}
.cc-panel__header{display:flex;align-items:center;gap:var(--spacing-50,8px);font-weight:600;font-size:14px;color:var(--color-base,#202122);margin-bottom:var(--spacing-50,8px);}
.cc-panel__icon{color:var(--color-subtle,#54595d);flex-shrink:0;}
.cc-btn svg{flex-shrink:0;}
.cc-panel__title{flex:1;}
.cc-panel__body{font-size:14px;}
.cc-link-item{margin:0 0 var(--spacing-50,8px) 0;}
.cc-link-item:last-child{margin-bottom:0;}
.cc-link{font-weight:600;color:var(--color-progressive,#36c);text-decoration:none;}
.cc-link:hover{text-decoration:underline;}
.cc-reason{color:var(--color-subtle,#54595d);}
.cc-btn{display:inline-flex;align-items:center;gap:6px;padding:5px 10px;font-size:13px;font-weight:500;border:1px solid var(--border-color-base,#a2a9b1);border-radius:var(--border-radius-base,2px);background-color:var(--background-color-base,#fff);color:var(--color-base,#202122);cursor:pointer;line-height:1.2;font-family:var(--font-family-system,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif);}
.cc-btn:hover{background-color:var(--background-color-neutral-subtle,#f8f9fa);border-color:var(--color-progressive,#36c);}
.cc-btn--quiet{border:1px solid var(--border-color-base,#a2a9b1);background-color:var(--background-color-base,#fff);color:var(--color-subtle,#54595d);padding:4px 8px;font-size:12px;}
.cc-btn--quiet:hover{background-color:var(--background-color-neutral-subtle,#f8f9fa);border-color:var(--color-progressive,#36c);}
.cc-btn--progressive{background-color:var(--color-progressive,#36c);border-color:var(--color-progressive,#36c);color:#fff;}
.cc-btn--progressive:hover{background-color:var(--color-progressive--hover,#2a4b8d);border-color:var(--color-progressive--hover,#2a4b8d);}
.cc-btn--muted{background-color:var(--background-color-neutral-subtle,#f8f9fa);}
.cc-note{margin:var(--spacing-50,8px) 0;padding:var(--spacing-50,8px) var(--spacing-75,12px);border-radius:var(--border-radius-base,2px);border:1px solid var(--border-color-subtle,#c8ccd1);background-color:var(--background-color-neutral-subtle,#f8f9fa);color:var(--color-subtle,#54595d);font-size:12px;font-style:italic;display:flex;align-items:center;gap:6px;cursor:pointer;font-family:var(--font-family-system,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif);}
.cc-note:hover{border-color:var(--color-progressive,#36c);}
.cc-form{margin:var(--spacing-50,8px) 0;padding:var(--spacing-100,16px);border-radius:var(--border-radius-base,2px);border:1px solid var(--border-color-progressive,#36c);background-color:var(--background-color-base,#fff);font-family:var(--font-family-system,-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif);}
.cc-form__title{font-weight:600;font-size:14px;color:var(--color-base,#202122);margin-bottom:var(--spacing-50,8px);}
.cc-form__desc{font-size:13px;color:var(--color-subtle,#54595d);margin-bottom:var(--spacing-75,12px);line-height:1.5;}
.cc-input{flex:1;padding:6px 8px;font-size:13px;font-family:monospace;border:1px solid var(--border-color-base,#a2a9b1);border-radius:var(--border-radius-base,2px);background-color:var(--background-color-base,#fff);color:var(--color-base,#202122);outline:none;box-sizing:border-box;}
.cc-input:focus{border-color:var(--border-color-progressive,#36c);box-shadow:inset 0 0 0 1px var(--border-color-progressive,#36c);}
.cc-textarea{width:100%;min-height:120px;resize:vertical;margin-top:0.5em;}
.cc-row{display:flex;gap:var(--spacing-50,8px);align-items:center;}
.cc-error{font-size:12px;color:var(--color-error,#bf3c2c);margin-top:var(--spacing-50,8px);}
.cc-noshrink{flex-shrink:0;}
.cc-card__buttons{display:flex;gap:8px;}
.cc-card__buttons--floating{margin-left:auto;}
.cc-card{margin:1em 0;}
`;
const el = document.createElement('style');
el.id = 'convocompass-codex-styles';
el.textContent = css;
document.head.appendChild(el);
})();
// Load core util module via ResourceLoader
await mw.loader.using(['mediawiki.util', 'mediawiki.user', 'mediawiki.api', 'mediawiki.Title']);
const mwApi = new mw.Api();
const codexList = await mwApi.get( {
action: 'query',
list: 'codexicons',
names: 'cdxIconCode|cdxIconRobot|cdxIconVolumeOff|cdxIconVolumeUp'
} );
const codexicons = codexList.query.codexicons;
// /llm/ is served on toolforge (gemini lives there). /api/ still proxies
// the live craft study to cornell and is not used by this script.
const SERVER = 'https://convocompass.toolforge.org/llm/';
// ---- UI strings and thresholds ----
const NAME = 'ConvoCompass';
const DEBUG = /(?:\?|&)convowizardDebug=1(?:&|$)/.test(location.search);
const OPTION_KEY = 'userjs-convowizard-token';
const YAML_OPTION_KEY = 'userjs-convocompass-assistant-yaml';
const MAX_YAML_CHARS = 32768;
// matches MAX_ARTICLE_CHARS on toolforge; the server clips again regardless
const MAX_ARTICLE_CHARS = 16384;
const SUMMARY_PLACEHOLDER = `${NAME} is reading this discussion and will summarize it here.`;
const GUIDANCE_PLACEHOLDER = `As you write, ${NAME} will suggest Wikipedia pages here that are worth reading for this discussion.`;
const ASSISTANT_PLACEHOLDER = `${NAME} will write guidance here from your yaml.`;
// Only links whose URL passes this prefix check are ever rendered. The server
// builds URLs from its own whitelist; this is defense in depth.
const ALLOWED_LINK_PREFIX = 'https://en.wikipedia.org/wiki/Wikipedia:';
// Keep ConvoWizard logs quiet by default to reduce console noise on Wikimedia pages.
const nativeConsole = window.console;
const console = {
log: (...args) => {
if (!DEBUG && typeof args[0] === 'string' && args[0].startsWith(`[${NAME}]`)) {
return;
}
nativeConsole.log(...args);
},
warn: (...args) => nativeConsole.warn(...args),
error: (...args) => nativeConsole.error(...args)
};
const isWikipediaHost = /(^|\.)wikipedia\.org$/.test(location.hostname) || location.hostname === 'meta.wikimedia.org';
const namespaceNumber = Number(mw.config.get('wgNamespaceNumber'));
const isTalkNamespace = Number.isFinite(namespaceNumber) && namespaceNumber % 2 === 1;
const isViewAction = mw.config.get('wgAction') === 'view';
// Gadget scope guard: only run on Wikipedia talk pages in normal view mode.
if (!isWikipediaHost || !isViewAction) {
console.log(
`[${NAME}] Skipping page (host=${location.hostname}, ns=${namespaceNumber}, action=${mw.config.get('wgAction')})`
);
return;
}
// ---------- helpers ----------
// ---- send pacing: wait for a pause in typing (shorter after a sentence ends), keep one request in
// flight, and stay under the server's per-token interval (3 s).
const SENTENCE_END = /[.!?…]["'”’)\]]*$/;
const MIN_DRAFT_CHARS = 10;
const PACE_MS = 3500;
// ---- draft comparison: trivial edits (case, spacing, trailing punctuation, a few more characters)
// don't warrant a new round, and a reply to such a draft still applies.
const norm = (t) => (t || '').toLowerCase().replace(/\s+/g, ' ').replace(/[\s.,!?…;:'"”’)\]]+$/, '').trim();
function similar(a, b) {
const x = norm(a), y = norm(b);
if (!x || !y) return x === y;
const [short, long] = x.length <= y.length ? [x, y] : [y, x];
return long.startsWith(short) && long.length - short.length <= 6;
}
function cdxToSvg(icon, color, className) {
const iconSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
iconSvg.setAttribute('width', '20');
iconSvg.setAttribute('height', '20');
iconSvg.setAttribute('viewBox', '0 0 20 20');
iconSvg.setAttribute('fill', color ? color : 'currentColor');
iconSvg.setAttribute('class', ((className || '') + ' cc-noshrink').trim());
iconSvg.innerHTML = icon;
return iconSvg;
}
// Rewrite Wikipedia URL so the reddit-style backend can extract a post_id.
// Backend does: url.split("comments/")[1][:6]
// We inject "comments/<hash>" where hash is derived from the wiki page path.
function wikiUrlToPostId(href) {
try {
const pagePath = href.split('/wiki/')[1] || href;
// Simple hash to produce a stable 6-char hex id per page
let h = 0;
for (let i = 0; i < pagePath.length; i++) {
h = ((h << 5) - h) + pagePath.charCodeAt(i);
h = h & h;
}
const hex = Math.abs(h).toString(16).padStart(6, '0').slice(0, 6);
return href.split('/wiki/')[0] + '/comments/' + hex;
} catch {
return href;
}
}
async function postJson(route, body) {
try {
const res = await fetch(SERVER + route, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const text = await res.text();
nativeConsole.log(`[${NAME}] response from ${route}: status=${res.status}`, text);
try {
const data = JSON.parse(text);
nativeConsole.log(`[${NAME}] json from ${route}:`, data);
return data;
} catch (err) {
console.warn(`[${NAME}] non-json response from ${route}:`, text);
return {};
}
} catch (err) {
console.warn(`[${NAME}] request failed for ${route}:`, err);
return {};
}
}
function getTokenFromOptions() {
try {
if (mw.user && mw.user.options && typeof mw.user.options.get === 'function') {
const raw = mw.user.options.get(OPTION_KEY);
if (typeof raw === 'string') {
const trimmed = raw.trim();
return trimmed.length > 0 ? trimmed : null;
}
}
} catch (err) {
console.warn(`[${NAME}] Failed to read token from user options`, err);
}
return null;
}
async function persistToken(storageKey, token) {
if (!token) {
localStorage.removeItem(storageKey);
} else {
localStorage.setItem(storageKey, token);
}
if (!(mw.user && mw.user.options && typeof mw.user.options.set === 'function')) {
return;
}
try {
mw.user.options.set(OPTION_KEY, token || '');
await new mw.Api().saveOption(OPTION_KEY, token || '');
console.log(`[${NAME}] ${token ? 'Saved' : 'Cleared'} token in user options (notepad)`);
} catch (err) {
console.warn(`[${NAME}] Could not persist token to user options`, err);
}
}
async function validateToken(token, username) {
try {
const response = await postJson('claim_token', { token, username });
return response.valid === true;
} catch {
return false;
}
}
// ========== CREDENTIAL SETUP ==========
const USERNAME = mw.config.get('wgUserName');
if (!USERNAME) {
console.log(`[${NAME}] Not logged in, skipping`);
return;
}
let TOKEN = null;
const STORAGE_KEY = `ConvoWizard:token:${USERNAME}`;
// Load persisted token (options → localStorage), validate, sync stores
async function loadPersistedToken() {
let token = getTokenFromOptions();
let source = 'options';
if (!token) {
token = localStorage.getItem(STORAGE_KEY);
source = 'localStorage';
}
if (token && typeof token === 'string') {
token = token.trim();
}
if (!token) return;
const valid = await validateToken(token, USERNAME);
if (valid) {
TOKEN = token;
// Sync both stores
if (source === 'options') {
localStorage.setItem(STORAGE_KEY, token);
} else {
await persistToken(STORAGE_KEY, token);
}
console.log(`[${NAME}] Token loaded from ${source}`);
} else {
console.log(`[${NAME}] Persisted token invalid, clearing`);
await persistToken(STORAGE_KEY, null);
}
}
await loadPersistedToken();
let ASSISTANT_YAML = '';
const YAML_STORAGE_KEY = `ConvoCompass:assistantYaml:${USERNAME}`;
function getYamlFromOptions() {
try {
if (mw.user && mw.user.options && typeof mw.user.options.get === 'function') {
const raw = mw.user.options.get(YAML_OPTION_KEY);
if (typeof raw === 'string' && raw.trim()) return raw;
}
} catch (err) {
console.warn(`[${NAME}] failed to read yaml from user options`, err);
}
return null;
}
async function persistYaml(yaml) {
if (!yaml) {
localStorage.removeItem(YAML_STORAGE_KEY);
} else {
localStorage.setItem(YAML_STORAGE_KEY, yaml);
}
// user options can reject large/multiline values; localstorage is the source of truth
if (!(mw.user && mw.user.options && typeof mw.user.options.set === 'function')) {
return;
}
try {
mw.user.options.set(YAML_OPTION_KEY, yaml || '');
await new mw.Api().saveOption(YAML_OPTION_KEY, yaml || '');
} catch (err) {
console.warn(`[${NAME}] could not persist yaml to user options`, err);
}
}
function loadPersistedYaml() {
const fromOptions = getYamlFromOptions();
const fromStorage = localStorage.getItem(YAML_STORAGE_KEY);
const raw = (fromOptions || fromStorage || '').trim();
if (raw && raw.length <= MAX_YAML_CHARS) {
ASSISTANT_YAML = raw;
if (!fromOptions && fromStorage) {
persistYaml(fromStorage);
} else if (fromOptions) {
localStorage.setItem(YAML_STORAGE_KEY, fromOptions);
}
}
}
// article text for the yaml ARTICLE_PAGE block: a plaintext extract of this
// talk page's subject page, fetched once per page load and only while a yaml
// is active (the non-yaml path never uses it).
let ARTICLE_PAGE_TEXT = '';
let articlePagePromise = null;
function ensureArticlePage() {
if (!ASSISTANT_YAML || articlePagePromise) return articlePagePromise;
articlePagePromise = (async () => {
try {
const title = new mw.Title(mw.config.get('wgPageName'));
const subject = title.getSubjectPage();
// non-talk pages are their own subject page; nothing to fetch there
if (!subject || subject.getPrefixedText() === title.getPrefixedText()) return;
const resp = await new mw.Api().get({
action: 'query',
prop: 'extracts',
explaintext: 1,
exsectionformat: 'plain',
redirects: 1,
titles: subject.getPrefixedText(),
formatversion: 2
});
const page = resp && resp.query && resp.query.pages && resp.query.pages[0];
if (page && !page.missing && typeof page.extract === 'string' && page.extract.trim()) {
ARTICLE_PAGE_TEXT = page.extract.trim().slice(0, MAX_ARTICLE_CHARS);
console.log(`[${NAME}] Article extract loaded for "${subject.getPrefixedText()}" (${ARTICLE_PAGE_TEXT.length} chars)`);
} else {
console.log(`[${NAME}] No article extract available for "${subject.getPrefixedText()}"`);
}
} catch (err) {
console.warn(`[${NAME}] Failed to fetch article extract`, err);
}
})();
return articlePagePromise;
}
function llmFields() {
const fields = {};
const yaml = (ASSISTANT_YAML || '').trim();
if (yaml) {
fields.assistant_yaml = yaml;
fields.topic_name = mw.config.get('wgTitle') || '';
if (ARTICLE_PAGE_TEXT) {
fields.article_page = ARTICLE_PAGE_TEXT;
}
}
return fields;
}
loadPersistedYaml();
ensureArticlePage();
console.log(`[${NAME}] === SCRIPT LOADED SUCCESSFULLY ===`);
console.log(`[${NAME}] Server: ${SERVER}`);
console.log(`[${NAME}] User: ${USERNAME}`);
console.log(`[${NAME}] Token: ${TOKEN ? 'present' : 'none (will prompt)'}`);
console.log(`[${NAME}] Assistant yaml: ${ASSISTANT_YAML ? 'present' : 'none'}`);
console.log(`[${NAME}] Page URL: ${location.href}`);
console.log(`[${NAME}] Installing click and mutation observers...`);
// ========== TOKEN INPUT FORM ==========
function showTokenForm(widgetEl) {
// Don't add duplicate forms
if (widgetEl.querySelector('.convowizard-auth-form')) return;
const form = document.createElement('div');
form.className = 'convowizard-auth-form cc-form';
const title = document.createElement('div');
title.className = 'cc-form__title';
title.textContent = `${NAME}: Authorization Required`;
const desc = document.createElement('div');
desc.className = 'cc-form__desc';
desc.textContent = `Please paste your ${NAME} token below to activate the extension.`;
const row = document.createElement('div');
row.className = 'cc-row';
const input = document.createElement('input');
input.type = 'password';
input.placeholder = 'Paste token here';
input.autocomplete = 'off';
input.spellcheck = false;
input.className = 'cc-input';
const btn = document.createElement('button');
btn.type = 'button';
btn.textContent = 'Authorize';
btn.className = 'cdx-button cdx-button--weight-primary cdx-button--action-progressive';
const errDiv = document.createElement('div');
errDiv.className = 'cc-error';
errDiv.style.display = 'none';
async function onAuthorize() {
const raw = input.value.trim();
// Input validation: 33 hex chars
if (!/^[0-9a-f]{33}$/i.test(raw)) {
errDiv.textContent = 'Invalid token format. A token is 33 hexadecimal characters.';
errDiv.style.display = 'block';
input.value = '';
input.focus();
return;
}
btn.disabled = true;
btn.textContent = 'Validating...';
errDiv.style.display = 'none';
const valid = await validateToken(raw, USERNAME);
// Clear input immediately after use
input.value = '';
if (valid) {
TOKEN = raw;
await persistToken(STORAGE_KEY, TOKEN);
// Remove all auth forms on page
document.querySelectorAll('.convowizard-auth-form').forEach(f => f.remove());
// Activate the widget
startForWidget(widgetEl);
} else {
errDiv.textContent = 'Token is invalid or expired. Please check and try again.';
errDiv.style.display = 'block';
btn.disabled = false;
btn.textContent = 'Authorize';
input.focus();
}
}
btn.addEventListener('click', onAuthorize);
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.preventDefault(); onAuthorize(); }
});
row.appendChild(input);
row.appendChild(btn);
form.appendChild(title);
form.appendChild(desc);
form.appendChild(row);
form.appendChild(errDiv);
widgetEl.prepend(form);
input.focus();
}
function yamlButtonLabel() { // Switch back for full labels, can be implemented as an option
return ''; // '<span>' + ASSISTANT_YAML ? 'Change YAML' : 'Load YAML' + '</span>';
}
function syncYamlButtons() {
document.querySelectorAll('.convowizard-yaml-btn').forEach((btn) => {
btn.textContent = yamlButtonLabel();
btn.prepend(cdxToSvg(codexicons.cdxIconCode, 'var(--color-progressive, #36c)'));
});
}
function showYamlForm(widgetEl) {
if (widgetEl.querySelector('.convowizard-yaml-form')) return;
const form = document.createElement('div');
form.className = 'convowizard-yaml-form cc-card cdx-card';
const iconSvg = cdxToSvg(codexicons.cdxIconCode, 'var(--color-subtle, #54595d)', 'cdx-card__icon');
const subForm = document.createElement('div');
subForm.className = 'convowizard-yaml-form cdx-card__text';
const title = document.createElement('div');
title.className = 'cdx-card__text__title';
title.textContent = `${NAME}: assistant.yaml`;
const desc = document.createElement('div');
desc.className = 'cdx-card__text__description cc-panel__body';
desc.textContent = 'Paste an assistant.yaml (or drag and drop the file here) to change the LLM behavior for you. Leave empty and apply to go back to the discussion summary and suggested reading.';
const textarea = document.createElement('textarea');
textarea.rows = 10;
textarea.placeholder = 'persona:\n name: ...';
textarea.spellcheck = false;
textarea.value = ASSISTANT_YAML || '';
textarea.className = 'cc-input cc-textarea';
const row = document.createElement('div');
row.className = 'cdx-card__text__supporting-text cc-card__buttons';
row.style.marginTop = '8px';
const btn = document.createElement('button');
btn.type = 'button';
btn.textContent = 'Apply';
btn.className = 'cdx-button cdx-button--weight-primary cdx-button--action-progressive';
const cancel = document.createElement('button');
cancel.type = 'button';
cancel.textContent = 'Close';
cancel.className = 'cdx-button cdx-button--weight-quiet cdx-button--action-destructive';
const errDiv = document.createElement('div');
errDiv.className = 'cc-error';
errDiv.style.display = 'none';
// drag-and-drop a yaml file anywhere on the form; it fills the textarea
// (still requires Apply, so the drop can be reviewed and cancelled)
async function loadYamlFile(file) {
if (!file) return;
if (file.size > MAX_YAML_CHARS) {
errDiv.textContent = `yaml file is too long (max ${MAX_YAML_CHARS} characters).`;
errDiv.style.display = 'block';
return;
}
try {
textarea.value = await file.text();
errDiv.style.display = 'none';
textarea.focus();
} catch (err) {
console.warn(`[${NAME}] could not read dropped file`, err);
errDiv.textContent = 'could not read the dropped file.';
errDiv.style.display = 'block';
}
}
const textareaBorder = textarea.style.border;
function isFileDrag(e) {
return e.dataTransfer && Array.from(e.dataTransfer.types || []).includes('Files');
}
form.addEventListener('dragover', (e) => {
if (!isFileDrag(e)) return;
// preventDefault is what makes the form a valid drop target (and stops
// the browser from navigating to the file); stopPropagation keeps the
// reply widget's own drop handling out of it
e.preventDefault();
e.stopPropagation();
e.dataTransfer.dropEffect = 'copy';
textarea.style.border = '2px dashed #0645ad';
});
form.addEventListener('dragleave', () => {
textarea.style.border = textareaBorder;
});
form.addEventListener('drop', (e) => {
if (!isFileDrag(e)) return;
e.preventDefault();
e.stopPropagation();
textarea.style.border = textareaBorder;
loadYamlFile(e.dataTransfer.files[0]);
});
async function onApply() {
const raw = textarea.value.trim();
if (raw.length > MAX_YAML_CHARS) {
errDiv.textContent = `yaml is too long (max ${MAX_YAML_CHARS} characters).`;
errDiv.style.display = 'block';
return;
}
ASSISTANT_YAML = raw;
await persistYaml(raw || null);
await ensureArticlePage();
syncYamlButtons();
form.remove();
const st = widgetState.get(widgetEl);
if (!st || !TOKEN) return;
const existing = utterancesToExisting(st);
const draft = (st.inputEl.innerText || st.inputEl.textContent || st.inputEl.value || '');
const data = await postJson(draft.trim() ? 'continue' : 'start', {
existing,
new: draft,
reply_id: existing.length > 0 ? existing[existing.length - 1].id : null,
url: wikiUrlToPostId(location.href),
interaction_id: st.interactionId,
token: TOKEN,
username: USERNAME,
...llmFields()
});
if (data && !data.error) {
if (data.interaction_id) {
widgetState.set(widgetEl, { ...st, interactionId: data.interaction_id });
}
handleIntervention(widgetEl, st.inputEl, data);
}
}
btn.addEventListener('click', onApply);
cancel.addEventListener('click', () => form.remove());
row.appendChild(btn);
row.appendChild(cancel);
subForm.appendChild(title);
subForm.appendChild(desc);
subForm.appendChild(textarea);
subForm.appendChild(row);
subForm.appendChild(errDiv);
form.appendChild(iconSvg);
form.appendChild(subForm);
widgetEl.prepend(form);
textarea.focus();
}
function createYamlButton(widgetEl) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'convowizard-yaml-btn cdx-button cdx-button--weight-quiet cdx-button--action-progressive cdx-button--icon-only';
btn.textContent = yamlButtonLabel();
btn.title = 'Load or change assistant.yaml';
btn.ariaLabel = 'Load YAML';
btn.prepend(cdxToSvg(codexicons.cdxIconCode, 'var(--color-progressive, #36c)'));
btn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
showYamlForm(widgetEl);
});
return btn;
}
// ---- state + logs ----
const widgetState = new WeakMap();
function cleanupDuplicatePanels() {
const allPanels = document.querySelectorAll('.craftDisplay, .passiveModeDisplay');
const seen = new Set();
allPanels.forEach(panel => {
const pid = panel.id;
if (pid && pid.includes('_')) {
const parts = pid.split('_');
const type = parts[0];
const wid = parts.slice(1).join('_').replace('_d', '');
const key = `${type}|${wid}`;
if (seen.has(key)) panel.remove(); else seen.add(key);
}
});
}
// ========== MUTE THREAD ==========
/**
* Generates a unique thread identifier by combining URL section, DOM position, and content hash.
* Ensures each thread on a page gets a distinct ID for per-thread mute functionality.
*/
function getThreadIdForWidget(widgetEl) {
try {
const urlHash = location.hash ? location.hash.replace('#', '') : 'root';
// Generate DOM path for widget position
const widgetPath = [];
let el = widgetEl;
for (let i = 0; i < 8 && el; i++) {
const parent = el.parentElement;
if (!parent || parent === document.body) break;
const siblings = Array.from(parent.children);
const index = siblings.indexOf(el);
widgetPath.unshift(`${parent.tagName}_${index}`);
el = parent;
}
const domPath = widgetPath.length > 0 ? widgetPath.join('_') : 'unknown';
// Get content-based hash from first comment
const context = getContextStringFor(widgetEl);
let contentHash = '';
if (context && context.length > 0) {
const firstComment = context.split('\n\n')[0] || context.substring(0, 150);
let hash = 0;
for (let i = 0; i < Math.min(firstComment.length, 150); i++) {
const char = firstComment.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash;
}
contentHash = Math.abs(hash).toString(36);
} else {
const parentText = widgetEl.parentElement ?
(widgetEl.parentElement.textContent || '').substring(0, 50).trim() : '';
if (parentText) {
let hash = 0;
for (let i = 0; i < parentText.length; i++) {
hash = ((hash << 5) - hash) + parentText.charCodeAt(i);
hash = hash & hash;
}
contentHash = Math.abs(hash).toString(36);
} else {
contentHash = Math.random().toString(36).slice(2, 8);
}
}
return `thread_${urlHash}_${domPath.substring(0, 50)}_${contentHash}`;
} catch (err) {
console.warn(`[${NAME}] Error generating thread ID:`, err);
return `thread_${Date.now()}_${Math.random().toString(36).slice(2, 12)}`;
}
}
function getMutedThreads() {
try {
const storageKey = `ConvoWizard:mutedThreads:${USERNAME}`;
const stored = localStorage.getItem(storageKey);
if (!stored) return new Set();
const threads = JSON.parse(stored);
return new Set(Array.isArray(threads) ? threads : []);
} catch {
return new Set();
}
}
function saveMutedThreads(threadSet) {
try {
const storageKey = `ConvoWizard:mutedThreads:${USERNAME}`;
const threads = Array.from(threadSet);
localStorage.setItem(storageKey, JSON.stringify(threads));
} catch (err) {
console.warn(`[${NAME}] Error saving muted threads:`, err);
}
}
function isThreadMuted(threadId) {
if (!threadId) return false;
const muted = getMutedThreads();
return muted.has(threadId);
}
function toggleThreadMute(threadId) {
if (!threadId) return false;
const muted = getMutedThreads();
const wasMuted = muted.has(threadId);
if (wasMuted) {
muted.delete(threadId);
} else {
muted.add(threadId);
}
saveMutedThreads(muted);
return !wasMuted;
}
/**
* Creates an unmute indicator button that appears when a thread is muted
* Allows users to restore ConvoWizard for a muted thread
*/
function setMuteButtonLabel(btn, isMuted) { // Switch back for full labels, can be implemented as an option
btn.innerHTML = ''; // '<span>' + (isMuted ? 'Unmute thread' : 'Mute thread') + '</span>';
btn.prepend(cdxToSvg(isMuted ? codexicons.cdxIconVolumeUp.ltr : codexicons.cdxIconVolumeOff.ltr, isMuted ? 'var(--color-progressive, #36c)' : 'var(--color-neutral, #404244)'));
}
function ensureUnmuteIndicator(widgetEl, threadId, widgetId) {
const existingIndicator = widgetEl.querySelector('.convowizard-unmute-indicator');
if (existingIndicator) return;
const indicator = document.createElement('button');
indicator.type = 'button';
indicator.id = `convowizard-unmute-${widgetId}`;
indicator.className = 'convowizard-unmute-indicator cdx-button cdx-button--weight-quiet cdx-button--action-progressive';
indicator.title = `${NAME} is muted for this thread`;
indicator.innerHTML = `<span>Unmute ${NAME} for this thread</span>`;
indicator.prepend(cdxToSvg(codexicons.cdxIconVolumeUp.ltr, 'var(--color-progressive, #36c)'));
indicator.addEventListener('click', async (e) => {
e.stopPropagation();
e.preventDefault();
const newMuteState = toggleThreadMute(threadId);
// Show panels and remove indicator
const contextPanel = document.getElementById(`context_${widgetId}_d`);
const replyPanel = document.getElementById(`reply_${widgetId}_d`);
if (contextPanel) contextPanel.style.display = '';
if (replyPanel) replyPanel.style.display = '';
widgetEl.querySelectorAll('.convowizard-unmute-indicator').forEach(ind => ind.remove());
// Update mute buttons
document.querySelectorAll(`button[data-thread-id="${threadId}"]`).forEach(btn => {
setMuteButtonLabel(btn, false);
btn.title = `Mute ${NAME} for this thread`;
btn.classList.remove('cc-btn--muted');
});
// Re-trigger analysis to refresh display
const st = widgetState.get(widgetEl);
if (st && st.inputEl) {
// Re-run the continue request to refresh the display
const inputText = st.inputEl.innerText || st.inputEl.textContent || st.inputEl.value || '';
if (inputText.trim().length > 0) {
const existingForContinue = utterancesToExisting(st);
const data = await postJson('continue', {
existing: existingForContinue,
new: inputText,
interaction_id: st.interactionId,
token: TOKEN,
username: USERNAME,
...llmFields()
});
if (data && !data.error) {
handleIntervention(widgetEl, st.inputEl, data);
}
} else {
const existing = utterancesToExisting(st);
const data = await postJson('start', {
existing,
reply_id: existing.length > 0 ? existing[existing.length - 1].id : null,
url: wikiUrlToPostId(location.href),
token: TOKEN,
username: USERNAME,
...llmFields()
});
if (data && !data.error && data.interaction_id) {
widgetState.set(widgetEl, { ...st, interactionId: data.interaction_id });
handleIntervention(widgetEl, st.inputEl, data);
}
}
} else {
startForWidget(widgetEl);
}
});
// Insert at the beginning of the widget
widgetEl.insertBefore(indicator, widgetEl.firstChild);
}
function createMuteButton(threadId, widgetId, isMuted) {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'convowizard-mute-btn cdx-button cdx-button--weight-quiet cdx-button--icon-only' + (isMuted ? ' cc-btn--muted' : ''); // Remove cdx-button--icon-only if using full names
btn.ariaLabel = isMuted ? 'Unmute' : 'Mute';
btn.setAttribute('data-thread-id', threadId);
btn.setAttribute('data-widget-id', widgetId);
btn.style.marginLeft = 'auto';
setMuteButtonLabel(btn, isMuted);
btn.title = isMuted
? `Unmute ${NAME} for this thread`
: `Mute ${NAME} for this thread`;
btn.addEventListener('click', (e) => {
e.stopPropagation();
e.preventDefault();
const newMuteState = toggleThreadMute(threadId);
// Update button state
setMuteButtonLabel(btn, newMuteState);
btn.title = newMuteState
? `Unmute ${NAME} for this thread`
: `Mute ${NAME} for this thread`;
btn.classList.toggle('cc-btn--muted', newMuteState);
// Hide/show panels and unmute indicator
const contextPanel = document.getElementById(`context_${widgetId}_d`);
const replyPanel = document.getElementById(`reply_${widgetId}_d`);
const widgetEl = contextPanel?.closest('.ext-discussiontools-ui-replyWidget') ||
replyPanel?.closest('.ext-discussiontools-ui-replyWidget') ||
contextPanel?.closest('.ext-discussiontools-ui-newTopicWidget') ||
replyPanel?.closest('.ext-discussiontools-ui-newTopicWidget');
if (contextPanel) contextPanel.style.display = newMuteState ? 'none' : '';
if (replyPanel) replyPanel.style.display = newMuteState ? 'none' : '';
// Clear textbox background when muting
if (newMuteState) {
const st = widgetState.get(widgetEl);
if (st && st.inputEl) {
st.inputEl.style.setProperty('background-color', 'transparent', 'important');
}
}
if (newMuteState && widgetEl) {
ensureUnmuteIndicator(widgetEl, threadId, widgetId);
} else if (widgetEl) {
widgetEl.querySelectorAll('.convowizard-unmute-indicator').forEach(ind => ind.remove());
}
// Update all mute buttons for this thread
document.querySelectorAll(`button[data-thread-id="${threadId}"]`).forEach(otherBtn => {
if (otherBtn !== btn) {
setMuteButtonLabel(otherBtn, newMuteState);
otherBtn.title = newMuteState
? `Unmute ${NAME} for this thread`
: `Mute ${NAME} for this thread`;
otherBtn.classList.toggle('cc-btn--muted', newMuteState);
}
});
});
return btn;
}
function ensurePanels(widgetEl, threadId = null) {
cleanupDuplicatePanels();
// Clean up duplicate unmute indicators
const unmuteIndicators = widgetEl.querySelectorAll('.convowizard-unmute-indicator');
if (unmuteIndicators.length > 1) {
for (let i = 1; i < unmuteIndicators.length; i++) {
unmuteIndicators[i].remove();
}
}
const widgetId = widgetEl.id || `widget_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
if (!widgetEl.id) widgetEl.id = widgetId;
const contextId = `context_${widgetId}`;
const replyId = `reply_${widgetId}`;
const isMuted = threadId ? isThreadMuted(threadId) : false;
// Ensure Context panel
if (!document.getElementById(`${contextId}_d`)) {
const contextBox = document.createElement('div');
contextBox.id = `${contextId}_d`;
contextBox.className = 'craftDisplay cc-card cdx-card';
const contextSubBox = document.createElement('div');
contextSubBox.id = `${contextId}_sd`;
contextSubBox.className = 'craftDisplay cdx-card__text';
// Icon
const iconSvg = cdxToSvg(codexicons.cdxIconRobot, 'var(--color-subtle, #54595d)', 'cdx-card__icon');
// Header
const cheader = document.createElement('div');
cheader.id = `${contextId}_h`;
cheader.className = 'cdx-card__text__title';
const textSpan = document.createElement('span');
textSpan.className = 'convowizard-header-text';
textSpan.textContent = ASSISTANT_YAML ? 'Guidance' : 'Discussion Summary';
cheader.appendChild(textSpan);
// Content
const ccontent = document.createElement('div');
ccontent.id = `${contextId}_p`;
ccontent.className = 'cdx-card__text__description cc-panel__body';
ccontent.textContent = ASSISTANT_YAML ? ASSISTANT_PLACEHOLDER : SUMMARY_PLACEHOLDER;
// Supporters
const csupporting = document.createElement('div');
csupporting.id = `${contextId}_st`;
csupporting.className = 'cc-card__buttons cc-card__buttons--floating';
csupporting.appendChild(createYamlButton(widgetEl));
if (threadId) {
const muteBtn = createMuteButton(threadId, widgetId, isMuted);
csupporting.appendChild(muteBtn);
}
contextSubBox.appendChild(cheader);
contextSubBox.appendChild(ccontent);
contextBox.appendChild(iconSvg);
contextBox.appendChild(contextSubBox);
contextBox.appendChild(csupporting);
if (isMuted) contextBox.style.display = 'none';
widgetEl.prepend(contextBox);
} else if (threadId) {
const existingPanel = document.getElementById(`${contextId}_d`);
if (existingPanel) {
existingPanel.style.display = isThreadMuted(threadId) ? 'none' : '';
}
}
// Show unmute indicator when muted (only if panels don't exist yet)
if (threadId && isMuted && !document.getElementById(`${contextId}_d`) && !document.getElementById(`${replyId}_d`)) {
ensureUnmuteIndicator(widgetEl, threadId, widgetId);
} else if (threadId && !isMuted) {
widgetEl.querySelectorAll('.convowizard-unmute-indicator').forEach(ind => ind.remove());
}
// Ensure Reply panel
if (!document.getElementById(`${replyId}_d`)) {
const replyBox = document.createElement('div');
replyBox.id = `${replyId}_d`;
replyBox.className = 'craftDisplay cc-card cdx-card';
const replySubBox = document.createElement('div');
replySubBox.id = `${replyId}_sd`;
replySubBox.className = 'craftDisplay cdx-card__text';
// Icon
const iconSvg = cdxToSvg(codexicons.cdxIconRobot, 'var(--color-subtle, #54595d)', 'cdx-card__icon');
// Header
const header = document.createElement('div');
header.id = `${replyId}_h`;
header.className = 'cdx-card__text__title';
const textSpan = document.createElement('span');
textSpan.className = 'convowizard-header-text cc-panel__title';
textSpan.textContent = 'Suggested reading';
header.appendChild(textSpan);
// Content
const content = document.createElement('div');
content.id = `${replyId}_p`;
content.className = 'cdx-card__text__description cc-panel__body';
content.textContent = GUIDANCE_PLACEHOLDER;
replySubBox.appendChild(header);
replySubBox.appendChild(content);
replyBox.appendChild(iconSvg);
replyBox.appendChild(replySubBox);
// Supporters
if (threadId) {
const csupporting = document.createElement('div');
csupporting.id = `${replyId}_st`;
csupporting.className = 'cdx-card__text__supporting-buttons';
const muteBtn = createMuteButton(threadId, widgetId, isMuted);
csupporting.appendChild(muteBtn);
replyBox.appendChild(csupporting);
}
if (isMuted) replyBox.style.display = 'none';
const preview = widgetEl.querySelector('.ext-discussiontools-ui-replyWidget-preview')
|| document.querySelector('.ext-discussiontools-ui-replyWidget-preview');
if (preview && preview.parentElement) {
preview.parentElement.insertBefore(replyBox, preview.nextSibling);
} else {
widgetEl.append(replyBox);
}
} else if (threadId) {
const existingPanel = document.getElementById(`${replyId}_d`);
if (existingPanel) {
existingPanel.style.display = isThreadMuted(threadId) ? 'none' : '';
}
}
return { replyId, contextId };
}
async function renderPolicyNutshells(titles) {
const output = document.createElement('div');
const responseObj = titles;
const pageDataPromise = await Promise.allSettled(responseObj.map((title) => mwApi.get( {
action: 'parse',
page: title,
formatversion: '2',
redirects: '1',
format: 'json'
})))
const pageData = pageDataPromise.filter(r => r.status === 'fulfilled').map(r => r.value).filter(
p => !Object.prototype.hasOwnProperty.call(p, 'error') &&
Object.prototype.hasOwnProperty.call(p, 'parse') &&
Object.prototype.hasOwnProperty.call(p.parse, 'text')
);
var policies = [];
for (var p of pageData) {
if (policies.includes(p.parse.title)) {
continue;
}
policies.push(p.parse.title);
var policyDoc = new DOMParser().parseFromString(p.parse.text, 'text/html');
var nutshellNode = policyDoc.querySelector('.ombox.nutshell .mbox-text');
var curNutshell;
if (nutshellNode) {
nutshellNode.removeChild(nutshellNode.firstChild); // We remove the bold "This page in a nutshell" thingie
curNutshell = nutshellNode.innerHTML.trim();
} else {
curNutshell = "No description found!";
}
var policyBlock = document.createElement('div');
var policyLink = document.createElement('a');
policyLink.textContent = p.parse.title.replace(/^Wikipedia:/, '');
policyLink.href = 'https://' + mw.config.get('wgServerName') + mw.config.get('wgArticlePath').replace('$1', mw.util.rawurlencode(p.parse.title));
policyLink.rel = 'mw:WikiLink';
policyLink.style.fontWeight = 'bold';
var policyText = document.createElement('span');
policyText.innerHTML = curNutshell; // Should not be vulnerable to injections as we're just recovering MediaWiki-parsed HTML from another page
policyBlock.appendChild(policyLink);
policyBlock.appendChild(document.createTextNode(' – '));
policyBlock.appendChild(policyText);
if (output.hasChildNodes()) {
policyBlock.style.marginTop = '6px';
}
output.appendChild(policyBlock);
}
return output;
}
// Safe markdown-lite renderer for plain-text guidance: paragraphs, "1." and
// "-" lists, **bold**. Built with DOM APIs only (no innerHTML).
function appendInline(parent, text) {
text.split(/(\*\*[^*]+\*\*)/).forEach((seg) => {
if (!seg) return;
if (/^\*\*[^*]+\*\*$/.test(seg)) {
const strong = document.createElement('strong');
strong.textContent = seg.slice(2, -2);
parent.appendChild(strong);
} else {
parent.appendChild(document.createTextNode(seg));
}
});
}
function renderPlainGuidance(text) {
const output = document.createElement('div');
let list = null;
let listTag = null;
let para = null;
const flush = () => { list = null; listTag = null; para = null; };
String(text).replace(/\r\n/g, '\n').split('\n').forEach((raw) => {
const line = raw.trim();
if (!line) { flush(); return; }
const num = /^\d+[.)]\s+(.*)$/.exec(line);
const bullet = /^[-*•]\s+(.*)$/.exec(line);
if (num || bullet) {
const tag = num ? 'ol' : 'ul';
if (!list || listTag !== tag) {
list = document.createElement(tag);
list.style.margin = '4px 0 4px 20px';
listTag = tag;
para = null;
output.appendChild(list);
}
const li = document.createElement('li');
appendInline(li, (num || bullet)[1]);
list.appendChild(li);
} else {
list = null; listTag = null;
if (!para) {
para = document.createElement('p');
para.style.margin = '0 0 6px 0';
output.appendChild(para);
} else {
para.appendChild(document.createElement('br'));
}
appendInline(para, line);
}
});
return output;
}
// The server sends llm_response as plain text. If a yaml instead asks the
// model for a JSON array of policy page titles, keep the nutshell rendering;
// anything else (including a {"response": "..."} object) is shown as text.
async function parseAssistantResponse(response) {
if (response == null || String(response).trim() === '') {
return document.createTextNode(ASSISTANT_PLACEHOLDER);
}
let parsed;
try { parsed = JSON.parse(response); } catch { parsed = undefined; }
if (Array.isArray(parsed) && parsed.length && parsed.every((t) => typeof t === 'string')) {
return renderPolicyNutshells(parsed);
}
if (parsed && typeof parsed === 'object' && typeof parsed.response === 'string') {
return renderPlainGuidance(parsed.response);
}
return renderPlainGuidance(response);
}
function updateAssistantPanel(contextId, replyId, response, personaName, draft) {
const box = document.getElementById(`${contextId}_d`);
const p = document.getElementById(`${contextId}_p`);
const h = document.getElementById(`${contextId}_h`);
if (!box || !p) return;
const muteBtn = box.querySelector('.convowizard-mute-btn');
if (muteBtn) {
const threadId = muteBtn.getAttribute('data-thread-id');
if (threadId && isThreadMuted(threadId)) {
box.style.display = 'none';
return;
}
}
if (box.style.display === 'none') box.style.display = '';
const empty = response == null || String(response).trim() === '';
// null means the server chose not to speak (or the round failed): keep the guidance already shown
// for (almost) this same draft, so a borderline draft doesn't flicker between it and the placeholder
const keep = empty && p.dataset.guidanceFor && similar(p.dataset.guidanceFor, draft);
if (!keep) parseAssistantResponse(response).then((response) => {
p.textContent = '';
p.appendChild(response);
p.dataset.guidanceFor = empty ? '' : (draft || '');
},
(error) => {
console.error(`Error while parsing assistant response: ${error.message}`);
p.textContent = ASSISTANT_PLACEHOLDER;
});
if (h) {
const textSpan = h.querySelector('span.convowizard-header-text');
if (textSpan) {
const label = (personaName && String(personaName).trim()) || NAME;
textSpan.textContent = `${label}`;
}
}
const replyBox = document.getElementById(`${replyId}_d`);
if (replyBox) replyBox.style.display = 'none';
}
// Renders the server-provided discussion summary. No scores, no thresholds:
// one neutral style, and a null summary keeps the placeholder text.
function updateSummaryPanel(contextId, summary) {
console.log(`[${NAME}] updateSummaryPanel called - contextId: ${contextId}`);
const box = document.getElementById(`${contextId}_d`);
const p = document.getElementById(`${contextId}_p`);
const h = document.getElementById(`${contextId}_h`);
if (!box || !p) return;
const muteBtn = box.querySelector('.convowizard-mute-btn');
if (muteBtn) {
const threadId = muteBtn.getAttribute('data-thread-id');
if (threadId && isThreadMuted(threadId)) {
box.style.display = 'none';
return;
}
}
if (box.style.display === 'none') box.style.display = '';
p.textContent = summary || SUMMARY_PLACEHOLDER;
if (h) {
const textSpan = h.querySelector('span.convowizard-header-text');
if (textSpan) textSpan.textContent = 'Discussion Summary';
}
}
// Renders the suggested-reading links. Links arrive as
// {shortcut, title, url, reason} with server-whitelisted URLs; everything is
// inserted via DOM APIs (createElement/textContent), never innerHTML.
function updateGuidancePanel(replyId, links, inputEl) {
// The editor background never tints in this condition.
if (inputEl) {
inputEl.style.setProperty('background-color', 'transparent', 'important');
}
const box = document.getElementById(`${replyId}_d`);
const p = document.getElementById(`${replyId}_p`);
const h = document.getElementById(`${replyId}_h`);
if (!box || !p) return;
const muteBtn = box.querySelector('.convowizard-mute-btn');
if (muteBtn) {
const threadId = muteBtn.getAttribute('data-thread-id');
if (threadId && isThreadMuted(threadId)) {
box.style.display = 'none';
return;
}
}
if (box.style.display === 'none') box.style.display = '';
const safeLinks = (Array.isArray(links) ? links : []).filter(
(l) => l && typeof l.url === 'string' && l.url.startsWith(ALLOWED_LINK_PREFIX)
&& typeof l.title === 'string' && typeof l.reason === 'string'
);
p.textContent = '';
if (safeLinks.length === 0) {
p.textContent = GUIDANCE_PLACEHOLDER;
} else {
for (const link of safeLinks) {
const item = document.createElement('div');
item.className = 'cc-link-item';
const a = document.createElement('a');
a.href = link.url;
a.target = '_blank';
a.rel = 'noopener noreferrer';
a.className = 'cc-link';
a.textContent = link.shortcut ? `${link.title} (${link.shortcut})` : link.title;
const reason = document.createElement('span');
reason.className = 'cc-reason';
reason.textContent = ` — ${link.reason}`;
item.appendChild(a);
item.appendChild(reason);
p.appendChild(item);
}
}
if (h) {
const textSpan = h.querySelector('span.convowizard-header-text');
if (textSpan) textSpan.textContent = 'Suggested Reading';
}
}
// ===== Section-scoped context via DiscussionTools comment markers =====
// DiscussionTools brackets every parsed comment with
// <span data-mw-comment-start id="c-Author-<timestamp>..."></span> ... <span data-mw-comment-end="c-..."></span>
// Collecting every marked comment in the enclosing section, in document
// order, up to the reply box gives "what the user sees above the box" —
// including equal-level siblings and {{outdent}}ed threads that the legacy
// ancestor-chain walk (getContextStringFor below, kept as fallback) misses.
const MAX_CONTEXT_UTTERANCES = 12;
function parseAuthorFromCommentId(id) {
// DiscussionTools ids embed the author before a timestamp, which is either
// ISO ("c-Author_name-2026-05-30T17:19:00.000Z-...") or compact
// ("c-Author_name-20260530171900-..."), depending on wiki/version.
try {
const m = /^c-(.+?)-(?:\d{4}-\d{2}-\d{2}T|\d{12,14}(?:[-.]|$))/.exec(id || '');
return m ? decodeURIComponent(m[1]).replace(/_/g, ' ') : null;
} catch {
return null;
}
}
function parseAuthorFromUserLink(container) {
// fallback: the signature is the last User:/User talk: link in the comment
try {
const links = container.querySelectorAll('a[href*="User:"], a[href*="User_talk:"]');
if (!links.length) return null;
const href = links[links.length - 1].getAttribute('href') || '';
const m = /User(?:_talk)?:([^?&#\/]+)/.exec(href);
return m ? decodeURIComponent(m[1]).replace(/_/g, ' ').trim() : null;
} catch {
return null;
}
}
function extractMarkedCommentText(startMarker) {
const id = startMarker.id;
let endMarker = null;
if (id) {
try {
endMarker = document.querySelector(`span[data-mw-comment-end="${CSS.escape(id)}"]`);
} catch {
endMarker = null;
}
}
try {
const range = document.createRange();
range.setStartAfter(startMarker);
if (endMarker) {
range.setEndBefore(endMarker);
} else {
// no end marker found: fall back to the containing block's content
const block = startMarker.closest('dd, li, p') || startMarker.parentElement;
if (!block) return { text: '', sigAuthor: null };
range.setEnd(block, block.childNodes.length);
}
const div = document.createElement('div');
div.appendChild(range.cloneContents());
// strip nested reply lists, our own UI, and DT chrome from the excerpt
div.querySelectorAll(
'dl, ul, ol, .ext-discussiontools-ui-replyWidget, .craftDisplay, .passiveModeDisplay, ' +
'.ext-discussiontools-init-replylink-buttons, .convowizard-yaml-form, .convowizard-auth-form, ' +
'.convowizard-unmute-indicator, [role="button"], [data-mw-comment-start], [data-mw-comment-end]'
).forEach(n => n.remove());
return {
text: (div.textContent || '').replace(/\s+/g, ' ').trim(),
sigAuthor: parseAuthorFromUserLink(div)
};
} catch (err) {
console.warn(`[${NAME}] Marker range extraction failed`, err);
return { text: '', sigAuthor: null };
}
}
function findSectionHeading(el) {
let node = el;
while (node && node !== document.body) {
let sib = node.previousElementSibling;
while (sib) {
if (/^H[1-6]$/.test(sib.tagName) || (sib.classList && sib.classList.contains('mw-heading'))) {
return sib;
}
sib = sib.previousElementSibling;
}
node = node.parentElement;
}
return null;
}
// Returns [{id, speaker, text}] in reading order, or null when the page has
// no DiscussionTools markers (caller falls back to the legacy DOM walk).
function collectUtterances(widgetEl) {
const markers = Array.from(document.querySelectorAll('span[data-mw-comment-start]'));
if (!markers.length) return null;
// only comments the user sees above the reply box
let inScope = markers.filter(
m => m.compareDocumentPosition(widgetEl) & Node.DOCUMENT_POSITION_FOLLOWING
);
// ...and only within the enclosing section
const heading = findSectionHeading(widgetEl);
if (heading) {
inScope = inScope.filter(
m => heading.compareDocumentPosition(m) & Node.DOCUMENT_POSITION_FOLLOWING
);
}
const utterances = [];
for (const m of inScope) {
const { text, sigAuthor } = extractMarkedCommentText(m);
if (!text || text.length < 2) continue;
utterances.push({
id: m.id || `c-unknown-${utterances.length}`,
speaker: parseAuthorFromCommentId(m.id) || sigAuthor,
text
});
}
return utterances.length ? utterances : null;
}
// Single builder for the `existing` array every request sends. Prefers the
// marker-based utterances stored in widget state; falls back to splitting
// the legacy context string on signature timestamps (tolerating "(UTC-4)"
// style user-preference timezones, which the old literal split missed).
function utterancesToExisting(st) {
const key = (st && st.interactionId) || Date.now();
if (st && Array.isArray(st.utterances) && st.utterances.length) {
let list = st.utterances;
if (list.length > MAX_CONTEXT_UTTERANCES) {
list = [list[0]].concat(list.slice(-(MAX_CONTEXT_UTTERANCES - 1)));
}
return list.map((u, i) => {
const item = { id: u.id || `wiki_${key}_${i}`, text: u.text };
if (u.speaker) item.speaker = u.speaker;
return item;
});
}
return ((st && st.context) || '')
.split(/\s*\(UTC[^)]{0,9}\)\s*/)
.filter(text => text.trim().length > 0)
.map((text, index) => ({ id: `wiki_${key}_${index}`, text: text.trim() }));
}
function getContextStringFor(widgetEl) {
try {
console.log(`[${NAME}] === Extracting context for widget ===`);
// Helper to check if element is a comment container
const isCommentNode = (el) => !!el && (el.tagName === 'DD' || el.tagName === 'LI' || el.tagName === 'P');
// Extract clean comment text, removing nested replies and UI elements
// IMPORTANT: Keep timestamps (UTC) because backend splits on them!
const extractCommentText = (el) => {
if (!el) return '';
const clone = el.cloneNode(true);
// Remove nested discussion lists, widgets, and UI elements
// NOTE: DO NOT remove .ext-discussiontools-init-timestamplink - backend needs timestamps to split utterances!
clone.querySelectorAll('dl, .ext-discussiontools-ui-replyWidget, .craftDisplay, .passiveModeDisplay, .ext-discussiontools-init-replylink-buttons, [role="button"]').forEach(n => n.remove());
const text = (clone.innerText || '').trim();
return text;
};
// Wikipedia Talk pages have structure: <dd>comment text<dl><dd>reply<dl><dd>widget
// When widget appears, it's nested inside a new <dd> inside a new <dl>
// We need to find the parent <dd> that contains actual comment text
let commentDD = null;
// Strategy 1: Widget is inside <dd> → <dl> → <dd> (the target comment)
// Find the <dd> containing the widget
const widgetDD = widgetEl.closest('dd, li');
console.log(`[${NAME}] Widget container DD:`, widgetDD ? widgetDD.tagName : 'not found');
if (widgetDD) {
// Check if this DD has meaningful text (not just the widget)
const widgetDDText = extractCommentText(widgetDD);
console.log(`[${NAME}] Widget DD text length:`, widgetDDText.length);
if (widgetDDText.length > 20) {
// This DD has actual comment text
commentDD = widgetDD;
} else {
// This DD is empty/minimal - the widget was just inserted
// Go up: DD → DL → parent DD (which should have the comment)
const parentDL = widgetDD.parentElement;
console.log(`[${NAME}] Parent DL:`, parentDL ? parentDL.tagName : 'not found');
if (parentDL && (parentDL.tagName === 'DL' || parentDL.tagName === 'UL' || parentDL.tagName === 'OL')) {
// Find the parent DD/LI/P that contains this DL
commentDD = parentDL.closest('dd, li, p');
console.log(`[${NAME}] Found parent comment node:`, commentDD ? commentDD.tagName : 'not found');
}
}
}
// Strategy 2: Look for preceding paragraph with comment markers (flat structure)
if (!commentDD) {
console.log(`[${NAME}] Trying flat structure search...`);
let probe = widgetEl;
let hops = 0;
while (probe && hops++ < 10) {
probe = probe.previousElementSibling || (probe.parentElement ? probe.parentElement.previousElementSibling : null);
if (probe && probe.tagName === 'P' && probe.querySelector('[data-mw-comment-start], [data-mw-comment-end]')) {
commentDD = probe;
console.log(`[${NAME}] Found comment via flat search at hop ${hops}`);
break;
}
}
}
// Strategy 3: Last resort - traverse DOM looking for any comment node
if (!commentDD) {
console.log(`[${NAME}] Trying fallback search...`);
let probe = widgetEl;
let hops = 0;
while (probe && hops++ < 20) {
if (isCommentNode(probe) && extractCommentText(probe).length > 20) {
commentDD = probe;
console.log(`[${NAME}] Found comment via fallback at hop ${hops}`);
break;
}
probe = probe.previousElementSibling ||
(probe.parentElement ? probe.parentElement.previousElementSibling : null) ||
(probe.parentElement ? probe.parentElement.parentElement : null);
}
}
if (!commentDD) {
console.warn(`[${NAME}] Could not find any comment node!`);
return '';
}
// Now traverse up the comment chain collecting all parent comments
const texts = [];
const seen = new Set();
let current = commentDD;
let iterations = 0;
console.log(`[${NAME}] Starting comment chain traversal from:`, current.tagName);
while (current && iterations++ < 20) {
if (seen.has(current)) {
console.log(`[${NAME}] Already seen this node, breaking`);
break;
}
seen.add(current);
const text = extractCommentText(current);
if (text && text.length > 0) {
console.log(`[${NAME}] Extracted comment (${text.length} chars):`, text.substring(0, 100) + '...');
texts.unshift(text); // Add to beginning to maintain chronological order
}
// Navigate up the discussion tree
// Structure: <p>text</p><dl><dd>reply<dl><dd>reply</dd></dl></dd></dl>
// From current <dd>, go to parent element (should be <dl>)
const parentList = current.parentElement;
if (!parentList) {
console.log(`[${NAME}] No parent element, stopping`);
break;
}
console.log(`[${NAME}] Parent list:`, parentList.tagName);
// From <dl>, find the parent <dd> or <li> or <p>
if (parentList.tagName === 'DL' || parentList.tagName === 'UL' || parentList.tagName === 'OL') {
// First try to find an ancestor comment node
current = parentList.closest('dd, li, p');
if (current) {
console.log(`[${NAME}] Found ancestor comment:`, current.tagName);
} else {
// No ancestor found - the <p> might be a SIBLING of this <dl>
// This happens in flat structure: <p>comment</p><dl><dd>reply...
console.log(`[${NAME}] No ancestor, searching for previous sibling of DL...`);
let sibling = parentList.previousElementSibling;
let siblingHops = 0;
while (sibling && siblingHops++ < 10) {
console.log(`[${NAME}] Checking sibling:`, sibling.tagName);
if (isCommentNode(sibling) && !seen.has(sibling)) {
const siblingText = extractCommentText(sibling);
if (siblingText.length > 20) {
current = sibling;
console.log(`[${NAME}] Found sibling comment with text (${siblingText.length} chars)`);
break;
}
}
sibling = sibling.previousElementSibling;
}
if (!current || current === parentList.closest('dd, li, p')) {
console.log(`[${NAME}] No more comments found, stopping`);
break;
}
}
} else if (isCommentNode(parentList)) {
// Parent is itself a comment node
current = parentList;
} else {
// Try to find previous sibling comment
let sibling = current.previousElementSibling;
while (sibling && !isCommentNode(sibling)) {
sibling = sibling.previousElementSibling;
}
if (sibling && !seen.has(sibling)) {
current = sibling;
console.log(`[${NAME}] Moving to sibling:`, current.tagName);
} else {
console.log(`[${NAME}] No more siblings, stopping`);
break;
}
}
}
console.log(`[${NAME}] Extracted ${texts.length} comments, total length: ${texts.join('\n\n').length}`);
// Additional pass: Look for any <p> elements with comment markers that might have been missed
// This catches the case where the first comment in a thread is a <p> that's a sibling of the DL
if (commentDD && commentDD.tagName === 'DD') {
const topDL = commentDD.closest('dl');
if (topDL && topDL.previousElementSibling) {
let probe = topDL.previousElementSibling;
let probeHops = 0;
console.log(`[${NAME}] Scanning backwards from top DL for more context...`);
while (probe && probeHops++ < 5) {
if (probe.tagName === 'P' && !seen.has(probe)) {
const probeText = extractCommentText(probe);
if (probeText.length > 20 && (probe.querySelector('[data-mw-comment-start]') || probe.querySelector('[data-mw-comment-end]'))) {
console.log(`[${NAME}] Found additional preceding comment (${probeText.length} chars):`, probeText.substring(0, 100) + '...');
texts.unshift(probeText);
seen.add(probe);
}
}
probe = probe.previousElementSibling;
}
}
}
console.log(`[${NAME}] Final extraction: ${texts.length} comments, total length: ${texts.join('\n\n').length}`);
if (texts.length > 0) {
const fullContext = texts.join('\n\n');
console.log(`[${NAME}] Full context:`, fullContext);
return fullContext;
}
console.warn(`[${NAME}] No context found, returning empty string`);
return '';
} catch (err) {
console.error(`[${NAME}] Error extracting context:`, err);
return '';
}
}
async function startForWidget(widgetEl) {
if (!TOKEN) { showTokenForm(widgetEl); return; }
if (widgetState.has(widgetEl) || widgetEl.getAttribute('data-convowizard-processing') === 'true') return;
widgetEl.setAttribute('data-convowizard-processing', 'true');
console.log(`[${NAME}] Starting widget processing:`, widgetEl.className);
function findInput(scope) {
// Prefer DT textbox role, then VE contenteditable surface, then textarea
return (
scope.querySelector('div[role="textbox"]') ||
scope.querySelector('[contenteditable="true"]') ||
scope.querySelector('.ve-ce-surface [contenteditable="true"]') ||
scope.querySelector('.ve-ce-surface') ||
scope.querySelector('textarea')
);
}
async function waitForInput(scope, timeoutMs = 8000) {
const deadline = Date.now() + timeoutMs;
return new Promise((resolve) => {
const tick = () => {
const el = findInput(scope) || findInput(document);
if (el) return resolve(el);
if (Date.now() > deadline) return resolve(null);
setTimeout(tick, 100);
};
tick();
});
}
const inputEl = await waitForInput(widgetEl);
if (!inputEl) {
console.warn(`[${NAME}] No input element found in widget (after waiting)`);
return;
}
console.log(`[${NAME}] Input element found:`, inputEl.tagName, inputEl.getAttribute('role') || (inputEl.getAttribute('contenteditable') ? 'contenteditable' : ''));
// DiscussionTools markers (real per-comment boundaries + authors).
// We fall back to a legacy ancestor-chain DOM walk.
const dtUtterances = collectUtterances(widgetEl);
let context = '';
if (dtUtterances) {
console.log(`[${NAME}] Context via DT markers: ${dtUtterances.length} comments in section`);
} else {
context = getContextStringFor(widgetEl);
console.log(`[${NAME}] Context via legacy walk (no DT markers), length: ${context.length} chars`);
}
const seedState = { interactionId: Date.now(), inputEl, context, utterances: dtUtterances };
const existing = utterancesToExisting(seedState);
console.log(`[${NAME}] Created ${existing.length} utterances`);
existing.forEach((utt, i) => {
console.log(`[${NAME}] ${i}: ID=${utt.id}${utt.speaker ? ` speaker=${utt.speaker}` : ''}, text=${utt.text.substring(0, 60)}...`);
});
const data = await postJson('start', {
existing, // Send as array like Reddit, not context string!
reply_id: existing.length > 0 ? existing[existing.length - 1].id : null,
url: wikiUrlToPostId(location.href),
token: TOKEN,
username: USERNAME,
...llmFields()
});
console.log(`[${NAME}] Received response from /start:`, data);
// Handle invalid token - clear and show auth form
if (data && data.error === 'Invalid Token') {
TOKEN = null;
await persistToken(STORAGE_KEY, null);
showTokenForm(widgetEl);
return;
}
if (!data || !data.interaction_id) return;
widgetState.set(widgetEl, { interactionId: data.interaction_id, inputEl, context, utterances: dtUtterances });
handleIntervention(widgetEl, inputEl, data);
const ws = { timer: 0, busy: false, dirty: false, lastSent: '', lastAt: 0 };
const readDraft = () => (inputEl.innerText || inputEl.textContent || inputEl.value || '');
const scheduleRound = (ms) => { clearTimeout(ws.timer); ws.timer = setTimeout(fireRound, ms); };
function onDraftChange() {
const raw = readDraft();
const text = raw.trim();
if (!text) { clearTimeout(ws.timer); ws.lastSent = ''; return; }
scheduleRound(SENTENCE_END.test(text) || /\n\s*$/.test(raw) ? 400 : 900);
}
async function fireRound() {
const st = widgetState.get(widgetEl);
if (!st) return;
// Check if thread is muted
const threadId = getThreadIdForWidget(widgetEl);
if (threadId && isThreadMuted(threadId)) {
// Clear input background if muted
if (st.inputEl) {
st.inputEl.style.setProperty('background-color', 'transparent', 'important');
}
return;
}
const text = readDraft().trim();
if (text.length < MIN_DRAFT_CHARS || norm(text) === norm(ws.lastSent)) return;
if (ws.busy) { ws.dirty = true; return; } // one in flight; re-fire once it returns
const wait = ws.lastAt + PACE_MS - Date.now();
if (wait > 0) return scheduleRound(wait);
ws.busy = true;
ws.lastSent = text;
ws.lastAt = Date.now();
let headerSpan = null, headerBefore = '', shown = false;
try {
const widgetId = widgetEl.id;
const headerId = ASSISTANT_YAML ? `context_${widgetId}_h` : `reply_${widgetId}_h`;
const h = document.getElementById(headerId);
if (h) {
// Update the SVG icon
const existingSvg = h.querySelector('svg:not(.convowizard-mute-btn svg)');
if (existingSvg) {
const temp = document.createElement('div');
temp.innerHTML = `
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" class="cc-panel__icon">
<path fill-rule="evenodd" d="M8 0a1 1 0 0 1 .5 1.87V3.5H12A2 2 0 0 1 14 5.5v5a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h3.5V1.87A1 1 0 0 1 8 0ZM5.75 6.75a1 1 0 1 0 0 2 1 1 0 0 0 0-2Zm4.5 0a1 1 0 1 0 0 2 1 1 0 0 0 0-2ZM5.5 14h5v1.5h-5V14Z"/>
</svg>
`;
const newSvg = temp.querySelector('svg');
if (newSvg) {
existingSvg.replaceWith(newSvg);
}
}
// Update text span with processing indicator
const textSpan = h.querySelector('span.convowizard-header-text');
if (textSpan) {
headerSpan = textSpan;
headerBefore = textSpan.textContent;
textSpan.textContent = ASSISTANT_YAML
? 'Guidance (processing…)'
: 'Suggested Reading (processing…)';
}
}
const data2 = await postJson('continue', {
existing: utterancesToExisting(st),
new: text,
interaction_id: st.interactionId,
token: TOKEN,
username: USERNAME,
...llmFields()
});
if (data2 && data2.error === 'Invalid Token') {
TOKEN = null;
await persistToken(STORAGE_KEY, null);
showTokenForm(widgetEl);
return;
}
if (data2 && data2.limit === 'interval') { ws.lastSent = ''; ws.dirty = true; return; }
// the draft changed while this was in flight: drop the reply; the re-fire answers the current draft
if (!similar(readDraft().trim(), text)) { ws.dirty = true; return; }
handleIntervention(widgetEl, st.inputEl, { ...data2, _draft: text });
shown = true;
} finally {
if (!shown && headerSpan && /processing…/.test(headerSpan.textContent)) headerSpan.textContent = headerBefore;
ws.busy = false;
if (ws.dirty) { ws.dirty = false; scheduleRound(0); }
}
}
inputEl.addEventListener('input', onDraftChange);
inputEl.addEventListener('paste', onDraftChange);
inputEl.addEventListener('blur', () => scheduleRound(0));
let postBtn = widgetEl.querySelector(".oo-ui-buttonElement-button[title*='Reply']");
if (!postBtn) {
postBtn = Array.from(widgetEl.querySelectorAll('.oo-ui-buttonElement-button'))
.find(b => /reply/i.test((b.title || b.innerText || '').trim()));
}
if (postBtn) {
postBtn.addEventListener('click', async () => {
const st = widgetState.get(widgetEl);
if (!st) return;
try {
// Recreate existing array from cached state for submit
const existingForSubmit = utterancesToExisting(st);
await postJson('submit', {
existing: existingForSubmit,
new: (st.inputEl.innerText || st.inputEl.textContent || st.inputEl.value || ''),
interaction_id: st.interactionId,
submitted_id: null,
token: TOKEN,
username: USERNAME
});
} finally {
widgetState.delete(widgetEl);
}
});
}
}
function handleIntervention(widgetEl, inputEl, data) {
if (!data) {
console.warn(`[${NAME}] handleIntervention called with no data`);
return;
}
console.log(`[${NAME}] handleIntervention called with data:`, data);
if (data.error) {
return;
}
const interactionId = data.interaction_id;
if (!interactionId) {
console.warn(`[${NAME}] No interaction_id in data`);
return;
}
const threadId = getThreadIdForWidget(widgetEl);
// Skip display if thread is muted
if (threadId && isThreadMuted(threadId)) {
const widgetId = widgetEl.id || `widget_${Date.now()}`;
if (!widgetEl.id) widgetEl.id = widgetId;
ensureUnmuteIndicator(widgetEl, threadId, widgetId);
return;
}
const { replyId, contextId } = ensurePanels(widgetEl, threadId);
const showScores = Object.prototype.hasOwnProperty.call(data, 'show_scores');
console.log(`[${NAME}] Panel IDs - context: ${contextId}, reply: ${replyId}`);
console.log(`[${NAME}] Show scores: ${showScores}`);
const which = data.which || '';
console.log(`[${NAME}] Response type (which): ${which}`);
// Clear any "(Processing...)" header state no matter which condition the
// backend served — otherwise a non-llm response leaves it stuck.
const replyHeader = document.getElementById(`${replyId}_h`);
if (replyHeader) {
const procSpan = replyHeader.querySelector('span.convowizard-header-text');
if (procSpan) procSpan.textContent = 'Suggested Reading';
}
if (which.startsWith('llm')) {
if (which === 'llm_assistant' || data.llm_response != null) {
console.log(`[${NAME}] LLM assistant mode - response:`, data.llm_response != null);
updateAssistantPanel(contextId, replyId, data.llm_response, data.llm_persona_name, data._draft);
} else {
console.log(`[${NAME}] LLM mode - summary:`, data.llm_summary != null);
console.log(`[${NAME}] LLM mode - links:`, (data.llm_links || []).length);
const replyBox = document.getElementById(`${replyId}_d`);
if (replyBox) replyBox.style.display = '';
updateSummaryPanel(contextId, data.llm_summary);
updateGuidancePanel(replyId, data.llm_links, inputEl);
}
} else {
const noteId = `passivenote${interactionId}_d`;
if (!data.message && !document.getElementById(noteId)) {
const box = document.createElement('div');
box.id = noteId;
box.className = 'passiveModeDisplay cc-note';
const content = document.createElement('div');
content.id = `passivenote${interactionId}_p`;
content.textContent = `${NAME} is currently not active on this thread.`;
box.appendChild(content);
widgetEl.prepend(box);
}
}
}
function installClickDelegation() {
document.addEventListener('click', (e) => {
console.log(`[${NAME}] Click detected on:`, e.target.tagName, e.target.className);
// Match reply buttons and links (both old and new DiscussionTools formats)
// Need to check the actual clicked element AND traverse up
const link = e.target.closest && (
e.target.closest('.ext-discussiontools-init-replybutton') ||
e.target.closest('.ext-discussiontools-init-replylink-reply') ||
e.target.closest('a.ext-discussiontools-init-replylink') ||
e.target.closest('.ext-discussiontools-init-replylink-buttons') ||
e.target.closest('[class*="ext-discussiontools-init-"][class*="reply"]')
);
if (!link) return;
console.log(`[${NAME}] Reply link clicked:`, link.className);
// For flat discussion structures (like Research talk pages), the widget may be inserted
// outside the immediate parent. Search more broadly.
const anchorScope = link.closest('dd, li, p, div, section') || document.body;
const broadScope = anchorScope.closest('#mw-content-text, .mw-parser-output, #content') || document.body;
console.log(`[${NAME}] Anchor scope:`, anchorScope.tagName, anchorScope.className);
console.log(`[${NAME}] Broad scope:`, broadScope.tagName, broadScope.className);
const timeoutAt = Date.now() + 8000;
let attempts = 0;
const poll = setInterval(() => {
attempts++;
// Try multiple search strategies:
// 1. In immediate anchor scope (for nested structures like dd/li)
// 2. As sibling to anchor scope (for flat structures like p)
// 3. In broader content scope
// 4. Document-wide fallback
const widget = anchorScope.querySelector('.ext-discussiontools-ui-replyWidget')
|| anchorScope.querySelector('.ext-discussiontools-ui-newTopicWidget')
|| (anchorScope.nextElementSibling && (
anchorScope.nextElementSibling.classList.contains('ext-discussiontools-ui-replyWidget') ||
anchorScope.nextElementSibling.classList.contains('ext-discussiontools-ui-newTopicWidget')
) ? anchorScope.nextElementSibling : null)
|| (anchorScope.parentElement && anchorScope.parentElement.querySelector('.ext-discussiontools-ui-replyWidget:last-child'))
|| (anchorScope.parentElement && anchorScope.parentElement.querySelector('.ext-discussiontools-ui-newTopicWidget:last-child'))
|| broadScope.querySelector('.ext-discussiontools-ui-replyWidget')
|| broadScope.querySelector('.ext-discussiontools-ui-newTopicWidget')
|| document.querySelector('.ext-discussiontools-ui-replyWidget')
|| document.querySelector('.ext-discussiontools-ui-newTopicWidget');
if (widget) {
console.log(`[${NAME}] Widget found after ${attempts} attempts:`, widget.className);
clearInterval(poll);
startForWidget(widget);
}
else if (Date.now() > timeoutAt) {
console.log(`[${NAME}] Widget timeout after ${attempts} attempts - no widget found`);
console.log(`[${NAME}] Searched in anchorScope:`, anchorScope);
console.log(`[${NAME}] Searched in broadScope:`, broadScope);
clearInterval(poll);
} else if (attempts % 10 === 0) {
console.log(`[${NAME}] Still searching for widget, attempt ${attempts}...`);
}
}, 100);
});
}
function installWidgetObserver() {
const root = document.getElementById('content') || document.body;
console.log(`[${NAME}] Setting up mutation observer on:`, root.id || root.tagName);
const obs = new MutationObserver((mutations) => {
for (const m of mutations) {
for (const n of m.addedNodes) {
if (!(n instanceof HTMLElement)) continue;
// Log all significant additions for debugging
if (n.className && typeof n.className === 'string' && n.className.includes('ext-discussiontools')) {
console.log(`[${NAME}] DOM addition detected:`, n.tagName, n.className);
}
// Match specific DiscussionTools widgets
if (n.matches('.ext-discussiontools-ui-replyWidget') || n.matches('.ext-discussiontools-ui-newTopicWidget')) {
console.log(`[${NAME}] *** Widget detected via observer ***:`, n.className);
startForWidget(n);
} else {
const nested = n.querySelector && (
n.querySelector('.ext-discussiontools-ui-replyWidget') ||
n.querySelector('.ext-discussiontools-ui-newTopicWidget')
);
if (nested) {
console.log(`[${NAME}] *** Nested widget detected ***:`, nested.className);
startForWidget(nested);
}
}
}
}
});
obs.observe(root, { childList: true, subtree: true });
console.log(`[${NAME}] Mutation observer is now watching for widget creation`);
// Check for existing widgets on page load
const existingWidgets = document.querySelectorAll('.ext-discussiontools-ui-replyWidget, .ext-discussiontools-ui-newTopicWidget');
if (existingWidgets.length > 0) {
console.log(`[${NAME}] Found ${existingWidgets.length} existing widget(s) on page load`);
existingWidgets.forEach(w => {
console.log(`[${NAME}] Processing existing widget:`, w.className);
startForWidget(w);
});
} else {
console.log(`[${NAME}] No existing widgets found on page load`);
}
}
// kick off
installClickDelegation();
installWidgetObserver();
console.log(`[${NAME}] === INITIALIZATION COMPLETE ===`);
console.log(`[${NAME}] Click delegation: ACTIVE`);
console.log(`[${NAME}] Mutation observer: ACTIVE`);
console.log(`[${NAME}] Waiting for user interaction...`);
})();
//</nowiki>
h5l685z91bkjlymppvx3t7j5owr4dig
User:Nardog/sandbox11.js
2
178772
769555
769304
2026-10-22T01:14:36Z
Nardog
40946
769555
javascript
text/javascript
mw.hook('wikipage.diff').remove(window.sdcb);
mw.hook('wikipage.diff.wikitextBodyUpdate').remove(window.sdcbu);
$('.smartdiff-link').contents().unwrap().end().remove();
$(':is(.diff-addedline, .diff-addedline, .diff-context) > div, .mw-diff-inline-added, .mw-diff-inline-deleted, .mw-diff-inline-changed').contents().each(function () {
if (this.previousSibling && this.previousSibling.tagName === this.tagName) {
this.previousSibling.textContent += this.textContent;
this.remove();
}
});
window.smartdiffTemplates = [
{
names: ['T', 'Tl'],
namespace: 10,
forceNs: true,
end: 1
},
{
names: ['Tlx'],
namespace: 10,
end: 1
},
{
names: ['U'],
prefix: 'Special:Contributions/',
end: 1
},
{
names: ['Re', 'Reply to', 'Ping'],
prefix: 'Special:Contributions/'
},
{
names: ['About'],
start: 3,
skipEven: true
},
{
names: ['For'],
start: 2
},
{
names: ['Other uses', 'Otheruses'],
end: 1
},
{
names: ['Section link', 'Slink'],
end: 1
},
{
names: ['Redirect'],
skipEven: true,
noRedirectEnd: 1
},
{
names: ['Shortcut'],
noRedirectStart: 1
},
{
names: ['Tracked', 'Phab'],
prefix: 'phab:',
end: 1
},
{
names: [
'Distinguish',
'Main', 'Main article',
'Further',
'See also', 'Seealso'
]
},
{
names: ['Ll'],
end: 1
}
];
mw.loader.using([
'mediawiki.util', 'mediawiki.Title', 'mediawiki.api'
], () => {
mw.loader.addStyleTag(`body:is(.skin-vector-legacy, .skin-monobook) .smartdiff-link:is(.extiw, .external) {
color: #36b;
}
body:is(.skin-vector-legacy, .skin-monobook) .smartdiff-link:is(.extiw, .external):visited {
color: #636;
}
body:is(.skin-vector-legacy, .skin-monobook) .smartdiff-link:is(.extiw, .external):active {
color: #b63;
}`);
class SmartDiff {
constructor($diff) {
this.now = performance.now();
this.$diff = $diff;
this.isView = mw.config.get('wgNamespaceNumber') !== -1 &&
mw.config.get('wgAction') === 'view' &&
$diff.nextAll().is('.diff-hr');
[
['old', '#mw-diff-otitle1 a, #differences-prevlink'],
['new', '#mw-diff-ntitle1 a, #differences-nextlink']
].forEach(([side, selector]) => {
let link = this.$diff[0].querySelector(selector);
let pn = link && mw.util.getParamValue('title', link.search);
if (pn) {
this[side] = {
pn,
ns: mw.Title.newFromText(pn).getNamespaceId()
};
}
});
this.isSamePage = this.old?.pn === this.new?.pn;
this.titles = {};
this.noRedirLinks = new WeakSet();
SmartDiff.sds.set($diff[0], this);
this.init();
}
async init($rows = this.$diff) {
this.side = 'old';
for (let div of $rows.find(
'.diff-deletedline > div, .mw-diff-inline-deleted'
)) {
await this.processLine(div);
}
this.side = 'new';
for (let div of $rows.find(
'.diff-addedline > div, .mw-diff-inline-added, .mw-diff-inline-context'
)) {
await this.processLine(div);
}
for (let div of $rows.find(
'.mw-diff-inline-changed, .mw-diff-inline-moved'
)) {
await this.processLine(div, true);
}
let $contexts = $rows.find('.diff-context > div'), clones = [];
for (let [i, div] of $contexts.get().entries()) {
if (i % 2) {
this.side = 'new';
if (this.divHasLink) {
if (this.sidePropUsed) {
await this.processLine(div);
} else {
div.replaceChildren(
...$contexts.eq(i - 1).contents().clone()
);
clones.push(div);
}
}
} else {
this.side = 'old';
await this.processLine(div);
}
}
$(clones).find('.smartdiff-link[title]').each((_, link) => {
this.titles[link.title]?.links.push(link);
});
this.queryTitles();
if (this.hasError) {
mw.notify('SmartDiff error', { type: 'warn' });
}
console.log(performance.now() - this.now);
}
async processLine(div, isInlineChange) {
this.divHasLink = false;
this.sidePropUsed = false;
if (div.querySelector('a[href]')) return;
let tokens;
if (isInlineChange) {
tokens = this.tokenize(
div,
'del, .mw-diff-movedpara-left, .mw-diff-movedpara-right'
);
this.side = 'new';
await this.exec(tokens);
this.escapeTags(tokens, 'ins');
this.unescapeTags(tokens, 'del');
this.side = 'old';
await this.exec(tokens, true);
this.unescapeTags(tokens);
} else {
tokens = this.tokenize(div);
await this.exec(tokens);
}
if (!this.divHasLink) return;
try {
let newDiv = this.unescape(tokens);
this.verify(div, newDiv);
div.replaceChildren(...newDiv.childNodes);
} catch (e) {
debugger;
if (typeof e === 'string') {
this.hasError = true;
console.warn(`SmartDiff error: ${e}`);
} else {
throw e;
}
}
}
tokenize(el, escSelector) {
return [...el.childNodes].flatMap(node => {
if (node.nodeType === 3) {
return node.textContent.split('').map(s => ({ s }));
}
if (node.nodeType !== 1) {
return [];
}
if (escSelector && node.matches(escSelector)) {
return [
{ openingTag: node },
{
esc: this.tokenize(node, escSelector),
tag: node
},
{ closingTag: node }
];
}
return [
{ openingTag: node },
...this.tokenize(node, escSelector),
{ closingTag: node }
];
});
}
escapeTags(tokens, selector) {
let openTags = [], data = [];
tokens.forEach((token, i) => {
if (token.openingTag?.matches(selector)) {
openTags.push([token.openingTag, i + 1]);
} else if (token.closingTag && openTags.length &&
token.closingTag.tagName === openTags.at(-1)[0].tagName
) {
data.push([...openTags.pop(), i]);
}
});
data.reverse().forEach(([tag, start, end]) => {
let token = { tag };
token.esc = tokens.splice(start, end - start, token);
});
}
unescapeTags(tokens, selector) {
tokens.splice(0, Infinity, ...tokens.flatMap(token => (
token.esc && (!selector || token.tag.matches(selector))
? token.esc
: [token]
)));
}
unescape(tokens) {
let els = [$('<div>')[0]], lastText = '';
tokens.forEach(token => {
if (token.openingTag) {
if (lastText) {
els.at(-1).append(lastText);
lastText = '';
}
let el = token.openingTag;
if (!token.link) {
el = el.cloneNode();
}
els.at(-1).append(el);
els.push(el);
} else if (token.closingTag) {
let el = els.pop();
if (!els.length) {
throw `too many closing tags.\n\n${el.innerHTML}`;
}
if (el.tagName !== token.closingTag.tagName) {
throw `expected </${
token.closingTag.tagName
}>, got </${el.tagName}>.\n\n${els[0].innerHTML}`;
}
if (lastText) {
el.append(lastText);
lastText = '';
}
} else {
lastText += token.s;
}
});
if (lastText) {
els.at(-1).append(lastText);
}
if (els.length !== 1) {
throw `too many opening tags.\n\n${els[0].innerHTML}`;
}
return els[0];
}
verify(div, newDiv) {
let origHtml = div.innerHTML;
let $comp = $(newDiv).clone();
$comp.find('.smartdiff-link').contents().unwrap();
let comp = $comp.html().replace(/<\/(ins|del)><\1(?: [^>]+)?>/g, '');
if (comp !== origHtml) {
throw `changed substance.\n\nOriginal:\n${origHtml}\n\nCleaned:\n${comp}\n\nFull:\n${newDiv.innerHTML}`;
}
let emptyCells = newDiv.querySelectorAll(':is(ins, del):empty');
if (emptyCells.length) {
console.log('Empty cells found:', ...emptyCells);
mw.notify('Empty cells found');
}
}
async exec(tokens, isSecond) {
let s = tokens.map(token => token.s || '').join('');
let modes = isSecond
? ['url', 'link', 'template', 'gallery']
: ['url', 'link', 'template', 'heading', 'gallery'];
for (let mode of modes) {
await this.search(s, tokens, mode);
}
}
async search(s, tokens, mode, data) {
for (let match of s.matchAll(SmartDiff[mode + 'Re'])) {
await this[mode + 'Callback'](
tokens,
...match.indices.slice(1).map((indices, i) => indices && {
s: match[i + 1],
indices,
tokens,
tokenIndices: SmartDiff.tokenIndices
}),
data
);
}
}
static tokenIndices() {
let count = 0, start;
for (let [i, token] of this.tokens.entries()) {
if (token.s) {
if (count === this.indices[0]) {
start = i;
}
if (count === this.indices[1] - 1) {
return [start, i + 1];
}
count++;
}
}
}
static linkRe = /\[\[\s*(?:<tvar(?: [^>]*)?>\s*)?((?:[^<>\[\]{|}])+?)(?:\s*<\/tvar>|\{\{#translation:\s*\}\})?\s*(?:\||\]\])/dg;
async linkCallback(tokens, mid) {
let s = this.decodeWikitext(mid.s, true);
let t;
if (s.startsWith('/')) {
t = await this.getSubpage(s);
} else if (s.startsWith('#') && (!this.isView || !this.isSamePage)) {
t = mw.Title.newFromText(this.getProp() + s);
} else if (s.startsWith('../')) {
t = await this.getParentPage(s);
} else {
t = mw.Title.newFromText(s);
}
if (!t) return;
this.link(tokens, ...mid.tokenIndices(), t);
}
static templateRe = /((?<!{){{\s*(?:(?:#invoke|(?:safe)?subst|msg(?:nw)?|raw|int):\s*)?(?:<tvar(?: [^>]*)?>\s*)?)([^<>\[\]{|}]+?)((?:\s*<\/tvar>)?\s*(\|.*)?(?:}}|$))/dg;
async templateCallback(tokens, pre, mid, post, args) {
let s = this.decodeWikitext(mid.s, true);
let t;
if (pre.s.includes('invoke')) {
t = mw.Title.makeTitle(828, s);
} else if (s.startsWith('#')) {
return;
} else if (pre.s.includes('int')) {
t = mw.Title.makeTitle(8, s);
} else if (s.startsWith('/')) {
t = await this.getSubpage(s, 10);
} else if (s.startsWith('../')) {
t = await this.getParentPage(s, 10);
} else {
if (!pre.s.includes('msg') && !pre.s.includes('raw')) {
let match = s.match(/^[^:]+(?::(?=.)|$)/);
if (match && SmartDiff.magicWords.includes(match[0])) return;
}
t = mw.Title.newFromText(s, 10);
}
if (t) {
this.link(tokens, ...mid.tokenIndices(), t);
}
if (!args) return;
let [postStart, postEnd] = post.tokenIndices();
let newPost = tokens.slice(postStart, postEnd);
await this.search(post.s, newPost, 'template');
if (!t || !SmartDiff.templates) return;
let name = t.getMainText();
for (let data of SmartDiff.templates) {
if (Array.isArray(data?.names) &&
Array.prototype.includes.call(data.names, name)
) {
await this.search(
post.s,
newPost,
'templateArgs',
{ ...data, i: 1 }
);
}
}
tokens.splice(postStart, postEnd - postStart, ...newPost);
}
static templateArgsRe = /\|\s*(?:([^=|]*?)\s*=)?\s*(?:<tvar(?: [^>]*)?>\s*)?(.*?)(?:\s*<\/tvar>)?\s*(?=\||{{!}}|}}|$)/dg;
templateArgsCallback(tokens, name, mid, data) {
let n = name ? Number(name.s) || name.s : data.i++;
if (typeof n !== 'number' || (
n < data.start || n > data.end ||
(n % 2 ? data.skipOdd : data.skipEven)
)) {
return;
}
if (!mid.s) return;
let s = this.decodeWikitext(mid.s);
if (data.prefix) {
s = data.prefix + s;
}
if (data.suffix) {
s += data.suffix;
}
let t = data.forceNs
? mw.Title.makeTitle(data.namespace, s)
: mw.Title.newFromText(s, data.namespace);
if (!t) return;
this.link(
tokens,
...mid.tokenIndices(),
t,
n >= data.noRedirectStart || n <= data.noRedirectEnd
);
}
static urlRe = /(\[(?=.+\]))?((?:\bhttps?:)?\/\/[^\s"<>\[\]{|}]*)([^\s"<>\[\]{|}])/dg;
urlCallback(tokens, pre, mid, last) {
let omitLast;
if (!pre) {
if (mid.s.startsWith('//')) return;
if (last.s === ';') {
let match = mid.s.match(/&#?[\da-z]+$/i);
if (match) {
let source = match[0] + last.s;
omitLast = this.decodeWikitext(source) === source;
} else {
omitLast = true;
}
} else {
let trails = '!,.:?';
if (!mid.s.includes('(')) {
trails += ')';
}
omitLast = trails.includes(last.s);
}
}
let s = mid.s;
if (!omitLast) {
s += last.s;
}
let [midStart, midEnd] = mid.tokenIndices();
this.link(
tokens,
midStart,
omitLast ? midEnd : last.tokenIndices()[1],
this.decodeWikitext(s)
);
}
static headingRe = /^(={1,6})\s*(.+?)\s*(\1)\s*(?=<!--|$)/dg;
headingCallback(tokens, pre, mid, post) {
if (/{{|<\/?ref(?:\s[^>]*)?>|\[(?:https?:)?\/\/[^\s\[\]]+\]/i.test(mid.s)) return;
let s = this.decodeWikitext(mid.s).replace(
/'''(.+?)'''|\[\[:?(?:[^|\]]+\|)?([^\]]+)\]\]|<\/?(?:abbr|b|bdi|bdo|big|cite|code|data|del|dfn|em|font|i|ins|kbd|mark|nowiki|q|rb|rp|rt|rtc|ruby|s|samp|small|span|strike|strong|sub|sup|templatestyles|time|translate|tt|u|var)(?:\s[^>]*)?>|<!--.*?-->|\[(?:https?:)?\/\/[^\s\[\]]+\s([^\]]+)\]/gi,
'$1$2$3'
).replace(/''(.+?)''/g, '$1');
let t = mw.Title.newFromText(
`${!this.isView || !this.isSamePage ? this.getProp() : ''}#${s}`
);
if (!t) return;
let midIndices = mid.tokenIndices();
if (tokens.slice(...midIndices).some(({ link }) => link)) {
this.link(tokens, ...post.tokenIndices(), t);
this.link(tokens, ...pre.tokenIndices(), t);
} else {
this.link(tokens, ...midIndices, t);
}
}
// https://commons.wikimedia.org/wiki/Special:MediaStatistics
static galleryRe = /^\s*((?:[^<>\[\]{|}])+\.(?:apng|djv|djvu|flac|gif|jpe|jpeg|jpg|jps|kar|m4a|m4b|m4p|m4r|m4v|mid|midi|mp2|mp3|mp4|mpa|mpe|mpeg|mpg|mpga|oga|ogg|ogm|ogv|ogx|opus|pdf|png|spx|stl|svg|tif|tiff|wav|webm|webp|xcf))\s*(?:\||$)/dgi;
galleryCallback(tokens, mid) {
let s = this.decodeWikitext(mid.s, true);
let t = mw.Title.newFromText(s, 6);
if (t && t.getNamespaceId() !== 6) {
t = mw.Title.makeTitle(6, s);
}
if (!t) return;
this.link(tokens, ...mid.tokenIndices(), t);
}
link(tokens, start, end, mwtOrUrl, noRedirect) {
let indices = [], last;
tokens.slice(start, end).forEach((token, i) => {
if (!token.s && !token.link && !token.esc) return;
if (token.s && !token.link) {
if (!indices.length || last?.link || last?.esc) {
indices.push([start + i]);
}
indices.at(-1)[1] = start + i + 1;
}
last = token;
});
if (indices.some(chunkIndices => chunkIndices.length !== 2)) {
mw.notify('Corrupt chunks found');
debugger;
}
if (!indices.length) return;
let attribs = { class: 'smartdiff-link' }, obj;
if (typeof mwtOrUrl === 'string') {
attribs.class += ' external';
attribs.href = mwtOrUrl;
attribs.rel = 'nofollow';
} else {
let title = mwtOrUrl.toText();
if (title) {
if (Object.hasOwn(this.titles, title)) {
obj = this.titles[title];
if (obj.classes) {
attribs.class += ' ' + obj.classes.join(' ');
}
} else {
obj = { links: [] };
this.titles[title] = obj;
}
if (obj.url) {
attribs.href = obj.url;
} else {
let params = obj.red && { action: 'edit', redlink: 1 } ||
noRedirect && obj.redirect && { redirect: 'no' };
attribs.href = mw.util.getUrl(
obj.canonical || title,
params
);
}
let fragment = mwtOrUrl.getFragment();
if (fragment) {
attribs.href += mw.util.getUrl('#' + fragment);
}
attribs.title = obj.canonical || title;
} else {
attribs.class += ' mw-selflink-fragment';
attribs.href = mwtOrUrl.getUrl();
}
}
let links = indices.reverse().map(chunkIndices => (
this.linkChunk(tokens, ...chunkIndices, attribs)
));
if (obj && !obj.processed) {
obj.links.push(...links);
if (noRedirect) {
links.forEach(link => {
this.noRedirLinks.add(link);
});
}
}
this.divHasLink = true;
}
linkChunk(tokens, start, end, attribs) {
let newStart = start, newEnd = end;
let mid = tokens.slice(start, end);
let midTags = mid.filter(({ s }) => !s);
for (let i = 0; ; i += 2) {
if (tokens[newStart - 1]?.openingTag && midTags[i]?.closingTag &&
tokens[newStart - 1].openingTag.tagName === midTags[i].closingTag.tagName
) {
midTags.unshift(tokens[newStart - 1]);
newStart--;
} else {
break;
}
}
for (let i = midTags.length - 1; ; i--) {
if (tokens[newEnd]?.closingTag && midTags[i]?.openingTag &&
tokens[newEnd].closingTag.tagName === midTags[i].openingTag.tagName
) {
midTags.push(tokens[newEnd]);
newEnd++;
} else {
break;
}
}
if (start - newStart) {
console.log(`Moved up ${start - newStart} times`);
}
if (start - newStart > 1) {
mw.notify(`Moved up ${start - newStart} times`);
}
if (newEnd - end) {
console.log(`Moved down ${newEnd - end} times`);
}
if (newEnd - end > 1) {
mw.notify(`Moved down ${newEnd - end} times`);
}
let link = $('<a>').attr(attribs)[0];
let pre = [{ openingTag: link, link }];
let post = [{ closingTag: link, link }];
if (midTags[0]?.closingTag) {
pre.unshift({ ...midTags[0] });
pre.push({ openingTag: midTags[0].closingTag });
}
if (midTags.at(-1)?.openingTag) {
post.unshift({ closingTag: midTags.at(-1).openingTag });
post.push({ ...midTags.at(-1) });
}
tokens.splice(newEnd, 0, ...post);
tokens.splice(newStart, 0, ...pre);
mid.forEach(token => {
if (token.s) {
token.link = link;
}
});
return link;
}
decodeWikitext(text, decodePercents) {
let s = text;
if (decodePercents) {
try {
s = decodeURIComponent(s);
} catch {}
}
return $($.parseHTML(s)).text();
}
async getSubpage(s, fallbackNs) {
if (await this.canHaveSubpages()) {
return mw.Title.newFromText(
this.getProp() + s.replace(/\/+$/, '')
);
}
return mw.Title.newFromText(s, fallbackNs);
}
async getParentPage(s, fallbackNs) {
if (await this.canHaveSubpages()) {
let chunks = s.split('/');
let levelCount = chunks.findIndex(v => v !== '..');
let sup = this.getProp().split('/').slice(0, -levelCount).join('/');
if (sup) {
let sub = chunks.slice(levelCount).join('/').replace(/\/+$/, '');
return mw.Title.newFromText(sub ? `${sup}/${sub}` : sup);
}
}
return mw.Title.newFromText(s, fallbackNs);
}
async canHaveSubpages(ns = this.getProp('ns')) {
SmartDiff.subNs = SmartDiff.subNs ||
mw.config.get('wgVisualEditorConfig')?.namespacesWithSubpages ||
Object.values((await new mw.Api().get({
action: 'query',
meta: 'siteinfo',
siprop: 'namespaces',
formatversion: 2
})).query.namespaces).filter(n => n.subpages).map(n => n.id);
return SmartDiff.subNs.includes(ns);
}
getProp(n = 'pn') {
if (this[this.side]) {
this.sidePropUsed = !this.isSamePage;
return this[this.side][n];
}
if (Object.hasOwn(this, n)) {
return this[n];
}
this.pn = mw.config.get('wgPageName');
this.ns = mw.config.get('wgNamespaceNumber');
return this[n];
}
async queryTitles() {
let unprocessed = Object.entries(this.titles)
.filter(([, v]) => !v.processed).slice(0, 500).map(([k]) => k);
while (unprocessed.length) {
let curTitles = unprocessed.slice(0, 50);
curTitles.forEach(title => {
this.titles[title].processed = true;
});
let { query } = await new mw.Api().post({
action: 'query',
titles: curTitles,
iwurl: 1,
prop: 'info',
inprop: 'linkclasses',
inlinkcontext: this.getProp(),
formatversion: 2
}, {
headers: { 'Promise-Non-Write-API-Action': 1 }
});
query.pages?.forEach(p => {
let obj = (this.titles[p.title] ??= {});
let classes = p.linkclasses || [];
if (p.missing && !p.known) {
classes.push('new');
obj.red = true;
} else if (p.redirect) {
obj.redirect = true;
}
if (classes.length) {
obj.classes = classes;
}
});
query.interwiki?.forEach(iw => {
this.titles[iw.title] = { classes: ['extiw'], url: iw.url };
});
query.normalized?.forEach(({ from, to }) => {
this.titles[from] = {
...this.titles[from],
...this.titles[to],
canonical: to
};
});
curTitles.forEach(title => {
let obj = this.titles[title];
if (obj.classes) {
$(obj.links).addClass(obj.classes);
}
if (obj.redirect) {
let noRedirLinks = this.noRedirLinks;
$(obj.links).attr('href', function () {
return mw.util.getUrl(
obj.canonical || title,
noRedirLinks.has(this) && { redirect: 'no' }
) + this.hash;
});
} else {
let url = obj.url ||
obj.red && mw.util.getUrl(
obj.canonical || title,
{ action: 'edit', redlink: 1 }
) ||
obj.canonical && mw.util.getUrl(obj.canonical);
if (url) {
$(obj.links).attr('href', function () {
return url + this.hash;
});
}
}
if (obj.canonical) {
$(obj.links).attr('title', obj.canonical);
}
obj.links = [];
});
unprocessed = unprocessed.slice(50);
}
}
static sds = new WeakMap();
static magicWords = [
'!', '=', 'BASEPAGENAME', 'BASEPAGENAME:', 'BASEPAGENAMEE', 'BASEPAGENAMEE:',
'canonicalurl:', 'CURRENTDAY', 'CURRENTDAY2', 'CURRENTDAYNAME', 'CURRENTDOW',
'CURRENTHOUR', 'CURRENTMONTH', 'CURRENTMONTH1', 'CURRENTMONTHABBREV',
'CURRENTMONTHNAME', 'CURRENTMONTHNAMEGEN', 'CURRENTTIME', 'CURRENTTIMESTAMP',
'CURRENTVERSION', 'CURRENTWEEK', 'CURRENTYEAR', 'DEFAULTCATEGORYSORT:',
'DEFAULTSORT:', 'DEFAULTSORTKEY:', 'DISPLAYTITLE:', 'filepath:', 'formatnum:',
'FULLPAGENAME', 'FULLPAGENAME:', 'FULLPAGENAMEE', 'FULLPAGENAMEE:', 'fullurl:',
'gender:', 'lc:', 'lcfirst:', 'LOCALDAY', 'LOCALDAY2', 'LOCALDAYNAME',
'LOCALDOW', 'LOCALHOUR', 'LOCALMONTH', 'LOCALMONTH1', 'LOCALMONTHABBREV',
'LOCALMONTHNAME', 'LOCALMONTHNAMEGEN', 'LOCALTIME', 'LOCALTIMESTAMP',
'LOCALWEEK', 'LOCALYEAR','NAMESPACE', 'NAMESPACE:', 'NAMESPACEE', 'NAMESPACEE:',
'NAMESPACENUMBER', 'NAMESPACENUMBER:', 'ns:', 'NUMBEROFACTIVEUSERS',
'NUMBEROFARTICLES', 'NUMBEROFEDITS', 'NUMBEROFFILES', 'NUMBEROFPAGES',
'NUMBEROFUSERS', 'padleft:', 'PAGENAME', 'PAGENAMEE', 'PAGESINCAT:',
'PAGESINCATEGORY:', 'plural:', 'REVISIONDAY', 'REVISIONDAY:', 'REVISIONDAY2',
'REVISIONDAY2:', 'REVISIONID', 'REVISIONID:', 'REVISIONMONTH', 'REVISIONMONTH:',
'REVISIONMONTH1', 'REVISIONMONTH1:', 'REVISIONSIZE', 'REVISIONTIMESTAMP',
'REVISIONTIMESTAMP:', 'REVISIONUSER', 'REVISIONUSER:', 'REVISIONYEAR',
'REVISIONYEAR:', 'ROOTPAGENAME', 'ROOTPAGENAME:', 'ROOTPAGENAMEE',
'ROOTPAGENAMEE:', 'SHORTDESC:', 'SUBJECTPAGENAME', 'SUBJECTPAGENAME:',
'SUBJECTPAGENAMEE', 'SUBJECTPAGENAMEE:', 'SUBJECTSPACE', 'SUBJECTSPACE:',
'SUBJECTSPACEE', 'SUBJECTSPACEE:', 'SUBPAGENAME', 'SUBPAGENAME:',
'SUBPAGENAMEE', 'SUBPAGENAMEE:', 'TALKPAGENAME', 'TALKPAGENAME:',
'TALKPAGENAMEE', 'TALKPAGENAMEE:', 'TALKSPACE', 'TALKSPACE:', 'TALKSPACEE',
'TALKSPACEE:', 'uc:', 'ucfirst:', 'urlencode:'
];
}
if (Array.isArray(window.smartdiffMagicWords)) {
SmartDiff.magicWords.push(...window.smartdiffMagicWords);
}
if (Array.isArray(window.smartdiffTemplates)) {
SmartDiff.templates = window.smartdiffTemplates;
}
mw.hook('wikipage.diff').add((window.sdcb = ($diff => {
window.sd = new SmartDiff($diff);
})));
mw.hook('wikipage.diff.wikitextBodyUpdate').add((window.sdcbu = ($rows => {
let $diff = $rows.closest('.diff');
let sd = SmartDiff.sds.get($diff[0]);
if (sd) {
sd.init($rows.filter(function () {
return this.checkVisibility();
}));
} else {
window.sd = new SmartDiff($diff);
}
})));
});
8ke2rmqtlws6pp1nqxkaw6gnpehsz9n
769610
769555
2026-10-22T08:43:01Z
Nardog
40946
769610
javascript
text/javascript
mw.hook('wikipage.diff').remove(window.sdcb);
mw.hook('wikipage.diff.wikitextBodyUpdate').remove(window.sdcbu);
$('.smartdiff-link').contents().unwrap().end().remove();
$(':is(.diff-addedline, .diff-addedline, .diff-context) > div, .mw-diff-inline-added, .mw-diff-inline-deleted, .mw-diff-inline-changed').contents().each(function () {
if (this.previousSibling && this.previousSibling.tagName === this.tagName) {
this.previousSibling.textContent += this.textContent;
this.remove();
}
});
window.smartdiffTemplates = [
{
names: ['T', 'Tl'],
namespace: 10,
forceNs: true,
end: 1
},
{
names: ['Tlx'],
namespace: 10,
end: 1
},
{
names: ['U'],
prefix: 'Special:Contributions/',
end: 1
},
{
names: ['Re', 'Reply to', 'Ping'],
prefix: 'Special:Contributions/'
},
{
names: ['About'],
start: 3,
skipEven: true
},
{
names: ['For'],
start: 2
},
{
names: ['Other uses', 'Otheruses'],
end: 1
},
{
names: ['Section link', 'Slink'],
end: 1
},
{
names: ['Redirect'],
skipEven: true,
noRedirectEnd: 1
},
{
names: ['Shortcut'],
noRedirectStart: 1
},
{
names: ['Tracked', 'Phab'],
prefix: 'phab:',
end: 1
},
{
names: [
'Distinguish',
'Main', 'Main article',
'Further',
'See also', 'Seealso'
]
},
{
names: ['Ll'],
end: 1
}
];
mw.loader.using([
'mediawiki.util', 'mediawiki.Title', 'mediawiki.api'
], () => {
mw.loader.addStyleTag(`body:is(.skin-vector-legacy, .skin-monobook) .smartdiff-link:is(.extiw, .external) {
color: #36b;
}
body:is(.skin-vector-legacy, .skin-monobook) .smartdiff-link:is(.extiw, .external):visited {
color: #636;
}
body:is(.skin-vector-legacy, .skin-monobook) .smartdiff-link:is(.extiw, .external):active {
color: #b63;
}`);
class SmartDiff {
constructor($diff) {
this.now = performance.now();
this.$diff = $diff;
this.isView = mw.config.get('wgNamespaceNumber') !== -1 &&
mw.config.get('wgAction') === 'view' &&
$diff.nextAll().is('.diff-hr');
[
['old', '#mw-diff-otitle1 a, #differences-prevlink'],
['new', '#mw-diff-ntitle1 a, #differences-nextlink']
].forEach(([side, selector]) => {
let link = this.$diff[0].querySelector(selector);
let pn = link && mw.util.getParamValue('title', link.search);
if (pn) {
this[side] = {
pn,
ns: mw.Title.newFromText(pn).getNamespaceId()
};
}
});
this.isSamePage = this.old?.pn === this.new?.pn;
this.titles = {};
this.noRedirLinks = new WeakSet();
SmartDiff.sds.set($diff[0], this);
this.init();
}
async init($rows = this.$diff) {
this.side = 'old';
for (let div of $rows.find(
'.diff-deletedline > div, .mw-diff-inline-deleted'
)) {
await this.processLine(div);
}
this.side = 'new';
for (let div of $rows.find(
'.diff-addedline > div, .mw-diff-inline-added, .mw-diff-inline-context'
)) {
await this.processLine(div);
}
for (let div of $rows.find(
'.mw-diff-inline-changed, .mw-diff-inline-moved'
)) {
await this.processLine(div, true);
}
let $contexts = $rows.find('.diff-context > div'), clones = [];
for (let [i, div] of $contexts.get().entries()) {
if (i % 2) {
this.side = 'new';
if (this.divHasLink) {
if (this.sidePropUsed) {
await this.processLine(div);
} else {
div.replaceChildren(
...$contexts.eq(i - 1).contents().clone()
);
clones.push(div);
}
}
} else {
this.side = 'old';
await this.processLine(div);
}
}
$(clones).find('.smartdiff-link[title]').each((_, link) => {
this.titles[link.title]?.links.push(link);
});
this.queryTitles();
if (this.hasError) {
mw.notify('SmartDiff error', { type: 'warn' });
}
console.log(performance.now() - this.now);
}
async processLine(div, isInlineChange) {
this.divHasLink = false;
this.sidePropUsed = false;
if (div.querySelector('a[href]')) return;
let tokens;
if (isInlineChange) {
tokens = this.tokenize(
div,
'del, .mw-diff-movedpara-left, .mw-diff-movedpara-right'
);
this.side = 'new';
await this.exec(tokens);
this.escapeTags(tokens, 'ins');
this.unescapeTags(tokens, 'del');
this.side = 'old';
await this.exec(tokens, true);
this.unescapeTags(tokens);
} else {
tokens = this.tokenize(div);
await this.exec(tokens);
}
if (!this.divHasLink) return;
try {
let newDiv = this.unescape(tokens);
this.verify(div, newDiv);
div.replaceChildren(...newDiv.childNodes);
} catch (e) {
debugger;
if (typeof e === 'string') {
this.hasError = true;
console.warn(`SmartDiff error: ${e}`);
} else {
throw e;
}
}
}
tokenize(el, escSelector) {
return [...el.childNodes].flatMap(node => {
if (node.nodeType === 3) {
return node.textContent.split('').map(s => ({ s }));
}
if (node.nodeType !== 1) {
return [];
}
if (escSelector && node.matches(escSelector)) {
return [
{ openingTag: node },
{
esc: this.tokenize(node, escSelector),
tag: node
},
{ closingTag: node }
];
}
return [
{ openingTag: node },
...this.tokenize(node, escSelector),
{ closingTag: node }
];
});
}
escapeTags(tokens, selector) {
let openTags = [], data = [];
tokens.forEach((token, i) => {
if (token.openingTag?.matches(selector)) {
openTags.push([token.openingTag, i + 1]);
} else if (token.closingTag && openTags.length &&
token.closingTag.tagName === openTags.at(-1)[0].tagName
) {
data.push([...openTags.pop(), i]);
}
});
data.reverse().forEach(([tag, start, end]) => {
let token = { tag };
token.esc = tokens.splice(start, end - start, token);
});
}
unescapeTags(tokens, selector) {
tokens.splice(0, Infinity, ...tokens.flatMap(token => (
token.esc && (!selector || token.tag.matches(selector))
? token.esc
: [token]
)));
}
unescape(tokens) {
let els = [$('<div>')[0]], lastText = '';
tokens.forEach(token => {
if (token.openingTag) {
if (lastText) {
els.at(-1).append(lastText);
lastText = '';
}
let el = token.openingTag;
if (!token.link) {
el = el.cloneNode();
}
els.at(-1).append(el);
els.push(el);
} else if (token.closingTag) {
let el = els.pop();
if (!els.length) {
throw `too many closing tags.\n\n${el.innerHTML}`;
}
if (el.tagName !== token.closingTag.tagName) {
throw `expected </${
token.closingTag.tagName
}>, got </${el.tagName}>.\n\n${els[0].innerHTML}`;
}
if (lastText) {
el.append(lastText);
lastText = '';
}
} else {
lastText += token.s;
}
});
if (lastText) {
els.at(-1).append(lastText);
}
if (els.length !== 1) {
throw `too many opening tags.\n\n${els[0].innerHTML}`;
}
return els[0];
}
verify(div, newDiv) {
let origHtml = div.innerHTML;
let $comp = $(newDiv).clone();
$comp.find('.smartdiff-link').contents().unwrap();
let comp = $comp.html().replace(/<\/(ins|del)><\1(?: [^>]+)?>/g, '');
if (comp !== origHtml) {
throw `changed substance.\n\nOriginal:\n${origHtml}\n\nCleaned:\n${comp}\n\nFull:\n${newDiv.innerHTML}`;
}
let emptyCells = newDiv.querySelectorAll(':is(ins, del):empty');
if (emptyCells.length) {
console.log('Empty cells found:', ...emptyCells);
mw.notify('Empty cells found');
}
}
async exec(tokens, isSecond) {
let s = tokens.map(token => token.s || '').join('');
let modes = isSecond
? ['url', 'link', 'template', 'gallery']
: ['url', 'link', 'template', 'heading', 'gallery'];
for (let mode of modes) {
await this.search(s, tokens, mode);
}
}
async search(s, tokens, mode, data) {
for (let match of s.matchAll(SmartDiff[mode + 'Re'])) {
await this[mode + 'Callback'](
tokens,
...match.indices.slice(1).map((indices, i) => indices && {
s: match[i + 1],
indices,
tokens,
tokenIndices: SmartDiff.tokenIndices
}),
data
);
}
}
static tokenIndices() {
let count = 0, start;
for (let [i, token] of this.tokens.entries()) {
if (token.s) {
if (count === this.indices[0]) {
start = i;
}
if (count === this.indices[1] - 1) {
return [start, i + 1];
}
count++;
}
}
}
static linkRe = /\[\[\s*(?:<tvar(?: [^>]*)?>\s*)?((?:[^<>\[\]{|}])+?)(?:\s*<\/tvar>|\{\{#translation:\s*\}\})?\s*(?:\||\]\])/dg;
async linkCallback(tokens, mid) {
let s = this.decodeWikitext(mid.s, true);
let t;
if (s.startsWith('/')) {
t = await this.getSubpage(s);
} else if (s.startsWith('#') && (!this.isView || !this.isSamePage)) {
t = mw.Title.newFromText(this.getProp() + s);
} else if (s.startsWith('../')) {
t = await this.getParentPage(s);
} else {
t = mw.Title.newFromText(s);
}
if (!t) return;
this.link(tokens, ...mid.tokenIndices(), t);
}
static templateRe = /((?<!{){{\s*(?:(?:#invoke|(?:safe)?subst|msg(?:nw)?|raw|int):\s*)?(?:<tvar(?: [^>]*)?>\s*)?)([^<>\[\]{|}]+?)((?:\s*<\/tvar>)?\s*(\|.*)?(?:}}|$))/dg;
async templateCallback(tokens, pre, mid, post, args) {
let s = this.decodeWikitext(mid.s, true);
let t;
if (pre.s.includes('invoke')) {
t = mw.Title.makeTitle(828, s);
} else if (s.startsWith('#')) {
return;
} else if (pre.s.includes('int')) {
t = mw.Title.makeTitle(8, s);
} else if (s.startsWith('/')) {
t = await this.getSubpage(s, 10);
} else if (s.startsWith('../')) {
t = await this.getParentPage(s, 10);
} else {
if (!pre.s.includes('msg') && !pre.s.includes('raw')) {
let match = s.match(/^[^:]+(?::(?=.)|$)/);
if (match && SmartDiff.magicWords.includes(match[0])) return;
}
t = mw.Title.newFromText(s, 10);
}
if (t) {
this.link(tokens, ...mid.tokenIndices(), t);
}
if (!args) return;
let [postStart, postEnd] = post.tokenIndices();
let newPost = tokens.slice(postStart, postEnd);
await this.search(post.s, newPost, 'template');
if (!t || !SmartDiff.templates) return;
let name = t.getMainText();
for (let data of SmartDiff.templates) {
if (Array.isArray(data?.names) &&
Array.prototype.includes.call(data.names, name)
) {
await this.search(
post.s,
newPost,
'templateArgs',
{ ...data, i: 1 }
);
}
}
tokens.splice(postStart, postEnd - postStart, ...newPost);
}
static templateArgsRe = /\|\s*(?:([^=|]*?)\s*=)?\s*(?:<tvar(?: [^>]*)?>\s*)?(.*?)(?:\s*<\/tvar>)?\s*(?=\||{{!}}|}}|$)/dg;
templateArgsCallback(tokens, name, mid, data) {
let n = name ? Number(name.s) || name.s : data.i++;
if (typeof n !== 'number' || (
n < data.start || n > data.end ||
(n % 2 ? data.skipOdd : data.skipEven)
)) {
return;
}
if (!mid.s) return;
let s = this.decodeWikitext(mid.s);
if (data.prefix) {
s = data.prefix + s;
}
if (data.suffix) {
s += data.suffix;
}
let t = data.forceNs
? mw.Title.makeTitle(data.namespace, s)
: mw.Title.newFromText(s, data.namespace);
if (!t) return;
this.link(
tokens,
...mid.tokenIndices(),
t,
n >= data.noRedirectStart || n <= data.noRedirectEnd
);
}
static urlRe = /(\[(?=.+\]))?((?:\bhttps?:)?\/\/[^\s"<>\[\]{|}]*)([^\s"<>\[\]{|}])/dg;
urlCallback(tokens, pre, mid, last) {
let omitLast;
if (!pre) {
if (mid.s.startsWith('//')) return;
if (last.s === ';') {
let match = mid.s.match(/&#?[\da-z]+$/i);
if (match) {
let source = match[0] + last.s;
omitLast = this.decodeWikitext(source) === source;
} else {
omitLast = true;
}
} else {
let trails = '!,.:?';
if (!mid.s.includes('(')) {
trails += ')';
}
omitLast = trails.includes(last.s);
}
}
let s = mid.s;
if (!omitLast) {
s += last.s;
}
let [midStart, midEnd] = mid.tokenIndices();
this.link(
tokens,
midStart,
omitLast ? midEnd : last.tokenIndices()[1],
this.decodeWikitext(s)
);
}
static headingRe = /^(={1,6})\s*(.+?)\s*(\1)\s*(?=<!--|$)/dg;
headingCallback(tokens, pre, mid, post) {
if (/{{|<\/?ref(?:\s[^>]*)?>|\[(?:https?:)?\/\/[^\s\[\]]+\]/i.test(mid.s)) return;
let s = this.decodeWikitext(mid.s).replace(
/'''(.+?)'''|\[\[:?(?:[^|\]]+\|)?([^\]]+)\]\]|<\/?(?:abbr|b|bdi|bdo|big|cite|code|data|del|dfn|em|font|i|ins|kbd|mark|nowiki|q|rb|rp|rt|rtc|ruby|s|samp|small|span|strike|strong|sub|sup|templatestyles|time|translate|tt|u|var)(?:\s[^>]*)?>|<!--.*?-->|\[(?:https?:)?\/\/[^\s\[\]]+\s([^\]]+)\]/gi,
'$1$2$3'
).replace(/''(.+?)''/g, '$1').replace(/^[\s_]+|[\s_]+$/g, '');
let t = mw.Title.newFromText(
`${!this.isView || !this.isSamePage ? this.getProp() : ''}#${s}`
);
if (!t) return;
let midIndices = mid.tokenIndices();
if (tokens.slice(...midIndices).some(({ link }) => link)) {
this.link(tokens, ...post.tokenIndices(), t);
this.link(tokens, ...pre.tokenIndices(), t);
} else {
this.link(tokens, ...midIndices, t);
}
}
// https://commons.wikimedia.org/wiki/Special:MediaStatistics
static galleryRe = /^\s*((?:[^<>\[\]{|}])+\.(?:apng|djv|djvu|flac|gif|jpe|jpeg|jpg|jps|kar|m4a|m4b|m4p|m4r|m4v|mid|midi|mp2|mp3|mp4|mpa|mpe|mpeg|mpg|mpga|oga|ogg|ogm|ogv|ogx|opus|pdf|png|spx|stl|svg|tif|tiff|wav|webm|webp|xcf))\s*(?:\||$)/dgi;
galleryCallback(tokens, mid) {
let s = this.decodeWikitext(mid.s, true);
let t = mw.Title.newFromText(s, 6);
if (t && t.getNamespaceId() !== 6) {
t = mw.Title.makeTitle(6, s);
}
if (!t) return;
this.link(tokens, ...mid.tokenIndices(), t);
}
link(tokens, start, end, mwtOrUrl, noRedirect) {
let indices = [], last;
tokens.slice(start, end).forEach((token, i) => {
if (!token.s && !token.link && !token.esc) return;
if (token.s && !token.link) {
if (!indices.length || last?.link || last?.esc) {
indices.push([start + i]);
}
indices.at(-1)[1] = start + i + 1;
}
last = token;
});
if (indices.some(chunkIndices => chunkIndices.length !== 2)) {
mw.notify('Corrupt chunks found');
debugger;
}
if (!indices.length) return;
let attribs = { class: 'smartdiff-link' }, obj;
if (typeof mwtOrUrl === 'string') {
attribs.class += ' external';
attribs.href = mwtOrUrl;
attribs.rel = 'nofollow';
} else {
let title = mwtOrUrl.toText();
if (title) {
if (Object.hasOwn(this.titles, title)) {
obj = this.titles[title];
if (obj.classes) {
attribs.class += ' ' + obj.classes.join(' ');
}
} else {
obj = { links: [] };
this.titles[title] = obj;
}
if (obj.url) {
attribs.href = obj.url;
} else {
let params = obj.red && { action: 'edit', redlink: 1 } ||
noRedirect && obj.redirect && { redirect: 'no' };
attribs.href = mw.util.getUrl(
obj.canonical || title,
params
);
}
let fragment = mwtOrUrl.getFragment();
if (fragment) {
attribs.href += mw.util.getUrl('#' + fragment);
}
attribs.title = obj.canonical || title;
} else {
attribs.class += ' mw-selflink-fragment';
attribs.href = mwtOrUrl.getUrl();
}
}
let links = indices.reverse().map(chunkIndices => (
this.linkChunk(tokens, ...chunkIndices, attribs)
));
if (obj && !obj.processed) {
obj.links.push(...links);
if (noRedirect) {
links.forEach(link => {
this.noRedirLinks.add(link);
});
}
}
this.divHasLink = true;
}
linkChunk(tokens, start, end, attribs) {
let newStart = start, newEnd = end;
let mid = tokens.slice(start, end);
let midTags = mid.filter(({ s }) => !s);
for (let i = 0; ; i += 2) {
if (tokens[newStart - 1]?.openingTag && midTags[i]?.closingTag &&
tokens[newStart - 1].openingTag.tagName === midTags[i].closingTag.tagName
) {
midTags.unshift(tokens[newStart - 1]);
newStart--;
} else {
break;
}
}
for (let i = midTags.length - 1; ; i--) {
if (tokens[newEnd]?.closingTag && midTags[i]?.openingTag &&
tokens[newEnd].closingTag.tagName === midTags[i].openingTag.tagName
) {
midTags.push(tokens[newEnd]);
newEnd++;
} else {
break;
}
}
if (start - newStart) {
console.log(`Moved up ${start - newStart} times`);
}
if (start - newStart > 1) {
mw.notify(`Moved up ${start - newStart} times`);
}
if (newEnd - end) {
console.log(`Moved down ${newEnd - end} times`);
}
if (newEnd - end > 1) {
mw.notify(`Moved down ${newEnd - end} times`);
}
let link = $('<a>').attr(attribs)[0];
let pre = [{ openingTag: link, link }];
let post = [{ closingTag: link, link }];
if (midTags[0]?.closingTag) {
pre.unshift({ ...midTags[0] });
pre.push({ openingTag: midTags[0].closingTag });
}
if (midTags.at(-1)?.openingTag) {
post.unshift({ closingTag: midTags.at(-1).openingTag });
post.push({ ...midTags.at(-1) });
}
tokens.splice(newEnd, 0, ...post);
tokens.splice(newStart, 0, ...pre);
mid.forEach(token => {
if (token.s) {
token.link = link;
}
});
return link;
}
decodeWikitext(text, decodePercents) {
let s = text;
if (decodePercents) {
try {
s = decodeURIComponent(s);
} catch {}
}
return $($.parseHTML(s)).text();
}
async getSubpage(s, fallbackNs) {
if (await this.canHaveSubpages()) {
return mw.Title.newFromText(
this.getProp() + s.replace(/\/+$/, '')
);
}
return mw.Title.newFromText(s, fallbackNs);
}
async getParentPage(s, fallbackNs) {
if (await this.canHaveSubpages()) {
let chunks = s.split('/');
let levelCount = chunks.findIndex(v => v !== '..');
let sup = this.getProp().split('/').slice(0, -levelCount).join('/');
if (sup) {
let sub = chunks.slice(levelCount).join('/').replace(/\/+$/, '');
return mw.Title.newFromText(sub ? `${sup}/${sub}` : sup);
}
}
return mw.Title.newFromText(s, fallbackNs);
}
async canHaveSubpages(ns = this.getProp('ns')) {
SmartDiff.subNs = SmartDiff.subNs ||
mw.config.get('wgVisualEditorConfig')?.namespacesWithSubpages ||
Object.values((await new mw.Api().get({
action: 'query',
meta: 'siteinfo',
siprop: 'namespaces',
formatversion: 2
})).query.namespaces).filter(n => n.subpages).map(n => n.id);
return SmartDiff.subNs.includes(ns);
}
getProp(n = 'pn') {
if (this[this.side]) {
this.sidePropUsed = !this.isSamePage;
return this[this.side][n];
}
if (Object.hasOwn(this, n)) {
return this[n];
}
this.pn = mw.config.get('wgPageName');
this.ns = mw.config.get('wgNamespaceNumber');
return this[n];
}
async queryTitles() {
let unprocessed = Object.entries(this.titles)
.filter(([, v]) => !v.processed).slice(0, 500).map(([k]) => k);
while (unprocessed.length) {
let curTitles = unprocessed.slice(0, 50);
curTitles.forEach(title => {
this.titles[title].processed = true;
});
let { query } = await new mw.Api().post({
action: 'query',
titles: curTitles,
iwurl: 1,
prop: 'info',
inprop: 'linkclasses',
inlinkcontext: this.getProp(),
formatversion: 2
}, {
headers: { 'Promise-Non-Write-API-Action': 1 }
});
query.pages?.forEach(p => {
let obj = (this.titles[p.title] ??= {});
let classes = p.linkclasses || [];
if (p.missing && !p.known) {
classes.push('new');
obj.red = true;
} else if (p.redirect) {
obj.redirect = true;
}
if (classes.length) {
obj.classes = classes;
}
});
query.interwiki?.forEach(iw => {
this.titles[iw.title] = { classes: ['extiw'], url: iw.url };
});
query.normalized?.forEach(({ from, to }) => {
this.titles[from] = {
...this.titles[from],
...this.titles[to],
canonical: to
};
});
curTitles.forEach(title => {
let obj = this.titles[title];
if (obj.classes) {
$(obj.links).addClass(obj.classes);
}
if (obj.redirect) {
let noRedirLinks = this.noRedirLinks;
$(obj.links).attr('href', function () {
return mw.util.getUrl(
obj.canonical || title,
noRedirLinks.has(this) && { redirect: 'no' }
) + this.hash;
});
} else {
let url = obj.url ||
obj.red && mw.util.getUrl(
obj.canonical || title,
{ action: 'edit', redlink: 1 }
) ||
obj.canonical && mw.util.getUrl(obj.canonical);
if (url) {
$(obj.links).attr('href', function () {
return url + this.hash;
});
}
}
if (obj.canonical) {
$(obj.links).attr('title', obj.canonical);
}
obj.links = [];
});
unprocessed = unprocessed.slice(50);
}
}
static sds = new WeakMap();
static magicWords = [
'!', '=', 'BASEPAGENAME', 'BASEPAGENAME:', 'BASEPAGENAMEE', 'BASEPAGENAMEE:',
'canonicalurl:', 'CURRENTDAY', 'CURRENTDAY2', 'CURRENTDAYNAME', 'CURRENTDOW',
'CURRENTHOUR', 'CURRENTMONTH', 'CURRENTMONTH1', 'CURRENTMONTHABBREV',
'CURRENTMONTHNAME', 'CURRENTMONTHNAMEGEN', 'CURRENTTIME', 'CURRENTTIMESTAMP',
'CURRENTVERSION', 'CURRENTWEEK', 'CURRENTYEAR', 'DEFAULTCATEGORYSORT:',
'DEFAULTSORT:', 'DEFAULTSORTKEY:', 'DISPLAYTITLE:', 'filepath:', 'formatnum:',
'FULLPAGENAME', 'FULLPAGENAME:', 'FULLPAGENAMEE', 'FULLPAGENAMEE:', 'fullurl:',
'gender:', 'lc:', 'lcfirst:', 'LOCALDAY', 'LOCALDAY2', 'LOCALDAYNAME',
'LOCALDOW', 'LOCALHOUR', 'LOCALMONTH', 'LOCALMONTH1', 'LOCALMONTHABBREV',
'LOCALMONTHNAME', 'LOCALMONTHNAMEGEN', 'LOCALTIME', 'LOCALTIMESTAMP',
'LOCALWEEK', 'LOCALYEAR','NAMESPACE', 'NAMESPACE:', 'NAMESPACEE', 'NAMESPACEE:',
'NAMESPACENUMBER', 'NAMESPACENUMBER:', 'ns:', 'NUMBEROFACTIVEUSERS',
'NUMBEROFARTICLES', 'NUMBEROFEDITS', 'NUMBEROFFILES', 'NUMBEROFPAGES',
'NUMBEROFUSERS', 'padleft:', 'PAGENAME', 'PAGENAMEE', 'PAGESINCAT:',
'PAGESINCATEGORY:', 'plural:', 'REVISIONDAY', 'REVISIONDAY:', 'REVISIONDAY2',
'REVISIONDAY2:', 'REVISIONID', 'REVISIONID:', 'REVISIONMONTH', 'REVISIONMONTH:',
'REVISIONMONTH1', 'REVISIONMONTH1:', 'REVISIONSIZE', 'REVISIONTIMESTAMP',
'REVISIONTIMESTAMP:', 'REVISIONUSER', 'REVISIONUSER:', 'REVISIONYEAR',
'REVISIONYEAR:', 'ROOTPAGENAME', 'ROOTPAGENAME:', 'ROOTPAGENAMEE',
'ROOTPAGENAMEE:', 'SHORTDESC:', 'SUBJECTPAGENAME', 'SUBJECTPAGENAME:',
'SUBJECTPAGENAMEE', 'SUBJECTPAGENAMEE:', 'SUBJECTSPACE', 'SUBJECTSPACE:',
'SUBJECTSPACEE', 'SUBJECTSPACEE:', 'SUBPAGENAME', 'SUBPAGENAME:',
'SUBPAGENAMEE', 'SUBPAGENAMEE:', 'TALKPAGENAME', 'TALKPAGENAME:',
'TALKPAGENAMEE', 'TALKPAGENAMEE:', 'TALKSPACE', 'TALKSPACE:', 'TALKSPACEE',
'TALKSPACEE:', 'uc:', 'ucfirst:', 'urlencode:'
];
}
if (Array.isArray(window.smartdiffMagicWords)) {
SmartDiff.magicWords.push(...window.smartdiffMagicWords);
}
if (Array.isArray(window.smartdiffTemplates)) {
SmartDiff.templates = window.smartdiffTemplates;
}
mw.hook('wikipage.diff').add((window.sdcb = ($diff => {
window.sd = new SmartDiff($diff);
})));
mw.hook('wikipage.diff.wikitextBodyUpdate').add((window.sdcbu = ($rows => {
let $diff = $rows.closest('.diff');
let sd = SmartDiff.sds.get($diff[0]);
if (sd) {
sd.init($rows.filter(function () {
return this.checkVisibility();
}));
} else {
window.sd = new SmartDiff($diff);
}
})));
});
1sb7n78aapg349uafgknwqro6gbjxqd
769702
769610
2026-10-22T11:22:38Z
Nardog
40946
769702
javascript
text/javascript
mw.hook('wikipage.diff').remove(window.sdcb);
mw.hook('wikipage.diff.wikitextBodyUpdate').remove(window.sdcbu);
$('.smartdiff-link').contents().unwrap().end().remove();
$(':is(.diff-addedline, .diff-addedline, .diff-context) > div, .mw-diff-inline-added, .mw-diff-inline-deleted, .mw-diff-inline-changed').contents().each(function () {
if (this.previousSibling && this.previousSibling.tagName === this.tagName) {
this.previousSibling.textContent += this.textContent;
this.remove();
}
});
window.smartdiffTemplates = [
{
names: ['T', 'Tl'],
namespace: 10,
forceNs: true,
end: 1
},
{
names: ['Tlx'],
namespace: 10,
end: 1
},
{
names: ['U'],
prefix: 'Special:Contributions/',
end: 1
},
{
names: ['Re', 'Reply to', 'Ping'],
prefix: 'Special:Contributions/'
},
{
names: ['About'],
start: 3,
skipEven: true
},
{
names: ['For'],
start: 2
},
{
names: ['Other uses', 'Otheruses'],
end: 1
},
{
names: ['Section link', 'Slink'],
end: 1
},
{
names: ['Redirect'],
skipEven: true,
noRedirectEnd: 1
},
{
names: ['Shortcut'],
noRedirectStart: 1
},
{
names: ['Tracked', 'Phab'],
prefix: 'phab:',
end: 1
},
{
names: [
'Distinguish',
'Main', 'Main article',
'Further',
'See also', 'Seealso'
]
},
{
names: ['Ll'],
end: 1
}
];
mw.loader.using([
'mediawiki.util', 'mediawiki.Title', 'mediawiki.api'
], () => {
mw.loader.addStyleTag(`body:is(.skin-vector-legacy, .skin-monobook) .smartdiff-link:is(.extiw, .external) {
color: #36b;
}
body:is(.skin-vector-legacy, .skin-monobook) .smartdiff-link:is(.extiw, .external):visited {
color: #636;
}
body:is(.skin-vector-legacy, .skin-monobook) .smartdiff-link:is(.extiw, .external):active {
color: #b63;
}`);
class SmartDiff {
constructor($diff) {
this.now = performance.now();
this.$diff = $diff;
this.isView = mw.config.get('wgNamespaceNumber') !== -1 &&
mw.config.get('wgAction') === 'view' &&
$diff.nextAll().is('.diff-hr');
[
['old', '#mw-diff-otitle1 a, #differences-prevlink'],
['new', '#mw-diff-ntitle1 a, #differences-nextlink']
].forEach(([side, selector]) => {
let link = this.$diff[0].querySelector(selector);
let pn = link && mw.util.getParamValue('title', link.search);
if (pn) {
this[side] = {
pn,
ns: mw.Title.newFromText(pn).getNamespaceId()
};
}
});
this.isSamePage = this.old?.pn === this.new?.pn;
this.titles = {};
this.noRedirLinks = new WeakSet();
SmartDiff.sds.set($diff[0], this);
this.init();
}
async init($rows = this.$diff) {
this.side = 'old';
for (let div of $rows.find(
'.diff-deletedline > div, .mw-diff-inline-deleted'
)) {
await this.processLine(div);
}
this.side = 'new';
for (let div of $rows.find(
'.diff-addedline > div, .mw-diff-inline-added, .mw-diff-inline-context'
)) {
await this.processLine(div);
}
for (let div of $rows.find(
'.mw-diff-inline-changed, .mw-diff-inline-moved'
)) {
await this.processLine(div, true);
}
let $contexts = $rows.find('.diff-context > div'), clones = [];
for (let [i, div] of $contexts.get().entries()) {
if (i % 2) {
this.side = 'new';
if (this.divHasLink) {
if (this.sidePropUsed) {
await this.processLine(div);
} else {
div.replaceChildren(
...$contexts.eq(i - 1).contents().clone()
);
clones.push(div);
}
}
} else {
this.side = 'old';
await this.processLine(div);
}
}
$(clones).find('.smartdiff-link[title]').each((_, link) => {
this.titles[link.title]?.links.push(link);
});
this.queryTitles();
if (this.hasError) {
mw.notify('SmartDiff error', { type: 'warn' });
}
console.log(performance.now() - this.now);
}
async processLine(div, isInlineChange) {
this.divHasLink = false;
this.sidePropUsed = false;
if (div.querySelector('a[href]')) return;
let tokens;
if (isInlineChange) {
tokens = this.tokenize(
div,
'del, .mw-diff-movedpara-left, .mw-diff-movedpara-right'
);
this.side = 'new';
await this.exec(tokens);
this.escapeTags(tokens, 'ins');
this.unescapeTags(tokens, 'del');
this.side = 'old';
await this.exec(tokens, true);
this.unescapeTags(tokens);
} else {
tokens = this.tokenize(div);
await this.exec(tokens);
}
if (!this.divHasLink) return;
try {
let newDiv = this.unescape(tokens);
this.verify(div, newDiv);
div.replaceChildren(...newDiv.childNodes);
} catch (e) {
debugger;
if (typeof e === 'string') {
this.hasError = true;
console.warn(`SmartDiff error: ${e}`);
} else {
throw e;
}
}
}
tokenize(el, escSelector) {
return [...el.childNodes].flatMap(node => {
if (node.nodeType === 3) {
return node.textContent.split('').map(s => ({ s }));
}
if (node.nodeType !== 1) {
return [];
}
if (escSelector && node.matches(escSelector)) {
return [
{ openingTag: node },
{
esc: this.tokenize(node, escSelector),
tag: node
},
{ closingTag: node }
];
}
return [
{ openingTag: node },
...this.tokenize(node, escSelector),
{ closingTag: node }
];
});
}
escapeTags(tokens, selector) {
let openTags = [], data = [];
tokens.forEach((token, i) => {
if (token.openingTag?.matches(selector)) {
openTags.push([token.openingTag, i + 1]);
} else if (token.closingTag && openTags.length &&
token.closingTag.tagName === openTags.at(-1)[0].tagName
) {
data.push([...openTags.pop(), i]);
}
});
data.reverse().forEach(([tag, start, end]) => {
let token = { tag };
token.esc = tokens.splice(start, end - start, token);
});
}
unescapeTags(tokens, selector) {
tokens.splice(0, Infinity, ...tokens.flatMap(token => (
token.esc && (!selector || token.tag.matches(selector))
? token.esc
: [token]
)));
}
unescape(tokens) {
let els = [$('<div>')[0]], lastText = '';
tokens.forEach(token => {
if (token.openingTag) {
if (lastText) {
els.at(-1).append(lastText);
lastText = '';
}
let el = token.openingTag;
if (!token.link) {
el = el.cloneNode();
}
els.at(-1).append(el);
els.push(el);
} else if (token.closingTag) {
let el = els.pop();
if (!els.length) {
throw `too many closing tags.\n\n${el.innerHTML}`;
}
if (el.tagName !== token.closingTag.tagName) {
throw `expected </${
token.closingTag.tagName
}>, got </${el.tagName}>.\n\n${els[0].innerHTML}`;
}
if (lastText) {
el.append(lastText);
lastText = '';
}
} else {
lastText += token.s;
}
});
if (lastText) {
els.at(-1).append(lastText);
}
if (els.length !== 1) {
throw `too many opening tags.\n\n${els[0].innerHTML}`;
}
return els[0];
}
verify(div, newDiv) {
let origHtml = div.innerHTML;
let $comp = $(newDiv).clone();
$comp.find('.smartdiff-link').contents().unwrap();
let comp = $comp.html().replace(/<\/(ins|del)><\1(?: [^>]+)?>/g, '');
if (comp !== origHtml) {
throw `changed substance.\n\nOriginal:\n${origHtml}\n\nCleaned:\n${comp}\n\nFull:\n${newDiv.innerHTML}`;
}
let emptyCells = newDiv.querySelectorAll(':is(ins, del):empty');
if (emptyCells.length) {
console.log('Empty cells found:', ...emptyCells);
mw.notify('Empty cells found');
}
}
async exec(tokens, isSecond) {
let s = tokens.map(token => token.s || '').join('');
let modes = isSecond
? ['url', 'link', 'template', 'gallery']
: ['url', 'link', 'template', 'heading', 'gallery'];
for (let mode of modes) {
await this.search(s, tokens, mode);
}
}
async search(s, tokens, mode, data) {
for (let match of s.matchAll(SmartDiff[mode + 'Re'])) {
await this[mode + 'Callback'](
tokens,
...match.indices.slice(1).map((indices, i) => indices && {
s: match[i + 1],
indices,
tokens,
tokenIndices: SmartDiff.tokenIndices
}),
data
);
}
}
static tokenIndices() {
let count = 0, start;
for (let [i, token] of this.tokens.entries()) {
if (token.s) {
if (count === this.indices[0]) {
start = i;
}
if (count === this.indices[1] - 1) {
return [start, i + 1];
}
count++;
}
}
}
static linkRe = /\[\[\s*(?:<tvar(?: [^>]*)?>\s*)?((?:[^<>\[\]{|}])+?)(?:\s*<\/tvar>|\{\{#translation:\s*\}\})?\s*(?:\||\]\])/dg;
async linkCallback(tokens, mid) {
let s = this.decodeWikitext(mid.s, true);
let t;
if (s.startsWith('/')) {
t = await this.getSubpage(s);
} else if (s.startsWith('#') && (!this.isView || !this.isSamePage)) {
t = mw.Title.newFromText(this.getProp() + s);
} else if (s.startsWith('../')) {
t = await this.getParentPage(s);
} else {
t = mw.Title.newFromText(s);
}
if (!t) return;
this.link(tokens, ...mid.tokenIndices(), t);
}
static templateRe = /((?<!{){{\s*(?:(?:#invoke|(?:safe)?subst|msg(?:nw)?|raw|int):\s*)?(?:<tvar(?: [^>]*)?>\s*)?)([^<>\[\]{|}]+?)((?:\s*<\/tvar>)?\s*(\|.*)?(?:}}|$))/dg;
async templateCallback(tokens, pre, mid, post, args) {
let s = this.decodeWikitext(mid.s, true);
let t;
if (pre.s.includes('invoke')) {
t = mw.Title.makeTitle(828, s);
} else if (s.startsWith('#')) {
return;
} else if (pre.s.includes('int')) {
t = mw.Title.makeTitle(8, s);
} else if (s.startsWith('/')) {
t = await this.getSubpage(s, 10);
} else if (s.startsWith('../')) {
t = await this.getParentPage(s, 10);
} else {
if (!pre.s.includes('msg') && !pre.s.includes('raw')) {
let match = s.match(/^[^:]+(?::(?=.)|$)/);
if (match && SmartDiff.magicWords.includes(match[0])) return;
}
t = mw.Title.newFromText(s, 10);
}
if (t) {
this.link(tokens, ...mid.tokenIndices(), t);
}
if (!args) return;
let [postStart, postEnd] = post.tokenIndices();
let newPost = tokens.slice(postStart, postEnd);
await this.search(post.s, newPost, 'template');
if (!t || !SmartDiff.templates) return;
let name = t.getMainText();
for (let data of SmartDiff.templates) {
if (Array.isArray(data?.names) &&
Array.prototype.includes.call(data.names, name)
) {
await this.search(
post.s,
newPost,
'templateArgs',
{ ...data, i: 1 }
);
}
}
tokens.splice(postStart, postEnd - postStart, ...newPost);
}
static templateArgsRe = /\|\s*(?:([^=|]*?)\s*=)?\s*(?:<tvar(?: [^>]*)?>\s*)?(.*?)(?:\s*<\/tvar>)?\s*(?=\||{{!}}|}}|$)/dg;
templateArgsCallback(tokens, name, mid, data) {
let n = name ? Number(name.s) || name.s : data.i++;
if (typeof n !== 'number' || (
n < data.start || n > data.end ||
(n % 2 ? data.skipOdd : data.skipEven)
)) {
return;
}
if (!mid.s) return;
let s = this.decodeWikitext(mid.s);
if (data.prefix) {
s = data.prefix + s;
}
if (data.suffix) {
s += data.suffix;
}
let t = data.forceNs
? mw.Title.makeTitle(data.namespace, s)
: mw.Title.newFromText(s, data.namespace);
if (!t) return;
this.link(
tokens,
...mid.tokenIndices(),
t,
n >= data.noRedirectStart || n <= data.noRedirectEnd
);
}
static urlRe = /(\[(?=.+\]))?((?:\bhttps?:)?\/\/[^\s"<>\[\]{|}]*)([^\s"<>\[\]{|}])/dg;
urlCallback(tokens, pre, mid, last) {
let omitLast;
if (!pre) {
if (mid.s.startsWith('//')) return;
if (last.s === ';') {
let match = mid.s.match(/&#?[\da-z]+$/i);
if (match) {
let source = match[0] + last.s;
omitLast = this.decodeWikitext(source) === source;
} else {
omitLast = true;
}
} else {
let trails = '!,.:?';
if (!mid.s.includes('(')) {
trails += ')';
}
omitLast = trails.includes(last.s);
}
}
let s = mid.s;
if (!omitLast) {
s += last.s;
}
let [midStart, midEnd] = mid.tokenIndices();
this.link(
tokens,
midStart,
omitLast ? midEnd : last.tokenIndices()[1],
this.decodeWikitext(s)
);
}
static headingRe = /^(={1,6})\s*(.+?)\s*(\1)\s*(?=<!--|$)/dg;
headingCallback(tokens, pre, mid, post) {
if (/{{|<\/?ref(?:\s[^>]*)?>|\[(?:https?:)?\/\/[^\s\[\]]+\]/i.test(mid.s)) return;
let s = this.decodeWikitext(mid.s).replace(
/'''(.+?)'''|\[\[:?(?:[^|\]]+\|)?([^\]]+)\]\]|<\/?(?:abbr|b|bdi|bdo|big|cite|code|data|del|dfn|em|font|i|ins|kbd|mark|nowiki|q|rb|rp|rt|rtc|ruby|s|samp|small|span|strike|strong|sub|sup|templatestyles|time|translate|tt|u|var)(?:\s[^>]*)?>|<!--.*?-->|\[(?:https?:)?\/\/[^\s\[\]]+\s([^\]]+)\]/gi,
'$1$2$3'
).replace(/''(.+?)''/g, '$1').replace(/^[\s_]+|[\s_]+$/g, '');
let t = mw.Title.newFromText(
`${!this.isView || !this.isSamePage ? this.getProp() : ''}#${s}`
);
if (!t) return;
let midIndices = mid.tokenIndices();
if (tokens.slice(...midIndices).some(({ link }) => link)) {
this.link(tokens, ...post.tokenIndices(), t);
this.link(tokens, ...pre.tokenIndices(), t);
} else {
this.link(tokens, ...midIndices, t);
}
}
// https://commons.wikimedia.org/wiki/Special:MediaStatistics
static galleryRe = /^\s*((?:[^<>\[\]{|}])+\.(?:apng|djv|djvu|flac|gif|jpe|jpeg|jpg|jps|kar|m4a|m4b|m4p|m4r|m4v|mid|midi|mp2|mp3|mp4|mpa|mpe|mpeg|mpg|mpga|oga|ogg|ogm|ogv|ogx|opus|pdf|png|spx|stl|svg|tif|tiff|wav|webm|webp|xcf))\s*(?:\||$)/dgi;
galleryCallback(tokens, mid) {
let s = this.decodeWikitext(mid.s, true);
let t = mw.Title.newFromText(s, 6);
if (t && t.getNamespaceId() !== 6) {
t = mw.Title.makeTitle(6, s);
}
if (!t) return;
this.link(tokens, ...mid.tokenIndices(), t);
}
link(tokens, start, end, mwtOrUrl, noRedirect) {
let indices = [], last;
tokens.slice(start, end).forEach((token, i) => {
if (!token.s && !token.link && !token.esc) return;
if (token.s && !token.link) {
if (!indices.length || last?.link || last?.esc) {
indices.push([start + i]);
}
indices.at(-1)[1] = start + i + 1;
}
last = token;
});
if (indices.some(chunkIndices => chunkIndices.length !== 2)) {
mw.notify('Corrupt chunks found');
debugger;
}
if (!indices.length) return;
let attribs = { class: 'smartdiff-link' }, obj;
if (typeof mwtOrUrl === 'string') {
attribs.class += ' external';
attribs.href = mwtOrUrl;
attribs.rel = 'nofollow';
} else {
let title = mwtOrUrl.toText();
if (title) {
if (Object.hasOwn(this.titles, title)) {
obj = this.titles[title];
if (obj.classes) {
attribs.class += ' ' + obj.classes.join(' ');
}
} else {
obj = { links: [] };
this.titles[title] = obj;
}
if (obj.url) {
attribs.href = obj.url;
let fragment = mwtOrUrl.getFragment();
if (fragment) {
attribs.href += mw.util.getUrl('#' + fragment);
}
} else {
let params = obj.red && { action: 'edit', redlink: 1 } ||
noRedirect && obj.redirect && { redirect: 'no' };
if (obj.canonical) {
attribs.href = mw.util.getUrl(obj.canonical, params);
let fragment = mwtOrUrl.getFragment();
if (fragment) {
attribs.href += mw.util.getUrl('#' + fragment);
}
} else {
attribs.href = mwtOrUrl.getUrl(params);
}
}
attribs.title = obj.canonical || title;
} else {
attribs.class += ' mw-selflink-fragment';
attribs.href = mwtOrUrl.getUrl();
}
}
let links = indices.reverse().map(chunkIndices => (
this.linkChunk(tokens, ...chunkIndices, attribs)
));
if (obj && !obj.processed) {
obj.links.push(...links);
if (noRedirect) {
links.forEach(link => {
this.noRedirLinks.add(link);
});
}
}
this.divHasLink = true;
}
linkChunk(tokens, start, end, attribs) {
let newStart = start, newEnd = end;
let mid = tokens.slice(start, end);
let midTags = mid.filter(({ s }) => !s);
for (let i = 0; ; i += 2) {
if (tokens[newStart - 1]?.openingTag && midTags[i]?.closingTag &&
tokens[newStart - 1].openingTag.tagName === midTags[i].closingTag.tagName
) {
midTags.unshift(tokens[newStart - 1]);
newStart--;
} else {
break;
}
}
for (let i = midTags.length - 1; ; i--) {
if (tokens[newEnd]?.closingTag && midTags[i]?.openingTag &&
tokens[newEnd].closingTag.tagName === midTags[i].openingTag.tagName
) {
midTags.push(tokens[newEnd]);
newEnd++;
} else {
break;
}
}
if (start - newStart) {
console.log(`Moved up ${start - newStart} times`);
}
if (start - newStart > 1) {
mw.notify(`Moved up ${start - newStart} times`);
}
if (newEnd - end) {
console.log(`Moved down ${newEnd - end} times`);
}
if (newEnd - end > 1) {
mw.notify(`Moved down ${newEnd - end} times`);
}
let link = $('<a>').attr(attribs)[0];
let pre = [{ openingTag: link, link }];
let post = [{ closingTag: link, link }];
if (midTags[0]?.closingTag) {
pre.unshift({ ...midTags[0] });
pre.push({ openingTag: midTags[0].closingTag });
}
if (midTags.at(-1)?.openingTag) {
post.unshift({ closingTag: midTags.at(-1).openingTag });
post.push({ ...midTags.at(-1) });
}
tokens.splice(newEnd, 0, ...post);
tokens.splice(newStart, 0, ...pre);
mid.forEach(token => {
if (token.s) {
token.link = link;
}
});
return link;
}
decodeWikitext(text, decodePercents) {
let s = text;
if (decodePercents) {
try {
s = decodeURIComponent(s);
} catch {}
}
return $($.parseHTML(s)).text();
}
async getSubpage(s, fallbackNs) {
if (await this.canHaveSubpages()) {
return mw.Title.newFromText(
this.getProp() + s.replace(/\/+$/, '')
);
}
return mw.Title.newFromText(s, fallbackNs);
}
async getParentPage(s, fallbackNs) {
if (await this.canHaveSubpages()) {
let chunks = s.split('/');
let levelCount = chunks.findIndex(v => v !== '..');
let sup = this.getProp().split('/').slice(0, -levelCount).join('/');
if (sup) {
let sub = chunks.slice(levelCount).join('/').replace(/\/+$/, '');
return mw.Title.newFromText(sub ? `${sup}/${sub}` : sup);
}
}
return mw.Title.newFromText(s, fallbackNs);
}
async canHaveSubpages(ns = this.getProp('ns')) {
SmartDiff.subNs = SmartDiff.subNs ||
mw.config.get('wgVisualEditorConfig')?.namespacesWithSubpages ||
Object.values((await new mw.Api().get({
action: 'query',
meta: 'siteinfo',
siprop: 'namespaces',
formatversion: 2
})).query.namespaces).filter(n => n.subpages).map(n => n.id);
return SmartDiff.subNs.includes(ns);
}
getProp(n = 'pn') {
if (this[this.side]) {
this.sidePropUsed = !this.isSamePage;
return this[this.side][n];
}
if (Object.hasOwn(this, n)) {
return this[n];
}
this.pn = mw.config.get('wgPageName');
this.ns = mw.config.get('wgNamespaceNumber');
return this[n];
}
async queryTitles() {
let unprocessed = Object.entries(this.titles)
.filter(([, v]) => !v.processed).slice(0, 500).map(([k]) => k);
while (unprocessed.length) {
let curTitles = unprocessed.slice(0, 50);
curTitles.forEach(title => {
this.titles[title].processed = true;
});
let { query } = await new mw.Api().post({
action: 'query',
titles: curTitles,
iwurl: 1,
prop: 'info',
inprop: 'linkclasses',
inlinkcontext: this.getProp(),
formatversion: 2
}, {
headers: { 'Promise-Non-Write-API-Action': 1 }
});
query.pages?.forEach(p => {
let obj = (this.titles[p.title] ??= {});
let classes = p.linkclasses || [];
if (p.missing && !p.known) {
classes.push('new');
obj.red = true;
} else if (p.redirect) {
obj.redirect = true;
}
if (classes.length) {
obj.classes = classes;
}
});
query.interwiki?.forEach(iw => {
this.titles[iw.title] = { classes: ['extiw'], url: iw.url };
});
query.normalized?.forEach(({ from, to }) => {
this.titles[from] = {
...this.titles[from],
...this.titles[to],
canonical: to
};
});
curTitles.forEach(title => {
let obj = this.titles[title];
if (obj.classes) {
$(obj.links).addClass(obj.classes);
}
if (obj.redirect) {
let noRedirLinks = this.noRedirLinks;
$(obj.links).attr('href', function () {
return mw.util.getUrl(
obj.canonical || title,
noRedirLinks.has(this) && { redirect: 'no' }
) + this.hash;
});
} else {
let url = obj.url ||
obj.red && mw.util.getUrl(
obj.canonical || title,
{ action: 'edit', redlink: 1 }
) ||
obj.canonical && mw.util.getUrl(obj.canonical);
if (url) {
$(obj.links).attr('href', function () {
return url + this.hash;
});
}
}
if (obj.canonical) {
$(obj.links).attr('title', obj.canonical);
}
obj.links = [];
});
unprocessed = unprocessed.slice(50);
}
}
static sds = new WeakMap();
static magicWords = [
'!', '=', 'BASEPAGENAME', 'BASEPAGENAME:', 'BASEPAGENAMEE', 'BASEPAGENAMEE:',
'canonicalurl:', 'CURRENTDAY', 'CURRENTDAY2', 'CURRENTDAYNAME', 'CURRENTDOW',
'CURRENTHOUR', 'CURRENTMONTH', 'CURRENTMONTH1', 'CURRENTMONTHABBREV',
'CURRENTMONTHNAME', 'CURRENTMONTHNAMEGEN', 'CURRENTTIME', 'CURRENTTIMESTAMP',
'CURRENTVERSION', 'CURRENTWEEK', 'CURRENTYEAR', 'DEFAULTCATEGORYSORT:',
'DEFAULTSORT:', 'DEFAULTSORTKEY:', 'DISPLAYTITLE:', 'filepath:', 'formatnum:',
'FULLPAGENAME', 'FULLPAGENAME:', 'FULLPAGENAMEE', 'FULLPAGENAMEE:', 'fullurl:',
'gender:', 'lc:', 'lcfirst:', 'LOCALDAY', 'LOCALDAY2', 'LOCALDAYNAME',
'LOCALDOW', 'LOCALHOUR', 'LOCALMONTH', 'LOCALMONTH1', 'LOCALMONTHABBREV',
'LOCALMONTHNAME', 'LOCALMONTHNAMEGEN', 'LOCALTIME', 'LOCALTIMESTAMP',
'LOCALWEEK', 'LOCALYEAR','NAMESPACE', 'NAMESPACE:', 'NAMESPACEE', 'NAMESPACEE:',
'NAMESPACENUMBER', 'NAMESPACENUMBER:', 'ns:', 'NUMBEROFACTIVEUSERS',
'NUMBEROFARTICLES', 'NUMBEROFEDITS', 'NUMBEROFFILES', 'NUMBEROFPAGES',
'NUMBEROFUSERS', 'padleft:', 'PAGENAME', 'PAGENAMEE', 'PAGESINCAT:',
'PAGESINCATEGORY:', 'plural:', 'REVISIONDAY', 'REVISIONDAY:', 'REVISIONDAY2',
'REVISIONDAY2:', 'REVISIONID', 'REVISIONID:', 'REVISIONMONTH', 'REVISIONMONTH:',
'REVISIONMONTH1', 'REVISIONMONTH1:', 'REVISIONSIZE', 'REVISIONTIMESTAMP',
'REVISIONTIMESTAMP:', 'REVISIONUSER', 'REVISIONUSER:', 'REVISIONYEAR',
'REVISIONYEAR:', 'ROOTPAGENAME', 'ROOTPAGENAME:', 'ROOTPAGENAMEE',
'ROOTPAGENAMEE:', 'SHORTDESC:', 'SUBJECTPAGENAME', 'SUBJECTPAGENAME:',
'SUBJECTPAGENAMEE', 'SUBJECTPAGENAMEE:', 'SUBJECTSPACE', 'SUBJECTSPACE:',
'SUBJECTSPACEE', 'SUBJECTSPACEE:', 'SUBPAGENAME', 'SUBPAGENAME:',
'SUBPAGENAMEE', 'SUBPAGENAMEE:', 'TALKPAGENAME', 'TALKPAGENAME:',
'TALKPAGENAMEE', 'TALKPAGENAMEE:', 'TALKSPACE', 'TALKSPACE:', 'TALKSPACEE',
'TALKSPACEE:', 'uc:', 'ucfirst:', 'urlencode:'
];
}
if (Array.isArray(window.smartdiffMagicWords)) {
SmartDiff.magicWords.push(...window.smartdiffMagicWords);
}
if (Array.isArray(window.smartdiffTemplates)) {
SmartDiff.templates = window.smartdiffTemplates;
}
mw.hook('wikipage.diff').add((window.sdcb = ($diff => {
window.sd = new SmartDiff($diff);
})));
mw.hook('wikipage.diff.wikitextBodyUpdate').add((window.sdcbu = ($rows => {
let $diff = $rows.closest('.diff');
let sd = SmartDiff.sds.get($diff[0]);
if (sd) {
sd.init($rows.filter(function () {
return this.checkVisibility();
}));
} else {
window.sd = new SmartDiff($diff);
}
})));
});
3wtryh27jux2kbgl8kjstmqz6kzqe51
769704
769702
2026-10-22T11:23:24Z
Nardog
40946
769704
javascript
text/javascript
mw.hook('wikipage.diff').remove(window.sdcb);
mw.hook('wikipage.diff.wikitextBodyUpdate').remove(window.sdcbu);
$('.smartdiff-link').contents().unwrap().end().remove();
$(':is(.diff-addedline, .diff-addedline, .diff-context) > div, .mw-diff-inline-added, .mw-diff-inline-deleted, .mw-diff-inline-changed').contents().each(function () {
if (this.previousSibling && this.previousSibling.tagName === this.tagName) {
this.previousSibling.textContent += this.textContent;
this.remove();
}
});
window.smartdiffTemplates = [
{
names: ['T', 'Tl'],
namespace: 10,
forceNs: true,
end: 1
},
{
names: ['Tlx'],
namespace: 10,
end: 1
},
{
names: ['U'],
prefix: 'Special:Contributions/',
end: 1
},
{
names: ['Re', 'Reply to', 'Ping'],
prefix: 'Special:Contributions/'
},
{
names: ['About'],
start: 3,
skipEven: true
},
{
names: ['For'],
start: 2
},
{
names: ['Other uses', 'Otheruses'],
end: 1
},
{
names: ['Section link', 'Slink'],
end: 1
},
{
names: ['Redirect'],
skipEven: true,
noRedirectEnd: 1
},
{
names: ['Shortcut'],
noRedirectStart: 1
},
{
names: ['Tracked', 'Phab'],
prefix: 'phab:',
end: 1
},
{
names: [
'Distinguish',
'Main', 'Main article',
'Further',
'See also', 'Seealso'
]
},
{
names: ['Ll'],
end: 1
}
];
mw.loader.using([
'mediawiki.util', 'mediawiki.Title', 'mediawiki.api'
], () => {
mw.loader.addStyleTag(`body:is(.skin-vector-legacy, .skin-monobook) .smartdiff-link:is(.extiw, .external) {
color: #36b;
}
body:is(.skin-vector-legacy, .skin-monobook) .smartdiff-link:is(.extiw, .external):visited {
color: #636;
}
body:is(.skin-vector-legacy, .skin-monobook) .smartdiff-link:is(.extiw, .external):active {
color: #b63;
}`);
class SmartDiff {
constructor($diff) {
this.now = performance.now();
this.$diff = $diff;
this.isView = mw.config.get('wgNamespaceNumber') !== -1 &&
mw.config.get('wgAction') === 'view' &&
$diff.nextAll().is('.diff-hr');
[
['old', '#mw-diff-otitle1 a, #differences-prevlink'],
['new', '#mw-diff-ntitle1 a, #differences-nextlink']
].forEach(([side, selector]) => {
let link = this.$diff[0].querySelector(selector);
let pn = link && mw.util.getParamValue('title', link.search);
if (pn) {
this[side] = {
pn,
ns: mw.Title.newFromText(pn).getNamespaceId()
};
}
});
this.isSamePage = this.old?.pn === this.new?.pn;
this.titles = {};
this.noRedirLinks = new WeakSet();
SmartDiff.sds.set($diff[0], this);
this.init();
}
async init($rows = this.$diff) {
this.side = 'old';
for (let div of $rows.find(
'.diff-deletedline > div, .mw-diff-inline-deleted'
)) {
await this.processLine(div);
}
this.side = 'new';
for (let div of $rows.find(
'.diff-addedline > div, .mw-diff-inline-added, .mw-diff-inline-context'
)) {
await this.processLine(div);
}
for (let div of $rows.find(
'.mw-diff-inline-changed, .mw-diff-inline-moved'
)) {
await this.processLine(div, true);
}
let $contexts = $rows.find('.diff-context > div'), clones = [];
for (let [i, div] of $contexts.get().entries()) {
if (i % 2) {
this.side = 'new';
if (this.divHasLink) {
if (this.sidePropUsed) {
await this.processLine(div);
} else {
div.replaceChildren(
...$contexts.eq(i - 1).contents().clone()
);
clones.push(div);
}
}
} else {
this.side = 'old';
await this.processLine(div);
}
}
$(clones).find('.smartdiff-link[title]').each((_, link) => {
this.titles[link.title]?.links.push(link);
});
this.queryTitles();
if (this.hasError) {
mw.notify('SmartDiff error', { type: 'warn' });
}
console.log(performance.now() - this.now);
}
async processLine(div, isInlineChange) {
this.divHasLink = false;
this.sidePropUsed = false;
if (div.querySelector('a[href]')) return;
let tokens;
if (isInlineChange) {
tokens = this.tokenize(
div,
'del, .mw-diff-movedpara-left, .mw-diff-movedpara-right'
);
this.side = 'new';
await this.exec(tokens);
this.escapeTags(tokens, 'ins');
this.unescapeTags(tokens, 'del');
this.side = 'old';
await this.exec(tokens, true);
this.unescapeTags(tokens);
} else {
tokens = this.tokenize(div);
await this.exec(tokens);
}
if (!this.divHasLink) return;
try {
let newDiv = this.unescape(tokens);
this.verify(div, newDiv);
div.replaceChildren(...newDiv.childNodes);
} catch (e) {
debugger;
if (typeof e === 'string') {
this.hasError = true;
console.warn(`SmartDiff error: ${e}`);
} else {
throw e;
}
}
}
tokenize(el, escSelector) {
return [...el.childNodes].flatMap(node => {
if (node.nodeType === 3) {
return node.textContent.split('').map(s => ({ s }));
}
if (node.nodeType !== 1) {
return [];
}
if (escSelector && node.matches(escSelector)) {
return [
{ openingTag: node },
{
esc: this.tokenize(node, escSelector),
tag: node
},
{ closingTag: node }
];
}
return [
{ openingTag: node },
...this.tokenize(node, escSelector),
{ closingTag: node }
];
});
}
escapeTags(tokens, selector) {
let openTags = [], data = [];
tokens.forEach((token, i) => {
if (token.openingTag?.matches(selector)) {
openTags.push([token.openingTag, i + 1]);
} else if (token.closingTag && openTags.length &&
token.closingTag.tagName === openTags.at(-1)[0].tagName
) {
data.push([...openTags.pop(), i]);
}
});
data.reverse().forEach(([tag, start, end]) => {
let token = { tag };
token.esc = tokens.splice(start, end - start, token);
});
}
unescapeTags(tokens, selector) {
tokens.splice(0, Infinity, ...tokens.flatMap(token => (
token.esc && (!selector || token.tag.matches(selector))
? token.esc
: [token]
)));
}
unescape(tokens) {
let els = [$('<div>')[0]], lastText = '';
tokens.forEach(token => {
if (token.openingTag) {
if (lastText) {
els.at(-1).append(lastText);
lastText = '';
}
let el = token.openingTag;
if (!token.link) {
el = el.cloneNode();
}
els.at(-1).append(el);
els.push(el);
} else if (token.closingTag) {
let el = els.pop();
if (!els.length) {
throw `too many closing tags.\n\n${el.innerHTML}`;
}
if (el.tagName !== token.closingTag.tagName) {
throw `expected </${
token.closingTag.tagName
}>, got </${el.tagName}>.\n\n${els[0].innerHTML}`;
}
if (lastText) {
el.append(lastText);
lastText = '';
}
} else {
lastText += token.s;
}
});
if (lastText) {
els.at(-1).append(lastText);
}
if (els.length !== 1) {
throw `too many opening tags.\n\n${els[0].innerHTML}`;
}
return els[0];
}
verify(div, newDiv) {
let origHtml = div.innerHTML;
let $comp = $(newDiv).clone();
$comp.find('.smartdiff-link').contents().unwrap();
let comp = $comp.html().replace(/<\/(ins|del)><\1(?: [^>]+)?>/g, '');
if (comp !== origHtml) {
throw `changed substance.\n\nOriginal:\n${origHtml}\n\nCleaned:\n${comp}\n\nFull:\n${newDiv.innerHTML}`;
}
let emptyCells = newDiv.querySelectorAll(':is(ins, del):empty');
if (emptyCells.length) {
console.log('Empty cells found:', ...emptyCells);
mw.notify('Empty cells found');
}
}
async exec(tokens, isSecond) {
let s = tokens.map(token => token.s || '').join('');
let modes = isSecond
? ['url', 'link', 'template', 'gallery']
: ['url', 'link', 'template', 'heading', 'gallery'];
for (let mode of modes) {
await this.search(s, tokens, mode);
}
}
async search(s, tokens, mode, data) {
for (let match of s.matchAll(SmartDiff[mode + 'Re'])) {
await this[mode + 'Callback'](
tokens,
...match.indices.slice(1).map((indices, i) => indices && {
s: match[i + 1],
indices,
tokens,
tokenIndices: SmartDiff.tokenIndices
}),
data
);
}
}
static tokenIndices() {
let count = 0, start;
for (let [i, token] of this.tokens.entries()) {
if (token.s) {
if (count === this.indices[0]) {
start = i;
}
if (count === this.indices[1] - 1) {
return [start, i + 1];
}
count++;
}
}
}
static linkRe = /\[\[\s*(?:<tvar(?: [^>]*)?>\s*)?((?:[^<>\[\]{|}])+?)(?:\s*<\/tvar>|\{\{#translation:\s*\}\})?\s*(?:\||\]\])/dg;
async linkCallback(tokens, mid) {
let s = this.decodeWikitext(mid.s, true);
let t;
if (s.startsWith('/')) {
t = await this.getSubpage(s);
} else if (s.startsWith('#') && (!this.isView || !this.isSamePage)) {
t = mw.Title.newFromText(this.getProp() + s);
} else if (s.startsWith('../')) {
t = await this.getParentPage(s);
} else {
t = mw.Title.newFromText(s);
}
if (!t) return;
this.link(tokens, ...mid.tokenIndices(), t);
}
static templateRe = /((?<!{){{\s*(?:(?:#invoke|(?:safe)?subst|msg(?:nw)?|raw|int):\s*)?(?:<tvar(?: [^>]*)?>\s*)?)([^<>\[\]{|}]+?)((?:\s*<\/tvar>)?\s*(\|.*)?(?:}}|$))/dg;
async templateCallback(tokens, pre, mid, post, args) {
let s = this.decodeWikitext(mid.s, true);
let t;
if (pre.s.includes('invoke')) {
t = mw.Title.makeTitle(828, s);
} else if (s.startsWith('#')) {
return;
} else if (pre.s.includes('int')) {
t = mw.Title.makeTitle(8, s);
} else if (s.startsWith('/')) {
t = await this.getSubpage(s, 10);
} else if (s.startsWith('../')) {
t = await this.getParentPage(s, 10);
} else {
if (!pre.s.includes('msg') && !pre.s.includes('raw')) {
let match = s.match(/^[^:]+(?::(?=.)|$)/);
if (match && SmartDiff.magicWords.includes(match[0])) return;
}
t = mw.Title.newFromText(s, 10);
}
if (t) {
this.link(tokens, ...mid.tokenIndices(), t);
}
if (!args) return;
let [postStart, postEnd] = post.tokenIndices();
let newPost = tokens.slice(postStart, postEnd);
await this.search(post.s, newPost, 'template');
if (!t || !SmartDiff.templates) return;
let name = t.getMainText();
for (let data of SmartDiff.templates) {
if (Array.isArray(data?.names) &&
Array.prototype.includes.call(data.names, name)
) {
await this.search(
post.s,
newPost,
'templateArgs',
{ ...data, i: 1 }
);
}
}
tokens.splice(postStart, postEnd - postStart, ...newPost);
}
static templateArgsRe = /\|\s*(?:([^=|]*?)\s*=)?\s*(?:<tvar(?: [^>]*)?>\s*)?(.*?)(?:\s*<\/tvar>)?\s*(?=\||{{!}}|}}|$)/dg;
templateArgsCallback(tokens, name, mid, data) {
let n = name ? Number(name.s) || name.s : data.i++;
if (typeof n !== 'number' || (
n < data.start || n > data.end ||
(n % 2 ? data.skipOdd : data.skipEven)
)) {
return;
}
if (!mid.s) return;
let s = this.decodeWikitext(mid.s);
if (data.prefix) {
s = data.prefix + s;
}
if (data.suffix) {
s += data.suffix;
}
let t = data.forceNs
? mw.Title.makeTitle(data.namespace, s)
: mw.Title.newFromText(s, data.namespace);
if (!t) return;
this.link(
tokens,
...mid.tokenIndices(),
t,
n >= data.noRedirectStart || n <= data.noRedirectEnd
);
}
static urlRe = /(\[(?=.+\]))?((?:\bhttps?:)?\/\/[^\s"<>\[\]{|}]*)([^\s"<>\[\]{|}])/dg;
urlCallback(tokens, pre, mid, last) {
let omitLast;
if (!pre) {
if (mid.s.startsWith('//')) return;
if (last.s === ';') {
let match = mid.s.match(/&#?[\da-z]+$/i);
if (match) {
let source = match[0] + last.s;
omitLast = this.decodeWikitext(source) === source;
} else {
omitLast = true;
}
} else {
let trails = '!,.:?';
if (!mid.s.includes('(')) {
trails += ')';
}
omitLast = trails.includes(last.s);
}
}
let s = mid.s;
if (!omitLast) {
s += last.s;
}
let [midStart, midEnd] = mid.tokenIndices();
this.link(
tokens,
midStart,
omitLast ? midEnd : last.tokenIndices()[1],
this.decodeWikitext(s)
);
}
static headingRe = /^(={1,6})\s*(.+?)\s*(\1)\s*(?=<!--|$)/dg;
headingCallback(tokens, pre, mid, post) {
if (/{{|<\/?ref(?:\s[^>]*)?>|\[(?:https?:)?\/\/[^\s\[\]]+\]/i.test(mid.s)) return;
let s = this.decodeWikitext(mid.s).replace(
/'''(.+?)'''|\[\[:?(?:[^|\]]+\|)?([^\]]+)\]\]|<\/?(?:abbr|b|bdi|bdo|big|cite|code|data|del|dfn|em|font|i|ins|kbd|mark|nowiki|q|rb|rp|rt|rtc|ruby|s|samp|small|span|strike|strong|sub|sup|templatestyles|time|translate|tt|u|var)(?:\s[^>]*)?>|<!--.*?-->|\[(?:https?:)?\/\/[^\s\[\]]+\s([^\]]+)\]/gi,
'$1$2$3'
).replace(/''(.+?)''/g, '$1').replace(/^[\s_]+|[\s_]+$/g, '');
let t = mw.Title.newFromText(
`${!this.isView || !this.isSamePage ? this.getProp() : ''}#${s}`
);
if (!t) return;
let midIndices = mid.tokenIndices();
if (tokens.slice(...midIndices).some(({ link }) => link)) {
this.link(tokens, ...post.tokenIndices(), t);
this.link(tokens, ...pre.tokenIndices(), t);
} else {
this.link(tokens, ...midIndices, t);
}
}
// https://commons.wikimedia.org/wiki/Special:MediaStatistics
static galleryRe = /^\s*((?:[^<>\[\]{|}])+\.(?:apng|djv|djvu|flac|gif|jpe|jpeg|jpg|jps|kar|m4a|m4b|m4p|m4r|m4v|mid|midi|mp2|mp3|mp4|mpa|mpe|mpeg|mpg|mpga|oga|ogg|ogm|ogv|ogx|opus|pdf|png|spx|stl|svg|tif|tiff|wav|webm|webp|xcf))\s*(?:\||$)/dgi;
galleryCallback(tokens, mid) {
let s = this.decodeWikitext(mid.s, true);
let t = mw.Title.newFromText(s, 6);
if (t && t.getNamespaceId() !== 6) {
t = mw.Title.makeTitle(6, s);
}
if (!t) return;
this.link(tokens, ...mid.tokenIndices(), t);
}
link(tokens, start, end, mwtOrUrl, noRedirect) {
let indices = [], last;
tokens.slice(start, end).forEach((token, i) => {
if (!token.s && !token.link && !token.esc) return;
if (token.s && !token.link) {
if (!indices.length || last?.link || last?.esc) {
indices.push([start + i]);
}
indices.at(-1)[1] = start + i + 1;
}
last = token;
});
if (indices.some(chunkIndices => chunkIndices.length !== 2)) {
mw.notify('Corrupt chunks found');
debugger;
}
if (!indices.length) return;
let attribs = { class: 'smartdiff-link' }, obj;
if (typeof mwtOrUrl === 'string') {
attribs.class += ' external';
attribs.href = mwtOrUrl;
attribs.rel = 'nofollow';
} else {
let title = mwtOrUrl.toText();
if (title) {
if (Object.hasOwn(this.titles, title)) {
obj = this.titles[title];
if (obj.classes) {
attribs.class += ' ' + obj.classes.join(' ');
}
} else {
obj = { links: [] };
this.titles[title] = obj;
}
let fragment;
if (obj.url) {
attribs.href = obj.url;
fragment = mwtOrUrl.getFragment();
} else {
let params = obj.red && { action: 'edit', redlink: 1 } ||
noRedirect && obj.redirect && { redirect: 'no' };
if (obj.canonical) {
attribs.href = mw.util.getUrl(obj.canonical, params);
fragment = mwtOrUrl.getFragment();
} else {
attribs.href = mwtOrUrl.getUrl(params);
}
}
if (fragment) {
attribs.href += mw.util.getUrl('#' + fragment);
}
attribs.title = obj.canonical || title;
} else {
attribs.class += ' mw-selflink-fragment';
attribs.href = mwtOrUrl.getUrl();
}
}
let links = indices.reverse().map(chunkIndices => (
this.linkChunk(tokens, ...chunkIndices, attribs)
));
if (obj && !obj.processed) {
obj.links.push(...links);
if (noRedirect) {
links.forEach(link => {
this.noRedirLinks.add(link);
});
}
}
this.divHasLink = true;
}
linkChunk(tokens, start, end, attribs) {
let newStart = start, newEnd = end;
let mid = tokens.slice(start, end);
let midTags = mid.filter(({ s }) => !s);
for (let i = 0; ; i += 2) {
if (tokens[newStart - 1]?.openingTag && midTags[i]?.closingTag &&
tokens[newStart - 1].openingTag.tagName === midTags[i].closingTag.tagName
) {
midTags.unshift(tokens[newStart - 1]);
newStart--;
} else {
break;
}
}
for (let i = midTags.length - 1; ; i--) {
if (tokens[newEnd]?.closingTag && midTags[i]?.openingTag &&
tokens[newEnd].closingTag.tagName === midTags[i].openingTag.tagName
) {
midTags.push(tokens[newEnd]);
newEnd++;
} else {
break;
}
}
if (start - newStart) {
console.log(`Moved up ${start - newStart} times`);
}
if (start - newStart > 1) {
mw.notify(`Moved up ${start - newStart} times`);
}
if (newEnd - end) {
console.log(`Moved down ${newEnd - end} times`);
}
if (newEnd - end > 1) {
mw.notify(`Moved down ${newEnd - end} times`);
}
let link = $('<a>').attr(attribs)[0];
let pre = [{ openingTag: link, link }];
let post = [{ closingTag: link, link }];
if (midTags[0]?.closingTag) {
pre.unshift({ ...midTags[0] });
pre.push({ openingTag: midTags[0].closingTag });
}
if (midTags.at(-1)?.openingTag) {
post.unshift({ closingTag: midTags.at(-1).openingTag });
post.push({ ...midTags.at(-1) });
}
tokens.splice(newEnd, 0, ...post);
tokens.splice(newStart, 0, ...pre);
mid.forEach(token => {
if (token.s) {
token.link = link;
}
});
return link;
}
decodeWikitext(text, decodePercents) {
let s = text;
if (decodePercents) {
try {
s = decodeURIComponent(s);
} catch {}
}
return $($.parseHTML(s)).text();
}
async getSubpage(s, fallbackNs) {
if (await this.canHaveSubpages()) {
return mw.Title.newFromText(
this.getProp() + s.replace(/\/+$/, '')
);
}
return mw.Title.newFromText(s, fallbackNs);
}
async getParentPage(s, fallbackNs) {
if (await this.canHaveSubpages()) {
let chunks = s.split('/');
let levelCount = chunks.findIndex(v => v !== '..');
let sup = this.getProp().split('/').slice(0, -levelCount).join('/');
if (sup) {
let sub = chunks.slice(levelCount).join('/').replace(/\/+$/, '');
return mw.Title.newFromText(sub ? `${sup}/${sub}` : sup);
}
}
return mw.Title.newFromText(s, fallbackNs);
}
async canHaveSubpages(ns = this.getProp('ns')) {
SmartDiff.subNs = SmartDiff.subNs ||
mw.config.get('wgVisualEditorConfig')?.namespacesWithSubpages ||
Object.values((await new mw.Api().get({
action: 'query',
meta: 'siteinfo',
siprop: 'namespaces',
formatversion: 2
})).query.namespaces).filter(n => n.subpages).map(n => n.id);
return SmartDiff.subNs.includes(ns);
}
getProp(n = 'pn') {
if (this[this.side]) {
this.sidePropUsed = !this.isSamePage;
return this[this.side][n];
}
if (Object.hasOwn(this, n)) {
return this[n];
}
this.pn = mw.config.get('wgPageName');
this.ns = mw.config.get('wgNamespaceNumber');
return this[n];
}
async queryTitles() {
let unprocessed = Object.entries(this.titles)
.filter(([, v]) => !v.processed).slice(0, 500).map(([k]) => k);
while (unprocessed.length) {
let curTitles = unprocessed.slice(0, 50);
curTitles.forEach(title => {
this.titles[title].processed = true;
});
let { query } = await new mw.Api().post({
action: 'query',
titles: curTitles,
iwurl: 1,
prop: 'info',
inprop: 'linkclasses',
inlinkcontext: this.getProp(),
formatversion: 2
}, {
headers: { 'Promise-Non-Write-API-Action': 1 }
});
query.pages?.forEach(p => {
let obj = (this.titles[p.title] ??= {});
let classes = p.linkclasses || [];
if (p.missing && !p.known) {
classes.push('new');
obj.red = true;
} else if (p.redirect) {
obj.redirect = true;
}
if (classes.length) {
obj.classes = classes;
}
});
query.interwiki?.forEach(iw => {
this.titles[iw.title] = { classes: ['extiw'], url: iw.url };
});
query.normalized?.forEach(({ from, to }) => {
this.titles[from] = {
...this.titles[from],
...this.titles[to],
canonical: to
};
});
curTitles.forEach(title => {
let obj = this.titles[title];
if (obj.classes) {
$(obj.links).addClass(obj.classes);
}
if (obj.redirect) {
let noRedirLinks = this.noRedirLinks;
$(obj.links).attr('href', function () {
return mw.util.getUrl(
obj.canonical || title,
noRedirLinks.has(this) && { redirect: 'no' }
) + this.hash;
});
} else {
let url = obj.url ||
obj.red && mw.util.getUrl(
obj.canonical || title,
{ action: 'edit', redlink: 1 }
) ||
obj.canonical && mw.util.getUrl(obj.canonical);
if (url) {
$(obj.links).attr('href', function () {
return url + this.hash;
});
}
}
if (obj.canonical) {
$(obj.links).attr('title', obj.canonical);
}
obj.links = [];
});
unprocessed = unprocessed.slice(50);
}
}
static sds = new WeakMap();
static magicWords = [
'!', '=', 'BASEPAGENAME', 'BASEPAGENAME:', 'BASEPAGENAMEE', 'BASEPAGENAMEE:',
'canonicalurl:', 'CURRENTDAY', 'CURRENTDAY2', 'CURRENTDAYNAME', 'CURRENTDOW',
'CURRENTHOUR', 'CURRENTMONTH', 'CURRENTMONTH1', 'CURRENTMONTHABBREV',
'CURRENTMONTHNAME', 'CURRENTMONTHNAMEGEN', 'CURRENTTIME', 'CURRENTTIMESTAMP',
'CURRENTVERSION', 'CURRENTWEEK', 'CURRENTYEAR', 'DEFAULTCATEGORYSORT:',
'DEFAULTSORT:', 'DEFAULTSORTKEY:', 'DISPLAYTITLE:', 'filepath:', 'formatnum:',
'FULLPAGENAME', 'FULLPAGENAME:', 'FULLPAGENAMEE', 'FULLPAGENAMEE:', 'fullurl:',
'gender:', 'lc:', 'lcfirst:', 'LOCALDAY', 'LOCALDAY2', 'LOCALDAYNAME',
'LOCALDOW', 'LOCALHOUR', 'LOCALMONTH', 'LOCALMONTH1', 'LOCALMONTHABBREV',
'LOCALMONTHNAME', 'LOCALMONTHNAMEGEN', 'LOCALTIME', 'LOCALTIMESTAMP',
'LOCALWEEK', 'LOCALYEAR','NAMESPACE', 'NAMESPACE:', 'NAMESPACEE', 'NAMESPACEE:',
'NAMESPACENUMBER', 'NAMESPACENUMBER:', 'ns:', 'NUMBEROFACTIVEUSERS',
'NUMBEROFARTICLES', 'NUMBEROFEDITS', 'NUMBEROFFILES', 'NUMBEROFPAGES',
'NUMBEROFUSERS', 'padleft:', 'PAGENAME', 'PAGENAMEE', 'PAGESINCAT:',
'PAGESINCATEGORY:', 'plural:', 'REVISIONDAY', 'REVISIONDAY:', 'REVISIONDAY2',
'REVISIONDAY2:', 'REVISIONID', 'REVISIONID:', 'REVISIONMONTH', 'REVISIONMONTH:',
'REVISIONMONTH1', 'REVISIONMONTH1:', 'REVISIONSIZE', 'REVISIONTIMESTAMP',
'REVISIONTIMESTAMP:', 'REVISIONUSER', 'REVISIONUSER:', 'REVISIONYEAR',
'REVISIONYEAR:', 'ROOTPAGENAME', 'ROOTPAGENAME:', 'ROOTPAGENAMEE',
'ROOTPAGENAMEE:', 'SHORTDESC:', 'SUBJECTPAGENAME', 'SUBJECTPAGENAME:',
'SUBJECTPAGENAMEE', 'SUBJECTPAGENAMEE:', 'SUBJECTSPACE', 'SUBJECTSPACE:',
'SUBJECTSPACEE', 'SUBJECTSPACEE:', 'SUBPAGENAME', 'SUBPAGENAME:',
'SUBPAGENAMEE', 'SUBPAGENAMEE:', 'TALKPAGENAME', 'TALKPAGENAME:',
'TALKPAGENAMEE', 'TALKPAGENAMEE:', 'TALKSPACE', 'TALKSPACE:', 'TALKSPACEE',
'TALKSPACEE:', 'uc:', 'ucfirst:', 'urlencode:'
];
}
if (Array.isArray(window.smartdiffMagicWords)) {
SmartDiff.magicWords.push(...window.smartdiffMagicWords);
}
if (Array.isArray(window.smartdiffTemplates)) {
SmartDiff.templates = window.smartdiffTemplates;
}
mw.hook('wikipage.diff').add((window.sdcb = ($diff => {
window.sd = new SmartDiff($diff);
})));
mw.hook('wikipage.diff.wikitextBodyUpdate').add((window.sdcbu = ($rows => {
let $diff = $rows.closest('.diff');
let sd = SmartDiff.sds.get($diff[0]);
if (sd) {
sd.init($rows.filter(function () {
return this.checkVisibility();
}));
} else {
window.sd = new SmartDiff($diff);
}
})));
});
ifpm34nqafv00ce11tjzkhw3zpmbdub
The Reflective Tour
0
179062
769504
768622
2026-10-21T20:30:30Z
Island Hines
76160
/* Set lists */
769504
wikitext
text/x-wiki
{{Short description|2005–06 concert tour by Backstreet Girls}}
{{Use mdy dates|date=October 2020}}
{{Infobox concert
|image =
|image_size =
|border =
|location = {{hlist|Asia|Europe|North America}}
|image_caption =
|album = ''[[Reflective (Backstreet Girls album)|Reflective]]''
|start_date = {{start date|2005|02|27}}
|end_date = {{end date|2006|02|12}}
|attendance =
| gross =
| last_tour = [[Backstreet Times a Charm Tour]] <br>(2004)
| this_tour = '''The Reflective Tour''' <br>(2005–06)
| next_tour = [[The 7/24 Tour]] <br>(2006–07)
|number_of_shows = 63
|concert_tour_name = The Reflective Tour
| artist = [[Backstreet Girls (girl group)|Backstreet Girls]]
|number_of_legs = 5
}}
''' The Reflective Tour''' was the fourth concert tour by American comedy girl group [[Backstreet Girls]]. Visiting Europe, North America and Asia (one date in [[Abu Dhabi|Adu Dhabi]]), the tour supported their debut studio album, ''[[Reflective (Backstreet Girls album)|Reflective]]'' (2005). Backstreet Girls was not backed by a band or backing singers, instead, they opted for studio recorded versions of songs from their album and an acoustic set for covers. The setlist consisted mostly of songs from ''Reflective'', with several adapted from their debut extended play, ''[[Better Tomorrow (EP)|Better Tomorrow]]'' (2003), along with several medley covers. The shows garnered acclaim from critical reviews, many praising the group's vocal performance, the show's overall themes and the song transitions.
==Background==
==Extensions==
===Reflective: The Summer Tour===
===European Reflective Tour===
==Personnel==
===Backstreet Girls===
*[[Jenny McCarthy]] – Vocals
*[[Cindy Crawford]] – Vocals
*[[Pamela Anderson]] – Vocals
*[[Jenna Jameson]] – Vocals
*[[Carmen Electra]] – Vocals
===Band===
*[[John McCurry]] – Guitar
*Alan St. Jon – Keyboards, Backing Vocals
*Roger Chouinard Jr. – Drums
*[[Kane Roberts]] – Guitar, Backing Vocals
*[[Stephen Perkins]] – Percussion
*[[Hugh McDonald]] – Bass, Backing Vocals
===Dancers===
*The Bombshell Babes
==Opening acts==
==Set lists==
{{col-begin}}
{{col-3}}
{{hidden
| headercss = background: #ccccff; font-size: 100%; width: 90%;
| contentcss = text-align: left; font-size: 100%; width: 90%;
| header = Reflective Tour
| content =
# "Nasty Ghetto"
# "Twisted"
# "[[I'll Stand by You (song)|I'll Stand by You]]"
# "[[A Little Bit (song)|A Little Bit]]"
# "Virgin Sexy"
# "Sometimes"
# "[[All for You (Backstreet Girls song)|All for You]]"
# "Interlude: Buster/Conversation's Over" <small> (Instrumental) </small>
# "Blue"
# "[[The Power of Good-Bye]]" <small> ([[Madonna]] song) </small>
# "Maya"
# "[[Love Don't Cost a Thing (song)|Love Don't Cost a Thing]]" / "[[Waiting for Tonight]]" / "[[If You Had My Love]]" <small> (Jennifer Lopez [[Medley (music)|medley]]) </small>
# "We Could Have It All"
# "Million Different Ways"
# "Whatever Makes You Happy"
# "Situation's Heavy"
# "[[Escape (Backstreet Girls song)|Escape]]"
# "More Than a Million Miles"
}}
{{col-3}}
{{hidden
| headercss = background: #ccccff; font-size: 100%; width: 90%;
| contentcss = text-align: left; font-size: 100%; width: 90%;
| header = Summer Tour
| content =
# "[[Escape (Backstreet Girls song)|Escape]]"
# "Nasty Ghetto"
# "Twisted"
# "[[I'll Stand by You (song)|I'll Stand by You]]" / "[[Don't Speak]]"{{efn|This is a cover originally performed by American [[rock music|rock]] band [[No Doubt]].}} / "[[Beautiful (Christina Aguilera song)|Beautiful]]"{{efn|This is a cover originally performed by American singer [[Christina Aguilera]].}} / "[[Never Ever (All Saints song)|Never Ever]]"{{efn|This is a cover originally performed by English girl group [[All Saints (group)|All Saints]].}}
# "[[A Little Bit (song)|A Little Bit]]"
# "Sometimes"
# "Conversation's Over"
# "Whatever Makes You Happy"
# "Virgin Sexy"
# "Situation's Heavy"
# "More Than a Million Miles"
# "We Could Have It All"
# "Maya"
# "Blue"
# "[[Say My Name]]"{{efn|This is a cover originally performed by American girl group [[Destiny's Child]].}} / "[[Family Affair (Mary J. Blige song)|Family Affair]]"{{efn|This is a cover originally performed by American singer [[Mary J. Blige]].}} / "[[Intuition (Jewel song)|Intuition]]"{{efn|This is a cover originally performed by American singer [[Jewel (singer)|Jewel]].}} / "[[I Want It That Way]]"{{efn|This is a cover originally performed by American boy band [[Backstreet Boys]].}} / "[[Bye Bye Bye]]"{{efn|This is a cover originally performed by American boy band [[NSYNC]].}}
# "[[Toxic (song)|Toxic]]" <small> ([[Britney Spears]] cover) </small>
# "Million Different Ways"
# "[[All for You (Backstreet Girls song)|All for You]]"
# "Buster"
}}
{{col end}}
*{{small|'''Note''': All songs in the summer leg are performed in the European leg except "Conversation's Over", which was replaced with "Just a Little While".}}
==Shows==
== Canceled shows ==
==Notes==
{{Notelist}}
==References==
{{Reflist|30em}}
==External links==
*[http://backstreetgirls.com/ Backstreet Girls Official Website]
{{Backstreet Girls (girl group)}}
{{DEFAULTSORT:Reflective Tour, The}}
[[Category:2005 concert tours]]
[[Category:2006 concert tours]]
[[Category:Backstreet Girls (girl group) concert tours]]
la6ymri7zxwdep2opzqh1h51nr8mb1b
Wikipedia:Requests/Permissions/TienMinh-mun6xnChing
4
179200
769618
768282
2026-10-22T10:08:29Z
Johannnes89
48060
/* TienMinh-mun6xnChing */ aw
769618
wikitext
text/x-wiki
=== [[User:TienMinh-mun6xnChing|TienMinh-mun6xnChing]] ===
* {{User3|TienMinh-mun6xnChing}}, [[Special:CentralAuth/TienMinh-mun6xnChing|global contribs]] 12:09, 12 October 2026 (UTC)
* '''Motive for request:''' I'm a new member on this project, I need autoconfirmed status and these rights for technical repairing on Vietnamese Wiktionary, I'm having problem on moving page and getting some restrictions from abuse filter.
* '''Requested rights:''' Confirmed user, Account creator, event organizer, new page reviewer, template editor, temporary account IP viewer, rollbacker, transwiki importer
* '''Comments:''' You are autoconfirmed on testwiki which would make ''confirmed'' redundant. You don't meet the criteria for temporary account IP viewer and haven't demonstrated any need for account creator, event organizer, new page reviewer, template editor, rollbacker and transwiki importer. It doesn't seem like you're aware of what you're actually requesting. --[[User:Johannnes89|Johannnes89]] ([[User talk:Johannnes89|talk]]) 10:08, 22 October 2026 (UTC)
9ten485k2i5w6rbzx4evwlrq5e8sp8h
User:OUT is this/WikiFontsAddonBigdataTest.js
2
179224
769437
769422
2026-10-21T12:44:26Z
OUT is this
73801
769437
javascript
text/javascript
(function() {
'use strict';
if (!mw.config.get('wgUserId')) return;
var KEY = 'wikifonts_history_addon', T_KEY = 'wikifonts_history_toggle_state';
var DB = 'PureFontUploaderDB', STORE = 'fontStore';
var isOpen = localStorage.getItem(T_KEY) === 'open';
var newUploadedDataUrl = null;
var currentSelectedFontName = '';
var originalFontBeforeOpen = null;
function openDB() {
return $.Deferred(function(d) {
var r = indexedDB.open(DB, 1);
r.onupgradeneeded = function(e) { if (!e.target.result.objectStoreNames.contains(STORE)) e.target.result.createObjectStore(STORE, { keyPath: 'id' }); };
r.onsuccess = function(e) { d.resolve(e.target.result); };
}).promise();
}
function getFont(name, cb) {
openDB().done(function(db) {
db.transaction([STORE], 'readonly').objectStore(STORE).get(name).onsuccess = function(e) { cb(e.target.result); };
});
}
function loadH() {
try {
var h = JSON.parse(localStorage.getItem(KEY) || '[]');
if (!h.some(function(i){return i.name==='default'})) { h.push({name:'default',starred:false,time:0}); localStorage.setItem(KEY, JSON.stringify(h)); }
return h;
} catch(e) { return [{name:'default',starred:false,time:0}]; }
}
function applyFont(name, url) {
$('#testFontFace').remove();
if (name === 'default') {
document.body.style.removeProperty('font-family');
return;
}
if (url) $('<style>', { id: 'testFontFace' }).text('@font-face { font-family: "' + name + '"; src: url("' + url + '"); }').appendTo('head');
document.body.style.setProperty('font-family', name, 'important');
}
// 【同期】リロード時の自動復元
$(document).ready(function() {
var h = loadH();
if (h.length && h[0].name !== 'default') {
getFont(h[0].name, function(r) { if (r) applyFont(r.id, r.dataUrl); else applyFont(h[0].name); });
}
});
$(document).ready(function() {
$('#pt-font-setup').remove();
mw.util.addPortletLink('p-personal', 'javascript:void(0);', '⚙️ Font Setup', 'pt-font-setup', 'Open uploader', null, '#pt-logout');
function updateH(font) {
if (!font) return;
var h = loadH(), idx = h.findIndex(function(i){return i.name===font}), star = idx !== -1 ? h[idx].starred : false;
if (idx !== -1) h.splice(idx, 1);
h.unshift({name: font, starred: star, time: Date.now()});
var s = h.filter(function(i){return i.starred && i.name!=='default'}),
u = h.filter(function(i){return !i.starred && i.name!=='default'});
h = s.sort(function(a,b){return b.time-a.time}).concat(u.sort(function(a,b){return b.time-a.time}));
h.push({name:'default',starred:false,time:0});
try {
localStorage.setItem(KEY, JSON.stringify(h));
} catch (e) {
alert('⚠️ 【容量警告】LocalStorageの容量が不足しているため履歴の保存に失敗しました。');
}
ui();
}
function ui() {
var $box = $('#pureFontModal'); if (!$box.length) return;
$('#wikifonts-history-section').remove();
var $sec = $('<div>',{id: 'wikifonts-history-section'}).css({'margin-top':'15px','padding-top':'10px','border-top':'1px solid #72777d','text-align':'left'});
var $hdr = $('<div>').css({'display':'flex','justify-content':'space-between','align-items':'center','padding':'4px 0','color':'#000','font-size':'13px'});
var $titleLeft = $('<span>').text('History').css({'color':'#000','font-weight':'bold'});
var $toggleLink = $('<span>', { id: 'wf-history-arrow' }).text(isOpen ? '[非表示]' : '[表示]').css({'color':'#36c','cursor':'pointer','font-size':'12px'});
$toggleLink.on('click', function() {
isOpen = $list.is(':hidden');
$list.toggle(isOpen);
$toggleLink.text(isOpen ? '[非表示]' : '[表示]');
localStorage.setItem(T_KEY, isOpen ? 'open' : 'close');
});
$hdr.append($titleLeft).append($toggleLink);
var $list = $('<div>',{id: 'wikifonts-history-list'}).css({'display':isOpen?'block':'none','max-height':'120px','overflow-y':'auto','margin-top':'5px'});
loadH().forEach(function(item) {
if (item.name === 'default') return;
var $row = $('<div>').css({'display':'flex','align-items':'center','justify-content':'space-between','padding':'4px 0','border-bottom':'1px solid rgba(0,0,0,0.1)'});
var $left = $('<div>').css({'display':'flex','align-items':'center','gap':'8px'});
$('<span>').text(item.starred?'⭐':'☆').css({'cursor':'pointer','font-size':'12px','color':item.starred?'#ffc107':'#000'})
.on('click', function(e) { e.stopPropagation();
var h = loadH(), i = h.find(function(f){return f.name===item.name});
if (i) { i.starred = !i.starred; localStorage.setItem(KEY, JSON.stringify(h)); ui(); }
}).appendTo($left);
// 各行のフォント名ラベルに最初から確実に自身のfont-familyを適用
var $nameLabel = $('<span>').text(item.name).css({
'font-family': '"' + item.name + '", sans-serif',
'cursor': 'pointer',
'font-size': '13px',
'color': '#000'
}).on('mouseenter', function() { $(this).css({'color':'#0074D9','text-decoration':'underline'}); })
.on('mouseleave', function() { $(this).css({'color':'#000','text-decoration':'none'}); })
.on('click', function() {
$('#pureNameInput').val(item.name);
currentSelectedFontName = item.name;
newUploadedDataUrl = null;
getFont(item.name, function(r){ applyFont(item.name, r?r.dataUrl:null); });
});
// 各フォントごとの@font-faceスタイルを動的にheadに追加
getFont(item.name, function(r) {
if (r && r.dataUrl) {
var listStyleId = 'testFontFace-' + item.name.replace(/[^a-zA-Z0-9_-]/g, '');
if (!$('#' + listStyleId).length) {
$('<style>', { id: listStyleId }).text('@font-face { font-family: "' + item.name + '"; src: url("' + r.dataUrl + '"); }').appendTo('head');
}
}
});
$nameLabel.appendTo($left);
var $rightBtns = $('<div>').css({'display':'flex','align-items':'center','gap':'4px'});
// ✏️ 編集ボタン(名前変更用)
var $editBtn = $('<span>').text('✏️').css({'cursor':'pointer','font-size':'10px','padding':'2px','opacity':'0.6'})
.on('mouseenter', function() { $(this).css('opacity', '1'); })
.on('mouseleave', function() { $(this).css('opacity', '0.6'); })
.on('click', function(e) {
e.stopPropagation();
var newName = prompt('新しいフォント名を入力してください:', item.name);
if (!newName || !(newName = newName.trim())) return;
if (newName === item.name) return;
getFont(item.name, function(r) {
if (!r || !r.dataUrl) {
alert('フォントデータの取得に失敗しました。');
return;
}
openDB().done(function(db) {
var tx = db.transaction([STORE], 'readwrite');
var store = tx.objectStore(STORE);
store.delete(item.name);
store.put({ id: newName, dataUrl: r.dataUrl });
tx.oncomplete = function() {
var h = loadH();
var target = h.find(function(f){return f.name===item.name});
var star = target ? target.starred : false;
h = h.filter(function(f){return f.name!==item.name});
h.unshift({name: newName, starred: star, time: Date.now()});
var s = h.filter(function(i){return i.starred && i.name!=='default'}),
u = h.filter(function(i){return !i.starred && i.name!=='default'});
h = s.sort(function(a,b){return b.time-a.time}).concat(u.sort(function(a,b){return b.time-a.time}));
h.push({name:'default',starred:false,time:0});
localStorage.setItem(KEY, JSON.stringify(h));
applyFont(newName, r.dataUrl);
ui();
};
});
});
});
// ❌ 削除ボタン
var $deleteBtn = $('<span>').text('❌').css({'cursor':'pointer','font-size':'10px','padding':'2px','opacity':'0.5'})
.on('mouseenter', function() { $(this).css('opacity', '1'); })
.on('mouseleave', function() { $(this).css('opacity', '0.5'); })
.on('click', function(e) { e.stopPropagation();
if (confirm('フォント「' + item.name + '」を削除しますか?')) {
var h = loadH().filter(function(f){return f.name!==item.name});
localStorage.setItem(KEY, JSON.stringify(h));
openDB().done(function(db){
db.transaction([STORE], 'readwrite').objectStore(STORE).delete(item.name);
}); ui();
}
});
$rightBtns.append($editBtn).append($deleteBtn);
$row.append($left).append($rightBtns).appendTo($list);
});
if (!$list.children().length) $('<div>').text('No history').css({'color':'#54595d','font-style':'italic','padding':'4px 0','font-size':'12px'}).appendTo($list);
$('#pureFontModal-btn-container').before($sec.append($hdr).append($list));
}
$(document).on('click', '#pt-font-setup', function() {
$('#pureFontModal').remove();
newUploadedDataUrl = null;
currentSelectedFontName = '';
var hLoad = loadH();
originalFontBeforeOpen = (hLoad.length && hLoad[0].name) ? hLoad[0].name : 'default';
var topName = (hLoad.length && hLoad[0].name !== 'default') ? hLoad[0].name : '';
var $modal = $('<div>', { id: 'pureFontModal' }).css({
'position': 'fixed', 'top': '20%', 'left': '50%', 'transform': 'translate(-50%, -20%)',
'background': '#fff', 'padding': '20px', 'border': '1px solid #72777d', 'border-radius': '4px',
'box-shadow': '0 2px 10px rgba(0,0,0,0.3)', 'z-index': '999999', 'width': '320px', 'color': '#000'
}).html(
'<h3 style="margin:0 0 15px 0; font-size:16px; text-align:left;">Font Uploader & History</h3>' +
'<label style="display:block; font-size:12px; margin-bottom:5px; text-align:left;">1. Select Font File (.ttf / .woff2 / .otf):</label>' +
'<input type="file" id="pureFileInput" accept=".ttf,.woff,.woff2,.otf" style="display:block; margin-bottom:15px; width:100%; font-size:12px;">' +
'<label style="display:block; font-size:12px; margin-bottom:5px; text-align:left;">2. Enter Font Name:</label>' +
'<input type="text" id="pureNameInput" style="display:block; margin-bottom:20px; width:100%; padding:4px; box-sizing:border-box;" value="' + topName + '">'
);
var $btnBox = $('<div>', { id: 'pureFontModal-btn-container' }).css({ 'display': 'flex', 'justify-content': 'space-between', 'align-items': 'center', 'margin-top': '15px' });
var $resetBtn = $('<button>', { id: 'pureResetBtn' }).text('default').css({ 'padding': '5px 10px', 'font-size': '12px', 'cursor': 'pointer', 'background': '#d33', 'color': '#fff', 'border': 'none', 'border-radius': '2px', 'font-weight': 'bold' });
$resetBtn.on('click', function(e) {
e.stopPropagation();
var h = loadH();
var idx = h.findIndex(function(i){return i.name==='default'});
if (idx !== -1) {
var dItem = h.splice(idx, 1)[0];
h.unshift(dItem);
}
localStorage.setItem(KEY, JSON.stringify(h));
applyFont('default');
$('#pureNameInput').val('');
newUploadedDataUrl = null;
currentSelectedFontName = 'default';
ui();
});
var $rightBtns = $('<div>').css({ 'display': 'flex', 'gap': '10px' });
var $cancel = $('<button>', { id: 'pureCloseBtn' }).text('Cancel').css({ 'padding': '5px 10px', 'font-size': '12px', 'cursor': 'pointer' });
var $save = $('<button>', { id: 'pureSaveBtn' }).text('Save').css({ 'padding': '5px 15px', 'font-size': '12px', 'cursor': 'pointer', 'background': '#36c', 'color': '#fff', 'border': 'none', 'border-radius': '2px' });
$rightBtns.append($cancel).append($save);
$btnBox.append($resetBtn).append($rightBtns);
$modal.append($btnBox).appendTo('body');
ui();
});
$(document).on('change', '#pureFileInput', function() {
if (this.files && this.files[0]) {
var file = this.files[0];
var r = new FileReader();
r.onload = function(e) {
newUploadedDataUrl = e.target.result;
currentSelectedFontName = '';
var fontName = $('#pureNameInput').val().trim();
if (!fontName) {
fontName = file.name.substring(0, file.name.lastIndexOf('.')) || 'TempFont';
$('#pureNameInput').val(fontName);
}
applyFont(fontName, newUploadedDataUrl);
};
r.readAsDataURL(file);
}
});
$(document).on('input', '#pureNameInput', function() {
var fontName = $(this).val().trim();
if (fontName && newUploadedDataUrl) {
applyFont(fontName, newUploadedDataUrl);
}
});
$(document).on('click', '#pureSaveBtn', function() {
var name = $('#pureNameInput').val().trim(); if (!name) return alert('フォント名を入力してください。');
try {
if (newUploadedDataUrl) {
openDB().done(function(db) {
var tx = db.transaction([STORE], 'readwrite');
tx.objectStore(STORE).put({ id: name, dataUrl: newUploadedDataUrl });
tx.oncomplete = function() {
updateH(name); alert('保存しました!'); $('#pureFontModal').remove(); newUploadedDataUrl = null;
};
tx.onerror = function() {
alert('⚠️ 【容量オーバー警告】IndexedDBへの書き込みに失敗しました。');
};
});
} else if (currentSelectedFontName) {
if (currentSelectedFontName === 'default') {
applyFont('default');
}
updateH(currentSelectedFontName); $('#pureFontModal').remove();
} else {
updateH(name); applyFont(name); $('#pureFontModal').remove();
}
} catch (error) {
alert('❌ エラーが発生したため保存を中断しました:\n' + error.message);
$('#pureFontModal').remove();
}
}).on('click', '#pureCloseBtn', function() {
$('#pureFontModal').remove();
newUploadedDataUrl = null;
currentSelectedFontName = '';
var targetFont = originalFontBeforeOpen;
if (targetFont && targetFont !== 'default') {
getFont(targetFont, function(r) { applyFont(targetFont, r ? r.dataUrl : null); });
} else {
applyFont('default');
}
});
});
})();
lfk1blbxnwjhszgdkc1ipjkdi8vb49y
769439
769437
2026-10-21T12:47:33Z
OUT is this
73801
769439
javascript
text/javascript
(function() {
'use strict';
if (!mw.config.get('wgUserId')) return;
var KEY = 'wikifonts_history_addon', T_KEY = 'wikifonts_history_toggle_state';
var DB = 'PureFontUploaderDB', STORE = 'fontStore';
var isOpen = localStorage.getItem(T_KEY) === 'open';
var newUploadedDataUrl = null;
var currentSelectedFontName = '';
var originalFontBeforeOpen = null;
function openDB() {
return $.Deferred(function(d) {
var r = indexedDB.open(DB, 1);
r.onupgradeneeded = function(e) { if (!e.target.result.objectStoreNames.contains(STORE)) e.target.result.createObjectStore(STORE, { keyPath: 'id' }); };
r.onsuccess = function(e) { d.resolve(e.target.result); };
}).promise();
}
function getFont(name, cb) {
openDB().done(function(db) {
db.transaction([STORE], 'readonly').objectStore(STORE).get(name).onsuccess = function(e) { cb(e.target.result); };
});
}
function loadH() {
try {
var h = JSON.parse(localStorage.getItem(KEY) || '[]');
if (!h.some(function(i){return i.name==='default'})) { h.push({name:'default',starred:false,time:0}); localStorage.setItem(KEY, JSON.stringify(h)); }
return h;
} catch(e) { return [{name:'default',starred:false,time:0}]; }
}
function applyFont(name, url) {
$('#testFontFace').remove();
if (name === 'default') {
document.body.style.removeProperty('font-family');
return;
}
if (url) $('<style>', { id: 'testFontFace' }).text('@font-face { font-family: "' + name + '"; src: url("' + url + '"); }').appendTo('head');
document.body.style.setProperty('font-family', name, 'important');
}
// 【同期】リロード時の自動復元
$(document).ready(function() {
var h = loadH();
if (h.length && h[0].name !== 'default') {
getFont(h[0].name, function(r) { if (r) applyFont(r.id, r.dataUrl); else applyFont(h[0].name); });
}
});
$(document).ready(function() {
$('#pt-font-setup').remove();
mw.util.addPortletLink('p-personal', 'javascript:void(0);', '⚙️ Font Setup', 'pt-font-setup', 'Open uploader', null, '#pt-logout');
function updateH(font) {
if (!font) return;
var h = loadH(), idx = h.findIndex(function(i){return i.name===font}), star = idx !== -1 ? h[idx].starred : false;
if (idx !== -1) h.splice(idx, 1);
h.unshift({name: font, starred: star, time: Date.now()});
var s = h.filter(function(i){return i.starred && i.name!=='default'}),
u = h.filter(function(i){return !i.starred && i.name!=='default'});
h = s.sort(function(a,b){return b.time-a.time}).concat(u.sort(function(a,b){return b.time-a.time}));
h.push({name:'default',starred:false,time:0});
try {
localStorage.setItem(KEY, JSON.stringify(h));
} catch (e) {
alert('⚠️ 【容量警告】LocalStorageの容量が不足しているため履歴の保存に失敗しました。');
}
ui();
}
function ui() {
var $box = $('#pureFontModal'); if (!$box.length) return;
$('#wikifonts-history-section').remove();
var $sec = $('<div>',{id: 'wikifonts-history-section'}).css({'margin-top':'15px','padding-top':'10px','border-top':'1px solid #72777d','text-align':'left'});
var $hdr = $('<div>').css({'display':'flex','justify-content':'space-between','align-items':'center','padding':'4px 0','color':'#000','font-size':'13px'});
var $titleLeft = $('<span>').text('History').css({'color':'#000','font-weight':'bold'});
var $toggleLink = $('<span>', { id: 'wf-history-arrow' }).text(isOpen ? '[非表示]' : '[表示]').css({'color':'#36c','cursor':'pointer','font-size':'12px'});
$toggleLink.on('click', function() {
isOpen = $list.is(':hidden');
$list.toggle(isOpen);
$toggleLink.text(isOpen ? '[非表示]' : '[表示]');
localStorage.setItem(T_KEY, isOpen ? 'open' : 'close');
});
$hdr.append($titleLeft).append($toggleLink);
var $list = $('<div>',{id: 'wikifonts-history-list'}).css({'display':isOpen?'block':'none','max-height':'120px','overflow-y':'auto','margin-top':'5px'});
loadH().forEach(function(item) {
if (item.name === 'default') return;
var $row = $('<div>').css({'display':'flex','align-items':'center','justify-content':'space-between','padding':'4px 0','border-bottom':'1px solid rgba(0,0,0,0.1)'});
var $left = $('<div>').css({'display':'flex','align-items':'center','gap':'8px'});
$('<span>').text(item.starred?'⭐':'☆').css({'cursor':'pointer','font-size':'12px','color':item.starred?'#ffc107':'#000'})
.on('click', function(e) { e.stopPropagation();
var h = loadH(), i = h.find(function(f){return f.name===item.name});
if (i) { i.starred = !i.starred; localStorage.setItem(KEY, JSON.stringify(h)); ui(); }
}).appendTo($left);
var $nameLabel = $('<span>').text(item.name).css({
'font-family': '"' + item.name + '", sans-serif',
'cursor': 'pointer',
'font-size': '13px',
'color': '#000'
}).on('mouseenter', function() { $(this).css({'color':'#0074D9','text-decoration':'underline'}); })
.on('mouseleave', function() { $(this).css({'color':'#000','text-decoration':'none'}); })
.on('click', function() {
$('#pureNameInput').val(item.name);
currentSelectedFontName = item.name;
newUploadedDataUrl = null;
getFont(item.name, function(r){ applyFont(item.name, r?r.dataUrl:null); });
});
// 各行ごとに確実に@font-faceを生成・適用
getFont(item.name, function(r) {
if (r && r.dataUrl) {
var listStyleId = 'testFontFace-' + item.name.replace(/[^a-zA-Z0-9_-]/g, '');
$('#' + listStyleId).remove(); // 古いスタイルがあれば一度削除して競合を防ぐ
$('<style>', { id: listStyleId }).text('@font-face { font-family: "' + item.name + '"; src: url("' + r.dataUrl + '"); }').appendTo('head');
$nameLabel.css('font-family', '"' + item.name + '", sans-serif');
}
});
$nameLabel.appendTo($left);
var $rightBtns = $('<div>').css({'display':'flex','align-items':'center','gap':'4px'});
// ✏️ 編集ボタン(名前変更用)
var $editBtn = $('<span>').text('✏️').css({'cursor':'pointer','font-size':'10px','padding':'2px','opacity':'0.6'})
.on('mouseenter', function() { $(this).css('opacity', '1'); })
.on('mouseleave', function() { $(this).css('opacity', '0.6'); })
.on('click', function(e) {
e.stopPropagation();
var newName = prompt('新しいフォント名を入力してください:', item.name);
if (!newName || !(newName = newName.trim())) return;
if (newName === item.name) return;
getFont(item.name, function(r) {
if (!r || !r.dataUrl) {
alert('フォントデータの取得に失敗しました。');
return;
}
openDB().done(function(db) {
var tx = db.transaction([STORE], 'readwrite');
var store = tx.objectStore(STORE);
store.delete(item.name); // 旧データの削除
store.put({ id: newName, dataUrl: r.dataUrl }); // 新名で登録
tx.oncomplete = function() {
// 古いスタイルのDOMを削除
$('#testFontFace-' + item.name.replace(/[^a-zA-Z0-9_-]/g, '')).remove();
var h = loadH();
var target = h.find(function(f){return f.name===item.name});
var star = target ? target.starred : false;
h = h.filter(function(f){return f.name!==item.name});
h.unshift({name: newName, starred: star, time: Date.now()});
var s = h.filter(function(i){return i.starred && i.name!=='default'}),
u = h.filter(function(i){return !i.starred && i.name!=='default'});
h = s.sort(function(a,b){return b.time-a.time}).concat(u.sort(function(a,b){return b.time-a.time}));
h.push({name:'default',starred:false,time:0});
localStorage.setItem(KEY, JSON.stringify(h));
applyFont(newName, r.dataUrl);
ui();
};
});
});
});
// ❌ 削除ボタン
var $deleteBtn = $('<span>').text('❌').css({'cursor':'pointer','font-size':'10px','padding':'2px','opacity':'0.5'})
.on('mouseenter', function() { $(this).css('opacity', '1'); })
.on('mouseleave', function() { $(this).css('opacity', '0.5'); })
.on('click', function(e) { e.stopPropagation();
if (confirm('フォント「' + item.name + '」を削除しますか?')) {
var h = loadH().filter(function(f){return f.name!==item.name});
localStorage.setItem(KEY, JSON.stringify(h));
$('#testFontFace-' + item.name.replace(/[^a-zA-Z0-9_-]/g, '')).remove();
openDB().done(function(db){
db.transaction([STORE], 'readwrite').objectStore(STORE).delete(item.name);
}); ui();
}
});
$rightBtns.append($editBtn).append($deleteBtn);
$row.append($left).append($rightBtns).appendTo($list);
});
if (!$list.children().length) $('<div>').text('No history').css({'color':'#54595d','font-style':'italic','padding':'4px 0','font-size':'12px'}).appendTo($list);
$('#pureFontModal-btn-container').before($sec.append($hdr).append($list));
}
$(document).on('click', '#pt-font-setup', function() {
$('#pureFontModal').remove();
newUploadedDataUrl = null;
currentSelectedFontName = '';
var hLoad = loadH();
originalFontBeforeOpen = (hLoad.length && hLoad[0].name) ? hLoad[0].name : 'default';
var topName = (hLoad.length && hLoad[0].name !== 'default') ? hLoad[0].name : '';
var $modal = $('<div>', { id: 'pureFontModal' }).css({
'position': 'fixed', 'top': '20%', 'left': '50%', 'transform': 'translate(-50%, -20%)',
'background': '#fff', 'padding': '20px', 'border': '1px solid #72777d', 'border-radius': '4px',
'box-shadow': '0 2px 10px rgba(0,0,0,0.3)', 'z-index': '999999', 'width': '320px', 'color': '#000'
}).html(
'<h3 style="margin:0 0 15px 0; font-size:16px; text-align:left;">Font Uploader & History</h3>' +
'<label style="display:block; font-size:12px; margin-bottom:5px; text-align:left;">1. Select Font File (.ttf / .woff2 / .otf):</label>' +
'<input type="file" id="pureFileInput" accept=".ttf,.woff,.woff2,.otf" style="display:block; margin-bottom:15px; width:100%; font-size:12px;">' +
'<label style="display:block; font-size:12px; margin-bottom:5px; text-align:left;">2. Enter Font Name:</label>' +
'<input type="text" id="pureNameInput" style="display:block; margin-bottom:20px; width:100%; padding:4px; box-sizing:border-box;" value="' + topName + '">'
);
var $btnBox = $('<div>', { id: 'pureFontModal-btn-container' }).css({ 'display': 'flex', 'justify-content': 'space-between', 'align-items': 'center', 'margin-top': '15px' });
var $resetBtn = $('<button>', { id: 'pureResetBtn' }).text('default').css({ 'padding': '5px 10px', 'font-size': '12px', 'cursor': 'pointer', 'background': '#d33', 'color': '#fff', 'border': 'none', 'border-radius': '2px', 'font-weight': 'bold' });
$resetBtn.on('click', function(e) {
e.stopPropagation();
var h = loadH();
var idx = h.findIndex(function(i){return i.name==='default'});
if (idx !== -1) {
var dItem = h.splice(idx, 1)[0];
h.unshift(dItem);
}
localStorage.setItem(KEY, JSON.stringify(h));
applyFont('default');
$('#pureNameInput').val('');
newUploadedDataUrl = null;
currentSelectedFontName = 'default';
ui();
});
var $rightBtns = $('<div>').css({ 'display': 'flex', 'gap': '10px' });
var $cancel = $('<button>', { id: 'pureCloseBtn' }).text('Cancel').css({ 'padding': '5px 10px', 'font-size': '12px', 'cursor': 'pointer' });
var $save = $('<button>', { id: 'pureSaveBtn' }).text('Save').css({ 'padding': '5px 15px', 'font-size': '12px', 'cursor': 'pointer', 'background': '#36c', 'color': '#fff', 'border': 'none', 'border-radius': '2px' });
$rightBtns.append($cancel).append($save);
$btnBox.append($resetBtn).append($rightBtns);
$modal.append($btnBox).appendTo('body');
ui();
});
$(document).on('change', '#pureFileInput', function() {
if (this.files && this.files[0]) {
var file = this.files[0];
var r = new FileReader();
r.onload = function(e) {
newUploadedDataUrl = e.target.result;
currentSelectedFontName = '';
var fontName = $('#pureNameInput').val().trim();
if (!fontName) {
fontName = file.name.substring(0, file.name.lastIndexOf('.')) || 'TempFont';
$('#pureNameInput').val(fontName);
}
applyFont(fontName, newUploadedDataUrl);
};
r.readAsDataURL(file);
}
});
$(document).on('input', '#pureNameInput', function() {
var fontName = $(this).val().trim();
if (fontName && newUploadedDataUrl) {
applyFont(fontName, newUploadedDataUrl);
}
});
$(document).on('click', '#pureSaveBtn', function() {
var name = $('#pureNameInput').val().trim(); if (!name) return alert('フォント名を入力してください。');
try {
if (newUploadedDataUrl) {
openDB().done(function(db) {
var tx = db.transaction([STORE], 'readwrite');
tx.objectStore(STORE).put({ id: name, dataUrl: newUploadedDataUrl });
tx.oncomplete = function() {
updateH(name); alert('保存しました!'); $('#pureFontModal').remove(); newUploadedDataUrl = null;
};
tx.onerror = function() {
alert('⚠️ 【容量オーバー警告】IndexedDBへの書き込みに失敗しました。');
};
});
} else if (currentSelectedFontName) {
if (currentSelectedFontName === 'default') {
applyFont('default');
}
updateH(currentSelectedFontName); $('#pureFontModal').remove();
} else {
updateH(name); applyFont(name); $('#pureFontModal').remove();
}
} catch (error) {
alert('❌ エラーが発生したため保存を中断しました:\n' + error.message);
$('#pureFontModal').remove();
}
}).on('click', '#pureCloseBtn', function() {
$('#pureFontModal').remove();
newUploadedDataUrl = null;
currentSelectedFontName = '';
var targetFont = originalFontBeforeOpen;
if (targetFont && targetFont !== 'default') {
getFont(targetFont, function(r) { applyFont(targetFont, r ? r.dataUrl : null); });
} else {
applyFont('default');
}
});
});
})();
1ut41m2kebiz6e8rxnhr1n0qj4xogyh
769441
769439
2026-10-21T12:51:54Z
OUT is this
73801
769441
javascript
text/javascript
(function() {
'use strict';
if (!mw.config.get('wgUserId')) return;
var KEY = 'wikifonts_history_addon', T_KEY = 'wikifonts_history_toggle_state';
var DB = 'PureFontUploaderDB', STORE = 'fontStore';
var isOpen = localStorage.getItem(T_KEY) === 'open';
var newUploadedDataUrl = null;
var currentSelectedFontName = '';
var originalFontBeforeOpen = null;
function openDB() {
return $.Deferred(function(d) {
var r = indexedDB.open(DB, 1);
r.onupgradeneeded = function(e) { if (!e.target.result.objectStoreNames.contains(STORE)) e.target.result.createObjectStore(STORE, { keyPath: 'id' }); };
r.onsuccess = function(e) { d.resolve(e.target.result); };
}).promise();
}
function getFont(name, cb) {
openDB().done(function(db) {
db.transaction([STORE], 'readonly').objectStore(STORE).get(name).onsuccess = function(e) { cb(e.target.result); };
});
}
function loadH() {
try {
var h = JSON.parse(localStorage.getItem(KEY) || '[]');
if (!h.some(function(i){return i.name==='default'})) { h.push({name:'default',starred:false,time:0}); localStorage.setItem(KEY, JSON.stringify(h)); }
return h;
} catch(e) { return [{name:'default',starred:false,time:0}]; }
}
function applyFont(name, url) {
$('#testFontFace').remove();
if (name === 'default') {
document.body.style.removeProperty('font-family');
return;
}
if (url) $('<style>', { id: 'testFontFace' }).text('@font-face { font-family: "' + name + '"; src: url("' + url + '"); }').appendTo('head');
document.body.style.setProperty('font-family', name, 'important');
}
// 【同期】リロード時の自動復元
$(document).ready(function() {
var h = loadH();
if (h.length && h[0].name !== 'default') {
getFont(h[0].name, function(r) { if (r) applyFont(r.id, r.dataUrl); else applyFont(h[0].name); });
}
});
$(document).ready(function() {
$('#pt-font-setup').remove();
mw.util.addPortletLink('p-personal', 'javascript:void(0);', '⚙️ Font Setup', 'pt-font-setup', 'Open uploader', null, '#pt-logout');
function updateH(font) {
if (!font) return;
var h = loadH(), idx = h.findIndex(function(i){return i.name===font}), star = idx !== -1 ? h[idx].starred : false;
if (idx !== -1) h.splice(idx, 1);
h.unshift({name: font, starred: star, time: Date.now()});
var s = h.filter(function(i){return i.starred && i.name!=='default'}),
u = h.filter(function(i){return !i.starred && i.name!=='default'});
h = s.sort(function(a,b){return b.time-a.time}).concat(u.sort(function(a,b){return b.time-a.time}));
h.push({name:'default',starred:false,time:0});
try {
localStorage.setItem(KEY, JSON.stringify(h));
} catch (e) {
alert('⚠️ 【容量警告】LocalStorageの容量が不足しているため履歴の保存に失敗しました。');
}
ui();
}
function ui() {
var $box = $('#pureFontModal'); if (!$box.length) return;
$('#wikifonts-history-section').remove();
var $sec = $('<div>',{id: 'wikifonts-history-section'}).css({'margin-top':'15px','padding-top':'10px','border-top':'1px solid #72777d','text-align':'left'});
var $hdr = $('<div>').css({'display':'flex','justify-content':'space-between','align-items':'center','padding':'4px 0','color':'#000','font-size':'13px'});
var $titleLeft = $('<span>').text('History').css({'color':'#000','font-weight':'bold'});
var $toggleLink = $('<span>', { id: 'wf-history-arrow' }).text(isOpen ? '[非表示]' : '[表示]').css({'color':'#36c','cursor':'pointer','font-size':'12px'});
$toggleLink.on('click', function() {
isOpen = $list.is(':hidden');
$list.toggle(isOpen);
$toggleLink.text(isOpen ? '[非表示]' : '[表示]');
localStorage.setItem(T_KEY, isOpen ? 'open' : 'close');
});
$hdr.append($titleLeft).append($toggleLink);
var $list = $('<div>',{id: 'wikifonts-history-list'}).css({'display':isOpen?'block':'none','max-height':'120px','overflow-y':'auto','margin-top':'5px'});
loadH().forEach(function(item) {
if (item.name === 'default') return;
var $row = $('<div>').css({'display':'flex','align-items':'center','justify-content':'space-between','padding':'4px 0','border-bottom':'1px solid rgba(0,0,0,0.1)'});
var $left = $('<div>').css({'display':'flex','align-items':'center','gap':'8px'});
$('<span>').text(item.starred?'⭐':'☆').css({'cursor':'pointer','font-size':'12px','color':item.starred?'#ffc107':'#000'})
.on('click', function(e) { e.stopPropagation();
var h = loadH(), i = h.find(function(f){return f.name===item.name});
if (i) { i.starred = !i.starred; localStorage.setItem(KEY, JSON.stringify(h)); ui(); }
}).appendTo($left);
var $nameLabel = $('<span>').text(item.name).css({
'font-family': '"' + item.name + '", sans-serif',
'cursor': 'pointer',
'font-size': '13px',
'color': '#000'
}).on('mouseenter', function() { $(this).css({'color':'#0074D9','text-decoration':'underline'}); })
.on('mouseleave', function() { $(this).css({'color':'#000','text-decoration':'none'}); })
.on('click', function() {
$('#pureNameInput').val(item.name);
currentSelectedFontName = item.name;
newUploadedDataUrl = null;
getFont(item.name, function(r){ applyFont(item.name, r?r.dataUrl:null); });
});
// 各行ごとに確実に@font-faceを生成・適用
getFont(item.name, function(r) {
if (r && r.dataUrl) {
var listStyleId = 'testFontFace-' + item.name.replace(/[^a-zA-Z0-9_-]/g, '');
if (!$('#' + listStyleId).length) {
$('<style>', { id: listStyleId }).text('@font-face { font-family: "' + item.name + '"; src: url("' + r.dataUrl + '"); }').appendTo('head');
}
}
});
$nameLabel.appendTo($left);
var $rightBtns = $('<div>').css({'display':'flex','align-items':'center','gap':'4px'});
// ✏️ 編集ボタン(名前変更用)
var $editBtn = $('<span>').text('✏️').css({'cursor':'pointer','font-size':'10px','padding':'2px','opacity':'0.6'})
.on('mouseenter', function() { $(this).css('opacity', '1'); })
.on('mouseleave', function() { $(this).css('opacity', '0.6'); })
.on('click', function(e) {
e.stopPropagation();
var newName = prompt('新しいフォント名を入力してください:', item.name);
if (!newName || !(newName = newName.trim())) return;
if (newName === item.name) return;
getFont(item.name, function(r) {
if (!r || !r.dataUrl) {
alert('フォントデータの取得に失敗しました。');
return;
}
openDB().done(function(db) {
var tx = db.transaction([STORE], 'readwrite');
var store = tx.objectStore(STORE);
store.delete(item.name); // 旧データの削除
store.put({ id: newName, dataUrl: r.dataUrl }); // 新名で登録
tx.oncomplete = function() {
// 古いスタイルのDOMを確実に削除
$('#testFontFace-' + item.name.replace(/[^a-zA-Z0-9_-]/g, '')).remove();
// 新しい名前用のスタイルを即座に登録
var newStyleId = 'testFontFace-' + newName.replace(/[^a-zA-Z0-9_-]/g, '');
if (!$('#' + newStyleId).length) {
$('<style>', { id: newStyleId }).text('@font-face { font-family: "' + newName + '"; src: url("' + r.dataUrl + '"); }').appendTo('head');
}
var h = loadH();
var target = h.find(function(f){return f.name===item.name});
var star = target ? target.starred : false;
h = h.filter(function(f){return f.name!==item.name});
h.unshift({name: newName, starred: star, time: Date.now()});
var s = h.filter(function(i){return i.starred && i.name!=='default'}),
u = h.filter(function(i){return !i.starred && i.name!=='default'});
h = s.sort(function(a,b){return b.time-a.time}).concat(u.sort(function(a,b){return b.time-a.time}));
h.push({name:'default',starred:false,time:0});
localStorage.setItem(KEY, JSON.stringify(h));
applyFont(newName, r.dataUrl);
ui();
};
});
});
});
// ❌ 削除ボタン
var $deleteBtn = $('<span>').text('❌').css({'cursor':'pointer','font-size':'10px','padding':'2px','opacity':'0.5'})
.on('mouseenter', function() { $(this).css('opacity', '1'); })
.on('mouseleave', function() { $(this).css('opacity', '0.5'); })
.on('click', function(e) { e.stopPropagation();
if (confirm('フォント「' + item.name + '」を削除しますか?')) {
var h = loadH().filter(function(f){return f.name!==item.name});
localStorage.setItem(KEY, JSON.stringify(h));
$('#testFontFace-' + item.name.replace(/[^a-zA-Z0-9_-]/g, '')).remove();
openDB().done(function(db){
db.transaction([STORE], 'readwrite').objectStore(STORE).delete(item.name);
}); ui();
}
});
$rightBtns.append($editBtn).append($deleteBtn);
$row.append($left).append($rightBtns).appendTo($list);
});
if (!$list.children().length) $('<div>').text('No history').css({'color':'#54595d','font-style':'italic','padding':'4px 0','font-size':'12px'}).appendTo($list);
$('#pureFontModal-btn-container').before($sec.append($hdr).append($list));
}
$(document).on('click', '#pt-font-setup', function() {
$('#pureFontModal').remove();
newUploadedDataUrl = null;
currentSelectedFontName = '';
var hLoad = loadH();
originalFontBeforeOpen = (hLoad.length && hLoad[0].name) ? hLoad[0].name : 'default';
var topName = (hLoad.length && hLoad[0].name !== 'default') ? hLoad[0].name : '';
var $modal = $('<div>', { id: 'pureFontModal' }).css({
'position': 'fixed', 'top': '20%', 'left': '50%', 'transform': 'translate(-50%, -20%)',
'background': '#fff', 'padding': '20px', 'border': '1px solid #72777d', 'border-radius': '4px',
'box-shadow': '0 2px 10px rgba(0,0,0,0.3)', 'z-index': '999999', 'width': '320px', 'color': '#000'
}).html(
'<h3 style="margin:0 0 15px 0; font-size:16px; text-align:left;">Font Uploader & History</h3>' +
'<label style="display:block; font-size:12px; margin-bottom:5px; text-align:left;">1. Select Font File (.ttf / .woff2 / .otf):</label>' +
'<input type="file" id="pureFileInput" accept=".ttf,.woff,.woff2,.otf" style="display:block; margin-bottom:15px; width:100%; font-size:12px;">' +
'<label style="display:block; font-size:12px; margin-bottom:5px; text-align:left;">2. Enter Font Name:</label>' +
'<input type="text" id="pureNameInput" style="display:block; margin-bottom:20px; width:100%; padding:4px; box-sizing:border-box;" value="' + topName + '">'
);
var $btnBox = $('<div>', { id: 'pureFontModal-btn-container' }).css({ 'display': 'flex', 'justify-content': 'space-between', 'align-items': 'center', 'margin-top': '15px' });
var $resetBtn = $('<button>', { id: 'pureResetBtn' }).text('default').css({ 'padding': '5px 10px', 'font-size': '12px', 'cursor': 'pointer', 'background': '#d33', 'color': '#fff', 'border': 'none', 'border-radius': '2px', 'font-weight': 'bold' });
$resetBtn.on('click', function(e) {
e.stopPropagation();
var h = loadH();
var idx = h.findIndex(function(i){return i.name==='default'});
if (idx !== -1) {
var dItem = h.splice(idx, 1)[0];
h.unshift(dItem);
}
localStorage.setItem(KEY, JSON.stringify(h));
applyFont('default');
$('#pureNameInput').val('');
newUploadedDataUrl = null;
currentSelectedFontName = 'default';
ui();
});
var $rightBtns = $('<div>').css({ 'display': 'flex', 'gap': '10px' });
var $cancel = $('<button>', { id: 'pureCloseBtn' }).text('Cancel').css({ 'padding': '5px 10px', 'font-size': '12px', 'cursor': 'pointer' });
var $save = $('<button>', { id: 'pureSaveBtn' }).text('Save').css({ 'padding': '5px 15px', 'font-size': '12px', 'cursor': 'pointer', 'background': '#36c', 'color': '#fff', 'border': 'none', 'border-radius': '2px' });
$rightBtns.append($cancel).append($save);
$btnBox.append($resetBtn).append($rightBtns);
$modal.append($btnBox).appendTo('body');
ui();
});
$(document).on('change', '#pureFileInput', function() {
if (this.files && this.files[0]) {
var file = this.files[0];
var r = new FileReader();
r.onload = function(e) {
newUploadedDataUrl = e.target.result;
currentSelectedFontName = '';
var fontName = $('#pureNameInput').val().trim();
if (!fontName) {
fontName = file.name.substring(0, file.name.lastIndexOf('.')) || 'TempFont';
$('#pureNameInput').val(fontName);
}
applyFont(fontName, newUploadedDataUrl);
};
r.readAsDataURL(file);
}
});
$(document).on('input', '#pureNameInput', function() {
var fontName = $(this).val().trim();
if (fontName && newUploadedDataUrl) {
applyFont(fontName, newUploadedDataUrl);
}
});
$(document).on('click', '#pureSaveBtn', function() {
var name = $('#pureNameInput').val().trim(); if (!name) return alert('フォント名を入力してください。');
try {
if (newUploadedDataUrl) {
openDB().done(function(db) {
var tx = db.transaction([STORE], 'readwrite');
tx.objectStore(STORE).put({ id: name, dataUrl: newUploadedDataUrl });
tx.oncomplete = function() {
updateH(name); alert('保存しました!'); $('#pureFontModal').remove(); newUploadedDataUrl = null;
};
tx.onerror = function() {
alert('⚠️ 【容量オーバー警告】IndexedDBへの書き込みに失敗しました。');
};
});
} else if (currentSelectedFontName) {
if (currentSelectedFontName === 'default') {
applyFont('default');
}
updateH(currentSelectedFontName); $('#pureFontModal').remove();
} else {
updateH(name); applyFont(name); $('#pureFontModal').remove();
}
} catch (error) {
alert('❌ エラーが発生したため保存を中断しました:\n' + error.message);
$('#pureFontModal').remove();
}
}).on('click', '#pureCloseBtn', function() {
$('#pureFontModal').remove();
newUploadedDataUrl = null;
currentSelectedFontName = '';
var targetFont = originalFontBeforeOpen;
if (targetFont && targetFont !== 'default') {
getFont(targetFont, function(r) { applyFont(targetFont, r ? r.dataUrl : null); });
} else {
applyFont('default');
}
});
});
})();
c3rhxjppv0ji1feyduyhdvg20gv34ev
769442
769441
2026-10-21T12:54:27Z
OUT is this
73801
769442
javascript
text/javascript
(function() {
'use strict';
if (!mw.config.get('wgUserId')) return;
var KEY = 'wikifonts_history_addon', T_KEY = 'wikifonts_history_toggle_state';
var DB = 'PureFontUploaderDB', STORE = 'fontStore';
var isOpen = localStorage.getItem(T_KEY) === 'open';
var newUploadedDataUrl = null;
var currentSelectedFontName = '';
var originalFontBeforeOpen = null;
function openDB() {
return $.Deferred(function(d) {
var r = indexedDB.open(DB, 1);
r.onupgradeneeded = function(e) { if (!e.target.result.objectStoreNames.contains(STORE)) e.target.result.createObjectStore(STORE, { keyPath: 'id' }); };
r.onsuccess = function(e) { d.resolve(e.target.result); };
}).promise();
}
function getFont(name, cb) {
openDB().done(function(db) {
db.transaction([STORE], 'readonly').objectStore(STORE).get(name).onsuccess = function(e) { cb(e.target.result); };
});
}
function loadH() {
try {
var h = JSON.parse(localStorage.getItem(KEY) || '[]');
if (!h.some(function(i){return i.name==='default'})) { h.push({name:'default',starred:false,time:0}); localStorage.setItem(KEY, JSON.stringify(h)); }
return h;
} catch(e) { return [{name:'default',starred:false,time:0}]; }
}
function applyFont(name, url) {
$('#testFontFace').remove();
if (name === 'default') {
document.body.style.removeProperty('font-family');
return;
}
if (url) $('<style>', { id: 'testFontFace' }).text('@font-face { font-family: "' + name.replace(/"/g, '\\"') + '"; src: url("' + url + '"); }').appendTo('head');
document.body.style.setProperty('font-family', '"' + name + '", sans-serif', 'important');
}
// 【同期】リロード時の自動復元
$(document).ready(function() {
var h = loadH();
if (h.length && h[0].name !== 'default') {
getFont(h[0].name, function(r) { if (r) applyFont(r.id, r.dataUrl); else applyFont(h[0].name); });
}
});
$(document).ready(function() {
$('#pt-font-setup').remove();
mw.util.addPortletLink('p-personal', 'javascript:void(0);', '⚙️ Font Setup', 'pt-font-setup', 'Open uploader', null, '#pt-logout');
function updateH(font) {
if (!font) return;
var h = loadH(), idx = h.findIndex(function(i){return i.name===font}), star = idx !== -1 ? h[idx].starred : false;
if (idx !== -1) h.splice(idx, 1);
h.unshift({name: font, starred: star, time: Date.now()});
var s = h.filter(function(i){return i.starred && i.name!=='default'}),
u = h.filter(function(i){return !i.starred && i.name!=='default'});
h = s.sort(function(a,b){return b.time-a.time}).concat(u.sort(function(a,b){return b.time-a.time}));
h.push({name:'default',starred:false,time:0});
try {
localStorage.setItem(KEY, JSON.stringify(h));
} catch (e) {
alert('⚠️ 【容量警告】LocalStorageの容量が不足しているため履歴の保存に失敗しました。');
}
ui();
}
function ui() {
var $box = $('#pureFontModal'); if (!$box.length) return;
$('#wikifonts-history-section').remove();
var $sec = $('<div>',{id: 'wikifonts-history-section'}).css({'margin-top':'15px','padding-top':'10px','border-top':'1px solid #72777d','text-align':'left'});
var $hdr = $('<div>').css({'display':'flex','justify-content':'space-between','align-items':'center','padding':'4px 0','color':'#000','font-size':'13px'});
var $titleLeft = $('<span>').text('History').css({'color':'#000','font-weight':'bold'});
var $toggleLink = $('<span>', { id: 'wf-history-arrow' }).text(isOpen ? '[非表示]' : '[表示]').css({'color':'#36c','cursor':'pointer','font-size':'12px'});
$toggleLink.on('click', function() {
isOpen = $list.is(':hidden');
$list.toggle(isOpen);
$toggleLink.text(isOpen ? '[非表示]' : '[表示]');
localStorage.setItem(T_KEY, isOpen ? 'open' : 'close');
});
$hdr.append($titleLeft).append($toggleLink);
var $list = $('<div>',{id: 'wikifonts-history-list'}).css({'display':isOpen?'block':'none','max-height':'120px','overflow-y':'auto','margin-top':'5px'});
loadH().forEach(function(item) {
if (item.name === 'default') return;
var $row = $('<div>').css({'display':'flex','align-items':'center','justify-content':'space-between','padding':'4px 0','border-bottom':'1px solid rgba(0,0,0,0.1)'});
var $left = $('<div>').css({'display':'flex','align-items':'center','gap':'8px'});
$('<span>').text(item.starred?'⭐':'☆').css({'cursor':'pointer','font-size':'12px','color':item.starred?'#ffc107':'#000'})
.on('click', function(e) { e.stopPropagation();
var h = loadH(), i = h.find(function(f){return f.name===item.name});
if (i) { i.starred = !i.starred; localStorage.setItem(KEY, JSON.stringify(h)); ui(); }
}).appendTo($left);
var escapedName = item.name.replace(/"/g, '\\"');
var $nameLabel = $('<span>').text(item.name).css({
'font-family': '"' + escapedName + '", sans-serif',
'cursor': 'pointer',
'font-size': '13px',
'color': '#000'
}).on('mouseenter', function() { $(this).css({'color':'#0074D9','text-decoration':'underline'}); })
.on('mouseleave', function() { $(this).css({'color':'#000','text-decoration':'none'}); })
.on('click', function() {
$('#pureNameInput').val(item.name);
currentSelectedFontName = item.name;
newUploadedDataUrl = null;
getFont(item.name, function(r){ applyFont(item.name, r?r.dataUrl:null); });
});
// 各行ごとに確実に@font-faceを生成・適用(安全なCSS IDを使用)
getFont(item.name, function(r) {
if (r && r.dataUrl) {
var safeId = btoa(unescape(encodeURIComponent(item.name))).replace(/[^a-zA-Z0-9_-]/g, '_');
var listStyleId = 'testFontFace-' + safeId;
if (!$('#' + listStyleId).length) {
$('<style>', { id: listStyleId }).text('@font-face { font-family: "' + escapedName + '"; src: url("' + r.dataUrl + '"); }').appendTo('head');
}
}
});
$nameLabel.appendTo($left);
var $rightBtns = $('<div>').css({'display':'flex','align-items':'center','gap':'4px'});
// ✏️ 編集ボタン(名前変更用)
var $editBtn = $('<span>').text('✏️').css({'cursor':'pointer','font-size':'10px','padding':'2px','opacity':'0.6'})
.on('mouseenter', function() { $(this).css('opacity', '1'); })
.on('mouseleave', function() { $(this).css('opacity', '0.6'); })
.on('click', function(e) {
e.stopPropagation();
var newName = prompt('新しいフォント名を入力してください:', item.name);
if (!newName || !(newName = newName.trim())) return;
if (newName === item.name) return;
getFont(item.name, function(r) {
if (!r || !r.dataUrl) {
alert('フォントデータの取得に失敗しました。');
return;
}
openDB().done(function(db) {
var tx = db.transaction([STORE], 'readwrite');
var store = tx.objectStore(STORE);
store.delete(item.name); // 旧データの削除
store.put({ id: newName, dataUrl: r.dataUrl }); // 新名で登録
tx.oncomplete = function() {
// 古いスタイルのDOMを確実に削除
var oldSafeId = btoa(unescape(encodeURIComponent(item.name))).replace(/[^a-zA-Z0-9_-]/g, '_');
$('#testFontFace-' + oldSafeId).remove();
// 新しい名前用のスタイルを即座に登録
var newEscapedName = newName.replace(/"/g, '\\"');
var newSafeId = btoa(unescape(encodeURIComponent(newName))).replace(/[^a-zA-Z0-9_-]/g, '_');
var newStyleId = 'testFontFace-' + newSafeId;
if (!$('#' + newStyleId).length) {
$('<style>', { id: newStyleId }).text('@font-face { font-family: "' + newEscapedName + '"; src: url("' + r.dataUrl + '"); }').appendTo('head');
}
var h = loadH();
var target = h.find(function(f){return f.name===item.name});
var star = target ? target.starred : false;
h = h.filter(function(f){return f.name!==item.name});
h.unshift({name: newName, starred: star, time: Date.now()});
var s = h.filter(function(i){return i.starred && i.name!=='default'}),
u = h.filter(function(i){return !i.starred && i.name!=='default'});
h = s.sort(function(a,b){return b.time-a.time}).concat(u.sort(function(a,b){return b.time-a.time}));
h.push({name:'default',starred:false,time:0});
localStorage.setItem(KEY, JSON.stringify(h));
applyFont(newName, r.dataUrl);
ui();
};
});
});
});
// ❌ 削除ボタン
var $deleteBtn = $('<span>').text('❌').css({'cursor':'pointer','font-size':'10px','padding':'2px','opacity':'0.5'})
.on('mouseenter', function() { $(this).css('opacity', '1'); })
.on('mouseleave', function() { $(this).css('opacity', '0.5'); })
.on('click', function(e) { e.stopPropagation();
if (confirm('フォント「' + item.name + '」を削除しますか?')) {
var h = loadH().filter(function(f){return f.name!==item.name});
localStorage.setItem(KEY, JSON.stringify(h));
var safeId = btoa(unescape(encodeURIComponent(item.name))).replace(/[^a-zA-Z0-9_-]/g, '_');
$('#testFontFace-' + safeId).remove();
openDB().done(function(db){
db.transaction([STORE], 'readwrite').objectStore(STORE).delete(item.name);
}); ui();
}
});
$rightBtns.append($editBtn).append($deleteBtn);
$row.append($left).append($rightBtns).appendTo($list);
});
if (!$list.children().length) $('<div>').text('No history').css({'color':'#54595d','font-style':'italic','padding':'4px 0','font-size':'12px'}).appendTo($list);
$('#pureFontModal-btn-container').before($sec.append($hdr).append($list));
}
$(document).on('click', '#pt-font-setup', function() {
$('#pureFontModal').remove();
newUploadedDataUrl = null;
currentSelectedFontName = '';
var hLoad = loadH();
originalFontBeforeOpen = (hLoad.length && hLoad[0].name) ? hLoad[0].name : 'default';
var topName = (hLoad.length && hLoad[0].name !== 'default') ? hLoad[0].name : '';
var $modal = $('<div>', { id: 'pureFontModal' }).css({
'position': 'fixed', 'top': '20%', 'left': '50%', 'transform': 'translate(-50%, -20%)',
'background': '#fff', 'padding': '20px', 'border': '1px solid #72777d', 'border-radius': '4px',
'box-shadow': '0 2px 10px rgba(0,0,0,0.3)', 'z-index': '999999', 'width': '320px', 'color': '#000'
}).html(
'<h3 style="margin:0 0 15px 0; font-size:16px; text-align:left;">Font Uploader & History</h3>' +
'<label style="display:block; font-size:12px; margin-bottom:5px; text-align:left;">1. Select Font File (.ttf / .woff2 / .otf):</label>' +
'<input type="file" id="pureFileInput" accept=".ttf,.woff,.woff2,.otf" style="display:block; margin-bottom:15px; width:100%; font-size:12px;">' +
'<label style="display:block; font-size:12px; margin-bottom:5px; text-align:left;">2. Enter Font Name:</label>' +
'<input type="text" id="pureNameInput" style="display:block; margin-bottom:20px; width:100%; padding:4px; box-sizing:border-box;" value="' + topName + '">'
);
var $btnBox = $('<div>', { id: 'pureFontModal-btn-container' }).css({ 'display': 'flex', 'justify-content': 'space-between', 'align-items': 'center', 'margin-top': '15px' });
var $resetBtn = $('<button>', { id: 'pureResetBtn' }).text('default').css({ 'padding': '5px 10px', 'font-size': '12px', 'cursor': 'pointer', 'background': '#d33', 'color': '#fff', 'border': 'none', 'border-radius': '2px', 'font-weight': 'bold' });
$resetBtn.on('click', function(e) {
e.stopPropagation();
var h = loadH();
var idx = h.findIndex(function(i){return i.name==='default'});
if (idx !== -1) {
var dItem = h.splice(idx, 1)[0];
h.unshift(dItem);
}
localStorage.setItem(KEY, JSON.stringify(h));
applyFont('default');
$('#pureNameInput').val('');
newUploadedDataUrl = null;
currentSelectedFontName = 'default';
ui();
});
var $rightBtns = $('<div>').css({ 'display': 'flex', 'gap': '10px' });
var $cancel = $('<button>', { id: 'pureCloseBtn' }).text('Cancel').css({ 'padding': '5px 10px', 'font-size': '12px', 'cursor': 'pointer' });
var $save = $('<button>', { id: 'pureSaveBtn' }).text('Save').css({ 'padding': '5px 15px', 'font-size': '12px', 'cursor': 'pointer', 'background': '#36c', 'color': '#fff', 'border': 'none', 'border-radius': '2px' });
$rightBtns.append($cancel).append($save);
$btnBox.append($resetBtn).append($rightBtns);
$modal.append($btnBox).appendTo('body');
ui();
});
$(document).on('change', '#pureFileInput', function() {
if (this.files && this.files[0]) {
var file = this.files[0];
var r = new FileReader();
r.onload = function(e) {
newUploadedDataUrl = e.target.result;
currentSelectedFontName = '';
var fontName = $('#pureNameInput').val().trim();
if (!fontName) {
fontName = file.name.substring(0, file.name.lastIndexOf('.')) || 'TempFont';
$('#pureNameInput').val(fontName);
}
applyFont(fontName, newUploadedDataUrl);
};
r.readAsDataURL(file);
}
});
$(document).on('input', '#pureNameInput', function() {
var fontName = $(this).val().trim();
if (fontName && newUploadedDataUrl) {
applyFont(fontName, newUploadedDataUrl);
}
});
$(document).on('click', '#pureSaveBtn', function() {
var name = $('#pureNameInput').val().trim(); if (!name) return alert('フォント名を入力してください。');
try {
if (newUploadedDataUrl) {
openDB().done(function(db) {
var tx = db.transaction([STORE], 'readwrite');
tx.objectStore(STORE).put({ id: name, dataUrl: newUploadedDataUrl });
tx.oncomplete = function() {
updateH(name); alert('保存しました!'); $('#pureFontModal').remove(); newUploadedDataUrl = null;
};
tx.onerror = function() {
alert('⚠️ 【容量オーバー警告】IndexedDBへの書き込みに失敗しました。');
};
});
} else if (currentSelectedFontName) {
if (currentSelectedFontName === 'default') {
applyFont('default');
}
updateH(currentSelectedFontName); $('#pureFontModal').remove();
} else {
updateH(name); applyFont(name); $('#pureFontModal').remove();
}
} catch (error) {
alert('❌ エラーが発生したため保存を中断しました:\n' + error.message);
$('#pureFontModal').remove();
}
}).on('click', '#pureCloseBtn', function() {
$('#pureFontModal').remove();
newUploadedDataUrl = null;
currentSelectedFontName = '';
var targetFont = originalFontBeforeOpen;
if (targetFont && targetFont !== 'default') {
getFont(targetFont, function(r) { applyFont(targetFont, r ? r.dataUrl : null); });
} else {
applyFont('default');
}
});
});
})();
3q1ri6vjk4uew3oqk2rj4cd558ssy13
769445
769442
2026-10-21T13:18:52Z
OUT is this
73801
769445
javascript
text/javascript
(function() {
'use strict';
if (!mw.config.get('wgUserId')) return;
var KEY = 'wikifonts_history_addon', T_KEY = 'wikifonts_history_toggle_state';
var DB = 'PureFontUploaderDB', STORE = 'fontStore';
var isOpen = localStorage.getItem(T_KEY) === 'open';
var newUploadedDataUrl = null;
var currentSelectedFontName = '';
var originalFontBeforeOpen = null;
function openDB() {
return $.Deferred(function(d) {
var r = indexedDB.open(DB, 1);
r.onupgradeneeded = function(e) { if (!e.target.result.objectStoreNames.contains(STORE)) e.target.result.createObjectStore(STORE, { keyPath: 'id' }); };
r.onsuccess = function(e) { d.resolve(e.target.result); };
}).promise();
}
function getFont(name, cb) {
openDB().done(function(db) {
db.transaction([STORE], 'readonly').objectStore(STORE).get(name).onsuccess = function(e) { cb(e.target.result); };
});
}
function loadH() {
try {
var h = JSON.parse(localStorage.getItem(KEY) || '[]');
if (!h.some(function(i){return i.name==='default'})) { h.push({name:'default',starred:false,time:0}); localStorage.setItem(KEY, JSON.stringify(h)); }
return h;
} catch(e) { return [{name:'default',starred:false,time:0}]; }
}
function applyFont(name, url) {
$('#testFontFace').remove();
if (name === 'default') {
document.body.style.removeProperty('font-family');
return;
}
if (url) $('<style>', { id: 'testFontFace' }).text('@font-face { font-family: "' + name.replace(/"/g, '\\"') + '"; src: url("' + url + '"); }').appendTo('head');
document.body.style.setProperty('font-family', '"' + name + '", sans-serif', 'important');
}
// 【同期】リロード時の自動復元
$(document).ready(function() {
var h = loadH();
if (h.length && h[0].name !== 'default') {
getFont(h[0].name, function(r) { if (r) applyFont(r.id, r.dataUrl); else applyFont(h[0].name); });
}
});
$(document).ready(function() {
$('#pt-font-setup').remove();
// sm link
var svgIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" style="vertical-align: middle; fill: currentColor; margin-right: 4px;">' +
'<path d="M19.43 12.98c.04-.32.07-.64.07-.98 0-.34-.03-.66-.07-.98l2.11-1.65c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.3-.61-.22l-2.49 1c-.52-.4-1.08-.73-1.69-.98l-.38-2.65C14.46 2.18 14.25 2 14 2h-4c-.25 0-.46.18-.49.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1c-.23-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64l2.11 1.65c-.04.32-.07.65-.07.98 0 .33.03.66.07.98l-2.11 1.65c-.19.15-.24.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65c.03.24.24.42.49.42h4c.25 0 .46-.18.49-.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1c.23.09.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.65zM12 15.5c-1.93 0-3.5-1.57-3.5-3.5s1.57-3.5 3.5-3.5 3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5z"/>' +
'<text x="12" y="14.5" font-family="sans-serif" font-size="8.5" font-weight="bold" text-anchor="middle" fill="#fff" stroke="#fff" stroke-width="2" stroke-linejoin="round">Aa</text>' +
'<text x="12" y="14.5" font-family="sans-serif" font-size="8.5" font-weight="bold" text-anchor="middle" fill="#000">Aa</text>' +
'</svg>';
mw.util.addPortletLink('p-personal', 'javascript:void(0);', '', 'pt-font-setup', 'Open uploader', null, '#pt-logout');
$('#pt-font-setup a').html(svgIcon + 'Font Setup');
function updateH(font) {
if (!font) return;
var h = loadH(), idx = h.findIndex(function(i){return i.name===font}), star = idx !== -1 ? h[idx].starred : false;
if (idx !== -1) h.splice(idx, 1);
h.unshift({name: font, starred: star, time: Date.now()});
var s = h.filter(function(i){return i.starred && i.name!=='default'}),
u = h.filter(function(i){return !i.starred && i.name!=='default'});
h = s.sort(function(a,b){return b.time-a.time}).concat(u.sort(function(a,b){return b.time-a.time}));
h.push({name:'default',starred:false,time:0});
try {
localStorage.setItem(KEY, JSON.stringify(h));
} catch (e) {
alert('⚠️ 【容量警告】LocalStorageの容量が不足しているため履歴の保存に失敗しました。');
}
ui();
}
function ui() {
var $box = $('#pureFontModal'); if (!$box.length) return;
$('#wikifonts-history-section').remove();
var $sec = $('<div>',{id: 'wikifonts-history-section'}).css({'margin-top':'15px','padding-top':'10px','border-top':'1px solid #72777d','text-align':'left'});
var $hdr = $('<div>').css({'display':'flex','justify-content':'space-between','align-items':'center','padding':'4px 0','color':'#000','font-size':'13px'});
var $titleLeft = $('<span>').text('History').css({'color':'#000','font-weight':'bold'});
var $toggleLink = $('<span>', { id: 'wf-history-arrow' }).text(isOpen ? '[非表示]' : '[表示]').css({'color':'#36c','cursor':'pointer','font-size':'12px'});
$toggleLink.on('click', function() {
isOpen = $list.is(':hidden');
$list.toggle(isOpen);
$toggleLink.text(isOpen ? '[非表示]' : '[表示]');
localStorage.setItem(T_KEY, isOpen ? 'open' : 'close');
});
$hdr.append($titleLeft).append($toggleLink);
var $list = $('<div>',{id: 'wikifonts-history-list'}).css({'display':isOpen?'block':'none','max-height':'120px','overflow-y':'auto','margin-top':'5px'});
loadH().forEach(function(item) {
if (item.name === 'default') return;
var $row = $('<div>').css({'display':'flex','align-items':'center','justify-content':'space-between','padding':'4px 0','border-bottom':'1px solid rgba(0,0,0,0.1)'});
var $left = $('<div>').css({'display':'flex','align-items':'center','gap':'8px'});
$('<span>').text(item.starred?'⭐':'☆').css({'cursor':'pointer','font-size':'12px','color':item.starred?'#ffc107':'#000'})
.on('click', function(e) { e.stopPropagation();
var h = loadH(), i = h.find(function(f){return f.name===item.name});
if (i) { i.starred = !i.starred; localStorage.setItem(KEY, JSON.stringify(h)); ui(); }
}).appendTo($left);
var escapedName = item.name.replace(/"/g, '\\"');
var $nameLabel = $('<span>').text(item.name).css({
'font-family': '"' + escapedName + '", sans-serif',
'cursor': 'pointer',
'font-size': '13px',
'color': '#000'
}).on('mouseenter', function() { $(this).css({'color':'#0074D9','text-decoration':'underline'}); })
.on('mouseleave', function() { $(this).css({'color':'#000','text-decoration':'none'}); })
.on('click', function() {
$('#pureNameInput').val(item.name);
currentSelectedFontName = item.name;
newUploadedDataUrl = null;
getFont(item.name, function(r){ applyFont(item.name, r?r.dataUrl:null); });
});
// 各行ごとに確実に@font-faceを生成・適用(安全なCSS IDを使用)
getFont(item.name, function(r) {
if (r && r.dataUrl) {
var safeId = btoa(unescape(encodeURIComponent(item.name))).replace(/[^a-zA-Z0-9_-]/g, '_');
var listStyleId = 'testFontFace-' + safeId;
if (!$('#' + listStyleId).length) {
$('<style>', { id: listStyleId }).text('@font-face { font-family: "' + escapedName + '"; src: url("' + r.dataUrl + '"); }').appendTo('head');
}
}
});
$nameLabel.appendTo($left);
var $rightBtns = $('<div>').css({'display':'flex','align-items':'center','gap':'4px'});
// ✏️ 編集ボタン(名前変更用)
var $editBtn = $('<span>').text('✏️').css({'cursor':'pointer','font-size':'10px','padding':'2px','opacity':'0.6'})
.on('mouseenter', function() { $(this).css('opacity', '1'); })
.on('mouseleave', function() { $(this).css('opacity', '0.6'); })
.on('click', function(e) {
e.stopPropagation();
var newName = prompt('新しいフォント名を入力してください:', item.name);
if (!newName || !(newName = newName.trim())) return;
if (newName === item.name) return;
getFont(item.name, function(r) {
if (!r || !r.dataUrl) {
alert('フォントデータの取得に失敗しました。');
return;
}
openDB().done(function(db) {
var tx = db.transaction([STORE], 'readwrite');
var store = tx.objectStore(STORE);
store.delete(item.name); // 旧データの削除
store.put({ id: newName, dataUrl: r.dataUrl }); // 新名で登録
tx.oncomplete = function() {
// 古いスタイルのDOMを確実に削除
var oldSafeId = btoa(unescape(encodeURIComponent(item.name))).replace(/[^a-zA-Z0-9_-]/g, '_');
$('#testFontFace-' + oldSafeId).remove();
// 新しい名前用のスタイルを即座に登録
var newEscapedName = newName.replace(/"/g, '\\"');
var newSafeId = btoa(unescape(encodeURIComponent(newName))).replace(/[^a-zA-Z0-9_-]/g, '_');
var newStyleId = 'testFontFace-' + newSafeId;
if (!$('#' + newStyleId).length) {
$('<style>', { id: newStyleId }).text('@font-face { font-family: "' + newEscapedName + '"; src: url("' + r.dataUrl + '"); }').appendTo('head');
}
var h = loadH();
var target = h.find(function(f){return f.name===item.name});
var star = target ? target.starred : false;
h = h.filter(function(f){return f.name!==item.name});
h.unshift({name: newName, starred: star, time: Date.now()});
var s = h.filter(function(i){return i.starred && i.name!=='default'}),
u = h.filter(function(i){return !i.starred && i.name!=='default'});
h = s.sort(function(a,b){return b.time-a.time}).concat(u.sort(function(a,b){return b.time-a.time}));
h.push({name:'default',starred:false,time:0});
localStorage.setItem(KEY, JSON.stringify(h));
applyFont(newName, r.dataUrl);
ui();
};
});
});
});
// ❌ 削除ボタン
var $deleteBtn = $('<span>').text('❌').css({'cursor':'pointer','font-size':'10px','padding':'2px','opacity':'0.5'})
.on('mouseenter', function() { $(this).css('opacity', '1'); })
.on('mouseleave', function() { $(this).css('opacity', '0.5'); })
.on('click', function(e) { e.stopPropagation();
if (confirm('フォント「' + item.name + '」を削除しますか?')) {
var h = loadH().filter(function(f){return f.name!==item.name});
localStorage.setItem(KEY, JSON.stringify(h));
var safeId = btoa(unescape(encodeURIComponent(item.name))).replace(/[^a-zA-Z0-9_-]/g, '_');
$('#testFontFace-' + safeId).remove();
openDB().done(function(db){
db.transaction([STORE], 'readwrite').objectStore(STORE).delete(item.name);
}); ui();
}
});
$rightBtns.append($editBtn).append($deleteBtn);
$row.append($left).append($rightBtns).appendTo($list);
});
if (!$list.children().length) $('<div>').text('No history').css({'color':'#54595d','font-style':'italic','padding':'4px 0','font-size':'12px'}).appendTo($list);
$('#pureFontModal-btn-container').before($sec.append($hdr).append($list));
}
$(document).on('click', '#pt-font-setup', function() {
$('#pureFontModal').remove();
newUploadedDataUrl = null;
currentSelectedFontName = '';
var hLoad = loadH();
originalFontBeforeOpen = (hLoad.length && hLoad[0].name) ? hLoad[0].name : 'default';
var topName = (hLoad.length && hLoad[0].name !== 'default') ? hLoad[0].name : '';
var $modal = $('<div>', { id: 'pureFontModal' }).css({
'position': 'fixed', 'top': '20%', 'left': '50%', 'transform': 'translate(-50%, -20%)',
'background': '#fff', 'padding': '20px', 'border': '1px solid #72777d', 'border-radius': '4px',
'box-shadow': '0 2px 10px rgba(0,0,0,0.3)', 'z-index': '999999', 'width': '320px', 'color': '#000'
}).html(
'<h3 style="margin:0 0 15px 0; font-size:16px; text-align:left;">Font Uploader & History</h3>' +
'<label style="display:block; font-size:12px; margin-bottom:5px; text-align:left;">1. Select Font File (.ttf / .woff2 / .otf):</label>' +
'<input type="file" id="pureFileInput" accept=".ttf,.woff,.woff2,.otf" style="display:block; margin-bottom:15px; width:100%; font-size:12px;">' +
'<label style="display:block; font-size:12px; margin-bottom:5px; text-align:left;">2. Enter Font Name:</label>' +
'<input type="text" id="pureNameInput" style="display:block; margin-bottom:20px; width:100%; padding:4px; box-sizing:border-box;" value="' + topName + '">'
);
var $btnBox = $('<div>', { id: 'pureFontModal-btn-container' }).css({ 'display': 'flex', 'justify-content': 'space-between', 'align-items': 'center', 'margin-top': '15px' });
var $resetBtn = $('<button>', { id: 'pureResetBtn' }).text('default').css({ 'padding': '5px 10px', 'font-size': '12px', 'cursor': 'pointer', 'background': '#d33', 'color': '#fff', 'border': 'none', 'border-radius': '2px', 'font-weight': 'bold' });
$resetBtn.on('click', function(e) {
e.stopPropagation();
var h = loadH();
var idx = h.findIndex(function(i){return i.name==='default'});
if (idx !== -1) {
var dItem = h.splice(idx, 1)[0];
h.unshift(dItem);
}
localStorage.setItem(KEY, JSON.stringify(h));
applyFont('default');
$('#pureNameInput').val('');
newUploadedDataUrl = null;
currentSelectedFontName = 'default';
ui();
});
var $rightBtns = $('<div>').css({ 'display': 'flex', 'gap': '10px' });
var $cancel = $('<button>', { id: 'pureCloseBtn' }).text('Cancel').css({ 'padding': '5px 10px', 'font-size': '12px', 'cursor': 'pointer' });
var $save = $('<button>', { id: 'pureSaveBtn' }).text('Save').css({ 'padding': '5px 15px', 'font-size': '12px', 'cursor': 'pointer', 'background': '#36c', 'color': '#fff', 'border': 'none', 'border-radius': '2px' });
$rightBtns.append($cancel).append($save);
$btnBox.append($resetBtn).append($rightBtns);
$modal.append($btnBox).appendTo('body');
ui();
});
$(document).on('change', '#pureFileInput', function() {
if (this.files && this.files[0]) {
var file = this.files[0];
var r = new FileReader();
r.onload = function(e) {
newUploadedDataUrl = e.target.result;
currentSelectedFontName = '';
var fontName = $('#pureNameInput').val().trim();
if (!fontName) {
fontName = file.name.substring(0, file.name.lastIndexOf('.')) || 'TempFont';
$('#pureNameInput').val(fontName);
}
applyFont(fontName, newUploadedDataUrl);
};
r.readAsDataURL(file);
}
});
$(document).on('input', '#pureNameInput', function() {
var fontName = $(this).val().trim();
if (fontName && newUploadedDataUrl) {
applyFont(fontName, newUploadedDataUrl);
}
});
$(document).on('click', '#pureSaveBtn', function() {
var name = $('#pureNameInput').val().trim(); if (!name) return alert('フォント名を入力してください。');
try {
if (newUploadedDataUrl) {
openDB().done(function(db) {
var tx = db.transaction([STORE], 'readwrite');
tx.objectStore(STORE).put({ id: name, dataUrl: newUploadedDataUrl });
tx.oncomplete = function() {
updateH(name); alert('保存しました!'); $('#pureFontModal').remove(); newUploadedDataUrl = null;
};
tx.onerror = function() {
alert('⚠️ 【容量オーバー警告】IndexedDBへの書き込みに失敗しました。');
};
});
} else if (currentSelectedFontName) {
if (currentSelectedFontName === 'default') {
applyFont('default');
}
updateH(currentSelectedFontName); $('#pureFontModal').remove();
} else {
updateH(name); applyFont(name); $('#pureFontModal').remove();
}
} catch (error) {
alert('❌ エラーが発生したため保存を中断しました:\n' + error.message);
$('#pureFontModal').remove();
}
}).on('click', '#pureCloseBtn', function() {
$('#pureFontModal').remove();
newUploadedDataUrl = null;
currentSelectedFontName = '';
var targetFont = originalFontBeforeOpen;
if (targetFont && targetFont !== 'default') {
getFont(targetFont, function(r) { applyFont(targetFont, r ? r.dataUrl : null); });
} else {
applyFont('default');
}
});
});
})();
1aqd28d78mqwi3ept3fizmfbs58qvkr
769446
769445
2026-10-21T13:21:20Z
OUT is this
73801
769446
javascript
text/javascript
(function() {
'use strict';
if (!mw.config.get('wgUserId')) return;
var KEY = 'wikifonts_history_addon', T_KEY = 'wikifonts_history_toggle_state';
var DB = 'PureFontUploaderDB', STORE = 'fontStore';
var isOpen = localStorage.getItem(T_KEY) === 'open';
var newUploadedDataUrl = null;
var currentSelectedFontName = '';
var originalFontBeforeOpen = null;
function openDB() {
return $.Deferred(function(d) {
var r = indexedDB.open(DB, 1);
r.onupgradeneeded = function(e) { if (!e.target.result.objectStoreNames.contains(STORE)) e.target.result.createObjectStore(STORE, { keyPath: 'id' }); };
r.onsuccess = function(e) { d.resolve(e.target.result); };
}).promise();
}
function getFont(name, cb) {
openDB().done(function(db) {
db.transaction([STORE], 'readonly').objectStore(STORE).get(name).onsuccess = function(e) { cb(e.target.result); };
});
}
function loadH() {
try {
var h = JSON.parse(localStorage.getItem(KEY) || '[]');
if (!h.some(function(i){return i.name==='default'})) { h.push({name:'default',starred:false,time:0}); localStorage.setItem(KEY, JSON.stringify(h)); }
return h;
} catch(e) { return [{name:'default',starred:false,time:0}]; }
}
function applyFont(name, url) {
$('#testFontFace').remove();
if (name === 'default') {
document.body.style.removeProperty('font-family');
return;
}
if (url) $('<style>', { id: 'testFontFace' }).text('@font-face { font-family: "' + name.replace(/"/g, '\\"') + '"; src: url("' + url + '"); }').appendTo('head');
document.body.style.setProperty('font-family', '"' + name + '", sans-serif', 'important');
}
// 【同期】リロード時の自動復元
$(document).ready(function() {
var h = loadH();
if (h.length && h[0].name !== 'default') {
getFont(h[0].name, function(r) { if (r) applyFont(r.id, r.dataUrl); else applyFont(h[0].name); });
}
});
$(document).ready(function() {
$('#pt-font-setup').remove();
// sm link
var svgIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" style="vertical-align: middle; fill: currentColor; margin-right: 4px;">' +
'<path d="M19.43 12.98c.04-.32.07-.64.07-.98 0-.34-.03-.66-.07-.98l2.11-1.65c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.3-.61-.22l-2.49 1c-.52-.4-1.08-.73-1.69-.98l-.38-2.65C14.46 2.18 14.25 2 14 2h-4c-.25 0-.46.18-.49.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1c-.23-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64l2.11 1.65c-.04.32-.07.65-.07.98 0 .33.03.66.07.98l-2.11 1.65c-.19.15-.24.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65c.03.24.24.42.49.42h4c.25 0 .46-.18.49-.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1c.23.09.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.65zM12 15.5c-1.93 0-3.5-1.57-3.5-3.5s1.57-3.5 3.5-3.5 3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5z"/>' +
'<text x="12" y="14.5" font-family="sans-serif" font-size="8.5" font-weight="bold" text-anchor="middle" fill="#fff" stroke="#fff" stroke-width="2" stroke-linejoin="round">Aa</text>' +
'<text x="12" y="14.5" font-family="sans-serif" font-size="8.5" font-weight="bold" text-anchor="middle" fill="#000">Aa</text>' +
'</svg>';
mw.util.addPortletLink('p-personal', 'javascript:void(0);', '', 'pt-font-setup', 'Open uploader', null, '#pt-logout');
$('#pt-font-setup a').html(svgIcon + 'Font Setup');
function updateH(font) {
if (!font) return;
var h = loadH(), idx = h.findIndex(function(i){return i.name===font}), star = idx !== -1 ? h[idx].starred : false;
if (idx !== -1) h.splice(idx, 1);
h.unshift({name: font, starred: star, time: Date.now()});
var s = h.filter(function(i){return i.starred && i.name!=='default'}),
u = h.filter(function(i){return !i.starred && i.name!=='default'});
h = s.sort(function(a,b){return b.time-a.time}).concat(u.sort(function(a,b){return b.time-a.time}));
h.push({name:'default',starred:false,time:0});
try {
localStorage.setItem(KEY, JSON.stringify(h));
} catch (e) {
alert('⚠️ 【容量警告】LocalStorageの容量が不足しているため履歴の保存に失敗しました。');
}
ui();
}
function ui() {
var $box = $('#pureFontModal'); if (!$box.length) return;
$('#wikifonts-history-section').remove();
var $sec = $('<div>',{id: 'wikifonts-history-section'}).css({'margin-top':'15px','padding-top':'10px','border-top':'1px solid #72777d','text-align':'left'});
var $hdr = $('<div>').css({'display':'flex','justify-content':'space-between','align-items':'center','padding':'4px 0','color':'#000','font-size':'13px'});
var $titleLeft = $('<span>').text('History').css({'color':'#000','font-weight':'bold'});
var $toggleLink = $('<span>', { id: 'wf-history-arrow' }).text(isOpen ? '[非表示]' : '[表示]').css({'color':'#36c','cursor':'pointer','font-size':'12px'});
$toggleLink.on('click', function() {
isOpen = $list.is(':hidden');
$list.toggle(isOpen);
$toggleLink.text(isOpen ? '[非表示]' : '[表示]');
localStorage.setItem(T_KEY, isOpen ? 'open' : 'close');
});
$hdr.append($titleLeft).append($toggleLink);
var $list = $('<div>',{id: 'wikifonts-history-list'}).css({'display':isOpen?'block':'none','max-height':'120px','overflow-y':'auto','margin-top':'5px'});
loadH().forEach(function(item) {
if (item.name === 'default') return;
var $row = $('<div>').css({'display':'flex','align-items':'center','justify-content':'space-between','padding':'4px 0','border-bottom':'1px solid rgba(0,0,0,0.1)'});
var $left = $('<div>').css({'display':'flex','align-items':'center','gap':'8px'});
$('<span>').text(item.starred?'⭐':'☆').css({'cursor':'pointer','font-size':'12px','color':item.starred?'#ffc107':'#000'})
.on('click', function(e) { e.stopPropagation();
var h = loadH(), i = h.find(function(f){return f.name===item.name});
if (i) { i.starred = !i.starred; localStorage.setItem(KEY, JSON.stringify(h)); ui(); }
}).appendTo($left);
var escapedName = item.name.replace(/"/g, '\\"');
var $nameLabel = $('<span>').text(item.name).css({
'font-family': '"' + escapedName + '", sans-serif',
'cursor': 'pointer',
'font-size': '13px',
'color': '#000'
}).on('mouseenter', function() { $(this).css({'color':'#0074D9','text-decoration':'underline'}); })
.on('mouseleave', function() { $(this).css({'color':'#000','text-decoration':'none'}); })
.on('click', function() {
$('#pureNameInput').val(item.name);
currentSelectedFontName = item.name;
newUploadedDataUrl = null;
getFont(item.name, function(r){ applyFont(item.name, r?r.dataUrl:null); });
});
// 各行ごとに確実に@font-faceを生成・適用(安全なCSS IDを使用)
getFont(item.name, function(r) {
if (r && r.dataUrl) {
var safeId = btoa(unescape(encodeURIComponent(item.name))).replace(/[^a-zA-Z0-9_-]/g, '_');
var listStyleId = 'testFontFace-' + safeId;
if (!$('#' + listStyleId).length) {
$('<style>', { id: listStyleId }).text('@font-face { font-family: "' + escapedName + '"; src: url("' + r.dataUrl + '"); }').appendTo('head');
}
}
});
$nameLabel.appendTo($left);
var $rightBtns = $('<div>').css({'display':'flex','align-items':'center','gap':'4px'});
// ✏️ 編集ボタン(名前変更用)
var $editBtn = $('<span>').text('✏️').css({'cursor':'pointer','font-size':'10px','padding':'2px','opacity':'0.6'})
.on('mouseenter', function() { $(this).css('opacity', '1'); })
.on('mouseleave', function() { $(this).css('opacity', '0.6'); })
.on('click', function(e) {
e.stopPropagation();
var newName = prompt('新しいフォント名を入力してください:', item.name);
if (!newName || !(newName = newName.trim())) return;
if (newName === item.name) return;
getFont(item.name, function(r) {
if (!r || !r.dataUrl) {
alert('フォントデータの取得に失敗しました。');
return;
}
openDB().done(function(db) {
var tx = db.transaction([STORE], 'readwrite');
var store = tx.objectStore(STORE);
store.delete(item.name); // 旧データの削除
store.put({ id: newName, dataUrl: r.dataUrl }); // 新名で登録
tx.oncomplete = function() {
// 古いスタイルのDOMを確実に削除
var oldSafeId = btoa(unescape(encodeURIComponent(item.name))).replace(/[^a-zA-Z0-9_-]/g, '_');
$('#testFontFace-' + oldSafeId).remove();
// 新しい名前用のスタイルを即座に登録
var newEscapedName = newName.replace(/"/g, '\\"');
var newSafeId = btoa(unescape(encodeURIComponent(newName))).replace(/[^a-zA-Z0-9_-]/g, '_');
var newStyleId = 'testFontFace-' + newSafeId;
if (!$('#' + newStyleId).length) {
$('<style>', { id: newStyleId }).text('@font-face { font-family: "' + newEscapedName + '"; src: url("' + r.dataUrl + '"); }').appendTo('head');
}
var h = loadH();
var target = h.find(function(f){return f.name===item.name});
var star = target ? target.starred : false;
h = h.filter(function(f){return f.name!==item.name});
h.unshift({name: newName, starred: star, time: Date.now()});
var s = h.filter(function(i){return i.starred && i.name!=='default'}),
u = h.filter(function(i){return !i.starred && i.name!=='default'});
h = s.sort(function(a,b){return b.time-a.time}).concat(u.sort(function(a,b){return b.time-a.time}));
h.push({name:'default',starred:false,time:0});
localStorage.setItem(KEY, JSON.stringify(h));
applyFont(newName, r.dataUrl);
ui();
};
});
});
});
// ❌ 削除ボタン
var $deleteBtn = $('<span>').text('❌').css({'cursor':'pointer','font-size':'10px','padding':'2px','opacity':'0.5'})
.on('mouseenter', function() { $(this).css('opacity', '1'); })
.on('mouseleave', function() { $(this).css('opacity', '0.5'); })
.on('click', function(e) { e.stopPropagation();
if (confirm('フォント「' + item.name + '」を削除しますか?')) {
var h = loadH().filter(function(f){return f.name!==item.name});
localStorage.setItem(KEY, JSON.stringify(h));
var safeId = btoa(unescape(encodeURIComponent(item.name))).replace(/[^a-zA-Z0-9_-]/g, '_');
$('#testFontFace-' + safeId).remove();
openDB().done(function(db){
db.transaction([STORE], 'readwrite').objectStore(STORE).delete(item.name);
}); ui();
}
});
$rightBtns.append($editBtn).append($deleteBtn);
$row.append($left).append($rightBtns).appendTo($list);
});
if (!$list.children().length) $('<div>').text('No history').css({'color':'#54595d','font-style':'italic','padding':'4px 0','font-size':'12px'}).appendTo($list);
$('#pureFontModal-btn-container').before($sec.append($hdr).append($list));
}
$(document).on('click', '#pt-font-setup', function() {
$('#pureFontModal').remove();
newUploadedDataUrl = null;
currentSelectedFontName = '';
var hLoad = loadH();
originalFontBeforeOpen = (hLoad.length && hLoad[0].name) ? hLoad[0].name : 'default';
var topName = (hLoad.length && hLoad[0].name !== 'default') ? hLoad[0].name : '';
var $modal = $('<div>', { id: 'pureFontModal' }).css({
'position': 'fixed', 'top': '20%', 'left': '50%', 'transform': 'translate(-50%, -20%)',
'background': '#fff', 'padding': '20px', 'border': '1px solid #72777d', 'border-radius': '4px',
'box-shadow': '0 2px 10px rgba(0,0,0,0.3)', 'z-index': '999999', 'width': '320px', 'color': '#000'
}).html(
'<h3 style="margin:0 0 15px 0; font-size:16px; text-align:left;">Font Uploader & History</h3>' +
'<label style="display:block; font-size:12px; margin-bottom:5px; text-align:left;">1. Select Font File (.ttf / .woff2 / .otf):</label>' +
'<input type="file" id="pureFileInput" accept=".ttf,.woff,.woff2,.otf" style="display:block; margin-bottom:15px; width:100%; font-size:12px;">' +
'<label style="display:block; font-size:12px; margin-bottom:5px; text-align:left;">2. Enter Font Name:</label>' +
'<input type="text" id="pureNameInput" style="display:block; margin-bottom:20px; width:100%; padding:4px; box-sizing:border-box;" value="' + topName + '">'
);
var $btnBox = $('<div>', { id: 'pureFontModal-btn-container' }).css({ 'display': 'flex', 'justify-content': 'space-between', 'align-items': 'center', 'margin-top': '15px' });
var $resetBtn = $('<button>', { id: 'pureResetBtn' }).text('default').css({ 'padding': '5px 10px', 'font-size': '12px', 'cursor': 'pointer', 'background': '#d33', 'color': '#fff', 'border': 'none', 'border-radius': '2px', 'font-weight': 'bold' });
$resetBtn.on('click', function(e) {
e.stopPropagation();
var h = loadH();
var idx = h.findIndex(function(i){return i.name==='default'});
if (idx !== -1) {
var dItem = h.splice(idx, 1)[0];
h.unshift(dItem);
}
localStorage.setItem(KEY, JSON.stringify(h));
applyFont('default');
$('#pureNameInput').val('');
newUploadedDataUrl = null;
currentSelectedFontName = 'default';
ui();
});
var $rightBtns = $('<div>').css({ 'display': 'flex', 'gap': '10px' });
var $cancel = $('<button>', { id: 'pureCloseBtn' }).text('Cancel').css({ 'padding': '5px 10px', 'font-size': '12px', 'cursor': 'pointer' });
var $save = $('<button>', { id: 'pureSaveBtn' }).text('Save').css({ 'padding': '5px 15px', 'font-size': '12px', 'cursor': 'pointer', 'background': '#36c', 'color': '#fff', 'border': 'none', 'border-radius': '2px' });
$rightBtns.append($cancel).append($save);
$btnBox.append($resetBtn).append($rightBtns);
$modal.append($btnBox).appendTo('body');
ui();
});
$(document).on('change', '#pureFileInput', function() {
if (this.files && this.files[0]) {
var file = this.files[0];
var r = new FileReader();
r.onload = function(e) {
newUploadedDataUrl = e.target.result;
currentSelectedFontName = '';
var fontName = $('#pureNameInput').val().trim();
if (!fontName) {
fontName = file.name.substring(0, file.name.lastIndexOf('.')) || 'TempFont';
$('#pureNameInput').val(fontName);
}
applyFont(fontName, newUploadedDataUrl);
};
r.readAsDataURL(file);
}
});
$(document).on('input', '#pureNameInput', function() {
var fontName = $(this).val().trim();
if (fontName && newUploadedDataUrl) {
applyFont(fontName, newUploadedDataUrl);
}
});
$(document).on('click', '#pureSaveBtn', function() {
var name = $('#pureNameInput').val().trim(); if (!name) return alert('フォント名を入力してください。');
try {
if (newUploadedDataUrl) {
openDB().done(function(db) {
var tx = db.transaction([STORE], 'readwrite');
tx.objectStore(STORE).put({ id: name, dataUrl: newUploadedDataUrl });
tx.oncomplete = function() {
updateH(name); alert('保存しました!'); $('#pureFontModal').remove(); newUploadedDataUrl = null;
};
tx.onerror = function() {
alert('⚠️ 【容量オーバー警告】IndexedDBへの書き込みに失敗しました。');
};
});
} else if (currentSelectedFontName) {
if (currentSelectedFontName === 'default') {
applyFont('default');
}
updateH(currentSelectedFontName); $('#pureFontModal').remove();
} else {
updateH(name); applyFont(name); $('#pureFontModal').remove();
}
} catch (error) {
alert('❌ エラーが発生したため保存を中断しました:\n' + error.message);
$('#pureFontModal').remove();
}
}).on('click', '#pureCloseBtn', function() {
$('#pureFontModal').remove();
newUploadedDataUrl = null;
currentSelectedFontName = '';
var targetFont = originalFontBeforeOpen;
if (targetFont && targetFont !== 'default') {
getFont(targetFont, function(r) { applyFont(targetFont, r ? r.dataUrl : null); });
} else {
applyFont('default');
}
});
});
})();
084npk8powfjm2dnwn5c7tits357m1a
769447
769446
2026-10-21T13:30:27Z
OUT is this
73801
769447
javascript
text/javascript
(function() {
'use strict';
if (!mw.config.get('wgUserId')) return;
var KEY = 'wikifonts_history_addon', T_KEY = 'wikifonts_history_toggle_state';
var DB = 'PureFontUploaderDB', STORE = 'fontStore';
var isOpen = localStorage.getItem(T_KEY) === 'open';
var newUploadedDataUrl = null;
var currentSelectedFontName = '';
var originalFontBeforeOpen = null;
function openDB() {
return $.Deferred(function(d) {
var r = indexedDB.open(DB, 1);
r.onupgradeneeded = function(e) { if (!e.target.result.objectStoreNames.contains(STORE)) e.target.result.createObjectStore(STORE, { keyPath: 'id' }); };
r.onsuccess = function(e) { d.resolve(e.target.result); };
}).promise();
}
function getFont(name, cb) {
openDB().done(function(db) {
db.transaction([STORE], 'readonly').objectStore(STORE).get(name).onsuccess = function(e) { cb(e.target.result); };
});
}
function loadH() {
try {
var h = JSON.parse(localStorage.getItem(KEY) || '[]');
if (!h.some(function(i){return i.name==='default'})) { h.push({name:'default',starred:false,time:0}); localStorage.setItem(KEY, JSON.stringify(h)); }
return h;
} catch(e) { return [{name:'default',starred:false,time:0}]; }
}
function applyFont(name, url) {
$('#testFontFace').remove();
if (name === 'default') {
document.body.style.removeProperty('font-family');
return;
}
if (url) $('<style>', { id: 'testFontFace' }).text('@font-face { font-family: "' + name.replace(/"/g, '\\"') + '"; src: url("' + url + '"); }').appendTo('head');
document.body.style.setProperty('font-family', '"' + name + '", sans-serif', 'important');
}
// 【同期】リロード時の自動復元
$(document).ready(function() {
var h = loadH();
if (h.length && h[0].name !== 'default') {
getFont(h[0].name, function(r) { if (r) applyFont(r.id, r.dataUrl); else applyFont(h[0].name); });
}
});
$(document).ready(function() {
$('#pt-font-setup').remove();
var smLinks = $(
['', '-sticky-header']
.map(function(suffix) {
var parentId = $('#p-user-menu-pages' + suffix).length ? 'p-user-menu-pages' : 'p-personal';
return mw.util.addPortletLink(
parentId + suffix,
'javascript:void(0);',
'',
'pt-font-setup' + suffix,
'Open uploader',
null,
'#pt-logout' + suffix
);
})
.filter(function(elOrNull) { return elOrNull !== null; })
);
if (smLinks.length > 0) {
const style = `
li[id^="svgIcon"] a {
width: auto !important;
padding-left: 25px;
white-space: nowrap;
height: 20px;
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" style="vertical-align: middle; fill: currentColor; margin-right: 4px;">' +
'<path d="M19.43 12.98c.04-.32.07-.64.07-.98 0-.34-.03-.66-.07-.98l2.11-1.65c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.3-.61-.22l-2.49 1c-.52-.4-1.08-.73-1.69-.98l-.38-2.65C14.46 2.18 14.25 2 14 2h-4c-.25 0-.46.18-.49.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1c-.23-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64l2.11 1.65c-.04.32-.07.65-.07.98 0 .33.03.66.07.98l-2.11 1.65c-.19.15-.24.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65c.03.24.24.42.49.42h4c.25 0 .46-.18.49-.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1c.23.09.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.65zM12 15.5c-1.93 0-3.5-1.57-3.5-3.5s1.57-3.5 3.5-3.5 3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5z"/>' +
'<text x="12" y="14.5" font-family="sans-serif" font-size="8.5" font-weight="bold" text-anchor="middle" fill="#fff" stroke="#fff" stroke-width="2" stroke-linejoin="round">Aa</text>' +
'<text x="12" y="14.5" font-family="sans-serif" font-size="8.5" font-weight="bold" text-anchor="middle" fill="#000">Aa</text>' +
'</svg>');
background-repeat: no-repeat;
background-size: 20px 20px;
background-position: center left;
}
`;
$('<style>').text(style).appendTo('head');
}
function updateH(font) {
if (!font) return;
var h = loadH(), idx = h.findIndex(function(i){return i.name===font}), star = idx !== -1 ? h[idx].starred : false;
if (idx !== -1) h.splice(idx, 1);
h.unshift({name: font, starred: star, time: Date.now()});
var s = h.filter(function(i){return i.starred && i.name!=='default'}),
u = h.filter(function(i){return !i.starred && i.name!=='default'});
h = s.sort(function(a,b){return b.time-a.time}).concat(u.sort(function(a,b){return b.time-a.time}));
h.push({name:'default',starred:false,time:0});
try {
localStorage.setItem(KEY, JSON.stringify(h));
} catch (e) {
alert('⚠️ 【容量警告】LocalStorageの容量が不足しているため履歴の保存に失敗しました。');
}
ui();
}
function ui() {
var $box = $('#pureFontModal'); if (!$box.length) return;
$('#wikifonts-history-section').remove();
var $sec = $('<div>',{id: 'wikifonts-history-section'}).css({'margin-top':'15px','padding-top':'10px','border-top':'1px solid #72777d','text-align':'left'});
var $hdr = $('<div>').css({'display':'flex','justify-content':'space-between','align-items':'center','padding':'4px 0','color':'#000','font-size':'13px'});
var $titleLeft = $('<span>').text('History').css({'color':'#000','font-weight':'bold'});
var $toggleLink = $('<span>', { id: 'wf-history-arrow' }).text(isOpen ? '[非表示]' : '[表示]').css({'color':'#36c','cursor':'pointer','font-size':'12px'});
$toggleLink.on('click', function() {
isOpen = $list.is(':hidden');
$list.toggle(isOpen);
$toggleLink.text(isOpen ? '[非表示]' : '[表示]');
localStorage.setItem(T_KEY, isOpen ? 'open' : 'close');
});
$hdr.append($titleLeft).append($toggleLink);
var $list = $('<div>',{id: 'wikifonts-history-list'}).css({'display':isOpen?'block':'none','max-height':'120px','overflow-y':'auto','margin-top':'5px'});
loadH().forEach(function(item) {
if (item.name === 'default') return;
var $row = $('<div>').css({'display':'flex','align-items':'center','justify-content':'space-between','padding':'4px 0','border-bottom':'1px solid rgba(0,0,0,0.1)'});
var $left = $('<div>').css({'display':'flex','align-items':'center','gap':'8px'});
$('<span>').text(item.starred?'⭐':'☆').css({'cursor':'pointer','font-size':'12px','color':item.starred?'#ffc107':'#000'})
.on('click', function(e) { e.stopPropagation();
var h = loadH(), i = h.find(function(f){return f.name===item.name});
if (i) { i.starred = !i.starred; localStorage.setItem(KEY, JSON.stringify(h)); ui(); }
}).appendTo($left);
var escapedName = item.name.replace(/"/g, '\\"');
var $nameLabel = $('<span>').text(item.name).css({
'font-family': '"' + escapedName + '", sans-serif',
'cursor': 'pointer',
'font-size': '13px',
'color': '#000'
}).on('mouseenter', function() { $(this).css({'color':'#0074D9','text-decoration':'underline'}); })
.on('mouseleave', function() { $(this).css({'color':'#000','text-decoration':'none'}); })
.on('click', function() {
$('#pureNameInput').val(item.name);
currentSelectedFontName = item.name;
newUploadedDataUrl = null;
getFont(item.name, function(r){ applyFont(item.name, r?r.dataUrl:null); });
});
// 各行ごとに確実に@font-faceを生成・適用(安全なCSS IDを使用)
getFont(item.name, function(r) {
if (r && r.dataUrl) {
var safeId = btoa(unescape(encodeURIComponent(item.name))).replace(/[^a-zA-Z0-9_-]/g, '_');
var listStyleId = 'testFontFace-' + safeId;
if (!$('#' + listStyleId).length) {
$('<style>', { id: listStyleId }).text('@font-face { font-family: "' + escapedName + '"; src: url("' + r.dataUrl + '"); }').appendTo('head');
}
}
});
$nameLabel.appendTo($left);
var $rightBtns = $('<div>').css({'display':'flex','align-items':'center','gap':'4px'});
// ✏️ 編集ボタン(名前変更用)
var $editBtn = $('<span>').text('✏️').css({'cursor':'pointer','font-size':'10px','padding':'2px','opacity':'0.6'})
.on('mouseenter', function() { $(this).css('opacity', '1'); })
.on('mouseleave', function() { $(this).css('opacity', '0.6'); })
.on('click', function(e) {
e.stopPropagation();
var newName = prompt('新しいフォント名を入力してください:', item.name);
if (!newName || !(newName = newName.trim())) return;
if (newName === item.name) return;
getFont(item.name, function(r) {
if (!r || !r.dataUrl) {
alert('フォントデータの取得に失敗しました。');
return;
}
openDB().done(function(db) {
var tx = db.transaction([STORE], 'readwrite');
var store = tx.objectStore(STORE);
store.delete(item.name); // 旧データの削除
store.put({ id: newName, dataUrl: r.dataUrl }); // 新名で登録
tx.oncomplete = function() {
// 古いスタイルのDOMを確実に削除
var oldSafeId = btoa(unescape(encodeURIComponent(item.name))).replace(/[^a-zA-Z0-9_-]/g, '_');
$('#testFontFace-' + oldSafeId).remove();
// 新しい名前用のスタイルを即座に登録
var newEscapedName = newName.replace(/"/g, '\\"');
var newSafeId = btoa(unescape(encodeURIComponent(newName))).replace(/[^a-zA-Z0-9_-]/g, '_');
var newStyleId = 'testFontFace-' + newSafeId;
if (!$('#' + newStyleId).length) {
$('<style>', { id: newStyleId }).text('@font-face { font-family: "' + newEscapedName + '"; src: url("' + r.dataUrl + '"); }').appendTo('head');
}
var h = loadH();
var target = h.find(function(f){return f.name===item.name});
var star = target ? target.starred : false;
h = h.filter(function(f){return f.name!==item.name});
h.unshift({name: newName, starred: star, time: Date.now()});
var s = h.filter(function(i){return i.starred && i.name!=='default'}),
u = h.filter(function(i){return !i.starred && i.name!=='default'});
h = s.sort(function(a,b){return b.time-a.time}).concat(u.sort(function(a,b){return b.time-a.time}));
h.push({name:'default',starred:false,time:0});
localStorage.setItem(KEY, JSON.stringify(h));
applyFont(newName, r.dataUrl);
ui();
};
});
});
});
// ❌ 削除ボタン
var $deleteBtn = $('<span>').text('❌').css({'cursor':'pointer','font-size':'10px','padding':'2px','opacity':'0.5'})
.on('mouseenter', function() { $(this).css('opacity', '1'); })
.on('mouseleave', function() { $(this).css('opacity', '0.5'); })
.on('click', function(e) { e.stopPropagation();
if (confirm('フォント「' + item.name + '」を削除しますか?')) {
var h = loadH().filter(function(f){return f.name!==item.name});
localStorage.setItem(KEY, JSON.stringify(h));
var safeId = btoa(unescape(encodeURIComponent(item.name))).replace(/[^a-zA-Z0-9_-]/g, '_');
$('#testFontFace-' + safeId).remove();
openDB().done(function(db){
db.transaction([STORE], 'readwrite').objectStore(STORE).delete(item.name);
}); ui();
}
});
$rightBtns.append($editBtn).append($deleteBtn);
$row.append($left).append($rightBtns).appendTo($list);
});
if (!$list.children().length) $('<div>').text('No history').css({'color':'#54595d','font-style':'italic','padding':'4px 0','font-size':'12px'}).appendTo($list);
$('#pureFontModal-btn-container').before($sec.append($hdr).append($list));
}
$(document).on('click', '#pt-font-setup', function() {
$('#pureFontModal').remove();
newUploadedDataUrl = null;
currentSelectedFontName = '';
var hLoad = loadH();
originalFontBeforeOpen = (hLoad.length && hLoad[0].name) ? hLoad[0].name : 'default';
var topName = (hLoad.length && hLoad[0].name !== 'default') ? hLoad[0].name : '';
var $modal = $('<div>', { id: 'pureFontModal' }).css({
'position': 'fixed', 'top': '20%', 'left': '50%', 'transform': 'translate(-50%, -20%)',
'background': '#fff', 'padding': '20px', 'border': '1px solid #72777d', 'border-radius': '4px',
'box-shadow': '0 2px 10px rgba(0,0,0,0.3)', 'z-index': '999999', 'width': '320px', 'color': '#000'
}).html(
'<h3 style="margin:0 0 15px 0; font-size:16px; text-align:left;">Font Uploader & History</h3>' +
'<label style="display:block; font-size:12px; margin-bottom:5px; text-align:left;">1. Select Font File (.ttf / .woff2 / .otf):</label>' +
'<input type="file" id="pureFileInput" accept=".ttf,.woff,.woff2,.otf" style="display:block; margin-bottom:15px; width:100%; font-size:12px;">' +
'<label style="display:block; font-size:12px; margin-bottom:5px; text-align:left;">2. Enter Font Name:</label>' +
'<input type="text" id="pureNameInput" style="display:block; margin-bottom:20px; width:100%; padding:4px; box-sizing:border-box;" value="' + topName + '">'
);
var $btnBox = $('<div>', { id: 'pureFontModal-btn-container' }).css({ 'display': 'flex', 'justify-content': 'space-between', 'align-items': 'center', 'margin-top': '15px' });
var $resetBtn = $('<button>', { id: 'pureResetBtn' }).text('default').css({ 'padding': '5px 10px', 'font-size': '12px', 'cursor': 'pointer', 'background': '#d33', 'color': '#fff', 'border': 'none', 'border-radius': '2px', 'font-weight': 'bold' });
$resetBtn.on('click', function(e) {
e.stopPropagation();
var h = loadH();
var idx = h.findIndex(function(i){return i.name==='default'});
if (idx !== -1) {
var dItem = h.splice(idx, 1)[0];
h.unshift(dItem);
}
localStorage.setItem(KEY, JSON.stringify(h));
applyFont('default');
$('#pureNameInput').val('');
newUploadedDataUrl = null;
currentSelectedFontName = 'default';
ui();
});
var $rightBtns = $('<div>').css({ 'display': 'flex', 'gap': '10px' });
var $cancel = $('<button>', { id: 'pureCloseBtn' }).text('Cancel').css({ 'padding': '5px 10px', 'font-size': '12px', 'cursor': 'pointer' });
var $save = $('<button>', { id: 'pureSaveBtn' }).text('Save').css({ 'padding': '5px 15px', 'font-size': '12px', 'cursor': 'pointer', 'background': '#36c', 'color': '#fff', 'border': 'none', 'border-radius': '2px' });
$rightBtns.append($cancel).append($save);
$btnBox.append($resetBtn).append($rightBtns);
$modal.append($btnBox).appendTo('body');
ui();
});
$(document).on('change', '#pureFileInput', function() {
if (this.files && this.files[0]) {
var file = this.files[0];
var r = new FileReader();
r.onload = function(e) {
newUploadedDataUrl = e.target.result;
currentSelectedFontName = '';
var fontName = $('#pureNameInput').val().trim();
if (!fontName) {
fontName = file.name.substring(0, file.name.lastIndexOf('.')) || 'TempFont';
$('#pureNameInput').val(fontName);
}
applyFont(fontName, newUploadedDataUrl);
};
r.readAsDataURL(file);
}
});
$(document).on('input', '#pureNameInput', function() {
var fontName = $(this).val().trim();
if (fontName && newUploadedDataUrl) {
applyFont(fontName, newUploadedDataUrl);
}
});
$(document).on('click', '#pureSaveBtn', function() {
var name = $('#pureNameInput').val().trim(); if (!name) return alert('フォント名を入力してください。');
try {
if (newUploadedDataUrl) {
openDB().done(function(db) {
var tx = db.transaction([STORE], 'readwrite');
tx.objectStore(STORE).put({ id: name, dataUrl: newUploadedDataUrl });
tx.oncomplete = function() {
updateH(name); alert('保存しました!'); $('#pureFontModal').remove(); newUploadedDataUrl = null;
};
tx.onerror = function() {
alert('⚠️ 【容量オーバー警告】IndexedDBへの書き込みに失敗しました。');
};
});
} else if (currentSelectedFontName) {
if (currentSelectedFontName === 'default') {
applyFont('default');
}
updateH(currentSelectedFontName); $('#pureFontModal').remove();
} else {
updateH(name); applyFont(name); $('#pureFontModal').remove();
}
} catch (error) {
alert('❌ エラーが発生したため保存を中断しました:\n' + error.message);
$('#pureFontModal').remove();
}
}).on('click', '#pureCloseBtn', function() {
$('#pureFontModal').remove();
newUploadedDataUrl = null;
currentSelectedFontName = '';
var targetFont = originalFontBeforeOpen;
if (targetFont && targetFont !== 'default') {
getFont(targetFont, function(r) { applyFont(targetFont, r ? r.dataUrl : null); });
} else {
applyFont('default');
}
});
});
})();
870xz71wpj7hvlec289aibxo0q5dmeo
769448
769447
2026-10-21T13:31:28Z
OUT is this
73801
Undid revision [[Special:Diff/769447|769447]] by [[Special:Contributions/OUT is this|OUT is this]] ([[User talk:OUT is this|talk]])
769448
javascript
text/javascript
(function() {
'use strict';
if (!mw.config.get('wgUserId')) return;
var KEY = 'wikifonts_history_addon', T_KEY = 'wikifonts_history_toggle_state';
var DB = 'PureFontUploaderDB', STORE = 'fontStore';
var isOpen = localStorage.getItem(T_KEY) === 'open';
var newUploadedDataUrl = null;
var currentSelectedFontName = '';
var originalFontBeforeOpen = null;
function openDB() {
return $.Deferred(function(d) {
var r = indexedDB.open(DB, 1);
r.onupgradeneeded = function(e) { if (!e.target.result.objectStoreNames.contains(STORE)) e.target.result.createObjectStore(STORE, { keyPath: 'id' }); };
r.onsuccess = function(e) { d.resolve(e.target.result); };
}).promise();
}
function getFont(name, cb) {
openDB().done(function(db) {
db.transaction([STORE], 'readonly').objectStore(STORE).get(name).onsuccess = function(e) { cb(e.target.result); };
});
}
function loadH() {
try {
var h = JSON.parse(localStorage.getItem(KEY) || '[]');
if (!h.some(function(i){return i.name==='default'})) { h.push({name:'default',starred:false,time:0}); localStorage.setItem(KEY, JSON.stringify(h)); }
return h;
} catch(e) { return [{name:'default',starred:false,time:0}]; }
}
function applyFont(name, url) {
$('#testFontFace').remove();
if (name === 'default') {
document.body.style.removeProperty('font-family');
return;
}
if (url) $('<style>', { id: 'testFontFace' }).text('@font-face { font-family: "' + name.replace(/"/g, '\\"') + '"; src: url("' + url + '"); }').appendTo('head');
document.body.style.setProperty('font-family', '"' + name + '", sans-serif', 'important');
}
// 【同期】リロード時の自動復元
$(document).ready(function() {
var h = loadH();
if (h.length && h[0].name !== 'default') {
getFont(h[0].name, function(r) { if (r) applyFont(r.id, r.dataUrl); else applyFont(h[0].name); });
}
});
$(document).ready(function() {
$('#pt-font-setup').remove();
// sm link
var svgIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" style="vertical-align: middle; fill: currentColor; margin-right: 4px;">' +
'<path d="M19.43 12.98c.04-.32.07-.64.07-.98 0-.34-.03-.66-.07-.98l2.11-1.65c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.3-.61-.22l-2.49 1c-.52-.4-1.08-.73-1.69-.98l-.38-2.65C14.46 2.18 14.25 2 14 2h-4c-.25 0-.46.18-.49.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1c-.23-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64l2.11 1.65c-.04.32-.07.65-.07.98 0 .33.03.66.07.98l-2.11 1.65c-.19.15-.24.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65c.03.24.24.42.49.42h4c.25 0 .46-.18.49-.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1c.23.09.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.65zM12 15.5c-1.93 0-3.5-1.57-3.5-3.5s1.57-3.5 3.5-3.5 3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5z"/>' +
'<text x="12" y="14.5" font-family="sans-serif" font-size="8.5" font-weight="bold" text-anchor="middle" fill="#fff" stroke="#fff" stroke-width="2" stroke-linejoin="round">Aa</text>' +
'<text x="12" y="14.5" font-family="sans-serif" font-size="8.5" font-weight="bold" text-anchor="middle" fill="#000">Aa</text>' +
'</svg>';
mw.util.addPortletLink('p-personal', 'javascript:void(0);', '', 'pt-font-setup', 'Open uploader', null, '#pt-logout');
$('#pt-font-setup a').html(svgIcon + 'Font Setup');
function updateH(font) {
if (!font) return;
var h = loadH(), idx = h.findIndex(function(i){return i.name===font}), star = idx !== -1 ? h[idx].starred : false;
if (idx !== -1) h.splice(idx, 1);
h.unshift({name: font, starred: star, time: Date.now()});
var s = h.filter(function(i){return i.starred && i.name!=='default'}),
u = h.filter(function(i){return !i.starred && i.name!=='default'});
h = s.sort(function(a,b){return b.time-a.time}).concat(u.sort(function(a,b){return b.time-a.time}));
h.push({name:'default',starred:false,time:0});
try {
localStorage.setItem(KEY, JSON.stringify(h));
} catch (e) {
alert('⚠️ 【容量警告】LocalStorageの容量が不足しているため履歴の保存に失敗しました。');
}
ui();
}
function ui() {
var $box = $('#pureFontModal'); if (!$box.length) return;
$('#wikifonts-history-section').remove();
var $sec = $('<div>',{id: 'wikifonts-history-section'}).css({'margin-top':'15px','padding-top':'10px','border-top':'1px solid #72777d','text-align':'left'});
var $hdr = $('<div>').css({'display':'flex','justify-content':'space-between','align-items':'center','padding':'4px 0','color':'#000','font-size':'13px'});
var $titleLeft = $('<span>').text('History').css({'color':'#000','font-weight':'bold'});
var $toggleLink = $('<span>', { id: 'wf-history-arrow' }).text(isOpen ? '[非表示]' : '[表示]').css({'color':'#36c','cursor':'pointer','font-size':'12px'});
$toggleLink.on('click', function() {
isOpen = $list.is(':hidden');
$list.toggle(isOpen);
$toggleLink.text(isOpen ? '[非表示]' : '[表示]');
localStorage.setItem(T_KEY, isOpen ? 'open' : 'close');
});
$hdr.append($titleLeft).append($toggleLink);
var $list = $('<div>',{id: 'wikifonts-history-list'}).css({'display':isOpen?'block':'none','max-height':'120px','overflow-y':'auto','margin-top':'5px'});
loadH().forEach(function(item) {
if (item.name === 'default') return;
var $row = $('<div>').css({'display':'flex','align-items':'center','justify-content':'space-between','padding':'4px 0','border-bottom':'1px solid rgba(0,0,0,0.1)'});
var $left = $('<div>').css({'display':'flex','align-items':'center','gap':'8px'});
$('<span>').text(item.starred?'⭐':'☆').css({'cursor':'pointer','font-size':'12px','color':item.starred?'#ffc107':'#000'})
.on('click', function(e) { e.stopPropagation();
var h = loadH(), i = h.find(function(f){return f.name===item.name});
if (i) { i.starred = !i.starred; localStorage.setItem(KEY, JSON.stringify(h)); ui(); }
}).appendTo($left);
var escapedName = item.name.replace(/"/g, '\\"');
var $nameLabel = $('<span>').text(item.name).css({
'font-family': '"' + escapedName + '", sans-serif',
'cursor': 'pointer',
'font-size': '13px',
'color': '#000'
}).on('mouseenter', function() { $(this).css({'color':'#0074D9','text-decoration':'underline'}); })
.on('mouseleave', function() { $(this).css({'color':'#000','text-decoration':'none'}); })
.on('click', function() {
$('#pureNameInput').val(item.name);
currentSelectedFontName = item.name;
newUploadedDataUrl = null;
getFont(item.name, function(r){ applyFont(item.name, r?r.dataUrl:null); });
});
// 各行ごとに確実に@font-faceを生成・適用(安全なCSS IDを使用)
getFont(item.name, function(r) {
if (r && r.dataUrl) {
var safeId = btoa(unescape(encodeURIComponent(item.name))).replace(/[^a-zA-Z0-9_-]/g, '_');
var listStyleId = 'testFontFace-' + safeId;
if (!$('#' + listStyleId).length) {
$('<style>', { id: listStyleId }).text('@font-face { font-family: "' + escapedName + '"; src: url("' + r.dataUrl + '"); }').appendTo('head');
}
}
});
$nameLabel.appendTo($left);
var $rightBtns = $('<div>').css({'display':'flex','align-items':'center','gap':'4px'});
// ✏️ 編集ボタン(名前変更用)
var $editBtn = $('<span>').text('✏️').css({'cursor':'pointer','font-size':'10px','padding':'2px','opacity':'0.6'})
.on('mouseenter', function() { $(this).css('opacity', '1'); })
.on('mouseleave', function() { $(this).css('opacity', '0.6'); })
.on('click', function(e) {
e.stopPropagation();
var newName = prompt('新しいフォント名を入力してください:', item.name);
if (!newName || !(newName = newName.trim())) return;
if (newName === item.name) return;
getFont(item.name, function(r) {
if (!r || !r.dataUrl) {
alert('フォントデータの取得に失敗しました。');
return;
}
openDB().done(function(db) {
var tx = db.transaction([STORE], 'readwrite');
var store = tx.objectStore(STORE);
store.delete(item.name); // 旧データの削除
store.put({ id: newName, dataUrl: r.dataUrl }); // 新名で登録
tx.oncomplete = function() {
// 古いスタイルのDOMを確実に削除
var oldSafeId = btoa(unescape(encodeURIComponent(item.name))).replace(/[^a-zA-Z0-9_-]/g, '_');
$('#testFontFace-' + oldSafeId).remove();
// 新しい名前用のスタイルを即座に登録
var newEscapedName = newName.replace(/"/g, '\\"');
var newSafeId = btoa(unescape(encodeURIComponent(newName))).replace(/[^a-zA-Z0-9_-]/g, '_');
var newStyleId = 'testFontFace-' + newSafeId;
if (!$('#' + newStyleId).length) {
$('<style>', { id: newStyleId }).text('@font-face { font-family: "' + newEscapedName + '"; src: url("' + r.dataUrl + '"); }').appendTo('head');
}
var h = loadH();
var target = h.find(function(f){return f.name===item.name});
var star = target ? target.starred : false;
h = h.filter(function(f){return f.name!==item.name});
h.unshift({name: newName, starred: star, time: Date.now()});
var s = h.filter(function(i){return i.starred && i.name!=='default'}),
u = h.filter(function(i){return !i.starred && i.name!=='default'});
h = s.sort(function(a,b){return b.time-a.time}).concat(u.sort(function(a,b){return b.time-a.time}));
h.push({name:'default',starred:false,time:0});
localStorage.setItem(KEY, JSON.stringify(h));
applyFont(newName, r.dataUrl);
ui();
};
});
});
});
// ❌ 削除ボタン
var $deleteBtn = $('<span>').text('❌').css({'cursor':'pointer','font-size':'10px','padding':'2px','opacity':'0.5'})
.on('mouseenter', function() { $(this).css('opacity', '1'); })
.on('mouseleave', function() { $(this).css('opacity', '0.5'); })
.on('click', function(e) { e.stopPropagation();
if (confirm('フォント「' + item.name + '」を削除しますか?')) {
var h = loadH().filter(function(f){return f.name!==item.name});
localStorage.setItem(KEY, JSON.stringify(h));
var safeId = btoa(unescape(encodeURIComponent(item.name))).replace(/[^a-zA-Z0-9_-]/g, '_');
$('#testFontFace-' + safeId).remove();
openDB().done(function(db){
db.transaction([STORE], 'readwrite').objectStore(STORE).delete(item.name);
}); ui();
}
});
$rightBtns.append($editBtn).append($deleteBtn);
$row.append($left).append($rightBtns).appendTo($list);
});
if (!$list.children().length) $('<div>').text('No history').css({'color':'#54595d','font-style':'italic','padding':'4px 0','font-size':'12px'}).appendTo($list);
$('#pureFontModal-btn-container').before($sec.append($hdr).append($list));
}
$(document).on('click', '#pt-font-setup', function() {
$('#pureFontModal').remove();
newUploadedDataUrl = null;
currentSelectedFontName = '';
var hLoad = loadH();
originalFontBeforeOpen = (hLoad.length && hLoad[0].name) ? hLoad[0].name : 'default';
var topName = (hLoad.length && hLoad[0].name !== 'default') ? hLoad[0].name : '';
var $modal = $('<div>', { id: 'pureFontModal' }).css({
'position': 'fixed', 'top': '20%', 'left': '50%', 'transform': 'translate(-50%, -20%)',
'background': '#fff', 'padding': '20px', 'border': '1px solid #72777d', 'border-radius': '4px',
'box-shadow': '0 2px 10px rgba(0,0,0,0.3)', 'z-index': '999999', 'width': '320px', 'color': '#000'
}).html(
'<h3 style="margin:0 0 15px 0; font-size:16px; text-align:left;">Font Uploader & History</h3>' +
'<label style="display:block; font-size:12px; margin-bottom:5px; text-align:left;">1. Select Font File (.ttf / .woff2 / .otf):</label>' +
'<input type="file" id="pureFileInput" accept=".ttf,.woff,.woff2,.otf" style="display:block; margin-bottom:15px; width:100%; font-size:12px;">' +
'<label style="display:block; font-size:12px; margin-bottom:5px; text-align:left;">2. Enter Font Name:</label>' +
'<input type="text" id="pureNameInput" style="display:block; margin-bottom:20px; width:100%; padding:4px; box-sizing:border-box;" value="' + topName + '">'
);
var $btnBox = $('<div>', { id: 'pureFontModal-btn-container' }).css({ 'display': 'flex', 'justify-content': 'space-between', 'align-items': 'center', 'margin-top': '15px' });
var $resetBtn = $('<button>', { id: 'pureResetBtn' }).text('default').css({ 'padding': '5px 10px', 'font-size': '12px', 'cursor': 'pointer', 'background': '#d33', 'color': '#fff', 'border': 'none', 'border-radius': '2px', 'font-weight': 'bold' });
$resetBtn.on('click', function(e) {
e.stopPropagation();
var h = loadH();
var idx = h.findIndex(function(i){return i.name==='default'});
if (idx !== -1) {
var dItem = h.splice(idx, 1)[0];
h.unshift(dItem);
}
localStorage.setItem(KEY, JSON.stringify(h));
applyFont('default');
$('#pureNameInput').val('');
newUploadedDataUrl = null;
currentSelectedFontName = 'default';
ui();
});
var $rightBtns = $('<div>').css({ 'display': 'flex', 'gap': '10px' });
var $cancel = $('<button>', { id: 'pureCloseBtn' }).text('Cancel').css({ 'padding': '5px 10px', 'font-size': '12px', 'cursor': 'pointer' });
var $save = $('<button>', { id: 'pureSaveBtn' }).text('Save').css({ 'padding': '5px 15px', 'font-size': '12px', 'cursor': 'pointer', 'background': '#36c', 'color': '#fff', 'border': 'none', 'border-radius': '2px' });
$rightBtns.append($cancel).append($save);
$btnBox.append($resetBtn).append($rightBtns);
$modal.append($btnBox).appendTo('body');
ui();
});
$(document).on('change', '#pureFileInput', function() {
if (this.files && this.files[0]) {
var file = this.files[0];
var r = new FileReader();
r.onload = function(e) {
newUploadedDataUrl = e.target.result;
currentSelectedFontName = '';
var fontName = $('#pureNameInput').val().trim();
if (!fontName) {
fontName = file.name.substring(0, file.name.lastIndexOf('.')) || 'TempFont';
$('#pureNameInput').val(fontName);
}
applyFont(fontName, newUploadedDataUrl);
};
r.readAsDataURL(file);
}
});
$(document).on('input', '#pureNameInput', function() {
var fontName = $(this).val().trim();
if (fontName && newUploadedDataUrl) {
applyFont(fontName, newUploadedDataUrl);
}
});
$(document).on('click', '#pureSaveBtn', function() {
var name = $('#pureNameInput').val().trim(); if (!name) return alert('フォント名を入力してください。');
try {
if (newUploadedDataUrl) {
openDB().done(function(db) {
var tx = db.transaction([STORE], 'readwrite');
tx.objectStore(STORE).put({ id: name, dataUrl: newUploadedDataUrl });
tx.oncomplete = function() {
updateH(name); alert('保存しました!'); $('#pureFontModal').remove(); newUploadedDataUrl = null;
};
tx.onerror = function() {
alert('⚠️ 【容量オーバー警告】IndexedDBへの書き込みに失敗しました。');
};
});
} else if (currentSelectedFontName) {
if (currentSelectedFontName === 'default') {
applyFont('default');
}
updateH(currentSelectedFontName); $('#pureFontModal').remove();
} else {
updateH(name); applyFont(name); $('#pureFontModal').remove();
}
} catch (error) {
alert('❌ エラーが発生したため保存を中断しました:\n' + error.message);
$('#pureFontModal').remove();
}
}).on('click', '#pureCloseBtn', function() {
$('#pureFontModal').remove();
newUploadedDataUrl = null;
currentSelectedFontName = '';
var targetFont = originalFontBeforeOpen;
if (targetFont && targetFont !== 'default') {
getFont(targetFont, function(r) { applyFont(targetFont, r ? r.dataUrl : null); });
} else {
applyFont('default');
}
});
});
})();
084npk8powfjm2dnwn5c7tits357m1a
769449
769448
2026-10-21T13:35:02Z
OUT is this
73801
769449
javascript
text/javascript
(function() {
'use strict';
if (!mw.config.get('wgUserId')) return;
var KEY = 'wikifonts_history_addon', T_KEY = 'wikifonts_history_toggle_state';
var DB = 'PureFontUploaderDB', STORE = 'fontStore';
var isOpen = localStorage.getItem(T_KEY) === 'open';
var newUploadedDataUrl = null;
var currentSelectedFontName = '';
var originalFontBeforeOpen = null;
function openDB() {
return $.Deferred(function(d) {
var r = indexedDB.open(DB, 1);
r.onupgradeneeded = function(e) { if (!e.target.result.objectStoreNames.contains(STORE)) e.target.result.createObjectStore(STORE, { keyPath: 'id' }); };
r.onsuccess = function(e) { d.resolve(e.target.result); };
}).promise();
}
function getFont(name, cb) {
openDB().done(function(db) {
db.transaction([STORE], 'readonly').objectStore(STORE).get(name).onsuccess = function(e) { cb(e.target.result); };
});
}
function loadH() {
try {
var h = JSON.parse(localStorage.getItem(KEY) || '[]');
if (!h.some(function(i){return i.name==='default'})) { h.push({name:'default',starred:false,time:0}); localStorage.setItem(KEY, JSON.stringify(h)); }
return h;
} catch(e) { return [{name:'default',starred:false,time:0}]; }
}
function applyFont(name, url) {
$('#testFontFace').remove();
if (name === 'default') {
document.body.style.removeProperty('font-family');
return;
}
if (url) $('<style>', { id: 'testFontFace' }).text('@font-face { font-family: "' + name.replace(/"/g, '\\"') + '"; src: url("' + url + '"); }').appendTo('head');
document.body.style.setProperty('font-family', '"' + name + '", sans-serif', 'important');
}
// 【同期】リロード時の自動復元
$(document).ready(function() {
var h = loadH();
if (h.length && h[0].name !== 'default') {
getFont(h[0].name, function(r) { if (r) applyFont(r.id, r.dataUrl); else applyFont(h[0].name); });
}
});
$(document).ready(function() {$('#pt-font-setup').remove();
var fontLinks = $(
['', '-sticky-header']
.map(function(suffix) {
var parentId = $('#p-user-menu-pages' + suffix).length ? 'p-user-menu-pages' : 'p-personal';
return mw.util.addPortletLink(
parentId + suffix,
'javascript:void(0);',
'Font Setup',
'pt-font-setup' + suffix,
'Open uploader',
null,
'#pt-logout' + suffix
);
})
.filter(function(elOrNull) { return elOrNull !== null; })
);
if (fontLinks.length > 0) {
const style = `
li[id^="pt-font-setup"] a {
width: auto !important;
padding-left: 25px;
white-space: nowrap;
height: 20px;
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"><path d="M19.43 12.98c.04-.32.07-.64.07-.98 0-.34-.03-.66-.07-.98l2.11-1.65c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.3-.61-.22l-2.49 1c-.52-.4-1.08-.73-1.69-.98l-.38-2.65C14.46 2.18 14.25 2 14 2h-4c-.25 0-.46.18-.49.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1c-.23-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64l2.11 1.65c-.04.32-.07.65-.07.98 0 .33.03.66.07.98l-2.11 1.65c-.19.15-.24.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65c.03.24.24.42.49.42h4c.25 0 .46-.18.49-.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1c.23.09.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.65zM12 15.5c-1.93 0-3.5-1.57-3.5-3.5s1.57-3.5 3.5-3.5 3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5z" fill="%23202122"/><text x="12" y="14.5" font-family="sans-serif" font-size="8.5" font-weight="bold" text-anchor="middle" fill="%23fff" stroke="%23fff" stroke-width="3" stroke-linejoin="round">Aa</text><text x="12" y="14.5" font-family="sans-serif" font-size="8.5" font-weight="bold" text-anchor="middle" fill="%23000">Aa</text></svg>');
background-repeat: no-repeat;
background-size: 20px 20px;
background-position: center left;
}
`;
$('<style>').text(style).appendTo('head');
}
});
function updateH(font) {
if (!font) return;
var h = loadH(), idx = h.findIndex(function(i){return i.name===font}), star = idx !== -1 ? h[idx].starred : false;
if (idx !== -1) h.splice(idx, 1);
h.unshift({name: font, starred: star, time: Date.now()});
var s = h.filter(function(i){return i.starred && i.name!=='default'}),
u = h.filter(function(i){return !i.starred && i.name!=='default'});
h = s.sort(function(a,b){return b.time-a.time}).concat(u.sort(function(a,b){return b.time-a.time}));
h.push({name:'default',starred:false,time:0});
try {
localStorage.setItem(KEY, JSON.stringify(h));
} catch (e) {
alert('⚠️ 【容量警告】LocalStorageの容量が不足しているため履歴の保存に失敗しました。');
}
ui();
}
function ui() {
var $box = $('#pureFontModal'); if (!$box.length) return;
$('#wikifonts-history-section').remove();
var $sec = $('<div>',{id: 'wikifonts-history-section'}).css({'margin-top':'15px','padding-top':'10px','border-top':'1px solid #72777d','text-align':'left'});
var $hdr = $('<div>').css({'display':'flex','justify-content':'space-between','align-items':'center','padding':'4px 0','color':'#000','font-size':'13px'});
var $titleLeft = $('<span>').text('History').css({'color':'#000','font-weight':'bold'});
var $toggleLink = $('<span>', { id: 'wf-history-arrow' }).text(isOpen ? '[非表示]' : '[表示]').css({'color':'#36c','cursor':'pointer','font-size':'12px'});
$toggleLink.on('click', function() {
isOpen = $list.is(':hidden');
$list.toggle(isOpen);
$toggleLink.text(isOpen ? '[非表示]' : '[表示]');
localStorage.setItem(T_KEY, isOpen ? 'open' : 'close');
});
$hdr.append($titleLeft).append($toggleLink);
var $list = $('<div>',{id: 'wikifonts-history-list'}).css({'display':isOpen?'block':'none','max-height':'120px','overflow-y':'auto','margin-top':'5px'});
loadH().forEach(function(item) {
if (item.name === 'default') return;
var $row = $('<div>').css({'display':'flex','align-items':'center','justify-content':'space-between','padding':'4px 0','border-bottom':'1px solid rgba(0,0,0,0.1)'});
var $left = $('<div>').css({'display':'flex','align-items':'center','gap':'8px'});
$('<span>').text(item.starred?'⭐':'☆').css({'cursor':'pointer','font-size':'12px','color':item.starred?'#ffc107':'#000'})
.on('click', function(e) { e.stopPropagation();
var h = loadH(), i = h.find(function(f){return f.name===item.name});
if (i) { i.starred = !i.starred; localStorage.setItem(KEY, JSON.stringify(h)); ui(); }
}).appendTo($left);
var escapedName = item.name.replace(/"/g, '\\"');
var $nameLabel = $('<span>').text(item.name).css({
'font-family': '"' + escapedName + '", sans-serif',
'cursor': 'pointer',
'font-size': '13px',
'color': '#000'
}).on('mouseenter', function() { $(this).css({'color':'#0074D9','text-decoration':'underline'}); })
.on('mouseleave', function() { $(this).css({'color':'#000','text-decoration':'none'}); })
.on('click', function() {
$('#pureNameInput').val(item.name);
currentSelectedFontName = item.name;
newUploadedDataUrl = null;
getFont(item.name, function(r){ applyFont(item.name, r?r.dataUrl:null); });
});
// 各行ごとに確実に@font-faceを生成・適用(安全なCSS IDを使用)
getFont(item.name, function(r) {
if (r && r.dataUrl) {
var safeId = btoa(unescape(encodeURIComponent(item.name))).replace(/[^a-zA-Z0-9_-]/g, '_');
var listStyleId = 'testFontFace-' + safeId;
if (!$('#' + listStyleId).length) {
$('<style>', { id: listStyleId }).text('@font-face { font-family: "' + escapedName + '"; src: url("' + r.dataUrl + '"); }').appendTo('head');
}
}
});
$nameLabel.appendTo($left);
var $rightBtns = $('<div>').css({'display':'flex','align-items':'center','gap':'4px'});
// ✏️ 編集ボタン(名前変更用)
var $editBtn = $('<span>').text('✏️').css({'cursor':'pointer','font-size':'10px','padding':'2px','opacity':'0.6'})
.on('mouseenter', function() { $(this).css('opacity', '1'); })
.on('mouseleave', function() { $(this).css('opacity', '0.6'); })
.on('click', function(e) {
e.stopPropagation();
var newName = prompt('新しいフォント名を入力してください:', item.name);
if (!newName || !(newName = newName.trim())) return;
if (newName === item.name) return;
getFont(item.name, function(r) {
if (!r || !r.dataUrl) {
alert('フォントデータの取得に失敗しました。');
return;
}
openDB().done(function(db) {
var tx = db.transaction([STORE], 'readwrite');
var store = tx.objectStore(STORE);
store.delete(item.name); // 旧データの削除
store.put({ id: newName, dataUrl: r.dataUrl }); // 新名で登録
tx.oncomplete = function() {
// 古いスタイルのDOMを確実に削除
var oldSafeId = btoa(unescape(encodeURIComponent(item.name))).replace(/[^a-zA-Z0-9_-]/g, '_');
$('#testFontFace-' + oldSafeId).remove();
// 新しい名前用のスタイルを即座に登録
var newEscapedName = newName.replace(/"/g, '\\"');
var newSafeId = btoa(unescape(encodeURIComponent(newName))).replace(/[^a-zA-Z0-9_-]/g, '_');
var newStyleId = 'testFontFace-' + newSafeId;
if (!$('#' + newStyleId).length) {
$('<style>', { id: newStyleId }).text('@font-face { font-family: "' + newEscapedName + '"; src: url("' + r.dataUrl + '"); }').appendTo('head');
}
var h = loadH();
var target = h.find(function(f){return f.name===item.name});
var star = target ? target.starred : false;
h = h.filter(function(f){return f.name!==item.name});
h.unshift({name: newName, starred: star, time: Date.now()});
var s = h.filter(function(i){return i.starred && i.name!=='default'}),
u = h.filter(function(i){return !i.starred && i.name!=='default'});
h = s.sort(function(a,b){return b.time-a.time}).concat(u.sort(function(a,b){return b.time-a.time}));
h.push({name:'default',starred:false,time:0});
localStorage.setItem(KEY, JSON.stringify(h));
applyFont(newName, r.dataUrl);
ui();
};
});
});
});
// ❌ 削除ボタン
var $deleteBtn = $('<span>').text('❌').css({'cursor':'pointer','font-size':'10px','padding':'2px','opacity':'0.5'})
.on('mouseenter', function() { $(this).css('opacity', '1'); })
.on('mouseleave', function() { $(this).css('opacity', '0.5'); })
.on('click', function(e) { e.stopPropagation();
if (confirm('フォント「' + item.name + '」を削除しますか?')) {
var h = loadH().filter(function(f){return f.name!==item.name});
localStorage.setItem(KEY, JSON.stringify(h));
var safeId = btoa(unescape(encodeURIComponent(item.name))).replace(/[^a-zA-Z0-9_-]/g, '_');
$('#testFontFace-' + safeId).remove();
openDB().done(function(db){
db.transaction([STORE], 'readwrite').objectStore(STORE).delete(item.name);
}); ui();
}
});
$rightBtns.append($editBtn).append($deleteBtn);
$row.append($left).append($rightBtns).appendTo($list);
});
if (!$list.children().length) $('<div>').text('No history').css({'color':'#54595d','font-style':'italic','padding':'4px 0','font-size':'12px'}).appendTo($list);
$('#pureFontModal-btn-container').before($sec.append($hdr).append($list));
}
$(document).on('click', '#pt-font-setup', function() {
$('#pureFontModal').remove();
newUploadedDataUrl = null;
currentSelectedFontName = '';
var hLoad = loadH();
originalFontBeforeOpen = (hLoad.length && hLoad[0].name) ? hLoad[0].name : 'default';
var topName = (hLoad.length && hLoad[0].name !== 'default') ? hLoad[0].name : '';
var $modal = $('<div>', { id: 'pureFontModal' }).css({
'position': 'fixed', 'top': '20%', 'left': '50%', 'transform': 'translate(-50%, -20%)',
'background': '#fff', 'padding': '20px', 'border': '1px solid #72777d', 'border-radius': '4px',
'box-shadow': '0 2px 10px rgba(0,0,0,0.3)', 'z-index': '999999', 'width': '320px', 'color': '#000'
}).html(
'<h3 style="margin:0 0 15px 0; font-size:16px; text-align:left;">Font Uploader & History</h3>' +
'<label style="display:block; font-size:12px; margin-bottom:5px; text-align:left;">1. Select Font File (.ttf / .woff2 / .otf):</label>' +
'<input type="file" id="pureFileInput" accept=".ttf,.woff,.woff2,.otf" style="display:block; margin-bottom:15px; width:100%; font-size:12px;">' +
'<label style="display:block; font-size:12px; margin-bottom:5px; text-align:left;">2. Enter Font Name:</label>' +
'<input type="text" id="pureNameInput" style="display:block; margin-bottom:20px; width:100%; padding:4px; box-sizing:border-box;" value="' + topName + '">'
);
var $btnBox = $('<div>', { id: 'pureFontModal-btn-container' }).css({ 'display': 'flex', 'justify-content': 'space-between', 'align-items': 'center', 'margin-top': '15px' });
var $resetBtn = $('<button>', { id: 'pureResetBtn' }).text('default').css({ 'padding': '5px 10px', 'font-size': '12px', 'cursor': 'pointer', 'background': '#d33', 'color': '#fff', 'border': 'none', 'border-radius': '2px', 'font-weight': 'bold' });
$resetBtn.on('click', function(e) {
e.stopPropagation();
var h = loadH();
var idx = h.findIndex(function(i){return i.name==='default'});
if (idx !== -1) {
var dItem = h.splice(idx, 1)[0];
h.unshift(dItem);
}
localStorage.setItem(KEY, JSON.stringify(h));
applyFont('default');
$('#pureNameInput').val('');
newUploadedDataUrl = null;
currentSelectedFontName = 'default';
ui();
});
var $rightBtns = $('<div>').css({ 'display': 'flex', 'gap': '10px' });
var $cancel = $('<button>', { id: 'pureCloseBtn' }).text('Cancel').css({ 'padding': '5px 10px', 'font-size': '12px', 'cursor': 'pointer' });
var $save = $('<button>', { id: 'pureSaveBtn' }).text('Save').css({ 'padding': '5px 15px', 'font-size': '12px', 'cursor': 'pointer', 'background': '#36c', 'color': '#fff', 'border': 'none', 'border-radius': '2px' });
$rightBtns.append($cancel).append($save);
$btnBox.append($resetBtn).append($rightBtns);
$modal.append($btnBox).appendTo('body');
ui();
});
$(document).on('change', '#pureFileInput', function() {
if (this.files && this.files[0]) {
var file = this.files[0];
var r = new FileReader();
r.onload = function(e) {
newUploadedDataUrl = e.target.result;
currentSelectedFontName = '';
var fontName = $('#pureNameInput').val().trim();
if (!fontName) {
fontName = file.name.substring(0, file.name.lastIndexOf('.')) || 'TempFont';
$('#pureNameInput').val(fontName);
}
applyFont(fontName, newUploadedDataUrl);
};
r.readAsDataURL(file);
}
});
$(document).on('input', '#pureNameInput', function() {
var fontName = $(this).val().trim();
if (fontName && newUploadedDataUrl) {
applyFont(fontName, newUploadedDataUrl);
}
});
$(document).on('click', '#pureSaveBtn', function() {
var name = $('#pureNameInput').val().trim(); if (!name) return alert('フォント名を入力してください。');
try {
if (newUploadedDataUrl) {
openDB().done(function(db) {
var tx = db.transaction([STORE], 'readwrite');
tx.objectStore(STORE).put({ id: name, dataUrl: newUploadedDataUrl });
tx.oncomplete = function() {
updateH(name); alert('保存しました!'); $('#pureFontModal').remove(); newUploadedDataUrl = null;
};
tx.onerror = function() {
alert('⚠️ 【容量オーバー警告】IndexedDBへの書き込みに失敗しました。');
};
});
} else if (currentSelectedFontName) {
if (currentSelectedFontName === 'default') {
applyFont('default');
}
updateH(currentSelectedFontName); $('#pureFontModal').remove();
} else {
updateH(name); applyFont(name); $('#pureFontModal').remove();
}
} catch (error) {
alert('❌ エラーが発生したため保存を中断しました:\n' + error.message);
$('#pureFontModal').remove();
}
}).on('click', '#pureCloseBtn', function() {
$('#pureFontModal').remove();
newUploadedDataUrl = null;
currentSelectedFontName = '';
var targetFont = originalFontBeforeOpen;
if (targetFont && targetFont !== 'default') {
getFont(targetFont, function(r) { applyFont(targetFont, r ? r.dataUrl : null); });
} else {
applyFont('default');
}
});
});
83ijv9yun0nb3tjbshe3450ccmre7hk
769450
769449
2026-10-21T13:35:39Z
OUT is this
73801
Undid revision [[Special:Diff/769449|769449]] by [[Special:Contributions/OUT is this|OUT is this]] ([[User talk:OUT is this|talk]])
769450
javascript
text/javascript
(function() {
'use strict';
if (!mw.config.get('wgUserId')) return;
var KEY = 'wikifonts_history_addon', T_KEY = 'wikifonts_history_toggle_state';
var DB = 'PureFontUploaderDB', STORE = 'fontStore';
var isOpen = localStorage.getItem(T_KEY) === 'open';
var newUploadedDataUrl = null;
var currentSelectedFontName = '';
var originalFontBeforeOpen = null;
function openDB() {
return $.Deferred(function(d) {
var r = indexedDB.open(DB, 1);
r.onupgradeneeded = function(e) { if (!e.target.result.objectStoreNames.contains(STORE)) e.target.result.createObjectStore(STORE, { keyPath: 'id' }); };
r.onsuccess = function(e) { d.resolve(e.target.result); };
}).promise();
}
function getFont(name, cb) {
openDB().done(function(db) {
db.transaction([STORE], 'readonly').objectStore(STORE).get(name).onsuccess = function(e) { cb(e.target.result); };
});
}
function loadH() {
try {
var h = JSON.parse(localStorage.getItem(KEY) || '[]');
if (!h.some(function(i){return i.name==='default'})) { h.push({name:'default',starred:false,time:0}); localStorage.setItem(KEY, JSON.stringify(h)); }
return h;
} catch(e) { return [{name:'default',starred:false,time:0}]; }
}
function applyFont(name, url) {
$('#testFontFace').remove();
if (name === 'default') {
document.body.style.removeProperty('font-family');
return;
}
if (url) $('<style>', { id: 'testFontFace' }).text('@font-face { font-family: "' + name.replace(/"/g, '\\"') + '"; src: url("' + url + '"); }').appendTo('head');
document.body.style.setProperty('font-family', '"' + name + '", sans-serif', 'important');
}
// 【同期】リロード時の自動復元
$(document).ready(function() {
var h = loadH();
if (h.length && h[0].name !== 'default') {
getFont(h[0].name, function(r) { if (r) applyFont(r.id, r.dataUrl); else applyFont(h[0].name); });
}
});
$(document).ready(function() {
$('#pt-font-setup').remove();
// sm link
var svgIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" style="vertical-align: middle; fill: currentColor; margin-right: 4px;">' +
'<path d="M19.43 12.98c.04-.32.07-.64.07-.98 0-.34-.03-.66-.07-.98l2.11-1.65c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.3-.61-.22l-2.49 1c-.52-.4-1.08-.73-1.69-.98l-.38-2.65C14.46 2.18 14.25 2 14 2h-4c-.25 0-.46.18-.49.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1c-.23-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64l2.11 1.65c-.04.32-.07.65-.07.98 0 .33.03.66.07.98l-2.11 1.65c-.19.15-.24.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65c.03.24.24.42.49.42h4c.25 0 .46-.18.49-.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1c.23.09.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.65zM12 15.5c-1.93 0-3.5-1.57-3.5-3.5s1.57-3.5 3.5-3.5 3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5z"/>' +
'<text x="12" y="14.5" font-family="sans-serif" font-size="8.5" font-weight="bold" text-anchor="middle" fill="#fff" stroke="#fff" stroke-width="2" stroke-linejoin="round">Aa</text>' +
'<text x="12" y="14.5" font-family="sans-serif" font-size="8.5" font-weight="bold" text-anchor="middle" fill="#000">Aa</text>' +
'</svg>';
mw.util.addPortletLink('p-personal', 'javascript:void(0);', '', 'pt-font-setup', 'Open uploader', null, '#pt-logout');
$('#pt-font-setup a').html(svgIcon + 'Font Setup');
function updateH(font) {
if (!font) return;
var h = loadH(), idx = h.findIndex(function(i){return i.name===font}), star = idx !== -1 ? h[idx].starred : false;
if (idx !== -1) h.splice(idx, 1);
h.unshift({name: font, starred: star, time: Date.now()});
var s = h.filter(function(i){return i.starred && i.name!=='default'}),
u = h.filter(function(i){return !i.starred && i.name!=='default'});
h = s.sort(function(a,b){return b.time-a.time}).concat(u.sort(function(a,b){return b.time-a.time}));
h.push({name:'default',starred:false,time:0});
try {
localStorage.setItem(KEY, JSON.stringify(h));
} catch (e) {
alert('⚠️ 【容量警告】LocalStorageの容量が不足しているため履歴の保存に失敗しました。');
}
ui();
}
function ui() {
var $box = $('#pureFontModal'); if (!$box.length) return;
$('#wikifonts-history-section').remove();
var $sec = $('<div>',{id: 'wikifonts-history-section'}).css({'margin-top':'15px','padding-top':'10px','border-top':'1px solid #72777d','text-align':'left'});
var $hdr = $('<div>').css({'display':'flex','justify-content':'space-between','align-items':'center','padding':'4px 0','color':'#000','font-size':'13px'});
var $titleLeft = $('<span>').text('History').css({'color':'#000','font-weight':'bold'});
var $toggleLink = $('<span>', { id: 'wf-history-arrow' }).text(isOpen ? '[非表示]' : '[表示]').css({'color':'#36c','cursor':'pointer','font-size':'12px'});
$toggleLink.on('click', function() {
isOpen = $list.is(':hidden');
$list.toggle(isOpen);
$toggleLink.text(isOpen ? '[非表示]' : '[表示]');
localStorage.setItem(T_KEY, isOpen ? 'open' : 'close');
});
$hdr.append($titleLeft).append($toggleLink);
var $list = $('<div>',{id: 'wikifonts-history-list'}).css({'display':isOpen?'block':'none','max-height':'120px','overflow-y':'auto','margin-top':'5px'});
loadH().forEach(function(item) {
if (item.name === 'default') return;
var $row = $('<div>').css({'display':'flex','align-items':'center','justify-content':'space-between','padding':'4px 0','border-bottom':'1px solid rgba(0,0,0,0.1)'});
var $left = $('<div>').css({'display':'flex','align-items':'center','gap':'8px'});
$('<span>').text(item.starred?'⭐':'☆').css({'cursor':'pointer','font-size':'12px','color':item.starred?'#ffc107':'#000'})
.on('click', function(e) { e.stopPropagation();
var h = loadH(), i = h.find(function(f){return f.name===item.name});
if (i) { i.starred = !i.starred; localStorage.setItem(KEY, JSON.stringify(h)); ui(); }
}).appendTo($left);
var escapedName = item.name.replace(/"/g, '\\"');
var $nameLabel = $('<span>').text(item.name).css({
'font-family': '"' + escapedName + '", sans-serif',
'cursor': 'pointer',
'font-size': '13px',
'color': '#000'
}).on('mouseenter', function() { $(this).css({'color':'#0074D9','text-decoration':'underline'}); })
.on('mouseleave', function() { $(this).css({'color':'#000','text-decoration':'none'}); })
.on('click', function() {
$('#pureNameInput').val(item.name);
currentSelectedFontName = item.name;
newUploadedDataUrl = null;
getFont(item.name, function(r){ applyFont(item.name, r?r.dataUrl:null); });
});
// 各行ごとに確実に@font-faceを生成・適用(安全なCSS IDを使用)
getFont(item.name, function(r) {
if (r && r.dataUrl) {
var safeId = btoa(unescape(encodeURIComponent(item.name))).replace(/[^a-zA-Z0-9_-]/g, '_');
var listStyleId = 'testFontFace-' + safeId;
if (!$('#' + listStyleId).length) {
$('<style>', { id: listStyleId }).text('@font-face { font-family: "' + escapedName + '"; src: url("' + r.dataUrl + '"); }').appendTo('head');
}
}
});
$nameLabel.appendTo($left);
var $rightBtns = $('<div>').css({'display':'flex','align-items':'center','gap':'4px'});
// ✏️ 編集ボタン(名前変更用)
var $editBtn = $('<span>').text('✏️').css({'cursor':'pointer','font-size':'10px','padding':'2px','opacity':'0.6'})
.on('mouseenter', function() { $(this).css('opacity', '1'); })
.on('mouseleave', function() { $(this).css('opacity', '0.6'); })
.on('click', function(e) {
e.stopPropagation();
var newName = prompt('新しいフォント名を入力してください:', item.name);
if (!newName || !(newName = newName.trim())) return;
if (newName === item.name) return;
getFont(item.name, function(r) {
if (!r || !r.dataUrl) {
alert('フォントデータの取得に失敗しました。');
return;
}
openDB().done(function(db) {
var tx = db.transaction([STORE], 'readwrite');
var store = tx.objectStore(STORE);
store.delete(item.name); // 旧データの削除
store.put({ id: newName, dataUrl: r.dataUrl }); // 新名で登録
tx.oncomplete = function() {
// 古いスタイルのDOMを確実に削除
var oldSafeId = btoa(unescape(encodeURIComponent(item.name))).replace(/[^a-zA-Z0-9_-]/g, '_');
$('#testFontFace-' + oldSafeId).remove();
// 新しい名前用のスタイルを即座に登録
var newEscapedName = newName.replace(/"/g, '\\"');
var newSafeId = btoa(unescape(encodeURIComponent(newName))).replace(/[^a-zA-Z0-9_-]/g, '_');
var newStyleId = 'testFontFace-' + newSafeId;
if (!$('#' + newStyleId).length) {
$('<style>', { id: newStyleId }).text('@font-face { font-family: "' + newEscapedName + '"; src: url("' + r.dataUrl + '"); }').appendTo('head');
}
var h = loadH();
var target = h.find(function(f){return f.name===item.name});
var star = target ? target.starred : false;
h = h.filter(function(f){return f.name!==item.name});
h.unshift({name: newName, starred: star, time: Date.now()});
var s = h.filter(function(i){return i.starred && i.name!=='default'}),
u = h.filter(function(i){return !i.starred && i.name!=='default'});
h = s.sort(function(a,b){return b.time-a.time}).concat(u.sort(function(a,b){return b.time-a.time}));
h.push({name:'default',starred:false,time:0});
localStorage.setItem(KEY, JSON.stringify(h));
applyFont(newName, r.dataUrl);
ui();
};
});
});
});
// ❌ 削除ボタン
var $deleteBtn = $('<span>').text('❌').css({'cursor':'pointer','font-size':'10px','padding':'2px','opacity':'0.5'})
.on('mouseenter', function() { $(this).css('opacity', '1'); })
.on('mouseleave', function() { $(this).css('opacity', '0.5'); })
.on('click', function(e) { e.stopPropagation();
if (confirm('フォント「' + item.name + '」を削除しますか?')) {
var h = loadH().filter(function(f){return f.name!==item.name});
localStorage.setItem(KEY, JSON.stringify(h));
var safeId = btoa(unescape(encodeURIComponent(item.name))).replace(/[^a-zA-Z0-9_-]/g, '_');
$('#testFontFace-' + safeId).remove();
openDB().done(function(db){
db.transaction([STORE], 'readwrite').objectStore(STORE).delete(item.name);
}); ui();
}
});
$rightBtns.append($editBtn).append($deleteBtn);
$row.append($left).append($rightBtns).appendTo($list);
});
if (!$list.children().length) $('<div>').text('No history').css({'color':'#54595d','font-style':'italic','padding':'4px 0','font-size':'12px'}).appendTo($list);
$('#pureFontModal-btn-container').before($sec.append($hdr).append($list));
}
$(document).on('click', '#pt-font-setup', function() {
$('#pureFontModal').remove();
newUploadedDataUrl = null;
currentSelectedFontName = '';
var hLoad = loadH();
originalFontBeforeOpen = (hLoad.length && hLoad[0].name) ? hLoad[0].name : 'default';
var topName = (hLoad.length && hLoad[0].name !== 'default') ? hLoad[0].name : '';
var $modal = $('<div>', { id: 'pureFontModal' }).css({
'position': 'fixed', 'top': '20%', 'left': '50%', 'transform': 'translate(-50%, -20%)',
'background': '#fff', 'padding': '20px', 'border': '1px solid #72777d', 'border-radius': '4px',
'box-shadow': '0 2px 10px rgba(0,0,0,0.3)', 'z-index': '999999', 'width': '320px', 'color': '#000'
}).html(
'<h3 style="margin:0 0 15px 0; font-size:16px; text-align:left;">Font Uploader & History</h3>' +
'<label style="display:block; font-size:12px; margin-bottom:5px; text-align:left;">1. Select Font File (.ttf / .woff2 / .otf):</label>' +
'<input type="file" id="pureFileInput" accept=".ttf,.woff,.woff2,.otf" style="display:block; margin-bottom:15px; width:100%; font-size:12px;">' +
'<label style="display:block; font-size:12px; margin-bottom:5px; text-align:left;">2. Enter Font Name:</label>' +
'<input type="text" id="pureNameInput" style="display:block; margin-bottom:20px; width:100%; padding:4px; box-sizing:border-box;" value="' + topName + '">'
);
var $btnBox = $('<div>', { id: 'pureFontModal-btn-container' }).css({ 'display': 'flex', 'justify-content': 'space-between', 'align-items': 'center', 'margin-top': '15px' });
var $resetBtn = $('<button>', { id: 'pureResetBtn' }).text('default').css({ 'padding': '5px 10px', 'font-size': '12px', 'cursor': 'pointer', 'background': '#d33', 'color': '#fff', 'border': 'none', 'border-radius': '2px', 'font-weight': 'bold' });
$resetBtn.on('click', function(e) {
e.stopPropagation();
var h = loadH();
var idx = h.findIndex(function(i){return i.name==='default'});
if (idx !== -1) {
var dItem = h.splice(idx, 1)[0];
h.unshift(dItem);
}
localStorage.setItem(KEY, JSON.stringify(h));
applyFont('default');
$('#pureNameInput').val('');
newUploadedDataUrl = null;
currentSelectedFontName = 'default';
ui();
});
var $rightBtns = $('<div>').css({ 'display': 'flex', 'gap': '10px' });
var $cancel = $('<button>', { id: 'pureCloseBtn' }).text('Cancel').css({ 'padding': '5px 10px', 'font-size': '12px', 'cursor': 'pointer' });
var $save = $('<button>', { id: 'pureSaveBtn' }).text('Save').css({ 'padding': '5px 15px', 'font-size': '12px', 'cursor': 'pointer', 'background': '#36c', 'color': '#fff', 'border': 'none', 'border-radius': '2px' });
$rightBtns.append($cancel).append($save);
$btnBox.append($resetBtn).append($rightBtns);
$modal.append($btnBox).appendTo('body');
ui();
});
$(document).on('change', '#pureFileInput', function() {
if (this.files && this.files[0]) {
var file = this.files[0];
var r = new FileReader();
r.onload = function(e) {
newUploadedDataUrl = e.target.result;
currentSelectedFontName = '';
var fontName = $('#pureNameInput').val().trim();
if (!fontName) {
fontName = file.name.substring(0, file.name.lastIndexOf('.')) || 'TempFont';
$('#pureNameInput').val(fontName);
}
applyFont(fontName, newUploadedDataUrl);
};
r.readAsDataURL(file);
}
});
$(document).on('input', '#pureNameInput', function() {
var fontName = $(this).val().trim();
if (fontName && newUploadedDataUrl) {
applyFont(fontName, newUploadedDataUrl);
}
});
$(document).on('click', '#pureSaveBtn', function() {
var name = $('#pureNameInput').val().trim(); if (!name) return alert('フォント名を入力してください。');
try {
if (newUploadedDataUrl) {
openDB().done(function(db) {
var tx = db.transaction([STORE], 'readwrite');
tx.objectStore(STORE).put({ id: name, dataUrl: newUploadedDataUrl });
tx.oncomplete = function() {
updateH(name); alert('保存しました!'); $('#pureFontModal').remove(); newUploadedDataUrl = null;
};
tx.onerror = function() {
alert('⚠️ 【容量オーバー警告】IndexedDBへの書き込みに失敗しました。');
};
});
} else if (currentSelectedFontName) {
if (currentSelectedFontName === 'default') {
applyFont('default');
}
updateH(currentSelectedFontName); $('#pureFontModal').remove();
} else {
updateH(name); applyFont(name); $('#pureFontModal').remove();
}
} catch (error) {
alert('❌ エラーが発生したため保存を中断しました:\n' + error.message);
$('#pureFontModal').remove();
}
}).on('click', '#pureCloseBtn', function() {
$('#pureFontModal').remove();
newUploadedDataUrl = null;
currentSelectedFontName = '';
var targetFont = originalFontBeforeOpen;
if (targetFont && targetFont !== 'default') {
getFont(targetFont, function(r) { applyFont(targetFont, r ? r.dataUrl : null); });
} else {
applyFont('default');
}
});
});
})();
084npk8powfjm2dnwn5c7tits357m1a
769451
769450
2026-10-21T13:43:30Z
OUT is this
73801
769451
javascript
text/javascript
(function() {
'use strict';
if (!mw.config.get('wgUserId')) return;
var KEY = 'wikifonts_history_addon', T_KEY = 'wikifonts_history_toggle_state';
var DB = 'PureFontUploaderDB', STORE = 'fontStore';
var isOpen = localStorage.getItem(T_KEY) === 'open';
var newUploadedDataUrl = null;
var currentSelectedFontName = '';
var originalFontBeforeOpen = null;
function openDB() {
return $.Deferred(function(d) {
var r = indexedDB.open(DB, 1);
r.onupgradeneeded = function(e) { if (!e.target.result.objectStoreNames.contains(STORE)) e.target.result.createObjectStore(STORE, { keyPath: 'id' }); };
r.onsuccess = function(e) { d.resolve(e.target.result); };
}).promise();
}
function getFont(name, cb) {
openDB().done(function(db) {
db.transaction([STORE], 'readonly').objectStore(STORE).get(name).onsuccess = function(e) { cb(e.target.result); };
});
}
function loadH() {
try {
var h = JSON.parse(localStorage.getItem(KEY) || '[]');
if (!h.some(function(i){return i.name==='default'})) { h.push({name:'default',starred:false,time:0}); localStorage.setItem(KEY, JSON.stringify(h)); }
return h;
} catch(e) { return [{name:'default',starred:false,time:0}]; }
}
function applyFont(name, url) {
$('#testFontFace').remove();
if (name === 'default') {
document.body.style.removeProperty('font-family');
return;
}
if (url) $('<style>', { id: 'testFontFace' }).text('@font-face { font-family: "' + name.replace(/"/g, '\\"') + '"; src: url("' + url + '"); }').appendTo('head');
document.body.style.setProperty('font-family', '"' + name + '", sans-serif', 'important');
}
// 【同期】リロード時の自動復元
$(document).ready(function() {
var h = loadH();
if (h.length && h[0].name !== 'default') {
getFont(h[0].name, function(r) { if (r) applyFont(r.id, r.dataUrl); else applyFont(h[0].name); });
}
});
$(document).ready(function() {
$('#pt-font-setup').remove();
// 設定マーク(歯車)のなかに「Aa」が書かれ、重なる部分を白抜きにしたSVGアイコン(サイズ・色調整済み)
var svgIcon = '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" style="vertical-align: middle; fill: %23202122; margin-right: 4px;">' +
'<path d="M16.19 10.81c.03-.26.06-.53.06-.81 0-.28-.03-.55-.06-.81l1.75-1.37c.16-.12.2-.35.1-.53l-1.66-2.88c-.1-.18-.32-.25-.51-.18l-2.07.83c-.43-.33-.9-.6-1.4-.81l-.32-2.2c-.03-.2-.19-.35-.4-.35h-3.32c-.21 0-.37.15-.4.35l-.32 2.2c-.5.21-.97.48-1.4.81l-2.07-.83c-.19-.07-.41 0-.51.18l-1.66 2.88c-.1.18-.06.41.1.53l1.75 1.37c-.03.26-.06.54-.06.81 0 .27.03.55.06.81l-1.75 1.37c-.16.12-.2.35-.1.53l1.66 2.88c.1.18.32.25.51.18l2.07-.83c.43.33.9.6 1.4.81l.32 2.2c.03.2.19.35.4.35h3.32c.21 0 .37-.15.4-.35l.32-2.2c.5-.21.97-.48 1.4-.81l2.07.83c.19.07.41 0 .51-.18l1.66-2.88c.1-.18.06-.41-.1-.53l-1.75-1.37zM10 13c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3z"/>' +
'<text x="10" y="12.5" font-family="sans-serif" font-size="7.5" font-weight="bold" text-anchor="middle" fill="#fff" stroke="#fff" stroke-width="2.5" stroke-linejoin="round">Aa</text>' +
'<text x="10" y="12.5" font-family="sans-serif" font-size="7.5" font-weight="bold" text-anchor="middle" fill="#202122">Aa</text>' +
'</svg>';
mw.util.addPortletLink('p-personal', 'javascript:void(0);', '', 'pt-font-setup', 'Open uploader', null, '#pt-logout');
$('#pt-font-setup a').html(svgIcon + 'Font Setup');
});
function updateH(font) {
if (!font) return;
var h = loadH(), idx = h.findIndex(function(i){return i.name===font}), star = idx !== -1 ? h[idx].starred : false;
if (idx !== -1) h.splice(idx, 1);
h.unshift({name: font, starred: star, time: Date.now()});
var s = h.filter(function(i){return i.starred && i.name!=='default'}),
u = h.filter(function(i){return !i.starred && i.name!=='default'});
h = s.sort(function(a,b){return b.time-a.time}).concat(u.sort(function(a,b){return b.time-a.time}));
h.push({name:'default',starred:false,time:0});
try {
localStorage.setItem(KEY, JSON.stringify(h));
} catch (e) {
alert('⚠️ 【容量警告】LocalStorageの容量が不足しているため履歴の保存に失敗しました。');
}
ui();
}
function ui() {
var $box = $('#pureFontModal'); if (!$box.length) return;
$('#wikifonts-history-section').remove();
var $sec = $('<div>',{id: 'wikifonts-history-section'}).css({'margin-top':'15px','padding-top':'10px','border-top':'1px solid #72777d','text-align':'left'});
var $hdr = $('<div>').css({'display':'flex','justify-content':'space-between','align-items':'center','padding':'4px 0','color':'#000','font-size':'13px'});
var $titleLeft = $('<span>').text('History').css({'color':'#000','font-weight':'bold'});
var $toggleLink = $('<span>', { id: 'wf-history-arrow' }).text(isOpen ? '[非表示]' : '[表示]').css({'color':'#36c','cursor':'pointer','font-size':'12px'});
$toggleLink.on('click', function() {
isOpen = $list.is(':hidden');
$list.toggle(isOpen);
$toggleLink.text(isOpen ? '[非表示]' : '[表示]');
localStorage.setItem(T_KEY, isOpen ? 'open' : 'close');
});
$hdr.append($titleLeft).append($toggleLink);
var $list = $('<div>',{id: 'wikifonts-history-list'}).css({'display':isOpen?'block':'none','max-height':'120px','overflow-y':'auto','margin-top':'5px'});
loadH().forEach(function(item) {
if (item.name === 'default') return;
var $row = $('<div>').css({'display':'flex','align-items':'center','justify-content':'space-between','padding':'4px 0','border-bottom':'1px solid rgba(0,0,0,0.1)'});
var $left = $('<div>').css({'display':'flex','align-items':'center','gap':'8px'});
$('<span>').text(item.starred?'⭐':'☆').css({'cursor':'pointer','font-size':'12px','color':item.starred?'#ffc107':'#000'})
.on('click', function(e) { e.stopPropagation();
var h = loadH(), i = h.find(function(f){return f.name===item.name});
if (i) { i.starred = !i.starred; localStorage.setItem(KEY, JSON.stringify(h)); ui(); }
}).appendTo($left);
var escapedName = item.name.replace(/"/g, '\\"');
var $nameLabel = $('<span>').text(item.name).css({
'font-family': '"' + escapedName + '", sans-serif',
'cursor': 'pointer',
'font-size': '13px',
'color': '#000'
}).on('mouseenter', function() { $(this).css({'color':'#0074D9','text-decoration':'underline'}); })
.on('mouseleave', function() { $(this).css({'color':'#000','text-decoration':'none'}); })
.on('click', function() {
$('#pureNameInput').val(item.name);
currentSelectedFontName = item.name;
newUploadedDataUrl = null;
getFont(item.name, function(r){ applyFont(item.name, r?r.dataUrl:null); });
});
// 各行ごとに確実に@font-faceを生成・適用(安全なCSS IDを使用)
getFont(item.name, function(r) {
if (r && r.dataUrl) {
var safeId = btoa(unescape(encodeURIComponent(item.name))).replace(/[^a-zA-Z0-9_-]/g, '_');
var listStyleId = 'testFontFace-' + safeId;
if (!$('#' + listStyleId).length) {
$('<style>', { id: listStyleId }).text('@font-face { font-family: "' + escapedName + '"; src: url("' + r.dataUrl + '"); }').appendTo('head');
}
}
});
$nameLabel.appendTo($left);
var $rightBtns = $('<div>').css({'display':'flex','align-items':'center','gap':'4px'});
// ✏️ 編集ボタン(名前変更用)
var $editBtn = $('<span>').text('✏️').css({'cursor':'pointer','font-size':'10px','padding':'2px','opacity':'0.6'})
.on('mouseenter', function() { $(this).css('opacity', '1'); })
.on('mouseleave', function() { $(this).css('opacity', '0.6'); })
.on('click', function(e) {
e.stopPropagation();
var newName = prompt('新しいフォント名を入力してください:', item.name);
if (!newName || !(newName = newName.trim())) return;
if (newName === item.name) return;
getFont(item.name, function(r) {
if (!r || !r.dataUrl) {
alert('フォントデータの取得に失敗しました。');
return;
}
openDB().done(function(db) {
var tx = db.transaction([STORE], 'readwrite');
var store = tx.objectStore(STORE);
store.delete(item.name); // 旧データの削除
store.put({ id: newName, dataUrl: r.dataUrl }); // 新名で登録
tx.oncomplete = function() {
// 古いスタイルのDOMを確実に削除
var oldSafeId = btoa(unescape(encodeURIComponent(item.name))).replace(/[^a-zA-Z0-9_-]/g, '_');
$('#testFontFace-' + oldSafeId).remove();
// 新しい名前用のスタイルを即座に登録
var newEscapedName = newName.replace(/"/g, '\\"');
var newSafeId = btoa(unescape(encodeURIComponent(newName))).replace(/[^a-zA-Z0-9_-]/g, '_');
var newStyleId = 'testFontFace-' + newSafeId;
if (!$('#' + newStyleId).length) {
$('<style>', { id: newStyleId }).text('@font-face { font-family: "' + newEscapedName + '"; src: url("' + r.dataUrl + '"); }').appendTo('head');
}
var h = loadH();
var target = h.find(function(f){return f.name===item.name});
var star = target ? target.starred : false;
h = h.filter(function(f){return f.name!==item.name});
h.unshift({name: newName, starred: star, time: Date.now()});
var s = h.filter(function(i){return i.starred && i.name!=='default'}),
u = h.filter(function(i){return !i.starred && i.name!=='default'});
h = s.sort(function(a,b){return b.time-a.time}).concat(u.sort(function(a,b){return b.time-a.time}));
h.push({name:'default',starred:false,time:0});
localStorage.setItem(KEY, JSON.stringify(h));
applyFont(newName, r.dataUrl);
ui();
};
});
});
});
// ❌ 削除ボタン
var $deleteBtn = $('<span>').text('❌').css({'cursor':'pointer','font-size':'10px','padding':'2px','opacity':'0.5'})
.on('mouseenter', function() { $(this).css('opacity', '1'); })
.on('mouseleave', function() { $(this).css('opacity', '0.5'); })
.on('click', function(e) { e.stopPropagation();
if (confirm('フォント「' + item.name + '」を削除しますか?')) {
var h = loadH().filter(function(f){return f.name!==item.name});
localStorage.setItem(KEY, JSON.stringify(h));
var safeId = btoa(unescape(encodeURIComponent(item.name))).replace(/[^a-zA-Z0-9_-]/g, '_');
$('#testFontFace-' + safeId).remove();
openDB().done(function(db){
db.transaction([STORE], 'readwrite').objectStore(STORE).delete(item.name);
}); ui();
}
});
$rightBtns.append($editBtn).append($deleteBtn);
$row.append($left).append($rightBtns).appendTo($list);
});
if (!$list.children().length) $('<div>').text('No history').css({'color':'#54595d','font-style':'italic','padding':'4px 0','font-size':'12px'}).appendTo($list);
$('#pureFontModal-btn-container').before($sec.append($hdr).append($list));
}
$(document).on('click', '#pt-font-setup', function() {
$('#pureFontModal').remove();
newUploadedDataUrl = null;
currentSelectedFontName = '';
var hLoad = loadH();
originalFontBeforeOpen = (hLoad.length && hLoad[0].name) ? hLoad[0].name : 'default';
var topName = (hLoad.length && hLoad[0].name !== 'default') ? hLoad[0].name : '';
var $modal = $('<div>', { id: 'pureFontModal' }).css({
'position': 'fixed', 'top': '20%', 'left': '50%', 'transform': 'translate(-50%, -20%)',
'background': '#fff', 'padding': '20px', 'border': '1px solid #72777d', 'border-radius': '4px',
'box-shadow': '0 2px 10px rgba(0,0,0,0.3)', 'z-index': '999999', 'width': '320px', 'color': '#000'
}).html(
'<h3 style="margin:0 0 15px 0; font-size:16px; text-align:left;">Font Uploader & History</h3>' +
'<label style="display:block; font-size:12px; margin-bottom:5px; text-align:left;">1. Select Font File (.ttf / .woff2 / .otf):</label>' +
'<input type="file" id="pureFileInput" accept=".ttf,.woff,.woff2,.otf" style="display:block; margin-bottom:15px; width:100%; font-size:12px;">' +
'<label style="display:block; font-size:12px; margin-bottom:5px; text-align:left;">2. Enter Font Name:</label>' +
'<input type="text" id="pureNameInput" style="display:block; margin-bottom:20px; width:100%; padding:4px; box-sizing:border-box;" value="' + topName + '">'
);
var $btnBox = $('<div>', { id: 'pureFontModal-btn-container' }).css({ 'display': 'flex', 'justify-content': 'space-between', 'align-items': 'center', 'margin-top': '15px' });
var $resetBtn = $('<button>', { id: 'pureResetBtn' }).text('default').css({ 'padding': '5px 10px', 'font-size': '12px', 'cursor': 'pointer', 'background': '#d33', 'color': '#fff', 'border': 'none', 'border-radius': '2px', 'font-weight': 'bold' });
$resetBtn.on('click', function(e) {
e.stopPropagation();
var h = loadH();
var idx = h.findIndex(function(i){return i.name==='default'});
if (idx !== -1) {
var dItem = h.splice(idx, 1)[0];
h.unshift(dItem);
}
localStorage.setItem(KEY, JSON.stringify(h));
applyFont('default');
$('#pureNameInput').val('');
newUploadedDataUrl = null;
currentSelectedFontName = 'default';
ui();
});
var $rightBtns = $('<div>').css({ 'display': 'flex', 'gap': '10px' });
var $cancel = $('<button>', { id: 'pureCloseBtn' }).text('Cancel').css({ 'padding': '5px 10px', 'font-size': '12px', 'cursor': 'pointer' });
var $save = $('<button>', { id: 'pureSaveBtn' }).text('Save').css({ 'padding': '5px 15px', 'font-size': '12px', 'cursor': 'pointer', 'background': '#36c', 'color': '#fff', 'border': 'none', 'border-radius': '2px' });
$rightBtns.append($cancel).append($save);
$btnBox.append($resetBtn).append($rightBtns);
$modal.append($btnBox).appendTo('body');
ui();
});
$(document).on('change', '#pureFileInput', function() {
if (this.files && this.files[0]) {
var file = this.files[0];
var r = new FileReader();
r.onload = function(e) {
newUploadedDataUrl = e.target.result;
currentSelectedFontName = '';
var fontName = $('#pureNameInput').val().trim();
if (!fontName) {
fontName = file.name.substring(0, file.name.lastIndexOf('.')) || 'TempFont';
$('#pureNameInput').val(fontName);
}
applyFont(fontName, newUploadedDataUrl);
};
r.readAsDataURL(file);
}
});
$(document).on('input', '#pureNameInput', function() {
var fontName = $(this).val().trim();
if (fontName && newUploadedDataUrl) {
applyFont(fontName, newUploadedDataUrl);
}
});
$(document).on('click', '#pureSaveBtn', function() {
var name = $('#pureNameInput').val().trim(); if (!name) return alert('フォント名を入力してください。');
try {
if (newUploadedDataUrl) {
openDB().done(function(db) {
var tx = db.transaction([STORE], 'readwrite');
tx.objectStore(STORE).put({ id: name, dataUrl: newUploadedDataUrl });
tx.oncomplete = function() {
updateH(name); alert('保存しました!'); $('#pureFontModal').remove(); newUploadedDataUrl = null;
};
tx.onerror = function() {
alert('⚠️ 【容量オーバー警告】IndexedDBへの書き込みに失敗しました。');
};
});
} else if (currentSelectedFontName) {
if (currentSelectedFontName === 'default') {
applyFont('default');
}
updateH(currentSelectedFontName); $('#pureFontModal').remove();
} else {
updateH(name); applyFont(name); $('#pureFontModal').remove();
}
} catch (error) {
alert('❌ エラーが発生したため保存を中断しました:\n' + error.message);
$('#pureFontModal').remove();
}
}).on('click', '#pureCloseBtn', function() {
$('#pureFontModal').remove();
newUploadedDataUrl = null;
currentSelectedFontName = '';
var targetFont = originalFontBeforeOpen;
if (targetFont && targetFont !== 'default') {
getFont(targetFont, function(r) { applyFont(targetFont, r ? r.dataUrl : null); });
} else {
applyFont('default');
}
});
});
8wks7eqvrhex6y8lgmhj5a3l9ou9fjf
769452
769451
2026-10-21T13:47:35Z
OUT is this
73801
769452
javascript
text/javascript
(function() {
'use strict';
if (!mw.config.get('wgUserId')) return;
var KEY = 'wikifonts_history_addon', T_KEY = 'wikifonts_history_toggle_state';
var DB = 'PureFontUploaderDB', STORE = 'fontStore';
var isOpen = localStorage.getItem(T_KEY) === 'open';
var newUploadedDataUrl = null;
var currentSelectedFontName = '';
var originalFontBeforeOpen = null;
function openDB() {
return $.Deferred(function(d) {
var r = indexedDB.open(DB, 1);
r.onupgradeneeded = function(e) { if (!e.target.result.objectStoreNames.contains(STORE)) e.target.result.createObjectStore(STORE, { keyPath: 'id' }); };
r.onsuccess = function(e) { d.resolve(e.target.result); };
}).promise();
}
function getFont(name, cb) {
openDB().done(function(db) {
db.transaction([STORE], 'readonly').objectStore(STORE).get(name).onsuccess = function(e) { cb(e.target.result); };
});
}
function loadH() {
try {
var h = JSON.parse(localStorage.getItem(KEY) || '[]');
if (!h.some(function(i){return i.name==='default'})) { h.push({name:'default',starred:false,time:0}); localStorage.setItem(KEY, JSON.stringify(h)); }
return h;
} catch(e) { return [{name:'default',starred:false,time:0}]; }
}
function applyFont(name, url) {
$('#testFontFace').remove();
if (name === 'default') {
document.body.style.removeProperty('font-family');
return;
}
if (url) $('<style>', { id: 'testFontFace' }).text('@font-face { font-family: "' + name.replace(/"/g, '\\"') + '"; src: url("' + url + '"); }').appendTo('head');
document.body.style.setProperty('font-family', '"' + name + '", sans-serif', 'important');
}
// 【同期】リロード時の自動復元
$(document).ready(function() {
var h = loadH();
if (h.length && h[0].name !== 'default') {
getFont(h[0].name, function(r) { if (r) applyFont(r.id, r.dataUrl); else applyFont(h[0].name); });
}
});
$(document).ready(function() {
$('#pt-font-setup').remove();
mw.util.addPortletLink('p-personal', 'javascript:void(0);', 'Font Setup', 'pt-font-setup', 'Open uploader', null, '#pt-logout');
function updateH(font) {
if (!font) return;
var h = loadH(), idx = h.findIndex(function(i){return i.name===font}), star = idx !== -1 ? h[idx].starred : false;
if (idx !== -1) h.splice(idx, 1);
h.unshift({name: font, starred: star, time: Date.now()});
var s = h.filter(function(i){return i.starred && i.name!=='default'}),
u = h.filter(function(i){return !i.starred && i.name!=='default'});
h = s.sort(function(a,b){return b.time-a.time}).concat(u.sort(function(a,b){return b.time-a.time}));
h.push({name:'default',starred:false,time:0});
try {
localStorage.setItem(KEY, JSON.stringify(h));
} catch (e) {
alert('⚠️ 【容量警告】LocalStorageの容量が不足しているため履歴の保存に失敗しました。');
}
ui();
}
function ui() {
var $box = $('#pureFontModal'); if (!$box.length) return;
$('#wikifonts-history-section').remove();
var $sec = $('<div>',{id: 'wikifonts-history-section'}).css({'margin-top':'15px','padding-top':'10px','border-top':'1px solid #72777d','text-align':'left'});
var $hdr = $('<div>').css({'display':'flex','justify-content':'space-between','align-items':'center','padding':'4px 0','color':'#000','font-size':'13px'});
var $titleLeft = $('<span>').text('History').css({'color':'#000','font-weight':'bold'});
var $toggleLink = $('<span>', { id: 'wf-history-arrow' }).text(isOpen ? '[非表示]' : '[表示]').css({'color':'#36c','cursor':'pointer','font-size':'12px'});
$toggleLink.on('click', function() {
isOpen = $list.is(':hidden');
$list.toggle(isOpen);
$toggleLink.text(isOpen ? '[非表示]' : '[表示]');
localStorage.setItem(T_KEY, isOpen ? 'open' : 'close');
});
$hdr.append($titleLeft).append($toggleLink);
var $list = $('<div>',{id: 'wikifonts-history-list'}).css({'display':isOpen?'block':'none','max-height':'120px','overflow-y':'auto','margin-top':'5px'});
loadH().forEach(function(item) {
if (item.name === 'default') return;
var $row = $('<div>').css({'display':'flex','align-items':'center','justify-content':'space-between','padding':'4px 0','border-bottom':'1px solid rgba(0,0,0,0.1)'});
var $left = $('<div>').css({'display':'flex','align-items':'center','gap':'8px'});
$('<span>').text(item.starred?'⭐':'☆').css({'cursor':'pointer','font-size':'12px','color':item.starred?'#ffc107':'#000'})
.on('click', function(e) { e.stopPropagation();
var h = loadH(), i = h.find(function(f){return f.name===item.name});
if (i) { i.starred = !i.starred; localStorage.setItem(KEY, JSON.stringify(h)); ui(); }
}).appendTo($left);
var escapedName = item.name.replace(/"/g, '\\"');
var $nameLabel = $('<span>').text(item.name).css({
'font-family': '"' + escapedName + '", sans-serif',
'cursor': 'pointer',
'font-size': '13px',
'color': '#000'
}).on('mouseenter', function() { $(this).css({'color':'#0074D9','text-decoration':'underline'}); })
.on('mouseleave', function() { $(this).css({'color':'#000','text-decoration':'none'}); })
.on('click', function() {
$('#pureNameInput').val(item.name);
currentSelectedFontName = item.name;
newUploadedDataUrl = null;
getFont(item.name, function(r){ applyFont(item.name, r?r.dataUrl:null); });
});
// 各行ごとに確実に@font-faceを生成・適用(安全なCSS IDを使用)
getFont(item.name, function(r) {
if (r && r.dataUrl) {
var safeId = btoa(unescape(encodeURIComponent(item.name))).replace(/[^a-zA-Z0-9_-]/g, '_');
var listStyleId = 'testFontFace-' + safeId;
if (!$('#' + listStyleId).length) {
$('<style>', { id: listStyleId }).text('@font-face { font-family: "' + escapedName + '"; src: url("' + r.dataUrl + '"); }').appendTo('head');
}
}
});
$nameLabel.appendTo($left);
var $rightBtns = $('<div>').css({'display':'flex','align-items':'center','gap':'4px'});
// ✏️ 編集ボタン(名前変更用)
var $editBtn = $('<span>').text('✏️').css({'cursor':'pointer','font-size':'10px','padding':'2px','opacity':'0.6'})
.on('mouseenter', function() { $(this).css('opacity', '1'); })
.on('mouseleave', function() { $(this).css('opacity', '0.6'); })
.on('click', function(e) {
e.stopPropagation();
var newName = prompt('新しいフォント名を入力してください:', item.name);
if (!newName || !(newName = newName.trim())) return;
if (newName === item.name) return;
getFont(item.name, function(r) {
if (!r || !r.dataUrl) {
alert('フォントデータの取得に失敗しました。');
return;
}
openDB().done(function(db) {
var tx = db.transaction([STORE], 'readwrite');
var store = tx.objectStore(STORE);
store.delete(item.name); // 旧データの削除
store.put({ id: newName, dataUrl: r.dataUrl }); // 新名で登録
tx.oncomplete = function() {
// 古いスタイルのDOMを確実に削除
var oldSafeId = btoa(unescape(encodeURIComponent(item.name))).replace(/[^a-zA-Z0-9_-]/g, '_');
$('#testFontFace-' + oldSafeId).remove();
// 新しい名前用のスタイルを即座に登録
var newEscapedName = newName.replace(/"/g, '\\"');
var newSafeId = btoa(unescape(encodeURIComponent(newName))).replace(/[^a-zA-Z0-9_-]/g, '_');
var newStyleId = 'testFontFace-' + newSafeId;
if (!$('#' + newStyleId).length) {
$('<style>', { id: newStyleId }).text('@font-face { font-family: "' + newEscapedName + '"; src: url("' + r.dataUrl + '"); }').appendTo('head');
}
var h = loadH();
var target = h.find(function(f){return f.name===item.name});
var star = target ? target.starred : false;
h = h.filter(function(f){return f.name!==item.name});
h.unshift({name: newName, starred: star, time: Date.now()});
var s = h.filter(function(i){return i.starred && i.name!=='default'}),
u = h.filter(function(i){return !i.starred && i.name!=='default'});
h = s.sort(function(a,b){return b.time-a.time}).concat(u.sort(function(a,b){return b.time-a.time}));
h.push({name:'default',starred:false,time:0});
localStorage.setItem(KEY, JSON.stringify(h));
applyFont(newName, r.dataUrl);
ui();
};
});
});
});
// ❌ 削除ボタン
var $deleteBtn = $('<span>').text('❌').css({'cursor':'pointer','font-size':'10px','padding':'2px','opacity':'0.5'})
.on('mouseenter', function() { $(this).css('opacity', '1'); })
.on('mouseleave', function() { $(this).css('opacity', '0.5'); })
.on('click', function(e) { e.stopPropagation();
if (confirm('フォント「' + item.name + '」を削除しますか?')) {
var h = loadH().filter(function(f){return f.name!==item.name});
localStorage.setItem(KEY, JSON.stringify(h));
var safeId = btoa(unescape(encodeURIComponent(item.name))).replace(/[^a-zA-Z0-9_-]/g, '_');
$('#testFontFace-' + safeId).remove();
openDB().done(function(db){
db.transaction([STORE], 'readwrite').objectStore(STORE).delete(item.name);
}); ui();
}
});
$rightBtns.append($editBtn).append($deleteBtn);
$row.append($left).append($rightBtns).appendTo($list);
});
if (!$list.children().length) $('<div>').text('No history').css({'color':'#54595d','font-style':'italic','padding':'4px 0','font-size':'12px'}).appendTo($list);
$('#pureFontModal-btn-container').before($sec.append($hdr).append($list));
}
$(document).on('click', '#pt-font-setup', function() {
$('#pureFontModal').remove();
newUploadedDataUrl = null;
currentSelectedFontName = '';
var hLoad = loadH();
originalFontBeforeOpen = (hLoad.length && hLoad[0].name) ? hLoad[0].name : 'default';
var topName = (hLoad.length && hLoad[0].name !== 'default') ? hLoad[0].name : '';
var $modal = $('<div>', { id: 'pureFontModal' }).css({
'position': 'fixed', 'top': '20%', 'left': '50%', 'transform': 'translate(-50%, -20%)',
'background': '#fff', 'padding': '20px', 'border': '1px solid #72777d', 'border-radius': '4px',
'box-shadow': '0 2px 10px rgba(0,0,0,0.3)', 'z-index': '999999', 'width': '320px', 'color': '#000'
}).html(
'<h3 style="margin:0 0 15px 0; font-size:16px; text-align:left;">Font Uploader & History</h3>' +
'<label style="display:block; font-size:12px; margin-bottom:5px; text-align:left;">1. Select Font File (.ttf / .woff2 / .otf):</label>' +
'<input type="file" id="pureFileInput" accept=".ttf,.woff,.woff2,.otf" style="display:block; margin-bottom:15px; width:100%; font-size:12px;">' +
'<label style="display:block; font-size:12px; margin-bottom:5px; text-align:left;">2. Enter Font Name:</label>' +
'<input type="text" id="pureNameInput" style="display:block; margin-bottom:20px; width:100%; padding:4px; box-sizing:border-box;" value="' + topName + '">'
);
var $btnBox = $('<div>', { id: 'pureFontModal-btn-container' }).css({ 'display': 'flex', 'justify-content': 'space-between', 'align-items': 'center', 'margin-top': '15px' });
var $resetBtn = $('<button>', { id: 'pureResetBtn' }).text('default').css({ 'padding': '5px 10px', 'font-size': '12px', 'cursor': 'pointer', 'background': '#d33', 'color': '#fff', 'border': 'none', 'border-radius': '2px', 'font-weight': 'bold' });
$resetBtn.on('click', function(e) {
e.stopPropagation();
var h = loadH();
var idx = h.findIndex(function(i){return i.name==='default'});
if (idx !== -1) {
var dItem = h.splice(idx, 1)[0];
h.unshift(dItem);
}
localStorage.setItem(KEY, JSON.stringify(h));
applyFont('default');
$('#pureNameInput').val('');
newUploadedDataUrl = null;
currentSelectedFontName = 'default';
ui();
});
var $rightBtns = $('<div>').css({ 'display': 'flex', 'gap': '10px' });
var $cancel = $('<button>', { id: 'pureCloseBtn' }).text('Cancel').css({ 'padding': '5px 10px', 'font-size': '12px', 'cursor': 'pointer' });
var $save = $('<button>', { id: 'pureSaveBtn' }).text('Save').css({ 'padding': '5px 15px', 'font-size': '12px', 'cursor': 'pointer', 'background': '#36c', 'color': '#fff', 'border': 'none', 'border-radius': '2px' });
$rightBtns.append($cancel).append($save);
$btnBox.append($resetBtn).append($rightBtns);
$modal.append($btnBox).appendTo('body');
ui();
});
$(document).on('change', '#pureFileInput', function() {
if (this.files && this.files[0]) {
var file = this.files[0];
var r = new FileReader();
r.onload = function(e) {
newUploadedDataUrl = e.target.result;
currentSelectedFontName = '';
var fontName = $('#pureNameInput').val().trim();
if (!fontName) {
fontName = file.name.substring(0, file.name.lastIndexOf('.')) || 'TempFont';
$('#pureNameInput').val(fontName);
}
applyFont(fontName, newUploadedDataUrl);
};
r.readAsDataURL(file);
}
});
$(document).on('input', '#pureNameInput', function() {
var fontName = $(this).val().trim();
if (fontName && newUploadedDataUrl) {
applyFont(fontName, newUploadedDataUrl);
}
});
$(document).on('click', '#pureSaveBtn', function() {
var name = $('#pureNameInput').val().trim(); if (!name) return alert('フォント名を入力してください。');
try {
if (newUploadedDataUrl) {
openDB().done(function(db) {
var tx = db.transaction([STORE], 'readwrite');
tx.objectStore(STORE).put({ id: name, dataUrl: newUploadedDataUrl });
tx.oncomplete = function() {
updateH(name); alert('保存しました!'); $('#pureFontModal').remove(); newUploadedDataUrl = null;
};
tx.onerror = function() {
alert('⚠️ 【容量オーバー警告】IndexedDBへの書き込みに失敗しました。');
};
});
} else if (currentSelectedFontName) {
if (currentSelectedFontName === 'default') {
applyFont('default');
}
updateH(currentSelectedFontName); $('#pureFontModal').remove();
} else {
updateH(name); applyFont(name); $('#pureFontModal').remove();
}
} catch (error) {
alert('❌ エラーが発生したため保存を中断しました:\n' + error.message);
$('#pureFontModal').remove();
}
}).on('click', '#pureCloseBtn', function() {
$('#pureFontModal').remove();
newUploadedDataUrl = null;
currentSelectedFontName = '';
var targetFont = originalFontBeforeOpen;
if (targetFont && targetFont !== 'default') {
getFont(targetFont, function(r) { applyFont(targetFont, r ? r.dataUrl : null); });
} else {
applyFont('default');
}
});
});
})();
guwpf5lobfc6ovipnzfsuwvlt0bsjsb
User:Phoenix3452
2
179360
769481
769283
2026-10-21T14:16:28Z
Phoenix3452
76309
769481
wikitext
text/x-wiki
<div style="background:linear-gradient(135deg,#160606,#541000,#130505);border:3px solid #ff5a1f;border-radius:18px;padding:30px;text-align:center;color:#ffd6a0;">
<div style="font-size:38px;font-weight:bold;color:#ff6a00;text-shadow:0 0 12px #ff3300;">
🔥 PHOENIX RISING 🔥
</div>
<div style="font-size:16px;margin:12px 0;color:#ffccaa;">
REBORN FROM THE ASHES
</div>
<hr style="border:1px solid #ff5a1f;">
<div style="font-size:18px;font-weight:bold;color:#ff8c42;">
✦ WELCOME TO MY DOMAIN ✦
</div>
<p style="color:#ffe0c2;">
A legend never truly dies. It rises again.
</p>
<div style="margin:20px auto;">
<span style="display:inline-block;background:#8b1a08;padding:10px 18px;border-radius:8px;margin:5px;">
[[User page|🏠 HOME]]
</span>
<span style="display:inline-block;background:#8b1a08;padding:10px 18px;border-radius:8px;margin:5px;">
[[Special:Contributions/{{PAGENAME}}|📜 CONTRIBUTIONS]]
</span>
<span style="display:inline-block;background:#8b1a08;padding:10px 18px;border-radius:8px;margin:5px;">
[[Special:Talk/{{PAGENAME}}|💬 TALK]]
</span>
</div>
<hr style="border:1px solid #ff5a1f;">
<div style="font-size:13px;color:#ff9966;">
⚡ BORN IN FIRE · BUILT TO RISE · PHOENIX FOREVER ⚡
</div>
</div>
04sndax5d4c3hrq65efj2fdctflslw3
769482
769481
2026-10-21T14:18:27Z
Phoenix3452
76309
769482
wikitext
text/x-wiki
<div style="background:linear-gradient(145deg,#100707,#310909,#160606);border:3px solid #ff5722;border-radius:16px;padding:24px;color:#ffe4c4;font-family:sans-serif;">
<div style="text-align:center;padding:24px 8px;background:linear-gradient(120deg,#210606,#781b0b,#210606);border:2px solid #ff762e;border-radius:12px;">
<div style="font-size:42px;font-weight:bold;color:#ff792f;text-shadow:0 0 15px #ff2600;">
🐦🔥 PHOENIX RISING
</div>
<div style="font-size:16px;letter-spacing:3px;color:#ffd0a0;">
RISE FROM THE ASHES
</div>
<div style="font-size:13px;color:#ffb080;margin-top:12px;">
━━━━━━━━━━━━━━━━━━━━
</div>
<div style="font-size:19px;color:#fff0d8;font-weight:bold;">
WELCOME TO MY USER PAGE
</div>
</div>
<div style="text-align:center;margin:24px 0 12px;font-size:25px;font-weight:bold;color:#ff762e;">
🔥 THE MENTOR'S CORNER 🔥
</div>
<div style="background:#251010;border:1px solid #c94725;border-left:6px solid #ff6a26;border-radius:10px;padding:18px;margin:12px 0;">
<div style="font-size:21px;font-weight:bold;color:#ffb16e;">
Need help? I've got your back!
</div>
<p style="font-size:15px;line-height:1.8;color:#ffe2cf;">
Hey! Welcome to my page. 👋
</p>
<p style="line-height:1.8;color:#ffe2cf;">
I'm here to help out fellow editors and newcomers! If you have questions about editing, wiki markup, formatting, creating pages, or figuring out how things work around here, feel free to reach out.
</p>
<p style="line-height:1.8;color:#ffe2cf;">
<strong style="color:#ff8a43;">🔥 Don't be afraid to ask questions.</strong> Everyone starts somewhere, and we're all here to learn. Leave me a message on my talk page, and I'll do my best to help!
</p>
<div style="text-align:center;font-size:16px;font-weight:bold;color:#ffb16e;padding:10px;">
⚔️ LEARN · HELP · IMPROVE · RISE ⚔️
</div>
</div>
<div style="text-align:center;font-size:23px;font-weight:bold;color:#ff762e;margin:24px 0 14px;">
⚡ QUICK LINKS ⚡
</div>
<div style="text-align:center;padding:10px;background:#200909;border:1px solid #87351d;border-radius:10px;">
<div style="display:inline-block;background:#76200e;border:1px solid #ff762e;border-radius:8px;padding:12px;margin:5px;">
[[User talk:{{PAGENAME}}|💬 MESSAGE ME]]
</div>
<div style="display:inline-block;background:#76200e;border:1px solid #ff762e;border-radius:8px;padding:12px;margin:5px;">
[[Special:Contributions/{{PAGENAME}}|📜 MY EDITS]]
</div>
<div style="display:inline-block;background:#76200e;border:1px solid #ff762e;border-radius:8px;padding:12px;margin:5px;">
[[Special:RecentChanges|📰 RECENT CHANGES]]
</div>
<div style="display:inline-block;background:#76200e;border:1px solid #ff762e;border-radius:8px;padding:12px;margin:5px;">
[[Special:AllPages|📚 ALL PAGES]]
</div>
</div>
<div style="margin-top:24px;background:#190808;border:1px solid #87351d;border-radius:10px;padding:16px;">
<div style="font-size:20px;font-weight:bold;color:#ff8a43;text-align:center;">
👑 MY PHILOSOPHY
</div>
<p style="text-align:center;line-height:1.8;color:#ffdec6;">
"Even the brightest phoenix began as ashes. Keep learning, keep helping, and never stop rising."
</p>
</div>
<div style="text-align:center;margin-top:22px;padding:16px;border-top:2px solid #ff5722;color:#ff9966;">
<strong>🐦🔥 THE PHOENIX NEVER STAYS DOWN 🐦🔥</strong>
<div style="font-size:12px;margin-top:8px;">
Here to learn. Here to help. Always rising.
</div>
</div>
</div>
2wahk6i2ugzjp1aw66xgtbyyapp2igo
769489
769482
2026-10-21T14:21:40Z
Phoenix3452
76309
769489
wikitext
text/x-wiki
<div style="background:linear-gradient(145deg,#100707,#310909,#160606);border:3px solid #ff5722;border-radius:16px;padding:24px;color:#ffe4c4;font-family:sans-serif;">
<div style="text-align:center;padding:28px 10px;background:linear-gradient(120deg,#210606,#781b0b,#210606);border:2px solid #ff762e;border-radius:12px;">
<div style="font-size:42px;font-weight:bold;color:#ff792f;text-shadow:0 0 15px #ff2600;">
🐦🔥 PHOENIX RISING
</div>
<div style="font-size:16px;letter-spacing:3px;color:#ffd0a0;">
RISE FROM THE ASHES
</div>
<div style="color:#ffb080;margin:12px;">━━━━━━━━━━━━━━━━━━━━</div>
<div style="font-size:20px;color:#fff0d8;font-weight:bold;">
WELCOME TO MY DOMAIN
</div>
<div style="font-size:14px;color:#ffccaa;margin-top:8px;">
A LEGEND NEVER STOPS RISING
</div>
</div>
<div style="text-align:center;margin:25px 0 15px;font-size:25px;font-weight:bold;color:#ff762e;">
⚡ QUICK LINKS ⚡
</div>
<div style="text-align:center;background:#200909;border:1px solid #87351d;border-radius:10px;padding:12px;">
<div style="display:inline-block;background:#76200e;border:2px solid #ff762e;border-radius:8px;padding:12px;margin:5px;">
[[User:{{PAGENAME}}|🏠 HOME]]
</div>
<div style="display:inline-block;background:#76200e;border:2px solid #ff762e;border-radius:8px;padding:12px;margin:5px;">
[[User:{{PAGENAME}}#Mentor|🔥 MENTOR CENTER]]
</div>
<div style="display:inline-block;background:#76200e;border:2px solid #ff762e;border-radius:8px;padding:12px;margin:5px;">
[[User talk:{{PAGENAME}}|💬 ASK YOUR MENTOR]]
</div>
<div style="display:inline-block;background:#76200e;border:2px solid #ff762e;border-radius:8px;padding:12px;margin:5px;">
[[Special:Contributions/{{PAGENAME}}|📜 MY EDITS]]
</div>
<div style="display:inline-block;background:#76200e;border:2px solid #ff762e;border-radius:8px;padding:12px;margin:5px;">
[[Special:RecentChanges|📰 RECENT CHANGES]]
</div>
<div style="display:inline-block;background:#76200e;border:2px solid #ff762e;border-radius:8px;padding:12px;margin:5px;">
[[Special:AllPages|📚 ALL PAGES]]
</div>
</div>
<div style="text-align:center;margin:28px 0 14px;font-size:25px;font-weight:bold;color:#ff762e;">
🐦🔥 ABOUT ME
</div>
<div style="background:#251010;border:1px solid #c94725;border-radius:10px;padding:18px;line-height:1.8;">
Welcome to my user page! 👋
I'm here to learn, contribute, improve the wiki, and help other editors along the way.
I believe everyone starts somewhere, and a friendly community makes learning easier. Feel free to explore my page and check out my contributions!
</div>
<div id="Mentor"></div>
<div style="text-align:center;margin:28px 0 14px;font-size:27px;font-weight:bold;color:#ff762e;">
🔥 MENTOR CENTER 🔥
</div>
<div style="background:#251010;border:2px solid #ff5722;border-left:6px solid #ff762e;border-radius:10px;padding:20px;">
<div style="font-size:22px;font-weight:bold;color:#ffb16e;">
Need help? I've got your back!
</div>
<p style="line-height:1.8;color:#ffe2cf;">
Hey there! Welcome to my Mentor Center. If you're new around here or need a hand, feel free to reach out!
</p>
<p style="line-height:1.8;color:#ffe2cf;">
I can try to help with wiki editing, wiki markup, formatting, finding your way around, and general questions about contributing.
</p>
<p style="line-height:1.8;color:#ffe2cf;">
<strong style="color:#ff8a43;">🔥 No question is too small.</strong> Don't be afraid to ask! Leave a message on my talk page, and I'll do my best to help when I can.
</p>
<div style="text-align:center;margin:18px 0;">
<div style="display:inline-block;background:#8b1a08;border:2px solid #ff9a50;border-radius:8px;padding:14px 20px;margin:5px;font-weight:bold;">
[[User talk:{{PAGENAME}}|💬 ASK YOUR MENTOR HERE]]
</div>
<div style="display:inline-block;background:#8b1a08;border:2px solid #ff9a50;border-radius:8px;padding:14px 20px;margin:5px;font-weight:bold;">
[[Special:Contributions/{{PAGENAME}}|📜 CHECK MY EDITS]]
</div>
</div>
<div style="text-align:center;font-size:16px;font-weight:bold;color:#ffb16e;padding:10px;">
⚔️ LEARN · HELP · IMPROVE · RISE ⚔️
</div>
</div>
<div style="text-align:center;margin:28px 0 14px;font-size:24px;font-weight:bold;color:#ff762e;">
👑 MY PHILOSOPHY
</div>
<div style="background:#190808;border:1px solid #87351d;border-radius:10px;padding:20px;text-align:center;line-height:1.8;color:#ffdec6;">
"Even the brightest phoenix began as ashes. Keep learning, keep helping, and never stop rising."
</div>
<div style="text-align:center;margin-top:25px;padding:20px;border-top:2px solid #ff5722;color:#ff9966;">
<div style="font-size:22px;font-weight:bold;">
🐦🔥 THE PHOENIX NEVER STAYS DOWN 🐦🔥
</div>
<div style="font-size:13px;margin-top:10px;">
HERE TO LEARN · HERE TO HELP · ALWAYS RISING
</div>
</div>
</div>
2m8ta9smvj230bqhn6ivtwldewmun90
769491
769489
2026-10-21T14:45:42Z
Phoenix3452
76309
769491
wikitext
text/x-wiki
<div style="background:linear-gradient(145deg,#100707,#310909,#160606);border:3px solid #ff5722;border-radius:16px;padding:24px;color:#ffe4c4;font-family:sans-serif;">
<div style="text-align:center;padding:28px 10px;background:linear-gradient(120deg,#210606,#781b0b,#210606);border:2px solid #ff762e;border-radius:12px;">
<div style="font-size:42px;font-weight:bold;color:#ff792f;text-shadow:0 0 15px #ff2600;">
🐦🔥 PHOENIX RISING
</div>
<div style="font-size:16px;letter-spacing:3px;color:#ffd0a0;">
RISE FROM THE ASHES
</div>
<div style="color:#ffb080;margin:12px;">━━━━━━━━━━━━━━━━━━━━</div>
<div style="font-size:20px;color:#fff0d8;font-weight:bold;">
WELCOME TO MY DOMAIN
</div>
<div style="font-size:14px;color:#ffccaa;margin-top:8px;">
A LEGEND NEVER STOPS RISING
</div>
</div>
<div style="text-align:center;margin:25px 0 15px;font-size:25px;font-weight:bold;color:#ff762e;">
⚡ QUICK LINKS ⚡
</div>
<div style="text-align:center;background:#200909;border:1px solid #87351d;border-radius:10px;padding:12px;">
<div style="display:inline-block;background:#76200e;border:2px solid #ff762e;border-radius:8px;padding:12px;margin:5px;">
[[User:{{PAGENAME}}|🏠 HOME]]
</div>
<div style="display:inline-block;background:#76200e;border:2px solid #ff762e;border-radius:8px;padding:12px;margin:5px;">
[[User:{{PAGENAME}}#Mentor|🔥 MENTOR CENTER]]
</div>
<div style="display:inline-block;background:#76200e;border:2px solid #ff762e;border-radius:8px;padding:12px;margin:5px;">
[[User talk:{{PAGENAME}}|💬 ASK YOUR MENTOR]]
</div>
<div style="display:inline-block;background:#76200e;border:2px solid #ff762e;border-radius:8px;padding:12px;margin:5px;">
[[Special:Contributions/{{PAGENAME}}|📜 MY EDITS]]
</div>
<div style="display:inline-block;background:#76200e;border:2px solid #ff762e;border-radius:8px;padding:12px;margin:5px;">
[[Special:RecentChanges|📰 RECENT CHANGES]]
</div>
<div style="display:inline-block;background:#76200e;border:2px solid #ff762e;border-radius:8px;padding:12px;margin:5px;">
[[Special:AllPages|📚 ALL PAGES]]
<div style="display:inline-block;background:#76200e;border:2px solid #ff762e;border-radius:8px;padding:12px;margin:5px;">
[[User:{{PAGENAME}}/sandbox|🔥 MY PHOENIX SANDBOX]]
</div>
</div>
</div>
<div style="text-align:center;margin:28px 0 14px;font-size:25px;font-weight:bold;color:#ff762e;">
🐦🔥 ABOUT ME
</div>
<div style="background:#251010;border:1px solid #c94725;border-radius:10px;padding:18px;line-height:1.8;">
Welcome to my user page! 👋
I'm here to learn, contribute, improve the wiki, and help other editors along the way.
I believe everyone starts somewhere, and a friendly community makes learning easier. Feel free to explore my page and check out my contributions!
</div>
<div id="Mentor"></div>
<div style="text-align:center;margin:28px 0 14px;font-size:27px;font-weight:bold;color:#ff762e;">
🔥 MENTOR CENTER 🔥
</div>
<div style="background:#251010;border:2px solid #ff5722;border-left:6px solid #ff762e;border-radius:10px;padding:20px;">
<div style="font-size:22px;font-weight:bold;color:#ffb16e;">
Need help? I've got your back!
</div>
<p style="line-height:1.8;color:#ffe2cf;">
Hey there! Welcome to my Mentor Center. If you're new around here or need a hand, feel free to reach out!
</p>
<p style="line-height:1.8;color:#ffe2cf;">
I can try to help with wiki editing, wiki markup, formatting, finding your way around, and general questions about contributing.
</p>
<p style="line-height:1.8;color:#ffe2cf;">
<strong style="color:#ff8a43;">🔥 No question is too small.</strong> Don't be afraid to ask! Leave a message on my talk page, and I'll do my best to help when I can.
</p>
<div style="text-align:center;margin:18px 0;">
<div style="display:inline-block;background:#8b1a08;border:2px solid #ff9a50;border-radius:8px;padding:14px 20px;margin:5px;font-weight:bold;">
[[User talk:{{PAGENAME}}|💬 ASK YOUR MENTOR HERE]]
</div>
<div style="display:inline-block;background:#8b1a08;border:2px solid #ff9a50;border-radius:8px;padding:14px 20px;margin:5px;font-weight:bold;">
[[Special:Contributions/{{PAGENAME}}|📜 CHECK MY EDITS]]
</div>
</div>
<div style="text-align:center;font-size:16px;font-weight:bold;color:#ffb16e;padding:10px;">
⚔️ LEARN · HELP · IMPROVE · RISE ⚔️
</div>
</div>
<div style="text-align:center;margin:28px 0 14px;font-size:24px;font-weight:bold;color:#ff762e;">
👑 MY PHILOSOPHY
</div>
<div style="background:#190808;border:1px solid #87351d;border-radius:10px;padding:20px;text-align:center;line-height:1.8;color:#ffdec6;">
"Even the brightest phoenix began as ashes. Keep learning, keep helping, and never stop rising."
</div>
<div style="text-align:center;margin-top:25px;padding:20px;border-top:2px solid #ff5722;color:#ff9966;">
<div style="font-size:22px;font-weight:bold;">
🐦🔥 THE PHOENIX NEVER STAYS DOWN 🐦🔥
</div>
<div style="font-size:13px;margin-top:10px;">
HERE TO LEARN · HERE TO HELP · ALWAYS RISING
</div>
</div>
</div>
3hc4t2fxa02sejzq0vl1i21y7uf5pd7
769492
769491
2026-10-21T14:48:04Z
Phoenix3452
76309
769492
wikitext
text/x-wiki
<div style="background:linear-gradient(145deg,#100707,#310909,#160606);border:3px solid #ff5722;border-radius:16px;padding:24px;color:#ffe4c4;font-family:sans-serif;">
<div style="text-align:center;padding:28px 10px;background:linear-gradient(120deg,#210606,#781b0b,#210606);border:2px solid #ff762e;border-radius:12px;">
<div style="font-size:42px;font-weight:bold;color:#ff792f;text-shadow:0 0 15px #ff2600;">
🐦🔥 PHOENIX RISING
</div>
<div style="font-size:16px;letter-spacing:3px;color:#ffd0a0;">
RISE FROM THE ASHES
</div>
<div style="color:#ffb080;margin:12px;">━━━━━━━━━━━━━━━━━━━━</div>
<div style="font-size:20px;color:#fff0d8;font-weight:bold;">
WELCOME TO MY DOMAIN
</div>
<div style="font-size:14px;color:#ffccaa;margin-top:8px;">
A LEGEND NEVER STOPS RISING
</div>
</div>
<div style="text-align:center;margin:25px 0 15px;font-size:25px;font-weight:bold;color:#ff762e;">
⚡ QUICK LINKS ⚡
</div>
<div style="text-align:center;background:#200909;border:1px solid #87351d;border-radius:10px;padding:12px;">
<div style="display:inline-block;background:#76200e;border:2px solid #ff762e;border-radius:8px;padding:12px;margin:5px;">
[[User:{{PAGENAME}}|🏠 HOME]]
</div>
<div style="display:inline-block;background:#76200e;border:2px solid #ff762e;border-radius:8px;padding:12px;margin:5px;">
[[User:{{PAGENAME}}/sandbox|🔥 MY PHOENIX SANDBOX]]
</div>
<div style="display:inline-block;background:#76200e;border:2px solid #ff762e;border-radius:8px;padding:12px;margin:5px;">
[[User:{{PAGENAME}}#Mentor|🔥 MENTOR CENTER]]
</div>
<div style="display:inline-block;background:#76200e;border:2px solid #ff762e;border-radius:8px;padding:12px;margin:5px;">
[[User talk:{{PAGENAME}}|💬 ASK YOUR MENTOR]]
</div>
<div style="display:inline-block;background:#76200e;border:2px solid #ff762e;border-radius:8px;padding:12px;margin:5px;">
[[Special:Contributions/{{PAGENAME}}|📜 MY EDITS]]
</div>
<div style="display:inline-block;background:#76200e;border:2px solid #ff762e;border-radius:8px;padding:12px;margin:5px;">
[[Special:RecentChanges|📰 RECENT CHANGES]]
</div>
<div style="display:inline-block;background:#76200e;border:2px solid #ff762e;border-radius:8px;padding:12px;margin:5px;">
[[Special:AllPages|📚 ALL PAGES]]
</div>
</div>
</div>
<div style="text-align:center;margin:28px 0 14px;font-size:25px;font-weight:bold;color:#ff762e;">
🐦🔥 ABOUT ME
</div>
<div style="background:#251010;border:1px solid #c94725;border-radius:10px;padding:18px;line-height:1.8;">
Welcome to my user page! 👋
I'm here to learn, contribute, improve the wiki, and help other editors along the way.
I believe everyone starts somewhere, and a friendly community makes learning easier. Feel free to explore my page and check out my contributions!
</div>
<div id="Mentor"></div>
<div style="text-align:center;margin:28px 0 14px;font-size:27px;font-weight:bold;color:#ff762e;">
🔥 MENTOR CENTER 🔥
</div>
<div style="background:#251010;border:2px solid #ff5722;border-left:6px solid #ff762e;border-radius:10px;padding:20px;">
<div style="font-size:22px;font-weight:bold;color:#ffb16e;">
Need help? I've got your back!
</div>
<p style="line-height:1.8;color:#ffe2cf;">
Hey there! Welcome to my Mentor Center. If you're new around here or need a hand, feel free to reach out!
</p>
<p style="line-height:1.8;color:#ffe2cf;">
I can try to help with wiki editing, wiki markup, formatting, finding your way around, and general questions about contributing.
</p>
<p style="line-height:1.8;color:#ffe2cf;">
<strong style="color:#ff8a43;">🔥 No question is too small.</strong> Don't be afraid to ask! Leave a message on my talk page, and I'll do my best to help when I can.
</p>
<div style="text-align:center;margin:18px 0;">
<div style="display:inline-block;background:#8b1a08;border:2px solid #ff9a50;border-radius:8px;padding:14px 20px;margin:5px;font-weight:bold;">
[[User talk:{{PAGENAME}}|💬 ASK YOUR MENTOR HERE]]
</div>
<div style="display:inline-block;background:#8b1a08;border:2px solid #ff9a50;border-radius:8px;padding:14px 20px;margin:5px;font-weight:bold;">
[[Special:Contributions/{{PAGENAME}}|📜 CHECK MY EDITS]]
</div>
</div>
<div style="text-align:center;font-size:16px;font-weight:bold;color:#ffb16e;padding:10px;">
⚔️ LEARN · HELP · IMPROVE · RISE ⚔️
</div>
</div>
<div style="text-align:center;margin:28px 0 14px;font-size:24px;font-weight:bold;color:#ff762e;">
👑 MY PHILOSOPHY
</div>
<div style="background:#190808;border:1px solid #87351d;border-radius:10px;padding:20px;text-align:center;line-height:1.8;color:#ffdec6;">
"Even the brightest phoenix began as ashes. Keep learning, keep helping, and never stop rising."
</div>
<div style="text-align:center;margin-top:25px;padding:20px;border-top:2px solid #ff5722;color:#ff9966;">
<div style="font-size:22px;font-weight:bold;">
🐦🔥 THE PHOENIX NEVER STAYS DOWN 🐦🔥
</div>
<div style="font-size:13px;margin-top:10px;">
HERE TO LEARN · HERE TO HELP · ALWAYS RISING
</div>
</div>
</div>
rwxarqjoipe1649sq397cc4608jvrd0
769493
769492
2026-10-21T14:54:05Z
Phoenix3452
76309
769493
wikitext
text/x-wiki
<div style="background:linear-gradient(145deg,#100707,#280909,#100707);border:3px solid #ff5722;border-radius:18px;padding:20px;color:#ffe4c4;font-family:sans-serif;">
<div style="text-align:center;background:linear-gradient(120deg,#170606,#651507,#250707);border:2px solid #ff762e;border-radius:14px;padding:30px 12px;">
<div style="font-size:13px;letter-spacing:5px;color:#ffd0a0;">
THE LEGEND RETURNS
</div>
<div style="font-size:42px;font-weight:bold;color:#ff792f;text-shadow:0 0 14px #ff2600;">
🐦🔥 PHOENIX
</div>
<div style="font-size:27px;font-weight:bold;color:#ffe0ad;">
RISING
</div>
<div style="color:#ffb080;margin:12px;">━━━━━━━━━━━━━━━━━━</div>
<div style="font-size:15px;letter-spacing:3px;color:#ffd0a0;">
LEARN • BUILD • HELP • RISE
</div>
</div>
<div style="text-align:center;margin:22px 0;">
<span style="display:inline-block;background:linear-gradient(135deg,#7b210d,#321008);border:2px solid #ff9c42;border-radius:8px;padding:12px 18px;margin:4px;color:#ffd6a0;font-weight:bold;">
👑 PHOENIX ELITE
</span>
<span style="display:inline-block;background:linear-gradient(135deg,#7b210d,#321008);border:2px solid #ff9c42;border-radius:8px;padding:12px 18px;margin:4px;color:#ffd6a0;font-weight:bold;">
🔥 COMMUNITY MENTOR
</span>
<span style="display:inline-block;background:linear-gradient(135deg,#7b210d,#321008);border:2px solid #ff9c42;border-radius:8px;padding:12px 18px;margin:4px;color:#ffd6a0;font-weight:bold;">
⚔️ WIKI EXPLORER
</span>
</div>
<div style="text-align:center;font-size:24px;font-weight:bold;color:#ff762e;margin:25px 0 12px;">
⚡ COMMAND CENTER
</div>
<div style="text-align:center;background:#200909;border:1px solid #87351d;border-radius:12px;padding:15px;">
<div style="display:inline-block;background:#76200e;border:2px solid #ff762e;border-radius:8px;padding:12px;margin:5px;">
[[User:{{PAGENAME}}|🏠 USER PAGE]]
</div>
<div style="display:inline-block;background:#76200e;border:2px solid #ff762e;border-radius:8px;padding:12px;margin:5px;">
[[User:{{PAGENAME}}#Mentor|🛡️ MENTOR DASHBOARD]]
</div>
<div style="display:inline-block;background:#76200e;border:2px solid #ff762e;border-radius:8px;padding:12px;margin:5px;">
[[User talk:{{PAGENAME}}|💬 MENTOR MESSAGES]]
</div>
<div style="display:inline-block;background:#76200e;border:2px solid #ff762e;border-radius:8px;padding:12px;margin:5px;">
[[User:{{PAGENAME}}/sandbox|🔥 PHOENIX SANDBOX]]
</div>
<div style="display:inline-block;background:#76200e;border:2px solid #ff762e;border-radius:8px;padding:12px;margin:5px;">
[[Special:Contributions/{{PAGENAME}}|📜 MY CONTRIBUTIONS]]
</div>
<div style="display:inline-block;background:#76200e;border:2px solid #ff762e;border-radius:8px;padding:12px;margin:5px;">
[[Special:RecentChanges|📰 RECENT CHANGES]]
</div>
</div>
<div style="text-align:center;font-size:24px;font-weight:bold;color:#ff762e;margin:25px 0 12px;">
🔥 ABOUT THE PHOENIX
</div>
<div style="background:#251010;border:1px solid #c94725;border-radius:12px;padding:20px;line-height:1.9;">
Welcome to my corner of the wiki! 🐦🔥
I'm here to learn, contribute, experiment with wiki markup, and help make the community a better place.
I enjoy creating custom page designs, testing new ideas in my sandbox, and helping fellow editors figure things out.
<div style="text-align:center;font-size:17px;font-weight:bold;color:#ffb16e;margin-top:15px;">
RISE ABOVE. KEEP LEARNING. NEVER STOP.
</div>
</div>
<div id="Mentor"></div>
<div style="text-align:center;font-size:25px;font-weight:bold;color:#ff762e;margin:25px 0 12px;">
🛡️ MENTOR DASHBOARD
</div>
<div style="background:#251010;border:2px solid #ff5722;border-left:6px solid #ff762e;border-radius:12px;padding:20px;line-height:1.9;">
<div style="font-size:22px;font-weight:bold;color:#ffb16e;">
🔥 NEED HELP? I'VE GOT YOUR BACK!
</div>
Welcome! If you're new to the wiki or need a hand with editing, formatting, wiki markup, or finding your way around, feel free to contact me.
Everyone starts somewhere. Ask questions, keep learning, and don't be afraid to experiment!
<div style="text-align:center;margin:20px 0;">
<div style="display:inline-block;background:#8b1a08;border:2px solid #ff9a50;border-radius:8px;padding:14px;margin:5px;font-weight:bold;">
[[User talk:{{PAGENAME}}|💬 ASK YOUR MENTOR]]
</div>
<div style="display:inline-block;background:#8b1a08;border:2px solid #ff9a50;border-radius:8px;padding:14px;margin:5px;font-weight:bold;">
[[User:{{PAGENAME}}/sandbox|🛠️ OPEN MY SANDBOX]]
</div>
</div>
<div style="text-align:center;color:#ffb16e;font-weight:bold;">
⚔️ LEARN • HELP • IMPROVE • RISE ⚔️
</div>
</div>
<div style="text-align:center;font-size:24px;font-weight:bold;color:#ff762e;margin:25px 0 12px;">
🏆 ACHIEVEMENT VAULT
</div>
<div style="background:#190808;border:1px solid #87351d;border-radius:12px;padding:18px;line-height:1.9;">
<div style="color:#ffb16e;font-weight:bold;">MY WIKI GOALS</div>
* 🔥 Improve my editing skills
* 🐦🔥 Build creative user-page designs
* ⚔️ Help fellow editors when possible
* 🛠️ Experiment with new ideas in my sandbox
<div style="font-size:12px;color:#c9a18a;margin-top:12px;">
Add real achievements and milestones here as you accomplish them.
</div>
</div>
<div style="text-align:center;font-size:24px;font-weight:bold;color:#ff762e;margin:25px 0 12px;">
📜 THE PHOENIX CODE
</div>
<div style="background:linear-gradient(135deg,#2b0b08,#160707);border:1px solid #c94725;border-radius:12px;padding:22px;text-align:center;line-height:2;color:#ffdec6;">
"Even the brightest phoenix began as ashes."
Learn with patience.
Help with kindness.
Build with creativity.
Rise with every challenge.
</div>
<div style="text-align:center;margin-top:25px;padding:22px;border-top:2px solid #ff5722;">
<div style="font-size:25px;font-weight:bold;color:#ff792f;">
🐦🔥 THE PHOENIX NEVER STAYS DOWN 🔥
</div>
<div style="font-size:13px;letter-spacing:2px;color:#ffb080;margin-top:10px;">
BORN TO LEARN • BUILT TO RISE
</div>
<div style="margin-top:14px;font-size:12px;color:#d9a18a;">
Thanks for stopping by. Leave a message if you need help!
</div>
</div>
</div>
c6i1xx3kg2477flv8izlqprm4j7q0hu
Wikipedia:Requests/Permissions/Ahecht
4
179362
769620
769289
2026-10-22T10:12:40Z
EPIC
44279
done
769620
wikitext
text/x-wiki
{{Request-done|1=[[User:EPIC|EPIC]] ([[User talk:EPIC|talk]]) 10:12, 22 October 2026 (UTC)|2=Reasonable rationale, sysop on a content wiki}}
=== [[User:Ahecht|Ahecht]] ===
* {{User3|Ahecht}}, [[Special:CentralAuth/Ahecht|global contribs]] 14:00, 20 October 2026 (UTC)
* '''Motive for request:''' I want to test templates/modules for generating [[:mw:Help:Cite/VisualEditor reference names|VisualEditor reference names]] before proposing them for rollout on the English Wikipedia. This requires editing pages such as [[MediaWiki:Visualeditor-cite-tool-name-book]]
* '''Requested rights:''' Administrator
* '''Comments:''' Current Administrator and long-time template editor on the English Wikipedia
<!-- DO NOT FORGET to transclude your request at THE TOP of [[Wikipedia:Requests/Permissions]] or nobody will see it! -->
[[Category:!Requests]]
[[Category:Really big category]]
__NOINDEX__
{{Request closed}}
1rqsvzc7680jwu8kd5599b88ofbjg4z
Ibero-American MAB Network (IberoMAB)
0
179378
769619
769395
2026-10-22T10:12:16Z
AtomAnt9
76333
.
769619
wikitext
text/x-wiki
== Geographic scope and membership ==
'''This section is still under construction'''
== Objectives and activities ==
The main objectives of IberoMAB are the enhancement of the UNESCO's Man and the Biosphere Programme implementation in the member states. These are:
* Enhancement of the national committees implementing the MAB programme.
* Promotion of cooperation and communication among the member states and biosphere reserves.
* Promotion of the creation and development of biosphere reserves.
* Promotion of biodiversity conservation and sustainable development.
* Promotion of the exchange of scientific information and experiences.<ref name="UNESCO" />
25qvsxjbu6x65ok7xsy2kq859059chh
769625
769619
2026-10-22T10:17:03Z
AtomAnt9
76333
history
769625
wikitext
text/x-wiki
History ==
The Ibero-American MAB Network was created in 1992 to strengthen cooperation among countries that're part of UNESCOs Man and the Biosphere Programme. Its creation provided a structure for sharing experiences and supporting biosphere reserves across Latin America, the Caribbean, Spain and Portugal.<ref name="UNESCO2" />
In 2018 the network’s Action Plan for 2018–2025 was approved at the XVIII IberoMAB Meeting held in Santa Marta, Colombia. This plan set out a framework for cooperation and joint activities among member countries.<ref name="UNESCO2" />
In October 2020 IberoMAB released a declaration that addressed the challenges of the COVID-19 pandemic and its impact on communities and the environment. The declaration highlighted the importance of recovery, community involvement, scientific research and the role of biosphere reserves in protecting biodiversity and promoting human well-being.<ref name="Declaration">{{cite web |date=15 October 2020 |title=IberoMAB Biosphere Reserves pledge for a sustainable 'new normal' to overcome the COVID19 crisis |url=https://www.unesco.org/en/articles/iberomab-biosphere-reserves-pledge-sustainable-new-normal-overcome-covid19-crisis |access-date=21 October 2026 |website=UNESCO}}</ref>
In 2023 the network organized its congress in the Trifinio-Fraternidad Transboundary Biosphere Reserve. Delegates reviewed progress, on collaboration and looked at how well the Lima Action Plan 2016–2025 and the IberoMAB Action Plan 2018–2025 were being carried out.<ref name="UNESCO2" />
== Geographic scope and membership ==
'''This section is still under construction'''
== Objectives and activities ==
The main objectives of IberoMAB are the enhancement of the UNESCO's Man and the Biosphere Programme implementation in the member states. These are:
* Enhancement of the national committees implementing the MAB programme.
* Promotion of cooperation and communication among the member states and biosphere reserves.
* Promotion of the creation and development of biosphere reserves.
* Promotion of biodiversity conservation and sustainable development.
* Promotion of the exchange of scientific information and experiences.<ref name="UNESCO" />
moce1ea5pxl2onzw9z647yp2h32dpaq
769628
769625
2026-10-22T10:18:10Z
OrangeLemonadee
76341
769628
wikitext
text/x-wiki
History
The Ibero-American MAB Network was created in 1992 to strengthen cooperation among countries that're part of UNESCOs Man and the Biosphere Programme. Its creation provided a structure for sharing experiences and supporting biosphere reserves across Latin America, the Caribbean, Spain and Portugal.<ref name="UNESCO2" />
In 2018 the network’s Action Plan for 2018–2025 was approved at the XVIII IberoMAB Meeting held in Santa Marta, Colombia. This plan set out a framework for cooperation and joint activities among member countries.<ref name="UNESCO2" />
In October 2020 IberoMAB released a declaration that addressed the challenges of the COVID-19 pandemic and its impact on communities and the environment. The declaration highlighted the importance of recovery, community involvement, scientific research and the role of biosphere reserves in protecting biodiversity and promoting human well-being.<ref name="Declaration">{{cite web |date=15 October 2020 |title=IberoMAB Biosphere Reserves pledge for a sustainable 'new normal' to overcome the COVID19 crisis |url=https://www.unesco.org/en/articles/iberomab-biosphere-reserves-pledge-sustainable-new-normal-overcome-covid19-crisis |access-date=21 October 2026 |website=UNESCO}}</ref>
In 2023 the network organized its congress in the Trifinio-Fraternidad Transboundary Biosphere Reserve. Delegates reviewed progress, on collaboration and looked at how well the Lima Action Plan 2016–2025 and the IberoMAB Action Plan 2018–2025 were being carried out.<ref name="UNESCO2" />
== Geographic scope and membership ==
'''This section is still under construction'''
== Objectives and activities ==
The main objectives of IberoMAB are the enhancement of the UNESCO's Man and the Biosphere Programme implementation in the member states. These are:
* Enhancement of the national committees implementing the MAB programme.
* Promotion of cooperation and communication among the member states and biosphere reserves.
* Promotion of the creation and development of biosphere reserves.
* Promotion of biodiversity conservation and sustainable development.
* Promotion of the exchange of scientific information and experiences.<ref name="UNESCO" />
tdkak0d9xqqao80axxs25x4xs2nej5s
769629
769628
2026-10-22T10:19:25Z
OrangeLemonadee
76341
769629
wikitext
text/x-wiki
== History ==
The Ibero-American MAB Network was created in 1992 to strengthen cooperation among countries that're part of UNESCOs Man and the Biosphere Programme. Its creation provided a structure for sharing experiences and supporting biosphere reserves across Latin America, the Caribbean, Spain and Portugal.<ref name="UNESCO2" />
In 2018 the network’s Action Plan for 2018–2025 was approved at the XVIII IberoMAB Meeting held in Santa Marta, Colombia. This plan set out a framework for cooperation and joint activities among member countries.<ref name="UNESCO2" />
In October 2020 IberoMAB released a declaration that addressed the challenges of the COVID-19 pandemic and its impact on communities and the environment. The declaration highlighted the importance of recovery, community involvement, scientific research and the role of biosphere reserves in protecting biodiversity and promoting human well-being.<ref name="Declaration">{{cite web |date=15 October 2020 |title=IberoMAB Biosphere Reserves pledge for a sustainable 'new normal' to overcome the COVID19 crisis |url=https://www.unesco.org/en/articles/iberomab-biosphere-reserves-pledge-sustainable-new-normal-overcome-covid19-crisis |access-date=21 October 2026 |website=UNESCO}}</ref>
In 2023 the network organized its congress in the Trifinio-Fraternidad Transboundary Biosphere Reserve. Delegates reviewed progress, on collaboration and looked at how well the Lima Action Plan 2016–2025 and the IberoMAB Action Plan 2018–2025 were being carried out.<ref name="UNESCO2" />
== Geographic scope and membership ==
'''This section is still under construction'''
== Objectives and activities ==
The main objectives of IberoMAB are the enhancement of the UNESCO's Man and the Biosphere Programme implementation in the member states. These are:
* Enhancement of the national committees implementing the MAB programme.
* Promotion of cooperation and communication among the member states and biosphere reserves.
* Promotion of the creation and development of biosphere reserves.
* Promotion of biodiversity conservation and sustainable development.
* Promotion of the exchange of scientific information and experiences.<ref name="UNESCO" />
fesxwfnxvus7z81m3gikmv1ny6mzjl8
769640
769629
2026-10-22T10:27:57Z
OrangeLemonadee
76341
769640
wikitext
text/x-wiki
== History ==
The Ibero-American MAB Network was created in 1992 to strengthen cooperation among countries that're part of UNESCOs Man and the Biosphere Programme. Its creation provided a structure for sharing experiences and supporting biosphere reserves across Latin America, the Caribbean, Spain and Portugal.|url=https://www.unesco.org/en/articles/iberomab-biosphere-reserves-pledge-sustainable-new-normal-overcome-covid19-crisis |access-date=21 October 2026 |website=UNESCO}}</ref>
In 2018 the network’s Action Plan for 2018–2025 was approved at the XVIII IberoMAB Meeting held in Santa Marta, Colombia. This plan set out a framework for cooperation and joint activities among member countries.|url=https://www.unesco.org/en/articles/iberomab-biosphere-reserves-pledge-sustainable-new-normal-overcome-covid19-crisis |access-date=21 October 2026 |website=UNESCO}}</ref>
In October 2020 IberoMAB released a declaration that addressed the challenges of the COVID-19 pandemic and its impact on communities and the environment. The declaration highlighted the importance of recovery, community involvement, scientific research and the role of biosphere reserves in protecting biodiversity and promoting human well-being.<ref name="Declaration">{{cite web |date=15 October 2020 |title=IberoMAB Biosphere Reserves pledge for a sustainable 'new normal' to overcome the COVID19 crisis |url=https://www.unesco.org/en/articles/iberomab-biosphere-reserves-pledge-sustainable-new-normal-overcome-covid19-crisis |access-date=21 October 2026 |website=UNESCO}}</ref>
In 2023 the network organized its congress in the Trifinio-Fraternidad Transboundary Biosphere Reserve. Delegates reviewed progress, on collaboration and looked at how well the Lima Action Plan 2016–2025 and the IberoMAB Action Plan 2018–2025 were being carried out.<ref name="UNESCO2" />
== Geographic scope and membership ==
'''This section is still under construction'''
== Objectives and activities ==
The main objectives of IberoMAB are the enhancement of the UNESCO's Man and the Biosphere Programme implementation in the member states. These are:
* Enhancement of the national committees implementing the MAB programme.
* Promotion of cooperation and communication among the member states and biosphere reserves.
* Promotion of the creation and development of biosphere reserves.
* Promotion of biodiversity conservation and sustainable development.
* Promotion of the exchange of scientific information and experiences.<ref name="UNESCO" />
3rdtaebx4zb2ao98ae9hdz5u4xo1avk
769644
769640
2026-10-22T10:29:49Z
OrangeLemonadee
76341
769644
wikitext
text/x-wiki
== History ==
The Ibero-American MAB Network was created in 1992 to strengthen cooperation among countries that're part of UNESCOs Man and the Biosphere Programme. Its creation provided a structure for sharing experiences and supporting biosphere reserves across Latin America, the Caribbean, Spain and Portugal.|url=https://www.unesco.org/en/mab |access-date=21 October 2026 |website=UNESCO}}</ref>
In 2018 the network’s Action Plan for 2018–2025 was approved at the XVIII IberoMAB Meeting held in Santa Marta, Colombia. This plan set out a framework for cooperation and joint activities among member countries.|url=https://www.unesco.org/en/articles/iberomab-biosphere-reserves-pledge-sustainable-new-normal-overcome-covid19-crisis |access-date=21 October 2026 |website=UNESCO}}</ref>
In October 2020 IberoMAB released a declaration that addressed the challenges of the COVID-19 pandemic and its impact on communities and the environment. The declaration highlighted the importance of recovery, community involvement, scientific research and the role of biosphere reserves in protecting biodiversity and promoting human well-being.<ref name="Declaration">{{cite web |date=15 October 2020 |title=IberoMAB Biosphere Reserves pledge for a sustainable 'new normal' to overcome the COVID19 crisis |url=https://www.unesco.org/en/articles/iberomab-biosphere-reserves-pledge-sustainable-new-normal-overcome-covid19-crisis |access-date=21 October 2026 |website=UNESCO}}</ref>
In 2023 the network organized its congress in the Trifinio-Fraternidad Transboundary Biosphere Reserve. Delegates reviewed progress, on collaboration and looked at how well the Lima Action Plan 2016–2025 and the IberoMAB Action Plan 2018–2025 were being carried out.<ref name="UNESCO2" />
== Geographic scope and membership ==
'''This section is still under construction'''
== Objectives and activities ==
The main objectives of IberoMAB are the enhancement of the UNESCO's Man and the Biosphere Programme implementation in the member states. These are:
* Enhancement of the national committees implementing the MAB programme.
* Promotion of cooperation and communication among the member states and biosphere reserves.
* Promotion of the creation and development of biosphere reserves.
* Promotion of biodiversity conservation and sustainable development.
* Promotion of the exchange of scientific information and experiences.<ref name="UNESCO" />
q1ibccb1n15dr8hb0hjqzvclxenlntn
769662
769644
2026-10-22T10:37:19Z
AtomAnt9
76333
.
769662
wikitext
text/x-wiki
== History ==
The Ibero-American MAB Network was created in 1992 to strengthen cooperation among countries that're part of UNESCOs Man and the Biosphere Programme. Its creation provided a structure for sharing experiences and supporting biosphere reserves across Latin America, the Caribbean, Spain and Portugal.
<ref>{{cite web |title=... |url=[https://www.unesco.org/en/mab](https://www.unesco.org/en/mab) |access-date=21 October 2026 |website=UNESCO}}</ref>
In 2018 the network’s Action Plan for 2018–2025 was approved at the XVIII IberoMAB Meeting held in Santa Marta, Colombia. This plan set out a framework for cooperation and joint activities among member countries.|url=https://www.unesco.org/en/articles/iberomab-biosphere-reserves-pledge-sustainable-new-normal-overcome-covid19-crisis |access-date=21 October 2026 |website=UNESCO}}</ref>
In October 2020 IberoMAB released a declaration that addressed the challenges of the COVID-19 pandemic and its impact on communities and the environment. The declaration highlighted the importance of recovery, community involvement, scientific research and the role of biosphere reserves in protecting biodiversity and promoting human well-being.<ref name="Declaration">{{cite web |date=15 October 2020 |title=IberoMAB Biosphere Reserves pledge for a sustainable 'new normal' to overcome the COVID19 crisis |url=https://www.unesco.org/en/articles/iberomab-biosphere-reserves-pledge-sustainable-new-normal-overcome-covid19-crisis |access-date=21 October 2026 |website=UNESCO}}</ref>
In 2023 the network organized its congress in the Trifinio-Fraternidad Transboundary Biosphere Reserve. Delegates reviewed progress, on collaboration and looked at how well the Lima Action Plan 2016–2025 and the IberoMAB Action Plan 2018–2025 were being carried out.<ref name="UNESCO2" />
== Geographic scope and membership ==
'''This section is still under construction'''
== Objectives and activities ==
The main objectives of IberoMAB are the enhancement of the UNESCO's Man and the Biosphere Programme implementation in the member states. These are:
* Enhancement of the national committees implementing the MAB programme.
* Promotion of cooperation and communication among the member states and biosphere reserves.
* Promotion of the creation and development of biosphere reserves.
* Promotion of biodiversity conservation and sustainable development.
* Promotion of the exchange of scientific information and experiences.<ref name="UNESCO" />
gdxzozwceb6mjg1040ews681krhwqq8
769674
769662
2026-10-22T10:44:57Z
AtomAnt9
76333
769674
wikitext
text/x-wiki
== History ==
The Ibero-American MAB Network was created in 1992 to strengthen cooperation among countries that're part of UNESCOs Man and the Biosphere Programme. Its creation provided a structure for sharing experiences and supporting biosphere reserves across Latin America, the Caribbean, Spain and Portugal. <ref>{{cite web |title=Man and the Biosphere Programme |url=https://www.unesco.org/en/mab |access-date=21 October 2026 |publisher=UNESCO}}</ref>
In 2018 the network’s Action Plan for 2018–2025 was approved at the XVIII IberoMAB Meeting held in Santa Marta, Colombia. This plan set out a framework for cooperation and joint activities among member countries. <ref>{{cite web |title=Man and the Biosphere Programme |url=https://www.unesco.org/en/mab |access-date=21 October 2026 |publisher=UNESCO}}</ref>
In October 2020 IberoMAB released a declaration that addressed the challenges of the COVID-19 pandemic and its impact on communities and the environment. The declaration highlighted the importance of recovery, community involvement, scientific research and the role of biosphere reserves in protecting biodiversity and promoting human well-being. <ref name="iberomab-covid">{{cite web |date=15 October 2020 |title=IberoMAB Biosphere Reserves pledge for a sustainable 'new normal' to overcome the COVID19 crisis |url=https://www.unesco.org/en/articles/iberomab-biosphere-reserves-pledge-sustainable-new-normal-overcome-covid19-crisis |access-date=21 October 2026 |publisher=UNESCO}}</ref>
In 2023 the network organized its congress in the Trifinio-Fraternidad Transboundary Biosphere Reserve. Delegates reviewed progress, on collaboration and looked at how well the Lima Action Plan 2016–2025 and the IberoMAB Action Plan 2018–2025 were being carried out. <ref name="iberomab-covid2">{{cite web |date=15 October 2020 |title=IberoMAB Biosphere Reserves pledge for a sustainable 'new normal' to overcome the COVID19 crisis |url=https://www.unesco.org/en/articles/iberomab-biosphere-reserves-pledge-sustainable-new-normal-overcome-covid19-crisis |access-date=21 October 2026 |publisher=UNESCO}}</ref>
== Geographic scope and membership ==
'''This section is still under construction'''
== Objectives and activities ==
The main objectives of IberoMAB are the enhancement of the UNESCO's Man and the Biosphere Programme implementation in the member states. These are:
* Enhancement of the national committees implementing the MAB programme.
* Promotion of cooperation and communication among the member states and biosphere reserves.
* Promotion of the creation and development of biosphere reserves.
* Promotion of biodiversity conservation and sustainable development.
* Promotion of the exchange of scientific information and experiences.<ref name="UNESCO" />
hj8bu6dzwt110ij4y34mgbx7ipfxyz7
769675
769674
2026-10-22T10:46:47Z
AtomAnt9
76333
.
769675
wikitext
text/x-wiki
== History ==
The Ibero-American MAB Network was created in 1992 to strengthen cooperation among countries that're part of UNESCOs Man and the Biosphere Programme. Its creation provided a structure for sharing experiences and supporting biosphere reserves across Latin America, the Caribbean, Spain and Portugal. <ref>{{cite web |title=Man and the Biosphere Programme |url=https://www.unesco.org/en/mab |access-date=21 October 2026 |publisher=UNESCO}}</ref>
In 2018 the network’s Action Plan for 2018–2025 was approved at the XVIII IberoMAB Meeting held in Santa Marta, Colombia. This plan set out a framework for cooperation and joint activities among member countries. <ref>{{cite web |title=Man and the Biosphere Programme |url=https://www.unesco.org/en/mab |access-date=21 October 2026 |publisher=UNESCO}}</ref>
In October 2020 IberoMAB released a declaration that addressed the challenges of the COVID-19 pandemic and its impact on communities and the environment. The declaration highlighted the importance of recovery, community involvement, scientific research and the role of biosphere reserves in protecting biodiversity and promoting human well-being. <ref name="iberomab-covid">{{cite web |date=15 October 2020 |title=IberoMAB Biosphere Reserves pledge for a sustainable 'new normal' to overcome the COVID19 crisis |url=https://www.unesco.org/en/articles/iberomab-biosphere-reserves-pledge-sustainable-new-normal-overcome-covid19-crisis |access-date=21 October 2026 |publisher=UNESCO}}</ref>
In 2023 the network organized its congress in the Trifinio-Fraternidad Transboundary Biosphere Reserve. Delegates reviewed progress, on collaboration and looked at how well the Lima Action Plan 2016–2025 and the IberoMAB Action Plan 2018–2025 were being carried out. <ref name="iberomab-covid2">{{cite web |date=15 October 2020 |title=IberoMAB Biosphere Reserves pledge for a sustainable 'new normal' to overcome the COVID19 crisis |url=https://www.unesco.org/en/articles/iberomab-biosphere-reserves-pledge-sustainable-new-normal-overcome-covid19-crisis |access-date=21 October 2026 |publisher=UNESCO}}</ref>
== Geographic scope and membership ==
'''This section is still under construction'''
== Objectives and activities ==
The main objectives of IberoMAB are the enhancement of the UNESCO's Man and the Biosphere Programme implementation in the member states. These are:
* Enhancement of the national committees implementing the MAB programme.
* Promotion of cooperation and communication among the member states and biosphere reserves.
* Promotion of the creation and development of biosphere reserves.
* Promotion of biodiversity conservation and sustainable development.
* Promotion of the exchange of scientific information and experiences.<ref name="UNESCO" />
== Organization and cooperation ==
IberoMAB is part of UNESCO's Man and the Biosphere (MAB) Programme and brings together national MAB committees and representatives of participating countries. It promotes regional cooperation, knowledge exchange and coordination on shared priorities related to biosphere reserves and sustainable development.<ref name="UNESCO2" />
The IberoMAB Network Bureau coordinates the network's activities. UNESCO lists Honduras as President, Peru as Vice-President and Spain as the Secretariat.<ref name="UNESCO2" />
IberoMAB also cooperates with thematic networks focused on island and coastal biosphere reserves, mountain biosphere reserves and youth. These collaborations aim to strengthen cooperation and share good practices in biodiversity conservation, biosphere reserve management and sustainable development.<ref>{{cite web |title=MAB Secretariat support to strategic initiatives for sustainable development |url=https://core.unesco.org/en/project/3210211102 |access-date=22 October 2026 |website=UNESCO Core Data Portal}}</ref>
5u5sjknvtjv3pseyxvgoskb7x75vheu
769689
769675
2026-10-22T11:06:33Z
AtomAnt9
76333
769689
wikitext
text/x-wiki
== History ==
The Ibero-American MAB Network was created in 1992 to strengthen cooperation among countries that're part of UNESCOs Man and the Biosphere Programme. Its creation provided a structure for sharing experiences and supporting biosphere reserves across Latin America, the Caribbean, Spain and Portugal. <ref>{{cite web |title=Man and the Biosphere Programme |url=https://www.unesco.org/en/mab |access-date=21 October 2026 |publisher=UNESCO}}</ref>
In 2018 the network’s Action Plan for 2018–2025 was approved at the XVIII IberoMAB Meeting held in Santa Marta, Colombia. This plan set out a framework for cooperation and joint activities among member countries. <ref>{{cite web |title=Man and the Biosphere Programme |url=https://www.unesco.org/en/mab |access-date=21 October 2026 |publisher=UNESCO}}</ref>
In October 2020 IberoMAB released a declaration that addressed the challenges of the COVID-19 pandemic and its impact on communities and the environment. The declaration highlighted the importance of recovery, community involvement, scientific research and the role of biosphere reserves in protecting biodiversity and promoting human well-being. <ref name="iberomab-covid">{{cite web |date=15 October 2020 |title=IberoMAB Biosphere Reserves pledge for a sustainable 'new normal' to overcome the COVID19 crisis |url=https://www.unesco.org/en/articles/iberomab-biosphere-reserves-pledge-sustainable-new-normal-overcome-covid19-crisis |access-date=21 October 2026 |publisher=UNESCO}}</ref>
In 2023 the network organized its congress in the Trifinio-Fraternidad Transboundary Biosphere Reserve. Delegates reviewed progress, on collaboration and looked at how well the Lima Action Plan 2016–2025 and the IberoMAB Action Plan 2018–2025 were being carried out. <ref name="iberomab-covid2">{{cite web |date=15 October 2020 |title=IberoMAB Biosphere Reserves pledge for a sustainable 'new normal' to overcome the COVID19 crisis |url=https://www.unesco.org/en/articles/iberomab-biosphere-reserves-pledge-sustainable-new-normal-overcome-covid19-crisis |access-date=21 October 2026 |publisher=UNESCO}}</ref>
== Geographic scope and membership ==
'''This section is still under construction'''
== Objectives and activities ==
The main objectives of IberoMAB are the enhancement of the UNESCO's Man and the Biosphere Programme implementation in the member states. These are:
* Enhancement of the national committees implementing the MAB programme.
* Promotion of cooperation and communication among the member states and biosphere reserves.
* Promotion of the creation and development of biosphere reserves.
* Promotion of biodiversity conservation and sustainable development.
* Promotion of the exchange of scientific information and experiences.<ref name="UNESCO" />
== Organization and cooperation ==
IberoMAB is part of UNESCO's Man and the Biosphere (MAB) Programme and brings together national MAB committees and representatives of participating countries. It promotes regional cooperation, knowledge exchange and coordination on shared priorities related to biosphere reserves and sustainable development.<ref name="UNESCO2" />
The IberoMAB Network Bureau coordinates the network's activities. UNESCO lists Honduras as President, Peru as Vice-President and Spain as the Secretariat.<ref name="UNESCO2" />
IberoMAB also cooperates with thematic networks focused on island and coastal biosphere reserves, mountain biosphere reserves and youth. These collaborations aim to strengthen cooperation and share good practices in biodiversity conservation, biosphere reserve management and sustainable development.<ref>{{cite web |title=MAB Secretariat support to strategic initiatives for sustainable development |url=https://core.unesco.org/en/project/3210211102 |access-date=22 October 2026 |website=UNESCO Core Data Portal}}</ref>
ke9g3b6hp3yvdp3gg18ykjy3jhbb918
769691
769689
2026-10-22T11:07:33Z
AtomAnt9
76333
769691
wikitext
text/x-wiki
== History ==
The Ibero-American MAB Network was created in 1992 to strengthen cooperation among countries that're part of UNESCOs Man and the Biosphere Programme. Its creation provided a structure for sharing experiences and supporting biosphere reserves across Latin America, the Caribbean, Spain and Portugal. <ref>{{cite web |title=Man and the Biosphere Programme |url=https://www.unesco.org/en/mab |access-date=21 October 2026 |publisher=UNESCO}}</ref>
In 2018 the network’s Action Plan for 2018–2025 was approved at the XVIII IberoMAB Meeting held in Santa Marta, Colombia. This plan set out a framework for cooperation and joint activities among member countries. <ref>{{cite web |title=Man and the Biosphere Programme |url=https://www.unesco.org/en/mab |access-date=21 October 2026 |publisher=UNESCO}}</ref>
In October 2020 IberoMAB released a declaration that addressed the challenges of the COVID-19 pandemic and its impact on communities and the environment. The declaration highlighted the importance of recovery, community involvement, scientific research and the role of biosphere reserves in protecting biodiversity and promoting human well-being. <ref name="iberomab-covid">{{cite web |date=15 October 2020 |title=IberoMAB Biosphere Reserves pledge for a sustainable 'new normal' to overcome the COVID19 crisis |url=https://www.unesco.org/en/articles/iberomab-biosphere-reserves-pledge-sustainable-new-normal-overcome-covid19-crisis |access-date=21 October 2026 |publisher=UNESCO}}</ref>
In 2023 the network organized its congress in the Trifinio-Fraternidad Transboundary Biosphere Reserve. Delegates reviewed progress, on collaboration and looked at how well the Lima Action Plan 2016–2025 and the IberoMAB Action Plan 2018–2025 were being carried out. <ref name="iberomab-covid2">{{cite web |date=15 October 2020 |title=IberoMAB Biosphere Reserves pledge for a sustainable 'new normal' to overcome the COVID19 crisis |url=https://www.unesco.org/en/articles/iberomab-biosphere-reserves-pledge-sustainable-new-normal-overcome-covid19-crisis |access-date=21 October 2026 |publisher=UNESCO}}</ref>
== Geographic scope and membership ==
'''This section is still under construction'''
== Objectives and activities ==
The main objectives of IberoMAB are the enhancement of the UNESCO's Man and the Biosphere Programme implementation in the member states. These are:
* Enhancement of the national committees implementing the MAB programme.
* Promotion of cooperation and communication among the member states and biosphere reserves.
* Promotion of the creation and development of biosphere reserves.
* Promotion of biodiversity conservation and sustainable development.
* Promotion of the exchange of scientific information and experiences.<ref>{{cite web |title=Man and the Biosphere Programme |url=https://www.unesco.org/en/mab |access-date=21 October 2026 |publisher=UNESCO}}</ref>
== Organization and cooperation ==
IberoMAB is part of UNESCO's Man and the Biosphere (MAB) Programme and brings together national MAB committees and representatives of participating countries. It promotes regional cooperation, knowledge exchange and coordination on shared priorities related to biosphere reserves and sustainable development.<ref name="UNESCO2" />
The IberoMAB Network Bureau coordinates the network's activities. UNESCO lists Honduras as President, Peru as Vice-President and Spain as the Secretariat.<ref name="UNESCO2" />
IberoMAB also cooperates with thematic networks focused on island and coastal biosphere reserves, mountain biosphere reserves and youth. These collaborations aim to strengthen cooperation and share good practices in biodiversity conservation, biosphere reserve management and sustainable development.<ref>{{cite web |title=MAB Secretariat support to strategic initiatives for sustainable development |url=https://core.unesco.org/en/project/3210211102 |access-date=22 October 2026 |website=UNESCO Core Data Portal}}</ref>
li68h5pwyvaq4lsrgqdfdsm69nx7dk0
769703
769691
2026-10-22T11:22:44Z
AtomAnt9
76333
769703
wikitext
text/x-wiki
== History ==
The Ibero-American MAB Network was created in 1992 to strengthen cooperation among countries that're part of UNESCOs Man and the Biosphere Programme. Its creation provided a structure for sharing experiences and supporting biosphere reserves across Latin America, the Caribbean, Spain and Portugal. <ref name="MAB">{{cite web |url=https://www.unesco.org/en/mab |access-date=21 October 2026 |publisher=UNESCO}}</ref>
In 2018 the network’s Action Plan for 2018–2025 was approved at the XVIII IberoMAB Meeting held in Santa Marta, Colombia. This plan set out a framework for cooperation and joint activities among member countries. <ref name="MAB"></ref>
In October 2020 IberoMAB released a declaration that addressed the challenges of the COVID-19 pandemic and its impact on communities and the environment. The declaration highlighted the importance of recovery, community involvement, scientific research and the role of biosphere reserves in protecting biodiversity and promoting human well-being. <ref name="iberomab-covid">{{cite web |date=15 October 2020 |title=IberoMAB Biosphere Reserves pledge for a sustainable 'new normal' to overcome the COVID19 crisis |url=https://www.unesco.org/en/articles/iberomab-biosphere-reserves-pledge-sustainable-new-normal-overcome-covid19-crisis |access-date=21 October 2026 |publisher=UNESCO}}</ref>
In 2023 the network organized its congress in the Trifinio-Fraternidad Transboundary Biosphere Reserve. Delegates reviewed progress, on collaboration and looked at how well the Lima Action Plan 2016–2025 and the IberoMAB Action Plan 2018–2025 were being carried out. <ref name="iberomab-covid2">{{cite web |date=15 October 2020 |title=IberoMAB Biosphere Reserves pledge for a sustainable 'new normal' to overcome the COVID19 crisis |url=https://www.unesco.org/en/articles/iberomab-biosphere-reserves-pledge-sustainable-new-normal-overcome-covid19-crisis |access-date=21 October 2026 |publisher=UNESCO}}</ref>
== Geographic scope and membership ==
'''This section is still under construction'''
== Objectives and activities ==
The main objectives of IberoMAB are the enhancement of the UNESCO's Man and the Biosphere Programme implementation in the member states. These are:
* Enhancement of the national committees implementing the MAB programme.
* Promotion of cooperation and communication among the member states and biosphere reserves.
* Promotion of the creation and development of biosphere reserves.
* Promotion of biodiversity conservation and sustainable development.
* Promotion of the exchange of scientific information and experiences.<ref name="MAB"></ref>
== Organization and cooperation ==
IberoMAB is part of UNESCO's Man and the Biosphere (MAB) Programme and brings together national MAB committees and representatives of participating countries. It promotes regional cooperation, knowledge exchange and coordination on shared priorities related to biosphere reserves and sustainable development.<ref name="UNESCO2" />
The IberoMAB Network Bureau coordinates the network's activities. UNESCO lists Honduras as President, Peru as Vice-President and Spain as the Secretariat.<ref name="UNESCO2" />
IberoMAB also cooperates with thematic networks focused on island and coastal biosphere reserves, mountain biosphere reserves and youth. These collaborations aim to strengthen cooperation and share good practices in biodiversity conservation, biosphere reserve management and sustainable development.<ref>{{cite web |title=MAB Secretariat support to strategic initiatives for sustainable development |url=https://core.unesco.org/en/project/3210211102 |access-date=22 October 2026 |website=UNESCO Core Data Portal}}</ref>
9lewcctxddil8136rdx9ae1l6w0awqs
769706
769703
2026-10-22T11:24:22Z
AtomAnt9
76333
769706
wikitext
text/x-wiki
== Ibero-American MAB Network ==
The '''Ibero-American MAB Network''' ('''IberoMAB''') is a regional network established in 1992 under UNESCO's Man and the Biosphere (MAB) Programme. It connects national MAB committees and biosphere reserves in Latin America, the Caribbean, Spain and Portugal. Its aims include strengthening the MAB Programme, promoting cooperation among participating countries and supporting the creation of new biosphere reserves.<ref name="UNESCO-IberoMAB">{{cite web |title=Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks |website=UNESCO |access-date=22 October 2026}}</ref>
== History ==
IberoMAB was established in 1992 to strengthen cooperation among countries participating in UNESCO's Man and the Biosphere Programme. It provided a regional framework for collaboration between national MAB committees and biosphere reserves in Latin America, the Caribbean, Spain and Portugal.<ref name="UNESCO-IberoMAB" />
In May 2018, the network adopted its Action Plan for 2018–2025 at its 18th meeting in Santa Marta, Colombia. The plan established priorities for cooperation and activities across the network.<ref name="OAPN-IberoMAB">{{cite web |title=¿Qué es la Red IberoMaB? |url=https://www.miteco.gob.es/es/parques-nacionales-oapn/reservas-biosfera-mab/redes-regionales-y-tematicas/iberomab.html |website=Ministerio para la Transición Ecológica y el Reto Demográfico |access-date=22 October 2026}}</ref>
In October 2020, IberoMAB issued a declaration on the COVID-19 pandemic and its effects on communities and the environment. The declaration called for recovery efforts that support vulnerable communities, scientific research, biodiversity conservation and sustainable development.<ref name="IberoMAB-COVID">{{cite web |title=IberoMAB Biosphere Reserves pledge for a sustainable 'new normal' to overcome the COVID19 crisis |url=https://www.unesco.org/en/articles/iberomab-biosphere-reserves-pledge-sustainable-new-normal-overcome-covid19-crisis |date=15 October 2020 |website=UNESCO |access-date=22 October 2026}}</ref>
In 2023, the network held its second congress in the Trifinio-Fraternidad Transboundary Biosphere Reserve. Participants reviewed the network's progress and achievements, assessed the Lima Action Plan for 2016–2025 and reviewed implementation of the IberoMAB Action Plan for 2018–2025.<ref name="UNESCO-IberoMAB" />
== Objectives and activities ==
IberoMAB aims to strengthen the implementation of UNESCO's Man and the Biosphere Programme in participating countries. Its main objectives include:
* Strengthening national MAB committees and their activities.
* Promoting cooperation and communication among participating countries, national committees and biosphere reserves.
* Supporting the creation and development of biosphere reserves.
* Encouraging biodiversity conservation and sustainable development.
* Facilitating the exchange of scientific knowledge, information and experience.<ref name="UNESCO-IberoMAB" /><ref name="OAPN-IberoMAB" />
The network also organizes training activities and thematic meetings for people involved in biosphere reserve management. Its working groups address topics including gender equality, Indigenous peoples and youth.<ref name="OAPN-IberoMAB" />
== Organization and cooperation ==
IberoMAB operates within UNESCO's Man and the Biosphere Programme and brings together national MAB committees and representatives of participating countries. Its activities provide opportunities for cooperation and the exchange of knowledge and experience related to biosphere reserves and sustainable development.<ref name="UNESCO-IberoMAB" />
Spain's Autonomous National Parks Organisation (Organismo Autónomo Parques Nacionales, OAPN) serves as the network's secretariat. It works with the network's presidency and UNESCO to organize meetings and support cooperation among participating countries.<ref name="OAPN-IberoMAB" />
The network also supports thematic activities and working groups on issues such as gender equality, Indigenous peoples and youth. These activities promote participation, knowledge exchange and cooperation in the management of biosphere reserves.<ref name="OAPN-IberoMAB" />
== See also ==
* [[:en:Man and the Biosphere Programme]]
* [[:en:World Network of Biosphere Reserves]]
* [[:en:Biosphere reserve]]
== References ==
{{reflist}}
== External links ==
* [https://www.unesco.org/en/mab/networks UNESCO: Regional and Thematic Networks]
* [https://www.unesco.org/en/mab UNESCO: Man and the Biosphere Programme]
9gac0cxgl846pew4iy4vinnh5dnmroh
769723
769706
2026-10-22T11:35:23Z
AtomAnt9
76333
769723
wikitext
text/x-wiki
== History ==
IberoMAB was established in 1992 to strengthen cooperation among countries participating in UNESCO's Man and the Biosphere Programme. It provided a regional framework for collaboration between national MAB committees and biosphere reserves in Latin America, the Caribbean, Spain and Portugal.<ref name="UNESCO-IberoMAB">{{cite web |title=Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks |website=UNESCO |access-date=22 October 2026}}</ref>
In May 2018, the network adopted its Action Plan for 2018–2025 at its 18th meeting in Santa Marta, Colombia. The plan established priorities for cooperation and activities across the network.<ref name="OAPN-IberoMAB">{{cite web |title=¿Qué es la Red IberoMaB? |url=https://www.miteco.gob.es/es/parques-nacionales-oapn/reservas-biosfera-mab/redes-regionales-y-tematicas/iberomab.html |website=Ministerio para la Transición Ecológica y el Reto Demográfico |access-date=22 October 2026}}</ref>
In October 2020, IberoMAB issued a declaration on the COVID-19 pandemic and its effects on communities and the environment. The declaration called for recovery efforts that support vulnerable communities, scientific research, biodiversity conservation and sustainable development.<ref name="IberoMAB-COVID">{{cite web |title=IberoMAB Biosphere Reserves pledge for a sustainable 'new normal' to overcome the COVID19 crisis |url=https://www.unesco.org/en/articles/iberomab-biosphere-reserves-pledge-sustainable-new-normal-overcome-covid19-crisis |date=15 October 2020 |website=UNESCO |access-date=22 October 2026}}</ref>
In 2023, the network held its second congress in the Trifinio-Fraternidad Transboundary Biosphere Reserve. Participants reviewed the network's progress and achievements, assessed the Lima Action Plan for 2016–2025 and reviewed implementation of the IberoMAB Action Plan for 2018–2025.<ref name="UNESCO-IberoMAB">{{cite web |title=Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks |website=UNESCO |access-date=22 October 2026}}</ref>
== Objectives and activities ==
IberoMAB aims to strengthen the implementation of UNESCO's Man and the Biosphere Programme in participating countries. Its main objectives include:
* Strengthening national MAB committees and their activities.
* Promoting cooperation and communication among participating countries, national committees and biosphere reserves.
* Supporting the creation and development of biosphere reserves.
* Encouraging biodiversity conservation and sustainable development.
* Facilitating the exchange of scientific knowledge, information and experience.<ref name="UNESCO-IberoMAB">{{cite web |title=Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks |website=UNESCO |access-date=22 October 2026}}</ref><ref name="OAPN-IberoMAB" />
The network also organizes training activities and thematic meetings for people involved in biosphere reserve management. Its working groups address topics including gender equality, Indigenous peoples and youth.<ref name="OAPN-IberoMAB" />
8plh57wqxag37xluztnafkvdo1583g8
769727
769723
2026-10-22T11:39:24Z
AdamWaheed1
76335
769727
wikitext
text/x-wiki
== Ibero-American MAB Network ==
The '''Ibero-American MAB Network''' ('''IberoMAB''') is a regional network established in 1992 within the framework of the Man and the Biosphere (MAB) Programme of the United Nations Educational, Scientific and Cultural Organization (UNESCO). It is a link between countries in Latin America and the Caribbean and between Spain and Portugal to foster cooperation between biosphere reserves and sustainable development. The network's objectives are to support the strengthening of national MAB committees, foster cooperation among the member countries and encourage the establishment of new biosphere reserves.<ref name="UNESCO">{{cite web |title=Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks |website=UNESCO |access-date=21 October 2026}}</ref>
== History ==
The Ibero-American MAB Network was created in 1992 to strengthen cooperation among countries participating in UNESCO's Man and the Biosphere Programme. Its establishment provided a regional framework for sharing experiences and supporting biosphere reserves in Latin America, the Caribbean, Spain and Portugal.<ref name="UNESCO" />
In 2018, the network's Action Plan for 2018–2025 was endorsed at the XVIII IberoMAB Meeting in Santa Marta, Colombia. The plan provided a framework for cooperation and activities among member countries.<ref name="UNESCO" />
In October 2020, IberoMAB published a declaration addressing the COVID-19 pandemic and its effects on communities and the environment. The declaration emphasized sustainable recovery, community participation, scientific research and the role of biosphere reserves in protecting biodiversity and supporting human well-being.<ref name="Declaration">{{cite web |title=IberoMAB Biosphere Reserves pledge for a sustainable 'new normal' to overcome the COVID19 crisis |url=https://www.unesco.org/en/articles/iberomab-biosphere-reserves-pledge-sustainable-new-normal-overcome-covid19-crisis |website=UNESCO |date=15 October 2020 |access-date=21 October 2026}}</ref>
In 2023, the network held its second congress in the Trifinio-Fraternidad Transboundary Biosphere Reserve. Participants reviewed progress on regional cooperation and assessed the implementation of the Lima Action Plan 2016–2025 and the IberoMAB Action Plan 2018–2025.<ref name="UNESCO" />
== Geographic scope and membership ==
''This section has not yet been completed.''
== Objectives and activities ==
IberoMAB aims to strengthen the implementation of UNESCO's Man and the Biosphere Programme across its member countries. Its main objectives include:
* Strengthening national committees responsible for the MAB Programme.
* Improving cooperation and communication between member countries and biosphere reserves.
* Supporting the establishment and development of biosphere reserves.
* Promoting biodiversity conservation and sustainable development.
* Encouraging the exchange of scientific knowledge, experiences and good practices.<ref name="UNESCO" />
The network also supports initiatives addressing environmental and social challenges. In its 2020 declaration on the COVID-19 pandemic, IberoMAB encouraged community participation, the involvement of young people and vulnerable groups, scientific and technical research, and the integration of biodiversity and sustainable development into public policies.<ref name="Declaration" />
More recently, UNESCO has continued to support regional cooperation on biodiversity conservation, sustainable management, community participation and green economies through initiatives involving biosphere reserves in Latin America and the Caribbean and their international partners.<ref name="Project">{{cite web |title=MAB Secretariat support to strategic initiatives for sustainable development |url=https://core.unesco.org/en/project/3210211102 |website=UNESCO Core Data Portal |access-date=21 October 2026}}</ref>
== Organization and cooperation ==
IberoMAB operates within UNESCO's Man and the Biosphere Programme and brings together national MAB committees and representatives from participating countries. Its activities provide opportunities for members to exchange knowledge, discuss common challenges and coordinate regional priorities.<ref name="UNESCO" />
The network has a Bureau that supports its coordination. According to UNESCO's regional networks information, Honduras serves as president, Peru as vice-president and Spain as secretariat.<ref name="UNESCO" />
IberoMAB also cooperates with other regional and thematic networks within the World Network of Biosphere Reserves. This cooperation supports the sharing of expertise and the development of approaches to biodiversity conservation, sustainable development and environmental management.<ref name="Project" />
== See also ==
* [[Man and the Biosphere Programme]]
* [[World Network of Biosphere Reserves]]
* [[Biosphere reserve]]
* [[UNESCO]]
== References ==
{{reflist}}
== External links ==
* [https://www.unesco.org/en/mab/networks UNESCO – Regional and Thematic Networks]
* [https://www.unesco.org/en/mab/about UNESCO – About the Man and the Biosphere Programme]
* [https://www.unesco.org/en/mab/list UNESCO – List of Biosphere Reserves]
* [https://core.unesco.org/en/project/3210211102 UNESCO Core Data Portal – MAB strategic initiatives for sustainable development]
mqoygry23lazmkibcf5rrboc1crecvf
Southest Asia Biosphere Reserve Network
0
179379
769643
769410
2026-10-22T10:29:34Z
MonetaVetus
76332
/* Introduction */
769643
wikitext
text/x-wiki
{{Short description|Regional network of UNESCO biosphere reserves in Southeast Asia}}
{{Infobox organization
| name = Southeast Asian Biosphere Reserve Network
| abbreviation = SeaBRnet
| formation = October 1998
| type = Regional [[Man and the Biosphere Programme|MAB]] Network
| headquarters = [[Jakarta]], Indonesia (UNESCO Regional Office)
| region_served = [[Southeast Asia]]
| membership = 51 biosphere reserves across 8 countries
| parent_organization = [[UNESCO]] [[Man and the Biosphere Programme]]
| website = {{URL|https://www.unesco.org/en/fieldoffice/jakarta/science-network/about-seabrnet}}
}}
== <big>Introduction</big> ==
The Southeast Asia Biosphere Reserve Network (SeaBRnet) is a regional collaborative platform which established under UNESCO's Man and the Biosphere (MAB) Programme.The program was established in 1998. SeaBRnet aims to promote knowledge-sharing, capacity building among biosphere reserves. The network contains eight Southeasian countries and 51 biosphere reserves in it<ref>{{Cite web |title=About SeaBRnet (Southeast Asian Biosphere Reserve Network) |url=https://www.unesco.org/en/fieldoffice/jakarta/science-network/about-seabrnet |archive-date=22.10.2026 |access-date={{subst:DATE}}}}</ref>.
== History ==
SeaBRnet was officially created in October
== Objectives and activities ==
The primary objective of SeaBRnet is to integrate biodiversity conservation with sustainable socio-economic growth
* '''Annual SeaBRnet Meetings:'''
* '''Capacity-Building and Training Series:'''
* '''Youth of SeaBRnet:'''
== Organization and cooperation ==
== Referances ==
j05fbwfr9foxi71hu2s1qp6bcx6cnlm
769649
769643
2026-10-22T10:31:27Z
MonetaVetus
76332
/* Referances */
769649
wikitext
text/x-wiki
{{Short description|Regional network of UNESCO biosphere reserves in Southeast Asia}}
{{Infobox organization
| name = Southeast Asian Biosphere Reserve Network
| abbreviation = SeaBRnet
| formation = October 1998
| type = Regional [[Man and the Biosphere Programme|MAB]] Network
| headquarters = [[Jakarta]], Indonesia (UNESCO Regional Office)
| region_served = [[Southeast Asia]]
| membership = 51 biosphere reserves across 8 countries
| parent_organization = [[UNESCO]] [[Man and the Biosphere Programme]]
| website = {{URL|https://www.unesco.org/en/fieldoffice/jakarta/science-network/about-seabrnet}}
}}
== <big>Introduction</big> ==
The Southeast Asia Biosphere Reserve Network (SeaBRnet) is a regional collaborative platform which established under UNESCO's Man and the Biosphere (MAB) Programme.The program was established in 1998. SeaBRnet aims to promote knowledge-sharing, capacity building among biosphere reserves. The network contains eight Southeasian countries and 51 biosphere reserves in it<ref>{{Cite web |title=About SeaBRnet (Southeast Asian Biosphere Reserve Network) |url=https://www.unesco.org/en/fieldoffice/jakarta/science-network/about-seabrnet |archive-date=22.10.2026 |access-date=22.10.2026|website=UNESCO Jakarta Natural Sciences Network|last=UNOESCO|archive-url=https://www.unesco.org/en/fieldoffice/jakarta/science-network/about-seabrnet}}</ref>.
== History ==
SeaBRnet was officially created in October
== Objectives and activities ==
The primary objective of SeaBRnet is to integrate biodiversity conservation with sustainable socio-economic growth
* '''Annual SeaBRnet Meetings:'''
* '''Capacity-Building and Training Series:'''
* '''Youth of SeaBRnet:'''
== Organization and cooperation ==
== Referances ==
b7cmiikgf4z7h30x6y9m3jgl0vzt9m1
769650
769649
2026-10-22T10:32:50Z
MonetaVetus
76332
/* Referances */
769650
wikitext
text/x-wiki
{{Short description|Regional network of UNESCO biosphere reserves in Southeast Asia}}
{{Infobox organization
| name = Southeast Asian Biosphere Reserve Network
| abbreviation = SeaBRnet
| formation = October 1998
| type = Regional [[Man and the Biosphere Programme|MAB]] Network
| headquarters = [[Jakarta]], Indonesia (UNESCO Regional Office)
| region_served = [[Southeast Asia]]
| membership = 51 biosphere reserves across 8 countries
| parent_organization = [[UNESCO]] [[Man and the Biosphere Programme]]
| website = {{URL|https://www.unesco.org/en/fieldoffice/jakarta/science-network/about-seabrnet}}
}}
== <big>Introduction</big> ==
The Southeast Asia Biosphere Reserve Network (SeaBRnet) is a regional collaborative platform which established under UNESCO's Man and the Biosphere (MAB) Programme.The program was established in 1998. SeaBRnet aims to promote knowledge-sharing, capacity building among biosphere reserves. The network contains eight Southeasian countries and 51 biosphere reserves in it<ref>{{Cite web |title=About SeaBRnet (Southeast Asian Biosphere Reserve Network) |url=https://www.unesco.org/en/fieldoffice/jakarta/science-network/about-seabrnet |archive-date=22.10.2026 |access-date=22.10.2026|website=UNESCO Jakarta Natural Sciences Network|last=UNESCO|archive-url=https://www.unesco.org/en/fieldoffice/jakarta/science-network/about-seabrnet|date=04.08.2026}}</ref>.
== History ==
SeaBRnet was officially created in October
== Objectives and activities ==
The primary objective of SeaBRnet is to integrate biodiversity conservation with sustainable socio-economic growth
* '''Annual SeaBRnet Meetings:'''
* '''Capacity-Building and Training Series:'''
* '''Youth of SeaBRnet:'''
== Organization and cooperation ==
== Referances ==
6b56qblhozd98e5u4t0j6m1bgssw9j6
769652
769650
2026-10-22T10:33:25Z
Aeronomad4
76342
769652
wikitext
text/x-wiki
{{Short description|Regional network of UNESCO biosphere reserves in Southeast Asia}}
{{Infobox organization
| name = Southeast Asian Biosphere Reserve Network
| abbreviation = SeaBRnet
| formation = October 1998
| type = Regional [[Man and the Biosphere Programme|MAB]] Network
| headquarters = [[Jakarta]], Indonesia (UNESCO Regional Office)
| region_served = [[Southeast Asia]]
| membership = 51 biosphere reserves across 8 countries
| parent_organization = [[UNESCO]] [[Man and the Biosphere Programme]]
| website = {{URL|https://www.unesco.org/en/fieldoffice/jakarta/science-network/about-seabrnet}}
}}
== <big>Introduction</big> ==
The Southeast Asia Biosphere Reserve Network (SeaBRnet) is a regional collaborative platform which established under UNESCO's Man and the Biosphere (MAB) Programme.The program was established in 1998. SeaBRnet aims to promote knowledge-sharing, capacity building among biosphere reserves. The network contains eight Southeasian countries and 51 biosphere reserves in it<ref>{{Cite web |title=About SeaBRnet (Southeast Asian Biosphere Reserve Network) |url=https://www.unesco.org/en/fieldoffice/jakarta/science-network/about-seabrnet |archive-date=22.10.2026 |access-date=22.10.2026|website=UNESCO Jakarta Natural Sciences Network|last=UNESCO|archive-url=https://www.unesco.org/en/fieldoffice/jakarta/science-network/about-seabrnet|date=04.08.2026}}</ref>.
== History ==
The Southeast Asian Biosphere Reserve Network (SeaBRnet) is a collaborative platform established under UNESCO’s MAB Programme. It was initiated in October 1998.<ref>{{cite web |title=Networks |url=https://en.wikipedia.org/wiki/Man_and_the_Biosphere_Programme |accessdate=2026-10-22 |publisher=[[UNESCO]]}}</ref> It was created to address the unique ecological, geographical, and socio-economic characteristics of the Southeast Asia region, which is globally recognized for its exceptional biodiversity, vast tropical rainforest and marine ecosystems, and high population density.<ref>{{cite web |title=Biosphere reserve |url=https://www.britannica.com/science/biosphere-reserve |accessdate=2026-10-22 |publisher=[[Encyclopædia Britannica]]}}</ref>
* '''Subsequent Expansion:''' Over the following decades, the network grew to encompass 10 member states: Cambodia, China, Indonesia, Japan, Lao PDR, Malaysia, Myanmar, Philippines, Thailand, and Viet Nam.<ref>{{cite web |title=Networks |url=https://en.wikipedia.org/wiki/Man_and_the_Biosphere_Programme |accessdate=2026-10-22 |publisher=[[UNESCO]]}}</ref> They focus on transboundary conservation and sustainable resource management.<ref>{{cite web |title=MAB Strategy |url=https://www.unesco.org/en/mab |accessdate=2026-10-22 |publisher=[[UNESCO]]}}</ref> Through regular regional meetings, capacity-building workshops, and joint research initiatives, the network continues to strengthen the management effectiveness of biosphere reserves, promoting them as premier learning sites for sustainable development throughout Southeast Asia.<ref>{{cite web |title=Networks |url=https://en.wikipedia.org/wiki/Man_and_the_Biosphere_Programme |accessdate=2026-10-22 |publisher=[[UNESCO]]}}</ref>
* '''Recent Milestones:''' The network has continued its active regional collaboration through periodic meetings and virtual coordination (such as regional preparatory workshops leading up to the global MAB Congresses), reinforcing its role as a key platform for biodiversity protection and local livelihood resilience in Southeast Asia.<ref>{{cite web |title=Networks |url=https://en.wikipedia.org/wiki/Man_and_the_Biosphere_Programme |accessdate=2026-10-22 |publisher=[[UNESCO]]}}</ref> Since its inception, SeaBRnet has served as a vital collaborative platform for countries across the region to share scientific knowledge, exchange best practices in site management, and tackle common environmental challenges such as habitat fragmentation, deforestation, climate change impacts, and balancing conservation with sustainable local livelihoods.<ref>{{cite web |title=Networks |url=https://en.wikipedia.org/wiki/Man_and_the_Biosphere_Programme |accessdate=2026-10-22 |publisher=[[UNESCO]]}}</ref><ref>{{cite web |title=MAB Strategy |url=https://www.unesco.org/en/mab |accessdate=2026-10-22 |publisher=[[UNESCO]]}}</ref>
<references />
== Objectives and activities ==
The primary objective of SeaBRnet is to integrate biodiversity conservation with sustainable socio-economic growth
* '''Annual SeaBRnet Meetings:'''
* '''Capacity-Building and Training Series:'''
* '''Youth of SeaBRnet:'''
== Organization and cooperation ==
== Referances ==
b3j1y3dd9ggvkf5y88h3xwlxnzcm30b
769667
769652
2026-10-22T10:40:52Z
Aeronomad4
76342
769667
wikitext
text/x-wiki
{{Short description|Regional network of UNESCO biosphere reserves in Southeast Asia}}
{{Infobox organization
| name = Southeast Asian Biosphere Reserve Network
| abbreviation = SeaBRnet
| formation = October 1998
| type = Regional [[Man and the Biosphere Programme|MAB]] Network
| headquarters = [[Jakarta]], Indonesia (UNESCO Regional Office)
| region_served = [[Southeast Asia]]
| membership = 51 biosphere reserves across 8 countries
| parent_organization = [[UNESCO]] [[Man and the Biosphere Programme]]
| website = {{URL|https://www.unesco.org/en/fieldoffice/jakarta/science-network/about-seabrnet}}
}}
== <big>Introduction</big> ==
The Southeast Asia Biosphere Reserve Network (SeaBRnet) is a regional collaborative platform which established under UNESCO's Man and the Biosphere (MAB) Programme.The program was established in 1998. SeaBRnet aims to promote knowledge-sharing, capacity building among biosphere reserves. The network contains eight Southeasian countries and 51 biosphere reserves in it<ref>{{Cite web |title=About SeaBRnet (Southeast Asian Biosphere Reserve Network) |url=https://www.unesco.org/en/fieldoffice/jakarta/science-network/about-seabrnet |archive-date=22.10.2026 |access-date=22.10.2026|website=UNESCO Jakarta Natural Sciences Network|last=UNESCO|archive-url=https://www.unesco.org/en/fieldoffice/jakarta/science-network/about-seabrnet|date=04.08.2026}}</ref>.
== History ==
The **Southeast Asian Biosphere Reserve Network** (SeaBRnet) is a collaborative platform established under UNESCO’s MAB Programme. It was initiated in 1998.It was created to address the unique ecological, geographical, and socio-economic characteristics of the Southeast Asia region, which is globally recognized for its exceptional biodiversity, vast tropical rainforest and marine ecosystems, and high population density.<ref>{{Cite web |title= |url=https://www.unesco.org/en/fieldoffice/jakarta/science-network/about-seabrnet |access-date={{subst:DATE}}}}</ref>
* '''Subsequent Expansion:''' Over the following decades, the network grew to encompass 10 member states: Cambodia, China, Indonesia, Japan, Lao PDR, Malaysia, Myanmar, Philippines, Thailand, and Viet Nam. They focus on transboundary conservation and sustainable resource management. Through regular regional meetings, capacity-building workshops, and joint research initiatives, the network continues to strengthen the management effectiveness of biosphere reserves, promoting them as premier learning sites for sustainable development throughout Southeast Asia.
* '''Recent Milestones:''' The network has continued its active regional collaboration through periodic meetings and virtual coordination (such as regional preparatory workshops leading up to the global MAB Congresses), reinforcing its role as a key platform for biodiversity protection and local livelihood resilience in Southeast Asia. Since its inception, SeaBRnet has served as a vital collaborative platform for countries across the region to share scientific knowledge, exchange best practices in site management, and tackle common environmental challenges such as habitat fragmentation, deforestation, climate change impacts, and balancing conservation with sustainable local livelihoods.
== Objectives and activities ==
The primary objective of SeaBRnet is to integrate biodiversity conservation with sustainable socio-economic growth
* '''Annual SeaBRnet Meetings:'''
* '''Capacity-Building and Training Series:'''
* '''Youth of SeaBRnet:'''
== Organization and cooperation ==
== Referances ==
jaqwcjvy5s8pn13mhscafpy07lwkv6l
769673
769667
2026-10-22T10:44:18Z
40meh40
76214
769673
wikitext
text/x-wiki
{{Short description|Regional network of UNESCO biosphere reserves in Southeast Asia}}
{{Infobox organization
| name = Southeast Asian Biosphere Reserve Network
| abbreviation = SeaBRnet
| formation = October 1998
| type = Regional [[Man and the Biosphere Programme|MAB]] Network
| headquarters = [[Jakarta]], Indonesia (UNESCO Regional Office)
| region_served = [[Southeast Asia]]
| membership = 51 biosphere reserves across 8 countries
| parent_organization = [[UNESCO]] [[Man and the Biosphere Programme]]
| website = {{URL|https://www.unesco.org/en/fieldoffice/jakarta/science-network/about-seabrnet}}
}}
The '''Southeast Asian Biosphere Reserve Network''' ('''SeaBRnet''') is a regional collaborative platform established under [[UNESCO]]'s [[Man and the Biosphere Programme]] (MAB). Founded in October 1998, the network promotes knowledge-sharing, research collaboration, and capacity-building among biosphere reserves across [[Southeast Asia]]. As of 2026, the network comprises 51 biosphere reserves across eight Southeast Asian nations.<ref name="AboutSeaBRnet">{{Cite web |last=UNESCO |date=4 August 2026 |title=About SeaBRnet (Southeast Asian Biosphere Reserve Network) |url=https://www.unesco.org/en/fieldoffice/jakarta/science-network/about-seabrnet |website=UNESCO Jakarta Natural Sciences Network |access-date=22 October 2026}}</ref>
== History ==
SeaBRnet was initiated in October 1998 under UNESCO's Man and the Biosphere Programme to address the ecological, geographical, and socio-economic characteristics of Southeast Asia—a region recognized for its high biodiversity, extensive tropical rainforest and marine ecosystems, and dense human populations.<ref name="AboutSeaBRnet" /> Since its inception, the network has served as a forum for member countries to exchange scientific knowledge, share site-management practices, and address shared environmental challenges, including habitat fragmentation, deforestation, and climate change.
Over the following decades, cooperation through the network expanded to involve participation and support from up to ten countries across East and Southeast Asia—including [[Cambodia]], [[China]], [[Indonesia]], [[Japan]], [[Laos|Lao PDR]], [[Malaysia]], [[Myanmar]], the [[Philippines]], [[Thailand]], and [[Vietnam]]—focusing on transboundary conservation and sustainable resource management. Through regular regional meetings and joint research initiatives, SeaBRnet has continued to coordinate regional preparatory workshops leading up to global MAB Congresses, reinforcing biosphere reserves as learning sites for sustainable development.<ref name="AboutSeaBRnet" />
== Objectives and activities ==
SeaBRnet assists biosphere reserves in balancing biodiversity conservation with community-driven, sustainable economic development through local empowerment and regional cooperation.<ref name="AboutSeaBRnet" /> Its primary activities are structured around three core areas:
* '''Annual SeaBRnet Meetings:''' Regional assemblies that bring reserve managers, scientists, and stakeholders together to evaluate progress against global MAB strategies, exchange local case studies, and set strategic conservation priorities.<ref name="AboutSeaBRnet" />
* '''Capacity-Building and Training Series:''' A practical learning program designed to upgrade reserve management capabilities across Southeast Asia. It includes hands-on technical workshops, peer case studies, and on-site field visits or virtual tours to observe reserve operations and community initiatives.<ref>{{Cite web |title=SeaBRnet Capacity-Building and Training |url=https://www.publicnow.com/view/B92006316134009B64F0E93A61F5C92F77C7DB5E |access-date=22 October 2026}}</ref><ref>{{Cite web |title=Palawan BR Model for Sustainable Development: PCSDS Shares Best Practices During SeaBRnet Training in Indonesia |url=https://pcsd.gov.ph/palawan-br-model-for-sustainable-development-pcsds-shares-best-practices-during-seabrnet-training-in-indonesia/ |website=Palawan Council for Sustainable Development |access-date=22 October 2026}}</ref>
* '''Youth of SeaBRnet:''' A regional platform that supports youth participation in conservation and climate resilience through research grants (such as the MAB Young Scientists Awards), water resilience competitions, skill-building workshops, and project showcases.<ref>{{Cite web |date=26 June 2025 |title=Youth of SeaBRnet |url=https://www.unesco.org/en/fieldoffice/jakarta/science-network/seabrnet/youth |website=UNESCO Jakarta Natural Sciences Network |access-date=22 October 2026}}</ref>
== Organization and cooperation ==
The Secretariat of SeaBRnet is hosted by the UNESCO Regional Science Bureau for Asia and the Pacific, located in [[Jakarta]], Indonesia.<ref name="AboutSeaBRnet" /> The network operates in partnership with National MAB Committees, local governments, academic institutions, and community stakeholders across its member countries.
== References ==
{{Reflist}}
[[Category:Man and the Biosphere Programme]]
[[Category:International environmental organizations]]
[[Category:Organizations established in 1998]]
[[Category:Conservation in Southeast Asia]]
06vzvqbdf5zsb3o14j2h1gw2eku3kwp
769676
769673
2026-10-22T10:48:17Z
40meh40
76214
769676
wikitext
text/x-wiki
{{Short description|Regional network of UNESCO biosphere reserves in Southeast Asia}}
{{Infobox organization
| name = Southeast Asian Biosphere Reserve Network
| abbreviation = SeaBRnet
| formation = October 1998
| type = Regional [[Man and the Biosphere Programme|MAB]] Network
| headquarters = [[Jakarta]], Indonesia (UNESCO Regional Office)
| region_served = [[Southeast Asia]]
| membership = 51 biosphere reserves across 8 countries
| parent_organization = [[UNESCO]] [[Man and the Biosphere Programme]]
| website = {{URL|https://www.unesco.org/en/fieldoffice/jakarta/science-network/about-seabrnet}}
}}
The '''Southeast Asian Biosphere Reserve Network''' ('''SeaBRnet''') is a regional collaborative platform established under [[UNESCO]]'s [[Man and the Biosphere Programme]] (MAB). Founded in October 1998, the network promotes knowledge-sharing, research collaboration, and capacity-building among biosphere reserves across [[Southeast Asia]]. As of 2026, the network comprises 51 biosphere reserves across eight Southeast Asian nations.<ref name="AboutSeaBRnet">{{Cite web |last=UNESCO |date=4 August 2026 |title=About SeaBRnet (Southeast Asian Biosphere Reserve Network) |url=https://www.unesco.org/en/fieldoffice/jakarta/science-network/about-seabrnet |website=UNESCO Jakarta Natural Sciences Network |access-date=22 October 2026}}</ref>
== History ==
SeaBRnet was initiated in October 1998 under UNESCO's Man and the Biosphere Programme to address the unique ecological, geographical, and socio-economic characteristics of Southeast Asia—a region recognized for its high biodiversity, extensive tropical rainforest and marine ecosystems, and dense human populations.<ref name="AboutSeaBRnet" />
* '''Subsequent Expansion:''' Over the following decades, cooperation through the network grew to involve 10 participating countries: [[Cambodia]], [[China]], [[Indonesia]], [[Japan]], [[Laos|Lao PDR]], [[Malaysia]], [[Myanmar]], the [[Philippines]], [[Thailand]], and [[Vietnam]], focusing on transboundary conservation and sustainable resource management. Through regular regional meetings, capacity-building workshops, and joint research initiatives, the network continues to strengthen the management effectiveness of biosphere reserves, promoting them as premier learning sites for sustainable development throughout Southeast Asia.<ref name="AboutSeaBRnet" />
* '''Recent Milestones:''' The network has continued its active regional collaboration through periodic meetings and virtual coordination (such as regional preparatory workshops leading up to the global MAB Congresses), reinforcing its role as a key platform for biodiversity protection and local livelihood resilience in Southeast Asia. Since its inception, SeaBRnet has served as a vital collaborative platform for countries across the region to share scientific knowledge, exchange best practices in site management, and tackle common environmental challenges such as habitat fragmentation, deforestation, climate change impacts, and balancing conservation with sustainable local livelihoods.<ref name="AboutSeaBRnet" />
== Objectives and activities ==
SeaBRnet assists biosphere reserves in balancing biodiversity conservation with community-driven, sustainable economic development through local empowerment and regional cooperation.<ref name="AboutSeaBRnet" /> Its primary activities are structured around three core areas:
* '''Annual SeaBRnet Meetings:''' Regional assemblies that bring reserve managers, scientists, and stakeholders together to evaluate progress against global MAB strategies, exchange local case studies, and set strategic conservation priorities.<ref name="AboutSeaBRnet" />
* '''Capacity-Building and Training Series:''' A practical learning program designed to upgrade reserve management capabilities across Southeast Asia. It includes hands-on technical workshops, peer case studies, and on-site field visits or virtual tours to observe reserve operations and community initiatives.<ref>{{Cite web |title=SeaBRnet Capacity-Building and Training |url=https://www.publicnow.com/view/B92006316134009B64F0E93A61F5C92F77C7DB5E |access-date=22 October 2026}}</ref><ref>{{Cite web |title=Palawan BR Model for Sustainable Development: PCSDS Shares Best Practices During SeaBRnet Training in Indonesia |url=https://pcsd.gov.ph/palawan-br-model-for-sustainable-development-pcsds-shares-best-practices-during-seabrnet-training-in-indonesia/ |website=Palawan Council for Sustainable Development |access-date=22 October 2026}}</ref>
* '''Youth of SeaBRnet:''' A regional platform that supports youth participation in conservation and climate resilience through research grants (such as the MAB Young Scientists Awards), water resilience competitions, skill-building workshops, and project showcases.<ref>{{Cite web |date=26 June 2025 |title=Youth of SeaBRnet |url=https://www.unesco.org/en/fieldoffice/jakarta/science-network/seabrnet/youth |website=UNESCO Jakarta Natural Sciences Network |access-date=22 October 2026}}</ref>
== Organization and cooperation ==
The Secretariat of SeaBRnet is hosted by the UNESCO Regional Science Bureau for Asia and the Pacific, located in [[Jakarta]], Indonesia.<ref name="AboutSeaBRnet" /> The network operates in partnership with National MAB Committees, local governments, academic institutions, and community stakeholders across its member countries.
== References ==
{{Reflist}}
[[Category:Man and the Biosphere Programme]]
[[Category:International environmental organizations]]
[[Category:Organizations established in 1998]]
[[Category:Conservation in Southeast Asia]]
4wsay4eoo1z0ic82f6mjmrqrmgmzg0g
769687
769676
2026-10-22T11:05:02Z
40meh40
76214
769687
wikitext
text/x-wiki
{{Short description|Regional network of UNESCO biosphere reserves in Southeast Asia}}
{{Infobox organization
| name = Southeast Asian Biosphere Reserve Network
| abbreviation = SeaBRnet
| formation = October 1998
| type = Regional [[Man and the Biosphere Programme|MAB]] Network
| headquarters = [[Jakarta]], Indonesia (UNESCO Regional Office)
| region_served = [[Southeast Asia]]
| membership = 51 biosphere reserves across 8 countries
| parent_organization = [[UNESCO]] [[Man and the Biosphere Programme]]
| website = {{URL|https://www.unesco.org/en/fieldoffice/jakarta/science-network/about-seabrnet}}
}}
The '''Southeast Asian Biosphere Reserve Network''' ('''SeaBRnet''') is a regional collaborative platform established under [[UNESCO]]'s [[Man and the Biosphere Programme]] (MAB). Founded in October 1998, the network promotes knowledge-sharing, research collaboration, and capacity-building among biosphere reserves across [[Southeast Asia]]. As of 2026, the network comprises 51 biosphere reserves across eight Southeast Asian nations.<ref name="AboutSeaBRnet">{{Cite web |last=UNESCO |date=4 August 2026 |title=About SeaBRnet (Southeast Asian Biosphere Reserve Network) |url=https://www.unesco.org/en/fieldoffice/jakarta/science-network/about-seabrnet |website=UNESCO Jakarta Natural Sciences Network |access-date=22 October 2026}}</ref>
== History ==
SeaBRnet was initiated in October 1998 under UNESCO's Man and the Biosphere Programme to address the unique ecological, geographical, and socio-economic characteristics of Southeast Asia—a region recognized for its high biodiversity, extensive tropical rainforest and marine ecosystems, and dense human populations.<ref name="AboutSeaBRnet" />
* '''Subsequent Expansion:''' Over the following decades, cooperation through the network grew to involve 10 participating countries: [[Cambodia]], [[China]], [[Indonesia]], [[Japan]], [[Laos|Lao PDR]], [[Malaysia]], [[Myanmar]], the [[Philippines]], [[Thailand]], and [[Vietnam]], focusing on transboundary conservation and sustainable resource management. Through regular regional meetings, capacity-building workshops, and joint research initiatives, the network continues to strengthen the management effectiveness of biosphere reserves, promoting them as premier learning sites for sustainable development throughout Southeast Asia.<ref name="AboutSeaBRnet" />
* '''Recent Milestones:''' The network has continued its active regional collaboration through periodic meetings and virtual coordination (such as regional preparatory workshops leading up to the global MAB Congresses), reinforcing its role as a key platform for biodiversity protection and local livelihood resilience in Southeast Asia. Since its inception, SeaBRnet has served as a vital collaborative platform for countries across the region to share scientific knowledge, exchange best practices in site management, and tackle common environmental challenges such as habitat fragmentation, deforestation, climate change impacts, and balancing conservation with sustainable local livelihoods.<ref name="AboutSeaBRnet" />
== Objectives and activities ==
SeaBRnet assists biosphere reserves in balancing biodiversity conservation with community-driven, sustainable economic development through local empowerment and regional cooperation.<ref name="AboutSeaBRnet" /> Its primary activities are structured around three core areas:
* '''Annual SeaBRnet Meetings:''' Regional assemblies that bring reserve managers, scientists, and stakeholders together to evaluate progress against global MAB strategies, exchange local case studies, and set strategic conservation priorities.<ref name="AboutSeaBRnet" />
* '''Capacity-Building and Training Series:''' A practical learning program designed to upgrade reserve management capabilities across Southeast Asia. It includes hands-on technical workshops, peer case studies, and on-site field visits or virtual tours to observe reserve operations and community initiatives.<ref>{{Cite web |title=SeaBRnet Capacity-Building and Training |url=https://www.publicnow.com/view/B92006316134009B64F0E93A61F5C92F77C7DB5E |access-date=22 October 2026}}</ref><ref>{{Cite web |title=Palawan BR Model for Sustainable Development: PCSDS Shares Best Practices During SeaBRnet Training in Indonesia |url=https://pcsd.gov.ph/palawan-br-model-for-sustainable-development-pcsds-shares-best-practices-during-seabrnet-training-in-indonesia/ |website=Palawan Council for Sustainable Development |access-date=22 October 2026}}</ref>
* '''Youth of SeaBRnet:''' A regional platform that supports youth participation in conservation and climate resilience through research grants (such as the MAB Young Scientists Awards), water resilience competitions, skill-building workshops, and project showcases.<ref>{{Cite web |date=26 June 2025 |title=Youth of SeaBRnet |url=https://www.unesco.org/en/fieldoffice/jakarta/science-network/youth-seabrnet |website=UNESCO Jakarta Natural Sciences Network |access-date=22 October 2026}}</ref>
== Organization and cooperation ==
The Secretariat of SeaBRnet is hosted by the UNESCO Regional Science Bureau for Asia and the Pacific, located in [[Jakarta]], Indonesia.<ref name="AboutSeaBRnet" /> The network operates in partnership with National MAB Committees, local governments, academic institutions, and community stakeholders across its member countries.
== References ==
{{Reflist}}
[[Category:Man and the Biosphere Programme]]
[[Category:International environmental organizations]]
[[Category:Organizations established in 1998]]
[[Category:Conservation in Southeast Asia]]
oslcmng7r0ymrserkz45tnvmtnxucie
The World Network of Island and Coastal Biosphere Reserves
0
179381
769632
769411
2026-10-22T10:23:21Z
Berkesmob
76347
/* */
769632
wikitext
text/x-wiki
{{Infobox organization
| name = World Network of Island and Coastal Biosphere Reserves
| abbreviation = WNICBR
| type = International thematic network
| parent_organisation = [[Man and the Biosphere Programme]] (UNESCO)
| headquarter = [[Jeju Province|Jeju]] ([[South Korea]]) & [[Menorca]] ([[Spain]])
| website = {{URL|https://www.unesco.org/en/mab/networks}}
}}
The '''World Network of Island and Coastal Biosphere Reserves''' ('''WNICBR''') is a thematic network under [[UNESCO]]'s [[Man and the Biosphere Programme|Man and the Biosphere (MAB) Programme]]. It promotes sustainable development, climate adaptation, and marine biodiversity conservation across island and coastal biosphere reserves globally.<ref name="UNESCO-Networks">{{cite web |title=UNESCO MAB Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks |publisher=UNESCO |access-date=2026-10-21}}</ref> Its joint Technical Secretariat is co-managed by the [[Jeju Volcanic Island and Lava Tubes|Jeju Biosphere Reserve]] (South Korea) and the [[Menorca|Menorca Biosphere Reserve]] (Spain).<ref name="UNDESA-Partnership">{{cite web |title=World Network of Island and Coastal Biosphere Reserves (WNICBR) |url=https://sdgs.un.org/partnerships/world-network-island-and-coastal-biosphere-reserves-wnicbr |publisher=United Nations Department of Economic and Social Affairs |access-date=2026-10-21}}</ref>
== History ==
In February 2009, representatives from several island and coastal biosphere reserves met in Menorca, Spain, to address shared vulnerabilities like sea-level rise, coastal erosion, and extreme weather. The network was formally established within UNESCO's MAB framework between 2009 and 2012.<ref name="UNDESA-Partnership" />
In 2012, a dual technical secretariat structure was set up in Jeju Island (South Korea) and Menorca (Spain) to manage administrative and scientific coordination.<ref name="IsleOfMan">{{cite web |title=World Network of Island and Coastal BRs |url=https://www.biosphere.im/our-networks/world-network-of-island-and-coastal-brs |publisher=UNESCO Biosphere Isle of Man |access-date=2026-10-21}}</ref> Since its founding, the network has grown to include over 80 member sites across more than 30 countries.<ref name="UNESCO-Networks" />
The '''World Network of Island and Coastal Biosphere Reserves''' ('''WNICBR''') is a thematic network under [[UNESCO]]'s [[Man and the Biosphere Programme|Man and the Biosphere (MAB) Programme]]. It promotes sustainable development, climate adaptation, and marine biodiversity conservation across island and coastal biosphere reserves globally.<ref name="UNESCO-Networks">{{cite web |title=UNESCO MAB Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks |publisher=UNESCO |access-date=2026-10-21}}</ref> Its joint Technical Secretariat is co-managed by the [[Jeju Volcanic Island and Lava Tubes|Jeju Biosphere Reserve]] (South Korea) and the [[Menorca|Menorca Biosphere Reserve]] (Spain).<ref name="UNDESA-Partnership">{{cite web |title=World Network of Island and Coastal Biosphere Reserves (WNICBR) |url=https://sdgs.un.org/partnerships/world-network-island-and-coastal-biosphere-reserves-wnicbr |publisher=United Nations Department of Economic and Social Affairs |access-date=2026-10-21}}</ref>
a98vi0g81oyruw3qc3rutpq3xwn74gr
769636
769632
2026-10-22T10:25:46Z
Berkesmob
76347
/* */ fixed edited
769636
wikitext
text/x-wiki
{{Infobox organization
| name = World Network of Island and Coastal Biosphere Reserves
| abbreviation = WNICBR
| type = International thematic network
| parent_organisation = [[Man and the Biosphere Programme]] (UNESCO)
| headquarter = [[Jeju Province|Jeju]] ([[South Korea]]) & [[Menorca]] ([[Spain]])
| website = {{URL|https://www.unesco.org/en/mab/networks}}
}}
The '''World Network of Island and Coastal Biosphere Reserves''' ('''WNICBR''') is a thematic network under [[UNESCO]]'s [[Man and the Biosphere Programme|Man and the Biosphere (MAB) Programme]]. It promotes sustainable development, climate adaptation, and marine biodiversity conservation across island and coastal biosphere reserves globally.<ref name="UNESCO-Networks">{{cite web |title=UNESCO MAB Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks |publisher=UNESCO |access-date=2026-10-22}}</ref> Its joint Technical Secretariat is co-managed by the [[Jeju Volcanic Island and Lava Tubes|Jeju Biosphere Reserve]] (South Korea) and the [[Menorca|Menorca Biosphere Reserve]] (Spain).<ref name="UNDESA-Partnership">{{cite web |title=World Network of Island and Coastal Biosphere Reserves (WNICBR) |url=https://sdgs.un.org/partnerships/world-network-island-and-coastal-biosphere-reserves-wnicbr |publisher=United Nations Department of Economic and Social Affairs |access-date=2026-10-22}}</ref>
== History ==
In February 2009, representatives from several island and coastal biosphere reserves met in Menorca, Spain, to address shared vulnerabilities like sea-level rise, coastal erosion, and extreme weather. The network was formally established within UNESCO's MAB framework between 2009 and 2012.<ref name="UNDESA-Partnership" />
In 2012, a dual technical secretariat structure was set up in Jeju Island (South Korea) and Menorca (Spain) to manage administrative and scientific coordination.<ref name="IsleOfMan">{{cite web |title=World Network of Island and Coastal BRs |url=https://www.biosphere.im/our-networks/world-network-of-island-and-coastal-brs |publisher=UNESCO Biosphere Isle of Man |access-date=2026-10-22}}</ref> Since its founding, the network has grown to include over 80 member sites across more than 30 countries.<ref name="UNESCO-Networks" />
== Objectives and activities ==
The core mission of the network is to develop, test, and share sustainable management strategies suited to island and coastal ecosystems.
=== Main objectives ===
* '''Climate Change Adaptation:''' Developing local actions to counter sea-level rise, coastal flooding, and marine temperature changes.
* '''Biodiversity Conservation:''' Protecting sensitive habitats such as coral reefs, mangroves, estuaries, and island-endemic species.
* '''Sustainable Development:''' Promoting eco-friendly coastal tourism, sustainable fisheries, and renewable energy adoption.
* '''Knowledge Sharing:''' Facilitating joint scientific research, technical training, and educational exchange among site managers.
=== Key activities ===
* '''International Meetings:''' Regular global conferences bring together reserve representatives to evaluate progress and update work plans.
* '''Capacity Building:''' Annual training programs—frequently held at the Global Research and Training Centre for Internationally Designated Areas in Jeju—focus on climate resilience and resource management.
* '''Publications:''' The network publishes research reports, policy briefs, and bi-annual newsletters detailing best practices across member sites.<ref name="IsleOfMan" />
== Organization and cooperation ==
The network operates through a collaborative governance model involving local governments, scientific experts, and UNESCO leadership.
=== Technical Secretariat ===
Co-coordination is divided between two main locations:
# '''Jeju Biosphere Reserve (South Korea):''' Coordinates scientific research, training courses, and climate change initiatives.
# '''Menorca Biosphere Reserve (Spain):''' Manages communication, sustainable development models, and regional networking.
=== International alignment ===
The network's initiatives directly support the [[Sustainable Development Goals]] (SDGs) of the United Nations 2030 Agenda, with a primary focus on [[Sustainable Development Goal 14|SDG 14 (Life Below Water)]] and [[Sustainable Development Goal 13|SDG 13 (Climate Action)]].<ref name="UNDESA-Partnership" />
== See also ==
* [[Man and the Biosphere Programme]]
* [[World Network of Biosphere Reserves]]
* [[Biosphere reserve]]
* [[UNESCO]]
* [[Sustainable Development Goal 14]]
== References ==
{{reflist}}
== External links ==
* [https://www.unesco.org/en/mab/networks Official UNESCO MAB Regional and Thematic Networks Page]
* [https://sdgs.un.org/partnerships/world-network-island-and-coastal-biosphere-reserves-wnicbr UN SDGs Partnership Platform - WNICBR Profile]
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
>
== History ==
In February 2009, representatives from several island and coastal biosphere reserves met in Menorca, Spain, to address shared vulnerabilities like sea-level rise, coastal erosion, and extreme weather. The network was formally established within UNESCO's MAB framework between 2009 and 2012.<ref name="UNDESA-Partnership" />
In 2012, a dual technical secretariat structure was set up in Jeju Island (South Korea) and Menorca (Spain) to manage administrative and scientific coordination.<ref name="IsleOfMan">{{cite web |title=World Network of Island and Coastal BRs |url=https://www.biosphere.im/our-networks/world-network-of-island-and-coastal-brs |publisher=UNESCO Biosphere Isle of Man |access-date=2026-10-21}}</ref> Since its founding, the network has grown to include over 80 member sites across more than 30 countries.<ref name="UNESCO-Networks" />
The '''World Network of Island and Coastal Biosphere Reserves''' ('''WNICBR''') is a thematic network under [[UNESCO]]'s [[Man and the Biosphere Programme|Man and the Biosphere (MAB) Programme]]. It promotes sustainable development, climate adaptation, and marine biodiversity conservation across island and coastal biosphere reserves globally.<ref name="UNESCO-Networks">{{cite web |title=UNESCO MAB Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks |publisher=UNESCO |access-date=2026-10-21}}</ref> Its joint Technical Secretariat is co-managed by the [[Jeju Volcanic Island and Lava Tubes|Jeju Biosphere Reserve]] (South Korea) and the [[Menorca|Menorca Biosphere Reserve]] (Spain).<ref name="UNDESA-Partnership">{{cite web |title=World Network of Island and Coastal Biosphere Reserves (WNICBR) |url=https://sdgs.un.org/partnerships/world-network-island-and-coastal-biosphere-reserves-wnicbr |publisher=United Nations Department of Economic and Social Affairs |access-date=2026-10-21}}</ref>
t8anphrk2dxn5nhnxj9i4p9v9mjafpe
769639
769636
2026-10-22T10:27:51Z
Berkesmob
76347
/* */ fixed
769639
wikitext
text/x-wiki
{{Infobox organization
| name = World Network of Island and Coastal Biosphere Reserves
| abbreviation = WNICBR
| type = International thematic network
| parent_organisation = [[Man and the Biosphere Programme]] (UNESCO)
| headquarter = [[Jeju Province|Jeju]] ([[South Korea]]) & [[Menorca]] ([[Spain]])
| website = {{URL|https://www.unesco.org/en/mab/networks}}
}}
The '''World Network of Island and Coastal Biosphere Reserves''' ('''WNICBR''') is a thematic network under [[UNESCO]]'s [[Man and the Biosphere Programme|Man and the Biosphere (MAB) Programme]]. It promotes sustainable development, climate adaptation, and marine biodiversity conservation across island and coastal biosphere reserves globally.<ref name="UNESCO-Networks">{{cite web |title=UNESCO MAB Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks |publisher=UNESCO |access-date=2026-10-22}}</ref> Its joint Technical Secretariat is co-managed by the [[Jeju Volcanic Island and Lava Tubes|Jeju Biosphere Reserve]] (South Korea) and the [[Menorca|Menorca Biosphere Reserve]] (Spain).<ref name="UNDESA-Partnership">{{cite web |title=World Network of Island and Coastal Biosphere Reserves (WNICBR) |url=https://sdgs.un.org/partnerships/world-network-island-and-coastal-biosphere-reserves-wnicbr |publisher=United Nations Department of Economic and Social Affairs |access-date=2026-10-22}}</ref>
== History ==
In February 2009, representatives from several island and coastal biosphere reserves met in Menorca, Spain, to address shared ecological vulnerabilities such as sea-level rise, coastal erosion, and extreme weather. The network was formally established within UNESCO's MAB framework between 2009 and 2012.<ref name="UNDESA-Partnership" />
In 2012, a dual technical secretariat structure was set up in Jeju Island (South Korea) and Menorca (Spain) to manage administrative, scientific, and operational coordination.<ref name="IsleOfMan">{{cite web |title=World Network of Island and Coastal BRs |url=https://www.biosphere.im/our-networks/world-network-of-island-and-coastal-brs |publisher=UNESCO Biosphere Isle of Man |access-date=2026-10-22}}</ref> Since its founding, the network has expanded to include over 80 member sites across more than 30 countries.<ref name="UNESCO-Networks" />
== References ==
{{reflist}}
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
== History ==
In February 2009, representatives from several island and coastal biosphere reserves met in Menorca, Spain, to address shared vulnerabilities like sea-level rise, coastal erosion, and extreme weather. The network was formally established within UNESCO's MAB framework between 2009 and 2012.<ref name="UNDESA-Partnership" />
In 2012, a dual technical secretariat structure was set up in Jeju Island (South Korea) and Menorca (Spain) to manage administrative and scientific coordination.<ref name="IsleOfMan">{{cite web |title=World Network of Island and Coastal BRs |url=https://www.biosphere.im/our-networks/world-network-of-island-and-coastal-brs |publisher=UNESCO Biosphere Isle of Man |access-date=2026-10-22}}</ref> Since its founding, the network has grown to include over 80 member sites across more than 30 countries.<ref name="UNESCO-Networks" />
== Objectives and activities ==
The core mission of the network is to develop, test, and share sustainable management strategies suited to island and coastal ecosystems.
=== Main objectives ===
* '''Climate Change Adaptation:''' Developing local actions to counter sea-level rise, coastal flooding, and marine temperature changes.
* '''Biodiversity Conservation:''' Protecting sensitive habitats such as coral reefs, mangroves, estuaries, and island-endemic species.
* '''Sustainable Development:''' Promoting eco-friendly coastal tourism, sustainable fisheries, and renewable energy adoption.
* '''Knowledge Sharing:''' Facilitating joint scientific research, technical training, and educational exchange among site managers.
=== Key activities ===
* '''International Meetings:''' Regular global conferences bring together reserve representatives to evaluate progress and update work plans.
* '''Capacity Building:''' Annual training programs—frequently held at the Global Research and Training Centre for Internationally Designated Areas in Jeju—focus on climate resilience and resource management.
* '''Publications:''' The network publishes research reports, policy briefs, and bi-annual newsletters detailing best practices across member sites.<ref name="IsleOfMan" />
== Organization and cooperation ==
The network operates through a collaborative governance model involving local governments, scientific experts, and UNESCO leadership.
=== Technical Secretariat ===
Co-coordination is divided between two main locations:
# '''Jeju Biosphere Reserve (South Korea):''' Coordinates scientific research, training courses, and climate change initiatives.
# '''Menorca Biosphere Reserve (Spain):''' Manages communication, sustainable development models, and regional networking.
=== International alignment ===
The network's initiatives directly support the [[Sustainable Development Goals]] (SDGs) of the United Nations 2030 Agenda, with a primary focus on [[Sustainable Development Goal 14|SDG 14 (Life Below Water)]] and [[Sustainable Development Goal 13|SDG 13 (Climate Action)]].<ref name="UNDESA-Partnership" />
== See also ==
* [[Man and the Biosphere Programme]]
* [[World Network of Biosphere Reserves]]
* [[Biosphere reserve]]
* [[UNESCO]]
* [[Sustainable Development Goal 14]]
== References ==
{{reflist}}
== External links ==
* [https://www.unesco.org/en/mab/networks Official UNESCO MAB Regional and Thematic Networks Page]
* [https://sdgs.un.org/partnerships/world-network-island-and-coastal-biosphere-reserves-wnicbr UN SDGs Partnership Platform - WNICBR Profile]
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
>
== History ==
In February 2009, representatives from several island and coastal biosphere reserves met in Menorca, Spain, to address shared vulnerabilities like sea-level rise, coastal erosion, and extreme weather. The network was formally established within UNESCO's MAB framework between 2009 and 2012.<ref name="UNDESA-Partnership" />
In 2012, a dual technical secretariat structure was set up in Jeju Island (South Korea) and Menorca (Spain) to manage administrative and scientific coordination.<ref name="IsleOfMan">{{cite web |title=World Network of Island and Coastal BRs |url=https://www.biosphere.im/our-networks/world-network-of-island-and-coastal-brs |publisher=UNESCO Biosphere Isle of Man |access-date=2026-10-21}}</ref> Since its founding, the network has grown to include over 80 member sites across more than 30 countries.<ref name="UNESCO-Networks" />
The '''World Network of Island and Coastal Biosphere Reserves''' ('''WNICBR''') is a thematic network under [[UNESCO]]'s [[Man and the Biosphere Programme|Man and the Biosphere (MAB) Programme]]. It promotes sustainable development, climate adaptation, and marine biodiversity conservation across island and coastal biosphere reserves globally.<ref name="UNESCO-Networks">{{cite web |title=UNESCO MAB Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks |publisher=UNESCO |access-date=2026-10-21}}</ref> Its joint Technical Secretariat is co-managed by the [[Jeju Volcanic Island and Lava Tubes|Jeju Biosphere Reserve]] (South Korea) and the [[Menorca|Menorca Biosphere Reserve]] (Spain).<ref name="UNDESA-Partnership">{{cite web |title=World Network of Island and Coastal Biosphere Reserves (WNICBR) |url=https://sdgs.un.org/partnerships/world-network-island-and-coastal-biosphere-reserves-wnicbr |publisher=United Nations Department of Economic and Social Affairs |access-date=2026-10-21}}</ref>
4urq4g7wkylhudvq93g0s6t7l6aowqg
769642
769639
2026-10-22T10:29:11Z
Berkesmob
76347
/* */ fixed
769642
wikitext
text/x-wiki
{{Infobox organization
| name = World Network of Island and Coastal Biosphere Reserves
| abbreviation = WNICBR
| type = International thematic network
| parent_organisation = [[Man and the Biosphere Programme]] (UNESCO)
| headquarter = [[Jeju Province|Jeju]] ([[South Korea]]) & [[Menorca]] ([[Spain]])
| website = {{URL|https://www.unesco.org/en/mab/networks}}
}}
The '''World Network of Island and Coastal Biosphere Reserves''' ('''WNICBR''') is a thematic network operating under [[UNESCO]]'s [[Man and the Biosphere Programme|Man and the Biosphere (MAB) Programme]]. It promotes sustainable development, climate change adaptation, and marine biodiversity conservation across island and coastal biosphere reserves globally.<ref name="UNESCO-Networks">{{cite web |title=Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks |publisher=UNESCO |access-date=2026-10-22}}</ref> Its joint Technical Secretariat is co-managed by the [[Jeju Volcanic Island and Lava Tubes|Jeju Biosphere Reserve]] (South Korea) and the [[Menorca|Menorca Biosphere Reserve]] (Spain).<ref name="UNDESA-Partnership">{{cite web |title=World Network of Island and Coastal Biosphere Reserves (WNICBR) |url=https://sdgs.un.org/partnerships/world-network-island-and-coastal-biosphere-reserves-wnicbr |publisher=United Nations Department of Economic and Social Affairs |access-date=2026-10-22}}</ref>
== History ==
In February 2009, representatives from several island and coastal biosphere reserves met in Menorca, Spain, to address shared ecological vulnerabilities such as sea-level rise, coastal erosion, and extreme weather. The initiative led to the official launching of the network within UNESCO's MAB framework.<ref name="UNDESA-Partnership" />
The network was formally established in 2012, setting up a dual technical secretariat structure in Jeju Island (South Korea) and Menorca (Spain) to manage administrative, scientific, and operational coordination.<ref name="UNESCO-Anniversary">{{cite web |title=Celebrating the 10th anniversary of the World Network of Island and Coastal Biosphere Reserves |url=https://www.unesco.org/en/articles/celebrating-10th-anniversary-world-network-island-and-coastal-biosphere-reserves |publisher=UNESCO |access-date=2026-10-22}}</ref> Since its establishment, the network has grown to include member sites across more than 30 countries.<ref name="UNESCO-Networks" />
== References ==
{{reflist}}
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
== History ==
In February 2009, representatives from several island and coastal biosphere reserves met in Menorca, Spain, to address shared ecological vulnerabilities such as sea-level rise, coastal erosion, and extreme weather. The network was formally established within UNESCO's MAB framework between 2009 and 2012.<ref name="UNDESA-Partnership" />
In 2012, a dual technical secretariat structure was set up in Jeju Island (South Korea) and Menorca (Spain) to manage administrative, scientific, and operational coordination.<ref name="IsleOfMan">{{cite web |title=World Network of Island and Coastal BRs |url=https://www.biosphere.im/our-networks/world-network-of-island-and-coastal-brs |publisher=UNESCO Biosphere Isle of Man |access-date=2026-10-22}}</ref> Since its founding, the network has expanded to include over 80 member sites across more than 30 countries.<ref name="UNESCO-Networks" />
== References ==
{{reflist}}
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
== History ==
In February 2009, representatives from several island and coastal biosphere reserves met in Menorca, Spain, to address shared vulnerabilities like sea-level rise, coastal erosion, and extreme weather. The network was formally established within UNESCO's MAB framework between 2009 and 2012.<ref name="UNDESA-Partnership" />
In 2012, a dual technical secretariat structure was set up in Jeju Island (South Korea) and Menorca (Spain) to manage administrative and scientific coordination.<ref name="IsleOfMan">{{cite web |title=World Network of Island and Coastal BRs |url=https://www.biosphere.im/our-networks/world-network-of-island-and-coastal-brs |publisher=UNESCO Biosphere Isle of Man |access-date=2026-10-22}}</ref> Since its founding, the network has grown to include over 80 member sites across more than 30 countries.<ref name="UNESCO-Networks" />
== Objectives and activities ==
The core mission of the network is to develop, test, and share sustainable management strategies suited to island and coastal ecosystems.
=== Main objectives ===
* '''Climate Change Adaptation:''' Developing local actions to counter sea-level rise, coastal flooding, and marine temperature changes.
* '''Biodiversity Conservation:''' Protecting sensitive habitats such as coral reefs, mangroves, estuaries, and island-endemic species.
* '''Sustainable Development:''' Promoting eco-friendly coastal tourism, sustainable fisheries, and renewable energy adoption.
* '''Knowledge Sharing:''' Facilitating joint scientific research, technical training, and educational exchange among site managers.
=== Key activities ===
* '''International Meetings:''' Regular global conferences bring together reserve representatives to evaluate progress and update work plans.
* '''Capacity Building:''' Annual training programs—frequently held at the Global Research and Training Centre for Internationally Designated Areas in Jeju—focus on climate resilience and resource management.
* '''Publications:''' The network publishes research reports, policy briefs, and bi-annual newsletters detailing best practices across member sites.<ref name="IsleOfMan" />
== Organization and cooperation ==
The network operates through a collaborative governance model involving local governments, scientific experts, and UNESCO leadership.
=== Technical Secretariat ===
Co-coordination is divided between two main locations:
# '''Jeju Biosphere Reserve (South Korea):''' Coordinates scientific research, training courses, and climate change initiatives.
# '''Menorca Biosphere Reserve (Spain):''' Manages communication, sustainable development models, and regional networking.
=== International alignment ===
The network's initiatives directly support the [[Sustainable Development Goals]] (SDGs) of the United Nations 2030 Agenda, with a primary focus on [[Sustainable Development Goal 14|SDG 14 (Life Below Water)]] and [[Sustainable Development Goal 13|SDG 13 (Climate Action)]].<ref name="UNDESA-Partnership" />
== See also ==
* [[Man and the Biosphere Programme]]
* [[World Network of Biosphere Reserves]]
* [[Biosphere reserve]]
* [[UNESCO]]
* [[Sustainable Development Goal 14]]
== References ==
{{reflist}}
== External links ==
* [https://www.unesco.org/en/mab/networks Official UNESCO MAB Regional and Thematic Networks Page]
* [https://sdgs.un.org/partnerships/world-network-island-and-coastal-biosphere-reserves-wnicbr UN SDGs Partnership Platform - WNICBR Profile]
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
>
== History ==
In February 2009, representatives from several island and coastal biosphere reserves met in Menorca, Spain, to address shared vulnerabilities like sea-level rise, coastal erosion, and extreme weather. The network was formally established within UNESCO's MAB framework between 2009 and 2012.<ref name="UNDESA-Partnership" />
In 2012, a dual technical secretariat structure was set up in Jeju Island (South Korea) and Menorca (Spain) to manage administrative and scientific coordination.<ref name="IsleOfMan">{{cite web |title=World Network of Island and Coastal BRs |url=https://www.biosphere.im/our-networks/world-network-of-island-and-coastal-brs |publisher=UNESCO Biosphere Isle of Man |access-date=2026-10-21}}</ref> Since its founding, the network has grown to include over 80 member sites across more than 30 countries.<ref name="UNESCO-Networks" />
The '''World Network of Island and Coastal Biosphere Reserves''' ('''WNICBR''') is a thematic network under [[UNESCO]]'s [[Man and the Biosphere Programme|Man and the Biosphere (MAB) Programme]]. It promotes sustainable development, climate adaptation, and marine biodiversity conservation across island and coastal biosphere reserves globally.<ref name="UNESCO-Networks">{{cite web |title=UNESCO MAB Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks |publisher=UNESCO |access-date=2026-10-21}}</ref> Its joint Technical Secretariat is co-managed by the [[Jeju Volcanic Island and Lava Tubes|Jeju Biosphere Reserve]] (South Korea) and the [[Menorca|Menorca Biosphere Reserve]] (Spain).<ref name="UNDESA-Partnership">{{cite web |title=World Network of Island and Coastal Biosphere Reserves (WNICBR) |url=https://sdgs.un.org/partnerships/world-network-island-and-coastal-biosphere-reserves-wnicbr |publisher=United Nations Department of Economic and Social Affairs |access-date=2026-10-21}}</ref>
fdh4ckzy59kp4aso5lx2u6qmr2zp6zh
769647
769642
2026-10-22T10:31:09Z
Berkesmob
76347
/* */ rewritten
769647
wikitext
text/x-wiki
{{Infobox organization
| name = World Network of Island and Coastal Biosphere Reserves
| abbreviation = WNICBR
| type = International thematic network
| parent_organisation = [[Man and the Biosphere Programme]] (UNESCO)
| headquarter = [[Jeju Province|Jeju]] ([[South Korea]]) & [[Menorca]] ([[Spain]])
| website = {{URL|https://www.unesco.org/en/mab/networks}}
}}
The '''World Network of Island and Coastal Biosphere Reserves''' ('''WNICBR''') is a thematic network operating under [[UNESCO]]'s [[Man and the Biosphere Programme|Man and the Biosphere (MAB) Programme]]. It promotes sustainable development, climate change adaptation, and marine biodiversity conservation across island and coastal biosphere reserves globally.<ref name="UNESCO-Networks">{{cite web |title=Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks |publisher=UNESCO |access-date=2026-10-22}}</ref> Its joint Technical Secretariat is co-managed by the [[Jeju Volcanic Island and Lava Tubes|Jeju Biosphere Reserve]] (South Korea) and the [[Menorca|Menorca Biosphere Reserve]] (Spain).<ref name="UNDESA-Partnership">{{cite web |title=World Network of Island and Coastal Biosphere Reserves (WNICBR) |url=https://sdgs.un.org/partnerships/world-network-island-and-coastal-biosphere-reserves-wnicbr |publisher=United Nations Department of Economic and Social Affairs |access-date=2026-10-22}}</ref>
== References ==
{{reflist}}
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
== History ==
In February 2009, representatives from several island and coastal biosphere reserves met in Menorca, Spain, to address shared ecological vulnerabilities such as sea-level rise, coastal erosion, and extreme weather. The initiative led to the official launching of the network within UNESCO's MAB framework.<ref name="UNDESA-Partnership" />
The network was formally established in 2012, setting up a dual technical secretariat structure in Jeju Island (South Korea) and Menorca (Spain) to manage administrative, scientific, and operational coordination.<ref name="UNESCO-Anniversary">{{cite web |title=Celebrating the 10th anniversary of the World Network of Island and Coastal Biosphere Reserves |url=https://www.unesco.org/en/articles/celebrating-10th-anniversary-world-network-island-and-coastal-biosphere-reserves |publisher=UNESCO |access-date=2026-10-22}}</ref> Since its establishment, the network has grown to include member sites across more than 30 countries.<ref name="UNESCO-Networks" />
== References ==
{{reflist}}
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
== History ==
In February 2009, representatives from several island and coastal biosphere reserves met in Menorca, Spain, to address shared ecological vulnerabilities such as sea-level rise, coastal erosion, and extreme weather. The network was formally established within UNESCO's MAB framework between 2009 and 2012.<ref name="UNDESA-Partnership" />
In 2012, a dual technical secretariat structure was set up in Jeju Island (South Korea) and Menorca (Spain) to manage administrative, scientific, and operational coordination.<ref name="IsleOfMan">{{cite web |title=World Network of Island and Coastal BRs |url=https://www.biosphere.im/our-networks/world-network-of-island-and-coastal-brs |publisher=UNESCO Biosphere Isle of Man |access-date=2026-10-22}}</ref> Since its founding, the network has expanded to include over 80 member sites across more than 30 countries.<ref name="UNESCO-Networks" />
== References ==
{{reflist}}
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
== History ==
In February 2009, representatives from several island and coastal biosphere reserves met in Menorca, Spain, to address shared vulnerabilities like sea-level rise, coastal erosion, and extreme weather. The network was formally established within UNESCO's MAB framework between 2009 and 2012.<ref name="UNDESA-Partnership" />
In 2012, a dual technical secretariat structure was set up in Jeju Island (South Korea) and Menorca (Spain) to manage administrative and scientific coordination.<ref name="IsleOfMan">{{cite web |title=World Network of Island and Coastal BRs |url=https://www.biosphere.im/our-networks/world-network-of-island-and-coastal-brs |publisher=UNESCO Biosphere Isle of Man |access-date=2026-10-22}}</ref> Since its founding, the network has grown to include over 80 member sites across more than 30 countries.<ref name="UNESCO-Networks" />
== Objectives and activities ==
The core mission of the network is to develop, test, and share sustainable management strategies suited to island and coastal ecosystems.
=== Main objectives ===
* '''Climate Change Adaptation:''' Developing local actions to counter sea-level rise, coastal flooding, and marine temperature changes.
* '''Biodiversity Conservation:''' Protecting sensitive habitats such as coral reefs, mangroves, estuaries, and island-endemic species.
* '''Sustainable Development:''' Promoting eco-friendly coastal tourism, sustainable fisheries, and renewable energy adoption.
* '''Knowledge Sharing:''' Facilitating joint scientific research, technical training, and educational exchange among site managers.
=== Key activities ===
* '''International Meetings:''' Regular global conferences bring together reserve representatives to evaluate progress and update work plans.
* '''Capacity Building:''' Annual training programs—frequently held at the Global Research and Training Centre for Internationally Designated Areas in Jeju—focus on climate resilience and resource management.
* '''Publications:''' The network publishes research reports, policy briefs, and bi-annual newsletters detailing best practices across member sites.<ref name="IsleOfMan" />
== Organization and cooperation ==
The network operates through a collaborative governance model involving local governments, scientific experts, and UNESCO leadership.
=== Technical Secretariat ===
Co-coordination is divided between two main locations:
# '''Jeju Biosphere Reserve (South Korea):''' Coordinates scientific research, training courses, and climate change initiatives.
# '''Menorca Biosphere Reserve (Spain):''' Manages communication, sustainable development models, and regional networking.
=== International alignment ===
The network's initiatives directly support the [[Sustainable Development Goals]] (SDGs) of the United Nations 2030 Agenda, with a primary focus on [[Sustainable Development Goal 14|SDG 14 (Life Below Water)]] and [[Sustainable Development Goal 13|SDG 13 (Climate Action)]].<ref name="UNDESA-Partnership" />
== See also ==
* [[Man and the Biosphere Programme]]
* [[World Network of Biosphere Reserves]]
* [[Biosphere reserve]]
* [[UNESCO]]
* [[Sustainable Development Goal 14]]
== References ==
{{reflist}}
== External links ==
* [https://www.unesco.org/en/mab/networks Official UNESCO MAB Regional and Thematic Networks Page]
* [https://sdgs.un.org/partnerships/world-network-island-and-coastal-biosphere-reserves-wnicbr UN SDGs Partnership Platform - WNICBR Profile]
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
>
== History ==
In February 2009, representatives from several island and coastal biosphere reserves met in Menorca, Spain, to address shared vulnerabilities like sea-level rise, coastal erosion, and extreme weather. The network was formally established within UNESCO's MAB framework between 2009 and 2012.<ref name="UNDESA-Partnership" />
In 2012, a dual technical secretariat structure was set up in Jeju Island (South Korea) and Menorca (Spain) to manage administrative and scientific coordination.<ref name="IsleOfMan">{{cite web |title=World Network of Island and Coastal BRs |url=https://www.biosphere.im/our-networks/world-network-of-island-and-coastal-brs |publisher=UNESCO Biosphere Isle of Man |access-date=2026-10-21}}</ref> Since its founding, the network has grown to include over 80 member sites across more than 30 countries.<ref name="UNESCO-Networks" />
The '''World Network of Island and Coastal Biosphere Reserves''' ('''WNICBR''') is a thematic network under [[UNESCO]]'s [[Man and the Biosphere Programme|Man and the Biosphere (MAB) Programme]]. It promotes sustainable development, climate adaptation, and marine biodiversity conservation across island and coastal biosphere reserves globally.<ref name="UNESCO-Networks">{{cite web |title=UNESCO MAB Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks |publisher=UNESCO |access-date=2026-10-21}}</ref> Its joint Technical Secretariat is co-managed by the [[Jeju Volcanic Island and Lava Tubes|Jeju Biosphere Reserve]] (South Korea) and the [[Menorca|Menorca Biosphere Reserve]] (Spain).<ref name="UNDESA-Partnership">{{cite web |title=World Network of Island and Coastal Biosphere Reserves (WNICBR) |url=https://sdgs.un.org/partnerships/world-network-island-and-coastal-biosphere-reserves-wnicbr |publisher=United Nations Department of Economic and Social Affairs |access-date=2026-10-21}}</ref>
onn5ubiz7ep9s74egre01vc0n6a25cr
769663
769647
2026-10-22T10:37:57Z
Berkesmob
76347
/* */ fixed edited
769663
wikitext
text/x-wiki
{{Infobox organization
| name = World Network of Island and Coastal Biosphere Reserves
| abbreviation = WNICBR
| type = International thematic network
| parent_organisation = [[Man and the Biosphere Programme]] (UNESCO)
| headquarter = [[Jeju Province|Jeju]] ([[South Korea]]) & [[Menorca]] ([[Spain]])
| website = {{URL|https://www.unesco.org/en/mab/networks}}
}}
The '''World Network of Island and Coastal Biosphere Reserves''' ('''WNICBR''') is a thematic network operating under [[UNESCO]]'s [[Man and the Biosphere Programme|Man and the Biosphere (MAB) Programme]]. It promotes sustainable development, climate change adaptation, and marine biodiversity conservation across island and coastal biosphere reserves globally.<ref name="UNESCO-Networks">{{cite web |title=Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks |publisher=UNESCO |access-date=2026-10-22}}</ref> Its joint Technical Secretariat is co-managed by the [[Jeju Volcanic Island and Lava Tubes|Jeju Biosphere Reserve]] (South Korea) and the [[Menorca|Menorca Biosphere Reserve]] (Spain).<ref name="UNDESA-Partnership">{{cite web |title=World Network of Island and Coastal Biosphere Reserves (WNICBR) |url=https://sdgs.un.org/partnerships/world-network-island-and-coastal-biosphere-reserves-wnicbr |publisher=United Nations Department of Economic and Social Affairs |access-date=2026-10-22}}</ref>
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
== History ==
In February 2009, representatives from several island and coastal biosphere reserves met in Menorca, Spain, to address shared ecological vulnerabilities such as sea-level rise, coastal erosion, and extreme weather. The initiative led to the official launching of the network within UNESCO's MAB framework.<ref name="UNDESA-Partnership" />
The network was formally established in 2012, setting up a dual technical secretariat structure in Jeju Island (South Korea) and Menorca (Spain) to manage administrative, scientific, and operational coordination.<ref name="UNESCO-Anniversary">{{cite web |title=Celebrating the 10th anniversary of the World Network of Island and Coastal Biosphere Reserves |url=https://www.unesco.org/en/articles/celebrating-10th-anniversary-world-network-island-and-coastal-biosphere-reserves |publisher=UNESCO |access-date=2026-10-22}}</ref> Since its establishment, the network has grown to include member sites across more than 30 countries.<ref name="UNESCO-Networks" />
== References ==
{{reflist}}
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
n1mx1v19gyu6yf3kt562n9xmw0xf3b2
769670
769663
2026-10-22T10:42:34Z
Berkesmob
76347
/* */
769670
wikitext
text/x-wiki
{{Infobox organization
| name = World Network of Island and Coastal Biosphere Reserves
| abbreviation = WNICBR
| type = International thematic network
| parent_organisation = [[Man and the Biosphere Programme]] (UNESCO)
| headquarter = [[Jeju Province|Jeju]] ([[South Korea]]) & [[Menorca]] ([[Spain]])
| website = {{URL|https://www.unesco.org/en/mab/networks}}
}}
The '''World Network of Island and Coastal Biosphere Reserves''' ('''WNICBR''') is a thematic network operating under [[UNESCO]]'s [[Man and the Biosphere Programme|Man and the Biosphere (MAB) Programme]]. It promotes sustainable development, climate change adaptation, and marine biodiversity conservation across island and coastal biosphere reserves globally.<ref name="UNESCO-Networks">{{cite web |title=Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks |publisher=UNESCO |access-date=2026-10-22}}</ref> Its joint Technical Secretariat is co-managed by the [[Jeju Volcanic Island and Lava Tubes|Jeju Biosphere Reserve]] (South Korea) and the [[Menorca|Menorca Biosphere Reserve]] (Spain).<ref name="UNDESA-Partnership">{{cite web |title=World Network of Island and Coastal Biosphere Reserves (WNICBR) |url=https://sdgs.un.org/partnerships/world-network-island-coastal-biosphere-reserves-wnicbr |publisher=United Nations Department of Economic and Social Affairs |access-date=2026-10-22}}</ref>
== Objectives and activities ==
The network aims to strengthen cooperation among island and coastal biosphere reserves, promote the conservation of marine and coastal biodiversity, and support sustainable development and climate change adaptation. It encourages the exchange of scientific knowledge, good practices, and experiences between member reserves, while supporting research, environmental education, and local community participation in conservation initiatives.<ref name="UNESCO-Networks" /><ref name="UNDESA-Partnership" />
Its activities include international collaboration and knowledge-sharing between biosphere reserves, particularly through its joint Technical Secretariat co-managed by the Jeju Biosphere Reserve in South Korea and the Menorca Biosphere Reserve in Spain. These efforts help participating reserves share approaches to common environmental challenges and promote sustainable management of island and coastal ecosystems.
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
== History ==
In February 2009, representatives from several island and coastal biosphere reserves met in Menorca, Spain, to address shared ecological vulnerabilities such as sea-level rise, coastal erosion, and extreme weather. The initiative led to the official launching of the network within UNESCO's MAB framework.<ref name="UNDESA-Partnership" />
The network was formally established in 2012, setting up a dual technical secretariat structure in Jeju Island (South Korea) and Menorca (Spain) to manage administrative, scientific, and operational coordination.<ref name="UNESCO-Anniversary">{{cite web |title=Celebrating the 10th anniversary of the World Network of Island and Coastal Biosphere Reserves |url=https://www.unesco.org/en/articles/celebrating-10th-anniversary-world-network-island-and-coastal-biosphere-reserves |publisher=UNESCO |access-date=2026-10-22}}</ref> Since its establishment, the network has grown to include member sites across more than 30 countries.<ref name="UNESCO-Networks" />
== References ==
{{reflist}}
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
5txi86pbashkyq3t9rn6tzahsxqx4z6
769671
769670
2026-10-22T10:43:25Z
Berkesmob
76347
/* */ typo
769671
wikitext
text/x-wiki
{{Infobox organization
| name = World Network of Island and Coastal Biosphere Reserves
| abbreviation = WNICBR
| type = International thematic network
| parent_organisation = [[Man and the Biosphere Programme]] (UNESCO)
| headquarter = [[Jeju Province|Jeju]] ([[South Korea]]) & [[Menorca]] ([[Spain]])
| website = {{URL|https://www.unesco.org/en/mab/networks}}
}}
'''World Network of Island and Coastal Biosphere Reserves''' ('''WNICBR''') is a thematic network operating under [[UNESCO]]'s [[Man and the Biosphere Programme|Man and the Biosphere (MAB) Programme]]. It promotes sustainable development, climate change adaptation, and marine biodiversity conservation across island and coastal biosphere reserves globally.<ref name="UNESCO-Networks">{{cite web |title=Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks |publisher=UNESCO |access-date=2026-10-22}}</ref> Its joint Technical Secretariat is co-managed by the [[Jeju Volcanic Island and Lava Tubes|Jeju Biosphere Reserve]] (South Korea) and the [[Menorca|Menorca Biosphere Reserve]] (Spain).<ref name="UNDESA-Partnership">{{cite web |title=World Network of Island and Coastal Biosphere Reserves (WNICBR) |url=https://sdgs.un.org/partnerships/world-network-island-coastal-biosphere-reserves-wnicbr |publisher=United Nations Department of Economic and Social Affairs |access-date=2026-10-22}}</ref>
== Objectives and activities ==
The network aims to strengthen cooperation among island and coastal biosphere reserves, promote the conservation of marine and coastal biodiversity, and support sustainable development and climate change adaptation. It encourages the exchange of scientific knowledge, good practices, and experiences between member reserves, while supporting research, environmental education, and local community participation in conservation initiatives.<ref name="UNESCO-Networks" /><ref name="UNDESA-Partnership" />
Its activities include international collaboration and knowledge-sharing between biosphere reserves, particularly through its joint Technical Secretariat co-managed by the Jeju Biosphere Reserve in South Korea and the Menorca Biosphere Reserve in Spain. These efforts help participating reserves share approaches to common environmental challenges and promote sustainable management of island and coastal ecosystems.
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
== History ==
In February 2009, representatives from several island and coastal biosphere reserves met in Menorca, Spain, to address shared ecological vulnerabilities such as sea-level rise, coastal erosion, and extreme weather. The initiative led to the official launching of the network within UNESCO's MAB framework.<ref name="UNDESA-Partnership" />
The network was formally established in 2012, setting up a dual technical secretariat structure in Jeju Island (South Korea) and Menorca (Spain) to manage administrative, scientific, and operational coordination.<ref name="UNESCO-Anniversary">{{cite web |title=Celebrating the 10th anniversary of the World Network of Island and Coastal Biosphere Reserves |url=https://www.unesco.org/en/articles/celebrating-10th-anniversary-world-network-island-and-coastal-biosphere-reserves |publisher=UNESCO |access-date=2026-10-22}}</ref> Since its establishment, the network has grown to include member sites across more than 30 countries.<ref name="UNESCO-Networks" />
== References ==
{{reflist}}
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
gb0rylac18xcc384l8r6enaoad7ih7z
769672
769671
2026-10-22T10:44:14Z
Berkesmob
76347
/* */ typo
769672
wikitext
text/x-wiki
{{Infobox organization
| name = World Network of Island and Coastal Biosphere Reserves
| abbreviation = WNICBR
| type = International thematic network
| parent_organisation = [[Man and the Biosphere Programme]] (UNESCO)
| headquarter = [[Jeju Province|Jeju]] ([[South Korea]]) & [[Menorca]] ([[Spain]])
| website = {{URL|https://www.unesco.org/en/mab/networks}}
}}
The '''World Network of Island and Coastal Biosphere Reserves''' ('''WNICBR''') is a thematic network operating under [[UNESCO]]'s [[Man and the Biosphere Programme|Man and the Biosphere (MAB) Programme]]. It promotes sustainable development, climate change adaptation, and marine biodiversity conservation across island and coastal biosphere reserves globally.<ref name="UNESCO-Networks">{{cite web |title=Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks |publisher=UNESCO |access-date=2026-10-22}}</ref> Its joint Technical Secretariat is co-managed by the [[Jeju Volcanic Island and Lava Tubes|Jeju Biosphere Reserve]] (South Korea) and the [[Menorca|Menorca Biosphere Reserve]] (Spain).<ref name="UNDESA-Partnership">{{cite web |title=World Network of Island and Coastal Biosphere Reserves (WNICBR) |url=https://sdgs.un.org/partnerships/world-network-island-coastal-biosphere-reserves-wnicbr |publisher=United Nations Department of Economic and Social Affairs |access-date=2026-10-22}}</ref>
== Objectives and activities ==
The network aims to strengthen cooperation among island and coastal biosphere reserves, promote the conservation of marine and coastal biodiversity, and support sustainable development and climate change adaptation. It encourages the exchange of scientific knowledge, good practices, and experiences between member reserves, while supporting research, environmental education, and local community participation in conservation initiatives.<ref name="UNESCO-Networks" /><ref name="UNDESA-Partnership" />
Its activities include international collaboration and knowledge-sharing between biosphere reserves, particularly through its joint Technical Secretariat co-managed by the Jeju Biosphere Reserve in South Korea and the Menorca Biosphere Reserve in Spain. These efforts help participating reserves share approaches to common environmental challenges and promote sustainable management of island and coastal ecosystems.
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
== History ==
In February 2009, representatives from several island and coastal biosphere reserves met in Menorca, Spain, to address shared ecological vulnerabilities such as sea-level rise, coastal erosion, and extreme weather. The initiative led to the official launching of the network within UNESCO's MAB framework.<ref name="UNDESA-Partnership" />
The network was formally established in 2012, setting up a dual technical secretariat structure in Jeju Island (South Korea) and Menorca (Spain) to manage administrative, scientific, and operational coordination.<ref name="UNESCO-Anniversary">{{cite web |title=Celebrating the 10th anniversary of the World Network of Island and Coastal Biosphere Reserves |url=https://www.unesco.org/en/articles/celebrating-10th-anniversary-world-network-island-and-coastal-biosphere-reserves |publisher=UNESCO |access-date=2026-10-22}}</ref> Since its establishment, the network has grown to include member sites across more than 30 countries.<ref name="UNESCO-Networks" />
== References ==
{{reflist}}
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
5txi86pbashkyq3t9rn6tzahsxqx4z6
769682
769672
2026-10-22T10:59:42Z
Berkesmob
76347
/* */ added content
769682
wikitext
text/x-wiki
{{Infobox organization
| name = World Network of Island and Coastal Biosphere Reserves
| abbreviation = WNICBR
| type = International thematic network
| parent_organisation = [[Man and the Biosphere Programme]] (UNESCO)
| headquarter = [[Jeju Province|Jeju]] ([[South Korea]]) & [[Menorca]] ([[Spain]])
| website = {{URL|https://www.unesco.org/en/mab/networks}}
}}
The '''World Network of Island and Coastal Biosphere Reserves''' ('''WNICBR''') is a thematic network operating under [[UNESCO]]'s [[Man and the Biosphere Programme|Man and the Biosphere (MAB) Programme]]. It promotes sustainable development, climate change adaptation, and marine biodiversity conservation across island and coastal biosphere reserves globally.<ref name="UNESCO-Networks">{{cite web |title=Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks |publisher=UNESCO |access-date=2026-10-22}}</ref> Its joint Technical Secretariat is co-managed by the [[Jeju Volcanic Island and Lava Tubes|Jeju Biosphere Reserve]] (South Korea) and the [[Menorca|Menorca Biosphere Reserve]] (Spain).<ref name="UNDESA-Partnership">{{cite web |title=World Network of Island and Coastal Biosphere Reserves (WNICBR) |url=https://sdgs.un.org/partnerships/world-network-island-coastal-biosphere-reserves-wnicbr |publisher=United Nations Department of Economic and Social Affairs |access-date=2026-10-22}}</ref>
== Objectives and activities ==
The network aims to strengthen cooperation among island and coastal biosphere reserves, promote the conservation of marine and coastal biodiversity, and support sustainable development and climate change adaptation. It encourages the exchange of scientific knowledge, good practices, and experiences between member reserves, while supporting research, environmental education, and local community participation in conservation initiatives.<ref name="UNESCO-Networks" /><ref name="UNDESA-Partnership" />
Its activities include international collaboration and knowledge-sharing between biosphere reserves, particularly through its joint Technical Secretariat co-managed by the Jeju Biosphere Reserve in South Korea and the Menorca Biosphere Reserve in Spain. These efforts help participating reserves share approaches to common environmental challenges and promote sustainable management of island and coastal ecosystems.
== Organization and cooperation ==
The World Network of Island and Coastal Biosphere Reserves operates under [[UNESCO]]'s [[Man and the Biosphere Programme]]. Its Technical Secretariat is shared by the [[Jeju Island Biosphere Reserve]] in South Korea and the [[Menorca|Menorca Biosphere Reserve]] in Spain, which coordinate the network's activities and facilitate cooperation among participating reserves.<ref name="UNESCO-Networks" />
The network brings together more than 80 biosphere reserves in 30 countries. Members cooperate through international meetings, collaborative research, knowledge exchange, and capacity-building activities focused on biodiversity conservation, sustainable development, and climate change adaptation.<ref name="UNESCO-Networks" />
An example of this cooperation was the network's 11th meeting, held in September 2023 at the [[Great Sandy Biosphere Reserve]] in Australia. The network also held its 12th annual meeting online on 12 March 2025, addressing future participation in international biosphere reserve events, the inclusion of new members, and training for network managers.<ref>{{cite web |title=National Report for the 37th MAB ICC: Spain |url=https://articles.unesco.org/sites/default/files/medias/fichiers/2025/09/SPAIN_Informe%20actividades-Espan%CC%83a%20%28CIC%202025%29%20ENG.pdf |publisher=UNESCO |access-date=2026-10-22}}</ref>
At the wider programme level, the [[UNESCO]] MAB Secretariat coordinates the World Network of Biosphere Reserves and its regional and thematic networks, supporting international collaboration and the exchange of experience between participating sites.<ref>{{cite web |title=MAB Secretariat |url=https://www.unesco.org/en/mab/secretariat |publisher=UNESCO |access-date=2026-10-22}}</ref>
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
== Objectives and activities ==
The network aims to strengthen cooperation among island and coastal biosphere reserves, promote the conservation of marine and coastal biodiversity, and support sustainable development and climate change adaptation. It encourages the exchange of scientific knowledge, good practices, and experiences between member reserves, while supporting research, environmental education, and local community participation in conservation initiatives.<ref name="UNESCO-Networks" /><ref name="UNDESA-Partnership" />
Its activities include international collaboration and knowledge-sharing between biosphere reserves, particularly through its joint Technical Secretariat co-managed by the Jeju Biosphere Reserve in South Korea and the Menorca Biosphere Reserve in Spain. These efforts help participating reserves share approaches to common environmental challenges and promote sustainable management of island and coastal ecosystems.
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
== History ==
In February 2009, representatives from several island and coastal biosphere reserves met in Menorca, Spain, to address shared ecological vulnerabilities such as sea-level rise, coastal erosion, and extreme weather. The initiative led to the official launching of the network within UNESCO's MAB framework.<ref name="UNDESA-Partnership" />
The network was formally established in 2012, setting up a dual technical secretariat structure in Jeju Island (South Korea) and Menorca (Spain) to manage administrative, scientific, and operational coordination.<ref name="UNESCO-Anniversary">{{cite web |title=Celebrating the 10th anniversary of the World Network of Island and Coastal Biosphere Reserves |url=https://www.unesco.org/en/articles/celebrating-10th-anniversary-world-network-island-and-coastal-biosphere-reserves |publisher=UNESCO |access-date=2026-10-22}}</ref> Since its establishment, the network has grown to include member sites across more than 30 countries.<ref name="UNESCO-Networks" />
== References ==
{{reflist}}
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
9zxju6z7lgz4bhe7dgobt5c9ogf0it4
769684
769682
2026-10-22T11:00:50Z
Berkesmob
76347
fix
769684
wikitext
text/x-wiki
{{Infobox organization
| name = World Network of Island and Coastal Biosphere Reserves
| abbreviation = WNICBR
| type = International thematic network
| parent_organisation = [[Man and the Biosphere Programme]] (UNESCO)
| headquarter = [[Jeju Province|Jeju]] ([[South Korea]]) & [[Menorca]] ([[Spain]])
| website = {{URL|https://www.unesco.org/en/mab/networks}}
}}
The '''World Network of Island and Coastal Biosphere Reserves''' ('''WNICBR''') is a thematic network operating under [[UNESCO]]'s [[Man and the Biosphere Programme|Man and the Biosphere (MAB) Programme]]. It promotes sustainable development, climate change adaptation, and marine biodiversity conservation across island and coastal biosphere reserves globally.<ref name="UNESCO-Networks">{{cite web |title=Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks |publisher=UNESCO |access-date=2026-10-22}}</ref> Its joint Technical Secretariat is co-managed by the [[Jeju Volcanic Island and Lava Tubes|Jeju Biosphere Reserve]] (South Korea) and the [[Menorca|Menorca Biosphere Reserve]] (Spain).<ref name="UNDESA-Partnership">{{cite web |title=World Network of Island and Coastal Biosphere Reserves (WNICBR) |url=https://sdgs.un.org/partnerships/world-network-island-coastal-biosphere-reserves-wnicbr |publisher=United Nations Department of Economic and Social Affairs |access-date=2026-10-22}}</ref>
== Objectives and activities ==
The network aims to strengthen cooperation among island and coastal biosphere reserves, promote the conservation of marine and coastal biodiversity, and support sustainable development and climate change adaptation. It encourages the exchange of scientific knowledge, good practices, and experiences between member reserves, while supporting research, environmental education, and local community participation in conservation initiatives.<ref name="UNESCO-Networks" /><ref name="UNDESA-Partnership" />
Its activities include international collaboration and knowledge-sharing between biosphere reserves, particularly through its joint Technical Secretariat co-managed by the Jeju Biosphere Reserve in South Korea and the Menorca Biosphere Reserve in Spain. These efforts help participating reserves share approaches to common environmental challenges and promote sustainable management of island and coastal ecosystems.
== Organization and cooperation ==
The World Network of Island and Coastal Biosphere Reserves operates under [[UNESCO]]'s [[Man and the Biosphere Programme]]. Its Technical Secretariat is shared by the [[Jeju Island Biosphere Reserve]] in South Korea and the [[Menorca|Menorca Biosphere Reserve]] in Spain, which coordinate the network's activities and facilitate cooperation among participating reserves.<ref name="UNESCO-Networks" />
The network brings together more than 80 biosphere reserves in 30 countries. Members cooperate through international meetings, collaborative research, knowledge exchange, and capacity-building activities focused on biodiversity conservation, sustainable development, and climate change adaptation.<ref name="UNESCO-Networks" />
An example of this cooperation was the network's 11th meeting, held in September 2023 at the [[Great Sandy Biosphere Reserve]] in Australia. The network also held its 12th annual meeting online on 12 March 2025, addressing future participation in international biosphere reserve events, the inclusion of new members, and training for network managers.<ref>{{cite web |title=National Report for the 37th MAB ICC: Spain |url=https://articles.unesco.org/sites/default/files/medias/fichiers/2025/09/SPAIN_Informe%20actividades-Espan%CC%83a%20%28CIC%202025%29%20ENG.pdf |publisher=UNESCO |access-date=2026-10-22}}</ref>
At the wider programme level, the [[UNESCO]] MAB Secretariat coordinates the World Network of Biosphere Reserves and its regional and thematic networks, supporting international collaboration and the exchange of experience between participating sites.<ref>{{cite web |title=MAB Secretariat |url=https://www.unesco.org/en/mab/secretariat |publisher=UNESCO |access-date=2026-10-22}}</ref>
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
== History ==
In February 2009, representatives from several island and coastal biosphere reserves met in Menorca, Spain, to address shared ecological vulnerabilities such as sea-level rise, coastal erosion, and extreme weather. The initiative led to the official launching of the network within UNESCO's MAB framework.<ref name="UNDESA-Partnership" />
The network was formally established in 2012, setting up a dual technical secretariat structure in Jeju Island (South Korea) and Menorca (Spain) to manage administrative, scientific, and operational coordination.<ref name="UNESCO-Anniversary">{{cite web |title=Celebrating the 10th anniversary of the World Network of Island and Coastal Biosphere Reserves |url=https://www.unesco.org/en/articles/celebrating-10th-anniversary-world-network-island-and-coastal-biosphere-reserves |publisher=UNESCO |access-date=2026-10-22}}</ref> Since its establishment, the network has grown to include member sites across more than 30 countries.<ref name="UNESCO-Networks" />
== References ==
{{reflist}}
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
f9jy5l19yhuodbxy53vyses8bast52z
769690
769684
2026-10-22T11:07:12Z
Berkesmob
76347
/* */ added
769690
wikitext
text/x-wiki
{{Infobox organization
| name = World Network of Island and Coastal Biosphere Reserves
| abbreviation = WNICBR
| type = International thematic network
| parent_organisation = [[Man and the Biosphere Programme]] (UNESCO)
| headquarter = [[Jeju Province|Jeju]] ([[South Korea]]) & [[Menorca]] ([[Spain]])
| website = {{URL|https://www.unesco.org/en/mab/networks}}
}}
== Introduction ==
The '''World Network of Island and Coastal Biosphere Reserves''' ('''WNICBR''') is an international thematic network operating under [[UNESCO]]'s [[Man and the Biosphere Programme|Man and the Biosphere (MAB) Programme]]. It promotes sustainable development, climate change adaptation, and marine biodiversity conservation across island and coastal biosphere reserves globally.<ref name="UNESCO-Networks">{{cite web |title=Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks |publisher=UNESCO |access-date=2026-10-22}}</ref> Its joint Technical Secretariat is co-managed by the [[Jeju Volcanic Island and Lava Tubes|Jeju Biosphere Reserve]] in South Korea and the [[Menorca|Menorca Biosphere Reserve]] in Spain.<ref name="UNDESA-Partnership">{{cite web |title=World Network of Island and Coastal Biosphere Reserves (WNICBR) |url=https://sdgs.un.org/partnerships/world-network-island-coastal-biosphere-reserves-wnicbr |publisher=United Nations Department of Economic and Social Affairs |access-date=2026-10-22}}</ref>
== History ==
The network was established in 2009 to encourage sustainable development in island and coastal areas and to promote strategies for climate change adaptation and mitigation. Its first international congress was held in Menorca, Spain, in February 2012, where the network's operating structure and two technical secretariats were established.<ref>{{cite web |title=Working Plan of the World Network of Island and Coastal Biosphere Reserves |url=https://xarxabiosfera.cime.es/WebEditor/Pagines/file/Workplan%20and%20documents/WorkingPlanWNIBR_EN_Jan19.pdf |publisher=World Network of Island and Coastal Biosphere Reserves |access-date=2026-10-22}}</ref>
== Objectives and activities ==
The network aims to strengthen cooperation among island and coastal biosphere reserves, promote the conservation of marine and coastal biodiversity, and support sustainable development and climate change adaptation. It encourages the exchange of scientific knowledge, good practices, and experiences between member reserves, while supporting research, environmental education, and local community participation in conservation initiatives.<ref name="UNESCO-Networks" /><ref name="UNDESA-Partnership" />
Its activities include international collaboration and knowledge-sharing between biosphere reserves, particularly through its joint Technical Secretariat co-managed by the Jeju Biosphere Reserve in South Korea and the Menorca Biosphere Reserve in Spain. The network has also promoted initiatives such as the Zero Plastic campaign to address plastic pollution and its effects on marine ecosystems.<ref>{{cite web |title=Biosphere Reserves |url=https://www.unesco.org/sites/default/files/medias/fichiers/2022/06/english-ocean-presskit-2.pdf |publisher=UNESCO |access-date=2026-10-22}}</ref>
== Organization and cooperation ==
The World Network of Island and Coastal Biosphere Reserves operates under [[UNESCO]]'s [[Man and the Biosphere Programme]]. Its Technical Secretariat is shared by the [[Jeju Island Biosphere Reserve]] in South Korea and the [[Menorca|Menorca Biosphere Reserve]] in Spain. Jeju focuses on climate change issues, while Menorca specializes in sustainable development.<ref>{{cite web |title=Technical Guidelines for Biosphere Reserves |url=https://unesco.org.uk/site/assets/files/2049/mab_technical_guidelines_for_biosphere_reserves_2022.pdf |publisher=UNESCO |access-date=2026-10-22}}</ref>
Members cooperate through international meetings, collaborative research, knowledge exchange, and capacity-building activities focused on biodiversity conservation, sustainable development, and climate change adaptation. The network provides a platform for participating island and coastal biosphere reserves to share experiences and develop responses to common environmental challenges.<ref name="UNESCO-Networks" />
== See also ==
* [[Man and the Biosphere Programme]]
* [[World Network of Biosphere Reserves]]
* [[Biosphere reserve]]
* [[Marine conservation]]
== References ==
{{reflist}}
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
== Objectives and activities ==
The network aims to strengthen cooperation among island and coastal biosphere reserves, promote the conservation of marine and coastal biodiversity, and support sustainable development and climate change adaptation. It encourages the exchange of scientific knowledge, good practices, and experiences between member reserves, while supporting research, environmental education, and local community participation in conservation initiatives.<ref name="UNESCO-Networks" /><ref name="UNDESA-Partnership" />
Its activities include international collaboration and knowledge-sharing between biosphere reserves, particularly through its joint Technical Secretariat co-managed by the Jeju Biosphere Reserve in South Korea and the Menorca Biosphere Reserve in Spain. These efforts help participating reserves share approaches to common environmental challenges and promote sustainable management of island and coastal ecosystems.
== Organization and cooperation ==
The World Network of Island and Coastal Biosphere Reserves operates under [[UNESCO]]'s [[Man and the Biosphere Programme]]. Its Technical Secretariat is shared by the [[Jeju Island Biosphere Reserve]] in South Korea and the [[Menorca|Menorca Biosphere Reserve]] in Spain, which coordinate the network's activities and facilitate cooperation among participating reserves.<ref name="UNESCO-Networks" />
The network brings together more than 80 biosphere reserves in 30 countries. Members cooperate through international meetings, collaborative research, knowledge exchange, and capacity-building activities focused on biodiversity conservation, sustainable development, and climate change adaptation.<ref name="UNESCO-Networks" />
An example of this cooperation was the network's 11th meeting, held in September 2023 at the [[Great Sandy Biosphere Reserve]] in Australia. The network also held its 12th annual meeting online on 12 March 2025, addressing future participation in international biosphere reserve events, the inclusion of new members, and training for network managers.<ref>{{cite web |title=National Report for the 37th MAB ICC: Spain |url=https://articles.unesco.org/sites/default/files/medias/fichiers/2025/09/SPAIN_Informe%20actividades-Espan%CC%83a%20%28CIC%202025%29%20ENG.pdf |publisher=UNESCO |access-date=2026-10-22}}</ref>
At the wider programme level, the [[UNESCO]] MAB Secretariat coordinates the World Network of Biosphere Reserves and its regional and thematic networks, supporting international collaboration and the exchange of experience between participating sites.<ref>{{cite web |title=MAB Secretariat |url=https://www.unesco.org/en/mab/secretariat |publisher=UNESCO |access-date=2026-10-22}}</ref>
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
== History ==
In February 2009, representatives from several island and coastal biosphere reserves met in Menorca, Spain, to address shared ecological vulnerabilities such as sea-level rise, coastal erosion, and extreme weather. The initiative led to the official launching of the network within UNESCO's MAB framework.<ref name="UNDESA-Partnership" />
The network was formally established in 2012, setting up a dual technical secretariat structure in Jeju Island (South Korea) and Menorca (Spain) to manage administrative, scientific, and operational coordination.<ref name="UNESCO-Anniversary">{{cite web |title=Celebrating the 10th anniversary of the World Network of Island and Coastal Biosphere Reserves |url=https://www.unesco.org/en/articles/celebrating-10th-anniversary-world-network-island-and-coastal-biosphere-reserves |publisher=UNESCO |access-date=2026-10-22}}</ref> Since its establishment, the network has grown to include member sites across more than 30 countries.<ref name="UNESCO-Networks" />
== References ==
{{reflist}}
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
jggxonuc0dg1fopgp2n0oqod73yvl5p
769692
769690
2026-10-22T11:07:50Z
Berkesmob
76347
remove
769692
wikitext
text/x-wiki
{{Infobox organization
| name = World Network of Island and Coastal Biosphere Reserves
| abbreviation = WNICBR
| type = International thematic network
| parent_organisation = [[Man and the Biosphere Programme]] (UNESCO)
| headquarter = [[Jeju Province|Jeju]] ([[South Korea]]) & [[Menorca]] ([[Spain]])
| website = {{URL|https://www.unesco.org/en/mab/networks}}
}}
== Introduction ==
The '''World Network of Island and Coastal Biosphere Reserves''' ('''WNICBR''') is an international thematic network operating under [[UNESCO]]'s [[Man and the Biosphere Programme|Man and the Biosphere (MAB) Programme]]. It promotes sustainable development, climate change adaptation, and marine biodiversity conservation across island and coastal biosphere reserves globally.<ref name="UNESCO-Networks">{{cite web |title=Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks |publisher=UNESCO |access-date=2026-10-22}}</ref> Its joint Technical Secretariat is co-managed by the [[Jeju Volcanic Island and Lava Tubes|Jeju Biosphere Reserve]] in South Korea and the [[Menorca|Menorca Biosphere Reserve]] in Spain.<ref name="UNDESA-Partnership">{{cite web |title=World Network of Island and Coastal Biosphere Reserves (WNICBR) |url=https://sdgs.un.org/partnerships/world-network-island-coastal-biosphere-reserves-wnicbr |publisher=United Nations Department of Economic and Social Affairs |access-date=2026-10-22}}</ref>
== History ==
The network was established in 2009 to encourage sustainable development in island and coastal areas and to promote strategies for climate change adaptation and mitigation. Its first international congress was held in Menorca, Spain, in February 2012, where the network's operating structure and two technical secretariats were established.<ref>{{cite web |title=Working Plan of the World Network of Island and Coastal Biosphere Reserves |url=https://xarxabiosfera.cime.es/WebEditor/Pagines/file/Workplan%20and%20documents/WorkingPlanWNIBR_EN_Jan19.pdf |publisher=World Network of Island and Coastal Biosphere Reserves |access-date=2026-10-22}}</ref>
== Objectives and activities ==
The network aims to strengthen cooperation among island and coastal biosphere reserves, promote the conservation of marine and coastal biodiversity, and support sustainable development and climate change adaptation. It encourages the exchange of scientific knowledge, good practices, and experiences between member reserves, while supporting research, environmental education, and local community participation in conservation initiatives.<ref name="UNESCO-Networks" /><ref name="UNDESA-Partnership" />
Its activities include international collaboration and knowledge-sharing between biosphere reserves, particularly through its joint Technical Secretariat co-managed by the Jeju Biosphere Reserve in South Korea and the Menorca Biosphere Reserve in Spain. The network has also promoted initiatives such as the Zero Plastic campaign to address plastic pollution and its effects on marine ecosystems.<ref>{{cite web |title=Biosphere Reserves |url=https://www.unesco.org/sites/default/files/medias/fichiers/2022/06/english-ocean-presskit-2.pdf |publisher=UNESCO |access-date=2026-10-22}}</ref>
== Organization and cooperation ==
The World Network of Island and Coastal Biosphere Reserves operates under [[UNESCO]]'s [[Man and the Biosphere Programme]]. Its Technical Secretariat is shared by the [[Jeju Island Biosphere Reserve]] in South Korea and the [[Menorca|Menorca Biosphere Reserve]] in Spain. Jeju focuses on climate change issues, while Menorca specializes in sustainable development.<ref>{{cite web |title=Technical Guidelines for Biosphere Reserves |url=https://unesco.org.uk/site/assets/files/2049/mab_technical_guidelines_for_biosphere_reserves_2022.pdf |publisher=UNESCO |access-date=2026-10-22}}</ref>
Members cooperate through international meetings, collaborative research, knowledge exchange, and capacity-building activities focused on biodiversity conservation, sustainable development, and climate change adaptation. The network provides a platform for participating island and coastal biosphere reserves to share experiences and develop responses to common environmental challenges.<ref name="UNESCO-Networks" />
== See also ==
* [[Man and the Biosphere Programme]]
* [[World Network of Biosphere Reserves]]
* [[Biosphere reserve]]
* [[Marine conservation]]
== References ==
{{reflist}}
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
== Organization and cooperation ==
The World Network of Island and Coastal Biosphere Reserves operates under [[UNESCO]]'s [[Man and the Biosphere Programme]]. Its Technical Secretariat is shared by the [[Jeju Island Biosphere Reserve]] in South Korea and the [[Menorca|Menorca Biosphere Reserve]] in Spain, which coordinate the network's activities and facilitate cooperation among participating reserves.<ref name="UNESCO-Networks" />
The network brings together more than 80 biosphere reserves in 30 countries. Members cooperate through international meetings, collaborative research, knowledge exchange, and capacity-building activities focused on biodiversity conservation, sustainable development, and climate change adaptation.<ref name="UNESCO-Networks" />
An example of this cooperation was the network's 11th meeting, held in September 2023 at the [[Great Sandy Biosphere Reserve]] in Australia. The network also held its 12th annual meeting online on 12 March 2025, addressing future participation in international biosphere reserve events, the inclusion of new members, and training for network managers.<ref>{{cite web |title=National Report for the 37th MAB ICC: Spain |url=https://articles.unesco.org/sites/default/files/medias/fichiers/2025/09/SPAIN_Informe%20actividades-Espan%CC%83a%20%28CIC%202025%29%20ENG.pdf |publisher=UNESCO |access-date=2026-10-22}}</ref>
At the wider programme level, the [[UNESCO]] MAB Secretariat coordinates the World Network of Biosphere Reserves and its regional and thematic networks, supporting international collaboration and the exchange of experience between participating sites.<ref>{{cite web |title=MAB Secretariat |url=https://www.unesco.org/en/mab/secretariat |publisher=UNESCO |access-date=2026-10-22}}</ref>
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
== History ==
In February 2009, representatives from several island and coastal biosphere reserves met in Menorca, Spain, to address shared ecological vulnerabilities such as sea-level rise, coastal erosion, and extreme weather. The initiative led to the official launching of the network within UNESCO's MAB framework.<ref name="UNDESA-Partnership" />
The network was formally established in 2012, setting up a dual technical secretariat structure in Jeju Island (South Korea) and Menorca (Spain) to manage administrative, scientific, and operational coordination.<ref name="UNESCO-Anniversary">{{cite web |title=Celebrating the 10th anniversary of the World Network of Island and Coastal Biosphere Reserves |url=https://www.unesco.org/en/articles/celebrating-10th-anniversary-world-network-island-and-coastal-biosphere-reserves |publisher=UNESCO |access-date=2026-10-22}}</ref> Since its establishment, the network has grown to include member sites across more than 30 countries.<ref name="UNESCO-Networks" />
== References ==
{{reflist}}
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
nh1fycf05kstw3uh9tbg5bshpz211cq
769693
769692
2026-10-22T11:08:16Z
Berkesmob
76347
removed
769693
wikitext
text/x-wiki
{{Infobox organization
| name = World Network of Island and Coastal Biosphere Reserves
| abbreviation = WNICBR
| type = International thematic network
| parent_organisation = [[Man and the Biosphere Programme]] (UNESCO)
| headquarter = [[Jeju Province|Jeju]] ([[South Korea]]) & [[Menorca]] ([[Spain]])
| website = {{URL|https://www.unesco.org/en/mab/networks}}
}}
== Introduction ==
The '''World Network of Island and Coastal Biosphere Reserves''' ('''WNICBR''') is an international thematic network operating under [[UNESCO]]'s [[Man and the Biosphere Programme|Man and the Biosphere (MAB) Programme]]. It promotes sustainable development, climate change adaptation, and marine biodiversity conservation across island and coastal biosphere reserves globally.<ref name="UNESCO-Networks">{{cite web |title=Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks |publisher=UNESCO |access-date=2026-10-22}}</ref> Its joint Technical Secretariat is co-managed by the [[Jeju Volcanic Island and Lava Tubes|Jeju Biosphere Reserve]] in South Korea and the [[Menorca|Menorca Biosphere Reserve]] in Spain.<ref name="UNDESA-Partnership">{{cite web |title=World Network of Island and Coastal Biosphere Reserves (WNICBR) |url=https://sdgs.un.org/partnerships/world-network-island-coastal-biosphere-reserves-wnicbr |publisher=United Nations Department of Economic and Social Affairs |access-date=2026-10-22}}</ref>
== History ==
The network was established in 2009 to encourage sustainable development in island and coastal areas and to promote strategies for climate change adaptation and mitigation. Its first international congress was held in Menorca, Spain, in February 2012, where the network's operating structure and two technical secretariats were established.<ref>{{cite web |title=Working Plan of the World Network of Island and Coastal Biosphere Reserves |url=https://xarxabiosfera.cime.es/WebEditor/Pagines/file/Workplan%20and%20documents/WorkingPlanWNIBR_EN_Jan19.pdf |publisher=World Network of Island and Coastal Biosphere Reserves |access-date=2026-10-22}}</ref>
== Objectives and activities ==
The network aims to strengthen cooperation among island and coastal biosphere reserves, promote the conservation of marine and coastal biodiversity, and support sustainable development and climate change adaptation. It encourages the exchange of scientific knowledge, good practices, and experiences between member reserves, while supporting research, environmental education, and local community participation in conservation initiatives.<ref name="UNESCO-Networks" /><ref name="UNDESA-Partnership" />
Its activities include international collaboration and knowledge-sharing between biosphere reserves, particularly through its joint Technical Secretariat co-managed by the Jeju Biosphere Reserve in South Korea and the Menorca Biosphere Reserve in Spain. The network has also promoted initiatives such as the Zero Plastic campaign to address plastic pollution and its effects on marine ecosystems.<ref>{{cite web |title=Biosphere Reserves |url=https://www.unesco.org/sites/default/files/medias/fichiers/2022/06/english-ocean-presskit-2.pdf |publisher=UNESCO |access-date=2026-10-22}}</ref>
== Organization and cooperation ==
The World Network of Island and Coastal Biosphere Reserves operates under [[UNESCO]]'s [[Man and the Biosphere Programme]]. Its Technical Secretariat is shared by the [[Jeju Island Biosphere Reserve]] in South Korea and the [[Menorca|Menorca Biosphere Reserve]] in Spain. Jeju focuses on climate change issues, while Menorca specializes in sustainable development.<ref>{{cite web |title=Technical Guidelines for Biosphere Reserves |url=https://unesco.org.uk/site/assets/files/2049/mab_technical_guidelines_for_biosphere_reserves_2022.pdf |publisher=UNESCO |access-date=2026-10-22}}</ref>
Members cooperate through international meetings, collaborative research, knowledge exchange, and capacity-building activities focused on biodiversity conservation, sustainable development, and climate change adaptation. The network provides a platform for participating island and coastal biosphere reserves to share experiences and develop responses to common environmental challenges.<ref name="UNESCO-Networks" />
== See also ==
* [[Man and the Biosphere Programme]]
* [[World Network of Biosphere Reserves]]
* [[Biosphere reserve]]
* [[Marine conservation]]
== References ==
{{reflist}}
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
== History ==
In February 2009, representatives from several island and coastal biosphere reserves met in Menorca, Spain, to address shared ecological vulnerabilities such as sea-level rise, coastal erosion, and extreme weather. The initiative led to the official launching of the network within UNESCO's MAB framework.<ref name="UNDESA-Partnership" />
The network was formally established in 2012, setting up a dual technical secretariat structure in Jeju Island (South Korea) and Menorca (Spain) to manage administrative, scientific, and operational coordination.<ref name="UNESCO-Anniversary">{{cite web |title=Celebrating the 10th anniversary of the World Network of Island and Coastal Biosphere Reserves |url=https://www.unesco.org/en/articles/celebrating-10th-anniversary-world-network-island-and-coastal-biosphere-reserves |publisher=UNESCO |access-date=2026-10-22}}</ref> Since its establishment, the network has grown to include member sites across more than 30 countries.<ref name="UNESCO-Networks" />
== References ==
{{reflist}}
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
g61g70gtq1yok7kgz184f2p8njvekhp
769695
769693
2026-10-22T11:08:45Z
Berkesmob
76347
removed
769695
wikitext
text/x-wiki
{{Infobox organization
| name = World Network of Island and Coastal Biosphere Reserves
| abbreviation = WNICBR
| type = International thematic network
| parent_organisation = [[Man and the Biosphere Programme]] (UNESCO)
| headquarter = [[Jeju Province|Jeju]] ([[South Korea]]) & [[Menorca]] ([[Spain]])
| website = {{URL|https://www.unesco.org/en/mab/networks}}
}}
== Introduction ==
The '''World Network of Island and Coastal Biosphere Reserves''' ('''WNICBR''') is an international thematic network operating under [[UNESCO]]'s [[Man and the Biosphere Programme|Man and the Biosphere (MAB) Programme]]. It promotes sustainable development, climate change adaptation, and marine biodiversity conservation across island and coastal biosphere reserves globally.<ref name="UNESCO-Networks">{{cite web |title=Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks |publisher=UNESCO |access-date=2026-10-22}}</ref> Its joint Technical Secretariat is co-managed by the [[Jeju Volcanic Island and Lava Tubes|Jeju Biosphere Reserve]] in South Korea and the [[Menorca|Menorca Biosphere Reserve]] in Spain.<ref name="UNDESA-Partnership">{{cite web |title=World Network of Island and Coastal Biosphere Reserves (WNICBR) |url=https://sdgs.un.org/partnerships/world-network-island-coastal-biosphere-reserves-wnicbr |publisher=United Nations Department of Economic and Social Affairs |access-date=2026-10-22}}</ref>
== History ==
The network was established in 2009 to encourage sustainable development in island and coastal areas and to promote strategies for climate change adaptation and mitigation. Its first international congress was held in Menorca, Spain, in February 2012, where the network's operating structure and two technical secretariats were established.<ref>{{cite web |title=Working Plan of the World Network of Island and Coastal Biosphere Reserves |url=https://xarxabiosfera.cime.es/WebEditor/Pagines/file/Workplan%20and%20documents/WorkingPlanWNIBR_EN_Jan19.pdf |publisher=World Network of Island and Coastal Biosphere Reserves |access-date=2026-10-22}}</ref>
== Objectives and activities ==
The network aims to strengthen cooperation among island and coastal biosphere reserves, promote the conservation of marine and coastal biodiversity, and support sustainable development and climate change adaptation. It encourages the exchange of scientific knowledge, good practices, and experiences between member reserves, while supporting research, environmental education, and local community participation in conservation initiatives.<ref name="UNESCO-Networks" /><ref name="UNDESA-Partnership" />
Its activities include international collaboration and knowledge-sharing between biosphere reserves, particularly through its joint Technical Secretariat co-managed by the Jeju Biosphere Reserve in South Korea and the Menorca Biosphere Reserve in Spain. The network has also promoted initiatives such as the Zero Plastic campaign to address plastic pollution and its effects on marine ecosystems.<ref>{{cite web |title=Biosphere Reserves |url=https://www.unesco.org/sites/default/files/medias/fichiers/2022/06/english-ocean-presskit-2.pdf |publisher=UNESCO |access-date=2026-10-22}}</ref>
== Organization and cooperation ==
The World Network of Island and Coastal Biosphere Reserves operates under [[UNESCO]]'s [[Man and the Biosphere Programme]]. Its Technical Secretariat is shared by the [[Jeju Island Biosphere Reserve]] in South Korea and the [[Menorca|Menorca Biosphere Reserve]] in Spain. Jeju focuses on climate change issues, while Menorca specializes in sustainable development.<ref>{{cite web |title=Technical Guidelines for Biosphere Reserves |url=https://unesco.org.uk/site/assets/files/2049/mab_technical_guidelines_for_biosphere_reserves_2022.pdf |publisher=UNESCO |access-date=2026-10-22}}</ref>
Members cooperate through international meetings, collaborative research, knowledge exchange, and capacity-building activities focused on biodiversity conservation, sustainable development, and climate change adaptation. The network provides a platform for participating island and coastal biosphere reserves to share experiences and develop responses to common environmental challenges.<ref name="UNESCO-Networks" />
== See also ==
* [[Man and the Biosphere Programme]]
* [[World Network of Biosphere Reserves]]
* [[Biosphere reserve]]
* [[Marine conservation]]
== History ==
In February 2009, representatives from several island and coastal biosphere reserves met in Menorca, Spain, to address shared ecological vulnerabilities such as sea-level rise, coastal erosion, and extreme weather. The initiative led to the official launching of the network within UNESCO's MAB framework.<ref name="UNDESA-Partnership" />
The network was formally established in 2012, setting up a dual technical secretariat structure in Jeju Island (South Korea) and Menorca (Spain) to manage administrative, scientific, and operational coordination.<ref name="UNESCO-Anniversary">{{cite web |title=Celebrating the 10th anniversary of the World Network of Island and Coastal Biosphere Reserves |url=https://www.unesco.org/en/articles/celebrating-10th-anniversary-world-network-island-and-coastal-biosphere-reserves |publisher=UNESCO |access-date=2026-10-22}}</ref> Since its establishment, the network has grown to include member sites across more than 30 countries.<ref name="UNESCO-Networks" />
== References ==
{{reflist}}
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
55pzdy6iub2ue6wu13ia41c1x7ioren
769696
769695
2026-10-22T11:09:53Z
Berkesmob
76347
removed
769696
wikitext
text/x-wiki
{{Infobox organization
| name = World Network of Island and Coastal Biosphere Reserves
| abbreviation = WNICBR
| type = International thematic network
| parent_organisation = [[Man and the Biosphere Programme]] (UNESCO)
| headquarter = [[Jeju Province|Jeju]] ([[South Korea]]) & [[Menorca]] ([[Spain]])
| website = {{URL|https://www.unesco.org/en/mab/networks}}
}}
== Introduction ==
The '''World Network of Island and Coastal Biosphere Reserves''' ('''WNICBR''') is an international thematic network operating under [[UNESCO]]'s [[Man and the Biosphere Programme|Man and the Biosphere (MAB) Programme]]. It promotes sustainable development, climate change adaptation, and marine biodiversity conservation across island and coastal biosphere reserves globally.<ref name="UNESCO-Networks">{{cite web |title=Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks |publisher=UNESCO |access-date=2026-10-22}}</ref> Its joint Technical Secretariat is co-managed by the [[Jeju Volcanic Island and Lava Tubes|Jeju Biosphere Reserve]] in South Korea and the [[Menorca|Menorca Biosphere Reserve]] in Spain.<ref name="UNDESA-Partnership">{{cite web |title=World Network of Island and Coastal Biosphere Reserves (WNICBR) |url=https://sdgs.un.org/partnerships/world-network-island-coastal-biosphere-reserves-wnicbr |publisher=United Nations Department of Economic and Social Affairs |access-date=2026-10-22}}</ref>
== History ==
The network was established in 2009 to encourage sustainable development in island and coastal areas and to promote strategies for climate change adaptation and mitigation. Its first international congress was held in Menorca, Spain, in February 2012, where the network's operating structure and two technical secretariats were established.<ref>{{cite web |title=Working Plan of the World Network of Island and Coastal Biosphere Reserves |url=https://xarxabiosfera.cime.es/WebEditor/Pagines/file/Workplan%20and%20documents/WorkingPlanWNIBR_EN_Jan19.pdf |publisher=World Network of Island and Coastal Biosphere Reserves |access-date=2026-10-22}}</ref>
== Objectives and activities ==
The network aims to strengthen cooperation among island and coastal biosphere reserves, promote the conservation of marine and coastal biodiversity, and support sustainable development and climate change adaptation. It encourages the exchange of scientific knowledge, good practices, and experiences between member reserves, while supporting research, environmental education, and local community participation in conservation initiatives.<ref name="UNESCO-Networks" /><ref name="UNDESA-Partnership" />
Its activities include international collaboration and knowledge-sharing between biosphere reserves, particularly through its joint Technical Secretariat co-managed by the Jeju Biosphere Reserve in South Korea and the Menorca Biosphere Reserve in Spain. The network has also promoted initiatives such as the Zero Plastic campaign to address plastic pollution and its effects on marine ecosystems.<ref>{{cite web |title=Biosphere Reserves |url=https://www.unesco.org/sites/default/files/medias/fichiers/2022/06/english-ocean-presskit-2.pdf |publisher=UNESCO |access-date=2026-10-22}}</ref>
== Organization and cooperation ==
The World Network of Island and Coastal Biosphere Reserves operates under [[UNESCO]]'s [[Man and the Biosphere Programme]]. Its Technical Secretariat is shared by the [[Jeju Island Biosphere Reserve]] in South Korea and the [[Menorca|Menorca Biosphere Reserve]] in Spain. Jeju focuses on climate change issues, while Menorca specializes in sustainable development.<ref>{{cite web |title=Technical Guidelines for Biosphere Reserves |url=https://unesco.org.uk/site/assets/files/2049/mab_technical_guidelines_for_biosphere_reserves_2022.pdf |publisher=UNESCO |access-date=2026-10-22}}</ref>
Members cooperate through international meetings, collaborative research, knowledge exchange, and capacity-building activities focused on biodiversity conservation, sustainable development, and climate change adaptation. The network provides a platform for participating island and coastal biosphere reserves to share experiences and develop responses to common environmental challenges.<ref name="UNESCO-Networks" />
== See also ==
* [[Man and the Biosphere Programme]]
* [[World Network of Biosphere Reserves]]
* [[Biosphere reserve]]
* [[Marine conservation]]
== References ==
{{reflist}}
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
ifqy98282wds9fhx6h08xdwjcn7wslj
769698
769696
2026-10-22T11:13:43Z
Berkesmob
76347
/* See also */ added
769698
wikitext
text/x-wiki
{{Infobox organization
| name = World Network of Island and Coastal Biosphere Reserves
| abbreviation = WNICBR
| type = International thematic network
| parent_organisation = [[Man and the Biosphere Programme]] (UNESCO)
| headquarter = [[Jeju Province|Jeju]] ([[South Korea]]) & [[Menorca]] ([[Spain]])
| website = {{URL|https://www.unesco.org/en/mab/networks}}
}}
== Introduction ==
The '''World Network of Island and Coastal Biosphere Reserves''' ('''WNICBR''') is an international thematic network operating under [[UNESCO]]'s [[Man and the Biosphere Programme|Man and the Biosphere (MAB) Programme]]. It promotes sustainable development, climate change adaptation, and marine biodiversity conservation across island and coastal biosphere reserves globally.<ref name="UNESCO-Networks">{{cite web |title=Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks |publisher=UNESCO |access-date=2026-10-22}}</ref> Its joint Technical Secretariat is co-managed by the [[Jeju Volcanic Island and Lava Tubes|Jeju Biosphere Reserve]] in South Korea and the [[Menorca|Menorca Biosphere Reserve]] in Spain.<ref name="UNDESA-Partnership">{{cite web |title=World Network of Island and Coastal Biosphere Reserves (WNICBR) |url=https://sdgs.un.org/partnerships/world-network-island-coastal-biosphere-reserves-wnicbr |publisher=United Nations Department of Economic and Social Affairs |access-date=2026-10-22}}</ref>
== History ==
The network was established in 2009 to encourage sustainable development in island and coastal areas and to promote strategies for climate change adaptation and mitigation. Its first international congress was held in Menorca, Spain, in February 2012, where the network's operating structure and two technical secretariats were established.<ref>{{cite web |title=Working Plan of the World Network of Island and Coastal Biosphere Reserves |url=https://xarxabiosfera.cime.es/WebEditor/Pagines/file/Workplan%20and%20documents/WorkingPlanWNIBR_EN_Jan19.pdf |publisher=World Network of Island and Coastal Biosphere Reserves |access-date=2026-10-22}}</ref>
== Objectives and activities ==
The network aims to strengthen cooperation among island and coastal biosphere reserves, promote the conservation of marine and coastal biodiversity, and support sustainable development and climate change adaptation. It encourages the exchange of scientific knowledge, good practices, and experiences between member reserves, while supporting research, environmental education, and local community participation in conservation initiatives.<ref name="UNESCO-Networks" /><ref name="UNDESA-Partnership" />
Its activities include international collaboration and knowledge-sharing between biosphere reserves, particularly through its joint Technical Secretariat co-managed by the Jeju Biosphere Reserve in South Korea and the Menorca Biosphere Reserve in Spain. The network has also promoted initiatives such as the Zero Plastic campaign to address plastic pollution and its effects on marine ecosystems.<ref>{{cite web |title=Biosphere Reserves |url=https://www.unesco.org/sites/default/files/medias/fichiers/2022/06/english-ocean-presskit-2.pdf |publisher=UNESCO |access-date=2026-10-22}}</ref>
== Organization and cooperation ==
The World Network of Island and Coastal Biosphere Reserves operates under [[UNESCO]]'s [[Man and the Biosphere Programme]]. Its Technical Secretariat is shared by the [[Jeju Island Biosphere Reserve]] in South Korea and the [[Menorca|Menorca Biosphere Reserve]] in Spain. Jeju focuses on climate change issues, while Menorca specializes in sustainable development.<ref>{{cite web |title=Technical Guidelines for Biosphere Reserves |url=https://unesco.org.uk/site/assets/files/2049/mab_technical_guidelines_for_biosphere_reserves_2022.pdf |publisher=UNESCO |access-date=2026-10-22}}</ref>
Members cooperate through international meetings, collaborative research, knowledge exchange, and capacity-building activities focused on biodiversity conservation, sustainable development, and climate change adaptation. The network provides a platform for participating island and coastal biosphere reserves to share experiences and develop responses to common environmental challenges.<ref name="UNESCO-Networks" />
== See also ==
* [[Man and the Biosphere Programme]]
* [[Biosphere reserve]]
* [[Marine conservation]]
* [[UNESCO]]
* [[Sustainable development]]
* [[Climate change adaptation]]
* [[Biodiversity]]
* [[Coastal management]]
== References ==
{{reflist}}
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
ixx7gl1q3266hn0xf939ek5ul18l07d
769700
769698
2026-10-22T11:18:18Z
Berkesmob
76347
/* References */ added links
769700
wikitext
text/x-wiki
{{Infobox organization
| name = World Network of Island and Coastal Biosphere Reserves
| abbreviation = WNICBR
| type = International thematic network
| parent_organisation = [[Man and the Biosphere Programme]] (UNESCO)
| headquarter = [[Jeju Province|Jeju]] ([[South Korea]]) & [[Menorca]] ([[Spain]])
| website = {{URL|https://www.unesco.org/en/mab/networks}}
}}
== Introduction ==
The '''World Network of Island and Coastal Biosphere Reserves''' ('''WNICBR''') is an international thematic network operating under [[UNESCO]]'s [[Man and the Biosphere Programme|Man and the Biosphere (MAB) Programme]]. It promotes sustainable development, climate change adaptation, and marine biodiversity conservation across island and coastal biosphere reserves globally.<ref name="UNESCO-Networks">{{cite web |title=Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks |publisher=UNESCO |access-date=2026-10-22}}</ref> Its joint Technical Secretariat is co-managed by the [[Jeju Volcanic Island and Lava Tubes|Jeju Biosphere Reserve]] in South Korea and the [[Menorca|Menorca Biosphere Reserve]] in Spain.<ref name="UNDESA-Partnership">{{cite web |title=World Network of Island and Coastal Biosphere Reserves (WNICBR) |url=https://sdgs.un.org/partnerships/world-network-island-coastal-biosphere-reserves-wnicbr |publisher=United Nations Department of Economic and Social Affairs |access-date=2026-10-22}}</ref>
== History ==
The network was established in 2009 to encourage sustainable development in island and coastal areas and to promote strategies for climate change adaptation and mitigation. Its first international congress was held in Menorca, Spain, in February 2012, where the network's operating structure and two technical secretariats were established.<ref>{{cite web |title=Working Plan of the World Network of Island and Coastal Biosphere Reserves |url=https://xarxabiosfera.cime.es/WebEditor/Pagines/file/Workplan%20and%20documents/WorkingPlanWNIBR_EN_Jan19.pdf |publisher=World Network of Island and Coastal Biosphere Reserves |access-date=2026-10-22}}</ref>
== Objectives and activities ==
The network aims to strengthen cooperation among island and coastal biosphere reserves, promote the conservation of marine and coastal biodiversity, and support sustainable development and climate change adaptation. It encourages the exchange of scientific knowledge, good practices, and experiences between member reserves, while supporting research, environmental education, and local community participation in conservation initiatives.<ref name="UNESCO-Networks" /><ref name="UNDESA-Partnership" />
Its activities include international collaboration and knowledge-sharing between biosphere reserves, particularly through its joint Technical Secretariat co-managed by the Jeju Biosphere Reserve in South Korea and the Menorca Biosphere Reserve in Spain. The network has also promoted initiatives such as the Zero Plastic campaign to address plastic pollution and its effects on marine ecosystems.<ref>{{cite web |title=Biosphere Reserves |url=https://www.unesco.org/sites/default/files/medias/fichiers/2022/06/english-ocean-presskit-2.pdf |publisher=UNESCO |access-date=2026-10-22}}</ref>
== Organization and cooperation ==
The World Network of Island and Coastal Biosphere Reserves operates under [[UNESCO]]'s [[Man and the Biosphere Programme]]. Its Technical Secretariat is shared by the [[Jeju Island Biosphere Reserve]] in South Korea and the [[Menorca|Menorca Biosphere Reserve]] in Spain. Jeju focuses on climate change issues, while Menorca specializes in sustainable development.<ref>{{cite web |title=Technical Guidelines for Biosphere Reserves |url=https://unesco.org.uk/site/assets/files/2049/mab_technical_guidelines_for_biosphere_reserves_2022.pdf |publisher=UNESCO |access-date=2026-10-22}}</ref>
Members cooperate through international meetings, collaborative research, knowledge exchange, and capacity-building activities focused on biodiversity conservation, sustainable development, and climate change adaptation. The network provides a platform for participating island and coastal biosphere reserves to share experiences and develop responses to common environmental challenges.<ref name="UNESCO-Networks" />
== See also ==
* [[Man and the Biosphere Programme]]
* [[Biosphere reserve]]
* [[Marine conservation]]
* [[UNESCO]]
* [[Sustainable development]]
* [[Climate change adaptation]]
* [[Biodiversity]]
* [[Coastal management]]
== References ==
{{reflist}}
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
== External links ==
* [https://www.unesco.org/en/mab/networks UNESCO – Regional and Thematic Networks]
* [https://www.biosphere.im/our-networks/world-network-of-island-and-coastal-brs World Network of Island and Coastal Biosphere Reserves – Biosphere Isle of Man]
* [https://www.unesco.org/en/articles/new-issue-world-network-island-and-coastal-biosphere-reserves-out UNESCO – WNICBR newsletter]
* [https://www.unesco.org/en/mab/secretariat UNESCO – Man and the Biosphere Programme Secretariat]
* [https://www.unesco.org/en/mab/map UNESCO – World Network of Biosphere Reserves map]
39x151ec1ytsoxz2zo37876xjsq79sc
769714
769700
2026-10-22T11:29:27Z
Berkesmob
76347
removed external
769714
wikitext
text/x-wiki
{{Infobox organization
| name = World Network of Island and Coastal Biosphere Reserves
| abbreviation = WNICBR
| type = International thematic network
| parent_organisation = [[Man and the Biosphere Programme]] (UNESCO)
| headquarter = [[Jeju Province|Jeju]] ([[South Korea]]) & [[Menorca]] ([[Spain]])
| website = {{URL|https://www.unesco.org/en/mab/networks}}
}}
== Introduction ==
The '''World Network of Island and Coastal Biosphere Reserves''' ('''WNICBR''') is an international thematic network operating under [[UNESCO]]'s [[Man and the Biosphere Programme|Man and the Biosphere (MAB) Programme]]. It promotes sustainable development, climate change adaptation, and marine biodiversity conservation across island and coastal biosphere reserves globally.<ref name="UNESCO-Networks">{{cite web |title=Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks |publisher=UNESCO |access-date=2026-10-22}}</ref> Its joint Technical Secretariat is co-managed by the [[Jeju Volcanic Island and Lava Tubes|Jeju Biosphere Reserve]] in South Korea and the [[Menorca|Menorca Biosphere Reserve]] in Spain.<ref name="UNDESA-Partnership">{{cite web |title=World Network of Island and Coastal Biosphere Reserves (WNICBR) |url=https://sdgs.un.org/partnerships/world-network-island-coastal-biosphere-reserves-wnicbr |publisher=United Nations Department of Economic and Social Affairs |access-date=2026-10-22}}</ref>
== History ==
The network was established in 2009 to encourage sustainable development in island and coastal areas and to promote strategies for climate change adaptation and mitigation. Its first international congress was held in Menorca, Spain, in February 2012, where the network's operating structure and two technical secretariats were established.<ref>{{cite web |title=Working Plan of the World Network of Island and Coastal Biosphere Reserves |url=https://xarxabiosfera.cime.es/WebEditor/Pagines/file/Workplan%20and%20documents/WorkingPlanWNIBR_EN_Jan19.pdf |publisher=World Network of Island and Coastal Biosphere Reserves |access-date=2026-10-22}}</ref>
== Objectives and activities ==
The network aims to strengthen cooperation among island and coastal biosphere reserves, promote the conservation of marine and coastal biodiversity, and support sustainable development and climate change adaptation. It encourages the exchange of scientific knowledge, good practices, and experiences between member reserves, while supporting research, environmental education, and local community participation in conservation initiatives.<ref name="UNESCO-Networks" /><ref name="UNDESA-Partnership" />
Its activities include international collaboration and knowledge-sharing between biosphere reserves, particularly through its joint Technical Secretariat co-managed by the Jeju Biosphere Reserve in South Korea and the Menorca Biosphere Reserve in Spain. The network has also promoted initiatives such as the Zero Plastic campaign to address plastic pollution and its effects on marine ecosystems.<ref>{{cite web |title=Biosphere Reserves |url=https://www.unesco.org/sites/default/files/medias/fichiers/2022/06/english-ocean-presskit-2.pdf |publisher=UNESCO |access-date=2026-10-22}}</ref>
== Organization and cooperation ==
The World Network of Island and Coastal Biosphere Reserves operates under [[UNESCO]]'s [[Man and the Biosphere Programme]]. Its Technical Secretariat is shared by the [[Jeju Island Biosphere Reserve]] in South Korea and the [[Menorca|Menorca Biosphere Reserve]] in Spain. Jeju focuses on climate change issues, while Menorca specializes in sustainable development.<ref>{{cite web |title=Technical Guidelines for Biosphere Reserves |url=https://unesco.org.uk/site/assets/files/2049/mab_technical_guidelines_for_biosphere_reserves_2022.pdf |publisher=UNESCO |access-date=2026-10-22}}</ref>
Members cooperate through international meetings, collaborative research, knowledge exchange, and capacity-building activities focused on biodiversity conservation, sustainable development, and climate change adaptation. The network provides a platform for participating island and coastal biosphere reserves to share experiences and develop responses to common environmental challenges.<ref name="UNESCO-Networks" />
== See also ==
* [[Man and the Biosphere Programme]]
* [[Biosphere reserve]]
* [[Marine conservation]]
* [[UNESCO]]
* [[Sustainable development]]
* [[Climate change adaptation]]
* [[Biodiversity]]
* [[Coastal management]]
== References ==
{{reflist}}
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
[[Category:Biosphere reserves]]
[[Category:UNESCO]]
[[Category:International environmental organizations]]
ixx7gl1q3266hn0xf939ek5ul18l07d
East Asian Biosphere Reserve Network
0
179382
769613
769421
2026-10-22T09:44:51Z
Mehlikaaa
76336
I added more information about the topic.
769613
wikitext
text/x-wiki
The '''East Asian Biosphere Reserve Network (EABRN)''' is a regional network that promotes cooperation among countries in East Asia on the conservation and management of biosphere reserves. It was initiated in 1994 and formally established in 1995 under the framework of UNESCO’s Man and the Biosphere (MAB) Programme. The network initially involved China, the Democratic People’s Republic of Korea, Japan, Mongolia and the Republic of Korea. The Russian Federation joined in 1998, followed by Kazakhstan in 2011. EABRN facilitates the exchange of information and experience among its members to support biodiversity conservation, scientific research and sustainable development. <ref>{{Cite web |title= |url=https://www.unesco.org/en/mab/networks |access-date={{subst:DATE}}}}</ref><ref>{{Cite web |title= |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/eabrn/about-network-eabrn?utm_source |access-date={{subst:DATE}}}}</ref>
'''History'''
The East Asian Biosphere Reserve Network (EABRN) was initiated in 1994 and formally established in 1995 within the framework of UNESCO’s Man and the Biosphere (MAB) Programme. It was developed to encourage regional cooperation in biosphere reserve management, biodiversity conservation and sustainable development. The network initially included China, the Democratic People’s Republic of Korea, Japan, Mongolia and the Republic of Korea. The Russian Federation joined the network in 1998, followed by Kazakhstan in 2011, expanding its regional membership. <ref>{{Cite web |title=About Network EABRN and its Statutes |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/eabrn/about-network-eabrn |access-date=22 October 2026 |website=Kazakhstan National Committee for UNESCO MAB}}</ref><ref>{{Cite web |title=Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks?hub=66369 |access-date=22 October 2026 |website=UNESCO}}</ref>
'''Geographic''' '''Scope and Membership'''
The East Asian Biosphere Reserve Network (EABRN) covers seven countries: China, the Democratic People’s Republic of Korea, Japan, Kazakhstan, Mongolia, the Republic of Korea and the Russian Federation. <ref name="Website-1">{{Cite web |title=Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks?hub=66369 |access-date=22 October 2026 |website=UNESCO}}</ref>
The network initially included five founding countries: China, the Democratic People’s Republic of Korea, Japan, Mongolia and the Republic of Korea. <ref name="Website-2">{{Cite web |title=About Network EABRN and its Statutes |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/eabrn/about-network-eabrn |access-date=22 October 2026 |website=Kazakhstan National Committee for UNESCO MAB}}</ref>The Russian Federation joined in 1998, followed by Kazakhstan in 2011. <ref name="Website-2" />
EABRN provides a platform for cooperation and information exchange among its member countries on biosphere reserve management and conservation. <ref name="Website-1" />
'''Objectives and activities'''
The East Asian Biosphere Reserve Network (EABRN) aims to promote cooperation among its member countries in biodiversity conservation and sustainable development. According to its statutes, the network supports the exchange of information and experience on biosphere reserve management, scientific research, environmental education and monitoring. It also encourages studies that explore the relationship between biodiversity conservation and the socioeconomic development of local communities. <ref name="Website-3">{{Cite web |title=About Network EABRN and its Statutes |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/eabrn/about-network-eabrn |access-date=22 October 2026 |website=Kazakhstan National Committee for UNESCO MAB}}</ref>
EABRN organizes regional meetings, scientific seminars and training workshops to strengthen cooperation among its members. Its activities also include the exchange of specialists and the sharing of knowledge about biosphere reserve management. For example, the network held its 14th meeting at Shiga Highland Biosphere Reserve in Japan from 6 to 9 October 2015. Participants discussed the role of local communities in managing biosphere reserves and identified priorities for the network’s activities over the following decade. <ref name="Website-3" />
obre2zz8nlbrmz76jf0hqlz38ktmich
769617
769613
2026-10-22T10:08:09Z
Mehlikaaa
76336
769617
wikitext
text/x-wiki
The '''East Asian Biosphere Reserve Network (EABRN)''' is a regional network that promotes cooperation among countries in East Asia on the conservation and management of biosphere reserves. It was initiated in 1994 and formally established in 1995 under the framework of UNESCO’s Man and the Biosphere (MAB) Programme. The network initially involved China, the Democratic People’s Republic of Korea, Japan, Mongolia and the Republic of Korea. The Russian Federation joined in 1998, followed by Kazakhstan in 2011. EABRN facilitates the exchange of information and experience among its members to support biodiversity conservation, scientific research and sustainable development. <ref>{{Cite web |title= |url=https://www.unesco.org/en/mab/networks |access-date={{subst:DATE}}}}</ref><ref>{{Cite web |title= |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/eabrn/about-network-eabrn?utm_source |access-date={{subst:DATE}}}}</ref>
'''History'''
The East Asian Biosphere Reserve Network (EABRN) was initiated in 1994 and formally established in 1995 within the framework of UNESCO’s Man and the Biosphere (MAB) Programme. It was developed to encourage regional cooperation in biosphere reserve management, biodiversity conservation and sustainable development. The network initially included China, the Democratic People’s Republic of Korea, Japan, Mongolia and the Republic of Korea. The Russian Federation joined the network in 1998, followed by Kazakhstan in 2011, expanding its regional membership. <ref>{{Cite web |title=About Network EABRN and its Statutes |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/eabrn/about-network-eabrn |access-date=22 October 2026 |website=Kazakhstan National Committee for UNESCO MAB}}</ref><ref>{{Cite web |title=Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks?hub=66369 |access-date=22 October 2026 |website=UNESCO}}</ref>
'''Geographic''' '''Scope and Membership'''
The East Asian Biosphere Reserve Network (EABRN) covers seven countries: China, the Democratic People’s Republic of Korea, Japan, Kazakhstan, Mongolia, the Republic of Korea and the Russian Federation. <ref name="Website-1">{{Cite web |title=Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks?hub=66369 |access-date=22 October 2026 |website=UNESCO}}</ref>
The network initially included five founding countries: China, the Democratic People’s Republic of Korea, Japan, Mongolia and the Republic of Korea. <ref name="Website-2">{{Cite web |title=About Network EABRN and its Statutes |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/eabrn/about-network-eabrn |access-date=22 October 2026 |website=Kazakhstan National Committee for UNESCO MAB}}</ref>The Russian Federation joined in 1998, followed by Kazakhstan in 2011. <ref name="Website-2" />
EABRN provides a platform for cooperation and information exchange among its member countries on biosphere reserve management and conservation. <ref name="Website-1" />
'''Objectives and activities'''
The East Asian Biosphere Reserve Network (EABRN) aims to promote cooperation among its member countries in biodiversity conservation and sustainable development. According to its statutes, the network supports the exchange of information and experience on biosphere reserve management, scientific research, environmental education and monitoring. It also encourages studies that explore the relationship between biodiversity conservation and the socioeconomic development of local communities. <ref name="Website-3">{{Cite web |title=About Network EABRN and its Statutes |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/eabrn/about-network-eabrn |access-date=22 October 2026 |website=Kazakhstan National Committee for UNESCO MAB}}</ref>
EABRN organizes regional meetings, scientific seminars and training workshops to strengthen cooperation among its members. Its activities also include the exchange of specialists and the sharing of knowledge about biosphere reserve management. For example, the network held its 14th meeting at Shiga Highland Biosphere Reserve in Japan from 6 to 9 October 2015. Participants discussed the role of local communities in managing biosphere reserves and identified priorities for the network’s activities over the following decade. <ref name="Website-3" />
'''Organization and cooperation'''
The East Asian Biosphere Reserve Network (EABRN) is coordinated by the UNESCO Office in Beijing, the UNESCO Man and the Biosphere (MAB) Programme Secretariat in Paris, and national focal points in its member countries. The network also has a Steering Committee composed of the national focal points, which provides guidance for its activities. The committee’s chairperson and vice-chairperson(s) are elected at the network’s biennial meetings. <ref name="Website-4">{{Cite web |date=2025 |title=UNESCO Beijing Office Annual Report 2024–2025 |url=https://articles.unesco.org/sites/default/files/medias/fichiers/2025/05/UNESCO%20BEJ%202024-25%20AR_0.pdf |access-date={{subst:DATE}} |website=UNESCO}}</ref>
EABRN members cooperate by exchanging scientific information, sharing experience in biosphere reserve management, organizing regional meetings and training workshops, and conducting joint research. The network also encourages cooperation between individual biosphere reserves across national borders. These activities support collaboration on biodiversity conservation and sustainable development throughout the region. <ref name="Website-4" details="UNESCO" />
* Man and the Biosphere Programme
* World Network of Biosphere Reserves
* Biosphere reserve
* UNESCO
phl550graguxlj0nddxofq21ygk962z
769627
769617
2026-10-22T10:17:49Z
Hmyray
76343
769627
wikitext
text/x-wiki
The '''East Asian Biosphere Reserve Network (EABRN)''' is a regional network that promotes cooperation among countries in East Asia on the conservation and management of biosphere reserves. It was initiated in 1994 and formally established in 1995 under the framework of UNESCO’s Man and the Biosphere (MAB) Programme. The network initially involved China, the Democratic People’s Republic of Korea, Japan, Mongolia and the Republic of Korea. The Russian Federation joined in 1998, followed by Kazakhstan in 2011. EABRN facilitates the exchange of information and experience among its members to support biodiversity conservation, scientific research and sustainable development. <ref>{{Cite web |title= |url=https://www.unesco.org/en/mab/networks |access-date={{subst:DATE}}}}</ref><ref>{{Cite web |title= |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/eabrn/about-network-eabrn?utm_source |access-date={{subst:DATE}}}}</ref>
== History ==
The East Asian Biosphere Reserve Network (EABRN) was initiated in 1994 and formally established in 1995 within the framework of UNESCO’s Man and the Biosphere (MAB) Programme. It was developed to encourage regional cooperation in biosphere reserve management, biodiversity conservation and sustainable development. The network initially included China, the Democratic People’s Republic of Korea, Japan, Mongolia and the Republic of Korea. The Russian Federation joined the network in 1998, followed by Kazakhstan in 2011, expanding its regional membership. <ref>{{Cite web |title=About Network EABRN and its Statutes |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/eabrn/about-network-eabrn |access-date=22 October 2026 |website=Kazakhstan National Committee for UNESCO MAB}}</ref><ref>{{Cite web |title=Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks?hub=66369 |access-date=22 October 2026 |website=UNESCO}}</ref>
== Geographic Scope and Membership ==
The East Asian Biosphere Reserve Network (EABRN) covers seven countries: China, the Democratic People’s Republic of Korea, Japan, Kazakhstan, Mongolia, the Republic of Korea and the Russian Federation. <ref name="Website-1">{{Cite web |title=Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks?hub=66369 |access-date=22 October 2026 |website=UNESCO}}</ref>
The network initially included five founding countries: China, the Democratic People’s Republic of Korea, Japan, Mongolia and the Republic of Korea. <ref name="Website-2">{{Cite web |title=About Network EABRN and its Statutes |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/eabrn/about-network-eabrn |access-date=22 October 2026 |website=Kazakhstan National Committee for UNESCO MAB}}</ref>The Russian Federation joined in 1998, followed by Kazakhstan in 2011. <ref name="Website-2" />
EABRN provides a platform for cooperation and information exchange among its member countries on biosphere reserve management and conservation. <ref name="Website-1" />
'''Objectives and activities'''
The East Asian Biosphere Reserve Network (EABRN) aims to promote cooperation among its member countries in biodiversity conservation and sustainable development. According to its statutes, the network supports the exchange of information and experience on biosphere reserve management, scientific research, environmental education and monitoring. It also encourages studies that explore the relationship between biodiversity conservation and the socioeconomic development of local communities. <ref name="Website-3">{{Cite web |title=About Network EABRN and its Statutes |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/eabrn/about-network-eabrn |access-date=22 October 2026 |website=Kazakhstan National Committee for UNESCO MAB}}</ref>
EABRN organizes regional meetings, scientific seminars and training workshops to strengthen cooperation among its members. Its activities also include the exchange of specialists and the sharing of knowledge about biosphere reserve management. For example, the network held its 14th meeting at Shiga Highland Biosphere Reserve in Japan from 6 to 9 October 2015. Participants discussed the role of local communities in managing biosphere reserves and identified priorities for the network’s activities over the following decade. <ref name="Website-3" />
'''Organization and cooperation'''
The East Asian Biosphere Reserve Network (EABRN) is coordinated by the UNESCO Office in Beijing, the UNESCO Man and the Biosphere (MAB) Programme Secretariat in Paris, and national focal points in its member countries. The network also has a Steering Committee composed of the national focal points, which provides guidance for its activities. The committee’s chairperson and vice-chairperson(s) are elected at the network’s biennial meetings. <ref name="Website-4">{{Cite web |date=2025 |title=UNESCO Beijing Office Annual Report 2024–2025 |url=https://articles.unesco.org/sites/default/files/medias/fichiers/2025/05/UNESCO%20BEJ%202024-25%20AR_0.pdf |access-date={{subst:DATE}} |website=UNESCO}}</ref>
EABRN members cooperate by exchanging scientific information, sharing experience in biosphere reserve management, organizing regional meetings and training workshops, and conducting joint research. The network also encourages cooperation between individual biosphere reserves across national borders. These activities support collaboration on biodiversity conservation and sustainable development throughout the region. <ref name="Website-4" details="UNESCO" />
* Man and the Biosphere Programme
* World Network of Biosphere Reserves
* Biosphere reserve
* UNESCO
fa6a6bbo3kvcgofw77egtdgtehyxgq7
769633
769627
2026-10-22T10:24:50Z
~2026-56438-30
76365
769633
wikitext
text/x-wiki
The '''East Asian Biosphere Reserve Network (EABRN)''' is a regional network that promotes cooperation among countries in East Asia on the conservation and management of biosphere reserves. It was initiated in 1994 and formally established in 1995 under the framework of UNESCO’s Man and the Biosphere (MAB) Programme. The network initially involved China, the Democratic People’s Republic of Korea, Japan, Mongolia and the Republic of Korea. The Russian Federation joined in 1998, followed by Kazakhstan in 2011. EABRN facilitates the exchange of information and experience among its members to support biodiversity conservation, scientific research and sustainable development. <ref>{{Cite web |title= |url=https://www.unesco.org/en/mab/networks |access-date={{subst:DATE}}}}</ref><ref>{{Cite web |title= |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/eabrn/about-network-eabrn?utm_source |access-date={{subst:DATE}}}}</ref>
== History ==
The East Asian Biosphere Reserve Network (EABRN) was initiated in 1994 and formally established in 1995 within the framework of UNESCO’s Man and the Biosphere (MAB) Programme. It was developed to encourage regional cooperation in biosphere reserve management, biodiversity conservation and sustainable development. The network initially included China, the Democratic People’s Republic of Korea, Japan, Mongolia and the Republic of Korea. The Russian Federation joined the network in 1998, followed by Kazakhstan in 2011, expanding its regional membership. <ref>{{Cite web |title=About Network EABRN and its Statutes |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/eabrn/about-network-eabrn |access-date=22 October 2026 |website=Kazakhstan National Committee for UNESCO MAB}}</ref><ref>{{Cite web |title=Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks?hub=66369 |access-date=22 October 2026 |website=UNESCO}}</ref>
== Geographic Scope and Membership ==
The East Asian Biosphere Reserve Network (EABRN) covers seven countries: China, the Democratic People’s Republic of Korea, Japan, Kazakhstan, Mongolia, the Republic of Korea and the Russian Federation. <ref name="Website-1">{{Cite web |title=Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks?hub=66369 |access-date=22 October 2026 |website=UNESCO}}</ref>
The network initially included five founding countries: China, the Democratic People’s Republic of Korea, Japan, Mongolia and the Republic of Korea. <ref name="Website-2">{{Cite web |title=About Network EABRN and its Statutes |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/eabrn/about-network-eabrn |access-date=22 October 2026 |website=Kazakhstan National Committee for UNESCO MAB}}</ref>The Russian Federation joined in 1998, followed by Kazakhstan in 2011. <ref name="Website-2" />
EABRN provides a platform for cooperation and information exchange among its member countries on biosphere reserve management and conservation. <ref name="Website-1" />
'''Objectives and activities'''
The East Asian Biosphere Reserve Network (EABRN) aims to promote cooperation among its member countries in biodiversity conservation and sustainable development. According to its statutes, the network supports the exchange of information and experience on biosphere reserve management, scientific research, environmental education and monitoring. It also encourages studies that explore the relationship between biodiversity conservation and the socioeconomic development of local communities. <ref name="Website-3">{{Cite web |title=About Network EABRN and its Statutes |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/eabrn/about-network-eabrn |access-date=22 October 2026 |website=Kazakhstan National Committee for UNESCO MAB}}</ref>
EABRN organizes regional meetings, scientific seminars and training workshops to strengthen cooperation among its members. Its activities also include the exchange of specialists and the sharing of knowledge about biosphere reserve management. For example, the network held its 14th meeting at Shiga Highland Biosphere Reserve in Japan from 6 to 9 October 2015. Participants discussed the role of local communities in managing biosphere reserves and identified priorities for the network’s activities over the following decade. <ref name="Website-3" />
'''Organization and cooperation'''
The East Asian Biosphere Reserve Network (EABRN) is coordinated by the UNESCO Office in Beijing, the UNESCO Man and the Biosphere (MAB) Programme Secretariat in Paris, and national focal points in its member countries. The network also has a Steering Committee composed of the national focal points, which provides guidance for its activities. The committee’s chairperson and vice-chairperson(s) are elected at the network’s biennial meetings. <ref name="Website-4">{{Cite web |date=2025 |title=UNESCO Beijing Office Annual Report 2024–2025 |url=https://articles.unesco.org/sites/default/files/medias/fichiers/2025/05/UNESCO%20BEJ%202024-25%20AR_0.pdf |access-date={{subst:DATE}} |website=UNESCO}}</ref>
EABRN members cooperate by exchanging scientific information, sharing experience in biosphere reserve management, organizing regional meetings and training workshops, and conducting joint research. The network also encourages cooperation between individual biosphere reserves across national borders. These activities support collaboration on biodiversity conservation and sustainable development throughout the region. <ref name="Website-4" details="UNESCO" />
* Man and the Biosphere Programme
* World Network of Biosphere Reserves
* Biosphere reserve
* UNESCO
3by5m68z2rajboxk54ayt9dqc19u9yz
769641
769633
2026-10-22T10:28:36Z
Hmyray
76343
769641
wikitext
text/x-wiki
The '''East Asian Biosphere Reserve Network (EABRN)''' helps East Asian countries work together to protect and manage biosphere reserves. It began in 1994 and was formally established in 1995 as part of UNESCO’s Man and the Biosphere (MAB) Programme. The network first included China, the Democratic People’s Republic of Korea, Japan, Mongolia and the Republic of Korea. The Russian Federation joined in 1998, followed by Kazakhstan in 2011. EABRN helps its members share information and experience to support the protection of biodiversity, scientific research and sustainable development. <ref>{{Cite web |title= |url=https://www.unesco.org/en/mab/networks |access-date={{subst:DATE}}}}</ref><ref>{{Cite web |title= |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/eabrn/about-network-eabrn?utm_source |access-date={{subst:DATE}}}}</ref>
== History ==
The East Asian Biosphere Reserve Network (EABRN) was initiated in 1994 and formally established in 1995 within the framework of UNESCO’s Man and the Biosphere (MAB) Programme. It was developed to encourage regional cooperation in biosphere reserve management, biodiversity conservation and sustainable development. The network initially included China, the Democratic People’s Republic of Korea, Japan, Mongolia and the Republic of Korea. The Russian Federation joined the network in 1998, followed by Kazakhstan in 2011, expanding its regional membership. <ref>{{Cite web |title=About Network EABRN and its Statutes |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/eabrn/about-network-eabrn |access-date=22 October 2026 |website=Kazakhstan National Committee for UNESCO MAB}}</ref><ref>{{Cite web |title=Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks?hub=66369 |access-date=22 October 2026 |website=UNESCO}}</ref>
== Geographic Scope and Membership ==
The East Asian Biosphere Reserve Network (EABRN) covers seven countries: China, the Democratic People’s Republic of Korea, Japan, Kazakhstan, Mongolia, the Republic of Korea and the Russian Federation. <ref name="Website-1">{{Cite web |title=Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks?hub=66369 |access-date=22 October 2026 |website=UNESCO}}</ref>
The network initially included five founding countries: China, the Democratic People’s Republic of Korea, Japan, Mongolia and the Republic of Korea. <ref name="Website-2">{{Cite web |title=About Network EABRN and its Statutes |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/eabrn/about-network-eabrn |access-date=22 October 2026 |website=Kazakhstan National Committee for UNESCO MAB}}</ref>The Russian Federation joined in 1998, followed by Kazakhstan in 2011. <ref name="Website-2" />
EABRN provides a platform for cooperation and information exchange among its member countries on biosphere reserve management and conservation. <ref name="Website-1" />
'''Objectives and activities'''
The East Asian Biosphere Reserve Network (EABRN) aims to promote cooperation among its member countries in biodiversity conservation and sustainable development. According to its statutes, the network supports the exchange of information and experience on biosphere reserve management, scientific research, environmental education and monitoring. It also encourages studies that explore the relationship between biodiversity conservation and the socioeconomic development of local communities. <ref name="Website-3">{{Cite web |title=About Network EABRN and its Statutes |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/eabrn/about-network-eabrn |access-date=22 October 2026 |website=Kazakhstan National Committee for UNESCO MAB}}</ref>
EABRN organizes regional meetings, scientific seminars and training workshops to strengthen cooperation among its members. Its activities also include the exchange of specialists and the sharing of knowledge about biosphere reserve management. For example, the network held its 14th meeting at Shiga Highland Biosphere Reserve in Japan from 6 to 9 October 2015. Participants discussed the role of local communities in managing biosphere reserves and identified priorities for the network’s activities over the following decade. <ref name="Website-3" />
'''Organization and cooperation'''
The East Asian Biosphere Reserve Network (EABRN) is coordinated by the UNESCO Office in Beijing, the UNESCO Man and the Biosphere (MAB) Programme Secretariat in Paris, and national focal points in its member countries. The network also has a Steering Committee composed of the national focal points, which provides guidance for its activities. The committee’s chairperson and vice-chairperson(s) are elected at the network’s biennial meetings. <ref name="Website-4">{{Cite web |date=2025 |title=UNESCO Beijing Office Annual Report 2024–2025 |url=https://articles.unesco.org/sites/default/files/medias/fichiers/2025/05/UNESCO%20BEJ%202024-25%20AR_0.pdf |access-date={{subst:DATE}} |website=UNESCO}}</ref>
EABRN members cooperate by exchanging scientific information, sharing experience in biosphere reserve management, organizing regional meetings and training workshops, and conducting joint research. The network also encourages cooperation between individual biosphere reserves across national borders. These activities support collaboration on biodiversity conservation and sustainable development throughout the region. <ref name="Website-4" details="UNESCO" />
* Man and the Biosphere Programme
* World Network of Biosphere Reserves
* Biosphere reserve
* UNESCO
qa1okyz9jexfuz4hj3oeypwaf4dhsjd
769657
769641
2026-10-22T10:34:50Z
Hmyray
76343
769657
wikitext
text/x-wiki
The '''East Asian Biosphere Reserve Network (EABRN)''' helps East Asian countries work together to protect and manage biosphere reserves. It began in 1994 and was formally established in 1995 as part of UNESCO’s Man and the Biosphere (MAB) Programme. The network first included China, the Democratic People’s Republic of Korea, Japan, Mongolia and the Republic of Korea. The Russian Federation joined in 1998, followed by Kazakhstan in 2011. EABRN helps its members share information and experience to support the protection of biodiversity, scientific research and sustainable development. <ref>{{Cite web |title= |url=https://www.unesco.org/en/mab/networks |access-date={{subst:DATE}}}}</ref><ref>{{Cite web |title= |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/eabrn/about-network-eabrn?utm_source |access-date={{subst:DATE}}}}</ref>
== History ==
The East Asian Biosphere Reserve Network (EABRN) began in 1994 and was formally established in 1995 as part of UNESCO’s Man and the Biosphere (MAB) Programme. It was created to help countries work together to manage biosphere reserves, protect biodiversity and support sustainable development. The network first included China, the Democratic People’s Republic of Korea, Japan, Mongolia and the Republic of Korea. The Russian Federation joined in 1998, followed by Kazakhstan in 2011. These new members expanded the network. <ref>{{Cite web |title=About Network EABRN and its Statutes |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/eabrn/about-network-eabrn |access-date=22 October 2026 |website=Kazakhstan National Committee for UNESCO MAB}}</ref><ref>{{Cite web |title=Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks?hub=66369 |access-date=22 October 2026 |website=UNESCO}}</ref>
== Geographic Scope and Membership ==
The East Asian Biosphere Reserve Network (EABRN) includes seven countries: China, the Democratic People’s Republic of Korea, Japan, Kazakhstan, Mongolia, the Republic of Korea and the Russian Federation. <ref name="Website-1">{{Cite web |title=Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks?hub=66369 |access-date=22 October 2026 |website=UNESCO}}</ref>
The network first included five countries: China, the Democratic People’s Republic of Korea, Japan, Mongolia and the Republic of Korea. <ref name="Website-2">{{Cite web |title=About Network EABRN and its Statutes |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/eabrn/about-network-eabrn |access-date=22 October 2026 |website=Kazakhstan National Committee for UNESCO MAB}}</ref> The Russian Federation joined in 1998, followed by Kazakhstan in 2011. <ref name="Website-2" />
EABRN helps its member countries work together and share information about managing and protecting biosphere reserves. <ref name="Website-1" />
'''Objectives and activities'''
The East Asian Biosphere Reserve Network (EABRN) aims to help its member countries protect biodiversity and support sustainable development. Its statutes say that the network helps members share information and experience about managing biosphere reserves, scientific research, environmental education and monitoring. It also supports studies about how protecting biodiversity is linked to the social and economic development of local communities. <ref name="Website-3">{{Cite web |title=About Network EABRN and its Statutes |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/eabrn/about-network-eabrn |access-date=22 October 2026 |website=Kazakhstan National Committee for UNESCO MAB}}</ref>
EABRN holds regional meetings, scientific seminars and training workshops to help its members work together. Its activities also include exchanges between experts and sharing knowledge about managing biosphere reserves. For example, the network held its 14th meeting at Shiga Highland Biosphere Reserve in Japan from 6 to 9 October 2015. Participants discussed how local communities can help manage biosphere reserves and chose the network’s main goals for the next ten years. <ref name="Website-3" />
'''Organization and cooperation'''
The East Asian Biosphere Reserve Network (EABRN) is coordinated by the UNESCO Office in Beijing, the UNESCO Man and the Biosphere (MAB) Programme Secretariat in Paris, and national focal points from its member countries. The network also has a Steering Committee made up of the national focal points. It guides the network’s activities. The committee’s chairperson and vice-chairperson(s) are chosen at meetings held every two years. <ref name="Website-4">{{Cite web |date=2025 |title=UNESCO Beijing Office Annual Report 2024–2025 |url=https://articles.unesco.org/sites/default/files/medias/fichiers/2025/05/UNESCO%20BEJ%202024-25%20AR_0.pdf |access-date={{subst:DATE}} |website=UNESCO}}</ref>
EABRN members work together by sharing scientific information and experience in managing biosphere reserves. They also hold regional meetings and training workshops, and carry out joint research. The network also supports cooperation between biosphere reserves in different countries. These activities help the region work together to protect biodiversity and support sustainable development. <ref name="Website-4" details="UNESCO" />
* Man and the Biosphere Programme
* World Network of Biosphere Reserves
* Biosphere reserve
* UNESCO
2fgssqafii4c93042vk6tp8m2pyxuuq
769666
769657
2026-10-22T10:39:25Z
Hmyray
76343
769666
wikitext
text/x-wiki
The '''East Asian Biosphere Reserve Network (EABRN)''' helps East Asian countries work together to protect and manage biosphere reserves. It began in 1994 and was formally established in 1995 as part of UNESCO’s Man and the Biosphere (MAB) Programme. The network first included China, the Democratic People’s Republic of Korea, Japan, Mongolia and the Republic of Korea. The Russian Federation joined in 1998, followed by Kazakhstan in 2011. EABRN helps its members share information and experience to support the protection of biodiversity, scientific research and sustainable development. <ref>{{Cite web |title= |url=https://www.unesco.org/en/mab/networks |access-date={{subst:DATE}}}}</ref><ref>{{Cite web |title= |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/eabrn/about-network-eabrn?utm_source |access-date={{subst:DATE}}}}</ref>
== History ==
The East Asian Biosphere Reserve Network (EABRN) began in 1994 and was formally established in 1995 as part of UNESCO’s Man and the Biosphere (MAB) Programme. It was created to help countries work together to manage biosphere reserves, protect biodiversity and support sustainable development. The network first included China, the Democratic People’s Republic of Korea, Japan, Mongolia and the Republic of Korea. The Russian Federation joined in 1998, followed by Kazakhstan in 2011. These new members expanded the network. <ref>{{Cite web |title=About Network EABRN and its Statutes |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/eabrn/about-network-eabrn |access-date=22 October 2026 |website=Kazakhstan National Committee for UNESCO MAB}}</ref><ref>{{Cite web |title=Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks?hub=66369 |access-date=22 October 2026 |website=UNESCO}}</ref>
== Geographic Scope and Membership ==
The East Asian Biosphere Reserve Network (EABRN) includes seven countries: China, the Democratic People’s Republic of Korea, Japan, Kazakhstan, Mongolia, the Republic of Korea and the Russian Federation. <ref name="Website-1">{{Cite web |title=Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks?hub=66369 |access-date=22 October 2026 |website=UNESCO}}</ref>
The network first included five countries: China, the Democratic People’s Republic of Korea, Japan, Mongolia and the Republic of Korea. <ref name="Website-2">{{Cite web |title=About Network EABRN and its Statutes |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/eabrn/about-network-eabrn |access-date=22 October 2026 |website=Kazakhstan National Committee for UNESCO MAB}}</ref> The Russian Federation joined in 1998, followed by Kazakhstan in 2011. <ref name="Website-2" />
EABRN helps its member countries work together and share information about managing and protecting biosphere reserves. <ref name="Website-1" />
== Objectives and activities ==
The East Asian Biosphere Reserve Network (EABRN) aims to help its member countries protect biodiversity and support sustainable development. Its statutes say that the network helps members share information and experience about managing biosphere reserves, scientific research, environmental education and monitoring. It also supports studies about how protecting biodiversity is linked to the social and economic development of local communities. <ref name="Website-3">{{Cite web |title=About Network EABRN and its Statutes |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/eabrn/about-network-eabrn |access-date=22 October 2026 |website=Kazakhstan National Committee for UNESCO MAB}}</ref>
EABRN holds regional meetings, scientific seminars and training workshops to help its members work together. Its activities also include exchanges between experts and sharing knowledge about managing biosphere reserves. For example, the network held its 14th meeting at Shiga Highland Biosphere Reserve in Japan from 6 to 9 October 2015. Participants discussed how local communities can help manage biosphere reserves and chose the network’s main goals for the next ten years. <ref name="Website-3" />
== Organization and cooperation ==
The East Asian Biosphere Reserve Network (EABRN) is coordinated by the UNESCO Office in Beijing, the UNESCO Man and the Biosphere (MAB) Programme Secretariat in Paris, and national focal points from its member countries. The network also has a Steering Committee made up of the national focal points. It guides the network’s activities. The committee’s chairperson and vice-chairperson(s) are chosen at meetings held every two years. <ref name="Website-4">{{Cite web |date=2025 |title=UNESCO Beijing Office Annual Report 2024–2025 |url=https://articles.unesco.org/sites/default/files/medias/fichiers/2025/05/UNESCO%20BEJ%202024-25%20AR_0.pdf |access-date={{subst:DATE}} |website=UNESCO}}</ref>
EABRN members work together by sharing scientific information and experience in managing biosphere reserves. They also hold regional meetings and training workshops, and carry out joint research. The network also supports cooperation between biosphere reserves in different countries. These activities help the region work together to protect biodiversity and support sustainable development. <ref name="Website-4" />
== See also ==
* [[Man and the Biosphere Programme]]
* [[World Network of Biosphere Reserves]]
* [[Biosphere reserve]]
* [[UNESCO]]
== References ==
{{reflist}}
== External links ==
* [https://www.unesco.org/en/mab/networks?hub=66369 UNESCO: Regional and Thematic Networks]
n80hb6cifqp9acmg1t9i2lq9f66czdf
769683
769666
2026-10-22T10:59:55Z
Mehlikaaa
76336
I added a link of the site that Man and Biosphere Programe
769683
wikitext
text/x-wiki
The '''East Asian Biosphere Reserve Network (EABRN)''' helps East Asian countries work together to protect and manage biosphere reserves. It began in 1994 and was formally established in 1995 as part of UNESCO’s Man and the Biosphere (MAB) Programme. The network first included China, the Democratic People’s Republic of Korea, Japan, Mongolia and the Republic of Korea. The Russian Federation joined in 1998, followed by Kazakhstan in 2011. EABRN helps its members share information and experience to support the protection of biodiversity, scientific research and sustainable development. <ref>{{Cite web |title= |url=https://www.unesco.org/en/mab/networks |access-date={{subst:DATE}}}}</ref><ref>{{Cite web |title= |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/eabrn/about-network-eabrn?utm_source |access-date={{subst:DATE}}}}</ref>
== History ==
The East Asian Biosphere Reserve Network (EABRN) began in 1994 and was formally established in 1995 as part of UNESCO’s Man and the Biosphere (MAB) Programme. It was created to help countries work together to manage biosphere reserves, protect biodiversity and support sustainable development. The network first included China, the Democratic People’s Republic of Korea, Japan, Mongolia and the Republic of Korea. The Russian Federation joined in 1998, followed by Kazakhstan in 2011. These new members expanded the network. <ref>{{Cite web |title=About Network EABRN and its Statutes |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/eabrn/about-network-eabrn |access-date=22 October 2026 |website=Kazakhstan National Committee for UNESCO MAB}}</ref><ref>{{Cite web |title=Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks?hub=66369 |access-date=22 October 2026 |website=UNESCO}}</ref>
== Geographic Scope and Membership ==
The East Asian Biosphere Reserve Network (EABRN) includes seven countries: China, the Democratic People’s Republic of Korea, Japan, Kazakhstan, Mongolia, the Republic of Korea and the Russian Federation. <ref name="Website-1">{{Cite web |title=Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks?hub=66369 |access-date=22 October 2026 |website=UNESCO}}</ref>
The network first included five countries: China, the Democratic People’s Republic of Korea, Japan, Mongolia and the Republic of Korea. <ref name="Website-2">{{Cite web |title=About Network EABRN and its Statutes |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/eabrn/about-network-eabrn |access-date=22 October 2026 |website=Kazakhstan National Committee for UNESCO MAB}}</ref> The Russian Federation joined in 1998, followed by Kazakhstan in 2011. <ref name="Website-2" />
EABRN helps its member countries work together and share information about managing and protecting biosphere reserves. <ref name="Website-1" />
== Objectives and activities ==
The East Asian Biosphere Reserve Network (EABRN) aims to help its member countries protect biodiversity and support sustainable development. Its statutes say that the network helps members share information and experience about managing biosphere reserves, scientific research, environmental education and monitoring. It also supports studies about how protecting biodiversity is linked to the social and economic development of local communities. <ref name="Website-3">{{Cite web |title=About Network EABRN and its Statutes |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/eabrn/about-network-eabrn |access-date=22 October 2026 |website=Kazakhstan National Committee for UNESCO MAB}}</ref>
EABRN holds regional meetings, scientific seminars and training workshops to help its members work together. Its activities also include exchanges between experts and sharing knowledge about managing biosphere reserves. For example, the network held its 14th meeting at Shiga Highland Biosphere Reserve in Japan from 6 to 9 October 2015. Participants discussed how local communities can help manage biosphere reserves and chose the network’s main goals for the next ten years. <ref name="Website-3" />
== Organization and cooperation ==
The East Asian Biosphere Reserve Network (EABRN) is coordinated by the UNESCO Office in Beijing, the UNESCO Man and the Biosphere (MAB) Programme Secretariat in Paris, and national focal points from its member countries. The network also has a Steering Committee made up of the national focal points. It guides the network’s activities. The committee’s chairperson and vice-chairperson(s) are chosen at meetings held every two years. <ref name="Website-4">{{Cite web |date=2025 |title=UNESCO Beijing Office Annual Report 2024–2025 |url=https://articles.unesco.org/sites/default/files/medias/fichiers/2025/05/UNESCO%20BEJ%202024-25%20AR_0.pdf |access-date={{subst:DATE}} |website=UNESCO}}</ref>
EABRN members work together by sharing scientific information and experience in managing biosphere reserves. They also hold regional meetings and training workshops, and carry out joint research. The network also supports cooperation between biosphere reserves in different countries. These activities help the region work together to protect biodiversity and support sustainable development. <ref name="Website-4" />
== See also ==
https://www.unesco.org/en/mab
== References ==
{{reflist}}
== External links ==
* [https://www.unesco.org/en/mab/networks?hub=66369 UNESCO: Regional and Thematic Networks]
eoe784bg6ogzysogyi1tk5iy38hj2q3
769685
769683
2026-10-22T11:03:54Z
Hmyray
76343
769685
wikitext
text/x-wiki
The '''East Asian Biosphere Reserve Network (EABRN)''' helps East Asian countries work together to protect and manage biosphere reserves. It began in 1994 and was formally established in 1995 as part of UNESCO’s Man and the Biosphere (MAB) Programme. The network first included China, the Democratic People’s Republic of Korea, Japan, Mongolia and the Republic of Korea. The Russian Federation joined in 1998, followed by Kazakhstan in 2011. EABRN helps its members share information and experience to support the protection of biodiversity, scientific research and sustainable development. <ref>{{Cite web |title= |url=https://www.unesco.org/en/mab/networks |access-date={{subst:DATE}}}}</ref><ref>{{Cite web |title= |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/eabrn/about-network-eabrn?utm_source |access-date={{subst:DATE}}}}</ref>
== History ==
The East Asian Biosphere Reserve Network (EABRN) began in 1994 and was formally established in 1995 as part of UNESCO’s Man and the Biosphere (MAB) Programme. It was created to help countries work together to manage biosphere reserves, protect biodiversity and support sustainable development. The network first included China, the Democratic People’s Republic of Korea, Japan, Mongolia and the Republic of Korea. The Russian Federation joined in 1998, followed by Kazakhstan in 2011. These new members expanded the network. <ref>{{Cite web |title=About Network EABRN and its Statutes |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/eabrn/about-network-eabrn |access-date=22 October 2026 |website=Kazakhstan National Committee for UNESCO MAB}}</ref><ref>{{Cite web |title=Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks?hub=66369 |access-date=22 October 2026 |website=UNESCO}}</ref>
== Geographic Scope and Membership ==
The East Asian Biosphere Reserve Network (EABRN) includes seven countries: China, the Democratic People’s Republic of Korea, Japan, Kazakhstan, Mongolia, the Republic of Korea and the Russian Federation. <ref name="Website-1">{{Cite web |title=Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks?hub=66369 |access-date=22 October 2026 |website=UNESCO}}</ref>
The network first included five countries: China, the Democratic People’s Republic of Korea, Japan, Mongolia and the Republic of Korea. <ref name="Website-2">{{Cite web |title=About Network EABRN and its Statutes |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/eabrn/about-network-eabrn |access-date=22 October 2026 |website=Kazakhstan National Committee for UNESCO MAB}}</ref> The Russian Federation joined in 1998, followed by Kazakhstan in 2011. <ref name="Website-2" />
EABRN helps its member countries work together and share information about managing and protecting biosphere reserves. <ref name="Website-1" />
== Objectives and activities ==
The East Asian Biosphere Reserve Network (EABRN) aims to help its member countries protect biodiversity and support sustainable development. Its statutes say that the network helps members share information and experience about managing biosphere reserves, scientific research, environmental education and monitoring. It also supports studies about how protecting biodiversity is linked to the social and economic development of local communities. <ref name="Website-3">{{Cite web |title=About Network EABRN and its Statutes |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/eabrn/about-network-eabrn |access-date=22 October 2026 |website=Kazakhstan National Committee for UNESCO MAB}}</ref>
EABRN holds regional meetings, scientific seminars and training workshops to help its members work together. Its activities also include exchanges between experts and sharing knowledge about managing biosphere reserves. For example, the network held its 14th meeting at Shiga Highland Biosphere Reserve in Japan from 6 to 9 October 2015. Participants discussed how local communities can help manage biosphere reserves and chose the network’s main goals for the next ten years. <ref name="Website-3" />
== Organization and cooperation ==
The East Asian Biosphere Reserve Network (EABRN) is coordinated by the UNESCO Office in Beijing, the UNESCO Man and the Biosphere (MAB) Programme Secretariat in Paris, and national focal points from its member countries. The network also has a Steering Committee made up of the national focal points. It guides the network’s activities. The committee’s chairperson and vice-chairperson(s) are chosen at meetings held every two years. <ref name="Website-4">{{Cite web |date=2025 |title=UNESCO Beijing Office Annual Report 2024–2025 |url=https://articles.unesco.org/sites/default/files/medias/fichiers/2025/05/UNESCO%20BEJ%202024-25%20AR_0.pdf |access-date={{subst:DATE}} |website=UNESCO}}</ref>
EABRN members work together by sharing scientific information and experience in managing biosphere reserves. They also hold regional meetings and training workshops, and carry out joint research. The network also supports cooperation between biosphere reserves in different countries. These activities help the region work together to protect biodiversity and support sustainable development. <ref name="Website-4" />
== See also ==
* [[Man and the Biosphere Programme]]
* [[World Network of Biosphere Reserves]]
* [[Biosphere reserve]]
* [[UNESCO]]
== References ==
{{reflist}}
== External links ==
* [https://www.unesco.org/en/mab/networks?hub=66369 UNESCO: Regional and Thematic Networks]
n80hb6cifqp9acmg1t9i2lq9f66czdf
769707
769685
2026-10-22T11:25:01Z
Mehlikaaa
76336
/* History */
769707
wikitext
text/x-wiki
The '''East Asian Biosphere Reserve Network (EABRN)''' helps East Asian countries work together to protect and manage biosphere reserves. It began in 1994 and was formally established in 1995 as part of UNESCO’s Man and the Biosphere (MAB) Programme. The network first included China, the Democratic People’s Republic of Korea, Japan, Mongolia and the Republic of Korea. The Russian Federation joined in 1998, followed by Kazakhstan in 2011. EABRN helps its members share information and experience to support the protection of biodiversity, scientific research and sustainable development. <ref>{{Cite web |title= |url=https://www.unesco.org/en/mab/networks |access-date={{subst:DATE}}}}</ref><ref name="Website-5">{{Cite web |title= |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/eabrn/about-network-eabrn?utm_source |access-date={{subst:DATE}}}}</ref>
== History ==
The East Asian Biosphere Reserve Network (EABRN) began in 1994 and was formally established in 1995 as part of UNESCO’s Man and the Biosphere (MAB) Programme. It was created to help countries work together to manage biosphere reserves, protect biodiversity and support sustainable development. The network first included China, the Democratic People’s Republic of Korea, Japan, Mongolia and the Republic of Korea. The Russian Federation joined in 1998, followed by Kazakhstan in 2011. These new members expanded the network. <ref name="Website-5" /><ref name="Website-2" />
== Geographic Scope and Membership ==
The East Asian Biosphere Reserve Network (EABRN) includes seven countries: China, the Democratic People’s Republic of Korea, Japan, Kazakhstan, Mongolia, the Republic of Korea and the Russian Federation. <ref name="Website-1">{{Cite web |title=Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks?hub=66369 |access-date=22 October 2026 |website=UNESCO}}</ref>
The network first included five countries: China, the Democratic People’s Republic of Korea, Japan, Mongolia and the Republic of Korea. <ref name="Website-2">{{Cite web |title=About Network EABRN and its Statutes |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/eabrn/about-network-eabrn |access-date=22 October 2026 |website=Kazakhstan National Committee for UNESCO MAB}}</ref> The Russian Federation joined in 1998, followed by Kazakhstan in 2011. <ref name="Website-2" />
EABRN helps its member countries work together and share information about managing and protecting biosphere reserves. <ref name="Website-1" />
== Objectives and activities ==
The East Asian Biosphere Reserve Network (EABRN) aims to help its member countries protect biodiversity and support sustainable development. Its statutes say that the network helps members share information and experience about managing biosphere reserves, scientific research, environmental education and monitoring. It also supports studies about how protecting biodiversity is linked to the social and economic development of local communities. <ref name="Website-3">{{Cite web |title=About Network EABRN and its Statutes |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/eabrn/about-network-eabrn |access-date=22 October 2026 |website=Kazakhstan National Committee for UNESCO MAB}}</ref>
EABRN holds regional meetings, scientific seminars and training workshops to help its members work together. Its activities also include exchanges between experts and sharing knowledge about managing biosphere reserves. For example, the network held its 14th meeting at Shiga Highland Biosphere Reserve in Japan from 6 to 9 October 2015. Participants discussed how local communities can help manage biosphere reserves and chose the network’s main goals for the next ten years. <ref name="Website-3" />
== Organization and cooperation ==
The East Asian Biosphere Reserve Network (EABRN) is coordinated by the UNESCO Office in Beijing, the UNESCO Man and the Biosphere (MAB) Programme Secretariat in Paris, and national focal points from its member countries. The network also has a Steering Committee made up of the national focal points. It guides the network’s activities. The committee’s chairperson and vice-chairperson(s) are chosen at meetings held every two years. <ref name="Website-4">{{Cite web |date=2025 |title=UNESCO Beijing Office Annual Report 2024–2025 |url=https://articles.unesco.org/sites/default/files/medias/fichiers/2025/05/UNESCO%20BEJ%202024-25%20AR_0.pdf |access-date={{subst:DATE}} |website=UNESCO}}</ref>
EABRN members work together by sharing scientific information and experience in managing biosphere reserves. They also hold regional meetings and training workshops, and carry out joint research. The network also supports cooperation between biosphere reserves in different countries. These activities help the region work together to protect biodiversity and support sustainable development. <ref name="Website-4" />
== See also ==
* [[Man and the Biosphere Programme]]
* [[World Network of Biosphere Reserves]]
* [[Biosphere reserve]]
* [[UNESCO]]
== References ==
{{reflist}}
== External links ==
* [https://www.unesco.org/en/mab/networks?hub=66369 UNESCO: Regional and Thematic Networks]
f98ccmstbdncfohpx978khv777z8iu9
769711
769707
2026-10-22T11:27:51Z
Mehlikaaa
76336
/* Objectives and activities */
769711
wikitext
text/x-wiki
The '''East Asian Biosphere Reserve Network (EABRN)''' helps East Asian countries work together to protect and manage biosphere reserves. It began in 1994 and was formally established in 1995 as part of UNESCO’s Man and the Biosphere (MAB) Programme. The network first included China, the Democratic People’s Republic of Korea, Japan, Mongolia and the Republic of Korea. The Russian Federation joined in 1998, followed by Kazakhstan in 2011. EABRN helps its members share information and experience to support the protection of biodiversity, scientific research and sustainable development. <ref>{{Cite web |title= |url=https://www.unesco.org/en/mab/networks |access-date={{subst:DATE}}}}</ref><ref name="Website-5">{{Cite web |title= |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/eabrn/about-network-eabrn?utm_source |access-date={{subst:DATE}}}}</ref>
== History ==
The East Asian Biosphere Reserve Network (EABRN) began in 1994 and was formally established in 1995 as part of UNESCO’s Man and the Biosphere (MAB) Programme. It was created to help countries work together to manage biosphere reserves, protect biodiversity and support sustainable development. The network first included China, the Democratic People’s Republic of Korea, Japan, Mongolia and the Republic of Korea. The Russian Federation joined in 1998, followed by Kazakhstan in 2011. These new members expanded the network. <ref name="Website-5" /><ref name="Website-2" />
== Geographic Scope and Membership ==
The East Asian Biosphere Reserve Network (EABRN) includes seven countries: China, the Democratic People’s Republic of Korea, Japan, Kazakhstan, Mongolia, the Republic of Korea and the Russian Federation. <ref name="Website-1">{{Cite web |title=Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks?hub=66369 |access-date=22 October 2026 |website=UNESCO}}</ref>
The network first included five countries: China, the Democratic People’s Republic of Korea, Japan, Mongolia and the Republic of Korea. <ref name="Website-2">{{Cite web |title=About Network EABRN and its Statutes |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/eabrn/about-network-eabrn |access-date=22 October 2026 |website=Kazakhstan National Committee for UNESCO MAB}}</ref> The Russian Federation joined in 1998, followed by Kazakhstan in 2011. <ref name="Website-2" />
EABRN helps its member countries work together and share information about managing and protecting biosphere reserves. <ref name="Website-1" />
== Objectives and activities ==
The East Asian Biosphere Reserve Network (EABRN) aims to help its member countries protect biodiversity and support sustainable development. Its statutes say that the network helps members share information and experience about managing biosphere reserves, scientific research, environmental education and monitoring. It also supports studies about how protecting biodiversity is linked to the social and economic development of local communities. <ref name="Website-2" />
EABRN holds regional meetings, scientific seminars and training workshops to help its members work together. Its activities also include exchanges between experts and sharing knowledge about managing biosphere reserves. For example, the network held its 14th meeting at Shiga Highland Biosphere Reserve in Japan from 6 to 9 October 2015. Participants discussed how local communities can help manage biosphere reserves and chose the network’s main goals for the next ten years. <ref name="Website-2" />
== Organization and cooperation ==
The East Asian Biosphere Reserve Network (EABRN) is coordinated by the UNESCO Office in Beijing, the UNESCO Man and the Biosphere (MAB) Programme Secretariat in Paris, and national focal points from its member countries. The network also has a Steering Committee made up of the national focal points. It guides the network’s activities. The committee’s chairperson and vice-chairperson(s) are chosen at meetings held every two years. <ref name="Website-4">{{Cite web |date=2025 |title=UNESCO Beijing Office Annual Report 2024–2025 |url=https://articles.unesco.org/sites/default/files/medias/fichiers/2025/05/UNESCO%20BEJ%202024-25%20AR_0.pdf |access-date={{subst:DATE}} |website=UNESCO}}</ref>
EABRN members work together by sharing scientific information and experience in managing biosphere reserves. They also hold regional meetings and training workshops, and carry out joint research. The network also supports cooperation between biosphere reserves in different countries. These activities help the region work together to protect biodiversity and support sustainable development. <ref name="Website-4" />
== See also ==
* [[Man and the Biosphere Programme]]
* [[World Network of Biosphere Reserves]]
* [[Biosphere reserve]]
* [[UNESCO]]
== References ==
{{reflist}}
== External links ==
* [https://www.unesco.org/en/mab/networks?hub=66369 UNESCO: Regional and Thematic Networks]
i5qe2qi01gl4ljv3kt0dl2jevx7f6ka
769724
769711
2026-10-22T11:35:39Z
Hmyray
76343
769724
wikitext
text/x-wiki
{| class="infobox" style="width:22em; font-size:90%;"
|+ class="infobox-title" | East Asian Biosphere Reserve Network
|-
! Abbreviation
| EABRN
|-
! Established
| Initiated in 1994; formally established in 1995
|-
! Programme
| [[Man and the Biosphere Programme|UNESCO Man and the Biosphere (MAB) Programme]]
|-
! Region
| East Asia
|-
! Initial member countries
| China; Democratic People’s Republic of Korea; Japan; Mongolia; Republic of Korea
|-
! Later members
| Russian Federation (1998); Kazakhstan (2011)
|}
The '''East Asian Biosphere Reserve Network (EABRN)''' helps East Asian countries work together to protect and manage biosphere reserves. It began in 1994 and was formally established in 1995 as part of UNESCO’s Man and the Biosphere (MAB) Programme. The network first included China, the Democratic People’s Republic of Korea, Japan, Mongolia and the Republic of Korea. The Russian Federation joined in 1998, followed by Kazakhstan in 2011. EABRN helps its members share information and experience to support the protection of biodiversity, scientific research and sustainable development. <ref>{{Cite web |title= |url=https://www.unesco.org/en/mab/networks |access-date={{subst:DATE}}}}</ref><ref name="Website-5">{{Cite web |title= |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/eabrn/about-network-eabrn?utm_source |access-date={{subst:DATE}}}}</ref>
== History ==
The East Asian Biosphere Reserve Network (EABRN) began in 1994 and was formally established in 1995 as part of UNESCO’s Man and the Biosphere (MAB) Programme. It was created to help countries work together to manage biosphere reserves, protect biodiversity and support sustainable development. The network first included China, the Democratic People’s Republic of Korea, Japan, Mongolia and the Republic of Korea. The Russian Federation joined in 1998, followed by Kazakhstan in 2011. These new members expanded the network. <ref name="Website-5" /><ref name="Website-2" />
== Geographic Scope and Membership ==
The East Asian Biosphere Reserve Network (EABRN) includes seven countries: China, the Democratic People’s Republic of Korea, Japan, Kazakhstan, Mongolia, the Republic of Korea and the Russian Federation. <ref name="Website-1">{{Cite web |title=Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks?hub=66369 |access-date=22 October 2026 |website=UNESCO}}</ref>
The network first included five countries: China, the Democratic People’s Republic of Korea, Japan, Mongolia and the Republic of Korea. <ref name="Website-2">{{Cite web |title=About Network EABRN and its Statutes |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/eabrn/about-network-eabrn |access-date=22 October 2026 |website=Kazakhstan National Committee for UNESCO MAB}}</ref> The Russian Federation joined in 1998, followed by Kazakhstan in 2011. <ref name="Website-2" />
EABRN helps its member countries work together and share information about managing and protecting biosphere reserves. <ref name="Website-1" />
== Objectives and activities ==
The East Asian Biosphere Reserve Network (EABRN) aims to help its member countries protect biodiversity and support sustainable development. Its statutes say that the network helps members share information and experience about managing biosphere reserves, scientific research, environmental education and monitoring. It also supports studies about how protecting biodiversity is linked to the social and economic development of local communities. <ref name="Website-2" />
EABRN holds regional meetings, scientific seminars and training workshops to help its members work together. Its activities also include exchanges between experts and sharing knowledge about managing biosphere reserves. For example, the network held its 14th meeting at Shiga Highland Biosphere Reserve in Japan from 6 to 9 October 2015. Participants discussed how local communities can help manage biosphere reserves and chose the network’s main goals for the next ten years. <ref name="Website-2" />
== Organization and cooperation ==
The East Asian Biosphere Reserve Network (EABRN) is coordinated by the UNESCO Office in Beijing, the UNESCO Man and the Biosphere (MAB) Programme Secretariat in Paris, and national focal points from its member countries. The network also has a Steering Committee made up of the national focal points. It guides the network’s activities. The committee’s chairperson and vice-chairperson(s) are chosen at meetings held every two years. <ref name="Website-4">{{Cite web |date=2025 |title=UNESCO Beijing Office Annual Report 2024–2025 |url=https://articles.unesco.org/sites/default/files/medias/fichiers/2025/05/UNESCO%20BEJ%202024-25%20AR_0.pdf |access-date={{subst:DATE}} |website=UNESCO}}</ref>
EABRN members work together by sharing scientific information and experience in managing biosphere reserves. They also hold regional meetings and training workshops, and carry out joint research. The network also supports cooperation between biosphere reserves in different countries. These activities help the region work together to protect biodiversity and support sustainable development. <ref name="Website-4" />
== See also ==
* [[Man and the Biosphere Programme]]
* [[World Network of Biosphere Reserves]]
* [[Biosphere reserve]]
* [[UNESCO]]
== References ==
{{reflist}}
== External links ==
* [https://www.unesco.org/en/mab/networks?hub=66369 UNESCO: Regional and Thematic Networks]
2juyu8xhuxf8k0qd6zzondr6ivknups
769725
769724
2026-10-22T11:36:39Z
Mehlikaaa
76336
/* External links */
769725
wikitext
text/x-wiki
{| class="infobox" style="width:22em; font-size:90%;"
|+ class="infobox-title" | East Asian Biosphere Reserve Network
|-
! Abbreviation
| EABRN
|-
! Established
| Initiated in 1994; formally established in 1995
|-
! Programme
| [[Man and the Biosphere Programme|UNESCO Man and the Biosphere (MAB) Programme]]
|-
! Region
| East Asia
|-
! Initial member countries
| China; Democratic People’s Republic of Korea; Japan; Mongolia; Republic of Korea
|-
! Later members
| Russian Federation (1998); Kazakhstan (2011)
|}
The '''East Asian Biosphere Reserve Network (EABRN)''' helps East Asian countries work together to protect and manage biosphere reserves. It began in 1994 and was formally established in 1995 as part of UNESCO’s Man and the Biosphere (MAB) Programme. The network first included China, the Democratic People’s Republic of Korea, Japan, Mongolia and the Republic of Korea. The Russian Federation joined in 1998, followed by Kazakhstan in 2011. EABRN helps its members share information and experience to support the protection of biodiversity, scientific research and sustainable development. <ref>{{Cite web |title= |url=https://www.unesco.org/en/mab/networks |access-date={{subst:DATE}}}}</ref><ref name="Website-5">{{Cite web |title= |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/eabrn/about-network-eabrn?utm_source |access-date={{subst:DATE}}}}</ref>
== History ==
The East Asian Biosphere Reserve Network (EABRN) began in 1994 and was formally established in 1995 as part of UNESCO’s Man and the Biosphere (MAB) Programme. It was created to help countries work together to manage biosphere reserves, protect biodiversity and support sustainable development. The network first included China, the Democratic People’s Republic of Korea, Japan, Mongolia and the Republic of Korea. The Russian Federation joined in 1998, followed by Kazakhstan in 2011. These new members expanded the network. <ref name="Website-5" /><ref name="Website-2" />
== Geographic Scope and Membership ==
The East Asian Biosphere Reserve Network (EABRN) includes seven countries: China, the Democratic People’s Republic of Korea, Japan, Kazakhstan, Mongolia, the Republic of Korea and the Russian Federation. <ref name="Website-1">{{Cite web |title=Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks?hub=66369 |access-date=22 October 2026 |website=UNESCO}}</ref>
The network first included five countries: China, the Democratic People’s Republic of Korea, Japan, Mongolia and the Republic of Korea. <ref name="Website-2">{{Cite web |title=About Network EABRN and its Statutes |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/eabrn/about-network-eabrn |access-date=22 October 2026 |website=Kazakhstan National Committee for UNESCO MAB}}</ref> The Russian Federation joined in 1998, followed by Kazakhstan in 2011. <ref name="Website-2" />
EABRN helps its member countries work together and share information about managing and protecting biosphere reserves. <ref name="Website-1" />
== Objectives and activities ==
The East Asian Biosphere Reserve Network (EABRN) aims to help its member countries protect biodiversity and support sustainable development. Its statutes say that the network helps members share information and experience about managing biosphere reserves, scientific research, environmental education and monitoring. It also supports studies about how protecting biodiversity is linked to the social and economic development of local communities. <ref name="Website-2" />
EABRN holds regional meetings, scientific seminars and training workshops to help its members work together. Its activities also include exchanges between experts and sharing knowledge about managing biosphere reserves. For example, the network held its 14th meeting at Shiga Highland Biosphere Reserve in Japan from 6 to 9 October 2015. Participants discussed how local communities can help manage biosphere reserves and chose the network’s main goals for the next ten years. <ref name="Website-2" />
== Organization and cooperation ==
The East Asian Biosphere Reserve Network (EABRN) is coordinated by the UNESCO Office in Beijing, the UNESCO Man and the Biosphere (MAB) Programme Secretariat in Paris, and national focal points from its member countries. The network also has a Steering Committee made up of the national focal points. It guides the network’s activities. The committee’s chairperson and vice-chairperson(s) are chosen at meetings held every two years. <ref name="Website-4">{{Cite web |date=2025 |title=UNESCO Beijing Office Annual Report 2024–2025 |url=https://articles.unesco.org/sites/default/files/medias/fichiers/2025/05/UNESCO%20BEJ%202024-25%20AR_0.pdf |access-date={{subst:DATE}} |website=UNESCO}}</ref>
EABRN members work together by sharing scientific information and experience in managing biosphere reserves. They also hold regional meetings and training workshops, and carry out joint research. The network also supports cooperation between biosphere reserves in different countries. These activities help the region work together to protect biodiversity and support sustainable development. <ref name="Website-4" />
== See also ==
* [[Man and the Biosphere Programme]]
* [[World Network of Biosphere Reserves]]
* [[Biosphere reserve]]
* [[UNESCO]]
== References ==
{{reflist}}
nvx9z3ebobj01yjf4kvbd3j658gu9kn
East Atlantic Biosphere Reserve Network
0
179383
769615
769413
2026-10-22T10:05:58Z
Batusmatus
76338
769615
wikitext
text/x-wiki
== Introduction ==
The East Atlantic Biosphere Reserve Network (REDBIOS) is a regional network within UNESCO's Man and the Biosphere (MAB) Programme.
jl7mu7uqpq5sgyx0jdl5u1bpo32dj88
769616
769615
2026-10-22T10:06:58Z
Batusmatus
76338
769616
wikitext
text/x-wiki
== Introduction ==
The East Atlantic Biosphere Reserve Network (REDBIOS) is a regional network within UNESCO's Man and the Biosphere (MAB) Programme.
== History ==
tl7oij953kee29fvgatbuo1e36tomg9
769623
769616
2026-10-22T10:14:45Z
Batusmatus
76338
Topics
769623
wikitext
text/x-wiki
== Introduction ==
The East Atlantic Biosphere Reserve Network (REDBIOS) is a regional network within UNESCO's Man and the Biosphere (MAB) Programme.
== History ==
== Geographic scope and membership ==
== Objectives and activities ==
== See also ==
== References ==
== External links ==
ekgychxuhuiiq93mxdzy4l1p7s0ttpp
769648
769623
2026-10-22T10:31:20Z
~2026-56420-76
76366
769648
wikitext
text/x-wiki
== Introduction ==
The '''East Atlantic Biosphere Reserve Network''' ('''REDBIOS''') is an interregional network established in 1994 within UNESCO's <nowiki>[[Man and the Biosphere Programme]]</nowiki> (MAB). It connects biosphere reserves in the Macaronesian region and parts of West Africa, including the Canary Islands (Spain), the Azores and Madeira (Portugal), Cape Verde, Guinea-Bissau, Mauritania, Morocco, Senegal, and São Tomé and Príncipe. The network promotes cooperation and the exchange of knowledge and experience among biosphere reserves. Its activities focus on biodiversity conservation, the sustainable use of natural resources, environmental education, and sustainable development, particularly in island and coastal areas.
== History ==
== Geographic scope and membership ==
== Objectives and activities ==
== See also ==
== References ==
== External links ==
sncsyi4qwp57oegkyilfqss8fex93ob
769654
769648
2026-10-22T10:34:16Z
~2026-56420-76
76366
/* Introduction */
769654
wikitext
text/x-wiki
== Introduction ==
The East Atlantic Biosphere Reserve Network (REDBIOS) is an interregional network established in 1994 within UNESCO's [[Man and the Biosphere Programme]] (MAB). It connects biosphere reserves in the Macaronesian region and parts of West Africa, including the Canary Islands (Spain), the Azores and Madeira (Portugal), Cape Verde, Guinea-Bissau, Mauritania, Morocco, Senegal, and São Tomé and Príncipe. The network promotes cooperation and the exchange of knowledge and experience among biosphere reserves. Its activities focus on biodiversity conservation, the sustainable use of natural resources, environmental education, and sustainable development, particularly in island and coastal areas.
== History ==
== Geographic scope and membership ==
== Objectives and activities ==
== See also ==
== References ==
== External links ==
a6q3hq0a2hjfy4no2du8rcoco9fn76g
769655
769654
2026-10-22T10:34:27Z
Batusmatus
76338
769655
wikitext
text/x-wiki
== Introduction ==
The East Atlantic Biosphere Reserve Network (REDBIOS) is an interregional network established in 1994 within UNESCO's [[Man and the Biosphere Programme]] (MAB). It connects biosphere reserves in the Macaronesian region and parts of West Africa, including the Canary Islands (Spain), the Azores and Madeira (Portugal), Cape Verde, Guinea-Bissau, Mauritania, Morocco, Senegal, and São Tomé and Príncipe. The network promotes cooperation and the exchange of knowledge and experience among biosphere reserves. Its activities focus on biodiversity conservation, the sustainable use of natural resources, environmental education, and sustainable development, particularly in island and coastal areas.
== History ==
The East Atlantic Biosphere Reserve Network (REDBIOS) was established under the broader framework of the UNESCO Man and the Biosphere (MAB) Programme to foster comprehensive cooperation, biodiversity conservation, and sustainable development among various coastal and island regions situated throughout the East Atlantic.<nowiki><ref>{{Cite web |url=https://en.wikipedia.org/wiki/Man_and_the_Biosphere_Programme |title=Man and the Biosphere Programme |website=Wikipedia}}</ref></nowiki> Over the years, the network has significantly evolved through a series of collaborative international assemblies, most notably demonstrating its ongoing commitment during the eleventh REDBIOS meeting where representatives from participating member states gathered to establish strategic priorities and formulate joint declarations aimed at addressing contemporary environmental challenges.<nowiki><ref>{{Cite web |url=https://www.unesco.org/en/articles/declaration-eleventh-east-atlantic-biosphere-reserves-meeting |title=Declaration of the eleventh East Atlantic Biosphere Reserves meeting |publisher=UNESCO}}</ref></nowiki><nowiki><ref>{{Cite web |url=https://sdg.iisd.org/news/meeting-of-the-east-atlantic-biosphere-reserve-network-sets-priorities/ |title=Meeting of the East Atlantic Biosphere Reserve Network Sets Priorities |publisher=IISD}}</ref></nowiki> These continuous collaborative efforts ultimately strive to strengthen the management capacities of individual biosphere reserves while promoting extensive scientific research, environmental education, and essential knowledge exchange across the interconnected ecosystems of the involved African and European nations.<nowiki><ref>{{Cite web |url=https://www.unesco.org/en/articles/east-atlantic-biosphere-reserves-representatives-participate-eleventh-redbios-meeting |title=East Atlantic Biosphere Reserves representatives participate in the eleventh REDBIOS meeting |publisher=UNESCO}}</ref></nowiki><ref>{{Cite web |title= |url=https://www.unesco.org/en/articles/east-atlantic-biosphere-reserves-representatives-participate-eleventh-redbios-meeting |access-date={{subst:DATE}}}}</ref>
== Geographic scope and membership ==
== Objectives and activities ==
== See also ==
== References ==
== External links ==
esy7v77o5k77rv9c7avxprzri2rbl5h
769660
769655
2026-10-22T10:36:29Z
Batusmatus
76338
769660
wikitext
text/x-wiki
== Introduction ==
The East Atlantic Biosphere Reserve Network (REDBIOS) is an interregional network established in 1994 within UNESCO's [[Man and the Biosphere Programme]] (MAB). It connects biosphere reserves in the Macaronesian region and parts of West Africa, including the Canary Islands (Spain), the Azores and Madeira (Portugal), Cape Verde, Guinea-Bissau, Mauritania, Morocco, Senegal, and São Tomé and Príncipe. The network promotes cooperation and the exchange of knowledge and experience among biosphere reserves. Its activities focus on biodiversity conservation, the sustainable use of natural resources, environmental education, and sustainable development, particularly in island and coastal areas.
== History ==
The East Atlantic Biosphere Reserve Network (REDBIOS) was established under the broader framework of the UNESCO Man and the Biosphere (MAB) Programme to foster comprehensive cooperation, biodiversity conservation, and sustainable development among various coastal and island regions situated throughout the East Atlantic.<ref>{{Cite web |url=https://en.wikipedia.org/wiki/Man_and_the_Biosphere_Programme |title=Man and the Biosphere Programme |website=Wikipedia}}</ref> Over the years, the network has significantly evolved through a series of collaborative international assemblies, most notably demonstrating its ongoing commitment during the eleventh REDBIOS meeting where representatives from participating member states gathered to establish strategic priorities and formulate joint declarations aimed at addressing contemporary environmental challenges.<ref>{{Cite web |url=https://www.unesco.org/en/articles/declaration-eleventh-east-atlantic-biosphere-reserves-meeting |title=Declaration of the eleventh East Atlantic Biosphere Reserves meeting |publisher=UNESCO}}</ref><ref>{{Cite web |url=https://sdg.iisd.org/news/meeting-of-the-east-atlantic-biosphere-reserve-network-sets-priorities/ |title=Meeting of the East Atlantic Biosphere Reserve Network Sets Priorities |publisher=IISD}}</ref> These continuous collaborative efforts ultimately strive to strengthen the management capacities of individual biosphere reserves while promoting extensive scientific research, environmental education, and essential knowledge exchange across the interconnected ecosystems of the involved African and European nations.<ref>{{Cite web |url=https://www.unesco.org/en/articles/east-atlantic-biosphere-reserves-representatives-participate-eleventh-redbios-meeting |title=East Atlantic Biosphere Reserves representatives participate in the eleventh REDBIOS meeting |publisher=UNESCO}}</ref>
== Geographic scope and membership ==
== Objectives and activities ==
== See also ==
== References ==
== External links ==
6pv6xgcqvkqei2xmupk4u6yt1o0454m
769677
769660
2026-10-22T10:48:24Z
Batusmatus
76338
769677
wikitext
text/x-wiki
== Introduction ==
The East Atlantic Biosphere Reserve Network (REDBIOS) is an interregional network established in 1994 within UNESCO's [[Man and the Biosphere Programme]] (MAB). It connects biosphere reserves in the Macaronesian region and parts of West Africa, including the Canary Islands (Spain), the Azores and Madeira (Portugal), Cape Verde, Guinea-Bissau, Mauritania, Morocco, Senegal, and São Tomé and Príncipe. The network promotes cooperation and the exchange of knowledge and experience among biosphere reserves. Its activities focus on biodiversity conservation, the sustainable use of natural resources, environmental education, and sustainable development, particularly in island and coastal areas.
== History ==
The East Atlantic Biosphere Reserve Network (REDBIOS) was established under the broader framework of the UNESCO Man and the Biosphere (MAB) Programme to foster comprehensive cooperation, biodiversity conservation, and sustainable development among various coastal and island regions located around the East Atlantic.<ref>{{Cite web |url=https://en.wikipedia.org/wiki/Man_and_the_Biosphere_Programme |title=Man and the Biosphere Programme |website=Wikipedia}}</ref> Over the years, the network has evolved significantly through a series of collaborative international assemblies. most importantly, he demonstrated his ongoing commitment at the eleventh REDBIOS meeting, where representatives from participating member states gathered to set strategic priorities and create joint statements aimed at addressing contemporary environmental issues.<ref>{{Cite web |url=https://www.unesco.org/en/articles/declaration-eleventh-east-atlantic-biosphere-reserves-meeting |title=Declaration of the eleventh East Atlantic Biosphere Reserves meeting |publisher=UNESCO}}</ref><ref>{{Cite web |url=https://sdg.iisd.org/news/meeting-of-the-east-atlantic-biosphere-reserve-network-sets-priorities/ |title=Meeting of the East Atlantic Biosphere Reserve Network Sets Priorities |publisher=IISD}}</ref> These continuous cooperative efforts ultimately seek to strengthen the management capacities of individual biosphere reserves Decently while promoting comprehensive scientific research, environmental education and basic knowledge exchange between the interconnected ecosystems of the respective African and European countries.<ref>{{Cite web |url=https://www.unesco.org/en/articles/east-atlantic-biosphere-reserves-representatives-participate-eleventh-redbios-meeting |title=East Atlantic Biosphere Reserves representatives participate in the eleventh REDBIOS meeting |publisher=UNESCO}}</ref>
== Geographic scope and membership ==
== Objectives and activities ==
== See also ==
== References ==
== External links ==
2ems7sg2wxjf1m9ewtq3viies32pe6r
769679
769677
2026-10-22T10:49:13Z
Batusmatus
76338
769679
wikitext
text/x-wiki
== Introduction ==
The East Atlantic Biosphere Reserve Network (REDBIOS) is an interregional network established in 1994 within UNESCO's [[Man and the Biosphere Programme]] (MAB). It connects biosphere reserves in the Macaronesian region and parts of West Africa, including the Canary Islands (Spain), the Azores and Madeira (Portugal), Cape Verde, Guinea-Bissau, Mauritania, Morocco, Senegal, and São Tomé and Príncipe.<ref>{{Cite web |url=https://www.unesco.org/en/articles/east-atlantic-biosphere-reserves-representatives-participate-eleventh-redbios-meeting |title=East Atlantic Biosphere Reserves representatives participate in the eleventh REDBIOS meeting |publisher=UNESCO}}</ref> The network promotes cooperation and the exchange of knowledge and experience among biosphere reserves. Its activities focus on biodiversity conservation, the sustainable use of natural resources, environmental education, and sustainable development, particularly in island and coastal areas.
== History ==
The East Atlantic Biosphere Reserve Network (REDBIOS) was established under the broader framework of the UNESCO Man and the Biosphere (MAB) Programme to foster comprehensive cooperation, biodiversity conservation, and sustainable development among various coastal and island regions located around the East Atlantic.<ref>{{Cite web |url=https://en.wikipedia.org/wiki/Man_and_the_Biosphere_Programme |title=Man and the Biosphere Programme |website=Wikipedia}}</ref> Over the years, the network has evolved significantly through a series of collaborative international assemblies. most importantly, he demonstrated his ongoing commitment at the eleventh REDBIOS meeting, where representatives from participating member states gathered to set strategic priorities and create joint statements aimed at addressing contemporary environmental issues.<ref>{{Cite web |url=https://www.unesco.org/en/articles/declaration-eleventh-east-atlantic-biosphere-reserves-meeting |title=Declaration of the eleventh East Atlantic Biosphere Reserves meeting |publisher=UNESCO}}</ref><ref>{{Cite web |url=https://sdg.iisd.org/news/meeting-of-the-east-atlantic-biosphere-reserve-network-sets-priorities/ |title=Meeting of the East Atlantic Biosphere Reserve Network Sets Priorities |publisher=IISD}}</ref> These continuous cooperative efforts ultimately seek to strengthen the management capacities of individual biosphere reserves Decently while promoting comprehensive scientific research, environmental education and basic knowledge exchange between the interconnected ecosystems of the respective African and European countries.<ref>{{Cite web |url=https://www.unesco.org/en/articles/east-atlantic-biosphere-reserves-representatives-participate-eleventh-redbios-meeting |title=East Atlantic Biosphere Reserves representatives participate in the eleventh REDBIOS meeting |publisher=UNESCO}}</ref>
== Geographic scope and membership ==
== Objectives and activities ==
== See also ==
== References ==
== External links ==
q3fkj4042zq6nwf9k7y89f2q9t49ft4
769680
769679
2026-10-22T10:54:03Z
~2026-56420-76
76366
/* Objectives and activities */
769680
wikitext
text/x-wiki
== Introduction ==
The East Atlantic Biosphere Reserve Network (REDBIOS) is an interregional network established in 1994 within UNESCO's [[Man and the Biosphere Programme]] (MAB). It connects biosphere reserves in the Macaronesian region and parts of West Africa, including the Canary Islands (Spain), the Azores and Madeira (Portugal), Cape Verde, Guinea-Bissau, Mauritania, Morocco, Senegal, and São Tomé and Príncipe.<ref>{{Cite web |url=https://www.unesco.org/en/articles/east-atlantic-biosphere-reserves-representatives-participate-eleventh-redbios-meeting |title=East Atlantic Biosphere Reserves representatives participate in the eleventh REDBIOS meeting |publisher=UNESCO}}</ref> The network promotes cooperation and the exchange of knowledge and experience among biosphere reserves. Its activities focus on biodiversity conservation, the sustainable use of natural resources, environmental education, and sustainable development, particularly in island and coastal areas.
== History ==
The East Atlantic Biosphere Reserve Network (REDBIOS) was established under the broader framework of the UNESCO Man and the Biosphere (MAB) Programme to foster comprehensive cooperation, biodiversity conservation, and sustainable development among various coastal and island regions located around the East Atlantic.<ref>{{Cite web |url=https://en.wikipedia.org/wiki/Man_and_the_Biosphere_Programme |title=Man and the Biosphere Programme |website=Wikipedia}}</ref> Over the years, the network has evolved significantly through a series of collaborative international assemblies. most importantly, he demonstrated his ongoing commitment at the eleventh REDBIOS meeting, where representatives from participating member states gathered to set strategic priorities and create joint statements aimed at addressing contemporary environmental issues.<ref>{{Cite web |url=https://www.unesco.org/en/articles/declaration-eleventh-east-atlantic-biosphere-reserves-meeting |title=Declaration of the eleventh East Atlantic Biosphere Reserves meeting |publisher=UNESCO}}</ref><ref>{{Cite web |url=https://sdg.iisd.org/news/meeting-of-the-east-atlantic-biosphere-reserve-network-sets-priorities/ |title=Meeting of the East Atlantic Biosphere Reserve Network Sets Priorities |publisher=IISD}}</ref> These continuous cooperative efforts ultimately seek to strengthen the management capacities of individual biosphere reserves Decently while promoting comprehensive scientific research, environmental education and basic knowledge exchange between the interconnected ecosystems of the respective African and European countries.<ref>{{Cite web |url=https://www.unesco.org/en/articles/east-atlantic-biosphere-reserves-representatives-participate-eleventh-redbios-meeting |title=East Atlantic Biosphere Reserves representatives participate in the eleventh REDBIOS meeting |publisher=UNESCO}}</ref>
== Geographic scope and membership ==
== Objectives and activities ==
The East Atlantic Biosphere Reserve Network (REDBIOS) promotes cooperation among biosphere reserves, biodiversity conservation, environmental education, and sustainable development in island and coastal areas. It supports these goals through international meetings, knowledge-sharing, and collaborative projects. For example, at its eleventh meeting in Príncipe, São Tomé and Príncipe, in May 2013, participants discussed strengthening cooperation and promoting responsible tourism and sustainable resource management. In September 2014, the network held its twelfth meeting in the Azores, focusing on knowledge transfer and partnerships between biosphere reserves.<ref>{{cite web |title=Declaration of the Eleventh East Atlantic Biosphere Reserves Meeting |url=https://www.unesco.org/en/articles/declaration-eleventh-east-atlantic-biosphere-reserves-meeting |publisher=UNESCO |access-date=22 October 2026}}</ref><ref>{{cite web |title=REDBIOS Network, knowledge transfer and collaborative partnerships in the Macaronesian Region |url=https://www.unesco.org/en/articles/redbios-network-knowledge-transfer-and-collaborative-partnerships-macaronesian-region |publisher=UNESCO |access-date=22 October 2026}}</ref>
== See also ==
== References ==
== External links ==
kewcnmu84a79st8pvnob3eae3565oym
769681
769680
2026-10-22T10:56:35Z
~2026-56420-76
76366
/* Objectives and activities */
769681
wikitext
text/x-wiki
== Introduction ==
The East Atlantic Biosphere Reserve Network (REDBIOS) is an interregional network established in 1994 within UNESCO's [[Man and the Biosphere Programme]] (MAB). It connects biosphere reserves in the Macaronesian region and parts of West Africa, including the Canary Islands (Spain), the Azores and Madeira (Portugal), Cape Verde, Guinea-Bissau, Mauritania, Morocco, Senegal, and São Tomé and Príncipe.<ref>{{Cite web |url=https://www.unesco.org/en/articles/east-atlantic-biosphere-reserves-representatives-participate-eleventh-redbios-meeting |title=East Atlantic Biosphere Reserves representatives participate in the eleventh REDBIOS meeting |publisher=UNESCO}}</ref> The network promotes cooperation and the exchange of knowledge and experience among biosphere reserves. Its activities focus on biodiversity conservation, the sustainable use of natural resources, environmental education, and sustainable development, particularly in island and coastal areas.
== History ==
The East Atlantic Biosphere Reserve Network (REDBIOS) was established under the broader framework of the UNESCO Man and the Biosphere (MAB) Programme to foster comprehensive cooperation, biodiversity conservation, and sustainable development among various coastal and island regions located around the East Atlantic.<ref>{{Cite web |url=https://en.wikipedia.org/wiki/Man_and_the_Biosphere_Programme |title=Man and the Biosphere Programme |website=Wikipedia}}</ref> Over the years, the network has evolved significantly through a series of collaborative international assemblies. most importantly, he demonstrated his ongoing commitment at the eleventh REDBIOS meeting, where representatives from participating member states gathered to set strategic priorities and create joint statements aimed at addressing contemporary environmental issues.<ref>{{Cite web |url=https://www.unesco.org/en/articles/declaration-eleventh-east-atlantic-biosphere-reserves-meeting |title=Declaration of the eleventh East Atlantic Biosphere Reserves meeting |publisher=UNESCO}}</ref><ref>{{Cite web |url=https://sdg.iisd.org/news/meeting-of-the-east-atlantic-biosphere-reserve-network-sets-priorities/ |title=Meeting of the East Atlantic Biosphere Reserve Network Sets Priorities |publisher=IISD}}</ref> These continuous cooperative efforts ultimately seek to strengthen the management capacities of individual biosphere reserves Decently while promoting comprehensive scientific research, environmental education and basic knowledge exchange between the interconnected ecosystems of the respective African and European countries.<ref>{{Cite web |url=https://www.unesco.org/en/articles/east-atlantic-biosphere-reserves-representatives-participate-eleventh-redbios-meeting |title=East Atlantic Biosphere Reserves representatives participate in the eleventh REDBIOS meeting |publisher=UNESCO}}</ref>
== Geographic scope and membership ==
== Objectives and activities ==
The East Atlantic Biosphere Reserve Network (REDBIOS) promotes cooperation among biosphere reserves, biodiversity conservation, environmental education, and sustainable development in island and coastal areas. It supports these goals through international meetings, knowledge-sharing, and collaborative projects. For example, at its eleventh meeting in Príncipe, São Tomé and Príncipe, in May 2013, participants discussed strengthening cooperation and promoting responsible tourism and sustainable resource management.<ref>{{cite web |title=Declaration of the Eleventh East Atlantic Biosphere Reserves Meeting |url=https://www.unesco.org/en/articles/declaration-eleventh-east-atlantic-biosphere-reserves-meeting |publisher=UNESCO |access-date=22 October 2026}}</ref> In September 2014, the network held its twelfth meeting in the Azores, focusing on knowledge transfer and partnerships between biosphere reserves.<ref>{{cite web |title=REDBIOS Network, knowledge transfer and collaborative partnerships in the Macaronesian Region |url=https://www.unesco.org/en/articles/redbios-network-knowledge-transfer-and-collaborative-partnerships-macaronesian-region |publisher=UNESCO |access-date=22 October 2026}}</ref>
== See also ==
== References ==
== External links ==
1n3o87gqcv8ffaazo54tijix8t0u4j4
769686
769681
2026-10-22T11:04:55Z
Batusmatus
76338
769686
wikitext
text/x-wiki
== Introduction ==
The East Atlantic Biosphere Reserve Network (REDBIOS) is an interregional network established in 1994 within UNESCO's [[Man and the Biosphere Programme]] (MAB). It connects biosphere reserves in the Macaronesian region and parts of West Africa, including the Canary Islands (Spain), the Azores and Madeira (Portugal), Cape Verde, Guinea-Bissau, Mauritania, Morocco, Senegal, and São Tomé and Príncipe.<ref>{{Cite web |url=https://www.unesco.org/en/articles/east-atlantic-biosphere-reserves-representatives-participate-eleventh-redbios-meeting |title=East Atlantic Biosphere Reserves representatives participate in the eleventh REDBIOS meeting |publisher=UNESCO}}</ref> The network promotes cooperation and the exchange of knowledge and experience among biosphere reserves. Its activities focus on biodiversity conservation, the sustainable use of natural resources, environmental education, and sustainable development, particularly in island and coastal areas.
== History ==
The East Atlantic Biosphere Reserve Network (REDBIOS) was officially established in 1994 under the broader framework of the UNESCO Man and the Biosphere (MAB) Programme to foster comprehensive cooperation, biodiversity conservation, and sustainable development among various coastal and island regions located around the East Atlantic.<ref>{{Cite web |url=https://en.wikipedia.org/wiki/Man_and_the_Biosphere_Programme |title=Man and the Biosphere Programme |website=Wikipedia}}</ref> Over the years, the network has evolved significantly through a series of collaborative international assemblies. Most importantly, it showed its ongoing approval at the eleventh REDBIOS meeting, where representatives from participating member states came together to set strategic priorities and create joint explanation aimed at addressing contemporary environmental issues.<ref>{{Cite web |url=https://www.unesco.org/en/articles/declaration-eleventh-east-atlantic-biosphere-reserves-meeting |title=Declaration of the eleventh East Atlantic Biosphere Reserves meeting |publisher=UNESCO}}</ref><ref>{{Cite web |url=https://sdg.iisd.org/news/meeting-of-the-east-atlantic-biosphere-reserve-network-sets-priorities/ |title=Meeting of the East Atlantic Biosphere Reserve Network Sets Priorities |publisher=IISD}}</ref> These continuous cooperation efforts are ultimately aimed at strengthening the management capacities of individual biosphere reserves, as well as promoting comprehensive scientific research, environmental education and basic information exchange between the interconnected ecosystems of the respective African and European countries.<ref>{{Cite web |url=https://www.unesco.org/en/articles/east-atlantic-biosphere-reserves-representatives-participate-eleventh-redbios-meeting |title=East Atlantic Biosphere Reserves representatives participate in the eleventh REDBIOS meeting |publisher=UNESCO}}</ref>
== Geographic scope and membership ==
== Objectives and activities ==
== Organization and cooperation ==
== See also ==
== References ==
== External links ==
f8d1jts8x8hqx7hd7m7tb2h4j6wxm69
769688
769686
2026-10-22T11:06:27Z
~2026-56420-76
76366
/* Objectives and activities */
769688
wikitext
text/x-wiki
== Introduction ==
The East Atlantic Biosphere Reserve Network (REDBIOS) is an interregional network established in 1994 within UNESCO's [[Man and the Biosphere Programme]] (MAB). It connects biosphere reserves in the Macaronesian region and parts of West Africa, including the Canary Islands (Spain), the Azores and Madeira (Portugal), Cape Verde, Guinea-Bissau, Mauritania, Morocco, Senegal, and São Tomé and Príncipe.<ref>{{Cite web |url=https://www.unesco.org/en/articles/east-atlantic-biosphere-reserves-representatives-participate-eleventh-redbios-meeting |title=East Atlantic Biosphere Reserves representatives participate in the eleventh REDBIOS meeting |publisher=UNESCO}}</ref> The network promotes cooperation and the exchange of knowledge and experience among biosphere reserves. Its activities focus on biodiversity conservation, the sustainable use of natural resources, environmental education, and sustainable development, particularly in island and coastal areas.
== History ==
The East Atlantic Biosphere Reserve Network (REDBIOS) was officially established in 1994 under the broader framework of the UNESCO Man and the Biosphere (MAB) Programme to foster comprehensive cooperation, biodiversity conservation, and sustainable development among various coastal and island regions located around the East Atlantic.<ref>{{Cite web |url=https://en.wikipedia.org/wiki/Man_and_the_Biosphere_Programme |title=Man and the Biosphere Programme |website=Wikipedia}}</ref> Over the years, the network has evolved significantly through a series of collaborative international assemblies. Most importantly, it showed its ongoing approval at the eleventh REDBIOS meeting, where representatives from participating member states came together to set strategic priorities and create joint explanation aimed at addressing contemporary environmental issues.<ref>{{Cite web |url=https://www.unesco.org/en/articles/declaration-eleventh-east-atlantic-biosphere-reserves-meeting |title=Declaration of the eleventh East Atlantic Biosphere Reserves meeting |publisher=UNESCO}}</ref><ref>{{Cite web |url=https://sdg.iisd.org/news/meeting-of-the-east-atlantic-biosphere-reserve-network-sets-priorities/ |title=Meeting of the East Atlantic Biosphere Reserve Network Sets Priorities |publisher=IISD}}</ref> These continuous cooperation efforts are ultimately aimed at strengthening the management capacities of individual biosphere reserves, as well as promoting comprehensive scientific research, environmental education and basic information exchange between the interconnected ecosystems of the respective African and European countries.<ref>{{Cite web |url=https://www.unesco.org/en/articles/east-atlantic-biosphere-reserves-representatives-participate-eleventh-redbios-meeting |title=East Atlantic Biosphere Reserves representatives participate in the eleventh REDBIOS meeting |publisher=UNESCO}}</ref>
== Geographic scope and membership ==
== Objectives and activities ==
The East Atlantic Biosphere Reserve Network (REDBIOS) promotes cooperation among biosphere reserves, biodiversity conservation, environmental education, and sustainable development in island and coastal areas. It supports these goals through international meetings, knowledge-sharing, and collaborative projects. For example, at its eleventh meeting in Príncipe, São Tomé and Príncipe, in May 2013, participants discussed strengthening cooperation and promoting responsible tourism and sustainable resource management.<ref>{{cite web |title=Declaration of the Eleventh East Atlantic Biosphere Reserves Meeting |url=https://www.unesco.org/en/articles/declaration-eleventh-east-atlantic-biosphere-reserves-meeting |publisher=UNESCO |access-date=22 October 2026}}</ref> In September 2014, the network held its twelfth meeting in the Azores, focusing on knowledge transfer and partnerships between biosphere reserves.<ref>{{cite web |title=REDBIOS Network, knowledge transfer and collaborative partnerships in the Macaronesian Region |url=https://www.unesco.org/en/articles/redbios-network-knowledge-transfer-and-collaborative-partnerships-macaronesian-region |publisher=UNESCO |access-date=22 October 2026}}</ref>
== Organization and cooperation ==
== See also ==
== References ==
== External links ==
ib3ulb8rwwyqb8srb0j3tsay41qkui6
769694
769688
2026-10-22T11:08:36Z
Batusmatus
76338
769694
wikitext
text/x-wiki
== Introduction ==
The East Atlantic Biosphere Reserve Network (REDBIOS) is an interregional network established in 1994 within UNESCO's [[Man and the Biosphere Programme]] (MAB). It connects biosphere reserves in the Macaronesian region and parts of West Africa, including the Canary Islands (Spain), the Azores and Madeira (Portugal), Cape Verde, Guinea-Bissau, Mauritania, Morocco, Senegal, and São Tomé and Príncipe.<ref>{{Cite web |url=https://www.unesco.org/en/articles/east-atlantic-biosphere-reserves-representatives-participate-eleventh-redbios-meeting |title=East Atlantic Biosphere Reserves representatives participate in the eleventh REDBIOS meeting |publisher=UNESCO}}</ref> The network promotes cooperation and the exchange of knowledge and experience among biosphere reserves. Its activities focus on biodiversity conservation, the sustainable use of natural resources, environmental education, and sustainable development, particularly in island and coastal areas.
== History ==
The East Atlantic Biosphere Reserve Network (REDBIOS) was officially established in 1994 under the broader framework of the UNESCO Man and the Biosphere (MAB) Programme to foster comprehensive cooperation, biodiversity conservation, and sustainable development among various coastal and island regions located around the East Atlantic.<ref>{{Cite web |url=https://en.wikipedia.org/wiki/Man_and_the_Biosphere_Programme |title=Man and the Biosphere Programme |website=Wikipedia}}</ref> Over the years, the network has evolved significantly through a series of collaborative international assemblies. Most importantly, it showed its ongoing approval at the eleventh REDBIOS meeting, where representatives from participating member states came together to set strategic priorities and create joint explanation aimed at addressing contemporary environmental issues.<ref>{{Cite web |url=https://www.unesco.org/en/articles/declaration-eleventh-east-atlantic-biosphere-reserves-meeting |title=Declaration of the eleventh East Atlantic Biosphere Reserves meeting |publisher=UNESCO}}</ref><ref>{{Cite web |url=https://sdg.iisd.org/news/meeting-of-the-east-atlantic-biosphere-reserve-network-sets-priorities/ |title=Meeting of the East Atlantic Biosphere Reserve Network Sets Priorities |publisher=IISD}}</ref> These continuous cooperation efforts are ultimately aimed at strengthening the management capacities of individual biosphere reserves, as well as promoting comprehensive scientific research, environmental education and basic information exchange between the interconnected ecosystems of the respective African and European countries.<ref>{{Cite web |url=https://www.unesco.org/en/articles/east-atlantic-biosphere-reserves-representatives-participate-eleventh-redbios-meeting |title=East Atlantic Biosphere Reserves representatives participate in the eleventh REDBIOS meeting |publisher=UNESCO}}</ref>
== Geographic scope and membership ==
== Objectives and activities ==
The East Atlantic Biosphere Reserve Network (REDBIOS) promotes cooperation among biosphere reserves, biodiversity conservation, environmental education, and sustainable development in island and coastal areas. It supports these goals through international meetings, knowledge-sharing, and collaborative projects. For example, at its eleventh meeting in Príncipe, São Tomé and Príncipe, in May 2013, participants discussed strengthening cooperation and promoting responsible tourism and sustainable resource management.<ref>{{cite web |title=Declaration of the Eleventh East Atlantic Biosphere Reserves Meeting |url=https://www.unesco.org/en/articles/declaration-eleventh-east-atlantic-biosphere-reserves-meeting |publisher=UNESCO |access-date=22 October 2026}}</ref> In September 2014, the network held its twelfth meeting in the Azores, focusing on knowledge transfer and partnerships between biosphere reserves.<ref>{{cite web |title=REDBIOS Network, knowledge transfer and collaborative partnerships in the Macaronesian Region |url=https://www.unesco.org/en/articles/redbios-network-knowledge-transfer-and-collaborative-partnerships-macaronesian-region |publisher=UNESCO |access-date=22 October 2026}}</ref>
== Organization and cooperation ==
== See also ==
== References ==
{{reflist}}
== External links ==
305jgxn2edmmat88tdhi5l1xppqrayn
769697
769694
2026-10-22T11:13:27Z
~2026-56420-76
76366
/* Organization and cooperation */
769697
wikitext
text/x-wiki
== Introduction ==
The East Atlantic Biosphere Reserve Network (REDBIOS) is an interregional network established in 1994 within UNESCO's [[Man and the Biosphere Programme]] (MAB). It connects biosphere reserves in the Macaronesian region and parts of West Africa, including the Canary Islands (Spain), the Azores and Madeira (Portugal), Cape Verde, Guinea-Bissau, Mauritania, Morocco, Senegal, and São Tomé and Príncipe.<ref>{{Cite web |url=https://www.unesco.org/en/articles/east-atlantic-biosphere-reserves-representatives-participate-eleventh-redbios-meeting |title=East Atlantic Biosphere Reserves representatives participate in the eleventh REDBIOS meeting |publisher=UNESCO}}</ref> The network promotes cooperation and the exchange of knowledge and experience among biosphere reserves. Its activities focus on biodiversity conservation, the sustainable use of natural resources, environmental education, and sustainable development, particularly in island and coastal areas.
== History ==
The East Atlantic Biosphere Reserve Network (REDBIOS) was officially established in 1994 under the broader framework of the UNESCO Man and the Biosphere (MAB) Programme to foster comprehensive cooperation, biodiversity conservation, and sustainable development among various coastal and island regions located around the East Atlantic.<ref>{{Cite web |url=https://en.wikipedia.org/wiki/Man_and_the_Biosphere_Programme |title=Man and the Biosphere Programme |website=Wikipedia}}</ref> Over the years, the network has evolved significantly through a series of collaborative international assemblies. Most importantly, it showed its ongoing approval at the eleventh REDBIOS meeting, where representatives from participating member states came together to set strategic priorities and create joint explanation aimed at addressing contemporary environmental issues.<ref>{{Cite web |url=https://www.unesco.org/en/articles/declaration-eleventh-east-atlantic-biosphere-reserves-meeting |title=Declaration of the eleventh East Atlantic Biosphere Reserves meeting |publisher=UNESCO}}</ref><ref>{{Cite web |url=https://sdg.iisd.org/news/meeting-of-the-east-atlantic-biosphere-reserve-network-sets-priorities/ |title=Meeting of the East Atlantic Biosphere Reserve Network Sets Priorities |publisher=IISD}}</ref> These continuous cooperation efforts are ultimately aimed at strengthening the management capacities of individual biosphere reserves, as well as promoting comprehensive scientific research, environmental education and basic information exchange between the interconnected ecosystems of the respective African and European countries.<ref>{{Cite web |url=https://www.unesco.org/en/articles/east-atlantic-biosphere-reserves-representatives-participate-eleventh-redbios-meeting |title=East Atlantic Biosphere Reserves representatives participate in the eleventh REDBIOS meeting |publisher=UNESCO}}</ref>
== Geographic scope and membership ==
== Objectives and activities ==
The East Atlantic Biosphere Reserve Network (REDBIOS) promotes cooperation among biosphere reserves, biodiversity conservation, environmental education, and sustainable development in island and coastal areas. It supports these goals through international meetings, knowledge-sharing, and collaborative projects. For example, at its eleventh meeting in Príncipe, São Tomé and Príncipe, in May 2013, participants discussed strengthening cooperation and promoting responsible tourism and sustainable resource management.<ref>{{cite web |title=Declaration of the Eleventh East Atlantic Biosphere Reserves Meeting |url=https://www.unesco.org/en/articles/declaration-eleventh-east-atlantic-biosphere-reserves-meeting |publisher=UNESCO |access-date=22 October 2026}}</ref> In September 2014, the network held its twelfth meeting in the Azores, focusing on knowledge transfer and partnerships between biosphere reserves.<ref>{{cite web |title=REDBIOS Network, knowledge transfer and collaborative partnerships in the Macaronesian Region |url=https://www.unesco.org/en/articles/redbios-network-knowledge-transfer-and-collaborative-partnerships-macaronesian-region |publisher=UNESCO |access-date=22 October 2026}}</ref>
== Organization and cooperation ==
The REDBIOS network is coordinated through the collaboration of UNESCO's Man and the Biosphere (MAB) Secretariat and the Spanish Autonomous Body for National Parks (OAPN), with active involvement from national MAB committees and local biosphere reserve managers.<ref>{{cite report |author=UNESCO |title=Seville +5: International Meeting of Experts on the Implementation of the Seville Strategy of the World Network of Biosphere Reserves |publisher=UNESCO MAB Secretariat |year=2000 |location=Paris}}</ref>
Cooperation among member territories is maintained through regular regional meetings, thematic workshops, and technical exchange programmes hosted rotatively across member archipelagos and coastal areas.<ref>{{cite web |title=Exchanging experiences and transferring knowledge in Macaronesian and West African biosphere reserves |url=https://www.unesco.org/en/articles/exchanging-experiences-and-transferring-knowledge-macaronesian-and-west-african-biosphere-reserves |publisher=UNESCO |website=unesco.org}}</ref> The network also fosters North-South and transboundary environmental cooperation; notably, member countries Spain and Morocco collaborate directly across marine and terrestrial boundaries, including partnerships related to the Intercontinental Biosphere Reserve of the Mediterranean.<ref>{{cite report |author=UNESCO |title=Science in Africa: UNESCO's contribution to Africa's plan for science |publisher=UNESCO |year=2007 |url=https://unesdoc.unesco.org/ark:/48223/pf0000150449}}</ref>
== See also ==
== References ==
{{reflist}}
== External links ==
nf5gxbd2qd264vqnzdo6avv4gk9438g
769699
769697
2026-10-22T11:16:27Z
Batusmatus
76338
769699
wikitext
text/x-wiki
== Introduction ==
The East Atlantic Biosphere Reserve Network (REDBIOS) is an interregional network established in 1994 within UNESCO's [[Man and the Biosphere Programme]] (MAB). It connects biosphere reserves in the Macaronesian region and parts of West Africa, including the Canary Islands (Spain), the Azores and Madeira (Portugal), Cape Verde, Guinea-Bissau, Mauritania, Morocco, Senegal, and São Tomé and Príncipe.<ref>{{Cite web |url=https://www.unesco.org/en/articles/east-atlantic-biosphere-reserves-representatives-participate-eleventh-redbios-meeting |title=East Atlantic Biosphere Reserves representatives participate in the eleventh REDBIOS meeting |publisher=UNESCO}}</ref> The network promotes cooperation and the exchange of knowledge and experience among biosphere reserves. Its activities focus on biodiversity conservation, the sustainable use of natural resources, environmental education, and sustainable development, particularly in island and coastal areas.
== History ==
The East Atlantic Biosphere Reserve Network (REDBIOS) was officially established in 1994 under the broader framework of the UNESCO Man and the Biosphere (MAB) Programme to foster comprehensive cooperation, biodiversity conservation, and sustainable development among various coastal and island regions located around the East Atlantic.<ref>{{Cite web |url=https://en.wikipedia.org/wiki/Man_and_the_Biosphere_Programme |title=Man and the Biosphere Programme |website=Wikipedia}}</ref> Over the years, the network has evolved significantly through a series of collaborative international assemblies. Most importantly, it showed its ongoing approval at the eleventh REDBIOS meeting, where representatives from participating member states came together to set strategic priorities and create joint explanation aimed at addressing contemporary environmental issues.<ref>{{Cite web |url=https://www.unesco.org/en/articles/declaration-eleventh-east-atlantic-biosphere-reserves-meeting |title=Declaration of the eleventh East Atlantic Biosphere Reserves meeting |publisher=UNESCO}}</ref><ref>{{Cite web |url=https://sdg.iisd.org/news/meeting-of-the-east-atlantic-biosphere-reserve-network-sets-priorities/ |title=Meeting of the East Atlantic Biosphere Reserve Network Sets Priorities |publisher=IISD}}</ref> These continuous cooperation efforts are ultimately aimed at strengthening the management capacities of individual biosphere reserves, as well as promoting comprehensive scientific research, environmental education and basic information exchange between the interconnected ecosystems of the respective African and European countries.<ref>{{Cite web |url=https://www.unesco.org/en/articles/east-atlantic-biosphere-reserves-representatives-participate-eleventh-redbios-meeting |title=East Atlantic Biosphere Reserves representatives participate in the eleventh REDBIOS meeting |publisher=UNESCO}}</ref>
== Geographic scope and membership ==
== Objectives and activities ==
The East Atlantic Biosphere Reserve Network (REDBIOS) promotes cooperation among biosphere reserves, biodiversity conservation, environmental education, and sustainable development in island and coastal areas. It supports these goals through international meetings, knowledge-sharing, and collaborative projects. For example, at its eleventh meeting in Príncipe, São Tomé and Príncipe, in May 2013, participants discussed strengthening cooperation and promoting responsible tourism and sustainable resource management.<ref>{{cite web |title=Declaration of the Eleventh East Atlantic Biosphere Reserves Meeting |url=https://www.unesco.org/en/articles/declaration-eleventh-east-atlantic-biosphere-reserves-meeting |publisher=UNESCO |access-date=22 October 2026}}</ref> In September 2014, the network held its twelfth meeting in the Azores, focusing on knowledge transfer and partnerships between biosphere reserves.<ref>{{cite web |title=REDBIOS Network, knowledge transfer and collaborative partnerships in the Macaronesian Region |url=https://www.unesco.org/en/articles/redbios-network-knowledge-transfer-and-collaborative-partnerships-macaronesian-region |publisher=UNESCO |access-date=22 October 2026}}</ref>
== Organization and cooperation ==
The REDBIOS network is coordinated through the collaboration of UNESCO's Man and the Biosphere (MAB) Secretariat and the Spanish Autonomous Body for National Parks (OAPN), with active involvement from national MAB committees and local biosphere reserve managers.<ref>{{cite report |author=UNESCO |title=Seville +5: International Meeting of Experts on the Implementation of the Seville Strategy of the World Network of Biosphere Reserves |publisher=UNESCO MAB Secretariat |year=2000 |location=Paris}}</ref>
Cooperation among member territories is maintained through regular regional meetings, thematic workshops, and technical exchange programmes hosted rotatively across member archipelagos and coastal areas.<ref>{{cite web |title=Exchanging experiences and transferring knowledge in Macaronesian and West African biosphere reserves |url=https://www.unesco.org/en/articles/exchanging-experiences-and-transferring-knowledge-macaronesian-and-west-african-biosphere-reserves |publisher=UNESCO |website=unesco.org}}</ref> The network also fosters North-South and transboundary environmental cooperation; notably, member countries Spain and Morocco collaborate directly across marine and terrestrial boundaries, including partnerships related to the Intercontinental Biosphere Reserve of the Mediterranean.<ref>{{cite report |author=UNESCO |title=Science in Africa: UNESCO's contribution to Africa's plan for science |publisher=UNESCO |year=2007 |url=https://unesdoc.unesco.org/ark:/48223/pf0000150449}}</ref>
== See also ==
* [[Man and the Biosphere Programme]]
* [[World Network of Biosphere Reserves]]
* [[World Network of Biosphere Reserves in Africa]]
* [[World Network of Biosphere Reserves in Europe and North America]]
== References ==
{{reflist}}
== External links ==
* [https://www.unesco.org/en UNESCO official website]
ht2xga056371tqlf6ynm6fwp7v9uya4
769701
769699
2026-10-22T11:21:45Z
Batusmatus
76338
769701
wikitext
text/x-wiki
== Introduction ==
The East Atlantic Biosphere Reserve Network (REDBIOS) is an interregional network established in 1994 within UNESCO's [[Man and the Biosphere Programme]] (MAB). It connects biosphere reserves in the Macaronesian region and parts of West Africa, including the Canary Islands (Spain), the Azores and Madeira (Portugal), Cape Verde, Guinea-Bissau, Mauritania, Morocco, Senegal, and São Tomé and Príncipe.<ref>{{Cite web |url=https://www.unesco.org/en/articles/east-atlantic-biosphere-reserves-representatives-participate-eleventh-redbios-meeting |title=East Atlantic Biosphere Reserves representatives participate in the eleventh REDBIOS meeting |publisher=UNESCO}}</ref> The network promotes cooperation and the exchange of knowledge and experience among biosphere reserves. Its activities focus on biodiversity conservation, the sustainable use of natural resources, environmental education, and sustainable development, particularly in island and coastal areas.
== History ==
The East Atlantic Biosphere Reserve Network (REDBIOS) was officially established in 1994 under the broader framework of the UNESCO Man and the Biosphere (MAB) Programme to foster comprehensive cooperation, biodiversity conservation, and sustainable development among various coastal and island regions located around the East Atlantic.<ref>{{Cite web |url=https://en.wikipedia.org/wiki/Man_and_the_Biosphere_Programme |title=Man and the Biosphere Programme |website=Wikipedia}}</ref> Over the years, the network has evolved significantly through a series of collaborative international assemblies. Most importantly, it showed its ongoing approval at the eleventh REDBIOS meeting, where representatives from participating member states came together to set strategic priorities and create joint explanation aimed at addressing contemporary environmental issues.<ref>{{Cite web |url=https://www.unesco.org/en/articles/declaration-eleventh-east-atlantic-biosphere-reserves-meeting |title=Declaration of the eleventh East Atlantic Biosphere Reserves meeting |publisher=UNESCO}}</ref><ref>{{Cite web |url=https://sdg.iisd.org/news/meeting-of-the-east-atlantic-biosphere-reserve-network-sets-priorities/ |title=Meeting of the East Atlantic Biosphere Reserve Network Sets Priorities |publisher=IISD}}</ref> These continuous cooperation efforts are ultimately aimed at strengthening the management capacities of individual biosphere reserves, as well as promoting comprehensive scientific research, environmental education and basic information exchange between the interconnected ecosystems of the respective African and European countries.<ref>{{Cite web |url=https://www.unesco.org/en/articles/east-atlantic-biosphere-reserves-representatives-participate-eleventh-redbios-meeting |title=East Atlantic Biosphere Reserves representatives participate in the eleventh REDBIOS meeting |publisher=UNESCO}}</ref>
== Geographic scope and membership ==
== Objectives and activities ==
The East Atlantic Biosphere Reserve Network (REDBIOS) promotes cooperation among biosphere reserves, biodiversity conservation, environmental education, and sustainable development in island and coastal areas. It supports these goals through international meetings, knowledge-sharing, and collaborative projects. For example, at its eleventh meeting in Príncipe, São Tomé and Príncipe, in May 2013, participants discussed strengthening cooperation and promoting responsible tourism and sustainable resource management.<ref>{{cite web |title=Declaration of the Eleventh East Atlantic Biosphere Reserves Meeting |url=https://www.unesco.org/en/articles/declaration-eleventh-east-atlantic-biosphere-reserves-meeting |publisher=UNESCO |access-date=22 October 2026}}</ref> In September 2014, the network held its twelfth meeting in the Azores, focusing on knowledge transfer and partnerships between biosphere reserves.<ref>{{cite web |title=REDBIOS Network, knowledge transfer and collaborative partnerships in the Macaronesian Region |url=https://www.unesco.org/en/articles/redbios-network-knowledge-transfer-and-collaborative-partnerships-macaronesian-region |publisher=UNESCO |access-date=22 October 2026}}</ref>
== Organization and cooperation ==
The REDBIOS network is coordinated through the collaboration of UNESCO's Man and the Biosphere (MAB) Secretariat and the Spanish Autonomous Body for National Parks (OAPN), with active involvement from national MAB committees and local biosphere reserve managers. <ref>{{Cite web |title=Seville +5; International Meeting of Experts on the Implementation of the Seville Strategy for Biosphere Reserves, Pamplona, Spain, 23-27 October 2000 |url=https://unesdoc.unesco.org/ark:/48223/pf0000123605 |access-date={{subst:DATE}}}}</ref>
Cooperation among member territories is maintained through regular regional meetings, thematic workshops, and technical exchange programmes hosted rotatively across member archipelagos and coastal areas.<ref>{{cite web |title=Exchanging experiences and transferring knowledge in Macaronesian and West African biosphere reserves |url=https://www.unesco.org/en/articles/exchanging-experiences-and-transferring-knowledge-macaronesian-and-west-african-biosphere-reserves |publisher=UNESCO |website=unesco.org}}</ref> The network also fosters North-South and transboundary environmental cooperation; notably, member countries Spain and Morocco collaborate directly across marine and terrestrial boundaries, including partnerships related to the Intercontinental Biosphere Reserve of the Mediterranean.<ref>{{cite report |author=UNESCO |title=Science in Africa: UNESCO's contribution to Africa's plan for science |publisher=UNESCO |year=2007 |url=https://unesdoc.unesco.org/ark:/48223/pf0000150449}}</ref>
== See also ==
* [[Man and the Biosphere Programme]]
* [[World Network of Biosphere Reserves]]
* [[World Network of Biosphere Reserves in Africa]]
* [[World Network of Biosphere Reserves in Europe and North America]]
== References ==
{{reflist}}
== External links ==
* [https://www.unesco.org/en UNESCO official website]
alhcl2n95sw5syxigq6nbyci1gznwrb
769705
769701
2026-10-22T11:24:04Z
Batusmatus
76338
769705
wikitext
text/x-wiki
[[File:Man and the Biosphere Programme Logo.svg|thumb|Man and the Biosphere Programme Logo]]
== Introduction ==
The East Atlantic Biosphere Reserve Network (REDBIOS) is an interregional network established in 1994 within UNESCO's [[Man and the Biosphere Programme]] (MAB). It connects biosphere reserves in the Macaronesian region and parts of West Africa, including the Canary Islands (Spain), the Azores and Madeira (Portugal), Cape Verde, Guinea-Bissau, Mauritania, Morocco, Senegal, and São Tomé and Príncipe.<ref>{{Cite web |url=https://www.unesco.org/en/articles/east-atlantic-biosphere-reserves-representatives-participate-eleventh-redbios-meeting |title=East Atlantic Biosphere Reserves representatives participate in the eleventh REDBIOS meeting |publisher=UNESCO}}</ref> The network promotes cooperation and the exchange of knowledge and experience among biosphere reserves. Its activities focus on biodiversity conservation, the sustainable use of natural resources, environmental education, and sustainable development, particularly in island and coastal areas.
== History ==
The East Atlantic Biosphere Reserve Network (REDBIOS) was officially established in 1994 under the broader framework of the UNESCO Man and the Biosphere (MAB) Programme to foster comprehensive cooperation, biodiversity conservation, and sustainable development among various coastal and island regions located around the East Atlantic.<ref>{{Cite web |url=https://en.wikipedia.org/wiki/Man_and_the_Biosphere_Programme |title=Man and the Biosphere Programme |website=Wikipedia}}</ref> Over the years, the network has evolved significantly through a series of collaborative international assemblies. Most importantly, it showed its ongoing approval at the eleventh REDBIOS meeting, where representatives from participating member states came together to set strategic priorities and create joint explanation aimed at addressing contemporary environmental issues.<ref>{{Cite web |url=https://www.unesco.org/en/articles/declaration-eleventh-east-atlantic-biosphere-reserves-meeting |title=Declaration of the eleventh East Atlantic Biosphere Reserves meeting |publisher=UNESCO}}</ref><ref>{{Cite web |url=https://sdg.iisd.org/news/meeting-of-the-east-atlantic-biosphere-reserve-network-sets-priorities/ |title=Meeting of the East Atlantic Biosphere Reserve Network Sets Priorities |publisher=IISD}}</ref> These continuous cooperation efforts are ultimately aimed at strengthening the management capacities of individual biosphere reserves, as well as promoting comprehensive scientific research, environmental education and basic information exchange between the interconnected ecosystems of the respective African and European countries.<ref>{{Cite web |url=https://www.unesco.org/en/articles/east-atlantic-biosphere-reserves-representatives-participate-eleventh-redbios-meeting |title=East Atlantic Biosphere Reserves representatives participate in the eleventh REDBIOS meeting |publisher=UNESCO}}</ref>
== Geographic scope and membership ==
== Objectives and activities ==
The East Atlantic Biosphere Reserve Network (REDBIOS) promotes cooperation among biosphere reserves, biodiversity conservation, environmental education, and sustainable development in island and coastal areas. It supports these goals through international meetings, knowledge-sharing, and collaborative projects. For example, at its eleventh meeting in Príncipe, São Tomé and Príncipe, in May 2013, participants discussed strengthening cooperation and promoting responsible tourism and sustainable resource management.<ref>{{cite web |title=Declaration of the Eleventh East Atlantic Biosphere Reserves Meeting |url=https://www.unesco.org/en/articles/declaration-eleventh-east-atlantic-biosphere-reserves-meeting |publisher=UNESCO |access-date=22 October 2026}}</ref> In September 2014, the network held its twelfth meeting in the Azores, focusing on knowledge transfer and partnerships between biosphere reserves.<ref>{{cite web |title=REDBIOS Network, knowledge transfer and collaborative partnerships in the Macaronesian Region |url=https://www.unesco.org/en/articles/redbios-network-knowledge-transfer-and-collaborative-partnerships-macaronesian-region |publisher=UNESCO |access-date=22 October 2026}}</ref>
== Organization and cooperation ==
The REDBIOS network is coordinated through the collaboration of UNESCO's Man and the Biosphere (MAB) Secretariat and the Spanish Autonomous Body for National Parks (OAPN), with active involvement from national MAB committees and local biosphere reserve managers. <ref>{{Cite web |title=Seville +5; International Meeting of Experts on the Implementation of the Seville Strategy for Biosphere Reserves, Pamplona, Spain, 23-27 October 2000 |url=https://unesdoc.unesco.org/ark:/48223/pf0000123605 |access-date={{subst:DATE}}}}</ref>
Cooperation among member territories is maintained through regular regional meetings, thematic workshops, and technical exchange programmes hosted rotatively across member archipelagos and coastal areas.<ref>{{cite web |title=Exchanging experiences and transferring knowledge in Macaronesian and West African biosphere reserves |url=https://www.unesco.org/en/articles/exchanging-experiences-and-transferring-knowledge-macaronesian-and-west-african-biosphere-reserves |publisher=UNESCO |website=unesco.org}}</ref> The network also fosters North-South and transboundary environmental cooperation; notably, member countries Spain and Morocco collaborate directly across marine and terrestrial boundaries, including partnerships related to the Intercontinental Biosphere Reserve of the Mediterranean.<ref>{{cite report |author=UNESCO |title=Science in Africa: UNESCO's contribution to Africa's plan for science |publisher=UNESCO |year=2007 |url=https://unesdoc.unesco.org/ark:/48223/pf0000150449}}</ref>
== See also ==
* [[Man and the Biosphere Programme]]
* [[World Network of Biosphere Reserves]]
* [[World Network of Biosphere Reserves in Africa]]
* [[World Network of Biosphere Reserves in Europe and North America]]
== References ==
{{reflist}}
== External links ==
* [https://www.unesco.org/en UNESCO official website]
1kaxt4enfw3tmisk4b8agjq98vr6ne3
769713
769705
2026-10-22T11:29:11Z
~2026-56420-76
76366
/* Organization and cooperation */
769713
wikitext
text/x-wiki
[[File:Man and the Biosphere Programme Logo.svg|thumb|Man and the Biosphere Programme Logo]]
== Introduction ==
The East Atlantic Biosphere Reserve Network (REDBIOS) is an interregional network established in 1994 within UNESCO's [[Man and the Biosphere Programme]] (MAB). It connects biosphere reserves in the Macaronesian region and parts of West Africa, including the Canary Islands (Spain), the Azores and Madeira (Portugal), Cape Verde, Guinea-Bissau, Mauritania, Morocco, Senegal, and São Tomé and Príncipe.<ref>{{Cite web |url=https://www.unesco.org/en/articles/east-atlantic-biosphere-reserves-representatives-participate-eleventh-redbios-meeting |title=East Atlantic Biosphere Reserves representatives participate in the eleventh REDBIOS meeting |publisher=UNESCO}}</ref> The network promotes cooperation and the exchange of knowledge and experience among biosphere reserves. Its activities focus on biodiversity conservation, the sustainable use of natural resources, environmental education, and sustainable development, particularly in island and coastal areas.
== History ==
The East Atlantic Biosphere Reserve Network (REDBIOS) was officially established in 1994 under the broader framework of the UNESCO Man and the Biosphere (MAB) Programme to foster comprehensive cooperation, biodiversity conservation, and sustainable development among various coastal and island regions located around the East Atlantic.<ref>{{Cite web |url=https://en.wikipedia.org/wiki/Man_and_the_Biosphere_Programme |title=Man and the Biosphere Programme |website=Wikipedia}}</ref> Over the years, the network has evolved significantly through a series of collaborative international assemblies. Most importantly, it showed its ongoing approval at the eleventh REDBIOS meeting, where representatives from participating member states came together to set strategic priorities and create joint explanation aimed at addressing contemporary environmental issues.<ref>{{Cite web |url=https://www.unesco.org/en/articles/declaration-eleventh-east-atlantic-biosphere-reserves-meeting |title=Declaration of the eleventh East Atlantic Biosphere Reserves meeting |publisher=UNESCO}}</ref><ref>{{Cite web |url=https://sdg.iisd.org/news/meeting-of-the-east-atlantic-biosphere-reserve-network-sets-priorities/ |title=Meeting of the East Atlantic Biosphere Reserve Network Sets Priorities |publisher=IISD}}</ref> These continuous cooperation efforts are ultimately aimed at strengthening the management capacities of individual biosphere reserves, as well as promoting comprehensive scientific research, environmental education and basic information exchange between the interconnected ecosystems of the respective African and European countries.<ref>{{Cite web |url=https://www.unesco.org/en/articles/east-atlantic-biosphere-reserves-representatives-participate-eleventh-redbios-meeting |title=East Atlantic Biosphere Reserves representatives participate in the eleventh REDBIOS meeting |publisher=UNESCO}}</ref>
== Geographic scope and membership ==
== Objectives and activities ==
The East Atlantic Biosphere Reserve Network (REDBIOS) promotes cooperation among biosphere reserves, biodiversity conservation, environmental education, and sustainable development in island and coastal areas. It supports these goals through international meetings, knowledge-sharing, and collaborative projects. For example, at its eleventh meeting in Príncipe, São Tomé and Príncipe, in May 2013, participants discussed strengthening cooperation and promoting responsible tourism and sustainable resource management.<ref>{{cite web |title=Declaration of the Eleventh East Atlantic Biosphere Reserves Meeting |url=https://www.unesco.org/en/articles/declaration-eleventh-east-atlantic-biosphere-reserves-meeting |publisher=UNESCO |access-date=22 October 2026}}</ref> In September 2014, the network held its twelfth meeting in the Azores, focusing on knowledge transfer and partnerships between biosphere reserves.<ref>{{cite web |title=REDBIOS Network, knowledge transfer and collaborative partnerships in the Macaronesian Region |url=https://www.unesco.org/en/articles/redbios-network-knowledge-transfer-and-collaborative-partnerships-macaronesian-region |publisher=UNESCO |access-date=22 October 2026}}</ref>
== Organization and cooperation ==
The REDBIOS network is coordinated through the collaboration of UNESCO's Man and the Biosphere (MAB) Secretariat and the Spanish Autonomous Body for National Parks (OAPN), with active involvement from national MAB committees and local biosphere reserve managers. <ref>{{Cite web |title=Seville +5; International Meeting of Experts on the Implementation of the Seville Strategy for Biosphere Reserves, Pamplona, Spain, 23-27 October 2000 |url=https://unesdoc.unesco.org/ark:/48223/pf0000123605 |access-date={{subst:DATE}}}}</ref>
Cooperation among member territories is maintained through regular regional meetings, thematic workshops, and technical exchange programmes hosted rotatively across member archipelagos and coastal areas.<ref>{{cite web |title=Exchanging experiences and transferring knowledge in Macaronesian and West African biosphere reserves |url=https://www.unesco.org/en/articles/exchanging-experiences-and-transferring-knowledge-macaronesian-and-west-african-biosphere-reserves |publisher=UNESCO |website=unesco.org}}</ref>
== See also ==
* [[Man and the Biosphere Programme]]
* [[World Network of Biosphere Reserves]]
* [[World Network of Biosphere Reserves in Africa]]
* [[World Network of Biosphere Reserves in Europe and North America]]
== References ==
{{reflist}}
== External links ==
* [https://www.unesco.org/en UNESCO official website]
btrbhpotpxzqm3fjn3r56mciraxmw8s
769717
769713
2026-10-22T11:32:48Z
Batusmatus
76338
769717
wikitext
text/x-wiki
[[File:Man and the Biosphere Programme Logo.svg|thumb|Man and the Biosphere Programme Logo]]
== Introduction ==
The East Atlantic Biosphere Reserve Network (REDBIOS) is an interregional network established in 1994 within UNESCO's [[Man and the Biosphere Programme]] (MAB). It connects biosphere reserves in the Macaronesian region and parts of West Africa, including the Canary Islands (Spain), the Azores and Madeira (Portugal), Cape Verde, Guinea-Bissau, Mauritania, Morocco, Senegal, and São Tomé and Príncipe.<ref>{{Cite web |url=https://www.unesco.org/en/articles/east-atlantic-biosphere-reserves-representatives-participate-eleventh-redbios-meeting |title=East Atlantic Biosphere Reserves representatives participate in the eleventh REDBIOS meeting |publisher=UNESCO}}</ref> The network promotes cooperation and the exchange of knowledge and experience among biosphere reserves. Its activities focus on biodiversity conservation, the sustainable use of natural resources, environmental education, and sustainable development, particularly in island and coastal areas.
== History ==
The East Atlantic Biosphere Reserve Network (REDBIOS) was officially established in 1994 under the broader framework of the UNESCO Man and the Biosphere (MAB) Programme to foster comprehensive cooperation, biodiversity conservation, and sustainable development among various coastal and island regions located around the East Atlantic.<ref>{{Cite web |url=https://en.wikipedia.org/wiki/Man_and_the_Biosphere_Programme |title=Man and the Biosphere Programme |website=Wikipedia}}</ref> Over the years, the network has evolved significantly through a series of collaborative international assemblies. Most importantly, it showed its ongoing approval at the eleventh REDBIOS meeting, where representatives from participating member states came together to set strategic priorities and create joint explanation aimed at addressing contemporary environmental issues.<ref>{{Cite web |url=https://www.unesco.org/en/articles/declaration-eleventh-east-atlantic-biosphere-reserves-meeting |title=Declaration of the eleventh East Atlantic Biosphere Reserves meeting |publisher=UNESCO}}</ref><ref>{{Cite web |url=https://sdg.iisd.org/news/meeting-of-the-east-atlantic-biosphere-reserve-network-sets-priorities/ |title=Meeting of the East Atlantic Biosphere Reserve Network Sets Priorities |publisher=IISD}}</ref> These continuous cooperation efforts are ultimately aimed at strengthening the management capacities of individual biosphere reserves, as well as promoting comprehensive scientific research, environmental education and basic information exchange between the interconnected ecosystems of the respective African and European countries.<ref>{{Cite web |url=https://www.unesco.org/en/articles/east-atlantic-biosphere-reserves-representatives-participate-eleventh-redbios-meeting |title=East Atlantic Biosphere Reserves representatives participate in the eleventh REDBIOS meeting |publisher=UNESCO}}</ref>
== Geographic scope and membership ==
== Objectives and activities ==
The East Atlantic Biosphere Reserve Network (REDBIOS) promotes cooperation among biosphere reserves, biodiversity conservation, environmental education, and sustainable development in island and coastal areas. It supports these goals through international meetings, knowledge-sharing, and collaborative projects. For example, at its eleventh meeting in Príncipe, São Tomé and Príncipe, in May 2013, participants discussed strengthening cooperation and promoting responsible tourism and sustainable resource management.<ref>{{cite web |title=Declaration of the Eleventh East Atlantic Biosphere Reserves Meeting |url=https://www.unesco.org/en/articles/declaration-eleventh-east-atlantic-biosphere-reserves-meeting |publisher=UNESCO |access-date=22 October 2026}}</ref> In September 2014, the network held its twelfth meeting in the Azores, focusing on knowledge transfer and partnerships between biosphere reserves.<ref>{{cite web |title=REDBIOS Network, knowledge transfer and collaborative partnerships in the Macaronesian Region |url=https://www.unesco.org/en/articles/redbios-network-knowledge-transfer-and-collaborative-partnerships-macaronesian-region |publisher=UNESCO |access-date=22 October 2026}}</ref>
== Organization and cooperation ==
The REDBIOS network is coordinated through the collaboration of UNESCO's Man and the Biosphere (MAB) Secretariat and the Spanish Autonomous Body for National Parks (OAPN), with active involvement from national MAB committees and local biosphere reserve managers. <ref>{{Cite web |url=https://unesdoc.unesco.org/ark:/48223/pf0000123605 |title=UNESCO Digital Library Document |publisher=UNESCO |access-date=22 October 2026}}</ref>
Cooperation among member territories is maintained through regular regional meetings, thematic workshops, and technical exchange programmes hosted rotatively across member archipelagos and coastal areas.<ref>{{cite web |title=Exchanging experiences and transferring knowledge in Macaronesian and West African biosphere reserves |url=https://www.unesco.org/en/articles/exchanging-experiences-and-transferring-knowledge-macaronesian-and-west-african-biosphere-reserves |publisher=UNESCO |website=unesco.org}}</ref>
== See also ==
* [[Man and the Biosphere Programme]]
* [[World Network of Biosphere Reserves]]
* [[World Network of Biosphere Reserves in Africa]]
* [[World Network of Biosphere Reserves in Europe and North America]]
== References ==
{{reflist}}
== External links ==
* [https://www.unesco.org/en UNESCO official website]
djlhm8qpxxpq0xqexejz611cryl1cwe
769721
769717
2026-10-22T11:34:36Z
Batusmatus
76338
769721
wikitext
text/x-wiki
[[File:Man and the Biosphere Programme Logo.svg|thumb|Man and the Biosphere Programme Logo]]
== Introduction ==
The East Atlantic Biosphere Reserve Network (REDBIOS) is an interregional network established in 1994 within UNESCO's [[Man and the Biosphere Programme]] (MAB). It connects biosphere reserves in the Macaronesian region and parts of West Africa, including the Canary Islands (Spain), the Azores and Madeira (Portugal), Cape Verde, Guinea-Bissau, Mauritania, Morocco, Senegal, and São Tomé and Príncipe.<ref>{{Cite web |url=https://www.unesco.org/en/articles/east-atlantic-biosphere-reserves-representatives-participate-eleventh-redbios-meeting |title=East Atlantic Biosphere Reserves representatives participate in the eleventh REDBIOS meeting |publisher=UNESCO}}</ref> The network promotes cooperation and the exchange of knowledge and experience among biosphere reserves. Its activities focus on biodiversity conservation, the sustainable use of natural resources, environmental education, and sustainable development, particularly in island and coastal areas.
== History ==
The East Atlantic Biosphere Reserve Network (REDBIOS) was officially established in 1994 under the broader framework of the UNESCO Man and the Biosphere (MAB) Programme to foster comprehensive cooperation, biodiversity conservation, and sustainable development among various coastal and island regions located around the East Atlantic.<ref>{{Cite web |url=https://en.wikipedia.org/wiki/Man_and_the_Biosphere_Programme |title=Man and the Biosphere Programme |website=Wikipedia}}</ref> Over the years, the network has evolved significantly through a series of collaborative international assemblies. Most importantly, it showed its ongoing approval at the eleventh REDBIOS meeting, where representatives from participating member states came together to set strategic priorities and create joint explanation aimed at addressing contemporary environmental issues.<ref>{{Cite web |url=https://www.unesco.org/en/articles/declaration-eleventh-east-atlantic-biosphere-reserves-meeting |title=Declaration of the eleventh East Atlantic Biosphere Reserves meeting |publisher=UNESCO}}</ref><ref>{{Cite web |url=https://sdg.iisd.org/news/meeting-of-the-east-atlantic-biosphere-reserve-network-sets-priorities/ |title=Meeting of the East Atlantic Biosphere Reserve Network Sets Priorities |publisher=IISD}}</ref> These continuous cooperation efforts are ultimately aimed at strengthening the management capacities of individual biosphere reserves, as well as promoting comprehensive scientific research, environmental education and basic information exchange between the interconnected ecosystems of the respective African and European countries.<ref>{{Cite web |url=https://www.unesco.org/en/articles/east-atlantic-biosphere-reserves-representatives-participate-eleventh-redbios-meeting |title=East Atlantic Biosphere Reserves representatives participate in the eleventh REDBIOS meeting |publisher=UNESCO}}</ref>
== Geographic scope and membership ==
== Objectives and activities ==
The East Atlantic Biosphere Reserve Network (REDBIOS) promotes cooperation among biosphere reserves, biodiversity conservation, environmental education, and sustainable development in island and coastal areas. It supports these goals through international meetings, knowledge-sharing, and collaborative projects. For example, at its eleventh meeting in Príncipe, São Tomé and Príncipe, in May 2013, participants discussed strengthening cooperation and promoting responsible tourism and sustainable resource management.<ref>{{cite web |title=Declaration of the Eleventh East Atlantic Biosphere Reserves Meeting |url=https://www.unesco.org/en/articles/declaration-eleventh-east-atlantic-biosphere-reserves-meeting |publisher=UNESCO |access-date=22 October 2026}}</ref> In September 2014, the network held its twelfth meeting in the Azores, focusing on knowledge transfer and partnerships between biosphere reserves.<ref>{{cite web |title=REDBIOS Network, knowledge transfer and collaborative partnerships in the Macaronesian Region |url=https://www.unesco.org/en/articles/redbios-network-knowledge-transfer-and-collaborative-partnerships-macaronesian-region |publisher=UNESCO |access-date=22 October 2026}}</ref>
== Organization and cooperation ==
The REDBIOS network is coordinated through the collaboration of UNESCO's Man and the Biosphere (MAB) Secretariat and the Spanish Autonomous Body for National Parks (OAPN), with active involvement from national MAB committees and local biosphere reserve managers. <ref>{{Cite web |url=https://unesdoc.unesco.org/ark:/48223/pf0000123605 |title=UNESCO Digital Library Document |publisher=UNESCO |access-date=22 October 2026}}</ref>
Cooperation among member territories is maintained through regular regional meetings, thematic workshops, and technical exchange programmes hosted rotatively across member archipelagos and coastal areas.<ref>{{cite web |title=Exchanging experiences and transferring knowledge in Macaronesian and West African biosphere reserves |url=https://www.unesco.org/en/articles/exchanging-experiences-and-transferring-knowledge-macaronesian-and-west-african-biosphere-reserves |publisher=UNESCO |website=unesco.org}}</ref>
== See also ==
* [[Man and the Biosphere Programme]]
* [[World Network of Biosphere Reserves]]
* [[World Network of Biosphere Reserves in Africa]]
* [[World Network of Biosphere Reserves in Europe and North America]]
== References ==
{{reflist}}
ftengywsyj4d2pzdgg77ffj9cp2aoa6
769726
769721
2026-10-22T11:38:06Z
Batusmatus
76338
769726
wikitext
text/x-wiki
[[File:Man and the Biosphere Programme Logo.svg|thumb|Man and the Biosphere Programme Logo]]
== Introduction ==
The East Atlantic Biosphere Reserve Network (REDBIOS) is an interregional network established in 1994 within UNESCO's [[Man and the Biosphere Programme]] (MAB). It connects biosphere reserves in the Macaronesian region and parts of West Africa, including the Canary Islands (Spain), the Azores and Madeira (Portugal), Cape Verde, Guinea-Bissau, Mauritania, Morocco, Senegal, and São Tomé and Príncipe.<ref name= "11th Meeting" >{{Cite web |url=https://www.unesco.org/en/articles/east-atlantic-biosphere-reserves-representatives-participate-eleventh-redbios-meeting |title=East Atlantic Biosphere Reserves representatives participate in the eleventh REDBIOS meeting |publisher=UNESCO}}</ref> The network promotes cooperation and the exchange of knowledge and experience among biosphere reserves. Its activities focus on biodiversity conservation, the sustainable use of natural resources, environmental education, and sustainable development, particularly in island and coastal areas.
== History ==
The East Atlantic Biosphere Reserve Network (REDBIOS) was officially established in 1994 under the broader framework of the UNESCO Man and the Biosphere (MAB) Programme to foster comprehensive cooperation, biodiversity conservation, and sustainable development among various coastal and island regions located around the East Atlantic.<ref>{{Cite web |url=https://en.wikipedia.org/wiki/Man_and_the_Biosphere_Programme |title=Man and the Biosphere Programme |website=Wikipedia}}</ref> Over the years, the network has evolved significantly through a series of collaborative international assemblies. Most importantly, it showed its ongoing approval at the eleventh REDBIOS meeting, where representatives from participating member states came together to set strategic priorities and create joint explanation aimed at addressing contemporary environmental issues.<ref>{{Cite web |url=https://www.unesco.org/en/articles/declaration-eleventh-east-atlantic-biosphere-reserves-meeting |title=Declaration of the eleventh East Atlantic Biosphere Reserves meeting |publisher=UNESCO}}</ref><ref>{{Cite web |url=https://sdg.iisd.org/news/meeting-of-the-east-atlantic-biosphere-reserve-network-sets-priorities/ |title=Meeting of the East Atlantic Biosphere Reserve Network Sets Priorities |publisher=IISD}}</ref> These continuous cooperation efforts are ultimately aimed at strengthening the management capacities of individual biosphere reserves, as well as promoting comprehensive scientific research, environmental education and basic information exchange between the interconnected ecosystems of the respective African and European countries.<ref name= "11th Meeting" >
== Geographic scope and membership ==
== Objectives and activities ==
The East Atlantic Biosphere Reserve Network (REDBIOS) promotes cooperation among biosphere reserves, biodiversity conservation, environmental education, and sustainable development in island and coastal areas. It supports these goals through international meetings, knowledge-sharing, and collaborative projects. For example, at its eleventh meeting in Príncipe, São Tomé and Príncipe, in May 2013, participants discussed strengthening cooperation and promoting responsible tourism and sustainable resource management.<ref>{{cite web |title=Declaration of the Eleventh East Atlantic Biosphere Reserves Meeting |url=https://www.unesco.org/en/articles/declaration-eleventh-east-atlantic-biosphere-reserves-meeting |publisher=UNESCO |access-date=22 October 2026}}</ref> In September 2014, the network held its twelfth meeting in the Azores, focusing on knowledge transfer and partnerships between biosphere reserves.<ref>{{cite web |title=REDBIOS Network, knowledge transfer and collaborative partnerships in the Macaronesian Region |url=https://www.unesco.org/en/articles/redbios-network-knowledge-transfer-and-collaborative-partnerships-macaronesian-region |publisher=UNESCO |access-date=22 October 2026}}</ref>
== Organization and cooperation ==
The REDBIOS network is coordinated through the collaboration of UNESCO's Man and the Biosphere (MAB) Secretariat and the Spanish Autonomous Body for National Parks (OAPN), with active involvement from national MAB committees and local biosphere reserve managers. <ref>{{Cite web |url=https://unesdoc.unesco.org/ark:/48223/pf0000123605 |title=UNESCO Digital Library Document |publisher=UNESCO |access-date=22 October 2026}}</ref>
Cooperation among member territories is maintained through regular regional meetings, thematic workshops, and technical exchange programmes hosted rotatively across member archipelagos and coastal areas.<ref>{{cite web |title=Exchanging experiences and transferring knowledge in Macaronesian and West African biosphere reserves |url=https://www.unesco.org/en/articles/exchanging-experiences-and-transferring-knowledge-macaronesian-and-west-african-biosphere-reserves |publisher=UNESCO |website=unesco.org}}</ref>
== See also ==
* [[Man and the Biosphere Programme]]
* [[World Network of Biosphere Reserves]]
* [[World Network of Biosphere Reserves in Africa]]
* [[World Network of Biosphere Reserves in Europe and North America]]
== References ==
{{reflist}}
nrqbt2nkjj4539ukkbc593d8zb0aeis
769728
769726
2026-10-22T11:39:49Z
Batusmatus
76338
769728
wikitext
text/x-wiki
[[File:Man and the Biosphere Programme Logo.svg|thumb|Man and the Biosphere Programme Logo]]
== Introduction ==
The East Atlantic Biosphere Reserve Network (REDBIOS) is an interregional network established in 1994 within UNESCO's [[Man and the Biosphere Programme]] (MAB). It connects biosphere reserves in the Macaronesian region and parts of West Africa, including the Canary Islands (Spain), the Azores and Madeira (Portugal), Cape Verde, Guinea-Bissau, Mauritania, Morocco, Senegal, and São Tomé and Príncipe.<ref name= "11th Meeting" >{{Cite web |url=https://www.unesco.org/en/articles/east-atlantic-biosphere-reserves-representatives-participate-eleventh-redbios-meeting |title=East Atlantic Biosphere Reserves representatives participate in the eleventh REDBIOS meeting |publisher=UNESCO}}</ref> The network promotes cooperation and the exchange of knowledge and experience among biosphere reserves. Its activities focus on biodiversity conservation, the sustainable use of natural resources, environmental education, and sustainable development, particularly in island and coastal areas.
== History ==
The East Atlantic Biosphere Reserve Network (REDBIOS) was officially established in 1994 under the broader framework of the UNESCO Man and the Biosphere (MAB) Programme to foster comprehensive cooperation, biodiversity conservation, and sustainable development among various coastal and island regions located around the East Atlantic.<ref>{{Cite web |url=https://en.wikipedia.org/wiki/Man_and_the_Biosphere_Programme |title=Man and the Biosphere Programme |website=Wikipedia}}</ref> Over the years, the network has evolved significantly through a series of collaborative international assemblies. Most importantly, it showed its ongoing approval at the eleventh REDBIOS meeting, where representatives from participating member states came together to set strategic priorities and create joint explanation aimed at addressing contemporary environmental issues.<ref>{{Cite web |url=https://www.unesco.org/en/articles/declaration-eleventh-east-atlantic-biosphere-reserves-meeting |title=Declaration of the eleventh East Atlantic Biosphere Reserves meeting |publisher=UNESCO}}</ref><ref>{{Cite web |url=https://sdg.iisd.org/news/meeting-of-the-east-atlantic-biosphere-reserve-network-sets-priorities/ |title=Meeting of the East Atlantic Biosphere Reserve Network Sets Priorities |publisher=IISD}}</ref> These continuous cooperation efforts are ultimately aimed at strengthening the management capacities of individual biosphere reserves, as well as promoting comprehensive scientific research, environmental education and basic information exchange between the interconnected ecosystems of the respective African and European countries.<ref name= "11th Meeting" />
== Geographic scope and membership ==
== Objectives and activities ==
The East Atlantic Biosphere Reserve Network (REDBIOS) promotes cooperation among biosphere reserves, biodiversity conservation, environmental education, and sustainable development in island and coastal areas. It supports these goals through international meetings, knowledge-sharing, and collaborative projects. For example, at its eleventh meeting in Príncipe, São Tomé and Príncipe, in May 2013, participants discussed strengthening cooperation and promoting responsible tourism and sustainable resource management.<ref>{{cite web |title=Declaration of the Eleventh East Atlantic Biosphere Reserves Meeting |url=https://www.unesco.org/en/articles/declaration-eleventh-east-atlantic-biosphere-reserves-meeting |publisher=UNESCO |access-date=22 October 2026}}</ref> In September 2014, the network held its twelfth meeting in the Azores, focusing on knowledge transfer and partnerships between biosphere reserves.<ref>{{cite web |title=REDBIOS Network, knowledge transfer and collaborative partnerships in the Macaronesian Region |url=https://www.unesco.org/en/articles/redbios-network-knowledge-transfer-and-collaborative-partnerships-macaronesian-region |publisher=UNESCO |access-date=22 October 2026}}</ref>
== Organization and cooperation ==
The REDBIOS network is coordinated through the collaboration of UNESCO's Man and the Biosphere (MAB) Secretariat and the Spanish Autonomous Body for National Parks (OAPN), with active involvement from national MAB committees and local biosphere reserve managers. <ref>{{Cite web |url=https://unesdoc.unesco.org/ark:/48223/pf0000123605 |title=UNESCO Digital Library Document |publisher=UNESCO |access-date=22 October 2026}}</ref>
Cooperation among member territories is maintained through regular regional meetings, thematic workshops, and technical exchange programmes hosted rotatively across member archipelagos and coastal areas.<ref>{{cite web |title=Exchanging experiences and transferring knowledge in Macaronesian and West African biosphere reserves |url=https://www.unesco.org/en/articles/exchanging-experiences-and-transferring-knowledge-macaronesian-and-west-african-biosphere-reserves |publisher=UNESCO |website=unesco.org}}</ref>
== See also ==
* [[Man and the Biosphere Programme]]
* [[World Network of Biosphere Reserves]]
* [[World Network of Biosphere Reserves in Africa]]
* [[World Network of Biosphere Reserves in Europe and North America]]
== References ==
{{reflist}}
jm15847o6cq3kzhpeqfkigltj1jjopg
The South and Central Asia MAB Network (SACAM)
0
179384
769622
769416
2026-10-22T10:13:40Z
Shinybella
76334
769622
wikitext
text/x-wiki
= The South and Central Asia MAB Network =
=== '''Introduction''' ===
The South and Central Asia MAB Network (SACAM)<ref>{{Cite web |first=Unesco |date=19.01.26 |title=Unesco, Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks |url-status=live |access-date={{subst:DATE}} |website=Unesco}}</ref> was established in 2002. It includes ten countries: Afghanistan, Bangladesh, Bhutan, India, Iran, Kazakhstan, Maldives, Nepal, Pakistan and Sri Lanka. It helps countries in South and Central Asia work together to protect biodiversity, manage biosphere reserves, and support sustainable development. The network allows countries to share information and experience about nature protection and environmental research. It also focuses on issues such as climate change, land damage, and the use of natural resources.
==== History ====
The history of SACAM began in February 2001, when MAB representatives gathered in the context of the "Regional Meeting of MAB National Committees and Biosphere Reserve Coordinators in South and Central Asia," held in Dehra Dun (India). <ref>{{Cite web |title=About network SACAM |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/sacam/about-network-sacam |website=www.kazmab.kz}}</ref>
38jonlv4wbcjl7b770lqxw8zr02uwj5
769678
769622
2026-10-22T10:48:42Z
Shinybella
76334
769678
wikitext
text/x-wiki
{{speedy|Duplicate test article created by mistake for a university group assignment. We are keeping one shared SACAM article and no longer need this page.}}
tvoh8y1gankl2sjm4nrloin2shuoqq0
South and Central Asia MAB Network
0
179385
769621
769419
2026-10-22T10:13:33Z
Liliyi
76340
769621
wikitext
text/x-wiki
'''Objectives and activities'''
SACAM aims to protect biodiversity and support sustainable development in South and Central Asia. It helps countries share information, research and experience in managing biosphere reserves. The network also organizes meetings and workshops. In November 2023, SACAM held its tenth regional meeting in Chennai, India to discuss environmental protection and cooperation. <ref>{{Cite web |title=10th South and Central Asian Biosphere Reserve Network Meeting |url=http://www.decadeonrestoration.org/events/10th-south-and-central-asian-biosphere-reserve-network-meeting |website=UN Decade on Restoration |language=en}}</ref>
'''Organization and cooperation'''
SACAM is coordinated by UNESCO New Delhi Office, together with UNESCO's MAB Secretariat and national MAB committees. Member countries work together by sharing information supporting research on biosphere reserves. The network also encourages cooperation between countries that share natural areas, such as the Sundarbans in Bangladesh and India.<ref>{{Cite web |title=About network SACAM |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/sacam/about-network-sacam |website=www.kazmab.kz}}</ref>
<references />
fwll70hkvet8dbkgzue68nx07fqfty0
769624
769621
2026-10-22T10:16:24Z
Shinybella
76334
769624
wikitext
text/x-wiki
=== '''Introduction''' ===
The South and Central Asia MAB Network (SACAM)<ref>{{Cite web |first=Unesco |date=19.01.26 |title=Unesco, Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks |url-status=live |access-date={{subst:DATE}} |website=Unesco}}</ref> was established in 2002. It includes ten countries: Afghanistan, Bangladesh, Bhutan, India, Iran, Kazakhstan, Maldives, Nepal, Pakistan and Sri Lanka. It helps countries in South and Central Asia work together to protect biodiversity, manage biosphere reserves, and support sustainable development. The network allows countries to share information and experience about nature protection and environmental research. It also focuses on issues such as climate change, land damage, and the use of natural resources.
==== History ====
The history of SACAM began in February 2001, when MAB representatives gathered in the context of the "Regional Meeting of MAB National Committees and Biosphere Reserve Coordinators in South and Central Asia," held in Dehra Dun (India). <ref>{{Cite web |title=About network SACAM |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/sacam/about-network-sacam |website=www.kazmab.kz}}</ref>
'''Objectives and activities'''
SACAM aims to protect biodiversity and support sustainable development in South and Central Asia. It helps countries share information, research and experience in managing biosphere reserves. The network also organizes meetings and workshops. In November 2023, SACAM held its tenth regional meeting in Chennai, India to discuss environmental protection and cooperation. <ref>{{Cite web |title=10th South and Central Asian Biosphere Reserve Network Meeting |url=http://www.decadeonrestoration.org/events/10th-south-and-central-asian-biosphere-reserve-network-meeting |website=UN Decade on Restoration |language=en}}</ref>
'''Organization and cooperation'''
SACAM is coordinated by UNESCO New Delhi Office, together with UNESCO's MAB Secretariat and national MAB committees. Member countries work together by sharing information supporting research on biosphere reserves. The network also encourages cooperation between countries that share natural areas, such as the Sundarbans in Bangladesh and India.<ref>{{Cite web |title=About network SACAM |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/sacam/about-network-sacam |website=www.kazmab.kz}}</ref>
<references />
gbh9fn6fql2a2zwbe581ttxtpq64n19
769626
769624
2026-10-22T10:17:12Z
Shinybella
76334
769626
wikitext
text/x-wiki
==== Introduction ====
The South and Central Asia MAB Network (SACAM)<ref>{{Cite web |first=Unesco |date=19.01.26 |title=Unesco, Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks |url-status=live |access-date={{subst:DATE}} |website=Unesco}}</ref> was established in 2002. It includes ten countries: Afghanistan, Bangladesh, Bhutan, India, Iran, Kazakhstan, Maldives, Nepal, Pakistan and Sri Lanka. It helps countries in South and Central Asia work together to protect biodiversity, manage biosphere reserves, and support sustainable development. The network allows countries to share information and experience about nature protection and environmental research. It also focuses on issues such as climate change, land damage, and the use of natural resources.
==== History ====
The history of SACAM began in February 2001, when MAB representatives gathered in the context of the "Regional Meeting of MAB National Committees and Biosphere Reserve Coordinators in South and Central Asia," held in Dehra Dun (India). <ref>{{Cite web |title=About network SACAM |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/sacam/about-network-sacam |website=www.kazmab.kz}}</ref>
'''Objectives and activities'''
SACAM aims to protect biodiversity and support sustainable development in South and Central Asia. It helps countries share information, research and experience in managing biosphere reserves. The network also organizes meetings and workshops. In November 2023, SACAM held its tenth regional meeting in Chennai, India to discuss environmental protection and cooperation. <ref>{{Cite web |title=10th South and Central Asian Biosphere Reserve Network Meeting |url=http://www.decadeonrestoration.org/events/10th-south-and-central-asian-biosphere-reserve-network-meeting |website=UN Decade on Restoration |language=en}}</ref>
'''Organization and cooperation'''
SACAM is coordinated by UNESCO New Delhi Office, together with UNESCO's MAB Secretariat and national MAB committees. Member countries work together by sharing information supporting research on biosphere reserves. The network also encourages cooperation between countries that share natural areas, such as the Sundarbans in Bangladesh and India.<ref>{{Cite web |title=About network SACAM |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/sacam/about-network-sacam |website=www.kazmab.kz}}</ref>
<references />
szoco1ce46mzhi92mqciedt63xejkrl
769630
769626
2026-10-22T10:22:38Z
Shinybella
76334
769630
wikitext
text/x-wiki
==== Introduction ====
The South and Central Asia MAB Network (SACAM)<ref>{{cite web |url=https://www.unesco.org |title=Unesco, Regional and Thematic Networks |publisher=Unesco |date=2026-01-19 |access-date=2026-01-19}}</ref> was established in 2002. It includes ten countries: Afghanistan, Bangladesh, Bhutan, India, Iran, Kazakhstan, Maldives, Nepal, Pakistan and Sri Lanka. It helps countries in South and Central Asia work together to protect biodiversity, manage biosphere reserves, and support sustainable development. The network allows countries to share information and experience about nature protection and environmental research. It also focuses on issues such as climate change, land damage, and the use of natural resources.
==== History ====
The history of SACAM began in February 2001, when MAB representatives gathered in the context of the "Regional Meeting of MAB National Committees and Biosphere Reserve Coordinators in South and Central Asia," held in Dehra Dun (India). <ref>{{Cite web |title=About network SACAM |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/sacam/about-network-sacam |website=www.kazmab.kz}}</ref>
'''Objectives and activities'''
SACAM aims to protect biodiversity and support sustainable development in South and Central Asia. It helps countries share information, research and experience in managing biosphere reserves. The network also organizes meetings and workshops. In November 2023, SACAM held its tenth regional meeting in Chennai, India to discuss environmental protection and cooperation. <ref>{{Cite web |title=10th South and Central Asian Biosphere Reserve Network Meeting |url=http://www.decadeonrestoration.org/events/10th-south-and-central-asian-biosphere-reserve-network-meeting |website=UN Decade on Restoration |language=en}}</ref>
'''Organization and cooperation'''
SACAM is coordinated by UNESCO New Delhi Office, together with UNESCO's MAB Secretariat and national MAB committees. Member countries work together by sharing information supporting research on biosphere reserves. The network also encourages cooperation between countries that share natural areas, such as the Sundarbans in Bangladesh and India.<ref>{{Cite web |title=About network SACAM |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/sacam/about-network-sacam |website=www.kazmab.kz}}</ref>
<references />
b4vjwwf3sl6k8b2rgmay3zrpyg17zjj
769631
769630
2026-10-22T10:22:49Z
Liliyi
76340
769631
wikitext
text/x-wiki
==== Introduction ====
The South and Central Asia MAB Network (SACAM)<ref>{{cite web |url=https://www.unesco.org |title=Unesco, Regional and Thematic Networks |publisher=Unesco |date=2026-01-19 |access-date=2026-01-19}}</ref> was established in 2002. It includes ten countries: Afghanistan, Bangladesh, Bhutan, India, Iran, Kazakhstan, Maldives, Nepal, Pakistan and Sri Lanka. It helps countries in South and Central Asia work together to protect biodiversity, manage biosphere reserves, and support sustainable development. The network allows countries to share information and experience about nature protection and environmental research. It also focuses on issues such as climate change, land damage, and the use of natural resources.
==== History ====
The history of SACAM began in February 2001, when MAB representatives gathered in the context of the "Regional Meeting of MAB National Committees and Biosphere Reserve Coordinators in South and Central Asia," held in Dehra Dun (India). <ref name="Website-1">{{Cite web |title=About network SACAM |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/sacam/about-network-sacam |website=www.kazmab.kz}}</ref>
'''Objectives and activities'''
SACAM aims to protect biodiversity and support sustainable development in South and Central Asia. It helps countries share information, research and experience in managing biosphere reserves. The network also organizes meetings and workshops. In November 2023, SACAM held its tenth regional meeting in Chennai, India to discuss environmental protection and cooperation. <ref>{{Cite web |title=10th South and Central Asian Biosphere Reserve Network Meeting |url=http://www.decadeonrestoration.org/events/10th-south-and-central-asian-biosphere-reserve-network-meeting |website=UN Decade on Restoration |language=en}}</ref>
'''Organization and cooperation'''
SACAM is coordinated by UNESCO New Delhi Office, together with UNESCO's MAB Secretariat and national MAB committees. Member countries work together by sharing information supporting research on biosphere reserves. The network also encourages cooperation between countries that share natural areas, such as the Sundarbans in Bangladesh and India.<ref name="Website-1" />
<references />
09fxyhjekz0765cl2bjsf3bvpxe1ede
769635
769631
2026-10-22T10:25:44Z
Liliyi
76340
769635
wikitext
text/x-wiki
==== Introduction ====
The South and Central Asia MAB Network (SACAM)<ref>{{cite web |url=https://www.unesco.org |title=Unesco, Regional and Thematic Networks |publisher=Unesco |date=2026-01-19 |access-date=2026-01-19}}</ref> was established in 2002. It includes ten countries: Afghanistan, Bangladesh, Bhutan, India, Iran, Kazakhstan, Maldives, Nepal, Pakistan and Sri Lanka. It helps countries in South and Central Asia work together to protect biodiversity, manage biosphere reserves, and support sustainable development. The network allows countries to share information and experience about nature protection and environmental research. It also focuses on issues such as climate change, land damage, and the use of natural resources.
==== History ====
The history of SACAM began in February 2001, when MAB representatives gathered in the context of the "Regional Meeting of MAB National Committees and Biosphere Reserve Coordinators in South and Central Asia," held in Dehra Dun (India).<ref name="Website-1">{{Cite web |title=About network SACAM |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/sacam/about-network-sacam |website=www.kazmab.kz}}</ref>
'''Objectives and activities'''
SACAM aims to protect biodiversity and support sustainable development in South and Central Asia. It helps countries share information, research and experience in managing biosphere reserves. The network also organizes meetings and workshops. In November 2023, SACAM held its tenth regional meeting in Chennai, India to discuss environmental protection and cooperation. <ref>{{Cite web |title=10th South and Central Asian Biosphere Reserve Network Meeting |url=http://www.decadeonrestoration.org/events/10th-south-and-central-asian-biosphere-reserve-network-meeting |website=UN Decade on Restoration |language=en}}</ref>
'''Organization and cooperation'''
SACAM is coordinated by UNESCO New Delhi Office, together with UNESCO's MAB Secretariat and national MAB committees. Member countries work together by sharing information supporting research on biosphere reserves. The network also encourages cooperation between countries that share natural areas, such as the Sundarbans in Bangladesh and India.<ref name="Website-1" />
<references />
5zpcggyziv8rp12j6wehxds5k3jp1tm
769645
769635
2026-10-22T10:30:18Z
Shinybella
76334
769645
wikitext
text/x-wiki
==== Introduction ====
The South and Central Asia MAB Network (SACAM)<ref>{{cite web |url=https://www.unesco.org |title=Unesco, Regional and Thematic Networks |publisher=Unesco |date=2026-01-19 |access-date=2026-01-19}}</ref> was established in 2002. It includes ten countries: Afghanistan, Bangladesh, Bhutan, India, Iran, Kazakhstan, Maldives, Nepal, Pakistan and Sri Lanka. It helps countries in South and Central Asia work together to protect biodiversity, manage biosphere reserves, and support sustainable development. The network allows countries to share information and experience about nature protection and environmental research. It also focuses on issues such as climate change, land damage, and the use of natural resources.
==== History ====
The history of SACAM began in February 2001, when MAB representatives gathered in the context of the "Regional Meeting of MAB National Committees and Biosphere Reserve Coordinators in South and Central Asia," held in Dehra Dun (India).<ref name="Website-1">{{Cite web |title=About network SACAM |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/sacam/about-network-sacam |website=www.kazmab.kz}}</ref> The network held several meetings over the following years. In 2004, the meeting in Iran focused on sustainable tourism in biosphere reserves. In 2007, the meeting in India focused on the sustainable management of drylands, and Afghanistan joined the network. Kazakhstan became a member in 2011.<ref>{{cite web |title=About Network SACAM and the Network's Statutes |url=https://sacam-mab.com/ |publisher=South and Central Asia MAB Network |access-date=22 October 2026}}</ref>
In 2013, SACAM held meetings in Pakistan and Iran. These meetings focused on biodiversity, local people's livelihoods, and the membership of Central Asian countries. The sixth meeting in Pakistan also discussed the effects of climate change on natural ecosystems.<ref>{{cite web |title=About Network SACAM and the Network's Statutes |url=https://sacam-mab.com/ |publisher=South and Central Asia MAB Network |access-date=22 October 2026}}</ref>
In October 2016, SACAM held its eighth meeting in Almaty, Kazakhstan. The members approved changes to the network's rules, and the Kyrgyz Republic, Tajikistan, and Uzbekistan joined the network.<ref>{{cite web |title=8th Meeting of the South and Central Asia MAB Network (SACAM) |url=https://www.eco4science.org/sacam-2 |publisher=ECO Science Foundation |access-date=22 October 2026}}</ref><ref>{{cite web |title=About Network SACAM and the Network's Statutes |url=https://sacam-mab.com/ |publisher=South and Central Asia MAB Network |access-date=22 October 2026}}</ref>
'''Objectives and activities'''
SACAM aims to protect biodiversity and support sustainable development in South and Central Asia. It helps countries share information, research and experience in managing biosphere reserves. The network also organizes meetings and workshops. In November 2023, SACAM held its tenth regional meeting in Chennai, India to discuss environmental protection and cooperation. <ref>{{Cite web |title=10th South and Central Asian Biosphere Reserve Network Meeting |url=http://www.decadeonrestoration.org/events/10th-south-and-central-asian-biosphere-reserve-network-meeting |website=UN Decade on Restoration |language=en}}</ref>
'''Organization and cooperation'''
SACAM is coordinated by UNESCO New Delhi Office, together with UNESCO's MAB Secretariat and national MAB committees. Member countries work together by sharing information supporting research on biosphere reserves. The network also encourages cooperation between countries that share natural areas, such as the Sundarbans in Bangladesh and India.<ref name="Website-1" />
<references />
i3m1a9emrqzrqpcyr78alkgtp4yuf64
769653
769645
2026-10-22T10:34:04Z
Shinybella
76334
769653
wikitext
text/x-wiki
==== Introduction ====
The South and Central Asia MAB Network (SACAM)<ref>{{cite web |url=https://www.unesco.org |title=Unesco, Regional and Thematic Networks |publisher=Unesco |date=2026-01-19 |access-date=2026-01-19}}</ref> was established in 2002. It includes ten countries: Afghanistan, Bangladesh, Bhutan, India, Iran, Kazakhstan, Maldives, Nepal, Pakistan and Sri Lanka. It helps countries in South and Central Asia work together to protect biodiversity, manage biosphere reserves, and support sustainable development. The network allows countries to share information and experience about nature protection and environmental research. It also focuses on issues such as climate change, land damage, and the use of natural resources.
==== History ====
The history of SACAM began in February 2001, when MAB representatives met in Dehra Dun, India, to discuss cooperation between countries in South and Central Asia.<ref>{{Cite web |title=About Network SACAM |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/sacam/about-network-sacam |website=KazMAB}}</ref> SACAM was officially established in 2002. In 2004, its meeting in Iran focused on sustainable tourism, while the 2007 meeting in India focused on dryland management. Afghanistan joined in 2007, and Kazakhstan joined in 2011. In 2013, meetings focused on biodiversity, local people's livelihoods, and climate change. In October 2016, SACAM held its eighth meeting in Almaty, Kazakhstan. The members approved changes to the network's rules, and the Kyrgyz Republic, Tajikistan, and Uzbekistan joined the network.<ref>{{Cite web |title=About Network SACAM and the Network's Statutes |url=https://sacam-mab.com/ |website=South and Central Asia MAB Network}}</ref>
'''Objectives and activities'''
SACAM aims to protect biodiversity and support sustainable development in South and Central Asia. It helps countries share information, research and experience in managing biosphere reserves. The network also organizes meetings and workshops. In November 2023, SACAM held its tenth regional meeting in Chennai, India to discuss environmental protection and cooperation. <ref>{{Cite web |title=10th South and Central Asian Biosphere Reserve Network Meeting |url=http://www.decadeonrestoration.org/events/10th-south-and-central-asian-biosphere-reserve-network-meeting |website=UN Decade on Restoration |language=en}}</ref>
'''Organization and cooperation'''
SACAM is coordinated by UNESCO New Delhi Office, together with UNESCO's MAB Secretariat and national MAB committees. Member countries work together by sharing information supporting research on biosphere reserves. The network also encourages cooperation between countries that share natural areas, such as the Sundarbans in Bangladesh and India.<ref name="Website-1" />
<references />
137z9xiwm117bxrysjucfjgo7463n7d
769658
769653
2026-10-22T10:35:04Z
Liliyi
76340
769658
wikitext
text/x-wiki
==== Introduction ====
The South and Central Asia MAB Network (SACAM)<ref>{{cite web |url=https://www.unesco.org |title=Unesco, Regional and Thematic Networks |publisher=Unesco |date=2026-01-19 |access-date=2026-01-19}}</ref> was established in 2002. It includes ten countries: Afghanistan, Bangladesh, Bhutan, India, Iran, Kazakhstan, Maldives, Nepal, Pakistan and Sri Lanka. It helps countries in South and Central Asia work together to protect biodiversity, manage biosphere reserves, and support sustainable development. The network allows countries to share information and experience about nature protection and environmental research. It also focuses on issues such as climate change, land damage, and the use of natural resources.
==== History ====
The history of SACAM began in February 2001, when MAB representatives met in Dehra Dun, India, to discuss cooperation between countries in South and Central Asia.<ref name="Website-1">{{Cite web |title=About Network SACAM |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/sacam/about-network-sacam |website=KazMAB}}</ref> SACAM was officially established in 2002. In 2004, its meeting in Iran focused on sustainable tourism, while the 2007 meeting in India focused on dryland management. Afghanistan joined in 2007, and Kazakhstan joined in 2011. In 2013, meetings focused on biodiversity, local people's livelihoods, and climate change. In October 2016, SACAM held its eighth meeting in Almaty, Kazakhstan. The members approved changes to the network's rules, and the Kyrgyz Republic, Tajikistan, and Uzbekistan joined the network.<ref>{{Cite web |title=About Network SACAM and the Network's Statutes |url=https://sacam-mab.com/ |website=South and Central Asia MAB Network}}</ref>
'''Objectives and activities'''
SACAM aims to protect biodiversity and support sustainable development in South and Central Asia. It helps countries share information, research and experience in managing biosphere reserves. The network also organizes meetings and workshops. In November 2023, SACAM held its tenth regional meeting in Chennai, India to discuss environmental protection and cooperation. <ref>{{Cite web |title=10th South and Central Asian Biosphere Reserve Network Meeting |url=http://www.decadeonrestoration.org/events/10th-south-and-central-asian-biosphere-reserve-network-meeting |website=UN Decade on Restoration |language=en}}</ref>
'''Organization and cooperation'''
SACAM is coordinated by UNESCO New Delhi Office, together with UNESCO's MAB Secretariat and national MAB committees. Member countries work together by sharing information supporting research on biosphere reserves. The network also encourages cooperation between countries that share natural areas, such as the Sundarbans in Bangladesh and India.<ref name="Website-1" />
<references />
kcyj1a4eu70xzglodblsa1zal2p9jac
769661
769658
2026-10-22T10:36:50Z
Shinybella
76334
/* History */
769661
wikitext
text/x-wiki
==== Introduction ====
The South and Central Asia MAB Network (SACAM)<ref>{{cite web |url=https://www.unesco.org |title=Unesco, Regional and Thematic Networks |publisher=Unesco |date=2026-01-19 |access-date=2026-01-19}}</ref> was established in 2002. It includes ten countries: Afghanistan, Bangladesh, Bhutan, India, Iran, Kazakhstan, Maldives, Nepal, Pakistan and Sri Lanka. It helps countries in South and Central Asia work together to protect biodiversity, manage biosphere reserves, and support sustainable development. The network allows countries to share information and experience about nature protection and environmental research. It also focuses on issues such as climate change, land damage, and the use of natural resources.
==== History ====
The history of SACAM began in February 2001, when MAB representatives met in Dehra Dun, India, to discuss cooperation between countries in South and Central Asia.<ref name="Website-1">{{Cite web |title=About Network SACAM |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/sacam/about-network-sacam |website=KazMAB}}</ref> SACAM was officially established in 2002. In 2004, its meeting in Iran focused on sustainable tourism, while the 2007 meeting in India focused on dryland management. Afghanistan joined in 2007, and Kazakhstan joined in 2011. In 2013, meetings focused on biodiversity, local people's livelihoods, and climate change. In October 2016, SACAM held its eighth meeting in Almaty, Kazakhstan. The members approved changes to the network's rules, and the Kyrgyz Republic, Tajikistan, and Uzbekistan joined the network. <ref>{{Cite web
|title=About network SACAM
|url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/sacam/about-network-sacam
|website=Kazakhstan National Committee for the UNESCO Programme Man and the Biosphere
|access-date=22 October 2026
}}</ref>
'''Objectives and activities'''
SACAM aims to protect biodiversity and support sustainable development in South and Central Asia. It helps countries share information, research and experience in managing biosphere reserves. The network also organizes meetings and workshops. In November 2023, SACAM held its tenth regional meeting in Chennai, India to discuss environmental protection and cooperation. <ref>{{Cite web |title=10th South and Central Asian Biosphere Reserve Network Meeting |url=http://www.decadeonrestoration.org/events/10th-south-and-central-asian-biosphere-reserve-network-meeting |website=UN Decade on Restoration |language=en}}</ref>
'''Organization and cooperation'''
SACAM is coordinated by UNESCO New Delhi Office, together with UNESCO's MAB Secretariat and national MAB committees. Member countries work together by sharing information supporting research on biosphere reserves. The network also encourages cooperation between countries that share natural areas, such as the Sundarbans in Bangladesh and India.<ref name="Website-1" />
<references />
9vzo06hhemevp889rdm3fkn19o3806t
769665
769661
2026-10-22T10:38:47Z
Liliyi
76340
769665
wikitext
text/x-wiki
==== Introduction ====
The South and Central Asia MAB Network (SACAM)<ref>{{cite web |url=https://www.unesco.org |title=Unesco, Regional and Thematic Networks |publisher=Unesco |date=2026-01-19 |access-date=2026-01-19}}</ref> was established in 2002. It includes ten countries: Afghanistan, Bangladesh, Bhutan, India, Iran, Kazakhstan, Maldives, Nepal, Pakistan and Sri Lanka. It helps countries in South and Central Asia work together to protect biodiversity, manage biosphere reserves, and support sustainable development. The network allows countries to share information and experience about nature protection and environmental research. It also focuses on issues such as climate change, land damage, and the use of natural resources.
==== History ====
The history of SACAM began in February 2001, when MAB representatives met in Dehra Dun, India, to discuss cooperation between countries in South and Central Asia.<ref name="Website-1">{{Cite web |title=About Network SACAM |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/sacam/about-network-sacam |website=KazMAB}}</ref> SACAM was officially established in 2002. In 2004, its meeting in Iran focused on sustainable tourism, while the 2007 meeting in India focused on dryland management. Afghanistan joined in 2007, and Kazakhstan joined in 2011. In 2013, meetings focused on biodiversity, local people's livelihoods, and climate change. In October 2016, SACAM held its eighth meeting in Almaty, Kazakhstan. The members approved changes to the network's rules, and the Kyrgyz Republic, Tajikistan, and Uzbekistan joined the network. <ref name="Website-1" />
'''Objectives and activities'''
SACAM aims to protect biodiversity and support sustainable development in South and Central Asia. It helps countries share information, research and experience in managing biosphere reserves. The network also organizes meetings and workshops. In November 2023, SACAM held its tenth regional meeting in Chennai, India to discuss environmental protection and cooperation. <ref>{{Cite web |title=10th South and Central Asian Biosphere Reserve Network Meeting |url=http://www.decadeonrestoration.org/events/10th-south-and-central-asian-biosphere-reserve-network-meeting |website=UN Decade on Restoration |language=en}}</ref>
'''Organization and cooperation'''
SACAM is coordinated by UNESCO New Delhi Office, together with UNESCO's MAB Secretariat and national MAB committees. Member countries work together by sharing information supporting research on biosphere reserves. The network also encourages cooperation between countries that share natural areas, such as the Sundarbans in Bangladesh and India.<ref name="Website-1" />
<references />
hyek5v8ub04vf8gvhq0c6t09hxkk5h2
769668
769665
2026-10-22T10:41:16Z
Shinybella
76334
769668
wikitext
text/x-wiki
==== Introduction ====
The South and Central Asia MAB Network (SACAM)<ref>{{cite web |url=https://www.unesco.org |title=Unesco, Regional and Thematic Networks |publisher=Unesco |date=2026-01-19 |access-date=2026-01-19}}</ref> was established in 2002. It includes ten countries: Afghanistan, Bangladesh, Bhutan, India, Iran, Kazakhstan, Maldives, Nepal, Pakistan and Sri Lanka. It helps countries in South and Central Asia work together to protect biodiversity, manage biosphere reserves, and support sustainable development. The network allows countries to share information and experience about nature protection and environmental research. It also focuses on issues such as climate change, land damage, and the use of natural resources.
==== History ====
The history of SACAM began in February 2001, when MAB representatives met in Dehra Dun, India, to discuss cooperation between countries in South and Central Asia.<ref name="Website-1">{{Cite web |title=About Network SACAM |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/sacam/about-network-sacam |website=KazMAB}}</ref> SACAM was officially established in 2002. In 2004, its meeting in Iran focused on sustainable tourism, while the 2007 meeting in India focused on dryland management. Afghanistan joined in 2007, and Kazakhstan joined in 2011. In 2013, meetings focused on biodiversity, local people's livelihoods, and climate change. In October 2016, SACAM held its eighth meeting in Almaty, Kazakhstan. The members approved changes to the network's rules, and the Kyrgyz Republic, Tajikistan, and Uzbekistan joined the network. <ref>{{cite web |title=About Network SACAM and the Network's Statutes |url=https://sacam-mab.com/ |publisher=South and Central Asia MAB Network}}</ref><ref>{{cite web |title=8th Meeting of the South and Central Asia MAB Network (SACAM) |url=https://www.eco4science.org/sacam-2 |publisher=ECO Science Foundation}}</ref>
'''Objectives and activities'''
SACAM aims to protect biodiversity and support sustainable development in South and Central Asia. It helps countries share information, research and experience in managing biosphere reserves. The network also organizes meetings and workshops. In November 2023, SACAM held its tenth regional meeting in Chennai, India to discuss environmental protection and cooperation. <ref>{{Cite web |title=10th South and Central Asian Biosphere Reserve Network Meeting |url=http://www.decadeonrestoration.org/events/10th-south-and-central-asian-biosphere-reserve-network-meeting |website=UN Decade on Restoration |language=en}}</ref>
'''Organization and cooperation'''
SACAM is coordinated by UNESCO New Delhi Office, together with UNESCO's MAB Secretariat and national MAB committees. Member countries work together by sharing information supporting research on biosphere reserves. The network also encourages cooperation between countries that share natural areas, such as the Sundarbans in Bangladesh and India.<ref name="Website-1" />
<references />
hd3iwixmyoybhawxxi1swbog4c88ejv
769709
769668
2026-10-22T11:26:48Z
Liliyi
76340
769709
wikitext
text/x-wiki
==== Introduction ====
The South and Central Asia MAB Network (SACAM)<ref>{{cite web |url=https://www.unesco.org |title=Unesco, Regional and Thematic Networks |publisher=Unesco |date=2026-01-19 |access-date=2026-01-19}}</ref> was established in 2002. It includes ten countries: Afghanistan, Bangladesh, Bhutan, India, Iran, Kazakhstan, Maldives, Nepal, Pakistan and Sri Lanka. It helps countries in South and Central Asia work together to protect biodiversity, manage biosphere reserves, and support sustainable development. The network allows countries to share information and experience about nature protection and environmental research. It also focuses on issues such as climate change, land damage, and the use of natural resources.
==== History ====
The history of SACAM began in February 2001, when MAB representatives met in Dehra Dun, India, to discuss cooperation between countries in South and Central Asia.<ref name="Website-1">{{Cite web |title=About Network SACAM |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/sacam/about-network-sacam |website=KazMAB}}</ref> SACAM was officially established in 2002. In 2004, its meeting in Iran focused on sustainable tourism, while the 2007 meeting in India focused on dryland management. Afghanistan joined in 2007, and Kazakhstan joined in 2011. In 2013, meetings focused on biodiversity, local people's livelihoods, and climate change. In October 2016, SACAM held its eighth meeting in Almaty, Kazakhstan. The members approved changes to the network's rules, and the Kyrgyz Republic, Tajikistan, and Uzbekistan joined the network. <ref>{{cite web |title=About Network SACAM and the Network's Statutes |url=https://sacam-mab.com/ |publisher=South and Central Asia MAB Network}}</ref><ref>{{cite web |title=8th Meeting of the South and Central Asia MAB Network (SACAM) |url=https://www.eco4science.org/sacam-2 |publisher=ECO Science Foundation}}</ref>
'''Objectives and activities'''
SACAM aims to protect biodiversity and support sustainable development in South and Central Asia. It helps countries share information, research and experience in managing biosphere reserves. The network also organizes meetings and workshops. In November 2023, SACAM held its tenth regional meeting in Chennai, India to discuss environmental protection and cooperation. <ref>{{Cite web |title=10th South and Central Asian Biosphere Reserve Network Meeting |url=http://www.decadeonrestoration.org/events/10th-south-and-central-asian-biosphere-reserve-network-meeting |website=UN Decade on Restoration |language=en}}</ref>
'''Organization and cooperation'''
SACAM is coordinated by UNESCO New Delhi Office, together with UNESCO's MAB Secretariat and national MAB committees. Member countries work together by sharing information supporting research on biosphere reserves. The network also encourages cooperation between countries that share natural areas, such as the Sundarbans in Bangladesh and India.<ref name="Website-1" />
==== References ====
<references />
ifute3lc6wv8x7bgvtj1rwknphr7gwf
769715
769709
2026-10-22T11:29:46Z
Shinybella
76334
769715
wikitext
text/x-wiki
==== Introduction ====
The South and Central Asia MAB Network (SACAM)<ref>{{cite web |url=https://www.unesco.org |title=Unesco, Regional and Thematic Networks |publisher=Unesco |date=2026-01-19 |access-date=2026-01-19}}</ref> was established in 2002. It includes ten countries: Afghanistan, Bangladesh, Bhutan, India, Iran, Kazakhstan, Maldives, Nepal, Pakistan and Sri Lanka. It helps countries in South and Central Asia work together to protect biodiversity, manage biosphere reserves, and support sustainable development. The network allows countries to share information and experience about nature protection and environmental research. It also focuses on issues such as climate change, land damage, and the use of natural resources.
==== History ====
The history of SACAM began in February 2001, when MAB representatives met in Dehra Dun, India, to discuss cooperation between countries in South and Central Asia.<ref name="Website-1">{{Cite web |title=About Network SACAM |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/sacam/about-network-sacam |website=KazMAB}}</ref> SACAM was officially established in 2002. In 2004, its meeting in Iran focused on sustainable tourism, while the 2007 meeting in India focused on dryland management. Afghanistan joined in 2007, and Kazakhstan joined in 2011. In 2013, meetings focused on biodiversity, local people's livelihoods, and climate change. In October 2016, SACAM held its eighth meeting in Almaty, Kazakhstan. The members approved changes to the network's rules, and the Kyrgyz Republic, Tajikistan, and Uzbekistan joined the network. <ref>{{cite web |title=About Network SACAM and the Network's Statutes |url=https://sacam-mab.com/ |publisher=South and Central Asia MAB Network}}</ref><ref>{{cite web |title=8th Meeting of the South and Central Asia MAB Network (SACAM) |url=https://www.eco4science.org/sacam-2 |publisher=ECO Science Foundation}}</ref>
'''Objectives and activities'''
SACAM aims to protect biodiversity and support sustainable development in South and Central Asia. It helps countries share information, research and experience in managing biosphere reserves. The network also organizes meetings and workshops. In November 2023, SACAM held its tenth regional meeting in Chennai, India to discuss environmental protection and cooperation. <ref>{{Cite web |title=10th South and Central Asian Biosphere Reserve Network Meeting |url=http://www.decadeonrestoration.org/events/10th-south-and-central-asian-biosphere-reserve-network-meeting |website=UN Decade on Restoration |language=en}}</ref>
'''Organization and cooperation'''
SACAM is coordinated by [https://www.unesco.org/en UNESCO] New Delhi Office, together with UNESCO's MAB Secretariat and national MAB committees. Member countries work together by sharing information supporting research on biosphere reserves. The network also encourages cooperation between countries that share natural areas, such as the Sundarbans in Bangladesh and India.<ref name="Website-1" />
<references />
n03t5kujtsfk4jqa2llnbtzqf0zcmtp
769720
769715
2026-10-22T11:34:36Z
Liliyi
76340
769720
wikitext
text/x-wiki
==== Introduction ====
The South and Central Asia MAB Network (SACAM)<ref>{{cite web |url=https://www.unesco.org |title=Unesco, Regional and Thematic Networks |publisher=Unesco |date=2026-01-19 |access-date=2026-01-19}}</ref> was established in 2002. It includes ten countries: Afghanistan, Bangladesh, Bhutan, India, Iran, Kazakhstan, Maldives, Nepal, Pakistan and Sri Lanka. It helps countries in South and Central Asia work together to protect biodiversity, manage biosphere reserves, and support sustainable development. The network allows countries to share information and experience about nature protection and environmental research. It also focuses on issues such as climate change, land damage, and the use of natural resources.
==== History ====
The history of SACAM began in February 2001, when MAB representatives met in Dehra Dun, India, to discuss cooperation between countries in South and Central Asia.<ref name="Website-1">{{Cite web |title=About Network SACAM |url=https://www.kazmab.kz/index.php/en/international-relations/membership-in-regional-networks/sacam/about-network-sacam |website=KazMAB}}</ref> SACAM was officially established in 2002. In 2004, its meeting in Iran focused on sustainable tourism, while the 2007 meeting in India focused on dryland management. Afghanistan joined in 2007, and Kazakhstan joined in 2011. In 2013, meetings focused on biodiversity, local people's livelihoods, and climate change. In October 2016, SACAM held its eighth meeting in Almaty, Kazakhstan. The members approved changes to the network's rules, and the Kyrgyz Republic, Tajikistan, and Uzbekistan joined the network. <ref>{{cite web |title=About Network SACAM and the Network's Statutes |url=https://sacam-mab.com/ |publisher=South and Central Asia MAB Network}}</ref><ref>{{cite web |title=8th Meeting of the South and Central Asia MAB Network (SACAM) |url=https://www.eco4science.org/sacam-2 |publisher=ECO Science Foundation}}</ref>
'''Objectives and activities'''
SACAM aims to protect biodiversity and support sustainable development in South and Central Asia. It helps countries share information, research and experience in managing biosphere reserves. The network also organizes meetings and workshops. In November 2023, SACAM held its tenth regional meeting in Chennai, India to discuss environmental protection and cooperation. <ref>{{Cite web |title=10th South and Central Asian Biosphere Reserve Network Meeting |url=http://www.decadeonrestoration.org/events/10th-south-and-central-asian-biosphere-reserve-network-meeting |website=UN Decade on Restoration |language=en}}</ref>
'''Organization and cooperation'''
SACAM is coordinated by [https://www.unesco.org/en UNESCO] New Delhi Office, together with UNESCO's MAB Secretariat and national MAB committees. Member countries work together by sharing information supporting research on biosphere reserves. The network also encourages cooperation between countries that share natural areas, such as the Sundarbans in Bangladesh and India.<ref name="Website-1" />
==== References ====
<references />
c4xyac0fz9gbhoeubz33e59cksj0whz
User:Syunsyunminmin/Twinkle/Twinkle.js
2
179388
769453
2026-10-21T13:56:50Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 3680108: init
769453
javascript
text/javascript
/*
Twinkle: adapted for Japanese Wikipedia; upstream authors and Japanese adaptations are credited in UPSTREAM.md.
Upstream source: https://github.com/wikimedia-gadgets/twinkle/blob/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/src/twinkle.js
Upstream author history: https://github.com/wikimedia-gadgets/twinkle/commits/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/src/twinkle.js
License: CC BY-SA 3.0 Unported. See https://github.com/wikimedia-gadgets/twinkle/blob/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/LICENSE.txt
Japanese adaptation: Syunsyunminmin; includes later synchronization changes.
*/
/**
* +-------------------------------------------------------------------------+
* | === WARNING: GLOBAL GADGET FILE === |
* | Changes to this page affect many users. |
* | Please discuss changes at [[WT:TW]] before editing. |
* +-------------------------------------------------------------------------+
*
* Imported from github [https://github.com/wikimedia-gadgets/twinkle].
* All changes should be made in the repository, otherwise they will be lost.
*
* ----------
*
* This is AzaToth's Twinkle, the popular script sidekick for newbies, admins, and
* every Wikipedian in between. Visit [[WP:TW]] for more information.
*/
// <nowiki>
/* global Morebits */
(function() {
// Check if account is experienced enough to use Twinkle
if (!Morebits.userIsInGroup('autoconfirmed') && !Morebits.userIsInGroup('confirmed')) {
return;
}
const Twinkle = {};
window.Twinkle = Twinkle; // allow global access
Twinkle.initCallbacks = [];
/**
* Adds a callback to execute when Twinkle has loaded.
*
* @param {Function} func
* @param {string} [name] - name of module used to check if is disabled.
* If name is not given, module is loaded unconditionally.
*/
Twinkle.addInitCallback = function twinkleAddInitCallback(func, name) {
Twinkle.initCallbacks.push({ func: func, name: name });
};
Twinkle.defaultConfig = {};
/**
* This holds the default set of preferences used by Twinkle.
* It is important that all new preferences added here, especially admin-only ones, are also added to
* |Twinkle.config.sections| in twinkleconfig.js, so they are configurable via the Twinkle preferences panel.
* For help on the actual preferences, see the comments in twinkleconfig.js.
*/
Twinkle.defaultConfig = {
// General
userTalkPageMode: 'tab',
dialogLargeFont: false,
disabledModules: ['arv', 'warn', 'talkback', 'xfd', 'tag'],
disabledSysopModules: [],
// ARV
spiWatchReport: 'yes',
// Block
defaultToBlock64: false,
defaultToPartialBlocks: false,
blankTalkpageOnIndefBlock: false,
// Fluff (revert and rollback)
autoMenuAfterRollback: false,
openTalkPage: ['norm', 'vand'],
openTalkPageOnAutoRevert: false,
rollbackInPlace: false,
markRevertedPagesAsMinor: ['vand'],
watchRevertedPages: ['norm', 'vand', 'torev'],
watchRevertedExpiry: '1 month',
offerReasonOnNormalRevert: true,
confirmOnRollback: false,
confirmOnMobileRollback: true,
showRollbackLinks: ['diff', 'others'],
SummaryOnVandalRevert: '差し戻し',
// DI (twinkleimage)
notifyUserOnDeli: true,
deliWatchPage: '1 month',
deliWatchUser: '1 month',
// Protect
watchRequestedPages: 'yes',
watchPPTaggedPages: 'default',
watchProtectedPages: 'default',
// CSD
speedySelectionStyle: 'buttonClick',
watchSpeedyPages: ['g3', 'g4', 'g9'],
watchSpeedyExpiry: '1 month',
markSpeedyPagesAsPatrolled: false,
watchSpeedyUser: '1 month',
// these next two should probably be identical by default
promptForSpeedyDeletionSummary: [],
deleteTalkPageOnDelete: false,
deleteRedirectsOnDelete: false,
deleteSysopDefaultToDelete: true,
speedyWindowHeight: 500,
speedyWindowWidth: 800,
logSpeedyNominations: false,
speedyLogPageName: 'CSD log',
noLogOnSpeedyNomination: ['u1', 'g3'],
// Unlink
unlinkNamespaces: ['0', '10', '100', '118'],
// Warn
defaultWarningGroup: '10',
combinedSingletMenus: false,
showSharedIPNotice: true,
watchWarnings: '1 month',
oldSelect: false,
customWarningList: [],
// XfD
logXfdNominations: false,
xfdLogPageName: 'XfD log',
noLogOnXfdNomination: [],
xfdWatchDiscussion: 'default',
xfdWatchList: 'no',
xfdWatchPage: '1 month',
xfdWatchUser: '1 month',
xfdWatchRelated: '1 month',
markXfdPagesAsPatrolled: true,
// Hidden preferences
autolevelStaleDays: 3, // Huggle is 3, CBNG is 2
revertMaxRevisions: 50, // intentionally limited
batchMax: 5000,
batchChunks: 50,
// Deprecated options, as a fallback for add-on scripts/modules
summaryAd: ' ([[H:TW|Twinkle]]使用)',
deletionSummaryAd: ' ([[H:TW|Twinkle]]使用)',
protectionSummaryAd: ' ([[H:TW|Twinkle]]使用)',
// Formerly defaultConfig.friendly:
// Tag
groupByDefault: true,
watchTaggedVenues: ['articles', 'drafts', 'redirects', 'files'],
watchTaggedPages: '1 month',
watchMergeDiscussions: '1 month',
markTaggedPagesAsMinor: false,
markTaggedPagesAsPatrolled: false,
tagArticleSortOrder: 'cat',
customTagList: [],
customFileTagList: [],
customRedirectTagList: [],
// Talkback
markTalkbackAsMinor: true,
insertTalkbackSignature: true, // always sign talkback templates
talkbackHeading: 'New message from ' + mw.config.get('wgUserName'),
mailHeading: "You've got mail!",
// Shared
markSharedIPAsMinor: true
};
Twinkle.getPref = function twinkleGetPref(name) {
const aliases = {
confirmOnRollback: 'confirmOnFluff',
confirmOnMobileRollback: 'confirmOnMobileFluff',
SummaryOnVandalRevert: 'vandusersummary'
};
// Support both current preferences and pre-twinkleoptions.js configurations.
for (const prefs of [Twinkle.prefs, window.TwinkleConfig, window.FriendlyConfig, Twinkle.defaultConfig]) {
if (!prefs || typeof prefs !== 'object') {
continue;
}
const value = prefs[name] !== undefined ? prefs[name] : prefs[aliases[name]];
if (value !== undefined) {
// Do not mutate the user's stored array while migrating the renamed module.
return name === 'disabledModules' ? value.map((module) => module === 'fluff' ? 'rollback' : module) : value;
}
}
};
/**
* Adds a portlet menu to one of the navigation areas on the page.
*
* @return {string} portletId
*/
Twinkle.addPortlet = function() {
/** @type {string} id of the target navigation area (skin dependent, on vector either of "#left-navigation", "#right-navigation", or "#mw-panel") */
let navigation;
/** @type {string} id of the portlet menu to create, preferably start with "p-". */
let id;
/** @type {string} name of the portlet menu to create. Visibility depends on the class used. */
let text;
/** @type {Node} the id of the node before which the new item should be added, should be another item in the same list, or undefined to place it at the end. */
let nextnodeid;
switch (mw.config.get('skin')) {
case 'vector':
case 'vector-2022':
navigation = '#right-navigation';
id = 'p-twinkle';
text = 'TW';
// In order to get mw.util.addPortlet to generate a dropdown menu in vector and vector-2022, the nextnodeid must be p-cactions. Any other nextnodeid will generate a non-dropdown portlet instead.
nextnodeid = 'p-cactions';
break;
case 'timeless':
navigation = '#page-tools .sidebar-inner';
id = 'p-twinkle';
text = 'Twinkle';
nextnodeid = 'p-userpagetools';
break;
default:
navigation = null;
id = 'p-cactions';
}
if (navigation === null) {
return id;
}
// make sure navigation is a valid CSS selector
const root = document.querySelector(navigation);
if (!root) {
return id;
}
// if we already created the portlet, return early. we don't want to create it again.
const item = document.getElementById(id);
if (item) {
return id;
}
mw.util.addPortlet(id, text, '#' + nextnodeid);
// The Twinkle dropdown menu has been added to the left of p-cactions, since that is the only spot that will create a dropdown menu. But we want it on the right. Move it to the right.
if (mw.config.get('skin') === 'vector') {
$('#p-twinkle').insertAfter('#p-cactions');
} else if (mw.config.get('skin') === 'vector-2022') {
const $landmark = $('#right-navigation > .vector-page-tools-landmark');
$('#p-twinkle-dropdown').insertBefore($landmark);
// .vector-page-tools-landmark is unstable and could change. If so, log it to console, to hopefully get someone's attention.
if (!$landmark) {
mw.log.warn('Unexpected change in DOM');
}
}
return id;
};
/**
* Builds a portlet menu if it doesn't exist yet, and adds a portlet link. This function runs at the top of every Twinkle module, ensuring that the first module to be loaded adds the portlet, and that every module can add a link to itself to the portlet.
*
* @param {string|Function} task Either a URL for the portlet link or a function to execute.
*/
Twinkle.addPortletLink = function(task, text, id, tooltip) {
// Create a portlet to hold all the portlet links (if not created already). And get the portletId.
const portletId = Twinkle.addPortlet();
// Create a portlet link and add it to the portlet.
const link = mw.util.addPortletLink(portletId, typeof task === 'string' ? task : '#', text, id, tooltip);
// Related to the hidden peer gadget that prevents jumpiness when the page first loads
$('.client-js .skin-vector #p-cactions').css('margin-right', 'initial');
// Add a click listener for the portlet link
if (typeof task === 'function') {
$(link).on('click', (ev) => {
task();
ev.preventDefault();
});
}
// $.collapsibleTabs is a feature of Vector 2010
if ($.collapsibleTabs) {
// Manually trigger a recalculation of what tabs to put where. This is to account for the space that the TW menu we just added is taking up.
$.collapsibleTabs.handleResize();
}
return link;
};
/**
* **************** General initialization code ****************
*/
// Retrieve the user's Twinkle preferences
Morebits.wiki.getCachedPage(`User:${mw.config.get('wgUserName')}/twinkleoptions.js`)
.then((optionsText) => {
if (!optionsText) {
// User has no options
return;
}
// Twinkle options are basically a JSON object with some comments. Strip those:
optionsText = optionsText.replace(/(?:^(?:\/\/[^\n]*\n)*\n*|(?:\/\/[^\n]*(?:\n|$))*$)/g, '');
// First version of options had some boilerplate code to make it eval-able -- strip that too. This part may become obsolete down the line.
if (optionsText.lastIndexOf('window.Twinkle.prefs = ', 0) === 0) {
optionsText = optionsText.replace(/(?:^window.Twinkle.prefs = |;\n*$)/g, '');
}
try {
const options = JSON.parse(optionsText);
if (options) {
if (options.twinkle || options.friendly) { // Old preferences format
Twinkle.prefs = $.extend(options.twinkle, options.friendly);
} else {
Twinkle.prefs = options;
}
// v2 established after unification of Twinkle/Friendly objects
Twinkle.prefs.optionsVersion = Twinkle.prefs.optionsVersion || 1;
}
} catch (e) {
mw.notify('Twinkleの設定を解析できませんでした', {type: 'error'});
}
})
.catch(() => {
console.log('Twinkleの個人設定が読み込めなかったため、デフォルトの個人設定になっています'); // eslint-disable-line no-console
})
.always(() => {
$(Twinkle.load);
});
// Developers: you can import custom Twinkle modules here
// For example, mw.loader.load(scriptpathbefore + "User:UncleDouggie/morebits-test.js" + scriptpathafter);
Twinkle.load = function() {
// Don't activate on special pages other than those listed here, so
// that others load faster, especially the watchlist.
let activeSpecialPageList = ['Block', 'Contributions', 'IPContributions', 'Recentchanges', 'Recentchangeslinked']; // wgRelevantUserName defined for non-sysops on Special:Block
if (Morebits.userIsSysop) {
activeSpecialPageList = activeSpecialPageList.concat(['DeletedContributions', 'Prefixindex']);
}
if (mw.config.get('wgNamespaceNumber') === -1 &&
!activeSpecialPageList.includes(mw.config.get('wgCanonicalSpecialPageName'))) {
return;
}
// Prevent clickjacking
if (window.top !== window.self) {
return;
}
// Set custom Api-User-Agent header, for server-side logging purposes
Morebits.wiki.Api.setApiUserAgent('Twinkle (' + mw.config.get('wgWikiID') + ')');
Twinkle.disabledModules = Twinkle.getPref('disabledModules').concat(Twinkle.getPref('disabledSysopModules'));
// Redefine addInitCallback so that any modules being loaded now on are directly
// initialised rather than added to initCallbacks array
Twinkle.addInitCallback = function(func, name) {
if (!name || !Twinkle.disabledModules.includes(name)) {
func();
}
};
// Initialise modules that were saved in initCallbacks array
Twinkle.initCallbacks.forEach((module) => {
Twinkle.addInitCallback(module.func, module.name);
});
// Increases text size in Twinkle dialogs, if so configured
if (Twinkle.getPref('dialogLargeFont')) {
mw.util.addCSS('.morebits-dialog-content, .morebits-dialog-footerlinks { font-size: 100% !important; } ' +
'.morebits-dialog input, .morebits-dialog select, .morebits-dialog-content button { font-size: inherit !important; }');
}
// Hide the lingering space if the TW menu is empty
const isVector = mw.config.get('skin') === 'vector' || mw.config.get('skin') === 'vector-2022';
if (isVector && Twinkle.getPref('portletType') === 'menu' && $('#p-twinkle').length === 0) {
$('#p-cactions').css('margin-right', 'initial');
}
// If using a skin with space for lots of modules, display a link to Twinkle Preferences
const usingSkinWithDropDownMenu = mw.config.get('skin') === 'vector' || mw.config.get('skin') === 'vector-2022' || mw.config.get('skin') === 'timeless';
if (usingSkinWithDropDownMenu) {
Twinkle.addPortletLink(mw.util.getUrl('User:Syunsyunminmin/Twinkle/Preferences'), 'Config', 'tw-config', 'Twinkleの設定ページを開く');
}
};
/**
* Twinkle-specific data shared by multiple modules
* Likely customized per installation
*/
// Change tags are disabled for every module on Japanese Wikipedia.
// Keep this empty so Morebits omits the tags parameter from API requests.
Twinkle.changeTags = '';
// Available for actions that don't (yet) support tags
// currently: FlaggedRevs and PageTriage
Twinkle.summaryAd = ' ([[Help:Twinkle|Twinkle]]使用)';
// Various hatnote templates, used when tagging (csd/xfd/tag/prod/protect) to
// ensure MOS:ORDER
Twinkle.hatnoteRegex = 'short description|hatnote|main|correct title|dablink|distinguish|for|further|selfref|year dab|similar names|highway detail hatnote|broader|about(?:-distinguish| other people)?|other\\s?(?:hurricane(?: use)?s|people|persons|places|ships|uses(?: of)?)|redirect(?:-(?:distinguish|synonym|multi))?|see\\s?(?:wiktionary|also(?: if exists)?)';
/* Twinkle-specific utility functions shared by multiple modules */
/**
* When performing rollbacks with [rollback] links, then visiting a user talk page, some data such as page name can be prefilled into Wel/AIV/Warn. Twinkle calls this a "prefill". This method gets a prefill, either from URL parameters (e.g. &vanarticle=Test) or from data previously stored using Twinkle.setPrefill()
*/
Twinkle.getPrefill = function(key) {
Twinkle.prefill = Twinkle.prefill || {};
if (!Object.prototype.hasOwnProperty.call(Twinkle.prefill, key)) {
Twinkle.prefill[key] = mw.util.getParamValue(key);
}
return Twinkle.prefill[key];
};
/**
* When performing rollbacks with [rollback] links, then visiting a user talk page, some data such as page name can be prefilled into Wel/AIV/Warn. Twinkle calls this a "prefill". This method sets a prefill. This data will be lost if the page is refreshed, unless it is added to the URL as a parameter.
*/
Twinkle.setPrefill = function(key, value) {
Twinkle.prefill = Twinkle.prefill || {};
Twinkle.prefill[key] = value;
};
/*
* Used in XFD and PROD
*/
Twinkle.makeFindSourcesDiv = function makeSourcesDiv(divID) {
if (!$(divID).length) {
return;
}
if (!Twinkle.findSources) {
const parser = new Morebits.wiki.Preview($(divID)[0]);
parser.beginRender('({{Find sources|' + Morebits.pageNameNorm + '}})', 'WP:AFD').then(() => {
// Save for second-time around
Twinkle.findSources = parser.previewbox.innerHTML;
$(divID).removeClass('morebits-previewbox');
});
} else {
$(divID).html(Twinkle.findSources);
}
};
/**
* Used in batch, unlink, and deprod to sort pages by namespace, as
* json formatversion=2 sorts by pageid instead (#1251)
*/
Twinkle.sortByNamespace = function(first, second) {
return first.ns - second.ns || (first.title > second.title ? 1 : -1);
};
/**
* Used in batch listings to link to the page in question with >
*/
Twinkle.generateArrowLinks = function(checkbox) {
const link = Morebits.htmlNode('a', ' >');
link.setAttribute('class', 'tw-arrowpage-link');
link.setAttribute('href', mw.util.getUrl(checkbox.value));
link.setAttribute('target', '_blank');
checkbox.nextElementSibling.append(link);
};
/**
* Used in deprod and unlink listings to link the page title
*/
Twinkle.generateBatchPageLinks = function(checkbox) {
const $checkbox = $(checkbox);
const link = Morebits.htmlNode('a', $checkbox.val());
link.setAttribute('class', 'tw-batchpage-link');
link.setAttribute('href', mw.util.getUrl($checkbox.val()));
link.setAttribute('target', '_blank');
$checkbox.next().prepend([link, ' ']);
};
/**
* remove "move to Commons" tag - deletion-tagged files cannot be moved to Commons
*/
Twinkle.removeMoveToCommonsTagsFromWikicode = (wikicode) => wikicode.replace(/\{\{(mtc|(copy |move )?to ?commons|move to wikimedia commons|copy to wikimedia commons)(?!( in))[^}]*\}\}/gi, '');
}());
// </nowiki>
g7d8kajyvkvft0rvwlfs7t5df67u6z5
769520
769453
2026-10-21T23:59:37Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 1406828:
769520
javascript
text/javascript
/**
* +-------------------------------------------------------------------------+
* | === WARNING: GLOBAL GADGET FILE === |
* | Changes to this page affect many users. |
* | Please discuss changes at [[WT:TW]] before editing. |
* +-------------------------------------------------------------------------+
*
* Imported from github [https://github.com/wikimedia-gadgets/twinkle].
* All changes should be made in the repository, otherwise they will be lost.
*
* ----------
*
* This is AzaToth's Twinkle, the popular script sidekick for newbies, admins, and
* every Wikipedian in between. Visit [[WP:TW]] for more information.
*/
// <nowiki>
/* global Morebits */
(function() {
// Check if account is experienced enough to use Twinkle
if (!Morebits.userIsInGroup('autoconfirmed') && !Morebits.userIsInGroup('confirmed')) {
return;
}
const Twinkle = {};
window.Twinkle = Twinkle; // allow global access
Twinkle.initCallbacks = [];
/**
* Adds a callback to execute when Twinkle has loaded.
*
* @param {Function} func
* @param {string} [name] - name of module used to check if is disabled.
* If name is not given, module is loaded unconditionally.
*/
Twinkle.addInitCallback = function twinkleAddInitCallback(func, name) {
Twinkle.initCallbacks.push({ func: func, name: name });
};
Twinkle.defaultConfig = {};
/**
* This holds the default set of preferences used by Twinkle.
* It is important that all new preferences added here, especially admin-only ones, are also added to
* |Twinkle.config.sections| in twinkleconfig.js, so they are configurable via the Twinkle preferences panel.
* For help on the actual preferences, see the comments in twinkleconfig.js.
*/
Twinkle.defaultConfig = {
// General
userTalkPageMode: 'tab',
dialogLargeFont: false,
disabledModules: ['arv', 'warn', 'talkback', 'xfd', 'tag'],
disabledSysopModules: [],
// ARV
spiWatchReport: 'yes',
// Block
defaultToBlock64: false,
defaultToPartialBlocks: false,
blankTalkpageOnIndefBlock: false,
// Fluff (revert and rollback)
autoMenuAfterRollback: false,
openTalkPage: ['norm', 'vand'],
openTalkPageOnAutoRevert: false,
rollbackInPlace: false,
markRevertedPagesAsMinor: ['vand'],
watchRevertedPages: ['norm', 'vand', 'torev'],
watchRevertedExpiry: '1 month',
offerReasonOnNormalRevert: true,
confirmOnRollback: false,
confirmOnMobileRollback: true,
showRollbackLinks: ['diff', 'others'],
SummaryOnVandalRevert: '差し戻し',
// DI (twinkleimage)
notifyUserOnDeli: true,
deliWatchPage: '1 month',
deliWatchUser: '1 month',
// Protect
watchRequestedPages: 'yes',
watchPPTaggedPages: 'default',
watchProtectedPages: 'default',
// CSD
speedySelectionStyle: 'buttonClick',
watchSpeedyPages: ['g3', 'g4', 'g9'],
watchSpeedyExpiry: '1 month',
markSpeedyPagesAsPatrolled: false,
watchSpeedyUser: '1 month',
// these next two should probably be identical by default
promptForSpeedyDeletionSummary: [],
deleteTalkPageOnDelete: false,
deleteRedirectsOnDelete: false,
deleteSysopDefaultToDelete: true,
speedyWindowHeight: 500,
speedyWindowWidth: 800,
logSpeedyNominations: false,
speedyLogPageName: 'CSD log',
noLogOnSpeedyNomination: ['u1', 'g3'],
// Unlink
unlinkNamespaces: ['0', '10', '100', '118'],
// Warn
defaultWarningGroup: '10',
combinedSingletMenus: false,
showSharedIPNotice: true,
watchWarnings: '1 month',
oldSelect: false,
customWarningList: [],
// XfD
logXfdNominations: false,
xfdLogPageName: 'XfD log',
noLogOnXfdNomination: [],
xfdWatchDiscussion: 'default',
xfdWatchList: 'no',
xfdWatchPage: '1 month',
xfdWatchUser: '1 month',
xfdWatchRelated: '1 month',
markXfdPagesAsPatrolled: true,
// Hidden preferences
autolevelStaleDays: 3, // Huggle is 3, CBNG is 2
revertMaxRevisions: 50, // intentionally limited
batchMax: 5000,
batchChunks: 50,
// Deprecated options, as a fallback for add-on scripts/modules
summaryAd: ' ([[H:TW|Twinkle]]使用)',
deletionSummaryAd: ' ([[H:TW|Twinkle]]使用)',
protectionSummaryAd: ' ([[H:TW|Twinkle]]使用)',
// Formerly defaultConfig.friendly:
// Tag
groupByDefault: true,
watchTaggedVenues: ['articles', 'drafts', 'redirects', 'files'],
watchTaggedPages: '1 month',
watchMergeDiscussions: '1 month',
markTaggedPagesAsMinor: false,
markTaggedPagesAsPatrolled: false,
tagArticleSortOrder: 'cat',
customTagList: [],
customFileTagList: [],
customRedirectTagList: [],
// Talkback
markTalkbackAsMinor: true,
insertTalkbackSignature: true, // always sign talkback templates
talkbackHeading: 'New message from ' + mw.config.get('wgUserName'),
mailHeading: "You've got mail!",
// Shared
markSharedIPAsMinor: true
};
Twinkle.getPref = function twinkleGetPref(name) {
const aliases = {
confirmOnRollback: 'confirmOnFluff',
confirmOnMobileRollback: 'confirmOnMobileFluff',
SummaryOnVandalRevert: 'vandusersummary'
};
// Support both current preferences and pre-twinkleoptions.js configurations.
for (const prefs of [Twinkle.prefs, window.TwinkleConfig, window.FriendlyConfig, Twinkle.defaultConfig]) {
if (!prefs || typeof prefs !== 'object') {
continue;
}
const value = prefs[name] !== undefined ? prefs[name] : prefs[aliases[name]];
if (value !== undefined) {
// Do not mutate the user's stored array while migrating the renamed module.
return name === 'disabledModules' ? value.map((module) => module === 'fluff' ? 'rollback' : module) : value;
}
}
};
/**
* Adds a portlet menu to one of the navigation areas on the page.
*
* @return {string} portletId
*/
Twinkle.addPortlet = function() {
/** @type {string} id of the target navigation area (skin dependent, on vector either of "#left-navigation", "#right-navigation", or "#mw-panel") */
let navigation;
/** @type {string} id of the portlet menu to create, preferably start with "p-". */
let id;
/** @type {string} name of the portlet menu to create. Visibility depends on the class used. */
let text;
/** @type {Node} the id of the node before which the new item should be added, should be another item in the same list, or undefined to place it at the end. */
let nextnodeid;
switch (mw.config.get('skin')) {
case 'vector':
case 'vector-2022':
navigation = '#right-navigation';
id = 'p-twinkle';
text = 'TW';
// In order to get mw.util.addPortlet to generate a dropdown menu in vector and vector-2022, the nextnodeid must be p-cactions. Any other nextnodeid will generate a non-dropdown portlet instead.
nextnodeid = 'p-cactions';
break;
case 'timeless':
navigation = '#page-tools .sidebar-inner';
id = 'p-twinkle';
text = 'Twinkle';
nextnodeid = 'p-userpagetools';
break;
default:
navigation = null;
id = 'p-cactions';
}
if (navigation === null) {
return id;
}
// make sure navigation is a valid CSS selector
const root = document.querySelector(navigation);
if (!root) {
return id;
}
// if we already created the portlet, return early. we don't want to create it again.
const item = document.getElementById(id);
if (item) {
return id;
}
mw.util.addPortlet(id, text, '#' + nextnodeid);
// The Twinkle dropdown menu has been added to the left of p-cactions, since that is the only spot that will create a dropdown menu. But we want it on the right. Move it to the right.
if (mw.config.get('skin') === 'vector') {
$('#p-twinkle').insertAfter('#p-cactions');
} else if (mw.config.get('skin') === 'vector-2022') {
const $landmark = $('#right-navigation > .vector-page-tools-landmark');
$('#p-twinkle-dropdown').insertBefore($landmark);
// .vector-page-tools-landmark is unstable and could change. If so, log it to console, to hopefully get someone's attention.
if (!$landmark) {
mw.log.warn('Unexpected change in DOM');
}
}
return id;
};
/**
* Builds a portlet menu if it doesn't exist yet, and adds a portlet link. This function runs at the top of every Twinkle module, ensuring that the first module to be loaded adds the portlet, and that every module can add a link to itself to the portlet.
*
* @param {string|Function} task Either a URL for the portlet link or a function to execute.
*/
Twinkle.addPortletLink = function(task, text, id, tooltip) {
// Create a portlet to hold all the portlet links (if not created already). And get the portletId.
const portletId = Twinkle.addPortlet();
// Create a portlet link and add it to the portlet.
const link = mw.util.addPortletLink(portletId, typeof task === 'string' ? task : '#', text, id, tooltip);
// Related to the hidden peer gadget that prevents jumpiness when the page first loads
$('.client-js .skin-vector #p-cactions').css('margin-right', 'initial');
// Add a click listener for the portlet link
if (typeof task === 'function') {
$(link).on('click', (ev) => {
task();
ev.preventDefault();
});
}
// $.collapsibleTabs is a feature of Vector 2010
if ($.collapsibleTabs) {
// Manually trigger a recalculation of what tabs to put where. This is to account for the space that the TW menu we just added is taking up.
$.collapsibleTabs.handleResize();
}
return link;
};
/**
* **************** General initialization code ****************
*/
// Retrieve the user's Twinkle preferences
Morebits.wiki.getCachedPage(`User:${mw.config.get('wgUserName')}/twinkleoptions.js`)
.then((optionsText) => {
if (!optionsText) {
// User has no options
return;
}
// Twinkle options are basically a JSON object with some comments. Strip those:
optionsText = optionsText.replace(/(?:^(?:\/\/[^\n]*\n)*\n*|(?:\/\/[^\n]*(?:\n|$))*$)/g, '');
// First version of options had some boilerplate code to make it eval-able -- strip that too. This part may become obsolete down the line.
if (optionsText.lastIndexOf('window.Twinkle.prefs = ', 0) === 0) {
optionsText = optionsText.replace(/(?:^window.Twinkle.prefs = |;\n*$)/g, '');
}
try {
const options = JSON.parse(optionsText);
if (options) {
if (options.twinkle || options.friendly) { // Old preferences format
Twinkle.prefs = $.extend(options.twinkle, options.friendly);
} else {
Twinkle.prefs = options;
}
// v2 established after unification of Twinkle/Friendly objects
Twinkle.prefs.optionsVersion = Twinkle.prefs.optionsVersion || 1;
}
} catch (e) {
mw.notify('Twinkleの設定を解析できませんでした', {type: 'error'});
}
})
.catch(() => {
console.log('Twinkleの個人設定が読み込めなかったため、デフォルトの個人設定になっています'); // eslint-disable-line no-console
})
.always(() => {
$(Twinkle.load);
});
// Developers: you can import custom Twinkle modules here
// For example, mw.loader.load(scriptpathbefore + "User:UncleDouggie/morebits-test.js" + scriptpathafter);
Twinkle.load = function() {
// Don't activate on special pages other than those listed here, so
// that others load faster, especially the watchlist.
let activeSpecialPageList = ['Block', 'Contributions', 'IPContributions', 'Recentchanges', 'Recentchangeslinked']; // wgRelevantUserName defined for non-sysops on Special:Block
if (Morebits.userIsSysop) {
activeSpecialPageList = activeSpecialPageList.concat(['DeletedContributions', 'Prefixindex']);
}
if (mw.config.get('wgNamespaceNumber') === -1 &&
!activeSpecialPageList.includes(mw.config.get('wgCanonicalSpecialPageName'))) {
return;
}
// Prevent clickjacking
if (window.top !== window.self) {
return;
}
// Set custom Api-User-Agent header, for server-side logging purposes
Morebits.wiki.Api.setApiUserAgent('Twinkle (' + mw.config.get('wgWikiID') + ')');
Twinkle.disabledModules = Twinkle.getPref('disabledModules').concat(Twinkle.getPref('disabledSysopModules'));
// Redefine addInitCallback so that any modules being loaded now on are directly
// initialised rather than added to initCallbacks array
Twinkle.addInitCallback = function(func, name) {
if (!name || !Twinkle.disabledModules.includes(name)) {
func();
}
};
// Initialise modules that were saved in initCallbacks array
Twinkle.initCallbacks.forEach((module) => {
Twinkle.addInitCallback(module.func, module.name);
});
// Increases text size in Twinkle dialogs, if so configured
if (Twinkle.getPref('dialogLargeFont')) {
mw.util.addCSS('.morebits-dialog-content, .morebits-dialog-footerlinks { font-size: 100% !important; } ' +
'.morebits-dialog input, .morebits-dialog select, .morebits-dialog-content button { font-size: inherit !important; }');
}
// Hide the lingering space if the TW menu is empty
const isVector = mw.config.get('skin') === 'vector' || mw.config.get('skin') === 'vector-2022';
if (isVector && Twinkle.getPref('portletType') === 'menu' && $('#p-twinkle').length === 0) {
$('#p-cactions').css('margin-right', 'initial');
}
// If using a skin with space for lots of modules, display a link to Twinkle Preferences
const usingSkinWithDropDownMenu = mw.config.get('skin') === 'vector' || mw.config.get('skin') === 'vector-2022' || mw.config.get('skin') === 'timeless';
if (usingSkinWithDropDownMenu) {
Twinkle.addPortletLink(mw.util.getUrl('User:Syunsyunminmin/Twinkle/Preferences'), 'Config', 'tw-config', 'Twinkleの設定ページを開く');
}
};
/**
* Twinkle-specific data shared by multiple modules
* Likely customized per installation
*/
// Change tags are disabled for every module on Japanese Wikipedia.
// Keep this empty so Morebits omits the tags parameter from API requests.
Twinkle.changeTags = '';
// Available for actions that don't (yet) support tags
// currently: FlaggedRevs and PageTriage
Twinkle.summaryAd = ' ([[Help:Twinkle|Twinkle]]使用)';
// Various hatnote templates, used when tagging (csd/xfd/tag/prod/protect) to
// ensure MOS:ORDER
Twinkle.hatnoteRegex = 'short description|hatnote|main|correct title|dablink|distinguish|for|further|selfref|year dab|similar names|highway detail hatnote|broader|about(?:-distinguish| other people)?|other\\s?(?:hurricane(?: use)?s|people|persons|places|ships|uses(?: of)?)|redirect(?:-(?:distinguish|synonym|multi))?|see\\s?(?:wiktionary|also(?: if exists)?)';
/* Twinkle-specific utility functions shared by multiple modules */
/**
* When performing rollbacks with [rollback] links, then visiting a user talk page, some data such as page name can be prefilled into Wel/AIV/Warn. Twinkle calls this a "prefill". This method gets a prefill, either from URL parameters (e.g. &vanarticle=Test) or from data previously stored using Twinkle.setPrefill()
*/
Twinkle.getPrefill = function(key) {
Twinkle.prefill = Twinkle.prefill || {};
if (!Object.prototype.hasOwnProperty.call(Twinkle.prefill, key)) {
Twinkle.prefill[key] = mw.util.getParamValue(key);
}
return Twinkle.prefill[key];
};
/**
* When performing rollbacks with [rollback] links, then visiting a user talk page, some data such as page name can be prefilled into Wel/AIV/Warn. Twinkle calls this a "prefill". This method sets a prefill. This data will be lost if the page is refreshed, unless it is added to the URL as a parameter.
*/
Twinkle.setPrefill = function(key, value) {
Twinkle.prefill = Twinkle.prefill || {};
Twinkle.prefill[key] = value;
};
/*
* Used in XFD and PROD
*/
Twinkle.makeFindSourcesDiv = function makeSourcesDiv(divID) {
if (!$(divID).length) {
return;
}
if (!Twinkle.findSources) {
const parser = new Morebits.wiki.Preview($(divID)[0]);
parser.beginRender('({{Find sources|' + Morebits.pageNameNorm + '}})', 'WP:AFD').then(() => {
// Save for second-time around
Twinkle.findSources = parser.previewbox.innerHTML;
$(divID).removeClass('morebits-previewbox');
});
} else {
$(divID).html(Twinkle.findSources);
}
};
/**
* Used in batch, unlink, and deprod to sort pages by namespace, as
* json formatversion=2 sorts by pageid instead (#1251)
*/
Twinkle.sortByNamespace = function(first, second) {
return first.ns - second.ns || (first.title > second.title ? 1 : -1);
};
/**
* Used in batch listings to link to the page in question with >
*/
Twinkle.generateArrowLinks = function(checkbox) {
const link = Morebits.htmlNode('a', ' >');
link.setAttribute('class', 'tw-arrowpage-link');
link.setAttribute('href', mw.util.getUrl(checkbox.value));
link.setAttribute('target', '_blank');
checkbox.nextElementSibling.append(link);
};
/**
* Used in deprod and unlink listings to link the page title
*/
Twinkle.generateBatchPageLinks = function(checkbox) {
const $checkbox = $(checkbox);
const link = Morebits.htmlNode('a', $checkbox.val());
link.setAttribute('class', 'tw-batchpage-link');
link.setAttribute('href', mw.util.getUrl($checkbox.val()));
link.setAttribute('target', '_blank');
$checkbox.next().prepend([link, ' ']);
};
/**
* remove "move to Commons" tag - deletion-tagged files cannot be moved to Commons
*/
Twinkle.removeMoveToCommonsTagsFromWikicode = (wikicode) => wikicode.replace(/\{\{(mtc|(copy |move )?to ?commons|move to wikimedia commons|copy to wikimedia commons)(?!( in))[^}]*\}\}/gi, '');
}());
// </nowiki>
caol4pbt0hxo4e1xikmfj27aetx3sqf
769552
769520
2026-10-22T01:08:59Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at ff10bbf0c6a2cbb87decbc1d177b18916f75af77: キャッシュ処理
769552
javascript
text/javascript
/**
* +-------------------------------------------------------------------------+
* | === WARNING: GLOBAL GADGET FILE === |
* | Changes to this page affect many users. |
* | Please discuss changes at [[WT:TW]] before editing. |
* +-------------------------------------------------------------------------+
*
* Imported from github [https://github.com/wikimedia-gadgets/twinkle].
* All changes should be made in the repository, otherwise they will be lost.
*
* ----------
*
* This is AzaToth's Twinkle, the popular script sidekick for newbies, admins, and
* every Wikipedian in between. Visit [[WP:TW]] for more information.
*/
// <nowiki>
/* global Morebits */
(function() {
// Check if account is experienced enough to use Twinkle
if (!Morebits.userIsInGroup('autoconfirmed') && !Morebits.userIsInGroup('confirmed')) {
return;
}
const Twinkle = {};
window.Twinkle = Twinkle; // allow global access
Twinkle.initCallbacks = [];
/**
* Adds a callback to execute when Twinkle has loaded.
*
* @param {Function} func
* @param {string} [name] - name of module used to check if is disabled.
* If name is not given, module is loaded unconditionally.
*/
Twinkle.addInitCallback = function twinkleAddInitCallback(func, name) {
Twinkle.initCallbacks.push({ func: func, name: name });
};
Twinkle.defaultConfig = {};
/**
* This holds the default set of preferences used by Twinkle.
* It is important that all new preferences added here, especially admin-only ones, are also added to
* |Twinkle.config.sections| in twinkleconfig.js, so they are configurable via the Twinkle preferences panel.
* For help on the actual preferences, see the comments in twinkleconfig.js.
*/
Twinkle.defaultConfig = {
// General
userTalkPageMode: 'tab',
dialogLargeFont: false,
disabledModules: ['arv', 'warn', 'talkback', 'xfd', 'tag'],
disabledSysopModules: [],
// ARV
spiWatchReport: 'yes',
// Block
defaultToBlock64: false,
defaultToPartialBlocks: false,
blankTalkpageOnIndefBlock: false,
// Fluff (revert and rollback)
autoMenuAfterRollback: false,
openTalkPage: ['norm', 'vand'],
openTalkPageOnAutoRevert: false,
rollbackInPlace: false,
markRevertedPagesAsMinor: ['vand'],
watchRevertedPages: ['norm', 'vand', 'torev'],
watchRevertedExpiry: '1 month',
offerReasonOnNormalRevert: true,
confirmOnRollback: false,
confirmOnMobileRollback: true,
showRollbackLinks: ['diff', 'others'],
SummaryOnVandalRevert: '差し戻し',
// DI (twinkleimage)
notifyUserOnDeli: true,
deliWatchPage: '1 month',
deliWatchUser: '1 month',
// Protect
watchRequestedPages: 'yes',
watchPPTaggedPages: 'default',
watchProtectedPages: 'default',
// CSD
speedySelectionStyle: 'buttonClick',
watchSpeedyPages: ['g3', 'g4', 'g9'],
watchSpeedyExpiry: '1 month',
markSpeedyPagesAsPatrolled: false,
watchSpeedyUser: '1 month',
// these next two should probably be identical by default
promptForSpeedyDeletionSummary: [],
deleteTalkPageOnDelete: false,
deleteRedirectsOnDelete: false,
deleteSysopDefaultToDelete: true,
speedyWindowHeight: 500,
speedyWindowWidth: 800,
logSpeedyNominations: false,
speedyLogPageName: 'CSD log',
noLogOnSpeedyNomination: ['u1', 'g3'],
// Unlink
unlinkNamespaces: ['0', '10', '100', '118'],
// Warn
defaultWarningGroup: '10',
combinedSingletMenus: false,
showSharedIPNotice: true,
watchWarnings: '1 month',
oldSelect: false,
customWarningList: [],
// XfD
logXfdNominations: false,
xfdLogPageName: 'XfD log',
noLogOnXfdNomination: [],
xfdWatchDiscussion: 'default',
xfdWatchList: 'no',
xfdWatchPage: '1 month',
xfdWatchUser: '1 month',
xfdWatchRelated: '1 month',
markXfdPagesAsPatrolled: true,
// Hidden preferences
autolevelStaleDays: 3, // Huggle is 3, CBNG is 2
revertMaxRevisions: 50, // intentionally limited
batchMax: 5000,
batchChunks: 50,
// Deprecated options, as a fallback for add-on scripts/modules
summaryAd: ' ([[H:TW|Twinkle]]使用)',
deletionSummaryAd: ' ([[H:TW|Twinkle]]使用)',
protectionSummaryAd: ' ([[H:TW|Twinkle]]使用)',
// Formerly defaultConfig.friendly:
// Tag
groupByDefault: true,
watchTaggedVenues: ['articles', 'drafts', 'redirects', 'files'],
watchTaggedPages: '1 month',
watchMergeDiscussions: '1 month',
markTaggedPagesAsMinor: false,
markTaggedPagesAsPatrolled: false,
tagArticleSortOrder: 'cat',
customTagList: [],
customFileTagList: [],
customRedirectTagList: [],
// Talkback
markTalkbackAsMinor: true,
insertTalkbackSignature: true, // always sign talkback templates
talkbackHeading: 'New message from ' + mw.config.get('wgUserName'),
mailHeading: "You've got mail!",
// Shared
markSharedIPAsMinor: true
};
Twinkle.getPref = function twinkleGetPref(name) {
const aliases = {
confirmOnRollback: 'confirmOnFluff',
confirmOnMobileRollback: 'confirmOnMobileFluff',
SummaryOnVandalRevert: 'vandusersummary'
};
// Support both current preferences and pre-twinkleoptions.js configurations.
for (const prefs of [Twinkle.prefs, window.TwinkleConfig, window.FriendlyConfig, Twinkle.defaultConfig]) {
if (!prefs || typeof prefs !== 'object') {
continue;
}
const value = prefs[name] !== undefined ? prefs[name] : prefs[aliases[name]];
if (value !== undefined) {
// Do not mutate the user's stored array while migrating the renamed module.
return name === 'disabledModules' ? value.map((module) => module === 'fluff' ? 'rollback' : module) : value;
}
}
};
/**
* Adds a portlet menu to one of the navigation areas on the page.
*
* @return {string} portletId
*/
Twinkle.addPortlet = function() {
/** @type {string} id of the target navigation area (skin dependent, on vector either of "#left-navigation", "#right-navigation", or "#mw-panel") */
let navigation;
/** @type {string} id of the portlet menu to create, preferably start with "p-". */
let id;
/** @type {string} name of the portlet menu to create. Visibility depends on the class used. */
let text;
/** @type {Node} the id of the node before which the new item should be added, should be another item in the same list, or undefined to place it at the end. */
let nextnodeid;
switch (mw.config.get('skin')) {
case 'vector':
case 'vector-2022':
navigation = '#right-navigation';
id = 'p-twinkle';
text = 'TW';
// In order to get mw.util.addPortlet to generate a dropdown menu in vector and vector-2022, the nextnodeid must be p-cactions. Any other nextnodeid will generate a non-dropdown portlet instead.
nextnodeid = 'p-cactions';
break;
case 'timeless':
navigation = '#page-tools .sidebar-inner';
id = 'p-twinkle';
text = 'Twinkle';
nextnodeid = 'p-userpagetools';
break;
default:
navigation = null;
id = 'p-cactions';
}
if (navigation === null) {
return id;
}
// make sure navigation is a valid CSS selector
const root = document.querySelector(navigation);
if (!root) {
return id;
}
// if we already created the portlet, return early. we don't want to create it again.
const item = document.getElementById(id);
if (item) {
return id;
}
mw.util.addPortlet(id, text, '#' + nextnodeid);
// The Twinkle dropdown menu has been added to the left of p-cactions, since that is the only spot that will create a dropdown menu. But we want it on the right. Move it to the right.
if (mw.config.get('skin') === 'vector') {
$('#p-twinkle').insertAfter('#p-cactions');
} else if (mw.config.get('skin') === 'vector-2022') {
const $landmark = $('#right-navigation > .vector-page-tools-landmark');
$('#p-twinkle-dropdown').insertBefore($landmark);
// .vector-page-tools-landmark is unstable and could change. If so, log it to console, to hopefully get someone's attention.
if (!$landmark) {
mw.log.warn('Unexpected change in DOM');
}
}
return id;
};
/**
* Builds a portlet menu if it doesn't exist yet, and adds a portlet link. This function runs at the top of every Twinkle module, ensuring that the first module to be loaded adds the portlet, and that every module can add a link to itself to the portlet.
*
* @param {string|Function} task Either a URL for the portlet link or a function to execute.
*/
Twinkle.addPortletLink = function(task, text, id, tooltip) {
// Create a portlet to hold all the portlet links (if not created already). And get the portletId.
const portletId = Twinkle.addPortlet();
// Create a portlet link and add it to the portlet.
const link = mw.util.addPortletLink(portletId, typeof task === 'string' ? task : '#', text, id, tooltip);
// Related to the hidden peer gadget that prevents jumpiness when the page first loads
$('.client-js .skin-vector #p-cactions').css('margin-right', 'initial');
// Add a click listener for the portlet link
if (typeof task === 'function') {
$(link).on('click', (ev) => {
task();
ev.preventDefault();
});
}
// $.collapsibleTabs is a feature of Vector 2010
if ($.collapsibleTabs) {
// Manually trigger a recalculation of what tabs to put where. This is to account for the space that the TW menu we just added is taking up.
$.collapsibleTabs.handleResize();
}
return link;
};
/**
* **************** General initialization code ****************
*/
// Retrieve current preferences, including changes made since the last page view.
Morebits.wiki.getCachedPage(`User:${mw.config.get('wgUserName')}/twinkleoptions.js`, 0)
.then((optionsText) => {
if (!optionsText) {
// User has no options
return;
}
// Twinkle options are basically a JSON object with some comments. Strip those:
optionsText = optionsText.replace(/(?:^(?:\/\/[^\n]*\n)*\n*|(?:\/\/[^\n]*(?:\n|$))*$)/g, '');
// First version of options had some boilerplate code to make it eval-able -- strip that too. This part may become obsolete down the line.
if (optionsText.lastIndexOf('window.Twinkle.prefs = ', 0) === 0) {
optionsText = optionsText.replace(/(?:^window.Twinkle.prefs = |;\n*$)/g, '');
}
try {
const options = JSON.parse(optionsText);
if (options) {
if (options.twinkle || options.friendly) { // Old preferences format
Twinkle.prefs = $.extend(options.twinkle, options.friendly);
} else {
Twinkle.prefs = options;
}
// v2 established after unification of Twinkle/Friendly objects
Twinkle.prefs.optionsVersion = Twinkle.prefs.optionsVersion || 1;
}
} catch (e) {
mw.notify('Twinkleの設定を解析できませんでした', {type: 'error'});
}
})
.catch(() => {
console.log('Twinkleの個人設定が読み込めなかったため、デフォルトの個人設定になっています'); // eslint-disable-line no-console
})
.always(() => {
$(Twinkle.load);
});
// Developers: you can import custom Twinkle modules here
// For example, mw.loader.load(scriptpathbefore + "User:UncleDouggie/morebits-test.js" + scriptpathafter);
Twinkle.load = function() {
// Don't activate on special pages other than those listed here, so
// that others load faster, especially the watchlist.
let activeSpecialPageList = ['Block', 'Contributions', 'IPContributions', 'Recentchanges', 'Recentchangeslinked']; // wgRelevantUserName defined for non-sysops on Special:Block
if (Morebits.userIsSysop) {
activeSpecialPageList = activeSpecialPageList.concat(['DeletedContributions', 'Prefixindex']);
}
if (mw.config.get('wgNamespaceNumber') === -1 &&
!activeSpecialPageList.includes(mw.config.get('wgCanonicalSpecialPageName'))) {
return;
}
// Prevent clickjacking
if (window.top !== window.self) {
return;
}
// Set custom Api-User-Agent header, for server-side logging purposes
Morebits.wiki.Api.setApiUserAgent('Twinkle (' + mw.config.get('wgWikiID') + ')');
Twinkle.disabledModules = Twinkle.getPref('disabledModules').concat(Twinkle.getPref('disabledSysopModules'));
// Redefine addInitCallback so that any modules being loaded now on are directly
// initialised rather than added to initCallbacks array
Twinkle.addInitCallback = function(func, name) {
if (!name || !Twinkle.disabledModules.includes(name)) {
func();
}
};
// Initialise modules that were saved in initCallbacks array
Twinkle.initCallbacks.forEach((module) => {
Twinkle.addInitCallback(module.func, module.name);
});
// Increases text size in Twinkle dialogs, if so configured
if (Twinkle.getPref('dialogLargeFont')) {
mw.util.addCSS('.morebits-dialog-content, .morebits-dialog-footerlinks { font-size: 100% !important; } ' +
'.morebits-dialog input, .morebits-dialog select, .morebits-dialog-content button { font-size: inherit !important; }');
}
// Hide the lingering space if the TW menu is empty
const isVector = mw.config.get('skin') === 'vector' || mw.config.get('skin') === 'vector-2022';
if (isVector && Twinkle.getPref('portletType') === 'menu' && $('#p-twinkle').length === 0) {
$('#p-cactions').css('margin-right', 'initial');
}
// If using a skin with space for lots of modules, display a link to Twinkle Preferences
const usingSkinWithDropDownMenu = mw.config.get('skin') === 'vector' || mw.config.get('skin') === 'vector-2022' || mw.config.get('skin') === 'timeless';
if (usingSkinWithDropDownMenu) {
Twinkle.addPortletLink(mw.util.getUrl('User:Syunsyunminmin/Twinkle/Preferences'), 'Config', 'tw-config', 'Twinkleの設定ページを開く');
}
};
/**
* Twinkle-specific data shared by multiple modules
* Likely customized per installation
*/
// Change tags are disabled for every module on Japanese Wikipedia.
// Keep this empty so Morebits omits the tags parameter from API requests.
Twinkle.changeTags = '';
// Available for actions that don't (yet) support tags
// currently: FlaggedRevs and PageTriage
Twinkle.summaryAd = ' ([[Help:Twinkle|Twinkle]]使用)';
// Various hatnote templates, used when tagging (csd/xfd/tag/prod/protect) to
// ensure MOS:ORDER
Twinkle.hatnoteRegex = 'short description|hatnote|main|correct title|dablink|distinguish|for|further|selfref|year dab|similar names|highway detail hatnote|broader|about(?:-distinguish| other people)?|other\\s?(?:hurricane(?: use)?s|people|persons|places|ships|uses(?: of)?)|redirect(?:-(?:distinguish|synonym|multi))?|see\\s?(?:wiktionary|also(?: if exists)?)';
/* Twinkle-specific utility functions shared by multiple modules */
/**
* When performing rollbacks with [rollback] links, then visiting a user talk page, some data such as page name can be prefilled into Wel/AIV/Warn. Twinkle calls this a "prefill". This method gets a prefill, either from URL parameters (e.g. &vanarticle=Test) or from data previously stored using Twinkle.setPrefill()
*/
Twinkle.getPrefill = function(key) {
Twinkle.prefill = Twinkle.prefill || {};
if (!Object.prototype.hasOwnProperty.call(Twinkle.prefill, key)) {
Twinkle.prefill[key] = mw.util.getParamValue(key);
}
return Twinkle.prefill[key];
};
/**
* When performing rollbacks with [rollback] links, then visiting a user talk page, some data such as page name can be prefilled into Wel/AIV/Warn. Twinkle calls this a "prefill". This method sets a prefill. This data will be lost if the page is refreshed, unless it is added to the URL as a parameter.
*/
Twinkle.setPrefill = function(key, value) {
Twinkle.prefill = Twinkle.prefill || {};
Twinkle.prefill[key] = value;
};
/*
* Used in XFD and PROD
*/
Twinkle.makeFindSourcesDiv = function makeSourcesDiv(divID) {
if (!$(divID).length) {
return;
}
if (!Twinkle.findSources) {
const parser = new Morebits.wiki.Preview($(divID)[0]);
parser.beginRender('({{Find sources|' + Morebits.pageNameNorm + '}})', 'WP:AFD').then(() => {
// Save for second-time around
Twinkle.findSources = parser.previewbox.innerHTML;
$(divID).removeClass('morebits-previewbox');
});
} else {
$(divID).html(Twinkle.findSources);
}
};
/**
* Used in batch, unlink, and deprod to sort pages by namespace, as
* json formatversion=2 sorts by pageid instead (#1251)
*/
Twinkle.sortByNamespace = function(first, second) {
return first.ns - second.ns || (first.title > second.title ? 1 : -1);
};
/**
* Used in batch listings to link to the page in question with >
*/
Twinkle.generateArrowLinks = function(checkbox) {
const link = Morebits.htmlNode('a', ' >');
link.setAttribute('class', 'tw-arrowpage-link');
link.setAttribute('href', mw.util.getUrl(checkbox.value));
link.setAttribute('target', '_blank');
checkbox.nextElementSibling.append(link);
};
/**
* Used in deprod and unlink listings to link the page title
*/
Twinkle.generateBatchPageLinks = function(checkbox) {
const $checkbox = $(checkbox);
const link = Morebits.htmlNode('a', $checkbox.val());
link.setAttribute('class', 'tw-batchpage-link');
link.setAttribute('href', mw.util.getUrl($checkbox.val()));
link.setAttribute('target', '_blank');
$checkbox.next().prepend([link, ' ']);
};
/**
* remove "move to Commons" tag - deletion-tagged files cannot be moved to Commons
*/
Twinkle.removeMoveToCommonsTagsFromWikicode = (wikicode) => wikicode.replace(/\{\{(mtc|(copy |move )?to ?commons|move to wikimedia commons|copy to wikimedia commons)(?!( in))[^}]*\}\}/gi, '');
}());
// </nowiki>
b0wocffn2yygzckdd3z4xgyhccwgd90
769558
769552
2026-10-22T02:13:32Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at d9cd3461baca43ac99ac2c24fb76a3aae6ddada9: minify
769558
javascript
text/javascript
// <nowiki>
/**
* +-------------------------------------------------------------------------+
* | === WARNING: GLOBAL GADGET FILE === |
* | Changes to this page affect many users. |
* | Please discuss changes at [[WT:TW]] before editing. |
* +-------------------------------------------------------------------------+
*
* Imported from github [https://github.com/wikimedia-gadgets/twinkle].
* All changes should be made in the repository, otherwise they will be lost.
*
* ----------
*
* This is AzaToth's Twinkle, the popular script sidekick for newbies, admins, and
* every Wikipedian in between. Visit [[WP:TW]] for more information.
*/
(function(){if(!Morebits.userIsInGroup("autoconfirmed")&&!Morebits.userIsInGroup("confirmed")){return}const Twinkle={};window.Twinkle=Twinkle;Twinkle.initCallbacks=[];Twinkle.addInitCallback=function twinkleAddInitCallback(func,name){Twinkle.initCallbacks.push({func:func,name:name})};Twinkle.defaultConfig={};Twinkle.defaultConfig={userTalkPageMode:"tab",dialogLargeFont:false,disabledModules:["arv","warn","talkback","xfd","tag"],disabledSysopModules:[],spiWatchReport:"yes",defaultToBlock64:false,defaultToPartialBlocks:false,blankTalkpageOnIndefBlock:false,autoMenuAfterRollback:false,openTalkPage:["norm","vand"],openTalkPageOnAutoRevert:false,rollbackInPlace:false,markRevertedPagesAsMinor:["vand"],watchRevertedPages:["norm","vand","torev"],watchRevertedExpiry:"1 month",offerReasonOnNormalRevert:true,confirmOnRollback:false,confirmOnMobileRollback:true,showRollbackLinks:["diff","others"],SummaryOnVandalRevert:"差し戻し",notifyUserOnDeli:true,deliWatchPage:"1 month",deliWatchUser:"1 month",watchRequestedPages:"yes",watchPPTaggedPages:"default",watchProtectedPages:"default",speedySelectionStyle:"buttonClick",watchSpeedyPages:["g3","g4","g9"],watchSpeedyExpiry:"1 month",markSpeedyPagesAsPatrolled:false,watchSpeedyUser:"1 month",promptForSpeedyDeletionSummary:[],deleteTalkPageOnDelete:false,deleteRedirectsOnDelete:false,deleteSysopDefaultToDelete:true,speedyWindowHeight:500,speedyWindowWidth:800,logSpeedyNominations:false,speedyLogPageName:"CSD log",noLogOnSpeedyNomination:["u1","g3"],unlinkNamespaces:["0","10","100","118"],defaultWarningGroup:"10",combinedSingletMenus:false,showSharedIPNotice:true,watchWarnings:"1 month",oldSelect:false,customWarningList:[],logXfdNominations:false,xfdLogPageName:"XfD log",noLogOnXfdNomination:[],xfdWatchDiscussion:"default",xfdWatchList:"no",xfdWatchPage:"1 month",xfdWatchUser:"1 month",xfdWatchRelated:"1 month",markXfdPagesAsPatrolled:true,autolevelStaleDays:3,revertMaxRevisions:50,batchMax:5e3,batchChunks:50,summaryAd:" ([[H:TW|Twinkle]]使用)",deletionSummaryAd:" ([[H:TW|Twinkle]]使用)",protectionSummaryAd:" ([[H:TW|Twinkle]]使用)",groupByDefault:true,watchTaggedVenues:["articles","drafts","redirects","files"],watchTaggedPages:"1 month",watchMergeDiscussions:"1 month",markTaggedPagesAsMinor:false,markTaggedPagesAsPatrolled:false,tagArticleSortOrder:"cat",customTagList:[],customFileTagList:[],customRedirectTagList:[],markTalkbackAsMinor:true,insertTalkbackSignature:true,talkbackHeading:"New message from "+mw.config.get("wgUserName"),mailHeading:"You've got mail!",markSharedIPAsMinor:true};Twinkle.getPref=function twinkleGetPref(name){const aliases={confirmOnRollback:"confirmOnFluff",confirmOnMobileRollback:"confirmOnMobileFluff",SummaryOnVandalRevert:"vandusersummary"};for(const prefs of[Twinkle.prefs,window.TwinkleConfig,window.FriendlyConfig,Twinkle.defaultConfig]){if(!prefs||typeof prefs!=="object"){continue}const value=prefs[name]!==undefined?prefs[name]:prefs[aliases[name]];if(value!==undefined){return name==="disabledModules"?value.map(module=>module==="fluff"?"rollback":module):value}}};Twinkle.addPortlet=function(){let navigation;let id;let text;let nextnodeid;switch(mw.config.get("skin")){case"vector":case"vector-2022":navigation="#right-navigation";id="p-twinkle";text="TW";nextnodeid="p-cactions";break;case"timeless":navigation="#page-tools .sidebar-inner";id="p-twinkle";text="Twinkle";nextnodeid="p-userpagetools";break;default:navigation=null;id="p-cactions"}if(navigation===null){return id}const root=document.querySelector(navigation);if(!root){return id}const item=document.getElementById(id);if(item){return id}mw.util.addPortlet(id,text,"#"+nextnodeid);if(mw.config.get("skin")==="vector"){$("#p-twinkle").insertAfter("#p-cactions")}else if(mw.config.get("skin")==="vector-2022"){const $landmark=$("#right-navigation > .vector-page-tools-landmark");$("#p-twinkle-dropdown").insertBefore($landmark);if(!$landmark){mw.log.warn("Unexpected change in DOM")}}return id};Twinkle.addPortletLink=function(task,text,id,tooltip){const portletId=Twinkle.addPortlet();const link=mw.util.addPortletLink(portletId,typeof task==="string"?task:"#",text,id,tooltip);$(".client-js .skin-vector #p-cactions").css("margin-right","initial");if(typeof task==="function"){$(link).on("click",ev=>{task();ev.preventDefault()})}if($.collapsibleTabs){$.collapsibleTabs.handleResize()}return link};Morebits.wiki.getCachedPage(`User:${mw.config.get("wgUserName")}/twinkleoptions.js`,0).then(optionsText=>{if(!optionsText){return}optionsText=optionsText.replace(/(?:^(?:\/\/[^\n]*\n)*\n*|(?:\/\/[^\n]*(?:\n|$))*$)/g,"");if(optionsText.lastIndexOf("window.Twinkle.prefs = ",0)===0){optionsText=optionsText.replace(/(?:^window.Twinkle.prefs = |;\n*$)/g,"")}try{const options=JSON.parse(optionsText);if(options){if(options.twinkle||options.friendly){Twinkle.prefs=$.extend(options.twinkle,options.friendly)}else{Twinkle.prefs=options}Twinkle.prefs.optionsVersion=Twinkle.prefs.optionsVersion||1}}catch(e){mw.notify("Twinkleの設定を解析できませんでした",{type:"error"})}}).catch(()=>{console.log("Twinkleの個人設定が読み込めなかったため、デフォルトの個人設定になっています")}).always(()=>{$(Twinkle.load)});Twinkle.load=function(){let activeSpecialPageList=["Block","Contributions","IPContributions","Recentchanges","Recentchangeslinked"];if(Morebits.userIsSysop){activeSpecialPageList=activeSpecialPageList.concat(["DeletedContributions","Prefixindex"])}if(mw.config.get("wgNamespaceNumber")===-1&&!activeSpecialPageList.includes(mw.config.get("wgCanonicalSpecialPageName"))){return}if(window.top!==window.self){return}Morebits.wiki.Api.setApiUserAgent("Twinkle ("+mw.config.get("wgWikiID")+")");Twinkle.disabledModules=Twinkle.getPref("disabledModules").concat(Twinkle.getPref("disabledSysopModules"));Twinkle.addInitCallback=function(func,name){if(!name||!Twinkle.disabledModules.includes(name)){func()}};Twinkle.initCallbacks.forEach(module=>{Twinkle.addInitCallback(module.func,module.name)});if(Twinkle.getPref("dialogLargeFont")){mw.util.addCSS(".morebits-dialog-content, .morebits-dialog-footerlinks { font-size: 100% !important; } "+".morebits-dialog input, .morebits-dialog select, .morebits-dialog-content button { font-size: inherit !important; }")}const isVector=mw.config.get("skin")==="vector"||mw.config.get("skin")==="vector-2022";if(isVector&&Twinkle.getPref("portletType")==="menu"&&$("#p-twinkle").length===0){$("#p-cactions").css("margin-right","initial")}const usingSkinWithDropDownMenu=mw.config.get("skin")==="vector"||mw.config.get("skin")==="vector-2022"||mw.config.get("skin")==="timeless";if(usingSkinWithDropDownMenu){Twinkle.addPortletLink(mw.util.getUrl("User:Syunsyunminmin/Twinkle/Preferences"),"Config","tw-config","Twinkleの設定ページを開く")}};Twinkle.changeTags="";Twinkle.summaryAd=" ([[Help:Twinkle|Twinkle]]使用)";Twinkle.hatnoteRegex="short description|hatnote|main|correct title|dablink|distinguish|for|further|selfref|year dab|similar names|highway detail hatnote|broader|about(?:-distinguish| other people)?|other\\s?(?:hurricane(?: use)?s|people|persons|places|ships|uses(?: of)?)|redirect(?:-(?:distinguish|synonym|multi))?|see\\s?(?:wiktionary|also(?: if exists)?)";Twinkle.getPrefill=function(key){Twinkle.prefill=Twinkle.prefill||{};if(!Object.prototype.hasOwnProperty.call(Twinkle.prefill,key)){Twinkle.prefill[key]=mw.util.getParamValue(key)}return Twinkle.prefill[key]};Twinkle.setPrefill=function(key,value){Twinkle.prefill=Twinkle.prefill||{};Twinkle.prefill[key]=value};Twinkle.makeFindSourcesDiv=function makeSourcesDiv(divID){if(!$(divID).length){return}if(!Twinkle.findSources){const parser=new Morebits.wiki.Preview($(divID)[0]);parser.beginRender("({{Find sources|"+Morebits.pageNameNorm+"}})","WP:AFD").then(()=>{Twinkle.findSources=parser.previewbox.innerHTML;$(divID).removeClass("morebits-previewbox")})}else{$(divID).html(Twinkle.findSources)}};Twinkle.sortByNamespace=function(first,second){return first.ns-second.ns||(first.title>second.title?1:-1)};Twinkle.generateArrowLinks=function(checkbox){const link=Morebits.htmlNode("a"," >");link.setAttribute("class","tw-arrowpage-link");link.setAttribute("href",mw.util.getUrl(checkbox.value));link.setAttribute("target","_blank");checkbox.nextElementSibling.append(link)};Twinkle.generateBatchPageLinks=function(checkbox){const $checkbox=$(checkbox);const link=Morebits.htmlNode("a",$checkbox.val());link.setAttribute("class","tw-batchpage-link");link.setAttribute("href",mw.util.getUrl($checkbox.val()));link.setAttribute("target","_blank");$checkbox.next().prepend([link," "])};Twinkle.removeMoveToCommonsTagsFromWikicode=wikicode=>wikicode.replace(/\{\{(mtc|(copy |move )?to ?commons|move to wikimedia commons|copy to wikimedia commons)(?!( in))[^}]*\}\}/gi,"")})();
// </nowiki>
85dzx3ue2xsf1toopjduhukd22a4lq2
User:Syunsyunminmin/Twinkle/Twinkle.css
2
179389
769454
2026-10-21T13:56:55Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 3680108: init
769454
css
text/css
/*
Twinkle: adapted for Japanese Wikipedia; upstream authors and Japanese adaptations are credited in UPSTREAM.md.
Upstream source: https://github.com/wikimedia-gadgets/twinkle/blob/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/src/twinkle.css
Upstream author history: https://github.com/wikimedia-gadgets/twinkle/commits/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/src/twinkle.css
License: CC BY-SA 3.0 Unported. See https://github.com/wikimedia-gadgets/twinkle/blob/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/LICENSE.txt
Japanese adaptation: Syunsyunminmin; includes later synchronization changes.
*/
/* Define CSS variables */
html {
--twinkle-bgcolor-dialog: #f0f8ff;
--twinkle-bgcolor-titlebar: #bccadf;
}
@media screen {
html.skin-theme-clientpref-night {
--twinkle-bgcolor-dialog: #141c26;
--twinkle-bgcolor-titlebar: #1c2a52;
}
}
/* stylelint-disable-next-line plugin/no-unsupported-browser-features */
@media screen and (prefers-color-scheme: dark) {
html.skin-theme-clientpref-os {
--twinkle-bgcolor-dialog: #141c26;
--twinkle-bgcolor-titlebar: #1c2a52;
}
}
/**
* Explicitly set width of TW menu so that we can use a hidden peer gadget
* to add space where the TW menu would go before it loads. See
* twinkle-pagestyles.css
*/
.skin-vector .vector-menu-dropdown #p-twinkle {
width: 3.24em;
}
/**
* In skin vector-2022, open the TW menu when mouse hovers over it. Credit to
* [[en:User:Nardog]] for this fix.
*/
#p-twinkle:hover > .vector-menu-content {
opacity: 1;
visibility: visible;
height: auto;
}
/* The additional box on user skin.js and twinklepreferences.js pages */
#twinkle-config-headerbox {
border: 1px #f60 solid;
background: #fed;
padding: 0.6em;
margin: 0.5em auto;
text-align: center;
color: inherit;
}
/* twinkleoptions.js */
#twinkle-config-headerbox.config-twopt-box {
font-weight: bold;
width: 80%;
border-width: 2px;
}
/* skin-specific js */
#twinkle-config-headerbox.config-userskin-box {
width: 60%;
}
/* WP:TWPREFS */
#twinkle-config-content input[type=checkbox] {
margin-right: 2px;
}
/* Twinkle config */
#twinkle-config {
border: 1px solid #666;
background: var(--twinkle-bgcolor-dialog);
color: inherit;
}
#twinkle-config label {
color: var(--color-base, #202122);
}
#twinkle-config-titlebar {
background: var(--twinkle-bgcolor-titlebar);
color: inherit;
}
#twinkle-config-buttonpane {
background: var(--twinkle-bgcolor-titlebar);
color: inherit;
padding: 0.5em;
}
.twinkle-config-helptip {
font-size: 90%;
color: var(--color-subtle, #54595d);
}
/* Flexbox gap not supported by old Safari versions. Rendering without gap is okay. */
/* stylelint-disable-next-line plugin/no-unsupported-browser-features */
.twinkle-ombox {
display: flex;
align-items: center;
gap: 0.9em;
padding-left: 0.9em;
margin: 4px 10%;
background: var(--background-color-neutral-subtle, #f8f9fa);
border: 1px solid #f28500;
}
#p-twinkle-dropdown {
display: flex;
min-width: 40px;
}
@media screen {
html.skin-theme-clientpref-night #twinkle-config-headerbox {
background: #532b18;
}
}
/* stylelint-disable-next-line plugin/no-unsupported-browser-features */
@media screen and (prefers-color-scheme: dark) {
html.skin-theme-clientpref-os #twinkle-config-headerbox {
background: #532b18;
}
}
k5vd24qhkx0kz09ng2g8ibjrlc51dyy
769521
769454
2026-10-21T23:59:40Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 1406828:
769521
css
text/css
/* Define CSS variables */
html {
--twinkle-bgcolor-dialog: #f0f8ff;
--twinkle-bgcolor-titlebar: #bccadf;
}
@media screen {
html.skin-theme-clientpref-night {
--twinkle-bgcolor-dialog: #141c26;
--twinkle-bgcolor-titlebar: #1c2a52;
}
}
/* stylelint-disable-next-line plugin/no-unsupported-browser-features */
@media screen and (prefers-color-scheme: dark) {
html.skin-theme-clientpref-os {
--twinkle-bgcolor-dialog: #141c26;
--twinkle-bgcolor-titlebar: #1c2a52;
}
}
/**
* Explicitly set width of TW menu so that we can use a hidden peer gadget
* to add space where the TW menu would go before it loads. See
* twinkle-pagestyles.css
*/
.skin-vector .vector-menu-dropdown #p-twinkle {
width: 3.24em;
}
/**
* In skin vector-2022, open the TW menu when mouse hovers over it. Credit to
* [[en:User:Nardog]] for this fix.
*/
#p-twinkle:hover > .vector-menu-content {
opacity: 1;
visibility: visible;
height: auto;
}
/* The additional box on user skin.js and twinklepreferences.js pages */
#twinkle-config-headerbox {
border: 1px #f60 solid;
background: #fed;
padding: 0.6em;
margin: 0.5em auto;
text-align: center;
color: inherit;
}
/* twinkleoptions.js */
#twinkle-config-headerbox.config-twopt-box {
font-weight: bold;
width: 80%;
border-width: 2px;
}
/* skin-specific js */
#twinkle-config-headerbox.config-userskin-box {
width: 60%;
}
/* WP:TWPREFS */
#twinkle-config-content input[type=checkbox] {
margin-right: 2px;
}
/* Twinkle config */
#twinkle-config {
border: 1px solid #666;
background: var(--twinkle-bgcolor-dialog);
color: inherit;
}
#twinkle-config label {
color: var(--color-base, #202122);
}
#twinkle-config-titlebar {
background: var(--twinkle-bgcolor-titlebar);
color: inherit;
}
#twinkle-config-buttonpane {
background: var(--twinkle-bgcolor-titlebar);
color: inherit;
padding: 0.5em;
}
.twinkle-config-helptip {
font-size: 90%;
color: var(--color-subtle, #54595d);
}
/* Flexbox gap not supported by old Safari versions. Rendering without gap is okay. */
/* stylelint-disable-next-line plugin/no-unsupported-browser-features */
.twinkle-ombox {
display: flex;
align-items: center;
gap: 0.9em;
padding-left: 0.9em;
margin: 4px 10%;
background: var(--background-color-neutral-subtle, #f8f9fa);
border: 1px solid #f28500;
}
#p-twinkle-dropdown {
display: flex;
min-width: 40px;
}
@media screen {
html.skin-theme-clientpref-night #twinkle-config-headerbox {
background: #532b18;
}
}
/* stylelint-disable-next-line plugin/no-unsupported-browser-features */
@media screen and (prefers-color-scheme: dark) {
html.skin-theme-clientpref-os #twinkle-config-headerbox {
background: #532b18;
}
}
2cj8reae8okbk095bxwtivuixi81br5
769559
769521
2026-10-22T02:14:33Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at d9cd3461baca43ac99ac2c24fb76a3aae6ddada9: minify
769559
css
text/css
/* Define CSS variables */html{--twinkle-bgcolor-dialog:#f0f8ff;--twinkle-bgcolor-titlebar:#bccadf}@media screen{html.skin-theme-clientpref-night{--twinkle-bgcolor-dialog:#141c26;--twinkle-bgcolor-titlebar:#1c2a52}}/* stylelint-disable-next-line plugin/no-unsupported-browser-features */@media screen and (prefers-color-scheme: dark){html.skin-theme-clientpref-os{--twinkle-bgcolor-dialog:#141c26;--twinkle-bgcolor-titlebar:#1c2a52}}/**
* Explicitly set width of TW menu so that we can use a hidden peer gadget
* to add space where the TW menu would go before it loads. See
* twinkle-pagestyles.css
*/.skin-vector .vector-menu-dropdown #p-twinkle{width:3.24em}/**
* In skin vector-2022, open the TW menu when mouse hovers over it. Credit to
* [[en:User:Nardog]] for this fix.
*/#p-twinkle:hover > .vector-menu-content{opacity:1;visibility:visible;height:auto}/* The additional box on user skin.js and twinklepreferences.js pages */#twinkle-config-headerbox{border:1px #f60 solid;background:#fed;padding:0.6em;margin:0.5em auto;text-align:center;color:inherit}/* twinkleoptions.js */#twinkle-config-headerbox.config-twopt-box{font-weight:bold;width:80%;border-width:2px}/* skin-specific js */#twinkle-config-headerbox.config-userskin-box{width:60%}/* WP:TWPREFS */#twinkle-config-content input[type=checkbox]{margin-right:2px}/* Twinkle config */#twinkle-config{border:1px solid #666;background:var(--twinkle-bgcolor-dialog);color:inherit}#twinkle-config label{color:var(--color-base, #202122)}#twinkle-config-titlebar{background:var(--twinkle-bgcolor-titlebar);color:inherit}#twinkle-config-buttonpane{background:var(--twinkle-bgcolor-titlebar);color:inherit;padding:0.5em}.twinkle-config-helptip{font-size:90%;color:var(--color-subtle, #54595d)}/* Flexbox gap not supported by old Safari versions. Rendering without gap is okay. *//* stylelint-disable-next-line plugin/no-unsupported-browser-features */.twinkle-ombox{display:flex;align-items:center;gap:0.9em;padding-left:0.9em;margin:4px 10%;background:var(--background-color-neutral-subtle, #f8f9fa);border:1px solid #f28500}#p-twinkle-dropdown{display:flex;min-width:40px}@media screen{html.skin-theme-clientpref-night #twinkle-config-headerbox{background:#532b18}}/* stylelint-disable-next-line plugin/no-unsupported-browser-features */@media screen and (prefers-color-scheme: dark){html.skin-theme-clientpref-os #twinkle-config-headerbox{background:#532b18}}
e06x6v5wzx4jtt087issukqtj6y6xai
User:Syunsyunminmin/Twinkle/Twinkle-pagestyles.css
2
179390
769455
2026-10-21T13:56:59Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 3680108: init
769455
css
text/css
/*
Twinkle: adapted for Japanese Wikipedia; upstream authors and Japanese adaptations are credited in UPSTREAM.md.
Upstream source: https://github.com/wikimedia-gadgets/twinkle/blob/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/src/twinkle-pagestyles.css
Upstream author history: https://github.com/wikimedia-gadgets/twinkle/commits/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/src/twinkle-pagestyles.css
License: CC BY-SA 3.0 Unported. See https://github.com/wikimedia-gadgets/twinkle/blob/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/LICENSE.txt
Japanese adaptation: Syunsyunminmin; includes later synchronization changes.
*/
/**
* This is loaded as a hidden peer gadget of Twinkle.
* Why is this a peer gadget? Because it will load before the page finishes its first paint.
* It adds whitespace where the "TW" menu goes in the Vector 2010 skin, basically reserving
* a spot for the "TW" menu so that the top bar does not "jump".
* The "TW" menu itself is painted later, after the gadget JS files finish loading.
*/
.client-js > body.skin-vector:not(.ns-special) #p-cactions,
.client-js > body.skin-vector.mw-special-Block #p-cactions,
.client-js > body.skin-vector.mw-special-Contributions #p-cactions,
.client-js > body.skin-vector.mw-special-IPContributions #p-cactions,
.client-js > body.skin-vector.mw-special-DeletedContributions #p-cactions,
.client-js > body.skin-vector.mw-special-Prefixindex #p-cactions {
margin-right: 3.24em;
}
cbhnjcnfxte5yjkkchx1ifzvodep3v7
769522
769455
2026-10-21T23:59:45Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 1406828:
769522
css
text/css
/**
* This is loaded as a hidden peer gadget of Twinkle.
* Why is this a peer gadget? Because it will load before the page finishes its first paint.
* It adds whitespace where the "TW" menu goes in the Vector 2010 skin, basically reserving
* a spot for the "TW" menu so that the top bar does not "jump".
* The "TW" menu itself is painted later, after the gadget JS files finish loading.
*/
.client-js > body.skin-vector:not(.ns-special) #p-cactions,
.client-js > body.skin-vector.mw-special-Block #p-cactions,
.client-js > body.skin-vector.mw-special-Contributions #p-cactions,
.client-js > body.skin-vector.mw-special-IPContributions #p-cactions,
.client-js > body.skin-vector.mw-special-DeletedContributions #p-cactions,
.client-js > body.skin-vector.mw-special-Prefixindex #p-cactions {
margin-right: 3.24em;
}
4e6mflsjmyw3ok9t8q315j2l85q4ltp
769560
769522
2026-10-22T02:15:34Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at d9cd3461baca43ac99ac2c24fb76a3aae6ddada9: minify
769560
css
text/css
/**
* This is loaded as a hidden peer gadget of Twinkle.
* Why is this a peer gadget? Because it will load before the page finishes its first paint.
* It adds whitespace where the "TW" menu goes in the Vector 2010 skin, basically reserving
* a spot for the "TW" menu so that the top bar does not "jump".
* The "TW" menu itself is painted later, after the gadget JS files finish loading.
*/.client-js > body.skin-vector:not(.ns-special) #p-cactions,.client-js > body.skin-vector.mw-special-Block #p-cactions,.client-js > body.skin-vector.mw-special-Contributions #p-cactions,.client-js > body.skin-vector.mw-special-IPContributions #p-cactions,.client-js > body.skin-vector.mw-special-DeletedContributions #p-cactions,.client-js > body.skin-vector.mw-special-Prefixindex #p-cactions{margin-right:3.24em}
rsmid763txrh87i1vtzn7oeoe8i1wwc
User:Syunsyunminmin/morebits/morebits.js
2
179391
769456
2026-10-21T13:57:04Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 3680108:
769456
javascript
text/javascript
/*
Twinkle: adapted for Japanese Wikipedia; upstream authors and Japanese adaptations are credited in UPSTREAM.md.
Upstream source: https://github.com/wikimedia-gadgets/twinkle/blob/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/src/morebits.js
Upstream author history: https://github.com/wikimedia-gadgets/twinkle/commits/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/src/morebits.js
License: CC BY-SA 3.0 Unported. See https://github.com/wikimedia-gadgets/twinkle/blob/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/LICENSE.txt
Japanese adaptation: Syunsyunminmin; includes later synchronization changes.
*/
// <nowiki>
/**
* A library full of lots of goodness for user scripts on MediaWiki wikis, including Wikipedia.
*
* The highlights include:
* - {@link Morebits.wiki.Api} - make calls to the MediaWiki API
* - {@link Morebits.wiki.Page} - modify pages on the wiki (edit, revert, delete, etc.)
* - {@link Morebits.Date} - enhanced date object processing, sort of a light moment.js
* - {@link Morebits.QuickForm} - generate quick HTML forms on the fly
* - {@link Morebits.SimpleWindow} - generate dialog windows and modals
* - {@link Morebits.Status} - a rough-and-ready status message displayer, used by the Morebits.wiki classes
* - {@link Morebits.wikitext} - utilities for dealing with wikitext
* - {@link Morebits.string} - utilities for manipulating strings
* - {@link Morebits.array} - utilities for manipulating arrays
* - {@link Morebits.ip} - utilities to help process IP addresses
*
* Dependencies:
* - The whole thing relies on jQuery. But most wikis should provide this by default.
* - {@link Morebits.QuickForm}, {@link Morebits.SimpleWindow}, and {@link Morebits.Status} rely on the "morebits.css" file for their styling.
* - To create a gadget based on morebits.js, use this syntax in MediaWiki:Gadgets-definition:
* - `*GadgetName[ResourceLoader|dependencies=mediawiki.user,mediawiki.util,mediawiki.Title]|morebits.js|morebits.css|GadgetName.js`
* - Alternatively, you can configure morebits.js as a hidden gadget in MediaWiki:Gadgets-definition:
* - `*morebits[ResourceLoader|dependencies=mediawiki.user,mediawiki.util,mediawiki.Title|hidden]|morebits.js|morebits.css`
* and then load ext.gadget.morebits as one of the dependencies for the new gadget.
*
* All the stuff here works on all browsers for which MediaWiki provides JavaScript support.
*
* This library is maintained by the maintainers of Twinkle.
* For queries, suggestions, help, etc., head to [Wikipedia talk:Twinkle on English Wikipedia](http://en.wikipedia.org/wiki/WT:TW).
* The latest development source is available at {@link https://github.com/wikimedia-gadgets/twinkle/blob/master/src/morebits.js|GitHub}.
*
* @namespace Morebits
*/
(function() {
/** @lends Morebits */
const Morebits = {};
window.Morebits = Morebits; // allow global access
/**
* Wiki-specific configurations for Morebits
*/
Morebits.l10n = {
/**
* Local aliases for "redirect" magic word.
* Check using api.php?action=query&format=json&meta=siteinfo&formatversion=2&siprop=magicwords
*/
redirectTagAliases: ['#REDIRECT'],
/**
* Takes a string as argument and checks if it is a timestamp or not
* If not, it returns null. If yes, it returns an array of integers
* in the format [year, month, date, hour, minute, second]
* which can be passed to Date.UTC()
*
* @param {string} str
* @return {number[] | null}
*/
signatureTimestampFormat: function(str) {
// Japanese Wikipedia signatures use UTC with year/month/day before the time.
const japanese = /(\d{4})年(\d{1,2})月(\d{1,2})日\s*\([日月火水木金土]\)\s*(\d{1,2}):(\d{2})\s*\(UTC\)/.exec(str);
if (japanese) {
return [japanese[1], japanese[2] - 1, japanese[3], japanese[4], japanese[5]];
}
// HH:mm, DD Month YYYY (UTC)
const rgx = /(\d{2}):(\d{2}), (\d{1,2}) (\w+) (\d{4}) \(UTC\)/;
const match = rgx.exec(str);
if (!match) {
return null;
}
const month = ['January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'].indexOf(match[4]);
if (month === -1) {
return null;
}
// ..... year ... month .. date ... hour .... minute
return [match[5], month, match[3], match[1], match[2]];
}
};
/**
* Simple helper function to see what groups a user might belong.
*
* @param {string} group - e.g. `sysop`, `extendedconfirmed`, etc.
* @return {boolean}
*/
Morebits.userIsInGroup = function(group) {
return mw.config.get('wgUserGroups').includes(group);
};
/**
* Hardcodes whether the user is a sysop, used a lot.
*
* @type {boolean}
*/
Morebits.userIsSysop = Morebits.userIsInGroup('sysop');
/**
* Determines whether the current page is a redirect or soft redirect. Fails
* to detect soft redirects on edit, history, etc. pages. Will attempt to
* detect Module:RfD, with the same failure points.
*
* @return {boolean}
*/
Morebits.isPageRedirect = function() {
return !!(mw.config.get('wgIsRedirect') || document.getElementById('softredirect') || $('.box-RfD').length);
};
/**
* Stores a normalized (underscores converted to spaces) version of the
* `wgPageName` variable.
*
* @type {string}
*/
Morebits.pageNameNorm = mw.config.get('wgPageName').replace(/_/g, ' ');
/**
* Create a string for use in regex matching a page name. Accounts for
* leading character's capitalization, underscores as spaces, and special
* characters being escaped. See also {@link Morebits.namespaceRegex}.
*
* @param {string} pageName - Page name without namespace.
* @return {string} - For a page name `Foo bar`, returns the string `[Ff]oo[_ ]bar`.
*/
Morebits.pageNameRegex = function(pageName) {
if (pageName === '') {
return '';
}
const firstChar = pageName[0],
remainder = Morebits.string.escapeRegExp(pageName.slice(1));
if (mw.Title.phpCharToUpper(firstChar) !== firstChar.toLowerCase()) {
return '[' + mw.Title.phpCharToUpper(firstChar) + firstChar.toLowerCase() + ']' + remainder;
}
return Morebits.string.escapeRegExp(firstChar) + remainder;
};
/**
* Converts string or array of DOM nodes into an HTML fragment.
* Wikilink syntax (`[[...]]`) is transformed into HTML anchor.
* Used in Morebits.QuickForm and Morebits.Status
*
* @internal
* @param {string|Node|(string|Node)[]} input
* @return {DocumentFragment}
*/
Morebits.createHtml = function(input) {
const fragment = document.createDocumentFragment();
if (!input) {
return fragment;
}
if (!Array.isArray(input)) {
input = [input];
}
for (let i = 0; i < input.length; ++i) {
if (input[i] instanceof Node) {
fragment.appendChild(input[i]);
} else {
$.parseHTML(Morebits.createHtml.renderWikilinks(input[i])).forEach((node) => {
fragment.appendChild(node);
});
}
}
return fragment;
};
/**
* Converts wikilinks to HTML anchor tags.
*
* @param {string} text
* @return {string}
*/
Morebits.createHtml.renderWikilinks = function(text) {
const ub = new Morebits.Unbinder(text);
// Don't convert wikilinks within code tags as they're used for displaying wiki-code
ub.unbind('<code>', '</code>');
ub.content = ub.content.replace(
/\[\[:?(?:([^|\]]+?)\|)?([^\]|]+?)\]\]/g,
(_, target, text) => {
if (!target) {
target = text;
}
return '<a target="_blank" href="' + mw.util.getUrl(target) +
'" title="' + target.replace(/"/g, '"') + '">' + text + '</a>';
});
return ub.rebind();
};
/**
* Create a string for use in regex matching all namespace aliases, regardless
* of the capitalization and underscores/spaces. Doesn't include the optional
* leading `:`, but if there's more than one item, wraps the list in a
* non-capturing group. This means you can do `Morebits.namespaceRegex([4]) +
* ':' + Morebits.pageNameRegex('Twinkle')` to match a full page. Uses
* {@link Morebits.pageNameRegex}.
*
* @param {number[]} namespaces - Array of namespace numbers. Unused/invalid
* namespace numbers are silently discarded.
* @example
* // returns '(?:[Ff][Ii][Ll][Ee]|[Ii][Mm][Aa][Gg][Ee])'
* Morebits.namespaceRegex([6])
* @return {string} - Regex-suitable string of all namespace aliases.
*/
Morebits.namespaceRegex = function(namespaces) {
if (!Array.isArray(namespaces)) {
namespaces = [namespaces];
}
const aliases = [];
let regex;
$.each(mw.config.get('wgNamespaceIds'), (name, number) => {
if (namespaces.includes(number)) {
// Namespaces are completely agnostic as to case,
// and a regex string is more useful/compatible than a RegExp object,
// so we accept any casing for any letter.
aliases.push(name.split('').map((char) => Morebits.pageNameRegex(char)).join(''));
}
});
switch (aliases.length) {
case 0:
regex = '';
break;
case 1:
regex = aliases[0];
break;
default:
regex = '(?:' + aliases.join('|') + ')';
break;
}
return regex;
};
/* **************** Morebits.QuickForm **************** */
/**
* Creation of simple and standard forms without much specific coding.
*
* @namespace Morebits.QuickForm
* @memberof Morebits
* @class
* @param {event} event - Function to execute when form is submitted.
* @param {string} [eventType=submit] - Type of the event.
*/
Morebits.QuickForm = function QuickForm(event, eventType) {
this.root = new Morebits.QuickForm.Element({ type: 'form', event: event, eventType: eventType });
};
/**
* Renders the HTML output of the quickForm.
*
* @memberof Morebits.QuickForm
* @return {HTMLElement}
*/
Morebits.QuickForm.prototype.render = function QuickFormRender() {
const ret = this.root.render();
ret.names = {};
return ret;
};
/**
* Append element to the form.
*
* @memberof Morebits.QuickForm
* @param {(object|Morebits.QuickForm.Element)} data - A quickform element, or the object with which
* a quickform element is constructed.
* @return {Morebits.QuickForm.Element} - Same as what is passed to the function.
*/
Morebits.QuickForm.prototype.append = function QuickFormAppend(data) {
return this.root.append(data);
};
/**
* Create a new element for the the form.
*
* Index to Morebits.QuickForm.Element types:
* - Global attributes: id, className, style, tooltip, extra, $data, adminonly
* - `select`: A combo box (aka drop-down).
* - Attributes: name, label, multiple, size, list, event, disabled
* - `option`: An element for a combo box.
* - Attributes: value, label, selected, disabled
* - `optgroup`: A group of "option"s.
* - Attributes: label, list
* - `field`: A fieldset (aka group box).
* - Attributes: name, label, disabled
* - `checkbox`: A checkbox. Must use "list" parameter.
* - Attributes: name, list, event
* - Attributes (within list): name, label, value, checked, disabled, event, subgroup
* - `radio`: A radio button. Must use "list" parameter.
* - Attributes: name, list, event
* - Attributes (within list): name, label, value, checked, disabled, event, subgroup
* - `input`: A text input box.
* - Attributes: name, label, value, size, placeholder, maxlength, disabled, required, readonly, event
* - `number`: A number input box.
* - Attributes: Everything the text `input` has, as well as: min, max, step, list
* - `dyninput`: A set of text boxes with "Remove" buttons and an "Add" button.
* - Attributes: name, label, min, max, inputs, sublabel, value, size, maxlength, event
* - `hidden`: An invisible form field.
* - Attributes: name, value
* - `header`: A level 5 header.
* - Attributes: label
* - `div`: A generic placeholder element or label.
* - Attributes: name, label
* - `submit`: A submit button. Morebits.SimpleWindow moves these to the footer of the dialog.
* - Attributes: name, label, disabled
* - `button`: A generic button.
* - Attributes: name, label, disabled, event
* - `textarea`: A big, multi-line text box.
* - Attributes: name, label, value, cols, rows, disabled, required, readonly
* - `fragment`: A DocumentFragment object.
* - No attributes, and no global attributes except adminonly.
* There is some difference on how types handle the `label` attribute:
* - `div`, `select`, `field`, `checkbox`/`radio`, `input`, `textarea`, `header`, and `dyninput` can accept an array of items,
* and the label item(s) can be `Element`s.
* - `option`, `optgroup`, `_dyninput_cell`, `submit`, and `button` accept only a single string.
*
* @memberof Morebits.QuickForm
* @class
* @param {Object} data - Object representing the quickform element. Should
* specify one of the available types from the index above, as well as any
* relevant and available attributes.
* @example new Morebits.QuickForm.Element({
* name: 'target',
* type: 'input',
* label: 'Your target:',
* tooltip: 'Enter your target. Required.',
* required: true
* });
*/
Morebits.QuickForm.Element = function QuickFormElement(data) {
this.data = data;
this.childs = [];
};
/**
* @memberof Morebits.QuickForm.Element
* @type {number}
*/
Morebits.QuickForm.Element.id = 0;
/**
* Appends an element to current element.
*
* @memberof Morebits.QuickForm.Element
* @param {Morebits.QuickForm.Element} data - A quickForm element or the object required to
* create the quickForm element.
* @return {Morebits.QuickForm.Element} The same element passed in.
*/
Morebits.QuickForm.Element.prototype.append = function QuickFormElementAppend(data) {
let child;
if (data instanceof Morebits.QuickForm.Element) {
child = data;
} else {
child = new Morebits.QuickForm.Element(data);
}
this.childs.push(child);
return child;
};
/**
* Renders the HTML output for the quickForm element. This should be called
* without parameters: `form.render()`.
*
* @memberof Morebits.QuickForm.Element
* @return {HTMLElement}
*/
Morebits.QuickForm.Element.prototype.render = function QuickFormElementRender(internal_subgroup_id) {
const currentNode = this.compute(this.data, internal_subgroup_id);
for (let i = 0; i < this.childs.length; ++i) {
// do not pass internal_subgroup_id to recursive calls
currentNode[1].appendChild(this.childs[i].render());
}
return currentNode[0];
};
/** @memberof Morebits.QuickForm.Element */
Morebits.QuickForm.Element.prototype.compute = function QuickFormElementCompute(data, in_id) {
let node;
let childContainer = null;
let label;
const id = (in_id ? in_id + '_' : '') + 'node_' + Morebits.QuickForm.Element.id++;
if (data.adminonly && !Morebits.userIsSysop) {
// hell hack alpha
data.type = 'hidden';
}
let i, current, subnode;
switch (data.type) {
case 'form':
node = document.createElement('form');
node.className = 'quickform';
node.setAttribute('action', 'javascript:void(0);');
if (data.event) {
node.addEventListener(data.eventType || 'submit', data.event, false);
}
break;
case 'fragment':
node = document.createDocumentFragment();
// fragments can't have any attributes, so just return it straight away
return [node, node];
// Sometimes Twinkle uses fancy searchable "select" elements. This is powered by the third party library "select2". Activate it by creating a Morebits "select" element, then call `$('select[name=sub_group]').select2({});` or similar towards the end of your main code.
case 'select':
node = document.createElement('div');
node.setAttribute('id', 'div_' + id);
if (data.label) {
label = node.appendChild(document.createElement('label'));
label.setAttribute('for', id);
label.appendChild(Morebits.createHtml(data.label));
label.style.marginRight = '3px';
}
var select = node.appendChild(document.createElement('select'));
if (data.event) {
select.addEventListener('change', data.event, false);
}
if (data.multiple) {
select.setAttribute('multiple', 'multiple');
}
if (data.size) {
select.setAttribute('size', data.size);
}
if (data.disabled) {
select.setAttribute('disabled', 'disabled');
}
select.setAttribute('name', data.name);
if (data.list) {
for (i = 0; i < data.list.length; ++i) {
current = data.list[i];
if (current.list) {
current.type = 'optgroup';
} else {
current.type = 'option';
}
subnode = this.compute(current);
select.appendChild(subnode[0]);
}
}
childContainer = select;
break;
case 'option':
node = document.createElement('option');
node.values = data.value;
node.setAttribute('value', data.value);
if (data.selected) {
node.setAttribute('selected', 'selected');
}
if (data.disabled) {
node.setAttribute('disabled', 'disabled');
}
node.setAttribute('label', data.label);
node.appendChild(document.createTextNode(data.label));
break;
case 'optgroup':
node = document.createElement('optgroup');
node.setAttribute('label', data.label);
if (data.list) {
for (i = 0; i < data.list.length; ++i) {
current = data.list[i];
current.type = 'option'; // must be options here
subnode = this.compute(current);
node.appendChild(subnode[0]);
}
}
break;
case 'field':
node = document.createElement('fieldset');
label = node.appendChild(document.createElement('legend'));
label.appendChild(Morebits.createHtml(data.label));
if (data.name) {
node.setAttribute('name', data.name);
}
if (data.disabled) {
node.setAttribute('disabled', 'disabled');
}
break;
case 'checkbox':
case 'radio':
node = document.createElement('div');
if (data.list) {
for (i = 0; i < data.list.length; ++i) {
const cur_id = id + '_' + i;
current = data.list[i];
var cur_div;
if (current.type === 'header') {
// inline hack
cur_div = node.appendChild(document.createElement('h6'));
cur_div.appendChild(document.createTextNode(current.label));
if (current.tooltip) {
Morebits.QuickForm.Element.generateTooltip(cur_div, current);
}
continue;
}
cur_div = node.appendChild(document.createElement('div'));
subnode = cur_div.appendChild(document.createElement('input'));
subnode.values = current.value;
subnode.setAttribute('value', current.value);
subnode.setAttribute('type', data.type);
subnode.setAttribute('id', cur_id);
subnode.setAttribute('name', current.name || data.name);
// If name is provided on the individual checkbox, add a data-single
// attribute which indicates it isn't part of a list of checkboxes with
// same name. Used in getInputData()
if (current.name) {
subnode.setAttribute('data-single', 'data-single');
}
if (current.checked) {
subnode.setAttribute('checked', 'checked');
}
if (current.disabled) {
subnode.setAttribute('disabled', 'disabled');
}
label = cur_div.appendChild(document.createElement('label'));
label.appendChild(Morebits.createHtml(current.label));
label.setAttribute('for', cur_id);
if (current.tooltip) {
Morebits.QuickForm.Element.generateTooltip(label, current);
}
// styles go on the label, doesn't make sense to style a checkbox/radio
if (current.style) {
label.setAttribute('style', current.style);
}
var event;
if (current.subgroup) {
let tmpgroup = current.subgroup;
if (!Array.isArray(tmpgroup)) {
tmpgroup = [tmpgroup];
}
var subgroupRaw = new Morebits.QuickForm.Element({
type: 'div',
id: id + '_' + i + '_subgroup'
});
$.each(tmpgroup, (idx, el) => {
const newEl = $.extend({}, el);
if (!newEl.type) {
newEl.type = data.type;
}
newEl.name = (current.name || data.name) + '.' + newEl.name;
subgroupRaw.append(newEl);
});
const subgroup = subgroupRaw.render(cur_id);
subgroup.className = 'quickformSubgroup';
subnode.subgroup = subgroup;
subnode.shown = false;
event = function(e) {
if (e.target.checked) {
e.target.parentNode.appendChild(e.target.subgroup);
if (e.target.type === 'radio') {
const name = e.target.name;
if (e.target.form.names[name] !== undefined) {
e.target.form.names[name].parentNode.removeChild(e.target.form.names[name].subgroup);
}
e.target.form.names[name] = e.target;
}
} else {
e.target.parentNode.removeChild(e.target.subgroup);
}
};
subnode.addEventListener('change', event, true);
if (current.checked) {
subnode.parentNode.appendChild(subgroup);
}
} else if (data.type === 'radio') {
event = function(e) {
if (e.target.checked) {
const name = e.target.name;
if (e.target.form.names[name] !== undefined) {
e.target.form.names[name].parentNode.removeChild(e.target.form.names[name].subgroup);
}
delete e.target.form.names[name];
}
};
subnode.addEventListener('change', event, true);
}
// add users' event last, so it can interact with the subgroup
if (data.event) {
subnode.addEventListener('change', data.event, false);
} else if (current.event) {
subnode.addEventListener('change', current.event, true);
}
}
}
if (data.shiftClickSupport && data.type === 'checkbox') {
Morebits.checkboxShiftClickSupport(Morebits.QuickForm.getElements(node, data.name));
}
break;
// input is actually a text-type, so number here inherits the same stuff
case 'number':
case 'input':
node = document.createElement('div');
node.setAttribute('id', 'div_' + id);
if (data.label) {
label = node.appendChild(document.createElement('label'));
label.appendChild(Morebits.createHtml(data.label));
label.setAttribute('for', data.id || id);
label.style.marginRight = '3px';
}
subnode = node.appendChild(document.createElement('input'));
subnode.setAttribute('name', data.name);
if (data.type === 'input') {
subnode.setAttribute('type', 'text');
} else {
subnode.setAttribute('type', 'number');
['min', 'max', 'step', 'list'].forEach((att) => {
if (data[att]) {
subnode.setAttribute(att, data[att]);
}
});
}
['value', 'size', 'placeholder', 'maxlength'].forEach((att) => {
if (data[att]) {
subnode.setAttribute(att, data[att]);
}
});
['disabled', 'required', 'readonly'].forEach((att) => {
if (data[att]) {
subnode.setAttribute(att, att);
}
});
if (data.event) {
subnode.addEventListener('input', data.event, false);
}
childContainer = subnode;
break;
case 'dyninput':
var min = data.min || 1;
var max = data.max || Infinity;
node = document.createElement('div');
label = node.appendChild(document.createElement('h5'));
label.appendChild(Morebits.createHtml(data.label));
var listNode = node.appendChild(document.createElement('div'));
var more = this.compute({
type: 'button',
label: 'more',
disabled: min >= max,
event: function(e) {
const new_node = new Morebits.QuickForm.Element(e.target.sublist);
e.target.area.appendChild(new_node.render());
if (++e.target.counter >= e.target.max) {
e.target.setAttribute('disabled', 'disabled');
}
e.stopPropagation();
}
});
node.appendChild(more[0]);
var moreButton = more[1];
var sublist = {
type: '_dyninput_row',
remove: false,
maxlength: data.maxlength,
event: data.event,
inputs: data.inputs || [{
// compatibility
label: data.sublabel || data.label,
name: data.name,
value: data.value,
size: data.size
}]
};
for (i = 0; i < min; ++i) {
const elem = new Morebits.QuickForm.Element(sublist);
listNode.appendChild(elem.render());
}
sublist.remove = true;
sublist.morebutton = moreButton;
sublist.listnode = listNode;
moreButton.sublist = sublist;
moreButton.area = listNode;
moreButton.max = max - min;
moreButton.counter = 0;
break;
case '_dyninput_row': // Private
node = document.createElement('div');
data.inputs.forEach((subdata) => {
const cell = new Morebits.QuickForm.Element($.extend(subdata, { type: '_dyninput_cell' }));
node.appendChild(cell.render());
});
if (data.remove) {
const remove = this.compute({
type: 'button',
label: 'remove',
event: function(e) {
const list = e.target.listnode;
const node = e.target.inputnode;
const more = e.target.morebutton;
list.removeChild(node);
--more.counter;
more.removeAttribute('disabled');
e.stopPropagation();
}
});
node.appendChild(remove[0]);
const removeButton = remove[1];
removeButton.inputnode = node;
removeButton.listnode = data.listnode;
removeButton.morebutton = data.morebutton;
}
break;
case '_dyninput_cell': // Private, similar to normal input
node = document.createElement('span');
if (data.label) {
label = node.appendChild(document.createElement('label'));
label.appendChild(document.createTextNode(data.label));
label.setAttribute('for', id + '_input');
label.style.marginRight = '3px';
}
subnode = node.appendChild(document.createElement('input'));
subnode.setAttribute('id', id + '_input');
if (data.value) {
subnode.setAttribute('value', data.value);
}
subnode.setAttribute('name', data.name);
subnode.setAttribute('type', 'text');
subnode.setAttribute('data-dyninput', 'data-dyninput');
if (data.size) {
subnode.setAttribute('size', data.size);
}
if (data.maxlength) {
subnode.setAttribute('maxlength', data.maxlength);
}
if (data.required) {
subnode.setAttribute('required', 'required');
}
if (data.disabled) {
subnode.setAttribute('required', 'disabled');
}
if (data.event) {
subnode.addEventListener('input', data.event, false);
}
node.style.marginRight = '3px';
break;
case 'hidden':
node = document.createElement('input');
node.setAttribute('type', 'hidden');
node.values = data.value;
node.setAttribute('value', data.value);
node.setAttribute('name', data.name);
break;
case 'header':
node = document.createElement('h5');
node.appendChild(Morebits.createHtml(data.label));
break;
case 'div':
node = document.createElement('div');
if (data.name) {
node.setAttribute('name', data.name);
}
if (data.label) {
const result = document.createElement('span');
result.className = 'quickformDescription';
result.appendChild(Morebits.createHtml(data.label));
node.appendChild(result);
}
break;
case 'submit':
node = document.createElement('span');
childContainer = node.appendChild(document.createElement('input'));
childContainer.setAttribute('type', 'submit');
if (data.label) {
childContainer.setAttribute('value', data.label);
}
childContainer.setAttribute('name', data.name || 'submit');
if (data.disabled) {
childContainer.setAttribute('disabled', 'disabled');
}
break;
case 'button':
node = document.createElement('span');
childContainer = node.appendChild(document.createElement('input'));
childContainer.setAttribute('type', 'button');
if (data.label) {
childContainer.setAttribute('value', data.label);
}
childContainer.setAttribute('name', data.name);
if (data.disabled) {
childContainer.setAttribute('disabled', 'disabled');
}
if (data.event) {
childContainer.addEventListener('click', data.event, false);
}
break;
case 'textarea':
node = document.createElement('div');
node.setAttribute('id', 'div_' + id);
if (data.label) {
label = node.appendChild(document.createElement('h5'));
const labelElement = document.createElement('label');
labelElement.appendChild(Morebits.createHtml(data.label));
labelElement.setAttribute('for', data.id || id);
label.appendChild(labelElement);
}
subnode = node.appendChild(document.createElement('textarea'));
subnode.setAttribute('name', data.name);
if (data.cols) {
subnode.setAttribute('cols', data.cols);
}
if (data.rows) {
subnode.setAttribute('rows', data.rows);
}
if (data.disabled) {
subnode.setAttribute('disabled', 'disabled');
}
if (data.required) {
subnode.setAttribute('required', 'required');
}
if (data.readonly) {
subnode.setAttribute('readonly', 'readonly');
}
if (data.value) {
subnode.value = data.value;
}
childContainer = subnode;
break;
default:
throw new Error('Morebits.QuickForm: unknown element type ' + data.type.toString());
}
if (!childContainer) {
childContainer = node;
}
if (data.tooltip) {
Morebits.QuickForm.Element.generateTooltip(label || node, data);
}
if (data.extra) {
childContainer.extra = data.extra;
}
if (data.$data) {
$(childContainer).data(data.$data);
}
if (data.style) {
childContainer.setAttribute('style', data.style);
}
if (data.className) {
childContainer.className = childContainer.className ?
childContainer.className + ' ' + data.className :
data.className;
}
childContainer.setAttribute('id', data.id || id);
return [node, childContainer];
};
Morebits.QuickForm.$tooltip = null;
/**
* Create a tooltip.
*
* @memberof Morebits.QuickForm.Element
* @param {HTMLElement} node - The HTML element beside which a tooltip is to be generated.
* @param {Object} data - Tooltip-related configuration data.
*/
Morebits.QuickForm.Element.generateTooltip = function QuickFormElementGenerateTooltip(node, data) {
if (!Morebits.QuickForm.$tooltip) {
Morebits.QuickForm.$tooltip = $('<div>')
.attr('id', 'morebits-ui-tooltip')
.attr('role', 'tooltip')
.addClass('morebits-ui-tooltip')
.appendTo('body');
}
const $tooltip = Morebits.QuickForm.$tooltip;
const $button = $('<span>')
.addClass('morebits-tooltipButton')
.text('?')
.appendTo(node);
$button.on('mouseenter', () => {
$tooltip.html(data.tooltip).addClass('visible');
const buttonRect = $button[0].getBoundingClientRect();
const tooltipRect = $tooltip[0].getBoundingClientRect();
const topOffset = buttonRect.bottom + tooltipRect.height < window.innerHeight ?
buttonRect.bottom : // It fits at the top of the question mark - place it there
Math.max(0, buttonRect.top - tooltipRect.height); // Else, place to the bottom of the question mark
const leftOffset = buttonRect.right + tooltipRect.width < window.innerWidth ?
buttonRect.right : // It fits at the right of the question mark - place it there
Math.max(0, buttonRect.left - tooltipRect.width); // Else, place to the left of the question mark
$tooltip.css('top', window.scrollY + topOffset);
$tooltip.css('left', window.scrollX + leftOffset);
}).on('mouseleave', () => {
$tooltip.removeClass('visible');
});
};
// Some utility methods for manipulating quickForms after their creation:
// (None of these work for "dyninput" type fields at present)
/**
* Returns an object containing all filled form data entered by the user, with the object
* keys being the form element names. Disabled fields will be ignored, but not hidden fields.
*
* @memberof Morebits.QuickForm
* @param {HTMLFormElement} form
* @return {Object} With field names as keys, input data as values.
*/
Morebits.QuickForm.getInputData = function(form) {
const result = {};
for (let i = 0; i < form.elements.length; i++) {
const field = form.elements[i];
if (field.disabled || !field.name || !field.type ||
field.type === 'submit' || field.type === 'button') {
continue;
}
// For elements in subgroups, quickform prepends element names with
// name of the parent group followed by a period, get rid of that.
const fieldNameNorm = field.name.slice(field.name.indexOf('.') + 1);
switch (field.type) {
case 'radio':
if (field.checked) {
result[fieldNameNorm] = field.value;
}
break;
case 'checkbox':
if (field.dataset.single) {
result[fieldNameNorm] = field.checked; // boolean
} else {
result[fieldNameNorm] = result[fieldNameNorm] || [];
if (field.checked) {
result[fieldNameNorm].push(field.value);
}
}
break;
case 'select-multiple':
result[fieldNameNorm] = $(field).val(); // field.value doesn't work
break;
case 'text': // falls through
case 'textarea':
if (field.dataset.dyninput) {
result[fieldNameNorm] = result[fieldNameNorm] || [];
result[fieldNameNorm].push(field.value.trim());
} else {
result[fieldNameNorm] = field.value.trim();
}
break;
default: // could be select-one, date, number, email, etc
if (field.value) {
result[fieldNameNorm] = field.value;
}
break;
}
}
return result;
};
/**
* Returns all form elements with a given field name or ID.
*
* @memberof Morebits.QuickForm
* @param {HTMLFormElement} form
* @param {string} fieldName - The name or id of the fields.
* @return {HTMLElement[]} - Array of matching form elements.
*/
Morebits.QuickForm.getElements = function QuickFormGetElements(form, fieldName) {
const $form = $(form);
fieldName = $.escapeSelector(fieldName); // sanitize input
let $elements = $form.find('[name="' + fieldName + '"]');
if ($elements.length > 0) {
return $elements.toArray();
}
$elements = $form.find('#' + fieldName);
return $elements.toArray();
};
/**
* Searches the array of elements for a checkbox or radio button with a certain
* `value` attribute, and returns the first such element. Returns null if not found.
*
* @memberof Morebits.QuickForm
* @param {HTMLInputElement[]} elementArray - Array of checkbox or radio elements.
* @param {string} value - Value to search for.
* @return {HTMLInputElement}
*/
Morebits.QuickForm.getCheckboxOrRadio = function QuickFormGetCheckboxOrRadio(elementArray, value) {
const found = $.grep(elementArray, (el) => el.value === value);
if (found.length > 0) {
return found[0];
}
return null;
};
/**
* Returns the <div> containing the form element, or the form element itself
* May not work as expected on checkboxes or radios.
*
* @memberof Morebits.QuickForm
* @param {HTMLElement} element
* @return {HTMLElement}
*/
Morebits.QuickForm.getElementContainer = function QuickFormGetElementContainer(element) {
// for divs, headings and fieldsets, the container is the element itself
if (element instanceof HTMLFieldSetElement || element instanceof HTMLDivElement ||
element instanceof HTMLHeadingElement) {
return element;
}
// for others, just return the parent node
return element.parentNode;
};
/**
* Gets the HTML element that contains the label of the given form element
* (mainly for internal use).
*
* @memberof Morebits.QuickForm
* @param {(HTMLElement|Morebits.QuickForm.Element)} element
* @return {HTMLElement}
*/
Morebits.QuickForm.getElementLabelObject = function QuickFormGetElementLabelObject(element) {
// for buttons, divs and headers, the label is on the element itself
if (element.type === 'button' || element.type === 'submit' ||
element instanceof HTMLDivElement || element instanceof HTMLHeadingElement) {
return element;
// for fieldsets, the label is the child <legend> element
} else if (element instanceof HTMLFieldSetElement) {
return element.getElementsByTagName('legend')[0];
// for textareas, the label is the sibling <h5> element
} else if (element instanceof HTMLTextAreaElement) {
return element.parentNode.getElementsByTagName('h5')[0];
}
// for others, the label is the sibling <label> element
return element.parentNode.getElementsByTagName('label')[0];
};
/**
* Gets the label text of the element.
*
* @memberof Morebits.QuickForm
* @param {(HTMLElement|Morebits.QuickForm.Element)} element
* @return {string}
*/
Morebits.QuickForm.getElementLabel = function QuickFormGetElementLabel(element) {
const labelElement = Morebits.QuickForm.getElementLabelObject(element);
if (!labelElement) {
return null;
}
return labelElement.firstChild.textContent;
};
/**
* Sets the label of the element to the given text.
*
* @memberof Morebits.QuickForm
* @param {(HTMLElement|Morebits.QuickForm.Element)} element
* @param {string} labelText
* @return {boolean} True if succeeded, false if the label element is unavailable.
*/
Morebits.QuickForm.setElementLabel = function QuickFormSetElementLabel(element, labelText) {
const labelElement = Morebits.QuickForm.getElementLabelObject(element);
if (!labelElement) {
return false;
}
labelElement.firstChild.textContent = labelText;
return true;
};
/**
* Stores the element's current label, and temporarily sets the label to the given text.
*
* @memberof Morebits.QuickForm
* @param {(HTMLElement|Morebits.QuickForm.Element)} element
* @param {string} temporaryLabelText
* @return {boolean} `true` if succeeded, `false` if the label element is unavailable.
*/
Morebits.QuickForm.overrideElementLabel = function QuickFormOverrideElementLabel(element, temporaryLabelText) {
if (!element.hasAttribute('data-oldlabel')) {
element.setAttribute('data-oldlabel', Morebits.QuickForm.getElementLabel(element));
}
return Morebits.QuickForm.setElementLabel(element, temporaryLabelText);
};
/**
* Restores the label stored by overrideElementLabel.
*
* @memberof Morebits.QuickForm
* @param {(HTMLElement|Morebits.QuickForm.Element)} element
* @return {boolean} True if succeeded, false if the label element is unavailable.
*/
Morebits.QuickForm.resetElementLabel = function QuickFormResetElementLabel(element) {
if (element.hasAttribute('data-oldlabel')) {
return Morebits.QuickForm.setElementLabel(element, element.getAttribute('data-oldlabel'));
}
return null;
};
/**
* Shows or hides a form element plus its label and tooltip.
*
* @memberof Morebits.QuickForm
* @param {(HTMLElement|jQuery|string)} element - HTML/jQuery element, or jQuery selector string.
* @param {boolean} [visibility] - Skip this to toggle visibility.
*/
Morebits.QuickForm.setElementVisibility = function QuickFormSetElementVisibility(element, visibility) {
$(element).toggle(visibility);
};
/**
* Shows or hides the question mark icon (which displays the tooltip) next to a form element.
*
* @memberof Morebits.QuickForm
* @param {(HTMLElement|jQuery)} element
* @param {boolean} [visibility] - Skip this to toggle visibility.
*/
Morebits.QuickForm.setElementTooltipVisibility = function QuickFormSetElementTooltipVisibility(element, visibility) {
$(Morebits.QuickForm.getElementContainer(element)).find('.morebits-tooltipButton').toggle(visibility);
};
/**
* @external HTMLFormElement
*/
/**
* Get checked items in the form.
*
* @method external:HTMLFormElement.getChecked
* @param {string} name - Find checked property of elements (i.e. a checkbox
* or a radiobutton) with the given name, or select options that have selected
* set to true (don't try to mix selects with radio/checkboxes).
* @param {string} [type] - Optionally specify either radio or checkbox (for
* the event that both checkboxes and radiobuttons have the same name).
* @return {string[]} - Contains the values of elements with the given name
* checked property set to true.
*/
HTMLFormElement.prototype.getChecked = function(name, type) {
const elements = this.elements[name];
if (!elements) {
return [];
}
const return_array = [];
let i;
if (elements instanceof HTMLSelectElement) {
const options = elements.options;
for (i = 0; i < options.length; ++i) {
if (options[i].selected) {
if (options[i].values) {
return_array.push(options[i].values);
} else {
return_array.push(options[i].value);
}
}
}
} else if (elements instanceof HTMLInputElement) {
if (type && elements.type !== type) {
return [];
} else if (elements.checked) {
return [elements.value];
}
} else {
for (i = 0; i < elements.length; ++i) {
if (elements[i].checked) {
if (type && elements[i].type !== type) {
continue;
}
if (elements[i].values) {
return_array.push(elements[i].values);
} else {
return_array.push(elements[i].value);
}
}
}
}
return return_array;
};
/**
* Does the same as {@link HTMLFormElement.getChecked|getChecked}, but with unchecked elements.
*
* @method external:HTMLFormElement.getUnchecked
* @param {string} name - Find checked property of elements (i.e. a checkbox
* or a radiobutton) with the given name, or select options that have selected
* set to true (don't try to mix selects with radio/checkboxes).
* @param {string} [type] - Optionally specify either radio or checkbox (for
* the event that both checkboxes and radiobuttons have the same name).
* @return {string[]} - Contains the values of elements with the given name
* checked property set to true.
*/
HTMLFormElement.prototype.getUnchecked = function(name, type) {
const elements = this.elements[name];
if (!elements) {
return [];
}
const return_array = [];
let i;
if (elements instanceof HTMLSelectElement) {
const options = elements.options;
for (i = 0; i < options.length; ++i) {
if (!options[i].selected) {
if (options[i].values) {
return_array.push(options[i].values);
} else {
return_array.push(options[i].value);
}
}
}
} else if (elements instanceof HTMLInputElement) {
if (type && elements.type !== type) {
return [];
} else if (!elements.checked) {
return [elements.value];
}
} else {
for (i = 0; i < elements.length; ++i) {
if (!elements[i].checked) {
if (type && elements[i].type !== type) {
continue;
}
if (elements[i].values) {
return_array.push(elements[i].values);
} else {
return_array.push(elements[i].value);
}
}
}
}
return return_array;
};
/**
* Utilities to help process IP addresses.
*
* @namespace Morebits.ip
* @memberof Morebits
*/
Morebits.ip = {
/**
* Converts an IPv6 address to the canonical form stored and used by MediaWiki.
* JavaScript translation of the {@link https://gerrit.wikimedia.org/r/plugins/gitiles/mediawiki/core/+/8eb6ac3e84ea3312d391ca96c12c49e3ad0753bb/includes/utils/IP.php#131|`IP::sanitizeIP()`}
* function from the IPUtils library. Addresses are verbose, uppercase,
* normalized, and expanded to 8 words.
*
* @param {string} address - The IPv6 address, with or without CIDR.
* @return {string}
*/
sanitizeIPv6: function(address) {
address = address.trim();
if (address === '') {
return null;
}
if (!mw.util.isIPv6Address(address, true)) {
return address; // nothing else to do for IPv4 addresses or invalid ones
}
// Remove any whitespaces, convert to upper case
address = address.toUpperCase();
// Expand zero abbreviations
const abbrevPos = address.indexOf('::');
if (abbrevPos > -1) {
// We know this is valid IPv6. Find the last index of the
// address before any CIDR number (e.g. "a:b:c::/24").
const CIDRStart = address.indexOf('/');
const addressEnd = CIDRStart !== -1 ? CIDRStart - 1 : address.length - 1;
// If the '::' is at the beginning...
let repeat, extra, pad;
if (abbrevPos === 0) {
repeat = '0:';
extra = address === '::' ? '0' : ''; // for the address '::'
pad = 9; // 7+2 (due to '::')
// If the '::' is at the end...
} else if (abbrevPos === (addressEnd - 1)) {
repeat = ':0';
extra = '';
pad = 9; // 7+2 (due to '::')
// If the '::' is in the middle...
} else {
repeat = ':0';
extra = ':';
pad = 8; // 6+2 (due to '::')
}
let replacement = repeat;
pad -= address.split(':').length - 1;
for (let i = 1; i < pad; i++) {
replacement += repeat;
}
replacement += extra;
address = address.replace('::', replacement);
}
// Remove leading zeros from each bloc as needed
return address.replace(/(^|:)0+([0-9A-Fa-f]{1,4})/g, '$1$2');
},
/**
* Determine if the given IP address is a range. Just conjoins
* `mw.util.isIPAddress` with and without the `allowBlock` option.
*
* @param {string} ip
* @return {boolean} - True if given a valid IP address range, false otherwise.
*/
isRange: function(ip) {
return mw.util.isIPAddress(ip, true) && !mw.util.isIPAddress(ip);
},
/**
* Check that an IP range is within the CIDR limits. Most likely to be useful
* in conjunction with `wgRelevantUserName`. CIDR limits are hardcoded as /16
* for IPv4 and /32 for IPv6.
*
* @return {boolean} - True for valid ranges within the CIDR limits,
* otherwise false (ranges outside the limit, single IPs, non-IPs).
*/
validCIDR: function(ip) {
if (Morebits.ip.isRange(ip)) {
const subnet = parseInt(ip.match(/\/(\d{1,3})$/)[1], 10);
if (subnet) { // Should be redundant
if (mw.util.isIPv6Address(ip, true)) {
if (subnet >= 32) {
return true;
}
} else {
if (subnet >= 16) {
return true;
}
}
}
}
return false;
},
/**
* Get the /64 subnet for an IPv6 address.
*
* @param {string} ipv6 - The IPv6 address, with or without a subnet.
* @return {boolean|string} - False if not IPv6 or bigger than a 64,
* otherwise the (sanitized) /64 address.
*/
get64: function(ipv6) {
if (!ipv6 || !mw.util.isIPv6Address(ipv6, true)) {
return false;
}
const subnetMatch = ipv6.match(/\/(\d{1,3})$/);
if (subnetMatch && parseInt(subnetMatch[1], 10) < 64) {
return false;
}
ipv6 = Morebits.ip.sanitizeIPv6(ipv6);
const ip_re = /^((?:[0-9A-F]{1,4}:){4})(?:[0-9A-F]{1,4}:){3}[0-9A-F]{1,4}(?:\/\d{1,3})?$/;
// eslint-disable-next-line no-useless-concat
return ipv6.replace(ip_re, '$1' + '0:0:0:0/64');
}
};
/**
* Helper functions to manipulate strings.
*
* @namespace Morebits.string
* @memberof Morebits
*/
Morebits.string = {
/**
* @param {string} str
* @return {string}
*/
toUpperCaseFirstChar: function(str) {
str = str.toString();
return str.slice(0, 1).toUpperCase() + str.slice(1);
},
/**
* @param {string} str
* @return {string}
*/
toLowerCaseFirstChar: function(str) {
str = str.toString();
return str.slice(0, 1).toLowerCase() + str.slice(1);
},
/**
* Gives an array of substrings of `str` - starting with `start` and
* ending with `end` - which is not in `skiplist`. Intended for use
* on wikitext with templates or links.
*
* @param {string} str
* @param {string} start
* @param {string} end
* @param {(string[]|string)} [skiplist]
* @return {string[]}
* @throws {Error} If the `start` and `end` strings aren't of the same length.
* @throws {Error} If `skiplist` isn't an array or string
*/
splitWeightedByKeys: function(str, start, end, skiplist) {
if (start.length !== end.length) {
throw new Error('start marker and end marker must be of the same length');
}
let level = 0;
let initial = null;
const result = [];
if (!Array.isArray(skiplist)) {
if (skiplist === undefined) {
skiplist = [];
} else if (typeof skiplist === 'string') {
skiplist = [skiplist];
} else {
throw new Error('non-applicable skiplist parameter');
}
}
for (let i = 0; i < str.length; ++i) {
for (let j = 0; j < skiplist.length; ++j) {
if (str.substr(i, skiplist[j].length) === skiplist[j]) {
i += skiplist[j].length - 1;
continue;
}
}
if (str.substr(i, start.length) === start) {
if (initial === null) {
initial = i;
}
++level;
i += start.length - 1;
} else if (str.substr(i, end.length) === end) {
--level;
i += end.length - 1;
}
if (!level && initial !== null) {
result.push(str.substring(initial, i + 1));
initial = null;
}
}
return result;
},
/**
* Formats freeform "reason" (from a textarea) for deletion/other
* templates that are going to be substituted, (e.g. PROD, XFD, RPP).
* Handles `|` outside a nowiki tag.
* Optionally, also adds a signature if not present already.
*
* @param {string} str
* @param {boolean} [addSig]
* @return {string}
*/
formatReasonText: function(str, addSig) {
let reason = (str || '').toString().trim();
const unbinder = new Morebits.Unbinder(reason);
// eslint-disable-next-line no-useless-concat
unbinder.unbind('<no' + 'wiki>', '</no' + 'wiki>');
unbinder.content = unbinder.content.replace(/\|/g, '{{subst:!}}');
reason = unbinder.rebind();
if (addSig) {
const sig = '~~~~', sigIndex = reason.lastIndexOf(sig);
if (sigIndex === -1 || sigIndex !== reason.length - sig.length) {
reason += ' ' + sig;
}
}
return reason.trim();
},
/**
* Formats a "reason" (from a textarea) for inclusion in a userspace
* log. Replaces newlines with {{Pb}}, and adds an extra `#` before
* list items for proper formatting.
*
* @param {string} str
* @return {string}
*/
formatReasonForLog: function(str) {
return str
// handle line breaks, which otherwise break numbering
.replace(/\n+/g, '{{pb}}')
// put an extra # in front before bulleted or numbered list items
.replace(/^(#+)/mg, '#$1')
.replace(/^(\*+)/mg, '#$1');
},
/**
* Like `String.prototype.replace()`, but escapes any dollar signs in
* the replacement string. Useful when the the replacement string is
* arbitrary, such as a username or freeform user input, and could
* contain dollar signs.
*
* @param {string} string - Text in which to replace.
* @param {(string|RegExp)} pattern
* @param {string} replacement
* @return {string}
*/
safeReplace: function morebitsStringSafeReplace(string, pattern, replacement) {
return string.replace(pattern, replacement.replace(/\$/g, '$$$$'));
},
/**
* Determine if the user-provided expiration will be considered an
* infinite-length by MW.
*
* @see {@link https://phabricator.wikimedia.org/T68646}
*
* @param {string} expiry
* @return {boolean}
*/
isInfinity: function morebitsStringIsInfinity(expiry) {
return ['indefinite', 'infinity', 'infinite', 'never'].includes(expiry);
},
/**
* Escapes a string to be used in a RegExp, replacing spaces and
* underscores with `[_ ]` as they are often equivalent.
*
* @param {string} text - String to be escaped.
* @return {string} - The escaped text.
*/
escapeRegExp: function(text) {
return mw.util.escapeRegExp(text).replace(/ |_/g, '[_ ]');
}
};
/**
* Helper functions to manipulate arrays.
*
* @namespace Morebits.array
* @memberof Morebits
*/
Morebits.array = {
/**
* Remove duplicated items from an array.
*
* @param {Array} arr
* @return {Array} A copy of the array with duplicates removed.
* @throws {Error} When provided a non-array.
*/
uniq: function(arr) {
if (!Array.isArray(arr)) {
throw new Error('A non-array object passed to Morebits.array.uniq');
}
return arr.filter((item, idx) => arr.indexOf(item) === idx);
},
/**
* Remove non-duplicated items from an array.
*
* @param {Array} arr
* @return {Array} A copy of the array with the first instance of each value
* removed; subsequent instances of those values (duplicates) remain.
* @throws {Error} When provided a non-array.
*/
dups: function(arr) {
if (!Array.isArray(arr)) {
throw new Error('A non-array object passed to Morebits.array.dups');
}
return arr.filter((item, idx) => arr.indexOf(item) !== idx);
},
/**
* Break up an array into smaller arrays.
*
* @param {Array} arr
* @param {number} size - Size of each chunk (except the last, which could be different).
* @return {Array[]} An array containing the smaller, chunked arrays.
* @throws {Error} When provided a non-array.
*/
chunk: function(arr, size) {
if (!Array.isArray(arr)) {
throw new Error('A non-array object passed to Morebits.array.chunk');
}
if (typeof size !== 'number' || size <= 0) { // pretty impossible to do anything :)
return [arr]; // we return an array consisting of this array.
}
const numChunks = Math.ceil(arr.length / size);
const result = new Array(numChunks);
for (let i = 0; i < numChunks; i++) {
result[i] = arr.slice(i * size, (i + 1) * size);
}
return result;
}
};
/**
* Utilities to enhance select2 menus. See twinklewarn, twinklexfd,
* twinkleblock for sample usages.
*
* @see {@link https://select2.org/}
*
* @namespace Morebits.select2
* @memberof Morebits
* @requires jQuery.select2
*/
Morebits.select2 = {
matchers: {
/**
* Custom matcher in which if the optgroup name matches, all options in that
* group are shown, like in jquery.chosen.
*/
optgroupFull: function(params, data) {
const originalMatcher = $.fn.select2.defaults.defaults.matcher;
const result = originalMatcher(params, data);
if (result && params.term &&
data.text.toUpperCase().includes(params.term.toUpperCase())) {
result.children = data.children;
}
return result;
},
/** Custom matcher that matches from the beginning of words only. */
wordBeginning: function(params, data) {
const originalMatcher = $.fn.select2.defaults.defaults.matcher;
const result = originalMatcher(params, data);
if (!params.term || (result &&
new RegExp('\\b' + mw.util.escapeRegExp(params.term), 'i').test(result.text))) {
return result;
}
return null;
}
},
/** Underline matched part of options. */
highlightSearchMatches: function(data) {
const searchTerm = Morebits.select2SearchQuery;
if (!searchTerm || data.loading) {
return data.text;
}
const idx = data.text.toUpperCase().indexOf(searchTerm.toUpperCase());
if (idx < 0) {
return data.text;
}
return $('<span>').append(
data.text.slice(0, idx),
$('<span>').css('text-decoration', 'underline').text(data.text.slice(idx, idx + searchTerm.length)),
data.text.slice(idx + searchTerm.length)
);
},
/** Intercept query as it is happening, for use in highlightSearchMatches. */
queryInterceptor: function(params) {
Morebits.select2SearchQuery = params && params.term;
},
/**
* Open dropdown and begin search when the `.select2-selection` has
* focus and a key is pressed.
*
* @see {@link https://github.com/select2/select2/issues/3279#issuecomment-442524147}
*/
autoStart: function(ev) {
if (ev.which < 48) {
return;
}
let $target = $(ev.target).closest('.select2-container');
if (!$target.length) {
return;
}
$target = $target.prev();
$target.select2('open');
const search = $target.data('select2').dropdown.$search ||
$target.data('select2').selection.$search;
// Use DOM .focus() to work around a jQuery 3.6.0 regression (https://github.com/select2/select2/issues/5993)
search[0].focus();
}
};
/**
* Temporarily hide a part of a string while processing the rest of it.
* Used by {@link Morebits.wikitext.Page#commentOutImage|Morebits.wikitext.Page.commentOutImage}.
*
* @memberof Morebits
* @class
* @param {string} string - The initial text to process.
* @example var u = new Morebits.Unbinder('Hello world <!-- world --> world');
* u.unbind('<!--', '-->'); // text inside comment remains intact
* u.content = u.content.replace(/world/g, 'earth');
* u.rebind(); // gives 'Hello earth <!-- world --> earth'
*/
Morebits.Unbinder = function Unbinder(string) {
if (typeof string !== 'string') {
throw new Error('not a string');
}
/** The text being processed. */
this.content = string;
this.counter = 0;
this.history = {};
this.prefix = '%UNIQ::' + Math.random() + '::';
this.postfix = '::UNIQ%';
};
Morebits.Unbinder.prototype = {
/**
* Hide the region encapsulated by the `prefix` and `postfix` from
* string processing. `prefix` and `postfix` will be used in a
* RegExp, so items that need escaping should be use `\\`.
*
* @param {string} prefix
* @param {string} postfix
* @throws {Error} If either `prefix` or `postfix` is missing.
*/
unbind: function UnbinderUnbind(prefix, postfix) {
if (!prefix || !postfix) {
throw new Error('Both prefix and postfix must be provided');
}
const re = new RegExp(prefix + '([\\s\\S]*?)' + postfix, 'g');
this.content = this.content.replace(re, Morebits.Unbinder.getCallback(this));
},
/**
* Restore the hidden portion of the `content` string.
*
* @return {string} The processed output.
*/
rebind: function UnbinderRebind() {
let content = this.content;
content.self = this;
for (const current in this.history) {
if (Object.prototype.hasOwnProperty.call(this.history, current)) {
content = content.replace(current, this.history[current]);
}
}
return content;
},
prefix: null, // %UNIQ::0.5955981644938324::
postfix: null, // ::UNIQ%
content: null, // string
counter: null, // 0++
history: null // {}
};
/** @memberof Morebits.Unbinder */
Morebits.Unbinder.getCallback = function UnbinderGetCallback(self) {
return function UnbinderCallback(match) {
const current = self.prefix + self.counter + self.postfix;
self.history[current] = match;
++self.counter;
return current;
};
};
/* **************** Morebits.Date **************** */
/**
* Create a date object with enhanced processing capabilities, a la
* {@link https://momentjs.com/|moment.js}. MediaWiki timestamp format is also
* acceptable, in addition to everything that JS Date() accepts.
*
* @memberof Morebits
* @class
*/
Morebits.Date = class extends Date {
constructor(...args) {
// Check MediaWiki formats
// Must be first since firefox erroneously accepts the timestamp
// format, sans timezone (See also: #921, #936, #1174, #1187), and the
// 14-digit string will be interpreted differently.
if (args.length === 1) {
const param = args[0];
if (/^\d{14}$/.test(param)) {
// YYYYMMDDHHmmss
const digitMatch = /(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/.exec(param);
if (digitMatch) {
// ..... year ... month .. date ... hour .... minute ..... second
super(Date.UTC(digitMatch[1], digitMatch[2] - 1, digitMatch[3], digitMatch[4], digitMatch[5], digitMatch[6]));
} else {
super(...args);
}
} else if (typeof param === 'string') {
// Wikitext signature timestamp
const dateParts = Morebits.l10n.signatureTimestampFormat(param);
if (dateParts) {
super(Date.UTC(...dateParts));
} else {
super(...args);
}
} else {
super(...args);
}
} else {
super(...args);
}
if (!this.isValid()) {
mw.log.warn('Invalid Morebits.Date initialisation:', args);
}
}
/** @return {boolean} */
isValid() {
return !isNaN(this.getTime());
}
/**
* @param {(Date|Morebits.Date)} date
* @return {boolean}
*/
isBefore(date) {
return this.getTime() < date.getTime();
}
/**
* @param {(Date|Morebits.Date)} date
* @return {boolean}
*/
isAfter(date) {
return this.getTime() > date.getTime();
}
/** @return {string} */
getUTCMonthName() {
return Morebits.Date.localeData.months[this.getUTCMonth()];
}
/** @return {string} */
getUTCMonthNameAbbrev() {
return Morebits.Date.localeData.monthsShort[this.getUTCMonth()];
}
/** @return {string} */
getMonthName() {
return Morebits.Date.localeData.months[this.getMonth()];
}
/** @return {string} */
getMonthNameAbbrev() {
return Morebits.Date.localeData.monthsShort[this.getMonth()];
}
/** @return {string} */
getUTCDayName() {
return Morebits.Date.localeData.days[this.getUTCDay()];
}
/** @return {string} */
getUTCDayNameAbbrev() {
return Morebits.Date.localeData.daysShort[this.getUTCDay()];
}
/** @return {string} */
getDayName() {
return Morebits.Date.localeData.days[this.getDay()];
}
/** @return {string} */
getDayNameAbbrev() {
return Morebits.Date.localeData.daysShort[this.getDay()];
}
/**
* Add a given number of minutes, hours, days, weeks, months, or years to the date.
* This is done in-place. The modified date object is also returned, allowing chaining.
*
* @param {number} number - Should be an integer.
* @param {string} unit
* @throws {Error} If invalid or unsupported unit is given.
* @return {Morebits.Date}
*/
add(number, unit) {
let num = parseInt(number, 10); // normalize
if (isNaN(num)) {
throw new Error('Invalid number "' + number + '" provided.');
}
unit = unit.toLowerCase(); // normalize
const unitMap = Morebits.Date.unitMap;
let unitNorm = unitMap[unit] || unitMap[unit + 's']; // so that both singular and plural forms work
if (unitNorm) {
// No built-in week functions, so rather than build out ISO's getWeek/setWeek, just multiply
// Probably can't be used for Julian->Gregorian changeovers, etc.
if (unitNorm === 'Week') {
unitNorm = 'Date';
num *= 7;
}
this['set' + unitNorm](this['get' + unitNorm]() + num);
return this;
}
throw new Error('Invalid unit "' + unit + '": Only ' + Object.keys(unitMap).join(', ') + ' are allowed.');
}
/**
* Subtracts a given number of minutes, hours, days, weeks, months, or years to the date.
* This is done in-place. The modified date object is also returned, allowing chaining.
*
* @param {number} number - Should be an integer.
* @param {string} unit
* @throws {Error} If invalid or unsupported unit is given.
* @return {Morebits.Date}
*/
subtract(number, unit) {
return this.add(-number, unit);
}
/**
* Format the date into a string per the given format string.
* Replacement syntax is a subset of that in moment.js:
*
* | Syntax | Output |
* |--------|--------|
* | H | Hours (24-hour) |
* | HH | Hours (24-hour, padded to 2 digits) |
* | h | Hours (12-hour) |
* | hh | Hours (12-hour, padded to 2 digits) |
* | A | AM or PM |
* | m | Minutes |
* | mm | Minutes (padded to 2 digits) |
* | s | Seconds |
* | ss | Seconds (padded to 2 digits) |
* | SSS | Milliseconds fragment, 3 digits |
* | d | Day number of the week (Sun=0) |
* | ddd | Abbreviated day name |
* | dddd | Full day name |
* | D | Date |
* | DD | Date (padded to 2 digits) |
* | M | Month number (1-indexed) |
* | MM | Month number (1-indexed, padded to 2 digits) |
* | MMM | Abbreviated month name |
* | MMMM | Full month name |
* | Y | Year |
* | YY | Final two digits of year (20 for 2020, 42 for 1942) |
* | YYYY | Year (same as `Y`) |
*
* @param {string} formatstr - Format the date into a string, using
* the replacement syntax. Use `[` and `]` to escape items. If not
* provided, will return the ISO-8601-formatted string.
* @param {(string|number)} [zone=system] - `system` (for browser-default time zone),
* `utc`, or specify a time zone as number of minutes relative to UTC.
* @return {string}
*/
format(formatstr, zone) {
if (!this.isValid()) {
return 'Invalid date'; // Put the truth out, preferable to "NaNNaNNan NaN:NaN" or whatever
}
let udate = this;
// create a new date object that will contain the date to display as system time
if (zone === 'utc') {
udate = new Morebits.Date(this.getTime()).add(this.getTimezoneOffset(), 'minutes');
} else if (typeof zone === 'number') {
// convert to utc, then add the utc offset given
udate = new Morebits.Date(this.getTime()).add(this.getTimezoneOffset() + zone, 'minutes');
}
// default to ISOString
if (!formatstr) {
return udate.toISOString();
}
const pad = function(num, len) {
len = len || 2; // Up to length of 00 + 1
return ('00' + num).toString().slice(0 - len);
};
var h24 = udate.getHours(), m = udate.getMinutes(), s = udate.getSeconds(), ms = udate.getMilliseconds();
var D = udate.getDate(), M = udate.getMonth() + 1, Y = udate.getFullYear();
var h12 = h24 % 12 || 12, amOrPm = h24 >= 12 ? '午後' : '午前';
var replacementMap = {
HH: pad(h24), H: h24, hh: pad(h12), h: h12, A: amOrPm,
mm: pad(m), m: m,
ss: pad(s), s: s,
SSS: pad(ms, 3),
dddd: udate.getDayName(), ddd: udate.getDayNameAbbrev(), d: udate.getDay(),
DD: pad(D), D: D,
MMMM: udate.getMonthName(), MMM: udate.getMonthNameAbbrev(), MM: pad(M), M: M,
YYYY: Y, YY: pad(Y % 100), Y: Y
};
const unbinder = new Morebits.Unbinder(formatstr); // escape stuff between [...]
unbinder.unbind('\\[', '\\]');
unbinder.content = unbinder.content.replace(
/* Regex notes:
* d(d{2,3})? matches exactly 1, 3 or 4 occurrences of 'd' ('dd' is treated as a double match of 'd')
* Y{1,2}(Y{2})? matches exactly 1, 2 or 4 occurrences of 'Y'
*/
/H{1,2}|h{1,2}|m{1,2}|s{1,2}|SSS|d(d{2,3})?|D{1,2}|M{1,4}|Y{1,2}(Y{2})?|A/g,
(match) => replacementMap[match]
);
return unbinder.rebind().replace(/\[(.*?)\]/g, '$1');
}
/**
* Gives a readable relative time string such as "Yesterday at 6:43 PM" or "Last Thursday at 11:45 AM".
* Similar to `calendar` in moment.js, but with time zone support.
*
* @param {(string|number)} [zone=system] - 'system' (for browser-default time zone),
* 'utc' (for UTC), or specify a time zone as number of minutes past UTC.
* @return {string}
*/
calendar(zone) {
// Zero out the hours, minutes, seconds and milliseconds - keeping only the date;
// find the difference. Note that setHours() returns the same thing as getTime().
const dateDiff = (new Date().setHours(0, 0, 0, 0) -
new Date(this).setHours(0, 0, 0, 0)) / 8.64e7;
switch (true) {
case dateDiff === 0:
return this.format(Morebits.Date.localeData.relativeTimes.thisDay, zone);
case dateDiff === 1:
return this.format(Morebits.Date.localeData.relativeTimes.prevDay, zone);
case dateDiff > 0 && dateDiff < 7:
return this.format(Morebits.Date.localeData.relativeTimes.pastWeek, zone);
case dateDiff === -1:
return this.format(Morebits.Date.localeData.relativeTimes.nextDay, zone);
case dateDiff < 0 && dateDiff > -7:
return this.format(Morebits.Date.localeData.relativeTimes.thisWeek, zone);
default:
return this.format(Morebits.Date.localeData.relativeTimes.other, zone);
}
}
/**
* Get a regular expression that matches wikitext section titles, such
* as `==December 2019==` or `=== Jan 2018 ===`.
*
* @return {RegExp}
*/
monthHeaderRegex() {
return new RegExp('^(==+)\\s*' + this.getUTCFullYear() +
'年+(?:' + this.getUTCMonthName() + '|' + this.getUTCMonthNameAbbrev() + ')\\s*\\1', 'mg');
}
/**
* Creates a wikitext section header with the month and year.
*
* @param {number} [level=2] - Header level. Pass 0 for just the text
* with no wikitext markers (==).
* @return {string}
*/
monthHeader(level) {
// Default to 2, but allow for 0 or stringy numbers
level = parseInt(level, 10);
level = isNaN(level) ? 2 : level;
var header = Array(level + 1).join('='); // String.prototype.repeat not supported in IE 11
var text = this.getUTCFullYear() + '年' + this.getUTCMonthName();
if (header.length) { // wikitext-formatted header
return header + ' ' + text + ' ' + header;
}
return text; // Just the string
}
};
/**
* Localized strings for date processing.
*
* @memberof Morebits.Date
* @type {object.<string, string>}
* @property {string[]} months
* @property {string[]} monthsShort
* @property {string[]} days
* @property {string[]} daysShort
* @property {object.<string, string>} relativeTimes
* @private
*/
Morebits.Date.localeData = {
// message names here correspond to MediaWiki message names
months: ['1月', '2月', '3月',
'4月', '5月', '6月',
'7月', '8月', '9月',
'10月', '11月', '12月'],
monthsShort: ['1月', '2月', '3月',
'4月', '5月', '6月',
'7月', '8月', '9月',
'10月', '11月', '12月'],
days: ['日曜日', '月曜日', '火曜日',
'水曜日', '木曜日', '金曜日',
'土曜日'],
daysShort: ['日', '月', '火',
'水', '木', '金',
'土'],
relativeTimes: {
thisDay: '[今日の]H:mm',
prevDay: '[昨日の]H:mm',
nextDay: '[明日の]H:mm',
thisWeek: 'dddd[の]H:mm',
pastWeek: '[先週]dddd[の]H:mm',
other: 'YYYY MM DD'
}
};
/**
* Map units with getter/setter function names, for `add` and `subtract`
* methods.
*
* @memberof Morebits.Date
* @type {object.<string, string>}
* @property {string} seconds
* @property {string} minutes
* @property {string} hours
* @property {string} days
* @property {string} weeks
* @property {string} months
* @property {string} years
*/
Morebits.Date.unitMap = {
seconds: 'Seconds',
minutes: 'Minutes',
hours: 'Hours',
days: 'Date',
weeks: 'Week', // Not a function but handled in `add` through cunning use of multiplication
months: 'Month',
years: 'FullYear'
};
/* **************** Morebits.wiki **************** */
/**
* Various objects for wiki editing and API access, including
* {@link Morebits.wiki.Api} and {@link Morebits.wiki.Page}.
*
* @namespace Morebits.wiki
* @memberof Morebits
*/
Morebits.wiki = {};
/* **************** Morebits.wiki.actionCompleted **************** */
/**
* @memberof Morebits.wiki
* @type {number}
*/
Morebits.wiki.numberOfActionsLeft = 0;
/**
* @memberof Morebits.wiki
* @type {number}
*/
Morebits.wiki.nbrOfCheckpointsLeft = 0;
/**
* Display message and/or redirect to page upon completion of tasks.
*
* Every call to Morebits.wiki.Api.post() results in the dispatch of an
* asynchronous callback. Each callback can in turn make an additional call to
* Morebits.wiki.Api.post() to continue a processing sequence. At the
* conclusion of the final callback of a processing sequence, it is not
* possible to simply return to the original caller because there is no call
* stack leading back to the original context. Instead,
* Morebits.wiki.actionCompleted.event() is called to display the result to
* the user and to perform an optional page redirect.
*
* The determination of when to call Morebits.wiki.actionCompleted.event() is
* managed through the globals Morebits.wiki.numberOfActionsLeft and
* Morebits.wiki.nbrOfCheckpointsLeft. Morebits.wiki.numberOfActionsLeft is
* incremented at the start of every Morebits.wiki.Api call and decremented
* after the completion of a callback function. If a callback function does
* not create a new Morebits.wiki.Api object before exiting, it is the final
* step in the processing chain and Morebits.wiki.actionCompleted.event() will
* then be called.
*
* Optionally, callers may use Morebits.wiki.addCheckpoint() to indicate that
* processing is not complete upon the conclusion of the final callback
* function. This is used for batch operations. The end of a batch is
* signaled by calling Morebits.wiki.removeCheckpoint().
*
* @memberof Morebits.wiki
*/
Morebits.wiki.actionCompleted = function(self) {
if (--Morebits.wiki.numberOfActionsLeft <= 0 && Morebits.wiki.nbrOfCheckpointsLeft <= 0) {
Morebits.wiki.actionCompleted.event(self);
}
};
// Change per action wanted
/** @memberof Morebits.wiki */
Morebits.wiki.actionCompleted.event = function() {
if (Morebits.wiki.actionCompleted.notice) {
Morebits.Status.actionCompleted(Morebits.wiki.actionCompleted.notice);
}
if (Morebits.wiki.actionCompleted.redirect) {
// if it isn't a URL, make it one. TODO: This breaks on the articles 'http://', 'ftp://', and similar ones.
if (!(/^\w+:\/\//).test(Morebits.wiki.actionCompleted.redirect)) {
Morebits.wiki.actionCompleted.redirect = mw.util.getUrl(Morebits.wiki.actionCompleted.redirect);
if (Morebits.wiki.actionCompleted.followRedirect === false) {
Morebits.wiki.actionCompleted.redirect += '?redirect=no';
}
}
window.setTimeout(() => {
window.location = Morebits.wiki.actionCompleted.redirect;
}, Morebits.wiki.actionCompleted.timeOut);
}
};
/** @memberof Morebits.wiki */
Morebits.wiki.actionCompleted.timeOut = typeof window.wpActionCompletedTimeOut === 'undefined' ? 5000 : window.wpActionCompletedTimeOut;
/** @memberof Morebits.wiki */
Morebits.wiki.actionCompleted.redirect = null;
/** @memberof Morebits.wiki */
Morebits.wiki.actionCompleted.notice = null;
/** @memberof Morebits.wiki */
Morebits.wiki.addCheckpoint = function() {
++Morebits.wiki.nbrOfCheckpointsLeft;
};
/** @memberof Morebits.wiki */
Morebits.wiki.removeCheckpoint = function() {
if (--Morebits.wiki.nbrOfCheckpointsLeft <= 0 && Morebits.wiki.numberOfActionsLeft <= 0) {
Morebits.wiki.actionCompleted.event();
}
};
/* **************** Morebits.wiki.Api **************** */
/**
* An easy way to talk to the MediaWiki API. Accepts either json or xml
* (default) formats; if json is selected, will default to `formatversion=2`
* unless otherwise specified. Similarly, enforces newer `errorformat`s,
* defaulting to `html` if unspecified. `uselang` enforced to the wiki's
* content language.
*
* In new code, the use of the last 3 parameters should be avoided, instead
* use {@link Morebits.wiki.Api#setStatusElement|setStatusElement()} to bind
* the status element (if needed) and use `.then()` or `.catch()` on the
* promise returned by `post()`, rather than specify the `onSuccess` or
* `onFailure` callbacks.
*
* @memberof Morebits.wiki
* @class
* @param {string} currentAction - The current action (required).
* @param {Object} query - The query (required).
* @param {Function} [onSuccess] - The function to call when request is successful.
* @param {Morebits.Status} [statusElement] - A Morebits.Status object to use for status messages.
* @param {Function} [onError] - The function to call if an error occurs.
*/
Morebits.wiki.Api = function(currentAction, query, onSuccess, statusElement, onError) {
this.currentAction = currentAction;
this.query = query;
this.query.assert = 'user';
// Enforce newer error formats, preferring html
if (!query.errorformat || !['wikitext', 'plaintext'].includes(query.errorformat)) {
this.query.errorformat = 'html';
}
// Explicitly use the wiki's content language to minimize confusion,
// see #1179 for discussion
this.query.uselang = 'content';
this.query.errorlang = 'uselang';
this.query.errorsuselocal = 1;
this.onSuccess = onSuccess;
this.onError = onError;
if (statusElement) {
this.setStatusElement(statusElement);
} else {
this.statelem = new Morebits.Status(currentAction);
}
// JSON is used throughout Morebits/Twinkle, but xml remains the default for backwards compatibility
if (!query.format) {
this.query.format = 'xml';
} else if (query.format === 'json' && !query.formatversion) {
this.query.formatversion = '2';
} else if (!['xml', 'json'].includes(query.format)) {
this.statelem.error('Invalid API format: only xml and json are supported.');
}
// Ignore tags for queries and most common unsupported actions, produces warnings
if (query.action && ['query', 'review', 'stabilize', 'watch'].includes(query.action)) {
delete query.tags;
}
};
Morebits.wiki.Api.prototype = {
currentAction: '',
onSuccess: null,
onError: null,
parent: window, // use global context if there is no parent object
query: null,
response: null,
responseXML: null, // use `response` instead; retained for backwards compatibility
statelem: null, // this non-standard name kept for backwards compatibility
statusText: null, // result received from the API, normally "success" or "error"
errorCode: null, // short text error code, if any, as documented in the MediaWiki API
errorText: null, // full error description, if any
badtokenRetry: false, // set to true if this on a retry attempted after a badtoken error
/**
* Keep track of parent object for callbacks.
*
* @param {*} parent
*/
setParent: function(parent) {
this.parent = parent;
},
/** @param {Morebits.Status} statusElement */
setStatusElement: function(statusElement) {
this.statelem = statusElement;
this.statelem.status(this.currentAction);
},
/**
* Carry out the request.
*
* @param {Object} callerAjaxParameters - Do not specify a parameter unless you really
* really want to give jQuery some extra parameters.
* @return {jQuery.Promise} - A jQuery promise object that is resolved or rejected with the api object.
*/
post: function(callerAjaxParameters) {
++Morebits.wiki.numberOfActionsLeft;
const queryString = $.map(this.query, (val, i) => {
if (Array.isArray(val)) {
return encodeURIComponent(i) + '=' + val.map(encodeURIComponent).join('|');
} else if (val !== undefined) {
return encodeURIComponent(i) + '=' + encodeURIComponent(val);
}
}).join('&').replace(/^(.*?)(\btoken=[^&]*)&(.*)/, '$1$3&$2');
// token should always be the last item in the query string (bug TW-B-0013)
const headers = {
'Api-User-Agent': morebitsWikiApiUserAgent
};
if (this.query.action === 'parse') {
// Per https://www.mediawiki.org/wiki/API:Etiquette
headers['Promise-Non-Write-API-Action'] = 'true';
}
const ajaxparams = $.extend({}, {
context: this,
type: this.query.action === 'query' ? 'GET' : 'POST',
url: mw.util.wikiScript('api'),
data: queryString,
dataType: this.query.format,
headers
}, callerAjaxParameters);
return $.ajax(ajaxparams).then(
function onAPIsuccess(response, statusText) {
this.statusText = statusText;
this.response = this.responseXML = response;
// Limit to first error
if (this.query.format === 'json') {
this.errorCode = response.errors && response.errors[0].code;
if (this.query.errorformat === 'html') {
this.errorText = response.errors && response.errors[0].html;
} else if (this.query.errorformat === 'wikitext' || this.query.errorformat === 'plaintext') {
this.errorText = response.errors && response.errors[0].text;
}
} else {
this.errorCode = $(response).find('errors error').eq(0).attr('code');
// Sufficient for html, wikitext, or plaintext errorformats
this.errorText = $(response).find('errors error').eq(0).text();
}
if (typeof this.errorCode === 'string') {
// the API didn't like what we told it, e.g., bad edit token or an error creating a page
return this.returnError(callerAjaxParameters);
}
// invoke success callback if one was supplied
if (this.onSuccess) {
// set the callback context to this.parent for new code and supply the API object
// as the first argument to the callback (for legacy code)
this.onSuccess.call(this.parent, this);
} else {
this.statelem.info('完了');
}
Morebits.wiki.actionCompleted();
return $.Deferred().resolveWith(this.parent, [this]);
},
// only network and server errors reach here - complaints from the API itself are caught in success()
function onAPIfailure(jqXHR, statusText, errorThrown) {
this.statusText = statusText;
this.errorThrown = errorThrown; // frequently undefined
this.errorText = statusText + ' "' + jqXHR.statusText + '" occurred while contacting the API.';
return this.returnError();
}
);
},
returnError: function(callerAjaxParameters) {
if (this.errorCode === 'badtoken' && !this.badtokenRetry) {
this.statelem.warn('トークンが無効です。新しいトークンを取得して再試行します...');
this.badtokenRetry = true;
// Get a new CSRF token and retry. If the original action needs a different
// type of action than CSRF, we do one pointless retry before bailing out
return Morebits.wiki.Api.getToken().then((token) => {
this.query.token = token;
return this.post(callerAjaxParameters);
});
}
this.statelem.error(this.errorText + ' (' + this.errorCode + ')');
// invoke failure callback if one was supplied
if (this.onError) {
// set the callback context to this.parent for new code and supply the API object
// as the first argument to the callback for legacy code
this.onError.call(this.parent, this);
}
// don't complete the action so that the error remains displayed
return $.Deferred().rejectWith(this.parent, [this]);
},
getStatusElement: function() {
return this.statelem;
},
getErrorCode: function() {
return this.errorCode;
},
getErrorText: function() {
return this.errorText;
},
getXML: function() { // retained for backwards compatibility, use getResponse() instead
return this.responseXML;
},
getResponse: function() {
return this.response;
}
};
/**
* Retrieves wikitext from a page. Caching is enabled with a duration of 1 day.
*
* @param {string} title - Page title
* @return {Promise<string|null>} Returns page content, or null if the page doesn't exist.
*/
Morebits.wiki.getCachedPage = function(title) {
return new mw.Api({ userAgent: morebitsWikiApiUserAgent }).get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
rvslots: '*',
format: 'json',
formatversion: '2',
smaxage: '86400', // cache for 1 day
maxage: '86400', // cache for 1 day
uselang: 'content'
}).then((data) => {
const page = data.query.pages[0];
if (page.missing) {
return null;
}
return page.revisions[0].slots.main.content;
});
};
/**
* Retrieves JSON from a page. Caching is enabled with a duration of 1 day.
*
* @param {string} title - Page title
* @return {Promise<string>}
*/
Morebits.wiki.getCachedJson = function(title) {
return Morebits.wiki.getCachedPage(title).then((wikitext) => JSON.parse(wikitext));
};
var morebitsWikiApiUserAgent = 'morebits.js ([[w:WT:TW]])';
/**
* Set the custom user agent header, which is used for server-side logging.
* Note that doing so will set the useragent for every `Morebits.wiki.Api`
* process performed thereafter.
*
* @see {@link https://lists.wikimedia.org/pipermail/mediawiki-api-announce/2014-November/000075.html}
* for original announcement.
*
* @memberof Morebits.wiki.Api
* @param {string} [ua=morebits.js ([[w:WT:TW]])] - User agent. The default
* value of `morebits.js ([[w:WT:TW]])` will be appended to any provided
* value.
*/
Morebits.wiki.Api.setApiUserAgent = function(ua) {
morebitsWikiApiUserAgent = (ua ? ua + ' ' : '') + 'morebits.js ([[w:WT:TW]])';
};
/**
* Get a new CSRF token on encountering token errors.
*
* @memberof Morebits.wiki.Api
* @return {string} MediaWiki CSRF token.
*/
Morebits.wiki.Api.getToken = function() {
var tokenApi = new Morebits.wiki.Api('トークンを取得', {
action: 'query',
meta: 'tokens',
type: 'csrf',
format: 'json'
});
return tokenApi.post().then((apiobj) => apiobj.response.query.tokens.csrftoken);
};
/* **************** Morebits.wiki.Page **************** */
/**
* Use the MediaWiki API to load a page and optionally edit it, move it, etc.
*
* Callers are not permitted to directly access the properties of this class!
* All property access is through the appropriate get___() or set___() method.
*
* Callers should set {@link Morebits.wiki.actionCompleted.notice} and {@link Morebits.wiki.actionCompleted.redirect}
* before the first call to {@link Morebits.wiki.Page.load()}.
*
* Each of the callback functions takes one parameter, which is a
* reference to the Morebits.wiki.Page object that registered the callback.
* Callback functions may invoke any Morebits.wiki.Page prototype method using this reference.
*
*
* Call sequence for common operations (optional final user callbacks not shown):
*
* - Edit current contents of a page (no edit conflict):
* `.load(userTextEditCallback) -> ctx.loadApi.post() ->
* ctx.loadApi.post.success() -> ctx.fnLoadSuccess() -> userTextEditCallback() ->
* .save() -> ctx.saveApi.post() -> ctx.loadApi.post.success() -> ctx.fnSaveSuccess()`
*
* - Edit current contents of a page (with edit conflict):
* `.load(userTextEditCallback) -> ctx.loadApi.post() ->
* ctx.loadApi.post.success() -> ctx.fnLoadSuccess() -> userTextEditCallback() ->
* .save() -> ctx.saveApi.post() -> ctx.loadApi.post.success() ->
* ctx.fnSaveError() -> ctx.loadApi.post() -> ctx.loadApi.post.success() ->
* ctx.fnLoadSuccess() -> userTextEditCallback() -> .save() ->
* ctx.saveApi.post() -> ctx.loadApi.post.success() -> ctx.fnSaveSuccess()`
*
* - Append to a page (similar for prepend and newSection):
* `.append() -> ctx.loadApi.post() -> ctx.loadApi.post.success() ->
* ctx.fnLoadSuccess() -> ctx.fnAutoSave() -> .save() -> ctx.saveApi.post() ->
* ctx.loadApi.post.success() -> ctx.fnSaveSuccess()`
*
* Notes:
* 1. All functions following Morebits.wiki.Api.post() are invoked asynchronously from the jQuery AJAX library.
* 2. The sequence for append/prepend/newSection could be slightly shortened,
* but it would require significant duplication of code for little benefit.
*
* @memberof Morebits.wiki
* @class
* @param {string} pageName - The name of the page, prefixed by the namespace (if any).
* For the current page, use `mw.config.get('wgPageName')`.
* @param {string|Morebits.Status} [status] - A string describing the action about to be undertaken,
* or a Morebits.Status object
*/
Morebits.wiki.Page = function(pageName, status) {
if (!status) {
status = '"' + pageName + '"を開く';
}
/**
* Private context variables.
*
* This context is not visible to the outside, thus all the data here
* must be accessed via getter and setter functions.
*
* @private
*/
const ctx = {
// backing fields for public properties
pageName: pageName,
pageExists: false,
editSummary: null,
changeTags: null,
testActions: null, // array if any valid actions
callbackParameters: null,
statusElement: status instanceof Morebits.Status ? status : new Morebits.Status(status),
// - edit
pageText: null,
editMode: 'all', // save() replaces entire contents of the page by default
appendText: null, // can't reuse pageText for this because pageText is needed to follow a redirect
prependText: null, // can't reuse pageText for this because pageText is needed to follow a redirect
newSectionText: null,
newSectionTitle: null,
createOption: null,
minorEdit: false,
botEdit: false,
pageSection: null,
maxConflictRetries: 2,
maxRetries: 2,
followRedirect: false,
followCrossNsRedirect: true,
watchlistOption: 'nochange',
watchlistExpiry: null,
discussionToolsAutoSubscribe: null,
creator: null,
timestamp: null,
// - revert
revertOldID: null,
// - move
moveDestination: null,
moveTalkPage: false,
moveSubpages: false,
moveSuppressRedirect: false,
// - protect
protectEdit: null,
protectMove: null,
protectCreate: null,
protectCascade: null,
// - delete
deleteTalkPage: false,
// - undelete
undeleteTalkPage: false,
// - creation lookup
lookupNonRedirectCreator: false,
// - stabilize (FlaggedRevs)
flaggedRevs: null,
// internal status
pageLoaded: false,
csrfToken: null,
loadTime: null,
lastEditTime: null,
pageID: null,
contentModel: null,
pageParsed: null,
sections: null,
revertCurID: null,
revertUser: null,
watched: false,
fullyProtected: false,
suppressProtectWarning: false,
conflictRetries: 0,
retries: 0,
// callbacks
onLoadSuccess: null,
onLoadFailure: null,
onParseSuccess: null,
onParseFailure: null,
onSaveSuccess: null,
onSaveFailure: null,
onLookupCreationSuccess: null,
onLookupCreationFailure: null,
onMoveSuccess: null,
onMoveFailure: null,
onDeleteSuccess: null,
onDeleteFailure: null,
onUndeleteSuccess: null,
onUndeleteFailure: null,
onProtectSuccess: null,
onProtectFailure: null,
onStabilizeSuccess: null,
onStabilizeFailure: null,
// internal objects
loadQuery: null,
loadApi: null,
parseApi: null,
saveApi: null,
lookupCreationApi: null,
moveApi: null,
moveProcessApi: null,
patrolApi: null,
patrolProcessApi: null,
triageApi: null,
triageProcessListApi: null,
triageProcessApi: null,
deleteApi: null,
deleteProcessApi: null,
undeleteApi: null,
undeleteProcessApi: null,
protectApi: null,
protectProcessApi: null,
stabilizeApi: null,
stabilizeProcessApi: null
};
const emptyFunction = function() { };
/**
* Loads the text for the page.
*
* @param {Function} onSuccess - Callback function which is called when the load has succeeded.
* @param {Function} [onFailure] - Callback function which is called when the load fails.
*/
this.load = function(onSuccess, onFailure) {
ctx.onLoadSuccess = onSuccess;
ctx.onLoadFailure = onFailure || emptyFunction;
// Need to be able to do something after the page loads
if (!onSuccess) {
ctx.statusElement.error('Internal error: no onSuccess callback provided to load()!');
ctx.onLoadFailure(this);
return;
}
ctx.loadQuery = {
action: 'query',
prop: 'info|revisions',
inprop: 'watched',
intestactions: 'edit', // can be expanded
curtimestamp: '',
meta: 'tokens',
type: 'csrf',
titles: ctx.pageName,
format: 'json'
// don't need rvlimit=1 because we don't need rvstartid here and only one actual rev is returned by default
};
if (ctx.editMode === 'all') {
ctx.loadQuery.rvprop = 'content|timestamp'; // get the page content at the same time, if needed
} else if (ctx.editMode === 'revert') {
ctx.loadQuery.rvprop = 'timestamp';
ctx.loadQuery.rvlimit = 1;
ctx.loadQuery.rvstartid = ctx.revertOldID;
}
if (ctx.followRedirect) {
ctx.loadQuery.redirects = ''; // follow all redirects
}
if (typeof ctx.pageSection === 'number') {
ctx.loadQuery.rvsection = ctx.pageSection;
}
if (Morebits.userIsSysop) {
ctx.loadQuery.inprop += '|protection';
}
ctx.loadApi = new Morebits.wiki.Api('ページを取得中...', ctx.loadQuery, fnLoadSuccess, ctx.statusElement, ctx.onLoadFailure);
ctx.loadApi.setParent(this);
ctx.loadApi.post();
};
/**
* parse the text for the page.
*
* @param {Function} onSuccess - Callback function which is called when the load has succeeded.
* @param {Function} [onFailure] - Callback function which is called when the load fails.
*/
this.parse = function(onSuccess, onFailure) {
ctx.onParseSuccess = onSuccess;
ctx.onParseFailure = onFailure || emptyFunction;
// Need to be able to do something after the page loads
if (!onSuccess) {
ctx.statusElement.error('Internal error: no onSuccess callback provided to parse()!');
ctx.onParseFailure(this);
return;
}
var query = {
action: 'parse',
format: 'json',
page: ctx.pageName,
prop: 'sections',
utf8: 1
};
ctx.parseApi = new Morebits.wiki.Api('ページを取得中...', query, fnParseSuccess, ctx.statusElement, ctx.onParseFailure);
ctx.parseApi.setParent(this);
ctx.parseApi.post();
};
/**
* Saves the text for the page to Wikipedia.
* Must be preceded by successfully calling `load()`.
*
* Warning: Calling `save()` can result in additional calls to the
* previous `load()` callbacks to recover from edit conflicts! In this
* case, callers must make the same edit to the new pageText and
* re-invoke `save()`. This behavior can be disabled with
* `setMaxConflictRetries(0)`.
*
* @param {Function} [onSuccess] - Callback function which is called when the save has succeeded.
* @param {Function} [onFailure] - Callback function which is called when the save fails.
*/
this.save = function(onSuccess, onFailure) {
ctx.onSaveSuccess = onSuccess;
ctx.onSaveFailure = onFailure || emptyFunction;
// are we getting our editing token from mw.user.tokens?
const canUseMwUserToken = fnCanUseMwUserToken('edit');
if (!ctx.pageLoaded && !canUseMwUserToken) {
ctx.statusElement.error('Internal error: attempt to save a page that has not been loaded!');
ctx.onSaveFailure(this);
return;
}
if (!ctx.editSummary) {
// new section mode allows (nay, encourages) using the
// title as the edit summary, but the query needs
// editSummary to be undefined or '', not null
if (ctx.editMode === 'new' && ctx.newSectionTitle) {
ctx.editSummary = '';
} else {
ctx.statusElement.error('Internal error: edit summary not set before save!');
ctx.onSaveFailure(this);
return;
}
}
// shouldn't happen if canUseMwUserToken === true
if (ctx.fullyProtected && !ctx.suppressProtectWarning &&
!confirm(
ctx.fullyProtected === 'infinity' ?
'全保護されたページ "' + ctx.pageName + '" (無期限保護) を編集しようとしています。\n\n編集を続行するにはOKをクリックし、この編集をスキップするにはキャンセルをクリックしてください。' :
'全保護されたページ "' + ctx.pageName +
'" (保護期限: ' + new Morebits.Date(ctx.fullyProtected).calendar('utc') + ' (UTC)) を編集しようとしています。\n\n編集を続行するにはOKをクリックし、この編集をスキップするにはキャンセルをクリックしてください。'
)
) {
ctx.statusElement.error('全保護されたページの編集が中断されました。');
ctx.onSaveFailure(this);
return;
}
ctx.retries = 0;
const query = {
action: 'edit',
title: ctx.pageName,
summary: ctx.editSummary,
token: canUseMwUserToken ? mw.user.tokens.get('csrfToken') : ctx.csrfToken,
watchlist: ctx.watchlistOption,
format: 'json'
};
if (ctx.changeTags) {
query.tags = ctx.changeTags;
}
if (fnApplyWatchlistExpiry()) {
query.watchlistexpiry = ctx.watchlistExpiry;
}
if (typeof ctx.pageSection === 'number') {
query.section = ctx.pageSection;
}
// Set minor edit attribute. If these parameters are present with any value, it is interpreted as true
if (ctx.minorEdit) {
query.minor = true;
} else {
query.notminor = true; // force Twinkle config to override user preference setting for "all edits are minor"
}
// Set bot edit attribute. If this parameter is present with any value, it is interpreted as true
if (ctx.botEdit) {
query.bot = true;
}
if (ctx.discussionToolsAutoSubscribe !== null) {
query.discussiontoolsautosubscribe = ctx.discussionToolsAutoSubscribe ? 'yes' : 'no';
}
switch (ctx.editMode) {
case 'append':
if (ctx.appendText === null) {
ctx.statusElement.error('Internal error: append text not set before save!');
ctx.onSaveFailure(this);
return;
}
query.appendtext = ctx.appendText; // use mode to append to current page contents
break;
case 'prepend':
if (ctx.prependText === null) {
ctx.statusElement.error('Internal error: prepend text not set before save!');
ctx.onSaveFailure(this);
return;
}
query.prependtext = ctx.prependText; // use mode to prepend to current page contents
break;
case 'new':
if (!ctx.newSectionText) { // API doesn't allow empty new section text
ctx.statusElement.error('Internal error: new section text not set before save!');
ctx.onSaveFailure(this);
return;
}
query.section = 'new';
query.text = ctx.newSectionText; // add a new section to current page
query.sectiontitle = ctx.newSectionTitle || ctx.editSummary; // done by the API, but non-'' values would get treated as text
break;
case 'revert':
query.undo = ctx.revertCurID;
query.undoafter = ctx.revertOldID;
if (ctx.lastEditTime) {
query.basetimestamp = ctx.lastEditTime; // check that page hasn't been edited since it was loaded
}
query.starttimestamp = ctx.loadTime; // check that page hasn't been deleted since it was loaded (don't recreate bad stuff)
break;
default: // 'all'
query.text = ctx.pageText; // replace entire contents of the page
if (ctx.lastEditTime) {
query.basetimestamp = ctx.lastEditTime; // check that page hasn't been edited since it was loaded
}
query.starttimestamp = ctx.loadTime; // check that page hasn't been deleted since it was loaded (don't recreate bad stuff)
break;
}
if (['recreate', 'createonly', 'nocreate'].includes(ctx.createOption)) {
query[ctx.createOption] = '';
}
if (canUseMwUserToken && ctx.followRedirect) {
query.redirect = true;
}
ctx.saveApi = new Morebits.wiki.Api('ページを保存中...', query, fnSaveSuccess, ctx.statusElement, fnSaveError);
ctx.saveApi.setParent(this);
ctx.saveApi.post();
};
/**
* Adds the text provided via `setAppendText()` to the end of the
* page. Does not require calling `load()` first, unless a watchlist
* expiry is used.
*
* @param {Function} [onSuccess] - Callback function which is called when the method has succeeded.
* @param {Function} [onFailure] - Callback function which is called when the method fails.
*/
this.append = function(onSuccess, onFailure) {
ctx.editMode = 'append';
if (fnCanUseMwUserToken('edit')) {
this.save(onSuccess, onFailure);
} else {
ctx.onSaveSuccess = onSuccess;
ctx.onSaveFailure = onFailure || emptyFunction;
this.load(fnAutoSave, ctx.onSaveFailure);
}
};
/**
* Adds the text provided via `setPrependText()` to the start of the
* page. Does not require calling `load()` first, unless a watchlist
* expiry is used.
*
* @param {Function} [onSuccess] - Callback function which is called when the method has succeeded.
* @param {Function} [onFailure] - Callback function which is called when the method fails.
*/
this.prepend = function(onSuccess, onFailure) {
ctx.editMode = 'prepend';
if (fnCanUseMwUserToken('edit')) {
this.save(onSuccess, onFailure);
} else {
ctx.onSaveSuccess = onSuccess;
ctx.onSaveFailure = onFailure || emptyFunction;
this.load(fnAutoSave, ctx.onSaveFailure);
}
};
/**
* Creates a new section with the text provided by `setNewSectionText()`
* and section title from `setNewSectionTitle()`.
* If `editSummary` is provided, that will be used instead of the
* autogenerated "->Title (new section" edit summary.
* Does not require calling `load()` first, unless a watchlist expiry
* is used.
*
* @param {Function} [onSuccess] - Callback function which is called when the method has succeeded.
* @param {Function} [onFailure] - Callback function which is called when the method fails.
*/
this.newSection = function(onSuccess, onFailure) {
ctx.editMode = 'new';
if (fnCanUseMwUserToken('edit')) {
this.save(onSuccess, onFailure);
} else {
ctx.onSaveSuccess = onSuccess;
ctx.onSaveFailure = onFailure || emptyFunction;
this.load(fnAutoSave, ctx.onSaveFailure);
}
};
/** @return {string} The name of the loaded page, including the namespace */
this.getPageName = function() {
return ctx.pageName;
};
/** @return {string} The text of the page after a successful load() */
this.getPageText = function() {
return ctx.pageText;
};
/** @return {string} Sections of the page after a successful parse() */
this.getSections = function() {
return ctx.sections;
};
/** @param {string} pageText - Updated page text that will be saved when `save()` is called */
this.setPageText = function(pageText) {
ctx.editMode = 'all';
ctx.pageText = pageText;
};
/** @param {string} appendText - Text that will be appended to the page when `append()` is called */
this.setAppendText = function(appendText) {
ctx.editMode = 'append';
ctx.appendText = appendText;
};
/** @param {string} prependText - Text that will be prepended to the page when `prepend()` is called */
this.setPrependText = function(prependText) {
ctx.editMode = 'prepend';
ctx.prependText = prependText;
};
/** @param {string} newSectionText - Text that will be added in a new section on the page when `newSection()` is called */
this.setNewSectionText = function(newSectionText) {
ctx.editMode = 'new';
ctx.newSectionText = newSectionText;
};
/**
* @param {string} newSectionTitle - Title for the new section created when `newSection()` is called
* If missing, `ctx.editSummary` will be used. Issues may occur if a substituted template is used.
*/
this.setNewSectionTitle = function(newSectionTitle) {
ctx.editMode = 'new';
ctx.newSectionTitle = newSectionTitle;
};
// Edit-related setter methods:
/**
* Set the edit summary that will be used when `save()` is called.
* Unnecessary if editMode is 'new' and newSectionTitle is provided.
*
* @param {string} summary
*/
this.setEditSummary = function(summary) {
ctx.editSummary = summary;
};
/**
* Set any custom tag(s) to be applied to the API action.
* A number of actions don't support it, most notably watch, review,
* and stabilize ({@link https://phabricator.wikimedia.org/T247721|T247721}).
*
* @param {string|string[]} tags - String or array of tag(s).
*/
this.setChangeTags = function(tags) {
ctx.changeTags = tags;
};
/**
* @param {string} [createOption=null] - Can take the following four values:
* - recreate: create the page if it does not exist, or edit it if it exists.
* - createonly: create the page if it does not exist, but return an
* error if it already exists.
* - nocreate: don't create the page, only edit it if it already exists.
* - `null`: create the page if it does not exist, unless it was deleted
* in the moment between loading the page and saving the edit (default).
*/
this.setCreateOption = function(createOption) {
ctx.createOption = createOption;
};
/** @param {boolean} minorEdit - Set true to mark the edit as a minor edit. */
this.setMinorEdit = function(minorEdit) {
ctx.minorEdit = minorEdit;
};
/** @param {boolean} botEdit - Set true to mark the edit as a bot edit */
this.setBotEdit = function(botEdit) {
ctx.botEdit = botEdit;
};
/**
* @param {number} pageSection - Integer specifying the section number to load or save.
* If specified as `null`, the entire page will be retrieved.
*/
this.setPageSection = function(pageSection) {
ctx.pageSection = pageSection;
};
/**
* @param {number} maxConflictRetries - Number of retries for save errors involving an edit conflict or
* loss of token. Default: 2.
*/
this.setMaxConflictRetries = function(maxConflictRetries) {
ctx.maxConflictRetries = maxConflictRetries;
};
/**
* @param {number} maxRetries - Number of retries for save errors not involving an edit conflict or
* loss of token. Default: 2.
*/
this.setMaxRetries = function(maxRetries) {
ctx.maxRetries = maxRetries;
};
/**
* Set whether and how to watch the page, including setting an expiry.
*
* @param {boolean|string|Morebits.Date|Date} [watchlistOption=false] -
* Basically a mix of MW API and Twinkley options available pre-expiry:
* - `true`|`'yes'`|`'watch'`: page will be added to the user's
* watchlist when the action is called. Defaults to an indefinite
* watch unless `watchlistExpiry` is provided.
* - `false`|`'no'`|`'nochange'`: watchlist status of the page (including expiry) will not be changed.
* - `'default'`|`'preferences'`: watchlist status of the page will be
* set based on the user's preference settings when the action is
* called. Defaults to an indefinite watch unless `watchlistExpiry` is
* provided.
* - `'unwatch'`: explicitly unwatch the page.
* - Any other `string` or `number`, or a `Morebits.Date` or `Date`
* object: watch page until the specified time, deferring to
* `watchlistExpiry` if provided.
* @param {string|number|Morebits.Date|Date} [watchlistExpiry=infinity] -
* A date-like string or number, or a date object. If a string or number,
* can be relative (2 weeks) or other similarly date-like (i.e. NOT "potato"):
* ISO 8601: 2038-01-09T03:14:07Z
* MediaWiki: 20380109031407
* UNIX: 2147483647
* SQL: 2038-01-09 03:14:07
* Can also be `infinity` or infinity-like (`infinite`, `indefinite`, and `never`).
* See {@link https://phabricator.wikimedia.org/source/mediawiki-libs-Timestamp/browse/master/src/ConvertibleTimestamp.php;4e53b859a9580c55958078f46dd4f3a44d0fcaa0$57-109?as=source&blame=off}
*/
this.setWatchlist = function(watchlistOption, watchlistExpiry) {
if (watchlistOption instanceof Morebits.Date || watchlistOption instanceof Date) {
watchlistOption = watchlistOption.toISOString();
}
if (typeof watchlistExpiry === 'undefined') {
watchlistExpiry = 'infinity';
} else if (watchlistExpiry instanceof Morebits.Date || watchlistExpiry instanceof Date) {
watchlistExpiry = watchlistExpiry.toISOString();
}
switch (watchlistOption) {
case 'nochange':
case 'no':
case false:
case undefined:
ctx.watchlistOption = 'nochange';
// The MW API allows for changing expiry with nochange (as "nochange" refers to the binary status),
// but by keeping this null it will default to any existing expiry, ensure there is actually "no change."
ctx.watchlistExpiry = null;
break;
case 'unwatch':
// expiry unimportant
ctx.watchlistOption = 'unwatch';
break;
case 'preferences':
case 'default':
ctx.watchlistOption = 'preferences';
// The API allows an expiry here, but there is as of yet (T265716)
// no expiry preference option, so it's a bit devoid of context.
ctx.watchlistExpiry = watchlistExpiry;
break;
case 'watch':
case 'yes':
case true:
ctx.watchlistOption = 'watch';
ctx.watchlistExpiry = watchlistExpiry;
break;
default: // Not really a "default" per se but catches "any other string"
ctx.watchlistOption = 'watch';
ctx.watchlistExpiry = watchlistOption;
break;
}
};
/**
* Set a watchlist expiry. setWatchlist can mostly handle this by
* itself, so this is here largely for completeness and compatibility
* with the full suite of options.
*
* @param {string|number|Morebits.Date|Date} [watchlistExpiry=infinity] -
* A date-like string or number, or a date object. If a string or number,
* can be relative (2 weeks) or other similarly date-like (i.e. NOT "potato"):
* ISO 8601: 2038-01-09T03:14:07Z
* MediaWiki: 20380109031407
* UNIX: 2147483647
* SQL: 2038-01-09 03:14:07
* Can also be `infinity` or infinity-like (`infinite`, `indefinite`, and `never`).
* See {@link https://phabricator.wikimedia.org/source/mediawiki-libs-Timestamp/browse/master/src/ConvertibleTimestamp.php;4e53b859a9580c55958078f46dd4f3a44d0fcaa0$57-109?as=source&blame=off}
*/
this.setWatchlistExpiry = function(watchlistExpiry) {
if (typeof watchlistExpiry === 'undefined') {
watchlistExpiry = 'infinity';
} else if (watchlistExpiry instanceof Morebits.Date || watchlistExpiry instanceof Date) {
watchlistExpiry = watchlistExpiry.toISOString();
}
ctx.watchlistExpiry = watchlistExpiry;
};
/**
* If editing a talk page, set whether to subscribe to any talk
* page thread created by the edit.
*
* @param {boolean} subscribe
*/
this.setDiscussionToolsAutoSubscribe = function(subscribe) {
ctx.discussionToolsAutoSubscribe = subscribe;
};
/**
* @param {boolean} [followRedirect=false] -
* - `true`: a maximum of one redirect will be followed. In the event
* of a redirect, a message is displayed to the user and the redirect
* target can be retrieved with getPageName().
* - `false`: (default) the requested pageName will be used without regard to any redirect.
* @param {boolean} [followCrossNsRedirect=true] - Not applicable if `followRedirect` is not set true.
* - `true`: (default) follow redirect even if it is a cross-namespace redirect
* - `false`: don't follow redirect if it is cross-namespace, edit the redirect itself.
*/
this.setFollowRedirect = function(followRedirect, followCrossNsRedirect) {
if (ctx.pageLoaded) {
ctx.statusElement.error('Internal error: cannot change redirect setting after the page has been loaded!');
return;
}
ctx.followRedirect = followRedirect;
ctx.followCrossNsRedirect = typeof followCrossNsRedirect !== 'undefined' ? followCrossNsRedirect : ctx.followCrossNsRedirect;
};
// lookup-creation setter function
/**
* @param {boolean} flag - If set true, the author and timestamp of
* the first non-redirect version of the page is retrieved.
*
* Warning:
* 1. If there are no revisions among the first 50 that are
* non-redirects, or if there are less 50 revisions and all are
* redirects, the original creation is retrieved.
* 2. Revisions that the user is not privileged to access
* (revdeled/suppressed) will be treated as non-redirects.
* 3. Must not be used when the page has a non-wikitext contentmodel
* such as Modulespace Lua or user JavaScript/CSS.
*/
this.setLookupNonRedirectCreator = function(flag) {
ctx.lookupNonRedirectCreator = flag;
};
// Move-related setter functions
/** @param {string} destination */
this.setMoveDestination = function(destination) {
ctx.moveDestination = destination;
};
/** @param {boolean} flag */
this.setMoveTalkPage = function(flag) {
ctx.moveTalkPage = !!flag;
};
/** @param {boolean} flag */
this.setMoveSubpages = function(flag) {
ctx.moveSubpages = !!flag;
};
/** @param {boolean} flag */
this.setMoveSuppressRedirect = function(flag) {
ctx.moveSuppressRedirect = !!flag;
};
// Protect-related setter functions
/**
* @param {string} level - The right required for the specific action
* e.g. autoconfirmed, sysop, templateeditor, extendedconfirmed
* (enWiki-only).
* @param {string} [expiry=infinity]
*/
this.setEditProtection = function(level, expiry) {
ctx.protectEdit = { level: level, expiry: expiry || 'infinity' };
};
this.setMoveProtection = function(level, expiry) {
ctx.protectMove = { level: level, expiry: expiry || 'infinity' };
};
this.setCreateProtection = function(level, expiry) {
ctx.protectCreate = { level: level, expiry: expiry || 'infinity' };
};
this.setCascadingProtection = function(flag) {
ctx.protectCascade = !!flag;
};
this.suppressProtectWarning = function() {
ctx.suppressProtectWarning = true;
};
// Delete-related setter
/** @param {boolean} flag */
this.setDeleteTalkPage = function(flag) {
ctx.deleteTalkPage = !!flag;
};
// Undelete-related setter
/** @param {boolean} flag */
this.setUndeleteTalkPage = function(flag) {
ctx.undeleteTalkPage = !!flag;
};
// Revert-related getters/setters:
this.setOldID = function(oldID) {
ctx.revertOldID = oldID;
};
/** @return {string} The current revision ID of the page */
this.getCurrentID = function() {
return ctx.revertCurID;
};
/** @return {string} Last editor of the page */
this.getRevisionUser = function() {
return ctx.revertUser;
};
/** @return {string} ISO 8601 timestamp at which the page was last edited. */
this.getLastEditTime = function() {
return ctx.lastEditTime;
};
// Miscellaneous getters/setters:
/**
* Define an object for use in a callback function.
*
* `callbackParameters` is for use by the caller only. The parameters
* allow a caller to pass the proper context into its callback
* function. Callers must ensure that any changes to the
* callbackParameters object within a `load()` callback still permit a
* proper re-entry into the `load()` callback if an edit conflict is
* detected upon calling `save()`.
*
* @param {Object} callbackParameters
*/
this.setCallbackParameters = function(callbackParameters) {
ctx.callbackParameters = callbackParameters;
};
/**
* @return {Object} - The object previously set by `setCallbackParameters()`.
*/
this.getCallbackParameters = function() {
return ctx.callbackParameters;
};
/**
* @param {Morebits.Status} statusElement
*/
this.setStatusElement = function(statusElement) {
ctx.statusElement = statusElement;
};
/**
* @return {Morebits.Status} Status element created by the constructor.
*/
this.getStatusElement = function() {
return ctx.statusElement;
};
/**
* @param {string} level - The right required for edits not to require
* review. Possible options: none, autoconfirmed, review (not on enWiki).
* @param {string} [expiry=infinity]
*/
this.setFlaggedRevs = function(level, expiry) {
ctx.flaggedRevs = { level: level, expiry: expiry || 'infinity' };
};
/**
* @return {boolean} True if the page existed on the wiki when it was last loaded.
*/
this.exists = function() {
return ctx.pageExists;
};
/**
* @return {string} Page ID of the page loaded. 0 if the page doesn't
* exist.
*/
this.getPageID = function() {
return ctx.pageID;
};
/**
* @return {string} - Content model of the page. Possible values
* include (but may not be limited to): `wikitext`, `javascript`,
* `css`, `json`, `Scribunto`, `sanitized-css`, `MassMessageListContent`.
* Also gettable via `mw.config.get('wgPageContentModel')`.
*/
this.getContentModel = function() {
return ctx.contentModel;
};
/**
* @return {boolean|string} - Watched status of the page. Boolean
* unless it's being watched temporarily, in which case returns the
* expiry string.
*/
this.getWatched = function() {
return ctx.watched;
};
/**
* @return {string} ISO 8601 timestamp at which the page was last loaded.
*/
this.getLoadTime = function() {
return ctx.loadTime;
};
/**
* @return {string} The user who created the page following `lookupCreation()`.
*/
this.getCreator = function() {
return ctx.creator;
};
/**
* @return {string} The ISOString timestamp of page creation following `lookupCreation()`.
*/
this.getCreationTimestamp = function() {
return ctx.timestamp;
};
/** @return {boolean} whether or not you can edit the page */
this.canEdit = function() {
return !!ctx.testActions && ctx.testActions.includes('edit');
};
/**
* Retrieves the username of the user who created the page as well as
* the timestamp of creation. The username can be retrieved using the
* `getCreator()` function; the timestamp can be retrieved using the
* `getCreationTimestamp()` function.
* Prior to June 2019 known as `lookupCreator()`.
*
* @param {Function} onSuccess - Callback function to be called when
* the username and timestamp are found within the callback.
* @param {Function} [onFailure] - Callback function to be called when
* the lookup fails
*/
this.lookupCreation = function(onSuccess, onFailure) {
ctx.onLookupCreationSuccess = onSuccess;
ctx.onLookupCreationFailure = onFailure || emptyFunction;
if (!onSuccess) {
ctx.statusElement.error('Internal error: no onSuccess callback provided to lookupCreation()!');
ctx.onLookupCreationFailure(this);
return;
}
const query = {
action: 'query',
prop: 'revisions',
titles: ctx.pageName,
rvlimit: 1,
rvprop: 'user|timestamp',
rvdir: 'newer',
format: 'json'
};
// Only the wikitext content model can reliably handle
// rvsection, others return an error when paired with the
// content rvprop. Relatedly, non-wikitext models don't
// understand the #REDIRECT concept, so we shouldn't attempt
// the redirect resolution in fnLookupCreationSuccess
if (ctx.lookupNonRedirectCreator) {
query.rvsection = 0;
query.rvprop += '|content';
}
if (ctx.followRedirect) {
query.redirects = ''; // follow all redirects
}
ctx.lookupCreationApi = new Morebits.wiki.Api('ページ作成情報の取得中', query, fnLookupCreationSuccess, ctx.statusElement, ctx.onLookupCreationFailure);
ctx.lookupCreationApi.setParent(this);
ctx.lookupCreationApi.post();
};
/**
* Reverts a page to `revertOldID` set by `setOldID`.
*
* @param {Function} [onSuccess] - Callback function to run on success.
* @param {Function} [onFailure] - Callback function to run on failure.
*/
this.revert = function(onSuccess, onFailure) {
ctx.onSaveSuccess = onSuccess;
ctx.onSaveFailure = onFailure || emptyFunction;
if (!ctx.revertOldID) {
ctx.statusElement.error('Internal error: revision ID to revert to was not set before revert!');
ctx.onSaveFailure(this);
return;
}
ctx.editMode = 'revert';
this.load(fnAutoSave, ctx.onSaveFailure);
};
/**
* Moves a page to another title.
*
* @param {Function} [onSuccess] - Callback function to run on success.
* @param {Function} [onFailure] - Callback function to run on failure.
*/
this.move = function(onSuccess, onFailure) {
ctx.onMoveSuccess = onSuccess;
ctx.onMoveFailure = onFailure || emptyFunction;
if (!fnPreflightChecks.call(this, 'move', ctx.onMoveFailure)) {
return; // abort
}
if (!ctx.moveDestination) {
ctx.statusElement.error('Internal error: destination page name was not set before move!');
ctx.onMoveFailure(this);
return;
}
if (fnCanUseMwUserToken('move')) {
fnProcessMove.call(this, this);
} else {
const query = fnNeedTokenInfoQuery('move');
ctx.moveApi = new Morebits.wiki.Api('トークンの取得中...', query, fnProcessMove, ctx.statusElement, ctx.onMoveFailure);
ctx.moveApi.setParent(this);
ctx.moveApi.post();
}
};
/**
* Marks the page as patrolled, using `rcid` (if available) or `revid`.
*
* Patrolling as such doesn't need to rely on loading the page in
* question; simply passing a revid to the API is sufficient, so in
* those cases just using {@link Morebits.wiki.Api} is probably preferable.
*
* No error handling since we don't actually care about the errors.
*/
this.patrol = function() {
if (!Morebits.userIsSysop && !Morebits.userIsInGroup('patroller')) {
return;
}
// If a link is present, don't need to check if it's patrolled
if ($('.patrollink').length) {
const patrolhref = $('.patrollink a').attr('href');
ctx.rcid = mw.util.getParamValue('rcid', patrolhref);
fnProcessPatrol(this, this);
} else {
const patrolQuery = {
action: 'query',
prop: 'info',
meta: 'tokens',
type: 'patrol', // as long as we're querying, might as well get a token
list: 'recentchanges', // check if the page is unpatrolled
titles: ctx.pageName,
rcprop: 'patrolled',
rctitle: ctx.pageName,
rclimit: 1,
format: 'json'
};
ctx.patrolApi = new Morebits.wiki.Api('トークンの取得中...', patrolQuery, fnProcessPatrol);
ctx.patrolApi.setParent(this);
ctx.patrolApi.post();
}
};
/**
* Marks the page as reviewed by the PageTriage extension.
*
* Will, by it's nature, mark as patrolled as well. Falls back to
* patrolling if not in an appropriate namespace.
*
* Doesn't inherently rely on loading the page in question; simply
* passing a `pageid` to the API is sufficient, so in those cases just
* using {@link Morebits.wiki.Api} is probably preferable.
*
* Will first check if the page is queued via
* {@link Morebits.wiki.Page~fnProcessTriageList|fnProcessTriageList}.
*
* No error handling since we don't actually care about the errors.
*
* @see {@link https://www.mediawiki.org/wiki/Extension:PageTriage} Referred to as "review" on-wiki.
*/
this.triage = function() {
// Fall back to patrol if not a valid triage namespace
if (!mw.config.get('pageTriageNamespaces').includes(new mw.Title(ctx.pageName).getNamespaceId())) {
this.patrol();
} else {
if (!Morebits.userIsSysop && !Morebits.userIsInGroup('patroller')) {
return;
}
// If on the page in question, don't need to query for page ID
if (new mw.Title(Morebits.pageNameNorm).getPrefixedText() === new mw.Title(ctx.pageName).getPrefixedText()) {
ctx.pageID = mw.config.get('wgArticleId');
fnProcessTriageList(this, this);
} else {
const query = fnNeedTokenInfoQuery('triage');
ctx.triageApi = new Morebits.wiki.Api('トークンの取得中...', query, fnProcessTriageList);
ctx.triageApi.setParent(this);
ctx.triageApi.post();
}
}
};
// |delete| is a reserved word in some flavours of JS
/**
* Deletes a page (for admins only).
*
* @param {Function} [onSuccess] - Callback function to run on success.
* @param {Function} [onFailure] - Callback function to run on failure.
*/
this.deletePage = function(onSuccess, onFailure) {
ctx.onDeleteSuccess = onSuccess;
ctx.onDeleteFailure = onFailure || emptyFunction;
if (!fnPreflightChecks.call(this, 'delete', ctx.onDeleteFailure)) {
return; // abort
}
if (fnCanUseMwUserToken('delete')) {
fnProcessDelete.call(this, this);
} else {
const query = fnNeedTokenInfoQuery('delete');
ctx.deleteApi = new Morebits.wiki.Api('トークンの取得中...', query, fnProcessDelete, ctx.statusElement, ctx.onDeleteFailure);
ctx.deleteApi.setParent(this);
ctx.deleteApi.post();
}
};
/**
* Undeletes a page (for admins only).
*
* @param {Function} [onSuccess] - Callback function to run on success.
* @param {Function} [onFailure] - Callback function to run on failure.
*/
this.undeletePage = function(onSuccess, onFailure) {
ctx.onUndeleteSuccess = onSuccess;
ctx.onUndeleteFailure = onFailure || emptyFunction;
if (!fnPreflightChecks.call(this, 'undelete', ctx.onUndeleteFailure)) {
return; // abort
}
if (fnCanUseMwUserToken('undelete')) {
fnProcessUndelete.call(this, this);
} else {
const query = fnNeedTokenInfoQuery('undelete');
ctx.undeleteApi = new Morebits.wiki.Api('トークンの取得中...', query, fnProcessUndelete, ctx.statusElement, ctx.onUndeleteFailure);
ctx.undeleteApi.setParent(this);
ctx.undeleteApi.post();
}
};
/**
* Protects a page (for admins only).
*
* @param {Function} [onSuccess] - Callback function to run on success.
* @param {Function} [onFailure] - Callback function to run on failure.
*/
this.protect = function(onSuccess, onFailure) {
ctx.onProtectSuccess = onSuccess;
ctx.onProtectFailure = onFailure || emptyFunction;
if (!fnPreflightChecks.call(this, 'protect', ctx.onProtectFailure)) {
return; // abort
}
if (!ctx.protectEdit && !ctx.protectMove && !ctx.protectCreate) {
ctx.statusElement.error('Internal error: you must set edit and/or move and/or create protection before calling protect()!');
ctx.onProtectFailure(this);
return;
}
// because of the way MW API interprets protection levels
// (absolute, not differential), we always need to request
// protection levels from the server
const query = fnNeedTokenInfoQuery('protect');
ctx.protectApi = new Morebits.wiki.Api('トークンの取得中...', query, fnProcessProtect, ctx.statusElement, ctx.onProtectFailure);
ctx.protectApi.setParent(this);
ctx.protectApi.post();
};
/**
* Apply FlaggedRevs protection settings. Only works on wikis where
* the extension is installed (`$wgFlaggedRevsProtection = true`
* i.e. where FlaggedRevs settings appear on the "protect" tab).
*
* @see {@link https://www.mediawiki.org/wiki/Extension:FlaggedRevs}
* Referred to as "pending changes" on-wiki.
*
* @param {Function} [onSuccess]
* @param {Function} [onFailure]
*/
this.stabilize = function(onSuccess, onFailure) {
ctx.onStabilizeSuccess = onSuccess;
ctx.onStabilizeFailure = onFailure || emptyFunction;
if (!fnPreflightChecks.call(this, 'FlaggedRevs', ctx.onStabilizeFailure)) {
return; // abort
}
if (!ctx.flaggedRevs) {
ctx.statusElement.error('Internal error: you must set flaggedRevs before calling stabilize()!');
ctx.onStabilizeFailure(this);
return;
}
if (fnCanUseMwUserToken('stabilize')) {
fnProcessStabilize.call(this, this);
} else {
const query = fnNeedTokenInfoQuery('stabilize');
ctx.stabilizeApi = new Morebits.wiki.Api('トークンの取得中...', query, fnProcessStabilize, ctx.statusElement, ctx.onStabilizeFailure);
ctx.stabilizeApi.setParent(this);
ctx.stabilizeApi.post();
}
};
/*
* Private member functions
* These are not exposed outside
*/
/**
* Determines whether we can save an API call by using the csrf token
* sent with the page HTML, or whether we need to ask the server for
* more info (e.g. protection or watchlist expiry).
*
* Currently used for `append`, `prepend`, `newSection`, `move`,
* `stabilize`, `deletePage`, and `undeletePage`. Not used for
* `protect` since it always needs to request protection status.
*
* @param {string} [action=edit] - The action being undertaken, e.g.
* "edit" or "delete". In practice, only "edit" or "notedit" matters.
* @return {boolean}
*/
var fnCanUseMwUserToken = function(action = 'edit') {
// If a watchlist expiry is set, we must always load the page
// to avoid overwriting indefinite protection. Of course, not
// needed if setting indefinite watching!
if (ctx.watchlistExpiry && !Morebits.string.isInfinity(ctx.watchlistExpiry)) {
return false;
}
// API-based redirect resolution only works for action=query and
// action=edit in append/prepend/new modes
if (ctx.followRedirect) {
if (!ctx.followCrossNsRedirect) {
return false; // must load the page to check for cross namespace redirects
}
if (action !== 'edit' || (ctx.editMode === 'all' || ctx.editMode === 'revert')) {
return false;
}
}
// do we need to fetch the edit protection expiry?
if (Morebits.userIsSysop && !ctx.suppressProtectWarning) {
if (new mw.Title(Morebits.pageNameNorm).getPrefixedText() !== new mw.Title(ctx.pageName).getPrefixedText()) {
return false;
}
// wgRestrictionEdit is null on non-existent pages,
// so this neatly handles nonexistent pages
const editRestriction = mw.config.get('wgRestrictionEdit');
if (!editRestriction || editRestriction.includes('sysop')) {
return false;
}
}
return !!mw.user.tokens.get('csrfToken');
};
/**
* When functions can't use
* {@link Morebits.wiki.Page~fnCanUseMwUserToken|fnCanUseMwUserToken}
* or require checking protection or watched status, maintain the query
* in one place. Used for {@link Morebits.wiki.Page#deletePage|delete},
* {@link Morebits.wiki.Page#undeletePage|undelete},
* {@link Morebits.wiki.Page#protect|protect},
* {@link Morebits.wiki.Page#stabilize|stabilize},
* and {@link Morebits.wiki.Page#move|move}
* (basically, just not {@link Morebits.wiki.Page#load|load}).
*
* @param {string} action - The action being undertaken, e.g. "edit" or
* "delete".
* @return {Object} Appropriate query.
*/
var fnNeedTokenInfoQuery = function(action) {
const query = {
action: 'query',
meta: 'tokens',
type: 'csrf',
titles: ctx.pageName,
prop: 'info',
inprop: 'watched',
format: 'json'
};
// Protection not checked for flagged-revs or non-sysop moves
if (action !== 'stabilize' && (action !== 'move' || Morebits.userIsSysop)) {
query.inprop += '|protection';
}
if (ctx.followRedirect && action !== 'undelete') {
query.redirects = ''; // follow all redirects
}
return query;
};
// callback from loadSuccess() for append(), prepend(), and newSection() threads
var fnAutoSave = function(pageobj) {
pageobj.save(ctx.onSaveSuccess, ctx.onSaveFailure);
};
// callback from loadApi.post()
var fnLoadSuccess = function() {
const response = ctx.loadApi.getResponse().query;
if (!fnCheckPageName(response, ctx.onLoadFailure)) {
return; // abort
}
const page = response.pages[0];
let rev;
ctx.pageExists = !page.missing;
if (ctx.pageExists) {
rev = page.revisions[0];
ctx.lastEditTime = rev.timestamp;
ctx.pageText = rev.content;
ctx.pageID = page.pageid;
} else {
ctx.pageText = ''; // allow for concatenation, etc.
ctx.pageID = 0; // nonexistent in response, matches wgArticleId
}
ctx.csrfToken = response.tokens.csrftoken;
if (!ctx.csrfToken) {
ctx.statusElement.error('編集トークンの取得に失敗しました。');
ctx.onLoadFailure(this);
return;
}
ctx.loadTime = ctx.loadApi.getResponse().curtimestamp;
if (!ctx.loadTime) {
ctx.statusElement.error('現在のタイムスタンプの取得に失敗しました。');
ctx.onLoadFailure(this);
return;
}
ctx.contentModel = page.contentmodel;
ctx.watched = page.watchlistexpiry || page.watched;
// extract protection info, to alert admins when they are about to edit a protected page
// Includes cascading protection
if (Morebits.userIsSysop) {
const editProt = page.protection.filter((pr) => pr.type === 'edit' && pr.level === 'sysop').pop();
if (editProt) {
ctx.fullyProtected = editProt.expiry;
} else {
ctx.fullyProtected = false;
}
}
ctx.revertCurID = page.lastrevid;
const testactions = page.actions;
ctx.testActions = []; // was null
Object.keys(testactions).forEach((action) => {
if (testactions[action]) {
ctx.testActions.push(action);
}
});
if (ctx.editMode === 'revert') {
ctx.revertCurID = rev && rev.revid;
if (!ctx.revertCurID) {
ctx.statusElement.error('Failed to retrieve current revision ID.');
ctx.onLoadFailure(this);
return;
}
ctx.revertUser = rev && rev.user;
if (!ctx.revertUser) {
if (rev && rev.userhidden) { // username was RevDel'd or oversighted
ctx.revertUser = '<username hidden>';
} else {
ctx.statusElement.error('Failed to retrieve user who made the revision.');
ctx.onLoadFailure(this);
return;
}
}
// set revert edit summary
ctx.editSummary = '[[Help:Revert|Reverted]] to revision ' + ctx.revertOldID + ' by ' + ctx.revertUser + ': ' + ctx.editSummary;
}
ctx.pageLoaded = true;
// alert("Generate edit conflict now"); // for testing edit conflict recovery logic
ctx.onLoadSuccess(this); // invoke callback
};
// callback from parseApi.post()
var fnParseSuccess = function() {
if (ctx.parseApi.getResponse().error) {
ctx.statusElement.error('ページの取得に失敗しました。');
ctx.onParseFailure(this);
return;
}
var response = ctx.parseApi.getResponse().parse;
ctx.pageParsed = true;
ctx.sections = response.sections;
// alert("Generate edit conflict now"); // for testing edit conflict recovery logic
ctx.onParseSuccess(this); // invoke callback
};
// helper function to parse the page name returned from the API
var fnCheckPageName = function(response, onFailure) {
if (!onFailure) {
onFailure = emptyFunction;
}
const page = response.pages && response.pages[0];
if (page) {
// check for invalid titles
if (page.invalid) {
ctx.statusElement.error('無効なページタイトル: ' + ctx.pageName);
onFailure(this);
return false; // abort
}
// retrieve actual title of the page after normalization and redirects
const resolvedName = page.title;
if (response.redirects) {
// check for cross-namespace redirect:
const origNs = new mw.Title(ctx.pageName).namespace;
const newNs = new mw.Title(resolvedName).namespace;
if (origNs !== newNs && !ctx.followCrossNsRedirect) {
ctx.statusElement.error(ctx.pageName + 'は' + resolvedName + 'への名前空間が違うリダイレクトです。中止します。');
onFailure(this);
return false;
}
// only notify user for redirects, not normalization
new Morebits.Status('Note', ctx.pageName + 'から' + resolvedName + 'へのリダイレクト');
}
ctx.pageName = resolvedName; // update to redirect target or normalized name
} else {
// could be a circular redirect or other problem
ctx.statusElement.error('Could not resolve redirects for: ' + ctx.pageName);
onFailure(this);
// force error to stay on the screen
++Morebits.wiki.numberOfActionsLeft;
return false; // abort
}
return true; // all OK
};
/**
* Determine whether we should provide a watchlist expiry. Will not
* do so if the page is currently permanently watched, or the current
* expiry is *after* the new, provided expiry. Only handles strings
* recognized by {@link Morebits.Date} or relative timeframes with
* unit it can process. Relies on the fact that fnCanUseMwUserToken
* requires page loading if a watchlistexpiry is provided, so we are
* ensured of knowing the watch status by the use of this.
*
* @return {boolean}
*/
var fnApplyWatchlistExpiry = function() {
if (ctx.watchlistExpiry) {
if (!ctx.watched || Morebits.string.isInfinity(ctx.watchlistExpiry)) {
return true;
} else if (typeof ctx.watched === 'string') {
let newExpiry;
// Attempt to determine if the new expiry is a
// relative (e.g. `1 month`) or absolute datetime
const rel = ctx.watchlistExpiry.split(' ');
try {
newExpiry = new Morebits.Date().add(rel[0], rel[1]);
} catch (e) {
newExpiry = new Morebits.Date(ctx.watchlistExpiry);
}
// If the date is valid, only use it if it extends the current expiry
if (newExpiry.isValid()) {
if (newExpiry.isAfter(new Morebits.Date(ctx.watched))) {
return true;
}
} else {
// If it's still not valid, hope it's a valid MW expiry format that
// Morebits.Date doesn't recognize, so just default to using it.
// This will also include minor typos.
return true;
}
}
}
return false;
};
// callback from saveApi.post()
var fnSaveSuccess = function() {
ctx.editMode = 'all'; // cancel append/prepend/newSection/revert modes
const response = ctx.saveApi.getResponse();
// see if the API thinks we were successful
if (response.edit.result === 'Success') {
// real success
// default on success action - display link for edited page
const link = document.createElement('a');
link.setAttribute('href', mw.util.getUrl(ctx.pageName));
link.appendChild(document.createTextNode(ctx.pageName));
ctx.statusElement.info(['完了 (', link, ')']);
if (ctx.onSaveSuccess) {
ctx.onSaveSuccess(this); // invoke callback
}
return;
}
// errors here are only generated by extensions which hook APIEditBeforeSave within MediaWiki,
// which as of 1.34.0-wmf.23 (Sept 2019) should only encompass captcha messages
if (response.edit.captcha) {
ctx.statusElement.error('ウィキのサーバーがキャプチャを入力するよう要求したため、ページを保存できませんでした。');
} else {
ctx.statusElement.error('ページの保存中にAPIから不明なエラーを受信');
}
// force error to stay on the screen
++Morebits.wiki.numberOfActionsLeft;
ctx.onSaveFailure(this);
};
// callback from saveApi.post()
var fnSaveError = function() {
const errorCode = ctx.saveApi.getErrorCode();
// check for edit conflict
if (errorCode === 'editconflict' && ctx.conflictRetries++ < ctx.maxConflictRetries) {
// edit conflicts can occur when the page needs to be purged from the server cache
const purgeQuery = {
action: 'purge',
titles: ctx.pageName // redirects are already resolved
};
var purgeApi = new Morebits.wiki.Api('編集の競合が検出されたため、サーバーのキャッシュを破棄します。', purgeQuery, (() => {
--Morebits.wiki.numberOfActionsLeft; // allow for normal completion if retry succeeds
ctx.statusElement.info('編集の競合が検出されたため、編集をやり直します');
if (fnCanUseMwUserToken('edit')) {
ctx.saveApi.post(); // necessarily append, prepend, or newSection, so this should work as desired
} else {
ctx.loadApi.post(); // reload the page and reapply the edit
}
}), ctx.statusElement);
purgeApi.post();
// check for network or server error
} else if ((errorCode === null || errorCode === undefined) && ctx.retries++ < ctx.maxRetries) {
// the error might be transient, so try again
ctx.statusElement.info('保存に失敗しました。2秒後に再試行します ...');
--Morebits.wiki.numberOfActionsLeft; // allow for normal completion if retry succeeds
// wait for sometime for client to regain connectivity
sleep(2000).then(() => {
ctx.saveApi.post(); // give it another go!
});
// hard error, give up
} else {
const response = ctx.saveApi.getResponse();
const errorData = response.error || // bc error format
response.errors[0].data; // html/wikitext/plaintext error format
switch (errorCode) {
case 'protectedpage':
// non-admin attempting to edit a protected page - this gives a friendlier message than the default
ctx.statusElement.error('Failed to save edit: Page is protected');
break;
case 'abusefilter-disallowed':
ctx.statusElement.error('The edit was disallowed by the edit filter: "' + errorData.abusefilter.description + '".');
break;
case 'abusefilter-warning':
ctx.statusElement.error(['A warning was returned by the edit filter: "', errorData.abusefilter.description, '". If you wish to proceed with the edit, please carry it out again. This warning will not appear a second time.']);
// We should provide the user with a way to automatically retry the action if they so choose -
// I can't see how to do this without creating a UI dependency on Morebits.wiki.Page though -- TTO
break;
case 'spamblacklist':
// If multiple items are blacklisted, we only return the first
var spam = errorData.spamblacklist.matches[0];
ctx.statusElement.error('Could not save the page because the URL ' + spam + ' is on the spam blacklist');
break;
default:
ctx.statusElement.error('Failed to save edit: ' + ctx.saveApi.getErrorText());
}
ctx.editMode = 'all'; // cancel append/prepend/newSection/revert modes
if (ctx.onSaveFailure) {
ctx.onSaveFailure(this); // invoke callback
}
}
};
const isTextRedirect = function(text) {
if (!text) { // no text - content empty or inaccessible (revdelled or suppressed)
return false;
}
return Morebits.l10n.redirectTagAliases.some((tag) => new RegExp('^\\s*' + tag + '\\W', 'i').test(text));
};
var fnLookupCreationSuccess = function() {
const response = ctx.lookupCreationApi.getResponse().query;
if (!fnCheckPageName(response, ctx.onLookupCreationFailure)) {
return; // abort
}
const rev = response.pages[0].revisions && response.pages[0].revisions[0];
if (!rev) {
ctx.statusElement.error('Could not find any revisions of ' + ctx.pageName);
ctx.onLookupCreationFailure(this);
return;
}
if (!ctx.lookupNonRedirectCreator || !isTextRedirect(rev.content)) {
ctx.creator = rev.user;
if (!ctx.creator) {
ctx.statusElement.error('Could not find name of page creator');
ctx.onLookupCreationFailure(this);
return;
}
ctx.timestamp = rev.timestamp;
if (!ctx.timestamp) {
ctx.statusElement.error('Could not find timestamp of page creation');
ctx.onLookupCreationFailure(this);
return;
}
ctx.statusElement.info('retrieved page creation information');
ctx.onLookupCreationSuccess(this);
} else {
ctx.lookupCreationApi.query.rvlimit = 50; // modify previous query to fetch more revisions
ctx.lookupCreationApi.query.titles = ctx.pageName; // update pageName if redirect resolution took place in earlier query
ctx.lookupCreationApi = new Morebits.wiki.Api('ページ作成情報の取得中', ctx.lookupCreationApi.query, fnLookupNonRedirectCreator, ctx.statusElement, ctx.onLookupCreationFailure);
ctx.lookupCreationApi.setParent(this);
ctx.lookupCreationApi.post();
}
};
var fnLookupNonRedirectCreator = function() {
const response = ctx.lookupCreationApi.getResponse().query;
const revs = response.pages[0].revisions;
for (let i = 0; i < revs.length; i++) {
if (!isTextRedirect(revs[i].content)) {
ctx.creator = revs[i].user;
ctx.timestamp = revs[i].timestamp;
break;
}
}
if (!ctx.creator) {
// fallback to give first revision author if no non-redirect version in the first 50
ctx.creator = revs[0].user;
ctx.timestamp = revs[0].timestamp;
if (!ctx.creator) {
ctx.statusElement.error('Could not find name of page creator');
ctx.onLookupCreationFailure(this);
return;
}
}
if (!ctx.timestamp) {
ctx.statusElement.error('Could not find timestamp of page creation');
ctx.onLookupCreationFailure(this);
return;
}
ctx.statusElement.info('retrieved page creation information');
ctx.onLookupCreationSuccess(this);
};
/**
* Common checks for action methods. Used for move, undelete, delete,
* protect, stabilize.
*
* @param {string} action - The action being checked.
* @param {string} onFailure - Failure callback.
* @return {boolean}
*/
var fnPreflightChecks = function(action, onFailure) {
// if a non-admin tries to do this, don't bother
if (!Morebits.userIsSysop && action !== 'move') {
ctx.statusElement.error('Cannot ' + action + 'page : only admins can do that');
onFailure(this);
return false;
}
if (!ctx.editSummary) {
ctx.statusElement.error('Internal error: ' + action + ' reason not set (use setEditSummary function)!');
onFailure(this);
return false;
}
return true; // all OK
};
/**
* Common checks for fnProcess functions (`fnProcessDelete`, `fnProcessMove`, etc.
* Used for move, undelete, delete, protect, stabilize.
*
* @param {string} action - The action being checked.
* @param {string} onFailure - Failure callback.
* @param {string} response - The response document from the API call.
* @return {boolean}
*/
const fnProcessChecks = function(action, onFailure, response) {
const missing = response.pages[0].missing;
// No undelete as an existing page could have deleted revisions
const actionMissing = missing && ['delete', 'stabilize', 'move'].includes(action);
const protectMissing = action === 'protect' && missing && (ctx.protectEdit || ctx.protectMove);
const saltMissing = action === 'protect' && !missing && ctx.protectCreate;
if (actionMissing || protectMissing || saltMissing) {
ctx.statusElement.error('Cannot ' + action + ' the page because it ' + (missing ? 'no longer' : 'already') + ' exists');
onFailure(this);
return false;
}
// Delete, undelete, move
// extract protection info
let editprot;
if (action === 'undelete') {
editprot = response.pages[0].protection.filter((pr) => pr.type === 'create' && pr.level === 'sysop').pop();
} else if (action === 'delete' || action === 'move') {
editprot = response.pages[0].protection.filter((pr) => pr.type === 'edit' && pr.level === 'sysop').pop();
}
if (editprot && !ctx.suppressProtectWarning &&
!confirm('You are about to ' + action + ' the fully protected page "' + ctx.pageName +
(editprot.expiry === 'infinity' ? '" (protected indefinitely)' : '" (protection expiring ' + new Morebits.Date(editprot.expiry).calendar('utc') + ' (UTC))') +
'. \n\nClick OK to proceed with ' + action + ', or Cancel to skip.')) {
ctx.statusElement.error('Aborted ' + action + ' on fully protected page.');
onFailure(this);
return false;
}
if (!response.tokens.csrftoken) {
ctx.statusElement.error('Failed to retrieve token.');
onFailure(this);
return false;
}
return true; // all OK
};
var fnProcessMove = function() {
let pageTitle, token;
if (fnCanUseMwUserToken('move')) {
token = mw.user.tokens.get('csrfToken');
pageTitle = ctx.pageName;
} else {
const response = ctx.moveApi.getResponse().query;
if (!fnProcessChecks('move', ctx.onMoveFailure, response)) {
return; // abort
}
token = response.tokens.csrftoken;
const page = response.pages[0];
pageTitle = page.title;
ctx.watched = page.watchlistexpiry || page.watched;
}
const query = {
action: 'move',
from: pageTitle,
to: ctx.moveDestination,
token: token,
reason: ctx.editSummary,
watchlist: ctx.watchlistOption,
format: 'json'
};
if (ctx.changeTags) {
query.tags = ctx.changeTags;
}
if (fnApplyWatchlistExpiry()) {
query.watchlistexpiry = ctx.watchlistExpiry;
}
if (ctx.moveTalkPage) {
query.movetalk = 'true';
}
if (ctx.moveSubpages) {
query.movesubpages = 'true';
}
if (ctx.moveSuppressRedirect) {
query.noredirect = 'true';
}
ctx.moveProcessApi = new Morebits.wiki.Api('ページの移動...', query, ctx.onMoveSuccess, ctx.statusElement, ctx.onMoveFailure);
ctx.moveProcessApi.setParent(this);
ctx.moveProcessApi.post();
};
var fnProcessPatrol = function() {
const query = {
action: 'patrol',
format: 'json'
};
// Didn't need to load the page
if (ctx.rcid) {
query.rcid = ctx.rcid;
query.token = mw.user.tokens.get('patrolToken');
} else {
const response = ctx.patrolApi.getResponse().query;
// Don't patrol if not unpatrolled
if (!response.recentchanges[0].unpatrolled) {
return;
}
const lastrevid = response.pages[0].lastrevid;
if (!lastrevid) {
return;
}
query.revid = lastrevid;
const token = response.tokens.csrftoken;
if (!token) {
return;
}
query.token = token;
}
if (ctx.changeTags) {
query.tags = ctx.changeTags;
}
const patrolStat = new Morebits.Status('Marking page as patrolled');
ctx.patrolProcessApi = new Morebits.wiki.Api('patrolling page...', query, null, patrolStat);
ctx.patrolProcessApi.setParent(this);
ctx.patrolProcessApi.post();
};
// Ensure that the page is curatable
var fnProcessTriageList = function() {
if (ctx.pageID) {
ctx.csrfToken = mw.user.tokens.get('csrfToken');
} else {
const response = ctx.triageApi.getResponse().query;
ctx.pageID = response.pages[0].pageid;
if (!ctx.pageID) {
return;
}
ctx.csrfToken = response.tokens.csrftoken;
if (!ctx.csrfToken) {
return;
}
}
const query = {
action: 'pagetriagelist',
page_id: ctx.pageID,
format: 'json'
};
ctx.triageProcessListApi = new Morebits.wiki.Api('checking curation status...', query, fnProcessTriage);
ctx.triageProcessListApi.setParent(this);
ctx.triageProcessListApi.post();
};
// callback from triageProcessListApi.post()
var fnProcessTriage = function() {
const responseList = ctx.triageProcessListApi.getResponse().pagetriagelist;
// Exit if not in the queue
if (!responseList || responseList.result !== 'success') {
return;
}
const page = responseList.pages && responseList.pages[0];
// Do nothing if page already triaged/patrolled
if (!page || !parseInt(page.patrol_status, 10)) {
const query = {
action: 'pagetriageaction',
pageid: ctx.pageID,
reviewed: 1,
token: ctx.csrfToken,
format: 'json'
};
if (ctx.changeTags) {
query.tags = ctx.changeTags;
}
const triageStat = new Morebits.Status('Marking page as curated');
ctx.triageProcessApi = new Morebits.wiki.Api('curating page...', query, null, triageStat);
ctx.triageProcessApi.setParent(this);
ctx.triageProcessApi.post();
}
};
var fnProcessDelete = function() {
let pageTitle, token;
if (fnCanUseMwUserToken('delete')) {
token = mw.user.tokens.get('csrfToken');
pageTitle = ctx.pageName;
} else {
const response = ctx.deleteApi.getResponse().query;
if (!fnProcessChecks('delete', ctx.onDeleteFailure, response)) {
return; // abort
}
token = response.tokens.csrftoken;
const page = response.pages[0];
pageTitle = page.title;
ctx.watched = page.watchlistexpiry || page.watched;
}
const query = {
action: 'delete',
title: pageTitle,
token: token,
reason: ctx.editSummary,
watchlist: ctx.watchlistOption,
format: 'json'
};
if (ctx.changeTags) {
query.tags = ctx.changeTags;
}
if (ctx.deleteTalkPage) {
query.deletetalk = 'true';
}
if (fnApplyWatchlistExpiry()) {
query.watchlistexpiry = ctx.watchlistExpiry;
}
ctx.deleteProcessApi = new Morebits.wiki.Api('deleting page...', query, ctx.onDeleteSuccess, ctx.statusElement, fnProcessDeleteError);
ctx.deleteProcessApi.setParent(this);
ctx.deleteProcessApi.post();
};
// callback from deleteProcessApi.post()
var fnProcessDeleteError = function() {
const errorCode = ctx.deleteProcessApi.getErrorCode();
// check for "Database query error"
if (errorCode === 'internal_api_error_DBQueryError' && ctx.retries++ < ctx.maxRetries) {
ctx.statusElement.info('Database query error, retrying');
--Morebits.wiki.numberOfActionsLeft; // allow for normal completion if retry succeeds
ctx.deleteProcessApi.post(); // give it another go!
} else if (errorCode === 'missingtitle') {
ctx.statusElement.error('Cannot delete the page, because it no longer exists');
if (ctx.onDeleteFailure) {
ctx.onDeleteFailure.call(this, ctx.deleteProcessApi); // invoke callback
}
// hard error, give up
} else {
ctx.statusElement.error('Failed to delete the page: ' + ctx.deleteProcessApi.getErrorText());
if (ctx.onDeleteFailure) {
ctx.onDeleteFailure.call(this, ctx.deleteProcessApi); // invoke callback
}
}
};
var fnProcessUndelete = function() {
let pageTitle, token;
if (fnCanUseMwUserToken('undelete')) {
token = mw.user.tokens.get('csrfToken');
pageTitle = ctx.pageName;
} else {
const response = ctx.undeleteApi.getResponse().query;
if (!fnProcessChecks('undelete', ctx.onUndeleteFailure, response)) {
return; // abort
}
token = response.tokens.csrftoken;
const page = response.pages[0];
pageTitle = page.title;
ctx.watched = page.watchlistexpiry || page.watched;
}
const query = {
action: 'undelete',
title: pageTitle,
token: token,
reason: ctx.editSummary,
watchlist: ctx.watchlistOption,
format: 'json'
};
if (ctx.changeTags) {
query.tags = ctx.changeTags;
}
if (ctx.undeleteTalkPage) {
query.undeletetalk = 'true';
}
if (fnApplyWatchlistExpiry()) {
query.watchlistexpiry = ctx.watchlistExpiry;
}
ctx.undeleteProcessApi = new Morebits.wiki.Api('undeleting page...', query, ctx.onUndeleteSuccess, ctx.statusElement, fnProcessUndeleteError);
ctx.undeleteProcessApi.setParent(this);
ctx.undeleteProcessApi.post();
};
// callback from undeleteProcessApi.post()
var fnProcessUndeleteError = function() {
const errorCode = ctx.undeleteProcessApi.getErrorCode();
// check for "Database query error"
if (errorCode === 'internal_api_error_DBQueryError') {
if (ctx.retries++ < ctx.maxRetries) {
ctx.statusElement.info('Database query error, retrying');
--Morebits.wiki.numberOfActionsLeft; // allow for normal completion if retry succeeds
ctx.undeleteProcessApi.post(); // give it another go!
} else {
ctx.statusElement.error('Repeated database query error, please try again');
if (ctx.onUndeleteFailure) {
ctx.onUndeleteFailure.call(this, ctx.undeleteProcessApi); // invoke callback
}
}
} else if (errorCode === 'cantundelete') {
ctx.statusElement.error('Cannot undelete the page, either because there are no revisions to undelete or because it has already been undeleted');
if (ctx.onUndeleteFailure) {
ctx.onUndeleteFailure.call(this, ctx.undeleteProcessApi); // invoke callback
}
// hard error, give up
} else {
ctx.statusElement.error('Failed to undelete the page: ' + ctx.undeleteProcessApi.getErrorText());
if (ctx.onUndeleteFailure) {
ctx.onUndeleteFailure.call(this, ctx.undeleteProcessApi); // invoke callback
}
}
};
var fnProcessProtect = function() {
const response = ctx.protectApi.getResponse().query;
if (!fnProcessChecks('protect', ctx.onProtectFailure, response)) {
return; // abort
}
const token = response.tokens.csrftoken;
const page = response.pages[0];
const pageTitle = page.title;
ctx.watched = page.watchlistexpiry || page.watched;
// Fetch existing protection levels
const prs = response.pages[0].protection;
let editprot, moveprot, createprot;
prs.forEach((pr) => {
// Filter out protection from cascading
if (pr.type === 'edit' && !pr.source) {
editprot = pr;
} else if (pr.type === 'move') {
moveprot = pr;
} else if (pr.type === 'create') {
createprot = pr;
}
});
// Fall back to current levels if not explicitly set
if (!ctx.protectEdit && editprot) {
ctx.protectEdit = { level: editprot.level, expiry: editprot.expiry };
}
if (!ctx.protectMove && moveprot) {
ctx.protectMove = { level: moveprot.level, expiry: moveprot.expiry };
}
if (!ctx.protectCreate && createprot) {
ctx.protectCreate = { level: createprot.level, expiry: createprot.expiry };
}
// Default to pre-existing cascading protection if unchanged (similar to above)
if (ctx.protectCascade === null) {
ctx.protectCascade = !!prs.filter((pr) => pr.cascade).length;
}
// Warn if cascading protection being applied with an invalid protection level,
// which for edit protection will cause cascading to be silently stripped
if (ctx.protectCascade) {
// On move protection, this is technically stricter than the MW API,
// but seems reasonable to avoid dumb values and misleading log entries (T265626)
if (((!ctx.protectEdit || ctx.protectEdit.level !== 'sysop') ||
(!ctx.protectMove || ctx.protectMove.level !== 'sysop')) &&
!confirm('"' + ctx.pageName + '"ではカスケード保護が有効になっていますが、管理者レベルの保護を選択していません。\n\n' +
'OKをクリックすることで調節して管理者レベルのカスケード保護を続行するか、キャンセルをクリックしてこのアクションをスキップします。')) {
ctx.statusElement.error('カスケード保護は中止されました。');
ctx.onProtectFailure(this);
return;
}
ctx.protectEdit.level = 'sysop';
ctx.protectMove.level = 'sysop';
}
// Build protection levels and expirys (expiries?) for query
const protections = [], expirys = [];
if (ctx.protectEdit) {
protections.push('edit=' + ctx.protectEdit.level);
expirys.push(ctx.protectEdit.expiry);
}
if (ctx.protectMove) {
protections.push('move=' + ctx.protectMove.level);
expirys.push(ctx.protectMove.expiry);
}
if (ctx.protectCreate) {
protections.push('create=' + ctx.protectCreate.level);
expirys.push(ctx.protectCreate.expiry);
}
const query = {
action: 'protect',
title: pageTitle,
token: token,
protections: protections.join('|'),
expiry: expirys.join('|'),
reason: ctx.editSummary,
watchlist: ctx.watchlistOption,
format: 'json'
};
// Only shows up in logs, not page history [[phab:T259983]]
if (ctx.changeTags) {
query.tags = ctx.changeTags;
}
if (fnApplyWatchlistExpiry()) {
query.watchlistexpiry = ctx.watchlistExpiry;
}
if (ctx.protectCascade) {
query.cascade = 'true';
}
ctx.protectProcessApi = new Morebits.wiki.Api('protecting page...', query, ctx.onProtectSuccess, ctx.statusElement, ctx.onProtectFailure);
ctx.protectProcessApi.setParent(this);
ctx.protectProcessApi.post();
};
var fnProcessStabilize = function() {
let pageTitle, token;
if (fnCanUseMwUserToken('stabilize')) {
token = mw.user.tokens.get('csrfToken');
pageTitle = ctx.pageName;
} else {
const response = ctx.stabilizeApi.getResponse().query;
// 'stabilize' as a verb not necessarily well understood
if (!fnProcessChecks('stabilize', ctx.onStabilizeFailure, response)) {
return; // abort
}
token = response.tokens.csrftoken;
const page = response.pages[0];
pageTitle = page.title;
// Doesn't support watchlist expiry [[phab:T263336]]
// ctx.watched = page.watchlistexpiry || page.watched;
}
const query = {
action: 'stabilize',
title: pageTitle,
token: token,
protectlevel: ctx.flaggedRevs.level,
expiry: ctx.flaggedRevs.expiry,
// tags: ctx.changeTags, // flaggedrevs tag support: [[phab:T247721]]
reason: ctx.editSummary,
watchlist: ctx.watchlistOption,
format: 'json'
};
/* Doesn't support watchlist expiry [[phab:T263336]]
if (fnApplyWatchlistExpiry()) {
query.watchlistexpiry = ctx.watchlistExpiry;
}
*/
ctx.stabilizeProcessApi = new Morebits.wiki.Api('configuring stabilization settings...', query, ctx.onStabilizeSuccess, ctx.statusElement, ctx.onStabilizeFailure);
ctx.stabilizeProcessApi.setParent(this);
ctx.stabilizeProcessApi.post();
};
var sleep = function(milliseconds) {
const deferred = $.Deferred();
setTimeout(deferred.resolve, milliseconds);
return deferred;
};
}; // end Morebits.wiki.Page
/* Morebits.wiki.Page TODO: (XXX)
* - Should we retry loads also?
* - Need to reset current action before the save?
* - Deal with action.completed stuff
* - Need to reset all parameters once done (e.g. edit summary, move destination, etc.)
*/
/* **************** Morebits.wiki.Preview **************** */
/**
* Use the API to parse a fragment of wikitext and render it as HTML.
*
* The suggested implementation pattern (in {@link Morebits.SimpleWindow} and
* {@link Morebits.QuickForm} situations) is to construct a
* `Morebits.wiki.Preview` object after rendering a `Morebits.QuickForm`, and
* bind the object to an arbitrary property of the form (e.g. |previewer|).
* For an example, see twinklewarn.js.
*
* @memberof Morebits.wiki
* @class
* @param {HTMLElement} previewbox - The element that will contain the rendered HTML,
* usually a <div> element.
*/
Morebits.wiki.Preview = function(previewbox) {
this.previewbox = previewbox;
$(previewbox).addClass('morebits-previewbox').hide();
/**
* Displays the preview box, and begins an asynchronous attempt
* to render the specified wikitext.
*
* @param {string} wikitext - Wikitext to render; most things should work, including `subst:` and `~~~~`.
* @param {string} [pageTitle] - Optional parameter for the page this should be rendered as being on, if omitted it is taken as the current page.
* @param {string} [sectionTitle] - If provided, render the text as a new section using this as the title.
* @return {jQuery.promise}
*/
this.beginRender = function(wikitext, pageTitle, sectionTitle) {
$(previewbox).show();
const statusspan = document.createElement('span');
previewbox.appendChild(statusspan);
Morebits.Status.init(statusspan);
const query = {
action: 'parse',
prop: ['text', 'modules'],
pst: true, // PST = pre-save transform; this makes substitution work properly
preview: true,
text: wikitext,
title: pageTitle || mw.config.get('wgPageName'),
disablelimitreport: true,
disableeditsection: true,
format: 'json'
};
if (sectionTitle) {
query.section = 'new';
query.sectiontitle = sectionTitle;
}
const renderApi = new Morebits.wiki.Api('loading...', query, fnRenderSuccess, new Morebits.Status('Preview'));
return renderApi.post();
};
var fnRenderSuccess = function(apiobj) {
const response = apiobj.getResponse();
const html = response.parse.text;
if (!html) {
apiobj.statelem.error('failed to retrieve preview, or template was blanked');
return;
}
previewbox.innerHTML = html;
mw.loader.load(response.parse.modulestyles);
mw.loader.load(response.parse.modules);
// this makes links open in new tab
$(previewbox).find('a').attr('target', '_blank');
// Integrate with scripts that do things on rendered content, like navpopups
mw.hook('wikipage.content').fire($(previewbox));
};
/** Hides the preview box and clears it. */
this.closePreview = function() {
$(previewbox).empty().hide();
};
};
/* **************** Morebits.wikitext **************** */
/**
* Wikitext manipulation.
*
* @namespace Morebits.wikitext
* @memberof Morebits
*/
Morebits.wikitext = {};
/**
* Get the value of every parameter found in the wikitext of a given template.
*
* @memberof Morebits.wikitext
* @param {string} text - Wikitext containing a template.
* @param {number} [start=0] - Index noting where in the text the template begins.
* @return {Object} `{name: templateName, parameters: {key: value}}`.
*/
Morebits.wikitext.parseTemplate = function(text, start) {
start = start || 0;
const level = []; // Track of how deep we are ({{, {{{, or [[)
let count = -1; // Number of parameters found
let unnamed = 0; // Keep track of what number an unnamed parameter should receive
let equals = -1; // After finding "=" before a parameter, the index; otherwise, -1
let current = '';
const result = {
name: '',
parameters: {}
};
let key, value;
/**
* Function to handle finding parameter values.
*
* @param {boolean} [final=false] - Whether this is the final
* parameter and we need to remove the trailing `}}`.
*/
function findParam(final) {
// Nothing found yet, this must be the template name
if (count === -1) {
result.name = current.slice(2).trim();
++count;
} else {
// In a parameter
if (equals !== -1) {
// We found an equals, so save the parameter as key: value
key = current.substring(0, equals).trim();
value = final ? current.substring(equals + 1, current.length - 2).trim() : current.substring(equals + 1).trim();
result.parameters[key] = value;
equals = -1;
} else {
// No equals, so it must be unnamed; no trim since whitespace allowed
const param = final ? current.substring(equals + 1, current.length - 2) : current;
if (param) {
result.parameters[++unnamed] = param;
++count;
}
}
}
}
for (let i = start; i < text.length; ++i) {
const test3 = text.substr(i, 3);
if (test3 === '{{{' || (test3 === '}}}' && level[level.length - 1] === 3)) {
current += test3;
i += 2;
if (test3 === '{{{') {
level.push(3);
} else {
level.pop();
}
continue;
}
const test2 = text.substr(i, 2);
// Entering a template (or link)
if (test2 === '{{' || test2 === '[[') {
current += test2;
++i;
if (test2 === '{{') {
level.push(2);
} else {
level.push('wl');
}
continue;
}
// Either leaving a link or template/parser function
if ((test2 === '}}' && level[level.length - 1] === 2) ||
(test2 === ']]' && level[level.length - 1] === 'wl')) {
current += test2;
++i;
level.pop();
// Find the final parameter if this really is the end
if (test2 === '}}' && level.length === 0) {
findParam(true);
break;
}
continue;
}
if (text.charAt(i) === '|' && level.length === 1) {
// Another pipe found, toplevel, so parameter coming up!
findParam();
current = '';
} else if (equals === -1 && text.charAt(i) === '=' && level.length === 1) {
// Equals found, toplevel
equals = current.length;
current += text.charAt(i);
} else {
// Just advance the position
current += text.charAt(i);
}
}
return result;
};
/**
* Adjust and manipulate the wikitext of a page.
*
* @class
* @memberof Morebits.wikitext
* @param {string} text - Wikitext to be manipulated.
*/
Morebits.wikitext.Page = function mediawikiPage(text) {
this.text = text;
};
Morebits.wikitext.Page.prototype = {
text: '',
/**
* Removes links to `link_target` from the page text.
*
* @param {string} link_target
* @return {Morebits.wikitext.Page}
*/
removeLink: function(link_target) {
const mwTitle = mw.Title.newFromText(link_target);
const namespaceID = mwTitle.getNamespaceId();
const title = mwTitle.getMainText();
let link_regex_string = '';
if (namespaceID !== 0) {
link_regex_string = Morebits.namespaceRegex(namespaceID) + ':';
}
link_regex_string += Morebits.pageNameRegex(title);
// For most namespaces, unlink both [[User:Test]] and [[:User:Test]]
// For files and categories, only unlink [[:Category:Test]]. Do not unlink [[Category:Test]]
const isFileOrCategory = [6, 14].includes(namespaceID);
const colon = isFileOrCategory ? ':' : ':?';
const simple_link_regex = new RegExp('\\[\\[' + colon + '(' + link_regex_string + ')\\]\\]', 'g');
const piped_link_regex = new RegExp('\\[\\[' + colon + link_regex_string + '\\|(.+?)\\]\\]', 'g');
this.text = this.text.replace(simple_link_regex, '$1').replace(piped_link_regex, '$1');
return this;
},
/**
* Comments out images from page text; if used in a gallery, deletes the whole line.
* If used as a template argument (not necessarily with `File:` prefix), the template parameter is commented out.
*
* @param {string} image - Image name without `File:` prefix.
* @param {string} [reason] - Reason to be included in comment, alongside the commented-out image.
* @return {Morebits.wikitext.Page}
*/
commentOutImage: function(image, reason) {
const unbinder = new Morebits.Unbinder(this.text);
unbinder.unbind('<!--', '-->');
reason = reason ? reason + ': ' : '';
const image_re_string = Morebits.pageNameRegex(image);
// Check for normal image links, i.e. [[File:Foobar.png|...]]
// Will eat the whole link
const links_re = new RegExp('\\[\\[' + Morebits.namespaceRegex(6) + ':\\s*' + image_re_string + '\\s*[\\|(?:\\]\\])]');
const allLinks = Morebits.string.splitWeightedByKeys(unbinder.content, '[[', ']]');
for (let i = 0; i < allLinks.length; ++i) {
if (links_re.test(allLinks[i])) {
const replacement = '<!-- ' + reason + allLinks[i] + ' -->';
unbinder.content = unbinder.content.replace(allLinks[i], replacement);
// unbind the newly created comments
unbinder.unbind('<!--', '-->');
}
}
// Check for gallery images, i.e. instances that must start on a new line,
// eventually preceded with some space, and must include File: prefix
// Will eat the whole line.
const gallery_image_re = new RegExp('(^\\s*' + Morebits.namespaceRegex(6) + ':\\s*' + image_re_string + '\\s*(?:\\|.*?$|$))', 'mg');
unbinder.content = unbinder.content.replace(gallery_image_re, '<!-- ' + reason + '$1 -->');
// unbind the newly created comments
unbinder.unbind('<!--', '-->');
// Check free image usages, for example as template arguments, might have the File: prefix excluded, but must be preceded by an |
// Will only eat the image name and the preceding bar and an eventual named parameter
const free_image_re = new RegExp('(\\|\\s*(?:[\\w\\s]+\\=)?\\s*(?:' + Morebits.namespaceRegex(6) + ':\\s*)?' + image_re_string + ')', 'mg');
unbinder.content = unbinder.content.replace(free_image_re, '<!-- ' + reason + '$1 -->');
// Rebind the content now, we are done!
this.text = unbinder.rebind();
return this;
},
removeImage: function(image) {
var unbinder = new Morebits.Unbinder(this.text);
unbinder.unbind('<!--', '-->');
var image_re_string = Morebits.pageNameRegex(image);
// Check for normal image links, i.e. [[File:Foobar.png|...]]
// Will eat the whole link
var links_re = new RegExp('\\[\\[' + Morebits.namespaceRegex(6) + ':\\s*' + image_re_string + '\\s*[\\|(?:\\]\\])]');
var allLinks = Morebits.string.splitWeightedByKeys(unbinder.content, '[[', ']]');
for (var i = 0; i < allLinks.length; ++i) {
if (links_re.test(allLinks[i])) {
var replacingtext = Morebits.string.escapeRegExp(allLinks[i]);
var replacing = new RegExp('(\n|)' + replacingtext + '$');
unbinder.content = unbinder.content.replace(replacing, '').replace(allLinks[i], '');
}
}
// unbind the newly created comments
unbinder.unbind('<!--', '-->');
// Check for gallery images, i.e. instances that must start on a new line,
// eventually preceded with some space, and must include File: prefix
// Will eat the whole line.
var gallery_image_re = new RegExp('\\n(^\\s*' + Morebits.namespaceRegex(6) + ':\\s*' + image_re_string + '\\s*(?:\\|.*?$|$))', 'mg');
unbinder.content = unbinder.content.replace(gallery_image_re, '');
// unbind the newly created comments
unbinder.unbind('<!--', '-->');
// Check free image usages, for example as template arguments, might have the File: prefix excluded, but must be preceded by an |
// Will only eat the image name and the preceding bar and an eventual named parameter
var free_image_re = new RegExp('(\\|\\s*(?:[\\w\\s]+\\=)?\\s*(?:' + Morebits.namespaceRegex(6) + ':\\s*)?' + image_re_string + ')', 'mg');
unbinder.content = unbinder.content.replace(free_image_re, '');
// Rebind the content now, we are done!
this.text = unbinder.rebind();
return this;
},
/**
* Converts uses of [[File:`image`]] to [[File:`image`|`data`]].
*
* @param {string} image - Image name without File: prefix.
* @param {string} data - The display options.
* @return {Morebits.wikitext.Page}
*/
addToImageComment: function(image, data) {
const image_re_string = Morebits.pageNameRegex(image);
const links_re = new RegExp('\\[\\[' + Morebits.namespaceRegex(6) + ':\\s*' + image_re_string + '\\s*[\\|(?:\\]\\])]');
const allLinks = Morebits.string.splitWeightedByKeys(this.text, '[[', ']]');
for (let i = 0; i < allLinks.length; ++i) {
if (links_re.test(allLinks[i])) {
let replacement = allLinks[i];
// just put it at the end?
replacement = replacement.replace(/\]\]$/, '|' + data + ']]');
this.text = this.text.replace(allLinks[i], replacement);
}
}
const gallery_re = new RegExp('^(\\s*' + image_re_string + '.*?)\\|?(.*?)$', 'mg');
const newtext = '$1|$2 ' + data;
this.text = this.text.replace(gallery_re, newtext);
return this;
},
/**
* Remove all transclusions of a template from page text.
*
* @param {string} template - Page name whose transclusions are to be removed,
* include namespace prefix only if not in template namespace.
* @return {Morebits.wikitext.Page}
*/
removeTemplate: function(template) {
const template_re_string = Morebits.pageNameRegex(template);
const links_re = new RegExp('\\{\\{(?:' + Morebits.namespaceRegex(10) + ':)?\\s*' + template_re_string + '\\s*[\\|(?:\\}\\})]');
const allTemplates = Morebits.string.splitWeightedByKeys(this.text, '{{', '}}', ['{{{', '}}}']);
for (let i = 0; i < allTemplates.length; ++i) {
if (links_re.test(allTemplates[i])) {
this.text = this.text.replace(allTemplates[i], '');
}
}
return this;
},
/**
* Smartly insert a tag atop page text but after specified templates,
* such as hatnotes, short description, or deletion and protection templates.
* Notably, does *not* insert a newline after the tag.
*
* @param {string} tag - The tag to be inserted.
* @param {string|string[]} regex - Templates after which to insert tag,
* given as either as a (regex-valid) string or an array to be joined by pipes.
* @param {string} [flags=i] - Regex flags to apply. `''` to provide no flags;
* other falsey values will default to `i`.
* @param {string|string[]} [preRegex] - Optional regex string or array to match
* before any template matches (i.e. before `{{`), such as html comments.
* @return {Morebits.wikitext.Page}
*/
insertAfterTemplates: function(tag, regex, flags, preRegex) {
if (typeof tag === 'undefined') {
throw new Error('No tag provided');
}
// .length is only a property of strings and arrays so we
// shouldn't need to check type
if (typeof regex === 'undefined' || !regex.length) {
throw new Error('No regex provided');
} else if (Array.isArray(regex)) {
regex = regex.join('|');
}
if (typeof flags !== 'string') {
flags = 'i';
}
if (!preRegex || !preRegex.length) {
preRegex = '';
} else if (Array.isArray(preRegex)) {
preRegex = preRegex.join('|');
}
// Regex is extra complicated to allow for templates with
// parameters and to handle whitespace properly
this.text = this.text.replace(
new RegExp(
// leading whitespace
'^\\s*' +
// capture template(s)
'(?:((?:\\s*' +
// Pre-template regex, such as leading html comments
preRegex + '|' +
// begin template format
'\\{\\{\\s*(?:' +
// Template regex
regex +
// end main template name, optionally with a number
// Probably remove the (?:) though
')\\d*\\s*' +
// template parameters
'(\\|(?:\\{\\{[^{}]*\\}\\}|[^{}])*)?' +
// end template format
'\\}\\})+' +
// end capture
'(?:\\s*\\n)?)' +
// trailing whitespace
'\\s*)?',
flags), '$1' + tag
);
return this;
},
/**
* Get the manipulated wikitext.
*
* @return {string}
*/
getText: function() {
return this.text;
}
};
/* *********** Morebits.UserspaceLogger ************ */
/**
* Handles logging actions to a userspace log.
* Used in CSD, PROD, and XFD.
*
* @memberof Morebits
* @class
* @param {string} logPageName - Title of the subpage of the current user's log.
*/
Morebits.UserspaceLogger = function(logPageName) {
if (!logPageName) {
throw new Error('記録ページ名が指定されていません');
}
/**
* The text to prefix the log with upon creation, defaults to empty.
*
* @type {string}
*/
this.initialText = '';
/**
* The header level to use for months, defaults to 3 (`===`).
*
* @type {number}
*/
this.headerLevel = 3;
this.changeTags = '';
/**
* Log the entry.
*
* @param {string} logText - Doesn't include leading `#` or `*`.
* @param {string} summaryText - Edit summary.
* @return {jQuery.Promise}
*/
this.log = function(logText, summaryText) {
const def = $.Deferred();
if (!logText) {
return def.reject();
}
var page = new Morebits.wiki.Page('User:' + mw.config.get('wgUserName') + '/' + logPageName,
'ユーザースペースログに項目を追加する'); // make this '... to ' + logPageName ?
page.load((pageobj) => {
// add blurb if log page doesn't exist or is blank
let text = pageobj.getPageText() || this.initialText;
// create monthly header if it doesn't exist already
const date = new Morebits.Date(pageobj.getLoadTime());
if (!date.monthHeaderRegex().exec(text)) {
text += '\n\n' + date.monthHeader(this.headerLevel);
}
pageobj.setPageText(text + '\n' + logText);
pageobj.setEditSummary(summaryText);
pageobj.setChangeTags(this.changeTags);
pageobj.setCreateOption('recreate');
pageobj.save(def.resolve, def.reject);
});
return def;
};
};
/* **************** Morebits.Status **************** */
/**
* Create and show status messages of varying urgency.
* {@link Morebits.Status.init|Morebits.Status.init()} must be called before
* any status object is created, otherwise those statuses won't be visible.
*
* @memberof Morebits
* @class
* @param {string} text - Text before the the colon `:`.
* @param {string} stat - Text after the colon `:`.
* @param {string} [type=status] - Determine the font color of the status
* line, allowable values are: `status` (blue), `info` (green), `warn` (red),
* or `error` (bold red).
*/
Morebits.Status = function Status(text, stat, type) {
this.textRaw = text;
this.text = Morebits.createHtml(text);
this.type = type || 'status';
this.generate();
if (stat) {
this.update(stat, type);
}
};
/**
* Specify an area for status message elements to be added to.
*
* @memberof Morebits.Status
* @param {HTMLElement} root - Usually a div element.
* @throws {Error} If `root` is not an `HTMLElement`.
*/
Morebits.Status.init = function(root) {
if (!(root instanceof Element)) {
throw new Error('object not an instance of Element');
}
while (root.hasChildNodes()) {
root.removeChild(root.firstChild);
}
Morebits.Status.root = root;
Morebits.Status.errorEvent = null;
};
Morebits.Status.root = null;
/**
* @memberof Morebits.Status
* @param {Function} handler - Function to execute on error.
* @throws {Error} When `handler` is not a function.
*/
Morebits.Status.onError = function(handler) {
if (typeof handler === 'function') {
Morebits.Status.errorEvent = handler;
} else {
throw new Error('Morebits.Status.onError: handler is not a function');
}
};
Morebits.Status.prototype = {
stat: null,
statRaw: null,
text: null,
textRaw: null,
type: 'status',
target: null,
node: null,
linked: false,
/** Add the status element node to the DOM. */
link: function() {
if (!this.linked && Morebits.Status.root) {
Morebits.Status.root.appendChild(this.node);
this.linked = true;
}
},
/** Remove the status element node from the DOM. */
unlink: function() {
if (this.linked) {
Morebits.Status.root.removeChild(this.node);
this.linked = false;
}
},
/**
* Update the status.
*
* @param {string} status - Part of status message after colon.
* @param {string} type - 'status' (blue), 'info' (green), 'warn'
* (red), or 'error' (bold red).
*/
update: function(status, type) {
this.statRaw = status;
this.stat = Morebits.createHtml(status);
if (type) {
this.type = type;
if (type === 'error') {
// hack to force the page not to reload when an error is output - see also Morebits.Status() above
Morebits.wiki.numberOfActionsLeft = 1000;
// call error callback
if (Morebits.Status.errorEvent) {
Morebits.Status.errorEvent();
}
// also log error messages in the browser console
console.error(this.textRaw + ': ' + this.statRaw); // eslint-disable-line no-console
}
}
this.render();
},
/** Produce the html for first part of the status message. */
generate: function() {
this.node = document.createElement('div');
this.node.appendChild(document.createElement('span')).appendChild(this.text);
this.node.appendChild(document.createElement('span')).appendChild(document.createTextNode(': '));
this.target = this.node.appendChild(document.createElement('span'));
this.target.appendChild(document.createTextNode('')); // dummy node
},
/** Complete the html, for the second part of the status message. */
render: function() {
this.node.className = 'morebits_status_' + this.type;
while (this.target.hasChildNodes()) {
this.target.removeChild(this.target.firstChild);
}
this.target.appendChild(this.stat);
this.link();
},
status: function(status) {
this.update(status, 'status');
},
info: function(status) {
this.update(status, 'info');
},
warn: function(status) {
this.update(status, 'warn');
},
error: function(status) {
this.update(status, 'error');
}
};
/**
* @memberof Morebits.Status
* @param {string} text - Before colon
* @param {string} status - After colon
* @return {Morebits.Status} - `status`-type (blue)
*/
Morebits.Status.status = function(text, status) {
return new Morebits.Status(text, status);
};
/**
* @memberof Morebits.Status
* @param {string} text - Before colon
* @param {string} status - After colon
* @return {Morebits.Status} - `info`-type (green)
*/
Morebits.Status.info = function(text, status) {
return new Morebits.Status(text, status, 'info');
};
/**
* @memberof Morebits.Status
* @param {string} text - Before colon
* @param {string} status - After colon
* @return {Morebits.Status} - `warn`-type (red)
*/
Morebits.Status.warn = function(text, status) {
return new Morebits.Status(text, status, 'warn');
};
/**
* @memberof Morebits.Status
* @param {string} text - Before colon
* @param {string} status - After colon
* @return {Morebits.Status} - `error`-type (bold red)
*/
Morebits.Status.error = function(text, status) {
return new Morebits.Status(text, status, 'error');
};
/**
* For the action complete message at the end, create a status line without
* a colon separator.
*
* @memberof Morebits.Status
* @param {string} text
*/
Morebits.Status.actionCompleted = function(text) {
const node = document.createElement('div');
node.appendChild(document.createElement('b')).appendChild(document.createTextNode(text));
node.className = 'morebits_status_info morebits_action_complete';
if (Morebits.Status.root) {
Morebits.Status.root.appendChild(node);
}
};
/**
* Display the user's rationale, comments, etc. Back to them after a failure,
* so that they may re-use it.
*
* @memberof Morebits.Status
* @param {string} comments
* @param {string} message
*/
Morebits.Status.printUserText = function(comments, message) {
const p = document.createElement('p');
p.innerHTML = message;
const div = document.createElement('div');
div.className = 'morebits-usertext';
div.style.marginTop = '0';
div.style.whiteSpace = 'pre-wrap';
div.textContent = comments;
p.appendChild(div);
Morebits.Status.root.appendChild(p);
};
/**
* Simple helper function to create a simple node.
*
* @param {string} type - Type of HTML element.
* @param {string} content - Text content.
* @param {string} [color] - Font color.
* @return {HTMLElement}
*/
Morebits.htmlNode = function(type, content, color) {
const node = document.createElement(type);
if (color) {
node.style.color = color;
}
node.appendChild(document.createTextNode(content));
return node;
};
/**
* Add shift-click support for checkboxes. The wikibits version
* (`window.addCheckboxClickHandlers`) has some restrictions, and doesn't work
* with checkboxes inside a sortable table, so let's build our own.
*
* @param {jQuery} jQuerySelector
* @param {jQuery} jQueryContext
*/
Morebits.checkboxShiftClickSupport = function(jQuerySelector, jQueryContext) {
let lastCheckbox = null;
function clickHandler(event) {
const thisCb = this;
if (event.shiftKey && lastCheckbox !== null) {
const $cbs = $(jQuerySelector, jQueryContext); // can't cache them, obviously, if we want to support re-sorting
let index = -1, lastIndex = -1, i;
for (i = 0; i < $cbs.length; i++) {
if ($cbs[i] === thisCb) {
index = i;
if (lastIndex > -1) {
break;
}
}
if ($cbs[i] === lastCheckbox) {
lastIndex = i;
if (index > -1) {
break;
}
}
}
if (index > -1 && lastIndex > -1) {
// inspired by wikibits
const endState = thisCb.checked;
let start, finish;
if (index < lastIndex) {
start = index + 1;
finish = lastIndex;
} else {
start = lastIndex;
finish = index - 1;
}
for (i = start; i <= finish; i++) {
if ($cbs[i].checked !== endState) {
$cbs[i].click();
}
}
}
}
lastCheckbox = thisCb;
return true;
}
$(jQuerySelector, jQueryContext).on('click', clickHandler);
};
/* **************** Morebits.BatchOperation **************** */
/**
* Iterates over a group of pages (or arbitrary objects) and executes a worker function
* for each.
*
* `setPageList(pageList)`: Sets the list of pages to work on. It should be an
* array of page names strings.
*
* `setOption(optionName, optionValue)`: Sets a known option:
* - `chunkSize` (integer): The size of chunks to break the array into (default
* 50). Setting this to a small value (<5) can cause problems.
* - `preserveIndividualStatusLines` (boolean): Keep each page's status element
* visible when worker is complete? See note below.
*
* `run(worker, postFinish)`: Runs the callback `worker` for each page in the
* list. The callback must call `workerSuccess` when succeeding, or
* `workerFailure` when failing. If using {@link Morebits.wiki.Api} or
* {@link Morebits.wiki.Page}, this is easily done by passing these two
* functions as parameters to the methods on those objects: for instance,
* `page.save(batchOp.workerSuccess, batchOp.workerFailure)`. Make sure the
* methods are called directly if special success/failure cases arise. If you
* omit to call these methods, the batch operation will stall after the first
* chunk! Also ensure that either workerSuccess or workerFailure is called no
* more than once. The second callback `postFinish` is executed when the
* entire batch has been processed.
*
* If using `preserveIndividualStatusLines`, you should try to ensure that the
* `workerSuccess` callback has access to the page title. This is no problem for
* {@link Morebits.wiki.Page} objects. But when using the API, please set the
* |pageName| property on the {@link Morebits.wiki.Api} object.
*
* There are sample batchOperation implementations using Morebits.wiki.Page in
* twinklebatchdelete.js, twinklebatchundelete.js, and twinklebatchprotect.js.
*
* @memberof Morebits
* @class
* @param {string} [currentAction]
*/
Morebits.BatchOperation = function(currentAction) {
const ctx = {
// backing fields for public properties
pageList: null,
options: {
chunkSize: 50,
preserveIndividualStatusLines: false
},
// internal counters, etc.
statusElement: new Morebits.Status(currentAction || 'バッチ処理の実行'),
worker: null, // function that executes for each item in pageList
postFinish: null, // function that executes when the whole batch has been processed
countStarted: 0,
countFinished: 0,
countFinishedSuccess: 0,
currentChunkIndex: -1,
pageChunks: [],
running: false
};
// shouldn't be needed by external users, but provided anyway for maximum flexibility
this.getStatusElement = function() {
return ctx.statusElement;
};
/**
* Sets the list of pages to work on.
*
* @param {Array} pageList - Array of objects over which you wish to execute the worker function
* This is usually the list of page names (strings).
*/
this.setPageList = function(pageList) {
ctx.pageList = pageList;
};
/**
* Sets a known option.
*
* @param {string} optionName - Name of the option:
* - chunkSize (integer): The size of chunks to break the array into
* (default 50). Setting this to a small value (<5) can cause problems.
* - preserveIndividualStatusLines (boolean): Keep each page's status
* element visible when worker is complete?
* @param {number|boolean} optionValue - Value to which the option is
* to be set. Should be an integer for chunkSize and a boolean for
* preserveIndividualStatusLines.
*/
this.setOption = function(optionName, optionValue) {
ctx.options[optionName] = optionValue;
};
/**
* Runs the first callback for each page in the list.
* The callback must call workerSuccess when succeeding, or workerFailure when failing.
* Runs the optional second callback when the whole batch has been processed.
*
* @param {Function} worker
* @param {Function} [postFinish]
*/
this.run = function(worker, postFinish) {
if (ctx.running) {
ctx.statusElement.error('Batch operation is already running');
return;
}
ctx.running = true;
ctx.worker = worker;
ctx.postFinish = postFinish;
ctx.countStarted = 0;
ctx.countFinished = 0;
ctx.countFinishedSuccess = 0;
ctx.currentChunkIndex = -1;
ctx.pageChunks = [];
const total = ctx.pageList.length;
if (!total) {
ctx.statusElement.info('ページが指定されていません');
ctx.running = false;
if (ctx.postFinish) {
ctx.postFinish();
}
return;
}
// chunk page list into more manageable units
ctx.pageChunks = Morebits.array.chunk(ctx.pageList, ctx.options.chunkSize);
// start the process
Morebits.wiki.addCheckpoint();
ctx.statusElement.status('0%');
fnStartNewChunk();
};
/**
* To be called by worker before it terminates successfully.
*
* @param {(Morebits.wiki.Page|Morebits.wiki.Api|string)} arg -
* This should be the `Morebits.wiki.Page` or `Morebits.wiki.Api` object used by worker
* (for the adjustment of status lines emitted by them).
* If no Morebits.wiki.* object is used (e.g. you're using `mw.Api()` or something else), and
* `preserveIndividualStatusLines` option is on, give the page name (string) as argument.
*/
this.workerSuccess = function(arg) {
if (arg instanceof Morebits.wiki.Api || arg instanceof Morebits.wiki.Page) {
// update or remove status line
const statelem = arg.getStatusElement();
if (ctx.options.preserveIndividualStatusLines) {
if (arg.getPageName || arg.pageName || (arg.query && arg.query.title)) {
// we know the page title - display a relevant message
var pageName = arg.getPageName ? arg.getPageName() : arg.pageName || arg.query.title;
statelem.info('完了 ([[' + pageName + ']])');
} else {
// we don't know the page title - just display a generic message
statelem.info('完了');
}
} else {
// remove the status line automatically produced by Morebits.wiki.*
statelem.unlink();
}
} else if (typeof arg === 'string' && ctx.options.preserveIndividualStatusLines) {
new Morebits.Status(arg, '完了 ([[' + arg + ']])');
}
ctx.countFinishedSuccess++;
fnDoneOne();
};
this.workerFailure = function() {
fnDoneOne();
};
// private functions
const thisProxy = this;
var fnStartNewChunk = function() {
const chunk = ctx.pageChunks[++ctx.currentChunkIndex];
if (!chunk) {
return; // done! yay
}
// start workers for the current chunk
ctx.countStarted += chunk.length;
chunk.forEach((page) => {
ctx.worker(page, thisProxy);
});
};
var fnDoneOne = function() {
ctx.countFinished++;
// update overall status line
const total = ctx.pageList.length;
if (ctx.countFinished < total) {
const progress = Math.round(100 * ctx.countFinished / total);
ctx.statusElement.status(progress + '%');
// start a new chunk if we're close enough to the end of the previous chunk, and
// we haven't already started the next one
if (ctx.countFinished >= (ctx.countStarted - Math.max(ctx.options.chunkSize / 10, 2)) &&
Math.floor(ctx.countFinished / ctx.options.chunkSize) > ctx.currentChunkIndex) {
fnStartNewChunk();
}
} else if (ctx.countFinished === total) {
var statusString = '完了 (' + ctx.countFinishedSuccess +
'/' + ctx.countFinished + ' アクションが正常に完了)';
if (ctx.countFinishedSuccess < ctx.countFinished) {
ctx.statusElement.warn(statusString);
} else {
ctx.statusElement.info(statusString);
}
if (ctx.postFinish) {
ctx.postFinish();
}
Morebits.wiki.removeCheckpoint();
ctx.running = false;
} else {
// ctx.countFinished > total
// just for giggles! (well, serious debugging, actually)
ctx.statusElement.warn('完了 (' + (ctx.countFinished - total) + '件上回った)');
Morebits.wiki.removeCheckpoint();
ctx.running = false;
}
};
};
/**
* Given a set of asynchronous functions to run along with their dependencies,
* run them in an efficient sequence so that multiple functions
* that don't depend on each other are triggered simultaneously. Where
* dependencies exist, it ensures that the dependency functions finish running
* before the dependent function runs. The values resolved by the dependencies
* are made available to the dependant as arguments.
*
* @memberof Morebits
* @class
*/
Morebits.TaskManager = function(context) {
this.taskDependencyMap = new Map();
this.failureCallbackMap = new Map();
this.deferreds = new Map();
this.context = context || window;
/**
* Register a task along with its dependencies (tasks which should have finished
* execution before we can begin this one). Each task is a function that must return
* a promise. The function will get the values resolved by the dependency functions
* as arguments.
*
* @param {Function} func - A task.
* @param {Function[]} deps - Its dependencies.
* @param {Function} [onFailure] - a failure callback that's run if the task or any one
* of its dependencies fail.
*/
this.add = function(func, deps, onFailure) {
this.taskDependencyMap.set(func, deps);
this.failureCallbackMap.set(func, onFailure || (() => {}));
const deferred = $.Deferred();
this.deferreds.set(func, deferred);
};
/**
* Run all the tasks. Multiple tasks may be run at once.
*
* @return {jQuery.Promise} - Resolved if all tasks succeed, rejected otherwise.
*/
this.execute = function() {
const self = this; // proxy for `this` for use inside functions where `this` is something else
this.taskDependencyMap.forEach((deps, task) => {
const dependencyPromisesArray = deps.map((dep) => self.deferreds.get(dep));
$.when.apply(self.context, dependencyPromisesArray).then(function() {
const result = task.apply(self.context, arguments);
if (result === undefined) { // maybe the function threw, or it didn't return anything
mw.log.error('Morebits.TaskManager: task returned undefined');
self.deferreds.get(task).reject.apply(self.context, arguments);
self.failureCallbackMap.get(task).apply(self.context, []);
}
result.then(function() {
self.deferreds.get(task).resolve.apply(self.context, arguments);
}, function() { // task failed
self.deferreds.get(task).reject.apply(self.context, arguments);
self.failureCallbackMap.get(task).apply(self.context, arguments);
});
}, function() { // one or more of the dependencies failed
self.failureCallbackMap.get(task).apply(self.context, arguments);
});
});
return $.when.apply(null, [...this.deferreds.values()]); // resolved when everything is done!
};
};
/**
* A simple draggable window. No longer uses jQuery UI.
*
* @memberof Morebits
* @class
* @param {number} width
* @param {number} height - The maximum allowable height for the content area.
*/
Morebits.SimpleWindow = function SimpleWindow(width, height) {
const $dialog = $('<div>').addClass('morebits-dialog').attr('role', 'dialog').attr('tabindex', -1);
const $titleBar = $('<div>').addClass('morebits-dialog-titlebar').append(
$('<span>').addClass('morebits-dialog-title'),
$('<button>')
.addClass('morebits-dialog-close')
.text('\u00D7')
.on('click', () => this.close())
);
const $content = $('<div>')
.addClass('morebits-dialog-content')
.attr('id', 'morebits-dialog-content-' + Math.round(Math.random() * 1e15));
const $buttonPane = $('<div>').addClass('morebits-dialog-buttonpane').append(
$('<span>').addClass('morebits-dialog-buttons'),
$('<span>').addClass('morebits-dialog-footerlinks')
);
const $resizer = $('<div>').addClass('morebits-dialog-resizer');
$dialog.append($titleBar, $content, $buttonPane, $resizer)
.css('width', Math.min(parseInt(width || 800, 10), $(window).width()))
.css('height', 'auto')
.css('max-height', height + 20)
.css('top', Math.max(0, window.scrollY + $(window).height() / 2 - (height + 20) / 2)) // Centre it (assume max height)
.css('left', Math.max(0, window.scrollX + $(window).width() / 2 - $dialog.width() / 2)); // Centre it
$dialog.on('focus', () => this.focus()).on('keydown', (e) => {
if (e.key === 'Escape') {
this.close();
}
});
// Make dialog draggable and resizable
let isDragging = false, isResizing = false, initialX, initialY, initialWidth, initialHeight;
$titleBar.on('mousedown', (e) => {
isDragging = true;
initialX = e.clientX - $dialog.offset().left;
initialY = e.clientY - $dialog.offset().top;
});
$resizer.on('mousedown', (e) => {
isResizing = true;
$dialog.css('height', $dialog.height());
$dialog.css('max-height', 'none');
initialWidth = $dialog.width();
initialHeight = $dialog.height();
initialX = e.clientX;
initialY = e.clientY;
});
$(document).on('mousemove', (e) => {
if (isDragging) {
$dialog.css('left', Math.max(0, e.clientX - initialX))
.css('top', Math.max(0, e.clientY - initialY));
} else if (isResizing) {
$dialog.css('width', initialWidth + (e.clientX - initialX))
.css('height', initialHeight + (e.clientY - initialY));
}
}).on('mouseup', () => {
isDragging = false;
isResizing = false;
});
this.$dialog = $dialog;
this.content = $content[0];
this.height = height;
};
Morebits.SimpleWindow.prototype = {
buttons: [],
height: 600,
hasFooterLinks: false,
scriptName: null,
/**
* Bring a dialog to the top, when there are other overlapping dialogs open.
*
* @return {Morebits.SimpleWindow}
*/
focus: function() {
if (!this.$dialog.hasClass('morebits-dialog-modal')) {
$('.morebits-dialog:not(.morebits-dialog-modal)').get()
.filter((dialog) => dialog !== this.$dialog[0])
.concat(this.$dialog[0])
.forEach((dialog, idx) => {
dialog.style.zIndex = 501 + idx;
});
}
return this;
},
/**
* Closes the dialog. If this is set as an event handler, it will stop the event
* from doing anything more.
*
* @param {event} [event]
* @return {Morebits.SimpleWindow}
*/
close: function(event) {
if (event) {
event.preventDefault();
}
this.setModality(false);
this.$dialog.remove();
return this;
},
/**
* Shows the dialog. Calling display() on a dialog that has previously been closed
* might work, but it is not guaranteed.
*
* @return {Morebits.SimpleWindow}
*/
display: function() {
if (this.scriptName) {
const $titleBar = this.$dialog.find('.morebits-dialog-title');
$titleBar.find('.morebits-dialog-scriptname').remove();
$titleBar.prepend($('<span>')
.addClass('morebits-dialog-scriptname')
.text(this.scriptName + ' \u00B7 ') // U+00B7 MIDDLE DOT = ·
);
}
if (this.$dialog.find('.morebits-scrollbox').length) {
// quickform needs some extra CSS if it happens to contain a scrollbox.
// CSS :has() selector can be used in the future which is easier and more reliable.
this.$dialog.find('.quickform').addClass('has-scrollbox');
}
this.$dialog.appendTo(document.body);
// Put focus on the first form element in the dialog, or on dialog itself.
const $firstField = this.$dialog.find('input, select, textarea').first();
const $firstFieldOrDialog = $firstField.length ? $firstField : this.$dialog;
$firstFieldOrDialog.trigger('focus');
return this;
},
/**
* Sets the dialog title.
*
* @param {string} title
* @return {Morebits.SimpleWindow}
*/
setTitle: function(title) {
this.$dialog.find('.morebits-dialog-title').text(title);
return this;
},
/**
* Sets the script name, appearing as a prefix to the title to help users determine which
* user script is producing which dialog. For instance, Twinkle modules set this to "Twinkle".
*
* @param {string} name
* @return {Morebits.SimpleWindow}
*/
setScriptName: function(name) {
this.scriptName = name;
return this;
},
/**
* Sets the dialog width.
*
* @param {number} width
* @return {Morebits.SimpleWindow}
*/
setWidth: function(width) {
this.$dialog.css('width', width + 'px');
return this;
},
/**
* Sets the dialog's maximum height. The dialog will auto-size to fit its contents.
*
* @param {number} height
* @return {Morebits.SimpleWindow}
*/
setHeight: function(height) {
this.$dialog.css('height', 'auto');
this.$dialog.css('max-height', (height + 20) + 'px');
return this;
},
/**
* Sets the content of the dialog to the given element node, usually from rendering
* a {@link Morebits.QuickForm}.
* Re-enumerates the footer buttons, but leaves the footer links as they are.
* Be sure to call this at least once before the dialog is displayed...
*
* @param {HTMLElement} content
* @return {Morebits.SimpleWindow}
*/
setContent: function(content) {
this.purgeContent();
this.addContent(content);
return this;
},
/**
* Adds the given element node to the dialog content.
*
* @param {HTMLElement} content
* @return {Morebits.SimpleWindow}
*/
addContent: function(content) {
this.content.appendChild(content);
// look for submit buttons in the content, hide them, and add a proxy button to the button pane
$(this.content).find('input[type="submit"], button[type="submit"]').get().forEach((node) => {
node.style.display = 'none';
const button = document.createElement('button');
button.textContent = node.getAttribute('value') || node.textContent || '送信';
button.className = node.className || 'submitButtonProxy';
button.addEventListener('click', () => node.click(), false);
this.buttons.push(button);
});
// remove all buttons from the button pane and re-add them
this.$dialog.find('.morebits-dialog-buttons')
.empty()
.append(...this.buttons)
// Set data-empty attribute only if there are no buttons. Used by CSS.
.attr('data-empty', this.buttons.length ? null : 'data-empty');
return this;
},
/**
* Removes all contents from the dialog, barring any footer links.
*
* @return {Morebits.SimpleWindow}
*/
purgeContent: function() {
this.buttons = [];
// delete all buttons in the buttonpane
this.$dialog.find('.morebits-dialog-buttons').empty();
$(this.content).empty();
return this;
},
/**
* Adds a link in the bottom-right corner of the dialog.
* This can be used to provide help or policy links.
* For example, Twinkle's CSD module adds a link to the CSD policy page,
* as well as a link to Twinkle's documentation.
*
* @param {string} text - Display text.
* @param {string} wikiPage - Link target.
* @return {Morebits.SimpleWindow}
*/
addFooterLink: function(text, wikiPage) {
const $footerlinks = this.$dialog.find('.morebits-dialog-footerlinks');
if (this.hasFooterLinks) {
const bullet = document.createElement('span');
bullet.textContent = ' \u2022 '; // U+2022 BULLET
$footerlinks.append(bullet);
}
const link = document.createElement('a');
link.setAttribute('href', mw.util.getUrl(wikiPage));
link.setAttribute('title', wikiPage);
link.setAttribute('target', '_blank');
link.textContent = text;
$footerlinks.append(link);
this.hasFooterLinks = true;
return this;
},
/**
* Sets whether the window should be modal or not. Modal dialogs create
* an overlay below the dialog but above other page elements. This
* must be used (if necessary) before calling display().
*
* @param {boolean} [modal=false] - If set to true, other items on the
* page will be disabled, i.e., cannot be interacted with.
* @return {Morebits.SimpleWindow}
*/
setModality: function(modal) {
if (modal) {
$('<div>').addClass('morebits-dialog-overlay').appendTo(document.body);
this.$dialog.addClass('morebits-dialog-modal');
} else {
$('.morebits-dialog-overlay').remove();
this.$dialog.removeClass('morebits-dialog-modal');
}
return this;
}
};
/**
* Enables or disables all footer buttons on all {@link Morebits.SimpleWindow}s in the current page.
* This should be called with `false` when the button(s) become irrelevant (e.g. just before
* {@link Morebits.Status.init} is called).
* This is not an instance method so that consumers don't have to keep a reference to the
* original `Morebits.SimpleWindow` object sitting around somewhere. Anyway, most of the time
* there will only be one `Morebits.SimpleWindow` open, so this shouldn't matter.
*
* @memberof Morebits.SimpleWindow
* @param {boolean} enabled
*/
Morebits.SimpleWindow.setButtonsEnabled = function(enabled) {
$('.morebits-dialog-buttons button').prop('disabled', !enabled);
};
// Deprecated class name aliases in camelCase. Please use the PascalCase names instead.
Morebits.batchOperation = Morebits.BatchOperation;
Morebits.date = Morebits.Date;
Morebits.quickForm = Morebits.QuickForm;
Morebits.quickForm.element = Morebits.QuickForm.Element;
Morebits.simpleWindow = Morebits.SimpleWindow;
Morebits.status = Morebits.Status;
Morebits.taskManager = Morebits.TaskManager;
Morebits.unbinder = Morebits.Unbinder;
Morebits.userspaceLogger = Morebits.UserspaceLogger;
Morebits.wiki.api = Morebits.wiki.Api;
Morebits.wiki.page = Morebits.wiki.Page;
Morebits.wiki.preview = Morebits.wiki.Preview;
Morebits.wikitext.page = Morebits.wikitext.Page;
}());
/**
* If this script is being executed outside a ResourceLoader context, we add some
* global assignments for legacy scripts, hopefully these can be removed down the line.
*
* IMPORTANT NOTE:
* PLEASE DO NOT USE THESE ALIASES IN NEW CODE!
* Thanks.
*/
if (typeof arguments === 'undefined') { // typeof is here for a reason...
/* global Morebits */
window.SimpleWindow = Morebits.SimpleWindow;
window.QuickForm = Morebits.QuickForm;
window.Wikipedia = Morebits.wiki;
window.Status = Morebits.Status;
}
// </nowiki>
4a9v1ttg3028b97hnextgze0we8c1uh
769523
769456
2026-10-21T23:59:55Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 1406828: rm cmt
769523
javascript
text/javascript
// <nowiki>
/**
* A library full of lots of goodness for user scripts on MediaWiki wikis, including Wikipedia.
*
* The highlights include:
* - {@link Morebits.wiki.Api} - make calls to the MediaWiki API
* - {@link Morebits.wiki.Page} - modify pages on the wiki (edit, revert, delete, etc.)
* - {@link Morebits.Date} - enhanced date object processing, sort of a light moment.js
* - {@link Morebits.QuickForm} - generate quick HTML forms on the fly
* - {@link Morebits.SimpleWindow} - generate dialog windows and modals
* - {@link Morebits.Status} - a rough-and-ready status message displayer, used by the Morebits.wiki classes
* - {@link Morebits.wikitext} - utilities for dealing with wikitext
* - {@link Morebits.string} - utilities for manipulating strings
* - {@link Morebits.array} - utilities for manipulating arrays
* - {@link Morebits.ip} - utilities to help process IP addresses
*
* Dependencies:
* - The whole thing relies on jQuery. But most wikis should provide this by default.
* - {@link Morebits.QuickForm}, {@link Morebits.SimpleWindow}, and {@link Morebits.Status} rely on the "morebits.css" file for their styling.
* - To create a gadget based on morebits.js, use this syntax in MediaWiki:Gadgets-definition:
* - `*GadgetName[ResourceLoader|dependencies=mediawiki.user,mediawiki.util,mediawiki.Title]|morebits.js|morebits.css|GadgetName.js`
* - Alternatively, you can configure morebits.js as a hidden gadget in MediaWiki:Gadgets-definition:
* - `*morebits[ResourceLoader|dependencies=mediawiki.user,mediawiki.util,mediawiki.Title|hidden]|morebits.js|morebits.css`
* and then load ext.gadget.morebits as one of the dependencies for the new gadget.
*
* All the stuff here works on all browsers for which MediaWiki provides JavaScript support.
*
* This library is maintained by the maintainers of Twinkle.
* For queries, suggestions, help, etc., head to [Wikipedia talk:Twinkle on English Wikipedia](http://en.wikipedia.org/wiki/WT:TW).
* The latest development source is available at {@link https://github.com/wikimedia-gadgets/twinkle/blob/master/src/morebits.js|GitHub}.
*
* @namespace Morebits
*/
(function() {
/** @lends Morebits */
const Morebits = {};
window.Morebits = Morebits; // allow global access
/**
* Wiki-specific configurations for Morebits
*/
Morebits.l10n = {
/**
* Local aliases for "redirect" magic word.
* Check using api.php?action=query&format=json&meta=siteinfo&formatversion=2&siprop=magicwords
*/
redirectTagAliases: ['#REDIRECT'],
/**
* Takes a string as argument and checks if it is a timestamp or not
* If not, it returns null. If yes, it returns an array of integers
* in the format [year, month, date, hour, minute, second]
* which can be passed to Date.UTC()
*
* @param {string} str
* @return {number[] | null}
*/
signatureTimestampFormat: function(str) {
// Japanese Wikipedia signatures use UTC with year/month/day before the time.
const japanese = /(\d{4})年(\d{1,2})月(\d{1,2})日\s*\([日月火水木金土]\)\s*(\d{1,2}):(\d{2})\s*\(UTC\)/.exec(str);
if (japanese) {
return [japanese[1], japanese[2] - 1, japanese[3], japanese[4], japanese[5]];
}
// HH:mm, DD Month YYYY (UTC)
const rgx = /(\d{2}):(\d{2}), (\d{1,2}) (\w+) (\d{4}) \(UTC\)/;
const match = rgx.exec(str);
if (!match) {
return null;
}
const month = ['January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'].indexOf(match[4]);
if (month === -1) {
return null;
}
// ..... year ... month .. date ... hour .... minute
return [match[5], month, match[3], match[1], match[2]];
}
};
/**
* Simple helper function to see what groups a user might belong.
*
* @param {string} group - e.g. `sysop`, `extendedconfirmed`, etc.
* @return {boolean}
*/
Morebits.userIsInGroup = function(group) {
return mw.config.get('wgUserGroups').includes(group);
};
/**
* Hardcodes whether the user is a sysop, used a lot.
*
* @type {boolean}
*/
Morebits.userIsSysop = Morebits.userIsInGroup('sysop');
/**
* Determines whether the current page is a redirect or soft redirect. Fails
* to detect soft redirects on edit, history, etc. pages. Will attempt to
* detect Module:RfD, with the same failure points.
*
* @return {boolean}
*/
Morebits.isPageRedirect = function() {
return !!(mw.config.get('wgIsRedirect') || document.getElementById('softredirect') || $('.box-RfD').length);
};
/**
* Stores a normalized (underscores converted to spaces) version of the
* `wgPageName` variable.
*
* @type {string}
*/
Morebits.pageNameNorm = mw.config.get('wgPageName').replace(/_/g, ' ');
/**
* Create a string for use in regex matching a page name. Accounts for
* leading character's capitalization, underscores as spaces, and special
* characters being escaped. See also {@link Morebits.namespaceRegex}.
*
* @param {string} pageName - Page name without namespace.
* @return {string} - For a page name `Foo bar`, returns the string `[Ff]oo[_ ]bar`.
*/
Morebits.pageNameRegex = function(pageName) {
if (pageName === '') {
return '';
}
const firstChar = pageName[0],
remainder = Morebits.string.escapeRegExp(pageName.slice(1));
if (mw.Title.phpCharToUpper(firstChar) !== firstChar.toLowerCase()) {
return '[' + mw.Title.phpCharToUpper(firstChar) + firstChar.toLowerCase() + ']' + remainder;
}
return Morebits.string.escapeRegExp(firstChar) + remainder;
};
/**
* Converts string or array of DOM nodes into an HTML fragment.
* Wikilink syntax (`[[...]]`) is transformed into HTML anchor.
* Used in Morebits.QuickForm and Morebits.Status
*
* @internal
* @param {string|Node|(string|Node)[]} input
* @return {DocumentFragment}
*/
Morebits.createHtml = function(input) {
const fragment = document.createDocumentFragment();
if (!input) {
return fragment;
}
if (!Array.isArray(input)) {
input = [input];
}
for (let i = 0; i < input.length; ++i) {
if (input[i] instanceof Node) {
fragment.appendChild(input[i]);
} else {
$.parseHTML(Morebits.createHtml.renderWikilinks(input[i])).forEach((node) => {
fragment.appendChild(node);
});
}
}
return fragment;
};
/**
* Converts wikilinks to HTML anchor tags.
*
* @param {string} text
* @return {string}
*/
Morebits.createHtml.renderWikilinks = function(text) {
const ub = new Morebits.Unbinder(text);
// Don't convert wikilinks within code tags as they're used for displaying wiki-code
ub.unbind('<code>', '</code>');
ub.content = ub.content.replace(
/\[\[:?(?:([^|\]]+?)\|)?([^\]|]+?)\]\]/g,
(_, target, text) => {
if (!target) {
target = text;
}
return '<a target="_blank" href="' + mw.util.getUrl(target) +
'" title="' + target.replace(/"/g, '"') + '">' + text + '</a>';
});
return ub.rebind();
};
/**
* Create a string for use in regex matching all namespace aliases, regardless
* of the capitalization and underscores/spaces. Doesn't include the optional
* leading `:`, but if there's more than one item, wraps the list in a
* non-capturing group. This means you can do `Morebits.namespaceRegex([4]) +
* ':' + Morebits.pageNameRegex('Twinkle')` to match a full page. Uses
* {@link Morebits.pageNameRegex}.
*
* @param {number[]} namespaces - Array of namespace numbers. Unused/invalid
* namespace numbers are silently discarded.
* @example
* // returns '(?:[Ff][Ii][Ll][Ee]|[Ii][Mm][Aa][Gg][Ee])'
* Morebits.namespaceRegex([6])
* @return {string} - Regex-suitable string of all namespace aliases.
*/
Morebits.namespaceRegex = function(namespaces) {
if (!Array.isArray(namespaces)) {
namespaces = [namespaces];
}
const aliases = [];
let regex;
$.each(mw.config.get('wgNamespaceIds'), (name, number) => {
if (namespaces.includes(number)) {
// Namespaces are completely agnostic as to case,
// and a regex string is more useful/compatible than a RegExp object,
// so we accept any casing for any letter.
aliases.push(name.split('').map((char) => Morebits.pageNameRegex(char)).join(''));
}
});
switch (aliases.length) {
case 0:
regex = '';
break;
case 1:
regex = aliases[0];
break;
default:
regex = '(?:' + aliases.join('|') + ')';
break;
}
return regex;
};
/* **************** Morebits.QuickForm **************** */
/**
* Creation of simple and standard forms without much specific coding.
*
* @namespace Morebits.QuickForm
* @memberof Morebits
* @class
* @param {event} event - Function to execute when form is submitted.
* @param {string} [eventType=submit] - Type of the event.
*/
Morebits.QuickForm = function QuickForm(event, eventType) {
this.root = new Morebits.QuickForm.Element({ type: 'form', event: event, eventType: eventType });
};
/**
* Renders the HTML output of the quickForm.
*
* @memberof Morebits.QuickForm
* @return {HTMLElement}
*/
Morebits.QuickForm.prototype.render = function QuickFormRender() {
const ret = this.root.render();
ret.names = {};
return ret;
};
/**
* Append element to the form.
*
* @memberof Morebits.QuickForm
* @param {(object|Morebits.QuickForm.Element)} data - A quickform element, or the object with which
* a quickform element is constructed.
* @return {Morebits.QuickForm.Element} - Same as what is passed to the function.
*/
Morebits.QuickForm.prototype.append = function QuickFormAppend(data) {
return this.root.append(data);
};
/**
* Create a new element for the the form.
*
* Index to Morebits.QuickForm.Element types:
* - Global attributes: id, className, style, tooltip, extra, $data, adminonly
* - `select`: A combo box (aka drop-down).
* - Attributes: name, label, multiple, size, list, event, disabled
* - `option`: An element for a combo box.
* - Attributes: value, label, selected, disabled
* - `optgroup`: A group of "option"s.
* - Attributes: label, list
* - `field`: A fieldset (aka group box).
* - Attributes: name, label, disabled
* - `checkbox`: A checkbox. Must use "list" parameter.
* - Attributes: name, list, event
* - Attributes (within list): name, label, value, checked, disabled, event, subgroup
* - `radio`: A radio button. Must use "list" parameter.
* - Attributes: name, list, event
* - Attributes (within list): name, label, value, checked, disabled, event, subgroup
* - `input`: A text input box.
* - Attributes: name, label, value, size, placeholder, maxlength, disabled, required, readonly, event
* - `number`: A number input box.
* - Attributes: Everything the text `input` has, as well as: min, max, step, list
* - `dyninput`: A set of text boxes with "Remove" buttons and an "Add" button.
* - Attributes: name, label, min, max, inputs, sublabel, value, size, maxlength, event
* - `hidden`: An invisible form field.
* - Attributes: name, value
* - `header`: A level 5 header.
* - Attributes: label
* - `div`: A generic placeholder element or label.
* - Attributes: name, label
* - `submit`: A submit button. Morebits.SimpleWindow moves these to the footer of the dialog.
* - Attributes: name, label, disabled
* - `button`: A generic button.
* - Attributes: name, label, disabled, event
* - `textarea`: A big, multi-line text box.
* - Attributes: name, label, value, cols, rows, disabled, required, readonly
* - `fragment`: A DocumentFragment object.
* - No attributes, and no global attributes except adminonly.
* There is some difference on how types handle the `label` attribute:
* - `div`, `select`, `field`, `checkbox`/`radio`, `input`, `textarea`, `header`, and `dyninput` can accept an array of items,
* and the label item(s) can be `Element`s.
* - `option`, `optgroup`, `_dyninput_cell`, `submit`, and `button` accept only a single string.
*
* @memberof Morebits.QuickForm
* @class
* @param {Object} data - Object representing the quickform element. Should
* specify one of the available types from the index above, as well as any
* relevant and available attributes.
* @example new Morebits.QuickForm.Element({
* name: 'target',
* type: 'input',
* label: 'Your target:',
* tooltip: 'Enter your target. Required.',
* required: true
* });
*/
Morebits.QuickForm.Element = function QuickFormElement(data) {
this.data = data;
this.childs = [];
};
/**
* @memberof Morebits.QuickForm.Element
* @type {number}
*/
Morebits.QuickForm.Element.id = 0;
/**
* Appends an element to current element.
*
* @memberof Morebits.QuickForm.Element
* @param {Morebits.QuickForm.Element} data - A quickForm element or the object required to
* create the quickForm element.
* @return {Morebits.QuickForm.Element} The same element passed in.
*/
Morebits.QuickForm.Element.prototype.append = function QuickFormElementAppend(data) {
let child;
if (data instanceof Morebits.QuickForm.Element) {
child = data;
} else {
child = new Morebits.QuickForm.Element(data);
}
this.childs.push(child);
return child;
};
/**
* Renders the HTML output for the quickForm element. This should be called
* without parameters: `form.render()`.
*
* @memberof Morebits.QuickForm.Element
* @return {HTMLElement}
*/
Morebits.QuickForm.Element.prototype.render = function QuickFormElementRender(internal_subgroup_id) {
const currentNode = this.compute(this.data, internal_subgroup_id);
for (let i = 0; i < this.childs.length; ++i) {
// do not pass internal_subgroup_id to recursive calls
currentNode[1].appendChild(this.childs[i].render());
}
return currentNode[0];
};
/** @memberof Morebits.QuickForm.Element */
Morebits.QuickForm.Element.prototype.compute = function QuickFormElementCompute(data, in_id) {
let node;
let childContainer = null;
let label;
const id = (in_id ? in_id + '_' : '') + 'node_' + Morebits.QuickForm.Element.id++;
if (data.adminonly && !Morebits.userIsSysop) {
// hell hack alpha
data.type = 'hidden';
}
let i, current, subnode;
switch (data.type) {
case 'form':
node = document.createElement('form');
node.className = 'quickform';
node.setAttribute('action', 'javascript:void(0);');
if (data.event) {
node.addEventListener(data.eventType || 'submit', data.event, false);
}
break;
case 'fragment':
node = document.createDocumentFragment();
// fragments can't have any attributes, so just return it straight away
return [node, node];
// Sometimes Twinkle uses fancy searchable "select" elements. This is powered by the third party library "select2". Activate it by creating a Morebits "select" element, then call `$('select[name=sub_group]').select2({});` or similar towards the end of your main code.
case 'select':
node = document.createElement('div');
node.setAttribute('id', 'div_' + id);
if (data.label) {
label = node.appendChild(document.createElement('label'));
label.setAttribute('for', id);
label.appendChild(Morebits.createHtml(data.label));
label.style.marginRight = '3px';
}
var select = node.appendChild(document.createElement('select'));
if (data.event) {
select.addEventListener('change', data.event, false);
}
if (data.multiple) {
select.setAttribute('multiple', 'multiple');
}
if (data.size) {
select.setAttribute('size', data.size);
}
if (data.disabled) {
select.setAttribute('disabled', 'disabled');
}
select.setAttribute('name', data.name);
if (data.list) {
for (i = 0; i < data.list.length; ++i) {
current = data.list[i];
if (current.list) {
current.type = 'optgroup';
} else {
current.type = 'option';
}
subnode = this.compute(current);
select.appendChild(subnode[0]);
}
}
childContainer = select;
break;
case 'option':
node = document.createElement('option');
node.values = data.value;
node.setAttribute('value', data.value);
if (data.selected) {
node.setAttribute('selected', 'selected');
}
if (data.disabled) {
node.setAttribute('disabled', 'disabled');
}
node.setAttribute('label', data.label);
node.appendChild(document.createTextNode(data.label));
break;
case 'optgroup':
node = document.createElement('optgroup');
node.setAttribute('label', data.label);
if (data.list) {
for (i = 0; i < data.list.length; ++i) {
current = data.list[i];
current.type = 'option'; // must be options here
subnode = this.compute(current);
node.appendChild(subnode[0]);
}
}
break;
case 'field':
node = document.createElement('fieldset');
label = node.appendChild(document.createElement('legend'));
label.appendChild(Morebits.createHtml(data.label));
if (data.name) {
node.setAttribute('name', data.name);
}
if (data.disabled) {
node.setAttribute('disabled', 'disabled');
}
break;
case 'checkbox':
case 'radio':
node = document.createElement('div');
if (data.list) {
for (i = 0; i < data.list.length; ++i) {
const cur_id = id + '_' + i;
current = data.list[i];
var cur_div;
if (current.type === 'header') {
// inline hack
cur_div = node.appendChild(document.createElement('h6'));
cur_div.appendChild(document.createTextNode(current.label));
if (current.tooltip) {
Morebits.QuickForm.Element.generateTooltip(cur_div, current);
}
continue;
}
cur_div = node.appendChild(document.createElement('div'));
subnode = cur_div.appendChild(document.createElement('input'));
subnode.values = current.value;
subnode.setAttribute('value', current.value);
subnode.setAttribute('type', data.type);
subnode.setAttribute('id', cur_id);
subnode.setAttribute('name', current.name || data.name);
// If name is provided on the individual checkbox, add a data-single
// attribute which indicates it isn't part of a list of checkboxes with
// same name. Used in getInputData()
if (current.name) {
subnode.setAttribute('data-single', 'data-single');
}
if (current.checked) {
subnode.setAttribute('checked', 'checked');
}
if (current.disabled) {
subnode.setAttribute('disabled', 'disabled');
}
label = cur_div.appendChild(document.createElement('label'));
label.appendChild(Morebits.createHtml(current.label));
label.setAttribute('for', cur_id);
if (current.tooltip) {
Morebits.QuickForm.Element.generateTooltip(label, current);
}
// styles go on the label, doesn't make sense to style a checkbox/radio
if (current.style) {
label.setAttribute('style', current.style);
}
var event;
if (current.subgroup) {
let tmpgroup = current.subgroup;
if (!Array.isArray(tmpgroup)) {
tmpgroup = [tmpgroup];
}
var subgroupRaw = new Morebits.QuickForm.Element({
type: 'div',
id: id + '_' + i + '_subgroup'
});
$.each(tmpgroup, (idx, el) => {
const newEl = $.extend({}, el);
if (!newEl.type) {
newEl.type = data.type;
}
newEl.name = (current.name || data.name) + '.' + newEl.name;
subgroupRaw.append(newEl);
});
const subgroup = subgroupRaw.render(cur_id);
subgroup.className = 'quickformSubgroup';
subnode.subgroup = subgroup;
subnode.shown = false;
event = function(e) {
if (e.target.checked) {
e.target.parentNode.appendChild(e.target.subgroup);
if (e.target.type === 'radio') {
const name = e.target.name;
if (e.target.form.names[name] !== undefined) {
e.target.form.names[name].parentNode.removeChild(e.target.form.names[name].subgroup);
}
e.target.form.names[name] = e.target;
}
} else {
e.target.parentNode.removeChild(e.target.subgroup);
}
};
subnode.addEventListener('change', event, true);
if (current.checked) {
subnode.parentNode.appendChild(subgroup);
}
} else if (data.type === 'radio') {
event = function(e) {
if (e.target.checked) {
const name = e.target.name;
if (e.target.form.names[name] !== undefined) {
e.target.form.names[name].parentNode.removeChild(e.target.form.names[name].subgroup);
}
delete e.target.form.names[name];
}
};
subnode.addEventListener('change', event, true);
}
// add users' event last, so it can interact with the subgroup
if (data.event) {
subnode.addEventListener('change', data.event, false);
} else if (current.event) {
subnode.addEventListener('change', current.event, true);
}
}
}
if (data.shiftClickSupport && data.type === 'checkbox') {
Morebits.checkboxShiftClickSupport(Morebits.QuickForm.getElements(node, data.name));
}
break;
// input is actually a text-type, so number here inherits the same stuff
case 'number':
case 'input':
node = document.createElement('div');
node.setAttribute('id', 'div_' + id);
if (data.label) {
label = node.appendChild(document.createElement('label'));
label.appendChild(Morebits.createHtml(data.label));
label.setAttribute('for', data.id || id);
label.style.marginRight = '3px';
}
subnode = node.appendChild(document.createElement('input'));
subnode.setAttribute('name', data.name);
if (data.type === 'input') {
subnode.setAttribute('type', 'text');
} else {
subnode.setAttribute('type', 'number');
['min', 'max', 'step', 'list'].forEach((att) => {
if (data[att]) {
subnode.setAttribute(att, data[att]);
}
});
}
['value', 'size', 'placeholder', 'maxlength'].forEach((att) => {
if (data[att]) {
subnode.setAttribute(att, data[att]);
}
});
['disabled', 'required', 'readonly'].forEach((att) => {
if (data[att]) {
subnode.setAttribute(att, att);
}
});
if (data.event) {
subnode.addEventListener('input', data.event, false);
}
childContainer = subnode;
break;
case 'dyninput':
var min = data.min || 1;
var max = data.max || Infinity;
node = document.createElement('div');
label = node.appendChild(document.createElement('h5'));
label.appendChild(Morebits.createHtml(data.label));
var listNode = node.appendChild(document.createElement('div'));
var more = this.compute({
type: 'button',
label: 'more',
disabled: min >= max,
event: function(e) {
const new_node = new Morebits.QuickForm.Element(e.target.sublist);
e.target.area.appendChild(new_node.render());
if (++e.target.counter >= e.target.max) {
e.target.setAttribute('disabled', 'disabled');
}
e.stopPropagation();
}
});
node.appendChild(more[0]);
var moreButton = more[1];
var sublist = {
type: '_dyninput_row',
remove: false,
maxlength: data.maxlength,
event: data.event,
inputs: data.inputs || [{
// compatibility
label: data.sublabel || data.label,
name: data.name,
value: data.value,
size: data.size
}]
};
for (i = 0; i < min; ++i) {
const elem = new Morebits.QuickForm.Element(sublist);
listNode.appendChild(elem.render());
}
sublist.remove = true;
sublist.morebutton = moreButton;
sublist.listnode = listNode;
moreButton.sublist = sublist;
moreButton.area = listNode;
moreButton.max = max - min;
moreButton.counter = 0;
break;
case '_dyninput_row': // Private
node = document.createElement('div');
data.inputs.forEach((subdata) => {
const cell = new Morebits.QuickForm.Element($.extend(subdata, { type: '_dyninput_cell' }));
node.appendChild(cell.render());
});
if (data.remove) {
const remove = this.compute({
type: 'button',
label: 'remove',
event: function(e) {
const list = e.target.listnode;
const node = e.target.inputnode;
const more = e.target.morebutton;
list.removeChild(node);
--more.counter;
more.removeAttribute('disabled');
e.stopPropagation();
}
});
node.appendChild(remove[0]);
const removeButton = remove[1];
removeButton.inputnode = node;
removeButton.listnode = data.listnode;
removeButton.morebutton = data.morebutton;
}
break;
case '_dyninput_cell': // Private, similar to normal input
node = document.createElement('span');
if (data.label) {
label = node.appendChild(document.createElement('label'));
label.appendChild(document.createTextNode(data.label));
label.setAttribute('for', id + '_input');
label.style.marginRight = '3px';
}
subnode = node.appendChild(document.createElement('input'));
subnode.setAttribute('id', id + '_input');
if (data.value) {
subnode.setAttribute('value', data.value);
}
subnode.setAttribute('name', data.name);
subnode.setAttribute('type', 'text');
subnode.setAttribute('data-dyninput', 'data-dyninput');
if (data.size) {
subnode.setAttribute('size', data.size);
}
if (data.maxlength) {
subnode.setAttribute('maxlength', data.maxlength);
}
if (data.required) {
subnode.setAttribute('required', 'required');
}
if (data.disabled) {
subnode.setAttribute('required', 'disabled');
}
if (data.event) {
subnode.addEventListener('input', data.event, false);
}
node.style.marginRight = '3px';
break;
case 'hidden':
node = document.createElement('input');
node.setAttribute('type', 'hidden');
node.values = data.value;
node.setAttribute('value', data.value);
node.setAttribute('name', data.name);
break;
case 'header':
node = document.createElement('h5');
node.appendChild(Morebits.createHtml(data.label));
break;
case 'div':
node = document.createElement('div');
if (data.name) {
node.setAttribute('name', data.name);
}
if (data.label) {
const result = document.createElement('span');
result.className = 'quickformDescription';
result.appendChild(Morebits.createHtml(data.label));
node.appendChild(result);
}
break;
case 'submit':
node = document.createElement('span');
childContainer = node.appendChild(document.createElement('input'));
childContainer.setAttribute('type', 'submit');
if (data.label) {
childContainer.setAttribute('value', data.label);
}
childContainer.setAttribute('name', data.name || 'submit');
if (data.disabled) {
childContainer.setAttribute('disabled', 'disabled');
}
break;
case 'button':
node = document.createElement('span');
childContainer = node.appendChild(document.createElement('input'));
childContainer.setAttribute('type', 'button');
if (data.label) {
childContainer.setAttribute('value', data.label);
}
childContainer.setAttribute('name', data.name);
if (data.disabled) {
childContainer.setAttribute('disabled', 'disabled');
}
if (data.event) {
childContainer.addEventListener('click', data.event, false);
}
break;
case 'textarea':
node = document.createElement('div');
node.setAttribute('id', 'div_' + id);
if (data.label) {
label = node.appendChild(document.createElement('h5'));
const labelElement = document.createElement('label');
labelElement.appendChild(Morebits.createHtml(data.label));
labelElement.setAttribute('for', data.id || id);
label.appendChild(labelElement);
}
subnode = node.appendChild(document.createElement('textarea'));
subnode.setAttribute('name', data.name);
if (data.cols) {
subnode.setAttribute('cols', data.cols);
}
if (data.rows) {
subnode.setAttribute('rows', data.rows);
}
if (data.disabled) {
subnode.setAttribute('disabled', 'disabled');
}
if (data.required) {
subnode.setAttribute('required', 'required');
}
if (data.readonly) {
subnode.setAttribute('readonly', 'readonly');
}
if (data.value) {
subnode.value = data.value;
}
childContainer = subnode;
break;
default:
throw new Error('Morebits.QuickForm: unknown element type ' + data.type.toString());
}
if (!childContainer) {
childContainer = node;
}
if (data.tooltip) {
Morebits.QuickForm.Element.generateTooltip(label || node, data);
}
if (data.extra) {
childContainer.extra = data.extra;
}
if (data.$data) {
$(childContainer).data(data.$data);
}
if (data.style) {
childContainer.setAttribute('style', data.style);
}
if (data.className) {
childContainer.className = childContainer.className ?
childContainer.className + ' ' + data.className :
data.className;
}
childContainer.setAttribute('id', data.id || id);
return [node, childContainer];
};
Morebits.QuickForm.$tooltip = null;
/**
* Create a tooltip.
*
* @memberof Morebits.QuickForm.Element
* @param {HTMLElement} node - The HTML element beside which a tooltip is to be generated.
* @param {Object} data - Tooltip-related configuration data.
*/
Morebits.QuickForm.Element.generateTooltip = function QuickFormElementGenerateTooltip(node, data) {
if (!Morebits.QuickForm.$tooltip) {
Morebits.QuickForm.$tooltip = $('<div>')
.attr('id', 'morebits-ui-tooltip')
.attr('role', 'tooltip')
.addClass('morebits-ui-tooltip')
.appendTo('body');
}
const $tooltip = Morebits.QuickForm.$tooltip;
const $button = $('<span>')
.addClass('morebits-tooltipButton')
.text('?')
.appendTo(node);
$button.on('mouseenter', () => {
$tooltip.html(data.tooltip).addClass('visible');
const buttonRect = $button[0].getBoundingClientRect();
const tooltipRect = $tooltip[0].getBoundingClientRect();
const topOffset = buttonRect.bottom + tooltipRect.height < window.innerHeight ?
buttonRect.bottom : // It fits at the top of the question mark - place it there
Math.max(0, buttonRect.top - tooltipRect.height); // Else, place to the bottom of the question mark
const leftOffset = buttonRect.right + tooltipRect.width < window.innerWidth ?
buttonRect.right : // It fits at the right of the question mark - place it there
Math.max(0, buttonRect.left - tooltipRect.width); // Else, place to the left of the question mark
$tooltip.css('top', window.scrollY + topOffset);
$tooltip.css('left', window.scrollX + leftOffset);
}).on('mouseleave', () => {
$tooltip.removeClass('visible');
});
};
// Some utility methods for manipulating quickForms after their creation:
// (None of these work for "dyninput" type fields at present)
/**
* Returns an object containing all filled form data entered by the user, with the object
* keys being the form element names. Disabled fields will be ignored, but not hidden fields.
*
* @memberof Morebits.QuickForm
* @param {HTMLFormElement} form
* @return {Object} With field names as keys, input data as values.
*/
Morebits.QuickForm.getInputData = function(form) {
const result = {};
for (let i = 0; i < form.elements.length; i++) {
const field = form.elements[i];
if (field.disabled || !field.name || !field.type ||
field.type === 'submit' || field.type === 'button') {
continue;
}
// For elements in subgroups, quickform prepends element names with
// name of the parent group followed by a period, get rid of that.
const fieldNameNorm = field.name.slice(field.name.indexOf('.') + 1);
switch (field.type) {
case 'radio':
if (field.checked) {
result[fieldNameNorm] = field.value;
}
break;
case 'checkbox':
if (field.dataset.single) {
result[fieldNameNorm] = field.checked; // boolean
} else {
result[fieldNameNorm] = result[fieldNameNorm] || [];
if (field.checked) {
result[fieldNameNorm].push(field.value);
}
}
break;
case 'select-multiple':
result[fieldNameNorm] = $(field).val(); // field.value doesn't work
break;
case 'text': // falls through
case 'textarea':
if (field.dataset.dyninput) {
result[fieldNameNorm] = result[fieldNameNorm] || [];
result[fieldNameNorm].push(field.value.trim());
} else {
result[fieldNameNorm] = field.value.trim();
}
break;
default: // could be select-one, date, number, email, etc
if (field.value) {
result[fieldNameNorm] = field.value;
}
break;
}
}
return result;
};
/**
* Returns all form elements with a given field name or ID.
*
* @memberof Morebits.QuickForm
* @param {HTMLFormElement} form
* @param {string} fieldName - The name or id of the fields.
* @return {HTMLElement[]} - Array of matching form elements.
*/
Morebits.QuickForm.getElements = function QuickFormGetElements(form, fieldName) {
const $form = $(form);
fieldName = $.escapeSelector(fieldName); // sanitize input
let $elements = $form.find('[name="' + fieldName + '"]');
if ($elements.length > 0) {
return $elements.toArray();
}
$elements = $form.find('#' + fieldName);
return $elements.toArray();
};
/**
* Searches the array of elements for a checkbox or radio button with a certain
* `value` attribute, and returns the first such element. Returns null if not found.
*
* @memberof Morebits.QuickForm
* @param {HTMLInputElement[]} elementArray - Array of checkbox or radio elements.
* @param {string} value - Value to search for.
* @return {HTMLInputElement}
*/
Morebits.QuickForm.getCheckboxOrRadio = function QuickFormGetCheckboxOrRadio(elementArray, value) {
const found = $.grep(elementArray, (el) => el.value === value);
if (found.length > 0) {
return found[0];
}
return null;
};
/**
* Returns the <div> containing the form element, or the form element itself
* May not work as expected on checkboxes or radios.
*
* @memberof Morebits.QuickForm
* @param {HTMLElement} element
* @return {HTMLElement}
*/
Morebits.QuickForm.getElementContainer = function QuickFormGetElementContainer(element) {
// for divs, headings and fieldsets, the container is the element itself
if (element instanceof HTMLFieldSetElement || element instanceof HTMLDivElement ||
element instanceof HTMLHeadingElement) {
return element;
}
// for others, just return the parent node
return element.parentNode;
};
/**
* Gets the HTML element that contains the label of the given form element
* (mainly for internal use).
*
* @memberof Morebits.QuickForm
* @param {(HTMLElement|Morebits.QuickForm.Element)} element
* @return {HTMLElement}
*/
Morebits.QuickForm.getElementLabelObject = function QuickFormGetElementLabelObject(element) {
// for buttons, divs and headers, the label is on the element itself
if (element.type === 'button' || element.type === 'submit' ||
element instanceof HTMLDivElement || element instanceof HTMLHeadingElement) {
return element;
// for fieldsets, the label is the child <legend> element
} else if (element instanceof HTMLFieldSetElement) {
return element.getElementsByTagName('legend')[0];
// for textareas, the label is the sibling <h5> element
} else if (element instanceof HTMLTextAreaElement) {
return element.parentNode.getElementsByTagName('h5')[0];
}
// for others, the label is the sibling <label> element
return element.parentNode.getElementsByTagName('label')[0];
};
/**
* Gets the label text of the element.
*
* @memberof Morebits.QuickForm
* @param {(HTMLElement|Morebits.QuickForm.Element)} element
* @return {string}
*/
Morebits.QuickForm.getElementLabel = function QuickFormGetElementLabel(element) {
const labelElement = Morebits.QuickForm.getElementLabelObject(element);
if (!labelElement) {
return null;
}
return labelElement.firstChild.textContent;
};
/**
* Sets the label of the element to the given text.
*
* @memberof Morebits.QuickForm
* @param {(HTMLElement|Morebits.QuickForm.Element)} element
* @param {string} labelText
* @return {boolean} True if succeeded, false if the label element is unavailable.
*/
Morebits.QuickForm.setElementLabel = function QuickFormSetElementLabel(element, labelText) {
const labelElement = Morebits.QuickForm.getElementLabelObject(element);
if (!labelElement) {
return false;
}
labelElement.firstChild.textContent = labelText;
return true;
};
/**
* Stores the element's current label, and temporarily sets the label to the given text.
*
* @memberof Morebits.QuickForm
* @param {(HTMLElement|Morebits.QuickForm.Element)} element
* @param {string} temporaryLabelText
* @return {boolean} `true` if succeeded, `false` if the label element is unavailable.
*/
Morebits.QuickForm.overrideElementLabel = function QuickFormOverrideElementLabel(element, temporaryLabelText) {
if (!element.hasAttribute('data-oldlabel')) {
element.setAttribute('data-oldlabel', Morebits.QuickForm.getElementLabel(element));
}
return Morebits.QuickForm.setElementLabel(element, temporaryLabelText);
};
/**
* Restores the label stored by overrideElementLabel.
*
* @memberof Morebits.QuickForm
* @param {(HTMLElement|Morebits.QuickForm.Element)} element
* @return {boolean} True if succeeded, false if the label element is unavailable.
*/
Morebits.QuickForm.resetElementLabel = function QuickFormResetElementLabel(element) {
if (element.hasAttribute('data-oldlabel')) {
return Morebits.QuickForm.setElementLabel(element, element.getAttribute('data-oldlabel'));
}
return null;
};
/**
* Shows or hides a form element plus its label and tooltip.
*
* @memberof Morebits.QuickForm
* @param {(HTMLElement|jQuery|string)} element - HTML/jQuery element, or jQuery selector string.
* @param {boolean} [visibility] - Skip this to toggle visibility.
*/
Morebits.QuickForm.setElementVisibility = function QuickFormSetElementVisibility(element, visibility) {
$(element).toggle(visibility);
};
/**
* Shows or hides the question mark icon (which displays the tooltip) next to a form element.
*
* @memberof Morebits.QuickForm
* @param {(HTMLElement|jQuery)} element
* @param {boolean} [visibility] - Skip this to toggle visibility.
*/
Morebits.QuickForm.setElementTooltipVisibility = function QuickFormSetElementTooltipVisibility(element, visibility) {
$(Morebits.QuickForm.getElementContainer(element)).find('.morebits-tooltipButton').toggle(visibility);
};
/**
* @external HTMLFormElement
*/
/**
* Get checked items in the form.
*
* @method external:HTMLFormElement.getChecked
* @param {string} name - Find checked property of elements (i.e. a checkbox
* or a radiobutton) with the given name, or select options that have selected
* set to true (don't try to mix selects with radio/checkboxes).
* @param {string} [type] - Optionally specify either radio or checkbox (for
* the event that both checkboxes and radiobuttons have the same name).
* @return {string[]} - Contains the values of elements with the given name
* checked property set to true.
*/
HTMLFormElement.prototype.getChecked = function(name, type) {
const elements = this.elements[name];
if (!elements) {
return [];
}
const return_array = [];
let i;
if (elements instanceof HTMLSelectElement) {
const options = elements.options;
for (i = 0; i < options.length; ++i) {
if (options[i].selected) {
if (options[i].values) {
return_array.push(options[i].values);
} else {
return_array.push(options[i].value);
}
}
}
} else if (elements instanceof HTMLInputElement) {
if (type && elements.type !== type) {
return [];
} else if (elements.checked) {
return [elements.value];
}
} else {
for (i = 0; i < elements.length; ++i) {
if (elements[i].checked) {
if (type && elements[i].type !== type) {
continue;
}
if (elements[i].values) {
return_array.push(elements[i].values);
} else {
return_array.push(elements[i].value);
}
}
}
}
return return_array;
};
/**
* Does the same as {@link HTMLFormElement.getChecked|getChecked}, but with unchecked elements.
*
* @method external:HTMLFormElement.getUnchecked
* @param {string} name - Find checked property of elements (i.e. a checkbox
* or a radiobutton) with the given name, or select options that have selected
* set to true (don't try to mix selects with radio/checkboxes).
* @param {string} [type] - Optionally specify either radio or checkbox (for
* the event that both checkboxes and radiobuttons have the same name).
* @return {string[]} - Contains the values of elements with the given name
* checked property set to true.
*/
HTMLFormElement.prototype.getUnchecked = function(name, type) {
const elements = this.elements[name];
if (!elements) {
return [];
}
const return_array = [];
let i;
if (elements instanceof HTMLSelectElement) {
const options = elements.options;
for (i = 0; i < options.length; ++i) {
if (!options[i].selected) {
if (options[i].values) {
return_array.push(options[i].values);
} else {
return_array.push(options[i].value);
}
}
}
} else if (elements instanceof HTMLInputElement) {
if (type && elements.type !== type) {
return [];
} else if (!elements.checked) {
return [elements.value];
}
} else {
for (i = 0; i < elements.length; ++i) {
if (!elements[i].checked) {
if (type && elements[i].type !== type) {
continue;
}
if (elements[i].values) {
return_array.push(elements[i].values);
} else {
return_array.push(elements[i].value);
}
}
}
}
return return_array;
};
/**
* Utilities to help process IP addresses.
*
* @namespace Morebits.ip
* @memberof Morebits
*/
Morebits.ip = {
/**
* Converts an IPv6 address to the canonical form stored and used by MediaWiki.
* JavaScript translation of the {@link https://gerrit.wikimedia.org/r/plugins/gitiles/mediawiki/core/+/8eb6ac3e84ea3312d391ca96c12c49e3ad0753bb/includes/utils/IP.php#131|`IP::sanitizeIP()`}
* function from the IPUtils library. Addresses are verbose, uppercase,
* normalized, and expanded to 8 words.
*
* @param {string} address - The IPv6 address, with or without CIDR.
* @return {string}
*/
sanitizeIPv6: function(address) {
address = address.trim();
if (address === '') {
return null;
}
if (!mw.util.isIPv6Address(address, true)) {
return address; // nothing else to do for IPv4 addresses or invalid ones
}
// Remove any whitespaces, convert to upper case
address = address.toUpperCase();
// Expand zero abbreviations
const abbrevPos = address.indexOf('::');
if (abbrevPos > -1) {
// We know this is valid IPv6. Find the last index of the
// address before any CIDR number (e.g. "a:b:c::/24").
const CIDRStart = address.indexOf('/');
const addressEnd = CIDRStart !== -1 ? CIDRStart - 1 : address.length - 1;
// If the '::' is at the beginning...
let repeat, extra, pad;
if (abbrevPos === 0) {
repeat = '0:';
extra = address === '::' ? '0' : ''; // for the address '::'
pad = 9; // 7+2 (due to '::')
// If the '::' is at the end...
} else if (abbrevPos === (addressEnd - 1)) {
repeat = ':0';
extra = '';
pad = 9; // 7+2 (due to '::')
// If the '::' is in the middle...
} else {
repeat = ':0';
extra = ':';
pad = 8; // 6+2 (due to '::')
}
let replacement = repeat;
pad -= address.split(':').length - 1;
for (let i = 1; i < pad; i++) {
replacement += repeat;
}
replacement += extra;
address = address.replace('::', replacement);
}
// Remove leading zeros from each bloc as needed
return address.replace(/(^|:)0+([0-9A-Fa-f]{1,4})/g, '$1$2');
},
/**
* Determine if the given IP address is a range. Just conjoins
* `mw.util.isIPAddress` with and without the `allowBlock` option.
*
* @param {string} ip
* @return {boolean} - True if given a valid IP address range, false otherwise.
*/
isRange: function(ip) {
return mw.util.isIPAddress(ip, true) && !mw.util.isIPAddress(ip);
},
/**
* Check that an IP range is within the CIDR limits. Most likely to be useful
* in conjunction with `wgRelevantUserName`. CIDR limits are hardcoded as /16
* for IPv4 and /32 for IPv6.
*
* @return {boolean} - True for valid ranges within the CIDR limits,
* otherwise false (ranges outside the limit, single IPs, non-IPs).
*/
validCIDR: function(ip) {
if (Morebits.ip.isRange(ip)) {
const subnet = parseInt(ip.match(/\/(\d{1,3})$/)[1], 10);
if (subnet) { // Should be redundant
if (mw.util.isIPv6Address(ip, true)) {
if (subnet >= 32) {
return true;
}
} else {
if (subnet >= 16) {
return true;
}
}
}
}
return false;
},
/**
* Get the /64 subnet for an IPv6 address.
*
* @param {string} ipv6 - The IPv6 address, with or without a subnet.
* @return {boolean|string} - False if not IPv6 or bigger than a 64,
* otherwise the (sanitized) /64 address.
*/
get64: function(ipv6) {
if (!ipv6 || !mw.util.isIPv6Address(ipv6, true)) {
return false;
}
const subnetMatch = ipv6.match(/\/(\d{1,3})$/);
if (subnetMatch && parseInt(subnetMatch[1], 10) < 64) {
return false;
}
ipv6 = Morebits.ip.sanitizeIPv6(ipv6);
const ip_re = /^((?:[0-9A-F]{1,4}:){4})(?:[0-9A-F]{1,4}:){3}[0-9A-F]{1,4}(?:\/\d{1,3})?$/;
// eslint-disable-next-line no-useless-concat
return ipv6.replace(ip_re, '$1' + '0:0:0:0/64');
}
};
/**
* Helper functions to manipulate strings.
*
* @namespace Morebits.string
* @memberof Morebits
*/
Morebits.string = {
/**
* @param {string} str
* @return {string}
*/
toUpperCaseFirstChar: function(str) {
str = str.toString();
return str.slice(0, 1).toUpperCase() + str.slice(1);
},
/**
* @param {string} str
* @return {string}
*/
toLowerCaseFirstChar: function(str) {
str = str.toString();
return str.slice(0, 1).toLowerCase() + str.slice(1);
},
/**
* Gives an array of substrings of `str` - starting with `start` and
* ending with `end` - which is not in `skiplist`. Intended for use
* on wikitext with templates or links.
*
* @param {string} str
* @param {string} start
* @param {string} end
* @param {(string[]|string)} [skiplist]
* @return {string[]}
* @throws {Error} If the `start` and `end` strings aren't of the same length.
* @throws {Error} If `skiplist` isn't an array or string
*/
splitWeightedByKeys: function(str, start, end, skiplist) {
if (start.length !== end.length) {
throw new Error('start marker and end marker must be of the same length');
}
let level = 0;
let initial = null;
const result = [];
if (!Array.isArray(skiplist)) {
if (skiplist === undefined) {
skiplist = [];
} else if (typeof skiplist === 'string') {
skiplist = [skiplist];
} else {
throw new Error('non-applicable skiplist parameter');
}
}
for (let i = 0; i < str.length; ++i) {
for (let j = 0; j < skiplist.length; ++j) {
if (str.substr(i, skiplist[j].length) === skiplist[j]) {
i += skiplist[j].length - 1;
continue;
}
}
if (str.substr(i, start.length) === start) {
if (initial === null) {
initial = i;
}
++level;
i += start.length - 1;
} else if (str.substr(i, end.length) === end) {
--level;
i += end.length - 1;
}
if (!level && initial !== null) {
result.push(str.substring(initial, i + 1));
initial = null;
}
}
return result;
},
/**
* Formats freeform "reason" (from a textarea) for deletion/other
* templates that are going to be substituted, (e.g. PROD, XFD, RPP).
* Handles `|` outside a nowiki tag.
* Optionally, also adds a signature if not present already.
*
* @param {string} str
* @param {boolean} [addSig]
* @return {string}
*/
formatReasonText: function(str, addSig) {
let reason = (str || '').toString().trim();
const unbinder = new Morebits.Unbinder(reason);
// eslint-disable-next-line no-useless-concat
unbinder.unbind('<no' + 'wiki>', '</no' + 'wiki>');
unbinder.content = unbinder.content.replace(/\|/g, '{{subst:!}}');
reason = unbinder.rebind();
if (addSig) {
const sig = '~~~~', sigIndex = reason.lastIndexOf(sig);
if (sigIndex === -1 || sigIndex !== reason.length - sig.length) {
reason += ' ' + sig;
}
}
return reason.trim();
},
/**
* Formats a "reason" (from a textarea) for inclusion in a userspace
* log. Replaces newlines with {{Pb}}, and adds an extra `#` before
* list items for proper formatting.
*
* @param {string} str
* @return {string}
*/
formatReasonForLog: function(str) {
return str
// handle line breaks, which otherwise break numbering
.replace(/\n+/g, '{{pb}}')
// put an extra # in front before bulleted or numbered list items
.replace(/^(#+)/mg, '#$1')
.replace(/^(\*+)/mg, '#$1');
},
/**
* Like `String.prototype.replace()`, but escapes any dollar signs in
* the replacement string. Useful when the the replacement string is
* arbitrary, such as a username or freeform user input, and could
* contain dollar signs.
*
* @param {string} string - Text in which to replace.
* @param {(string|RegExp)} pattern
* @param {string} replacement
* @return {string}
*/
safeReplace: function morebitsStringSafeReplace(string, pattern, replacement) {
return string.replace(pattern, replacement.replace(/\$/g, '$$$$'));
},
/**
* Determine if the user-provided expiration will be considered an
* infinite-length by MW.
*
* @see {@link https://phabricator.wikimedia.org/T68646}
*
* @param {string} expiry
* @return {boolean}
*/
isInfinity: function morebitsStringIsInfinity(expiry) {
return ['indefinite', 'infinity', 'infinite', 'never'].includes(expiry);
},
/**
* Escapes a string to be used in a RegExp, replacing spaces and
* underscores with `[_ ]` as they are often equivalent.
*
* @param {string} text - String to be escaped.
* @return {string} - The escaped text.
*/
escapeRegExp: function(text) {
return mw.util.escapeRegExp(text).replace(/ |_/g, '[_ ]');
}
};
/**
* Helper functions to manipulate arrays.
*
* @namespace Morebits.array
* @memberof Morebits
*/
Morebits.array = {
/**
* Remove duplicated items from an array.
*
* @param {Array} arr
* @return {Array} A copy of the array with duplicates removed.
* @throws {Error} When provided a non-array.
*/
uniq: function(arr) {
if (!Array.isArray(arr)) {
throw new Error('A non-array object passed to Morebits.array.uniq');
}
return arr.filter((item, idx) => arr.indexOf(item) === idx);
},
/**
* Remove non-duplicated items from an array.
*
* @param {Array} arr
* @return {Array} A copy of the array with the first instance of each value
* removed; subsequent instances of those values (duplicates) remain.
* @throws {Error} When provided a non-array.
*/
dups: function(arr) {
if (!Array.isArray(arr)) {
throw new Error('A non-array object passed to Morebits.array.dups');
}
return arr.filter((item, idx) => arr.indexOf(item) !== idx);
},
/**
* Break up an array into smaller arrays.
*
* @param {Array} arr
* @param {number} size - Size of each chunk (except the last, which could be different).
* @return {Array[]} An array containing the smaller, chunked arrays.
* @throws {Error} When provided a non-array.
*/
chunk: function(arr, size) {
if (!Array.isArray(arr)) {
throw new Error('A non-array object passed to Morebits.array.chunk');
}
if (typeof size !== 'number' || size <= 0) { // pretty impossible to do anything :)
return [arr]; // we return an array consisting of this array.
}
const numChunks = Math.ceil(arr.length / size);
const result = new Array(numChunks);
for (let i = 0; i < numChunks; i++) {
result[i] = arr.slice(i * size, (i + 1) * size);
}
return result;
}
};
/**
* Utilities to enhance select2 menus. See twinklewarn, twinklexfd,
* twinkleblock for sample usages.
*
* @see {@link https://select2.org/}
*
* @namespace Morebits.select2
* @memberof Morebits
* @requires jQuery.select2
*/
Morebits.select2 = {
matchers: {
/**
* Custom matcher in which if the optgroup name matches, all options in that
* group are shown, like in jquery.chosen.
*/
optgroupFull: function(params, data) {
const originalMatcher = $.fn.select2.defaults.defaults.matcher;
const result = originalMatcher(params, data);
if (result && params.term &&
data.text.toUpperCase().includes(params.term.toUpperCase())) {
result.children = data.children;
}
return result;
},
/** Custom matcher that matches from the beginning of words only. */
wordBeginning: function(params, data) {
const originalMatcher = $.fn.select2.defaults.defaults.matcher;
const result = originalMatcher(params, data);
if (!params.term || (result &&
new RegExp('\\b' + mw.util.escapeRegExp(params.term), 'i').test(result.text))) {
return result;
}
return null;
}
},
/** Underline matched part of options. */
highlightSearchMatches: function(data) {
const searchTerm = Morebits.select2SearchQuery;
if (!searchTerm || data.loading) {
return data.text;
}
const idx = data.text.toUpperCase().indexOf(searchTerm.toUpperCase());
if (idx < 0) {
return data.text;
}
return $('<span>').append(
data.text.slice(0, idx),
$('<span>').css('text-decoration', 'underline').text(data.text.slice(idx, idx + searchTerm.length)),
data.text.slice(idx + searchTerm.length)
);
},
/** Intercept query as it is happening, for use in highlightSearchMatches. */
queryInterceptor: function(params) {
Morebits.select2SearchQuery = params && params.term;
},
/**
* Open dropdown and begin search when the `.select2-selection` has
* focus and a key is pressed.
*
* @see {@link https://github.com/select2/select2/issues/3279#issuecomment-442524147}
*/
autoStart: function(ev) {
if (ev.which < 48) {
return;
}
let $target = $(ev.target).closest('.select2-container');
if (!$target.length) {
return;
}
$target = $target.prev();
$target.select2('open');
const search = $target.data('select2').dropdown.$search ||
$target.data('select2').selection.$search;
// Use DOM .focus() to work around a jQuery 3.6.0 regression (https://github.com/select2/select2/issues/5993)
search[0].focus();
}
};
/**
* Temporarily hide a part of a string while processing the rest of it.
* Used by {@link Morebits.wikitext.Page#commentOutImage|Morebits.wikitext.Page.commentOutImage}.
*
* @memberof Morebits
* @class
* @param {string} string - The initial text to process.
* @example var u = new Morebits.Unbinder('Hello world <!-- world --> world');
* u.unbind('<!--', '-->'); // text inside comment remains intact
* u.content = u.content.replace(/world/g, 'earth');
* u.rebind(); // gives 'Hello earth <!-- world --> earth'
*/
Morebits.Unbinder = function Unbinder(string) {
if (typeof string !== 'string') {
throw new Error('not a string');
}
/** The text being processed. */
this.content = string;
this.counter = 0;
this.history = {};
this.prefix = '%UNIQ::' + Math.random() + '::';
this.postfix = '::UNIQ%';
};
Morebits.Unbinder.prototype = {
/**
* Hide the region encapsulated by the `prefix` and `postfix` from
* string processing. `prefix` and `postfix` will be used in a
* RegExp, so items that need escaping should be use `\\`.
*
* @param {string} prefix
* @param {string} postfix
* @throws {Error} If either `prefix` or `postfix` is missing.
*/
unbind: function UnbinderUnbind(prefix, postfix) {
if (!prefix || !postfix) {
throw new Error('Both prefix and postfix must be provided');
}
const re = new RegExp(prefix + '([\\s\\S]*?)' + postfix, 'g');
this.content = this.content.replace(re, Morebits.Unbinder.getCallback(this));
},
/**
* Restore the hidden portion of the `content` string.
*
* @return {string} The processed output.
*/
rebind: function UnbinderRebind() {
let content = this.content;
content.self = this;
for (const current in this.history) {
if (Object.prototype.hasOwnProperty.call(this.history, current)) {
content = content.replace(current, this.history[current]);
}
}
return content;
},
prefix: null, // %UNIQ::0.5955981644938324::
postfix: null, // ::UNIQ%
content: null, // string
counter: null, // 0++
history: null // {}
};
/** @memberof Morebits.Unbinder */
Morebits.Unbinder.getCallback = function UnbinderGetCallback(self) {
return function UnbinderCallback(match) {
const current = self.prefix + self.counter + self.postfix;
self.history[current] = match;
++self.counter;
return current;
};
};
/* **************** Morebits.Date **************** */
/**
* Create a date object with enhanced processing capabilities, a la
* {@link https://momentjs.com/|moment.js}. MediaWiki timestamp format is also
* acceptable, in addition to everything that JS Date() accepts.
*
* @memberof Morebits
* @class
*/
Morebits.Date = class extends Date {
constructor(...args) {
// Check MediaWiki formats
// Must be first since firefox erroneously accepts the timestamp
// format, sans timezone (See also: #921, #936, #1174, #1187), and the
// 14-digit string will be interpreted differently.
if (args.length === 1) {
const param = args[0];
if (/^\d{14}$/.test(param)) {
// YYYYMMDDHHmmss
const digitMatch = /(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/.exec(param);
if (digitMatch) {
// ..... year ... month .. date ... hour .... minute ..... second
super(Date.UTC(digitMatch[1], digitMatch[2] - 1, digitMatch[3], digitMatch[4], digitMatch[5], digitMatch[6]));
} else {
super(...args);
}
} else if (typeof param === 'string') {
// Wikitext signature timestamp
const dateParts = Morebits.l10n.signatureTimestampFormat(param);
if (dateParts) {
super(Date.UTC(...dateParts));
} else {
super(...args);
}
} else {
super(...args);
}
} else {
super(...args);
}
if (!this.isValid()) {
mw.log.warn('Invalid Morebits.Date initialisation:', args);
}
}
/** @return {boolean} */
isValid() {
return !isNaN(this.getTime());
}
/**
* @param {(Date|Morebits.Date)} date
* @return {boolean}
*/
isBefore(date) {
return this.getTime() < date.getTime();
}
/**
* @param {(Date|Morebits.Date)} date
* @return {boolean}
*/
isAfter(date) {
return this.getTime() > date.getTime();
}
/** @return {string} */
getUTCMonthName() {
return Morebits.Date.localeData.months[this.getUTCMonth()];
}
/** @return {string} */
getUTCMonthNameAbbrev() {
return Morebits.Date.localeData.monthsShort[this.getUTCMonth()];
}
/** @return {string} */
getMonthName() {
return Morebits.Date.localeData.months[this.getMonth()];
}
/** @return {string} */
getMonthNameAbbrev() {
return Morebits.Date.localeData.monthsShort[this.getMonth()];
}
/** @return {string} */
getUTCDayName() {
return Morebits.Date.localeData.days[this.getUTCDay()];
}
/** @return {string} */
getUTCDayNameAbbrev() {
return Morebits.Date.localeData.daysShort[this.getUTCDay()];
}
/** @return {string} */
getDayName() {
return Morebits.Date.localeData.days[this.getDay()];
}
/** @return {string} */
getDayNameAbbrev() {
return Morebits.Date.localeData.daysShort[this.getDay()];
}
/**
* Add a given number of minutes, hours, days, weeks, months, or years to the date.
* This is done in-place. The modified date object is also returned, allowing chaining.
*
* @param {number} number - Should be an integer.
* @param {string} unit
* @throws {Error} If invalid or unsupported unit is given.
* @return {Morebits.Date}
*/
add(number, unit) {
let num = parseInt(number, 10); // normalize
if (isNaN(num)) {
throw new Error('Invalid number "' + number + '" provided.');
}
unit = unit.toLowerCase(); // normalize
const unitMap = Morebits.Date.unitMap;
let unitNorm = unitMap[unit] || unitMap[unit + 's']; // so that both singular and plural forms work
if (unitNorm) {
// No built-in week functions, so rather than build out ISO's getWeek/setWeek, just multiply
// Probably can't be used for Julian->Gregorian changeovers, etc.
if (unitNorm === 'Week') {
unitNorm = 'Date';
num *= 7;
}
this['set' + unitNorm](this['get' + unitNorm]() + num);
return this;
}
throw new Error('Invalid unit "' + unit + '": Only ' + Object.keys(unitMap).join(', ') + ' are allowed.');
}
/**
* Subtracts a given number of minutes, hours, days, weeks, months, or years to the date.
* This is done in-place. The modified date object is also returned, allowing chaining.
*
* @param {number} number - Should be an integer.
* @param {string} unit
* @throws {Error} If invalid or unsupported unit is given.
* @return {Morebits.Date}
*/
subtract(number, unit) {
return this.add(-number, unit);
}
/**
* Format the date into a string per the given format string.
* Replacement syntax is a subset of that in moment.js:
*
* | Syntax | Output |
* |--------|--------|
* | H | Hours (24-hour) |
* | HH | Hours (24-hour, padded to 2 digits) |
* | h | Hours (12-hour) |
* | hh | Hours (12-hour, padded to 2 digits) |
* | A | AM or PM |
* | m | Minutes |
* | mm | Minutes (padded to 2 digits) |
* | s | Seconds |
* | ss | Seconds (padded to 2 digits) |
* | SSS | Milliseconds fragment, 3 digits |
* | d | Day number of the week (Sun=0) |
* | ddd | Abbreviated day name |
* | dddd | Full day name |
* | D | Date |
* | DD | Date (padded to 2 digits) |
* | M | Month number (1-indexed) |
* | MM | Month number (1-indexed, padded to 2 digits) |
* | MMM | Abbreviated month name |
* | MMMM | Full month name |
* | Y | Year |
* | YY | Final two digits of year (20 for 2020, 42 for 1942) |
* | YYYY | Year (same as `Y`) |
*
* @param {string} formatstr - Format the date into a string, using
* the replacement syntax. Use `[` and `]` to escape items. If not
* provided, will return the ISO-8601-formatted string.
* @param {(string|number)} [zone=system] - `system` (for browser-default time zone),
* `utc`, or specify a time zone as number of minutes relative to UTC.
* @return {string}
*/
format(formatstr, zone) {
if (!this.isValid()) {
return 'Invalid date'; // Put the truth out, preferable to "NaNNaNNan NaN:NaN" or whatever
}
let udate = this;
// create a new date object that will contain the date to display as system time
if (zone === 'utc') {
udate = new Morebits.Date(this.getTime()).add(this.getTimezoneOffset(), 'minutes');
} else if (typeof zone === 'number') {
// convert to utc, then add the utc offset given
udate = new Morebits.Date(this.getTime()).add(this.getTimezoneOffset() + zone, 'minutes');
}
// default to ISOString
if (!formatstr) {
return udate.toISOString();
}
const pad = function(num, len) {
len = len || 2; // Up to length of 00 + 1
return ('00' + num).toString().slice(0 - len);
};
var h24 = udate.getHours(), m = udate.getMinutes(), s = udate.getSeconds(), ms = udate.getMilliseconds();
var D = udate.getDate(), M = udate.getMonth() + 1, Y = udate.getFullYear();
var h12 = h24 % 12 || 12, amOrPm = h24 >= 12 ? '午後' : '午前';
var replacementMap = {
HH: pad(h24), H: h24, hh: pad(h12), h: h12, A: amOrPm,
mm: pad(m), m: m,
ss: pad(s), s: s,
SSS: pad(ms, 3),
dddd: udate.getDayName(), ddd: udate.getDayNameAbbrev(), d: udate.getDay(),
DD: pad(D), D: D,
MMMM: udate.getMonthName(), MMM: udate.getMonthNameAbbrev(), MM: pad(M), M: M,
YYYY: Y, YY: pad(Y % 100), Y: Y
};
const unbinder = new Morebits.Unbinder(formatstr); // escape stuff between [...]
unbinder.unbind('\\[', '\\]');
unbinder.content = unbinder.content.replace(
/* Regex notes:
* d(d{2,3})? matches exactly 1, 3 or 4 occurrences of 'd' ('dd' is treated as a double match of 'd')
* Y{1,2}(Y{2})? matches exactly 1, 2 or 4 occurrences of 'Y'
*/
/H{1,2}|h{1,2}|m{1,2}|s{1,2}|SSS|d(d{2,3})?|D{1,2}|M{1,4}|Y{1,2}(Y{2})?|A/g,
(match) => replacementMap[match]
);
return unbinder.rebind().replace(/\[(.*?)\]/g, '$1');
}
/**
* Gives a readable relative time string such as "Yesterday at 6:43 PM" or "Last Thursday at 11:45 AM".
* Similar to `calendar` in moment.js, but with time zone support.
*
* @param {(string|number)} [zone=system] - 'system' (for browser-default time zone),
* 'utc' (for UTC), or specify a time zone as number of minutes past UTC.
* @return {string}
*/
calendar(zone) {
// Zero out the hours, minutes, seconds and milliseconds - keeping only the date;
// find the difference. Note that setHours() returns the same thing as getTime().
const dateDiff = (new Date().setHours(0, 0, 0, 0) -
new Date(this).setHours(0, 0, 0, 0)) / 8.64e7;
switch (true) {
case dateDiff === 0:
return this.format(Morebits.Date.localeData.relativeTimes.thisDay, zone);
case dateDiff === 1:
return this.format(Morebits.Date.localeData.relativeTimes.prevDay, zone);
case dateDiff > 0 && dateDiff < 7:
return this.format(Morebits.Date.localeData.relativeTimes.pastWeek, zone);
case dateDiff === -1:
return this.format(Morebits.Date.localeData.relativeTimes.nextDay, zone);
case dateDiff < 0 && dateDiff > -7:
return this.format(Morebits.Date.localeData.relativeTimes.thisWeek, zone);
default:
return this.format(Morebits.Date.localeData.relativeTimes.other, zone);
}
}
/**
* Get a regular expression that matches wikitext section titles, such
* as `==December 2019==` or `=== Jan 2018 ===`.
*
* @return {RegExp}
*/
monthHeaderRegex() {
return new RegExp('^(==+)\\s*' + this.getUTCFullYear() +
'年+(?:' + this.getUTCMonthName() + '|' + this.getUTCMonthNameAbbrev() + ')\\s*\\1', 'mg');
}
/**
* Creates a wikitext section header with the month and year.
*
* @param {number} [level=2] - Header level. Pass 0 for just the text
* with no wikitext markers (==).
* @return {string}
*/
monthHeader(level) {
// Default to 2, but allow for 0 or stringy numbers
level = parseInt(level, 10);
level = isNaN(level) ? 2 : level;
var header = Array(level + 1).join('='); // String.prototype.repeat not supported in IE 11
var text = this.getUTCFullYear() + '年' + this.getUTCMonthName();
if (header.length) { // wikitext-formatted header
return header + ' ' + text + ' ' + header;
}
return text; // Just the string
}
};
/**
* Localized strings for date processing.
*
* @memberof Morebits.Date
* @type {object.<string, string>}
* @property {string[]} months
* @property {string[]} monthsShort
* @property {string[]} days
* @property {string[]} daysShort
* @property {object.<string, string>} relativeTimes
* @private
*/
Morebits.Date.localeData = {
// message names here correspond to MediaWiki message names
months: ['1月', '2月', '3月',
'4月', '5月', '6月',
'7月', '8月', '9月',
'10月', '11月', '12月'],
monthsShort: ['1月', '2月', '3月',
'4月', '5月', '6月',
'7月', '8月', '9月',
'10月', '11月', '12月'],
days: ['日曜日', '月曜日', '火曜日',
'水曜日', '木曜日', '金曜日',
'土曜日'],
daysShort: ['日', '月', '火',
'水', '木', '金',
'土'],
relativeTimes: {
thisDay: '[今日の]H:mm',
prevDay: '[昨日の]H:mm',
nextDay: '[明日の]H:mm',
thisWeek: 'dddd[の]H:mm',
pastWeek: '[先週]dddd[の]H:mm',
other: 'YYYY MM DD'
}
};
/**
* Map units with getter/setter function names, for `add` and `subtract`
* methods.
*
* @memberof Morebits.Date
* @type {object.<string, string>}
* @property {string} seconds
* @property {string} minutes
* @property {string} hours
* @property {string} days
* @property {string} weeks
* @property {string} months
* @property {string} years
*/
Morebits.Date.unitMap = {
seconds: 'Seconds',
minutes: 'Minutes',
hours: 'Hours',
days: 'Date',
weeks: 'Week', // Not a function but handled in `add` through cunning use of multiplication
months: 'Month',
years: 'FullYear'
};
/* **************** Morebits.wiki **************** */
/**
* Various objects for wiki editing and API access, including
* {@link Morebits.wiki.Api} and {@link Morebits.wiki.Page}.
*
* @namespace Morebits.wiki
* @memberof Morebits
*/
Morebits.wiki = {};
/* **************** Morebits.wiki.actionCompleted **************** */
/**
* @memberof Morebits.wiki
* @type {number}
*/
Morebits.wiki.numberOfActionsLeft = 0;
/**
* @memberof Morebits.wiki
* @type {number}
*/
Morebits.wiki.nbrOfCheckpointsLeft = 0;
/**
* Display message and/or redirect to page upon completion of tasks.
*
* Every call to Morebits.wiki.Api.post() results in the dispatch of an
* asynchronous callback. Each callback can in turn make an additional call to
* Morebits.wiki.Api.post() to continue a processing sequence. At the
* conclusion of the final callback of a processing sequence, it is not
* possible to simply return to the original caller because there is no call
* stack leading back to the original context. Instead,
* Morebits.wiki.actionCompleted.event() is called to display the result to
* the user and to perform an optional page redirect.
*
* The determination of when to call Morebits.wiki.actionCompleted.event() is
* managed through the globals Morebits.wiki.numberOfActionsLeft and
* Morebits.wiki.nbrOfCheckpointsLeft. Morebits.wiki.numberOfActionsLeft is
* incremented at the start of every Morebits.wiki.Api call and decremented
* after the completion of a callback function. If a callback function does
* not create a new Morebits.wiki.Api object before exiting, it is the final
* step in the processing chain and Morebits.wiki.actionCompleted.event() will
* then be called.
*
* Optionally, callers may use Morebits.wiki.addCheckpoint() to indicate that
* processing is not complete upon the conclusion of the final callback
* function. This is used for batch operations. The end of a batch is
* signaled by calling Morebits.wiki.removeCheckpoint().
*
* @memberof Morebits.wiki
*/
Morebits.wiki.actionCompleted = function(self) {
if (--Morebits.wiki.numberOfActionsLeft <= 0 && Morebits.wiki.nbrOfCheckpointsLeft <= 0) {
Morebits.wiki.actionCompleted.event(self);
}
};
// Change per action wanted
/** @memberof Morebits.wiki */
Morebits.wiki.actionCompleted.event = function() {
if (Morebits.wiki.actionCompleted.notice) {
Morebits.Status.actionCompleted(Morebits.wiki.actionCompleted.notice);
}
if (Morebits.wiki.actionCompleted.redirect) {
// if it isn't a URL, make it one. TODO: This breaks on the articles 'http://', 'ftp://', and similar ones.
if (!(/^\w+:\/\//).test(Morebits.wiki.actionCompleted.redirect)) {
Morebits.wiki.actionCompleted.redirect = mw.util.getUrl(Morebits.wiki.actionCompleted.redirect);
if (Morebits.wiki.actionCompleted.followRedirect === false) {
Morebits.wiki.actionCompleted.redirect += '?redirect=no';
}
}
window.setTimeout(() => {
window.location = Morebits.wiki.actionCompleted.redirect;
}, Morebits.wiki.actionCompleted.timeOut);
}
};
/** @memberof Morebits.wiki */
Morebits.wiki.actionCompleted.timeOut = typeof window.wpActionCompletedTimeOut === 'undefined' ? 5000 : window.wpActionCompletedTimeOut;
/** @memberof Morebits.wiki */
Morebits.wiki.actionCompleted.redirect = null;
/** @memberof Morebits.wiki */
Morebits.wiki.actionCompleted.notice = null;
/** @memberof Morebits.wiki */
Morebits.wiki.addCheckpoint = function() {
++Morebits.wiki.nbrOfCheckpointsLeft;
};
/** @memberof Morebits.wiki */
Morebits.wiki.removeCheckpoint = function() {
if (--Morebits.wiki.nbrOfCheckpointsLeft <= 0 && Morebits.wiki.numberOfActionsLeft <= 0) {
Morebits.wiki.actionCompleted.event();
}
};
/* **************** Morebits.wiki.Api **************** */
/**
* An easy way to talk to the MediaWiki API. Accepts either json or xml
* (default) formats; if json is selected, will default to `formatversion=2`
* unless otherwise specified. Similarly, enforces newer `errorformat`s,
* defaulting to `html` if unspecified. `uselang` enforced to the wiki's
* content language.
*
* In new code, the use of the last 3 parameters should be avoided, instead
* use {@link Morebits.wiki.Api#setStatusElement|setStatusElement()} to bind
* the status element (if needed) and use `.then()` or `.catch()` on the
* promise returned by `post()`, rather than specify the `onSuccess` or
* `onFailure` callbacks.
*
* @memberof Morebits.wiki
* @class
* @param {string} currentAction - The current action (required).
* @param {Object} query - The query (required).
* @param {Function} [onSuccess] - The function to call when request is successful.
* @param {Morebits.Status} [statusElement] - A Morebits.Status object to use for status messages.
* @param {Function} [onError] - The function to call if an error occurs.
*/
Morebits.wiki.Api = function(currentAction, query, onSuccess, statusElement, onError) {
this.currentAction = currentAction;
this.query = query;
this.query.assert = 'user';
// Enforce newer error formats, preferring html
if (!query.errorformat || !['wikitext', 'plaintext'].includes(query.errorformat)) {
this.query.errorformat = 'html';
}
// Explicitly use the wiki's content language to minimize confusion,
// see #1179 for discussion
this.query.uselang = 'content';
this.query.errorlang = 'uselang';
this.query.errorsuselocal = 1;
this.onSuccess = onSuccess;
this.onError = onError;
if (statusElement) {
this.setStatusElement(statusElement);
} else {
this.statelem = new Morebits.Status(currentAction);
}
// JSON is used throughout Morebits/Twinkle, but xml remains the default for backwards compatibility
if (!query.format) {
this.query.format = 'xml';
} else if (query.format === 'json' && !query.formatversion) {
this.query.formatversion = '2';
} else if (!['xml', 'json'].includes(query.format)) {
this.statelem.error('Invalid API format: only xml and json are supported.');
}
// Ignore tags for queries and most common unsupported actions, produces warnings
if (query.action && ['query', 'review', 'stabilize', 'watch'].includes(query.action)) {
delete query.tags;
}
};
Morebits.wiki.Api.prototype = {
currentAction: '',
onSuccess: null,
onError: null,
parent: window, // use global context if there is no parent object
query: null,
response: null,
responseXML: null, // use `response` instead; retained for backwards compatibility
statelem: null, // this non-standard name kept for backwards compatibility
statusText: null, // result received from the API, normally "success" or "error"
errorCode: null, // short text error code, if any, as documented in the MediaWiki API
errorText: null, // full error description, if any
badtokenRetry: false, // set to true if this on a retry attempted after a badtoken error
/**
* Keep track of parent object for callbacks.
*
* @param {*} parent
*/
setParent: function(parent) {
this.parent = parent;
},
/** @param {Morebits.Status} statusElement */
setStatusElement: function(statusElement) {
this.statelem = statusElement;
this.statelem.status(this.currentAction);
},
/**
* Carry out the request.
*
* @param {Object} callerAjaxParameters - Do not specify a parameter unless you really
* really want to give jQuery some extra parameters.
* @return {jQuery.Promise} - A jQuery promise object that is resolved or rejected with the api object.
*/
post: function(callerAjaxParameters) {
++Morebits.wiki.numberOfActionsLeft;
const queryString = $.map(this.query, (val, i) => {
if (Array.isArray(val)) {
return encodeURIComponent(i) + '=' + val.map(encodeURIComponent).join('|');
} else if (val !== undefined) {
return encodeURIComponent(i) + '=' + encodeURIComponent(val);
}
}).join('&').replace(/^(.*?)(\btoken=[^&]*)&(.*)/, '$1$3&$2');
// token should always be the last item in the query string (bug TW-B-0013)
const headers = {
'Api-User-Agent': morebitsWikiApiUserAgent
};
if (this.query.action === 'parse') {
// Per https://www.mediawiki.org/wiki/API:Etiquette
headers['Promise-Non-Write-API-Action'] = 'true';
}
const ajaxparams = $.extend({}, {
context: this,
type: this.query.action === 'query' ? 'GET' : 'POST',
url: mw.util.wikiScript('api'),
data: queryString,
dataType: this.query.format,
headers
}, callerAjaxParameters);
return $.ajax(ajaxparams).then(
function onAPIsuccess(response, statusText) {
this.statusText = statusText;
this.response = this.responseXML = response;
// Limit to first error
if (this.query.format === 'json') {
this.errorCode = response.errors && response.errors[0].code;
if (this.query.errorformat === 'html') {
this.errorText = response.errors && response.errors[0].html;
} else if (this.query.errorformat === 'wikitext' || this.query.errorformat === 'plaintext') {
this.errorText = response.errors && response.errors[0].text;
}
} else {
this.errorCode = $(response).find('errors error').eq(0).attr('code');
// Sufficient for html, wikitext, or plaintext errorformats
this.errorText = $(response).find('errors error').eq(0).text();
}
if (typeof this.errorCode === 'string') {
// the API didn't like what we told it, e.g., bad edit token or an error creating a page
return this.returnError(callerAjaxParameters);
}
// invoke success callback if one was supplied
if (this.onSuccess) {
// set the callback context to this.parent for new code and supply the API object
// as the first argument to the callback (for legacy code)
this.onSuccess.call(this.parent, this);
} else {
this.statelem.info('完了');
}
Morebits.wiki.actionCompleted();
return $.Deferred().resolveWith(this.parent, [this]);
},
// only network and server errors reach here - complaints from the API itself are caught in success()
function onAPIfailure(jqXHR, statusText, errorThrown) {
this.statusText = statusText;
this.errorThrown = errorThrown; // frequently undefined
this.errorText = statusText + ' "' + jqXHR.statusText + '" occurred while contacting the API.';
return this.returnError();
}
);
},
returnError: function(callerAjaxParameters) {
if (this.errorCode === 'badtoken' && !this.badtokenRetry) {
this.statelem.warn('トークンが無効です。新しいトークンを取得して再試行します...');
this.badtokenRetry = true;
// Get a new CSRF token and retry. If the original action needs a different
// type of action than CSRF, we do one pointless retry before bailing out
return Morebits.wiki.Api.getToken().then((token) => {
this.query.token = token;
return this.post(callerAjaxParameters);
});
}
this.statelem.error(this.errorText + ' (' + this.errorCode + ')');
// invoke failure callback if one was supplied
if (this.onError) {
// set the callback context to this.parent for new code and supply the API object
// as the first argument to the callback for legacy code
this.onError.call(this.parent, this);
}
// don't complete the action so that the error remains displayed
return $.Deferred().rejectWith(this.parent, [this]);
},
getStatusElement: function() {
return this.statelem;
},
getErrorCode: function() {
return this.errorCode;
},
getErrorText: function() {
return this.errorText;
},
getXML: function() { // retained for backwards compatibility, use getResponse() instead
return this.responseXML;
},
getResponse: function() {
return this.response;
}
};
/**
* Retrieves wikitext from a page. Caching is enabled with a duration of 1 day.
*
* @param {string} title - Page title
* @return {Promise<string|null>} Returns page content, or null if the page doesn't exist.
*/
Morebits.wiki.getCachedPage = function(title) {
return new mw.Api({ userAgent: morebitsWikiApiUserAgent }).get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
rvslots: '*',
format: 'json',
formatversion: '2',
smaxage: '86400', // cache for 1 day
maxage: '86400', // cache for 1 day
uselang: 'content'
}).then((data) => {
const page = data.query.pages[0];
if (page.missing) {
return null;
}
return page.revisions[0].slots.main.content;
});
};
/**
* Retrieves JSON from a page. Caching is enabled with a duration of 1 day.
*
* @param {string} title - Page title
* @return {Promise<string>}
*/
Morebits.wiki.getCachedJson = function(title) {
return Morebits.wiki.getCachedPage(title).then((wikitext) => JSON.parse(wikitext));
};
var morebitsWikiApiUserAgent = 'morebits.js ([[w:WT:TW]])';
/**
* Set the custom user agent header, which is used for server-side logging.
* Note that doing so will set the useragent for every `Morebits.wiki.Api`
* process performed thereafter.
*
* @see {@link https://lists.wikimedia.org/pipermail/mediawiki-api-announce/2014-November/000075.html}
* for original announcement.
*
* @memberof Morebits.wiki.Api
* @param {string} [ua=morebits.js ([[w:WT:TW]])] - User agent. The default
* value of `morebits.js ([[w:WT:TW]])` will be appended to any provided
* value.
*/
Morebits.wiki.Api.setApiUserAgent = function(ua) {
morebitsWikiApiUserAgent = (ua ? ua + ' ' : '') + 'morebits.js ([[w:WT:TW]])';
};
/**
* Get a new CSRF token on encountering token errors.
*
* @memberof Morebits.wiki.Api
* @return {string} MediaWiki CSRF token.
*/
Morebits.wiki.Api.getToken = function() {
var tokenApi = new Morebits.wiki.Api('トークンを取得', {
action: 'query',
meta: 'tokens',
type: 'csrf',
format: 'json'
});
return tokenApi.post().then((apiobj) => apiobj.response.query.tokens.csrftoken);
};
/* **************** Morebits.wiki.Page **************** */
/**
* Use the MediaWiki API to load a page and optionally edit it, move it, etc.
*
* Callers are not permitted to directly access the properties of this class!
* All property access is through the appropriate get___() or set___() method.
*
* Callers should set {@link Morebits.wiki.actionCompleted.notice} and {@link Morebits.wiki.actionCompleted.redirect}
* before the first call to {@link Morebits.wiki.Page.load()}.
*
* Each of the callback functions takes one parameter, which is a
* reference to the Morebits.wiki.Page object that registered the callback.
* Callback functions may invoke any Morebits.wiki.Page prototype method using this reference.
*
*
* Call sequence for common operations (optional final user callbacks not shown):
*
* - Edit current contents of a page (no edit conflict):
* `.load(userTextEditCallback) -> ctx.loadApi.post() ->
* ctx.loadApi.post.success() -> ctx.fnLoadSuccess() -> userTextEditCallback() ->
* .save() -> ctx.saveApi.post() -> ctx.loadApi.post.success() -> ctx.fnSaveSuccess()`
*
* - Edit current contents of a page (with edit conflict):
* `.load(userTextEditCallback) -> ctx.loadApi.post() ->
* ctx.loadApi.post.success() -> ctx.fnLoadSuccess() -> userTextEditCallback() ->
* .save() -> ctx.saveApi.post() -> ctx.loadApi.post.success() ->
* ctx.fnSaveError() -> ctx.loadApi.post() -> ctx.loadApi.post.success() ->
* ctx.fnLoadSuccess() -> userTextEditCallback() -> .save() ->
* ctx.saveApi.post() -> ctx.loadApi.post.success() -> ctx.fnSaveSuccess()`
*
* - Append to a page (similar for prepend and newSection):
* `.append() -> ctx.loadApi.post() -> ctx.loadApi.post.success() ->
* ctx.fnLoadSuccess() -> ctx.fnAutoSave() -> .save() -> ctx.saveApi.post() ->
* ctx.loadApi.post.success() -> ctx.fnSaveSuccess()`
*
* Notes:
* 1. All functions following Morebits.wiki.Api.post() are invoked asynchronously from the jQuery AJAX library.
* 2. The sequence for append/prepend/newSection could be slightly shortened,
* but it would require significant duplication of code for little benefit.
*
* @memberof Morebits.wiki
* @class
* @param {string} pageName - The name of the page, prefixed by the namespace (if any).
* For the current page, use `mw.config.get('wgPageName')`.
* @param {string|Morebits.Status} [status] - A string describing the action about to be undertaken,
* or a Morebits.Status object
*/
Morebits.wiki.Page = function(pageName, status) {
if (!status) {
status = '"' + pageName + '"を開く';
}
/**
* Private context variables.
*
* This context is not visible to the outside, thus all the data here
* must be accessed via getter and setter functions.
*
* @private
*/
const ctx = {
// backing fields for public properties
pageName: pageName,
pageExists: false,
editSummary: null,
changeTags: null,
testActions: null, // array if any valid actions
callbackParameters: null,
statusElement: status instanceof Morebits.Status ? status : new Morebits.Status(status),
// - edit
pageText: null,
editMode: 'all', // save() replaces entire contents of the page by default
appendText: null, // can't reuse pageText for this because pageText is needed to follow a redirect
prependText: null, // can't reuse pageText for this because pageText is needed to follow a redirect
newSectionText: null,
newSectionTitle: null,
createOption: null,
minorEdit: false,
botEdit: false,
pageSection: null,
maxConflictRetries: 2,
maxRetries: 2,
followRedirect: false,
followCrossNsRedirect: true,
watchlistOption: 'nochange',
watchlistExpiry: null,
discussionToolsAutoSubscribe: null,
creator: null,
timestamp: null,
// - revert
revertOldID: null,
// - move
moveDestination: null,
moveTalkPage: false,
moveSubpages: false,
moveSuppressRedirect: false,
// - protect
protectEdit: null,
protectMove: null,
protectCreate: null,
protectCascade: null,
// - delete
deleteTalkPage: false,
// - undelete
undeleteTalkPage: false,
// - creation lookup
lookupNonRedirectCreator: false,
// - stabilize (FlaggedRevs)
flaggedRevs: null,
// internal status
pageLoaded: false,
csrfToken: null,
loadTime: null,
lastEditTime: null,
pageID: null,
contentModel: null,
pageParsed: null,
sections: null,
revertCurID: null,
revertUser: null,
watched: false,
fullyProtected: false,
suppressProtectWarning: false,
conflictRetries: 0,
retries: 0,
// callbacks
onLoadSuccess: null,
onLoadFailure: null,
onParseSuccess: null,
onParseFailure: null,
onSaveSuccess: null,
onSaveFailure: null,
onLookupCreationSuccess: null,
onLookupCreationFailure: null,
onMoveSuccess: null,
onMoveFailure: null,
onDeleteSuccess: null,
onDeleteFailure: null,
onUndeleteSuccess: null,
onUndeleteFailure: null,
onProtectSuccess: null,
onProtectFailure: null,
onStabilizeSuccess: null,
onStabilizeFailure: null,
// internal objects
loadQuery: null,
loadApi: null,
parseApi: null,
saveApi: null,
lookupCreationApi: null,
moveApi: null,
moveProcessApi: null,
patrolApi: null,
patrolProcessApi: null,
triageApi: null,
triageProcessListApi: null,
triageProcessApi: null,
deleteApi: null,
deleteProcessApi: null,
undeleteApi: null,
undeleteProcessApi: null,
protectApi: null,
protectProcessApi: null,
stabilizeApi: null,
stabilizeProcessApi: null
};
const emptyFunction = function() { };
/**
* Loads the text for the page.
*
* @param {Function} onSuccess - Callback function which is called when the load has succeeded.
* @param {Function} [onFailure] - Callback function which is called when the load fails.
*/
this.load = function(onSuccess, onFailure) {
ctx.onLoadSuccess = onSuccess;
ctx.onLoadFailure = onFailure || emptyFunction;
// Need to be able to do something after the page loads
if (!onSuccess) {
ctx.statusElement.error('Internal error: no onSuccess callback provided to load()!');
ctx.onLoadFailure(this);
return;
}
ctx.loadQuery = {
action: 'query',
prop: 'info|revisions',
inprop: 'watched',
intestactions: 'edit', // can be expanded
curtimestamp: '',
meta: 'tokens',
type: 'csrf',
titles: ctx.pageName,
format: 'json'
// don't need rvlimit=1 because we don't need rvstartid here and only one actual rev is returned by default
};
if (ctx.editMode === 'all') {
ctx.loadQuery.rvprop = 'content|timestamp'; // get the page content at the same time, if needed
} else if (ctx.editMode === 'revert') {
ctx.loadQuery.rvprop = 'timestamp';
ctx.loadQuery.rvlimit = 1;
ctx.loadQuery.rvstartid = ctx.revertOldID;
}
if (ctx.followRedirect) {
ctx.loadQuery.redirects = ''; // follow all redirects
}
if (typeof ctx.pageSection === 'number') {
ctx.loadQuery.rvsection = ctx.pageSection;
}
if (Morebits.userIsSysop) {
ctx.loadQuery.inprop += '|protection';
}
ctx.loadApi = new Morebits.wiki.Api('ページを取得中...', ctx.loadQuery, fnLoadSuccess, ctx.statusElement, ctx.onLoadFailure);
ctx.loadApi.setParent(this);
ctx.loadApi.post();
};
/**
* parse the text for the page.
*
* @param {Function} onSuccess - Callback function which is called when the load has succeeded.
* @param {Function} [onFailure] - Callback function which is called when the load fails.
*/
this.parse = function(onSuccess, onFailure) {
ctx.onParseSuccess = onSuccess;
ctx.onParseFailure = onFailure || emptyFunction;
// Need to be able to do something after the page loads
if (!onSuccess) {
ctx.statusElement.error('Internal error: no onSuccess callback provided to parse()!');
ctx.onParseFailure(this);
return;
}
var query = {
action: 'parse',
format: 'json',
page: ctx.pageName,
prop: 'sections',
utf8: 1
};
ctx.parseApi = new Morebits.wiki.Api('ページを取得中...', query, fnParseSuccess, ctx.statusElement, ctx.onParseFailure);
ctx.parseApi.setParent(this);
ctx.parseApi.post();
};
/**
* Saves the text for the page to Wikipedia.
* Must be preceded by successfully calling `load()`.
*
* Warning: Calling `save()` can result in additional calls to the
* previous `load()` callbacks to recover from edit conflicts! In this
* case, callers must make the same edit to the new pageText and
* re-invoke `save()`. This behavior can be disabled with
* `setMaxConflictRetries(0)`.
*
* @param {Function} [onSuccess] - Callback function which is called when the save has succeeded.
* @param {Function} [onFailure] - Callback function which is called when the save fails.
*/
this.save = function(onSuccess, onFailure) {
ctx.onSaveSuccess = onSuccess;
ctx.onSaveFailure = onFailure || emptyFunction;
// are we getting our editing token from mw.user.tokens?
const canUseMwUserToken = fnCanUseMwUserToken('edit');
if (!ctx.pageLoaded && !canUseMwUserToken) {
ctx.statusElement.error('Internal error: attempt to save a page that has not been loaded!');
ctx.onSaveFailure(this);
return;
}
if (!ctx.editSummary) {
// new section mode allows (nay, encourages) using the
// title as the edit summary, but the query needs
// editSummary to be undefined or '', not null
if (ctx.editMode === 'new' && ctx.newSectionTitle) {
ctx.editSummary = '';
} else {
ctx.statusElement.error('Internal error: edit summary not set before save!');
ctx.onSaveFailure(this);
return;
}
}
// shouldn't happen if canUseMwUserToken === true
if (ctx.fullyProtected && !ctx.suppressProtectWarning &&
!confirm(
ctx.fullyProtected === 'infinity' ?
'全保護されたページ "' + ctx.pageName + '" (無期限保護) を編集しようとしています。\n\n編集を続行するにはOKをクリックし、この編集をスキップするにはキャンセルをクリックしてください。' :
'全保護されたページ "' + ctx.pageName +
'" (保護期限: ' + new Morebits.Date(ctx.fullyProtected).calendar('utc') + ' (UTC)) を編集しようとしています。\n\n編集を続行するにはOKをクリックし、この編集をスキップするにはキャンセルをクリックしてください。'
)
) {
ctx.statusElement.error('全保護されたページの編集が中断されました。');
ctx.onSaveFailure(this);
return;
}
ctx.retries = 0;
const query = {
action: 'edit',
title: ctx.pageName,
summary: ctx.editSummary,
token: canUseMwUserToken ? mw.user.tokens.get('csrfToken') : ctx.csrfToken,
watchlist: ctx.watchlistOption,
format: 'json'
};
if (ctx.changeTags) {
query.tags = ctx.changeTags;
}
if (fnApplyWatchlistExpiry()) {
query.watchlistexpiry = ctx.watchlistExpiry;
}
if (typeof ctx.pageSection === 'number') {
query.section = ctx.pageSection;
}
// Set minor edit attribute. If these parameters are present with any value, it is interpreted as true
if (ctx.minorEdit) {
query.minor = true;
} else {
query.notminor = true; // force Twinkle config to override user preference setting for "all edits are minor"
}
// Set bot edit attribute. If this parameter is present with any value, it is interpreted as true
if (ctx.botEdit) {
query.bot = true;
}
if (ctx.discussionToolsAutoSubscribe !== null) {
query.discussiontoolsautosubscribe = ctx.discussionToolsAutoSubscribe ? 'yes' : 'no';
}
switch (ctx.editMode) {
case 'append':
if (ctx.appendText === null) {
ctx.statusElement.error('Internal error: append text not set before save!');
ctx.onSaveFailure(this);
return;
}
query.appendtext = ctx.appendText; // use mode to append to current page contents
break;
case 'prepend':
if (ctx.prependText === null) {
ctx.statusElement.error('Internal error: prepend text not set before save!');
ctx.onSaveFailure(this);
return;
}
query.prependtext = ctx.prependText; // use mode to prepend to current page contents
break;
case 'new':
if (!ctx.newSectionText) { // API doesn't allow empty new section text
ctx.statusElement.error('Internal error: new section text not set before save!');
ctx.onSaveFailure(this);
return;
}
query.section = 'new';
query.text = ctx.newSectionText; // add a new section to current page
query.sectiontitle = ctx.newSectionTitle || ctx.editSummary; // done by the API, but non-'' values would get treated as text
break;
case 'revert':
query.undo = ctx.revertCurID;
query.undoafter = ctx.revertOldID;
if (ctx.lastEditTime) {
query.basetimestamp = ctx.lastEditTime; // check that page hasn't been edited since it was loaded
}
query.starttimestamp = ctx.loadTime; // check that page hasn't been deleted since it was loaded (don't recreate bad stuff)
break;
default: // 'all'
query.text = ctx.pageText; // replace entire contents of the page
if (ctx.lastEditTime) {
query.basetimestamp = ctx.lastEditTime; // check that page hasn't been edited since it was loaded
}
query.starttimestamp = ctx.loadTime; // check that page hasn't been deleted since it was loaded (don't recreate bad stuff)
break;
}
if (['recreate', 'createonly', 'nocreate'].includes(ctx.createOption)) {
query[ctx.createOption] = '';
}
if (canUseMwUserToken && ctx.followRedirect) {
query.redirect = true;
}
ctx.saveApi = new Morebits.wiki.Api('ページを保存中...', query, fnSaveSuccess, ctx.statusElement, fnSaveError);
ctx.saveApi.setParent(this);
ctx.saveApi.post();
};
/**
* Adds the text provided via `setAppendText()` to the end of the
* page. Does not require calling `load()` first, unless a watchlist
* expiry is used.
*
* @param {Function} [onSuccess] - Callback function which is called when the method has succeeded.
* @param {Function} [onFailure] - Callback function which is called when the method fails.
*/
this.append = function(onSuccess, onFailure) {
ctx.editMode = 'append';
if (fnCanUseMwUserToken('edit')) {
this.save(onSuccess, onFailure);
} else {
ctx.onSaveSuccess = onSuccess;
ctx.onSaveFailure = onFailure || emptyFunction;
this.load(fnAutoSave, ctx.onSaveFailure);
}
};
/**
* Adds the text provided via `setPrependText()` to the start of the
* page. Does not require calling `load()` first, unless a watchlist
* expiry is used.
*
* @param {Function} [onSuccess] - Callback function which is called when the method has succeeded.
* @param {Function} [onFailure] - Callback function which is called when the method fails.
*/
this.prepend = function(onSuccess, onFailure) {
ctx.editMode = 'prepend';
if (fnCanUseMwUserToken('edit')) {
this.save(onSuccess, onFailure);
} else {
ctx.onSaveSuccess = onSuccess;
ctx.onSaveFailure = onFailure || emptyFunction;
this.load(fnAutoSave, ctx.onSaveFailure);
}
};
/**
* Creates a new section with the text provided by `setNewSectionText()`
* and section title from `setNewSectionTitle()`.
* If `editSummary` is provided, that will be used instead of the
* autogenerated "->Title (new section" edit summary.
* Does not require calling `load()` first, unless a watchlist expiry
* is used.
*
* @param {Function} [onSuccess] - Callback function which is called when the method has succeeded.
* @param {Function} [onFailure] - Callback function which is called when the method fails.
*/
this.newSection = function(onSuccess, onFailure) {
ctx.editMode = 'new';
if (fnCanUseMwUserToken('edit')) {
this.save(onSuccess, onFailure);
} else {
ctx.onSaveSuccess = onSuccess;
ctx.onSaveFailure = onFailure || emptyFunction;
this.load(fnAutoSave, ctx.onSaveFailure);
}
};
/** @return {string} The name of the loaded page, including the namespace */
this.getPageName = function() {
return ctx.pageName;
};
/** @return {string} The text of the page after a successful load() */
this.getPageText = function() {
return ctx.pageText;
};
/** @return {string} Sections of the page after a successful parse() */
this.getSections = function() {
return ctx.sections;
};
/** @param {string} pageText - Updated page text that will be saved when `save()` is called */
this.setPageText = function(pageText) {
ctx.editMode = 'all';
ctx.pageText = pageText;
};
/** @param {string} appendText - Text that will be appended to the page when `append()` is called */
this.setAppendText = function(appendText) {
ctx.editMode = 'append';
ctx.appendText = appendText;
};
/** @param {string} prependText - Text that will be prepended to the page when `prepend()` is called */
this.setPrependText = function(prependText) {
ctx.editMode = 'prepend';
ctx.prependText = prependText;
};
/** @param {string} newSectionText - Text that will be added in a new section on the page when `newSection()` is called */
this.setNewSectionText = function(newSectionText) {
ctx.editMode = 'new';
ctx.newSectionText = newSectionText;
};
/**
* @param {string} newSectionTitle - Title for the new section created when `newSection()` is called
* If missing, `ctx.editSummary` will be used. Issues may occur if a substituted template is used.
*/
this.setNewSectionTitle = function(newSectionTitle) {
ctx.editMode = 'new';
ctx.newSectionTitle = newSectionTitle;
};
// Edit-related setter methods:
/**
* Set the edit summary that will be used when `save()` is called.
* Unnecessary if editMode is 'new' and newSectionTitle is provided.
*
* @param {string} summary
*/
this.setEditSummary = function(summary) {
ctx.editSummary = summary;
};
/**
* Set any custom tag(s) to be applied to the API action.
* A number of actions don't support it, most notably watch, review,
* and stabilize ({@link https://phabricator.wikimedia.org/T247721|T247721}).
*
* @param {string|string[]} tags - String or array of tag(s).
*/
this.setChangeTags = function(tags) {
ctx.changeTags = tags;
};
/**
* @param {string} [createOption=null] - Can take the following four values:
* - recreate: create the page if it does not exist, or edit it if it exists.
* - createonly: create the page if it does not exist, but return an
* error if it already exists.
* - nocreate: don't create the page, only edit it if it already exists.
* - `null`: create the page if it does not exist, unless it was deleted
* in the moment between loading the page and saving the edit (default).
*/
this.setCreateOption = function(createOption) {
ctx.createOption = createOption;
};
/** @param {boolean} minorEdit - Set true to mark the edit as a minor edit. */
this.setMinorEdit = function(minorEdit) {
ctx.minorEdit = minorEdit;
};
/** @param {boolean} botEdit - Set true to mark the edit as a bot edit */
this.setBotEdit = function(botEdit) {
ctx.botEdit = botEdit;
};
/**
* @param {number} pageSection - Integer specifying the section number to load or save.
* If specified as `null`, the entire page will be retrieved.
*/
this.setPageSection = function(pageSection) {
ctx.pageSection = pageSection;
};
/**
* @param {number} maxConflictRetries - Number of retries for save errors involving an edit conflict or
* loss of token. Default: 2.
*/
this.setMaxConflictRetries = function(maxConflictRetries) {
ctx.maxConflictRetries = maxConflictRetries;
};
/**
* @param {number} maxRetries - Number of retries for save errors not involving an edit conflict or
* loss of token. Default: 2.
*/
this.setMaxRetries = function(maxRetries) {
ctx.maxRetries = maxRetries;
};
/**
* Set whether and how to watch the page, including setting an expiry.
*
* @param {boolean|string|Morebits.Date|Date} [watchlistOption=false] -
* Basically a mix of MW API and Twinkley options available pre-expiry:
* - `true`|`'yes'`|`'watch'`: page will be added to the user's
* watchlist when the action is called. Defaults to an indefinite
* watch unless `watchlistExpiry` is provided.
* - `false`|`'no'`|`'nochange'`: watchlist status of the page (including expiry) will not be changed.
* - `'default'`|`'preferences'`: watchlist status of the page will be
* set based on the user's preference settings when the action is
* called. Defaults to an indefinite watch unless `watchlistExpiry` is
* provided.
* - `'unwatch'`: explicitly unwatch the page.
* - Any other `string` or `number`, or a `Morebits.Date` or `Date`
* object: watch page until the specified time, deferring to
* `watchlistExpiry` if provided.
* @param {string|number|Morebits.Date|Date} [watchlistExpiry=infinity] -
* A date-like string or number, or a date object. If a string or number,
* can be relative (2 weeks) or other similarly date-like (i.e. NOT "potato"):
* ISO 8601: 2038-01-09T03:14:07Z
* MediaWiki: 20380109031407
* UNIX: 2147483647
* SQL: 2038-01-09 03:14:07
* Can also be `infinity` or infinity-like (`infinite`, `indefinite`, and `never`).
* See {@link https://phabricator.wikimedia.org/source/mediawiki-libs-Timestamp/browse/master/src/ConvertibleTimestamp.php;4e53b859a9580c55958078f46dd4f3a44d0fcaa0$57-109?as=source&blame=off}
*/
this.setWatchlist = function(watchlistOption, watchlistExpiry) {
if (watchlistOption instanceof Morebits.Date || watchlistOption instanceof Date) {
watchlistOption = watchlistOption.toISOString();
}
if (typeof watchlistExpiry === 'undefined') {
watchlistExpiry = 'infinity';
} else if (watchlistExpiry instanceof Morebits.Date || watchlistExpiry instanceof Date) {
watchlistExpiry = watchlistExpiry.toISOString();
}
switch (watchlistOption) {
case 'nochange':
case 'no':
case false:
case undefined:
ctx.watchlistOption = 'nochange';
// The MW API allows for changing expiry with nochange (as "nochange" refers to the binary status),
// but by keeping this null it will default to any existing expiry, ensure there is actually "no change."
ctx.watchlistExpiry = null;
break;
case 'unwatch':
// expiry unimportant
ctx.watchlistOption = 'unwatch';
break;
case 'preferences':
case 'default':
ctx.watchlistOption = 'preferences';
// The API allows an expiry here, but there is as of yet (T265716)
// no expiry preference option, so it's a bit devoid of context.
ctx.watchlistExpiry = watchlistExpiry;
break;
case 'watch':
case 'yes':
case true:
ctx.watchlistOption = 'watch';
ctx.watchlistExpiry = watchlistExpiry;
break;
default: // Not really a "default" per se but catches "any other string"
ctx.watchlistOption = 'watch';
ctx.watchlistExpiry = watchlistOption;
break;
}
};
/**
* Set a watchlist expiry. setWatchlist can mostly handle this by
* itself, so this is here largely for completeness and compatibility
* with the full suite of options.
*
* @param {string|number|Morebits.Date|Date} [watchlistExpiry=infinity] -
* A date-like string or number, or a date object. If a string or number,
* can be relative (2 weeks) or other similarly date-like (i.e. NOT "potato"):
* ISO 8601: 2038-01-09T03:14:07Z
* MediaWiki: 20380109031407
* UNIX: 2147483647
* SQL: 2038-01-09 03:14:07
* Can also be `infinity` or infinity-like (`infinite`, `indefinite`, and `never`).
* See {@link https://phabricator.wikimedia.org/source/mediawiki-libs-Timestamp/browse/master/src/ConvertibleTimestamp.php;4e53b859a9580c55958078f46dd4f3a44d0fcaa0$57-109?as=source&blame=off}
*/
this.setWatchlistExpiry = function(watchlistExpiry) {
if (typeof watchlistExpiry === 'undefined') {
watchlistExpiry = 'infinity';
} else if (watchlistExpiry instanceof Morebits.Date || watchlistExpiry instanceof Date) {
watchlistExpiry = watchlistExpiry.toISOString();
}
ctx.watchlistExpiry = watchlistExpiry;
};
/**
* If editing a talk page, set whether to subscribe to any talk
* page thread created by the edit.
*
* @param {boolean} subscribe
*/
this.setDiscussionToolsAutoSubscribe = function(subscribe) {
ctx.discussionToolsAutoSubscribe = subscribe;
};
/**
* @param {boolean} [followRedirect=false] -
* - `true`: a maximum of one redirect will be followed. In the event
* of a redirect, a message is displayed to the user and the redirect
* target can be retrieved with getPageName().
* - `false`: (default) the requested pageName will be used without regard to any redirect.
* @param {boolean} [followCrossNsRedirect=true] - Not applicable if `followRedirect` is not set true.
* - `true`: (default) follow redirect even if it is a cross-namespace redirect
* - `false`: don't follow redirect if it is cross-namespace, edit the redirect itself.
*/
this.setFollowRedirect = function(followRedirect, followCrossNsRedirect) {
if (ctx.pageLoaded) {
ctx.statusElement.error('Internal error: cannot change redirect setting after the page has been loaded!');
return;
}
ctx.followRedirect = followRedirect;
ctx.followCrossNsRedirect = typeof followCrossNsRedirect !== 'undefined' ? followCrossNsRedirect : ctx.followCrossNsRedirect;
};
// lookup-creation setter function
/**
* @param {boolean} flag - If set true, the author and timestamp of
* the first non-redirect version of the page is retrieved.
*
* Warning:
* 1. If there are no revisions among the first 50 that are
* non-redirects, or if there are less 50 revisions and all are
* redirects, the original creation is retrieved.
* 2. Revisions that the user is not privileged to access
* (revdeled/suppressed) will be treated as non-redirects.
* 3. Must not be used when the page has a non-wikitext contentmodel
* such as Modulespace Lua or user JavaScript/CSS.
*/
this.setLookupNonRedirectCreator = function(flag) {
ctx.lookupNonRedirectCreator = flag;
};
// Move-related setter functions
/** @param {string} destination */
this.setMoveDestination = function(destination) {
ctx.moveDestination = destination;
};
/** @param {boolean} flag */
this.setMoveTalkPage = function(flag) {
ctx.moveTalkPage = !!flag;
};
/** @param {boolean} flag */
this.setMoveSubpages = function(flag) {
ctx.moveSubpages = !!flag;
};
/** @param {boolean} flag */
this.setMoveSuppressRedirect = function(flag) {
ctx.moveSuppressRedirect = !!flag;
};
// Protect-related setter functions
/**
* @param {string} level - The right required for the specific action
* e.g. autoconfirmed, sysop, templateeditor, extendedconfirmed
* (enWiki-only).
* @param {string} [expiry=infinity]
*/
this.setEditProtection = function(level, expiry) {
ctx.protectEdit = { level: level, expiry: expiry || 'infinity' };
};
this.setMoveProtection = function(level, expiry) {
ctx.protectMove = { level: level, expiry: expiry || 'infinity' };
};
this.setCreateProtection = function(level, expiry) {
ctx.protectCreate = { level: level, expiry: expiry || 'infinity' };
};
this.setCascadingProtection = function(flag) {
ctx.protectCascade = !!flag;
};
this.suppressProtectWarning = function() {
ctx.suppressProtectWarning = true;
};
// Delete-related setter
/** @param {boolean} flag */
this.setDeleteTalkPage = function(flag) {
ctx.deleteTalkPage = !!flag;
};
// Undelete-related setter
/** @param {boolean} flag */
this.setUndeleteTalkPage = function(flag) {
ctx.undeleteTalkPage = !!flag;
};
// Revert-related getters/setters:
this.setOldID = function(oldID) {
ctx.revertOldID = oldID;
};
/** @return {string} The current revision ID of the page */
this.getCurrentID = function() {
return ctx.revertCurID;
};
/** @return {string} Last editor of the page */
this.getRevisionUser = function() {
return ctx.revertUser;
};
/** @return {string} ISO 8601 timestamp at which the page was last edited. */
this.getLastEditTime = function() {
return ctx.lastEditTime;
};
// Miscellaneous getters/setters:
/**
* Define an object for use in a callback function.
*
* `callbackParameters` is for use by the caller only. The parameters
* allow a caller to pass the proper context into its callback
* function. Callers must ensure that any changes to the
* callbackParameters object within a `load()` callback still permit a
* proper re-entry into the `load()` callback if an edit conflict is
* detected upon calling `save()`.
*
* @param {Object} callbackParameters
*/
this.setCallbackParameters = function(callbackParameters) {
ctx.callbackParameters = callbackParameters;
};
/**
* @return {Object} - The object previously set by `setCallbackParameters()`.
*/
this.getCallbackParameters = function() {
return ctx.callbackParameters;
};
/**
* @param {Morebits.Status} statusElement
*/
this.setStatusElement = function(statusElement) {
ctx.statusElement = statusElement;
};
/**
* @return {Morebits.Status} Status element created by the constructor.
*/
this.getStatusElement = function() {
return ctx.statusElement;
};
/**
* @param {string} level - The right required for edits not to require
* review. Possible options: none, autoconfirmed, review (not on enWiki).
* @param {string} [expiry=infinity]
*/
this.setFlaggedRevs = function(level, expiry) {
ctx.flaggedRevs = { level: level, expiry: expiry || 'infinity' };
};
/**
* @return {boolean} True if the page existed on the wiki when it was last loaded.
*/
this.exists = function() {
return ctx.pageExists;
};
/**
* @return {string} Page ID of the page loaded. 0 if the page doesn't
* exist.
*/
this.getPageID = function() {
return ctx.pageID;
};
/**
* @return {string} - Content model of the page. Possible values
* include (but may not be limited to): `wikitext`, `javascript`,
* `css`, `json`, `Scribunto`, `sanitized-css`, `MassMessageListContent`.
* Also gettable via `mw.config.get('wgPageContentModel')`.
*/
this.getContentModel = function() {
return ctx.contentModel;
};
/**
* @return {boolean|string} - Watched status of the page. Boolean
* unless it's being watched temporarily, in which case returns the
* expiry string.
*/
this.getWatched = function() {
return ctx.watched;
};
/**
* @return {string} ISO 8601 timestamp at which the page was last loaded.
*/
this.getLoadTime = function() {
return ctx.loadTime;
};
/**
* @return {string} The user who created the page following `lookupCreation()`.
*/
this.getCreator = function() {
return ctx.creator;
};
/**
* @return {string} The ISOString timestamp of page creation following `lookupCreation()`.
*/
this.getCreationTimestamp = function() {
return ctx.timestamp;
};
/** @return {boolean} whether or not you can edit the page */
this.canEdit = function() {
return !!ctx.testActions && ctx.testActions.includes('edit');
};
/**
* Retrieves the username of the user who created the page as well as
* the timestamp of creation. The username can be retrieved using the
* `getCreator()` function; the timestamp can be retrieved using the
* `getCreationTimestamp()` function.
* Prior to June 2019 known as `lookupCreator()`.
*
* @param {Function} onSuccess - Callback function to be called when
* the username and timestamp are found within the callback.
* @param {Function} [onFailure] - Callback function to be called when
* the lookup fails
*/
this.lookupCreation = function(onSuccess, onFailure) {
ctx.onLookupCreationSuccess = onSuccess;
ctx.onLookupCreationFailure = onFailure || emptyFunction;
if (!onSuccess) {
ctx.statusElement.error('Internal error: no onSuccess callback provided to lookupCreation()!');
ctx.onLookupCreationFailure(this);
return;
}
const query = {
action: 'query',
prop: 'revisions',
titles: ctx.pageName,
rvlimit: 1,
rvprop: 'user|timestamp',
rvdir: 'newer',
format: 'json'
};
// Only the wikitext content model can reliably handle
// rvsection, others return an error when paired with the
// content rvprop. Relatedly, non-wikitext models don't
// understand the #REDIRECT concept, so we shouldn't attempt
// the redirect resolution in fnLookupCreationSuccess
if (ctx.lookupNonRedirectCreator) {
query.rvsection = 0;
query.rvprop += '|content';
}
if (ctx.followRedirect) {
query.redirects = ''; // follow all redirects
}
ctx.lookupCreationApi = new Morebits.wiki.Api('ページ作成情報の取得中', query, fnLookupCreationSuccess, ctx.statusElement, ctx.onLookupCreationFailure);
ctx.lookupCreationApi.setParent(this);
ctx.lookupCreationApi.post();
};
/**
* Reverts a page to `revertOldID` set by `setOldID`.
*
* @param {Function} [onSuccess] - Callback function to run on success.
* @param {Function} [onFailure] - Callback function to run on failure.
*/
this.revert = function(onSuccess, onFailure) {
ctx.onSaveSuccess = onSuccess;
ctx.onSaveFailure = onFailure || emptyFunction;
if (!ctx.revertOldID) {
ctx.statusElement.error('Internal error: revision ID to revert to was not set before revert!');
ctx.onSaveFailure(this);
return;
}
ctx.editMode = 'revert';
this.load(fnAutoSave, ctx.onSaveFailure);
};
/**
* Moves a page to another title.
*
* @param {Function} [onSuccess] - Callback function to run on success.
* @param {Function} [onFailure] - Callback function to run on failure.
*/
this.move = function(onSuccess, onFailure) {
ctx.onMoveSuccess = onSuccess;
ctx.onMoveFailure = onFailure || emptyFunction;
if (!fnPreflightChecks.call(this, 'move', ctx.onMoveFailure)) {
return; // abort
}
if (!ctx.moveDestination) {
ctx.statusElement.error('Internal error: destination page name was not set before move!');
ctx.onMoveFailure(this);
return;
}
if (fnCanUseMwUserToken('move')) {
fnProcessMove.call(this, this);
} else {
const query = fnNeedTokenInfoQuery('move');
ctx.moveApi = new Morebits.wiki.Api('トークンの取得中...', query, fnProcessMove, ctx.statusElement, ctx.onMoveFailure);
ctx.moveApi.setParent(this);
ctx.moveApi.post();
}
};
/**
* Marks the page as patrolled, using `rcid` (if available) or `revid`.
*
* Patrolling as such doesn't need to rely on loading the page in
* question; simply passing a revid to the API is sufficient, so in
* those cases just using {@link Morebits.wiki.Api} is probably preferable.
*
* No error handling since we don't actually care about the errors.
*/
this.patrol = function() {
if (!Morebits.userIsSysop && !Morebits.userIsInGroup('patroller')) {
return;
}
// If a link is present, don't need to check if it's patrolled
if ($('.patrollink').length) {
const patrolhref = $('.patrollink a').attr('href');
ctx.rcid = mw.util.getParamValue('rcid', patrolhref);
fnProcessPatrol(this, this);
} else {
const patrolQuery = {
action: 'query',
prop: 'info',
meta: 'tokens',
type: 'patrol', // as long as we're querying, might as well get a token
list: 'recentchanges', // check if the page is unpatrolled
titles: ctx.pageName,
rcprop: 'patrolled',
rctitle: ctx.pageName,
rclimit: 1,
format: 'json'
};
ctx.patrolApi = new Morebits.wiki.Api('トークンの取得中...', patrolQuery, fnProcessPatrol);
ctx.patrolApi.setParent(this);
ctx.patrolApi.post();
}
};
/**
* Marks the page as reviewed by the PageTriage extension.
*
* Will, by it's nature, mark as patrolled as well. Falls back to
* patrolling if not in an appropriate namespace.
*
* Doesn't inherently rely on loading the page in question; simply
* passing a `pageid` to the API is sufficient, so in those cases just
* using {@link Morebits.wiki.Api} is probably preferable.
*
* Will first check if the page is queued via
* {@link Morebits.wiki.Page~fnProcessTriageList|fnProcessTriageList}.
*
* No error handling since we don't actually care about the errors.
*
* @see {@link https://www.mediawiki.org/wiki/Extension:PageTriage} Referred to as "review" on-wiki.
*/
this.triage = function() {
// Fall back to patrol if not a valid triage namespace
if (!mw.config.get('pageTriageNamespaces').includes(new mw.Title(ctx.pageName).getNamespaceId())) {
this.patrol();
} else {
if (!Morebits.userIsSysop && !Morebits.userIsInGroup('patroller')) {
return;
}
// If on the page in question, don't need to query for page ID
if (new mw.Title(Morebits.pageNameNorm).getPrefixedText() === new mw.Title(ctx.pageName).getPrefixedText()) {
ctx.pageID = mw.config.get('wgArticleId');
fnProcessTriageList(this, this);
} else {
const query = fnNeedTokenInfoQuery('triage');
ctx.triageApi = new Morebits.wiki.Api('トークンの取得中...', query, fnProcessTriageList);
ctx.triageApi.setParent(this);
ctx.triageApi.post();
}
}
};
// |delete| is a reserved word in some flavours of JS
/**
* Deletes a page (for admins only).
*
* @param {Function} [onSuccess] - Callback function to run on success.
* @param {Function} [onFailure] - Callback function to run on failure.
*/
this.deletePage = function(onSuccess, onFailure) {
ctx.onDeleteSuccess = onSuccess;
ctx.onDeleteFailure = onFailure || emptyFunction;
if (!fnPreflightChecks.call(this, 'delete', ctx.onDeleteFailure)) {
return; // abort
}
if (fnCanUseMwUserToken('delete')) {
fnProcessDelete.call(this, this);
} else {
const query = fnNeedTokenInfoQuery('delete');
ctx.deleteApi = new Morebits.wiki.Api('トークンの取得中...', query, fnProcessDelete, ctx.statusElement, ctx.onDeleteFailure);
ctx.deleteApi.setParent(this);
ctx.deleteApi.post();
}
};
/**
* Undeletes a page (for admins only).
*
* @param {Function} [onSuccess] - Callback function to run on success.
* @param {Function} [onFailure] - Callback function to run on failure.
*/
this.undeletePage = function(onSuccess, onFailure) {
ctx.onUndeleteSuccess = onSuccess;
ctx.onUndeleteFailure = onFailure || emptyFunction;
if (!fnPreflightChecks.call(this, 'undelete', ctx.onUndeleteFailure)) {
return; // abort
}
if (fnCanUseMwUserToken('undelete')) {
fnProcessUndelete.call(this, this);
} else {
const query = fnNeedTokenInfoQuery('undelete');
ctx.undeleteApi = new Morebits.wiki.Api('トークンの取得中...', query, fnProcessUndelete, ctx.statusElement, ctx.onUndeleteFailure);
ctx.undeleteApi.setParent(this);
ctx.undeleteApi.post();
}
};
/**
* Protects a page (for admins only).
*
* @param {Function} [onSuccess] - Callback function to run on success.
* @param {Function} [onFailure] - Callback function to run on failure.
*/
this.protect = function(onSuccess, onFailure) {
ctx.onProtectSuccess = onSuccess;
ctx.onProtectFailure = onFailure || emptyFunction;
if (!fnPreflightChecks.call(this, 'protect', ctx.onProtectFailure)) {
return; // abort
}
if (!ctx.protectEdit && !ctx.protectMove && !ctx.protectCreate) {
ctx.statusElement.error('Internal error: you must set edit and/or move and/or create protection before calling protect()!');
ctx.onProtectFailure(this);
return;
}
// because of the way MW API interprets protection levels
// (absolute, not differential), we always need to request
// protection levels from the server
const query = fnNeedTokenInfoQuery('protect');
ctx.protectApi = new Morebits.wiki.Api('トークンの取得中...', query, fnProcessProtect, ctx.statusElement, ctx.onProtectFailure);
ctx.protectApi.setParent(this);
ctx.protectApi.post();
};
/**
* Apply FlaggedRevs protection settings. Only works on wikis where
* the extension is installed (`$wgFlaggedRevsProtection = true`
* i.e. where FlaggedRevs settings appear on the "protect" tab).
*
* @see {@link https://www.mediawiki.org/wiki/Extension:FlaggedRevs}
* Referred to as "pending changes" on-wiki.
*
* @param {Function} [onSuccess]
* @param {Function} [onFailure]
*/
this.stabilize = function(onSuccess, onFailure) {
ctx.onStabilizeSuccess = onSuccess;
ctx.onStabilizeFailure = onFailure || emptyFunction;
if (!fnPreflightChecks.call(this, 'FlaggedRevs', ctx.onStabilizeFailure)) {
return; // abort
}
if (!ctx.flaggedRevs) {
ctx.statusElement.error('Internal error: you must set flaggedRevs before calling stabilize()!');
ctx.onStabilizeFailure(this);
return;
}
if (fnCanUseMwUserToken('stabilize')) {
fnProcessStabilize.call(this, this);
} else {
const query = fnNeedTokenInfoQuery('stabilize');
ctx.stabilizeApi = new Morebits.wiki.Api('トークンの取得中...', query, fnProcessStabilize, ctx.statusElement, ctx.onStabilizeFailure);
ctx.stabilizeApi.setParent(this);
ctx.stabilizeApi.post();
}
};
/*
* Private member functions
* These are not exposed outside
*/
/**
* Determines whether we can save an API call by using the csrf token
* sent with the page HTML, or whether we need to ask the server for
* more info (e.g. protection or watchlist expiry).
*
* Currently used for `append`, `prepend`, `newSection`, `move`,
* `stabilize`, `deletePage`, and `undeletePage`. Not used for
* `protect` since it always needs to request protection status.
*
* @param {string} [action=edit] - The action being undertaken, e.g.
* "edit" or "delete". In practice, only "edit" or "notedit" matters.
* @return {boolean}
*/
var fnCanUseMwUserToken = function(action = 'edit') {
// If a watchlist expiry is set, we must always load the page
// to avoid overwriting indefinite protection. Of course, not
// needed if setting indefinite watching!
if (ctx.watchlistExpiry && !Morebits.string.isInfinity(ctx.watchlistExpiry)) {
return false;
}
// API-based redirect resolution only works for action=query and
// action=edit in append/prepend/new modes
if (ctx.followRedirect) {
if (!ctx.followCrossNsRedirect) {
return false; // must load the page to check for cross namespace redirects
}
if (action !== 'edit' || (ctx.editMode === 'all' || ctx.editMode === 'revert')) {
return false;
}
}
// do we need to fetch the edit protection expiry?
if (Morebits.userIsSysop && !ctx.suppressProtectWarning) {
if (new mw.Title(Morebits.pageNameNorm).getPrefixedText() !== new mw.Title(ctx.pageName).getPrefixedText()) {
return false;
}
// wgRestrictionEdit is null on non-existent pages,
// so this neatly handles nonexistent pages
const editRestriction = mw.config.get('wgRestrictionEdit');
if (!editRestriction || editRestriction.includes('sysop')) {
return false;
}
}
return !!mw.user.tokens.get('csrfToken');
};
/**
* When functions can't use
* {@link Morebits.wiki.Page~fnCanUseMwUserToken|fnCanUseMwUserToken}
* or require checking protection or watched status, maintain the query
* in one place. Used for {@link Morebits.wiki.Page#deletePage|delete},
* {@link Morebits.wiki.Page#undeletePage|undelete},
* {@link Morebits.wiki.Page#protect|protect},
* {@link Morebits.wiki.Page#stabilize|stabilize},
* and {@link Morebits.wiki.Page#move|move}
* (basically, just not {@link Morebits.wiki.Page#load|load}).
*
* @param {string} action - The action being undertaken, e.g. "edit" or
* "delete".
* @return {Object} Appropriate query.
*/
var fnNeedTokenInfoQuery = function(action) {
const query = {
action: 'query',
meta: 'tokens',
type: 'csrf',
titles: ctx.pageName,
prop: 'info',
inprop: 'watched',
format: 'json'
};
// Protection not checked for flagged-revs or non-sysop moves
if (action !== 'stabilize' && (action !== 'move' || Morebits.userIsSysop)) {
query.inprop += '|protection';
}
if (ctx.followRedirect && action !== 'undelete') {
query.redirects = ''; // follow all redirects
}
return query;
};
// callback from loadSuccess() for append(), prepend(), and newSection() threads
var fnAutoSave = function(pageobj) {
pageobj.save(ctx.onSaveSuccess, ctx.onSaveFailure);
};
// callback from loadApi.post()
var fnLoadSuccess = function() {
const response = ctx.loadApi.getResponse().query;
if (!fnCheckPageName(response, ctx.onLoadFailure)) {
return; // abort
}
const page = response.pages[0];
let rev;
ctx.pageExists = !page.missing;
if (ctx.pageExists) {
rev = page.revisions[0];
ctx.lastEditTime = rev.timestamp;
ctx.pageText = rev.content;
ctx.pageID = page.pageid;
} else {
ctx.pageText = ''; // allow for concatenation, etc.
ctx.pageID = 0; // nonexistent in response, matches wgArticleId
}
ctx.csrfToken = response.tokens.csrftoken;
if (!ctx.csrfToken) {
ctx.statusElement.error('編集トークンの取得に失敗しました。');
ctx.onLoadFailure(this);
return;
}
ctx.loadTime = ctx.loadApi.getResponse().curtimestamp;
if (!ctx.loadTime) {
ctx.statusElement.error('現在のタイムスタンプの取得に失敗しました。');
ctx.onLoadFailure(this);
return;
}
ctx.contentModel = page.contentmodel;
ctx.watched = page.watchlistexpiry || page.watched;
// extract protection info, to alert admins when they are about to edit a protected page
// Includes cascading protection
if (Morebits.userIsSysop) {
const editProt = page.protection.filter((pr) => pr.type === 'edit' && pr.level === 'sysop').pop();
if (editProt) {
ctx.fullyProtected = editProt.expiry;
} else {
ctx.fullyProtected = false;
}
}
ctx.revertCurID = page.lastrevid;
const testactions = page.actions;
ctx.testActions = []; // was null
Object.keys(testactions).forEach((action) => {
if (testactions[action]) {
ctx.testActions.push(action);
}
});
if (ctx.editMode === 'revert') {
ctx.revertCurID = rev && rev.revid;
if (!ctx.revertCurID) {
ctx.statusElement.error('Failed to retrieve current revision ID.');
ctx.onLoadFailure(this);
return;
}
ctx.revertUser = rev && rev.user;
if (!ctx.revertUser) {
if (rev && rev.userhidden) { // username was RevDel'd or oversighted
ctx.revertUser = '<username hidden>';
} else {
ctx.statusElement.error('Failed to retrieve user who made the revision.');
ctx.onLoadFailure(this);
return;
}
}
// set revert edit summary
ctx.editSummary = '[[Help:Revert|Reverted]] to revision ' + ctx.revertOldID + ' by ' + ctx.revertUser + ': ' + ctx.editSummary;
}
ctx.pageLoaded = true;
// alert("Generate edit conflict now"); // for testing edit conflict recovery logic
ctx.onLoadSuccess(this); // invoke callback
};
// callback from parseApi.post()
var fnParseSuccess = function() {
if (ctx.parseApi.getResponse().error) {
ctx.statusElement.error('ページの取得に失敗しました。');
ctx.onParseFailure(this);
return;
}
var response = ctx.parseApi.getResponse().parse;
ctx.pageParsed = true;
ctx.sections = response.sections;
// alert("Generate edit conflict now"); // for testing edit conflict recovery logic
ctx.onParseSuccess(this); // invoke callback
};
// helper function to parse the page name returned from the API
var fnCheckPageName = function(response, onFailure) {
if (!onFailure) {
onFailure = emptyFunction;
}
const page = response.pages && response.pages[0];
if (page) {
// check for invalid titles
if (page.invalid) {
ctx.statusElement.error('無効なページタイトル: ' + ctx.pageName);
onFailure(this);
return false; // abort
}
// retrieve actual title of the page after normalization and redirects
const resolvedName = page.title;
if (response.redirects) {
// check for cross-namespace redirect:
const origNs = new mw.Title(ctx.pageName).namespace;
const newNs = new mw.Title(resolvedName).namespace;
if (origNs !== newNs && !ctx.followCrossNsRedirect) {
ctx.statusElement.error(ctx.pageName + 'は' + resolvedName + 'への名前空間が違うリダイレクトです。中止します。');
onFailure(this);
return false;
}
// only notify user for redirects, not normalization
new Morebits.Status('Note', ctx.pageName + 'から' + resolvedName + 'へのリダイレクト');
}
ctx.pageName = resolvedName; // update to redirect target or normalized name
} else {
// could be a circular redirect or other problem
ctx.statusElement.error('Could not resolve redirects for: ' + ctx.pageName);
onFailure(this);
// force error to stay on the screen
++Morebits.wiki.numberOfActionsLeft;
return false; // abort
}
return true; // all OK
};
/**
* Determine whether we should provide a watchlist expiry. Will not
* do so if the page is currently permanently watched, or the current
* expiry is *after* the new, provided expiry. Only handles strings
* recognized by {@link Morebits.Date} or relative timeframes with
* unit it can process. Relies on the fact that fnCanUseMwUserToken
* requires page loading if a watchlistexpiry is provided, so we are
* ensured of knowing the watch status by the use of this.
*
* @return {boolean}
*/
var fnApplyWatchlistExpiry = function() {
if (ctx.watchlistExpiry) {
if (!ctx.watched || Morebits.string.isInfinity(ctx.watchlistExpiry)) {
return true;
} else if (typeof ctx.watched === 'string') {
let newExpiry;
// Attempt to determine if the new expiry is a
// relative (e.g. `1 month`) or absolute datetime
const rel = ctx.watchlistExpiry.split(' ');
try {
newExpiry = new Morebits.Date().add(rel[0], rel[1]);
} catch (e) {
newExpiry = new Morebits.Date(ctx.watchlistExpiry);
}
// If the date is valid, only use it if it extends the current expiry
if (newExpiry.isValid()) {
if (newExpiry.isAfter(new Morebits.Date(ctx.watched))) {
return true;
}
} else {
// If it's still not valid, hope it's a valid MW expiry format that
// Morebits.Date doesn't recognize, so just default to using it.
// This will also include minor typos.
return true;
}
}
}
return false;
};
// callback from saveApi.post()
var fnSaveSuccess = function() {
ctx.editMode = 'all'; // cancel append/prepend/newSection/revert modes
const response = ctx.saveApi.getResponse();
// see if the API thinks we were successful
if (response.edit.result === 'Success') {
// real success
// default on success action - display link for edited page
const link = document.createElement('a');
link.setAttribute('href', mw.util.getUrl(ctx.pageName));
link.appendChild(document.createTextNode(ctx.pageName));
ctx.statusElement.info(['完了 (', link, ')']);
if (ctx.onSaveSuccess) {
ctx.onSaveSuccess(this); // invoke callback
}
return;
}
// errors here are only generated by extensions which hook APIEditBeforeSave within MediaWiki,
// which as of 1.34.0-wmf.23 (Sept 2019) should only encompass captcha messages
if (response.edit.captcha) {
ctx.statusElement.error('ウィキのサーバーがキャプチャを入力するよう要求したため、ページを保存できませんでした。');
} else {
ctx.statusElement.error('ページの保存中にAPIから不明なエラーを受信');
}
// force error to stay on the screen
++Morebits.wiki.numberOfActionsLeft;
ctx.onSaveFailure(this);
};
// callback from saveApi.post()
var fnSaveError = function() {
const errorCode = ctx.saveApi.getErrorCode();
// check for edit conflict
if (errorCode === 'editconflict' && ctx.conflictRetries++ < ctx.maxConflictRetries) {
// edit conflicts can occur when the page needs to be purged from the server cache
const purgeQuery = {
action: 'purge',
titles: ctx.pageName // redirects are already resolved
};
var purgeApi = new Morebits.wiki.Api('編集の競合が検出されたため、サーバーのキャッシュを破棄します。', purgeQuery, (() => {
--Morebits.wiki.numberOfActionsLeft; // allow for normal completion if retry succeeds
ctx.statusElement.info('編集の競合が検出されたため、編集をやり直します');
if (fnCanUseMwUserToken('edit')) {
ctx.saveApi.post(); // necessarily append, prepend, or newSection, so this should work as desired
} else {
ctx.loadApi.post(); // reload the page and reapply the edit
}
}), ctx.statusElement);
purgeApi.post();
// check for network or server error
} else if ((errorCode === null || errorCode === undefined) && ctx.retries++ < ctx.maxRetries) {
// the error might be transient, so try again
ctx.statusElement.info('保存に失敗しました。2秒後に再試行します ...');
--Morebits.wiki.numberOfActionsLeft; // allow for normal completion if retry succeeds
// wait for sometime for client to regain connectivity
sleep(2000).then(() => {
ctx.saveApi.post(); // give it another go!
});
// hard error, give up
} else {
const response = ctx.saveApi.getResponse();
const errorData = response.error || // bc error format
response.errors[0].data; // html/wikitext/plaintext error format
switch (errorCode) {
case 'protectedpage':
// non-admin attempting to edit a protected page - this gives a friendlier message than the default
ctx.statusElement.error('Failed to save edit: Page is protected');
break;
case 'abusefilter-disallowed':
ctx.statusElement.error('The edit was disallowed by the edit filter: "' + errorData.abusefilter.description + '".');
break;
case 'abusefilter-warning':
ctx.statusElement.error(['A warning was returned by the edit filter: "', errorData.abusefilter.description, '". If you wish to proceed with the edit, please carry it out again. This warning will not appear a second time.']);
// We should provide the user with a way to automatically retry the action if they so choose -
// I can't see how to do this without creating a UI dependency on Morebits.wiki.Page though -- TTO
break;
case 'spamblacklist':
// If multiple items are blacklisted, we only return the first
var spam = errorData.spamblacklist.matches[0];
ctx.statusElement.error('Could not save the page because the URL ' + spam + ' is on the spam blacklist');
break;
default:
ctx.statusElement.error('Failed to save edit: ' + ctx.saveApi.getErrorText());
}
ctx.editMode = 'all'; // cancel append/prepend/newSection/revert modes
if (ctx.onSaveFailure) {
ctx.onSaveFailure(this); // invoke callback
}
}
};
const isTextRedirect = function(text) {
if (!text) { // no text - content empty or inaccessible (revdelled or suppressed)
return false;
}
return Morebits.l10n.redirectTagAliases.some((tag) => new RegExp('^\\s*' + tag + '\\W', 'i').test(text));
};
var fnLookupCreationSuccess = function() {
const response = ctx.lookupCreationApi.getResponse().query;
if (!fnCheckPageName(response, ctx.onLookupCreationFailure)) {
return; // abort
}
const rev = response.pages[0].revisions && response.pages[0].revisions[0];
if (!rev) {
ctx.statusElement.error('Could not find any revisions of ' + ctx.pageName);
ctx.onLookupCreationFailure(this);
return;
}
if (!ctx.lookupNonRedirectCreator || !isTextRedirect(rev.content)) {
ctx.creator = rev.user;
if (!ctx.creator) {
ctx.statusElement.error('Could not find name of page creator');
ctx.onLookupCreationFailure(this);
return;
}
ctx.timestamp = rev.timestamp;
if (!ctx.timestamp) {
ctx.statusElement.error('Could not find timestamp of page creation');
ctx.onLookupCreationFailure(this);
return;
}
ctx.statusElement.info('retrieved page creation information');
ctx.onLookupCreationSuccess(this);
} else {
ctx.lookupCreationApi.query.rvlimit = 50; // modify previous query to fetch more revisions
ctx.lookupCreationApi.query.titles = ctx.pageName; // update pageName if redirect resolution took place in earlier query
ctx.lookupCreationApi = new Morebits.wiki.Api('ページ作成情報の取得中', ctx.lookupCreationApi.query, fnLookupNonRedirectCreator, ctx.statusElement, ctx.onLookupCreationFailure);
ctx.lookupCreationApi.setParent(this);
ctx.lookupCreationApi.post();
}
};
var fnLookupNonRedirectCreator = function() {
const response = ctx.lookupCreationApi.getResponse().query;
const revs = response.pages[0].revisions;
for (let i = 0; i < revs.length; i++) {
if (!isTextRedirect(revs[i].content)) {
ctx.creator = revs[i].user;
ctx.timestamp = revs[i].timestamp;
break;
}
}
if (!ctx.creator) {
// fallback to give first revision author if no non-redirect version in the first 50
ctx.creator = revs[0].user;
ctx.timestamp = revs[0].timestamp;
if (!ctx.creator) {
ctx.statusElement.error('Could not find name of page creator');
ctx.onLookupCreationFailure(this);
return;
}
}
if (!ctx.timestamp) {
ctx.statusElement.error('Could not find timestamp of page creation');
ctx.onLookupCreationFailure(this);
return;
}
ctx.statusElement.info('retrieved page creation information');
ctx.onLookupCreationSuccess(this);
};
/**
* Common checks for action methods. Used for move, undelete, delete,
* protect, stabilize.
*
* @param {string} action - The action being checked.
* @param {string} onFailure - Failure callback.
* @return {boolean}
*/
var fnPreflightChecks = function(action, onFailure) {
// if a non-admin tries to do this, don't bother
if (!Morebits.userIsSysop && action !== 'move') {
ctx.statusElement.error('Cannot ' + action + 'page : only admins can do that');
onFailure(this);
return false;
}
if (!ctx.editSummary) {
ctx.statusElement.error('Internal error: ' + action + ' reason not set (use setEditSummary function)!');
onFailure(this);
return false;
}
return true; // all OK
};
/**
* Common checks for fnProcess functions (`fnProcessDelete`, `fnProcessMove`, etc.
* Used for move, undelete, delete, protect, stabilize.
*
* @param {string} action - The action being checked.
* @param {string} onFailure - Failure callback.
* @param {string} response - The response document from the API call.
* @return {boolean}
*/
const fnProcessChecks = function(action, onFailure, response) {
const missing = response.pages[0].missing;
// No undelete as an existing page could have deleted revisions
const actionMissing = missing && ['delete', 'stabilize', 'move'].includes(action);
const protectMissing = action === 'protect' && missing && (ctx.protectEdit || ctx.protectMove);
const saltMissing = action === 'protect' && !missing && ctx.protectCreate;
if (actionMissing || protectMissing || saltMissing) {
ctx.statusElement.error('Cannot ' + action + ' the page because it ' + (missing ? 'no longer' : 'already') + ' exists');
onFailure(this);
return false;
}
// Delete, undelete, move
// extract protection info
let editprot;
if (action === 'undelete') {
editprot = response.pages[0].protection.filter((pr) => pr.type === 'create' && pr.level === 'sysop').pop();
} else if (action === 'delete' || action === 'move') {
editprot = response.pages[0].protection.filter((pr) => pr.type === 'edit' && pr.level === 'sysop').pop();
}
if (editprot && !ctx.suppressProtectWarning &&
!confirm('You are about to ' + action + ' the fully protected page "' + ctx.pageName +
(editprot.expiry === 'infinity' ? '" (protected indefinitely)' : '" (protection expiring ' + new Morebits.Date(editprot.expiry).calendar('utc') + ' (UTC))') +
'. \n\nClick OK to proceed with ' + action + ', or Cancel to skip.')) {
ctx.statusElement.error('Aborted ' + action + ' on fully protected page.');
onFailure(this);
return false;
}
if (!response.tokens.csrftoken) {
ctx.statusElement.error('Failed to retrieve token.');
onFailure(this);
return false;
}
return true; // all OK
};
var fnProcessMove = function() {
let pageTitle, token;
if (fnCanUseMwUserToken('move')) {
token = mw.user.tokens.get('csrfToken');
pageTitle = ctx.pageName;
} else {
const response = ctx.moveApi.getResponse().query;
if (!fnProcessChecks('move', ctx.onMoveFailure, response)) {
return; // abort
}
token = response.tokens.csrftoken;
const page = response.pages[0];
pageTitle = page.title;
ctx.watched = page.watchlistexpiry || page.watched;
}
const query = {
action: 'move',
from: pageTitle,
to: ctx.moveDestination,
token: token,
reason: ctx.editSummary,
watchlist: ctx.watchlistOption,
format: 'json'
};
if (ctx.changeTags) {
query.tags = ctx.changeTags;
}
if (fnApplyWatchlistExpiry()) {
query.watchlistexpiry = ctx.watchlistExpiry;
}
if (ctx.moveTalkPage) {
query.movetalk = 'true';
}
if (ctx.moveSubpages) {
query.movesubpages = 'true';
}
if (ctx.moveSuppressRedirect) {
query.noredirect = 'true';
}
ctx.moveProcessApi = new Morebits.wiki.Api('ページの移動...', query, ctx.onMoveSuccess, ctx.statusElement, ctx.onMoveFailure);
ctx.moveProcessApi.setParent(this);
ctx.moveProcessApi.post();
};
var fnProcessPatrol = function() {
const query = {
action: 'patrol',
format: 'json'
};
// Didn't need to load the page
if (ctx.rcid) {
query.rcid = ctx.rcid;
query.token = mw.user.tokens.get('patrolToken');
} else {
const response = ctx.patrolApi.getResponse().query;
// Don't patrol if not unpatrolled
if (!response.recentchanges[0].unpatrolled) {
return;
}
const lastrevid = response.pages[0].lastrevid;
if (!lastrevid) {
return;
}
query.revid = lastrevid;
const token = response.tokens.csrftoken;
if (!token) {
return;
}
query.token = token;
}
if (ctx.changeTags) {
query.tags = ctx.changeTags;
}
const patrolStat = new Morebits.Status('Marking page as patrolled');
ctx.patrolProcessApi = new Morebits.wiki.Api('patrolling page...', query, null, patrolStat);
ctx.patrolProcessApi.setParent(this);
ctx.patrolProcessApi.post();
};
// Ensure that the page is curatable
var fnProcessTriageList = function() {
if (ctx.pageID) {
ctx.csrfToken = mw.user.tokens.get('csrfToken');
} else {
const response = ctx.triageApi.getResponse().query;
ctx.pageID = response.pages[0].pageid;
if (!ctx.pageID) {
return;
}
ctx.csrfToken = response.tokens.csrftoken;
if (!ctx.csrfToken) {
return;
}
}
const query = {
action: 'pagetriagelist',
page_id: ctx.pageID,
format: 'json'
};
ctx.triageProcessListApi = new Morebits.wiki.Api('checking curation status...', query, fnProcessTriage);
ctx.triageProcessListApi.setParent(this);
ctx.triageProcessListApi.post();
};
// callback from triageProcessListApi.post()
var fnProcessTriage = function() {
const responseList = ctx.triageProcessListApi.getResponse().pagetriagelist;
// Exit if not in the queue
if (!responseList || responseList.result !== 'success') {
return;
}
const page = responseList.pages && responseList.pages[0];
// Do nothing if page already triaged/patrolled
if (!page || !parseInt(page.patrol_status, 10)) {
const query = {
action: 'pagetriageaction',
pageid: ctx.pageID,
reviewed: 1,
token: ctx.csrfToken,
format: 'json'
};
if (ctx.changeTags) {
query.tags = ctx.changeTags;
}
const triageStat = new Morebits.Status('Marking page as curated');
ctx.triageProcessApi = new Morebits.wiki.Api('curating page...', query, null, triageStat);
ctx.triageProcessApi.setParent(this);
ctx.triageProcessApi.post();
}
};
var fnProcessDelete = function() {
let pageTitle, token;
if (fnCanUseMwUserToken('delete')) {
token = mw.user.tokens.get('csrfToken');
pageTitle = ctx.pageName;
} else {
const response = ctx.deleteApi.getResponse().query;
if (!fnProcessChecks('delete', ctx.onDeleteFailure, response)) {
return; // abort
}
token = response.tokens.csrftoken;
const page = response.pages[0];
pageTitle = page.title;
ctx.watched = page.watchlistexpiry || page.watched;
}
const query = {
action: 'delete',
title: pageTitle,
token: token,
reason: ctx.editSummary,
watchlist: ctx.watchlistOption,
format: 'json'
};
if (ctx.changeTags) {
query.tags = ctx.changeTags;
}
if (ctx.deleteTalkPage) {
query.deletetalk = 'true';
}
if (fnApplyWatchlistExpiry()) {
query.watchlistexpiry = ctx.watchlistExpiry;
}
ctx.deleteProcessApi = new Morebits.wiki.Api('deleting page...', query, ctx.onDeleteSuccess, ctx.statusElement, fnProcessDeleteError);
ctx.deleteProcessApi.setParent(this);
ctx.deleteProcessApi.post();
};
// callback from deleteProcessApi.post()
var fnProcessDeleteError = function() {
const errorCode = ctx.deleteProcessApi.getErrorCode();
// check for "Database query error"
if (errorCode === 'internal_api_error_DBQueryError' && ctx.retries++ < ctx.maxRetries) {
ctx.statusElement.info('Database query error, retrying');
--Morebits.wiki.numberOfActionsLeft; // allow for normal completion if retry succeeds
ctx.deleteProcessApi.post(); // give it another go!
} else if (errorCode === 'missingtitle') {
ctx.statusElement.error('Cannot delete the page, because it no longer exists');
if (ctx.onDeleteFailure) {
ctx.onDeleteFailure.call(this, ctx.deleteProcessApi); // invoke callback
}
// hard error, give up
} else {
ctx.statusElement.error('Failed to delete the page: ' + ctx.deleteProcessApi.getErrorText());
if (ctx.onDeleteFailure) {
ctx.onDeleteFailure.call(this, ctx.deleteProcessApi); // invoke callback
}
}
};
var fnProcessUndelete = function() {
let pageTitle, token;
if (fnCanUseMwUserToken('undelete')) {
token = mw.user.tokens.get('csrfToken');
pageTitle = ctx.pageName;
} else {
const response = ctx.undeleteApi.getResponse().query;
if (!fnProcessChecks('undelete', ctx.onUndeleteFailure, response)) {
return; // abort
}
token = response.tokens.csrftoken;
const page = response.pages[0];
pageTitle = page.title;
ctx.watched = page.watchlistexpiry || page.watched;
}
const query = {
action: 'undelete',
title: pageTitle,
token: token,
reason: ctx.editSummary,
watchlist: ctx.watchlistOption,
format: 'json'
};
if (ctx.changeTags) {
query.tags = ctx.changeTags;
}
if (ctx.undeleteTalkPage) {
query.undeletetalk = 'true';
}
if (fnApplyWatchlistExpiry()) {
query.watchlistexpiry = ctx.watchlistExpiry;
}
ctx.undeleteProcessApi = new Morebits.wiki.Api('undeleting page...', query, ctx.onUndeleteSuccess, ctx.statusElement, fnProcessUndeleteError);
ctx.undeleteProcessApi.setParent(this);
ctx.undeleteProcessApi.post();
};
// callback from undeleteProcessApi.post()
var fnProcessUndeleteError = function() {
const errorCode = ctx.undeleteProcessApi.getErrorCode();
// check for "Database query error"
if (errorCode === 'internal_api_error_DBQueryError') {
if (ctx.retries++ < ctx.maxRetries) {
ctx.statusElement.info('Database query error, retrying');
--Morebits.wiki.numberOfActionsLeft; // allow for normal completion if retry succeeds
ctx.undeleteProcessApi.post(); // give it another go!
} else {
ctx.statusElement.error('Repeated database query error, please try again');
if (ctx.onUndeleteFailure) {
ctx.onUndeleteFailure.call(this, ctx.undeleteProcessApi); // invoke callback
}
}
} else if (errorCode === 'cantundelete') {
ctx.statusElement.error('Cannot undelete the page, either because there are no revisions to undelete or because it has already been undeleted');
if (ctx.onUndeleteFailure) {
ctx.onUndeleteFailure.call(this, ctx.undeleteProcessApi); // invoke callback
}
// hard error, give up
} else {
ctx.statusElement.error('Failed to undelete the page: ' + ctx.undeleteProcessApi.getErrorText());
if (ctx.onUndeleteFailure) {
ctx.onUndeleteFailure.call(this, ctx.undeleteProcessApi); // invoke callback
}
}
};
var fnProcessProtect = function() {
const response = ctx.protectApi.getResponse().query;
if (!fnProcessChecks('protect', ctx.onProtectFailure, response)) {
return; // abort
}
const token = response.tokens.csrftoken;
const page = response.pages[0];
const pageTitle = page.title;
ctx.watched = page.watchlistexpiry || page.watched;
// Fetch existing protection levels
const prs = response.pages[0].protection;
let editprot, moveprot, createprot;
prs.forEach((pr) => {
// Filter out protection from cascading
if (pr.type === 'edit' && !pr.source) {
editprot = pr;
} else if (pr.type === 'move') {
moveprot = pr;
} else if (pr.type === 'create') {
createprot = pr;
}
});
// Fall back to current levels if not explicitly set
if (!ctx.protectEdit && editprot) {
ctx.protectEdit = { level: editprot.level, expiry: editprot.expiry };
}
if (!ctx.protectMove && moveprot) {
ctx.protectMove = { level: moveprot.level, expiry: moveprot.expiry };
}
if (!ctx.protectCreate && createprot) {
ctx.protectCreate = { level: createprot.level, expiry: createprot.expiry };
}
// Default to pre-existing cascading protection if unchanged (similar to above)
if (ctx.protectCascade === null) {
ctx.protectCascade = !!prs.filter((pr) => pr.cascade).length;
}
// Warn if cascading protection being applied with an invalid protection level,
// which for edit protection will cause cascading to be silently stripped
if (ctx.protectCascade) {
// On move protection, this is technically stricter than the MW API,
// but seems reasonable to avoid dumb values and misleading log entries (T265626)
if (((!ctx.protectEdit || ctx.protectEdit.level !== 'sysop') ||
(!ctx.protectMove || ctx.protectMove.level !== 'sysop')) &&
!confirm('"' + ctx.pageName + '"ではカスケード保護が有効になっていますが、管理者レベルの保護を選択していません。\n\n' +
'OKをクリックすることで調節して管理者レベルのカスケード保護を続行するか、キャンセルをクリックしてこのアクションをスキップします。')) {
ctx.statusElement.error('カスケード保護は中止されました。');
ctx.onProtectFailure(this);
return;
}
ctx.protectEdit.level = 'sysop';
ctx.protectMove.level = 'sysop';
}
// Build protection levels and expirys (expiries?) for query
const protections = [], expirys = [];
if (ctx.protectEdit) {
protections.push('edit=' + ctx.protectEdit.level);
expirys.push(ctx.protectEdit.expiry);
}
if (ctx.protectMove) {
protections.push('move=' + ctx.protectMove.level);
expirys.push(ctx.protectMove.expiry);
}
if (ctx.protectCreate) {
protections.push('create=' + ctx.protectCreate.level);
expirys.push(ctx.protectCreate.expiry);
}
const query = {
action: 'protect',
title: pageTitle,
token: token,
protections: protections.join('|'),
expiry: expirys.join('|'),
reason: ctx.editSummary,
watchlist: ctx.watchlistOption,
format: 'json'
};
// Only shows up in logs, not page history [[phab:T259983]]
if (ctx.changeTags) {
query.tags = ctx.changeTags;
}
if (fnApplyWatchlistExpiry()) {
query.watchlistexpiry = ctx.watchlistExpiry;
}
if (ctx.protectCascade) {
query.cascade = 'true';
}
ctx.protectProcessApi = new Morebits.wiki.Api('protecting page...', query, ctx.onProtectSuccess, ctx.statusElement, ctx.onProtectFailure);
ctx.protectProcessApi.setParent(this);
ctx.protectProcessApi.post();
};
var fnProcessStabilize = function() {
let pageTitle, token;
if (fnCanUseMwUserToken('stabilize')) {
token = mw.user.tokens.get('csrfToken');
pageTitle = ctx.pageName;
} else {
const response = ctx.stabilizeApi.getResponse().query;
// 'stabilize' as a verb not necessarily well understood
if (!fnProcessChecks('stabilize', ctx.onStabilizeFailure, response)) {
return; // abort
}
token = response.tokens.csrftoken;
const page = response.pages[0];
pageTitle = page.title;
// Doesn't support watchlist expiry [[phab:T263336]]
// ctx.watched = page.watchlistexpiry || page.watched;
}
const query = {
action: 'stabilize',
title: pageTitle,
token: token,
protectlevel: ctx.flaggedRevs.level,
expiry: ctx.flaggedRevs.expiry,
// tags: ctx.changeTags, // flaggedrevs tag support: [[phab:T247721]]
reason: ctx.editSummary,
watchlist: ctx.watchlistOption,
format: 'json'
};
/* Doesn't support watchlist expiry [[phab:T263336]]
if (fnApplyWatchlistExpiry()) {
query.watchlistexpiry = ctx.watchlistExpiry;
}
*/
ctx.stabilizeProcessApi = new Morebits.wiki.Api('configuring stabilization settings...', query, ctx.onStabilizeSuccess, ctx.statusElement, ctx.onStabilizeFailure);
ctx.stabilizeProcessApi.setParent(this);
ctx.stabilizeProcessApi.post();
};
var sleep = function(milliseconds) {
const deferred = $.Deferred();
setTimeout(deferred.resolve, milliseconds);
return deferred;
};
}; // end Morebits.wiki.Page
/* Morebits.wiki.Page TODO: (XXX)
* - Should we retry loads also?
* - Need to reset current action before the save?
* - Deal with action.completed stuff
* - Need to reset all parameters once done (e.g. edit summary, move destination, etc.)
*/
/* **************** Morebits.wiki.Preview **************** */
/**
* Use the API to parse a fragment of wikitext and render it as HTML.
*
* The suggested implementation pattern (in {@link Morebits.SimpleWindow} and
* {@link Morebits.QuickForm} situations) is to construct a
* `Morebits.wiki.Preview` object after rendering a `Morebits.QuickForm`, and
* bind the object to an arbitrary property of the form (e.g. |previewer|).
* For an example, see twinklewarn.js.
*
* @memberof Morebits.wiki
* @class
* @param {HTMLElement} previewbox - The element that will contain the rendered HTML,
* usually a <div> element.
*/
Morebits.wiki.Preview = function(previewbox) {
this.previewbox = previewbox;
$(previewbox).addClass('morebits-previewbox').hide();
/**
* Displays the preview box, and begins an asynchronous attempt
* to render the specified wikitext.
*
* @param {string} wikitext - Wikitext to render; most things should work, including `subst:` and `~~~~`.
* @param {string} [pageTitle] - Optional parameter for the page this should be rendered as being on, if omitted it is taken as the current page.
* @param {string} [sectionTitle] - If provided, render the text as a new section using this as the title.
* @return {jQuery.promise}
*/
this.beginRender = function(wikitext, pageTitle, sectionTitle) {
$(previewbox).show();
const statusspan = document.createElement('span');
previewbox.appendChild(statusspan);
Morebits.Status.init(statusspan);
const query = {
action: 'parse',
prop: ['text', 'modules'],
pst: true, // PST = pre-save transform; this makes substitution work properly
preview: true,
text: wikitext,
title: pageTitle || mw.config.get('wgPageName'),
disablelimitreport: true,
disableeditsection: true,
format: 'json'
};
if (sectionTitle) {
query.section = 'new';
query.sectiontitle = sectionTitle;
}
const renderApi = new Morebits.wiki.Api('loading...', query, fnRenderSuccess, new Morebits.Status('Preview'));
return renderApi.post();
};
var fnRenderSuccess = function(apiobj) {
const response = apiobj.getResponse();
const html = response.parse.text;
if (!html) {
apiobj.statelem.error('failed to retrieve preview, or template was blanked');
return;
}
previewbox.innerHTML = html;
mw.loader.load(response.parse.modulestyles);
mw.loader.load(response.parse.modules);
// this makes links open in new tab
$(previewbox).find('a').attr('target', '_blank');
// Integrate with scripts that do things on rendered content, like navpopups
mw.hook('wikipage.content').fire($(previewbox));
};
/** Hides the preview box and clears it. */
this.closePreview = function() {
$(previewbox).empty().hide();
};
};
/* **************** Morebits.wikitext **************** */
/**
* Wikitext manipulation.
*
* @namespace Morebits.wikitext
* @memberof Morebits
*/
Morebits.wikitext = {};
/**
* Get the value of every parameter found in the wikitext of a given template.
*
* @memberof Morebits.wikitext
* @param {string} text - Wikitext containing a template.
* @param {number} [start=0] - Index noting where in the text the template begins.
* @return {Object} `{name: templateName, parameters: {key: value}}`.
*/
Morebits.wikitext.parseTemplate = function(text, start) {
start = start || 0;
const level = []; // Track of how deep we are ({{, {{{, or [[)
let count = -1; // Number of parameters found
let unnamed = 0; // Keep track of what number an unnamed parameter should receive
let equals = -1; // After finding "=" before a parameter, the index; otherwise, -1
let current = '';
const result = {
name: '',
parameters: {}
};
let key, value;
/**
* Function to handle finding parameter values.
*
* @param {boolean} [final=false] - Whether this is the final
* parameter and we need to remove the trailing `}}`.
*/
function findParam(final) {
// Nothing found yet, this must be the template name
if (count === -1) {
result.name = current.slice(2).trim();
++count;
} else {
// In a parameter
if (equals !== -1) {
// We found an equals, so save the parameter as key: value
key = current.substring(0, equals).trim();
value = final ? current.substring(equals + 1, current.length - 2).trim() : current.substring(equals + 1).trim();
result.parameters[key] = value;
equals = -1;
} else {
// No equals, so it must be unnamed; no trim since whitespace allowed
const param = final ? current.substring(equals + 1, current.length - 2) : current;
if (param) {
result.parameters[++unnamed] = param;
++count;
}
}
}
}
for (let i = start; i < text.length; ++i) {
const test3 = text.substr(i, 3);
if (test3 === '{{{' || (test3 === '}}}' && level[level.length - 1] === 3)) {
current += test3;
i += 2;
if (test3 === '{{{') {
level.push(3);
} else {
level.pop();
}
continue;
}
const test2 = text.substr(i, 2);
// Entering a template (or link)
if (test2 === '{{' || test2 === '[[') {
current += test2;
++i;
if (test2 === '{{') {
level.push(2);
} else {
level.push('wl');
}
continue;
}
// Either leaving a link or template/parser function
if ((test2 === '}}' && level[level.length - 1] === 2) ||
(test2 === ']]' && level[level.length - 1] === 'wl')) {
current += test2;
++i;
level.pop();
// Find the final parameter if this really is the end
if (test2 === '}}' && level.length === 0) {
findParam(true);
break;
}
continue;
}
if (text.charAt(i) === '|' && level.length === 1) {
// Another pipe found, toplevel, so parameter coming up!
findParam();
current = '';
} else if (equals === -1 && text.charAt(i) === '=' && level.length === 1) {
// Equals found, toplevel
equals = current.length;
current += text.charAt(i);
} else {
// Just advance the position
current += text.charAt(i);
}
}
return result;
};
/**
* Adjust and manipulate the wikitext of a page.
*
* @class
* @memberof Morebits.wikitext
* @param {string} text - Wikitext to be manipulated.
*/
Morebits.wikitext.Page = function mediawikiPage(text) {
this.text = text;
};
Morebits.wikitext.Page.prototype = {
text: '',
/**
* Removes links to `link_target` from the page text.
*
* @param {string} link_target
* @return {Morebits.wikitext.Page}
*/
removeLink: function(link_target) {
const mwTitle = mw.Title.newFromText(link_target);
const namespaceID = mwTitle.getNamespaceId();
const title = mwTitle.getMainText();
let link_regex_string = '';
if (namespaceID !== 0) {
link_regex_string = Morebits.namespaceRegex(namespaceID) + ':';
}
link_regex_string += Morebits.pageNameRegex(title);
// For most namespaces, unlink both [[User:Test]] and [[:User:Test]]
// For files and categories, only unlink [[:Category:Test]]. Do not unlink [[Category:Test]]
const isFileOrCategory = [6, 14].includes(namespaceID);
const colon = isFileOrCategory ? ':' : ':?';
const simple_link_regex = new RegExp('\\[\\[' + colon + '(' + link_regex_string + ')\\]\\]', 'g');
const piped_link_regex = new RegExp('\\[\\[' + colon + link_regex_string + '\\|(.+?)\\]\\]', 'g');
this.text = this.text.replace(simple_link_regex, '$1').replace(piped_link_regex, '$1');
return this;
},
/**
* Comments out images from page text; if used in a gallery, deletes the whole line.
* If used as a template argument (not necessarily with `File:` prefix), the template parameter is commented out.
*
* @param {string} image - Image name without `File:` prefix.
* @param {string} [reason] - Reason to be included in comment, alongside the commented-out image.
* @return {Morebits.wikitext.Page}
*/
commentOutImage: function(image, reason) {
const unbinder = new Morebits.Unbinder(this.text);
unbinder.unbind('<!--', '-->');
reason = reason ? reason + ': ' : '';
const image_re_string = Morebits.pageNameRegex(image);
// Check for normal image links, i.e. [[File:Foobar.png|...]]
// Will eat the whole link
const links_re = new RegExp('\\[\\[' + Morebits.namespaceRegex(6) + ':\\s*' + image_re_string + '\\s*[\\|(?:\\]\\])]');
const allLinks = Morebits.string.splitWeightedByKeys(unbinder.content, '[[', ']]');
for (let i = 0; i < allLinks.length; ++i) {
if (links_re.test(allLinks[i])) {
const replacement = '<!-- ' + reason + allLinks[i] + ' -->';
unbinder.content = unbinder.content.replace(allLinks[i], replacement);
// unbind the newly created comments
unbinder.unbind('<!--', '-->');
}
}
// Check for gallery images, i.e. instances that must start on a new line,
// eventually preceded with some space, and must include File: prefix
// Will eat the whole line.
const gallery_image_re = new RegExp('(^\\s*' + Morebits.namespaceRegex(6) + ':\\s*' + image_re_string + '\\s*(?:\\|.*?$|$))', 'mg');
unbinder.content = unbinder.content.replace(gallery_image_re, '<!-- ' + reason + '$1 -->');
// unbind the newly created comments
unbinder.unbind('<!--', '-->');
// Check free image usages, for example as template arguments, might have the File: prefix excluded, but must be preceded by an |
// Will only eat the image name and the preceding bar and an eventual named parameter
const free_image_re = new RegExp('(\\|\\s*(?:[\\w\\s]+\\=)?\\s*(?:' + Morebits.namespaceRegex(6) + ':\\s*)?' + image_re_string + ')', 'mg');
unbinder.content = unbinder.content.replace(free_image_re, '<!-- ' + reason + '$1 -->');
// Rebind the content now, we are done!
this.text = unbinder.rebind();
return this;
},
removeImage: function(image) {
var unbinder = new Morebits.Unbinder(this.text);
unbinder.unbind('<!--', '-->');
var image_re_string = Morebits.pageNameRegex(image);
// Check for normal image links, i.e. [[File:Foobar.png|...]]
// Will eat the whole link
var links_re = new RegExp('\\[\\[' + Morebits.namespaceRegex(6) + ':\\s*' + image_re_string + '\\s*[\\|(?:\\]\\])]');
var allLinks = Morebits.string.splitWeightedByKeys(unbinder.content, '[[', ']]');
for (var i = 0; i < allLinks.length; ++i) {
if (links_re.test(allLinks[i])) {
var replacingtext = Morebits.string.escapeRegExp(allLinks[i]);
var replacing = new RegExp('(\n|)' + replacingtext + '$');
unbinder.content = unbinder.content.replace(replacing, '').replace(allLinks[i], '');
}
}
// unbind the newly created comments
unbinder.unbind('<!--', '-->');
// Check for gallery images, i.e. instances that must start on a new line,
// eventually preceded with some space, and must include File: prefix
// Will eat the whole line.
var gallery_image_re = new RegExp('\\n(^\\s*' + Morebits.namespaceRegex(6) + ':\\s*' + image_re_string + '\\s*(?:\\|.*?$|$))', 'mg');
unbinder.content = unbinder.content.replace(gallery_image_re, '');
// unbind the newly created comments
unbinder.unbind('<!--', '-->');
// Check free image usages, for example as template arguments, might have the File: prefix excluded, but must be preceded by an |
// Will only eat the image name and the preceding bar and an eventual named parameter
var free_image_re = new RegExp('(\\|\\s*(?:[\\w\\s]+\\=)?\\s*(?:' + Morebits.namespaceRegex(6) + ':\\s*)?' + image_re_string + ')', 'mg');
unbinder.content = unbinder.content.replace(free_image_re, '');
// Rebind the content now, we are done!
this.text = unbinder.rebind();
return this;
},
/**
* Converts uses of [[File:`image`]] to [[File:`image`|`data`]].
*
* @param {string} image - Image name without File: prefix.
* @param {string} data - The display options.
* @return {Morebits.wikitext.Page}
*/
addToImageComment: function(image, data) {
const image_re_string = Morebits.pageNameRegex(image);
const links_re = new RegExp('\\[\\[' + Morebits.namespaceRegex(6) + ':\\s*' + image_re_string + '\\s*[\\|(?:\\]\\])]');
const allLinks = Morebits.string.splitWeightedByKeys(this.text, '[[', ']]');
for (let i = 0; i < allLinks.length; ++i) {
if (links_re.test(allLinks[i])) {
let replacement = allLinks[i];
// just put it at the end?
replacement = replacement.replace(/\]\]$/, '|' + data + ']]');
this.text = this.text.replace(allLinks[i], replacement);
}
}
const gallery_re = new RegExp('^(\\s*' + image_re_string + '.*?)\\|?(.*?)$', 'mg');
const newtext = '$1|$2 ' + data;
this.text = this.text.replace(gallery_re, newtext);
return this;
},
/**
* Remove all transclusions of a template from page text.
*
* @param {string} template - Page name whose transclusions are to be removed,
* include namespace prefix only if not in template namespace.
* @return {Morebits.wikitext.Page}
*/
removeTemplate: function(template) {
const template_re_string = Morebits.pageNameRegex(template);
const links_re = new RegExp('\\{\\{(?:' + Morebits.namespaceRegex(10) + ':)?\\s*' + template_re_string + '\\s*[\\|(?:\\}\\})]');
const allTemplates = Morebits.string.splitWeightedByKeys(this.text, '{{', '}}', ['{{{', '}}}']);
for (let i = 0; i < allTemplates.length; ++i) {
if (links_re.test(allTemplates[i])) {
this.text = this.text.replace(allTemplates[i], '');
}
}
return this;
},
/**
* Smartly insert a tag atop page text but after specified templates,
* such as hatnotes, short description, or deletion and protection templates.
* Notably, does *not* insert a newline after the tag.
*
* @param {string} tag - The tag to be inserted.
* @param {string|string[]} regex - Templates after which to insert tag,
* given as either as a (regex-valid) string or an array to be joined by pipes.
* @param {string} [flags=i] - Regex flags to apply. `''` to provide no flags;
* other falsey values will default to `i`.
* @param {string|string[]} [preRegex] - Optional regex string or array to match
* before any template matches (i.e. before `{{`), such as html comments.
* @return {Morebits.wikitext.Page}
*/
insertAfterTemplates: function(tag, regex, flags, preRegex) {
if (typeof tag === 'undefined') {
throw new Error('No tag provided');
}
// .length is only a property of strings and arrays so we
// shouldn't need to check type
if (typeof regex === 'undefined' || !regex.length) {
throw new Error('No regex provided');
} else if (Array.isArray(regex)) {
regex = regex.join('|');
}
if (typeof flags !== 'string') {
flags = 'i';
}
if (!preRegex || !preRegex.length) {
preRegex = '';
} else if (Array.isArray(preRegex)) {
preRegex = preRegex.join('|');
}
// Regex is extra complicated to allow for templates with
// parameters and to handle whitespace properly
this.text = this.text.replace(
new RegExp(
// leading whitespace
'^\\s*' +
// capture template(s)
'(?:((?:\\s*' +
// Pre-template regex, such as leading html comments
preRegex + '|' +
// begin template format
'\\{\\{\\s*(?:' +
// Template regex
regex +
// end main template name, optionally with a number
// Probably remove the (?:) though
')\\d*\\s*' +
// template parameters
'(\\|(?:\\{\\{[^{}]*\\}\\}|[^{}])*)?' +
// end template format
'\\}\\})+' +
// end capture
'(?:\\s*\\n)?)' +
// trailing whitespace
'\\s*)?',
flags), '$1' + tag
);
return this;
},
/**
* Get the manipulated wikitext.
*
* @return {string}
*/
getText: function() {
return this.text;
}
};
/* *********** Morebits.UserspaceLogger ************ */
/**
* Handles logging actions to a userspace log.
* Used in CSD, PROD, and XFD.
*
* @memberof Morebits
* @class
* @param {string} logPageName - Title of the subpage of the current user's log.
*/
Morebits.UserspaceLogger = function(logPageName) {
if (!logPageName) {
throw new Error('記録ページ名が指定されていません');
}
/**
* The text to prefix the log with upon creation, defaults to empty.
*
* @type {string}
*/
this.initialText = '';
/**
* The header level to use for months, defaults to 3 (`===`).
*
* @type {number}
*/
this.headerLevel = 3;
this.changeTags = '';
/**
* Log the entry.
*
* @param {string} logText - Doesn't include leading `#` or `*`.
* @param {string} summaryText - Edit summary.
* @return {jQuery.Promise}
*/
this.log = function(logText, summaryText) {
const def = $.Deferred();
if (!logText) {
return def.reject();
}
var page = new Morebits.wiki.Page('User:' + mw.config.get('wgUserName') + '/' + logPageName,
'ユーザースペースログに項目を追加する'); // make this '... to ' + logPageName ?
page.load((pageobj) => {
// add blurb if log page doesn't exist or is blank
let text = pageobj.getPageText() || this.initialText;
// create monthly header if it doesn't exist already
const date = new Morebits.Date(pageobj.getLoadTime());
if (!date.monthHeaderRegex().exec(text)) {
text += '\n\n' + date.monthHeader(this.headerLevel);
}
pageobj.setPageText(text + '\n' + logText);
pageobj.setEditSummary(summaryText);
pageobj.setChangeTags(this.changeTags);
pageobj.setCreateOption('recreate');
pageobj.save(def.resolve, def.reject);
});
return def;
};
};
/* **************** Morebits.Status **************** */
/**
* Create and show status messages of varying urgency.
* {@link Morebits.Status.init|Morebits.Status.init()} must be called before
* any status object is created, otherwise those statuses won't be visible.
*
* @memberof Morebits
* @class
* @param {string} text - Text before the the colon `:`.
* @param {string} stat - Text after the colon `:`.
* @param {string} [type=status] - Determine the font color of the status
* line, allowable values are: `status` (blue), `info` (green), `warn` (red),
* or `error` (bold red).
*/
Morebits.Status = function Status(text, stat, type) {
this.textRaw = text;
this.text = Morebits.createHtml(text);
this.type = type || 'status';
this.generate();
if (stat) {
this.update(stat, type);
}
};
/**
* Specify an area for status message elements to be added to.
*
* @memberof Morebits.Status
* @param {HTMLElement} root - Usually a div element.
* @throws {Error} If `root` is not an `HTMLElement`.
*/
Morebits.Status.init = function(root) {
if (!(root instanceof Element)) {
throw new Error('object not an instance of Element');
}
while (root.hasChildNodes()) {
root.removeChild(root.firstChild);
}
Morebits.Status.root = root;
Morebits.Status.errorEvent = null;
};
Morebits.Status.root = null;
/**
* @memberof Morebits.Status
* @param {Function} handler - Function to execute on error.
* @throws {Error} When `handler` is not a function.
*/
Morebits.Status.onError = function(handler) {
if (typeof handler === 'function') {
Morebits.Status.errorEvent = handler;
} else {
throw new Error('Morebits.Status.onError: handler is not a function');
}
};
Morebits.Status.prototype = {
stat: null,
statRaw: null,
text: null,
textRaw: null,
type: 'status',
target: null,
node: null,
linked: false,
/** Add the status element node to the DOM. */
link: function() {
if (!this.linked && Morebits.Status.root) {
Morebits.Status.root.appendChild(this.node);
this.linked = true;
}
},
/** Remove the status element node from the DOM. */
unlink: function() {
if (this.linked) {
Morebits.Status.root.removeChild(this.node);
this.linked = false;
}
},
/**
* Update the status.
*
* @param {string} status - Part of status message after colon.
* @param {string} type - 'status' (blue), 'info' (green), 'warn'
* (red), or 'error' (bold red).
*/
update: function(status, type) {
this.statRaw = status;
this.stat = Morebits.createHtml(status);
if (type) {
this.type = type;
if (type === 'error') {
// hack to force the page not to reload when an error is output - see also Morebits.Status() above
Morebits.wiki.numberOfActionsLeft = 1000;
// call error callback
if (Morebits.Status.errorEvent) {
Morebits.Status.errorEvent();
}
// also log error messages in the browser console
console.error(this.textRaw + ': ' + this.statRaw); // eslint-disable-line no-console
}
}
this.render();
},
/** Produce the html for first part of the status message. */
generate: function() {
this.node = document.createElement('div');
this.node.appendChild(document.createElement('span')).appendChild(this.text);
this.node.appendChild(document.createElement('span')).appendChild(document.createTextNode(': '));
this.target = this.node.appendChild(document.createElement('span'));
this.target.appendChild(document.createTextNode('')); // dummy node
},
/** Complete the html, for the second part of the status message. */
render: function() {
this.node.className = 'morebits_status_' + this.type;
while (this.target.hasChildNodes()) {
this.target.removeChild(this.target.firstChild);
}
this.target.appendChild(this.stat);
this.link();
},
status: function(status) {
this.update(status, 'status');
},
info: function(status) {
this.update(status, 'info');
},
warn: function(status) {
this.update(status, 'warn');
},
error: function(status) {
this.update(status, 'error');
}
};
/**
* @memberof Morebits.Status
* @param {string} text - Before colon
* @param {string} status - After colon
* @return {Morebits.Status} - `status`-type (blue)
*/
Morebits.Status.status = function(text, status) {
return new Morebits.Status(text, status);
};
/**
* @memberof Morebits.Status
* @param {string} text - Before colon
* @param {string} status - After colon
* @return {Morebits.Status} - `info`-type (green)
*/
Morebits.Status.info = function(text, status) {
return new Morebits.Status(text, status, 'info');
};
/**
* @memberof Morebits.Status
* @param {string} text - Before colon
* @param {string} status - After colon
* @return {Morebits.Status} - `warn`-type (red)
*/
Morebits.Status.warn = function(text, status) {
return new Morebits.Status(text, status, 'warn');
};
/**
* @memberof Morebits.Status
* @param {string} text - Before colon
* @param {string} status - After colon
* @return {Morebits.Status} - `error`-type (bold red)
*/
Morebits.Status.error = function(text, status) {
return new Morebits.Status(text, status, 'error');
};
/**
* For the action complete message at the end, create a status line without
* a colon separator.
*
* @memberof Morebits.Status
* @param {string} text
*/
Morebits.Status.actionCompleted = function(text) {
const node = document.createElement('div');
node.appendChild(document.createElement('b')).appendChild(document.createTextNode(text));
node.className = 'morebits_status_info morebits_action_complete';
if (Morebits.Status.root) {
Morebits.Status.root.appendChild(node);
}
};
/**
* Display the user's rationale, comments, etc. Back to them after a failure,
* so that they may re-use it.
*
* @memberof Morebits.Status
* @param {string} comments
* @param {string} message
*/
Morebits.Status.printUserText = function(comments, message) {
const p = document.createElement('p');
p.innerHTML = message;
const div = document.createElement('div');
div.className = 'morebits-usertext';
div.style.marginTop = '0';
div.style.whiteSpace = 'pre-wrap';
div.textContent = comments;
p.appendChild(div);
Morebits.Status.root.appendChild(p);
};
/**
* Simple helper function to create a simple node.
*
* @param {string} type - Type of HTML element.
* @param {string} content - Text content.
* @param {string} [color] - Font color.
* @return {HTMLElement}
*/
Morebits.htmlNode = function(type, content, color) {
const node = document.createElement(type);
if (color) {
node.style.color = color;
}
node.appendChild(document.createTextNode(content));
return node;
};
/**
* Add shift-click support for checkboxes. The wikibits version
* (`window.addCheckboxClickHandlers`) has some restrictions, and doesn't work
* with checkboxes inside a sortable table, so let's build our own.
*
* @param {jQuery} jQuerySelector
* @param {jQuery} jQueryContext
*/
Morebits.checkboxShiftClickSupport = function(jQuerySelector, jQueryContext) {
let lastCheckbox = null;
function clickHandler(event) {
const thisCb = this;
if (event.shiftKey && lastCheckbox !== null) {
const $cbs = $(jQuerySelector, jQueryContext); // can't cache them, obviously, if we want to support re-sorting
let index = -1, lastIndex = -1, i;
for (i = 0; i < $cbs.length; i++) {
if ($cbs[i] === thisCb) {
index = i;
if (lastIndex > -1) {
break;
}
}
if ($cbs[i] === lastCheckbox) {
lastIndex = i;
if (index > -1) {
break;
}
}
}
if (index > -1 && lastIndex > -1) {
// inspired by wikibits
const endState = thisCb.checked;
let start, finish;
if (index < lastIndex) {
start = index + 1;
finish = lastIndex;
} else {
start = lastIndex;
finish = index - 1;
}
for (i = start; i <= finish; i++) {
if ($cbs[i].checked !== endState) {
$cbs[i].click();
}
}
}
}
lastCheckbox = thisCb;
return true;
}
$(jQuerySelector, jQueryContext).on('click', clickHandler);
};
/* **************** Morebits.BatchOperation **************** */
/**
* Iterates over a group of pages (or arbitrary objects) and executes a worker function
* for each.
*
* `setPageList(pageList)`: Sets the list of pages to work on. It should be an
* array of page names strings.
*
* `setOption(optionName, optionValue)`: Sets a known option:
* - `chunkSize` (integer): The size of chunks to break the array into (default
* 50). Setting this to a small value (<5) can cause problems.
* - `preserveIndividualStatusLines` (boolean): Keep each page's status element
* visible when worker is complete? See note below.
*
* `run(worker, postFinish)`: Runs the callback `worker` for each page in the
* list. The callback must call `workerSuccess` when succeeding, or
* `workerFailure` when failing. If using {@link Morebits.wiki.Api} or
* {@link Morebits.wiki.Page}, this is easily done by passing these two
* functions as parameters to the methods on those objects: for instance,
* `page.save(batchOp.workerSuccess, batchOp.workerFailure)`. Make sure the
* methods are called directly if special success/failure cases arise. If you
* omit to call these methods, the batch operation will stall after the first
* chunk! Also ensure that either workerSuccess or workerFailure is called no
* more than once. The second callback `postFinish` is executed when the
* entire batch has been processed.
*
* If using `preserveIndividualStatusLines`, you should try to ensure that the
* `workerSuccess` callback has access to the page title. This is no problem for
* {@link Morebits.wiki.Page} objects. But when using the API, please set the
* |pageName| property on the {@link Morebits.wiki.Api} object.
*
* There are sample batchOperation implementations using Morebits.wiki.Page in
* twinklebatchdelete.js, twinklebatchundelete.js, and twinklebatchprotect.js.
*
* @memberof Morebits
* @class
* @param {string} [currentAction]
*/
Morebits.BatchOperation = function(currentAction) {
const ctx = {
// backing fields for public properties
pageList: null,
options: {
chunkSize: 50,
preserveIndividualStatusLines: false
},
// internal counters, etc.
statusElement: new Morebits.Status(currentAction || 'バッチ処理の実行'),
worker: null, // function that executes for each item in pageList
postFinish: null, // function that executes when the whole batch has been processed
countStarted: 0,
countFinished: 0,
countFinishedSuccess: 0,
currentChunkIndex: -1,
pageChunks: [],
running: false
};
// shouldn't be needed by external users, but provided anyway for maximum flexibility
this.getStatusElement = function() {
return ctx.statusElement;
};
/**
* Sets the list of pages to work on.
*
* @param {Array} pageList - Array of objects over which you wish to execute the worker function
* This is usually the list of page names (strings).
*/
this.setPageList = function(pageList) {
ctx.pageList = pageList;
};
/**
* Sets a known option.
*
* @param {string} optionName - Name of the option:
* - chunkSize (integer): The size of chunks to break the array into
* (default 50). Setting this to a small value (<5) can cause problems.
* - preserveIndividualStatusLines (boolean): Keep each page's status
* element visible when worker is complete?
* @param {number|boolean} optionValue - Value to which the option is
* to be set. Should be an integer for chunkSize and a boolean for
* preserveIndividualStatusLines.
*/
this.setOption = function(optionName, optionValue) {
ctx.options[optionName] = optionValue;
};
/**
* Runs the first callback for each page in the list.
* The callback must call workerSuccess when succeeding, or workerFailure when failing.
* Runs the optional second callback when the whole batch has been processed.
*
* @param {Function} worker
* @param {Function} [postFinish]
*/
this.run = function(worker, postFinish) {
if (ctx.running) {
ctx.statusElement.error('Batch operation is already running');
return;
}
ctx.running = true;
ctx.worker = worker;
ctx.postFinish = postFinish;
ctx.countStarted = 0;
ctx.countFinished = 0;
ctx.countFinishedSuccess = 0;
ctx.currentChunkIndex = -1;
ctx.pageChunks = [];
const total = ctx.pageList.length;
if (!total) {
ctx.statusElement.info('ページが指定されていません');
ctx.running = false;
if (ctx.postFinish) {
ctx.postFinish();
}
return;
}
// chunk page list into more manageable units
ctx.pageChunks = Morebits.array.chunk(ctx.pageList, ctx.options.chunkSize);
// start the process
Morebits.wiki.addCheckpoint();
ctx.statusElement.status('0%');
fnStartNewChunk();
};
/**
* To be called by worker before it terminates successfully.
*
* @param {(Morebits.wiki.Page|Morebits.wiki.Api|string)} arg -
* This should be the `Morebits.wiki.Page` or `Morebits.wiki.Api` object used by worker
* (for the adjustment of status lines emitted by them).
* If no Morebits.wiki.* object is used (e.g. you're using `mw.Api()` or something else), and
* `preserveIndividualStatusLines` option is on, give the page name (string) as argument.
*/
this.workerSuccess = function(arg) {
if (arg instanceof Morebits.wiki.Api || arg instanceof Morebits.wiki.Page) {
// update or remove status line
const statelem = arg.getStatusElement();
if (ctx.options.preserveIndividualStatusLines) {
if (arg.getPageName || arg.pageName || (arg.query && arg.query.title)) {
// we know the page title - display a relevant message
var pageName = arg.getPageName ? arg.getPageName() : arg.pageName || arg.query.title;
statelem.info('完了 ([[' + pageName + ']])');
} else {
// we don't know the page title - just display a generic message
statelem.info('完了');
}
} else {
// remove the status line automatically produced by Morebits.wiki.*
statelem.unlink();
}
} else if (typeof arg === 'string' && ctx.options.preserveIndividualStatusLines) {
new Morebits.Status(arg, '完了 ([[' + arg + ']])');
}
ctx.countFinishedSuccess++;
fnDoneOne();
};
this.workerFailure = function() {
fnDoneOne();
};
// private functions
const thisProxy = this;
var fnStartNewChunk = function() {
const chunk = ctx.pageChunks[++ctx.currentChunkIndex];
if (!chunk) {
return; // done! yay
}
// start workers for the current chunk
ctx.countStarted += chunk.length;
chunk.forEach((page) => {
ctx.worker(page, thisProxy);
});
};
var fnDoneOne = function() {
ctx.countFinished++;
// update overall status line
const total = ctx.pageList.length;
if (ctx.countFinished < total) {
const progress = Math.round(100 * ctx.countFinished / total);
ctx.statusElement.status(progress + '%');
// start a new chunk if we're close enough to the end of the previous chunk, and
// we haven't already started the next one
if (ctx.countFinished >= (ctx.countStarted - Math.max(ctx.options.chunkSize / 10, 2)) &&
Math.floor(ctx.countFinished / ctx.options.chunkSize) > ctx.currentChunkIndex) {
fnStartNewChunk();
}
} else if (ctx.countFinished === total) {
var statusString = '完了 (' + ctx.countFinishedSuccess +
'/' + ctx.countFinished + ' アクションが正常に完了)';
if (ctx.countFinishedSuccess < ctx.countFinished) {
ctx.statusElement.warn(statusString);
} else {
ctx.statusElement.info(statusString);
}
if (ctx.postFinish) {
ctx.postFinish();
}
Morebits.wiki.removeCheckpoint();
ctx.running = false;
} else {
// ctx.countFinished > total
// just for giggles! (well, serious debugging, actually)
ctx.statusElement.warn('完了 (' + (ctx.countFinished - total) + '件上回った)');
Morebits.wiki.removeCheckpoint();
ctx.running = false;
}
};
};
/**
* Given a set of asynchronous functions to run along with their dependencies,
* run them in an efficient sequence so that multiple functions
* that don't depend on each other are triggered simultaneously. Where
* dependencies exist, it ensures that the dependency functions finish running
* before the dependent function runs. The values resolved by the dependencies
* are made available to the dependant as arguments.
*
* @memberof Morebits
* @class
*/
Morebits.TaskManager = function(context) {
this.taskDependencyMap = new Map();
this.failureCallbackMap = new Map();
this.deferreds = new Map();
this.context = context || window;
/**
* Register a task along with its dependencies (tasks which should have finished
* execution before we can begin this one). Each task is a function that must return
* a promise. The function will get the values resolved by the dependency functions
* as arguments.
*
* @param {Function} func - A task.
* @param {Function[]} deps - Its dependencies.
* @param {Function} [onFailure] - a failure callback that's run if the task or any one
* of its dependencies fail.
*/
this.add = function(func, deps, onFailure) {
this.taskDependencyMap.set(func, deps);
this.failureCallbackMap.set(func, onFailure || (() => {}));
const deferred = $.Deferred();
this.deferreds.set(func, deferred);
};
/**
* Run all the tasks. Multiple tasks may be run at once.
*
* @return {jQuery.Promise} - Resolved if all tasks succeed, rejected otherwise.
*/
this.execute = function() {
const self = this; // proxy for `this` for use inside functions where `this` is something else
this.taskDependencyMap.forEach((deps, task) => {
const dependencyPromisesArray = deps.map((dep) => self.deferreds.get(dep));
$.when.apply(self.context, dependencyPromisesArray).then(function() {
const result = task.apply(self.context, arguments);
if (result === undefined) { // maybe the function threw, or it didn't return anything
mw.log.error('Morebits.TaskManager: task returned undefined');
self.deferreds.get(task).reject.apply(self.context, arguments);
self.failureCallbackMap.get(task).apply(self.context, []);
}
result.then(function() {
self.deferreds.get(task).resolve.apply(self.context, arguments);
}, function() { // task failed
self.deferreds.get(task).reject.apply(self.context, arguments);
self.failureCallbackMap.get(task).apply(self.context, arguments);
});
}, function() { // one or more of the dependencies failed
self.failureCallbackMap.get(task).apply(self.context, arguments);
});
});
return $.when.apply(null, [...this.deferreds.values()]); // resolved when everything is done!
};
};
/**
* A simple draggable window. No longer uses jQuery UI.
*
* @memberof Morebits
* @class
* @param {number} width
* @param {number} height - The maximum allowable height for the content area.
*/
Morebits.SimpleWindow = function SimpleWindow(width, height) {
const $dialog = $('<div>').addClass('morebits-dialog').attr('role', 'dialog').attr('tabindex', -1);
const $titleBar = $('<div>').addClass('morebits-dialog-titlebar').append(
$('<span>').addClass('morebits-dialog-title'),
$('<button>')
.addClass('morebits-dialog-close')
.text('\u00D7')
.on('click', () => this.close())
);
const $content = $('<div>')
.addClass('morebits-dialog-content')
.attr('id', 'morebits-dialog-content-' + Math.round(Math.random() * 1e15));
const $buttonPane = $('<div>').addClass('morebits-dialog-buttonpane').append(
$('<span>').addClass('morebits-dialog-buttons'),
$('<span>').addClass('morebits-dialog-footerlinks')
);
const $resizer = $('<div>').addClass('morebits-dialog-resizer');
$dialog.append($titleBar, $content, $buttonPane, $resizer)
.css('width', Math.min(parseInt(width || 800, 10), $(window).width()))
.css('height', 'auto')
.css('max-height', height + 20)
.css('top', Math.max(0, window.scrollY + $(window).height() / 2 - (height + 20) / 2)) // Centre it (assume max height)
.css('left', Math.max(0, window.scrollX + $(window).width() / 2 - $dialog.width() / 2)); // Centre it
$dialog.on('focus', () => this.focus()).on('keydown', (e) => {
if (e.key === 'Escape') {
this.close();
}
});
// Make dialog draggable and resizable
let isDragging = false, isResizing = false, initialX, initialY, initialWidth, initialHeight;
$titleBar.on('mousedown', (e) => {
isDragging = true;
initialX = e.clientX - $dialog.offset().left;
initialY = e.clientY - $dialog.offset().top;
});
$resizer.on('mousedown', (e) => {
isResizing = true;
$dialog.css('height', $dialog.height());
$dialog.css('max-height', 'none');
initialWidth = $dialog.width();
initialHeight = $dialog.height();
initialX = e.clientX;
initialY = e.clientY;
});
$(document).on('mousemove', (e) => {
if (isDragging) {
$dialog.css('left', Math.max(0, e.clientX - initialX))
.css('top', Math.max(0, e.clientY - initialY));
} else if (isResizing) {
$dialog.css('width', initialWidth + (e.clientX - initialX))
.css('height', initialHeight + (e.clientY - initialY));
}
}).on('mouseup', () => {
isDragging = false;
isResizing = false;
});
this.$dialog = $dialog;
this.content = $content[0];
this.height = height;
};
Morebits.SimpleWindow.prototype = {
buttons: [],
height: 600,
hasFooterLinks: false,
scriptName: null,
/**
* Bring a dialog to the top, when there are other overlapping dialogs open.
*
* @return {Morebits.SimpleWindow}
*/
focus: function() {
if (!this.$dialog.hasClass('morebits-dialog-modal')) {
$('.morebits-dialog:not(.morebits-dialog-modal)').get()
.filter((dialog) => dialog !== this.$dialog[0])
.concat(this.$dialog[0])
.forEach((dialog, idx) => {
dialog.style.zIndex = 501 + idx;
});
}
return this;
},
/**
* Closes the dialog. If this is set as an event handler, it will stop the event
* from doing anything more.
*
* @param {event} [event]
* @return {Morebits.SimpleWindow}
*/
close: function(event) {
if (event) {
event.preventDefault();
}
this.setModality(false);
this.$dialog.remove();
return this;
},
/**
* Shows the dialog. Calling display() on a dialog that has previously been closed
* might work, but it is not guaranteed.
*
* @return {Morebits.SimpleWindow}
*/
display: function() {
if (this.scriptName) {
const $titleBar = this.$dialog.find('.morebits-dialog-title');
$titleBar.find('.morebits-dialog-scriptname').remove();
$titleBar.prepend($('<span>')
.addClass('morebits-dialog-scriptname')
.text(this.scriptName + ' \u00B7 ') // U+00B7 MIDDLE DOT = ·
);
}
if (this.$dialog.find('.morebits-scrollbox').length) {
// quickform needs some extra CSS if it happens to contain a scrollbox.
// CSS :has() selector can be used in the future which is easier and more reliable.
this.$dialog.find('.quickform').addClass('has-scrollbox');
}
this.$dialog.appendTo(document.body);
// Put focus on the first form element in the dialog, or on dialog itself.
const $firstField = this.$dialog.find('input, select, textarea').first();
const $firstFieldOrDialog = $firstField.length ? $firstField : this.$dialog;
$firstFieldOrDialog.trigger('focus');
return this;
},
/**
* Sets the dialog title.
*
* @param {string} title
* @return {Morebits.SimpleWindow}
*/
setTitle: function(title) {
this.$dialog.find('.morebits-dialog-title').text(title);
return this;
},
/**
* Sets the script name, appearing as a prefix to the title to help users determine which
* user script is producing which dialog. For instance, Twinkle modules set this to "Twinkle".
*
* @param {string} name
* @return {Morebits.SimpleWindow}
*/
setScriptName: function(name) {
this.scriptName = name;
return this;
},
/**
* Sets the dialog width.
*
* @param {number} width
* @return {Morebits.SimpleWindow}
*/
setWidth: function(width) {
this.$dialog.css('width', width + 'px');
return this;
},
/**
* Sets the dialog's maximum height. The dialog will auto-size to fit its contents.
*
* @param {number} height
* @return {Morebits.SimpleWindow}
*/
setHeight: function(height) {
this.$dialog.css('height', 'auto');
this.$dialog.css('max-height', (height + 20) + 'px');
return this;
},
/**
* Sets the content of the dialog to the given element node, usually from rendering
* a {@link Morebits.QuickForm}.
* Re-enumerates the footer buttons, but leaves the footer links as they are.
* Be sure to call this at least once before the dialog is displayed...
*
* @param {HTMLElement} content
* @return {Morebits.SimpleWindow}
*/
setContent: function(content) {
this.purgeContent();
this.addContent(content);
return this;
},
/**
* Adds the given element node to the dialog content.
*
* @param {HTMLElement} content
* @return {Morebits.SimpleWindow}
*/
addContent: function(content) {
this.content.appendChild(content);
// look for submit buttons in the content, hide them, and add a proxy button to the button pane
$(this.content).find('input[type="submit"], button[type="submit"]').get().forEach((node) => {
node.style.display = 'none';
const button = document.createElement('button');
button.textContent = node.getAttribute('value') || node.textContent || '送信';
button.className = node.className || 'submitButtonProxy';
button.addEventListener('click', () => node.click(), false);
this.buttons.push(button);
});
// remove all buttons from the button pane and re-add them
this.$dialog.find('.morebits-dialog-buttons')
.empty()
.append(...this.buttons)
// Set data-empty attribute only if there are no buttons. Used by CSS.
.attr('data-empty', this.buttons.length ? null : 'data-empty');
return this;
},
/**
* Removes all contents from the dialog, barring any footer links.
*
* @return {Morebits.SimpleWindow}
*/
purgeContent: function() {
this.buttons = [];
// delete all buttons in the buttonpane
this.$dialog.find('.morebits-dialog-buttons').empty();
$(this.content).empty();
return this;
},
/**
* Adds a link in the bottom-right corner of the dialog.
* This can be used to provide help or policy links.
* For example, Twinkle's CSD module adds a link to the CSD policy page,
* as well as a link to Twinkle's documentation.
*
* @param {string} text - Display text.
* @param {string} wikiPage - Link target.
* @return {Morebits.SimpleWindow}
*/
addFooterLink: function(text, wikiPage) {
const $footerlinks = this.$dialog.find('.morebits-dialog-footerlinks');
if (this.hasFooterLinks) {
const bullet = document.createElement('span');
bullet.textContent = ' \u2022 '; // U+2022 BULLET
$footerlinks.append(bullet);
}
const link = document.createElement('a');
link.setAttribute('href', mw.util.getUrl(wikiPage));
link.setAttribute('title', wikiPage);
link.setAttribute('target', '_blank');
link.textContent = text;
$footerlinks.append(link);
this.hasFooterLinks = true;
return this;
},
/**
* Sets whether the window should be modal or not. Modal dialogs create
* an overlay below the dialog but above other page elements. This
* must be used (if necessary) before calling display().
*
* @param {boolean} [modal=false] - If set to true, other items on the
* page will be disabled, i.e., cannot be interacted with.
* @return {Morebits.SimpleWindow}
*/
setModality: function(modal) {
if (modal) {
$('<div>').addClass('morebits-dialog-overlay').appendTo(document.body);
this.$dialog.addClass('morebits-dialog-modal');
} else {
$('.morebits-dialog-overlay').remove();
this.$dialog.removeClass('morebits-dialog-modal');
}
return this;
}
};
/**
* Enables or disables all footer buttons on all {@link Morebits.SimpleWindow}s in the current page.
* This should be called with `false` when the button(s) become irrelevant (e.g. just before
* {@link Morebits.Status.init} is called).
* This is not an instance method so that consumers don't have to keep a reference to the
* original `Morebits.SimpleWindow` object sitting around somewhere. Anyway, most of the time
* there will only be one `Morebits.SimpleWindow` open, so this shouldn't matter.
*
* @memberof Morebits.SimpleWindow
* @param {boolean} enabled
*/
Morebits.SimpleWindow.setButtonsEnabled = function(enabled) {
$('.morebits-dialog-buttons button').prop('disabled', !enabled);
};
// Deprecated class name aliases in camelCase. Please use the PascalCase names instead.
Morebits.batchOperation = Morebits.BatchOperation;
Morebits.date = Morebits.Date;
Morebits.quickForm = Morebits.QuickForm;
Morebits.quickForm.element = Morebits.QuickForm.Element;
Morebits.simpleWindow = Morebits.SimpleWindow;
Morebits.status = Morebits.Status;
Morebits.taskManager = Morebits.TaskManager;
Morebits.unbinder = Morebits.Unbinder;
Morebits.userspaceLogger = Morebits.UserspaceLogger;
Morebits.wiki.api = Morebits.wiki.Api;
Morebits.wiki.page = Morebits.wiki.Page;
Morebits.wiki.preview = Morebits.wiki.Preview;
Morebits.wikitext.page = Morebits.wikitext.Page;
}());
/**
* If this script is being executed outside a ResourceLoader context, we add some
* global assignments for legacy scripts, hopefully these can be removed down the line.
*
* IMPORTANT NOTE:
* PLEASE DO NOT USE THESE ALIASES IN NEW CODE!
* Thanks.
*/
if (typeof arguments === 'undefined') { // typeof is here for a reason...
/* global Morebits */
window.SimpleWindow = Morebits.SimpleWindow;
window.QuickForm = Morebits.QuickForm;
window.Wikipedia = Morebits.wiki;
window.Status = Morebits.Status;
}
// </nowiki>
bcjv33do4z2hvyem8awbjl8tt5f5r1d
769553
769523
2026-10-22T01:10:01Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at ff10bbf0c6a2cbb87decbc1d177b18916f75af77: キャッシュ処理
769553
javascript
text/javascript
// <nowiki>
/**
* A library full of lots of goodness for user scripts on MediaWiki wikis, including Wikipedia.
*
* The highlights include:
* - {@link Morebits.wiki.Api} - make calls to the MediaWiki API
* - {@link Morebits.wiki.Page} - modify pages on the wiki (edit, revert, delete, etc.)
* - {@link Morebits.Date} - enhanced date object processing, sort of a light moment.js
* - {@link Morebits.QuickForm} - generate quick HTML forms on the fly
* - {@link Morebits.SimpleWindow} - generate dialog windows and modals
* - {@link Morebits.Status} - a rough-and-ready status message displayer, used by the Morebits.wiki classes
* - {@link Morebits.wikitext} - utilities for dealing with wikitext
* - {@link Morebits.string} - utilities for manipulating strings
* - {@link Morebits.array} - utilities for manipulating arrays
* - {@link Morebits.ip} - utilities to help process IP addresses
*
* Dependencies:
* - The whole thing relies on jQuery. But most wikis should provide this by default.
* - {@link Morebits.QuickForm}, {@link Morebits.SimpleWindow}, and {@link Morebits.Status} rely on the "morebits.css" file for their styling.
* - To create a gadget based on morebits.js, use this syntax in MediaWiki:Gadgets-definition:
* - `*GadgetName[ResourceLoader|dependencies=mediawiki.user,mediawiki.util,mediawiki.Title]|morebits.js|morebits.css|GadgetName.js`
* - Alternatively, you can configure morebits.js as a hidden gadget in MediaWiki:Gadgets-definition:
* - `*morebits[ResourceLoader|dependencies=mediawiki.user,mediawiki.util,mediawiki.Title|hidden]|morebits.js|morebits.css`
* and then load ext.gadget.morebits as one of the dependencies for the new gadget.
*
* All the stuff here works on all browsers for which MediaWiki provides JavaScript support.
*
* This library is maintained by the maintainers of Twinkle.
* For queries, suggestions, help, etc., head to [Wikipedia talk:Twinkle on English Wikipedia](http://en.wikipedia.org/wiki/WT:TW).
* The latest development source is available at {@link https://github.com/wikimedia-gadgets/twinkle/blob/master/src/morebits.js|GitHub}.
*
* @namespace Morebits
*/
(function() {
/** @lends Morebits */
const Morebits = {};
window.Morebits = Morebits; // allow global access
/**
* Wiki-specific configurations for Morebits
*/
Morebits.l10n = {
/**
* Local aliases for "redirect" magic word.
* Check using api.php?action=query&format=json&meta=siteinfo&formatversion=2&siprop=magicwords
*/
redirectTagAliases: ['#REDIRECT'],
/**
* Takes a string as argument and checks if it is a timestamp or not
* If not, it returns null. If yes, it returns an array of integers
* in the format [year, month, date, hour, minute, second]
* which can be passed to Date.UTC()
*
* @param {string} str
* @return {number[] | null}
*/
signatureTimestampFormat: function(str) {
// Japanese Wikipedia signatures use UTC with year/month/day before the time.
const japanese = /(\d{4})年(\d{1,2})月(\d{1,2})日\s*\([日月火水木金土]\)\s*(\d{1,2}):(\d{2})\s*\(UTC\)/.exec(str);
if (japanese) {
return [japanese[1], japanese[2] - 1, japanese[3], japanese[4], japanese[5]];
}
// HH:mm, DD Month YYYY (UTC)
const rgx = /(\d{2}):(\d{2}), (\d{1,2}) (\w+) (\d{4}) \(UTC\)/;
const match = rgx.exec(str);
if (!match) {
return null;
}
const month = ['January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'].indexOf(match[4]);
if (month === -1) {
return null;
}
// ..... year ... month .. date ... hour .... minute
return [match[5], month, match[3], match[1], match[2]];
}
};
/**
* Simple helper function to see what groups a user might belong.
*
* @param {string} group - e.g. `sysop`, `extendedconfirmed`, etc.
* @return {boolean}
*/
Morebits.userIsInGroup = function(group) {
return mw.config.get('wgUserGroups').includes(group);
};
/**
* Hardcodes whether the user is a sysop, used a lot.
*
* @type {boolean}
*/
Morebits.userIsSysop = Morebits.userIsInGroup('sysop');
/**
* Determines whether the current page is a redirect or soft redirect. Fails
* to detect soft redirects on edit, history, etc. pages. Will attempt to
* detect Module:RfD, with the same failure points.
*
* @return {boolean}
*/
Morebits.isPageRedirect = function() {
return !!(mw.config.get('wgIsRedirect') || document.getElementById('softredirect') || $('.box-RfD').length);
};
/**
* Stores a normalized (underscores converted to spaces) version of the
* `wgPageName` variable.
*
* @type {string}
*/
Morebits.pageNameNorm = mw.config.get('wgPageName').replace(/_/g, ' ');
/**
* Create a string for use in regex matching a page name. Accounts for
* leading character's capitalization, underscores as spaces, and special
* characters being escaped. See also {@link Morebits.namespaceRegex}.
*
* @param {string} pageName - Page name without namespace.
* @return {string} - For a page name `Foo bar`, returns the string `[Ff]oo[_ ]bar`.
*/
Morebits.pageNameRegex = function(pageName) {
if (pageName === '') {
return '';
}
const firstChar = pageName[0],
remainder = Morebits.string.escapeRegExp(pageName.slice(1));
if (mw.Title.phpCharToUpper(firstChar) !== firstChar.toLowerCase()) {
return '[' + mw.Title.phpCharToUpper(firstChar) + firstChar.toLowerCase() + ']' + remainder;
}
return Morebits.string.escapeRegExp(firstChar) + remainder;
};
/**
* Converts string or array of DOM nodes into an HTML fragment.
* Wikilink syntax (`[[...]]`) is transformed into HTML anchor.
* Used in Morebits.QuickForm and Morebits.Status
*
* @internal
* @param {string|Node|(string|Node)[]} input
* @return {DocumentFragment}
*/
Morebits.createHtml = function(input) {
const fragment = document.createDocumentFragment();
if (!input) {
return fragment;
}
if (!Array.isArray(input)) {
input = [input];
}
for (let i = 0; i < input.length; ++i) {
if (input[i] instanceof Node) {
fragment.appendChild(input[i]);
} else {
$.parseHTML(Morebits.createHtml.renderWikilinks(input[i])).forEach((node) => {
fragment.appendChild(node);
});
}
}
return fragment;
};
/**
* Converts wikilinks to HTML anchor tags.
*
* @param {string} text
* @return {string}
*/
Morebits.createHtml.renderWikilinks = function(text) {
const ub = new Morebits.Unbinder(text);
// Don't convert wikilinks within code tags as they're used for displaying wiki-code
ub.unbind('<code>', '</code>');
ub.content = ub.content.replace(
/\[\[:?(?:([^|\]]+?)\|)?([^\]|]+?)\]\]/g,
(_, target, text) => {
if (!target) {
target = text;
}
return '<a target="_blank" href="' + mw.util.getUrl(target) +
'" title="' + target.replace(/"/g, '"') + '">' + text + '</a>';
});
return ub.rebind();
};
/**
* Create a string for use in regex matching all namespace aliases, regardless
* of the capitalization and underscores/spaces. Doesn't include the optional
* leading `:`, but if there's more than one item, wraps the list in a
* non-capturing group. This means you can do `Morebits.namespaceRegex([4]) +
* ':' + Morebits.pageNameRegex('Twinkle')` to match a full page. Uses
* {@link Morebits.pageNameRegex}.
*
* @param {number[]} namespaces - Array of namespace numbers. Unused/invalid
* namespace numbers are silently discarded.
* @example
* // returns '(?:[Ff][Ii][Ll][Ee]|[Ii][Mm][Aa][Gg][Ee])'
* Morebits.namespaceRegex([6])
* @return {string} - Regex-suitable string of all namespace aliases.
*/
Morebits.namespaceRegex = function(namespaces) {
if (!Array.isArray(namespaces)) {
namespaces = [namespaces];
}
const aliases = [];
let regex;
$.each(mw.config.get('wgNamespaceIds'), (name, number) => {
if (namespaces.includes(number)) {
// Namespaces are completely agnostic as to case,
// and a regex string is more useful/compatible than a RegExp object,
// so we accept any casing for any letter.
aliases.push(name.split('').map((char) => Morebits.pageNameRegex(char)).join(''));
}
});
switch (aliases.length) {
case 0:
regex = '';
break;
case 1:
regex = aliases[0];
break;
default:
regex = '(?:' + aliases.join('|') + ')';
break;
}
return regex;
};
/* **************** Morebits.QuickForm **************** */
/**
* Creation of simple and standard forms without much specific coding.
*
* @namespace Morebits.QuickForm
* @memberof Morebits
* @class
* @param {event} event - Function to execute when form is submitted.
* @param {string} [eventType=submit] - Type of the event.
*/
Morebits.QuickForm = function QuickForm(event, eventType) {
this.root = new Morebits.QuickForm.Element({ type: 'form', event: event, eventType: eventType });
};
/**
* Renders the HTML output of the quickForm.
*
* @memberof Morebits.QuickForm
* @return {HTMLElement}
*/
Morebits.QuickForm.prototype.render = function QuickFormRender() {
const ret = this.root.render();
ret.names = {};
return ret;
};
/**
* Append element to the form.
*
* @memberof Morebits.QuickForm
* @param {(object|Morebits.QuickForm.Element)} data - A quickform element, or the object with which
* a quickform element is constructed.
* @return {Morebits.QuickForm.Element} - Same as what is passed to the function.
*/
Morebits.QuickForm.prototype.append = function QuickFormAppend(data) {
return this.root.append(data);
};
/**
* Create a new element for the the form.
*
* Index to Morebits.QuickForm.Element types:
* - Global attributes: id, className, style, tooltip, extra, $data, adminonly
* - `select`: A combo box (aka drop-down).
* - Attributes: name, label, multiple, size, list, event, disabled
* - `option`: An element for a combo box.
* - Attributes: value, label, selected, disabled
* - `optgroup`: A group of "option"s.
* - Attributes: label, list
* - `field`: A fieldset (aka group box).
* - Attributes: name, label, disabled
* - `checkbox`: A checkbox. Must use "list" parameter.
* - Attributes: name, list, event
* - Attributes (within list): name, label, value, checked, disabled, event, subgroup
* - `radio`: A radio button. Must use "list" parameter.
* - Attributes: name, list, event
* - Attributes (within list): name, label, value, checked, disabled, event, subgroup
* - `input`: A text input box.
* - Attributes: name, label, value, size, placeholder, maxlength, disabled, required, readonly, event
* - `number`: A number input box.
* - Attributes: Everything the text `input` has, as well as: min, max, step, list
* - `dyninput`: A set of text boxes with "Remove" buttons and an "Add" button.
* - Attributes: name, label, min, max, inputs, sublabel, value, size, maxlength, event
* - `hidden`: An invisible form field.
* - Attributes: name, value
* - `header`: A level 5 header.
* - Attributes: label
* - `div`: A generic placeholder element or label.
* - Attributes: name, label
* - `submit`: A submit button. Morebits.SimpleWindow moves these to the footer of the dialog.
* - Attributes: name, label, disabled
* - `button`: A generic button.
* - Attributes: name, label, disabled, event
* - `textarea`: A big, multi-line text box.
* - Attributes: name, label, value, cols, rows, disabled, required, readonly
* - `fragment`: A DocumentFragment object.
* - No attributes, and no global attributes except adminonly.
* There is some difference on how types handle the `label` attribute:
* - `div`, `select`, `field`, `checkbox`/`radio`, `input`, `textarea`, `header`, and `dyninput` can accept an array of items,
* and the label item(s) can be `Element`s.
* - `option`, `optgroup`, `_dyninput_cell`, `submit`, and `button` accept only a single string.
*
* @memberof Morebits.QuickForm
* @class
* @param {Object} data - Object representing the quickform element. Should
* specify one of the available types from the index above, as well as any
* relevant and available attributes.
* @example new Morebits.QuickForm.Element({
* name: 'target',
* type: 'input',
* label: 'Your target:',
* tooltip: 'Enter your target. Required.',
* required: true
* });
*/
Morebits.QuickForm.Element = function QuickFormElement(data) {
this.data = data;
this.childs = [];
};
/**
* @memberof Morebits.QuickForm.Element
* @type {number}
*/
Morebits.QuickForm.Element.id = 0;
/**
* Appends an element to current element.
*
* @memberof Morebits.QuickForm.Element
* @param {Morebits.QuickForm.Element} data - A quickForm element or the object required to
* create the quickForm element.
* @return {Morebits.QuickForm.Element} The same element passed in.
*/
Morebits.QuickForm.Element.prototype.append = function QuickFormElementAppend(data) {
let child;
if (data instanceof Morebits.QuickForm.Element) {
child = data;
} else {
child = new Morebits.QuickForm.Element(data);
}
this.childs.push(child);
return child;
};
/**
* Renders the HTML output for the quickForm element. This should be called
* without parameters: `form.render()`.
*
* @memberof Morebits.QuickForm.Element
* @return {HTMLElement}
*/
Morebits.QuickForm.Element.prototype.render = function QuickFormElementRender(internal_subgroup_id) {
const currentNode = this.compute(this.data, internal_subgroup_id);
for (let i = 0; i < this.childs.length; ++i) {
// do not pass internal_subgroup_id to recursive calls
currentNode[1].appendChild(this.childs[i].render());
}
return currentNode[0];
};
/** @memberof Morebits.QuickForm.Element */
Morebits.QuickForm.Element.prototype.compute = function QuickFormElementCompute(data, in_id) {
let node;
let childContainer = null;
let label;
const id = (in_id ? in_id + '_' : '') + 'node_' + Morebits.QuickForm.Element.id++;
if (data.adminonly && !Morebits.userIsSysop) {
// hell hack alpha
data.type = 'hidden';
}
let i, current, subnode;
switch (data.type) {
case 'form':
node = document.createElement('form');
node.className = 'quickform';
node.setAttribute('action', 'javascript:void(0);');
if (data.event) {
node.addEventListener(data.eventType || 'submit', data.event, false);
}
break;
case 'fragment':
node = document.createDocumentFragment();
// fragments can't have any attributes, so just return it straight away
return [node, node];
// Sometimes Twinkle uses fancy searchable "select" elements. This is powered by the third party library "select2". Activate it by creating a Morebits "select" element, then call `$('select[name=sub_group]').select2({});` or similar towards the end of your main code.
case 'select':
node = document.createElement('div');
node.setAttribute('id', 'div_' + id);
if (data.label) {
label = node.appendChild(document.createElement('label'));
label.setAttribute('for', id);
label.appendChild(Morebits.createHtml(data.label));
label.style.marginRight = '3px';
}
var select = node.appendChild(document.createElement('select'));
if (data.event) {
select.addEventListener('change', data.event, false);
}
if (data.multiple) {
select.setAttribute('multiple', 'multiple');
}
if (data.size) {
select.setAttribute('size', data.size);
}
if (data.disabled) {
select.setAttribute('disabled', 'disabled');
}
select.setAttribute('name', data.name);
if (data.list) {
for (i = 0; i < data.list.length; ++i) {
current = data.list[i];
if (current.list) {
current.type = 'optgroup';
} else {
current.type = 'option';
}
subnode = this.compute(current);
select.appendChild(subnode[0]);
}
}
childContainer = select;
break;
case 'option':
node = document.createElement('option');
node.values = data.value;
node.setAttribute('value', data.value);
if (data.selected) {
node.setAttribute('selected', 'selected');
}
if (data.disabled) {
node.setAttribute('disabled', 'disabled');
}
node.setAttribute('label', data.label);
node.appendChild(document.createTextNode(data.label));
break;
case 'optgroup':
node = document.createElement('optgroup');
node.setAttribute('label', data.label);
if (data.list) {
for (i = 0; i < data.list.length; ++i) {
current = data.list[i];
current.type = 'option'; // must be options here
subnode = this.compute(current);
node.appendChild(subnode[0]);
}
}
break;
case 'field':
node = document.createElement('fieldset');
label = node.appendChild(document.createElement('legend'));
label.appendChild(Morebits.createHtml(data.label));
if (data.name) {
node.setAttribute('name', data.name);
}
if (data.disabled) {
node.setAttribute('disabled', 'disabled');
}
break;
case 'checkbox':
case 'radio':
node = document.createElement('div');
if (data.list) {
for (i = 0; i < data.list.length; ++i) {
const cur_id = id + '_' + i;
current = data.list[i];
var cur_div;
if (current.type === 'header') {
// inline hack
cur_div = node.appendChild(document.createElement('h6'));
cur_div.appendChild(document.createTextNode(current.label));
if (current.tooltip) {
Morebits.QuickForm.Element.generateTooltip(cur_div, current);
}
continue;
}
cur_div = node.appendChild(document.createElement('div'));
subnode = cur_div.appendChild(document.createElement('input'));
subnode.values = current.value;
subnode.setAttribute('value', current.value);
subnode.setAttribute('type', data.type);
subnode.setAttribute('id', cur_id);
subnode.setAttribute('name', current.name || data.name);
// If name is provided on the individual checkbox, add a data-single
// attribute which indicates it isn't part of a list of checkboxes with
// same name. Used in getInputData()
if (current.name) {
subnode.setAttribute('data-single', 'data-single');
}
if (current.checked) {
subnode.setAttribute('checked', 'checked');
}
if (current.disabled) {
subnode.setAttribute('disabled', 'disabled');
}
label = cur_div.appendChild(document.createElement('label'));
label.appendChild(Morebits.createHtml(current.label));
label.setAttribute('for', cur_id);
if (current.tooltip) {
Morebits.QuickForm.Element.generateTooltip(label, current);
}
// styles go on the label, doesn't make sense to style a checkbox/radio
if (current.style) {
label.setAttribute('style', current.style);
}
var event;
if (current.subgroup) {
let tmpgroup = current.subgroup;
if (!Array.isArray(tmpgroup)) {
tmpgroup = [tmpgroup];
}
var subgroupRaw = new Morebits.QuickForm.Element({
type: 'div',
id: id + '_' + i + '_subgroup'
});
$.each(tmpgroup, (idx, el) => {
const newEl = $.extend({}, el);
if (!newEl.type) {
newEl.type = data.type;
}
newEl.name = (current.name || data.name) + '.' + newEl.name;
subgroupRaw.append(newEl);
});
const subgroup = subgroupRaw.render(cur_id);
subgroup.className = 'quickformSubgroup';
subnode.subgroup = subgroup;
subnode.shown = false;
event = function(e) {
if (e.target.checked) {
e.target.parentNode.appendChild(e.target.subgroup);
if (e.target.type === 'radio') {
const name = e.target.name;
if (e.target.form.names[name] !== undefined) {
e.target.form.names[name].parentNode.removeChild(e.target.form.names[name].subgroup);
}
e.target.form.names[name] = e.target;
}
} else {
e.target.parentNode.removeChild(e.target.subgroup);
}
};
subnode.addEventListener('change', event, true);
if (current.checked) {
subnode.parentNode.appendChild(subgroup);
}
} else if (data.type === 'radio') {
event = function(e) {
if (e.target.checked) {
const name = e.target.name;
if (e.target.form.names[name] !== undefined) {
e.target.form.names[name].parentNode.removeChild(e.target.form.names[name].subgroup);
}
delete e.target.form.names[name];
}
};
subnode.addEventListener('change', event, true);
}
// add users' event last, so it can interact with the subgroup
if (data.event) {
subnode.addEventListener('change', data.event, false);
} else if (current.event) {
subnode.addEventListener('change', current.event, true);
}
}
}
if (data.shiftClickSupport && data.type === 'checkbox') {
Morebits.checkboxShiftClickSupport(Morebits.QuickForm.getElements(node, data.name));
}
break;
// input is actually a text-type, so number here inherits the same stuff
case 'number':
case 'input':
node = document.createElement('div');
node.setAttribute('id', 'div_' + id);
if (data.label) {
label = node.appendChild(document.createElement('label'));
label.appendChild(Morebits.createHtml(data.label));
label.setAttribute('for', data.id || id);
label.style.marginRight = '3px';
}
subnode = node.appendChild(document.createElement('input'));
subnode.setAttribute('name', data.name);
if (data.type === 'input') {
subnode.setAttribute('type', 'text');
} else {
subnode.setAttribute('type', 'number');
['min', 'max', 'step', 'list'].forEach((att) => {
if (data[att]) {
subnode.setAttribute(att, data[att]);
}
});
}
['value', 'size', 'placeholder', 'maxlength'].forEach((att) => {
if (data[att]) {
subnode.setAttribute(att, data[att]);
}
});
['disabled', 'required', 'readonly'].forEach((att) => {
if (data[att]) {
subnode.setAttribute(att, att);
}
});
if (data.event) {
subnode.addEventListener('input', data.event, false);
}
childContainer = subnode;
break;
case 'dyninput':
var min = data.min || 1;
var max = data.max || Infinity;
node = document.createElement('div');
label = node.appendChild(document.createElement('h5'));
label.appendChild(Morebits.createHtml(data.label));
var listNode = node.appendChild(document.createElement('div'));
var more = this.compute({
type: 'button',
label: 'more',
disabled: min >= max,
event: function(e) {
const new_node = new Morebits.QuickForm.Element(e.target.sublist);
e.target.area.appendChild(new_node.render());
if (++e.target.counter >= e.target.max) {
e.target.setAttribute('disabled', 'disabled');
}
e.stopPropagation();
}
});
node.appendChild(more[0]);
var moreButton = more[1];
var sublist = {
type: '_dyninput_row',
remove: false,
maxlength: data.maxlength,
event: data.event,
inputs: data.inputs || [{
// compatibility
label: data.sublabel || data.label,
name: data.name,
value: data.value,
size: data.size
}]
};
for (i = 0; i < min; ++i) {
const elem = new Morebits.QuickForm.Element(sublist);
listNode.appendChild(elem.render());
}
sublist.remove = true;
sublist.morebutton = moreButton;
sublist.listnode = listNode;
moreButton.sublist = sublist;
moreButton.area = listNode;
moreButton.max = max - min;
moreButton.counter = 0;
break;
case '_dyninput_row': // Private
node = document.createElement('div');
data.inputs.forEach((subdata) => {
const cell = new Morebits.QuickForm.Element($.extend(subdata, { type: '_dyninput_cell' }));
node.appendChild(cell.render());
});
if (data.remove) {
const remove = this.compute({
type: 'button',
label: 'remove',
event: function(e) {
const list = e.target.listnode;
const node = e.target.inputnode;
const more = e.target.morebutton;
list.removeChild(node);
--more.counter;
more.removeAttribute('disabled');
e.stopPropagation();
}
});
node.appendChild(remove[0]);
const removeButton = remove[1];
removeButton.inputnode = node;
removeButton.listnode = data.listnode;
removeButton.morebutton = data.morebutton;
}
break;
case '_dyninput_cell': // Private, similar to normal input
node = document.createElement('span');
if (data.label) {
label = node.appendChild(document.createElement('label'));
label.appendChild(document.createTextNode(data.label));
label.setAttribute('for', id + '_input');
label.style.marginRight = '3px';
}
subnode = node.appendChild(document.createElement('input'));
subnode.setAttribute('id', id + '_input');
if (data.value) {
subnode.setAttribute('value', data.value);
}
subnode.setAttribute('name', data.name);
subnode.setAttribute('type', 'text');
subnode.setAttribute('data-dyninput', 'data-dyninput');
if (data.size) {
subnode.setAttribute('size', data.size);
}
if (data.maxlength) {
subnode.setAttribute('maxlength', data.maxlength);
}
if (data.required) {
subnode.setAttribute('required', 'required');
}
if (data.disabled) {
subnode.setAttribute('required', 'disabled');
}
if (data.event) {
subnode.addEventListener('input', data.event, false);
}
node.style.marginRight = '3px';
break;
case 'hidden':
node = document.createElement('input');
node.setAttribute('type', 'hidden');
node.values = data.value;
node.setAttribute('value', data.value);
node.setAttribute('name', data.name);
break;
case 'header':
node = document.createElement('h5');
node.appendChild(Morebits.createHtml(data.label));
break;
case 'div':
node = document.createElement('div');
if (data.name) {
node.setAttribute('name', data.name);
}
if (data.label) {
const result = document.createElement('span');
result.className = 'quickformDescription';
result.appendChild(Morebits.createHtml(data.label));
node.appendChild(result);
}
break;
case 'submit':
node = document.createElement('span');
childContainer = node.appendChild(document.createElement('input'));
childContainer.setAttribute('type', 'submit');
if (data.label) {
childContainer.setAttribute('value', data.label);
}
childContainer.setAttribute('name', data.name || 'submit');
if (data.disabled) {
childContainer.setAttribute('disabled', 'disabled');
}
break;
case 'button':
node = document.createElement('span');
childContainer = node.appendChild(document.createElement('input'));
childContainer.setAttribute('type', 'button');
if (data.label) {
childContainer.setAttribute('value', data.label);
}
childContainer.setAttribute('name', data.name);
if (data.disabled) {
childContainer.setAttribute('disabled', 'disabled');
}
if (data.event) {
childContainer.addEventListener('click', data.event, false);
}
break;
case 'textarea':
node = document.createElement('div');
node.setAttribute('id', 'div_' + id);
if (data.label) {
label = node.appendChild(document.createElement('h5'));
const labelElement = document.createElement('label');
labelElement.appendChild(Morebits.createHtml(data.label));
labelElement.setAttribute('for', data.id || id);
label.appendChild(labelElement);
}
subnode = node.appendChild(document.createElement('textarea'));
subnode.setAttribute('name', data.name);
if (data.cols) {
subnode.setAttribute('cols', data.cols);
}
if (data.rows) {
subnode.setAttribute('rows', data.rows);
}
if (data.disabled) {
subnode.setAttribute('disabled', 'disabled');
}
if (data.required) {
subnode.setAttribute('required', 'required');
}
if (data.readonly) {
subnode.setAttribute('readonly', 'readonly');
}
if (data.value) {
subnode.value = data.value;
}
childContainer = subnode;
break;
default:
throw new Error('Morebits.QuickForm: unknown element type ' + data.type.toString());
}
if (!childContainer) {
childContainer = node;
}
if (data.tooltip) {
Morebits.QuickForm.Element.generateTooltip(label || node, data);
}
if (data.extra) {
childContainer.extra = data.extra;
}
if (data.$data) {
$(childContainer).data(data.$data);
}
if (data.style) {
childContainer.setAttribute('style', data.style);
}
if (data.className) {
childContainer.className = childContainer.className ?
childContainer.className + ' ' + data.className :
data.className;
}
childContainer.setAttribute('id', data.id || id);
return [node, childContainer];
};
Morebits.QuickForm.$tooltip = null;
/**
* Create a tooltip.
*
* @memberof Morebits.QuickForm.Element
* @param {HTMLElement} node - The HTML element beside which a tooltip is to be generated.
* @param {Object} data - Tooltip-related configuration data.
*/
Morebits.QuickForm.Element.generateTooltip = function QuickFormElementGenerateTooltip(node, data) {
if (!Morebits.QuickForm.$tooltip) {
Morebits.QuickForm.$tooltip = $('<div>')
.attr('id', 'morebits-ui-tooltip')
.attr('role', 'tooltip')
.addClass('morebits-ui-tooltip')
.appendTo('body');
}
const $tooltip = Morebits.QuickForm.$tooltip;
const $button = $('<span>')
.addClass('morebits-tooltipButton')
.text('?')
.appendTo(node);
$button.on('mouseenter', () => {
$tooltip.html(data.tooltip).addClass('visible');
const buttonRect = $button[0].getBoundingClientRect();
const tooltipRect = $tooltip[0].getBoundingClientRect();
const topOffset = buttonRect.bottom + tooltipRect.height < window.innerHeight ?
buttonRect.bottom : // It fits at the top of the question mark - place it there
Math.max(0, buttonRect.top - tooltipRect.height); // Else, place to the bottom of the question mark
const leftOffset = buttonRect.right + tooltipRect.width < window.innerWidth ?
buttonRect.right : // It fits at the right of the question mark - place it there
Math.max(0, buttonRect.left - tooltipRect.width); // Else, place to the left of the question mark
$tooltip.css('top', window.scrollY + topOffset);
$tooltip.css('left', window.scrollX + leftOffset);
}).on('mouseleave', () => {
$tooltip.removeClass('visible');
});
};
// Some utility methods for manipulating quickForms after their creation:
// (None of these work for "dyninput" type fields at present)
/**
* Returns an object containing all filled form data entered by the user, with the object
* keys being the form element names. Disabled fields will be ignored, but not hidden fields.
*
* @memberof Morebits.QuickForm
* @param {HTMLFormElement} form
* @return {Object} With field names as keys, input data as values.
*/
Morebits.QuickForm.getInputData = function(form) {
const result = {};
for (let i = 0; i < form.elements.length; i++) {
const field = form.elements[i];
if (field.disabled || !field.name || !field.type ||
field.type === 'submit' || field.type === 'button') {
continue;
}
// For elements in subgroups, quickform prepends element names with
// name of the parent group followed by a period, get rid of that.
const fieldNameNorm = field.name.slice(field.name.indexOf('.') + 1);
switch (field.type) {
case 'radio':
if (field.checked) {
result[fieldNameNorm] = field.value;
}
break;
case 'checkbox':
if (field.dataset.single) {
result[fieldNameNorm] = field.checked; // boolean
} else {
result[fieldNameNorm] = result[fieldNameNorm] || [];
if (field.checked) {
result[fieldNameNorm].push(field.value);
}
}
break;
case 'select-multiple':
result[fieldNameNorm] = $(field).val(); // field.value doesn't work
break;
case 'text': // falls through
case 'textarea':
if (field.dataset.dyninput) {
result[fieldNameNorm] = result[fieldNameNorm] || [];
result[fieldNameNorm].push(field.value.trim());
} else {
result[fieldNameNorm] = field.value.trim();
}
break;
default: // could be select-one, date, number, email, etc
if (field.value) {
result[fieldNameNorm] = field.value;
}
break;
}
}
return result;
};
/**
* Returns all form elements with a given field name or ID.
*
* @memberof Morebits.QuickForm
* @param {HTMLFormElement} form
* @param {string} fieldName - The name or id of the fields.
* @return {HTMLElement[]} - Array of matching form elements.
*/
Morebits.QuickForm.getElements = function QuickFormGetElements(form, fieldName) {
const $form = $(form);
fieldName = $.escapeSelector(fieldName); // sanitize input
let $elements = $form.find('[name="' + fieldName + '"]');
if ($elements.length > 0) {
return $elements.toArray();
}
$elements = $form.find('#' + fieldName);
return $elements.toArray();
};
/**
* Searches the array of elements for a checkbox or radio button with a certain
* `value` attribute, and returns the first such element. Returns null if not found.
*
* @memberof Morebits.QuickForm
* @param {HTMLInputElement[]} elementArray - Array of checkbox or radio elements.
* @param {string} value - Value to search for.
* @return {HTMLInputElement}
*/
Morebits.QuickForm.getCheckboxOrRadio = function QuickFormGetCheckboxOrRadio(elementArray, value) {
const found = $.grep(elementArray, (el) => el.value === value);
if (found.length > 0) {
return found[0];
}
return null;
};
/**
* Returns the <div> containing the form element, or the form element itself
* May not work as expected on checkboxes or radios.
*
* @memberof Morebits.QuickForm
* @param {HTMLElement} element
* @return {HTMLElement}
*/
Morebits.QuickForm.getElementContainer = function QuickFormGetElementContainer(element) {
// for divs, headings and fieldsets, the container is the element itself
if (element instanceof HTMLFieldSetElement || element instanceof HTMLDivElement ||
element instanceof HTMLHeadingElement) {
return element;
}
// for others, just return the parent node
return element.parentNode;
};
/**
* Gets the HTML element that contains the label of the given form element
* (mainly for internal use).
*
* @memberof Morebits.QuickForm
* @param {(HTMLElement|Morebits.QuickForm.Element)} element
* @return {HTMLElement}
*/
Morebits.QuickForm.getElementLabelObject = function QuickFormGetElementLabelObject(element) {
// for buttons, divs and headers, the label is on the element itself
if (element.type === 'button' || element.type === 'submit' ||
element instanceof HTMLDivElement || element instanceof HTMLHeadingElement) {
return element;
// for fieldsets, the label is the child <legend> element
} else if (element instanceof HTMLFieldSetElement) {
return element.getElementsByTagName('legend')[0];
// for textareas, the label is the sibling <h5> element
} else if (element instanceof HTMLTextAreaElement) {
return element.parentNode.getElementsByTagName('h5')[0];
}
// for others, the label is the sibling <label> element
return element.parentNode.getElementsByTagName('label')[0];
};
/**
* Gets the label text of the element.
*
* @memberof Morebits.QuickForm
* @param {(HTMLElement|Morebits.QuickForm.Element)} element
* @return {string}
*/
Morebits.QuickForm.getElementLabel = function QuickFormGetElementLabel(element) {
const labelElement = Morebits.QuickForm.getElementLabelObject(element);
if (!labelElement) {
return null;
}
return labelElement.firstChild.textContent;
};
/**
* Sets the label of the element to the given text.
*
* @memberof Morebits.QuickForm
* @param {(HTMLElement|Morebits.QuickForm.Element)} element
* @param {string} labelText
* @return {boolean} True if succeeded, false if the label element is unavailable.
*/
Morebits.QuickForm.setElementLabel = function QuickFormSetElementLabel(element, labelText) {
const labelElement = Morebits.QuickForm.getElementLabelObject(element);
if (!labelElement) {
return false;
}
labelElement.firstChild.textContent = labelText;
return true;
};
/**
* Stores the element's current label, and temporarily sets the label to the given text.
*
* @memberof Morebits.QuickForm
* @param {(HTMLElement|Morebits.QuickForm.Element)} element
* @param {string} temporaryLabelText
* @return {boolean} `true` if succeeded, `false` if the label element is unavailable.
*/
Morebits.QuickForm.overrideElementLabel = function QuickFormOverrideElementLabel(element, temporaryLabelText) {
if (!element.hasAttribute('data-oldlabel')) {
element.setAttribute('data-oldlabel', Morebits.QuickForm.getElementLabel(element));
}
return Morebits.QuickForm.setElementLabel(element, temporaryLabelText);
};
/**
* Restores the label stored by overrideElementLabel.
*
* @memberof Morebits.QuickForm
* @param {(HTMLElement|Morebits.QuickForm.Element)} element
* @return {boolean} True if succeeded, false if the label element is unavailable.
*/
Morebits.QuickForm.resetElementLabel = function QuickFormResetElementLabel(element) {
if (element.hasAttribute('data-oldlabel')) {
return Morebits.QuickForm.setElementLabel(element, element.getAttribute('data-oldlabel'));
}
return null;
};
/**
* Shows or hides a form element plus its label and tooltip.
*
* @memberof Morebits.QuickForm
* @param {(HTMLElement|jQuery|string)} element - HTML/jQuery element, or jQuery selector string.
* @param {boolean} [visibility] - Skip this to toggle visibility.
*/
Morebits.QuickForm.setElementVisibility = function QuickFormSetElementVisibility(element, visibility) {
$(element).toggle(visibility);
};
/**
* Shows or hides the question mark icon (which displays the tooltip) next to a form element.
*
* @memberof Morebits.QuickForm
* @param {(HTMLElement|jQuery)} element
* @param {boolean} [visibility] - Skip this to toggle visibility.
*/
Morebits.QuickForm.setElementTooltipVisibility = function QuickFormSetElementTooltipVisibility(element, visibility) {
$(Morebits.QuickForm.getElementContainer(element)).find('.morebits-tooltipButton').toggle(visibility);
};
/**
* @external HTMLFormElement
*/
/**
* Get checked items in the form.
*
* @method external:HTMLFormElement.getChecked
* @param {string} name - Find checked property of elements (i.e. a checkbox
* or a radiobutton) with the given name, or select options that have selected
* set to true (don't try to mix selects with radio/checkboxes).
* @param {string} [type] - Optionally specify either radio or checkbox (for
* the event that both checkboxes and radiobuttons have the same name).
* @return {string[]} - Contains the values of elements with the given name
* checked property set to true.
*/
HTMLFormElement.prototype.getChecked = function(name, type) {
const elements = this.elements[name];
if (!elements) {
return [];
}
const return_array = [];
let i;
if (elements instanceof HTMLSelectElement) {
const options = elements.options;
for (i = 0; i < options.length; ++i) {
if (options[i].selected) {
if (options[i].values) {
return_array.push(options[i].values);
} else {
return_array.push(options[i].value);
}
}
}
} else if (elements instanceof HTMLInputElement) {
if (type && elements.type !== type) {
return [];
} else if (elements.checked) {
return [elements.value];
}
} else {
for (i = 0; i < elements.length; ++i) {
if (elements[i].checked) {
if (type && elements[i].type !== type) {
continue;
}
if (elements[i].values) {
return_array.push(elements[i].values);
} else {
return_array.push(elements[i].value);
}
}
}
}
return return_array;
};
/**
* Does the same as {@link HTMLFormElement.getChecked|getChecked}, but with unchecked elements.
*
* @method external:HTMLFormElement.getUnchecked
* @param {string} name - Find checked property of elements (i.e. a checkbox
* or a radiobutton) with the given name, or select options that have selected
* set to true (don't try to mix selects with radio/checkboxes).
* @param {string} [type] - Optionally specify either radio or checkbox (for
* the event that both checkboxes and radiobuttons have the same name).
* @return {string[]} - Contains the values of elements with the given name
* checked property set to true.
*/
HTMLFormElement.prototype.getUnchecked = function(name, type) {
const elements = this.elements[name];
if (!elements) {
return [];
}
const return_array = [];
let i;
if (elements instanceof HTMLSelectElement) {
const options = elements.options;
for (i = 0; i < options.length; ++i) {
if (!options[i].selected) {
if (options[i].values) {
return_array.push(options[i].values);
} else {
return_array.push(options[i].value);
}
}
}
} else if (elements instanceof HTMLInputElement) {
if (type && elements.type !== type) {
return [];
} else if (!elements.checked) {
return [elements.value];
}
} else {
for (i = 0; i < elements.length; ++i) {
if (!elements[i].checked) {
if (type && elements[i].type !== type) {
continue;
}
if (elements[i].values) {
return_array.push(elements[i].values);
} else {
return_array.push(elements[i].value);
}
}
}
}
return return_array;
};
/**
* Utilities to help process IP addresses.
*
* @namespace Morebits.ip
* @memberof Morebits
*/
Morebits.ip = {
/**
* Converts an IPv6 address to the canonical form stored and used by MediaWiki.
* JavaScript translation of the {@link https://gerrit.wikimedia.org/r/plugins/gitiles/mediawiki/core/+/8eb6ac3e84ea3312d391ca96c12c49e3ad0753bb/includes/utils/IP.php#131|`IP::sanitizeIP()`}
* function from the IPUtils library. Addresses are verbose, uppercase,
* normalized, and expanded to 8 words.
*
* @param {string} address - The IPv6 address, with or without CIDR.
* @return {string}
*/
sanitizeIPv6: function(address) {
address = address.trim();
if (address === '') {
return null;
}
if (!mw.util.isIPv6Address(address, true)) {
return address; // nothing else to do for IPv4 addresses or invalid ones
}
// Remove any whitespaces, convert to upper case
address = address.toUpperCase();
// Expand zero abbreviations
const abbrevPos = address.indexOf('::');
if (abbrevPos > -1) {
// We know this is valid IPv6. Find the last index of the
// address before any CIDR number (e.g. "a:b:c::/24").
const CIDRStart = address.indexOf('/');
const addressEnd = CIDRStart !== -1 ? CIDRStart - 1 : address.length - 1;
// If the '::' is at the beginning...
let repeat, extra, pad;
if (abbrevPos === 0) {
repeat = '0:';
extra = address === '::' ? '0' : ''; // for the address '::'
pad = 9; // 7+2 (due to '::')
// If the '::' is at the end...
} else if (abbrevPos === (addressEnd - 1)) {
repeat = ':0';
extra = '';
pad = 9; // 7+2 (due to '::')
// If the '::' is in the middle...
} else {
repeat = ':0';
extra = ':';
pad = 8; // 6+2 (due to '::')
}
let replacement = repeat;
pad -= address.split(':').length - 1;
for (let i = 1; i < pad; i++) {
replacement += repeat;
}
replacement += extra;
address = address.replace('::', replacement);
}
// Remove leading zeros from each bloc as needed
return address.replace(/(^|:)0+([0-9A-Fa-f]{1,4})/g, '$1$2');
},
/**
* Determine if the given IP address is a range. Just conjoins
* `mw.util.isIPAddress` with and without the `allowBlock` option.
*
* @param {string} ip
* @return {boolean} - True if given a valid IP address range, false otherwise.
*/
isRange: function(ip) {
return mw.util.isIPAddress(ip, true) && !mw.util.isIPAddress(ip);
},
/**
* Check that an IP range is within the CIDR limits. Most likely to be useful
* in conjunction with `wgRelevantUserName`. CIDR limits are hardcoded as /16
* for IPv4 and /32 for IPv6.
*
* @return {boolean} - True for valid ranges within the CIDR limits,
* otherwise false (ranges outside the limit, single IPs, non-IPs).
*/
validCIDR: function(ip) {
if (Morebits.ip.isRange(ip)) {
const subnet = parseInt(ip.match(/\/(\d{1,3})$/)[1], 10);
if (subnet) { // Should be redundant
if (mw.util.isIPv6Address(ip, true)) {
if (subnet >= 32) {
return true;
}
} else {
if (subnet >= 16) {
return true;
}
}
}
}
return false;
},
/**
* Get the /64 subnet for an IPv6 address.
*
* @param {string} ipv6 - The IPv6 address, with or without a subnet.
* @return {boolean|string} - False if not IPv6 or bigger than a 64,
* otherwise the (sanitized) /64 address.
*/
get64: function(ipv6) {
if (!ipv6 || !mw.util.isIPv6Address(ipv6, true)) {
return false;
}
const subnetMatch = ipv6.match(/\/(\d{1,3})$/);
if (subnetMatch && parseInt(subnetMatch[1], 10) < 64) {
return false;
}
ipv6 = Morebits.ip.sanitizeIPv6(ipv6);
const ip_re = /^((?:[0-9A-F]{1,4}:){4})(?:[0-9A-F]{1,4}:){3}[0-9A-F]{1,4}(?:\/\d{1,3})?$/;
// eslint-disable-next-line no-useless-concat
return ipv6.replace(ip_re, '$1' + '0:0:0:0/64');
}
};
/**
* Helper functions to manipulate strings.
*
* @namespace Morebits.string
* @memberof Morebits
*/
Morebits.string = {
/**
* @param {string} str
* @return {string}
*/
toUpperCaseFirstChar: function(str) {
str = str.toString();
return str.slice(0, 1).toUpperCase() + str.slice(1);
},
/**
* @param {string} str
* @return {string}
*/
toLowerCaseFirstChar: function(str) {
str = str.toString();
return str.slice(0, 1).toLowerCase() + str.slice(1);
},
/**
* Gives an array of substrings of `str` - starting with `start` and
* ending with `end` - which is not in `skiplist`. Intended for use
* on wikitext with templates or links.
*
* @param {string} str
* @param {string} start
* @param {string} end
* @param {(string[]|string)} [skiplist]
* @return {string[]}
* @throws {Error} If the `start` and `end` strings aren't of the same length.
* @throws {Error} If `skiplist` isn't an array or string
*/
splitWeightedByKeys: function(str, start, end, skiplist) {
if (start.length !== end.length) {
throw new Error('start marker and end marker must be of the same length');
}
let level = 0;
let initial = null;
const result = [];
if (!Array.isArray(skiplist)) {
if (skiplist === undefined) {
skiplist = [];
} else if (typeof skiplist === 'string') {
skiplist = [skiplist];
} else {
throw new Error('non-applicable skiplist parameter');
}
}
for (let i = 0; i < str.length; ++i) {
for (let j = 0; j < skiplist.length; ++j) {
if (str.substr(i, skiplist[j].length) === skiplist[j]) {
i += skiplist[j].length - 1;
continue;
}
}
if (str.substr(i, start.length) === start) {
if (initial === null) {
initial = i;
}
++level;
i += start.length - 1;
} else if (str.substr(i, end.length) === end) {
--level;
i += end.length - 1;
}
if (!level && initial !== null) {
result.push(str.substring(initial, i + 1));
initial = null;
}
}
return result;
},
/**
* Formats freeform "reason" (from a textarea) for deletion/other
* templates that are going to be substituted, (e.g. PROD, XFD, RPP).
* Handles `|` outside a nowiki tag.
* Optionally, also adds a signature if not present already.
*
* @param {string} str
* @param {boolean} [addSig]
* @return {string}
*/
formatReasonText: function(str, addSig) {
let reason = (str || '').toString().trim();
const unbinder = new Morebits.Unbinder(reason);
// eslint-disable-next-line no-useless-concat
unbinder.unbind('<no' + 'wiki>', '</no' + 'wiki>');
unbinder.content = unbinder.content.replace(/\|/g, '{{subst:!}}');
reason = unbinder.rebind();
if (addSig) {
const sig = '~~~~', sigIndex = reason.lastIndexOf(sig);
if (sigIndex === -1 || sigIndex !== reason.length - sig.length) {
reason += ' ' + sig;
}
}
return reason.trim();
},
/**
* Formats a "reason" (from a textarea) for inclusion in a userspace
* log. Replaces newlines with {{Pb}}, and adds an extra `#` before
* list items for proper formatting.
*
* @param {string} str
* @return {string}
*/
formatReasonForLog: function(str) {
return str
// handle line breaks, which otherwise break numbering
.replace(/\n+/g, '{{pb}}')
// put an extra # in front before bulleted or numbered list items
.replace(/^(#+)/mg, '#$1')
.replace(/^(\*+)/mg, '#$1');
},
/**
* Like `String.prototype.replace()`, but escapes any dollar signs in
* the replacement string. Useful when the the replacement string is
* arbitrary, such as a username or freeform user input, and could
* contain dollar signs.
*
* @param {string} string - Text in which to replace.
* @param {(string|RegExp)} pattern
* @param {string} replacement
* @return {string}
*/
safeReplace: function morebitsStringSafeReplace(string, pattern, replacement) {
return string.replace(pattern, replacement.replace(/\$/g, '$$$$'));
},
/**
* Determine if the user-provided expiration will be considered an
* infinite-length by MW.
*
* @see {@link https://phabricator.wikimedia.org/T68646}
*
* @param {string} expiry
* @return {boolean}
*/
isInfinity: function morebitsStringIsInfinity(expiry) {
return ['indefinite', 'infinity', 'infinite', 'never'].includes(expiry);
},
/**
* Escapes a string to be used in a RegExp, replacing spaces and
* underscores with `[_ ]` as they are often equivalent.
*
* @param {string} text - String to be escaped.
* @return {string} - The escaped text.
*/
escapeRegExp: function(text) {
return mw.util.escapeRegExp(text).replace(/ |_/g, '[_ ]');
}
};
/**
* Helper functions to manipulate arrays.
*
* @namespace Morebits.array
* @memberof Morebits
*/
Morebits.array = {
/**
* Remove duplicated items from an array.
*
* @param {Array} arr
* @return {Array} A copy of the array with duplicates removed.
* @throws {Error} When provided a non-array.
*/
uniq: function(arr) {
if (!Array.isArray(arr)) {
throw new Error('A non-array object passed to Morebits.array.uniq');
}
return arr.filter((item, idx) => arr.indexOf(item) === idx);
},
/**
* Remove non-duplicated items from an array.
*
* @param {Array} arr
* @return {Array} A copy of the array with the first instance of each value
* removed; subsequent instances of those values (duplicates) remain.
* @throws {Error} When provided a non-array.
*/
dups: function(arr) {
if (!Array.isArray(arr)) {
throw new Error('A non-array object passed to Morebits.array.dups');
}
return arr.filter((item, idx) => arr.indexOf(item) !== idx);
},
/**
* Break up an array into smaller arrays.
*
* @param {Array} arr
* @param {number} size - Size of each chunk (except the last, which could be different).
* @return {Array[]} An array containing the smaller, chunked arrays.
* @throws {Error} When provided a non-array.
*/
chunk: function(arr, size) {
if (!Array.isArray(arr)) {
throw new Error('A non-array object passed to Morebits.array.chunk');
}
if (typeof size !== 'number' || size <= 0) { // pretty impossible to do anything :)
return [arr]; // we return an array consisting of this array.
}
const numChunks = Math.ceil(arr.length / size);
const result = new Array(numChunks);
for (let i = 0; i < numChunks; i++) {
result[i] = arr.slice(i * size, (i + 1) * size);
}
return result;
}
};
/**
* Utilities to enhance select2 menus. See twinklewarn, twinklexfd,
* twinkleblock for sample usages.
*
* @see {@link https://select2.org/}
*
* @namespace Morebits.select2
* @memberof Morebits
* @requires jQuery.select2
*/
Morebits.select2 = {
matchers: {
/**
* Custom matcher in which if the optgroup name matches, all options in that
* group are shown, like in jquery.chosen.
*/
optgroupFull: function(params, data) {
const originalMatcher = $.fn.select2.defaults.defaults.matcher;
const result = originalMatcher(params, data);
if (result && params.term &&
data.text.toUpperCase().includes(params.term.toUpperCase())) {
result.children = data.children;
}
return result;
},
/** Custom matcher that matches from the beginning of words only. */
wordBeginning: function(params, data) {
const originalMatcher = $.fn.select2.defaults.defaults.matcher;
const result = originalMatcher(params, data);
if (!params.term || (result &&
new RegExp('\\b' + mw.util.escapeRegExp(params.term), 'i').test(result.text))) {
return result;
}
return null;
}
},
/** Underline matched part of options. */
highlightSearchMatches: function(data) {
const searchTerm = Morebits.select2SearchQuery;
if (!searchTerm || data.loading) {
return data.text;
}
const idx = data.text.toUpperCase().indexOf(searchTerm.toUpperCase());
if (idx < 0) {
return data.text;
}
return $('<span>').append(
data.text.slice(0, idx),
$('<span>').css('text-decoration', 'underline').text(data.text.slice(idx, idx + searchTerm.length)),
data.text.slice(idx + searchTerm.length)
);
},
/** Intercept query as it is happening, for use in highlightSearchMatches. */
queryInterceptor: function(params) {
Morebits.select2SearchQuery = params && params.term;
},
/**
* Open dropdown and begin search when the `.select2-selection` has
* focus and a key is pressed.
*
* @see {@link https://github.com/select2/select2/issues/3279#issuecomment-442524147}
*/
autoStart: function(ev) {
if (ev.which < 48) {
return;
}
let $target = $(ev.target).closest('.select2-container');
if (!$target.length) {
return;
}
$target = $target.prev();
$target.select2('open');
const search = $target.data('select2').dropdown.$search ||
$target.data('select2').selection.$search;
// Use DOM .focus() to work around a jQuery 3.6.0 regression (https://github.com/select2/select2/issues/5993)
search[0].focus();
}
};
/**
* Temporarily hide a part of a string while processing the rest of it.
* Used by {@link Morebits.wikitext.Page#commentOutImage|Morebits.wikitext.Page.commentOutImage}.
*
* @memberof Morebits
* @class
* @param {string} string - The initial text to process.
* @example var u = new Morebits.Unbinder('Hello world <!-- world --> world');
* u.unbind('<!--', '-->'); // text inside comment remains intact
* u.content = u.content.replace(/world/g, 'earth');
* u.rebind(); // gives 'Hello earth <!-- world --> earth'
*/
Morebits.Unbinder = function Unbinder(string) {
if (typeof string !== 'string') {
throw new Error('not a string');
}
/** The text being processed. */
this.content = string;
this.counter = 0;
this.history = {};
this.prefix = '%UNIQ::' + Math.random() + '::';
this.postfix = '::UNIQ%';
};
Morebits.Unbinder.prototype = {
/**
* Hide the region encapsulated by the `prefix` and `postfix` from
* string processing. `prefix` and `postfix` will be used in a
* RegExp, so items that need escaping should be use `\\`.
*
* @param {string} prefix
* @param {string} postfix
* @throws {Error} If either `prefix` or `postfix` is missing.
*/
unbind: function UnbinderUnbind(prefix, postfix) {
if (!prefix || !postfix) {
throw new Error('Both prefix and postfix must be provided');
}
const re = new RegExp(prefix + '([\\s\\S]*?)' + postfix, 'g');
this.content = this.content.replace(re, Morebits.Unbinder.getCallback(this));
},
/**
* Restore the hidden portion of the `content` string.
*
* @return {string} The processed output.
*/
rebind: function UnbinderRebind() {
let content = this.content;
content.self = this;
for (const current in this.history) {
if (Object.prototype.hasOwnProperty.call(this.history, current)) {
content = content.replace(current, this.history[current]);
}
}
return content;
},
prefix: null, // %UNIQ::0.5955981644938324::
postfix: null, // ::UNIQ%
content: null, // string
counter: null, // 0++
history: null // {}
};
/** @memberof Morebits.Unbinder */
Morebits.Unbinder.getCallback = function UnbinderGetCallback(self) {
return function UnbinderCallback(match) {
const current = self.prefix + self.counter + self.postfix;
self.history[current] = match;
++self.counter;
return current;
};
};
/* **************** Morebits.Date **************** */
/**
* Create a date object with enhanced processing capabilities, a la
* {@link https://momentjs.com/|moment.js}. MediaWiki timestamp format is also
* acceptable, in addition to everything that JS Date() accepts.
*
* @memberof Morebits
* @class
*/
Morebits.Date = class extends Date {
constructor(...args) {
// Check MediaWiki formats
// Must be first since firefox erroneously accepts the timestamp
// format, sans timezone (See also: #921, #936, #1174, #1187), and the
// 14-digit string will be interpreted differently.
if (args.length === 1) {
const param = args[0];
if (/^\d{14}$/.test(param)) {
// YYYYMMDDHHmmss
const digitMatch = /(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/.exec(param);
if (digitMatch) {
// ..... year ... month .. date ... hour .... minute ..... second
super(Date.UTC(digitMatch[1], digitMatch[2] - 1, digitMatch[3], digitMatch[4], digitMatch[5], digitMatch[6]));
} else {
super(...args);
}
} else if (typeof param === 'string') {
// Wikitext signature timestamp
const dateParts = Morebits.l10n.signatureTimestampFormat(param);
if (dateParts) {
super(Date.UTC(...dateParts));
} else {
super(...args);
}
} else {
super(...args);
}
} else {
super(...args);
}
if (!this.isValid()) {
mw.log.warn('Invalid Morebits.Date initialisation:', args);
}
}
/** @return {boolean} */
isValid() {
return !isNaN(this.getTime());
}
/**
* @param {(Date|Morebits.Date)} date
* @return {boolean}
*/
isBefore(date) {
return this.getTime() < date.getTime();
}
/**
* @param {(Date|Morebits.Date)} date
* @return {boolean}
*/
isAfter(date) {
return this.getTime() > date.getTime();
}
/** @return {string} */
getUTCMonthName() {
return Morebits.Date.localeData.months[this.getUTCMonth()];
}
/** @return {string} */
getUTCMonthNameAbbrev() {
return Morebits.Date.localeData.monthsShort[this.getUTCMonth()];
}
/** @return {string} */
getMonthName() {
return Morebits.Date.localeData.months[this.getMonth()];
}
/** @return {string} */
getMonthNameAbbrev() {
return Morebits.Date.localeData.monthsShort[this.getMonth()];
}
/** @return {string} */
getUTCDayName() {
return Morebits.Date.localeData.days[this.getUTCDay()];
}
/** @return {string} */
getUTCDayNameAbbrev() {
return Morebits.Date.localeData.daysShort[this.getUTCDay()];
}
/** @return {string} */
getDayName() {
return Morebits.Date.localeData.days[this.getDay()];
}
/** @return {string} */
getDayNameAbbrev() {
return Morebits.Date.localeData.daysShort[this.getDay()];
}
/**
* Add a given number of minutes, hours, days, weeks, months, or years to the date.
* This is done in-place. The modified date object is also returned, allowing chaining.
*
* @param {number} number - Should be an integer.
* @param {string} unit
* @throws {Error} If invalid or unsupported unit is given.
* @return {Morebits.Date}
*/
add(number, unit) {
let num = parseInt(number, 10); // normalize
if (isNaN(num)) {
throw new Error('Invalid number "' + number + '" provided.');
}
unit = unit.toLowerCase(); // normalize
const unitMap = Morebits.Date.unitMap;
let unitNorm = unitMap[unit] || unitMap[unit + 's']; // so that both singular and plural forms work
if (unitNorm) {
// No built-in week functions, so rather than build out ISO's getWeek/setWeek, just multiply
// Probably can't be used for Julian->Gregorian changeovers, etc.
if (unitNorm === 'Week') {
unitNorm = 'Date';
num *= 7;
}
this['set' + unitNorm](this['get' + unitNorm]() + num);
return this;
}
throw new Error('Invalid unit "' + unit + '": Only ' + Object.keys(unitMap).join(', ') + ' are allowed.');
}
/**
* Subtracts a given number of minutes, hours, days, weeks, months, or years to the date.
* This is done in-place. The modified date object is also returned, allowing chaining.
*
* @param {number} number - Should be an integer.
* @param {string} unit
* @throws {Error} If invalid or unsupported unit is given.
* @return {Morebits.Date}
*/
subtract(number, unit) {
return this.add(-number, unit);
}
/**
* Format the date into a string per the given format string.
* Replacement syntax is a subset of that in moment.js:
*
* | Syntax | Output |
* |--------|--------|
* | H | Hours (24-hour) |
* | HH | Hours (24-hour, padded to 2 digits) |
* | h | Hours (12-hour) |
* | hh | Hours (12-hour, padded to 2 digits) |
* | A | AM or PM |
* | m | Minutes |
* | mm | Minutes (padded to 2 digits) |
* | s | Seconds |
* | ss | Seconds (padded to 2 digits) |
* | SSS | Milliseconds fragment, 3 digits |
* | d | Day number of the week (Sun=0) |
* | ddd | Abbreviated day name |
* | dddd | Full day name |
* | D | Date |
* | DD | Date (padded to 2 digits) |
* | M | Month number (1-indexed) |
* | MM | Month number (1-indexed, padded to 2 digits) |
* | MMM | Abbreviated month name |
* | MMMM | Full month name |
* | Y | Year |
* | YY | Final two digits of year (20 for 2020, 42 for 1942) |
* | YYYY | Year (same as `Y`) |
*
* @param {string} formatstr - Format the date into a string, using
* the replacement syntax. Use `[` and `]` to escape items. If not
* provided, will return the ISO-8601-formatted string.
* @param {(string|number)} [zone=system] - `system` (for browser-default time zone),
* `utc`, or specify a time zone as number of minutes relative to UTC.
* @return {string}
*/
format(formatstr, zone) {
if (!this.isValid()) {
return 'Invalid date'; // Put the truth out, preferable to "NaNNaNNan NaN:NaN" or whatever
}
let udate = this;
// create a new date object that will contain the date to display as system time
if (zone === 'utc') {
udate = new Morebits.Date(this.getTime()).add(this.getTimezoneOffset(), 'minutes');
} else if (typeof zone === 'number') {
// convert to utc, then add the utc offset given
udate = new Morebits.Date(this.getTime()).add(this.getTimezoneOffset() + zone, 'minutes');
}
// default to ISOString
if (!formatstr) {
return udate.toISOString();
}
const pad = function(num, len) {
len = len || 2; // Up to length of 00 + 1
return ('00' + num).toString().slice(0 - len);
};
var h24 = udate.getHours(), m = udate.getMinutes(), s = udate.getSeconds(), ms = udate.getMilliseconds();
var D = udate.getDate(), M = udate.getMonth() + 1, Y = udate.getFullYear();
var h12 = h24 % 12 || 12, amOrPm = h24 >= 12 ? '午後' : '午前';
var replacementMap = {
HH: pad(h24), H: h24, hh: pad(h12), h: h12, A: amOrPm,
mm: pad(m), m: m,
ss: pad(s), s: s,
SSS: pad(ms, 3),
dddd: udate.getDayName(), ddd: udate.getDayNameAbbrev(), d: udate.getDay(),
DD: pad(D), D: D,
MMMM: udate.getMonthName(), MMM: udate.getMonthNameAbbrev(), MM: pad(M), M: M,
YYYY: Y, YY: pad(Y % 100), Y: Y
};
const unbinder = new Morebits.Unbinder(formatstr); // escape stuff between [...]
unbinder.unbind('\\[', '\\]');
unbinder.content = unbinder.content.replace(
/* Regex notes:
* d(d{2,3})? matches exactly 1, 3 or 4 occurrences of 'd' ('dd' is treated as a double match of 'd')
* Y{1,2}(Y{2})? matches exactly 1, 2 or 4 occurrences of 'Y'
*/
/H{1,2}|h{1,2}|m{1,2}|s{1,2}|SSS|d(d{2,3})?|D{1,2}|M{1,4}|Y{1,2}(Y{2})?|A/g,
(match) => replacementMap[match]
);
return unbinder.rebind().replace(/\[(.*?)\]/g, '$1');
}
/**
* Gives a readable relative time string such as "Yesterday at 6:43 PM" or "Last Thursday at 11:45 AM".
* Similar to `calendar` in moment.js, but with time zone support.
*
* @param {(string|number)} [zone=system] - 'system' (for browser-default time zone),
* 'utc' (for UTC), or specify a time zone as number of minutes past UTC.
* @return {string}
*/
calendar(zone) {
// Zero out the hours, minutes, seconds and milliseconds - keeping only the date;
// find the difference. Note that setHours() returns the same thing as getTime().
const dateDiff = (new Date().setHours(0, 0, 0, 0) -
new Date(this).setHours(0, 0, 0, 0)) / 8.64e7;
switch (true) {
case dateDiff === 0:
return this.format(Morebits.Date.localeData.relativeTimes.thisDay, zone);
case dateDiff === 1:
return this.format(Morebits.Date.localeData.relativeTimes.prevDay, zone);
case dateDiff > 0 && dateDiff < 7:
return this.format(Morebits.Date.localeData.relativeTimes.pastWeek, zone);
case dateDiff === -1:
return this.format(Morebits.Date.localeData.relativeTimes.nextDay, zone);
case dateDiff < 0 && dateDiff > -7:
return this.format(Morebits.Date.localeData.relativeTimes.thisWeek, zone);
default:
return this.format(Morebits.Date.localeData.relativeTimes.other, zone);
}
}
/**
* Get a regular expression that matches wikitext section titles, such
* as `==December 2019==` or `=== Jan 2018 ===`.
*
* @return {RegExp}
*/
monthHeaderRegex() {
return new RegExp('^(==+)\\s*' + this.getUTCFullYear() +
'年+(?:' + this.getUTCMonthName() + '|' + this.getUTCMonthNameAbbrev() + ')\\s*\\1', 'mg');
}
/**
* Creates a wikitext section header with the month and year.
*
* @param {number} [level=2] - Header level. Pass 0 for just the text
* with no wikitext markers (==).
* @return {string}
*/
monthHeader(level) {
// Default to 2, but allow for 0 or stringy numbers
level = parseInt(level, 10);
level = isNaN(level) ? 2 : level;
var header = Array(level + 1).join('='); // String.prototype.repeat not supported in IE 11
var text = this.getUTCFullYear() + '年' + this.getUTCMonthName();
if (header.length) { // wikitext-formatted header
return header + ' ' + text + ' ' + header;
}
return text; // Just the string
}
};
/**
* Localized strings for date processing.
*
* @memberof Morebits.Date
* @type {object.<string, string>}
* @property {string[]} months
* @property {string[]} monthsShort
* @property {string[]} days
* @property {string[]} daysShort
* @property {object.<string, string>} relativeTimes
* @private
*/
Morebits.Date.localeData = {
// message names here correspond to MediaWiki message names
months: ['1月', '2月', '3月',
'4月', '5月', '6月',
'7月', '8月', '9月',
'10月', '11月', '12月'],
monthsShort: ['1月', '2月', '3月',
'4月', '5月', '6月',
'7月', '8月', '9月',
'10月', '11月', '12月'],
days: ['日曜日', '月曜日', '火曜日',
'水曜日', '木曜日', '金曜日',
'土曜日'],
daysShort: ['日', '月', '火',
'水', '木', '金',
'土'],
relativeTimes: {
thisDay: '[今日の]H:mm',
prevDay: '[昨日の]H:mm',
nextDay: '[明日の]H:mm',
thisWeek: 'dddd[の]H:mm',
pastWeek: '[先週]dddd[の]H:mm',
other: 'YYYY MM DD'
}
};
/**
* Map units with getter/setter function names, for `add` and `subtract`
* methods.
*
* @memberof Morebits.Date
* @type {object.<string, string>}
* @property {string} seconds
* @property {string} minutes
* @property {string} hours
* @property {string} days
* @property {string} weeks
* @property {string} months
* @property {string} years
*/
Morebits.Date.unitMap = {
seconds: 'Seconds',
minutes: 'Minutes',
hours: 'Hours',
days: 'Date',
weeks: 'Week', // Not a function but handled in `add` through cunning use of multiplication
months: 'Month',
years: 'FullYear'
};
/* **************** Morebits.wiki **************** */
/**
* Various objects for wiki editing and API access, including
* {@link Morebits.wiki.Api} and {@link Morebits.wiki.Page}.
*
* @namespace Morebits.wiki
* @memberof Morebits
*/
Morebits.wiki = {};
/* **************** Morebits.wiki.actionCompleted **************** */
/**
* @memberof Morebits.wiki
* @type {number}
*/
Morebits.wiki.numberOfActionsLeft = 0;
/**
* @memberof Morebits.wiki
* @type {number}
*/
Morebits.wiki.nbrOfCheckpointsLeft = 0;
/**
* Display message and/or redirect to page upon completion of tasks.
*
* Every call to Morebits.wiki.Api.post() results in the dispatch of an
* asynchronous callback. Each callback can in turn make an additional call to
* Morebits.wiki.Api.post() to continue a processing sequence. At the
* conclusion of the final callback of a processing sequence, it is not
* possible to simply return to the original caller because there is no call
* stack leading back to the original context. Instead,
* Morebits.wiki.actionCompleted.event() is called to display the result to
* the user and to perform an optional page redirect.
*
* The determination of when to call Morebits.wiki.actionCompleted.event() is
* managed through the globals Morebits.wiki.numberOfActionsLeft and
* Morebits.wiki.nbrOfCheckpointsLeft. Morebits.wiki.numberOfActionsLeft is
* incremented at the start of every Morebits.wiki.Api call and decremented
* after the completion of a callback function. If a callback function does
* not create a new Morebits.wiki.Api object before exiting, it is the final
* step in the processing chain and Morebits.wiki.actionCompleted.event() will
* then be called.
*
* Optionally, callers may use Morebits.wiki.addCheckpoint() to indicate that
* processing is not complete upon the conclusion of the final callback
* function. This is used for batch operations. The end of a batch is
* signaled by calling Morebits.wiki.removeCheckpoint().
*
* @memberof Morebits.wiki
*/
Morebits.wiki.actionCompleted = function(self) {
if (--Morebits.wiki.numberOfActionsLeft <= 0 && Morebits.wiki.nbrOfCheckpointsLeft <= 0) {
Morebits.wiki.actionCompleted.event(self);
}
};
// Change per action wanted
/** @memberof Morebits.wiki */
Morebits.wiki.actionCompleted.event = function() {
if (Morebits.wiki.actionCompleted.notice) {
Morebits.Status.actionCompleted(Morebits.wiki.actionCompleted.notice);
}
if (Morebits.wiki.actionCompleted.redirect) {
// if it isn't a URL, make it one. TODO: This breaks on the articles 'http://', 'ftp://', and similar ones.
if (!(/^\w+:\/\//).test(Morebits.wiki.actionCompleted.redirect)) {
Morebits.wiki.actionCompleted.redirect = mw.util.getUrl(Morebits.wiki.actionCompleted.redirect);
if (Morebits.wiki.actionCompleted.followRedirect === false) {
Morebits.wiki.actionCompleted.redirect += '?redirect=no';
}
}
window.setTimeout(() => {
window.location = Morebits.wiki.actionCompleted.redirect;
}, Morebits.wiki.actionCompleted.timeOut);
}
};
/** @memberof Morebits.wiki */
Morebits.wiki.actionCompleted.timeOut = typeof window.wpActionCompletedTimeOut === 'undefined' ? 5000 : window.wpActionCompletedTimeOut;
/** @memberof Morebits.wiki */
Morebits.wiki.actionCompleted.redirect = null;
/** @memberof Morebits.wiki */
Morebits.wiki.actionCompleted.notice = null;
/** @memberof Morebits.wiki */
Morebits.wiki.addCheckpoint = function() {
++Morebits.wiki.nbrOfCheckpointsLeft;
};
/** @memberof Morebits.wiki */
Morebits.wiki.removeCheckpoint = function() {
if (--Morebits.wiki.nbrOfCheckpointsLeft <= 0 && Morebits.wiki.numberOfActionsLeft <= 0) {
Morebits.wiki.actionCompleted.event();
}
};
/* **************** Morebits.wiki.Api **************** */
/**
* An easy way to talk to the MediaWiki API. Accepts either json or xml
* (default) formats; if json is selected, will default to `formatversion=2`
* unless otherwise specified. Similarly, enforces newer `errorformat`s,
* defaulting to `html` if unspecified. `uselang` enforced to the wiki's
* content language.
*
* In new code, the use of the last 3 parameters should be avoided, instead
* use {@link Morebits.wiki.Api#setStatusElement|setStatusElement()} to bind
* the status element (if needed) and use `.then()` or `.catch()` on the
* promise returned by `post()`, rather than specify the `onSuccess` or
* `onFailure` callbacks.
*
* @memberof Morebits.wiki
* @class
* @param {string} currentAction - The current action (required).
* @param {Object} query - The query (required).
* @param {Function} [onSuccess] - The function to call when request is successful.
* @param {Morebits.Status} [statusElement] - A Morebits.Status object to use for status messages.
* @param {Function} [onError] - The function to call if an error occurs.
*/
Morebits.wiki.Api = function(currentAction, query, onSuccess, statusElement, onError) {
this.currentAction = currentAction;
this.query = query;
this.query.assert = 'user';
// Enforce newer error formats, preferring html
if (!query.errorformat || !['wikitext', 'plaintext'].includes(query.errorformat)) {
this.query.errorformat = 'html';
}
// Explicitly use the wiki's content language to minimize confusion,
// see #1179 for discussion
this.query.uselang = 'content';
this.query.errorlang = 'uselang';
this.query.errorsuselocal = 1;
this.onSuccess = onSuccess;
this.onError = onError;
if (statusElement) {
this.setStatusElement(statusElement);
} else {
this.statelem = new Morebits.Status(currentAction);
}
// JSON is used throughout Morebits/Twinkle, but xml remains the default for backwards compatibility
if (!query.format) {
this.query.format = 'xml';
} else if (query.format === 'json' && !query.formatversion) {
this.query.formatversion = '2';
} else if (!['xml', 'json'].includes(query.format)) {
this.statelem.error('Invalid API format: only xml and json are supported.');
}
// Ignore tags for queries and most common unsupported actions, produces warnings
if (query.action && ['query', 'review', 'stabilize', 'watch'].includes(query.action)) {
delete query.tags;
}
};
Morebits.wiki.Api.prototype = {
currentAction: '',
onSuccess: null,
onError: null,
parent: window, // use global context if there is no parent object
query: null,
response: null,
responseXML: null, // use `response` instead; retained for backwards compatibility
statelem: null, // this non-standard name kept for backwards compatibility
statusText: null, // result received from the API, normally "success" or "error"
errorCode: null, // short text error code, if any, as documented in the MediaWiki API
errorText: null, // full error description, if any
badtokenRetry: false, // set to true if this on a retry attempted after a badtoken error
/**
* Keep track of parent object for callbacks.
*
* @param {*} parent
*/
setParent: function(parent) {
this.parent = parent;
},
/** @param {Morebits.Status} statusElement */
setStatusElement: function(statusElement) {
this.statelem = statusElement;
this.statelem.status(this.currentAction);
},
/**
* Carry out the request.
*
* @param {Object} callerAjaxParameters - Do not specify a parameter unless you really
* really want to give jQuery some extra parameters.
* @return {jQuery.Promise} - A jQuery promise object that is resolved or rejected with the api object.
*/
post: function(callerAjaxParameters) {
++Morebits.wiki.numberOfActionsLeft;
const queryString = $.map(this.query, (val, i) => {
if (Array.isArray(val)) {
return encodeURIComponent(i) + '=' + val.map(encodeURIComponent).join('|');
} else if (val !== undefined) {
return encodeURIComponent(i) + '=' + encodeURIComponent(val);
}
}).join('&').replace(/^(.*?)(\btoken=[^&]*)&(.*)/, '$1$3&$2');
// token should always be the last item in the query string (bug TW-B-0013)
const headers = {
'Api-User-Agent': morebitsWikiApiUserAgent
};
if (this.query.action === 'parse') {
// Per https://www.mediawiki.org/wiki/API:Etiquette
headers['Promise-Non-Write-API-Action'] = 'true';
}
const ajaxparams = $.extend({}, {
context: this,
type: this.query.action === 'query' ? 'GET' : 'POST',
url: mw.util.wikiScript('api'),
data: queryString,
dataType: this.query.format,
headers
}, callerAjaxParameters);
return $.ajax(ajaxparams).then(
function onAPIsuccess(response, statusText) {
this.statusText = statusText;
this.response = this.responseXML = response;
// Limit to first error
if (this.query.format === 'json') {
this.errorCode = response.errors && response.errors[0].code;
if (this.query.errorformat === 'html') {
this.errorText = response.errors && response.errors[0].html;
} else if (this.query.errorformat === 'wikitext' || this.query.errorformat === 'plaintext') {
this.errorText = response.errors && response.errors[0].text;
}
} else {
this.errorCode = $(response).find('errors error').eq(0).attr('code');
// Sufficient for html, wikitext, or plaintext errorformats
this.errorText = $(response).find('errors error').eq(0).text();
}
if (typeof this.errorCode === 'string') {
// the API didn't like what we told it, e.g., bad edit token or an error creating a page
return this.returnError(callerAjaxParameters);
}
// invoke success callback if one was supplied
if (this.onSuccess) {
// set the callback context to this.parent for new code and supply the API object
// as the first argument to the callback (for legacy code)
this.onSuccess.call(this.parent, this);
} else {
this.statelem.info('完了');
}
Morebits.wiki.actionCompleted();
return $.Deferred().resolveWith(this.parent, [this]);
},
// only network and server errors reach here - complaints from the API itself are caught in success()
function onAPIfailure(jqXHR, statusText, errorThrown) {
this.statusText = statusText;
this.errorThrown = errorThrown; // frequently undefined
this.errorText = statusText + ' "' + jqXHR.statusText + '" occurred while contacting the API.';
return this.returnError();
}
);
},
returnError: function(callerAjaxParameters) {
if (this.errorCode === 'badtoken' && !this.badtokenRetry) {
this.statelem.warn('トークンが無効です。新しいトークンを取得して再試行します...');
this.badtokenRetry = true;
// Get a new CSRF token and retry. If the original action needs a different
// type of action than CSRF, we do one pointless retry before bailing out
return Morebits.wiki.Api.getToken().then((token) => {
this.query.token = token;
return this.post(callerAjaxParameters);
});
}
this.statelem.error(this.errorText + ' (' + this.errorCode + ')');
// invoke failure callback if one was supplied
if (this.onError) {
// set the callback context to this.parent for new code and supply the API object
// as the first argument to the callback for legacy code
this.onError.call(this.parent, this);
}
// don't complete the action so that the error remains displayed
return $.Deferred().rejectWith(this.parent, [this]);
},
getStatusElement: function() {
return this.statelem;
},
getErrorCode: function() {
return this.errorCode;
},
getErrorText: function() {
return this.errorText;
},
getXML: function() { // retained for backwards compatibility, use getResponse() instead
return this.responseXML;
},
getResponse: function() {
return this.response;
}
};
/**
* Retrieves wikitext from a page. Caching defaults to a duration of 1 day.
*
* @param {string} title - Page title
* @param {number} [maxAge=86400] - Cache duration in seconds; use 0 to retrieve current content.
* @return {Promise<string|null>} Returns page content, or null if the page doesn't exist.
*/
Morebits.wiki.getCachedPage = function(title, maxAge = 86400) {
return new mw.Api({ userAgent: morebitsWikiApiUserAgent }).get({
action: 'query',
prop: 'revisions',
titles: title,
rvprop: 'content',
rvslots: '*',
format: 'json',
formatversion: '2',
smaxage: String(maxAge),
maxage: String(maxAge),
uselang: 'content'
}).then((data) => {
const page = data.query.pages[0];
if (page.missing) {
return null;
}
return page.revisions[0].slots.main.content;
});
};
/**
* Retrieves JSON from a page. Caching is enabled with a duration of 1 day.
*
* @param {string} title - Page title
* @return {Promise<string>}
*/
Morebits.wiki.getCachedJson = function(title) {
return Morebits.wiki.getCachedPage(title).then((wikitext) => JSON.parse(wikitext));
};
var morebitsWikiApiUserAgent = 'morebits.js ([[w:WT:TW]])';
/**
* Set the custom user agent header, which is used for server-side logging.
* Note that doing so will set the useragent for every `Morebits.wiki.Api`
* process performed thereafter.
*
* @see {@link https://lists.wikimedia.org/pipermail/mediawiki-api-announce/2014-November/000075.html}
* for original announcement.
*
* @memberof Morebits.wiki.Api
* @param {string} [ua=morebits.js ([[w:WT:TW]])] - User agent. The default
* value of `morebits.js ([[w:WT:TW]])` will be appended to any provided
* value.
*/
Morebits.wiki.Api.setApiUserAgent = function(ua) {
morebitsWikiApiUserAgent = (ua ? ua + ' ' : '') + 'morebits.js ([[w:WT:TW]])';
};
/**
* Get a new CSRF token on encountering token errors.
*
* @memberof Morebits.wiki.Api
* @return {string} MediaWiki CSRF token.
*/
Morebits.wiki.Api.getToken = function() {
var tokenApi = new Morebits.wiki.Api('トークンを取得', {
action: 'query',
meta: 'tokens',
type: 'csrf',
format: 'json'
});
return tokenApi.post().then((apiobj) => apiobj.response.query.tokens.csrftoken);
};
/* **************** Morebits.wiki.Page **************** */
/**
* Use the MediaWiki API to load a page and optionally edit it, move it, etc.
*
* Callers are not permitted to directly access the properties of this class!
* All property access is through the appropriate get___() or set___() method.
*
* Callers should set {@link Morebits.wiki.actionCompleted.notice} and {@link Morebits.wiki.actionCompleted.redirect}
* before the first call to {@link Morebits.wiki.Page.load()}.
*
* Each of the callback functions takes one parameter, which is a
* reference to the Morebits.wiki.Page object that registered the callback.
* Callback functions may invoke any Morebits.wiki.Page prototype method using this reference.
*
*
* Call sequence for common operations (optional final user callbacks not shown):
*
* - Edit current contents of a page (no edit conflict):
* `.load(userTextEditCallback) -> ctx.loadApi.post() ->
* ctx.loadApi.post.success() -> ctx.fnLoadSuccess() -> userTextEditCallback() ->
* .save() -> ctx.saveApi.post() -> ctx.loadApi.post.success() -> ctx.fnSaveSuccess()`
*
* - Edit current contents of a page (with edit conflict):
* `.load(userTextEditCallback) -> ctx.loadApi.post() ->
* ctx.loadApi.post.success() -> ctx.fnLoadSuccess() -> userTextEditCallback() ->
* .save() -> ctx.saveApi.post() -> ctx.loadApi.post.success() ->
* ctx.fnSaveError() -> ctx.loadApi.post() -> ctx.loadApi.post.success() ->
* ctx.fnLoadSuccess() -> userTextEditCallback() -> .save() ->
* ctx.saveApi.post() -> ctx.loadApi.post.success() -> ctx.fnSaveSuccess()`
*
* - Append to a page (similar for prepend and newSection):
* `.append() -> ctx.loadApi.post() -> ctx.loadApi.post.success() ->
* ctx.fnLoadSuccess() -> ctx.fnAutoSave() -> .save() -> ctx.saveApi.post() ->
* ctx.loadApi.post.success() -> ctx.fnSaveSuccess()`
*
* Notes:
* 1. All functions following Morebits.wiki.Api.post() are invoked asynchronously from the jQuery AJAX library.
* 2. The sequence for append/prepend/newSection could be slightly shortened,
* but it would require significant duplication of code for little benefit.
*
* @memberof Morebits.wiki
* @class
* @param {string} pageName - The name of the page, prefixed by the namespace (if any).
* For the current page, use `mw.config.get('wgPageName')`.
* @param {string|Morebits.Status} [status] - A string describing the action about to be undertaken,
* or a Morebits.Status object
*/
Morebits.wiki.Page = function(pageName, status) {
if (!status) {
status = '"' + pageName + '"を開く';
}
/**
* Private context variables.
*
* This context is not visible to the outside, thus all the data here
* must be accessed via getter and setter functions.
*
* @private
*/
const ctx = {
// backing fields for public properties
pageName: pageName,
pageExists: false,
editSummary: null,
changeTags: null,
testActions: null, // array if any valid actions
callbackParameters: null,
statusElement: status instanceof Morebits.Status ? status : new Morebits.Status(status),
// - edit
pageText: null,
editMode: 'all', // save() replaces entire contents of the page by default
appendText: null, // can't reuse pageText for this because pageText is needed to follow a redirect
prependText: null, // can't reuse pageText for this because pageText is needed to follow a redirect
newSectionText: null,
newSectionTitle: null,
createOption: null,
minorEdit: false,
botEdit: false,
pageSection: null,
maxConflictRetries: 2,
maxRetries: 2,
followRedirect: false,
followCrossNsRedirect: true,
watchlistOption: 'nochange',
watchlistExpiry: null,
discussionToolsAutoSubscribe: null,
creator: null,
timestamp: null,
// - revert
revertOldID: null,
// - move
moveDestination: null,
moveTalkPage: false,
moveSubpages: false,
moveSuppressRedirect: false,
// - protect
protectEdit: null,
protectMove: null,
protectCreate: null,
protectCascade: null,
// - delete
deleteTalkPage: false,
// - undelete
undeleteTalkPage: false,
// - creation lookup
lookupNonRedirectCreator: false,
// - stabilize (FlaggedRevs)
flaggedRevs: null,
// internal status
pageLoaded: false,
csrfToken: null,
loadTime: null,
lastEditTime: null,
pageID: null,
contentModel: null,
pageParsed: null,
sections: null,
revertCurID: null,
revertUser: null,
watched: false,
fullyProtected: false,
suppressProtectWarning: false,
conflictRetries: 0,
retries: 0,
// callbacks
onLoadSuccess: null,
onLoadFailure: null,
onParseSuccess: null,
onParseFailure: null,
onSaveSuccess: null,
onSaveFailure: null,
onLookupCreationSuccess: null,
onLookupCreationFailure: null,
onMoveSuccess: null,
onMoveFailure: null,
onDeleteSuccess: null,
onDeleteFailure: null,
onUndeleteSuccess: null,
onUndeleteFailure: null,
onProtectSuccess: null,
onProtectFailure: null,
onStabilizeSuccess: null,
onStabilizeFailure: null,
// internal objects
loadQuery: null,
loadApi: null,
parseApi: null,
saveApi: null,
lookupCreationApi: null,
moveApi: null,
moveProcessApi: null,
patrolApi: null,
patrolProcessApi: null,
triageApi: null,
triageProcessListApi: null,
triageProcessApi: null,
deleteApi: null,
deleteProcessApi: null,
undeleteApi: null,
undeleteProcessApi: null,
protectApi: null,
protectProcessApi: null,
stabilizeApi: null,
stabilizeProcessApi: null
};
const emptyFunction = function() { };
/**
* Loads the text for the page.
*
* @param {Function} onSuccess - Callback function which is called when the load has succeeded.
* @param {Function} [onFailure] - Callback function which is called when the load fails.
*/
this.load = function(onSuccess, onFailure) {
ctx.onLoadSuccess = onSuccess;
ctx.onLoadFailure = onFailure || emptyFunction;
// Need to be able to do something after the page loads
if (!onSuccess) {
ctx.statusElement.error('Internal error: no onSuccess callback provided to load()!');
ctx.onLoadFailure(this);
return;
}
ctx.loadQuery = {
action: 'query',
prop: 'info|revisions',
inprop: 'watched',
intestactions: 'edit', // can be expanded
curtimestamp: '',
meta: 'tokens',
type: 'csrf',
titles: ctx.pageName,
format: 'json'
// don't need rvlimit=1 because we don't need rvstartid here and only one actual rev is returned by default
};
if (ctx.editMode === 'all') {
ctx.loadQuery.rvprop = 'content|timestamp'; // get the page content at the same time, if needed
} else if (ctx.editMode === 'revert') {
ctx.loadQuery.rvprop = 'timestamp';
ctx.loadQuery.rvlimit = 1;
ctx.loadQuery.rvstartid = ctx.revertOldID;
}
if (ctx.followRedirect) {
ctx.loadQuery.redirects = ''; // follow all redirects
}
if (typeof ctx.pageSection === 'number') {
ctx.loadQuery.rvsection = ctx.pageSection;
}
if (Morebits.userIsSysop) {
ctx.loadQuery.inprop += '|protection';
}
ctx.loadApi = new Morebits.wiki.Api('ページを取得中...', ctx.loadQuery, fnLoadSuccess, ctx.statusElement, ctx.onLoadFailure);
ctx.loadApi.setParent(this);
ctx.loadApi.post();
};
/**
* parse the text for the page.
*
* @param {Function} onSuccess - Callback function which is called when the load has succeeded.
* @param {Function} [onFailure] - Callback function which is called when the load fails.
*/
this.parse = function(onSuccess, onFailure) {
ctx.onParseSuccess = onSuccess;
ctx.onParseFailure = onFailure || emptyFunction;
// Need to be able to do something after the page loads
if (!onSuccess) {
ctx.statusElement.error('Internal error: no onSuccess callback provided to parse()!');
ctx.onParseFailure(this);
return;
}
var query = {
action: 'parse',
format: 'json',
page: ctx.pageName,
prop: 'sections',
utf8: 1
};
ctx.parseApi = new Morebits.wiki.Api('ページを取得中...', query, fnParseSuccess, ctx.statusElement, ctx.onParseFailure);
ctx.parseApi.setParent(this);
ctx.parseApi.post();
};
/**
* Saves the text for the page to Wikipedia.
* Must be preceded by successfully calling `load()`.
*
* Warning: Calling `save()` can result in additional calls to the
* previous `load()` callbacks to recover from edit conflicts! In this
* case, callers must make the same edit to the new pageText and
* re-invoke `save()`. This behavior can be disabled with
* `setMaxConflictRetries(0)`.
*
* @param {Function} [onSuccess] - Callback function which is called when the save has succeeded.
* @param {Function} [onFailure] - Callback function which is called when the save fails.
*/
this.save = function(onSuccess, onFailure) {
ctx.onSaveSuccess = onSuccess;
ctx.onSaveFailure = onFailure || emptyFunction;
// are we getting our editing token from mw.user.tokens?
const canUseMwUserToken = fnCanUseMwUserToken('edit');
if (!ctx.pageLoaded && !canUseMwUserToken) {
ctx.statusElement.error('Internal error: attempt to save a page that has not been loaded!');
ctx.onSaveFailure(this);
return;
}
if (!ctx.editSummary) {
// new section mode allows (nay, encourages) using the
// title as the edit summary, but the query needs
// editSummary to be undefined or '', not null
if (ctx.editMode === 'new' && ctx.newSectionTitle) {
ctx.editSummary = '';
} else {
ctx.statusElement.error('Internal error: edit summary not set before save!');
ctx.onSaveFailure(this);
return;
}
}
// shouldn't happen if canUseMwUserToken === true
if (ctx.fullyProtected && !ctx.suppressProtectWarning &&
!confirm(
ctx.fullyProtected === 'infinity' ?
'全保護されたページ "' + ctx.pageName + '" (無期限保護) を編集しようとしています。\n\n編集を続行するにはOKをクリックし、この編集をスキップするにはキャンセルをクリックしてください。' :
'全保護されたページ "' + ctx.pageName +
'" (保護期限: ' + new Morebits.Date(ctx.fullyProtected).calendar('utc') + ' (UTC)) を編集しようとしています。\n\n編集を続行するにはOKをクリックし、この編集をスキップするにはキャンセルをクリックしてください。'
)
) {
ctx.statusElement.error('全保護されたページの編集が中断されました。');
ctx.onSaveFailure(this);
return;
}
ctx.retries = 0;
const query = {
action: 'edit',
title: ctx.pageName,
summary: ctx.editSummary,
token: canUseMwUserToken ? mw.user.tokens.get('csrfToken') : ctx.csrfToken,
watchlist: ctx.watchlistOption,
format: 'json'
};
if (ctx.changeTags) {
query.tags = ctx.changeTags;
}
if (fnApplyWatchlistExpiry()) {
query.watchlistexpiry = ctx.watchlistExpiry;
}
if (typeof ctx.pageSection === 'number') {
query.section = ctx.pageSection;
}
// Set minor edit attribute. If these parameters are present with any value, it is interpreted as true
if (ctx.minorEdit) {
query.minor = true;
} else {
query.notminor = true; // force Twinkle config to override user preference setting for "all edits are minor"
}
// Set bot edit attribute. If this parameter is present with any value, it is interpreted as true
if (ctx.botEdit) {
query.bot = true;
}
if (ctx.discussionToolsAutoSubscribe !== null) {
query.discussiontoolsautosubscribe = ctx.discussionToolsAutoSubscribe ? 'yes' : 'no';
}
switch (ctx.editMode) {
case 'append':
if (ctx.appendText === null) {
ctx.statusElement.error('Internal error: append text not set before save!');
ctx.onSaveFailure(this);
return;
}
query.appendtext = ctx.appendText; // use mode to append to current page contents
break;
case 'prepend':
if (ctx.prependText === null) {
ctx.statusElement.error('Internal error: prepend text not set before save!');
ctx.onSaveFailure(this);
return;
}
query.prependtext = ctx.prependText; // use mode to prepend to current page contents
break;
case 'new':
if (!ctx.newSectionText) { // API doesn't allow empty new section text
ctx.statusElement.error('Internal error: new section text not set before save!');
ctx.onSaveFailure(this);
return;
}
query.section = 'new';
query.text = ctx.newSectionText; // add a new section to current page
query.sectiontitle = ctx.newSectionTitle || ctx.editSummary; // done by the API, but non-'' values would get treated as text
break;
case 'revert':
query.undo = ctx.revertCurID;
query.undoafter = ctx.revertOldID;
if (ctx.lastEditTime) {
query.basetimestamp = ctx.lastEditTime; // check that page hasn't been edited since it was loaded
}
query.starttimestamp = ctx.loadTime; // check that page hasn't been deleted since it was loaded (don't recreate bad stuff)
break;
default: // 'all'
query.text = ctx.pageText; // replace entire contents of the page
if (ctx.lastEditTime) {
query.basetimestamp = ctx.lastEditTime; // check that page hasn't been edited since it was loaded
}
query.starttimestamp = ctx.loadTime; // check that page hasn't been deleted since it was loaded (don't recreate bad stuff)
break;
}
if (['recreate', 'createonly', 'nocreate'].includes(ctx.createOption)) {
query[ctx.createOption] = '';
}
if (canUseMwUserToken && ctx.followRedirect) {
query.redirect = true;
}
ctx.saveApi = new Morebits.wiki.Api('ページを保存中...', query, fnSaveSuccess, ctx.statusElement, fnSaveError);
ctx.saveApi.setParent(this);
ctx.saveApi.post();
};
/**
* Adds the text provided via `setAppendText()` to the end of the
* page. Does not require calling `load()` first, unless a watchlist
* expiry is used.
*
* @param {Function} [onSuccess] - Callback function which is called when the method has succeeded.
* @param {Function} [onFailure] - Callback function which is called when the method fails.
*/
this.append = function(onSuccess, onFailure) {
ctx.editMode = 'append';
if (fnCanUseMwUserToken('edit')) {
this.save(onSuccess, onFailure);
} else {
ctx.onSaveSuccess = onSuccess;
ctx.onSaveFailure = onFailure || emptyFunction;
this.load(fnAutoSave, ctx.onSaveFailure);
}
};
/**
* Adds the text provided via `setPrependText()` to the start of the
* page. Does not require calling `load()` first, unless a watchlist
* expiry is used.
*
* @param {Function} [onSuccess] - Callback function which is called when the method has succeeded.
* @param {Function} [onFailure] - Callback function which is called when the method fails.
*/
this.prepend = function(onSuccess, onFailure) {
ctx.editMode = 'prepend';
if (fnCanUseMwUserToken('edit')) {
this.save(onSuccess, onFailure);
} else {
ctx.onSaveSuccess = onSuccess;
ctx.onSaveFailure = onFailure || emptyFunction;
this.load(fnAutoSave, ctx.onSaveFailure);
}
};
/**
* Creates a new section with the text provided by `setNewSectionText()`
* and section title from `setNewSectionTitle()`.
* If `editSummary` is provided, that will be used instead of the
* autogenerated "->Title (new section" edit summary.
* Does not require calling `load()` first, unless a watchlist expiry
* is used.
*
* @param {Function} [onSuccess] - Callback function which is called when the method has succeeded.
* @param {Function} [onFailure] - Callback function which is called when the method fails.
*/
this.newSection = function(onSuccess, onFailure) {
ctx.editMode = 'new';
if (fnCanUseMwUserToken('edit')) {
this.save(onSuccess, onFailure);
} else {
ctx.onSaveSuccess = onSuccess;
ctx.onSaveFailure = onFailure || emptyFunction;
this.load(fnAutoSave, ctx.onSaveFailure);
}
};
/** @return {string} The name of the loaded page, including the namespace */
this.getPageName = function() {
return ctx.pageName;
};
/** @return {string} The text of the page after a successful load() */
this.getPageText = function() {
return ctx.pageText;
};
/** @return {string} Sections of the page after a successful parse() */
this.getSections = function() {
return ctx.sections;
};
/** @param {string} pageText - Updated page text that will be saved when `save()` is called */
this.setPageText = function(pageText) {
ctx.editMode = 'all';
ctx.pageText = pageText;
};
/** @param {string} appendText - Text that will be appended to the page when `append()` is called */
this.setAppendText = function(appendText) {
ctx.editMode = 'append';
ctx.appendText = appendText;
};
/** @param {string} prependText - Text that will be prepended to the page when `prepend()` is called */
this.setPrependText = function(prependText) {
ctx.editMode = 'prepend';
ctx.prependText = prependText;
};
/** @param {string} newSectionText - Text that will be added in a new section on the page when `newSection()` is called */
this.setNewSectionText = function(newSectionText) {
ctx.editMode = 'new';
ctx.newSectionText = newSectionText;
};
/**
* @param {string} newSectionTitle - Title for the new section created when `newSection()` is called
* If missing, `ctx.editSummary` will be used. Issues may occur if a substituted template is used.
*/
this.setNewSectionTitle = function(newSectionTitle) {
ctx.editMode = 'new';
ctx.newSectionTitle = newSectionTitle;
};
// Edit-related setter methods:
/**
* Set the edit summary that will be used when `save()` is called.
* Unnecessary if editMode is 'new' and newSectionTitle is provided.
*
* @param {string} summary
*/
this.setEditSummary = function(summary) {
ctx.editSummary = summary;
};
/**
* Set any custom tag(s) to be applied to the API action.
* A number of actions don't support it, most notably watch, review,
* and stabilize ({@link https://phabricator.wikimedia.org/T247721|T247721}).
*
* @param {string|string[]} tags - String or array of tag(s).
*/
this.setChangeTags = function(tags) {
ctx.changeTags = tags;
};
/**
* @param {string} [createOption=null] - Can take the following four values:
* - recreate: create the page if it does not exist, or edit it if it exists.
* - createonly: create the page if it does not exist, but return an
* error if it already exists.
* - nocreate: don't create the page, only edit it if it already exists.
* - `null`: create the page if it does not exist, unless it was deleted
* in the moment between loading the page and saving the edit (default).
*/
this.setCreateOption = function(createOption) {
ctx.createOption = createOption;
};
/** @param {boolean} minorEdit - Set true to mark the edit as a minor edit. */
this.setMinorEdit = function(minorEdit) {
ctx.minorEdit = minorEdit;
};
/** @param {boolean} botEdit - Set true to mark the edit as a bot edit */
this.setBotEdit = function(botEdit) {
ctx.botEdit = botEdit;
};
/**
* @param {number} pageSection - Integer specifying the section number to load or save.
* If specified as `null`, the entire page will be retrieved.
*/
this.setPageSection = function(pageSection) {
ctx.pageSection = pageSection;
};
/**
* @param {number} maxConflictRetries - Number of retries for save errors involving an edit conflict or
* loss of token. Default: 2.
*/
this.setMaxConflictRetries = function(maxConflictRetries) {
ctx.maxConflictRetries = maxConflictRetries;
};
/**
* @param {number} maxRetries - Number of retries for save errors not involving an edit conflict or
* loss of token. Default: 2.
*/
this.setMaxRetries = function(maxRetries) {
ctx.maxRetries = maxRetries;
};
/**
* Set whether and how to watch the page, including setting an expiry.
*
* @param {boolean|string|Morebits.Date|Date} [watchlistOption=false] -
* Basically a mix of MW API and Twinkley options available pre-expiry:
* - `true`|`'yes'`|`'watch'`: page will be added to the user's
* watchlist when the action is called. Defaults to an indefinite
* watch unless `watchlistExpiry` is provided.
* - `false`|`'no'`|`'nochange'`: watchlist status of the page (including expiry) will not be changed.
* - `'default'`|`'preferences'`: watchlist status of the page will be
* set based on the user's preference settings when the action is
* called. Defaults to an indefinite watch unless `watchlistExpiry` is
* provided.
* - `'unwatch'`: explicitly unwatch the page.
* - Any other `string` or `number`, or a `Morebits.Date` or `Date`
* object: watch page until the specified time, deferring to
* `watchlistExpiry` if provided.
* @param {string|number|Morebits.Date|Date} [watchlistExpiry=infinity] -
* A date-like string or number, or a date object. If a string or number,
* can be relative (2 weeks) or other similarly date-like (i.e. NOT "potato"):
* ISO 8601: 2038-01-09T03:14:07Z
* MediaWiki: 20380109031407
* UNIX: 2147483647
* SQL: 2038-01-09 03:14:07
* Can also be `infinity` or infinity-like (`infinite`, `indefinite`, and `never`).
* See {@link https://phabricator.wikimedia.org/source/mediawiki-libs-Timestamp/browse/master/src/ConvertibleTimestamp.php;4e53b859a9580c55958078f46dd4f3a44d0fcaa0$57-109?as=source&blame=off}
*/
this.setWatchlist = function(watchlistOption, watchlistExpiry) {
if (watchlistOption instanceof Morebits.Date || watchlistOption instanceof Date) {
watchlistOption = watchlistOption.toISOString();
}
if (typeof watchlistExpiry === 'undefined') {
watchlistExpiry = 'infinity';
} else if (watchlistExpiry instanceof Morebits.Date || watchlistExpiry instanceof Date) {
watchlistExpiry = watchlistExpiry.toISOString();
}
switch (watchlistOption) {
case 'nochange':
case 'no':
case false:
case undefined:
ctx.watchlistOption = 'nochange';
// The MW API allows for changing expiry with nochange (as "nochange" refers to the binary status),
// but by keeping this null it will default to any existing expiry, ensure there is actually "no change."
ctx.watchlistExpiry = null;
break;
case 'unwatch':
// expiry unimportant
ctx.watchlistOption = 'unwatch';
break;
case 'preferences':
case 'default':
ctx.watchlistOption = 'preferences';
// The API allows an expiry here, but there is as of yet (T265716)
// no expiry preference option, so it's a bit devoid of context.
ctx.watchlistExpiry = watchlistExpiry;
break;
case 'watch':
case 'yes':
case true:
ctx.watchlistOption = 'watch';
ctx.watchlistExpiry = watchlistExpiry;
break;
default: // Not really a "default" per se but catches "any other string"
ctx.watchlistOption = 'watch';
ctx.watchlistExpiry = watchlistOption;
break;
}
};
/**
* Set a watchlist expiry. setWatchlist can mostly handle this by
* itself, so this is here largely for completeness and compatibility
* with the full suite of options.
*
* @param {string|number|Morebits.Date|Date} [watchlistExpiry=infinity] -
* A date-like string or number, or a date object. If a string or number,
* can be relative (2 weeks) or other similarly date-like (i.e. NOT "potato"):
* ISO 8601: 2038-01-09T03:14:07Z
* MediaWiki: 20380109031407
* UNIX: 2147483647
* SQL: 2038-01-09 03:14:07
* Can also be `infinity` or infinity-like (`infinite`, `indefinite`, and `never`).
* See {@link https://phabricator.wikimedia.org/source/mediawiki-libs-Timestamp/browse/master/src/ConvertibleTimestamp.php;4e53b859a9580c55958078f46dd4f3a44d0fcaa0$57-109?as=source&blame=off}
*/
this.setWatchlistExpiry = function(watchlistExpiry) {
if (typeof watchlistExpiry === 'undefined') {
watchlistExpiry = 'infinity';
} else if (watchlistExpiry instanceof Morebits.Date || watchlistExpiry instanceof Date) {
watchlistExpiry = watchlistExpiry.toISOString();
}
ctx.watchlistExpiry = watchlistExpiry;
};
/**
* If editing a talk page, set whether to subscribe to any talk
* page thread created by the edit.
*
* @param {boolean} subscribe
*/
this.setDiscussionToolsAutoSubscribe = function(subscribe) {
ctx.discussionToolsAutoSubscribe = subscribe;
};
/**
* @param {boolean} [followRedirect=false] -
* - `true`: a maximum of one redirect will be followed. In the event
* of a redirect, a message is displayed to the user and the redirect
* target can be retrieved with getPageName().
* - `false`: (default) the requested pageName will be used without regard to any redirect.
* @param {boolean} [followCrossNsRedirect=true] - Not applicable if `followRedirect` is not set true.
* - `true`: (default) follow redirect even if it is a cross-namespace redirect
* - `false`: don't follow redirect if it is cross-namespace, edit the redirect itself.
*/
this.setFollowRedirect = function(followRedirect, followCrossNsRedirect) {
if (ctx.pageLoaded) {
ctx.statusElement.error('Internal error: cannot change redirect setting after the page has been loaded!');
return;
}
ctx.followRedirect = followRedirect;
ctx.followCrossNsRedirect = typeof followCrossNsRedirect !== 'undefined' ? followCrossNsRedirect : ctx.followCrossNsRedirect;
};
// lookup-creation setter function
/**
* @param {boolean} flag - If set true, the author and timestamp of
* the first non-redirect version of the page is retrieved.
*
* Warning:
* 1. If there are no revisions among the first 50 that are
* non-redirects, or if there are less 50 revisions and all are
* redirects, the original creation is retrieved.
* 2. Revisions that the user is not privileged to access
* (revdeled/suppressed) will be treated as non-redirects.
* 3. Must not be used when the page has a non-wikitext contentmodel
* such as Modulespace Lua or user JavaScript/CSS.
*/
this.setLookupNonRedirectCreator = function(flag) {
ctx.lookupNonRedirectCreator = flag;
};
// Move-related setter functions
/** @param {string} destination */
this.setMoveDestination = function(destination) {
ctx.moveDestination = destination;
};
/** @param {boolean} flag */
this.setMoveTalkPage = function(flag) {
ctx.moveTalkPage = !!flag;
};
/** @param {boolean} flag */
this.setMoveSubpages = function(flag) {
ctx.moveSubpages = !!flag;
};
/** @param {boolean} flag */
this.setMoveSuppressRedirect = function(flag) {
ctx.moveSuppressRedirect = !!flag;
};
// Protect-related setter functions
/**
* @param {string} level - The right required for the specific action
* e.g. autoconfirmed, sysop, templateeditor, extendedconfirmed
* (enWiki-only).
* @param {string} [expiry=infinity]
*/
this.setEditProtection = function(level, expiry) {
ctx.protectEdit = { level: level, expiry: expiry || 'infinity' };
};
this.setMoveProtection = function(level, expiry) {
ctx.protectMove = { level: level, expiry: expiry || 'infinity' };
};
this.setCreateProtection = function(level, expiry) {
ctx.protectCreate = { level: level, expiry: expiry || 'infinity' };
};
this.setCascadingProtection = function(flag) {
ctx.protectCascade = !!flag;
};
this.suppressProtectWarning = function() {
ctx.suppressProtectWarning = true;
};
// Delete-related setter
/** @param {boolean} flag */
this.setDeleteTalkPage = function(flag) {
ctx.deleteTalkPage = !!flag;
};
// Undelete-related setter
/** @param {boolean} flag */
this.setUndeleteTalkPage = function(flag) {
ctx.undeleteTalkPage = !!flag;
};
// Revert-related getters/setters:
this.setOldID = function(oldID) {
ctx.revertOldID = oldID;
};
/** @return {string} The current revision ID of the page */
this.getCurrentID = function() {
return ctx.revertCurID;
};
/** @return {string} Last editor of the page */
this.getRevisionUser = function() {
return ctx.revertUser;
};
/** @return {string} ISO 8601 timestamp at which the page was last edited. */
this.getLastEditTime = function() {
return ctx.lastEditTime;
};
// Miscellaneous getters/setters:
/**
* Define an object for use in a callback function.
*
* `callbackParameters` is for use by the caller only. The parameters
* allow a caller to pass the proper context into its callback
* function. Callers must ensure that any changes to the
* callbackParameters object within a `load()` callback still permit a
* proper re-entry into the `load()` callback if an edit conflict is
* detected upon calling `save()`.
*
* @param {Object} callbackParameters
*/
this.setCallbackParameters = function(callbackParameters) {
ctx.callbackParameters = callbackParameters;
};
/**
* @return {Object} - The object previously set by `setCallbackParameters()`.
*/
this.getCallbackParameters = function() {
return ctx.callbackParameters;
};
/**
* @param {Morebits.Status} statusElement
*/
this.setStatusElement = function(statusElement) {
ctx.statusElement = statusElement;
};
/**
* @return {Morebits.Status} Status element created by the constructor.
*/
this.getStatusElement = function() {
return ctx.statusElement;
};
/**
* @param {string} level - The right required for edits not to require
* review. Possible options: none, autoconfirmed, review (not on enWiki).
* @param {string} [expiry=infinity]
*/
this.setFlaggedRevs = function(level, expiry) {
ctx.flaggedRevs = { level: level, expiry: expiry || 'infinity' };
};
/**
* @return {boolean} True if the page existed on the wiki when it was last loaded.
*/
this.exists = function() {
return ctx.pageExists;
};
/**
* @return {string} Page ID of the page loaded. 0 if the page doesn't
* exist.
*/
this.getPageID = function() {
return ctx.pageID;
};
/**
* @return {string} - Content model of the page. Possible values
* include (but may not be limited to): `wikitext`, `javascript`,
* `css`, `json`, `Scribunto`, `sanitized-css`, `MassMessageListContent`.
* Also gettable via `mw.config.get('wgPageContentModel')`.
*/
this.getContentModel = function() {
return ctx.contentModel;
};
/**
* @return {boolean|string} - Watched status of the page. Boolean
* unless it's being watched temporarily, in which case returns the
* expiry string.
*/
this.getWatched = function() {
return ctx.watched;
};
/**
* @return {string} ISO 8601 timestamp at which the page was last loaded.
*/
this.getLoadTime = function() {
return ctx.loadTime;
};
/**
* @return {string} The user who created the page following `lookupCreation()`.
*/
this.getCreator = function() {
return ctx.creator;
};
/**
* @return {string} The ISOString timestamp of page creation following `lookupCreation()`.
*/
this.getCreationTimestamp = function() {
return ctx.timestamp;
};
/** @return {boolean} whether or not you can edit the page */
this.canEdit = function() {
return !!ctx.testActions && ctx.testActions.includes('edit');
};
/**
* Retrieves the username of the user who created the page as well as
* the timestamp of creation. The username can be retrieved using the
* `getCreator()` function; the timestamp can be retrieved using the
* `getCreationTimestamp()` function.
* Prior to June 2019 known as `lookupCreator()`.
*
* @param {Function} onSuccess - Callback function to be called when
* the username and timestamp are found within the callback.
* @param {Function} [onFailure] - Callback function to be called when
* the lookup fails
*/
this.lookupCreation = function(onSuccess, onFailure) {
ctx.onLookupCreationSuccess = onSuccess;
ctx.onLookupCreationFailure = onFailure || emptyFunction;
if (!onSuccess) {
ctx.statusElement.error('Internal error: no onSuccess callback provided to lookupCreation()!');
ctx.onLookupCreationFailure(this);
return;
}
const query = {
action: 'query',
prop: 'revisions',
titles: ctx.pageName,
rvlimit: 1,
rvprop: 'user|timestamp',
rvdir: 'newer',
format: 'json'
};
// Only the wikitext content model can reliably handle
// rvsection, others return an error when paired with the
// content rvprop. Relatedly, non-wikitext models don't
// understand the #REDIRECT concept, so we shouldn't attempt
// the redirect resolution in fnLookupCreationSuccess
if (ctx.lookupNonRedirectCreator) {
query.rvsection = 0;
query.rvprop += '|content';
}
if (ctx.followRedirect) {
query.redirects = ''; // follow all redirects
}
ctx.lookupCreationApi = new Morebits.wiki.Api('ページ作成情報の取得中', query, fnLookupCreationSuccess, ctx.statusElement, ctx.onLookupCreationFailure);
ctx.lookupCreationApi.setParent(this);
ctx.lookupCreationApi.post();
};
/**
* Reverts a page to `revertOldID` set by `setOldID`.
*
* @param {Function} [onSuccess] - Callback function to run on success.
* @param {Function} [onFailure] - Callback function to run on failure.
*/
this.revert = function(onSuccess, onFailure) {
ctx.onSaveSuccess = onSuccess;
ctx.onSaveFailure = onFailure || emptyFunction;
if (!ctx.revertOldID) {
ctx.statusElement.error('Internal error: revision ID to revert to was not set before revert!');
ctx.onSaveFailure(this);
return;
}
ctx.editMode = 'revert';
this.load(fnAutoSave, ctx.onSaveFailure);
};
/**
* Moves a page to another title.
*
* @param {Function} [onSuccess] - Callback function to run on success.
* @param {Function} [onFailure] - Callback function to run on failure.
*/
this.move = function(onSuccess, onFailure) {
ctx.onMoveSuccess = onSuccess;
ctx.onMoveFailure = onFailure || emptyFunction;
if (!fnPreflightChecks.call(this, 'move', ctx.onMoveFailure)) {
return; // abort
}
if (!ctx.moveDestination) {
ctx.statusElement.error('Internal error: destination page name was not set before move!');
ctx.onMoveFailure(this);
return;
}
if (fnCanUseMwUserToken('move')) {
fnProcessMove.call(this, this);
} else {
const query = fnNeedTokenInfoQuery('move');
ctx.moveApi = new Morebits.wiki.Api('トークンの取得中...', query, fnProcessMove, ctx.statusElement, ctx.onMoveFailure);
ctx.moveApi.setParent(this);
ctx.moveApi.post();
}
};
/**
* Marks the page as patrolled, using `rcid` (if available) or `revid`.
*
* Patrolling as such doesn't need to rely on loading the page in
* question; simply passing a revid to the API is sufficient, so in
* those cases just using {@link Morebits.wiki.Api} is probably preferable.
*
* No error handling since we don't actually care about the errors.
*/
this.patrol = function() {
if (!Morebits.userIsSysop && !Morebits.userIsInGroup('patroller')) {
return;
}
// If a link is present, don't need to check if it's patrolled
if ($('.patrollink').length) {
const patrolhref = $('.patrollink a').attr('href');
ctx.rcid = mw.util.getParamValue('rcid', patrolhref);
fnProcessPatrol(this, this);
} else {
const patrolQuery = {
action: 'query',
prop: 'info',
meta: 'tokens',
type: 'patrol', // as long as we're querying, might as well get a token
list: 'recentchanges', // check if the page is unpatrolled
titles: ctx.pageName,
rcprop: 'patrolled',
rctitle: ctx.pageName,
rclimit: 1,
format: 'json'
};
ctx.patrolApi = new Morebits.wiki.Api('トークンの取得中...', patrolQuery, fnProcessPatrol);
ctx.patrolApi.setParent(this);
ctx.patrolApi.post();
}
};
/**
* Marks the page as reviewed by the PageTriage extension.
*
* Will, by it's nature, mark as patrolled as well. Falls back to
* patrolling if not in an appropriate namespace.
*
* Doesn't inherently rely on loading the page in question; simply
* passing a `pageid` to the API is sufficient, so in those cases just
* using {@link Morebits.wiki.Api} is probably preferable.
*
* Will first check if the page is queued via
* {@link Morebits.wiki.Page~fnProcessTriageList|fnProcessTriageList}.
*
* No error handling since we don't actually care about the errors.
*
* @see {@link https://www.mediawiki.org/wiki/Extension:PageTriage} Referred to as "review" on-wiki.
*/
this.triage = function() {
// Fall back to patrol if not a valid triage namespace
if (!mw.config.get('pageTriageNamespaces').includes(new mw.Title(ctx.pageName).getNamespaceId())) {
this.patrol();
} else {
if (!Morebits.userIsSysop && !Morebits.userIsInGroup('patroller')) {
return;
}
// If on the page in question, don't need to query for page ID
if (new mw.Title(Morebits.pageNameNorm).getPrefixedText() === new mw.Title(ctx.pageName).getPrefixedText()) {
ctx.pageID = mw.config.get('wgArticleId');
fnProcessTriageList(this, this);
} else {
const query = fnNeedTokenInfoQuery('triage');
ctx.triageApi = new Morebits.wiki.Api('トークンの取得中...', query, fnProcessTriageList);
ctx.triageApi.setParent(this);
ctx.triageApi.post();
}
}
};
// |delete| is a reserved word in some flavours of JS
/**
* Deletes a page (for admins only).
*
* @param {Function} [onSuccess] - Callback function to run on success.
* @param {Function} [onFailure] - Callback function to run on failure.
*/
this.deletePage = function(onSuccess, onFailure) {
ctx.onDeleteSuccess = onSuccess;
ctx.onDeleteFailure = onFailure || emptyFunction;
if (!fnPreflightChecks.call(this, 'delete', ctx.onDeleteFailure)) {
return; // abort
}
if (fnCanUseMwUserToken('delete')) {
fnProcessDelete.call(this, this);
} else {
const query = fnNeedTokenInfoQuery('delete');
ctx.deleteApi = new Morebits.wiki.Api('トークンの取得中...', query, fnProcessDelete, ctx.statusElement, ctx.onDeleteFailure);
ctx.deleteApi.setParent(this);
ctx.deleteApi.post();
}
};
/**
* Undeletes a page (for admins only).
*
* @param {Function} [onSuccess] - Callback function to run on success.
* @param {Function} [onFailure] - Callback function to run on failure.
*/
this.undeletePage = function(onSuccess, onFailure) {
ctx.onUndeleteSuccess = onSuccess;
ctx.onUndeleteFailure = onFailure || emptyFunction;
if (!fnPreflightChecks.call(this, 'undelete', ctx.onUndeleteFailure)) {
return; // abort
}
if (fnCanUseMwUserToken('undelete')) {
fnProcessUndelete.call(this, this);
} else {
const query = fnNeedTokenInfoQuery('undelete');
ctx.undeleteApi = new Morebits.wiki.Api('トークンの取得中...', query, fnProcessUndelete, ctx.statusElement, ctx.onUndeleteFailure);
ctx.undeleteApi.setParent(this);
ctx.undeleteApi.post();
}
};
/**
* Protects a page (for admins only).
*
* @param {Function} [onSuccess] - Callback function to run on success.
* @param {Function} [onFailure] - Callback function to run on failure.
*/
this.protect = function(onSuccess, onFailure) {
ctx.onProtectSuccess = onSuccess;
ctx.onProtectFailure = onFailure || emptyFunction;
if (!fnPreflightChecks.call(this, 'protect', ctx.onProtectFailure)) {
return; // abort
}
if (!ctx.protectEdit && !ctx.protectMove && !ctx.protectCreate) {
ctx.statusElement.error('Internal error: you must set edit and/or move and/or create protection before calling protect()!');
ctx.onProtectFailure(this);
return;
}
// because of the way MW API interprets protection levels
// (absolute, not differential), we always need to request
// protection levels from the server
const query = fnNeedTokenInfoQuery('protect');
ctx.protectApi = new Morebits.wiki.Api('トークンの取得中...', query, fnProcessProtect, ctx.statusElement, ctx.onProtectFailure);
ctx.protectApi.setParent(this);
ctx.protectApi.post();
};
/**
* Apply FlaggedRevs protection settings. Only works on wikis where
* the extension is installed (`$wgFlaggedRevsProtection = true`
* i.e. where FlaggedRevs settings appear on the "protect" tab).
*
* @see {@link https://www.mediawiki.org/wiki/Extension:FlaggedRevs}
* Referred to as "pending changes" on-wiki.
*
* @param {Function} [onSuccess]
* @param {Function} [onFailure]
*/
this.stabilize = function(onSuccess, onFailure) {
ctx.onStabilizeSuccess = onSuccess;
ctx.onStabilizeFailure = onFailure || emptyFunction;
if (!fnPreflightChecks.call(this, 'FlaggedRevs', ctx.onStabilizeFailure)) {
return; // abort
}
if (!ctx.flaggedRevs) {
ctx.statusElement.error('Internal error: you must set flaggedRevs before calling stabilize()!');
ctx.onStabilizeFailure(this);
return;
}
if (fnCanUseMwUserToken('stabilize')) {
fnProcessStabilize.call(this, this);
} else {
const query = fnNeedTokenInfoQuery('stabilize');
ctx.stabilizeApi = new Morebits.wiki.Api('トークンの取得中...', query, fnProcessStabilize, ctx.statusElement, ctx.onStabilizeFailure);
ctx.stabilizeApi.setParent(this);
ctx.stabilizeApi.post();
}
};
/*
* Private member functions
* These are not exposed outside
*/
/**
* Determines whether we can save an API call by using the csrf token
* sent with the page HTML, or whether we need to ask the server for
* more info (e.g. protection or watchlist expiry).
*
* Currently used for `append`, `prepend`, `newSection`, `move`,
* `stabilize`, `deletePage`, and `undeletePage`. Not used for
* `protect` since it always needs to request protection status.
*
* @param {string} [action=edit] - The action being undertaken, e.g.
* "edit" or "delete". In practice, only "edit" or "notedit" matters.
* @return {boolean}
*/
var fnCanUseMwUserToken = function(action = 'edit') {
// If a watchlist expiry is set, we must always load the page
// to avoid overwriting indefinite protection. Of course, not
// needed if setting indefinite watching!
if (ctx.watchlistExpiry && !Morebits.string.isInfinity(ctx.watchlistExpiry)) {
return false;
}
// API-based redirect resolution only works for action=query and
// action=edit in append/prepend/new modes
if (ctx.followRedirect) {
if (!ctx.followCrossNsRedirect) {
return false; // must load the page to check for cross namespace redirects
}
if (action !== 'edit' || (ctx.editMode === 'all' || ctx.editMode === 'revert')) {
return false;
}
}
// do we need to fetch the edit protection expiry?
if (Morebits.userIsSysop && !ctx.suppressProtectWarning) {
if (new mw.Title(Morebits.pageNameNorm).getPrefixedText() !== new mw.Title(ctx.pageName).getPrefixedText()) {
return false;
}
// wgRestrictionEdit is null on non-existent pages,
// so this neatly handles nonexistent pages
const editRestriction = mw.config.get('wgRestrictionEdit');
if (!editRestriction || editRestriction.includes('sysop')) {
return false;
}
}
return !!mw.user.tokens.get('csrfToken');
};
/**
* When functions can't use
* {@link Morebits.wiki.Page~fnCanUseMwUserToken|fnCanUseMwUserToken}
* or require checking protection or watched status, maintain the query
* in one place. Used for {@link Morebits.wiki.Page#deletePage|delete},
* {@link Morebits.wiki.Page#undeletePage|undelete},
* {@link Morebits.wiki.Page#protect|protect},
* {@link Morebits.wiki.Page#stabilize|stabilize},
* and {@link Morebits.wiki.Page#move|move}
* (basically, just not {@link Morebits.wiki.Page#load|load}).
*
* @param {string} action - The action being undertaken, e.g. "edit" or
* "delete".
* @return {Object} Appropriate query.
*/
var fnNeedTokenInfoQuery = function(action) {
const query = {
action: 'query',
meta: 'tokens',
type: 'csrf',
titles: ctx.pageName,
prop: 'info',
inprop: 'watched',
format: 'json'
};
// Protection not checked for flagged-revs or non-sysop moves
if (action !== 'stabilize' && (action !== 'move' || Morebits.userIsSysop)) {
query.inprop += '|protection';
}
if (ctx.followRedirect && action !== 'undelete') {
query.redirects = ''; // follow all redirects
}
return query;
};
// callback from loadSuccess() for append(), prepend(), and newSection() threads
var fnAutoSave = function(pageobj) {
pageobj.save(ctx.onSaveSuccess, ctx.onSaveFailure);
};
// callback from loadApi.post()
var fnLoadSuccess = function() {
const response = ctx.loadApi.getResponse().query;
if (!fnCheckPageName(response, ctx.onLoadFailure)) {
return; // abort
}
const page = response.pages[0];
let rev;
ctx.pageExists = !page.missing;
if (ctx.pageExists) {
rev = page.revisions[0];
ctx.lastEditTime = rev.timestamp;
ctx.pageText = rev.content;
ctx.pageID = page.pageid;
} else {
ctx.pageText = ''; // allow for concatenation, etc.
ctx.pageID = 0; // nonexistent in response, matches wgArticleId
}
ctx.csrfToken = response.tokens.csrftoken;
if (!ctx.csrfToken) {
ctx.statusElement.error('編集トークンの取得に失敗しました。');
ctx.onLoadFailure(this);
return;
}
ctx.loadTime = ctx.loadApi.getResponse().curtimestamp;
if (!ctx.loadTime) {
ctx.statusElement.error('現在のタイムスタンプの取得に失敗しました。');
ctx.onLoadFailure(this);
return;
}
ctx.contentModel = page.contentmodel;
ctx.watched = page.watchlistexpiry || page.watched;
// extract protection info, to alert admins when they are about to edit a protected page
// Includes cascading protection
if (Morebits.userIsSysop) {
const editProt = page.protection.filter((pr) => pr.type === 'edit' && pr.level === 'sysop').pop();
if (editProt) {
ctx.fullyProtected = editProt.expiry;
} else {
ctx.fullyProtected = false;
}
}
ctx.revertCurID = page.lastrevid;
const testactions = page.actions;
ctx.testActions = []; // was null
Object.keys(testactions).forEach((action) => {
if (testactions[action]) {
ctx.testActions.push(action);
}
});
if (ctx.editMode === 'revert') {
ctx.revertCurID = rev && rev.revid;
if (!ctx.revertCurID) {
ctx.statusElement.error('Failed to retrieve current revision ID.');
ctx.onLoadFailure(this);
return;
}
ctx.revertUser = rev && rev.user;
if (!ctx.revertUser) {
if (rev && rev.userhidden) { // username was RevDel'd or oversighted
ctx.revertUser = '<username hidden>';
} else {
ctx.statusElement.error('Failed to retrieve user who made the revision.');
ctx.onLoadFailure(this);
return;
}
}
// set revert edit summary
ctx.editSummary = '[[Help:Revert|Reverted]] to revision ' + ctx.revertOldID + ' by ' + ctx.revertUser + ': ' + ctx.editSummary;
}
ctx.pageLoaded = true;
// alert("Generate edit conflict now"); // for testing edit conflict recovery logic
ctx.onLoadSuccess(this); // invoke callback
};
// callback from parseApi.post()
var fnParseSuccess = function() {
if (ctx.parseApi.getResponse().error) {
ctx.statusElement.error('ページの取得に失敗しました。');
ctx.onParseFailure(this);
return;
}
var response = ctx.parseApi.getResponse().parse;
ctx.pageParsed = true;
ctx.sections = response.sections;
// alert("Generate edit conflict now"); // for testing edit conflict recovery logic
ctx.onParseSuccess(this); // invoke callback
};
// helper function to parse the page name returned from the API
var fnCheckPageName = function(response, onFailure) {
if (!onFailure) {
onFailure = emptyFunction;
}
const page = response.pages && response.pages[0];
if (page) {
// check for invalid titles
if (page.invalid) {
ctx.statusElement.error('無効なページタイトル: ' + ctx.pageName);
onFailure(this);
return false; // abort
}
// retrieve actual title of the page after normalization and redirects
const resolvedName = page.title;
if (response.redirects) {
// check for cross-namespace redirect:
const origNs = new mw.Title(ctx.pageName).namespace;
const newNs = new mw.Title(resolvedName).namespace;
if (origNs !== newNs && !ctx.followCrossNsRedirect) {
ctx.statusElement.error(ctx.pageName + 'は' + resolvedName + 'への名前空間が違うリダイレクトです。中止します。');
onFailure(this);
return false;
}
// only notify user for redirects, not normalization
new Morebits.Status('Note', ctx.pageName + 'から' + resolvedName + 'へのリダイレクト');
}
ctx.pageName = resolvedName; // update to redirect target or normalized name
} else {
// could be a circular redirect or other problem
ctx.statusElement.error('Could not resolve redirects for: ' + ctx.pageName);
onFailure(this);
// force error to stay on the screen
++Morebits.wiki.numberOfActionsLeft;
return false; // abort
}
return true; // all OK
};
/**
* Determine whether we should provide a watchlist expiry. Will not
* do so if the page is currently permanently watched, or the current
* expiry is *after* the new, provided expiry. Only handles strings
* recognized by {@link Morebits.Date} or relative timeframes with
* unit it can process. Relies on the fact that fnCanUseMwUserToken
* requires page loading if a watchlistexpiry is provided, so we are
* ensured of knowing the watch status by the use of this.
*
* @return {boolean}
*/
var fnApplyWatchlistExpiry = function() {
if (ctx.watchlistExpiry) {
if (!ctx.watched || Morebits.string.isInfinity(ctx.watchlistExpiry)) {
return true;
} else if (typeof ctx.watched === 'string') {
let newExpiry;
// Attempt to determine if the new expiry is a
// relative (e.g. `1 month`) or absolute datetime
const rel = ctx.watchlistExpiry.split(' ');
try {
newExpiry = new Morebits.Date().add(rel[0], rel[1]);
} catch (e) {
newExpiry = new Morebits.Date(ctx.watchlistExpiry);
}
// If the date is valid, only use it if it extends the current expiry
if (newExpiry.isValid()) {
if (newExpiry.isAfter(new Morebits.Date(ctx.watched))) {
return true;
}
} else {
// If it's still not valid, hope it's a valid MW expiry format that
// Morebits.Date doesn't recognize, so just default to using it.
// This will also include minor typos.
return true;
}
}
}
return false;
};
// callback from saveApi.post()
var fnSaveSuccess = function() {
ctx.editMode = 'all'; // cancel append/prepend/newSection/revert modes
const response = ctx.saveApi.getResponse();
// see if the API thinks we were successful
if (response.edit.result === 'Success') {
// real success
// default on success action - display link for edited page
const link = document.createElement('a');
link.setAttribute('href', mw.util.getUrl(ctx.pageName));
link.appendChild(document.createTextNode(ctx.pageName));
ctx.statusElement.info(['完了 (', link, ')']);
if (ctx.onSaveSuccess) {
ctx.onSaveSuccess(this); // invoke callback
}
return;
}
// errors here are only generated by extensions which hook APIEditBeforeSave within MediaWiki,
// which as of 1.34.0-wmf.23 (Sept 2019) should only encompass captcha messages
if (response.edit.captcha) {
ctx.statusElement.error('ウィキのサーバーがキャプチャを入力するよう要求したため、ページを保存できませんでした。');
} else {
ctx.statusElement.error('ページの保存中にAPIから不明なエラーを受信');
}
// force error to stay on the screen
++Morebits.wiki.numberOfActionsLeft;
ctx.onSaveFailure(this);
};
// callback from saveApi.post()
var fnSaveError = function() {
const errorCode = ctx.saveApi.getErrorCode();
// check for edit conflict
if (errorCode === 'editconflict' && ctx.conflictRetries++ < ctx.maxConflictRetries) {
// edit conflicts can occur when the page needs to be purged from the server cache
const purgeQuery = {
action: 'purge',
titles: ctx.pageName // redirects are already resolved
};
var purgeApi = new Morebits.wiki.Api('編集の競合が検出されたため、サーバーのキャッシュを破棄します。', purgeQuery, (() => {
--Morebits.wiki.numberOfActionsLeft; // allow for normal completion if retry succeeds
ctx.statusElement.info('編集の競合が検出されたため、編集をやり直します');
if (fnCanUseMwUserToken('edit')) {
ctx.saveApi.post(); // necessarily append, prepend, or newSection, so this should work as desired
} else {
ctx.loadApi.post(); // reload the page and reapply the edit
}
}), ctx.statusElement);
purgeApi.post();
// check for network or server error
} else if ((errorCode === null || errorCode === undefined) && ctx.retries++ < ctx.maxRetries) {
// the error might be transient, so try again
ctx.statusElement.info('保存に失敗しました。2秒後に再試行します ...');
--Morebits.wiki.numberOfActionsLeft; // allow for normal completion if retry succeeds
// wait for sometime for client to regain connectivity
sleep(2000).then(() => {
ctx.saveApi.post(); // give it another go!
});
// hard error, give up
} else {
const response = ctx.saveApi.getResponse();
const errorData = response.error || // bc error format
response.errors[0].data; // html/wikitext/plaintext error format
switch (errorCode) {
case 'protectedpage':
// non-admin attempting to edit a protected page - this gives a friendlier message than the default
ctx.statusElement.error('Failed to save edit: Page is protected');
break;
case 'abusefilter-disallowed':
ctx.statusElement.error('The edit was disallowed by the edit filter: "' + errorData.abusefilter.description + '".');
break;
case 'abusefilter-warning':
ctx.statusElement.error(['A warning was returned by the edit filter: "', errorData.abusefilter.description, '". If you wish to proceed with the edit, please carry it out again. This warning will not appear a second time.']);
// We should provide the user with a way to automatically retry the action if they so choose -
// I can't see how to do this without creating a UI dependency on Morebits.wiki.Page though -- TTO
break;
case 'spamblacklist':
// If multiple items are blacklisted, we only return the first
var spam = errorData.spamblacklist.matches[0];
ctx.statusElement.error('Could not save the page because the URL ' + spam + ' is on the spam blacklist');
break;
default:
ctx.statusElement.error('Failed to save edit: ' + ctx.saveApi.getErrorText());
}
ctx.editMode = 'all'; // cancel append/prepend/newSection/revert modes
if (ctx.onSaveFailure) {
ctx.onSaveFailure(this); // invoke callback
}
}
};
const isTextRedirect = function(text) {
if (!text) { // no text - content empty or inaccessible (revdelled or suppressed)
return false;
}
return Morebits.l10n.redirectTagAliases.some((tag) => new RegExp('^\\s*' + tag + '\\W', 'i').test(text));
};
var fnLookupCreationSuccess = function() {
const response = ctx.lookupCreationApi.getResponse().query;
if (!fnCheckPageName(response, ctx.onLookupCreationFailure)) {
return; // abort
}
const rev = response.pages[0].revisions && response.pages[0].revisions[0];
if (!rev) {
ctx.statusElement.error('Could not find any revisions of ' + ctx.pageName);
ctx.onLookupCreationFailure(this);
return;
}
if (!ctx.lookupNonRedirectCreator || !isTextRedirect(rev.content)) {
ctx.creator = rev.user;
if (!ctx.creator) {
ctx.statusElement.error('Could not find name of page creator');
ctx.onLookupCreationFailure(this);
return;
}
ctx.timestamp = rev.timestamp;
if (!ctx.timestamp) {
ctx.statusElement.error('Could not find timestamp of page creation');
ctx.onLookupCreationFailure(this);
return;
}
ctx.statusElement.info('retrieved page creation information');
ctx.onLookupCreationSuccess(this);
} else {
ctx.lookupCreationApi.query.rvlimit = 50; // modify previous query to fetch more revisions
ctx.lookupCreationApi.query.titles = ctx.pageName; // update pageName if redirect resolution took place in earlier query
ctx.lookupCreationApi = new Morebits.wiki.Api('ページ作成情報の取得中', ctx.lookupCreationApi.query, fnLookupNonRedirectCreator, ctx.statusElement, ctx.onLookupCreationFailure);
ctx.lookupCreationApi.setParent(this);
ctx.lookupCreationApi.post();
}
};
var fnLookupNonRedirectCreator = function() {
const response = ctx.lookupCreationApi.getResponse().query;
const revs = response.pages[0].revisions;
for (let i = 0; i < revs.length; i++) {
if (!isTextRedirect(revs[i].content)) {
ctx.creator = revs[i].user;
ctx.timestamp = revs[i].timestamp;
break;
}
}
if (!ctx.creator) {
// fallback to give first revision author if no non-redirect version in the first 50
ctx.creator = revs[0].user;
ctx.timestamp = revs[0].timestamp;
if (!ctx.creator) {
ctx.statusElement.error('Could not find name of page creator');
ctx.onLookupCreationFailure(this);
return;
}
}
if (!ctx.timestamp) {
ctx.statusElement.error('Could not find timestamp of page creation');
ctx.onLookupCreationFailure(this);
return;
}
ctx.statusElement.info('retrieved page creation information');
ctx.onLookupCreationSuccess(this);
};
/**
* Common checks for action methods. Used for move, undelete, delete,
* protect, stabilize.
*
* @param {string} action - The action being checked.
* @param {string} onFailure - Failure callback.
* @return {boolean}
*/
var fnPreflightChecks = function(action, onFailure) {
// if a non-admin tries to do this, don't bother
if (!Morebits.userIsSysop && action !== 'move') {
ctx.statusElement.error('Cannot ' + action + 'page : only admins can do that');
onFailure(this);
return false;
}
if (!ctx.editSummary) {
ctx.statusElement.error('Internal error: ' + action + ' reason not set (use setEditSummary function)!');
onFailure(this);
return false;
}
return true; // all OK
};
/**
* Common checks for fnProcess functions (`fnProcessDelete`, `fnProcessMove`, etc.
* Used for move, undelete, delete, protect, stabilize.
*
* @param {string} action - The action being checked.
* @param {string} onFailure - Failure callback.
* @param {string} response - The response document from the API call.
* @return {boolean}
*/
const fnProcessChecks = function(action, onFailure, response) {
const missing = response.pages[0].missing;
// No undelete as an existing page could have deleted revisions
const actionMissing = missing && ['delete', 'stabilize', 'move'].includes(action);
const protectMissing = action === 'protect' && missing && (ctx.protectEdit || ctx.protectMove);
const saltMissing = action === 'protect' && !missing && ctx.protectCreate;
if (actionMissing || protectMissing || saltMissing) {
ctx.statusElement.error('Cannot ' + action + ' the page because it ' + (missing ? 'no longer' : 'already') + ' exists');
onFailure(this);
return false;
}
// Delete, undelete, move
// extract protection info
let editprot;
if (action === 'undelete') {
editprot = response.pages[0].protection.filter((pr) => pr.type === 'create' && pr.level === 'sysop').pop();
} else if (action === 'delete' || action === 'move') {
editprot = response.pages[0].protection.filter((pr) => pr.type === 'edit' && pr.level === 'sysop').pop();
}
if (editprot && !ctx.suppressProtectWarning &&
!confirm('You are about to ' + action + ' the fully protected page "' + ctx.pageName +
(editprot.expiry === 'infinity' ? '" (protected indefinitely)' : '" (protection expiring ' + new Morebits.Date(editprot.expiry).calendar('utc') + ' (UTC))') +
'. \n\nClick OK to proceed with ' + action + ', or Cancel to skip.')) {
ctx.statusElement.error('Aborted ' + action + ' on fully protected page.');
onFailure(this);
return false;
}
if (!response.tokens.csrftoken) {
ctx.statusElement.error('Failed to retrieve token.');
onFailure(this);
return false;
}
return true; // all OK
};
var fnProcessMove = function() {
let pageTitle, token;
if (fnCanUseMwUserToken('move')) {
token = mw.user.tokens.get('csrfToken');
pageTitle = ctx.pageName;
} else {
const response = ctx.moveApi.getResponse().query;
if (!fnProcessChecks('move', ctx.onMoveFailure, response)) {
return; // abort
}
token = response.tokens.csrftoken;
const page = response.pages[0];
pageTitle = page.title;
ctx.watched = page.watchlistexpiry || page.watched;
}
const query = {
action: 'move',
from: pageTitle,
to: ctx.moveDestination,
token: token,
reason: ctx.editSummary,
watchlist: ctx.watchlistOption,
format: 'json'
};
if (ctx.changeTags) {
query.tags = ctx.changeTags;
}
if (fnApplyWatchlistExpiry()) {
query.watchlistexpiry = ctx.watchlistExpiry;
}
if (ctx.moveTalkPage) {
query.movetalk = 'true';
}
if (ctx.moveSubpages) {
query.movesubpages = 'true';
}
if (ctx.moveSuppressRedirect) {
query.noredirect = 'true';
}
ctx.moveProcessApi = new Morebits.wiki.Api('ページの移動...', query, ctx.onMoveSuccess, ctx.statusElement, ctx.onMoveFailure);
ctx.moveProcessApi.setParent(this);
ctx.moveProcessApi.post();
};
var fnProcessPatrol = function() {
const query = {
action: 'patrol',
format: 'json'
};
// Didn't need to load the page
if (ctx.rcid) {
query.rcid = ctx.rcid;
query.token = mw.user.tokens.get('patrolToken');
} else {
const response = ctx.patrolApi.getResponse().query;
// Don't patrol if not unpatrolled
if (!response.recentchanges[0].unpatrolled) {
return;
}
const lastrevid = response.pages[0].lastrevid;
if (!lastrevid) {
return;
}
query.revid = lastrevid;
const token = response.tokens.csrftoken;
if (!token) {
return;
}
query.token = token;
}
if (ctx.changeTags) {
query.tags = ctx.changeTags;
}
const patrolStat = new Morebits.Status('Marking page as patrolled');
ctx.patrolProcessApi = new Morebits.wiki.Api('patrolling page...', query, null, patrolStat);
ctx.patrolProcessApi.setParent(this);
ctx.patrolProcessApi.post();
};
// Ensure that the page is curatable
var fnProcessTriageList = function() {
if (ctx.pageID) {
ctx.csrfToken = mw.user.tokens.get('csrfToken');
} else {
const response = ctx.triageApi.getResponse().query;
ctx.pageID = response.pages[0].pageid;
if (!ctx.pageID) {
return;
}
ctx.csrfToken = response.tokens.csrftoken;
if (!ctx.csrfToken) {
return;
}
}
const query = {
action: 'pagetriagelist',
page_id: ctx.pageID,
format: 'json'
};
ctx.triageProcessListApi = new Morebits.wiki.Api('checking curation status...', query, fnProcessTriage);
ctx.triageProcessListApi.setParent(this);
ctx.triageProcessListApi.post();
};
// callback from triageProcessListApi.post()
var fnProcessTriage = function() {
const responseList = ctx.triageProcessListApi.getResponse().pagetriagelist;
// Exit if not in the queue
if (!responseList || responseList.result !== 'success') {
return;
}
const page = responseList.pages && responseList.pages[0];
// Do nothing if page already triaged/patrolled
if (!page || !parseInt(page.patrol_status, 10)) {
const query = {
action: 'pagetriageaction',
pageid: ctx.pageID,
reviewed: 1,
token: ctx.csrfToken,
format: 'json'
};
if (ctx.changeTags) {
query.tags = ctx.changeTags;
}
const triageStat = new Morebits.Status('Marking page as curated');
ctx.triageProcessApi = new Morebits.wiki.Api('curating page...', query, null, triageStat);
ctx.triageProcessApi.setParent(this);
ctx.triageProcessApi.post();
}
};
var fnProcessDelete = function() {
let pageTitle, token;
if (fnCanUseMwUserToken('delete')) {
token = mw.user.tokens.get('csrfToken');
pageTitle = ctx.pageName;
} else {
const response = ctx.deleteApi.getResponse().query;
if (!fnProcessChecks('delete', ctx.onDeleteFailure, response)) {
return; // abort
}
token = response.tokens.csrftoken;
const page = response.pages[0];
pageTitle = page.title;
ctx.watched = page.watchlistexpiry || page.watched;
}
const query = {
action: 'delete',
title: pageTitle,
token: token,
reason: ctx.editSummary,
watchlist: ctx.watchlistOption,
format: 'json'
};
if (ctx.changeTags) {
query.tags = ctx.changeTags;
}
if (ctx.deleteTalkPage) {
query.deletetalk = 'true';
}
if (fnApplyWatchlistExpiry()) {
query.watchlistexpiry = ctx.watchlistExpiry;
}
ctx.deleteProcessApi = new Morebits.wiki.Api('deleting page...', query, ctx.onDeleteSuccess, ctx.statusElement, fnProcessDeleteError);
ctx.deleteProcessApi.setParent(this);
ctx.deleteProcessApi.post();
};
// callback from deleteProcessApi.post()
var fnProcessDeleteError = function() {
const errorCode = ctx.deleteProcessApi.getErrorCode();
// check for "Database query error"
if (errorCode === 'internal_api_error_DBQueryError' && ctx.retries++ < ctx.maxRetries) {
ctx.statusElement.info('Database query error, retrying');
--Morebits.wiki.numberOfActionsLeft; // allow for normal completion if retry succeeds
ctx.deleteProcessApi.post(); // give it another go!
} else if (errorCode === 'missingtitle') {
ctx.statusElement.error('Cannot delete the page, because it no longer exists');
if (ctx.onDeleteFailure) {
ctx.onDeleteFailure.call(this, ctx.deleteProcessApi); // invoke callback
}
// hard error, give up
} else {
ctx.statusElement.error('Failed to delete the page: ' + ctx.deleteProcessApi.getErrorText());
if (ctx.onDeleteFailure) {
ctx.onDeleteFailure.call(this, ctx.deleteProcessApi); // invoke callback
}
}
};
var fnProcessUndelete = function() {
let pageTitle, token;
if (fnCanUseMwUserToken('undelete')) {
token = mw.user.tokens.get('csrfToken');
pageTitle = ctx.pageName;
} else {
const response = ctx.undeleteApi.getResponse().query;
if (!fnProcessChecks('undelete', ctx.onUndeleteFailure, response)) {
return; // abort
}
token = response.tokens.csrftoken;
const page = response.pages[0];
pageTitle = page.title;
ctx.watched = page.watchlistexpiry || page.watched;
}
const query = {
action: 'undelete',
title: pageTitle,
token: token,
reason: ctx.editSummary,
watchlist: ctx.watchlistOption,
format: 'json'
};
if (ctx.changeTags) {
query.tags = ctx.changeTags;
}
if (ctx.undeleteTalkPage) {
query.undeletetalk = 'true';
}
if (fnApplyWatchlistExpiry()) {
query.watchlistexpiry = ctx.watchlistExpiry;
}
ctx.undeleteProcessApi = new Morebits.wiki.Api('undeleting page...', query, ctx.onUndeleteSuccess, ctx.statusElement, fnProcessUndeleteError);
ctx.undeleteProcessApi.setParent(this);
ctx.undeleteProcessApi.post();
};
// callback from undeleteProcessApi.post()
var fnProcessUndeleteError = function() {
const errorCode = ctx.undeleteProcessApi.getErrorCode();
// check for "Database query error"
if (errorCode === 'internal_api_error_DBQueryError') {
if (ctx.retries++ < ctx.maxRetries) {
ctx.statusElement.info('Database query error, retrying');
--Morebits.wiki.numberOfActionsLeft; // allow for normal completion if retry succeeds
ctx.undeleteProcessApi.post(); // give it another go!
} else {
ctx.statusElement.error('Repeated database query error, please try again');
if (ctx.onUndeleteFailure) {
ctx.onUndeleteFailure.call(this, ctx.undeleteProcessApi); // invoke callback
}
}
} else if (errorCode === 'cantundelete') {
ctx.statusElement.error('Cannot undelete the page, either because there are no revisions to undelete or because it has already been undeleted');
if (ctx.onUndeleteFailure) {
ctx.onUndeleteFailure.call(this, ctx.undeleteProcessApi); // invoke callback
}
// hard error, give up
} else {
ctx.statusElement.error('Failed to undelete the page: ' + ctx.undeleteProcessApi.getErrorText());
if (ctx.onUndeleteFailure) {
ctx.onUndeleteFailure.call(this, ctx.undeleteProcessApi); // invoke callback
}
}
};
var fnProcessProtect = function() {
const response = ctx.protectApi.getResponse().query;
if (!fnProcessChecks('protect', ctx.onProtectFailure, response)) {
return; // abort
}
const token = response.tokens.csrftoken;
const page = response.pages[0];
const pageTitle = page.title;
ctx.watched = page.watchlistexpiry || page.watched;
// Fetch existing protection levels
const prs = response.pages[0].protection;
let editprot, moveprot, createprot;
prs.forEach((pr) => {
// Filter out protection from cascading
if (pr.type === 'edit' && !pr.source) {
editprot = pr;
} else if (pr.type === 'move') {
moveprot = pr;
} else if (pr.type === 'create') {
createprot = pr;
}
});
// Fall back to current levels if not explicitly set
if (!ctx.protectEdit && editprot) {
ctx.protectEdit = { level: editprot.level, expiry: editprot.expiry };
}
if (!ctx.protectMove && moveprot) {
ctx.protectMove = { level: moveprot.level, expiry: moveprot.expiry };
}
if (!ctx.protectCreate && createprot) {
ctx.protectCreate = { level: createprot.level, expiry: createprot.expiry };
}
// Default to pre-existing cascading protection if unchanged (similar to above)
if (ctx.protectCascade === null) {
ctx.protectCascade = !!prs.filter((pr) => pr.cascade).length;
}
// Warn if cascading protection being applied with an invalid protection level,
// which for edit protection will cause cascading to be silently stripped
if (ctx.protectCascade) {
// On move protection, this is technically stricter than the MW API,
// but seems reasonable to avoid dumb values and misleading log entries (T265626)
if (((!ctx.protectEdit || ctx.protectEdit.level !== 'sysop') ||
(!ctx.protectMove || ctx.protectMove.level !== 'sysop')) &&
!confirm('"' + ctx.pageName + '"ではカスケード保護が有効になっていますが、管理者レベルの保護を選択していません。\n\n' +
'OKをクリックすることで調節して管理者レベルのカスケード保護を続行するか、キャンセルをクリックしてこのアクションをスキップします。')) {
ctx.statusElement.error('カスケード保護は中止されました。');
ctx.onProtectFailure(this);
return;
}
ctx.protectEdit.level = 'sysop';
ctx.protectMove.level = 'sysop';
}
// Build protection levels and expirys (expiries?) for query
const protections = [], expirys = [];
if (ctx.protectEdit) {
protections.push('edit=' + ctx.protectEdit.level);
expirys.push(ctx.protectEdit.expiry);
}
if (ctx.protectMove) {
protections.push('move=' + ctx.protectMove.level);
expirys.push(ctx.protectMove.expiry);
}
if (ctx.protectCreate) {
protections.push('create=' + ctx.protectCreate.level);
expirys.push(ctx.protectCreate.expiry);
}
const query = {
action: 'protect',
title: pageTitle,
token: token,
protections: protections.join('|'),
expiry: expirys.join('|'),
reason: ctx.editSummary,
watchlist: ctx.watchlistOption,
format: 'json'
};
// Only shows up in logs, not page history [[phab:T259983]]
if (ctx.changeTags) {
query.tags = ctx.changeTags;
}
if (fnApplyWatchlistExpiry()) {
query.watchlistexpiry = ctx.watchlistExpiry;
}
if (ctx.protectCascade) {
query.cascade = 'true';
}
ctx.protectProcessApi = new Morebits.wiki.Api('protecting page...', query, ctx.onProtectSuccess, ctx.statusElement, ctx.onProtectFailure);
ctx.protectProcessApi.setParent(this);
ctx.protectProcessApi.post();
};
var fnProcessStabilize = function() {
let pageTitle, token;
if (fnCanUseMwUserToken('stabilize')) {
token = mw.user.tokens.get('csrfToken');
pageTitle = ctx.pageName;
} else {
const response = ctx.stabilizeApi.getResponse().query;
// 'stabilize' as a verb not necessarily well understood
if (!fnProcessChecks('stabilize', ctx.onStabilizeFailure, response)) {
return; // abort
}
token = response.tokens.csrftoken;
const page = response.pages[0];
pageTitle = page.title;
// Doesn't support watchlist expiry [[phab:T263336]]
// ctx.watched = page.watchlistexpiry || page.watched;
}
const query = {
action: 'stabilize',
title: pageTitle,
token: token,
protectlevel: ctx.flaggedRevs.level,
expiry: ctx.flaggedRevs.expiry,
// tags: ctx.changeTags, // flaggedrevs tag support: [[phab:T247721]]
reason: ctx.editSummary,
watchlist: ctx.watchlistOption,
format: 'json'
};
/* Doesn't support watchlist expiry [[phab:T263336]]
if (fnApplyWatchlistExpiry()) {
query.watchlistexpiry = ctx.watchlistExpiry;
}
*/
ctx.stabilizeProcessApi = new Morebits.wiki.Api('configuring stabilization settings...', query, ctx.onStabilizeSuccess, ctx.statusElement, ctx.onStabilizeFailure);
ctx.stabilizeProcessApi.setParent(this);
ctx.stabilizeProcessApi.post();
};
var sleep = function(milliseconds) {
const deferred = $.Deferred();
setTimeout(deferred.resolve, milliseconds);
return deferred;
};
}; // end Morebits.wiki.Page
/* Morebits.wiki.Page TODO: (XXX)
* - Should we retry loads also?
* - Need to reset current action before the save?
* - Deal with action.completed stuff
* - Need to reset all parameters once done (e.g. edit summary, move destination, etc.)
*/
/* **************** Morebits.wiki.Preview **************** */
/**
* Use the API to parse a fragment of wikitext and render it as HTML.
*
* The suggested implementation pattern (in {@link Morebits.SimpleWindow} and
* {@link Morebits.QuickForm} situations) is to construct a
* `Morebits.wiki.Preview` object after rendering a `Morebits.QuickForm`, and
* bind the object to an arbitrary property of the form (e.g. |previewer|).
* For an example, see twinklewarn.js.
*
* @memberof Morebits.wiki
* @class
* @param {HTMLElement} previewbox - The element that will contain the rendered HTML,
* usually a <div> element.
*/
Morebits.wiki.Preview = function(previewbox) {
this.previewbox = previewbox;
$(previewbox).addClass('morebits-previewbox').hide();
/**
* Displays the preview box, and begins an asynchronous attempt
* to render the specified wikitext.
*
* @param {string} wikitext - Wikitext to render; most things should work, including `subst:` and `~~~~`.
* @param {string} [pageTitle] - Optional parameter for the page this should be rendered as being on, if omitted it is taken as the current page.
* @param {string} [sectionTitle] - If provided, render the text as a new section using this as the title.
* @return {jQuery.promise}
*/
this.beginRender = function(wikitext, pageTitle, sectionTitle) {
$(previewbox).show();
const statusspan = document.createElement('span');
previewbox.appendChild(statusspan);
Morebits.Status.init(statusspan);
const query = {
action: 'parse',
prop: ['text', 'modules'],
pst: true, // PST = pre-save transform; this makes substitution work properly
preview: true,
text: wikitext,
title: pageTitle || mw.config.get('wgPageName'),
disablelimitreport: true,
disableeditsection: true,
format: 'json'
};
if (sectionTitle) {
query.section = 'new';
query.sectiontitle = sectionTitle;
}
const renderApi = new Morebits.wiki.Api('loading...', query, fnRenderSuccess, new Morebits.Status('Preview'));
return renderApi.post();
};
var fnRenderSuccess = function(apiobj) {
const response = apiobj.getResponse();
const html = response.parse.text;
if (!html) {
apiobj.statelem.error('failed to retrieve preview, or template was blanked');
return;
}
previewbox.innerHTML = html;
mw.loader.load(response.parse.modulestyles);
mw.loader.load(response.parse.modules);
// this makes links open in new tab
$(previewbox).find('a').attr('target', '_blank');
// Integrate with scripts that do things on rendered content, like navpopups
mw.hook('wikipage.content').fire($(previewbox));
};
/** Hides the preview box and clears it. */
this.closePreview = function() {
$(previewbox).empty().hide();
};
};
/* **************** Morebits.wikitext **************** */
/**
* Wikitext manipulation.
*
* @namespace Morebits.wikitext
* @memberof Morebits
*/
Morebits.wikitext = {};
/**
* Get the value of every parameter found in the wikitext of a given template.
*
* @memberof Morebits.wikitext
* @param {string} text - Wikitext containing a template.
* @param {number} [start=0] - Index noting where in the text the template begins.
* @return {Object} `{name: templateName, parameters: {key: value}}`.
*/
Morebits.wikitext.parseTemplate = function(text, start) {
start = start || 0;
const level = []; // Track of how deep we are ({{, {{{, or [[)
let count = -1; // Number of parameters found
let unnamed = 0; // Keep track of what number an unnamed parameter should receive
let equals = -1; // After finding "=" before a parameter, the index; otherwise, -1
let current = '';
const result = {
name: '',
parameters: {}
};
let key, value;
/**
* Function to handle finding parameter values.
*
* @param {boolean} [final=false] - Whether this is the final
* parameter and we need to remove the trailing `}}`.
*/
function findParam(final) {
// Nothing found yet, this must be the template name
if (count === -1) {
result.name = current.slice(2).trim();
++count;
} else {
// In a parameter
if (equals !== -1) {
// We found an equals, so save the parameter as key: value
key = current.substring(0, equals).trim();
value = final ? current.substring(equals + 1, current.length - 2).trim() : current.substring(equals + 1).trim();
result.parameters[key] = value;
equals = -1;
} else {
// No equals, so it must be unnamed; no trim since whitespace allowed
const param = final ? current.substring(equals + 1, current.length - 2) : current;
if (param) {
result.parameters[++unnamed] = param;
++count;
}
}
}
}
for (let i = start; i < text.length; ++i) {
const test3 = text.substr(i, 3);
if (test3 === '{{{' || (test3 === '}}}' && level[level.length - 1] === 3)) {
current += test3;
i += 2;
if (test3 === '{{{') {
level.push(3);
} else {
level.pop();
}
continue;
}
const test2 = text.substr(i, 2);
// Entering a template (or link)
if (test2 === '{{' || test2 === '[[') {
current += test2;
++i;
if (test2 === '{{') {
level.push(2);
} else {
level.push('wl');
}
continue;
}
// Either leaving a link or template/parser function
if ((test2 === '}}' && level[level.length - 1] === 2) ||
(test2 === ']]' && level[level.length - 1] === 'wl')) {
current += test2;
++i;
level.pop();
// Find the final parameter if this really is the end
if (test2 === '}}' && level.length === 0) {
findParam(true);
break;
}
continue;
}
if (text.charAt(i) === '|' && level.length === 1) {
// Another pipe found, toplevel, so parameter coming up!
findParam();
current = '';
} else if (equals === -1 && text.charAt(i) === '=' && level.length === 1) {
// Equals found, toplevel
equals = current.length;
current += text.charAt(i);
} else {
// Just advance the position
current += text.charAt(i);
}
}
return result;
};
/**
* Adjust and manipulate the wikitext of a page.
*
* @class
* @memberof Morebits.wikitext
* @param {string} text - Wikitext to be manipulated.
*/
Morebits.wikitext.Page = function mediawikiPage(text) {
this.text = text;
};
Morebits.wikitext.Page.prototype = {
text: '',
/**
* Removes links to `link_target` from the page text.
*
* @param {string} link_target
* @return {Morebits.wikitext.Page}
*/
removeLink: function(link_target) {
const mwTitle = mw.Title.newFromText(link_target);
const namespaceID = mwTitle.getNamespaceId();
const title = mwTitle.getMainText();
let link_regex_string = '';
if (namespaceID !== 0) {
link_regex_string = Morebits.namespaceRegex(namespaceID) + ':';
}
link_regex_string += Morebits.pageNameRegex(title);
// For most namespaces, unlink both [[User:Test]] and [[:User:Test]]
// For files and categories, only unlink [[:Category:Test]]. Do not unlink [[Category:Test]]
const isFileOrCategory = [6, 14].includes(namespaceID);
const colon = isFileOrCategory ? ':' : ':?';
const simple_link_regex = new RegExp('\\[\\[' + colon + '(' + link_regex_string + ')\\]\\]', 'g');
const piped_link_regex = new RegExp('\\[\\[' + colon + link_regex_string + '\\|(.+?)\\]\\]', 'g');
this.text = this.text.replace(simple_link_regex, '$1').replace(piped_link_regex, '$1');
return this;
},
/**
* Comments out images from page text; if used in a gallery, deletes the whole line.
* If used as a template argument (not necessarily with `File:` prefix), the template parameter is commented out.
*
* @param {string} image - Image name without `File:` prefix.
* @param {string} [reason] - Reason to be included in comment, alongside the commented-out image.
* @return {Morebits.wikitext.Page}
*/
commentOutImage: function(image, reason) {
const unbinder = new Morebits.Unbinder(this.text);
unbinder.unbind('<!--', '-->');
reason = reason ? reason + ': ' : '';
const image_re_string = Morebits.pageNameRegex(image);
// Check for normal image links, i.e. [[File:Foobar.png|...]]
// Will eat the whole link
const links_re = new RegExp('\\[\\[' + Morebits.namespaceRegex(6) + ':\\s*' + image_re_string + '\\s*[\\|(?:\\]\\])]');
const allLinks = Morebits.string.splitWeightedByKeys(unbinder.content, '[[', ']]');
for (let i = 0; i < allLinks.length; ++i) {
if (links_re.test(allLinks[i])) {
const replacement = '<!-- ' + reason + allLinks[i] + ' -->';
unbinder.content = unbinder.content.replace(allLinks[i], replacement);
// unbind the newly created comments
unbinder.unbind('<!--', '-->');
}
}
// Check for gallery images, i.e. instances that must start on a new line,
// eventually preceded with some space, and must include File: prefix
// Will eat the whole line.
const gallery_image_re = new RegExp('(^\\s*' + Morebits.namespaceRegex(6) + ':\\s*' + image_re_string + '\\s*(?:\\|.*?$|$))', 'mg');
unbinder.content = unbinder.content.replace(gallery_image_re, '<!-- ' + reason + '$1 -->');
// unbind the newly created comments
unbinder.unbind('<!--', '-->');
// Check free image usages, for example as template arguments, might have the File: prefix excluded, but must be preceded by an |
// Will only eat the image name and the preceding bar and an eventual named parameter
const free_image_re = new RegExp('(\\|\\s*(?:[\\w\\s]+\\=)?\\s*(?:' + Morebits.namespaceRegex(6) + ':\\s*)?' + image_re_string + ')', 'mg');
unbinder.content = unbinder.content.replace(free_image_re, '<!-- ' + reason + '$1 -->');
// Rebind the content now, we are done!
this.text = unbinder.rebind();
return this;
},
removeImage: function(image) {
var unbinder = new Morebits.Unbinder(this.text);
unbinder.unbind('<!--', '-->');
var image_re_string = Morebits.pageNameRegex(image);
// Check for normal image links, i.e. [[File:Foobar.png|...]]
// Will eat the whole link
var links_re = new RegExp('\\[\\[' + Morebits.namespaceRegex(6) + ':\\s*' + image_re_string + '\\s*[\\|(?:\\]\\])]');
var allLinks = Morebits.string.splitWeightedByKeys(unbinder.content, '[[', ']]');
for (var i = 0; i < allLinks.length; ++i) {
if (links_re.test(allLinks[i])) {
var replacingtext = Morebits.string.escapeRegExp(allLinks[i]);
var replacing = new RegExp('(\n|)' + replacingtext + '$');
unbinder.content = unbinder.content.replace(replacing, '').replace(allLinks[i], '');
}
}
// unbind the newly created comments
unbinder.unbind('<!--', '-->');
// Check for gallery images, i.e. instances that must start on a new line,
// eventually preceded with some space, and must include File: prefix
// Will eat the whole line.
var gallery_image_re = new RegExp('\\n(^\\s*' + Morebits.namespaceRegex(6) + ':\\s*' + image_re_string + '\\s*(?:\\|.*?$|$))', 'mg');
unbinder.content = unbinder.content.replace(gallery_image_re, '');
// unbind the newly created comments
unbinder.unbind('<!--', '-->');
// Check free image usages, for example as template arguments, might have the File: prefix excluded, but must be preceded by an |
// Will only eat the image name and the preceding bar and an eventual named parameter
var free_image_re = new RegExp('(\\|\\s*(?:[\\w\\s]+\\=)?\\s*(?:' + Morebits.namespaceRegex(6) + ':\\s*)?' + image_re_string + ')', 'mg');
unbinder.content = unbinder.content.replace(free_image_re, '');
// Rebind the content now, we are done!
this.text = unbinder.rebind();
return this;
},
/**
* Converts uses of [[File:`image`]] to [[File:`image`|`data`]].
*
* @param {string} image - Image name without File: prefix.
* @param {string} data - The display options.
* @return {Morebits.wikitext.Page}
*/
addToImageComment: function(image, data) {
const image_re_string = Morebits.pageNameRegex(image);
const links_re = new RegExp('\\[\\[' + Morebits.namespaceRegex(6) + ':\\s*' + image_re_string + '\\s*[\\|(?:\\]\\])]');
const allLinks = Morebits.string.splitWeightedByKeys(this.text, '[[', ']]');
for (let i = 0; i < allLinks.length; ++i) {
if (links_re.test(allLinks[i])) {
let replacement = allLinks[i];
// just put it at the end?
replacement = replacement.replace(/\]\]$/, '|' + data + ']]');
this.text = this.text.replace(allLinks[i], replacement);
}
}
const gallery_re = new RegExp('^(\\s*' + image_re_string + '.*?)\\|?(.*?)$', 'mg');
const newtext = '$1|$2 ' + data;
this.text = this.text.replace(gallery_re, newtext);
return this;
},
/**
* Remove all transclusions of a template from page text.
*
* @param {string} template - Page name whose transclusions are to be removed,
* include namespace prefix only if not in template namespace.
* @return {Morebits.wikitext.Page}
*/
removeTemplate: function(template) {
const template_re_string = Morebits.pageNameRegex(template);
const links_re = new RegExp('\\{\\{(?:' + Morebits.namespaceRegex(10) + ':)?\\s*' + template_re_string + '\\s*[\\|(?:\\}\\})]');
const allTemplates = Morebits.string.splitWeightedByKeys(this.text, '{{', '}}', ['{{{', '}}}']);
for (let i = 0; i < allTemplates.length; ++i) {
if (links_re.test(allTemplates[i])) {
this.text = this.text.replace(allTemplates[i], '');
}
}
return this;
},
/**
* Smartly insert a tag atop page text but after specified templates,
* such as hatnotes, short description, or deletion and protection templates.
* Notably, does *not* insert a newline after the tag.
*
* @param {string} tag - The tag to be inserted.
* @param {string|string[]} regex - Templates after which to insert tag,
* given as either as a (regex-valid) string or an array to be joined by pipes.
* @param {string} [flags=i] - Regex flags to apply. `''` to provide no flags;
* other falsey values will default to `i`.
* @param {string|string[]} [preRegex] - Optional regex string or array to match
* before any template matches (i.e. before `{{`), such as html comments.
* @return {Morebits.wikitext.Page}
*/
insertAfterTemplates: function(tag, regex, flags, preRegex) {
if (typeof tag === 'undefined') {
throw new Error('No tag provided');
}
// .length is only a property of strings and arrays so we
// shouldn't need to check type
if (typeof regex === 'undefined' || !regex.length) {
throw new Error('No regex provided');
} else if (Array.isArray(regex)) {
regex = regex.join('|');
}
if (typeof flags !== 'string') {
flags = 'i';
}
if (!preRegex || !preRegex.length) {
preRegex = '';
} else if (Array.isArray(preRegex)) {
preRegex = preRegex.join('|');
}
// Regex is extra complicated to allow for templates with
// parameters and to handle whitespace properly
this.text = this.text.replace(
new RegExp(
// leading whitespace
'^\\s*' +
// capture template(s)
'(?:((?:\\s*' +
// Pre-template regex, such as leading html comments
preRegex + '|' +
// begin template format
'\\{\\{\\s*(?:' +
// Template regex
regex +
// end main template name, optionally with a number
// Probably remove the (?:) though
')\\d*\\s*' +
// template parameters
'(\\|(?:\\{\\{[^{}]*\\}\\}|[^{}])*)?' +
// end template format
'\\}\\})+' +
// end capture
'(?:\\s*\\n)?)' +
// trailing whitespace
'\\s*)?',
flags), '$1' + tag
);
return this;
},
/**
* Get the manipulated wikitext.
*
* @return {string}
*/
getText: function() {
return this.text;
}
};
/* *********** Morebits.UserspaceLogger ************ */
/**
* Handles logging actions to a userspace log.
* Used in CSD, PROD, and XFD.
*
* @memberof Morebits
* @class
* @param {string} logPageName - Title of the subpage of the current user's log.
*/
Morebits.UserspaceLogger = function(logPageName) {
if (!logPageName) {
throw new Error('記録ページ名が指定されていません');
}
/**
* The text to prefix the log with upon creation, defaults to empty.
*
* @type {string}
*/
this.initialText = '';
/**
* The header level to use for months, defaults to 3 (`===`).
*
* @type {number}
*/
this.headerLevel = 3;
this.changeTags = '';
/**
* Log the entry.
*
* @param {string} logText - Doesn't include leading `#` or `*`.
* @param {string} summaryText - Edit summary.
* @return {jQuery.Promise}
*/
this.log = function(logText, summaryText) {
const def = $.Deferred();
if (!logText) {
return def.reject();
}
var page = new Morebits.wiki.Page('User:' + mw.config.get('wgUserName') + '/' + logPageName,
'ユーザースペースログに項目を追加する'); // make this '... to ' + logPageName ?
page.load((pageobj) => {
// add blurb if log page doesn't exist or is blank
let text = pageobj.getPageText() || this.initialText;
// create monthly header if it doesn't exist already
const date = new Morebits.Date(pageobj.getLoadTime());
if (!date.monthHeaderRegex().exec(text)) {
text += '\n\n' + date.monthHeader(this.headerLevel);
}
pageobj.setPageText(text + '\n' + logText);
pageobj.setEditSummary(summaryText);
pageobj.setChangeTags(this.changeTags);
pageobj.setCreateOption('recreate');
pageobj.save(def.resolve, def.reject);
});
return def;
};
};
/* **************** Morebits.Status **************** */
/**
* Create and show status messages of varying urgency.
* {@link Morebits.Status.init|Morebits.Status.init()} must be called before
* any status object is created, otherwise those statuses won't be visible.
*
* @memberof Morebits
* @class
* @param {string} text - Text before the the colon `:`.
* @param {string} stat - Text after the colon `:`.
* @param {string} [type=status] - Determine the font color of the status
* line, allowable values are: `status` (blue), `info` (green), `warn` (red),
* or `error` (bold red).
*/
Morebits.Status = function Status(text, stat, type) {
this.textRaw = text;
this.text = Morebits.createHtml(text);
this.type = type || 'status';
this.generate();
if (stat) {
this.update(stat, type);
}
};
/**
* Specify an area for status message elements to be added to.
*
* @memberof Morebits.Status
* @param {HTMLElement} root - Usually a div element.
* @throws {Error} If `root` is not an `HTMLElement`.
*/
Morebits.Status.init = function(root) {
if (!(root instanceof Element)) {
throw new Error('object not an instance of Element');
}
while (root.hasChildNodes()) {
root.removeChild(root.firstChild);
}
Morebits.Status.root = root;
Morebits.Status.errorEvent = null;
};
Morebits.Status.root = null;
/**
* @memberof Morebits.Status
* @param {Function} handler - Function to execute on error.
* @throws {Error} When `handler` is not a function.
*/
Morebits.Status.onError = function(handler) {
if (typeof handler === 'function') {
Morebits.Status.errorEvent = handler;
} else {
throw new Error('Morebits.Status.onError: handler is not a function');
}
};
Morebits.Status.prototype = {
stat: null,
statRaw: null,
text: null,
textRaw: null,
type: 'status',
target: null,
node: null,
linked: false,
/** Add the status element node to the DOM. */
link: function() {
if (!this.linked && Morebits.Status.root) {
Morebits.Status.root.appendChild(this.node);
this.linked = true;
}
},
/** Remove the status element node from the DOM. */
unlink: function() {
if (this.linked) {
Morebits.Status.root.removeChild(this.node);
this.linked = false;
}
},
/**
* Update the status.
*
* @param {string} status - Part of status message after colon.
* @param {string} type - 'status' (blue), 'info' (green), 'warn'
* (red), or 'error' (bold red).
*/
update: function(status, type) {
this.statRaw = status;
this.stat = Morebits.createHtml(status);
if (type) {
this.type = type;
if (type === 'error') {
// hack to force the page not to reload when an error is output - see also Morebits.Status() above
Morebits.wiki.numberOfActionsLeft = 1000;
// call error callback
if (Morebits.Status.errorEvent) {
Morebits.Status.errorEvent();
}
// also log error messages in the browser console
console.error(this.textRaw + ': ' + this.statRaw); // eslint-disable-line no-console
}
}
this.render();
},
/** Produce the html for first part of the status message. */
generate: function() {
this.node = document.createElement('div');
this.node.appendChild(document.createElement('span')).appendChild(this.text);
this.node.appendChild(document.createElement('span')).appendChild(document.createTextNode(': '));
this.target = this.node.appendChild(document.createElement('span'));
this.target.appendChild(document.createTextNode('')); // dummy node
},
/** Complete the html, for the second part of the status message. */
render: function() {
this.node.className = 'morebits_status_' + this.type;
while (this.target.hasChildNodes()) {
this.target.removeChild(this.target.firstChild);
}
this.target.appendChild(this.stat);
this.link();
},
status: function(status) {
this.update(status, 'status');
},
info: function(status) {
this.update(status, 'info');
},
warn: function(status) {
this.update(status, 'warn');
},
error: function(status) {
this.update(status, 'error');
}
};
/**
* @memberof Morebits.Status
* @param {string} text - Before colon
* @param {string} status - After colon
* @return {Morebits.Status} - `status`-type (blue)
*/
Morebits.Status.status = function(text, status) {
return new Morebits.Status(text, status);
};
/**
* @memberof Morebits.Status
* @param {string} text - Before colon
* @param {string} status - After colon
* @return {Morebits.Status} - `info`-type (green)
*/
Morebits.Status.info = function(text, status) {
return new Morebits.Status(text, status, 'info');
};
/**
* @memberof Morebits.Status
* @param {string} text - Before colon
* @param {string} status - After colon
* @return {Morebits.Status} - `warn`-type (red)
*/
Morebits.Status.warn = function(text, status) {
return new Morebits.Status(text, status, 'warn');
};
/**
* @memberof Morebits.Status
* @param {string} text - Before colon
* @param {string} status - After colon
* @return {Morebits.Status} - `error`-type (bold red)
*/
Morebits.Status.error = function(text, status) {
return new Morebits.Status(text, status, 'error');
};
/**
* For the action complete message at the end, create a status line without
* a colon separator.
*
* @memberof Morebits.Status
* @param {string} text
*/
Morebits.Status.actionCompleted = function(text) {
const node = document.createElement('div');
node.appendChild(document.createElement('b')).appendChild(document.createTextNode(text));
node.className = 'morebits_status_info morebits_action_complete';
if (Morebits.Status.root) {
Morebits.Status.root.appendChild(node);
}
};
/**
* Display the user's rationale, comments, etc. Back to them after a failure,
* so that they may re-use it.
*
* @memberof Morebits.Status
* @param {string} comments
* @param {string} message
*/
Morebits.Status.printUserText = function(comments, message) {
const p = document.createElement('p');
p.innerHTML = message;
const div = document.createElement('div');
div.className = 'morebits-usertext';
div.style.marginTop = '0';
div.style.whiteSpace = 'pre-wrap';
div.textContent = comments;
p.appendChild(div);
Morebits.Status.root.appendChild(p);
};
/**
* Simple helper function to create a simple node.
*
* @param {string} type - Type of HTML element.
* @param {string} content - Text content.
* @param {string} [color] - Font color.
* @return {HTMLElement}
*/
Morebits.htmlNode = function(type, content, color) {
const node = document.createElement(type);
if (color) {
node.style.color = color;
}
node.appendChild(document.createTextNode(content));
return node;
};
/**
* Add shift-click support for checkboxes. The wikibits version
* (`window.addCheckboxClickHandlers`) has some restrictions, and doesn't work
* with checkboxes inside a sortable table, so let's build our own.
*
* @param {jQuery} jQuerySelector
* @param {jQuery} jQueryContext
*/
Morebits.checkboxShiftClickSupport = function(jQuerySelector, jQueryContext) {
let lastCheckbox = null;
function clickHandler(event) {
const thisCb = this;
if (event.shiftKey && lastCheckbox !== null) {
const $cbs = $(jQuerySelector, jQueryContext); // can't cache them, obviously, if we want to support re-sorting
let index = -1, lastIndex = -1, i;
for (i = 0; i < $cbs.length; i++) {
if ($cbs[i] === thisCb) {
index = i;
if (lastIndex > -1) {
break;
}
}
if ($cbs[i] === lastCheckbox) {
lastIndex = i;
if (index > -1) {
break;
}
}
}
if (index > -1 && lastIndex > -1) {
// inspired by wikibits
const endState = thisCb.checked;
let start, finish;
if (index < lastIndex) {
start = index + 1;
finish = lastIndex;
} else {
start = lastIndex;
finish = index - 1;
}
for (i = start; i <= finish; i++) {
if ($cbs[i].checked !== endState) {
$cbs[i].click();
}
}
}
}
lastCheckbox = thisCb;
return true;
}
$(jQuerySelector, jQueryContext).on('click', clickHandler);
};
/* **************** Morebits.BatchOperation **************** */
/**
* Iterates over a group of pages (or arbitrary objects) and executes a worker function
* for each.
*
* `setPageList(pageList)`: Sets the list of pages to work on. It should be an
* array of page names strings.
*
* `setOption(optionName, optionValue)`: Sets a known option:
* - `chunkSize` (integer): The size of chunks to break the array into (default
* 50). Setting this to a small value (<5) can cause problems.
* - `preserveIndividualStatusLines` (boolean): Keep each page's status element
* visible when worker is complete? See note below.
*
* `run(worker, postFinish)`: Runs the callback `worker` for each page in the
* list. The callback must call `workerSuccess` when succeeding, or
* `workerFailure` when failing. If using {@link Morebits.wiki.Api} or
* {@link Morebits.wiki.Page}, this is easily done by passing these two
* functions as parameters to the methods on those objects: for instance,
* `page.save(batchOp.workerSuccess, batchOp.workerFailure)`. Make sure the
* methods are called directly if special success/failure cases arise. If you
* omit to call these methods, the batch operation will stall after the first
* chunk! Also ensure that either workerSuccess or workerFailure is called no
* more than once. The second callback `postFinish` is executed when the
* entire batch has been processed.
*
* If using `preserveIndividualStatusLines`, you should try to ensure that the
* `workerSuccess` callback has access to the page title. This is no problem for
* {@link Morebits.wiki.Page} objects. But when using the API, please set the
* |pageName| property on the {@link Morebits.wiki.Api} object.
*
* There are sample batchOperation implementations using Morebits.wiki.Page in
* twinklebatchdelete.js, twinklebatchundelete.js, and twinklebatchprotect.js.
*
* @memberof Morebits
* @class
* @param {string} [currentAction]
*/
Morebits.BatchOperation = function(currentAction) {
const ctx = {
// backing fields for public properties
pageList: null,
options: {
chunkSize: 50,
preserveIndividualStatusLines: false
},
// internal counters, etc.
statusElement: new Morebits.Status(currentAction || 'バッチ処理の実行'),
worker: null, // function that executes for each item in pageList
postFinish: null, // function that executes when the whole batch has been processed
countStarted: 0,
countFinished: 0,
countFinishedSuccess: 0,
currentChunkIndex: -1,
pageChunks: [],
running: false
};
// shouldn't be needed by external users, but provided anyway for maximum flexibility
this.getStatusElement = function() {
return ctx.statusElement;
};
/**
* Sets the list of pages to work on.
*
* @param {Array} pageList - Array of objects over which you wish to execute the worker function
* This is usually the list of page names (strings).
*/
this.setPageList = function(pageList) {
ctx.pageList = pageList;
};
/**
* Sets a known option.
*
* @param {string} optionName - Name of the option:
* - chunkSize (integer): The size of chunks to break the array into
* (default 50). Setting this to a small value (<5) can cause problems.
* - preserveIndividualStatusLines (boolean): Keep each page's status
* element visible when worker is complete?
* @param {number|boolean} optionValue - Value to which the option is
* to be set. Should be an integer for chunkSize and a boolean for
* preserveIndividualStatusLines.
*/
this.setOption = function(optionName, optionValue) {
ctx.options[optionName] = optionValue;
};
/**
* Runs the first callback for each page in the list.
* The callback must call workerSuccess when succeeding, or workerFailure when failing.
* Runs the optional second callback when the whole batch has been processed.
*
* @param {Function} worker
* @param {Function} [postFinish]
*/
this.run = function(worker, postFinish) {
if (ctx.running) {
ctx.statusElement.error('Batch operation is already running');
return;
}
ctx.running = true;
ctx.worker = worker;
ctx.postFinish = postFinish;
ctx.countStarted = 0;
ctx.countFinished = 0;
ctx.countFinishedSuccess = 0;
ctx.currentChunkIndex = -1;
ctx.pageChunks = [];
const total = ctx.pageList.length;
if (!total) {
ctx.statusElement.info('ページが指定されていません');
ctx.running = false;
if (ctx.postFinish) {
ctx.postFinish();
}
return;
}
// chunk page list into more manageable units
ctx.pageChunks = Morebits.array.chunk(ctx.pageList, ctx.options.chunkSize);
// start the process
Morebits.wiki.addCheckpoint();
ctx.statusElement.status('0%');
fnStartNewChunk();
};
/**
* To be called by worker before it terminates successfully.
*
* @param {(Morebits.wiki.Page|Morebits.wiki.Api|string)} arg -
* This should be the `Morebits.wiki.Page` or `Morebits.wiki.Api` object used by worker
* (for the adjustment of status lines emitted by them).
* If no Morebits.wiki.* object is used (e.g. you're using `mw.Api()` or something else), and
* `preserveIndividualStatusLines` option is on, give the page name (string) as argument.
*/
this.workerSuccess = function(arg) {
if (arg instanceof Morebits.wiki.Api || arg instanceof Morebits.wiki.Page) {
// update or remove status line
const statelem = arg.getStatusElement();
if (ctx.options.preserveIndividualStatusLines) {
if (arg.getPageName || arg.pageName || (arg.query && arg.query.title)) {
// we know the page title - display a relevant message
var pageName = arg.getPageName ? arg.getPageName() : arg.pageName || arg.query.title;
statelem.info('完了 ([[' + pageName + ']])');
} else {
// we don't know the page title - just display a generic message
statelem.info('完了');
}
} else {
// remove the status line automatically produced by Morebits.wiki.*
statelem.unlink();
}
} else if (typeof arg === 'string' && ctx.options.preserveIndividualStatusLines) {
new Morebits.Status(arg, '完了 ([[' + arg + ']])');
}
ctx.countFinishedSuccess++;
fnDoneOne();
};
this.workerFailure = function() {
fnDoneOne();
};
// private functions
const thisProxy = this;
var fnStartNewChunk = function() {
const chunk = ctx.pageChunks[++ctx.currentChunkIndex];
if (!chunk) {
return; // done! yay
}
// start workers for the current chunk
ctx.countStarted += chunk.length;
chunk.forEach((page) => {
ctx.worker(page, thisProxy);
});
};
var fnDoneOne = function() {
ctx.countFinished++;
// update overall status line
const total = ctx.pageList.length;
if (ctx.countFinished < total) {
const progress = Math.round(100 * ctx.countFinished / total);
ctx.statusElement.status(progress + '%');
// start a new chunk if we're close enough to the end of the previous chunk, and
// we haven't already started the next one
if (ctx.countFinished >= (ctx.countStarted - Math.max(ctx.options.chunkSize / 10, 2)) &&
Math.floor(ctx.countFinished / ctx.options.chunkSize) > ctx.currentChunkIndex) {
fnStartNewChunk();
}
} else if (ctx.countFinished === total) {
var statusString = '完了 (' + ctx.countFinishedSuccess +
'/' + ctx.countFinished + ' アクションが正常に完了)';
if (ctx.countFinishedSuccess < ctx.countFinished) {
ctx.statusElement.warn(statusString);
} else {
ctx.statusElement.info(statusString);
}
if (ctx.postFinish) {
ctx.postFinish();
}
Morebits.wiki.removeCheckpoint();
ctx.running = false;
} else {
// ctx.countFinished > total
// just for giggles! (well, serious debugging, actually)
ctx.statusElement.warn('完了 (' + (ctx.countFinished - total) + '件上回った)');
Morebits.wiki.removeCheckpoint();
ctx.running = false;
}
};
};
/**
* Given a set of asynchronous functions to run along with their dependencies,
* run them in an efficient sequence so that multiple functions
* that don't depend on each other are triggered simultaneously. Where
* dependencies exist, it ensures that the dependency functions finish running
* before the dependent function runs. The values resolved by the dependencies
* are made available to the dependant as arguments.
*
* @memberof Morebits
* @class
*/
Morebits.TaskManager = function(context) {
this.taskDependencyMap = new Map();
this.failureCallbackMap = new Map();
this.deferreds = new Map();
this.context = context || window;
/**
* Register a task along with its dependencies (tasks which should have finished
* execution before we can begin this one). Each task is a function that must return
* a promise. The function will get the values resolved by the dependency functions
* as arguments.
*
* @param {Function} func - A task.
* @param {Function[]} deps - Its dependencies.
* @param {Function} [onFailure] - a failure callback that's run if the task or any one
* of its dependencies fail.
*/
this.add = function(func, deps, onFailure) {
this.taskDependencyMap.set(func, deps);
this.failureCallbackMap.set(func, onFailure || (() => {}));
const deferred = $.Deferred();
this.deferreds.set(func, deferred);
};
/**
* Run all the tasks. Multiple tasks may be run at once.
*
* @return {jQuery.Promise} - Resolved if all tasks succeed, rejected otherwise.
*/
this.execute = function() {
const self = this; // proxy for `this` for use inside functions where `this` is something else
this.taskDependencyMap.forEach((deps, task) => {
const dependencyPromisesArray = deps.map((dep) => self.deferreds.get(dep));
$.when.apply(self.context, dependencyPromisesArray).then(function() {
const result = task.apply(self.context, arguments);
if (result === undefined) { // maybe the function threw, or it didn't return anything
mw.log.error('Morebits.TaskManager: task returned undefined');
self.deferreds.get(task).reject.apply(self.context, arguments);
self.failureCallbackMap.get(task).apply(self.context, []);
}
result.then(function() {
self.deferreds.get(task).resolve.apply(self.context, arguments);
}, function() { // task failed
self.deferreds.get(task).reject.apply(self.context, arguments);
self.failureCallbackMap.get(task).apply(self.context, arguments);
});
}, function() { // one or more of the dependencies failed
self.failureCallbackMap.get(task).apply(self.context, arguments);
});
});
return $.when.apply(null, [...this.deferreds.values()]); // resolved when everything is done!
};
};
/**
* A simple draggable window. No longer uses jQuery UI.
*
* @memberof Morebits
* @class
* @param {number} width
* @param {number} height - The maximum allowable height for the content area.
*/
Morebits.SimpleWindow = function SimpleWindow(width, height) {
const $dialog = $('<div>').addClass('morebits-dialog').attr('role', 'dialog').attr('tabindex', -1);
const $titleBar = $('<div>').addClass('morebits-dialog-titlebar').append(
$('<span>').addClass('morebits-dialog-title'),
$('<button>')
.addClass('morebits-dialog-close')
.text('\u00D7')
.on('click', () => this.close())
);
const $content = $('<div>')
.addClass('morebits-dialog-content')
.attr('id', 'morebits-dialog-content-' + Math.round(Math.random() * 1e15));
const $buttonPane = $('<div>').addClass('morebits-dialog-buttonpane').append(
$('<span>').addClass('morebits-dialog-buttons'),
$('<span>').addClass('morebits-dialog-footerlinks')
);
const $resizer = $('<div>').addClass('morebits-dialog-resizer');
$dialog.append($titleBar, $content, $buttonPane, $resizer)
.css('width', Math.min(parseInt(width || 800, 10), $(window).width()))
.css('height', 'auto')
.css('max-height', height + 20)
.css('top', Math.max(0, window.scrollY + $(window).height() / 2 - (height + 20) / 2)) // Centre it (assume max height)
.css('left', Math.max(0, window.scrollX + $(window).width() / 2 - $dialog.width() / 2)); // Centre it
$dialog.on('focus', () => this.focus()).on('keydown', (e) => {
if (e.key === 'Escape') {
this.close();
}
});
// Make dialog draggable and resizable
let isDragging = false, isResizing = false, initialX, initialY, initialWidth, initialHeight;
$titleBar.on('mousedown', (e) => {
isDragging = true;
initialX = e.clientX - $dialog.offset().left;
initialY = e.clientY - $dialog.offset().top;
});
$resizer.on('mousedown', (e) => {
isResizing = true;
$dialog.css('height', $dialog.height());
$dialog.css('max-height', 'none');
initialWidth = $dialog.width();
initialHeight = $dialog.height();
initialX = e.clientX;
initialY = e.clientY;
});
$(document).on('mousemove', (e) => {
if (isDragging) {
$dialog.css('left', Math.max(0, e.clientX - initialX))
.css('top', Math.max(0, e.clientY - initialY));
} else if (isResizing) {
$dialog.css('width', initialWidth + (e.clientX - initialX))
.css('height', initialHeight + (e.clientY - initialY));
}
}).on('mouseup', () => {
isDragging = false;
isResizing = false;
});
this.$dialog = $dialog;
this.content = $content[0];
this.height = height;
};
Morebits.SimpleWindow.prototype = {
buttons: [],
height: 600,
hasFooterLinks: false,
scriptName: null,
/**
* Bring a dialog to the top, when there are other overlapping dialogs open.
*
* @return {Morebits.SimpleWindow}
*/
focus: function() {
if (!this.$dialog.hasClass('morebits-dialog-modal')) {
$('.morebits-dialog:not(.morebits-dialog-modal)').get()
.filter((dialog) => dialog !== this.$dialog[0])
.concat(this.$dialog[0])
.forEach((dialog, idx) => {
dialog.style.zIndex = 501 + idx;
});
}
return this;
},
/**
* Closes the dialog. If this is set as an event handler, it will stop the event
* from doing anything more.
*
* @param {event} [event]
* @return {Morebits.SimpleWindow}
*/
close: function(event) {
if (event) {
event.preventDefault();
}
this.setModality(false);
this.$dialog.remove();
return this;
},
/**
* Shows the dialog. Calling display() on a dialog that has previously been closed
* might work, but it is not guaranteed.
*
* @return {Morebits.SimpleWindow}
*/
display: function() {
if (this.scriptName) {
const $titleBar = this.$dialog.find('.morebits-dialog-title');
$titleBar.find('.morebits-dialog-scriptname').remove();
$titleBar.prepend($('<span>')
.addClass('morebits-dialog-scriptname')
.text(this.scriptName + ' \u00B7 ') // U+00B7 MIDDLE DOT = ·
);
}
if (this.$dialog.find('.morebits-scrollbox').length) {
// quickform needs some extra CSS if it happens to contain a scrollbox.
// CSS :has() selector can be used in the future which is easier and more reliable.
this.$dialog.find('.quickform').addClass('has-scrollbox');
}
this.$dialog.appendTo(document.body);
// Put focus on the first form element in the dialog, or on dialog itself.
const $firstField = this.$dialog.find('input, select, textarea').first();
const $firstFieldOrDialog = $firstField.length ? $firstField : this.$dialog;
$firstFieldOrDialog.trigger('focus');
return this;
},
/**
* Sets the dialog title.
*
* @param {string} title
* @return {Morebits.SimpleWindow}
*/
setTitle: function(title) {
this.$dialog.find('.morebits-dialog-title').text(title);
return this;
},
/**
* Sets the script name, appearing as a prefix to the title to help users determine which
* user script is producing which dialog. For instance, Twinkle modules set this to "Twinkle".
*
* @param {string} name
* @return {Morebits.SimpleWindow}
*/
setScriptName: function(name) {
this.scriptName = name;
return this;
},
/**
* Sets the dialog width.
*
* @param {number} width
* @return {Morebits.SimpleWindow}
*/
setWidth: function(width) {
this.$dialog.css('width', width + 'px');
return this;
},
/**
* Sets the dialog's maximum height. The dialog will auto-size to fit its contents.
*
* @param {number} height
* @return {Morebits.SimpleWindow}
*/
setHeight: function(height) {
this.$dialog.css('height', 'auto');
this.$dialog.css('max-height', (height + 20) + 'px');
return this;
},
/**
* Sets the content of the dialog to the given element node, usually from rendering
* a {@link Morebits.QuickForm}.
* Re-enumerates the footer buttons, but leaves the footer links as they are.
* Be sure to call this at least once before the dialog is displayed...
*
* @param {HTMLElement} content
* @return {Morebits.SimpleWindow}
*/
setContent: function(content) {
this.purgeContent();
this.addContent(content);
return this;
},
/**
* Adds the given element node to the dialog content.
*
* @param {HTMLElement} content
* @return {Morebits.SimpleWindow}
*/
addContent: function(content) {
this.content.appendChild(content);
// look for submit buttons in the content, hide them, and add a proxy button to the button pane
$(this.content).find('input[type="submit"], button[type="submit"]').get().forEach((node) => {
node.style.display = 'none';
const button = document.createElement('button');
button.textContent = node.getAttribute('value') || node.textContent || '送信';
button.className = node.className || 'submitButtonProxy';
button.addEventListener('click', () => node.click(), false);
this.buttons.push(button);
});
// remove all buttons from the button pane and re-add them
this.$dialog.find('.morebits-dialog-buttons')
.empty()
.append(...this.buttons)
// Set data-empty attribute only if there are no buttons. Used by CSS.
.attr('data-empty', this.buttons.length ? null : 'data-empty');
return this;
},
/**
* Removes all contents from the dialog, barring any footer links.
*
* @return {Morebits.SimpleWindow}
*/
purgeContent: function() {
this.buttons = [];
// delete all buttons in the buttonpane
this.$dialog.find('.morebits-dialog-buttons').empty();
$(this.content).empty();
return this;
},
/**
* Adds a link in the bottom-right corner of the dialog.
* This can be used to provide help or policy links.
* For example, Twinkle's CSD module adds a link to the CSD policy page,
* as well as a link to Twinkle's documentation.
*
* @param {string} text - Display text.
* @param {string} wikiPage - Link target.
* @return {Morebits.SimpleWindow}
*/
addFooterLink: function(text, wikiPage) {
const $footerlinks = this.$dialog.find('.morebits-dialog-footerlinks');
if (this.hasFooterLinks) {
const bullet = document.createElement('span');
bullet.textContent = ' \u2022 '; // U+2022 BULLET
$footerlinks.append(bullet);
}
const link = document.createElement('a');
link.setAttribute('href', mw.util.getUrl(wikiPage));
link.setAttribute('title', wikiPage);
link.setAttribute('target', '_blank');
link.textContent = text;
$footerlinks.append(link);
this.hasFooterLinks = true;
return this;
},
/**
* Sets whether the window should be modal or not. Modal dialogs create
* an overlay below the dialog but above other page elements. This
* must be used (if necessary) before calling display().
*
* @param {boolean} [modal=false] - If set to true, other items on the
* page will be disabled, i.e., cannot be interacted with.
* @return {Morebits.SimpleWindow}
*/
setModality: function(modal) {
if (modal) {
$('<div>').addClass('morebits-dialog-overlay').appendTo(document.body);
this.$dialog.addClass('morebits-dialog-modal');
} else {
$('.morebits-dialog-overlay').remove();
this.$dialog.removeClass('morebits-dialog-modal');
}
return this;
}
};
/**
* Enables or disables all footer buttons on all {@link Morebits.SimpleWindow}s in the current page.
* This should be called with `false` when the button(s) become irrelevant (e.g. just before
* {@link Morebits.Status.init} is called).
* This is not an instance method so that consumers don't have to keep a reference to the
* original `Morebits.SimpleWindow` object sitting around somewhere. Anyway, most of the time
* there will only be one `Morebits.SimpleWindow` open, so this shouldn't matter.
*
* @memberof Morebits.SimpleWindow
* @param {boolean} enabled
*/
Morebits.SimpleWindow.setButtonsEnabled = function(enabled) {
$('.morebits-dialog-buttons button').prop('disabled', !enabled);
};
// Deprecated class name aliases in camelCase. Please use the PascalCase names instead.
Morebits.batchOperation = Morebits.BatchOperation;
Morebits.date = Morebits.Date;
Morebits.quickForm = Morebits.QuickForm;
Morebits.quickForm.element = Morebits.QuickForm.Element;
Morebits.simpleWindow = Morebits.SimpleWindow;
Morebits.status = Morebits.Status;
Morebits.taskManager = Morebits.TaskManager;
Morebits.unbinder = Morebits.Unbinder;
Morebits.userspaceLogger = Morebits.UserspaceLogger;
Morebits.wiki.api = Morebits.wiki.Api;
Morebits.wiki.page = Morebits.wiki.Page;
Morebits.wiki.preview = Morebits.wiki.Preview;
Morebits.wikitext.page = Morebits.wikitext.Page;
}());
/**
* If this script is being executed outside a ResourceLoader context, we add some
* global assignments for legacy scripts, hopefully these can be removed down the line.
*
* IMPORTANT NOTE:
* PLEASE DO NOT USE THESE ALIASES IN NEW CODE!
* Thanks.
*/
if (typeof arguments === 'undefined') { // typeof is here for a reason...
/* global Morebits */
window.SimpleWindow = Morebits.SimpleWindow;
window.QuickForm = Morebits.QuickForm;
window.Wikipedia = Morebits.wiki;
window.Status = Morebits.Status;
}
// </nowiki>
i6yqdhqgllvgawty6tiji47tfe5vnnr
769561
769553
2026-10-22T02:16:35Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at d9cd3461baca43ac99ac2c24fb76a3aae6ddada9: minify
769561
javascript
text/javascript
// <nowiki>
/**
* A library full of lots of goodness for user scripts on MediaWiki wikis, including Wikipedia.
*
* The highlights include:
* - {@link Morebits.wiki.Api} - make calls to the MediaWiki API
* - {@link Morebits.wiki.Page} - modify pages on the wiki (edit, revert, delete, etc.)
* - {@link Morebits.Date} - enhanced date object processing, sort of a light moment.js
* - {@link Morebits.QuickForm} - generate quick HTML forms on the fly
* - {@link Morebits.SimpleWindow} - generate dialog windows and modals
* - {@link Morebits.Status} - a rough-and-ready status message displayer, used by the Morebits.wiki classes
* - {@link Morebits.wikitext} - utilities for dealing with wikitext
* - {@link Morebits.string} - utilities for manipulating strings
* - {@link Morebits.array} - utilities for manipulating arrays
* - {@link Morebits.ip} - utilities to help process IP addresses
*
* Dependencies:
* - The whole thing relies on jQuery. But most wikis should provide this by default.
* - {@link Morebits.QuickForm}, {@link Morebits.SimpleWindow}, and {@link Morebits.Status} rely on the "morebits.css" file for their styling.
* - To create a gadget based on morebits.js, use this syntax in MediaWiki:Gadgets-definition:
* - `*GadgetName[ResourceLoader|dependencies=mediawiki.user,mediawiki.util,mediawiki.Title]|morebits.js|morebits.css|GadgetName.js`
* - Alternatively, you can configure morebits.js as a hidden gadget in MediaWiki:Gadgets-definition:
* - `*morebits[ResourceLoader|dependencies=mediawiki.user,mediawiki.util,mediawiki.Title|hidden]|morebits.js|morebits.css`
* and then load ext.gadget.morebits as one of the dependencies for the new gadget.
*
* All the stuff here works on all browsers for which MediaWiki provides JavaScript support.
*
* This library is maintained by the maintainers of Twinkle.
* For queries, suggestions, help, etc., head to [Wikipedia talk:Twinkle on English Wikipedia](http://en.wikipedia.org/wiki/WT:TW).
* The latest development source is available at {@link https://github.com/wikimedia-gadgets/twinkle/blob/master/src/morebits.js|GitHub}.
*
* @namespace Morebits
*/
(function(){const Morebits={};window.Morebits=Morebits;Morebits.l10n={redirectTagAliases:["#REDIRECT"],signatureTimestampFormat:function(str){const japanese=/(\d{4})年(\d{1,2})月(\d{1,2})日\s*\([日月火水木金土]\)\s*(\d{1,2}):(\d{2})\s*\(UTC\)/.exec(str);if(japanese){return[japanese[1],japanese[2]-1,japanese[3],japanese[4],japanese[5]]}const rgx=/(\d{2}):(\d{2}), (\d{1,2}) (\w+) (\d{4}) \(UTC\)/;const match=rgx.exec(str);if(!match){return null}const month=["January","February","March","April","May","June","July","August","September","October","November","December"].indexOf(match[4]);if(month===-1){return null}return[match[5],month,match[3],match[1],match[2]]}};Morebits.userIsInGroup=function(group){return mw.config.get("wgUserGroups").includes(group)};Morebits.userIsSysop=Morebits.userIsInGroup("sysop");Morebits.isPageRedirect=function(){return!!(mw.config.get("wgIsRedirect")||document.getElementById("softredirect")||$(".box-RfD").length)};Morebits.pageNameNorm=mw.config.get("wgPageName").replace(/_/g," ");Morebits.pageNameRegex=function(pageName){if(pageName===""){return""}const firstChar=pageName[0],remainder=Morebits.string.escapeRegExp(pageName.slice(1));if(mw.Title.phpCharToUpper(firstChar)!==firstChar.toLowerCase()){return"["+mw.Title.phpCharToUpper(firstChar)+firstChar.toLowerCase()+"]"+remainder}return Morebits.string.escapeRegExp(firstChar)+remainder};Morebits.createHtml=function(input){const fragment=document.createDocumentFragment();if(!input){return fragment}if(!Array.isArray(input)){input=[input]}for(let i=0;i<input.length;++i){if(input[i]instanceof Node){fragment.appendChild(input[i])}else{$.parseHTML(Morebits.createHtml.renderWikilinks(input[i])).forEach(node=>{fragment.appendChild(node)})}}return fragment};Morebits.createHtml.renderWikilinks=function(text){const ub=new Morebits.Unbinder(text);ub.unbind("<code>","</code>");ub.content=ub.content.replace(/\[\[:?(?:([^|\]]+?)\|)?([^\]|]+?)\]\]/g,(_,target,text)=>{if(!target){target=text}return'<a target="_blank" href="'+mw.util.getUrl(target)+'" title="'+target.replace(/"/g,""")+'">'+text+"</a>"});return ub.rebind()};Morebits.namespaceRegex=function(namespaces){if(!Array.isArray(namespaces)){namespaces=[namespaces]}const aliases=[];let regex;$.each(mw.config.get("wgNamespaceIds"),(name,number)=>{if(namespaces.includes(number)){aliases.push(name.split("").map(char=>Morebits.pageNameRegex(char)).join(""))}});switch(aliases.length){case 0:regex="";break;case 1:regex=aliases[0];break;default:regex="(?:"+aliases.join("|")+")";break}return regex};Morebits.QuickForm=function QuickForm(event,eventType){this.root=new Morebits.QuickForm.Element({type:"form",event:event,eventType:eventType})};Morebits.QuickForm.prototype.render=function QuickFormRender(){const ret=this.root.render();ret.names={};return ret};Morebits.QuickForm.prototype.append=function QuickFormAppend(data){return this.root.append(data)};Morebits.QuickForm.Element=function QuickFormElement(data){this.data=data;this.childs=[]};Morebits.QuickForm.Element.id=0;Morebits.QuickForm.Element.prototype.append=function QuickFormElementAppend(data){let child;if(data instanceof Morebits.QuickForm.Element){child=data}else{child=new Morebits.QuickForm.Element(data)}this.childs.push(child);return child};Morebits.QuickForm.Element.prototype.render=function QuickFormElementRender(internal_subgroup_id){const currentNode=this.compute(this.data,internal_subgroup_id);for(let i=0;i<this.childs.length;++i){currentNode[1].appendChild(this.childs[i].render())}return currentNode[0]};Morebits.QuickForm.Element.prototype.compute=function QuickFormElementCompute(data,in_id){let node;let childContainer=null;let label;const id=(in_id?in_id+"_":"")+"node_"+Morebits.QuickForm.Element.id++;if(data.adminonly&&!Morebits.userIsSysop){data.type="hidden"}let i,current,subnode;switch(data.type){case"form":node=document.createElement("form");node.className="quickform";node.setAttribute("action","javascript:void(0);");if(data.event){node.addEventListener(data.eventType||"submit",data.event,false)}break;case"fragment":node=document.createDocumentFragment();return[node,node];case"select":node=document.createElement("div");node.setAttribute("id","div_"+id);if(data.label){label=node.appendChild(document.createElement("label"));label.setAttribute("for",id);label.appendChild(Morebits.createHtml(data.label));label.style.marginRight="3px"}var select=node.appendChild(document.createElement("select"));if(data.event){select.addEventListener("change",data.event,false)}if(data.multiple){select.setAttribute("multiple","multiple")}if(data.size){select.setAttribute("size",data.size)}if(data.disabled){select.setAttribute("disabled","disabled")}select.setAttribute("name",data.name);if(data.list){for(i=0;i<data.list.length;++i){current=data.list[i];if(current.list){current.type="optgroup"}else{current.type="option"}subnode=this.compute(current);select.appendChild(subnode[0])}}childContainer=select;break;case"option":node=document.createElement("option");node.values=data.value;node.setAttribute("value",data.value);if(data.selected){node.setAttribute("selected","selected")}if(data.disabled){node.setAttribute("disabled","disabled")}node.setAttribute("label",data.label);node.appendChild(document.createTextNode(data.label));break;case"optgroup":node=document.createElement("optgroup");node.setAttribute("label",data.label);if(data.list){for(i=0;i<data.list.length;++i){current=data.list[i];current.type="option";subnode=this.compute(current);node.appendChild(subnode[0])}}break;case"field":node=document.createElement("fieldset");label=node.appendChild(document.createElement("legend"));label.appendChild(Morebits.createHtml(data.label));if(data.name){node.setAttribute("name",data.name)}if(data.disabled){node.setAttribute("disabled","disabled")}break;case"checkbox":case"radio":node=document.createElement("div");if(data.list){for(i=0;i<data.list.length;++i){const cur_id=id+"_"+i;current=data.list[i];var cur_div;if(current.type==="header"){cur_div=node.appendChild(document.createElement("h6"));cur_div.appendChild(document.createTextNode(current.label));if(current.tooltip){Morebits.QuickForm.Element.generateTooltip(cur_div,current)}continue}cur_div=node.appendChild(document.createElement("div"));subnode=cur_div.appendChild(document.createElement("input"));subnode.values=current.value;subnode.setAttribute("value",current.value);subnode.setAttribute("type",data.type);subnode.setAttribute("id",cur_id);subnode.setAttribute("name",current.name||data.name);if(current.name){subnode.setAttribute("data-single","data-single")}if(current.checked){subnode.setAttribute("checked","checked")}if(current.disabled){subnode.setAttribute("disabled","disabled")}label=cur_div.appendChild(document.createElement("label"));label.appendChild(Morebits.createHtml(current.label));label.setAttribute("for",cur_id);if(current.tooltip){Morebits.QuickForm.Element.generateTooltip(label,current)}if(current.style){label.setAttribute("style",current.style)}var event;if(current.subgroup){let tmpgroup=current.subgroup;if(!Array.isArray(tmpgroup)){tmpgroup=[tmpgroup]}var subgroupRaw=new Morebits.QuickForm.Element({type:"div",id:id+"_"+i+"_subgroup"});$.each(tmpgroup,(idx,el)=>{const newEl=$.extend({},el);if(!newEl.type){newEl.type=data.type}newEl.name=(current.name||data.name)+"."+newEl.name;subgroupRaw.append(newEl)});const subgroup=subgroupRaw.render(cur_id);subgroup.className="quickformSubgroup";subnode.subgroup=subgroup;subnode.shown=false;event=function(e){if(e.target.checked){e.target.parentNode.appendChild(e.target.subgroup);if(e.target.type==="radio"){const name=e.target.name;if(e.target.form.names[name]!==undefined){e.target.form.names[name].parentNode.removeChild(e.target.form.names[name].subgroup)}e.target.form.names[name]=e.target}}else{e.target.parentNode.removeChild(e.target.subgroup)}};subnode.addEventListener("change",event,true);if(current.checked){subnode.parentNode.appendChild(subgroup)}}else if(data.type==="radio"){event=function(e){if(e.target.checked){const name=e.target.name;if(e.target.form.names[name]!==undefined){e.target.form.names[name].parentNode.removeChild(e.target.form.names[name].subgroup)}delete e.target.form.names[name]}};subnode.addEventListener("change",event,true)}if(data.event){subnode.addEventListener("change",data.event,false)}else if(current.event){subnode.addEventListener("change",current.event,true)}}}if(data.shiftClickSupport&&data.type==="checkbox"){Morebits.checkboxShiftClickSupport(Morebits.QuickForm.getElements(node,data.name))}break;case"number":case"input":node=document.createElement("div");node.setAttribute("id","div_"+id);if(data.label){label=node.appendChild(document.createElement("label"));label.appendChild(Morebits.createHtml(data.label));label.setAttribute("for",data.id||id);label.style.marginRight="3px"}subnode=node.appendChild(document.createElement("input"));subnode.setAttribute("name",data.name);if(data.type==="input"){subnode.setAttribute("type","text")}else{subnode.setAttribute("type","number");["min","max","step","list"].forEach(att=>{if(data[att]){subnode.setAttribute(att,data[att])}})}["value","size","placeholder","maxlength"].forEach(att=>{if(data[att]){subnode.setAttribute(att,data[att])}});["disabled","required","readonly"].forEach(att=>{if(data[att]){subnode.setAttribute(att,att)}});if(data.event){subnode.addEventListener("input",data.event,false)}childContainer=subnode;break;case"dyninput":var min=data.min||1;var max=data.max||Infinity;node=document.createElement("div");label=node.appendChild(document.createElement("h5"));label.appendChild(Morebits.createHtml(data.label));var listNode=node.appendChild(document.createElement("div"));var more=this.compute({type:"button",label:"more",disabled:min>=max,event:function(e){const new_node=new Morebits.QuickForm.Element(e.target.sublist);e.target.area.appendChild(new_node.render());if(++e.target.counter>=e.target.max){e.target.setAttribute("disabled","disabled")}e.stopPropagation()}});node.appendChild(more[0]);var moreButton=more[1];var sublist={type:"_dyninput_row",remove:false,maxlength:data.maxlength,event:data.event,inputs:data.inputs||[{label:data.sublabel||data.label,name:data.name,value:data.value,size:data.size}]};for(i=0;i<min;++i){const elem=new Morebits.QuickForm.Element(sublist);listNode.appendChild(elem.render())}sublist.remove=true;sublist.morebutton=moreButton;sublist.listnode=listNode;moreButton.sublist=sublist;moreButton.area=listNode;moreButton.max=max-min;moreButton.counter=0;break;case"_dyninput_row":node=document.createElement("div");data.inputs.forEach(subdata=>{const cell=new Morebits.QuickForm.Element($.extend(subdata,{type:"_dyninput_cell"}));node.appendChild(cell.render())});if(data.remove){const remove=this.compute({type:"button",label:"remove",event:function(e){const list=e.target.listnode;const node=e.target.inputnode;const more=e.target.morebutton;list.removeChild(node);--more.counter;more.removeAttribute("disabled");e.stopPropagation()}});node.appendChild(remove[0]);const removeButton=remove[1];removeButton.inputnode=node;removeButton.listnode=data.listnode;removeButton.morebutton=data.morebutton}break;case"_dyninput_cell":node=document.createElement("span");if(data.label){label=node.appendChild(document.createElement("label"));label.appendChild(document.createTextNode(data.label));label.setAttribute("for",id+"_input");label.style.marginRight="3px"}subnode=node.appendChild(document.createElement("input"));subnode.setAttribute("id",id+"_input");if(data.value){subnode.setAttribute("value",data.value)}subnode.setAttribute("name",data.name);subnode.setAttribute("type","text");subnode.setAttribute("data-dyninput","data-dyninput");if(data.size){subnode.setAttribute("size",data.size)}if(data.maxlength){subnode.setAttribute("maxlength",data.maxlength)}if(data.required){subnode.setAttribute("required","required")}if(data.disabled){subnode.setAttribute("required","disabled")}if(data.event){subnode.addEventListener("input",data.event,false)}node.style.marginRight="3px";break;case"hidden":node=document.createElement("input");node.setAttribute("type","hidden");node.values=data.value;node.setAttribute("value",data.value);node.setAttribute("name",data.name);break;case"header":node=document.createElement("h5");node.appendChild(Morebits.createHtml(data.label));break;case"div":node=document.createElement("div");if(data.name){node.setAttribute("name",data.name)}if(data.label){const result=document.createElement("span");result.className="quickformDescription";result.appendChild(Morebits.createHtml(data.label));node.appendChild(result)}break;case"submit":node=document.createElement("span");childContainer=node.appendChild(document.createElement("input"));childContainer.setAttribute("type","submit");if(data.label){childContainer.setAttribute("value",data.label)}childContainer.setAttribute("name",data.name||"submit");if(data.disabled){childContainer.setAttribute("disabled","disabled")}break;case"button":node=document.createElement("span");childContainer=node.appendChild(document.createElement("input"));childContainer.setAttribute("type","button");if(data.label){childContainer.setAttribute("value",data.label)}childContainer.setAttribute("name",data.name);if(data.disabled){childContainer.setAttribute("disabled","disabled")}if(data.event){childContainer.addEventListener("click",data.event,false)}break;case"textarea":node=document.createElement("div");node.setAttribute("id","div_"+id);if(data.label){label=node.appendChild(document.createElement("h5"));const labelElement=document.createElement("label");labelElement.appendChild(Morebits.createHtml(data.label));labelElement.setAttribute("for",data.id||id);label.appendChild(labelElement)}subnode=node.appendChild(document.createElement("textarea"));subnode.setAttribute("name",data.name);if(data.cols){subnode.setAttribute("cols",data.cols)}if(data.rows){subnode.setAttribute("rows",data.rows)}if(data.disabled){subnode.setAttribute("disabled","disabled")}if(data.required){subnode.setAttribute("required","required")}if(data.readonly){subnode.setAttribute("readonly","readonly")}if(data.value){subnode.value=data.value}childContainer=subnode;break;default:throw new Error("Morebits.QuickForm: unknown element type "+data.type.toString())}if(!childContainer){childContainer=node}if(data.tooltip){Morebits.QuickForm.Element.generateTooltip(label||node,data)}if(data.extra){childContainer.extra=data.extra}if(data.$data){$(childContainer).data(data.$data)}if(data.style){childContainer.setAttribute("style",data.style)}if(data.className){childContainer.className=childContainer.className?childContainer.className+" "+data.className:data.className}childContainer.setAttribute("id",data.id||id);return[node,childContainer]};Morebits.QuickForm.$tooltip=null;Morebits.QuickForm.Element.generateTooltip=function QuickFormElementGenerateTooltip(node,data){if(!Morebits.QuickForm.$tooltip){Morebits.QuickForm.$tooltip=$("<div>").attr("id","morebits-ui-tooltip").attr("role","tooltip").addClass("morebits-ui-tooltip").appendTo("body")}const $tooltip=Morebits.QuickForm.$tooltip;const $button=$("<span>").addClass("morebits-tooltipButton").text("?").appendTo(node);$button.on("mouseenter",()=>{$tooltip.html(data.tooltip).addClass("visible");const buttonRect=$button[0].getBoundingClientRect();const tooltipRect=$tooltip[0].getBoundingClientRect();const topOffset=buttonRect.bottom+tooltipRect.height<window.innerHeight?buttonRect.bottom:Math.max(0,buttonRect.top-tooltipRect.height);const leftOffset=buttonRect.right+tooltipRect.width<window.innerWidth?buttonRect.right:Math.max(0,buttonRect.left-tooltipRect.width);$tooltip.css("top",window.scrollY+topOffset);$tooltip.css("left",window.scrollX+leftOffset)}).on("mouseleave",()=>{$tooltip.removeClass("visible")})};Morebits.QuickForm.getInputData=function(form){const result={};for(let i=0;i<form.elements.length;i++){const field=form.elements[i];if(field.disabled||!field.name||!field.type||field.type==="submit"||field.type==="button"){continue}const fieldNameNorm=field.name.slice(field.name.indexOf(".")+1);switch(field.type){case"radio":if(field.checked){result[fieldNameNorm]=field.value}break;case"checkbox":if(field.dataset.single){result[fieldNameNorm]=field.checked}else{result[fieldNameNorm]=result[fieldNameNorm]||[];if(field.checked){result[fieldNameNorm].push(field.value)}}break;case"select-multiple":result[fieldNameNorm]=$(field).val();break;case"text":case"textarea":if(field.dataset.dyninput){result[fieldNameNorm]=result[fieldNameNorm]||[];result[fieldNameNorm].push(field.value.trim())}else{result[fieldNameNorm]=field.value.trim()}break;default:if(field.value){result[fieldNameNorm]=field.value}break}}return result};Morebits.QuickForm.getElements=function QuickFormGetElements(form,fieldName){const $form=$(form);fieldName=$.escapeSelector(fieldName);let $elements=$form.find('[name="'+fieldName+'"]');if($elements.length>0){return $elements.toArray()}$elements=$form.find("#"+fieldName);return $elements.toArray()};Morebits.QuickForm.getCheckboxOrRadio=function QuickFormGetCheckboxOrRadio(elementArray,value){const found=$.grep(elementArray,el=>el.value===value);if(found.length>0){return found[0]}return null};Morebits.QuickForm.getElementContainer=function QuickFormGetElementContainer(element){if(element instanceof HTMLFieldSetElement||element instanceof HTMLDivElement||element instanceof HTMLHeadingElement){return element}return element.parentNode};Morebits.QuickForm.getElementLabelObject=function QuickFormGetElementLabelObject(element){if(element.type==="button"||element.type==="submit"||element instanceof HTMLDivElement||element instanceof HTMLHeadingElement){return element}else if(element instanceof HTMLFieldSetElement){return element.getElementsByTagName("legend")[0]}else if(element instanceof HTMLTextAreaElement){return element.parentNode.getElementsByTagName("h5")[0]}return element.parentNode.getElementsByTagName("label")[0]};Morebits.QuickForm.getElementLabel=function QuickFormGetElementLabel(element){const labelElement=Morebits.QuickForm.getElementLabelObject(element);if(!labelElement){return null}return labelElement.firstChild.textContent};Morebits.QuickForm.setElementLabel=function QuickFormSetElementLabel(element,labelText){const labelElement=Morebits.QuickForm.getElementLabelObject(element);if(!labelElement){return false}labelElement.firstChild.textContent=labelText;return true};Morebits.QuickForm.overrideElementLabel=function QuickFormOverrideElementLabel(element,temporaryLabelText){if(!element.hasAttribute("data-oldlabel")){element.setAttribute("data-oldlabel",Morebits.QuickForm.getElementLabel(element))}return Morebits.QuickForm.setElementLabel(element,temporaryLabelText)};Morebits.QuickForm.resetElementLabel=function QuickFormResetElementLabel(element){if(element.hasAttribute("data-oldlabel")){return Morebits.QuickForm.setElementLabel(element,element.getAttribute("data-oldlabel"))}return null};Morebits.QuickForm.setElementVisibility=function QuickFormSetElementVisibility(element,visibility){$(element).toggle(visibility)};Morebits.QuickForm.setElementTooltipVisibility=function QuickFormSetElementTooltipVisibility(element,visibility){$(Morebits.QuickForm.getElementContainer(element)).find(".morebits-tooltipButton").toggle(visibility)};HTMLFormElement.prototype.getChecked=function(name,type){const elements=this.elements[name];if(!elements){return[]}const return_array=[];let i;if(elements instanceof HTMLSelectElement){const options=elements.options;for(i=0;i<options.length;++i){if(options[i].selected){if(options[i].values){return_array.push(options[i].values)}else{return_array.push(options[i].value)}}}}else if(elements instanceof HTMLInputElement){if(type&&elements.type!==type){return[]}else if(elements.checked){return[elements.value]}}else{for(i=0;i<elements.length;++i){if(elements[i].checked){if(type&&elements[i].type!==type){continue}if(elements[i].values){return_array.push(elements[i].values)}else{return_array.push(elements[i].value)}}}}return return_array};HTMLFormElement.prototype.getUnchecked=function(name,type){const elements=this.elements[name];if(!elements){return[]}const return_array=[];let i;if(elements instanceof HTMLSelectElement){const options=elements.options;for(i=0;i<options.length;++i){if(!options[i].selected){if(options[i].values){return_array.push(options[i].values)}else{return_array.push(options[i].value)}}}}else if(elements instanceof HTMLInputElement){if(type&&elements.type!==type){return[]}else if(!elements.checked){return[elements.value]}}else{for(i=0;i<elements.length;++i){if(!elements[i].checked){if(type&&elements[i].type!==type){continue}if(elements[i].values){return_array.push(elements[i].values)}else{return_array.push(elements[i].value)}}}}return return_array};Morebits.ip={sanitizeIPv6:function(address){address=address.trim();if(address===""){return null}if(!mw.util.isIPv6Address(address,true)){return address}address=address.toUpperCase();const abbrevPos=address.indexOf("::");if(abbrevPos>-1){const CIDRStart=address.indexOf("/");const addressEnd=CIDRStart!==-1?CIDRStart-1:address.length-1;let repeat,extra,pad;if(abbrevPos===0){repeat="0:";extra=address==="::"?"0":"";pad=9}else if(abbrevPos===addressEnd-1){repeat=":0";extra="";pad=9}else{repeat=":0";extra=":";pad=8}let replacement=repeat;pad-=address.split(":").length-1;for(let i=1;i<pad;i++){replacement+=repeat}replacement+=extra;address=address.replace("::",replacement)}return address.replace(/(^|:)0+([0-9A-Fa-f]{1,4})/g,"$1$2")},isRange:function(ip){return mw.util.isIPAddress(ip,true)&&!mw.util.isIPAddress(ip)},validCIDR:function(ip){if(Morebits.ip.isRange(ip)){const subnet=parseInt(ip.match(/\/(\d{1,3})$/)[1],10);if(subnet){if(mw.util.isIPv6Address(ip,true)){if(subnet>=32){return true}}else{if(subnet>=16){return true}}}}return false},get64:function(ipv6){if(!ipv6||!mw.util.isIPv6Address(ipv6,true)){return false}const subnetMatch=ipv6.match(/\/(\d{1,3})$/);if(subnetMatch&&parseInt(subnetMatch[1],10)<64){return false}ipv6=Morebits.ip.sanitizeIPv6(ipv6);const ip_re=/^((?:[0-9A-F]{1,4}:){4})(?:[0-9A-F]{1,4}:){3}[0-9A-F]{1,4}(?:\/\d{1,3})?$/;return ipv6.replace(ip_re,"$1"+"0:0:0:0/64")}};Morebits.string={toUpperCaseFirstChar:function(str){str=str.toString();return str.slice(0,1).toUpperCase()+str.slice(1)},toLowerCaseFirstChar:function(str){str=str.toString();return str.slice(0,1).toLowerCase()+str.slice(1)},splitWeightedByKeys:function(str,start,end,skiplist){if(start.length!==end.length){throw new Error("start marker and end marker must be of the same length")}let level=0;let initial=null;const result=[];if(!Array.isArray(skiplist)){if(skiplist===undefined){skiplist=[]}else if(typeof skiplist==="string"){skiplist=[skiplist]}else{throw new Error("non-applicable skiplist parameter")}}for(let i=0;i<str.length;++i){for(let j=0;j<skiplist.length;++j){if(str.substr(i,skiplist[j].length)===skiplist[j]){i+=skiplist[j].length-1;continue}}if(str.substr(i,start.length)===start){if(initial===null){initial=i}++level;i+=start.length-1}else if(str.substr(i,end.length)===end){--level;i+=end.length-1}if(!level&&initial!==null){result.push(str.substring(initial,i+1));initial=null}}return result},formatReasonText:function(str,addSig){let reason=(str||"").toString().trim();const unbinder=new Morebits.Unbinder(reason);unbinder.unbind("<no"+"wiki>","</no"+"wiki>");unbinder.content=unbinder.content.replace(/\|/g,"{{subst:!}}");reason=unbinder.rebind();if(addSig){const sig="~~~~",sigIndex=reason.lastIndexOf(sig);if(sigIndex===-1||sigIndex!==reason.length-sig.length){reason+=" "+sig}}return reason.trim()},formatReasonForLog:function(str){return str.replace(/\n+/g,"{{pb}}").replace(/^(#+)/gm,"#$1").replace(/^(\*+)/gm,"#$1")},safeReplace:function morebitsStringSafeReplace(string,pattern,replacement){return string.replace(pattern,replacement.replace(/\$/g,"$$$$"))},isInfinity:function morebitsStringIsInfinity(expiry){return["indefinite","infinity","infinite","never"].includes(expiry)},escapeRegExp:function(text){return mw.util.escapeRegExp(text).replace(/ |_/g,"[_ ]")}};Morebits.array={uniq:function(arr){if(!Array.isArray(arr)){throw new Error("A non-array object passed to Morebits.array.uniq")}return arr.filter((item,idx)=>arr.indexOf(item)===idx)},dups:function(arr){if(!Array.isArray(arr)){throw new Error("A non-array object passed to Morebits.array.dups")}return arr.filter((item,idx)=>arr.indexOf(item)!==idx)},chunk:function(arr,size){if(!Array.isArray(arr)){throw new Error("A non-array object passed to Morebits.array.chunk")}if(typeof size!=="number"||size<=0){return[arr]}const numChunks=Math.ceil(arr.length/size);const result=new Array(numChunks);for(let i=0;i<numChunks;i++){result[i]=arr.slice(i*size,(i+1)*size)}return result}};Morebits.select2={matchers:{optgroupFull:function(params,data){const originalMatcher=$.fn.select2.defaults.defaults.matcher;const result=originalMatcher(params,data);if(result&¶ms.term&&data.text.toUpperCase().includes(params.term.toUpperCase())){result.children=data.children}return result},wordBeginning:function(params,data){const originalMatcher=$.fn.select2.defaults.defaults.matcher;const result=originalMatcher(params,data);if(!params.term||result&&new RegExp("\\b"+mw.util.escapeRegExp(params.term),"i").test(result.text)){return result}return null}},highlightSearchMatches:function(data){const searchTerm=Morebits.select2SearchQuery;if(!searchTerm||data.loading){return data.text}const idx=data.text.toUpperCase().indexOf(searchTerm.toUpperCase());if(idx<0){return data.text}return $("<span>").append(data.text.slice(0,idx),$("<span>").css("text-decoration","underline").text(data.text.slice(idx,idx+searchTerm.length)),data.text.slice(idx+searchTerm.length))},queryInterceptor:function(params){Morebits.select2SearchQuery=params&¶ms.term},autoStart:function(ev){if(ev.which<48){return}let $target=$(ev.target).closest(".select2-container");if(!$target.length){return}$target=$target.prev();$target.select2("open");const search=$target.data("select2").dropdown.$search||$target.data("select2").selection.$search;search[0].focus()}};Morebits.Unbinder=function Unbinder(string){if(typeof string!=="string"){throw new Error("not a string")}this.content=string;this.counter=0;this.history={};this.prefix="%UNIQ::"+Math.random()+"::";this.postfix="::UNIQ%"};Morebits.Unbinder.prototype={unbind:function UnbinderUnbind(prefix,postfix){if(!prefix||!postfix){throw new Error("Both prefix and postfix must be provided")}const re=new RegExp(prefix+"([\\s\\S]*?)"+postfix,"g");this.content=this.content.replace(re,Morebits.Unbinder.getCallback(this))},rebind:function UnbinderRebind(){let content=this.content;content.self=this;for(const current in this.history){if(Object.prototype.hasOwnProperty.call(this.history,current)){content=content.replace(current,this.history[current])}}return content},prefix:null,postfix:null,content:null,counter:null,history:null};Morebits.Unbinder.getCallback=function UnbinderGetCallback(self){return function UnbinderCallback(match){const current=self.prefix+self.counter+self.postfix;self.history[current]=match;++self.counter;return current}};Morebits.Date=class extends Date{constructor(...args){if(args.length===1){const param=args[0];if(/^\d{14}$/.test(param)){const digitMatch=/(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/.exec(param);if(digitMatch){super(Date.UTC(digitMatch[1],digitMatch[2]-1,digitMatch[3],digitMatch[4],digitMatch[5],digitMatch[6]))}else{super(...args)}}else if(typeof param==="string"){const dateParts=Morebits.l10n.signatureTimestampFormat(param);if(dateParts){super(Date.UTC(...dateParts))}else{super(...args)}}else{super(...args)}}else{super(...args)}if(!this.isValid()){mw.log.warn("Invalid Morebits.Date initialisation:",args)}}isValid(){return!isNaN(this.getTime())}isBefore(date){return this.getTime()<date.getTime()}isAfter(date){return this.getTime()>date.getTime()}getUTCMonthName(){return Morebits.Date.localeData.months[this.getUTCMonth()]}getUTCMonthNameAbbrev(){return Morebits.Date.localeData.monthsShort[this.getUTCMonth()]}getMonthName(){return Morebits.Date.localeData.months[this.getMonth()]}getMonthNameAbbrev(){return Morebits.Date.localeData.monthsShort[this.getMonth()]}getUTCDayName(){return Morebits.Date.localeData.days[this.getUTCDay()]}getUTCDayNameAbbrev(){return Morebits.Date.localeData.daysShort[this.getUTCDay()]}getDayName(){return Morebits.Date.localeData.days[this.getDay()]}getDayNameAbbrev(){return Morebits.Date.localeData.daysShort[this.getDay()]}add(number,unit){let num=parseInt(number,10);if(isNaN(num)){throw new Error('Invalid number "'+number+'" provided.')}unit=unit.toLowerCase();const unitMap=Morebits.Date.unitMap;let unitNorm=unitMap[unit]||unitMap[unit+"s"];if(unitNorm){if(unitNorm==="Week"){unitNorm="Date";num*=7}this["set"+unitNorm](this["get"+unitNorm]()+num);return this}throw new Error('Invalid unit "'+unit+'": Only '+Object.keys(unitMap).join(", ")+" are allowed.")}subtract(number,unit){return this.add(-number,unit)}format(formatstr,zone){if(!this.isValid()){return"Invalid date"}let udate=this;if(zone==="utc"){udate=new Morebits.Date(this.getTime()).add(this.getTimezoneOffset(),"minutes")}else if(typeof zone==="number"){udate=new Morebits.Date(this.getTime()).add(this.getTimezoneOffset()+zone,"minutes")}if(!formatstr){return udate.toISOString()}const pad=function(num,len){len=len||2;return("00"+num).toString().slice(0-len)};var h24=udate.getHours(),m=udate.getMinutes(),s=udate.getSeconds(),ms=udate.getMilliseconds();var D=udate.getDate(),M=udate.getMonth()+1,Y=udate.getFullYear();var h12=h24%12||12,amOrPm=h24>=12?"午後":"午前";var replacementMap={HH:pad(h24),H:h24,hh:pad(h12),h:h12,A:amOrPm,mm:pad(m),m:m,ss:pad(s),s:s,SSS:pad(ms,3),dddd:udate.getDayName(),ddd:udate.getDayNameAbbrev(),d:udate.getDay(),DD:pad(D),D:D,MMMM:udate.getMonthName(),MMM:udate.getMonthNameAbbrev(),MM:pad(M),M:M,YYYY:Y,YY:pad(Y%100),Y:Y};const unbinder=new Morebits.Unbinder(formatstr);unbinder.unbind("\\[","\\]");unbinder.content=unbinder.content.replace(/H{1,2}|h{1,2}|m{1,2}|s{1,2}|SSS|d(d{2,3})?|D{1,2}|M{1,4}|Y{1,2}(Y{2})?|A/g,match=>replacementMap[match]);return unbinder.rebind().replace(/\[(.*?)\]/g,"$1")}calendar(zone){const dateDiff=((new Date).setHours(0,0,0,0)-new Date(this).setHours(0,0,0,0))/864e5;switch(true){case dateDiff===0:return this.format(Morebits.Date.localeData.relativeTimes.thisDay,zone);case dateDiff===1:return this.format(Morebits.Date.localeData.relativeTimes.prevDay,zone);case dateDiff>0&&dateDiff<7:return this.format(Morebits.Date.localeData.relativeTimes.pastWeek,zone);case dateDiff===-1:return this.format(Morebits.Date.localeData.relativeTimes.nextDay,zone);case dateDiff<0&&dateDiff>-7:return this.format(Morebits.Date.localeData.relativeTimes.thisWeek,zone);default:return this.format(Morebits.Date.localeData.relativeTimes.other,zone)}}monthHeaderRegex(){return new RegExp("^(==+)\\s*"+this.getUTCFullYear()+"年+(?:"+this.getUTCMonthName()+"|"+this.getUTCMonthNameAbbrev()+")\\s*\\1","mg")}monthHeader(level){level=parseInt(level,10);level=isNaN(level)?2:level;var header=Array(level+1).join("=");var text=this.getUTCFullYear()+"年"+this.getUTCMonthName();if(header.length){return header+" "+text+" "+header}return text}};Morebits.Date.localeData={months:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],monthsShort:["1月","2月","3月","4月","5月","6月","7月","8月","9月","10月","11月","12月"],days:["日曜日","月曜日","火曜日","水曜日","木曜日","金曜日","土曜日"],daysShort:["日","月","火","水","木","金","土"],relativeTimes:{thisDay:"[今日の]H:mm",prevDay:"[昨日の]H:mm",nextDay:"[明日の]H:mm",thisWeek:"dddd[の]H:mm",pastWeek:"[先週]dddd[の]H:mm",other:"YYYY MM DD"}};Morebits.Date.unitMap={seconds:"Seconds",minutes:"Minutes",hours:"Hours",days:"Date",weeks:"Week",months:"Month",years:"FullYear"};Morebits.wiki={};Morebits.wiki.numberOfActionsLeft=0;Morebits.wiki.nbrOfCheckpointsLeft=0;Morebits.wiki.actionCompleted=function(self){if(--Morebits.wiki.numberOfActionsLeft<=0&&Morebits.wiki.nbrOfCheckpointsLeft<=0){Morebits.wiki.actionCompleted.event(self)}};Morebits.wiki.actionCompleted.event=function(){if(Morebits.wiki.actionCompleted.notice){Morebits.Status.actionCompleted(Morebits.wiki.actionCompleted.notice)}if(Morebits.wiki.actionCompleted.redirect){if(!/^\w+:\/\//.test(Morebits.wiki.actionCompleted.redirect)){Morebits.wiki.actionCompleted.redirect=mw.util.getUrl(Morebits.wiki.actionCompleted.redirect);if(Morebits.wiki.actionCompleted.followRedirect===false){Morebits.wiki.actionCompleted.redirect+="?redirect=no"}}window.setTimeout(()=>{window.location=Morebits.wiki.actionCompleted.redirect},Morebits.wiki.actionCompleted.timeOut)}};Morebits.wiki.actionCompleted.timeOut=typeof window.wpActionCompletedTimeOut==="undefined"?5e3:window.wpActionCompletedTimeOut;Morebits.wiki.actionCompleted.redirect=null;Morebits.wiki.actionCompleted.notice=null;Morebits.wiki.addCheckpoint=function(){++Morebits.wiki.nbrOfCheckpointsLeft};Morebits.wiki.removeCheckpoint=function(){if(--Morebits.wiki.nbrOfCheckpointsLeft<=0&&Morebits.wiki.numberOfActionsLeft<=0){Morebits.wiki.actionCompleted.event()}};Morebits.wiki.Api=function(currentAction,query,onSuccess,statusElement,onError){this.currentAction=currentAction;this.query=query;this.query.assert="user";if(!query.errorformat||!["wikitext","plaintext"].includes(query.errorformat)){this.query.errorformat="html"}this.query.uselang="content";this.query.errorlang="uselang";this.query.errorsuselocal=1;this.onSuccess=onSuccess;this.onError=onError;if(statusElement){this.setStatusElement(statusElement)}else{this.statelem=new Morebits.Status(currentAction)}if(!query.format){this.query.format="xml"}else if(query.format==="json"&&!query.formatversion){this.query.formatversion="2"}else if(!["xml","json"].includes(query.format)){this.statelem.error("Invalid API format: only xml and json are supported.")}if(query.action&&["query","review","stabilize","watch"].includes(query.action)){delete query.tags}};Morebits.wiki.Api.prototype={currentAction:"",onSuccess:null,onError:null,parent:window,query:null,response:null,responseXML:null,statelem:null,statusText:null,errorCode:null,errorText:null,badtokenRetry:false,setParent:function(parent){this.parent=parent},setStatusElement:function(statusElement){this.statelem=statusElement;this.statelem.status(this.currentAction)},post:function(callerAjaxParameters){++Morebits.wiki.numberOfActionsLeft;const queryString=$.map(this.query,(val,i)=>{if(Array.isArray(val)){return encodeURIComponent(i)+"="+val.map(encodeURIComponent).join("|")}else if(val!==undefined){return encodeURIComponent(i)+"="+encodeURIComponent(val)}}).join("&").replace(/^(.*?)(\btoken=[^&]*)&(.*)/,"$1$3&$2");const headers={"Api-User-Agent":morebitsWikiApiUserAgent};if(this.query.action==="parse"){headers["Promise-Non-Write-API-Action"]="true"}const ajaxparams=$.extend({},{context:this,type:this.query.action==="query"?"GET":"POST",url:mw.util.wikiScript("api"),data:queryString,dataType:this.query.format,headers:headers},callerAjaxParameters);return $.ajax(ajaxparams).then(function onAPIsuccess(response,statusText){this.statusText=statusText;this.response=this.responseXML=response;if(this.query.format==="json"){this.errorCode=response.errors&&response.errors[0].code;if(this.query.errorformat==="html"){this.errorText=response.errors&&response.errors[0].html}else if(this.query.errorformat==="wikitext"||this.query.errorformat==="plaintext"){this.errorText=response.errors&&response.errors[0].text}}else{this.errorCode=$(response).find("errors error").eq(0).attr("code");this.errorText=$(response).find("errors error").eq(0).text()}if(typeof this.errorCode==="string"){return this.returnError(callerAjaxParameters)}if(this.onSuccess){this.onSuccess.call(this.parent,this)}else{this.statelem.info("完了")}Morebits.wiki.actionCompleted();return $.Deferred().resolveWith(this.parent,[this])},function onAPIfailure(jqXHR,statusText,errorThrown){this.statusText=statusText;this.errorThrown=errorThrown;this.errorText=statusText+' "'+jqXHR.statusText+'" occurred while contacting the API.';return this.returnError()})},returnError:function(callerAjaxParameters){if(this.errorCode==="badtoken"&&!this.badtokenRetry){this.statelem.warn("トークンが無効です。新しいトークンを取得して再試行します...");this.badtokenRetry=true;return Morebits.wiki.Api.getToken().then(token=>{this.query.token=token;return this.post(callerAjaxParameters)})}this.statelem.error(this.errorText+" ("+this.errorCode+")");if(this.onError){this.onError.call(this.parent,this)}return $.Deferred().rejectWith(this.parent,[this])},getStatusElement:function(){return this.statelem},getErrorCode:function(){return this.errorCode},getErrorText:function(){return this.errorText},getXML:function(){return this.responseXML},getResponse:function(){return this.response}};Morebits.wiki.getCachedPage=function(title,maxAge=86400){return new mw.Api({userAgent:morebitsWikiApiUserAgent}).get({action:"query",prop:"revisions",titles:title,rvprop:"content",rvslots:"*",format:"json",formatversion:"2",smaxage:String(maxAge),maxage:String(maxAge),uselang:"content"}).then(data=>{const page=data.query.pages[0];if(page.missing){return null}return page.revisions[0].slots.main.content})};Morebits.wiki.getCachedJson=function(title){return Morebits.wiki.getCachedPage(title).then(wikitext=>JSON.parse(wikitext))};var morebitsWikiApiUserAgent="morebits.js ([[w:WT:TW]])";Morebits.wiki.Api.setApiUserAgent=function(ua){morebitsWikiApiUserAgent=(ua?ua+" ":"")+"morebits.js ([[w:WT:TW]])"};Morebits.wiki.Api.getToken=function(){var tokenApi=new Morebits.wiki.Api("トークンを取得",{action:"query",meta:"tokens",type:"csrf",format:"json"});return tokenApi.post().then(apiobj=>apiobj.response.query.tokens.csrftoken)};Morebits.wiki.Page=function(pageName,status){if(!status){status='"'+pageName+'"を開く'}const ctx={pageName:pageName,pageExists:false,editSummary:null,changeTags:null,testActions:null,callbackParameters:null,statusElement:status instanceof Morebits.Status?status:new Morebits.Status(status),pageText:null,editMode:"all",appendText:null,prependText:null,newSectionText:null,newSectionTitle:null,createOption:null,minorEdit:false,botEdit:false,pageSection:null,maxConflictRetries:2,maxRetries:2,followRedirect:false,followCrossNsRedirect:true,watchlistOption:"nochange",watchlistExpiry:null,discussionToolsAutoSubscribe:null,creator:null,timestamp:null,revertOldID:null,moveDestination:null,moveTalkPage:false,moveSubpages:false,moveSuppressRedirect:false,protectEdit:null,protectMove:null,protectCreate:null,protectCascade:null,deleteTalkPage:false,undeleteTalkPage:false,lookupNonRedirectCreator:false,flaggedRevs:null,pageLoaded:false,csrfToken:null,loadTime:null,lastEditTime:null,pageID:null,contentModel:null,pageParsed:null,sections:null,revertCurID:null,revertUser:null,watched:false,fullyProtected:false,suppressProtectWarning:false,conflictRetries:0,retries:0,onLoadSuccess:null,onLoadFailure:null,onParseSuccess:null,onParseFailure:null,onSaveSuccess:null,onSaveFailure:null,onLookupCreationSuccess:null,onLookupCreationFailure:null,onMoveSuccess:null,onMoveFailure:null,onDeleteSuccess:null,onDeleteFailure:null,onUndeleteSuccess:null,onUndeleteFailure:null,onProtectSuccess:null,onProtectFailure:null,onStabilizeSuccess:null,onStabilizeFailure:null,loadQuery:null,loadApi:null,parseApi:null,saveApi:null,lookupCreationApi:null,moveApi:null,moveProcessApi:null,patrolApi:null,patrolProcessApi:null,triageApi:null,triageProcessListApi:null,triageProcessApi:null,deleteApi:null,deleteProcessApi:null,undeleteApi:null,undeleteProcessApi:null,protectApi:null,protectProcessApi:null,stabilizeApi:null,stabilizeProcessApi:null};const emptyFunction=function(){};this.load=function(onSuccess,onFailure){ctx.onLoadSuccess=onSuccess;ctx.onLoadFailure=onFailure||emptyFunction;if(!onSuccess){ctx.statusElement.error("Internal error: no onSuccess callback provided to load()!");ctx.onLoadFailure(this);return}ctx.loadQuery={action:"query",prop:"info|revisions",inprop:"watched",intestactions:"edit",curtimestamp:"",meta:"tokens",type:"csrf",titles:ctx.pageName,format:"json"};if(ctx.editMode==="all"){ctx.loadQuery.rvprop="content|timestamp"}else if(ctx.editMode==="revert"){ctx.loadQuery.rvprop="timestamp";ctx.loadQuery.rvlimit=1;ctx.loadQuery.rvstartid=ctx.revertOldID}if(ctx.followRedirect){ctx.loadQuery.redirects=""}if(typeof ctx.pageSection==="number"){ctx.loadQuery.rvsection=ctx.pageSection}if(Morebits.userIsSysop){ctx.loadQuery.inprop+="|protection"}ctx.loadApi=new Morebits.wiki.Api("ページを取得中...",ctx.loadQuery,fnLoadSuccess,ctx.statusElement,ctx.onLoadFailure);ctx.loadApi.setParent(this);ctx.loadApi.post()};this.parse=function(onSuccess,onFailure){ctx.onParseSuccess=onSuccess;ctx.onParseFailure=onFailure||emptyFunction;if(!onSuccess){ctx.statusElement.error("Internal error: no onSuccess callback provided to parse()!");ctx.onParseFailure(this);return}var query={action:"parse",format:"json",page:ctx.pageName,prop:"sections",utf8:1};ctx.parseApi=new Morebits.wiki.Api("ページを取得中...",query,fnParseSuccess,ctx.statusElement,ctx.onParseFailure);ctx.parseApi.setParent(this);ctx.parseApi.post()};this.save=function(onSuccess,onFailure){ctx.onSaveSuccess=onSuccess;ctx.onSaveFailure=onFailure||emptyFunction;const canUseMwUserToken=fnCanUseMwUserToken("edit");if(!ctx.pageLoaded&&!canUseMwUserToken){ctx.statusElement.error("Internal error: attempt to save a page that has not been loaded!");ctx.onSaveFailure(this);return}if(!ctx.editSummary){if(ctx.editMode==="new"&&ctx.newSectionTitle){ctx.editSummary=""}else{ctx.statusElement.error("Internal error: edit summary not set before save!");ctx.onSaveFailure(this);return}}if(ctx.fullyProtected&&!ctx.suppressProtectWarning&&!confirm(ctx.fullyProtected==="infinity"?'全保護されたページ "'+ctx.pageName+'" (無期限保護) を編集しようとしています。\n\n編集を続行するにはOKをクリックし、この編集をスキップするにはキャンセルをクリックしてください。':'全保護されたページ "'+ctx.pageName+'" (保護期限: '+new Morebits.Date(ctx.fullyProtected).calendar("utc")+" (UTC)) を編集しようとしています。\n\n編集を続行するにはOKをクリックし、この編集をスキップするにはキャンセルをクリックしてください。")){ctx.statusElement.error("全保護されたページの編集が中断されました。");ctx.onSaveFailure(this);return}ctx.retries=0;const query={action:"edit",title:ctx.pageName,summary:ctx.editSummary,token:canUseMwUserToken?mw.user.tokens.get("csrfToken"):ctx.csrfToken,watchlist:ctx.watchlistOption,format:"json"};if(ctx.changeTags){query.tags=ctx.changeTags}if(fnApplyWatchlistExpiry()){query.watchlistexpiry=ctx.watchlistExpiry}if(typeof ctx.pageSection==="number"){query.section=ctx.pageSection}if(ctx.minorEdit){query.minor=true}else{query.notminor=true}if(ctx.botEdit){query.bot=true}if(ctx.discussionToolsAutoSubscribe!==null){query.discussiontoolsautosubscribe=ctx.discussionToolsAutoSubscribe?"yes":"no"}switch(ctx.editMode){case"append":if(ctx.appendText===null){ctx.statusElement.error("Internal error: append text not set before save!");ctx.onSaveFailure(this);return}query.appendtext=ctx.appendText;break;case"prepend":if(ctx.prependText===null){ctx.statusElement.error("Internal error: prepend text not set before save!");ctx.onSaveFailure(this);return}query.prependtext=ctx.prependText;break;case"new":if(!ctx.newSectionText){ctx.statusElement.error("Internal error: new section text not set before save!");ctx.onSaveFailure(this);return}query.section="new";query.text=ctx.newSectionText;query.sectiontitle=ctx.newSectionTitle||ctx.editSummary;break;case"revert":query.undo=ctx.revertCurID;query.undoafter=ctx.revertOldID;if(ctx.lastEditTime){query.basetimestamp=ctx.lastEditTime}query.starttimestamp=ctx.loadTime;break;default:query.text=ctx.pageText;if(ctx.lastEditTime){query.basetimestamp=ctx.lastEditTime}query.starttimestamp=ctx.loadTime;break}if(["recreate","createonly","nocreate"].includes(ctx.createOption)){query[ctx.createOption]=""}if(canUseMwUserToken&&ctx.followRedirect){query.redirect=true}ctx.saveApi=new Morebits.wiki.Api("ページを保存中...",query,fnSaveSuccess,ctx.statusElement,fnSaveError);ctx.saveApi.setParent(this);ctx.saveApi.post()};this.append=function(onSuccess,onFailure){ctx.editMode="append";if(fnCanUseMwUserToken("edit")){this.save(onSuccess,onFailure)}else{ctx.onSaveSuccess=onSuccess;ctx.onSaveFailure=onFailure||emptyFunction;this.load(fnAutoSave,ctx.onSaveFailure)}};this.prepend=function(onSuccess,onFailure){ctx.editMode="prepend";if(fnCanUseMwUserToken("edit")){this.save(onSuccess,onFailure)}else{ctx.onSaveSuccess=onSuccess;ctx.onSaveFailure=onFailure||emptyFunction;this.load(fnAutoSave,ctx.onSaveFailure)}};this.newSection=function(onSuccess,onFailure){ctx.editMode="new";if(fnCanUseMwUserToken("edit")){this.save(onSuccess,onFailure)}else{ctx.onSaveSuccess=onSuccess;ctx.onSaveFailure=onFailure||emptyFunction;this.load(fnAutoSave,ctx.onSaveFailure)}};this.getPageName=function(){return ctx.pageName};this.getPageText=function(){return ctx.pageText};this.getSections=function(){return ctx.sections};this.setPageText=function(pageText){ctx.editMode="all";ctx.pageText=pageText};this.setAppendText=function(appendText){ctx.editMode="append";ctx.appendText=appendText};this.setPrependText=function(prependText){ctx.editMode="prepend";ctx.prependText=prependText};this.setNewSectionText=function(newSectionText){ctx.editMode="new";ctx.newSectionText=newSectionText};this.setNewSectionTitle=function(newSectionTitle){ctx.editMode="new";ctx.newSectionTitle=newSectionTitle};this.setEditSummary=function(summary){ctx.editSummary=summary};this.setChangeTags=function(tags){ctx.changeTags=tags};this.setCreateOption=function(createOption){ctx.createOption=createOption};this.setMinorEdit=function(minorEdit){ctx.minorEdit=minorEdit};this.setBotEdit=function(botEdit){ctx.botEdit=botEdit};this.setPageSection=function(pageSection){ctx.pageSection=pageSection};this.setMaxConflictRetries=function(maxConflictRetries){ctx.maxConflictRetries=maxConflictRetries};this.setMaxRetries=function(maxRetries){ctx.maxRetries=maxRetries};this.setWatchlist=function(watchlistOption,watchlistExpiry){if(watchlistOption instanceof Morebits.Date||watchlistOption instanceof Date){watchlistOption=watchlistOption.toISOString()}if(typeof watchlistExpiry==="undefined"){watchlistExpiry="infinity"}else if(watchlistExpiry instanceof Morebits.Date||watchlistExpiry instanceof Date){watchlistExpiry=watchlistExpiry.toISOString()}switch(watchlistOption){case"nochange":case"no":case false:case undefined:ctx.watchlistOption="nochange";ctx.watchlistExpiry=null;break;case"unwatch":ctx.watchlistOption="unwatch";break;case"preferences":case"default":ctx.watchlistOption="preferences";ctx.watchlistExpiry=watchlistExpiry;break;case"watch":case"yes":case true:ctx.watchlistOption="watch";ctx.watchlistExpiry=watchlistExpiry;break;default:ctx.watchlistOption="watch";ctx.watchlistExpiry=watchlistOption;break}};this.setWatchlistExpiry=function(watchlistExpiry){if(typeof watchlistExpiry==="undefined"){watchlistExpiry="infinity"}else if(watchlistExpiry instanceof Morebits.Date||watchlistExpiry instanceof Date){watchlistExpiry=watchlistExpiry.toISOString()}ctx.watchlistExpiry=watchlistExpiry};this.setDiscussionToolsAutoSubscribe=function(subscribe){ctx.discussionToolsAutoSubscribe=subscribe};this.setFollowRedirect=function(followRedirect,followCrossNsRedirect){if(ctx.pageLoaded){ctx.statusElement.error("Internal error: cannot change redirect setting after the page has been loaded!");return}ctx.followRedirect=followRedirect;ctx.followCrossNsRedirect=typeof followCrossNsRedirect!=="undefined"?followCrossNsRedirect:ctx.followCrossNsRedirect};
/**
* @param {boolean} flag - If set true, the author and timestamp of
* the first non-redirect version of the page is retrieved.
*
* Warning:
* 1. If there are no revisions among the first 50 that are
* non-redirects, or if there are less 50 revisions and all are
* redirects, the original creation is retrieved.
* 2. Revisions that the user is not privileged to access
* (revdeled/suppressed) will be treated as non-redirects.
* 3. Must not be used when the page has a non-wikitext contentmodel
* such as Modulespace Lua or user JavaScript/CSS.
*/this.setLookupNonRedirectCreator=function(flag){ctx.lookupNonRedirectCreator=flag};this.setMoveDestination=function(destination){ctx.moveDestination=destination};this.setMoveTalkPage=function(flag){ctx.moveTalkPage=!!flag};this.setMoveSubpages=function(flag){ctx.moveSubpages=!!flag};this.setMoveSuppressRedirect=function(flag){ctx.moveSuppressRedirect=!!flag};this.setEditProtection=function(level,expiry){ctx.protectEdit={level:level,expiry:expiry||"infinity"}};this.setMoveProtection=function(level,expiry){ctx.protectMove={level:level,expiry:expiry||"infinity"}};this.setCreateProtection=function(level,expiry){ctx.protectCreate={level:level,expiry:expiry||"infinity"}};this.setCascadingProtection=function(flag){ctx.protectCascade=!!flag};this.suppressProtectWarning=function(){ctx.suppressProtectWarning=true};this.setDeleteTalkPage=function(flag){ctx.deleteTalkPage=!!flag};this.setUndeleteTalkPage=function(flag){ctx.undeleteTalkPage=!!flag};this.setOldID=function(oldID){ctx.revertOldID=oldID};this.getCurrentID=function(){return ctx.revertCurID};this.getRevisionUser=function(){return ctx.revertUser};this.getLastEditTime=function(){return ctx.lastEditTime};this.setCallbackParameters=function(callbackParameters){ctx.callbackParameters=callbackParameters};this.getCallbackParameters=function(){return ctx.callbackParameters};this.setStatusElement=function(statusElement){ctx.statusElement=statusElement};this.getStatusElement=function(){return ctx.statusElement};this.setFlaggedRevs=function(level,expiry){ctx.flaggedRevs={level:level,expiry:expiry||"infinity"}};this.exists=function(){return ctx.pageExists};this.getPageID=function(){return ctx.pageID};this.getContentModel=function(){return ctx.contentModel};this.getWatched=function(){return ctx.watched};this.getLoadTime=function(){return ctx.loadTime};this.getCreator=function(){return ctx.creator};this.getCreationTimestamp=function(){return ctx.timestamp};this.canEdit=function(){return!!ctx.testActions&&ctx.testActions.includes("edit")};this.lookupCreation=function(onSuccess,onFailure){ctx.onLookupCreationSuccess=onSuccess;ctx.onLookupCreationFailure=onFailure||emptyFunction;if(!onSuccess){ctx.statusElement.error("Internal error: no onSuccess callback provided to lookupCreation()!");ctx.onLookupCreationFailure(this);return}const query={action:"query",prop:"revisions",titles:ctx.pageName,rvlimit:1,rvprop:"user|timestamp",rvdir:"newer",format:"json"};if(ctx.lookupNonRedirectCreator){query.rvsection=0;query.rvprop+="|content"}if(ctx.followRedirect){query.redirects=""}ctx.lookupCreationApi=new Morebits.wiki.Api("ページ作成情報の取得中",query,fnLookupCreationSuccess,ctx.statusElement,ctx.onLookupCreationFailure);ctx.lookupCreationApi.setParent(this);ctx.lookupCreationApi.post()};this.revert=function(onSuccess,onFailure){ctx.onSaveSuccess=onSuccess;ctx.onSaveFailure=onFailure||emptyFunction;if(!ctx.revertOldID){ctx.statusElement.error("Internal error: revision ID to revert to was not set before revert!");ctx.onSaveFailure(this);return}ctx.editMode="revert";this.load(fnAutoSave,ctx.onSaveFailure)};this.move=function(onSuccess,onFailure){ctx.onMoveSuccess=onSuccess;ctx.onMoveFailure=onFailure||emptyFunction;if(!fnPreflightChecks.call(this,"move",ctx.onMoveFailure)){return}if(!ctx.moveDestination){ctx.statusElement.error("Internal error: destination page name was not set before move!");ctx.onMoveFailure(this);return}if(fnCanUseMwUserToken("move")){fnProcessMove.call(this,this)}else{const query=fnNeedTokenInfoQuery("move");ctx.moveApi=new Morebits.wiki.Api("トークンの取得中...",query,fnProcessMove,ctx.statusElement,ctx.onMoveFailure);ctx.moveApi.setParent(this);ctx.moveApi.post()}};this.patrol=function(){if(!Morebits.userIsSysop&&!Morebits.userIsInGroup("patroller")){return}if($(".patrollink").length){const patrolhref=$(".patrollink a").attr("href");ctx.rcid=mw.util.getParamValue("rcid",patrolhref);fnProcessPatrol(this,this)}else{const patrolQuery={action:"query",prop:"info",meta:"tokens",type:"patrol",list:"recentchanges",titles:ctx.pageName,rcprop:"patrolled",rctitle:ctx.pageName,rclimit:1,format:"json"};ctx.patrolApi=new Morebits.wiki.Api("トークンの取得中...",patrolQuery,fnProcessPatrol);ctx.patrolApi.setParent(this);ctx.patrolApi.post()}};this.triage=function(){if(!mw.config.get("pageTriageNamespaces").includes(new mw.Title(ctx.pageName).getNamespaceId())){this.patrol()}else{if(!Morebits.userIsSysop&&!Morebits.userIsInGroup("patroller")){return}if(new mw.Title(Morebits.pageNameNorm).getPrefixedText()===new mw.Title(ctx.pageName).getPrefixedText()){ctx.pageID=mw.config.get("wgArticleId");fnProcessTriageList(this,this)}else{const query=fnNeedTokenInfoQuery("triage");ctx.triageApi=new Morebits.wiki.Api("トークンの取得中...",query,fnProcessTriageList);ctx.triageApi.setParent(this);ctx.triageApi.post()}}};this.deletePage=function(onSuccess,onFailure){ctx.onDeleteSuccess=onSuccess;ctx.onDeleteFailure=onFailure||emptyFunction;if(!fnPreflightChecks.call(this,"delete",ctx.onDeleteFailure)){return}if(fnCanUseMwUserToken("delete")){fnProcessDelete.call(this,this)}else{const query=fnNeedTokenInfoQuery("delete");ctx.deleteApi=new Morebits.wiki.Api("トークンの取得中...",query,fnProcessDelete,ctx.statusElement,ctx.onDeleteFailure);ctx.deleteApi.setParent(this);ctx.deleteApi.post()}};this.undeletePage=function(onSuccess,onFailure){ctx.onUndeleteSuccess=onSuccess;ctx.onUndeleteFailure=onFailure||emptyFunction;if(!fnPreflightChecks.call(this,"undelete",ctx.onUndeleteFailure)){return}if(fnCanUseMwUserToken("undelete")){fnProcessUndelete.call(this,this)}else{const query=fnNeedTokenInfoQuery("undelete");ctx.undeleteApi=new Morebits.wiki.Api("トークンの取得中...",query,fnProcessUndelete,ctx.statusElement,ctx.onUndeleteFailure);ctx.undeleteApi.setParent(this);ctx.undeleteApi.post()}};this.protect=function(onSuccess,onFailure){ctx.onProtectSuccess=onSuccess;ctx.onProtectFailure=onFailure||emptyFunction;if(!fnPreflightChecks.call(this,"protect",ctx.onProtectFailure)){return}if(!ctx.protectEdit&&!ctx.protectMove&&!ctx.protectCreate){ctx.statusElement.error("Internal error: you must set edit and/or move and/or create protection before calling protect()!");ctx.onProtectFailure(this);return}const query=fnNeedTokenInfoQuery("protect");ctx.protectApi=new Morebits.wiki.Api("トークンの取得中...",query,fnProcessProtect,ctx.statusElement,ctx.onProtectFailure);ctx.protectApi.setParent(this);ctx.protectApi.post()};this.stabilize=function(onSuccess,onFailure){ctx.onStabilizeSuccess=onSuccess;ctx.onStabilizeFailure=onFailure||emptyFunction;if(!fnPreflightChecks.call(this,"FlaggedRevs",ctx.onStabilizeFailure)){return}if(!ctx.flaggedRevs){ctx.statusElement.error("Internal error: you must set flaggedRevs before calling stabilize()!");ctx.onStabilizeFailure(this);return}if(fnCanUseMwUserToken("stabilize")){fnProcessStabilize.call(this,this)}else{const query=fnNeedTokenInfoQuery("stabilize");ctx.stabilizeApi=new Morebits.wiki.Api("トークンの取得中...",query,fnProcessStabilize,ctx.statusElement,ctx.onStabilizeFailure);ctx.stabilizeApi.setParent(this);ctx.stabilizeApi.post()}};var fnCanUseMwUserToken=function(action="edit"){if(ctx.watchlistExpiry&&!Morebits.string.isInfinity(ctx.watchlistExpiry)){return false}if(ctx.followRedirect){if(!ctx.followCrossNsRedirect){return false}if(action!=="edit"||(ctx.editMode==="all"||ctx.editMode==="revert")){return false}}if(Morebits.userIsSysop&&!ctx.suppressProtectWarning){if(new mw.Title(Morebits.pageNameNorm).getPrefixedText()!==new mw.Title(ctx.pageName).getPrefixedText()){return false}const editRestriction=mw.config.get("wgRestrictionEdit");if(!editRestriction||editRestriction.includes("sysop")){return false}}return!!mw.user.tokens.get("csrfToken")};
/**
* When functions can't use
* {@link Morebits.wiki.Page~fnCanUseMwUserToken|fnCanUseMwUserToken}
* or require checking protection or watched status, maintain the query
* in one place. Used for {@link Morebits.wiki.Page#deletePage|delete},
* {@link Morebits.wiki.Page#undeletePage|undelete},
* {@link Morebits.wiki.Page#protect|protect},
* {@link Morebits.wiki.Page#stabilize|stabilize},
* and {@link Morebits.wiki.Page#move|move}
* (basically, just not {@link Morebits.wiki.Page#load|load}).
*
* @param {string} action - The action being undertaken, e.g. "edit" or
* "delete".
* @return {Object} Appropriate query.
*/var fnNeedTokenInfoQuery=function(action){const query={action:"query",meta:"tokens",type:"csrf",titles:ctx.pageName,prop:"info",inprop:"watched",format:"json"};if(action!=="stabilize"&&(action!=="move"||Morebits.userIsSysop)){query.inprop+="|protection"}if(ctx.followRedirect&&action!=="undelete"){query.redirects=""}return query};var fnAutoSave=function(pageobj){pageobj.save(ctx.onSaveSuccess,ctx.onSaveFailure)};var fnLoadSuccess=function(){const response=ctx.loadApi.getResponse().query;if(!fnCheckPageName(response,ctx.onLoadFailure)){return}const page=response.pages[0];let rev;ctx.pageExists=!page.missing;if(ctx.pageExists){rev=page.revisions[0];ctx.lastEditTime=rev.timestamp;ctx.pageText=rev.content;ctx.pageID=page.pageid}else{ctx.pageText="";ctx.pageID=0}ctx.csrfToken=response.tokens.csrftoken;if(!ctx.csrfToken){ctx.statusElement.error("編集トークンの取得に失敗しました。");ctx.onLoadFailure(this);return}ctx.loadTime=ctx.loadApi.getResponse().curtimestamp;if(!ctx.loadTime){ctx.statusElement.error("現在のタイムスタンプの取得に失敗しました。");ctx.onLoadFailure(this);return}ctx.contentModel=page.contentmodel;ctx.watched=page.watchlistexpiry||page.watched;if(Morebits.userIsSysop){const editProt=page.protection.filter(pr=>pr.type==="edit"&&pr.level==="sysop").pop();if(editProt){ctx.fullyProtected=editProt.expiry}else{ctx.fullyProtected=false}}ctx.revertCurID=page.lastrevid;const testactions=page.actions;ctx.testActions=[];Object.keys(testactions).forEach(action=>{if(testactions[action]){ctx.testActions.push(action)}});if(ctx.editMode==="revert"){ctx.revertCurID=rev&&rev.revid;if(!ctx.revertCurID){ctx.statusElement.error("Failed to retrieve current revision ID.");ctx.onLoadFailure(this);return}ctx.revertUser=rev&&rev.user;if(!ctx.revertUser){if(rev&&rev.userhidden){ctx.revertUser="<username hidden>"}else{ctx.statusElement.error("Failed to retrieve user who made the revision.");ctx.onLoadFailure(this);return}}ctx.editSummary="[[Help:Revert|Reverted]] to revision "+ctx.revertOldID+" by "+ctx.revertUser+": "+ctx.editSummary}ctx.pageLoaded=true;ctx.onLoadSuccess(this)};var fnParseSuccess=function(){if(ctx.parseApi.getResponse().error){ctx.statusElement.error("ページの取得に失敗しました。");ctx.onParseFailure(this);return}var response=ctx.parseApi.getResponse().parse;ctx.pageParsed=true;ctx.sections=response.sections;ctx.onParseSuccess(this)};var fnCheckPageName=function(response,onFailure){if(!onFailure){onFailure=emptyFunction}const page=response.pages&&response.pages[0];if(page){if(page.invalid){ctx.statusElement.error("無効なページタイトル: "+ctx.pageName);onFailure(this);return false}const resolvedName=page.title;if(response.redirects){const origNs=new mw.Title(ctx.pageName).namespace;const newNs=new mw.Title(resolvedName).namespace;if(origNs!==newNs&&!ctx.followCrossNsRedirect){ctx.statusElement.error(ctx.pageName+"は"+resolvedName+"への名前空間が違うリダイレクトです。中止します。");onFailure(this);return false}new Morebits.Status("Note",ctx.pageName+"から"+resolvedName+"へのリダイレクト")}ctx.pageName=resolvedName}else{ctx.statusElement.error("Could not resolve redirects for: "+ctx.pageName);onFailure(this);++Morebits.wiki.numberOfActionsLeft;return false}return true};var fnApplyWatchlistExpiry=function(){if(ctx.watchlistExpiry){if(!ctx.watched||Morebits.string.isInfinity(ctx.watchlistExpiry)){return true}else if(typeof ctx.watched==="string"){let newExpiry;const rel=ctx.watchlistExpiry.split(" ");try{newExpiry=(new Morebits.Date).add(rel[0],rel[1])}catch(e){newExpiry=new Morebits.Date(ctx.watchlistExpiry)}if(newExpiry.isValid()){if(newExpiry.isAfter(new Morebits.Date(ctx.watched))){return true}}else{return true}}}return false};var fnSaveSuccess=function(){ctx.editMode="all";const response=ctx.saveApi.getResponse();if(response.edit.result==="Success"){const link=document.createElement("a");link.setAttribute("href",mw.util.getUrl(ctx.pageName));link.appendChild(document.createTextNode(ctx.pageName));ctx.statusElement.info(["完了 (",link,")"]);if(ctx.onSaveSuccess){ctx.onSaveSuccess(this)}return}if(response.edit.captcha){ctx.statusElement.error("ウィキのサーバーがキャプチャを入力するよう要求したため、ページを保存できませんでした。")}else{ctx.statusElement.error("ページの保存中にAPIから不明なエラーを受信")}++Morebits.wiki.numberOfActionsLeft;ctx.onSaveFailure(this)};var fnSaveError=function(){const errorCode=ctx.saveApi.getErrorCode();if(errorCode==="editconflict"&&ctx.conflictRetries++<ctx.maxConflictRetries){const purgeQuery={action:"purge",titles:ctx.pageName};var purgeApi=new Morebits.wiki.Api("編集の競合が検出されたため、サーバーのキャッシュを破棄します。",purgeQuery,()=>{--Morebits.wiki.numberOfActionsLeft;ctx.statusElement.info("編集の競合が検出されたため、編集をやり直します");if(fnCanUseMwUserToken("edit")){ctx.saveApi.post()}else{ctx.loadApi.post()}},ctx.statusElement);purgeApi.post()}else if((errorCode===null||errorCode===undefined)&&ctx.retries++<ctx.maxRetries){ctx.statusElement.info("保存に失敗しました。2秒後に再試行します ...");--Morebits.wiki.numberOfActionsLeft;sleep(2e3).then(()=>{ctx.saveApi.post()})}else{const response=ctx.saveApi.getResponse();const errorData=response.error||response.errors[0].data;switch(errorCode){case"protectedpage":ctx.statusElement.error("Failed to save edit: Page is protected");break;case"abusefilter-disallowed":ctx.statusElement.error('The edit was disallowed by the edit filter: "'+errorData.abusefilter.description+'".');break;case"abusefilter-warning":ctx.statusElement.error(['A warning was returned by the edit filter: "',errorData.abusefilter.description,'". If you wish to proceed with the edit, please carry it out again. This warning will not appear a second time.']);break;case"spamblacklist":var spam=errorData.spamblacklist.matches[0];ctx.statusElement.error("Could not save the page because the URL "+spam+" is on the spam blacklist");break;default:ctx.statusElement.error("Failed to save edit: "+ctx.saveApi.getErrorText())}ctx.editMode="all";if(ctx.onSaveFailure){ctx.onSaveFailure(this)}}};const isTextRedirect=function(text){if(!text){return false}return Morebits.l10n.redirectTagAliases.some(tag=>new RegExp("^\\s*"+tag+"\\W","i").test(text))};var fnLookupCreationSuccess=function(){const response=ctx.lookupCreationApi.getResponse().query;if(!fnCheckPageName(response,ctx.onLookupCreationFailure)){return}const rev=response.pages[0].revisions&&response.pages[0].revisions[0];if(!rev){ctx.statusElement.error("Could not find any revisions of "+ctx.pageName);ctx.onLookupCreationFailure(this);return}if(!ctx.lookupNonRedirectCreator||!isTextRedirect(rev.content)){ctx.creator=rev.user;if(!ctx.creator){ctx.statusElement.error("Could not find name of page creator");ctx.onLookupCreationFailure(this);return}ctx.timestamp=rev.timestamp;if(!ctx.timestamp){ctx.statusElement.error("Could not find timestamp of page creation");ctx.onLookupCreationFailure(this);return}ctx.statusElement.info("retrieved page creation information");ctx.onLookupCreationSuccess(this)}else{ctx.lookupCreationApi.query.rvlimit=50;ctx.lookupCreationApi.query.titles=ctx.pageName;ctx.lookupCreationApi=new Morebits.wiki.Api("ページ作成情報の取得中",ctx.lookupCreationApi.query,fnLookupNonRedirectCreator,ctx.statusElement,ctx.onLookupCreationFailure);ctx.lookupCreationApi.setParent(this);ctx.lookupCreationApi.post()}};var fnLookupNonRedirectCreator=function(){const response=ctx.lookupCreationApi.getResponse().query;const revs=response.pages[0].revisions;for(let i=0;i<revs.length;i++){if(!isTextRedirect(revs[i].content)){ctx.creator=revs[i].user;ctx.timestamp=revs[i].timestamp;break}}if(!ctx.creator){
// fallback to give first revision author if no non-redirect version in the first 50
ctx.creator=revs[0].user;ctx.timestamp=revs[0].timestamp;if(!ctx.creator){ctx.statusElement.error("Could not find name of page creator");ctx.onLookupCreationFailure(this);return}}if(!ctx.timestamp){ctx.statusElement.error("Could not find timestamp of page creation");ctx.onLookupCreationFailure(this);return}ctx.statusElement.info("retrieved page creation information");ctx.onLookupCreationSuccess(this)};var fnPreflightChecks=function(action,onFailure){if(!Morebits.userIsSysop&&action!=="move"){ctx.statusElement.error("Cannot "+action+"page : only admins can do that");onFailure(this);return false}if(!ctx.editSummary){ctx.statusElement.error("Internal error: "+action+" reason not set (use setEditSummary function)!");onFailure(this);return false}return true};const fnProcessChecks=function(action,onFailure,response){const missing=response.pages[0].missing;const actionMissing=missing&&["delete","stabilize","move"].includes(action);const protectMissing=action==="protect"&&missing&&(ctx.protectEdit||ctx.protectMove);const saltMissing=action==="protect"&&!missing&&ctx.protectCreate;if(actionMissing||protectMissing||saltMissing){ctx.statusElement.error("Cannot "+action+" the page because it "+(missing?"no longer":"already")+" exists");onFailure(this);return false}let editprot;if(action==="undelete"){editprot=response.pages[0].protection.filter(pr=>pr.type==="create"&&pr.level==="sysop").pop()}else if(action==="delete"||action==="move"){editprot=response.pages[0].protection.filter(pr=>pr.type==="edit"&&pr.level==="sysop").pop()}if(editprot&&!ctx.suppressProtectWarning&&!confirm("You are about to "+action+' the fully protected page "'+ctx.pageName+(editprot.expiry==="infinity"?'" (protected indefinitely)':'" (protection expiring '+new Morebits.Date(editprot.expiry).calendar("utc")+" (UTC))")+". \n\nClick OK to proceed with "+action+", or Cancel to skip.")){ctx.statusElement.error("Aborted "+action+" on fully protected page.");onFailure(this);return false}if(!response.tokens.csrftoken){ctx.statusElement.error("Failed to retrieve token.");onFailure(this);return false}return true};var fnProcessMove=function(){let pageTitle,token;if(fnCanUseMwUserToken("move")){token=mw.user.tokens.get("csrfToken");pageTitle=ctx.pageName}else{const response=ctx.moveApi.getResponse().query;if(!fnProcessChecks("move",ctx.onMoveFailure,response)){return}token=response.tokens.csrftoken;const page=response.pages[0];pageTitle=page.title;ctx.watched=page.watchlistexpiry||page.watched}const query={action:"move",from:pageTitle,to:ctx.moveDestination,token:token,reason:ctx.editSummary,watchlist:ctx.watchlistOption,format:"json"};if(ctx.changeTags){query.tags=ctx.changeTags}if(fnApplyWatchlistExpiry()){query.watchlistexpiry=ctx.watchlistExpiry}if(ctx.moveTalkPage){query.movetalk="true"}if(ctx.moveSubpages){query.movesubpages="true"}if(ctx.moveSuppressRedirect){query.noredirect="true"}ctx.moveProcessApi=new Morebits.wiki.Api("ページの移動...",query,ctx.onMoveSuccess,ctx.statusElement,ctx.onMoveFailure);ctx.moveProcessApi.setParent(this);ctx.moveProcessApi.post()};var fnProcessPatrol=function(){const query={action:"patrol",format:"json"};if(ctx.rcid){query.rcid=ctx.rcid;query.token=mw.user.tokens.get("patrolToken")}else{const response=ctx.patrolApi.getResponse().query;if(!response.recentchanges[0].unpatrolled){return}const lastrevid=response.pages[0].lastrevid;if(!lastrevid){return}query.revid=lastrevid;const token=response.tokens.csrftoken;if(!token){return}query.token=token}if(ctx.changeTags){query.tags=ctx.changeTags}const patrolStat=new Morebits.Status("Marking page as patrolled");ctx.patrolProcessApi=new Morebits.wiki.Api("patrolling page...",query,null,patrolStat);ctx.patrolProcessApi.setParent(this);ctx.patrolProcessApi.post()};var fnProcessTriageList=function(){if(ctx.pageID){ctx.csrfToken=mw.user.tokens.get("csrfToken")}else{const response=ctx.triageApi.getResponse().query;ctx.pageID=response.pages[0].pageid;if(!ctx.pageID){return}ctx.csrfToken=response.tokens.csrftoken;if(!ctx.csrfToken){return}}const query={action:"pagetriagelist",page_id:ctx.pageID,format:"json"};ctx.triageProcessListApi=new Morebits.wiki.Api("checking curation status...",query,fnProcessTriage);ctx.triageProcessListApi.setParent(this);ctx.triageProcessListApi.post()};var fnProcessTriage=function(){const responseList=ctx.triageProcessListApi.getResponse().pagetriagelist;if(!responseList||responseList.result!=="success"){return}const page=responseList.pages&&responseList.pages[0];if(!page||!parseInt(page.patrol_status,10)){const query={action:"pagetriageaction",pageid:ctx.pageID,reviewed:1,token:ctx.csrfToken,format:"json"};if(ctx.changeTags){query.tags=ctx.changeTags}const triageStat=new Morebits.Status("Marking page as curated");ctx.triageProcessApi=new Morebits.wiki.Api("curating page...",query,null,triageStat);ctx.triageProcessApi.setParent(this);ctx.triageProcessApi.post()}};var fnProcessDelete=function(){let pageTitle,token;if(fnCanUseMwUserToken("delete")){token=mw.user.tokens.get("csrfToken");pageTitle=ctx.pageName}else{const response=ctx.deleteApi.getResponse().query;if(!fnProcessChecks("delete",ctx.onDeleteFailure,response)){return}token=response.tokens.csrftoken;const page=response.pages[0];pageTitle=page.title;ctx.watched=page.watchlistexpiry||page.watched}const query={action:"delete",title:pageTitle,token:token,reason:ctx.editSummary,watchlist:ctx.watchlistOption,format:"json"};if(ctx.changeTags){query.tags=ctx.changeTags}if(ctx.deleteTalkPage){query.deletetalk="true"}if(fnApplyWatchlistExpiry()){query.watchlistexpiry=ctx.watchlistExpiry}ctx.deleteProcessApi=new Morebits.wiki.Api("deleting page...",query,ctx.onDeleteSuccess,ctx.statusElement,fnProcessDeleteError);ctx.deleteProcessApi.setParent(this);ctx.deleteProcessApi.post()};var fnProcessDeleteError=function(){const errorCode=ctx.deleteProcessApi.getErrorCode();if(errorCode==="internal_api_error_DBQueryError"&&ctx.retries++<ctx.maxRetries){ctx.statusElement.info("Database query error, retrying");--Morebits.wiki.numberOfActionsLeft;ctx.deleteProcessApi.post()}else if(errorCode==="missingtitle"){ctx.statusElement.error("Cannot delete the page, because it no longer exists");if(ctx.onDeleteFailure){ctx.onDeleteFailure.call(this,ctx.deleteProcessApi)}}else{ctx.statusElement.error("Failed to delete the page: "+ctx.deleteProcessApi.getErrorText());if(ctx.onDeleteFailure){ctx.onDeleteFailure.call(this,ctx.deleteProcessApi)}}};var fnProcessUndelete=function(){let pageTitle,token;if(fnCanUseMwUserToken("undelete")){token=mw.user.tokens.get("csrfToken");pageTitle=ctx.pageName}else{const response=ctx.undeleteApi.getResponse().query;if(!fnProcessChecks("undelete",ctx.onUndeleteFailure,response)){return}token=response.tokens.csrftoken;const page=response.pages[0];pageTitle=page.title;ctx.watched=page.watchlistexpiry||page.watched}const query={action:"undelete",title:pageTitle,token:token,reason:ctx.editSummary,watchlist:ctx.watchlistOption,format:"json"};if(ctx.changeTags){query.tags=ctx.changeTags}if(ctx.undeleteTalkPage){query.undeletetalk="true"}if(fnApplyWatchlistExpiry()){query.watchlistexpiry=ctx.watchlistExpiry}ctx.undeleteProcessApi=new Morebits.wiki.Api("undeleting page...",query,ctx.onUndeleteSuccess,ctx.statusElement,fnProcessUndeleteError);ctx.undeleteProcessApi.setParent(this);ctx.undeleteProcessApi.post()};var fnProcessUndeleteError=function(){const errorCode=ctx.undeleteProcessApi.getErrorCode();if(errorCode==="internal_api_error_DBQueryError"){if(ctx.retries++<ctx.maxRetries){ctx.statusElement.info("Database query error, retrying");--Morebits.wiki.numberOfActionsLeft;ctx.undeleteProcessApi.post()}else{ctx.statusElement.error("Repeated database query error, please try again");if(ctx.onUndeleteFailure){ctx.onUndeleteFailure.call(this,ctx.undeleteProcessApi)}}}else if(errorCode==="cantundelete"){ctx.statusElement.error("Cannot undelete the page, either because there are no revisions to undelete or because it has already been undeleted");if(ctx.onUndeleteFailure){ctx.onUndeleteFailure.call(this,ctx.undeleteProcessApi)}}else{ctx.statusElement.error("Failed to undelete the page: "+ctx.undeleteProcessApi.getErrorText());if(ctx.onUndeleteFailure){ctx.onUndeleteFailure.call(this,ctx.undeleteProcessApi)}}};var fnProcessProtect=function(){const response=ctx.protectApi.getResponse().query;if(!fnProcessChecks("protect",ctx.onProtectFailure,response)){return}const token=response.tokens.csrftoken;const page=response.pages[0];const pageTitle=page.title;ctx.watched=page.watchlistexpiry||page.watched;const prs=response.pages[0].protection;let editprot,moveprot,createprot;prs.forEach(pr=>{if(pr.type==="edit"&&!pr.source){editprot=pr}else if(pr.type==="move"){moveprot=pr}else if(pr.type==="create"){createprot=pr}});if(!ctx.protectEdit&&editprot){ctx.protectEdit={level:editprot.level,expiry:editprot.expiry}}if(!ctx.protectMove&&moveprot){ctx.protectMove={level:moveprot.level,expiry:moveprot.expiry}}if(!ctx.protectCreate&&createprot){ctx.protectCreate={level:createprot.level,expiry:createprot.expiry}}if(ctx.protectCascade===null){ctx.protectCascade=!!prs.filter(pr=>pr.cascade).length}if(ctx.protectCascade){if((!ctx.protectEdit||ctx.protectEdit.level!=="sysop"||(!ctx.protectMove||ctx.protectMove.level!=="sysop"))&&!confirm('"'+ctx.pageName+'"ではカスケード保護が有効になっていますが、管理者レベルの保護を選択していません。\n\n'+"OKをクリックすることで調節して管理者レベルのカスケード保護を続行するか、キャンセルをクリックしてこのアクションをスキップします。")){ctx.statusElement.error("カスケード保護は中止されました。");ctx.onProtectFailure(this);return}ctx.protectEdit.level="sysop";ctx.protectMove.level="sysop"}const protections=[],expirys=[];if(ctx.protectEdit){protections.push("edit="+ctx.protectEdit.level);expirys.push(ctx.protectEdit.expiry)}if(ctx.protectMove){protections.push("move="+ctx.protectMove.level);expirys.push(ctx.protectMove.expiry)}if(ctx.protectCreate){protections.push("create="+ctx.protectCreate.level);expirys.push(ctx.protectCreate.expiry)}const query={action:"protect",title:pageTitle,token:token,protections:protections.join("|"),expiry:expirys.join("|"),reason:ctx.editSummary,watchlist:ctx.watchlistOption,format:"json"};if(ctx.changeTags){query.tags=ctx.changeTags}if(fnApplyWatchlistExpiry()){query.watchlistexpiry=ctx.watchlistExpiry}if(ctx.protectCascade){query.cascade="true"}ctx.protectProcessApi=new Morebits.wiki.Api("protecting page...",query,ctx.onProtectSuccess,ctx.statusElement,ctx.onProtectFailure);ctx.protectProcessApi.setParent(this);ctx.protectProcessApi.post()};var fnProcessStabilize=function(){let pageTitle,token;if(fnCanUseMwUserToken("stabilize")){token=mw.user.tokens.get("csrfToken");pageTitle=ctx.pageName}else{const response=ctx.stabilizeApi.getResponse().query;if(!fnProcessChecks("stabilize",ctx.onStabilizeFailure,response)){return}token=response.tokens.csrftoken;const page=response.pages[0];pageTitle=page.title}const query={action:"stabilize",title:pageTitle,token:token,protectlevel:ctx.flaggedRevs.level,expiry:ctx.flaggedRevs.expiry,reason:ctx.editSummary,watchlist:ctx.watchlistOption,format:"json"};ctx.stabilizeProcessApi=new Morebits.wiki.Api("configuring stabilization settings...",query,ctx.onStabilizeSuccess,ctx.statusElement,ctx.onStabilizeFailure);ctx.stabilizeProcessApi.setParent(this);ctx.stabilizeProcessApi.post()};var sleep=function(milliseconds){const deferred=$.Deferred();setTimeout(deferred.resolve,milliseconds);return deferred}};Morebits.wiki.Preview=function(previewbox){this.previewbox=previewbox;$(previewbox).addClass("morebits-previewbox").hide();this.beginRender=function(wikitext,pageTitle,sectionTitle){$(previewbox).show();const statusspan=document.createElement("span");previewbox.appendChild(statusspan);Morebits.Status.init(statusspan);const query={action:"parse",prop:["text","modules"],pst:true,preview:true,text:wikitext,title:pageTitle||mw.config.get("wgPageName"),disablelimitreport:true,disableeditsection:true,format:"json"};if(sectionTitle){query.section="new";query.sectiontitle=sectionTitle}const renderApi=new Morebits.wiki.Api("loading...",query,fnRenderSuccess,new Morebits.Status("Preview"));return renderApi.post()};var fnRenderSuccess=function(apiobj){const response=apiobj.getResponse();const html=response.parse.text;if(!html){apiobj.statelem.error("failed to retrieve preview, or template was blanked");return}previewbox.innerHTML=html;mw.loader.load(response.parse.modulestyles);mw.loader.load(response.parse.modules);$(previewbox).find("a").attr("target","_blank");mw.hook("wikipage.content").fire($(previewbox))};this.closePreview=function(){$(previewbox).empty().hide()}};Morebits.wikitext={};Morebits.wikitext.parseTemplate=function(text,start){start=start||0;const level=[];let count=-1;let unnamed=0;let equals=-1;let current="";const result={name:"",parameters:{}};let key,value;function findParam(final){if(count===-1){result.name=current.slice(2).trim();++count}else{if(equals!==-1){key=current.substring(0,equals).trim();value=final?current.substring(equals+1,current.length-2).trim():current.substring(equals+1).trim();result.parameters[key]=value;equals=-1}else{const param=final?current.substring(equals+1,current.length-2):current;if(param){result.parameters[++unnamed]=param;++count}}}}for(let i=start;i<text.length;++i){const test3=text.substr(i,3);if(test3==="{{{"||test3==="}}}"&&level[level.length-1]===3){current+=test3;i+=2;if(test3==="{{{"){level.push(3)}else{level.pop()}continue}const test2=text.substr(i,2);if(test2==="{{"||test2==="[["){current+=test2;++i;if(test2==="{{"){level.push(2)}else{level.push("wl")}continue}if(test2==="}}"&&level[level.length-1]===2||test2==="]]"&&level[level.length-1]==="wl"){current+=test2;++i;level.pop();if(test2==="}}"&&level.length===0){findParam(true);break}continue}if(text.charAt(i)==="|"&&level.length===1){findParam();current=""}else if(equals===-1&&text.charAt(i)==="="&&level.length===1){equals=current.length;current+=text.charAt(i)}else{current+=text.charAt(i)}}return result};Morebits.wikitext.Page=function mediawikiPage(text){this.text=text};Morebits.wikitext.Page.prototype={text:"",removeLink:function(link_target){const mwTitle=mw.Title.newFromText(link_target);const namespaceID=mwTitle.getNamespaceId();const title=mwTitle.getMainText();let link_regex_string="";if(namespaceID!==0){link_regex_string=Morebits.namespaceRegex(namespaceID)+":"}link_regex_string+=Morebits.pageNameRegex(title);const isFileOrCategory=[6,14].includes(namespaceID);const colon=isFileOrCategory?":":":?";const simple_link_regex=new RegExp("\\[\\["+colon+"("+link_regex_string+")\\]\\]","g");const piped_link_regex=new RegExp("\\[\\["+colon+link_regex_string+"\\|(.+?)\\]\\]","g");this.text=this.text.replace(simple_link_regex,"$1").replace(piped_link_regex,"$1");return this},commentOutImage:function(image,reason){const unbinder=new Morebits.Unbinder(this.text);unbinder.unbind("\x3c!--","--\x3e");reason=reason?reason+": ":"";const image_re_string=Morebits.pageNameRegex(image);const links_re=new RegExp("\\[\\["+Morebits.namespaceRegex(6)+":\\s*"+image_re_string+"\\s*[\\|(?:\\]\\])]");const allLinks=Morebits.string.splitWeightedByKeys(unbinder.content,"[[","]]");for(let i=0;i<allLinks.length;++i){if(links_re.test(allLinks[i])){const replacement="\x3c!-- "+reason+allLinks[i]+" --\x3e";unbinder.content=unbinder.content.replace(allLinks[i],replacement);unbinder.unbind("\x3c!--","--\x3e")}}const gallery_image_re=new RegExp("(^\\s*"+Morebits.namespaceRegex(6)+":\\s*"+image_re_string+"\\s*(?:\\|.*?$|$))","mg");unbinder.content=unbinder.content.replace(gallery_image_re,"\x3c!-- "+reason+"$1 --\x3e");unbinder.unbind("\x3c!--","--\x3e");const free_image_re=new RegExp("(\\|\\s*(?:[\\w\\s]+\\=)?\\s*(?:"+Morebits.namespaceRegex(6)+":\\s*)?"+image_re_string+")","mg");unbinder.content=unbinder.content.replace(free_image_re,"\x3c!-- "+reason+"$1 --\x3e");this.text=unbinder.rebind();return this},removeImage:function(image){var unbinder=new Morebits.Unbinder(this.text);unbinder.unbind("\x3c!--","--\x3e");var image_re_string=Morebits.pageNameRegex(image);var links_re=new RegExp("\\[\\["+Morebits.namespaceRegex(6)+":\\s*"+image_re_string+"\\s*[\\|(?:\\]\\])]");var allLinks=Morebits.string.splitWeightedByKeys(unbinder.content,"[[","]]");for(var i=0;i<allLinks.length;++i){if(links_re.test(allLinks[i])){var replacingtext=Morebits.string.escapeRegExp(allLinks[i]);var replacing=new RegExp("(\n|)"+replacingtext+"$");unbinder.content=unbinder.content.replace(replacing,"").replace(allLinks[i],"")}}unbinder.unbind("\x3c!--","--\x3e");var gallery_image_re=new RegExp("\\n(^\\s*"+Morebits.namespaceRegex(6)+":\\s*"+image_re_string+"\\s*(?:\\|.*?$|$))","mg");unbinder.content=unbinder.content.replace(gallery_image_re,"");unbinder.unbind("\x3c!--","--\x3e");var free_image_re=new RegExp("(\\|\\s*(?:[\\w\\s]+\\=)?\\s*(?:"+Morebits.namespaceRegex(6)+":\\s*)?"+image_re_string+")","mg");unbinder.content=unbinder.content.replace(free_image_re,"");this.text=unbinder.rebind();return this},addToImageComment:function(image,data){const image_re_string=Morebits.pageNameRegex(image);const links_re=new RegExp("\\[\\["+Morebits.namespaceRegex(6)+":\\s*"+image_re_string+"\\s*[\\|(?:\\]\\])]");const allLinks=Morebits.string.splitWeightedByKeys(this.text,"[[","]]");for(let i=0;i<allLinks.length;++i){if(links_re.test(allLinks[i])){let replacement=allLinks[i];replacement=replacement.replace(/\]\]$/,"|"+data+"]]");this.text=this.text.replace(allLinks[i],replacement)}}const gallery_re=new RegExp("^(\\s*"+image_re_string+".*?)\\|?(.*?)$","mg");const newtext="$1|$2 "+data;this.text=this.text.replace(gallery_re,newtext);return this},removeTemplate:function(template){const template_re_string=Morebits.pageNameRegex(template);const links_re=new RegExp("\\{\\{(?:"+Morebits.namespaceRegex(10)+":)?\\s*"+template_re_string+"\\s*[\\|(?:\\}\\})]");const allTemplates=Morebits.string.splitWeightedByKeys(this.text,"{{","}}",["{{{","}}}"]);for(let i=0;i<allTemplates.length;++i){if(links_re.test(allTemplates[i])){this.text=this.text.replace(allTemplates[i],"")}}return this},insertAfterTemplates:function(tag,regex,flags,preRegex){if(typeof tag==="undefined"){throw new Error("No tag provided")}if(typeof regex==="undefined"||!regex.length){throw new Error("No regex provided")}else if(Array.isArray(regex)){regex=regex.join("|")}if(typeof flags!=="string"){flags="i"}if(!preRegex||!preRegex.length){preRegex=""}else if(Array.isArray(preRegex)){preRegex=preRegex.join("|")}this.text=this.text.replace(new RegExp("^\\s*"+"(?:((?:\\s*"+preRegex+"|"+"\\{\\{\\s*(?:"+regex+")\\d*\\s*"+"(\\|(?:\\{\\{[^{}]*\\}\\}|[^{}])*)?"+"\\}\\})+"+"(?:\\s*\\n)?)"+"\\s*)?",flags),"$1"+tag);return this},getText:function(){return this.text}};Morebits.UserspaceLogger=function(logPageName){if(!logPageName){throw new Error("記録ページ名が指定されていません")}this.initialText="";this.headerLevel=3;this.changeTags="";this.log=function(logText,summaryText){const def=$.Deferred();if(!logText){return def.reject()}var page=new Morebits.wiki.Page("User:"+mw.config.get("wgUserName")+"/"+logPageName,"ユーザースペースログに項目を追加する");page.load(pageobj=>{let text=pageobj.getPageText()||this.initialText;const date=new Morebits.Date(pageobj.getLoadTime());if(!date.monthHeaderRegex().exec(text)){text+="\n\n"+date.monthHeader(this.headerLevel)}pageobj.setPageText(text+"\n"+logText);pageobj.setEditSummary(summaryText);pageobj.setChangeTags(this.changeTags);pageobj.setCreateOption("recreate");pageobj.save(def.resolve,def.reject)});return def}};Morebits.Status=function Status(text,stat,type){this.textRaw=text;this.text=Morebits.createHtml(text);this.type=type||"status";this.generate();if(stat){this.update(stat,type)}};Morebits.Status.init=function(root){if(!(root instanceof Element)){throw new Error("object not an instance of Element")}while(root.hasChildNodes()){root.removeChild(root.firstChild)}Morebits.Status.root=root;Morebits.Status.errorEvent=null};Morebits.Status.root=null;Morebits.Status.onError=function(handler){if(typeof handler==="function"){Morebits.Status.errorEvent=handler}else{throw new Error("Morebits.Status.onError: handler is not a function")}};Morebits.Status.prototype={stat:null,statRaw:null,text:null,textRaw:null,type:"status",target:null,node:null,linked:false,link:function(){if(!this.linked&&Morebits.Status.root){Morebits.Status.root.appendChild(this.node);this.linked=true}},unlink:function(){if(this.linked){Morebits.Status.root.removeChild(this.node);this.linked=false}},update:function(status,type){this.statRaw=status;this.stat=Morebits.createHtml(status);if(type){this.type=type;if(type==="error"){Morebits.wiki.numberOfActionsLeft=1e3;if(Morebits.Status.errorEvent){Morebits.Status.errorEvent()}console.error(this.textRaw+": "+this.statRaw)}}this.render()},generate:function(){this.node=document.createElement("div");this.node.appendChild(document.createElement("span")).appendChild(this.text);this.node.appendChild(document.createElement("span")).appendChild(document.createTextNode(": "));this.target=this.node.appendChild(document.createElement("span"));this.target.appendChild(document.createTextNode(""))},render:function(){this.node.className="morebits_status_"+this.type;while(this.target.hasChildNodes()){this.target.removeChild(this.target.firstChild)}this.target.appendChild(this.stat);this.link()},status:function(status){this.update(status,"status")},info:function(status){this.update(status,"info")},warn:function(status){this.update(status,"warn")},error:function(status){this.update(status,"error")}};Morebits.Status.status=function(text,status){return new Morebits.Status(text,status)};Morebits.Status.info=function(text,status){return new Morebits.Status(text,status,"info")};Morebits.Status.warn=function(text,status){return new Morebits.Status(text,status,"warn")};Morebits.Status.error=function(text,status){return new Morebits.Status(text,status,"error")};Morebits.Status.actionCompleted=function(text){const node=document.createElement("div");node.appendChild(document.createElement("b")).appendChild(document.createTextNode(text));node.className="morebits_status_info morebits_action_complete";if(Morebits.Status.root){Morebits.Status.root.appendChild(node)}};Morebits.Status.printUserText=function(comments,message){const p=document.createElement("p");p.innerHTML=message;const div=document.createElement("div");div.className="morebits-usertext";div.style.marginTop="0";div.style.whiteSpace="pre-wrap";div.textContent=comments;p.appendChild(div);Morebits.Status.root.appendChild(p)};Morebits.htmlNode=function(type,content,color){const node=document.createElement(type);if(color){node.style.color=color}node.appendChild(document.createTextNode(content));return node};Morebits.checkboxShiftClickSupport=function(jQuerySelector,jQueryContext){let lastCheckbox=null;function clickHandler(event){const thisCb=this;if(event.shiftKey&&lastCheckbox!==null){const $cbs=$(jQuerySelector,jQueryContext);let index=-1,lastIndex=-1,i;for(i=0;i<$cbs.length;i++){if($cbs[i]===thisCb){index=i;if(lastIndex>-1){break}}if($cbs[i]===lastCheckbox){lastIndex=i;if(index>-1){break}}}if(index>-1&&lastIndex>-1){const endState=thisCb.checked;let start,finish;if(index<lastIndex){start=index+1;finish=lastIndex}else{start=lastIndex;finish=index-1}for(i=start;i<=finish;i++){if($cbs[i].checked!==endState){$cbs[i].click()}}}}lastCheckbox=thisCb;return true}$(jQuerySelector,jQueryContext).on("click",clickHandler)};Morebits.BatchOperation=function(currentAction){const ctx={pageList:null,options:{chunkSize:50,preserveIndividualStatusLines:false},statusElement:new Morebits.Status(currentAction||"バッチ処理の実行"),worker:null,postFinish:null,countStarted:0,countFinished:0,countFinishedSuccess:0,currentChunkIndex:-1,pageChunks:[],running:false};this.getStatusElement=function(){return ctx.statusElement};this.setPageList=function(pageList){ctx.pageList=pageList};this.setOption=function(optionName,optionValue){ctx.options[optionName]=optionValue};this.run=function(worker,postFinish){if(ctx.running){ctx.statusElement.error("Batch operation is already running");return}ctx.running=true;ctx.worker=worker;ctx.postFinish=postFinish;ctx.countStarted=0;ctx.countFinished=0;ctx.countFinishedSuccess=0;ctx.currentChunkIndex=-1;ctx.pageChunks=[];const total=ctx.pageList.length;if(!total){ctx.statusElement.info("ページが指定されていません");ctx.running=false;if(ctx.postFinish){ctx.postFinish()}return}ctx.pageChunks=Morebits.array.chunk(ctx.pageList,ctx.options.chunkSize);Morebits.wiki.addCheckpoint();ctx.statusElement.status("0%");fnStartNewChunk()};this.workerSuccess=function(arg){if(arg instanceof Morebits.wiki.Api||arg instanceof Morebits.wiki.Page){const statelem=arg.getStatusElement();if(ctx.options.preserveIndividualStatusLines){if(arg.getPageName||arg.pageName||arg.query&&arg.query.title){var pageName=arg.getPageName?arg.getPageName():arg.pageName||arg.query.title;statelem.info("完了 ([["+pageName+"]])")}else{statelem.info("完了")}}else{statelem.unlink()}}else if(typeof arg==="string"&&ctx.options.preserveIndividualStatusLines){new Morebits.Status(arg,"完了 ([["+arg+"]])")}ctx.countFinishedSuccess++;fnDoneOne()};this.workerFailure=function(){fnDoneOne()};const thisProxy=this;var fnStartNewChunk=function(){const chunk=ctx.pageChunks[++ctx.currentChunkIndex];if(!chunk){return}ctx.countStarted+=chunk.length;chunk.forEach(page=>{ctx.worker(page,thisProxy)})};var fnDoneOne=function(){ctx.countFinished++;const total=ctx.pageList.length;if(ctx.countFinished<total){const progress=Math.round(100*ctx.countFinished/total);ctx.statusElement.status(progress+"%");if(ctx.countFinished>=ctx.countStarted-Math.max(ctx.options.chunkSize/10,2)&&Math.floor(ctx.countFinished/ctx.options.chunkSize)>ctx.currentChunkIndex){fnStartNewChunk()}}else if(ctx.countFinished===total){var statusString="完了 ("+ctx.countFinishedSuccess+"/"+ctx.countFinished+" アクションが正常に完了)";if(ctx.countFinishedSuccess<ctx.countFinished){ctx.statusElement.warn(statusString)}else{ctx.statusElement.info(statusString)}if(ctx.postFinish){ctx.postFinish()}Morebits.wiki.removeCheckpoint();ctx.running=false}else{ctx.statusElement.warn("完了 ("+(ctx.countFinished-total)+"件上回った)");Morebits.wiki.removeCheckpoint();ctx.running=false}}};Morebits.TaskManager=function(context){this.taskDependencyMap=new Map;this.failureCallbackMap=new Map;this.deferreds=new Map;this.context=context||window;this.add=function(func,deps,onFailure){this.taskDependencyMap.set(func,deps);this.failureCallbackMap.set(func,onFailure||(()=>{}));const deferred=$.Deferred();this.deferreds.set(func,deferred)};this.execute=function(){const self=this;this.taskDependencyMap.forEach((deps,task)=>{const dependencyPromisesArray=deps.map(dep=>self.deferreds.get(dep));$.when.apply(self.context,dependencyPromisesArray).then(function(){const result=task.apply(self.context,arguments);if(result===undefined){mw.log.error("Morebits.TaskManager: task returned undefined");self.deferreds.get(task).reject.apply(self.context,arguments);self.failureCallbackMap.get(task).apply(self.context,[])}result.then(function(){self.deferreds.get(task).resolve.apply(self.context,arguments)},function(){self.deferreds.get(task).reject.apply(self.context,arguments);self.failureCallbackMap.get(task).apply(self.context,arguments)})},function(){self.failureCallbackMap.get(task).apply(self.context,arguments)})});return $.when.apply(null,[...this.deferreds.values()])}};Morebits.SimpleWindow=function SimpleWindow(width,height){const $dialog=$("<div>").addClass("morebits-dialog").attr("role","dialog").attr("tabindex",-1);const $titleBar=$("<div>").addClass("morebits-dialog-titlebar").append($("<span>").addClass("morebits-dialog-title"),$("<button>").addClass("morebits-dialog-close").text("×").on("click",()=>this.close()));const $content=$("<div>").addClass("morebits-dialog-content").attr("id","morebits-dialog-content-"+Math.round(Math.random()*1e15));const $buttonPane=$("<div>").addClass("morebits-dialog-buttonpane").append($("<span>").addClass("morebits-dialog-buttons"),$("<span>").addClass("morebits-dialog-footerlinks"));const $resizer=$("<div>").addClass("morebits-dialog-resizer");$dialog.append($titleBar,$content,$buttonPane,$resizer).css("width",Math.min(parseInt(width||800,10),$(window).width())).css("height","auto").css("max-height",height+20).css("top",Math.max(0,window.scrollY+$(window).height()/2-(height+20)/2)).css("left",Math.max(0,window.scrollX+$(window).width()/2-$dialog.width()/2));$dialog.on("focus",()=>this.focus()).on("keydown",e=>{if(e.key==="Escape"){this.close()}});let isDragging=false,isResizing=false,initialX,initialY,initialWidth,initialHeight;$titleBar.on("mousedown",e=>{isDragging=true;initialX=e.clientX-$dialog.offset().left;initialY=e.clientY-$dialog.offset().top});$resizer.on("mousedown",e=>{isResizing=true;$dialog.css("height",$dialog.height());$dialog.css("max-height","none");initialWidth=$dialog.width();initialHeight=$dialog.height();initialX=e.clientX;initialY=e.clientY});$(document).on("mousemove",e=>{if(isDragging){$dialog.css("left",Math.max(0,e.clientX-initialX)).css("top",Math.max(0,e.clientY-initialY))}else if(isResizing){$dialog.css("width",initialWidth+(e.clientX-initialX)).css("height",initialHeight+(e.clientY-initialY))}}).on("mouseup",()=>{isDragging=false;isResizing=false});this.$dialog=$dialog;this.content=$content[0];this.height=height};Morebits.SimpleWindow.prototype={buttons:[],height:600,hasFooterLinks:false,scriptName:null,focus:function(){if(!this.$dialog.hasClass("morebits-dialog-modal")){$(".morebits-dialog:not(.morebits-dialog-modal)").get().filter(dialog=>dialog!==this.$dialog[0]).concat(this.$dialog[0]).forEach((dialog,idx)=>{dialog.style.zIndex=501+idx})}return this},close:function(event){if(event){event.preventDefault()}this.setModality(false);this.$dialog.remove();return this},display:function(){if(this.scriptName){const $titleBar=this.$dialog.find(".morebits-dialog-title");$titleBar.find(".morebits-dialog-scriptname").remove();$titleBar.prepend($("<span>").addClass("morebits-dialog-scriptname").text(this.scriptName+" · "))}if(this.$dialog.find(".morebits-scrollbox").length){this.$dialog.find(".quickform").addClass("has-scrollbox")}this.$dialog.appendTo(document.body);const $firstField=this.$dialog.find("input, select, textarea").first();const $firstFieldOrDialog=$firstField.length?$firstField:this.$dialog;$firstFieldOrDialog.trigger("focus");return this},setTitle:function(title){this.$dialog.find(".morebits-dialog-title").text(title);return this},setScriptName:function(name){this.scriptName=name;return this},setWidth:function(width){this.$dialog.css("width",width+"px");return this},setHeight:function(height){this.$dialog.css("height","auto");this.$dialog.css("max-height",height+20+"px");return this},setContent:function(content){this.purgeContent();this.addContent(content);return this},addContent:function(content){this.content.appendChild(content);$(this.content).find('input[type="submit"], button[type="submit"]').get().forEach(node=>{node.style.display="none";const button=document.createElement("button");button.textContent=node.getAttribute("value")||node.textContent||"送信";button.className=node.className||"submitButtonProxy";button.addEventListener("click",()=>node.click(),false);this.buttons.push(button)});this.$dialog.find(".morebits-dialog-buttons").empty().append(...this.buttons).attr("data-empty",this.buttons.length?null:"data-empty");return this},purgeContent:function(){this.buttons=[];this.$dialog.find(".morebits-dialog-buttons").empty();$(this.content).empty();return this},addFooterLink:function(text,wikiPage){const $footerlinks=this.$dialog.find(".morebits-dialog-footerlinks");if(this.hasFooterLinks){const bullet=document.createElement("span");bullet.textContent=" • ";$footerlinks.append(bullet)}const link=document.createElement("a");link.setAttribute("href",mw.util.getUrl(wikiPage));link.setAttribute("title",wikiPage);link.setAttribute("target","_blank");link.textContent=text;$footerlinks.append(link);this.hasFooterLinks=true;return this},setModality:function(modal){if(modal){$("<div>").addClass("morebits-dialog-overlay").appendTo(document.body);this.$dialog.addClass("morebits-dialog-modal")}else{$(".morebits-dialog-overlay").remove();this.$dialog.removeClass("morebits-dialog-modal")}return this}};Morebits.SimpleWindow.setButtonsEnabled=function(enabled){$(".morebits-dialog-buttons button").prop("disabled",!enabled)};Morebits.batchOperation=Morebits.BatchOperation;Morebits.date=Morebits.Date;Morebits.quickForm=Morebits.QuickForm;Morebits.quickForm.element=Morebits.QuickForm.Element;Morebits.simpleWindow=Morebits.SimpleWindow;Morebits.status=Morebits.Status;Morebits.taskManager=Morebits.TaskManager;Morebits.unbinder=Morebits.Unbinder;Morebits.userspaceLogger=Morebits.UserspaceLogger;Morebits.wiki.api=Morebits.wiki.Api;Morebits.wiki.page=Morebits.wiki.Page;Morebits.wiki.preview=Morebits.wiki.Preview;Morebits.wikitext.page=Morebits.wikitext.Page})();if(typeof arguments==="undefined"){window.SimpleWindow=Morebits.SimpleWindow;window.QuickForm=Morebits.QuickForm;window.Wikipedia=Morebits.wiki;window.Status=Morebits.Status}
// </nowiki>
mobb50nje54rqbhi9q57h9q6bzt5ye8
User:Syunsyunminmin/morebits/morebits.css
2
179392
769457
2026-10-21T13:57:46Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 3680108:
769457
css
text/css
/*
Twinkle: adapted for Japanese Wikipedia; upstream authors and Japanese adaptations are credited in UPSTREAM.md.
Upstream source: https://github.com/wikimedia-gadgets/twinkle/blob/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/src/morebits.css
Upstream author history: https://github.com/wikimedia-gadgets/twinkle/commits/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/src/morebits.css
License: CC BY-SA 3.0 Unported. See https://github.com/wikimedia-gadgets/twinkle/blob/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/LICENSE.txt
Japanese adaptation: Syunsyunminmin; includes later synchronization changes.
*/
/**
* morebits.css
* ===========
* Styles to support morebits.js.
*
* The morebits library is maintained by the maintainers of Twinkle.
* For queries, suggestions, help, etc., head to [[WT:TW]].
* The latest development source is available at [https://github.com/wikimedia-gadgets/twinkle/blob/master/morebits.css].
*/
/* Define CSS variables */
html {
--morebits-color-status-status: #4682b4;
--morebits-color-legend: #31628f;
--morebits-color-tooltip: #0000cd;
--morebits-color-info: #228b22;
--morebits-color-warning: #ff4500;
--morebits-color-titlebar-links: #3062ad;
--morebits-bgcolor-dialog: #f0f8ff;
--morebits-bgcolor-titlebar: #bccadf;
}
@media screen {
html.skin-theme-clientpref-night {
--morebits-color-status-status: #a6c4de;
--morebits-color-legend: #6b9dcc;
--morebits-color-tooltip: #7a7aff;
--morebits-color-info: #6bdb6b;
--morebits-color-warning: #ff6d38;
--morebits-color-titlebar-links: #8baddf;
--morebits-bgcolor-dialog: #141c26;
--morebits-bgcolor-titlebar: #1c2a52;
}
}
/* stylelint-disable-next-line plugin/no-unsupported-browser-features */
@media screen and (prefers-color-scheme: dark) {
html.skin-theme-clientpref-os {
--morebits-color-status-status: #a6c4de;
--morebits-color-legend: #6b9dcc;
--morebits-color-tooltip: #7a7aff;
--morebits-color-info: #6bdb6b;
--morebits-color-warning: #ff6d38;
--morebits-color-titlebar-links: #8baddf;
--morebits-bgcolor-dialog: #141c26;
--morebits-bgcolor-titlebar: #1c2a52;
}
}
/* Morebits.Status */
.morebits_status_status {
color: var(--morebits-color-status-status);
}
.morebits_status_info {
color: var(--morebits-color-info);
}
.morebits_status_warn {
color: var(--morebits-color-warning);
}
.morebits_status_error {
color: var(--morebits-color-warning);
font-weight: bold;
}
/* Morebits.QuickForm */
form.quickform {
box-sizing: border-box;
width: 98%;
height: 100%;
min-height: 0;
margin: 0 auto;
padding: 0.5em;
color: var(--color-base, #202122);
}
/* use flex layout for quickform to make the scrollbox resize with
the dialog, and to keep it from obscuring elements below it */
form.quickform.has-scrollbox {
display: flex;
flex-direction: column;
}
form.quickform * {
font-family: sans-serif;
}
form.quickform fieldset {
margin: 0.4em 0 1em;
}
form.quickform legend {
color: var(--morebits-color-legend);
font-weight: bold;
}
form.quickform input[type=text],
form.quickform select {
min-width: 15em;
font-size: 110%;
}
form.quickform select {
/* stylelint-disable-next-line color-named */ /* FIXME */
border: 1px solid gray;
margin-left: 0.2em;
}
form.quickform input[type=checkbox],
form.quickform input[type=radio] {
height: 13px;
margin-top: 2px;
margin-right: 2px;
margin-bottom: 2px;
padding: 0;
width: 13px;
vertical-align: top;
}
form.quickform div {
/* stylelint-disable-next-line declaration-property-unit-disallowed-list */ /* FIXME */
line-height: 18px;
}
form.quickform h5 {
margin: 0.5em 0 0;
padding: 0.3em 0.2em 0.2em;
font-size: 108%; /* 100% is 12px => 108% is 12.96px */
}
/* only give the top border to headers with something above them */
form.quickform div + h5,
form.quickform h5 + h5,
form.quickform div + div > h5 {
border-top: 1px solid #88a;
}
form.quickform textarea {
width: 100%;
height: 4em;
font-size: 150%;
}
form.quickform input:disabled + label {
color: var(--color-disabled, #72777d);
}
form.quickform span.quickformDescription {
font-style: italic;
}
form.quickform span.quickformDescription code {
font-style: normal;
font-family: monospace;
}
form.quickform .quickformSubgroup {
margin-bottom: 0.5em;
margin-left: 3em;
}
/* The tooltip button and the content itself */
form.quickform .morebits-tooltipButton {
color: var(--morebits-color-tooltip);
font-weight: bold;
cursor: help;
padding: 0.3em;
}
.morebits-ui-tooltip {
position: absolute;
z-index: 1005;
padding: 4px 6px 4px 6px;
max-width: 300px;
transition: opacity 600ms ease;
opacity: 0;
visibility: hidden;
pointer-events: none;
/* stylelint-disable-next-line declaration-property-unit-disallowed-list */ /* FIXME */
font-size: 13px;
background: var(--background-color-neutral-subtle, #f8f9fa);
border: 2px solid var(--border-color-base, #a2ab91);
box-shadow: 0 0 5px #a2ab91;
}
.morebits-ui-tooltip.visible {
opacity: 1;
visibility: visible;
}
/* Scrollbox styles, for use within Morebits.SimpleWindow */
div.morebits-scrollbox {
background: var(--background-color-base, #fff);
/* stylelint-disable-next-line color-named */ /* FIXME */
border: 1px solid gray;
margin-bottom: 0.6em;
margin-top: 0.6em;
height: auto;
flex-grow: 1;
min-height: 0;
overflow: auto;
padding: 6px 6px 0;
}
div.morebits-scrollbox > h5:first-child {
border: 0;
margin-top: 0;
padding-top: 0;
}
div.morebits-scrollbox > :last-child {
margin-bottom: 6px;
}
/* Previewbox */
div.morebits-previewbox {
border: 2px inset;
margin: 0.4em auto 0.2em;
padding: 0.2em 0.4em;
}
div.morebits-usertext {
border: 1px solid #a2a9b1;
background-color: var(--background-color-neutral-subtle, #f8f9fa);
padding: 5px;
font-size: 95%;
}
/* Morebits.SimpleWindow */
.morebits-dialog {
padding: 0.2em;
border: 1px #666 solid;
font-family: sans-serif;
background-color: var(--morebits-bgcolor-dialog);
display: flex;
flex-direction: column;
position: absolute;
overflow: hidden;
min-width: 150px;
min-height: 150px;
/* z-index will be adjusted upwards by JS if there are multiple dialogs open and a dialog gets clicked on. */
z-index: 500;
/* top, left, width, height, and max-height are set via JS. */
}
.morebits-dialog-resizer {
position: absolute;
right: -5px;
bottom: -5px;
height: 20px;
width: 20px;
cursor: se-resize;
background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB2aWV3Qm94PSIwIDAgMTYgMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+Cgk8ZyBzdHJva2U9IiM3MmE3Y2UiPgoJCTxwYXRoIGQ9Im0wIDEwTDEwIDAiLz4KCQk8cGF0aCBkPSJtNCAxMEwxMCA0Ii8+CgkJPHBhdGggZD0ibTggMTBMMTAgOCIvPgoJPC9nPgo8L3N2Zz4K);
}
/* px translations in comments are w.r.t standard browser settings,
in other settings, the sizes would be scaled accordingly */
.skin-vector .morebits-dialog {
font-size: 75%; /* 100% is 16px => 75% is 12px */
}
.skin-timeless .morebits-dialog {
font-size: 79%; /* 100% is 15.2px => 79% is 12.008px */
}
.skin-monobook .morebits-dialog,
.skin-modern .morebits-dialog {
font-size: 120%; /* 100% is 10px => 120% is 12px */
}
.morebits-dialog .morebits-dialog-titlebar {
height: 1em;
min-height: 1em;
background-color: var(--morebits-bgcolor-titlebar);
font: bold 108% sans-serif; /* 100% is 12px (from above) => 108% is 12.96px */
overflow: hidden;
padding: 0.4em 0.3em 0.5em;
white-space: nowrap;
display: flex; /* needed so that align-items can be used */
align-items: center; /* to vertically center the close button */
cursor: move;
position: relative; /* so that the close button can be positioned relative to titlebar */
}
.morebits-dialog-scriptname {
font-weight: normal;
}
.morebits-dialog .morebits-dialog-close {
position: absolute;
right: 0;
background-color: transparent;
border: 0;
font-size: 1.2em;
font-weight: bold;
padding: 5px 10px;
cursor: pointer;
border-radius: 50%;
transition: background-color 0.2s, color 0.2s;
}
.morebits-dialog .morebits-dialog-close:hover {
background-color: #eee;
color: #000;
}
.morebits-dialog .morebits-dialog-close span {
margin: 0.33em;
}
.morebits-dialog .morebits-dialog-content {
padding: 0;
display: flex;
flex-direction: column;
box-sizing: border-box;
height: 100%;
flex-grow: 1;
overflow-y: auto;
min-height: 0;
}
.morebits-dialog .morebits-dialog-buttonpane {
background-color: var(--morebits-bgcolor-titlebar);
margin: auto 0 0;
padding: 0.3em 1.4em 0.5em 1.2em;
}
.morebits-dialog .morebits-dialog-buttonpane button {
margin-top: 0.2em;
}
.morebits-dialog-buttons {
font-size: 108%; /* 100% is 12px => 108% is 12.96px */
}
.morebits-dialog-footerlinks {
font-size: 97%; /* 100% is 12px (from above) => 97% is 11.64px */
float: right;
margin: 0.7em 0.4em 0 0;
}
.morebits-dialog .morebits-dialog-footerlinks a {
color: var(--morebits-color-titlebar-links);
}
.morebits-dialog-buttons[data-empty] + .morebits-dialog-footerlinks {
margin: 0.1em 0.4em -0.2em 0;
}
.morebits-dialog-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5); /* Semi-transparent black */
z-index: 700; /* Higher than morebits-dialog but lesser than morebits-dialog-modal */
}
.morebits-dialog.morebits-dialog-modal {
z-index: 701;
}
/* Override select2 silliness */
.select2-morebits,
.select2-morebits .select2-results,
.select2-morebits .select2-results__option[aria-selected=true],
.select2-morebits .select2-selection__rendered,
.select2-morebits .select2-selection--multiple,
.select2-morebits .select2-selection__choice {
background: var(--background-color-base, #fff) !important;
color: inherit !important;
}
i6a77bd70mlpqqioaxege3kvmxtg762
769524
769457
2026-10-21T23:59:58Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 1406828: rm cmt
769524
css
text/css
/**
* morebits.css
* ===========
* Styles to support morebits.js.
*
* The morebits library is maintained by the maintainers of Twinkle.
* For queries, suggestions, help, etc., head to [[WT:TW]].
* The latest development source is available at [https://github.com/wikimedia-gadgets/twinkle/blob/master/morebits.css].
*/
/* Define CSS variables */
html {
--morebits-color-status-status: #4682b4;
--morebits-color-legend: #31628f;
--morebits-color-tooltip: #0000cd;
--morebits-color-info: #228b22;
--morebits-color-warning: #ff4500;
--morebits-color-titlebar-links: #3062ad;
--morebits-bgcolor-dialog: #f0f8ff;
--morebits-bgcolor-titlebar: #bccadf;
}
@media screen {
html.skin-theme-clientpref-night {
--morebits-color-status-status: #a6c4de;
--morebits-color-legend: #6b9dcc;
--morebits-color-tooltip: #7a7aff;
--morebits-color-info: #6bdb6b;
--morebits-color-warning: #ff6d38;
--morebits-color-titlebar-links: #8baddf;
--morebits-bgcolor-dialog: #141c26;
--morebits-bgcolor-titlebar: #1c2a52;
}
}
/* stylelint-disable-next-line plugin/no-unsupported-browser-features */
@media screen and (prefers-color-scheme: dark) {
html.skin-theme-clientpref-os {
--morebits-color-status-status: #a6c4de;
--morebits-color-legend: #6b9dcc;
--morebits-color-tooltip: #7a7aff;
--morebits-color-info: #6bdb6b;
--morebits-color-warning: #ff6d38;
--morebits-color-titlebar-links: #8baddf;
--morebits-bgcolor-dialog: #141c26;
--morebits-bgcolor-titlebar: #1c2a52;
}
}
/* Morebits.Status */
.morebits_status_status {
color: var(--morebits-color-status-status);
}
.morebits_status_info {
color: var(--morebits-color-info);
}
.morebits_status_warn {
color: var(--morebits-color-warning);
}
.morebits_status_error {
color: var(--morebits-color-warning);
font-weight: bold;
}
/* Morebits.QuickForm */
form.quickform {
box-sizing: border-box;
width: 98%;
height: 100%;
min-height: 0;
margin: 0 auto;
padding: 0.5em;
color: var(--color-base, #202122);
}
/* use flex layout for quickform to make the scrollbox resize with
the dialog, and to keep it from obscuring elements below it */
form.quickform.has-scrollbox {
display: flex;
flex-direction: column;
}
form.quickform * {
font-family: sans-serif;
}
form.quickform fieldset {
margin: 0.4em 0 1em;
}
form.quickform legend {
color: var(--morebits-color-legend);
font-weight: bold;
}
form.quickform input[type=text],
form.quickform select {
min-width: 15em;
font-size: 110%;
}
form.quickform select {
/* stylelint-disable-next-line color-named */ /* FIXME */
border: 1px solid gray;
margin-left: 0.2em;
}
form.quickform input[type=checkbox],
form.quickform input[type=radio] {
height: 13px;
margin-top: 2px;
margin-right: 2px;
margin-bottom: 2px;
padding: 0;
width: 13px;
vertical-align: top;
}
form.quickform div {
/* stylelint-disable-next-line declaration-property-unit-disallowed-list */ /* FIXME */
line-height: 18px;
}
form.quickform h5 {
margin: 0.5em 0 0;
padding: 0.3em 0.2em 0.2em;
font-size: 108%; /* 100% is 12px => 108% is 12.96px */
}
/* only give the top border to headers with something above them */
form.quickform div + h5,
form.quickform h5 + h5,
form.quickform div + div > h5 {
border-top: 1px solid #88a;
}
form.quickform textarea {
width: 100%;
height: 4em;
font-size: 150%;
}
form.quickform input:disabled + label {
color: var(--color-disabled, #72777d);
}
form.quickform span.quickformDescription {
font-style: italic;
}
form.quickform span.quickformDescription code {
font-style: normal;
font-family: monospace;
}
form.quickform .quickformSubgroup {
margin-bottom: 0.5em;
margin-left: 3em;
}
/* The tooltip button and the content itself */
form.quickform .morebits-tooltipButton {
color: var(--morebits-color-tooltip);
font-weight: bold;
cursor: help;
padding: 0.3em;
}
.morebits-ui-tooltip {
position: absolute;
z-index: 1005;
padding: 4px 6px 4px 6px;
max-width: 300px;
transition: opacity 600ms ease;
opacity: 0;
visibility: hidden;
pointer-events: none;
/* stylelint-disable-next-line declaration-property-unit-disallowed-list */ /* FIXME */
font-size: 13px;
background: var(--background-color-neutral-subtle, #f8f9fa);
border: 2px solid var(--border-color-base, #a2ab91);
box-shadow: 0 0 5px #a2ab91;
}
.morebits-ui-tooltip.visible {
opacity: 1;
visibility: visible;
}
/* Scrollbox styles, for use within Morebits.SimpleWindow */
div.morebits-scrollbox {
background: var(--background-color-base, #fff);
/* stylelint-disable-next-line color-named */ /* FIXME */
border: 1px solid gray;
margin-bottom: 0.6em;
margin-top: 0.6em;
height: auto;
flex-grow: 1;
min-height: 0;
overflow: auto;
padding: 6px 6px 0;
}
div.morebits-scrollbox > h5:first-child {
border: 0;
margin-top: 0;
padding-top: 0;
}
div.morebits-scrollbox > :last-child {
margin-bottom: 6px;
}
/* Previewbox */
div.morebits-previewbox {
border: 2px inset;
margin: 0.4em auto 0.2em;
padding: 0.2em 0.4em;
}
div.morebits-usertext {
border: 1px solid #a2a9b1;
background-color: var(--background-color-neutral-subtle, #f8f9fa);
padding: 5px;
font-size: 95%;
}
/* Morebits.SimpleWindow */
.morebits-dialog {
padding: 0.2em;
border: 1px #666 solid;
font-family: sans-serif;
background-color: var(--morebits-bgcolor-dialog);
display: flex;
flex-direction: column;
position: absolute;
overflow: hidden;
min-width: 150px;
min-height: 150px;
/* z-index will be adjusted upwards by JS if there are multiple dialogs open and a dialog gets clicked on. */
z-index: 500;
/* top, left, width, height, and max-height are set via JS. */
}
.morebits-dialog-resizer {
position: absolute;
right: -5px;
bottom: -5px;
height: 20px;
width: 20px;
cursor: se-resize;
background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB2aWV3Qm94PSIwIDAgMTYgMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+Cgk8ZyBzdHJva2U9IiM3MmE3Y2UiPgoJCTxwYXRoIGQ9Im0wIDEwTDEwIDAiLz4KCQk8cGF0aCBkPSJtNCAxMEwxMCA0Ii8+CgkJPHBhdGggZD0ibTggMTBMMTAgOCIvPgoJPC9nPgo8L3N2Zz4K);
}
/* px translations in comments are w.r.t standard browser settings,
in other settings, the sizes would be scaled accordingly */
.skin-vector .morebits-dialog {
font-size: 75%; /* 100% is 16px => 75% is 12px */
}
.skin-timeless .morebits-dialog {
font-size: 79%; /* 100% is 15.2px => 79% is 12.008px */
}
.skin-monobook .morebits-dialog,
.skin-modern .morebits-dialog {
font-size: 120%; /* 100% is 10px => 120% is 12px */
}
.morebits-dialog .morebits-dialog-titlebar {
height: 1em;
min-height: 1em;
background-color: var(--morebits-bgcolor-titlebar);
font: bold 108% sans-serif; /* 100% is 12px (from above) => 108% is 12.96px */
overflow: hidden;
padding: 0.4em 0.3em 0.5em;
white-space: nowrap;
display: flex; /* needed so that align-items can be used */
align-items: center; /* to vertically center the close button */
cursor: move;
position: relative; /* so that the close button can be positioned relative to titlebar */
}
.morebits-dialog-scriptname {
font-weight: normal;
}
.morebits-dialog .morebits-dialog-close {
position: absolute;
right: 0;
background-color: transparent;
border: 0;
font-size: 1.2em;
font-weight: bold;
padding: 5px 10px;
cursor: pointer;
border-radius: 50%;
transition: background-color 0.2s, color 0.2s;
}
.morebits-dialog .morebits-dialog-close:hover {
background-color: #eee;
color: #000;
}
.morebits-dialog .morebits-dialog-close span {
margin: 0.33em;
}
.morebits-dialog .morebits-dialog-content {
padding: 0;
display: flex;
flex-direction: column;
box-sizing: border-box;
height: 100%;
flex-grow: 1;
overflow-y: auto;
min-height: 0;
}
.morebits-dialog .morebits-dialog-buttonpane {
background-color: var(--morebits-bgcolor-titlebar);
margin: auto 0 0;
padding: 0.3em 1.4em 0.5em 1.2em;
}
.morebits-dialog .morebits-dialog-buttonpane button {
margin-top: 0.2em;
}
.morebits-dialog-buttons {
font-size: 108%; /* 100% is 12px => 108% is 12.96px */
}
.morebits-dialog-footerlinks {
font-size: 97%; /* 100% is 12px (from above) => 97% is 11.64px */
float: right;
margin: 0.7em 0.4em 0 0;
}
.morebits-dialog .morebits-dialog-footerlinks a {
color: var(--morebits-color-titlebar-links);
}
.morebits-dialog-buttons[data-empty] + .morebits-dialog-footerlinks {
margin: 0.1em 0.4em -0.2em 0;
}
.morebits-dialog-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5); /* Semi-transparent black */
z-index: 700; /* Higher than morebits-dialog but lesser than morebits-dialog-modal */
}
.morebits-dialog.morebits-dialog-modal {
z-index: 701;
}
/* Override select2 silliness */
.select2-morebits,
.select2-morebits .select2-results,
.select2-morebits .select2-results__option[aria-selected=true],
.select2-morebits .select2-selection__rendered,
.select2-morebits .select2-selection--multiple,
.select2-morebits .select2-selection__choice {
background: var(--background-color-base, #fff) !important;
color: inherit !important;
}
o9jmp275nriv5rhor11wh1ba5n0wo8a
769562
769524
2026-10-22T02:17:36Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at d9cd3461baca43ac99ac2c24fb76a3aae6ddada9: minify
769562
css
text/css
/**
* morebits.css
* ===========
* Styles to support morebits.js.
*
* The morebits library is maintained by the maintainers of Twinkle.
* For queries, suggestions, help, etc., head to [[WT:TW]].
* The latest development source is available at [https://github.com/wikimedia-gadgets/twinkle/blob/master/morebits.css].
*//* Define CSS variables */html{--morebits-color-status-status:#4682b4;--morebits-color-legend:#31628f;--morebits-color-tooltip:#0000cd;--morebits-color-info:#228b22;--morebits-color-warning:#ff4500;--morebits-color-titlebar-links:#3062ad;--morebits-bgcolor-dialog:#f0f8ff;--morebits-bgcolor-titlebar:#bccadf}@media screen{html.skin-theme-clientpref-night{--morebits-color-status-status:#a6c4de;--morebits-color-legend:#6b9dcc;--morebits-color-tooltip:#7a7aff;--morebits-color-info:#6bdb6b;--morebits-color-warning:#ff6d38;--morebits-color-titlebar-links:#8baddf;--morebits-bgcolor-dialog:#141c26;--morebits-bgcolor-titlebar:#1c2a52}}/* stylelint-disable-next-line plugin/no-unsupported-browser-features */@media screen and (prefers-color-scheme: dark){html.skin-theme-clientpref-os{--morebits-color-status-status:#a6c4de;--morebits-color-legend:#6b9dcc;--morebits-color-tooltip:#7a7aff;--morebits-color-info:#6bdb6b;--morebits-color-warning:#ff6d38;--morebits-color-titlebar-links:#8baddf;--morebits-bgcolor-dialog:#141c26;--morebits-bgcolor-titlebar:#1c2a52}}/* Morebits.Status */.morebits_status_status{color:var(--morebits-color-status-status)}.morebits_status_info{color:var(--morebits-color-info)}.morebits_status_warn{color:var(--morebits-color-warning)}.morebits_status_error{color:var(--morebits-color-warning);font-weight:bold}/* Morebits.QuickForm */form.quickform{box-sizing:border-box;width:98%;height:100%;min-height:0;margin:0 auto;padding:0.5em;color:var(--color-base, #202122)}/* use flex layout for quickform to make the scrollbox resize with
the dialog, and to keep it from obscuring elements below it */form.quickform.has-scrollbox{display:flex;flex-direction:column}form.quickform *{font-family:sans-serif}form.quickform fieldset{margin:0.4em 0 1em}form.quickform legend{color:var(--morebits-color-legend);font-weight:bold}form.quickform input[type=text],form.quickform select{min-width:15em;font-size:110%}form.quickform select{/* stylelint-disable-next-line color-named *//* FIXME */border:1px solid gray;margin-left:0.2em}form.quickform input[type=checkbox],form.quickform input[type=radio]{height:13px;margin-top:2px;margin-right:2px;margin-bottom:2px;padding:0;width:13px;vertical-align:top}form.quickform div{/* stylelint-disable-next-line declaration-property-unit-disallowed-list *//* FIXME */line-height:18px}form.quickform h5{margin:0.5em 0 0;padding:0.3em 0.2em 0.2em;font-size:108%/* 100% is 12px => 108% is 12.96px */}/* only give the top border to headers with something above them */form.quickform div + h5,form.quickform h5 + h5,form.quickform div + div > h5{border-top:1px solid #88a}form.quickform textarea{width:100%;height:4em;font-size:150%}form.quickform input:disabled + label{color:var(--color-disabled, #72777d)}form.quickform span.quickformDescription{font-style:italic}form.quickform span.quickformDescription code{font-style:normal;font-family:monospace}form.quickform .quickformSubgroup{margin-bottom:0.5em;margin-left:3em}/* The tooltip button and the content itself */form.quickform .morebits-tooltipButton{color:var(--morebits-color-tooltip);font-weight:bold;cursor:help;padding:0.3em}.morebits-ui-tooltip{position:absolute;z-index:1005;padding:4px 6px 4px 6px;max-width:300px;transition:opacity 600ms ease;opacity:0;visibility:hidden;pointer-events:none;/* stylelint-disable-next-line declaration-property-unit-disallowed-list *//* FIXME */font-size:13px;background:var(--background-color-neutral-subtle, #f8f9fa);border:2px solid var(--border-color-base, #a2ab91);box-shadow:0 0 5px #a2ab91}.morebits-ui-tooltip.visible{opacity:1;visibility:visible}/* Scrollbox styles, for use within Morebits.SimpleWindow */div.morebits-scrollbox{background:var(--background-color-base, #fff);/* stylelint-disable-next-line color-named *//* FIXME */border:1px solid gray;margin-bottom:0.6em;margin-top:0.6em;height:auto;flex-grow:1;min-height:0;overflow:auto;padding:6px 6px 0}div.morebits-scrollbox > h5:first-child{border:0;margin-top:0;padding-top:0}div.morebits-scrollbox > :last-child{margin-bottom:6px}/* Previewbox */div.morebits-previewbox{border:2px inset;margin:0.4em auto 0.2em;padding:0.2em 0.4em}div.morebits-usertext{border:1px solid #a2a9b1;background-color:var(--background-color-neutral-subtle, #f8f9fa);padding:5px;font-size:95%}/* Morebits.SimpleWindow */.morebits-dialog{padding:0.2em;border:1px #666 solid;font-family:sans-serif;background-color:var(--morebits-bgcolor-dialog);display:flex;flex-direction:column;position:absolute;overflow:hidden;min-width:150px;min-height:150px;/* z-index will be adjusted upwards by JS if there are multiple dialogs open and a dialog gets clicked on. */z-index:500/* top, left, width, height, and max-height are set via JS. */}.morebits-dialog-resizer{position:absolute;right:-5px;bottom:-5px;height:20px;width:20px;cursor:se-resize;background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB2aWV3Qm94PSIwIDAgMTYgMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+Cgk8ZyBzdHJva2U9IiM3MmE3Y2UiPgoJCTxwYXRoIGQ9Im0wIDEwTDEwIDAiLz4KCQk8cGF0aCBkPSJtNCAxMEwxMCA0Ii8+CgkJPHBhdGggZD0ibTggMTBMMTAgOCIvPgoJPC9nPgo8L3N2Zz4K)}/* px translations in comments are w.r.t standard browser settings,
in other settings, the sizes would be scaled accordingly */.skin-vector .morebits-dialog{font-size:75%/* 100% is 16px => 75% is 12px */}.skin-timeless .morebits-dialog{font-size:79%/* 100% is 15.2px => 79% is 12.008px */}.skin-monobook .morebits-dialog,.skin-modern .morebits-dialog{font-size:120%/* 100% is 10px => 120% is 12px */}.morebits-dialog .morebits-dialog-titlebar{height:1em;min-height:1em;background-color:var(--morebits-bgcolor-titlebar);font:bold 108% sans-serif;/* 100% is 12px (from above) => 108% is 12.96px */overflow:hidden;padding:0.4em 0.3em 0.5em;white-space:nowrap;display:flex;/* needed so that align-items can be used */align-items:center;/* to vertically center the close button */cursor:move;position:relative/* so that the close button can be positioned relative to titlebar */}.morebits-dialog-scriptname{font-weight:normal}.morebits-dialog .morebits-dialog-close{position:absolute;right:0;background-color:transparent;border:0;font-size:1.2em;font-weight:bold;padding:5px 10px;cursor:pointer;border-radius:50%;transition:background-color 0.2s,color 0.2s}.morebits-dialog .morebits-dialog-close:hover{background-color:#eee;color:#000}.morebits-dialog .morebits-dialog-close span{margin:0.33em}.morebits-dialog .morebits-dialog-content{padding:0;display:flex;flex-direction:column;box-sizing:border-box;height:100%;flex-grow:1;overflow-y:auto;min-height:0}.morebits-dialog .morebits-dialog-buttonpane{background-color:var(--morebits-bgcolor-titlebar);margin:auto 0 0;padding:0.3em 1.4em 0.5em 1.2em}.morebits-dialog .morebits-dialog-buttonpane button{margin-top:0.2em}.morebits-dialog-buttons{font-size:108%/* 100% is 12px => 108% is 12.96px */}.morebits-dialog-footerlinks{font-size:97%;/* 100% is 12px (from above) => 97% is 11.64px */float:right;margin:0.7em 0.4em 0 0}.morebits-dialog .morebits-dialog-footerlinks a{color:var(--morebits-color-titlebar-links)}.morebits-dialog-buttons[data-empty] + .morebits-dialog-footerlinks{margin:0.1em 0.4em -0.2em 0}.morebits-dialog-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background-color:rgba(0, 0, 0, 0.5);/* Semi-transparent black */z-index:700/* Higher than morebits-dialog but lesser than morebits-dialog-modal */}.morebits-dialog.morebits-dialog-modal{z-index:701}/* Override select2 silliness */.select2-morebits,.select2-morebits .select2-results,.select2-morebits .select2-results__option[aria-selected=true],.select2-morebits .select2-selection__rendered,.select2-morebits .select2-selection--multiple,.select2-morebits .select2-selection__choice{background:var(--background-color-base, #fff) !important;color:inherit !important}
jddmemd205au2guo6na1qgtlhtoq0wr
User:Syunsyunminmin/select2/select2.min.js
2
179393
769458
2026-10-21T13:57:52Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 3680108:
769458
javascript
text/javascript
/*
Twinkle: adapted for Japanese Wikipedia; upstream authors and Japanese adaptations are credited in UPSTREAM.md.
Upstream source: https://github.com/wikimedia-gadgets/twinkle/blob/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/lib/select2.min.js
Upstream author history: https://github.com/wikimedia-gadgets/twinkle/commits/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/lib/select2.min.js
License: MIT; see the original license notice below. See https://github.com/wikimedia-gadgets/twinkle/blob/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/lib/select2-LICENSE.md
Japanese adaptation: Syunsyunminmin; includes later synchronization changes.
*/
// <nowiki>
/*! Select2 4.0.12 | https://github.com/select2/select2/blob/master/LICENSE.md */
!function(n){"function"==typeof define&&define.amd?define(["jquery"],n):/*"object"==typeof module&&module.exports?module.exports=function(e,t){return void 0===t&&(t="undefined"!=typeof window?require("jquery"):require("jquery")(e)),n(t),t}:*/n(jQuery)}(function(u){var e=function(){if(u&&u.fn&&u.fn.select2&&u.fn.select2.amd)var e=u.fn.select2.amd;var t,n,r,h,o,s,f,g,m,v,y,_,i,a,w;function b(e,t){return i.call(e,t)}function l(e,t){var n,r,i,o,s,a,l,c,u,d,p,h=t&&t.split("/"),f=y.map,g=f&&f["*"]||{};if(e){for(s=(e=e.split("/")).length-1,y.nodeIdCompat&&w.test(e[s])&&(e[s]=e[s].replace(w,"")),"."===e[0].charAt(0)&&h&&(e=h.slice(0,h.length-1).concat(e)),u=0;u<e.length;u++)if("."===(p=e[u]))e.splice(u,1),u-=1;else if(".."===p){if(0===u||1===u&&".."===e[2]||".."===e[u-1])continue;0<u&&(e.splice(u-1,2),u-=2)}e=e.join("/")}if((h||g)&&f){for(u=(n=e.split("/")).length;0<u;u-=1){if(r=n.slice(0,u).join("/"),h)for(d=h.length;0<d;d-=1)if(i=(i=f[h.slice(0,d).join("/")])&&i[r]){o=i,a=u;break}if(o)break;!l&&g&&g[r]&&(l=g[r],c=u)}!o&&l&&(o=l,a=c),o&&(n.splice(0,a,o),e=n.join("/"))}return e}function A(t,n){return function(){var e=a.call(arguments,0);return"string"!=typeof e[0]&&1===e.length&&e.push(null),s.apply(h,e.concat([t,n]))}}function x(t){return function(e){m[t]=e}}function D(e){if(b(v,e)){var t=v[e];delete v[e],_[e]=!0,o.apply(h,t)}if(!b(m,e)&&!b(_,e))throw new Error("No "+e);return m[e]}function c(e){var t,n=e?e.indexOf("!"):-1;return-1<n&&(t=e.substring(0,n),e=e.substring(n+1,e.length)),[t,e]}function S(e){return e?c(e):[]}return e&&e.requirejs||(e?n=e:e={},m={},v={},y={},_={},i=Object.prototype.hasOwnProperty,a=[].slice,w=/\.js$/,f=function(e,t){var n,r=c(e),i=r[0],o=t[1];return e=r[1],i&&(n=D(i=l(i,o))),i?e=n&&n.normalize?n.normalize(e,function(t){return function(e){return l(e,t)}}(o)):l(e,o):(i=(r=c(e=l(e,o)))[0],e=r[1],i&&(n=D(i))),{f:i?i+"!"+e:e,n:e,pr:i,p:n}},g={require:function(e){return A(e)},exports:function(e){var t=m[e];return void 0!==t?t:m[e]={}},module:function(e){return{id:e,uri:"",exports:m[e],config:function(e){return function(){return y&&y.config&&y.config[e]||{}}}(e)}}},o=function(e,t,n,r){var i,o,s,a,l,c,u,d=[],p=typeof n;if(c=S(r=r||e),"undefined"==p||"function"==p){for(t=!t.length&&n.length?["require","exports","module"]:t,l=0;l<t.length;l+=1)if("require"===(o=(a=f(t[l],c)).f))d[l]=g.require(e);else if("exports"===o)d[l]=g.exports(e),u=!0;else if("module"===o)i=d[l]=g.module(e);else if(b(m,o)||b(v,o)||b(_,o))d[l]=D(o);else{if(!a.p)throw new Error(e+" missing "+o);a.p.load(a.n,A(r,!0),x(o),{}),d[l]=m[o]}s=n?n.apply(m[e],d):void 0,e&&(i&&i.exports!==h&&i.exports!==m[e]?m[e]=i.exports:s===h&&u||(m[e]=s))}else e&&(m[e]=n)},t=n=s=function(e,t,n,r,i){if("string"==typeof e)return g[e]?g[e](t):D(f(e,S(t)).f);if(!e.splice){if((y=e).deps&&s(y.deps,y.callback),!t)return;t.splice?(e=t,t=n,n=null):e=h}return t=t||function(){},"function"==typeof n&&(n=r,r=i),r?o(h,e,t,n):setTimeout(function(){o(h,e,t,n)},4),s},s.config=function(e){return s(e)},t._defined=m,(r=function(e,t,n){if("string"!=typeof e)throw new Error("See almond README: incorrect module build, no module name");t.splice||(n=t,t=[]),b(m,e)||b(v,e)||(v[e]=[e,t,n])}).amd={jQuery:!0},e.requirejs=t,e.require=n,e.define=r),e.define("almond",function(){}),e.define("jquery",[],function(){var e=u||$;return null==e&&console&&console.error&&console.error("Select2: An instance of jQuery or a jQuery-compatible library was not found. Make sure that you are including jQuery before Select2 on your web page."),e}),e.define("select2/utils",["jquery"],function(o){var i={};function u(e){var t=e.prototype,n=[];for(var r in t){"function"==typeof t[r]&&"constructor"!==r&&n.push(r)}return n}i.Extend=function(e,t){var n={}.hasOwnProperty;function r(){this.constructor=e}for(var i in t)n.call(t,i)&&(e[i]=t[i]);return r.prototype=t.prototype,e.prototype=new r,e.__super__=t.prototype,e},i.Decorate=function(r,i){var e=u(i),t=u(r);function o(){var e=Array.prototype.unshift,t=i.prototype.constructor.length,n=r.prototype.constructor;0<t&&(e.call(arguments,r.prototype.constructor),n=i.prototype.constructor),n.apply(this,arguments)}i.displayName=r.displayName,o.prototype=new function(){this.constructor=o};for(var n=0;n<t.length;n++){var s=t[n];o.prototype[s]=r.prototype[s]}function a(e){var t=function(){};e in o.prototype&&(t=o.prototype[e]);var n=i.prototype[e];return function(){return Array.prototype.unshift.call(arguments,t),n.apply(this,arguments)}}for(var l=0;l<e.length;l++){var c=e[l];o.prototype[c]=a(c)}return o};function e(){this.listeners={}}e.prototype.on=function(e,t){this.listeners=this.listeners||{},e in this.listeners?this.listeners[e].push(t):this.listeners[e]=[t]},e.prototype.trigger=function(e){var t=Array.prototype.slice,n=t.call(arguments,1);this.listeners=this.listeners||{},null==n&&(n=[]),0===n.length&&n.push({}),(n[0]._type=e)in this.listeners&&this.invoke(this.listeners[e],t.call(arguments,1)),"*"in this.listeners&&this.invoke(this.listeners["*"],arguments)},e.prototype.invoke=function(e,t){for(var n=0,r=e.length;n<r;n++)e[n].apply(this,t)},i.Observable=e,i.generateChars=function(e){for(var t="",n=0;n<e;n++){t+=Math.floor(36*Math.random()).toString(36)}return t},i.bind=function(e,t){return function(){e.apply(t,arguments)}},i._convertData=function(e){for(var t in e){var n=t.split("-"),r=e;if(1!==n.length){for(var i=0;i<n.length;i++){var o=n[i];(o=o.substring(0,1).toLowerCase()+o.substring(1))in r||(r[o]={}),i==n.length-1&&(r[o]=e[t]),r=r[o]}delete e[t]}}return e},i.hasScroll=function(e,t){var n=o(t),r=t.style.overflowX,i=t.style.overflowY;return(r!==i||"hidden"!==i&&"visible"!==i)&&("scroll"===r||"scroll"===i||(n.innerHeight()<t.scrollHeight||n.innerWidth()<t.scrollWidth))},i.escapeMarkup=function(e){var t={"\\":"\","&":"&","<":"<",">":">",'"':""","'":"'","/":"/"};return"string"!=typeof e?e:String(e).replace(/[&<>"'\/\\]/g,function(e){return t[e]})},i.appendMany=function(e,t){if("1.7"===o.fn.jquery.substr(0,3)){var n=o();o.map(t,function(e){n=n.add(e)}),t=n}e.append(t)},i.__cache={};var n=0;return i.GetUniqueElementId=function(e){var t=e.getAttribute("data-select2-id");return null==t&&(e.id?(t=e.id,e.setAttribute("data-select2-id",t)):(e.setAttribute("data-select2-id",++n),t=n.toString())),t},i.StoreData=function(e,t,n){var r=i.GetUniqueElementId(e);i.__cache[r]||(i.__cache[r]={}),i.__cache[r][t]=n},i.GetData=function(e,t){var n=i.GetUniqueElementId(e);return t?i.__cache[n]&&null!=i.__cache[n][t]?i.__cache[n][t]:o(e).data(t):i.__cache[n]},i.RemoveData=function(e){var t=i.GetUniqueElementId(e);null!=i.__cache[t]&&delete i.__cache[t],e.removeAttribute("data-select2-id")},i}),e.define("select2/results",["jquery","./utils"],function(h,f){function r(e,t,n){this.$element=e,this.data=n,this.options=t,r.__super__.constructor.call(this)}return f.Extend(r,f.Observable),r.prototype.render=function(){var e=h('<ul class="select2-results__options" role="listbox"></ul>');return this.options.get("multiple")&&e.attr("aria-multiselectable","true"),this.$results=e},r.prototype.clear=function(){this.$results.empty()},r.prototype.displayMessage=function(e){var t=this.options.get("escapeMarkup");this.clear(),this.hideLoading();var n=h('<li role="alert" aria-live="assertive" class="select2-results__option"></li>'),r=this.options.get("translations").get(e.message);n.append(t(r(e.args))),n[0].className+=" select2-results__message",this.$results.append(n)},r.prototype.hideMessages=function(){this.$results.find(".select2-results__message").remove()},r.prototype.append=function(e){this.hideLoading();var t=[];if(null!=e.results&&0!==e.results.length){e.results=this.sort(e.results);for(var n=0;n<e.results.length;n++){var r=e.results[n],i=this.option(r);t.push(i)}this.$results.append(t)}else 0===this.$results.children().length&&this.trigger("results:message",{message:"noResults"})},r.prototype.position=function(e,t){t.find(".select2-results").append(e)},r.prototype.sort=function(e){return this.options.get("sorter")(e)},r.prototype.highlightFirstItem=function(){var e=this.$results.find(".select2-results__option[aria-selected]"),t=e.filter("[aria-selected=true]");0<t.length?t.first().trigger("mouseenter"):e.first().trigger("mouseenter"),this.ensureHighlightVisible()},r.prototype.setClasses=function(){var t=this;this.data.current(function(e){var r=h.map(e,function(e){return e.id.toString()});t.$results.find(".select2-results__option[aria-selected]").each(function(){var e=h(this),t=f.GetData(this,"data"),n=""+t.id;null!=t.element&&t.element.selected||null==t.element&&-1<h.inArray(n,r)?e.attr("aria-selected","true"):e.attr("aria-selected","false")})})},r.prototype.showLoading=function(e){this.hideLoading();var t={disabled:!0,loading:!0,text:this.options.get("translations").get("searching")(e)},n=this.option(t);n.className+=" loading-results",this.$results.prepend(n)},r.prototype.hideLoading=function(){this.$results.find(".loading-results").remove()},r.prototype.option=function(e){var t=document.createElement("li");t.className="select2-results__option";var n={role:"option","aria-selected":"false"},r=window.Element.prototype.matches||window.Element.prototype.msMatchesSelector||window.Element.prototype.webkitMatchesSelector;for(var i in(null!=e.element&&r.call(e.element,":disabled")||null==e.element&&e.disabled)&&(delete n["aria-selected"],n["aria-disabled"]="true"),null==e.id&&delete n["aria-selected"],null!=e._resultId&&(t.id=e._resultId),e.title&&(t.title=e.title),e.children&&(n.role="group",n["aria-label"]=e.text,delete n["aria-selected"]),n){var o=n[i];t.setAttribute(i,o)}if(e.children){var s=h(t),a=document.createElement("strong");a.className="select2-results__group";h(a);this.template(e,a);for(var l=[],c=0;c<e.children.length;c++){var u=e.children[c],d=this.option(u);l.push(d)}var p=h("<ul></ul>",{class:"select2-results__options select2-results__options--nested"});p.append(l),s.append(a),s.append(p)}else this.template(e,t);return f.StoreData(t,"data",e),t},r.prototype.bind=function(t,e){var l=this,n=t.id+"-results";this.$results.attr("id",n),t.on("results:all",function(e){l.clear(),l.append(e.data),t.isOpen()&&(l.setClasses(),l.highlightFirstItem())}),t.on("results:append",function(e){l.append(e.data),t.isOpen()&&l.setClasses()}),t.on("query",function(e){l.hideMessages(),l.showLoading(e)}),t.on("select",function(){t.isOpen()&&(l.setClasses(),l.options.get("scrollAfterSelect")&&l.highlightFirstItem())}),t.on("unselect",function(){t.isOpen()&&(l.setClasses(),l.options.get("scrollAfterSelect")&&l.highlightFirstItem())}),t.on("open",function(){l.$results.attr("aria-expanded","true"),l.$results.attr("aria-hidden","false"),l.setClasses(),l.ensureHighlightVisible()}),t.on("close",function(){l.$results.attr("aria-expanded","false"),l.$results.attr("aria-hidden","true"),l.$results.removeAttr("aria-activedescendant")}),t.on("results:toggle",function(){var e=l.getHighlightedResults();0!==e.length&&e.trigger("mouseup")}),t.on("results:select",function(){var e=l.getHighlightedResults();if(0!==e.length){var t=f.GetData(e[0],"data");"true"==e.attr("aria-selected")?l.trigger("close",{}):l.trigger("select",{data:t})}}),t.on("results:previous",function(){var e=l.getHighlightedResults(),t=l.$results.find("[aria-selected]"),n=t.index(e);if(!(n<=0)){var r=n-1;0===e.length&&(r=0);var i=t.eq(r);i.trigger("mouseenter");var o=l.$results.offset().top,s=i.offset().top,a=l.$results.scrollTop()+(s-o);0===r?l.$results.scrollTop(0):s-o<0&&l.$results.scrollTop(a)}}),t.on("results:next",function(){var e=l.getHighlightedResults(),t=l.$results.find("[aria-selected]"),n=t.index(e)+1;if(!(n>=t.length)){var r=t.eq(n);r.trigger("mouseenter");var i=l.$results.offset().top+l.$results.outerHeight(!1),o=r.offset().top+r.outerHeight(!1),s=l.$results.scrollTop()+o-i;0===n?l.$results.scrollTop(0):i<o&&l.$results.scrollTop(s)}}),t.on("results:focus",function(e){e.element.addClass("select2-results__option--highlighted")}),t.on("results:message",function(e){l.displayMessage(e)}),h.fn.mousewheel&&this.$results.on("mousewheel",function(e){var t=l.$results.scrollTop(),n=l.$results.get(0).scrollHeight-t+e.deltaY,r=0<e.deltaY&&t-e.deltaY<=0,i=e.deltaY<0&&n<=l.$results.height();r?(l.$results.scrollTop(0),e.preventDefault(),e.stopPropagation()):i&&(l.$results.scrollTop(l.$results.get(0).scrollHeight-l.$results.height()),e.preventDefault(),e.stopPropagation())}),this.$results.on("mouseup",".select2-results__option[aria-selected]",function(e){var t=h(this),n=f.GetData(this,"data");"true"!==t.attr("aria-selected")?l.trigger("select",{originalEvent:e,data:n}):l.options.get("multiple")?l.trigger("unselect",{originalEvent:e,data:n}):l.trigger("close",{})}),this.$results.on("mouseenter",".select2-results__option[aria-selected]",function(e){var t=f.GetData(this,"data");l.getHighlightedResults().removeClass("select2-results__option--highlighted"),l.trigger("results:focus",{data:t,element:h(this)})})},r.prototype.getHighlightedResults=function(){return this.$results.find(".select2-results__option--highlighted")},r.prototype.destroy=function(){this.$results.remove()},r.prototype.ensureHighlightVisible=function(){var e=this.getHighlightedResults();if(0!==e.length){var t=this.$results.find("[aria-selected]").index(e),n=this.$results.offset().top,r=e.offset().top,i=this.$results.scrollTop()+(r-n),o=r-n;i-=2*e.outerHeight(!1),t<=2?this.$results.scrollTop(0):(o>this.$results.outerHeight()||o<0)&&this.$results.scrollTop(i)}},r.prototype.template=function(e,t){var n=this.options.get("templateResult"),r=this.options.get("escapeMarkup"),i=n(e,t);null==i?t.style.display="none":"string"==typeof i?t.innerHTML=r(i):h(t).append(i)},r}),e.define("select2/keys",[],function(){return{BACKSPACE:8,TAB:9,ENTER:13,SHIFT:16,CTRL:17,ALT:18,ESC:27,SPACE:32,PAGE_UP:33,PAGE_DOWN:34,END:35,HOME:36,LEFT:37,UP:38,RIGHT:39,DOWN:40,DELETE:46}}),e.define("select2/selection/base",["jquery","../utils","../keys"],function(n,r,i){function o(e,t){this.$element=e,this.options=t,o.__super__.constructor.call(this)}return r.Extend(o,r.Observable),o.prototype.render=function(){var e=n('<span class="select2-selection" role="combobox" aria-haspopup="true" aria-expanded="false"></span>');return this._tabindex=0,null!=r.GetData(this.$element[0],"old-tabindex")?this._tabindex=r.GetData(this.$element[0],"old-tabindex"):null!=this.$element.attr("tabindex")&&(this._tabindex=this.$element.attr("tabindex")),e.attr("title",this.$element.attr("title")),e.attr("tabindex",this._tabindex),e.attr("aria-disabled","false"),this.$selection=e},o.prototype.bind=function(e,t){var n=this,r=e.id+"-results";this.container=e,this.$selection.on("focus",function(e){n.trigger("focus",e)}),this.$selection.on("blur",function(e){n._handleBlur(e)}),this.$selection.on("keydown",function(e){n.trigger("keypress",e),e.which===i.SPACE&&e.preventDefault()}),e.on("results:focus",function(e){n.$selection.attr("aria-activedescendant",e.data._resultId)}),e.on("selection:update",function(e){n.update(e.data)}),e.on("open",function(){n.$selection.attr("aria-expanded","true"),n.$selection.attr("aria-owns",r),n._attachCloseHandler(e)}),e.on("close",function(){n.$selection.attr("aria-expanded","false"),n.$selection.removeAttr("aria-activedescendant"),n.$selection.removeAttr("aria-owns"),n.$selection.trigger("focus"),n._detachCloseHandler(e)}),e.on("enable",function(){n.$selection.attr("tabindex",n._tabindex),n.$selection.attr("aria-disabled","false")}),e.on("disable",function(){n.$selection.attr("tabindex","-1"),n.$selection.attr("aria-disabled","true")})},o.prototype._handleBlur=function(e){var t=this;window.setTimeout(function(){document.activeElement==t.$selection[0]||n.contains(t.$selection[0],document.activeElement)||t.trigger("blur",e)},1)},o.prototype._attachCloseHandler=function(e){n(document.body).on("mousedown.select2."+e.id,function(e){var t=n(e.target).closest(".select2");n(".select2.select2-container--open").each(function(){this!=t[0]&&r.GetData(this,"element").select2("close")})})},o.prototype._detachCloseHandler=function(e){n(document.body).off("mousedown.select2."+e.id)},o.prototype.position=function(e,t){t.find(".selection").append(e)},o.prototype.destroy=function(){this._detachCloseHandler(this.container)},o.prototype.update=function(e){throw new Error("The `update` method must be defined in child classes.")},o}),e.define("select2/selection/single",["jquery","./base","../utils","../keys"],function(e,t,n,r){function i(){i.__super__.constructor.apply(this,arguments)}return n.Extend(i,t),i.prototype.render=function(){var e=i.__super__.render.call(this);return e.addClass("select2-selection--single"),e.html('<span class="select2-selection__rendered"></span><span class="select2-selection__arrow" role="presentation"><b role="presentation"></b></span>'),e},i.prototype.bind=function(t,e){var n=this;i.__super__.bind.apply(this,arguments);var r=t.id+"-container";this.$selection.find(".select2-selection__rendered").attr("id",r).attr("role","textbox").attr("aria-readonly","true"),this.$selection.attr("aria-labelledby",r),this.$selection.on("mousedown",function(e){1===e.which&&n.trigger("toggle",{originalEvent:e})}),this.$selection.on("focus",function(e){}),this.$selection.on("blur",function(e){}),t.on("focus",function(e){t.isOpen()||n.$selection.trigger("focus")})},i.prototype.clear=function(){var e=this.$selection.find(".select2-selection__rendered");e.empty(),e.removeAttr("title")},i.prototype.display=function(e,t){var n=this.options.get("templateSelection");return this.options.get("escapeMarkup")(n(e,t))},i.prototype.selectionContainer=function(){return e("<span></span>")},i.prototype.update=function(e){if(0!==e.length){var t=e[0],n=this.$selection.find(".select2-selection__rendered"),r=this.display(t,n);n.empty().append(r);var i=t.title||t.text;i?n.attr("title",i):n.removeAttr("title")}else this.clear()},i}),e.define("select2/selection/multiple",["jquery","./base","../utils"],function(i,e,l){function n(e,t){n.__super__.constructor.apply(this,arguments)}return l.Extend(n,e),n.prototype.render=function(){var e=n.__super__.render.call(this);return e.addClass("select2-selection--multiple"),e.html('<ul class="select2-selection__rendered"></ul>'),e},n.prototype.bind=function(e,t){var r=this;n.__super__.bind.apply(this,arguments),this.$selection.on("click",function(e){r.trigger("toggle",{originalEvent:e})}),this.$selection.on("click",".select2-selection__choice__remove",function(e){if(!r.options.get("disabled")){var t=i(this).parent(),n=l.GetData(t[0],"data");r.trigger("unselect",{originalEvent:e,data:n})}})},n.prototype.clear=function(){var e=this.$selection.find(".select2-selection__rendered");e.empty(),e.removeAttr("title")},n.prototype.display=function(e,t){var n=this.options.get("templateSelection");return this.options.get("escapeMarkup")(n(e,t))},n.prototype.selectionContainer=function(){return i('<li class="select2-selection__choice"><span class="select2-selection__choice__remove" role="presentation">×</span></li>')},n.prototype.update=function(e){if(this.clear(),0!==e.length){for(var t=[],n=0;n<e.length;n++){var r=e[n],i=this.selectionContainer(),o=this.display(r,i);i.append(o);var s=r.title||r.text;s&&i.attr("title",s),l.StoreData(i[0],"data",r),t.push(i)}var a=this.$selection.find(".select2-selection__rendered");l.appendMany(a,t)}},n}),e.define("select2/selection/placeholder",["../utils"],function(e){function t(e,t,n){this.placeholder=this.normalizePlaceholder(n.get("placeholder")),e.call(this,t,n)}return t.prototype.normalizePlaceholder=function(e,t){return"string"==typeof t&&(t={id:"",text:t}),t},t.prototype.createPlaceholder=function(e,t){var n=this.selectionContainer();return n.html(this.display(t)),n.addClass("select2-selection__placeholder").removeClass("select2-selection__choice"),n},t.prototype.update=function(e,t){var n=1==t.length&&t[0].id!=this.placeholder.id;if(1<t.length||n)return e.call(this,t);this.clear();var r=this.createPlaceholder(this.placeholder);this.$selection.find(".select2-selection__rendered").append(r)},t}),e.define("select2/selection/allowClear",["jquery","../keys","../utils"],function(i,r,a){function e(){}return e.prototype.bind=function(e,t,n){var r=this;e.call(this,t,n),null==this.placeholder&&this.options.get("debug")&&window.console&&console.error&&console.error("Select2: The `allowClear` option should be used in combination with the `placeholder` option."),this.$selection.on("mousedown",".select2-selection__clear",function(e){r._handleClear(e)}),t.on("keypress",function(e){r._handleKeyboardClear(e,t)})},e.prototype._handleClear=function(e,t){if(!this.options.get("disabled")){var n=this.$selection.find(".select2-selection__clear");if(0!==n.length){t.stopPropagation();var r=a.GetData(n[0],"data"),i=this.$element.val();this.$element.val(this.placeholder.id);var o={data:r};if(this.trigger("clear",o),o.prevented)this.$element.val(i);else{for(var s=0;s<r.length;s++)if(o={data:r[s]},this.trigger("unselect",o),o.prevented)return void this.$element.val(i);this.$element.trigger("change"),this.trigger("toggle",{})}}}},e.prototype._handleKeyboardClear=function(e,t,n){n.isOpen()||t.which!=r.DELETE&&t.which!=r.BACKSPACE||this._handleClear(t)},e.prototype.update=function(e,t){if(e.call(this,t),!(0<this.$selection.find(".select2-selection__placeholder").length||0===t.length)){var n=this.options.get("translations").get("removeAllItems"),r=i('<span class="select2-selection__clear" title="'+n()+'">×</span>');a.StoreData(r[0],"data",t),this.$selection.find(".select2-selection__rendered").prepend(r)}},e}),e.define("select2/selection/search",["jquery","../utils","../keys"],function(r,a,l){function e(e,t,n){e.call(this,t,n)}return e.prototype.render=function(e){var t=r('<li class="select2-search select2-search--inline"><input class="select2-search__field" type="search" tabindex="-1" autocomplete="off" autocorrect="off" autocapitalize="none" spellcheck="false" role="searchbox" aria-autocomplete="list" /></li>');this.$searchContainer=t,this.$search=t.find("input");var n=e.call(this);return this._transferTabIndex(),n},e.prototype.bind=function(e,t,n){var r=this,i=t.id+"-results";e.call(this,t,n),t.on("open",function(){r.$search.attr("aria-controls",i),r.$search.trigger("focus")}),t.on("close",function(){r.$search.val(""),r.$search.removeAttr("aria-controls"),r.$search.removeAttr("aria-activedescendant"),r.$search.trigger("focus")}),t.on("enable",function(){r.$search.prop("disabled",!1),r._transferTabIndex()}),t.on("disable",function(){r.$search.prop("disabled",!0)}),t.on("focus",function(e){r.$search.trigger("focus")}),t.on("results:focus",function(e){e.data._resultId?r.$search.attr("aria-activedescendant",e.data._resultId):r.$search.removeAttr("aria-activedescendant")}),this.$selection.on("focusin",".select2-search--inline",function(e){r.trigger("focus",e)}),this.$selection.on("focusout",".select2-search--inline",function(e){r._handleBlur(e)}),this.$selection.on("keydown",".select2-search--inline",function(e){if(e.stopPropagation(),r.trigger("keypress",e),r._keyUpPrevented=e.isDefaultPrevented(),e.which===l.BACKSPACE&&""===r.$search.val()){var t=r.$searchContainer.prev(".select2-selection__choice");if(0<t.length){var n=a.GetData(t[0],"data");r.searchRemoveChoice(n),e.preventDefault()}}}),this.$selection.on("click",".select2-search--inline",function(e){r.$search.val()&&e.stopPropagation()});var o=document.documentMode,s=o&&o<=11;this.$selection.on("input.searchcheck",".select2-search--inline",function(e){s?r.$selection.off("input.search input.searchcheck"):r.$selection.off("keyup.search")}),this.$selection.on("keyup.search input.search",".select2-search--inline",function(e){if(s&&"input"===e.type)r.$selection.off("input.search input.searchcheck");else{var t=e.which;t!=l.SHIFT&&t!=l.CTRL&&t!=l.ALT&&t!=l.TAB&&r.handleSearch(e)}})},e.prototype._transferTabIndex=function(e){this.$search.attr("tabindex",this.$selection.attr("tabindex")),this.$selection.attr("tabindex","-1")},e.prototype.createPlaceholder=function(e,t){this.$search.attr("placeholder",t.text)},e.prototype.update=function(e,t){var n=this.$search[0]==document.activeElement;this.$search.attr("placeholder",""),e.call(this,t),this.$selection.find(".select2-selection__rendered").append(this.$searchContainer),this.resizeSearch(),n&&this.$search.trigger("focus")},e.prototype.handleSearch=function(){if(this.resizeSearch(),!this._keyUpPrevented){var e=this.$search.val();this.trigger("query",{term:e})}this._keyUpPrevented=!1},e.prototype.searchRemoveChoice=function(e,t){this.trigger("unselect",{data:t}),this.$search.val(t.text),this.handleSearch()},e.prototype.resizeSearch=function(){this.$search.css("width","25px");var e="";""!==this.$search.attr("placeholder")?e=this.$selection.find(".select2-selection__rendered").width():e=.75*(this.$search.val().length+1)+"em";this.$search.css("width",e)},e}),e.define("select2/selection/eventRelay",["jquery"],function(s){function e(){}return e.prototype.bind=function(e,t,n){var r=this,i=["open","opening","close","closing","select","selecting","unselect","unselecting","clear","clearing"],o=["opening","closing","selecting","unselecting","clearing"];e.call(this,t,n),t.on("*",function(e,t){if(-1!==s.inArray(e,i)){t=t||{};var n=s.Event("select2:"+e,{params:t});r.$element.trigger(n),-1!==s.inArray(e,o)&&(t.prevented=n.isDefaultPrevented())}})},e}),e.define("select2/translation",["jquery","require"],function(t,n){function r(e){this.dict=e||{}}return r.prototype.all=function(){return this.dict},r.prototype.get=function(e){return this.dict[e]},r.prototype.extend=function(e){this.dict=t.extend({},e.all(),this.dict)},r._cache={},r.loadPath=function(e){if(!(e in r._cache)){var t=n(e);r._cache[e]=t}return new r(r._cache[e])},r}),e.define("select2/diacritics",[],function(){return{"Ⓐ":"A","A":"A","À":"A","Á":"A","Â":"A","Ầ":"A","Ấ":"A","Ẫ":"A","Ẩ":"A","Ã":"A","Ā":"A","Ă":"A","Ằ":"A","Ắ":"A","Ẵ":"A","Ẳ":"A","Ȧ":"A","Ǡ":"A","Ä":"A","Ǟ":"A","Ả":"A","Å":"A","Ǻ":"A","Ǎ":"A","Ȁ":"A","Ȃ":"A","Ạ":"A","Ậ":"A","Ặ":"A","Ḁ":"A","Ą":"A","Ⱥ":"A","Ɐ":"A","Ꜳ":"AA","Æ":"AE","Ǽ":"AE","Ǣ":"AE","Ꜵ":"AO","Ꜷ":"AU","Ꜹ":"AV","Ꜻ":"AV","Ꜽ":"AY","Ⓑ":"B","B":"B","Ḃ":"B","Ḅ":"B","Ḇ":"B","Ƀ":"B","Ƃ":"B","Ɓ":"B","Ⓒ":"C","C":"C","Ć":"C","Ĉ":"C","Ċ":"C","Č":"C","Ç":"C","Ḉ":"C","Ƈ":"C","Ȼ":"C","Ꜿ":"C","Ⓓ":"D","D":"D","Ḋ":"D","Ď":"D","Ḍ":"D","Ḑ":"D","Ḓ":"D","Ḏ":"D","Đ":"D","Ƌ":"D","Ɗ":"D","Ɖ":"D","Ꝺ":"D","DZ":"DZ","DŽ":"DZ","Dz":"Dz","Dž":"Dz","Ⓔ":"E","E":"E","È":"E","É":"E","Ê":"E","Ề":"E","Ế":"E","Ễ":"E","Ể":"E","Ẽ":"E","Ē":"E","Ḕ":"E","Ḗ":"E","Ĕ":"E","Ė":"E","Ë":"E","Ẻ":"E","Ě":"E","Ȅ":"E","Ȇ":"E","Ẹ":"E","Ệ":"E","Ȩ":"E","Ḝ":"E","Ę":"E","Ḙ":"E","Ḛ":"E","Ɛ":"E","Ǝ":"E","Ⓕ":"F","F":"F","Ḟ":"F","Ƒ":"F","Ꝼ":"F","Ⓖ":"G","G":"G","Ǵ":"G","Ĝ":"G","Ḡ":"G","Ğ":"G","Ġ":"G","Ǧ":"G","Ģ":"G","Ǥ":"G","Ɠ":"G","Ꞡ":"G","Ᵹ":"G","Ꝿ":"G","Ⓗ":"H","H":"H","Ĥ":"H","Ḣ":"H","Ḧ":"H","Ȟ":"H","Ḥ":"H","Ḩ":"H","Ḫ":"H","Ħ":"H","Ⱨ":"H","Ⱶ":"H","Ɥ":"H","Ⓘ":"I","I":"I","Ì":"I","Í":"I","Î":"I","Ĩ":"I","Ī":"I","Ĭ":"I","İ":"I","Ï":"I","Ḯ":"I","Ỉ":"I","Ǐ":"I","Ȉ":"I","Ȋ":"I","Ị":"I","Į":"I","Ḭ":"I","Ɨ":"I","Ⓙ":"J","J":"J","Ĵ":"J","Ɉ":"J","Ⓚ":"K","K":"K","Ḱ":"K","Ǩ":"K","Ḳ":"K","Ķ":"K","Ḵ":"K","Ƙ":"K","Ⱪ":"K","Ꝁ":"K","Ꝃ":"K","Ꝅ":"K","Ꞣ":"K","Ⓛ":"L","L":"L","Ŀ":"L","Ĺ":"L","Ľ":"L","Ḷ":"L","Ḹ":"L","Ļ":"L","Ḽ":"L","Ḻ":"L","Ł":"L","Ƚ":"L","Ɫ":"L","Ⱡ":"L","Ꝉ":"L","Ꝇ":"L","Ꞁ":"L","LJ":"LJ","Lj":"Lj","Ⓜ":"M","M":"M","Ḿ":"M","Ṁ":"M","Ṃ":"M","Ɱ":"M","Ɯ":"M","Ⓝ":"N","N":"N","Ǹ":"N","Ń":"N","Ñ":"N","Ṅ":"N","Ň":"N","Ṇ":"N","Ņ":"N","Ṋ":"N","Ṉ":"N","Ƞ":"N","Ɲ":"N","Ꞑ":"N","Ꞥ":"N","NJ":"NJ","Nj":"Nj","Ⓞ":"O","O":"O","Ò":"O","Ó":"O","Ô":"O","Ồ":"O","Ố":"O","Ỗ":"O","Ổ":"O","Õ":"O","Ṍ":"O","Ȭ":"O","Ṏ":"O","Ō":"O","Ṑ":"O","Ṓ":"O","Ŏ":"O","Ȯ":"O","Ȱ":"O","Ö":"O","Ȫ":"O","Ỏ":"O","Ő":"O","Ǒ":"O","Ȍ":"O","Ȏ":"O","Ơ":"O","Ờ":"O","Ớ":"O","Ỡ":"O","Ở":"O","Ợ":"O","Ọ":"O","Ộ":"O","Ǫ":"O","Ǭ":"O","Ø":"O","Ǿ":"O","Ɔ":"O","Ɵ":"O","Ꝋ":"O","Ꝍ":"O","Œ":"OE","Ƣ":"OI","Ꝏ":"OO","Ȣ":"OU","Ⓟ":"P","P":"P","Ṕ":"P","Ṗ":"P","Ƥ":"P","Ᵽ":"P","Ꝑ":"P","Ꝓ":"P","Ꝕ":"P","Ⓠ":"Q","Q":"Q","Ꝗ":"Q","Ꝙ":"Q","Ɋ":"Q","Ⓡ":"R","R":"R","Ŕ":"R","Ṙ":"R","Ř":"R","Ȑ":"R","Ȓ":"R","Ṛ":"R","Ṝ":"R","Ŗ":"R","Ṟ":"R","Ɍ":"R","Ɽ":"R","Ꝛ":"R","Ꞧ":"R","Ꞃ":"R","Ⓢ":"S","S":"S","ẞ":"S","Ś":"S","Ṥ":"S","Ŝ":"S","Ṡ":"S","Š":"S","Ṧ":"S","Ṣ":"S","Ṩ":"S","Ș":"S","Ş":"S","Ȿ":"S","Ꞩ":"S","Ꞅ":"S","Ⓣ":"T","T":"T","Ṫ":"T","Ť":"T","Ṭ":"T","Ț":"T","Ţ":"T","Ṱ":"T","Ṯ":"T","Ŧ":"T","Ƭ":"T","Ʈ":"T","Ⱦ":"T","Ꞇ":"T","Ꜩ":"TZ","Ⓤ":"U","U":"U","Ù":"U","Ú":"U","Û":"U","Ũ":"U","Ṹ":"U","Ū":"U","Ṻ":"U","Ŭ":"U","Ü":"U","Ǜ":"U","Ǘ":"U","Ǖ":"U","Ǚ":"U","Ủ":"U","Ů":"U","Ű":"U","Ǔ":"U","Ȕ":"U","Ȗ":"U","Ư":"U","Ừ":"U","Ứ":"U","Ữ":"U","Ử":"U","Ự":"U","Ụ":"U","Ṳ":"U","Ų":"U","Ṷ":"U","Ṵ":"U","Ʉ":"U","Ⓥ":"V","V":"V","Ṽ":"V","Ṿ":"V","Ʋ":"V","Ꝟ":"V","Ʌ":"V","Ꝡ":"VY","Ⓦ":"W","W":"W","Ẁ":"W","Ẃ":"W","Ŵ":"W","Ẇ":"W","Ẅ":"W","Ẉ":"W","Ⱳ":"W","Ⓧ":"X","X":"X","Ẋ":"X","Ẍ":"X","Ⓨ":"Y","Y":"Y","Ỳ":"Y","Ý":"Y","Ŷ":"Y","Ỹ":"Y","Ȳ":"Y","Ẏ":"Y","Ÿ":"Y","Ỷ":"Y","Ỵ":"Y","Ƴ":"Y","Ɏ":"Y","Ỿ":"Y","Ⓩ":"Z","Z":"Z","Ź":"Z","Ẑ":"Z","Ż":"Z","Ž":"Z","Ẓ":"Z","Ẕ":"Z","Ƶ":"Z","Ȥ":"Z","Ɀ":"Z","Ⱬ":"Z","Ꝣ":"Z","ⓐ":"a","a":"a","ẚ":"a","à":"a","á":"a","â":"a","ầ":"a","ấ":"a","ẫ":"a","ẩ":"a","ã":"a","ā":"a","ă":"a","ằ":"a","ắ":"a","ẵ":"a","ẳ":"a","ȧ":"a","ǡ":"a","ä":"a","ǟ":"a","ả":"a","å":"a","ǻ":"a","ǎ":"a","ȁ":"a","ȃ":"a","ạ":"a","ậ":"a","ặ":"a","ḁ":"a","ą":"a","ⱥ":"a","ɐ":"a","ꜳ":"aa","æ":"ae","ǽ":"ae","ǣ":"ae","ꜵ":"ao","ꜷ":"au","ꜹ":"av","ꜻ":"av","ꜽ":"ay","ⓑ":"b","b":"b","ḃ":"b","ḅ":"b","ḇ":"b","ƀ":"b","ƃ":"b","ɓ":"b","ⓒ":"c","c":"c","ć":"c","ĉ":"c","ċ":"c","č":"c","ç":"c","ḉ":"c","ƈ":"c","ȼ":"c","ꜿ":"c","ↄ":"c","ⓓ":"d","d":"d","ḋ":"d","ď":"d","ḍ":"d","ḑ":"d","ḓ":"d","ḏ":"d","đ":"d","ƌ":"d","ɖ":"d","ɗ":"d","ꝺ":"d","dz":"dz","dž":"dz","ⓔ":"e","e":"e","è":"e","é":"e","ê":"e","ề":"e","ế":"e","ễ":"e","ể":"e","ẽ":"e","ē":"e","ḕ":"e","ḗ":"e","ĕ":"e","ė":"e","ë":"e","ẻ":"e","ě":"e","ȅ":"e","ȇ":"e","ẹ":"e","ệ":"e","ȩ":"e","ḝ":"e","ę":"e","ḙ":"e","ḛ":"e","ɇ":"e","ɛ":"e","ǝ":"e","ⓕ":"f","f":"f","ḟ":"f","ƒ":"f","ꝼ":"f","ⓖ":"g","g":"g","ǵ":"g","ĝ":"g","ḡ":"g","ğ":"g","ġ":"g","ǧ":"g","ģ":"g","ǥ":"g","ɠ":"g","ꞡ":"g","ᵹ":"g","ꝿ":"g","ⓗ":"h","h":"h","ĥ":"h","ḣ":"h","ḧ":"h","ȟ":"h","ḥ":"h","ḩ":"h","ḫ":"h","ẖ":"h","ħ":"h","ⱨ":"h","ⱶ":"h","ɥ":"h","ƕ":"hv","ⓘ":"i","i":"i","ì":"i","í":"i","î":"i","ĩ":"i","ī":"i","ĭ":"i","ï":"i","ḯ":"i","ỉ":"i","ǐ":"i","ȉ":"i","ȋ":"i","ị":"i","į":"i","ḭ":"i","ɨ":"i","ı":"i","ⓙ":"j","j":"j","ĵ":"j","ǰ":"j","ɉ":"j","ⓚ":"k","k":"k","ḱ":"k","ǩ":"k","ḳ":"k","ķ":"k","ḵ":"k","ƙ":"k","ⱪ":"k","ꝁ":"k","ꝃ":"k","ꝅ":"k","ꞣ":"k","ⓛ":"l","l":"l","ŀ":"l","ĺ":"l","ľ":"l","ḷ":"l","ḹ":"l","ļ":"l","ḽ":"l","ḻ":"l","ſ":"l","ł":"l","ƚ":"l","ɫ":"l","ⱡ":"l","ꝉ":"l","ꞁ":"l","ꝇ":"l","lj":"lj","ⓜ":"m","m":"m","ḿ":"m","ṁ":"m","ṃ":"m","ɱ":"m","ɯ":"m","ⓝ":"n","n":"n","ǹ":"n","ń":"n","ñ":"n","ṅ":"n","ň":"n","ṇ":"n","ņ":"n","ṋ":"n","ṉ":"n","ƞ":"n","ɲ":"n","ʼn":"n","ꞑ":"n","ꞥ":"n","nj":"nj","ⓞ":"o","o":"o","ò":"o","ó":"o","ô":"o","ồ":"o","ố":"o","ỗ":"o","ổ":"o","õ":"o","ṍ":"o","ȭ":"o","ṏ":"o","ō":"o","ṑ":"o","ṓ":"o","ŏ":"o","ȯ":"o","ȱ":"o","ö":"o","ȫ":"o","ỏ":"o","ő":"o","ǒ":"o","ȍ":"o","ȏ":"o","ơ":"o","ờ":"o","ớ":"o","ỡ":"o","ở":"o","ợ":"o","ọ":"o","ộ":"o","ǫ":"o","ǭ":"o","ø":"o","ǿ":"o","ɔ":"o","ꝋ":"o","ꝍ":"o","ɵ":"o","œ":"oe","ƣ":"oi","ȣ":"ou","ꝏ":"oo","ⓟ":"p","p":"p","ṕ":"p","ṗ":"p","ƥ":"p","ᵽ":"p","ꝑ":"p","ꝓ":"p","ꝕ":"p","ⓠ":"q","q":"q","ɋ":"q","ꝗ":"q","ꝙ":"q","ⓡ":"r","r":"r","ŕ":"r","ṙ":"r","ř":"r","ȑ":"r","ȓ":"r","ṛ":"r","ṝ":"r","ŗ":"r","ṟ":"r","ɍ":"r","ɽ":"r","ꝛ":"r","ꞧ":"r","ꞃ":"r","ⓢ":"s","s":"s","ß":"s","ś":"s","ṥ":"s","ŝ":"s","ṡ":"s","š":"s","ṧ":"s","ṣ":"s","ṩ":"s","ș":"s","ş":"s","ȿ":"s","ꞩ":"s","ꞅ":"s","ẛ":"s","ⓣ":"t","t":"t","ṫ":"t","ẗ":"t","ť":"t","ṭ":"t","ț":"t","ţ":"t","ṱ":"t","ṯ":"t","ŧ":"t","ƭ":"t","ʈ":"t","ⱦ":"t","ꞇ":"t","ꜩ":"tz","ⓤ":"u","u":"u","ù":"u","ú":"u","û":"u","ũ":"u","ṹ":"u","ū":"u","ṻ":"u","ŭ":"u","ü":"u","ǜ":"u","ǘ":"u","ǖ":"u","ǚ":"u","ủ":"u","ů":"u","ű":"u","ǔ":"u","ȕ":"u","ȗ":"u","ư":"u","ừ":"u","ứ":"u","ữ":"u","ử":"u","ự":"u","ụ":"u","ṳ":"u","ų":"u","ṷ":"u","ṵ":"u","ʉ":"u","ⓥ":"v","v":"v","ṽ":"v","ṿ":"v","ʋ":"v","ꝟ":"v","ʌ":"v","ꝡ":"vy","ⓦ":"w","w":"w","ẁ":"w","ẃ":"w","ŵ":"w","ẇ":"w","ẅ":"w","ẘ":"w","ẉ":"w","ⱳ":"w","ⓧ":"x","x":"x","ẋ":"x","ẍ":"x","ⓨ":"y","y":"y","ỳ":"y","ý":"y","ŷ":"y","ỹ":"y","ȳ":"y","ẏ":"y","ÿ":"y","ỷ":"y","ẙ":"y","ỵ":"y","ƴ":"y","ɏ":"y","ỿ":"y","ⓩ":"z","z":"z","ź":"z","ẑ":"z","ż":"z","ž":"z","ẓ":"z","ẕ":"z","ƶ":"z","ȥ":"z","ɀ":"z","ⱬ":"z","ꝣ":"z","Ά":"Α","Έ":"Ε","Ή":"Η","Ί":"Ι","Ϊ":"Ι","Ό":"Ο","Ύ":"Υ","Ϋ":"Υ","Ώ":"Ω","ά":"α","έ":"ε","ή":"η","ί":"ι","ϊ":"ι","ΐ":"ι","ό":"ο","ύ":"υ","ϋ":"υ","ΰ":"υ","ώ":"ω","ς":"σ","’":"'"}}),e.define("select2/data/base",["../utils"],function(r){function n(e,t){n.__super__.constructor.call(this)}return r.Extend(n,r.Observable),n.prototype.current=function(e){throw new Error("The `current` method must be defined in child classes.")},n.prototype.query=function(e,t){throw new Error("The `query` method must be defined in child classes.")},n.prototype.bind=function(e,t){},n.prototype.destroy=function(){},n.prototype.generateResultId=function(e,t){var n=e.id+"-result-";return n+=r.generateChars(4),null!=t.id?n+="-"+t.id.toString():n+="-"+r.generateChars(4),n},n}),e.define("select2/data/select",["./base","../utils","jquery"],function(e,a,l){function n(e,t){this.$element=e,this.options=t,n.__super__.constructor.call(this)}return a.Extend(n,e),n.prototype.current=function(e){var n=[],r=this;this.$element.find(":selected").each(function(){var e=l(this),t=r.item(e);n.push(t)}),e(n)},n.prototype.select=function(i){var o=this;if(i.selected=!0,l(i.element).is("option"))return i.element.selected=!0,void this.$element.trigger("change");if(this.$element.prop("multiple"))this.current(function(e){var t=[];(i=[i]).push.apply(i,e);for(var n=0;n<i.length;n++){var r=i[n].id;-1===l.inArray(r,t)&&t.push(r)}o.$element.val(t),o.$element.trigger("change")});else{var e=i.id;this.$element.val(e),this.$element.trigger("change")}},n.prototype.unselect=function(i){var o=this;if(this.$element.prop("multiple")){if(i.selected=!1,l(i.element).is("option"))return i.element.selected=!1,void this.$element.trigger("change");this.current(function(e){for(var t=[],n=0;n<e.length;n++){var r=e[n].id;r!==i.id&&-1===l.inArray(r,t)&&t.push(r)}o.$element.val(t),o.$element.trigger("change")})}},n.prototype.bind=function(e,t){var n=this;(this.container=e).on("select",function(e){n.select(e.data)}),e.on("unselect",function(e){n.unselect(e.data)})},n.prototype.destroy=function(){this.$element.find("*").each(function(){a.RemoveData(this)})},n.prototype.query=function(r,e){var i=[],o=this;this.$element.children().each(function(){var e=l(this);if(e.is("option")||e.is("optgroup")){var t=o.item(e),n=o.matches(r,t);null!==n&&i.push(n)}}),e({results:i})},n.prototype.addOptions=function(e){a.appendMany(this.$element,e)},n.prototype.option=function(e){var t;e.children?(t=document.createElement("optgroup")).label=e.text:void 0!==(t=document.createElement("option")).textContent?t.textContent=e.text:t.innerText=e.text,void 0!==e.id&&(t.value=e.id),e.disabled&&(t.disabled=!0),e.selected&&(t.selected=!0),e.title&&(t.title=e.title);var n=l(t),r=this._normalizeItem(e);return r.element=t,a.StoreData(t,"data",r),n},n.prototype.item=function(e){var t={};if(null!=(t=a.GetData(e[0],"data")))return t;if(e.is("option"))t={id:e.val(),text:e.text(),disabled:e.prop("disabled"),selected:e.prop("selected"),title:e.prop("title")};else if(e.is("optgroup")){t={text:e.prop("label"),children:[],title:e.prop("title")};for(var n=e.children("option"),r=[],i=0;i<n.length;i++){var o=l(n[i]),s=this.item(o);r.push(s)}t.children=r}return(t=this._normalizeItem(t)).element=e[0],a.StoreData(e[0],"data",t),t},n.prototype._normalizeItem=function(e){e!==Object(e)&&(e={id:e,text:e});return null!=(e=l.extend({},{text:""},e)).id&&(e.id=e.id.toString()),null!=e.text&&(e.text=e.text.toString()),null==e._resultId&&e.id&&null!=this.container&&(e._resultId=this.generateResultId(this.container,e)),l.extend({},{selected:!1,disabled:!1},e)},n.prototype.matches=function(e,t){return this.options.get("matcher")(e,t)},n}),e.define("select2/data/array",["./select","../utils","jquery"],function(e,f,g){function r(e,t){this._dataToConvert=t.get("data")||[],r.__super__.constructor.call(this,e,t)}return f.Extend(r,e),r.prototype.bind=function(e,t){r.__super__.bind.call(this,e,t),this.addOptions(this.convertToOptions(this._dataToConvert))},r.prototype.select=function(n){var e=this.$element.find("option").filter(function(e,t){return t.value==n.id.toString()});0===e.length&&(e=this.option(n),this.addOptions(e)),r.__super__.select.call(this,n)},r.prototype.convertToOptions=function(e){var t=this,n=this.$element.find("option"),r=n.map(function(){return t.item(g(this)).id}).get(),i=[];function o(e){return function(){return g(this).val()==e.id}}for(var s=0;s<e.length;s++){var a=this._normalizeItem(e[s]);if(0<=g.inArray(a.id,r)){var l=n.filter(o(a)),c=this.item(l),u=g.extend(!0,{},a,c),d=this.option(u);l.replaceWith(d)}else{var p=this.option(a);if(a.children){var h=this.convertToOptions(a.children);f.appendMany(p,h)}i.push(p)}}return i},r}),e.define("select2/data/ajax",["./array","../utils","jquery"],function(e,t,o){function n(e,t){this.ajaxOptions=this._applyDefaults(t.get("ajax")),null!=this.ajaxOptions.processResults&&(this.processResults=this.ajaxOptions.processResults),n.__super__.constructor.call(this,e,t)}return t.Extend(n,e),n.prototype._applyDefaults=function(e){var t={data:function(e){return o.extend({},e,{q:e.term})},transport:function(e,t,n){var r=o.ajax(e);return r.then(t),r.fail(n),r}};return o.extend({},t,e,!0)},n.prototype.processResults=function(e){return e},n.prototype.query=function(n,r){var i=this;null!=this._request&&(o.isFunction(this._request.abort)&&this._request.abort(),this._request=null);var t=o.extend({type:"GET"},this.ajaxOptions);function e(){var e=t.transport(t,function(e){var t=i.processResults(e,n);i.options.get("debug")&&window.console&&console.error&&(t&&t.results&&o.isArray(t.results)||console.error("Select2: The AJAX results did not return an array in the `results` key of the response.")),r(t)},function(){"status"in e&&(0===e.status||"0"===e.status)||i.trigger("results:message",{message:"errorLoading"})});i._request=e}"function"==typeof t.url&&(t.url=t.url.call(this.$element,n)),"function"==typeof t.data&&(t.data=t.data.call(this.$element,n)),this.ajaxOptions.delay&&null!=n.term?(this._queryTimeout&&window.clearTimeout(this._queryTimeout),this._queryTimeout=window.setTimeout(e,this.ajaxOptions.delay)):e()},n}),e.define("select2/data/tags",["jquery"],function(u){function e(e,t,n){var r=n.get("tags"),i=n.get("createTag");void 0!==i&&(this.createTag=i);var o=n.get("insertTag");if(void 0!==o&&(this.insertTag=o),e.call(this,t,n),u.isArray(r))for(var s=0;s<r.length;s++){var a=r[s],l=this._normalizeItem(a),c=this.option(l);this.$element.append(c)}}return e.prototype.query=function(e,c,u){var d=this;this._removeOldTags(),null!=c.term&&null==c.page?e.call(this,c,function e(t,n){for(var r=t.results,i=0;i<r.length;i++){var o=r[i],s=null!=o.children&&!e({results:o.children},!0);if((o.text||"").toUpperCase()===(c.term||"").toUpperCase()||s)return!n&&(t.data=r,void u(t))}if(n)return!0;var a=d.createTag(c);if(null!=a){var l=d.option(a);l.attr("data-select2-tag",!0),d.addOptions([l]),d.insertTag(r,a)}t.results=r,u(t)}):e.call(this,c,u)},e.prototype.createTag=function(e,t){var n=u.trim(t.term);return""===n?null:{id:n,text:n}},e.prototype.insertTag=function(e,t,n){t.unshift(n)},e.prototype._removeOldTags=function(e){this.$element.find("option[data-select2-tag]").each(function(){this.selected||u(this).remove()})},e}),e.define("select2/data/tokenizer",["jquery"],function(d){function e(e,t,n){var r=n.get("tokenizer");void 0!==r&&(this.tokenizer=r),e.call(this,t,n)}return e.prototype.bind=function(e,t,n){e.call(this,t,n),this.$search=t.dropdown.$search||t.selection.$search||n.find(".select2-search__field")},e.prototype.query=function(e,t,n){var r=this;t.term=t.term||"";var i=this.tokenizer(t,this.options,function(e){var t=r._normalizeItem(e);if(!r.$element.find("option").filter(function(){return d(this).val()===t.id}).length){var n=r.option(t);n.attr("data-select2-tag",!0),r._removeOldTags(),r.addOptions([n])}!function(e){r.trigger("select",{data:e})}(t)});i.term!==t.term&&(this.$search.length&&(this.$search.val(i.term),this.$search.trigger("focus")),t.term=i.term),e.call(this,t,n)},e.prototype.tokenizer=function(e,t,n,r){for(var i=n.get("tokenSeparators")||[],o=t.term,s=0,a=this.createTag||function(e){return{id:e.term,text:e.term}};s<o.length;){var l=o[s];if(-1!==d.inArray(l,i)){var c=o.substr(0,s),u=a(d.extend({},t,{term:c}));null!=u?(r(u),o=o.substr(s+1)||"",s=0):s++}else s++}return{term:o}},e}),e.define("select2/data/minimumInputLength",[],function(){function e(e,t,n){this.minimumInputLength=n.get("minimumInputLength"),e.call(this,t,n)}return e.prototype.query=function(e,t,n){t.term=t.term||"",t.term.length<this.minimumInputLength?this.trigger("results:message",{message:"inputTooShort",args:{minimum:this.minimumInputLength,input:t.term,params:t}}):e.call(this,t,n)},e}),e.define("select2/data/maximumInputLength",[],function(){function e(e,t,n){this.maximumInputLength=n.get("maximumInputLength"),e.call(this,t,n)}return e.prototype.query=function(e,t,n){t.term=t.term||"",0<this.maximumInputLength&&t.term.length>this.maximumInputLength?this.trigger("results:message",{message:"inputTooLong",args:{maximum:this.maximumInputLength,input:t.term,params:t}}):e.call(this,t,n)},e}),e.define("select2/data/maximumSelectionLength",[],function(){function e(e,t,n){this.maximumSelectionLength=n.get("maximumSelectionLength"),e.call(this,t,n)}return e.prototype.bind=function(e,t,n){var r=this;e.call(this,t,n),t.on("select",function(){r._checkIfMaximumSelected()})},e.prototype.query=function(e,t,n){var r=this;this._checkIfMaximumSelected(function(){e.call(r,t,n)})},e.prototype._checkIfMaximumSelected=function(e,n){var r=this;this.current(function(e){var t=null!=e?e.length:0;0<r.maximumSelectionLength&&t>=r.maximumSelectionLength?r.trigger("results:message",{message:"maximumSelected",args:{maximum:r.maximumSelectionLength}}):n&&n()})},e}),e.define("select2/dropdown",["jquery","./utils"],function(t,e){function n(e,t){this.$element=e,this.options=t,n.__super__.constructor.call(this)}return e.Extend(n,e.Observable),n.prototype.render=function(){var e=t('<span class="select2-dropdown"><span class="select2-results"></span></span>');return e.attr("dir",this.options.get("dir")),this.$dropdown=e},n.prototype.bind=function(){},n.prototype.position=function(e,t){},n.prototype.destroy=function(){this.$dropdown.remove()},n}),e.define("select2/dropdown/search",["jquery","../utils"],function(o,e){function t(){}return t.prototype.render=function(e){var t=e.call(this),n=o('<span class="select2-search select2-search--dropdown"><input class="select2-search__field" type="search" tabindex="-1" autocomplete="off" autocorrect="off" autocapitalize="none" spellcheck="false" role="searchbox" aria-autocomplete="list" /></span>');return this.$searchContainer=n,this.$search=n.find("input"),t.prepend(n),t},t.prototype.bind=function(e,t,n){var r=this,i=t.id+"-results";e.call(this,t,n),this.$search.on("keydown",function(e){r.trigger("keypress",e),r._keyUpPrevented=e.isDefaultPrevented()}),this.$search.on("input",function(e){o(this).off("keyup")}),this.$search.on("keyup input",function(e){r.handleSearch(e)}),t.on("open",function(){r.$search.attr("tabindex",0),r.$search.attr("aria-controls",i),r.$search.trigger("focus"),window.setTimeout(function(){r.$search.trigger("focus")},0)}),t.on("close",function(){r.$search.attr("tabindex",-1),r.$search.removeAttr("aria-controls"),r.$search.removeAttr("aria-activedescendant"),r.$search.val(""),r.$search.trigger("blur")}),t.on("focus",function(){t.isOpen()||r.$search.trigger("focus")}),t.on("results:all",function(e){null!=e.query.term&&""!==e.query.term||(r.showSearch(e)?r.$searchContainer.removeClass("select2-search--hide"):r.$searchContainer.addClass("select2-search--hide"))}),t.on("results:focus",function(e){e.data._resultId?r.$search.attr("aria-activedescendant",e.data._resultId):r.$search.removeAttr("aria-activedescendant")})},t.prototype.handleSearch=function(e){if(!this._keyUpPrevented){var t=this.$search.val();this.trigger("query",{term:t})}this._keyUpPrevented=!1},t.prototype.showSearch=function(e,t){return!0},t}),e.define("select2/dropdown/hidePlaceholder",[],function(){function e(e,t,n,r){this.placeholder=this.normalizePlaceholder(n.get("placeholder")),e.call(this,t,n,r)}return e.prototype.append=function(e,t){t.results=this.removePlaceholder(t.results),e.call(this,t)},e.prototype.normalizePlaceholder=function(e,t){return"string"==typeof t&&(t={id:"",text:t}),t},e.prototype.removePlaceholder=function(e,t){for(var n=t.slice(0),r=t.length-1;0<=r;r--){var i=t[r];this.placeholder.id===i.id&&n.splice(r,1)}return n},e}),e.define("select2/dropdown/infiniteScroll",["jquery"],function(n){function e(e,t,n,r){this.lastParams={},e.call(this,t,n,r),this.$loadingMore=this.createLoadingMore(),this.loading=!1}return e.prototype.append=function(e,t){this.$loadingMore.remove(),this.loading=!1,e.call(this,t),this.showLoadingMore(t)&&(this.$results.append(this.$loadingMore),this.loadMoreIfNeeded())},e.prototype.bind=function(e,t,n){var r=this;e.call(this,t,n),t.on("query",function(e){r.lastParams=e,r.loading=!0}),t.on("query:append",function(e){r.lastParams=e,r.loading=!0}),this.$results.on("scroll",this.loadMoreIfNeeded.bind(this))},e.prototype.loadMoreIfNeeded=function(){var e=n.contains(document.documentElement,this.$loadingMore[0]);if(!this.loading&&e){var t=this.$results.offset().top+this.$results.outerHeight(!1);this.$loadingMore.offset().top+this.$loadingMore.outerHeight(!1)<=t+50&&this.loadMore()}},e.prototype.loadMore=function(){this.loading=!0;var e=n.extend({},{page:1},this.lastParams);e.page++,this.trigger("query:append",e)},e.prototype.showLoadingMore=function(e,t){return t.pagination&&t.pagination.more},e.prototype.createLoadingMore=function(){var e=n('<li class="select2-results__option select2-results__option--load-more"role="option" aria-disabled="true"></li>'),t=this.options.get("translations").get("loadingMore");return e.html(t(this.lastParams)),e},e}),e.define("select2/dropdown/attachBody",["jquery","../utils"],function(f,a){function e(e,t,n){this.$dropdownParent=f(n.get("dropdownParent")||document.body),e.call(this,t,n)}return e.prototype.bind=function(e,t,n){var r=this;e.call(this,t,n),t.on("open",function(){r._showDropdown(),r._attachPositioningHandler(t),r._bindContainerResultHandlers(t)}),t.on("close",function(){r._hideDropdown(),r._detachPositioningHandler(t)}),this.$dropdownContainer.on("mousedown",function(e){e.stopPropagation()})},e.prototype.destroy=function(e){e.call(this),this.$dropdownContainer.remove()},e.prototype.position=function(e,t,n){t.attr("class",n.attr("class")),t.removeClass("select2"),t.addClass("select2-container--open"),t.css({position:"absolute",top:-999999}),this.$container=n},e.prototype.render=function(e){var t=f("<span></span>"),n=e.call(this);return t.append(n),this.$dropdownContainer=t},e.prototype._hideDropdown=function(e){this.$dropdownContainer.detach()},e.prototype._bindContainerResultHandlers=function(e,t){if(!this._containerResultsHandlersBound){var n=this;t.on("results:all",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("results:append",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("results:message",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("select",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("unselect",function(){n._positionDropdown(),n._resizeDropdown()}),this._containerResultsHandlersBound=!0}},e.prototype._attachPositioningHandler=function(e,t){var n=this,r="scroll.select2."+t.id,i="resize.select2."+t.id,o="orientationchange.select2."+t.id,s=this.$container.parents().filter(a.hasScroll);s.each(function(){a.StoreData(this,"select2-scroll-position",{x:f(this).scrollLeft(),y:f(this).scrollTop()})}),s.on(r,function(e){var t=a.GetData(this,"select2-scroll-position");f(this).scrollTop(t.y)}),f(window).on(r+" "+i+" "+o,function(e){n._positionDropdown(),n._resizeDropdown()})},e.prototype._detachPositioningHandler=function(e,t){var n="scroll.select2."+t.id,r="resize.select2."+t.id,i="orientationchange.select2."+t.id;this.$container.parents().filter(a.hasScroll).off(n),f(window).off(n+" "+r+" "+i)},e.prototype._positionDropdown=function(){var e=f(window),t=this.$dropdown.hasClass("select2-dropdown--above"),n=this.$dropdown.hasClass("select2-dropdown--below"),r=null,i=this.$container.offset();i.bottom=i.top+this.$container.outerHeight(!1);var o={height:this.$container.outerHeight(!1)};o.top=i.top,o.bottom=i.top+o.height;var s=this.$dropdown.outerHeight(!1),a=e.scrollTop(),l=e.scrollTop()+e.height(),c=a<i.top-s,u=l>i.bottom+s,d={left:i.left,top:o.bottom},p=this.$dropdownParent;"static"===p.css("position")&&(p=p.offsetParent());var h={top:0,left:0};(f.contains(document.body,p[0])||p[0].isConnected)&&(h=p.offset()),d.top-=h.top,d.left-=h.left,t||n||(r="below"),u||!c||t?!c&&u&&t&&(r="below"):r="above",("above"==r||t&&"below"!==r)&&(d.top=o.top-h.top-s),null!=r&&(this.$dropdown.removeClass("select2-dropdown--below select2-dropdown--above").addClass("select2-dropdown--"+r),this.$container.removeClass("select2-container--below select2-container--above").addClass("select2-container--"+r)),this.$dropdownContainer.css(d)},e.prototype._resizeDropdown=function(){var e={width:this.$container.outerWidth(!1)+"px"};this.options.get("dropdownAutoWidth")&&(e.minWidth=e.width,e.position="relative",e.width="auto"),this.$dropdown.css(e)},e.prototype._showDropdown=function(e){this.$dropdownContainer.appendTo(this.$dropdownParent),this._positionDropdown(),this._resizeDropdown()},e}),e.define("select2/dropdown/minimumResultsForSearch",[],function(){function e(e,t,n,r){this.minimumResultsForSearch=n.get("minimumResultsForSearch"),this.minimumResultsForSearch<0&&(this.minimumResultsForSearch=1/0),e.call(this,t,n,r)}return e.prototype.showSearch=function(e,t){return!(function e(t){for(var n=0,r=0;r<t.length;r++){var i=t[r];i.children?n+=e(i.children):n++}return n}(t.data.results)<this.minimumResultsForSearch)&&e.call(this,t)},e}),e.define("select2/dropdown/selectOnClose",["../utils"],function(o){function e(){}return e.prototype.bind=function(e,t,n){var r=this;e.call(this,t,n),t.on("close",function(e){r._handleSelectOnClose(e)})},e.prototype._handleSelectOnClose=function(e,t){if(t&&null!=t.originalSelect2Event){var n=t.originalSelect2Event;if("select"===n._type||"unselect"===n._type)return}var r=this.getHighlightedResults();if(!(r.length<1)){var i=o.GetData(r[0],"data");null!=i.element&&i.element.selected||null==i.element&&i.selected||this.trigger("select",{data:i})}},e}),e.define("select2/dropdown/closeOnSelect",[],function(){function e(){}return e.prototype.bind=function(e,t,n){var r=this;e.call(this,t,n),t.on("select",function(e){r._selectTriggered(e)}),t.on("unselect",function(e){r._selectTriggered(e)})},e.prototype._selectTriggered=function(e,t){var n=t.originalEvent;n&&(n.ctrlKey||n.metaKey)||this.trigger("close",{originalEvent:n,originalSelect2Event:t})},e}),e.define("select2/i18n/en",[],function(){return{errorLoading:function(){return"The results could not be loaded."},inputTooLong:function(e){var t=e.input.length-e.maximum,n="Please delete "+t+" character";return 1!=t&&(n+="s"),n},inputTooShort:function(e){return"Please enter "+(e.minimum-e.input.length)+" or more characters"},loadingMore:function(){return"Loading more results…"},maximumSelected:function(e){var t="You can only select "+e.maximum+" item";return 1!=e.maximum&&(t+="s"),t},noResults:function(){return"No results found"},searching:function(){return"Searching…"},removeAllItems:function(){return"Remove all items"}}}),e.define("select2/defaults",["jquery","require","./results","./selection/single","./selection/multiple","./selection/placeholder","./selection/allowClear","./selection/search","./selection/eventRelay","./utils","./translation","./diacritics","./data/select","./data/array","./data/ajax","./data/tags","./data/tokenizer","./data/minimumInputLength","./data/maximumInputLength","./data/maximumSelectionLength","./dropdown","./dropdown/search","./dropdown/hidePlaceholder","./dropdown/infiniteScroll","./dropdown/attachBody","./dropdown/minimumResultsForSearch","./dropdown/selectOnClose","./dropdown/closeOnSelect","./i18n/en"],function(c,u,d,p,h,f,g,m,v,y,s,t,_,$,w,b,A,x,D,S,E,C,O,T,q,L,I,j,e){function n(){this.reset()}return n.prototype.apply=function(e){if(null==(e=c.extend(!0,{},this.defaults,e)).dataAdapter){if(null!=e.ajax?e.dataAdapter=w:null!=e.data?e.dataAdapter=$:e.dataAdapter=_,0<e.minimumInputLength&&(e.dataAdapter=y.Decorate(e.dataAdapter,x)),0<e.maximumInputLength&&(e.dataAdapter=y.Decorate(e.dataAdapter,D)),0<e.maximumSelectionLength&&(e.dataAdapter=y.Decorate(e.dataAdapter,S)),e.tags&&(e.dataAdapter=y.Decorate(e.dataAdapter,b)),null==e.tokenSeparators&&null==e.tokenizer||(e.dataAdapter=y.Decorate(e.dataAdapter,A)),null!=e.query){var t=u(e.amdBase+"compat/query");e.dataAdapter=y.Decorate(e.dataAdapter,t)}if(null!=e.initSelection){var n=u(e.amdBase+"compat/initSelection");e.dataAdapter=y.Decorate(e.dataAdapter,n)}}if(null==e.resultsAdapter&&(e.resultsAdapter=d,null!=e.ajax&&(e.resultsAdapter=y.Decorate(e.resultsAdapter,T)),null!=e.placeholder&&(e.resultsAdapter=y.Decorate(e.resultsAdapter,O)),e.selectOnClose&&(e.resultsAdapter=y.Decorate(e.resultsAdapter,I))),null==e.dropdownAdapter){if(e.multiple)e.dropdownAdapter=E;else{var r=y.Decorate(E,C);e.dropdownAdapter=r}if(0!==e.minimumResultsForSearch&&(e.dropdownAdapter=y.Decorate(e.dropdownAdapter,L)),e.closeOnSelect&&(e.dropdownAdapter=y.Decorate(e.dropdownAdapter,j)),null!=e.dropdownCssClass||null!=e.dropdownCss||null!=e.adaptDropdownCssClass){var i=u(e.amdBase+"compat/dropdownCss");e.dropdownAdapter=y.Decorate(e.dropdownAdapter,i)}e.dropdownAdapter=y.Decorate(e.dropdownAdapter,q)}if(null==e.selectionAdapter){if(e.multiple?e.selectionAdapter=h:e.selectionAdapter=p,null!=e.placeholder&&(e.selectionAdapter=y.Decorate(e.selectionAdapter,f)),e.allowClear&&(e.selectionAdapter=y.Decorate(e.selectionAdapter,g)),e.multiple&&(e.selectionAdapter=y.Decorate(e.selectionAdapter,m)),null!=e.containerCssClass||null!=e.containerCss||null!=e.adaptContainerCssClass){var o=u(e.amdBase+"compat/containerCss");e.selectionAdapter=y.Decorate(e.selectionAdapter,o)}e.selectionAdapter=y.Decorate(e.selectionAdapter,v)}e.language=this._resolveLanguage(e.language),e.language.push("en");for(var s=[],a=0;a<e.language.length;a++){var l=e.language[a];-1===s.indexOf(l)&&s.push(l)}return e.language=s,e.translations=this._processTranslations(e.language,e.debug),e},n.prototype.reset=function(){function a(e){return e.replace(/[^\u0000-\u007E]/g,function(e){return t[e]||e})}this.defaults={amdBase:"./",amdLanguageBase:"./i18n/",closeOnSelect:!0,debug:!1,dropdownAutoWidth:!1,escapeMarkup:y.escapeMarkup,language:{},matcher:function e(t,n){if(""===c.trim(t.term))return n;if(n.children&&0<n.children.length){for(var r=c.extend(!0,{},n),i=n.children.length-1;0<=i;i--)null==e(t,n.children[i])&&r.children.splice(i,1);return 0<r.children.length?r:e(t,r)}var o=a(n.text).toUpperCase(),s=a(t.term).toUpperCase();return-1<o.indexOf(s)?n:null},minimumInputLength:0,maximumInputLength:0,maximumSelectionLength:0,minimumResultsForSearch:0,selectOnClose:!1,scrollAfterSelect:!1,sorter:function(e){return e},templateResult:function(e){return e.text},templateSelection:function(e){return e.text},theme:"default",width:"resolve"}},n.prototype.applyFromElement=function(e,t){var n=e.language,r=this.defaults.language,i=t.prop("lang"),o=t.closest("[lang]").prop("lang"),s=Array.prototype.concat.call(this._resolveLanguage(i),this._resolveLanguage(n),this._resolveLanguage(r),this._resolveLanguage(o));return e.language=s,e},n.prototype._resolveLanguage=function(e){if(!e)return[];if(c.isEmptyObject(e))return[];if(c.isPlainObject(e))return[e];var t;t=c.isArray(e)?e:[e];for(var n=[],r=0;r<t.length;r++)if(n.push(t[r]),"string"==typeof t[r]&&0<t[r].indexOf("-")){var i=t[r].split("-")[0];n.push(i)}return n},n.prototype._processTranslations=function(e,t){for(var n=new s,r=0;r<e.length;r++){var i=new s,o=e[r];if("string"==typeof o)try{i=s.loadPath(o)}catch(e){try{o=this.defaults.amdLanguageBase+o,i=s.loadPath(o)}catch(e){t&&window.console&&console.warn&&console.warn('Select2: The language file for "'+o+'" could not be automatically loaded. A fallback will be used instead.')}}else i=c.isPlainObject(o)?new s(o):o;n.extend(i)}return n},n.prototype.set=function(e,t){var n={};n[c.camelCase(e)]=t;var r=y._convertData(n);c.extend(!0,this.defaults,r)},new n}),e.define("select2/options",["require","jquery","./defaults","./utils"],function(r,d,i,p){function e(e,t){if(this.options=e,null!=t&&this.fromElement(t),null!=t&&(this.options=i.applyFromElement(this.options,t)),this.options=i.apply(this.options),t&&t.is("input")){var n=r(this.get("amdBase")+"compat/inputData");this.options.dataAdapter=p.Decorate(this.options.dataAdapter,n)}}return e.prototype.fromElement=function(e){var t=["select2"];null==this.options.multiple&&(this.options.multiple=e.prop("multiple")),null==this.options.disabled&&(this.options.disabled=e.prop("disabled")),null==this.options.dir&&(e.prop("dir")?this.options.dir=e.prop("dir"):e.closest("[dir]").prop("dir")?this.options.dir=e.closest("[dir]").prop("dir"):this.options.dir="ltr"),e.prop("disabled",this.options.disabled),e.prop("multiple",this.options.multiple),p.GetData(e[0],"select2Tags")&&(this.options.debug&&window.console&&console.warn&&console.warn('Select2: The `data-select2-tags` attribute has been changed to use the `data-data` and `data-tags="true"` attributes and will be removed in future versions of Select2.'),p.StoreData(e[0],"data",p.GetData(e[0],"select2Tags")),p.StoreData(e[0],"tags",!0)),p.GetData(e[0],"ajaxUrl")&&(this.options.debug&&window.console&&console.warn&&console.warn("Select2: The `data-ajax-url` attribute has been changed to `data-ajax--url` and support for the old attribute will be removed in future versions of Select2."),e.attr("ajax--url",p.GetData(e[0],"ajaxUrl")),p.StoreData(e[0],"ajax-Url",p.GetData(e[0],"ajaxUrl")));var n={};function r(e,t){return t.toUpperCase()}for(var i=0;i<e[0].attributes.length;i++){var o=e[0].attributes[i].name,s="data-";if(o.substr(0,s.length)==s){var a=o.substring(s.length),l=p.GetData(e[0],a);n[a.replace(/-([a-z])/g,r)]=l}}d.fn.jquery&&"1."==d.fn.jquery.substr(0,2)&&e[0].dataset&&(n=d.extend(!0,{},e[0].dataset,n));var c=d.extend(!0,{},p.GetData(e[0]),n);for(var u in c=p._convertData(c))-1<d.inArray(u,t)||(d.isPlainObject(this.options[u])?d.extend(this.options[u],c[u]):this.options[u]=c[u]);return this},e.prototype.get=function(e){return this.options[e]},e.prototype.set=function(e,t){this.options[e]=t},e}),e.define("select2/core",["jquery","./options","./utils","./keys"],function(i,c,u,r){var d=function(e,t){null!=u.GetData(e[0],"select2")&&u.GetData(e[0],"select2").destroy(),this.$element=e,this.id=this._generateId(e),t=t||{},this.options=new c(t,e),d.__super__.constructor.call(this);var n=e.attr("tabindex")||0;u.StoreData(e[0],"old-tabindex",n),e.attr("tabindex","-1");var r=this.options.get("dataAdapter");this.dataAdapter=new r(e,this.options);var i=this.render();this._placeContainer(i);var o=this.options.get("selectionAdapter");this.selection=new o(e,this.options),this.$selection=this.selection.render(),this.selection.position(this.$selection,i);var s=this.options.get("dropdownAdapter");this.dropdown=new s(e,this.options),this.$dropdown=this.dropdown.render(),this.dropdown.position(this.$dropdown,i);var a=this.options.get("resultsAdapter");this.results=new a(e,this.options,this.dataAdapter),this.$results=this.results.render(),this.results.position(this.$results,this.$dropdown);var l=this;this._bindAdapters(),this._registerDomEvents(),this._registerDataEvents(),this._registerSelectionEvents(),this._registerDropdownEvents(),this._registerResultsEvents(),this._registerEvents(),this.dataAdapter.current(function(e){l.trigger("selection:update",{data:e})}),e.addClass("select2-hidden-accessible"),e.attr("aria-hidden","true"),this._syncAttributes(),u.StoreData(e[0],"select2",this),e.data("select2",this)};return u.Extend(d,u.Observable),d.prototype._generateId=function(e){return"select2-"+(null!=e.attr("id")?e.attr("id"):null!=e.attr("name")?e.attr("name")+"-"+u.generateChars(2):u.generateChars(4)).replace(/(:|\.|\[|\]|,)/g,"")},d.prototype._placeContainer=function(e){e.insertAfter(this.$element);var t=this._resolveWidth(this.$element,this.options.get("width"));null!=t&&e.css("width",t)},d.prototype._resolveWidth=function(e,t){var n=/^width:(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/i;if("resolve"==t){var r=this._resolveWidth(e,"style");return null!=r?r:this._resolveWidth(e,"element")}if("element"==t){var i=e.outerWidth(!1);return i<=0?"auto":i+"px"}if("style"!=t)return"computedstyle"!=t?t:window.getComputedStyle(e[0]).width;var o=e.attr("style");if("string"!=typeof o)return null;for(var s=o.split(";"),a=0,l=s.length;a<l;a+=1){var c=s[a].replace(/\s/g,"").match(n);if(null!==c&&1<=c.length)return c[1]}return null},d.prototype._bindAdapters=function(){this.dataAdapter.bind(this,this.$container),this.selection.bind(this,this.$container),this.dropdown.bind(this,this.$container),this.results.bind(this,this.$container)},d.prototype._registerDomEvents=function(){var t=this;this.$element.on("change.select2",function(){t.dataAdapter.current(function(e){t.trigger("selection:update",{data:e})})}),this.$element.on("focus.select2",function(e){t.trigger("focus",e)}),this._syncA=u.bind(this._syncAttributes,this),this._syncS=u.bind(this._syncSubtree,this),this.$element[0].attachEvent&&this.$element[0].attachEvent("onpropertychange",this._syncA);var e=window.MutationObserver||window.WebKitMutationObserver||window.MozMutationObserver;null!=e?(this._observer=new e(function(e){i.each(e,t._syncA),i.each(e,t._syncS)}),this._observer.observe(this.$element[0],{attributes:!0,childList:!0,subtree:!1})):this.$element[0].addEventListener&&(this.$element[0].addEventListener("DOMAttrModified",t._syncA,!1),this.$element[0].addEventListener("DOMNodeInserted",t._syncS,!1),this.$element[0].addEventListener("DOMNodeRemoved",t._syncS,!1))},d.prototype._registerDataEvents=function(){var n=this;this.dataAdapter.on("*",function(e,t){n.trigger(e,t)})},d.prototype._registerSelectionEvents=function(){var n=this,r=["toggle","focus"];this.selection.on("toggle",function(){n.toggleDropdown()}),this.selection.on("focus",function(e){n.focus(e)}),this.selection.on("*",function(e,t){-1===i.inArray(e,r)&&n.trigger(e,t)})},d.prototype._registerDropdownEvents=function(){var n=this;this.dropdown.on("*",function(e,t){n.trigger(e,t)})},d.prototype._registerResultsEvents=function(){var n=this;this.results.on("*",function(e,t){n.trigger(e,t)})},d.prototype._registerEvents=function(){var n=this;this.on("open",function(){n.$container.addClass("select2-container--open")}),this.on("close",function(){n.$container.removeClass("select2-container--open")}),this.on("enable",function(){n.$container.removeClass("select2-container--disabled")}),this.on("disable",function(){n.$container.addClass("select2-container--disabled")}),this.on("blur",function(){n.$container.removeClass("select2-container--focus")}),this.on("query",function(t){n.isOpen()||n.trigger("open",{}),this.dataAdapter.query(t,function(e){n.trigger("results:all",{data:e,query:t})})}),this.on("query:append",function(t){this.dataAdapter.query(t,function(e){n.trigger("results:append",{data:e,query:t})})}),this.on("keypress",function(e){var t=e.which;n.isOpen()?t===r.ESC||t===r.TAB||t===r.UP&&e.altKey?(n.close(),e.preventDefault()):t===r.ENTER?(n.trigger("results:select",{}),e.preventDefault()):t===r.SPACE&&e.ctrlKey?(n.trigger("results:toggle",{}),e.preventDefault()):t===r.UP?(n.trigger("results:previous",{}),e.preventDefault()):t===r.DOWN&&(n.trigger("results:next",{}),e.preventDefault()):(t===r.ENTER||t===r.SPACE||t===r.DOWN&&e.altKey)&&(n.open(),e.preventDefault())})},d.prototype._syncAttributes=function(){this.options.set("disabled",this.$element.prop("disabled")),this.options.get("disabled")?(this.isOpen()&&this.close(),this.trigger("disable",{})):this.trigger("enable",{})},d.prototype._syncSubtree=function(e,t){var n=!1,r=this;if(!e||!e.target||"OPTION"===e.target.nodeName||"OPTGROUP"===e.target.nodeName){if(t)if(t.addedNodes&&0<t.addedNodes.length)for(var i=0;i<t.addedNodes.length;i++){t.addedNodes[i].selected&&(n=!0)}else t.removedNodes&&0<t.removedNodes.length&&(n=!0);else n=!0;n&&this.dataAdapter.current(function(e){r.trigger("selection:update",{data:e})})}},d.prototype.trigger=function(e,t){var n=d.__super__.trigger,r={open:"opening",close:"closing",select:"selecting",unselect:"unselecting",clear:"clearing"};if(void 0===t&&(t={}),e in r){var i=r[e],o={prevented:!1,name:e,args:t};if(n.call(this,i,o),o.prevented)return void(t.prevented=!0)}n.call(this,e,t)},d.prototype.toggleDropdown=function(){this.options.get("disabled")||(this.isOpen()?this.close():this.open())},d.prototype.open=function(){this.isOpen()||this.trigger("query",{})},d.prototype.close=function(){this.isOpen()&&this.trigger("close",{})},d.prototype.isOpen=function(){return this.$container.hasClass("select2-container--open")},d.prototype.hasFocus=function(){return this.$container.hasClass("select2-container--focus")},d.prototype.focus=function(e){this.hasFocus()||(this.$container.addClass("select2-container--focus"),this.trigger("focus",{}))},d.prototype.enable=function(e){this.options.get("debug")&&window.console&&console.warn&&console.warn('Select2: The `select2("enable")` method has been deprecated and will be removed in later Select2 versions. Use $element.prop("disabled") instead.'),null!=e&&0!==e.length||(e=[!0]);var t=!e[0];this.$element.prop("disabled",t)},d.prototype.data=function(){this.options.get("debug")&&0<arguments.length&&window.console&&console.warn&&console.warn('Select2: Data can no longer be set using `select2("data")`. You should consider setting the value instead using `$element.val()`.');var t=[];return this.dataAdapter.current(function(e){t=e}),t},d.prototype.val=function(e){if(this.options.get("debug")&&window.console&&console.warn&&console.warn('Select2: The `select2("val")` method has been deprecated and will be removed in later Select2 versions. Use $element.val() instead.'),null==e||0===e.length)return this.$element.val();var t=e[0];i.isArray(t)&&(t=i.map(t,function(e){return e.toString()})),this.$element.val(t).trigger("change")},d.prototype.destroy=function(){this.$container.remove(),this.$element[0].detachEvent&&this.$element[0].detachEvent("onpropertychange",this._syncA),null!=this._observer?(this._observer.disconnect(),this._observer=null):this.$element[0].removeEventListener&&(this.$element[0].removeEventListener("DOMAttrModified",this._syncA,!1),this.$element[0].removeEventListener("DOMNodeInserted",this._syncS,!1),this.$element[0].removeEventListener("DOMNodeRemoved",this._syncS,!1)),this._syncA=null,this._syncS=null,this.$element.off(".select2"),this.$element.attr("tabindex",u.GetData(this.$element[0],"old-tabindex")),this.$element.removeClass("select2-hidden-accessible"),this.$element.attr("aria-hidden","false"),u.RemoveData(this.$element[0]),this.$element.removeData("select2"),this.dataAdapter.destroy(),this.selection.destroy(),this.dropdown.destroy(),this.results.destroy(),this.dataAdapter=null,this.selection=null,this.dropdown=null,this.results=null},d.prototype.render=function(){var e=i('<span class="select2 select2-container"><span class="selection"></span><span class="dropdown-wrapper" aria-hidden="true"></span></span>');return e.attr("dir",this.options.get("dir")),this.$container=e,this.$container.addClass("select2-container--"+this.options.get("theme")),u.StoreData(e[0],"element",this.$element),e},d}),e.define("jquery-mousewheel",["jquery"],function(e){return e}),e.define("jquery.select2",["jquery","jquery-mousewheel","./select2/core","./select2/defaults","./select2/utils"],function(i,e,o,t,s){if(null==i.fn.select2){var a=["open","close","destroy"];i.fn.select2=function(t){if("object"==typeof(t=t||{}))return this.each(function(){var e=i.extend(!0,{},t);new o(i(this),e)}),this;if("string"!=typeof t)throw new Error("Invalid arguments for Select2: "+t);var n,r=Array.prototype.slice.call(arguments,1);return this.each(function(){var e=s.GetData(this,"select2");null==e&&window.console&&console.error&&console.error("The select2('"+t+"') method was called on an element that is not using Select2."),n=e[t].apply(e,r)}),-1<i.inArray(t,a)?this:n}}return null==i.fn.select2.defaults&&(i.fn.select2.defaults=t),o}),{define:e.define,require:e.require}}(),t=e.require("jquery.select2");return u.fn.select2.amd=e,t});
// </nowiki>
b74rqbtcbbumj4r1yylbr46ndagz0vr
769525
769458
2026-10-22T00:00:04Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 1406828: rm cmt
769525
javascript
text/javascript
// <nowiki>
/*! Select2 4.0.12 | https://github.com/select2/select2/blob/master/LICENSE.md */
!function(n){"function"==typeof define&&define.amd?define(["jquery"],n):/*"object"==typeof module&&module.exports?module.exports=function(e,t){return void 0===t&&(t="undefined"!=typeof window?require("jquery"):require("jquery")(e)),n(t),t}:*/n(jQuery)}(function(u){var e=function(){if(u&&u.fn&&u.fn.select2&&u.fn.select2.amd)var e=u.fn.select2.amd;var t,n,r,h,o,s,f,g,m,v,y,_,i,a,w;function b(e,t){return i.call(e,t)}function l(e,t){var n,r,i,o,s,a,l,c,u,d,p,h=t&&t.split("/"),f=y.map,g=f&&f["*"]||{};if(e){for(s=(e=e.split("/")).length-1,y.nodeIdCompat&&w.test(e[s])&&(e[s]=e[s].replace(w,"")),"."===e[0].charAt(0)&&h&&(e=h.slice(0,h.length-1).concat(e)),u=0;u<e.length;u++)if("."===(p=e[u]))e.splice(u,1),u-=1;else if(".."===p){if(0===u||1===u&&".."===e[2]||".."===e[u-1])continue;0<u&&(e.splice(u-1,2),u-=2)}e=e.join("/")}if((h||g)&&f){for(u=(n=e.split("/")).length;0<u;u-=1){if(r=n.slice(0,u).join("/"),h)for(d=h.length;0<d;d-=1)if(i=(i=f[h.slice(0,d).join("/")])&&i[r]){o=i,a=u;break}if(o)break;!l&&g&&g[r]&&(l=g[r],c=u)}!o&&l&&(o=l,a=c),o&&(n.splice(0,a,o),e=n.join("/"))}return e}function A(t,n){return function(){var e=a.call(arguments,0);return"string"!=typeof e[0]&&1===e.length&&e.push(null),s.apply(h,e.concat([t,n]))}}function x(t){return function(e){m[t]=e}}function D(e){if(b(v,e)){var t=v[e];delete v[e],_[e]=!0,o.apply(h,t)}if(!b(m,e)&&!b(_,e))throw new Error("No "+e);return m[e]}function c(e){var t,n=e?e.indexOf("!"):-1;return-1<n&&(t=e.substring(0,n),e=e.substring(n+1,e.length)),[t,e]}function S(e){return e?c(e):[]}return e&&e.requirejs||(e?n=e:e={},m={},v={},y={},_={},i=Object.prototype.hasOwnProperty,a=[].slice,w=/\.js$/,f=function(e,t){var n,r=c(e),i=r[0],o=t[1];return e=r[1],i&&(n=D(i=l(i,o))),i?e=n&&n.normalize?n.normalize(e,function(t){return function(e){return l(e,t)}}(o)):l(e,o):(i=(r=c(e=l(e,o)))[0],e=r[1],i&&(n=D(i))),{f:i?i+"!"+e:e,n:e,pr:i,p:n}},g={require:function(e){return A(e)},exports:function(e){var t=m[e];return void 0!==t?t:m[e]={}},module:function(e){return{id:e,uri:"",exports:m[e],config:function(e){return function(){return y&&y.config&&y.config[e]||{}}}(e)}}},o=function(e,t,n,r){var i,o,s,a,l,c,u,d=[],p=typeof n;if(c=S(r=r||e),"undefined"==p||"function"==p){for(t=!t.length&&n.length?["require","exports","module"]:t,l=0;l<t.length;l+=1)if("require"===(o=(a=f(t[l],c)).f))d[l]=g.require(e);else if("exports"===o)d[l]=g.exports(e),u=!0;else if("module"===o)i=d[l]=g.module(e);else if(b(m,o)||b(v,o)||b(_,o))d[l]=D(o);else{if(!a.p)throw new Error(e+" missing "+o);a.p.load(a.n,A(r,!0),x(o),{}),d[l]=m[o]}s=n?n.apply(m[e],d):void 0,e&&(i&&i.exports!==h&&i.exports!==m[e]?m[e]=i.exports:s===h&&u||(m[e]=s))}else e&&(m[e]=n)},t=n=s=function(e,t,n,r,i){if("string"==typeof e)return g[e]?g[e](t):D(f(e,S(t)).f);if(!e.splice){if((y=e).deps&&s(y.deps,y.callback),!t)return;t.splice?(e=t,t=n,n=null):e=h}return t=t||function(){},"function"==typeof n&&(n=r,r=i),r?o(h,e,t,n):setTimeout(function(){o(h,e,t,n)},4),s},s.config=function(e){return s(e)},t._defined=m,(r=function(e,t,n){if("string"!=typeof e)throw new Error("See almond README: incorrect module build, no module name");t.splice||(n=t,t=[]),b(m,e)||b(v,e)||(v[e]=[e,t,n])}).amd={jQuery:!0},e.requirejs=t,e.require=n,e.define=r),e.define("almond",function(){}),e.define("jquery",[],function(){var e=u||$;return null==e&&console&&console.error&&console.error("Select2: An instance of jQuery or a jQuery-compatible library was not found. Make sure that you are including jQuery before Select2 on your web page."),e}),e.define("select2/utils",["jquery"],function(o){var i={};function u(e){var t=e.prototype,n=[];for(var r in t){"function"==typeof t[r]&&"constructor"!==r&&n.push(r)}return n}i.Extend=function(e,t){var n={}.hasOwnProperty;function r(){this.constructor=e}for(var i in t)n.call(t,i)&&(e[i]=t[i]);return r.prototype=t.prototype,e.prototype=new r,e.__super__=t.prototype,e},i.Decorate=function(r,i){var e=u(i),t=u(r);function o(){var e=Array.prototype.unshift,t=i.prototype.constructor.length,n=r.prototype.constructor;0<t&&(e.call(arguments,r.prototype.constructor),n=i.prototype.constructor),n.apply(this,arguments)}i.displayName=r.displayName,o.prototype=new function(){this.constructor=o};for(var n=0;n<t.length;n++){var s=t[n];o.prototype[s]=r.prototype[s]}function a(e){var t=function(){};e in o.prototype&&(t=o.prototype[e]);var n=i.prototype[e];return function(){return Array.prototype.unshift.call(arguments,t),n.apply(this,arguments)}}for(var l=0;l<e.length;l++){var c=e[l];o.prototype[c]=a(c)}return o};function e(){this.listeners={}}e.prototype.on=function(e,t){this.listeners=this.listeners||{},e in this.listeners?this.listeners[e].push(t):this.listeners[e]=[t]},e.prototype.trigger=function(e){var t=Array.prototype.slice,n=t.call(arguments,1);this.listeners=this.listeners||{},null==n&&(n=[]),0===n.length&&n.push({}),(n[0]._type=e)in this.listeners&&this.invoke(this.listeners[e],t.call(arguments,1)),"*"in this.listeners&&this.invoke(this.listeners["*"],arguments)},e.prototype.invoke=function(e,t){for(var n=0,r=e.length;n<r;n++)e[n].apply(this,t)},i.Observable=e,i.generateChars=function(e){for(var t="",n=0;n<e;n++){t+=Math.floor(36*Math.random()).toString(36)}return t},i.bind=function(e,t){return function(){e.apply(t,arguments)}},i._convertData=function(e){for(var t in e){var n=t.split("-"),r=e;if(1!==n.length){for(var i=0;i<n.length;i++){var o=n[i];(o=o.substring(0,1).toLowerCase()+o.substring(1))in r||(r[o]={}),i==n.length-1&&(r[o]=e[t]),r=r[o]}delete e[t]}}return e},i.hasScroll=function(e,t){var n=o(t),r=t.style.overflowX,i=t.style.overflowY;return(r!==i||"hidden"!==i&&"visible"!==i)&&("scroll"===r||"scroll"===i||(n.innerHeight()<t.scrollHeight||n.innerWidth()<t.scrollWidth))},i.escapeMarkup=function(e){var t={"\\":"\","&":"&","<":"<",">":">",'"':""","'":"'","/":"/"};return"string"!=typeof e?e:String(e).replace(/[&<>"'\/\\]/g,function(e){return t[e]})},i.appendMany=function(e,t){if("1.7"===o.fn.jquery.substr(0,3)){var n=o();o.map(t,function(e){n=n.add(e)}),t=n}e.append(t)},i.__cache={};var n=0;return i.GetUniqueElementId=function(e){var t=e.getAttribute("data-select2-id");return null==t&&(e.id?(t=e.id,e.setAttribute("data-select2-id",t)):(e.setAttribute("data-select2-id",++n),t=n.toString())),t},i.StoreData=function(e,t,n){var r=i.GetUniqueElementId(e);i.__cache[r]||(i.__cache[r]={}),i.__cache[r][t]=n},i.GetData=function(e,t){var n=i.GetUniqueElementId(e);return t?i.__cache[n]&&null!=i.__cache[n][t]?i.__cache[n][t]:o(e).data(t):i.__cache[n]},i.RemoveData=function(e){var t=i.GetUniqueElementId(e);null!=i.__cache[t]&&delete i.__cache[t],e.removeAttribute("data-select2-id")},i}),e.define("select2/results",["jquery","./utils"],function(h,f){function r(e,t,n){this.$element=e,this.data=n,this.options=t,r.__super__.constructor.call(this)}return f.Extend(r,f.Observable),r.prototype.render=function(){var e=h('<ul class="select2-results__options" role="listbox"></ul>');return this.options.get("multiple")&&e.attr("aria-multiselectable","true"),this.$results=e},r.prototype.clear=function(){this.$results.empty()},r.prototype.displayMessage=function(e){var t=this.options.get("escapeMarkup");this.clear(),this.hideLoading();var n=h('<li role="alert" aria-live="assertive" class="select2-results__option"></li>'),r=this.options.get("translations").get(e.message);n.append(t(r(e.args))),n[0].className+=" select2-results__message",this.$results.append(n)},r.prototype.hideMessages=function(){this.$results.find(".select2-results__message").remove()},r.prototype.append=function(e){this.hideLoading();var t=[];if(null!=e.results&&0!==e.results.length){e.results=this.sort(e.results);for(var n=0;n<e.results.length;n++){var r=e.results[n],i=this.option(r);t.push(i)}this.$results.append(t)}else 0===this.$results.children().length&&this.trigger("results:message",{message:"noResults"})},r.prototype.position=function(e,t){t.find(".select2-results").append(e)},r.prototype.sort=function(e){return this.options.get("sorter")(e)},r.prototype.highlightFirstItem=function(){var e=this.$results.find(".select2-results__option[aria-selected]"),t=e.filter("[aria-selected=true]");0<t.length?t.first().trigger("mouseenter"):e.first().trigger("mouseenter"),this.ensureHighlightVisible()},r.prototype.setClasses=function(){var t=this;this.data.current(function(e){var r=h.map(e,function(e){return e.id.toString()});t.$results.find(".select2-results__option[aria-selected]").each(function(){var e=h(this),t=f.GetData(this,"data"),n=""+t.id;null!=t.element&&t.element.selected||null==t.element&&-1<h.inArray(n,r)?e.attr("aria-selected","true"):e.attr("aria-selected","false")})})},r.prototype.showLoading=function(e){this.hideLoading();var t={disabled:!0,loading:!0,text:this.options.get("translations").get("searching")(e)},n=this.option(t);n.className+=" loading-results",this.$results.prepend(n)},r.prototype.hideLoading=function(){this.$results.find(".loading-results").remove()},r.prototype.option=function(e){var t=document.createElement("li");t.className="select2-results__option";var n={role:"option","aria-selected":"false"},r=window.Element.prototype.matches||window.Element.prototype.msMatchesSelector||window.Element.prototype.webkitMatchesSelector;for(var i in(null!=e.element&&r.call(e.element,":disabled")||null==e.element&&e.disabled)&&(delete n["aria-selected"],n["aria-disabled"]="true"),null==e.id&&delete n["aria-selected"],null!=e._resultId&&(t.id=e._resultId),e.title&&(t.title=e.title),e.children&&(n.role="group",n["aria-label"]=e.text,delete n["aria-selected"]),n){var o=n[i];t.setAttribute(i,o)}if(e.children){var s=h(t),a=document.createElement("strong");a.className="select2-results__group";h(a);this.template(e,a);for(var l=[],c=0;c<e.children.length;c++){var u=e.children[c],d=this.option(u);l.push(d)}var p=h("<ul></ul>",{class:"select2-results__options select2-results__options--nested"});p.append(l),s.append(a),s.append(p)}else this.template(e,t);return f.StoreData(t,"data",e),t},r.prototype.bind=function(t,e){var l=this,n=t.id+"-results";this.$results.attr("id",n),t.on("results:all",function(e){l.clear(),l.append(e.data),t.isOpen()&&(l.setClasses(),l.highlightFirstItem())}),t.on("results:append",function(e){l.append(e.data),t.isOpen()&&l.setClasses()}),t.on("query",function(e){l.hideMessages(),l.showLoading(e)}),t.on("select",function(){t.isOpen()&&(l.setClasses(),l.options.get("scrollAfterSelect")&&l.highlightFirstItem())}),t.on("unselect",function(){t.isOpen()&&(l.setClasses(),l.options.get("scrollAfterSelect")&&l.highlightFirstItem())}),t.on("open",function(){l.$results.attr("aria-expanded","true"),l.$results.attr("aria-hidden","false"),l.setClasses(),l.ensureHighlightVisible()}),t.on("close",function(){l.$results.attr("aria-expanded","false"),l.$results.attr("aria-hidden","true"),l.$results.removeAttr("aria-activedescendant")}),t.on("results:toggle",function(){var e=l.getHighlightedResults();0!==e.length&&e.trigger("mouseup")}),t.on("results:select",function(){var e=l.getHighlightedResults();if(0!==e.length){var t=f.GetData(e[0],"data");"true"==e.attr("aria-selected")?l.trigger("close",{}):l.trigger("select",{data:t})}}),t.on("results:previous",function(){var e=l.getHighlightedResults(),t=l.$results.find("[aria-selected]"),n=t.index(e);if(!(n<=0)){var r=n-1;0===e.length&&(r=0);var i=t.eq(r);i.trigger("mouseenter");var o=l.$results.offset().top,s=i.offset().top,a=l.$results.scrollTop()+(s-o);0===r?l.$results.scrollTop(0):s-o<0&&l.$results.scrollTop(a)}}),t.on("results:next",function(){var e=l.getHighlightedResults(),t=l.$results.find("[aria-selected]"),n=t.index(e)+1;if(!(n>=t.length)){var r=t.eq(n);r.trigger("mouseenter");var i=l.$results.offset().top+l.$results.outerHeight(!1),o=r.offset().top+r.outerHeight(!1),s=l.$results.scrollTop()+o-i;0===n?l.$results.scrollTop(0):i<o&&l.$results.scrollTop(s)}}),t.on("results:focus",function(e){e.element.addClass("select2-results__option--highlighted")}),t.on("results:message",function(e){l.displayMessage(e)}),h.fn.mousewheel&&this.$results.on("mousewheel",function(e){var t=l.$results.scrollTop(),n=l.$results.get(0).scrollHeight-t+e.deltaY,r=0<e.deltaY&&t-e.deltaY<=0,i=e.deltaY<0&&n<=l.$results.height();r?(l.$results.scrollTop(0),e.preventDefault(),e.stopPropagation()):i&&(l.$results.scrollTop(l.$results.get(0).scrollHeight-l.$results.height()),e.preventDefault(),e.stopPropagation())}),this.$results.on("mouseup",".select2-results__option[aria-selected]",function(e){var t=h(this),n=f.GetData(this,"data");"true"!==t.attr("aria-selected")?l.trigger("select",{originalEvent:e,data:n}):l.options.get("multiple")?l.trigger("unselect",{originalEvent:e,data:n}):l.trigger("close",{})}),this.$results.on("mouseenter",".select2-results__option[aria-selected]",function(e){var t=f.GetData(this,"data");l.getHighlightedResults().removeClass("select2-results__option--highlighted"),l.trigger("results:focus",{data:t,element:h(this)})})},r.prototype.getHighlightedResults=function(){return this.$results.find(".select2-results__option--highlighted")},r.prototype.destroy=function(){this.$results.remove()},r.prototype.ensureHighlightVisible=function(){var e=this.getHighlightedResults();if(0!==e.length){var t=this.$results.find("[aria-selected]").index(e),n=this.$results.offset().top,r=e.offset().top,i=this.$results.scrollTop()+(r-n),o=r-n;i-=2*e.outerHeight(!1),t<=2?this.$results.scrollTop(0):(o>this.$results.outerHeight()||o<0)&&this.$results.scrollTop(i)}},r.prototype.template=function(e,t){var n=this.options.get("templateResult"),r=this.options.get("escapeMarkup"),i=n(e,t);null==i?t.style.display="none":"string"==typeof i?t.innerHTML=r(i):h(t).append(i)},r}),e.define("select2/keys",[],function(){return{BACKSPACE:8,TAB:9,ENTER:13,SHIFT:16,CTRL:17,ALT:18,ESC:27,SPACE:32,PAGE_UP:33,PAGE_DOWN:34,END:35,HOME:36,LEFT:37,UP:38,RIGHT:39,DOWN:40,DELETE:46}}),e.define("select2/selection/base",["jquery","../utils","../keys"],function(n,r,i){function o(e,t){this.$element=e,this.options=t,o.__super__.constructor.call(this)}return r.Extend(o,r.Observable),o.prototype.render=function(){var e=n('<span class="select2-selection" role="combobox" aria-haspopup="true" aria-expanded="false"></span>');return this._tabindex=0,null!=r.GetData(this.$element[0],"old-tabindex")?this._tabindex=r.GetData(this.$element[0],"old-tabindex"):null!=this.$element.attr("tabindex")&&(this._tabindex=this.$element.attr("tabindex")),e.attr("title",this.$element.attr("title")),e.attr("tabindex",this._tabindex),e.attr("aria-disabled","false"),this.$selection=e},o.prototype.bind=function(e,t){var n=this,r=e.id+"-results";this.container=e,this.$selection.on("focus",function(e){n.trigger("focus",e)}),this.$selection.on("blur",function(e){n._handleBlur(e)}),this.$selection.on("keydown",function(e){n.trigger("keypress",e),e.which===i.SPACE&&e.preventDefault()}),e.on("results:focus",function(e){n.$selection.attr("aria-activedescendant",e.data._resultId)}),e.on("selection:update",function(e){n.update(e.data)}),e.on("open",function(){n.$selection.attr("aria-expanded","true"),n.$selection.attr("aria-owns",r),n._attachCloseHandler(e)}),e.on("close",function(){n.$selection.attr("aria-expanded","false"),n.$selection.removeAttr("aria-activedescendant"),n.$selection.removeAttr("aria-owns"),n.$selection.trigger("focus"),n._detachCloseHandler(e)}),e.on("enable",function(){n.$selection.attr("tabindex",n._tabindex),n.$selection.attr("aria-disabled","false")}),e.on("disable",function(){n.$selection.attr("tabindex","-1"),n.$selection.attr("aria-disabled","true")})},o.prototype._handleBlur=function(e){var t=this;window.setTimeout(function(){document.activeElement==t.$selection[0]||n.contains(t.$selection[0],document.activeElement)||t.trigger("blur",e)},1)},o.prototype._attachCloseHandler=function(e){n(document.body).on("mousedown.select2."+e.id,function(e){var t=n(e.target).closest(".select2");n(".select2.select2-container--open").each(function(){this!=t[0]&&r.GetData(this,"element").select2("close")})})},o.prototype._detachCloseHandler=function(e){n(document.body).off("mousedown.select2."+e.id)},o.prototype.position=function(e,t){t.find(".selection").append(e)},o.prototype.destroy=function(){this._detachCloseHandler(this.container)},o.prototype.update=function(e){throw new Error("The `update` method must be defined in child classes.")},o}),e.define("select2/selection/single",["jquery","./base","../utils","../keys"],function(e,t,n,r){function i(){i.__super__.constructor.apply(this,arguments)}return n.Extend(i,t),i.prototype.render=function(){var e=i.__super__.render.call(this);return e.addClass("select2-selection--single"),e.html('<span class="select2-selection__rendered"></span><span class="select2-selection__arrow" role="presentation"><b role="presentation"></b></span>'),e},i.prototype.bind=function(t,e){var n=this;i.__super__.bind.apply(this,arguments);var r=t.id+"-container";this.$selection.find(".select2-selection__rendered").attr("id",r).attr("role","textbox").attr("aria-readonly","true"),this.$selection.attr("aria-labelledby",r),this.$selection.on("mousedown",function(e){1===e.which&&n.trigger("toggle",{originalEvent:e})}),this.$selection.on("focus",function(e){}),this.$selection.on("blur",function(e){}),t.on("focus",function(e){t.isOpen()||n.$selection.trigger("focus")})},i.prototype.clear=function(){var e=this.$selection.find(".select2-selection__rendered");e.empty(),e.removeAttr("title")},i.prototype.display=function(e,t){var n=this.options.get("templateSelection");return this.options.get("escapeMarkup")(n(e,t))},i.prototype.selectionContainer=function(){return e("<span></span>")},i.prototype.update=function(e){if(0!==e.length){var t=e[0],n=this.$selection.find(".select2-selection__rendered"),r=this.display(t,n);n.empty().append(r);var i=t.title||t.text;i?n.attr("title",i):n.removeAttr("title")}else this.clear()},i}),e.define("select2/selection/multiple",["jquery","./base","../utils"],function(i,e,l){function n(e,t){n.__super__.constructor.apply(this,arguments)}return l.Extend(n,e),n.prototype.render=function(){var e=n.__super__.render.call(this);return e.addClass("select2-selection--multiple"),e.html('<ul class="select2-selection__rendered"></ul>'),e},n.prototype.bind=function(e,t){var r=this;n.__super__.bind.apply(this,arguments),this.$selection.on("click",function(e){r.trigger("toggle",{originalEvent:e})}),this.$selection.on("click",".select2-selection__choice__remove",function(e){if(!r.options.get("disabled")){var t=i(this).parent(),n=l.GetData(t[0],"data");r.trigger("unselect",{originalEvent:e,data:n})}})},n.prototype.clear=function(){var e=this.$selection.find(".select2-selection__rendered");e.empty(),e.removeAttr("title")},n.prototype.display=function(e,t){var n=this.options.get("templateSelection");return this.options.get("escapeMarkup")(n(e,t))},n.prototype.selectionContainer=function(){return i('<li class="select2-selection__choice"><span class="select2-selection__choice__remove" role="presentation">×</span></li>')},n.prototype.update=function(e){if(this.clear(),0!==e.length){for(var t=[],n=0;n<e.length;n++){var r=e[n],i=this.selectionContainer(),o=this.display(r,i);i.append(o);var s=r.title||r.text;s&&i.attr("title",s),l.StoreData(i[0],"data",r),t.push(i)}var a=this.$selection.find(".select2-selection__rendered");l.appendMany(a,t)}},n}),e.define("select2/selection/placeholder",["../utils"],function(e){function t(e,t,n){this.placeholder=this.normalizePlaceholder(n.get("placeholder")),e.call(this,t,n)}return t.prototype.normalizePlaceholder=function(e,t){return"string"==typeof t&&(t={id:"",text:t}),t},t.prototype.createPlaceholder=function(e,t){var n=this.selectionContainer();return n.html(this.display(t)),n.addClass("select2-selection__placeholder").removeClass("select2-selection__choice"),n},t.prototype.update=function(e,t){var n=1==t.length&&t[0].id!=this.placeholder.id;if(1<t.length||n)return e.call(this,t);this.clear();var r=this.createPlaceholder(this.placeholder);this.$selection.find(".select2-selection__rendered").append(r)},t}),e.define("select2/selection/allowClear",["jquery","../keys","../utils"],function(i,r,a){function e(){}return e.prototype.bind=function(e,t,n){var r=this;e.call(this,t,n),null==this.placeholder&&this.options.get("debug")&&window.console&&console.error&&console.error("Select2: The `allowClear` option should be used in combination with the `placeholder` option."),this.$selection.on("mousedown",".select2-selection__clear",function(e){r._handleClear(e)}),t.on("keypress",function(e){r._handleKeyboardClear(e,t)})},e.prototype._handleClear=function(e,t){if(!this.options.get("disabled")){var n=this.$selection.find(".select2-selection__clear");if(0!==n.length){t.stopPropagation();var r=a.GetData(n[0],"data"),i=this.$element.val();this.$element.val(this.placeholder.id);var o={data:r};if(this.trigger("clear",o),o.prevented)this.$element.val(i);else{for(var s=0;s<r.length;s++)if(o={data:r[s]},this.trigger("unselect",o),o.prevented)return void this.$element.val(i);this.$element.trigger("change"),this.trigger("toggle",{})}}}},e.prototype._handleKeyboardClear=function(e,t,n){n.isOpen()||t.which!=r.DELETE&&t.which!=r.BACKSPACE||this._handleClear(t)},e.prototype.update=function(e,t){if(e.call(this,t),!(0<this.$selection.find(".select2-selection__placeholder").length||0===t.length)){var n=this.options.get("translations").get("removeAllItems"),r=i('<span class="select2-selection__clear" title="'+n()+'">×</span>');a.StoreData(r[0],"data",t),this.$selection.find(".select2-selection__rendered").prepend(r)}},e}),e.define("select2/selection/search",["jquery","../utils","../keys"],function(r,a,l){function e(e,t,n){e.call(this,t,n)}return e.prototype.render=function(e){var t=r('<li class="select2-search select2-search--inline"><input class="select2-search__field" type="search" tabindex="-1" autocomplete="off" autocorrect="off" autocapitalize="none" spellcheck="false" role="searchbox" aria-autocomplete="list" /></li>');this.$searchContainer=t,this.$search=t.find("input");var n=e.call(this);return this._transferTabIndex(),n},e.prototype.bind=function(e,t,n){var r=this,i=t.id+"-results";e.call(this,t,n),t.on("open",function(){r.$search.attr("aria-controls",i),r.$search.trigger("focus")}),t.on("close",function(){r.$search.val(""),r.$search.removeAttr("aria-controls"),r.$search.removeAttr("aria-activedescendant"),r.$search.trigger("focus")}),t.on("enable",function(){r.$search.prop("disabled",!1),r._transferTabIndex()}),t.on("disable",function(){r.$search.prop("disabled",!0)}),t.on("focus",function(e){r.$search.trigger("focus")}),t.on("results:focus",function(e){e.data._resultId?r.$search.attr("aria-activedescendant",e.data._resultId):r.$search.removeAttr("aria-activedescendant")}),this.$selection.on("focusin",".select2-search--inline",function(e){r.trigger("focus",e)}),this.$selection.on("focusout",".select2-search--inline",function(e){r._handleBlur(e)}),this.$selection.on("keydown",".select2-search--inline",function(e){if(e.stopPropagation(),r.trigger("keypress",e),r._keyUpPrevented=e.isDefaultPrevented(),e.which===l.BACKSPACE&&""===r.$search.val()){var t=r.$searchContainer.prev(".select2-selection__choice");if(0<t.length){var n=a.GetData(t[0],"data");r.searchRemoveChoice(n),e.preventDefault()}}}),this.$selection.on("click",".select2-search--inline",function(e){r.$search.val()&&e.stopPropagation()});var o=document.documentMode,s=o&&o<=11;this.$selection.on("input.searchcheck",".select2-search--inline",function(e){s?r.$selection.off("input.search input.searchcheck"):r.$selection.off("keyup.search")}),this.$selection.on("keyup.search input.search",".select2-search--inline",function(e){if(s&&"input"===e.type)r.$selection.off("input.search input.searchcheck");else{var t=e.which;t!=l.SHIFT&&t!=l.CTRL&&t!=l.ALT&&t!=l.TAB&&r.handleSearch(e)}})},e.prototype._transferTabIndex=function(e){this.$search.attr("tabindex",this.$selection.attr("tabindex")),this.$selection.attr("tabindex","-1")},e.prototype.createPlaceholder=function(e,t){this.$search.attr("placeholder",t.text)},e.prototype.update=function(e,t){var n=this.$search[0]==document.activeElement;this.$search.attr("placeholder",""),e.call(this,t),this.$selection.find(".select2-selection__rendered").append(this.$searchContainer),this.resizeSearch(),n&&this.$search.trigger("focus")},e.prototype.handleSearch=function(){if(this.resizeSearch(),!this._keyUpPrevented){var e=this.$search.val();this.trigger("query",{term:e})}this._keyUpPrevented=!1},e.prototype.searchRemoveChoice=function(e,t){this.trigger("unselect",{data:t}),this.$search.val(t.text),this.handleSearch()},e.prototype.resizeSearch=function(){this.$search.css("width","25px");var e="";""!==this.$search.attr("placeholder")?e=this.$selection.find(".select2-selection__rendered").width():e=.75*(this.$search.val().length+1)+"em";this.$search.css("width",e)},e}),e.define("select2/selection/eventRelay",["jquery"],function(s){function e(){}return e.prototype.bind=function(e,t,n){var r=this,i=["open","opening","close","closing","select","selecting","unselect","unselecting","clear","clearing"],o=["opening","closing","selecting","unselecting","clearing"];e.call(this,t,n),t.on("*",function(e,t){if(-1!==s.inArray(e,i)){t=t||{};var n=s.Event("select2:"+e,{params:t});r.$element.trigger(n),-1!==s.inArray(e,o)&&(t.prevented=n.isDefaultPrevented())}})},e}),e.define("select2/translation",["jquery","require"],function(t,n){function r(e){this.dict=e||{}}return r.prototype.all=function(){return this.dict},r.prototype.get=function(e){return this.dict[e]},r.prototype.extend=function(e){this.dict=t.extend({},e.all(),this.dict)},r._cache={},r.loadPath=function(e){if(!(e in r._cache)){var t=n(e);r._cache[e]=t}return new r(r._cache[e])},r}),e.define("select2/diacritics",[],function(){return{"Ⓐ":"A","A":"A","À":"A","Á":"A","Â":"A","Ầ":"A","Ấ":"A","Ẫ":"A","Ẩ":"A","Ã":"A","Ā":"A","Ă":"A","Ằ":"A","Ắ":"A","Ẵ":"A","Ẳ":"A","Ȧ":"A","Ǡ":"A","Ä":"A","Ǟ":"A","Ả":"A","Å":"A","Ǻ":"A","Ǎ":"A","Ȁ":"A","Ȃ":"A","Ạ":"A","Ậ":"A","Ặ":"A","Ḁ":"A","Ą":"A","Ⱥ":"A","Ɐ":"A","Ꜳ":"AA","Æ":"AE","Ǽ":"AE","Ǣ":"AE","Ꜵ":"AO","Ꜷ":"AU","Ꜹ":"AV","Ꜻ":"AV","Ꜽ":"AY","Ⓑ":"B","B":"B","Ḃ":"B","Ḅ":"B","Ḇ":"B","Ƀ":"B","Ƃ":"B","Ɓ":"B","Ⓒ":"C","C":"C","Ć":"C","Ĉ":"C","Ċ":"C","Č":"C","Ç":"C","Ḉ":"C","Ƈ":"C","Ȼ":"C","Ꜿ":"C","Ⓓ":"D","D":"D","Ḋ":"D","Ď":"D","Ḍ":"D","Ḑ":"D","Ḓ":"D","Ḏ":"D","Đ":"D","Ƌ":"D","Ɗ":"D","Ɖ":"D","Ꝺ":"D","DZ":"DZ","DŽ":"DZ","Dz":"Dz","Dž":"Dz","Ⓔ":"E","E":"E","È":"E","É":"E","Ê":"E","Ề":"E","Ế":"E","Ễ":"E","Ể":"E","Ẽ":"E","Ē":"E","Ḕ":"E","Ḗ":"E","Ĕ":"E","Ė":"E","Ë":"E","Ẻ":"E","Ě":"E","Ȅ":"E","Ȇ":"E","Ẹ":"E","Ệ":"E","Ȩ":"E","Ḝ":"E","Ę":"E","Ḙ":"E","Ḛ":"E","Ɛ":"E","Ǝ":"E","Ⓕ":"F","F":"F","Ḟ":"F","Ƒ":"F","Ꝼ":"F","Ⓖ":"G","G":"G","Ǵ":"G","Ĝ":"G","Ḡ":"G","Ğ":"G","Ġ":"G","Ǧ":"G","Ģ":"G","Ǥ":"G","Ɠ":"G","Ꞡ":"G","Ᵹ":"G","Ꝿ":"G","Ⓗ":"H","H":"H","Ĥ":"H","Ḣ":"H","Ḧ":"H","Ȟ":"H","Ḥ":"H","Ḩ":"H","Ḫ":"H","Ħ":"H","Ⱨ":"H","Ⱶ":"H","Ɥ":"H","Ⓘ":"I","I":"I","Ì":"I","Í":"I","Î":"I","Ĩ":"I","Ī":"I","Ĭ":"I","İ":"I","Ï":"I","Ḯ":"I","Ỉ":"I","Ǐ":"I","Ȉ":"I","Ȋ":"I","Ị":"I","Į":"I","Ḭ":"I","Ɨ":"I","Ⓙ":"J","J":"J","Ĵ":"J","Ɉ":"J","Ⓚ":"K","K":"K","Ḱ":"K","Ǩ":"K","Ḳ":"K","Ķ":"K","Ḵ":"K","Ƙ":"K","Ⱪ":"K","Ꝁ":"K","Ꝃ":"K","Ꝅ":"K","Ꞣ":"K","Ⓛ":"L","L":"L","Ŀ":"L","Ĺ":"L","Ľ":"L","Ḷ":"L","Ḹ":"L","Ļ":"L","Ḽ":"L","Ḻ":"L","Ł":"L","Ƚ":"L","Ɫ":"L","Ⱡ":"L","Ꝉ":"L","Ꝇ":"L","Ꞁ":"L","LJ":"LJ","Lj":"Lj","Ⓜ":"M","M":"M","Ḿ":"M","Ṁ":"M","Ṃ":"M","Ɱ":"M","Ɯ":"M","Ⓝ":"N","N":"N","Ǹ":"N","Ń":"N","Ñ":"N","Ṅ":"N","Ň":"N","Ṇ":"N","Ņ":"N","Ṋ":"N","Ṉ":"N","Ƞ":"N","Ɲ":"N","Ꞑ":"N","Ꞥ":"N","NJ":"NJ","Nj":"Nj","Ⓞ":"O","O":"O","Ò":"O","Ó":"O","Ô":"O","Ồ":"O","Ố":"O","Ỗ":"O","Ổ":"O","Õ":"O","Ṍ":"O","Ȭ":"O","Ṏ":"O","Ō":"O","Ṑ":"O","Ṓ":"O","Ŏ":"O","Ȯ":"O","Ȱ":"O","Ö":"O","Ȫ":"O","Ỏ":"O","Ő":"O","Ǒ":"O","Ȍ":"O","Ȏ":"O","Ơ":"O","Ờ":"O","Ớ":"O","Ỡ":"O","Ở":"O","Ợ":"O","Ọ":"O","Ộ":"O","Ǫ":"O","Ǭ":"O","Ø":"O","Ǿ":"O","Ɔ":"O","Ɵ":"O","Ꝋ":"O","Ꝍ":"O","Œ":"OE","Ƣ":"OI","Ꝏ":"OO","Ȣ":"OU","Ⓟ":"P","P":"P","Ṕ":"P","Ṗ":"P","Ƥ":"P","Ᵽ":"P","Ꝑ":"P","Ꝓ":"P","Ꝕ":"P","Ⓠ":"Q","Q":"Q","Ꝗ":"Q","Ꝙ":"Q","Ɋ":"Q","Ⓡ":"R","R":"R","Ŕ":"R","Ṙ":"R","Ř":"R","Ȑ":"R","Ȓ":"R","Ṛ":"R","Ṝ":"R","Ŗ":"R","Ṟ":"R","Ɍ":"R","Ɽ":"R","Ꝛ":"R","Ꞧ":"R","Ꞃ":"R","Ⓢ":"S","S":"S","ẞ":"S","Ś":"S","Ṥ":"S","Ŝ":"S","Ṡ":"S","Š":"S","Ṧ":"S","Ṣ":"S","Ṩ":"S","Ș":"S","Ş":"S","Ȿ":"S","Ꞩ":"S","Ꞅ":"S","Ⓣ":"T","T":"T","Ṫ":"T","Ť":"T","Ṭ":"T","Ț":"T","Ţ":"T","Ṱ":"T","Ṯ":"T","Ŧ":"T","Ƭ":"T","Ʈ":"T","Ⱦ":"T","Ꞇ":"T","Ꜩ":"TZ","Ⓤ":"U","U":"U","Ù":"U","Ú":"U","Û":"U","Ũ":"U","Ṹ":"U","Ū":"U","Ṻ":"U","Ŭ":"U","Ü":"U","Ǜ":"U","Ǘ":"U","Ǖ":"U","Ǚ":"U","Ủ":"U","Ů":"U","Ű":"U","Ǔ":"U","Ȕ":"U","Ȗ":"U","Ư":"U","Ừ":"U","Ứ":"U","Ữ":"U","Ử":"U","Ự":"U","Ụ":"U","Ṳ":"U","Ų":"U","Ṷ":"U","Ṵ":"U","Ʉ":"U","Ⓥ":"V","V":"V","Ṽ":"V","Ṿ":"V","Ʋ":"V","Ꝟ":"V","Ʌ":"V","Ꝡ":"VY","Ⓦ":"W","W":"W","Ẁ":"W","Ẃ":"W","Ŵ":"W","Ẇ":"W","Ẅ":"W","Ẉ":"W","Ⱳ":"W","Ⓧ":"X","X":"X","Ẋ":"X","Ẍ":"X","Ⓨ":"Y","Y":"Y","Ỳ":"Y","Ý":"Y","Ŷ":"Y","Ỹ":"Y","Ȳ":"Y","Ẏ":"Y","Ÿ":"Y","Ỷ":"Y","Ỵ":"Y","Ƴ":"Y","Ɏ":"Y","Ỿ":"Y","Ⓩ":"Z","Z":"Z","Ź":"Z","Ẑ":"Z","Ż":"Z","Ž":"Z","Ẓ":"Z","Ẕ":"Z","Ƶ":"Z","Ȥ":"Z","Ɀ":"Z","Ⱬ":"Z","Ꝣ":"Z","ⓐ":"a","a":"a","ẚ":"a","à":"a","á":"a","â":"a","ầ":"a","ấ":"a","ẫ":"a","ẩ":"a","ã":"a","ā":"a","ă":"a","ằ":"a","ắ":"a","ẵ":"a","ẳ":"a","ȧ":"a","ǡ":"a","ä":"a","ǟ":"a","ả":"a","å":"a","ǻ":"a","ǎ":"a","ȁ":"a","ȃ":"a","ạ":"a","ậ":"a","ặ":"a","ḁ":"a","ą":"a","ⱥ":"a","ɐ":"a","ꜳ":"aa","æ":"ae","ǽ":"ae","ǣ":"ae","ꜵ":"ao","ꜷ":"au","ꜹ":"av","ꜻ":"av","ꜽ":"ay","ⓑ":"b","b":"b","ḃ":"b","ḅ":"b","ḇ":"b","ƀ":"b","ƃ":"b","ɓ":"b","ⓒ":"c","c":"c","ć":"c","ĉ":"c","ċ":"c","č":"c","ç":"c","ḉ":"c","ƈ":"c","ȼ":"c","ꜿ":"c","ↄ":"c","ⓓ":"d","d":"d","ḋ":"d","ď":"d","ḍ":"d","ḑ":"d","ḓ":"d","ḏ":"d","đ":"d","ƌ":"d","ɖ":"d","ɗ":"d","ꝺ":"d","dz":"dz","dž":"dz","ⓔ":"e","e":"e","è":"e","é":"e","ê":"e","ề":"e","ế":"e","ễ":"e","ể":"e","ẽ":"e","ē":"e","ḕ":"e","ḗ":"e","ĕ":"e","ė":"e","ë":"e","ẻ":"e","ě":"e","ȅ":"e","ȇ":"e","ẹ":"e","ệ":"e","ȩ":"e","ḝ":"e","ę":"e","ḙ":"e","ḛ":"e","ɇ":"e","ɛ":"e","ǝ":"e","ⓕ":"f","f":"f","ḟ":"f","ƒ":"f","ꝼ":"f","ⓖ":"g","g":"g","ǵ":"g","ĝ":"g","ḡ":"g","ğ":"g","ġ":"g","ǧ":"g","ģ":"g","ǥ":"g","ɠ":"g","ꞡ":"g","ᵹ":"g","ꝿ":"g","ⓗ":"h","h":"h","ĥ":"h","ḣ":"h","ḧ":"h","ȟ":"h","ḥ":"h","ḩ":"h","ḫ":"h","ẖ":"h","ħ":"h","ⱨ":"h","ⱶ":"h","ɥ":"h","ƕ":"hv","ⓘ":"i","i":"i","ì":"i","í":"i","î":"i","ĩ":"i","ī":"i","ĭ":"i","ï":"i","ḯ":"i","ỉ":"i","ǐ":"i","ȉ":"i","ȋ":"i","ị":"i","į":"i","ḭ":"i","ɨ":"i","ı":"i","ⓙ":"j","j":"j","ĵ":"j","ǰ":"j","ɉ":"j","ⓚ":"k","k":"k","ḱ":"k","ǩ":"k","ḳ":"k","ķ":"k","ḵ":"k","ƙ":"k","ⱪ":"k","ꝁ":"k","ꝃ":"k","ꝅ":"k","ꞣ":"k","ⓛ":"l","l":"l","ŀ":"l","ĺ":"l","ľ":"l","ḷ":"l","ḹ":"l","ļ":"l","ḽ":"l","ḻ":"l","ſ":"l","ł":"l","ƚ":"l","ɫ":"l","ⱡ":"l","ꝉ":"l","ꞁ":"l","ꝇ":"l","lj":"lj","ⓜ":"m","m":"m","ḿ":"m","ṁ":"m","ṃ":"m","ɱ":"m","ɯ":"m","ⓝ":"n","n":"n","ǹ":"n","ń":"n","ñ":"n","ṅ":"n","ň":"n","ṇ":"n","ņ":"n","ṋ":"n","ṉ":"n","ƞ":"n","ɲ":"n","ʼn":"n","ꞑ":"n","ꞥ":"n","nj":"nj","ⓞ":"o","o":"o","ò":"o","ó":"o","ô":"o","ồ":"o","ố":"o","ỗ":"o","ổ":"o","õ":"o","ṍ":"o","ȭ":"o","ṏ":"o","ō":"o","ṑ":"o","ṓ":"o","ŏ":"o","ȯ":"o","ȱ":"o","ö":"o","ȫ":"o","ỏ":"o","ő":"o","ǒ":"o","ȍ":"o","ȏ":"o","ơ":"o","ờ":"o","ớ":"o","ỡ":"o","ở":"o","ợ":"o","ọ":"o","ộ":"o","ǫ":"o","ǭ":"o","ø":"o","ǿ":"o","ɔ":"o","ꝋ":"o","ꝍ":"o","ɵ":"o","œ":"oe","ƣ":"oi","ȣ":"ou","ꝏ":"oo","ⓟ":"p","p":"p","ṕ":"p","ṗ":"p","ƥ":"p","ᵽ":"p","ꝑ":"p","ꝓ":"p","ꝕ":"p","ⓠ":"q","q":"q","ɋ":"q","ꝗ":"q","ꝙ":"q","ⓡ":"r","r":"r","ŕ":"r","ṙ":"r","ř":"r","ȑ":"r","ȓ":"r","ṛ":"r","ṝ":"r","ŗ":"r","ṟ":"r","ɍ":"r","ɽ":"r","ꝛ":"r","ꞧ":"r","ꞃ":"r","ⓢ":"s","s":"s","ß":"s","ś":"s","ṥ":"s","ŝ":"s","ṡ":"s","š":"s","ṧ":"s","ṣ":"s","ṩ":"s","ș":"s","ş":"s","ȿ":"s","ꞩ":"s","ꞅ":"s","ẛ":"s","ⓣ":"t","t":"t","ṫ":"t","ẗ":"t","ť":"t","ṭ":"t","ț":"t","ţ":"t","ṱ":"t","ṯ":"t","ŧ":"t","ƭ":"t","ʈ":"t","ⱦ":"t","ꞇ":"t","ꜩ":"tz","ⓤ":"u","u":"u","ù":"u","ú":"u","û":"u","ũ":"u","ṹ":"u","ū":"u","ṻ":"u","ŭ":"u","ü":"u","ǜ":"u","ǘ":"u","ǖ":"u","ǚ":"u","ủ":"u","ů":"u","ű":"u","ǔ":"u","ȕ":"u","ȗ":"u","ư":"u","ừ":"u","ứ":"u","ữ":"u","ử":"u","ự":"u","ụ":"u","ṳ":"u","ų":"u","ṷ":"u","ṵ":"u","ʉ":"u","ⓥ":"v","v":"v","ṽ":"v","ṿ":"v","ʋ":"v","ꝟ":"v","ʌ":"v","ꝡ":"vy","ⓦ":"w","w":"w","ẁ":"w","ẃ":"w","ŵ":"w","ẇ":"w","ẅ":"w","ẘ":"w","ẉ":"w","ⱳ":"w","ⓧ":"x","x":"x","ẋ":"x","ẍ":"x","ⓨ":"y","y":"y","ỳ":"y","ý":"y","ŷ":"y","ỹ":"y","ȳ":"y","ẏ":"y","ÿ":"y","ỷ":"y","ẙ":"y","ỵ":"y","ƴ":"y","ɏ":"y","ỿ":"y","ⓩ":"z","z":"z","ź":"z","ẑ":"z","ż":"z","ž":"z","ẓ":"z","ẕ":"z","ƶ":"z","ȥ":"z","ɀ":"z","ⱬ":"z","ꝣ":"z","Ά":"Α","Έ":"Ε","Ή":"Η","Ί":"Ι","Ϊ":"Ι","Ό":"Ο","Ύ":"Υ","Ϋ":"Υ","Ώ":"Ω","ά":"α","έ":"ε","ή":"η","ί":"ι","ϊ":"ι","ΐ":"ι","ό":"ο","ύ":"υ","ϋ":"υ","ΰ":"υ","ώ":"ω","ς":"σ","’":"'"}}),e.define("select2/data/base",["../utils"],function(r){function n(e,t){n.__super__.constructor.call(this)}return r.Extend(n,r.Observable),n.prototype.current=function(e){throw new Error("The `current` method must be defined in child classes.")},n.prototype.query=function(e,t){throw new Error("The `query` method must be defined in child classes.")},n.prototype.bind=function(e,t){},n.prototype.destroy=function(){},n.prototype.generateResultId=function(e,t){var n=e.id+"-result-";return n+=r.generateChars(4),null!=t.id?n+="-"+t.id.toString():n+="-"+r.generateChars(4),n},n}),e.define("select2/data/select",["./base","../utils","jquery"],function(e,a,l){function n(e,t){this.$element=e,this.options=t,n.__super__.constructor.call(this)}return a.Extend(n,e),n.prototype.current=function(e){var n=[],r=this;this.$element.find(":selected").each(function(){var e=l(this),t=r.item(e);n.push(t)}),e(n)},n.prototype.select=function(i){var o=this;if(i.selected=!0,l(i.element).is("option"))return i.element.selected=!0,void this.$element.trigger("change");if(this.$element.prop("multiple"))this.current(function(e){var t=[];(i=[i]).push.apply(i,e);for(var n=0;n<i.length;n++){var r=i[n].id;-1===l.inArray(r,t)&&t.push(r)}o.$element.val(t),o.$element.trigger("change")});else{var e=i.id;this.$element.val(e),this.$element.trigger("change")}},n.prototype.unselect=function(i){var o=this;if(this.$element.prop("multiple")){if(i.selected=!1,l(i.element).is("option"))return i.element.selected=!1,void this.$element.trigger("change");this.current(function(e){for(var t=[],n=0;n<e.length;n++){var r=e[n].id;r!==i.id&&-1===l.inArray(r,t)&&t.push(r)}o.$element.val(t),o.$element.trigger("change")})}},n.prototype.bind=function(e,t){var n=this;(this.container=e).on("select",function(e){n.select(e.data)}),e.on("unselect",function(e){n.unselect(e.data)})},n.prototype.destroy=function(){this.$element.find("*").each(function(){a.RemoveData(this)})},n.prototype.query=function(r,e){var i=[],o=this;this.$element.children().each(function(){var e=l(this);if(e.is("option")||e.is("optgroup")){var t=o.item(e),n=o.matches(r,t);null!==n&&i.push(n)}}),e({results:i})},n.prototype.addOptions=function(e){a.appendMany(this.$element,e)},n.prototype.option=function(e){var t;e.children?(t=document.createElement("optgroup")).label=e.text:void 0!==(t=document.createElement("option")).textContent?t.textContent=e.text:t.innerText=e.text,void 0!==e.id&&(t.value=e.id),e.disabled&&(t.disabled=!0),e.selected&&(t.selected=!0),e.title&&(t.title=e.title);var n=l(t),r=this._normalizeItem(e);return r.element=t,a.StoreData(t,"data",r),n},n.prototype.item=function(e){var t={};if(null!=(t=a.GetData(e[0],"data")))return t;if(e.is("option"))t={id:e.val(),text:e.text(),disabled:e.prop("disabled"),selected:e.prop("selected"),title:e.prop("title")};else if(e.is("optgroup")){t={text:e.prop("label"),children:[],title:e.prop("title")};for(var n=e.children("option"),r=[],i=0;i<n.length;i++){var o=l(n[i]),s=this.item(o);r.push(s)}t.children=r}return(t=this._normalizeItem(t)).element=e[0],a.StoreData(e[0],"data",t),t},n.prototype._normalizeItem=function(e){e!==Object(e)&&(e={id:e,text:e});return null!=(e=l.extend({},{text:""},e)).id&&(e.id=e.id.toString()),null!=e.text&&(e.text=e.text.toString()),null==e._resultId&&e.id&&null!=this.container&&(e._resultId=this.generateResultId(this.container,e)),l.extend({},{selected:!1,disabled:!1},e)},n.prototype.matches=function(e,t){return this.options.get("matcher")(e,t)},n}),e.define("select2/data/array",["./select","../utils","jquery"],function(e,f,g){function r(e,t){this._dataToConvert=t.get("data")||[],r.__super__.constructor.call(this,e,t)}return f.Extend(r,e),r.prototype.bind=function(e,t){r.__super__.bind.call(this,e,t),this.addOptions(this.convertToOptions(this._dataToConvert))},r.prototype.select=function(n){var e=this.$element.find("option").filter(function(e,t){return t.value==n.id.toString()});0===e.length&&(e=this.option(n),this.addOptions(e)),r.__super__.select.call(this,n)},r.prototype.convertToOptions=function(e){var t=this,n=this.$element.find("option"),r=n.map(function(){return t.item(g(this)).id}).get(),i=[];function o(e){return function(){return g(this).val()==e.id}}for(var s=0;s<e.length;s++){var a=this._normalizeItem(e[s]);if(0<=g.inArray(a.id,r)){var l=n.filter(o(a)),c=this.item(l),u=g.extend(!0,{},a,c),d=this.option(u);l.replaceWith(d)}else{var p=this.option(a);if(a.children){var h=this.convertToOptions(a.children);f.appendMany(p,h)}i.push(p)}}return i},r}),e.define("select2/data/ajax",["./array","../utils","jquery"],function(e,t,o){function n(e,t){this.ajaxOptions=this._applyDefaults(t.get("ajax")),null!=this.ajaxOptions.processResults&&(this.processResults=this.ajaxOptions.processResults),n.__super__.constructor.call(this,e,t)}return t.Extend(n,e),n.prototype._applyDefaults=function(e){var t={data:function(e){return o.extend({},e,{q:e.term})},transport:function(e,t,n){var r=o.ajax(e);return r.then(t),r.fail(n),r}};return o.extend({},t,e,!0)},n.prototype.processResults=function(e){return e},n.prototype.query=function(n,r){var i=this;null!=this._request&&(o.isFunction(this._request.abort)&&this._request.abort(),this._request=null);var t=o.extend({type:"GET"},this.ajaxOptions);function e(){var e=t.transport(t,function(e){var t=i.processResults(e,n);i.options.get("debug")&&window.console&&console.error&&(t&&t.results&&o.isArray(t.results)||console.error("Select2: The AJAX results did not return an array in the `results` key of the response.")),r(t)},function(){"status"in e&&(0===e.status||"0"===e.status)||i.trigger("results:message",{message:"errorLoading"})});i._request=e}"function"==typeof t.url&&(t.url=t.url.call(this.$element,n)),"function"==typeof t.data&&(t.data=t.data.call(this.$element,n)),this.ajaxOptions.delay&&null!=n.term?(this._queryTimeout&&window.clearTimeout(this._queryTimeout),this._queryTimeout=window.setTimeout(e,this.ajaxOptions.delay)):e()},n}),e.define("select2/data/tags",["jquery"],function(u){function e(e,t,n){var r=n.get("tags"),i=n.get("createTag");void 0!==i&&(this.createTag=i);var o=n.get("insertTag");if(void 0!==o&&(this.insertTag=o),e.call(this,t,n),u.isArray(r))for(var s=0;s<r.length;s++){var a=r[s],l=this._normalizeItem(a),c=this.option(l);this.$element.append(c)}}return e.prototype.query=function(e,c,u){var d=this;this._removeOldTags(),null!=c.term&&null==c.page?e.call(this,c,function e(t,n){for(var r=t.results,i=0;i<r.length;i++){var o=r[i],s=null!=o.children&&!e({results:o.children},!0);if((o.text||"").toUpperCase()===(c.term||"").toUpperCase()||s)return!n&&(t.data=r,void u(t))}if(n)return!0;var a=d.createTag(c);if(null!=a){var l=d.option(a);l.attr("data-select2-tag",!0),d.addOptions([l]),d.insertTag(r,a)}t.results=r,u(t)}):e.call(this,c,u)},e.prototype.createTag=function(e,t){var n=u.trim(t.term);return""===n?null:{id:n,text:n}},e.prototype.insertTag=function(e,t,n){t.unshift(n)},e.prototype._removeOldTags=function(e){this.$element.find("option[data-select2-tag]").each(function(){this.selected||u(this).remove()})},e}),e.define("select2/data/tokenizer",["jquery"],function(d){function e(e,t,n){var r=n.get("tokenizer");void 0!==r&&(this.tokenizer=r),e.call(this,t,n)}return e.prototype.bind=function(e,t,n){e.call(this,t,n),this.$search=t.dropdown.$search||t.selection.$search||n.find(".select2-search__field")},e.prototype.query=function(e,t,n){var r=this;t.term=t.term||"";var i=this.tokenizer(t,this.options,function(e){var t=r._normalizeItem(e);if(!r.$element.find("option").filter(function(){return d(this).val()===t.id}).length){var n=r.option(t);n.attr("data-select2-tag",!0),r._removeOldTags(),r.addOptions([n])}!function(e){r.trigger("select",{data:e})}(t)});i.term!==t.term&&(this.$search.length&&(this.$search.val(i.term),this.$search.trigger("focus")),t.term=i.term),e.call(this,t,n)},e.prototype.tokenizer=function(e,t,n,r){for(var i=n.get("tokenSeparators")||[],o=t.term,s=0,a=this.createTag||function(e){return{id:e.term,text:e.term}};s<o.length;){var l=o[s];if(-1!==d.inArray(l,i)){var c=o.substr(0,s),u=a(d.extend({},t,{term:c}));null!=u?(r(u),o=o.substr(s+1)||"",s=0):s++}else s++}return{term:o}},e}),e.define("select2/data/minimumInputLength",[],function(){function e(e,t,n){this.minimumInputLength=n.get("minimumInputLength"),e.call(this,t,n)}return e.prototype.query=function(e,t,n){t.term=t.term||"",t.term.length<this.minimumInputLength?this.trigger("results:message",{message:"inputTooShort",args:{minimum:this.minimumInputLength,input:t.term,params:t}}):e.call(this,t,n)},e}),e.define("select2/data/maximumInputLength",[],function(){function e(e,t,n){this.maximumInputLength=n.get("maximumInputLength"),e.call(this,t,n)}return e.prototype.query=function(e,t,n){t.term=t.term||"",0<this.maximumInputLength&&t.term.length>this.maximumInputLength?this.trigger("results:message",{message:"inputTooLong",args:{maximum:this.maximumInputLength,input:t.term,params:t}}):e.call(this,t,n)},e}),e.define("select2/data/maximumSelectionLength",[],function(){function e(e,t,n){this.maximumSelectionLength=n.get("maximumSelectionLength"),e.call(this,t,n)}return e.prototype.bind=function(e,t,n){var r=this;e.call(this,t,n),t.on("select",function(){r._checkIfMaximumSelected()})},e.prototype.query=function(e,t,n){var r=this;this._checkIfMaximumSelected(function(){e.call(r,t,n)})},e.prototype._checkIfMaximumSelected=function(e,n){var r=this;this.current(function(e){var t=null!=e?e.length:0;0<r.maximumSelectionLength&&t>=r.maximumSelectionLength?r.trigger("results:message",{message:"maximumSelected",args:{maximum:r.maximumSelectionLength}}):n&&n()})},e}),e.define("select2/dropdown",["jquery","./utils"],function(t,e){function n(e,t){this.$element=e,this.options=t,n.__super__.constructor.call(this)}return e.Extend(n,e.Observable),n.prototype.render=function(){var e=t('<span class="select2-dropdown"><span class="select2-results"></span></span>');return e.attr("dir",this.options.get("dir")),this.$dropdown=e},n.prototype.bind=function(){},n.prototype.position=function(e,t){},n.prototype.destroy=function(){this.$dropdown.remove()},n}),e.define("select2/dropdown/search",["jquery","../utils"],function(o,e){function t(){}return t.prototype.render=function(e){var t=e.call(this),n=o('<span class="select2-search select2-search--dropdown"><input class="select2-search__field" type="search" tabindex="-1" autocomplete="off" autocorrect="off" autocapitalize="none" spellcheck="false" role="searchbox" aria-autocomplete="list" /></span>');return this.$searchContainer=n,this.$search=n.find("input"),t.prepend(n),t},t.prototype.bind=function(e,t,n){var r=this,i=t.id+"-results";e.call(this,t,n),this.$search.on("keydown",function(e){r.trigger("keypress",e),r._keyUpPrevented=e.isDefaultPrevented()}),this.$search.on("input",function(e){o(this).off("keyup")}),this.$search.on("keyup input",function(e){r.handleSearch(e)}),t.on("open",function(){r.$search.attr("tabindex",0),r.$search.attr("aria-controls",i),r.$search.trigger("focus"),window.setTimeout(function(){r.$search.trigger("focus")},0)}),t.on("close",function(){r.$search.attr("tabindex",-1),r.$search.removeAttr("aria-controls"),r.$search.removeAttr("aria-activedescendant"),r.$search.val(""),r.$search.trigger("blur")}),t.on("focus",function(){t.isOpen()||r.$search.trigger("focus")}),t.on("results:all",function(e){null!=e.query.term&&""!==e.query.term||(r.showSearch(e)?r.$searchContainer.removeClass("select2-search--hide"):r.$searchContainer.addClass("select2-search--hide"))}),t.on("results:focus",function(e){e.data._resultId?r.$search.attr("aria-activedescendant",e.data._resultId):r.$search.removeAttr("aria-activedescendant")})},t.prototype.handleSearch=function(e){if(!this._keyUpPrevented){var t=this.$search.val();this.trigger("query",{term:t})}this._keyUpPrevented=!1},t.prototype.showSearch=function(e,t){return!0},t}),e.define("select2/dropdown/hidePlaceholder",[],function(){function e(e,t,n,r){this.placeholder=this.normalizePlaceholder(n.get("placeholder")),e.call(this,t,n,r)}return e.prototype.append=function(e,t){t.results=this.removePlaceholder(t.results),e.call(this,t)},e.prototype.normalizePlaceholder=function(e,t){return"string"==typeof t&&(t={id:"",text:t}),t},e.prototype.removePlaceholder=function(e,t){for(var n=t.slice(0),r=t.length-1;0<=r;r--){var i=t[r];this.placeholder.id===i.id&&n.splice(r,1)}return n},e}),e.define("select2/dropdown/infiniteScroll",["jquery"],function(n){function e(e,t,n,r){this.lastParams={},e.call(this,t,n,r),this.$loadingMore=this.createLoadingMore(),this.loading=!1}return e.prototype.append=function(e,t){this.$loadingMore.remove(),this.loading=!1,e.call(this,t),this.showLoadingMore(t)&&(this.$results.append(this.$loadingMore),this.loadMoreIfNeeded())},e.prototype.bind=function(e,t,n){var r=this;e.call(this,t,n),t.on("query",function(e){r.lastParams=e,r.loading=!0}),t.on("query:append",function(e){r.lastParams=e,r.loading=!0}),this.$results.on("scroll",this.loadMoreIfNeeded.bind(this))},e.prototype.loadMoreIfNeeded=function(){var e=n.contains(document.documentElement,this.$loadingMore[0]);if(!this.loading&&e){var t=this.$results.offset().top+this.$results.outerHeight(!1);this.$loadingMore.offset().top+this.$loadingMore.outerHeight(!1)<=t+50&&this.loadMore()}},e.prototype.loadMore=function(){this.loading=!0;var e=n.extend({},{page:1},this.lastParams);e.page++,this.trigger("query:append",e)},e.prototype.showLoadingMore=function(e,t){return t.pagination&&t.pagination.more},e.prototype.createLoadingMore=function(){var e=n('<li class="select2-results__option select2-results__option--load-more"role="option" aria-disabled="true"></li>'),t=this.options.get("translations").get("loadingMore");return e.html(t(this.lastParams)),e},e}),e.define("select2/dropdown/attachBody",["jquery","../utils"],function(f,a){function e(e,t,n){this.$dropdownParent=f(n.get("dropdownParent")||document.body),e.call(this,t,n)}return e.prototype.bind=function(e,t,n){var r=this;e.call(this,t,n),t.on("open",function(){r._showDropdown(),r._attachPositioningHandler(t),r._bindContainerResultHandlers(t)}),t.on("close",function(){r._hideDropdown(),r._detachPositioningHandler(t)}),this.$dropdownContainer.on("mousedown",function(e){e.stopPropagation()})},e.prototype.destroy=function(e){e.call(this),this.$dropdownContainer.remove()},e.prototype.position=function(e,t,n){t.attr("class",n.attr("class")),t.removeClass("select2"),t.addClass("select2-container--open"),t.css({position:"absolute",top:-999999}),this.$container=n},e.prototype.render=function(e){var t=f("<span></span>"),n=e.call(this);return t.append(n),this.$dropdownContainer=t},e.prototype._hideDropdown=function(e){this.$dropdownContainer.detach()},e.prototype._bindContainerResultHandlers=function(e,t){if(!this._containerResultsHandlersBound){var n=this;t.on("results:all",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("results:append",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("results:message",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("select",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("unselect",function(){n._positionDropdown(),n._resizeDropdown()}),this._containerResultsHandlersBound=!0}},e.prototype._attachPositioningHandler=function(e,t){var n=this,r="scroll.select2."+t.id,i="resize.select2."+t.id,o="orientationchange.select2."+t.id,s=this.$container.parents().filter(a.hasScroll);s.each(function(){a.StoreData(this,"select2-scroll-position",{x:f(this).scrollLeft(),y:f(this).scrollTop()})}),s.on(r,function(e){var t=a.GetData(this,"select2-scroll-position");f(this).scrollTop(t.y)}),f(window).on(r+" "+i+" "+o,function(e){n._positionDropdown(),n._resizeDropdown()})},e.prototype._detachPositioningHandler=function(e,t){var n="scroll.select2."+t.id,r="resize.select2."+t.id,i="orientationchange.select2."+t.id;this.$container.parents().filter(a.hasScroll).off(n),f(window).off(n+" "+r+" "+i)},e.prototype._positionDropdown=function(){var e=f(window),t=this.$dropdown.hasClass("select2-dropdown--above"),n=this.$dropdown.hasClass("select2-dropdown--below"),r=null,i=this.$container.offset();i.bottom=i.top+this.$container.outerHeight(!1);var o={height:this.$container.outerHeight(!1)};o.top=i.top,o.bottom=i.top+o.height;var s=this.$dropdown.outerHeight(!1),a=e.scrollTop(),l=e.scrollTop()+e.height(),c=a<i.top-s,u=l>i.bottom+s,d={left:i.left,top:o.bottom},p=this.$dropdownParent;"static"===p.css("position")&&(p=p.offsetParent());var h={top:0,left:0};(f.contains(document.body,p[0])||p[0].isConnected)&&(h=p.offset()),d.top-=h.top,d.left-=h.left,t||n||(r="below"),u||!c||t?!c&&u&&t&&(r="below"):r="above",("above"==r||t&&"below"!==r)&&(d.top=o.top-h.top-s),null!=r&&(this.$dropdown.removeClass("select2-dropdown--below select2-dropdown--above").addClass("select2-dropdown--"+r),this.$container.removeClass("select2-container--below select2-container--above").addClass("select2-container--"+r)),this.$dropdownContainer.css(d)},e.prototype._resizeDropdown=function(){var e={width:this.$container.outerWidth(!1)+"px"};this.options.get("dropdownAutoWidth")&&(e.minWidth=e.width,e.position="relative",e.width="auto"),this.$dropdown.css(e)},e.prototype._showDropdown=function(e){this.$dropdownContainer.appendTo(this.$dropdownParent),this._positionDropdown(),this._resizeDropdown()},e}),e.define("select2/dropdown/minimumResultsForSearch",[],function(){function e(e,t,n,r){this.minimumResultsForSearch=n.get("minimumResultsForSearch"),this.minimumResultsForSearch<0&&(this.minimumResultsForSearch=1/0),e.call(this,t,n,r)}return e.prototype.showSearch=function(e,t){return!(function e(t){for(var n=0,r=0;r<t.length;r++){var i=t[r];i.children?n+=e(i.children):n++}return n}(t.data.results)<this.minimumResultsForSearch)&&e.call(this,t)},e}),e.define("select2/dropdown/selectOnClose",["../utils"],function(o){function e(){}return e.prototype.bind=function(e,t,n){var r=this;e.call(this,t,n),t.on("close",function(e){r._handleSelectOnClose(e)})},e.prototype._handleSelectOnClose=function(e,t){if(t&&null!=t.originalSelect2Event){var n=t.originalSelect2Event;if("select"===n._type||"unselect"===n._type)return}var r=this.getHighlightedResults();if(!(r.length<1)){var i=o.GetData(r[0],"data");null!=i.element&&i.element.selected||null==i.element&&i.selected||this.trigger("select",{data:i})}},e}),e.define("select2/dropdown/closeOnSelect",[],function(){function e(){}return e.prototype.bind=function(e,t,n){var r=this;e.call(this,t,n),t.on("select",function(e){r._selectTriggered(e)}),t.on("unselect",function(e){r._selectTriggered(e)})},e.prototype._selectTriggered=function(e,t){var n=t.originalEvent;n&&(n.ctrlKey||n.metaKey)||this.trigger("close",{originalEvent:n,originalSelect2Event:t})},e}),e.define("select2/i18n/en",[],function(){return{errorLoading:function(){return"The results could not be loaded."},inputTooLong:function(e){var t=e.input.length-e.maximum,n="Please delete "+t+" character";return 1!=t&&(n+="s"),n},inputTooShort:function(e){return"Please enter "+(e.minimum-e.input.length)+" or more characters"},loadingMore:function(){return"Loading more results…"},maximumSelected:function(e){var t="You can only select "+e.maximum+" item";return 1!=e.maximum&&(t+="s"),t},noResults:function(){return"No results found"},searching:function(){return"Searching…"},removeAllItems:function(){return"Remove all items"}}}),e.define("select2/defaults",["jquery","require","./results","./selection/single","./selection/multiple","./selection/placeholder","./selection/allowClear","./selection/search","./selection/eventRelay","./utils","./translation","./diacritics","./data/select","./data/array","./data/ajax","./data/tags","./data/tokenizer","./data/minimumInputLength","./data/maximumInputLength","./data/maximumSelectionLength","./dropdown","./dropdown/search","./dropdown/hidePlaceholder","./dropdown/infiniteScroll","./dropdown/attachBody","./dropdown/minimumResultsForSearch","./dropdown/selectOnClose","./dropdown/closeOnSelect","./i18n/en"],function(c,u,d,p,h,f,g,m,v,y,s,t,_,$,w,b,A,x,D,S,E,C,O,T,q,L,I,j,e){function n(){this.reset()}return n.prototype.apply=function(e){if(null==(e=c.extend(!0,{},this.defaults,e)).dataAdapter){if(null!=e.ajax?e.dataAdapter=w:null!=e.data?e.dataAdapter=$:e.dataAdapter=_,0<e.minimumInputLength&&(e.dataAdapter=y.Decorate(e.dataAdapter,x)),0<e.maximumInputLength&&(e.dataAdapter=y.Decorate(e.dataAdapter,D)),0<e.maximumSelectionLength&&(e.dataAdapter=y.Decorate(e.dataAdapter,S)),e.tags&&(e.dataAdapter=y.Decorate(e.dataAdapter,b)),null==e.tokenSeparators&&null==e.tokenizer||(e.dataAdapter=y.Decorate(e.dataAdapter,A)),null!=e.query){var t=u(e.amdBase+"compat/query");e.dataAdapter=y.Decorate(e.dataAdapter,t)}if(null!=e.initSelection){var n=u(e.amdBase+"compat/initSelection");e.dataAdapter=y.Decorate(e.dataAdapter,n)}}if(null==e.resultsAdapter&&(e.resultsAdapter=d,null!=e.ajax&&(e.resultsAdapter=y.Decorate(e.resultsAdapter,T)),null!=e.placeholder&&(e.resultsAdapter=y.Decorate(e.resultsAdapter,O)),e.selectOnClose&&(e.resultsAdapter=y.Decorate(e.resultsAdapter,I))),null==e.dropdownAdapter){if(e.multiple)e.dropdownAdapter=E;else{var r=y.Decorate(E,C);e.dropdownAdapter=r}if(0!==e.minimumResultsForSearch&&(e.dropdownAdapter=y.Decorate(e.dropdownAdapter,L)),e.closeOnSelect&&(e.dropdownAdapter=y.Decorate(e.dropdownAdapter,j)),null!=e.dropdownCssClass||null!=e.dropdownCss||null!=e.adaptDropdownCssClass){var i=u(e.amdBase+"compat/dropdownCss");e.dropdownAdapter=y.Decorate(e.dropdownAdapter,i)}e.dropdownAdapter=y.Decorate(e.dropdownAdapter,q)}if(null==e.selectionAdapter){if(e.multiple?e.selectionAdapter=h:e.selectionAdapter=p,null!=e.placeholder&&(e.selectionAdapter=y.Decorate(e.selectionAdapter,f)),e.allowClear&&(e.selectionAdapter=y.Decorate(e.selectionAdapter,g)),e.multiple&&(e.selectionAdapter=y.Decorate(e.selectionAdapter,m)),null!=e.containerCssClass||null!=e.containerCss||null!=e.adaptContainerCssClass){var o=u(e.amdBase+"compat/containerCss");e.selectionAdapter=y.Decorate(e.selectionAdapter,o)}e.selectionAdapter=y.Decorate(e.selectionAdapter,v)}e.language=this._resolveLanguage(e.language),e.language.push("en");for(var s=[],a=0;a<e.language.length;a++){var l=e.language[a];-1===s.indexOf(l)&&s.push(l)}return e.language=s,e.translations=this._processTranslations(e.language,e.debug),e},n.prototype.reset=function(){function a(e){return e.replace(/[^\u0000-\u007E]/g,function(e){return t[e]||e})}this.defaults={amdBase:"./",amdLanguageBase:"./i18n/",closeOnSelect:!0,debug:!1,dropdownAutoWidth:!1,escapeMarkup:y.escapeMarkup,language:{},matcher:function e(t,n){if(""===c.trim(t.term))return n;if(n.children&&0<n.children.length){for(var r=c.extend(!0,{},n),i=n.children.length-1;0<=i;i--)null==e(t,n.children[i])&&r.children.splice(i,1);return 0<r.children.length?r:e(t,r)}var o=a(n.text).toUpperCase(),s=a(t.term).toUpperCase();return-1<o.indexOf(s)?n:null},minimumInputLength:0,maximumInputLength:0,maximumSelectionLength:0,minimumResultsForSearch:0,selectOnClose:!1,scrollAfterSelect:!1,sorter:function(e){return e},templateResult:function(e){return e.text},templateSelection:function(e){return e.text},theme:"default",width:"resolve"}},n.prototype.applyFromElement=function(e,t){var n=e.language,r=this.defaults.language,i=t.prop("lang"),o=t.closest("[lang]").prop("lang"),s=Array.prototype.concat.call(this._resolveLanguage(i),this._resolveLanguage(n),this._resolveLanguage(r),this._resolveLanguage(o));return e.language=s,e},n.prototype._resolveLanguage=function(e){if(!e)return[];if(c.isEmptyObject(e))return[];if(c.isPlainObject(e))return[e];var t;t=c.isArray(e)?e:[e];for(var n=[],r=0;r<t.length;r++)if(n.push(t[r]),"string"==typeof t[r]&&0<t[r].indexOf("-")){var i=t[r].split("-")[0];n.push(i)}return n},n.prototype._processTranslations=function(e,t){for(var n=new s,r=0;r<e.length;r++){var i=new s,o=e[r];if("string"==typeof o)try{i=s.loadPath(o)}catch(e){try{o=this.defaults.amdLanguageBase+o,i=s.loadPath(o)}catch(e){t&&window.console&&console.warn&&console.warn('Select2: The language file for "'+o+'" could not be automatically loaded. A fallback will be used instead.')}}else i=c.isPlainObject(o)?new s(o):o;n.extend(i)}return n},n.prototype.set=function(e,t){var n={};n[c.camelCase(e)]=t;var r=y._convertData(n);c.extend(!0,this.defaults,r)},new n}),e.define("select2/options",["require","jquery","./defaults","./utils"],function(r,d,i,p){function e(e,t){if(this.options=e,null!=t&&this.fromElement(t),null!=t&&(this.options=i.applyFromElement(this.options,t)),this.options=i.apply(this.options),t&&t.is("input")){var n=r(this.get("amdBase")+"compat/inputData");this.options.dataAdapter=p.Decorate(this.options.dataAdapter,n)}}return e.prototype.fromElement=function(e){var t=["select2"];null==this.options.multiple&&(this.options.multiple=e.prop("multiple")),null==this.options.disabled&&(this.options.disabled=e.prop("disabled")),null==this.options.dir&&(e.prop("dir")?this.options.dir=e.prop("dir"):e.closest("[dir]").prop("dir")?this.options.dir=e.closest("[dir]").prop("dir"):this.options.dir="ltr"),e.prop("disabled",this.options.disabled),e.prop("multiple",this.options.multiple),p.GetData(e[0],"select2Tags")&&(this.options.debug&&window.console&&console.warn&&console.warn('Select2: The `data-select2-tags` attribute has been changed to use the `data-data` and `data-tags="true"` attributes and will be removed in future versions of Select2.'),p.StoreData(e[0],"data",p.GetData(e[0],"select2Tags")),p.StoreData(e[0],"tags",!0)),p.GetData(e[0],"ajaxUrl")&&(this.options.debug&&window.console&&console.warn&&console.warn("Select2: The `data-ajax-url` attribute has been changed to `data-ajax--url` and support for the old attribute will be removed in future versions of Select2."),e.attr("ajax--url",p.GetData(e[0],"ajaxUrl")),p.StoreData(e[0],"ajax-Url",p.GetData(e[0],"ajaxUrl")));var n={};function r(e,t){return t.toUpperCase()}for(var i=0;i<e[0].attributes.length;i++){var o=e[0].attributes[i].name,s="data-";if(o.substr(0,s.length)==s){var a=o.substring(s.length),l=p.GetData(e[0],a);n[a.replace(/-([a-z])/g,r)]=l}}d.fn.jquery&&"1."==d.fn.jquery.substr(0,2)&&e[0].dataset&&(n=d.extend(!0,{},e[0].dataset,n));var c=d.extend(!0,{},p.GetData(e[0]),n);for(var u in c=p._convertData(c))-1<d.inArray(u,t)||(d.isPlainObject(this.options[u])?d.extend(this.options[u],c[u]):this.options[u]=c[u]);return this},e.prototype.get=function(e){return this.options[e]},e.prototype.set=function(e,t){this.options[e]=t},e}),e.define("select2/core",["jquery","./options","./utils","./keys"],function(i,c,u,r){var d=function(e,t){null!=u.GetData(e[0],"select2")&&u.GetData(e[0],"select2").destroy(),this.$element=e,this.id=this._generateId(e),t=t||{},this.options=new c(t,e),d.__super__.constructor.call(this);var n=e.attr("tabindex")||0;u.StoreData(e[0],"old-tabindex",n),e.attr("tabindex","-1");var r=this.options.get("dataAdapter");this.dataAdapter=new r(e,this.options);var i=this.render();this._placeContainer(i);var o=this.options.get("selectionAdapter");this.selection=new o(e,this.options),this.$selection=this.selection.render(),this.selection.position(this.$selection,i);var s=this.options.get("dropdownAdapter");this.dropdown=new s(e,this.options),this.$dropdown=this.dropdown.render(),this.dropdown.position(this.$dropdown,i);var a=this.options.get("resultsAdapter");this.results=new a(e,this.options,this.dataAdapter),this.$results=this.results.render(),this.results.position(this.$results,this.$dropdown);var l=this;this._bindAdapters(),this._registerDomEvents(),this._registerDataEvents(),this._registerSelectionEvents(),this._registerDropdownEvents(),this._registerResultsEvents(),this._registerEvents(),this.dataAdapter.current(function(e){l.trigger("selection:update",{data:e})}),e.addClass("select2-hidden-accessible"),e.attr("aria-hidden","true"),this._syncAttributes(),u.StoreData(e[0],"select2",this),e.data("select2",this)};return u.Extend(d,u.Observable),d.prototype._generateId=function(e){return"select2-"+(null!=e.attr("id")?e.attr("id"):null!=e.attr("name")?e.attr("name")+"-"+u.generateChars(2):u.generateChars(4)).replace(/(:|\.|\[|\]|,)/g,"")},d.prototype._placeContainer=function(e){e.insertAfter(this.$element);var t=this._resolveWidth(this.$element,this.options.get("width"));null!=t&&e.css("width",t)},d.prototype._resolveWidth=function(e,t){var n=/^width:(([-+]?([0-9]*\.)?[0-9]+)(px|em|ex|%|in|cm|mm|pt|pc))/i;if("resolve"==t){var r=this._resolveWidth(e,"style");return null!=r?r:this._resolveWidth(e,"element")}if("element"==t){var i=e.outerWidth(!1);return i<=0?"auto":i+"px"}if("style"!=t)return"computedstyle"!=t?t:window.getComputedStyle(e[0]).width;var o=e.attr("style");if("string"!=typeof o)return null;for(var s=o.split(";"),a=0,l=s.length;a<l;a+=1){var c=s[a].replace(/\s/g,"").match(n);if(null!==c&&1<=c.length)return c[1]}return null},d.prototype._bindAdapters=function(){this.dataAdapter.bind(this,this.$container),this.selection.bind(this,this.$container),this.dropdown.bind(this,this.$container),this.results.bind(this,this.$container)},d.prototype._registerDomEvents=function(){var t=this;this.$element.on("change.select2",function(){t.dataAdapter.current(function(e){t.trigger("selection:update",{data:e})})}),this.$element.on("focus.select2",function(e){t.trigger("focus",e)}),this._syncA=u.bind(this._syncAttributes,this),this._syncS=u.bind(this._syncSubtree,this),this.$element[0].attachEvent&&this.$element[0].attachEvent("onpropertychange",this._syncA);var e=window.MutationObserver||window.WebKitMutationObserver||window.MozMutationObserver;null!=e?(this._observer=new e(function(e){i.each(e,t._syncA),i.each(e,t._syncS)}),this._observer.observe(this.$element[0],{attributes:!0,childList:!0,subtree:!1})):this.$element[0].addEventListener&&(this.$element[0].addEventListener("DOMAttrModified",t._syncA,!1),this.$element[0].addEventListener("DOMNodeInserted",t._syncS,!1),this.$element[0].addEventListener("DOMNodeRemoved",t._syncS,!1))},d.prototype._registerDataEvents=function(){var n=this;this.dataAdapter.on("*",function(e,t){n.trigger(e,t)})},d.prototype._registerSelectionEvents=function(){var n=this,r=["toggle","focus"];this.selection.on("toggle",function(){n.toggleDropdown()}),this.selection.on("focus",function(e){n.focus(e)}),this.selection.on("*",function(e,t){-1===i.inArray(e,r)&&n.trigger(e,t)})},d.prototype._registerDropdownEvents=function(){var n=this;this.dropdown.on("*",function(e,t){n.trigger(e,t)})},d.prototype._registerResultsEvents=function(){var n=this;this.results.on("*",function(e,t){n.trigger(e,t)})},d.prototype._registerEvents=function(){var n=this;this.on("open",function(){n.$container.addClass("select2-container--open")}),this.on("close",function(){n.$container.removeClass("select2-container--open")}),this.on("enable",function(){n.$container.removeClass("select2-container--disabled")}),this.on("disable",function(){n.$container.addClass("select2-container--disabled")}),this.on("blur",function(){n.$container.removeClass("select2-container--focus")}),this.on("query",function(t){n.isOpen()||n.trigger("open",{}),this.dataAdapter.query(t,function(e){n.trigger("results:all",{data:e,query:t})})}),this.on("query:append",function(t){this.dataAdapter.query(t,function(e){n.trigger("results:append",{data:e,query:t})})}),this.on("keypress",function(e){var t=e.which;n.isOpen()?t===r.ESC||t===r.TAB||t===r.UP&&e.altKey?(n.close(),e.preventDefault()):t===r.ENTER?(n.trigger("results:select",{}),e.preventDefault()):t===r.SPACE&&e.ctrlKey?(n.trigger("results:toggle",{}),e.preventDefault()):t===r.UP?(n.trigger("results:previous",{}),e.preventDefault()):t===r.DOWN&&(n.trigger("results:next",{}),e.preventDefault()):(t===r.ENTER||t===r.SPACE||t===r.DOWN&&e.altKey)&&(n.open(),e.preventDefault())})},d.prototype._syncAttributes=function(){this.options.set("disabled",this.$element.prop("disabled")),this.options.get("disabled")?(this.isOpen()&&this.close(),this.trigger("disable",{})):this.trigger("enable",{})},d.prototype._syncSubtree=function(e,t){var n=!1,r=this;if(!e||!e.target||"OPTION"===e.target.nodeName||"OPTGROUP"===e.target.nodeName){if(t)if(t.addedNodes&&0<t.addedNodes.length)for(var i=0;i<t.addedNodes.length;i++){t.addedNodes[i].selected&&(n=!0)}else t.removedNodes&&0<t.removedNodes.length&&(n=!0);else n=!0;n&&this.dataAdapter.current(function(e){r.trigger("selection:update",{data:e})})}},d.prototype.trigger=function(e,t){var n=d.__super__.trigger,r={open:"opening",close:"closing",select:"selecting",unselect:"unselecting",clear:"clearing"};if(void 0===t&&(t={}),e in r){var i=r[e],o={prevented:!1,name:e,args:t};if(n.call(this,i,o),o.prevented)return void(t.prevented=!0)}n.call(this,e,t)},d.prototype.toggleDropdown=function(){this.options.get("disabled")||(this.isOpen()?this.close():this.open())},d.prototype.open=function(){this.isOpen()||this.trigger("query",{})},d.prototype.close=function(){this.isOpen()&&this.trigger("close",{})},d.prototype.isOpen=function(){return this.$container.hasClass("select2-container--open")},d.prototype.hasFocus=function(){return this.$container.hasClass("select2-container--focus")},d.prototype.focus=function(e){this.hasFocus()||(this.$container.addClass("select2-container--focus"),this.trigger("focus",{}))},d.prototype.enable=function(e){this.options.get("debug")&&window.console&&console.warn&&console.warn('Select2: The `select2("enable")` method has been deprecated and will be removed in later Select2 versions. Use $element.prop("disabled") instead.'),null!=e&&0!==e.length||(e=[!0]);var t=!e[0];this.$element.prop("disabled",t)},d.prototype.data=function(){this.options.get("debug")&&0<arguments.length&&window.console&&console.warn&&console.warn('Select2: Data can no longer be set using `select2("data")`. You should consider setting the value instead using `$element.val()`.');var t=[];return this.dataAdapter.current(function(e){t=e}),t},d.prototype.val=function(e){if(this.options.get("debug")&&window.console&&console.warn&&console.warn('Select2: The `select2("val")` method has been deprecated and will be removed in later Select2 versions. Use $element.val() instead.'),null==e||0===e.length)return this.$element.val();var t=e[0];i.isArray(t)&&(t=i.map(t,function(e){return e.toString()})),this.$element.val(t).trigger("change")},d.prototype.destroy=function(){this.$container.remove(),this.$element[0].detachEvent&&this.$element[0].detachEvent("onpropertychange",this._syncA),null!=this._observer?(this._observer.disconnect(),this._observer=null):this.$element[0].removeEventListener&&(this.$element[0].removeEventListener("DOMAttrModified",this._syncA,!1),this.$element[0].removeEventListener("DOMNodeInserted",this._syncS,!1),this.$element[0].removeEventListener("DOMNodeRemoved",this._syncS,!1)),this._syncA=null,this._syncS=null,this.$element.off(".select2"),this.$element.attr("tabindex",u.GetData(this.$element[0],"old-tabindex")),this.$element.removeClass("select2-hidden-accessible"),this.$element.attr("aria-hidden","false"),u.RemoveData(this.$element[0]),this.$element.removeData("select2"),this.dataAdapter.destroy(),this.selection.destroy(),this.dropdown.destroy(),this.results.destroy(),this.dataAdapter=null,this.selection=null,this.dropdown=null,this.results=null},d.prototype.render=function(){var e=i('<span class="select2 select2-container"><span class="selection"></span><span class="dropdown-wrapper" aria-hidden="true"></span></span>');return e.attr("dir",this.options.get("dir")),this.$container=e,this.$container.addClass("select2-container--"+this.options.get("theme")),u.StoreData(e[0],"element",this.$element),e},d}),e.define("jquery-mousewheel",["jquery"],function(e){return e}),e.define("jquery.select2",["jquery","jquery-mousewheel","./select2/core","./select2/defaults","./select2/utils"],function(i,e,o,t,s){if(null==i.fn.select2){var a=["open","close","destroy"];i.fn.select2=function(t){if("object"==typeof(t=t||{}))return this.each(function(){var e=i.extend(!0,{},t);new o(i(this),e)}),this;if("string"!=typeof t)throw new Error("Invalid arguments for Select2: "+t);var n,r=Array.prototype.slice.call(arguments,1);return this.each(function(){var e=s.GetData(this,"select2");null==e&&window.console&&console.error&&console.error("The select2('"+t+"') method was called on an element that is not using Select2."),n=e[t].apply(e,r)}),-1<i.inArray(t,a)?this:n}}return null==i.fn.select2.defaults&&(i.fn.select2.defaults=t),o}),{define:e.define,require:e.require}}(),t=e.require("jquery.select2");return u.fn.select2.amd=e,t});
// </nowiki>
42xu27lpnwj8a1n5oae6me1jq6szo63
User:Syunsyunminmin/select2/select2.min.css
2
179394
769459
2026-10-21T13:57:54Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 3680108:
769459
css
text/css
/*
Twinkle: adapted for Japanese Wikipedia; upstream authors and Japanese adaptations are credited in UPSTREAM.md.
Upstream source: https://github.com/wikimedia-gadgets/twinkle/blob/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/lib/select2.min.css
Upstream author history: https://github.com/wikimedia-gadgets/twinkle/commits/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/lib/select2.min.css
License: MIT; see the original license notice below. See https://github.com/wikimedia-gadgets/twinkle/blob/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/lib/select2-LICENSE.md
Japanese adaptation: Syunsyunminmin; includes later synchronization changes.
*/
/*! Select2 4.0.12 | https://github.com/select2/select2/blob/master/LICENSE.md */
.select2-container{box-sizing:border-box;display:inline-block;margin:0;position:relative;vertical-align:middle}.select2-container .select2-selection--single{box-sizing:border-box;cursor:pointer;display:block;height:28px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--single .select2-selection__rendered{display:block;padding-left:8px;padding-right:20px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-selection--single .select2-selection__clear{position:relative}.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered{padding-right:8px;padding-left:20px}.select2-container .select2-selection--multiple{box-sizing:border-box;cursor:pointer;display:block;min-height:32px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--multiple .select2-selection__rendered{display:inline-block;overflow:hidden;padding-left:8px;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-search--inline{float:left}.select2-container .select2-search--inline .select2-search__field{box-sizing:border-box;border:none;font-size:100%;margin-top:5px;padding:0}.select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-dropdown{background-color:white;border:1px solid #aaa;border-radius:4px;box-sizing:border-box;display:block;position:absolute;left:-100000px;width:100%;z-index:1051}.select2-results{display:block}.select2-results__options{list-style:none;margin:0;padding:0}.select2-results__option{padding:6px;user-select:none;-webkit-user-select:none}.select2-results__option[aria-selected]{cursor:pointer}.select2-container--open .select2-dropdown{left:0}.select2-container--open .select2-dropdown--above{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--open .select2-dropdown--below{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-search--dropdown{display:block;padding:4px}.select2-search--dropdown .select2-search__field{padding:4px;width:100%;box-sizing:border-box}.select2-search--dropdown .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-search--dropdown.select2-search--hide{display:none}.select2-close-mask{border:0;margin:0;padding:0;display:block;position:fixed;left:0;top:0;min-height:100%;min-width:100%;height:auto;width:auto;opacity:0;z-index:99;background-color:#fff;filter:alpha(opacity=0)}.select2-hidden-accessible{border:0 !important;clip:rect(0 0 0 0) !important;-webkit-clip-path:inset(50%) !important;clip-path:inset(50%) !important;height:1px !important;overflow:hidden !important;padding:0 !important;position:absolute !important;width:1px !important;white-space:nowrap !important}.select2-container--default .select2-selection--single{background-color:#fff;border:1px solid #aaa;border-radius:4px}.select2-container--default .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--default .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold}.select2-container--default .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--default .select2-selection--single .select2-selection__arrow{height:26px;position:absolute;top:1px;right:1px;width:20px}.select2-container--default .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow{left:1px;right:auto}.select2-container--default.select2-container--disabled .select2-selection--single{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear{display:none}.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--default .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text}.select2-container--default .select2-selection--multiple .select2-selection__rendered{box-sizing:border-box;list-style:none;margin:0;padding:0 5px;width:100%}.select2-container--default .select2-selection--multiple .select2-selection__rendered li{list-style:none}.select2-container--default .select2-selection--multiple .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;margin-top:5px;margin-right:10px;padding:1px}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;cursor:default;float:left;margin-right:5px;margin-top:5px;padding:0 5px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:#999;cursor:pointer;display:inline-block;font-weight:bold;margin-right:2px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#333}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice,.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-search--inline{float:right}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice{margin-left:5px;margin-right:auto}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{margin-left:2px;margin-right:auto}.select2-container--default.select2-container--focus .select2-selection--multiple{border:solid black 1px;outline:0}.select2-container--default.select2-container--disabled .select2-selection--multiple{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection__choice__remove{display:none}.select2-container--default.select2-container--open.select2-container--above .select2-selection--single,.select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple{border-top-left-radius:0;border-top-right-radius:0}.select2-container--default.select2-container--open.select2-container--below .select2-selection--single,.select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--default .select2-search--dropdown .select2-search__field{border:1px solid #aaa}.select2-container--default .select2-search--inline .select2-search__field{background:transparent;border:none;outline:0;box-shadow:none;-webkit-appearance:textfield}.select2-container--default .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--default .select2-results__option[role=group]{padding:0}.select2-container--default .select2-results__option[aria-disabled=true]{color:#999}.select2-container--default .select2-results__option[aria-selected=true]{background-color:#ddd}.select2-container--default .select2-results__option .select2-results__option{padding-left:1em}.select2-container--default .select2-results__option .select2-results__option .select2-results__group{padding-left:0}.select2-container--default .select2-results__option .select2-results__option .select2-results__option{margin-left:-1em;padding-left:2em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-2em;padding-left:3em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-3em;padding-left:4em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-4em;padding-left:5em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-5em;padding-left:6em}.select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#5897fb;color:white}.select2-container--default .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic .select2-selection--single{background-color:#f7f7f7;border:1px solid #aaa;border-radius:4px;outline:0;background-image:-webkit-linear-gradient(top, #fff 50%, #eee 100%);background-image:-o-linear-gradient(top, #fff 50%, #eee 100%);background-image:linear-gradient(to bottom, #fff 50%, #eee 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic .select2-selection--single:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--classic .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;margin-right:10px}.select2-container--classic .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--classic .select2-selection--single .select2-selection__arrow{background-color:#ddd;border:none;border-left:1px solid #aaa;border-top-right-radius:4px;border-bottom-right-radius:4px;height:26px;position:absolute;top:1px;right:1px;width:20px;background-image:-webkit-linear-gradient(top, #eee 50%, #ccc 100%);background-image:-o-linear-gradient(top, #eee 50%, #ccc 100%);background-image:linear-gradient(to bottom, #eee 50%, #ccc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0)}.select2-container--classic .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow{border:none;border-right:1px solid #aaa;border-radius:0;border-top-left-radius:4px;border-bottom-left-radius:4px;left:1px;right:auto}.select2-container--classic.select2-container--open .select2-selection--single{border:1px solid #5897fb}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow{background:transparent;border:none}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single{border-top:none;border-top-left-radius:0;border-top-right-radius:0;background-image:-webkit-linear-gradient(top, #fff 0%, #eee 50%);background-image:-o-linear-gradient(top, #fff 0%, #eee 50%);background-image:linear-gradient(to bottom, #fff 0%, #eee 50%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0;background-image:-webkit-linear-gradient(top, #eee 50%, #fff 100%);background-image:-o-linear-gradient(top, #eee 50%, #fff 100%);background-image:linear-gradient(to bottom, #eee 50%, #fff 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0)}.select2-container--classic .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text;outline:0}.select2-container--classic .select2-selection--multiple:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--multiple .select2-selection__rendered{list-style:none;margin:0;padding:0 5px}.select2-container--classic .select2-selection--multiple .select2-selection__clear{display:none}.select2-container--classic .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;cursor:default;float:left;margin-right:5px;margin-top:5px;padding:0 5px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove{color:#888;cursor:pointer;display:inline-block;font-weight:bold;margin-right:2px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover{color:#555}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice{float:right;margin-left:5px;margin-right:auto}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{margin-left:2px;margin-right:auto}.select2-container--classic.select2-container--open .select2-selection--multiple{border:1px solid #5897fb}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--classic .select2-search--dropdown .select2-search__field{border:1px solid #aaa;outline:0}.select2-container--classic .select2-search--inline .select2-search__field{outline:0;box-shadow:none}.select2-container--classic .select2-dropdown{background-color:#fff;border:1px solid transparent}.select2-container--classic .select2-dropdown--above{border-bottom:none}.select2-container--classic .select2-dropdown--below{border-top:none}.select2-container--classic .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--classic .select2-results__option[role=group]{padding:0}.select2-container--classic .select2-results__option[aria-disabled=true]{color:grey}.select2-container--classic .select2-results__option--highlighted[aria-selected]{background-color:#3875d7;color:#fff}.select2-container--classic .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic.select2-container--open .select2-dropdown{border-color:#5897fb}
feedslv6y9tw760gpadxa0698zj4qta
769526
769459
2026-10-22T00:00:09Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 1406828: rm cmt
769526
css
text/css
/*! Select2 4.0.12 | https://github.com/select2/select2/blob/master/LICENSE.md */
.select2-container{box-sizing:border-box;display:inline-block;margin:0;position:relative;vertical-align:middle}.select2-container .select2-selection--single{box-sizing:border-box;cursor:pointer;display:block;height:28px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--single .select2-selection__rendered{display:block;padding-left:8px;padding-right:20px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-selection--single .select2-selection__clear{position:relative}.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered{padding-right:8px;padding-left:20px}.select2-container .select2-selection--multiple{box-sizing:border-box;cursor:pointer;display:block;min-height:32px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--multiple .select2-selection__rendered{display:inline-block;overflow:hidden;padding-left:8px;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-search--inline{float:left}.select2-container .select2-search--inline .select2-search__field{box-sizing:border-box;border:none;font-size:100%;margin-top:5px;padding:0}.select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-dropdown{background-color:white;border:1px solid #aaa;border-radius:4px;box-sizing:border-box;display:block;position:absolute;left:-100000px;width:100%;z-index:1051}.select2-results{display:block}.select2-results__options{list-style:none;margin:0;padding:0}.select2-results__option{padding:6px;user-select:none;-webkit-user-select:none}.select2-results__option[aria-selected]{cursor:pointer}.select2-container--open .select2-dropdown{left:0}.select2-container--open .select2-dropdown--above{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--open .select2-dropdown--below{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-search--dropdown{display:block;padding:4px}.select2-search--dropdown .select2-search__field{padding:4px;width:100%;box-sizing:border-box}.select2-search--dropdown .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-search--dropdown.select2-search--hide{display:none}.select2-close-mask{border:0;margin:0;padding:0;display:block;position:fixed;left:0;top:0;min-height:100%;min-width:100%;height:auto;width:auto;opacity:0;z-index:99;background-color:#fff;filter:alpha(opacity=0)}.select2-hidden-accessible{border:0 !important;clip:rect(0 0 0 0) !important;-webkit-clip-path:inset(50%) !important;clip-path:inset(50%) !important;height:1px !important;overflow:hidden !important;padding:0 !important;position:absolute !important;width:1px !important;white-space:nowrap !important}.select2-container--default .select2-selection--single{background-color:#fff;border:1px solid #aaa;border-radius:4px}.select2-container--default .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--default .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold}.select2-container--default .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--default .select2-selection--single .select2-selection__arrow{height:26px;position:absolute;top:1px;right:1px;width:20px}.select2-container--default .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow{left:1px;right:auto}.select2-container--default.select2-container--disabled .select2-selection--single{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear{display:none}.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--default .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text}.select2-container--default .select2-selection--multiple .select2-selection__rendered{box-sizing:border-box;list-style:none;margin:0;padding:0 5px;width:100%}.select2-container--default .select2-selection--multiple .select2-selection__rendered li{list-style:none}.select2-container--default .select2-selection--multiple .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;margin-top:5px;margin-right:10px;padding:1px}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;cursor:default;float:left;margin-right:5px;margin-top:5px;padding:0 5px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:#999;cursor:pointer;display:inline-block;font-weight:bold;margin-right:2px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#333}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice,.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-search--inline{float:right}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice{margin-left:5px;margin-right:auto}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{margin-left:2px;margin-right:auto}.select2-container--default.select2-container--focus .select2-selection--multiple{border:solid black 1px;outline:0}.select2-container--default.select2-container--disabled .select2-selection--multiple{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection__choice__remove{display:none}.select2-container--default.select2-container--open.select2-container--above .select2-selection--single,.select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple{border-top-left-radius:0;border-top-right-radius:0}.select2-container--default.select2-container--open.select2-container--below .select2-selection--single,.select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--default .select2-search--dropdown .select2-search__field{border:1px solid #aaa}.select2-container--default .select2-search--inline .select2-search__field{background:transparent;border:none;outline:0;box-shadow:none;-webkit-appearance:textfield}.select2-container--default .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--default .select2-results__option[role=group]{padding:0}.select2-container--default .select2-results__option[aria-disabled=true]{color:#999}.select2-container--default .select2-results__option[aria-selected=true]{background-color:#ddd}.select2-container--default .select2-results__option .select2-results__option{padding-left:1em}.select2-container--default .select2-results__option .select2-results__option .select2-results__group{padding-left:0}.select2-container--default .select2-results__option .select2-results__option .select2-results__option{margin-left:-1em;padding-left:2em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-2em;padding-left:3em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-3em;padding-left:4em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-4em;padding-left:5em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-5em;padding-left:6em}.select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#5897fb;color:white}.select2-container--default .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic .select2-selection--single{background-color:#f7f7f7;border:1px solid #aaa;border-radius:4px;outline:0;background-image:-webkit-linear-gradient(top, #fff 50%, #eee 100%);background-image:-o-linear-gradient(top, #fff 50%, #eee 100%);background-image:linear-gradient(to bottom, #fff 50%, #eee 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic .select2-selection--single:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--classic .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;margin-right:10px}.select2-container--classic .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--classic .select2-selection--single .select2-selection__arrow{background-color:#ddd;border:none;border-left:1px solid #aaa;border-top-right-radius:4px;border-bottom-right-radius:4px;height:26px;position:absolute;top:1px;right:1px;width:20px;background-image:-webkit-linear-gradient(top, #eee 50%, #ccc 100%);background-image:-o-linear-gradient(top, #eee 50%, #ccc 100%);background-image:linear-gradient(to bottom, #eee 50%, #ccc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0)}.select2-container--classic .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow{border:none;border-right:1px solid #aaa;border-radius:0;border-top-left-radius:4px;border-bottom-left-radius:4px;left:1px;right:auto}.select2-container--classic.select2-container--open .select2-selection--single{border:1px solid #5897fb}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow{background:transparent;border:none}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single{border-top:none;border-top-left-radius:0;border-top-right-radius:0;background-image:-webkit-linear-gradient(top, #fff 0%, #eee 50%);background-image:-o-linear-gradient(top, #fff 0%, #eee 50%);background-image:linear-gradient(to bottom, #fff 0%, #eee 50%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0;background-image:-webkit-linear-gradient(top, #eee 50%, #fff 100%);background-image:-o-linear-gradient(top, #eee 50%, #fff 100%);background-image:linear-gradient(to bottom, #eee 50%, #fff 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0)}.select2-container--classic .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text;outline:0}.select2-container--classic .select2-selection--multiple:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--multiple .select2-selection__rendered{list-style:none;margin:0;padding:0 5px}.select2-container--classic .select2-selection--multiple .select2-selection__clear{display:none}.select2-container--classic .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;cursor:default;float:left;margin-right:5px;margin-top:5px;padding:0 5px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove{color:#888;cursor:pointer;display:inline-block;font-weight:bold;margin-right:2px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover{color:#555}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice{float:right;margin-left:5px;margin-right:auto}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{margin-left:2px;margin-right:auto}.select2-container--classic.select2-container--open .select2-selection--multiple{border:1px solid #5897fb}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--classic .select2-search--dropdown .select2-search__field{border:1px solid #aaa;outline:0}.select2-container--classic .select2-search--inline .select2-search__field{outline:0;box-shadow:none}.select2-container--classic .select2-dropdown{background-color:#fff;border:1px solid transparent}.select2-container--classic .select2-dropdown--above{border-bottom:none}.select2-container--classic .select2-dropdown--below{border-top:none}.select2-container--classic .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--classic .select2-results__option[role=group]{padding:0}.select2-container--classic .select2-results__option[aria-disabled=true]{color:grey}.select2-container--classic .select2-results__option--highlighted[aria-selected]{background-color:#3875d7;color:#fff}.select2-container--classic .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic.select2-container--open .select2-dropdown{border-color:#5897fb}
g1469rftkzxri386o3a0m1ueea3cqkp
User:Syunsyunminmin/Twinkle/twinkleconfig.js
2
179395
769460
2026-10-21T13:57:56Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 3680108:
769460
javascript
text/javascript
/*
Twinkle: adapted for Japanese Wikipedia; upstream authors and Japanese adaptations are credited in UPSTREAM.md.
Upstream source: https://github.com/wikimedia-gadgets/twinkle/blob/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/src/modules/twinkleconfig.js
Upstream author history: https://github.com/wikimedia-gadgets/twinkle/commits/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/src/modules/twinkleconfig.js
License: CC BY-SA 3.0 Unported. See https://github.com/wikimedia-gadgets/twinkle/blob/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/LICENSE.txt
Japanese adaptation: Syunsyunminmin; includes later synchronization changes.
*/
// <nowiki>
(function() {
/*
****************************************
*** twinkleconfig.js: Preferences module
****************************************
* Mode of invocation: Adds configuration form to User:Syunsyunminmin/Twinkle/Preferences,
and adds an ad box to the top of user subpages belonging to the
currently logged-in user which end in '.js'
* Active on: What I just said. Yeah.
*/
Twinkle.config = {};
Twinkle.config.watchlistEnums = {
yes: 'ウォッチリストに追加 (無期限)',
no: 'ウォッチリストに追加しない',
default: 'サイトの設定に従う',
'1 week': '1週間ウォッチ',
'1 month': '1ヶ月ウォッチ',
'3 months': '3ヶ月ウォッチ',
'6 months': '6ヶ月ウォッチ'
};
Twinkle.config.commonSets = {
csdCriteria: {
db: 'その他 ({{即時削除|その他}})',
g1: 'G1', g2: 'G2', g3: 'G3', g4: 'G4', g5: 'G5', g6: 'G6', g8: 'G8', g9: 'G9', g10: 'G10',
a1: 'A1',
u1: 'U1', u2: 'U2', u3: 'U3',
'f1-2': 'F1-2', 'f1-3': 'F1-3', 'f1-4': 'F1-4', 'f1-5': 'F1-5', 'f1-6': 'F1-6',
f3: 'F3', f5: 'F5', f6: 'F6', f7: 'F7', f8: 'F8', f9: 'F9',
c1: 'C1', c3: 'C3', c6: 'C6',
'r1-1': 'R1-1', 'r1-2': 'R1-2', 'r1-3': 'R1-3', 'r1-4': 'R1-4',
'r2-1': 'R2-1', 'r2-2': 'R2-2', 'r2-3': 'R2-3', 'r2-5': 'R2-5', 'r2-6': 'R2-6',
r4: 'R4'
},
csdCriteriaDisplayOrder: [
'db',
'g1', 'g2', 'g3', 'g4', 'g5', 'g6', 'g8', 'g9', 'g10',
'a1',
'u1', 'u2', 'u3',
'f1-2', 'f1-3', 'f1-4', 'f1-5', 'f1-6', 'f3', 'f5', 'f6', 'f7', 'f8', 'f9',
'c1', 'c3', 'c6',
'r1-1', 'r1-2', 'r1-3', 'r1-4', 'r2-1', 'r2-2', 'r2-3', 'r2-5', 'r2-6', 'r4'
],
csdAndImageDeletionCriteria: {
db: 'その他 ({{即時削除|その他}})',
g1: 'G1', g2: 'G2', g3: 'G3', g4: 'G4', g5: 'G5', g6: 'G6', g8: 'G8', g9: 'G9', g10: 'G10',
a1: 'A1',
u1: 'U1', u2: 'U2', u3: 'U3',
'f1-2': 'F1-2', 'f1-3': 'F1-3', 'f1-4': 'F1-4', 'f1-5': 'F1-5', 'f1-6': 'F1-6',
f3: 'F3', f5: 'F5', f6: 'F6', f7: 'F7', f8: 'F8', f9: 'F9',
c1: 'C1', c3: 'C3', c6: 'C6',
'r1-1': 'R1-1', 'r1-2': 'R1-2', 'r1-3': 'R1-3', 'r1-4': 'R1-4',
'r2-1': 'R2-1', 'r2-2': 'R2-2', 'r2-3': 'R2-3', 'r2-5': 'R2-5', 'r2-6': 'R2-6',
r4: 'R4'
},
csdAndImageDeletionCriteriaDisplayOrder: [
'db',
'g1', 'g2', 'g3', 'g4', 'g5', 'g6', 'g8', 'g9', 'g10',
'a1',
'u1', 'u2', 'u3',
'f1-2', 'f1-3', 'f1-4', 'f1-5', 'f1-6', 'f3', 'f5', 'f6', 'f7', 'f8', 'f9',
'c1', 'c3', 'c6',
'r1-1', 'r1-2', 'r1-3', 'r1-4', 'r2-1', 'r2-2', 'r2-3', 'r2-5', 'r2-6', 'r4'
],
namespacesNoSpecial: {
0: '標準',
1: 'ノート (標準)',
2: '利用者',
3: '利用者‐会話',
4: 'Wikipedia',
5: 'Wikipedia‐ノート',
6: 'ファイル',
7: 'ファイル‐ノート',
8: 'MediaWiki',
9: 'MediaWiki‐ノート',
10: 'Template',
11: 'Template‐ノート',
12: 'Help',
13: 'Help‐ノート',
14: 'Category',
15: 'Category‐ノート',
100: 'Portal',
101: 'Portal‐ノート',
710: 'TimedText',
711: 'TimedText‐ノート',
828: 'Module',
829: 'Module‐ノート'
}
};
Twinkle.config.commonSets.csdCriteriaDisplayOrder = Object.keys(Twinkle.config.commonSets.csdCriteria);
Twinkle.config.commonSets.csdAndImageDeletionCriteriaDisplayOrder = Object.keys(Twinkle.config.commonSets.csdAndImageDeletionCriteria);
/**
* Section entry format:
*
* {
* title: <human-readable section title>,
* module: <name of the associated module, used to link to sections>,
* adminOnly: <true for admin-only sections>,
* hidden: <true for advanced preferences that rarely need to be changed - they can still be modified by manually editing twinkleoptions.js>,
* preferences: [
* {
* name: <TwinkleConfig property name>,
* label: <human-readable short description - used as a form label>,
* helptip: <(optional) human-readable text (using valid HTML) that complements the description, like limits, warnings, etc.>
* adminOnly: <true for admin-only preferences>,
* type: <string|boolean|integer|enum|set|customList> (customList stores an array of JSON objects { value, label }),
* enumValues: <for type = "enum": a JSON object where the keys are the internal names and the values are human-readable strings>,
* setValues: <for type = "set": a JSON object where the keys are the internal names and the values are human-readable strings>,
* setDisplayOrder: <(optional) for type = "set": an array containing the keys of setValues (as strings) in the order that they are displayed>,
* customListValueTitle: <for type = "customList": the heading for the left "value" column in the custom list editor>,
* customListLabelTitle: <for type = "customList": the heading for the right "label" column in the custom list editor>
* },
* . . .
* ]
* },
* . . .
*
*/
Twinkle.config.sections = [
{
title: '全般',
module: 'general',
preferences: [
// TwinkleConfig.userTalkPageMode may take arguments:
// 'window': open a new window, remember the opened window
// 'tab': opens in a new tab, if possible.
// 'blank': force open in a new window, even if such a window exists
{
name: 'userTalkPageMode',
label: '利用者会話ページを開く時は次のページで開く',
type: 'enum',
enumValues: { window: 'ウィンドウ内で他の利用者会話ページを置き換える', tab: '新しいタブ', blank: '完全に新しいウィンドウ' }
},
// TwinkleConfig.dialogLargeFont (boolean)
{
name: 'dialogLargeFont',
label: 'Twinkleダイアログで大きな文字を使用する',
type: 'boolean'
},
// Twinkle.config.disabledModules (array)
{
name: 'disabledModules',
label: '選択したTwinkleモジュールをオフにする',
helptip: 'チェックを入れた機能は無効になります。再び有効にするにはチェックを外してください。<br />既定でチェックが入っているものは準備中の機能です。無効にしていても、関連する設定項目は表示されます。',
type: 'set',
setValues: { arv: '荒らしの報告 (ARV)', warn: '利用者への警告 (Warn)', talkback: '返信通知 (Talkback)', speedy: '即時削除 (CSD)', xfd: '削除依頼 (XfD)', image: '画像 (DI)', protect: '保護 (RPP)', tag: 'メンテナンス用テンプレートの付与 (Tag)', diff: '差分', unlink: 'リンク解除', rollback: '差し戻しと巻き戻し' }
},
// Twinkle.config.disabledSysopModules (array)
{
name: 'disabledSysopModules',
label: '選択した管理者専用モジュールをオフにする',
helptip: 'ここで選択したものは使用できないので、注意してください。再び有効にするにはチェックを外してください。',
adminOnly: true,
type: 'set',
setValues: { block: '利用者のブロック', batchdelete: '一括削除 (D-batch)', batchprotect: '一括保護 (P-batch)', batchundelete: '一括復元 (Und-batch)' }
}
]
},
{
title: '荒らしの報告 (ARV)',
module: 'arv',
preferences: [
{
name: 'spiWatchReport',
label: '多重アカウントの調査依頼ページをウォッチリストに追加する',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
}
]
},
{
title: '利用者のブロック',
module: 'block',
adminOnly: true,
preferences: [
// TwinkleConfig.defaultToBlock64 (boolean)
// Whether to default to just blocking the /64 on or off
{
name: 'defaultToBlock64',
label: 'IPv6アドレスでは、既定で/64の範囲をブロックする',
type: 'boolean'
},
// TwinkleConfig.defaultToPartialBlocks (boolean)
// Whether to default partial blocks on or off
{
name: 'defaultToPartialBlocks',
label: 'ブロックメニューを開いた際、既定で部分ブロックを選択する',
helptip: '利用者がすでにブロックされている場合は、現在のブロックの種類が優先されます。',
type: 'boolean'
},
// TwinkleConfig.blankTalkpageOnIndefBlock (boolean)
// if true, blank the talk page when issuing an indef block notice (per [[WP:UWUL#Indefinitely blocked users]])
{
name: 'blankTalkpageOnIndefBlock',
label: '無期限ブロックの通知時に利用者会話ページを白紙化する',
helptip: '詳しくは<a href="' + mw.util.getUrl('Wikipedia:WikiProject_User_warnings/Usage_and_layout#Indefinitely_blocked_users') + '">WP:UWUL</a>を参照してください。',
type: 'boolean'
}
]
},
{
title: '画像削除 (DI)',
module: 'image',
preferences: [
// TwinkleConfig.notifyUserOnDeli (boolean)
// If the user should be notified after placing a file deletion tag
{
name: 'notifyUserOnDeli',
label: '既定で"アップロード者に通知"にチェックを入れる',
type: 'boolean'
},
// TwinkleConfig.deliWatchPage (string)
// The watchlist setting of the page tagged for deletion.
{
name: 'deliWatchPage',
label: 'タグ付け時にファイルページをウォッチリストに追加',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
// TwinkleConfig.deliWatchUser (string)
// The watchlist setting of the user talk page if a notification is placed.
{
name: 'deliWatchUser',
label: '通知時に、アップロード者の会話ページをウォッチリストに追加する',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
}
]
},
{
title: 'ページの保護 ' + (Morebits.userIsSysop ? '(PP)' : '(RPP)'),
module: 'protect',
preferences: [
{
name: 'watchRequestedPages',
label: '保護依頼時にページをウォッチリストに追加する',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
{
name: 'watchPPTaggedPages',
label: '保護テンプレートでページをタグ付けする際、ウォッチリストにページを追加する',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
{
name: 'watchProtectedPages',
label: '保護する際にページをウォッチリストに追加する',
helptip: 'ページ保護後にタグ付けも行う場合は、そちらが優先されます。',
adminOnly: true,
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
}
]
},
{
title: '差し戻しと巻き戻し', // twinklerollback module
module: 'rollback',
preferences: [
// TwinkleConfig.autoMenuAfterRollback (bool)
// Option to automatically open the warning menu if the user talk page is opened post-reversion
{
name: 'autoMenuAfterRollback',
label: 'Twinkle差し戻し後、利用者会話ページでTwinkle警告メニューを自動的に開く',
helptip: '以下の該当するボックスにチェックが入っている場合のみ動作します。',
type: 'boolean'
},
// TwinkleConfig.openTalkPage (array)
// What types of actions that should result in opening of talk page
{
name: 'openTalkPage',
label: '以下の種類の差し戻し後に、会話ページを開く。',
type: 'set',
setValues: { norm: '通常の差し戻し', vand: '荒らしの差し戻し' }
},
// TwinkleConfig.openTalkPageOnAutoRevert (bool)
// Defines if talk page should be opened when calling revert from contribs or recent changes pages. If set to true, openTalkPage defines then if talk page will be opened.
{
name: 'openTalkPageOnAutoRevert',
label: '投稿記録や最近の更新から差し戻しを実行した際に、会話ページを開く',
helptip: 'これが有効になっている場合、前の設定で必要なオプションが有効になっていなければ機能しません。',
type: 'boolean'
},
// TwinkleConfig.rollbackInPlace (bool)
//
{
name: 'rollbackInPlace',
label: '投稿記録や最近の更新から差し戻しする際、ページを更新しない。',
helptip: 'これが有効になっている場合、Twinkleは差し戻し後に投稿記録や最近の更新フィードを再読み込みしないため、一度に複数の編集を差し戻すことができます。',
type: 'boolean'
},
// TwinkleConfig.markRevertedPagesAsMinor (array)
// What types of actions that should result in marking edit as minor
{
name: 'markRevertedPagesAsMinor',
label: '以下の種類の差し戻しは細部の編集として行います',
type: 'set',
setValues: { norm: '通常の差し戻し', vand: '荒らしの差し戻し', torev: '"版の復元"' }
},
// TwinkleConfig.watchRevertedPages (array)
// What types of actions that should result in forced addition to watchlist
{
name: 'watchRevertedPages',
label: '以下の種類の差し戻しの際、ページをウォッチリストに追加する',
type: 'set',
setValues: { norm: '通常の差し戻し', vand: '荒らしの差し戻し', torev: '"版の復元"' }
},
// TwinkleConfig.watchRevertedExpiry
// If any of the above items are selected, whether to expire the watch
{
name: 'watchRevertedExpiry',
label: 'ページを差し戻した際、ウォッチする期間は',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
// TwinkleConfig.offerReasonOnNormalRevert (boolean)
// If to offer a prompt for extra summary reason for normal reverts, default to true
{
name: 'offerReasonOnNormalRevert',
label: '通常の差し戻しに理由の入力を求めるプロンプト',
helptip: '"通常の"差し戻しは [差し戻し] リンクから実行される差し戻しです。',
type: 'boolean'
},
{
name: 'confirmOnRollback',
label: '差し戻す前に確認を求める (すべての機器)',
helptip: 'ペンやタッチデバイスを使う人、慢性的に優柔不断な人向け。',
type: 'boolean'
},
{
name: 'confirmOnMobileRollback',
label: '差し戻す前に確認を求める (携帯機器のみ)',
helptip: '携帯機器での誤動作を防ぐ。',
type: 'boolean'
},
// TwinkleConfig.showRollbackLinks (array)
// Where Twinkle should show rollback links:
// diff, others, mine, contribs, history, recent
// Note from TTO: |contribs| seems to be equal to |others| + |mine|, i.e. redundant, so I left it out heres
{
name: 'showRollbackLinks',
label: 'これらのページで差し戻しリンクを表示',
type: 'set',
setValues: { diff: '差分ページ', others: '他の利用者の投稿記録ページ', mine: '自分の投稿記録ページ', recent: '最近の更新や関連ページの更新の特別ページ', history: '履歴ページ' }
},
{
name: 'SummaryOnVandalRevert',
label: '荒らしの差し戻しの際の要約欄をカスタムする',
helptip: 'このボックスに要約を入力します。',
type: 'string',
obsolete: 'vandusersummary'
}
]
},
{
title: '即時削除 (CSD)',
module: 'speedy',
preferences: [
{
name: 'speedySelectionStyle',
label: 'ページをタグ付け/削除するタイミング',
type: 'enum',
enumValues: { buttonClick: '送信ボタンをクリックしたとき', radioClick: 'オプションをクリックするとすぐ' }
},
// TwinkleConfig.watchSpeedyPages (array)
// Whether to add speedy tagged or deleted pages to watchlist
{
name: 'watchSpeedyPages',
label: 'これらの基準を使用する場合は、ウォッチリストにページを追加する',
type: 'set',
setValues: Twinkle.config.commonSets.csdCriteria,
setDisplayOrder: Twinkle.config.commonSets.csdCriteriaDisplayOrder
},
// TwinkleConfig.watchSpeedyExpiry
// If any of the above items are selected, whether to expire the watch
{
name: 'watchSpeedyExpiry',
label: 'ページをタグ付けする場合、それをウォッチする期間',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
// TwinkleConfig.markSpeedyPagesAsPatrolled (boolean)
// If, when applying speedy template to page, to mark the page as triaged/patrolled (if the page was reached from NewPages)
{
name: 'markSpeedyPagesAsPatrolled',
label: 'タグ付けの際、ページを巡回済みとしてマークする。(可能な場合)',
helptip: 'これは最善の慣行に反するおそれがあるので、おそらくチェックすべきではないです。',
type: 'boolean'
},
// TwinkleConfig.promptForSpeedyDeletionSummary (array of strings)
{
name: 'promptForSpeedyDeletionSummary',
label: 'これらの基準で削除する場合、削除要約の編集を行う',
adminOnly: true,
type: 'set',
setValues: Twinkle.config.commonSets.csdAndImageDeletionCriteria,
setDisplayOrder: Twinkle.config.commonSets.csdAndImageDeletionCriteriaDisplayOrder
},
// TwinkleConfig.deleteTalkPageOnDelete (boolean)
// If talk page if exists should also be deleted (CSD G8) when spedying a page (admin only)
{
name: 'deleteTalkPageOnDelete',
label: '既定で"トークページも削除する"にチェックを入れる',
adminOnly: true,
type: 'boolean'
},
{
name: 'deleteRedirectsOnDelete',
label: '既定で「リダイレクトも削除」にチェックを入れる',
adminOnly: true,
type: 'boolean'
},
// TwinkleConfig.deleteSysopDefaultToDelete (boolean)
// Make the CSD screen default to "delete" instead of "tag" (admin only)
{
name: 'deleteSysopDefaultToDelete',
label: '既定でタグ付けではなく即時削除を行う',
helptip: 'CSDタグが既に存在する場合、Twinkleは常に「削除」モードが既定となります。',
adminOnly: true,
type: 'boolean'
},
// TwinkleConfig.speedyWindowWidth (integer)
// Defines the width of the Twinkle SD window in pixels
{
name: 'speedyWindowWidth',
label: '即時削除ウィンドウの幅(ピクセル)',
type: 'integer'
},
// TwinkleConfig.speedyWindowWidth (integer)
// Defines the width of the Twinkle SD window in pixels
{
name: 'speedyWindowHeight',
label: '即時削除ウィンドウの高さ(ピクセル)',
helptip: '大きなモニターをお持ちの場合は、これを増やしてください。',
type: 'integer'
},
{
name: 'logSpeedyNominations',
label: 'すべてのCSDへの指定記録を利用者ページに保持します',
helptip: '管理者以外は削除された投稿にアクセスできないため、利用者ページのログは、Twinkleを使用してCSDに指定したすべてのページを追跡するための優れた方法を提供します。 DIを使用してタグ付けされたファイルもこのログに追加されます。',
type: 'boolean'
},
{
name: 'speedyLogPageName',
label: 'この利用者サブページでCSDユーザースペースログを保持します',
helptip: 'このボックスにサブページ名を入力します。 CSDログは利用者:' + mw.config.get('wgUserName') + '/"サブページ名"にあります。 CSD利用者ページへの記録をオンにした場合にのみ機能します。',
type: 'string'
},
{
name: 'noLogOnSpeedyNomination',
label: 'これらの基準でタグ付けする場合は、利用者サブページの記録項目を作成しない',
type: 'set',
setValues: Twinkle.config.commonSets.csdAndImageDeletionCriteria,
setDisplayOrder: Twinkle.config.commonSets.csdAndImageDeletionCriteriaDisplayOrder
}
]
},
{
title: 'メンテナンス用テンプレートの付与 (Tag)',
module: 'tag',
preferences: [
{
name: 'watchTaggedVenues',
label: '次の種類のページにテンプレートを付与した際、ウォッチリストに追加する',
type: 'set',
setValues: { articles: '記事', drafts: '下書き', redirects: 'リダイレクト', files: 'ファイル' }
},
{
name: 'watchTaggedPages',
label: 'テンプレートを付与したページのウォッチ期間',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
{
name: 'watchMergeDiscussions',
label: '統合提案を開始した際、会話ページをウォッチリストに追加する',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
{
name: 'markTaggedPagesAsMinor',
label: 'テンプレートの付与を細部の編集として扱う',
type: 'boolean'
},
{
name: 'markTaggedPagesAsPatrolled',
label: '既定で「ページを巡回済み・確認済みにする」にチェックを入れる',
type: 'boolean'
},
{
name: 'tagArticleSortOrder',
label: '記事用テンプレートの既定の表示順',
type: 'enum',
enumValues: { cat: '分類別', alpha: 'アルファベット順' }
},
{
name: 'customTagList',
label: '追加で表示する記事・下書きのメンテナンス用テンプレート',
helptip: 'テンプレート一覧の末尾に選択肢を追加します。Twinkleにまだ登録されていないメンテナンス用テンプレートなどを指定できます。',
type: 'customList',
customListValueTitle: 'テンプレート名(波括弧なし)',
customListLabelTitle: 'テンプレート付与画面に表示する名前'
},
{
name: 'customFileTagList',
label: '追加で表示するファイルのメンテナンス用テンプレート',
helptip: 'ファイルに付与できるテンプレートを追加します。',
type: 'customList',
customListValueTitle: 'テンプレート名(波括弧なし)',
customListLabelTitle: 'テンプレート付与画面に表示する名前'
},
{
name: 'customRedirectTagList',
label: '追加で表示するリダイレクト分類用テンプレート',
helptip: 'リダイレクトに付与できるテンプレートを追加します。',
type: 'customList',
customListValueTitle: 'テンプレート名(波括弧なし)',
customListLabelTitle: 'テンプレート付与画面に表示する名前'
}
]
},
{
title: '返信通知 (Talkback)',
module: 'talkback',
preferences: [
{
name: 'markTalkbackAsMinor',
label: '返信通知を細部の編集として扱う',
type: 'boolean'
},
{
name: 'insertTalkbackSignature',
label: '返信通知に署名を挿入する',
type: 'boolean'
},
{
name: 'talkbackHeading',
label: '返信通知と会話ページへの案内に使用する節見出し',
tooltip: 'ウィキテキストの見出しに使う等号(==)は入力しないでください。',
type: 'string'
},
{
name: 'mailHeading',
label: 'メール送信通知に使用する節見出し',
tooltip: 'ウィキテキストの見出しに使う等号(==)は入力しないでください。',
type: 'string'
}
]
},
{
title: 'リンク解除 (Unlink)',
module: 'unlink',
preferences: [
// TwinkleConfig.unlinkNamespaces (array)
// In what namespaces unlink should happen, default in 0 (article), 10 (template), 100 (portal), and 118 (draft)
{
name: 'unlinkNamespaces',
label: '次の名前空間のページをリンク解除の対象にする',
helptip: '会話ページの過去ログ内のリンクまで解除されるおそれがあるため、会話用の名前空間は選択しないでください。',
type: 'set',
setValues: Twinkle.config.commonSets.namespacesNoSpecial
}
]
},
{
title: '利用者への警告 (Warn)',
module: 'warn',
preferences: [
// TwinkleConfig.defaultWarningGroup (int)
// Which level warning should be the default selected group, default is 1
{
name: 'defaultWarningGroup',
label: '既定の警告レベル',
type: 'enum',
enumValues: {
1: 'レベル1',
2: 'レベル2',
3: 'レベル3',
4: 'レベル4',
5: '即時の最終警告(レベル4im)',
6: '個別の問題に関する通知',
7: '個別の問題に関する警告',
// 8 was used for block templates before #260
9: '独自の警告',
10: 'すべての警告テンプレート',
11: 'レベルを自動選択(1~4)'
}
},
// TwinkleConfig.combinedSingletMenus (boolean)
// if true, show one menu with both single-issue notices and warnings instead of two separately
{
name: 'combinedSingletMenus',
label: '個別の問題に関する通知と警告のメニューをまとめる',
helptip: '有効にすると、既定の警告レベルで個別の問題に関する通知または警告を選んでいる場合に、統合したメニューを既定で表示します。',
type: 'boolean'
},
// TwinkleConfig.watchWarnings (string)
// Watchlist setting for the page which has been dispatched an warning or notice
{
name: 'watchWarnings',
label: '通知した利用者の会話ページをウォッチリストに追加する',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
// TwinkleConfig.oldSelect (boolean)
// if true, use the native select menu rather the select2-based one
{
name: 'oldSelect',
label: '検索機能のない従来の選択メニューを使用する',
type: 'boolean'
},
{
name: 'customWarningList',
label: '追加で表示する警告テンプレート',
helptip: 'テンプレートや利用者サブページを追加できます。追加した項目は警告画面の「独自の警告」に表示されます。',
type: 'customList',
customListValueTitle: 'テンプレート名(波括弧なし)',
customListLabelTitle: '警告一覧に表示する名前(編集要約にも使用)'
}
]
},
{
title: '削除依頼 (XfD)',
module: 'xfd',
preferences: [
{
name: 'logXfdNominations',
label: '削除依頼(XfD)を提出したページを利用者サブページに記録する',
helptip: 'Twinkleで削除依頼を提出したページを、利用者サブページの記録から確認できます。',
type: 'boolean'
},
{
name: 'xfdLogPageName',
label: '削除依頼の記録を保存する利用者サブページ名',
helptip: 'サブページ名を入力してください。記録先は「利用者:<i>利用者名</i>/<i>サブページ名</i>」です。削除依頼の記録を有効にしている場合にのみ機能します。',
type: 'string'
},
{
name: 'noLogOnXfdNomination',
label: '次の手続きに提出する場合は利用者サブページに記録しない',
type: 'set',
setValues: { afd: '記事の削除依頼 (AfD)', tfd: 'テンプレートの削除・統合依頼 (TfD)', ffd: 'ファイルの削除依頼 (FfD)', cfd: 'カテゴリの削除・変更依頼 (CfD)', cfds: 'カテゴリの即時変更依頼 (CfD/S)', mfd: 'その他のページの削除依頼 (MfD)', rfd: 'リダイレクトの削除・転送先変更依頼 (RfD)', rm: '改名依頼 (RM)' }
},
// TwinkleConfig.xfdWatchPage (string)
// The watchlist setting of the page being nominated for XfD.
{
name: 'xfdWatchPage',
label: '削除依頼の対象ページをウォッチリストに追加する',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
// TwinkleConfig.xfdWatchDiscussion (string)
// The watchlist setting of the newly created XfD page (for those processes that create discussion pages for each nomination),
// or the list page for the other processes.
{
name: 'xfdWatchDiscussion',
label: '削除依頼ページをウォッチリストに追加する',
helptip: 'AfD・MfDでは個別の依頼ページ、TfD・CfD・RfD・FfDでは日付別の依頼一覧を対象にします。',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
// TwinkleConfig.xfdWatchList (string)
// The watchlist setting of the XfD list page, *if* the discussion is on a separate page.
{
name: 'xfdWatchList',
label: '日付別の依頼一覧や一覧ページをウォッチリストに追加する(AfD・MfD)',
helptip: 'AfD・MfDにのみ適用します。AfDでは日付別の依頼一覧、MfDでは総合一覧ページに依頼ページが読み込まれます。',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
// TwinkleConfig.xfdWatchUser (string)
// The watchlist setting of the user talk page if they receive a notification.
{
name: 'xfdWatchUser',
label: '通知した初版投稿者の会話ページをウォッチリストに追加する',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
// TwinkleConfig.xfdWatchRelated (string)
// The watchlist setting of the target of a redirect being nominated for RfD.
{
name: 'xfdWatchRelated',
label: '通知時にリダイレクト先のページをウォッチリストに追加する',
helptip: 'RfDで、リダイレクト先の会話ページに通知する場合にのみ適用します。',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
{
name: 'markXfdPagesAsPatrolled',
label: '記事の削除依頼(AfD)への提出時にページを巡回済み・確認済みにする(可能な場合)',
type: 'boolean'
}
]
},
{
title: '詳細設定(非表示)',
hidden: true,
preferences: [
// twinklerollback.js: defines how many revision to query maximum, maximum possible is 50, default is 50
{
name: 'revertMaxRevisions',
type: 'integer'
},
// twinklewarn.js: When using the autolevel select option, how many days makes a prior warning stale
// Huggle is three days ([[Special:Diff/918980316]] and [[Special:Diff/919417999]]) while ClueBotNG is two:
// https://github.com/DamianZaremba/cluebotng/blob/4958e25d6874cba01c75f11debd2e511fd5a2ce5/bot/action_functions.php#L62
{
name: 'autolevelStaleDays',
type: 'integer'
},
// How many pages should be queried by deprod and batchdelete/protect/undelete
{
name: 'batchMax',
type: 'integer',
adminOnly: true
},
// How many pages should be processed at a time by deprod and batchdelete/protect/undelete
{
name: 'batchChunks',
type: 'integer',
adminOnly: true
}
]
}
]; // end of Twinkle.config.sections
Twinkle.config.init = function twinkleconfigInit() {
// create the config page at User:Syunsyunminmin/Twinkle/Preferences
if ((mw.config.get('wgNamespaceNumber') === mw.config.get('wgNamespaceIds').user && mw.config.get('wgTitle') === 'Syunsyunminmin/Twinkle/Preferences') &&
mw.config.get('wgAction') === 'view') {
if (!document.getElementById('twinkle-config')) {
return; // maybe the page is misconfigured, or something - but any attempt to modify it will be pointless
}
// set style to nothing to prevent conflict with external css
document.getElementById('twinkle-config').removeAttribute('style');
document.getElementById('twinkle-config-titlebar').removeAttribute('style');
const contentdiv = document.getElementById('twinkle-config-content');
contentdiv.textContent = ''; // clear children
// let user know about possible conflict with skin js/common.js file
// (settings in that file will still work, but they will be overwritten by twinkleoptions.js settings)
if (window.TwinkleConfig || window.FriendlyConfig) {
const contentnotice = document.createElement('div');
contentnotice.className = 'plainlinks twinkle-ombox';
contentnotice.innerHTML =
'<div>' +
'<img alt="" src="https://upload.wikimedia.org/wikipedia/commons/3/38/Imbox_content.png" />' +
'</div>' +
'<div>' +
'<p><big><b>ここで設定を変更する前に、</b>個人用JavaScriptから古いTwinkleとFriendlyの設定を削除してください。</big></p>' +
'<p><a href="' + mw.util.getUrl('User:' + mw.config.get('wgUserName') + '/' + mw.config.get('skin') +
'.js', { action: 'edit' }) + '" target="_blank"><b>使用中の外装の個人用JavaScript</b></a>または<a href="' +
mw.util.getUrl('User:' + mw.config.get('wgUserName') + '/common.js', { action: 'edit'}) + '" target="_blank"><b>common.js</b></a>を編集し、<code>TwinkleConfig</code>と<code>FriendlyConfig</code>を参照する設定行を削除してください。</p>' +
'</div>';
contentdiv.appendChild(contentnotice);
}
// start a table of contents
const toctable = document.createElement('div');
toctable.className = 'toc';
toctable.style.marginLeft = '0.4em';
// create TOC title
const toctitle = document.createElement('div');
toctitle.id = 'toctitle';
var toch2 = document.createElement('h2');
toch2.textContent = '目次 ';
toctitle.appendChild(toch2);
// add TOC show/hide link
const toctoggle = document.createElement('span');
toctoggle.className = 'toctoggle';
toctoggle.appendChild(document.createTextNode('['));
const toctogglelink = document.createElement('a');
toctogglelink.className = 'internal';
toctogglelink.setAttribute('href', '#tw-tocshowhide');
toctogglelink.textContent = '非表示';
toctoggle.appendChild(toctogglelink);
toctoggle.appendChild(document.createTextNode(']'));
toctitle.appendChild(toctoggle);
toctable.appendChild(toctitle);
// create item container: this is what we add stuff to
const tocul = document.createElement('ul');
toctogglelink.addEventListener('click', () => {
const $tocul = $(tocul);
$tocul.toggle();
if ($tocul.find(':visible').length) {
toctogglelink.textContent = '非表示';
} else {
toctogglelink.textContent = '表示';
}
}, false);
toctable.appendChild(tocul);
contentdiv.appendChild(toctable);
const contentform = document.createElement('form');
contentform.setAttribute('action', 'javascript:void(0)'); // was #tw-save - changed to void(0) to work around Chrome issue
contentform.addEventListener('submit', Twinkle.config.save, true);
contentdiv.appendChild(contentform);
const container = document.createElement('table');
container.style.width = '100%';
contentform.appendChild(container);
$(Twinkle.config.sections).each((sectionkey, section) => {
if (section.hidden || (section.adminOnly && !Morebits.userIsSysop)) {
return true; // i.e. "continue" in this context
}
// add to TOC
const tocli = document.createElement('li');
tocli.className = 'toclevel-1';
const toca = document.createElement('a');
toca.setAttribute('href', '#' + section.module);
toca.appendChild(document.createTextNode(section.title));
tocli.appendChild(toca);
tocul.appendChild(tocli);
let row = document.createElement('tr');
let cell = document.createElement('td');
cell.setAttribute('colspan', '3');
const heading = document.createElement('h4');
heading.style.borderBottom = '1px solid gray';
heading.style.marginTop = '0.2em';
heading.id = section.module;
heading.appendChild(document.createTextNode(section.title));
cell.appendChild(heading);
row.appendChild(cell);
container.appendChild(row);
let rowcount = 1; // for row banding
// add each of the preferences to the form
$(section.preferences).each((prefkey, pref) => {
if (pref.adminOnly && !Morebits.userIsSysop) {
return true; // i.e. "continue" in this context
}
row = document.createElement('tr');
row.style.marginBottom = '0.2em';
// create odd row banding
if (rowcount++ % 2 === 0) {
row.style.backgroundColor = 'rgba(128, 128, 128, 0.1)';
}
cell = document.createElement('td');
var label, input, gotPref = Twinkle.getPref(pref.name), obsoletePref = Twinkle.getPref(pref.obsolete);
switch (pref.type) {
case 'boolean': // create a checkbox
cell.setAttribute('colspan', '2');
label = document.createElement('label');
input = document.createElement('input');
input.setAttribute('type', 'checkbox');
input.setAttribute('id', pref.name);
input.setAttribute('name', pref.name);
if (gotPref === true || obsoletePref === true) {
input.setAttribute('checked', 'checked');
}
label.appendChild(input);
label.appendChild(document.createTextNode(pref.label));
cell.appendChild(label);
break;
case 'string': // create an input box
case 'integer':
// add label to first column
cell.style.textAlign = 'right';
cell.style.paddingRight = '0.5em';
label = document.createElement('label');
label.setAttribute('for', pref.name);
label.appendChild(document.createTextNode(pref.label + ':'));
cell.appendChild(label);
row.appendChild(cell);
// add input box to second column
cell = document.createElement('td');
cell.style.paddingRight = '1em';
input = document.createElement('input');
input.setAttribute('type', 'text');
input.setAttribute('id', pref.name);
input.setAttribute('name', pref.name);
if (pref.type === 'integer') {
input.setAttribute('size', 6);
input.setAttribute('type', 'number');
input.setAttribute('step', '1'); // integers only
}
if (gotPref) {
input.setAttribute('value', gotPref);
}
if (obsoletePref) {
input.setAttribute('value', obsoletePref);
}
cell.appendChild(input);
break;
case 'enum': // create a combo box
// add label to first column
// note: duplicates the code above, under string/integer
cell.style.textAlign = 'right';
cell.style.paddingRight = '0.5em';
label = document.createElement('label');
label.setAttribute('for', pref.name);
label.appendChild(document.createTextNode(pref.label + ':'));
cell.appendChild(label);
row.appendChild(cell);
// add input box to second column
cell = document.createElement('td');
cell.style.paddingRight = '1em';
input = document.createElement('select');
input.setAttribute('id', pref.name);
input.setAttribute('name', pref.name);
$.each(pref.enumValues, (enumvalue, enumdisplay) => {
const option = document.createElement('option');
option.setAttribute('value', enumvalue);
if ((gotPref === enumvalue || obsoletePref === enumvalue) ||
// Hack to convert old boolean watchlist prefs
// to corresponding enums (added in v2.1)
((typeof gotPref === 'boolean' || typeof obsoletePref === 'boolean') &&
(((gotPref || obsoletePref) && enumvalue === 'yes') ||
(!(gotPref && obsoletePref) && enumvalue === 'no')))) {
option.setAttribute('selected', 'selected');
}
option.appendChild(document.createTextNode(enumdisplay));
input.appendChild(option);
});
cell.appendChild(input);
break;
case 'set': // create a set of check boxes
// add label first of all
cell.setAttribute('colspan', '2');
label = document.createElement('label'); // not really necessary to use a label element here, but we do it for consistency of styling
label.appendChild(document.createTextNode(pref.label + ':'));
cell.appendChild(label);
var checkdiv = document.createElement('div');
checkdiv.style.paddingLeft = '1em';
var worker = function(itemkey, itemvalue) {
const checklabel = document.createElement('label');
checklabel.style.marginRight = '0.7em';
checklabel.style.display = 'inline-block';
const check = document.createElement('input');
check.setAttribute('type', 'checkbox');
check.setAttribute('id', pref.name + '_' + itemkey);
check.setAttribute('name', pref.name + '_' + itemkey);
if ((gotPref && gotPref.includes(itemkey)) || (obsoletePref && obsoletePref.includes(itemkey))) {
check.setAttribute('checked', 'checked');
}
// cater for legacy integer array values for unlinkNamespaces (this can be removed a few years down the track...)
if (pref.name === 'unlinkNamespaces') {
if ((gotPref && gotPref.includes(parseInt(itemkey, 10))) || (obsoletePref && obsoletePref.includes(parseInt(itemkey, 10)))) {
check.setAttribute('checked', 'checked');
}
}
checklabel.appendChild(check);
checklabel.appendChild(document.createTextNode(itemvalue));
checkdiv.appendChild(checklabel);
};
if (pref.setDisplayOrder) {
// add check boxes according to the given display order
$.each(pref.setDisplayOrder, (itemkey, item) => {
worker(item, pref.setValues[item]);
});
} else {
// add check boxes according to the order it gets fed to us (probably strict alphabetical)
$.each(pref.setValues, worker);
}
cell.appendChild(checkdiv);
break;
case 'customList':
// add label to first column
cell.style.textAlign = 'right';
cell.style.paddingRight = '0.5em';
label = document.createElement('label');
label.setAttribute('for', pref.name);
label.appendChild(document.createTextNode(pref.label + ':'));
cell.appendChild(label);
row.appendChild(cell);
// add button to second column
cell = document.createElement('td');
cell.style.paddingRight = '1em';
var button = document.createElement('button');
button.setAttribute('id', pref.name);
button.setAttribute('name', pref.name);
button.setAttribute('type', 'button');
button.addEventListener('click', Twinkle.config.listDialog.display, false);
// use jQuery data on the button to store the current config value
$(button).data({
value: (obsoletePref) || gotPref,
pref: pref
});
button.appendChild(document.createTextNode('アイテムを編集'));
cell.appendChild(button);
break;
default:
alert('twinkleconfig: 設定 ' + pref.name + ' の不明なデータ型');
break;
}
row.appendChild(cell);
// add help tip
cell = document.createElement('td');
cell.className = 'twinkle-config-helptip';
if (pref.helptip) {
// convert mentions of templates in the helptip to clickable links
cell.innerHTML = pref.helptip.replace(/{{(.+?)}}/g,
'{{<a href="' + mw.util.getUrl('Template:') + '$1" target="_blank">$1</a>}}');
}
// add reset link (custom lists don't need this, as their config value isn't displayed on the form)
if (pref.type !== 'customList') {
const resetlink = document.createElement('a');
resetlink.setAttribute('href', '#tw-reset');
resetlink.setAttribute('id', 'twinkle-config-reset-' + pref.name);
resetlink.addEventListener('click', Twinkle.config.resetPrefLink, false);
resetlink.style.cssFloat = 'right';
resetlink.style.margin = '0 0.6em';
resetlink.appendChild(document.createTextNode('初期化'));
cell.appendChild(resetlink);
}
row.appendChild(cell);
container.appendChild(row);
return true;
});
return true;
});
const footerbox = document.createElement('div');
footerbox.setAttribute('id', 'twinkle-config-buttonpane');
const button = document.createElement('button');
button.setAttribute('id', 'twinkle-config-submit');
button.setAttribute('type', 'submit');
button.appendChild(document.createTextNode('変更を保存'));
footerbox.appendChild(button);
const footerspan = document.createElement('span');
footerspan.className = 'plainlinks';
footerspan.style.marginLeft = '2.4em';
footerspan.style.fontSize = '90%';
const footera = document.createElement('a');
footera.setAttribute('href', '#tw-reset-all');
footera.setAttribute('id', 'twinkle-config-resetall');
footera.addEventListener('click', Twinkle.config.resetAllPrefs, false);
footera.appendChild(document.createTextNode('デフォルトに戻す'));
footerspan.appendChild(footera);
footerbox.appendChild(footerspan);
contentform.appendChild(footerbox);
// since all the section headers exist now, we can try going to the requested anchor
if (window.location.hash) {
const loc = window.location.hash;
window.location.hash = '';
window.location.hash = loc;
}
} else if (mw.config.get('wgNamespaceNumber') === mw.config.get('wgNamespaceIds').user &&
mw.config.get('wgTitle').indexOf(mw.config.get('wgUserName')) === 0 &&
mw.config.get('wgPageName').slice(-3) === '.js') {
const box = document.createElement('div');
// Styled in twinkle.css
box.setAttribute('id', 'twinkle-config-headerbox');
let link;
const scriptPageName = mw.config.get('wgPageName').slice(
mw.config.get('wgPageName').lastIndexOf('/') + 1,
mw.config.get('wgPageName').lastIndexOf('.js')
);
if (scriptPageName === 'twinkleoptions') {
// place "why not try the preference panel" notice
box.setAttribute('class', 'config-twopt-box');
link = document.createElement('a');
link.setAttribute('href', mw.util.getUrl(mw.config.get('wgFormattedNamespaces')[mw.config.get('wgNamespaceIds').user] + ':Syunsyunminmin/Twinkle/Preferences'));
link.appendChild(document.createTextNode('Twinkle個人設定パネル'));
if (mw.config.get('wgArticleId') > 0) { // page exists
box.appendChild(document.createTextNode('このページにはTwinkleの設定が含まれています。設定は '));
box.appendChild(link);
box.appendChild(document.createTextNode('を使用するか、このページを編集することで変更できます。'));
} else { // page does not exist
box.appendChild(link);
box.appendChild(document.createTextNode('またはこのページを編集することで、Twinkleを個人の好みに合わせてカスタマイズできます。'));
}
$(box).insertAfter($('#contentSub'));
} else if (['monobook', 'vector', 'vector-2022', 'cologneblue', 'modern', 'timeless', 'minerva', 'common'].includes(scriptPageName)) {
// place "Looking for Twinkle options?" notice
box.setAttribute('class', 'config-userskin-box');
box.appendChild(document.createTextNode('Twinkle個人設定を設定したい場合、'));
link = document.createElement('a');
link.setAttribute('href', mw.util.getUrl(mw.config.get('wgFormattedNamespaces')[mw.config.get('wgNamespaceIds').user] + ':Syunsyunminmin/Twinkle/Preferences'));
link.appendChild(document.createTextNode('Twinkle個人設定パネル'));
box.appendChild(link);
box.appendChild(document.createTextNode('を使用して行えます。'));
$(box).insertAfter($('#contentSub'));
}
}
};
// custom list-related stuff
Twinkle.config.listDialog = {};
Twinkle.config.listDialog.addRow = function twinkleconfigListDialogAddRow($dlgtable, value, label) {
let $contenttr, $valueInput, $labelInput;
$dlgtable.append(
$contenttr = $('<tr>').append(
$('<td>').append(
$('<button>')
.attr('type', 'button')
.css('white-space', 'nowrap')
.on('click', () => {
$contenttr.remove();
})
.text('除去')
),
$('<td>').append(
$valueInput = $('<input>')
.attr('type', 'text')
.addClass('twinkle-config-customlist-value')
.css('width', '97%')
),
$('<td>').append(
$labelInput = $('<input>')
.attr('type', 'text')
.addClass('twinkle-config-customlist-label')
.css('width', '98%')
)
)
);
if (value) {
$valueInput.val(value);
}
if (label) {
$labelInput.val(label);
}
};
Twinkle.config.listDialog.display = function twinkleconfigListDialogDisplay(e) {
const $prefbutton = $(e.target);
const curvalue = $prefbutton.data('value');
const curpref = $prefbutton.data('pref');
const dialog = new Morebits.SimpleWindow(720, 400);
dialog.setTitle(curpref.label);
dialog.setScriptName('Twinkle個人設定');
let $dlgtbody;
dialog.setContent(
$('<div>').append(
$('<table>')
.addClass('wikitable')
.css({
margin: '1.4em 1em',
width: 'auto'
})
.append(
$dlgtbody = $('<tbody>').append(
// header row
$('<tr>').append(
$('<th>') // top-left cell
.css('width', '5%'),
$('<th>') // value column header
.css('width', '35%')
.text(curpref.customListValueTitle ? curpref.customListValueTitle : '値'),
$('<th>') // label column header
.css('width', '60%')
.text(curpref.customListLabelTitle ? curpref.customListLabelTitle : '表示名')
)
),
$('<tfoot>').append(
$('<tr>').append(
$('<td>')
.attr('colspan', '3')
.append(
$('<button>')
.text('追加')
.css('min-width', '8em')
.attr('type', 'button')
.on('click', () => {
Twinkle.config.listDialog.addRow($dlgtbody);
})
)
)
)
),
$('<button>')
.text('変更を保存')
.attr('type', 'submit') // so Morebits.SimpleWindow puts the button in the button pane
.on('click', () => {
Twinkle.config.listDialog.save($prefbutton, $dlgtbody);
dialog.close();
}),
$('<button>')
.text('初期化')
.attr('type', 'submit')
.on('click', () => {
Twinkle.config.listDialog.reset($prefbutton, $dlgtbody);
}),
$('<button>')
.text('キャンセル')
.attr('type', 'submit')
.on('click', () => {
dialog.close();
})
)[0]
);
// content rows
let gotRow = false;
$.each(curvalue, (k, v) => {
gotRow = true;
Twinkle.config.listDialog.addRow($dlgtbody, v.value, v.label);
});
// if there are no values present, add a blank row to start the user off
if (!gotRow) {
Twinkle.config.listDialog.addRow($dlgtbody);
}
dialog.display();
};
// Resets the data value, re-populates based on the new (default) value, then saves the
// old data value again (less surprising behaviour)
Twinkle.config.listDialog.reset = function twinkleconfigListDialogReset($button, $tbody) {
// reset value on button
const curpref = $button.data('pref');
const oldvalue = $button.data('value');
Twinkle.config.resetPref(curpref);
// reset form
$tbody.find('tr').slice(1).remove(); // all rows except the first (header) row
// add the new values
const curvalue = $button.data('value');
$.each(curvalue, (k, v) => {
Twinkle.config.listDialog.addRow($tbody, v.value, v.label);
});
// save the old value
$button.data('value', oldvalue);
};
Twinkle.config.listDialog.save = function twinkleconfigListDialogSave($button, $tbody) {
const result = [];
let current = {};
$tbody.find('input[type="text"]').each((inputkey, input) => {
if ($(input).hasClass('twinkle-config-customlist-value')) {
current = { value: input.value };
} else {
current.label = input.value;
// exclude totally empty rows
if (current.value || current.label) {
result.push(current);
}
}
});
$button.data('value', result);
};
// reset/restore defaults
Twinkle.config.resetPrefLink = function twinkleconfigResetPrefLink(e) {
const wantedpref = e.target.id.slice(21); // "twinkle-config-reset-" prefix is stripped
// search tactics
$(Twinkle.config.sections).each((sectionkey, section) => {
if (section.hidden || (section.adminOnly && !Morebits.userIsSysop)) {
return true; // continue: skip impossibilities
}
let foundit = false;
$(section.preferences).each((prefkey, pref) => {
if (pref.name !== wantedpref) {
return true; // continue
}
Twinkle.config.resetPref(pref);
foundit = true;
return false; // break
});
if (foundit) {
return false; // break
}
});
return false; // stop link from scrolling page
};
Twinkle.config.resetPref = function twinkleconfigResetPref(pref) {
switch (pref.type) {
case 'boolean':
document.getElementById(pref.name).checked = Twinkle.defaultConfig[pref.name];
break;
case 'string':
case 'integer':
case 'enum':
document.getElementById(pref.name).value = Twinkle.defaultConfig[pref.name];
break;
case 'set':
$.each(pref.setValues, (itemkey) => {
if (document.getElementById(pref.name + '_' + itemkey)) {
document.getElementById(pref.name + '_' + itemkey).checked = Twinkle.defaultConfig[pref.name].includes(itemkey);
}
});
break;
case 'customList':
$(document.getElementById(pref.name)).data('value', Twinkle.defaultConfig[pref.name]);
break;
default:
alert('twinkleconfig: 設定 ' + pref.name + ' の不明なデータ型');
break;
}
};
Twinkle.config.resetAllPrefs = function twinkleconfigResetAllPrefs() {
// no confirmation message - the user can just refresh/close the page to abort
$(Twinkle.config.sections).each((sectionkey, section) => {
if (section.hidden || (section.adminOnly && !Morebits.userIsSysop)) {
return true; // continue: skip impossibilities
}
$(section.preferences).each((prefkey, pref) => {
if (!pref.adminOnly || Morebits.userIsSysop) {
Twinkle.config.resetPref(pref);
}
});
return true;
});
return false; // stop link from scrolling page
};
Twinkle.config.save = function twinkleconfigSave(e) {
Morebits.Status.init(document.getElementById('twinkle-config-content'));
var userjs = mw.config.get('wgFormattedNamespaces')[mw.config.get('wgNamespaceIds').user] + ':' + mw.config.get('wgUserName') + '/twinkleoptions.js';
var wikipedia_page = new Morebits.wiki.Page(userjs, userjs + 'に設定を保存');
wikipedia_page.setCallbackParameters(e.target);
wikipedia_page.load(Twinkle.config.writePrefs);
return false;
};
Twinkle.config.writePrefs = function twinkleconfigWritePrefs(pageobj) {
const form = pageobj.getCallbackParameters();
// this is the object which gets serialized into JSON; only
// preferences that this script knows about are kept
const newConfig = {optionsVersion: 2.1};
// a comparison function is needed later on
// it is just enough for our purposes (i.e. comparing strings, numbers, booleans,
// arrays of strings, and arrays of { value, label })
// and it is not very robust: e.g. compare([2], ["2"]) === true, and
// compare({}, {}) === false, but it's good enough for our purposes here
const compare = function(a, b) {
if (Array.isArray(a)) {
if (a.length !== b.length) {
return false;
}
const asort = a.sort(), bsort = b.sort();
for (let i = 0; asort[i]; ++i) {
// comparison of the two properties of custom lists
if ((typeof asort[i] === 'object') && (asort[i].label !== bsort[i].label ||
asort[i].value !== bsort[i].value)) {
return false;
} else if (asort[i].toString() !== bsort[i].toString()) {
return false;
}
}
return true;
}
return a === b;
};
$(Twinkle.config.sections).each((sectionkey, section) => {
if (section.adminOnly && !Morebits.userIsSysop) {
return; // i.e. "continue" in this context
}
// reach each of the preferences from the form
$(section.preferences).each((prefkey, pref) => {
let userValue; // = undefined
// only read form values for those prefs that have them
if (!pref.adminOnly || Morebits.userIsSysop) {
if (!section.hidden) {
switch (pref.type) {
case 'boolean': // read from the checkbox
userValue = form[pref.name].checked;
break;
case 'string': // read from the input box or combo box
case 'enum':
userValue = form[pref.name].value;
break;
case 'integer': // read from the input box
userValue = parseInt(form[pref.name].value, 10);
if (isNaN(userValue)) {
Morebits.Status.warn('保存', pref.name + 'に対して指定した値 (' + pref.value + ') は無効です。保存は続行しますが、無効なデータ値は飛ばされます。');
userValue = null;
}
break;
case 'set': // read from the set of check boxes
userValue = [];
if (pref.setDisplayOrder) {
// read only those keys specified in the display order
$.each(pref.setDisplayOrder, (itemkey, item) => {
if (form[pref.name + '_' + item].checked) {
userValue.push(item);
}
});
} else {
// read all the keys in the list of values
$.each(pref.setValues, (itemkey) => {
if (form[pref.name + '_' + itemkey].checked) {
userValue.push(itemkey);
}
});
}
break;
case 'customList': // read from the jQuery data stored on the button object
userValue = $(form[pref.name]).data('value');
break;
default:
alert('twinkleconfig: 設定 ' + pref.name + ' の不明なデータ型');
break;
}
} else if (Twinkle.prefs) {
// Retain the hidden preferences that may have customised by the user from twinkleoptions.js
// undefined if not set
userValue = Twinkle.prefs[pref.name];
}
}
// only save those preferences that are *different* from the default
if (userValue !== undefined && !compare(userValue, Twinkle.defaultConfig[pref.name])) {
newConfig[pref.name] = userValue;
}
});
});
var text =
'// twinkleoptions.js: Twinkleの個人設定ファイル\n' +
'//\n' +
'// 注意: Twinkleの設定を変更する最も簡単な方法は、\n' +
'// Twinkle設定パネルを使用することです。設定パネルは[[' + Morebits.pageNameNorm + ']]にあります。\n' +
'//\n' +
'// このファイルは自動的に生成されます。\n' +
'// (有効なJavaScriptの方法で構成パラメーターを変更する以外に)行った変更は、\n' +
'// 次にTwinkle設定パネルで「保存」をクリックしたときに上書きされます。\n' +
'// このファイルを直接変更する場合は、必ず正しいJavaScriptを使用してください。\n' +
// eslint-disable-next-line no-useless-concat
'// <no' + 'wiki>\n' +
'\n' +
'window.Twinkle.prefs = ';
text += JSON.stringify(newConfig, null, 2);
text +=
';\n' +
'\n' +
// eslint-disable-next-line no-useless-concat
'// </no' + 'wiki>\n' +
'// twinkleoptions.js の終わり\n';
pageobj.setPageText(text);
pageobj.setEditSummary('Twinkle個人設定を保存: [[:' + Morebits.pageNameNorm + ']]からの自動編集');
pageobj.setCreateOption('recreate');
pageobj.save(Twinkle.config.saveSuccess);
};
Twinkle.config.saveSuccess = function twinkleconfigSaveSuccess(pageobj) {
pageobj.getStatusElement().info('成功');
const noticebox = document.createElement('div');
noticebox.className = 'cdx-message cdx-message--success';
noticebox.style.fontSize = '100%';
noticebox.style.marginTop = '2em';
noticebox.innerHTML = '<p><b>Twinkle個人設定は保存されました。</b></p><p>変更を確認するには、<b>ブラウザのキャッシュを完全にクリアする</b>必要があります。 (手順については <a href="' + mw.util.getUrl('WP:BYPASS') + '" title="WP:BYPASS">WP:BYPASS</a> 参照</p>';
Morebits.Status.root.appendChild(noticebox);
var noticeclear = document.createElement('br');
noticeclear.style.clear = 'both';
Morebits.Status.root.appendChild(noticeclear);
};
Twinkle.addInitCallback(Twinkle.config.init);
}());
// </nowiki>
caq0lw0u4fd5nm7ithdmrpkgfxgkusu
769527
769460
2026-10-22T00:00:13Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 1406828: rm cmt
769527
javascript
text/javascript
// <nowiki>
(function() {
/*
****************************************
*** twinkleconfig.js: Preferences module
****************************************
* Mode of invocation: Adds configuration form to User:Syunsyunminmin/Twinkle/Preferences,
and adds an ad box to the top of user subpages belonging to the
currently logged-in user which end in '.js'
* Active on: What I just said. Yeah.
*/
Twinkle.config = {};
Twinkle.config.watchlistEnums = {
yes: 'ウォッチリストに追加 (無期限)',
no: 'ウォッチリストに追加しない',
default: 'サイトの設定に従う',
'1 week': '1週間ウォッチ',
'1 month': '1ヶ月ウォッチ',
'3 months': '3ヶ月ウォッチ',
'6 months': '6ヶ月ウォッチ'
};
Twinkle.config.commonSets = {
csdCriteria: {
db: 'その他 ({{即時削除|その他}})',
g1: 'G1', g2: 'G2', g3: 'G3', g4: 'G4', g5: 'G5', g6: 'G6', g8: 'G8', g9: 'G9', g10: 'G10',
a1: 'A1',
u1: 'U1', u2: 'U2', u3: 'U3',
'f1-2': 'F1-2', 'f1-3': 'F1-3', 'f1-4': 'F1-4', 'f1-5': 'F1-5', 'f1-6': 'F1-6',
f3: 'F3', f5: 'F5', f6: 'F6', f7: 'F7', f8: 'F8', f9: 'F9',
c1: 'C1', c3: 'C3', c6: 'C6',
'r1-1': 'R1-1', 'r1-2': 'R1-2', 'r1-3': 'R1-3', 'r1-4': 'R1-4',
'r2-1': 'R2-1', 'r2-2': 'R2-2', 'r2-3': 'R2-3', 'r2-5': 'R2-5', 'r2-6': 'R2-6',
r4: 'R4'
},
csdCriteriaDisplayOrder: [
'db',
'g1', 'g2', 'g3', 'g4', 'g5', 'g6', 'g8', 'g9', 'g10',
'a1',
'u1', 'u2', 'u3',
'f1-2', 'f1-3', 'f1-4', 'f1-5', 'f1-6', 'f3', 'f5', 'f6', 'f7', 'f8', 'f9',
'c1', 'c3', 'c6',
'r1-1', 'r1-2', 'r1-3', 'r1-4', 'r2-1', 'r2-2', 'r2-3', 'r2-5', 'r2-6', 'r4'
],
csdAndImageDeletionCriteria: {
db: 'その他 ({{即時削除|その他}})',
g1: 'G1', g2: 'G2', g3: 'G3', g4: 'G4', g5: 'G5', g6: 'G6', g8: 'G8', g9: 'G9', g10: 'G10',
a1: 'A1',
u1: 'U1', u2: 'U2', u3: 'U3',
'f1-2': 'F1-2', 'f1-3': 'F1-3', 'f1-4': 'F1-4', 'f1-5': 'F1-5', 'f1-6': 'F1-6',
f3: 'F3', f5: 'F5', f6: 'F6', f7: 'F7', f8: 'F8', f9: 'F9',
c1: 'C1', c3: 'C3', c6: 'C6',
'r1-1': 'R1-1', 'r1-2': 'R1-2', 'r1-3': 'R1-3', 'r1-4': 'R1-4',
'r2-1': 'R2-1', 'r2-2': 'R2-2', 'r2-3': 'R2-3', 'r2-5': 'R2-5', 'r2-6': 'R2-6',
r4: 'R4'
},
csdAndImageDeletionCriteriaDisplayOrder: [
'db',
'g1', 'g2', 'g3', 'g4', 'g5', 'g6', 'g8', 'g9', 'g10',
'a1',
'u1', 'u2', 'u3',
'f1-2', 'f1-3', 'f1-4', 'f1-5', 'f1-6', 'f3', 'f5', 'f6', 'f7', 'f8', 'f9',
'c1', 'c3', 'c6',
'r1-1', 'r1-2', 'r1-3', 'r1-4', 'r2-1', 'r2-2', 'r2-3', 'r2-5', 'r2-6', 'r4'
],
namespacesNoSpecial: {
0: '標準',
1: 'ノート (標準)',
2: '利用者',
3: '利用者‐会話',
4: 'Wikipedia',
5: 'Wikipedia‐ノート',
6: 'ファイル',
7: 'ファイル‐ノート',
8: 'MediaWiki',
9: 'MediaWiki‐ノート',
10: 'Template',
11: 'Template‐ノート',
12: 'Help',
13: 'Help‐ノート',
14: 'Category',
15: 'Category‐ノート',
100: 'Portal',
101: 'Portal‐ノート',
710: 'TimedText',
711: 'TimedText‐ノート',
828: 'Module',
829: 'Module‐ノート'
}
};
Twinkle.config.commonSets.csdCriteriaDisplayOrder = Object.keys(Twinkle.config.commonSets.csdCriteria);
Twinkle.config.commonSets.csdAndImageDeletionCriteriaDisplayOrder = Object.keys(Twinkle.config.commonSets.csdAndImageDeletionCriteria);
/**
* Section entry format:
*
* {
* title: <human-readable section title>,
* module: <name of the associated module, used to link to sections>,
* adminOnly: <true for admin-only sections>,
* hidden: <true for advanced preferences that rarely need to be changed - they can still be modified by manually editing twinkleoptions.js>,
* preferences: [
* {
* name: <TwinkleConfig property name>,
* label: <human-readable short description - used as a form label>,
* helptip: <(optional) human-readable text (using valid HTML) that complements the description, like limits, warnings, etc.>
* adminOnly: <true for admin-only preferences>,
* type: <string|boolean|integer|enum|set|customList> (customList stores an array of JSON objects { value, label }),
* enumValues: <for type = "enum": a JSON object where the keys are the internal names and the values are human-readable strings>,
* setValues: <for type = "set": a JSON object where the keys are the internal names and the values are human-readable strings>,
* setDisplayOrder: <(optional) for type = "set": an array containing the keys of setValues (as strings) in the order that they are displayed>,
* customListValueTitle: <for type = "customList": the heading for the left "value" column in the custom list editor>,
* customListLabelTitle: <for type = "customList": the heading for the right "label" column in the custom list editor>
* },
* . . .
* ]
* },
* . . .
*
*/
Twinkle.config.sections = [
{
title: '全般',
module: 'general',
preferences: [
// TwinkleConfig.userTalkPageMode may take arguments:
// 'window': open a new window, remember the opened window
// 'tab': opens in a new tab, if possible.
// 'blank': force open in a new window, even if such a window exists
{
name: 'userTalkPageMode',
label: '利用者会話ページを開く時は次のページで開く',
type: 'enum',
enumValues: { window: 'ウィンドウ内で他の利用者会話ページを置き換える', tab: '新しいタブ', blank: '完全に新しいウィンドウ' }
},
// TwinkleConfig.dialogLargeFont (boolean)
{
name: 'dialogLargeFont',
label: 'Twinkleダイアログで大きな文字を使用する',
type: 'boolean'
},
// Twinkle.config.disabledModules (array)
{
name: 'disabledModules',
label: '選択したTwinkleモジュールをオフにする',
helptip: 'チェックを入れた機能は無効になります。再び有効にするにはチェックを外してください。<br />既定でチェックが入っているものは準備中の機能です。無効にしていても、関連する設定項目は表示されます。',
type: 'set',
setValues: { arv: '荒らしの報告 (ARV)', warn: '利用者への警告 (Warn)', talkback: '返信通知 (Talkback)', speedy: '即時削除 (CSD)', xfd: '削除依頼 (XfD)', image: '画像 (DI)', protect: '保護 (RPP)', tag: 'メンテナンス用テンプレートの付与 (Tag)', diff: '差分', unlink: 'リンク解除', rollback: '差し戻しと巻き戻し' }
},
// Twinkle.config.disabledSysopModules (array)
{
name: 'disabledSysopModules',
label: '選択した管理者専用モジュールをオフにする',
helptip: 'ここで選択したものは使用できないので、注意してください。再び有効にするにはチェックを外してください。',
adminOnly: true,
type: 'set',
setValues: { block: '利用者のブロック', batchdelete: '一括削除 (D-batch)', batchprotect: '一括保護 (P-batch)', batchundelete: '一括復元 (Und-batch)' }
}
]
},
{
title: '荒らしの報告 (ARV)',
module: 'arv',
preferences: [
{
name: 'spiWatchReport',
label: '多重アカウントの調査依頼ページをウォッチリストに追加する',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
}
]
},
{
title: '利用者のブロック',
module: 'block',
adminOnly: true,
preferences: [
// TwinkleConfig.defaultToBlock64 (boolean)
// Whether to default to just blocking the /64 on or off
{
name: 'defaultToBlock64',
label: 'IPv6アドレスでは、既定で/64の範囲をブロックする',
type: 'boolean'
},
// TwinkleConfig.defaultToPartialBlocks (boolean)
// Whether to default partial blocks on or off
{
name: 'defaultToPartialBlocks',
label: 'ブロックメニューを開いた際、既定で部分ブロックを選択する',
helptip: '利用者がすでにブロックされている場合は、現在のブロックの種類が優先されます。',
type: 'boolean'
},
// TwinkleConfig.blankTalkpageOnIndefBlock (boolean)
// if true, blank the talk page when issuing an indef block notice (per [[WP:UWUL#Indefinitely blocked users]])
{
name: 'blankTalkpageOnIndefBlock',
label: '無期限ブロックの通知時に利用者会話ページを白紙化する',
helptip: '詳しくは<a href="' + mw.util.getUrl('Wikipedia:WikiProject_User_warnings/Usage_and_layout#Indefinitely_blocked_users') + '">WP:UWUL</a>を参照してください。',
type: 'boolean'
}
]
},
{
title: '画像削除 (DI)',
module: 'image',
preferences: [
// TwinkleConfig.notifyUserOnDeli (boolean)
// If the user should be notified after placing a file deletion tag
{
name: 'notifyUserOnDeli',
label: '既定で"アップロード者に通知"にチェックを入れる',
type: 'boolean'
},
// TwinkleConfig.deliWatchPage (string)
// The watchlist setting of the page tagged for deletion.
{
name: 'deliWatchPage',
label: 'タグ付け時にファイルページをウォッチリストに追加',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
// TwinkleConfig.deliWatchUser (string)
// The watchlist setting of the user talk page if a notification is placed.
{
name: 'deliWatchUser',
label: '通知時に、アップロード者の会話ページをウォッチリストに追加する',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
}
]
},
{
title: 'ページの保護 ' + (Morebits.userIsSysop ? '(PP)' : '(RPP)'),
module: 'protect',
preferences: [
{
name: 'watchRequestedPages',
label: '保護依頼時にページをウォッチリストに追加する',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
{
name: 'watchPPTaggedPages',
label: '保護テンプレートでページをタグ付けする際、ウォッチリストにページを追加する',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
{
name: 'watchProtectedPages',
label: '保護する際にページをウォッチリストに追加する',
helptip: 'ページ保護後にタグ付けも行う場合は、そちらが優先されます。',
adminOnly: true,
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
}
]
},
{
title: '差し戻しと巻き戻し', // twinklerollback module
module: 'rollback',
preferences: [
// TwinkleConfig.autoMenuAfterRollback (bool)
// Option to automatically open the warning menu if the user talk page is opened post-reversion
{
name: 'autoMenuAfterRollback',
label: 'Twinkle差し戻し後、利用者会話ページでTwinkle警告メニューを自動的に開く',
helptip: '以下の該当するボックスにチェックが入っている場合のみ動作します。',
type: 'boolean'
},
// TwinkleConfig.openTalkPage (array)
// What types of actions that should result in opening of talk page
{
name: 'openTalkPage',
label: '以下の種類の差し戻し後に、会話ページを開く。',
type: 'set',
setValues: { norm: '通常の差し戻し', vand: '荒らしの差し戻し' }
},
// TwinkleConfig.openTalkPageOnAutoRevert (bool)
// Defines if talk page should be opened when calling revert from contribs or recent changes pages. If set to true, openTalkPage defines then if talk page will be opened.
{
name: 'openTalkPageOnAutoRevert',
label: '投稿記録や最近の更新から差し戻しを実行した際に、会話ページを開く',
helptip: 'これが有効になっている場合、前の設定で必要なオプションが有効になっていなければ機能しません。',
type: 'boolean'
},
// TwinkleConfig.rollbackInPlace (bool)
//
{
name: 'rollbackInPlace',
label: '投稿記録や最近の更新から差し戻しする際、ページを更新しない。',
helptip: 'これが有効になっている場合、Twinkleは差し戻し後に投稿記録や最近の更新フィードを再読み込みしないため、一度に複数の編集を差し戻すことができます。',
type: 'boolean'
},
// TwinkleConfig.markRevertedPagesAsMinor (array)
// What types of actions that should result in marking edit as minor
{
name: 'markRevertedPagesAsMinor',
label: '以下の種類の差し戻しは細部の編集として行います',
type: 'set',
setValues: { norm: '通常の差し戻し', vand: '荒らしの差し戻し', torev: '"版の復元"' }
},
// TwinkleConfig.watchRevertedPages (array)
// What types of actions that should result in forced addition to watchlist
{
name: 'watchRevertedPages',
label: '以下の種類の差し戻しの際、ページをウォッチリストに追加する',
type: 'set',
setValues: { norm: '通常の差し戻し', vand: '荒らしの差し戻し', torev: '"版の復元"' }
},
// TwinkleConfig.watchRevertedExpiry
// If any of the above items are selected, whether to expire the watch
{
name: 'watchRevertedExpiry',
label: 'ページを差し戻した際、ウォッチする期間は',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
// TwinkleConfig.offerReasonOnNormalRevert (boolean)
// If to offer a prompt for extra summary reason for normal reverts, default to true
{
name: 'offerReasonOnNormalRevert',
label: '通常の差し戻しに理由の入力を求めるプロンプト',
helptip: '"通常の"差し戻しは [差し戻し] リンクから実行される差し戻しです。',
type: 'boolean'
},
{
name: 'confirmOnRollback',
label: '差し戻す前に確認を求める (すべての機器)',
helptip: 'ペンやタッチデバイスを使う人、慢性的に優柔不断な人向け。',
type: 'boolean'
},
{
name: 'confirmOnMobileRollback',
label: '差し戻す前に確認を求める (携帯機器のみ)',
helptip: '携帯機器での誤動作を防ぐ。',
type: 'boolean'
},
// TwinkleConfig.showRollbackLinks (array)
// Where Twinkle should show rollback links:
// diff, others, mine, contribs, history, recent
// Note from TTO: |contribs| seems to be equal to |others| + |mine|, i.e. redundant, so I left it out heres
{
name: 'showRollbackLinks',
label: 'これらのページで差し戻しリンクを表示',
type: 'set',
setValues: { diff: '差分ページ', others: '他の利用者の投稿記録ページ', mine: '自分の投稿記録ページ', recent: '最近の更新や関連ページの更新の特別ページ', history: '履歴ページ' }
},
{
name: 'SummaryOnVandalRevert',
label: '荒らしの差し戻しの際の要約欄をカスタムする',
helptip: 'このボックスに要約を入力します。',
type: 'string',
obsolete: 'vandusersummary'
}
]
},
{
title: '即時削除 (CSD)',
module: 'speedy',
preferences: [
{
name: 'speedySelectionStyle',
label: 'ページをタグ付け/削除するタイミング',
type: 'enum',
enumValues: { buttonClick: '送信ボタンをクリックしたとき', radioClick: 'オプションをクリックするとすぐ' }
},
// TwinkleConfig.watchSpeedyPages (array)
// Whether to add speedy tagged or deleted pages to watchlist
{
name: 'watchSpeedyPages',
label: 'これらの基準を使用する場合は、ウォッチリストにページを追加する',
type: 'set',
setValues: Twinkle.config.commonSets.csdCriteria,
setDisplayOrder: Twinkle.config.commonSets.csdCriteriaDisplayOrder
},
// TwinkleConfig.watchSpeedyExpiry
// If any of the above items are selected, whether to expire the watch
{
name: 'watchSpeedyExpiry',
label: 'ページをタグ付けする場合、それをウォッチする期間',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
// TwinkleConfig.markSpeedyPagesAsPatrolled (boolean)
// If, when applying speedy template to page, to mark the page as triaged/patrolled (if the page was reached from NewPages)
{
name: 'markSpeedyPagesAsPatrolled',
label: 'タグ付けの際、ページを巡回済みとしてマークする。(可能な場合)',
helptip: 'これは最善の慣行に反するおそれがあるので、おそらくチェックすべきではないです。',
type: 'boolean'
},
// TwinkleConfig.promptForSpeedyDeletionSummary (array of strings)
{
name: 'promptForSpeedyDeletionSummary',
label: 'これらの基準で削除する場合、削除要約の編集を行う',
adminOnly: true,
type: 'set',
setValues: Twinkle.config.commonSets.csdAndImageDeletionCriteria,
setDisplayOrder: Twinkle.config.commonSets.csdAndImageDeletionCriteriaDisplayOrder
},
// TwinkleConfig.deleteTalkPageOnDelete (boolean)
// If talk page if exists should also be deleted (CSD G8) when spedying a page (admin only)
{
name: 'deleteTalkPageOnDelete',
label: '既定で"トークページも削除する"にチェックを入れる',
adminOnly: true,
type: 'boolean'
},
{
name: 'deleteRedirectsOnDelete',
label: '既定で「リダイレクトも削除」にチェックを入れる',
adminOnly: true,
type: 'boolean'
},
// TwinkleConfig.deleteSysopDefaultToDelete (boolean)
// Make the CSD screen default to "delete" instead of "tag" (admin only)
{
name: 'deleteSysopDefaultToDelete',
label: '既定でタグ付けではなく即時削除を行う',
helptip: 'CSDタグが既に存在する場合、Twinkleは常に「削除」モードが既定となります。',
adminOnly: true,
type: 'boolean'
},
// TwinkleConfig.speedyWindowWidth (integer)
// Defines the width of the Twinkle SD window in pixels
{
name: 'speedyWindowWidth',
label: '即時削除ウィンドウの幅(ピクセル)',
type: 'integer'
},
// TwinkleConfig.speedyWindowWidth (integer)
// Defines the width of the Twinkle SD window in pixels
{
name: 'speedyWindowHeight',
label: '即時削除ウィンドウの高さ(ピクセル)',
helptip: '大きなモニターをお持ちの場合は、これを増やしてください。',
type: 'integer'
},
{
name: 'logSpeedyNominations',
label: 'すべてのCSDへの指定記録を利用者ページに保持します',
helptip: '管理者以外は削除された投稿にアクセスできないため、利用者ページのログは、Twinkleを使用してCSDに指定したすべてのページを追跡するための優れた方法を提供します。 DIを使用してタグ付けされたファイルもこのログに追加されます。',
type: 'boolean'
},
{
name: 'speedyLogPageName',
label: 'この利用者サブページでCSDユーザースペースログを保持します',
helptip: 'このボックスにサブページ名を入力します。 CSDログは利用者:' + mw.config.get('wgUserName') + '/"サブページ名"にあります。 CSD利用者ページへの記録をオンにした場合にのみ機能します。',
type: 'string'
},
{
name: 'noLogOnSpeedyNomination',
label: 'これらの基準でタグ付けする場合は、利用者サブページの記録項目を作成しない',
type: 'set',
setValues: Twinkle.config.commonSets.csdAndImageDeletionCriteria,
setDisplayOrder: Twinkle.config.commonSets.csdAndImageDeletionCriteriaDisplayOrder
}
]
},
{
title: 'メンテナンス用テンプレートの付与 (Tag)',
module: 'tag',
preferences: [
{
name: 'watchTaggedVenues',
label: '次の種類のページにテンプレートを付与した際、ウォッチリストに追加する',
type: 'set',
setValues: { articles: '記事', drafts: '下書き', redirects: 'リダイレクト', files: 'ファイル' }
},
{
name: 'watchTaggedPages',
label: 'テンプレートを付与したページのウォッチ期間',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
{
name: 'watchMergeDiscussions',
label: '統合提案を開始した際、会話ページをウォッチリストに追加する',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
{
name: 'markTaggedPagesAsMinor',
label: 'テンプレートの付与を細部の編集として扱う',
type: 'boolean'
},
{
name: 'markTaggedPagesAsPatrolled',
label: '既定で「ページを巡回済み・確認済みにする」にチェックを入れる',
type: 'boolean'
},
{
name: 'tagArticleSortOrder',
label: '記事用テンプレートの既定の表示順',
type: 'enum',
enumValues: { cat: '分類別', alpha: 'アルファベット順' }
},
{
name: 'customTagList',
label: '追加で表示する記事・下書きのメンテナンス用テンプレート',
helptip: 'テンプレート一覧の末尾に選択肢を追加します。Twinkleにまだ登録されていないメンテナンス用テンプレートなどを指定できます。',
type: 'customList',
customListValueTitle: 'テンプレート名(波括弧なし)',
customListLabelTitle: 'テンプレート付与画面に表示する名前'
},
{
name: 'customFileTagList',
label: '追加で表示するファイルのメンテナンス用テンプレート',
helptip: 'ファイルに付与できるテンプレートを追加します。',
type: 'customList',
customListValueTitle: 'テンプレート名(波括弧なし)',
customListLabelTitle: 'テンプレート付与画面に表示する名前'
},
{
name: 'customRedirectTagList',
label: '追加で表示するリダイレクト分類用テンプレート',
helptip: 'リダイレクトに付与できるテンプレートを追加します。',
type: 'customList',
customListValueTitle: 'テンプレート名(波括弧なし)',
customListLabelTitle: 'テンプレート付与画面に表示する名前'
}
]
},
{
title: '返信通知 (Talkback)',
module: 'talkback',
preferences: [
{
name: 'markTalkbackAsMinor',
label: '返信通知を細部の編集として扱う',
type: 'boolean'
},
{
name: 'insertTalkbackSignature',
label: '返信通知に署名を挿入する',
type: 'boolean'
},
{
name: 'talkbackHeading',
label: '返信通知と会話ページへの案内に使用する節見出し',
tooltip: 'ウィキテキストの見出しに使う等号(==)は入力しないでください。',
type: 'string'
},
{
name: 'mailHeading',
label: 'メール送信通知に使用する節見出し',
tooltip: 'ウィキテキストの見出しに使う等号(==)は入力しないでください。',
type: 'string'
}
]
},
{
title: 'リンク解除 (Unlink)',
module: 'unlink',
preferences: [
// TwinkleConfig.unlinkNamespaces (array)
// In what namespaces unlink should happen, default in 0 (article), 10 (template), 100 (portal), and 118 (draft)
{
name: 'unlinkNamespaces',
label: '次の名前空間のページをリンク解除の対象にする',
helptip: '会話ページの過去ログ内のリンクまで解除されるおそれがあるため、会話用の名前空間は選択しないでください。',
type: 'set',
setValues: Twinkle.config.commonSets.namespacesNoSpecial
}
]
},
{
title: '利用者への警告 (Warn)',
module: 'warn',
preferences: [
// TwinkleConfig.defaultWarningGroup (int)
// Which level warning should be the default selected group, default is 1
{
name: 'defaultWarningGroup',
label: '既定の警告レベル',
type: 'enum',
enumValues: {
1: 'レベル1',
2: 'レベル2',
3: 'レベル3',
4: 'レベル4',
5: '即時の最終警告(レベル4im)',
6: '個別の問題に関する通知',
7: '個別の問題に関する警告',
// 8 was used for block templates before #260
9: '独自の警告',
10: 'すべての警告テンプレート',
11: 'レベルを自動選択(1~4)'
}
},
// TwinkleConfig.combinedSingletMenus (boolean)
// if true, show one menu with both single-issue notices and warnings instead of two separately
{
name: 'combinedSingletMenus',
label: '個別の問題に関する通知と警告のメニューをまとめる',
helptip: '有効にすると、既定の警告レベルで個別の問題に関する通知または警告を選んでいる場合に、統合したメニューを既定で表示します。',
type: 'boolean'
},
// TwinkleConfig.watchWarnings (string)
// Watchlist setting for the page which has been dispatched an warning or notice
{
name: 'watchWarnings',
label: '通知した利用者の会話ページをウォッチリストに追加する',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
// TwinkleConfig.oldSelect (boolean)
// if true, use the native select menu rather the select2-based one
{
name: 'oldSelect',
label: '検索機能のない従来の選択メニューを使用する',
type: 'boolean'
},
{
name: 'customWarningList',
label: '追加で表示する警告テンプレート',
helptip: 'テンプレートや利用者サブページを追加できます。追加した項目は警告画面の「独自の警告」に表示されます。',
type: 'customList',
customListValueTitle: 'テンプレート名(波括弧なし)',
customListLabelTitle: '警告一覧に表示する名前(編集要約にも使用)'
}
]
},
{
title: '削除依頼 (XfD)',
module: 'xfd',
preferences: [
{
name: 'logXfdNominations',
label: '削除依頼(XfD)を提出したページを利用者サブページに記録する',
helptip: 'Twinkleで削除依頼を提出したページを、利用者サブページの記録から確認できます。',
type: 'boolean'
},
{
name: 'xfdLogPageName',
label: '削除依頼の記録を保存する利用者サブページ名',
helptip: 'サブページ名を入力してください。記録先は「利用者:<i>利用者名</i>/<i>サブページ名</i>」です。削除依頼の記録を有効にしている場合にのみ機能します。',
type: 'string'
},
{
name: 'noLogOnXfdNomination',
label: '次の手続きに提出する場合は利用者サブページに記録しない',
type: 'set',
setValues: { afd: '記事の削除依頼 (AfD)', tfd: 'テンプレートの削除・統合依頼 (TfD)', ffd: 'ファイルの削除依頼 (FfD)', cfd: 'カテゴリの削除・変更依頼 (CfD)', cfds: 'カテゴリの即時変更依頼 (CfD/S)', mfd: 'その他のページの削除依頼 (MfD)', rfd: 'リダイレクトの削除・転送先変更依頼 (RfD)', rm: '改名依頼 (RM)' }
},
// TwinkleConfig.xfdWatchPage (string)
// The watchlist setting of the page being nominated for XfD.
{
name: 'xfdWatchPage',
label: '削除依頼の対象ページをウォッチリストに追加する',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
// TwinkleConfig.xfdWatchDiscussion (string)
// The watchlist setting of the newly created XfD page (for those processes that create discussion pages for each nomination),
// or the list page for the other processes.
{
name: 'xfdWatchDiscussion',
label: '削除依頼ページをウォッチリストに追加する',
helptip: 'AfD・MfDでは個別の依頼ページ、TfD・CfD・RfD・FfDでは日付別の依頼一覧を対象にします。',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
// TwinkleConfig.xfdWatchList (string)
// The watchlist setting of the XfD list page, *if* the discussion is on a separate page.
{
name: 'xfdWatchList',
label: '日付別の依頼一覧や一覧ページをウォッチリストに追加する(AfD・MfD)',
helptip: 'AfD・MfDにのみ適用します。AfDでは日付別の依頼一覧、MfDでは総合一覧ページに依頼ページが読み込まれます。',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
// TwinkleConfig.xfdWatchUser (string)
// The watchlist setting of the user talk page if they receive a notification.
{
name: 'xfdWatchUser',
label: '通知した初版投稿者の会話ページをウォッチリストに追加する',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
// TwinkleConfig.xfdWatchRelated (string)
// The watchlist setting of the target of a redirect being nominated for RfD.
{
name: 'xfdWatchRelated',
label: '通知時にリダイレクト先のページをウォッチリストに追加する',
helptip: 'RfDで、リダイレクト先の会話ページに通知する場合にのみ適用します。',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
{
name: 'markXfdPagesAsPatrolled',
label: '記事の削除依頼(AfD)への提出時にページを巡回済み・確認済みにする(可能な場合)',
type: 'boolean'
}
]
},
{
title: '詳細設定(非表示)',
hidden: true,
preferences: [
// twinklerollback.js: defines how many revision to query maximum, maximum possible is 50, default is 50
{
name: 'revertMaxRevisions',
type: 'integer'
},
// twinklewarn.js: When using the autolevel select option, how many days makes a prior warning stale
// Huggle is three days ([[Special:Diff/918980316]] and [[Special:Diff/919417999]]) while ClueBotNG is two:
// https://github.com/DamianZaremba/cluebotng/blob/4958e25d6874cba01c75f11debd2e511fd5a2ce5/bot/action_functions.php#L62
{
name: 'autolevelStaleDays',
type: 'integer'
},
// How many pages should be queried by deprod and batchdelete/protect/undelete
{
name: 'batchMax',
type: 'integer',
adminOnly: true
},
// How many pages should be processed at a time by deprod and batchdelete/protect/undelete
{
name: 'batchChunks',
type: 'integer',
adminOnly: true
}
]
}
]; // end of Twinkle.config.sections
Twinkle.config.init = function twinkleconfigInit() {
// create the config page at User:Syunsyunminmin/Twinkle/Preferences
if ((mw.config.get('wgNamespaceNumber') === mw.config.get('wgNamespaceIds').user && mw.config.get('wgTitle') === 'Syunsyunminmin/Twinkle/Preferences') &&
mw.config.get('wgAction') === 'view') {
if (!document.getElementById('twinkle-config')) {
return; // maybe the page is misconfigured, or something - but any attempt to modify it will be pointless
}
// set style to nothing to prevent conflict with external css
document.getElementById('twinkle-config').removeAttribute('style');
document.getElementById('twinkle-config-titlebar').removeAttribute('style');
const contentdiv = document.getElementById('twinkle-config-content');
contentdiv.textContent = ''; // clear children
// let user know about possible conflict with skin js/common.js file
// (settings in that file will still work, but they will be overwritten by twinkleoptions.js settings)
if (window.TwinkleConfig || window.FriendlyConfig) {
const contentnotice = document.createElement('div');
contentnotice.className = 'plainlinks twinkle-ombox';
contentnotice.innerHTML =
'<div>' +
'<img alt="" src="https://upload.wikimedia.org/wikipedia/commons/3/38/Imbox_content.png" />' +
'</div>' +
'<div>' +
'<p><big><b>ここで設定を変更する前に、</b>個人用JavaScriptから古いTwinkleとFriendlyの設定を削除してください。</big></p>' +
'<p><a href="' + mw.util.getUrl('User:' + mw.config.get('wgUserName') + '/' + mw.config.get('skin') +
'.js', { action: 'edit' }) + '" target="_blank"><b>使用中の外装の個人用JavaScript</b></a>または<a href="' +
mw.util.getUrl('User:' + mw.config.get('wgUserName') + '/common.js', { action: 'edit'}) + '" target="_blank"><b>common.js</b></a>を編集し、<code>TwinkleConfig</code>と<code>FriendlyConfig</code>を参照する設定行を削除してください。</p>' +
'</div>';
contentdiv.appendChild(contentnotice);
}
// start a table of contents
const toctable = document.createElement('div');
toctable.className = 'toc';
toctable.style.marginLeft = '0.4em';
// create TOC title
const toctitle = document.createElement('div');
toctitle.id = 'toctitle';
var toch2 = document.createElement('h2');
toch2.textContent = '目次 ';
toctitle.appendChild(toch2);
// add TOC show/hide link
const toctoggle = document.createElement('span');
toctoggle.className = 'toctoggle';
toctoggle.appendChild(document.createTextNode('['));
const toctogglelink = document.createElement('a');
toctogglelink.className = 'internal';
toctogglelink.setAttribute('href', '#tw-tocshowhide');
toctogglelink.textContent = '非表示';
toctoggle.appendChild(toctogglelink);
toctoggle.appendChild(document.createTextNode(']'));
toctitle.appendChild(toctoggle);
toctable.appendChild(toctitle);
// create item container: this is what we add stuff to
const tocul = document.createElement('ul');
toctogglelink.addEventListener('click', () => {
const $tocul = $(tocul);
$tocul.toggle();
if ($tocul.find(':visible').length) {
toctogglelink.textContent = '非表示';
} else {
toctogglelink.textContent = '表示';
}
}, false);
toctable.appendChild(tocul);
contentdiv.appendChild(toctable);
const contentform = document.createElement('form');
contentform.setAttribute('action', 'javascript:void(0)'); // was #tw-save - changed to void(0) to work around Chrome issue
contentform.addEventListener('submit', Twinkle.config.save, true);
contentdiv.appendChild(contentform);
const container = document.createElement('table');
container.style.width = '100%';
contentform.appendChild(container);
$(Twinkle.config.sections).each((sectionkey, section) => {
if (section.hidden || (section.adminOnly && !Morebits.userIsSysop)) {
return true; // i.e. "continue" in this context
}
// add to TOC
const tocli = document.createElement('li');
tocli.className = 'toclevel-1';
const toca = document.createElement('a');
toca.setAttribute('href', '#' + section.module);
toca.appendChild(document.createTextNode(section.title));
tocli.appendChild(toca);
tocul.appendChild(tocli);
let row = document.createElement('tr');
let cell = document.createElement('td');
cell.setAttribute('colspan', '3');
const heading = document.createElement('h4');
heading.style.borderBottom = '1px solid gray';
heading.style.marginTop = '0.2em';
heading.id = section.module;
heading.appendChild(document.createTextNode(section.title));
cell.appendChild(heading);
row.appendChild(cell);
container.appendChild(row);
let rowcount = 1; // for row banding
// add each of the preferences to the form
$(section.preferences).each((prefkey, pref) => {
if (pref.adminOnly && !Morebits.userIsSysop) {
return true; // i.e. "continue" in this context
}
row = document.createElement('tr');
row.style.marginBottom = '0.2em';
// create odd row banding
if (rowcount++ % 2 === 0) {
row.style.backgroundColor = 'rgba(128, 128, 128, 0.1)';
}
cell = document.createElement('td');
var label, input, gotPref = Twinkle.getPref(pref.name), obsoletePref = Twinkle.getPref(pref.obsolete);
switch (pref.type) {
case 'boolean': // create a checkbox
cell.setAttribute('colspan', '2');
label = document.createElement('label');
input = document.createElement('input');
input.setAttribute('type', 'checkbox');
input.setAttribute('id', pref.name);
input.setAttribute('name', pref.name);
if (gotPref === true || obsoletePref === true) {
input.setAttribute('checked', 'checked');
}
label.appendChild(input);
label.appendChild(document.createTextNode(pref.label));
cell.appendChild(label);
break;
case 'string': // create an input box
case 'integer':
// add label to first column
cell.style.textAlign = 'right';
cell.style.paddingRight = '0.5em';
label = document.createElement('label');
label.setAttribute('for', pref.name);
label.appendChild(document.createTextNode(pref.label + ':'));
cell.appendChild(label);
row.appendChild(cell);
// add input box to second column
cell = document.createElement('td');
cell.style.paddingRight = '1em';
input = document.createElement('input');
input.setAttribute('type', 'text');
input.setAttribute('id', pref.name);
input.setAttribute('name', pref.name);
if (pref.type === 'integer') {
input.setAttribute('size', 6);
input.setAttribute('type', 'number');
input.setAttribute('step', '1'); // integers only
}
if (gotPref) {
input.setAttribute('value', gotPref);
}
if (obsoletePref) {
input.setAttribute('value', obsoletePref);
}
cell.appendChild(input);
break;
case 'enum': // create a combo box
// add label to first column
// note: duplicates the code above, under string/integer
cell.style.textAlign = 'right';
cell.style.paddingRight = '0.5em';
label = document.createElement('label');
label.setAttribute('for', pref.name);
label.appendChild(document.createTextNode(pref.label + ':'));
cell.appendChild(label);
row.appendChild(cell);
// add input box to second column
cell = document.createElement('td');
cell.style.paddingRight = '1em';
input = document.createElement('select');
input.setAttribute('id', pref.name);
input.setAttribute('name', pref.name);
$.each(pref.enumValues, (enumvalue, enumdisplay) => {
const option = document.createElement('option');
option.setAttribute('value', enumvalue);
if ((gotPref === enumvalue || obsoletePref === enumvalue) ||
// Hack to convert old boolean watchlist prefs
// to corresponding enums (added in v2.1)
((typeof gotPref === 'boolean' || typeof obsoletePref === 'boolean') &&
(((gotPref || obsoletePref) && enumvalue === 'yes') ||
(!(gotPref && obsoletePref) && enumvalue === 'no')))) {
option.setAttribute('selected', 'selected');
}
option.appendChild(document.createTextNode(enumdisplay));
input.appendChild(option);
});
cell.appendChild(input);
break;
case 'set': // create a set of check boxes
// add label first of all
cell.setAttribute('colspan', '2');
label = document.createElement('label'); // not really necessary to use a label element here, but we do it for consistency of styling
label.appendChild(document.createTextNode(pref.label + ':'));
cell.appendChild(label);
var checkdiv = document.createElement('div');
checkdiv.style.paddingLeft = '1em';
var worker = function(itemkey, itemvalue) {
const checklabel = document.createElement('label');
checklabel.style.marginRight = '0.7em';
checklabel.style.display = 'inline-block';
const check = document.createElement('input');
check.setAttribute('type', 'checkbox');
check.setAttribute('id', pref.name + '_' + itemkey);
check.setAttribute('name', pref.name + '_' + itemkey);
if ((gotPref && gotPref.includes(itemkey)) || (obsoletePref && obsoletePref.includes(itemkey))) {
check.setAttribute('checked', 'checked');
}
// cater for legacy integer array values for unlinkNamespaces (this can be removed a few years down the track...)
if (pref.name === 'unlinkNamespaces') {
if ((gotPref && gotPref.includes(parseInt(itemkey, 10))) || (obsoletePref && obsoletePref.includes(parseInt(itemkey, 10)))) {
check.setAttribute('checked', 'checked');
}
}
checklabel.appendChild(check);
checklabel.appendChild(document.createTextNode(itemvalue));
checkdiv.appendChild(checklabel);
};
if (pref.setDisplayOrder) {
// add check boxes according to the given display order
$.each(pref.setDisplayOrder, (itemkey, item) => {
worker(item, pref.setValues[item]);
});
} else {
// add check boxes according to the order it gets fed to us (probably strict alphabetical)
$.each(pref.setValues, worker);
}
cell.appendChild(checkdiv);
break;
case 'customList':
// add label to first column
cell.style.textAlign = 'right';
cell.style.paddingRight = '0.5em';
label = document.createElement('label');
label.setAttribute('for', pref.name);
label.appendChild(document.createTextNode(pref.label + ':'));
cell.appendChild(label);
row.appendChild(cell);
// add button to second column
cell = document.createElement('td');
cell.style.paddingRight = '1em';
var button = document.createElement('button');
button.setAttribute('id', pref.name);
button.setAttribute('name', pref.name);
button.setAttribute('type', 'button');
button.addEventListener('click', Twinkle.config.listDialog.display, false);
// use jQuery data on the button to store the current config value
$(button).data({
value: (obsoletePref) || gotPref,
pref: pref
});
button.appendChild(document.createTextNode('アイテムを編集'));
cell.appendChild(button);
break;
default:
alert('twinkleconfig: 設定 ' + pref.name + ' の不明なデータ型');
break;
}
row.appendChild(cell);
// add help tip
cell = document.createElement('td');
cell.className = 'twinkle-config-helptip';
if (pref.helptip) {
// convert mentions of templates in the helptip to clickable links
cell.innerHTML = pref.helptip.replace(/{{(.+?)}}/g,
'{{<a href="' + mw.util.getUrl('Template:') + '$1" target="_blank">$1</a>}}');
}
// add reset link (custom lists don't need this, as their config value isn't displayed on the form)
if (pref.type !== 'customList') {
const resetlink = document.createElement('a');
resetlink.setAttribute('href', '#tw-reset');
resetlink.setAttribute('id', 'twinkle-config-reset-' + pref.name);
resetlink.addEventListener('click', Twinkle.config.resetPrefLink, false);
resetlink.style.cssFloat = 'right';
resetlink.style.margin = '0 0.6em';
resetlink.appendChild(document.createTextNode('初期化'));
cell.appendChild(resetlink);
}
row.appendChild(cell);
container.appendChild(row);
return true;
});
return true;
});
const footerbox = document.createElement('div');
footerbox.setAttribute('id', 'twinkle-config-buttonpane');
const button = document.createElement('button');
button.setAttribute('id', 'twinkle-config-submit');
button.setAttribute('type', 'submit');
button.appendChild(document.createTextNode('変更を保存'));
footerbox.appendChild(button);
const footerspan = document.createElement('span');
footerspan.className = 'plainlinks';
footerspan.style.marginLeft = '2.4em';
footerspan.style.fontSize = '90%';
const footera = document.createElement('a');
footera.setAttribute('href', '#tw-reset-all');
footera.setAttribute('id', 'twinkle-config-resetall');
footera.addEventListener('click', Twinkle.config.resetAllPrefs, false);
footera.appendChild(document.createTextNode('デフォルトに戻す'));
footerspan.appendChild(footera);
footerbox.appendChild(footerspan);
contentform.appendChild(footerbox);
// since all the section headers exist now, we can try going to the requested anchor
if (window.location.hash) {
const loc = window.location.hash;
window.location.hash = '';
window.location.hash = loc;
}
} else if (mw.config.get('wgNamespaceNumber') === mw.config.get('wgNamespaceIds').user &&
mw.config.get('wgTitle').indexOf(mw.config.get('wgUserName')) === 0 &&
mw.config.get('wgPageName').slice(-3) === '.js') {
const box = document.createElement('div');
// Styled in twinkle.css
box.setAttribute('id', 'twinkle-config-headerbox');
let link;
const scriptPageName = mw.config.get('wgPageName').slice(
mw.config.get('wgPageName').lastIndexOf('/') + 1,
mw.config.get('wgPageName').lastIndexOf('.js')
);
if (scriptPageName === 'twinkleoptions') {
// place "why not try the preference panel" notice
box.setAttribute('class', 'config-twopt-box');
link = document.createElement('a');
link.setAttribute('href', mw.util.getUrl(mw.config.get('wgFormattedNamespaces')[mw.config.get('wgNamespaceIds').user] + ':Syunsyunminmin/Twinkle/Preferences'));
link.appendChild(document.createTextNode('Twinkle個人設定パネル'));
if (mw.config.get('wgArticleId') > 0) { // page exists
box.appendChild(document.createTextNode('このページにはTwinkleの設定が含まれています。設定は '));
box.appendChild(link);
box.appendChild(document.createTextNode('を使用するか、このページを編集することで変更できます。'));
} else { // page does not exist
box.appendChild(link);
box.appendChild(document.createTextNode('またはこのページを編集することで、Twinkleを個人の好みに合わせてカスタマイズできます。'));
}
$(box).insertAfter($('#contentSub'));
} else if (['monobook', 'vector', 'vector-2022', 'cologneblue', 'modern', 'timeless', 'minerva', 'common'].includes(scriptPageName)) {
// place "Looking for Twinkle options?" notice
box.setAttribute('class', 'config-userskin-box');
box.appendChild(document.createTextNode('Twinkle個人設定を設定したい場合、'));
link = document.createElement('a');
link.setAttribute('href', mw.util.getUrl(mw.config.get('wgFormattedNamespaces')[mw.config.get('wgNamespaceIds').user] + ':Syunsyunminmin/Twinkle/Preferences'));
link.appendChild(document.createTextNode('Twinkle個人設定パネル'));
box.appendChild(link);
box.appendChild(document.createTextNode('を使用して行えます。'));
$(box).insertAfter($('#contentSub'));
}
}
};
// custom list-related stuff
Twinkle.config.listDialog = {};
Twinkle.config.listDialog.addRow = function twinkleconfigListDialogAddRow($dlgtable, value, label) {
let $contenttr, $valueInput, $labelInput;
$dlgtable.append(
$contenttr = $('<tr>').append(
$('<td>').append(
$('<button>')
.attr('type', 'button')
.css('white-space', 'nowrap')
.on('click', () => {
$contenttr.remove();
})
.text('除去')
),
$('<td>').append(
$valueInput = $('<input>')
.attr('type', 'text')
.addClass('twinkle-config-customlist-value')
.css('width', '97%')
),
$('<td>').append(
$labelInput = $('<input>')
.attr('type', 'text')
.addClass('twinkle-config-customlist-label')
.css('width', '98%')
)
)
);
if (value) {
$valueInput.val(value);
}
if (label) {
$labelInput.val(label);
}
};
Twinkle.config.listDialog.display = function twinkleconfigListDialogDisplay(e) {
const $prefbutton = $(e.target);
const curvalue = $prefbutton.data('value');
const curpref = $prefbutton.data('pref');
const dialog = new Morebits.SimpleWindow(720, 400);
dialog.setTitle(curpref.label);
dialog.setScriptName('Twinkle個人設定');
let $dlgtbody;
dialog.setContent(
$('<div>').append(
$('<table>')
.addClass('wikitable')
.css({
margin: '1.4em 1em',
width: 'auto'
})
.append(
$dlgtbody = $('<tbody>').append(
// header row
$('<tr>').append(
$('<th>') // top-left cell
.css('width', '5%'),
$('<th>') // value column header
.css('width', '35%')
.text(curpref.customListValueTitle ? curpref.customListValueTitle : '値'),
$('<th>') // label column header
.css('width', '60%')
.text(curpref.customListLabelTitle ? curpref.customListLabelTitle : '表示名')
)
),
$('<tfoot>').append(
$('<tr>').append(
$('<td>')
.attr('colspan', '3')
.append(
$('<button>')
.text('追加')
.css('min-width', '8em')
.attr('type', 'button')
.on('click', () => {
Twinkle.config.listDialog.addRow($dlgtbody);
})
)
)
)
),
$('<button>')
.text('変更を保存')
.attr('type', 'submit') // so Morebits.SimpleWindow puts the button in the button pane
.on('click', () => {
Twinkle.config.listDialog.save($prefbutton, $dlgtbody);
dialog.close();
}),
$('<button>')
.text('初期化')
.attr('type', 'submit')
.on('click', () => {
Twinkle.config.listDialog.reset($prefbutton, $dlgtbody);
}),
$('<button>')
.text('キャンセル')
.attr('type', 'submit')
.on('click', () => {
dialog.close();
})
)[0]
);
// content rows
let gotRow = false;
$.each(curvalue, (k, v) => {
gotRow = true;
Twinkle.config.listDialog.addRow($dlgtbody, v.value, v.label);
});
// if there are no values present, add a blank row to start the user off
if (!gotRow) {
Twinkle.config.listDialog.addRow($dlgtbody);
}
dialog.display();
};
// Resets the data value, re-populates based on the new (default) value, then saves the
// old data value again (less surprising behaviour)
Twinkle.config.listDialog.reset = function twinkleconfigListDialogReset($button, $tbody) {
// reset value on button
const curpref = $button.data('pref');
const oldvalue = $button.data('value');
Twinkle.config.resetPref(curpref);
// reset form
$tbody.find('tr').slice(1).remove(); // all rows except the first (header) row
// add the new values
const curvalue = $button.data('value');
$.each(curvalue, (k, v) => {
Twinkle.config.listDialog.addRow($tbody, v.value, v.label);
});
// save the old value
$button.data('value', oldvalue);
};
Twinkle.config.listDialog.save = function twinkleconfigListDialogSave($button, $tbody) {
const result = [];
let current = {};
$tbody.find('input[type="text"]').each((inputkey, input) => {
if ($(input).hasClass('twinkle-config-customlist-value')) {
current = { value: input.value };
} else {
current.label = input.value;
// exclude totally empty rows
if (current.value || current.label) {
result.push(current);
}
}
});
$button.data('value', result);
};
// reset/restore defaults
Twinkle.config.resetPrefLink = function twinkleconfigResetPrefLink(e) {
const wantedpref = e.target.id.slice(21); // "twinkle-config-reset-" prefix is stripped
// search tactics
$(Twinkle.config.sections).each((sectionkey, section) => {
if (section.hidden || (section.adminOnly && !Morebits.userIsSysop)) {
return true; // continue: skip impossibilities
}
let foundit = false;
$(section.preferences).each((prefkey, pref) => {
if (pref.name !== wantedpref) {
return true; // continue
}
Twinkle.config.resetPref(pref);
foundit = true;
return false; // break
});
if (foundit) {
return false; // break
}
});
return false; // stop link from scrolling page
};
Twinkle.config.resetPref = function twinkleconfigResetPref(pref) {
switch (pref.type) {
case 'boolean':
document.getElementById(pref.name).checked = Twinkle.defaultConfig[pref.name];
break;
case 'string':
case 'integer':
case 'enum':
document.getElementById(pref.name).value = Twinkle.defaultConfig[pref.name];
break;
case 'set':
$.each(pref.setValues, (itemkey) => {
if (document.getElementById(pref.name + '_' + itemkey)) {
document.getElementById(pref.name + '_' + itemkey).checked = Twinkle.defaultConfig[pref.name].includes(itemkey);
}
});
break;
case 'customList':
$(document.getElementById(pref.name)).data('value', Twinkle.defaultConfig[pref.name]);
break;
default:
alert('twinkleconfig: 設定 ' + pref.name + ' の不明なデータ型');
break;
}
};
Twinkle.config.resetAllPrefs = function twinkleconfigResetAllPrefs() {
// no confirmation message - the user can just refresh/close the page to abort
$(Twinkle.config.sections).each((sectionkey, section) => {
if (section.hidden || (section.adminOnly && !Morebits.userIsSysop)) {
return true; // continue: skip impossibilities
}
$(section.preferences).each((prefkey, pref) => {
if (!pref.adminOnly || Morebits.userIsSysop) {
Twinkle.config.resetPref(pref);
}
});
return true;
});
return false; // stop link from scrolling page
};
Twinkle.config.save = function twinkleconfigSave(e) {
Morebits.Status.init(document.getElementById('twinkle-config-content'));
var userjs = mw.config.get('wgFormattedNamespaces')[mw.config.get('wgNamespaceIds').user] + ':' + mw.config.get('wgUserName') + '/twinkleoptions.js';
var wikipedia_page = new Morebits.wiki.Page(userjs, userjs + 'に設定を保存');
wikipedia_page.setCallbackParameters(e.target);
wikipedia_page.load(Twinkle.config.writePrefs);
return false;
};
Twinkle.config.writePrefs = function twinkleconfigWritePrefs(pageobj) {
const form = pageobj.getCallbackParameters();
// this is the object which gets serialized into JSON; only
// preferences that this script knows about are kept
const newConfig = {optionsVersion: 2.1};
// a comparison function is needed later on
// it is just enough for our purposes (i.e. comparing strings, numbers, booleans,
// arrays of strings, and arrays of { value, label })
// and it is not very robust: e.g. compare([2], ["2"]) === true, and
// compare({}, {}) === false, but it's good enough for our purposes here
const compare = function(a, b) {
if (Array.isArray(a)) {
if (a.length !== b.length) {
return false;
}
const asort = a.sort(), bsort = b.sort();
for (let i = 0; asort[i]; ++i) {
// comparison of the two properties of custom lists
if ((typeof asort[i] === 'object') && (asort[i].label !== bsort[i].label ||
asort[i].value !== bsort[i].value)) {
return false;
} else if (asort[i].toString() !== bsort[i].toString()) {
return false;
}
}
return true;
}
return a === b;
};
$(Twinkle.config.sections).each((sectionkey, section) => {
if (section.adminOnly && !Morebits.userIsSysop) {
return; // i.e. "continue" in this context
}
// reach each of the preferences from the form
$(section.preferences).each((prefkey, pref) => {
let userValue; // = undefined
// only read form values for those prefs that have them
if (!pref.adminOnly || Morebits.userIsSysop) {
if (!section.hidden) {
switch (pref.type) {
case 'boolean': // read from the checkbox
userValue = form[pref.name].checked;
break;
case 'string': // read from the input box or combo box
case 'enum':
userValue = form[pref.name].value;
break;
case 'integer': // read from the input box
userValue = parseInt(form[pref.name].value, 10);
if (isNaN(userValue)) {
Morebits.Status.warn('保存', pref.name + 'に対して指定した値 (' + pref.value + ') は無効です。保存は続行しますが、無効なデータ値は飛ばされます。');
userValue = null;
}
break;
case 'set': // read from the set of check boxes
userValue = [];
if (pref.setDisplayOrder) {
// read only those keys specified in the display order
$.each(pref.setDisplayOrder, (itemkey, item) => {
if (form[pref.name + '_' + item].checked) {
userValue.push(item);
}
});
} else {
// read all the keys in the list of values
$.each(pref.setValues, (itemkey) => {
if (form[pref.name + '_' + itemkey].checked) {
userValue.push(itemkey);
}
});
}
break;
case 'customList': // read from the jQuery data stored on the button object
userValue = $(form[pref.name]).data('value');
break;
default:
alert('twinkleconfig: 設定 ' + pref.name + ' の不明なデータ型');
break;
}
} else if (Twinkle.prefs) {
// Retain the hidden preferences that may have customised by the user from twinkleoptions.js
// undefined if not set
userValue = Twinkle.prefs[pref.name];
}
}
// only save those preferences that are *different* from the default
if (userValue !== undefined && !compare(userValue, Twinkle.defaultConfig[pref.name])) {
newConfig[pref.name] = userValue;
}
});
});
var text =
'// twinkleoptions.js: Twinkleの個人設定ファイル\n' +
'//\n' +
'// 注意: Twinkleの設定を変更する最も簡単な方法は、\n' +
'// Twinkle設定パネルを使用することです。設定パネルは[[' + Morebits.pageNameNorm + ']]にあります。\n' +
'//\n' +
'// このファイルは自動的に生成されます。\n' +
'// (有効なJavaScriptの方法で構成パラメーターを変更する以外に)行った変更は、\n' +
'// 次にTwinkle設定パネルで「保存」をクリックしたときに上書きされます。\n' +
'// このファイルを直接変更する場合は、必ず正しいJavaScriptを使用してください。\n' +
// eslint-disable-next-line no-useless-concat
'// <no' + 'wiki>\n' +
'\n' +
'window.Twinkle.prefs = ';
text += JSON.stringify(newConfig, null, 2);
text +=
';\n' +
'\n' +
// eslint-disable-next-line no-useless-concat
'// </no' + 'wiki>\n' +
'// twinkleoptions.js の終わり\n';
pageobj.setPageText(text);
pageobj.setEditSummary('Twinkle個人設定を保存: [[:' + Morebits.pageNameNorm + ']]からの自動編集');
pageobj.setCreateOption('recreate');
pageobj.save(Twinkle.config.saveSuccess);
};
Twinkle.config.saveSuccess = function twinkleconfigSaveSuccess(pageobj) {
pageobj.getStatusElement().info('成功');
const noticebox = document.createElement('div');
noticebox.className = 'cdx-message cdx-message--success';
noticebox.style.fontSize = '100%';
noticebox.style.marginTop = '2em';
noticebox.innerHTML = '<p><b>Twinkle個人設定は保存されました。</b></p><p>変更を確認するには、<b>ブラウザのキャッシュを完全にクリアする</b>必要があります。 (手順については <a href="' + mw.util.getUrl('WP:BYPASS') + '" title="WP:BYPASS">WP:BYPASS</a> 参照</p>';
Morebits.Status.root.appendChild(noticebox);
var noticeclear = document.createElement('br');
noticeclear.style.clear = 'both';
Morebits.Status.root.appendChild(noticeclear);
};
Twinkle.addInitCallback(Twinkle.config.init);
}());
// </nowiki>
4u549oz9g3hdx20orjxrlb52jtel5rn
769545
769527
2026-10-22T00:34:19Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 64781a2167c76216d4920097bd68990736ae168d: config変更
769545
javascript
text/javascript
// <nowiki>
(function() {
/*
****************************************
*** twinkleconfig.js: Preferences module
****************************************
* Mode of invocation: Adds configuration form to User:Syunsyunminmin/Twinkle/Preferences,
and adds an ad box to the top of user subpages belonging to the
currently logged-in user which end in '.js'
* Active on: What I just said. Yeah.
*/
Twinkle.config = {};
Twinkle.config.watchlistEnums = {
yes: 'ウォッチリストに追加 (無期限)',
no: 'ウォッチリストに追加しない',
default: 'サイトの設定に従う',
'1 week': '1週間ウォッチ',
'1 month': '1ヶ月ウォッチ',
'3 months': '3ヶ月ウォッチ',
'6 months': '6ヶ月ウォッチ'
};
Twinkle.config.commonSets = {
csdCriteria: {
db: 'その他 ({{即時削除|その他}})',
g1: 'G1', g2: 'G2', g3: 'G3', g4: 'G4', g5: 'G5', g6: 'G6', g8: 'G8', g9: 'G9', g10: 'G10',
a1: 'A1',
u1: 'U1', u2: 'U2', u3: 'U3',
'f1-2': 'F1-2', 'f1-3': 'F1-3', 'f1-4': 'F1-4', 'f1-5': 'F1-5', 'f1-6': 'F1-6',
f3: 'F3', f5: 'F5', f6: 'F6', f7: 'F7', f8: 'F8', f9: 'F9',
c1: 'C1', c3: 'C3', c6: 'C6',
'r1-1': 'R1-1', 'r1-2': 'R1-2', 'r1-3': 'R1-3', 'r1-4': 'R1-4',
'r2-1': 'R2-1', 'r2-2': 'R2-2', 'r2-3': 'R2-3', 'r2-5': 'R2-5', 'r2-6': 'R2-6',
r4: 'R4'
},
csdCriteriaDisplayOrder: [
'db',
'g1', 'g2', 'g3', 'g4', 'g5', 'g6', 'g8', 'g9', 'g10',
'a1',
'u1', 'u2', 'u3',
'f1-2', 'f1-3', 'f1-4', 'f1-5', 'f1-6', 'f3', 'f5', 'f6', 'f7', 'f8', 'f9',
'c1', 'c3', 'c6',
'r1-1', 'r1-2', 'r1-3', 'r1-4', 'r2-1', 'r2-2', 'r2-3', 'r2-5', 'r2-6', 'r4'
],
csdAndImageDeletionCriteria: {
db: 'その他 ({{即時削除|その他}})',
g1: 'G1', g2: 'G2', g3: 'G3', g4: 'G4', g5: 'G5', g6: 'G6', g8: 'G8', g9: 'G9', g10: 'G10',
a1: 'A1',
u1: 'U1', u2: 'U2', u3: 'U3',
'f1-2': 'F1-2', 'f1-3': 'F1-3', 'f1-4': 'F1-4', 'f1-5': 'F1-5', 'f1-6': 'F1-6',
f3: 'F3', f5: 'F5', f6: 'F6', f7: 'F7', f8: 'F8', f9: 'F9',
c1: 'C1', c3: 'C3', c6: 'C6',
'r1-1': 'R1-1', 'r1-2': 'R1-2', 'r1-3': 'R1-3', 'r1-4': 'R1-4',
'r2-1': 'R2-1', 'r2-2': 'R2-2', 'r2-3': 'R2-3', 'r2-5': 'R2-5', 'r2-6': 'R2-6',
r4: 'R4'
},
csdAndImageDeletionCriteriaDisplayOrder: [
'db',
'g1', 'g2', 'g3', 'g4', 'g5', 'g6', 'g8', 'g9', 'g10',
'a1',
'u1', 'u2', 'u3',
'f1-2', 'f1-3', 'f1-4', 'f1-5', 'f1-6', 'f3', 'f5', 'f6', 'f7', 'f8', 'f9',
'c1', 'c3', 'c6',
'r1-1', 'r1-2', 'r1-3', 'r1-4', 'r2-1', 'r2-2', 'r2-3', 'r2-5', 'r2-6', 'r4'
],
namespacesNoSpecial: {
0: '標準',
1: 'ノート (標準)',
2: '利用者',
3: '利用者‐会話',
4: 'Wikipedia',
5: 'Wikipedia‐ノート',
6: 'ファイル',
7: 'ファイル‐ノート',
8: 'MediaWiki',
9: 'MediaWiki‐ノート',
10: 'Template',
11: 'Template‐ノート',
12: 'Help',
13: 'Help‐ノート',
14: 'Category',
15: 'Category‐ノート',
100: 'Portal',
101: 'Portal‐ノート',
710: 'TimedText',
711: 'TimedText‐ノート',
828: 'Module',
829: 'Module‐ノート'
}
};
Twinkle.config.commonSets.csdCriteriaDisplayOrder = Object.keys(Twinkle.config.commonSets.csdCriteria);
Twinkle.config.commonSets.csdAndImageDeletionCriteriaDisplayOrder = Object.keys(Twinkle.config.commonSets.csdAndImageDeletionCriteria);
/**
* Section entry format:
*
* {
* title: <human-readable section title>,
* module: <name of the associated module, used to link to sections>,
* adminOnly: <true for admin-only sections>,
* hidden: <true for advanced preferences that rarely need to be changed - they can still be modified by manually editing twinkleoptions.js>,
* preferences: [
* {
* name: <TwinkleConfig property name>,
* label: <human-readable short description - used as a form label>,
* helptip: <(optional) human-readable text (using valid HTML) that complements the description, like limits, warnings, etc.>
* adminOnly: <true for admin-only preferences>,
* type: <string|boolean|integer|enum|set|customList> (customList stores an array of JSON objects { value, label }),
* enumValues: <for type = "enum": a JSON object where the keys are the internal names and the values are human-readable strings>,
* setValues: <for type = "set": a JSON object where the keys are the internal names and the values are human-readable strings>,
* setDisplayOrder: <(optional) for type = "set": an array containing the keys of setValues (as strings) in the order that they are displayed>,
* customListValueTitle: <for type = "customList": the heading for the left "value" column in the custom list editor>,
* customListLabelTitle: <for type = "customList": the heading for the right "label" column in the custom list editor>
* },
* . . .
* ]
* },
* . . .
*
*/
Twinkle.config.sections = [
{
title: '全般',
module: 'general',
preferences: [
// TwinkleConfig.userTalkPageMode may take arguments:
// 'window': open a new window, remember the opened window
// 'tab': opens in a new tab, if possible.
// 'blank': force open in a new window, even if such a window exists
{
name: 'userTalkPageMode',
label: '利用者会話ページを開く時は次のページで開く',
type: 'enum',
enumValues: { window: 'ウィンドウ内で他の利用者会話ページを置き換える', tab: '新しいタブ', blank: '完全に新しいウィンドウ' }
},
// TwinkleConfig.dialogLargeFont (boolean)
{
name: 'dialogLargeFont',
label: 'Twinkleダイアログで大きな文字を使用する',
type: 'boolean'
},
// Twinkle.config.disabledModules (array)
{
name: 'disabledModules',
label: '選択したTwinkleモジュールをオフにする',
helptip: 'チェックを入れた機能は無効になります。再び有効にするにはチェックを外してください。<br />既定でチェックが入っているものは準備中の機能です。無効にしていても、関連する設定項目は表示されます。',
type: 'set',
setValues: { arv: 'ARV', warn: 'Warn', talkback: 'Talkback', speedy: 'CSD', xfd: 'XfD', image: 'DI', protect: 'RPP', tag: 'Tag', diff: 'Diff', unlink: 'Unlink', rollback: 'Rollback' }
},
// Twinkle.config.disabledSysopModules (array)
{
name: 'disabledSysopModules',
label: '選択した管理者専用モジュールをオフにする',
helptip: 'ここで選択したものは使用できないので、注意してください。再び有効にするにはチェックを外してください。',
adminOnly: true,
type: 'set',
setValues: { block: 'Block', batchdelete: 'D-batch', batchprotect: 'P-batch', batchundelete: 'Und-batch' }
}
]
},
{
title: 'ARV: 荒らしの報告',
module: 'arv',
preferences: [
{
name: 'spiWatchReport',
label: '多重アカウントの調査依頼ページをウォッチリストに追加する',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
}
]
},
{
title: 'Block: 利用者のブロック',
module: 'block',
adminOnly: true,
preferences: [
// TwinkleConfig.defaultToBlock64 (boolean)
// Whether to default to just blocking the /64 on or off
{
name: 'defaultToBlock64',
label: 'IPv6アドレスでは、既定で/64の範囲をブロックする',
type: 'boolean'
},
// TwinkleConfig.defaultToPartialBlocks (boolean)
// Whether to default partial blocks on or off
{
name: 'defaultToPartialBlocks',
label: 'ブロックメニューを開いた際、既定で部分ブロックを選択する',
helptip: '利用者がすでにブロックされている場合は、現在のブロックの種類が優先されます。',
type: 'boolean'
},
// TwinkleConfig.blankTalkpageOnIndefBlock (boolean)
// if true, blank the talk page when issuing an indef block notice (per [[WP:UWUL#Indefinitely blocked users]])
{
name: 'blankTalkpageOnIndefBlock',
label: '無期限ブロックの通知時に利用者会話ページを白紙化する',
helptip: '詳しくは<a href="' + mw.util.getUrl('Wikipedia:WikiProject_User_warnings/Usage_and_layout#Indefinitely_blocked_users') + '">WP:UWUL</a>を参照してください。',
type: 'boolean'
}
]
},
{
title: 'DI: 画像削除',
module: 'image',
preferences: [
// TwinkleConfig.notifyUserOnDeli (boolean)
// If the user should be notified after placing a file deletion tag
{
name: 'notifyUserOnDeli',
label: '既定で"アップロード者に通知"にチェックを入れる',
type: 'boolean'
},
// TwinkleConfig.deliWatchPage (string)
// The watchlist setting of the page tagged for deletion.
{
name: 'deliWatchPage',
label: 'タグ付け時にファイルページをウォッチリストに追加',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
// TwinkleConfig.deliWatchUser (string)
// The watchlist setting of the user talk page if a notification is placed.
{
name: 'deliWatchUser',
label: '通知時に、アップロード者の会話ページをウォッチリストに追加する',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
}
]
},
{
title: (Morebits.userIsSysop ? 'PP' : 'RPP') + ': ページの保護',
module: 'protect',
preferences: [
{
name: 'watchRequestedPages',
label: '保護依頼時にページをウォッチリストに追加する',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
{
name: 'watchPPTaggedPages',
label: '保護テンプレートでページをタグ付けする際、ウォッチリストにページを追加する',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
{
name: 'watchProtectedPages',
label: '保護する際にページをウォッチリストに追加する',
helptip: 'ページ保護後にタグ付けも行う場合は、そちらが優先されます。',
adminOnly: true,
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
}
]
},
{
title: 'Rollback: 差し戻しと巻き戻し', // twinklerollback module
module: 'rollback',
preferences: [
// TwinkleConfig.autoMenuAfterRollback (bool)
// Option to automatically open the warning menu if the user talk page is opened post-reversion
{
name: 'autoMenuAfterRollback',
label: 'Twinkle差し戻し後、利用者会話ページでTwinkle警告メニューを自動的に開く',
helptip: '以下の該当するボックスにチェックが入っている場合のみ動作します。',
type: 'boolean'
},
// TwinkleConfig.openTalkPage (array)
// What types of actions that should result in opening of talk page
{
name: 'openTalkPage',
label: '以下の種類の差し戻し後に、会話ページを開く。',
type: 'set',
setValues: { norm: '通常の差し戻し', vand: '荒らしの差し戻し' }
},
// TwinkleConfig.openTalkPageOnAutoRevert (bool)
// Defines if talk page should be opened when calling revert from contribs or recent changes pages. If set to true, openTalkPage defines then if talk page will be opened.
{
name: 'openTalkPageOnAutoRevert',
label: '投稿記録や最近の更新から差し戻しを実行した際に、会話ページを開く',
helptip: 'これが有効になっている場合、前の設定で必要なオプションが有効になっていなければ機能しません。',
type: 'boolean'
},
// TwinkleConfig.rollbackInPlace (bool)
//
{
name: 'rollbackInPlace',
label: '投稿記録や最近の更新から差し戻しする際、ページを更新しない。',
helptip: 'これが有効になっている場合、Twinkleは差し戻し後に投稿記録や最近の更新フィードを再読み込みしないため、一度に複数の編集を差し戻すことができます。',
type: 'boolean'
},
// TwinkleConfig.markRevertedPagesAsMinor (array)
// What types of actions that should result in marking edit as minor
{
name: 'markRevertedPagesAsMinor',
label: '以下の種類の差し戻しは細部の編集として行います',
type: 'set',
setValues: { norm: '通常の差し戻し', vand: '荒らしの差し戻し', torev: '"版の復元"' }
},
// TwinkleConfig.watchRevertedPages (array)
// What types of actions that should result in forced addition to watchlist
{
name: 'watchRevertedPages',
label: '以下の種類の差し戻しの際、ページをウォッチリストに追加する',
type: 'set',
setValues: { norm: '通常の差し戻し', vand: '荒らしの差し戻し', torev: '"版の復元"' }
},
// TwinkleConfig.watchRevertedExpiry
// If any of the above items are selected, whether to expire the watch
{
name: 'watchRevertedExpiry',
label: 'ページを差し戻した際、ウォッチする期間は',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
// TwinkleConfig.offerReasonOnNormalRevert (boolean)
// If to offer a prompt for extra summary reason for normal reverts, default to true
{
name: 'offerReasonOnNormalRevert',
label: '通常の差し戻しに理由の入力を求めるプロンプト',
helptip: '"通常の"差し戻しは [差し戻し] リンクから実行される差し戻しです。',
type: 'boolean'
},
{
name: 'confirmOnRollback',
label: '差し戻す前に確認を求める (すべての機器)',
helptip: 'ペンやタッチデバイスを使う人、慢性的に優柔不断な人向け。',
type: 'boolean'
},
{
name: 'confirmOnMobileRollback',
label: '差し戻す前に確認を求める (携帯機器のみ)',
helptip: '携帯機器での誤動作を防ぐ。',
type: 'boolean'
},
// TwinkleConfig.showRollbackLinks (array)
// Where Twinkle should show rollback links:
// diff, others, mine, contribs, history, recent
// Note from TTO: |contribs| seems to be equal to |others| + |mine|, i.e. redundant, so I left it out heres
{
name: 'showRollbackLinks',
label: 'これらのページで差し戻しリンクを表示',
type: 'set',
setValues: { diff: '差分ページ', others: '他の利用者の投稿記録ページ', mine: '自分の投稿記録ページ', recent: '最近の更新や関連ページの更新の特別ページ', history: '履歴ページ' }
},
{
name: 'SummaryOnVandalRevert',
label: '荒らしの差し戻しの際の要約欄をカスタムする',
helptip: 'このボックスに要約を入力します。',
type: 'string',
obsolete: 'vandusersummary'
}
]
},
{
title: 'CSD: 即時削除',
module: 'speedy',
preferences: [
{
name: 'speedySelectionStyle',
label: 'ページをタグ付け/削除するタイミング',
type: 'enum',
enumValues: { buttonClick: '送信ボタンをクリックしたとき', radioClick: 'オプションをクリックするとすぐ' }
},
// TwinkleConfig.watchSpeedyPages (array)
// Whether to add speedy tagged or deleted pages to watchlist
{
name: 'watchSpeedyPages',
label: 'これらの基準を使用する場合は、ウォッチリストにページを追加する',
type: 'set',
setValues: Twinkle.config.commonSets.csdCriteria,
setDisplayOrder: Twinkle.config.commonSets.csdCriteriaDisplayOrder
},
// TwinkleConfig.watchSpeedyExpiry
// If any of the above items are selected, whether to expire the watch
{
name: 'watchSpeedyExpiry',
label: 'ページをタグ付けする場合、それをウォッチする期間',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
// TwinkleConfig.markSpeedyPagesAsPatrolled (boolean)
// If, when applying speedy template to page, to mark the page as triaged/patrolled (if the page was reached from NewPages)
{
name: 'markSpeedyPagesAsPatrolled',
label: 'タグ付けの際、ページを巡回済みとしてマークする。(可能な場合)',
helptip: 'これは最善の慣行に反するおそれがあるので、おそらくチェックすべきではないです。',
type: 'boolean'
},
// TwinkleConfig.promptForSpeedyDeletionSummary (array of strings)
{
name: 'promptForSpeedyDeletionSummary',
label: 'これらの基準で削除する場合、削除要約の編集を行う',
adminOnly: true,
type: 'set',
setValues: Twinkle.config.commonSets.csdAndImageDeletionCriteria,
setDisplayOrder: Twinkle.config.commonSets.csdAndImageDeletionCriteriaDisplayOrder
},
// TwinkleConfig.deleteTalkPageOnDelete (boolean)
// If talk page if exists should also be deleted (CSD G8) when spedying a page (admin only)
{
name: 'deleteTalkPageOnDelete',
label: '既定で"トークページも削除する"にチェックを入れる',
adminOnly: true,
type: 'boolean'
},
{
name: 'deleteRedirectsOnDelete',
label: '既定で「リダイレクトも削除」にチェックを入れる',
adminOnly: true,
type: 'boolean'
},
// TwinkleConfig.deleteSysopDefaultToDelete (boolean)
// Make the CSD screen default to "delete" instead of "tag" (admin only)
{
name: 'deleteSysopDefaultToDelete',
label: '既定でタグ付けではなく即時削除を行う',
helptip: 'CSDタグが既に存在する場合、Twinkleは常に「削除」モードが既定となります。',
adminOnly: true,
type: 'boolean'
},
// TwinkleConfig.speedyWindowWidth (integer)
// Defines the width of the Twinkle SD window in pixels
{
name: 'speedyWindowWidth',
label: '即時削除ウィンドウの幅(ピクセル)',
type: 'integer'
},
// TwinkleConfig.speedyWindowWidth (integer)
// Defines the width of the Twinkle SD window in pixels
{
name: 'speedyWindowHeight',
label: '即時削除ウィンドウの高さ(ピクセル)',
helptip: '大きなモニターをお持ちの場合は、これを増やしてください。',
type: 'integer'
},
{
name: 'logSpeedyNominations',
label: 'すべてのCSDへの指定記録を利用者ページに保持します',
helptip: '管理者以外は削除された投稿にアクセスできないため、利用者ページのログは、Twinkleを使用してCSDに指定したすべてのページを追跡するための優れた方法を提供します。 DIを使用してタグ付けされたファイルもこのログに追加されます。',
type: 'boolean'
},
{
name: 'speedyLogPageName',
label: 'この利用者サブページでCSDユーザースペースログを保持します',
helptip: 'このボックスにサブページ名を入力します。 CSDログは利用者:' + mw.config.get('wgUserName') + '/"サブページ名"にあります。 CSD利用者ページへの記録をオンにした場合にのみ機能します。',
type: 'string'
},
{
name: 'noLogOnSpeedyNomination',
label: 'これらの基準でタグ付けする場合は、利用者サブページの記録項目を作成しない',
type: 'set',
setValues: Twinkle.config.commonSets.csdAndImageDeletionCriteria,
setDisplayOrder: Twinkle.config.commonSets.csdAndImageDeletionCriteriaDisplayOrder
}
]
},
{
title: 'Tag: メンテナンス用テンプレートの付与',
module: 'tag',
preferences: [
{
name: 'watchTaggedVenues',
label: '次の種類のページにテンプレートを付与した際、ウォッチリストに追加する',
type: 'set',
setValues: { articles: '記事', drafts: '下書き', redirects: 'リダイレクト', files: 'ファイル' }
},
{
name: 'watchTaggedPages',
label: 'テンプレートを付与したページのウォッチ期間',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
{
name: 'watchMergeDiscussions',
label: '統合提案を開始した際、会話ページをウォッチリストに追加する',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
{
name: 'markTaggedPagesAsMinor',
label: 'テンプレートの付与を細部の編集として扱う',
type: 'boolean'
},
{
name: 'markTaggedPagesAsPatrolled',
label: '既定で「ページを巡回済み・確認済みにする」にチェックを入れる',
type: 'boolean'
},
{
name: 'tagArticleSortOrder',
label: '記事用テンプレートの既定の表示順',
type: 'enum',
enumValues: { cat: '分類別', alpha: 'アルファベット順' }
},
{
name: 'customTagList',
label: '追加で表示する記事・下書きのメンテナンス用テンプレート',
helptip: 'テンプレート一覧の末尾に選択肢を追加します。Twinkleにまだ登録されていないメンテナンス用テンプレートなどを指定できます。',
type: 'customList',
customListValueTitle: 'テンプレート名(波括弧なし)',
customListLabelTitle: 'テンプレート付与画面に表示する名前'
},
{
name: 'customFileTagList',
label: '追加で表示するファイルのメンテナンス用テンプレート',
helptip: 'ファイルに付与できるテンプレートを追加します。',
type: 'customList',
customListValueTitle: 'テンプレート名(波括弧なし)',
customListLabelTitle: 'テンプレート付与画面に表示する名前'
},
{
name: 'customRedirectTagList',
label: '追加で表示するリダイレクト分類用テンプレート',
helptip: 'リダイレクトに付与できるテンプレートを追加します。',
type: 'customList',
customListValueTitle: 'テンプレート名(波括弧なし)',
customListLabelTitle: 'テンプレート付与画面に表示する名前'
}
]
},
{
title: 'Talkback: 返信通知',
module: 'talkback',
preferences: [
{
name: 'markTalkbackAsMinor',
label: '返信通知を細部の編集として扱う',
type: 'boolean'
},
{
name: 'insertTalkbackSignature',
label: '返信通知に署名を挿入する',
type: 'boolean'
},
{
name: 'talkbackHeading',
label: '返信通知と会話ページへの案内に使用する節見出し',
tooltip: 'ウィキテキストの見出しに使う等号(==)は入力しないでください。',
type: 'string'
},
{
name: 'mailHeading',
label: 'メール送信通知に使用する節見出し',
tooltip: 'ウィキテキストの見出しに使う等号(==)は入力しないでください。',
type: 'string'
}
]
},
{
title: 'Unlink: リンク解除',
module: 'unlink',
preferences: [
// TwinkleConfig.unlinkNamespaces (array)
// In what namespaces unlink should happen, default in 0 (article), 10 (template), 100 (portal), and 118 (draft)
{
name: 'unlinkNamespaces',
label: '次の名前空間のページをリンク解除の対象にする',
helptip: '会話ページの過去ログ内のリンクまで解除されるおそれがあるため、会話用の名前空間は選択しないでください。',
type: 'set',
setValues: Twinkle.config.commonSets.namespacesNoSpecial
}
]
},
{
title: 'Warn: 利用者への警告',
module: 'warn',
preferences: [
// TwinkleConfig.defaultWarningGroup (int)
// Which level warning should be the default selected group, default is 1
{
name: 'defaultWarningGroup',
label: '既定の警告レベル',
type: 'enum',
enumValues: {
1: 'レベル1',
2: 'レベル2',
3: 'レベル3',
4: 'レベル4',
5: '即時の最終警告(レベル4im)',
6: '個別の問題に関する通知',
7: '個別の問題に関する警告',
// 8 was used for block templates before #260
9: '独自の警告',
10: 'すべての警告テンプレート',
11: 'レベルを自動選択(1~4)'
}
},
// TwinkleConfig.combinedSingletMenus (boolean)
// if true, show one menu with both single-issue notices and warnings instead of two separately
{
name: 'combinedSingletMenus',
label: '個別の問題に関する通知と警告のメニューをまとめる',
helptip: '有効にすると、既定の警告レベルで個別の問題に関する通知または警告を選んでいる場合に、統合したメニューを既定で表示します。',
type: 'boolean'
},
// TwinkleConfig.watchWarnings (string)
// Watchlist setting for the page which has been dispatched an warning or notice
{
name: 'watchWarnings',
label: '通知した利用者の会話ページをウォッチリストに追加する',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
// TwinkleConfig.oldSelect (boolean)
// if true, use the native select menu rather the select2-based one
{
name: 'oldSelect',
label: '検索機能のない従来の選択メニューを使用する',
type: 'boolean'
},
{
name: 'customWarningList',
label: '追加で表示する警告テンプレート',
helptip: 'テンプレートや利用者サブページを追加できます。追加した項目は警告画面の「独自の警告」に表示されます。',
type: 'customList',
customListValueTitle: 'テンプレート名(波括弧なし)',
customListLabelTitle: '警告一覧に表示する名前(編集要約にも使用)'
}
]
},
{
title: 'XfD: 削除依頼',
module: 'xfd',
preferences: [
{
name: 'logXfdNominations',
label: '削除依頼(XfD)を提出したページを利用者サブページに記録する',
helptip: 'Twinkleで削除依頼を提出したページを、利用者サブページの記録から確認できます。',
type: 'boolean'
},
{
name: 'xfdLogPageName',
label: '削除依頼の記録を保存する利用者サブページ名',
helptip: 'サブページ名を入力してください。記録先は「利用者:<i>利用者名</i>/<i>サブページ名</i>」です。削除依頼の記録を有効にしている場合にのみ機能します。',
type: 'string'
},
{
name: 'noLogOnXfdNomination',
label: '次の手続きに提出する場合は利用者サブページに記録しない',
type: 'set',
setValues: { afd: '記事の削除依頼 (AfD)', tfd: 'テンプレートの削除・統合依頼 (TfD)', ffd: 'ファイルの削除依頼 (FfD)', cfd: 'カテゴリの削除・変更依頼 (CfD)', cfds: 'カテゴリの即時変更依頼 (CfD/S)', mfd: 'その他のページの削除依頼 (MfD)', rfd: 'リダイレクトの削除・転送先変更依頼 (RfD)', rm: '改名依頼 (RM)' }
},
// TwinkleConfig.xfdWatchPage (string)
// The watchlist setting of the page being nominated for XfD.
{
name: 'xfdWatchPage',
label: '削除依頼の対象ページをウォッチリストに追加する',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
// TwinkleConfig.xfdWatchDiscussion (string)
// The watchlist setting of the newly created XfD page (for those processes that create discussion pages for each nomination),
// or the list page for the other processes.
{
name: 'xfdWatchDiscussion',
label: '削除依頼ページをウォッチリストに追加する',
helptip: 'AfD・MfDでは個別の依頼ページ、TfD・CfD・RfD・FfDでは日付別の依頼一覧を対象にします。',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
// TwinkleConfig.xfdWatchList (string)
// The watchlist setting of the XfD list page, *if* the discussion is on a separate page.
{
name: 'xfdWatchList',
label: '日付別の依頼一覧や一覧ページをウォッチリストに追加する(AfD・MfD)',
helptip: 'AfD・MfDにのみ適用します。AfDでは日付別の依頼一覧、MfDでは総合一覧ページに依頼ページが読み込まれます。',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
// TwinkleConfig.xfdWatchUser (string)
// The watchlist setting of the user talk page if they receive a notification.
{
name: 'xfdWatchUser',
label: '通知した初版投稿者の会話ページをウォッチリストに追加する',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
// TwinkleConfig.xfdWatchRelated (string)
// The watchlist setting of the target of a redirect being nominated for RfD.
{
name: 'xfdWatchRelated',
label: '通知時にリダイレクト先のページをウォッチリストに追加する',
helptip: 'RfDで、リダイレクト先の会話ページに通知する場合にのみ適用します。',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
{
name: 'markXfdPagesAsPatrolled',
label: '記事の削除依頼(AfD)への提出時にページを巡回済み・確認済みにする(可能な場合)',
type: 'boolean'
}
]
},
{
title: '詳細設定(非表示)',
hidden: true,
preferences: [
// twinklerollback.js: defines how many revision to query maximum, maximum possible is 50, default is 50
{
name: 'revertMaxRevisions',
type: 'integer'
},
// twinklewarn.js: When using the autolevel select option, how many days makes a prior warning stale
// Huggle is three days ([[Special:Diff/918980316]] and [[Special:Diff/919417999]]) while ClueBotNG is two:
// https://github.com/DamianZaremba/cluebotng/blob/4958e25d6874cba01c75f11debd2e511fd5a2ce5/bot/action_functions.php#L62
{
name: 'autolevelStaleDays',
type: 'integer'
},
// How many pages should be queried by deprod and batchdelete/protect/undelete
{
name: 'batchMax',
type: 'integer',
adminOnly: true
},
// How many pages should be processed at a time by deprod and batchdelete/protect/undelete
{
name: 'batchChunks',
type: 'integer',
adminOnly: true
}
]
}
]; // end of Twinkle.config.sections
Twinkle.config.init = function twinkleconfigInit() {
// create the config page at User:Syunsyunminmin/Twinkle/Preferences
if ((mw.config.get('wgNamespaceNumber') === mw.config.get('wgNamespaceIds').user && mw.config.get('wgTitle') === 'Syunsyunminmin/Twinkle/Preferences') &&
mw.config.get('wgAction') === 'view') {
if (!document.getElementById('twinkle-config')) {
return; // maybe the page is misconfigured, or something - but any attempt to modify it will be pointless
}
// set style to nothing to prevent conflict with external css
document.getElementById('twinkle-config').removeAttribute('style');
document.getElementById('twinkle-config-titlebar').removeAttribute('style');
const contentdiv = document.getElementById('twinkle-config-content');
contentdiv.textContent = ''; // clear children
// let user know about possible conflict with skin js/common.js file
// (settings in that file will still work, but they will be overwritten by twinkleoptions.js settings)
if (window.TwinkleConfig || window.FriendlyConfig) {
const contentnotice = document.createElement('div');
contentnotice.className = 'plainlinks twinkle-ombox';
contentnotice.innerHTML =
'<div>' +
'<img alt="" src="https://upload.wikimedia.org/wikipedia/commons/3/38/Imbox_content.png" />' +
'</div>' +
'<div>' +
'<p><big><b>ここで設定を変更する前に、</b>個人用JavaScriptから古いTwinkleとFriendlyの設定を削除してください。</big></p>' +
'<p><a href="' + mw.util.getUrl('User:' + mw.config.get('wgUserName') + '/' + mw.config.get('skin') +
'.js', { action: 'edit' }) + '" target="_blank"><b>使用中の外装の個人用JavaScript</b></a>または<a href="' +
mw.util.getUrl('User:' + mw.config.get('wgUserName') + '/common.js', { action: 'edit'}) + '" target="_blank"><b>common.js</b></a>を編集し、<code>TwinkleConfig</code>と<code>FriendlyConfig</code>を参照する設定行を削除してください。</p>' +
'</div>';
contentdiv.appendChild(contentnotice);
}
// start a table of contents
const toctable = document.createElement('div');
toctable.className = 'toc';
toctable.style.marginLeft = '0.4em';
// create TOC title
const toctitle = document.createElement('div');
toctitle.id = 'toctitle';
var toch2 = document.createElement('h2');
toch2.textContent = '目次 ';
toctitle.appendChild(toch2);
// add TOC show/hide link
const toctoggle = document.createElement('span');
toctoggle.className = 'toctoggle';
toctoggle.appendChild(document.createTextNode('['));
const toctogglelink = document.createElement('a');
toctogglelink.className = 'internal';
toctogglelink.setAttribute('href', '#tw-tocshowhide');
toctogglelink.textContent = '非表示';
toctoggle.appendChild(toctogglelink);
toctoggle.appendChild(document.createTextNode(']'));
toctitle.appendChild(toctoggle);
toctable.appendChild(toctitle);
// create item container: this is what we add stuff to
const tocul = document.createElement('ul');
toctogglelink.addEventListener('click', () => {
const $tocul = $(tocul);
$tocul.toggle();
if ($tocul.find(':visible').length) {
toctogglelink.textContent = '非表示';
} else {
toctogglelink.textContent = '表示';
}
}, false);
toctable.appendChild(tocul);
contentdiv.appendChild(toctable);
const contentform = document.createElement('form');
contentform.setAttribute('action', 'javascript:void(0)'); // was #tw-save - changed to void(0) to work around Chrome issue
contentform.addEventListener('submit', Twinkle.config.save, true);
contentdiv.appendChild(contentform);
const container = document.createElement('table');
container.style.width = '100%';
contentform.appendChild(container);
$(Twinkle.config.sections).each((sectionkey, section) => {
if (section.hidden || (section.adminOnly && !Morebits.userIsSysop)) {
return true; // i.e. "continue" in this context
}
// add to TOC
const tocli = document.createElement('li');
tocli.className = 'toclevel-1';
const toca = document.createElement('a');
toca.setAttribute('href', '#' + section.module);
toca.appendChild(document.createTextNode(section.title));
tocli.appendChild(toca);
tocul.appendChild(tocli);
let row = document.createElement('tr');
let cell = document.createElement('td');
cell.setAttribute('colspan', '3');
const heading = document.createElement('h4');
heading.style.borderBottom = '1px solid gray';
heading.style.marginTop = '0.2em';
heading.id = section.module;
heading.appendChild(document.createTextNode(section.title));
cell.appendChild(heading);
row.appendChild(cell);
container.appendChild(row);
let rowcount = 1; // for row banding
// add each of the preferences to the form
$(section.preferences).each((prefkey, pref) => {
if (pref.adminOnly && !Morebits.userIsSysop) {
return true; // i.e. "continue" in this context
}
row = document.createElement('tr');
row.style.marginBottom = '0.2em';
// create odd row banding
if (rowcount++ % 2 === 0) {
row.style.backgroundColor = 'rgba(128, 128, 128, 0.1)';
}
cell = document.createElement('td');
var label, input, gotPref = Twinkle.getPref(pref.name), obsoletePref = Twinkle.getPref(pref.obsolete);
switch (pref.type) {
case 'boolean': // create a checkbox
cell.setAttribute('colspan', '2');
label = document.createElement('label');
input = document.createElement('input');
input.setAttribute('type', 'checkbox');
input.setAttribute('id', pref.name);
input.setAttribute('name', pref.name);
if (gotPref === true || obsoletePref === true) {
input.setAttribute('checked', 'checked');
}
label.appendChild(input);
label.appendChild(document.createTextNode(pref.label));
cell.appendChild(label);
break;
case 'string': // create an input box
case 'integer':
// add label to first column
cell.style.textAlign = 'right';
cell.style.paddingRight = '0.5em';
label = document.createElement('label');
label.setAttribute('for', pref.name);
label.appendChild(document.createTextNode(pref.label + ':'));
cell.appendChild(label);
row.appendChild(cell);
// add input box to second column
cell = document.createElement('td');
cell.style.paddingRight = '1em';
input = document.createElement('input');
input.setAttribute('type', 'text');
input.setAttribute('id', pref.name);
input.setAttribute('name', pref.name);
if (pref.type === 'integer') {
input.setAttribute('size', 6);
input.setAttribute('type', 'number');
input.setAttribute('step', '1'); // integers only
}
if (gotPref) {
input.setAttribute('value', gotPref);
}
if (obsoletePref) {
input.setAttribute('value', obsoletePref);
}
cell.appendChild(input);
break;
case 'enum': // create a combo box
// add label to first column
// note: duplicates the code above, under string/integer
cell.style.textAlign = 'right';
cell.style.paddingRight = '0.5em';
label = document.createElement('label');
label.setAttribute('for', pref.name);
label.appendChild(document.createTextNode(pref.label + ':'));
cell.appendChild(label);
row.appendChild(cell);
// add input box to second column
cell = document.createElement('td');
cell.style.paddingRight = '1em';
input = document.createElement('select');
input.setAttribute('id', pref.name);
input.setAttribute('name', pref.name);
$.each(pref.enumValues, (enumvalue, enumdisplay) => {
const option = document.createElement('option');
option.setAttribute('value', enumvalue);
if ((gotPref === enumvalue || obsoletePref === enumvalue) ||
// Hack to convert old boolean watchlist prefs
// to corresponding enums (added in v2.1)
((typeof gotPref === 'boolean' || typeof obsoletePref === 'boolean') &&
(((gotPref || obsoletePref) && enumvalue === 'yes') ||
(!(gotPref && obsoletePref) && enumvalue === 'no')))) {
option.setAttribute('selected', 'selected');
}
option.appendChild(document.createTextNode(enumdisplay));
input.appendChild(option);
});
cell.appendChild(input);
break;
case 'set': // create a set of check boxes
// add label first of all
cell.setAttribute('colspan', '2');
label = document.createElement('label'); // not really necessary to use a label element here, but we do it for consistency of styling
label.appendChild(document.createTextNode(pref.label + ':'));
cell.appendChild(label);
var checkdiv = document.createElement('div');
checkdiv.style.paddingLeft = '1em';
var worker = function(itemkey, itemvalue) {
const checklabel = document.createElement('label');
checklabel.style.marginRight = '0.7em';
checklabel.style.display = 'inline-block';
const check = document.createElement('input');
check.setAttribute('type', 'checkbox');
check.setAttribute('id', pref.name + '_' + itemkey);
check.setAttribute('name', pref.name + '_' + itemkey);
if ((gotPref && gotPref.includes(itemkey)) || (obsoletePref && obsoletePref.includes(itemkey))) {
check.setAttribute('checked', 'checked');
}
// cater for legacy integer array values for unlinkNamespaces (this can be removed a few years down the track...)
if (pref.name === 'unlinkNamespaces') {
if ((gotPref && gotPref.includes(parseInt(itemkey, 10))) || (obsoletePref && obsoletePref.includes(parseInt(itemkey, 10)))) {
check.setAttribute('checked', 'checked');
}
}
checklabel.appendChild(check);
checklabel.appendChild(document.createTextNode(itemvalue));
checkdiv.appendChild(checklabel);
};
if (pref.setDisplayOrder) {
// add check boxes according to the given display order
$.each(pref.setDisplayOrder, (itemkey, item) => {
worker(item, pref.setValues[item]);
});
} else {
// add check boxes according to the order it gets fed to us (probably strict alphabetical)
$.each(pref.setValues, worker);
}
cell.appendChild(checkdiv);
break;
case 'customList':
// add label to first column
cell.style.textAlign = 'right';
cell.style.paddingRight = '0.5em';
label = document.createElement('label');
label.setAttribute('for', pref.name);
label.appendChild(document.createTextNode(pref.label + ':'));
cell.appendChild(label);
row.appendChild(cell);
// add button to second column
cell = document.createElement('td');
cell.style.paddingRight = '1em';
var button = document.createElement('button');
button.setAttribute('id', pref.name);
button.setAttribute('name', pref.name);
button.setAttribute('type', 'button');
button.addEventListener('click', Twinkle.config.listDialog.display, false);
// use jQuery data on the button to store the current config value
$(button).data({
value: (obsoletePref) || gotPref,
pref: pref
});
button.appendChild(document.createTextNode('アイテムを編集'));
cell.appendChild(button);
break;
default:
alert('twinkleconfig: 設定 ' + pref.name + ' の不明なデータ型');
break;
}
row.appendChild(cell);
// add help tip
cell = document.createElement('td');
cell.className = 'twinkle-config-helptip';
if (pref.helptip) {
// convert mentions of templates in the helptip to clickable links
cell.innerHTML = pref.helptip.replace(/{{(.+?)}}/g,
'{{<a href="' + mw.util.getUrl('Template:') + '$1" target="_blank">$1</a>}}');
}
// add reset link (custom lists don't need this, as their config value isn't displayed on the form)
if (pref.type !== 'customList') {
const resetlink = document.createElement('a');
resetlink.setAttribute('href', '#tw-reset');
resetlink.setAttribute('id', 'twinkle-config-reset-' + pref.name);
resetlink.addEventListener('click', Twinkle.config.resetPrefLink, false);
resetlink.style.cssFloat = 'right';
resetlink.style.margin = '0 0.6em';
resetlink.appendChild(document.createTextNode('初期化'));
cell.appendChild(resetlink);
}
row.appendChild(cell);
container.appendChild(row);
return true;
});
return true;
});
const footerbox = document.createElement('div');
footerbox.setAttribute('id', 'twinkle-config-buttonpane');
const button = document.createElement('button');
button.setAttribute('id', 'twinkle-config-submit');
button.setAttribute('type', 'submit');
button.appendChild(document.createTextNode('変更を保存'));
footerbox.appendChild(button);
const footerspan = document.createElement('span');
footerspan.className = 'plainlinks';
footerspan.style.marginLeft = '2.4em';
footerspan.style.fontSize = '90%';
const footera = document.createElement('a');
footera.setAttribute('href', '#tw-reset-all');
footera.setAttribute('id', 'twinkle-config-resetall');
footera.addEventListener('click', Twinkle.config.resetAllPrefs, false);
footera.appendChild(document.createTextNode('デフォルトに戻す'));
footerspan.appendChild(footera);
footerbox.appendChild(footerspan);
contentform.appendChild(footerbox);
// since all the section headers exist now, we can try going to the requested anchor
if (window.location.hash) {
const loc = window.location.hash;
window.location.hash = '';
window.location.hash = loc;
}
} else if (mw.config.get('wgNamespaceNumber') === mw.config.get('wgNamespaceIds').user &&
mw.config.get('wgTitle').indexOf(mw.config.get('wgUserName')) === 0 &&
mw.config.get('wgPageName').slice(-3) === '.js') {
const box = document.createElement('div');
// Styled in twinkle.css
box.setAttribute('id', 'twinkle-config-headerbox');
let link;
const scriptPageName = mw.config.get('wgPageName').slice(
mw.config.get('wgPageName').lastIndexOf('/') + 1,
mw.config.get('wgPageName').lastIndexOf('.js')
);
if (scriptPageName === 'twinkleoptions') {
// place "why not try the preference panel" notice
box.setAttribute('class', 'config-twopt-box');
link = document.createElement('a');
link.setAttribute('href', mw.util.getUrl(mw.config.get('wgFormattedNamespaces')[mw.config.get('wgNamespaceIds').user] + ':Syunsyunminmin/Twinkle/Preferences'));
link.appendChild(document.createTextNode('Twinkle個人設定パネル'));
if (mw.config.get('wgArticleId') > 0) { // page exists
box.appendChild(document.createTextNode('このページにはTwinkleの設定が含まれています。設定は '));
box.appendChild(link);
box.appendChild(document.createTextNode('を使用するか、このページを編集することで変更できます。'));
} else { // page does not exist
box.appendChild(link);
box.appendChild(document.createTextNode('またはこのページを編集することで、Twinkleを個人の好みに合わせてカスタマイズできます。'));
}
$(box).insertAfter($('#contentSub'));
} else if (['monobook', 'vector', 'vector-2022', 'cologneblue', 'modern', 'timeless', 'minerva', 'common'].includes(scriptPageName)) {
// place "Looking for Twinkle options?" notice
box.setAttribute('class', 'config-userskin-box');
box.appendChild(document.createTextNode('Twinkle個人設定を設定したい場合、'));
link = document.createElement('a');
link.setAttribute('href', mw.util.getUrl(mw.config.get('wgFormattedNamespaces')[mw.config.get('wgNamespaceIds').user] + ':Syunsyunminmin/Twinkle/Preferences'));
link.appendChild(document.createTextNode('Twinkle個人設定パネル'));
box.appendChild(link);
box.appendChild(document.createTextNode('を使用して行えます。'));
$(box).insertAfter($('#contentSub'));
}
}
};
// custom list-related stuff
Twinkle.config.listDialog = {};
Twinkle.config.listDialog.addRow = function twinkleconfigListDialogAddRow($dlgtable, value, label) {
let $contenttr, $valueInput, $labelInput;
$dlgtable.append(
$contenttr = $('<tr>').append(
$('<td>').append(
$('<button>')
.attr('type', 'button')
.css('white-space', 'nowrap')
.on('click', () => {
$contenttr.remove();
})
.text('除去')
),
$('<td>').append(
$valueInput = $('<input>')
.attr('type', 'text')
.addClass('twinkle-config-customlist-value')
.css('width', '97%')
),
$('<td>').append(
$labelInput = $('<input>')
.attr('type', 'text')
.addClass('twinkle-config-customlist-label')
.css('width', '98%')
)
)
);
if (value) {
$valueInput.val(value);
}
if (label) {
$labelInput.val(label);
}
};
Twinkle.config.listDialog.display = function twinkleconfigListDialogDisplay(e) {
const $prefbutton = $(e.target);
const curvalue = $prefbutton.data('value');
const curpref = $prefbutton.data('pref');
const dialog = new Morebits.SimpleWindow(720, 400);
dialog.setTitle(curpref.label);
dialog.setScriptName('Twinkle個人設定');
let $dlgtbody;
dialog.setContent(
$('<div>').append(
$('<table>')
.addClass('wikitable')
.css({
margin: '1.4em 1em',
width: 'auto'
})
.append(
$dlgtbody = $('<tbody>').append(
// header row
$('<tr>').append(
$('<th>') // top-left cell
.css('width', '5%'),
$('<th>') // value column header
.css('width', '35%')
.text(curpref.customListValueTitle ? curpref.customListValueTitle : '値'),
$('<th>') // label column header
.css('width', '60%')
.text(curpref.customListLabelTitle ? curpref.customListLabelTitle : '表示名')
)
),
$('<tfoot>').append(
$('<tr>').append(
$('<td>')
.attr('colspan', '3')
.append(
$('<button>')
.text('追加')
.css('min-width', '8em')
.attr('type', 'button')
.on('click', () => {
Twinkle.config.listDialog.addRow($dlgtbody);
})
)
)
)
),
$('<button>')
.text('変更を保存')
.attr('type', 'submit') // so Morebits.SimpleWindow puts the button in the button pane
.on('click', () => {
Twinkle.config.listDialog.save($prefbutton, $dlgtbody);
dialog.close();
}),
$('<button>')
.text('初期化')
.attr('type', 'submit')
.on('click', () => {
Twinkle.config.listDialog.reset($prefbutton, $dlgtbody);
}),
$('<button>')
.text('キャンセル')
.attr('type', 'submit')
.on('click', () => {
dialog.close();
})
)[0]
);
// content rows
let gotRow = false;
$.each(curvalue, (k, v) => {
gotRow = true;
Twinkle.config.listDialog.addRow($dlgtbody, v.value, v.label);
});
// if there are no values present, add a blank row to start the user off
if (!gotRow) {
Twinkle.config.listDialog.addRow($dlgtbody);
}
dialog.display();
};
// Resets the data value, re-populates based on the new (default) value, then saves the
// old data value again (less surprising behaviour)
Twinkle.config.listDialog.reset = function twinkleconfigListDialogReset($button, $tbody) {
// reset value on button
const curpref = $button.data('pref');
const oldvalue = $button.data('value');
Twinkle.config.resetPref(curpref);
// reset form
$tbody.find('tr').slice(1).remove(); // all rows except the first (header) row
// add the new values
const curvalue = $button.data('value');
$.each(curvalue, (k, v) => {
Twinkle.config.listDialog.addRow($tbody, v.value, v.label);
});
// save the old value
$button.data('value', oldvalue);
};
Twinkle.config.listDialog.save = function twinkleconfigListDialogSave($button, $tbody) {
const result = [];
let current = {};
$tbody.find('input[type="text"]').each((inputkey, input) => {
if ($(input).hasClass('twinkle-config-customlist-value')) {
current = { value: input.value };
} else {
current.label = input.value;
// exclude totally empty rows
if (current.value || current.label) {
result.push(current);
}
}
});
$button.data('value', result);
};
// reset/restore defaults
Twinkle.config.resetPrefLink = function twinkleconfigResetPrefLink(e) {
const wantedpref = e.target.id.slice(21); // "twinkle-config-reset-" prefix is stripped
// search tactics
$(Twinkle.config.sections).each((sectionkey, section) => {
if (section.hidden || (section.adminOnly && !Morebits.userIsSysop)) {
return true; // continue: skip impossibilities
}
let foundit = false;
$(section.preferences).each((prefkey, pref) => {
if (pref.name !== wantedpref) {
return true; // continue
}
Twinkle.config.resetPref(pref);
foundit = true;
return false; // break
});
if (foundit) {
return false; // break
}
});
return false; // stop link from scrolling page
};
Twinkle.config.resetPref = function twinkleconfigResetPref(pref) {
switch (pref.type) {
case 'boolean':
document.getElementById(pref.name).checked = Twinkle.defaultConfig[pref.name];
break;
case 'string':
case 'integer':
case 'enum':
document.getElementById(pref.name).value = Twinkle.defaultConfig[pref.name];
break;
case 'set':
$.each(pref.setValues, (itemkey) => {
if (document.getElementById(pref.name + '_' + itemkey)) {
document.getElementById(pref.name + '_' + itemkey).checked = Twinkle.defaultConfig[pref.name].includes(itemkey);
}
});
break;
case 'customList':
$(document.getElementById(pref.name)).data('value', Twinkle.defaultConfig[pref.name]);
break;
default:
alert('twinkleconfig: 設定 ' + pref.name + ' の不明なデータ型');
break;
}
};
Twinkle.config.resetAllPrefs = function twinkleconfigResetAllPrefs() {
// no confirmation message - the user can just refresh/close the page to abort
$(Twinkle.config.sections).each((sectionkey, section) => {
if (section.hidden || (section.adminOnly && !Morebits.userIsSysop)) {
return true; // continue: skip impossibilities
}
$(section.preferences).each((prefkey, pref) => {
if (!pref.adminOnly || Morebits.userIsSysop) {
Twinkle.config.resetPref(pref);
}
});
return true;
});
return false; // stop link from scrolling page
};
Twinkle.config.save = function twinkleconfigSave(e) {
Morebits.Status.init(document.getElementById('twinkle-config-content'));
var userjs = mw.config.get('wgFormattedNamespaces')[mw.config.get('wgNamespaceIds').user] + ':' + mw.config.get('wgUserName') + '/twinkleoptions.js';
var wikipedia_page = new Morebits.wiki.Page(userjs, userjs + 'に設定を保存');
wikipedia_page.setCallbackParameters(e.target);
wikipedia_page.load(Twinkle.config.writePrefs);
return false;
};
Twinkle.config.writePrefs = function twinkleconfigWritePrefs(pageobj) {
const form = pageobj.getCallbackParameters();
// this is the object which gets serialized into JSON; only
// preferences that this script knows about are kept
const newConfig = {optionsVersion: 2.1};
// a comparison function is needed later on
// it is just enough for our purposes (i.e. comparing strings, numbers, booleans,
// arrays of strings, and arrays of { value, label })
// and it is not very robust: e.g. compare([2], ["2"]) === true, and
// compare({}, {}) === false, but it's good enough for our purposes here
const compare = function(a, b) {
if (Array.isArray(a)) {
if (a.length !== b.length) {
return false;
}
const asort = a.sort(), bsort = b.sort();
for (let i = 0; asort[i]; ++i) {
// comparison of the two properties of custom lists
if ((typeof asort[i] === 'object') && (asort[i].label !== bsort[i].label ||
asort[i].value !== bsort[i].value)) {
return false;
} else if (asort[i].toString() !== bsort[i].toString()) {
return false;
}
}
return true;
}
return a === b;
};
$(Twinkle.config.sections).each((sectionkey, section) => {
if (section.adminOnly && !Morebits.userIsSysop) {
return; // i.e. "continue" in this context
}
// reach each of the preferences from the form
$(section.preferences).each((prefkey, pref) => {
let userValue; // = undefined
// only read form values for those prefs that have them
if (!pref.adminOnly || Morebits.userIsSysop) {
if (!section.hidden) {
switch (pref.type) {
case 'boolean': // read from the checkbox
userValue = form[pref.name].checked;
break;
case 'string': // read from the input box or combo box
case 'enum':
userValue = form[pref.name].value;
break;
case 'integer': // read from the input box
userValue = parseInt(form[pref.name].value, 10);
if (isNaN(userValue)) {
Morebits.Status.warn('保存', pref.name + 'に対して指定した値 (' + pref.value + ') は無効です。保存は続行しますが、無効なデータ値は飛ばされます。');
userValue = null;
}
break;
case 'set': // read from the set of check boxes
userValue = [];
if (pref.setDisplayOrder) {
// read only those keys specified in the display order
$.each(pref.setDisplayOrder, (itemkey, item) => {
if (form[pref.name + '_' + item].checked) {
userValue.push(item);
}
});
} else {
// read all the keys in the list of values
$.each(pref.setValues, (itemkey) => {
if (form[pref.name + '_' + itemkey].checked) {
userValue.push(itemkey);
}
});
}
break;
case 'customList': // read from the jQuery data stored on the button object
userValue = $(form[pref.name]).data('value');
break;
default:
alert('twinkleconfig: 設定 ' + pref.name + ' の不明なデータ型');
break;
}
} else if (Twinkle.prefs) {
// Retain the hidden preferences that may have customised by the user from twinkleoptions.js
// undefined if not set
userValue = Twinkle.prefs[pref.name];
}
}
// only save those preferences that are *different* from the default
if (userValue !== undefined && !compare(userValue, Twinkle.defaultConfig[pref.name])) {
newConfig[pref.name] = userValue;
}
});
});
var text =
'// twinkleoptions.js: Twinkleの個人設定ファイル\n' +
'//\n' +
'// 注意: Twinkleの設定を変更する最も簡単な方法は、\n' +
'// Twinkle設定パネルを使用することです。設定パネルは[[' + Morebits.pageNameNorm + ']]にあります。\n' +
'//\n' +
'// このファイルは自動的に生成されます。\n' +
'// (有効なJavaScriptの方法で構成パラメーターを変更する以外に)行った変更は、\n' +
'// 次にTwinkle設定パネルで「保存」をクリックしたときに上書きされます。\n' +
'// このファイルを直接変更する場合は、必ず正しいJavaScriptを使用してください。\n' +
// eslint-disable-next-line no-useless-concat
'// <no' + 'wiki>\n' +
'\n' +
'window.Twinkle.prefs = ';
text += JSON.stringify(newConfig, null, 2);
text +=
';\n' +
'\n' +
// eslint-disable-next-line no-useless-concat
'// </no' + 'wiki>\n' +
'// twinkleoptions.js の終わり\n';
pageobj.setPageText(text);
pageobj.setEditSummary('Twinkle個人設定を保存: [[:' + Morebits.pageNameNorm + ']]からの自動編集');
pageobj.setCreateOption('recreate');
pageobj.save(Twinkle.config.saveSuccess);
};
Twinkle.config.saveSuccess = function twinkleconfigSaveSuccess(pageobj) {
pageobj.getStatusElement().info('成功');
const noticebox = document.createElement('div');
noticebox.className = 'cdx-message cdx-message--success';
noticebox.style.fontSize = '100%';
noticebox.style.marginTop = '2em';
noticebox.innerHTML = '<p><b>Twinkle個人設定は保存されました。</b></p><p>変更を確認するには、<b>ブラウザのキャッシュを完全にクリアする</b>必要があります。 (手順については <a href="' + mw.util.getUrl('WP:BYPASS') + '" title="WP:BYPASS">WP:BYPASS</a> 参照</p>';
Morebits.Status.root.appendChild(noticebox);
var noticeclear = document.createElement('br');
noticeclear.style.clear = 'both';
Morebits.Status.root.appendChild(noticeclear);
};
Twinkle.addInitCallback(Twinkle.config.init);
}());
// </nowiki>
t590fzkju2f8uxi9mh3tx98kwl18jxp
769554
769545
2026-10-22T01:11:02Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at ff10bbf0c6a2cbb87decbc1d177b18916f75af77: 表記修正; キャッシュ処理
769554
javascript
text/javascript
// <nowiki>
(function() {
/*
****************************************
*** twinkleconfig.js: Preferences module
****************************************
* Mode of invocation: Adds configuration form to User:Syunsyunminmin/Twinkle/Preferences,
and adds an ad box to the top of user subpages belonging to the
currently logged-in user which end in '.js'
* Active on: What I just said. Yeah.
*/
Twinkle.config = {};
Twinkle.config.watchlistEnums = {
yes: 'ウォッチリストに追加 (無期限)',
no: 'ウォッチリストに追加しない',
default: 'サイトの設定に従う',
'1 week': '1週間ウォッチ',
'1 month': '1ヶ月ウォッチ',
'3 months': '3ヶ月ウォッチ',
'6 months': '6ヶ月ウォッチ'
};
Twinkle.config.commonSets = {
csdCriteria: {
db: 'その他 ({{即時削除|その他}})',
g1: 'G1', g2: 'G2', g3: 'G3', g4: 'G4', g5: 'G5', g6: 'G6', g8: 'G8', g9: 'G9', g10: 'G10',
a1: 'A1',
u1: 'U1', u2: 'U2', u3: 'U3',
'f1-2': 'F1-2', 'f1-3': 'F1-3', 'f1-4': 'F1-4', 'f1-5': 'F1-5', 'f1-6': 'F1-6',
f3: 'F3', f5: 'F5', f6: 'F6', f7: 'F7', f8: 'F8', f9: 'F9',
c1: 'C1', c3: 'C3', c6: 'C6',
'r1-1': 'R1-1', 'r1-2': 'R1-2', 'r1-3': 'R1-3', 'r1-4': 'R1-4',
'r2-1': 'R2-1', 'r2-2': 'R2-2', 'r2-3': 'R2-3', 'r2-5': 'R2-5', 'r2-6': 'R2-6',
r4: 'R4'
},
csdCriteriaDisplayOrder: [
'db',
'g1', 'g2', 'g3', 'g4', 'g5', 'g6', 'g8', 'g9', 'g10',
'a1',
'u1', 'u2', 'u3',
'f1-2', 'f1-3', 'f1-4', 'f1-5', 'f1-6', 'f3', 'f5', 'f6', 'f7', 'f8', 'f9',
'c1', 'c3', 'c6',
'r1-1', 'r1-2', 'r1-3', 'r1-4', 'r2-1', 'r2-2', 'r2-3', 'r2-5', 'r2-6', 'r4'
],
csdAndImageDeletionCriteria: {
db: 'その他 ({{即時削除|その他}})',
g1: 'G1', g2: 'G2', g3: 'G3', g4: 'G4', g5: 'G5', g6: 'G6', g8: 'G8', g9: 'G9', g10: 'G10',
a1: 'A1',
u1: 'U1', u2: 'U2', u3: 'U3',
'f1-2': 'F1-2', 'f1-3': 'F1-3', 'f1-4': 'F1-4', 'f1-5': 'F1-5', 'f1-6': 'F1-6',
f3: 'F3', f5: 'F5', f6: 'F6', f7: 'F7', f8: 'F8', f9: 'F9',
c1: 'C1', c3: 'C3', c6: 'C6',
'r1-1': 'R1-1', 'r1-2': 'R1-2', 'r1-3': 'R1-3', 'r1-4': 'R1-4',
'r2-1': 'R2-1', 'r2-2': 'R2-2', 'r2-3': 'R2-3', 'r2-5': 'R2-5', 'r2-6': 'R2-6',
r4: 'R4'
},
csdAndImageDeletionCriteriaDisplayOrder: [
'db',
'g1', 'g2', 'g3', 'g4', 'g5', 'g6', 'g8', 'g9', 'g10',
'a1',
'u1', 'u2', 'u3',
'f1-2', 'f1-3', 'f1-4', 'f1-5', 'f1-6', 'f3', 'f5', 'f6', 'f7', 'f8', 'f9',
'c1', 'c3', 'c6',
'r1-1', 'r1-2', 'r1-3', 'r1-4', 'r2-1', 'r2-2', 'r2-3', 'r2-5', 'r2-6', 'r4'
],
namespacesNoSpecial: {
0: '標準',
1: 'ノート (標準)',
2: '利用者',
3: '利用者‐会話',
4: 'Wikipedia',
5: 'Wikipedia‐ノート',
6: 'ファイル',
7: 'ファイル‐ノート',
8: 'MediaWiki',
9: 'MediaWiki‐ノート',
10: 'Template',
11: 'Template‐ノート',
12: 'Help',
13: 'Help‐ノート',
14: 'Category',
15: 'Category‐ノート',
100: 'Portal',
101: 'Portal‐ノート',
710: 'TimedText',
711: 'TimedText‐ノート',
828: 'Module',
829: 'Module‐ノート'
}
};
Twinkle.config.commonSets.csdCriteriaDisplayOrder = Object.keys(Twinkle.config.commonSets.csdCriteria);
Twinkle.config.commonSets.csdAndImageDeletionCriteriaDisplayOrder = Object.keys(Twinkle.config.commonSets.csdAndImageDeletionCriteria);
/**
* Section entry format:
*
* {
* title: <human-readable section title>,
* module: <name of the associated module, used to link to sections>,
* adminOnly: <true for admin-only sections>,
* hidden: <true for advanced preferences that rarely need to be changed - they can still be modified by manually editing twinkleoptions.js>,
* preferences: [
* {
* name: <TwinkleConfig property name>,
* label: <human-readable short description - used as a form label>,
* helptip: <(optional) human-readable text (using valid HTML) that complements the description, like limits, warnings, etc.>
* adminOnly: <true for admin-only preferences>,
* type: <string|boolean|integer|enum|set|customList> (customList stores an array of JSON objects { value, label }),
* enumValues: <for type = "enum": a JSON object where the keys are the internal names and the values are human-readable strings>,
* setValues: <for type = "set": a JSON object where the keys are the internal names and the values are human-readable strings>,
* setDisplayOrder: <(optional) for type = "set": an array containing the keys of setValues (as strings) in the order that they are displayed>,
* customListValueTitle: <for type = "customList": the heading for the left "value" column in the custom list editor>,
* customListLabelTitle: <for type = "customList": the heading for the right "label" column in the custom list editor>
* },
* . . .
* ]
* },
* . . .
*
*/
Twinkle.config.sections = [
{
title: '全般',
module: 'general',
preferences: [
// TwinkleConfig.userTalkPageMode may take arguments:
// 'window': open a new window, remember the opened window
// 'tab': opens in a new tab, if possible.
// 'blank': force open in a new window, even if such a window exists
{
name: 'userTalkPageMode',
label: '利用者会話ページを開く時は次のページで開く',
type: 'enum',
enumValues: { window: 'ウィンドウ内で他の利用者会話ページを置き換える', tab: '新しいタブ', blank: '完全に新しいウィンドウ' }
},
// TwinkleConfig.dialogLargeFont (boolean)
{
name: 'dialogLargeFont',
label: 'Twinkleダイアログで大きな文字を使用する',
type: 'boolean'
},
// Twinkle.config.disabledModules (array)
{
name: 'disabledModules',
label: '選択したTwinkleモジュールをオフにする',
helptip: 'チェックを入れた機能は無効になります。再び有効にするにはチェックを外してください。<br />既定でチェックが入っているものは準備中の機能です。無効にしていても、関連する設定項目は表示されます。',
type: 'set',
setValues: { arv: 'ARV', warn: 'Warn', talkback: 'Talkback', speedy: 'CSD', xfd: 'XfD', image: 'DI', protect: 'RPP', tag: 'Tag', diff: 'Diff', unlink: 'Unlink', rollback: 'Rollback' }
},
// Twinkle.config.disabledSysopModules (array)
{
name: 'disabledSysopModules',
label: '選択した管理者専用モジュールをオフにする',
helptip: 'ここで選択したものは使用できないので、注意してください。再び有効にするにはチェックを外してください。',
adminOnly: true,
type: 'set',
setValues: { block: 'Block', batchdelete: 'D-batch', batchprotect: 'P-batch', batchundelete: 'Und-batch' }
}
]
},
{
title: 'ARV: 荒らしの報告',
module: 'arv',
preferences: [
{
name: 'spiWatchReport',
label: '多重アカウントの調査依頼ページをウォッチリストに追加する',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
}
]
},
{
title: 'Block: 利用者のブロック',
module: 'block',
adminOnly: true,
preferences: [
// TwinkleConfig.defaultToBlock64 (boolean)
// Whether to default to just blocking the /64 on or off
{
name: 'defaultToBlock64',
label: 'IPv6アドレスでは、既定で/64の範囲をブロックする',
type: 'boolean'
},
// TwinkleConfig.defaultToPartialBlocks (boolean)
// Whether to default partial blocks on or off
{
name: 'defaultToPartialBlocks',
label: 'ブロックメニューを開いた際、既定で部分ブロックを選択する',
helptip: '利用者がすでにブロックされている場合は、現在のブロックの種類が優先されます。',
type: 'boolean'
},
// TwinkleConfig.blankTalkpageOnIndefBlock (boolean)
// if true, blank the talk page when issuing an indef block notice (per [[WP:UWUL#Indefinitely blocked users]])
{
name: 'blankTalkpageOnIndefBlock',
label: '無期限ブロックの通知時に利用者会話ページを白紙化する',
helptip: '詳しくは<a href="' + mw.util.getUrl('Wikipedia:WikiProject_User_warnings/Usage_and_layout#Indefinitely_blocked_users') + '">WP:UWUL</a>を参照してください。',
type: 'boolean'
}
]
},
{
title: 'DI: 画像削除',
module: 'image',
preferences: [
// TwinkleConfig.notifyUserOnDeli (boolean)
// If the user should be notified after placing a file deletion tag
{
name: 'notifyUserOnDeli',
label: '既定で"アップロード者に通知"にチェックを入れる',
type: 'boolean'
},
// TwinkleConfig.deliWatchPage (string)
// The watchlist setting of the page tagged for deletion.
{
name: 'deliWatchPage',
label: 'タグ付け時にファイルページをウォッチリストに追加',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
// TwinkleConfig.deliWatchUser (string)
// The watchlist setting of the user talk page if a notification is placed.
{
name: 'deliWatchUser',
label: '通知時に、アップロード者の会話ページをウォッチリストに追加する',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
}
]
},
{
title: (Morebits.userIsSysop ? 'PP' : 'RPP') + ': ページの保護',
module: 'protect',
preferences: [
{
name: 'watchRequestedPages',
label: '保護依頼時にページをウォッチリストに追加する',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
{
name: 'watchPPTaggedPages',
label: '保護テンプレートでページをタグ付けする際、ウォッチリストにページを追加する',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
{
name: 'watchProtectedPages',
label: '保護する際にページをウォッチリストに追加する',
helptip: 'ページ保護後にタグ付けも行う場合は、そちらが優先されます。',
adminOnly: true,
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
}
]
},
{
title: 'Rollback: 差し戻しと巻き戻し', // twinklerollback module
module: 'rollback',
preferences: [
// TwinkleConfig.autoMenuAfterRollback (bool)
// Option to automatically open the warning menu if the user talk page is opened post-reversion
{
name: 'autoMenuAfterRollback',
label: 'Twinkle差し戻し後、利用者会話ページでTwinkle警告メニューを自動的に開く',
helptip: '以下の該当するボックスにチェックが入っている場合のみ動作します。',
type: 'boolean'
},
// TwinkleConfig.openTalkPage (array)
// What types of actions that should result in opening of talk page
{
name: 'openTalkPage',
label: '以下の種類の差し戻し後に、会話ページを開く。',
type: 'set',
setValues: { norm: '通常の差し戻し', vand: '荒らしの差し戻し' }
},
// TwinkleConfig.openTalkPageOnAutoRevert (bool)
// Defines if talk page should be opened when calling revert from contribs or recent changes pages. If set to true, openTalkPage defines then if talk page will be opened.
{
name: 'openTalkPageOnAutoRevert',
label: '投稿記録や最近の更新から差し戻しを実行した際に、会話ページを開く',
helptip: 'これが有効になっている場合、前の設定で必要なオプションが有効になっていなければ機能しません。',
type: 'boolean'
},
// TwinkleConfig.rollbackInPlace (bool)
//
{
name: 'rollbackInPlace',
label: '投稿記録や最近の更新から差し戻しする際、ページを更新しない。',
helptip: 'これが有効になっている場合、Twinkleは差し戻し後に投稿記録や最近の更新フィードを再読み込みしないため、一度に複数の編集を差し戻すことができます。',
type: 'boolean'
},
// TwinkleConfig.markRevertedPagesAsMinor (array)
// What types of actions that should result in marking edit as minor
{
name: 'markRevertedPagesAsMinor',
label: '以下の種類の差し戻しは細部の編集として行います',
type: 'set',
setValues: { norm: '通常の差し戻し', vand: '荒らしの差し戻し', torev: '版の復元' }
},
// TwinkleConfig.watchRevertedPages (array)
// What types of actions that should result in forced addition to watchlist
{
name: 'watchRevertedPages',
label: '以下の種類の差し戻しの際、ページをウォッチリストに追加する',
type: 'set',
setValues: { norm: '通常の差し戻し', vand: '荒らしの差し戻し', torev: '版の復元' }
},
// TwinkleConfig.watchRevertedExpiry
// If any of the above items are selected, whether to expire the watch
{
name: 'watchRevertedExpiry',
label: 'ページを差し戻した際、ウォッチする期間は',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
// TwinkleConfig.offerReasonOnNormalRevert (boolean)
// If to offer a prompt for extra summary reason for normal reverts, default to true
{
name: 'offerReasonOnNormalRevert',
label: '通常の差し戻しに理由の入力を求めるプロンプト',
helptip: '"通常の"差し戻しは [差し戻し] リンクから実行される差し戻しです。',
type: 'boolean'
},
{
name: 'confirmOnRollback',
label: '差し戻す前に確認を求める (すべての機器)',
helptip: 'ペンやタッチデバイスを使う人、慢性的に優柔不断な人向け。',
type: 'boolean'
},
{
name: 'confirmOnMobileRollback',
label: '差し戻す前に確認を求める (携帯機器のみ)',
helptip: '携帯機器での誤動作を防ぐ。',
type: 'boolean'
},
// TwinkleConfig.showRollbackLinks (array)
// Where Twinkle should show rollback links:
// diff, others, mine, contribs, history, recent
// Note from TTO: |contribs| seems to be equal to |others| + |mine|, i.e. redundant, so I left it out heres
{
name: 'showRollbackLinks',
label: 'これらのページで差し戻しリンクを表示',
type: 'set',
setValues: { diff: '差分ページ', others: '他の利用者の投稿記録ページ', mine: '自分の投稿記録ページ', recent: '最近の更新や関連ページの更新の特別ページ', history: '履歴ページ' }
},
{
name: 'SummaryOnVandalRevert',
label: '荒らしの差し戻しの際の要約欄をカスタムする',
helptip: 'このボックスに要約を入力します。',
type: 'string',
obsolete: 'vandusersummary'
}
]
},
{
title: 'CSD: 即時削除',
module: 'speedy',
preferences: [
{
name: 'speedySelectionStyle',
label: 'ページをタグ付け/削除するタイミング',
type: 'enum',
enumValues: { buttonClick: '送信ボタンをクリックしたとき', radioClick: 'オプションをクリックするとすぐ' }
},
// TwinkleConfig.watchSpeedyPages (array)
// Whether to add speedy tagged or deleted pages to watchlist
{
name: 'watchSpeedyPages',
label: 'これらの基準を使用する場合は、ウォッチリストにページを追加する',
type: 'set',
setValues: Twinkle.config.commonSets.csdCriteria,
setDisplayOrder: Twinkle.config.commonSets.csdCriteriaDisplayOrder
},
// TwinkleConfig.watchSpeedyExpiry
// If any of the above items are selected, whether to expire the watch
{
name: 'watchSpeedyExpiry',
label: 'ページをタグ付けする場合、それをウォッチする期間',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
// TwinkleConfig.markSpeedyPagesAsPatrolled (boolean)
// If, when applying speedy template to page, to mark the page as triaged/patrolled (if the page was reached from NewPages)
{
name: 'markSpeedyPagesAsPatrolled',
label: 'タグ付けの際、ページを巡回済みとしてマークする。(可能な場合)',
helptip: 'これは最善の慣行に反するおそれがあるので、おそらくチェックすべきではないです。',
type: 'boolean'
},
// TwinkleConfig.promptForSpeedyDeletionSummary (array of strings)
{
name: 'promptForSpeedyDeletionSummary',
label: 'これらの基準で削除する場合、削除要約の編集を行う',
adminOnly: true,
type: 'set',
setValues: Twinkle.config.commonSets.csdAndImageDeletionCriteria,
setDisplayOrder: Twinkle.config.commonSets.csdAndImageDeletionCriteriaDisplayOrder
},
// TwinkleConfig.deleteTalkPageOnDelete (boolean)
// If talk page if exists should also be deleted (CSD G8) when spedying a page (admin only)
{
name: 'deleteTalkPageOnDelete',
label: '既定で「トークページも削除する」にチェックを入れる',
adminOnly: true,
type: 'boolean'
},
{
name: 'deleteRedirectsOnDelete',
label: '既定で「リダイレクトも削除」にチェックを入れる',
adminOnly: true,
type: 'boolean'
},
// TwinkleConfig.deleteSysopDefaultToDelete (boolean)
// Make the CSD screen default to "delete" instead of "tag" (admin only)
{
name: 'deleteSysopDefaultToDelete',
label: '既定でタグ付けではなく即時削除を行う',
helptip: 'CSDタグが既に存在する場合、Twinkleは常に「削除」モードが既定となります。',
adminOnly: true,
type: 'boolean'
},
// TwinkleConfig.speedyWindowWidth (integer)
// Defines the width of the Twinkle SD window in pixels
{
name: 'speedyWindowWidth',
label: '即時削除ウィンドウの幅(ピクセル)',
type: 'integer'
},
// TwinkleConfig.speedyWindowWidth (integer)
// Defines the width of the Twinkle SD window in pixels
{
name: 'speedyWindowHeight',
label: '即時削除ウィンドウの高さ(ピクセル)',
helptip: '大きなモニターをお持ちの場合は、これを増やしてください。',
type: 'integer'
},
{
name: 'logSpeedyNominations',
label: 'すべてのCSDへの指定記録を利用者ページに保持します',
helptip: '管理者以外は削除された投稿にアクセスできないため、利用者ページのログは、Twinkleを使用してCSDに指定したすべてのページを追跡するための優れた方法を提供します。 DIを使用してタグ付けされたファイルもこのログに追加されます。',
type: 'boolean'
},
{
name: 'speedyLogPageName',
label: 'この利用者サブページでCSDユーザースペースログを保持します',
helptip: 'このボックスにサブページ名を入力します。 CSDログは利用者:' + mw.config.get('wgUserName') + '/"サブページ名"にあります。 CSD利用者ページへの記録をオンにした場合にのみ機能します。',
type: 'string'
},
{
name: 'noLogOnSpeedyNomination',
label: 'これらの基準でタグ付けする場合は、利用者サブページの記録項目を作成しない',
type: 'set',
setValues: Twinkle.config.commonSets.csdAndImageDeletionCriteria,
setDisplayOrder: Twinkle.config.commonSets.csdAndImageDeletionCriteriaDisplayOrder
}
]
},
{
title: 'Tag: メンテナンス用テンプレートの付与',
module: 'tag',
preferences: [
{
name: 'watchTaggedVenues',
label: '次の種類のページにテンプレートを付与した際、ウォッチリストに追加する',
type: 'set',
setValues: { articles: '記事', drafts: '下書き', redirects: 'リダイレクト', files: 'ファイル' }
},
{
name: 'watchTaggedPages',
label: 'テンプレートを付与したページのウォッチ期間',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
{
name: 'watchMergeDiscussions',
label: '統合提案を開始した際、会話ページをウォッチリストに追加する',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
{
name: 'markTaggedPagesAsMinor',
label: 'テンプレートの付与を細部の編集として扱う',
type: 'boolean'
},
{
name: 'markTaggedPagesAsPatrolled',
label: '既定で「ページを巡回済み・確認済みにする」にチェックを入れる',
type: 'boolean'
},
{
name: 'tagArticleSortOrder',
label: '記事用テンプレートの既定の表示順',
type: 'enum',
enumValues: { cat: '分類別', alpha: 'アルファベット順' }
},
{
name: 'customTagList',
label: '追加で表示する記事・下書きのメンテナンス用テンプレート',
helptip: 'テンプレート一覧の末尾に選択肢を追加します。Twinkleにまだ登録されていないメンテナンス用テンプレートなどを指定できます。',
type: 'customList',
customListValueTitle: 'テンプレート名(波括弧なし)',
customListLabelTitle: 'テンプレート付与画面に表示する名前'
},
{
name: 'customFileTagList',
label: '追加で表示するファイルのメンテナンス用テンプレート',
helptip: 'ファイルに付与できるテンプレートを追加します。',
type: 'customList',
customListValueTitle: 'テンプレート名(波括弧なし)',
customListLabelTitle: 'テンプレート付与画面に表示する名前'
},
{
name: 'customRedirectTagList',
label: '追加で表示するリダイレクト分類用テンプレート',
helptip: 'リダイレクトに付与できるテンプレートを追加します。',
type: 'customList',
customListValueTitle: 'テンプレート名(波括弧なし)',
customListLabelTitle: 'テンプレート付与画面に表示する名前'
}
]
},
{
title: 'Talkback: 返信通知',
module: 'talkback',
preferences: [
{
name: 'markTalkbackAsMinor',
label: '返信通知を細部の編集として扱う',
type: 'boolean'
},
{
name: 'insertTalkbackSignature',
label: '返信通知に署名を挿入する',
type: 'boolean'
},
{
name: 'talkbackHeading',
label: '返信通知と会話ページへの案内に使用する節見出し',
tooltip: 'ウィキテキストの見出しに使う等号(==)は入力しないでください。',
type: 'string'
},
{
name: 'mailHeading',
label: 'メール送信通知に使用する節見出し',
tooltip: 'ウィキテキストの見出しに使う等号(==)は入力しないでください。',
type: 'string'
}
]
},
{
title: 'Unlink: リンク解除',
module: 'unlink',
preferences: [
// TwinkleConfig.unlinkNamespaces (array)
// In what namespaces unlink should happen, default in 0 (article), 10 (template), 100 (portal), and 118 (draft)
{
name: 'unlinkNamespaces',
label: '次の名前空間のページをリンク解除の対象にする',
helptip: '会話ページの過去ログ内のリンクまで解除されるおそれがあるため、会話用の名前空間は選択しないでください。',
type: 'set',
setValues: Twinkle.config.commonSets.namespacesNoSpecial
}
]
},
{
title: 'Warn: 利用者への警告',
module: 'warn',
preferences: [
// TwinkleConfig.defaultWarningGroup (int)
// Which level warning should be the default selected group, default is 1
{
name: 'defaultWarningGroup',
label: '既定の警告レベル',
type: 'enum',
enumValues: {
1: 'レベル1',
2: 'レベル2',
3: 'レベル3',
4: 'レベル4',
5: '即時の最終警告(レベル4im)',
6: '個別の問題に関する通知',
7: '個別の問題に関する警告',
// 8 was used for block templates before #260
9: '独自の警告',
10: 'すべての警告テンプレート',
11: 'レベルを自動選択(1~4)'
}
},
// TwinkleConfig.combinedSingletMenus (boolean)
// if true, show one menu with both single-issue notices and warnings instead of two separately
{
name: 'combinedSingletMenus',
label: '個別の問題に関する通知と警告のメニューをまとめる',
helptip: '有効にすると、既定の警告レベルで個別の問題に関する通知または警告を選んでいる場合に、統合したメニューを既定で表示します。',
type: 'boolean'
},
// TwinkleConfig.watchWarnings (string)
// Watchlist setting for the page which has been dispatched an warning or notice
{
name: 'watchWarnings',
label: '通知した利用者の会話ページをウォッチリストに追加する',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
// TwinkleConfig.oldSelect (boolean)
// if true, use the native select menu rather the select2-based one
{
name: 'oldSelect',
label: '検索機能のない従来の選択メニューを使用する',
type: 'boolean'
},
{
name: 'customWarningList',
label: '追加で表示する警告テンプレート',
helptip: 'テンプレートや利用者サブページを追加できます。追加した項目は警告画面の「独自の警告」に表示されます。',
type: 'customList',
customListValueTitle: 'テンプレート名(波括弧なし)',
customListLabelTitle: '警告一覧に表示する名前(編集要約にも使用)'
}
]
},
{
title: 'XfD: 削除依頼',
module: 'xfd',
preferences: [
{
name: 'logXfdNominations',
label: '削除依頼(XfD)を提出したページを利用者サブページに記録する',
helptip: 'Twinkleで削除依頼を提出したページを、利用者サブページの記録から確認できます。',
type: 'boolean'
},
{
name: 'xfdLogPageName',
label: '削除依頼の記録を保存する利用者サブページ名',
helptip: 'サブページ名を入力してください。記録先は「利用者:<i>利用者名</i>/<i>サブページ名</i>」です。削除依頼の記録を有効にしている場合にのみ機能します。',
type: 'string'
},
{
name: 'noLogOnXfdNomination',
label: '次の手続きに提出する場合は利用者サブページに記録しない',
type: 'set',
setValues: { afd: '記事の削除依頼 (AfD)', tfd: 'テンプレートの削除・統合依頼 (TfD)', ffd: 'ファイルの削除依頼 (FfD)', cfd: 'カテゴリの削除・変更依頼 (CfD)', cfds: 'カテゴリの即時変更依頼 (CfD/S)', mfd: 'その他のページの削除依頼 (MfD)', rfd: 'リダイレクトの削除・転送先変更依頼 (RfD)', rm: '改名依頼 (RM)' }
},
// TwinkleConfig.xfdWatchPage (string)
// The watchlist setting of the page being nominated for XfD.
{
name: 'xfdWatchPage',
label: '削除依頼の対象ページをウォッチリストに追加する',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
// TwinkleConfig.xfdWatchDiscussion (string)
// The watchlist setting of the newly created XfD page (for those processes that create discussion pages for each nomination),
// or the list page for the other processes.
{
name: 'xfdWatchDiscussion',
label: '削除依頼ページをウォッチリストに追加する',
helptip: 'AfD・MfDでは個別の依頼ページ、TfD・CfD・RfD・FfDでは日付別の依頼一覧を対象にします。',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
// TwinkleConfig.xfdWatchList (string)
// The watchlist setting of the XfD list page, *if* the discussion is on a separate page.
{
name: 'xfdWatchList',
label: '日付別の依頼一覧や一覧ページをウォッチリストに追加する(AfD・MfD)',
helptip: 'AfD・MfDにのみ適用します。AfDでは日付別の依頼一覧、MfDでは総合一覧ページに依頼ページが読み込まれます。',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
// TwinkleConfig.xfdWatchUser (string)
// The watchlist setting of the user talk page if they receive a notification.
{
name: 'xfdWatchUser',
label: '通知した初版投稿者の会話ページをウォッチリストに追加する',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
// TwinkleConfig.xfdWatchRelated (string)
// The watchlist setting of the target of a redirect being nominated for RfD.
{
name: 'xfdWatchRelated',
label: '通知時にリダイレクト先のページをウォッチリストに追加する',
helptip: 'RfDで、リダイレクト先の会話ページに通知する場合にのみ適用します。',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
{
name: 'markXfdPagesAsPatrolled',
label: '記事の削除依頼(AfD)への提出時にページを巡回済み・確認済みにする(可能な場合)',
type: 'boolean'
}
]
},
{
title: '詳細設定(非表示)',
hidden: true,
preferences: [
// twinklerollback.js: defines how many revision to query maximum, maximum possible is 50, default is 50
{
name: 'revertMaxRevisions',
type: 'integer'
},
// twinklewarn.js: When using the autolevel select option, how many days makes a prior warning stale
// Huggle is three days ([[Special:Diff/918980316]] and [[Special:Diff/919417999]]) while ClueBotNG is two:
// https://github.com/DamianZaremba/cluebotng/blob/4958e25d6874cba01c75f11debd2e511fd5a2ce5/bot/action_functions.php#L62
{
name: 'autolevelStaleDays',
type: 'integer'
},
// How many pages should be queried by deprod and batchdelete/protect/undelete
{
name: 'batchMax',
type: 'integer',
adminOnly: true
},
// How many pages should be processed at a time by deprod and batchdelete/protect/undelete
{
name: 'batchChunks',
type: 'integer',
adminOnly: true
}
]
}
]; // end of Twinkle.config.sections
Twinkle.config.init = function twinkleconfigInit() {
// create the config page at User:Syunsyunminmin/Twinkle/Preferences
if ((mw.config.get('wgNamespaceNumber') === mw.config.get('wgNamespaceIds').user && mw.config.get('wgTitle') === 'Syunsyunminmin/Twinkle/Preferences') &&
mw.config.get('wgAction') === 'view') {
if (!document.getElementById('twinkle-config')) {
return; // maybe the page is misconfigured, or something - but any attempt to modify it will be pointless
}
// set style to nothing to prevent conflict with external css
document.getElementById('twinkle-config').removeAttribute('style');
document.getElementById('twinkle-config-titlebar').removeAttribute('style');
const contentdiv = document.getElementById('twinkle-config-content');
contentdiv.textContent = ''; // clear children
// let user know about possible conflict with skin js/common.js file
// (settings in that file will still work, but they will be overwritten by twinkleoptions.js settings)
if (window.TwinkleConfig || window.FriendlyConfig) {
const contentnotice = document.createElement('div');
contentnotice.className = 'plainlinks twinkle-ombox';
contentnotice.innerHTML =
'<div>' +
'<img alt="" src="https://upload.wikimedia.org/wikipedia/commons/3/38/Imbox_content.png" />' +
'</div>' +
'<div>' +
'<p><big><b>ここで設定を変更する前に、</b>個人用JavaScriptから古いTwinkleとFriendlyの設定を削除してください。</big></p>' +
'<p><a href="' + mw.util.getUrl('User:' + mw.config.get('wgUserName') + '/' + mw.config.get('skin') +
'.js', { action: 'edit' }) + '" target="_blank"><b>使用中の外装の個人用JavaScript</b></a>または<a href="' +
mw.util.getUrl('User:' + mw.config.get('wgUserName') + '/common.js', { action: 'edit'}) + '" target="_blank"><b>common.js</b></a>を編集し、<code>TwinkleConfig</code>と<code>FriendlyConfig</code>を参照する設定行を削除してください。</p>' +
'</div>';
contentdiv.appendChild(contentnotice);
}
// start a table of contents
const toctable = document.createElement('div');
toctable.className = 'toc';
toctable.style.marginLeft = '0.4em';
// create TOC title
const toctitle = document.createElement('div');
toctitle.id = 'toctitle';
var toch2 = document.createElement('h2');
toch2.textContent = '目次 ';
toctitle.appendChild(toch2);
// add TOC show/hide link
const toctoggle = document.createElement('span');
toctoggle.className = 'toctoggle';
toctoggle.appendChild(document.createTextNode('['));
const toctogglelink = document.createElement('a');
toctogglelink.className = 'internal';
toctogglelink.setAttribute('href', '#tw-tocshowhide');
toctogglelink.textContent = '非表示';
toctoggle.appendChild(toctogglelink);
toctoggle.appendChild(document.createTextNode(']'));
toctitle.appendChild(toctoggle);
toctable.appendChild(toctitle);
// create item container: this is what we add stuff to
const tocul = document.createElement('ul');
toctogglelink.addEventListener('click', () => {
const $tocul = $(tocul);
$tocul.toggle();
if ($tocul.find(':visible').length) {
toctogglelink.textContent = '非表示';
} else {
toctogglelink.textContent = '表示';
}
}, false);
toctable.appendChild(tocul);
contentdiv.appendChild(toctable);
const contentform = document.createElement('form');
contentform.setAttribute('action', 'javascript:void(0)'); // was #tw-save - changed to void(0) to work around Chrome issue
// Use loaded preferences instead of restoring earlier browser form values.
contentform.setAttribute('autocomplete', 'off');
contentform.addEventListener('submit', Twinkle.config.save, true);
contentdiv.appendChild(contentform);
const container = document.createElement('table');
container.style.width = '100%';
contentform.appendChild(container);
$(Twinkle.config.sections).each((sectionkey, section) => {
if (section.hidden || (section.adminOnly && !Morebits.userIsSysop)) {
return true; // i.e. "continue" in this context
}
// add to TOC
const tocli = document.createElement('li');
tocli.className = 'toclevel-1';
const toca = document.createElement('a');
toca.setAttribute('href', '#' + section.module);
toca.appendChild(document.createTextNode(section.title));
tocli.appendChild(toca);
tocul.appendChild(tocli);
let row = document.createElement('tr');
let cell = document.createElement('td');
cell.setAttribute('colspan', '3');
const heading = document.createElement('h4');
heading.style.borderBottom = '1px solid gray';
heading.style.marginTop = '0.2em';
heading.id = section.module;
heading.appendChild(document.createTextNode(section.title));
cell.appendChild(heading);
row.appendChild(cell);
container.appendChild(row);
let rowcount = 1; // for row banding
// add each of the preferences to the form
$(section.preferences).each((prefkey, pref) => {
if (pref.adminOnly && !Morebits.userIsSysop) {
return true; // i.e. "continue" in this context
}
row = document.createElement('tr');
row.style.marginBottom = '0.2em';
// create odd row banding
if (rowcount++ % 2 === 0) {
row.style.backgroundColor = 'rgba(128, 128, 128, 0.1)';
}
cell = document.createElement('td');
var label, input, gotPref = Twinkle.getPref(pref.name), obsoletePref = Twinkle.getPref(pref.obsolete);
switch (pref.type) {
case 'boolean': // create a checkbox
cell.setAttribute('colspan', '2');
label = document.createElement('label');
input = document.createElement('input');
input.setAttribute('type', 'checkbox');
input.setAttribute('id', pref.name);
input.setAttribute('name', pref.name);
if (gotPref === true || obsoletePref === true) {
input.setAttribute('checked', 'checked');
}
label.appendChild(input);
label.appendChild(document.createTextNode(pref.label));
cell.appendChild(label);
break;
case 'string': // create an input box
case 'integer':
// add label to first column
cell.style.textAlign = 'right';
cell.style.paddingRight = '0.5em';
label = document.createElement('label');
label.setAttribute('for', pref.name);
label.appendChild(document.createTextNode(pref.label + ':'));
cell.appendChild(label);
row.appendChild(cell);
// add input box to second column
cell = document.createElement('td');
cell.style.paddingRight = '1em';
input = document.createElement('input');
input.setAttribute('type', 'text');
input.setAttribute('id', pref.name);
input.setAttribute('name', pref.name);
if (pref.type === 'integer') {
input.setAttribute('size', 6);
input.setAttribute('type', 'number');
input.setAttribute('step', '1'); // integers only
}
if (gotPref) {
input.setAttribute('value', gotPref);
}
if (obsoletePref) {
input.setAttribute('value', obsoletePref);
}
cell.appendChild(input);
break;
case 'enum': // create a combo box
// add label to first column
// note: duplicates the code above, under string/integer
cell.style.textAlign = 'right';
cell.style.paddingRight = '0.5em';
label = document.createElement('label');
label.setAttribute('for', pref.name);
label.appendChild(document.createTextNode(pref.label + ':'));
cell.appendChild(label);
row.appendChild(cell);
// add input box to second column
cell = document.createElement('td');
cell.style.paddingRight = '1em';
input = document.createElement('select');
input.setAttribute('id', pref.name);
input.setAttribute('name', pref.name);
$.each(pref.enumValues, (enumvalue, enumdisplay) => {
const option = document.createElement('option');
option.setAttribute('value', enumvalue);
if ((gotPref === enumvalue || obsoletePref === enumvalue) ||
// Hack to convert old boolean watchlist prefs
// to corresponding enums (added in v2.1)
((typeof gotPref === 'boolean' || typeof obsoletePref === 'boolean') &&
(((gotPref || obsoletePref) && enumvalue === 'yes') ||
(!(gotPref && obsoletePref) && enumvalue === 'no')))) {
option.setAttribute('selected', 'selected');
}
option.appendChild(document.createTextNode(enumdisplay));
input.appendChild(option);
});
cell.appendChild(input);
break;
case 'set': // create a set of check boxes
// add label first of all
cell.setAttribute('colspan', '2');
label = document.createElement('label'); // not really necessary to use a label element here, but we do it for consistency of styling
label.appendChild(document.createTextNode(pref.label + ':'));
cell.appendChild(label);
var checkdiv = document.createElement('div');
checkdiv.style.paddingLeft = '1em';
var worker = function(itemkey, itemvalue) {
const checklabel = document.createElement('label');
checklabel.style.marginRight = '0.7em';
checklabel.style.display = 'inline-block';
const check = document.createElement('input');
check.setAttribute('type', 'checkbox');
check.setAttribute('id', pref.name + '_' + itemkey);
check.setAttribute('name', pref.name + '_' + itemkey);
if ((gotPref && gotPref.includes(itemkey)) || (obsoletePref && obsoletePref.includes(itemkey))) {
check.setAttribute('checked', 'checked');
}
// cater for legacy integer array values for unlinkNamespaces (this can be removed a few years down the track...)
if (pref.name === 'unlinkNamespaces') {
if ((gotPref && gotPref.includes(parseInt(itemkey, 10))) || (obsoletePref && obsoletePref.includes(parseInt(itemkey, 10)))) {
check.setAttribute('checked', 'checked');
}
}
checklabel.appendChild(check);
checklabel.appendChild(document.createTextNode(itemvalue));
checkdiv.appendChild(checklabel);
};
if (pref.setDisplayOrder) {
// add check boxes according to the given display order
$.each(pref.setDisplayOrder, (itemkey, item) => {
worker(item, pref.setValues[item]);
});
} else {
// add check boxes according to the order it gets fed to us (probably strict alphabetical)
$.each(pref.setValues, worker);
}
cell.appendChild(checkdiv);
break;
case 'customList':
// add label to first column
cell.style.textAlign = 'right';
cell.style.paddingRight = '0.5em';
label = document.createElement('label');
label.setAttribute('for', pref.name);
label.appendChild(document.createTextNode(pref.label + ':'));
cell.appendChild(label);
row.appendChild(cell);
// add button to second column
cell = document.createElement('td');
cell.style.paddingRight = '1em';
var button = document.createElement('button');
button.setAttribute('id', pref.name);
button.setAttribute('name', pref.name);
button.setAttribute('type', 'button');
button.addEventListener('click', Twinkle.config.listDialog.display, false);
// use jQuery data on the button to store the current config value
$(button).data({
value: (obsoletePref) || gotPref,
pref: pref
});
button.appendChild(document.createTextNode('アイテムを編集'));
cell.appendChild(button);
break;
default:
alert('twinkleconfig: 設定 ' + pref.name + ' の不明なデータ型');
break;
}
row.appendChild(cell);
// add help tip
cell = document.createElement('td');
cell.className = 'twinkle-config-helptip';
if (pref.helptip) {
// convert mentions of templates in the helptip to clickable links
cell.innerHTML = pref.helptip.replace(/{{(.+?)}}/g,
'{{<a href="' + mw.util.getUrl('Template:') + '$1" target="_blank">$1</a>}}');
}
// add reset link (custom lists don't need this, as their config value isn't displayed on the form)
if (pref.type !== 'customList') {
const resetlink = document.createElement('a');
resetlink.setAttribute('href', '#tw-reset');
resetlink.setAttribute('id', 'twinkle-config-reset-' + pref.name);
resetlink.addEventListener('click', Twinkle.config.resetPrefLink, false);
resetlink.style.cssFloat = 'right';
resetlink.style.margin = '0 0.6em';
resetlink.appendChild(document.createTextNode('初期化'));
cell.appendChild(resetlink);
}
row.appendChild(cell);
container.appendChild(row);
return true;
});
return true;
});
const footerbox = document.createElement('div');
footerbox.setAttribute('id', 'twinkle-config-buttonpane');
const button = document.createElement('button');
button.setAttribute('id', 'twinkle-config-submit');
button.setAttribute('type', 'submit');
button.appendChild(document.createTextNode('変更を保存'));
footerbox.appendChild(button);
const footerspan = document.createElement('span');
footerspan.className = 'plainlinks';
footerspan.style.marginLeft = '2.4em';
footerspan.style.fontSize = '90%';
const footera = document.createElement('a');
footera.setAttribute('href', '#tw-reset-all');
footera.setAttribute('id', 'twinkle-config-resetall');
footera.addEventListener('click', Twinkle.config.resetAllPrefs, false);
footera.appendChild(document.createTextNode('デフォルトに戻す'));
footerspan.appendChild(footera);
footerbox.appendChild(footerspan);
contentform.appendChild(footerbox);
// since all the section headers exist now, we can try going to the requested anchor
if (window.location.hash) {
const loc = window.location.hash;
window.location.hash = '';
window.location.hash = loc;
}
} else if (mw.config.get('wgNamespaceNumber') === mw.config.get('wgNamespaceIds').user &&
mw.config.get('wgTitle').indexOf(mw.config.get('wgUserName')) === 0 &&
mw.config.get('wgPageName').slice(-3) === '.js') {
const box = document.createElement('div');
// Styled in twinkle.css
box.setAttribute('id', 'twinkle-config-headerbox');
let link;
const scriptPageName = mw.config.get('wgPageName').slice(
mw.config.get('wgPageName').lastIndexOf('/') + 1,
mw.config.get('wgPageName').lastIndexOf('.js')
);
if (scriptPageName === 'twinkleoptions') {
// place "why not try the preference panel" notice
box.setAttribute('class', 'config-twopt-box');
link = document.createElement('a');
link.setAttribute('href', mw.util.getUrl(mw.config.get('wgFormattedNamespaces')[mw.config.get('wgNamespaceIds').user] + ':Syunsyunminmin/Twinkle/Preferences'));
link.appendChild(document.createTextNode('Twinkle個人設定パネル'));
if (mw.config.get('wgArticleId') > 0) { // page exists
box.appendChild(document.createTextNode('このページにはTwinkleの設定が含まれています。設定は '));
box.appendChild(link);
box.appendChild(document.createTextNode('を使用するか、このページを編集することで変更できます。'));
} else { // page does not exist
box.appendChild(link);
box.appendChild(document.createTextNode('またはこのページを編集することで、Twinkleを個人の好みに合わせてカスタマイズできます。'));
}
$(box).insertAfter($('#contentSub'));
} else if (['monobook', 'vector', 'vector-2022', 'cologneblue', 'modern', 'timeless', 'minerva', 'common'].includes(scriptPageName)) {
// place "Looking for Twinkle options?" notice
box.setAttribute('class', 'config-userskin-box');
box.appendChild(document.createTextNode('Twinkle個人設定を設定したい場合、'));
link = document.createElement('a');
link.setAttribute('href', mw.util.getUrl(mw.config.get('wgFormattedNamespaces')[mw.config.get('wgNamespaceIds').user] + ':Syunsyunminmin/Twinkle/Preferences'));
link.appendChild(document.createTextNode('Twinkle個人設定パネル'));
box.appendChild(link);
box.appendChild(document.createTextNode('を使用して行えます。'));
$(box).insertAfter($('#contentSub'));
}
}
};
// custom list-related stuff
Twinkle.config.listDialog = {};
Twinkle.config.listDialog.addRow = function twinkleconfigListDialogAddRow($dlgtable, value, label) {
let $contenttr, $valueInput, $labelInput;
$dlgtable.append(
$contenttr = $('<tr>').append(
$('<td>').append(
$('<button>')
.attr('type', 'button')
.css('white-space', 'nowrap')
.on('click', () => {
$contenttr.remove();
})
.text('除去')
),
$('<td>').append(
$valueInput = $('<input>')
.attr('type', 'text')
.addClass('twinkle-config-customlist-value')
.css('width', '97%')
),
$('<td>').append(
$labelInput = $('<input>')
.attr('type', 'text')
.addClass('twinkle-config-customlist-label')
.css('width', '98%')
)
)
);
if (value) {
$valueInput.val(value);
}
if (label) {
$labelInput.val(label);
}
};
Twinkle.config.listDialog.display = function twinkleconfigListDialogDisplay(e) {
const $prefbutton = $(e.target);
const curvalue = $prefbutton.data('value');
const curpref = $prefbutton.data('pref');
const dialog = new Morebits.SimpleWindow(720, 400);
dialog.setTitle(curpref.label);
dialog.setScriptName('Twinkle個人設定');
let $dlgtbody;
dialog.setContent(
$('<div>').append(
$('<table>')
.addClass('wikitable')
.css({
margin: '1.4em 1em',
width: 'auto'
})
.append(
$dlgtbody = $('<tbody>').append(
// header row
$('<tr>').append(
$('<th>') // top-left cell
.css('width', '5%'),
$('<th>') // value column header
.css('width', '35%')
.text(curpref.customListValueTitle ? curpref.customListValueTitle : '値'),
$('<th>') // label column header
.css('width', '60%')
.text(curpref.customListLabelTitle ? curpref.customListLabelTitle : '表示名')
)
),
$('<tfoot>').append(
$('<tr>').append(
$('<td>')
.attr('colspan', '3')
.append(
$('<button>')
.text('追加')
.css('min-width', '8em')
.attr('type', 'button')
.on('click', () => {
Twinkle.config.listDialog.addRow($dlgtbody);
})
)
)
)
),
$('<button>')
.text('変更を保存')
.attr('type', 'submit') // so Morebits.SimpleWindow puts the button in the button pane
.on('click', () => {
Twinkle.config.listDialog.save($prefbutton, $dlgtbody);
dialog.close();
}),
$('<button>')
.text('初期化')
.attr('type', 'submit')
.on('click', () => {
Twinkle.config.listDialog.reset($prefbutton, $dlgtbody);
}),
$('<button>')
.text('キャンセル')
.attr('type', 'submit')
.on('click', () => {
dialog.close();
})
)[0]
);
// content rows
let gotRow = false;
$.each(curvalue, (k, v) => {
gotRow = true;
Twinkle.config.listDialog.addRow($dlgtbody, v.value, v.label);
});
// if there are no values present, add a blank row to start the user off
if (!gotRow) {
Twinkle.config.listDialog.addRow($dlgtbody);
}
dialog.display();
};
// Resets the data value, re-populates based on the new (default) value, then saves the
// old data value again (less surprising behaviour)
Twinkle.config.listDialog.reset = function twinkleconfigListDialogReset($button, $tbody) {
// reset value on button
const curpref = $button.data('pref');
const oldvalue = $button.data('value');
Twinkle.config.resetPref(curpref);
// reset form
$tbody.find('tr').slice(1).remove(); // all rows except the first (header) row
// add the new values
const curvalue = $button.data('value');
$.each(curvalue, (k, v) => {
Twinkle.config.listDialog.addRow($tbody, v.value, v.label);
});
// save the old value
$button.data('value', oldvalue);
};
Twinkle.config.listDialog.save = function twinkleconfigListDialogSave($button, $tbody) {
const result = [];
let current = {};
$tbody.find('input[type="text"]').each((inputkey, input) => {
if ($(input).hasClass('twinkle-config-customlist-value')) {
current = { value: input.value };
} else {
current.label = input.value;
// exclude totally empty rows
if (current.value || current.label) {
result.push(current);
}
}
});
$button.data('value', result);
};
// reset/restore defaults
Twinkle.config.resetPrefLink = function twinkleconfigResetPrefLink(e) {
const wantedpref = e.target.id.slice(21); // "twinkle-config-reset-" prefix is stripped
// search tactics
$(Twinkle.config.sections).each((sectionkey, section) => {
if (section.hidden || (section.adminOnly && !Morebits.userIsSysop)) {
return true; // continue: skip impossibilities
}
let foundit = false;
$(section.preferences).each((prefkey, pref) => {
if (pref.name !== wantedpref) {
return true; // continue
}
Twinkle.config.resetPref(pref);
foundit = true;
return false; // break
});
if (foundit) {
return false; // break
}
});
return false; // stop link from scrolling page
};
Twinkle.config.resetPref = function twinkleconfigResetPref(pref) {
switch (pref.type) {
case 'boolean':
document.getElementById(pref.name).checked = Twinkle.defaultConfig[pref.name];
break;
case 'string':
case 'integer':
case 'enum':
document.getElementById(pref.name).value = Twinkle.defaultConfig[pref.name];
break;
case 'set':
$.each(pref.setValues, (itemkey) => {
if (document.getElementById(pref.name + '_' + itemkey)) {
document.getElementById(pref.name + '_' + itemkey).checked = Twinkle.defaultConfig[pref.name].includes(itemkey);
}
});
break;
case 'customList':
$(document.getElementById(pref.name)).data('value', Twinkle.defaultConfig[pref.name]);
break;
default:
alert('twinkleconfig: 設定 ' + pref.name + ' の不明なデータ型');
break;
}
};
Twinkle.config.resetAllPrefs = function twinkleconfigResetAllPrefs() {
// no confirmation message - the user can just refresh/close the page to abort
$(Twinkle.config.sections).each((sectionkey, section) => {
if (section.hidden || (section.adminOnly && !Morebits.userIsSysop)) {
return true; // continue: skip impossibilities
}
$(section.preferences).each((prefkey, pref) => {
if (!pref.adminOnly || Morebits.userIsSysop) {
Twinkle.config.resetPref(pref);
}
});
return true;
});
return false; // stop link from scrolling page
};
Twinkle.config.save = function twinkleconfigSave(e) {
Morebits.Status.init(document.getElementById('twinkle-config-content'));
var userjs = mw.config.get('wgFormattedNamespaces')[mw.config.get('wgNamespaceIds').user] + ':' + mw.config.get('wgUserName') + '/twinkleoptions.js';
var wikipedia_page = new Morebits.wiki.Page(userjs, userjs + 'に設定を保存');
wikipedia_page.setCallbackParameters(e.target);
wikipedia_page.load(Twinkle.config.writePrefs);
return false;
};
Twinkle.config.writePrefs = function twinkleconfigWritePrefs(pageobj) {
const form = pageobj.getCallbackParameters();
// this is the object which gets serialized into JSON; only
// preferences that this script knows about are kept
const newConfig = {optionsVersion: 2.1};
// a comparison function is needed later on
// it is just enough for our purposes (i.e. comparing strings, numbers, booleans,
// arrays of strings, and arrays of { value, label })
// and it is not very robust: e.g. compare([2], ["2"]) === true, and
// compare({}, {}) === false, but it's good enough for our purposes here
const compare = function(a, b) {
if (Array.isArray(a)) {
if (a.length !== b.length) {
return false;
}
const asort = a.sort(), bsort = b.sort();
for (let i = 0; asort[i]; ++i) {
// comparison of the two properties of custom lists
if ((typeof asort[i] === 'object') && (asort[i].label !== bsort[i].label ||
asort[i].value !== bsort[i].value)) {
return false;
} else if (asort[i].toString() !== bsort[i].toString()) {
return false;
}
}
return true;
}
return a === b;
};
$(Twinkle.config.sections).each((sectionkey, section) => {
if (section.adminOnly && !Morebits.userIsSysop) {
return; // i.e. "continue" in this context
}
// reach each of the preferences from the form
$(section.preferences).each((prefkey, pref) => {
let userValue; // = undefined
// only read form values for those prefs that have them
if (!pref.adminOnly || Morebits.userIsSysop) {
if (!section.hidden) {
switch (pref.type) {
case 'boolean': // read from the checkbox
userValue = form[pref.name].checked;
break;
case 'string': // read from the input box or combo box
case 'enum':
userValue = form[pref.name].value;
break;
case 'integer': // read from the input box
userValue = parseInt(form[pref.name].value, 10);
if (isNaN(userValue)) {
Morebits.Status.warn('保存', pref.name + 'に対して指定した値 (' + pref.value + ') は無効です。保存は続行しますが、無効なデータ値は飛ばされます。');
userValue = null;
}
break;
case 'set': // read from the set of check boxes
userValue = [];
if (pref.setDisplayOrder) {
// read only those keys specified in the display order
$.each(pref.setDisplayOrder, (itemkey, item) => {
if (form[pref.name + '_' + item].checked) {
userValue.push(item);
}
});
} else {
// read all the keys in the list of values
$.each(pref.setValues, (itemkey) => {
if (form[pref.name + '_' + itemkey].checked) {
userValue.push(itemkey);
}
});
}
break;
case 'customList': // read from the jQuery data stored on the button object
userValue = $(form[pref.name]).data('value');
break;
default:
alert('twinkleconfig: 設定 ' + pref.name + ' の不明なデータ型');
break;
}
} else if (Twinkle.prefs) {
// Retain the hidden preferences that may have customised by the user from twinkleoptions.js
// undefined if not set
userValue = Twinkle.prefs[pref.name];
}
}
// only save those preferences that are *different* from the default
if (userValue !== undefined && !compare(userValue, Twinkle.defaultConfig[pref.name])) {
newConfig[pref.name] = userValue;
}
});
});
var text =
'// twinkleoptions.js: Twinkleの個人設定ファイル\n' +
'//\n' +
'// 注意: Twinkleの設定を変更する最も簡単な方法は、\n' +
'// Twinkle設定パネルを使用することです。設定パネルは[[' + Morebits.pageNameNorm + ']]にあります。\n' +
'//\n' +
'// このファイルは自動的に生成されます。\n' +
'// (有効なJavaScriptの方法で構成パラメーターを変更する以外に)行った変更は、\n' +
'// 次にTwinkle設定パネルで「保存」をクリックしたときに上書きされます。\n' +
'// このファイルを直接変更する場合は、必ず正しいJavaScriptを使用してください。\n' +
// eslint-disable-next-line no-useless-concat
'// <no' + 'wiki>\n' +
'\n' +
'window.Twinkle.prefs = ';
text += JSON.stringify(newConfig, null, 2);
text +=
';\n' +
'\n' +
// eslint-disable-next-line no-useless-concat
'// </no' + 'wiki>\n' +
'// twinkleoptions.js の終わり\n';
pageobj.setPageText(text);
pageobj.setEditSummary('Twinkle個人設定を保存: [[:' + Morebits.pageNameNorm + ']]からの自動編集');
pageobj.setCreateOption('recreate');
pageobj.save(Twinkle.config.saveSuccess);
};
Twinkle.config.saveSuccess = function twinkleconfigSaveSuccess(pageobj) {
pageobj.getStatusElement().info('成功');
const noticebox = document.createElement('div');
noticebox.className = 'cdx-message cdx-message--success';
noticebox.style.fontSize = '100%';
noticebox.style.marginTop = '2em';
noticebox.innerHTML = '<p><b>Twinkle個人設定は保存されました。</b></p><p>変更を確認するには、<b>ブラウザのキャッシュを完全にクリアする</b>必要があります。 (手順については <a href="' + mw.util.getUrl('WP:BYPASS') + '" title="WP:BYPASS">WP:BYPASS</a> 参照</p>';
Morebits.Status.root.appendChild(noticebox);
var noticeclear = document.createElement('br');
noticeclear.style.clear = 'both';
Morebits.Status.root.appendChild(noticeclear);
};
Twinkle.addInitCallback(Twinkle.config.init);
}());
// </nowiki>
au8pmanyu15clmtr1i2cru602gj2ww2
769556
769554
2026-10-22T01:29:38Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 4ef623086ddf6bd20e55878b084e156c84ef06f2: 校正
769556
javascript
text/javascript
// <nowiki>
(function() {
/*
****************************************
*** twinkleconfig.js: Preferences module
****************************************
* Mode of invocation: Adds configuration form to User:Syunsyunminmin/Twinkle/Preferences,
and adds an ad box to the top of user subpages belonging to the
currently logged-in user which end in '.js'
* Active on: What I just said. Yeah.
*/
Twinkle.config = {};
Twinkle.config.watchlistEnums = {
yes: 'ウォッチリストに追加 (無期限)',
no: 'ウォッチリストに追加しない',
default: 'サイトの設定に従う',
'1 week': '1週間ウォッチ',
'1 month': '1ヶ月ウォッチ',
'3 months': '3ヶ月ウォッチ',
'6 months': '6ヶ月ウォッチ'
};
Twinkle.config.commonSets = {
csdCriteria: {
db: 'その他 ({{即時削除|その他}})',
g1: 'G1', g2: 'G2', g3: 'G3', g4: 'G4', g5: 'G5', g6: 'G6', g8: 'G8', g9: 'G9', g10: 'G10',
a1: 'A1',
u1: 'U1', u2: 'U2', u3: 'U3',
'f1-2': 'F1-2', 'f1-3': 'F1-3', 'f1-4': 'F1-4', 'f1-5': 'F1-5', 'f1-6': 'F1-6',
f3: 'F3', f5: 'F5', f6: 'F6', f7: 'F7', f8: 'F8', f9: 'F9',
c1: 'C1', c3: 'C3', c6: 'C6',
'r1-1': 'R1-1', 'r1-2': 'R1-2', 'r1-3': 'R1-3', 'r1-4': 'R1-4',
'r2-1': 'R2-1', 'r2-2': 'R2-2', 'r2-3': 'R2-3', 'r2-5': 'R2-5', 'r2-6': 'R2-6',
r4: 'R4'
},
csdCriteriaDisplayOrder: [
'db',
'g1', 'g2', 'g3', 'g4', 'g5', 'g6', 'g8', 'g9', 'g10',
'a1',
'u1', 'u2', 'u3',
'f1-2', 'f1-3', 'f1-4', 'f1-5', 'f1-6', 'f3', 'f5', 'f6', 'f7', 'f8', 'f9',
'c1', 'c3', 'c6',
'r1-1', 'r1-2', 'r1-3', 'r1-4', 'r2-1', 'r2-2', 'r2-3', 'r2-5', 'r2-6', 'r4'
],
csdAndImageDeletionCriteria: {
db: 'その他 ({{即時削除|その他}})',
g1: 'G1', g2: 'G2', g3: 'G3', g4: 'G4', g5: 'G5', g6: 'G6', g8: 'G8', g9: 'G9', g10: 'G10',
a1: 'A1',
u1: 'U1', u2: 'U2', u3: 'U3',
'f1-2': 'F1-2', 'f1-3': 'F1-3', 'f1-4': 'F1-4', 'f1-5': 'F1-5', 'f1-6': 'F1-6',
f3: 'F3', f5: 'F5', f6: 'F6', f7: 'F7', f8: 'F8', f9: 'F9',
c1: 'C1', c3: 'C3', c6: 'C6',
'r1-1': 'R1-1', 'r1-2': 'R1-2', 'r1-3': 'R1-3', 'r1-4': 'R1-4',
'r2-1': 'R2-1', 'r2-2': 'R2-2', 'r2-3': 'R2-3', 'r2-5': 'R2-5', 'r2-6': 'R2-6',
r4: 'R4'
},
csdAndImageDeletionCriteriaDisplayOrder: [
'db',
'g1', 'g2', 'g3', 'g4', 'g5', 'g6', 'g8', 'g9', 'g10',
'a1',
'u1', 'u2', 'u3',
'f1-2', 'f1-3', 'f1-4', 'f1-5', 'f1-6', 'f3', 'f5', 'f6', 'f7', 'f8', 'f9',
'c1', 'c3', 'c6',
'r1-1', 'r1-2', 'r1-3', 'r1-4', 'r2-1', 'r2-2', 'r2-3', 'r2-5', 'r2-6', 'r4'
],
namespacesNoSpecial: {
0: '標準',
1: 'ノート (標準)',
2: '利用者',
3: '利用者‐会話',
4: 'Wikipedia',
5: 'Wikipedia‐ノート',
6: 'ファイル',
7: 'ファイル‐ノート',
8: 'MediaWiki',
9: 'MediaWiki‐ノート',
10: 'Template',
11: 'Template‐ノート',
12: 'Help',
13: 'Help‐ノート',
14: 'Category',
15: 'Category‐ノート',
100: 'Portal',
101: 'Portal‐ノート',
710: 'TimedText',
711: 'TimedText‐ノート',
828: 'Module',
829: 'Module‐ノート'
}
};
Twinkle.config.commonSets.csdCriteriaDisplayOrder = Object.keys(Twinkle.config.commonSets.csdCriteria);
Twinkle.config.commonSets.csdAndImageDeletionCriteriaDisplayOrder = Object.keys(Twinkle.config.commonSets.csdAndImageDeletionCriteria);
/**
* Section entry format:
*
* {
* title: <human-readable section title>,
* module: <name of the associated module, used to link to sections>,
* adminOnly: <true for admin-only sections>,
* hidden: <true for advanced preferences that rarely need to be changed - they can still be modified by manually editing twinkleoptions.js>,
* preferences: [
* {
* name: <TwinkleConfig property name>,
* label: <human-readable short description - used as a form label>,
* helptip: <(optional) human-readable text (using valid HTML) that complements the description, like limits, warnings, etc.>
* adminOnly: <true for admin-only preferences>,
* type: <string|boolean|integer|enum|set|customList> (customList stores an array of JSON objects { value, label }),
* enumValues: <for type = "enum": a JSON object where the keys are the internal names and the values are human-readable strings>,
* setValues: <for type = "set": a JSON object where the keys are the internal names and the values are human-readable strings>,
* setDisplayOrder: <(optional) for type = "set": an array containing the keys of setValues (as strings) in the order that they are displayed>,
* customListValueTitle: <for type = "customList": the heading for the left "value" column in the custom list editor>,
* customListLabelTitle: <for type = "customList": the heading for the right "label" column in the custom list editor>
* },
* . . .
* ]
* },
* . . .
*
*/
Twinkle.config.sections = [
{
title: '全般',
module: 'general',
preferences: [
// TwinkleConfig.userTalkPageMode may take arguments:
// 'window': open a new window, remember the opened window
// 'tab': opens in a new tab, if possible.
// 'blank': force open in a new window, even if such a window exists
{
name: 'userTalkPageMode',
label: '利用者会話ページを開く時は次のページで開く',
type: 'enum',
enumValues: { window: 'ウィンドウ内で他の利用者会話ページを置き換える', tab: '新しいタブ', blank: '完全に新しいウィンドウ' }
},
// TwinkleConfig.dialogLargeFont (boolean)
{
name: 'dialogLargeFont',
label: 'Twinkleダイアログで大きな文字を使用',
type: 'boolean'
},
// Twinkle.config.disabledModules (array)
{
name: 'disabledModules',
label: '選択したTwinkleモジュールをオフ',
helptip: 'チェックを入れた機能は無効になります。再び有効にするにはチェックを外してください。<br />既定でチェックが入っているものは準備中の機能です。無効にしていても、関連する設定項目は表示されます。',
type: 'set',
setValues: { arv: 'ARV', warn: 'Warn', talkback: 'Talkback', speedy: 'CSD', xfd: 'XfD', image: 'DI', protect: 'RPP', tag: 'Tag', diff: 'Diff', unlink: 'Unlink', rollback: 'Rollback' }
},
// Twinkle.config.disabledSysopModules (array)
{
name: 'disabledSysopModules',
label: '選択した管理者専用モジュールをオフ',
helptip: 'ここで選択したものは使用できないので、注意してください。再び有効にするにはチェックを外してください。',
adminOnly: true,
type: 'set',
setValues: { block: 'Block', batchdelete: 'D-batch', batchprotect: 'P-batch', batchundelete: 'Und-batch' }
}
]
},
{
title: 'ARV: 荒らしの報告',
module: 'arv',
preferences: [
{
name: 'spiWatchReport',
label: '多重アカウントの調査依頼ページをウォッチリストに追加',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
}
]
},
{
title: 'Block: 利用者のブロック',
module: 'block',
adminOnly: true,
preferences: [
// TwinkleConfig.defaultToBlock64 (boolean)
// Whether to default to just blocking the /64 on or off
{
name: 'defaultToBlock64',
label: 'IPv6アドレスでは、既定で/64の範囲をブロック',
type: 'boolean'
},
// TwinkleConfig.defaultToPartialBlocks (boolean)
// Whether to default partial blocks on or off
{
name: 'defaultToPartialBlocks',
label: 'ブロックメニューを開いた際、既定で部分ブロックを選択',
helptip: '利用者がすでにブロックされている場合は、現在のブロックの種類が優先されます。',
type: 'boolean'
},
// TwinkleConfig.blankTalkpageOnIndefBlock (boolean)
// if true, blank the talk page when issuing an indef block notice (per [[WP:UWUL#Indefinitely blocked users]])
{
name: 'blankTalkpageOnIndefBlock',
label: '無期限ブロックの通知時に利用者会話ページを白紙化',
helptip: '詳しくは<a href="' + mw.util.getUrl('Wikipedia:WikiProject_User_warnings/Usage_and_layout#Indefinitely_blocked_users') + '">WP:UWUL</a>を参照してください。',
type: 'boolean'
}
]
},
{
title: 'DI: 画像削除',
module: 'image',
preferences: [
// TwinkleConfig.notifyUserOnDeli (boolean)
// If the user should be notified after placing a file deletion tag
{
name: 'notifyUserOnDeli',
label: '既定で"アップロード者に通知"にチェックを入れる',
type: 'boolean'
},
// TwinkleConfig.deliWatchPage (string)
// The watchlist setting of the page tagged for deletion.
{
name: 'deliWatchPage',
label: 'タグ付け時にファイルページをウォッチリストに追加',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
// TwinkleConfig.deliWatchUser (string)
// The watchlist setting of the user talk page if a notification is placed.
{
name: 'deliWatchUser',
label: '通知時に、アップロード者の会話ページをウォッチリストに追加',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
}
]
},
{
title: (Morebits.userIsSysop ? 'PP' : 'RPP') + ': ページの保護',
module: 'protect',
preferences: [
{
name: 'watchRequestedPages',
label: '保護依頼時にページをウォッチリストに追加',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
{
name: 'watchPPTaggedPages',
label: '保護テンプレートでページをタグ付けする際、ウォッチリストにページを追加',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
{
name: 'watchProtectedPages',
label: '保護する際にページをウォッチリストに追加',
helptip: 'ページ保護後にタグ付けも行う場合は、そちらが優先されます。',
adminOnly: true,
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
}
]
},
{
title: 'Rollback: 差し戻しと巻き戻し', // twinklerollback module
module: 'rollback',
preferences: [
// TwinkleConfig.autoMenuAfterRollback (bool)
// Option to automatically open the warning menu if the user talk page is opened post-reversion
{
name: 'autoMenuAfterRollback',
label: 'Twinkle差し戻し後、利用者会話ページでTwinkle警告メニューを自動的に開く',
helptip: '以下の該当するボックスにチェックが入っている場合のみ動作します。',
type: 'boolean'
},
// TwinkleConfig.openTalkPage (array)
// What types of actions that should result in opening of talk page
{
name: 'openTalkPage',
label: '以下の種類の差し戻し後に、会話ページを開く',
type: 'set',
setValues: { norm: '通常の差し戻し', vand: '荒らしの差し戻し' }
},
// TwinkleConfig.openTalkPageOnAutoRevert (bool)
// Defines if talk page should be opened when calling revert from contribs or recent changes pages. If set to true, openTalkPage defines then if talk page will be opened.
{
name: 'openTalkPageOnAutoRevert',
label: '投稿記録や最近の更新から差し戻しを実行した際に、会話ページを開く',
helptip: 'これが有効になっている場合、前の設定で必要なオプションが有効になっていなければ機能しません。',
type: 'boolean'
},
// TwinkleConfig.rollbackInPlace (bool)
//
{
name: 'rollbackInPlace',
label: '投稿記録や最近の更新から差し戻しする際、ページを更新しない',
helptip: 'これが有効になっている場合、Twinkleは差し戻し後に投稿記録や最近の更新フィードを再読み込みしないため、一度に複数の編集を差し戻すことができます。',
type: 'boolean'
},
// TwinkleConfig.markRevertedPagesAsMinor (array)
// What types of actions that should result in marking edit as minor
{
name: 'markRevertedPagesAsMinor',
label: '以下の種類の差し戻しは細部の編集として行う',
type: 'set',
setValues: { norm: '通常の差し戻し', vand: '荒らしの差し戻し', torev: '版の復元' }
},
// TwinkleConfig.watchRevertedPages (array)
// What types of actions that should result in forced addition to watchlist
{
name: 'watchRevertedPages',
label: '以下の種類の差し戻しの際、ページをウォッチリストに追加',
type: 'set',
setValues: { norm: '通常の差し戻し', vand: '荒らしの差し戻し', torev: '版の復元' }
},
// TwinkleConfig.watchRevertedExpiry
// If any of the above items are selected, whether to expire the watch
{
name: 'watchRevertedExpiry',
label: 'ページを差し戻した際、ウォッチする期間',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
// TwinkleConfig.offerReasonOnNormalRevert (boolean)
// If to offer a prompt for extra summary reason for normal reverts, default to true
{
name: 'offerReasonOnNormalRevert',
label: '通常の差し戻しに理由の入力を求めるプロンプトを表示',
helptip: '"通常の"差し戻しは [差し戻し] リンクから実行される差し戻しです。',
type: 'boolean'
},
{
name: 'confirmOnRollback',
label: '差し戻す前に確認を求める (すべての機器)',
helptip: 'ペンやタッチデバイスを使う人、慢性的に優柔不断な人向け。',
type: 'boolean'
},
{
name: 'confirmOnMobileRollback',
label: '差し戻す前に確認を求める (携帯機器のみ)',
helptip: '携帯機器での誤動作を防ぐ。',
type: 'boolean'
},
// TwinkleConfig.showRollbackLinks (array)
// Where Twinkle should show rollback links:
// diff, others, mine, contribs, history, recent
// Note from TTO: |contribs| seems to be equal to |others| + |mine|, i.e. redundant, so I left it out heres
{
name: 'showRollbackLinks',
label: 'これらのページで差し戻しリンクを表示',
type: 'set',
setValues: { diff: '差分ページ', others: '他の利用者の投稿記録ページ', mine: '自分の投稿記録ページ', recent: '最近の更新や関連ページの更新の特別ページ', history: '履歴ページ' }
},
{
name: 'SummaryOnVandalRevert',
label: '荒らしの差し戻しの際の要約欄をカスタム',
helptip: 'このボックスに要約を入力します。',
type: 'string',
obsolete: 'vandusersummary'
}
]
},
{
title: 'CSD: 即時削除',
module: 'speedy',
preferences: [
{
name: 'speedySelectionStyle',
label: 'ページをタグ付け/削除するタイミング',
type: 'enum',
enumValues: { buttonClick: '送信ボタンをクリックしたとき', radioClick: 'オプションをクリックするとすぐ' }
},
// TwinkleConfig.watchSpeedyPages (array)
// Whether to add speedy tagged or deleted pages to watchlist
{
name: 'watchSpeedyPages',
label: 'これらの基準を使用する場合は、ウォッチリストにページを追加',
type: 'set',
setValues: Twinkle.config.commonSets.csdCriteria,
setDisplayOrder: Twinkle.config.commonSets.csdCriteriaDisplayOrder
},
// TwinkleConfig.watchSpeedyExpiry
// If any of the above items are selected, whether to expire the watch
{
name: 'watchSpeedyExpiry',
label: 'ページをタグ付けする場合、それをウォッチする期間',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
// TwinkleConfig.markSpeedyPagesAsPatrolled (boolean)
// If, when applying speedy template to page, to mark the page as triaged/patrolled (if the page was reached from NewPages)
{
name: 'markSpeedyPagesAsPatrolled',
label: 'タグ付けの際、ページを巡回済みとしてマーク(可能な場合)',
helptip: 'これは最善の慣行に反するおそれがあるので、おそらくチェックすべきではないです。',
type: 'boolean'
},
// TwinkleConfig.promptForSpeedyDeletionSummary (array of strings)
{
name: 'promptForSpeedyDeletionSummary',
label: 'これらの基準で削除する場合、削除要約の編集を行う',
adminOnly: true,
type: 'set',
setValues: Twinkle.config.commonSets.csdAndImageDeletionCriteria,
setDisplayOrder: Twinkle.config.commonSets.csdAndImageDeletionCriteriaDisplayOrder
},
// TwinkleConfig.deleteTalkPageOnDelete (boolean)
// If talk page if exists should also be deleted (CSD G8) when spedying a page (admin only)
{
name: 'deleteTalkPageOnDelete',
label: '既定で「トークページも削除する」にチェックを入れる',
adminOnly: true,
type: 'boolean'
},
{
name: 'deleteRedirectsOnDelete',
label: '既定で「リダイレクトも削除」にチェックを入れる',
adminOnly: true,
type: 'boolean'
},
// TwinkleConfig.deleteSysopDefaultToDelete (boolean)
// Make the CSD screen default to "delete" instead of "tag" (admin only)
{
name: 'deleteSysopDefaultToDelete',
label: '既定でタグ付けではなく即時削除を行う',
helptip: 'CSDタグが既に存在する場合、Twinkleは常に「削除」モードが既定となります。',
adminOnly: true,
type: 'boolean'
},
// TwinkleConfig.speedyWindowWidth (integer)
// Defines the width of the Twinkle SD window in pixels
{
name: 'speedyWindowWidth',
label: '即時削除ウィンドウの幅(ピクセル)',
type: 'integer'
},
// TwinkleConfig.speedyWindowWidth (integer)
// Defines the width of the Twinkle SD window in pixels
{
name: 'speedyWindowHeight',
label: '即時削除ウィンドウの高さ(ピクセル)',
helptip: '大きなモニターをお持ちの場合は、これを増やしてください。',
type: 'integer'
},
{
name: 'logSpeedyNominations',
label: 'すべてのCSDへの指定記録を利用者ページに保存',
helptip: '管理者以外は削除された投稿にアクセスできないため、利用者ページでの記録は、Twinkleを使用してCSDに指定したすべてのページを追跡するための優れた方法になります。DIを使用してタグ付けされたファイルもこの記録ページに追加されます。',
type: 'boolean'
},
{
name: 'speedyLogPageName',
label: 'この利用者サブページでCSDへの指定記録を保存',
helptip: 'このボックスにサブページ名を入力します。 CSD記録は利用者:' + mw.config.get('wgUserName') + '/"サブページ名"にあります。 CSD利用者ページへの記録をオンにした場合にのみ機能します。',
type: 'string'
},
{
name: 'noLogOnSpeedyNomination',
label: 'これらの基準でタグ付けする場合は、利用者サブページでの記録を作成しない',
type: 'set',
setValues: Twinkle.config.commonSets.csdAndImageDeletionCriteria,
setDisplayOrder: Twinkle.config.commonSets.csdAndImageDeletionCriteriaDisplayOrder
}
]
},
{
title: 'Tag: メンテナンス用テンプレートの付与',
module: 'tag',
preferences: [
{
name: 'watchTaggedVenues',
label: '次の種類のページにテンプレートを付与した際、ウォッチリストに追加',
type: 'set',
setValues: { articles: '記事', drafts: '下書き', redirects: 'リダイレクト', files: 'ファイル' }
},
{
name: 'watchTaggedPages',
label: 'テンプレートを付与したページのウォッチ期間',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
{
name: 'watchMergeDiscussions',
label: '統合提案を開始した際、会話ページをウォッチリストに追加',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
{
name: 'markTaggedPagesAsMinor',
label: 'テンプレートの付与を細部の編集として扱う',
type: 'boolean'
},
{
name: 'markTaggedPagesAsPatrolled',
label: '既定で「ページを巡回済み・確認済みにする」にチェックを入れる',
type: 'boolean'
},
{
name: 'tagArticleSortOrder',
label: '記事用テンプレートの既定の表示順',
type: 'enum',
enumValues: { cat: '分類別', alpha: 'アルファベット順' }
},
{
name: 'customTagList',
label: '追加で表示する記事・下書きのメンテナンス用テンプレート',
helptip: 'テンプレート一覧の末尾に選択肢を追加します。Twinkleにまだ登録されていないメンテナンス用テンプレートなどを指定できます。',
type: 'customList',
customListValueTitle: 'テンプレート名(波括弧なし)',
customListLabelTitle: 'テンプレート付与画面に表示する名前'
},
{
name: 'customFileTagList',
label: '追加で表示するファイルのメンテナンス用テンプレート',
helptip: 'ファイルに付与できるテンプレートを追加します。',
type: 'customList',
customListValueTitle: 'テンプレート名(波括弧なし)',
customListLabelTitle: 'テンプレート付与画面に表示する名前'
},
{
name: 'customRedirectTagList',
label: '追加で表示するリダイレクト分類用テンプレート',
helptip: 'リダイレクトに付与できるテンプレートを追加します。',
type: 'customList',
customListValueTitle: 'テンプレート名(波括弧なし)',
customListLabelTitle: 'テンプレート付与画面に表示する名前'
}
]
},
{
title: 'Talkback: 返信通知',
module: 'talkback',
preferences: [
{
name: 'markTalkbackAsMinor',
label: '返信通知を細部の編集として扱う',
type: 'boolean'
},
{
name: 'insertTalkbackSignature',
label: '返信通知に署名を挿入',
type: 'boolean'
},
{
name: 'talkbackHeading',
label: '返信通知と会話ページへの案内に使用する節見出し',
tooltip: 'ウィキテキストの見出しに使う等号(==)は入力しないでください。',
type: 'string'
},
{
name: 'mailHeading',
label: 'メール送信通知に使用する節見出し',
tooltip: 'ウィキテキストの見出しに使う等号(==)は入力しないでください。',
type: 'string'
}
]
},
{
title: 'Unlink: リンク解除',
module: 'unlink',
preferences: [
// TwinkleConfig.unlinkNamespaces (array)
// In what namespaces unlink should happen, default in 0 (article), 10 (template), 100 (portal), and 118 (draft)
{
name: 'unlinkNamespaces',
label: 'リンク解除の対象にする名前空間',
helptip: '会話ページの過去ログ内のリンクまで解除されるおそれがあるため、会話用の名前空間は選択しないでください。',
type: 'set',
setValues: Twinkle.config.commonSets.namespacesNoSpecial
}
]
},
{
title: 'Warn: 利用者への警告',
module: 'warn',
preferences: [
// TwinkleConfig.defaultWarningGroup (int)
// Which level warning should be the default selected group, default is 1
{
name: 'defaultWarningGroup',
label: '既定の警告レベル',
type: 'enum',
enumValues: {
1: 'レベル1',
2: 'レベル2',
3: 'レベル3',
4: 'レベル4',
5: '即時の最終警告(レベル4im)',
6: '個別の問題に関する通知',
7: '個別の問題に関する警告',
// 8 was used for block templates before #260
9: '独自の警告',
10: 'すべての警告テンプレート',
11: 'レベルを自動選択(1~4)'
}
},
// TwinkleConfig.combinedSingletMenus (boolean)
// if true, show one menu with both single-issue notices and warnings instead of two separately
{
name: 'combinedSingletMenus',
label: '個別の問題に関する通知と警告のメニューをまとめる',
helptip: '有効にすると、既定の警告レベルで個別の問題に関する通知または警告を選んでいる場合に、統合したメニューを既定で表示します。',
type: 'boolean'
},
// TwinkleConfig.watchWarnings (string)
// Watchlist setting for the page which has been dispatched an warning or notice
{
name: 'watchWarnings',
label: '通知した利用者の会話ページをウォッチリストに追加',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
// TwinkleConfig.oldSelect (boolean)
// if true, use the native select menu rather the select2-based one
{
name: 'oldSelect',
label: '検索機能のない従来の選択メニューを使用',
type: 'boolean'
},
{
name: 'customWarningList',
label: '追加で表示する警告テンプレート',
helptip: 'テンプレートや利用者サブページを追加できます。追加した項目は警告画面の「独自の警告」に表示されます。',
type: 'customList',
customListValueTitle: 'テンプレート名(波括弧なし)',
customListLabelTitle: '警告一覧に表示する名前(編集要約にも使用)'
}
]
},
{
title: 'XfD: 削除依頼',
module: 'xfd',
preferences: [
{
name: 'logXfdNominations',
label: '削除依頼(XfD)を提出したページを利用者サブページに記録',
helptip: 'Twinkleで削除依頼を提出したページを、利用者サブページの記録から確認できます。',
type: 'boolean'
},
{
name: 'xfdLogPageName',
label: '削除依頼の記録を保存する利用者サブページ名',
helptip: 'サブページ名を入力してください。記録先は「利用者:<i>利用者名</i>/<i>サブページ名</i>」です。削除依頼の記録を有効にしている場合にのみ機能します。',
type: 'string'
},
{
name: 'noLogOnXfdNomination',
label: '次の手続きに提出する場合は利用者サブページに記録しない',
type: 'set',
setValues: { afd: '記事の削除依頼 (AfD)', tfd: 'テンプレートの削除・統合依頼 (TfD)', ffd: 'ファイルの削除依頼 (FfD)', cfd: 'カテゴリの削除・変更依頼 (CfD)', cfds: 'カテゴリの即時変更依頼 (CfD/S)', mfd: 'その他のページの削除依頼 (MfD)', rfd: 'リダイレクトの削除・転送先変更依頼 (RfD)', rm: '改名依頼 (RM)' }
},
// TwinkleConfig.xfdWatchPage (string)
// The watchlist setting of the page being nominated for XfD.
{
name: 'xfdWatchPage',
label: '削除依頼の対象ページをウォッチリストに追加',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
// TwinkleConfig.xfdWatchDiscussion (string)
// The watchlist setting of the newly created XfD page (for those processes that create discussion pages for each nomination),
// or the list page for the other processes.
{
name: 'xfdWatchDiscussion',
label: '削除依頼ページをウォッチリストに追加',
helptip: 'AfD・MfDでは個別の依頼ページ、TfD・CfD・RfD・FfDでは日付別の依頼一覧を対象にします。',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
// TwinkleConfig.xfdWatchList (string)
// The watchlist setting of the XfD list page, *if* the discussion is on a separate page.
{
name: 'xfdWatchList',
label: '日付別の依頼一覧や一覧ページをウォッチリストに追加(AfD・MfD)',
helptip: 'AfD・MfDにのみ適用します。AfDでは日付別の依頼一覧、MfDでは総合一覧ページに依頼ページが読み込まれます。',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
// TwinkleConfig.xfdWatchUser (string)
// The watchlist setting of the user talk page if they receive a notification.
{
name: 'xfdWatchUser',
label: '通知した初版投稿者の会話ページをウォッチリストに追加',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
// TwinkleConfig.xfdWatchRelated (string)
// The watchlist setting of the target of a redirect being nominated for RfD.
{
name: 'xfdWatchRelated',
label: '通知時にリダイレクト先のページをウォッチリストに追加',
helptip: 'RfDで、リダイレクト先の会話ページに通知する場合にのみ適用します。',
type: 'enum',
enumValues: Twinkle.config.watchlistEnums
},
{
name: 'markXfdPagesAsPatrolled',
label: '記事の削除依頼(AfD)への提出時にページを巡回済み・確認済みに(可能な場合)',
type: 'boolean'
}
]
},
{
title: '詳細設定(非表示)',
hidden: true,
preferences: [
// twinklerollback.js: defines how many revision to query maximum, maximum possible is 50, default is 50
{
name: 'revertMaxRevisions',
type: 'integer'
},
// twinklewarn.js: When using the autolevel select option, how many days makes a prior warning stale
// Huggle is three days ([[Special:Diff/918980316]] and [[Special:Diff/919417999]]) while ClueBotNG is two:
// https://github.com/DamianZaremba/cluebotng/blob/4958e25d6874cba01c75f11debd2e511fd5a2ce5/bot/action_functions.php#L62
{
name: 'autolevelStaleDays',
type: 'integer'
},
// How many pages should be queried by deprod and batchdelete/protect/undelete
{
name: 'batchMax',
type: 'integer',
adminOnly: true
},
// How many pages should be processed at a time by deprod and batchdelete/protect/undelete
{
name: 'batchChunks',
type: 'integer',
adminOnly: true
}
]
}
]; // end of Twinkle.config.sections
Twinkle.config.init = function twinkleconfigInit() {
// create the config page at User:Syunsyunminmin/Twinkle/Preferences
if ((mw.config.get('wgNamespaceNumber') === mw.config.get('wgNamespaceIds').user && mw.config.get('wgTitle') === 'Syunsyunminmin/Twinkle/Preferences') &&
mw.config.get('wgAction') === 'view') {
if (!document.getElementById('twinkle-config')) {
return; // maybe the page is misconfigured, or something - but any attempt to modify it will be pointless
}
// set style to nothing to prevent conflict with external css
document.getElementById('twinkle-config').removeAttribute('style');
document.getElementById('twinkle-config-titlebar').removeAttribute('style');
const contentdiv = document.getElementById('twinkle-config-content');
contentdiv.textContent = ''; // clear children
// let user know about possible conflict with skin js/common.js file
// (settings in that file will still work, but they will be overwritten by twinkleoptions.js settings)
if (window.TwinkleConfig || window.FriendlyConfig) {
const contentnotice = document.createElement('div');
contentnotice.className = 'plainlinks twinkle-ombox';
contentnotice.innerHTML =
'<div>' +
'<img alt="" src="https://upload.wikimedia.org/wikipedia/commons/3/38/Imbox_content.png" />' +
'</div>' +
'<div>' +
'<p><big><b>ここで設定を変更する前に、</b>個人用JavaScriptから古いTwinkleとFriendlyの設定を削除してください。</big></p>' +
'<p><a href="' + mw.util.getUrl('User:' + mw.config.get('wgUserName') + '/' + mw.config.get('skin') +
'.js', { action: 'edit' }) + '" target="_blank"><b>使用中の外装の個人用JavaScript</b></a>または<a href="' +
mw.util.getUrl('User:' + mw.config.get('wgUserName') + '/common.js', { action: 'edit'}) + '" target="_blank"><b>common.js</b></a>を編集し、<code>TwinkleConfig</code>と<code>FriendlyConfig</code>を参照する設定行を削除してください。</p>' +
'</div>';
contentdiv.appendChild(contentnotice);
}
// start a table of contents
const toctable = document.createElement('div');
toctable.className = 'toc';
toctable.style.marginLeft = '0.4em';
// create TOC title
const toctitle = document.createElement('div');
toctitle.id = 'toctitle';
var toch2 = document.createElement('h2');
toch2.textContent = '目次 ';
toctitle.appendChild(toch2);
// add TOC show/hide link
const toctoggle = document.createElement('span');
toctoggle.className = 'toctoggle';
toctoggle.appendChild(document.createTextNode('['));
const toctogglelink = document.createElement('a');
toctogglelink.className = 'internal';
toctogglelink.setAttribute('href', '#tw-tocshowhide');
toctogglelink.textContent = '非表示';
toctoggle.appendChild(toctogglelink);
toctoggle.appendChild(document.createTextNode(']'));
toctitle.appendChild(toctoggle);
toctable.appendChild(toctitle);
// create item container: this is what we add stuff to
const tocul = document.createElement('ul');
toctogglelink.addEventListener('click', () => {
const $tocul = $(tocul);
$tocul.toggle();
if ($tocul.find(':visible').length) {
toctogglelink.textContent = '非表示';
} else {
toctogglelink.textContent = '表示';
}
}, false);
toctable.appendChild(tocul);
contentdiv.appendChild(toctable);
const contentform = document.createElement('form');
contentform.setAttribute('action', 'javascript:void(0)'); // was #tw-save - changed to void(0) to work around Chrome issue
// Use loaded preferences instead of restoring earlier browser form values.
contentform.setAttribute('autocomplete', 'off');
contentform.addEventListener('submit', Twinkle.config.save, true);
contentdiv.appendChild(contentform);
const container = document.createElement('table');
container.style.width = '100%';
contentform.appendChild(container);
$(Twinkle.config.sections).each((sectionkey, section) => {
if (section.hidden || (section.adminOnly && !Morebits.userIsSysop)) {
return true; // i.e. "continue" in this context
}
// add to TOC
const tocli = document.createElement('li');
tocli.className = 'toclevel-1';
const toca = document.createElement('a');
toca.setAttribute('href', '#' + section.module);
toca.appendChild(document.createTextNode(section.title));
tocli.appendChild(toca);
tocul.appendChild(tocli);
let row = document.createElement('tr');
let cell = document.createElement('td');
cell.setAttribute('colspan', '3');
const heading = document.createElement('h4');
heading.style.borderBottom = '1px solid gray';
heading.style.marginTop = '0.2em';
heading.id = section.module;
heading.appendChild(document.createTextNode(section.title));
cell.appendChild(heading);
row.appendChild(cell);
container.appendChild(row);
let rowcount = 1; // for row banding
// add each of the preferences to the form
$(section.preferences).each((prefkey, pref) => {
if (pref.adminOnly && !Morebits.userIsSysop) {
return true; // i.e. "continue" in this context
}
row = document.createElement('tr');
row.style.marginBottom = '0.2em';
// create odd row banding
if (rowcount++ % 2 === 0) {
row.style.backgroundColor = 'rgba(128, 128, 128, 0.1)';
}
cell = document.createElement('td');
var label, input, gotPref = Twinkle.getPref(pref.name), obsoletePref = Twinkle.getPref(pref.obsolete);
switch (pref.type) {
case 'boolean': // create a checkbox
cell.setAttribute('colspan', '2');
label = document.createElement('label');
input = document.createElement('input');
input.setAttribute('type', 'checkbox');
input.setAttribute('id', pref.name);
input.setAttribute('name', pref.name);
if (gotPref === true || obsoletePref === true) {
input.setAttribute('checked', 'checked');
}
label.appendChild(input);
label.appendChild(document.createTextNode(pref.label));
cell.appendChild(label);
break;
case 'string': // create an input box
case 'integer':
// add label to first column
cell.style.textAlign = 'right';
cell.style.paddingRight = '0.5em';
label = document.createElement('label');
label.setAttribute('for', pref.name);
label.appendChild(document.createTextNode(pref.label + ':'));
cell.appendChild(label);
row.appendChild(cell);
// add input box to second column
cell = document.createElement('td');
cell.style.paddingRight = '1em';
input = document.createElement('input');
input.setAttribute('type', 'text');
input.setAttribute('id', pref.name);
input.setAttribute('name', pref.name);
if (pref.type === 'integer') {
input.setAttribute('size', 6);
input.setAttribute('type', 'number');
input.setAttribute('step', '1'); // integers only
}
if (gotPref) {
input.setAttribute('value', gotPref);
}
if (obsoletePref) {
input.setAttribute('value', obsoletePref);
}
cell.appendChild(input);
break;
case 'enum': // create a combo box
// add label to first column
// note: duplicates the code above, under string/integer
cell.style.textAlign = 'right';
cell.style.paddingRight = '0.5em';
label = document.createElement('label');
label.setAttribute('for', pref.name);
label.appendChild(document.createTextNode(pref.label + ':'));
cell.appendChild(label);
row.appendChild(cell);
// add input box to second column
cell = document.createElement('td');
cell.style.paddingRight = '1em';
input = document.createElement('select');
input.setAttribute('id', pref.name);
input.setAttribute('name', pref.name);
$.each(pref.enumValues, (enumvalue, enumdisplay) => {
const option = document.createElement('option');
option.setAttribute('value', enumvalue);
if ((gotPref === enumvalue || obsoletePref === enumvalue) ||
// Hack to convert old boolean watchlist prefs
// to corresponding enums (added in v2.1)
((typeof gotPref === 'boolean' || typeof obsoletePref === 'boolean') &&
(((gotPref || obsoletePref) && enumvalue === 'yes') ||
(!(gotPref && obsoletePref) && enumvalue === 'no')))) {
option.setAttribute('selected', 'selected');
}
option.appendChild(document.createTextNode(enumdisplay));
input.appendChild(option);
});
cell.appendChild(input);
break;
case 'set': // create a set of check boxes
// add label first of all
cell.setAttribute('colspan', '2');
label = document.createElement('label'); // not really necessary to use a label element here, but we do it for consistency of styling
label.appendChild(document.createTextNode(pref.label + ':'));
cell.appendChild(label);
var checkdiv = document.createElement('div');
checkdiv.style.paddingLeft = '1em';
var worker = function(itemkey, itemvalue) {
const checklabel = document.createElement('label');
checklabel.style.marginRight = '0.7em';
checklabel.style.display = 'inline-block';
const check = document.createElement('input');
check.setAttribute('type', 'checkbox');
check.setAttribute('id', pref.name + '_' + itemkey);
check.setAttribute('name', pref.name + '_' + itemkey);
if ((gotPref && gotPref.includes(itemkey)) || (obsoletePref && obsoletePref.includes(itemkey))) {
check.setAttribute('checked', 'checked');
}
// cater for legacy integer array values for unlinkNamespaces (this can be removed a few years down the track...)
if (pref.name === 'unlinkNamespaces') {
if ((gotPref && gotPref.includes(parseInt(itemkey, 10))) || (obsoletePref && obsoletePref.includes(parseInt(itemkey, 10)))) {
check.setAttribute('checked', 'checked');
}
}
checklabel.appendChild(check);
checklabel.appendChild(document.createTextNode(itemvalue));
checkdiv.appendChild(checklabel);
};
if (pref.setDisplayOrder) {
// add check boxes according to the given display order
$.each(pref.setDisplayOrder, (itemkey, item) => {
worker(item, pref.setValues[item]);
});
} else {
// add check boxes according to the order it gets fed to us (probably strict alphabetical)
$.each(pref.setValues, worker);
}
cell.appendChild(checkdiv);
break;
case 'customList':
// add label to first column
cell.style.textAlign = 'right';
cell.style.paddingRight = '0.5em';
label = document.createElement('label');
label.setAttribute('for', pref.name);
label.appendChild(document.createTextNode(pref.label + ':'));
cell.appendChild(label);
row.appendChild(cell);
// add button to second column
cell = document.createElement('td');
cell.style.paddingRight = '1em';
var button = document.createElement('button');
button.setAttribute('id', pref.name);
button.setAttribute('name', pref.name);
button.setAttribute('type', 'button');
button.addEventListener('click', Twinkle.config.listDialog.display, false);
// use jQuery data on the button to store the current config value
$(button).data({
value: (obsoletePref) || gotPref,
pref: pref
});
button.appendChild(document.createTextNode('アイテムを編集'));
cell.appendChild(button);
break;
default:
alert('twinkleconfig: 設定 ' + pref.name + ' の不明なデータ型');
break;
}
row.appendChild(cell);
// add help tip
cell = document.createElement('td');
cell.className = 'twinkle-config-helptip';
if (pref.helptip) {
// convert mentions of templates in the helptip to clickable links
cell.innerHTML = pref.helptip.replace(/{{(.+?)}}/g,
'{{<a href="' + mw.util.getUrl('Template:') + '$1" target="_blank">$1</a>}}');
}
// add reset link (custom lists don't need this, as their config value isn't displayed on the form)
if (pref.type !== 'customList') {
const resetlink = document.createElement('a');
resetlink.setAttribute('href', '#tw-reset');
resetlink.setAttribute('id', 'twinkle-config-reset-' + pref.name);
resetlink.addEventListener('click', Twinkle.config.resetPrefLink, false);
resetlink.style.cssFloat = 'right';
resetlink.style.margin = '0 0.6em';
resetlink.appendChild(document.createTextNode('初期化'));
cell.appendChild(resetlink);
}
row.appendChild(cell);
container.appendChild(row);
return true;
});
return true;
});
const footerbox = document.createElement('div');
footerbox.setAttribute('id', 'twinkle-config-buttonpane');
const button = document.createElement('button');
button.setAttribute('id', 'twinkle-config-submit');
button.setAttribute('type', 'submit');
button.appendChild(document.createTextNode('変更を保存'));
footerbox.appendChild(button);
const footerspan = document.createElement('span');
footerspan.className = 'plainlinks';
footerspan.style.marginLeft = '2.4em';
footerspan.style.fontSize = '90%';
const footera = document.createElement('a');
footera.setAttribute('href', '#tw-reset-all');
footera.setAttribute('id', 'twinkle-config-resetall');
footera.addEventListener('click', Twinkle.config.resetAllPrefs, false);
footera.appendChild(document.createTextNode('デフォルトに戻す'));
footerspan.appendChild(footera);
footerbox.appendChild(footerspan);
contentform.appendChild(footerbox);
// since all the section headers exist now, we can try going to the requested anchor
if (window.location.hash) {
const loc = window.location.hash;
window.location.hash = '';
window.location.hash = loc;
}
} else if (mw.config.get('wgNamespaceNumber') === mw.config.get('wgNamespaceIds').user &&
mw.config.get('wgTitle').indexOf(mw.config.get('wgUserName')) === 0 &&
mw.config.get('wgPageName').slice(-3) === '.js') {
const box = document.createElement('div');
// Styled in twinkle.css
box.setAttribute('id', 'twinkle-config-headerbox');
let link;
const scriptPageName = mw.config.get('wgPageName').slice(
mw.config.get('wgPageName').lastIndexOf('/') + 1,
mw.config.get('wgPageName').lastIndexOf('.js')
);
if (scriptPageName === 'twinkleoptions') {
// place "why not try the preference panel" notice
box.setAttribute('class', 'config-twopt-box');
link = document.createElement('a');
link.setAttribute('href', mw.util.getUrl(mw.config.get('wgFormattedNamespaces')[mw.config.get('wgNamespaceIds').user] + ':Syunsyunminmin/Twinkle/Preferences'));
link.appendChild(document.createTextNode('Twinkle個人設定パネル'));
if (mw.config.get('wgArticleId') > 0) { // page exists
box.appendChild(document.createTextNode('このページにはTwinkleの設定が含まれています。設定は '));
box.appendChild(link);
box.appendChild(document.createTextNode('を使用するか、このページを編集することで変更できます。'));
} else { // page does not exist
box.appendChild(link);
box.appendChild(document.createTextNode('またはこのページを編集することで、Twinkleを個人の好みに合わせてカスタマイズできます。'));
}
$(box).insertAfter($('#contentSub'));
} else if (['monobook', 'vector', 'vector-2022', 'cologneblue', 'modern', 'timeless', 'minerva', 'common'].includes(scriptPageName)) {
// place "Looking for Twinkle options?" notice
box.setAttribute('class', 'config-userskin-box');
box.appendChild(document.createTextNode('Twinkle個人設定を設定したい場合、'));
link = document.createElement('a');
link.setAttribute('href', mw.util.getUrl(mw.config.get('wgFormattedNamespaces')[mw.config.get('wgNamespaceIds').user] + ':Syunsyunminmin/Twinkle/Preferences'));
link.appendChild(document.createTextNode('Twinkle個人設定パネル'));
box.appendChild(link);
box.appendChild(document.createTextNode('を使用して行えます。'));
$(box).insertAfter($('#contentSub'));
}
}
};
// custom list-related stuff
Twinkle.config.listDialog = {};
Twinkle.config.listDialog.addRow = function twinkleconfigListDialogAddRow($dlgtable, value, label) {
let $contenttr, $valueInput, $labelInput;
$dlgtable.append(
$contenttr = $('<tr>').append(
$('<td>').append(
$('<button>')
.attr('type', 'button')
.css('white-space', 'nowrap')
.on('click', () => {
$contenttr.remove();
})
.text('除去')
),
$('<td>').append(
$valueInput = $('<input>')
.attr('type', 'text')
.addClass('twinkle-config-customlist-value')
.css('width', '97%')
),
$('<td>').append(
$labelInput = $('<input>')
.attr('type', 'text')
.addClass('twinkle-config-customlist-label')
.css('width', '98%')
)
)
);
if (value) {
$valueInput.val(value);
}
if (label) {
$labelInput.val(label);
}
};
Twinkle.config.listDialog.display = function twinkleconfigListDialogDisplay(e) {
const $prefbutton = $(e.target);
const curvalue = $prefbutton.data('value');
const curpref = $prefbutton.data('pref');
const dialog = new Morebits.SimpleWindow(720, 400);
dialog.setTitle(curpref.label);
dialog.setScriptName('Twinkle個人設定');
let $dlgtbody;
dialog.setContent(
$('<div>').append(
$('<table>')
.addClass('wikitable')
.css({
margin: '1.4em 1em',
width: 'auto'
})
.append(
$dlgtbody = $('<tbody>').append(
// header row
$('<tr>').append(
$('<th>') // top-left cell
.css('width', '5%'),
$('<th>') // value column header
.css('width', '35%')
.text(curpref.customListValueTitle ? curpref.customListValueTitle : '値'),
$('<th>') // label column header
.css('width', '60%')
.text(curpref.customListLabelTitle ? curpref.customListLabelTitle : '表示名')
)
),
$('<tfoot>').append(
$('<tr>').append(
$('<td>')
.attr('colspan', '3')
.append(
$('<button>')
.text('追加')
.css('min-width', '8em')
.attr('type', 'button')
.on('click', () => {
Twinkle.config.listDialog.addRow($dlgtbody);
})
)
)
)
),
$('<button>')
.text('変更を保存')
.attr('type', 'submit') // so Morebits.SimpleWindow puts the button in the button pane
.on('click', () => {
Twinkle.config.listDialog.save($prefbutton, $dlgtbody);
dialog.close();
}),
$('<button>')
.text('初期化')
.attr('type', 'submit')
.on('click', () => {
Twinkle.config.listDialog.reset($prefbutton, $dlgtbody);
}),
$('<button>')
.text('キャンセル')
.attr('type', 'submit')
.on('click', () => {
dialog.close();
})
)[0]
);
// content rows
let gotRow = false;
$.each(curvalue, (k, v) => {
gotRow = true;
Twinkle.config.listDialog.addRow($dlgtbody, v.value, v.label);
});
// if there are no values present, add a blank row to start the user off
if (!gotRow) {
Twinkle.config.listDialog.addRow($dlgtbody);
}
dialog.display();
};
// Resets the data value, re-populates based on the new (default) value, then saves the
// old data value again (less surprising behaviour)
Twinkle.config.listDialog.reset = function twinkleconfigListDialogReset($button, $tbody) {
// reset value on button
const curpref = $button.data('pref');
const oldvalue = $button.data('value');
Twinkle.config.resetPref(curpref);
// reset form
$tbody.find('tr').slice(1).remove(); // all rows except the first (header) row
// add the new values
const curvalue = $button.data('value');
$.each(curvalue, (k, v) => {
Twinkle.config.listDialog.addRow($tbody, v.value, v.label);
});
// save the old value
$button.data('value', oldvalue);
};
Twinkle.config.listDialog.save = function twinkleconfigListDialogSave($button, $tbody) {
const result = [];
let current = {};
$tbody.find('input[type="text"]').each((inputkey, input) => {
if ($(input).hasClass('twinkle-config-customlist-value')) {
current = { value: input.value };
} else {
current.label = input.value;
// exclude totally empty rows
if (current.value || current.label) {
result.push(current);
}
}
});
$button.data('value', result);
};
// reset/restore defaults
Twinkle.config.resetPrefLink = function twinkleconfigResetPrefLink(e) {
const wantedpref = e.target.id.slice(21); // "twinkle-config-reset-" prefix is stripped
// search tactics
$(Twinkle.config.sections).each((sectionkey, section) => {
if (section.hidden || (section.adminOnly && !Morebits.userIsSysop)) {
return true; // continue: skip impossibilities
}
let foundit = false;
$(section.preferences).each((prefkey, pref) => {
if (pref.name !== wantedpref) {
return true; // continue
}
Twinkle.config.resetPref(pref);
foundit = true;
return false; // break
});
if (foundit) {
return false; // break
}
});
return false; // stop link from scrolling page
};
Twinkle.config.resetPref = function twinkleconfigResetPref(pref) {
switch (pref.type) {
case 'boolean':
document.getElementById(pref.name).checked = Twinkle.defaultConfig[pref.name];
break;
case 'string':
case 'integer':
case 'enum':
document.getElementById(pref.name).value = Twinkle.defaultConfig[pref.name];
break;
case 'set':
$.each(pref.setValues, (itemkey) => {
if (document.getElementById(pref.name + '_' + itemkey)) {
document.getElementById(pref.name + '_' + itemkey).checked = Twinkle.defaultConfig[pref.name].includes(itemkey);
}
});
break;
case 'customList':
$(document.getElementById(pref.name)).data('value', Twinkle.defaultConfig[pref.name]);
break;
default:
alert('twinkleconfig: 設定 ' + pref.name + ' の不明なデータ型');
break;
}
};
Twinkle.config.resetAllPrefs = function twinkleconfigResetAllPrefs() {
// no confirmation message - the user can just refresh/close the page to abort
$(Twinkle.config.sections).each((sectionkey, section) => {
if (section.hidden || (section.adminOnly && !Morebits.userIsSysop)) {
return true; // continue: skip impossibilities
}
$(section.preferences).each((prefkey, pref) => {
if (!pref.adminOnly || Morebits.userIsSysop) {
Twinkle.config.resetPref(pref);
}
});
return true;
});
return false; // stop link from scrolling page
};
Twinkle.config.save = function twinkleconfigSave(e) {
Morebits.Status.init(document.getElementById('twinkle-config-content'));
var userjs = mw.config.get('wgFormattedNamespaces')[mw.config.get('wgNamespaceIds').user] + ':' + mw.config.get('wgUserName') + '/twinkleoptions.js';
var wikipedia_page = new Morebits.wiki.Page(userjs, userjs + 'に設定を保存');
wikipedia_page.setCallbackParameters(e.target);
wikipedia_page.load(Twinkle.config.writePrefs);
return false;
};
Twinkle.config.writePrefs = function twinkleconfigWritePrefs(pageobj) {
const form = pageobj.getCallbackParameters();
// this is the object which gets serialized into JSON; only
// preferences that this script knows about are kept
const newConfig = {optionsVersion: 2.1};
// a comparison function is needed later on
// it is just enough for our purposes (i.e. comparing strings, numbers, booleans,
// arrays of strings, and arrays of { value, label })
// and it is not very robust: e.g. compare([2], ["2"]) === true, and
// compare({}, {}) === false, but it's good enough for our purposes here
const compare = function(a, b) {
if (Array.isArray(a)) {
if (a.length !== b.length) {
return false;
}
const asort = a.sort(), bsort = b.sort();
for (let i = 0; asort[i]; ++i) {
// comparison of the two properties of custom lists
if ((typeof asort[i] === 'object') && (asort[i].label !== bsort[i].label ||
asort[i].value !== bsort[i].value)) {
return false;
} else if (asort[i].toString() !== bsort[i].toString()) {
return false;
}
}
return true;
}
return a === b;
};
$(Twinkle.config.sections).each((sectionkey, section) => {
if (section.adminOnly && !Morebits.userIsSysop) {
return; // i.e. "continue" in this context
}
// reach each of the preferences from the form
$(section.preferences).each((prefkey, pref) => {
let userValue; // = undefined
// only read form values for those prefs that have them
if (!pref.adminOnly || Morebits.userIsSysop) {
if (!section.hidden) {
switch (pref.type) {
case 'boolean': // read from the checkbox
userValue = form[pref.name].checked;
break;
case 'string': // read from the input box or combo box
case 'enum':
userValue = form[pref.name].value;
break;
case 'integer': // read from the input box
userValue = parseInt(form[pref.name].value, 10);
if (isNaN(userValue)) {
Morebits.Status.warn('保存', pref.name + 'に対して指定した値 (' + pref.value + ') は無効です。保存は続行しますが、無効なデータ値は飛ばされます。');
userValue = null;
}
break;
case 'set': // read from the set of check boxes
userValue = [];
if (pref.setDisplayOrder) {
// read only those keys specified in the display order
$.each(pref.setDisplayOrder, (itemkey, item) => {
if (form[pref.name + '_' + item].checked) {
userValue.push(item);
}
});
} else {
// read all the keys in the list of values
$.each(pref.setValues, (itemkey) => {
if (form[pref.name + '_' + itemkey].checked) {
userValue.push(itemkey);
}
});
}
break;
case 'customList': // read from the jQuery data stored on the button object
userValue = $(form[pref.name]).data('value');
break;
default:
alert('twinkleconfig: 設定 ' + pref.name + ' の不明なデータ型');
break;
}
} else if (Twinkle.prefs) {
// Retain the hidden preferences that may have customised by the user from twinkleoptions.js
// undefined if not set
userValue = Twinkle.prefs[pref.name];
}
}
// only save those preferences that are *different* from the default
if (userValue !== undefined && !compare(userValue, Twinkle.defaultConfig[pref.name])) {
newConfig[pref.name] = userValue;
}
});
});
var text =
'// twinkleoptions.js: Twinkleの個人設定ファイル\n' +
'//\n' +
'// 注意: Twinkleの設定を変更する最も簡単な方法は、\n' +
'// Twinkle設定パネルを使用することです。設定パネルは[[' + Morebits.pageNameNorm + ']]にあります。\n' +
'//\n' +
'// このファイルは自動的に生成されます。\n' +
'// (有効なJavaScriptの方法で構成パラメーターを変更する以外に)行った変更は、\n' +
'// 次にTwinkle設定パネルで「保存」をクリックしたときに上書きされます。\n' +
'// このファイルを直接変更する場合は、必ず正しいJavaScriptを使用してください。\n' +
// eslint-disable-next-line no-useless-concat
'// <no' + 'wiki>\n' +
'\n' +
'window.Twinkle.prefs = ';
text += JSON.stringify(newConfig, null, 2);
text +=
';\n' +
'\n' +
// eslint-disable-next-line no-useless-concat
'// </no' + 'wiki>\n' +
'// twinkleoptions.js の終わり\n';
pageobj.setPageText(text);
pageobj.setEditSummary('Twinkle個人設定を保存: [[:' + Morebits.pageNameNorm + ']]からの自動編集');
pageobj.setCreateOption('recreate');
pageobj.save(Twinkle.config.saveSuccess);
};
Twinkle.config.saveSuccess = function twinkleconfigSaveSuccess(pageobj) {
pageobj.getStatusElement().info('成功');
const noticebox = document.createElement('div');
noticebox.className = 'cdx-message cdx-message--success';
noticebox.style.fontSize = '100%';
noticebox.style.marginTop = '2em';
noticebox.innerHTML = '<p><b>Twinkle個人設定は保存されました。</b></p><p>変更を確認するには、<b>ブラウザのキャッシュを完全にクリアする</b>必要があります。 (手順については <a href="' + mw.util.getUrl('WP:BYPASS') + '" title="WP:BYPASS">WP:BYPASS</a> 参照</p>';
Morebits.Status.root.appendChild(noticebox);
var noticeclear = document.createElement('br');
noticeclear.style.clear = 'both';
Morebits.Status.root.appendChild(noticeclear);
};
Twinkle.addInitCallback(Twinkle.config.init);
}());
// </nowiki>
50xmv198ulb9zkvlislfmfkv5g4hsnb
769563
769556
2026-10-22T02:18:38Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at d9cd3461baca43ac99ac2c24fb76a3aae6ddada9:
769563
javascript
text/javascript
// <nowiki>
(function(){Twinkle.config={};Twinkle.config.watchlistEnums={yes:"ウォッチリストに追加 (無期限)",no:"ウォッチリストに追加しない",default:"サイトの設定に従う","1 week":"1週間ウォッチ","1 month":"1ヶ月ウォッチ","3 months":"3ヶ月ウォッチ","6 months":"6ヶ月ウォッチ"};Twinkle.config.commonSets={csdCriteria:{db:"その他 ({{即時削除|その他}})",g1:"G1",g2:"G2",g3:"G3",g4:"G4",g5:"G5",g6:"G6",g8:"G8",g9:"G9",g10:"G10",a1:"A1",u1:"U1",u2:"U2",u3:"U3","f1-2":"F1-2","f1-3":"F1-3","f1-4":"F1-4","f1-5":"F1-5","f1-6":"F1-6",f3:"F3",f5:"F5",f6:"F6",f7:"F7",f8:"F8",f9:"F9",c1:"C1",c3:"C3",c6:"C6","r1-1":"R1-1","r1-2":"R1-2","r1-3":"R1-3","r1-4":"R1-4","r2-1":"R2-1","r2-2":"R2-2","r2-3":"R2-3","r2-5":"R2-5","r2-6":"R2-6",r4:"R4"},csdCriteriaDisplayOrder:["db","g1","g2","g3","g4","g5","g6","g8","g9","g10","a1","u1","u2","u3","f1-2","f1-3","f1-4","f1-5","f1-6","f3","f5","f6","f7","f8","f9","c1","c3","c6","r1-1","r1-2","r1-3","r1-4","r2-1","r2-2","r2-3","r2-5","r2-6","r4"],csdAndImageDeletionCriteria:{db:"その他 ({{即時削除|その他}})",g1:"G1",g2:"G2",g3:"G3",g4:"G4",g5:"G5",g6:"G6",g8:"G8",g9:"G9",g10:"G10",a1:"A1",u1:"U1",u2:"U2",u3:"U3","f1-2":"F1-2","f1-3":"F1-3","f1-4":"F1-4","f1-5":"F1-5","f1-6":"F1-6",f3:"F3",f5:"F5",f6:"F6",f7:"F7",f8:"F8",f9:"F9",c1:"C1",c3:"C3",c6:"C6","r1-1":"R1-1","r1-2":"R1-2","r1-3":"R1-3","r1-4":"R1-4","r2-1":"R2-1","r2-2":"R2-2","r2-3":"R2-3","r2-5":"R2-5","r2-6":"R2-6",r4:"R4"},csdAndImageDeletionCriteriaDisplayOrder:["db","g1","g2","g3","g4","g5","g6","g8","g9","g10","a1","u1","u2","u3","f1-2","f1-3","f1-4","f1-5","f1-6","f3","f5","f6","f7","f8","f9","c1","c3","c6","r1-1","r1-2","r1-3","r1-4","r2-1","r2-2","r2-3","r2-5","r2-6","r4"],namespacesNoSpecial:{0:"標準",1:"ノート (標準)",2:"利用者",3:"利用者‐会話",4:"Wikipedia",5:"Wikipedia‐ノート",6:"ファイル",7:"ファイル‐ノート",8:"MediaWiki",9:"MediaWiki‐ノート",10:"Template",11:"Template‐ノート",12:"Help",13:"Help‐ノート",14:"Category",15:"Category‐ノート",100:"Portal",101:"Portal‐ノート",710:"TimedText",711:"TimedText‐ノート",828:"Module",829:"Module‐ノート"}};Twinkle.config.commonSets.csdCriteriaDisplayOrder=Object.keys(Twinkle.config.commonSets.csdCriteria);Twinkle.config.commonSets.csdAndImageDeletionCriteriaDisplayOrder=Object.keys(Twinkle.config.commonSets.csdAndImageDeletionCriteria);Twinkle.config.sections=[{title:"全般",module:"general",preferences:[{name:"userTalkPageMode",label:"利用者会話ページを開く時は次のページで開く",type:"enum",enumValues:{window:"ウィンドウ内で他の利用者会話ページを置き換える",tab:"新しいタブ",blank:"完全に新しいウィンドウ"}},{name:"dialogLargeFont",label:"Twinkleダイアログで大きな文字を使用",type:"boolean"},{name:"disabledModules",label:"選択したTwinkleモジュールをオフ",helptip:"チェックを入れた機能は無効になります。再び有効にするにはチェックを外してください。<br />既定でチェックが入っているものは準備中の機能です。無効にしていても、関連する設定項目は表示されます。",type:"set",setValues:{arv:"ARV",warn:"Warn",talkback:"Talkback",speedy:"CSD",xfd:"XfD",image:"DI",protect:"RPP",tag:"Tag",diff:"Diff",unlink:"Unlink",rollback:"Rollback"}},{name:"disabledSysopModules",label:"選択した管理者専用モジュールをオフ",helptip:"ここで選択したものは使用できないので、注意してください。再び有効にするにはチェックを外してください。",adminOnly:true,type:"set",setValues:{block:"Block",batchdelete:"D-batch",batchprotect:"P-batch",batchundelete:"Und-batch"}}]},{title:"ARV: 荒らしの報告",module:"arv",preferences:[{name:"spiWatchReport",label:"多重アカウントの調査依頼ページをウォッチリストに追加",type:"enum",enumValues:Twinkle.config.watchlistEnums}]},{title:"Block: 利用者のブロック",module:"block",adminOnly:true,preferences:[{name:"defaultToBlock64",label:"IPv6アドレスでは、既定で/64の範囲をブロック",type:"boolean"},{name:"defaultToPartialBlocks",label:"ブロックメニューを開いた際、既定で部分ブロックを選択",helptip:"利用者がすでにブロックされている場合は、現在のブロックの種類が優先されます。",type:"boolean"},{name:"blankTalkpageOnIndefBlock",label:"無期限ブロックの通知時に利用者会話ページを白紙化",helptip:'詳しくは<a href="'+mw.util.getUrl("Wikipedia:WikiProject_User_warnings/Usage_and_layout#Indefinitely_blocked_users")+'">WP:UWUL</a>を参照してください。',type:"boolean"}]},{title:"DI: 画像削除",module:"image",preferences:[{name:"notifyUserOnDeli",label:'既定で"アップロード者に通知"にチェックを入れる',type:"boolean"},{name:"deliWatchPage",label:"タグ付け時にファイルページをウォッチリストに追加",type:"enum",enumValues:Twinkle.config.watchlistEnums},{name:"deliWatchUser",label:"通知時に、アップロード者の会話ページをウォッチリストに追加",type:"enum",enumValues:Twinkle.config.watchlistEnums}]},{title:(Morebits.userIsSysop?"PP":"RPP")+": ページの保護",module:"protect",preferences:[{name:"watchRequestedPages",label:"保護依頼時にページをウォッチリストに追加",type:"enum",enumValues:Twinkle.config.watchlistEnums},{name:"watchPPTaggedPages",label:"保護テンプレートでページをタグ付けする際、ウォッチリストにページを追加",type:"enum",enumValues:Twinkle.config.watchlistEnums},{name:"watchProtectedPages",label:"保護する際にページをウォッチリストに追加",helptip:"ページ保護後にタグ付けも行う場合は、そちらが優先されます。",adminOnly:true,type:"enum",enumValues:Twinkle.config.watchlistEnums}]},{title:"Rollback: 差し戻しと巻き戻し",module:"rollback",preferences:[{name:"autoMenuAfterRollback",label:"Twinkle差し戻し後、利用者会話ページでTwinkle警告メニューを自動的に開く",helptip:"以下の該当するボックスにチェックが入っている場合のみ動作します。",type:"boolean"},{name:"openTalkPage",label:"以下の種類の差し戻し後に、会話ページを開く",type:"set",setValues:{norm:"通常の差し戻し",vand:"荒らしの差し戻し"}},{name:"openTalkPageOnAutoRevert",label:"投稿記録や最近の更新から差し戻しを実行した際に、会話ページを開く",helptip:"これが有効になっている場合、前の設定で必要なオプションが有効になっていなければ機能しません。",type:"boolean"},{name:"rollbackInPlace",label:"投稿記録や最近の更新から差し戻しする際、ページを更新しない",helptip:"これが有効になっている場合、Twinkleは差し戻し後に投稿記録や最近の更新フィードを再読み込みしないため、一度に複数の編集を差し戻すことができます。",type:"boolean"},{name:"markRevertedPagesAsMinor",label:"以下の種類の差し戻しは細部の編集として行う",type:"set",setValues:{norm:"通常の差し戻し",vand:"荒らしの差し戻し",torev:"版の復元"}},{name:"watchRevertedPages",label:"以下の種類の差し戻しの際、ページをウォッチリストに追加",type:"set",setValues:{norm:"通常の差し戻し",vand:"荒らしの差し戻し",torev:"版の復元"}},{name:"watchRevertedExpiry",label:"ページを差し戻した際、ウォッチする期間",type:"enum",enumValues:Twinkle.config.watchlistEnums},{name:"offerReasonOnNormalRevert",label:"通常の差し戻しに理由の入力を求めるプロンプトを表示",helptip:'"通常の"差し戻しは [差し戻し] リンクから実行される差し戻しです。',type:"boolean"},{name:"confirmOnRollback",label:"差し戻す前に確認を求める (すべての機器)",helptip:"ペンやタッチデバイスを使う人、慢性的に優柔不断な人向け。",type:"boolean"},{name:"confirmOnMobileRollback",label:"差し戻す前に確認を求める (携帯機器のみ)",helptip:"携帯機器での誤動作を防ぐ。",type:"boolean"},{name:"showRollbackLinks",label:"これらのページで差し戻しリンクを表示",type:"set",setValues:{diff:"差分ページ",others:"他の利用者の投稿記録ページ",mine:"自分の投稿記録ページ",recent:"最近の更新や関連ページの更新の特別ページ",history:"履歴ページ"}},{name:"SummaryOnVandalRevert",label:"荒らしの差し戻しの際の要約欄をカスタム",helptip:"このボックスに要約を入力します。",type:"string",obsolete:"vandusersummary"}]},{title:"CSD: 即時削除",module:"speedy",preferences:[{name:"speedySelectionStyle",label:"ページをタグ付け/削除するタイミング",type:"enum",enumValues:{buttonClick:"送信ボタンをクリックしたとき",radioClick:"オプションをクリックするとすぐ"}},{name:"watchSpeedyPages",label:"これらの基準を使用する場合は、ウォッチリストにページを追加",type:"set",setValues:Twinkle.config.commonSets.csdCriteria,setDisplayOrder:Twinkle.config.commonSets.csdCriteriaDisplayOrder},{name:"watchSpeedyExpiry",label:"ページをタグ付けする場合、それをウォッチする期間",type:"enum",enumValues:Twinkle.config.watchlistEnums},{name:"markSpeedyPagesAsPatrolled",label:"タグ付けの際、ページを巡回済みとしてマーク(可能な場合)",helptip:"これは最善の慣行に反するおそれがあるので、おそらくチェックすべきではないです。",type:"boolean"},{name:"promptForSpeedyDeletionSummary",label:"これらの基準で削除する場合、削除要約の編集を行う",adminOnly:true,type:"set",setValues:Twinkle.config.commonSets.csdAndImageDeletionCriteria,setDisplayOrder:Twinkle.config.commonSets.csdAndImageDeletionCriteriaDisplayOrder},{name:"deleteTalkPageOnDelete",label:"既定で「トークページも削除する」にチェックを入れる",adminOnly:true,type:"boolean"},{name:"deleteRedirectsOnDelete",label:"既定で「リダイレクトも削除」にチェックを入れる",adminOnly:true,type:"boolean"},{name:"deleteSysopDefaultToDelete",label:"既定でタグ付けではなく即時削除を行う",helptip:"CSDタグが既に存在する場合、Twinkleは常に「削除」モードが既定となります。",adminOnly:true,type:"boolean"},{name:"speedyWindowWidth",label:"即時削除ウィンドウの幅(ピクセル)",type:"integer"},{name:"speedyWindowHeight",label:"即時削除ウィンドウの高さ(ピクセル)",helptip:"大きなモニターをお持ちの場合は、これを増やしてください。",type:"integer"},{name:"logSpeedyNominations",label:"すべてのCSDへの指定記録を利用者ページに保存",helptip:"管理者以外は削除された投稿にアクセスできないため、利用者ページでの記録は、Twinkleを使用してCSDに指定したすべてのページを追跡するための優れた方法になります。DIを使用してタグ付けされたファイルもこの記録ページに追加されます。",type:"boolean"},{name:"speedyLogPageName",label:"この利用者サブページでCSDへの指定記録を保存",helptip:"このボックスにサブページ名を入力します。 CSD記録は利用者:"+mw.config.get("wgUserName")+'/"サブページ名"にあります。 CSD利用者ページへの記録をオンにした場合にのみ機能します。',type:"string"},{name:"noLogOnSpeedyNomination",label:"これらの基準でタグ付けする場合は、利用者サブページでの記録を作成しない",type:"set",setValues:Twinkle.config.commonSets.csdAndImageDeletionCriteria,setDisplayOrder:Twinkle.config.commonSets.csdAndImageDeletionCriteriaDisplayOrder}]},{title:"Tag: メンテナンス用テンプレートの付与",module:"tag",preferences:[{name:"watchTaggedVenues",label:"次の種類のページにテンプレートを付与した際、ウォッチリストに追加",type:"set",setValues:{articles:"記事",drafts:"下書き",redirects:"リダイレクト",files:"ファイル"}},{name:"watchTaggedPages",label:"テンプレートを付与したページのウォッチ期間",type:"enum",enumValues:Twinkle.config.watchlistEnums},{name:"watchMergeDiscussions",label:"統合提案を開始した際、会話ページをウォッチリストに追加",type:"enum",enumValues:Twinkle.config.watchlistEnums},{name:"markTaggedPagesAsMinor",label:"テンプレートの付与を細部の編集として扱う",type:"boolean"},{name:"markTaggedPagesAsPatrolled",label:"既定で「ページを巡回済み・確認済みにする」にチェックを入れる",type:"boolean"},{name:"tagArticleSortOrder",label:"記事用テンプレートの既定の表示順",type:"enum",enumValues:{cat:"分類別",alpha:"アルファベット順"}},{name:"customTagList",label:"追加で表示する記事・下書きのメンテナンス用テンプレート",helptip:"テンプレート一覧の末尾に選択肢を追加します。Twinkleにまだ登録されていないメンテナンス用テンプレートなどを指定できます。",type:"customList",customListValueTitle:"テンプレート名(波括弧なし)",customListLabelTitle:"テンプレート付与画面に表示する名前"},{name:"customFileTagList",label:"追加で表示するファイルのメンテナンス用テンプレート",helptip:"ファイルに付与できるテンプレートを追加します。",type:"customList",customListValueTitle:"テンプレート名(波括弧なし)",customListLabelTitle:"テンプレート付与画面に表示する名前"},{name:"customRedirectTagList",label:"追加で表示するリダイレクト分類用テンプレート",helptip:"リダイレクトに付与できるテンプレートを追加します。",type:"customList",customListValueTitle:"テンプレート名(波括弧なし)",customListLabelTitle:"テンプレート付与画面に表示する名前"}]},{title:"Talkback: 返信通知",module:"talkback",preferences:[{name:"markTalkbackAsMinor",label:"返信通知を細部の編集として扱う",type:"boolean"},{name:"insertTalkbackSignature",label:"返信通知に署名を挿入",type:"boolean"},{name:"talkbackHeading",label:"返信通知と会話ページへの案内に使用する節見出し",tooltip:"ウィキテキストの見出しに使う等号(==)は入力しないでください。",type:"string"},{name:"mailHeading",label:"メール送信通知に使用する節見出し",tooltip:"ウィキテキストの見出しに使う等号(==)は入力しないでください。",type:"string"}]},{title:"Unlink: リンク解除",module:"unlink",preferences:[{name:"unlinkNamespaces",label:"リンク解除の対象にする名前空間",helptip:"会話ページの過去ログ内のリンクまで解除されるおそれがあるため、会話用の名前空間は選択しないでください。",type:"set",setValues:Twinkle.config.commonSets.namespacesNoSpecial}]},{title:"Warn: 利用者への警告",module:"warn",preferences:[{name:"defaultWarningGroup",label:"既定の警告レベル",type:"enum",enumValues:{1:"レベル1",2:"レベル2",3:"レベル3",4:"レベル4",5:"即時の最終警告(レベル4im)",6:"個別の問題に関する通知",7:"個別の問題に関する警告",9:"独自の警告",10:"すべての警告テンプレート",11:"レベルを自動選択(1~4)"}},{name:"combinedSingletMenus",label:"個別の問題に関する通知と警告のメニューをまとめる",helptip:"有効にすると、既定の警告レベルで個別の問題に関する通知または警告を選んでいる場合に、統合したメニューを既定で表示します。",type:"boolean"},{name:"watchWarnings",label:"通知した利用者の会話ページをウォッチリストに追加",type:"enum",enumValues:Twinkle.config.watchlistEnums},{name:"oldSelect",label:"検索機能のない従来の選択メニューを使用",type:"boolean"},{name:"customWarningList",label:"追加で表示する警告テンプレート",helptip:"テンプレートや利用者サブページを追加できます。追加した項目は警告画面の「独自の警告」に表示されます。",type:"customList",customListValueTitle:"テンプレート名(波括弧なし)",customListLabelTitle:"警告一覧に表示する名前(編集要約にも使用)"}]},{title:"XfD: 削除依頼",module:"xfd",preferences:[{name:"logXfdNominations",label:"削除依頼(XfD)を提出したページを利用者サブページに記録",helptip:"Twinkleで削除依頼を提出したページを、利用者サブページの記録から確認できます。",type:"boolean"},{name:"xfdLogPageName",label:"削除依頼の記録を保存する利用者サブページ名",helptip:"サブページ名を入力してください。記録先は「利用者:<i>利用者名</i>/<i>サブページ名</i>」です。削除依頼の記録を有効にしている場合にのみ機能します。",type:"string"},{name:"noLogOnXfdNomination",label:"次の手続きに提出する場合は利用者サブページに記録しない",type:"set",setValues:{afd:"記事の削除依頼 (AfD)",tfd:"テンプレートの削除・統合依頼 (TfD)",ffd:"ファイルの削除依頼 (FfD)",cfd:"カテゴリの削除・変更依頼 (CfD)",cfds:"カテゴリの即時変更依頼 (CfD/S)",mfd:"その他のページの削除依頼 (MfD)",rfd:"リダイレクトの削除・転送先変更依頼 (RfD)",rm:"改名依頼 (RM)"}},{name:"xfdWatchPage",label:"削除依頼の対象ページをウォッチリストに追加",type:"enum",enumValues:Twinkle.config.watchlistEnums},{name:"xfdWatchDiscussion",label:"削除依頼ページをウォッチリストに追加",helptip:"AfD・MfDでは個別の依頼ページ、TfD・CfD・RfD・FfDでは日付別の依頼一覧を対象にします。",type:"enum",enumValues:Twinkle.config.watchlistEnums},{name:"xfdWatchList",label:"日付別の依頼一覧や一覧ページをウォッチリストに追加(AfD・MfD)",helptip:"AfD・MfDにのみ適用します。AfDでは日付別の依頼一覧、MfDでは総合一覧ページに依頼ページが読み込まれます。",type:"enum",enumValues:Twinkle.config.watchlistEnums},{name:"xfdWatchUser",label:"通知した初版投稿者の会話ページをウォッチリストに追加",type:"enum",enumValues:Twinkle.config.watchlistEnums},{name:"xfdWatchRelated",label:"通知時にリダイレクト先のページをウォッチリストに追加",helptip:"RfDで、リダイレクト先の会話ページに通知する場合にのみ適用します。",type:"enum",enumValues:Twinkle.config.watchlistEnums},{name:"markXfdPagesAsPatrolled",label:"記事の削除依頼(AfD)への提出時にページを巡回済み・確認済みに(可能な場合)",type:"boolean"}]},{title:"詳細設定(非表示)",hidden:true,preferences:[{name:"revertMaxRevisions",type:"integer"},{name:"autolevelStaleDays",type:"integer"},{name:"batchMax",type:"integer",adminOnly:true},{name:"batchChunks",type:"integer",adminOnly:true}]}];Twinkle.config.init=function twinkleconfigInit(){if(mw.config.get("wgNamespaceNumber")===mw.config.get("wgNamespaceIds").user&&mw.config.get("wgTitle")==="Syunsyunminmin/Twinkle/Preferences"&&mw.config.get("wgAction")==="view"){if(!document.getElementById("twinkle-config")){return}document.getElementById("twinkle-config").removeAttribute("style");document.getElementById("twinkle-config-titlebar").removeAttribute("style");const contentdiv=document.getElementById("twinkle-config-content");contentdiv.textContent="";if(window.TwinkleConfig||window.FriendlyConfig){const contentnotice=document.createElement("div");contentnotice.className="plainlinks twinkle-ombox";contentnotice.innerHTML="<div>"+'<img alt="" src="https://upload.wikimedia.org/wikipedia/commons/3/38/Imbox_content.png" />'+"</div>"+"<div>"+"<p><big><b>ここで設定を変更する前に、</b>個人用JavaScriptから古いTwinkleとFriendlyの設定を削除してください。</big></p>"+'<p><a href="'+mw.util.getUrl("User:"+mw.config.get("wgUserName")+"/"+mw.config.get("skin")+".js",{action:"edit"})+'" target="_blank"><b>使用中の外装の個人用JavaScript</b></a>または<a href="'+mw.util.getUrl("User:"+mw.config.get("wgUserName")+"/common.js",{action:"edit"})+'" target="_blank"><b>common.js</b></a>を編集し、<code>TwinkleConfig</code>と<code>FriendlyConfig</code>を参照する設定行を削除してください。</p>'+"</div>";contentdiv.appendChild(contentnotice)}const toctable=document.createElement("div");toctable.className="toc";toctable.style.marginLeft="0.4em";const toctitle=document.createElement("div");toctitle.id="toctitle";var toch2=document.createElement("h2");toch2.textContent="目次 ";toctitle.appendChild(toch2);const toctoggle=document.createElement("span");toctoggle.className="toctoggle";toctoggle.appendChild(document.createTextNode("["));const toctogglelink=document.createElement("a");toctogglelink.className="internal";toctogglelink.setAttribute("href","#tw-tocshowhide");toctogglelink.textContent="非表示";toctoggle.appendChild(toctogglelink);toctoggle.appendChild(document.createTextNode("]"));toctitle.appendChild(toctoggle);toctable.appendChild(toctitle);const tocul=document.createElement("ul");toctogglelink.addEventListener("click",()=>{const $tocul=$(tocul);$tocul.toggle();if($tocul.find(":visible").length){toctogglelink.textContent="非表示"}else{toctogglelink.textContent="表示"}},false);toctable.appendChild(tocul);contentdiv.appendChild(toctable);const contentform=document.createElement("form");contentform.setAttribute("action","javascript:void(0)");contentform.setAttribute("autocomplete","off");contentform.addEventListener("submit",Twinkle.config.save,true);contentdiv.appendChild(contentform);const container=document.createElement("table");container.style.width="100%";contentform.appendChild(container);$(Twinkle.config.sections).each((sectionkey,section)=>{if(section.hidden||section.adminOnly&&!Morebits.userIsSysop){return true}const tocli=document.createElement("li");tocli.className="toclevel-1";const toca=document.createElement("a");toca.setAttribute("href","#"+section.module);toca.appendChild(document.createTextNode(section.title));tocli.appendChild(toca);tocul.appendChild(tocli);let row=document.createElement("tr");let cell=document.createElement("td");cell.setAttribute("colspan","3");const heading=document.createElement("h4");heading.style.borderBottom="1px solid gray";heading.style.marginTop="0.2em";heading.id=section.module;heading.appendChild(document.createTextNode(section.title));cell.appendChild(heading);row.appendChild(cell);container.appendChild(row);let rowcount=1;$(section.preferences).each((prefkey,pref)=>{if(pref.adminOnly&&!Morebits.userIsSysop){return true}row=document.createElement("tr");row.style.marginBottom="0.2em";if(rowcount++%2===0){row.style.backgroundColor="rgba(128, 128, 128, 0.1)"}cell=document.createElement("td");var label,input,gotPref=Twinkle.getPref(pref.name),obsoletePref=Twinkle.getPref(pref.obsolete);switch(pref.type){case"boolean":cell.setAttribute("colspan","2");label=document.createElement("label");input=document.createElement("input");input.setAttribute("type","checkbox");input.setAttribute("id",pref.name);input.setAttribute("name",pref.name);if(gotPref===true||obsoletePref===true){input.setAttribute("checked","checked")}label.appendChild(input);label.appendChild(document.createTextNode(pref.label));cell.appendChild(label);break;case"string":case"integer":cell.style.textAlign="right";cell.style.paddingRight="0.5em";label=document.createElement("label");label.setAttribute("for",pref.name);label.appendChild(document.createTextNode(pref.label+":"));cell.appendChild(label);row.appendChild(cell);cell=document.createElement("td");cell.style.paddingRight="1em";input=document.createElement("input");input.setAttribute("type","text");input.setAttribute("id",pref.name);input.setAttribute("name",pref.name);if(pref.type==="integer"){input.setAttribute("size",6);input.setAttribute("type","number");input.setAttribute("step","1")}if(gotPref){input.setAttribute("value",gotPref)}if(obsoletePref){input.setAttribute("value",obsoletePref)}cell.appendChild(input);break;case"enum":cell.style.textAlign="right";cell.style.paddingRight="0.5em";label=document.createElement("label");label.setAttribute("for",pref.name);label.appendChild(document.createTextNode(pref.label+":"));cell.appendChild(label);row.appendChild(cell);cell=document.createElement("td");cell.style.paddingRight="1em";input=document.createElement("select");input.setAttribute("id",pref.name);input.setAttribute("name",pref.name);$.each(pref.enumValues,(enumvalue,enumdisplay)=>{const option=document.createElement("option");option.setAttribute("value",enumvalue);if(gotPref===enumvalue||obsoletePref===enumvalue||(typeof gotPref==="boolean"||typeof obsoletePref==="boolean")&&((gotPref||obsoletePref)&&enumvalue==="yes"||!(gotPref&&obsoletePref)&&enumvalue==="no")){option.setAttribute("selected","selected")}option.appendChild(document.createTextNode(enumdisplay));input.appendChild(option)});cell.appendChild(input);break;case"set":cell.setAttribute("colspan","2");label=document.createElement("label");label.appendChild(document.createTextNode(pref.label+":"));cell.appendChild(label);var checkdiv=document.createElement("div");checkdiv.style.paddingLeft="1em";var worker=function(itemkey,itemvalue){const checklabel=document.createElement("label");checklabel.style.marginRight="0.7em";checklabel.style.display="inline-block";const check=document.createElement("input");check.setAttribute("type","checkbox");check.setAttribute("id",pref.name+"_"+itemkey);check.setAttribute("name",pref.name+"_"+itemkey);if(gotPref&&gotPref.includes(itemkey)||obsoletePref&&obsoletePref.includes(itemkey)){check.setAttribute("checked","checked")}if(pref.name==="unlinkNamespaces"){if(gotPref&&gotPref.includes(parseInt(itemkey,10))||obsoletePref&&obsoletePref.includes(parseInt(itemkey,10))){check.setAttribute("checked","checked")}}checklabel.appendChild(check);checklabel.appendChild(document.createTextNode(itemvalue));checkdiv.appendChild(checklabel)};if(pref.setDisplayOrder){$.each(pref.setDisplayOrder,(itemkey,item)=>{worker(item,pref.setValues[item])})}else{$.each(pref.setValues,worker)}cell.appendChild(checkdiv);break;case"customList":cell.style.textAlign="right";cell.style.paddingRight="0.5em";label=document.createElement("label");label.setAttribute("for",pref.name);label.appendChild(document.createTextNode(pref.label+":"));cell.appendChild(label);row.appendChild(cell);cell=document.createElement("td");cell.style.paddingRight="1em";var button=document.createElement("button");button.setAttribute("id",pref.name);button.setAttribute("name",pref.name);button.setAttribute("type","button");button.addEventListener("click",Twinkle.config.listDialog.display,false);$(button).data({value:obsoletePref||gotPref,pref:pref});button.appendChild(document.createTextNode("アイテムを編集"));cell.appendChild(button);break;default:alert("twinkleconfig: 設定 "+pref.name+" の不明なデータ型");break}row.appendChild(cell);cell=document.createElement("td");cell.className="twinkle-config-helptip";if(pref.helptip){cell.innerHTML=pref.helptip.replace(/{{(.+?)}}/g,'{{<a href="'+mw.util.getUrl("Template:")+'$1" target="_blank">$1</a>}}')}if(pref.type!=="customList"){const resetlink=document.createElement("a");resetlink.setAttribute("href","#tw-reset");resetlink.setAttribute("id","twinkle-config-reset-"+pref.name);resetlink.addEventListener("click",Twinkle.config.resetPrefLink,false);resetlink.style.cssFloat="right";resetlink.style.margin="0 0.6em";resetlink.appendChild(document.createTextNode("初期化"));cell.appendChild(resetlink)}row.appendChild(cell);container.appendChild(row);return true});return true});const footerbox=document.createElement("div");footerbox.setAttribute("id","twinkle-config-buttonpane");const button=document.createElement("button");button.setAttribute("id","twinkle-config-submit");button.setAttribute("type","submit");button.appendChild(document.createTextNode("変更を保存"));footerbox.appendChild(button);const footerspan=document.createElement("span");footerspan.className="plainlinks";footerspan.style.marginLeft="2.4em";footerspan.style.fontSize="90%";const footera=document.createElement("a");footera.setAttribute("href","#tw-reset-all");footera.setAttribute("id","twinkle-config-resetall");footera.addEventListener("click",Twinkle.config.resetAllPrefs,false);footera.appendChild(document.createTextNode("デフォルトに戻す"));footerspan.appendChild(footera);footerbox.appendChild(footerspan);contentform.appendChild(footerbox);if(window.location.hash){const loc=window.location.hash;window.location.hash="";window.location.hash=loc}}else if(mw.config.get("wgNamespaceNumber")===mw.config.get("wgNamespaceIds").user&&mw.config.get("wgTitle").indexOf(mw.config.get("wgUserName"))===0&&mw.config.get("wgPageName").slice(-3)===".js"){const box=document.createElement("div");box.setAttribute("id","twinkle-config-headerbox");let link;const scriptPageName=mw.config.get("wgPageName").slice(mw.config.get("wgPageName").lastIndexOf("/")+1,mw.config.get("wgPageName").lastIndexOf(".js"));if(scriptPageName==="twinkleoptions"){box.setAttribute("class","config-twopt-box");link=document.createElement("a");link.setAttribute("href",mw.util.getUrl(mw.config.get("wgFormattedNamespaces")[mw.config.get("wgNamespaceIds").user]+":Syunsyunminmin/Twinkle/Preferences"));link.appendChild(document.createTextNode("Twinkle個人設定パネル"));if(mw.config.get("wgArticleId")>0){box.appendChild(document.createTextNode("このページにはTwinkleの設定が含まれています。設定は "));box.appendChild(link);box.appendChild(document.createTextNode("を使用するか、このページを編集することで変更できます。"))}else{box.appendChild(link);box.appendChild(document.createTextNode("またはこのページを編集することで、Twinkleを個人の好みに合わせてカスタマイズできます。"))}$(box).insertAfter($("#contentSub"))}else if(["monobook","vector","vector-2022","cologneblue","modern","timeless","minerva","common"].includes(scriptPageName)){box.setAttribute("class","config-userskin-box");box.appendChild(document.createTextNode("Twinkle個人設定を設定したい場合、"));link=document.createElement("a");link.setAttribute("href",mw.util.getUrl(mw.config.get("wgFormattedNamespaces")[mw.config.get("wgNamespaceIds").user]+":Syunsyunminmin/Twinkle/Preferences"));link.appendChild(document.createTextNode("Twinkle個人設定パネル"));box.appendChild(link);box.appendChild(document.createTextNode("を使用して行えます。"));$(box).insertAfter($("#contentSub"))}}};Twinkle.config.listDialog={};Twinkle.config.listDialog.addRow=function twinkleconfigListDialogAddRow($dlgtable,value,label){let $contenttr,$valueInput,$labelInput;$dlgtable.append($contenttr=$("<tr>").append($("<td>").append($("<button>").attr("type","button").css("white-space","nowrap").on("click",()=>{$contenttr.remove()}).text("除去")),$("<td>").append($valueInput=$("<input>").attr("type","text").addClass("twinkle-config-customlist-value").css("width","97%")),$("<td>").append($labelInput=$("<input>").attr("type","text").addClass("twinkle-config-customlist-label").css("width","98%"))));if(value){$valueInput.val(value)}if(label){$labelInput.val(label)}};Twinkle.config.listDialog.display=function twinkleconfigListDialogDisplay(e){const $prefbutton=$(e.target);const curvalue=$prefbutton.data("value");const curpref=$prefbutton.data("pref");const dialog=new Morebits.SimpleWindow(720,400);dialog.setTitle(curpref.label);dialog.setScriptName("Twinkle個人設定");let $dlgtbody;dialog.setContent($("<div>").append($("<table>").addClass("wikitable").css({margin:"1.4em 1em",width:"auto"}).append($dlgtbody=$("<tbody>").append($("<tr>").append($("<th>").css("width","5%"),$("<th>").css("width","35%").text(curpref.customListValueTitle?curpref.customListValueTitle:"値"),$("<th>").css("width","60%").text(curpref.customListLabelTitle?curpref.customListLabelTitle:"表示名"))),$("<tfoot>").append($("<tr>").append($("<td>").attr("colspan","3").append($("<button>").text("追加").css("min-width","8em").attr("type","button").on("click",()=>{Twinkle.config.listDialog.addRow($dlgtbody)}))))),$("<button>").text("変更を保存").attr("type","submit").on("click",()=>{Twinkle.config.listDialog.save($prefbutton,$dlgtbody);dialog.close()}),$("<button>").text("初期化").attr("type","submit").on("click",()=>{Twinkle.config.listDialog.reset($prefbutton,$dlgtbody)}),$("<button>").text("キャンセル").attr("type","submit").on("click",()=>{dialog.close()}))[0]);let gotRow=false;$.each(curvalue,(k,v)=>{gotRow=true;Twinkle.config.listDialog.addRow($dlgtbody,v.value,v.label)});if(!gotRow){Twinkle.config.listDialog.addRow($dlgtbody)}dialog.display()};Twinkle.config.listDialog.reset=function twinkleconfigListDialogReset($button,$tbody){const curpref=$button.data("pref");const oldvalue=$button.data("value");Twinkle.config.resetPref(curpref);$tbody.find("tr").slice(1).remove();const curvalue=$button.data("value");$.each(curvalue,(k,v)=>{Twinkle.config.listDialog.addRow($tbody,v.value,v.label)});$button.data("value",oldvalue)};Twinkle.config.listDialog.save=function twinkleconfigListDialogSave($button,$tbody){const result=[];let current={};$tbody.find('input[type="text"]').each((inputkey,input)=>{if($(input).hasClass("twinkle-config-customlist-value")){current={value:input.value}}else{current.label=input.value;if(current.value||current.label){result.push(current)}}});$button.data("value",result)};Twinkle.config.resetPrefLink=function twinkleconfigResetPrefLink(e){const wantedpref=e.target.id.slice(21);$(Twinkle.config.sections).each((sectionkey,section)=>{if(section.hidden||section.adminOnly&&!Morebits.userIsSysop){return true}let foundit=false;$(section.preferences).each((prefkey,pref)=>{if(pref.name!==wantedpref){return true}Twinkle.config.resetPref(pref);foundit=true;return false});if(foundit){return false}});return false};Twinkle.config.resetPref=function twinkleconfigResetPref(pref){switch(pref.type){case"boolean":document.getElementById(pref.name).checked=Twinkle.defaultConfig[pref.name];break;case"string":case"integer":case"enum":document.getElementById(pref.name).value=Twinkle.defaultConfig[pref.name];break;case"set":$.each(pref.setValues,itemkey=>{if(document.getElementById(pref.name+"_"+itemkey)){document.getElementById(pref.name+"_"+itemkey).checked=Twinkle.defaultConfig[pref.name].includes(itemkey)}});break;case"customList":$(document.getElementById(pref.name)).data("value",Twinkle.defaultConfig[pref.name]);break;default:alert("twinkleconfig: 設定 "+pref.name+" の不明なデータ型");break}};Twinkle.config.resetAllPrefs=function twinkleconfigResetAllPrefs(){$(Twinkle.config.sections).each((sectionkey,section)=>{if(section.hidden||section.adminOnly&&!Morebits.userIsSysop){return true}$(section.preferences).each((prefkey,pref)=>{if(!pref.adminOnly||Morebits.userIsSysop){Twinkle.config.resetPref(pref)}});return true});return false};Twinkle.config.save=function twinkleconfigSave(e){Morebits.Status.init(document.getElementById("twinkle-config-content"));var userjs=mw.config.get("wgFormattedNamespaces")[mw.config.get("wgNamespaceIds").user]+":"+mw.config.get("wgUserName")+"/twinkleoptions.js";var wikipedia_page=new Morebits.wiki.Page(userjs,userjs+"に設定を保存");wikipedia_page.setCallbackParameters(e.target);wikipedia_page.load(Twinkle.config.writePrefs);return false};Twinkle.config.writePrefs=function twinkleconfigWritePrefs(pageobj){const form=pageobj.getCallbackParameters();const newConfig={optionsVersion:2.1};const compare=function(a,b){if(Array.isArray(a)){if(a.length!==b.length){return false}const asort=a.sort(),bsort=b.sort();for(let i=0;asort[i];++i){if(typeof asort[i]==="object"&&(asort[i].label!==bsort[i].label||asort[i].value!==bsort[i].value)){return false}else if(asort[i].toString()!==bsort[i].toString()){return false}}return true}return a===b};$(Twinkle.config.sections).each((sectionkey,section)=>{if(section.adminOnly&&!Morebits.userIsSysop){return}$(section.preferences).each((prefkey,pref)=>{let userValue;if(!pref.adminOnly||Morebits.userIsSysop){if(!section.hidden){switch(pref.type){case"boolean":userValue=form[pref.name].checked;break;case"string":case"enum":userValue=form[pref.name].value;break;case"integer":userValue=parseInt(form[pref.name].value,10);if(isNaN(userValue)){Morebits.Status.warn("保存",pref.name+"に対して指定した値 ("+pref.value+") は無効です。保存は続行しますが、無効なデータ値は飛ばされます。");userValue=null}break;case"set":userValue=[];if(pref.setDisplayOrder){$.each(pref.setDisplayOrder,(itemkey,item)=>{if(form[pref.name+"_"+item].checked){userValue.push(item)}})}else{$.each(pref.setValues,itemkey=>{if(form[pref.name+"_"+itemkey].checked){userValue.push(itemkey)}})}break;case"customList":userValue=$(form[pref.name]).data("value");break;default:alert("twinkleconfig: 設定 "+pref.name+" の不明なデータ型");break}}else if(Twinkle.prefs){userValue=Twinkle.prefs[pref.name]}}if(userValue!==undefined&&!compare(userValue,Twinkle.defaultConfig[pref.name])){newConfig[pref.name]=userValue}})});var text="// twinkleoptions.js: Twinkleの個人設定ファイル\n"+"//\n"+"// 注意: Twinkleの設定を変更する最も簡単な方法は、\n"+"// Twinkle設定パネルを使用することです。設定パネルは[["+Morebits.pageNameNorm+"]]にあります。\n"+"//\n"+"// このファイルは自動的に生成されます。\n"+"// (有効なJavaScriptの方法で構成パラメーターを変更する以外に)行った変更は、\n"+"// 次にTwinkle設定パネルで「保存」をクリックしたときに上書きされます。\n"+"// このファイルを直接変更する場合は、必ず正しいJavaScriptを使用してください。\n"+"// <no"+"wiki>\n"+"\n"+"window.Twinkle.prefs = ";text+=JSON.stringify(newConfig,null,2);text+=";\n"+"\n"+"// </no"+"wiki>\n"+"// twinkleoptions.js の終わり\n";pageobj.setPageText(text);pageobj.setEditSummary("Twinkle個人設定を保存: [[:"+Morebits.pageNameNorm+"]]からの自動編集");pageobj.setCreateOption("recreate");pageobj.save(Twinkle.config.saveSuccess)};Twinkle.config.saveSuccess=function twinkleconfigSaveSuccess(pageobj){pageobj.getStatusElement().info("成功");const noticebox=document.createElement("div");noticebox.className="cdx-message cdx-message--success";noticebox.style.fontSize="100%";noticebox.style.marginTop="2em";noticebox.innerHTML='<p><b>Twinkle個人設定は保存されました。</b></p><p>変更を確認するには、<b>ブラウザのキャッシュを完全にクリアする</b>必要があります。 (手順については <a href="'+mw.util.getUrl("WP:BYPASS")+'" title="WP:BYPASS">WP:BYPASS</a> 参照</p>';Morebits.Status.root.appendChild(noticebox);var noticeclear=document.createElement("br");noticeclear.style.clear="both";Morebits.Status.root.appendChild(noticeclear)};Twinkle.addInitCallback(Twinkle.config.init)})();
// </nowiki>
fh9gqyvpele0uzidc7nvgudk3hpv2wb
User:Syunsyunminmin/Twinkle/twinklespeedy.js
2
179396
769461
2026-10-21T13:57:58Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 3680108:
769461
javascript
text/javascript
/*
Twinkle: adapted for Japanese Wikipedia; upstream authors and Japanese adaptations are credited in UPSTREAM.md.
Upstream source: https://github.com/wikimedia-gadgets/twinkle/blob/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/src/modules/twinklespeedy.js
Upstream author history: https://github.com/wikimedia-gadgets/twinkle/commits/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/src/modules/twinklespeedy.js
License: CC BY-SA 3.0 Unported. See https://github.com/wikimedia-gadgets/twinkle/blob/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/LICENSE.txt
Japanese adaptation: Syunsyunminmin; includes later synchronization changes.
*/
// <nowiki>
(function() {
/*
****************************************
*** twinklespeedy.js: CSD module
****************************************
* Mode of invocation: Tab ("CSD")
* Active on: Non-special, existing pages
*
* NOTE FOR DEVELOPERS:
* If adding a new criterion, add it to the appropriate places at the top of
* twinkleconfig.js. Also check out the default values of the CSD preferences
* in twinkle.js, and add your new criterion to those if you think it would be
* good.
*/
Twinkle.speedy = function twinklespeedy() {
// Disable on:
// * special pages
// * non-existent pages
if (mw.config.get('wgNamespaceNumber') < 0 || !mw.config.get('wgArticleId')) {
return;
}
Twinkle.addPortletLink(Twinkle.speedy.callback, 'CSD', 'tw-csd', Morebits.userIsSysop ? 'WP:CSDに従いページを削除する' : 'WP:CSDに従い即時削除を依頼');
};
// This function is run when the CSD tab/header link is clicked
Twinkle.speedy.callback = function twinklespeedyCallback() {
Twinkle.speedy.initDialog(Morebits.userIsSysop ? Twinkle.speedy.callback.evaluateSysop : Twinkle.speedy.callback.evaluateUser, true);
};
// Used by unlink feature
Twinkle.speedy.dialog = null;
// Used throughout
Twinkle.speedy.hasCSD = !!$('#delete-reason').length;
// Prepares the speedy deletion dialog and displays it
Twinkle.speedy.initDialog = function twinklespeedyInitDialog(callbackfunc) {
var dialog;
Twinkle.speedy.dialog = new Morebits.SimpleWindow(Twinkle.getPref('speedyWindowWidth'), Twinkle.getPref('speedyWindowHeight'));
dialog = Twinkle.speedy.dialog;
dialog.setTitle('即時削除の基準を選択する');
dialog.setScriptName('Twinkle');
dialog.addFooterLink('即時削除の方針', 'WP:CSD');
dialog.addFooterLink('CSD設定', 'User:Syunsyunminmin/Twinkle/Preferences#speedy');
dialog.addFooterLink('Twinkle help', 'WP:TW/DOC#speedy');
dialog.addFooterLink('フィードバック', 'User talk:Syunsyunminmin/Twinkle');
var form = new Morebits.QuickForm(callbackfunc, Twinkle.getPref('speedySelectionStyle') === 'radioClick' ? 'change' : null);
if (Morebits.userIsSysop) {
form.append({
type: 'checkbox',
list: [
{
label: 'タグ付けのみ行い削除しない',
value: 'tag_only',
name: 'tag_only',
tooltip: '今すぐページを削除するのではなく、タグ付けだけしたい場合',
checked: !(Twinkle.speedy.hasCSD || mw.config.get('wgRelevantUserName') === mw.config.get('wgUserName') || Twinkle.getPref('deleteSysopDefaultToDelete')),
event: function(event) {
var cForm = event.target.form;
var cChecked = event.target.checked;
// enable talk page checkbox
if (cForm.talkpage) {
cForm.talkpage.checked = !cChecked && Twinkle.getPref('deleteTalkPageOnDelete');
}
// enable redirects checkbox
cForm.redirects.checked = !cChecked && Twinkle.getPref('deleteRedirectsOnDelete');
Twinkle.speedy.callback.modeChanged(cForm);
event.stopPropagation();
}
}
]
});
var deleteOptions = form.append({
type: 'div',
name: 'delete_options'
});
deleteOptions.append({
type: 'header',
label: '削除関係のオプション'
});
if (mw.config.get('wgNamespaceNumber') % 2 === 0 && (mw.config.get('wgNamespaceNumber') !== 2 || (/\//).test(mw.config.get('wgTitle')))) { // hide option for user pages, to avoid accidentally deleting user talk page
deleteOptions.append({
type: 'checkbox',
list: [
{
label: 'トークページも削除',
value: 'talkpage',
name: 'talkpage',
tooltip: 'このオプションはこのページのトークページも削除します。日本語版には対応する方針がないため基本的にはチェックしないでください。',
checked: Twinkle.getPref('deleteTalkPageOnDelete'),
event: function(event) {
event.stopPropagation();
}
}
]
});
}
deleteOptions.append({
type: 'checkbox',
list: [
{
label: 'リダイレクトも削除',
value: 'redirects',
name: 'redirects',
tooltip: 'このオプションはこのページへのすべてのリダイレクトも削除します。転送先を変更できるかもしれないリダイレクトも削除してしまう可能性があります。',
checked: Twinkle.getPref('deleteRedirectsOnDelete'),
event: function(event) {
event.stopPropagation();
}
}
]
});
}
var tagOptions = form.append({
type: 'div',
name: 'tag_options'
});
if (Morebits.userIsSysop) {
tagOptions.append({
type: 'header',
label: '削除関係のオプション'
});
}
tagOptions.append({
type: 'checkbox',
list: [
{
label: 'ベージを白紙化する',
value: 'blankpage',
name: 'blankpage',
tooltip: '削除タグを貼り付ける前にページを白紙化します。',
event: function(event) {
event.stopPropagation();
}
},
{
label: 'コメントを追加する',
value: 'add_comments',
name: 'add_comments',
tooltip: 'コメントを追加する',
subgroup: {
name: 'additional_comments',
type: 'input',
label: 'コメント:',
size: 60
},
event: function(event) {
event.stopPropagation();
}
},
{
label: 'ユーザーログを残さない',
value: 'notsavelog',
name: 'notsavelog',
tooltip: 'Twinkle設定でユーザーログを有効にしている場合、この項目にチェックすることでユーザーログを残さないようにします。' +
'不適切な記事名など、ログを保持したくない場合に使用してください。',
event: function(event) {
event.stopPropagation();
}
}
]
});
form.append({
type: 'div',
id: 'prior-deletion-count',
style: 'font-style: italic'
});
form.append({
type: 'div',
name: 'work_area',
label: 'CSDモジュールの初期化に失敗しました。もう一度試すか、Twinkleの開発者に問題を伝えてください。'
});
if (Twinkle.getPref('speedySelectionStyle') !== 'radioClick') {
form.append({ type: 'submit', className: 'tw-speedy-submit' }); // Renamed in modeChanged
}
var result = form.render();
dialog.setContent(result);
dialog.display();
Twinkle.speedy.callback.modeChanged(result);
// Check for prior deletions. Just once, upon init
Twinkle.speedy.callback.priorDeletionCount();
};
Twinkle.speedy.callback.modeChanged = function twinklespeedyCallbackModeChanged(form) {
var namespace = mw.config.get('wgNamespaceNumber');
// first figure out what mode we're in
var mode = {
isSysop: !!form.tag_only && !form.tag_only.checked,
isRadioClick: Twinkle.getPref('speedySelectionStyle') === 'radioClick'
};
if (mode.isSysop) {
$('[name=delete_options]').show();
$('[name=tag_options]').hide();
$('button.tw-speedy-submit').text('削除');
} else {
$('[name=delete_options]').hide();
$('[name=tag_options]').show();
$('button.tw-speedy-submit').text('タグ付け');
}
var work_area = new Morebits.QuickForm.Element({
type: 'div',
name: 'work_area'
});
var appendList = function(headerLabel, csdList) {
work_area.append({ type: 'header', label: headerLabel });
work_area.append({ type: 'radio', name: 'csd', list: Twinkle.speedy.generateCsdList(csdList, mode) });
};
if (mode.isSysop) {
appendList('その他の基準', Twinkle.speedy.customRationale);
}
if (!Morebits.isPageRedirect()) {
switch (namespace) {
case 0: // article
case 1: // talk
appendList('記事', Twinkle.speedy.articleList);
break;
case 2: // user
case 3: // user talk
appendList('利用者ページ', Twinkle.speedy.userList);
break;
case 6: // file
case 7: // file talk
appendList('ファイル', Twinkle.speedy.fileList);
if (!mode.isSysop) {
work_area.append({ type: 'div', label: '{{No source}}または{{No license}}のタグ付けは、Twinkleの "DI "タブを使って行うことができます。' });
}
break;
case 14: // category
case 15: // category talk
appendList('カテゴリ', Twinkle.speedy.categoryList);
break;
default:
break;
}
} else {
if (namespace === 2 || namespace === 3) {
appendList('利用者ページ', Twinkle.speedy.userList);
}
appendList('リダイレクト', Twinkle.speedy.redirectList);
}
var generalCriteria = Twinkle.speedy.generalList;
// custom rationale lives under general criteria when tagging
if (!mode.isSysop) {
generalCriteria = Twinkle.speedy.customRationale.concat(generalCriteria);
}
appendList('全般', generalCriteria);
var old_area = Morebits.QuickForm.getElements(form, 'work_area')[0];
form.replaceChild(work_area.render(), old_area);
// if sysop, check if CSD is already on the page and fill in custom rationale
if (mode.isSysop && Twinkle.speedy.hasCSD) {
var customOption = $('input[name=csd][value=その他]')[0];
if (customOption) {
if (Twinkle.getPref('speedySelectionStyle') !== 'radioClick') {
// force listeners to re-init
customOption.click();
customOption.parentNode.appendChild(customOption.subgroup);
}
customOption.subgroup.querySelector('input').value = decodeURIComponent($('#delete-reason').text()).replace(/\+/g, ' ');
}
}
};
Twinkle.speedy.callback.priorDeletionCount = function() {
var query = {
action: 'query',
format: 'json',
list: 'logevents',
letype: 'delete',
leaction: 'delete/delete', // Just pure page deletion, no redirect overwrites or revdel
letitle: mw.config.get('wgPageName'),
leprop: '', // We're just counting we don't actually care about the entries
lelimit: 5 // A little bit goes a long way
};
new Morebits.wiki.Api('過去の削除を確認', query, ((apiobj) => {
var response = apiobj.getResponse();
var delCount = response.query.logevents.length;
if (delCount) {
var message = delCount + '回' + (response.continue ? '以上' : '') + '過去に削除';
// 3+ seems problematic
if (delCount >= 3) {
$('#prior-deletion-count').css('color', 'red');
}
// Provide a link to page logs (CSD templates have one for sysops)
var link = Morebits.htmlNode('a', '(記録)');
link.setAttribute('href', mw.util.getUrl('Special:Log', {page: mw.config.get('wgPageName')}));
link.setAttribute('target', '_blank');
$('#prior-deletion-count').text(message + ' '); // Space before log link
$('#prior-deletion-count').append(link);
}
})).post();
};
Twinkle.speedy.generateCsdList = function twinklespeedyGenerateCsdList(list, mode) {
var pageNamespace = mw.config.get('wgNamespaceNumber');
var openSubgroupHandler = function(e) {
$(e.target.form).find('input').prop('disabled', true);
$(e.target.form).children().css('color', 'gray');
$(e.target).parent().css('color', 'black').find('input').prop('disabled', false);
$(e.target).parent().find('input:text')[0].focus();
e.stopPropagation();
};
var submitSubgroupHandler = function(e) {
var evaluateType = mode.isSysop ? 'evaluateSysop' : 'evaluateUser';
Twinkle.speedy.callback[evaluateType](e);
e.stopPropagation();
};
return $.map(list, (critElement) => {
var criterion = $.extend({}, critElement);
if (mode.isSysop) {
if (criterion.hideWhenSysop) {
return null;
}
if (criterion.hideSubgroupWhenSysop) {
criterion.subgroup = null;
}
} else {
if (criterion.hideWhenUser) {
return null;
}
if (criterion.hideSubgroupWhenUser) {
criterion.subgroup = null;
}
}
if (Morebits.isPageRedirect() && criterion.hideWhenRedirect) {
return null;
}
if (criterion.showInNamespaces && !criterion.showInNamespaces.includes(pageNamespace)) {
return null;
}
if (criterion.hideInNamespaces && criterion.hideInNamespaces.includes(pageNamespace)) {
return null;
}
if (criterion.subgroup && mode.isRadioClick) {
if (Array.isArray(criterion.subgroup)) {
criterion.subgroup = criterion.subgroup.concat({
type: 'button',
name: 'submit',
label: mode.isSysop ? 'ページを削除' : 'ページをタグ付け',
event: submitSubgroupHandler
});
} else {
criterion.subgroup = [
criterion.subgroup,
{
type: 'button',
name: 'submit', // ends up being called "csd.submit" so this is OK
label: mode.isSysop ? 'ページを削除' : 'ページをタグ付け',
event: submitSubgroupHandler
}
];
}
// FIXME: does this do anything?
criterion.event = openSubgroupHandler;
}
return criterion;
});
};
Twinkle.speedy.customRationale = [
{
label: 'その他の基準。' + (Morebits.userIsSysop ? ' (カスタムの削除理由)' : '{{即時削除|その他}}を使用します。'),
value: 'その他',
tooltip: !Morebits.userIsSysop ? '明らかに即時削除の方針の基準に合致しているが、各基準番号のテンプレートでどうしても呼び出せない場合など、どうしても各基準番号のテンプレートを利用できないようなやむを得ない場合の救済措置です。' : '',
subgroup: {
name: 'reason_1',
type: 'input',
label: Morebits.userIsSysop ? 'その他の理由:' : '即時削除の方針に合致していることの説明:',
size: 60
}
}
];
Twinkle.speedy.fileList = [
{
label: 'F1-2: コモンズと重複(コピー)',
value: 'ファイル1-2',
tooltip: '同名のファイルが日本語版ウィキペディアに存在しウィキメディア・コモンズから表示できない場合は除きます。',
subgroup: {
name: 'redundantcopy_filename',
type: 'input',
label: 'コモンズでのファイル名:',
tooltip: '接頭辞"File:"は省略してください',
value: Morebits.pageNameNorm
}
},
{
label: 'F1-3: コモンズと重複(他プロジェクト由来)',
value: 'ファイル1-3',
tooltip: '他プロジェクト由来のファイルで、次の条件を全て満たすもの。(1) ウィキメディア・コモンズのファイル説明ページでそのプロジェクトを正しく出典元として掲示されている。(2) 出典元のファイル、ウィキメディア・コモンズのファイル、および日本語版のファイルのすべてに変更が加えられていないもの。',
subgroup: {
name: 'redundantproject_filename',
type: 'input',
label: 'コモンズでのファイル名:',
tooltip: '接頭辞"File:"は省略してください',
value: Morebits.pageNameNorm
}
},
{
label: 'F1-4: コモンズと重複(同一投稿者)',
value: 'ファイル1-4',
tooltip: 'ウィキメディア・コモンズに、日本語版ウィキペディアと同一の投稿者によって投稿されたもの',
subgroup: {
name: 'redundantauthor_filename',
type: 'input',
label: 'コモンズでのファイル名:',
tooltip: '接頭辞"File:"は省略してください',
value: Morebits.pageNameNorm
}
},
{
label: 'F1-5: コモンズへ移動',
value: 'ファイル1-5',
tooltip: '次の条件を全て満たす、ウィキメディア・コモンズへコピーされたファイル。(1) コモンズのファイルページの初版に、ウィキペディア日本語版に記載されていたファイルの作者名、投稿日時、ライセンス情報が正確に転記されている。(2) コモンズのファイルページの初版または最新版に、ウィキペディア日本語版に記載されていた更新履歴、ページ内の説明文などを含む全てのファイル情報が記載されている。',
subgroup: {
name: 'movetocommons_filename',
type: 'input',
label: 'コモンズでのファイル名:',
tooltip: '接頭辞"File:"は省略してください',
value: Morebits.pageNameNorm
}
},
{
label: 'F1-6: 転送元・転送先がコモンズと同一構造のファイルリダイレクト',
value: 'ファイル1-6',
tooltip: '転送元・転送先がコモンズと同一構造のファイルリダイレクトで、ローカルに転送先が存在しないもの',
subgroup: {
name: 'redir_filename',
type: 'input',
label: '転送先のファイル名:',
tooltip: '接頭辞"File:"は省略してください'
}
},
{
label: 'F3: 重複ファイル',
value: 'ファイル3',
tooltip: '同じ様なファイルがあるという意味ではなく、ファイル名が違うだけで全く同じファイルを指します。',
subgroup: {
name: 'redundantimage_filename',
type: 'input',
label: '重複先のファイル名:',
tooltip: '接頭辞"File:"は省略してください'
}
},
{
label: 'F5: 出典またはライセンス不明のまま1週間経過',
value: 'ファイル5',
tooltip: '次の各条件を全て満たすもの。(1) {{No source}}、{{No license}}の各タグが貼り付けられており、貼り付けから一週間以上経過している。(2) {{Image source}}等で投稿者に通知されており、通知から一週間以上経過している。当該のファイルについて個別に通知されている必要はなく、過去に同様な通知がされているならば、その時点から一週間が経過していればよい。',
subgroup: {
name: 'notify_talkname',
type: 'input',
label: '投稿者への通知先:'
}
},
{
label: 'F6: 自由利用できないファイル',
value: 'ファイル6',
tooltip: 'ファイルの内容およびファイルページの記述によれば、著作権法上の理由により自由利用できないことが明らかであって、自由利用できないファイルの受け入れ方針にも適合しないファイル。ただし、ファイルページや記事の編集、改変したファイルの再アップロードなどの対応によって、自由利用できないファイルの受け入れ方針への違反状態を解消できる可能性がある場合には、当該ファイルページおよび投稿者の会話ページへの通知から一定期間が経過しても、依然として違反状態が解消されないことを条件として削除する。',
subgroup: {
name: 'unfree_filename',
type: 'input',
label: '自由利用ができない根拠:'
}
},
{
label: 'F7: コモンズのファイルページ',
value: 'ファイル7',
tooltip: 'ウィキメディア・コモンズから呼び出しているファイル(秀逸な画像に選出された画像を除く)のファイルページ。ただし、有意な内容があるものについては、ウィキメディア・コモンズに転記した後に削除するものとする。'
},
{
label: 'F8: ファイルが存在しないファイルページ',
value: 'ファイル8',
tooltip: 'ローカルのファイルページ自体にアップロードされたメディアがない非リダイレクト。ただし、サーバ等のエラーによって一時的にファイルが見つからなくなっている場合があるので注意すること。'
},
{
label: 'F9: 初版投稿後7日以内の投稿者による依頼',
value: 'ファイル9',
tooltip: 'ファイルの投稿者自身によって、初版投稿時から168時間(7日間)以内に {{即時削除}} 貼付が行われたもので、リンク元がないか、そのページが削除されても問題のないリンクのみ(例えば削除依頼からのリンクなど)のもの。'
}
];
Twinkle.speedy.articleList = [
{
label: 'A1: 定義になっていない、あるいは文章になっていないもの',
value: '記事1',
tooltip: '定義がないということの意味は「『項目名は○○である』という定義文がない」という意味ではありません。冒頭に定型定義文がないことは、即時削除の理由とはなりません。また、定義文があっても百科事典としての解説に足る定義がないものは、対象となります。(1) 項目名だけ書いてあるもの。(2) 言語間リンク・カテゴリ・外部リンクのみのページ。(3) 文章になっているが、定義になっていないもの(例:「彼は非常に有名で多くの人に慕われていた。死ぬまでに3冊の本を書いて社会に貢献した。」)。(4) その他、単なる単語の羅列等。',
subgroup: {
name: 'no_definition',
type: 'input',
label: '定義なしと判断した理由:',
size: 60
}
}
];
Twinkle.speedy.categoryList = [
{
label: 'C1: 初版からリダイレクト',
value: 'カテゴリ1',
tooltip: '初版から継続して他のページへのリダイレクトであるもの。ただし、カテゴリページの移動により生成されたリダイレクトを除く。移動により生成されたリダイレクトは、C6の適用を検討してください。'
},
{
label: 'C3: 私的カテゴリ',
value: 'カテゴリ3',
tooltip: '「Category:利用者:(利用者名)」のような、私的カテゴリ'
},
{
label: 'C6: 改名・統合等による事前合意を経て未使用となったカテゴリ',
value: 'カテゴリ6',
tooltip: '事前の所定の議論による合意に基づく、移行(改名・統合)先へのカテゴリ変更により、空カテゴリとなったもの(統合には複数の異なる移行先への吸収合併を含む)',
subgroup: {
name: 'catdesc',
type: 'input',
label: '議論場所:',
tooltip: '"[[]]"は外して入力してください。例: プロジェクト:カテゴリ関連/議論/20xx年/x月x日#議論セクション'
}
}
];
Twinkle.speedy.userList = [
{
label: 'U1: 本人希望',
value: '利用者ページ1',
tooltip: '他の利用者の編集が入っているものについては、会話ページとして使用されてきた履歴があるもの(会話ページからの移動により作成された過去ログなど)を除き即時削除対象とする。'
},
{
label: 'U2: 登録されていない利用者の利用者ページ',
value: '利用者ページ2',
tooltip: 'Wikipedia日本語版に利用者登録されていない利用者の利用者ページ、会話ページ。サブページを含む。利用者名変更により生じたリダイレクトは対象外とする。'
},
{
label: 'U3: IP利用者の利用者ページ',
value: '利用者ページ3',
tooltip: 'IP利用者の利用者ページ / 利用者ページのサブページ。',
hideWhenRedirect: true
}
];
Twinkle.speedy.generalList = [
{
label: 'G1: 意味不明な内容のページ',
value: '全般1',
tooltip: '内容が全く意味をもっておらず理解することができないページ。',
hideInNamespaces: [2] // Not applicable in userspace
},
{
label: 'G2: テストページ',
value: '全般2',
tooltip: '編集やその他のウィキペディアの機能をテストするために作成されたページ。 User名前空間のページは含まれていません。',
hideInNamespaces: [2] // Not applicable in userspace
},
{
label: 'G3: 荒らし投稿',
value: '全般3',
tooltip: '純粋な破壊行為のページ。'
},
{
label: 'G4: 露骨な宣伝・広告のみが目的',
value: '全般4',
tooltip: '露骨な宣伝・広告のみが目的と思われるページ。特定商品の宣伝・広告で全文が占められているものや、百科事典的な記事にするためには根本的に書き換える必要があるもののことです。単に企業あるいは製品を主題とした記事に関してはこの方針における削除対象にはならないことにご注意ください。また、立項アカウントが記事名(人名・団体名など)と同じということは即時削除理由になりません。',
subgroup: {
name: 'spam_reason',
type: 'input',
label: '「露骨な宣伝・広告のみが目的」と判断される根拠:',
size: 60
}
},
{
label: 'G5: 削除されたページの改善なき再作成',
value: '全般5',
tooltip: '過去に削除依頼を経て削除されたページや文章・ファイルなどについて、その削除理由となった問題点が解消されていないものの再投稿。過去に削除審議を経ずして削除されたもの(削除依頼を一度も経ておらず、即時削除された履歴しかないもの)には適用されません。再作成された項目に、この条項以外の理由による即時削除理由が存在すれば、その即時削除理由を附して、再度の即時削除を求めることはできます。',
subgroup: {
name: 'repost_xfd',
type: 'input',
label: '削除の議論が行われたページ:',
tooltip: '"Wikipedia:削除依頼/"は外して書く必要があります。"',
size: 60
}
},
{
label: 'G6: ウィキペディア内のコピペ',
value: '全般6',
tooltip: 'ウィキペディア(他言語版を含む)内ページのコピー&ペーストによって作成された、またはリダイレクトなどが上書きされ記事となったページで、ペースト後に意味のある加筆が行われていないもの。ただし翻訳や分割などコピー&ペーストが有意な目的のためになされており、かつ必要な履歴継承もなされている場合は除きます。また、移動すべき場合にもかかわらずコピー&ペーストにより作成されたページについても削除の対象となります。',
hideInNamespaces: [6], // Not applicable in filespace
subgroup: [
{
name: 'original_page',
type: 'input',
label: 'コピペ元ページ名:'
},
{
name: 'language_code',
type: 'input',
label: '言語コード:',
tooltip: '日本語版の場合は指定不要です。',
size: 60
}
]
},
{
label: 'G8: 初版投稿者による依頼または白紙化',
value: '全般8',
tooltip: '初版投稿者または初めて有益な記述が行われた版の投稿者自身によって{{即時削除}}添付または白紙化が行われたもので、次の要件全てを満たしているもの: (1)ページの履歴にその投稿者の投稿しかない、あるいは、誤字脱字の訂正、カテゴリやリンクの追加など、ごく単純な編集しかないもの(2)移動により自動生成されたリダイレクトでないこと(3)ファイル名前空間ではないもの※ファイル名前空間の場合はファイル9で扱います。',
hideInNamespaces: [6] // Not applicable in filespace
},
{
label: 'G9: 明白な著作権侵害',
value: '全般9',
tooltip: '次の条件を全て満たしており、著作権侵害が明白であると判断されるもの。次のうち1つでも当てはまらない可能性がある場合は削除依頼に回してください。(1)ウェブ上で認証なしで閲覧できるページからの完全な複製であるもの(翻案は含まない)(2)初版からほぼ全ての版において著作権侵害が認められ、著作権侵害部分を除去した場合に記事として成り立たないもの(3)自著作物の持ち込みでないことが明白であるもの(4)複製部分に明確な著作物性が認められるもの(きわめて短い文章、事実の羅列、リスト、表、年表、略歴等は含まない)(5)複製部分の著作権が保護期間内であることが明らかであるもの(6)複製元であるウェブページが著作権を有していることが明白であるもの(ウィキペディアもしくは他のウィキメディア・プロジェクトを含む、複製元ウェブページ以外のウェブサイトをソースとするものは含まない)(7)GFDLとCC BY-SA 3.0のデュアルライセンスで利用できないことが明らかであり、適切な引用とも認められないもの以上のうち1つでも当てはまらない可能性がある場合は削除依頼に回してください。',
subgroup: {
name: 'copyvio_url',
type: 'input',
label: '著作権の侵害元ページのURL:',
tooltip: '「http://」または「https://」プロトコルを含むURLを入力してください。',
size: 60
}
},
{
label: 'G10: <a href="https://ja.wikipedia.org/wiki/WP:CSD/G10" target="_blank">WP:CSD/G10</a>に掲載されている特定の荒らしが作成したページ',
value: '全般10',
tooltip: 'Wikipedia:即時削除の方針/全般10に掲載されている荒らしが作成したページで、「ページの履歴にLTAまたはVIPの投稿しかない、あるいは、他利用者の履歴があっても、それが誤字脱字の訂正、カテゴリやリンクの追加など、ごく単純な編集でしかないもの」に該当しているもの。記事としての履歴がないリダイレクトのリダイレクト起こしも含む。',
subgroup: {
name: 'banned_user',
type: 'input',
label: '荒らしの名称:',
size: 40
}
}
];
Twinkle.speedy.redirectList = [
{
label: 'R1-1: 直接関係のないページへのリダイレクト',
value: 'リダイレクト1-1',
tooltip: '赤リンクへのリダイレクトは、必ずしも即時削除の理由とはなりません。'
},
{
label: 'R1-2: 単純な書き誤り',
value: 'リダイレクト1-2',
tooltip: 'よくある間違いや表記揺れの場合は、リダイレクトのまま存続となることがあります。少しでも微妙であればリダイレクトの削除依頼に提出してください。(1) 名前空間名の書き誤りなど - 存在しない名前空間のものなど。(2) 名前空間を間違えて移動したことによる残骸ページ。(3) 字体の似たひらがな・カタカナ・漢字等の取り違え。',
subgroup: {
name: 'err_reason',
type: 'input',
label: '書き誤り箇所:',
size: 40
}
},
{
label: 'R1-3: 転送先がないリダイレクトであって、他のページへのリダイレクトに変更できないもの',
value: 'リダイレクト1-3',
tooltip: 'ページ名の別名や略称など。ただし単に「現在の転送先が存在しない」というだけでは即時削除の対象となりません。転送先が変更できないか判断に迷う場合はリダイレクトの削除依頼へ提出してください。'
},
{
label: 'R1-4: 他プロジェクトへのリダイレクト',
value: 'リダイレクト1-4',
tooltip: '転送先がプロジェクト接頭辞(「en:」など)を含むものに限る'
},
{
label: 'R2-1: 半角と全角の使い分け違反',
value: 'リダイレクト2-1',
tooltip: '全角と半角の使い分けに反する半角カタカナ、全角英数字、全角スペースなどを使用しているもの',
subgroup: {
name: 'width_character',
type: 'input',
label: '使い分けが間違っている文字:',
size: 40
}
},
{
label: 'R2-2: 作品名を鍵括弧等でくくったもの',
value: 'リダイレクト2-2',
tooltip: '作品名を「」または『』などでくくっているもの'
},
{
label: 'R2-3: 作品名以外の記事名における読み・別表記等の併記',
value: 'リダイレクト2-3',
tooltip: '項目名に、読みがな、別名、別表記、原語を付記しているもの。作品名については読み等までを含めて正式名称である可能性を考慮し、リダイレクトの削除依頼で削除してください。'
},
{
label: 'R2-5: 曖昧さ回避括弧の使い方違反',
value: 'リダイレクト2-5',
tooltip: '(1) 項目名に曖昧さ回避の括弧を使用する場合に、左括弧の前に半角スペースがない、または、半角丸括弧以外を使用しているもの。(2) 曖昧さ回避括弧の中に曖昧さ回避括弧を含むもの。',
subgroup: {
name: 'usage_violation',
type: 'input',
label: '使い方の違反内容:',
size: 40
}
},
{
label: 'R2-6: 漢字表記人名の姓名間の空白',
value: 'リダイレクト2-6',
tooltip: '漢字で表記される人物の記事名で、姓と名の間に、半角全角かかわらず空白が入っているもの'
},
{
label: 'R4: 唯一の投稿者による移動の残骸で本人依頼のもの',
value: 'リダイレクト4',
tooltip: '漢字で表記される人物の記事名で、姓と名の間に、半角全角かかわらず空白が入っているもの'
}
];
Twinkle.speedy.normalizeHash = {
その他: 'db',
全般1: 'g1',
全般2: 'g2',
全般3: 'g3',
全般4: 'g4',
全般5: 'g5',
'全般5 (リダイレクト)': 'g5-r',
全般6: 'g6',
全般8: 'g8',
全般9: 'g9',
全般10: 'g10',
記事1: 'a1',
利用者ページ1: 'u1',
利用者ページ2: 'u2',
利用者ページ3: 'u3',
カテゴリ1: 'c1',
カテゴリ3: 'c3',
カテゴリ6: 'c6',
'ファイル1-2': 'f1-2',
'ファイル1-3': 'f1-3',
'ファイル1-4': 'f1-4',
'ファイル1-5': 'f1-5',
'ファイル1-6': 'f1-6',
ファイル3: 'f3',
ファイル5: 'f5',
ファイル6: 'f6',
ファイル7: 'f7',
ファイル8: 'f8',
ファイル9: 'f9',
'リダイレクト1-1': 'r1-1',
'リダイレクト1-2': 'r1-2',
'リダイレクト1-3': 'r1-3',
'リダイレクト1-4': 'r1-4',
'リダイレクト2-1': 'r2-1',
'リダイレクト2-2': 'r2-2',
'リダイレクト2-3': 'r2-3',
'リダイレクト2-5': 'r2-5',
'リダイレクト2-6': 'r2-6',
リダイレクト4: 'r4'
};
Twinkle.speedy.callbacks = {
getTemplateCodeAndParams: function(params) {
var code, parameters, i;
parameters = params.templateParams[0] || [];
code = '{{即時削除|1=' + params.values[0];
for (i in parameters) {
if (typeof parameters[i] === 'string') {
code += '|' + i + '=' + parameters[i];
}
}
code += '}}';
return code;
},
parseWikitext: function(wikitext, callback) {
var query = {
action: 'parse',
prop: 'text',
pst: 'true',
text: wikitext,
contentmodel: 'wikitext',
title: mw.config.get('wgPageName'),
disablelimitreport: true,
format: 'json'
};
var statusIndicator = new Morebits.Status('削除要約の生成');
var api = new Morebits.wiki.Api('即時削除テンプレートの構文解析', query, ((apiobj) => {
var reason = decodeURIComponent($(apiobj.getResponse().parse.text).find('#delete-reason').text()).replace(/\+/g, ' ');
if (!reason) {
statusIndicator.warn('削除テンプレートから要約を生成できませんでした');
} else {
statusIndicator.info('完了');
}
callback(reason);
}), statusIndicator);
api.post();
},
sysop: {
main: function(params) {
var code = Twinkle.speedy.callbacks.getTemplateCodeAndParams(params);
Twinkle.speedy.callbacks.parseWikitext(code, (reason) => {
if (params.promptForSummary) {
reason = prompt('使用する削除要約を入力するか、またはOKを押して自動的に生成される要約を使用します。', reason);
}
Twinkle.speedy.callbacks.sysop.deletePage(reason, params);
});
},
deletePage: function(reason, params) {
var thispage = new Morebits.wiki.Page(mw.config.get('wgPageName'), 'ページを削除');
if (reason === null) {
return Morebits.Status.error('理由を尋ねる', '中止');
} else if (!reason || !reason.replace(/^\s*/, '').replace(/\s*$/, '')) {
return Morebits.Status.error('理由を尋ねる', 'あなたは何も言わなかった。分からないよ...管理者とその無気力な態度は... 降参だ...');
}
var deleteMain = function(callback) {
thispage.setEditSummary(reason);
thispage.setWatchlist(params.watch);
thispage.deletePage(() => {
thispage.getStatusElement().info('完了');
typeof callback === 'function' && callback();
Twinkle.speedy.callbacks.sysop.deleteTalk(params);
});
};
deleteMain();
},
deleteTalk: function(params) {
// delete talk page
if (params.deleteTalkPage &&
params.normalized !== 'f8' &&
!document.getElementById('ca-talk').classList.contains('new')) {
var talkpage = new Morebits.wiki.Page(mw.config.get('wgFormattedNamespaces')[mw.config.get('wgNamespaceNumber') + 1] + ':' + mw.config.get('wgTitle'), 'トークページの削除');
talkpage.setEditSummary('削除された"' + Morebits.pageNameNorm + '"のトークページ');
talkpage.deletePage();
// this is ugly, but because of the architecture of wiki.api, it is needed
// (otherwise success/failure messages for the previous action would be suppressed)
window.setTimeout(() => {
Twinkle.speedy.callbacks.sysop.deleteRedirects(params);
}, 1800);
} else {
Twinkle.speedy.callbacks.sysop.deleteRedirects(params);
}
},
deleteRedirects: function(params) {
// delete redirects
if (params.deleteRedirects) {
var query = {
action: 'query',
titles: mw.config.get('wgPageName'),
prop: 'redirects',
rdlimit: 'max', // 500 is max for normal users, 5000 for bots and sysops
format: 'json'
};
var wikipedia_api = new Morebits.wiki.Api('リダイレクトの一覧を取得...', query, Twinkle.speedy.callbacks.sysop.deleteRedirectsMain,
new Morebits.Status('リダイレクトの削除'));
wikipedia_api.params = params;
wikipedia_api.post();
}
// promote Unlink tool
var $link, $bigtext;
if (mw.config.get('wgNamespaceNumber') === 6 && params.normalized !== 'f7') {
$link = $('<a/>').attr({
href: '#'
}).text('クリックしてリンク解除ツールへ進む')
.css({ fontSize: '130%', fontWeight: 'bold' }).on('click', () => {
Morebits.wiki.actionCompleted.redirect = null;
Twinkle.speedy.dialog.close();
Twinkle.unlink.callback('削除されたファイルの使用やリンクの除去 ' + Morebits.pageNameNorm);
});
$bigtext = $('<span/>').text('バックリンクを無効にし、ファイルの使用箇所を除去する。')
.css({ fontSize: '130%', fontWeight: 'bold' });
Morebits.Status.info($bigtext[0], $link[0]);
} else if (params.normalized !== 'f7') {
$link = $('<a/>').attr({
href: '#'
}).text('クリックしてリンク解除ツールへ進む')
.css({ fontSize: '130%', fontWeight: 'bold' }).on('click', () => {
Morebits.wiki.actionCompleted.redirect = null;
Twinkle.speedy.dialog.close();
Twinkle.unlink.callback('削除された ' + Morebits.pageNameNorm + ' へのリンクの削除 ');
});
$bigtext = $('<span/>').text('バックリンクを無効にする')
.css({ fontSize: '130%', fontWeight: 'bold' });
Morebits.Status.info($bigtext[0], $link[0]);
}
},
deleteRedirectsMain: function(apiobj) {
var response = apiobj.getResponse();
var snapshot = response.query.pages[0].redirects || [];
var total = snapshot.length;
var statusIndicator = apiobj.statelem;
if (!total) {
statusIndicator.status('リダイレクトが見つかりませんでした');
return;
}
statusIndicator.status('0%');
var current = 0;
var onsuccess = function(apiobjInner) {
var now = parseInt(100 * ++current / total, 10) + '%';
statusIndicator.update(now);
apiobjInner.statelem.unlink();
if (current >= total) {
statusIndicator.info(now + ' (完了)');
Morebits.wiki.removeCheckpoint();
}
};
Morebits.wiki.addCheckpoint();
snapshot.forEach((value) => {
var title = value.title;
var page = new Morebits.wiki.Page(title, 'リダイレクト "' + title + '" の削除');
page.setEditSummary('[[WP:CSD#R1-3|R1-3]]: 転送先がないリダイレクト 転送先: [["' + Morebits.pageNameNorm + ']]"');
page.deletePage(onsuccess);
});
}
},
user: {
main: function(pageobj) {
var statelem = pageobj.getStatusElement();
if (!pageobj.exists()) {
statelem.error('ページが存在しないようです。もしかしたら、すでに削除されているのかもしれません。');
return;
}
var params = pageobj.getCallbackParameters();
// given the params, builds the template parameters to the params that were passed in
// returns => <string> wikitext
var code = Twinkle.speedy.callbacks.getTemplateCodeAndParams(params);
// Tag if possible, post on talk if not
if (pageobj.canEdit() && ['wikitext', 'Scribunto', 'javascript', 'css', 'sanitized-css'].includes(pageobj.getContentModel()) && mw.config.get('wgNamespaceNumber') !== 710 /* TimedText */) {
var text = pageobj.getPageText();
statelem.status('ページ上のタグの確認...');
// check for existing deletion tags
var tag = /(?:\{\{\s*([sS][dD]2?|即時削除2?(\/.*)?|[dD]elete)(?:\s*\||\s*\}\}))/.exec(text);
// This won't make use of the db-multiple template but it probably should
if (tag && !confirm('ページ上には既にCSD関係のテンプレート {{' + tag[1] + '}} があります。別のCSDテンプレートを追加しますか?')) {
return;
}
var xfd = /\{\{((?:Sakujo\/本体)|[cfm]fd\b)/i.exec(text) || /RFD notice/.exec(text);
if (xfd && !confirm('削除関連のテンプレート {{' + xfd[1] + '}} が見つかりました。このまま即時削除テンプレートを追加しますか?')) {
return;
}
// curate/patrol the page
if (Twinkle.getPref('markSpeedyPagesAsPatrolled')) {
pageobj.triage();
}
// Wrap SD template in noinclude tags if we are in template space.
// Won't work with userboxes in userspace, or any other transcluded page outside template space
if (mw.config.get('wgNamespaceNumber') === 10) { // Template:
code = '<noinclude>' + code + '</noinclude>';
}
// Remove tags that become superfluous with this action
text = text.replace(/\{\{\s*([Uu]serspace draft)\s*(\|(?:\{\{[^{}]*\}\}|[^{}])*)?\}\}\s*/g, '');
if (mw.config.get('wgNamespaceNumber') === 6) {
// remove "move to Commons" tag - deletion-tagged files cannot be moved to Commons
text = Twinkle.removeMoveToCommonsTagsFromWikicode(text);
}
if (mw.config.get('wgPageContentModel') === 'Scribunto') {
// Scribunto isn't parsed like wikitext, so CSD templates on modules need special handling to work
var equals = '';
while (code.includes(']' + equals + ']')) {
equals += '=';
}
code = "require('Module:Module wikitext')._addText([" + equals + '[' + code + ']' + equals + ']);';
} else if (['javascript', 'css', 'sanitized-css'].includes(mw.config.get('wgPageContentModel'))) {
// Likewise for JS/CSS pages
code = '/* ' + code + ' */';
}
// Generate edit summary for edit
var editsummary;
if (params.normalizeds[0] === 'db') {
editsummary = '"' + params.templateParams[0]['2'] + '"として[[WP:CSD|即時削除]]を依頼';
} else {
editsummary = '即時削除を依頼 ([[WP:CSD#' + params.normalizeds[0].toUpperCase() + '|CSD ' + params.normalizeds[0].toUpperCase() + ']])';
}
// Blank pages
if (params.blankpage) {
text = code;
} else {
// Insert tag after short description or any hatnotes
var wikipage = new Morebits.wikitext.Page(text);
text = wikipage.insertAfterTemplates(code + '\n', Twinkle.hatnoteRegex).getText();
}
pageobj.setPageText(text);
pageobj.setEditSummary(editsummary);
pageobj.setWatchlist(params.watch);
pageobj.save(Twinkle.speedy.callbacks.user.tagComplete);
} else { // Attempt to place on talk page
var talkName = new mw.Title(pageobj.getPageName()).getTalkPage().toText();
if (talkName !== pageobj.getPageName()) {
pageobj.getStatusElement().warn('ページの編集ができなかったため、トークページにタグが置かれます。');
var talk_page = new Morebits.wiki.Page(talkName, 'トークページにタグを自動配置');
talk_page.setNewSectionTitle(pageobj.getPageName() + 'の即時削除依頼');
talk_page.setNewSectionText(code + '\n\n' + pageobj.getPageName() + 'に直接タグ付けできなかったため、この会話ページに即時削除タグを配置しました。対象ページの削除を依頼します。--~~~~');
talk_page.setCreateOption('recreate');
talk_page.setFollowRedirect(true);
talk_page.setWatchlist(params.watch);
talk_page.setCallbackParameters(params);
talk_page.newSection(Twinkle.speedy.callbacks.user.tagComplete);
} else {
pageobj.getStatusElement().error('ページを編集できず、即時削除を依頼できる別の場所もありません。中止します。');
}
}
},
tagComplete: function(pageobj) {
var params = pageobj.getCallbackParameters();
// Add this nomination to the user's userspace log
if (params.lognomination) {
Twinkle.speedy.callbacks.user.addToLog(params);
}
},
addToLog: function(params) {
var usl = new Morebits.UserspaceLogger(Twinkle.getPref('speedyLogPageName'));
usl.initialText =
'{{Notice|style=announce|text=即時削除の際、このページへのリンクは考慮する必要はありません。}}\n' +
'<!-- このテンプレートは、[[Wikipedia:即時削除の方針#リダイレクト]]にリンク元がある場合は削除しないよう定めてあるため、このページがリンクしていることによる影響を避けるためのものです。 -->\n\n' +
'このページは[[H:TW|Twinkle]]のCSDモジュールによって生成されており、[[WP:CSD|即時削除]]に指定したページを記録しています。\n\n' +
'記録をこれ以上保持したくない場合、[[利用者:Syunsyunminmin/Twinkle/Preferences|設定パネル]]を使用してこの機能をオフにした後、' +
'[[WP:CSD#U1|CSD U1]]でこのページの即時削除を依頼することが出来ます。' +
(Morebits.userIsSysop ? '\n\nこのログは、Twinkleを使用して行われた即時削除を完全に追跡してはいません。' : '');
var formatParamLog = function(normalize, csdparam, input) {
if ((normalize === 'C6' && csdparam === 'catdesc') ||
(normalize === 'F1' && csdparam === 'filename')) {
input = '[[:' + input + ']]';
} else if (normalize === 'G5' && csdparam === '2') {
input = '[[:Wikipedia:削除依頼/' + input + ']]';
} else if (normalize === 'G9' && csdparam.lastIndexOf('url', 0) === 0 && input.lastIndexOf('http', 0) === 0) {
input = '[' + input + ' ' + input + ']';
} else if (normalize === 'F8' && csdparam === 'filename') {
input = '[[commons:' + input + ']]';
}
return ' {' + normalize + ' ' + csdparam + ': ' + input + '}';
};
var extraInfo = '';
// If a logged file is deleted but exists on commons, the wikilink will be blue, so provide a link to the log
var fileLogLink = mw.config.get('wgNamespaceNumber') === 6 ? ' ([{{fullurl:Special:Log|page=' + mw.util.wikiUrlencode(mw.config.get('wgPageName')) + '}} 記録])' : '';
var editsummary = '[[:' + Morebits.pageNameNorm + ']]の即時削除への指定を記録' + Twinkle.summaryAd;
var appendText = '# [[:' + Morebits.pageNameNorm + ']]' + fileLogLink + ': ';
if (params.normalizeds[0] === 'db') {
appendText += '{{tlp|即時削除|その他}}';
} else {
appendText += '[[WP:CSD#' + params.normalizeds[0].toUpperCase() + '|CSD ' + params.normalizeds[0].toUpperCase() + ']] ({{tlp|即時削除|' + params.values[0] + '}})';
}
// If params is "empty" it will still be full of empty arrays, but ask anyway
if (params.templateParams) {
// Treat custom rationale individually
if (params.normalizeds[0] && params.normalizeds[0] === 'db') {
extraInfo += formatParamLog('Custom', 'rationale', params.templateParams[0]['1']);
} else {
params.templateParams.forEach((item, index) => {
var keys = Object.keys(item);
if (keys[0] !== undefined && keys[0].length > 0) {
// Second loop required since some items (G12, F9) may have multiple keys
keys.forEach((key, keyIndex) => {
if (keys[keyIndex] === 'blanked' || keys[keyIndex] === 'ts') {
return true; // Not worth logging
}
extraInfo += formatParamLog(params.normalizeds[index].toUpperCase(), keys[keyIndex], item[key]);
});
}
});
}
}
if (params.blankpage) {
appendText += '; ページを白紙化した上でのタグ付け。';
}
if (extraInfo) {
appendText += '; 追加情報:' + extraInfo;
}
appendText += ' ~~~~~\n';
usl.log(appendText, editsummary);
}
}
};
// validate subgroups in the form passed into the speedy deletion tag
Twinkle.speedy.getParameters = function twinklespeedyGetParameters(form, values) {
var parameters = [];
$.each(values, (index, value) => {
var currentParams = [];
if (form.add_comments && form.add_comments.checked) {
currentParams.コメント = form['add_comments.additional_comments'].value;
}
switch (value) {
case 'その他':
if (form['csd.reason_1']) {
var dbrationale = form['csd.reason_1'].value;
if (!dbrationale || !dbrationale.trim()) {
alert('その他の基準: 即時削除の方針に合致していることの説明を入力してください。');
parameters = null;
return false;
}
currentParams['2'] = dbrationale;
}
break;
case '全般4': // G4
if (form['csd.spam_reason']) {
if (form['csd.spam_reason'].value) {
currentParams['2'] = form['csd.spam_reason'].value;
} else {
alert('CSD G4: 「露骨な宣伝・広告のみが目的」と判断される根拠を入力してください。');
parameters = null;
return false;
}
}
break;
case '全般5': // G5
if (form['csd.repost_xfd']) {
var deldisc = form['csd.repost_xfd'].value;
if (deldisc) {
if (new RegExp('^:?' + Morebits.namespaceRegex(4) + ':', 'i').test(deldisc)) {
alert('CSD G5: 削除依頼サブページを入力する場合は、"Wikipedia:削除依頼/"は外して書く必要があります。');
parameters = null;
return false;
}
currentParams['2'] = deldisc;
} else {
alert('CSD G5: 削除依頼サブページを入力してください。');
parameters = null;
return false;
}
}
break;
case '全般6': // G6
if (form['csd.original_page'] && form['csd.language_code']) {
var oroginal = form['csd.original_page'].value,
lang = form['csd.language_code'].value;
if (!oroginal || !oroginal.trim()) {
alert('CSD G6: コピペ元のページ名を指定してください');
parameters = null;
return false;
}
if (lang) {
currentParams['3'] = lang;
}
currentParams['2'] = oroginal;
}
break;
case '全般9': // G9
if (form['csd.copyvio_url']) {
if (form['csd.copyvio_url'].value) {
currentParams['2'] = form['csd.copyvio_url'].value;
} else {
alert('CSD G9: 転載元のURLを入力してください。');
}
}
break;
case '全般10': // G10
if (form['csd.banned_user'] && form['csd.banned_user'].value) {
currentParams['2'] = form['csd.banned_user'].value;
}
break;
case '記事1': // A1
if (form['csd.no_definition']) {
var nodefinition = form['csd.no_definition'].value;
if (!nodefinition || !nodefinition.trim()) {
alert('CSD A1: 定義なしと判断した理由を入力してください');
parameters = null;
return false;
}
currentParams[2] = nodefinition;
}
break;
case 'カテゴリ6': // C6
if (form['csd.catdesc'] && form['csd.catdesc'].value) {
var catdesc = form['csd.catdesc'].value;
if (/^\[\[(.*)\]\]$/.test(catdesc)) {
alert('CSD C6: 議論場所は"[[]]"を外して入力してください。');
parameters = null;
return false;
}
currentParams['2'] = form['csd.catdesc'].value;
}
break;
case 'リダイレクト1-2': // R1-2
if (form['csd.err_reason'] && form['csd.err_reason'].value) {
if (!form['csd.err_reason'].value) {
alert('CSD R1-2: 書き誤り箇所を入力してください。');
parameters = null;
return false;
}
currentParams['2'] = form['csd.err_reason'].value;
}
break;
case 'リダイレクト2-1': // R2-1
if (form['csd.width_character'] && form['csd.width_character'].value) {
if (!form['csd.width_character'].value) {
alert('CSD R2-1: 使い分けが間違っている文字を入力してください。');
parameters = null;
return false;
}
currentParams['2'] = form['csd.width_character'].value;
}
break;
case 'リダイレクト2-5': // R2-5
if (form['csd.usage_violation'] && form['csd.usage_violation'].value) {
if (!form['csd.usage_violation'].value) {
alert('CSD R2-5: 使い方の違反内容を入力してください。');
parameters = null;
return false;
}
currentParams['2'] = form['csd.usage_violation'].value;
}
break;
case 'ファイル1-2': // F1-2
if (form['csd.redundantcopy_filename']) {
var redcopyimage = form['csd.redundantcopy_filename'].value;
currentParams['2'] = redcopyimage.replace(new RegExp('^\\s*' + Morebits.namespaceRegex(6) + ':', 'i'), '');
}
break;
case 'ファイル1-3': // F1-3
if (form['csd.redundantproject_filename']) {
var redprojimage = form['csd.redundantproject_filename'].value;
currentParams['2'] = redprojimage.replace(new RegExp('^\\s*' + Morebits.namespaceRegex(6) + ':', 'i'), '');
}
break;
case 'ファイル1-4': // F1-4
if (form['csd.redundantauthor_filename']) {
var redauthimage = form['csd.redundantauthor_filename'].value;
currentParams['2'] = redauthimage.replace(new RegExp('^\\s*' + Morebits.namespaceRegex(6) + ':', 'i'), '');
}
break;
case 'ファイル1-5': // F1-5
if (form['csd.movetocommons_filename']) {
var movedimage = form['csd.movetocommons_filename'].value;
currentParams['2'] = movedimage.replace(new RegExp('^\\s*' + Morebits.namespaceRegex(6) + ':', 'i'), '');
}
break;
case 'ファイル1-6': // F1-6
if (form['csd.redir_filename']) {
var redirimage = form['csd.redir_filename'].value;
if (redirimage) {
currentParams['2'] = redirimage.replace(new RegExp('^\\s*' + Morebits.namespaceRegex(6) + ':', 'i'), '');
} else {
alert('CSD F1-6: ファイルの転送先を入力してください。');
parameters = null;
return false;
}
}
break;
case 'ファイル3': // F3
if (form['csd.redundantimage_filename']) {
var redimage = form['csd.redundantimage_filename'].value;
if (redimage) {
currentParams['2'] = redimage.replace(new RegExp('^\\s*' + Morebits.namespaceRegex(6) + ':', 'i'), '');
} else {
alert('CSD F3: ファイルの重複先を入力してください。');
parameters = null;
return false;
}
}
break;
case 'ファイル5': // F5
if (form['csd.redundantimage_filename']) {
var notifytalkpage = form['csd.redundantimage_filename'].value;
if (notifytalkpage) {
if (/^\[\[(.*)\]\]$/.test(notifytalkpage)) {
alert('CSD F5: 通知先は"[[]]"を外して入力してください。');
parameters = null;
return false;
}
currentParams['2'] = notifytalkpage.replace(new RegExp('^\\s*' + Morebits.namespaceRegex(6) + ':', 'i'), '');
} else {
alert('CSD F5: 投稿者への通知先を入力してください。');
parameters = null;
return false;
}
}
break;
case 'ファイル6': // F6
if (form['csd.unfree_filename']) {
if (form['csd.unfree_filename'].value) {
currentParams['2'] = form['csd.unfree_filename'].value;
} else {
alert('CSD F6: 自由利用ができない根拠を入力してください。');
}
}
break;
default:
break;
}
parameters.push(currentParams);
});
return parameters;
};
/**
* @param {Event} e
* @return {Array}
*/
Twinkle.speedy.resolveCsdValues = function twinklespeedyResolveCsdValues(e) {
var values = (e.target.form ? e.target.form : e.target).getChecked('csd');
if (values.length === 0) {
alert('Please select a criterion!');
return null;
}
return values;
};
Twinkle.speedy.callback.evaluateSysop = function twinklespeedyCallbackEvaluateSysop(e) {
var form = e.target.form ? e.target.form : e.target;
if (e.target.type === 'checkbox' || e.target.type === 'text' ||
e.target.type === 'select') {
return;
}
var tag_only = form.tag_only;
if (tag_only && tag_only.checked) {
Twinkle.speedy.callback.evaluateUser(e);
return;
}
var values = Twinkle.speedy.resolveCsdValues(e);
if (!values) {
return;
}
var templateParams = Twinkle.speedy.getParameters(form, values);
if (!templateParams) {
return;
}
var normalizeds = values.map((value) => Twinkle.speedy.normalizeHash[value]);
// analyse each criterion to determine whether to watch the page, or prompt for summary
var watchPage, promptForSummary;
normalizeds.forEach((norm) => {
if (Twinkle.getPref('watchSpeedyPages').includes(norm)) {
watchPage = Twinkle.getPref('watchSpeedyExpiry');
}
if (Twinkle.getPref('promptForSpeedyDeletionSummary').includes(norm)) {
promptForSummary = true;
}
});
var notsavelog = form.notsavelog.checked;
var params = {
values: values,
normalizeds: normalizeds,
watch: watchPage,
deleteTalkPage: form.talkpage && form.talkpage.checked,
deleteRedirects: form.redirects.checked,
promptForSummary: promptForSummary,
templateParams: templateParams,
notsavelog: notsavelog
};
Morebits.SimpleWindow.setButtonsEnabled(false);
Morebits.Status.init(form);
Twinkle.speedy.callbacks.sysop.main(params);
};
Twinkle.speedy.callback.evaluateUser = function twinklespeedyCallbackEvaluateUser(e) {
var form = e.target.form ? e.target.form : e.target;
if (e.target.type === 'checkbox' || e.target.type === 'text' ||
e.target.type === 'select') {
return;
}
var values = Twinkle.speedy.resolveCsdValues(e);
if (!values) {
return;
}
var templateParams = Twinkle.speedy.getParameters(form, values);
if (!templateParams) {
return;
}
var normalizeds = values.map((value) => Twinkle.speedy.normalizeHash[value]);
// analyse each criterion to determine whether to watch the page the creator
var watchPage = normalizeds.some((csdCriteria) => Twinkle.getPref('watchSpeedyPages').includes(csdCriteria)) && Twinkle.getPref('watchSpeedyExpiry');
var csdlog = !form.notsavelog.checked && Twinkle.getPref('logSpeedyNominations') && normalizeds.some((norm) => !Twinkle.getPref('noLogOnSpeedyNomination').includes(norm));
var blankpage = form.blankpage.checked;
var notsavelog = form.notsavelog.checked;
var params = {
values: values,
normalizeds: normalizeds,
watch: watchPage,
lognomination: csdlog,
templateParams: templateParams,
blankpage: blankpage,
notsavelog: notsavelog
};
Morebits.SimpleWindow.setButtonsEnabled(false);
Morebits.Status.init(form);
Morebits.wiki.actionCompleted.redirect = mw.config.get('wgPageName');
Morebits.wiki.actionCompleted.notice = 'Tagging complete';
var wikipedia_page = new Morebits.wiki.Page(mw.config.get('wgPageName'), 'ページをタグ付け');
wikipedia_page.setCallbackParameters(params);
wikipedia_page.load(Twinkle.speedy.callbacks.user.main);
};
Twinkle.addInitCallback(Twinkle.speedy, 'speedy');
}(jQuery));
// </nowiki>
ndy3alyqgah7stjnf3hcuc1pq0byyo0
769528
769461
2026-10-22T00:00:18Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 1406828: rm cmt
769528
javascript
text/javascript
// <nowiki>
(function() {
/*
****************************************
*** twinklespeedy.js: CSD module
****************************************
* Mode of invocation: Tab ("CSD")
* Active on: Non-special, existing pages
*
* NOTE FOR DEVELOPERS:
* If adding a new criterion, add it to the appropriate places at the top of
* twinkleconfig.js. Also check out the default values of the CSD preferences
* in twinkle.js, and add your new criterion to those if you think it would be
* good.
*/
Twinkle.speedy = function twinklespeedy() {
// Disable on:
// * special pages
// * non-existent pages
if (mw.config.get('wgNamespaceNumber') < 0 || !mw.config.get('wgArticleId')) {
return;
}
Twinkle.addPortletLink(Twinkle.speedy.callback, 'CSD', 'tw-csd', Morebits.userIsSysop ? 'WP:CSDに従いページを削除する' : 'WP:CSDに従い即時削除を依頼');
};
// This function is run when the CSD tab/header link is clicked
Twinkle.speedy.callback = function twinklespeedyCallback() {
Twinkle.speedy.initDialog(Morebits.userIsSysop ? Twinkle.speedy.callback.evaluateSysop : Twinkle.speedy.callback.evaluateUser, true);
};
// Used by unlink feature
Twinkle.speedy.dialog = null;
// Used throughout
Twinkle.speedy.hasCSD = !!$('#delete-reason').length;
// Prepares the speedy deletion dialog and displays it
Twinkle.speedy.initDialog = function twinklespeedyInitDialog(callbackfunc) {
var dialog;
Twinkle.speedy.dialog = new Morebits.SimpleWindow(Twinkle.getPref('speedyWindowWidth'), Twinkle.getPref('speedyWindowHeight'));
dialog = Twinkle.speedy.dialog;
dialog.setTitle('即時削除の基準を選択する');
dialog.setScriptName('Twinkle');
dialog.addFooterLink('即時削除の方針', 'WP:CSD');
dialog.addFooterLink('CSD設定', 'User:Syunsyunminmin/Twinkle/Preferences#speedy');
dialog.addFooterLink('Twinkle help', 'WP:TW/DOC#speedy');
dialog.addFooterLink('フィードバック', 'User talk:Syunsyunminmin/Twinkle');
var form = new Morebits.QuickForm(callbackfunc, Twinkle.getPref('speedySelectionStyle') === 'radioClick' ? 'change' : null);
if (Morebits.userIsSysop) {
form.append({
type: 'checkbox',
list: [
{
label: 'タグ付けのみ行い削除しない',
value: 'tag_only',
name: 'tag_only',
tooltip: '今すぐページを削除するのではなく、タグ付けだけしたい場合',
checked: !(Twinkle.speedy.hasCSD || mw.config.get('wgRelevantUserName') === mw.config.get('wgUserName') || Twinkle.getPref('deleteSysopDefaultToDelete')),
event: function(event) {
var cForm = event.target.form;
var cChecked = event.target.checked;
// enable talk page checkbox
if (cForm.talkpage) {
cForm.talkpage.checked = !cChecked && Twinkle.getPref('deleteTalkPageOnDelete');
}
// enable redirects checkbox
cForm.redirects.checked = !cChecked && Twinkle.getPref('deleteRedirectsOnDelete');
Twinkle.speedy.callback.modeChanged(cForm);
event.stopPropagation();
}
}
]
});
var deleteOptions = form.append({
type: 'div',
name: 'delete_options'
});
deleteOptions.append({
type: 'header',
label: '削除関係のオプション'
});
if (mw.config.get('wgNamespaceNumber') % 2 === 0 && (mw.config.get('wgNamespaceNumber') !== 2 || (/\//).test(mw.config.get('wgTitle')))) { // hide option for user pages, to avoid accidentally deleting user talk page
deleteOptions.append({
type: 'checkbox',
list: [
{
label: 'トークページも削除',
value: 'talkpage',
name: 'talkpage',
tooltip: 'このオプションはこのページのトークページも削除します。日本語版には対応する方針がないため基本的にはチェックしないでください。',
checked: Twinkle.getPref('deleteTalkPageOnDelete'),
event: function(event) {
event.stopPropagation();
}
}
]
});
}
deleteOptions.append({
type: 'checkbox',
list: [
{
label: 'リダイレクトも削除',
value: 'redirects',
name: 'redirects',
tooltip: 'このオプションはこのページへのすべてのリダイレクトも削除します。転送先を変更できるかもしれないリダイレクトも削除してしまう可能性があります。',
checked: Twinkle.getPref('deleteRedirectsOnDelete'),
event: function(event) {
event.stopPropagation();
}
}
]
});
}
var tagOptions = form.append({
type: 'div',
name: 'tag_options'
});
if (Morebits.userIsSysop) {
tagOptions.append({
type: 'header',
label: '削除関係のオプション'
});
}
tagOptions.append({
type: 'checkbox',
list: [
{
label: 'ベージを白紙化する',
value: 'blankpage',
name: 'blankpage',
tooltip: '削除タグを貼り付ける前にページを白紙化します。',
event: function(event) {
event.stopPropagation();
}
},
{
label: 'コメントを追加する',
value: 'add_comments',
name: 'add_comments',
tooltip: 'コメントを追加する',
subgroup: {
name: 'additional_comments',
type: 'input',
label: 'コメント:',
size: 60
},
event: function(event) {
event.stopPropagation();
}
},
{
label: 'ユーザーログを残さない',
value: 'notsavelog',
name: 'notsavelog',
tooltip: 'Twinkle設定でユーザーログを有効にしている場合、この項目にチェックすることでユーザーログを残さないようにします。' +
'不適切な記事名など、ログを保持したくない場合に使用してください。',
event: function(event) {
event.stopPropagation();
}
}
]
});
form.append({
type: 'div',
id: 'prior-deletion-count',
style: 'font-style: italic'
});
form.append({
type: 'div',
name: 'work_area',
label: 'CSDモジュールの初期化に失敗しました。もう一度試すか、Twinkleの開発者に問題を伝えてください。'
});
if (Twinkle.getPref('speedySelectionStyle') !== 'radioClick') {
form.append({ type: 'submit', className: 'tw-speedy-submit' }); // Renamed in modeChanged
}
var result = form.render();
dialog.setContent(result);
dialog.display();
Twinkle.speedy.callback.modeChanged(result);
// Check for prior deletions. Just once, upon init
Twinkle.speedy.callback.priorDeletionCount();
};
Twinkle.speedy.callback.modeChanged = function twinklespeedyCallbackModeChanged(form) {
var namespace = mw.config.get('wgNamespaceNumber');
// first figure out what mode we're in
var mode = {
isSysop: !!form.tag_only && !form.tag_only.checked,
isRadioClick: Twinkle.getPref('speedySelectionStyle') === 'radioClick'
};
if (mode.isSysop) {
$('[name=delete_options]').show();
$('[name=tag_options]').hide();
$('button.tw-speedy-submit').text('削除');
} else {
$('[name=delete_options]').hide();
$('[name=tag_options]').show();
$('button.tw-speedy-submit').text('タグ付け');
}
var work_area = new Morebits.QuickForm.Element({
type: 'div',
name: 'work_area'
});
var appendList = function(headerLabel, csdList) {
work_area.append({ type: 'header', label: headerLabel });
work_area.append({ type: 'radio', name: 'csd', list: Twinkle.speedy.generateCsdList(csdList, mode) });
};
if (mode.isSysop) {
appendList('その他の基準', Twinkle.speedy.customRationale);
}
if (!Morebits.isPageRedirect()) {
switch (namespace) {
case 0: // article
case 1: // talk
appendList('記事', Twinkle.speedy.articleList);
break;
case 2: // user
case 3: // user talk
appendList('利用者ページ', Twinkle.speedy.userList);
break;
case 6: // file
case 7: // file talk
appendList('ファイル', Twinkle.speedy.fileList);
if (!mode.isSysop) {
work_area.append({ type: 'div', label: '{{No source}}または{{No license}}のタグ付けは、Twinkleの "DI "タブを使って行うことができます。' });
}
break;
case 14: // category
case 15: // category talk
appendList('カテゴリ', Twinkle.speedy.categoryList);
break;
default:
break;
}
} else {
if (namespace === 2 || namespace === 3) {
appendList('利用者ページ', Twinkle.speedy.userList);
}
appendList('リダイレクト', Twinkle.speedy.redirectList);
}
var generalCriteria = Twinkle.speedy.generalList;
// custom rationale lives under general criteria when tagging
if (!mode.isSysop) {
generalCriteria = Twinkle.speedy.customRationale.concat(generalCriteria);
}
appendList('全般', generalCriteria);
var old_area = Morebits.QuickForm.getElements(form, 'work_area')[0];
form.replaceChild(work_area.render(), old_area);
// if sysop, check if CSD is already on the page and fill in custom rationale
if (mode.isSysop && Twinkle.speedy.hasCSD) {
var customOption = $('input[name=csd][value=その他]')[0];
if (customOption) {
if (Twinkle.getPref('speedySelectionStyle') !== 'radioClick') {
// force listeners to re-init
customOption.click();
customOption.parentNode.appendChild(customOption.subgroup);
}
customOption.subgroup.querySelector('input').value = decodeURIComponent($('#delete-reason').text()).replace(/\+/g, ' ');
}
}
};
Twinkle.speedy.callback.priorDeletionCount = function() {
var query = {
action: 'query',
format: 'json',
list: 'logevents',
letype: 'delete',
leaction: 'delete/delete', // Just pure page deletion, no redirect overwrites or revdel
letitle: mw.config.get('wgPageName'),
leprop: '', // We're just counting we don't actually care about the entries
lelimit: 5 // A little bit goes a long way
};
new Morebits.wiki.Api('過去の削除を確認', query, ((apiobj) => {
var response = apiobj.getResponse();
var delCount = response.query.logevents.length;
if (delCount) {
var message = delCount + '回' + (response.continue ? '以上' : '') + '過去に削除';
// 3+ seems problematic
if (delCount >= 3) {
$('#prior-deletion-count').css('color', 'red');
}
// Provide a link to page logs (CSD templates have one for sysops)
var link = Morebits.htmlNode('a', '(記録)');
link.setAttribute('href', mw.util.getUrl('Special:Log', {page: mw.config.get('wgPageName')}));
link.setAttribute('target', '_blank');
$('#prior-deletion-count').text(message + ' '); // Space before log link
$('#prior-deletion-count').append(link);
}
})).post();
};
Twinkle.speedy.generateCsdList = function twinklespeedyGenerateCsdList(list, mode) {
var pageNamespace = mw.config.get('wgNamespaceNumber');
var openSubgroupHandler = function(e) {
$(e.target.form).find('input').prop('disabled', true);
$(e.target.form).children().css('color', 'gray');
$(e.target).parent().css('color', 'black').find('input').prop('disabled', false);
$(e.target).parent().find('input:text')[0].focus();
e.stopPropagation();
};
var submitSubgroupHandler = function(e) {
var evaluateType = mode.isSysop ? 'evaluateSysop' : 'evaluateUser';
Twinkle.speedy.callback[evaluateType](e);
e.stopPropagation();
};
return $.map(list, (critElement) => {
var criterion = $.extend({}, critElement);
if (mode.isSysop) {
if (criterion.hideWhenSysop) {
return null;
}
if (criterion.hideSubgroupWhenSysop) {
criterion.subgroup = null;
}
} else {
if (criterion.hideWhenUser) {
return null;
}
if (criterion.hideSubgroupWhenUser) {
criterion.subgroup = null;
}
}
if (Morebits.isPageRedirect() && criterion.hideWhenRedirect) {
return null;
}
if (criterion.showInNamespaces && !criterion.showInNamespaces.includes(pageNamespace)) {
return null;
}
if (criterion.hideInNamespaces && criterion.hideInNamespaces.includes(pageNamespace)) {
return null;
}
if (criterion.subgroup && mode.isRadioClick) {
if (Array.isArray(criterion.subgroup)) {
criterion.subgroup = criterion.subgroup.concat({
type: 'button',
name: 'submit',
label: mode.isSysop ? 'ページを削除' : 'ページをタグ付け',
event: submitSubgroupHandler
});
} else {
criterion.subgroup = [
criterion.subgroup,
{
type: 'button',
name: 'submit', // ends up being called "csd.submit" so this is OK
label: mode.isSysop ? 'ページを削除' : 'ページをタグ付け',
event: submitSubgroupHandler
}
];
}
// FIXME: does this do anything?
criterion.event = openSubgroupHandler;
}
return criterion;
});
};
Twinkle.speedy.customRationale = [
{
label: 'その他の基準。' + (Morebits.userIsSysop ? ' (カスタムの削除理由)' : '{{即時削除|その他}}を使用します。'),
value: 'その他',
tooltip: !Morebits.userIsSysop ? '明らかに即時削除の方針の基準に合致しているが、各基準番号のテンプレートでどうしても呼び出せない場合など、どうしても各基準番号のテンプレートを利用できないようなやむを得ない場合の救済措置です。' : '',
subgroup: {
name: 'reason_1',
type: 'input',
label: Morebits.userIsSysop ? 'その他の理由:' : '即時削除の方針に合致していることの説明:',
size: 60
}
}
];
Twinkle.speedy.fileList = [
{
label: 'F1-2: コモンズと重複(コピー)',
value: 'ファイル1-2',
tooltip: '同名のファイルが日本語版ウィキペディアに存在しウィキメディア・コモンズから表示できない場合は除きます。',
subgroup: {
name: 'redundantcopy_filename',
type: 'input',
label: 'コモンズでのファイル名:',
tooltip: '接頭辞"File:"は省略してください',
value: Morebits.pageNameNorm
}
},
{
label: 'F1-3: コモンズと重複(他プロジェクト由来)',
value: 'ファイル1-3',
tooltip: '他プロジェクト由来のファイルで、次の条件を全て満たすもの。(1) ウィキメディア・コモンズのファイル説明ページでそのプロジェクトを正しく出典元として掲示されている。(2) 出典元のファイル、ウィキメディア・コモンズのファイル、および日本語版のファイルのすべてに変更が加えられていないもの。',
subgroup: {
name: 'redundantproject_filename',
type: 'input',
label: 'コモンズでのファイル名:',
tooltip: '接頭辞"File:"は省略してください',
value: Morebits.pageNameNorm
}
},
{
label: 'F1-4: コモンズと重複(同一投稿者)',
value: 'ファイル1-4',
tooltip: 'ウィキメディア・コモンズに、日本語版ウィキペディアと同一の投稿者によって投稿されたもの',
subgroup: {
name: 'redundantauthor_filename',
type: 'input',
label: 'コモンズでのファイル名:',
tooltip: '接頭辞"File:"は省略してください',
value: Morebits.pageNameNorm
}
},
{
label: 'F1-5: コモンズへ移動',
value: 'ファイル1-5',
tooltip: '次の条件を全て満たす、ウィキメディア・コモンズへコピーされたファイル。(1) コモンズのファイルページの初版に、ウィキペディア日本語版に記載されていたファイルの作者名、投稿日時、ライセンス情報が正確に転記されている。(2) コモンズのファイルページの初版または最新版に、ウィキペディア日本語版に記載されていた更新履歴、ページ内の説明文などを含む全てのファイル情報が記載されている。',
subgroup: {
name: 'movetocommons_filename',
type: 'input',
label: 'コモンズでのファイル名:',
tooltip: '接頭辞"File:"は省略してください',
value: Morebits.pageNameNorm
}
},
{
label: 'F1-6: 転送元・転送先がコモンズと同一構造のファイルリダイレクト',
value: 'ファイル1-6',
tooltip: '転送元・転送先がコモンズと同一構造のファイルリダイレクトで、ローカルに転送先が存在しないもの',
subgroup: {
name: 'redir_filename',
type: 'input',
label: '転送先のファイル名:',
tooltip: '接頭辞"File:"は省略してください'
}
},
{
label: 'F3: 重複ファイル',
value: 'ファイル3',
tooltip: '同じ様なファイルがあるという意味ではなく、ファイル名が違うだけで全く同じファイルを指します。',
subgroup: {
name: 'redundantimage_filename',
type: 'input',
label: '重複先のファイル名:',
tooltip: '接頭辞"File:"は省略してください'
}
},
{
label: 'F5: 出典またはライセンス不明のまま1週間経過',
value: 'ファイル5',
tooltip: '次の各条件を全て満たすもの。(1) {{No source}}、{{No license}}の各タグが貼り付けられており、貼り付けから一週間以上経過している。(2) {{Image source}}等で投稿者に通知されており、通知から一週間以上経過している。当該のファイルについて個別に通知されている必要はなく、過去に同様な通知がされているならば、その時点から一週間が経過していればよい。',
subgroup: {
name: 'notify_talkname',
type: 'input',
label: '投稿者への通知先:'
}
},
{
label: 'F6: 自由利用できないファイル',
value: 'ファイル6',
tooltip: 'ファイルの内容およびファイルページの記述によれば、著作権法上の理由により自由利用できないことが明らかであって、自由利用できないファイルの受け入れ方針にも適合しないファイル。ただし、ファイルページや記事の編集、改変したファイルの再アップロードなどの対応によって、自由利用できないファイルの受け入れ方針への違反状態を解消できる可能性がある場合には、当該ファイルページおよび投稿者の会話ページへの通知から一定期間が経過しても、依然として違反状態が解消されないことを条件として削除する。',
subgroup: {
name: 'unfree_filename',
type: 'input',
label: '自由利用ができない根拠:'
}
},
{
label: 'F7: コモンズのファイルページ',
value: 'ファイル7',
tooltip: 'ウィキメディア・コモンズから呼び出しているファイル(秀逸な画像に選出された画像を除く)のファイルページ。ただし、有意な内容があるものについては、ウィキメディア・コモンズに転記した後に削除するものとする。'
},
{
label: 'F8: ファイルが存在しないファイルページ',
value: 'ファイル8',
tooltip: 'ローカルのファイルページ自体にアップロードされたメディアがない非リダイレクト。ただし、サーバ等のエラーによって一時的にファイルが見つからなくなっている場合があるので注意すること。'
},
{
label: 'F9: 初版投稿後7日以内の投稿者による依頼',
value: 'ファイル9',
tooltip: 'ファイルの投稿者自身によって、初版投稿時から168時間(7日間)以内に {{即時削除}} 貼付が行われたもので、リンク元がないか、そのページが削除されても問題のないリンクのみ(例えば削除依頼からのリンクなど)のもの。'
}
];
Twinkle.speedy.articleList = [
{
label: 'A1: 定義になっていない、あるいは文章になっていないもの',
value: '記事1',
tooltip: '定義がないということの意味は「『項目名は○○である』という定義文がない」という意味ではありません。冒頭に定型定義文がないことは、即時削除の理由とはなりません。また、定義文があっても百科事典としての解説に足る定義がないものは、対象となります。(1) 項目名だけ書いてあるもの。(2) 言語間リンク・カテゴリ・外部リンクのみのページ。(3) 文章になっているが、定義になっていないもの(例:「彼は非常に有名で多くの人に慕われていた。死ぬまでに3冊の本を書いて社会に貢献した。」)。(4) その他、単なる単語の羅列等。',
subgroup: {
name: 'no_definition',
type: 'input',
label: '定義なしと判断した理由:',
size: 60
}
}
];
Twinkle.speedy.categoryList = [
{
label: 'C1: 初版からリダイレクト',
value: 'カテゴリ1',
tooltip: '初版から継続して他のページへのリダイレクトであるもの。ただし、カテゴリページの移動により生成されたリダイレクトを除く。移動により生成されたリダイレクトは、C6の適用を検討してください。'
},
{
label: 'C3: 私的カテゴリ',
value: 'カテゴリ3',
tooltip: '「Category:利用者:(利用者名)」のような、私的カテゴリ'
},
{
label: 'C6: 改名・統合等による事前合意を経て未使用となったカテゴリ',
value: 'カテゴリ6',
tooltip: '事前の所定の議論による合意に基づく、移行(改名・統合)先へのカテゴリ変更により、空カテゴリとなったもの(統合には複数の異なる移行先への吸収合併を含む)',
subgroup: {
name: 'catdesc',
type: 'input',
label: '議論場所:',
tooltip: '"[[]]"は外して入力してください。例: プロジェクト:カテゴリ関連/議論/20xx年/x月x日#議論セクション'
}
}
];
Twinkle.speedy.userList = [
{
label: 'U1: 本人希望',
value: '利用者ページ1',
tooltip: '他の利用者の編集が入っているものについては、会話ページとして使用されてきた履歴があるもの(会話ページからの移動により作成された過去ログなど)を除き即時削除対象とする。'
},
{
label: 'U2: 登録されていない利用者の利用者ページ',
value: '利用者ページ2',
tooltip: 'Wikipedia日本語版に利用者登録されていない利用者の利用者ページ、会話ページ。サブページを含む。利用者名変更により生じたリダイレクトは対象外とする。'
},
{
label: 'U3: IP利用者の利用者ページ',
value: '利用者ページ3',
tooltip: 'IP利用者の利用者ページ / 利用者ページのサブページ。',
hideWhenRedirect: true
}
];
Twinkle.speedy.generalList = [
{
label: 'G1: 意味不明な内容のページ',
value: '全般1',
tooltip: '内容が全く意味をもっておらず理解することができないページ。',
hideInNamespaces: [2] // Not applicable in userspace
},
{
label: 'G2: テストページ',
value: '全般2',
tooltip: '編集やその他のウィキペディアの機能をテストするために作成されたページ。 User名前空間のページは含まれていません。',
hideInNamespaces: [2] // Not applicable in userspace
},
{
label: 'G3: 荒らし投稿',
value: '全般3',
tooltip: '純粋な破壊行為のページ。'
},
{
label: 'G4: 露骨な宣伝・広告のみが目的',
value: '全般4',
tooltip: '露骨な宣伝・広告のみが目的と思われるページ。特定商品の宣伝・広告で全文が占められているものや、百科事典的な記事にするためには根本的に書き換える必要があるもののことです。単に企業あるいは製品を主題とした記事に関してはこの方針における削除対象にはならないことにご注意ください。また、立項アカウントが記事名(人名・団体名など)と同じということは即時削除理由になりません。',
subgroup: {
name: 'spam_reason',
type: 'input',
label: '「露骨な宣伝・広告のみが目的」と判断される根拠:',
size: 60
}
},
{
label: 'G5: 削除されたページの改善なき再作成',
value: '全般5',
tooltip: '過去に削除依頼を経て削除されたページや文章・ファイルなどについて、その削除理由となった問題点が解消されていないものの再投稿。過去に削除審議を経ずして削除されたもの(削除依頼を一度も経ておらず、即時削除された履歴しかないもの)には適用されません。再作成された項目に、この条項以外の理由による即時削除理由が存在すれば、その即時削除理由を附して、再度の即時削除を求めることはできます。',
subgroup: {
name: 'repost_xfd',
type: 'input',
label: '削除の議論が行われたページ:',
tooltip: '"Wikipedia:削除依頼/"は外して書く必要があります。"',
size: 60
}
},
{
label: 'G6: ウィキペディア内のコピペ',
value: '全般6',
tooltip: 'ウィキペディア(他言語版を含む)内ページのコピー&ペーストによって作成された、またはリダイレクトなどが上書きされ記事となったページで、ペースト後に意味のある加筆が行われていないもの。ただし翻訳や分割などコピー&ペーストが有意な目的のためになされており、かつ必要な履歴継承もなされている場合は除きます。また、移動すべき場合にもかかわらずコピー&ペーストにより作成されたページについても削除の対象となります。',
hideInNamespaces: [6], // Not applicable in filespace
subgroup: [
{
name: 'original_page',
type: 'input',
label: 'コピペ元ページ名:'
},
{
name: 'language_code',
type: 'input',
label: '言語コード:',
tooltip: '日本語版の場合は指定不要です。',
size: 60
}
]
},
{
label: 'G8: 初版投稿者による依頼または白紙化',
value: '全般8',
tooltip: '初版投稿者または初めて有益な記述が行われた版の投稿者自身によって{{即時削除}}添付または白紙化が行われたもので、次の要件全てを満たしているもの: (1)ページの履歴にその投稿者の投稿しかない、あるいは、誤字脱字の訂正、カテゴリやリンクの追加など、ごく単純な編集しかないもの(2)移動により自動生成されたリダイレクトでないこと(3)ファイル名前空間ではないもの※ファイル名前空間の場合はファイル9で扱います。',
hideInNamespaces: [6] // Not applicable in filespace
},
{
label: 'G9: 明白な著作権侵害',
value: '全般9',
tooltip: '次の条件を全て満たしており、著作権侵害が明白であると判断されるもの。次のうち1つでも当てはまらない可能性がある場合は削除依頼に回してください。(1)ウェブ上で認証なしで閲覧できるページからの完全な複製であるもの(翻案は含まない)(2)初版からほぼ全ての版において著作権侵害が認められ、著作権侵害部分を除去した場合に記事として成り立たないもの(3)自著作物の持ち込みでないことが明白であるもの(4)複製部分に明確な著作物性が認められるもの(きわめて短い文章、事実の羅列、リスト、表、年表、略歴等は含まない)(5)複製部分の著作権が保護期間内であることが明らかであるもの(6)複製元であるウェブページが著作権を有していることが明白であるもの(ウィキペディアもしくは他のウィキメディア・プロジェクトを含む、複製元ウェブページ以外のウェブサイトをソースとするものは含まない)(7)GFDLとCC BY-SA 3.0のデュアルライセンスで利用できないことが明らかであり、適切な引用とも認められないもの以上のうち1つでも当てはまらない可能性がある場合は削除依頼に回してください。',
subgroup: {
name: 'copyvio_url',
type: 'input',
label: '著作権の侵害元ページのURL:',
tooltip: '「http://」または「https://」プロトコルを含むURLを入力してください。',
size: 60
}
},
{
label: 'G10: <a href="https://ja.wikipedia.org/wiki/WP:CSD/G10" target="_blank">WP:CSD/G10</a>に掲載されている特定の荒らしが作成したページ',
value: '全般10',
tooltip: 'Wikipedia:即時削除の方針/全般10に掲載されている荒らしが作成したページで、「ページの履歴にLTAまたはVIPの投稿しかない、あるいは、他利用者の履歴があっても、それが誤字脱字の訂正、カテゴリやリンクの追加など、ごく単純な編集でしかないもの」に該当しているもの。記事としての履歴がないリダイレクトのリダイレクト起こしも含む。',
subgroup: {
name: 'banned_user',
type: 'input',
label: '荒らしの名称:',
size: 40
}
}
];
Twinkle.speedy.redirectList = [
{
label: 'R1-1: 直接関係のないページへのリダイレクト',
value: 'リダイレクト1-1',
tooltip: '赤リンクへのリダイレクトは、必ずしも即時削除の理由とはなりません。'
},
{
label: 'R1-2: 単純な書き誤り',
value: 'リダイレクト1-2',
tooltip: 'よくある間違いや表記揺れの場合は、リダイレクトのまま存続となることがあります。少しでも微妙であればリダイレクトの削除依頼に提出してください。(1) 名前空間名の書き誤りなど - 存在しない名前空間のものなど。(2) 名前空間を間違えて移動したことによる残骸ページ。(3) 字体の似たひらがな・カタカナ・漢字等の取り違え。',
subgroup: {
name: 'err_reason',
type: 'input',
label: '書き誤り箇所:',
size: 40
}
},
{
label: 'R1-3: 転送先がないリダイレクトであって、他のページへのリダイレクトに変更できないもの',
value: 'リダイレクト1-3',
tooltip: 'ページ名の別名や略称など。ただし単に「現在の転送先が存在しない」というだけでは即時削除の対象となりません。転送先が変更できないか判断に迷う場合はリダイレクトの削除依頼へ提出してください。'
},
{
label: 'R1-4: 他プロジェクトへのリダイレクト',
value: 'リダイレクト1-4',
tooltip: '転送先がプロジェクト接頭辞(「en:」など)を含むものに限る'
},
{
label: 'R2-1: 半角と全角の使い分け違反',
value: 'リダイレクト2-1',
tooltip: '全角と半角の使い分けに反する半角カタカナ、全角英数字、全角スペースなどを使用しているもの',
subgroup: {
name: 'width_character',
type: 'input',
label: '使い分けが間違っている文字:',
size: 40
}
},
{
label: 'R2-2: 作品名を鍵括弧等でくくったもの',
value: 'リダイレクト2-2',
tooltip: '作品名を「」または『』などでくくっているもの'
},
{
label: 'R2-3: 作品名以外の記事名における読み・別表記等の併記',
value: 'リダイレクト2-3',
tooltip: '項目名に、読みがな、別名、別表記、原語を付記しているもの。作品名については読み等までを含めて正式名称である可能性を考慮し、リダイレクトの削除依頼で削除してください。'
},
{
label: 'R2-5: 曖昧さ回避括弧の使い方違反',
value: 'リダイレクト2-5',
tooltip: '(1) 項目名に曖昧さ回避の括弧を使用する場合に、左括弧の前に半角スペースがない、または、半角丸括弧以外を使用しているもの。(2) 曖昧さ回避括弧の中に曖昧さ回避括弧を含むもの。',
subgroup: {
name: 'usage_violation',
type: 'input',
label: '使い方の違反内容:',
size: 40
}
},
{
label: 'R2-6: 漢字表記人名の姓名間の空白',
value: 'リダイレクト2-6',
tooltip: '漢字で表記される人物の記事名で、姓と名の間に、半角全角かかわらず空白が入っているもの'
},
{
label: 'R4: 唯一の投稿者による移動の残骸で本人依頼のもの',
value: 'リダイレクト4',
tooltip: '漢字で表記される人物の記事名で、姓と名の間に、半角全角かかわらず空白が入っているもの'
}
];
Twinkle.speedy.normalizeHash = {
その他: 'db',
全般1: 'g1',
全般2: 'g2',
全般3: 'g3',
全般4: 'g4',
全般5: 'g5',
'全般5 (リダイレクト)': 'g5-r',
全般6: 'g6',
全般8: 'g8',
全般9: 'g9',
全般10: 'g10',
記事1: 'a1',
利用者ページ1: 'u1',
利用者ページ2: 'u2',
利用者ページ3: 'u3',
カテゴリ1: 'c1',
カテゴリ3: 'c3',
カテゴリ6: 'c6',
'ファイル1-2': 'f1-2',
'ファイル1-3': 'f1-3',
'ファイル1-4': 'f1-4',
'ファイル1-5': 'f1-5',
'ファイル1-6': 'f1-6',
ファイル3: 'f3',
ファイル5: 'f5',
ファイル6: 'f6',
ファイル7: 'f7',
ファイル8: 'f8',
ファイル9: 'f9',
'リダイレクト1-1': 'r1-1',
'リダイレクト1-2': 'r1-2',
'リダイレクト1-3': 'r1-3',
'リダイレクト1-4': 'r1-4',
'リダイレクト2-1': 'r2-1',
'リダイレクト2-2': 'r2-2',
'リダイレクト2-3': 'r2-3',
'リダイレクト2-5': 'r2-5',
'リダイレクト2-6': 'r2-6',
リダイレクト4: 'r4'
};
Twinkle.speedy.callbacks = {
getTemplateCodeAndParams: function(params) {
var code, parameters, i;
parameters = params.templateParams[0] || [];
code = '{{即時削除|1=' + params.values[0];
for (i in parameters) {
if (typeof parameters[i] === 'string') {
code += '|' + i + '=' + parameters[i];
}
}
code += '}}';
return code;
},
parseWikitext: function(wikitext, callback) {
var query = {
action: 'parse',
prop: 'text',
pst: 'true',
text: wikitext,
contentmodel: 'wikitext',
title: mw.config.get('wgPageName'),
disablelimitreport: true,
format: 'json'
};
var statusIndicator = new Morebits.Status('削除要約の生成');
var api = new Morebits.wiki.Api('即時削除テンプレートの構文解析', query, ((apiobj) => {
var reason = decodeURIComponent($(apiobj.getResponse().parse.text).find('#delete-reason').text()).replace(/\+/g, ' ');
if (!reason) {
statusIndicator.warn('削除テンプレートから要約を生成できませんでした');
} else {
statusIndicator.info('完了');
}
callback(reason);
}), statusIndicator);
api.post();
},
sysop: {
main: function(params) {
var code = Twinkle.speedy.callbacks.getTemplateCodeAndParams(params);
Twinkle.speedy.callbacks.parseWikitext(code, (reason) => {
if (params.promptForSummary) {
reason = prompt('使用する削除要約を入力するか、またはOKを押して自動的に生成される要約を使用します。', reason);
}
Twinkle.speedy.callbacks.sysop.deletePage(reason, params);
});
},
deletePage: function(reason, params) {
var thispage = new Morebits.wiki.Page(mw.config.get('wgPageName'), 'ページを削除');
if (reason === null) {
return Morebits.Status.error('理由を尋ねる', '中止');
} else if (!reason || !reason.replace(/^\s*/, '').replace(/\s*$/, '')) {
return Morebits.Status.error('理由を尋ねる', 'あなたは何も言わなかった。分からないよ...管理者とその無気力な態度は... 降参だ...');
}
var deleteMain = function(callback) {
thispage.setEditSummary(reason);
thispage.setWatchlist(params.watch);
thispage.deletePage(() => {
thispage.getStatusElement().info('完了');
typeof callback === 'function' && callback();
Twinkle.speedy.callbacks.sysop.deleteTalk(params);
});
};
deleteMain();
},
deleteTalk: function(params) {
// delete talk page
if (params.deleteTalkPage &&
params.normalized !== 'f8' &&
!document.getElementById('ca-talk').classList.contains('new')) {
var talkpage = new Morebits.wiki.Page(mw.config.get('wgFormattedNamespaces')[mw.config.get('wgNamespaceNumber') + 1] + ':' + mw.config.get('wgTitle'), 'トークページの削除');
talkpage.setEditSummary('削除された"' + Morebits.pageNameNorm + '"のトークページ');
talkpage.deletePage();
// this is ugly, but because of the architecture of wiki.api, it is needed
// (otherwise success/failure messages for the previous action would be suppressed)
window.setTimeout(() => {
Twinkle.speedy.callbacks.sysop.deleteRedirects(params);
}, 1800);
} else {
Twinkle.speedy.callbacks.sysop.deleteRedirects(params);
}
},
deleteRedirects: function(params) {
// delete redirects
if (params.deleteRedirects) {
var query = {
action: 'query',
titles: mw.config.get('wgPageName'),
prop: 'redirects',
rdlimit: 'max', // 500 is max for normal users, 5000 for bots and sysops
format: 'json'
};
var wikipedia_api = new Morebits.wiki.Api('リダイレクトの一覧を取得...', query, Twinkle.speedy.callbacks.sysop.deleteRedirectsMain,
new Morebits.Status('リダイレクトの削除'));
wikipedia_api.params = params;
wikipedia_api.post();
}
// promote Unlink tool
var $link, $bigtext;
if (mw.config.get('wgNamespaceNumber') === 6 && params.normalized !== 'f7') {
$link = $('<a/>').attr({
href: '#'
}).text('クリックしてリンク解除ツールへ進む')
.css({ fontSize: '130%', fontWeight: 'bold' }).on('click', () => {
Morebits.wiki.actionCompleted.redirect = null;
Twinkle.speedy.dialog.close();
Twinkle.unlink.callback('削除されたファイルの使用やリンクの除去 ' + Morebits.pageNameNorm);
});
$bigtext = $('<span/>').text('バックリンクを無効にし、ファイルの使用箇所を除去する。')
.css({ fontSize: '130%', fontWeight: 'bold' });
Morebits.Status.info($bigtext[0], $link[0]);
} else if (params.normalized !== 'f7') {
$link = $('<a/>').attr({
href: '#'
}).text('クリックしてリンク解除ツールへ進む')
.css({ fontSize: '130%', fontWeight: 'bold' }).on('click', () => {
Morebits.wiki.actionCompleted.redirect = null;
Twinkle.speedy.dialog.close();
Twinkle.unlink.callback('削除された ' + Morebits.pageNameNorm + ' へのリンクの削除 ');
});
$bigtext = $('<span/>').text('バックリンクを無効にする')
.css({ fontSize: '130%', fontWeight: 'bold' });
Morebits.Status.info($bigtext[0], $link[0]);
}
},
deleteRedirectsMain: function(apiobj) {
var response = apiobj.getResponse();
var snapshot = response.query.pages[0].redirects || [];
var total = snapshot.length;
var statusIndicator = apiobj.statelem;
if (!total) {
statusIndicator.status('リダイレクトが見つかりませんでした');
return;
}
statusIndicator.status('0%');
var current = 0;
var onsuccess = function(apiobjInner) {
var now = parseInt(100 * ++current / total, 10) + '%';
statusIndicator.update(now);
apiobjInner.statelem.unlink();
if (current >= total) {
statusIndicator.info(now + ' (完了)');
Morebits.wiki.removeCheckpoint();
}
};
Morebits.wiki.addCheckpoint();
snapshot.forEach((value) => {
var title = value.title;
var page = new Morebits.wiki.Page(title, 'リダイレクト "' + title + '" の削除');
page.setEditSummary('[[WP:CSD#R1-3|R1-3]]: 転送先がないリダイレクト 転送先: [["' + Morebits.pageNameNorm + ']]"');
page.deletePage(onsuccess);
});
}
},
user: {
main: function(pageobj) {
var statelem = pageobj.getStatusElement();
if (!pageobj.exists()) {
statelem.error('ページが存在しないようです。もしかしたら、すでに削除されているのかもしれません。');
return;
}
var params = pageobj.getCallbackParameters();
// given the params, builds the template parameters to the params that were passed in
// returns => <string> wikitext
var code = Twinkle.speedy.callbacks.getTemplateCodeAndParams(params);
// Tag if possible, post on talk if not
if (pageobj.canEdit() && ['wikitext', 'Scribunto', 'javascript', 'css', 'sanitized-css'].includes(pageobj.getContentModel()) && mw.config.get('wgNamespaceNumber') !== 710 /* TimedText */) {
var text = pageobj.getPageText();
statelem.status('ページ上のタグの確認...');
// check for existing deletion tags
var tag = /(?:\{\{\s*([sS][dD]2?|即時削除2?(\/.*)?|[dD]elete)(?:\s*\||\s*\}\}))/.exec(text);
// This won't make use of the db-multiple template but it probably should
if (tag && !confirm('ページ上には既にCSD関係のテンプレート {{' + tag[1] + '}} があります。別のCSDテンプレートを追加しますか?')) {
return;
}
var xfd = /\{\{((?:Sakujo\/本体)|[cfm]fd\b)/i.exec(text) || /RFD notice/.exec(text);
if (xfd && !confirm('削除関連のテンプレート {{' + xfd[1] + '}} が見つかりました。このまま即時削除テンプレートを追加しますか?')) {
return;
}
// curate/patrol the page
if (Twinkle.getPref('markSpeedyPagesAsPatrolled')) {
pageobj.triage();
}
// Wrap SD template in noinclude tags if we are in template space.
// Won't work with userboxes in userspace, or any other transcluded page outside template space
if (mw.config.get('wgNamespaceNumber') === 10) { // Template:
code = '<noinclude>' + code + '</noinclude>';
}
// Remove tags that become superfluous with this action
text = text.replace(/\{\{\s*([Uu]serspace draft)\s*(\|(?:\{\{[^{}]*\}\}|[^{}])*)?\}\}\s*/g, '');
if (mw.config.get('wgNamespaceNumber') === 6) {
// remove "move to Commons" tag - deletion-tagged files cannot be moved to Commons
text = Twinkle.removeMoveToCommonsTagsFromWikicode(text);
}
if (mw.config.get('wgPageContentModel') === 'Scribunto') {
// Scribunto isn't parsed like wikitext, so CSD templates on modules need special handling to work
var equals = '';
while (code.includes(']' + equals + ']')) {
equals += '=';
}
code = "require('Module:Module wikitext')._addText([" + equals + '[' + code + ']' + equals + ']);';
} else if (['javascript', 'css', 'sanitized-css'].includes(mw.config.get('wgPageContentModel'))) {
// Likewise for JS/CSS pages
code = '/* ' + code + ' */';
}
// Generate edit summary for edit
var editsummary;
if (params.normalizeds[0] === 'db') {
editsummary = '"' + params.templateParams[0]['2'] + '"として[[WP:CSD|即時削除]]を依頼';
} else {
editsummary = '即時削除を依頼 ([[WP:CSD#' + params.normalizeds[0].toUpperCase() + '|CSD ' + params.normalizeds[0].toUpperCase() + ']])';
}
// Blank pages
if (params.blankpage) {
text = code;
} else {
// Insert tag after short description or any hatnotes
var wikipage = new Morebits.wikitext.Page(text);
text = wikipage.insertAfterTemplates(code + '\n', Twinkle.hatnoteRegex).getText();
}
pageobj.setPageText(text);
pageobj.setEditSummary(editsummary);
pageobj.setWatchlist(params.watch);
pageobj.save(Twinkle.speedy.callbacks.user.tagComplete);
} else { // Attempt to place on talk page
var talkName = new mw.Title(pageobj.getPageName()).getTalkPage().toText();
if (talkName !== pageobj.getPageName()) {
pageobj.getStatusElement().warn('ページの編集ができなかったため、トークページにタグが置かれます。');
var talk_page = new Morebits.wiki.Page(talkName, 'トークページにタグを自動配置');
talk_page.setNewSectionTitle(pageobj.getPageName() + 'の即時削除依頼');
talk_page.setNewSectionText(code + '\n\n' + pageobj.getPageName() + 'に直接タグ付けできなかったため、この会話ページに即時削除タグを配置しました。対象ページの削除を依頼します。--~~~~');
talk_page.setCreateOption('recreate');
talk_page.setFollowRedirect(true);
talk_page.setWatchlist(params.watch);
talk_page.setCallbackParameters(params);
talk_page.newSection(Twinkle.speedy.callbacks.user.tagComplete);
} else {
pageobj.getStatusElement().error('ページを編集できず、即時削除を依頼できる別の場所もありません。中止します。');
}
}
},
tagComplete: function(pageobj) {
var params = pageobj.getCallbackParameters();
// Add this nomination to the user's userspace log
if (params.lognomination) {
Twinkle.speedy.callbacks.user.addToLog(params);
}
},
addToLog: function(params) {
var usl = new Morebits.UserspaceLogger(Twinkle.getPref('speedyLogPageName'));
usl.initialText =
'{{Notice|style=announce|text=即時削除の際、このページへのリンクは考慮する必要はありません。}}\n' +
'<!-- このテンプレートは、[[Wikipedia:即時削除の方針#リダイレクト]]にリンク元がある場合は削除しないよう定めてあるため、このページがリンクしていることによる影響を避けるためのものです。 -->\n\n' +
'このページは[[H:TW|Twinkle]]のCSDモジュールによって生成されており、[[WP:CSD|即時削除]]に指定したページを記録しています。\n\n' +
'記録をこれ以上保持したくない場合、[[利用者:Syunsyunminmin/Twinkle/Preferences|設定パネル]]を使用してこの機能をオフにした後、' +
'[[WP:CSD#U1|CSD U1]]でこのページの即時削除を依頼することが出来ます。' +
(Morebits.userIsSysop ? '\n\nこのログは、Twinkleを使用して行われた即時削除を完全に追跡してはいません。' : '');
var formatParamLog = function(normalize, csdparam, input) {
if ((normalize === 'C6' && csdparam === 'catdesc') ||
(normalize === 'F1' && csdparam === 'filename')) {
input = '[[:' + input + ']]';
} else if (normalize === 'G5' && csdparam === '2') {
input = '[[:Wikipedia:削除依頼/' + input + ']]';
} else if (normalize === 'G9' && csdparam.lastIndexOf('url', 0) === 0 && input.lastIndexOf('http', 0) === 0) {
input = '[' + input + ' ' + input + ']';
} else if (normalize === 'F8' && csdparam === 'filename') {
input = '[[commons:' + input + ']]';
}
return ' {' + normalize + ' ' + csdparam + ': ' + input + '}';
};
var extraInfo = '';
// If a logged file is deleted but exists on commons, the wikilink will be blue, so provide a link to the log
var fileLogLink = mw.config.get('wgNamespaceNumber') === 6 ? ' ([{{fullurl:Special:Log|page=' + mw.util.wikiUrlencode(mw.config.get('wgPageName')) + '}} 記録])' : '';
var editsummary = '[[:' + Morebits.pageNameNorm + ']]の即時削除への指定を記録' + Twinkle.summaryAd;
var appendText = '# [[:' + Morebits.pageNameNorm + ']]' + fileLogLink + ': ';
if (params.normalizeds[0] === 'db') {
appendText += '{{tlp|即時削除|その他}}';
} else {
appendText += '[[WP:CSD#' + params.normalizeds[0].toUpperCase() + '|CSD ' + params.normalizeds[0].toUpperCase() + ']] ({{tlp|即時削除|' + params.values[0] + '}})';
}
// If params is "empty" it will still be full of empty arrays, but ask anyway
if (params.templateParams) {
// Treat custom rationale individually
if (params.normalizeds[0] && params.normalizeds[0] === 'db') {
extraInfo += formatParamLog('Custom', 'rationale', params.templateParams[0]['1']);
} else {
params.templateParams.forEach((item, index) => {
var keys = Object.keys(item);
if (keys[0] !== undefined && keys[0].length > 0) {
// Second loop required since some items (G12, F9) may have multiple keys
keys.forEach((key, keyIndex) => {
if (keys[keyIndex] === 'blanked' || keys[keyIndex] === 'ts') {
return true; // Not worth logging
}
extraInfo += formatParamLog(params.normalizeds[index].toUpperCase(), keys[keyIndex], item[key]);
});
}
});
}
}
if (params.blankpage) {
appendText += '; ページを白紙化した上でのタグ付け。';
}
if (extraInfo) {
appendText += '; 追加情報:' + extraInfo;
}
appendText += ' ~~~~~\n';
usl.log(appendText, editsummary);
}
}
};
// validate subgroups in the form passed into the speedy deletion tag
Twinkle.speedy.getParameters = function twinklespeedyGetParameters(form, values) {
var parameters = [];
$.each(values, (index, value) => {
var currentParams = [];
if (form.add_comments && form.add_comments.checked) {
currentParams.コメント = form['add_comments.additional_comments'].value;
}
switch (value) {
case 'その他':
if (form['csd.reason_1']) {
var dbrationale = form['csd.reason_1'].value;
if (!dbrationale || !dbrationale.trim()) {
alert('その他の基準: 即時削除の方針に合致していることの説明を入力してください。');
parameters = null;
return false;
}
currentParams['2'] = dbrationale;
}
break;
case '全般4': // G4
if (form['csd.spam_reason']) {
if (form['csd.spam_reason'].value) {
currentParams['2'] = form['csd.spam_reason'].value;
} else {
alert('CSD G4: 「露骨な宣伝・広告のみが目的」と判断される根拠を入力してください。');
parameters = null;
return false;
}
}
break;
case '全般5': // G5
if (form['csd.repost_xfd']) {
var deldisc = form['csd.repost_xfd'].value;
if (deldisc) {
if (new RegExp('^:?' + Morebits.namespaceRegex(4) + ':', 'i').test(deldisc)) {
alert('CSD G5: 削除依頼サブページを入力する場合は、"Wikipedia:削除依頼/"は外して書く必要があります。');
parameters = null;
return false;
}
currentParams['2'] = deldisc;
} else {
alert('CSD G5: 削除依頼サブページを入力してください。');
parameters = null;
return false;
}
}
break;
case '全般6': // G6
if (form['csd.original_page'] && form['csd.language_code']) {
var oroginal = form['csd.original_page'].value,
lang = form['csd.language_code'].value;
if (!oroginal || !oroginal.trim()) {
alert('CSD G6: コピペ元のページ名を指定してください');
parameters = null;
return false;
}
if (lang) {
currentParams['3'] = lang;
}
currentParams['2'] = oroginal;
}
break;
case '全般9': // G9
if (form['csd.copyvio_url']) {
if (form['csd.copyvio_url'].value) {
currentParams['2'] = form['csd.copyvio_url'].value;
} else {
alert('CSD G9: 転載元のURLを入力してください。');
}
}
break;
case '全般10': // G10
if (form['csd.banned_user'] && form['csd.banned_user'].value) {
currentParams['2'] = form['csd.banned_user'].value;
}
break;
case '記事1': // A1
if (form['csd.no_definition']) {
var nodefinition = form['csd.no_definition'].value;
if (!nodefinition || !nodefinition.trim()) {
alert('CSD A1: 定義なしと判断した理由を入力してください');
parameters = null;
return false;
}
currentParams[2] = nodefinition;
}
break;
case 'カテゴリ6': // C6
if (form['csd.catdesc'] && form['csd.catdesc'].value) {
var catdesc = form['csd.catdesc'].value;
if (/^\[\[(.*)\]\]$/.test(catdesc)) {
alert('CSD C6: 議論場所は"[[]]"を外して入力してください。');
parameters = null;
return false;
}
currentParams['2'] = form['csd.catdesc'].value;
}
break;
case 'リダイレクト1-2': // R1-2
if (form['csd.err_reason'] && form['csd.err_reason'].value) {
if (!form['csd.err_reason'].value) {
alert('CSD R1-2: 書き誤り箇所を入力してください。');
parameters = null;
return false;
}
currentParams['2'] = form['csd.err_reason'].value;
}
break;
case 'リダイレクト2-1': // R2-1
if (form['csd.width_character'] && form['csd.width_character'].value) {
if (!form['csd.width_character'].value) {
alert('CSD R2-1: 使い分けが間違っている文字を入力してください。');
parameters = null;
return false;
}
currentParams['2'] = form['csd.width_character'].value;
}
break;
case 'リダイレクト2-5': // R2-5
if (form['csd.usage_violation'] && form['csd.usage_violation'].value) {
if (!form['csd.usage_violation'].value) {
alert('CSD R2-5: 使い方の違反内容を入力してください。');
parameters = null;
return false;
}
currentParams['2'] = form['csd.usage_violation'].value;
}
break;
case 'ファイル1-2': // F1-2
if (form['csd.redundantcopy_filename']) {
var redcopyimage = form['csd.redundantcopy_filename'].value;
currentParams['2'] = redcopyimage.replace(new RegExp('^\\s*' + Morebits.namespaceRegex(6) + ':', 'i'), '');
}
break;
case 'ファイル1-3': // F1-3
if (form['csd.redundantproject_filename']) {
var redprojimage = form['csd.redundantproject_filename'].value;
currentParams['2'] = redprojimage.replace(new RegExp('^\\s*' + Morebits.namespaceRegex(6) + ':', 'i'), '');
}
break;
case 'ファイル1-4': // F1-4
if (form['csd.redundantauthor_filename']) {
var redauthimage = form['csd.redundantauthor_filename'].value;
currentParams['2'] = redauthimage.replace(new RegExp('^\\s*' + Morebits.namespaceRegex(6) + ':', 'i'), '');
}
break;
case 'ファイル1-5': // F1-5
if (form['csd.movetocommons_filename']) {
var movedimage = form['csd.movetocommons_filename'].value;
currentParams['2'] = movedimage.replace(new RegExp('^\\s*' + Morebits.namespaceRegex(6) + ':', 'i'), '');
}
break;
case 'ファイル1-6': // F1-6
if (form['csd.redir_filename']) {
var redirimage = form['csd.redir_filename'].value;
if (redirimage) {
currentParams['2'] = redirimage.replace(new RegExp('^\\s*' + Morebits.namespaceRegex(6) + ':', 'i'), '');
} else {
alert('CSD F1-6: ファイルの転送先を入力してください。');
parameters = null;
return false;
}
}
break;
case 'ファイル3': // F3
if (form['csd.redundantimage_filename']) {
var redimage = form['csd.redundantimage_filename'].value;
if (redimage) {
currentParams['2'] = redimage.replace(new RegExp('^\\s*' + Morebits.namespaceRegex(6) + ':', 'i'), '');
} else {
alert('CSD F3: ファイルの重複先を入力してください。');
parameters = null;
return false;
}
}
break;
case 'ファイル5': // F5
if (form['csd.redundantimage_filename']) {
var notifytalkpage = form['csd.redundantimage_filename'].value;
if (notifytalkpage) {
if (/^\[\[(.*)\]\]$/.test(notifytalkpage)) {
alert('CSD F5: 通知先は"[[]]"を外して入力してください。');
parameters = null;
return false;
}
currentParams['2'] = notifytalkpage.replace(new RegExp('^\\s*' + Morebits.namespaceRegex(6) + ':', 'i'), '');
} else {
alert('CSD F5: 投稿者への通知先を入力してください。');
parameters = null;
return false;
}
}
break;
case 'ファイル6': // F6
if (form['csd.unfree_filename']) {
if (form['csd.unfree_filename'].value) {
currentParams['2'] = form['csd.unfree_filename'].value;
} else {
alert('CSD F6: 自由利用ができない根拠を入力してください。');
}
}
break;
default:
break;
}
parameters.push(currentParams);
});
return parameters;
};
/**
* @param {Event} e
* @return {Array}
*/
Twinkle.speedy.resolveCsdValues = function twinklespeedyResolveCsdValues(e) {
var values = (e.target.form ? e.target.form : e.target).getChecked('csd');
if (values.length === 0) {
alert('Please select a criterion!');
return null;
}
return values;
};
Twinkle.speedy.callback.evaluateSysop = function twinklespeedyCallbackEvaluateSysop(e) {
var form = e.target.form ? e.target.form : e.target;
if (e.target.type === 'checkbox' || e.target.type === 'text' ||
e.target.type === 'select') {
return;
}
var tag_only = form.tag_only;
if (tag_only && tag_only.checked) {
Twinkle.speedy.callback.evaluateUser(e);
return;
}
var values = Twinkle.speedy.resolveCsdValues(e);
if (!values) {
return;
}
var templateParams = Twinkle.speedy.getParameters(form, values);
if (!templateParams) {
return;
}
var normalizeds = values.map((value) => Twinkle.speedy.normalizeHash[value]);
// analyse each criterion to determine whether to watch the page, or prompt for summary
var watchPage, promptForSummary;
normalizeds.forEach((norm) => {
if (Twinkle.getPref('watchSpeedyPages').includes(norm)) {
watchPage = Twinkle.getPref('watchSpeedyExpiry');
}
if (Twinkle.getPref('promptForSpeedyDeletionSummary').includes(norm)) {
promptForSummary = true;
}
});
var notsavelog = form.notsavelog.checked;
var params = {
values: values,
normalizeds: normalizeds,
watch: watchPage,
deleteTalkPage: form.talkpage && form.talkpage.checked,
deleteRedirects: form.redirects.checked,
promptForSummary: promptForSummary,
templateParams: templateParams,
notsavelog: notsavelog
};
Morebits.SimpleWindow.setButtonsEnabled(false);
Morebits.Status.init(form);
Twinkle.speedy.callbacks.sysop.main(params);
};
Twinkle.speedy.callback.evaluateUser = function twinklespeedyCallbackEvaluateUser(e) {
var form = e.target.form ? e.target.form : e.target;
if (e.target.type === 'checkbox' || e.target.type === 'text' ||
e.target.type === 'select') {
return;
}
var values = Twinkle.speedy.resolveCsdValues(e);
if (!values) {
return;
}
var templateParams = Twinkle.speedy.getParameters(form, values);
if (!templateParams) {
return;
}
var normalizeds = values.map((value) => Twinkle.speedy.normalizeHash[value]);
// analyse each criterion to determine whether to watch the page the creator
var watchPage = normalizeds.some((csdCriteria) => Twinkle.getPref('watchSpeedyPages').includes(csdCriteria)) && Twinkle.getPref('watchSpeedyExpiry');
var csdlog = !form.notsavelog.checked && Twinkle.getPref('logSpeedyNominations') && normalizeds.some((norm) => !Twinkle.getPref('noLogOnSpeedyNomination').includes(norm));
var blankpage = form.blankpage.checked;
var notsavelog = form.notsavelog.checked;
var params = {
values: values,
normalizeds: normalizeds,
watch: watchPage,
lognomination: csdlog,
templateParams: templateParams,
blankpage: blankpage,
notsavelog: notsavelog
};
Morebits.SimpleWindow.setButtonsEnabled(false);
Morebits.Status.init(form);
Morebits.wiki.actionCompleted.redirect = mw.config.get('wgPageName');
Morebits.wiki.actionCompleted.notice = 'Tagging complete';
var wikipedia_page = new Morebits.wiki.Page(mw.config.get('wgPageName'), 'ページをタグ付け');
wikipedia_page.setCallbackParameters(params);
wikipedia_page.load(Twinkle.speedy.callbacks.user.main);
};
Twinkle.addInitCallback(Twinkle.speedy, 'speedy');
}(jQuery));
// </nowiki>
lw8tjfc9gy54pgzfnyjlg08g4ykilov
769564
769528
2026-10-22T02:19:39Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at d9cd3461baca43ac99ac2c24fb76a3aae6ddada9: minify
769564
javascript
text/javascript
// <nowiki>
(function(){Twinkle.speedy=function twinklespeedy(){if(mw.config.get("wgNamespaceNumber")<0||!mw.config.get("wgArticleId")){return}Twinkle.addPortletLink(Twinkle.speedy.callback,"CSD","tw-csd",Morebits.userIsSysop?"WP:CSDに従いページを削除する":"WP:CSDに従い即時削除を依頼")};Twinkle.speedy.callback=function twinklespeedyCallback(){Twinkle.speedy.initDialog(Morebits.userIsSysop?Twinkle.speedy.callback.evaluateSysop:Twinkle.speedy.callback.evaluateUser,true)};Twinkle.speedy.dialog=null;Twinkle.speedy.hasCSD=!!$("#delete-reason").length;Twinkle.speedy.initDialog=function twinklespeedyInitDialog(callbackfunc){var dialog;Twinkle.speedy.dialog=new Morebits.SimpleWindow(Twinkle.getPref("speedyWindowWidth"),Twinkle.getPref("speedyWindowHeight"));dialog=Twinkle.speedy.dialog;dialog.setTitle("即時削除の基準を選択する");dialog.setScriptName("Twinkle");dialog.addFooterLink("即時削除の方針","WP:CSD");dialog.addFooterLink("CSD設定","User:Syunsyunminmin/Twinkle/Preferences#speedy");dialog.addFooterLink("Twinkle help","WP:TW/DOC#speedy");dialog.addFooterLink("フィードバック","User talk:Syunsyunminmin/Twinkle");var form=new Morebits.QuickForm(callbackfunc,Twinkle.getPref("speedySelectionStyle")==="radioClick"?"change":null);if(Morebits.userIsSysop){form.append({type:"checkbox",list:[{label:"タグ付けのみ行い削除しない",value:"tag_only",name:"tag_only",tooltip:"今すぐページを削除するのではなく、タグ付けだけしたい場合",checked:!(Twinkle.speedy.hasCSD||mw.config.get("wgRelevantUserName")===mw.config.get("wgUserName")||Twinkle.getPref("deleteSysopDefaultToDelete")),event:function(event){var cForm=event.target.form;var cChecked=event.target.checked;if(cForm.talkpage){cForm.talkpage.checked=!cChecked&&Twinkle.getPref("deleteTalkPageOnDelete")}cForm.redirects.checked=!cChecked&&Twinkle.getPref("deleteRedirectsOnDelete");Twinkle.speedy.callback.modeChanged(cForm);event.stopPropagation()}}]});var deleteOptions=form.append({type:"div",name:"delete_options"});deleteOptions.append({type:"header",label:"削除関係のオプション"});if(mw.config.get("wgNamespaceNumber")%2===0&&(mw.config.get("wgNamespaceNumber")!==2||/\//.test(mw.config.get("wgTitle")))){deleteOptions.append({type:"checkbox",list:[{label:"トークページも削除",value:"talkpage",name:"talkpage",tooltip:"このオプションはこのページのトークページも削除します。日本語版には対応する方針がないため基本的にはチェックしないでください。",checked:Twinkle.getPref("deleteTalkPageOnDelete"),event:function(event){event.stopPropagation()}}]})}deleteOptions.append({type:"checkbox",list:[{label:"リダイレクトも削除",value:"redirects",name:"redirects",tooltip:"このオプションはこのページへのすべてのリダイレクトも削除します。転送先を変更できるかもしれないリダイレクトも削除してしまう可能性があります。",checked:Twinkle.getPref("deleteRedirectsOnDelete"),event:function(event){event.stopPropagation()}}]})}var tagOptions=form.append({type:"div",name:"tag_options"});if(Morebits.userIsSysop){tagOptions.append({type:"header",label:"削除関係のオプション"})}tagOptions.append({type:"checkbox",list:[{label:"ベージを白紙化する",value:"blankpage",name:"blankpage",tooltip:"削除タグを貼り付ける前にページを白紙化します。",event:function(event){event.stopPropagation()}},{label:"コメントを追加する",value:"add_comments",name:"add_comments",tooltip:"コメントを追加する",subgroup:{name:"additional_comments",type:"input",label:"コメント:",size:60},event:function(event){event.stopPropagation()}},{label:"ユーザーログを残さない",value:"notsavelog",name:"notsavelog",tooltip:"Twinkle設定でユーザーログを有効にしている場合、この項目にチェックすることでユーザーログを残さないようにします。"+"不適切な記事名など、ログを保持したくない場合に使用してください。",event:function(event){event.stopPropagation()}}]});form.append({type:"div",id:"prior-deletion-count",style:"font-style: italic"});form.append({type:"div",name:"work_area",label:"CSDモジュールの初期化に失敗しました。もう一度試すか、Twinkleの開発者に問題を伝えてください。"});if(Twinkle.getPref("speedySelectionStyle")!=="radioClick"){form.append({type:"submit",className:"tw-speedy-submit"})}var result=form.render();dialog.setContent(result);dialog.display();Twinkle.speedy.callback.modeChanged(result);Twinkle.speedy.callback.priorDeletionCount()};Twinkle.speedy.callback.modeChanged=function twinklespeedyCallbackModeChanged(form){var namespace=mw.config.get("wgNamespaceNumber");var mode={isSysop:!!form.tag_only&&!form.tag_only.checked,isRadioClick:Twinkle.getPref("speedySelectionStyle")==="radioClick"};if(mode.isSysop){$("[name=delete_options]").show();$("[name=tag_options]").hide();$("button.tw-speedy-submit").text("削除")}else{$("[name=delete_options]").hide();$("[name=tag_options]").show();$("button.tw-speedy-submit").text("タグ付け")}var work_area=new Morebits.QuickForm.Element({type:"div",name:"work_area"});var appendList=function(headerLabel,csdList){work_area.append({type:"header",label:headerLabel});work_area.append({type:"radio",name:"csd",list:Twinkle.speedy.generateCsdList(csdList,mode)})};if(mode.isSysop){appendList("その他の基準",Twinkle.speedy.customRationale)}if(!Morebits.isPageRedirect()){switch(namespace){case 0:case 1:appendList("記事",Twinkle.speedy.articleList);break;case 2:case 3:appendList("利用者ページ",Twinkle.speedy.userList);break;case 6:case 7:appendList("ファイル",Twinkle.speedy.fileList);if(!mode.isSysop){work_area.append({type:"div",label:'{{No source}}または{{No license}}のタグ付けは、Twinkleの "DI "タブを使って行うことができます。'})}break;case 14:case 15:appendList("カテゴリ",Twinkle.speedy.categoryList);break;default:break}}else{if(namespace===2||namespace===3){appendList("利用者ページ",Twinkle.speedy.userList)}appendList("リダイレクト",Twinkle.speedy.redirectList)}var generalCriteria=Twinkle.speedy.generalList;if(!mode.isSysop){generalCriteria=Twinkle.speedy.customRationale.concat(generalCriteria)}appendList("全般",generalCriteria);var old_area=Morebits.QuickForm.getElements(form,"work_area")[0];form.replaceChild(work_area.render(),old_area);if(mode.isSysop&&Twinkle.speedy.hasCSD){var customOption=$("input[name=csd][value=その他]")[0];if(customOption){if(Twinkle.getPref("speedySelectionStyle")!=="radioClick"){customOption.click();customOption.parentNode.appendChild(customOption.subgroup)}customOption.subgroup.querySelector("input").value=decodeURIComponent($("#delete-reason").text()).replace(/\+/g," ")}}};Twinkle.speedy.callback.priorDeletionCount=function(){var query={action:"query",format:"json",list:"logevents",letype:"delete",leaction:"delete/delete",letitle:mw.config.get("wgPageName"),leprop:"",lelimit:5};new Morebits.wiki.Api("過去の削除を確認",query,apiobj=>{var response=apiobj.getResponse();var delCount=response.query.logevents.length;if(delCount){var message=delCount+"回"+(response.continue?"以上":"")+"過去に削除";if(delCount>=3){$("#prior-deletion-count").css("color","red")}var link=Morebits.htmlNode("a","(記録)");link.setAttribute("href",mw.util.getUrl("Special:Log",{page:mw.config.get("wgPageName")}));link.setAttribute("target","_blank");$("#prior-deletion-count").text(message+" ");$("#prior-deletion-count").append(link)}}).post()};Twinkle.speedy.generateCsdList=function twinklespeedyGenerateCsdList(list,mode){var pageNamespace=mw.config.get("wgNamespaceNumber");var openSubgroupHandler=function(e){$(e.target.form).find("input").prop("disabled",true);$(e.target.form).children().css("color","gray");$(e.target).parent().css("color","black").find("input").prop("disabled",false);$(e.target).parent().find("input:text")[0].focus();e.stopPropagation()};var submitSubgroupHandler=function(e){var evaluateType=mode.isSysop?"evaluateSysop":"evaluateUser";Twinkle.speedy.callback[evaluateType](e);e.stopPropagation()};return $.map(list,critElement=>{var criterion=$.extend({},critElement);if(mode.isSysop){if(criterion.hideWhenSysop){return null}if(criterion.hideSubgroupWhenSysop){criterion.subgroup=null}}else{if(criterion.hideWhenUser){return null}if(criterion.hideSubgroupWhenUser){criterion.subgroup=null}}if(Morebits.isPageRedirect()&&criterion.hideWhenRedirect){return null}if(criterion.showInNamespaces&&!criterion.showInNamespaces.includes(pageNamespace)){return null}if(criterion.hideInNamespaces&&criterion.hideInNamespaces.includes(pageNamespace)){return null}if(criterion.subgroup&&mode.isRadioClick){if(Array.isArray(criterion.subgroup)){criterion.subgroup=criterion.subgroup.concat({type:"button",name:"submit",label:mode.isSysop?"ページを削除":"ページをタグ付け",event:submitSubgroupHandler})}else{criterion.subgroup=[criterion.subgroup,{type:"button",name:"submit",label:mode.isSysop?"ページを削除":"ページをタグ付け",event:submitSubgroupHandler}]}criterion.event=openSubgroupHandler}return criterion})};Twinkle.speedy.customRationale=[{label:"その他の基準。"+(Morebits.userIsSysop?" (カスタムの削除理由)":"{{即時削除|その他}}を使用します。"),value:"その他",tooltip:!Morebits.userIsSysop?"明らかに即時削除の方針の基準に合致しているが、各基準番号のテンプレートでどうしても呼び出せない場合など、どうしても各基準番号のテンプレートを利用できないようなやむを得ない場合の救済措置です。":"",subgroup:{name:"reason_1",type:"input",label:Morebits.userIsSysop?"その他の理由:":"即時削除の方針に合致していることの説明:",size:60}}];Twinkle.speedy.fileList=[{label:"F1-2: コモンズと重複(コピー)",value:"ファイル1-2",tooltip:"同名のファイルが日本語版ウィキペディアに存在しウィキメディア・コモンズから表示できない場合は除きます。",subgroup:{name:"redundantcopy_filename",type:"input",label:"コモンズでのファイル名:",tooltip:'接頭辞"File:"は省略してください',value:Morebits.pageNameNorm}},{label:"F1-3: コモンズと重複(他プロジェクト由来)",value:"ファイル1-3",tooltip:"他プロジェクト由来のファイルで、次の条件を全て満たすもの。(1) ウィキメディア・コモンズのファイル説明ページでそのプロジェクトを正しく出典元として掲示されている。(2) 出典元のファイル、ウィキメディア・コモンズのファイル、および日本語版のファイルのすべてに変更が加えられていないもの。",subgroup:{name:"redundantproject_filename",type:"input",label:"コモンズでのファイル名:",tooltip:'接頭辞"File:"は省略してください',value:Morebits.pageNameNorm}},{label:"F1-4: コモンズと重複(同一投稿者)",value:"ファイル1-4",tooltip:"ウィキメディア・コモンズに、日本語版ウィキペディアと同一の投稿者によって投稿されたもの",subgroup:{name:"redundantauthor_filename",type:"input",label:"コモンズでのファイル名:",tooltip:'接頭辞"File:"は省略してください',value:Morebits.pageNameNorm}},{label:"F1-5: コモンズへ移動",value:"ファイル1-5",tooltip:"次の条件を全て満たす、ウィキメディア・コモンズへコピーされたファイル。(1) コモンズのファイルページの初版に、ウィキペディア日本語版に記載されていたファイルの作者名、投稿日時、ライセンス情報が正確に転記されている。(2) コモンズのファイルページの初版または最新版に、ウィキペディア日本語版に記載されていた更新履歴、ページ内の説明文などを含む全てのファイル情報が記載されている。",subgroup:{name:"movetocommons_filename",type:"input",label:"コモンズでのファイル名:",tooltip:'接頭辞"File:"は省略してください',value:Morebits.pageNameNorm}},{label:"F1-6: 転送元・転送先がコモンズと同一構造のファイルリダイレクト",value:"ファイル1-6",tooltip:"転送元・転送先がコモンズと同一構造のファイルリダイレクトで、ローカルに転送先が存在しないもの",subgroup:{name:"redir_filename",type:"input",label:"転送先のファイル名:",tooltip:'接頭辞"File:"は省略してください'}},{label:"F3: 重複ファイル",value:"ファイル3",tooltip:"同じ様なファイルがあるという意味ではなく、ファイル名が違うだけで全く同じファイルを指します。",subgroup:{name:"redundantimage_filename",type:"input",label:"重複先のファイル名:",tooltip:'接頭辞"File:"は省略してください'}},{label:"F5: 出典またはライセンス不明のまま1週間経過",value:"ファイル5",tooltip:"次の各条件を全て満たすもの。(1) {{No source}}、{{No license}}の各タグが貼り付けられており、貼り付けから一週間以上経過している。(2) {{Image source}}等で投稿者に通知されており、通知から一週間以上経過している。当該のファイルについて個別に通知されている必要はなく、過去に同様な通知がされているならば、その時点から一週間が経過していればよい。",subgroup:{name:"notify_talkname",type:"input",label:"投稿者への通知先:"}},{label:"F6: 自由利用できないファイル",value:"ファイル6",tooltip:"ファイルの内容およびファイルページの記述によれば、著作権法上の理由により自由利用できないことが明らかであって、自由利用できないファイルの受け入れ方針にも適合しないファイル。ただし、ファイルページや記事の編集、改変したファイルの再アップロードなどの対応によって、自由利用できないファイルの受け入れ方針への違反状態を解消できる可能性がある場合には、当該ファイルページおよび投稿者の会話ページへの通知から一定期間が経過しても、依然として違反状態が解消されないことを条件として削除する。",subgroup:{name:"unfree_filename",type:"input",label:"自由利用ができない根拠:"}},{label:"F7: コモンズのファイルページ",value:"ファイル7",tooltip:"ウィキメディア・コモンズから呼び出しているファイル(秀逸な画像に選出された画像を除く)のファイルページ。ただし、有意な内容があるものについては、ウィキメディア・コモンズに転記した後に削除するものとする。"},{label:"F8: ファイルが存在しないファイルページ",value:"ファイル8",tooltip:"ローカルのファイルページ自体にアップロードされたメディアがない非リダイレクト。ただし、サーバ等のエラーによって一時的にファイルが見つからなくなっている場合があるので注意すること。"},{label:"F9: 初版投稿後7日以内の投稿者による依頼",value:"ファイル9",tooltip:"ファイルの投稿者自身によって、初版投稿時から168時間(7日間)以内に {{即時削除}} 貼付が行われたもので、リンク元がないか、そのページが削除されても問題のないリンクのみ(例えば削除依頼からのリンクなど)のもの。"}];Twinkle.speedy.articleList=[{label:"A1: 定義になっていない、あるいは文章になっていないもの",value:"記事1",tooltip:"定義がないということの意味は「『項目名は○○である』という定義文がない」という意味ではありません。冒頭に定型定義文がないことは、即時削除の理由とはなりません。また、定義文があっても百科事典としての解説に足る定義がないものは、対象となります。(1) 項目名だけ書いてあるもの。(2) 言語間リンク・カテゴリ・外部リンクのみのページ。(3) 文章になっているが、定義になっていないもの(例:「彼は非常に有名で多くの人に慕われていた。死ぬまでに3冊の本を書いて社会に貢献した。」)。(4) その他、単なる単語の羅列等。",subgroup:{name:"no_definition",type:"input",label:"定義なしと判断した理由:",size:60}}];Twinkle.speedy.categoryList=[{label:"C1: 初版からリダイレクト",value:"カテゴリ1",tooltip:"初版から継続して他のページへのリダイレクトであるもの。ただし、カテゴリページの移動により生成されたリダイレクトを除く。移動により生成されたリダイレクトは、C6の適用を検討してください。"},{label:"C3: 私的カテゴリ",value:"カテゴリ3",tooltip:"「Category:利用者:(利用者名)」のような、私的カテゴリ"},{label:"C6: 改名・統合等による事前合意を経て未使用となったカテゴリ",value:"カテゴリ6",tooltip:"事前の所定の議論による合意に基づく、移行(改名・統合)先へのカテゴリ変更により、空カテゴリとなったもの(統合には複数の異なる移行先への吸収合併を含む)",subgroup:{name:"catdesc",type:"input",label:"議論場所:",tooltip:'"[[]]"は外して入力してください。例: プロジェクト:カテゴリ関連/議論/20xx年/x月x日#議論セクション'}}];Twinkle.speedy.userList=[{label:"U1: 本人希望",value:"利用者ページ1",tooltip:"他の利用者の編集が入っているものについては、会話ページとして使用されてきた履歴があるもの(会話ページからの移動により作成された過去ログなど)を除き即時削除対象とする。"},{label:"U2: 登録されていない利用者の利用者ページ",value:"利用者ページ2",tooltip:"Wikipedia日本語版に利用者登録されていない利用者の利用者ページ、会話ページ。サブページを含む。利用者名変更により生じたリダイレクトは対象外とする。"},{label:"U3: IP利用者の利用者ページ",value:"利用者ページ3",tooltip:"IP利用者の利用者ページ / 利用者ページのサブページ。",hideWhenRedirect:true}];Twinkle.speedy.generalList=[{label:"G1: 意味不明な内容のページ",value:"全般1",tooltip:"内容が全く意味をもっておらず理解することができないページ。",hideInNamespaces:[2]},{label:"G2: テストページ",value:"全般2",tooltip:"編集やその他のウィキペディアの機能をテストするために作成されたページ。 User名前空間のページは含まれていません。",hideInNamespaces:[2]},{label:"G3: 荒らし投稿",value:"全般3",tooltip:"純粋な破壊行為のページ。"},{label:"G4: 露骨な宣伝・広告のみが目的",value:"全般4",tooltip:"露骨な宣伝・広告のみが目的と思われるページ。特定商品の宣伝・広告で全文が占められているものや、百科事典的な記事にするためには根本的に書き換える必要があるもののことです。単に企業あるいは製品を主題とした記事に関してはこの方針における削除対象にはならないことにご注意ください。また、立項アカウントが記事名(人名・団体名など)と同じということは即時削除理由になりません。",subgroup:{name:"spam_reason",type:"input",label:"「露骨な宣伝・広告のみが目的」と判断される根拠:",size:60}},{label:"G5: 削除されたページの改善なき再作成",value:"全般5",tooltip:"過去に削除依頼を経て削除されたページや文章・ファイルなどについて、その削除理由となった問題点が解消されていないものの再投稿。過去に削除審議を経ずして削除されたもの(削除依頼を一度も経ておらず、即時削除された履歴しかないもの)には適用されません。再作成された項目に、この条項以外の理由による即時削除理由が存在すれば、その即時削除理由を附して、再度の即時削除を求めることはできます。",subgroup:{name:"repost_xfd",type:"input",label:"削除の議論が行われたページ:",tooltip:'"Wikipedia:削除依頼/"は外して書く必要があります。"',size:60}},{label:"G6: ウィキペディア内のコピペ",value:"全般6",tooltip:"ウィキペディア(他言語版を含む)内ページのコピー&ペーストによって作成された、またはリダイレクトなどが上書きされ記事となったページで、ペースト後に意味のある加筆が行われていないもの。ただし翻訳や分割などコピー&ペーストが有意な目的のためになされており、かつ必要な履歴継承もなされている場合は除きます。また、移動すべき場合にもかかわらずコピー&ペーストにより作成されたページについても削除の対象となります。",hideInNamespaces:[6],subgroup:[{name:"original_page",type:"input",label:"コピペ元ページ名:"},{name:"language_code",type:"input",label:"言語コード:",tooltip:"日本語版の場合は指定不要です。",size:60}]},{label:"G8: 初版投稿者による依頼または白紙化",value:"全般8",tooltip:"初版投稿者または初めて有益な記述が行われた版の投稿者自身によって{{即時削除}}添付または白紙化が行われたもので、次の要件全てを満たしているもの: (1)ページの履歴にその投稿者の投稿しかない、あるいは、誤字脱字の訂正、カテゴリやリンクの追加など、ごく単純な編集しかないもの(2)移動により自動生成されたリダイレクトでないこと(3)ファイル名前空間ではないもの※ファイル名前空間の場合はファイル9で扱います。",hideInNamespaces:[6]},{label:"G9: 明白な著作権侵害",value:"全般9",tooltip:"次の条件を全て満たしており、著作権侵害が明白であると判断されるもの。次のうち1つでも当てはまらない可能性がある場合は削除依頼に回してください。(1)ウェブ上で認証なしで閲覧できるページからの完全な複製であるもの(翻案は含まない)(2)初版からほぼ全ての版において著作権侵害が認められ、著作権侵害部分を除去した場合に記事として成り立たないもの(3)自著作物の持ち込みでないことが明白であるもの(4)複製部分に明確な著作物性が認められるもの(きわめて短い文章、事実の羅列、リスト、表、年表、略歴等は含まない)(5)複製部分の著作権が保護期間内であることが明らかであるもの(6)複製元であるウェブページが著作権を有していることが明白であるもの(ウィキペディアもしくは他のウィキメディア・プロジェクトを含む、複製元ウェブページ以外のウェブサイトをソースとするものは含まない)(7)GFDLとCC BY-SA 3.0のデュアルライセンスで利用できないことが明らかであり、適切な引用とも認められないもの以上のうち1つでも当てはまらない可能性がある場合は削除依頼に回してください。",subgroup:{name:"copyvio_url",type:"input",label:"著作権の侵害元ページのURL:",tooltip:"「http://」または「https://」プロトコルを含むURLを入力してください。",size:60}},{label:'G10: <a href="https://ja.wikipedia.org/wiki/WP:CSD/G10" target="_blank">WP:CSD/G10</a>に掲載されている特定の荒らしが作成したページ',value:"全般10",tooltip:"Wikipedia:即時削除の方針/全般10に掲載されている荒らしが作成したページで、「ページの履歴にLTAまたはVIPの投稿しかない、あるいは、他利用者の履歴があっても、それが誤字脱字の訂正、カテゴリやリンクの追加など、ごく単純な編集でしかないもの」に該当しているもの。記事としての履歴がないリダイレクトのリダイレクト起こしも含む。",subgroup:{name:"banned_user",type:"input",label:"荒らしの名称:",size:40}}];Twinkle.speedy.redirectList=[{label:"R1-1: 直接関係のないページへのリダイレクト",value:"リダイレクト1-1",tooltip:"赤リンクへのリダイレクトは、必ずしも即時削除の理由とはなりません。"},{label:"R1-2: 単純な書き誤り",value:"リダイレクト1-2",tooltip:"よくある間違いや表記揺れの場合は、リダイレクトのまま存続となることがあります。少しでも微妙であればリダイレクトの削除依頼に提出してください。(1) 名前空間名の書き誤りなど - 存在しない名前空間のものなど。(2) 名前空間を間違えて移動したことによる残骸ページ。(3) 字体の似たひらがな・カタカナ・漢字等の取り違え。",subgroup:{name:"err_reason",type:"input",label:"書き誤り箇所:",size:40}},{label:"R1-3: 転送先がないリダイレクトであって、他のページへのリダイレクトに変更できないもの",value:"リダイレクト1-3",tooltip:"ページ名の別名や略称など。ただし単に「現在の転送先が存在しない」というだけでは即時削除の対象となりません。転送先が変更できないか判断に迷う場合はリダイレクトの削除依頼へ提出してください。"},{label:"R1-4: 他プロジェクトへのリダイレクト",value:"リダイレクト1-4",tooltip:"転送先がプロジェクト接頭辞(「en:」など)を含むものに限る"},{label:"R2-1: 半角と全角の使い分け違反",value:"リダイレクト2-1",tooltip:"全角と半角の使い分けに反する半角カタカナ、全角英数字、全角スペースなどを使用しているもの",subgroup:{name:"width_character",type:"input",label:"使い分けが間違っている文字:",size:40}},{label:"R2-2: 作品名を鍵括弧等でくくったもの",value:"リダイレクト2-2",tooltip:"作品名を「」または『』などでくくっているもの"},{label:"R2-3: 作品名以外の記事名における読み・別表記等の併記",value:"リダイレクト2-3",tooltip:"項目名に、読みがな、別名、別表記、原語を付記しているもの。作品名については読み等までを含めて正式名称である可能性を考慮し、リダイレクトの削除依頼で削除してください。"},{label:"R2-5: 曖昧さ回避括弧の使い方違反",value:"リダイレクト2-5",tooltip:"(1) 項目名に曖昧さ回避の括弧を使用する場合に、左括弧の前に半角スペースがない、または、半角丸括弧以外を使用しているもの。(2) 曖昧さ回避括弧の中に曖昧さ回避括弧を含むもの。",subgroup:{name:"usage_violation",type:"input",label:"使い方の違反内容:",size:40}},{label:"R2-6: 漢字表記人名の姓名間の空白",value:"リダイレクト2-6",tooltip:"漢字で表記される人物の記事名で、姓と名の間に、半角全角かかわらず空白が入っているもの"},{label:"R4: 唯一の投稿者による移動の残骸で本人依頼のもの",value:"リダイレクト4",tooltip:"漢字で表記される人物の記事名で、姓と名の間に、半角全角かかわらず空白が入っているもの"}];Twinkle.speedy.normalizeHash={"その他":"db","全般1":"g1","全般2":"g2","全般3":"g3","全般4":"g4","全般5":"g5","全般5 (リダイレクト)":"g5-r","全般6":"g6","全般8":"g8","全般9":"g9","全般10":"g10","記事1":"a1","利用者ページ1":"u1","利用者ページ2":"u2","利用者ページ3":"u3","カテゴリ1":"c1","カテゴリ3":"c3","カテゴリ6":"c6","ファイル1-2":"f1-2","ファイル1-3":"f1-3","ファイル1-4":"f1-4","ファイル1-5":"f1-5","ファイル1-6":"f1-6","ファイル3":"f3","ファイル5":"f5","ファイル6":"f6","ファイル7":"f7","ファイル8":"f8","ファイル9":"f9","リダイレクト1-1":"r1-1","リダイレクト1-2":"r1-2","リダイレクト1-3":"r1-3","リダイレクト1-4":"r1-4","リダイレクト2-1":"r2-1","リダイレクト2-2":"r2-2","リダイレクト2-3":"r2-3","リダイレクト2-5":"r2-5","リダイレクト2-6":"r2-6","リダイレクト4":"r4"};Twinkle.speedy.callbacks={getTemplateCodeAndParams:function(params){var code,parameters,i;parameters=params.templateParams[0]||[];code="{{即時削除|1="+params.values[0];for(i in parameters){if(typeof parameters[i]==="string"){code+="|"+i+"="+parameters[i]}}code+="}}";return code},parseWikitext:function(wikitext,callback){var query={action:"parse",prop:"text",pst:"true",text:wikitext,contentmodel:"wikitext",title:mw.config.get("wgPageName"),disablelimitreport:true,format:"json"};var statusIndicator=new Morebits.Status("削除要約の生成");var api=new Morebits.wiki.Api("即時削除テンプレートの構文解析",query,apiobj=>{var reason=decodeURIComponent($(apiobj.getResponse().parse.text).find("#delete-reason").text()).replace(/\+/g," ");if(!reason){statusIndicator.warn("削除テンプレートから要約を生成できませんでした")}else{statusIndicator.info("完了")}callback(reason)},statusIndicator);api.post()},sysop:{main:function(params){var code=Twinkle.speedy.callbacks.getTemplateCodeAndParams(params);Twinkle.speedy.callbacks.parseWikitext(code,reason=>{if(params.promptForSummary){reason=prompt("使用する削除要約を入力するか、またはOKを押して自動的に生成される要約を使用します。",reason)}Twinkle.speedy.callbacks.sysop.deletePage(reason,params)})},deletePage:function(reason,params){var thispage=new Morebits.wiki.Page(mw.config.get("wgPageName"),"ページを削除");if(reason===null){return Morebits.Status.error("理由を尋ねる","中止")}else if(!reason||!reason.replace(/^\s*/,"").replace(/\s*$/,"")){return Morebits.Status.error("理由を尋ねる","あなたは何も言わなかった。分からないよ...管理者とその無気力な態度は... 降参だ...")}var deleteMain=function(callback){thispage.setEditSummary(reason);thispage.setWatchlist(params.watch);thispage.deletePage(()=>{thispage.getStatusElement().info("完了");typeof callback==="function"&&callback();Twinkle.speedy.callbacks.sysop.deleteTalk(params)})};deleteMain()},deleteTalk:function(params){if(params.deleteTalkPage&¶ms.normalized!=="f8"&&!document.getElementById("ca-talk").classList.contains("new")){var talkpage=new Morebits.wiki.Page(mw.config.get("wgFormattedNamespaces")[mw.config.get("wgNamespaceNumber")+1]+":"+mw.config.get("wgTitle"),"トークページの削除");talkpage.setEditSummary('削除された"'+Morebits.pageNameNorm+'"のトークページ');talkpage.deletePage();window.setTimeout(()=>{Twinkle.speedy.callbacks.sysop.deleteRedirects(params)},1800)}else{Twinkle.speedy.callbacks.sysop.deleteRedirects(params)}},deleteRedirects:function(params){if(params.deleteRedirects){var query={action:"query",titles:mw.config.get("wgPageName"),prop:"redirects",rdlimit:"max",format:"json"};var wikipedia_api=new Morebits.wiki.Api("リダイレクトの一覧を取得...",query,Twinkle.speedy.callbacks.sysop.deleteRedirectsMain,new Morebits.Status("リダイレクトの削除"));wikipedia_api.params=params;wikipedia_api.post()}var $link,$bigtext;if(mw.config.get("wgNamespaceNumber")===6&¶ms.normalized!=="f7"){$link=$("<a/>").attr({href:"#"}).text("クリックしてリンク解除ツールへ進む").css({fontSize:"130%",fontWeight:"bold"}).on("click",()=>{Morebits.wiki.actionCompleted.redirect=null;Twinkle.speedy.dialog.close();Twinkle.unlink.callback("削除されたファイルの使用やリンクの除去 "+Morebits.pageNameNorm)});$bigtext=$("<span/>").text("バックリンクを無効にし、ファイルの使用箇所を除去する。").css({fontSize:"130%",fontWeight:"bold"});Morebits.Status.info($bigtext[0],$link[0])}else if(params.normalized!=="f7"){$link=$("<a/>").attr({href:"#"}).text("クリックしてリンク解除ツールへ進む").css({fontSize:"130%",fontWeight:"bold"}).on("click",()=>{Morebits.wiki.actionCompleted.redirect=null;Twinkle.speedy.dialog.close();Twinkle.unlink.callback("削除された "+Morebits.pageNameNorm+" へのリンクの削除 ")});$bigtext=$("<span/>").text("バックリンクを無効にする").css({fontSize:"130%",fontWeight:"bold"});Morebits.Status.info($bigtext[0],$link[0])}},deleteRedirectsMain:function(apiobj){var response=apiobj.getResponse();var snapshot=response.query.pages[0].redirects||[];var total=snapshot.length;var statusIndicator=apiobj.statelem;if(!total){statusIndicator.status("リダイレクトが見つかりませんでした");return}statusIndicator.status("0%");var current=0;var onsuccess=function(apiobjInner){var now=parseInt(100*++current/total,10)+"%";statusIndicator.update(now);apiobjInner.statelem.unlink();if(current>=total){statusIndicator.info(now+" (完了)");Morebits.wiki.removeCheckpoint()}};Morebits.wiki.addCheckpoint();snapshot.forEach(value=>{var title=value.title;var page=new Morebits.wiki.Page(title,'リダイレクト "'+title+'" の削除');page.setEditSummary('[[WP:CSD#R1-3|R1-3]]: 転送先がないリダイレクト 転送先: [["'+Morebits.pageNameNorm+']]"');page.deletePage(onsuccess)})}},user:{main:function(pageobj){var statelem=pageobj.getStatusElement();if(!pageobj.exists()){statelem.error("ページが存在しないようです。もしかしたら、すでに削除されているのかもしれません。");return}var params=pageobj.getCallbackParameters();var code=Twinkle.speedy.callbacks.getTemplateCodeAndParams(params);if(pageobj.canEdit()&&["wikitext","Scribunto","javascript","css","sanitized-css"].includes(pageobj.getContentModel())&&mw.config.get("wgNamespaceNumber")!==710){var text=pageobj.getPageText();statelem.status("ページ上のタグの確認...");var tag=/(?:\{\{\s*([sS][dD]2?|即時削除2?(\/.*)?|[dD]elete)(?:\s*\||\s*\}\}))/.exec(text);if(tag&&!confirm("ページ上には既にCSD関係のテンプレート {{"+tag[1]+"}} があります。別のCSDテンプレートを追加しますか?")){return}var xfd=/\{\{((?:Sakujo\/本体)|[cfm]fd\b)/i.exec(text)||/RFD notice/.exec(text);if(xfd&&!confirm("削除関連のテンプレート {{"+xfd[1]+"}} が見つかりました。このまま即時削除テンプレートを追加しますか?")){return}if(Twinkle.getPref("markSpeedyPagesAsPatrolled")){pageobj.triage()}if(mw.config.get("wgNamespaceNumber")===10){code="<noinclude>"+code+"</noinclude>"}text=text.replace(/\{\{\s*([Uu]serspace draft)\s*(\|(?:\{\{[^{}]*\}\}|[^{}])*)?\}\}\s*/g,"");if(mw.config.get("wgNamespaceNumber")===6){text=Twinkle.removeMoveToCommonsTagsFromWikicode(text)}if(mw.config.get("wgPageContentModel")==="Scribunto"){var equals="";while(code.includes("]"+equals+"]")){equals+="="}code="require('Module:Module wikitext')._addText(["+equals+"["+code+"]"+equals+"]);"}else if(["javascript","css","sanitized-css"].includes(mw.config.get("wgPageContentModel"))){code="/* "+code+" */"}var editsummary;if(params.normalizeds[0]==="db"){editsummary='"'+params.templateParams[0]["2"]+'"として[[WP:CSD|即時削除]]を依頼'}else{editsummary="即時削除を依頼 ([[WP:CSD#"+params.normalizeds[0].toUpperCase()+"|CSD "+params.normalizeds[0].toUpperCase()+"]])"}if(params.blankpage){text=code}else{var wikipage=new Morebits.wikitext.Page(text);text=wikipage.insertAfterTemplates(code+"\n",Twinkle.hatnoteRegex).getText()}pageobj.setPageText(text);pageobj.setEditSummary(editsummary);pageobj.setWatchlist(params.watch);pageobj.save(Twinkle.speedy.callbacks.user.tagComplete)}else{var talkName=new mw.Title(pageobj.getPageName()).getTalkPage().toText();if(talkName!==pageobj.getPageName()){pageobj.getStatusElement().warn("ページの編集ができなかったため、トークページにタグが置かれます。");var talk_page=new Morebits.wiki.Page(talkName,"トークページにタグを自動配置");talk_page.setNewSectionTitle(pageobj.getPageName()+"の即時削除依頼");talk_page.setNewSectionText(code+"\n\n"+pageobj.getPageName()+"に直接タグ付けできなかったため、この会話ページに即時削除タグを配置しました。対象ページの削除を依頼します。--~~~~");talk_page.setCreateOption("recreate");talk_page.setFollowRedirect(true);talk_page.setWatchlist(params.watch);talk_page.setCallbackParameters(params);talk_page.newSection(Twinkle.speedy.callbacks.user.tagComplete)}else{pageobj.getStatusElement().error("ページを編集できず、即時削除を依頼できる別の場所もありません。中止します。")}}},tagComplete:function(pageobj){var params=pageobj.getCallbackParameters();if(params.lognomination){Twinkle.speedy.callbacks.user.addToLog(params)}},addToLog:function(params){var usl=new Morebits.UserspaceLogger(Twinkle.getPref("speedyLogPageName"));usl.initialText="{{Notice|style=announce|text=即時削除の際、このページへのリンクは考慮する必要はありません。}}\n"+"\x3c!-- このテンプレートは、[[Wikipedia:即時削除の方針#リダイレクト]]にリンク元がある場合は削除しないよう定めてあるため、このページがリンクしていることによる影響を避けるためのものです。 --\x3e\n\n"+"このページは[[H:TW|Twinkle]]のCSDモジュールによって生成されており、[[WP:CSD|即時削除]]に指定したページを記録しています。\n\n"+"記録をこれ以上保持したくない場合、[[利用者:Syunsyunminmin/Twinkle/Preferences|設定パネル]]を使用してこの機能をオフにした後、"+"[[WP:CSD#U1|CSD U1]]でこのページの即時削除を依頼することが出来ます。"+(Morebits.userIsSysop?"\n\nこのログは、Twinkleを使用して行われた即時削除を完全に追跡してはいません。":"");var formatParamLog=function(normalize,csdparam,input){if(normalize==="C6"&&csdparam==="catdesc"||normalize==="F1"&&csdparam==="filename"){input="[[:"+input+"]]"}else if(normalize==="G5"&&csdparam==="2"){input="[[:Wikipedia:削除依頼/"+input+"]]"}else if(normalize==="G9"&&csdparam.lastIndexOf("url",0)===0&&input.lastIndexOf("http",0)===0){input="["+input+" "+input+"]"}else if(normalize==="F8"&&csdparam==="filename"){input="[[commons:"+input+"]]"}return" {"+normalize+" "+csdparam+": "+input+"}"};var extraInfo="";var fileLogLink=mw.config.get("wgNamespaceNumber")===6?" ([{{fullurl:Special:Log|page="+mw.util.wikiUrlencode(mw.config.get("wgPageName"))+"}} 記録])":"";var editsummary="[[:"+Morebits.pageNameNorm+"]]の即時削除への指定を記録"+Twinkle.summaryAd;var appendText="# [[:"+Morebits.pageNameNorm+"]]"+fileLogLink+": ";if(params.normalizeds[0]==="db"){appendText+="{{tlp|即時削除|その他}}"}else{appendText+="[[WP:CSD#"+params.normalizeds[0].toUpperCase()+"|CSD "+params.normalizeds[0].toUpperCase()+"]] ({{tlp|即時削除|"+params.values[0]+"}})"}if(params.templateParams){if(params.normalizeds[0]&¶ms.normalizeds[0]==="db"){extraInfo+=formatParamLog("Custom","rationale",params.templateParams[0]["1"])}else{params.templateParams.forEach((item,index)=>{var keys=Object.keys(item);if(keys[0]!==undefined&&keys[0].length>0){keys.forEach((key,keyIndex)=>{if(keys[keyIndex]==="blanked"||keys[keyIndex]==="ts"){return true}extraInfo+=formatParamLog(params.normalizeds[index].toUpperCase(),keys[keyIndex],item[key])})}})}}if(params.blankpage){appendText+="; ページを白紙化した上でのタグ付け。"}if(extraInfo){appendText+="; 追加情報:"+extraInfo}appendText+=" ~~~~~\n";usl.log(appendText,editsummary)}}};Twinkle.speedy.getParameters=function twinklespeedyGetParameters(form,values){var parameters=[];$.each(values,(index,value)=>{var currentParams=[];if(form.add_comments&&form.add_comments.checked){currentParams.コメント=form["add_comments.additional_comments"].value}switch(value){case"その他":if(form["csd.reason_1"]){var dbrationale=form["csd.reason_1"].value;if(!dbrationale||!dbrationale.trim()){alert("その他の基準: 即時削除の方針に合致していることの説明を入力してください。");parameters=null;return false}currentParams["2"]=dbrationale}break;case"全般4":if(form["csd.spam_reason"]){if(form["csd.spam_reason"].value){currentParams["2"]=form["csd.spam_reason"].value}else{alert("CSD G4: 「露骨な宣伝・広告のみが目的」と判断される根拠を入力してください。");parameters=null;return false}}break;case"全般5":if(form["csd.repost_xfd"]){var deldisc=form["csd.repost_xfd"].value;if(deldisc){if(new RegExp("^:?"+Morebits.namespaceRegex(4)+":","i").test(deldisc)){alert('CSD G5: 削除依頼サブページを入力する場合は、"Wikipedia:削除依頼/"は外して書く必要があります。');parameters=null;return false}currentParams["2"]=deldisc}else{alert("CSD G5: 削除依頼サブページを入力してください。");parameters=null;return false}}break;case"全般6":if(form["csd.original_page"]&&form["csd.language_code"]){var oroginal=form["csd.original_page"].value,lang=form["csd.language_code"].value;if(!oroginal||!oroginal.trim()){alert("CSD G6: コピペ元のページ名を指定してください");parameters=null;return false}if(lang){currentParams["3"]=lang}currentParams["2"]=oroginal}break;case"全般9":if(form["csd.copyvio_url"]){if(form["csd.copyvio_url"].value){currentParams["2"]=form["csd.copyvio_url"].value}else{alert("CSD G9: 転載元のURLを入力してください。")}}break;case"全般10":if(form["csd.banned_user"]&&form["csd.banned_user"].value){currentParams["2"]=form["csd.banned_user"].value}break;case"記事1":if(form["csd.no_definition"]){var nodefinition=form["csd.no_definition"].value;if(!nodefinition||!nodefinition.trim()){alert("CSD A1: 定義なしと判断した理由を入力してください");parameters=null;return false}currentParams[2]=nodefinition}break;case"カテゴリ6":if(form["csd.catdesc"]&&form["csd.catdesc"].value){var catdesc=form["csd.catdesc"].value;if(/^\[\[(.*)\]\]$/.test(catdesc)){alert('CSD C6: 議論場所は"[[]]"を外して入力してください。');parameters=null;return false}currentParams["2"]=form["csd.catdesc"].value}break;case"リダイレクト1-2":if(form["csd.err_reason"]&&form["csd.err_reason"].value){if(!form["csd.err_reason"].value){alert("CSD R1-2: 書き誤り箇所を入力してください。");parameters=null;return false}currentParams["2"]=form["csd.err_reason"].value}break;case"リダイレクト2-1":if(form["csd.width_character"]&&form["csd.width_character"].value){if(!form["csd.width_character"].value){alert("CSD R2-1: 使い分けが間違っている文字を入力してください。");parameters=null;return false}currentParams["2"]=form["csd.width_character"].value}break;case"リダイレクト2-5":if(form["csd.usage_violation"]&&form["csd.usage_violation"].value){if(!form["csd.usage_violation"].value){alert("CSD R2-5: 使い方の違反内容を入力してください。");parameters=null;return false}currentParams["2"]=form["csd.usage_violation"].value}break;case"ファイル1-2":if(form["csd.redundantcopy_filename"]){var redcopyimage=form["csd.redundantcopy_filename"].value;currentParams["2"]=redcopyimage.replace(new RegExp("^\\s*"+Morebits.namespaceRegex(6)+":","i"),"")}break;case"ファイル1-3":if(form["csd.redundantproject_filename"]){var redprojimage=form["csd.redundantproject_filename"].value;currentParams["2"]=redprojimage.replace(new RegExp("^\\s*"+Morebits.namespaceRegex(6)+":","i"),"")}break;case"ファイル1-4":if(form["csd.redundantauthor_filename"]){var redauthimage=form["csd.redundantauthor_filename"].value;currentParams["2"]=redauthimage.replace(new RegExp("^\\s*"+Morebits.namespaceRegex(6)+":","i"),"")}break;case"ファイル1-5":if(form["csd.movetocommons_filename"]){var movedimage=form["csd.movetocommons_filename"].value;currentParams["2"]=movedimage.replace(new RegExp("^\\s*"+Morebits.namespaceRegex(6)+":","i"),"")}break;case"ファイル1-6":if(form["csd.redir_filename"]){var redirimage=form["csd.redir_filename"].value;if(redirimage){currentParams["2"]=redirimage.replace(new RegExp("^\\s*"+Morebits.namespaceRegex(6)+":","i"),"")}else{alert("CSD F1-6: ファイルの転送先を入力してください。");parameters=null;return false}}break;case"ファイル3":if(form["csd.redundantimage_filename"]){var redimage=form["csd.redundantimage_filename"].value;if(redimage){currentParams["2"]=redimage.replace(new RegExp("^\\s*"+Morebits.namespaceRegex(6)+":","i"),"")}else{alert("CSD F3: ファイルの重複先を入力してください。");parameters=null;return false}}break;case"ファイル5":if(form["csd.redundantimage_filename"]){var notifytalkpage=form["csd.redundantimage_filename"].value;if(notifytalkpage){if(/^\[\[(.*)\]\]$/.test(notifytalkpage)){alert('CSD F5: 通知先は"[[]]"を外して入力してください。');parameters=null;return false}currentParams["2"]=notifytalkpage.replace(new RegExp("^\\s*"+Morebits.namespaceRegex(6)+":","i"),"")}else{alert("CSD F5: 投稿者への通知先を入力してください。");parameters=null;return false}}break;case"ファイル6":if(form["csd.unfree_filename"]){if(form["csd.unfree_filename"].value){currentParams["2"]=form["csd.unfree_filename"].value}else{alert("CSD F6: 自由利用ができない根拠を入力してください。")}}break;default:break}parameters.push(currentParams)});return parameters};Twinkle.speedy.resolveCsdValues=function twinklespeedyResolveCsdValues(e){var values=(e.target.form?e.target.form:e.target).getChecked("csd");if(values.length===0){alert("Please select a criterion!");return null}return values};Twinkle.speedy.callback.evaluateSysop=function twinklespeedyCallbackEvaluateSysop(e){var form=e.target.form?e.target.form:e.target;if(e.target.type==="checkbox"||e.target.type==="text"||e.target.type==="select"){return}var tag_only=form.tag_only;if(tag_only&&tag_only.checked){Twinkle.speedy.callback.evaluateUser(e);return}var values=Twinkle.speedy.resolveCsdValues(e);if(!values){return}var templateParams=Twinkle.speedy.getParameters(form,values);if(!templateParams){return}var normalizeds=values.map(value=>Twinkle.speedy.normalizeHash[value]);var watchPage,promptForSummary;normalizeds.forEach(norm=>{if(Twinkle.getPref("watchSpeedyPages").includes(norm)){watchPage=Twinkle.getPref("watchSpeedyExpiry")}if(Twinkle.getPref("promptForSpeedyDeletionSummary").includes(norm)){promptForSummary=true}});var notsavelog=form.notsavelog.checked;var params={values:values,normalizeds:normalizeds,watch:watchPage,deleteTalkPage:form.talkpage&&form.talkpage.checked,deleteRedirects:form.redirects.checked,promptForSummary:promptForSummary,templateParams:templateParams,notsavelog:notsavelog};Morebits.SimpleWindow.setButtonsEnabled(false);Morebits.Status.init(form);Twinkle.speedy.callbacks.sysop.main(params)};Twinkle.speedy.callback.evaluateUser=function twinklespeedyCallbackEvaluateUser(e){var form=e.target.form?e.target.form:e.target;if(e.target.type==="checkbox"||e.target.type==="text"||e.target.type==="select"){return}var values=Twinkle.speedy.resolveCsdValues(e);if(!values){return}var templateParams=Twinkle.speedy.getParameters(form,values);if(!templateParams){return}var normalizeds=values.map(value=>Twinkle.speedy.normalizeHash[value]);var watchPage=normalizeds.some(csdCriteria=>Twinkle.getPref("watchSpeedyPages").includes(csdCriteria))&&Twinkle.getPref("watchSpeedyExpiry");var csdlog=!form.notsavelog.checked&&Twinkle.getPref("logSpeedyNominations")&&normalizeds.some(norm=>!Twinkle.getPref("noLogOnSpeedyNomination").includes(norm));var blankpage=form.blankpage.checked;var notsavelog=form.notsavelog.checked;var params={values:values,normalizeds:normalizeds,watch:watchPage,lognomination:csdlog,templateParams:templateParams,blankpage:blankpage,notsavelog:notsavelog};Morebits.SimpleWindow.setButtonsEnabled(false);Morebits.Status.init(form);Morebits.wiki.actionCompleted.redirect=mw.config.get("wgPageName");Morebits.wiki.actionCompleted.notice="Tagging complete";var wikipedia_page=new Morebits.wiki.Page(mw.config.get("wgPageName"),"ページをタグ付け");wikipedia_page.setCallbackParameters(params);wikipedia_page.load(Twinkle.speedy.callbacks.user.main)};Twinkle.addInitCallback(Twinkle.speedy,"speedy")})(jQuery);
// </nowiki>
di549uperwfu13goztfthwjasp98r2y
User:Syunsyunminmin/Twinkle/twinklediff.js
2
179397
769462
2026-10-21T13:57:59Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 3680108:
769462
javascript
text/javascript
/*
Twinkle: adapted for Japanese Wikipedia; upstream authors and Japanese adaptations are credited in UPSTREAM.md.
Upstream source: https://github.com/wikimedia-gadgets/twinkle/blob/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/src/modules/twinklediff.js
Upstream author history: https://github.com/wikimedia-gadgets/twinkle/commits/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/src/modules/twinklediff.js
License: CC BY-SA 3.0 Unported. See https://github.com/wikimedia-gadgets/twinkle/blob/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/LICENSE.txt
Japanese adaptation: Syunsyunminmin; includes later synchronization changes.
*/
// <nowiki>
(function() {
/*
****************************************
*** twinklediff.js: Diff module
****************************************
* Mode of invocation: Tab on non-diff pages ("Last"); tabs on diff pages ("Since", "Since mine", "Current")
* Active on: Existing non-special pages
*/
Twinkle.diff = function twinklediff() {
if (mw.config.get('wgNamespaceNumber') < 0 || !mw.config.get('wgArticleId')) {
return;
}
Twinkle.addPortletLink(mw.util.getUrl(mw.config.get('wgPageName'), {diff: 'cur', oldid: 'prev'}), 'Last', 'tw-lastdiff', '最新版の差分を表示');
// Show additional tabs only on diff pages
if (mw.config.get('wgDiffNewId')) {
Twinkle.addPortletLink(() => {
Twinkle.diff.evaluate(false);
}, 'Since', 'tw-since', '最新版とこの利用者による最後の版との差分を表示');
Twinkle.addPortletLink(() => {
Twinkle.diff.evaluate(true);
}, 'Since mine', 'tw-sincemine', '最新版と自分による最後の版との差分を表示');
Twinkle.addPortletLink(mw.util.getUrl(mw.config.get('wgPageName'), {diff: 'cur', oldid: mw.config.get('wgDiffNewId')}), 'Current', 'tw-curdiff', '最新版との差分を表示');
}
};
Twinkle.diff.evaluate = function twinklediffEvaluate(me) {
let user;
if (me) {
user = mw.config.get('wgUserName');
} else {
const node = document.getElementById('mw-diff-ntitle2');
if (!node) {
// nothing to do?
return;
}
user = $(node).find('a').first().text();
}
const query = {
prop: 'revisions',
action: 'query',
titles: mw.config.get('wgPageName'),
rvlimit: 1,
rvprop: ['ids', 'user'],
rvstartid: mw.config.get('wgCurRevisionId') - 1, // i.e. not the current one
rvuser: user,
format: 'json'
};
Morebits.Status.init(document.getElementById('mw-content-text'));
var wikipedia_api = new Morebits.wiki.Api('初版投稿者のデータを取得', query, Twinkle.diff.callbacks.main);
wikipedia_api.params = { user: user };
wikipedia_api.post();
};
Twinkle.diff.callbacks = {
main: function(self) {
const rev = self.response.query.pages[0].revisions;
const revid = rev && rev[0].revid;
if (!revid) {
self.statelem.error('適切な過去版が見つからないか、' + self.params.user + 'が唯一の投稿者です。中止します。');
return;
}
window.location = mw.util.getUrl(mw.config.get('wgPageName'), {
diff: mw.config.get('wgCurRevisionId'),
oldid: revid
});
}
};
Twinkle.addInitCallback(Twinkle.diff, 'diff');
}());
// </nowiki>
4x8gnwzso2j6vcg4h8ijwsogh3c6f78
769529
769462
2026-10-22T00:00:22Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 1406828: rm cmt
769529
javascript
text/javascript
// <nowiki>
(function() {
/*
****************************************
*** twinklediff.js: Diff module
****************************************
* Mode of invocation: Tab on non-diff pages ("Last"); tabs on diff pages ("Since", "Since mine", "Current")
* Active on: Existing non-special pages
*/
Twinkle.diff = function twinklediff() {
if (mw.config.get('wgNamespaceNumber') < 0 || !mw.config.get('wgArticleId')) {
return;
}
Twinkle.addPortletLink(mw.util.getUrl(mw.config.get('wgPageName'), {diff: 'cur', oldid: 'prev'}), 'Last', 'tw-lastdiff', '最新版の差分を表示');
// Show additional tabs only on diff pages
if (mw.config.get('wgDiffNewId')) {
Twinkle.addPortletLink(() => {
Twinkle.diff.evaluate(false);
}, 'Since', 'tw-since', '最新版とこの利用者による最後の版との差分を表示');
Twinkle.addPortletLink(() => {
Twinkle.diff.evaluate(true);
}, 'Since mine', 'tw-sincemine', '最新版と自分による最後の版との差分を表示');
Twinkle.addPortletLink(mw.util.getUrl(mw.config.get('wgPageName'), {diff: 'cur', oldid: mw.config.get('wgDiffNewId')}), 'Current', 'tw-curdiff', '最新版との差分を表示');
}
};
Twinkle.diff.evaluate = function twinklediffEvaluate(me) {
let user;
if (me) {
user = mw.config.get('wgUserName');
} else {
const node = document.getElementById('mw-diff-ntitle2');
if (!node) {
// nothing to do?
return;
}
user = $(node).find('a').first().text();
}
const query = {
prop: 'revisions',
action: 'query',
titles: mw.config.get('wgPageName'),
rvlimit: 1,
rvprop: ['ids', 'user'],
rvstartid: mw.config.get('wgCurRevisionId') - 1, // i.e. not the current one
rvuser: user,
format: 'json'
};
Morebits.Status.init(document.getElementById('mw-content-text'));
var wikipedia_api = new Morebits.wiki.Api('初版投稿者のデータを取得', query, Twinkle.diff.callbacks.main);
wikipedia_api.params = { user: user };
wikipedia_api.post();
};
Twinkle.diff.callbacks = {
main: function(self) {
const rev = self.response.query.pages[0].revisions;
const revid = rev && rev[0].revid;
if (!revid) {
self.statelem.error('適切な過去版が見つからないか、' + self.params.user + 'が唯一の投稿者です。中止します。');
return;
}
window.location = mw.util.getUrl(mw.config.get('wgPageName'), {
diff: mw.config.get('wgCurRevisionId'),
oldid: revid
});
}
};
Twinkle.addInitCallback(Twinkle.diff, 'diff');
}());
// </nowiki>
aip8k7bpl1we7np7o557kcuafkzx759
769565
769529
2026-10-22T02:20:40Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at d9cd3461baca43ac99ac2c24fb76a3aae6ddada9: minify
769565
javascript
text/javascript
// <nowiki>
(function(){Twinkle.diff=function twinklediff(){if(mw.config.get("wgNamespaceNumber")<0||!mw.config.get("wgArticleId")){return}Twinkle.addPortletLink(mw.util.getUrl(mw.config.get("wgPageName"),{diff:"cur",oldid:"prev"}),"Last","tw-lastdiff","最新版の差分を表示");if(mw.config.get("wgDiffNewId")){Twinkle.addPortletLink(()=>{Twinkle.diff.evaluate(false)},"Since","tw-since","最新版とこの利用者による最後の版との差分を表示");Twinkle.addPortletLink(()=>{Twinkle.diff.evaluate(true)},"Since mine","tw-sincemine","最新版と自分による最後の版との差分を表示");Twinkle.addPortletLink(mw.util.getUrl(mw.config.get("wgPageName"),{diff:"cur",oldid:mw.config.get("wgDiffNewId")}),"Current","tw-curdiff","最新版との差分を表示")}};Twinkle.diff.evaluate=function twinklediffEvaluate(me){let user;if(me){user=mw.config.get("wgUserName")}else{const node=document.getElementById("mw-diff-ntitle2");if(!node){return}user=$(node).find("a").first().text()}const query={prop:"revisions",action:"query",titles:mw.config.get("wgPageName"),rvlimit:1,rvprop:["ids","user"],rvstartid:mw.config.get("wgCurRevisionId")-1,rvuser:user,format:"json"};Morebits.Status.init(document.getElementById("mw-content-text"));var wikipedia_api=new Morebits.wiki.Api("初版投稿者のデータを取得",query,Twinkle.diff.callbacks.main);wikipedia_api.params={user:user};wikipedia_api.post()};Twinkle.diff.callbacks={main:function(self){const rev=self.response.query.pages[0].revisions;const revid=rev&&rev[0].revid;if(!revid){self.statelem.error("適切な過去版が見つからないか、"+self.params.user+"が唯一の投稿者です。中止します。");return}window.location=mw.util.getUrl(mw.config.get("wgPageName"),{diff:mw.config.get("wgCurRevisionId"),oldid:revid})}};Twinkle.addInitCallback(Twinkle.diff,"diff")})();
// </nowiki>
tsz1pxiu1uo1njp2ezb6h41oqd6da6o
User:Syunsyunminmin/Twinkle/twinkleunlink.js
2
179398
769463
2026-10-21T13:58:04Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 3680108:
769463
javascript
text/javascript
/*
Twinkle: adapted for Japanese Wikipedia; upstream authors and Japanese adaptations are credited in UPSTREAM.md.
Upstream source: https://github.com/wikimedia-gadgets/twinkle/blob/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/src/modules/twinkleunlink.js
Upstream author history: https://github.com/wikimedia-gadgets/twinkle/commits/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/src/modules/twinkleunlink.js
License: CC BY-SA 3.0 Unported. See https://github.com/wikimedia-gadgets/twinkle/blob/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/LICENSE.txt
Japanese adaptation: Syunsyunminmin; includes later synchronization changes.
*/
// <nowiki>
(function() {
/*
****************************************
*** twinkleunlink.js: Unlink module
****************************************
* Mode of invocation: Tab ("Unlink")
* Active on: Non-special pages, except Wikipedia:Sandbox
*/
Twinkle.unlink = function twinkleunlink() {
if (mw.config.get('wgNamespaceNumber') < 0 || mw.config.get('wgPageName') === 'Wikipedia:サンドボックス' ||
// Restrict to extended confirmed users (see #428)
(!Morebits.userIsInGroup('extendedconfirmed') && !Morebits.userIsSysop)) {
return;
}
Twinkle.addPortletLink(Twinkle.unlink.callback, 'Unlink', 'tw-unlink', 'バックリンクの解除');
};
// the parameter is used when invoking unlink from admin speedy
Twinkle.unlink.callback = function(presetReason) {
const fileSpace = mw.config.get('wgNamespaceNumber') === 6;
var Window = new Morebits.SimpleWindow(600, 440);
Window.setTitle('バックリンク' + (fileSpace ? 'とファイルの使用' : '') + 'を解除する');
Window.setScriptName('Twinkle');
Window.addFooterLink('Unlink設定', 'User:Syunsyunminmin/Twinkle/Preferences#unlink');
Window.addFooterLink('Twinkle help', ':en:WP:TW/DOC#unlink');
Window.addFooterLink('Give feedback', 'User Talk:Syunsyunminmin/Twinkle');
const form = new Morebits.QuickForm(Twinkle.unlink.callback.evaluate);
// prepend some documentation: files are commented out, while any
// display text is preserved for links (otherwise the link itself is used)
const linkTextBefore = Morebits.htmlNode('code', '[[' + (fileSpace ? ':' : '') + Morebits.pageNameNorm + '|link text]]');
const linkTextAfter = Morebits.htmlNode('code', 'link text');
const linkPlainBefore = Morebits.htmlNode('code', '[[' + Morebits.pageNameNorm + ']]');
let linkPlainAfter;
if (fileSpace) {
linkPlainAfter = Morebits.htmlNode('code', '<!-- [[' + Morebits.pageNameNorm + ']] -->');
} else {
linkPlainAfter = Morebits.htmlNode('code', Morebits.pageNameNorm);
}
form.append({
type: 'div',
style: 'margin-bottom: 0.5em',
label: [
'このツールを使用すると、このページを指しているすべてのリンク元 (「バックリンク」) のリンクを解除できます。' +
(fileSpace ? 'また、このファイルのすべての使用を <!-- --> で囲んでコメントアウトし、非表示にします。' : '') +
'例えば、',
linkTextBefore, ' が ', linkTextAfter, ' になり、 ',
linkPlainBefore, ' が ', linkPlainAfter, 'になります。このツールは、このページへのリダイレクトやこのページ内のリンク(「セルフリンク」)を解除しません。注意して使用してください。'
]
});
form.append({
type: 'input',
name: 'reason',
label: '理由:',
value: presetReason || '',
size: 60
});
const query = {
action: 'query',
list: 'backlinks',
bltitle: mw.config.get('wgPageName'),
bllimit: 'max', // 500 is max for normal users, 5000 for bots and sysops
blnamespace: Twinkle.getPref('unlinkNamespaces'),
rawcontinue: true,
format: 'json'
};
if (fileSpace) {
query.list += '|imageusage';
query.iutitle = query.bltitle;
query.iulimit = query.bllimit;
query.iunamespace = query.blnamespace;
} else {
query.blfilterredir = 'nonredirects';
}
var wikipedia_api = new Morebits.wiki.Api('バックリンクを取得中', query, Twinkle.unlink.callbacks.display.backlinks);
wikipedia_api.params = { form: form, Window: Window, image: fileSpace };
wikipedia_api.post();
const root = document.createElement('div');
root.style.padding = '15px'; // just so it doesn't look broken
Morebits.Status.init(root);
wikipedia_api.statelem.status('読込中...');
Window.setContent(root);
Window.display();
};
Twinkle.unlink.callback.evaluate = function twinkleunlinkCallbackEvaluate(event) {
const form = event.target;
const input = Morebits.QuickForm.getInputData(form);
if (!input.reason) {
alert('リンクを解除する理由を指定する必要があります。');
return;
}
input.backlinks = input.backlinks || [];
input.imageusage = input.imageusage || [];
const pages = Morebits.array.uniq(input.backlinks.concat(input.imageusage));
if (!pages.length) {
alert('リンクを解除するページを少なくとも1つ選択する必要があります。');
return;
}
Morebits.SimpleWindow.setButtonsEnabled(false);
Morebits.Status.init(form);
var unlinker = new Morebits.BatchOperation((input.backlinks.length ? 'バックリンク' +
(input.imageusage.length ? 'およびファイルの使用' : '') : 'ファイルの使用') + 'を解除');
unlinker.setOption('preserveIndividualStatusLines', true);
unlinker.setPageList(pages);
const params = { reason: input.reason, unlinker: unlinker };
unlinker.run((pageName) => {
var wikipedia_page = new Morebits.wiki.Page(pageName, '"' + pageName + '" のリンクを解除');
wikipedia_page.setBotEdit(true); // unlink considered a floody operation
wikipedia_page.setCallbackParameters($.extend({
doBacklinks: input.backlinks.includes(pageName),
doImageusage: input.imageusage.includes(pageName),
doImagecomprem: input.compremove
}, params));
wikipedia_page.load(Twinkle.unlink.callbacks.unlinkBacklinks);
});
};
Twinkle.unlink.callbacks = {
display: {
backlinks: function twinkleunlinkCallbackDisplayBacklinks(apiobj) {
const response = apiobj.getResponse();
let havecontent = false;
let list, namespaces, i;
if (apiobj.params.image) {
const imageusage = response.query.imageusage.sort(Twinkle.sortByNamespace);
list = [];
for (i = 0; i < imageusage.length; ++i) {
// Label made by Twinkle.generateBatchPageLinks
list.push({ label: '', value: imageusage[i].title, checked: true });
}
if (!list.length) {
apiobj.params.form.append({ type: 'div', label: 'ファイルを使用例が見つかりません。' });
} else {
apiobj.params.form.append({ type: 'header', label: 'ファイルの使用状況' });
namespaces = [];
$.each(Twinkle.getPref('unlinkNamespaces'), (k, v) => {
namespaces.push(v === '0' ? '(標準)' : mw.config.get('wgFormattedNamespaces')[v]);
});
apiobj.params.form.append({
type: 'checkbox',
list: [
{
label: 'ファイルへのリンクを完全に除去する',
value: 'compremove',
name: 'compremove'
}
]
});
apiobj.params.form.append({
type: 'div',
label: '選択された名前空間: ' + namespaces.join(', '),
tooltip: 'これは[[利用者:Syunsyunminmin/Twinkle/Preferences|Twinkleの設定]]で変更できます。'
});
if (response['query-continue'] && response['query-continue'].imageusage) {
apiobj.params.form.append({
type: 'div',
label: '最初の' + mw.language.convertNumber(list.length) + '件のファイルの使用状況を表示。'
});
}
apiobj.params.form.append({
type: 'button',
label: '全て選択',
event: function(e) {
$(Morebits.QuickForm.getElements(e.target.form, 'imageusage')).prop('checked', true);
}
});
apiobj.params.form.append({
type: 'button',
label: '全ての選択を外す',
event: function(e) {
$(Morebits.QuickForm.getElements(e.target.form, 'imageusage')).prop('checked', false);
}
});
apiobj.params.form.append({
type: 'checkbox',
name: 'imageusage',
shiftClickSupport: true,
list: list
});
havecontent = true;
}
}
const backlinks = response.query.backlinks.sort(Twinkle.sortByNamespace);
if (backlinks.length > 0) {
list = [];
for (i = 0; i < backlinks.length; ++i) {
// Label made by Twinkle.generateBatchPageLinks
list.push({ label: '', value: backlinks[i].title, checked: true });
}
apiobj.params.form.append({ type: 'header', label: 'バックリンク' });
namespaces = [];
$.each(Twinkle.getPref('unlinkNamespaces'), (k, v) => {
namespaces.push(v === '0' ? '(標準)' : mw.config.get('wgFormattedNamespaces')[v]);
});
apiobj.params.form.append({
type: 'div',
label: '選択された名前空間: ' + namespaces.join(', '),
tooltip: 'これは、このTwinkleウィンドウの下部にリンクされている Twinkleの設定で変更できます'
});
if (response['query-continue'] && response['query-continue'].backlinks) {
apiobj.params.form.append({
type: 'div',
label: '最初の' + mw.language.convertNumber(list.length) + '件のバックリンクを表示。'
});
}
apiobj.params.form.append({
type: 'button',
label: '全て選択',
event: function(e) {
$(Morebits.QuickForm.getElements(e.target.form, 'backlinks')).prop('checked', true);
}
});
apiobj.params.form.append({
type: 'button',
label: '全ての選択を外す',
event: function(e) {
$(Morebits.QuickForm.getElements(e.target.form, 'backlinks')).prop('checked', false);
}
});
apiobj.params.form.append({
type: 'checkbox',
name: 'backlinks',
shiftClickSupport: true,
list: list
});
havecontent = true;
} else {
apiobj.params.form.append({ type: 'div', label: 'バックリンクが見つかりませんでした。' });
}
if (havecontent) {
apiobj.params.form.append({ type: 'submit' });
}
const result = apiobj.params.form.render();
apiobj.params.Window.setContent(result);
Morebits.QuickForm.getElements(result, 'backlinks').forEach(Twinkle.generateBatchPageLinks);
Morebits.QuickForm.getElements(result, 'imageusage').forEach(Twinkle.generateBatchPageLinks);
}
},
unlinkBacklinks: function twinkleunlinkCallbackUnlinkBacklinks(pageobj) {
let oldtext = pageobj.getPageText();
const params = pageobj.getCallbackParameters();
const wikiPage = new Morebits.wikitext.Page(oldtext);
let summaryText = '', warningString = false;
let text;
// remove image usages
if (params.doImageusage) {
if (params.doImagecomprem) {
text = wikiPage.removeImage(mw.config.get('wgTitle')).getText();
} else {
text = wikiPage.commentOutImage(mw.config.get('wgTitle'), 'コメントアウト').getText();
}
// did we actually make any changes?
if (text === oldtext) {
warningString = 'ファイルの使用状況';
} else {
if (params.doImagecomprem) {
summaryText = 'のファイル使用を除去する';
} else {
summaryText = 'のファイル使用をコメントアウト';
}
oldtext = text;
}
}
// remove backlinks
if (params.doBacklinks) {
text = wikiPage.removeLink(Morebits.pageNameNorm).getText();
// did we actually make any changes?
if (text === oldtext) {
warningString = warningString ? 'バックリンクまたはファイルの使用状況' : 'バックリンク';
} else {
summaryText = (summaryText ? summaryText + ' / ' : 'への') + 'リンクを解除';
oldtext = text;
}
}
if (warningString) {
// nothing to do!
pageobj.getStatusElement().error('ページに' + warningString + 'が見つかりませんでした。');
params.unlinker.workerFailure(pageobj);
return;
}
pageobj.setPageText(text);
pageobj.setEditSummary('"' + Morebits.pageNameNorm + '"' + summaryText + ': ' + params.reason + Twinkle.summaryAd);
pageobj.setCreateOption('nocreate');
pageobj.save(params.unlinker.workerSuccess, params.unlinker.workerFailure);
}
};
Twinkle.addInitCallback(Twinkle.unlink, 'unlink');
}());
// </nowiki>
mc63sylewgncy4voy30hfxzggmk5vfx
769530
769463
2026-10-22T00:00:26Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 1406828: rm cmt
769530
javascript
text/javascript
// <nowiki>
(function() {
/*
****************************************
*** twinkleunlink.js: Unlink module
****************************************
* Mode of invocation: Tab ("Unlink")
* Active on: Non-special pages, except Wikipedia:Sandbox
*/
Twinkle.unlink = function twinkleunlink() {
if (mw.config.get('wgNamespaceNumber') < 0 || mw.config.get('wgPageName') === 'Wikipedia:サンドボックス' ||
// Restrict to extended confirmed users (see #428)
(!Morebits.userIsInGroup('extendedconfirmed') && !Morebits.userIsSysop)) {
return;
}
Twinkle.addPortletLink(Twinkle.unlink.callback, 'Unlink', 'tw-unlink', 'バックリンクの解除');
};
// the parameter is used when invoking unlink from admin speedy
Twinkle.unlink.callback = function(presetReason) {
const fileSpace = mw.config.get('wgNamespaceNumber') === 6;
var Window = new Morebits.SimpleWindow(600, 440);
Window.setTitle('バックリンク' + (fileSpace ? 'とファイルの使用' : '') + 'を解除する');
Window.setScriptName('Twinkle');
Window.addFooterLink('Unlink設定', 'User:Syunsyunminmin/Twinkle/Preferences#unlink');
Window.addFooterLink('Twinkle help', ':en:WP:TW/DOC#unlink');
Window.addFooterLink('Give feedback', 'User Talk:Syunsyunminmin/Twinkle');
const form = new Morebits.QuickForm(Twinkle.unlink.callback.evaluate);
// prepend some documentation: files are commented out, while any
// display text is preserved for links (otherwise the link itself is used)
const linkTextBefore = Morebits.htmlNode('code', '[[' + (fileSpace ? ':' : '') + Morebits.pageNameNorm + '|link text]]');
const linkTextAfter = Morebits.htmlNode('code', 'link text');
const linkPlainBefore = Morebits.htmlNode('code', '[[' + Morebits.pageNameNorm + ']]');
let linkPlainAfter;
if (fileSpace) {
linkPlainAfter = Morebits.htmlNode('code', '<!-- [[' + Morebits.pageNameNorm + ']] -->');
} else {
linkPlainAfter = Morebits.htmlNode('code', Morebits.pageNameNorm);
}
form.append({
type: 'div',
style: 'margin-bottom: 0.5em',
label: [
'このツールを使用すると、このページを指しているすべてのリンク元 (「バックリンク」) のリンクを解除できます。' +
(fileSpace ? 'また、このファイルのすべての使用を <!-- --> で囲んでコメントアウトし、非表示にします。' : '') +
'例えば、',
linkTextBefore, ' が ', linkTextAfter, ' になり、 ',
linkPlainBefore, ' が ', linkPlainAfter, 'になります。このツールは、このページへのリダイレクトやこのページ内のリンク(「セルフリンク」)を解除しません。注意して使用してください。'
]
});
form.append({
type: 'input',
name: 'reason',
label: '理由:',
value: presetReason || '',
size: 60
});
const query = {
action: 'query',
list: 'backlinks',
bltitle: mw.config.get('wgPageName'),
bllimit: 'max', // 500 is max for normal users, 5000 for bots and sysops
blnamespace: Twinkle.getPref('unlinkNamespaces'),
rawcontinue: true,
format: 'json'
};
if (fileSpace) {
query.list += '|imageusage';
query.iutitle = query.bltitle;
query.iulimit = query.bllimit;
query.iunamespace = query.blnamespace;
} else {
query.blfilterredir = 'nonredirects';
}
var wikipedia_api = new Morebits.wiki.Api('バックリンクを取得中', query, Twinkle.unlink.callbacks.display.backlinks);
wikipedia_api.params = { form: form, Window: Window, image: fileSpace };
wikipedia_api.post();
const root = document.createElement('div');
root.style.padding = '15px'; // just so it doesn't look broken
Morebits.Status.init(root);
wikipedia_api.statelem.status('読込中...');
Window.setContent(root);
Window.display();
};
Twinkle.unlink.callback.evaluate = function twinkleunlinkCallbackEvaluate(event) {
const form = event.target;
const input = Morebits.QuickForm.getInputData(form);
if (!input.reason) {
alert('リンクを解除する理由を指定する必要があります。');
return;
}
input.backlinks = input.backlinks || [];
input.imageusage = input.imageusage || [];
const pages = Morebits.array.uniq(input.backlinks.concat(input.imageusage));
if (!pages.length) {
alert('リンクを解除するページを少なくとも1つ選択する必要があります。');
return;
}
Morebits.SimpleWindow.setButtonsEnabled(false);
Morebits.Status.init(form);
var unlinker = new Morebits.BatchOperation((input.backlinks.length ? 'バックリンク' +
(input.imageusage.length ? 'およびファイルの使用' : '') : 'ファイルの使用') + 'を解除');
unlinker.setOption('preserveIndividualStatusLines', true);
unlinker.setPageList(pages);
const params = { reason: input.reason, unlinker: unlinker };
unlinker.run((pageName) => {
var wikipedia_page = new Morebits.wiki.Page(pageName, '"' + pageName + '" のリンクを解除');
wikipedia_page.setBotEdit(true); // unlink considered a floody operation
wikipedia_page.setCallbackParameters($.extend({
doBacklinks: input.backlinks.includes(pageName),
doImageusage: input.imageusage.includes(pageName),
doImagecomprem: input.compremove
}, params));
wikipedia_page.load(Twinkle.unlink.callbacks.unlinkBacklinks);
});
};
Twinkle.unlink.callbacks = {
display: {
backlinks: function twinkleunlinkCallbackDisplayBacklinks(apiobj) {
const response = apiobj.getResponse();
let havecontent = false;
let list, namespaces, i;
if (apiobj.params.image) {
const imageusage = response.query.imageusage.sort(Twinkle.sortByNamespace);
list = [];
for (i = 0; i < imageusage.length; ++i) {
// Label made by Twinkle.generateBatchPageLinks
list.push({ label: '', value: imageusage[i].title, checked: true });
}
if (!list.length) {
apiobj.params.form.append({ type: 'div', label: 'ファイルを使用例が見つかりません。' });
} else {
apiobj.params.form.append({ type: 'header', label: 'ファイルの使用状況' });
namespaces = [];
$.each(Twinkle.getPref('unlinkNamespaces'), (k, v) => {
namespaces.push(v === '0' ? '(標準)' : mw.config.get('wgFormattedNamespaces')[v]);
});
apiobj.params.form.append({
type: 'checkbox',
list: [
{
label: 'ファイルへのリンクを完全に除去する',
value: 'compremove',
name: 'compremove'
}
]
});
apiobj.params.form.append({
type: 'div',
label: '選択された名前空間: ' + namespaces.join(', '),
tooltip: 'これは[[利用者:Syunsyunminmin/Twinkle/Preferences|Twinkleの設定]]で変更できます。'
});
if (response['query-continue'] && response['query-continue'].imageusage) {
apiobj.params.form.append({
type: 'div',
label: '最初の' + mw.language.convertNumber(list.length) + '件のファイルの使用状況を表示。'
});
}
apiobj.params.form.append({
type: 'button',
label: '全て選択',
event: function(e) {
$(Morebits.QuickForm.getElements(e.target.form, 'imageusage')).prop('checked', true);
}
});
apiobj.params.form.append({
type: 'button',
label: '全ての選択を外す',
event: function(e) {
$(Morebits.QuickForm.getElements(e.target.form, 'imageusage')).prop('checked', false);
}
});
apiobj.params.form.append({
type: 'checkbox',
name: 'imageusage',
shiftClickSupport: true,
list: list
});
havecontent = true;
}
}
const backlinks = response.query.backlinks.sort(Twinkle.sortByNamespace);
if (backlinks.length > 0) {
list = [];
for (i = 0; i < backlinks.length; ++i) {
// Label made by Twinkle.generateBatchPageLinks
list.push({ label: '', value: backlinks[i].title, checked: true });
}
apiobj.params.form.append({ type: 'header', label: 'バックリンク' });
namespaces = [];
$.each(Twinkle.getPref('unlinkNamespaces'), (k, v) => {
namespaces.push(v === '0' ? '(標準)' : mw.config.get('wgFormattedNamespaces')[v]);
});
apiobj.params.form.append({
type: 'div',
label: '選択された名前空間: ' + namespaces.join(', '),
tooltip: 'これは、このTwinkleウィンドウの下部にリンクされている Twinkleの設定で変更できます'
});
if (response['query-continue'] && response['query-continue'].backlinks) {
apiobj.params.form.append({
type: 'div',
label: '最初の' + mw.language.convertNumber(list.length) + '件のバックリンクを表示。'
});
}
apiobj.params.form.append({
type: 'button',
label: '全て選択',
event: function(e) {
$(Morebits.QuickForm.getElements(e.target.form, 'backlinks')).prop('checked', true);
}
});
apiobj.params.form.append({
type: 'button',
label: '全ての選択を外す',
event: function(e) {
$(Morebits.QuickForm.getElements(e.target.form, 'backlinks')).prop('checked', false);
}
});
apiobj.params.form.append({
type: 'checkbox',
name: 'backlinks',
shiftClickSupport: true,
list: list
});
havecontent = true;
} else {
apiobj.params.form.append({ type: 'div', label: 'バックリンクが見つかりませんでした。' });
}
if (havecontent) {
apiobj.params.form.append({ type: 'submit' });
}
const result = apiobj.params.form.render();
apiobj.params.Window.setContent(result);
Morebits.QuickForm.getElements(result, 'backlinks').forEach(Twinkle.generateBatchPageLinks);
Morebits.QuickForm.getElements(result, 'imageusage').forEach(Twinkle.generateBatchPageLinks);
}
},
unlinkBacklinks: function twinkleunlinkCallbackUnlinkBacklinks(pageobj) {
let oldtext = pageobj.getPageText();
const params = pageobj.getCallbackParameters();
const wikiPage = new Morebits.wikitext.Page(oldtext);
let summaryText = '', warningString = false;
let text;
// remove image usages
if (params.doImageusage) {
if (params.doImagecomprem) {
text = wikiPage.removeImage(mw.config.get('wgTitle')).getText();
} else {
text = wikiPage.commentOutImage(mw.config.get('wgTitle'), 'コメントアウト').getText();
}
// did we actually make any changes?
if (text === oldtext) {
warningString = 'ファイルの使用状況';
} else {
if (params.doImagecomprem) {
summaryText = 'のファイル使用を除去する';
} else {
summaryText = 'のファイル使用をコメントアウト';
}
oldtext = text;
}
}
// remove backlinks
if (params.doBacklinks) {
text = wikiPage.removeLink(Morebits.pageNameNorm).getText();
// did we actually make any changes?
if (text === oldtext) {
warningString = warningString ? 'バックリンクまたはファイルの使用状況' : 'バックリンク';
} else {
summaryText = (summaryText ? summaryText + ' / ' : 'への') + 'リンクを解除';
oldtext = text;
}
}
if (warningString) {
// nothing to do!
pageobj.getStatusElement().error('ページに' + warningString + 'が見つかりませんでした。');
params.unlinker.workerFailure(pageobj);
return;
}
pageobj.setPageText(text);
pageobj.setEditSummary('"' + Morebits.pageNameNorm + '"' + summaryText + ': ' + params.reason + Twinkle.summaryAd);
pageobj.setCreateOption('nocreate');
pageobj.save(params.unlinker.workerSuccess, params.unlinker.workerFailure);
}
};
Twinkle.addInitCallback(Twinkle.unlink, 'unlink');
}());
// </nowiki>
rqh9o4yq2t51ouv8iryz57ye58puxly
769567
769530
2026-10-22T02:21:41Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at d9cd3461baca43ac99ac2c24fb76a3aae6ddada9: minify
769567
javascript
text/javascript
// <nowiki>
(function(){Twinkle.unlink=function twinkleunlink(){if(mw.config.get("wgNamespaceNumber")<0||mw.config.get("wgPageName")==="Wikipedia:サンドボックス"||!Morebits.userIsInGroup("extendedconfirmed")&&!Morebits.userIsSysop){return}Twinkle.addPortletLink(Twinkle.unlink.callback,"Unlink","tw-unlink","バックリンクの解除")};Twinkle.unlink.callback=function(presetReason){const fileSpace=mw.config.get("wgNamespaceNumber")===6;var Window=new Morebits.SimpleWindow(600,440);Window.setTitle("バックリンク"+(fileSpace?"とファイルの使用":"")+"を解除する");Window.setScriptName("Twinkle");Window.addFooterLink("Unlink設定","User:Syunsyunminmin/Twinkle/Preferences#unlink");Window.addFooterLink("Twinkle help",":en:WP:TW/DOC#unlink");Window.addFooterLink("Give feedback","User Talk:Syunsyunminmin/Twinkle");const form=new Morebits.QuickForm(Twinkle.unlink.callback.evaluate);const linkTextBefore=Morebits.htmlNode("code","[["+(fileSpace?":":"")+Morebits.pageNameNorm+"|link text]]");const linkTextAfter=Morebits.htmlNode("code","link text");const linkPlainBefore=Morebits.htmlNode("code","[["+Morebits.pageNameNorm+"]]");let linkPlainAfter;if(fileSpace){linkPlainAfter=Morebits.htmlNode("code","\x3c!-- [["+Morebits.pageNameNorm+"]] --\x3e")}else{linkPlainAfter=Morebits.htmlNode("code",Morebits.pageNameNorm)}form.append({type:"div",style:"margin-bottom: 0.5em",label:["このツールを使用すると、このページを指しているすべてのリンク元 (「バックリンク」) のリンクを解除できます。"+(fileSpace?"また、このファイルのすべての使用を \x3c!-- --\x3e で囲んでコメントアウトし、非表示にします。":"")+"例えば、",linkTextBefore," が ",linkTextAfter," になり、 ",linkPlainBefore," が ",linkPlainAfter,"になります。このツールは、このページへのリダイレクトやこのページ内のリンク(「セルフリンク」)を解除しません。注意して使用してください。"]});form.append({type:"input",name:"reason",label:"理由:",value:presetReason||"",size:60});const query={action:"query",list:"backlinks",bltitle:mw.config.get("wgPageName"),bllimit:"max",blnamespace:Twinkle.getPref("unlinkNamespaces"),rawcontinue:true,format:"json"};if(fileSpace){query.list+="|imageusage";query.iutitle=query.bltitle;query.iulimit=query.bllimit;query.iunamespace=query.blnamespace}else{query.blfilterredir="nonredirects"}var wikipedia_api=new Morebits.wiki.Api("バックリンクを取得中",query,Twinkle.unlink.callbacks.display.backlinks);wikipedia_api.params={form:form,Window:Window,image:fileSpace};wikipedia_api.post();const root=document.createElement("div");root.style.padding="15px";Morebits.Status.init(root);wikipedia_api.statelem.status("読込中...");Window.setContent(root);Window.display()};Twinkle.unlink.callback.evaluate=function twinkleunlinkCallbackEvaluate(event){const form=event.target;const input=Morebits.QuickForm.getInputData(form);if(!input.reason){alert("リンクを解除する理由を指定する必要があります。");return}input.backlinks=input.backlinks||[];input.imageusage=input.imageusage||[];const pages=Morebits.array.uniq(input.backlinks.concat(input.imageusage));if(!pages.length){alert("リンクを解除するページを少なくとも1つ選択する必要があります。");return}Morebits.SimpleWindow.setButtonsEnabled(false);Morebits.Status.init(form);var unlinker=new Morebits.BatchOperation((input.backlinks.length?"バックリンク"+(input.imageusage.length?"およびファイルの使用":""):"ファイルの使用")+"を解除");unlinker.setOption("preserveIndividualStatusLines",true);unlinker.setPageList(pages);const params={reason:input.reason,unlinker:unlinker};unlinker.run(pageName=>{var wikipedia_page=new Morebits.wiki.Page(pageName,'"'+pageName+'" のリンクを解除');wikipedia_page.setBotEdit(true);wikipedia_page.setCallbackParameters($.extend({doBacklinks:input.backlinks.includes(pageName),doImageusage:input.imageusage.includes(pageName),doImagecomprem:input.compremove},params));wikipedia_page.load(Twinkle.unlink.callbacks.unlinkBacklinks)})};Twinkle.unlink.callbacks={display:{backlinks:function twinkleunlinkCallbackDisplayBacklinks(apiobj){const response=apiobj.getResponse();let havecontent=false;let list,namespaces,i;if(apiobj.params.image){const imageusage=response.query.imageusage.sort(Twinkle.sortByNamespace);list=[];for(i=0;i<imageusage.length;++i){list.push({label:"",value:imageusage[i].title,checked:true})}if(!list.length){apiobj.params.form.append({type:"div",label:"ファイルを使用例が見つかりません。"})}else{apiobj.params.form.append({type:"header",label:"ファイルの使用状況"});namespaces=[];$.each(Twinkle.getPref("unlinkNamespaces"),(k,v)=>{namespaces.push(v==="0"?"(標準)":mw.config.get("wgFormattedNamespaces")[v])});apiobj.params.form.append({type:"checkbox",list:[{label:"ファイルへのリンクを完全に除去する",value:"compremove",name:"compremove"}]});apiobj.params.form.append({type:"div",label:"選択された名前空間: "+namespaces.join(", "),tooltip:"これは[[利用者:Syunsyunminmin/Twinkle/Preferences|Twinkleの設定]]で変更できます。"});if(response["query-continue"]&&response["query-continue"].imageusage){apiobj.params.form.append({type:"div",label:"最初の"+mw.language.convertNumber(list.length)+"件のファイルの使用状況を表示。"})}apiobj.params.form.append({type:"button",label:"全て選択",event:function(e){$(Morebits.QuickForm.getElements(e.target.form,"imageusage")).prop("checked",true)}});apiobj.params.form.append({type:"button",label:"全ての選択を外す",event:function(e){$(Morebits.QuickForm.getElements(e.target.form,"imageusage")).prop("checked",false)}});apiobj.params.form.append({type:"checkbox",name:"imageusage",shiftClickSupport:true,list:list});havecontent=true}}const backlinks=response.query.backlinks.sort(Twinkle.sortByNamespace);if(backlinks.length>0){list=[];for(i=0;i<backlinks.length;++i){list.push({label:"",value:backlinks[i].title,checked:true})}apiobj.params.form.append({type:"header",label:"バックリンク"});namespaces=[];$.each(Twinkle.getPref("unlinkNamespaces"),(k,v)=>{namespaces.push(v==="0"?"(標準)":mw.config.get("wgFormattedNamespaces")[v])});apiobj.params.form.append({type:"div",label:"選択された名前空間: "+namespaces.join(", "),tooltip:"これは、このTwinkleウィンドウの下部にリンクされている Twinkleの設定で変更できます"});if(response["query-continue"]&&response["query-continue"].backlinks){apiobj.params.form.append({type:"div",label:"最初の"+mw.language.convertNumber(list.length)+"件のバックリンクを表示。"})}apiobj.params.form.append({type:"button",label:"全て選択",event:function(e){$(Morebits.QuickForm.getElements(e.target.form,"backlinks")).prop("checked",true)}});apiobj.params.form.append({type:"button",label:"全ての選択を外す",event:function(e){$(Morebits.QuickForm.getElements(e.target.form,"backlinks")).prop("checked",false)}});apiobj.params.form.append({type:"checkbox",name:"backlinks",shiftClickSupport:true,list:list});havecontent=true}else{apiobj.params.form.append({type:"div",label:"バックリンクが見つかりませんでした。"})}if(havecontent){apiobj.params.form.append({type:"submit"})}const result=apiobj.params.form.render();apiobj.params.Window.setContent(result);Morebits.QuickForm.getElements(result,"backlinks").forEach(Twinkle.generateBatchPageLinks);Morebits.QuickForm.getElements(result,"imageusage").forEach(Twinkle.generateBatchPageLinks)}},unlinkBacklinks:function twinkleunlinkCallbackUnlinkBacklinks(pageobj){let oldtext=pageobj.getPageText();const params=pageobj.getCallbackParameters();const wikiPage=new Morebits.wikitext.Page(oldtext);let summaryText="",warningString=false;let text;if(params.doImageusage){if(params.doImagecomprem){text=wikiPage.removeImage(mw.config.get("wgTitle")).getText()}else{text=wikiPage.commentOutImage(mw.config.get("wgTitle"),"コメントアウト").getText()}if(text===oldtext){warningString="ファイルの使用状況"}else{if(params.doImagecomprem){summaryText="のファイル使用を除去する"}else{summaryText="のファイル使用をコメントアウト"}oldtext=text}}if(params.doBacklinks){text=wikiPage.removeLink(Morebits.pageNameNorm).getText();if(text===oldtext){warningString=warningString?"バックリンクまたはファイルの使用状況":"バックリンク"}else{summaryText=(summaryText?summaryText+" / ":"への")+"リンクを解除";oldtext=text}}if(warningString){pageobj.getStatusElement().error("ページに"+warningString+"が見つかりませんでした。");params.unlinker.workerFailure(pageobj);return}pageobj.setPageText(text);pageobj.setEditSummary('"'+Morebits.pageNameNorm+'"'+summaryText+": "+params.reason+Twinkle.summaryAd);pageobj.setCreateOption("nocreate");pageobj.save(params.unlinker.workerSuccess,params.unlinker.workerFailure)}};Twinkle.addInitCallback(Twinkle.unlink,"unlink")})();
// </nowiki>
jud22b743b3sh8usff8qb9xn0sfvatb
User:Syunsyunminmin/Twinkle/twinklefluff.js
2
179399
769464
2026-10-21T13:58:05Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 3680108:
769464
javascript
text/javascript
/*
Twinkle: adapted for Japanese Wikipedia; upstream authors and Japanese adaptations are credited in UPSTREAM.md.
Upstream source: https://github.com/wikimedia-gadgets/twinkle/blob/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/src/modules/twinklerollback.js
Upstream author history: https://github.com/wikimedia-gadgets/twinkle/commits/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/src/modules/twinklerollback.js
License: CC BY-SA 3.0 Unported. See https://github.com/wikimedia-gadgets/twinkle/blob/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/LICENSE.txt
Japanese adaptation: Syunsyunminmin; includes later synchronization changes.
*/
// <nowiki>
(function() {
/*
****************************************
*** twinklerollback.js: Revert/rollback module
****************************************
* Mode of invocation: Links on contributions, recent changes, history, and diff pages
* Active on: Diff pages, history pages, Special:RecentChanges(Linked),
and Special:Contributions
*/
/**
* Twinklerollback revert and antivandalism utility
*/
Twinkle.rollback = function twinklerollback() {
// Only proceed if the user can actually edit the page in question
// (see #632 for contribs issue). wgIsProbablyEditable should take
// care of namespace/contentModel restrictions as well as explicit
// protections; it won't take care of cascading or TitleBlacklist.
if (mw.config.get('wgIsProbablyEditable')) {
if (mw.config.get('wgAction') === 'view' && mw.config.get('wgRevisionId') && mw.config.get('wgCurRevisionId') !== mw.config.get('wgRevisionId')) {
Twinkle.rollback.addLinks.oldid();
} else if (mw.config.get('wgAction') === 'history' && mw.config.get('wgArticleId')) {
Twinkle.rollback.addLinks.history();
}
} else if (mw.config.get('wgNamespaceNumber') === -1) {
Twinkle.rollback.skipTalk = !Twinkle.getPref('openTalkPageOnAutoRevert');
Twinkle.rollback.rollbackInPlace = Twinkle.getPref('rollbackInPlace');
switch (mw.config.get('wgCanonicalSpecialPageName')) {
case 'Contributions':
case 'IPContributions':
Twinkle.rollback.addLinks.contributions();
break;
case 'Recentchanges':
case 'Recentchangeslinked':
// Reload with recent changes updates
// structuredChangeFilters.ui.initialized is just on load
mw.hook('wikipage.content').add(($context) => {
if (!$context || !$context.is('div')) {
return;
}
Twinkle.rollback.addLinks.recentchanges($context);
});
break;
}
}
// Reload when revision slider or other scripts dynamically load diff content.
mw.hook('wikipage.diff').add(($context) => {
if (!$context) {
return;
}
// Only proceed if the user can actually edit the page in question,
// wgDiffOldId included for clarity in if else loop [[phab:T214985]]
if (mw.config.get('wgIsProbablyEditable') && (mw.config.get('wgDiffNewId') || mw.config.get('wgDiffOldId'))) {
Twinkle.rollback.addLinks.diff($context);
}
});
};
// A list of usernames, usually only bots, that vandalism revert is jumped
// over; that is, if vandalism revert was chosen on such username, then its
// target is on the revision before. This is for handling quick bots that
// makes edits seconds after the original edit is made. This only affects
// vandalism rollback; for good faith rollback, it will stop, indicating a bot
// has no faith, and for normal rollback, it will rollback that edit.
Twinkle.rollback.trustedBots = ['AnomieBOT', 'SineBot', 'MajavahBot'];
Twinkle.rollback.skipTalk = null;
Twinkle.rollback.rollbackInPlace = null;
// String to insert when a username is hidden
Twinkle.rollback.hiddenName = '不明な利用者';
// Consolidated construction of rollback links
Twinkle.rollback.linkBuilder = {
spanTag: function(color, content) {
const span = document.createElement('span');
span.style.color = color;
span.appendChild(document.createTextNode(content));
return span;
},
buildLink: function(color, text) {
const link = document.createElement('a');
link.appendChild(Twinkle.rollback.linkBuilder.spanTag('Black', '['));
link.appendChild(Twinkle.rollback.linkBuilder.spanTag(color, text));
link.appendChild(Twinkle.rollback.linkBuilder.spanTag('Black', ']'));
link.href = '#';
return link;
},
/**
* @param {string} [vandal=null] - Username of the editor being reverted
* Provide a falsey value if the username is hidden, defaults to null
* @param {boolean} inline - True to create two links in a span, false
* to create three links in a div (optional)
* @param {number|string} [rev=wgCurRevisionId] - Revision ID being reverted (optional)
* @param {string} [page=wgPageName] - Page being reverted (optional)
*/
rollbackLinks: function(vandal, inline, rev, page) {
vandal = vandal || null;
const elem = inline ? 'span' : 'div';
const revNode = document.createElement(elem);
rev = parseInt(rev, 10);
if (rev) {
revNode.setAttribute('id', 'tw-revert' + rev);
} else {
revNode.setAttribute('id', 'tw-revert');
}
const separator = inline ? ' ' : ' || ';
const sepNode1 = document.createElement('span');
const sepText = document.createTextNode(separator);
sepNode1.setAttribute('class', 'tw-rollback-link-separator');
sepNode1.appendChild(sepText);
const sepNode2 = sepNode1.cloneNode(true);
const normNode = document.createElement('span');
const vandNode = document.createElement('span');
var normLink = Twinkle.rollback.linkBuilder.buildLink('SteelBlue', '差し戻し');
var vandLink = Twinkle.rollback.linkBuilder.buildLink('Red', '荒らし');
normLink.style.fontWeight = 'bold';
vandLink.style.fontWeight = 'bold';
$(normLink).on('click', (e) => {
e.preventDefault();
Twinkle.rollback.revert('norm', vandal, rev, page);
Twinkle.rollback.disableLinks(revNode);
});
$(vandLink).on('click', (e) => {
e.preventDefault();
Twinkle.rollback.revert('vand', vandal, rev, page);
Twinkle.rollback.disableLinks(revNode);
});
normNode.setAttribute('class', 'tw-rollback-link-normal');
vandNode.setAttribute('class', 'tw-rollback-link-vandalism');
vandNode.appendChild(sepNode2);
normNode.appendChild(normLink);
vandNode.appendChild(vandLink);
revNode.appendChild(normNode);
revNode.appendChild(vandNode);
return revNode;
},
// Build [restore this revision] links
restoreThisRevisionLink: function(revisionRef, inline) {
// If not a specific revision number, should be wgDiffNewId/wgDiffOldId/wgRevisionId
revisionRef = typeof revisionRef === 'number' ? revisionRef : mw.config.get(revisionRef);
const elem = inline ? 'span' : 'div';
const revertToRevisionNode = document.createElement(elem);
revertToRevisionNode.setAttribute('id', 'tw-revert-to-' + revisionRef);
revertToRevisionNode.style.fontWeight = 'bold';
var revertToRevisionLink = Twinkle.rollback.linkBuilder.buildLink('SaddleBrown', 'この版を復元');
$(revertToRevisionLink).on('click', (e) => {
e.preventDefault();
Twinkle.rollback.revertToRevision(revisionRef);
});
if (inline) {
revertToRevisionNode.appendChild(document.createTextNode(' '));
}
revertToRevisionNode.appendChild(revertToRevisionLink);
return revertToRevisionNode;
}
};
Twinkle.rollback.addLinks = {
contributions: function() {
// $('sp-contributions-footer-anon-range') relies on the fmbox
// id in [[MediaWiki:Sp-contributions-footer-anon-range]] and
// is used to show rollback/vandalism links for IP ranges
const isRange = !!$('#sp-contributions-footer-anon-range')[0];
if (mw.config.exists('wgRelevantUserName') || isRange) {
// Get the username these contributions are for
let username = mw.config.get('wgRelevantUserName');
if (Twinkle.getPref('showRollbackLinks').includes('contribs') ||
(mw.config.get('wgUserName') !== username && Twinkle.getPref('showRollbackLinks').includes('others')) ||
(mw.config.get('wgUserName') === username && Twinkle.getPref('showRollbackLinks').includes('mine'))) {
const $list = $('#mw-content-text').find('ul li:has(span.mw-uctop):has(.mw-changeslist-diff)');
$list.each((key, current) => {
// revid is also available in the href of both
// .mw-changeslist-date or .mw-changeslist-diff
const page = $(current).find('.mw-contributions-title').text();
// Get username for IP ranges (wgRelevantUserName is null)
if (isRange) {
// The :not is possibly unnecessary, as it appears that
// .mw-userlink is simply not present if the username is hidden
username = $(current).find('.mw-userlink:not(.history-deleted)').text();
}
// It's unlikely, but we can't easily check for revdel'd usernames
// since only a strong element is provided, with no easy selector [[phab:T255903]]
current.appendChild(Twinkle.rollback.linkBuilder.rollbackLinks(username, true, current.dataset.mwRevid, page));
});
}
}
},
recentchanges: function($context) {
if (Twinkle.getPref('showRollbackLinks').includes('recent')) {
// Latest and revertable (not page creations, logs, categorizations, etc.)
const selector = '.mw-changeslist-last.mw-changeslist-src-mw-edit';
let $list = $context.hasClass('mw-changeslist') ? $context.find(selector) : $context.find('.mw-changeslist ' + selector);
if (!$list.length) {
return;
}
// Exclude top-level header if "group changes" preference is used
// and find only individual lines or nested lines
$list = $list.not('.mw-rcfilters-ui-highlights-enhanced-toplevel').find('.mw-changeslist-line-inner, td.mw-enhanced-rc-nested');
$list.each((key, current) => {
// The :not is possibly unnecessary, as it appears that
// .mw-userlink is simply not present if the username is hidden
const vandal = $(current).find('.mw-userlink:not(.history-deleted)').text();
const href = $(current).find('.mw-changeslist-diff').attr('href');
const rev = mw.util.getParamValue('diff', href);
const page = current.dataset.targetPage;
current.appendChild(Twinkle.rollback.linkBuilder.rollbackLinks(vandal, true, rev, page));
});
}
},
history: function() {
if (Twinkle.getPref('showRollbackLinks').includes('history')) {
// All revs
const histList = $('#pagehistory li').toArray();
// On first page of results, so add revert/rollback
// links to the top revision
if (!$('a.mw-firstlink').length) {
const firstRow = histList.shift();
const firstUser = $(firstRow).find('.mw-userlink:not(.history-deleted)').text();
// Check for first username different than the top user,
// only apply rollback links if/when found
// for() faster than every()
for (let i = 0; i < histList.length; i++) {
const hasMoreThanOneUser = $(histList[i]).find('.mw-userlink').text() !== firstUser;
if (hasMoreThanOneUser) {
firstRow.appendChild(Twinkle.rollback.linkBuilder.rollbackLinks(firstUser, true));
break;
}
}
}
// oldid
histList.forEach((rev) => {
// From restoreThisRevision, non-transferable
// If the text has been revdel'd, it gets wrapped in a span with .history-deleted,
// and href will be undefined (and thus oldid is NaN)
const href = rev.querySelector('.mw-changeslist-date').href;
const oldid = parseInt(mw.util.getParamValue('oldid', href), 10);
if (!isNaN(oldid)) {
rev.appendChild(Twinkle.rollback.linkBuilder.restoreThisRevisionLink(oldid, true));
}
});
}
},
diff: function($context) {
// Autofill user talk links on diffs with vanarticle for easy warning, but don't autowarn
const warnFromTalk = function(xtitle) {
const $talkLink = $context.find('#mw-diff-' + xtitle + '2 .mw-usertoollinks a').first();
if ($talkLink.length) {
let extraParams = 'vanarticle=' + mw.util.rawurlencode(Morebits.pageNameNorm) + '&noautowarn=true';
// diffIDs for vanarticlerevid
extraParams += '&vanarticlerevid=';
extraParams += xtitle === 'otitle' ? mw.config.get('wgDiffOldId') : mw.config.get('wgDiffNewId');
const href = $talkLink.attr('href');
if (!href.includes('?')) {
$talkLink.attr('href', href + '?' + extraParams);
} else {
$talkLink.attr('href', href + '&' + extraParams);
}
}
};
// Older revision
warnFromTalk('otitle'); // Add quick-warn link to user talk link
// Don't load if there's a single revision or weird diff (cur on latest)
if (mw.config.get('wgDiffOldId') && (mw.config.get('wgDiffOldId') !== mw.config.get('wgDiffNewId'))) {
// Add a [restore this revision] link to the older revision
const oldTitle = $context.find('#mw-diff-otitle1').parent().get(0);
if (oldTitle) {
oldTitle.insertBefore(Twinkle.rollback.linkBuilder.restoreThisRevisionLink('wgDiffOldId'), oldTitle.firstChild);
}
}
// Newer revision
warnFromTalk('ntitle'); // Add quick-warn link to user talk link
// Add either restore or rollback links to the newer revision
// Don't show if there's a single revision or weird diff (prev on first)
if ($context.find('#differences-nextlink').length) {
// Not latest revision, add [restore this revision] link to newer revision
const newTitle = $context.find('#mw-diff-ntitle1').parent().get(0);
if (newTitle) {
newTitle.insertBefore(Twinkle.rollback.linkBuilder.restoreThisRevisionLink('wgDiffNewId'), newTitle.firstChild);
}
} else if (Twinkle.getPref('showRollbackLinks').includes('diff') && mw.config.get('wgDiffOldId') && (mw.config.get('wgDiffOldId') !== mw.config.get('wgDiffNewId') || $context.find('#differences-prevlink').length)) {
// Normally .mw-userlink is a link, but if the
// username is hidden, it will be a span with
// .history-deleted as well. When a sysop views the
// hidden content, the span contains the username in a
// link element, which will *just* have
// .mw-userlink. The below thus finds the first
// instance of the class, which if hidden is the span
// and thus text returns undefined. Technically, this
// is a place where sysops *could* have more
// information available to them (as above, via
// &unhide=1), since the username will be available by
// checking a.mw-userlink instead, but revert() will
// need reworking around userHidden
let vandal = $context.find('#mw-diff-ntitle2').find('.mw-userlink')[0];
// See #1337
vandal = vandal ? vandal.text : '';
const ntitle = $context.find('#mw-diff-ntitle1').parent().get(0);
if (ntitle) {
ntitle.insertBefore(Twinkle.rollback.linkBuilder.rollbackLinks(vandal), ntitle.firstChild);
}
}
},
oldid: function() { // Add a [restore this revision] link on old revisions
const revisionInfo = document.getElementById('mw-revision-info');
if (revisionInfo) {
const title = revisionInfo.parentNode;
title.insertBefore(Twinkle.rollback.linkBuilder.restoreThisRevisionLink('wgRevisionId'), title.firstChild);
}
}
};
Twinkle.rollback.disableLinks = function disablelinks(parentNode) {
$(parentNode).children().each((_ix, node) => {
node.innerHTML = node.textContent; // Feels like cheating
$(node).css('font-weight', 'normal').css('color', 'darkgray');
});
};
Twinkle.rollback.revert = function revertPage(type, vandal, rev, page) {
if (mw.util.isIPv6Address(vandal)) {
vandal = Morebits.ip.sanitizeIPv6(vandal);
}
const pagename = page || mw.config.get('wgPageName');
const revid = rev || mw.config.get('wgCurRevisionId');
if (Twinkle.rollback.rollbackInPlace) {
const notifyStatus = document.createElement('span');
mw.notify(notifyStatus, {
autoHide: false,
title: page + 'の差し戻し',
tag: 'twinklerollback_' + rev // Shouldn't be necessary given disableLink
});
Morebits.Status.init(notifyStatus);
} else {
Morebits.Status.init(document.getElementById('mw-content-text'));
$('#catlinks').remove();
}
const params = {
type: type,
user: vandal,
userHidden: !vandal, // Keep track of whether the username was hidden
pagename: pagename,
revid: revid
};
const query = {
action: 'query',
prop: ['info', 'revisions', 'flagged'],
titles: pagename,
inprop: 'watched',
intestactions: 'edit',
rvlimit: Twinkle.getPref('revertMaxRevisions'),
rvprop: ['ids', 'timestamp', 'user'],
curtimestamp: '',
meta: 'tokens',
type: 'csrf',
format: 'json'
};
var wikipedia_api = new Morebits.wiki.Api('以前の版のデータを取得', query, Twinkle.rollback.callbacks.main);
wikipedia_api.params = params;
wikipedia_api.post();
};
Twinkle.rollback.revertToRevision = function revertToRevision(oldrev) {
Morebits.Status.init(document.getElementById('mw-content-text'));
const query = {
action: 'query',
prop: ['info', 'revisions'],
titles: mw.config.get('wgPageName'),
inprop: 'watched',
rvlimit: 1,
rvstartid: oldrev,
rvprop: ['ids', 'user'],
curtimestamp: '',
meta: 'tokens',
type: 'csrf',
format: 'json'
};
var wikipedia_api = new Morebits.wiki.Api('以前の版のデータを取得', query, Twinkle.rollback.callbacks.toRevision);
wikipedia_api.params = { rev: oldrev };
wikipedia_api.post();
};
Twinkle.rollback.callbacks = {
toRevision: function(apiobj) {
const response = apiobj.getResponse();
const loadtimestamp = response.curtimestamp;
const csrftoken = response.query.tokens.csrftoken;
const page = response.query.pages[0];
const lastrevid = parseInt(page.lastrevid, 10);
const touched = page.touched;
const rev = page.revisions[0];
const revertToRevID = parseInt(rev.revid, 10);
const revertToUser = rev.user;
const revertToUserHidden = !!rev.userhidden;
if (revertToRevID !== apiobj.params.rev) {
apiobj.statelem.error('取得した版が要求された版と一致しませんでした。差し戻しを中止します。');
return;
}
var optional_summary = prompt('差し戻しの理由を入力してください: ', ''); // padded out to widen prompt in Firefox
if (optional_summary === null) {
apiobj.statelem.error('利用者により中止。');
return;
}
var summary = Twinkle.rollback.formatSummary('$USERさんによる ID:' + revertToRevID + 'の版を復元',
revertToUserHidden ? null : revertToUser, optional_summary);
const query = {
action: 'edit',
title: mw.config.get('wgPageName'),
summary: summary + Twinkle.summaryAd,
token: csrftoken,
undo: lastrevid,
undoafter: revertToRevID,
basetimestamp: touched,
starttimestamp: loadtimestamp,
minor: Twinkle.getPref('markRevertedPagesAsMinor').includes('torev') ? true : undefined,
format: 'json'
};
// Handle watching, possible expiry
if (Twinkle.getPref('watchRevertedPages').includes('torev')) {
const watchOrExpiry = Twinkle.getPref('watchRevertedExpiry');
if (!watchOrExpiry || watchOrExpiry === 'no') {
query.watchlist = 'nochange';
} else if (watchOrExpiry === 'default' || watchOrExpiry === 'preferences') {
query.watchlist = 'preferences';
} else {
query.watchlist = 'watch';
// number allowed but not used in Twinkle.config.watchlistEnums
if ((!page.watched || page.watchlistexpiry) && typeof watchOrExpiry === 'string' && watchOrExpiry !== 'yes') {
query.watchlistexpiry = watchOrExpiry;
}
}
}
Morebits.wiki.actionCompleted.redirect = mw.config.get('wgPageName');
Morebits.wiki.actionCompleted.notice = 'Reversion completed';
var wikipedia_api = new Morebits.wiki.Api('差し戻した内容を保存しています', query, Twinkle.rollback.callbacks.complete, apiobj.statelem);
wikipedia_api.params = apiobj.params;
wikipedia_api.post();
},
main: function(apiobj) {
const response = apiobj.getResponse();
const loadtimestamp = response.curtimestamp;
const csrftoken = response.query.tokens.csrftoken;
const page = response.query.pages[0];
if (!page.actions.edit) {
apiobj.statelem.error('ページを編集できませんでした。おそらく保護されています。');
return;
}
const lastrevid = parseInt(page.lastrevid, 10);
const touched = page.touched;
const revs = page.revisions;
const statelem = apiobj.statelem;
const params = apiobj.params;
if (revs.length < 1) {
statelem.error('版の数が1版以下であるため、差し戻すことは不可能です。');
return;
}
const top = revs[0];
const lastuser = top.user;
if (lastrevid < params.revid) {
Morebits.Status.error('Error', ['サーバーから取得した最新の版ID(', Morebits.htmlNode('strong', lastrevid), ')が表示されている版のIDより小さいです。現在の版が削除されたか、サーバーの遅延、または誤ったデータを取得した可能性があります。差し戻しを停止します。']);
return;
}
// Used for user-facing alerts, messages, etc., not edits or summaries
let userNorm = params.user || Twinkle.rollback.hiddenName;
let index = 1;
if (params.revid !== lastrevid) {
Morebits.Status.warn('Warning', ['最新版 ', Morebits.htmlNode('strong', lastrevid), ' は現在の版 ', Morebits.htmlNode('strong', params.revid), ' と一致しません。']);
// Treat ipv6 users on same 64 block as the same
if (lastuser === params.user || (mw.util.isIPv6Address(params.user) && Morebits.ip.get64(lastuser) === Morebits.ip.get64(params.user))) {
switch (params.type) {
case 'vand':
var diffUser = lastuser !== params.user;
Morebits.Status.info('情報', ['最新版' + (diffUser ? 'は同じ/64サブネット上の' : 'も'), Morebits.htmlNode('strong', userNorm),
'さんによる編集です。破壊行為とみなして、差し戻しを進めます。']);
break;
default:
Morebits.Status.warn('Notice', ['最新版は', Morebits.htmlNode('strong', userNorm), 'さんによるものですが、差し戻しを停止します。']);
return;
}
} else if (params.type === 'vand' &&
// Okay to test on user since it will either fail or sysop will correctly access it
// Besides, none of the trusted bots are going to be revdel'd
Twinkle.rollback.trustedBots.includes(top.user) && revs.length > 1 &&
revs[1].revid === params.revid) {
Morebits.Status.info('情報', ['Latest revision was made by ', Morebits.htmlNode('strong', lastuser), ', a trusted bot, and the revision before was made by our vandal, so we will proceed with the revert.']);
index = 2;
} else {
Morebits.Status.error('Error', ['最新版は', Morebits.htmlNode('strong', lastuser), 'さんによるものであるため既に差し戻されたかもしれません。差し戻しを停止します。']);
return;
}
} else {
// Expected revision is the same, so the users must match;
// this allows sysops to know whether the users are the same
params.user = lastuser;
userNorm = params.user || Twinkle.rollback.hiddenName;
}
if (Twinkle.rollback.trustedBots.includes(params.user)) {
switch (params.type) {
case 'vand':
Morebits.Status.info('情報', ['Vandalism revert was chosen on ', Morebits.htmlNode('strong', userNorm), '. As this is a trusted bot, we assume you wanted to revert vandalism made by the previous user instead.']);
index = 2;
params.user = revs[1].user;
params.userHidden = !!revs[1].userhidden;
break;
case 'norm':
/* falls through */
default:
var cont = confirm('Normal revert was chosen, but the most recent edit was made by a trusted bot (' + userNorm + '). Do you want to revert the revision before instead?');
if (cont) {
Morebits.Status.info('情報', ['Normal revert was chosen on ', Morebits.htmlNode('strong', userNorm), '. This is a trusted bot, and per confirmation, we\'ll revert the previous revision instead.']);
index = 2;
params.user = revs[1].user;
params.userHidden = !!revs[1].userhidden;
userNorm = params.user || Twinkle.rollback.hiddenName;
} else {
Morebits.Status.warn('Notice', ['Normal revert was chosen on ', Morebits.htmlNode('strong', userNorm), '. This is a trusted bot, but per confirmation, revert on selected revision will proceed.']);
}
break;
}
}
let found = false;
let count = 0;
let seen64 = false;
for (let i = index; i < revs.length; ++i) {
++count;
if (revs[i].user !== params.user) {
// Treat ipv6 users on same 64 block as the same
if (mw.util.isIPv6Address(revs[i].user) && Morebits.ip.get64(revs[i].user) === Morebits.ip.get64(params.user)) {
if (!seen64) {
new Morebits.Status('Note', '連続してる同じ/64サブネット内のIPv6アドレスは同じ利用者として扱います。');
seen64 = true;
}
continue;
}
found = i;
break;
}
}
if (!found) {
statelem.error(['No previous revision found. Perhaps ', Morebits.htmlNode('strong', userNorm), ' is the only contributor, or they have made more than ' + mw.language.convertNumber(Twinkle.getPref('revertMaxRevisions')) + ' edits in a row.']);
statelem.error(['以前の版が見つかりませんでした。もしかすると', Morebits.htmlNode('strong', userNorm), 'さんが唯一の投稿者であるか、' + mw.language.convertNumber(Twinkle.getPref('revertMaxRevisions')) + '回以上の編集を連続して行ったかもしれません。']);
return;
}
if (!count) {
Morebits.Status.error('Error', '0版を差し戻すことはできませんので、差し戻しを中止します。編集が既に差し戻されているにもかかわらず、版IDが同じままになっている可能性があります。');
return;
}
const good_revision = revs[found];
let userHasAlreadyConfirmedAction = false;
if (params.type !== 'vand' && count > 1) {
if (!confirm(userNorm + 'さんは' + mw.language.convertNumber(count) + '回以上の編集を連続して行いました。本当にすべて差し戻しますか?')) {
Morebits.Status.info('通知', '差し戻しを中止');
return;
}
userHasAlreadyConfirmedAction = true;
}
params.count = count;
params.goodid = good_revision.revid;
params.gooduser = good_revision.user;
params.gooduserHidden = !!good_revision.userhidden;
statelem.status([' revision ', Morebits.htmlNode('strong', params.goodid), ' that was made ', Morebits.htmlNode('strong', mw.language.convertNumber(count)), ' revisions ago by ', Morebits.htmlNode('strong', params.gooduserHidden ? Twinkle.rollback.hiddenName : params.gooduser)]);
let summary, extra_summary;
switch (params.type) {
case 'vand':
summary = Twinkle.getPref('SummaryOnVandalRevert');
break;
case 'norm':
/* falls through */
default:
if (Twinkle.getPref('offerReasonOnNormalRevert')) {
extra_summary = prompt('編集要約用の追加コメント: ', ''); // padded out to widen prompt in Firefox
if (extra_summary === null) {
statelem.error('利用者により中止。');
return;
}
userHasAlreadyConfirmedAction = true;
}
summary = Twinkle.rollback.formatSummary('$USERさんによる' + params.count + '版を差し戻し',
params.userHidden ? null : params.user, extra_summary);
break;
}
const needToDisplayConfirmation =
(
Twinkle.getPref('confirmOnRollback') ||
(
Twinkle.getPref('confirmOnMobileRollback') &&
// Mobile user agent taken from [[en:MediaWiki:Gadget-confirmationRollback-mobile.js]]
/Android|webOS|iPhone|iPad|iPod|BlackBerry|Mobile|Opera Mini/i.test(navigator.userAgent)
)
) &&
!userHasAlreadyConfirmedAction;
if (needToDisplayConfirmation && !confirm('ページの差し戻し: よろしいですか?')) {
statelem.error('Aborted by user.');
return;
}
// Decide whether to notify the user on success
if (!Twinkle.rollback.skipTalk && Twinkle.getPref('openTalkPage').includes(params.type) &&
!params.userHidden && mw.config.get('wgUserName') !== params.user) {
params.notifyUser = true;
// Pass along to the warn module
params.vantimestamp = top.timestamp;
}
// figure out whether we need to/can review the edit
const flagged = page.flagged;
if ((Morebits.userIsInGroup('reviewer') || Morebits.userIsSysop) &&
!!flagged &&
flagged.stable_revid >= params.goodid &&
!!flagged.pending_since) {
params.reviewRevert = true;
params.csrftoken = csrftoken;
}
const query = {
action: 'edit',
title: params.pagename,
summary: summary + Twinkle.summaryAd,
token: csrftoken,
undo: lastrevid,
undoafter: params.goodid,
basetimestamp: touched,
starttimestamp: loadtimestamp,
minor: Twinkle.getPref('markRevertedPagesAsMinor').includes(params.type) ? true : undefined,
format: 'json'
};
// Handle watching, possible expiry
if (Twinkle.getPref('watchRevertedPages').includes(params.type)) {
const watchOrExpiry = Twinkle.getPref('watchRevertedExpiry');
if (!watchOrExpiry || watchOrExpiry === 'no') {
query.watchlist = 'nochange';
} else if (watchOrExpiry === 'default' || watchOrExpiry === 'preferences') {
query.watchlist = 'preferences';
} else {
query.watchlist = 'watch';
// number allowed but not used in Twinkle.config.watchlistEnums
if ((!page.watched || page.watchlistexpiry) && typeof watchOrExpiry === 'string' && watchOrExpiry !== 'yes') {
query.watchlistexpiry = watchOrExpiry;
}
}
}
if (!Twinkle.rollback.rollbackInPlace) {
Morebits.wiki.actionCompleted.redirect = params.pagename;
}
Morebits.wiki.actionCompleted.notice = '復元完了';
var wikipedia_api = new Morebits.wiki.Api('差し戻した内容を保存します', query, Twinkle.rollback.callbacks.complete, statelem);
wikipedia_api.params = params;
wikipedia_api.post();
},
complete: function(apiobj) {
// TODO Most of this is copy-pasted from Morebits.wiki.Page#fnSaveSuccess. Unify it
const response = apiobj.getResponse();
const edit = response.edit;
if (edit.captcha) {
apiobj.statelem.error('サーバーがCAPTCHAを入力するように要求したため、差し戻せませんでした。');
} else if (edit.nochange) {
apiobj.statelem.error('差し戻し先の版は、現在の版の内容と同じです。');
} else {
apiobj.statelem.info('完了');
var params = apiobj.params;
if (params.notifyUser && !params.userHidden) { // notifyUser only from main, not from toRevision
Morebits.Status.info('情報', ['利用者:', Morebits.htmlNode('strong', params.user), 'の会話ページ編集フォームを開きます']);
const url = mw.util.getUrl('User talk:' + params.user, {
action: 'edit',
preview: 'yes',
vanarticle: params.pagename.replace(/_/g, ' '),
vanarticlerevid: params.revid,
vantimestamp: params.vantimestamp,
vanarticlegoodrevid: params.goodid,
type: params.type,
count: params.count
});
switch (Twinkle.getPref('userTalkPageMode')) {
case 'tab':
window.open(url, '_blank');
break;
case 'blank':
window.open(url, '_blank',
'location=no,toolbar=no,status=no,directories=no,scrollbars=yes,width=1200,height=800');
break;
case 'window':
/* falls through */
default:
window.open(url,
window.name === 'twinklewarnwindow' ? '_blank' : 'twinklewarnwindow',
'location=no,toolbar=no,status=no,directories=no,scrollbars=yes,width=1200,height=800');
break;
}
// prefill Wel/ARV/Warn when rollback used on Special:Contributions page
} else if (Twinkle.rollback.rollbackInPlace &&
mw.config.get('wgCanonicalSpecialPageName') === 'Contributions') {
Twinkle.setPrefill('vanarticle', params.pagename.replace(/_/g, ' '));
Twinkle.setPrefill('vanarticlerevid', params.revid);
Twinkle.setPrefill('vantimestamp', params.vantimestamp);
Twinkle.setPrefill('vanarticlegoodrevid', params.goodid);
}
// review the revert, if needed
if (apiobj.params.reviewRevert) {
const query = {
action: 'review',
revid: edit.newrevid,
token: apiobj.params.csrftoken,
comment: 'Automatically reviewing reversion' + Twinkle.summaryAd // until the below
// 'tags': Twinkle.changeTags // flaggedrevs tag support: [[phab:T247721]]
};
const wikipedia_api = new Morebits.wiki.Api('Automatically accepting your changes', query);
wikipedia_api.post();
}
}
}
};
// If builtInString contains the string "$USER", it will be replaced
// by an appropriate user link if a user name is provided
Twinkle.rollback.formatSummary = function(builtInString, userName, customString) {
let result = builtInString;
// append user's custom reason
if (customString) {
result += ': ' + Morebits.string.toUpperCaseFirstChar(customString);
}
// find number of UTF-8 bytes the resulting string takes up, and possibly add
// a contributions or contributions+talk link if it doesn't push the edit summary
// over the 499-byte limit
if (/\$USER/.test(builtInString)) {
if (userName) {
const resultLen = unescape(encodeURIComponent(result.replace('$USER', ''))).length;
const contribsLink = '[[Special:Contributions/' + userName + '|' + userName + ']]';
const contribsLen = unescape(encodeURIComponent(contribsLink)).length;
if (resultLen + contribsLen <= 499) {
var talkLink = ' ([[User talk:' + userName + '|会話]])';
if (resultLen + contribsLen + unescape(encodeURIComponent(talkLink)).length <= 499) {
result = Morebits.string.safeReplace(result, '$USER', contribsLink + talkLink);
} else {
result = Morebits.string.safeReplace(result, '$USER', contribsLink);
}
} else {
result = Morebits.string.safeReplace(result, '$USER', userName);
}
} else {
result = Morebits.string.safeReplace(result, '$USER', Twinkle.rollback.hiddenName);
}
}
return result;
};
// Keep the public name used by existing jawiki add-on scripts.
Twinkle.fluff = Twinkle.rollback;
Twinkle.addInitCallback(Twinkle.rollback, 'rollback');
}());
// </nowiki>
766l64zioay10rwmzuakpn2jr8i2gjt
769531
769464
2026-10-22T00:00:30Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 1406828: rm cmt
769531
javascript
text/javascript
// <nowiki>
(function() {
/*
****************************************
*** twinklerollback.js: Revert/rollback module
****************************************
* Mode of invocation: Links on contributions, recent changes, history, and diff pages
* Active on: Diff pages, history pages, Special:RecentChanges(Linked),
and Special:Contributions
*/
/**
* Twinklerollback revert and antivandalism utility
*/
Twinkle.rollback = function twinklerollback() {
// Only proceed if the user can actually edit the page in question
// (see #632 for contribs issue). wgIsProbablyEditable should take
// care of namespace/contentModel restrictions as well as explicit
// protections; it won't take care of cascading or TitleBlacklist.
if (mw.config.get('wgIsProbablyEditable')) {
if (mw.config.get('wgAction') === 'view' && mw.config.get('wgRevisionId') && mw.config.get('wgCurRevisionId') !== mw.config.get('wgRevisionId')) {
Twinkle.rollback.addLinks.oldid();
} else if (mw.config.get('wgAction') === 'history' && mw.config.get('wgArticleId')) {
Twinkle.rollback.addLinks.history();
}
} else if (mw.config.get('wgNamespaceNumber') === -1) {
Twinkle.rollback.skipTalk = !Twinkle.getPref('openTalkPageOnAutoRevert');
Twinkle.rollback.rollbackInPlace = Twinkle.getPref('rollbackInPlace');
switch (mw.config.get('wgCanonicalSpecialPageName')) {
case 'Contributions':
case 'IPContributions':
Twinkle.rollback.addLinks.contributions();
break;
case 'Recentchanges':
case 'Recentchangeslinked':
// Reload with recent changes updates
// structuredChangeFilters.ui.initialized is just on load
mw.hook('wikipage.content').add(($context) => {
if (!$context || !$context.is('div')) {
return;
}
Twinkle.rollback.addLinks.recentchanges($context);
});
break;
}
}
// Reload when revision slider or other scripts dynamically load diff content.
mw.hook('wikipage.diff').add(($context) => {
if (!$context) {
return;
}
// Only proceed if the user can actually edit the page in question,
// wgDiffOldId included for clarity in if else loop [[phab:T214985]]
if (mw.config.get('wgIsProbablyEditable') && (mw.config.get('wgDiffNewId') || mw.config.get('wgDiffOldId'))) {
Twinkle.rollback.addLinks.diff($context);
}
});
};
// A list of usernames, usually only bots, that vandalism revert is jumped
// over; that is, if vandalism revert was chosen on such username, then its
// target is on the revision before. This is for handling quick bots that
// makes edits seconds after the original edit is made. This only affects
// vandalism rollback; for good faith rollback, it will stop, indicating a bot
// has no faith, and for normal rollback, it will rollback that edit.
Twinkle.rollback.trustedBots = ['AnomieBOT', 'SineBot', 'MajavahBot'];
Twinkle.rollback.skipTalk = null;
Twinkle.rollback.rollbackInPlace = null;
// String to insert when a username is hidden
Twinkle.rollback.hiddenName = '不明な利用者';
// Consolidated construction of rollback links
Twinkle.rollback.linkBuilder = {
spanTag: function(color, content) {
const span = document.createElement('span');
span.style.color = color;
span.appendChild(document.createTextNode(content));
return span;
},
buildLink: function(color, text) {
const link = document.createElement('a');
link.appendChild(Twinkle.rollback.linkBuilder.spanTag('Black', '['));
link.appendChild(Twinkle.rollback.linkBuilder.spanTag(color, text));
link.appendChild(Twinkle.rollback.linkBuilder.spanTag('Black', ']'));
link.href = '#';
return link;
},
/**
* @param {string} [vandal=null] - Username of the editor being reverted
* Provide a falsey value if the username is hidden, defaults to null
* @param {boolean} inline - True to create two links in a span, false
* to create three links in a div (optional)
* @param {number|string} [rev=wgCurRevisionId] - Revision ID being reverted (optional)
* @param {string} [page=wgPageName] - Page being reverted (optional)
*/
rollbackLinks: function(vandal, inline, rev, page) {
vandal = vandal || null;
const elem = inline ? 'span' : 'div';
const revNode = document.createElement(elem);
rev = parseInt(rev, 10);
if (rev) {
revNode.setAttribute('id', 'tw-revert' + rev);
} else {
revNode.setAttribute('id', 'tw-revert');
}
const separator = inline ? ' ' : ' || ';
const sepNode1 = document.createElement('span');
const sepText = document.createTextNode(separator);
sepNode1.setAttribute('class', 'tw-rollback-link-separator');
sepNode1.appendChild(sepText);
const sepNode2 = sepNode1.cloneNode(true);
const normNode = document.createElement('span');
const vandNode = document.createElement('span');
var normLink = Twinkle.rollback.linkBuilder.buildLink('SteelBlue', '差し戻し');
var vandLink = Twinkle.rollback.linkBuilder.buildLink('Red', '荒らし');
normLink.style.fontWeight = 'bold';
vandLink.style.fontWeight = 'bold';
$(normLink).on('click', (e) => {
e.preventDefault();
Twinkle.rollback.revert('norm', vandal, rev, page);
Twinkle.rollback.disableLinks(revNode);
});
$(vandLink).on('click', (e) => {
e.preventDefault();
Twinkle.rollback.revert('vand', vandal, rev, page);
Twinkle.rollback.disableLinks(revNode);
});
normNode.setAttribute('class', 'tw-rollback-link-normal');
vandNode.setAttribute('class', 'tw-rollback-link-vandalism');
vandNode.appendChild(sepNode2);
normNode.appendChild(normLink);
vandNode.appendChild(vandLink);
revNode.appendChild(normNode);
revNode.appendChild(vandNode);
return revNode;
},
// Build [restore this revision] links
restoreThisRevisionLink: function(revisionRef, inline) {
// If not a specific revision number, should be wgDiffNewId/wgDiffOldId/wgRevisionId
revisionRef = typeof revisionRef === 'number' ? revisionRef : mw.config.get(revisionRef);
const elem = inline ? 'span' : 'div';
const revertToRevisionNode = document.createElement(elem);
revertToRevisionNode.setAttribute('id', 'tw-revert-to-' + revisionRef);
revertToRevisionNode.style.fontWeight = 'bold';
var revertToRevisionLink = Twinkle.rollback.linkBuilder.buildLink('SaddleBrown', 'この版を復元');
$(revertToRevisionLink).on('click', (e) => {
e.preventDefault();
Twinkle.rollback.revertToRevision(revisionRef);
});
if (inline) {
revertToRevisionNode.appendChild(document.createTextNode(' '));
}
revertToRevisionNode.appendChild(revertToRevisionLink);
return revertToRevisionNode;
}
};
Twinkle.rollback.addLinks = {
contributions: function() {
// $('sp-contributions-footer-anon-range') relies on the fmbox
// id in [[MediaWiki:Sp-contributions-footer-anon-range]] and
// is used to show rollback/vandalism links for IP ranges
const isRange = !!$('#sp-contributions-footer-anon-range')[0];
if (mw.config.exists('wgRelevantUserName') || isRange) {
// Get the username these contributions are for
let username = mw.config.get('wgRelevantUserName');
if (Twinkle.getPref('showRollbackLinks').includes('contribs') ||
(mw.config.get('wgUserName') !== username && Twinkle.getPref('showRollbackLinks').includes('others')) ||
(mw.config.get('wgUserName') === username && Twinkle.getPref('showRollbackLinks').includes('mine'))) {
const $list = $('#mw-content-text').find('ul li:has(span.mw-uctop):has(.mw-changeslist-diff)');
$list.each((key, current) => {
// revid is also available in the href of both
// .mw-changeslist-date or .mw-changeslist-diff
const page = $(current).find('.mw-contributions-title').text();
// Get username for IP ranges (wgRelevantUserName is null)
if (isRange) {
// The :not is possibly unnecessary, as it appears that
// .mw-userlink is simply not present if the username is hidden
username = $(current).find('.mw-userlink:not(.history-deleted)').text();
}
// It's unlikely, but we can't easily check for revdel'd usernames
// since only a strong element is provided, with no easy selector [[phab:T255903]]
current.appendChild(Twinkle.rollback.linkBuilder.rollbackLinks(username, true, current.dataset.mwRevid, page));
});
}
}
},
recentchanges: function($context) {
if (Twinkle.getPref('showRollbackLinks').includes('recent')) {
// Latest and revertable (not page creations, logs, categorizations, etc.)
const selector = '.mw-changeslist-last.mw-changeslist-src-mw-edit';
let $list = $context.hasClass('mw-changeslist') ? $context.find(selector) : $context.find('.mw-changeslist ' + selector);
if (!$list.length) {
return;
}
// Exclude top-level header if "group changes" preference is used
// and find only individual lines or nested lines
$list = $list.not('.mw-rcfilters-ui-highlights-enhanced-toplevel').find('.mw-changeslist-line-inner, td.mw-enhanced-rc-nested');
$list.each((key, current) => {
// The :not is possibly unnecessary, as it appears that
// .mw-userlink is simply not present if the username is hidden
const vandal = $(current).find('.mw-userlink:not(.history-deleted)').text();
const href = $(current).find('.mw-changeslist-diff').attr('href');
const rev = mw.util.getParamValue('diff', href);
const page = current.dataset.targetPage;
current.appendChild(Twinkle.rollback.linkBuilder.rollbackLinks(vandal, true, rev, page));
});
}
},
history: function() {
if (Twinkle.getPref('showRollbackLinks').includes('history')) {
// All revs
const histList = $('#pagehistory li').toArray();
// On first page of results, so add revert/rollback
// links to the top revision
if (!$('a.mw-firstlink').length) {
const firstRow = histList.shift();
const firstUser = $(firstRow).find('.mw-userlink:not(.history-deleted)').text();
// Check for first username different than the top user,
// only apply rollback links if/when found
// for() faster than every()
for (let i = 0; i < histList.length; i++) {
const hasMoreThanOneUser = $(histList[i]).find('.mw-userlink').text() !== firstUser;
if (hasMoreThanOneUser) {
firstRow.appendChild(Twinkle.rollback.linkBuilder.rollbackLinks(firstUser, true));
break;
}
}
}
// oldid
histList.forEach((rev) => {
// From restoreThisRevision, non-transferable
// If the text has been revdel'd, it gets wrapped in a span with .history-deleted,
// and href will be undefined (and thus oldid is NaN)
const href = rev.querySelector('.mw-changeslist-date').href;
const oldid = parseInt(mw.util.getParamValue('oldid', href), 10);
if (!isNaN(oldid)) {
rev.appendChild(Twinkle.rollback.linkBuilder.restoreThisRevisionLink(oldid, true));
}
});
}
},
diff: function($context) {
// Autofill user talk links on diffs with vanarticle for easy warning, but don't autowarn
const warnFromTalk = function(xtitle) {
const $talkLink = $context.find('#mw-diff-' + xtitle + '2 .mw-usertoollinks a').first();
if ($talkLink.length) {
let extraParams = 'vanarticle=' + mw.util.rawurlencode(Morebits.pageNameNorm) + '&noautowarn=true';
// diffIDs for vanarticlerevid
extraParams += '&vanarticlerevid=';
extraParams += xtitle === 'otitle' ? mw.config.get('wgDiffOldId') : mw.config.get('wgDiffNewId');
const href = $talkLink.attr('href');
if (!href.includes('?')) {
$talkLink.attr('href', href + '?' + extraParams);
} else {
$talkLink.attr('href', href + '&' + extraParams);
}
}
};
// Older revision
warnFromTalk('otitle'); // Add quick-warn link to user talk link
// Don't load if there's a single revision or weird diff (cur on latest)
if (mw.config.get('wgDiffOldId') && (mw.config.get('wgDiffOldId') !== mw.config.get('wgDiffNewId'))) {
// Add a [restore this revision] link to the older revision
const oldTitle = $context.find('#mw-diff-otitle1').parent().get(0);
if (oldTitle) {
oldTitle.insertBefore(Twinkle.rollback.linkBuilder.restoreThisRevisionLink('wgDiffOldId'), oldTitle.firstChild);
}
}
// Newer revision
warnFromTalk('ntitle'); // Add quick-warn link to user talk link
// Add either restore or rollback links to the newer revision
// Don't show if there's a single revision or weird diff (prev on first)
if ($context.find('#differences-nextlink').length) {
// Not latest revision, add [restore this revision] link to newer revision
const newTitle = $context.find('#mw-diff-ntitle1').parent().get(0);
if (newTitle) {
newTitle.insertBefore(Twinkle.rollback.linkBuilder.restoreThisRevisionLink('wgDiffNewId'), newTitle.firstChild);
}
} else if (Twinkle.getPref('showRollbackLinks').includes('diff') && mw.config.get('wgDiffOldId') && (mw.config.get('wgDiffOldId') !== mw.config.get('wgDiffNewId') || $context.find('#differences-prevlink').length)) {
// Normally .mw-userlink is a link, but if the
// username is hidden, it will be a span with
// .history-deleted as well. When a sysop views the
// hidden content, the span contains the username in a
// link element, which will *just* have
// .mw-userlink. The below thus finds the first
// instance of the class, which if hidden is the span
// and thus text returns undefined. Technically, this
// is a place where sysops *could* have more
// information available to them (as above, via
// &unhide=1), since the username will be available by
// checking a.mw-userlink instead, but revert() will
// need reworking around userHidden
let vandal = $context.find('#mw-diff-ntitle2').find('.mw-userlink')[0];
// See #1337
vandal = vandal ? vandal.text : '';
const ntitle = $context.find('#mw-diff-ntitle1').parent().get(0);
if (ntitle) {
ntitle.insertBefore(Twinkle.rollback.linkBuilder.rollbackLinks(vandal), ntitle.firstChild);
}
}
},
oldid: function() { // Add a [restore this revision] link on old revisions
const revisionInfo = document.getElementById('mw-revision-info');
if (revisionInfo) {
const title = revisionInfo.parentNode;
title.insertBefore(Twinkle.rollback.linkBuilder.restoreThisRevisionLink('wgRevisionId'), title.firstChild);
}
}
};
Twinkle.rollback.disableLinks = function disablelinks(parentNode) {
$(parentNode).children().each((_ix, node) => {
node.innerHTML = node.textContent; // Feels like cheating
$(node).css('font-weight', 'normal').css('color', 'darkgray');
});
};
Twinkle.rollback.revert = function revertPage(type, vandal, rev, page) {
if (mw.util.isIPv6Address(vandal)) {
vandal = Morebits.ip.sanitizeIPv6(vandal);
}
const pagename = page || mw.config.get('wgPageName');
const revid = rev || mw.config.get('wgCurRevisionId');
if (Twinkle.rollback.rollbackInPlace) {
const notifyStatus = document.createElement('span');
mw.notify(notifyStatus, {
autoHide: false,
title: page + 'の差し戻し',
tag: 'twinklerollback_' + rev // Shouldn't be necessary given disableLink
});
Morebits.Status.init(notifyStatus);
} else {
Morebits.Status.init(document.getElementById('mw-content-text'));
$('#catlinks').remove();
}
const params = {
type: type,
user: vandal,
userHidden: !vandal, // Keep track of whether the username was hidden
pagename: pagename,
revid: revid
};
const query = {
action: 'query',
prop: ['info', 'revisions', 'flagged'],
titles: pagename,
inprop: 'watched',
intestactions: 'edit',
rvlimit: Twinkle.getPref('revertMaxRevisions'),
rvprop: ['ids', 'timestamp', 'user'],
curtimestamp: '',
meta: 'tokens',
type: 'csrf',
format: 'json'
};
var wikipedia_api = new Morebits.wiki.Api('以前の版のデータを取得', query, Twinkle.rollback.callbacks.main);
wikipedia_api.params = params;
wikipedia_api.post();
};
Twinkle.rollback.revertToRevision = function revertToRevision(oldrev) {
Morebits.Status.init(document.getElementById('mw-content-text'));
const query = {
action: 'query',
prop: ['info', 'revisions'],
titles: mw.config.get('wgPageName'),
inprop: 'watched',
rvlimit: 1,
rvstartid: oldrev,
rvprop: ['ids', 'user'],
curtimestamp: '',
meta: 'tokens',
type: 'csrf',
format: 'json'
};
var wikipedia_api = new Morebits.wiki.Api('以前の版のデータを取得', query, Twinkle.rollback.callbacks.toRevision);
wikipedia_api.params = { rev: oldrev };
wikipedia_api.post();
};
Twinkle.rollback.callbacks = {
toRevision: function(apiobj) {
const response = apiobj.getResponse();
const loadtimestamp = response.curtimestamp;
const csrftoken = response.query.tokens.csrftoken;
const page = response.query.pages[0];
const lastrevid = parseInt(page.lastrevid, 10);
const touched = page.touched;
const rev = page.revisions[0];
const revertToRevID = parseInt(rev.revid, 10);
const revertToUser = rev.user;
const revertToUserHidden = !!rev.userhidden;
if (revertToRevID !== apiobj.params.rev) {
apiobj.statelem.error('取得した版が要求された版と一致しませんでした。差し戻しを中止します。');
return;
}
var optional_summary = prompt('差し戻しの理由を入力してください: ', ''); // padded out to widen prompt in Firefox
if (optional_summary === null) {
apiobj.statelem.error('利用者により中止。');
return;
}
var summary = Twinkle.rollback.formatSummary('$USERさんによる ID:' + revertToRevID + 'の版を復元',
revertToUserHidden ? null : revertToUser, optional_summary);
const query = {
action: 'edit',
title: mw.config.get('wgPageName'),
summary: summary + Twinkle.summaryAd,
token: csrftoken,
undo: lastrevid,
undoafter: revertToRevID,
basetimestamp: touched,
starttimestamp: loadtimestamp,
minor: Twinkle.getPref('markRevertedPagesAsMinor').includes('torev') ? true : undefined,
format: 'json'
};
// Handle watching, possible expiry
if (Twinkle.getPref('watchRevertedPages').includes('torev')) {
const watchOrExpiry = Twinkle.getPref('watchRevertedExpiry');
if (!watchOrExpiry || watchOrExpiry === 'no') {
query.watchlist = 'nochange';
} else if (watchOrExpiry === 'default' || watchOrExpiry === 'preferences') {
query.watchlist = 'preferences';
} else {
query.watchlist = 'watch';
// number allowed but not used in Twinkle.config.watchlistEnums
if ((!page.watched || page.watchlistexpiry) && typeof watchOrExpiry === 'string' && watchOrExpiry !== 'yes') {
query.watchlistexpiry = watchOrExpiry;
}
}
}
Morebits.wiki.actionCompleted.redirect = mw.config.get('wgPageName');
Morebits.wiki.actionCompleted.notice = 'Reversion completed';
var wikipedia_api = new Morebits.wiki.Api('差し戻した内容を保存しています', query, Twinkle.rollback.callbacks.complete, apiobj.statelem);
wikipedia_api.params = apiobj.params;
wikipedia_api.post();
},
main: function(apiobj) {
const response = apiobj.getResponse();
const loadtimestamp = response.curtimestamp;
const csrftoken = response.query.tokens.csrftoken;
const page = response.query.pages[0];
if (!page.actions.edit) {
apiobj.statelem.error('ページを編集できませんでした。おそらく保護されています。');
return;
}
const lastrevid = parseInt(page.lastrevid, 10);
const touched = page.touched;
const revs = page.revisions;
const statelem = apiobj.statelem;
const params = apiobj.params;
if (revs.length < 1) {
statelem.error('版の数が1版以下であるため、差し戻すことは不可能です。');
return;
}
const top = revs[0];
const lastuser = top.user;
if (lastrevid < params.revid) {
Morebits.Status.error('Error', ['サーバーから取得した最新の版ID(', Morebits.htmlNode('strong', lastrevid), ')が表示されている版のIDより小さいです。現在の版が削除されたか、サーバーの遅延、または誤ったデータを取得した可能性があります。差し戻しを停止します。']);
return;
}
// Used for user-facing alerts, messages, etc., not edits or summaries
let userNorm = params.user || Twinkle.rollback.hiddenName;
let index = 1;
if (params.revid !== lastrevid) {
Morebits.Status.warn('Warning', ['最新版 ', Morebits.htmlNode('strong', lastrevid), ' は現在の版 ', Morebits.htmlNode('strong', params.revid), ' と一致しません。']);
// Treat ipv6 users on same 64 block as the same
if (lastuser === params.user || (mw.util.isIPv6Address(params.user) && Morebits.ip.get64(lastuser) === Morebits.ip.get64(params.user))) {
switch (params.type) {
case 'vand':
var diffUser = lastuser !== params.user;
Morebits.Status.info('情報', ['最新版' + (diffUser ? 'は同じ/64サブネット上の' : 'も'), Morebits.htmlNode('strong', userNorm),
'さんによる編集です。破壊行為とみなして、差し戻しを進めます。']);
break;
default:
Morebits.Status.warn('Notice', ['最新版は', Morebits.htmlNode('strong', userNorm), 'さんによるものですが、差し戻しを停止します。']);
return;
}
} else if (params.type === 'vand' &&
// Okay to test on user since it will either fail or sysop will correctly access it
// Besides, none of the trusted bots are going to be revdel'd
Twinkle.rollback.trustedBots.includes(top.user) && revs.length > 1 &&
revs[1].revid === params.revid) {
Morebits.Status.info('情報', ['Latest revision was made by ', Morebits.htmlNode('strong', lastuser), ', a trusted bot, and the revision before was made by our vandal, so we will proceed with the revert.']);
index = 2;
} else {
Morebits.Status.error('Error', ['最新版は', Morebits.htmlNode('strong', lastuser), 'さんによるものであるため既に差し戻されたかもしれません。差し戻しを停止します。']);
return;
}
} else {
// Expected revision is the same, so the users must match;
// this allows sysops to know whether the users are the same
params.user = lastuser;
userNorm = params.user || Twinkle.rollback.hiddenName;
}
if (Twinkle.rollback.trustedBots.includes(params.user)) {
switch (params.type) {
case 'vand':
Morebits.Status.info('情報', ['Vandalism revert was chosen on ', Morebits.htmlNode('strong', userNorm), '. As this is a trusted bot, we assume you wanted to revert vandalism made by the previous user instead.']);
index = 2;
params.user = revs[1].user;
params.userHidden = !!revs[1].userhidden;
break;
case 'norm':
/* falls through */
default:
var cont = confirm('Normal revert was chosen, but the most recent edit was made by a trusted bot (' + userNorm + '). Do you want to revert the revision before instead?');
if (cont) {
Morebits.Status.info('情報', ['Normal revert was chosen on ', Morebits.htmlNode('strong', userNorm), '. This is a trusted bot, and per confirmation, we\'ll revert the previous revision instead.']);
index = 2;
params.user = revs[1].user;
params.userHidden = !!revs[1].userhidden;
userNorm = params.user || Twinkle.rollback.hiddenName;
} else {
Morebits.Status.warn('Notice', ['Normal revert was chosen on ', Morebits.htmlNode('strong', userNorm), '. This is a trusted bot, but per confirmation, revert on selected revision will proceed.']);
}
break;
}
}
let found = false;
let count = 0;
let seen64 = false;
for (let i = index; i < revs.length; ++i) {
++count;
if (revs[i].user !== params.user) {
// Treat ipv6 users on same 64 block as the same
if (mw.util.isIPv6Address(revs[i].user) && Morebits.ip.get64(revs[i].user) === Morebits.ip.get64(params.user)) {
if (!seen64) {
new Morebits.Status('Note', '連続してる同じ/64サブネット内のIPv6アドレスは同じ利用者として扱います。');
seen64 = true;
}
continue;
}
found = i;
break;
}
}
if (!found) {
statelem.error(['No previous revision found. Perhaps ', Morebits.htmlNode('strong', userNorm), ' is the only contributor, or they have made more than ' + mw.language.convertNumber(Twinkle.getPref('revertMaxRevisions')) + ' edits in a row.']);
statelem.error(['以前の版が見つかりませんでした。もしかすると', Morebits.htmlNode('strong', userNorm), 'さんが唯一の投稿者であるか、' + mw.language.convertNumber(Twinkle.getPref('revertMaxRevisions')) + '回以上の編集を連続して行ったかもしれません。']);
return;
}
if (!count) {
Morebits.Status.error('Error', '0版を差し戻すことはできませんので、差し戻しを中止します。編集が既に差し戻されているにもかかわらず、版IDが同じままになっている可能性があります。');
return;
}
const good_revision = revs[found];
let userHasAlreadyConfirmedAction = false;
if (params.type !== 'vand' && count > 1) {
if (!confirm(userNorm + 'さんは' + mw.language.convertNumber(count) + '回以上の編集を連続して行いました。本当にすべて差し戻しますか?')) {
Morebits.Status.info('通知', '差し戻しを中止');
return;
}
userHasAlreadyConfirmedAction = true;
}
params.count = count;
params.goodid = good_revision.revid;
params.gooduser = good_revision.user;
params.gooduserHidden = !!good_revision.userhidden;
statelem.status([' revision ', Morebits.htmlNode('strong', params.goodid), ' that was made ', Morebits.htmlNode('strong', mw.language.convertNumber(count)), ' revisions ago by ', Morebits.htmlNode('strong', params.gooduserHidden ? Twinkle.rollback.hiddenName : params.gooduser)]);
let summary, extra_summary;
switch (params.type) {
case 'vand':
summary = Twinkle.getPref('SummaryOnVandalRevert');
break;
case 'norm':
/* falls through */
default:
if (Twinkle.getPref('offerReasonOnNormalRevert')) {
extra_summary = prompt('編集要約用の追加コメント: ', ''); // padded out to widen prompt in Firefox
if (extra_summary === null) {
statelem.error('利用者により中止。');
return;
}
userHasAlreadyConfirmedAction = true;
}
summary = Twinkle.rollback.formatSummary('$USERさんによる' + params.count + '版を差し戻し',
params.userHidden ? null : params.user, extra_summary);
break;
}
const needToDisplayConfirmation =
(
Twinkle.getPref('confirmOnRollback') ||
(
Twinkle.getPref('confirmOnMobileRollback') &&
// Mobile user agent taken from [[en:MediaWiki:Gadget-confirmationRollback-mobile.js]]
/Android|webOS|iPhone|iPad|iPod|BlackBerry|Mobile|Opera Mini/i.test(navigator.userAgent)
)
) &&
!userHasAlreadyConfirmedAction;
if (needToDisplayConfirmation && !confirm('ページの差し戻し: よろしいですか?')) {
statelem.error('Aborted by user.');
return;
}
// Decide whether to notify the user on success
if (!Twinkle.rollback.skipTalk && Twinkle.getPref('openTalkPage').includes(params.type) &&
!params.userHidden && mw.config.get('wgUserName') !== params.user) {
params.notifyUser = true;
// Pass along to the warn module
params.vantimestamp = top.timestamp;
}
// figure out whether we need to/can review the edit
const flagged = page.flagged;
if ((Morebits.userIsInGroup('reviewer') || Morebits.userIsSysop) &&
!!flagged &&
flagged.stable_revid >= params.goodid &&
!!flagged.pending_since) {
params.reviewRevert = true;
params.csrftoken = csrftoken;
}
const query = {
action: 'edit',
title: params.pagename,
summary: summary + Twinkle.summaryAd,
token: csrftoken,
undo: lastrevid,
undoafter: params.goodid,
basetimestamp: touched,
starttimestamp: loadtimestamp,
minor: Twinkle.getPref('markRevertedPagesAsMinor').includes(params.type) ? true : undefined,
format: 'json'
};
// Handle watching, possible expiry
if (Twinkle.getPref('watchRevertedPages').includes(params.type)) {
const watchOrExpiry = Twinkle.getPref('watchRevertedExpiry');
if (!watchOrExpiry || watchOrExpiry === 'no') {
query.watchlist = 'nochange';
} else if (watchOrExpiry === 'default' || watchOrExpiry === 'preferences') {
query.watchlist = 'preferences';
} else {
query.watchlist = 'watch';
// number allowed but not used in Twinkle.config.watchlistEnums
if ((!page.watched || page.watchlistexpiry) && typeof watchOrExpiry === 'string' && watchOrExpiry !== 'yes') {
query.watchlistexpiry = watchOrExpiry;
}
}
}
if (!Twinkle.rollback.rollbackInPlace) {
Morebits.wiki.actionCompleted.redirect = params.pagename;
}
Morebits.wiki.actionCompleted.notice = '復元完了';
var wikipedia_api = new Morebits.wiki.Api('差し戻した内容を保存します', query, Twinkle.rollback.callbacks.complete, statelem);
wikipedia_api.params = params;
wikipedia_api.post();
},
complete: function(apiobj) {
// TODO Most of this is copy-pasted from Morebits.wiki.Page#fnSaveSuccess. Unify it
const response = apiobj.getResponse();
const edit = response.edit;
if (edit.captcha) {
apiobj.statelem.error('サーバーがCAPTCHAを入力するように要求したため、差し戻せませんでした。');
} else if (edit.nochange) {
apiobj.statelem.error('差し戻し先の版は、現在の版の内容と同じです。');
} else {
apiobj.statelem.info('完了');
var params = apiobj.params;
if (params.notifyUser && !params.userHidden) { // notifyUser only from main, not from toRevision
Morebits.Status.info('情報', ['利用者:', Morebits.htmlNode('strong', params.user), 'の会話ページ編集フォームを開きます']);
const url = mw.util.getUrl('User talk:' + params.user, {
action: 'edit',
preview: 'yes',
vanarticle: params.pagename.replace(/_/g, ' '),
vanarticlerevid: params.revid,
vantimestamp: params.vantimestamp,
vanarticlegoodrevid: params.goodid,
type: params.type,
count: params.count
});
switch (Twinkle.getPref('userTalkPageMode')) {
case 'tab':
window.open(url, '_blank');
break;
case 'blank':
window.open(url, '_blank',
'location=no,toolbar=no,status=no,directories=no,scrollbars=yes,width=1200,height=800');
break;
case 'window':
/* falls through */
default:
window.open(url,
window.name === 'twinklewarnwindow' ? '_blank' : 'twinklewarnwindow',
'location=no,toolbar=no,status=no,directories=no,scrollbars=yes,width=1200,height=800');
break;
}
// prefill Wel/ARV/Warn when rollback used on Special:Contributions page
} else if (Twinkle.rollback.rollbackInPlace &&
mw.config.get('wgCanonicalSpecialPageName') === 'Contributions') {
Twinkle.setPrefill('vanarticle', params.pagename.replace(/_/g, ' '));
Twinkle.setPrefill('vanarticlerevid', params.revid);
Twinkle.setPrefill('vantimestamp', params.vantimestamp);
Twinkle.setPrefill('vanarticlegoodrevid', params.goodid);
}
// review the revert, if needed
if (apiobj.params.reviewRevert) {
const query = {
action: 'review',
revid: edit.newrevid,
token: apiobj.params.csrftoken,
comment: 'Automatically reviewing reversion' + Twinkle.summaryAd // until the below
// 'tags': Twinkle.changeTags // flaggedrevs tag support: [[phab:T247721]]
};
const wikipedia_api = new Morebits.wiki.Api('Automatically accepting your changes', query);
wikipedia_api.post();
}
}
}
};
// If builtInString contains the string "$USER", it will be replaced
// by an appropriate user link if a user name is provided
Twinkle.rollback.formatSummary = function(builtInString, userName, customString) {
let result = builtInString;
// append user's custom reason
if (customString) {
result += ': ' + Morebits.string.toUpperCaseFirstChar(customString);
}
// find number of UTF-8 bytes the resulting string takes up, and possibly add
// a contributions or contributions+talk link if it doesn't push the edit summary
// over the 499-byte limit
if (/\$USER/.test(builtInString)) {
if (userName) {
const resultLen = unescape(encodeURIComponent(result.replace('$USER', ''))).length;
const contribsLink = '[[Special:Contributions/' + userName + '|' + userName + ']]';
const contribsLen = unescape(encodeURIComponent(contribsLink)).length;
if (resultLen + contribsLen <= 499) {
var talkLink = ' ([[User talk:' + userName + '|会話]])';
if (resultLen + contribsLen + unescape(encodeURIComponent(talkLink)).length <= 499) {
result = Morebits.string.safeReplace(result, '$USER', contribsLink + talkLink);
} else {
result = Morebits.string.safeReplace(result, '$USER', contribsLink);
}
} else {
result = Morebits.string.safeReplace(result, '$USER', userName);
}
} else {
result = Morebits.string.safeReplace(result, '$USER', Twinkle.rollback.hiddenName);
}
}
return result;
};
// Keep the public name used by existing jawiki add-on scripts.
Twinkle.fluff = Twinkle.rollback;
Twinkle.addInitCallback(Twinkle.rollback, 'rollback');
}());
// </nowiki>
l9uvfglbc23eobyloerymqxtdzpywrp
769568
769531
2026-10-22T02:22:42Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at d9cd3461baca43ac99ac2c24fb76a3aae6ddada9: minify
769568
javascript
text/javascript
// <nowiki>
(function(){Twinkle.rollback=function twinklerollback(){if(mw.config.get("wgIsProbablyEditable")){if(mw.config.get("wgAction")==="view"&&mw.config.get("wgRevisionId")&&mw.config.get("wgCurRevisionId")!==mw.config.get("wgRevisionId")){Twinkle.rollback.addLinks.oldid()}else if(mw.config.get("wgAction")==="history"&&mw.config.get("wgArticleId")){Twinkle.rollback.addLinks.history()}}else if(mw.config.get("wgNamespaceNumber")===-1){Twinkle.rollback.skipTalk=!Twinkle.getPref("openTalkPageOnAutoRevert");Twinkle.rollback.rollbackInPlace=Twinkle.getPref("rollbackInPlace");switch(mw.config.get("wgCanonicalSpecialPageName")){case"Contributions":case"IPContributions":Twinkle.rollback.addLinks.contributions();break;case"Recentchanges":case"Recentchangeslinked":mw.hook("wikipage.content").add($context=>{if(!$context||!$context.is("div")){return}Twinkle.rollback.addLinks.recentchanges($context)});break}}mw.hook("wikipage.diff").add($context=>{if(!$context){return}if(mw.config.get("wgIsProbablyEditable")&&(mw.config.get("wgDiffNewId")||mw.config.get("wgDiffOldId"))){Twinkle.rollback.addLinks.diff($context)}})};Twinkle.rollback.trustedBots=["AnomieBOT","SineBot","MajavahBot"];Twinkle.rollback.skipTalk=null;Twinkle.rollback.rollbackInPlace=null;Twinkle.rollback.hiddenName="不明な利用者";Twinkle.rollback.linkBuilder={spanTag:function(color,content){const span=document.createElement("span");span.style.color=color;span.appendChild(document.createTextNode(content));return span},buildLink:function(color,text){const link=document.createElement("a");link.appendChild(Twinkle.rollback.linkBuilder.spanTag("Black","["));link.appendChild(Twinkle.rollback.linkBuilder.spanTag(color,text));link.appendChild(Twinkle.rollback.linkBuilder.spanTag("Black","]"));link.href="#";return link},rollbackLinks:function(vandal,inline,rev,page){vandal=vandal||null;const elem=inline?"span":"div";const revNode=document.createElement(elem);rev=parseInt(rev,10);if(rev){revNode.setAttribute("id","tw-revert"+rev)}else{revNode.setAttribute("id","tw-revert")}const separator=inline?" ":" || ";const sepNode1=document.createElement("span");const sepText=document.createTextNode(separator);sepNode1.setAttribute("class","tw-rollback-link-separator");sepNode1.appendChild(sepText);const sepNode2=sepNode1.cloneNode(true);const normNode=document.createElement("span");const vandNode=document.createElement("span");var normLink=Twinkle.rollback.linkBuilder.buildLink("SteelBlue","差し戻し");var vandLink=Twinkle.rollback.linkBuilder.buildLink("Red","荒らし");normLink.style.fontWeight="bold";vandLink.style.fontWeight="bold";$(normLink).on("click",e=>{e.preventDefault();Twinkle.rollback.revert("norm",vandal,rev,page);Twinkle.rollback.disableLinks(revNode)});$(vandLink).on("click",e=>{e.preventDefault();Twinkle.rollback.revert("vand",vandal,rev,page);Twinkle.rollback.disableLinks(revNode)});normNode.setAttribute("class","tw-rollback-link-normal");vandNode.setAttribute("class","tw-rollback-link-vandalism");vandNode.appendChild(sepNode2);normNode.appendChild(normLink);vandNode.appendChild(vandLink);revNode.appendChild(normNode);revNode.appendChild(vandNode);return revNode},restoreThisRevisionLink:function(revisionRef,inline){revisionRef=typeof revisionRef==="number"?revisionRef:mw.config.get(revisionRef);const elem=inline?"span":"div";const revertToRevisionNode=document.createElement(elem);revertToRevisionNode.setAttribute("id","tw-revert-to-"+revisionRef);revertToRevisionNode.style.fontWeight="bold";var revertToRevisionLink=Twinkle.rollback.linkBuilder.buildLink("SaddleBrown","この版を復元");$(revertToRevisionLink).on("click",e=>{e.preventDefault();Twinkle.rollback.revertToRevision(revisionRef)});if(inline){revertToRevisionNode.appendChild(document.createTextNode(" "))}revertToRevisionNode.appendChild(revertToRevisionLink);return revertToRevisionNode}};Twinkle.rollback.addLinks={contributions:function(){const isRange=!!$("#sp-contributions-footer-anon-range")[0];if(mw.config.exists("wgRelevantUserName")||isRange){let username=mw.config.get("wgRelevantUserName");if(Twinkle.getPref("showRollbackLinks").includes("contribs")||mw.config.get("wgUserName")!==username&&Twinkle.getPref("showRollbackLinks").includes("others")||mw.config.get("wgUserName")===username&&Twinkle.getPref("showRollbackLinks").includes("mine")){const $list=$("#mw-content-text").find("ul li:has(span.mw-uctop):has(.mw-changeslist-diff)");$list.each((key,current)=>{const page=$(current).find(".mw-contributions-title").text();if(isRange){username=$(current).find(".mw-userlink:not(.history-deleted)").text()}current.appendChild(Twinkle.rollback.linkBuilder.rollbackLinks(username,true,current.dataset.mwRevid,page))})}}},recentchanges:function($context){if(Twinkle.getPref("showRollbackLinks").includes("recent")){const selector=".mw-changeslist-last.mw-changeslist-src-mw-edit";let $list=$context.hasClass("mw-changeslist")?$context.find(selector):$context.find(".mw-changeslist "+selector);if(!$list.length){return}$list=$list.not(".mw-rcfilters-ui-highlights-enhanced-toplevel").find(".mw-changeslist-line-inner, td.mw-enhanced-rc-nested");$list.each((key,current)=>{const vandal=$(current).find(".mw-userlink:not(.history-deleted)").text();const href=$(current).find(".mw-changeslist-diff").attr("href");const rev=mw.util.getParamValue("diff",href);const page=current.dataset.targetPage;current.appendChild(Twinkle.rollback.linkBuilder.rollbackLinks(vandal,true,rev,page))})}},history:function(){if(Twinkle.getPref("showRollbackLinks").includes("history")){const histList=$("#pagehistory li").toArray();if(!$("a.mw-firstlink").length){const firstRow=histList.shift();const firstUser=$(firstRow).find(".mw-userlink:not(.history-deleted)").text();for(let i=0;i<histList.length;i++){const hasMoreThanOneUser=$(histList[i]).find(".mw-userlink").text()!==firstUser;if(hasMoreThanOneUser){firstRow.appendChild(Twinkle.rollback.linkBuilder.rollbackLinks(firstUser,true));break}}}histList.forEach(rev=>{const href=rev.querySelector(".mw-changeslist-date").href;const oldid=parseInt(mw.util.getParamValue("oldid",href),10);if(!isNaN(oldid)){rev.appendChild(Twinkle.rollback.linkBuilder.restoreThisRevisionLink(oldid,true))}})}},diff:function($context){const warnFromTalk=function(xtitle){const $talkLink=$context.find("#mw-diff-"+xtitle+"2 .mw-usertoollinks a").first();if($talkLink.length){let extraParams="vanarticle="+mw.util.rawurlencode(Morebits.pageNameNorm)+"&noautowarn=true";extraParams+="&vanarticlerevid=";extraParams+=xtitle==="otitle"?mw.config.get("wgDiffOldId"):mw.config.get("wgDiffNewId");const href=$talkLink.attr("href");if(!href.includes("?")){$talkLink.attr("href",href+"?"+extraParams)}else{$talkLink.attr("href",href+"&"+extraParams)}}};warnFromTalk("otitle");if(mw.config.get("wgDiffOldId")&&mw.config.get("wgDiffOldId")!==mw.config.get("wgDiffNewId")){const oldTitle=$context.find("#mw-diff-otitle1").parent().get(0);if(oldTitle){oldTitle.insertBefore(Twinkle.rollback.linkBuilder.restoreThisRevisionLink("wgDiffOldId"),oldTitle.firstChild)}}warnFromTalk("ntitle");if($context.find("#differences-nextlink").length){const newTitle=$context.find("#mw-diff-ntitle1").parent().get(0);if(newTitle){newTitle.insertBefore(Twinkle.rollback.linkBuilder.restoreThisRevisionLink("wgDiffNewId"),newTitle.firstChild)}}else if(Twinkle.getPref("showRollbackLinks").includes("diff")&&mw.config.get("wgDiffOldId")&&(mw.config.get("wgDiffOldId")!==mw.config.get("wgDiffNewId")||$context.find("#differences-prevlink").length)){let vandal=$context.find("#mw-diff-ntitle2").find(".mw-userlink")[0];vandal=vandal?vandal.text:"";const ntitle=$context.find("#mw-diff-ntitle1").parent().get(0);if(ntitle){ntitle.insertBefore(Twinkle.rollback.linkBuilder.rollbackLinks(vandal),ntitle.firstChild)}}},oldid:function(){const revisionInfo=document.getElementById("mw-revision-info");if(revisionInfo){const title=revisionInfo.parentNode;title.insertBefore(Twinkle.rollback.linkBuilder.restoreThisRevisionLink("wgRevisionId"),title.firstChild)}}};Twinkle.rollback.disableLinks=function disablelinks(parentNode){$(parentNode).children().each((_ix,node)=>{node.innerHTML=node.textContent;$(node).css("font-weight","normal").css("color","darkgray")})};Twinkle.rollback.revert=function revertPage(type,vandal,rev,page){if(mw.util.isIPv6Address(vandal)){vandal=Morebits.ip.sanitizeIPv6(vandal)}const pagename=page||mw.config.get("wgPageName");const revid=rev||mw.config.get("wgCurRevisionId");if(Twinkle.rollback.rollbackInPlace){const notifyStatus=document.createElement("span");mw.notify(notifyStatus,{autoHide:false,title:page+"の差し戻し",tag:"twinklerollback_"+rev});Morebits.Status.init(notifyStatus)}else{Morebits.Status.init(document.getElementById("mw-content-text"));$("#catlinks").remove()}const params={type:type,user:vandal,userHidden:!vandal,pagename:pagename,revid:revid};const query={action:"query",prop:["info","revisions","flagged"],titles:pagename,inprop:"watched",intestactions:"edit",rvlimit:Twinkle.getPref("revertMaxRevisions"),rvprop:["ids","timestamp","user"],curtimestamp:"",meta:"tokens",type:"csrf",format:"json"};var wikipedia_api=new Morebits.wiki.Api("以前の版のデータを取得",query,Twinkle.rollback.callbacks.main);wikipedia_api.params=params;wikipedia_api.post()};Twinkle.rollback.revertToRevision=function revertToRevision(oldrev){Morebits.Status.init(document.getElementById("mw-content-text"));const query={action:"query",prop:["info","revisions"],titles:mw.config.get("wgPageName"),inprop:"watched",rvlimit:1,rvstartid:oldrev,rvprop:["ids","user"],curtimestamp:"",meta:"tokens",type:"csrf",format:"json"};var wikipedia_api=new Morebits.wiki.Api("以前の版のデータを取得",query,Twinkle.rollback.callbacks.toRevision);wikipedia_api.params={rev:oldrev};wikipedia_api.post()};Twinkle.rollback.callbacks={toRevision:function(apiobj){const response=apiobj.getResponse();const loadtimestamp=response.curtimestamp;const csrftoken=response.query.tokens.csrftoken;const page=response.query.pages[0];const lastrevid=parseInt(page.lastrevid,10);const touched=page.touched;const rev=page.revisions[0];const revertToRevID=parseInt(rev.revid,10);const revertToUser=rev.user;const revertToUserHidden=!!rev.userhidden;if(revertToRevID!==apiobj.params.rev){apiobj.statelem.error("取得した版が要求された版と一致しませんでした。差し戻しを中止します。");return}var optional_summary=prompt("差し戻しの理由を入力してください: ","");if(optional_summary===null){apiobj.statelem.error("利用者により中止。");return}var summary=Twinkle.rollback.formatSummary("$USERさんによる ID:"+revertToRevID+"の版を復元",revertToUserHidden?null:revertToUser,optional_summary);const query={action:"edit",title:mw.config.get("wgPageName"),summary:summary+Twinkle.summaryAd,token:csrftoken,undo:lastrevid,undoafter:revertToRevID,basetimestamp:touched,starttimestamp:loadtimestamp,minor:Twinkle.getPref("markRevertedPagesAsMinor").includes("torev")?true:undefined,format:"json"};if(Twinkle.getPref("watchRevertedPages").includes("torev")){const watchOrExpiry=Twinkle.getPref("watchRevertedExpiry");if(!watchOrExpiry||watchOrExpiry==="no"){query.watchlist="nochange"}else if(watchOrExpiry==="default"||watchOrExpiry==="preferences"){query.watchlist="preferences"}else{query.watchlist="watch";if((!page.watched||page.watchlistexpiry)&&typeof watchOrExpiry==="string"&&watchOrExpiry!=="yes"){query.watchlistexpiry=watchOrExpiry}}}Morebits.wiki.actionCompleted.redirect=mw.config.get("wgPageName");Morebits.wiki.actionCompleted.notice="Reversion completed";var wikipedia_api=new Morebits.wiki.Api("差し戻した内容を保存しています",query,Twinkle.rollback.callbacks.complete,apiobj.statelem);wikipedia_api.params=apiobj.params;wikipedia_api.post()},main:function(apiobj){const response=apiobj.getResponse();const loadtimestamp=response.curtimestamp;const csrftoken=response.query.tokens.csrftoken;const page=response.query.pages[0];if(!page.actions.edit){apiobj.statelem.error("ページを編集できませんでした。おそらく保護されています。");return}const lastrevid=parseInt(page.lastrevid,10);const touched=page.touched;const revs=page.revisions;const statelem=apiobj.statelem;const params=apiobj.params;if(revs.length<1){statelem.error("版の数が1版以下であるため、差し戻すことは不可能です。");return}const top=revs[0];const lastuser=top.user;if(lastrevid<params.revid){Morebits.Status.error("Error",["サーバーから取得した最新の版ID(",Morebits.htmlNode("strong",lastrevid),")が表示されている版のIDより小さいです。現在の版が削除されたか、サーバーの遅延、または誤ったデータを取得した可能性があります。差し戻しを停止します。"]);return}let userNorm=params.user||Twinkle.rollback.hiddenName;let index=1;if(params.revid!==lastrevid){Morebits.Status.warn("Warning",["最新版 ",Morebits.htmlNode("strong",lastrevid)," は現在の版 ",Morebits.htmlNode("strong",params.revid)," と一致しません。"]);if(lastuser===params.user||mw.util.isIPv6Address(params.user)&&Morebits.ip.get64(lastuser)===Morebits.ip.get64(params.user)){switch(params.type){case"vand":var diffUser=lastuser!==params.user;Morebits.Status.info("情報",["最新版"+(diffUser?"は同じ/64サブネット上の":"も"),Morebits.htmlNode("strong",userNorm),"さんによる編集です。破壊行為とみなして、差し戻しを進めます。"]);break;default:Morebits.Status.warn("Notice",["最新版は",Morebits.htmlNode("strong",userNorm),"さんによるものですが、差し戻しを停止します。"]);return}}else if(params.type==="vand"&&Twinkle.rollback.trustedBots.includes(top.user)&&revs.length>1&&revs[1].revid===params.revid){Morebits.Status.info("情報",["Latest revision was made by ",Morebits.htmlNode("strong",lastuser),", a trusted bot, and the revision before was made by our vandal, so we will proceed with the revert."]);index=2}else{Morebits.Status.error("Error",["最新版は",Morebits.htmlNode("strong",lastuser),"さんによるものであるため既に差し戻されたかもしれません。差し戻しを停止します。"]);return}}else{params.user=lastuser;userNorm=params.user||Twinkle.rollback.hiddenName}if(Twinkle.rollback.trustedBots.includes(params.user)){switch(params.type){case"vand":Morebits.Status.info("情報",["Vandalism revert was chosen on ",Morebits.htmlNode("strong",userNorm),". As this is a trusted bot, we assume you wanted to revert vandalism made by the previous user instead."]);index=2;params.user=revs[1].user;params.userHidden=!!revs[1].userhidden;break;case"norm":default:var cont=confirm("Normal revert was chosen, but the most recent edit was made by a trusted bot ("+userNorm+"). Do you want to revert the revision before instead?");if(cont){Morebits.Status.info("情報",["Normal revert was chosen on ",Morebits.htmlNode("strong",userNorm),". This is a trusted bot, and per confirmation, we'll revert the previous revision instead."]);index=2;params.user=revs[1].user;params.userHidden=!!revs[1].userhidden;userNorm=params.user||Twinkle.rollback.hiddenName}else{Morebits.Status.warn("Notice",["Normal revert was chosen on ",Morebits.htmlNode("strong",userNorm),". This is a trusted bot, but per confirmation, revert on selected revision will proceed."])}break}}let found=false;let count=0;let seen64=false;for(let i=index;i<revs.length;++i){++count;if(revs[i].user!==params.user){if(mw.util.isIPv6Address(revs[i].user)&&Morebits.ip.get64(revs[i].user)===Morebits.ip.get64(params.user)){if(!seen64){new Morebits.Status("Note","連続してる同じ/64サブネット内のIPv6アドレスは同じ利用者として扱います。");seen64=true}continue}found=i;break}}if(!found){statelem.error(["No previous revision found. Perhaps ",Morebits.htmlNode("strong",userNorm)," is the only contributor, or they have made more than "+mw.language.convertNumber(Twinkle.getPref("revertMaxRevisions"))+" edits in a row."]);statelem.error(["以前の版が見つかりませんでした。もしかすると",Morebits.htmlNode("strong",userNorm),"さんが唯一の投稿者であるか、"+mw.language.convertNumber(Twinkle.getPref("revertMaxRevisions"))+"回以上の編集を連続して行ったかもしれません。"]);return}if(!count){Morebits.Status.error("Error","0版を差し戻すことはできませんので、差し戻しを中止します。編集が既に差し戻されているにもかかわらず、版IDが同じままになっている可能性があります。");return}const good_revision=revs[found];let userHasAlreadyConfirmedAction=false;if(params.type!=="vand"&&count>1){if(!confirm(userNorm+"さんは"+mw.language.convertNumber(count)+"回以上の編集を連続して行いました。本当にすべて差し戻しますか?")){Morebits.Status.info("通知","差し戻しを中止");return}userHasAlreadyConfirmedAction=true}params.count=count;params.goodid=good_revision.revid;params.gooduser=good_revision.user;params.gooduserHidden=!!good_revision.userhidden;statelem.status([" revision ",Morebits.htmlNode("strong",params.goodid)," that was made ",Morebits.htmlNode("strong",mw.language.convertNumber(count))," revisions ago by ",Morebits.htmlNode("strong",params.gooduserHidden?Twinkle.rollback.hiddenName:params.gooduser)]);let summary,extra_summary;switch(params.type){case"vand":summary=Twinkle.getPref("SummaryOnVandalRevert");break;case"norm":default:if(Twinkle.getPref("offerReasonOnNormalRevert")){extra_summary=prompt("編集要約用の追加コメント: ","");if(extra_summary===null){statelem.error("利用者により中止。");return}userHasAlreadyConfirmedAction=true}summary=Twinkle.rollback.formatSummary("$USERさんによる"+params.count+"版を差し戻し",params.userHidden?null:params.user,extra_summary);break}const needToDisplayConfirmation=(Twinkle.getPref("confirmOnRollback")||Twinkle.getPref("confirmOnMobileRollback")&&/Android|webOS|iPhone|iPad|iPod|BlackBerry|Mobile|Opera Mini/i.test(navigator.userAgent))&&!userHasAlreadyConfirmedAction;if(needToDisplayConfirmation&&!confirm("ページの差し戻し: よろしいですか?")){statelem.error("Aborted by user.");return}if(!Twinkle.rollback.skipTalk&&Twinkle.getPref("openTalkPage").includes(params.type)&&!params.userHidden&&mw.config.get("wgUserName")!==params.user){params.notifyUser=true;params.vantimestamp=top.timestamp}const flagged=page.flagged;if((Morebits.userIsInGroup("reviewer")||Morebits.userIsSysop)&&!!flagged&&flagged.stable_revid>=params.goodid&&!!flagged.pending_since){params.reviewRevert=true;params.csrftoken=csrftoken}const query={action:"edit",title:params.pagename,summary:summary+Twinkle.summaryAd,token:csrftoken,undo:lastrevid,undoafter:params.goodid,basetimestamp:touched,starttimestamp:loadtimestamp,minor:Twinkle.getPref("markRevertedPagesAsMinor").includes(params.type)?true:undefined,format:"json"};if(Twinkle.getPref("watchRevertedPages").includes(params.type)){const watchOrExpiry=Twinkle.getPref("watchRevertedExpiry");if(!watchOrExpiry||watchOrExpiry==="no"){query.watchlist="nochange"}else if(watchOrExpiry==="default"||watchOrExpiry==="preferences"){query.watchlist="preferences"}else{query.watchlist="watch";if((!page.watched||page.watchlistexpiry)&&typeof watchOrExpiry==="string"&&watchOrExpiry!=="yes"){query.watchlistexpiry=watchOrExpiry}}}if(!Twinkle.rollback.rollbackInPlace){Morebits.wiki.actionCompleted.redirect=params.pagename}Morebits.wiki.actionCompleted.notice="復元完了";var wikipedia_api=new Morebits.wiki.Api("差し戻した内容を保存します",query,Twinkle.rollback.callbacks.complete,statelem);wikipedia_api.params=params;wikipedia_api.post()},complete:function(apiobj){const response=apiobj.getResponse();const edit=response.edit;if(edit.captcha){apiobj.statelem.error("サーバーがCAPTCHAを入力するように要求したため、差し戻せませんでした。")}else if(edit.nochange){apiobj.statelem.error("差し戻し先の版は、現在の版の内容と同じです。")}else{apiobj.statelem.info("完了");var params=apiobj.params;if(params.notifyUser&&!params.userHidden){Morebits.Status.info("情報",["利用者:",Morebits.htmlNode("strong",params.user),"の会話ページ編集フォームを開きます"]);const url=mw.util.getUrl("User talk:"+params.user,{action:"edit",preview:"yes",vanarticle:params.pagename.replace(/_/g," "),vanarticlerevid:params.revid,vantimestamp:params.vantimestamp,vanarticlegoodrevid:params.goodid,type:params.type,count:params.count});switch(Twinkle.getPref("userTalkPageMode")){case"tab":window.open(url,"_blank");break;case"blank":window.open(url,"_blank","location=no,toolbar=no,status=no,directories=no,scrollbars=yes,width=1200,height=800");break;case"window":default:window.open(url,window.name==="twinklewarnwindow"?"_blank":"twinklewarnwindow","location=no,toolbar=no,status=no,directories=no,scrollbars=yes,width=1200,height=800");break}}else if(Twinkle.rollback.rollbackInPlace&&mw.config.get("wgCanonicalSpecialPageName")==="Contributions"){Twinkle.setPrefill("vanarticle",params.pagename.replace(/_/g," "));Twinkle.setPrefill("vanarticlerevid",params.revid);Twinkle.setPrefill("vantimestamp",params.vantimestamp);Twinkle.setPrefill("vanarticlegoodrevid",params.goodid)}if(apiobj.params.reviewRevert){const query={action:"review",revid:edit.newrevid,token:apiobj.params.csrftoken,comment:"Automatically reviewing reversion"+Twinkle.summaryAd};const wikipedia_api=new Morebits.wiki.Api("Automatically accepting your changes",query);wikipedia_api.post()}}}};Twinkle.rollback.formatSummary=function(builtInString,userName,customString){let result=builtInString;if(customString){result+=": "+Morebits.string.toUpperCaseFirstChar(customString)}if(/\$USER/.test(builtInString)){if(userName){const resultLen=unescape(encodeURIComponent(result.replace("$USER",""))).length;const contribsLink="[[Special:Contributions/"+userName+"|"+userName+"]]";const contribsLen=unescape(encodeURIComponent(contribsLink)).length;if(resultLen+contribsLen<=499){var talkLink=" ([[User talk:"+userName+"|会話]])";if(resultLen+contribsLen+unescape(encodeURIComponent(talkLink)).length<=499){result=Morebits.string.safeReplace(result,"$USER",contribsLink+talkLink)}else{result=Morebits.string.safeReplace(result,"$USER",contribsLink)}}else{result=Morebits.string.safeReplace(result,"$USER",userName)}}else{result=Morebits.string.safeReplace(result,"$USER",Twinkle.rollback.hiddenName)}}return result};Twinkle.fluff=Twinkle.rollback;Twinkle.addInitCallback(Twinkle.rollback,"rollback")})();
// </nowiki>
4pdv2pquj3qcl9jy7zmt1t8mwft0svg
User:Syunsyunminmin/Twinkle/twinklewarn.js
2
179400
769465
2026-10-21T13:58:07Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 3680108:
769465
javascript
text/javascript
/*
Twinkle: adapted for Japanese Wikipedia; upstream authors and Japanese adaptations are credited in UPSTREAM.md.
Upstream source: https://github.com/wikimedia-gadgets/twinkle/blob/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/src/modules/twinklewarn.js
Upstream author history: https://github.com/wikimedia-gadgets/twinkle/commits/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/src/modules/twinklewarn.js
License: CC BY-SA 3.0 Unported. See https://github.com/wikimedia-gadgets/twinkle/blob/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/LICENSE.txt
Japanese adaptation: Syunsyunminmin; includes later synchronization changes.
*/
// <nowiki>
(function() {
/*
****************************************
*** twinklewarn.js: Warn module
****************************************
* Mode of invocation: Tab ("Warn")
* Active on: Any page with relevant user name (userspace, contribs,
* etc.) (not IP ranges), as well as the rollback success page
*/
Twinkle.warn = function twinklewarn() {
// Users and IPs but not IP ranges
if (mw.config.exists('wgRelevantUserName') && !Morebits.ip.isRange(mw.config.get('wgRelevantUserName'))) {
Twinkle.addPortletLink(Twinkle.warn.callback, 'Warn', 'tw-warn', 'Warn/notify user');
if (Twinkle.getPref('autoMenuAfterRollback') &&
mw.config.get('wgNamespaceNumber') === 3 &&
Twinkle.getPrefill('vanarticle') &&
!Twinkle.getPrefill('twinklewelcome') &&
!Twinkle.getPrefill('noautowarn')) {
Twinkle.warn.callback();
}
}
// Modify URL of talk page on rollback success pages, makes use of a
// custom message box in [[MediaWiki:Rollback-success]]
if (mw.config.get('wgAction') === 'rollback') {
const $vandalTalkLink = $('#mw-rollback-success').find('.mw-usertoollinks a').first();
if ($vandalTalkLink.length) {
$vandalTalkLink.css('font-weight', 'bold');
$vandalTalkLink.wrapInner($('<span>').attr('title', 'If appropriate, you can use Twinkle to warn the user about their edits to this page.'));
// Can't provide vanarticlerevid as only wgCurRevisionId is provided
const extraParam = 'vanarticle=' + mw.util.rawurlencode(Morebits.pageNameNorm);
const href = $vandalTalkLink.attr('href');
if (!href.includes('?')) {
$vandalTalkLink.attr('href', href + '?' + extraParam);
} else {
$vandalTalkLink.attr('href', href + '&' + extraParam);
}
}
}
};
// Used to close window when switching to ARV in autolevel
Twinkle.warn.dialog = null;
Twinkle.warn.callback = function twinklewarnCallback() {
if (mw.config.get('wgRelevantUserName') === mw.config.get('wgUserName') &&
!confirm('You are about to warn yourself! Are you sure you want to proceed?')) {
return;
}
Twinkle.warn.dialog = new Morebits.SimpleWindow(600, 440);
const dialog = Twinkle.warn.dialog;
dialog.setTitle('Warn/notify user');
dialog.setScriptName('Twinkle');
dialog.addFooterLink('Choosing a warning level', 'WP:UWUL#Levels');
dialog.addFooterLink('Warn prefs', 'WP:TW/PREF#warn');
dialog.addFooterLink('Twinkle help', 'WP:TW/DOC#warn');
dialog.addFooterLink('Give feedback', 'WT:TW');
const form = new Morebits.QuickForm(Twinkle.warn.callback.evaluate);
const main_select = form.append({
type: 'field',
label: 'Choose type of warning/notice to issue',
tooltip: 'First choose a main warning group, then the specific warning to issue.'
});
const main_group = main_select.append({
type: 'select',
name: 'main_group',
tooltip: 'You can customize the default selection in your Twinkle preferences',
event: Twinkle.warn.callback.change_category
});
const defaultGroup = parseInt(Twinkle.getPref('defaultWarningGroup'), 10);
main_group.append({ type: 'option', label: 'Auto-select level (1-4)', value: 'autolevel', selected: defaultGroup === 11 });
main_group.append({ type: 'option', label: '1: General note', value: 'level1', selected: defaultGroup === 1 });
main_group.append({ type: 'option', label: '2: Caution', value: 'level2', selected: defaultGroup === 2 });
main_group.append({ type: 'option', label: '3: Warning', value: 'level3', selected: defaultGroup === 3 });
main_group.append({ type: 'option', label: '4: Final warning', value: 'level4', selected: defaultGroup === 4 });
main_group.append({ type: 'option', label: '4im: Only warning', value: 'level4im', selected: defaultGroup === 5 });
if (Twinkle.getPref('combinedSingletMenus')) {
main_group.append({ type: 'option', label: 'Single-issue messages', value: 'singlecombined', selected: defaultGroup === 6 || defaultGroup === 7 });
} else {
main_group.append({ type: 'option', label: 'Single-issue notices', value: 'singlenotice', selected: defaultGroup === 6 });
main_group.append({ type: 'option', label: 'Single-issue warnings', value: 'singlewarn', selected: defaultGroup === 7 });
}
if (Twinkle.getPref('customWarningList').length) {
main_group.append({ type: 'option', label: 'Custom warnings', value: 'custom', selected: defaultGroup === 9 });
}
main_group.append({ type: 'option', label: 'All warning templates', value: 'kitchensink', selected: defaultGroup === 10 });
main_select.append({ type: 'select', name: 'sub_group', event: Twinkle.warn.callback.change_subcategory }); // Will be empty to begin with.
form.append({
type: 'input',
name: 'article',
label: 'Linked page',
value: Twinkle.getPrefill('vanarticle') || '',
tooltip: 'A page can be linked within the notice, perhaps because it was a revert to said page that dispatched this notice. Leave empty for no page to be linked.'
});
form.append({
type: 'div',
label: '',
style: 'color: red',
id: 'twinkle-warn-warning-messages'
});
const more = form.append({ type: 'field', name: 'reasonGroup', label: 'Warning information' });
more.append({ type: 'textarea', label: 'Optional message:', name: 'reason', tooltip: 'Perhaps a reason, or that a more detailed notice must be appended' });
const previewlink = document.createElement('a');
$(previewlink).on('click', () => {
Twinkle.warn.callbacks.preview(result); // |result| is defined below
});
previewlink.style.cursor = 'pointer';
previewlink.textContent = 'Preview';
more.append({ type: 'div', id: 'warningpreview', label: [previewlink] });
more.append({ type: 'div', id: 'twinklewarn-previewbox', style: 'display: none' });
more.append({ type: 'submit', label: 'Submit' });
var result = form.render();
dialog.setContent(result);
dialog.display();
result.main_group.root = result;
result.previewer = new Morebits.wiki.Preview($(result).find('div#twinklewarn-previewbox').last()[0]);
// Potential notices for staleness and missed reverts
const vanrevid = Twinkle.getPrefill('vanarticlerevid');
if (vanrevid) {
let message = '';
let query = {};
// If you tried reverting, check if *you* actually reverted
if (!Twinkle.getPrefill('noautowarn') && Twinkle.getPrefill('vanarticle')) { // Via rollback link
query = {
action: 'query',
titles: Twinkle.getPrefill('vanarticle'),
prop: 'revisions',
rvstartid: vanrevid,
rvlimit: 2,
rvdir: 'newer',
rvprop: 'user',
format: 'json'
};
new Morebits.wiki.Api('Checking if you successfully reverted the page', query, ((apiobj) => {
const rev = apiobj.getResponse().query.pages[0].revisions;
const revertUser = rev && rev[1].user;
if (revertUser && revertUser !== mw.config.get('wgUserName')) {
message += ' Someone else reverted the page and may have already warned the user.';
$('#twinkle-warn-warning-messages').text('Note:' + message);
}
})).post();
}
// Confirm edit wasn't too old for a warning
const checkStale = function(vantimestamp) {
const revDate = new Morebits.Date(vantimestamp);
if (vantimestamp && revDate.isValid()) {
if (revDate.add(24, 'hours').isBefore(new Date())) {
message += ' This edit was made more than 24 hours ago so a warning may be stale.';
$('#twinkle-warn-warning-messages').text('Note:' + message);
}
}
};
let vantimestamp = Twinkle.getPrefill('vantimestamp');
// If from a rollback module-based revert, no API lookup necessary
if (vantimestamp) {
checkStale(vantimestamp);
} else {
query = {
action: 'query',
prop: 'revisions',
rvprop: 'timestamp',
revids: vanrevid,
format: 'json'
};
new Morebits.wiki.Api('Grabbing the revision timestamps', query, ((apiobj) => {
const rev = apiobj.getResponse().query.pages[0].revisions;
vantimestamp = rev && rev[0].timestamp;
checkStale(vantimestamp);
})).post();
}
}
// We must init the first choice (General Note);
const evt = document.createEvent('Event');
evt.initEvent('change', true, true);
result.main_group.dispatchEvent(evt);
};
// This is all the messages that might be dispatched by the code
// Each of the individual templates require the following information:
// label (required): A short description displayed in the dialog
// summary (required): The edit summary used. If an article name is entered, the summary is postfixed with "on [[article]]", and it is always postfixed with "."
// suppressArticleInSummary (optional): Set to true to suppress showing the article name in the edit summary. Useful if the warning relates to attack pages, or some such.
// hideLinkedPage (optional): Set to true to hide the "Linked page" text box. Some warning templates do not have a linked article parameter.
// hideReason (optional): Set to true to hide the "Optional message" text box. Some warning templates do not have a reason parameter.
Twinkle.warn.messages = {
levels: {
'Common warnings': {
'uw-vandalism': {
level1: {
label: 'Vandalism',
summary: 'General note: Unconstructive editing'
},
level2: {
label: 'Vandalism',
summary: 'Caution: Unconstructive editing'
},
level3: {
label: 'Vandalism',
summary: 'Warning: Vandalism'
},
level4: {
label: 'Vandalism',
summary: 'Final warning: Vandalism'
},
level4im: {
label: 'Vandalism',
summary: 'Only warning: Vandalism'
}
},
'uw-disruptive': {
level1: {
label: 'Disruptive editing',
summary: 'General note: Unconstructive editing'
},
level2: {
label: 'Disruptive editing',
summary: 'Caution: Unconstructive editing'
},
level3: {
label: 'Disruptive editing',
summary: 'Warning: Disruptive editing'
}
},
'uw-test': {
level1: {
label: 'Editing tests',
summary: 'General note: Editing tests'
},
level2: {
label: 'Editing tests',
summary: 'Caution: Editing tests'
},
level3: {
label: 'Editing tests',
summary: 'Warning: Editing tests'
}
},
'uw-delete': {
level1: {
label: 'Removal of content, blanking',
summary: 'General note: Removal of content, blanking'
},
level2: {
label: 'Removal of content, blanking',
summary: 'Caution: Removal of content, blanking'
},
level3: {
label: 'Removal of content, blanking',
summary: 'Warning: Removal of content, blanking'
},
level4: {
label: 'Removal of content, blanking',
summary: 'Final warning: Removal of content, blanking'
},
level4im: {
label: 'Removal of content, blanking',
summary: 'Only warning: Removal of content, blanking'
}
},
'uw-generic': {
level4: {
label: 'Generic warning (for template series missing level 4)',
summary: 'Final warning notice'
}
}
},
'Behavior in articles': {
'uw-ai': {
level1: {
label: 'Using a large language model',
summary: 'General note: Using a large language model'
},
level2: {
label: 'Using a large language model',
summary: 'Caution: Using a large language model'
},
level3: {
label: 'Using a large language model',
summary: 'Warning: Using a large language model'
},
level4: {
label: 'Using a large language model',
summary: 'Final warning: Using a large language model'
}
},
'uw-biog': {
level1: {
label: 'Adding unreferenced controversial information about living persons',
summary: 'General note: Adding unreferenced controversial information about living persons'
},
level2: {
label: 'Adding unreferenced controversial information about living persons',
summary: 'Caution: Adding unreferenced controversial information about living persons'
},
level3: {
label: 'Adding unreferenced controversial/defamatory information about living persons',
summary: 'Warning: Adding unreferenced controversial information about living persons'
},
level4: {
label: 'Adding unreferenced defamatory information about living persons',
summary: 'Final warning: Adding unreferenced controversial information about living persons'
},
level4im: {
label: 'Adding unreferenced defamatory information about living persons',
summary: 'Only warning: Adding unreferenced controversial information about living persons'
}
},
'uw-defamatory': {
level1: {
label: 'Addition of defamatory content',
summary: 'General note: Addition of defamatory content'
},
level2: {
label: 'Addition of defamatory content',
summary: 'Caution: Addition of defamatory content'
},
level3: {
label: 'Addition of defamatory content',
summary: 'Warning: Addition of defamatory content'
},
level4: {
label: 'Addition of defamatory content',
summary: 'Final warning: Addition of defamatory content'
},
level4im: {
label: 'Addition of defamatory content',
summary: 'Only warning: Addition of defamatory content'
}
},
'uw-error': {
level1: {
label: 'Introducing deliberate factual errors',
summary: 'General note: Introducing factual errors'
},
level2: {
label: 'Introducing deliberate factual errors',
summary: 'Caution: Introducing factual errors'
},
level3: {
label: 'Introducing deliberate factual errors',
summary: 'Warning: Introducing deliberate factual errors'
},
level4: {
label: 'Introducing deliberate factual errors',
summary: 'Final warning: Introducing deliberate factual errors'
}
},
'uw-fringe': {
level1: {
label: 'Introducing fringe theories',
summary: 'General note: Introducing fringe theories'
},
level2: {
label: 'Introducing fringe theories',
summary: 'Caution: Introducing fringe theories'
},
level3: {
label: 'Introducing fringe theories',
summary: 'Warning: Introducing fringe theories'
}
},
'uw-genre': {
level1: {
label: 'Frequent or mass changes to genres without consensus or references',
summary: 'General note: Frequent or mass changes to genres without consensus or references'
},
level2: {
label: 'Frequent or mass changes to genres without consensus or references',
summary: 'Caution: Frequent or mass changes to genres without consensus or references'
},
level3: {
label: 'Frequent or mass changes to genres without consensus or reference',
summary: 'Warning: Frequent or mass changes to genres without consensus or reference'
},
level4: {
label: 'Frequent or mass changes to genres without consensus or reference',
summary: 'Final warning: Frequent or mass changes to genres without consensus or reference'
}
},
'uw-image': {
level1: {
label: 'Inappropriate images in articles',
summary: 'General note: Inappropriate images in articles'
},
level2: {
label: 'Image-related vandalism in articles',
summary: 'Caution: Image-related vandalism in articles'
},
level3: {
label: 'Image-related vandalism in articles',
summary: 'Warning: Image-related vandalism in articles'
},
level4: {
label: 'Image-related vandalism in articles',
summary: 'Final warning: Image-related vandalism in articles'
},
level4im: {
label: 'Image-related vandalism',
summary: 'Only warning: Image-related vandalism'
}
},
'uw-joke': {
level1: {
label: 'Using improper humor in articles',
summary: 'General note: Using improper humor in articles'
},
level2: {
label: 'Using improper humor in articles',
summary: 'Caution: Using improper humor in articles'
},
level3: {
label: 'Using improper humor in articles',
summary: 'Warning: Using improper humor in articles'
},
level4: {
label: 'Using improper humor in articles',
summary: 'Final warning: Using improper humor in articles'
},
level4im: {
label: 'Using improper humor',
summary: 'Only warning: Using improper humor'
}
},
'uw-nor': {
level1: {
label: 'Adding original research',
summary: 'General note: Adding original research'
},
level2: {
label: 'Adding original research',
summary: 'Caution: Adding original research'
},
level3: {
label: 'Adding original research',
summary: 'Warning: Adding original research'
},
level4: {
label: 'Adding original research',
summary: 'Final warning: Adding original research'
}
},
'uw-notcensored': {
level1: {
label: 'Censorship of material',
summary: 'General note: Censorship of material'
},
level2: {
label: 'Censorship of material',
summary: 'Caution: Censorship of material'
},
level3: {
label: 'Censorship of material',
summary: 'Warning: Censorship of material'
}
},
'uw-own': {
level1: {
label: 'Ownership of articles',
summary: 'General note: Ownership of articles'
},
level2: {
label: 'Ownership of articles',
summary: 'Caution: Ownership of articles'
},
level3: {
label: 'Ownership of articles',
summary: 'Warning: Ownership of articles'
},
level4: {
label: 'Ownership of articles',
summary: 'Final warning: Ownership of articles'
},
level4im: {
label: 'Ownership of articles',
summary: 'Only warning: Ownership of articles'
}
},
'uw-pronouns': {
level1: {
label: 'Introducing incorrect pronouns',
summary: 'General note: Introducing incorrect pronouns'
},
level2: {
label: 'Introducing incorrect pronouns',
summary: 'Caution: Introducing incorrect pronouns'
},
level3: {
label: 'Introducing incorrect pronouns',
summary: 'Warning: Introducing incorrect pronouns'
}
},
'uw-subtle': {
level1: {
label: 'Subtle vandalism',
summary: 'General note: Possible unconstructive editing'
},
level2: {
label: 'Subtle vandalism',
summary: 'Caution: Likely unconstructive editing'
},
level3: {
label: 'Subtle vandalism',
summary: 'Warning: Subtle vandalism'
},
level4: {
label: 'Subtle vandalism',
summary: 'Final warning: Subtle vandalism'
}
},
'uw-talkinarticle': {
level1: {
label: 'Adding commentary to an article',
summary: 'General note: Adding commentary to an article'
},
level2: {
label: 'Adding commentary to an article',
summary: 'Caution: Adding commentary to an article'
},
level3: {
label: 'Adding commentary to an article',
summary: 'Warning: Adding commentary to an article'
}
},
'uw-tdel': {
level1: {
label: 'Removal of maintenance templates',
summary: 'General note: Removal of maintenance templates'
},
level2: {
label: 'Removal of maintenance templates',
summary: 'Caution: Removal of maintenance templates'
},
level3: {
label: 'Removal of maintenance templates',
summary: 'Warning: Removal of maintenance templates'
},
level4: {
label: 'Removal of maintenance templates',
summary: 'Final warning: Removal of maintenance templates'
}
},
'uw-unsourced': {
level1: {
label: 'Addition of unsourced or improperly cited material',
summary: 'General note: Addition of unsourced or improperly cited material'
},
level2: {
label: 'Addition of unsourced or improperly cited material',
summary: 'Caution: Addition of unsourced or improperly cited material'
},
level3: {
label: 'Addition of unsourced or improperly cited material',
summary: 'Warning: Addition of unsourced or improperly cited material'
},
level4: {
label: 'Addition of unsourced or improperly cited material',
summary: 'Final warning: Addition of unsourced or improperly cited material'
}
}
},
'Promotions and spam': {
'uw-advert': {
level1: {
label: 'Using Wikipedia for advertising or promotion',
summary: 'General note: Using Wikipedia for advertising or promotion'
},
level2: {
label: 'Using Wikipedia for advertising or promotion',
summary: 'Caution: Using Wikipedia for advertising or promotion'
},
level3: {
label: 'Using Wikipedia for advertising or promotion',
summary: 'Warning: Using Wikipedia for advertising or promotion'
},
level4: {
label: 'Using Wikipedia for advertising or promotion',
summary: 'Final warning: Using Wikipedia for advertising or promotion'
},
level4im: {
label: 'Using Wikipedia for advertising or promotion',
summary: 'Only warning: Using Wikipedia for advertising or promotion'
}
},
'uw-npov': {
level1: {
label: 'Not adhering to neutral point of view',
summary: 'General note: Not adhering to neutral point of view'
},
level2: {
label: 'Not adhering to neutral point of view',
summary: 'Caution: Not adhering to neutral point of view'
},
level3: {
label: 'Not adhering to neutral point of view',
summary: 'Warning: Not adhering to neutral point of view'
},
level4: {
label: 'Not adhering to neutral point of view',
summary: 'Final warning: Not adhering to neutral point of view'
}
},
'uw-paid': {
level1: {
label: 'Paid editing without disclosure under the Wikimedia Terms of Use',
summary: 'General note: Disclosure requirements for paid editing under the Wikimedia Terms of Use'
},
level2: {
label: 'Paid editing without disclosure under the Wikimedia Terms of Use',
summary: 'Caution: Disclosure requirements for paid editing under the Wikimedia Terms of Use'
},
level3: {
label: 'Paid editing without disclosure under the Wikimedia Terms of Use',
summary: 'Warning: Disclosure requirements for paid editing under the Wikimedia Terms of Use'
},
level4: {
label: 'Paid editing without disclosure under the Wikimedia Terms of Use',
summary: 'Final warning: Disclosure requirements for paid editing under the Wikimedia Terms of Use'
}
},
'uw-spam': {
level1: {
label: 'Adding inappropriate external links',
summary: 'General note: Adding inappropriate external links'
},
level2: {
label: 'Adding spam links',
summary: 'Caution: Adding spam links'
},
level3: {
label: 'Adding spam links',
summary: 'Warning: Adding spam links'
},
level4: {
label: 'Adding spam links',
summary: 'Final warning: Adding spam links'
},
level4im: {
label: 'Adding spam links',
summary: 'Only warning: Adding spam links'
}
}
},
'Behavior towards other editors': {
'uw-agf': {
level1: {
label: 'Not assuming good faith',
summary: 'General note: Not assuming good faith'
},
level2: {
label: 'Not assuming good faith',
summary: 'Caution: Not assuming good faith'
},
level3: {
label: 'Not assuming good faith',
summary: 'Warning: Not assuming good faith'
}
},
'uw-aitalk': {
level1: {
label: 'Posting LLM-generated comments',
summary: 'General note: Posting LLM-generated comments'
},
level2: {
label: 'Posting LLM-generated comments',
summary: 'Caution: Posting LLM-generated comments'
},
level3: {
label: 'Posting LLM-generated comments',
summary: 'Warning: Posting LLM-generated comments'
},
level4: {
label: 'Posting LLM-generated comments',
summary: 'Final warning: Posting LLM-generated comments'
}
},
'uw-harass': {
level1: {
label: 'Harassment of other users',
summary: 'General note: Harassment of other users'
},
level2: {
label: 'Harassment of other users',
summary: 'Caution: Harassment of other users'
},
level3: {
label: 'Harassment of other users',
summary: 'Warning: Harassment of other users'
},
level4: {
label: 'Harassment of other users',
summary: 'Final warning: Harassment of other users'
},
level4im: {
label: 'Harassment of other users',
summary: 'Only warning: Harassment of other users'
}
},
'uw-npa': {
level1: {
label: 'Personal attack directed at a specific editor',
summary: 'General note: Personal attack directed at a specific editor'
},
level2: {
label: 'Personal attack directed at a specific editor',
summary: 'Caution: Personal attack directed at a specific editor'
},
level3: {
label: 'Personal attack directed at a specific editor',
summary: 'Warning: Personal attack directed at a specific editor'
},
level4: {
label: 'Personal attack directed at a specific editor',
summary: 'Final warning: Personal attack directed at a specific editor'
},
level4im: {
label: 'Personal attack directed at a specific editor',
summary: 'Only warning: Personal attack directed at a specific editor'
}
},
'uw-tempabuse': {
level1: {
label: 'Improper use of warning or blocking template',
summary: 'General note: Improper use of warning or blocking template'
},
level2: {
label: 'Improper use of warning or blocking template',
summary: 'Caution: Improper use of warning or blocking template'
}
}
},
'Removal of deletion tags': {
'uw-afd': {
level1: {
label: 'Removing {{afd}} templates',
summary: 'General note: Removing {{afd}} templates'
},
level2: {
label: 'Removing {{afd}} templates',
summary: 'Caution: Removing {{afd}} templates'
},
level3: {
label: 'Removing {{afd}} templates',
summary: 'Warning: Removing {{afd}} templates'
},
level4: {
label: 'Removing {{afd}} templates',
summary: 'Final warning: Removing {{afd}} templates'
}
},
'uw-blpprod': {
level1: {
label: 'Removing {{blp prod}} templates',
summary: 'General note: Removing {{blp prod}} templates'
},
level2: {
label: 'Removing {{blp prod}} templates',
summary: 'Caution: Removing {{blp prod}} templates'
},
level3: {
label: 'Removing {{blp prod}} templates',
summary: 'Warning: Removing {{blp prod}} templates'
},
level4: {
label: 'Removing {{blp prod}} templates',
summary: 'Final warning: Removing {{blp prod}} templates'
}
},
'uw-idt': {
level1: {
label: 'Removing file deletion tags',
summary: 'General note: Removing file deletion tags'
},
level2: {
label: 'Removing file deletion tags',
summary: 'Caution: Removing file deletion tags'
},
level3: {
label: 'Removing file deletion tags',
summary: 'Warning: Removing file deletion tags'
},
level4: {
label: 'Removing file deletion tags',
summary: 'Final warning: Removing file deletion tags'
}
},
'uw-rfd': {
level1: {
label: 'Removing redirects for discussion tags',
summary: 'General note: Removing redirects for discussion tags'
},
level2: {
label: 'Removing redirects for discussion tags',
summary: 'Caution: Removing redirects for discussion tags'
},
level3: {
label: 'Removing redirects for discussion tags',
summary: 'Warning: Removing redirects for discussion tags'
},
level4: {
label: 'Removing redirects for discussion tags',
summary: 'Final warning: Removing redirects for discussion tags'
}
},
'uw-speedy': {
level1: {
label: 'Removing speedy deletion tags',
summary: 'General note: Removing speedy deletion tags'
},
level2: {
label: 'Removing speedy deletion tags',
summary: 'Caution: Removing speedy deletion tags'
},
level3: {
label: 'Removing speedy deletion tags',
summary: 'Warning: Removing speedy deletion tags'
},
level4: {
label: 'Removing speedy deletion tags',
summary: 'Final warning: Removing speedy deletion tags'
}
},
'uw-tfd': {
level1: {
label: 'Removing {{tfd}} templates',
summary: 'General note: Removing {{tfd}} templates'
},
level2: {
label: 'Removing {{tfd}} templates',
summary: 'Caution: Removing {{tfd}} templates'
},
level3: {
label: 'Removing {{tfd}} templates',
summary: 'Warning: Removing {{tfd}} templates'
},
level4: {
label: 'Removing {{tfd}} templates',
summary: 'Final warning: Removing {{tfd}} templates'
}
}
},
Other: {
'uw-attempt': {
level1: {
label: 'Triggering the edit filter',
summary: 'General note: Triggering the edit filter'
},
level2: {
label: 'Triggering the edit filter',
summary: 'Caution: Triggering the edit filter'
},
level3: {
label: 'Triggering the edit filter',
summary: 'Warning: Triggering the edit filter'
},
level4: {
label: 'Triggering the edit filter',
summary: 'Final warning: Triggering the edit filter'
},
level4im: {
label: 'Triggering the edit filter',
summary: 'Only warning: Triggering the edit filter'
}
},
'uw-chat': {
level1: {
label: 'Using talk page as forum',
summary: 'General note: Using talk page as forum'
},
level2: {
label: 'Using talk page as forum',
summary: 'Caution: Using talk page as forum'
},
level3: {
label: 'Using talk page as forum',
summary: 'Warning: Using talk page as forum'
},
level4: {
label: 'Using talk page as forum',
summary: 'Final warning: Using talk page as forum'
}
},
'uw-create': {
level1: {
label: 'Creating inappropriate pages',
summary: 'General note: Creating inappropriate pages'
},
level2: {
label: 'Creating inappropriate pages',
summary: 'Caution: Creating inappropriate pages'
},
level3: {
label: 'Creating inappropriate pages',
summary: 'Warning: Creating inappropriate pages'
},
level4: {
label: 'Creating inappropriate pages',
summary: 'Final warning: Creating inappropriate pages'
},
level4im: {
label: 'Creating inappropriate pages',
summary: 'Only warning: Creating inappropriate pages'
}
},
'uw-fv': {
level1: {
label: 'Added statement had source, but it did not verify content',
summary: 'General note: Added statement had source, but it did not verify content'
}
},
'uw-mislead': {
level1: {
label: 'Using misleading edit summaries',
summary: 'General note: Using misleading edit summaries'
},
level2: {
label: 'Using misleading edit summaries',
summary: 'Caution: Using misleading edit summaries'
},
level3: {
label: 'Using misleading edit summaries',
summary: 'Warning: Using misleading edit summaries'
}
},
'uw-mos': {
level1: {
label: 'Manual of style',
summary: 'General note: Formatting, date, language, etc (Manual of style)'
},
level2: {
label: 'Manual of style',
summary: 'Caution: Formatting, date, language, etc (Manual of style)'
},
level3: {
label: 'Manual of style',
summary: 'Warning: Formatting, date, language, etc (Manual of style)'
},
level4: {
label: 'Manual of style',
summary: 'Final warning: Formatting, date, language, etc (Manual of style)'
}
},
'uw-move': {
level1: {
label: 'Page moves against naming conventions or consensus',
summary: 'General note: Page moves against naming conventions or consensus'
},
level2: {
label: 'Page moves against naming conventions or consensus',
summary: 'Caution: Page moves against naming conventions or consensus'
},
level3: {
label: 'Page moves against naming conventions or consensus',
summary: 'Warning: Page moves against naming conventions or consensus'
},
level4: {
label: 'Page moves against naming conventions or consensus',
summary: 'Final warning: Page moves against naming conventions or consensus'
},
level4im: {
label: 'Page moves against naming conventions or consensus',
summary: 'Only warning: Page moves against naming conventions or consensus'
}
},
'uw-redirect': {
level1: {
label: 'Creating inappropriate redirects',
summary: 'General note: Creating inappropriate redirects'
},
level2: {
label: 'Creating inappropriate redirects',
summary: 'Caution: Creating inappropriate redirects'
},
level3: {
label: 'Creating inappropriate redirects',
summary: 'Warning: Creating inappropriate redirects'
},
level4: {
label: 'Creating inappropriate redirects',
summary: 'Final warning: Creating inappropriate redirects'
},
level4im: {
label: 'Creating inappropriate redirects',
summary: 'Only warning: Creating inappropriate redirects'
}
},
'uw-tpv': {
level1: {
label: "Refactoring others' talk page comments",
summary: "General note: Refactoring others' talk page comments"
},
level2: {
label: "Refactoring others' talk page comments",
summary: "Caution: Refactoring others' talk page comments"
},
level3: {
label: "Refactoring others' talk page comments",
summary: "Warning: Refactoring others' talk page comments"
},
level4: {
label: "Refactoring others' talk page comments",
summary: "Final warning: Refactoring others' talk page comments"
},
level4im: {
label: "Refactoring others' talk page comments",
summary: "Only warning: Refactoring others' talk page comments"
}
},
'uw-upload': {
level1: {
label: 'Uploading unencyclopedic images',
summary: 'General note: Uploading unencyclopedic images'
},
level2: {
label: 'Uploading unencyclopedic images',
summary: 'Caution: Uploading unencyclopedic images'
},
level3: {
label: 'Uploading unencyclopedic images',
summary: 'Warning: Uploading unencyclopedic images'
},
level4: {
label: 'Uploading unencyclopedic images',
summary: 'Final warning: Uploading unencyclopedic images'
},
level4im: {
label: 'Uploading unencyclopedic images',
summary: 'Only warning: Uploading unencyclopedic images'
}
}
}
},
singlenotice: {
'uw-addalink': {
label: 'Mistakes with the Add a Link newcomer task',
summary: 'Notice: Mistakes with the Add a Link newcomer task'
},
'uw-agf-sock': {
label: 'Use of multiple accounts (assuming good faith)',
summary: 'Notice: Using multiple accounts'
},
'uw-aiv': {
label: 'Bad AIV report',
summary: 'Notice: Bad AIV report'
},
'uw-articletodraft': {
label: 'Article moved to draftspace',
summary: 'Notice: Article moved to draftspace',
hideReason: true
},
'uw-autobiography': {
label: 'Creating autobiographies',
summary: 'Notice: Creating autobiographies'
},
'uw-badcat': {
label: 'Adding incorrect categories',
summary: 'Notice: Adding incorrect categories'
},
'uw-badlistentry': {
label: 'Adding inappropriate entries to lists',
summary: 'Notice: Adding inappropriate entries to lists'
},
'uw-bareurl': {
label: 'Adding a bare URL',
summary: 'Notice: Adding a bare URL'
},
'uw-bite': {
label: '"Biting" newcomers',
summary: 'Notice: "Biting" newcomers',
suppressArticleInSummary: true // non-standard (user name, not article), and not necessary
},
'uw-blar': {
label: 'Article blanked and redirected',
summary: 'Notice: Article blanked and redirected',
hideReason: true
},
'uw-circular': {
label: 'Using circular sources',
summary: 'Notice: Using circular sources'
},
'uw-coi': {
label: 'Conflict of interest',
summary: 'Notice: Conflict of interest',
heading: 'Managing a conflict of interest'
},
'uw-copying': {
label: 'Copying text to another page',
summary: 'Notice: Copying text to another page'
},
'uw-crystal': {
label: 'Adding speculative or unconfirmed information',
summary: 'Notice: Adding speculative or unconfirmed information'
},
'uw-c&pmove': {
label: 'Cut and paste moves',
summary: 'Notice: Cut and paste moves'
},
'uw-dab': {
label: 'Incorrect edit to a disambiguation page',
summary: 'Notice: Incorrect edit to a disambiguation page'
},
'uw-date': {
label: 'Unnecessarily changing date formats',
summary: 'Notice: Unnecessarily changing date formats'
},
'uw-deadlink': {
label: 'Removing proper sources containing dead links',
summary: 'Notice: Removing proper sources containing dead links'
},
'uw-displaytitle': {
label: 'Incorrect use of DISPLAYTITLE',
summary: 'Notice: Incorrect use of DISPLAYTITLE'
},
'uw-draftfirst': {
label: 'User should draft in userspace without the risk of speedy deletion',
summary: 'Notice: Consider drafting your article in [[Help:Userspace draft|userspace]]'
},
'uw-editsummary': {
label: 'New user not using edit summary',
summary: 'Notice: Not using edit summary'
},
'uw-editsummary2': {
label: 'Experienced user not using edit summary',
summary: 'Notice: Not using edit summary',
hideLinkedPage: true,
hideReason: true
},
'uw-elinbody': {
label: 'Adding external links to the body of an article',
summary: 'Notice: Keep external links to External links sections at the bottom of an article'
},
'uw-english': {
label: 'Not communicating in English',
summary: 'Notice: Not communicating in English'
},
'uw-hasty': {
label: 'Hasty addition of speedy deletion tags',
summary: 'Notice: Allow creators time to improve their articles before tagging them for deletion'
},
'uw-islamhon': {
label: 'Use of Islamic honorifics',
summary: 'Notice: Use of Islamic honorifics'
},
'uw-italicize': {
label: 'Italicize books, films, albums, magazines, TV series, etc within articles',
summary: 'Notice: Italicize books, films, albums, magazines, TV series, etc within articles'
},
'uw-lang': {
label: 'Unnecessarily changing between British and American English',
summary: 'Notice: Unnecessarily changing between British and American English',
heading: 'National varieties of English'
},
'uw-linking': {
label: 'Excessive addition of redlinks or repeated blue links',
summary: 'Notice: Excessive addition of redlinks or repeated blue links'
},
'uw-longsd': {
label: 'Insertion of long short description',
summary: 'Notice: Insertion of long short description'
},
'uw-minor': {
label: 'Incorrect use of minor edits check box',
summary: 'Notice: Incorrect use of minor edits check box'
},
'uw-mostm': {
label: 'Formatting of trademarks',
summary: 'Notice: Formatting of trademarks'
},
'uw-multiple-accts': {
label: 'Inappropriate use of alternative accounts',
summary: 'Notice: Inappropriate use of alternative accounts'
},
'uw-notenglish': {
label: 'Creating non-English articles',
summary: 'Notice: Creating non-English articles'
},
'uw-notenglishedit': {
label: 'Adding non-English content to articles',
summary: 'Notice: Adding non-English content to articles'
},
'uw-notvote': {
label: 'We use consensus, not voting',
summary: 'Notice: We use consensus, not voting'
},
'uw-orphantalk': {
label: 'Talk page created with no article',
summary: 'Notice: Talk page created with no article'
},
'uw-plagiarism': {
label: 'Copying from public domain sources without attribution',
summary: 'Notice: Copying from public domain sources without attribution'
},
'uw-preview': {
label: 'Use preview button to avoid mistakes',
summary: 'Notice: Use preview button to avoid mistakes'
},
'uw-redlink': {
label: 'Indiscriminate removal of redlinks',
summary: 'Notice: Be careful when removing redlinks'
},
'uw-refspam': {
label: 'Adding citations to research published by a small group of researchers',
summary: 'Notice: Adding citations to research published by a small group of researchers',
hideLinkedPage: true,
hideReason: true
},
'uw-selfrevert': {
label: 'Self-reverted editing tests',
summary: 'Notice: Self-reverted editing tests'
},
'uw-socialnetwork': {
label: 'Wikipedia is not a social network',
summary: 'Notice: Wikipedia is not a social network'
},
'uw-sofixit': {
label: 'Be bold and fix things yourself',
summary: 'Notice: You can be bold and fix things yourself'
},
'uw-spoiler': {
label: 'Adding spoiler alerts or removing spoilers from appropriate sections',
summary: "Notice: Don't delete or flag potential 'spoilers' in Wikipedia articles"
},
'uw-talkinarticle': {
label: 'Talk in article',
summary: 'Notice: Talk in article'
},
'uw-tilde': {
label: 'Not signing posts',
summary: 'Notice: Not signing posts'
},
'uw-toppost': {
label: 'Posting at the top of talk pages',
summary: 'Notice: Posting at the top of talk pages'
},
'uw-translation': {
label: 'Adding translations without proper attribution',
summary: 'Notice: Attribution required when translating articles'
},
'uw-unattribcc': {
label: 'Copying from compatibly-licensed sources without attribution',
summary: 'Notice: Copying from compatibly-licensed sources without attribution'
},
'uw-userspace draft finish': {
label: 'Stale userspace draft',
summary: 'Notice: Stale userspace draft'
},
'uw-usertalk': {
label: 'Misuse of user talk page',
summary: 'Notice: Misuse of user talk page',
hideLinkedPage: true
},
'uw-vgscope': {
label: 'Adding video game walkthroughs, cheats or instructions',
summary: 'Notice: Adding video game walkthroughs, cheats or instructions'
},
'uw-warn': {
label: 'Place user warning templates when reverting vandalism',
summary: 'Notice: You can use user warning templates when reverting vandalism'
},
'uw-wrongsummary': {
label: 'Using inaccurate or inappropriate edit summaries',
summary: 'Notice: Using inaccurate or inappropriate edit summaries'
}
},
singlewarn: {
'uw-3rr': {
label: 'Potential three-revert rule violation; see also uw-ew',
summary: 'Warning: Three-revert rule'
},
'uw-affiliate': {
label: 'Affiliate marketing',
summary: 'Warning: Affiliate marketing'
},
'uw-attack': {
label: 'Creating attack pages',
summary: 'Warning: Creating attack pages',
suppressArticleInSummary: true
},
'uw-botun': {
label: 'Bot username',
summary: 'Warning: Bot username'
},
'uw-canvass': {
label: 'Canvassing',
summary: 'Warning: Canvassing'
},
'uw-copyright': {
label: 'Copyright violation',
summary: 'Warning: Copyright violation'
},
'uw-copyright-link': {
label: 'Linking to copyrighted works violation',
summary: 'Warning: Linking to copyrighted works violation'
},
'uw-copyright-new': {
label: 'Copyright violation (with explanation for new users)',
summary: 'Notice: Avoiding copyright problems',
heading: 'Wikipedia and copyright'
},
'uw-copyright-remove': {
label: 'Removing {{copyvio}} template from articles',
summary: 'Warning: Removing {{copyvio}} templates'
},
'uw-derogatory': {
label: 'Addition of derogatory/hateful content',
summary: 'Warning: Addition of derogatory content'
},
'uw-efsummary': {
label: 'Edit summary triggering the edit filter',
summary: 'Warning: Edit summary triggering the edit filter'
},
'uw-ew': {
label: 'Edit warring (stronger wording)',
summary: 'Warning: Edit warring'
},
'uw-ewsoft': {
label: 'Edit warring (softer wording for newcomers)',
summary: 'Warning: Edit warring'
},
'uw-hijacking': {
label: 'Hijacking articles',
summary: 'Warning: Hijacking articles'
},
'uw-hoax': {
label: 'Creating hoaxes',
summary: 'Warning: Creating hoaxes'
},
'uw-legal': {
label: 'Making legal threats',
summary: 'Warning: Making legal threats'
},
'uw-login': {
label: 'Editing while logged out',
summary: 'Warning: Editing while logged out'
},
'uw-multipleTAs': {
label: 'Usage of multiple temporary accounts',
summary: 'Warning: Vandalism using multiple temporary accounts'
},
'uw-paraphrase': {
label: 'Close paraphrasing',
summary: 'Warning: Close paraphrasing'
},
'uw-pinfo': {
label: 'Personal info (outing)',
summary: 'Warning: Personal info'
},
'uw-salt': {
label: 'Recreating salted articles under a different title',
summary: 'Notice: Recreating creation-protected articles under a different title'
},
'uw-socksuspect': {
label: 'Sockpuppetry',
summary: 'Warning: You are a suspected [[WP:SOCK|sockpuppet]]' // of User:...
},
'uw-upv': {
label: 'Userpage vandalism',
summary: 'Warning: Userpage vandalism'
},
'uw-username': {
label: 'Username is against policy',
summary: 'Warning: Your username might be against policy',
suppressArticleInSummary: true // not relevant for this template
},
'uw-coi-username': {
label: 'Username is against policy, and conflict of interest',
summary: 'Warning: Username and conflict of interest',
heading: 'Your username'
},
'uw-userpage': {
label: 'Userpage or subpage is against policy',
summary: 'Warning: Userpage or subpage is against policy'
}
}
};
/**
* Reads Twinkle.warn.messages and returns a specified template's property (such as label, summary,
* suppressArticleInSummary, hideLinkedPage, or hideReason)
*/
Twinkle.warn.getTemplateProperty = function(templates, templateName, propertyName) {
let result;
const isNumberedTemplate = templateName.match(/(1|2|3|4|4im)$/);
if (isNumberedTemplate) {
const unNumberedTemplateName = templateName.replace(/(?:1|2|3|4|4im)$/, '');
const level = isNumberedTemplate[0];
const numberedWarnings = {};
$.each(templates.levels, (key, val) => {
$.extend(numberedWarnings, val);
});
$.each(numberedWarnings, (key) => {
if (key === unNumberedTemplateName) {
result = numberedWarnings[key]['level' + level][propertyName];
}
});
}
// Non-level templates can also end in a number. So check this for all templates.
const otherWarnings = {};
$.each(templates, (key, val) => {
if (key !== 'levels') {
$.extend(otherWarnings, val);
}
});
$.each(otherWarnings, (key) => {
if (key === templateName) {
result = otherWarnings[key][propertyName];
}
});
return result;
};
// Used repeatedly below across menu rebuilds
Twinkle.warn.prev_article = null;
Twinkle.warn.prev_reason = null;
Twinkle.warn.talkpageObj = null;
Twinkle.warn.callback.change_category = function twinklewarnCallbackChangeCategory(e) {
const value = e.target.value;
const sub_group = e.target.root.sub_group;
sub_group.main_group = value;
let old_subvalue = sub_group.value;
let old_subvalue_re;
if (old_subvalue) {
if (value === 'kitchensink') { // Exact match possible in kitchensink menu
old_subvalue_re = new RegExp(mw.util.escapeRegExp(old_subvalue));
} else {
old_subvalue = old_subvalue.replace(/\d*(im)?$/, '');
old_subvalue_re = new RegExp(mw.util.escapeRegExp(old_subvalue) + '(\\d*(?:im)?)$');
}
}
while (sub_group.hasChildNodes()) {
sub_group.removeChild(sub_group.firstChild);
}
let selected = false;
// worker function to create the combo box entries
const createEntries = function(contents, container, wrapInOptgroup, val = value) {
// level2->2, singlewarn->''; also used to distinguish the
// scaled levels from singlenotice, singlewarn, and custom
const level = val.replace(/^\D+/g, '');
// due to an apparent iOS bug, we have to add an option-group to prevent truncation of text
// (search WT:TW archives for "Problem selecting warnings on an iPhone")
if (wrapInOptgroup && $.client.profile().platform === 'iphone') {
let wrapperOptgroup = new Morebits.QuickForm.Element({
type: 'optgroup',
label: 'Available templates'
});
wrapperOptgroup = wrapperOptgroup.render();
container.appendChild(wrapperOptgroup);
container = wrapperOptgroup;
}
$.each(contents, (itemKey, itemProperties) => {
// Skip if the current template doesn't have a version for the current level
if (!!level && !itemProperties[val]) {
return;
}
const key = typeof itemKey === 'string' ? itemKey : itemProperties.value;
const template = key + level;
const elem = new Morebits.QuickForm.Element({
type: 'option',
label: '{{' + template + '}}: ' + (level ? itemProperties[val].label : itemProperties.label),
value: template
});
// Select item best corresponding to previous selection
if (!selected && old_subvalue && old_subvalue_re.test(template)) {
elem.data.selected = selected = true;
}
const elemRendered = container.appendChild(elem.render());
$(elemRendered).data('messageData', itemProperties);
});
};
const createGroup = function(warnGroup, label, wrapInOptgroup, val) {
wrapInOptgroup = typeof wrapInOptgroup !== 'undefined' ? wrapInOptgroup : true;
let optgroup = new Morebits.QuickForm.Element({
type: 'optgroup',
label: label
});
optgroup = optgroup.render();
sub_group.appendChild(optgroup);
createEntries(warnGroup, optgroup, wrapInOptgroup, val);
};
switch (value) {
case 'singlenotice':
case 'singlewarn':
createEntries(Twinkle.warn.messages[value], sub_group, true);
break;
case 'singlecombined':
var unSortedSinglets = $.extend({}, Twinkle.warn.messages.singlenotice, Twinkle.warn.messages.singlewarn);
var sortedSingletMessages = {};
Object.keys(unSortedSinglets).sort().forEach((key) => {
sortedSingletMessages[key] = unSortedSinglets[key];
});
createEntries(sortedSingletMessages, sub_group, true);
break;
case 'custom':
createEntries(Twinkle.getPref('customWarningList'), sub_group, true);
break;
case 'kitchensink':
['level1', 'level2', 'level3', 'level4', 'level4im'].forEach((lvl) => {
$.each(Twinkle.warn.messages.levels, (levelGroupLabel, levelGroup) => {
createGroup(levelGroup, 'Level ' + lvl.slice(5) + ': ' + levelGroupLabel, true, lvl);
});
});
createGroup(Twinkle.warn.messages.singlenotice, 'Single-issue notices');
createGroup(Twinkle.warn.messages.singlewarn, 'Single-issue warnings');
createGroup(Twinkle.getPref('customWarningList'), 'Custom warnings');
break;
case 'level1':
case 'level2':
case 'level3':
case 'level4':
case 'level4im':
// Creates subgroup regardless of whether there is anything to place in it;
// leaves "Removal of deletion tags" empty for 4im
$.each(Twinkle.warn.messages.levels, (groupLabel, groupContents) => {
createGroup(groupContents, groupLabel, false);
});
break;
case 'autolevel':
// Check user page to determine appropriate level
var autolevelProc = function() {
const wikitext = Twinkle.warn.talkpageObj.getPageText();
// history not needed for autolevel
const latest = Twinkle.warn.callbacks.dateProcessing(wikitext)[0];
// Pseudo-params with only what's needed to parse the level i.e. no messageData
const params = {
sub_group: old_subvalue,
article: e.target.root.article.value
};
const lvl = 'level' + Twinkle.warn.callbacks.autolevelParseWikitext(wikitext, params, latest)[1];
// Identical to level1, etc. above but explicitly provides the level
$.each(Twinkle.warn.messages.levels, (groupLabel, groupContents) => {
createGroup(groupContents, groupLabel, false, lvl);
});
// Trigger subcategory change, add select menu, etc.
Twinkle.warn.callback.postCategoryCleanup(e);
};
if (Twinkle.warn.talkpageObj) {
autolevelProc();
} else {
const usertalk_page = new Morebits.wiki.Page('User_talk:' + mw.config.get('wgRelevantUserName'), 'Loading previous warnings');
usertalk_page.setFollowRedirect(true, false);
usertalk_page.load((pageobj) => {
Twinkle.warn.talkpageObj = pageobj; // Update talkpageObj
autolevelProc();
}, () => {
// Catch and warn if the talkpage can't load,
// most likely because it's a cross-namespace redirect
// Supersedes the typical $autolevelMessage added in autolevelParseWikitext
const $noTalkPageNode = $('<strong>')
.text('Unable to load user talk page; it might be a cross-namespace redirect. Autolevel detection will not work.')
.attr('id', 'twinkle-warn-autolevel-message')
.css('color', 'red');
$noTalkPageNode.insertBefore($('#twinkle-warn-warning-messages'));
// If a preview was opened while in a different mode, close it
// Should nullify the need to catch the error in preview callback
e.target.root.previewer.closePreview();
});
}
break;
default:
alert('Unknown warning group in twinklewarn');
break;
}
// Trigger subcategory change, add select menu, etc.
// Here because of the async load for autolevel
if (value !== 'autolevel') {
// reset any autolevel-specific messages while we're here
$('#twinkle-warn-autolevel-message').remove();
Twinkle.warn.callback.postCategoryCleanup(e);
}
};
Twinkle.warn.callback.postCategoryCleanup = function twinklewarnCallbackPostCategoryCleanup(e) {
// clear overridden label on article textbox
Morebits.QuickForm.setElementTooltipVisibility(e.target.root.article, true);
Morebits.QuickForm.resetElementLabel(e.target.root.article);
// Trigger custom label/change on main category change
Twinkle.warn.callback.change_subcategory(e);
// Use select2 to make the select menu searchable
if (!Twinkle.getPref('oldSelect')) {
$('select[name=sub_group]')
.select2({
theme: 'default select2-morebits',
width: '100%',
matcher: Morebits.select2.matchers.optgroupFull,
templateResult: Morebits.select2.highlightSearchMatches,
language: {
searching: Morebits.select2.queryInterceptor
}
})
.change(Twinkle.warn.callback.change_subcategory);
$('.select2-selection').on('keydown', Morebits.select2.autoStart).trigger('focus');
mw.util.addCSS(
// Increase height
'.select2-container .select2-dropdown .select2-results > .select2-results__options { max-height: 350px; }' +
// Reduce padding
'.select2-results .select2-results__option { padding-top: 1px; padding-bottom: 1px; }' +
'.select2-results .select2-results__group { padding-top: 1px; padding-bottom: 1px; } ' +
// Adjust font size
'.select2-container .select2-dropdown .select2-results { font-size: 13px; }' +
'.select2-container .selection .select2-selection__rendered { font-size: 13px; }'
);
}
};
Twinkle.warn.callback.change_subcategory = function twinklewarnCallbackChangeSubcategory(e) {
const selected_main_group = e.target.form.main_group.value;
const selected_template = e.target.form.sub_group.value;
// If template shouldn't have a linked article, hide the linked article label and text box
const hideLinkedPage = Twinkle.warn.getTemplateProperty(Twinkle.warn.messages, selected_template, 'hideLinkedPage');
if (hideLinkedPage) {
e.target.form.article.value = '';
Morebits.QuickForm.setElementVisibility(e.target.form.article.parentElement, false);
} else {
Morebits.QuickForm.setElementVisibility(e.target.form.article.parentElement, true);
}
// If template shouldn't have an optional message, hide the optional message label and text box
const hideReason = Twinkle.warn.getTemplateProperty(Twinkle.warn.messages, selected_template, 'hideReason');
if (hideReason) {
e.target.form.reason.value = '';
Morebits.QuickForm.setElementVisibility(e.target.form.reason.parentElement, false);
} else {
Morebits.QuickForm.setElementVisibility(e.target.form.reason.parentElement, true);
}
// Tags that don't take a linked article, but something else (often a username).
// The value of each tag is the label next to the input field
const notLinkedArticle = {
'uw-agf-sock': 'Optional username of other account (without User:) ',
'uw-bite': "Username of 'bitten' user (without User:) ",
'uw-socksuspect': 'Username of sock master, if known (without User:) ',
'uw-username': 'Username violates policy because... ',
'uw-aiv': 'Optional username that was reported (without User:) '
};
const hasLevel = ['singlenotice', 'singlewarn', 'singlecombined', 'kitchensink'].includes(selected_main_group);
if (hasLevel) {
if (notLinkedArticle[selected_template]) {
if (Twinkle.warn.prev_article === null) {
Twinkle.warn.prev_article = e.target.form.article.value;
}
e.target.form.article.notArticle = true;
e.target.form.article.value = '';
// change form labels according to the warning selected
Morebits.QuickForm.setElementTooltipVisibility(e.target.form.article, false);
Morebits.QuickForm.overrideElementLabel(e.target.form.article, notLinkedArticle[selected_template]);
} else if (e.target.form.article.notArticle) {
if (Twinkle.warn.prev_article !== null) {
e.target.form.article.value = Twinkle.warn.prev_article;
Twinkle.warn.prev_article = null;
}
e.target.form.article.notArticle = false;
Morebits.QuickForm.setElementTooltipVisibility(e.target.form.article, true);
Morebits.QuickForm.resetElementLabel(e.target.form.article);
}
}
// add big red notice, warning users about how to use {{uw-[coi-]username}} appropriately
$('#tw-warn-red-notice').remove();
let $redWarning;
if (selected_template === 'uw-username') {
$redWarning = $("<div style='color: red;' id='tw-warn-red-notice'>{{uw-username}} should <b>not</b> be used for <b>blatant</b> username policy violations. " +
"Blatant violations should be reported directly to UAA (via Twinkle's ARV tab). " +
'{{uw-username}} should only be used in edge cases in order to engage in discussion with the user.</div>');
$redWarning.insertAfter(Morebits.QuickForm.getElementLabelObject(e.target.form.reasonGroup));
} else if (selected_template === 'uw-coi-username') {
$redWarning = $("<div style='color: red;' id='tw-warn-red-notice'>{{uw-coi-username}} should <b>not</b> be used for <b>blatant</b> username policy violations. " +
"Blatant violations should be reported directly to UAA (via Twinkle's ARV tab). " +
'{{uw-coi-username}} should only be used in edge cases in order to engage in discussion with the user.</div>');
$redWarning.insertAfter(Morebits.QuickForm.getElementLabelObject(e.target.form.reasonGroup));
}
};
Twinkle.warn.callbacks = {
getWarningWikitext: function(templateName, article, reason, isCustom) {
let text = '{{subst:' + templateName;
// add linked article for user warnings
if (article) {
// c&pmove has the source as the first parameter
if (templateName === 'uw-c&pmove') {
text += '|to=' + article;
} else {
text += '|1=' + article;
}
}
if (reason && !isCustom) {
// add extra message
if (templateName === 'uw-userpage') {
text += "|3=''" + Morebits.string.formatReasonText(reason) + "''";
} else {
text += "|2=''" + Morebits.string.formatReasonText(reason) + "''";
}
}
text += '}}';
if (reason && isCustom) {
// we assume that custom warnings lack a {{{2}}} parameter
text += " ''" + reason + "''";
}
return text + ' ~~~~';
},
showPreview: function(form, templatename) {
const input = Morebits.QuickForm.getInputData(form);
// Provided on autolevel, not otherwise
templatename = templatename || input.sub_group;
const linkedarticle = input.article;
const templatetext = Twinkle.warn.callbacks.getWarningWikitext(templatename, linkedarticle,
input.reason, input.main_group === 'custom');
form.previewer.beginRender(templatetext, 'User_talk:' + mw.config.get('wgRelevantUserName')); // Force wikitext/correct username
},
// Just a pass-through unless the autolevel option was selected
preview: function(form) {
if (form.main_group.value === 'autolevel') {
// Always get a new, updated talkpage for autolevel processing
const usertalk_page = new Morebits.wiki.Page('User_talk:' + mw.config.get('wgRelevantUserName'), 'Loading previous warnings');
usertalk_page.setFollowRedirect(true, false);
// Will fail silently if the talk page is a cross-ns redirect,
// removal of the preview box handled when loading the menu
usertalk_page.load((pageobj) => {
Twinkle.warn.talkpageObj = pageobj; // Update talkpageObj
const wikitext = pageobj.getPageText();
// history not needed for autolevel
const latest = Twinkle.warn.callbacks.dateProcessing(wikitext)[0];
const params = {
sub_group: form.sub_group.value,
article: form.article.value,
messageData: $(form.sub_group).find('option[value="' + $(form.sub_group).val() + '"]').data('messageData')
};
const template = Twinkle.warn.callbacks.autolevelParseWikitext(wikitext, params, latest)[0];
Twinkle.warn.callbacks.showPreview(form, template);
// If the templates have diverged, fake a change event
// to reload the menu with the updated pageobj
if (form.sub_group.value !== template) {
const evt = document.createEvent('Event');
evt.initEvent('change', true, true);
form.main_group.dispatchEvent(evt);
}
});
} else {
Twinkle.warn.callbacks.showPreview(form);
}
},
/**
* Used in the main and autolevel loops to determine when to warn
* about excessively recent, stale, or identical warnings.
*
* @param {string} wikitext The text of a user's talk page, from getPageText()
* @return {Object[]} - Array of objects: latest contains most recent
* warning and date; history lists all prior warnings
*/
dateProcessing: function(wikitext) {
const history_re = /<!--\s?Template:([uU]w-.*?)\s?-->.*?(\d{1,2}:\d{1,2}, \d{1,2} \w+ \d{4} \(UTC\))/g;
const history = {};
const latest = { date: new Morebits.Date(0), type: '' };
let current;
while ((current = history_re.exec(wikitext)) !== null) {
const template = current[1], current_date = new Morebits.Date(current[2]);
if (!(template in history) || history[template].isBefore(current_date)) {
history[template] = current_date;
}
if (!latest.date.isAfter(current_date)) {
latest.date = current_date;
latest.type = template;
}
}
return [latest, history];
},
// False positive
// eslint-disable-next-line jsdoc/require-returns-check
/**
* Main loop for deciding what the level should increment to. Most of
* this is really just error catching and updating the subsequent data.
* May produce up to two notices in a twinkle-warn-autolevel-messages div
*
* @param {string} wikitext The text of a user's talk page, from getPageText() (required)
* @param {Object} params Params object: sub_group is the template (required);
* article is the user-provided article (form.article) used to link ARV on recent level4 warnings;
* messageData is only necessary if getting the full template, as it's
* used to ensure a valid template of that level exists
* @param {Object} latest First element of the array returned from
* dateProcessing. Provided here rather than processed within to avoid
* repeated call to dateProcessing
* @param {(Date|Morebits.Date)} date Date from which staleness is determined
* @param {Morebits.Status} statelem Status element, only used for handling error in final execution
*
* @return {Array} - Array that contains the full template and just the warning level
*/
autolevelParseWikitext: function(wikitext, params, latest, date, statelem) {
let level; // undefined rather than '' means the isNaN below will return true
if (/\d(?:im)?$/.test(latest.type)) { // level1-4im
level = parseInt(latest.type.replace(/.*(\d)(?:im)?$/, '$1'), 10);
} else if (latest.type) { // Non-numbered warning
// Try to leverage existing categorization of
// warnings, all but one are universally lowercased
const loweredType = /uw-multipleTAs/i.test(latest.type) ? 'uw-multipleTAs' : latest.type.toLowerCase();
// It would be nice to account for blocks, but in most
// cases the hidden message is terminal, not the sig
if (Twinkle.warn.messages.singlewarn[loweredType]) {
level = 3;
} else {
level = 1; // singlenotice or not found
}
}
const $autolevelMessage = $('<div>')
.attr('id', 'twinkle-warn-autolevel-message');
if (isNaN(level)) { // No prior warnings found, this is the first
level = 1;
} else if (level > 4 || level < 1) { // Shouldn't happen
const message = 'Unable to parse previous warning level, please manually select a warning level.';
if (statelem) {
statelem.error(message);
} else {
alert(message);
}
return;
} else {
date = date || new Date();
const autoTimeout = new Morebits.Date(latest.date.getTime()).add(parseInt(Twinkle.getPref('autolevelStaleDays'), 10), 'days');
if (autoTimeout.isAfter(date)) {
if (level === 4) {
level = 4;
// Basically indicates whether we're in the final Main evaluation or not,
// and thus whether we can continue or need to display the warning and link
if (!statelem) {
const $link = $('<a>')
.attr('href', '#')
.text('click here to open the ARV tool.')
.css('fontWeight', 'bold')
.on('click', () => {
Morebits.wiki.actionCompleted.redirect = null;
Twinkle.warn.dialog.close();
Twinkle.arv.callback(mw.config.get('wgRelevantUserName'));
$('input[name=page]').val(params.article); // Target page
$('input[value=final]').prop('checked', true); // Vandalism after final
});
const $statusNode = $('<div>')
.text(mw.config.get('wgRelevantUserName') + ' recently received a level 4 warning (' + latest.type + ') so it might be better to report them instead; ')
.css('color', 'red');
$statusNode.append($link[0]);
$autolevelMessage.append($statusNode);
}
} else { // Automatically increase severity
level += 1;
}
} else { // Reset warning level if most-recent warning is too old
level = 1;
}
}
$autolevelMessage.prepend($('<div>Will issue a <span style="font-weight: bold;">level ' + level + '</span> template.</div>'));
// Place after the stale and other-user-reverted (text-only) messages
$('#twinkle-warn-autolevel-message').remove(); // clean slate
$autolevelMessage.insertAfter($('#twinkle-warn-warning-messages'));
let template = params.sub_group.replace(/(.*)\d$/, '$1');
// Validate warning level, falling back to the uw-generic series.
// Only a few items are missing a level, and in all but a handful
// of cases, the uw-generic series is explicitly used elsewhere per WP:UTM.
if (params.messageData && !params.messageData['level' + level]) {
template = 'uw-generic';
}
template += level;
return [template, level];
},
main: function(pageobj) {
const text = pageobj.getPageText();
const statelem = pageobj.getStatusElement();
const params = pageobj.getCallbackParameters();
let messageData = params.messageData;
const [latest, history] = Twinkle.warn.callbacks.dateProcessing(text);
const now = new Morebits.Date(pageobj.getLoadTime());
Twinkle.warn.talkpageObj = pageobj; // Update talkpageObj, just in case
if (params.main_group === 'autolevel') {
// [template, level]
const templateAndLevel = Twinkle.warn.callbacks.autolevelParseWikitext(text, params, latest, now, statelem);
// Only if there's a change from the prior display/load
if (params.sub_group !== templateAndLevel[0] && !confirm('Will issue a {{' + templateAndLevel[0] + '}} template to the user, okay?')) {
statelem.error('aborted per user request');
return;
}
// Update params now that we've selected a warning
params.sub_group = templateAndLevel[0];
messageData = params.messageData['level' + templateAndLevel[1]];
} else if (params.sub_group in history) {
if (new Morebits.Date(history[params.sub_group]).add(1, 'day').isAfter(now)) {
if (!confirm('An identical ' + params.sub_group + ' has been issued in the last 24 hours. \nWould you still like to add this warning/notice?')) {
statelem.error('aborted per user request');
return;
}
}
}
latest.date.add(1, 'minute'); // after long debate, one minute is max
if (latest.date.isAfter(now)) {
if (!confirm('A ' + latest.type + ' has been issued in the last minute. \nWould you still like to add this warning/notice?')) {
statelem.error('aborted per user request');
return;
}
}
// build the edit summary
// Function to handle generation of summary prefix for custom templates
const customProcess = function(template) {
template = template.split('|')[0];
let prefix;
switch (template.slice(-1)) {
case '1':
prefix = 'General note';
break;
case '2':
prefix = 'Caution';
break;
case '3':
prefix = 'Warning';
break;
case '4':
prefix = 'Final warning';
break;
case 'm':
if (template.slice(-3) === '4im') {
prefix = 'Only warning';
break;
}
// falls through
default:
prefix = 'Notice';
break;
}
return prefix + ': ' + Morebits.string.toUpperCaseFirstChar(messageData.label);
};
let summary;
if (params.main_group === 'custom') {
summary = customProcess(params.sub_group);
} else {
// Normalize kitchensink to the 1-4im style
if (params.main_group === 'kitchensink' && !/^D+$/.test(params.sub_group)) {
let sub = params.sub_group.slice(-1);
if (sub === 'm') {
sub = params.sub_group.slice(-3);
}
// Don't overwrite uw-3rr, technically unnecessary
if (/\d/.test(sub)) {
params.main_group = 'level' + sub;
}
}
// singlet || level1-4im, no need to /^\D+$/.test(params.main_group)
summary = messageData.summary || (messageData[params.main_group] && messageData[params.main_group].summary);
// Not in Twinkle.warn.messages, assume custom template
if (!summary) {
summary = customProcess(params.sub_group);
}
if (messageData.suppressArticleInSummary !== true && params.article) {
if (params.sub_group === 'uw-agf-sock' ||
params.sub_group === 'uw-socksuspect' ||
params.sub_group === 'uw-aiv') { // these templates require a username
summary += ' of [[:User:' + params.article + ']]';
} else {
summary += ' on [[:' + params.article + ']]';
}
}
}
pageobj.setEditSummary(summary + '.');
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.setWatchlist(Twinkle.getPref('watchWarnings'));
// Get actual warning text
const warningText = Twinkle.warn.callbacks.getWarningWikitext(params.sub_group, params.article,
params.reason, params.main_group === 'custom');
let sectionExists = false, sectionNumber = 0;
// Only check sections if there are sections or there's a chance we won't create our own
if (!messageData.heading && text.length) {
// Get all sections
const sections = text.match(/^(==*).+\1/gm);
if (sections && sections.length !== 0) {
// Find the index of the section header in question
const dateHeaderRegex = now.monthHeaderRegex();
sectionNumber = 0;
// Find this month's section among L2 sections, preferring the bottom-most
sectionExists = sections.reverse().some((sec, idx) => /^(==)[^=].+\1/m.test(sec) && dateHeaderRegex.test(sec) && typeof (sectionNumber = sections.length - 1 - idx) === 'number');
}
}
if (sectionExists) { // append to existing section
pageobj.setPageSection(sectionNumber + 1);
pageobj.setAppendText('\n\n' + warningText);
pageobj.append();
} else {
if (messageData.heading) { // create new section
pageobj.setNewSectionTitle(messageData.heading);
} else {
Morebits.Status.info('Info', 'Will create a new talk page section for this month, as none was found');
pageobj.setNewSectionTitle(now.monthHeader(0));
}
pageobj.setNewSectionText(warningText);
pageobj.newSection();
}
}
};
Twinkle.warn.callback.evaluate = function twinklewarnCallbackEvaluate(e) {
const userTalkPage = 'User_talk:' + mw.config.get('wgRelevantUserName');
// reason, main_group, sub_group, article
const params = Morebits.QuickForm.getInputData(e.target);
// Check that a reason was filled in if uw-username was selected
if (params.sub_group === 'uw-username' && !params.article) {
alert('You must supply a reason for the {{uw-username}} template.');
return;
}
// The autolevel option will already know by now if a user talk page
// is a cross-namespace redirect (via !!Twinkle.warn.talkpageObj), so
// technically we could alert an error here, but the user will have
// already ignored the bold red error above. Moreover, they probably
// *don't* want to actually issue a warning, so the error handling
// after the form is submitted is probably preferable
// Find the selected <option> element so we can fetch the data structure
const $selectedEl = $(e.target.sub_group).find('option[value="' + $(e.target.sub_group).val() + '"]');
params.messageData = $selectedEl.data('messageData');
Morebits.SimpleWindow.setButtonsEnabled(false);
Morebits.Status.init(e.target);
Morebits.wiki.actionCompleted.redirect = userTalkPage;
Morebits.wiki.actionCompleted.notice = 'Warning complete, reloading talk page in a few seconds';
const wikipedia_page = new Morebits.wiki.Page(userTalkPage, 'User talk page modification');
wikipedia_page.setCallbackParameters(params);
wikipedia_page.setFollowRedirect(true, false);
wikipedia_page.load(Twinkle.warn.callbacks.main);
};
Twinkle.addInitCallback(Twinkle.warn, 'warn');
}());
// </nowiki>
t6dsgua815xkdushyhfj8dhyg3wusyh
769532
769465
2026-10-22T00:00:34Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 1406828: rm cmt
769532
javascript
text/javascript
// <nowiki>
(function() {
/*
****************************************
*** twinklewarn.js: Warn module
****************************************
* Mode of invocation: Tab ("Warn")
* Active on: Any page with relevant user name (userspace, contribs,
* etc.) (not IP ranges), as well as the rollback success page
*/
Twinkle.warn = function twinklewarn() {
// Users and IPs but not IP ranges
if (mw.config.exists('wgRelevantUserName') && !Morebits.ip.isRange(mw.config.get('wgRelevantUserName'))) {
Twinkle.addPortletLink(Twinkle.warn.callback, 'Warn', 'tw-warn', 'Warn/notify user');
if (Twinkle.getPref('autoMenuAfterRollback') &&
mw.config.get('wgNamespaceNumber') === 3 &&
Twinkle.getPrefill('vanarticle') &&
!Twinkle.getPrefill('twinklewelcome') &&
!Twinkle.getPrefill('noautowarn')) {
Twinkle.warn.callback();
}
}
// Modify URL of talk page on rollback success pages, makes use of a
// custom message box in [[MediaWiki:Rollback-success]]
if (mw.config.get('wgAction') === 'rollback') {
const $vandalTalkLink = $('#mw-rollback-success').find('.mw-usertoollinks a').first();
if ($vandalTalkLink.length) {
$vandalTalkLink.css('font-weight', 'bold');
$vandalTalkLink.wrapInner($('<span>').attr('title', 'If appropriate, you can use Twinkle to warn the user about their edits to this page.'));
// Can't provide vanarticlerevid as only wgCurRevisionId is provided
const extraParam = 'vanarticle=' + mw.util.rawurlencode(Morebits.pageNameNorm);
const href = $vandalTalkLink.attr('href');
if (!href.includes('?')) {
$vandalTalkLink.attr('href', href + '?' + extraParam);
} else {
$vandalTalkLink.attr('href', href + '&' + extraParam);
}
}
}
};
// Used to close window when switching to ARV in autolevel
Twinkle.warn.dialog = null;
Twinkle.warn.callback = function twinklewarnCallback() {
if (mw.config.get('wgRelevantUserName') === mw.config.get('wgUserName') &&
!confirm('You are about to warn yourself! Are you sure you want to proceed?')) {
return;
}
Twinkle.warn.dialog = new Morebits.SimpleWindow(600, 440);
const dialog = Twinkle.warn.dialog;
dialog.setTitle('Warn/notify user');
dialog.setScriptName('Twinkle');
dialog.addFooterLink('Choosing a warning level', 'WP:UWUL#Levels');
dialog.addFooterLink('Warn prefs', 'WP:TW/PREF#warn');
dialog.addFooterLink('Twinkle help', 'WP:TW/DOC#warn');
dialog.addFooterLink('Give feedback', 'WT:TW');
const form = new Morebits.QuickForm(Twinkle.warn.callback.evaluate);
const main_select = form.append({
type: 'field',
label: 'Choose type of warning/notice to issue',
tooltip: 'First choose a main warning group, then the specific warning to issue.'
});
const main_group = main_select.append({
type: 'select',
name: 'main_group',
tooltip: 'You can customize the default selection in your Twinkle preferences',
event: Twinkle.warn.callback.change_category
});
const defaultGroup = parseInt(Twinkle.getPref('defaultWarningGroup'), 10);
main_group.append({ type: 'option', label: 'Auto-select level (1-4)', value: 'autolevel', selected: defaultGroup === 11 });
main_group.append({ type: 'option', label: '1: General note', value: 'level1', selected: defaultGroup === 1 });
main_group.append({ type: 'option', label: '2: Caution', value: 'level2', selected: defaultGroup === 2 });
main_group.append({ type: 'option', label: '3: Warning', value: 'level3', selected: defaultGroup === 3 });
main_group.append({ type: 'option', label: '4: Final warning', value: 'level4', selected: defaultGroup === 4 });
main_group.append({ type: 'option', label: '4im: Only warning', value: 'level4im', selected: defaultGroup === 5 });
if (Twinkle.getPref('combinedSingletMenus')) {
main_group.append({ type: 'option', label: 'Single-issue messages', value: 'singlecombined', selected: defaultGroup === 6 || defaultGroup === 7 });
} else {
main_group.append({ type: 'option', label: 'Single-issue notices', value: 'singlenotice', selected: defaultGroup === 6 });
main_group.append({ type: 'option', label: 'Single-issue warnings', value: 'singlewarn', selected: defaultGroup === 7 });
}
if (Twinkle.getPref('customWarningList').length) {
main_group.append({ type: 'option', label: 'Custom warnings', value: 'custom', selected: defaultGroup === 9 });
}
main_group.append({ type: 'option', label: 'All warning templates', value: 'kitchensink', selected: defaultGroup === 10 });
main_select.append({ type: 'select', name: 'sub_group', event: Twinkle.warn.callback.change_subcategory }); // Will be empty to begin with.
form.append({
type: 'input',
name: 'article',
label: 'Linked page',
value: Twinkle.getPrefill('vanarticle') || '',
tooltip: 'A page can be linked within the notice, perhaps because it was a revert to said page that dispatched this notice. Leave empty for no page to be linked.'
});
form.append({
type: 'div',
label: '',
style: 'color: red',
id: 'twinkle-warn-warning-messages'
});
const more = form.append({ type: 'field', name: 'reasonGroup', label: 'Warning information' });
more.append({ type: 'textarea', label: 'Optional message:', name: 'reason', tooltip: 'Perhaps a reason, or that a more detailed notice must be appended' });
const previewlink = document.createElement('a');
$(previewlink).on('click', () => {
Twinkle.warn.callbacks.preview(result); // |result| is defined below
});
previewlink.style.cursor = 'pointer';
previewlink.textContent = 'Preview';
more.append({ type: 'div', id: 'warningpreview', label: [previewlink] });
more.append({ type: 'div', id: 'twinklewarn-previewbox', style: 'display: none' });
more.append({ type: 'submit', label: 'Submit' });
var result = form.render();
dialog.setContent(result);
dialog.display();
result.main_group.root = result;
result.previewer = new Morebits.wiki.Preview($(result).find('div#twinklewarn-previewbox').last()[0]);
// Potential notices for staleness and missed reverts
const vanrevid = Twinkle.getPrefill('vanarticlerevid');
if (vanrevid) {
let message = '';
let query = {};
// If you tried reverting, check if *you* actually reverted
if (!Twinkle.getPrefill('noautowarn') && Twinkle.getPrefill('vanarticle')) { // Via rollback link
query = {
action: 'query',
titles: Twinkle.getPrefill('vanarticle'),
prop: 'revisions',
rvstartid: vanrevid,
rvlimit: 2,
rvdir: 'newer',
rvprop: 'user',
format: 'json'
};
new Morebits.wiki.Api('Checking if you successfully reverted the page', query, ((apiobj) => {
const rev = apiobj.getResponse().query.pages[0].revisions;
const revertUser = rev && rev[1].user;
if (revertUser && revertUser !== mw.config.get('wgUserName')) {
message += ' Someone else reverted the page and may have already warned the user.';
$('#twinkle-warn-warning-messages').text('Note:' + message);
}
})).post();
}
// Confirm edit wasn't too old for a warning
const checkStale = function(vantimestamp) {
const revDate = new Morebits.Date(vantimestamp);
if (vantimestamp && revDate.isValid()) {
if (revDate.add(24, 'hours').isBefore(new Date())) {
message += ' This edit was made more than 24 hours ago so a warning may be stale.';
$('#twinkle-warn-warning-messages').text('Note:' + message);
}
}
};
let vantimestamp = Twinkle.getPrefill('vantimestamp');
// If from a rollback module-based revert, no API lookup necessary
if (vantimestamp) {
checkStale(vantimestamp);
} else {
query = {
action: 'query',
prop: 'revisions',
rvprop: 'timestamp',
revids: vanrevid,
format: 'json'
};
new Morebits.wiki.Api('Grabbing the revision timestamps', query, ((apiobj) => {
const rev = apiobj.getResponse().query.pages[0].revisions;
vantimestamp = rev && rev[0].timestamp;
checkStale(vantimestamp);
})).post();
}
}
// We must init the first choice (General Note);
const evt = document.createEvent('Event');
evt.initEvent('change', true, true);
result.main_group.dispatchEvent(evt);
};
// This is all the messages that might be dispatched by the code
// Each of the individual templates require the following information:
// label (required): A short description displayed in the dialog
// summary (required): The edit summary used. If an article name is entered, the summary is postfixed with "on [[article]]", and it is always postfixed with "."
// suppressArticleInSummary (optional): Set to true to suppress showing the article name in the edit summary. Useful if the warning relates to attack pages, or some such.
// hideLinkedPage (optional): Set to true to hide the "Linked page" text box. Some warning templates do not have a linked article parameter.
// hideReason (optional): Set to true to hide the "Optional message" text box. Some warning templates do not have a reason parameter.
Twinkle.warn.messages = {
levels: {
'Common warnings': {
'uw-vandalism': {
level1: {
label: 'Vandalism',
summary: 'General note: Unconstructive editing'
},
level2: {
label: 'Vandalism',
summary: 'Caution: Unconstructive editing'
},
level3: {
label: 'Vandalism',
summary: 'Warning: Vandalism'
},
level4: {
label: 'Vandalism',
summary: 'Final warning: Vandalism'
},
level4im: {
label: 'Vandalism',
summary: 'Only warning: Vandalism'
}
},
'uw-disruptive': {
level1: {
label: 'Disruptive editing',
summary: 'General note: Unconstructive editing'
},
level2: {
label: 'Disruptive editing',
summary: 'Caution: Unconstructive editing'
},
level3: {
label: 'Disruptive editing',
summary: 'Warning: Disruptive editing'
}
},
'uw-test': {
level1: {
label: 'Editing tests',
summary: 'General note: Editing tests'
},
level2: {
label: 'Editing tests',
summary: 'Caution: Editing tests'
},
level3: {
label: 'Editing tests',
summary: 'Warning: Editing tests'
}
},
'uw-delete': {
level1: {
label: 'Removal of content, blanking',
summary: 'General note: Removal of content, blanking'
},
level2: {
label: 'Removal of content, blanking',
summary: 'Caution: Removal of content, blanking'
},
level3: {
label: 'Removal of content, blanking',
summary: 'Warning: Removal of content, blanking'
},
level4: {
label: 'Removal of content, blanking',
summary: 'Final warning: Removal of content, blanking'
},
level4im: {
label: 'Removal of content, blanking',
summary: 'Only warning: Removal of content, blanking'
}
},
'uw-generic': {
level4: {
label: 'Generic warning (for template series missing level 4)',
summary: 'Final warning notice'
}
}
},
'Behavior in articles': {
'uw-ai': {
level1: {
label: 'Using a large language model',
summary: 'General note: Using a large language model'
},
level2: {
label: 'Using a large language model',
summary: 'Caution: Using a large language model'
},
level3: {
label: 'Using a large language model',
summary: 'Warning: Using a large language model'
},
level4: {
label: 'Using a large language model',
summary: 'Final warning: Using a large language model'
}
},
'uw-biog': {
level1: {
label: 'Adding unreferenced controversial information about living persons',
summary: 'General note: Adding unreferenced controversial information about living persons'
},
level2: {
label: 'Adding unreferenced controversial information about living persons',
summary: 'Caution: Adding unreferenced controversial information about living persons'
},
level3: {
label: 'Adding unreferenced controversial/defamatory information about living persons',
summary: 'Warning: Adding unreferenced controversial information about living persons'
},
level4: {
label: 'Adding unreferenced defamatory information about living persons',
summary: 'Final warning: Adding unreferenced controversial information about living persons'
},
level4im: {
label: 'Adding unreferenced defamatory information about living persons',
summary: 'Only warning: Adding unreferenced controversial information about living persons'
}
},
'uw-defamatory': {
level1: {
label: 'Addition of defamatory content',
summary: 'General note: Addition of defamatory content'
},
level2: {
label: 'Addition of defamatory content',
summary: 'Caution: Addition of defamatory content'
},
level3: {
label: 'Addition of defamatory content',
summary: 'Warning: Addition of defamatory content'
},
level4: {
label: 'Addition of defamatory content',
summary: 'Final warning: Addition of defamatory content'
},
level4im: {
label: 'Addition of defamatory content',
summary: 'Only warning: Addition of defamatory content'
}
},
'uw-error': {
level1: {
label: 'Introducing deliberate factual errors',
summary: 'General note: Introducing factual errors'
},
level2: {
label: 'Introducing deliberate factual errors',
summary: 'Caution: Introducing factual errors'
},
level3: {
label: 'Introducing deliberate factual errors',
summary: 'Warning: Introducing deliberate factual errors'
},
level4: {
label: 'Introducing deliberate factual errors',
summary: 'Final warning: Introducing deliberate factual errors'
}
},
'uw-fringe': {
level1: {
label: 'Introducing fringe theories',
summary: 'General note: Introducing fringe theories'
},
level2: {
label: 'Introducing fringe theories',
summary: 'Caution: Introducing fringe theories'
},
level3: {
label: 'Introducing fringe theories',
summary: 'Warning: Introducing fringe theories'
}
},
'uw-genre': {
level1: {
label: 'Frequent or mass changes to genres without consensus or references',
summary: 'General note: Frequent or mass changes to genres without consensus or references'
},
level2: {
label: 'Frequent or mass changes to genres without consensus or references',
summary: 'Caution: Frequent or mass changes to genres without consensus or references'
},
level3: {
label: 'Frequent or mass changes to genres without consensus or reference',
summary: 'Warning: Frequent or mass changes to genres without consensus or reference'
},
level4: {
label: 'Frequent or mass changes to genres without consensus or reference',
summary: 'Final warning: Frequent or mass changes to genres without consensus or reference'
}
},
'uw-image': {
level1: {
label: 'Inappropriate images in articles',
summary: 'General note: Inappropriate images in articles'
},
level2: {
label: 'Image-related vandalism in articles',
summary: 'Caution: Image-related vandalism in articles'
},
level3: {
label: 'Image-related vandalism in articles',
summary: 'Warning: Image-related vandalism in articles'
},
level4: {
label: 'Image-related vandalism in articles',
summary: 'Final warning: Image-related vandalism in articles'
},
level4im: {
label: 'Image-related vandalism',
summary: 'Only warning: Image-related vandalism'
}
},
'uw-joke': {
level1: {
label: 'Using improper humor in articles',
summary: 'General note: Using improper humor in articles'
},
level2: {
label: 'Using improper humor in articles',
summary: 'Caution: Using improper humor in articles'
},
level3: {
label: 'Using improper humor in articles',
summary: 'Warning: Using improper humor in articles'
},
level4: {
label: 'Using improper humor in articles',
summary: 'Final warning: Using improper humor in articles'
},
level4im: {
label: 'Using improper humor',
summary: 'Only warning: Using improper humor'
}
},
'uw-nor': {
level1: {
label: 'Adding original research',
summary: 'General note: Adding original research'
},
level2: {
label: 'Adding original research',
summary: 'Caution: Adding original research'
},
level3: {
label: 'Adding original research',
summary: 'Warning: Adding original research'
},
level4: {
label: 'Adding original research',
summary: 'Final warning: Adding original research'
}
},
'uw-notcensored': {
level1: {
label: 'Censorship of material',
summary: 'General note: Censorship of material'
},
level2: {
label: 'Censorship of material',
summary: 'Caution: Censorship of material'
},
level3: {
label: 'Censorship of material',
summary: 'Warning: Censorship of material'
}
},
'uw-own': {
level1: {
label: 'Ownership of articles',
summary: 'General note: Ownership of articles'
},
level2: {
label: 'Ownership of articles',
summary: 'Caution: Ownership of articles'
},
level3: {
label: 'Ownership of articles',
summary: 'Warning: Ownership of articles'
},
level4: {
label: 'Ownership of articles',
summary: 'Final warning: Ownership of articles'
},
level4im: {
label: 'Ownership of articles',
summary: 'Only warning: Ownership of articles'
}
},
'uw-pronouns': {
level1: {
label: 'Introducing incorrect pronouns',
summary: 'General note: Introducing incorrect pronouns'
},
level2: {
label: 'Introducing incorrect pronouns',
summary: 'Caution: Introducing incorrect pronouns'
},
level3: {
label: 'Introducing incorrect pronouns',
summary: 'Warning: Introducing incorrect pronouns'
}
},
'uw-subtle': {
level1: {
label: 'Subtle vandalism',
summary: 'General note: Possible unconstructive editing'
},
level2: {
label: 'Subtle vandalism',
summary: 'Caution: Likely unconstructive editing'
},
level3: {
label: 'Subtle vandalism',
summary: 'Warning: Subtle vandalism'
},
level4: {
label: 'Subtle vandalism',
summary: 'Final warning: Subtle vandalism'
}
},
'uw-talkinarticle': {
level1: {
label: 'Adding commentary to an article',
summary: 'General note: Adding commentary to an article'
},
level2: {
label: 'Adding commentary to an article',
summary: 'Caution: Adding commentary to an article'
},
level3: {
label: 'Adding commentary to an article',
summary: 'Warning: Adding commentary to an article'
}
},
'uw-tdel': {
level1: {
label: 'Removal of maintenance templates',
summary: 'General note: Removal of maintenance templates'
},
level2: {
label: 'Removal of maintenance templates',
summary: 'Caution: Removal of maintenance templates'
},
level3: {
label: 'Removal of maintenance templates',
summary: 'Warning: Removal of maintenance templates'
},
level4: {
label: 'Removal of maintenance templates',
summary: 'Final warning: Removal of maintenance templates'
}
},
'uw-unsourced': {
level1: {
label: 'Addition of unsourced or improperly cited material',
summary: 'General note: Addition of unsourced or improperly cited material'
},
level2: {
label: 'Addition of unsourced or improperly cited material',
summary: 'Caution: Addition of unsourced or improperly cited material'
},
level3: {
label: 'Addition of unsourced or improperly cited material',
summary: 'Warning: Addition of unsourced or improperly cited material'
},
level4: {
label: 'Addition of unsourced or improperly cited material',
summary: 'Final warning: Addition of unsourced or improperly cited material'
}
}
},
'Promotions and spam': {
'uw-advert': {
level1: {
label: 'Using Wikipedia for advertising or promotion',
summary: 'General note: Using Wikipedia for advertising or promotion'
},
level2: {
label: 'Using Wikipedia for advertising or promotion',
summary: 'Caution: Using Wikipedia for advertising or promotion'
},
level3: {
label: 'Using Wikipedia for advertising or promotion',
summary: 'Warning: Using Wikipedia for advertising or promotion'
},
level4: {
label: 'Using Wikipedia for advertising or promotion',
summary: 'Final warning: Using Wikipedia for advertising or promotion'
},
level4im: {
label: 'Using Wikipedia for advertising or promotion',
summary: 'Only warning: Using Wikipedia for advertising or promotion'
}
},
'uw-npov': {
level1: {
label: 'Not adhering to neutral point of view',
summary: 'General note: Not adhering to neutral point of view'
},
level2: {
label: 'Not adhering to neutral point of view',
summary: 'Caution: Not adhering to neutral point of view'
},
level3: {
label: 'Not adhering to neutral point of view',
summary: 'Warning: Not adhering to neutral point of view'
},
level4: {
label: 'Not adhering to neutral point of view',
summary: 'Final warning: Not adhering to neutral point of view'
}
},
'uw-paid': {
level1: {
label: 'Paid editing without disclosure under the Wikimedia Terms of Use',
summary: 'General note: Disclosure requirements for paid editing under the Wikimedia Terms of Use'
},
level2: {
label: 'Paid editing without disclosure under the Wikimedia Terms of Use',
summary: 'Caution: Disclosure requirements for paid editing under the Wikimedia Terms of Use'
},
level3: {
label: 'Paid editing without disclosure under the Wikimedia Terms of Use',
summary: 'Warning: Disclosure requirements for paid editing under the Wikimedia Terms of Use'
},
level4: {
label: 'Paid editing without disclosure under the Wikimedia Terms of Use',
summary: 'Final warning: Disclosure requirements for paid editing under the Wikimedia Terms of Use'
}
},
'uw-spam': {
level1: {
label: 'Adding inappropriate external links',
summary: 'General note: Adding inappropriate external links'
},
level2: {
label: 'Adding spam links',
summary: 'Caution: Adding spam links'
},
level3: {
label: 'Adding spam links',
summary: 'Warning: Adding spam links'
},
level4: {
label: 'Adding spam links',
summary: 'Final warning: Adding spam links'
},
level4im: {
label: 'Adding spam links',
summary: 'Only warning: Adding spam links'
}
}
},
'Behavior towards other editors': {
'uw-agf': {
level1: {
label: 'Not assuming good faith',
summary: 'General note: Not assuming good faith'
},
level2: {
label: 'Not assuming good faith',
summary: 'Caution: Not assuming good faith'
},
level3: {
label: 'Not assuming good faith',
summary: 'Warning: Not assuming good faith'
}
},
'uw-aitalk': {
level1: {
label: 'Posting LLM-generated comments',
summary: 'General note: Posting LLM-generated comments'
},
level2: {
label: 'Posting LLM-generated comments',
summary: 'Caution: Posting LLM-generated comments'
},
level3: {
label: 'Posting LLM-generated comments',
summary: 'Warning: Posting LLM-generated comments'
},
level4: {
label: 'Posting LLM-generated comments',
summary: 'Final warning: Posting LLM-generated comments'
}
},
'uw-harass': {
level1: {
label: 'Harassment of other users',
summary: 'General note: Harassment of other users'
},
level2: {
label: 'Harassment of other users',
summary: 'Caution: Harassment of other users'
},
level3: {
label: 'Harassment of other users',
summary: 'Warning: Harassment of other users'
},
level4: {
label: 'Harassment of other users',
summary: 'Final warning: Harassment of other users'
},
level4im: {
label: 'Harassment of other users',
summary: 'Only warning: Harassment of other users'
}
},
'uw-npa': {
level1: {
label: 'Personal attack directed at a specific editor',
summary: 'General note: Personal attack directed at a specific editor'
},
level2: {
label: 'Personal attack directed at a specific editor',
summary: 'Caution: Personal attack directed at a specific editor'
},
level3: {
label: 'Personal attack directed at a specific editor',
summary: 'Warning: Personal attack directed at a specific editor'
},
level4: {
label: 'Personal attack directed at a specific editor',
summary: 'Final warning: Personal attack directed at a specific editor'
},
level4im: {
label: 'Personal attack directed at a specific editor',
summary: 'Only warning: Personal attack directed at a specific editor'
}
},
'uw-tempabuse': {
level1: {
label: 'Improper use of warning or blocking template',
summary: 'General note: Improper use of warning or blocking template'
},
level2: {
label: 'Improper use of warning or blocking template',
summary: 'Caution: Improper use of warning or blocking template'
}
}
},
'Removal of deletion tags': {
'uw-afd': {
level1: {
label: 'Removing {{afd}} templates',
summary: 'General note: Removing {{afd}} templates'
},
level2: {
label: 'Removing {{afd}} templates',
summary: 'Caution: Removing {{afd}} templates'
},
level3: {
label: 'Removing {{afd}} templates',
summary: 'Warning: Removing {{afd}} templates'
},
level4: {
label: 'Removing {{afd}} templates',
summary: 'Final warning: Removing {{afd}} templates'
}
},
'uw-blpprod': {
level1: {
label: 'Removing {{blp prod}} templates',
summary: 'General note: Removing {{blp prod}} templates'
},
level2: {
label: 'Removing {{blp prod}} templates',
summary: 'Caution: Removing {{blp prod}} templates'
},
level3: {
label: 'Removing {{blp prod}} templates',
summary: 'Warning: Removing {{blp prod}} templates'
},
level4: {
label: 'Removing {{blp prod}} templates',
summary: 'Final warning: Removing {{blp prod}} templates'
}
},
'uw-idt': {
level1: {
label: 'Removing file deletion tags',
summary: 'General note: Removing file deletion tags'
},
level2: {
label: 'Removing file deletion tags',
summary: 'Caution: Removing file deletion tags'
},
level3: {
label: 'Removing file deletion tags',
summary: 'Warning: Removing file deletion tags'
},
level4: {
label: 'Removing file deletion tags',
summary: 'Final warning: Removing file deletion tags'
}
},
'uw-rfd': {
level1: {
label: 'Removing redirects for discussion tags',
summary: 'General note: Removing redirects for discussion tags'
},
level2: {
label: 'Removing redirects for discussion tags',
summary: 'Caution: Removing redirects for discussion tags'
},
level3: {
label: 'Removing redirects for discussion tags',
summary: 'Warning: Removing redirects for discussion tags'
},
level4: {
label: 'Removing redirects for discussion tags',
summary: 'Final warning: Removing redirects for discussion tags'
}
},
'uw-speedy': {
level1: {
label: 'Removing speedy deletion tags',
summary: 'General note: Removing speedy deletion tags'
},
level2: {
label: 'Removing speedy deletion tags',
summary: 'Caution: Removing speedy deletion tags'
},
level3: {
label: 'Removing speedy deletion tags',
summary: 'Warning: Removing speedy deletion tags'
},
level4: {
label: 'Removing speedy deletion tags',
summary: 'Final warning: Removing speedy deletion tags'
}
},
'uw-tfd': {
level1: {
label: 'Removing {{tfd}} templates',
summary: 'General note: Removing {{tfd}} templates'
},
level2: {
label: 'Removing {{tfd}} templates',
summary: 'Caution: Removing {{tfd}} templates'
},
level3: {
label: 'Removing {{tfd}} templates',
summary: 'Warning: Removing {{tfd}} templates'
},
level4: {
label: 'Removing {{tfd}} templates',
summary: 'Final warning: Removing {{tfd}} templates'
}
}
},
Other: {
'uw-attempt': {
level1: {
label: 'Triggering the edit filter',
summary: 'General note: Triggering the edit filter'
},
level2: {
label: 'Triggering the edit filter',
summary: 'Caution: Triggering the edit filter'
},
level3: {
label: 'Triggering the edit filter',
summary: 'Warning: Triggering the edit filter'
},
level4: {
label: 'Triggering the edit filter',
summary: 'Final warning: Triggering the edit filter'
},
level4im: {
label: 'Triggering the edit filter',
summary: 'Only warning: Triggering the edit filter'
}
},
'uw-chat': {
level1: {
label: 'Using talk page as forum',
summary: 'General note: Using talk page as forum'
},
level2: {
label: 'Using talk page as forum',
summary: 'Caution: Using talk page as forum'
},
level3: {
label: 'Using talk page as forum',
summary: 'Warning: Using talk page as forum'
},
level4: {
label: 'Using talk page as forum',
summary: 'Final warning: Using talk page as forum'
}
},
'uw-create': {
level1: {
label: 'Creating inappropriate pages',
summary: 'General note: Creating inappropriate pages'
},
level2: {
label: 'Creating inappropriate pages',
summary: 'Caution: Creating inappropriate pages'
},
level3: {
label: 'Creating inappropriate pages',
summary: 'Warning: Creating inappropriate pages'
},
level4: {
label: 'Creating inappropriate pages',
summary: 'Final warning: Creating inappropriate pages'
},
level4im: {
label: 'Creating inappropriate pages',
summary: 'Only warning: Creating inappropriate pages'
}
},
'uw-fv': {
level1: {
label: 'Added statement had source, but it did not verify content',
summary: 'General note: Added statement had source, but it did not verify content'
}
},
'uw-mislead': {
level1: {
label: 'Using misleading edit summaries',
summary: 'General note: Using misleading edit summaries'
},
level2: {
label: 'Using misleading edit summaries',
summary: 'Caution: Using misleading edit summaries'
},
level3: {
label: 'Using misleading edit summaries',
summary: 'Warning: Using misleading edit summaries'
}
},
'uw-mos': {
level1: {
label: 'Manual of style',
summary: 'General note: Formatting, date, language, etc (Manual of style)'
},
level2: {
label: 'Manual of style',
summary: 'Caution: Formatting, date, language, etc (Manual of style)'
},
level3: {
label: 'Manual of style',
summary: 'Warning: Formatting, date, language, etc (Manual of style)'
},
level4: {
label: 'Manual of style',
summary: 'Final warning: Formatting, date, language, etc (Manual of style)'
}
},
'uw-move': {
level1: {
label: 'Page moves against naming conventions or consensus',
summary: 'General note: Page moves against naming conventions or consensus'
},
level2: {
label: 'Page moves against naming conventions or consensus',
summary: 'Caution: Page moves against naming conventions or consensus'
},
level3: {
label: 'Page moves against naming conventions or consensus',
summary: 'Warning: Page moves against naming conventions or consensus'
},
level4: {
label: 'Page moves against naming conventions or consensus',
summary: 'Final warning: Page moves against naming conventions or consensus'
},
level4im: {
label: 'Page moves against naming conventions or consensus',
summary: 'Only warning: Page moves against naming conventions or consensus'
}
},
'uw-redirect': {
level1: {
label: 'Creating inappropriate redirects',
summary: 'General note: Creating inappropriate redirects'
},
level2: {
label: 'Creating inappropriate redirects',
summary: 'Caution: Creating inappropriate redirects'
},
level3: {
label: 'Creating inappropriate redirects',
summary: 'Warning: Creating inappropriate redirects'
},
level4: {
label: 'Creating inappropriate redirects',
summary: 'Final warning: Creating inappropriate redirects'
},
level4im: {
label: 'Creating inappropriate redirects',
summary: 'Only warning: Creating inappropriate redirects'
}
},
'uw-tpv': {
level1: {
label: "Refactoring others' talk page comments",
summary: "General note: Refactoring others' talk page comments"
},
level2: {
label: "Refactoring others' talk page comments",
summary: "Caution: Refactoring others' talk page comments"
},
level3: {
label: "Refactoring others' talk page comments",
summary: "Warning: Refactoring others' talk page comments"
},
level4: {
label: "Refactoring others' talk page comments",
summary: "Final warning: Refactoring others' talk page comments"
},
level4im: {
label: "Refactoring others' talk page comments",
summary: "Only warning: Refactoring others' talk page comments"
}
},
'uw-upload': {
level1: {
label: 'Uploading unencyclopedic images',
summary: 'General note: Uploading unencyclopedic images'
},
level2: {
label: 'Uploading unencyclopedic images',
summary: 'Caution: Uploading unencyclopedic images'
},
level3: {
label: 'Uploading unencyclopedic images',
summary: 'Warning: Uploading unencyclopedic images'
},
level4: {
label: 'Uploading unencyclopedic images',
summary: 'Final warning: Uploading unencyclopedic images'
},
level4im: {
label: 'Uploading unencyclopedic images',
summary: 'Only warning: Uploading unencyclopedic images'
}
}
}
},
singlenotice: {
'uw-addalink': {
label: 'Mistakes with the Add a Link newcomer task',
summary: 'Notice: Mistakes with the Add a Link newcomer task'
},
'uw-agf-sock': {
label: 'Use of multiple accounts (assuming good faith)',
summary: 'Notice: Using multiple accounts'
},
'uw-aiv': {
label: 'Bad AIV report',
summary: 'Notice: Bad AIV report'
},
'uw-articletodraft': {
label: 'Article moved to draftspace',
summary: 'Notice: Article moved to draftspace',
hideReason: true
},
'uw-autobiography': {
label: 'Creating autobiographies',
summary: 'Notice: Creating autobiographies'
},
'uw-badcat': {
label: 'Adding incorrect categories',
summary: 'Notice: Adding incorrect categories'
},
'uw-badlistentry': {
label: 'Adding inappropriate entries to lists',
summary: 'Notice: Adding inappropriate entries to lists'
},
'uw-bareurl': {
label: 'Adding a bare URL',
summary: 'Notice: Adding a bare URL'
},
'uw-bite': {
label: '"Biting" newcomers',
summary: 'Notice: "Biting" newcomers',
suppressArticleInSummary: true // non-standard (user name, not article), and not necessary
},
'uw-blar': {
label: 'Article blanked and redirected',
summary: 'Notice: Article blanked and redirected',
hideReason: true
},
'uw-circular': {
label: 'Using circular sources',
summary: 'Notice: Using circular sources'
},
'uw-coi': {
label: 'Conflict of interest',
summary: 'Notice: Conflict of interest',
heading: 'Managing a conflict of interest'
},
'uw-copying': {
label: 'Copying text to another page',
summary: 'Notice: Copying text to another page'
},
'uw-crystal': {
label: 'Adding speculative or unconfirmed information',
summary: 'Notice: Adding speculative or unconfirmed information'
},
'uw-c&pmove': {
label: 'Cut and paste moves',
summary: 'Notice: Cut and paste moves'
},
'uw-dab': {
label: 'Incorrect edit to a disambiguation page',
summary: 'Notice: Incorrect edit to a disambiguation page'
},
'uw-date': {
label: 'Unnecessarily changing date formats',
summary: 'Notice: Unnecessarily changing date formats'
},
'uw-deadlink': {
label: 'Removing proper sources containing dead links',
summary: 'Notice: Removing proper sources containing dead links'
},
'uw-displaytitle': {
label: 'Incorrect use of DISPLAYTITLE',
summary: 'Notice: Incorrect use of DISPLAYTITLE'
},
'uw-draftfirst': {
label: 'User should draft in userspace without the risk of speedy deletion',
summary: 'Notice: Consider drafting your article in [[Help:Userspace draft|userspace]]'
},
'uw-editsummary': {
label: 'New user not using edit summary',
summary: 'Notice: Not using edit summary'
},
'uw-editsummary2': {
label: 'Experienced user not using edit summary',
summary: 'Notice: Not using edit summary',
hideLinkedPage: true,
hideReason: true
},
'uw-elinbody': {
label: 'Adding external links to the body of an article',
summary: 'Notice: Keep external links to External links sections at the bottom of an article'
},
'uw-english': {
label: 'Not communicating in English',
summary: 'Notice: Not communicating in English'
},
'uw-hasty': {
label: 'Hasty addition of speedy deletion tags',
summary: 'Notice: Allow creators time to improve their articles before tagging them for deletion'
},
'uw-islamhon': {
label: 'Use of Islamic honorifics',
summary: 'Notice: Use of Islamic honorifics'
},
'uw-italicize': {
label: 'Italicize books, films, albums, magazines, TV series, etc within articles',
summary: 'Notice: Italicize books, films, albums, magazines, TV series, etc within articles'
},
'uw-lang': {
label: 'Unnecessarily changing between British and American English',
summary: 'Notice: Unnecessarily changing between British and American English',
heading: 'National varieties of English'
},
'uw-linking': {
label: 'Excessive addition of redlinks or repeated blue links',
summary: 'Notice: Excessive addition of redlinks or repeated blue links'
},
'uw-longsd': {
label: 'Insertion of long short description',
summary: 'Notice: Insertion of long short description'
},
'uw-minor': {
label: 'Incorrect use of minor edits check box',
summary: 'Notice: Incorrect use of minor edits check box'
},
'uw-mostm': {
label: 'Formatting of trademarks',
summary: 'Notice: Formatting of trademarks'
},
'uw-multiple-accts': {
label: 'Inappropriate use of alternative accounts',
summary: 'Notice: Inappropriate use of alternative accounts'
},
'uw-notenglish': {
label: 'Creating non-English articles',
summary: 'Notice: Creating non-English articles'
},
'uw-notenglishedit': {
label: 'Adding non-English content to articles',
summary: 'Notice: Adding non-English content to articles'
},
'uw-notvote': {
label: 'We use consensus, not voting',
summary: 'Notice: We use consensus, not voting'
},
'uw-orphantalk': {
label: 'Talk page created with no article',
summary: 'Notice: Talk page created with no article'
},
'uw-plagiarism': {
label: 'Copying from public domain sources without attribution',
summary: 'Notice: Copying from public domain sources without attribution'
},
'uw-preview': {
label: 'Use preview button to avoid mistakes',
summary: 'Notice: Use preview button to avoid mistakes'
},
'uw-redlink': {
label: 'Indiscriminate removal of redlinks',
summary: 'Notice: Be careful when removing redlinks'
},
'uw-refspam': {
label: 'Adding citations to research published by a small group of researchers',
summary: 'Notice: Adding citations to research published by a small group of researchers',
hideLinkedPage: true,
hideReason: true
},
'uw-selfrevert': {
label: 'Self-reverted editing tests',
summary: 'Notice: Self-reverted editing tests'
},
'uw-socialnetwork': {
label: 'Wikipedia is not a social network',
summary: 'Notice: Wikipedia is not a social network'
},
'uw-sofixit': {
label: 'Be bold and fix things yourself',
summary: 'Notice: You can be bold and fix things yourself'
},
'uw-spoiler': {
label: 'Adding spoiler alerts or removing spoilers from appropriate sections',
summary: "Notice: Don't delete or flag potential 'spoilers' in Wikipedia articles"
},
'uw-talkinarticle': {
label: 'Talk in article',
summary: 'Notice: Talk in article'
},
'uw-tilde': {
label: 'Not signing posts',
summary: 'Notice: Not signing posts'
},
'uw-toppost': {
label: 'Posting at the top of talk pages',
summary: 'Notice: Posting at the top of talk pages'
},
'uw-translation': {
label: 'Adding translations without proper attribution',
summary: 'Notice: Attribution required when translating articles'
},
'uw-unattribcc': {
label: 'Copying from compatibly-licensed sources without attribution',
summary: 'Notice: Copying from compatibly-licensed sources without attribution'
},
'uw-userspace draft finish': {
label: 'Stale userspace draft',
summary: 'Notice: Stale userspace draft'
},
'uw-usertalk': {
label: 'Misuse of user talk page',
summary: 'Notice: Misuse of user talk page',
hideLinkedPage: true
},
'uw-vgscope': {
label: 'Adding video game walkthroughs, cheats or instructions',
summary: 'Notice: Adding video game walkthroughs, cheats or instructions'
},
'uw-warn': {
label: 'Place user warning templates when reverting vandalism',
summary: 'Notice: You can use user warning templates when reverting vandalism'
},
'uw-wrongsummary': {
label: 'Using inaccurate or inappropriate edit summaries',
summary: 'Notice: Using inaccurate or inappropriate edit summaries'
}
},
singlewarn: {
'uw-3rr': {
label: 'Potential three-revert rule violation; see also uw-ew',
summary: 'Warning: Three-revert rule'
},
'uw-affiliate': {
label: 'Affiliate marketing',
summary: 'Warning: Affiliate marketing'
},
'uw-attack': {
label: 'Creating attack pages',
summary: 'Warning: Creating attack pages',
suppressArticleInSummary: true
},
'uw-botun': {
label: 'Bot username',
summary: 'Warning: Bot username'
},
'uw-canvass': {
label: 'Canvassing',
summary: 'Warning: Canvassing'
},
'uw-copyright': {
label: 'Copyright violation',
summary: 'Warning: Copyright violation'
},
'uw-copyright-link': {
label: 'Linking to copyrighted works violation',
summary: 'Warning: Linking to copyrighted works violation'
},
'uw-copyright-new': {
label: 'Copyright violation (with explanation for new users)',
summary: 'Notice: Avoiding copyright problems',
heading: 'Wikipedia and copyright'
},
'uw-copyright-remove': {
label: 'Removing {{copyvio}} template from articles',
summary: 'Warning: Removing {{copyvio}} templates'
},
'uw-derogatory': {
label: 'Addition of derogatory/hateful content',
summary: 'Warning: Addition of derogatory content'
},
'uw-efsummary': {
label: 'Edit summary triggering the edit filter',
summary: 'Warning: Edit summary triggering the edit filter'
},
'uw-ew': {
label: 'Edit warring (stronger wording)',
summary: 'Warning: Edit warring'
},
'uw-ewsoft': {
label: 'Edit warring (softer wording for newcomers)',
summary: 'Warning: Edit warring'
},
'uw-hijacking': {
label: 'Hijacking articles',
summary: 'Warning: Hijacking articles'
},
'uw-hoax': {
label: 'Creating hoaxes',
summary: 'Warning: Creating hoaxes'
},
'uw-legal': {
label: 'Making legal threats',
summary: 'Warning: Making legal threats'
},
'uw-login': {
label: 'Editing while logged out',
summary: 'Warning: Editing while logged out'
},
'uw-multipleTAs': {
label: 'Usage of multiple temporary accounts',
summary: 'Warning: Vandalism using multiple temporary accounts'
},
'uw-paraphrase': {
label: 'Close paraphrasing',
summary: 'Warning: Close paraphrasing'
},
'uw-pinfo': {
label: 'Personal info (outing)',
summary: 'Warning: Personal info'
},
'uw-salt': {
label: 'Recreating salted articles under a different title',
summary: 'Notice: Recreating creation-protected articles under a different title'
},
'uw-socksuspect': {
label: 'Sockpuppetry',
summary: 'Warning: You are a suspected [[WP:SOCK|sockpuppet]]' // of User:...
},
'uw-upv': {
label: 'Userpage vandalism',
summary: 'Warning: Userpage vandalism'
},
'uw-username': {
label: 'Username is against policy',
summary: 'Warning: Your username might be against policy',
suppressArticleInSummary: true // not relevant for this template
},
'uw-coi-username': {
label: 'Username is against policy, and conflict of interest',
summary: 'Warning: Username and conflict of interest',
heading: 'Your username'
},
'uw-userpage': {
label: 'Userpage or subpage is against policy',
summary: 'Warning: Userpage or subpage is against policy'
}
}
};
/**
* Reads Twinkle.warn.messages and returns a specified template's property (such as label, summary,
* suppressArticleInSummary, hideLinkedPage, or hideReason)
*/
Twinkle.warn.getTemplateProperty = function(templates, templateName, propertyName) {
let result;
const isNumberedTemplate = templateName.match(/(1|2|3|4|4im)$/);
if (isNumberedTemplate) {
const unNumberedTemplateName = templateName.replace(/(?:1|2|3|4|4im)$/, '');
const level = isNumberedTemplate[0];
const numberedWarnings = {};
$.each(templates.levels, (key, val) => {
$.extend(numberedWarnings, val);
});
$.each(numberedWarnings, (key) => {
if (key === unNumberedTemplateName) {
result = numberedWarnings[key]['level' + level][propertyName];
}
});
}
// Non-level templates can also end in a number. So check this for all templates.
const otherWarnings = {};
$.each(templates, (key, val) => {
if (key !== 'levels') {
$.extend(otherWarnings, val);
}
});
$.each(otherWarnings, (key) => {
if (key === templateName) {
result = otherWarnings[key][propertyName];
}
});
return result;
};
// Used repeatedly below across menu rebuilds
Twinkle.warn.prev_article = null;
Twinkle.warn.prev_reason = null;
Twinkle.warn.talkpageObj = null;
Twinkle.warn.callback.change_category = function twinklewarnCallbackChangeCategory(e) {
const value = e.target.value;
const sub_group = e.target.root.sub_group;
sub_group.main_group = value;
let old_subvalue = sub_group.value;
let old_subvalue_re;
if (old_subvalue) {
if (value === 'kitchensink') { // Exact match possible in kitchensink menu
old_subvalue_re = new RegExp(mw.util.escapeRegExp(old_subvalue));
} else {
old_subvalue = old_subvalue.replace(/\d*(im)?$/, '');
old_subvalue_re = new RegExp(mw.util.escapeRegExp(old_subvalue) + '(\\d*(?:im)?)$');
}
}
while (sub_group.hasChildNodes()) {
sub_group.removeChild(sub_group.firstChild);
}
let selected = false;
// worker function to create the combo box entries
const createEntries = function(contents, container, wrapInOptgroup, val = value) {
// level2->2, singlewarn->''; also used to distinguish the
// scaled levels from singlenotice, singlewarn, and custom
const level = val.replace(/^\D+/g, '');
// due to an apparent iOS bug, we have to add an option-group to prevent truncation of text
// (search WT:TW archives for "Problem selecting warnings on an iPhone")
if (wrapInOptgroup && $.client.profile().platform === 'iphone') {
let wrapperOptgroup = new Morebits.QuickForm.Element({
type: 'optgroup',
label: 'Available templates'
});
wrapperOptgroup = wrapperOptgroup.render();
container.appendChild(wrapperOptgroup);
container = wrapperOptgroup;
}
$.each(contents, (itemKey, itemProperties) => {
// Skip if the current template doesn't have a version for the current level
if (!!level && !itemProperties[val]) {
return;
}
const key = typeof itemKey === 'string' ? itemKey : itemProperties.value;
const template = key + level;
const elem = new Morebits.QuickForm.Element({
type: 'option',
label: '{{' + template + '}}: ' + (level ? itemProperties[val].label : itemProperties.label),
value: template
});
// Select item best corresponding to previous selection
if (!selected && old_subvalue && old_subvalue_re.test(template)) {
elem.data.selected = selected = true;
}
const elemRendered = container.appendChild(elem.render());
$(elemRendered).data('messageData', itemProperties);
});
};
const createGroup = function(warnGroup, label, wrapInOptgroup, val) {
wrapInOptgroup = typeof wrapInOptgroup !== 'undefined' ? wrapInOptgroup : true;
let optgroup = new Morebits.QuickForm.Element({
type: 'optgroup',
label: label
});
optgroup = optgroup.render();
sub_group.appendChild(optgroup);
createEntries(warnGroup, optgroup, wrapInOptgroup, val);
};
switch (value) {
case 'singlenotice':
case 'singlewarn':
createEntries(Twinkle.warn.messages[value], sub_group, true);
break;
case 'singlecombined':
var unSortedSinglets = $.extend({}, Twinkle.warn.messages.singlenotice, Twinkle.warn.messages.singlewarn);
var sortedSingletMessages = {};
Object.keys(unSortedSinglets).sort().forEach((key) => {
sortedSingletMessages[key] = unSortedSinglets[key];
});
createEntries(sortedSingletMessages, sub_group, true);
break;
case 'custom':
createEntries(Twinkle.getPref('customWarningList'), sub_group, true);
break;
case 'kitchensink':
['level1', 'level2', 'level3', 'level4', 'level4im'].forEach((lvl) => {
$.each(Twinkle.warn.messages.levels, (levelGroupLabel, levelGroup) => {
createGroup(levelGroup, 'Level ' + lvl.slice(5) + ': ' + levelGroupLabel, true, lvl);
});
});
createGroup(Twinkle.warn.messages.singlenotice, 'Single-issue notices');
createGroup(Twinkle.warn.messages.singlewarn, 'Single-issue warnings');
createGroup(Twinkle.getPref('customWarningList'), 'Custom warnings');
break;
case 'level1':
case 'level2':
case 'level3':
case 'level4':
case 'level4im':
// Creates subgroup regardless of whether there is anything to place in it;
// leaves "Removal of deletion tags" empty for 4im
$.each(Twinkle.warn.messages.levels, (groupLabel, groupContents) => {
createGroup(groupContents, groupLabel, false);
});
break;
case 'autolevel':
// Check user page to determine appropriate level
var autolevelProc = function() {
const wikitext = Twinkle.warn.talkpageObj.getPageText();
// history not needed for autolevel
const latest = Twinkle.warn.callbacks.dateProcessing(wikitext)[0];
// Pseudo-params with only what's needed to parse the level i.e. no messageData
const params = {
sub_group: old_subvalue,
article: e.target.root.article.value
};
const lvl = 'level' + Twinkle.warn.callbacks.autolevelParseWikitext(wikitext, params, latest)[1];
// Identical to level1, etc. above but explicitly provides the level
$.each(Twinkle.warn.messages.levels, (groupLabel, groupContents) => {
createGroup(groupContents, groupLabel, false, lvl);
});
// Trigger subcategory change, add select menu, etc.
Twinkle.warn.callback.postCategoryCleanup(e);
};
if (Twinkle.warn.talkpageObj) {
autolevelProc();
} else {
const usertalk_page = new Morebits.wiki.Page('User_talk:' + mw.config.get('wgRelevantUserName'), 'Loading previous warnings');
usertalk_page.setFollowRedirect(true, false);
usertalk_page.load((pageobj) => {
Twinkle.warn.talkpageObj = pageobj; // Update talkpageObj
autolevelProc();
}, () => {
// Catch and warn if the talkpage can't load,
// most likely because it's a cross-namespace redirect
// Supersedes the typical $autolevelMessage added in autolevelParseWikitext
const $noTalkPageNode = $('<strong>')
.text('Unable to load user talk page; it might be a cross-namespace redirect. Autolevel detection will not work.')
.attr('id', 'twinkle-warn-autolevel-message')
.css('color', 'red');
$noTalkPageNode.insertBefore($('#twinkle-warn-warning-messages'));
// If a preview was opened while in a different mode, close it
// Should nullify the need to catch the error in preview callback
e.target.root.previewer.closePreview();
});
}
break;
default:
alert('Unknown warning group in twinklewarn');
break;
}
// Trigger subcategory change, add select menu, etc.
// Here because of the async load for autolevel
if (value !== 'autolevel') {
// reset any autolevel-specific messages while we're here
$('#twinkle-warn-autolevel-message').remove();
Twinkle.warn.callback.postCategoryCleanup(e);
}
};
Twinkle.warn.callback.postCategoryCleanup = function twinklewarnCallbackPostCategoryCleanup(e) {
// clear overridden label on article textbox
Morebits.QuickForm.setElementTooltipVisibility(e.target.root.article, true);
Morebits.QuickForm.resetElementLabel(e.target.root.article);
// Trigger custom label/change on main category change
Twinkle.warn.callback.change_subcategory(e);
// Use select2 to make the select menu searchable
if (!Twinkle.getPref('oldSelect')) {
$('select[name=sub_group]')
.select2({
theme: 'default select2-morebits',
width: '100%',
matcher: Morebits.select2.matchers.optgroupFull,
templateResult: Morebits.select2.highlightSearchMatches,
language: {
searching: Morebits.select2.queryInterceptor
}
})
.change(Twinkle.warn.callback.change_subcategory);
$('.select2-selection').on('keydown', Morebits.select2.autoStart).trigger('focus');
mw.util.addCSS(
// Increase height
'.select2-container .select2-dropdown .select2-results > .select2-results__options { max-height: 350px; }' +
// Reduce padding
'.select2-results .select2-results__option { padding-top: 1px; padding-bottom: 1px; }' +
'.select2-results .select2-results__group { padding-top: 1px; padding-bottom: 1px; } ' +
// Adjust font size
'.select2-container .select2-dropdown .select2-results { font-size: 13px; }' +
'.select2-container .selection .select2-selection__rendered { font-size: 13px; }'
);
}
};
Twinkle.warn.callback.change_subcategory = function twinklewarnCallbackChangeSubcategory(e) {
const selected_main_group = e.target.form.main_group.value;
const selected_template = e.target.form.sub_group.value;
// If template shouldn't have a linked article, hide the linked article label and text box
const hideLinkedPage = Twinkle.warn.getTemplateProperty(Twinkle.warn.messages, selected_template, 'hideLinkedPage');
if (hideLinkedPage) {
e.target.form.article.value = '';
Morebits.QuickForm.setElementVisibility(e.target.form.article.parentElement, false);
} else {
Morebits.QuickForm.setElementVisibility(e.target.form.article.parentElement, true);
}
// If template shouldn't have an optional message, hide the optional message label and text box
const hideReason = Twinkle.warn.getTemplateProperty(Twinkle.warn.messages, selected_template, 'hideReason');
if (hideReason) {
e.target.form.reason.value = '';
Morebits.QuickForm.setElementVisibility(e.target.form.reason.parentElement, false);
} else {
Morebits.QuickForm.setElementVisibility(e.target.form.reason.parentElement, true);
}
// Tags that don't take a linked article, but something else (often a username).
// The value of each tag is the label next to the input field
const notLinkedArticle = {
'uw-agf-sock': 'Optional username of other account (without User:) ',
'uw-bite': "Username of 'bitten' user (without User:) ",
'uw-socksuspect': 'Username of sock master, if known (without User:) ',
'uw-username': 'Username violates policy because... ',
'uw-aiv': 'Optional username that was reported (without User:) '
};
const hasLevel = ['singlenotice', 'singlewarn', 'singlecombined', 'kitchensink'].includes(selected_main_group);
if (hasLevel) {
if (notLinkedArticle[selected_template]) {
if (Twinkle.warn.prev_article === null) {
Twinkle.warn.prev_article = e.target.form.article.value;
}
e.target.form.article.notArticle = true;
e.target.form.article.value = '';
// change form labels according to the warning selected
Morebits.QuickForm.setElementTooltipVisibility(e.target.form.article, false);
Morebits.QuickForm.overrideElementLabel(e.target.form.article, notLinkedArticle[selected_template]);
} else if (e.target.form.article.notArticle) {
if (Twinkle.warn.prev_article !== null) {
e.target.form.article.value = Twinkle.warn.prev_article;
Twinkle.warn.prev_article = null;
}
e.target.form.article.notArticle = false;
Morebits.QuickForm.setElementTooltipVisibility(e.target.form.article, true);
Morebits.QuickForm.resetElementLabel(e.target.form.article);
}
}
// add big red notice, warning users about how to use {{uw-[coi-]username}} appropriately
$('#tw-warn-red-notice').remove();
let $redWarning;
if (selected_template === 'uw-username') {
$redWarning = $("<div style='color: red;' id='tw-warn-red-notice'>{{uw-username}} should <b>not</b> be used for <b>blatant</b> username policy violations. " +
"Blatant violations should be reported directly to UAA (via Twinkle's ARV tab). " +
'{{uw-username}} should only be used in edge cases in order to engage in discussion with the user.</div>');
$redWarning.insertAfter(Morebits.QuickForm.getElementLabelObject(e.target.form.reasonGroup));
} else if (selected_template === 'uw-coi-username') {
$redWarning = $("<div style='color: red;' id='tw-warn-red-notice'>{{uw-coi-username}} should <b>not</b> be used for <b>blatant</b> username policy violations. " +
"Blatant violations should be reported directly to UAA (via Twinkle's ARV tab). " +
'{{uw-coi-username}} should only be used in edge cases in order to engage in discussion with the user.</div>');
$redWarning.insertAfter(Morebits.QuickForm.getElementLabelObject(e.target.form.reasonGroup));
}
};
Twinkle.warn.callbacks = {
getWarningWikitext: function(templateName, article, reason, isCustom) {
let text = '{{subst:' + templateName;
// add linked article for user warnings
if (article) {
// c&pmove has the source as the first parameter
if (templateName === 'uw-c&pmove') {
text += '|to=' + article;
} else {
text += '|1=' + article;
}
}
if (reason && !isCustom) {
// add extra message
if (templateName === 'uw-userpage') {
text += "|3=''" + Morebits.string.formatReasonText(reason) + "''";
} else {
text += "|2=''" + Morebits.string.formatReasonText(reason) + "''";
}
}
text += '}}';
if (reason && isCustom) {
// we assume that custom warnings lack a {{{2}}} parameter
text += " ''" + reason + "''";
}
return text + ' ~~~~';
},
showPreview: function(form, templatename) {
const input = Morebits.QuickForm.getInputData(form);
// Provided on autolevel, not otherwise
templatename = templatename || input.sub_group;
const linkedarticle = input.article;
const templatetext = Twinkle.warn.callbacks.getWarningWikitext(templatename, linkedarticle,
input.reason, input.main_group === 'custom');
form.previewer.beginRender(templatetext, 'User_talk:' + mw.config.get('wgRelevantUserName')); // Force wikitext/correct username
},
// Just a pass-through unless the autolevel option was selected
preview: function(form) {
if (form.main_group.value === 'autolevel') {
// Always get a new, updated talkpage for autolevel processing
const usertalk_page = new Morebits.wiki.Page('User_talk:' + mw.config.get('wgRelevantUserName'), 'Loading previous warnings');
usertalk_page.setFollowRedirect(true, false);
// Will fail silently if the talk page is a cross-ns redirect,
// removal of the preview box handled when loading the menu
usertalk_page.load((pageobj) => {
Twinkle.warn.talkpageObj = pageobj; // Update talkpageObj
const wikitext = pageobj.getPageText();
// history not needed for autolevel
const latest = Twinkle.warn.callbacks.dateProcessing(wikitext)[0];
const params = {
sub_group: form.sub_group.value,
article: form.article.value,
messageData: $(form.sub_group).find('option[value="' + $(form.sub_group).val() + '"]').data('messageData')
};
const template = Twinkle.warn.callbacks.autolevelParseWikitext(wikitext, params, latest)[0];
Twinkle.warn.callbacks.showPreview(form, template);
// If the templates have diverged, fake a change event
// to reload the menu with the updated pageobj
if (form.sub_group.value !== template) {
const evt = document.createEvent('Event');
evt.initEvent('change', true, true);
form.main_group.dispatchEvent(evt);
}
});
} else {
Twinkle.warn.callbacks.showPreview(form);
}
},
/**
* Used in the main and autolevel loops to determine when to warn
* about excessively recent, stale, or identical warnings.
*
* @param {string} wikitext The text of a user's talk page, from getPageText()
* @return {Object[]} - Array of objects: latest contains most recent
* warning and date; history lists all prior warnings
*/
dateProcessing: function(wikitext) {
const history_re = /<!--\s?Template:([uU]w-.*?)\s?-->.*?(\d{1,2}:\d{1,2}, \d{1,2} \w+ \d{4} \(UTC\))/g;
const history = {};
const latest = { date: new Morebits.Date(0), type: '' };
let current;
while ((current = history_re.exec(wikitext)) !== null) {
const template = current[1], current_date = new Morebits.Date(current[2]);
if (!(template in history) || history[template].isBefore(current_date)) {
history[template] = current_date;
}
if (!latest.date.isAfter(current_date)) {
latest.date = current_date;
latest.type = template;
}
}
return [latest, history];
},
// False positive
// eslint-disable-next-line jsdoc/require-returns-check
/**
* Main loop for deciding what the level should increment to. Most of
* this is really just error catching and updating the subsequent data.
* May produce up to two notices in a twinkle-warn-autolevel-messages div
*
* @param {string} wikitext The text of a user's talk page, from getPageText() (required)
* @param {Object} params Params object: sub_group is the template (required);
* article is the user-provided article (form.article) used to link ARV on recent level4 warnings;
* messageData is only necessary if getting the full template, as it's
* used to ensure a valid template of that level exists
* @param {Object} latest First element of the array returned from
* dateProcessing. Provided here rather than processed within to avoid
* repeated call to dateProcessing
* @param {(Date|Morebits.Date)} date Date from which staleness is determined
* @param {Morebits.Status} statelem Status element, only used for handling error in final execution
*
* @return {Array} - Array that contains the full template and just the warning level
*/
autolevelParseWikitext: function(wikitext, params, latest, date, statelem) {
let level; // undefined rather than '' means the isNaN below will return true
if (/\d(?:im)?$/.test(latest.type)) { // level1-4im
level = parseInt(latest.type.replace(/.*(\d)(?:im)?$/, '$1'), 10);
} else if (latest.type) { // Non-numbered warning
// Try to leverage existing categorization of
// warnings, all but one are universally lowercased
const loweredType = /uw-multipleTAs/i.test(latest.type) ? 'uw-multipleTAs' : latest.type.toLowerCase();
// It would be nice to account for blocks, but in most
// cases the hidden message is terminal, not the sig
if (Twinkle.warn.messages.singlewarn[loweredType]) {
level = 3;
} else {
level = 1; // singlenotice or not found
}
}
const $autolevelMessage = $('<div>')
.attr('id', 'twinkle-warn-autolevel-message');
if (isNaN(level)) { // No prior warnings found, this is the first
level = 1;
} else if (level > 4 || level < 1) { // Shouldn't happen
const message = 'Unable to parse previous warning level, please manually select a warning level.';
if (statelem) {
statelem.error(message);
} else {
alert(message);
}
return;
} else {
date = date || new Date();
const autoTimeout = new Morebits.Date(latest.date.getTime()).add(parseInt(Twinkle.getPref('autolevelStaleDays'), 10), 'days');
if (autoTimeout.isAfter(date)) {
if (level === 4) {
level = 4;
// Basically indicates whether we're in the final Main evaluation or not,
// and thus whether we can continue or need to display the warning and link
if (!statelem) {
const $link = $('<a>')
.attr('href', '#')
.text('click here to open the ARV tool.')
.css('fontWeight', 'bold')
.on('click', () => {
Morebits.wiki.actionCompleted.redirect = null;
Twinkle.warn.dialog.close();
Twinkle.arv.callback(mw.config.get('wgRelevantUserName'));
$('input[name=page]').val(params.article); // Target page
$('input[value=final]').prop('checked', true); // Vandalism after final
});
const $statusNode = $('<div>')
.text(mw.config.get('wgRelevantUserName') + ' recently received a level 4 warning (' + latest.type + ') so it might be better to report them instead; ')
.css('color', 'red');
$statusNode.append($link[0]);
$autolevelMessage.append($statusNode);
}
} else { // Automatically increase severity
level += 1;
}
} else { // Reset warning level if most-recent warning is too old
level = 1;
}
}
$autolevelMessage.prepend($('<div>Will issue a <span style="font-weight: bold;">level ' + level + '</span> template.</div>'));
// Place after the stale and other-user-reverted (text-only) messages
$('#twinkle-warn-autolevel-message').remove(); // clean slate
$autolevelMessage.insertAfter($('#twinkle-warn-warning-messages'));
let template = params.sub_group.replace(/(.*)\d$/, '$1');
// Validate warning level, falling back to the uw-generic series.
// Only a few items are missing a level, and in all but a handful
// of cases, the uw-generic series is explicitly used elsewhere per WP:UTM.
if (params.messageData && !params.messageData['level' + level]) {
template = 'uw-generic';
}
template += level;
return [template, level];
},
main: function(pageobj) {
const text = pageobj.getPageText();
const statelem = pageobj.getStatusElement();
const params = pageobj.getCallbackParameters();
let messageData = params.messageData;
const [latest, history] = Twinkle.warn.callbacks.dateProcessing(text);
const now = new Morebits.Date(pageobj.getLoadTime());
Twinkle.warn.talkpageObj = pageobj; // Update talkpageObj, just in case
if (params.main_group === 'autolevel') {
// [template, level]
const templateAndLevel = Twinkle.warn.callbacks.autolevelParseWikitext(text, params, latest, now, statelem);
// Only if there's a change from the prior display/load
if (params.sub_group !== templateAndLevel[0] && !confirm('Will issue a {{' + templateAndLevel[0] + '}} template to the user, okay?')) {
statelem.error('aborted per user request');
return;
}
// Update params now that we've selected a warning
params.sub_group = templateAndLevel[0];
messageData = params.messageData['level' + templateAndLevel[1]];
} else if (params.sub_group in history) {
if (new Morebits.Date(history[params.sub_group]).add(1, 'day').isAfter(now)) {
if (!confirm('An identical ' + params.sub_group + ' has been issued in the last 24 hours. \nWould you still like to add this warning/notice?')) {
statelem.error('aborted per user request');
return;
}
}
}
latest.date.add(1, 'minute'); // after long debate, one minute is max
if (latest.date.isAfter(now)) {
if (!confirm('A ' + latest.type + ' has been issued in the last minute. \nWould you still like to add this warning/notice?')) {
statelem.error('aborted per user request');
return;
}
}
// build the edit summary
// Function to handle generation of summary prefix for custom templates
const customProcess = function(template) {
template = template.split('|')[0];
let prefix;
switch (template.slice(-1)) {
case '1':
prefix = 'General note';
break;
case '2':
prefix = 'Caution';
break;
case '3':
prefix = 'Warning';
break;
case '4':
prefix = 'Final warning';
break;
case 'm':
if (template.slice(-3) === '4im') {
prefix = 'Only warning';
break;
}
// falls through
default:
prefix = 'Notice';
break;
}
return prefix + ': ' + Morebits.string.toUpperCaseFirstChar(messageData.label);
};
let summary;
if (params.main_group === 'custom') {
summary = customProcess(params.sub_group);
} else {
// Normalize kitchensink to the 1-4im style
if (params.main_group === 'kitchensink' && !/^D+$/.test(params.sub_group)) {
let sub = params.sub_group.slice(-1);
if (sub === 'm') {
sub = params.sub_group.slice(-3);
}
// Don't overwrite uw-3rr, technically unnecessary
if (/\d/.test(sub)) {
params.main_group = 'level' + sub;
}
}
// singlet || level1-4im, no need to /^\D+$/.test(params.main_group)
summary = messageData.summary || (messageData[params.main_group] && messageData[params.main_group].summary);
// Not in Twinkle.warn.messages, assume custom template
if (!summary) {
summary = customProcess(params.sub_group);
}
if (messageData.suppressArticleInSummary !== true && params.article) {
if (params.sub_group === 'uw-agf-sock' ||
params.sub_group === 'uw-socksuspect' ||
params.sub_group === 'uw-aiv') { // these templates require a username
summary += ' of [[:User:' + params.article + ']]';
} else {
summary += ' on [[:' + params.article + ']]';
}
}
}
pageobj.setEditSummary(summary + '.');
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.setWatchlist(Twinkle.getPref('watchWarnings'));
// Get actual warning text
const warningText = Twinkle.warn.callbacks.getWarningWikitext(params.sub_group, params.article,
params.reason, params.main_group === 'custom');
let sectionExists = false, sectionNumber = 0;
// Only check sections if there are sections or there's a chance we won't create our own
if (!messageData.heading && text.length) {
// Get all sections
const sections = text.match(/^(==*).+\1/gm);
if (sections && sections.length !== 0) {
// Find the index of the section header in question
const dateHeaderRegex = now.monthHeaderRegex();
sectionNumber = 0;
// Find this month's section among L2 sections, preferring the bottom-most
sectionExists = sections.reverse().some((sec, idx) => /^(==)[^=].+\1/m.test(sec) && dateHeaderRegex.test(sec) && typeof (sectionNumber = sections.length - 1 - idx) === 'number');
}
}
if (sectionExists) { // append to existing section
pageobj.setPageSection(sectionNumber + 1);
pageobj.setAppendText('\n\n' + warningText);
pageobj.append();
} else {
if (messageData.heading) { // create new section
pageobj.setNewSectionTitle(messageData.heading);
} else {
Morebits.Status.info('Info', 'Will create a new talk page section for this month, as none was found');
pageobj.setNewSectionTitle(now.monthHeader(0));
}
pageobj.setNewSectionText(warningText);
pageobj.newSection();
}
}
};
Twinkle.warn.callback.evaluate = function twinklewarnCallbackEvaluate(e) {
const userTalkPage = 'User_talk:' + mw.config.get('wgRelevantUserName');
// reason, main_group, sub_group, article
const params = Morebits.QuickForm.getInputData(e.target);
// Check that a reason was filled in if uw-username was selected
if (params.sub_group === 'uw-username' && !params.article) {
alert('You must supply a reason for the {{uw-username}} template.');
return;
}
// The autolevel option will already know by now if a user talk page
// is a cross-namespace redirect (via !!Twinkle.warn.talkpageObj), so
// technically we could alert an error here, but the user will have
// already ignored the bold red error above. Moreover, they probably
// *don't* want to actually issue a warning, so the error handling
// after the form is submitted is probably preferable
// Find the selected <option> element so we can fetch the data structure
const $selectedEl = $(e.target.sub_group).find('option[value="' + $(e.target.sub_group).val() + '"]');
params.messageData = $selectedEl.data('messageData');
Morebits.SimpleWindow.setButtonsEnabled(false);
Morebits.Status.init(e.target);
Morebits.wiki.actionCompleted.redirect = userTalkPage;
Morebits.wiki.actionCompleted.notice = 'Warning complete, reloading talk page in a few seconds';
const wikipedia_page = new Morebits.wiki.Page(userTalkPage, 'User talk page modification');
wikipedia_page.setCallbackParameters(params);
wikipedia_page.setFollowRedirect(true, false);
wikipedia_page.load(Twinkle.warn.callbacks.main);
};
Twinkle.addInitCallback(Twinkle.warn, 'warn');
}());
// </nowiki>
4j9irlvluhxsbjlmfu3pcz74npku6cj
769569
769532
2026-10-22T02:23:43Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at d9cd3461baca43ac99ac2c24fb76a3aae6ddada9: minify
769569
javascript
text/javascript
// <nowiki>
(function(){Twinkle.warn=function twinklewarn(){if(mw.config.exists("wgRelevantUserName")&&!Morebits.ip.isRange(mw.config.get("wgRelevantUserName"))){Twinkle.addPortletLink(Twinkle.warn.callback,"Warn","tw-warn","Warn/notify user");if(Twinkle.getPref("autoMenuAfterRollback")&&mw.config.get("wgNamespaceNumber")===3&&Twinkle.getPrefill("vanarticle")&&!Twinkle.getPrefill("twinklewelcome")&&!Twinkle.getPrefill("noautowarn")){Twinkle.warn.callback()}}if(mw.config.get("wgAction")==="rollback"){const $vandalTalkLink=$("#mw-rollback-success").find(".mw-usertoollinks a").first();if($vandalTalkLink.length){$vandalTalkLink.css("font-weight","bold");$vandalTalkLink.wrapInner($("<span>").attr("title","If appropriate, you can use Twinkle to warn the user about their edits to this page."));const extraParam="vanarticle="+mw.util.rawurlencode(Morebits.pageNameNorm);const href=$vandalTalkLink.attr("href");if(!href.includes("?")){$vandalTalkLink.attr("href",href+"?"+extraParam)}else{$vandalTalkLink.attr("href",href+"&"+extraParam)}}}};Twinkle.warn.dialog=null;Twinkle.warn.callback=function twinklewarnCallback(){if(mw.config.get("wgRelevantUserName")===mw.config.get("wgUserName")&&!confirm("You are about to warn yourself! Are you sure you want to proceed?")){return}Twinkle.warn.dialog=new Morebits.SimpleWindow(600,440);const dialog=Twinkle.warn.dialog;dialog.setTitle("Warn/notify user");dialog.setScriptName("Twinkle");dialog.addFooterLink("Choosing a warning level","WP:UWUL#Levels");dialog.addFooterLink("Warn prefs","WP:TW/PREF#warn");dialog.addFooterLink("Twinkle help","WP:TW/DOC#warn");dialog.addFooterLink("Give feedback","WT:TW");const form=new Morebits.QuickForm(Twinkle.warn.callback.evaluate);const main_select=form.append({type:"field",label:"Choose type of warning/notice to issue",tooltip:"First choose a main warning group, then the specific warning to issue."});const main_group=main_select.append({type:"select",name:"main_group",tooltip:"You can customize the default selection in your Twinkle preferences",event:Twinkle.warn.callback.change_category});const defaultGroup=parseInt(Twinkle.getPref("defaultWarningGroup"),10);main_group.append({type:"option",label:"Auto-select level (1-4)",value:"autolevel",selected:defaultGroup===11});main_group.append({type:"option",label:"1: General note",value:"level1",selected:defaultGroup===1});main_group.append({type:"option",label:"2: Caution",value:"level2",selected:defaultGroup===2});main_group.append({type:"option",label:"3: Warning",value:"level3",selected:defaultGroup===3});main_group.append({type:"option",label:"4: Final warning",value:"level4",selected:defaultGroup===4});main_group.append({type:"option",label:"4im: Only warning",value:"level4im",selected:defaultGroup===5});if(Twinkle.getPref("combinedSingletMenus")){main_group.append({type:"option",label:"Single-issue messages",value:"singlecombined",selected:defaultGroup===6||defaultGroup===7})}else{main_group.append({type:"option",label:"Single-issue notices",value:"singlenotice",selected:defaultGroup===6});main_group.append({type:"option",label:"Single-issue warnings",value:"singlewarn",selected:defaultGroup===7})}if(Twinkle.getPref("customWarningList").length){main_group.append({type:"option",label:"Custom warnings",value:"custom",selected:defaultGroup===9})}main_group.append({type:"option",label:"All warning templates",value:"kitchensink",selected:defaultGroup===10});main_select.append({type:"select",name:"sub_group",event:Twinkle.warn.callback.change_subcategory});form.append({type:"input",name:"article",label:"Linked page",value:Twinkle.getPrefill("vanarticle")||"",tooltip:"A page can be linked within the notice, perhaps because it was a revert to said page that dispatched this notice. Leave empty for no page to be linked."});form.append({type:"div",label:"",style:"color: red",id:"twinkle-warn-warning-messages"});const more=form.append({type:"field",name:"reasonGroup",label:"Warning information"});more.append({type:"textarea",label:"Optional message:",name:"reason",tooltip:"Perhaps a reason, or that a more detailed notice must be appended"});const previewlink=document.createElement("a");$(previewlink).on("click",()=>{Twinkle.warn.callbacks.preview(result)});previewlink.style.cursor="pointer";previewlink.textContent="Preview";more.append({type:"div",id:"warningpreview",label:[previewlink]});more.append({type:"div",id:"twinklewarn-previewbox",style:"display: none"});more.append({type:"submit",label:"Submit"});var result=form.render();dialog.setContent(result);dialog.display();result.main_group.root=result;result.previewer=new Morebits.wiki.Preview($(result).find("div#twinklewarn-previewbox").last()[0]);const vanrevid=Twinkle.getPrefill("vanarticlerevid");if(vanrevid){let message="";let query={};if(!Twinkle.getPrefill("noautowarn")&&Twinkle.getPrefill("vanarticle")){query={action:"query",titles:Twinkle.getPrefill("vanarticle"),prop:"revisions",rvstartid:vanrevid,rvlimit:2,rvdir:"newer",rvprop:"user",format:"json"};new Morebits.wiki.Api("Checking if you successfully reverted the page",query,apiobj=>{const rev=apiobj.getResponse().query.pages[0].revisions;const revertUser=rev&&rev[1].user;if(revertUser&&revertUser!==mw.config.get("wgUserName")){message+=" Someone else reverted the page and may have already warned the user.";$("#twinkle-warn-warning-messages").text("Note:"+message)}}).post()}const checkStale=function(vantimestamp){const revDate=new Morebits.Date(vantimestamp);if(vantimestamp&&revDate.isValid()){if(revDate.add(24,"hours").isBefore(new Date)){message+=" This edit was made more than 24 hours ago so a warning may be stale.";$("#twinkle-warn-warning-messages").text("Note:"+message)}}};let vantimestamp=Twinkle.getPrefill("vantimestamp");if(vantimestamp){checkStale(vantimestamp)}else{query={action:"query",prop:"revisions",rvprop:"timestamp",revids:vanrevid,format:"json"};new Morebits.wiki.Api("Grabbing the revision timestamps",query,apiobj=>{const rev=apiobj.getResponse().query.pages[0].revisions;vantimestamp=rev&&rev[0].timestamp;checkStale(vantimestamp)}).post()}}const evt=document.createEvent("Event");evt.initEvent("change",true,true);result.main_group.dispatchEvent(evt)};Twinkle.warn.messages={levels:{"Common warnings":{"uw-vandalism":{level1:{label:"Vandalism",summary:"General note: Unconstructive editing"},level2:{label:"Vandalism",summary:"Caution: Unconstructive editing"},level3:{label:"Vandalism",summary:"Warning: Vandalism"},level4:{label:"Vandalism",summary:"Final warning: Vandalism"},level4im:{label:"Vandalism",summary:"Only warning: Vandalism"}},"uw-disruptive":{level1:{label:"Disruptive editing",summary:"General note: Unconstructive editing"},level2:{label:"Disruptive editing",summary:"Caution: Unconstructive editing"},level3:{label:"Disruptive editing",summary:"Warning: Disruptive editing"}},"uw-test":{level1:{label:"Editing tests",summary:"General note: Editing tests"},level2:{label:"Editing tests",summary:"Caution: Editing tests"},level3:{label:"Editing tests",summary:"Warning: Editing tests"}},"uw-delete":{level1:{label:"Removal of content, blanking",summary:"General note: Removal of content, blanking"},level2:{label:"Removal of content, blanking",summary:"Caution: Removal of content, blanking"},level3:{label:"Removal of content, blanking",summary:"Warning: Removal of content, blanking"},level4:{label:"Removal of content, blanking",summary:"Final warning: Removal of content, blanking"},level4im:{label:"Removal of content, blanking",summary:"Only warning: Removal of content, blanking"}},"uw-generic":{level4:{label:"Generic warning (for template series missing level 4)",summary:"Final warning notice"}}},"Behavior in articles":{"uw-ai":{level1:{label:"Using a large language model",summary:"General note: Using a large language model"},level2:{label:"Using a large language model",summary:"Caution: Using a large language model"},level3:{label:"Using a large language model",summary:"Warning: Using a large language model"},level4:{label:"Using a large language model",summary:"Final warning: Using a large language model"}},"uw-biog":{level1:{label:"Adding unreferenced controversial information about living persons",summary:"General note: Adding unreferenced controversial information about living persons"},level2:{label:"Adding unreferenced controversial information about living persons",summary:"Caution: Adding unreferenced controversial information about living persons"},level3:{label:"Adding unreferenced controversial/defamatory information about living persons",summary:"Warning: Adding unreferenced controversial information about living persons"},level4:{label:"Adding unreferenced defamatory information about living persons",summary:"Final warning: Adding unreferenced controversial information about living persons"},level4im:{label:"Adding unreferenced defamatory information about living persons",summary:"Only warning: Adding unreferenced controversial information about living persons"}},"uw-defamatory":{level1:{label:"Addition of defamatory content",summary:"General note: Addition of defamatory content"},level2:{label:"Addition of defamatory content",summary:"Caution: Addition of defamatory content"},level3:{label:"Addition of defamatory content",summary:"Warning: Addition of defamatory content"},level4:{label:"Addition of defamatory content",summary:"Final warning: Addition of defamatory content"},level4im:{label:"Addition of defamatory content",summary:"Only warning: Addition of defamatory content"}},"uw-error":{level1:{label:"Introducing deliberate factual errors",summary:"General note: Introducing factual errors"},level2:{label:"Introducing deliberate factual errors",summary:"Caution: Introducing factual errors"},level3:{label:"Introducing deliberate factual errors",summary:"Warning: Introducing deliberate factual errors"},level4:{label:"Introducing deliberate factual errors",summary:"Final warning: Introducing deliberate factual errors"}},"uw-fringe":{level1:{label:"Introducing fringe theories",summary:"General note: Introducing fringe theories"},level2:{label:"Introducing fringe theories",summary:"Caution: Introducing fringe theories"},level3:{label:"Introducing fringe theories",summary:"Warning: Introducing fringe theories"}},"uw-genre":{level1:{label:"Frequent or mass changes to genres without consensus or references",summary:"General note: Frequent or mass changes to genres without consensus or references"},level2:{label:"Frequent or mass changes to genres without consensus or references",summary:"Caution: Frequent or mass changes to genres without consensus or references"},level3:{label:"Frequent or mass changes to genres without consensus or reference",summary:"Warning: Frequent or mass changes to genres without consensus or reference"},level4:{label:"Frequent or mass changes to genres without consensus or reference",summary:"Final warning: Frequent or mass changes to genres without consensus or reference"}},"uw-image":{level1:{label:"Inappropriate images in articles",summary:"General note: Inappropriate images in articles"},level2:{label:"Image-related vandalism in articles",summary:"Caution: Image-related vandalism in articles"},level3:{label:"Image-related vandalism in articles",summary:"Warning: Image-related vandalism in articles"},level4:{label:"Image-related vandalism in articles",summary:"Final warning: Image-related vandalism in articles"},level4im:{label:"Image-related vandalism",summary:"Only warning: Image-related vandalism"}},"uw-joke":{level1:{label:"Using improper humor in articles",summary:"General note: Using improper humor in articles"},level2:{label:"Using improper humor in articles",summary:"Caution: Using improper humor in articles"},level3:{label:"Using improper humor in articles",summary:"Warning: Using improper humor in articles"},level4:{label:"Using improper humor in articles",summary:"Final warning: Using improper humor in articles"},level4im:{label:"Using improper humor",summary:"Only warning: Using improper humor"}},"uw-nor":{level1:{label:"Adding original research",summary:"General note: Adding original research"},level2:{label:"Adding original research",summary:"Caution: Adding original research"},level3:{label:"Adding original research",summary:"Warning: Adding original research"},level4:{label:"Adding original research",summary:"Final warning: Adding original research"}},"uw-notcensored":{level1:{label:"Censorship of material",summary:"General note: Censorship of material"},level2:{label:"Censorship of material",summary:"Caution: Censorship of material"},level3:{label:"Censorship of material",summary:"Warning: Censorship of material"}},"uw-own":{level1:{label:"Ownership of articles",summary:"General note: Ownership of articles"},level2:{label:"Ownership of articles",summary:"Caution: Ownership of articles"},level3:{label:"Ownership of articles",summary:"Warning: Ownership of articles"},level4:{label:"Ownership of articles",summary:"Final warning: Ownership of articles"},level4im:{label:"Ownership of articles",summary:"Only warning: Ownership of articles"}},"uw-pronouns":{level1:{label:"Introducing incorrect pronouns",summary:"General note: Introducing incorrect pronouns"},level2:{label:"Introducing incorrect pronouns",summary:"Caution: Introducing incorrect pronouns"},level3:{label:"Introducing incorrect pronouns",summary:"Warning: Introducing incorrect pronouns"}},"uw-subtle":{level1:{label:"Subtle vandalism",summary:"General note: Possible unconstructive editing"},level2:{label:"Subtle vandalism",summary:"Caution: Likely unconstructive editing"},level3:{label:"Subtle vandalism",summary:"Warning: Subtle vandalism"},level4:{label:"Subtle vandalism",summary:"Final warning: Subtle vandalism"}},"uw-talkinarticle":{level1:{label:"Adding commentary to an article",summary:"General note: Adding commentary to an article"},level2:{label:"Adding commentary to an article",summary:"Caution: Adding commentary to an article"},level3:{label:"Adding commentary to an article",summary:"Warning: Adding commentary to an article"}},"uw-tdel":{level1:{label:"Removal of maintenance templates",summary:"General note: Removal of maintenance templates"},level2:{label:"Removal of maintenance templates",summary:"Caution: Removal of maintenance templates"},level3:{label:"Removal of maintenance templates",summary:"Warning: Removal of maintenance templates"},level4:{label:"Removal of maintenance templates",summary:"Final warning: Removal of maintenance templates"}},"uw-unsourced":{level1:{label:"Addition of unsourced or improperly cited material",summary:"General note: Addition of unsourced or improperly cited material"},level2:{label:"Addition of unsourced or improperly cited material",summary:"Caution: Addition of unsourced or improperly cited material"},level3:{label:"Addition of unsourced or improperly cited material",summary:"Warning: Addition of unsourced or improperly cited material"},level4:{label:"Addition of unsourced or improperly cited material",summary:"Final warning: Addition of unsourced or improperly cited material"}}},"Promotions and spam":{"uw-advert":{level1:{label:"Using Wikipedia for advertising or promotion",summary:"General note: Using Wikipedia for advertising or promotion"},level2:{label:"Using Wikipedia for advertising or promotion",summary:"Caution: Using Wikipedia for advertising or promotion"},level3:{label:"Using Wikipedia for advertising or promotion",summary:"Warning: Using Wikipedia for advertising or promotion"},level4:{label:"Using Wikipedia for advertising or promotion",summary:"Final warning: Using Wikipedia for advertising or promotion"},level4im:{label:"Using Wikipedia for advertising or promotion",summary:"Only warning: Using Wikipedia for advertising or promotion"}},"uw-npov":{level1:{label:"Not adhering to neutral point of view",summary:"General note: Not adhering to neutral point of view"},level2:{label:"Not adhering to neutral point of view",summary:"Caution: Not adhering to neutral point of view"},level3:{label:"Not adhering to neutral point of view",summary:"Warning: Not adhering to neutral point of view"},level4:{label:"Not adhering to neutral point of view",summary:"Final warning: Not adhering to neutral point of view"}},"uw-paid":{level1:{label:"Paid editing without disclosure under the Wikimedia Terms of Use",summary:"General note: Disclosure requirements for paid editing under the Wikimedia Terms of Use"},level2:{label:"Paid editing without disclosure under the Wikimedia Terms of Use",summary:"Caution: Disclosure requirements for paid editing under the Wikimedia Terms of Use"},level3:{label:"Paid editing without disclosure under the Wikimedia Terms of Use",summary:"Warning: Disclosure requirements for paid editing under the Wikimedia Terms of Use"},level4:{label:"Paid editing without disclosure under the Wikimedia Terms of Use",summary:"Final warning: Disclosure requirements for paid editing under the Wikimedia Terms of Use"}},"uw-spam":{level1:{label:"Adding inappropriate external links",summary:"General note: Adding inappropriate external links"},level2:{label:"Adding spam links",summary:"Caution: Adding spam links"},level3:{label:"Adding spam links",summary:"Warning: Adding spam links"},level4:{label:"Adding spam links",summary:"Final warning: Adding spam links"},level4im:{label:"Adding spam links",summary:"Only warning: Adding spam links"}}},"Behavior towards other editors":{"uw-agf":{level1:{label:"Not assuming good faith",summary:"General note: Not assuming good faith"},level2:{label:"Not assuming good faith",summary:"Caution: Not assuming good faith"},level3:{label:"Not assuming good faith",summary:"Warning: Not assuming good faith"}},"uw-aitalk":{level1:{label:"Posting LLM-generated comments",summary:"General note: Posting LLM-generated comments"},level2:{label:"Posting LLM-generated comments",summary:"Caution: Posting LLM-generated comments"},level3:{label:"Posting LLM-generated comments",summary:"Warning: Posting LLM-generated comments"},level4:{label:"Posting LLM-generated comments",summary:"Final warning: Posting LLM-generated comments"}},"uw-harass":{level1:{label:"Harassment of other users",summary:"General note: Harassment of other users"},level2:{label:"Harassment of other users",summary:"Caution: Harassment of other users"},level3:{label:"Harassment of other users",summary:"Warning: Harassment of other users"},level4:{label:"Harassment of other users",summary:"Final warning: Harassment of other users"},level4im:{label:"Harassment of other users",summary:"Only warning: Harassment of other users"}},"uw-npa":{level1:{label:"Personal attack directed at a specific editor",summary:"General note: Personal attack directed at a specific editor"},level2:{label:"Personal attack directed at a specific editor",summary:"Caution: Personal attack directed at a specific editor"},level3:{label:"Personal attack directed at a specific editor",summary:"Warning: Personal attack directed at a specific editor"},level4:{label:"Personal attack directed at a specific editor",summary:"Final warning: Personal attack directed at a specific editor"},level4im:{label:"Personal attack directed at a specific editor",summary:"Only warning: Personal attack directed at a specific editor"}},"uw-tempabuse":{level1:{label:"Improper use of warning or blocking template",summary:"General note: Improper use of warning or blocking template"},level2:{label:"Improper use of warning or blocking template",summary:"Caution: Improper use of warning or blocking template"}}},"Removal of deletion tags":{"uw-afd":{level1:{label:"Removing {{afd}} templates",summary:"General note: Removing {{afd}} templates"},level2:{label:"Removing {{afd}} templates",summary:"Caution: Removing {{afd}} templates"},level3:{label:"Removing {{afd}} templates",summary:"Warning: Removing {{afd}} templates"},level4:{label:"Removing {{afd}} templates",summary:"Final warning: Removing {{afd}} templates"}},"uw-blpprod":{level1:{label:"Removing {{blp prod}} templates",summary:"General note: Removing {{blp prod}} templates"},level2:{label:"Removing {{blp prod}} templates",summary:"Caution: Removing {{blp prod}} templates"},level3:{label:"Removing {{blp prod}} templates",summary:"Warning: Removing {{blp prod}} templates"},level4:{label:"Removing {{blp prod}} templates",summary:"Final warning: Removing {{blp prod}} templates"}},"uw-idt":{level1:{label:"Removing file deletion tags",summary:"General note: Removing file deletion tags"},level2:{label:"Removing file deletion tags",summary:"Caution: Removing file deletion tags"},level3:{label:"Removing file deletion tags",summary:"Warning: Removing file deletion tags"},level4:{label:"Removing file deletion tags",summary:"Final warning: Removing file deletion tags"}},"uw-rfd":{level1:{label:"Removing redirects for discussion tags",summary:"General note: Removing redirects for discussion tags"},level2:{label:"Removing redirects for discussion tags",summary:"Caution: Removing redirects for discussion tags"},level3:{label:"Removing redirects for discussion tags",summary:"Warning: Removing redirects for discussion tags"},level4:{label:"Removing redirects for discussion tags",summary:"Final warning: Removing redirects for discussion tags"}},"uw-speedy":{level1:{label:"Removing speedy deletion tags",summary:"General note: Removing speedy deletion tags"},level2:{label:"Removing speedy deletion tags",summary:"Caution: Removing speedy deletion tags"},level3:{label:"Removing speedy deletion tags",summary:"Warning: Removing speedy deletion tags"},level4:{label:"Removing speedy deletion tags",summary:"Final warning: Removing speedy deletion tags"}},"uw-tfd":{level1:{label:"Removing {{tfd}} templates",summary:"General note: Removing {{tfd}} templates"},level2:{label:"Removing {{tfd}} templates",summary:"Caution: Removing {{tfd}} templates"},level3:{label:"Removing {{tfd}} templates",summary:"Warning: Removing {{tfd}} templates"},level4:{label:"Removing {{tfd}} templates",summary:"Final warning: Removing {{tfd}} templates"}}},Other:{"uw-attempt":{level1:{label:"Triggering the edit filter",summary:"General note: Triggering the edit filter"},level2:{label:"Triggering the edit filter",summary:"Caution: Triggering the edit filter"},level3:{label:"Triggering the edit filter",summary:"Warning: Triggering the edit filter"},level4:{label:"Triggering the edit filter",summary:"Final warning: Triggering the edit filter"},level4im:{label:"Triggering the edit filter",summary:"Only warning: Triggering the edit filter"}},"uw-chat":{level1:{label:"Using talk page as forum",summary:"General note: Using talk page as forum"},level2:{label:"Using talk page as forum",summary:"Caution: Using talk page as forum"},level3:{label:"Using talk page as forum",summary:"Warning: Using talk page as forum"},level4:{label:"Using talk page as forum",summary:"Final warning: Using talk page as forum"}},"uw-create":{level1:{label:"Creating inappropriate pages",summary:"General note: Creating inappropriate pages"},level2:{label:"Creating inappropriate pages",summary:"Caution: Creating inappropriate pages"},level3:{label:"Creating inappropriate pages",summary:"Warning: Creating inappropriate pages"},level4:{label:"Creating inappropriate pages",summary:"Final warning: Creating inappropriate pages"},level4im:{label:"Creating inappropriate pages",summary:"Only warning: Creating inappropriate pages"}},"uw-fv":{level1:{label:"Added statement had source, but it did not verify content",summary:"General note: Added statement had source, but it did not verify content"}},"uw-mislead":{level1:{label:"Using misleading edit summaries",summary:"General note: Using misleading edit summaries"},level2:{label:"Using misleading edit summaries",summary:"Caution: Using misleading edit summaries"},level3:{label:"Using misleading edit summaries",summary:"Warning: Using misleading edit summaries"}},"uw-mos":{level1:{label:"Manual of style",summary:"General note: Formatting, date, language, etc (Manual of style)"},level2:{label:"Manual of style",summary:"Caution: Formatting, date, language, etc (Manual of style)"},level3:{label:"Manual of style",summary:"Warning: Formatting, date, language, etc (Manual of style)"},level4:{label:"Manual of style",summary:"Final warning: Formatting, date, language, etc (Manual of style)"}},"uw-move":{level1:{label:"Page moves against naming conventions or consensus",summary:"General note: Page moves against naming conventions or consensus"},level2:{label:"Page moves against naming conventions or consensus",summary:"Caution: Page moves against naming conventions or consensus"},level3:{label:"Page moves against naming conventions or consensus",summary:"Warning: Page moves against naming conventions or consensus"},level4:{label:"Page moves against naming conventions or consensus",summary:"Final warning: Page moves against naming conventions or consensus"},level4im:{label:"Page moves against naming conventions or consensus",summary:"Only warning: Page moves against naming conventions or consensus"}},"uw-redirect":{level1:{label:"Creating inappropriate redirects",summary:"General note: Creating inappropriate redirects"},level2:{label:"Creating inappropriate redirects",summary:"Caution: Creating inappropriate redirects"},level3:{label:"Creating inappropriate redirects",summary:"Warning: Creating inappropriate redirects"},level4:{label:"Creating inappropriate redirects",summary:"Final warning: Creating inappropriate redirects"},level4im:{label:"Creating inappropriate redirects",summary:"Only warning: Creating inappropriate redirects"}},"uw-tpv":{level1:{label:"Refactoring others' talk page comments",summary:"General note: Refactoring others' talk page comments"},level2:{label:"Refactoring others' talk page comments",summary:"Caution: Refactoring others' talk page comments"},level3:{label:"Refactoring others' talk page comments",summary:"Warning: Refactoring others' talk page comments"},level4:{label:"Refactoring others' talk page comments",summary:"Final warning: Refactoring others' talk page comments"},level4im:{label:"Refactoring others' talk page comments",summary:"Only warning: Refactoring others' talk page comments"}},"uw-upload":{level1:{label:"Uploading unencyclopedic images",summary:"General note: Uploading unencyclopedic images"},level2:{label:"Uploading unencyclopedic images",summary:"Caution: Uploading unencyclopedic images"},level3:{label:"Uploading unencyclopedic images",summary:"Warning: Uploading unencyclopedic images"},level4:{label:"Uploading unencyclopedic images",summary:"Final warning: Uploading unencyclopedic images"},level4im:{label:"Uploading unencyclopedic images",summary:"Only warning: Uploading unencyclopedic images"}}}},singlenotice:{"uw-addalink":{label:"Mistakes with the Add a Link newcomer task",summary:"Notice: Mistakes with the Add a Link newcomer task"},"uw-agf-sock":{label:"Use of multiple accounts (assuming good faith)",summary:"Notice: Using multiple accounts"},"uw-aiv":{label:"Bad AIV report",summary:"Notice: Bad AIV report"},"uw-articletodraft":{label:"Article moved to draftspace",summary:"Notice: Article moved to draftspace",hideReason:true},"uw-autobiography":{label:"Creating autobiographies",summary:"Notice: Creating autobiographies"},"uw-badcat":{label:"Adding incorrect categories",summary:"Notice: Adding incorrect categories"},"uw-badlistentry":{label:"Adding inappropriate entries to lists",summary:"Notice: Adding inappropriate entries to lists"},"uw-bareurl":{label:"Adding a bare URL",summary:"Notice: Adding a bare URL"},"uw-bite":{label:'"Biting" newcomers',summary:'Notice: "Biting" newcomers',suppressArticleInSummary:true},"uw-blar":{label:"Article blanked and redirected",summary:"Notice: Article blanked and redirected",hideReason:true},"uw-circular":{label:"Using circular sources",summary:"Notice: Using circular sources"},"uw-coi":{label:"Conflict of interest",summary:"Notice: Conflict of interest",heading:"Managing a conflict of interest"},"uw-copying":{label:"Copying text to another page",summary:"Notice: Copying text to another page"},"uw-crystal":{label:"Adding speculative or unconfirmed information",summary:"Notice: Adding speculative or unconfirmed information"},"uw-c&pmove":{label:"Cut and paste moves",summary:"Notice: Cut and paste moves"},"uw-dab":{label:"Incorrect edit to a disambiguation page",summary:"Notice: Incorrect edit to a disambiguation page"},"uw-date":{label:"Unnecessarily changing date formats",summary:"Notice: Unnecessarily changing date formats"},"uw-deadlink":{label:"Removing proper sources containing dead links",summary:"Notice: Removing proper sources containing dead links"},"uw-displaytitle":{label:"Incorrect use of DISPLAYTITLE",summary:"Notice: Incorrect use of DISPLAYTITLE"},"uw-draftfirst":{label:"User should draft in userspace without the risk of speedy deletion",summary:"Notice: Consider drafting your article in [[Help:Userspace draft|userspace]]"},"uw-editsummary":{label:"New user not using edit summary",summary:"Notice: Not using edit summary"},"uw-editsummary2":{label:"Experienced user not using edit summary",summary:"Notice: Not using edit summary",hideLinkedPage:true,hideReason:true},"uw-elinbody":{label:"Adding external links to the body of an article",summary:"Notice: Keep external links to External links sections at the bottom of an article"},"uw-english":{label:"Not communicating in English",summary:"Notice: Not communicating in English"},"uw-hasty":{label:"Hasty addition of speedy deletion tags",summary:"Notice: Allow creators time to improve their articles before tagging them for deletion"},"uw-islamhon":{label:"Use of Islamic honorifics",summary:"Notice: Use of Islamic honorifics"},"uw-italicize":{label:"Italicize books, films, albums, magazines, TV series, etc within articles",summary:"Notice: Italicize books, films, albums, magazines, TV series, etc within articles"},"uw-lang":{label:"Unnecessarily changing between British and American English",summary:"Notice: Unnecessarily changing between British and American English",heading:"National varieties of English"},"uw-linking":{label:"Excessive addition of redlinks or repeated blue links",summary:"Notice: Excessive addition of redlinks or repeated blue links"},"uw-longsd":{label:"Insertion of long short description",summary:"Notice: Insertion of long short description"},"uw-minor":{label:"Incorrect use of minor edits check box",summary:"Notice: Incorrect use of minor edits check box"},"uw-mostm":{label:"Formatting of trademarks",summary:"Notice: Formatting of trademarks"},"uw-multiple-accts":{label:"Inappropriate use of alternative accounts",summary:"Notice: Inappropriate use of alternative accounts"},"uw-notenglish":{label:"Creating non-English articles",summary:"Notice: Creating non-English articles"},"uw-notenglishedit":{label:"Adding non-English content to articles",summary:"Notice: Adding non-English content to articles"},"uw-notvote":{label:"We use consensus, not voting",summary:"Notice: We use consensus, not voting"},"uw-orphantalk":{label:"Talk page created with no article",summary:"Notice: Talk page created with no article"},"uw-plagiarism":{label:"Copying from public domain sources without attribution",summary:"Notice: Copying from public domain sources without attribution"},"uw-preview":{label:"Use preview button to avoid mistakes",summary:"Notice: Use preview button to avoid mistakes"},"uw-redlink":{label:"Indiscriminate removal of redlinks",summary:"Notice: Be careful when removing redlinks"},"uw-refspam":{label:"Adding citations to research published by a small group of researchers",summary:"Notice: Adding citations to research published by a small group of researchers",hideLinkedPage:true,hideReason:true},"uw-selfrevert":{label:"Self-reverted editing tests",summary:"Notice: Self-reverted editing tests"},"uw-socialnetwork":{label:"Wikipedia is not a social network",summary:"Notice: Wikipedia is not a social network"},"uw-sofixit":{label:"Be bold and fix things yourself",summary:"Notice: You can be bold and fix things yourself"},"uw-spoiler":{label:"Adding spoiler alerts or removing spoilers from appropriate sections",summary:"Notice: Don't delete or flag potential 'spoilers' in Wikipedia articles"},"uw-talkinarticle":{label:"Talk in article",summary:"Notice: Talk in article"},"uw-tilde":{label:"Not signing posts",summary:"Notice: Not signing posts"},"uw-toppost":{label:"Posting at the top of talk pages",summary:"Notice: Posting at the top of talk pages"},"uw-translation":{label:"Adding translations without proper attribution",summary:"Notice: Attribution required when translating articles"},"uw-unattribcc":{label:"Copying from compatibly-licensed sources without attribution",summary:"Notice: Copying from compatibly-licensed sources without attribution"},"uw-userspace draft finish":{label:"Stale userspace draft",summary:"Notice: Stale userspace draft"},"uw-usertalk":{label:"Misuse of user talk page",summary:"Notice: Misuse of user talk page",hideLinkedPage:true},"uw-vgscope":{label:"Adding video game walkthroughs, cheats or instructions",summary:"Notice: Adding video game walkthroughs, cheats or instructions"},"uw-warn":{label:"Place user warning templates when reverting vandalism",summary:"Notice: You can use user warning templates when reverting vandalism"},"uw-wrongsummary":{label:"Using inaccurate or inappropriate edit summaries",summary:"Notice: Using inaccurate or inappropriate edit summaries"}},singlewarn:{"uw-3rr":{label:"Potential three-revert rule violation; see also uw-ew",summary:"Warning: Three-revert rule"},"uw-affiliate":{label:"Affiliate marketing",summary:"Warning: Affiliate marketing"},"uw-attack":{label:"Creating attack pages",summary:"Warning: Creating attack pages",suppressArticleInSummary:true},"uw-botun":{label:"Bot username",summary:"Warning: Bot username"},"uw-canvass":{label:"Canvassing",summary:"Warning: Canvassing"},"uw-copyright":{label:"Copyright violation",summary:"Warning: Copyright violation"},"uw-copyright-link":{label:"Linking to copyrighted works violation",summary:"Warning: Linking to copyrighted works violation"},"uw-copyright-new":{label:"Copyright violation (with explanation for new users)",summary:"Notice: Avoiding copyright problems",heading:"Wikipedia and copyright"},"uw-copyright-remove":{label:"Removing {{copyvio}} template from articles",summary:"Warning: Removing {{copyvio}} templates"},"uw-derogatory":{label:"Addition of derogatory/hateful content",summary:"Warning: Addition of derogatory content"},"uw-efsummary":{label:"Edit summary triggering the edit filter",summary:"Warning: Edit summary triggering the edit filter"},"uw-ew":{label:"Edit warring (stronger wording)",summary:"Warning: Edit warring"},"uw-ewsoft":{label:"Edit warring (softer wording for newcomers)",summary:"Warning: Edit warring"},"uw-hijacking":{label:"Hijacking articles",summary:"Warning: Hijacking articles"},"uw-hoax":{label:"Creating hoaxes",summary:"Warning: Creating hoaxes"},"uw-legal":{label:"Making legal threats",summary:"Warning: Making legal threats"},"uw-login":{label:"Editing while logged out",summary:"Warning: Editing while logged out"},"uw-multipleTAs":{label:"Usage of multiple temporary accounts",summary:"Warning: Vandalism using multiple temporary accounts"},"uw-paraphrase":{label:"Close paraphrasing",summary:"Warning: Close paraphrasing"},"uw-pinfo":{label:"Personal info (outing)",summary:"Warning: Personal info"},"uw-salt":{label:"Recreating salted articles under a different title",summary:"Notice: Recreating creation-protected articles under a different title"},"uw-socksuspect":{label:"Sockpuppetry",summary:"Warning: You are a suspected [[WP:SOCK|sockpuppet]]"},"uw-upv":{label:"Userpage vandalism",summary:"Warning: Userpage vandalism"},"uw-username":{label:"Username is against policy",summary:"Warning: Your username might be against policy",suppressArticleInSummary:true},"uw-coi-username":{label:"Username is against policy, and conflict of interest",summary:"Warning: Username and conflict of interest",heading:"Your username"},"uw-userpage":{label:"Userpage or subpage is against policy",summary:"Warning: Userpage or subpage is against policy"}}};Twinkle.warn.getTemplateProperty=function(templates,templateName,propertyName){let result;const isNumberedTemplate=templateName.match(/(1|2|3|4|4im)$/);if(isNumberedTemplate){const unNumberedTemplateName=templateName.replace(/(?:1|2|3|4|4im)$/,"");const level=isNumberedTemplate[0];const numberedWarnings={};$.each(templates.levels,(key,val)=>{$.extend(numberedWarnings,val)});$.each(numberedWarnings,key=>{if(key===unNumberedTemplateName){result=numberedWarnings[key]["level"+level][propertyName]}})}const otherWarnings={};$.each(templates,(key,val)=>{if(key!=="levels"){$.extend(otherWarnings,val)}});$.each(otherWarnings,key=>{if(key===templateName){result=otherWarnings[key][propertyName]}});return result};Twinkle.warn.prev_article=null;Twinkle.warn.prev_reason=null;Twinkle.warn.talkpageObj=null;Twinkle.warn.callback.change_category=function twinklewarnCallbackChangeCategory(e){const value=e.target.value;const sub_group=e.target.root.sub_group;sub_group.main_group=value;let old_subvalue=sub_group.value;let old_subvalue_re;if(old_subvalue){if(value==="kitchensink"){old_subvalue_re=new RegExp(mw.util.escapeRegExp(old_subvalue))}else{old_subvalue=old_subvalue.replace(/\d*(im)?$/,"");old_subvalue_re=new RegExp(mw.util.escapeRegExp(old_subvalue)+"(\\d*(?:im)?)$")}}while(sub_group.hasChildNodes()){sub_group.removeChild(sub_group.firstChild)}let selected=false;const createEntries=function(contents,container,wrapInOptgroup,val=value){const level=val.replace(/^\D+/g,"");if(wrapInOptgroup&&$.client.profile().platform==="iphone"){let wrapperOptgroup=new Morebits.QuickForm.Element({type:"optgroup",label:"Available templates"});wrapperOptgroup=wrapperOptgroup.render();container.appendChild(wrapperOptgroup);container=wrapperOptgroup}$.each(contents,(itemKey,itemProperties)=>{if(!!level&&!itemProperties[val]){return}const key=typeof itemKey==="string"?itemKey:itemProperties.value;const template=key+level;const elem=new Morebits.QuickForm.Element({type:"option",label:"{{"+template+"}}: "+(level?itemProperties[val].label:itemProperties.label),value:template});if(!selected&&old_subvalue&&old_subvalue_re.test(template)){elem.data.selected=selected=true}const elemRendered=container.appendChild(elem.render());$(elemRendered).data("messageData",itemProperties)})};const createGroup=function(warnGroup,label,wrapInOptgroup,val){wrapInOptgroup=typeof wrapInOptgroup!=="undefined"?wrapInOptgroup:true;let optgroup=new Morebits.QuickForm.Element({type:"optgroup",label:label});optgroup=optgroup.render();sub_group.appendChild(optgroup);createEntries(warnGroup,optgroup,wrapInOptgroup,val)};switch(value){case"singlenotice":case"singlewarn":createEntries(Twinkle.warn.messages[value],sub_group,true);break;case"singlecombined":var unSortedSinglets=$.extend({},Twinkle.warn.messages.singlenotice,Twinkle.warn.messages.singlewarn);var sortedSingletMessages={};Object.keys(unSortedSinglets).sort().forEach(key=>{sortedSingletMessages[key]=unSortedSinglets[key]});createEntries(sortedSingletMessages,sub_group,true);break;case"custom":createEntries(Twinkle.getPref("customWarningList"),sub_group,true);break;case"kitchensink":["level1","level2","level3","level4","level4im"].forEach(lvl=>{$.each(Twinkle.warn.messages.levels,(levelGroupLabel,levelGroup)=>{createGroup(levelGroup,"Level "+lvl.slice(5)+": "+levelGroupLabel,true,lvl)})});createGroup(Twinkle.warn.messages.singlenotice,"Single-issue notices");createGroup(Twinkle.warn.messages.singlewarn,"Single-issue warnings");createGroup(Twinkle.getPref("customWarningList"),"Custom warnings");break;case"level1":case"level2":case"level3":case"level4":case"level4im":$.each(Twinkle.warn.messages.levels,(groupLabel,groupContents)=>{createGroup(groupContents,groupLabel,false)});break;case"autolevel":var autolevelProc=function(){const wikitext=Twinkle.warn.talkpageObj.getPageText();const latest=Twinkle.warn.callbacks.dateProcessing(wikitext)[0];const params={sub_group:old_subvalue,article:e.target.root.article.value};const lvl="level"+Twinkle.warn.callbacks.autolevelParseWikitext(wikitext,params,latest)[1];$.each(Twinkle.warn.messages.levels,(groupLabel,groupContents)=>{createGroup(groupContents,groupLabel,false,lvl)});Twinkle.warn.callback.postCategoryCleanup(e)};if(Twinkle.warn.talkpageObj){autolevelProc()}else{const usertalk_page=new Morebits.wiki.Page("User_talk:"+mw.config.get("wgRelevantUserName"),"Loading previous warnings");usertalk_page.setFollowRedirect(true,false);usertalk_page.load(pageobj=>{Twinkle.warn.talkpageObj=pageobj;autolevelProc()},()=>{const $noTalkPageNode=$("<strong>").text("Unable to load user talk page; it might be a cross-namespace redirect. Autolevel detection will not work.").attr("id","twinkle-warn-autolevel-message").css("color","red");$noTalkPageNode.insertBefore($("#twinkle-warn-warning-messages"));e.target.root.previewer.closePreview()})}break;default:alert("Unknown warning group in twinklewarn");break}if(value!=="autolevel"){$("#twinkle-warn-autolevel-message").remove();Twinkle.warn.callback.postCategoryCleanup(e)}};Twinkle.warn.callback.postCategoryCleanup=function twinklewarnCallbackPostCategoryCleanup(e){Morebits.QuickForm.setElementTooltipVisibility(e.target.root.article,true);Morebits.QuickForm.resetElementLabel(e.target.root.article);Twinkle.warn.callback.change_subcategory(e);if(!Twinkle.getPref("oldSelect")){$("select[name=sub_group]").select2({theme:"default select2-morebits",width:"100%",matcher:Morebits.select2.matchers.optgroupFull,templateResult:Morebits.select2.highlightSearchMatches,language:{searching:Morebits.select2.queryInterceptor}}).change(Twinkle.warn.callback.change_subcategory);$(".select2-selection").on("keydown",Morebits.select2.autoStart).trigger("focus");mw.util.addCSS(".select2-container .select2-dropdown .select2-results > .select2-results__options { max-height: 350px; }"+".select2-results .select2-results__option { padding-top: 1px; padding-bottom: 1px; }"+".select2-results .select2-results__group { padding-top: 1px; padding-bottom: 1px; } "+".select2-container .select2-dropdown .select2-results { font-size: 13px; }"+".select2-container .selection .select2-selection__rendered { font-size: 13px; }")}};Twinkle.warn.callback.change_subcategory=function twinklewarnCallbackChangeSubcategory(e){const selected_main_group=e.target.form.main_group.value;const selected_template=e.target.form.sub_group.value;const hideLinkedPage=Twinkle.warn.getTemplateProperty(Twinkle.warn.messages,selected_template,"hideLinkedPage");if(hideLinkedPage){e.target.form.article.value="";Morebits.QuickForm.setElementVisibility(e.target.form.article.parentElement,false)}else{Morebits.QuickForm.setElementVisibility(e.target.form.article.parentElement,true)}const hideReason=Twinkle.warn.getTemplateProperty(Twinkle.warn.messages,selected_template,"hideReason");if(hideReason){e.target.form.reason.value="";Morebits.QuickForm.setElementVisibility(e.target.form.reason.parentElement,false)}else{Morebits.QuickForm.setElementVisibility(e.target.form.reason.parentElement,true)}const notLinkedArticle={"uw-agf-sock":"Optional username of other account (without User:) ","uw-bite":"Username of 'bitten' user (without User:) ","uw-socksuspect":"Username of sock master, if known (without User:) ","uw-username":"Username violates policy because... ","uw-aiv":"Optional username that was reported (without User:) "};const hasLevel=["singlenotice","singlewarn","singlecombined","kitchensink"].includes(selected_main_group);if(hasLevel){if(notLinkedArticle[selected_template]){if(Twinkle.warn.prev_article===null){Twinkle.warn.prev_article=e.target.form.article.value}e.target.form.article.notArticle=true;e.target.form.article.value="";Morebits.QuickForm.setElementTooltipVisibility(e.target.form.article,false);Morebits.QuickForm.overrideElementLabel(e.target.form.article,notLinkedArticle[selected_template])}else if(e.target.form.article.notArticle){if(Twinkle.warn.prev_article!==null){e.target.form.article.value=Twinkle.warn.prev_article;Twinkle.warn.prev_article=null}e.target.form.article.notArticle=false;Morebits.QuickForm.setElementTooltipVisibility(e.target.form.article,true);Morebits.QuickForm.resetElementLabel(e.target.form.article)}}$("#tw-warn-red-notice").remove();let $redWarning;if(selected_template==="uw-username"){$redWarning=$("<div style='color: red;' id='tw-warn-red-notice'>{{uw-username}} should <b>not</b> be used for <b>blatant</b> username policy violations. "+"Blatant violations should be reported directly to UAA (via Twinkle's ARV tab). "+"{{uw-username}} should only be used in edge cases in order to engage in discussion with the user.</div>");$redWarning.insertAfter(Morebits.QuickForm.getElementLabelObject(e.target.form.reasonGroup))}else if(selected_template==="uw-coi-username"){$redWarning=$("<div style='color: red;' id='tw-warn-red-notice'>{{uw-coi-username}} should <b>not</b> be used for <b>blatant</b> username policy violations. "+"Blatant violations should be reported directly to UAA (via Twinkle's ARV tab). "+"{{uw-coi-username}} should only be used in edge cases in order to engage in discussion with the user.</div>");$redWarning.insertAfter(Morebits.QuickForm.getElementLabelObject(e.target.form.reasonGroup))}};Twinkle.warn.callbacks={getWarningWikitext:function(templateName,article,reason,isCustom){let text="{{subst:"+templateName;if(article){if(templateName==="uw-c&pmove"){text+="|to="+article}else{text+="|1="+article}}if(reason&&!isCustom){if(templateName==="uw-userpage"){text+="|3=''"+Morebits.string.formatReasonText(reason)+"''"}else{text+="|2=''"+Morebits.string.formatReasonText(reason)+"''"}}text+="}}";if(reason&&isCustom){text+=" ''"+reason+"''"}return text+" ~~~~"},showPreview:function(form,templatename){const input=Morebits.QuickForm.getInputData(form);templatename=templatename||input.sub_group;const linkedarticle=input.article;const templatetext=Twinkle.warn.callbacks.getWarningWikitext(templatename,linkedarticle,input.reason,input.main_group==="custom");form.previewer.beginRender(templatetext,"User_talk:"+mw.config.get("wgRelevantUserName"))},preview:function(form){if(form.main_group.value==="autolevel"){const usertalk_page=new Morebits.wiki.Page("User_talk:"+mw.config.get("wgRelevantUserName"),"Loading previous warnings");usertalk_page.setFollowRedirect(true,false);usertalk_page.load(pageobj=>{Twinkle.warn.talkpageObj=pageobj;const wikitext=pageobj.getPageText();const latest=Twinkle.warn.callbacks.dateProcessing(wikitext)[0];const params={sub_group:form.sub_group.value,article:form.article.value,messageData:$(form.sub_group).find('option[value="'+$(form.sub_group).val()+'"]').data("messageData")};const template=Twinkle.warn.callbacks.autolevelParseWikitext(wikitext,params,latest)[0];Twinkle.warn.callbacks.showPreview(form,template);if(form.sub_group.value!==template){const evt=document.createEvent("Event");evt.initEvent("change",true,true);form.main_group.dispatchEvent(evt)}})}else{Twinkle.warn.callbacks.showPreview(form)}},dateProcessing:function(wikitext){const history_re=/<!--\s?Template:([uU]w-.*?)\s?-->.*?(\d{1,2}:\d{1,2}, \d{1,2} \w+ \d{4} \(UTC\))/g;const history={};const latest={date:new Morebits.Date(0),type:""};let current;while((current=history_re.exec(wikitext))!==null){const template=current[1],current_date=new Morebits.Date(current[2]);if(!(template in history)||history[template].isBefore(current_date)){history[template]=current_date}if(!latest.date.isAfter(current_date)){latest.date=current_date;latest.type=template}}return[latest,history]},autolevelParseWikitext:function(wikitext,params,latest,date,statelem){let level;if(/\d(?:im)?$/.test(latest.type)){level=parseInt(latest.type.replace(/.*(\d)(?:im)?$/,"$1"),10)}else if(latest.type){const loweredType=/uw-multipleTAs/i.test(latest.type)?"uw-multipleTAs":latest.type.toLowerCase();if(Twinkle.warn.messages.singlewarn[loweredType]){level=3}else{level=1}}const $autolevelMessage=$("<div>").attr("id","twinkle-warn-autolevel-message");if(isNaN(level)){level=1}else if(level>4||level<1){const message="Unable to parse previous warning level, please manually select a warning level.";if(statelem){statelem.error(message)}else{alert(message)}return}else{date=date||new Date;const autoTimeout=new Morebits.Date(latest.date.getTime()).add(parseInt(Twinkle.getPref("autolevelStaleDays"),10),"days");if(autoTimeout.isAfter(date)){if(level===4){level=4;if(!statelem){const $link=$("<a>").attr("href","#").text("click here to open the ARV tool.").css("fontWeight","bold").on("click",()=>{Morebits.wiki.actionCompleted.redirect=null;Twinkle.warn.dialog.close();Twinkle.arv.callback(mw.config.get("wgRelevantUserName"));$("input[name=page]").val(params.article);$("input[value=final]").prop("checked",true)});const $statusNode=$("<div>").text(mw.config.get("wgRelevantUserName")+" recently received a level 4 warning ("+latest.type+") so it might be better to report them instead; ").css("color","red");$statusNode.append($link[0]);$autolevelMessage.append($statusNode)}}else{level+=1}}else{level=1}}$autolevelMessage.prepend($('<div>Will issue a <span style="font-weight: bold;">level '+level+"</span> template.</div>"));$("#twinkle-warn-autolevel-message").remove();$autolevelMessage.insertAfter($("#twinkle-warn-warning-messages"));let template=params.sub_group.replace(/(.*)\d$/,"$1");if(params.messageData&&!params.messageData["level"+level]){template="uw-generic"}template+=level;return[template,level]},main:function(pageobj){const text=pageobj.getPageText();const statelem=pageobj.getStatusElement();const params=pageobj.getCallbackParameters();let messageData=params.messageData;const[latest,history]=Twinkle.warn.callbacks.dateProcessing(text);const now=new Morebits.Date(pageobj.getLoadTime());Twinkle.warn.talkpageObj=pageobj;if(params.main_group==="autolevel"){const templateAndLevel=Twinkle.warn.callbacks.autolevelParseWikitext(text,params,latest,now,statelem);if(params.sub_group!==templateAndLevel[0]&&!confirm("Will issue a {{"+templateAndLevel[0]+"}} template to the user, okay?")){statelem.error("aborted per user request");return}params.sub_group=templateAndLevel[0];messageData=params.messageData["level"+templateAndLevel[1]]}else if(params.sub_group in history){if(new Morebits.Date(history[params.sub_group]).add(1,"day").isAfter(now)){if(!confirm("An identical "+params.sub_group+" has been issued in the last 24 hours. \nWould you still like to add this warning/notice?")){statelem.error("aborted per user request");return}}}latest.date.add(1,"minute");if(latest.date.isAfter(now)){if(!confirm("A "+latest.type+" has been issued in the last minute. \nWould you still like to add this warning/notice?")){statelem.error("aborted per user request");return}}const customProcess=function(template){template=template.split("|")[0];let prefix;switch(template.slice(-1)){case"1":prefix="General note";break;case"2":prefix="Caution";break;case"3":prefix="Warning";break;case"4":prefix="Final warning";break;case"m":if(template.slice(-3)==="4im"){prefix="Only warning";break}default:prefix="Notice";break}return prefix+": "+Morebits.string.toUpperCaseFirstChar(messageData.label)};let summary;if(params.main_group==="custom"){summary=customProcess(params.sub_group)}else{if(params.main_group==="kitchensink"&&!/^D+$/.test(params.sub_group)){let sub=params.sub_group.slice(-1);if(sub==="m"){sub=params.sub_group.slice(-3)}if(/\d/.test(sub)){params.main_group="level"+sub}}summary=messageData.summary||messageData[params.main_group]&&messageData[params.main_group].summary;if(!summary){summary=customProcess(params.sub_group)}if(messageData.suppressArticleInSummary!==true&¶ms.article){if(params.sub_group==="uw-agf-sock"||params.sub_group==="uw-socksuspect"||params.sub_group==="uw-aiv"){summary+=" of [[:User:"+params.article+"]]"}else{summary+=" on [[:"+params.article+"]]"}}}pageobj.setEditSummary(summary+".");pageobj.setChangeTags(Twinkle.changeTags);pageobj.setWatchlist(Twinkle.getPref("watchWarnings"));const warningText=Twinkle.warn.callbacks.getWarningWikitext(params.sub_group,params.article,params.reason,params.main_group==="custom");let sectionExists=false,sectionNumber=0;if(!messageData.heading&&text.length){const sections=text.match(/^(==*).+\1/gm);if(sections&§ions.length!==0){const dateHeaderRegex=now.monthHeaderRegex();sectionNumber=0;sectionExists=sections.reverse().some((sec,idx)=>/^(==)[^=].+\1/m.test(sec)&&dateHeaderRegex.test(sec)&&typeof(sectionNumber=sections.length-1-idx)==="number")}}if(sectionExists){pageobj.setPageSection(sectionNumber+1);pageobj.setAppendText("\n\n"+warningText);pageobj.append()}else{if(messageData.heading){pageobj.setNewSectionTitle(messageData.heading)}else{Morebits.Status.info("Info","Will create a new talk page section for this month, as none was found");pageobj.setNewSectionTitle(now.monthHeader(0))}pageobj.setNewSectionText(warningText);pageobj.newSection()}}};Twinkle.warn.callback.evaluate=function twinklewarnCallbackEvaluate(e){const userTalkPage="User_talk:"+mw.config.get("wgRelevantUserName");const params=Morebits.QuickForm.getInputData(e.target);if(params.sub_group==="uw-username"&&!params.article){alert("You must supply a reason for the {{uw-username}} template.");return}const $selectedEl=$(e.target.sub_group).find('option[value="'+$(e.target.sub_group).val()+'"]');params.messageData=$selectedEl.data("messageData");Morebits.SimpleWindow.setButtonsEnabled(false);Morebits.Status.init(e.target);Morebits.wiki.actionCompleted.redirect=userTalkPage;Morebits.wiki.actionCompleted.notice="Warning complete, reloading talk page in a few seconds";const wikipedia_page=new Morebits.wiki.Page(userTalkPage,"User talk page modification");wikipedia_page.setCallbackParameters(params);wikipedia_page.setFollowRedirect(true,false);wikipedia_page.load(Twinkle.warn.callbacks.main)};Twinkle.addInitCallback(Twinkle.warn,"warn")})();
// </nowiki>
cwx8u10a2g2x9zmi5svtv0qh5a2xomb
User:Syunsyunminmin/Twinkle/twinkleprotect.js
2
179401
769466
2026-10-21T13:58:10Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 3680108:
769466
javascript
text/javascript
/*
Twinkle: adapted for Japanese Wikipedia; upstream authors and Japanese adaptations are credited in UPSTREAM.md.
Upstream source: https://github.com/wikimedia-gadgets/twinkle/blob/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/src/modules/twinkleprotect.js
Upstream author history: https://github.com/wikimedia-gadgets/twinkle/commits/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/src/modules/twinkleprotect.js
License: CC BY-SA 3.0 Unported. See https://github.com/wikimedia-gadgets/twinkle/blob/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/LICENSE.txt
Japanese adaptation: Syunsyunminmin; includes later synchronization changes.
*/
// <nowiki>
(function() {
/*
****************************************
*** twinkleprotect.js: Protect/RPP module
****************************************
* Mode of invocation: Tab ("PP"/"RPP")
* Active on: Non-special, non-MediaWiki pages
*/
// Note: a lot of code in this module is re-used/called by batchprotect.
Twinkle.protect = function twinkleprotect() {
if (mw.config.get('wgNamespaceNumber') < 0 || mw.config.get('wgNamespaceNumber') === 8) {
return;
}
Twinkle.addPortletLink(Twinkle.protect.callback, Morebits.userIsSysop ? 'PP' : 'RPP', 'tw-rpp',
Morebits.userIsSysop ? 'ページを保護' : 'ページの保護を依頼');
};
Twinkle.protect.callback = function twinkleprotectCallback() {
var Window = new Morebits.SimpleWindow(620, 530);
Window.setTitle(Morebits.userIsSysop ? 'ページ保護の適用、依頼、タグ付け' : 'ページ保護の依頼、タグ付け');
Window.setScriptName('Twinkle');
Window.addFooterLink('保護テンプレート', 'Template:Pp#関連項目');
Window.addFooterLink('保護の方針', 'Wikipedia:保護の方針');
Window.addFooterLink('Twinkle help', ':en:WP:TW/DOC#protect');
Window.addFooterLink('フィードバック', 'User talk:Syunsyunminmin/Twinkle');
const form = new Morebits.QuickForm(Twinkle.protect.callback.evaluate);
const actionfield = form.append({
type: 'field',
label: '操作の種類'
});
if (Morebits.userIsSysop) {
actionfield.append({
type: 'radio',
name: 'actiontype',
event: Twinkle.protect.callback.changeAction,
list: [
{
label: 'ページを保護',
value: 'protect',
tooltip: 'ページに保護を適用する。',
checked: true
}
]
});
}
actionfield.append({
type: 'radio',
name: 'actiontype',
event: Twinkle.protect.callback.changeAction,
list: [
{
label: 'ページの保護を依頼',
value: 'request',
tooltip: (Morebits.userIsSysop ? '自分で保護する代わりに' : '') + 'WP:RFPPで保護を依頼する場合',
checked: !Morebits.userIsSysop
},
{
label: '保護テンプレートをページにタグ付けする',
value: 'tag',
tooltip: '保護する管理者が保護テンプレートを貼り忘れたり、タグを付けずにページを保護した場合、これを使って適切な保護タグを貼り付けることができます。',
disabled: mw.config.get('wgArticleId') === 0 || mw.config.get('wgPageContentModel') === 'Scribunto' || mw.config.get('wgNamespaceNumber') === 710
}
]
});
form.append({ type: 'field', label: 'プリセット', name: 'field_preset' });
form.append({ type: 'field', label: '1', name: 'field1' });
form.append({ type: 'field', label: '2', name: 'field2' });
form.append({ type: 'submit' });
const result = form.render();
Window.setContent(result);
Window.display();
// We must init the controls
const evt = document.createEvent('Event');
evt.initEvent('change', true, true);
result.actiontype[0].dispatchEvent(evt);
// get current protection level asynchronously
Twinkle.protect.fetchProtectionLevel();
};
// A list of bots who may be the protecting sysop, for whom we shouldn't
// remind the user contact before requesting unprotection (evaluate)
Twinkle.protect.trustedBots = ['MusikBot II', 'TFA Protector Bot'];
// Customizable namespace and FlaggedRevs settings
// In theory it'd be nice to have restrictionlevels defined here,
// but those are only available via a siteinfo query
// mw.loader.getState('ext.flaggedRevs.review') returns null if the
// FlaggedRevs extension is not registered. Previously, this was done with
// wgFlaggedRevsParams, but after 1.34-wmf4 it is no longer exported if empty
// (https://gerrit.wikimedia.org/r/c/mediawiki/extensions/FlaggedRevs/+/508427)
const hasFlaggedRevs = mw.loader.getState('ext.flaggedRevs.review') &&
// FlaggedRevs only valid in some namespaces, hardcoded until [[phab:T218479]]
(mw.config.get('wgNamespaceNumber') === 0 || mw.config.get('wgNamespaceNumber') === 4);
// Limit template editor; a Twinkle restriction, not a site setting
// Contains the current protection level in an object
// Once filled, it will look something like:
// { edit: { level: "sysop", expiry: <some date>, cascade: true }, ... }
Twinkle.protect.currentProtectionLevels = {};
// returns a jQuery Deferred object, usage:
// Twinkle.protect.fetchProtectingAdmin(apiObject, pageName, protect/stable).done(function(admin_username) { ...code... });
Twinkle.protect.fetchProtectingAdmin = function twinkleprotectFetchProtectingAdmin(api, pageName, protType, logIds) {
logIds = logIds || [];
return api.get({
format: 'json',
action: 'query',
list: 'logevents',
letitle: pageName,
letype: protType
}).then((data) => {
// don't check log entries that have already been checked (e.g. don't go into an infinite loop!)
const event = data.query ? $.grep(data.query.logevents, (le) => $.inArray(le.logid, logIds))[0] : null;
if (!event) {
// fail gracefully
return null;
} else if (event.action === 'move_prot' || event.action === 'move_stable') {
return twinkleprotectFetchProtectingAdmin(api, protType === 'protect' ? event.params.oldtitle_title : event.params.oldtitle, protType, logIds.concat(event.logid));
}
return event.user;
});
};
Twinkle.protect.fetchProtectionLevel = function twinkleprotectFetchProtectionLevel() {
const api = new mw.Api();
const protectDeferred = api.get({
format: 'json',
indexpageids: true,
action: 'query',
list: 'logevents',
letype: 'protect',
letitle: mw.config.get('wgPageName'),
prop: hasFlaggedRevs ? 'info|flagged' : 'info',
inprop: 'protection|watched',
titles: mw.config.get('wgPageName')
});
const stableDeferred = api.get({
format: 'json',
action: 'query',
list: 'logevents',
letype: 'stable',
letitle: mw.config.get('wgPageName')
});
const earlyDecision = [protectDeferred];
if (hasFlaggedRevs) {
earlyDecision.push(stableDeferred);
}
$.when.apply($, earlyDecision).done((protectData, stableData) => {
// $.when.apply is supposed to take an unknown number of promises
// via an array, which it does, but the type of data returned varies.
// If there are two or more deferreds, it returns an array (of objects),
// but if there's just one deferred, it retuns a simple object.
// This is annoying.
protectData = $(protectData).toArray();
const pageid = protectData[0].query.pageids[0];
const page = protectData[0].query.pages[pageid];
const current = {};
let adminEditDeferred;
// Save requested page's watched status for later in case needed when filing request
Twinkle.protect.watched = page.watchlistexpiry || page.watched === '';
$.each(page.protection, (index, protection) => {
// Don't overwrite actual page protection with cascading protection
if (!protection.source) {
current[protection.type] = {
level: protection.level,
expiry: protection.expiry,
cascade: protection.cascade === ''
};
// logs report last admin who made changes to either edit/move/create protection, regardless if they only modified one of them
if (!adminEditDeferred) {
adminEditDeferred = Twinkle.protect.fetchProtectingAdmin(api, mw.config.get('wgPageName'), 'protect');
}
} else {
// Account for the page being covered by cascading protection
current.cascading = {
expiry: protection.expiry,
source: protection.source,
level: protection.level // should always be sysop, unused
};
}
});
if (page.flagged) {
current.stabilize = {
level: page.flagged.protection_level,
expiry: page.flagged.protection_expiry
};
adminEditDeferred = Twinkle.protect.fetchProtectingAdmin(api, mw.config.get('wgPageName'), 'stable');
}
// show the protection level and log info
Twinkle.protect.hasProtectLog = !!protectData[0].query.logevents.length;
Twinkle.protect.protectLog = Twinkle.protect.hasProtectLog && protectData[0].query.logevents;
Twinkle.protect.hasStableLog = hasFlaggedRevs ? !!stableData[0].query.logevents.length : false;
Twinkle.protect.stableLog = Twinkle.protect.hasStableLog && stableData[0].query.logevents;
Twinkle.protect.currentProtectionLevels = current;
if (adminEditDeferred) {
adminEditDeferred.done((admin) => {
if (admin) {
$.each(['edit', 'move', 'create', 'stabilize', 'cascading'], (i, type) => {
if (Twinkle.protect.currentProtectionLevels[type]) {
Twinkle.protect.currentProtectionLevels[type].admin = admin;
}
});
}
Twinkle.protect.callback.showLogAndCurrentProtectInfo();
});
} else {
Twinkle.protect.callback.showLogAndCurrentProtectInfo();
}
});
};
Twinkle.protect.callback.showLogAndCurrentProtectInfo = function twinkleprotectCallbackShowLogAndCurrentProtectInfo() {
const currentlyProtected = !$.isEmptyObject(Twinkle.protect.currentProtectionLevels);
if (Twinkle.protect.hasProtectLog || Twinkle.protect.hasStableLog) {
const $linkMarkup = $('<span>');
if (Twinkle.protect.hasProtectLog) {
$linkMarkup.append(
$('<a target="_blank" href="' + mw.util.getUrl('Special:Log', {action: 'view', page: mw.config.get('wgPageName'), type: 'protect'}) + '">保護記録</a>'));
if (!currentlyProtected || (!Twinkle.protect.currentProtectionLevels.edit && !Twinkle.protect.currentProtectionLevels.move)) {
const lastProtectAction = Twinkle.protect.protectLog[0];
if (lastProtectAction.action === 'unprotect') {
$linkMarkup.append(' (' + new Morebits.Date(lastProtectAction.timestamp).calendar('utc') + 'に保護解除)');
} else { // protect or modify
$linkMarkup.append(' (' + new Morebits.Date(lastProtectAction.params.details[0].expiry).calendar('utc') + 'に期限切れ)');
}
}
$linkMarkup.append(Twinkle.protect.hasStableLog ? $('<span> • </span>') : null);
}
if (Twinkle.protect.hasStableLog) {
$linkMarkup.append($('<a target="_blank" href="' + mw.util.getUrl('Special:Log', {action: 'view', page: mw.config.get('wgPageName'), type: 'stable'}) + '">pending changes log</a>)'));
if (!currentlyProtected || !Twinkle.protect.currentProtectionLevels.stabilize) {
const lastStabilizeAction = Twinkle.protect.stableLog[0];
if (lastStabilizeAction.action === 'reset') {
$linkMarkup.append(' (reset ' + new Morebits.Date(lastStabilizeAction.timestamp).calendar('utc') + ')');
} else { // config or modify
$linkMarkup.append(' (expired ' + new Morebits.Date(lastStabilizeAction.params.expiry).calendar('utc') + ')');
}
}
}
Morebits.Status.init($('div[name="hasprotectlog"] span')[0]);
Morebits.Status.warn(
currentlyProtected ? '以前の保護' : 'このページは過去に保護されたことがあります',
$linkMarkup[0]
);
}
Morebits.Status.init($('div[name="currentprot"] span')[0]);
let protectionNode = [], statusLevel = 'info';
if (currentlyProtected) {
$.each(Twinkle.protect.currentProtectionLevels, (type, settings) => {
var label = type === 'stabilize' ? 'Pending Changes' : '';
switch (Morebits.string.toUpperCaseFirstChar(type)) {
case 'Edit':
label = '編集';
break;
case 'Move':
label = '移動';
break;
case 'create':
label = '作成';
break;
case 'upload':
label = 'アップロード';
break;
default:
label = Morebits.string.toUpperCaseFirstChar(type);
break;
}
if (type === 'cascading') { // Covered by another page
label = 'カスケード保護 ';
protectionNode.push($('<b>' + label + '</b>')[0]);
if (settings.source) { // Should by definition exist
const sourceLink = '<a target="_blank" href="' + mw.util.getUrl(settings.source) + '">' + settings.source + '</a>';
protectionNode.push($('<span>from ' + sourceLink + '</span>')[0]);
}
} else {
var level;
switch (settings.level) {
case 'sysop':
level = '管理者のみ';
break;
case 'extendedconfirmed':
level = '拡張承認された利用者のみ';
break;
case 'autoconfirmed':
level = '自動承認された利用者のみ';
break;
default:
level = settings.level;
break;
}
// Make cascading protection more prominent
if (settings.cascade) {
level += ' (カスケード)';
}
protectionNode.push($('<b>' + label + ': ' + level + '</b>')[0]);
}
if (settings.expiry === 'infinity') {
protectionNode.push(' (無期限) ');
} else {
protectionNode.push(' (' + new Morebits.Date(settings.expiry).calendar('utc') + 'に期限切れ) ');
}
if (settings.admin) {
var adminLink = '<a target="_blank" href="' + mw.util.getUrl('User talk:' + settings.admin) + '">' + settings.admin + '</a>';
protectionNode.push($('<span>' + adminLink + 'による</span>')[0]);
}
protectionNode.push($('<span> \u2022 </span>')[0]);
});
protectionNode = protectionNode.slice(0, -1); // remove the trailing bullet
statusLevel = 'warn';
} else {
protectionNode.push($('<b>保護なし</b>')[0]);
}
Morebits.Status[statusLevel]('現在の保護レベル', protectionNode);
};
Twinkle.protect.callback.changeAction = function twinkleprotectCallbackChangeAction(e) {
let field_preset;
let field1;
let field2;
switch (e.target.values) {
case 'protect':
field_preset = new Morebits.QuickForm.Element({ type: 'field', label: 'プリセット', name: 'field_preset' });
field_preset.append({
type: 'select',
name: 'category',
label: 'プリセットを選択:',
event: Twinkle.protect.callback.changePreset,
list: mw.config.get('wgArticleId') ? Twinkle.protect.protectionTypes : Twinkle.protect.protectionTypesCreate
});
field2 = new Morebits.QuickForm.Element({ type: 'field', label: '保護設定', name: 'field2' });
field2.append({ type: 'div', name: 'currentprot', label: ' ' }); // holds the current protection level, as filled out by the async callback
field2.append({ type: 'div', name: 'hasprotectlog', label: ' ' });
// for existing pages
if (mw.config.get('wgArticleId')) {
field2.append({
type: 'checkbox',
event: Twinkle.protect.formevents.editmodify,
list: [
{
label: '編集保護設定を変更',
name: 'editmodify',
tooltip: 'これをオフにすると、編集保護レベルと有効期限はそのままになります。',
checked: true
}
]
});
field2.append({
type: 'select',
name: 'editlevel',
label: '誰が編集できるか:',
event: Twinkle.protect.formevents.editlevel,
list: Twinkle.protect.protectionLevels
});
field2.append({
type: 'select',
name: 'editexpiry',
label: '期限:',
event: function(e) {
if (e.target.value === 'custom') {
Twinkle.protect.doCustomExpiry(e.target);
}
},
// default expiry selection (1 week) is conditionally set in Twinkle.protect.callback.changePreset
list: Twinkle.protect.protectionLengths
});
field2.append({
type: 'checkbox',
event: Twinkle.protect.formevents.movemodify,
list: [
{
label: '移動保護設定を変更',
name: 'movemodify',
tooltip: 'これをオフにすると、移動保護レベルと有効期限はそのままになります。',
checked: true
}
]
});
field2.append({
type: 'select',
name: 'movelevel',
label: '誰が移動できるか:',
event: Twinkle.protect.formevents.movelevel,
// Autoconfirmed is required for a move, redundant.
list: Twinkle.protect.protectionLevels.filter((level) => level.value !== 'autoconfirmed')
});
field2.append({
type: 'select',
name: 'moveexpiry',
label: '期限:',
event: function(e) {
if (e.target.value === 'custom') {
Twinkle.protect.doCustomExpiry(e.target);
}
},
// default expiry selection (2 days) is conditionally set in Twinkle.protect.callback.changePreset
list: Twinkle.protect.protectionLengths
});
if (hasFlaggedRevs) {
field2.append({
type: 'checkbox',
event: Twinkle.protect.formevents.pcmodify,
list: [
{
label: 'Modify pending changes protection',
name: 'pcmodify',
tooltip: 'If this is turned off, the pending changes level, and expiry time, will be left as is.',
checked: true
}
]
});
field2.append({
type: 'select',
name: 'pclevel',
label: 'Pending changes:',
event: Twinkle.protect.formevents.pclevel,
list: [
{ label: 'None', value: 'none' },
{ label: 'Pending change', value: 'autoconfirmed', selected: true }
]
});
field2.append({
type: 'select',
name: 'pcexpiry',
label: 'Expires:',
event: function(e) {
if (e.target.value === 'custom') {
Twinkle.protect.doCustomExpiry(e.target);
}
},
// default expiry selection (1 month) is conditionally set in Twinkle.protect.callback.changePreset
list: Twinkle.protect.protectionLengths
});
}
} else { // for non-existing pages
field2.append({
type: 'select',
name: 'createlevel',
label: '作成保護:',
event: Twinkle.protect.formevents.createlevel,
list: Twinkle.protect.protectionLevels
});
field2.append({
type: 'select',
name: 'createexpiry',
label: '期限:',
event: function(e) {
if (e.target.value === 'custom') {
Twinkle.protect.doCustomExpiry(e.target);
}
},
// default expiry selection (indefinite) is conditionally set in Twinkle.protect.callback.changePreset
list: Twinkle.protect.protectionLengths
});
}
field2.append({
type: 'textarea',
name: 'protectReason',
label: '理由 (保護記録用):'
});
field2.append({
type: 'div',
name: 'protectReason_notes',
label: '注記:',
style: 'display:inline-block; margin-top:4px;',
tooltip: '保護記録に、RFPPで依頼されたというメモを追加する。'
});
field2.append({
type: 'checkbox',
event: Twinkle.protect.callback.annotateProtectReason,
style: 'display:inline-block; margin-top:4px;',
list: [
{
label: 'RFPP依頼',
name: 'protectReason_notes_rfpp',
checked: false,
value: '[[WP:RFPP]]による'
}
]
});
field2.append({
type: 'input',
event: Twinkle.protect.callback.annotateProtectReason,
label: 'RFPP版番号',
name: 'protectReason_notes_rfppRevid',
value: '',
tooltip: '保護が依頼されたRFPPページの版番号。'
});
if (!mw.config.get('wgArticleId') || mw.config.get('wgPageContentModel') === 'Scribunto' || mw.config.get('wgNamespaceNumber') === 710) { // tagging isn't relevant for non-existing, module, or TimedText pages
break;
}
/* falls through */
case 'tag':
field1 = new Morebits.QuickForm.Element({ type: 'field', label: 'タグ付け設定', name: 'field1' });
field1.append({ type: 'div', name: 'currentprot', label: ' ' }); // holds the current protection level, as filled out by the async callback
field1.append({ type: 'div', name: 'hasprotectlog', label: ' ' });
field1.append({
type: 'select',
name: 'tagtype',
label: '保護テンプレートを選択:',
list: Twinkle.protect.protectionTags,
event: Twinkle.protect.formevents.tagtype
});
var isTemplateNamespace = mw.config.get('wgNamespaceNumber') === 10;
var isAFD = Morebits.pageNameNorm.startsWith('Wikipedia:削除依頼/');
var isCode = ['javascript', 'css', 'sanitized-css'].includes(mw.config.get('wgPageContentModel'));
field1.append({
type: 'checkbox',
list: [
{
name: 'small',
label: 'アイコン化 (small=yes)',
tooltip: 'テンプレートの|small=yes機能を使用し、インジケーターとしてのみ表示する。',
checked: true
},
{
name: 'noinclude',
label: '保護テンプレートを<noinclude>で囲む',
tooltip: '参照読み込みされないように保護テンプレートを<noinclude>で囲む',
checked: (isTemplateNamespace || isAFD) && !isCode
}
]
});
break;
case 'request':
field_preset = new Morebits.QuickForm.Element({ type: 'field', label: '保護の種類', name: 'field_preset' });
field_preset.append({
type: 'select',
name: 'category',
label: '種類と理由:',
event: Twinkle.protect.callback.changePreset,
list: mw.config.get('wgArticleId') ? Twinkle.protect.protectionTypes : Twinkle.protect.protectionTypesCreate
});
field_preset.append({
type: 'checkbox',
list: [
{
name: 'req_template',
label: '保護依頼テンプレートを貼り付ける',
tooltip: '保護を依頼するページに{{保護依頼}}テンプレートを貼り付ける',
checked: true
},
{
name: 'noinclude',
label: '保護テンプレートを<noinclude>で囲む',
tooltip: '参照読み込みされないように保護依頼テンプレートを<noinclude>で囲む',
checked: mw.config.get('wgNamespaceNumber') === 10 || (mw.config.get('wgNamespaceNumber') === mw.config.get('wgNamespaceIds').project && mw.config.get('wgTitle').indexOf('削除依頼/') === 0)
}
]
});
field1 = new Morebits.QuickForm.Element({ type: 'field', label: 'オプション', name: 'field1' });
field1.append({ type: 'div', name: 'currentprot', label: ' ' }); // holds the current protection level, as filled out by the async callback
field1.append({ type: 'div', name: 'hasprotectlog', label: ' ' });
field1.append({
type: 'textarea',
name: 'reason',
label: '理由:'
});
break;
default:
alert('twinkleprotectにて何かが進行中です');
break;
}
let oldfield;
if (field_preset) {
oldfield = $(e.target.form).find('fieldset[name="field_preset"]')[0];
oldfield.parentNode.replaceChild(field_preset.render(), oldfield);
} else {
$(e.target.form).find('fieldset[name="field_preset"]').css('display', 'none');
}
if (field1) {
oldfield = $(e.target.form).find('fieldset[name="field1"]')[0];
oldfield.parentNode.replaceChild(field1.render(), oldfield);
} else {
$(e.target.form).find('fieldset[name="field1"]').css('display', 'none');
}
if (field2) {
oldfield = $(e.target.form).find('fieldset[name="field2"]')[0];
oldfield.parentNode.replaceChild(field2.render(), oldfield);
} else {
$(e.target.form).find('fieldset[name="field2"]').css('display', 'none');
}
if (e.target.values === 'protect') {
// fake a change event on the preset dropdown
const evt = document.createEvent('Event');
evt.initEvent('change', true, true);
e.target.form.category.dispatchEvent(evt);
// reduce vertical height of dialog
$(e.target.form).find('fieldset[name="field2"] select').parent().css({ display: 'inline-block', marginRight: '0.5em' });
$(e.target.form).find('fieldset[name="field2"] input[name="protectReason_notes_rfppRevid"]').parent().css({display: 'inline-block', marginLeft: '15px'}).hide();
}
// re-add protection level and log info, if it's available
Twinkle.protect.callback.showLogAndCurrentProtectInfo();
};
// NOTE: This function is used by batchprotect as well
Twinkle.protect.formevents = {
editmodify: function twinkleprotectFormEditmodifyEvent(e) {
e.target.form.editlevel.disabled = !e.target.checked;
e.target.form.editexpiry.disabled = !e.target.checked || (e.target.form.editlevel.value === 'all');
e.target.form.editlevel.style.color = e.target.form.editexpiry.style.color = e.target.checked ? '' : 'transparent';
},
editlevel: function twinkleprotectFormEditlevelEvent(e) {
e.target.form.editexpiry.disabled = e.target.value === 'all';
},
movemodify: function twinkleprotectFormMovemodifyEvent(e) {
// sync move settings with edit settings if applicable
if (e.target.form.movelevel.disabled && !e.target.form.editlevel.disabled) {
e.target.form.movelevel.value = e.target.form.editlevel.value;
e.target.form.moveexpiry.value = e.target.form.editexpiry.value;
} else if (e.target.form.editlevel.disabled) {
e.target.form.movelevel.value = 'sysop';
e.target.form.moveexpiry.value = 'infinity';
}
e.target.form.movelevel.disabled = !e.target.checked;
e.target.form.moveexpiry.disabled = !e.target.checked || (e.target.form.movelevel.value === 'all');
e.target.form.movelevel.style.color = e.target.form.moveexpiry.style.color = e.target.checked ? '' : 'transparent';
},
movelevel: function twinkleprotectFormMovelevelEvent(e) {
e.target.form.moveexpiry.disabled = e.target.value === 'all';
},
pcmodify: function twinkleprotectFormPcmodifyEvent(e) {
e.target.form.pclevel.disabled = !e.target.checked;
e.target.form.pcexpiry.disabled = !e.target.checked || (e.target.form.pclevel.value === 'none');
e.target.form.pclevel.style.color = e.target.form.pcexpiry.style.color = e.target.checked ? '' : 'transparent';
},
pclevel: function twinkleprotectFormPclevelEvent(e) {
e.target.form.pcexpiry.disabled = e.target.value === 'none';
},
createlevel: function twinkleprotectFormCreatelevelEvent(e) {
e.target.form.createexpiry.disabled = e.target.value === 'all';
},
tagtype: function twinkleprotectFormTagtypeEvent(e) {
e.target.form.small.disabled = e.target.form.noinclude.disabled = (e.target.value === 'none') || (e.target.value === 'noop');
}
};
Twinkle.protect.doCustomExpiry = function twinkleprotectDoCustomExpiry(target) {
const custom = prompt('Enter a custom expiry time. \nYou can use relative times, like "1 minute" or "19 days", or absolute timestamps, "yyyymmddhhmm" (e.g. "200602011405" is Feb 1, 2006, at 14:05 UTC).', '');
if (custom) {
const option = document.createElement('option');
option.setAttribute('value', custom);
option.textContent = custom;
target.appendChild(option);
target.value = custom;
} else {
target.selectedIndex = 0;
}
};
// NOTE: This list is used by batchprotect as well
Twinkle.protect.protectionLevels = [
{ label: '全ての利用者', value: 'all' },
{ label: '自動承認された利用者', value: 'autoconfirmed' },
{ label: '拡張承認された利用者', value: 'extendedconfirmed' },
{ label: '管理者', value: 'sysop', selected: true }
];
// default expiry selection is conditionally set in Twinkle.protect.callback.changePreset
// NOTE: This list is used by batchprotect as well
Twinkle.protect.protectionLengths = [
{ label: '1時間', value: '1 hour' },
{ label: '2時間', value: '2 hours' },
{ label: '3時間', value: '3 hours' },
{ label: '6時間', value: '6 hours' },
{ label: '12時間', value: '12 hours' },
{ label: '1日間', value: '1 day' },
{ label: '2日間', value: '2 days' },
{ label: '3日間', value: '3 days' },
{ label: '4日間', value: '4 days' },
{ label: '1週間', value: '1 week' },
{ label: '2週間', value: '2 weeks' },
{ label: '1ヶ月間', value: '1 month' },
{ label: '2ヶ月間', value: '2 months' },
{ label: '3ヶ月間', value: '3 months' },
{ label: '1年', value: '1 year' },
{ label: '無期限', value: 'infinity' },
{ label: 'その他', value: 'custom' }
];
Twinkle.protect.protectionTypes = [
{ label: '保護解除', value: 'unprotect' },
{
label: '全保護',
list: [
{ label: '全般 (全)', value: 'pp' },
{ label: '編集合戦 (全)', value: 'pp-dispute' },
{ label: '拡張承認された利用者による問題投稿の繰り返し (全)', value: 'pp-vandalism' },
{ label: '影響が特に大きいテンプレート (全)', value: 'pp-template' }
]
},
{
label: '拡張半保護',
list: [
{ label: '全般 (拡半)', value: 'pp-120-500-protected' },
{ label: '(自動)承認された利用者による問題投稿の繰り返し (拡半)', value: 'pp-120-500-vandalism' },
{ label: 'プライバシー侵害 (拡半)', value: 'pp-120-500-blp' },
{ label: 'ソックパペット (拡半)', value: 'pp-120-500-sock' },
{ label: '影響が特に大きいテンプレート (拡半)', value: 'pp-120-500-template' }
]
},
{
label: '半保護',
list: [
{ label: '全般 (半)', value: 'pp-semi-protected' },
{ label: 'IP・新規利用者による問題投稿の繰り返し (半)', selected: true, value: 'pp-semi-vandalism' },
{ label: 'プライバシー侵害 (半)', value: 'pp-semi-blp' },
{ label: 'ソックパペット (半)', value: 'pp-semi-sock' },
{ label: '影響が特に大きいテンプレート (半)', value: 'pp-semi-template' }
]
},
{
label: '移動保護',
list: [
{ label: '全般 (移)', value: 'pp-move' },
{ label: '移動合戦 (移)', value: 'pp-move-dispute' },
{ label: '度重なる荒らし (移)', value: 'pp-move-vandalism' },
{ label: '移動不要ページ (移)', value: 'pp-move-indef' }
]
},
{
label: '移動拡張半保護',
list: [
{ label: '全般 (移拡)', value: 'pp-120-500-move' },
{ label: '度重なる荒らし (移拡)', value: 'pp-120-500-move-vandalism' }
]
}
];
Twinkle.protect.protectionTypesCreate = [
{ label: '保護解除', value: 'unprotect' },
{
label: '作成保護',
list: [
{ label: '全般 (全)', value: 'pp-create' },
{ label: 'ソックパペットによる問題投稿の繰り返し (全)', value: 'pp-create-sock' },
{ label: '拡張承認された利用者による問題投稿の繰り返し (全)', value: 'pp-create-vandalism' },
{ label: '度重なる宣伝 (全)', value: 'pp-create-spam' },
{ label: '削除されたページの改善なき再作成の繰り返し (全)', value: 'pp-create-salt' },
{ label: 'プライバシー侵害の記述の繰り返し (全)', value: 'pp-create-blp' }
]
},
{
label: '作成拡張半保護',
list: [
{ label: '全般 (拡)', value: 'pp-120-500-create' },
{ label: 'ソックパペットによる問題投稿の繰り返し (拡)', value: 'pp-120-500-create-sock' },
{ label: '(自動)承認された利用者による問題投稿の繰り返し (拡)', value: 'pp-120-500-create-vandalism' },
{ label: '度重なる宣伝 (拡)', value: 'pp-120-500-create-spam' },
{ label: '削除されたページの改善なき再作成の繰り返し (拡)', value: 'pp-120-500-create-salt' },
{ label: 'プライバシー侵害の記述の繰り返し (拡)', value: 'pp-120-500-create-blp' }
]
},
{
label: '作成半保護',
list: [
{ label: '全般 (半)', value: 'pp-semi-create' },
{ label: 'ソックパペットによる問題投稿の繰り返し (半)', value: 'pp-semi-create-sock' },
{ label: 'IP・新規利用者による問題投稿の繰り返し (半)', value: 'pp-semi-create-vandalism' },
{ label: '度重なる宣伝 (半)', value: 'pp-semi-create-spam' },
{ label: '削除されたページの改善なき再作成の繰り返し (半)', selected: true, value: 'pp-semi-create-salt' },
{ label: 'プライバシー侵害の記述の繰り返し (半)', value: 'pp-semi-create-blp' }
]
}
];
// NOTICE: keep this synched with [[MediaWiki:Protect-dropdown]]
// Also note: stabilize = Pending Changes level
// expiry will override any defaults
Twinkle.protect.protectionPresetsInfo = {
pp: {
edit: 'sysop',
move: 'sysop',
reason: null
},
'pp-dispute': {
edit: 'sysop',
move: 'sysop',
reason: '編集合戦'
},
'pp-vandalism': {
edit: 'sysop',
move: 'sysop',
reason: '拡張承認された利用者による問題投稿の繰り返し'
},
'pp-template': {
edit: 'sysop',
move: 'sysop',
expiry: 'infinity',
reason: '[[WP:HRT|影響が特に大きいテンプレート]]'
},
'pp-120-500-protected': {
edit: 'extendedconfirmed',
move: 'extendedconfirmed',
reason: null,
template: 'pp'
},
'pp-120-500-vandalism': {
edit: 'extendedconfirmed',
move: 'extendedconfirmed',
reason: '(自動)承認された利用者による問題投稿の繰り返し',
template: 'pp-vandalism'
},
'pp-120-500-blp': {
edit: 'extendedconfirmed',
move: 'extendedconfirmed',
reason: 'プライバシー侵害の記述の繰り返し',
template: 'pp-vandalism'
},
'pp-120-500-sock': {
edit: 'extendedconfirmed',
move: 'extendedconfirmed',
reason: '[[WP:SOCK|ソックパペット]]による問題投稿の繰り返し',
template: 'pp-vandalism'
},
'pp-120-500-template': {
edit: 'extendedconfirmed',
move: 'extendedconfirmed',
expiry: 'infinity',
reason: '[[WP:HRT|影響が特に大きいテンプレート]]',
template: 'pp-template'
},
'pp-semi-vandalism': {
edit: 'autoconfirmed',
reason: 'IP・新規利用者による問題投稿の繰り返し',
template: 'pp-vandalism'
},
'pp-semi-blp': {
edit: 'autoconfirmed',
reason: 'プライバシー侵害の記述の繰り返し',
template: 'pp-vandalism'
},
'pp-semi-template': { // removed for now
edit: 'autoconfirmed',
expiry: 'infinity',
reason: '[[WP:HRT|影響が特に大きいテンプレート]]',
template: 'pp-template'
},
'pp-semi-sock': {
edit: 'autoconfirmed',
reason: '[[WP:SOCK|ソックパペット]]による問題投稿の繰り返し',
template: 'pp-vandalism'
},
'pp-semi-protected': {
edit: 'autoconfirmed',
reason: null,
template: 'pp'
},
'pp-move': {
move: 'sysop',
reason: null
},
'pp-move-dispute': {
move: 'sysop',
expiry: '1 week',
reason: '移動合戦'
},
'pp-move-vandalism': {
move: 'sysop',
reason: '度重なる荒らし'
},
'pp-move-indef': {
move: 'sysop',
expiry: 'infinity',
reason: '移動不要ページ',
template: 'pp-move'
},
'pp-120-500-move': {
move: 'extendedconfirmed',
reason: null,
template: 'pp-move'
},
'pp-120-500-move-vandalism': {
move: 'extendedconfirmed',
reason: null,
template: 'pp-move-vandalism'
},
unprotect: {
edit: 'all',
move: 'all',
stabilize: 'none',
create: 'all',
reason: null,
template: 'none'
},
'pp-create-sock': {
create: 'sysop',
reason: '[[WP:SOCK|ソックパペット]]による問題投稿の繰り返し'
},
'pp-create': {
create: 'sysop',
reason: null
},
'pp-create-vandalism': {
create: 'sysop',
reason: '拡張承認された利用者による問題投稿の繰り返し'
},
'pp-create-spam': {
create: 'sysop',
reason: '度重なる宣伝'
},
'pp-create-salt': {
create: 'sysop',
reason: '削除されたページの改善なき再作成の繰り返し'
},
'pp-create-blp': {
create: 'sysop',
reason: 'プライバシー侵害の記述の繰り返し'
},
'pp-120-500-create': {
create: 'extendedconfirmed',
reason: null
},
'pp-120-500-create-sock': {
create: 'extendedconfirmed',
reason: '[[WP:SOCK|ソックパペット]]による問題投稿の繰り返し'
},
'pp-120-500-create-vandalism': {
create: 'extendedconfirmed',
reason: '(自動)承認された利用者による問題投稿の繰り返し'
},
'pp-120-500-create-spam': {
create: 'extendedconfirmed',
reason: '度重なる宣伝'
},
'pp-120-500-create-salt': {
create: 'extendedconfirmed',
reason: '削除されたページの改善なき再作成の繰り返し'
},
'pp-120-500-create-blp': {
create: 'sysop',
reason: 'プライバシー侵害の記述の繰り返し'
},
'pp-semi-create': {
create: 'autoconfirmed',
reason: null
},
'pp-semi-create-sock': {
create: 'autoconfirmed',
reason: '[[WP:SOCK|ソックパペット]]による問題投稿の繰り返し'
},
'pp-semi-create-vandalism': {
create: 'autoconfirmed',
reason: 'IP・新規利用者による問題投稿の繰り返し'
},
'pp-semi-create-spam': {
create: 'autoconfirmed',
reason: '度重なる宣伝'
},
'pp-semi-create-salt': {
create: 'autoconfirmed',
reason: '削除されたページの改善なき再作成の繰り返し'
},
'pp-semi-create-blp': {
create: 'autoconfirmed',
reason: 'プライバシー侵害の記述の繰り返し'
}
};
Twinkle.protect.protectionTags = [
{
label: 'なし (既にある保護テンプレートを除去する)',
value: 'none'
},
{
label: 'なし (既にある保護テンプレートを除去しない)',
value: 'noop'
},
{
label: '編集保護テンプレート',
list: [
{ label: '{{pp}}: 全般', value: 'pp' },
{ label: '{{pp-vandalism}}: 荒らし', value: 'pp-vandalism' },
{ label: '{{pp-dispute}}: 編集合戦', value: 'pp-dispute' },
{ label: '{{pp-template}}: 影響が特に大きいテンプレート', value: 'pp-template' },
{ label: '{{pp-semi-indef}}: 長期間の半保護', value: 'pp-semi-indef' }
]
},
{
label: '移動保護テンプレート',
list: [
{ label: '{{pp-move-dispute}}: 移動合戦', value: 'pp-move-dispute' },
{ label: '{{pp-move-vandalism}}: 度重なる荒らし', value: 'pp-move-vandalism' },
{ label: '{{pp-move}}: その他', value: 'pp-move' }
]
}
].filter((type) => hasFlaggedRevs || type.label !== 'Pending changes templates');
Twinkle.protect.callback.changePreset = function twinkleprotectCallbackChangePreset(e) {
const form = e.target.form;
const actiontypes = form.actiontype;
let actiontype;
for (let i = 0; i < actiontypes.length; i++) {
if (!actiontypes[i].checked) {
continue;
}
actiontype = actiontypes[i].values;
break;
}
if (actiontype === 'protect') { // actually protecting the page
const item = Twinkle.protect.protectionPresetsInfo[form.category.value];
if (mw.config.get('wgArticleId')) {
if (item.edit) {
form.editmodify.checked = true;
Twinkle.protect.formevents.editmodify({ target: form.editmodify });
form.editlevel.value = item.edit;
Twinkle.protect.formevents.editlevel({ target: form.editlevel });
} else {
form.editmodify.checked = false;
Twinkle.protect.formevents.editmodify({ target: form.editmodify });
}
if (item.move) {
form.movemodify.checked = true;
Twinkle.protect.formevents.movemodify({ target: form.movemodify });
form.movelevel.value = item.move;
Twinkle.protect.formevents.movelevel({ target: form.movelevel });
} else {
form.movemodify.checked = false;
Twinkle.protect.formevents.movemodify({ target: form.movemodify });
}
form.editexpiry.value = form.moveexpiry.value = item.expiry || '1 week';
if (form.pcmodify) {
if (item.stabilize) {
form.pcmodify.checked = true;
Twinkle.protect.formevents.pcmodify({ target: form.pcmodify });
form.pclevel.value = item.stabilize;
Twinkle.protect.formevents.pclevel({ target: form.pclevel });
} else {
form.pcmodify.checked = false;
Twinkle.protect.formevents.pcmodify({ target: form.pcmodify });
}
form.pcexpiry.value = item.expiry || '1 month';
}
} else {
if (item.create) {
form.createlevel.value = item.create;
Twinkle.protect.formevents.createlevel({ target: form.createlevel });
}
form.createexpiry.value = item.expiry || 'infinity';
}
const reasonField = actiontype === 'protect' ? form.protectReason : form.reason;
if (item.reason) {
reasonField.value = item.reason;
} else {
reasonField.value = '';
}
// Add any annotations
Twinkle.protect.callback.annotateProtectReason(e);
// sort out tagging options, disabled if nonexistent, lua, or TimedText
if (mw.config.get('wgArticleId') && mw.config.get('wgPageContentModel') !== 'Scribunto' && mw.config.get('wgNamespaceNumber') !== 710) {
if (form.category.value === 'unprotect') {
form.tagtype.value = 'none';
} else {
form.tagtype.value = item.template ? item.template : form.category.value;
}
Twinkle.protect.formevents.tagtype({ target: form.tagtype });
// Default settings for adding <noinclude> tags to protection templates
var isTemplateEditorProtection = form.category.value === 'pp-template';
var isAFD = Morebits.pageNameNorm.startsWith('Wikipedia:削除依頼/');
var isNotTemplateNamespace = mw.config.get('wgNamespaceNumber') !== 10;
var isCode = ['javascript', 'css', 'sanitized-css'].includes(mw.config.get('wgPageContentModel'));
if ((isTemplateEditorProtection || isAFD) && !isCode) {
form.noinclude.checked = true;
} else if (isCode || isNotTemplateNamespace) {
form.noinclude.checked = false;
}
}
} else { // RPP request
if (form.category.value === 'unprotect') {
form.expiry.value = '';
form.expiry.disabled = true;
} else {
form.expiry.value = '';
form.expiry.disabled = false;
}
}
};
Twinkle.protect.callback.evaluate = function twinkleprotectCallbackEvaluate(e) {
const form = e.target;
const input = Morebits.QuickForm.getInputData(form);
let tagparams;
if (input.actiontype === 'tag' || (input.actiontype === 'protect' && mw.config.get('wgArticleId') && mw.config.get('wgPageContentModel') !== 'Scribunto' && mw.config.get('wgNamespaceNumber') !== 710 /* TimedText */)) {
tagparams = {
tag: input.tagtype,
reason: false,
small: input.small,
noinclude: input.noinclude
};
} else if (input.actiontype === 'request') {
tagparams = {
tag: '保護依頼',
reason: false,
noinclude: input.noinclude
};
}
switch (input.actiontype) {
case 'protect':
// protect the page
Morebits.wiki.actionCompleted.redirect = mw.config.get('wgPageName');
Morebits.wiki.actionCompleted.notice = '保護完了';
var statusInited = false;
var thispage;
var allDone = function twinkleprotectCallbackAllDone() {
if (thispage) {
thispage.getStatusElement().info('完了');
}
if (tagparams) {
Twinkle.protect.callbacks.taggingPageInitial(tagparams);
}
};
var protectIt = function twinkleprotectCallbackProtectIt(next) {
thispage = new Morebits.wiki.Page(mw.config.get('wgPageName'), 'ページを保護');
if (mw.config.get('wgArticleId')) {
if (input.editmodify) {
thispage.setEditProtection(input.editlevel, input.editexpiry);
}
if (input.movemodify) {
// Ensure a level has actually been chosen
if (input.movelevel) {
thispage.setMoveProtection(input.movelevel, input.moveexpiry);
} else {
alert('移動保護のレベルを選択して下さい!');
return;
}
}
thispage.setWatchlist(Twinkle.getPref('watchProtectedPages'));
} else {
thispage.setCreateProtection(input.createlevel, input.createexpiry);
thispage.setWatchlist(false);
}
if (input.protectReason) {
thispage.setEditSummary(input.protectReason);
} else {
alert('保護理由を入力する必要があります。入力された理由は保護記録に記録されます。');
return;
}
if (input.protectReason_notes_rfppRevid && !/^\d+$/.test(input.protectReason_notes_rfppRevid)) {
alert('指定された版番号が無効です。正しい番号("oldid"とも言う)を見つける方法に関しては https://ja.wikipedia.org/wiki/Help:固定リンク をご覧ください。');
return;
}
if (!statusInited) {
Morebits.SimpleWindow.setButtonsEnabled(false);
Morebits.Status.init(form);
statusInited = true;
}
// thispage.setChangeTags(Twinkle.changeTags);
thispage.protect(next);
};
var stabilizeIt = function twinkleprotectCallbackStabilizeIt() {
if (thispage) {
thispage.getStatusElement().info('done');
}
thispage = new Morebits.wiki.Page(mw.config.get('wgPageName'), 'Applying pending changes protection');
thispage.setFlaggedRevs(input.pclevel, input.pcexpiry);
if (input.protectReason) {
thispage.setEditSummary(input.protectReason + Twinkle.summaryAd); // flaggedrevs tag support: [[phab:T247721]]
} else {
alert('You must enter a protect reason, which will be inscribed into the protection log.');
return;
}
if (!statusInited) {
Morebits.SimpleWindow.setButtonsEnabled(false);
Morebits.Status.init(form);
statusInited = true;
}
thispage.setWatchlist(Twinkle.getPref('watchProtectedPages'));
thispage.stabilize(allDone, (error) => {
if (error.errorCode === 'stabilize_denied') { // [[phab:T234743]]
thispage.getStatusElement().error('Failed trying to modify pending changes settings, likely due to a mediawiki bug. Other actions (tagging or regular protection) may have taken place. Please reload the page and try again.');
}
});
};
if (input.editmodify || input.movemodify || !mw.config.get('wgArticleId')) {
if (input.pcmodify) {
protectIt(stabilizeIt);
} else {
protectIt(allDone);
}
} else if (input.pcmodify) {
stabilizeIt();
} else {
alert("Twinkleに何かをさせてあげてください!\nページにタグを付けるだけなら、上部にある'保護テンプレートをページにタグ付けする'オプションを選ぶことができます。");
}
break;
case 'tag':
// apply a protection template
Morebits.SimpleWindow.setButtonsEnabled(false);
Morebits.Status.init(form);
Morebits.wiki.actionCompleted.redirect = mw.config.get('wgPageName');
Morebits.wiki.actionCompleted.followRedirect = false;
Morebits.wiki.actionCompleted.notice = 'タグ付け操作';
Twinkle.protect.callbacks.taggingPageInitial(tagparams);
break;
case 'request':
if (!input.reason) {
alert('保護が必要な理由を入力してください。');
return;
}
// file request at RFPP
var typename, typereason;
switch (input.category) {
case 'pp-dispute':
case 'pp-vandalism':
case 'pp':
case 'pp-template':
typename = '全保護';
break;
case 'pp-120-500-vandalism':
case 'pp-120-500-blp':
case 'pp-120-500-sock':
case 'pp-120-500-protected':
case 'pp-120-500-template':
typename = '拡張半保護';
break;
case 'pp-sock-noticeboard':
case 'pp-semi-vandalism':
case 'pp-semi-sock':
case 'pp-semi-blp':
case 'pp-semi-protected':
case 'pp-semi-template':
typename = '半保護';
break;
case 'pp-move':
case 'pp-move-dispute':
case 'pp-move-indef':
case 'pp-move-vandalism':
typename = '移動保護';
break;
case 'pp-120-500-move':
case 'pp-120-500-move-vandalism':
typename = '移動拡張半保護';
break;
case 'pp-create':
case 'pp-create-sock':
case 'pp-create-vandalism':
case 'pp-create-spam':
case 'pp-create-blp':
case 'pp-create-salt':
typename = '作成保護';
break;
case 'pp-120-500-create':
case 'pp-120-500-create-sock':
case 'pp-120-500-create-vandalism':
case 'pp-120-500-create-spam':
case 'pp-120-500-create-blp':
case 'pp-120-500-create-salt':
typename = '作成拡張半保護';
break;
case 'pp-semi-create':
case 'pp-semi-create-sock':
case 'pp-semi-create-vandalism':
case 'pp-semi-create-spam':
case 'pp-semi-create-blp':
case 'pp-semi-create-salt':
typename = '作成半保護';
break;
default:
typename = '保護解除';
break;
}
switch (input.category) {
case 'pp-dispute':
typereason = '編集合戦';
break;
case 'pp-vandalism':
case 'pp-create-vandalism':
typereason = '拡張承認された利用者による問題投稿の繰り返し';
break;
case 'pp-120-500-vandalism':
case 'pp-120-500-create-vandalism':
typereason = '(自動)承認された利用者による問題投稿の繰り返し';
break;
case 'pp-semi-vandalism':
case 'pp-semi-create-vandalism':
typereason = 'IP・新規利用者による問題投稿の繰り返し';
break;
case 'pp-move-vandalism':
case 'pp-move-120-500-vandalism':
typereason = '度重なる荒らし';
break;
case 'pp-template':
case 'pp-120-500-template':
case 'pp-semi-template':
typereason = '[[WP:HRT|影響が特に大きいテンプレート]]';
break;
case 'pp-sock-noticeboard':
case 'pp-semi-sock':
case 'pp-120-500-sock':
case 'pp-create-sock':
case 'pp-120-500-create-sock':
case 'pp-semi-create-sock':
typereason = '[[WP:SOCK|ソックパペット]]による問題投稿の繰り返し';
break;
case 'pp-semi-blp':
case 'pp-120-500-blp':
case 'pp-create-blp':
case 'pp-120-500-create-blp':
case 'pp-semi-create-blp':
typereason = 'プライバシー侵害の記述の繰り返し';
break;
case 'pp-move-dispute':
typereason = '移動合戦';
break;
case 'pp-create-spam':
case 'pp-120-500-create-spam':
case 'pp-semi-create-spam':
typereason = '度重なる宣伝';
break;
case 'pp-move-indef':
typereason = '移動不要ページ';
break;
case 'pp-create-salt':
case 'pp-120-500-create-salt':
case 'pp-semi-create-salt':
typereason = '削除されたページの改善なき再作成の繰り返し';
break;
default:
typereason = '';
break;
}
var reason = typereason;
if (input.reason !== '') {
if (typereason !== '') {
reason += '\u00A0\u2013 '; // U+00A0 NO-BREAK SPACE; U+2013 EN RULE
}
reason += input.reason;
}
var rppparams = {
reason: reason,
typename: typename,
category: input.category,
expiry: input.expiry
};
Morebits.SimpleWindow.setButtonsEnabled(false);
Morebits.Status.init(form);
var rppName = 'Wikipedia:保護依頼';
// Updating data for the action completed event
Morebits.wiki.actionCompleted.redirect = rppparams.typename !== '保護解除' ? 'Project:保護依頼' : 'Project:保護解除依頼';
Morebits.wiki.actionCompleted.notice = '依頼完了、依頼ページへリダイレクトします。';
var rppPage = new Morebits.wiki.Page(rppName, 'ページの保護を依頼');
rppPage.setFollowRedirect(true);
rppPage.setCallbackParameters(rppparams);
rppPage.load(Twinkle.protect.callbacks.fileRequest);
if (input.req_template && rppparams.typename !== '保護解除') {
Twinkle.protect.callbacks.taggingPageInitial(tagparams);
}
break;
default:
alert('twinkleprotect: 不明な操作');
break;
}
};
Twinkle.protect.protectReasonAnnotations = [];
Twinkle.protect.callback.annotateProtectReason = function twinkleprotectCallbackAnnotateProtectReason(e) {
const form = e.target.form;
const protectReason = form.protectReason.value.replace(new RegExp('(?:; )?' + mw.util.escapeRegExp(Twinkle.protect.protectReasonAnnotations.join(': '))), '');
if (this.name === 'protectReason_notes_rfpp') {
if (this.checked) {
Twinkle.protect.protectReasonAnnotations.push(this.value);
$(form.protectReason_notes_rfppRevid).parent().show();
} else {
Twinkle.protect.protectReasonAnnotations = [];
form.protectReason_notes_rfppRevid.value = '';
$(form.protectReason_notes_rfppRevid).parent().hide();
}
} else if (this.name === 'protectReason_notes_rfppRevid') {
Twinkle.protect.protectReasonAnnotations = Twinkle.protect.protectReasonAnnotations.filter((el) => !el.includes('[[Special:Permalink')).filter((el) => !el.includes('[[WP:RFPP'));
if (e.target.value.length) {
var permalink = '[[Special:Permalink/' + e.target.value + '#' + Morebits.pageNameNorm + '(ノート_/_履歴_/_ログ_/_リンク元)|WP:RFPP]]による';
Twinkle.protect.protectReasonAnnotations.push(permalink);
} else {
Twinkle.protect.protectReasonAnnotations.push('[[WP:RFPP]]による');
}
}
if (!Twinkle.protect.protectReasonAnnotations.length) {
form.protectReason.value = protectReason;
} else {
form.protectReason.value = (protectReason ? protectReason + '; ' : '') + Twinkle.protect.protectReasonAnnotations.join(': ');
}
};
Twinkle.protect.callbacks = {
taggingPageInitial: function(tagparams) {
if (tagparams.tag === 'noop') {
Morebits.Status.info('Applying protection template', 'nothing to do');
return;
}
var protectedPage = new Morebits.wiki.Page(mw.config.get('wgPageName'), 'ページにタグ付け');
protectedPage.setCallbackParameters(tagparams);
protectedPage.load(Twinkle.protect.callbacks.taggingPage);
},
taggingPage: function(protectedPage) {
const params = protectedPage.getCallbackParameters();
let text = protectedPage.getPageText();
let tag, summary;
var oldtag_re = /(?:\/\*)?\s*(?:<noinclude>)?\s*\{\{\s*((?:pp|保護依頼)[^{}]*?|protected|(?:t|v|s|p-|usertalk-v|usertalk-s|sb|move)protected(?:2)?|protected template|privacy protection)\s*?\}\}\s*(?:<\/noinclude>)?\s*(?:\*\/)?\s*/gi;
var re_result = oldtag_re.exec(text);
if (re_result) {
if (params.tag === 'none' || confirm('ページ上に保護関連テンプレート({{' + re_result[1] + '}}など)が見つかりました。\nOKをクリックして除去する、またはCancelをクリックしてそのままにしておきます。')) {
text = text.replace(oldtag_re, '');
}
}
if (params.tag === 'none') {
summary = '保護(依頼)テンプレートの除去';
} else {
tag = params.tag;
if (params.reason) {
tag += '|reason=' + params.reason;
}
if (params.small) {
tag += '|small=yes';
}
if (/^\s*#(redirect|転送)/i.test(text)) { // redirect page
// Only tag if no {{rcat shell}} is found
if (!text.match(/{{(?:redr|this is a redirect|r(?:edirect)?(?:.?cat.*)?[ _]?sh)/i)) {
text = text.replace(/#(?:redirect|転送) ?(\[\[.*?\]\])(.*)/i, '#REDIRECT $1$2\n\n{{' + tag + '}}');
} else {
Morebits.Status.info('Redirect category shell present', 'nothing to do');
return;
}
} else {
const needsTagToBeCommentedOut = ['javascript', 'css', 'sanitized-css'].includes(protectedPage.getContentModel());
if (needsTagToBeCommentedOut) {
if (params.noinclude) {
tag = '/* <noinclude>{{' + tag + '}}</noinclude> */';
} else {
tag = '/* {{' + tag + '}} */\n';
}
// Prepend tag at very top
text = tag + text;
} else {
if (params.noinclude) {
tag = '<noinclude>{{' + tag + '}}</noinclude>';
if (text.startsWith('==')) {
tag += '\n'; // a newline is needed to prevent section headings at the very beginning of the page from breaking
}
} else {
tag = '{{' + tag + '}}\n';
}
// Insert tag after short description or any hatnotes
const wikipage = new Morebits.wikitext.Page(text);
text = wikipage.insertAfterTemplates(tag, Twinkle.hatnoteRegex).getText();
}
}
summary = '{{' + params.tag + '}}を追加';
}
protectedPage.setEditSummary(summary + Twinkle.summaryAd);
// protectedPage.setChangeTags(Twinkle.changeTags);
protectedPage.setWatchlist(Twinkle.getPref('watchPPTaggedPages'));
protectedPage.setPageText(text);
protectedPage.setCreateOption('nocreate');
protectedPage.suppressProtectWarning(); // no need to let admins know they are editing through protection
protectedPage.save();
},
fileRequest: function(rppPage) {
var rppPage2 = new Morebits.wiki.Page('Wikipedia:保護解除依頼', '依頼ページを読み込み中');
rppPage2.load(() => {
var params = rppPage.getCallbackParameters();
var text = rppPage.getPageText();
var statusElement = rppPage.getStatusElement();
var text2 = rppPage2.getPageText();
// If either protection type results in a increased status, then post it under increase
// else we post it under decrease
var increase = false;
if (params.typename !== '保護解除') {
increase = true;
}
var rppRe = new RegExp('====\\s*(\\{\\{)?\\s*[pP]age\\s*\\|\\s*' + Morebits.string.escapeRegExp(Morebits.pageNameNorm) + '\\s*(\\}\\})?\\s*====', 'm');
var newtag = '\n==== {{Page|' + Morebits.pageNameNorm + '}} ====\n';
var tag = new RegExp('^' + mw.util.escapeRegExp(newtag).replace(/\s+/g, '\\s*'), 'm').test(text) || rppRe.exec(text);
var tag2 = new RegExp('^' + mw.util.escapeRegExp(newtag).replace(/\s+/g, '\\s*'), 'm').test(text2) || rppRe.exec(text2);
var rppLink = document.createElement('a');
rppLink.setAttribute('href', mw.util.getUrl('Wikipedia:保護依頼'));
rppLink.appendChild(document.createTextNode('Wikipedia:保護依頼'));
var rppLink2 = document.createElement('a');
rppLink2.setAttribute('href', mw.util.getUrl('Wikipedia:保護解除依頼'));
rppLink2.appendChild(document.createTextNode('Wikipedia:保護解除依頼'));
if (tag && increase) {
statusElement.error(['このページの保護依頼が', rppLink, 'で見つかりました、中止します。']);
return;
} else if (tag2 && !increase) {
statusElement.error(['このページの保護解除依頼が', rppLink2, 'で見つかりました、中止します。']);
return;
}
newtag += Morebits.string.formatReasonText(params.reason) + '--~~~~';
if (increase) {
rppPage.parse(() => {
var sections = rppPage.getSections();
// 依頼先の節取得
var time = new Morebits.Date();
var month = time.getMonth() + 1;
var date = time.getDate();
var section = null;
if (date < 11) {
section = month + '月上旬(1日から10日まで)';
} else if (date > 20) {
section = month + '月下旬(21日から末日まで)';
} else {
section = month + '月中旬(11日から20日まで)';
}
sections = sections.filter((sections) => sections.level === '3').filter((sections) => sections.fromtitle === 'Wikipedia:保護依頼').filter((sections) => sections.line === section);
if (sections.length !== 1) {
statusElement.error('報告先の節を正しく取得できませんでした。');
return;
}
statusElement.status('新規依頼を追加中...');
rppPage.setPageSection(Number(sections[0].index));
rppPage.setEditSummary('/* ' + Morebits.pageNameNorm + ' */ [[:' + Morebits.pageNameNorm + ']]の' + params.typename + 'を依頼' + Twinkle.summaryAd);
// rppPage.setChangeTags(Twinkle.changeTags);
rppPage.setAppendText(newtag);
rppPage.setCreateOption('recreate');
rppPage.save(() => {
// Watch the page being requested
var watchPref = Twinkle.getPref('watchRequestedPages');
// action=watch has no way to rely on user preferences (T262912), so we do it manually.
// The watchdefault pref appears to reliably return '1' (string),
// but that's not consistent among prefs so might as well be "correct"
var watch = watchPref !== 'no' && (watchPref !== 'default' || !!parseInt(mw.user.options.get('watchdefault'), 10));
if (watch) {
var watch_query = {
action: 'watch',
titles: mw.config.get('wgPageName'),
token: mw.user.tokens.get('watchToken')
};
// Only add the expiry if page is unwatched or already temporarily watched
if (Twinkle.protect.watched !== true && watchPref !== 'default' && watchPref !== 'yes') {
watch_query.expiry = watchPref;
}
new Morebits.wiki.Api('依頼したページをウォッチリストに追加中', watch_query).post();
}
});
});
} else {
rppPage2.parse(() => {
var sections = rppPage2.getSections();
// 依頼先の節取得
var time = new Morebits.Date();
var month = time.getMonth() + 1;
var date = time.getDate();
var section = null;
if (date < 11) {
section = month + '月上旬(1日から10日まで)';
} else if (date > 20) {
section = month + '月下旬(21日から末日まで)';
} else {
section = month + '月中旬(11日から20日まで)';
}
sections = sections.filter((sections) => sections.level === '3').filter((sections) => sections.fromtitle === 'Wikipedia:保護解除依頼').filter((sections) => sections.line === section);
if (sections.length !== 1) {
statusElement.error('報告先の節を正しく取得できませんでした。');
return;
}
text2 += '\n' + newtag;
statusElement.status('新規依頼を追加中...');
rppPage2.setPageSection(Number(sections[0].index));
rppPage2.setEditSummary('/* ' + Morebits.pageNameNorm + ' */ [[:' + Morebits.pageNameNorm + ']]の' + params.typename + 'を依頼' + Twinkle.summaryAd);
// rppPage2.setChangeTags(Twinkle.changeTags);
rppPage2.setAppendText(newtag);
rppPage2.setCreateOption('recreate');
rppPage2.save(() => {
// Watch the page being requested
var watchPref = Twinkle.getPref('watchRequestedPages');
// action=watch has no way to rely on user preferences (T262912), so we do it manually.
// The watchdefault pref appears to reliably return '1' (string),
// but that's not consistent among prefs so might as well be "correct"
var watch = watchPref !== 'no' && (watchPref !== 'default' || !!parseInt(mw.user.options.get('watchdefault'), 10));
if (watch) {
var watch_query = {
action: 'watch',
titles: mw.config.get('wgPageName'),
token: mw.user.tokens.get('watchToken')
};
// Only add the expiry if page is unwatched or already temporarily watched
if (Twinkle.protect.watched !== true && watchPref !== 'default' && watchPref !== 'yes') {
watch_query.expiry = watchPref;
}
new Morebits.wiki.Api('依頼したページをウォッチリストに追加中', watch_query).post();
}
});
});
}
});
}
};
Twinkle.addInitCallback(Twinkle.protect, 'protect');
}());
// </nowiki>
rlsa8atjxdzcofb00uxo9yk60d4n6gh
769533
769466
2026-10-22T00:00:39Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 1406828: rm cmt
769533
javascript
text/javascript
// <nowiki>
(function() {
/*
****************************************
*** twinkleprotect.js: Protect/RPP module
****************************************
* Mode of invocation: Tab ("PP"/"RPP")
* Active on: Non-special, non-MediaWiki pages
*/
// Note: a lot of code in this module is re-used/called by batchprotect.
Twinkle.protect = function twinkleprotect() {
if (mw.config.get('wgNamespaceNumber') < 0 || mw.config.get('wgNamespaceNumber') === 8) {
return;
}
Twinkle.addPortletLink(Twinkle.protect.callback, Morebits.userIsSysop ? 'PP' : 'RPP', 'tw-rpp',
Morebits.userIsSysop ? 'ページを保護' : 'ページの保護を依頼');
};
Twinkle.protect.callback = function twinkleprotectCallback() {
var Window = new Morebits.SimpleWindow(620, 530);
Window.setTitle(Morebits.userIsSysop ? 'ページ保護の適用、依頼、タグ付け' : 'ページ保護の依頼、タグ付け');
Window.setScriptName('Twinkle');
Window.addFooterLink('保護テンプレート', 'Template:Pp#関連項目');
Window.addFooterLink('保護の方針', 'Wikipedia:保護の方針');
Window.addFooterLink('Twinkle help', ':en:WP:TW/DOC#protect');
Window.addFooterLink('フィードバック', 'User talk:Syunsyunminmin/Twinkle');
const form = new Morebits.QuickForm(Twinkle.protect.callback.evaluate);
const actionfield = form.append({
type: 'field',
label: '操作の種類'
});
if (Morebits.userIsSysop) {
actionfield.append({
type: 'radio',
name: 'actiontype',
event: Twinkle.protect.callback.changeAction,
list: [
{
label: 'ページを保護',
value: 'protect',
tooltip: 'ページに保護を適用する。',
checked: true
}
]
});
}
actionfield.append({
type: 'radio',
name: 'actiontype',
event: Twinkle.protect.callback.changeAction,
list: [
{
label: 'ページの保護を依頼',
value: 'request',
tooltip: (Morebits.userIsSysop ? '自分で保護する代わりに' : '') + 'WP:RFPPで保護を依頼する場合',
checked: !Morebits.userIsSysop
},
{
label: '保護テンプレートをページにタグ付けする',
value: 'tag',
tooltip: '保護する管理者が保護テンプレートを貼り忘れたり、タグを付けずにページを保護した場合、これを使って適切な保護タグを貼り付けることができます。',
disabled: mw.config.get('wgArticleId') === 0 || mw.config.get('wgPageContentModel') === 'Scribunto' || mw.config.get('wgNamespaceNumber') === 710
}
]
});
form.append({ type: 'field', label: 'プリセット', name: 'field_preset' });
form.append({ type: 'field', label: '1', name: 'field1' });
form.append({ type: 'field', label: '2', name: 'field2' });
form.append({ type: 'submit' });
const result = form.render();
Window.setContent(result);
Window.display();
// We must init the controls
const evt = document.createEvent('Event');
evt.initEvent('change', true, true);
result.actiontype[0].dispatchEvent(evt);
// get current protection level asynchronously
Twinkle.protect.fetchProtectionLevel();
};
// A list of bots who may be the protecting sysop, for whom we shouldn't
// remind the user contact before requesting unprotection (evaluate)
Twinkle.protect.trustedBots = ['MusikBot II', 'TFA Protector Bot'];
// Customizable namespace and FlaggedRevs settings
// In theory it'd be nice to have restrictionlevels defined here,
// but those are only available via a siteinfo query
// mw.loader.getState('ext.flaggedRevs.review') returns null if the
// FlaggedRevs extension is not registered. Previously, this was done with
// wgFlaggedRevsParams, but after 1.34-wmf4 it is no longer exported if empty
// (https://gerrit.wikimedia.org/r/c/mediawiki/extensions/FlaggedRevs/+/508427)
const hasFlaggedRevs = mw.loader.getState('ext.flaggedRevs.review') &&
// FlaggedRevs only valid in some namespaces, hardcoded until [[phab:T218479]]
(mw.config.get('wgNamespaceNumber') === 0 || mw.config.get('wgNamespaceNumber') === 4);
// Limit template editor; a Twinkle restriction, not a site setting
// Contains the current protection level in an object
// Once filled, it will look something like:
// { edit: { level: "sysop", expiry: <some date>, cascade: true }, ... }
Twinkle.protect.currentProtectionLevels = {};
// returns a jQuery Deferred object, usage:
// Twinkle.protect.fetchProtectingAdmin(apiObject, pageName, protect/stable).done(function(admin_username) { ...code... });
Twinkle.protect.fetchProtectingAdmin = function twinkleprotectFetchProtectingAdmin(api, pageName, protType, logIds) {
logIds = logIds || [];
return api.get({
format: 'json',
action: 'query',
list: 'logevents',
letitle: pageName,
letype: protType
}).then((data) => {
// don't check log entries that have already been checked (e.g. don't go into an infinite loop!)
const event = data.query ? $.grep(data.query.logevents, (le) => $.inArray(le.logid, logIds))[0] : null;
if (!event) {
// fail gracefully
return null;
} else if (event.action === 'move_prot' || event.action === 'move_stable') {
return twinkleprotectFetchProtectingAdmin(api, protType === 'protect' ? event.params.oldtitle_title : event.params.oldtitle, protType, logIds.concat(event.logid));
}
return event.user;
});
};
Twinkle.protect.fetchProtectionLevel = function twinkleprotectFetchProtectionLevel() {
const api = new mw.Api();
const protectDeferred = api.get({
format: 'json',
indexpageids: true,
action: 'query',
list: 'logevents',
letype: 'protect',
letitle: mw.config.get('wgPageName'),
prop: hasFlaggedRevs ? 'info|flagged' : 'info',
inprop: 'protection|watched',
titles: mw.config.get('wgPageName')
});
const stableDeferred = api.get({
format: 'json',
action: 'query',
list: 'logevents',
letype: 'stable',
letitle: mw.config.get('wgPageName')
});
const earlyDecision = [protectDeferred];
if (hasFlaggedRevs) {
earlyDecision.push(stableDeferred);
}
$.when.apply($, earlyDecision).done((protectData, stableData) => {
// $.when.apply is supposed to take an unknown number of promises
// via an array, which it does, but the type of data returned varies.
// If there are two or more deferreds, it returns an array (of objects),
// but if there's just one deferred, it retuns a simple object.
// This is annoying.
protectData = $(protectData).toArray();
const pageid = protectData[0].query.pageids[0];
const page = protectData[0].query.pages[pageid];
const current = {};
let adminEditDeferred;
// Save requested page's watched status for later in case needed when filing request
Twinkle.protect.watched = page.watchlistexpiry || page.watched === '';
$.each(page.protection, (index, protection) => {
// Don't overwrite actual page protection with cascading protection
if (!protection.source) {
current[protection.type] = {
level: protection.level,
expiry: protection.expiry,
cascade: protection.cascade === ''
};
// logs report last admin who made changes to either edit/move/create protection, regardless if they only modified one of them
if (!adminEditDeferred) {
adminEditDeferred = Twinkle.protect.fetchProtectingAdmin(api, mw.config.get('wgPageName'), 'protect');
}
} else {
// Account for the page being covered by cascading protection
current.cascading = {
expiry: protection.expiry,
source: protection.source,
level: protection.level // should always be sysop, unused
};
}
});
if (page.flagged) {
current.stabilize = {
level: page.flagged.protection_level,
expiry: page.flagged.protection_expiry
};
adminEditDeferred = Twinkle.protect.fetchProtectingAdmin(api, mw.config.get('wgPageName'), 'stable');
}
// show the protection level and log info
Twinkle.protect.hasProtectLog = !!protectData[0].query.logevents.length;
Twinkle.protect.protectLog = Twinkle.protect.hasProtectLog && protectData[0].query.logevents;
Twinkle.protect.hasStableLog = hasFlaggedRevs ? !!stableData[0].query.logevents.length : false;
Twinkle.protect.stableLog = Twinkle.protect.hasStableLog && stableData[0].query.logevents;
Twinkle.protect.currentProtectionLevels = current;
if (adminEditDeferred) {
adminEditDeferred.done((admin) => {
if (admin) {
$.each(['edit', 'move', 'create', 'stabilize', 'cascading'], (i, type) => {
if (Twinkle.protect.currentProtectionLevels[type]) {
Twinkle.protect.currentProtectionLevels[type].admin = admin;
}
});
}
Twinkle.protect.callback.showLogAndCurrentProtectInfo();
});
} else {
Twinkle.protect.callback.showLogAndCurrentProtectInfo();
}
});
};
Twinkle.protect.callback.showLogAndCurrentProtectInfo = function twinkleprotectCallbackShowLogAndCurrentProtectInfo() {
const currentlyProtected = !$.isEmptyObject(Twinkle.protect.currentProtectionLevels);
if (Twinkle.protect.hasProtectLog || Twinkle.protect.hasStableLog) {
const $linkMarkup = $('<span>');
if (Twinkle.protect.hasProtectLog) {
$linkMarkup.append(
$('<a target="_blank" href="' + mw.util.getUrl('Special:Log', {action: 'view', page: mw.config.get('wgPageName'), type: 'protect'}) + '">保護記録</a>'));
if (!currentlyProtected || (!Twinkle.protect.currentProtectionLevels.edit && !Twinkle.protect.currentProtectionLevels.move)) {
const lastProtectAction = Twinkle.protect.protectLog[0];
if (lastProtectAction.action === 'unprotect') {
$linkMarkup.append(' (' + new Morebits.Date(lastProtectAction.timestamp).calendar('utc') + 'に保護解除)');
} else { // protect or modify
$linkMarkup.append(' (' + new Morebits.Date(lastProtectAction.params.details[0].expiry).calendar('utc') + 'に期限切れ)');
}
}
$linkMarkup.append(Twinkle.protect.hasStableLog ? $('<span> • </span>') : null);
}
if (Twinkle.protect.hasStableLog) {
$linkMarkup.append($('<a target="_blank" href="' + mw.util.getUrl('Special:Log', {action: 'view', page: mw.config.get('wgPageName'), type: 'stable'}) + '">pending changes log</a>)'));
if (!currentlyProtected || !Twinkle.protect.currentProtectionLevels.stabilize) {
const lastStabilizeAction = Twinkle.protect.stableLog[0];
if (lastStabilizeAction.action === 'reset') {
$linkMarkup.append(' (reset ' + new Morebits.Date(lastStabilizeAction.timestamp).calendar('utc') + ')');
} else { // config or modify
$linkMarkup.append(' (expired ' + new Morebits.Date(lastStabilizeAction.params.expiry).calendar('utc') + ')');
}
}
}
Morebits.Status.init($('div[name="hasprotectlog"] span')[0]);
Morebits.Status.warn(
currentlyProtected ? '以前の保護' : 'このページは過去に保護されたことがあります',
$linkMarkup[0]
);
}
Morebits.Status.init($('div[name="currentprot"] span')[0]);
let protectionNode = [], statusLevel = 'info';
if (currentlyProtected) {
$.each(Twinkle.protect.currentProtectionLevels, (type, settings) => {
var label = type === 'stabilize' ? 'Pending Changes' : '';
switch (Morebits.string.toUpperCaseFirstChar(type)) {
case 'Edit':
label = '編集';
break;
case 'Move':
label = '移動';
break;
case 'create':
label = '作成';
break;
case 'upload':
label = 'アップロード';
break;
default:
label = Morebits.string.toUpperCaseFirstChar(type);
break;
}
if (type === 'cascading') { // Covered by another page
label = 'カスケード保護 ';
protectionNode.push($('<b>' + label + '</b>')[0]);
if (settings.source) { // Should by definition exist
const sourceLink = '<a target="_blank" href="' + mw.util.getUrl(settings.source) + '">' + settings.source + '</a>';
protectionNode.push($('<span>from ' + sourceLink + '</span>')[0]);
}
} else {
var level;
switch (settings.level) {
case 'sysop':
level = '管理者のみ';
break;
case 'extendedconfirmed':
level = '拡張承認された利用者のみ';
break;
case 'autoconfirmed':
level = '自動承認された利用者のみ';
break;
default:
level = settings.level;
break;
}
// Make cascading protection more prominent
if (settings.cascade) {
level += ' (カスケード)';
}
protectionNode.push($('<b>' + label + ': ' + level + '</b>')[0]);
}
if (settings.expiry === 'infinity') {
protectionNode.push(' (無期限) ');
} else {
protectionNode.push(' (' + new Morebits.Date(settings.expiry).calendar('utc') + 'に期限切れ) ');
}
if (settings.admin) {
var adminLink = '<a target="_blank" href="' + mw.util.getUrl('User talk:' + settings.admin) + '">' + settings.admin + '</a>';
protectionNode.push($('<span>' + adminLink + 'による</span>')[0]);
}
protectionNode.push($('<span> \u2022 </span>')[0]);
});
protectionNode = protectionNode.slice(0, -1); // remove the trailing bullet
statusLevel = 'warn';
} else {
protectionNode.push($('<b>保護なし</b>')[0]);
}
Morebits.Status[statusLevel]('現在の保護レベル', protectionNode);
};
Twinkle.protect.callback.changeAction = function twinkleprotectCallbackChangeAction(e) {
let field_preset;
let field1;
let field2;
switch (e.target.values) {
case 'protect':
field_preset = new Morebits.QuickForm.Element({ type: 'field', label: 'プリセット', name: 'field_preset' });
field_preset.append({
type: 'select',
name: 'category',
label: 'プリセットを選択:',
event: Twinkle.protect.callback.changePreset,
list: mw.config.get('wgArticleId') ? Twinkle.protect.protectionTypes : Twinkle.protect.protectionTypesCreate
});
field2 = new Morebits.QuickForm.Element({ type: 'field', label: '保護設定', name: 'field2' });
field2.append({ type: 'div', name: 'currentprot', label: ' ' }); // holds the current protection level, as filled out by the async callback
field2.append({ type: 'div', name: 'hasprotectlog', label: ' ' });
// for existing pages
if (mw.config.get('wgArticleId')) {
field2.append({
type: 'checkbox',
event: Twinkle.protect.formevents.editmodify,
list: [
{
label: '編集保護設定を変更',
name: 'editmodify',
tooltip: 'これをオフにすると、編集保護レベルと有効期限はそのままになります。',
checked: true
}
]
});
field2.append({
type: 'select',
name: 'editlevel',
label: '誰が編集できるか:',
event: Twinkle.protect.formevents.editlevel,
list: Twinkle.protect.protectionLevels
});
field2.append({
type: 'select',
name: 'editexpiry',
label: '期限:',
event: function(e) {
if (e.target.value === 'custom') {
Twinkle.protect.doCustomExpiry(e.target);
}
},
// default expiry selection (1 week) is conditionally set in Twinkle.protect.callback.changePreset
list: Twinkle.protect.protectionLengths
});
field2.append({
type: 'checkbox',
event: Twinkle.protect.formevents.movemodify,
list: [
{
label: '移動保護設定を変更',
name: 'movemodify',
tooltip: 'これをオフにすると、移動保護レベルと有効期限はそのままになります。',
checked: true
}
]
});
field2.append({
type: 'select',
name: 'movelevel',
label: '誰が移動できるか:',
event: Twinkle.protect.formevents.movelevel,
// Autoconfirmed is required for a move, redundant.
list: Twinkle.protect.protectionLevels.filter((level) => level.value !== 'autoconfirmed')
});
field2.append({
type: 'select',
name: 'moveexpiry',
label: '期限:',
event: function(e) {
if (e.target.value === 'custom') {
Twinkle.protect.doCustomExpiry(e.target);
}
},
// default expiry selection (2 days) is conditionally set in Twinkle.protect.callback.changePreset
list: Twinkle.protect.protectionLengths
});
if (hasFlaggedRevs) {
field2.append({
type: 'checkbox',
event: Twinkle.protect.formevents.pcmodify,
list: [
{
label: 'Modify pending changes protection',
name: 'pcmodify',
tooltip: 'If this is turned off, the pending changes level, and expiry time, will be left as is.',
checked: true
}
]
});
field2.append({
type: 'select',
name: 'pclevel',
label: 'Pending changes:',
event: Twinkle.protect.formevents.pclevel,
list: [
{ label: 'None', value: 'none' },
{ label: 'Pending change', value: 'autoconfirmed', selected: true }
]
});
field2.append({
type: 'select',
name: 'pcexpiry',
label: 'Expires:',
event: function(e) {
if (e.target.value === 'custom') {
Twinkle.protect.doCustomExpiry(e.target);
}
},
// default expiry selection (1 month) is conditionally set in Twinkle.protect.callback.changePreset
list: Twinkle.protect.protectionLengths
});
}
} else { // for non-existing pages
field2.append({
type: 'select',
name: 'createlevel',
label: '作成保護:',
event: Twinkle.protect.formevents.createlevel,
list: Twinkle.protect.protectionLevels
});
field2.append({
type: 'select',
name: 'createexpiry',
label: '期限:',
event: function(e) {
if (e.target.value === 'custom') {
Twinkle.protect.doCustomExpiry(e.target);
}
},
// default expiry selection (indefinite) is conditionally set in Twinkle.protect.callback.changePreset
list: Twinkle.protect.protectionLengths
});
}
field2.append({
type: 'textarea',
name: 'protectReason',
label: '理由 (保護記録用):'
});
field2.append({
type: 'div',
name: 'protectReason_notes',
label: '注記:',
style: 'display:inline-block; margin-top:4px;',
tooltip: '保護記録に、RFPPで依頼されたというメモを追加する。'
});
field2.append({
type: 'checkbox',
event: Twinkle.protect.callback.annotateProtectReason,
style: 'display:inline-block; margin-top:4px;',
list: [
{
label: 'RFPP依頼',
name: 'protectReason_notes_rfpp',
checked: false,
value: '[[WP:RFPP]]による'
}
]
});
field2.append({
type: 'input',
event: Twinkle.protect.callback.annotateProtectReason,
label: 'RFPP版番号',
name: 'protectReason_notes_rfppRevid',
value: '',
tooltip: '保護が依頼されたRFPPページの版番号。'
});
if (!mw.config.get('wgArticleId') || mw.config.get('wgPageContentModel') === 'Scribunto' || mw.config.get('wgNamespaceNumber') === 710) { // tagging isn't relevant for non-existing, module, or TimedText pages
break;
}
/* falls through */
case 'tag':
field1 = new Morebits.QuickForm.Element({ type: 'field', label: 'タグ付け設定', name: 'field1' });
field1.append({ type: 'div', name: 'currentprot', label: ' ' }); // holds the current protection level, as filled out by the async callback
field1.append({ type: 'div', name: 'hasprotectlog', label: ' ' });
field1.append({
type: 'select',
name: 'tagtype',
label: '保護テンプレートを選択:',
list: Twinkle.protect.protectionTags,
event: Twinkle.protect.formevents.tagtype
});
var isTemplateNamespace = mw.config.get('wgNamespaceNumber') === 10;
var isAFD = Morebits.pageNameNorm.startsWith('Wikipedia:削除依頼/');
var isCode = ['javascript', 'css', 'sanitized-css'].includes(mw.config.get('wgPageContentModel'));
field1.append({
type: 'checkbox',
list: [
{
name: 'small',
label: 'アイコン化 (small=yes)',
tooltip: 'テンプレートの|small=yes機能を使用し、インジケーターとしてのみ表示する。',
checked: true
},
{
name: 'noinclude',
label: '保護テンプレートを<noinclude>で囲む',
tooltip: '参照読み込みされないように保護テンプレートを<noinclude>で囲む',
checked: (isTemplateNamespace || isAFD) && !isCode
}
]
});
break;
case 'request':
field_preset = new Morebits.QuickForm.Element({ type: 'field', label: '保護の種類', name: 'field_preset' });
field_preset.append({
type: 'select',
name: 'category',
label: '種類と理由:',
event: Twinkle.protect.callback.changePreset,
list: mw.config.get('wgArticleId') ? Twinkle.protect.protectionTypes : Twinkle.protect.protectionTypesCreate
});
field_preset.append({
type: 'checkbox',
list: [
{
name: 'req_template',
label: '保護依頼テンプレートを貼り付ける',
tooltip: '保護を依頼するページに{{保護依頼}}テンプレートを貼り付ける',
checked: true
},
{
name: 'noinclude',
label: '保護テンプレートを<noinclude>で囲む',
tooltip: '参照読み込みされないように保護依頼テンプレートを<noinclude>で囲む',
checked: mw.config.get('wgNamespaceNumber') === 10 || (mw.config.get('wgNamespaceNumber') === mw.config.get('wgNamespaceIds').project && mw.config.get('wgTitle').indexOf('削除依頼/') === 0)
}
]
});
field1 = new Morebits.QuickForm.Element({ type: 'field', label: 'オプション', name: 'field1' });
field1.append({ type: 'div', name: 'currentprot', label: ' ' }); // holds the current protection level, as filled out by the async callback
field1.append({ type: 'div', name: 'hasprotectlog', label: ' ' });
field1.append({
type: 'textarea',
name: 'reason',
label: '理由:'
});
break;
default:
alert('twinkleprotectにて何かが進行中です');
break;
}
let oldfield;
if (field_preset) {
oldfield = $(e.target.form).find('fieldset[name="field_preset"]')[0];
oldfield.parentNode.replaceChild(field_preset.render(), oldfield);
} else {
$(e.target.form).find('fieldset[name="field_preset"]').css('display', 'none');
}
if (field1) {
oldfield = $(e.target.form).find('fieldset[name="field1"]')[0];
oldfield.parentNode.replaceChild(field1.render(), oldfield);
} else {
$(e.target.form).find('fieldset[name="field1"]').css('display', 'none');
}
if (field2) {
oldfield = $(e.target.form).find('fieldset[name="field2"]')[0];
oldfield.parentNode.replaceChild(field2.render(), oldfield);
} else {
$(e.target.form).find('fieldset[name="field2"]').css('display', 'none');
}
if (e.target.values === 'protect') {
// fake a change event on the preset dropdown
const evt = document.createEvent('Event');
evt.initEvent('change', true, true);
e.target.form.category.dispatchEvent(evt);
// reduce vertical height of dialog
$(e.target.form).find('fieldset[name="field2"] select').parent().css({ display: 'inline-block', marginRight: '0.5em' });
$(e.target.form).find('fieldset[name="field2"] input[name="protectReason_notes_rfppRevid"]').parent().css({display: 'inline-block', marginLeft: '15px'}).hide();
}
// re-add protection level and log info, if it's available
Twinkle.protect.callback.showLogAndCurrentProtectInfo();
};
// NOTE: This function is used by batchprotect as well
Twinkle.protect.formevents = {
editmodify: function twinkleprotectFormEditmodifyEvent(e) {
e.target.form.editlevel.disabled = !e.target.checked;
e.target.form.editexpiry.disabled = !e.target.checked || (e.target.form.editlevel.value === 'all');
e.target.form.editlevel.style.color = e.target.form.editexpiry.style.color = e.target.checked ? '' : 'transparent';
},
editlevel: function twinkleprotectFormEditlevelEvent(e) {
e.target.form.editexpiry.disabled = e.target.value === 'all';
},
movemodify: function twinkleprotectFormMovemodifyEvent(e) {
// sync move settings with edit settings if applicable
if (e.target.form.movelevel.disabled && !e.target.form.editlevel.disabled) {
e.target.form.movelevel.value = e.target.form.editlevel.value;
e.target.form.moveexpiry.value = e.target.form.editexpiry.value;
} else if (e.target.form.editlevel.disabled) {
e.target.form.movelevel.value = 'sysop';
e.target.form.moveexpiry.value = 'infinity';
}
e.target.form.movelevel.disabled = !e.target.checked;
e.target.form.moveexpiry.disabled = !e.target.checked || (e.target.form.movelevel.value === 'all');
e.target.form.movelevel.style.color = e.target.form.moveexpiry.style.color = e.target.checked ? '' : 'transparent';
},
movelevel: function twinkleprotectFormMovelevelEvent(e) {
e.target.form.moveexpiry.disabled = e.target.value === 'all';
},
pcmodify: function twinkleprotectFormPcmodifyEvent(e) {
e.target.form.pclevel.disabled = !e.target.checked;
e.target.form.pcexpiry.disabled = !e.target.checked || (e.target.form.pclevel.value === 'none');
e.target.form.pclevel.style.color = e.target.form.pcexpiry.style.color = e.target.checked ? '' : 'transparent';
},
pclevel: function twinkleprotectFormPclevelEvent(e) {
e.target.form.pcexpiry.disabled = e.target.value === 'none';
},
createlevel: function twinkleprotectFormCreatelevelEvent(e) {
e.target.form.createexpiry.disabled = e.target.value === 'all';
},
tagtype: function twinkleprotectFormTagtypeEvent(e) {
e.target.form.small.disabled = e.target.form.noinclude.disabled = (e.target.value === 'none') || (e.target.value === 'noop');
}
};
Twinkle.protect.doCustomExpiry = function twinkleprotectDoCustomExpiry(target) {
const custom = prompt('Enter a custom expiry time. \nYou can use relative times, like "1 minute" or "19 days", or absolute timestamps, "yyyymmddhhmm" (e.g. "200602011405" is Feb 1, 2006, at 14:05 UTC).', '');
if (custom) {
const option = document.createElement('option');
option.setAttribute('value', custom);
option.textContent = custom;
target.appendChild(option);
target.value = custom;
} else {
target.selectedIndex = 0;
}
};
// NOTE: This list is used by batchprotect as well
Twinkle.protect.protectionLevels = [
{ label: '全ての利用者', value: 'all' },
{ label: '自動承認された利用者', value: 'autoconfirmed' },
{ label: '拡張承認された利用者', value: 'extendedconfirmed' },
{ label: '管理者', value: 'sysop', selected: true }
];
// default expiry selection is conditionally set in Twinkle.protect.callback.changePreset
// NOTE: This list is used by batchprotect as well
Twinkle.protect.protectionLengths = [
{ label: '1時間', value: '1 hour' },
{ label: '2時間', value: '2 hours' },
{ label: '3時間', value: '3 hours' },
{ label: '6時間', value: '6 hours' },
{ label: '12時間', value: '12 hours' },
{ label: '1日間', value: '1 day' },
{ label: '2日間', value: '2 days' },
{ label: '3日間', value: '3 days' },
{ label: '4日間', value: '4 days' },
{ label: '1週間', value: '1 week' },
{ label: '2週間', value: '2 weeks' },
{ label: '1ヶ月間', value: '1 month' },
{ label: '2ヶ月間', value: '2 months' },
{ label: '3ヶ月間', value: '3 months' },
{ label: '1年', value: '1 year' },
{ label: '無期限', value: 'infinity' },
{ label: 'その他', value: 'custom' }
];
Twinkle.protect.protectionTypes = [
{ label: '保護解除', value: 'unprotect' },
{
label: '全保護',
list: [
{ label: '全般 (全)', value: 'pp' },
{ label: '編集合戦 (全)', value: 'pp-dispute' },
{ label: '拡張承認された利用者による問題投稿の繰り返し (全)', value: 'pp-vandalism' },
{ label: '影響が特に大きいテンプレート (全)', value: 'pp-template' }
]
},
{
label: '拡張半保護',
list: [
{ label: '全般 (拡半)', value: 'pp-120-500-protected' },
{ label: '(自動)承認された利用者による問題投稿の繰り返し (拡半)', value: 'pp-120-500-vandalism' },
{ label: 'プライバシー侵害 (拡半)', value: 'pp-120-500-blp' },
{ label: 'ソックパペット (拡半)', value: 'pp-120-500-sock' },
{ label: '影響が特に大きいテンプレート (拡半)', value: 'pp-120-500-template' }
]
},
{
label: '半保護',
list: [
{ label: '全般 (半)', value: 'pp-semi-protected' },
{ label: 'IP・新規利用者による問題投稿の繰り返し (半)', selected: true, value: 'pp-semi-vandalism' },
{ label: 'プライバシー侵害 (半)', value: 'pp-semi-blp' },
{ label: 'ソックパペット (半)', value: 'pp-semi-sock' },
{ label: '影響が特に大きいテンプレート (半)', value: 'pp-semi-template' }
]
},
{
label: '移動保護',
list: [
{ label: '全般 (移)', value: 'pp-move' },
{ label: '移動合戦 (移)', value: 'pp-move-dispute' },
{ label: '度重なる荒らし (移)', value: 'pp-move-vandalism' },
{ label: '移動不要ページ (移)', value: 'pp-move-indef' }
]
},
{
label: '移動拡張半保護',
list: [
{ label: '全般 (移拡)', value: 'pp-120-500-move' },
{ label: '度重なる荒らし (移拡)', value: 'pp-120-500-move-vandalism' }
]
}
];
Twinkle.protect.protectionTypesCreate = [
{ label: '保護解除', value: 'unprotect' },
{
label: '作成保護',
list: [
{ label: '全般 (全)', value: 'pp-create' },
{ label: 'ソックパペットによる問題投稿の繰り返し (全)', value: 'pp-create-sock' },
{ label: '拡張承認された利用者による問題投稿の繰り返し (全)', value: 'pp-create-vandalism' },
{ label: '度重なる宣伝 (全)', value: 'pp-create-spam' },
{ label: '削除されたページの改善なき再作成の繰り返し (全)', value: 'pp-create-salt' },
{ label: 'プライバシー侵害の記述の繰り返し (全)', value: 'pp-create-blp' }
]
},
{
label: '作成拡張半保護',
list: [
{ label: '全般 (拡)', value: 'pp-120-500-create' },
{ label: 'ソックパペットによる問題投稿の繰り返し (拡)', value: 'pp-120-500-create-sock' },
{ label: '(自動)承認された利用者による問題投稿の繰り返し (拡)', value: 'pp-120-500-create-vandalism' },
{ label: '度重なる宣伝 (拡)', value: 'pp-120-500-create-spam' },
{ label: '削除されたページの改善なき再作成の繰り返し (拡)', value: 'pp-120-500-create-salt' },
{ label: 'プライバシー侵害の記述の繰り返し (拡)', value: 'pp-120-500-create-blp' }
]
},
{
label: '作成半保護',
list: [
{ label: '全般 (半)', value: 'pp-semi-create' },
{ label: 'ソックパペットによる問題投稿の繰り返し (半)', value: 'pp-semi-create-sock' },
{ label: 'IP・新規利用者による問題投稿の繰り返し (半)', value: 'pp-semi-create-vandalism' },
{ label: '度重なる宣伝 (半)', value: 'pp-semi-create-spam' },
{ label: '削除されたページの改善なき再作成の繰り返し (半)', selected: true, value: 'pp-semi-create-salt' },
{ label: 'プライバシー侵害の記述の繰り返し (半)', value: 'pp-semi-create-blp' }
]
}
];
// NOTICE: keep this synched with [[MediaWiki:Protect-dropdown]]
// Also note: stabilize = Pending Changes level
// expiry will override any defaults
Twinkle.protect.protectionPresetsInfo = {
pp: {
edit: 'sysop',
move: 'sysop',
reason: null
},
'pp-dispute': {
edit: 'sysop',
move: 'sysop',
reason: '編集合戦'
},
'pp-vandalism': {
edit: 'sysop',
move: 'sysop',
reason: '拡張承認された利用者による問題投稿の繰り返し'
},
'pp-template': {
edit: 'sysop',
move: 'sysop',
expiry: 'infinity',
reason: '[[WP:HRT|影響が特に大きいテンプレート]]'
},
'pp-120-500-protected': {
edit: 'extendedconfirmed',
move: 'extendedconfirmed',
reason: null,
template: 'pp'
},
'pp-120-500-vandalism': {
edit: 'extendedconfirmed',
move: 'extendedconfirmed',
reason: '(自動)承認された利用者による問題投稿の繰り返し',
template: 'pp-vandalism'
},
'pp-120-500-blp': {
edit: 'extendedconfirmed',
move: 'extendedconfirmed',
reason: 'プライバシー侵害の記述の繰り返し',
template: 'pp-vandalism'
},
'pp-120-500-sock': {
edit: 'extendedconfirmed',
move: 'extendedconfirmed',
reason: '[[WP:SOCK|ソックパペット]]による問題投稿の繰り返し',
template: 'pp-vandalism'
},
'pp-120-500-template': {
edit: 'extendedconfirmed',
move: 'extendedconfirmed',
expiry: 'infinity',
reason: '[[WP:HRT|影響が特に大きいテンプレート]]',
template: 'pp-template'
},
'pp-semi-vandalism': {
edit: 'autoconfirmed',
reason: 'IP・新規利用者による問題投稿の繰り返し',
template: 'pp-vandalism'
},
'pp-semi-blp': {
edit: 'autoconfirmed',
reason: 'プライバシー侵害の記述の繰り返し',
template: 'pp-vandalism'
},
'pp-semi-template': { // removed for now
edit: 'autoconfirmed',
expiry: 'infinity',
reason: '[[WP:HRT|影響が特に大きいテンプレート]]',
template: 'pp-template'
},
'pp-semi-sock': {
edit: 'autoconfirmed',
reason: '[[WP:SOCK|ソックパペット]]による問題投稿の繰り返し',
template: 'pp-vandalism'
},
'pp-semi-protected': {
edit: 'autoconfirmed',
reason: null,
template: 'pp'
},
'pp-move': {
move: 'sysop',
reason: null
},
'pp-move-dispute': {
move: 'sysop',
expiry: '1 week',
reason: '移動合戦'
},
'pp-move-vandalism': {
move: 'sysop',
reason: '度重なる荒らし'
},
'pp-move-indef': {
move: 'sysop',
expiry: 'infinity',
reason: '移動不要ページ',
template: 'pp-move'
},
'pp-120-500-move': {
move: 'extendedconfirmed',
reason: null,
template: 'pp-move'
},
'pp-120-500-move-vandalism': {
move: 'extendedconfirmed',
reason: null,
template: 'pp-move-vandalism'
},
unprotect: {
edit: 'all',
move: 'all',
stabilize: 'none',
create: 'all',
reason: null,
template: 'none'
},
'pp-create-sock': {
create: 'sysop',
reason: '[[WP:SOCK|ソックパペット]]による問題投稿の繰り返し'
},
'pp-create': {
create: 'sysop',
reason: null
},
'pp-create-vandalism': {
create: 'sysop',
reason: '拡張承認された利用者による問題投稿の繰り返し'
},
'pp-create-spam': {
create: 'sysop',
reason: '度重なる宣伝'
},
'pp-create-salt': {
create: 'sysop',
reason: '削除されたページの改善なき再作成の繰り返し'
},
'pp-create-blp': {
create: 'sysop',
reason: 'プライバシー侵害の記述の繰り返し'
},
'pp-120-500-create': {
create: 'extendedconfirmed',
reason: null
},
'pp-120-500-create-sock': {
create: 'extendedconfirmed',
reason: '[[WP:SOCK|ソックパペット]]による問題投稿の繰り返し'
},
'pp-120-500-create-vandalism': {
create: 'extendedconfirmed',
reason: '(自動)承認された利用者による問題投稿の繰り返し'
},
'pp-120-500-create-spam': {
create: 'extendedconfirmed',
reason: '度重なる宣伝'
},
'pp-120-500-create-salt': {
create: 'extendedconfirmed',
reason: '削除されたページの改善なき再作成の繰り返し'
},
'pp-120-500-create-blp': {
create: 'sysop',
reason: 'プライバシー侵害の記述の繰り返し'
},
'pp-semi-create': {
create: 'autoconfirmed',
reason: null
},
'pp-semi-create-sock': {
create: 'autoconfirmed',
reason: '[[WP:SOCK|ソックパペット]]による問題投稿の繰り返し'
},
'pp-semi-create-vandalism': {
create: 'autoconfirmed',
reason: 'IP・新規利用者による問題投稿の繰り返し'
},
'pp-semi-create-spam': {
create: 'autoconfirmed',
reason: '度重なる宣伝'
},
'pp-semi-create-salt': {
create: 'autoconfirmed',
reason: '削除されたページの改善なき再作成の繰り返し'
},
'pp-semi-create-blp': {
create: 'autoconfirmed',
reason: 'プライバシー侵害の記述の繰り返し'
}
};
Twinkle.protect.protectionTags = [
{
label: 'なし (既にある保護テンプレートを除去する)',
value: 'none'
},
{
label: 'なし (既にある保護テンプレートを除去しない)',
value: 'noop'
},
{
label: '編集保護テンプレート',
list: [
{ label: '{{pp}}: 全般', value: 'pp' },
{ label: '{{pp-vandalism}}: 荒らし', value: 'pp-vandalism' },
{ label: '{{pp-dispute}}: 編集合戦', value: 'pp-dispute' },
{ label: '{{pp-template}}: 影響が特に大きいテンプレート', value: 'pp-template' },
{ label: '{{pp-semi-indef}}: 長期間の半保護', value: 'pp-semi-indef' }
]
},
{
label: '移動保護テンプレート',
list: [
{ label: '{{pp-move-dispute}}: 移動合戦', value: 'pp-move-dispute' },
{ label: '{{pp-move-vandalism}}: 度重なる荒らし', value: 'pp-move-vandalism' },
{ label: '{{pp-move}}: その他', value: 'pp-move' }
]
}
].filter((type) => hasFlaggedRevs || type.label !== 'Pending changes templates');
Twinkle.protect.callback.changePreset = function twinkleprotectCallbackChangePreset(e) {
const form = e.target.form;
const actiontypes = form.actiontype;
let actiontype;
for (let i = 0; i < actiontypes.length; i++) {
if (!actiontypes[i].checked) {
continue;
}
actiontype = actiontypes[i].values;
break;
}
if (actiontype === 'protect') { // actually protecting the page
const item = Twinkle.protect.protectionPresetsInfo[form.category.value];
if (mw.config.get('wgArticleId')) {
if (item.edit) {
form.editmodify.checked = true;
Twinkle.protect.formevents.editmodify({ target: form.editmodify });
form.editlevel.value = item.edit;
Twinkle.protect.formevents.editlevel({ target: form.editlevel });
} else {
form.editmodify.checked = false;
Twinkle.protect.formevents.editmodify({ target: form.editmodify });
}
if (item.move) {
form.movemodify.checked = true;
Twinkle.protect.formevents.movemodify({ target: form.movemodify });
form.movelevel.value = item.move;
Twinkle.protect.formevents.movelevel({ target: form.movelevel });
} else {
form.movemodify.checked = false;
Twinkle.protect.formevents.movemodify({ target: form.movemodify });
}
form.editexpiry.value = form.moveexpiry.value = item.expiry || '1 week';
if (form.pcmodify) {
if (item.stabilize) {
form.pcmodify.checked = true;
Twinkle.protect.formevents.pcmodify({ target: form.pcmodify });
form.pclevel.value = item.stabilize;
Twinkle.protect.formevents.pclevel({ target: form.pclevel });
} else {
form.pcmodify.checked = false;
Twinkle.protect.formevents.pcmodify({ target: form.pcmodify });
}
form.pcexpiry.value = item.expiry || '1 month';
}
} else {
if (item.create) {
form.createlevel.value = item.create;
Twinkle.protect.formevents.createlevel({ target: form.createlevel });
}
form.createexpiry.value = item.expiry || 'infinity';
}
const reasonField = actiontype === 'protect' ? form.protectReason : form.reason;
if (item.reason) {
reasonField.value = item.reason;
} else {
reasonField.value = '';
}
// Add any annotations
Twinkle.protect.callback.annotateProtectReason(e);
// sort out tagging options, disabled if nonexistent, lua, or TimedText
if (mw.config.get('wgArticleId') && mw.config.get('wgPageContentModel') !== 'Scribunto' && mw.config.get('wgNamespaceNumber') !== 710) {
if (form.category.value === 'unprotect') {
form.tagtype.value = 'none';
} else {
form.tagtype.value = item.template ? item.template : form.category.value;
}
Twinkle.protect.formevents.tagtype({ target: form.tagtype });
// Default settings for adding <noinclude> tags to protection templates
var isTemplateEditorProtection = form.category.value === 'pp-template';
var isAFD = Morebits.pageNameNorm.startsWith('Wikipedia:削除依頼/');
var isNotTemplateNamespace = mw.config.get('wgNamespaceNumber') !== 10;
var isCode = ['javascript', 'css', 'sanitized-css'].includes(mw.config.get('wgPageContentModel'));
if ((isTemplateEditorProtection || isAFD) && !isCode) {
form.noinclude.checked = true;
} else if (isCode || isNotTemplateNamespace) {
form.noinclude.checked = false;
}
}
} else { // RPP request
if (form.category.value === 'unprotect') {
form.expiry.value = '';
form.expiry.disabled = true;
} else {
form.expiry.value = '';
form.expiry.disabled = false;
}
}
};
Twinkle.protect.callback.evaluate = function twinkleprotectCallbackEvaluate(e) {
const form = e.target;
const input = Morebits.QuickForm.getInputData(form);
let tagparams;
if (input.actiontype === 'tag' || (input.actiontype === 'protect' && mw.config.get('wgArticleId') && mw.config.get('wgPageContentModel') !== 'Scribunto' && mw.config.get('wgNamespaceNumber') !== 710 /* TimedText */)) {
tagparams = {
tag: input.tagtype,
reason: false,
small: input.small,
noinclude: input.noinclude
};
} else if (input.actiontype === 'request') {
tagparams = {
tag: '保護依頼',
reason: false,
noinclude: input.noinclude
};
}
switch (input.actiontype) {
case 'protect':
// protect the page
Morebits.wiki.actionCompleted.redirect = mw.config.get('wgPageName');
Morebits.wiki.actionCompleted.notice = '保護完了';
var statusInited = false;
var thispage;
var allDone = function twinkleprotectCallbackAllDone() {
if (thispage) {
thispage.getStatusElement().info('完了');
}
if (tagparams) {
Twinkle.protect.callbacks.taggingPageInitial(tagparams);
}
};
var protectIt = function twinkleprotectCallbackProtectIt(next) {
thispage = new Morebits.wiki.Page(mw.config.get('wgPageName'), 'ページを保護');
if (mw.config.get('wgArticleId')) {
if (input.editmodify) {
thispage.setEditProtection(input.editlevel, input.editexpiry);
}
if (input.movemodify) {
// Ensure a level has actually been chosen
if (input.movelevel) {
thispage.setMoveProtection(input.movelevel, input.moveexpiry);
} else {
alert('移動保護のレベルを選択して下さい!');
return;
}
}
thispage.setWatchlist(Twinkle.getPref('watchProtectedPages'));
} else {
thispage.setCreateProtection(input.createlevel, input.createexpiry);
thispage.setWatchlist(false);
}
if (input.protectReason) {
thispage.setEditSummary(input.protectReason);
} else {
alert('保護理由を入力する必要があります。入力された理由は保護記録に記録されます。');
return;
}
if (input.protectReason_notes_rfppRevid && !/^\d+$/.test(input.protectReason_notes_rfppRevid)) {
alert('指定された版番号が無効です。正しい番号("oldid"とも言う)を見つける方法に関しては https://ja.wikipedia.org/wiki/Help:固定リンク をご覧ください。');
return;
}
if (!statusInited) {
Morebits.SimpleWindow.setButtonsEnabled(false);
Morebits.Status.init(form);
statusInited = true;
}
// thispage.setChangeTags(Twinkle.changeTags);
thispage.protect(next);
};
var stabilizeIt = function twinkleprotectCallbackStabilizeIt() {
if (thispage) {
thispage.getStatusElement().info('done');
}
thispage = new Morebits.wiki.Page(mw.config.get('wgPageName'), 'Applying pending changes protection');
thispage.setFlaggedRevs(input.pclevel, input.pcexpiry);
if (input.protectReason) {
thispage.setEditSummary(input.protectReason + Twinkle.summaryAd); // flaggedrevs tag support: [[phab:T247721]]
} else {
alert('You must enter a protect reason, which will be inscribed into the protection log.');
return;
}
if (!statusInited) {
Morebits.SimpleWindow.setButtonsEnabled(false);
Morebits.Status.init(form);
statusInited = true;
}
thispage.setWatchlist(Twinkle.getPref('watchProtectedPages'));
thispage.stabilize(allDone, (error) => {
if (error.errorCode === 'stabilize_denied') { // [[phab:T234743]]
thispage.getStatusElement().error('Failed trying to modify pending changes settings, likely due to a mediawiki bug. Other actions (tagging or regular protection) may have taken place. Please reload the page and try again.');
}
});
};
if (input.editmodify || input.movemodify || !mw.config.get('wgArticleId')) {
if (input.pcmodify) {
protectIt(stabilizeIt);
} else {
protectIt(allDone);
}
} else if (input.pcmodify) {
stabilizeIt();
} else {
alert("Twinkleに何かをさせてあげてください!\nページにタグを付けるだけなら、上部にある'保護テンプレートをページにタグ付けする'オプションを選ぶことができます。");
}
break;
case 'tag':
// apply a protection template
Morebits.SimpleWindow.setButtonsEnabled(false);
Morebits.Status.init(form);
Morebits.wiki.actionCompleted.redirect = mw.config.get('wgPageName');
Morebits.wiki.actionCompleted.followRedirect = false;
Morebits.wiki.actionCompleted.notice = 'タグ付け操作';
Twinkle.protect.callbacks.taggingPageInitial(tagparams);
break;
case 'request':
if (!input.reason) {
alert('保護が必要な理由を入力してください。');
return;
}
// file request at RFPP
var typename, typereason;
switch (input.category) {
case 'pp-dispute':
case 'pp-vandalism':
case 'pp':
case 'pp-template':
typename = '全保護';
break;
case 'pp-120-500-vandalism':
case 'pp-120-500-blp':
case 'pp-120-500-sock':
case 'pp-120-500-protected':
case 'pp-120-500-template':
typename = '拡張半保護';
break;
case 'pp-sock-noticeboard':
case 'pp-semi-vandalism':
case 'pp-semi-sock':
case 'pp-semi-blp':
case 'pp-semi-protected':
case 'pp-semi-template':
typename = '半保護';
break;
case 'pp-move':
case 'pp-move-dispute':
case 'pp-move-indef':
case 'pp-move-vandalism':
typename = '移動保護';
break;
case 'pp-120-500-move':
case 'pp-120-500-move-vandalism':
typename = '移動拡張半保護';
break;
case 'pp-create':
case 'pp-create-sock':
case 'pp-create-vandalism':
case 'pp-create-spam':
case 'pp-create-blp':
case 'pp-create-salt':
typename = '作成保護';
break;
case 'pp-120-500-create':
case 'pp-120-500-create-sock':
case 'pp-120-500-create-vandalism':
case 'pp-120-500-create-spam':
case 'pp-120-500-create-blp':
case 'pp-120-500-create-salt':
typename = '作成拡張半保護';
break;
case 'pp-semi-create':
case 'pp-semi-create-sock':
case 'pp-semi-create-vandalism':
case 'pp-semi-create-spam':
case 'pp-semi-create-blp':
case 'pp-semi-create-salt':
typename = '作成半保護';
break;
default:
typename = '保護解除';
break;
}
switch (input.category) {
case 'pp-dispute':
typereason = '編集合戦';
break;
case 'pp-vandalism':
case 'pp-create-vandalism':
typereason = '拡張承認された利用者による問題投稿の繰り返し';
break;
case 'pp-120-500-vandalism':
case 'pp-120-500-create-vandalism':
typereason = '(自動)承認された利用者による問題投稿の繰り返し';
break;
case 'pp-semi-vandalism':
case 'pp-semi-create-vandalism':
typereason = 'IP・新規利用者による問題投稿の繰り返し';
break;
case 'pp-move-vandalism':
case 'pp-move-120-500-vandalism':
typereason = '度重なる荒らし';
break;
case 'pp-template':
case 'pp-120-500-template':
case 'pp-semi-template':
typereason = '[[WP:HRT|影響が特に大きいテンプレート]]';
break;
case 'pp-sock-noticeboard':
case 'pp-semi-sock':
case 'pp-120-500-sock':
case 'pp-create-sock':
case 'pp-120-500-create-sock':
case 'pp-semi-create-sock':
typereason = '[[WP:SOCK|ソックパペット]]による問題投稿の繰り返し';
break;
case 'pp-semi-blp':
case 'pp-120-500-blp':
case 'pp-create-blp':
case 'pp-120-500-create-blp':
case 'pp-semi-create-blp':
typereason = 'プライバシー侵害の記述の繰り返し';
break;
case 'pp-move-dispute':
typereason = '移動合戦';
break;
case 'pp-create-spam':
case 'pp-120-500-create-spam':
case 'pp-semi-create-spam':
typereason = '度重なる宣伝';
break;
case 'pp-move-indef':
typereason = '移動不要ページ';
break;
case 'pp-create-salt':
case 'pp-120-500-create-salt':
case 'pp-semi-create-salt':
typereason = '削除されたページの改善なき再作成の繰り返し';
break;
default:
typereason = '';
break;
}
var reason = typereason;
if (input.reason !== '') {
if (typereason !== '') {
reason += '\u00A0\u2013 '; // U+00A0 NO-BREAK SPACE; U+2013 EN RULE
}
reason += input.reason;
}
var rppparams = {
reason: reason,
typename: typename,
category: input.category,
expiry: input.expiry
};
Morebits.SimpleWindow.setButtonsEnabled(false);
Morebits.Status.init(form);
var rppName = 'Wikipedia:保護依頼';
// Updating data for the action completed event
Morebits.wiki.actionCompleted.redirect = rppparams.typename !== '保護解除' ? 'Project:保護依頼' : 'Project:保護解除依頼';
Morebits.wiki.actionCompleted.notice = '依頼完了、依頼ページへリダイレクトします。';
var rppPage = new Morebits.wiki.Page(rppName, 'ページの保護を依頼');
rppPage.setFollowRedirect(true);
rppPage.setCallbackParameters(rppparams);
rppPage.load(Twinkle.protect.callbacks.fileRequest);
if (input.req_template && rppparams.typename !== '保護解除') {
Twinkle.protect.callbacks.taggingPageInitial(tagparams);
}
break;
default:
alert('twinkleprotect: 不明な操作');
break;
}
};
Twinkle.protect.protectReasonAnnotations = [];
Twinkle.protect.callback.annotateProtectReason = function twinkleprotectCallbackAnnotateProtectReason(e) {
const form = e.target.form;
const protectReason = form.protectReason.value.replace(new RegExp('(?:; )?' + mw.util.escapeRegExp(Twinkle.protect.protectReasonAnnotations.join(': '))), '');
if (this.name === 'protectReason_notes_rfpp') {
if (this.checked) {
Twinkle.protect.protectReasonAnnotations.push(this.value);
$(form.protectReason_notes_rfppRevid).parent().show();
} else {
Twinkle.protect.protectReasonAnnotations = [];
form.protectReason_notes_rfppRevid.value = '';
$(form.protectReason_notes_rfppRevid).parent().hide();
}
} else if (this.name === 'protectReason_notes_rfppRevid') {
Twinkle.protect.protectReasonAnnotations = Twinkle.protect.protectReasonAnnotations.filter((el) => !el.includes('[[Special:Permalink')).filter((el) => !el.includes('[[WP:RFPP'));
if (e.target.value.length) {
var permalink = '[[Special:Permalink/' + e.target.value + '#' + Morebits.pageNameNorm + '(ノート_/_履歴_/_ログ_/_リンク元)|WP:RFPP]]による';
Twinkle.protect.protectReasonAnnotations.push(permalink);
} else {
Twinkle.protect.protectReasonAnnotations.push('[[WP:RFPP]]による');
}
}
if (!Twinkle.protect.protectReasonAnnotations.length) {
form.protectReason.value = protectReason;
} else {
form.protectReason.value = (protectReason ? protectReason + '; ' : '') + Twinkle.protect.protectReasonAnnotations.join(': ');
}
};
Twinkle.protect.callbacks = {
taggingPageInitial: function(tagparams) {
if (tagparams.tag === 'noop') {
Morebits.Status.info('Applying protection template', 'nothing to do');
return;
}
var protectedPage = new Morebits.wiki.Page(mw.config.get('wgPageName'), 'ページにタグ付け');
protectedPage.setCallbackParameters(tagparams);
protectedPage.load(Twinkle.protect.callbacks.taggingPage);
},
taggingPage: function(protectedPage) {
const params = protectedPage.getCallbackParameters();
let text = protectedPage.getPageText();
let tag, summary;
var oldtag_re = /(?:\/\*)?\s*(?:<noinclude>)?\s*\{\{\s*((?:pp|保護依頼)[^{}]*?|protected|(?:t|v|s|p-|usertalk-v|usertalk-s|sb|move)protected(?:2)?|protected template|privacy protection)\s*?\}\}\s*(?:<\/noinclude>)?\s*(?:\*\/)?\s*/gi;
var re_result = oldtag_re.exec(text);
if (re_result) {
if (params.tag === 'none' || confirm('ページ上に保護関連テンプレート({{' + re_result[1] + '}}など)が見つかりました。\nOKをクリックして除去する、またはCancelをクリックしてそのままにしておきます。')) {
text = text.replace(oldtag_re, '');
}
}
if (params.tag === 'none') {
summary = '保護(依頼)テンプレートの除去';
} else {
tag = params.tag;
if (params.reason) {
tag += '|reason=' + params.reason;
}
if (params.small) {
tag += '|small=yes';
}
if (/^\s*#(redirect|転送)/i.test(text)) { // redirect page
// Only tag if no {{rcat shell}} is found
if (!text.match(/{{(?:redr|this is a redirect|r(?:edirect)?(?:.?cat.*)?[ _]?sh)/i)) {
text = text.replace(/#(?:redirect|転送) ?(\[\[.*?\]\])(.*)/i, '#REDIRECT $1$2\n\n{{' + tag + '}}');
} else {
Morebits.Status.info('Redirect category shell present', 'nothing to do');
return;
}
} else {
const needsTagToBeCommentedOut = ['javascript', 'css', 'sanitized-css'].includes(protectedPage.getContentModel());
if (needsTagToBeCommentedOut) {
if (params.noinclude) {
tag = '/* <noinclude>{{' + tag + '}}</noinclude> */';
} else {
tag = '/* {{' + tag + '}} */\n';
}
// Prepend tag at very top
text = tag + text;
} else {
if (params.noinclude) {
tag = '<noinclude>{{' + tag + '}}</noinclude>';
if (text.startsWith('==')) {
tag += '\n'; // a newline is needed to prevent section headings at the very beginning of the page from breaking
}
} else {
tag = '{{' + tag + '}}\n';
}
// Insert tag after short description or any hatnotes
const wikipage = new Morebits.wikitext.Page(text);
text = wikipage.insertAfterTemplates(tag, Twinkle.hatnoteRegex).getText();
}
}
summary = '{{' + params.tag + '}}を追加';
}
protectedPage.setEditSummary(summary + Twinkle.summaryAd);
// protectedPage.setChangeTags(Twinkle.changeTags);
protectedPage.setWatchlist(Twinkle.getPref('watchPPTaggedPages'));
protectedPage.setPageText(text);
protectedPage.setCreateOption('nocreate');
protectedPage.suppressProtectWarning(); // no need to let admins know they are editing through protection
protectedPage.save();
},
fileRequest: function(rppPage) {
var rppPage2 = new Morebits.wiki.Page('Wikipedia:保護解除依頼', '依頼ページを読み込み中');
rppPage2.load(() => {
var params = rppPage.getCallbackParameters();
var text = rppPage.getPageText();
var statusElement = rppPage.getStatusElement();
var text2 = rppPage2.getPageText();
// If either protection type results in a increased status, then post it under increase
// else we post it under decrease
var increase = false;
if (params.typename !== '保護解除') {
increase = true;
}
var rppRe = new RegExp('====\\s*(\\{\\{)?\\s*[pP]age\\s*\\|\\s*' + Morebits.string.escapeRegExp(Morebits.pageNameNorm) + '\\s*(\\}\\})?\\s*====', 'm');
var newtag = '\n==== {{Page|' + Morebits.pageNameNorm + '}} ====\n';
var tag = new RegExp('^' + mw.util.escapeRegExp(newtag).replace(/\s+/g, '\\s*'), 'm').test(text) || rppRe.exec(text);
var tag2 = new RegExp('^' + mw.util.escapeRegExp(newtag).replace(/\s+/g, '\\s*'), 'm').test(text2) || rppRe.exec(text2);
var rppLink = document.createElement('a');
rppLink.setAttribute('href', mw.util.getUrl('Wikipedia:保護依頼'));
rppLink.appendChild(document.createTextNode('Wikipedia:保護依頼'));
var rppLink2 = document.createElement('a');
rppLink2.setAttribute('href', mw.util.getUrl('Wikipedia:保護解除依頼'));
rppLink2.appendChild(document.createTextNode('Wikipedia:保護解除依頼'));
if (tag && increase) {
statusElement.error(['このページの保護依頼が', rppLink, 'で見つかりました、中止します。']);
return;
} else if (tag2 && !increase) {
statusElement.error(['このページの保護解除依頼が', rppLink2, 'で見つかりました、中止します。']);
return;
}
newtag += Morebits.string.formatReasonText(params.reason) + '--~~~~';
if (increase) {
rppPage.parse(() => {
var sections = rppPage.getSections();
// 依頼先の節取得
var time = new Morebits.Date();
var month = time.getMonth() + 1;
var date = time.getDate();
var section = null;
if (date < 11) {
section = month + '月上旬(1日から10日まで)';
} else if (date > 20) {
section = month + '月下旬(21日から末日まで)';
} else {
section = month + '月中旬(11日から20日まで)';
}
sections = sections.filter((sections) => sections.level === '3').filter((sections) => sections.fromtitle === 'Wikipedia:保護依頼').filter((sections) => sections.line === section);
if (sections.length !== 1) {
statusElement.error('報告先の節を正しく取得できませんでした。');
return;
}
statusElement.status('新規依頼を追加中...');
rppPage.setPageSection(Number(sections[0].index));
rppPage.setEditSummary('/* ' + Morebits.pageNameNorm + ' */ [[:' + Morebits.pageNameNorm + ']]の' + params.typename + 'を依頼' + Twinkle.summaryAd);
// rppPage.setChangeTags(Twinkle.changeTags);
rppPage.setAppendText(newtag);
rppPage.setCreateOption('recreate');
rppPage.save(() => {
// Watch the page being requested
var watchPref = Twinkle.getPref('watchRequestedPages');
// action=watch has no way to rely on user preferences (T262912), so we do it manually.
// The watchdefault pref appears to reliably return '1' (string),
// but that's not consistent among prefs so might as well be "correct"
var watch = watchPref !== 'no' && (watchPref !== 'default' || !!parseInt(mw.user.options.get('watchdefault'), 10));
if (watch) {
var watch_query = {
action: 'watch',
titles: mw.config.get('wgPageName'),
token: mw.user.tokens.get('watchToken')
};
// Only add the expiry if page is unwatched or already temporarily watched
if (Twinkle.protect.watched !== true && watchPref !== 'default' && watchPref !== 'yes') {
watch_query.expiry = watchPref;
}
new Morebits.wiki.Api('依頼したページをウォッチリストに追加中', watch_query).post();
}
});
});
} else {
rppPage2.parse(() => {
var sections = rppPage2.getSections();
// 依頼先の節取得
var time = new Morebits.Date();
var month = time.getMonth() + 1;
var date = time.getDate();
var section = null;
if (date < 11) {
section = month + '月上旬(1日から10日まで)';
} else if (date > 20) {
section = month + '月下旬(21日から末日まで)';
} else {
section = month + '月中旬(11日から20日まで)';
}
sections = sections.filter((sections) => sections.level === '3').filter((sections) => sections.fromtitle === 'Wikipedia:保護解除依頼').filter((sections) => sections.line === section);
if (sections.length !== 1) {
statusElement.error('報告先の節を正しく取得できませんでした。');
return;
}
text2 += '\n' + newtag;
statusElement.status('新規依頼を追加中...');
rppPage2.setPageSection(Number(sections[0].index));
rppPage2.setEditSummary('/* ' + Morebits.pageNameNorm + ' */ [[:' + Morebits.pageNameNorm + ']]の' + params.typename + 'を依頼' + Twinkle.summaryAd);
// rppPage2.setChangeTags(Twinkle.changeTags);
rppPage2.setAppendText(newtag);
rppPage2.setCreateOption('recreate');
rppPage2.save(() => {
// Watch the page being requested
var watchPref = Twinkle.getPref('watchRequestedPages');
// action=watch has no way to rely on user preferences (T262912), so we do it manually.
// The watchdefault pref appears to reliably return '1' (string),
// but that's not consistent among prefs so might as well be "correct"
var watch = watchPref !== 'no' && (watchPref !== 'default' || !!parseInt(mw.user.options.get('watchdefault'), 10));
if (watch) {
var watch_query = {
action: 'watch',
titles: mw.config.get('wgPageName'),
token: mw.user.tokens.get('watchToken')
};
// Only add the expiry if page is unwatched or already temporarily watched
if (Twinkle.protect.watched !== true && watchPref !== 'default' && watchPref !== 'yes') {
watch_query.expiry = watchPref;
}
new Morebits.wiki.Api('依頼したページをウォッチリストに追加中', watch_query).post();
}
});
});
}
});
}
};
Twinkle.addInitCallback(Twinkle.protect, 'protect');
}());
// </nowiki>
nb9uopgscphbnd8e20qj7qhakja7l4x
769570
769533
2026-10-22T02:24:44Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at d9cd3461baca43ac99ac2c24fb76a3aae6ddada9: minify
769570
javascript
text/javascript
// <nowiki>
(function(){Twinkle.protect=function twinkleprotect(){if(mw.config.get("wgNamespaceNumber")<0||mw.config.get("wgNamespaceNumber")===8){return}Twinkle.addPortletLink(Twinkle.protect.callback,Morebits.userIsSysop?"PP":"RPP","tw-rpp",Morebits.userIsSysop?"ページを保護":"ページの保護を依頼")};Twinkle.protect.callback=function twinkleprotectCallback(){var Window=new Morebits.SimpleWindow(620,530);Window.setTitle(Morebits.userIsSysop?"ページ保護の適用、依頼、タグ付け":"ページ保護の依頼、タグ付け");Window.setScriptName("Twinkle");Window.addFooterLink("保護テンプレート","Template:Pp#関連項目");Window.addFooterLink("保護の方針","Wikipedia:保護の方針");Window.addFooterLink("Twinkle help",":en:WP:TW/DOC#protect");Window.addFooterLink("フィードバック","User talk:Syunsyunminmin/Twinkle");const form=new Morebits.QuickForm(Twinkle.protect.callback.evaluate);const actionfield=form.append({type:"field",label:"操作の種類"});if(Morebits.userIsSysop){actionfield.append({type:"radio",name:"actiontype",event:Twinkle.protect.callback.changeAction,list:[{label:"ページを保護",value:"protect",tooltip:"ページに保護を適用する。",checked:true}]})}actionfield.append({type:"radio",name:"actiontype",event:Twinkle.protect.callback.changeAction,list:[{label:"ページの保護を依頼",value:"request",tooltip:(Morebits.userIsSysop?"自分で保護する代わりに":"")+"WP:RFPPで保護を依頼する場合",checked:!Morebits.userIsSysop},{label:"保護テンプレートをページにタグ付けする",value:"tag",tooltip:"保護する管理者が保護テンプレートを貼り忘れたり、タグを付けずにページを保護した場合、これを使って適切な保護タグを貼り付けることができます。",disabled:mw.config.get("wgArticleId")===0||mw.config.get("wgPageContentModel")==="Scribunto"||mw.config.get("wgNamespaceNumber")===710}]});form.append({type:"field",label:"プリセット",name:"field_preset"});form.append({type:"field",label:"1",name:"field1"});form.append({type:"field",label:"2",name:"field2"});form.append({type:"submit"});const result=form.render();Window.setContent(result);Window.display();const evt=document.createEvent("Event");evt.initEvent("change",true,true);result.actiontype[0].dispatchEvent(evt);Twinkle.protect.fetchProtectionLevel()};Twinkle.protect.trustedBots=["MusikBot II","TFA Protector Bot"];const hasFlaggedRevs=mw.loader.getState("ext.flaggedRevs.review")&&(mw.config.get("wgNamespaceNumber")===0||mw.config.get("wgNamespaceNumber")===4);Twinkle.protect.currentProtectionLevels={};Twinkle.protect.fetchProtectingAdmin=function twinkleprotectFetchProtectingAdmin(api,pageName,protType,logIds){logIds=logIds||[];return api.get({format:"json",action:"query",list:"logevents",letitle:pageName,letype:protType}).then(data=>{const event=data.query?$.grep(data.query.logevents,le=>$.inArray(le.logid,logIds))[0]:null;if(!event){return null}else if(event.action==="move_prot"||event.action==="move_stable"){return twinkleprotectFetchProtectingAdmin(api,protType==="protect"?event.params.oldtitle_title:event.params.oldtitle,protType,logIds.concat(event.logid))}return event.user})};Twinkle.protect.fetchProtectionLevel=function twinkleprotectFetchProtectionLevel(){const api=new mw.Api;const protectDeferred=api.get({format:"json",indexpageids:true,action:"query",list:"logevents",letype:"protect",letitle:mw.config.get("wgPageName"),prop:hasFlaggedRevs?"info|flagged":"info",inprop:"protection|watched",titles:mw.config.get("wgPageName")});const stableDeferred=api.get({format:"json",action:"query",list:"logevents",letype:"stable",letitle:mw.config.get("wgPageName")});const earlyDecision=[protectDeferred];if(hasFlaggedRevs){earlyDecision.push(stableDeferred)}$.when.apply($,earlyDecision).done((protectData,stableData)=>{protectData=$(protectData).toArray();const pageid=protectData[0].query.pageids[0];const page=protectData[0].query.pages[pageid];const current={};let adminEditDeferred;Twinkle.protect.watched=page.watchlistexpiry||page.watched==="";$.each(page.protection,(index,protection)=>{if(!protection.source){current[protection.type]={level:protection.level,expiry:protection.expiry,cascade:protection.cascade===""};if(!adminEditDeferred){adminEditDeferred=Twinkle.protect.fetchProtectingAdmin(api,mw.config.get("wgPageName"),"protect")}}else{current.cascading={expiry:protection.expiry,source:protection.source,level:protection.level}}});if(page.flagged){current.stabilize={level:page.flagged.protection_level,expiry:page.flagged.protection_expiry};adminEditDeferred=Twinkle.protect.fetchProtectingAdmin(api,mw.config.get("wgPageName"),"stable")}Twinkle.protect.hasProtectLog=!!protectData[0].query.logevents.length;Twinkle.protect.protectLog=Twinkle.protect.hasProtectLog&&protectData[0].query.logevents;Twinkle.protect.hasStableLog=hasFlaggedRevs?!!stableData[0].query.logevents.length:false;Twinkle.protect.stableLog=Twinkle.protect.hasStableLog&&stableData[0].query.logevents;Twinkle.protect.currentProtectionLevels=current;if(adminEditDeferred){adminEditDeferred.done(admin=>{if(admin){$.each(["edit","move","create","stabilize","cascading"],(i,type)=>{if(Twinkle.protect.currentProtectionLevels[type]){Twinkle.protect.currentProtectionLevels[type].admin=admin}})}Twinkle.protect.callback.showLogAndCurrentProtectInfo()})}else{Twinkle.protect.callback.showLogAndCurrentProtectInfo()}})};Twinkle.protect.callback.showLogAndCurrentProtectInfo=function twinkleprotectCallbackShowLogAndCurrentProtectInfo(){const currentlyProtected=!$.isEmptyObject(Twinkle.protect.currentProtectionLevels);if(Twinkle.protect.hasProtectLog||Twinkle.protect.hasStableLog){const $linkMarkup=$("<span>");if(Twinkle.protect.hasProtectLog){$linkMarkup.append($('<a target="_blank" href="'+mw.util.getUrl("Special:Log",{action:"view",page:mw.config.get("wgPageName"),type:"protect"})+'">保護記録</a>'));if(!currentlyProtected||!Twinkle.protect.currentProtectionLevels.edit&&!Twinkle.protect.currentProtectionLevels.move){const lastProtectAction=Twinkle.protect.protectLog[0];if(lastProtectAction.action==="unprotect"){$linkMarkup.append(" ("+new Morebits.Date(lastProtectAction.timestamp).calendar("utc")+"に保護解除)")}else{$linkMarkup.append(" ("+new Morebits.Date(lastProtectAction.params.details[0].expiry).calendar("utc")+"に期限切れ)")}}$linkMarkup.append(Twinkle.protect.hasStableLog?$("<span> • </span>"):null)}if(Twinkle.protect.hasStableLog){$linkMarkup.append($('<a target="_blank" href="'+mw.util.getUrl("Special:Log",{action:"view",page:mw.config.get("wgPageName"),type:"stable"})+'">pending changes log</a>)'));if(!currentlyProtected||!Twinkle.protect.currentProtectionLevels.stabilize){const lastStabilizeAction=Twinkle.protect.stableLog[0];if(lastStabilizeAction.action==="reset"){$linkMarkup.append(" (reset "+new Morebits.Date(lastStabilizeAction.timestamp).calendar("utc")+")")}else{$linkMarkup.append(" (expired "+new Morebits.Date(lastStabilizeAction.params.expiry).calendar("utc")+")")}}}Morebits.Status.init($('div[name="hasprotectlog"] span')[0]);Morebits.Status.warn(currentlyProtected?"以前の保護":"このページは過去に保護されたことがあります",$linkMarkup[0])}Morebits.Status.init($('div[name="currentprot"] span')[0]);let protectionNode=[],statusLevel="info";if(currentlyProtected){$.each(Twinkle.protect.currentProtectionLevels,(type,settings)=>{var label=type==="stabilize"?"Pending Changes":"";switch(Morebits.string.toUpperCaseFirstChar(type)){case"Edit":label="編集";break;case"Move":label="移動";break;case"create":label="作成";break;case"upload":label="アップロード";break;default:label=Morebits.string.toUpperCaseFirstChar(type);break}if(type==="cascading"){label="カスケード保護 ";protectionNode.push($("<b>"+label+"</b>")[0]);if(settings.source){const sourceLink='<a target="_blank" href="'+mw.util.getUrl(settings.source)+'">'+settings.source+"</a>";protectionNode.push($("<span>from "+sourceLink+"</span>")[0])}}else{var level;switch(settings.level){case"sysop":level="管理者のみ";break;case"extendedconfirmed":level="拡張承認された利用者のみ";break;case"autoconfirmed":level="自動承認された利用者のみ";break;default:level=settings.level;break}if(settings.cascade){level+=" (カスケード)"}protectionNode.push($("<b>"+label+": "+level+"</b>")[0])}if(settings.expiry==="infinity"){protectionNode.push(" (無期限) ")}else{protectionNode.push(" ("+new Morebits.Date(settings.expiry).calendar("utc")+"に期限切れ) ")}if(settings.admin){var adminLink='<a target="_blank" href="'+mw.util.getUrl("User talk:"+settings.admin)+'">'+settings.admin+"</a>";protectionNode.push($("<span>"+adminLink+"による</span>")[0])}protectionNode.push($("<span> • </span>")[0])});protectionNode=protectionNode.slice(0,-1);statusLevel="warn"}else{protectionNode.push($("<b>保護なし</b>")[0])}Morebits.Status[statusLevel]("現在の保護レベル",protectionNode)};Twinkle.protect.callback.changeAction=function twinkleprotectCallbackChangeAction(e){let field_preset;let field1;let field2;switch(e.target.values){case"protect":field_preset=new Morebits.QuickForm.Element({type:"field",label:"プリセット",name:"field_preset"});field_preset.append({type:"select",name:"category",label:"プリセットを選択:",event:Twinkle.protect.callback.changePreset,list:mw.config.get("wgArticleId")?Twinkle.protect.protectionTypes:Twinkle.protect.protectionTypesCreate});field2=new Morebits.QuickForm.Element({type:"field",label:"保護設定",name:"field2"});field2.append({type:"div",name:"currentprot",label:" "});field2.append({type:"div",name:"hasprotectlog",label:" "});if(mw.config.get("wgArticleId")){field2.append({type:"checkbox",event:Twinkle.protect.formevents.editmodify,list:[{label:"編集保護設定を変更",name:"editmodify",tooltip:"これをオフにすると、編集保護レベルと有効期限はそのままになります。",checked:true}]});field2.append({type:"select",name:"editlevel",label:"誰が編集できるか:",event:Twinkle.protect.formevents.editlevel,list:Twinkle.protect.protectionLevels});field2.append({type:"select",name:"editexpiry",label:"期限:",event:function(e){if(e.target.value==="custom"){Twinkle.protect.doCustomExpiry(e.target)}},list:Twinkle.protect.protectionLengths});field2.append({type:"checkbox",event:Twinkle.protect.formevents.movemodify,list:[{label:"移動保護設定を変更",name:"movemodify",tooltip:"これをオフにすると、移動保護レベルと有効期限はそのままになります。",checked:true}]});field2.append({type:"select",name:"movelevel",label:"誰が移動できるか:",event:Twinkle.protect.formevents.movelevel,list:Twinkle.protect.protectionLevels.filter(level=>level.value!=="autoconfirmed")});field2.append({type:"select",name:"moveexpiry",label:"期限:",event:function(e){if(e.target.value==="custom"){Twinkle.protect.doCustomExpiry(e.target)}},list:Twinkle.protect.protectionLengths});if(hasFlaggedRevs){field2.append({type:"checkbox",event:Twinkle.protect.formevents.pcmodify,list:[{label:"Modify pending changes protection",name:"pcmodify",tooltip:"If this is turned off, the pending changes level, and expiry time, will be left as is.",checked:true}]});field2.append({type:"select",name:"pclevel",label:"Pending changes:",event:Twinkle.protect.formevents.pclevel,list:[{label:"None",value:"none"},{label:"Pending change",value:"autoconfirmed",selected:true}]});field2.append({type:"select",name:"pcexpiry",label:"Expires:",event:function(e){if(e.target.value==="custom"){Twinkle.protect.doCustomExpiry(e.target)}},list:Twinkle.protect.protectionLengths})}}else{field2.append({type:"select",name:"createlevel",label:"作成保護:",event:Twinkle.protect.formevents.createlevel,list:Twinkle.protect.protectionLevels});field2.append({type:"select",name:"createexpiry",label:"期限:",event:function(e){if(e.target.value==="custom"){Twinkle.protect.doCustomExpiry(e.target)}},list:Twinkle.protect.protectionLengths})}field2.append({type:"textarea",name:"protectReason",label:"理由 (保護記録用):"});field2.append({type:"div",name:"protectReason_notes",label:"注記:",style:"display:inline-block; margin-top:4px;",tooltip:"保護記録に、RFPPで依頼されたというメモを追加する。"});field2.append({type:"checkbox",event:Twinkle.protect.callback.annotateProtectReason,style:"display:inline-block; margin-top:4px;",list:[{label:"RFPP依頼",name:"protectReason_notes_rfpp",checked:false,value:"[[WP:RFPP]]による"}]});field2.append({type:"input",event:Twinkle.protect.callback.annotateProtectReason,label:"RFPP版番号",name:"protectReason_notes_rfppRevid",value:"",tooltip:"保護が依頼されたRFPPページの版番号。"});if(!mw.config.get("wgArticleId")||mw.config.get("wgPageContentModel")==="Scribunto"||mw.config.get("wgNamespaceNumber")===710){break}case"tag":field1=new Morebits.QuickForm.Element({type:"field",label:"タグ付け設定",name:"field1"});field1.append({type:"div",name:"currentprot",label:" "});field1.append({type:"div",name:"hasprotectlog",label:" "});field1.append({type:"select",name:"tagtype",label:"保護テンプレートを選択:",list:Twinkle.protect.protectionTags,event:Twinkle.protect.formevents.tagtype});var isTemplateNamespace=mw.config.get("wgNamespaceNumber")===10;var isAFD=Morebits.pageNameNorm.startsWith("Wikipedia:削除依頼/");var isCode=["javascript","css","sanitized-css"].includes(mw.config.get("wgPageContentModel"));field1.append({type:"checkbox",list:[{name:"small",label:"アイコン化 (small=yes)",tooltip:"テンプレートの|small=yes機能を使用し、インジケーターとしてのみ表示する。",checked:true},{name:"noinclude",label:"保護テンプレートを<noinclude>で囲む",tooltip:"参照読み込みされないように保護テンプレートを<noinclude>で囲む",checked:(isTemplateNamespace||isAFD)&&!isCode}]});break;case"request":field_preset=new Morebits.QuickForm.Element({type:"field",label:"保護の種類",name:"field_preset"});field_preset.append({type:"select",name:"category",label:"種類と理由:",event:Twinkle.protect.callback.changePreset,list:mw.config.get("wgArticleId")?Twinkle.protect.protectionTypes:Twinkle.protect.protectionTypesCreate});field_preset.append({type:"checkbox",list:[{name:"req_template",label:"保護依頼テンプレートを貼り付ける",tooltip:"保護を依頼するページに{{保護依頼}}テンプレートを貼り付ける",checked:true},{name:"noinclude",label:"保護テンプレートを<noinclude>で囲む",tooltip:"参照読み込みされないように保護依頼テンプレートを<noinclude>で囲む",checked:mw.config.get("wgNamespaceNumber")===10||mw.config.get("wgNamespaceNumber")===mw.config.get("wgNamespaceIds").project&&mw.config.get("wgTitle").indexOf("削除依頼/")===0}]});field1=new Morebits.QuickForm.Element({type:"field",label:"オプション",name:"field1"});field1.append({type:"div",name:"currentprot",label:" "});field1.append({type:"div",name:"hasprotectlog",label:" "});field1.append({type:"textarea",name:"reason",label:"理由:"});break;default:alert("twinkleprotectにて何かが進行中です");break}let oldfield;if(field_preset){oldfield=$(e.target.form).find('fieldset[name="field_preset"]')[0];oldfield.parentNode.replaceChild(field_preset.render(),oldfield)}else{$(e.target.form).find('fieldset[name="field_preset"]').css("display","none")}if(field1){oldfield=$(e.target.form).find('fieldset[name="field1"]')[0];oldfield.parentNode.replaceChild(field1.render(),oldfield)}else{$(e.target.form).find('fieldset[name="field1"]').css("display","none")}if(field2){oldfield=$(e.target.form).find('fieldset[name="field2"]')[0];oldfield.parentNode.replaceChild(field2.render(),oldfield)}else{$(e.target.form).find('fieldset[name="field2"]').css("display","none")}if(e.target.values==="protect"){const evt=document.createEvent("Event");evt.initEvent("change",true,true);e.target.form.category.dispatchEvent(evt);$(e.target.form).find('fieldset[name="field2"] select').parent().css({display:"inline-block",marginRight:"0.5em"});$(e.target.form).find('fieldset[name="field2"] input[name="protectReason_notes_rfppRevid"]').parent().css({display:"inline-block",marginLeft:"15px"}).hide()}Twinkle.protect.callback.showLogAndCurrentProtectInfo()};Twinkle.protect.formevents={editmodify:function twinkleprotectFormEditmodifyEvent(e){e.target.form.editlevel.disabled=!e.target.checked;e.target.form.editexpiry.disabled=!e.target.checked||e.target.form.editlevel.value==="all";e.target.form.editlevel.style.color=e.target.form.editexpiry.style.color=e.target.checked?"":"transparent"},editlevel:function twinkleprotectFormEditlevelEvent(e){e.target.form.editexpiry.disabled=e.target.value==="all"},movemodify:function twinkleprotectFormMovemodifyEvent(e){if(e.target.form.movelevel.disabled&&!e.target.form.editlevel.disabled){e.target.form.movelevel.value=e.target.form.editlevel.value;e.target.form.moveexpiry.value=e.target.form.editexpiry.value}else if(e.target.form.editlevel.disabled){e.target.form.movelevel.value="sysop";e.target.form.moveexpiry.value="infinity"}e.target.form.movelevel.disabled=!e.target.checked;e.target.form.moveexpiry.disabled=!e.target.checked||e.target.form.movelevel.value==="all";e.target.form.movelevel.style.color=e.target.form.moveexpiry.style.color=e.target.checked?"":"transparent"},movelevel:function twinkleprotectFormMovelevelEvent(e){e.target.form.moveexpiry.disabled=e.target.value==="all"},pcmodify:function twinkleprotectFormPcmodifyEvent(e){e.target.form.pclevel.disabled=!e.target.checked;e.target.form.pcexpiry.disabled=!e.target.checked||e.target.form.pclevel.value==="none";e.target.form.pclevel.style.color=e.target.form.pcexpiry.style.color=e.target.checked?"":"transparent"},pclevel:function twinkleprotectFormPclevelEvent(e){e.target.form.pcexpiry.disabled=e.target.value==="none"},createlevel:function twinkleprotectFormCreatelevelEvent(e){e.target.form.createexpiry.disabled=e.target.value==="all"},tagtype:function twinkleprotectFormTagtypeEvent(e){e.target.form.small.disabled=e.target.form.noinclude.disabled=e.target.value==="none"||e.target.value==="noop"}};Twinkle.protect.doCustomExpiry=function twinkleprotectDoCustomExpiry(target){const custom=prompt('Enter a custom expiry time. \nYou can use relative times, like "1 minute" or "19 days", or absolute timestamps, "yyyymmddhhmm" (e.g. "200602011405" is Feb 1, 2006, at 14:05 UTC).',"");if(custom){const option=document.createElement("option");option.setAttribute("value",custom);option.textContent=custom;target.appendChild(option);target.value=custom}else{target.selectedIndex=0}};Twinkle.protect.protectionLevels=[{label:"全ての利用者",value:"all"},{label:"自動承認された利用者",value:"autoconfirmed"},{label:"拡張承認された利用者",value:"extendedconfirmed"},{label:"管理者",value:"sysop",selected:true}];Twinkle.protect.protectionLengths=[{label:"1時間",value:"1 hour"},{label:"2時間",value:"2 hours"},{label:"3時間",value:"3 hours"},{label:"6時間",value:"6 hours"},{label:"12時間",value:"12 hours"},{label:"1日間",value:"1 day"},{label:"2日間",value:"2 days"},{label:"3日間",value:"3 days"},{label:"4日間",value:"4 days"},{label:"1週間",value:"1 week"},{label:"2週間",value:"2 weeks"},{label:"1ヶ月間",value:"1 month"},{label:"2ヶ月間",value:"2 months"},{label:"3ヶ月間",value:"3 months"},{label:"1年",value:"1 year"},{label:"無期限",value:"infinity"},{label:"その他",value:"custom"}];Twinkle.protect.protectionTypes=[{label:"保護解除",value:"unprotect"},{label:"全保護",list:[{label:"全般 (全)",value:"pp"},{label:"編集合戦 (全)",value:"pp-dispute"},{label:"拡張承認された利用者による問題投稿の繰り返し (全)",value:"pp-vandalism"},{label:"影響が特に大きいテンプレート (全)",value:"pp-template"}]},{label:"拡張半保護",list:[{label:"全般 (拡半)",value:"pp-120-500-protected"},{label:"(自動)承認された利用者による問題投稿の繰り返し (拡半)",value:"pp-120-500-vandalism"},{label:"プライバシー侵害 (拡半)",value:"pp-120-500-blp"},{label:"ソックパペット (拡半)",value:"pp-120-500-sock"},{label:"影響が特に大きいテンプレート (拡半)",value:"pp-120-500-template"}]},{label:"半保護",list:[{label:"全般 (半)",value:"pp-semi-protected"},{label:"IP・新規利用者による問題投稿の繰り返し (半)",selected:true,value:"pp-semi-vandalism"},{label:"プライバシー侵害 (半)",value:"pp-semi-blp"},{label:"ソックパペット (半)",value:"pp-semi-sock"},{label:"影響が特に大きいテンプレート (半)",value:"pp-semi-template"}]},{label:"移動保護",list:[{label:"全般 (移)",value:"pp-move"},{label:"移動合戦 (移)",value:"pp-move-dispute"},{label:"度重なる荒らし (移)",value:"pp-move-vandalism"},{label:"移動不要ページ (移)",value:"pp-move-indef"}]},{label:"移動拡張半保護",list:[{label:"全般 (移拡)",value:"pp-120-500-move"},{label:"度重なる荒らし (移拡)",value:"pp-120-500-move-vandalism"}]}];Twinkle.protect.protectionTypesCreate=[{label:"保護解除",value:"unprotect"},{label:"作成保護",list:[{label:"全般 (全)",value:"pp-create"},{label:"ソックパペットによる問題投稿の繰り返し (全)",value:"pp-create-sock"},{label:"拡張承認された利用者による問題投稿の繰り返し (全)",value:"pp-create-vandalism"},{label:"度重なる宣伝 (全)",value:"pp-create-spam"},{label:"削除されたページの改善なき再作成の繰り返し (全)",value:"pp-create-salt"},{label:"プライバシー侵害の記述の繰り返し (全)",value:"pp-create-blp"}]},{label:"作成拡張半保護",list:[{label:"全般 (拡)",value:"pp-120-500-create"},{label:"ソックパペットによる問題投稿の繰り返し (拡)",value:"pp-120-500-create-sock"},{label:"(自動)承認された利用者による問題投稿の繰り返し (拡)",value:"pp-120-500-create-vandalism"},{label:"度重なる宣伝 (拡)",value:"pp-120-500-create-spam"},{label:"削除されたページの改善なき再作成の繰り返し (拡)",value:"pp-120-500-create-salt"},{label:"プライバシー侵害の記述の繰り返し (拡)",value:"pp-120-500-create-blp"}]},{label:"作成半保護",list:[{label:"全般 (半)",value:"pp-semi-create"},{label:"ソックパペットによる問題投稿の繰り返し (半)",value:"pp-semi-create-sock"},{label:"IP・新規利用者による問題投稿の繰り返し (半)",value:"pp-semi-create-vandalism"},{label:"度重なる宣伝 (半)",value:"pp-semi-create-spam"},{label:"削除されたページの改善なき再作成の繰り返し (半)",selected:true,value:"pp-semi-create-salt"},{label:"プライバシー侵害の記述の繰り返し (半)",value:"pp-semi-create-blp"}]}];Twinkle.protect.protectionPresetsInfo={pp:{edit:"sysop",move:"sysop",reason:null},"pp-dispute":{edit:"sysop",move:"sysop",reason:"編集合戦"},"pp-vandalism":{edit:"sysop",move:"sysop",reason:"拡張承認された利用者による問題投稿の繰り返し"},"pp-template":{edit:"sysop",move:"sysop",expiry:"infinity",reason:"[[WP:HRT|影響が特に大きいテンプレート]]"},"pp-120-500-protected":{edit:"extendedconfirmed",move:"extendedconfirmed",reason:null,template:"pp"},"pp-120-500-vandalism":{edit:"extendedconfirmed",move:"extendedconfirmed",reason:"(自動)承認された利用者による問題投稿の繰り返し",template:"pp-vandalism"},"pp-120-500-blp":{edit:"extendedconfirmed",move:"extendedconfirmed",reason:"プライバシー侵害の記述の繰り返し",template:"pp-vandalism"},"pp-120-500-sock":{edit:"extendedconfirmed",move:"extendedconfirmed",reason:"[[WP:SOCK|ソックパペット]]による問題投稿の繰り返し",template:"pp-vandalism"},"pp-120-500-template":{edit:"extendedconfirmed",move:"extendedconfirmed",expiry:"infinity",reason:"[[WP:HRT|影響が特に大きいテンプレート]]",template:"pp-template"},"pp-semi-vandalism":{edit:"autoconfirmed",reason:"IP・新規利用者による問題投稿の繰り返し",template:"pp-vandalism"},"pp-semi-blp":{edit:"autoconfirmed",reason:"プライバシー侵害の記述の繰り返し",template:"pp-vandalism"},"pp-semi-template":{edit:"autoconfirmed",expiry:"infinity",reason:"[[WP:HRT|影響が特に大きいテンプレート]]",template:"pp-template"},"pp-semi-sock":{edit:"autoconfirmed",reason:"[[WP:SOCK|ソックパペット]]による問題投稿の繰り返し",template:"pp-vandalism"},"pp-semi-protected":{edit:"autoconfirmed",reason:null,template:"pp"},"pp-move":{move:"sysop",reason:null},"pp-move-dispute":{move:"sysop",expiry:"1 week",reason:"移動合戦"},"pp-move-vandalism":{move:"sysop",reason:"度重なる荒らし"},"pp-move-indef":{move:"sysop",expiry:"infinity",reason:"移動不要ページ",template:"pp-move"},"pp-120-500-move":{move:"extendedconfirmed",reason:null,template:"pp-move"},"pp-120-500-move-vandalism":{move:"extendedconfirmed",reason:null,template:"pp-move-vandalism"},unprotect:{edit:"all",move:"all",stabilize:"none",create:"all",reason:null,template:"none"},"pp-create-sock":{create:"sysop",reason:"[[WP:SOCK|ソックパペット]]による問題投稿の繰り返し"},"pp-create":{create:"sysop",reason:null},"pp-create-vandalism":{create:"sysop",reason:"拡張承認された利用者による問題投稿の繰り返し"},"pp-create-spam":{create:"sysop",reason:"度重なる宣伝"},"pp-create-salt":{create:"sysop",reason:"削除されたページの改善なき再作成の繰り返し"},"pp-create-blp":{create:"sysop",reason:"プライバシー侵害の記述の繰り返し"},"pp-120-500-create":{create:"extendedconfirmed",reason:null},"pp-120-500-create-sock":{create:"extendedconfirmed",reason:"[[WP:SOCK|ソックパペット]]による問題投稿の繰り返し"},"pp-120-500-create-vandalism":{create:"extendedconfirmed",reason:"(自動)承認された利用者による問題投稿の繰り返し"},"pp-120-500-create-spam":{create:"extendedconfirmed",reason:"度重なる宣伝"},"pp-120-500-create-salt":{create:"extendedconfirmed",reason:"削除されたページの改善なき再作成の繰り返し"},"pp-120-500-create-blp":{create:"sysop",reason:"プライバシー侵害の記述の繰り返し"},"pp-semi-create":{create:"autoconfirmed",reason:null},"pp-semi-create-sock":{create:"autoconfirmed",reason:"[[WP:SOCK|ソックパペット]]による問題投稿の繰り返し"},"pp-semi-create-vandalism":{create:"autoconfirmed",reason:"IP・新規利用者による問題投稿の繰り返し"},"pp-semi-create-spam":{create:"autoconfirmed",reason:"度重なる宣伝"},"pp-semi-create-salt":{create:"autoconfirmed",reason:"削除されたページの改善なき再作成の繰り返し"},"pp-semi-create-blp":{create:"autoconfirmed",reason:"プライバシー侵害の記述の繰り返し"}};Twinkle.protect.protectionTags=[{label:"なし (既にある保護テンプレートを除去する)",value:"none"},{label:"なし (既にある保護テンプレートを除去しない)",value:"noop"},{label:"編集保護テンプレート",list:[{label:"{{pp}}: 全般",value:"pp"},{label:"{{pp-vandalism}}: 荒らし",value:"pp-vandalism"},{label:"{{pp-dispute}}: 編集合戦",value:"pp-dispute"},{label:"{{pp-template}}: 影響が特に大きいテンプレート",value:"pp-template"},{label:"{{pp-semi-indef}}: 長期間の半保護",value:"pp-semi-indef"}]},{label:"移動保護テンプレート",list:[{label:"{{pp-move-dispute}}: 移動合戦",value:"pp-move-dispute"},{label:"{{pp-move-vandalism}}: 度重なる荒らし",value:"pp-move-vandalism"},{label:"{{pp-move}}: その他",value:"pp-move"}]}].filter(type=>hasFlaggedRevs||type.label!=="Pending changes templates");Twinkle.protect.callback.changePreset=function twinkleprotectCallbackChangePreset(e){const form=e.target.form;const actiontypes=form.actiontype;let actiontype;for(let i=0;i<actiontypes.length;i++){if(!actiontypes[i].checked){continue}actiontype=actiontypes[i].values;break}if(actiontype==="protect"){const item=Twinkle.protect.protectionPresetsInfo[form.category.value];if(mw.config.get("wgArticleId")){if(item.edit){form.editmodify.checked=true;Twinkle.protect.formevents.editmodify({target:form.editmodify});form.editlevel.value=item.edit;Twinkle.protect.formevents.editlevel({target:form.editlevel})}else{form.editmodify.checked=false;Twinkle.protect.formevents.editmodify({target:form.editmodify})}if(item.move){form.movemodify.checked=true;Twinkle.protect.formevents.movemodify({target:form.movemodify});form.movelevel.value=item.move;Twinkle.protect.formevents.movelevel({target:form.movelevel})}else{form.movemodify.checked=false;Twinkle.protect.formevents.movemodify({target:form.movemodify})}form.editexpiry.value=form.moveexpiry.value=item.expiry||"1 week";if(form.pcmodify){if(item.stabilize){form.pcmodify.checked=true;Twinkle.protect.formevents.pcmodify({target:form.pcmodify});form.pclevel.value=item.stabilize;Twinkle.protect.formevents.pclevel({target:form.pclevel})}else{form.pcmodify.checked=false;Twinkle.protect.formevents.pcmodify({target:form.pcmodify})}form.pcexpiry.value=item.expiry||"1 month"}}else{if(item.create){form.createlevel.value=item.create;Twinkle.protect.formevents.createlevel({target:form.createlevel})}form.createexpiry.value=item.expiry||"infinity"}const reasonField=actiontype==="protect"?form.protectReason:form.reason;if(item.reason){reasonField.value=item.reason}else{reasonField.value=""}Twinkle.protect.callback.annotateProtectReason(e);if(mw.config.get("wgArticleId")&&mw.config.get("wgPageContentModel")!=="Scribunto"&&mw.config.get("wgNamespaceNumber")!==710){if(form.category.value==="unprotect"){form.tagtype.value="none"}else{form.tagtype.value=item.template?item.template:form.category.value}Twinkle.protect.formevents.tagtype({target:form.tagtype});var isTemplateEditorProtection=form.category.value==="pp-template";var isAFD=Morebits.pageNameNorm.startsWith("Wikipedia:削除依頼/");var isNotTemplateNamespace=mw.config.get("wgNamespaceNumber")!==10;var isCode=["javascript","css","sanitized-css"].includes(mw.config.get("wgPageContentModel"));if((isTemplateEditorProtection||isAFD)&&!isCode){form.noinclude.checked=true}else if(isCode||isNotTemplateNamespace){form.noinclude.checked=false}}}else{if(form.category.value==="unprotect"){form.expiry.value="";form.expiry.disabled=true}else{form.expiry.value="";form.expiry.disabled=false}}};Twinkle.protect.callback.evaluate=function twinkleprotectCallbackEvaluate(e){const form=e.target;const input=Morebits.QuickForm.getInputData(form);let tagparams;if(input.actiontype==="tag"||input.actiontype==="protect"&&mw.config.get("wgArticleId")&&mw.config.get("wgPageContentModel")!=="Scribunto"&&mw.config.get("wgNamespaceNumber")!==710){tagparams={tag:input.tagtype,reason:false,small:input.small,noinclude:input.noinclude}}else if(input.actiontype==="request"){tagparams={tag:"保護依頼",reason:false,noinclude:input.noinclude}}switch(input.actiontype){case"protect":Morebits.wiki.actionCompleted.redirect=mw.config.get("wgPageName");Morebits.wiki.actionCompleted.notice="保護完了";var statusInited=false;var thispage;var allDone=function twinkleprotectCallbackAllDone(){if(thispage){thispage.getStatusElement().info("完了")}if(tagparams){Twinkle.protect.callbacks.taggingPageInitial(tagparams)}};var protectIt=function twinkleprotectCallbackProtectIt(next){thispage=new Morebits.wiki.Page(mw.config.get("wgPageName"),"ページを保護");if(mw.config.get("wgArticleId")){if(input.editmodify){thispage.setEditProtection(input.editlevel,input.editexpiry)}if(input.movemodify){if(input.movelevel){thispage.setMoveProtection(input.movelevel,input.moveexpiry)}else{alert("移動保護のレベルを選択して下さい!");return}}thispage.setWatchlist(Twinkle.getPref("watchProtectedPages"))}else{thispage.setCreateProtection(input.createlevel,input.createexpiry);thispage.setWatchlist(false)}if(input.protectReason){thispage.setEditSummary(input.protectReason)}else{alert("保護理由を入力する必要があります。入力された理由は保護記録に記録されます。");return}if(input.protectReason_notes_rfppRevid&&!/^\d+$/.test(input.protectReason_notes_rfppRevid)){alert('指定された版番号が無効です。正しい番号("oldid"とも言う)を見つける方法に関しては https://ja.wikipedia.org/wiki/Help:固定リンク をご覧ください。');return}if(!statusInited){Morebits.SimpleWindow.setButtonsEnabled(false);Morebits.Status.init(form);statusInited=true}thispage.protect(next)};var stabilizeIt=function twinkleprotectCallbackStabilizeIt(){if(thispage){thispage.getStatusElement().info("done")}thispage=new Morebits.wiki.Page(mw.config.get("wgPageName"),"Applying pending changes protection");thispage.setFlaggedRevs(input.pclevel,input.pcexpiry);if(input.protectReason){thispage.setEditSummary(input.protectReason+Twinkle.summaryAd)}else{alert("You must enter a protect reason, which will be inscribed into the protection log.");return}if(!statusInited){Morebits.SimpleWindow.setButtonsEnabled(false);Morebits.Status.init(form);statusInited=true}thispage.setWatchlist(Twinkle.getPref("watchProtectedPages"));thispage.stabilize(allDone,error=>{if(error.errorCode==="stabilize_denied"){thispage.getStatusElement().error("Failed trying to modify pending changes settings, likely due to a mediawiki bug. Other actions (tagging or regular protection) may have taken place. Please reload the page and try again.")}})};if(input.editmodify||input.movemodify||!mw.config.get("wgArticleId")){if(input.pcmodify){protectIt(stabilizeIt)}else{protectIt(allDone)}}else if(input.pcmodify){stabilizeIt()}else{alert("Twinkleに何かをさせてあげてください!\nページにタグを付けるだけなら、上部にある'保護テンプレートをページにタグ付けする'オプションを選ぶことができます。")}break;case"tag":Morebits.SimpleWindow.setButtonsEnabled(false);Morebits.Status.init(form);Morebits.wiki.actionCompleted.redirect=mw.config.get("wgPageName");Morebits.wiki.actionCompleted.followRedirect=false;Morebits.wiki.actionCompleted.notice="タグ付け操作";Twinkle.protect.callbacks.taggingPageInitial(tagparams);break;case"request":if(!input.reason){alert("保護が必要な理由を入力してください。");return}var typename,typereason;switch(input.category){case"pp-dispute":case"pp-vandalism":case"pp":case"pp-template":typename="全保護";break;case"pp-120-500-vandalism":case"pp-120-500-blp":case"pp-120-500-sock":case"pp-120-500-protected":case"pp-120-500-template":typename="拡張半保護";break;case"pp-sock-noticeboard":case"pp-semi-vandalism":case"pp-semi-sock":case"pp-semi-blp":case"pp-semi-protected":case"pp-semi-template":typename="半保護";break;case"pp-move":case"pp-move-dispute":case"pp-move-indef":case"pp-move-vandalism":typename="移動保護";break;case"pp-120-500-move":case"pp-120-500-move-vandalism":typename="移動拡張半保護";break;case"pp-create":case"pp-create-sock":case"pp-create-vandalism":case"pp-create-spam":case"pp-create-blp":case"pp-create-salt":typename="作成保護";break;case"pp-120-500-create":case"pp-120-500-create-sock":case"pp-120-500-create-vandalism":case"pp-120-500-create-spam":case"pp-120-500-create-blp":case"pp-120-500-create-salt":typename="作成拡張半保護";break;case"pp-semi-create":case"pp-semi-create-sock":case"pp-semi-create-vandalism":case"pp-semi-create-spam":case"pp-semi-create-blp":case"pp-semi-create-salt":typename="作成半保護";break;default:typename="保護解除";break}switch(input.category){case"pp-dispute":typereason="編集合戦";break;case"pp-vandalism":case"pp-create-vandalism":typereason="拡張承認された利用者による問題投稿の繰り返し";break;case"pp-120-500-vandalism":case"pp-120-500-create-vandalism":typereason="(自動)承認された利用者による問題投稿の繰り返し";break;case"pp-semi-vandalism":case"pp-semi-create-vandalism":typereason="IP・新規利用者による問題投稿の繰り返し";break;case"pp-move-vandalism":case"pp-move-120-500-vandalism":typereason="度重なる荒らし";break;case"pp-template":case"pp-120-500-template":case"pp-semi-template":typereason="[[WP:HRT|影響が特に大きいテンプレート]]";break;case"pp-sock-noticeboard":case"pp-semi-sock":case"pp-120-500-sock":case"pp-create-sock":case"pp-120-500-create-sock":case"pp-semi-create-sock":typereason="[[WP:SOCK|ソックパペット]]による問題投稿の繰り返し";break;case"pp-semi-blp":case"pp-120-500-blp":case"pp-create-blp":case"pp-120-500-create-blp":case"pp-semi-create-blp":typereason="プライバシー侵害の記述の繰り返し";break;case"pp-move-dispute":typereason="移動合戦";break;case"pp-create-spam":case"pp-120-500-create-spam":case"pp-semi-create-spam":typereason="度重なる宣伝";break;case"pp-move-indef":typereason="移動不要ページ";break;case"pp-create-salt":case"pp-120-500-create-salt":case"pp-semi-create-salt":typereason="削除されたページの改善なき再作成の繰り返し";break;default:typereason="";break}var reason=typereason;if(input.reason!==""){if(typereason!==""){reason+=" – "}reason+=input.reason}var rppparams={reason:reason,typename:typename,category:input.category,expiry:input.expiry};Morebits.SimpleWindow.setButtonsEnabled(false);Morebits.Status.init(form);var rppName="Wikipedia:保護依頼";Morebits.wiki.actionCompleted.redirect=rppparams.typename!=="保護解除"?"Project:保護依頼":"Project:保護解除依頼";Morebits.wiki.actionCompleted.notice="依頼完了、依頼ページへリダイレクトします。";var rppPage=new Morebits.wiki.Page(rppName,"ページの保護を依頼");rppPage.setFollowRedirect(true);rppPage.setCallbackParameters(rppparams);rppPage.load(Twinkle.protect.callbacks.fileRequest);if(input.req_template&&rppparams.typename!=="保護解除"){Twinkle.protect.callbacks.taggingPageInitial(tagparams)}break;default:alert("twinkleprotect: 不明な操作");break}};Twinkle.protect.protectReasonAnnotations=[];Twinkle.protect.callback.annotateProtectReason=function twinkleprotectCallbackAnnotateProtectReason(e){const form=e.target.form;const protectReason=form.protectReason.value.replace(new RegExp("(?:; )?"+mw.util.escapeRegExp(Twinkle.protect.protectReasonAnnotations.join(": "))),"");if(this.name==="protectReason_notes_rfpp"){if(this.checked){Twinkle.protect.protectReasonAnnotations.push(this.value);$(form.protectReason_notes_rfppRevid).parent().show()}else{Twinkle.protect.protectReasonAnnotations=[];form.protectReason_notes_rfppRevid.value="";$(form.protectReason_notes_rfppRevid).parent().hide()}}else if(this.name==="protectReason_notes_rfppRevid"){Twinkle.protect.protectReasonAnnotations=Twinkle.protect.protectReasonAnnotations.filter(el=>!el.includes("[[Special:Permalink")).filter(el=>!el.includes("[[WP:RFPP"));if(e.target.value.length){var permalink="[[Special:Permalink/"+e.target.value+"#"+Morebits.pageNameNorm+"(ノート_/_履歴_/_ログ_/_リンク元)|WP:RFPP]]による";Twinkle.protect.protectReasonAnnotations.push(permalink)}else{Twinkle.protect.protectReasonAnnotations.push("[[WP:RFPP]]による")}}if(!Twinkle.protect.protectReasonAnnotations.length){form.protectReason.value=protectReason}else{form.protectReason.value=(protectReason?protectReason+"; ":"")+Twinkle.protect.protectReasonAnnotations.join(": ")}};Twinkle.protect.callbacks={taggingPageInitial:function(tagparams){if(tagparams.tag==="noop"){Morebits.Status.info("Applying protection template","nothing to do");return}var protectedPage=new Morebits.wiki.Page(mw.config.get("wgPageName"),"ページにタグ付け");protectedPage.setCallbackParameters(tagparams);protectedPage.load(Twinkle.protect.callbacks.taggingPage)},taggingPage:function(protectedPage){const params=protectedPage.getCallbackParameters();let text=protectedPage.getPageText();let tag,summary;var oldtag_re=/(?:\/\*)?\s*(?:<noinclude>)?\s*\{\{\s*((?:pp|保護依頼)[^{}]*?|protected|(?:t|v|s|p-|usertalk-v|usertalk-s|sb|move)protected(?:2)?|protected template|privacy protection)\s*?\}\}\s*(?:<\/noinclude>)?\s*(?:\*\/)?\s*/gi;var re_result=oldtag_re.exec(text);if(re_result){if(params.tag==="none"||confirm("ページ上に保護関連テンプレート({{"+re_result[1]+"}}など)が見つかりました。\nOKをクリックして除去する、またはCancelをクリックしてそのままにしておきます。")){text=text.replace(oldtag_re,"")}}if(params.tag==="none"){summary="保護(依頼)テンプレートの除去"}else{tag=params.tag;if(params.reason){tag+="|reason="+params.reason}if(params.small){tag+="|small=yes"}if(/^\s*#(redirect|転送)/i.test(text)){if(!text.match(/{{(?:redr|this is a redirect|r(?:edirect)?(?:.?cat.*)?[ _]?sh)/i)){text=text.replace(/#(?:redirect|転送) ?(\[\[.*?\]\])(.*)/i,"#REDIRECT $1$2\n\n{{"+tag+"}}")}else{Morebits.Status.info("Redirect category shell present","nothing to do");return}}else{const needsTagToBeCommentedOut=["javascript","css","sanitized-css"].includes(protectedPage.getContentModel());if(needsTagToBeCommentedOut){if(params.noinclude){tag="/* <noinclude>{{"+tag+"}}</noinclude> */"}else{tag="/* {{"+tag+"}} */\n"}text=tag+text}else{if(params.noinclude){tag="<noinclude>{{"+tag+"}}</noinclude>";if(text.startsWith("==")){tag+="\n"}}else{tag="{{"+tag+"}}\n"}const wikipage=new Morebits.wikitext.Page(text);text=wikipage.insertAfterTemplates(tag,Twinkle.hatnoteRegex).getText()}}summary="{{"+params.tag+"}}を追加"}protectedPage.setEditSummary(summary+Twinkle.summaryAd);protectedPage.setWatchlist(Twinkle.getPref("watchPPTaggedPages"));protectedPage.setPageText(text);protectedPage.setCreateOption("nocreate");protectedPage.suppressProtectWarning();protectedPage.save()},fileRequest:function(rppPage){var rppPage2=new Morebits.wiki.Page("Wikipedia:保護解除依頼","依頼ページを読み込み中");rppPage2.load(()=>{var params=rppPage.getCallbackParameters();var text=rppPage.getPageText();var statusElement=rppPage.getStatusElement();var text2=rppPage2.getPageText();var increase=false;if(params.typename!=="保護解除"){increase=true}var rppRe=new RegExp("====\\s*(\\{\\{)?\\s*[pP]age\\s*\\|\\s*"+Morebits.string.escapeRegExp(Morebits.pageNameNorm)+"\\s*(\\}\\})?\\s*====","m");var newtag="\n==== {{Page|"+Morebits.pageNameNorm+"}} ====\n";var tag=new RegExp("^"+mw.util.escapeRegExp(newtag).replace(/\s+/g,"\\s*"),"m").test(text)||rppRe.exec(text);var tag2=new RegExp("^"+mw.util.escapeRegExp(newtag).replace(/\s+/g,"\\s*"),"m").test(text2)||rppRe.exec(text2);var rppLink=document.createElement("a");rppLink.setAttribute("href",mw.util.getUrl("Wikipedia:保護依頼"));rppLink.appendChild(document.createTextNode("Wikipedia:保護依頼"));var rppLink2=document.createElement("a");rppLink2.setAttribute("href",mw.util.getUrl("Wikipedia:保護解除依頼"));rppLink2.appendChild(document.createTextNode("Wikipedia:保護解除依頼"));if(tag&&increase){statusElement.error(["このページの保護依頼が",rppLink,"で見つかりました、中止します。"]);return}else if(tag2&&!increase){statusElement.error(["このページの保護解除依頼が",rppLink2,"で見つかりました、中止します。"]);return}newtag+=Morebits.string.formatReasonText(params.reason)+"--~~~~";if(increase){rppPage.parse(()=>{var sections=rppPage.getSections();var time=new Morebits.Date;var month=time.getMonth()+1;var date=time.getDate();var section=null;if(date<11){section=month+"月上旬(1日から10日まで)"}else if(date>20){section=month+"月下旬(21日から末日まで)"}else{section=month+"月中旬(11日から20日まで)"}sections=sections.filter(sections=>sections.level==="3").filter(sections=>sections.fromtitle==="Wikipedia:保護依頼").filter(sections=>sections.line===section);if(sections.length!==1){statusElement.error("報告先の節を正しく取得できませんでした。");return}statusElement.status("新規依頼を追加中...");rppPage.setPageSection(Number(sections[0].index));rppPage.setEditSummary("/* "+Morebits.pageNameNorm+" */ [[:"+Morebits.pageNameNorm+"]]の"+params.typename+"を依頼"+Twinkle.summaryAd);rppPage.setAppendText(newtag);rppPage.setCreateOption("recreate");rppPage.save(()=>{var watchPref=Twinkle.getPref("watchRequestedPages");var watch=watchPref!=="no"&&(watchPref!=="default"||!!parseInt(mw.user.options.get("watchdefault"),10));if(watch){var watch_query={action:"watch",titles:mw.config.get("wgPageName"),token:mw.user.tokens.get("watchToken")};if(Twinkle.protect.watched!==true&&watchPref!=="default"&&watchPref!=="yes"){watch_query.expiry=watchPref}new Morebits.wiki.Api("依頼したページをウォッチリストに追加中",watch_query).post()}})})}else{rppPage2.parse(()=>{var sections=rppPage2.getSections();var time=new Morebits.Date;var month=time.getMonth()+1;var date=time.getDate();var section=null;if(date<11){section=month+"月上旬(1日から10日まで)"}else if(date>20){section=month+"月下旬(21日から末日まで)"}else{section=month+"月中旬(11日から20日まで)"}sections=sections.filter(sections=>sections.level==="3").filter(sections=>sections.fromtitle==="Wikipedia:保護解除依頼").filter(sections=>sections.line===section);if(sections.length!==1){statusElement.error("報告先の節を正しく取得できませんでした。");return}text2+="\n"+newtag;statusElement.status("新規依頼を追加中...");rppPage2.setPageSection(Number(sections[0].index));rppPage2.setEditSummary("/* "+Morebits.pageNameNorm+" */ [[:"+Morebits.pageNameNorm+"]]の"+params.typename+"を依頼"+Twinkle.summaryAd);rppPage2.setAppendText(newtag);rppPage2.setCreateOption("recreate");rppPage2.save(()=>{var watchPref=Twinkle.getPref("watchRequestedPages");var watch=watchPref!=="no"&&(watchPref!=="default"||!!parseInt(mw.user.options.get("watchdefault"),10));if(watch){var watch_query={action:"watch",titles:mw.config.get("wgPageName"),token:mw.user.tokens.get("watchToken")};if(Twinkle.protect.watched!==true&&watchPref!=="default"&&watchPref!=="yes"){watch_query.expiry=watchPref}new Morebits.wiki.Api("依頼したページをウォッチリストに追加中",watch_query).post()}})})}})}};Twinkle.addInitCallback(Twinkle.protect,"protect")})();
// </nowiki>
1mremsdl5orj4p7xzsl5d9dyhhsj2jm
User:Syunsyunminmin/Twinkle/friendlytag.js
2
179402
769467
2026-10-21T13:58:12Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 3680108:
769467
javascript
text/javascript
/*
Twinkle: adapted for Japanese Wikipedia; upstream authors and Japanese adaptations are credited in UPSTREAM.md.
Upstream source: https://github.com/wikimedia-gadgets/twinkle/blob/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/src/modules/twinkletag.js
Upstream author history: https://github.com/wikimedia-gadgets/twinkle/commits/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/src/modules/twinkletag.js
License: CC BY-SA 3.0 Unported. See https://github.com/wikimedia-gadgets/twinkle/blob/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/LICENSE.txt
Japanese adaptation: Syunsyunminmin; includes later synchronization changes.
*/
// <nowiki>
(function() {
/*
****************************************
*** twinkletag.js: Tag module
****************************************
* Mode of invocation: Tab ("Tag")
* Active on: Existing articles and drafts; file pages with a corresponding file
* which is local (not on Commons); all redirects
*/
Twinkle.tag = function twinkletag() {
// redirect tagging (exclude category redirects, which are all soft redirects and so shouldn't be tagged with rcats)
if (Morebits.isPageRedirect() && mw.config.get('wgNamespaceNumber') !== 14) {
Twinkle.tag.mode = 'redirect';
Twinkle.addPortletLink(Twinkle.tag.callback, 'Tag', 'twinkle-tag', 'Tag redirect');
// file tagging
} else if (mw.config.get('wgNamespaceNumber') === 6 && !document.getElementById('mw-sharedupload') && document.getElementById('mw-imagepage-section-filehistory')) {
Twinkle.tag.mode = 'file';
Twinkle.addPortletLink(Twinkle.tag.callback, 'Tag', 'twinkle-tag', 'Add maintenance tags to file');
// article/draft article tagging
} else if ([0, 118].includes(mw.config.get('wgNamespaceNumber')) && mw.config.get('wgCurRevisionId')) {
Twinkle.tag.mode = 'article';
// Can't remove tags when not viewing current version
Twinkle.tag.canRemove = (mw.config.get('wgCurRevisionId') === mw.config.get('wgRevisionId')) &&
// Disabled on latest diff because the diff slider could be used to slide
// away from the latest diff without causing the script to reload
!mw.config.get('wgDiffNewId');
Twinkle.addPortletLink(Twinkle.tag.callback, 'Tag', 'twinkle-tag', 'Add or remove article maintenance tags');
}
};
Twinkle.tag.checkedTags = [];
Twinkle.tag.callback = function twinkletagCallback() {
const Window = new Morebits.SimpleWindow(630, Twinkle.tag.mode === 'article' ? 500 : 400);
Window.setScriptName('Twinkle');
// anyone got a good policy/guideline/info page/instructional page link??
Window.addFooterLink('Tag prefs', 'WP:TW/PREF#tag');
Window.addFooterLink('Twinkle help', 'WP:TW/DOC#tag');
Window.addFooterLink('Give feedback', 'WT:TW');
const form = new Morebits.QuickForm(Twinkle.tag.callback.evaluate);
// if page is unreviewed, add a checkbox to the form so that user can pick whether or not to review it
const isPatroller = mw.config.get('wgUserGroups').some((r) => ['patroller', 'sysop'].includes(r));
if (isPatroller) {
new mw.Api().get({
action: 'pagetriagelist',
format: 'json',
page_id: mw.config.get('wgArticleId')
}).then((response) => {
// Figure out whether the article is marked as reviewed in PageTriage.
// Recent articles will have a patrol_status that we can read.
// For articles that have been out of the new pages feed for awhile, pages[0] will be undefined.
const isReviewed = response.pagetriagelist.pages[0] ?
response.pagetriagelist.pages[0].patrol_status > 0 :
true;
// if article is not marked as reviewed, show the "mark as reviewed" check box
if (!isReviewed) {
// Quickform is probably already rendered. Instead of using form.append(), we need to make an element and then append it using JQuery.
const checkbox = new Morebits.QuickForm.Element({
type: 'checkbox',
list: [
{
label: 'Mark the page as patrolled/reviewed',
value: 'patrol',
name: 'patrol',
checked: Twinkle.getPref('markTaggedPagesAsPatrolled')
}
]
});
const html = checkbox.render();
$('.quickform').prepend(html);
}
});
}
form.append({
type: 'input',
label: 'Filter tag list:',
name: 'quickfilter',
size: '30',
event: function twinkletagquickfilter() {
// flush the DOM of all existing underline spans
$allCheckboxDivs.find('.search-hit').each((i, e) => {
const labelElement = e.parentElement;
// This would convert <label>Hello <span class=search-hit>wo</span>rld</label>
// to <label>Hello world</label>
labelElement.innerHTML = labelElement.textContent;
});
if (this.value) {
$allCheckboxDivs.hide();
$allHeaders.hide();
const searchString = this.value;
const searchRegex = new RegExp(mw.util.escapeRegExp(searchString), 'i');
$allCheckboxDivs.find('label').each(function() {
const labelText = this.textContent;
const searchHit = searchRegex.exec(labelText);
if (searchHit) {
const range = document.createRange();
const textnode = this.childNodes[0];
range.selectNodeContents(textnode);
range.setStart(textnode, searchHit.index);
range.setEnd(textnode, searchHit.index + searchString.length);
const underlineSpan = $('<span>').addClass('search-hit').css('text-decoration', 'underline')[0];
range.surroundContents(underlineSpan);
this.parentElement.style.display = 'block'; // show
}
});
} else {
$allCheckboxDivs.show();
$allHeaders.show();
}
}
});
switch (Twinkle.tag.mode) {
case 'article':
Window.setTitle('Article maintenance tagging');
// Build sorting and lookup object flatObject, which is always
// needed but also used to generate the alphabetical list
Twinkle.tag.article.flatObject = {};
Object.values(Twinkle.tag.article.tagList).forEach((group) => {
Object.values(group).forEach((subgroup) => {
if (Array.isArray(subgroup)) {
subgroup.forEach((item) => {
Twinkle.tag.article.flatObject[item.tag] = item;
});
} else {
Twinkle.tag.article.flatObject[subgroup.tag] = subgroup;
}
});
});
form.append({
type: 'select',
name: 'sortorder',
label: 'View this list:',
tooltip: 'You can change the default view order in your Twinkle preferences (WP:TWPREFS).',
event: Twinkle.tag.updateSortOrder,
list: [
{ type: 'option', value: 'cat', label: 'By categories', selected: Twinkle.getPref('tagArticleSortOrder') === 'cat' },
{ type: 'option', value: 'alpha', label: 'In alphabetical order', selected: Twinkle.getPref('tagArticleSortOrder') === 'alpha' }
]
});
if (!Twinkle.tag.canRemove) {
const divElement = document.createElement('div');
divElement.innerHTML = 'For removal of existing tags, please open Tag menu from the current version of article';
form.append({
type: 'div',
name: 'untagnotice',
label: divElement
});
}
form.append({
type: 'div',
id: 'tagWorkArea',
className: 'morebits-scrollbox'
});
form.append({
type: 'input',
label: 'Reason',
name: 'reason',
tooltip: 'Optional reason to be appended in edit summary. Recommended when removing tags.',
size: '60'
});
break;
case 'file':
Window.setTitle('File maintenance tagging');
$.each(Twinkle.tag.fileList, (groupName, group) => {
form.append({ type: 'header', label: groupName });
form.append({ type: 'checkbox', name: 'tags', list: group });
});
if (Twinkle.getPref('customFileTagList').length) {
form.append({ type: 'header', label: 'Custom tags' });
form.append({ type: 'checkbox', name: 'tags', list: Twinkle.getPref('customFileTagList') });
}
break;
case 'redirect':
Window.setTitle('Redirect tagging');
// If a tag has a restriction for this namespace or title, return true, so that we know not to display it in the list of check boxes.
var isRestricted = function(item) {
if (typeof item.restriction === 'undefined') {
return false;
}
const namespace = mw.config.get('wgNamespaceNumber');
switch (item.restriction) {
case 'insideMainspaceOnly':
if (namespace !== 0) {
return true;
}
break;
case 'outsideUserspaceOnly':
if (namespace === 2 || namespace === 3) {
return true;
}
break;
case 'insideTalkNamespaceOnly':
if (namespace % 2 !== 1 || namespace < 0) {
return true;
}
break;
case 'disambiguationPagesOnly':
if (!mw.config.get('wgPageName').endsWith('_(disambiguation)')) {
return true;
}
break;
default:
alert('Twinkle.tag: unknown restriction ' + item.restriction);
break;
}
return false;
};
// Generate the HTML form with the list of redirect tags that the user can choose to apply.
var i = 1;
$.each(Twinkle.tag.redirectList, (groupName, group) => {
form.append({ type: 'header', id: 'tagHeader' + i, label: groupName });
const subdiv = form.append({ type: 'div', id: 'tagSubdiv' + i++ });
$.each(group, (subgroupName, subgroup) => {
subdiv.append({ type: 'div', label: [Morebits.htmlNode('b', subgroupName)] });
subdiv.append({
type: 'checkbox',
name: 'tags',
list: subgroup
.filter((item) => !isRestricted(item))
.map((item) => ({ value: item.tag, label: '{{' + item.tag + '}}: ' + item.description, subgroup: item.subgroup }))
});
});
});
if (Twinkle.getPref('customRedirectTagList').length) {
form.append({ type: 'header', label: 'Custom tags' });
form.append({ type: 'checkbox', name: 'tags', list: Twinkle.getPref('customRedirectTagList') });
}
break;
default:
alert('Twinkle.tag: unknown mode ' + Twinkle.tag.mode);
break;
}
form.append({ type: 'submit', className: 'tw-tag-submit' });
const result = form.render();
Window.setContent(result);
Window.display();
// for quick filter:
$allCheckboxDivs = $(result).find('[name$=tags]').parent();
$allHeaders = $(result).find('h5, .quickformDescription');
result.quickfilter.focus(); // place cursor in the quick filter field as soon as window is opened
result.quickfilter.autocomplete = 'off'; // disable browser suggestions
result.quickfilter.addEventListener('keypress', (e) => {
if (e.keyCode === 13) { // prevent enter key from accidentally submitting the form
e.preventDefault();
return false;
}
});
if (Twinkle.tag.mode === 'article') {
Twinkle.tag.alreadyPresentTags = [];
if (Twinkle.tag.canRemove) {
// Look for existing maintenance tags in the lead section and put them in array
// All tags are HTML table elements that are direct children of .mw-parser-output
// (legacy parser) or the first <section> child (Parsoid),
// except when they are within {{multiple issues}}
// TODO: clean up after legacy parser is permanently replaced with Parsoid
const $oldParser = $('.mw-parser-output');
const $newParser = $('.mw-parser-output section[data-mw-section-id="0"]');
const $scanTarget = $newParser.length ? $newParser : $oldParser;
$scanTarget.children().each((i, e) => {
// break out on encountering the first heading, which means we are no
// longer in the lead section
if (e.classList.contains('mw-heading')) {
return false;
}
// The ability to remove tags depends on the template's {{ambox}} |name=
// parameter bearing the template's correct name (preferably) or a name that at
// least redirects to the actual name
// All tags have their first class name as "box-" + template name
if (e.className.indexOf('box-') === 0) {
if (e.classList[0] === 'box-Multiple_issues') {
$(e).find('.ambox').each((idx, e) => {
if (e.classList[0].indexOf('box-') === 0) {
const tag = e.classList[0].slice('box-'.length).replace(/_/g, ' ');
Twinkle.tag.alreadyPresentTags.push(tag);
}
});
return true; // continue
}
const tag = e.classList[0].slice('box-'.length).replace(/_/g, ' ');
Twinkle.tag.alreadyPresentTags.push(tag);
}
});
// {{Uncategorized}} and {{Improve categories}} are usually placed at the end
if ($('.box-Uncategorized').length) {
Twinkle.tag.alreadyPresentTags.push('Uncategorized');
}
if ($('.box-Improve_categories').length) {
Twinkle.tag.alreadyPresentTags.push('Improve categories');
}
}
// Add status text node after Submit button
const statusNode = document.createElement('small');
statusNode.id = 'tw-tag-status';
Twinkle.tag.status = {
// initial state; defined like this because these need to be available for reference
// in the click event handler
numAdded: 0,
numRemoved: 0
};
$('button.tw-tag-submit').after(statusNode);
// fake a change event on the sort dropdown, to initialize the tag list
const evt = document.createEvent('Event');
evt.initEvent('change', true, true);
result.sortorder.dispatchEvent(evt);
} else {
// Redirects and files: Add a link to each template's description page
Morebits.QuickForm.getElements(result, 'tags').forEach(generateLinks);
}
};
// $allCheckboxDivs and $allHeaders are defined globally, rather than in the
// quickfilter event function, to avoid having to recompute them on every keydown
let $allCheckboxDivs, $allHeaders;
Twinkle.tag.updateSortOrder = function(e) {
const form = e.target.form;
const sortorder = e.target.value;
Twinkle.tag.checkedTags = form.getChecked('tags');
const container = new Morebits.QuickForm.Element({ type: 'fragment' });
// function to generate a checkbox, with appropriate subgroup if needed
const makeCheckbox = function(item) {
const tag = item.tag, description = item.description;
const checkbox = { value: tag, label: '{{' + tag + '}}: ' + description };
if (Twinkle.tag.checkedTags.includes(tag)) {
checkbox.checked = true;
}
checkbox.subgroup = item.subgroup;
return checkbox;
};
const makeCheckboxesForAlreadyPresentTags = function() {
container.append({ type: 'header', id: 'tagHeader0', label: 'Tags already present' });
const subdiv = container.append({ type: 'div', id: 'tagSubdiv0' });
const checkboxes = [];
const unCheckedTags = e.target.form.getUnchecked('existingTags');
Twinkle.tag.alreadyPresentTags.forEach((tag) => {
const checkbox =
{
value: tag,
label: '{{' + tag + '}}' + (Twinkle.tag.article.flatObject[tag] ? ': ' + Twinkle.tag.article.flatObject[tag].description : ''),
checked: !unCheckedTags.includes(tag),
style: 'font-style: italic'
};
checkboxes.push(checkbox);
});
subdiv.append({
type: 'checkbox',
name: 'existingTags',
list: checkboxes
});
};
if (sortorder === 'cat') { // categorical sort order
// function to iterate through the tags and create a checkbox for each one
const doCategoryCheckboxes = function(subdiv, subgroup) {
const checkboxes = [];
$.each(subgroup, (k, item) => {
if (!Twinkle.tag.alreadyPresentTags.includes(item.tag)) {
checkboxes.push(makeCheckbox(item));
}
});
subdiv.append({
type: 'checkbox',
name: 'tags',
list: checkboxes
});
};
if (Twinkle.tag.alreadyPresentTags.length > 0) {
makeCheckboxesForAlreadyPresentTags();
}
let i = 1;
// go through each category and sub-category and append lists of checkboxes
$.each(Twinkle.tag.article.tagList, (groupName, group) => {
container.append({ type: 'header', id: 'tagHeader' + i, label: groupName });
const subdiv = container.append({ type: 'div', id: 'tagSubdiv' + i++ });
if (Array.isArray(group)) {
doCategoryCheckboxes(subdiv, group);
} else {
$.each(group, (subgroupName, subgroup) => {
subdiv.append({ type: 'div', label: [Morebits.htmlNode('b', subgroupName)] });
doCategoryCheckboxes(subdiv, subgroup);
});
}
});
} else { // alphabetical sort order
if (Twinkle.tag.alreadyPresentTags.length > 0) {
makeCheckboxesForAlreadyPresentTags();
container.append({ type: 'header', id: 'tagHeader1', label: 'Available tags' });
}
// Avoid repeatedly resorting
Twinkle.tag.article.alphabeticalList = Twinkle.tag.article.alphabeticalList || Object.keys(Twinkle.tag.article.flatObject).sort();
const checkboxes = [];
Twinkle.tag.article.alphabeticalList.forEach((tag) => {
if (!Twinkle.tag.alreadyPresentTags.includes(tag)) {
checkboxes.push(makeCheckbox(Twinkle.tag.article.flatObject[tag]));
}
});
container.append({
type: 'checkbox',
name: 'tags',
list: checkboxes
});
}
// append any custom tags
if (Twinkle.getPref('customTagList').length) {
container.append({ type: 'header', label: 'Custom tags' });
container.append({ type: 'checkbox', name: 'tags',
list: Twinkle.getPref('customTagList').map((el) => {
el.checked = Twinkle.tag.checkedTags.includes(el.value);
return el;
})
});
}
const $workarea = $(form).find('#tagWorkArea');
const rendered = container.render();
$workarea.empty().append(rendered);
// for quick filter:
$allCheckboxDivs = $workarea.find('[name=tags], [name=existingTags]').parent();
$allHeaders = $workarea.find('h5, .quickformDescription');
form.quickfilter.value = ''; // clear search, because the search results are not preserved over mode change
form.quickfilter.focus();
// style adjustments
$workarea.find('h5').css({ 'font-size': '110%' });
$workarea.find('h5:not(:first-child)').css({ 'margin-top': '1em' });
$workarea.find('div').filter(':has(span.quickformDescription)').css({ 'margin-top': '0.4em' });
Morebits.QuickForm.getElements(form, 'existingTags').forEach(generateLinks);
Morebits.QuickForm.getElements(form, 'tags').forEach(generateLinks);
// tally tags added/removed, update statusNode text
const statusNode = document.getElementById('tw-tag-status');
$('[name=tags], [name=existingTags]').on('click', function() {
if (this.name === 'tags') {
Twinkle.tag.status.numAdded += this.checked ? 1 : -1;
} else if (this.name === 'existingTags') {
Twinkle.tag.status.numRemoved += this.checked ? -1 : 1;
}
const firstPart = 'Adding ' + Twinkle.tag.status.numAdded + ' tag' + (Twinkle.tag.status.numAdded > 1 ? 's' : '');
const secondPart = 'Removing ' + Twinkle.tag.status.numRemoved + ' tag' + (Twinkle.tag.status.numRemoved > 1 ? 's' : '');
statusNode.textContent =
(Twinkle.tag.status.numAdded ? ' ' + firstPart : '') +
(Twinkle.tag.status.numRemoved ? (Twinkle.tag.status.numAdded ? '; ' : ' ') + secondPart : '');
});
};
/**
* Adds a link to each template's description page
*
* @param {Morebits.QuickForm.Element} checkbox associated with the template
*/
var generateLinks = function(checkbox) {
const link = Morebits.htmlNode('a', '>');
link.setAttribute('class', 'tag-template-link');
const tagname = checkbox.values;
link.setAttribute('href', mw.util.getUrl(
(!tagname.includes(':') ? 'Template:' : '') +
(!tagname.includes('|') ? tagname : tagname.slice(0, tagname.indexOf('|')))
));
link.setAttribute('target', '_blank');
$(checkbox).parent().append(['\u00A0', link]);
};
// Tags for ARTICLES start here
Twinkle.tag.article = {};
// Shared across {{Rough translation}} and {{Not English}}
const translationSubgroups = [
{
name: 'translationLanguage',
parameter: '1',
type: 'input',
label: 'Language of article (if known):',
tooltip: 'Consider looking at [[WP:LRC]] for help. If listing the article at PNT, please try to avoid leaving this box blank, unless you are completely unsure.'
}
].concat(mw.config.get('wgNamespaceNumber') === 0 ? [
{
type: 'checkbox',
list: [{
name: 'translationPostAtPNT',
label: 'List this article at Wikipedia:Pages needing translation into English (PNT)',
checked: true
}]
},
{
name: 'translationComments',
type: 'textarea',
label: 'Additional comments to post at PNT',
tooltip: 'Optional, and only relevant if "List this article ..." above is checked.'
}
] : []);
// Tags arranged by category; will be used to generate the alphabetical list,
// but tags should be in alphabetical order within the categories
// excludeMI: true indicate a tag that *does not* work inside {{multiple issues}}
// Add new categories with discretion - the list is long enough as is!
Twinkle.tag.article.tagList = {
'Cleanup and maintenance tags': {
'General cleanup': [
{
tag: 'Cleanup', description: 'requires cleanup',
subgroup: {
name: 'cleanup',
parameter: 'reason',
type: 'input',
label: 'Specific reason why cleanup is needed:',
tooltip: 'Required.',
size: 35,
required: true
}
}, // has a subgroup with text input
{
tag: 'Cleanup rewrite',
description: "needs to be rewritten entirely to comply with Wikipedia's quality standards"
},
{
tag: 'Copy edit',
description: 'requires copy editing for grammar, style, cohesion, tone, or spelling',
subgroup: {
name: 'copyEdit',
parameter: 'for',
type: 'input',
label: '"This article may require copy editing for..."',
tooltip: 'e.g. "consistent spelling". Optional.',
size: 35
}
} // has a subgroup with text input
],
'Potentially unwanted content': [
{
tag: 'Close paraphrasing',
description: 'contains close paraphrasing of a non-free copyrighted source',
subgroup: {
name: 'closeParaphrasing',
parameter: 'source',
type: 'input',
label: 'Source:',
tooltip: 'Source that has been closely paraphrased'
}
},
{
tag: 'Copypaste',
description: 'appears to have been copied and pasted from another location',
excludeMI: true,
subgroup: {
name: 'copypaste',
parameter: 'url',
type: 'input',
label: 'Source URL:',
tooltip: 'If known.',
size: 50
}
}, // has a subgroup with text input
{
tag: 'AI-generated',
description: 'content appears to be generated by a large language model',
subgroup: [
{
name: 'llmReason',
parameter: 'reason',
type: 'input',
label: 'Reason:',
tooltip: 'A short explanation to be added to the tag. Optional but recommended to fill in either Reason or Talk discussion.'
},
{
name: 'llmTalk',
parameter: 'talk',
type: 'input',
label: 'Talk discussion:',
tooltip: 'Name of the section of this article\'s talk page where the issue is being discussed. Do not give a link, just the name of the section. Optional but recommended to fill in either Reason or Talk discussion.'
}
]
},
{ tag: 'External links', description: 'external links may not follow content policies or guidelines' },
{ tag: 'Non-free', description: 'may contain excessive or improper use of copyrighted materials' }
],
'Structure, formatting, and lead section': [
{ tag: 'Cleanup reorganize', description: "needs reorganization to comply with Wikipedia's layout guidelines" },
{ tag: 'Lead missing', description: 'no lead section' },
{ tag: 'Lead rewrite', description: 'lead section needs to be rewritten to comply with guidelines' },
{ tag: 'Lead too long', description: 'lead section is too long for the length of the article' },
{ tag: 'Lead too short', description: 'lead section is too short and should be expanded to summarize key points' },
{ tag: 'Sections', description: 'needs to be divided into sections by topic' },
{ tag: 'Too many sections', description: 'too many section headers dividing up content, should be condensed' },
{ tag: 'Very long', description: 'too long to read and navigate comfortably' }
],
'Fiction-related cleanup': [
{ tag: 'All plot', description: 'almost entirely a plot summary' },
{ tag: 'Fiction', description: 'fails to distinguish between fact and fiction' },
{ tag: 'In-universe', description: 'subject is fictional and needs rewriting to provide a non-fictional perspective' },
{ tag: 'Long plot', description: 'plot summary is too long or excessively detailed' },
{ tag: 'More plot', description: 'plot summary is too short' },
{ tag: 'No plot', description: 'needs a plot summary' }
]
},
'General content issues': {
'Importance and notability': [
{ tag: 'Notability', description: 'subject may not meet the general notability guideline',
subgroup: {
name: 'notability',
parameter: '1',
type: 'select',
list: [
{ label: "{{notability}}: article's subject may not meet the general notability guideline", value: '' },
{ label: '{{notability|Academics}}: notability guideline for academics', value: 'Academics' },
{ label: '{{notability|Astro}}: notability guideline for astronomical objects', value: 'Astro' },
{ label: '{{notability|Biographies}}: notability guideline for biographies', value: 'Biographies' },
{ label: '{{notability|Books}}: notability guideline for books', value: 'Books' },
{ label: '{{notability|Companies}}: notability guideline for companies', value: 'Companies' },
{ label: '{{notability|Events}}: notability guideline for events', value: 'Events' },
{ label: '{{notability|Films}}: notability guideline for films', value: 'Films' },
{ label: '{{notability|Geographic}}: notability guideline for geographic features', value: 'Geographic' },
{ label: '{{notability|Lists}}: notability guideline for stand-alone lists', value: 'Lists' },
{ label: '{{notability|Music}}: notability guideline for music', value: 'Music' },
{ label: '{{notability|Neologisms}}: notability guideline for neologisms', value: 'Neologisms' },
{ label: '{{notability|Numbers}}: notability guideline for numbers', value: 'Numbers' },
{ label: '{{notability|Organizations}}: notability guideline for organizations', value: 'Organizations' },
{ label: '{{notability|Products}}: notability guideline for products and services', value: 'Products' },
{ label: '{{notability|Sports}}: notability guideline for sports and athletics', value: 'Sports' },
{ label: '{{notability|Television}}: notability guideline for television shows', value: 'Television' },
{ label: '{{notability|Web}}: notability guideline for web content', value: 'Web' }
]
}
}
],
'Style of writing': [
{ tag: 'Cleanup press release', description: 'reads like a press release or news article',
subgroup: {
type: 'hidden',
name: 'cleanupPR1',
parameter: '1',
value: 'article'
}
},
{ tag: 'Cleanup tense', description: 'does not follow guidelines on use of different tenses.' },
{ tag: 'Essay-like', description: 'written like a personal reflection, personal essay, or argumentative essay' },
{ tag: 'Fanpov', description: "written from a fan's point of view" },
{ tag: 'Inappropriate person', description: 'uses first-person or second-person inappropiately' },
{ tag: 'How-to', description: 'written like a manual or guidebook' },
{ tag: 'Over-quotation', description: 'too many or too-lengthy quotations for an encyclopedic entry' },
{ tag: 'Promotional', description: 'contains promotional content or is written like an advertisement' },
{ tag: 'Prose', description: 'written in a list format but may read better as prose' },
{ tag: 'Resume-like', description: 'written like a resume' },
{ tag: 'Technical', description: 'too technical for most readers to understand' },
{ tag: 'Tone', description: 'tone or style may not reflect the encyclopedic tone used on Wikipedia' }
],
'Sense (or lack thereof)': [
{ tag: 'Confusing', description: 'confusing or unclear' },
{ tag: 'Unfocused', description: 'lacks focus or is about more than one topic' }
],
'Information and detail': [
{ tag: 'Context', description: 'insufficient context for those unfamiliar with the subject' },
{ tag: 'Excessive examples', description: 'may contain indiscriminate, excessive, or irrelevant examples' },
{ tag: 'Expert needed', description: 'needs attention from an expert on the subject',
subgroup: [
{
name: 'expertNeeded',
parameter: '1',
type: 'input',
label: 'Name of relevant WikiProject:',
tooltip: 'Optionally, enter the name of a WikiProject which might be able to help recruit an expert. Don\'t include the "WikiProject" prefix.'
},
{
name: 'expertNeededReason',
parameter: 'reason',
type: 'input',
label: 'Reason:',
tooltip: 'Short explanation describing the issue. Either Reason or Talk link is required.'
},
{
name: 'expertNeededTalk',
parameter: 'talk',
type: 'input',
label: 'Talk discussion:',
tooltip: 'Name of the section of this article\'s talk page where the issue is being discussed. Do not give a link, just the name of the section. Either Reason or Talk link is required.'
}
]
},
{ tag: 'Overly detailed', description: 'excessive amount of intricate detail' },
{ tag: 'Undue weight', description: 'lends undue weight to certain ideas, incidents, or controversies' }
],
Timeliness: [
{ tag: 'Current', description: 'documents a current event', excludeMI: true }, // Works but not intended for use in MI
{ tag: 'Current related', description: 'documents a topic affected by a current event', excludeMI: true }, // Works but not intended for use in MI
{ tag: 'Update', description: 'needs additional up-to-date information added',
subgroup: [
{
name: 'updatePart',
parameter: 'part',
type: 'input',
label: 'What part of the article:',
tooltip: 'Part that needs updating',
size: '45'
},
{
name: 'updateReason',
parameter: 'reason',
type: 'input',
label: 'Reason:',
tooltip: 'Explanation why the article is out of date',
size: '55'
}
]
}
],
'Neutrality, bias, and factual accuracy': [
{ tag: 'Autobiography', description: 'autobiography and may not be written neutrally' },
{ tag: 'COI', description: 'creator or major contributor may have a conflict of interest', subgroup: mw.config.get('wgNamespaceNumber') === 0 ? {
name: 'coiReason',
type: 'textarea',
label: 'Explanation for COI tag (will be posted on this article\'s talk page):',
tooltip: 'Optional, but strongly recommended. Leave blank if not wanted.'
} : [] },
{ tag: 'Disputed', description: 'questionable factual accuracy' },
{ tag: 'Fringe theories', description: 'presents fringe theories as mainstream views' },
{ tag: 'Globalize', description: 'may not represent a worldwide view of the subject',
subgroup: [
{
type: 'hidden',
name: 'globalize1',
parameter: '1',
value: 'article'
}, {
name: 'globalizeRegion',
parameter: '2',
type: 'input',
label: 'Over-represented country or region'
}
]
},
{ tag: 'Hoax', description: 'may partially or completely be a hoax' },
{ tag: 'Paid contributions', description: 'contains paid contributions, and may therefore require cleanup' },
{ tag: 'Peacock', description: 'contains wording that promotes the subject in a subjective manner without adding information' },
{ tag: 'POV', description: 'does not maintain a neutral point of view' },
{ tag: 'Recentism', description: 'slanted towards recent events' },
{ tag: 'Too few opinions', description: 'may not include all significant viewpoints' },
{ tag: 'Undisclosed paid', description: 'may have been created or edited in return for undisclosed payments' },
{ tag: 'Weasel', description: 'neutrality or verifiability is compromised by the use of weasel words' }
],
'Verifiability and sources': [
{ tag: 'BLP no footnotes', description: 'BLP that lacks inline citations'},
{ tag: 'BLP one source', description: 'BLP that relies largely or entirely on a single source' },
{ tag: 'BLP sources', description: 'BLP that needs additional references or sources for verification' },
{ tag: 'BLP unreferenced', description: 'BLP does not cite any sources at all (use BLP PROD instead for new articles)' },
{ tag: 'Full citations needed', description: 'needs more complete citations for verification' },
{ tag: 'More citations needed', description: 'needs additional references or sources for verification' },
{ tag: 'No footnotes', description: 'has references, but lacks inline citations' },
{ tag: 'No significant coverage', description: 'does not cite any sources containing significant coverage' },
{ tag: 'No significant coverage (sports)', description: 'sports biography that does not cite any sources containing significant coverage' },
{ tag: 'One source', description: 'relies largely or entirely on a single source' },
{ tag: 'Only primary sources', description: 'relies only on references to primary sources, and needs secondary sources' },
{ tag: 'Original research', description: 'contains original research' },
{ tag: 'Primary sources', description: 'relies too much on references to primary sources, and needs secondary sources' },
{ tag: 'Self-published', description: 'contains excessive or inappropriate references to self-published sources' },
{ tag: 'Sources exist', description: 'notable topic, sources are available that could be added to article' },
{ tag: 'Third-party', description: 'relies too heavily on sources too closely associated with the subject' },
{ tag: 'Unreferenced', description: 'does not cite any sources at all' },
{ tag: 'Unreliable sources', description: 'some references may not be reliable' },
{ tag: 'User-generated', description: 'contains many references to user-generated (self-published) content'}
]
},
'Specific content issues': {
Accessibility: [
{ tag: 'Cleanup colors', description: 'uses color as only way to convey information' },
{ tag: 'Overcoloured', description: 'overuses color' },
{ tag: 'Dark mode problems', description: 'has problems when viewed in dark mode' }
],
Language: [
{ tag: 'Not English', description: 'written in a language other than English and needs translation',
excludeMI: true,
subgroup: translationSubgroups.slice(0, 1).concat([{
type: 'checkbox',
list: [
{
name: 'translationNotify',
label: 'Notify article creator',
checked: true,
tooltip: "Places {{uw-notenglish}} on the creator's talk page."
}
]
}]).concat(translationSubgroups.slice(1))
},
{ tag: 'Rough translation', description: 'poor translation from another language', excludeMI: true,
subgroup: translationSubgroups
},
{ tag: 'Expand language', description: 'should be expanded with text translated from a foreign-language article',
excludeMI: true,
subgroup: [{
type: 'hidden',
name: 'expandLangTopic',
parameter: 'topic',
value: '',
required: true // force empty topic param in output
}, {
name: 'expandLanguageLangCode',
parameter: 'langcode',
type: 'input',
label: 'Language code:',
tooltip: 'Language code of the language from which article is to be expanded from',
required: true
}, {
name: 'expandLanguageArticle',
parameter: 'otherarticle',
type: 'input',
label: 'Name of article:',
tooltip: 'Name of article to be expanded from, without the interwiki prefix'
}]
}
],
Links: [
{ tag: 'Dead end', description: 'article has no links to other articles' },
{ tag: 'Orphan', description: 'linked to from no other articles' },
{ tag: 'Overlinked', description: 'too many duplicate and/or irrelevant links to other articles' },
{ tag: 'Underlinked', description: 'needs more wikilinks to other articles' }
],
'Referencing technique': [
{ tag: 'Citation style', description: 'unclear or inconsistent citation style' },
{ tag: 'Cleanup bare URLs', description: 'uses bare URLs for references, which are prone to link rot' },
{ tag: 'More footnotes needed', description: 'has some references, but insufficient inline citations' },
{ tag: 'Parenthetical referencing', description: 'uses parenthetical referencing, which is deprecated on Wikipedia' }
],
Categories: [
{ tag: 'Improve categories', description: 'needs additional or more specific categories', excludeMI: true },
{ tag: 'Uncategorized', description: 'not added to any categories', excludeMI: true }
]
},
Merging: [
{
tag: 'History merge',
description: 'another page should be history merged into this one',
excludeMI: true,
subgroup: [
{
name: 'histmergeOriginalPage',
parameter: 'originalpage',
type: 'input',
label: 'Other article:',
tooltip: 'Name of the page that should be merged into this one (required).',
required: true
},
{
name: 'histmergeReason',
parameter: 'reason',
type: 'input',
label: 'Reason:',
tooltip: 'Short explanation describing the reason a history merge is needed. Should probably begin with "because" and end with a period.'
},
{
name: 'histmergeSysopDetails',
parameter: 'details',
type: 'input',
label: 'Extra details:',
tooltip: 'For complex cases, provide extra instructions for the reviewing administrator.'
}
]
}
],
Splitting: [
{ tag: 'Split', description: 'should be split into multiple pages' },
{ tag: 'Split dab', description: 'disambiguation page should be split into multiple pages' }
],
Informational: [
{ tag: 'GOCEinuse', description: 'currently undergoing a major copy edit by the Guild of Copy Editors', excludeMI: true },
{ tag: 'In use', description: 'undergoing a major edit for a short while', excludeMI: true },
{ tag: 'Under construction', description: 'in the process of an expansion or major restructuring', excludeMI: true }
]
};
// Tags for REDIRECTS start here
// Not by policy, but the list roughly approximates items with >500
// transclusions from Template:R template index
Twinkle.tag.redirectList = {
'Grammar, punctuation, and spelling': {
Abbreviation: [
{ tag: 'R from acronym', description: 'redirect from an acronym (e.g. POTUS) to its expanded form', restriction: 'insideMainspaceOnly' },
{ tag: 'R from airport code', description: 'redirect from an airport\'s IATA or ICAO code to that airport\'s article', restriction: 'insideMainspaceOnly' },
{ tag: 'R from airline code', description: 'redirect from an airline\'s IATA or ICAO code to that airline\'s article', restriction: 'insideMainspaceOnly' },
{ tag: 'R from initialism', description: 'redirect from an initialism (e.g. AGF) to its expanded form', restriction: 'insideMainspaceOnly' },
{ tag: 'R from MathSciNet abbreviation', description: 'redirect from MathSciNet publication title abbreviation to the unabbreviated title', restriction: 'insideMainspaceOnly' },
{ tag: 'R from NLM abbreviation', description: 'redirect from a NLM publication title abbreviation to the unabbreviated title', restriction: 'insideMainspaceOnly' }
],
Capitalisation: [
{ tag: 'R from CamelCase', description: 'redirect from a CamelCase title' },
{ tag: 'R from other capitalisation', description: 'redirect from a title with another method of capitalisation', restriction: 'insideMainspaceOnly' },
{ tag: 'R from miscapitalisation', description: 'redirect from a capitalisation error' }
],
'Grammar & punctuation': [
{ tag: 'R from modification', description: 'redirect from a modification of the target\'s title, such as with words rearranged' },
{ tag: 'R from plural', description: 'redirect from a plural word to the singular equivalent', restriction: 'insideMainspaceOnly' },
{ tag: 'R to plural', description: 'redirect from a singular noun to its plural form', restriction: 'insideMainspaceOnly' }
],
'Parts of speech': [
{ tag: 'R from verb', description: 'redirect from an English-language verb or verb phrase', restriction: 'insideMainspaceOnly' },
{ tag: 'R from adjective', description: 'redirect from an adjective (word or phrase that describes a noun)', restriction: 'insideMainspaceOnly' }
],
Spelling: [
{ tag: 'R from alternative spelling', description: 'redirect from a title with a different spelling' },
{ tag: 'R from alternative transliteration', description: 'redirect from an alternative English transliteration to a more common variation' },
{ tag: 'R from ASCII-only', description: 'redirect from a title in only basic ASCII to the formal title, with differences that are not diacritical marks or ligatures' },
{ tag: 'R to ASCII-only', description: 'redirect to a title in only basic ASCII from the formal title, with differences that are not diacritical marks or ligatures' },
{ tag: 'R from diacritic', description: 'redirect from a page name that has diacritical marks (accents, umlauts, etc.)' },
{ tag: 'R to diacritic', description: 'redirect to the article title with diacritical marks (accents, umlauts, etc.)' },
{ tag: 'R from misspelling', description: 'redirect from a misspelling or typographical error' }
]
},
'Alternative names': {
General: [
{
tag: 'R from alternative language',
description: 'redirect from or to a title in another language',
subgroup: [
{
name: 'altLangFrom',
type: 'input',
label: 'From language (two-letter code):',
tooltip: 'Enter the two-letter code of the language the redirect name is in; such as en for English, de for German'
},
{
name: 'altLangTo',
type: 'input',
label: 'To language (two-letter code):',
tooltip: 'Enter the two-letter code of the language the target name is in; such as en for English, de for German'
},
{
name: 'altLangInfo',
type: 'div',
label: $.parseHTML('<p>For a list of language codes, see <a href="/wiki/Wp:Template_messages/Redirect_language_codes">Wikipedia:Template messages/Redirect language codes</a></p>')
}
]
},
{ tag: 'R from alternative name', description: 'redirect from a title that is another name, a pseudonym, a nickname, or a synonym' },
{ tag: 'R from ambiguous sort name', description: 'redirect from an ambiguous sort name to a page or list that disambiguates it' },
{ tag: 'R from former name', description: 'redirect from a former or historic name or a working title', restriction: 'insideMainspaceOnly' },
{ tag: 'R from incomplete name', description: 'R from incomplete name' },
{ tag: 'R from incorrect name', description: 'redirect from an erroneus name that is unsuitable as a title' },
{ tag: 'R from less specific name', description: 'redirect from a less specific title to a more specific, less general one' },
{ tag: 'R from long name', description: 'redirect from a more complete title' },
{ tag: 'R from more specific name', description: 'redirect from a more specific title to a less specific, more general one' },
{ tag: 'R from non-neutral name', description: 'redirect from a title that contains a non-neutral, pejorative, controversial, or offensive word, phrase, or name' },
{ tag: 'R from short name', description: 'redirect from a title that is a shortened form of a person\'s full name, a book title, or other more complete title' },
{ tag: 'R from sort name', description: 'redirect from the target\'s sort name, such as beginning with their surname rather than given name', restriction: 'insideMainspaceOnly' },
{ tag: 'R from synonym', description: 'redirect from a semantic synonym of the target page title' }
],
People: [
{ tag: 'R from birth name', description: 'redirect from a person\'s birth name to a more common name', restriction: 'insideMainspaceOnly' },
{ tag: 'R from given name', description: 'redirect from a person\'s given name', restriction: 'insideMainspaceOnly' },
{ tag: 'R from married name', description: 'redirect from a person\'s married name to a more common name', restriction: 'insideMainspaceOnly' },
{ tag: 'R from name with title', description: 'redirect from a person\'s name preceded or followed by a title to the name with no title or with the title in parentheses', restriction: 'insideMainspaceOnly' },
{ tag: 'R from person', description: 'redirect from a person or persons to a related article', restriction: 'insideMainspaceOnly' },
{ tag: 'R from personal name', description: 'redirect from an individual\'s personal name to an article titled with their professional or other better known moniker', restriction: 'insideMainspaceOnly' },
{ tag: 'R from pseudonym', description: 'redirect from a pseudonym', restriction: 'insideMainspaceOnly' },
{ tag: 'R from surname', description: 'redirect from a title that is a surname', restriction: 'insideMainspaceOnly' }
],
Technical: [
{ tag: 'R from drug trade name', description: 'redirect from (or to) the trade name of a drug to (or from) the international nonproprietary name (INN)' },
{ tag: 'R from filename', description: 'redirect from a title that is a filename of the target', restriction: 'insideMainspaceOnly' },
{ tag: 'R from molecular formula', description: 'redirect from a molecular/chemical formula to its technical or trivial name' },
{ tag: 'R from gene symbol', description: 'redirect from a Human Genome Organisation (HUGO) symbol for a gene to an article about the gene', restriction: 'insideMainspaceOnly' }
],
Organisms: [
{ tag: 'R to scientific name', description: 'redirect from the common name to the scientific name', restriction: 'insideMainspaceOnly' },
{ tag: 'R from scientific name', description: 'redirect from the scientific name to the common name', restriction: 'insideMainspaceOnly' },
{ tag: 'R from alternative scientific name', description: 'redirect from an alternative scientific name to the accepted scientific name', restriction: 'insideMainspaceOnly' },
{ tag: 'R from scientific abbreviation', description: 'redirect from a scientific abbreviation', restriction: 'insideMainspaceOnly' },
{ tag: 'R to monotypic taxon', description: 'redirect from the only lower-ranking member of a monotypic taxon to its monotypic taxon', restriction: 'insideMainspaceOnly' },
{ tag: 'R from monotypic taxon', description: 'redirect from a monotypic taxon to its only lower-ranking member', restriction: 'insideMainspaceOnly' },
{ tag: 'R taxon with possibilities', description: 'redirect from a title related to a living organism that potentially could be expanded into an article', restriction: 'insideMainspaceOnly' }
],
Geography: [
{ tag: 'R from name and country', description: 'redirect from the specific name to the briefer name', restriction: 'insideMainspaceOnly' },
{ tag: 'R from more specific geographic name', description: 'redirect from a geographic location that includes extraneous identifiers such as the county or region of a city', restriction: 'insideMainspaceOnly' }
]
},
'Navigation aids': {
Navigation: [
{ tag: 'R to anchor', description: 'redirect from a topic that does not have its own page to an anchored part of a page on the subject' },
{
tag: 'R avoided double redirect',
description: 'redirect from an alternative title for another redirect',
subgroup: {
name: 'doubleRedirectTarget',
type: 'input',
label: 'Redirect target name',
tooltip: 'Enter the page this redirect would target if the page wasn\'t also a redirect'
}
},
{ tag: 'R from file metadata link', description: 'redirect of a wikilink created from EXIF, XMP, or other information (i.e. the "metadata" section on some image description pages)', restriction: 'insideMainspaceOnly' },
{ tag: 'R to list entry', description: 'redirect to a list which contains brief descriptions of subjects not notable enough to have separate articles', restriction: 'insideMainspaceOnly' },
{ tag: 'R mentioned in hatnote', description: 'redirect from a title that is mentioned in a hatnote at the redirect target' },
{ tag: 'R to section', description: 'similar to {{R to list entry}}, but when list is organized in sections, such as list of characters in a fictional universe' },
{ tag: 'R from shortcut', description: 'redirect from a Wikipedia shortcut' },
{ tag: 'R to subpage', description: 'redirect to a subpage' }
],
Disambiguation: [
{ tag: 'R from ambiguous term', description: 'redirect from an ambiguous page name to a page that disambiguates it. This template should never appear on a page that has "(disambiguation)" in its title, use R to disambiguation page instead' },
{ tag: 'R to disambiguation page', description: 'redirect to a disambiguation page', restriction: 'disambiguationPagesOnly' },
{ tag: 'R from incomplete disambiguation', description: 'redirect from a page name that is too ambiguous to be the title of an article and should redirect to an appropriate disambiguation page' },
{ tag: 'R from incorrect disambiguation', description: 'redirect from a page name with incorrect disambiguation due to an error or previous editorial misconception' },
{ tag: 'R from other disambiguation', description: 'redirect from a page name with an alternative disambiguation qualifier' },
{ tag: 'R from unnecessary disambiguation', description: 'redirect from a page name that has an unneeded disambiguation qualifier' }
],
'Merge, duplicate & move': [
{ tag: 'R from duplicated article', description: 'redirect to a similar article in order to preserve its edit history' },
{ tag: 'R with history', description: 'redirect from a page containing substantive page history, kept to preserve content and attributions' },
{ tag: 'R from move', description: 'redirect from a page that has been moved/renamed' },
{ tag: 'R from merge', description: 'redirect from a merged page in order to preserve its edit history' }
],
Namespace: [
{ tag: 'R from remote talk page', description: 'redirect from a talk page in any talk namespace to a corresponding page that is more heavily watched', restriction: 'insideTalkNamespaceOnly' },
{ tag: 'R to category namespace', description: 'redirect from a page outside the category namespace to a category page' },
{ tag: 'R to help namespace', description: 'redirect from any page inside or outside of help namespace to a page in that namespace' },
{ tag: 'R to main namespace', description: 'redirect from a page outside the main-article namespace to an article in mainspace' },
{ tag: 'R to portal namespace', description: 'redirect from any page inside or outside of portal space to a page in that namespace' },
{ tag: 'R to project namespace', description: 'redirect from any page inside or outside of project (Wikipedia: or WP:) space to any page in the project namespace' },
{ tag: 'R to user namespace', description: 'redirect from a page outside the user namespace to a user page (not to a user talk page)', restriction: 'outsideUserspaceOnly' }
]
},
Media: {
General: [
{ tag: 'R from album', description: 'redirect from an album to a related topic such as the recording artist or a list of albums', restriction: 'insideMainspaceOnly' },
{ tag: 'R from band name', description: 'redirect from a musical band or musical group name that redirects an article on a single person, i.e. the band or group leader' },
{ tag: 'R from book', description: 'redirect from a book title to a more general, relevant article', restriction: 'insideMainspaceOnly' },
{ tag: 'R from cover song', description: 'redirect from a cover version of a song to the article about the original song this version covers' },
{ tag: 'R from film', description: 'redirect from a film title that is a subtopic of the redirect target or a title in an alternative language that has been produced in that language', restriction: 'insideMainspaceOnly' },
{ tag: 'R from journal', description: 'redirect from a trade or professional journal article a more general, relevant Wikipedia article, such as the author or publisher of the article or to the title in an alternative language' },
{ tag: 'R from lyric', description: 'redirect from a lyric to a song or other source that describes the lyric' },
{ tag: 'R from meme', description: 'redirect from a name of an internet meme or other pop culture phenomenon that is a subtopic of the redirect target' },
{ tag: 'R from song', description: 'redirect from a song title to a more general, relevant article' },
{ tag: 'R from television episode', description: 'redirect from a television episode title to a related work or lists of episodes', restriction: 'insideMainspaceOnly' },
{ tag: 'R from television program', description: 'redirect from a title of television program, television series or web series that is a subtopic of the redirect target' },
{ tag: 'R from upcoming film', description: 'redirect from a title that potentially could be expanded into a new article or other type of associated page such as a new template.' },
{ tag: 'R from work', description: 'redirect from a creative work a related topic such as the author/artist, publisher, or a subject related to the work' }
],
Fiction: [
{ tag: 'R from fictional character', description: 'redirect from a fictional character to a related fictional work or list of characters', restriction: 'insideMainspaceOnly' },
{ tag: 'R from fictional element', description: 'redirect from a fictional element (such as an object or concept) to a related fictional work or list of similar elements', restriction: 'insideMainspaceOnly' },
{ tag: 'R from fictional location', description: 'redirect from a fictional location or setting to a related fictional work or list of places', restriction: 'insideMainspaceOnly' }
]
},
Miscellaneous: {
'Related information': [
{ tag: 'R to article without mention', description: 'redirect to an article without any mention of the redirected word or phrase', restriction: 'insideMainspaceOnly' },
{ tag: 'R from company name', description: 'redirect from a company name to a related article', restriction: 'insideMainspaceOnly' },
{ tag: 'R to decade', description: 'redirect from a year to the decade article', restriction: 'insideMainspaceOnly' },
{ tag: 'R from domain name', description: 'redirect from a domain name to an article about a website', restriction: 'insideMainspaceOnly' },
{ tag: 'R from emoji', description: 'redirect from an emoji to an article describing the depicted concept or the emoji itself' },
{ tag: 'R from phrase', description: 'redirect from a phrase to a more general relevant article covering the topic' },
{ tag: 'R from list topic', description: 'redirect from the topic of a list to the equivalent list' },
{ tag: 'R from member', description: 'redirect from a member of a group to a related topic such as the group or organization' },
{ tag: 'R to related topic', description: 'redirect to an article about a similar topic', restriction: 'insideMainspaceOnly' },
{ tag: 'R from related word', description: 'redirect from a related word' },
{ tag: 'R from school', description: 'redirect from a school article that had very little information', restriction: 'insideMainspaceOnly' },
{ tag: 'R from subtopic', description: 'redirect from a title that is a subtopic of the target article', restriction: 'insideMainspaceOnly' },
{ tag: 'R to subtopic', description: 'redirect to a subtopic of the redirect\'s title', restriction: 'insideMainspaceOnly' },
{ tag: 'R from Unicode character', description: 'redirect from a single Unicode character to an article or Wikipedia project page that infers meaning for the symbol', restriction: 'insideMainspaceOnly' },
{ tag: 'R from Unicode code', description: 'redirect from a Unicode code point to an article about the character it represents', restriction: 'insideMainspaceOnly' }
],
'With possibilities': [
{ tag: 'R with possibilities', description: 'redirect from a specific title to a more general, less detailed article (something which can and should be expanded)' }
],
'ISO codes': [
{ tag: 'R from ISO 4 abbreviation', description: 'redirect from an ISO 4 publication title abbreviation to the unabbreviated title', restriction: 'insideMainspaceOnly' },
{ tag: 'R from ISO 639 code', description: 'redirect from a title that is an ISO 639 language code to an article about the language', restriction: 'insideMainspaceOnly' }
],
Printworthiness: [
{ tag: 'R printworthy', description: 'redirect from a title that would be helpful in a printed or CD/DVD version of Wikipedia', restriction: 'insideMainspaceOnly' },
{ tag: 'R unprintworthy', description: 'redirect from a title that would NOT be helpful in a printed or CD/DVD version of Wikipedia', restriction: 'insideMainspaceOnly' }
]
}
};
// maintenance tags for FILES start here
Twinkle.tag.fileList = {
'License and sourcing problem tags': [
{ label: '{{Better source requested}}: source info consists of bare image URL/generic base URL only', value: 'Better source requested' },
{ label: '{{Maybe free media}}: currently tagged under non-free license, but free license may be available ', value: 'Maybe free media' },
{ label: '{{Non-free reduce}}: non-low-resolution fair use image (or too-long audio clip, etc)', value: 'Non-free reduce' },
{ label: '{{Orphaned non-free revisions}}: fair use media with old revisions that need to be deleted', value: 'Orphaned non-free revisions' }
],
'Wikimedia Commons-related tags': [
{ label: '{{Copy to Commons}}: free media that should be copied to Commons', value: 'Copy to Commons' },
{
label: '{{Deleted on Commons}}: file has previously been deleted from Commons',
value: 'Deleted on Commons',
subgroup: {
type: 'input',
name: 'deletedOnCommonsName',
label: 'Name on Commons:',
tooltip: 'Name of the image on Commons (if different from local name), excluding the File: prefix'
}
},
{
label: '{{Do not move to Commons}}: file not suitable for moving to Commons',
value: 'Do not move to Commons',
subgroup: [
{
type: 'input',
name: 'DoNotMoveToCommons_reason',
label: 'Reason:',
tooltip: 'Enter the reason why this image should not be moved to Commons (required). If the file is PD in the US but not in country of origin, enter "US only"',
required: true
},
{
type: 'number',
name: 'DoNotMoveToCommons_expiry',
label: 'Expiration year:',
min: new Morebits.Date().getFullYear(),
tooltip: 'If this file can be moved to Commons beginning in a certain year, you can enter it here (optional).'
}
]
},
{
label: '{{Keep local}}: request to keep local copy of a Commons file',
value: 'Keep local',
subgroup: {
type: 'input',
name: 'keeplocalName',
label: 'Commons image name if different:',
tooltip: 'Name of the image on Commons (if different from local name), excluding the File: prefix:'
}
},
{
label: '{{Nominated for deletion on Commons}}: file is nominated for deletion on Commons',
value: 'Nominated for deletion on Commons',
subgroup: {
type: 'input',
name: 'nominatedOnCommonsName',
label: 'Name on Commons:',
tooltip: 'Name of the image on Commons (if different from local name), excluding the File: prefix:'
}
}
],
'Cleanup tags': [
{ label: '{{Artifacts}}: PNG contains residual compression artifacts', value: 'Artifacts' },
{ label: '{{Bad font}}: SVG uses fonts not available on the thumbnail server', value: 'Bad font' },
{ label: '{{Bad format}}: PDF/DOC/... file should be converted to a more useful format', value: 'Bad format' },
{ label: '{{Bad GIF}}: GIF that should be PNG, JPEG, or SVG', value: 'Bad GIF' },
{ label: '{{Bad JPEG}}: JPEG that should be PNG or SVG', value: 'Bad JPEG' },
{ label: '{{Bad SVG}}: SVG with a mix of raster and vector graphics', value: 'Bad SVG' },
{ label: '{{Bad trace}}: auto-traced SVG requiring cleanup', value: 'Bad trace' },
{
label: '{{Cleanup image}}: general cleanup', value: 'Cleanup image',
subgroup: {
type: 'input',
name: 'cleanupimageReason',
label: 'Reason:',
tooltip: 'Enter the reason for cleanup (required)',
required: true
}
},
{ label: '{{ClearType}}: image (not screenshot) with ClearType anti-aliasing', value: 'ClearType' },
{ label: '{{Fake SVG}}: SVG solely containing raster graphics without true vector content', value: 'Fake SVG' },
{ label: '{{Imagewatermark}}: image contains visible or invisible watermarking', value: 'Imagewatermark' },
{ label: '{{NoCoins}}: image using coins to indicate scale', value: 'NoCoins' },
{ label: '{{Overcompressed JPEG}}: JPEG with high levels of artifacts', value: 'Overcompressed JPEG' },
{ label: '{{Opaque}}: opaque background should be transparent', value: 'Opaque' },
{ label: '{{Remove border}}: unneeded border, white space, etc.', value: 'Remove border' },
{
label: '{{Rename media}}: file should be renamed according to the criteria at [[WP:FMV]]',
value: 'Rename media',
subgroup: [
{
type: 'input',
name: 'renamemediaNewname',
label: 'New name:',
tooltip: 'Enter the new name for the image (optional)'
},
{
type: 'input',
name: 'renamemediaReason',
label: 'Reason:',
tooltip: 'Enter the reason for the rename (optional)'
}
]
},
{ label: '{{Should be PNG}}: GIF or JPEG should be lossless', value: 'Should be PNG' },
{
label: '{{Should be SVG}}: PNG, GIF or JPEG should be vector graphics', value: 'Should be SVG',
subgroup: {
name: 'svgCategory',
type: 'select',
list: [
{ label: '{{Should be SVG|other}}', value: 'other' },
{ label: '{{Should be SVG|alphabet}}: character images, font examples, etc.', value: 'alphabet' },
{ label: '{{Should be SVG|chemical}}: chemical diagrams, etc.', value: 'chemical' },
{ label: '{{Should be SVG|circuit}}: electronic circuit diagrams, etc.', value: 'circuit' },
{ label: '{{Should be SVG|coat of arms}}: coats of arms', value: 'coat of arms' },
{ label: '{{Should be SVG|diagram}}: diagrams that do not fit any other subcategory', value: 'diagram' },
{ label: '{{Should be SVG|emblem}}: emblems, free/libre logos, insignias, etc.', value: 'emblem' },
{ label: '{{Should be SVG|fair use}}: fair-use images, fair-use logos', value: 'fair use' },
{ label: '{{Should be SVG|flag}}: flags', value: 'flag' },
{ label: '{{Should be SVG|graph}}: visual plots of data', value: 'graph' },
{ label: '{{Should be SVG|logo}}: logos', value: 'logo' },
{ label: '{{Should be SVG|map}}: maps', value: 'map' },
{ label: '{{Should be SVG|music}}: musical scales, notes, etc.', value: 'music' },
{ label: '{{Should be SVG|physical}}: "realistic" images of physical objects, people, etc.', value: 'physical' },
{ label: '{{Should be SVG|symbol}}: miscellaneous symbols, icons, etc.', value: 'symbol' }
]
}
},
{ label: '{{Should be text}}: image should be represented as text, tables, or math markup', value: 'Should be text' }
],
'Image quality tags': [
{ label: '{{Image hoax}}: Image may be manipulated or constitute a hoax', value: 'Image hoax' },
{ label: '{{Image-blownout}}', value: 'Image-blownout' },
{ label: '{{Image-out-of-focus}}', value: 'Image-out-of-focus' },
{
label: '{{Image-Poor-Quality}}', value: 'Image-Poor-Quality',
subgroup: {
type: 'input',
name: 'ImagePoorQualityReason',
label: 'Reason:',
tooltip: 'Enter the reason why this image is so bad (required)',
required: true
}
},
{ label: '{{Image-underexposure}}', value: 'Image-underexposure' },
{
label: '{{Low quality chem}}: disputed chemical structures', value: 'Low quality chem',
subgroup: {
type: 'input',
name: 'lowQualityChemReason',
label: 'Reason:',
tooltip: 'Enter the reason why the diagram is disputed (required)',
required: true
}
}
],
'Replacement tags': [
{ label: '{{Obsolete}}: improved version available', value: 'Obsolete' },
{ label: '{{PNG version available}}', value: 'PNG version available' },
{ label: '{{Vector version available}}', value: 'Vector version available' }
]
};
Twinkle.tag.fileList['Replacement tags'].forEach((el) => {
el.subgroup = {
type: 'input',
label: 'Replacement file:',
tooltip: 'Enter the name of the file which replaces this one (required)',
name: el.value.replace(/ /g, '_') + 'File',
required: true
};
});
Twinkle.tag.callbacks = {
article: function articleCallback(pageobj) {
// Remove tags that become superfluous with this action
let pageText = pageobj.getPageText().replace(/\{\{\s*([Uu]serspace draft)\s*(\|(?:\{\{[^{}]*\}\}|[^{}])*)?\}\}\s*/g, '');
const params = pageobj.getCallbackParameters();
/**
* Saves the page following the removal of tags if any. The last step.
* Called from removeTags()
*/
const postRemoval = function() {
if (params.tagsToRemove.length) {
// Remove empty {{multiple issues}} if found
pageText = pageText.replace(/\{\{(multiple ?issues|article ?issues|mi)\s*\|\s*\}\}\n?/im, '');
// Remove single-element {{multiple issues}} if found
pageText = pageText.replace(/\{\{(?:multiple ?issues|article ?issues|mi)\s*\|\s*(\{\{[^}]+\}\})\s*\}\}/im, '$1');
}
// Build edit summary
const makeSentence = function(array) {
if (array.length < 3) {
return array.join(' and ');
}
const last = array.pop();
return array.join(', ') + ', and ' + last;
};
const makeTemplateLink = function(tag) {
let text = '{{[[';
// if it is a custom tag with a parameter
if (tag.includes('|')) {
tag = tag.slice(0, tag.indexOf('|'));
}
text += tag.includes(':') ? tag : 'Template:' + tag + '|' + tag;
return text + ']]}}';
};
let summaryText;
const addedTags = params.tags.map(makeTemplateLink);
const removedTags = params.tagsToRemove.map(makeTemplateLink);
if (addedTags.length) {
summaryText = 'Added ' + makeSentence(addedTags);
summaryText += removedTags.length ? '; and removed ' + makeSentence(removedTags) : '';
} else {
summaryText = 'Removed ' + makeSentence(removedTags);
}
summaryText += ' tag' + (addedTags.length + removedTags.length > 1 ? 's' : '');
if (params.reason) {
summaryText += ': ' + params.reason;
}
// avoid truncated summaries
if (summaryText.length > 499) {
summaryText = summaryText.replace(/\[\[[^|]+\|([^\]]+)\]\]/g, '$1');
}
pageobj.setPageText(pageText);
pageobj.setEditSummary(summaryText);
if ((mw.config.get('wgNamespaceNumber') === 0 && Twinkle.getPref('watchTaggedVenues').includes('articles')) || (mw.config.get('wgNamespaceNumber') === 118 && Twinkle.getPref('watchTaggedVenues').includes('drafts'))) {
pageobj.setWatchlist(Twinkle.getPref('watchTaggedPages'));
}
pageobj.setMinorEdit(Twinkle.getPref('markTaggedPagesAsMinor'));
pageobj.setCreateOption('nocreate');
pageobj.save(() => {
// COI: Start the discussion on the talk page (mainspace only)
if (params.coiReason) {
const coiTalkPage = new Morebits.wiki.Page('Talk:' + Morebits.pageNameNorm, 'Starting discussion on talk page');
coiTalkPage.setNewSectionText(params.coiReason + ' ~~~~');
coiTalkPage.setNewSectionTitle('COI tag (' + new Morebits.Date(pageobj.getLoadTime()).format('MMMM Y', 'utc') + ')');
coiTalkPage.setChangeTags(Twinkle.changeTags);
coiTalkPage.setCreateOption('recreate');
coiTalkPage.newSection();
}
// Special functions for {{not English}} and {{rough translation}}
// Post at WP:PNT (mainspace only)
if (params.translationPostAtPNT) {
const pntPage = new Morebits.wiki.Page('Wikipedia:Pages needing translation into English',
'Listing article at Wikipedia:Pages needing translation into English');
pntPage.setFollowRedirect(true);
pntPage.load((pageobj) => {
const oldText = pageobj.getPageText();
const lang = params.translationLanguage;
const reason = params.translationComments;
let templateText;
let text, summary;
if (params.tags.includes('Rough translation')) {
templateText = '{{subst:Dual fluency request|pg=' + Morebits.pageNameNorm + '|Language=' +
(lang || 'uncertain') + '|Comments=' + reason.trim() + '}} ~~~~';
// Place in section == Translated pages that could still use some cleanup ==
text = oldText + '\n\n' + templateText;
summary = 'Translation cleanup requested on ';
} else if (params.tags.includes('Not English')) {
templateText = '{{subst:Translation request|pg=' + Morebits.pageNameNorm + '|Language=' +
(lang || 'uncertain') + '|Comments=' + reason.trim() + '}} ~~~~';
// Place in section == Pages for consideration ==
text = oldText.replace(/\n+(==\s?Translated pages that could still use some cleanup\s?==)/,
'\n\n' + templateText + '\n\n$1');
summary = 'Translation' + (lang ? ' from ' + lang : '') + ' requested on ';
}
if (text === oldText) {
pageobj.getStatusElement().error('failed to find target spot for the discussion');
return;
}
pageobj.setPageText(text);
pageobj.setEditSummary(summary + ' [[:' + Morebits.pageNameNorm + ']]');
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.setCreateOption('recreate');
pageobj.save();
});
}
// Notify the user ({{Not English}} only)
if (params.translationNotify) {
new Morebits.wiki.Page(Morebits.pageNameNorm).lookupCreation((innerPageobj) => {
const initialContrib = innerPageobj.getCreator();
// Disallow warning yourself
if (initialContrib === mw.config.get('wgUserName')) {
innerPageobj.getStatusElement().warn('You (' + initialContrib + ') created this page; skipping user notification');
return;
}
const userTalkPage = new Morebits.wiki.Page('User talk:' + initialContrib,
'Notifying initial contributor (' + initialContrib + ')');
userTalkPage.setNewSectionTitle('Your article [[' + Morebits.pageNameNorm + ']]');
userTalkPage.setNewSectionText('{{subst:uw-notenglish|1=' + Morebits.pageNameNorm +
(params.translationPostAtPNT ? '' : '|nopnt=yes') + '}} ~~~~');
userTalkPage.setEditSummary('Notice: Please use English when contributing to the English Wikipedia.');
userTalkPage.setChangeTags(Twinkle.changeTags);
userTalkPage.setCreateOption('recreate');
userTalkPage.setFollowRedirect(true, false);
userTalkPage.newSection();
});
}
});
if (params.patrol) {
pageobj.triage();
}
};
/**
* Removes the existing tags that were deselected (if any)
* Calls postRemoval() when done
*/
const removeTags = function removeTags() {
if (params.tagsToRemove.length === 0) {
postRemoval();
return;
}
Morebits.Status.info('Info', 'Removing deselected tags that were already present');
const getRedirectsFor = [];
// Remove the tags from the page text, if found in its proper name,
// otherwise moves it to `getRedirectsFor` array earmarking it for
// later removal
params.tagsToRemove.forEach((tag) => {
const tagRegex = new RegExp('\\{\\{' + Morebits.pageNameRegex(tag) + '\\s*(\\|[^}]+)?\\}\\}\\n?');
if (tagRegex.test(pageText)) {
pageText = pageText.replace(tagRegex, '');
} else {
getRedirectsFor.push('Template:' + tag);
}
});
if (!getRedirectsFor.length) {
postRemoval();
return;
}
// Remove tags which appear in page text as redirects
const api = new Morebits.wiki.Api('Getting template redirects', {
action: 'query',
prop: 'linkshere',
titles: getRedirectsFor.join('|'),
redirects: 1, // follow redirect if the class name turns out to be a redirect page
lhnamespace: '10', // template namespace only
lhshow: 'redirect',
lhlimit: 'max', // 500 is max for normal users, 5000 for bots and sysops
format: 'json'
}, ((apiobj) => {
const pages = apiobj.getResponse().query.pages.filter((p) => !p.missing && !!p.linkshere);
pages.forEach((page) => {
let removed = false;
page.linkshere.concat({title: page.title}).forEach((el) => {
const tag = el.title.slice(9);
const tagRegex = new RegExp('\\{\\{' + Morebits.pageNameRegex(tag) + '\\s*(\\|[^}]*)?\\}\\}\\n?');
if (tagRegex.test(pageText)) {
pageText = pageText.replace(tagRegex, '');
removed = true;
return false; // break out of $.each
}
});
if (!removed) {
Morebits.Status.warn('Info', 'Failed to find {{' +
page.title.slice(9) + '}} on the page... excluding');
}
});
postRemoval();
}));
api.post();
};
if (!params.tags.length) {
removeTags();
return;
}
let tagRe, tagText = '', tags = [];
const groupableTags = [], groupableExistingTags = [];
// Executes first: addition of selected tags
/**
* Updates `tagText` with the syntax of `tagName` template with its parameters
*
* @param {number} tagIndex
* @param {string} tagName
*/
const addTag = function articleAddTag(tagIndex, tagName) {
let currentTag = '';
if (tagName === 'Uncategorized' || tagName === 'Improve categories') {
pageText += '\n\n{{' + tagName + '|date={{subst:CURRENTMONTHNAME}} {{subst:CURRENTYEAR}}}}';
} else {
currentTag += '{{' + tagName;
// fill in other parameters, based on the tag
const subgroupObj = Twinkle.tag.article.flatObject[tagName] &&
Twinkle.tag.article.flatObject[tagName].subgroup;
if (subgroupObj) {
const subgroups = Array.isArray(subgroupObj) ? subgroupObj : [subgroupObj];
subgroups.forEach((gr) => {
if (gr.parameter && (params[gr.name] || gr.required)) {
currentTag += '|' + gr.parameter + '=' + (params[gr.name] || '');
}
});
}
switch (tagName) {
case 'Not English':
case 'Rough translation':
if (params.translationPostAtPNT) {
currentTag += '|listed=yes';
}
break;
default:
break;
}
currentTag += '|date={{subst:CURRENTMONTHNAME}} {{subst:CURRENTYEAR}}}}\n';
tagText += currentTag;
}
};
/**
* Adds the tags which go outside {{multiple issues}}, either because
* these tags aren't supported in {{multiple issues}} or because
* {{multiple issues}} is not being added to the page at all
*/
const addUngroupedTags = function() {
$.each(tags, addTag);
// Insert tag after short description or any hatnotes,
// as well as deletion/protection-related templates
const wikipage = new Morebits.wikitext.Page(pageText);
const templatesAfter = Twinkle.hatnoteRegex +
// Protection templates
'pp|pp-.*?|' +
// CSD
'db|delete|db-.*?|speedy deletion-.*?|' +
// PROD
'(?:proposed deletion|prod blp)\\/dated(?:\\s*\\|(?:concern|user|timestamp|help).*)+|' +
// not a hatnote, but sometimes under a CSD or AfD
'salt|proposed deletion endorsed';
// AfD is special, as the tag includes html comments before and after the actual template
// trailing whitespace/newline needed since this subst's a newline
const afdRegex = '(?:<!--.*AfD.*\\n\\{\\{(?:Article for deletion\\/dated|AfDM).*\\}\\}\\n<!--.*(?:\\n<!--.*)?AfD.*(?:\\s*\\n))?';
pageText = wikipage.insertAfterTemplates(tagText, templatesAfter, null, afdRegex).getText();
removeTags();
};
// Separate tags into groupable ones (`groupableTags`) and non-groupable ones (`tags`)
params.tags.forEach((tag) => {
tagRe = new RegExp('\\{\\{' + tag + '(\\||\\}\\})', 'im');
// regex check for preexistence of tag can be skipped if in canRemove mode
if (Twinkle.tag.canRemove || !tagRe.exec(pageText)) {
// condition Twinkle.tag.article.tags[tag] to ensure that its not a custom tag
// Custom tags are assumed non-groupable, since we don't know whether MI template supports them
if (Twinkle.tag.article.flatObject[tag] && !Twinkle.tag.article.flatObject[tag].excludeMI) {
groupableTags.push(tag);
} else {
tags.push(tag);
}
} else {
if (tag === 'History merge') {
tags.push(tag);
} else {
Morebits.Status.warn('Info', 'Found {{' + tag +
'}} on the article already...excluding');
}
}
});
// To-be-retained existing tags that are groupable
params.tagsToRemain.forEach((tag) => {
// If the tag is unknown to us, we consider it non-groupable
if (Twinkle.tag.article.flatObject[tag] && !Twinkle.tag.article.flatObject[tag].excludeMI) {
groupableExistingTags.push(tag);
}
});
const miTest = /\{\{(multiple ?issues|article ?issues|mi)(?!\s*\|\s*section\s*=)[^}]+\{/im.exec(pageText);
if (miTest && groupableTags.length > 0) {
Morebits.Status.info('Info', 'Adding supported tags inside existing {{multiple issues}} tag');
tagText = '';
$.each(groupableTags, addTag);
const miRegex = new RegExp('(\\{\\{\\s*' + miTest[1] + '\\s*(?:\\|(?:\\{\\{[^{}]*\\}\\}|[^{}])*)?)\\}\\}\\s*', 'im');
pageText = pageText.replace(miRegex, '$1' + tagText + '}}\n');
tagText = '';
addUngroupedTags();
} else if (!miTest && (groupableExistingTags.length + groupableTags.length) >= 2) {
Morebits.Status.info('Info', 'Grouping supported tags inside {{multiple issues}}');
tagText += '{{Multiple issues|\n';
/**
* Adds newly added tags to MI
*/
const addNewTagsToMI = function() {
$.each(groupableTags, addTag);
tagText += '}}\n';
addUngroupedTags();
};
const getRedirectsFor = [];
// Reposition the tags on the page into {{multiple issues}}, if found with its
// proper name, else moves it to `getRedirectsFor` array to be handled later
groupableExistingTags.forEach((tag) => {
const tagRegex = new RegExp('(\\{\\{' + Morebits.pageNameRegex(tag) + '\\s*(\\|[^}]+)?\\}\\}\\n?)');
if (tagRegex.test(pageText)) {
tagText += tagRegex.exec(pageText)[1];
pageText = pageText.replace(tagRegex, '');
} else {
getRedirectsFor.push('Template:' + tag);
}
});
if (!getRedirectsFor.length) {
addNewTagsToMI();
return;
}
const api = new Morebits.wiki.Api('Getting template redirects', {
action: 'query',
prop: 'linkshere',
titles: getRedirectsFor.join('|'),
redirects: 1,
lhnamespace: '10', // template namespace only
lhshow: 'redirect',
lhlimit: 'max', // 500 is max for normal users, 5000 for bots and sysops
format: 'json'
}, ((apiobj) => {
const pages = apiobj.getResponse().query.pages.filter((p) => !p.missing && !!p.linkshere);
pages.forEach((page) => {
let found = false;
page.linkshere.forEach((el) => {
const tag = el.title.slice(9);
const tagRegex = new RegExp('(\\{\\{' + Morebits.pageNameRegex(tag) + '\\s*(\\|[^}]*)?\\}\\}\\n?)');
if (tagRegex.test(pageText)) {
tagText += tagRegex.exec(pageText)[1];
pageText = pageText.replace(tagRegex, '');
found = true;
return false; // break out of $.each
}
});
if (!found) {
Morebits.Status.warn('Info', 'Failed to find the existing {{' +
page.title.slice(9) + '}} on the page... skip repositioning');
}
});
addNewTagsToMI();
}));
api.post();
} else {
tags = tags.concat(groupableTags);
addUngroupedTags();
}
},
redirect: function redirect(pageobj) {
const params = pageobj.getCallbackParameters(),
tags = [];
let pageText = pageobj.getPageText(),
tagRe, tagText = '',
summaryText = 'Added',
i;
for (i = 0; i < params.tags.length; i++) {
tagRe = new RegExp('(\\{\\{' + params.tags[i] + '(\\||\\}\\}))', 'im');
if (!tagRe.exec(pageText)) {
tags.push(params.tags[i]);
} else {
Morebits.Status.warn('Info', 'Found {{' + params.tags[i] +
'}} on the redirect already...excluding');
}
}
const addTag = function redirectAddTag(tagIndex, tagName) {
tagText += '\n{{' + tagName;
if (tagName === 'R from alternative language') {
if (params.altLangFrom) {
tagText += '|from=' + params.altLangFrom;
}
if (params.altLangTo) {
tagText += '|to=' + params.altLangTo;
}
} else if (tagName === 'R avoided double redirect' && params.doubleRedirectTarget) {
tagText += '|1=' + params.doubleRedirectTarget;
}
tagText += '}}';
if (tagIndex > 0) {
if (tagIndex === (tags.length - 1)) {
summaryText += ' and';
} else if (tagIndex < (tags.length - 1)) {
summaryText += ',';
}
}
summaryText += ' {{[[:' + (tagName.includes(':') ? tagName : 'Template:' + tagName + '|' + tagName) + ']]}}';
};
if (!tags.length) {
Morebits.Status.warn('Info', 'No tags remaining to apply');
}
tags.sort();
$.each(tags, addTag);
// Check for all Rcat shell redirects (from #433)
if (pageText.match(/{{(?:redr|this is a redirect|r(?:edirect)?(?:.?cat.*)?[ _]?sh|RCS)/i)) {
// Regex inspired by [[User:Kephir/gadgets/sagittarius.js]] ([[Special:PermaLink/831402893]])
const oldTags = pageText.match(/(\s*{{[A-Za-z\s]+\|(?:\s*1=)?)((?:[^|{}]|{{[^}]+}})+)(}})\s*/i);
pageText = pageText.replace(oldTags[0], oldTags[1] + tagText + oldTags[2] + oldTags[3]);
} else {
// Fold any pre-existing Rcats into taglist and under Rcatshell
const pageTags = pageText.match(/\s*{{R(?:edirect)? .*?}}/img);
let oldPageTags = '';
if (pageTags) {
pageTags.forEach((pageTag) => {
const pageRe = new RegExp(Morebits.string.escapeRegExp(pageTag), 'img');
pageText = pageText.replace(pageRe, '');
pageTag = pageTag.trim();
oldPageTags += '\n' + pageTag;
});
}
pageText = pageText.trim() + '\n\n{{Redirect category shell|' + tagText + oldPageTags + '\n}}';
}
summaryText += (tags.length > 0 ? ' tag' + (tags.length > 1 ? 's' : ' ') : ' {{[[Template:Redirect category shell|Redirect category shell]]}}') + ' to redirect';
// avoid truncated summaries
if (summaryText.length > 499) {
summaryText = summaryText.replace(/\[\[[^|]+\|([^\]]+)\]\]/g, '$1');
}
pageobj.setPageText(pageText);
pageobj.setEditSummary(summaryText);
if (Twinkle.getPref('watchTaggedVenues').includes('redirects')) {
pageobj.setWatchlist(Twinkle.getPref('watchTaggedPages'));
}
pageobj.setMinorEdit(Twinkle.getPref('markTaggedPagesAsMinor'));
pageobj.setCreateOption('nocreate');
pageobj.save();
if (params.patrol) {
pageobj.triage();
}
},
file: function twinkletagCallbacksFile(pageobj) {
let text = pageobj.getPageText();
const params = pageobj.getCallbackParameters();
let summary = 'Adding ';
// Add maintenance tags
if (params.tags.length) {
let tagtext = '', currentTag;
$.each(params.tags, (k, tag) => {
// when other commons-related tags are placed, remove "move to Commons" tag
if (['Keep local', 'Do not move to Commons'].includes(tag)) {
text = Twinkle.removeMoveToCommonsTagsFromWikicode(text);
}
currentTag = tag;
switch (tag) {
case 'Keep local':
if (params.keeplocalName !== '') {
currentTag += '|1=' + params.keeplocalName;
}
break;
case 'Rename media':
if (params.renamemediaNewname !== '') {
currentTag += '|1=' + params.renamemediaNewname;
}
if (params.renamemediaReason !== '') {
currentTag += '|2=' + params.renamemediaReason;
}
break;
case 'Cleanup image':
currentTag += '|1=' + params.cleanupimageReason;
break;
case 'Image-Poor-Quality':
currentTag += '|1=' + params.ImagePoorQualityReason;
break;
case 'Image hoax':
currentTag += '|date={{subst:CURRENTMONTHNAME}} {{subst:CURRENTYEAR}}';
break;
case 'Low quality chem':
currentTag += '|1=' + params.lowQualityChemReason;
break;
case 'Vector version available':
text = text.replace(/\{\{((convert to |convertto|should be |shouldbe|to)?svg|badpng|vectorize)[^}]*\}\}/gi, '');
/* falls through */
case 'PNG version available':
/* falls through */
case 'Obsolete':
currentTag += '|1=' + params[tag.replace(/ /g, '_') + 'File'];
break;
case 'Do not move to Commons':
currentTag += '|reason=' + params.DoNotMoveToCommons_reason;
if (params.DoNotMoveToCommons_expiry) {
currentTag += '|expiry=' + params.DoNotMoveToCommons_expiry;
}
break;
case 'Orphaned non-free revisions':
currentTag = 'subst:' + currentTag; // subst
// remove {{non-free reduce}} and redirects
text = text.replace(/\{\{\s*(Template\s*:\s*)?(Non-free reduce|FairUseReduce|Fairusereduce|Fair Use Reduce|Fair use reduce|Reduce size|Reduce|Fair-use reduce|Image-toobig|Comic-ovrsize-img|Non-free-reduce|Nfr|Smaller image|Nonfree reduce)\s*(\|(?:\{\{[^{}]*\}\}|[^{}])*)?\}\}\s*/ig, '');
currentTag += '|date={{subst:date}}';
break;
case 'Copy to Commons':
currentTag += '|human=' + mw.config.get('wgUserName');
break;
case 'Should be SVG':
currentTag += '|' + params.svgCategory;
break;
case 'Nominated for deletion on Commons':
if (params.nominatedOnCommonsName !== '') {
currentTag += '|1=' + params.nominatedOnCommonsName;
}
break;
case 'Deleted on Commons':
if (params.deletedOnCommonsName !== '') {
currentTag += '|1=' + params.deletedOnCommonsName;
}
break;
default:
break; // don't care
}
currentTag = '{{' + currentTag + '}}\n';
tagtext += currentTag;
summary += '{{' + tag + '}}, ';
});
if (!tagtext) {
pageobj.getStatusElement().warn('User canceled operation; nothing to do');
return;
}
text = tagtext + text;
}
pageobj.setPageText(text);
pageobj.setEditSummary(summary.substring(0, summary.length - 2));
pageobj.setChangeTags(Twinkle.changeTags);
if (Twinkle.getPref('watchTaggedVenues').includes('files')) {
pageobj.setWatchlist(Twinkle.getPref('watchTaggedPages'));
}
pageobj.setMinorEdit(Twinkle.getPref('markTaggedPagesAsMinor'));
pageobj.setCreateOption('nocreate');
pageobj.save();
if (params.patrol) {
pageobj.triage();
}
}
};
/**
* Given an array of incompatible tags, check if we have two or more selected
*
* @param {Array} incompatibleTags
* @param {Array} tagsToCheck
* @param {string} [extraMessage]
* @return {true|undefined}
*/
Twinkle.tag.checkIncompatible = function(incompatibleTags, tagsToCheck, extraMessage = null) {
const count = incompatibleTags.filter((tag) => tagsToCheck.includes(tag)).length;
if (count > 1) {
const incompatibleTagsString = '{{' + incompatibleTags.join('}}, {{') + '}}';
let message = 'Please select only one of: ' + incompatibleTagsString + '.';
message += extraMessage ? ' ' + extraMessage : '';
alert(message);
return true;
}
};
Twinkle.tag.callback.evaluate = function twinkletagCallbackEvaluate(e) {
const form = e.target;
const params = Morebits.QuickForm.getInputData(form);
// Validation
// We could theoretically put them all checkIncompatible calls in a
// forEach loop, but it's probably clearer not to have [[array one],
// [array two]] devoid of context.
switch (Twinkle.tag.mode) {
case 'article':
params.tagsToRemove = form.getUnchecked('existingTags'); // not in `input`
params.tagsToRemain = params.existingTags || []; // container not created if none present
if (Twinkle.tag.checkIncompatible(['Not English', 'Rough translation'], params.tags)) {
return;
}
break;
case 'file':
if (Twinkle.tag.checkIncompatible(['Bad GIF', 'Bad JPEG', 'Bad SVG', 'Bad format'], params.tags)) {
return;
}
if (Twinkle.tag.checkIncompatible(['Should be PNG', 'Should be SVG', 'Should be text'], params.tags)) {
return;
}
if (Twinkle.tag.checkIncompatible(['Bad SVG', 'Vector version available'], params.tags)) {
return;
}
if (Twinkle.tag.checkIncompatible(['Bad JPEG', 'Overcompressed JPEG'], params.tags)) {
return;
}
if (Twinkle.tag.checkIncompatible(['PNG version available', 'Vector version available'], params.tags)) {
return;
}
// Get extension from either mime-type or title, if not present (e.g., SVGs)
var extension = ((extension = $('.mime-type').text()) && extension.split(/\//)[1]) || mw.Title.newFromText(Morebits.pageNameNorm).getExtension();
if (extension) {
const extensionUpper = extension.toUpperCase();
// What self-respecting file format has *two* extensions?!
if (extensionUpper === 'JPG') {
extension = 'JPEG';
}
// Check that selected templates make sense given the file's extension.
// {{Bad GIF|JPEG|SVG}}, {{Fake SVG}}
if (extensionUpper !== 'GIF' && params.tags.includes('Bad GIF')) {
alert('This appears to be a ' + extension + ' file, so {{Bad GIF}} is inappropriate.');
return;
} else if (extensionUpper !== 'JPEG' && params.tags.includes('Bad JPEG')) {
alert('This appears to be a ' + extension + ' file, so {{Bad JPEG}} is inappropriate.');
return;
} else if (extensionUpper !== 'SVG' && params.tags.includes('Bad SVG')) {
alert('This appears to be a ' + extension + ' file, so {{Bad SVG}} is inappropriate.');
return;
} else if (extensionUpper !== 'SVG' && params.tags.includes('Fake SVG')) {
alert('This appears to be a ' + extension + ' file, so {{Fake SVG}} is inappropriate.');
return;
}
// {{Should be PNG|SVG}}
if (params.tags.includes('Should be ' + extensionUpper)) {
alert('This is already a ' + extension + ' file, so {{Should be ' + extensionUpper + '}} is inappropriate.');
return;
}
// {{Overcompressed JPEG}}
if (params.tags.includes('Overcompressed JPEG') && extensionUpper !== 'JPEG') {
alert('This appears to be a ' + extension + ' file, so {{Overcompressed JPEG}} probably doesn\'t apply.');
return;
}
// {{Bad trace}} and {{Bad font}}
if (extensionUpper !== 'SVG') {
if (params.tags.includes('Bad trace')) {
alert('This appears to be a ' + extension + ' file, so {{Bad trace}} probably doesn\'t apply.');
return;
} else if (params.tags.includes('Bad font')) {
alert('This appears to be a ' + extension + ' file, so {{Bad font}} probably doesn\'t apply.');
return;
}
}
}
// {{Do not move to Commons}}
if (
params.tags.includes('Do not move to Commons') &&
params.DoNotMoveToCommons_expiry &&
(
!/^2\d{3}$/.test(params.DoNotMoveToCommons_expiry) ||
parseInt(params.DoNotMoveToCommons_expiry, 10) <= new Date().getFullYear()
)
) {
alert('Must be a valid future year.');
return;
}
break;
case 'redirect':
if (Twinkle.tag.checkIncompatible(['R printworthy', 'R unprintworthy'], params.tags)) {
return;
}
if (Twinkle.tag.checkIncompatible(['R from subtopic', 'R to subtopic'], params.tags)) {
return;
}
if (Twinkle.tag.checkIncompatible([
'R to category namespace',
'R to help namespace',
'R to main namespace',
'R to portal namespace',
'R to project namespace',
'R to user namespace'
], params.tags)) {
return;
}
break;
default:
alert('Twinkle.tag: unknown mode ' + Twinkle.tag.mode);
break;
}
// File/redirect: return if no tags selected
// Article: return if no tag is selected and no already present tag is deselected
if (params.tags.length === 0 && (Twinkle.tag.mode !== 'article' || params.tagsToRemove.length === 0)) {
alert('You must select at least one tag!');
return;
}
Morebits.SimpleWindow.setButtonsEnabled(false);
Morebits.Status.init(form);
Morebits.wiki.actionCompleted.redirect = Morebits.pageNameNorm;
Morebits.wiki.actionCompleted.notice = 'Tagging complete, reloading article in a few seconds';
if (Twinkle.tag.mode === 'redirect') {
Morebits.wiki.actionCompleted.followRedirect = false;
}
const wikipediaPage = new Morebits.wiki.Page(Morebits.pageNameNorm, 'Tagging ' + Twinkle.tag.mode);
wikipediaPage.setCallbackParameters(params);
wikipediaPage.setChangeTags(Twinkle.changeTags); // Here to apply to triage
wikipediaPage.load(Twinkle.tag.callbacks[Twinkle.tag.mode]);
};
Twinkle.addInitCallback(Twinkle.tag, 'tag');
}());
// </nowiki>
7ld2tjkk8dkanwutu7j44luq4smuvav
769534
769467
2026-10-22T00:00:44Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 1406828: rm cmt
769534
javascript
text/javascript
// <nowiki>
(function() {
/*
****************************************
*** twinkletag.js: Tag module
****************************************
* Mode of invocation: Tab ("Tag")
* Active on: Existing articles and drafts; file pages with a corresponding file
* which is local (not on Commons); all redirects
*/
Twinkle.tag = function twinkletag() {
// redirect tagging (exclude category redirects, which are all soft redirects and so shouldn't be tagged with rcats)
if (Morebits.isPageRedirect() && mw.config.get('wgNamespaceNumber') !== 14) {
Twinkle.tag.mode = 'redirect';
Twinkle.addPortletLink(Twinkle.tag.callback, 'Tag', 'twinkle-tag', 'Tag redirect');
// file tagging
} else if (mw.config.get('wgNamespaceNumber') === 6 && !document.getElementById('mw-sharedupload') && document.getElementById('mw-imagepage-section-filehistory')) {
Twinkle.tag.mode = 'file';
Twinkle.addPortletLink(Twinkle.tag.callback, 'Tag', 'twinkle-tag', 'Add maintenance tags to file');
// article/draft article tagging
} else if ([0, 118].includes(mw.config.get('wgNamespaceNumber')) && mw.config.get('wgCurRevisionId')) {
Twinkle.tag.mode = 'article';
// Can't remove tags when not viewing current version
Twinkle.tag.canRemove = (mw.config.get('wgCurRevisionId') === mw.config.get('wgRevisionId')) &&
// Disabled on latest diff because the diff slider could be used to slide
// away from the latest diff without causing the script to reload
!mw.config.get('wgDiffNewId');
Twinkle.addPortletLink(Twinkle.tag.callback, 'Tag', 'twinkle-tag', 'Add or remove article maintenance tags');
}
};
Twinkle.tag.checkedTags = [];
Twinkle.tag.callback = function twinkletagCallback() {
const Window = new Morebits.SimpleWindow(630, Twinkle.tag.mode === 'article' ? 500 : 400);
Window.setScriptName('Twinkle');
// anyone got a good policy/guideline/info page/instructional page link??
Window.addFooterLink('Tag prefs', 'WP:TW/PREF#tag');
Window.addFooterLink('Twinkle help', 'WP:TW/DOC#tag');
Window.addFooterLink('Give feedback', 'WT:TW');
const form = new Morebits.QuickForm(Twinkle.tag.callback.evaluate);
// if page is unreviewed, add a checkbox to the form so that user can pick whether or not to review it
const isPatroller = mw.config.get('wgUserGroups').some((r) => ['patroller', 'sysop'].includes(r));
if (isPatroller) {
new mw.Api().get({
action: 'pagetriagelist',
format: 'json',
page_id: mw.config.get('wgArticleId')
}).then((response) => {
// Figure out whether the article is marked as reviewed in PageTriage.
// Recent articles will have a patrol_status that we can read.
// For articles that have been out of the new pages feed for awhile, pages[0] will be undefined.
const isReviewed = response.pagetriagelist.pages[0] ?
response.pagetriagelist.pages[0].patrol_status > 0 :
true;
// if article is not marked as reviewed, show the "mark as reviewed" check box
if (!isReviewed) {
// Quickform is probably already rendered. Instead of using form.append(), we need to make an element and then append it using JQuery.
const checkbox = new Morebits.QuickForm.Element({
type: 'checkbox',
list: [
{
label: 'Mark the page as patrolled/reviewed',
value: 'patrol',
name: 'patrol',
checked: Twinkle.getPref('markTaggedPagesAsPatrolled')
}
]
});
const html = checkbox.render();
$('.quickform').prepend(html);
}
});
}
form.append({
type: 'input',
label: 'Filter tag list:',
name: 'quickfilter',
size: '30',
event: function twinkletagquickfilter() {
// flush the DOM of all existing underline spans
$allCheckboxDivs.find('.search-hit').each((i, e) => {
const labelElement = e.parentElement;
// This would convert <label>Hello <span class=search-hit>wo</span>rld</label>
// to <label>Hello world</label>
labelElement.innerHTML = labelElement.textContent;
});
if (this.value) {
$allCheckboxDivs.hide();
$allHeaders.hide();
const searchString = this.value;
const searchRegex = new RegExp(mw.util.escapeRegExp(searchString), 'i');
$allCheckboxDivs.find('label').each(function() {
const labelText = this.textContent;
const searchHit = searchRegex.exec(labelText);
if (searchHit) {
const range = document.createRange();
const textnode = this.childNodes[0];
range.selectNodeContents(textnode);
range.setStart(textnode, searchHit.index);
range.setEnd(textnode, searchHit.index + searchString.length);
const underlineSpan = $('<span>').addClass('search-hit').css('text-decoration', 'underline')[0];
range.surroundContents(underlineSpan);
this.parentElement.style.display = 'block'; // show
}
});
} else {
$allCheckboxDivs.show();
$allHeaders.show();
}
}
});
switch (Twinkle.tag.mode) {
case 'article':
Window.setTitle('Article maintenance tagging');
// Build sorting and lookup object flatObject, which is always
// needed but also used to generate the alphabetical list
Twinkle.tag.article.flatObject = {};
Object.values(Twinkle.tag.article.tagList).forEach((group) => {
Object.values(group).forEach((subgroup) => {
if (Array.isArray(subgroup)) {
subgroup.forEach((item) => {
Twinkle.tag.article.flatObject[item.tag] = item;
});
} else {
Twinkle.tag.article.flatObject[subgroup.tag] = subgroup;
}
});
});
form.append({
type: 'select',
name: 'sortorder',
label: 'View this list:',
tooltip: 'You can change the default view order in your Twinkle preferences (WP:TWPREFS).',
event: Twinkle.tag.updateSortOrder,
list: [
{ type: 'option', value: 'cat', label: 'By categories', selected: Twinkle.getPref('tagArticleSortOrder') === 'cat' },
{ type: 'option', value: 'alpha', label: 'In alphabetical order', selected: Twinkle.getPref('tagArticleSortOrder') === 'alpha' }
]
});
if (!Twinkle.tag.canRemove) {
const divElement = document.createElement('div');
divElement.innerHTML = 'For removal of existing tags, please open Tag menu from the current version of article';
form.append({
type: 'div',
name: 'untagnotice',
label: divElement
});
}
form.append({
type: 'div',
id: 'tagWorkArea',
className: 'morebits-scrollbox'
});
form.append({
type: 'input',
label: 'Reason',
name: 'reason',
tooltip: 'Optional reason to be appended in edit summary. Recommended when removing tags.',
size: '60'
});
break;
case 'file':
Window.setTitle('File maintenance tagging');
$.each(Twinkle.tag.fileList, (groupName, group) => {
form.append({ type: 'header', label: groupName });
form.append({ type: 'checkbox', name: 'tags', list: group });
});
if (Twinkle.getPref('customFileTagList').length) {
form.append({ type: 'header', label: 'Custom tags' });
form.append({ type: 'checkbox', name: 'tags', list: Twinkle.getPref('customFileTagList') });
}
break;
case 'redirect':
Window.setTitle('Redirect tagging');
// If a tag has a restriction for this namespace or title, return true, so that we know not to display it in the list of check boxes.
var isRestricted = function(item) {
if (typeof item.restriction === 'undefined') {
return false;
}
const namespace = mw.config.get('wgNamespaceNumber');
switch (item.restriction) {
case 'insideMainspaceOnly':
if (namespace !== 0) {
return true;
}
break;
case 'outsideUserspaceOnly':
if (namespace === 2 || namespace === 3) {
return true;
}
break;
case 'insideTalkNamespaceOnly':
if (namespace % 2 !== 1 || namespace < 0) {
return true;
}
break;
case 'disambiguationPagesOnly':
if (!mw.config.get('wgPageName').endsWith('_(disambiguation)')) {
return true;
}
break;
default:
alert('Twinkle.tag: unknown restriction ' + item.restriction);
break;
}
return false;
};
// Generate the HTML form with the list of redirect tags that the user can choose to apply.
var i = 1;
$.each(Twinkle.tag.redirectList, (groupName, group) => {
form.append({ type: 'header', id: 'tagHeader' + i, label: groupName });
const subdiv = form.append({ type: 'div', id: 'tagSubdiv' + i++ });
$.each(group, (subgroupName, subgroup) => {
subdiv.append({ type: 'div', label: [Morebits.htmlNode('b', subgroupName)] });
subdiv.append({
type: 'checkbox',
name: 'tags',
list: subgroup
.filter((item) => !isRestricted(item))
.map((item) => ({ value: item.tag, label: '{{' + item.tag + '}}: ' + item.description, subgroup: item.subgroup }))
});
});
});
if (Twinkle.getPref('customRedirectTagList').length) {
form.append({ type: 'header', label: 'Custom tags' });
form.append({ type: 'checkbox', name: 'tags', list: Twinkle.getPref('customRedirectTagList') });
}
break;
default:
alert('Twinkle.tag: unknown mode ' + Twinkle.tag.mode);
break;
}
form.append({ type: 'submit', className: 'tw-tag-submit' });
const result = form.render();
Window.setContent(result);
Window.display();
// for quick filter:
$allCheckboxDivs = $(result).find('[name$=tags]').parent();
$allHeaders = $(result).find('h5, .quickformDescription');
result.quickfilter.focus(); // place cursor in the quick filter field as soon as window is opened
result.quickfilter.autocomplete = 'off'; // disable browser suggestions
result.quickfilter.addEventListener('keypress', (e) => {
if (e.keyCode === 13) { // prevent enter key from accidentally submitting the form
e.preventDefault();
return false;
}
});
if (Twinkle.tag.mode === 'article') {
Twinkle.tag.alreadyPresentTags = [];
if (Twinkle.tag.canRemove) {
// Look for existing maintenance tags in the lead section and put them in array
// All tags are HTML table elements that are direct children of .mw-parser-output
// (legacy parser) or the first <section> child (Parsoid),
// except when they are within {{multiple issues}}
// TODO: clean up after legacy parser is permanently replaced with Parsoid
const $oldParser = $('.mw-parser-output');
const $newParser = $('.mw-parser-output section[data-mw-section-id="0"]');
const $scanTarget = $newParser.length ? $newParser : $oldParser;
$scanTarget.children().each((i, e) => {
// break out on encountering the first heading, which means we are no
// longer in the lead section
if (e.classList.contains('mw-heading')) {
return false;
}
// The ability to remove tags depends on the template's {{ambox}} |name=
// parameter bearing the template's correct name (preferably) or a name that at
// least redirects to the actual name
// All tags have their first class name as "box-" + template name
if (e.className.indexOf('box-') === 0) {
if (e.classList[0] === 'box-Multiple_issues') {
$(e).find('.ambox').each((idx, e) => {
if (e.classList[0].indexOf('box-') === 0) {
const tag = e.classList[0].slice('box-'.length).replace(/_/g, ' ');
Twinkle.tag.alreadyPresentTags.push(tag);
}
});
return true; // continue
}
const tag = e.classList[0].slice('box-'.length).replace(/_/g, ' ');
Twinkle.tag.alreadyPresentTags.push(tag);
}
});
// {{Uncategorized}} and {{Improve categories}} are usually placed at the end
if ($('.box-Uncategorized').length) {
Twinkle.tag.alreadyPresentTags.push('Uncategorized');
}
if ($('.box-Improve_categories').length) {
Twinkle.tag.alreadyPresentTags.push('Improve categories');
}
}
// Add status text node after Submit button
const statusNode = document.createElement('small');
statusNode.id = 'tw-tag-status';
Twinkle.tag.status = {
// initial state; defined like this because these need to be available for reference
// in the click event handler
numAdded: 0,
numRemoved: 0
};
$('button.tw-tag-submit').after(statusNode);
// fake a change event on the sort dropdown, to initialize the tag list
const evt = document.createEvent('Event');
evt.initEvent('change', true, true);
result.sortorder.dispatchEvent(evt);
} else {
// Redirects and files: Add a link to each template's description page
Morebits.QuickForm.getElements(result, 'tags').forEach(generateLinks);
}
};
// $allCheckboxDivs and $allHeaders are defined globally, rather than in the
// quickfilter event function, to avoid having to recompute them on every keydown
let $allCheckboxDivs, $allHeaders;
Twinkle.tag.updateSortOrder = function(e) {
const form = e.target.form;
const sortorder = e.target.value;
Twinkle.tag.checkedTags = form.getChecked('tags');
const container = new Morebits.QuickForm.Element({ type: 'fragment' });
// function to generate a checkbox, with appropriate subgroup if needed
const makeCheckbox = function(item) {
const tag = item.tag, description = item.description;
const checkbox = { value: tag, label: '{{' + tag + '}}: ' + description };
if (Twinkle.tag.checkedTags.includes(tag)) {
checkbox.checked = true;
}
checkbox.subgroup = item.subgroup;
return checkbox;
};
const makeCheckboxesForAlreadyPresentTags = function() {
container.append({ type: 'header', id: 'tagHeader0', label: 'Tags already present' });
const subdiv = container.append({ type: 'div', id: 'tagSubdiv0' });
const checkboxes = [];
const unCheckedTags = e.target.form.getUnchecked('existingTags');
Twinkle.tag.alreadyPresentTags.forEach((tag) => {
const checkbox =
{
value: tag,
label: '{{' + tag + '}}' + (Twinkle.tag.article.flatObject[tag] ? ': ' + Twinkle.tag.article.flatObject[tag].description : ''),
checked: !unCheckedTags.includes(tag),
style: 'font-style: italic'
};
checkboxes.push(checkbox);
});
subdiv.append({
type: 'checkbox',
name: 'existingTags',
list: checkboxes
});
};
if (sortorder === 'cat') { // categorical sort order
// function to iterate through the tags and create a checkbox for each one
const doCategoryCheckboxes = function(subdiv, subgroup) {
const checkboxes = [];
$.each(subgroup, (k, item) => {
if (!Twinkle.tag.alreadyPresentTags.includes(item.tag)) {
checkboxes.push(makeCheckbox(item));
}
});
subdiv.append({
type: 'checkbox',
name: 'tags',
list: checkboxes
});
};
if (Twinkle.tag.alreadyPresentTags.length > 0) {
makeCheckboxesForAlreadyPresentTags();
}
let i = 1;
// go through each category and sub-category and append lists of checkboxes
$.each(Twinkle.tag.article.tagList, (groupName, group) => {
container.append({ type: 'header', id: 'tagHeader' + i, label: groupName });
const subdiv = container.append({ type: 'div', id: 'tagSubdiv' + i++ });
if (Array.isArray(group)) {
doCategoryCheckboxes(subdiv, group);
} else {
$.each(group, (subgroupName, subgroup) => {
subdiv.append({ type: 'div', label: [Morebits.htmlNode('b', subgroupName)] });
doCategoryCheckboxes(subdiv, subgroup);
});
}
});
} else { // alphabetical sort order
if (Twinkle.tag.alreadyPresentTags.length > 0) {
makeCheckboxesForAlreadyPresentTags();
container.append({ type: 'header', id: 'tagHeader1', label: 'Available tags' });
}
// Avoid repeatedly resorting
Twinkle.tag.article.alphabeticalList = Twinkle.tag.article.alphabeticalList || Object.keys(Twinkle.tag.article.flatObject).sort();
const checkboxes = [];
Twinkle.tag.article.alphabeticalList.forEach((tag) => {
if (!Twinkle.tag.alreadyPresentTags.includes(tag)) {
checkboxes.push(makeCheckbox(Twinkle.tag.article.flatObject[tag]));
}
});
container.append({
type: 'checkbox',
name: 'tags',
list: checkboxes
});
}
// append any custom tags
if (Twinkle.getPref('customTagList').length) {
container.append({ type: 'header', label: 'Custom tags' });
container.append({ type: 'checkbox', name: 'tags',
list: Twinkle.getPref('customTagList').map((el) => {
el.checked = Twinkle.tag.checkedTags.includes(el.value);
return el;
})
});
}
const $workarea = $(form).find('#tagWorkArea');
const rendered = container.render();
$workarea.empty().append(rendered);
// for quick filter:
$allCheckboxDivs = $workarea.find('[name=tags], [name=existingTags]').parent();
$allHeaders = $workarea.find('h5, .quickformDescription');
form.quickfilter.value = ''; // clear search, because the search results are not preserved over mode change
form.quickfilter.focus();
// style adjustments
$workarea.find('h5').css({ 'font-size': '110%' });
$workarea.find('h5:not(:first-child)').css({ 'margin-top': '1em' });
$workarea.find('div').filter(':has(span.quickformDescription)').css({ 'margin-top': '0.4em' });
Morebits.QuickForm.getElements(form, 'existingTags').forEach(generateLinks);
Morebits.QuickForm.getElements(form, 'tags').forEach(generateLinks);
// tally tags added/removed, update statusNode text
const statusNode = document.getElementById('tw-tag-status');
$('[name=tags], [name=existingTags]').on('click', function() {
if (this.name === 'tags') {
Twinkle.tag.status.numAdded += this.checked ? 1 : -1;
} else if (this.name === 'existingTags') {
Twinkle.tag.status.numRemoved += this.checked ? -1 : 1;
}
const firstPart = 'Adding ' + Twinkle.tag.status.numAdded + ' tag' + (Twinkle.tag.status.numAdded > 1 ? 's' : '');
const secondPart = 'Removing ' + Twinkle.tag.status.numRemoved + ' tag' + (Twinkle.tag.status.numRemoved > 1 ? 's' : '');
statusNode.textContent =
(Twinkle.tag.status.numAdded ? ' ' + firstPart : '') +
(Twinkle.tag.status.numRemoved ? (Twinkle.tag.status.numAdded ? '; ' : ' ') + secondPart : '');
});
};
/**
* Adds a link to each template's description page
*
* @param {Morebits.QuickForm.Element} checkbox associated with the template
*/
var generateLinks = function(checkbox) {
const link = Morebits.htmlNode('a', '>');
link.setAttribute('class', 'tag-template-link');
const tagname = checkbox.values;
link.setAttribute('href', mw.util.getUrl(
(!tagname.includes(':') ? 'Template:' : '') +
(!tagname.includes('|') ? tagname : tagname.slice(0, tagname.indexOf('|')))
));
link.setAttribute('target', '_blank');
$(checkbox).parent().append(['\u00A0', link]);
};
// Tags for ARTICLES start here
Twinkle.tag.article = {};
// Shared across {{Rough translation}} and {{Not English}}
const translationSubgroups = [
{
name: 'translationLanguage',
parameter: '1',
type: 'input',
label: 'Language of article (if known):',
tooltip: 'Consider looking at [[WP:LRC]] for help. If listing the article at PNT, please try to avoid leaving this box blank, unless you are completely unsure.'
}
].concat(mw.config.get('wgNamespaceNumber') === 0 ? [
{
type: 'checkbox',
list: [{
name: 'translationPostAtPNT',
label: 'List this article at Wikipedia:Pages needing translation into English (PNT)',
checked: true
}]
},
{
name: 'translationComments',
type: 'textarea',
label: 'Additional comments to post at PNT',
tooltip: 'Optional, and only relevant if "List this article ..." above is checked.'
}
] : []);
// Tags arranged by category; will be used to generate the alphabetical list,
// but tags should be in alphabetical order within the categories
// excludeMI: true indicate a tag that *does not* work inside {{multiple issues}}
// Add new categories with discretion - the list is long enough as is!
Twinkle.tag.article.tagList = {
'Cleanup and maintenance tags': {
'General cleanup': [
{
tag: 'Cleanup', description: 'requires cleanup',
subgroup: {
name: 'cleanup',
parameter: 'reason',
type: 'input',
label: 'Specific reason why cleanup is needed:',
tooltip: 'Required.',
size: 35,
required: true
}
}, // has a subgroup with text input
{
tag: 'Cleanup rewrite',
description: "needs to be rewritten entirely to comply with Wikipedia's quality standards"
},
{
tag: 'Copy edit',
description: 'requires copy editing for grammar, style, cohesion, tone, or spelling',
subgroup: {
name: 'copyEdit',
parameter: 'for',
type: 'input',
label: '"This article may require copy editing for..."',
tooltip: 'e.g. "consistent spelling". Optional.',
size: 35
}
} // has a subgroup with text input
],
'Potentially unwanted content': [
{
tag: 'Close paraphrasing',
description: 'contains close paraphrasing of a non-free copyrighted source',
subgroup: {
name: 'closeParaphrasing',
parameter: 'source',
type: 'input',
label: 'Source:',
tooltip: 'Source that has been closely paraphrased'
}
},
{
tag: 'Copypaste',
description: 'appears to have been copied and pasted from another location',
excludeMI: true,
subgroup: {
name: 'copypaste',
parameter: 'url',
type: 'input',
label: 'Source URL:',
tooltip: 'If known.',
size: 50
}
}, // has a subgroup with text input
{
tag: 'AI-generated',
description: 'content appears to be generated by a large language model',
subgroup: [
{
name: 'llmReason',
parameter: 'reason',
type: 'input',
label: 'Reason:',
tooltip: 'A short explanation to be added to the tag. Optional but recommended to fill in either Reason or Talk discussion.'
},
{
name: 'llmTalk',
parameter: 'talk',
type: 'input',
label: 'Talk discussion:',
tooltip: 'Name of the section of this article\'s talk page where the issue is being discussed. Do not give a link, just the name of the section. Optional but recommended to fill in either Reason or Talk discussion.'
}
]
},
{ tag: 'External links', description: 'external links may not follow content policies or guidelines' },
{ tag: 'Non-free', description: 'may contain excessive or improper use of copyrighted materials' }
],
'Structure, formatting, and lead section': [
{ tag: 'Cleanup reorganize', description: "needs reorganization to comply with Wikipedia's layout guidelines" },
{ tag: 'Lead missing', description: 'no lead section' },
{ tag: 'Lead rewrite', description: 'lead section needs to be rewritten to comply with guidelines' },
{ tag: 'Lead too long', description: 'lead section is too long for the length of the article' },
{ tag: 'Lead too short', description: 'lead section is too short and should be expanded to summarize key points' },
{ tag: 'Sections', description: 'needs to be divided into sections by topic' },
{ tag: 'Too many sections', description: 'too many section headers dividing up content, should be condensed' },
{ tag: 'Very long', description: 'too long to read and navigate comfortably' }
],
'Fiction-related cleanup': [
{ tag: 'All plot', description: 'almost entirely a plot summary' },
{ tag: 'Fiction', description: 'fails to distinguish between fact and fiction' },
{ tag: 'In-universe', description: 'subject is fictional and needs rewriting to provide a non-fictional perspective' },
{ tag: 'Long plot', description: 'plot summary is too long or excessively detailed' },
{ tag: 'More plot', description: 'plot summary is too short' },
{ tag: 'No plot', description: 'needs a plot summary' }
]
},
'General content issues': {
'Importance and notability': [
{ tag: 'Notability', description: 'subject may not meet the general notability guideline',
subgroup: {
name: 'notability',
parameter: '1',
type: 'select',
list: [
{ label: "{{notability}}: article's subject may not meet the general notability guideline", value: '' },
{ label: '{{notability|Academics}}: notability guideline for academics', value: 'Academics' },
{ label: '{{notability|Astro}}: notability guideline for astronomical objects', value: 'Astro' },
{ label: '{{notability|Biographies}}: notability guideline for biographies', value: 'Biographies' },
{ label: '{{notability|Books}}: notability guideline for books', value: 'Books' },
{ label: '{{notability|Companies}}: notability guideline for companies', value: 'Companies' },
{ label: '{{notability|Events}}: notability guideline for events', value: 'Events' },
{ label: '{{notability|Films}}: notability guideline for films', value: 'Films' },
{ label: '{{notability|Geographic}}: notability guideline for geographic features', value: 'Geographic' },
{ label: '{{notability|Lists}}: notability guideline for stand-alone lists', value: 'Lists' },
{ label: '{{notability|Music}}: notability guideline for music', value: 'Music' },
{ label: '{{notability|Neologisms}}: notability guideline for neologisms', value: 'Neologisms' },
{ label: '{{notability|Numbers}}: notability guideline for numbers', value: 'Numbers' },
{ label: '{{notability|Organizations}}: notability guideline for organizations', value: 'Organizations' },
{ label: '{{notability|Products}}: notability guideline for products and services', value: 'Products' },
{ label: '{{notability|Sports}}: notability guideline for sports and athletics', value: 'Sports' },
{ label: '{{notability|Television}}: notability guideline for television shows', value: 'Television' },
{ label: '{{notability|Web}}: notability guideline for web content', value: 'Web' }
]
}
}
],
'Style of writing': [
{ tag: 'Cleanup press release', description: 'reads like a press release or news article',
subgroup: {
type: 'hidden',
name: 'cleanupPR1',
parameter: '1',
value: 'article'
}
},
{ tag: 'Cleanup tense', description: 'does not follow guidelines on use of different tenses.' },
{ tag: 'Essay-like', description: 'written like a personal reflection, personal essay, or argumentative essay' },
{ tag: 'Fanpov', description: "written from a fan's point of view" },
{ tag: 'Inappropriate person', description: 'uses first-person or second-person inappropiately' },
{ tag: 'How-to', description: 'written like a manual or guidebook' },
{ tag: 'Over-quotation', description: 'too many or too-lengthy quotations for an encyclopedic entry' },
{ tag: 'Promotional', description: 'contains promotional content or is written like an advertisement' },
{ tag: 'Prose', description: 'written in a list format but may read better as prose' },
{ tag: 'Resume-like', description: 'written like a resume' },
{ tag: 'Technical', description: 'too technical for most readers to understand' },
{ tag: 'Tone', description: 'tone or style may not reflect the encyclopedic tone used on Wikipedia' }
],
'Sense (or lack thereof)': [
{ tag: 'Confusing', description: 'confusing or unclear' },
{ tag: 'Unfocused', description: 'lacks focus or is about more than one topic' }
],
'Information and detail': [
{ tag: 'Context', description: 'insufficient context for those unfamiliar with the subject' },
{ tag: 'Excessive examples', description: 'may contain indiscriminate, excessive, or irrelevant examples' },
{ tag: 'Expert needed', description: 'needs attention from an expert on the subject',
subgroup: [
{
name: 'expertNeeded',
parameter: '1',
type: 'input',
label: 'Name of relevant WikiProject:',
tooltip: 'Optionally, enter the name of a WikiProject which might be able to help recruit an expert. Don\'t include the "WikiProject" prefix.'
},
{
name: 'expertNeededReason',
parameter: 'reason',
type: 'input',
label: 'Reason:',
tooltip: 'Short explanation describing the issue. Either Reason or Talk link is required.'
},
{
name: 'expertNeededTalk',
parameter: 'talk',
type: 'input',
label: 'Talk discussion:',
tooltip: 'Name of the section of this article\'s talk page where the issue is being discussed. Do not give a link, just the name of the section. Either Reason or Talk link is required.'
}
]
},
{ tag: 'Overly detailed', description: 'excessive amount of intricate detail' },
{ tag: 'Undue weight', description: 'lends undue weight to certain ideas, incidents, or controversies' }
],
Timeliness: [
{ tag: 'Current', description: 'documents a current event', excludeMI: true }, // Works but not intended for use in MI
{ tag: 'Current related', description: 'documents a topic affected by a current event', excludeMI: true }, // Works but not intended for use in MI
{ tag: 'Update', description: 'needs additional up-to-date information added',
subgroup: [
{
name: 'updatePart',
parameter: 'part',
type: 'input',
label: 'What part of the article:',
tooltip: 'Part that needs updating',
size: '45'
},
{
name: 'updateReason',
parameter: 'reason',
type: 'input',
label: 'Reason:',
tooltip: 'Explanation why the article is out of date',
size: '55'
}
]
}
],
'Neutrality, bias, and factual accuracy': [
{ tag: 'Autobiography', description: 'autobiography and may not be written neutrally' },
{ tag: 'COI', description: 'creator or major contributor may have a conflict of interest', subgroup: mw.config.get('wgNamespaceNumber') === 0 ? {
name: 'coiReason',
type: 'textarea',
label: 'Explanation for COI tag (will be posted on this article\'s talk page):',
tooltip: 'Optional, but strongly recommended. Leave blank if not wanted.'
} : [] },
{ tag: 'Disputed', description: 'questionable factual accuracy' },
{ tag: 'Fringe theories', description: 'presents fringe theories as mainstream views' },
{ tag: 'Globalize', description: 'may not represent a worldwide view of the subject',
subgroup: [
{
type: 'hidden',
name: 'globalize1',
parameter: '1',
value: 'article'
}, {
name: 'globalizeRegion',
parameter: '2',
type: 'input',
label: 'Over-represented country or region'
}
]
},
{ tag: 'Hoax', description: 'may partially or completely be a hoax' },
{ tag: 'Paid contributions', description: 'contains paid contributions, and may therefore require cleanup' },
{ tag: 'Peacock', description: 'contains wording that promotes the subject in a subjective manner without adding information' },
{ tag: 'POV', description: 'does not maintain a neutral point of view' },
{ tag: 'Recentism', description: 'slanted towards recent events' },
{ tag: 'Too few opinions', description: 'may not include all significant viewpoints' },
{ tag: 'Undisclosed paid', description: 'may have been created or edited in return for undisclosed payments' },
{ tag: 'Weasel', description: 'neutrality or verifiability is compromised by the use of weasel words' }
],
'Verifiability and sources': [
{ tag: 'BLP no footnotes', description: 'BLP that lacks inline citations'},
{ tag: 'BLP one source', description: 'BLP that relies largely or entirely on a single source' },
{ tag: 'BLP sources', description: 'BLP that needs additional references or sources for verification' },
{ tag: 'BLP unreferenced', description: 'BLP does not cite any sources at all (use BLP PROD instead for new articles)' },
{ tag: 'Full citations needed', description: 'needs more complete citations for verification' },
{ tag: 'More citations needed', description: 'needs additional references or sources for verification' },
{ tag: 'No footnotes', description: 'has references, but lacks inline citations' },
{ tag: 'No significant coverage', description: 'does not cite any sources containing significant coverage' },
{ tag: 'No significant coverage (sports)', description: 'sports biography that does not cite any sources containing significant coverage' },
{ tag: 'One source', description: 'relies largely or entirely on a single source' },
{ tag: 'Only primary sources', description: 'relies only on references to primary sources, and needs secondary sources' },
{ tag: 'Original research', description: 'contains original research' },
{ tag: 'Primary sources', description: 'relies too much on references to primary sources, and needs secondary sources' },
{ tag: 'Self-published', description: 'contains excessive or inappropriate references to self-published sources' },
{ tag: 'Sources exist', description: 'notable topic, sources are available that could be added to article' },
{ tag: 'Third-party', description: 'relies too heavily on sources too closely associated with the subject' },
{ tag: 'Unreferenced', description: 'does not cite any sources at all' },
{ tag: 'Unreliable sources', description: 'some references may not be reliable' },
{ tag: 'User-generated', description: 'contains many references to user-generated (self-published) content'}
]
},
'Specific content issues': {
Accessibility: [
{ tag: 'Cleanup colors', description: 'uses color as only way to convey information' },
{ tag: 'Overcoloured', description: 'overuses color' },
{ tag: 'Dark mode problems', description: 'has problems when viewed in dark mode' }
],
Language: [
{ tag: 'Not English', description: 'written in a language other than English and needs translation',
excludeMI: true,
subgroup: translationSubgroups.slice(0, 1).concat([{
type: 'checkbox',
list: [
{
name: 'translationNotify',
label: 'Notify article creator',
checked: true,
tooltip: "Places {{uw-notenglish}} on the creator's talk page."
}
]
}]).concat(translationSubgroups.slice(1))
},
{ tag: 'Rough translation', description: 'poor translation from another language', excludeMI: true,
subgroup: translationSubgroups
},
{ tag: 'Expand language', description: 'should be expanded with text translated from a foreign-language article',
excludeMI: true,
subgroup: [{
type: 'hidden',
name: 'expandLangTopic',
parameter: 'topic',
value: '',
required: true // force empty topic param in output
}, {
name: 'expandLanguageLangCode',
parameter: 'langcode',
type: 'input',
label: 'Language code:',
tooltip: 'Language code of the language from which article is to be expanded from',
required: true
}, {
name: 'expandLanguageArticle',
parameter: 'otherarticle',
type: 'input',
label: 'Name of article:',
tooltip: 'Name of article to be expanded from, without the interwiki prefix'
}]
}
],
Links: [
{ tag: 'Dead end', description: 'article has no links to other articles' },
{ tag: 'Orphan', description: 'linked to from no other articles' },
{ tag: 'Overlinked', description: 'too many duplicate and/or irrelevant links to other articles' },
{ tag: 'Underlinked', description: 'needs more wikilinks to other articles' }
],
'Referencing technique': [
{ tag: 'Citation style', description: 'unclear or inconsistent citation style' },
{ tag: 'Cleanup bare URLs', description: 'uses bare URLs for references, which are prone to link rot' },
{ tag: 'More footnotes needed', description: 'has some references, but insufficient inline citations' },
{ tag: 'Parenthetical referencing', description: 'uses parenthetical referencing, which is deprecated on Wikipedia' }
],
Categories: [
{ tag: 'Improve categories', description: 'needs additional or more specific categories', excludeMI: true },
{ tag: 'Uncategorized', description: 'not added to any categories', excludeMI: true }
]
},
Merging: [
{
tag: 'History merge',
description: 'another page should be history merged into this one',
excludeMI: true,
subgroup: [
{
name: 'histmergeOriginalPage',
parameter: 'originalpage',
type: 'input',
label: 'Other article:',
tooltip: 'Name of the page that should be merged into this one (required).',
required: true
},
{
name: 'histmergeReason',
parameter: 'reason',
type: 'input',
label: 'Reason:',
tooltip: 'Short explanation describing the reason a history merge is needed. Should probably begin with "because" and end with a period.'
},
{
name: 'histmergeSysopDetails',
parameter: 'details',
type: 'input',
label: 'Extra details:',
tooltip: 'For complex cases, provide extra instructions for the reviewing administrator.'
}
]
}
],
Splitting: [
{ tag: 'Split', description: 'should be split into multiple pages' },
{ tag: 'Split dab', description: 'disambiguation page should be split into multiple pages' }
],
Informational: [
{ tag: 'GOCEinuse', description: 'currently undergoing a major copy edit by the Guild of Copy Editors', excludeMI: true },
{ tag: 'In use', description: 'undergoing a major edit for a short while', excludeMI: true },
{ tag: 'Under construction', description: 'in the process of an expansion or major restructuring', excludeMI: true }
]
};
// Tags for REDIRECTS start here
// Not by policy, but the list roughly approximates items with >500
// transclusions from Template:R template index
Twinkle.tag.redirectList = {
'Grammar, punctuation, and spelling': {
Abbreviation: [
{ tag: 'R from acronym', description: 'redirect from an acronym (e.g. POTUS) to its expanded form', restriction: 'insideMainspaceOnly' },
{ tag: 'R from airport code', description: 'redirect from an airport\'s IATA or ICAO code to that airport\'s article', restriction: 'insideMainspaceOnly' },
{ tag: 'R from airline code', description: 'redirect from an airline\'s IATA or ICAO code to that airline\'s article', restriction: 'insideMainspaceOnly' },
{ tag: 'R from initialism', description: 'redirect from an initialism (e.g. AGF) to its expanded form', restriction: 'insideMainspaceOnly' },
{ tag: 'R from MathSciNet abbreviation', description: 'redirect from MathSciNet publication title abbreviation to the unabbreviated title', restriction: 'insideMainspaceOnly' },
{ tag: 'R from NLM abbreviation', description: 'redirect from a NLM publication title abbreviation to the unabbreviated title', restriction: 'insideMainspaceOnly' }
],
Capitalisation: [
{ tag: 'R from CamelCase', description: 'redirect from a CamelCase title' },
{ tag: 'R from other capitalisation', description: 'redirect from a title with another method of capitalisation', restriction: 'insideMainspaceOnly' },
{ tag: 'R from miscapitalisation', description: 'redirect from a capitalisation error' }
],
'Grammar & punctuation': [
{ tag: 'R from modification', description: 'redirect from a modification of the target\'s title, such as with words rearranged' },
{ tag: 'R from plural', description: 'redirect from a plural word to the singular equivalent', restriction: 'insideMainspaceOnly' },
{ tag: 'R to plural', description: 'redirect from a singular noun to its plural form', restriction: 'insideMainspaceOnly' }
],
'Parts of speech': [
{ tag: 'R from verb', description: 'redirect from an English-language verb or verb phrase', restriction: 'insideMainspaceOnly' },
{ tag: 'R from adjective', description: 'redirect from an adjective (word or phrase that describes a noun)', restriction: 'insideMainspaceOnly' }
],
Spelling: [
{ tag: 'R from alternative spelling', description: 'redirect from a title with a different spelling' },
{ tag: 'R from alternative transliteration', description: 'redirect from an alternative English transliteration to a more common variation' },
{ tag: 'R from ASCII-only', description: 'redirect from a title in only basic ASCII to the formal title, with differences that are not diacritical marks or ligatures' },
{ tag: 'R to ASCII-only', description: 'redirect to a title in only basic ASCII from the formal title, with differences that are not diacritical marks or ligatures' },
{ tag: 'R from diacritic', description: 'redirect from a page name that has diacritical marks (accents, umlauts, etc.)' },
{ tag: 'R to diacritic', description: 'redirect to the article title with diacritical marks (accents, umlauts, etc.)' },
{ tag: 'R from misspelling', description: 'redirect from a misspelling or typographical error' }
]
},
'Alternative names': {
General: [
{
tag: 'R from alternative language',
description: 'redirect from or to a title in another language',
subgroup: [
{
name: 'altLangFrom',
type: 'input',
label: 'From language (two-letter code):',
tooltip: 'Enter the two-letter code of the language the redirect name is in; such as en for English, de for German'
},
{
name: 'altLangTo',
type: 'input',
label: 'To language (two-letter code):',
tooltip: 'Enter the two-letter code of the language the target name is in; such as en for English, de for German'
},
{
name: 'altLangInfo',
type: 'div',
label: $.parseHTML('<p>For a list of language codes, see <a href="/wiki/Wp:Template_messages/Redirect_language_codes">Wikipedia:Template messages/Redirect language codes</a></p>')
}
]
},
{ tag: 'R from alternative name', description: 'redirect from a title that is another name, a pseudonym, a nickname, or a synonym' },
{ tag: 'R from ambiguous sort name', description: 'redirect from an ambiguous sort name to a page or list that disambiguates it' },
{ tag: 'R from former name', description: 'redirect from a former or historic name or a working title', restriction: 'insideMainspaceOnly' },
{ tag: 'R from incomplete name', description: 'R from incomplete name' },
{ tag: 'R from incorrect name', description: 'redirect from an erroneus name that is unsuitable as a title' },
{ tag: 'R from less specific name', description: 'redirect from a less specific title to a more specific, less general one' },
{ tag: 'R from long name', description: 'redirect from a more complete title' },
{ tag: 'R from more specific name', description: 'redirect from a more specific title to a less specific, more general one' },
{ tag: 'R from non-neutral name', description: 'redirect from a title that contains a non-neutral, pejorative, controversial, or offensive word, phrase, or name' },
{ tag: 'R from short name', description: 'redirect from a title that is a shortened form of a person\'s full name, a book title, or other more complete title' },
{ tag: 'R from sort name', description: 'redirect from the target\'s sort name, such as beginning with their surname rather than given name', restriction: 'insideMainspaceOnly' },
{ tag: 'R from synonym', description: 'redirect from a semantic synonym of the target page title' }
],
People: [
{ tag: 'R from birth name', description: 'redirect from a person\'s birth name to a more common name', restriction: 'insideMainspaceOnly' },
{ tag: 'R from given name', description: 'redirect from a person\'s given name', restriction: 'insideMainspaceOnly' },
{ tag: 'R from married name', description: 'redirect from a person\'s married name to a more common name', restriction: 'insideMainspaceOnly' },
{ tag: 'R from name with title', description: 'redirect from a person\'s name preceded or followed by a title to the name with no title or with the title in parentheses', restriction: 'insideMainspaceOnly' },
{ tag: 'R from person', description: 'redirect from a person or persons to a related article', restriction: 'insideMainspaceOnly' },
{ tag: 'R from personal name', description: 'redirect from an individual\'s personal name to an article titled with their professional or other better known moniker', restriction: 'insideMainspaceOnly' },
{ tag: 'R from pseudonym', description: 'redirect from a pseudonym', restriction: 'insideMainspaceOnly' },
{ tag: 'R from surname', description: 'redirect from a title that is a surname', restriction: 'insideMainspaceOnly' }
],
Technical: [
{ tag: 'R from drug trade name', description: 'redirect from (or to) the trade name of a drug to (or from) the international nonproprietary name (INN)' },
{ tag: 'R from filename', description: 'redirect from a title that is a filename of the target', restriction: 'insideMainspaceOnly' },
{ tag: 'R from molecular formula', description: 'redirect from a molecular/chemical formula to its technical or trivial name' },
{ tag: 'R from gene symbol', description: 'redirect from a Human Genome Organisation (HUGO) symbol for a gene to an article about the gene', restriction: 'insideMainspaceOnly' }
],
Organisms: [
{ tag: 'R to scientific name', description: 'redirect from the common name to the scientific name', restriction: 'insideMainspaceOnly' },
{ tag: 'R from scientific name', description: 'redirect from the scientific name to the common name', restriction: 'insideMainspaceOnly' },
{ tag: 'R from alternative scientific name', description: 'redirect from an alternative scientific name to the accepted scientific name', restriction: 'insideMainspaceOnly' },
{ tag: 'R from scientific abbreviation', description: 'redirect from a scientific abbreviation', restriction: 'insideMainspaceOnly' },
{ tag: 'R to monotypic taxon', description: 'redirect from the only lower-ranking member of a monotypic taxon to its monotypic taxon', restriction: 'insideMainspaceOnly' },
{ tag: 'R from monotypic taxon', description: 'redirect from a monotypic taxon to its only lower-ranking member', restriction: 'insideMainspaceOnly' },
{ tag: 'R taxon with possibilities', description: 'redirect from a title related to a living organism that potentially could be expanded into an article', restriction: 'insideMainspaceOnly' }
],
Geography: [
{ tag: 'R from name and country', description: 'redirect from the specific name to the briefer name', restriction: 'insideMainspaceOnly' },
{ tag: 'R from more specific geographic name', description: 'redirect from a geographic location that includes extraneous identifiers such as the county or region of a city', restriction: 'insideMainspaceOnly' }
]
},
'Navigation aids': {
Navigation: [
{ tag: 'R to anchor', description: 'redirect from a topic that does not have its own page to an anchored part of a page on the subject' },
{
tag: 'R avoided double redirect',
description: 'redirect from an alternative title for another redirect',
subgroup: {
name: 'doubleRedirectTarget',
type: 'input',
label: 'Redirect target name',
tooltip: 'Enter the page this redirect would target if the page wasn\'t also a redirect'
}
},
{ tag: 'R from file metadata link', description: 'redirect of a wikilink created from EXIF, XMP, or other information (i.e. the "metadata" section on some image description pages)', restriction: 'insideMainspaceOnly' },
{ tag: 'R to list entry', description: 'redirect to a list which contains brief descriptions of subjects not notable enough to have separate articles', restriction: 'insideMainspaceOnly' },
{ tag: 'R mentioned in hatnote', description: 'redirect from a title that is mentioned in a hatnote at the redirect target' },
{ tag: 'R to section', description: 'similar to {{R to list entry}}, but when list is organized in sections, such as list of characters in a fictional universe' },
{ tag: 'R from shortcut', description: 'redirect from a Wikipedia shortcut' },
{ tag: 'R to subpage', description: 'redirect to a subpage' }
],
Disambiguation: [
{ tag: 'R from ambiguous term', description: 'redirect from an ambiguous page name to a page that disambiguates it. This template should never appear on a page that has "(disambiguation)" in its title, use R to disambiguation page instead' },
{ tag: 'R to disambiguation page', description: 'redirect to a disambiguation page', restriction: 'disambiguationPagesOnly' },
{ tag: 'R from incomplete disambiguation', description: 'redirect from a page name that is too ambiguous to be the title of an article and should redirect to an appropriate disambiguation page' },
{ tag: 'R from incorrect disambiguation', description: 'redirect from a page name with incorrect disambiguation due to an error or previous editorial misconception' },
{ tag: 'R from other disambiguation', description: 'redirect from a page name with an alternative disambiguation qualifier' },
{ tag: 'R from unnecessary disambiguation', description: 'redirect from a page name that has an unneeded disambiguation qualifier' }
],
'Merge, duplicate & move': [
{ tag: 'R from duplicated article', description: 'redirect to a similar article in order to preserve its edit history' },
{ tag: 'R with history', description: 'redirect from a page containing substantive page history, kept to preserve content and attributions' },
{ tag: 'R from move', description: 'redirect from a page that has been moved/renamed' },
{ tag: 'R from merge', description: 'redirect from a merged page in order to preserve its edit history' }
],
Namespace: [
{ tag: 'R from remote talk page', description: 'redirect from a talk page in any talk namespace to a corresponding page that is more heavily watched', restriction: 'insideTalkNamespaceOnly' },
{ tag: 'R to category namespace', description: 'redirect from a page outside the category namespace to a category page' },
{ tag: 'R to help namespace', description: 'redirect from any page inside or outside of help namespace to a page in that namespace' },
{ tag: 'R to main namespace', description: 'redirect from a page outside the main-article namespace to an article in mainspace' },
{ tag: 'R to portal namespace', description: 'redirect from any page inside or outside of portal space to a page in that namespace' },
{ tag: 'R to project namespace', description: 'redirect from any page inside or outside of project (Wikipedia: or WP:) space to any page in the project namespace' },
{ tag: 'R to user namespace', description: 'redirect from a page outside the user namespace to a user page (not to a user talk page)', restriction: 'outsideUserspaceOnly' }
]
},
Media: {
General: [
{ tag: 'R from album', description: 'redirect from an album to a related topic such as the recording artist or a list of albums', restriction: 'insideMainspaceOnly' },
{ tag: 'R from band name', description: 'redirect from a musical band or musical group name that redirects an article on a single person, i.e. the band or group leader' },
{ tag: 'R from book', description: 'redirect from a book title to a more general, relevant article', restriction: 'insideMainspaceOnly' },
{ tag: 'R from cover song', description: 'redirect from a cover version of a song to the article about the original song this version covers' },
{ tag: 'R from film', description: 'redirect from a film title that is a subtopic of the redirect target or a title in an alternative language that has been produced in that language', restriction: 'insideMainspaceOnly' },
{ tag: 'R from journal', description: 'redirect from a trade or professional journal article a more general, relevant Wikipedia article, such as the author or publisher of the article or to the title in an alternative language' },
{ tag: 'R from lyric', description: 'redirect from a lyric to a song or other source that describes the lyric' },
{ tag: 'R from meme', description: 'redirect from a name of an internet meme or other pop culture phenomenon that is a subtopic of the redirect target' },
{ tag: 'R from song', description: 'redirect from a song title to a more general, relevant article' },
{ tag: 'R from television episode', description: 'redirect from a television episode title to a related work or lists of episodes', restriction: 'insideMainspaceOnly' },
{ tag: 'R from television program', description: 'redirect from a title of television program, television series or web series that is a subtopic of the redirect target' },
{ tag: 'R from upcoming film', description: 'redirect from a title that potentially could be expanded into a new article or other type of associated page such as a new template.' },
{ tag: 'R from work', description: 'redirect from a creative work a related topic such as the author/artist, publisher, or a subject related to the work' }
],
Fiction: [
{ tag: 'R from fictional character', description: 'redirect from a fictional character to a related fictional work or list of characters', restriction: 'insideMainspaceOnly' },
{ tag: 'R from fictional element', description: 'redirect from a fictional element (such as an object or concept) to a related fictional work or list of similar elements', restriction: 'insideMainspaceOnly' },
{ tag: 'R from fictional location', description: 'redirect from a fictional location or setting to a related fictional work or list of places', restriction: 'insideMainspaceOnly' }
]
},
Miscellaneous: {
'Related information': [
{ tag: 'R to article without mention', description: 'redirect to an article without any mention of the redirected word or phrase', restriction: 'insideMainspaceOnly' },
{ tag: 'R from company name', description: 'redirect from a company name to a related article', restriction: 'insideMainspaceOnly' },
{ tag: 'R to decade', description: 'redirect from a year to the decade article', restriction: 'insideMainspaceOnly' },
{ tag: 'R from domain name', description: 'redirect from a domain name to an article about a website', restriction: 'insideMainspaceOnly' },
{ tag: 'R from emoji', description: 'redirect from an emoji to an article describing the depicted concept or the emoji itself' },
{ tag: 'R from phrase', description: 'redirect from a phrase to a more general relevant article covering the topic' },
{ tag: 'R from list topic', description: 'redirect from the topic of a list to the equivalent list' },
{ tag: 'R from member', description: 'redirect from a member of a group to a related topic such as the group or organization' },
{ tag: 'R to related topic', description: 'redirect to an article about a similar topic', restriction: 'insideMainspaceOnly' },
{ tag: 'R from related word', description: 'redirect from a related word' },
{ tag: 'R from school', description: 'redirect from a school article that had very little information', restriction: 'insideMainspaceOnly' },
{ tag: 'R from subtopic', description: 'redirect from a title that is a subtopic of the target article', restriction: 'insideMainspaceOnly' },
{ tag: 'R to subtopic', description: 'redirect to a subtopic of the redirect\'s title', restriction: 'insideMainspaceOnly' },
{ tag: 'R from Unicode character', description: 'redirect from a single Unicode character to an article or Wikipedia project page that infers meaning for the symbol', restriction: 'insideMainspaceOnly' },
{ tag: 'R from Unicode code', description: 'redirect from a Unicode code point to an article about the character it represents', restriction: 'insideMainspaceOnly' }
],
'With possibilities': [
{ tag: 'R with possibilities', description: 'redirect from a specific title to a more general, less detailed article (something which can and should be expanded)' }
],
'ISO codes': [
{ tag: 'R from ISO 4 abbreviation', description: 'redirect from an ISO 4 publication title abbreviation to the unabbreviated title', restriction: 'insideMainspaceOnly' },
{ tag: 'R from ISO 639 code', description: 'redirect from a title that is an ISO 639 language code to an article about the language', restriction: 'insideMainspaceOnly' }
],
Printworthiness: [
{ tag: 'R printworthy', description: 'redirect from a title that would be helpful in a printed or CD/DVD version of Wikipedia', restriction: 'insideMainspaceOnly' },
{ tag: 'R unprintworthy', description: 'redirect from a title that would NOT be helpful in a printed or CD/DVD version of Wikipedia', restriction: 'insideMainspaceOnly' }
]
}
};
// maintenance tags for FILES start here
Twinkle.tag.fileList = {
'License and sourcing problem tags': [
{ label: '{{Better source requested}}: source info consists of bare image URL/generic base URL only', value: 'Better source requested' },
{ label: '{{Maybe free media}}: currently tagged under non-free license, but free license may be available ', value: 'Maybe free media' },
{ label: '{{Non-free reduce}}: non-low-resolution fair use image (or too-long audio clip, etc)', value: 'Non-free reduce' },
{ label: '{{Orphaned non-free revisions}}: fair use media with old revisions that need to be deleted', value: 'Orphaned non-free revisions' }
],
'Wikimedia Commons-related tags': [
{ label: '{{Copy to Commons}}: free media that should be copied to Commons', value: 'Copy to Commons' },
{
label: '{{Deleted on Commons}}: file has previously been deleted from Commons',
value: 'Deleted on Commons',
subgroup: {
type: 'input',
name: 'deletedOnCommonsName',
label: 'Name on Commons:',
tooltip: 'Name of the image on Commons (if different from local name), excluding the File: prefix'
}
},
{
label: '{{Do not move to Commons}}: file not suitable for moving to Commons',
value: 'Do not move to Commons',
subgroup: [
{
type: 'input',
name: 'DoNotMoveToCommons_reason',
label: 'Reason:',
tooltip: 'Enter the reason why this image should not be moved to Commons (required). If the file is PD in the US but not in country of origin, enter "US only"',
required: true
},
{
type: 'number',
name: 'DoNotMoveToCommons_expiry',
label: 'Expiration year:',
min: new Morebits.Date().getFullYear(),
tooltip: 'If this file can be moved to Commons beginning in a certain year, you can enter it here (optional).'
}
]
},
{
label: '{{Keep local}}: request to keep local copy of a Commons file',
value: 'Keep local',
subgroup: {
type: 'input',
name: 'keeplocalName',
label: 'Commons image name if different:',
tooltip: 'Name of the image on Commons (if different from local name), excluding the File: prefix:'
}
},
{
label: '{{Nominated for deletion on Commons}}: file is nominated for deletion on Commons',
value: 'Nominated for deletion on Commons',
subgroup: {
type: 'input',
name: 'nominatedOnCommonsName',
label: 'Name on Commons:',
tooltip: 'Name of the image on Commons (if different from local name), excluding the File: prefix:'
}
}
],
'Cleanup tags': [
{ label: '{{Artifacts}}: PNG contains residual compression artifacts', value: 'Artifacts' },
{ label: '{{Bad font}}: SVG uses fonts not available on the thumbnail server', value: 'Bad font' },
{ label: '{{Bad format}}: PDF/DOC/... file should be converted to a more useful format', value: 'Bad format' },
{ label: '{{Bad GIF}}: GIF that should be PNG, JPEG, or SVG', value: 'Bad GIF' },
{ label: '{{Bad JPEG}}: JPEG that should be PNG or SVG', value: 'Bad JPEG' },
{ label: '{{Bad SVG}}: SVG with a mix of raster and vector graphics', value: 'Bad SVG' },
{ label: '{{Bad trace}}: auto-traced SVG requiring cleanup', value: 'Bad trace' },
{
label: '{{Cleanup image}}: general cleanup', value: 'Cleanup image',
subgroup: {
type: 'input',
name: 'cleanupimageReason',
label: 'Reason:',
tooltip: 'Enter the reason for cleanup (required)',
required: true
}
},
{ label: '{{ClearType}}: image (not screenshot) with ClearType anti-aliasing', value: 'ClearType' },
{ label: '{{Fake SVG}}: SVG solely containing raster graphics without true vector content', value: 'Fake SVG' },
{ label: '{{Imagewatermark}}: image contains visible or invisible watermarking', value: 'Imagewatermark' },
{ label: '{{NoCoins}}: image using coins to indicate scale', value: 'NoCoins' },
{ label: '{{Overcompressed JPEG}}: JPEG with high levels of artifacts', value: 'Overcompressed JPEG' },
{ label: '{{Opaque}}: opaque background should be transparent', value: 'Opaque' },
{ label: '{{Remove border}}: unneeded border, white space, etc.', value: 'Remove border' },
{
label: '{{Rename media}}: file should be renamed according to the criteria at [[WP:FMV]]',
value: 'Rename media',
subgroup: [
{
type: 'input',
name: 'renamemediaNewname',
label: 'New name:',
tooltip: 'Enter the new name for the image (optional)'
},
{
type: 'input',
name: 'renamemediaReason',
label: 'Reason:',
tooltip: 'Enter the reason for the rename (optional)'
}
]
},
{ label: '{{Should be PNG}}: GIF or JPEG should be lossless', value: 'Should be PNG' },
{
label: '{{Should be SVG}}: PNG, GIF or JPEG should be vector graphics', value: 'Should be SVG',
subgroup: {
name: 'svgCategory',
type: 'select',
list: [
{ label: '{{Should be SVG|other}}', value: 'other' },
{ label: '{{Should be SVG|alphabet}}: character images, font examples, etc.', value: 'alphabet' },
{ label: '{{Should be SVG|chemical}}: chemical diagrams, etc.', value: 'chemical' },
{ label: '{{Should be SVG|circuit}}: electronic circuit diagrams, etc.', value: 'circuit' },
{ label: '{{Should be SVG|coat of arms}}: coats of arms', value: 'coat of arms' },
{ label: '{{Should be SVG|diagram}}: diagrams that do not fit any other subcategory', value: 'diagram' },
{ label: '{{Should be SVG|emblem}}: emblems, free/libre logos, insignias, etc.', value: 'emblem' },
{ label: '{{Should be SVG|fair use}}: fair-use images, fair-use logos', value: 'fair use' },
{ label: '{{Should be SVG|flag}}: flags', value: 'flag' },
{ label: '{{Should be SVG|graph}}: visual plots of data', value: 'graph' },
{ label: '{{Should be SVG|logo}}: logos', value: 'logo' },
{ label: '{{Should be SVG|map}}: maps', value: 'map' },
{ label: '{{Should be SVG|music}}: musical scales, notes, etc.', value: 'music' },
{ label: '{{Should be SVG|physical}}: "realistic" images of physical objects, people, etc.', value: 'physical' },
{ label: '{{Should be SVG|symbol}}: miscellaneous symbols, icons, etc.', value: 'symbol' }
]
}
},
{ label: '{{Should be text}}: image should be represented as text, tables, or math markup', value: 'Should be text' }
],
'Image quality tags': [
{ label: '{{Image hoax}}: Image may be manipulated or constitute a hoax', value: 'Image hoax' },
{ label: '{{Image-blownout}}', value: 'Image-blownout' },
{ label: '{{Image-out-of-focus}}', value: 'Image-out-of-focus' },
{
label: '{{Image-Poor-Quality}}', value: 'Image-Poor-Quality',
subgroup: {
type: 'input',
name: 'ImagePoorQualityReason',
label: 'Reason:',
tooltip: 'Enter the reason why this image is so bad (required)',
required: true
}
},
{ label: '{{Image-underexposure}}', value: 'Image-underexposure' },
{
label: '{{Low quality chem}}: disputed chemical structures', value: 'Low quality chem',
subgroup: {
type: 'input',
name: 'lowQualityChemReason',
label: 'Reason:',
tooltip: 'Enter the reason why the diagram is disputed (required)',
required: true
}
}
],
'Replacement tags': [
{ label: '{{Obsolete}}: improved version available', value: 'Obsolete' },
{ label: '{{PNG version available}}', value: 'PNG version available' },
{ label: '{{Vector version available}}', value: 'Vector version available' }
]
};
Twinkle.tag.fileList['Replacement tags'].forEach((el) => {
el.subgroup = {
type: 'input',
label: 'Replacement file:',
tooltip: 'Enter the name of the file which replaces this one (required)',
name: el.value.replace(/ /g, '_') + 'File',
required: true
};
});
Twinkle.tag.callbacks = {
article: function articleCallback(pageobj) {
// Remove tags that become superfluous with this action
let pageText = pageobj.getPageText().replace(/\{\{\s*([Uu]serspace draft)\s*(\|(?:\{\{[^{}]*\}\}|[^{}])*)?\}\}\s*/g, '');
const params = pageobj.getCallbackParameters();
/**
* Saves the page following the removal of tags if any. The last step.
* Called from removeTags()
*/
const postRemoval = function() {
if (params.tagsToRemove.length) {
// Remove empty {{multiple issues}} if found
pageText = pageText.replace(/\{\{(multiple ?issues|article ?issues|mi)\s*\|\s*\}\}\n?/im, '');
// Remove single-element {{multiple issues}} if found
pageText = pageText.replace(/\{\{(?:multiple ?issues|article ?issues|mi)\s*\|\s*(\{\{[^}]+\}\})\s*\}\}/im, '$1');
}
// Build edit summary
const makeSentence = function(array) {
if (array.length < 3) {
return array.join(' and ');
}
const last = array.pop();
return array.join(', ') + ', and ' + last;
};
const makeTemplateLink = function(tag) {
let text = '{{[[';
// if it is a custom tag with a parameter
if (tag.includes('|')) {
tag = tag.slice(0, tag.indexOf('|'));
}
text += tag.includes(':') ? tag : 'Template:' + tag + '|' + tag;
return text + ']]}}';
};
let summaryText;
const addedTags = params.tags.map(makeTemplateLink);
const removedTags = params.tagsToRemove.map(makeTemplateLink);
if (addedTags.length) {
summaryText = 'Added ' + makeSentence(addedTags);
summaryText += removedTags.length ? '; and removed ' + makeSentence(removedTags) : '';
} else {
summaryText = 'Removed ' + makeSentence(removedTags);
}
summaryText += ' tag' + (addedTags.length + removedTags.length > 1 ? 's' : '');
if (params.reason) {
summaryText += ': ' + params.reason;
}
// avoid truncated summaries
if (summaryText.length > 499) {
summaryText = summaryText.replace(/\[\[[^|]+\|([^\]]+)\]\]/g, '$1');
}
pageobj.setPageText(pageText);
pageobj.setEditSummary(summaryText);
if ((mw.config.get('wgNamespaceNumber') === 0 && Twinkle.getPref('watchTaggedVenues').includes('articles')) || (mw.config.get('wgNamespaceNumber') === 118 && Twinkle.getPref('watchTaggedVenues').includes('drafts'))) {
pageobj.setWatchlist(Twinkle.getPref('watchTaggedPages'));
}
pageobj.setMinorEdit(Twinkle.getPref('markTaggedPagesAsMinor'));
pageobj.setCreateOption('nocreate');
pageobj.save(() => {
// COI: Start the discussion on the talk page (mainspace only)
if (params.coiReason) {
const coiTalkPage = new Morebits.wiki.Page('Talk:' + Morebits.pageNameNorm, 'Starting discussion on talk page');
coiTalkPage.setNewSectionText(params.coiReason + ' ~~~~');
coiTalkPage.setNewSectionTitle('COI tag (' + new Morebits.Date(pageobj.getLoadTime()).format('MMMM Y', 'utc') + ')');
coiTalkPage.setChangeTags(Twinkle.changeTags);
coiTalkPage.setCreateOption('recreate');
coiTalkPage.newSection();
}
// Special functions for {{not English}} and {{rough translation}}
// Post at WP:PNT (mainspace only)
if (params.translationPostAtPNT) {
const pntPage = new Morebits.wiki.Page('Wikipedia:Pages needing translation into English',
'Listing article at Wikipedia:Pages needing translation into English');
pntPage.setFollowRedirect(true);
pntPage.load((pageobj) => {
const oldText = pageobj.getPageText();
const lang = params.translationLanguage;
const reason = params.translationComments;
let templateText;
let text, summary;
if (params.tags.includes('Rough translation')) {
templateText = '{{subst:Dual fluency request|pg=' + Morebits.pageNameNorm + '|Language=' +
(lang || 'uncertain') + '|Comments=' + reason.trim() + '}} ~~~~';
// Place in section == Translated pages that could still use some cleanup ==
text = oldText + '\n\n' + templateText;
summary = 'Translation cleanup requested on ';
} else if (params.tags.includes('Not English')) {
templateText = '{{subst:Translation request|pg=' + Morebits.pageNameNorm + '|Language=' +
(lang || 'uncertain') + '|Comments=' + reason.trim() + '}} ~~~~';
// Place in section == Pages for consideration ==
text = oldText.replace(/\n+(==\s?Translated pages that could still use some cleanup\s?==)/,
'\n\n' + templateText + '\n\n$1');
summary = 'Translation' + (lang ? ' from ' + lang : '') + ' requested on ';
}
if (text === oldText) {
pageobj.getStatusElement().error('failed to find target spot for the discussion');
return;
}
pageobj.setPageText(text);
pageobj.setEditSummary(summary + ' [[:' + Morebits.pageNameNorm + ']]');
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.setCreateOption('recreate');
pageobj.save();
});
}
// Notify the user ({{Not English}} only)
if (params.translationNotify) {
new Morebits.wiki.Page(Morebits.pageNameNorm).lookupCreation((innerPageobj) => {
const initialContrib = innerPageobj.getCreator();
// Disallow warning yourself
if (initialContrib === mw.config.get('wgUserName')) {
innerPageobj.getStatusElement().warn('You (' + initialContrib + ') created this page; skipping user notification');
return;
}
const userTalkPage = new Morebits.wiki.Page('User talk:' + initialContrib,
'Notifying initial contributor (' + initialContrib + ')');
userTalkPage.setNewSectionTitle('Your article [[' + Morebits.pageNameNorm + ']]');
userTalkPage.setNewSectionText('{{subst:uw-notenglish|1=' + Morebits.pageNameNorm +
(params.translationPostAtPNT ? '' : '|nopnt=yes') + '}} ~~~~');
userTalkPage.setEditSummary('Notice: Please use English when contributing to the English Wikipedia.');
userTalkPage.setChangeTags(Twinkle.changeTags);
userTalkPage.setCreateOption('recreate');
userTalkPage.setFollowRedirect(true, false);
userTalkPage.newSection();
});
}
});
if (params.patrol) {
pageobj.triage();
}
};
/**
* Removes the existing tags that were deselected (if any)
* Calls postRemoval() when done
*/
const removeTags = function removeTags() {
if (params.tagsToRemove.length === 0) {
postRemoval();
return;
}
Morebits.Status.info('Info', 'Removing deselected tags that were already present');
const getRedirectsFor = [];
// Remove the tags from the page text, if found in its proper name,
// otherwise moves it to `getRedirectsFor` array earmarking it for
// later removal
params.tagsToRemove.forEach((tag) => {
const tagRegex = new RegExp('\\{\\{' + Morebits.pageNameRegex(tag) + '\\s*(\\|[^}]+)?\\}\\}\\n?');
if (tagRegex.test(pageText)) {
pageText = pageText.replace(tagRegex, '');
} else {
getRedirectsFor.push('Template:' + tag);
}
});
if (!getRedirectsFor.length) {
postRemoval();
return;
}
// Remove tags which appear in page text as redirects
const api = new Morebits.wiki.Api('Getting template redirects', {
action: 'query',
prop: 'linkshere',
titles: getRedirectsFor.join('|'),
redirects: 1, // follow redirect if the class name turns out to be a redirect page
lhnamespace: '10', // template namespace only
lhshow: 'redirect',
lhlimit: 'max', // 500 is max for normal users, 5000 for bots and sysops
format: 'json'
}, ((apiobj) => {
const pages = apiobj.getResponse().query.pages.filter((p) => !p.missing && !!p.linkshere);
pages.forEach((page) => {
let removed = false;
page.linkshere.concat({title: page.title}).forEach((el) => {
const tag = el.title.slice(9);
const tagRegex = new RegExp('\\{\\{' + Morebits.pageNameRegex(tag) + '\\s*(\\|[^}]*)?\\}\\}\\n?');
if (tagRegex.test(pageText)) {
pageText = pageText.replace(tagRegex, '');
removed = true;
return false; // break out of $.each
}
});
if (!removed) {
Morebits.Status.warn('Info', 'Failed to find {{' +
page.title.slice(9) + '}} on the page... excluding');
}
});
postRemoval();
}));
api.post();
};
if (!params.tags.length) {
removeTags();
return;
}
let tagRe, tagText = '', tags = [];
const groupableTags = [], groupableExistingTags = [];
// Executes first: addition of selected tags
/**
* Updates `tagText` with the syntax of `tagName` template with its parameters
*
* @param {number} tagIndex
* @param {string} tagName
*/
const addTag = function articleAddTag(tagIndex, tagName) {
let currentTag = '';
if (tagName === 'Uncategorized' || tagName === 'Improve categories') {
pageText += '\n\n{{' + tagName + '|date={{subst:CURRENTMONTHNAME}} {{subst:CURRENTYEAR}}}}';
} else {
currentTag += '{{' + tagName;
// fill in other parameters, based on the tag
const subgroupObj = Twinkle.tag.article.flatObject[tagName] &&
Twinkle.tag.article.flatObject[tagName].subgroup;
if (subgroupObj) {
const subgroups = Array.isArray(subgroupObj) ? subgroupObj : [subgroupObj];
subgroups.forEach((gr) => {
if (gr.parameter && (params[gr.name] || gr.required)) {
currentTag += '|' + gr.parameter + '=' + (params[gr.name] || '');
}
});
}
switch (tagName) {
case 'Not English':
case 'Rough translation':
if (params.translationPostAtPNT) {
currentTag += '|listed=yes';
}
break;
default:
break;
}
currentTag += '|date={{subst:CURRENTMONTHNAME}} {{subst:CURRENTYEAR}}}}\n';
tagText += currentTag;
}
};
/**
* Adds the tags which go outside {{multiple issues}}, either because
* these tags aren't supported in {{multiple issues}} or because
* {{multiple issues}} is not being added to the page at all
*/
const addUngroupedTags = function() {
$.each(tags, addTag);
// Insert tag after short description or any hatnotes,
// as well as deletion/protection-related templates
const wikipage = new Morebits.wikitext.Page(pageText);
const templatesAfter = Twinkle.hatnoteRegex +
// Protection templates
'pp|pp-.*?|' +
// CSD
'db|delete|db-.*?|speedy deletion-.*?|' +
// PROD
'(?:proposed deletion|prod blp)\\/dated(?:\\s*\\|(?:concern|user|timestamp|help).*)+|' +
// not a hatnote, but sometimes under a CSD or AfD
'salt|proposed deletion endorsed';
// AfD is special, as the tag includes html comments before and after the actual template
// trailing whitespace/newline needed since this subst's a newline
const afdRegex = '(?:<!--.*AfD.*\\n\\{\\{(?:Article for deletion\\/dated|AfDM).*\\}\\}\\n<!--.*(?:\\n<!--.*)?AfD.*(?:\\s*\\n))?';
pageText = wikipage.insertAfterTemplates(tagText, templatesAfter, null, afdRegex).getText();
removeTags();
};
// Separate tags into groupable ones (`groupableTags`) and non-groupable ones (`tags`)
params.tags.forEach((tag) => {
tagRe = new RegExp('\\{\\{' + tag + '(\\||\\}\\})', 'im');
// regex check for preexistence of tag can be skipped if in canRemove mode
if (Twinkle.tag.canRemove || !tagRe.exec(pageText)) {
// condition Twinkle.tag.article.tags[tag] to ensure that its not a custom tag
// Custom tags are assumed non-groupable, since we don't know whether MI template supports them
if (Twinkle.tag.article.flatObject[tag] && !Twinkle.tag.article.flatObject[tag].excludeMI) {
groupableTags.push(tag);
} else {
tags.push(tag);
}
} else {
if (tag === 'History merge') {
tags.push(tag);
} else {
Morebits.Status.warn('Info', 'Found {{' + tag +
'}} on the article already...excluding');
}
}
});
// To-be-retained existing tags that are groupable
params.tagsToRemain.forEach((tag) => {
// If the tag is unknown to us, we consider it non-groupable
if (Twinkle.tag.article.flatObject[tag] && !Twinkle.tag.article.flatObject[tag].excludeMI) {
groupableExistingTags.push(tag);
}
});
const miTest = /\{\{(multiple ?issues|article ?issues|mi)(?!\s*\|\s*section\s*=)[^}]+\{/im.exec(pageText);
if (miTest && groupableTags.length > 0) {
Morebits.Status.info('Info', 'Adding supported tags inside existing {{multiple issues}} tag');
tagText = '';
$.each(groupableTags, addTag);
const miRegex = new RegExp('(\\{\\{\\s*' + miTest[1] + '\\s*(?:\\|(?:\\{\\{[^{}]*\\}\\}|[^{}])*)?)\\}\\}\\s*', 'im');
pageText = pageText.replace(miRegex, '$1' + tagText + '}}\n');
tagText = '';
addUngroupedTags();
} else if (!miTest && (groupableExistingTags.length + groupableTags.length) >= 2) {
Morebits.Status.info('Info', 'Grouping supported tags inside {{multiple issues}}');
tagText += '{{Multiple issues|\n';
/**
* Adds newly added tags to MI
*/
const addNewTagsToMI = function() {
$.each(groupableTags, addTag);
tagText += '}}\n';
addUngroupedTags();
};
const getRedirectsFor = [];
// Reposition the tags on the page into {{multiple issues}}, if found with its
// proper name, else moves it to `getRedirectsFor` array to be handled later
groupableExistingTags.forEach((tag) => {
const tagRegex = new RegExp('(\\{\\{' + Morebits.pageNameRegex(tag) + '\\s*(\\|[^}]+)?\\}\\}\\n?)');
if (tagRegex.test(pageText)) {
tagText += tagRegex.exec(pageText)[1];
pageText = pageText.replace(tagRegex, '');
} else {
getRedirectsFor.push('Template:' + tag);
}
});
if (!getRedirectsFor.length) {
addNewTagsToMI();
return;
}
const api = new Morebits.wiki.Api('Getting template redirects', {
action: 'query',
prop: 'linkshere',
titles: getRedirectsFor.join('|'),
redirects: 1,
lhnamespace: '10', // template namespace only
lhshow: 'redirect',
lhlimit: 'max', // 500 is max for normal users, 5000 for bots and sysops
format: 'json'
}, ((apiobj) => {
const pages = apiobj.getResponse().query.pages.filter((p) => !p.missing && !!p.linkshere);
pages.forEach((page) => {
let found = false;
page.linkshere.forEach((el) => {
const tag = el.title.slice(9);
const tagRegex = new RegExp('(\\{\\{' + Morebits.pageNameRegex(tag) + '\\s*(\\|[^}]*)?\\}\\}\\n?)');
if (tagRegex.test(pageText)) {
tagText += tagRegex.exec(pageText)[1];
pageText = pageText.replace(tagRegex, '');
found = true;
return false; // break out of $.each
}
});
if (!found) {
Morebits.Status.warn('Info', 'Failed to find the existing {{' +
page.title.slice(9) + '}} on the page... skip repositioning');
}
});
addNewTagsToMI();
}));
api.post();
} else {
tags = tags.concat(groupableTags);
addUngroupedTags();
}
},
redirect: function redirect(pageobj) {
const params = pageobj.getCallbackParameters(),
tags = [];
let pageText = pageobj.getPageText(),
tagRe, tagText = '',
summaryText = 'Added',
i;
for (i = 0; i < params.tags.length; i++) {
tagRe = new RegExp('(\\{\\{' + params.tags[i] + '(\\||\\}\\}))', 'im');
if (!tagRe.exec(pageText)) {
tags.push(params.tags[i]);
} else {
Morebits.Status.warn('Info', 'Found {{' + params.tags[i] +
'}} on the redirect already...excluding');
}
}
const addTag = function redirectAddTag(tagIndex, tagName) {
tagText += '\n{{' + tagName;
if (tagName === 'R from alternative language') {
if (params.altLangFrom) {
tagText += '|from=' + params.altLangFrom;
}
if (params.altLangTo) {
tagText += '|to=' + params.altLangTo;
}
} else if (tagName === 'R avoided double redirect' && params.doubleRedirectTarget) {
tagText += '|1=' + params.doubleRedirectTarget;
}
tagText += '}}';
if (tagIndex > 0) {
if (tagIndex === (tags.length - 1)) {
summaryText += ' and';
} else if (tagIndex < (tags.length - 1)) {
summaryText += ',';
}
}
summaryText += ' {{[[:' + (tagName.includes(':') ? tagName : 'Template:' + tagName + '|' + tagName) + ']]}}';
};
if (!tags.length) {
Morebits.Status.warn('Info', 'No tags remaining to apply');
}
tags.sort();
$.each(tags, addTag);
// Check for all Rcat shell redirects (from #433)
if (pageText.match(/{{(?:redr|this is a redirect|r(?:edirect)?(?:.?cat.*)?[ _]?sh|RCS)/i)) {
// Regex inspired by [[User:Kephir/gadgets/sagittarius.js]] ([[Special:PermaLink/831402893]])
const oldTags = pageText.match(/(\s*{{[A-Za-z\s]+\|(?:\s*1=)?)((?:[^|{}]|{{[^}]+}})+)(}})\s*/i);
pageText = pageText.replace(oldTags[0], oldTags[1] + tagText + oldTags[2] + oldTags[3]);
} else {
// Fold any pre-existing Rcats into taglist and under Rcatshell
const pageTags = pageText.match(/\s*{{R(?:edirect)? .*?}}/img);
let oldPageTags = '';
if (pageTags) {
pageTags.forEach((pageTag) => {
const pageRe = new RegExp(Morebits.string.escapeRegExp(pageTag), 'img');
pageText = pageText.replace(pageRe, '');
pageTag = pageTag.trim();
oldPageTags += '\n' + pageTag;
});
}
pageText = pageText.trim() + '\n\n{{Redirect category shell|' + tagText + oldPageTags + '\n}}';
}
summaryText += (tags.length > 0 ? ' tag' + (tags.length > 1 ? 's' : ' ') : ' {{[[Template:Redirect category shell|Redirect category shell]]}}') + ' to redirect';
// avoid truncated summaries
if (summaryText.length > 499) {
summaryText = summaryText.replace(/\[\[[^|]+\|([^\]]+)\]\]/g, '$1');
}
pageobj.setPageText(pageText);
pageobj.setEditSummary(summaryText);
if (Twinkle.getPref('watchTaggedVenues').includes('redirects')) {
pageobj.setWatchlist(Twinkle.getPref('watchTaggedPages'));
}
pageobj.setMinorEdit(Twinkle.getPref('markTaggedPagesAsMinor'));
pageobj.setCreateOption('nocreate');
pageobj.save();
if (params.patrol) {
pageobj.triage();
}
},
file: function twinkletagCallbacksFile(pageobj) {
let text = pageobj.getPageText();
const params = pageobj.getCallbackParameters();
let summary = 'Adding ';
// Add maintenance tags
if (params.tags.length) {
let tagtext = '', currentTag;
$.each(params.tags, (k, tag) => {
// when other commons-related tags are placed, remove "move to Commons" tag
if (['Keep local', 'Do not move to Commons'].includes(tag)) {
text = Twinkle.removeMoveToCommonsTagsFromWikicode(text);
}
currentTag = tag;
switch (tag) {
case 'Keep local':
if (params.keeplocalName !== '') {
currentTag += '|1=' + params.keeplocalName;
}
break;
case 'Rename media':
if (params.renamemediaNewname !== '') {
currentTag += '|1=' + params.renamemediaNewname;
}
if (params.renamemediaReason !== '') {
currentTag += '|2=' + params.renamemediaReason;
}
break;
case 'Cleanup image':
currentTag += '|1=' + params.cleanupimageReason;
break;
case 'Image-Poor-Quality':
currentTag += '|1=' + params.ImagePoorQualityReason;
break;
case 'Image hoax':
currentTag += '|date={{subst:CURRENTMONTHNAME}} {{subst:CURRENTYEAR}}';
break;
case 'Low quality chem':
currentTag += '|1=' + params.lowQualityChemReason;
break;
case 'Vector version available':
text = text.replace(/\{\{((convert to |convertto|should be |shouldbe|to)?svg|badpng|vectorize)[^}]*\}\}/gi, '');
/* falls through */
case 'PNG version available':
/* falls through */
case 'Obsolete':
currentTag += '|1=' + params[tag.replace(/ /g, '_') + 'File'];
break;
case 'Do not move to Commons':
currentTag += '|reason=' + params.DoNotMoveToCommons_reason;
if (params.DoNotMoveToCommons_expiry) {
currentTag += '|expiry=' + params.DoNotMoveToCommons_expiry;
}
break;
case 'Orphaned non-free revisions':
currentTag = 'subst:' + currentTag; // subst
// remove {{non-free reduce}} and redirects
text = text.replace(/\{\{\s*(Template\s*:\s*)?(Non-free reduce|FairUseReduce|Fairusereduce|Fair Use Reduce|Fair use reduce|Reduce size|Reduce|Fair-use reduce|Image-toobig|Comic-ovrsize-img|Non-free-reduce|Nfr|Smaller image|Nonfree reduce)\s*(\|(?:\{\{[^{}]*\}\}|[^{}])*)?\}\}\s*/ig, '');
currentTag += '|date={{subst:date}}';
break;
case 'Copy to Commons':
currentTag += '|human=' + mw.config.get('wgUserName');
break;
case 'Should be SVG':
currentTag += '|' + params.svgCategory;
break;
case 'Nominated for deletion on Commons':
if (params.nominatedOnCommonsName !== '') {
currentTag += '|1=' + params.nominatedOnCommonsName;
}
break;
case 'Deleted on Commons':
if (params.deletedOnCommonsName !== '') {
currentTag += '|1=' + params.deletedOnCommonsName;
}
break;
default:
break; // don't care
}
currentTag = '{{' + currentTag + '}}\n';
tagtext += currentTag;
summary += '{{' + tag + '}}, ';
});
if (!tagtext) {
pageobj.getStatusElement().warn('User canceled operation; nothing to do');
return;
}
text = tagtext + text;
}
pageobj.setPageText(text);
pageobj.setEditSummary(summary.substring(0, summary.length - 2));
pageobj.setChangeTags(Twinkle.changeTags);
if (Twinkle.getPref('watchTaggedVenues').includes('files')) {
pageobj.setWatchlist(Twinkle.getPref('watchTaggedPages'));
}
pageobj.setMinorEdit(Twinkle.getPref('markTaggedPagesAsMinor'));
pageobj.setCreateOption('nocreate');
pageobj.save();
if (params.patrol) {
pageobj.triage();
}
}
};
/**
* Given an array of incompatible tags, check if we have two or more selected
*
* @param {Array} incompatibleTags
* @param {Array} tagsToCheck
* @param {string} [extraMessage]
* @return {true|undefined}
*/
Twinkle.tag.checkIncompatible = function(incompatibleTags, tagsToCheck, extraMessage = null) {
const count = incompatibleTags.filter((tag) => tagsToCheck.includes(tag)).length;
if (count > 1) {
const incompatibleTagsString = '{{' + incompatibleTags.join('}}, {{') + '}}';
let message = 'Please select only one of: ' + incompatibleTagsString + '.';
message += extraMessage ? ' ' + extraMessage : '';
alert(message);
return true;
}
};
Twinkle.tag.callback.evaluate = function twinkletagCallbackEvaluate(e) {
const form = e.target;
const params = Morebits.QuickForm.getInputData(form);
// Validation
// We could theoretically put them all checkIncompatible calls in a
// forEach loop, but it's probably clearer not to have [[array one],
// [array two]] devoid of context.
switch (Twinkle.tag.mode) {
case 'article':
params.tagsToRemove = form.getUnchecked('existingTags'); // not in `input`
params.tagsToRemain = params.existingTags || []; // container not created if none present
if (Twinkle.tag.checkIncompatible(['Not English', 'Rough translation'], params.tags)) {
return;
}
break;
case 'file':
if (Twinkle.tag.checkIncompatible(['Bad GIF', 'Bad JPEG', 'Bad SVG', 'Bad format'], params.tags)) {
return;
}
if (Twinkle.tag.checkIncompatible(['Should be PNG', 'Should be SVG', 'Should be text'], params.tags)) {
return;
}
if (Twinkle.tag.checkIncompatible(['Bad SVG', 'Vector version available'], params.tags)) {
return;
}
if (Twinkle.tag.checkIncompatible(['Bad JPEG', 'Overcompressed JPEG'], params.tags)) {
return;
}
if (Twinkle.tag.checkIncompatible(['PNG version available', 'Vector version available'], params.tags)) {
return;
}
// Get extension from either mime-type or title, if not present (e.g., SVGs)
var extension = ((extension = $('.mime-type').text()) && extension.split(/\//)[1]) || mw.Title.newFromText(Morebits.pageNameNorm).getExtension();
if (extension) {
const extensionUpper = extension.toUpperCase();
// What self-respecting file format has *two* extensions?!
if (extensionUpper === 'JPG') {
extension = 'JPEG';
}
// Check that selected templates make sense given the file's extension.
// {{Bad GIF|JPEG|SVG}}, {{Fake SVG}}
if (extensionUpper !== 'GIF' && params.tags.includes('Bad GIF')) {
alert('This appears to be a ' + extension + ' file, so {{Bad GIF}} is inappropriate.');
return;
} else if (extensionUpper !== 'JPEG' && params.tags.includes('Bad JPEG')) {
alert('This appears to be a ' + extension + ' file, so {{Bad JPEG}} is inappropriate.');
return;
} else if (extensionUpper !== 'SVG' && params.tags.includes('Bad SVG')) {
alert('This appears to be a ' + extension + ' file, so {{Bad SVG}} is inappropriate.');
return;
} else if (extensionUpper !== 'SVG' && params.tags.includes('Fake SVG')) {
alert('This appears to be a ' + extension + ' file, so {{Fake SVG}} is inappropriate.');
return;
}
// {{Should be PNG|SVG}}
if (params.tags.includes('Should be ' + extensionUpper)) {
alert('This is already a ' + extension + ' file, so {{Should be ' + extensionUpper + '}} is inappropriate.');
return;
}
// {{Overcompressed JPEG}}
if (params.tags.includes('Overcompressed JPEG') && extensionUpper !== 'JPEG') {
alert('This appears to be a ' + extension + ' file, so {{Overcompressed JPEG}} probably doesn\'t apply.');
return;
}
// {{Bad trace}} and {{Bad font}}
if (extensionUpper !== 'SVG') {
if (params.tags.includes('Bad trace')) {
alert('This appears to be a ' + extension + ' file, so {{Bad trace}} probably doesn\'t apply.');
return;
} else if (params.tags.includes('Bad font')) {
alert('This appears to be a ' + extension + ' file, so {{Bad font}} probably doesn\'t apply.');
return;
}
}
}
// {{Do not move to Commons}}
if (
params.tags.includes('Do not move to Commons') &&
params.DoNotMoveToCommons_expiry &&
(
!/^2\d{3}$/.test(params.DoNotMoveToCommons_expiry) ||
parseInt(params.DoNotMoveToCommons_expiry, 10) <= new Date().getFullYear()
)
) {
alert('Must be a valid future year.');
return;
}
break;
case 'redirect':
if (Twinkle.tag.checkIncompatible(['R printworthy', 'R unprintworthy'], params.tags)) {
return;
}
if (Twinkle.tag.checkIncompatible(['R from subtopic', 'R to subtopic'], params.tags)) {
return;
}
if (Twinkle.tag.checkIncompatible([
'R to category namespace',
'R to help namespace',
'R to main namespace',
'R to portal namespace',
'R to project namespace',
'R to user namespace'
], params.tags)) {
return;
}
break;
default:
alert('Twinkle.tag: unknown mode ' + Twinkle.tag.mode);
break;
}
// File/redirect: return if no tags selected
// Article: return if no tag is selected and no already present tag is deselected
if (params.tags.length === 0 && (Twinkle.tag.mode !== 'article' || params.tagsToRemove.length === 0)) {
alert('You must select at least one tag!');
return;
}
Morebits.SimpleWindow.setButtonsEnabled(false);
Morebits.Status.init(form);
Morebits.wiki.actionCompleted.redirect = Morebits.pageNameNorm;
Morebits.wiki.actionCompleted.notice = 'Tagging complete, reloading article in a few seconds';
if (Twinkle.tag.mode === 'redirect') {
Morebits.wiki.actionCompleted.followRedirect = false;
}
const wikipediaPage = new Morebits.wiki.Page(Morebits.pageNameNorm, 'Tagging ' + Twinkle.tag.mode);
wikipediaPage.setCallbackParameters(params);
wikipediaPage.setChangeTags(Twinkle.changeTags); // Here to apply to triage
wikipediaPage.load(Twinkle.tag.callbacks[Twinkle.tag.mode]);
};
Twinkle.addInitCallback(Twinkle.tag, 'tag');
}());
// </nowiki>
meo9qjk670xwv3uxlzfri0349v1ai1f
769571
769534
2026-10-22T02:25:45Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at d9cd3461baca43ac99ac2c24fb76a3aae6ddada9: minify
769571
javascript
text/javascript
// <nowiki>
(function(){Twinkle.tag=function twinkletag(){if(Morebits.isPageRedirect()&&mw.config.get("wgNamespaceNumber")!==14){Twinkle.tag.mode="redirect";Twinkle.addPortletLink(Twinkle.tag.callback,"Tag","twinkle-tag","Tag redirect")}else if(mw.config.get("wgNamespaceNumber")===6&&!document.getElementById("mw-sharedupload")&&document.getElementById("mw-imagepage-section-filehistory")){Twinkle.tag.mode="file";Twinkle.addPortletLink(Twinkle.tag.callback,"Tag","twinkle-tag","Add maintenance tags to file")}else if([0,118].includes(mw.config.get("wgNamespaceNumber"))&&mw.config.get("wgCurRevisionId")){Twinkle.tag.mode="article";Twinkle.tag.canRemove=mw.config.get("wgCurRevisionId")===mw.config.get("wgRevisionId")&&!mw.config.get("wgDiffNewId");Twinkle.addPortletLink(Twinkle.tag.callback,"Tag","twinkle-tag","Add or remove article maintenance tags")}};Twinkle.tag.checkedTags=[];Twinkle.tag.callback=function twinkletagCallback(){const Window=new Morebits.SimpleWindow(630,Twinkle.tag.mode==="article"?500:400);Window.setScriptName("Twinkle");Window.addFooterLink("Tag prefs","WP:TW/PREF#tag");Window.addFooterLink("Twinkle help","WP:TW/DOC#tag");Window.addFooterLink("Give feedback","WT:TW");const form=new Morebits.QuickForm(Twinkle.tag.callback.evaluate);const isPatroller=mw.config.get("wgUserGroups").some(r=>["patroller","sysop"].includes(r));if(isPatroller){(new mw.Api).get({action:"pagetriagelist",format:"json",page_id:mw.config.get("wgArticleId")}).then(response=>{const isReviewed=response.pagetriagelist.pages[0]?response.pagetriagelist.pages[0].patrol_status>0:true;if(!isReviewed){const checkbox=new Morebits.QuickForm.Element({type:"checkbox",list:[{label:"Mark the page as patrolled/reviewed",value:"patrol",name:"patrol",checked:Twinkle.getPref("markTaggedPagesAsPatrolled")}]});const html=checkbox.render();$(".quickform").prepend(html)}})}form.append({type:"input",label:"Filter tag list:",name:"quickfilter",size:"30",event:function twinkletagquickfilter(){$allCheckboxDivs.find(".search-hit").each((i,e)=>{const labelElement=e.parentElement;labelElement.innerHTML=labelElement.textContent});if(this.value){$allCheckboxDivs.hide();$allHeaders.hide();const searchString=this.value;const searchRegex=new RegExp(mw.util.escapeRegExp(searchString),"i");$allCheckboxDivs.find("label").each(function(){const labelText=this.textContent;const searchHit=searchRegex.exec(labelText);if(searchHit){const range=document.createRange();const textnode=this.childNodes[0];range.selectNodeContents(textnode);range.setStart(textnode,searchHit.index);range.setEnd(textnode,searchHit.index+searchString.length);const underlineSpan=$("<span>").addClass("search-hit").css("text-decoration","underline")[0];range.surroundContents(underlineSpan);this.parentElement.style.display="block"}})}else{$allCheckboxDivs.show();$allHeaders.show()}}});switch(Twinkle.tag.mode){case"article":Window.setTitle("Article maintenance tagging");Twinkle.tag.article.flatObject={};Object.values(Twinkle.tag.article.tagList).forEach(group=>{Object.values(group).forEach(subgroup=>{if(Array.isArray(subgroup)){subgroup.forEach(item=>{Twinkle.tag.article.flatObject[item.tag]=item})}else{Twinkle.tag.article.flatObject[subgroup.tag]=subgroup}})});form.append({type:"select",name:"sortorder",label:"View this list:",tooltip:"You can change the default view order in your Twinkle preferences (WP:TWPREFS).",event:Twinkle.tag.updateSortOrder,list:[{type:"option",value:"cat",label:"By categories",selected:Twinkle.getPref("tagArticleSortOrder")==="cat"},{type:"option",value:"alpha",label:"In alphabetical order",selected:Twinkle.getPref("tagArticleSortOrder")==="alpha"}]});if(!Twinkle.tag.canRemove){const divElement=document.createElement("div");divElement.innerHTML="For removal of existing tags, please open Tag menu from the current version of article";form.append({type:"div",name:"untagnotice",label:divElement})}form.append({type:"div",id:"tagWorkArea",className:"morebits-scrollbox"});form.append({type:"input",label:"Reason",name:"reason",tooltip:"Optional reason to be appended in edit summary. Recommended when removing tags.",size:"60"});break;case"file":Window.setTitle("File maintenance tagging");$.each(Twinkle.tag.fileList,(groupName,group)=>{form.append({type:"header",label:groupName});form.append({type:"checkbox",name:"tags",list:group})});if(Twinkle.getPref("customFileTagList").length){form.append({type:"header",label:"Custom tags"});form.append({type:"checkbox",name:"tags",list:Twinkle.getPref("customFileTagList")})}break;case"redirect":Window.setTitle("Redirect tagging");var isRestricted=function(item){if(typeof item.restriction==="undefined"){return false}const namespace=mw.config.get("wgNamespaceNumber");switch(item.restriction){case"insideMainspaceOnly":if(namespace!==0){return true}break;case"outsideUserspaceOnly":if(namespace===2||namespace===3){return true}break;case"insideTalkNamespaceOnly":if(namespace%2!==1||namespace<0){return true}break;case"disambiguationPagesOnly":if(!mw.config.get("wgPageName").endsWith("_(disambiguation)")){return true}break;default:alert("Twinkle.tag: unknown restriction "+item.restriction);break}return false};var i=1;$.each(Twinkle.tag.redirectList,(groupName,group)=>{form.append({type:"header",id:"tagHeader"+i,label:groupName});const subdiv=form.append({type:"div",id:"tagSubdiv"+i++});$.each(group,(subgroupName,subgroup)=>{subdiv.append({type:"div",label:[Morebits.htmlNode("b",subgroupName)]});subdiv.append({type:"checkbox",name:"tags",list:subgroup.filter(item=>!isRestricted(item)).map(item=>({value:item.tag,label:"{{"+item.tag+"}}: "+item.description,subgroup:item.subgroup}))})})});if(Twinkle.getPref("customRedirectTagList").length){form.append({type:"header",label:"Custom tags"});form.append({type:"checkbox",name:"tags",list:Twinkle.getPref("customRedirectTagList")})}break;default:alert("Twinkle.tag: unknown mode "+Twinkle.tag.mode);break}form.append({type:"submit",className:"tw-tag-submit"});const result=form.render();Window.setContent(result);Window.display();$allCheckboxDivs=$(result).find("[name$=tags]").parent();$allHeaders=$(result).find("h5, .quickformDescription");result.quickfilter.focus();result.quickfilter.autocomplete="off";result.quickfilter.addEventListener("keypress",e=>{if(e.keyCode===13){e.preventDefault();return false}});if(Twinkle.tag.mode==="article"){Twinkle.tag.alreadyPresentTags=[];if(Twinkle.tag.canRemove){const $oldParser=$(".mw-parser-output");const $newParser=$('.mw-parser-output section[data-mw-section-id="0"]');const $scanTarget=$newParser.length?$newParser:$oldParser;$scanTarget.children().each((i,e)=>{if(e.classList.contains("mw-heading")){return false}if(e.className.indexOf("box-")===0){if(e.classList[0]==="box-Multiple_issues"){$(e).find(".ambox").each((idx,e)=>{if(e.classList[0].indexOf("box-")===0){const tag=e.classList[0].slice("box-".length).replace(/_/g," ");Twinkle.tag.alreadyPresentTags.push(tag)}});return true}const tag=e.classList[0].slice("box-".length).replace(/_/g," ");Twinkle.tag.alreadyPresentTags.push(tag)}});if($(".box-Uncategorized").length){Twinkle.tag.alreadyPresentTags.push("Uncategorized")}if($(".box-Improve_categories").length){Twinkle.tag.alreadyPresentTags.push("Improve categories")}}const statusNode=document.createElement("small");statusNode.id="tw-tag-status";Twinkle.tag.status={numAdded:0,numRemoved:0};$("button.tw-tag-submit").after(statusNode);const evt=document.createEvent("Event");evt.initEvent("change",true,true);result.sortorder.dispatchEvent(evt)}else{Morebits.QuickForm.getElements(result,"tags").forEach(generateLinks)}};let $allCheckboxDivs,$allHeaders;Twinkle.tag.updateSortOrder=function(e){const form=e.target.form;const sortorder=e.target.value;Twinkle.tag.checkedTags=form.getChecked("tags");const container=new Morebits.QuickForm.Element({type:"fragment"});const makeCheckbox=function(item){const tag=item.tag,description=item.description;const checkbox={value:tag,label:"{{"+tag+"}}: "+description};if(Twinkle.tag.checkedTags.includes(tag)){checkbox.checked=true}checkbox.subgroup=item.subgroup;return checkbox};const makeCheckboxesForAlreadyPresentTags=function(){container.append({type:"header",id:"tagHeader0",label:"Tags already present"});const subdiv=container.append({type:"div",id:"tagSubdiv0"});const checkboxes=[];const unCheckedTags=e.target.form.getUnchecked("existingTags");Twinkle.tag.alreadyPresentTags.forEach(tag=>{const checkbox={value:tag,label:"{{"+tag+"}}"+(Twinkle.tag.article.flatObject[tag]?": "+Twinkle.tag.article.flatObject[tag].description:""),checked:!unCheckedTags.includes(tag),style:"font-style: italic"};checkboxes.push(checkbox)});subdiv.append({type:"checkbox",name:"existingTags",list:checkboxes})};if(sortorder==="cat"){const doCategoryCheckboxes=function(subdiv,subgroup){const checkboxes=[];$.each(subgroup,(k,item)=>{if(!Twinkle.tag.alreadyPresentTags.includes(item.tag)){checkboxes.push(makeCheckbox(item))}});subdiv.append({type:"checkbox",name:"tags",list:checkboxes})};if(Twinkle.tag.alreadyPresentTags.length>0){makeCheckboxesForAlreadyPresentTags()}let i=1;$.each(Twinkle.tag.article.tagList,(groupName,group)=>{container.append({type:"header",id:"tagHeader"+i,label:groupName});const subdiv=container.append({type:"div",id:"tagSubdiv"+i++});if(Array.isArray(group)){doCategoryCheckboxes(subdiv,group)}else{$.each(group,(subgroupName,subgroup)=>{subdiv.append({type:"div",label:[Morebits.htmlNode("b",subgroupName)]});doCategoryCheckboxes(subdiv,subgroup)})}})}else{if(Twinkle.tag.alreadyPresentTags.length>0){makeCheckboxesForAlreadyPresentTags();container.append({type:"header",id:"tagHeader1",label:"Available tags"})}Twinkle.tag.article.alphabeticalList=Twinkle.tag.article.alphabeticalList||Object.keys(Twinkle.tag.article.flatObject).sort();const checkboxes=[];Twinkle.tag.article.alphabeticalList.forEach(tag=>{if(!Twinkle.tag.alreadyPresentTags.includes(tag)){checkboxes.push(makeCheckbox(Twinkle.tag.article.flatObject[tag]))}});container.append({type:"checkbox",name:"tags",list:checkboxes})}if(Twinkle.getPref("customTagList").length){container.append({type:"header",label:"Custom tags"});container.append({type:"checkbox",name:"tags",list:Twinkle.getPref("customTagList").map(el=>{el.checked=Twinkle.tag.checkedTags.includes(el.value);return el})})}const $workarea=$(form).find("#tagWorkArea");const rendered=container.render();$workarea.empty().append(rendered);$allCheckboxDivs=$workarea.find("[name=tags], [name=existingTags]").parent();$allHeaders=$workarea.find("h5, .quickformDescription");form.quickfilter.value="";form.quickfilter.focus();$workarea.find("h5").css({"font-size":"110%"});$workarea.find("h5:not(:first-child)").css({"margin-top":"1em"});$workarea.find("div").filter(":has(span.quickformDescription)").css({"margin-top":"0.4em"});Morebits.QuickForm.getElements(form,"existingTags").forEach(generateLinks);Morebits.QuickForm.getElements(form,"tags").forEach(generateLinks);const statusNode=document.getElementById("tw-tag-status");$("[name=tags], [name=existingTags]").on("click",function(){if(this.name==="tags"){Twinkle.tag.status.numAdded+=this.checked?1:-1}else if(this.name==="existingTags"){Twinkle.tag.status.numRemoved+=this.checked?-1:1}const firstPart="Adding "+Twinkle.tag.status.numAdded+" tag"+(Twinkle.tag.status.numAdded>1?"s":"");const secondPart="Removing "+Twinkle.tag.status.numRemoved+" tag"+(Twinkle.tag.status.numRemoved>1?"s":"");statusNode.textContent=(Twinkle.tag.status.numAdded?" "+firstPart:"")+(Twinkle.tag.status.numRemoved?(Twinkle.tag.status.numAdded?"; ":" ")+secondPart:"")})};var generateLinks=function(checkbox){const link=Morebits.htmlNode("a",">");link.setAttribute("class","tag-template-link");const tagname=checkbox.values;link.setAttribute("href",mw.util.getUrl((!tagname.includes(":")?"Template:":"")+(!tagname.includes("|")?tagname:tagname.slice(0,tagname.indexOf("|")))));link.setAttribute("target","_blank");$(checkbox).parent().append([" ",link])};Twinkle.tag.article={};const translationSubgroups=[{name:"translationLanguage",parameter:"1",type:"input",label:"Language of article (if known):",tooltip:"Consider looking at [[WP:LRC]] for help. If listing the article at PNT, please try to avoid leaving this box blank, unless you are completely unsure."}].concat(mw.config.get("wgNamespaceNumber")===0?[{type:"checkbox",list:[{name:"translationPostAtPNT",label:"List this article at Wikipedia:Pages needing translation into English (PNT)",checked:true}]},{name:"translationComments",type:"textarea",label:"Additional comments to post at PNT",tooltip:'Optional, and only relevant if "List this article ..." above is checked.'}]:[]);Twinkle.tag.article.tagList={"Cleanup and maintenance tags":{"General cleanup":[{tag:"Cleanup",description:"requires cleanup",subgroup:{name:"cleanup",parameter:"reason",type:"input",label:"Specific reason why cleanup is needed:",tooltip:"Required.",size:35,required:true}},{tag:"Cleanup rewrite",description:"needs to be rewritten entirely to comply with Wikipedia's quality standards"},{tag:"Copy edit",description:"requires copy editing for grammar, style, cohesion, tone, or spelling",subgroup:{name:"copyEdit",parameter:"for",type:"input",label:'"This article may require copy editing for..."',tooltip:'e.g. "consistent spelling". Optional.',size:35}}],"Potentially unwanted content":[{tag:"Close paraphrasing",description:"contains close paraphrasing of a non-free copyrighted source",subgroup:{name:"closeParaphrasing",parameter:"source",type:"input",label:"Source:",tooltip:"Source that has been closely paraphrased"}},{tag:"Copypaste",description:"appears to have been copied and pasted from another location",excludeMI:true,subgroup:{name:"copypaste",parameter:"url",type:"input",label:"Source URL:",tooltip:"If known.",size:50}},{tag:"AI-generated",description:"content appears to be generated by a large language model",subgroup:[{name:"llmReason",parameter:"reason",type:"input",label:"Reason:",tooltip:"A short explanation to be added to the tag. Optional but recommended to fill in either Reason or Talk discussion."},{name:"llmTalk",parameter:"talk",type:"input",label:"Talk discussion:",tooltip:"Name of the section of this article's talk page where the issue is being discussed. Do not give a link, just the name of the section. Optional but recommended to fill in either Reason or Talk discussion."}]},{tag:"External links",description:"external links may not follow content policies or guidelines"},{tag:"Non-free",description:"may contain excessive or improper use of copyrighted materials"}],"Structure, formatting, and lead section":[{tag:"Cleanup reorganize",description:"needs reorganization to comply with Wikipedia's layout guidelines"},{tag:"Lead missing",description:"no lead section"},{tag:"Lead rewrite",description:"lead section needs to be rewritten to comply with guidelines"},{tag:"Lead too long",description:"lead section is too long for the length of the article"},{tag:"Lead too short",description:"lead section is too short and should be expanded to summarize key points"},{tag:"Sections",description:"needs to be divided into sections by topic"},{tag:"Too many sections",description:"too many section headers dividing up content, should be condensed"},{tag:"Very long",description:"too long to read and navigate comfortably"}],"Fiction-related cleanup":[{tag:"All plot",description:"almost entirely a plot summary"},{tag:"Fiction",description:"fails to distinguish between fact and fiction"},{tag:"In-universe",description:"subject is fictional and needs rewriting to provide a non-fictional perspective"},{tag:"Long plot",description:"plot summary is too long or excessively detailed"},{tag:"More plot",description:"plot summary is too short"},{tag:"No plot",description:"needs a plot summary"}]},"General content issues":{"Importance and notability":[{tag:"Notability",description:"subject may not meet the general notability guideline",subgroup:{name:"notability",parameter:"1",type:"select",list:[{label:"{{notability}}: article's subject may not meet the general notability guideline",value:""},{label:"{{notability|Academics}}: notability guideline for academics",value:"Academics"},{label:"{{notability|Astro}}: notability guideline for astronomical objects",value:"Astro"},{label:"{{notability|Biographies}}: notability guideline for biographies",value:"Biographies"},{label:"{{notability|Books}}: notability guideline for books",value:"Books"},{label:"{{notability|Companies}}: notability guideline for companies",value:"Companies"},{label:"{{notability|Events}}: notability guideline for events",value:"Events"},{label:"{{notability|Films}}: notability guideline for films",value:"Films"},{label:"{{notability|Geographic}}: notability guideline for geographic features",value:"Geographic"},{label:"{{notability|Lists}}: notability guideline for stand-alone lists",value:"Lists"},{label:"{{notability|Music}}: notability guideline for music",value:"Music"},{label:"{{notability|Neologisms}}: notability guideline for neologisms",value:"Neologisms"},{label:"{{notability|Numbers}}: notability guideline for numbers",value:"Numbers"},{label:"{{notability|Organizations}}: notability guideline for organizations",value:"Organizations"},{label:"{{notability|Products}}: notability guideline for products and services",value:"Products"},{label:"{{notability|Sports}}: notability guideline for sports and athletics",value:"Sports"},{label:"{{notability|Television}}: notability guideline for television shows",value:"Television"},{label:"{{notability|Web}}: notability guideline for web content",value:"Web"}]}}],"Style of writing":[{tag:"Cleanup press release",description:"reads like a press release or news article",subgroup:{type:"hidden",name:"cleanupPR1",parameter:"1",value:"article"}},{tag:"Cleanup tense",description:"does not follow guidelines on use of different tenses."},{tag:"Essay-like",description:"written like a personal reflection, personal essay, or argumentative essay"},{tag:"Fanpov",description:"written from a fan's point of view"},{tag:"Inappropriate person",description:"uses first-person or second-person inappropiately"},{tag:"How-to",description:"written like a manual or guidebook"},{tag:"Over-quotation",description:"too many or too-lengthy quotations for an encyclopedic entry"},{tag:"Promotional",description:"contains promotional content or is written like an advertisement"},{tag:"Prose",description:"written in a list format but may read better as prose"},{tag:"Resume-like",description:"written like a resume"},{tag:"Technical",description:"too technical for most readers to understand"},{tag:"Tone",description:"tone or style may not reflect the encyclopedic tone used on Wikipedia"}],"Sense (or lack thereof)":[{tag:"Confusing",description:"confusing or unclear"},{tag:"Unfocused",description:"lacks focus or is about more than one topic"}],"Information and detail":[{tag:"Context",description:"insufficient context for those unfamiliar with the subject"},{tag:"Excessive examples",description:"may contain indiscriminate, excessive, or irrelevant examples"},{tag:"Expert needed",description:"needs attention from an expert on the subject",subgroup:[{name:"expertNeeded",parameter:"1",type:"input",label:"Name of relevant WikiProject:",tooltip:'Optionally, enter the name of a WikiProject which might be able to help recruit an expert. Don\'t include the "WikiProject" prefix.'},{name:"expertNeededReason",parameter:"reason",type:"input",label:"Reason:",tooltip:"Short explanation describing the issue. Either Reason or Talk link is required."},{name:"expertNeededTalk",parameter:"talk",type:"input",label:"Talk discussion:",tooltip:"Name of the section of this article's talk page where the issue is being discussed. Do not give a link, just the name of the section. Either Reason or Talk link is required."}]},{tag:"Overly detailed",description:"excessive amount of intricate detail"},{tag:"Undue weight",description:"lends undue weight to certain ideas, incidents, or controversies"}],Timeliness:[{tag:"Current",description:"documents a current event",excludeMI:true},{tag:"Current related",description:"documents a topic affected by a current event",excludeMI:true},{tag:"Update",description:"needs additional up-to-date information added",subgroup:[{name:"updatePart",parameter:"part",type:"input",label:"What part of the article:",tooltip:"Part that needs updating",size:"45"},{name:"updateReason",parameter:"reason",type:"input",label:"Reason:",tooltip:"Explanation why the article is out of date",size:"55"}]}],"Neutrality, bias, and factual accuracy":[{tag:"Autobiography",description:"autobiography and may not be written neutrally"},{tag:"COI",description:"creator or major contributor may have a conflict of interest",subgroup:mw.config.get("wgNamespaceNumber")===0?{name:"coiReason",type:"textarea",label:"Explanation for COI tag (will be posted on this article's talk page):",tooltip:"Optional, but strongly recommended. Leave blank if not wanted."}:[]},{tag:"Disputed",description:"questionable factual accuracy"},{tag:"Fringe theories",description:"presents fringe theories as mainstream views"},{tag:"Globalize",description:"may not represent a worldwide view of the subject",subgroup:[{type:"hidden",name:"globalize1",parameter:"1",value:"article"},{name:"globalizeRegion",parameter:"2",type:"input",label:"Over-represented country or region"}]},{tag:"Hoax",description:"may partially or completely be a hoax"},{tag:"Paid contributions",description:"contains paid contributions, and may therefore require cleanup"},{tag:"Peacock",description:"contains wording that promotes the subject in a subjective manner without adding information"},{tag:"POV",description:"does not maintain a neutral point of view"},{tag:"Recentism",description:"slanted towards recent events"},{tag:"Too few opinions",description:"may not include all significant viewpoints"},{tag:"Undisclosed paid",description:"may have been created or edited in return for undisclosed payments"},{tag:"Weasel",description:"neutrality or verifiability is compromised by the use of weasel words"}],"Verifiability and sources":[{tag:"BLP no footnotes",description:"BLP that lacks inline citations"},{tag:"BLP one source",description:"BLP that relies largely or entirely on a single source"},{tag:"BLP sources",description:"BLP that needs additional references or sources for verification"},{tag:"BLP unreferenced",description:"BLP does not cite any sources at all (use BLP PROD instead for new articles)"},{tag:"Full citations needed",description:"needs more complete citations for verification"},{tag:"More citations needed",description:"needs additional references or sources for verification"},{tag:"No footnotes",description:"has references, but lacks inline citations"},{tag:"No significant coverage",description:"does not cite any sources containing significant coverage"},{tag:"No significant coverage (sports)",description:"sports biography that does not cite any sources containing significant coverage"},{tag:"One source",description:"relies largely or entirely on a single source"},{tag:"Only primary sources",description:"relies only on references to primary sources, and needs secondary sources"},{tag:"Original research",description:"contains original research"},{tag:"Primary sources",description:"relies too much on references to primary sources, and needs secondary sources"},{tag:"Self-published",description:"contains excessive or inappropriate references to self-published sources"},{tag:"Sources exist",description:"notable topic, sources are available that could be added to article"},{tag:"Third-party",description:"relies too heavily on sources too closely associated with the subject"},{tag:"Unreferenced",description:"does not cite any sources at all"},{tag:"Unreliable sources",description:"some references may not be reliable"},{tag:"User-generated",description:"contains many references to user-generated (self-published) content"}]},"Specific content issues":{Accessibility:[{tag:"Cleanup colors",description:"uses color as only way to convey information"},{tag:"Overcoloured",description:"overuses color"},{tag:"Dark mode problems",description:"has problems when viewed in dark mode"}],Language:[{tag:"Not English",description:"written in a language other than English and needs translation",excludeMI:true,subgroup:translationSubgroups.slice(0,1).concat([{type:"checkbox",list:[{name:"translationNotify",label:"Notify article creator",checked:true,tooltip:"Places {{uw-notenglish}} on the creator's talk page."}]}]).concat(translationSubgroups.slice(1))},{tag:"Rough translation",description:"poor translation from another language",excludeMI:true,subgroup:translationSubgroups},{tag:"Expand language",description:"should be expanded with text translated from a foreign-language article",excludeMI:true,subgroup:[{type:"hidden",name:"expandLangTopic",parameter:"topic",value:"",required:true},{name:"expandLanguageLangCode",parameter:"langcode",type:"input",label:"Language code:",tooltip:"Language code of the language from which article is to be expanded from",required:true},{name:"expandLanguageArticle",parameter:"otherarticle",type:"input",label:"Name of article:",tooltip:"Name of article to be expanded from, without the interwiki prefix"}]}],Links:[{tag:"Dead end",description:"article has no links to other articles"},{tag:"Orphan",description:"linked to from no other articles"},{tag:"Overlinked",description:"too many duplicate and/or irrelevant links to other articles"},{tag:"Underlinked",description:"needs more wikilinks to other articles"}],"Referencing technique":[{tag:"Citation style",description:"unclear or inconsistent citation style"},{tag:"Cleanup bare URLs",description:"uses bare URLs for references, which are prone to link rot"},{tag:"More footnotes needed",description:"has some references, but insufficient inline citations"},{tag:"Parenthetical referencing",description:"uses parenthetical referencing, which is deprecated on Wikipedia"}],Categories:[{tag:"Improve categories",description:"needs additional or more specific categories",excludeMI:true},{tag:"Uncategorized",description:"not added to any categories",excludeMI:true}]},Merging:[{tag:"History merge",description:"another page should be history merged into this one",excludeMI:true,subgroup:[{name:"histmergeOriginalPage",parameter:"originalpage",type:"input",label:"Other article:",tooltip:"Name of the page that should be merged into this one (required).",required:true},{name:"histmergeReason",parameter:"reason",type:"input",label:"Reason:",tooltip:'Short explanation describing the reason a history merge is needed. Should probably begin with "because" and end with a period.'},{name:"histmergeSysopDetails",parameter:"details",type:"input",label:"Extra details:",tooltip:"For complex cases, provide extra instructions for the reviewing administrator."}]}],Splitting:[{tag:"Split",description:"should be split into multiple pages"},{tag:"Split dab",description:"disambiguation page should be split into multiple pages"}],Informational:[{tag:"GOCEinuse",description:"currently undergoing a major copy edit by the Guild of Copy Editors",excludeMI:true},{tag:"In use",description:"undergoing a major edit for a short while",excludeMI:true},{tag:"Under construction",description:"in the process of an expansion or major restructuring",excludeMI:true}]};Twinkle.tag.redirectList={"Grammar, punctuation, and spelling":{Abbreviation:[{tag:"R from acronym",description:"redirect from an acronym (e.g. POTUS) to its expanded form",restriction:"insideMainspaceOnly"},{tag:"R from airport code",description:"redirect from an airport's IATA or ICAO code to that airport's article",restriction:"insideMainspaceOnly"},{tag:"R from airline code",description:"redirect from an airline's IATA or ICAO code to that airline's article",restriction:"insideMainspaceOnly"},{tag:"R from initialism",description:"redirect from an initialism (e.g. AGF) to its expanded form",restriction:"insideMainspaceOnly"},{tag:"R from MathSciNet abbreviation",description:"redirect from MathSciNet publication title abbreviation to the unabbreviated title",restriction:"insideMainspaceOnly"},{tag:"R from NLM abbreviation",description:"redirect from a NLM publication title abbreviation to the unabbreviated title",restriction:"insideMainspaceOnly"}],Capitalisation:[{tag:"R from CamelCase",description:"redirect from a CamelCase title"},{tag:"R from other capitalisation",description:"redirect from a title with another method of capitalisation",restriction:"insideMainspaceOnly"},{tag:"R from miscapitalisation",description:"redirect from a capitalisation error"}],"Grammar & punctuation":[{tag:"R from modification",description:"redirect from a modification of the target's title, such as with words rearranged"},{tag:"R from plural",description:"redirect from a plural word to the singular equivalent",restriction:"insideMainspaceOnly"},{tag:"R to plural",description:"redirect from a singular noun to its plural form",restriction:"insideMainspaceOnly"}],"Parts of speech":[{tag:"R from verb",description:"redirect from an English-language verb or verb phrase",restriction:"insideMainspaceOnly"},{tag:"R from adjective",description:"redirect from an adjective (word or phrase that describes a noun)",restriction:"insideMainspaceOnly"}],Spelling:[{tag:"R from alternative spelling",description:"redirect from a title with a different spelling"},{tag:"R from alternative transliteration",description:"redirect from an alternative English transliteration to a more common variation"},{tag:"R from ASCII-only",description:"redirect from a title in only basic ASCII to the formal title, with differences that are not diacritical marks or ligatures"},{tag:"R to ASCII-only",description:"redirect to a title in only basic ASCII from the formal title, with differences that are not diacritical marks or ligatures"},{tag:"R from diacritic",description:"redirect from a page name that has diacritical marks (accents, umlauts, etc.)"},{tag:"R to diacritic",description:"redirect to the article title with diacritical marks (accents, umlauts, etc.)"},{tag:"R from misspelling",description:"redirect from a misspelling or typographical error"}]},"Alternative names":{General:[{tag:"R from alternative language",description:"redirect from or to a title in another language",subgroup:[{name:"altLangFrom",type:"input",label:"From language (two-letter code):",tooltip:"Enter the two-letter code of the language the redirect name is in; such as en for English, de for German"},{name:"altLangTo",type:"input",label:"To language (two-letter code):",tooltip:"Enter the two-letter code of the language the target name is in; such as en for English, de for German"},{name:"altLangInfo",type:"div",label:$.parseHTML('<p>For a list of language codes, see <a href="/wiki/Wp:Template_messages/Redirect_language_codes">Wikipedia:Template messages/Redirect language codes</a></p>')}]},{tag:"R from alternative name",description:"redirect from a title that is another name, a pseudonym, a nickname, or a synonym"},{tag:"R from ambiguous sort name",description:"redirect from an ambiguous sort name to a page or list that disambiguates it"},{tag:"R from former name",description:"redirect from a former or historic name or a working title",restriction:"insideMainspaceOnly"},{tag:"R from incomplete name",description:"R from incomplete name"},{tag:"R from incorrect name",description:"redirect from an erroneus name that is unsuitable as a title"},{tag:"R from less specific name",description:"redirect from a less specific title to a more specific, less general one"},{tag:"R from long name",description:"redirect from a more complete title"},{tag:"R from more specific name",description:"redirect from a more specific title to a less specific, more general one"},{tag:"R from non-neutral name",description:"redirect from a title that contains a non-neutral, pejorative, controversial, or offensive word, phrase, or name"},{tag:"R from short name",description:"redirect from a title that is a shortened form of a person's full name, a book title, or other more complete title"},{tag:"R from sort name",description:"redirect from the target's sort name, such as beginning with their surname rather than given name",restriction:"insideMainspaceOnly"},{tag:"R from synonym",description:"redirect from a semantic synonym of the target page title"}],People:[{tag:"R from birth name",description:"redirect from a person's birth name to a more common name",restriction:"insideMainspaceOnly"},{tag:"R from given name",description:"redirect from a person's given name",restriction:"insideMainspaceOnly"},{tag:"R from married name",description:"redirect from a person's married name to a more common name",restriction:"insideMainspaceOnly"},{tag:"R from name with title",description:"redirect from a person's name preceded or followed by a title to the name with no title or with the title in parentheses",restriction:"insideMainspaceOnly"},{tag:"R from person",description:"redirect from a person or persons to a related article",restriction:"insideMainspaceOnly"},{tag:"R from personal name",description:"redirect from an individual's personal name to an article titled with their professional or other better known moniker",restriction:"insideMainspaceOnly"},{tag:"R from pseudonym",description:"redirect from a pseudonym",restriction:"insideMainspaceOnly"},{tag:"R from surname",description:"redirect from a title that is a surname",restriction:"insideMainspaceOnly"}],Technical:[{tag:"R from drug trade name",description:"redirect from (or to) the trade name of a drug to (or from) the international nonproprietary name (INN)"},{tag:"R from filename",description:"redirect from a title that is a filename of the target",restriction:"insideMainspaceOnly"},{tag:"R from molecular formula",description:"redirect from a molecular/chemical formula to its technical or trivial name"},{tag:"R from gene symbol",description:"redirect from a Human Genome Organisation (HUGO) symbol for a gene to an article about the gene",restriction:"insideMainspaceOnly"}],Organisms:[{tag:"R to scientific name",description:"redirect from the common name to the scientific name",restriction:"insideMainspaceOnly"},{tag:"R from scientific name",description:"redirect from the scientific name to the common name",restriction:"insideMainspaceOnly"},{tag:"R from alternative scientific name",description:"redirect from an alternative scientific name to the accepted scientific name",restriction:"insideMainspaceOnly"},{tag:"R from scientific abbreviation",description:"redirect from a scientific abbreviation",restriction:"insideMainspaceOnly"},{tag:"R to monotypic taxon",description:"redirect from the only lower-ranking member of a monotypic taxon to its monotypic taxon",restriction:"insideMainspaceOnly"},{tag:"R from monotypic taxon",description:"redirect from a monotypic taxon to its only lower-ranking member",restriction:"insideMainspaceOnly"},{tag:"R taxon with possibilities",description:"redirect from a title related to a living organism that potentially could be expanded into an article",restriction:"insideMainspaceOnly"}],Geography:[{tag:"R from name and country",description:"redirect from the specific name to the briefer name",restriction:"insideMainspaceOnly"},{tag:"R from more specific geographic name",description:"redirect from a geographic location that includes extraneous identifiers such as the county or region of a city",restriction:"insideMainspaceOnly"}]},"Navigation aids":{Navigation:[{tag:"R to anchor",description:"redirect from a topic that does not have its own page to an anchored part of a page on the subject"},{tag:"R avoided double redirect",description:"redirect from an alternative title for another redirect",subgroup:{name:"doubleRedirectTarget",type:"input",label:"Redirect target name",tooltip:"Enter the page this redirect would target if the page wasn't also a redirect"}},{tag:"R from file metadata link",description:'redirect of a wikilink created from EXIF, XMP, or other information (i.e. the "metadata" section on some image description pages)',restriction:"insideMainspaceOnly"},{tag:"R to list entry",description:"redirect to a list which contains brief descriptions of subjects not notable enough to have separate articles",restriction:"insideMainspaceOnly"},{tag:"R mentioned in hatnote",description:"redirect from a title that is mentioned in a hatnote at the redirect target"},{tag:"R to section",description:"similar to {{R to list entry}}, but when list is organized in sections, such as list of characters in a fictional universe"},{tag:"R from shortcut",description:"redirect from a Wikipedia shortcut"},{tag:"R to subpage",description:"redirect to a subpage"}],Disambiguation:[{tag:"R from ambiguous term",description:'redirect from an ambiguous page name to a page that disambiguates it. This template should never appear on a page that has "(disambiguation)" in its title, use R to disambiguation page instead'},{tag:"R to disambiguation page",description:"redirect to a disambiguation page",restriction:"disambiguationPagesOnly"},{tag:"R from incomplete disambiguation",description:"redirect from a page name that is too ambiguous to be the title of an article and should redirect to an appropriate disambiguation page"},{tag:"R from incorrect disambiguation",description:"redirect from a page name with incorrect disambiguation due to an error or previous editorial misconception"},{tag:"R from other disambiguation",description:"redirect from a page name with an alternative disambiguation qualifier"},{tag:"R from unnecessary disambiguation",description:"redirect from a page name that has an unneeded disambiguation qualifier"}],"Merge, duplicate & move":[{tag:"R from duplicated article",description:"redirect to a similar article in order to preserve its edit history"},{tag:"R with history",description:"redirect from a page containing substantive page history, kept to preserve content and attributions"},{tag:"R from move",description:"redirect from a page that has been moved/renamed"},{tag:"R from merge",description:"redirect from a merged page in order to preserve its edit history"}],Namespace:[{tag:"R from remote talk page",description:"redirect from a talk page in any talk namespace to a corresponding page that is more heavily watched",restriction:"insideTalkNamespaceOnly"},{tag:"R to category namespace",description:"redirect from a page outside the category namespace to a category page"},{tag:"R to help namespace",description:"redirect from any page inside or outside of help namespace to a page in that namespace"},{tag:"R to main namespace",description:"redirect from a page outside the main-article namespace to an article in mainspace"},{tag:"R to portal namespace",description:"redirect from any page inside or outside of portal space to a page in that namespace"},{tag:"R to project namespace",description:"redirect from any page inside or outside of project (Wikipedia: or WP:) space to any page in the project namespace"},{tag:"R to user namespace",description:"redirect from a page outside the user namespace to a user page (not to a user talk page)",restriction:"outsideUserspaceOnly"}]},Media:{General:[{tag:"R from album",description:"redirect from an album to a related topic such as the recording artist or a list of albums",restriction:"insideMainspaceOnly"},{tag:"R from band name",description:"redirect from a musical band or musical group name that redirects an article on a single person, i.e. the band or group leader"},{tag:"R from book",description:"redirect from a book title to a more general, relevant article",restriction:"insideMainspaceOnly"},{tag:"R from cover song",description:"redirect from a cover version of a song to the article about the original song this version covers"},{tag:"R from film",description:"redirect from a film title that is a subtopic of the redirect target or a title in an alternative language that has been produced in that language",restriction:"insideMainspaceOnly"},{tag:"R from journal",description:"redirect from a trade or professional journal article a more general, relevant Wikipedia article, such as the author or publisher of the article or to the title in an alternative language"},{tag:"R from lyric",description:"redirect from a lyric to a song or other source that describes the lyric"},{tag:"R from meme",description:"redirect from a name of an internet meme or other pop culture phenomenon that is a subtopic of the redirect target"},{tag:"R from song",description:"redirect from a song title to a more general, relevant article"},{tag:"R from television episode",description:"redirect from a television episode title to a related work or lists of episodes",restriction:"insideMainspaceOnly"},{tag:"R from television program",description:"redirect from a title of television program, television series or web series that is a subtopic of the redirect target"},{tag:"R from upcoming film",description:"redirect from a title that potentially could be expanded into a new article or other type of associated page such as a new template."},{tag:"R from work",description:"redirect from a creative work a related topic such as the author/artist, publisher, or a subject related to the work"}],Fiction:[{tag:"R from fictional character",description:"redirect from a fictional character to a related fictional work or list of characters",restriction:"insideMainspaceOnly"},{tag:"R from fictional element",description:"redirect from a fictional element (such as an object or concept) to a related fictional work or list of similar elements",restriction:"insideMainspaceOnly"},{tag:"R from fictional location",description:"redirect from a fictional location or setting to a related fictional work or list of places",restriction:"insideMainspaceOnly"}]},Miscellaneous:{"Related information":[{tag:"R to article without mention",description:"redirect to an article without any mention of the redirected word or phrase",restriction:"insideMainspaceOnly"},{tag:"R from company name",description:"redirect from a company name to a related article",restriction:"insideMainspaceOnly"},{tag:"R to decade",description:"redirect from a year to the decade article",restriction:"insideMainspaceOnly"},{tag:"R from domain name",description:"redirect from a domain name to an article about a website",restriction:"insideMainspaceOnly"},{tag:"R from emoji",description:"redirect from an emoji to an article describing the depicted concept or the emoji itself"},{tag:"R from phrase",description:"redirect from a phrase to a more general relevant article covering the topic"},{tag:"R from list topic",description:"redirect from the topic of a list to the equivalent list"},{tag:"R from member",description:"redirect from a member of a group to a related topic such as the group or organization"},{tag:"R to related topic",description:"redirect to an article about a similar topic",restriction:"insideMainspaceOnly"},{tag:"R from related word",description:"redirect from a related word"},{tag:"R from school",description:"redirect from a school article that had very little information",restriction:"insideMainspaceOnly"},{tag:"R from subtopic",description:"redirect from a title that is a subtopic of the target article",restriction:"insideMainspaceOnly"},{tag:"R to subtopic",description:"redirect to a subtopic of the redirect's title",restriction:"insideMainspaceOnly"},{tag:"R from Unicode character",description:"redirect from a single Unicode character to an article or Wikipedia project page that infers meaning for the symbol",restriction:"insideMainspaceOnly"},{tag:"R from Unicode code",description:"redirect from a Unicode code point to an article about the character it represents",restriction:"insideMainspaceOnly"}],"With possibilities":[{tag:"R with possibilities",description:"redirect from a specific title to a more general, less detailed article (something which can and should be expanded)"}],"ISO codes":[{tag:"R from ISO 4 abbreviation",description:"redirect from an ISO 4 publication title abbreviation to the unabbreviated title",restriction:"insideMainspaceOnly"},{tag:"R from ISO 639 code",description:"redirect from a title that is an ISO 639 language code to an article about the language",restriction:"insideMainspaceOnly"}],Printworthiness:[{tag:"R printworthy",description:"redirect from a title that would be helpful in a printed or CD/DVD version of Wikipedia",restriction:"insideMainspaceOnly"},{tag:"R unprintworthy",description:"redirect from a title that would NOT be helpful in a printed or CD/DVD version of Wikipedia",restriction:"insideMainspaceOnly"}]}};Twinkle.tag.fileList={"License and sourcing problem tags":[{label:"{{Better source requested}}: source info consists of bare image URL/generic base URL only",value:"Better source requested"},{label:"{{Maybe free media}}: currently tagged under non-free license, but free license may be available ",value:"Maybe free media"},{label:"{{Non-free reduce}}: non-low-resolution fair use image (or too-long audio clip, etc)",value:"Non-free reduce"},{label:"{{Orphaned non-free revisions}}: fair use media with old revisions that need to be deleted",value:"Orphaned non-free revisions"}],"Wikimedia Commons-related tags":[{label:"{{Copy to Commons}}: free media that should be copied to Commons",value:"Copy to Commons"},{label:"{{Deleted on Commons}}: file has previously been deleted from Commons",value:"Deleted on Commons",subgroup:{type:"input",name:"deletedOnCommonsName",label:"Name on Commons:",tooltip:"Name of the image on Commons (if different from local name), excluding the File: prefix"}},{label:"{{Do not move to Commons}}: file not suitable for moving to Commons",value:"Do not move to Commons",subgroup:[{type:"input",name:"DoNotMoveToCommons_reason",label:"Reason:",tooltip:'Enter the reason why this image should not be moved to Commons (required). If the file is PD in the US but not in country of origin, enter "US only"',required:true},{type:"number",name:"DoNotMoveToCommons_expiry",label:"Expiration year:",min:(new Morebits.Date).getFullYear(),tooltip:"If this file can be moved to Commons beginning in a certain year, you can enter it here (optional)."}]},{label:"{{Keep local}}: request to keep local copy of a Commons file",value:"Keep local",subgroup:{type:"input",name:"keeplocalName",label:"Commons image name if different:",tooltip:"Name of the image on Commons (if different from local name), excluding the File: prefix:"}},{label:"{{Nominated for deletion on Commons}}: file is nominated for deletion on Commons",value:"Nominated for deletion on Commons",subgroup:{type:"input",name:"nominatedOnCommonsName",label:"Name on Commons:",tooltip:"Name of the image on Commons (if different from local name), excluding the File: prefix:"}}],"Cleanup tags":[{label:"{{Artifacts}}: PNG contains residual compression artifacts",value:"Artifacts"},{label:"{{Bad font}}: SVG uses fonts not available on the thumbnail server",value:"Bad font"},{label:"{{Bad format}}: PDF/DOC/... file should be converted to a more useful format",value:"Bad format"},{label:"{{Bad GIF}}: GIF that should be PNG, JPEG, or SVG",value:"Bad GIF"},{label:"{{Bad JPEG}}: JPEG that should be PNG or SVG",value:"Bad JPEG"},{label:"{{Bad SVG}}: SVG with a mix of raster and vector graphics",value:"Bad SVG"},{label:"{{Bad trace}}: auto-traced SVG requiring cleanup",value:"Bad trace"},{label:"{{Cleanup image}}: general cleanup",value:"Cleanup image",subgroup:{type:"input",name:"cleanupimageReason",label:"Reason:",tooltip:"Enter the reason for cleanup (required)",required:true}},{label:"{{ClearType}}: image (not screenshot) with ClearType anti-aliasing",value:"ClearType"},{label:"{{Fake SVG}}: SVG solely containing raster graphics without true vector content",value:"Fake SVG"},{label:"{{Imagewatermark}}: image contains visible or invisible watermarking",value:"Imagewatermark"},{label:"{{NoCoins}}: image using coins to indicate scale",value:"NoCoins"},{label:"{{Overcompressed JPEG}}: JPEG with high levels of artifacts",value:"Overcompressed JPEG"},{label:"{{Opaque}}: opaque background should be transparent",value:"Opaque"},{label:"{{Remove border}}: unneeded border, white space, etc.",value:"Remove border"},{label:"{{Rename media}}: file should be renamed according to the criteria at [[WP:FMV]]",value:"Rename media",subgroup:[{type:"input",name:"renamemediaNewname",label:"New name:",tooltip:"Enter the new name for the image (optional)"},{type:"input",name:"renamemediaReason",label:"Reason:",tooltip:"Enter the reason for the rename (optional)"}]},{label:"{{Should be PNG}}: GIF or JPEG should be lossless",value:"Should be PNG"},{label:"{{Should be SVG}}: PNG, GIF or JPEG should be vector graphics",value:"Should be SVG",subgroup:{name:"svgCategory",type:"select",list:[{label:"{{Should be SVG|other}}",value:"other"},{label:"{{Should be SVG|alphabet}}: character images, font examples, etc.",value:"alphabet"},{label:"{{Should be SVG|chemical}}: chemical diagrams, etc.",value:"chemical"},{label:"{{Should be SVG|circuit}}: electronic circuit diagrams, etc.",value:"circuit"},{label:"{{Should be SVG|coat of arms}}: coats of arms",value:"coat of arms"},{label:"{{Should be SVG|diagram}}: diagrams that do not fit any other subcategory",value:"diagram"},{label:"{{Should be SVG|emblem}}: emblems, free/libre logos, insignias, etc.",value:"emblem"},{label:"{{Should be SVG|fair use}}: fair-use images, fair-use logos",value:"fair use"},{label:"{{Should be SVG|flag}}: flags",value:"flag"},{label:"{{Should be SVG|graph}}: visual plots of data",value:"graph"},{label:"{{Should be SVG|logo}}: logos",value:"logo"},{label:"{{Should be SVG|map}}: maps",value:"map"},{label:"{{Should be SVG|music}}: musical scales, notes, etc.",value:"music"},{label:'{{Should be SVG|physical}}: "realistic" images of physical objects, people, etc.',value:"physical"},{label:"{{Should be SVG|symbol}}: miscellaneous symbols, icons, etc.",value:"symbol"}]}},{label:"{{Should be text}}: image should be represented as text, tables, or math markup",value:"Should be text"}],"Image quality tags":[{label:"{{Image hoax}}: Image may be manipulated or constitute a hoax",value:"Image hoax"},{label:"{{Image-blownout}}",value:"Image-blownout"},{label:"{{Image-out-of-focus}}",value:"Image-out-of-focus"},{label:"{{Image-Poor-Quality}}",value:"Image-Poor-Quality",subgroup:{type:"input",name:"ImagePoorQualityReason",label:"Reason:",tooltip:"Enter the reason why this image is so bad (required)",required:true}},{label:"{{Image-underexposure}}",value:"Image-underexposure"},{label:"{{Low quality chem}}: disputed chemical structures",value:"Low quality chem",subgroup:{type:"input",name:"lowQualityChemReason",label:"Reason:",tooltip:"Enter the reason why the diagram is disputed (required)",required:true}}],"Replacement tags":[{label:"{{Obsolete}}: improved version available",value:"Obsolete"},{label:"{{PNG version available}}",value:"PNG version available"},{label:"{{Vector version available}}",value:"Vector version available"}]};Twinkle.tag.fileList["Replacement tags"].forEach(el=>{el.subgroup={type:"input",label:"Replacement file:",tooltip:"Enter the name of the file which replaces this one (required)",name:el.value.replace(/ /g,"_")+"File",required:true}});Twinkle.tag.callbacks={article:function articleCallback(pageobj){let pageText=pageobj.getPageText().replace(/\{\{\s*([Uu]serspace draft)\s*(\|(?:\{\{[^{}]*\}\}|[^{}])*)?\}\}\s*/g,"");const params=pageobj.getCallbackParameters();const postRemoval=function(){if(params.tagsToRemove.length){pageText=pageText.replace(/\{\{(multiple ?issues|article ?issues|mi)\s*\|\s*\}\}\n?/im,"");pageText=pageText.replace(/\{\{(?:multiple ?issues|article ?issues|mi)\s*\|\s*(\{\{[^}]+\}\})\s*\}\}/im,"$1")}const makeSentence=function(array){if(array.length<3){return array.join(" and ")}const last=array.pop();return array.join(", ")+", and "+last};const makeTemplateLink=function(tag){let text="{{[[";if(tag.includes("|")){tag=tag.slice(0,tag.indexOf("|"))}text+=tag.includes(":")?tag:"Template:"+tag+"|"+tag;return text+"]]}}"};let summaryText;const addedTags=params.tags.map(makeTemplateLink);const removedTags=params.tagsToRemove.map(makeTemplateLink);if(addedTags.length){summaryText="Added "+makeSentence(addedTags);summaryText+=removedTags.length?"; and removed "+makeSentence(removedTags):""}else{summaryText="Removed "+makeSentence(removedTags)}summaryText+=" tag"+(addedTags.length+removedTags.length>1?"s":"");if(params.reason){summaryText+=": "+params.reason}if(summaryText.length>499){summaryText=summaryText.replace(/\[\[[^|]+\|([^\]]+)\]\]/g,"$1")}pageobj.setPageText(pageText);pageobj.setEditSummary(summaryText);if(mw.config.get("wgNamespaceNumber")===0&&Twinkle.getPref("watchTaggedVenues").includes("articles")||mw.config.get("wgNamespaceNumber")===118&&Twinkle.getPref("watchTaggedVenues").includes("drafts")){pageobj.setWatchlist(Twinkle.getPref("watchTaggedPages"))}pageobj.setMinorEdit(Twinkle.getPref("markTaggedPagesAsMinor"));pageobj.setCreateOption("nocreate");pageobj.save(()=>{if(params.coiReason){const coiTalkPage=new Morebits.wiki.Page("Talk:"+Morebits.pageNameNorm,"Starting discussion on talk page");coiTalkPage.setNewSectionText(params.coiReason+" ~~~~");coiTalkPage.setNewSectionTitle("COI tag ("+new Morebits.Date(pageobj.getLoadTime()).format("MMMM Y","utc")+")");coiTalkPage.setChangeTags(Twinkle.changeTags);coiTalkPage.setCreateOption("recreate");coiTalkPage.newSection()}if(params.translationPostAtPNT){const pntPage=new Morebits.wiki.Page("Wikipedia:Pages needing translation into English","Listing article at Wikipedia:Pages needing translation into English");pntPage.setFollowRedirect(true);pntPage.load(pageobj=>{const oldText=pageobj.getPageText();const lang=params.translationLanguage;const reason=params.translationComments;let templateText;let text,summary;if(params.tags.includes("Rough translation")){templateText="{{subst:Dual fluency request|pg="+Morebits.pageNameNorm+"|Language="+(lang||"uncertain")+"|Comments="+reason.trim()+"}} ~~~~";text=oldText+"\n\n"+templateText;summary="Translation cleanup requested on "}else if(params.tags.includes("Not English")){templateText="{{subst:Translation request|pg="+Morebits.pageNameNorm+"|Language="+(lang||"uncertain")+"|Comments="+reason.trim()+"}} ~~~~";text=oldText.replace(/\n+(==\s?Translated pages that could still use some cleanup\s?==)/,"\n\n"+templateText+"\n\n$1");summary="Translation"+(lang?" from "+lang:"")+" requested on "}if(text===oldText){pageobj.getStatusElement().error("failed to find target spot for the discussion");return}pageobj.setPageText(text);pageobj.setEditSummary(summary+" [[:"+Morebits.pageNameNorm+"]]");pageobj.setChangeTags(Twinkle.changeTags);pageobj.setCreateOption("recreate");pageobj.save()})}if(params.translationNotify){new Morebits.wiki.Page(Morebits.pageNameNorm).lookupCreation(innerPageobj=>{const initialContrib=innerPageobj.getCreator();if(initialContrib===mw.config.get("wgUserName")){innerPageobj.getStatusElement().warn("You ("+initialContrib+") created this page; skipping user notification");return}const userTalkPage=new Morebits.wiki.Page("User talk:"+initialContrib,"Notifying initial contributor ("+initialContrib+")");userTalkPage.setNewSectionTitle("Your article [["+Morebits.pageNameNorm+"]]");userTalkPage.setNewSectionText("{{subst:uw-notenglish|1="+Morebits.pageNameNorm+(params.translationPostAtPNT?"":"|nopnt=yes")+"}} ~~~~");userTalkPage.setEditSummary("Notice: Please use English when contributing to the English Wikipedia.");userTalkPage.setChangeTags(Twinkle.changeTags);userTalkPage.setCreateOption("recreate");userTalkPage.setFollowRedirect(true,false);userTalkPage.newSection()})}});if(params.patrol){pageobj.triage()}};const removeTags=function removeTags(){if(params.tagsToRemove.length===0){postRemoval();return}Morebits.Status.info("Info","Removing deselected tags that were already present");const getRedirectsFor=[];params.tagsToRemove.forEach(tag=>{const tagRegex=new RegExp("\\{\\{"+Morebits.pageNameRegex(tag)+"\\s*(\\|[^}]+)?\\}\\}\\n?");if(tagRegex.test(pageText)){pageText=pageText.replace(tagRegex,"")}else{getRedirectsFor.push("Template:"+tag)}});if(!getRedirectsFor.length){postRemoval();return}const api=new Morebits.wiki.Api("Getting template redirects",{action:"query",prop:"linkshere",titles:getRedirectsFor.join("|"),redirects:1,lhnamespace:"10",lhshow:"redirect",lhlimit:"max",format:"json"},apiobj=>{const pages=apiobj.getResponse().query.pages.filter(p=>!p.missing&&!!p.linkshere);pages.forEach(page=>{let removed=false;page.linkshere.concat({title:page.title}).forEach(el=>{const tag=el.title.slice(9);const tagRegex=new RegExp("\\{\\{"+Morebits.pageNameRegex(tag)+"\\s*(\\|[^}]*)?\\}\\}\\n?");if(tagRegex.test(pageText)){pageText=pageText.replace(tagRegex,"");removed=true;return false}});if(!removed){Morebits.Status.warn("Info","Failed to find {{"+page.title.slice(9)+"}} on the page... excluding")}});postRemoval()});api.post()};if(!params.tags.length){removeTags();return}let tagRe,tagText="",tags=[];const groupableTags=[],groupableExistingTags=[];const addTag=function articleAddTag(tagIndex,tagName){let currentTag="";if(tagName==="Uncategorized"||tagName==="Improve categories"){pageText+="\n\n{{"+tagName+"|date={{subst:CURRENTMONTHNAME}} {{subst:CURRENTYEAR}}}}"}else{currentTag+="{{"+tagName;const subgroupObj=Twinkle.tag.article.flatObject[tagName]&&Twinkle.tag.article.flatObject[tagName].subgroup;if(subgroupObj){const subgroups=Array.isArray(subgroupObj)?subgroupObj:[subgroupObj];subgroups.forEach(gr=>{if(gr.parameter&&(params[gr.name]||gr.required)){currentTag+="|"+gr.parameter+"="+(params[gr.name]||"")}})}switch(tagName){case"Not English":case"Rough translation":if(params.translationPostAtPNT){currentTag+="|listed=yes"}break;default:break}currentTag+="|date={{subst:CURRENTMONTHNAME}} {{subst:CURRENTYEAR}}}}\n";tagText+=currentTag}};const addUngroupedTags=function(){$.each(tags,addTag);const wikipage=new Morebits.wikitext.Page(pageText);const templatesAfter=Twinkle.hatnoteRegex+"pp|pp-.*?|"+"db|delete|db-.*?|speedy deletion-.*?|"+"(?:proposed deletion|prod blp)\\/dated(?:\\s*\\|(?:concern|user|timestamp|help).*)+|"+"salt|proposed deletion endorsed";const afdRegex="(?:\x3c!--.*AfD.*\\n\\{\\{(?:Article for deletion\\/dated|AfDM).*\\}\\}\\n\x3c!--.*(?:\\n\x3c!--.*)?AfD.*(?:\\s*\\n))?";pageText=wikipage.insertAfterTemplates(tagText,templatesAfter,null,afdRegex).getText();removeTags()};params.tags.forEach(tag=>{tagRe=new RegExp("\\{\\{"+tag+"(\\||\\}\\})","im");if(Twinkle.tag.canRemove||!tagRe.exec(pageText)){if(Twinkle.tag.article.flatObject[tag]&&!Twinkle.tag.article.flatObject[tag].excludeMI){groupableTags.push(tag)}else{tags.push(tag)}}else{if(tag==="History merge"){tags.push(tag)}else{Morebits.Status.warn("Info","Found {{"+tag+"}} on the article already...excluding")}}});params.tagsToRemain.forEach(tag=>{if(Twinkle.tag.article.flatObject[tag]&&!Twinkle.tag.article.flatObject[tag].excludeMI){groupableExistingTags.push(tag)}});const miTest=/\{\{(multiple ?issues|article ?issues|mi)(?!\s*\|\s*section\s*=)[^}]+\{/im.exec(pageText);if(miTest&&groupableTags.length>0){Morebits.Status.info("Info","Adding supported tags inside existing {{multiple issues}} tag");tagText="";$.each(groupableTags,addTag);const miRegex=new RegExp("(\\{\\{\\s*"+miTest[1]+"\\s*(?:\\|(?:\\{\\{[^{}]*\\}\\}|[^{}])*)?)\\}\\}\\s*","im");pageText=pageText.replace(miRegex,"$1"+tagText+"}}\n");tagText="";addUngroupedTags()}else if(!miTest&&groupableExistingTags.length+groupableTags.length>=2){Morebits.Status.info("Info","Grouping supported tags inside {{multiple issues}}");tagText+="{{Multiple issues|\n";const addNewTagsToMI=function(){$.each(groupableTags,addTag);tagText+="}}\n";addUngroupedTags()};const getRedirectsFor=[];groupableExistingTags.forEach(tag=>{const tagRegex=new RegExp("(\\{\\{"+Morebits.pageNameRegex(tag)+"\\s*(\\|[^}]+)?\\}\\}\\n?)");if(tagRegex.test(pageText)){tagText+=tagRegex.exec(pageText)[1];pageText=pageText.replace(tagRegex,"")}else{getRedirectsFor.push("Template:"+tag)}});if(!getRedirectsFor.length){addNewTagsToMI();return}const api=new Morebits.wiki.Api("Getting template redirects",{action:"query",prop:"linkshere",titles:getRedirectsFor.join("|"),redirects:1,lhnamespace:"10",lhshow:"redirect",lhlimit:"max",format:"json"},apiobj=>{const pages=apiobj.getResponse().query.pages.filter(p=>!p.missing&&!!p.linkshere);pages.forEach(page=>{let found=false;page.linkshere.forEach(el=>{const tag=el.title.slice(9);const tagRegex=new RegExp("(\\{\\{"+Morebits.pageNameRegex(tag)+"\\s*(\\|[^}]*)?\\}\\}\\n?)");if(tagRegex.test(pageText)){tagText+=tagRegex.exec(pageText)[1];pageText=pageText.replace(tagRegex,"");found=true;return false}});if(!found){Morebits.Status.warn("Info","Failed to find the existing {{"+page.title.slice(9)+"}} on the page... skip repositioning")}});addNewTagsToMI()});api.post()}else{tags=tags.concat(groupableTags);addUngroupedTags()}},redirect:function redirect(pageobj){const params=pageobj.getCallbackParameters(),tags=[];let pageText=pageobj.getPageText(),tagRe,tagText="",summaryText="Added",i;for(i=0;i<params.tags.length;i++){tagRe=new RegExp("(\\{\\{"+params.tags[i]+"(\\||\\}\\}))","im");if(!tagRe.exec(pageText)){tags.push(params.tags[i])}else{Morebits.Status.warn("Info","Found {{"+params.tags[i]+"}} on the redirect already...excluding")}}const addTag=function redirectAddTag(tagIndex,tagName){tagText+="\n{{"+tagName;if(tagName==="R from alternative language"){if(params.altLangFrom){tagText+="|from="+params.altLangFrom}if(params.altLangTo){tagText+="|to="+params.altLangTo}}else if(tagName==="R avoided double redirect"&¶ms.doubleRedirectTarget){tagText+="|1="+params.doubleRedirectTarget}tagText+="}}";if(tagIndex>0){if(tagIndex===tags.length-1){summaryText+=" and"}else if(tagIndex<tags.length-1){summaryText+=","}}summaryText+=" {{[[:"+(tagName.includes(":")?tagName:"Template:"+tagName+"|"+tagName)+"]]}}"};if(!tags.length){Morebits.Status.warn("Info","No tags remaining to apply")}tags.sort();$.each(tags,addTag);if(pageText.match(/{{(?:redr|this is a redirect|r(?:edirect)?(?:.?cat.*)?[ _]?sh|RCS)/i)){const oldTags=pageText.match(/(\s*{{[A-Za-z\s]+\|(?:\s*1=)?)((?:[^|{}]|{{[^}]+}})+)(}})\s*/i);pageText=pageText.replace(oldTags[0],oldTags[1]+tagText+oldTags[2]+oldTags[3])}else{const pageTags=pageText.match(/\s*{{R(?:edirect)? .*?}}/gim);let oldPageTags="";if(pageTags){pageTags.forEach(pageTag=>{const pageRe=new RegExp(Morebits.string.escapeRegExp(pageTag),"img");pageText=pageText.replace(pageRe,"");pageTag=pageTag.trim();oldPageTags+="\n"+pageTag})}pageText=pageText.trim()+"\n\n{{Redirect category shell|"+tagText+oldPageTags+"\n}}"}summaryText+=(tags.length>0?" tag"+(tags.length>1?"s":" "):" {{[[Template:Redirect category shell|Redirect category shell]]}}")+" to redirect";if(summaryText.length>499){summaryText=summaryText.replace(/\[\[[^|]+\|([^\]]+)\]\]/g,"$1")}pageobj.setPageText(pageText);pageobj.setEditSummary(summaryText);if(Twinkle.getPref("watchTaggedVenues").includes("redirects")){pageobj.setWatchlist(Twinkle.getPref("watchTaggedPages"))}pageobj.setMinorEdit(Twinkle.getPref("markTaggedPagesAsMinor"));pageobj.setCreateOption("nocreate");pageobj.save();if(params.patrol){pageobj.triage()}},file:function twinkletagCallbacksFile(pageobj){let text=pageobj.getPageText();const params=pageobj.getCallbackParameters();let summary="Adding ";if(params.tags.length){let tagtext="",currentTag;$.each(params.tags,(k,tag)=>{if(["Keep local","Do not move to Commons"].includes(tag)){text=Twinkle.removeMoveToCommonsTagsFromWikicode(text)}currentTag=tag;switch(tag){case"Keep local":if(params.keeplocalName!==""){currentTag+="|1="+params.keeplocalName}break;case"Rename media":if(params.renamemediaNewname!==""){currentTag+="|1="+params.renamemediaNewname}if(params.renamemediaReason!==""){currentTag+="|2="+params.renamemediaReason}break;case"Cleanup image":currentTag+="|1="+params.cleanupimageReason;break;case"Image-Poor-Quality":currentTag+="|1="+params.ImagePoorQualityReason;break;case"Image hoax":currentTag+="|date={{subst:CURRENTMONTHNAME}} {{subst:CURRENTYEAR}}";break;case"Low quality chem":currentTag+="|1="+params.lowQualityChemReason;break;case"Vector version available":text=text.replace(/\{\{((convert to |convertto|should be |shouldbe|to)?svg|badpng|vectorize)[^}]*\}\}/gi,"");case"PNG version available":case"Obsolete":currentTag+="|1="+params[tag.replace(/ /g,"_")+"File"];break;case"Do not move to Commons":currentTag+="|reason="+params.DoNotMoveToCommons_reason;if(params.DoNotMoveToCommons_expiry){currentTag+="|expiry="+params.DoNotMoveToCommons_expiry}break;case"Orphaned non-free revisions":currentTag="subst:"+currentTag;text=text.replace(/\{\{\s*(Template\s*:\s*)?(Non-free reduce|FairUseReduce|Fairusereduce|Fair Use Reduce|Fair use reduce|Reduce size|Reduce|Fair-use reduce|Image-toobig|Comic-ovrsize-img|Non-free-reduce|Nfr|Smaller image|Nonfree reduce)\s*(\|(?:\{\{[^{}]*\}\}|[^{}])*)?\}\}\s*/gi,"");currentTag+="|date={{subst:date}}";break;case"Copy to Commons":currentTag+="|human="+mw.config.get("wgUserName");break;case"Should be SVG":currentTag+="|"+params.svgCategory;break;case"Nominated for deletion on Commons":if(params.nominatedOnCommonsName!==""){currentTag+="|1="+params.nominatedOnCommonsName}break;case"Deleted on Commons":if(params.deletedOnCommonsName!==""){currentTag+="|1="+params.deletedOnCommonsName}break;default:break}currentTag="{{"+currentTag+"}}\n";tagtext+=currentTag;summary+="{{"+tag+"}}, "});if(!tagtext){pageobj.getStatusElement().warn("User canceled operation; nothing to do");return}text=tagtext+text}pageobj.setPageText(text);pageobj.setEditSummary(summary.substring(0,summary.length-2));pageobj.setChangeTags(Twinkle.changeTags);if(Twinkle.getPref("watchTaggedVenues").includes("files")){pageobj.setWatchlist(Twinkle.getPref("watchTaggedPages"))}pageobj.setMinorEdit(Twinkle.getPref("markTaggedPagesAsMinor"));pageobj.setCreateOption("nocreate");pageobj.save();if(params.patrol){pageobj.triage()}}};Twinkle.tag.checkIncompatible=function(incompatibleTags,tagsToCheck,extraMessage=null){const count=incompatibleTags.filter(tag=>tagsToCheck.includes(tag)).length;if(count>1){const incompatibleTagsString="{{"+incompatibleTags.join("}}, {{")+"}}";let message="Please select only one of: "+incompatibleTagsString+".";message+=extraMessage?" "+extraMessage:"";alert(message);return true}};Twinkle.tag.callback.evaluate=function twinkletagCallbackEvaluate(e){const form=e.target;const params=Morebits.QuickForm.getInputData(form);switch(Twinkle.tag.mode){case"article":params.tagsToRemove=form.getUnchecked("existingTags");params.tagsToRemain=params.existingTags||[];if(Twinkle.tag.checkIncompatible(["Not English","Rough translation"],params.tags)){return}break;case"file":if(Twinkle.tag.checkIncompatible(["Bad GIF","Bad JPEG","Bad SVG","Bad format"],params.tags)){return}if(Twinkle.tag.checkIncompatible(["Should be PNG","Should be SVG","Should be text"],params.tags)){return}if(Twinkle.tag.checkIncompatible(["Bad SVG","Vector version available"],params.tags)){return}if(Twinkle.tag.checkIncompatible(["Bad JPEG","Overcompressed JPEG"],params.tags)){return}if(Twinkle.tag.checkIncompatible(["PNG version available","Vector version available"],params.tags)){return}var extension=(extension=$(".mime-type").text())&&extension.split(/\//)[1]||mw.Title.newFromText(Morebits.pageNameNorm).getExtension();if(extension){const extensionUpper=extension.toUpperCase();if(extensionUpper==="JPG"){extension="JPEG"}if(extensionUpper!=="GIF"&¶ms.tags.includes("Bad GIF")){alert("This appears to be a "+extension+" file, so {{Bad GIF}} is inappropriate.");return}else if(extensionUpper!=="JPEG"&¶ms.tags.includes("Bad JPEG")){alert("This appears to be a "+extension+" file, so {{Bad JPEG}} is inappropriate.");return}else if(extensionUpper!=="SVG"&¶ms.tags.includes("Bad SVG")){alert("This appears to be a "+extension+" file, so {{Bad SVG}} is inappropriate.");return}else if(extensionUpper!=="SVG"&¶ms.tags.includes("Fake SVG")){alert("This appears to be a "+extension+" file, so {{Fake SVG}} is inappropriate.");return}if(params.tags.includes("Should be "+extensionUpper)){alert("This is already a "+extension+" file, so {{Should be "+extensionUpper+"}} is inappropriate.");return}if(params.tags.includes("Overcompressed JPEG")&&extensionUpper!=="JPEG"){alert("This appears to be a "+extension+" file, so {{Overcompressed JPEG}} probably doesn't apply.");return}if(extensionUpper!=="SVG"){if(params.tags.includes("Bad trace")){alert("This appears to be a "+extension+" file, so {{Bad trace}} probably doesn't apply.");return}else if(params.tags.includes("Bad font")){alert("This appears to be a "+extension+" file, so {{Bad font}} probably doesn't apply.");return}}}if(params.tags.includes("Do not move to Commons")&¶ms.DoNotMoveToCommons_expiry&&(!/^2\d{3}$/.test(params.DoNotMoveToCommons_expiry)||parseInt(params.DoNotMoveToCommons_expiry,10)<=(new Date).getFullYear())){alert("Must be a valid future year.");return}break;case"redirect":if(Twinkle.tag.checkIncompatible(["R printworthy","R unprintworthy"],params.tags)){return}if(Twinkle.tag.checkIncompatible(["R from subtopic","R to subtopic"],params.tags)){return}if(Twinkle.tag.checkIncompatible(["R to category namespace","R to help namespace","R to main namespace","R to portal namespace","R to project namespace","R to user namespace"],params.tags)){return}break;default:alert("Twinkle.tag: unknown mode "+Twinkle.tag.mode);break}if(params.tags.length===0&&(Twinkle.tag.mode!=="article"||params.tagsToRemove.length===0)){alert("You must select at least one tag!");return}Morebits.SimpleWindow.setButtonsEnabled(false);Morebits.Status.init(form);Morebits.wiki.actionCompleted.redirect=Morebits.pageNameNorm;Morebits.wiki.actionCompleted.notice="Tagging complete, reloading article in a few seconds";if(Twinkle.tag.mode==="redirect"){Morebits.wiki.actionCompleted.followRedirect=false}const wikipediaPage=new Morebits.wiki.Page(Morebits.pageNameNorm,"Tagging "+Twinkle.tag.mode);wikipediaPage.setCallbackParameters(params);wikipediaPage.setChangeTags(Twinkle.changeTags);wikipediaPage.load(Twinkle.tag.callbacks[Twinkle.tag.mode])};Twinkle.addInitCallback(Twinkle.tag,"tag")})();
// </nowiki>
km5h7y1m6o00lh7xinuoy8zmvbub9nz
User:Syunsyunminmin/Twinkle/friendlytalkback.js
2
179403
769468
2026-10-21T13:58:14Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 3680108:
769468
javascript
text/javascript
/*
Twinkle: adapted for Japanese Wikipedia; upstream authors and Japanese adaptations are credited in UPSTREAM.md.
Upstream source: https://github.com/wikimedia-gadgets/twinkle/blob/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/src/modules/twinkletalkback.js
Upstream author history: https://github.com/wikimedia-gadgets/twinkle/commits/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/src/modules/twinkletalkback.js
License: CC BY-SA 3.0 Unported. See https://github.com/wikimedia-gadgets/twinkle/blob/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/LICENSE.txt
Japanese adaptation: Syunsyunminmin; includes later synchronization changes.
*/
// <nowiki>
(function() {
/*
****************************************
*** twinkletalkback.js: Talkback module
****************************************
* Mode of invocation: Tab ("TB")
* Active on: Any page with relevant user name (userspace, contribs, etc.) except IP ranges
*/
Twinkle.talkback = function() {
if (!mw.config.exists('wgRelevantUserName') || Morebits.ip.isRange(mw.config.get('wgRelevantUserName'))) {
return;
}
Twinkle.addPortletLink(Twinkle.talkback.callback, 'TB', 'twinkle-talkback', 'Easy talkback');
};
Twinkle.talkback.callback = function() {
if (mw.config.get('wgRelevantUserName') === mw.config.get('wgUserName') && !confirm("Is it really so bad that you're talking back to yourself?")) {
return;
}
const Window = new Morebits.SimpleWindow(600, 350);
Window.setTitle('Talkback');
Window.setScriptName('Twinkle');
Window.addFooterLink('Talkback prefs', 'WP:TW/PREF#talkback');
Window.addFooterLink('Twinkle help', 'WP:TW/DOC#talkback');
Window.addFooterLink('Give feedback', 'WT:TW');
const form = new Morebits.QuickForm(Twinkle.talkback.evaluate);
form.append({ type: 'radio', name: 'tbtarget',
list: [
{
label: 'Talkback',
value: 'talkback',
checked: 'true'
},
{
label: 'Please see',
value: 'see'
},
{
label: 'Noticeboard notification',
value: 'notice'
},
{
label: "You've got mail",
value: 'mail'
}
],
event: Twinkle.talkback.changeTarget
});
form.append({
type: 'field',
label: 'Work area',
name: 'work_area'
});
const previewlink = document.createElement('a');
$(previewlink).on('click', () => {
Twinkle.talkback.callbacks.preview(result); // |result| is defined below
});
previewlink.style.cursor = 'pointer';
previewlink.textContent = 'Preview';
form.append({ type: 'div', id: 'talkbackpreview', label: [previewlink] });
form.append({ type: 'div', id: 'twinkletalkback-previewbox', style: 'display: none' });
form.append({ type: 'submit' });
var result = form.render();
Window.setContent(result);
Window.display();
result.previewer = new Morebits.wiki.Preview($(result).find('div#twinkletalkback-previewbox').last()[0]);
// We must init the
const evt = document.createEvent('Event');
evt.initEvent('change', true, true);
result.tbtarget[0].dispatchEvent(evt);
// Check whether the user has opted out from talkback
const query = {
action: 'query',
prop: 'extlinks',
titles: 'User talk:' + mw.config.get('wgRelevantUserName'),
elquery: 'userjs.invalid/noTalkback',
ellimit: '1',
format: 'json'
};
const wpapi = new Morebits.wiki.Api('Fetching talkback opt-out status', query, Twinkle.talkback.callback.optoutStatus);
wpapi.post();
};
Twinkle.talkback.optout = '';
Twinkle.talkback.callback.optoutStatus = function(apiobj) {
const el = apiobj.getResponse().query.pages[0].extlinks;
if (el && el.length) {
Twinkle.talkback.optout = mw.config.get('wgRelevantUserName') + ' prefers not to receive talkbacks';
const url = el[0].url;
const reason = mw.util.getParamValue('reason', url);
Twinkle.talkback.optout += reason ? ': ' + reason : '.';
}
$('#twinkle-talkback-optout-message').text(Twinkle.talkback.optout);
};
let prev_page = '';
let prev_section = '';
let prev_message = '';
Twinkle.talkback.changeTarget = function(e) {
const value = e.target.values;
const root = e.target.form;
const old_area = Morebits.QuickForm.getElements(root, 'work_area')[0];
if (root.section) {
prev_section = root.section.value;
}
if (root.message) {
prev_message = root.message.value;
}
if (root.page) {
prev_page = root.page.value;
}
let work_area = new Morebits.QuickForm.Element({
type: 'field',
label: 'Talkback information',
name: 'work_area'
});
root.previewer.closePreview();
switch (value) {
case 'talkback':
/* falls through */
default:
work_area.append({
type: 'div',
label: '',
style: 'color: red',
id: 'twinkle-talkback-optout-message'
});
work_area.append({
type: 'input',
name: 'page',
label: 'Page name of the discussion',
tooltip: "The page name where the discussion is being held. For example: 'User talk:Jimbo Wales' or Wikipedia talk:Twinkle'. Limited to all talks, Wikipedia-space, and Template-space.",
value: prev_page || 'User talk:' + mw.config.get('wgUserName')
});
work_area.append({
type: 'input',
name: 'section',
label: 'Linked section (optional)',
tooltip: "The section heading where the discussion is being held. For example: 'Merge proposal'.",
value: prev_section
});
break;
case 'notice':
var noticeboard = work_area.append({
type: 'select',
name: 'noticeboard',
label: 'Noticeboard:',
event: function(e) {
if (e.target.value === 'afchd') {
Morebits.QuickForm.overrideElementLabel(root.section, 'Title of draft (excluding the prefix): ');
Morebits.QuickForm.setElementTooltipVisibility(root.section, false);
} else {
Morebits.QuickForm.resetElementLabel(root.section);
Morebits.QuickForm.setElementTooltipVisibility(root.section, true);
}
}
});
$.each(Twinkle.talkback.noticeboards, (value, data) => {
noticeboard.append({
type: 'option',
label: data.label,
value: value,
selected: !!data.defaultSelected
});
});
work_area.append({
type: 'input',
name: 'section',
label: 'Linked thread',
tooltip: 'The heading of the relevant thread on the noticeboard page.',
value: prev_section
});
break;
case 'mail':
work_area.append({
type: 'input',
name: 'section',
label: 'Subject of email (optional)',
tooltip: 'The subject line of the email you sent.'
});
break;
}
if (value !== 'notice') {
work_area.append({ type: 'textarea', label: 'Additional message (optional):', name: 'message', tooltip: 'An additional message that you would like to leave below the talkback template. Your signature will be added to the end of the message if you leave one.' });
}
work_area = work_area.render();
root.replaceChild(work_area, old_area);
if (root.message) {
root.message.value = prev_message;
}
$('#twinkle-talkback-optout-message').text(Twinkle.talkback.optout);
};
Twinkle.talkback.noticeboards = {
ae: {
label: 'WP:AE (Arbitration/Requests/Enforcement)',
text: '{{subst:AE-notice|$SECTION}} ~~~~',
editSummary: 'Notice of discussion at [[Wikipedia:Arbitration/Requests/Enforcement]]'
},
an: {
label: "WP:AN (Administrators' noticeboard)",
text: '{{subst:AN-notice|thread=$SECTION}} ~~~~',
editSummary: 'Notice of discussion at [[Wikipedia:Administrators\' noticeboard]]'
},
an3: {
label: "WP:AN3 (Administrators' noticeboard/Edit warring)",
text: '{{subst:An3-notice|$SECTION}} ~~~~',
editSummary: "Notice of discussion at [[Wikipedia:Administrators' noticeboard/Edit warring]]"
},
ani: {
label: "WP:ANI (Administrators' noticeboard/Incidents)",
text: "== Notice of Administrators' noticeboard/Incidents discussion ==\n" +
'{{subst:ANI-notice|thread=$SECTION}} ~~~~',
editSummary: 'Notice of discussion at [[Wikipedia:Administrators\' noticeboard/Incidents]]',
defaultSelected: true
},
// let's keep AN and its cousins at the top
afchd: {
label: 'WP:AFCHD (Articles for creation/Help desk)',
text: '{{subst:AFCHD/u|$SECTION}} ~~~~',
editSummary: 'You have replies at the [[Wikipedia:AFCHD|Articles for Creation Help Desk]]'
},
blpn: {
label: 'WP:BLPN (Biographies of living persons noticeboard)',
text: '{{subst:BLPN-notice|thread=$SECTION}} ~~~~',
editSummary: 'Notice of discussion at [[Wikipedia:Biographies of living persons/Noticeboard]]'
},
coin: {
label: 'WP:COIN (Conflict of interest noticeboard)',
text: '{{subst:Coin-notice|thread=$SECTION}} ~~~~',
editSummary: 'Notice of discussion at [[Wikipedia:Conflict of interest/Noticeboard]]'
},
drn: {
label: 'WP:DRN (Dispute resolution noticeboard)',
text: '{{subst:DRN-notice|thread=$SECTION}} ~~~~',
editSummary: 'Notice of discussion at [[Wikipedia:Dispute resolution noticeboard]]'
},
effp: {
label: 'WP:EFFP/R (Edit filter false positive report)',
text: '{{EFFPReply|1=$SECTION|2=~~~~}}',
editSummary: 'You have replies to your [[Wikipedia:Edit filter/False positives/Reports|edit filter false positive report]]'
},
eln: {
label: 'WP:ELN (External links noticeboard)',
text: '{{subst:ELN-notice|thread=$SECTION}} ~~~~',
editSummary: 'Notice of discussion at [[Wikipedia:External links/Noticeboard]]'
},
ftn: {
label: 'WP:FTN (Fringe theories noticeboard)',
text: '{{subst:Ftn-notice|thread=$SECTION}} ~~~~',
editSummary: 'Notice of discussion at [[Wikipedia:Fringe theories/Noticeboard]]'
},
hd: {
label: 'WP:HD (Help desk)',
text: '== Your question at the Help desk ==\n{{helpdeskreply|1=$SECTION|ts=~~~~~}}',
editSummary: 'You have replies at the [[Wikipedia:Help desk|Wikipedia help desk]]'
},
norn: {
label: 'WP:NORN (No original research noticeboard)',
text: '{{subst:Norn-notice|thread=$SECTION}} ~~~~',
editSummary: 'Notice of discussion at [[Wikipedia:No original research/Noticeboard]]'
},
npovn: {
label: 'WP:NPOVN (Neutral point of view noticeboard)',
text: '{{subst:NPOVN-notice|thread=$SECTION}} ~~~~',
editSummary: 'Notice of discussion at [[Wikipedia:Neutral point of view/Noticeboard]]'
},
rsn: {
label: 'WP:RSN (Reliable sources noticeboard)',
text: '{{subst:RSN-notice|thread=$SECTION}} ~~~~',
editSummary: 'Notice of discussion at [[Wikipedia:Reliable sources/Noticeboard]]'
},
th: {
label: 'WP:TH (Teahouse)',
text: "== Teahouse talkback: you've got messages! ==\n{{WP:Teahouse/Teahouse talkback|WP:Teahouse|$SECTION|ts=~~~~}}",
editSummary: 'You have replies at the [[Wikipedia:Teahouse|Teahouse question board]]'
},
vrt: {
label: 'WP:VRTN (VRT noticeboard)',
text: '{{subst:VRTreply|1=$SECTION}}\n~~~~',
editSummary: 'You have replies at the [[Wikipedia:VRT noticeboard|VRT noticeboard]]'
}
};
Twinkle.talkback.evaluate = function(e) {
const input = Morebits.QuickForm.getInputData(e.target);
const fullUserTalkPageName = new mw.Title(mw.config.get('wgRelevantUserName'), 3).toText();
const talkpage = new Morebits.wiki.Page(fullUserTalkPageName, 'Adding talkback');
Morebits.SimpleWindow.setButtonsEnabled(false);
Morebits.Status.init(e.target);
Morebits.wiki.actionCompleted.redirect = fullUserTalkPageName;
Morebits.wiki.actionCompleted.notice = 'Talkback complete; reloading talk page in a few seconds';
switch (input.tbtarget) {
case 'notice':
talkpage.setEditSummary(Twinkle.talkback.noticeboards[input.noticeboard].editSummary);
break;
case 'mail':
talkpage.setEditSummary("Notification: You've got mail");
break;
case 'see':
input.page = Twinkle.talkback.callbacks.normalizeTalkbackPage(input.page);
talkpage.setEditSummary('Please check the discussion at [[:' + input.page +
(input.section ? '#' + input.section : '') + ']]');
break;
default: // talkback
input.page = Twinkle.talkback.callbacks.normalizeTalkbackPage(input.page);
talkpage.setEditSummary('Talkback ([[:' + input.page +
(input.section ? '#' + input.section : '') + ']])');
break;
}
talkpage.setFollowRedirect(true);
talkpage.load((pageobj) => {
const whitespaceToPrepend = pageobj.exists() && pageobj.getPageText() !== '' ? '\n\n' : '';
talkpage.setAppendText(whitespaceToPrepend + Twinkle.talkback.callbacks.getNoticeWikitext(input));
talkpage.setChangeTags(Twinkle.changeTags);
talkpage.setCreateOption('recreate');
talkpage.setMinorEdit(Twinkle.getPref('markTalkbackAsMinor'));
talkpage.append();
});
};
Twinkle.talkback.callbacks = {
// Not used for notice or mail, default to user page
normalizeTalkbackPage: function(page) {
page = page || mw.config.get('wgUserName');
// Assume no prefix is a username, convert to user talk space
let normal = mw.Title.newFromText(page, 3);
// Normalize erroneous or likely mis-entered items
if (normal) {
// Only allow talks and WPspace, as well as Template-space for DYK
if (normal.namespace !== 4 && normal.namespace !== 10) {
normal = normal.getTalkPage();
}
page = normal.getPrefixedText();
}
return page;
},
preview: function(form) {
const input = Morebits.QuickForm.getInputData(form);
if (input.tbtarget === 'talkback' || input.tbtarget === 'see') {
input.page = Twinkle.talkback.callbacks.normalizeTalkbackPage(input.page);
}
const noticetext = Twinkle.talkback.callbacks.getNoticeWikitext(input);
form.previewer.beginRender(noticetext, 'User talk:' + mw.config.get('wgRelevantUserName')); // Force wikitext/correct username
},
getNoticeWikitext: function(input) {
let text;
switch (input.tbtarget) {
case 'notice':
text = Morebits.string.safeReplace(Twinkle.talkback.noticeboards[input.noticeboard].text, '$SECTION', input.section);
break;
case 'mail':
text = '==' + Twinkle.getPref('mailHeading') + '==\n' +
"{{You've got mail|subject=" + input.section + '|ts=~~~~~}}';
if (input.message) {
text += '\n' + input.message + ' ~~~~';
} else if (Twinkle.getPref('insertTalkbackSignature')) {
text += '\n~~~~';
}
break;
case 'see':
var heading = Twinkle.getPref('talkbackHeading');
text = '{{subst:Please see|location=' + input.page + (input.section ? '#' + input.section : '') +
'|more=' + input.message + '|heading=' + heading + '}}';
break;
default: // talkback
text = '==' + Twinkle.getPref('talkbackHeading') + '==\n' +
'{{talkback|' + input.page + (input.section ? '|' + input.section : '') + '|ts=~~~~~}}';
if (input.message) {
text += '\n' + input.message + ' ~~~~';
} else if (Twinkle.getPref('insertTalkbackSignature')) {
text += '\n~~~~';
}
}
return text;
}
};
Twinkle.addInitCallback(Twinkle.talkback, 'talkback');
}());
// </nowiki>
0wp5z9cc4pcfpoj7gyh37dq03tvjplh
769535
769468
2026-10-22T00:00:49Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 1406828: rm cmt
769535
javascript
text/javascript
// <nowiki>
(function() {
/*
****************************************
*** twinkletalkback.js: Talkback module
****************************************
* Mode of invocation: Tab ("TB")
* Active on: Any page with relevant user name (userspace, contribs, etc.) except IP ranges
*/
Twinkle.talkback = function() {
if (!mw.config.exists('wgRelevantUserName') || Morebits.ip.isRange(mw.config.get('wgRelevantUserName'))) {
return;
}
Twinkle.addPortletLink(Twinkle.talkback.callback, 'TB', 'twinkle-talkback', 'Easy talkback');
};
Twinkle.talkback.callback = function() {
if (mw.config.get('wgRelevantUserName') === mw.config.get('wgUserName') && !confirm("Is it really so bad that you're talking back to yourself?")) {
return;
}
const Window = new Morebits.SimpleWindow(600, 350);
Window.setTitle('Talkback');
Window.setScriptName('Twinkle');
Window.addFooterLink('Talkback prefs', 'WP:TW/PREF#talkback');
Window.addFooterLink('Twinkle help', 'WP:TW/DOC#talkback');
Window.addFooterLink('Give feedback', 'WT:TW');
const form = new Morebits.QuickForm(Twinkle.talkback.evaluate);
form.append({ type: 'radio', name: 'tbtarget',
list: [
{
label: 'Talkback',
value: 'talkback',
checked: 'true'
},
{
label: 'Please see',
value: 'see'
},
{
label: 'Noticeboard notification',
value: 'notice'
},
{
label: "You've got mail",
value: 'mail'
}
],
event: Twinkle.talkback.changeTarget
});
form.append({
type: 'field',
label: 'Work area',
name: 'work_area'
});
const previewlink = document.createElement('a');
$(previewlink).on('click', () => {
Twinkle.talkback.callbacks.preview(result); // |result| is defined below
});
previewlink.style.cursor = 'pointer';
previewlink.textContent = 'Preview';
form.append({ type: 'div', id: 'talkbackpreview', label: [previewlink] });
form.append({ type: 'div', id: 'twinkletalkback-previewbox', style: 'display: none' });
form.append({ type: 'submit' });
var result = form.render();
Window.setContent(result);
Window.display();
result.previewer = new Morebits.wiki.Preview($(result).find('div#twinkletalkback-previewbox').last()[0]);
// We must init the
const evt = document.createEvent('Event');
evt.initEvent('change', true, true);
result.tbtarget[0].dispatchEvent(evt);
// Check whether the user has opted out from talkback
const query = {
action: 'query',
prop: 'extlinks',
titles: 'User talk:' + mw.config.get('wgRelevantUserName'),
elquery: 'userjs.invalid/noTalkback',
ellimit: '1',
format: 'json'
};
const wpapi = new Morebits.wiki.Api('Fetching talkback opt-out status', query, Twinkle.talkback.callback.optoutStatus);
wpapi.post();
};
Twinkle.talkback.optout = '';
Twinkle.talkback.callback.optoutStatus = function(apiobj) {
const el = apiobj.getResponse().query.pages[0].extlinks;
if (el && el.length) {
Twinkle.talkback.optout = mw.config.get('wgRelevantUserName') + ' prefers not to receive talkbacks';
const url = el[0].url;
const reason = mw.util.getParamValue('reason', url);
Twinkle.talkback.optout += reason ? ': ' + reason : '.';
}
$('#twinkle-talkback-optout-message').text(Twinkle.talkback.optout);
};
let prev_page = '';
let prev_section = '';
let prev_message = '';
Twinkle.talkback.changeTarget = function(e) {
const value = e.target.values;
const root = e.target.form;
const old_area = Morebits.QuickForm.getElements(root, 'work_area')[0];
if (root.section) {
prev_section = root.section.value;
}
if (root.message) {
prev_message = root.message.value;
}
if (root.page) {
prev_page = root.page.value;
}
let work_area = new Morebits.QuickForm.Element({
type: 'field',
label: 'Talkback information',
name: 'work_area'
});
root.previewer.closePreview();
switch (value) {
case 'talkback':
/* falls through */
default:
work_area.append({
type: 'div',
label: '',
style: 'color: red',
id: 'twinkle-talkback-optout-message'
});
work_area.append({
type: 'input',
name: 'page',
label: 'Page name of the discussion',
tooltip: "The page name where the discussion is being held. For example: 'User talk:Jimbo Wales' or Wikipedia talk:Twinkle'. Limited to all talks, Wikipedia-space, and Template-space.",
value: prev_page || 'User talk:' + mw.config.get('wgUserName')
});
work_area.append({
type: 'input',
name: 'section',
label: 'Linked section (optional)',
tooltip: "The section heading where the discussion is being held. For example: 'Merge proposal'.",
value: prev_section
});
break;
case 'notice':
var noticeboard = work_area.append({
type: 'select',
name: 'noticeboard',
label: 'Noticeboard:',
event: function(e) {
if (e.target.value === 'afchd') {
Morebits.QuickForm.overrideElementLabel(root.section, 'Title of draft (excluding the prefix): ');
Morebits.QuickForm.setElementTooltipVisibility(root.section, false);
} else {
Morebits.QuickForm.resetElementLabel(root.section);
Morebits.QuickForm.setElementTooltipVisibility(root.section, true);
}
}
});
$.each(Twinkle.talkback.noticeboards, (value, data) => {
noticeboard.append({
type: 'option',
label: data.label,
value: value,
selected: !!data.defaultSelected
});
});
work_area.append({
type: 'input',
name: 'section',
label: 'Linked thread',
tooltip: 'The heading of the relevant thread on the noticeboard page.',
value: prev_section
});
break;
case 'mail':
work_area.append({
type: 'input',
name: 'section',
label: 'Subject of email (optional)',
tooltip: 'The subject line of the email you sent.'
});
break;
}
if (value !== 'notice') {
work_area.append({ type: 'textarea', label: 'Additional message (optional):', name: 'message', tooltip: 'An additional message that you would like to leave below the talkback template. Your signature will be added to the end of the message if you leave one.' });
}
work_area = work_area.render();
root.replaceChild(work_area, old_area);
if (root.message) {
root.message.value = prev_message;
}
$('#twinkle-talkback-optout-message').text(Twinkle.talkback.optout);
};
Twinkle.talkback.noticeboards = {
ae: {
label: 'WP:AE (Arbitration/Requests/Enforcement)',
text: '{{subst:AE-notice|$SECTION}} ~~~~',
editSummary: 'Notice of discussion at [[Wikipedia:Arbitration/Requests/Enforcement]]'
},
an: {
label: "WP:AN (Administrators' noticeboard)",
text: '{{subst:AN-notice|thread=$SECTION}} ~~~~',
editSummary: 'Notice of discussion at [[Wikipedia:Administrators\' noticeboard]]'
},
an3: {
label: "WP:AN3 (Administrators' noticeboard/Edit warring)",
text: '{{subst:An3-notice|$SECTION}} ~~~~',
editSummary: "Notice of discussion at [[Wikipedia:Administrators' noticeboard/Edit warring]]"
},
ani: {
label: "WP:ANI (Administrators' noticeboard/Incidents)",
text: "== Notice of Administrators' noticeboard/Incidents discussion ==\n" +
'{{subst:ANI-notice|thread=$SECTION}} ~~~~',
editSummary: 'Notice of discussion at [[Wikipedia:Administrators\' noticeboard/Incidents]]',
defaultSelected: true
},
// let's keep AN and its cousins at the top
afchd: {
label: 'WP:AFCHD (Articles for creation/Help desk)',
text: '{{subst:AFCHD/u|$SECTION}} ~~~~',
editSummary: 'You have replies at the [[Wikipedia:AFCHD|Articles for Creation Help Desk]]'
},
blpn: {
label: 'WP:BLPN (Biographies of living persons noticeboard)',
text: '{{subst:BLPN-notice|thread=$SECTION}} ~~~~',
editSummary: 'Notice of discussion at [[Wikipedia:Biographies of living persons/Noticeboard]]'
},
coin: {
label: 'WP:COIN (Conflict of interest noticeboard)',
text: '{{subst:Coin-notice|thread=$SECTION}} ~~~~',
editSummary: 'Notice of discussion at [[Wikipedia:Conflict of interest/Noticeboard]]'
},
drn: {
label: 'WP:DRN (Dispute resolution noticeboard)',
text: '{{subst:DRN-notice|thread=$SECTION}} ~~~~',
editSummary: 'Notice of discussion at [[Wikipedia:Dispute resolution noticeboard]]'
},
effp: {
label: 'WP:EFFP/R (Edit filter false positive report)',
text: '{{EFFPReply|1=$SECTION|2=~~~~}}',
editSummary: 'You have replies to your [[Wikipedia:Edit filter/False positives/Reports|edit filter false positive report]]'
},
eln: {
label: 'WP:ELN (External links noticeboard)',
text: '{{subst:ELN-notice|thread=$SECTION}} ~~~~',
editSummary: 'Notice of discussion at [[Wikipedia:External links/Noticeboard]]'
},
ftn: {
label: 'WP:FTN (Fringe theories noticeboard)',
text: '{{subst:Ftn-notice|thread=$SECTION}} ~~~~',
editSummary: 'Notice of discussion at [[Wikipedia:Fringe theories/Noticeboard]]'
},
hd: {
label: 'WP:HD (Help desk)',
text: '== Your question at the Help desk ==\n{{helpdeskreply|1=$SECTION|ts=~~~~~}}',
editSummary: 'You have replies at the [[Wikipedia:Help desk|Wikipedia help desk]]'
},
norn: {
label: 'WP:NORN (No original research noticeboard)',
text: '{{subst:Norn-notice|thread=$SECTION}} ~~~~',
editSummary: 'Notice of discussion at [[Wikipedia:No original research/Noticeboard]]'
},
npovn: {
label: 'WP:NPOVN (Neutral point of view noticeboard)',
text: '{{subst:NPOVN-notice|thread=$SECTION}} ~~~~',
editSummary: 'Notice of discussion at [[Wikipedia:Neutral point of view/Noticeboard]]'
},
rsn: {
label: 'WP:RSN (Reliable sources noticeboard)',
text: '{{subst:RSN-notice|thread=$SECTION}} ~~~~',
editSummary: 'Notice of discussion at [[Wikipedia:Reliable sources/Noticeboard]]'
},
th: {
label: 'WP:TH (Teahouse)',
text: "== Teahouse talkback: you've got messages! ==\n{{WP:Teahouse/Teahouse talkback|WP:Teahouse|$SECTION|ts=~~~~}}",
editSummary: 'You have replies at the [[Wikipedia:Teahouse|Teahouse question board]]'
},
vrt: {
label: 'WP:VRTN (VRT noticeboard)',
text: '{{subst:VRTreply|1=$SECTION}}\n~~~~',
editSummary: 'You have replies at the [[Wikipedia:VRT noticeboard|VRT noticeboard]]'
}
};
Twinkle.talkback.evaluate = function(e) {
const input = Morebits.QuickForm.getInputData(e.target);
const fullUserTalkPageName = new mw.Title(mw.config.get('wgRelevantUserName'), 3).toText();
const talkpage = new Morebits.wiki.Page(fullUserTalkPageName, 'Adding talkback');
Morebits.SimpleWindow.setButtonsEnabled(false);
Morebits.Status.init(e.target);
Morebits.wiki.actionCompleted.redirect = fullUserTalkPageName;
Morebits.wiki.actionCompleted.notice = 'Talkback complete; reloading talk page in a few seconds';
switch (input.tbtarget) {
case 'notice':
talkpage.setEditSummary(Twinkle.talkback.noticeboards[input.noticeboard].editSummary);
break;
case 'mail':
talkpage.setEditSummary("Notification: You've got mail");
break;
case 'see':
input.page = Twinkle.talkback.callbacks.normalizeTalkbackPage(input.page);
talkpage.setEditSummary('Please check the discussion at [[:' + input.page +
(input.section ? '#' + input.section : '') + ']]');
break;
default: // talkback
input.page = Twinkle.talkback.callbacks.normalizeTalkbackPage(input.page);
talkpage.setEditSummary('Talkback ([[:' + input.page +
(input.section ? '#' + input.section : '') + ']])');
break;
}
talkpage.setFollowRedirect(true);
talkpage.load((pageobj) => {
const whitespaceToPrepend = pageobj.exists() && pageobj.getPageText() !== '' ? '\n\n' : '';
talkpage.setAppendText(whitespaceToPrepend + Twinkle.talkback.callbacks.getNoticeWikitext(input));
talkpage.setChangeTags(Twinkle.changeTags);
talkpage.setCreateOption('recreate');
talkpage.setMinorEdit(Twinkle.getPref('markTalkbackAsMinor'));
talkpage.append();
});
};
Twinkle.talkback.callbacks = {
// Not used for notice or mail, default to user page
normalizeTalkbackPage: function(page) {
page = page || mw.config.get('wgUserName');
// Assume no prefix is a username, convert to user talk space
let normal = mw.Title.newFromText(page, 3);
// Normalize erroneous or likely mis-entered items
if (normal) {
// Only allow talks and WPspace, as well as Template-space for DYK
if (normal.namespace !== 4 && normal.namespace !== 10) {
normal = normal.getTalkPage();
}
page = normal.getPrefixedText();
}
return page;
},
preview: function(form) {
const input = Morebits.QuickForm.getInputData(form);
if (input.tbtarget === 'talkback' || input.tbtarget === 'see') {
input.page = Twinkle.talkback.callbacks.normalizeTalkbackPage(input.page);
}
const noticetext = Twinkle.talkback.callbacks.getNoticeWikitext(input);
form.previewer.beginRender(noticetext, 'User talk:' + mw.config.get('wgRelevantUserName')); // Force wikitext/correct username
},
getNoticeWikitext: function(input) {
let text;
switch (input.tbtarget) {
case 'notice':
text = Morebits.string.safeReplace(Twinkle.talkback.noticeboards[input.noticeboard].text, '$SECTION', input.section);
break;
case 'mail':
text = '==' + Twinkle.getPref('mailHeading') + '==\n' +
"{{You've got mail|subject=" + input.section + '|ts=~~~~~}}';
if (input.message) {
text += '\n' + input.message + ' ~~~~';
} else if (Twinkle.getPref('insertTalkbackSignature')) {
text += '\n~~~~';
}
break;
case 'see':
var heading = Twinkle.getPref('talkbackHeading');
text = '{{subst:Please see|location=' + input.page + (input.section ? '#' + input.section : '') +
'|more=' + input.message + '|heading=' + heading + '}}';
break;
default: // talkback
text = '==' + Twinkle.getPref('talkbackHeading') + '==\n' +
'{{talkback|' + input.page + (input.section ? '|' + input.section : '') + '|ts=~~~~~}}';
if (input.message) {
text += '\n' + input.message + ' ~~~~';
} else if (Twinkle.getPref('insertTalkbackSignature')) {
text += '\n~~~~';
}
}
return text;
}
};
Twinkle.addInitCallback(Twinkle.talkback, 'talkback');
}());
// </nowiki>
9tg032ju1e7z9q57cj1nu3gpbdmjxkt
769572
769535
2026-10-22T02:26:47Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at d9cd3461baca43ac99ac2c24fb76a3aae6ddada9: minify
769572
javascript
text/javascript
// <nowiki>
(function(){Twinkle.talkback=function(){if(!mw.config.exists("wgRelevantUserName")||Morebits.ip.isRange(mw.config.get("wgRelevantUserName"))){return}Twinkle.addPortletLink(Twinkle.talkback.callback,"TB","twinkle-talkback","Easy talkback")};Twinkle.talkback.callback=function(){if(mw.config.get("wgRelevantUserName")===mw.config.get("wgUserName")&&!confirm("Is it really so bad that you're talking back to yourself?")){return}const Window=new Morebits.SimpleWindow(600,350);Window.setTitle("Talkback");Window.setScriptName("Twinkle");Window.addFooterLink("Talkback prefs","WP:TW/PREF#talkback");Window.addFooterLink("Twinkle help","WP:TW/DOC#talkback");Window.addFooterLink("Give feedback","WT:TW");const form=new Morebits.QuickForm(Twinkle.talkback.evaluate);form.append({type:"radio",name:"tbtarget",list:[{label:"Talkback",value:"talkback",checked:"true"},{label:"Please see",value:"see"},{label:"Noticeboard notification",value:"notice"},{label:"You've got mail",value:"mail"}],event:Twinkle.talkback.changeTarget});form.append({type:"field",label:"Work area",name:"work_area"});const previewlink=document.createElement("a");$(previewlink).on("click",()=>{Twinkle.talkback.callbacks.preview(result)});previewlink.style.cursor="pointer";previewlink.textContent="Preview";form.append({type:"div",id:"talkbackpreview",label:[previewlink]});form.append({type:"div",id:"twinkletalkback-previewbox",style:"display: none"});form.append({type:"submit"});var result=form.render();Window.setContent(result);Window.display();result.previewer=new Morebits.wiki.Preview($(result).find("div#twinkletalkback-previewbox").last()[0]);const evt=document.createEvent("Event");evt.initEvent("change",true,true);result.tbtarget[0].dispatchEvent(evt);const query={action:"query",prop:"extlinks",titles:"User talk:"+mw.config.get("wgRelevantUserName"),elquery:"userjs.invalid/noTalkback",ellimit:"1",format:"json"};const wpapi=new Morebits.wiki.Api("Fetching talkback opt-out status",query,Twinkle.talkback.callback.optoutStatus);wpapi.post()};Twinkle.talkback.optout="";Twinkle.talkback.callback.optoutStatus=function(apiobj){const el=apiobj.getResponse().query.pages[0].extlinks;if(el&&el.length){Twinkle.talkback.optout=mw.config.get("wgRelevantUserName")+" prefers not to receive talkbacks";const url=el[0].url;const reason=mw.util.getParamValue("reason",url);Twinkle.talkback.optout+=reason?": "+reason:"."}$("#twinkle-talkback-optout-message").text(Twinkle.talkback.optout)};let prev_page="";let prev_section="";let prev_message="";Twinkle.talkback.changeTarget=function(e){const value=e.target.values;const root=e.target.form;const old_area=Morebits.QuickForm.getElements(root,"work_area")[0];if(root.section){prev_section=root.section.value}if(root.message){prev_message=root.message.value}if(root.page){prev_page=root.page.value}let work_area=new Morebits.QuickForm.Element({type:"field",label:"Talkback information",name:"work_area"});root.previewer.closePreview();switch(value){case"talkback":default:work_area.append({type:"div",label:"",style:"color: red",id:"twinkle-talkback-optout-message"});work_area.append({type:"input",name:"page",label:"Page name of the discussion",tooltip:"The page name where the discussion is being held. For example: 'User talk:Jimbo Wales' or Wikipedia talk:Twinkle'. Limited to all talks, Wikipedia-space, and Template-space.",value:prev_page||"User talk:"+mw.config.get("wgUserName")});work_area.append({type:"input",name:"section",label:"Linked section (optional)",tooltip:"The section heading where the discussion is being held. For example: 'Merge proposal'.",value:prev_section});break;case"notice":var noticeboard=work_area.append({type:"select",name:"noticeboard",label:"Noticeboard:",event:function(e){if(e.target.value==="afchd"){Morebits.QuickForm.overrideElementLabel(root.section,"Title of draft (excluding the prefix): ");Morebits.QuickForm.setElementTooltipVisibility(root.section,false)}else{Morebits.QuickForm.resetElementLabel(root.section);Morebits.QuickForm.setElementTooltipVisibility(root.section,true)}}});$.each(Twinkle.talkback.noticeboards,(value,data)=>{noticeboard.append({type:"option",label:data.label,value:value,selected:!!data.defaultSelected})});work_area.append({type:"input",name:"section",label:"Linked thread",tooltip:"The heading of the relevant thread on the noticeboard page.",value:prev_section});break;case"mail":work_area.append({type:"input",name:"section",label:"Subject of email (optional)",tooltip:"The subject line of the email you sent."});break}if(value!=="notice"){work_area.append({type:"textarea",label:"Additional message (optional):",name:"message",tooltip:"An additional message that you would like to leave below the talkback template. Your signature will be added to the end of the message if you leave one."})}work_area=work_area.render();root.replaceChild(work_area,old_area);if(root.message){root.message.value=prev_message}$("#twinkle-talkback-optout-message").text(Twinkle.talkback.optout)};Twinkle.talkback.noticeboards={ae:{label:"WP:AE (Arbitration/Requests/Enforcement)",text:"{{subst:AE-notice|$SECTION}} ~~~~",editSummary:"Notice of discussion at [[Wikipedia:Arbitration/Requests/Enforcement]]"},an:{label:"WP:AN (Administrators' noticeboard)",text:"{{subst:AN-notice|thread=$SECTION}} ~~~~",editSummary:"Notice of discussion at [[Wikipedia:Administrators' noticeboard]]"},an3:{label:"WP:AN3 (Administrators' noticeboard/Edit warring)",text:"{{subst:An3-notice|$SECTION}} ~~~~",editSummary:"Notice of discussion at [[Wikipedia:Administrators' noticeboard/Edit warring]]"},ani:{label:"WP:ANI (Administrators' noticeboard/Incidents)",text:"== Notice of Administrators' noticeboard/Incidents discussion ==\n"+"{{subst:ANI-notice|thread=$SECTION}} ~~~~",editSummary:"Notice of discussion at [[Wikipedia:Administrators' noticeboard/Incidents]]",defaultSelected:true},afchd:{label:"WP:AFCHD (Articles for creation/Help desk)",text:"{{subst:AFCHD/u|$SECTION}} ~~~~",editSummary:"You have replies at the [[Wikipedia:AFCHD|Articles for Creation Help Desk]]"},blpn:{label:"WP:BLPN (Biographies of living persons noticeboard)",text:"{{subst:BLPN-notice|thread=$SECTION}} ~~~~",editSummary:"Notice of discussion at [[Wikipedia:Biographies of living persons/Noticeboard]]"},coin:{label:"WP:COIN (Conflict of interest noticeboard)",text:"{{subst:Coin-notice|thread=$SECTION}} ~~~~",editSummary:"Notice of discussion at [[Wikipedia:Conflict of interest/Noticeboard]]"},drn:{label:"WP:DRN (Dispute resolution noticeboard)",text:"{{subst:DRN-notice|thread=$SECTION}} ~~~~",editSummary:"Notice of discussion at [[Wikipedia:Dispute resolution noticeboard]]"},effp:{label:"WP:EFFP/R (Edit filter false positive report)",text:"{{EFFPReply|1=$SECTION|2=~~~~}}",editSummary:"You have replies to your [[Wikipedia:Edit filter/False positives/Reports|edit filter false positive report]]"},eln:{label:"WP:ELN (External links noticeboard)",text:"{{subst:ELN-notice|thread=$SECTION}} ~~~~",editSummary:"Notice of discussion at [[Wikipedia:External links/Noticeboard]]"},ftn:{label:"WP:FTN (Fringe theories noticeboard)",text:"{{subst:Ftn-notice|thread=$SECTION}} ~~~~",editSummary:"Notice of discussion at [[Wikipedia:Fringe theories/Noticeboard]]"},hd:{label:"WP:HD (Help desk)",text:"== Your question at the Help desk ==\n{{helpdeskreply|1=$SECTION|ts=~~~~~}}",editSummary:"You have replies at the [[Wikipedia:Help desk|Wikipedia help desk]]"},norn:{label:"WP:NORN (No original research noticeboard)",text:"{{subst:Norn-notice|thread=$SECTION}} ~~~~",editSummary:"Notice of discussion at [[Wikipedia:No original research/Noticeboard]]"},npovn:{label:"WP:NPOVN (Neutral point of view noticeboard)",text:"{{subst:NPOVN-notice|thread=$SECTION}} ~~~~",editSummary:"Notice of discussion at [[Wikipedia:Neutral point of view/Noticeboard]]"},rsn:{label:"WP:RSN (Reliable sources noticeboard)",text:"{{subst:RSN-notice|thread=$SECTION}} ~~~~",editSummary:"Notice of discussion at [[Wikipedia:Reliable sources/Noticeboard]]"},th:{label:"WP:TH (Teahouse)",text:"== Teahouse talkback: you've got messages! ==\n{{WP:Teahouse/Teahouse talkback|WP:Teahouse|$SECTION|ts=~~~~}}",editSummary:"You have replies at the [[Wikipedia:Teahouse|Teahouse question board]]"},vrt:{label:"WP:VRTN (VRT noticeboard)",text:"{{subst:VRTreply|1=$SECTION}}\n~~~~",editSummary:"You have replies at the [[Wikipedia:VRT noticeboard|VRT noticeboard]]"}};Twinkle.talkback.evaluate=function(e){const input=Morebits.QuickForm.getInputData(e.target);const fullUserTalkPageName=new mw.Title(mw.config.get("wgRelevantUserName"),3).toText();const talkpage=new Morebits.wiki.Page(fullUserTalkPageName,"Adding talkback");Morebits.SimpleWindow.setButtonsEnabled(false);Morebits.Status.init(e.target);Morebits.wiki.actionCompleted.redirect=fullUserTalkPageName;Morebits.wiki.actionCompleted.notice="Talkback complete; reloading talk page in a few seconds";switch(input.tbtarget){case"notice":talkpage.setEditSummary(Twinkle.talkback.noticeboards[input.noticeboard].editSummary);break;case"mail":talkpage.setEditSummary("Notification: You've got mail");break;case"see":input.page=Twinkle.talkback.callbacks.normalizeTalkbackPage(input.page);talkpage.setEditSummary("Please check the discussion at [[:"+input.page+(input.section?"#"+input.section:"")+"]]");break;default:input.page=Twinkle.talkback.callbacks.normalizeTalkbackPage(input.page);talkpage.setEditSummary("Talkback ([[:"+input.page+(input.section?"#"+input.section:"")+"]])");break}talkpage.setFollowRedirect(true);talkpage.load(pageobj=>{const whitespaceToPrepend=pageobj.exists()&&pageobj.getPageText()!==""?"\n\n":"";talkpage.setAppendText(whitespaceToPrepend+Twinkle.talkback.callbacks.getNoticeWikitext(input));talkpage.setChangeTags(Twinkle.changeTags);talkpage.setCreateOption("recreate");talkpage.setMinorEdit(Twinkle.getPref("markTalkbackAsMinor"));talkpage.append()})};Twinkle.talkback.callbacks={normalizeTalkbackPage:function(page){page=page||mw.config.get("wgUserName");let normal=mw.Title.newFromText(page,3);if(normal){if(normal.namespace!==4&&normal.namespace!==10){normal=normal.getTalkPage()}page=normal.getPrefixedText()}return page},preview:function(form){const input=Morebits.QuickForm.getInputData(form);if(input.tbtarget==="talkback"||input.tbtarget==="see"){input.page=Twinkle.talkback.callbacks.normalizeTalkbackPage(input.page)}const noticetext=Twinkle.talkback.callbacks.getNoticeWikitext(input);form.previewer.beginRender(noticetext,"User talk:"+mw.config.get("wgRelevantUserName"))},getNoticeWikitext:function(input){let text;switch(input.tbtarget){case"notice":text=Morebits.string.safeReplace(Twinkle.talkback.noticeboards[input.noticeboard].text,"$SECTION",input.section);break;case"mail":text="=="+Twinkle.getPref("mailHeading")+"==\n"+"{{You've got mail|subject="+input.section+"|ts=~~~~~}}";if(input.message){text+="\n"+input.message+" ~~~~"}else if(Twinkle.getPref("insertTalkbackSignature")){text+="\n~~~~"}break;case"see":var heading=Twinkle.getPref("talkbackHeading");text="{{subst:Please see|location="+input.page+(input.section?"#"+input.section:"")+"|more="+input.message+"|heading="+heading+"}}";break;default:text="=="+Twinkle.getPref("talkbackHeading")+"==\n"+"{{talkback|"+input.page+(input.section?"|"+input.section:"")+"|ts=~~~~~}}";if(input.message){text+="\n"+input.message+" ~~~~"}else if(Twinkle.getPref("insertTalkbackSignature")){text+="\n~~~~"}}return text}};Twinkle.addInitCallback(Twinkle.talkback,"talkback")})();
// </nowiki>
h0ma6whswcrfmnx4ygmzj9t8asr4hxj
User:Syunsyunminmin/Twinkle/twinklexfd.js
2
179404
769469
2026-10-21T13:58:16Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 3680108:
769469
javascript
text/javascript
/*
Twinkle: adapted for Japanese Wikipedia; upstream authors and Japanese adaptations are credited in UPSTREAM.md.
Upstream source: https://github.com/wikimedia-gadgets/twinkle/blob/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/src/modules/twinklexfd.js
Upstream author history: https://github.com/wikimedia-gadgets/twinkle/commits/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/src/modules/twinklexfd.js
License: CC BY-SA 3.0 Unported. See https://github.com/wikimedia-gadgets/twinkle/blob/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/LICENSE.txt
Japanese adaptation: Syunsyunminmin; includes later synchronization changes.
*/
// <nowiki>
(function() {
/*
****************************************
*** twinklexfd.js: XFD module
****************************************
* Mode of invocation: Tab ("XFD")
* Active on: Existing, non-special pages, except for file pages with no local (non-Commons) file which are not redirects
*/
Twinkle.xfd = function twinklexfd() {
// Disable on:
// * special pages
// * non-existent pages
// * files on Commons, whether there is a local page or not (unneeded local pages of files on Commons are eligible for CSD F2, or R4 if it's a redirect)
if (mw.config.get('wgNamespaceNumber') < 0 || !mw.config.get('wgArticleId') || (mw.config.get('wgNamespaceNumber') === 6 && document.getElementById('mw-sharedupload'))) {
return;
}
let tooltip = 'Start a discussion for deleting';
if (mw.config.get('wgIsRedirect')) {
tooltip += ' or retargeting this redirect';
} else {
switch (mw.config.get('wgNamespaceNumber')) {
case 0:
tooltip += ' or moving this article';
break;
case 10:
tooltip += ' or merging this template';
break;
case 828:
tooltip += ' or merging this module';
break;
case 6:
tooltip += ' this file';
break;
case 14:
tooltip += ', merging or renaming this category';
break;
default:
tooltip += ' this page';
break;
}
}
Twinkle.addPortletLink(Twinkle.xfd.callback, 'XFD', 'tw-xfd', tooltip);
};
const utils = {
/** Get ordinal number figure */
num2order: function(num) {
switch (num) {
case 1: return '';
case 2: return '2nd';
case 3: return '3rd';
default: return num + 'th';
}
},
/**
* Remove namespace name from title if present
* Exception-safe wrapper around mw.Title
*
* @param {string} title
*/
stripNs: function(title) {
const title_obj = mw.Title.newFromUserInput(title);
if (!title_obj) {
return title; // user entered invalid input; do nothing
}
return title_obj.getMainText();
},
/**
* Add namespace name to page title if not already given
* CAUTION: namespace name won't be added if a namespace (*not* necessarily
* the same as the one given) already is there in the title
*
* @param {string} title
* @param {number} namespaceNumber
*/
addNs: function(title, namespaceNumber) {
const title_obj = mw.Title.newFromUserInput(title, namespaceNumber);
if (!title_obj) {
return title; // user entered invalid input; do nothing
}
return title_obj.toText();
},
/**
* Provide Wikipedian TLA style: AfD, RfD, CfDS, RM, SfD, etc.
*
* @param {string} venue
* @return {string}
*/
toTLACase: function(venue) {
return venue
.toString()
// Everybody up, inclduing rm and the terminal s in cfds
.toUpperCase()
// Lowercase the central f in a given TLA and normalize sfd-t and sfr-t
.replace(/(.)F(.)(?:-.)?/, '$1f$2');
}
};
Twinkle.xfd.currentRationale = null;
// error callback on Morebits.Status.object
Twinkle.xfd.printRationale = function twinklexfdPrintRationale() {
if (Twinkle.xfd.currentRationale) {
Morebits.Status.printUserText(Twinkle.xfd.currentRationale, 'Your deletion rationale is provided below, which you can copy and paste into a new XFD dialog if you wish to try again:');
// only need to print the rationale once
Twinkle.xfd.currentRationale = null;
}
};
Twinkle.xfd.callback = function twinklexfdCallback() {
const Window = new Morebits.SimpleWindow(700, 400);
Window.setTitle('Start a deletion discussion (XfD)');
Window.setScriptName('Twinkle');
Window.addFooterLink('About deletion discussions', 'WP:XFD');
Window.addFooterLink('XfD prefs', 'WP:TW/PREF#xfd');
Window.addFooterLink('Twinkle help', 'WP:TW/DOC#xfd');
Window.addFooterLink('Give feedback', 'WT:TW');
const form = new Morebits.QuickForm(Twinkle.xfd.callback.evaluate);
const categories = form.append({
type: 'select',
name: 'venue',
label: 'Deletion discussion venue:',
tooltip: 'When activated, a default choice is made, based on what namespace you are in. This default should be the most appropriate.',
event: Twinkle.xfd.callback.change_category
});
const namespace = mw.config.get('wgNamespaceNumber');
categories.append({
type: 'option',
label: 'AfD (Articles for deletion)',
selected: namespace === 0, // Main namespace
value: 'afd'
});
categories.append({
type: 'option',
label: 'TfD (Templates for discussion)',
selected: [10, 828].includes(namespace), // Template and module namespaces
value: 'tfd'
});
categories.append({
type: 'option',
label: 'FfD (Files for discussion)',
selected: namespace === 6, // File namespace
value: 'ffd'
});
categories.append({
type: 'option',
label: 'CfD (Categories for discussion)',
selected: namespace === 14 || (namespace === 10 && /-stub$/.test(Morebits.pageNameNorm)), // Category namespace and stub templates
value: 'cfd'
});
categories.append({
type: 'option',
label: 'CfD/S (Categories for speedy renaming)',
value: 'cfds'
});
categories.append({
type: 'option',
label: 'MfD (Miscellany for deletion)',
selected: ![0, 6, 10, 14, 828].includes(namespace) || Morebits.pageNameNorm.indexOf('Template:User ', 0) === 0,
// Other namespaces, and userboxes in template namespace
value: 'mfd'
});
categories.append({
type: 'option',
label: 'RfD (Redirects for discussion)',
selected: mw.config.get('wgIsRedirect'),
value: 'rfd'
});
categories.append({
type: 'option',
label: 'RM (Requested moves)',
selected: false,
value: 'rm'
});
form.append({
type: 'div',
id: 'wrong-venue-warn',
style: 'color: red; font-style: italic'
});
form.append({
type: 'checkbox',
list: [
{
label: 'Notify page creator if possible',
value: 'notify',
name: 'notifycreator',
tooltip: "A notification template will be placed on the creator's talk page if this is true.",
checked: true
}
]
});
form.append({
type: 'field',
label: 'Work area',
name: 'work_area'
});
const previewlink = document.createElement('a');
$(previewlink).on('click', () => {
Twinkle.xfd.callbacks.preview(result); // |result| is defined below
});
previewlink.style.cursor = 'pointer';
previewlink.textContent = 'Preview';
form.append({ type: 'div', id: 'xfdpreview', label: [previewlink] });
form.append({ type: 'div', id: 'twinklexfd-previewbox', style: 'display: none' });
form.append({ type: 'submit' });
var result = form.render();
Window.setContent(result);
Window.display();
result.previewer = new Morebits.wiki.Preview($(result).find('div#twinklexfd-previewbox').last()[0]);
// We must init the controls
const evt = document.createEvent('Event');
evt.initEvent('change', true, true);
result.venue.dispatchEvent(evt);
};
Twinkle.xfd.callback.wrongVenueWarning = function twinklexfdWrongVenueWarning(venue) {
let text = '';
const namespace = mw.config.get('wgNamespaceNumber');
switch (venue) {
case 'afd':
if (namespace !== 0) {
text = 'AfD is generally appropriate only for articles.';
} else if (mw.config.get('wgIsRedirect')) {
text = 'Please use RfD for redirects.';
}
break;
case 'tfd':
if (namespace === 10 && /-stub$/.test(Morebits.pageNameNorm)) {
text = 'Use CfD for stub templates.';
} else if (Morebits.pageNameNorm.indexOf('Template:User ') === 0) {
text = 'Please use MfD for userboxes';
}
break;
case 'cfd':
if (![10, 14].includes(namespace)) {
text = 'CfD is only for categories and stub templates.';
}
break;
case 'cfds':
if (namespace !== 14) {
text = 'CfDS is only for categories.';
}
break;
case 'ffd':
if (namespace !== 6) {
text = 'FFD is selected but this page doesn\'t look like a file!';
}
break;
case 'rm':
if (namespace === 14) { // category
text = 'Please use CfD or CfDS for category renames.';
} else if ([118, 119, 2, 3].includes(namespace)) { // draft, draft talk, user, user talk
text = 'RMs are not permitted in draft and userspace, unless they are uncontroversial technical requests.';
}
break;
default: // mfd or rfd
break;
}
$('#wrong-venue-warn').text(text);
};
Twinkle.xfd.callback.change_category = function twinklexfdCallbackChangeCategory(e) {
const value = e.target.value;
const form = e.target.form;
const old_area = Morebits.QuickForm.getElements(e.target.form, 'work_area')[0];
let work_area = null;
const oldreasontextbox = form.getElementsByTagName('textarea')[0];
const oldreason = oldreasontextbox ? oldreasontextbox.value : '';
const appendReasonBox = function twinklexfdAppendReasonBox() {
work_area.append({
type: 'textarea',
name: 'reason',
label: 'Reason:',
value: oldreason,
tooltip: 'You can use wikimarkup in your reason. Twinkle will automatically sign your post.'
});
};
Twinkle.xfd.callback.wrongVenueWarning(value);
form.previewer.closePreview();
switch (value) {
case 'afd':
work_area = new Morebits.QuickForm.Element({
type: 'field',
label: 'Articles for deletion',
name: 'work_area'
});
work_area.append({
type: 'div',
label: '', // Added later by Twinkle.makeFindSourcesDiv()
id: 'twinkle-xfd-findsources',
style: 'margin-bottom: 5px; margin-top: -5px;'
});
work_area.append({
type: 'select',
name: 'outcome',
label: 'Desired outcome:',
event: Twinkle.xfd.callbacks.changeAfdOutcome,
list: [
{ label: 'Delete', value: 'deletion' },
{ label: 'Merge', value: 'merging' },
{ label: 'Redirect', value: 'redirecting' },
{ label: 'Draftify', value: 'draftification' }
]
});
work_area.append({
name: 'afdtarget',
type: 'input',
label: 'Target page:',
tooltip: 'Target page for the redirect or merge.',
event: Twinkle.xfd.callbacks.changeAfdTarget
});
work_area.append({
type: 'checkbox',
list: [
{
label: 'Wrap deletion tag with <noinclude>',
value: 'noinclude',
name: 'noinclude',
tooltip: 'Will wrap the deletion tag in <noinclude> tags, so that it won\'t transclude. This option is not normally required.'
}
]
});
work_area.append({
type: 'select',
name: 'xfdcat',
label: 'Choose what category this nomination belongs in:',
list: [
{ label: 'Unknown', value: '?', selected: true },
{ label: 'Media and music', value: 'M' },
{ label: 'Organisation, corporation, or product', value: 'O' },
{ label: 'Biographical', value: 'B' },
{ label: 'Society topics', value: 'S' },
{ label: 'Web or internet', value: 'W' },
{ label: 'Games or sports', value: 'G' },
{ label: 'Science and technology', value: 'T' },
{ label: 'Fiction and the arts', value: 'F' },
{ label: 'Places and transportation', value: 'P' },
{ label: 'Indiscernible or unclassifiable topic', value: 'I' },
{ label: 'Debate not yet sorted', value: 'U' }
]
});
work_area.append({
type: 'select',
multiple: true,
name: 'delsortCats',
label: 'Choose deletion sorting categories:',
tooltip: 'Select a few categories that are specifically relevant to the subject of the article. Be as precise as possible; categories like People and USA should only be used when no other categories apply.'
});
// grab deletion sort categories from en-wiki
Morebits.wiki.getCachedJson('Wikipedia:WikiProject_Deletion_sorting/Computer-readable.json').then((delsortCategories) => {
const $select = $('[name="delsortCats"]');
$.each(delsortCategories, (groupname, list) => {
const $optgroup = $('<optgroup>').attr('label', groupname);
const $delsortCat = $select.append($optgroup);
list.forEach((item) => {
const $option = $('<option>').val(item).text(item);
$delsortCat.append($option);
});
});
});
appendReasonBox();
work_area = work_area.render();
old_area.parentNode.replaceChild(work_area, old_area);
// Now that we've rendered the form, hide the target text box. Unhide it later for certain outcomes, using a callback.
$('[name="afdtarget"]').parent().hide();
Twinkle.makeFindSourcesDiv('#twinkle-xfd-findsources');
$(work_area).find('[name=delsortCats]')
.attr('data-placeholder', 'Select delsort pages')
.select2({
theme: 'default select2-morebits',
width: '100%',
matcher: Morebits.select2.matcher,
templateResult: Morebits.select2.highlightSearchMatches,
language: {
searching: Morebits.select2.queryInterceptor
},
// Link text to the page itself
templateSelection: function(choice) {
return $('<a>').text(choice.text).attr({
href: mw.util.getUrl('Wikipedia:WikiProject_Deletion_sorting/' + choice.text),
target: '_blank'
});
}
});
mw.util.addCSS(
// Remove black border
'.select2-container--default.select2-container--focus .select2-selection--multiple { border: 1px solid #aaa; }' +
// Reduce padding
'.select2-results .select2-results__option { padding-top: 1px; padding-bottom: 1px; }' +
'.select2-results .select2-results__group { padding-top: 1px; padding-bottom: 1px; } ' +
// Adjust font size
'.select2-container .select2-dropdown .select2-results { font-size: 13px; }' +
'.select2-container .selection .select2-selection__rendered { font-size: 13px; }' +
// Make the tiny cross larger
'.select2-selection__choice__remove { font-size: 130%; }'
);
break;
case 'tfd':
work_area = new Morebits.QuickForm.Element({
type: 'field',
label: 'Templates for discussion',
name: 'work_area'
});
var templateOrModule = mw.config.get('wgPageContentModel') === 'Scribunto' ? 'module' : 'template';
work_area.append({
type: 'select',
label: 'Choose type of action wanted:',
name: 'xfdcat',
event: function(e) {
const target = e.target;
let tfdtarget = target.form.tfdtarget;
// add/remove extra input box
if (target.value === 'tfm' && !tfdtarget) {
tfdtarget = new Morebits.QuickForm.Element({
name: 'tfdtarget',
type: 'input',
label: 'Other ' + templateOrModule + ' to be merged:',
tooltip: 'Required. Should not include the ' + Morebits.string.toUpperCaseFirstChar(templateOrModule) + ': namespace prefix.',
required: true
});
target.parentNode.appendChild(tfdtarget.render());
} else {
$(Morebits.QuickForm.getElementContainer(tfdtarget)).remove();
tfdtarget = null;
}
},
list: [
{ label: 'Deletion', value: 'tfd', selected: true },
{ label: 'Merge', value: 'tfm' }
]
});
work_area.append({
type: 'select',
name: 'templatetype',
label: 'Deletion tag display style:',
tooltip: 'Which <code>type=</code> parameter to pass to the TfD tag template.',
list: templateOrModule === 'module' ? [
{ value: 'module', label: 'Module', selected: true }
] : [
{ value: 'standard', label: 'Standard', selected: true },
{ value: 'sidebar', label: 'Sidebar/infobox', selected: $('.infobox').length },
{ value: 'inline', label: 'Inline template', selected: (() => {
// TODO: clean up after legacy parser is permanently replaced with Parsoid
const $oldParser = $('.mw-parser-output > p .Inline-Template');
const $newParser = $('.mw-parser-output section[data-mw-section-id="0"] p .Inline-Template');
const $scanTarget = $newParser.length ? $newParser : $oldParser;
return $scanTarget.length;
})() },
{ value: 'tiny', label: 'Tiny inline' },
{ value: 'disabled', label: 'Disabled' }
]
});
work_area.append({
type: 'checkbox',
list: [
{
label: 'Wrap deletion tag with <noinclude> (for substituted templates only)',
value: 'noinclude',
name: 'noinclude',
tooltip: 'Will wrap the deletion tag in <noinclude> tags, so that it won\'t get substituted along with the template.',
disabled: templateOrModule === 'module',
checked: !!$('.box-Subst_only').length // Default to checked if page carries {{subst only}}
}
]
});
work_area.append({
type: 'checkbox',
list: [
{
label: 'Notify talk pages of affected user scripts',
value: 'devpages',
name: 'devpages',
tooltip: 'A notification will be sent to Twinkle, AWB, and Ultraviolet\'s talk pages if those user scripts are marked as using this template.',
checked: true
}
]
});
appendReasonBox();
work_area = work_area.render();
old_area.parentNode.replaceChild(work_area, old_area);
break;
case 'mfd':
work_area = new Morebits.QuickForm.Element({
type: 'field',
label: 'Miscellany for deletion',
name: 'work_area'
});
if (mw.config.get('wgNamespaceNumber') !== 710) { // TimedText cannot be tagged, so asking whether to noinclude the tag is pointless
work_area.append({
type: 'checkbox',
list: [
{
label: 'Wrap deletion tag with <noinclude>',
value: 'noinclude',
name: 'noinclude',
tooltip: 'Will wrap the deletion tag in <noinclude> tags, so that it won\'t transclude. Select this option for userboxes.'
}
]
});
}
if ((mw.config.get('wgNamespaceNumber') === 2 /* User: */ || mw.config.get('wgNamespaceNumber') === 3 /* User talk: */) && mw.config.exists('wgRelevantUserName')) {
work_area.append({
type: 'checkbox',
list: [
{
label: 'Notify owner of userspace (if they are not the page creator)',
value: 'notifyuserspace',
name: 'notifyuserspace',
tooltip: 'If the user in whose userspace this page is located is not the page creator (for example, the page is a rescued article stored as a userspace draft), notify the userspace owner as well.',
checked: true
}
]
});
}
appendReasonBox();
work_area = work_area.render();
old_area.parentNode.replaceChild(work_area, old_area);
break;
case 'ffd':
work_area = new Morebits.QuickForm.Element({
type: 'field',
label: 'Discussion venues for files',
name: 'work_area'
});
appendReasonBox();
work_area = work_area.render();
old_area.parentNode.replaceChild(work_area, old_area);
break;
case 'cfd':
work_area = new Morebits.QuickForm.Element({
type: 'field',
label: 'Categories for discussion',
name: 'work_area'
});
var isCategory = mw.config.get('wgNamespaceNumber') === 14;
work_area.append({
type: 'select',
label: 'Choose type of action wanted:',
name: 'xfdcat',
event: function(e) {
const value = e.target.value,
cfdtarget = e.target.form.cfdtarget;
let cfdtarget2 = e.target.form.cfdtarget2;
// update enabled status
cfdtarget.disabled = value === 'cfd' || value === 'sfd-t';
if (isCategory) {
// update label
if (value === 'cfs') {
Morebits.QuickForm.setElementLabel(cfdtarget, 'Target categories: ');
} else if (value === 'cfc') {
Morebits.QuickForm.setElementLabel(cfdtarget, 'Target article: ');
} else {
Morebits.QuickForm.setElementLabel(cfdtarget, 'Target category: ');
}
// add/remove extra input box
if (value === 'cfs') {
if (cfdtarget2) {
cfdtarget2.disabled = false;
$(cfdtarget2).show();
} else {
cfdtarget2 = document.createElement('input');
cfdtarget2.setAttribute('name', 'cfdtarget2');
cfdtarget2.setAttribute('type', 'text');
cfdtarget2.setAttribute('required', 'true');
cfdtarget.parentNode.appendChild(cfdtarget2);
}
} else {
$(cfdtarget2).prop('disabled', true);
$(cfdtarget2).hide();
}
} else { // Update stub template label
Morebits.QuickForm.setElementLabel(cfdtarget, 'Target stub template: ');
}
},
list: isCategory ? [
{ label: 'Deletion', value: 'cfd', selected: true },
{ label: 'Merge', value: 'cfm' },
{ label: 'Renaming', value: 'cfr' },
{ label: 'Split', value: 'cfs' },
{ label: 'Convert into article', value: 'cfc' }
] : [
{ label: 'Stub Deletion', value: 'sfd-t', selected: true },
{ label: 'Stub Renaming', value: 'sfr-t' }
]
});
work_area.append({
type: 'input',
name: 'cfdtarget',
label: 'Target category:', // default, changed above
disabled: true,
required: true, // only when enabled
value: ''
});
appendReasonBox();
work_area = work_area.render();
old_area.parentNode.replaceChild(work_area, old_area);
break;
case 'cfds':
work_area = new Morebits.QuickForm.Element({
type: 'field',
label: 'Categories for speedy renaming',
name: 'work_area'
});
work_area.append({
type: 'select',
label: 'C2 sub-criterion:',
name: 'xfdcat',
tooltip: 'See WP:CFDS for full explanations.',
list: [
{ label: 'C2A: Typographic and spelling fixes', value: 'C2A', selected: true },
{ label: 'C2B: Naming conventions and disambiguation', value: 'C2B' },
{ label: 'C2C: Consistency with names of similar categories', value: 'C2C' },
{ label: 'C2D: Rename to match article name', value: 'C2D' },
{ label: 'C2E: Author request', value: 'C2E' },
{ label: 'C2F: One eponymous article', value: 'C2F' }
]
});
work_area.append({
type: 'input',
name: 'cfdstarget',
label: 'New name:',
size: 70,
value: '',
required: true
});
appendReasonBox();
work_area = work_area.render();
old_area.parentNode.replaceChild(work_area, old_area);
break;
case 'rfd':
work_area = new Morebits.QuickForm.Element({
type: 'field',
label: 'Redirects for discussion',
name: 'work_area'
});
work_area.append({
type: 'checkbox',
list: [
{
label: 'Notify target page if possible',
value: 'relatedpage',
name: 'relatedpage',
tooltip: "A notification template will be placed on the talk page of this redirect's target if this is true.",
checked: true
}
]
});
appendReasonBox();
work_area = work_area.render();
old_area.parentNode.replaceChild(work_area, old_area);
break;
case 'rm': {
work_area = new Morebits.QuickForm.Element({
type: 'field',
label: 'Requested moves',
name: 'work_area'
});
work_area.append({
type: 'checkbox',
list: [
{
label: 'Uncontroversial technical request',
value: 'rmtr',
name: 'rmtr',
tooltip: 'Use this option when you are unable to perform this uncontroversial move yourself because of a technical reason (e.g. a page already exists at the new title, or the page is protected)',
checked: false,
event: function() {
$('input[name="newname"]', form).prop('required', this.checked);
$('input[type="button"][value="more"]', form)[0].sublist.inputs[1].required = this.checked;
},
subgroup: {
type: 'checkbox',
list: [
{
label: 'Opt out of discussion if the request is contested',
value: 'rmtr-discuss',
name: 'rmtr-discuss',
tooltip: 'Use this option if you prefer to withdraw the request if contested, rather than discuss it. This suppresses the "discuss" link, which may be used to convert your request to a discussion on the talk page.',
checked: false
}
]
}
}
]
});
work_area.append({
type: 'dyninput',
inputs: [
{
label: 'From:',
name: 'currentname',
required: true
},
{
label: 'To:',
name: 'newname',
tooltip: 'Required for technical requests. Otherwise, if unsure of the appropriate title, you may leave it blank.'
}
],
min: 1
});
appendReasonBox();
work_area = work_area.render();
old_area.parentNode.replaceChild(work_area, old_area);
const currentNonTalkPage = mw.Title.newFromText(Morebits.pageNameNorm).getSubjectPage().toText();
form.currentname.value = currentNonTalkPage;
break;
}
default:
work_area = new Morebits.QuickForm.Element({
type: 'field',
label: 'Nothing for anything',
name: 'work_area'
});
work_area = work_area.render();
old_area.parentNode.replaceChild(work_area, old_area);
break;
}
// Return to checked state when switching, but no creator notification for CFDS or RM
form.notifycreator.disabled = value === 'cfds' || value === 'rm';
form.notifycreator.checked = !form.notifycreator.disabled;
};
Twinkle.xfd.callbacks = {
/** If the user hasn't modified the reason much, modify the reason to include the target article. If the user has modified the reason box with a custom reason, do nothing, since we don't want to blank their work. */
changeAfdTarget: function() {
const $afdTarget = $('[name="afdtarget"]');
const $reason = $('[name="reason"]');
const $outcome = $('[name="outcome"]');
if ($reason.val().endsWith('because ')) {
// Target has something typed in
if ($afdTarget.val()) {
if ($outcome.val() === 'redirecting') {
$reason.val(`I propose '''redirecting''' to [[${$afdTarget.val()}]] because `);
} else if ($outcome.val() === 'merging') {
$reason.val(`I propose '''merging''' to [[${$afdTarget.val()}]] because `);
}
// Target is blank
} else {
if ($outcome.val() === 'redirecting') {
$reason.val("I propose '''redirecting''' because ");
} else if ($outcome.val() === 'merging') {
$reason.val("I propose '''merging''' because ");
}
}
}
},
/** Print a default reason in the reason textarea, depending on which outcome is selected from the outcome dropdown list. */
changeAfdOutcome: function() {
const $outcome = $('[name="outcome"]');
const $reason = $('[name="reason"]');
const $afdTarget = $('[name="afdtarget"]');
$afdTarget.val('');
if ($outcome.val() === 'redirecting') {
$reason.val("I propose '''redirecting''' because ");
$afdTarget.parent().show();
} else if ($outcome.val() === 'merging') {
$reason.val("I propose '''merging''' because ");
$afdTarget.parent().show();
} else if ($outcome.val() === 'draftification') {
$reason.val("I propose '''draftifying''' because ");
$afdTarget.parent().hide();
} else if ($outcome.val() === 'deletion') {
$reason.val('');
$afdTarget.parent().hide();
}
},
/** Requires having the tag text (params.tagText) set ahead of time */
autoEditRequest: function(pageobj, params) {
const talkName = new mw.Title(pageobj.getPageName()).getTalkPage().toText();
if (talkName === pageobj.getPageName()) {
pageobj.getStatusElement().error('Page protected and nowhere to add an edit request, aborting');
} else {
pageobj.getStatusElement().warn('Page protected, requesting edit');
const editRequest = '{{subst:Xfd edit protected|page=' + pageobj.getPageName() +
'|discussion=' + params.discussionpage + (params.venue === 'rfd' ? '|rfd=yes' : '') +
'|tag=<nowiki>' + params.tagText + '\u003C/nowiki>}}'; // U+003C: <
const talk_page = new Morebits.wiki.Page(talkName, 'Automatically posting edit request on talk page');
talk_page.setNewSectionTitle('Edit request to complete ' + utils.toTLACase(params.venue) + ' nomination');
talk_page.setNewSectionText(editRequest);
talk_page.setCreateOption('recreate');
talk_page.setWatchlist(Twinkle.getPref('xfdWatchPage'));
talk_page.setFollowRedirect(true); // should never be needed, but if the article is moved, we would want to follow the redirect
talk_page.setChangeTags(Twinkle.changeTags);
talk_page.setCallbackParameters(params);
talk_page.newSection(null, () => {
talk_page.getStatusElement().warn('Unable to add edit request, the talk page may be protected');
});
}
},
getDiscussionWikitext: function(venue, params) {
if (venue === 'cfds') { // CfD/S takes a completely different style
return '* [[:' + Morebits.pageNameNorm + ']] to [[:' + params.cfdstarget + ']]\u00A0\u2013 ' +
params.xfdcat + (params.reason ? ': ' + Morebits.string.formatReasonText(params.reason) : '.') + ' ~~~~';
// U+00A0 NO-BREAK SPACE; U+2013 EN RULE
}
if (venue === 'rm') {
if (params.rmtr) {
const rmtrDiscuss = params['rmtr-discuss'] ? '|discuss=no' : '';
return params.currentname
.map((currentname, i) => `{{subst:RMassist|1=${currentname}|2=${params.newname[i]}${rmtrDiscuss}|reason=${params.reason}}}`)
.join('\n');
}
return `{{subst:Requested move${
params.currentname
.map((currentname, i) => `|current${i + 1}=${currentname}|new${i + 1}=${params.newname[i]}`)
.join('')
}|reason=${params.reason}}}`;
}
let text = '{{subst:' + venue + '2';
const reasonKey = venue === 'ffd' ? 'Reason' : 'text';
// Add a reason unconditionally, so that at least a signature is added
text += '|' + reasonKey + '=' + Morebits.string.formatReasonText(params.reason, true);
if (venue === 'afd' || venue === 'mfd') {
text += '|pg=' + Morebits.pageNameNorm;
if (venue === 'afd') {
text += '|cat=' + params.xfdcat;
}
} else if (venue === 'rfd') {
text += '|redirect=' + Morebits.pageNameNorm;
} else {
text += '|1=' + mw.config.get('wgTitle');
if (mw.config.get('wgPageContentModel') === 'Scribunto') {
text += '|module=Module:';
}
}
if (params.rfdtarget) {
text += '|target=' + params.rfdtarget + (params.section ? '#' + params.section : '');
} else if (params.tfdtarget) {
text += '|2=' + params.tfdtarget;
} else if (params.cfdtarget) {
text += '|2=' + params.cfdtarget;
if (params.cfdtarget2) {
text += '|3=' + params.cfdtarget2;
}
} else if (params.uploader) {
text += '|Uploader=' + params.uploader;
}
text += '}}';
if (venue === 'rfd' || venue === 'tfd' || venue === 'cfd') {
text += '\n';
}
// Don't delsort if delsortCats is undefined (TFD, FFD, etc.)
// Don't delsort if delsortCats is an empty array (AFD where user chose no categories)
if (Array.isArray(params.delsortCats) && params.delsortCats.length) {
text += '\n{{subst:Deletion sorting/multi|' + params.delsortCats.join('|') + '|sig=~~~~}}';
}
return text;
},
showPreview: function(form, venue, params) {
const templatetext = Twinkle.xfd.callbacks.getDiscussionWikitext(venue, params);
if (venue === 'rm') { // RM templates are sensitive to page title
form.previewer.beginRender(templatetext, params.rmtr ? 'Wikipedia:Requested moves/Technical requests' : new mw.Title(Morebits.pageNameNorm).getTalkPage().toText());
} else {
form.previewer.beginRender(templatetext, 'WP:TW'); // Force wikitext
}
},
preview: function(form) {
// venue, reason, xfdcat, tfdtarget, cfdtarget, cfdtarget2, cfdstarget, delsortCats, newname
const params = Morebits.QuickForm.getInputData(form);
const venue = params.venue;
// Remove CfD or TfD namespace prefixes if given
if (params.tfdtarget) {
params.tfdtarget = utils.stripNs(params.tfdtarget);
} else if (params.cfdtarget) {
params.cfdtarget = utils.stripNs(params.cfdtarget);
if (params.cfdtarget2) {
params.cfdtarget2 = utils.stripNs(params.cfdtarget2);
}
} else if (params.cfdstarget) { // Add namespace if not given (CFDS)
params.cfdstarget = utils.addNs(params.cfdstarget, 14);
}
if (venue === 'ffd') {
// Fetch the uploader
const page = new Morebits.wiki.Page(mw.config.get('wgPageName'));
page.lookupCreation(() => {
params.uploader = page.getCreator();
Twinkle.xfd.callbacks.showPreview(form, venue, params);
});
} else if (venue === 'rfd') { // Find the target
Twinkle.xfd.callbacks.rfd.findTarget(params, (params) => {
Twinkle.xfd.callbacks.showPreview(form, venue, params);
});
} else if (venue === 'cfd') { // Swap in CfD subactions
Twinkle.xfd.callbacks.showPreview(form, params.xfdcat, params);
} else {
Twinkle.xfd.callbacks.showPreview(form, venue, params);
}
},
/**
* Unified handler for sending {{Xfd notice}} notifications
* Also handles userspace logging
*
* @param {Object} params
* @param {string} notifyTarget The user or page being notified
* @param {boolean} [noLog=false] Whether to skip logging to userspace
* XfD log, especially useful in cases in where multiple notifications
* may be sent out (MfD, TfM, RfD)
* @param {string} [actionName] Alternative description of the action
* being undertaken. Required if not notifying a user talk page.
*/
notifyUser: function(params, notifyTarget, noLog, actionName) {
// Ensure items with User talk or no namespace prefix both end
// up at user talkspace as expected, but retain the
// prefix-less username for addToLog
const userTalkNamespace = 3;
notifyTarget = mw.Title.newFromText(notifyTarget, userTalkNamespace);
const targetNS = notifyTarget.getNamespaceId();
const usernameOrTarget = notifyTarget.getRelativeText(userTalkNamespace);
notifyTarget = notifyTarget.toText();
if (targetNS === userTalkNamespace) {
// Disallow warning yourself
if (usernameOrTarget === mw.config.get('wgUserName')) {
Morebits.Status.warn('You (' + usernameOrTarget + ') created this page; skipping user notification');
// if we thought we would notify someone but didn't,
// then jump to logging.
Twinkle.xfd.callbacks.addToLog(params, null);
return;
}
// Default is notifying the initial contributor, but MfD also
// notifies userspace page owner
actionName = actionName || 'Notifying initial contributor (' + usernameOrTarget + ')';
}
const notifyText = Twinkle.xfd.callbacks.generateUserTalkNoticeWikitext(params);
// Link to the venue; object used here rather than repetitive items in switch
const venueNames = {
afd: 'Articles for deletion',
tfd: 'Templates for discussion',
mfd: 'Miscellany for deletion',
cfd: 'Categories for discussion',
ffd: 'Files for discussion',
rfd: 'Redirects for discussion'
};
const editSummary = 'Notification: [[' + params.discussionpage + '|listing]] of [[:' +
Morebits.pageNameNorm + ']] at [[WP:' + venueNames[params.venue] + ']].';
const usertalkpage = new Morebits.wiki.Page(notifyTarget, actionName);
usertalkpage.setAppendText(notifyText);
usertalkpage.setEditSummary(editSummary);
usertalkpage.setChangeTags(Twinkle.changeTags);
usertalkpage.setCreateOption('recreate');
// Different pref for RfD target notifications
if (params.venue === 'rfd' && targetNS !== 3) {
usertalkpage.setWatchlist(Twinkle.getPref('xfdWatchRelated'));
} else {
usertalkpage.setWatchlist(Twinkle.getPref('xfdWatchUser'));
}
usertalkpage.setFollowRedirect(true, false);
if (noLog) {
usertalkpage.append();
} else {
usertalkpage.append(() => {
// Don't treat RfD target or MfD userspace owner as initialContrib in log
if (!params.notifycreator) {
notifyTarget = null;
}
// add this nomination to the user's userspace log
Twinkle.xfd.callbacks.addToLog(params, usernameOrTarget);
}, () => {
// if user could not be notified, log nomination without mentioning that notification was sent
Twinkle.xfd.callbacks.addToLog(params, null);
});
}
},
generateUserTalkNoticeWikitext: function(params) {
// For grep: Afd notice, Mfd notice, Tfd notice, Cfd notice, Ffd notice, Rfd notice
let notifytext = '\n{{subst:' + params.venue + ' notice';
const templateNamespace = 10;
// Venue-specific parameters
switch (params.venue) {
case 'afd':
notifytext += params.outcome !== 'deletion' ? '|outcome=' + params.outcome : '';
notifytext += params.afdtarget ? '|target=' + params.afdtarget : '';
// Tell the template to add " (Xnd nomination)" to the XFD title, if needed.
// The   (HTML space character) is needed to overcome MediaWiki's parameter auto-trim.
notifytext += params.numbering !== '' ? '|order= ' + params.numbering : '';
break;
case 'mfd':
// tell the template to add " (Xnd nomination)" to the XFD title, if needed
notifytext += params.numbering !== '' ? '|order= ' + params.numbering : '';
break;
case 'tfd':
if (params.xfdcat === 'tfm') {
notifytext = '\n{{subst:Tfm notice|2=' + params.tfdtarget;
}
break;
case 'cfd':
notifytext += '|action=' + params.action + (mw.config.get('wgNamespaceNumber') === templateNamespace ? '|stub=yes' : '');
break;
default: // ffd, rfd
break;
}
notifytext += '|1=' + Morebits.pageNameNorm + '}} ~~~~';
return notifytext;
},
addToLog: function(params, initialContrib) {
if (!Twinkle.getPref('logXfdNominations') || Twinkle.getPref('noLogOnXfdNomination').includes(params.venue)) {
return;
}
const usl = new Morebits.UserspaceLogger(Twinkle.getPref('xfdLogPageName'));// , 'Adding entry to userspace log');
usl.initialText =
"This is a log of all [[WP:XFD|deletion discussion]] nominations made by this user using [[WP:TW|Twinkle]]'s XfD module.\n\n" +
'If you no longer wish to keep this log, you can turn it off using the [[Wikipedia:Twinkle/Preferences|preferences panel]], and ' +
'nominate this page for speedy deletion under [[WP:CSD#U1|CSD U1]].' +
(Morebits.userIsSysop ? '\n\nThis log does not track XfD-related deletions made using Twinkle.' : '');
let editsummary;
if (params.discussionpage) {
editsummary = 'Logging [[' + params.discussionpage + '|' + utils.toTLACase(params.venue) + ' nomination]] of [[:' + Morebits.pageNameNorm + ']].';
} else {
editsummary = 'Logging ' + utils.toTLACase(params.venue) + ' nomination of [[:' + Morebits.pageNameNorm + ']].';
}
// If a logged file is deleted but exists on commons, the wikilink will be blue, so provide a link to the log
const fileLogLink = mw.config.get('wgNamespaceNumber') === 6 ? ' ([{{fullurl:Special:Log|page=' + mw.util.wikiUrlencode(mw.config.get('wgPageName')) + '}} log])' : '';
// CFD/S and RM don't have canonical links
const nominatedLink = params.discussionpage ? '[[' + params.discussionpage + '|nominated]]' : 'nominated';
let appendText = '# [[:' + Morebits.pageNameNorm + ']]:' + fileLogLink + ' ' + nominatedLink + ' at [[WP:' + params.venue.toUpperCase() + '|' + utils.toTLACase(params.venue) + ']]';
switch (params.venue) {
case 'tfd':
if (params.xfdcat === 'tfm') {
appendText += ' (merge)';
if (params.tfdtarget) {
const contentModel = mw.config.get('wgPageContentModel') === 'Scribunto' ? 'Module:' : 'Template:';
appendText += '; Other ' + contentModel.toLowerCase() + ' [[';
if (!new RegExp('^:?' + Morebits.namespaceRegex([10, 828]) + ':', 'i').test(params.tfdtarget)) {
appendText += contentModel;
}
appendText += params.tfdtarget + ']]';
}
}
break;
case 'mfd':
if (params.notifyuserspace && params.userspaceOwner && params.userspaceOwner !== initialContrib) {
appendText += '; notified {{user|1=' + params.userspaceOwner + '}}';
}
break;
case 'cfd':
appendText += ' (' + utils.toTLACase(params.xfdcat) + ')';
if (params.cfdtarget) {
const categoryOrTemplate = params.xfdcat.charAt(0) === 's' ? 'Template:' : ':Category:';
appendText += '; ' + params.action + ' to [[' + categoryOrTemplate + params.cfdtarget + ']]';
if (params.xfdcat === 'cfs' && params.cfdtarget2) {
appendText += ', [[' + categoryOrTemplate + params.cfdtarget2 + ']]';
}
}
break;
case 'cfds':
appendText += ' (' + utils.toTLACase(params.xfdcat) + ')';
// Ensure there's more than just 'Category:'
if (params.cfdstarget && params.cfdstarget.length > 9) {
appendText += '; New name: [[:' + params.cfdstarget + ']]';
}
break;
case 'rfd':
if (params.rfdtarget) {
appendText += '; Target: [[:' + params.rfdtarget + ']]';
if (params.relatedpage) {
appendText += ' (notified)';
}
}
break;
case 'rm':
appendText = params.currentname
.map((currentname, i) => `# [[:${currentname}]]: ${nominatedLink} at [[WP:RM${params.rmtr ? '/TR' : ''}|]]${params.newname[i] ? `; New name: [[:${params.newname[i]}]]` : ''}`)
.join('\n');
break;
default: // afd or ffd
break;
}
if (initialContrib && params.notifycreator) {
appendText += '; notified {{user|1=' + initialContrib + '}}';
}
appendText += ' ~~~~~';
if (params.reason) {
appendText += "\n#* '''Reason''': " + Morebits.string.formatReasonForLog(params.reason);
}
usl.changeTags = Twinkle.changeTags;
usl.log(appendText, editsummary);
},
afd: {
main: function(apiobj) {
const response = apiobj.getResponse();
const titles = response.query.allpages;
// There has been no earlier entries with this prefix, just go on.
if (titles.length <= 0) {
apiobj.params.numbering = apiobj.params.number = '';
} else {
let number = 0;
for (let i = 0; i < titles.length; ++i) {
const title = titles[i].title;
// First, simple test, is there an instance with this exact name?
if (title === 'Wikipedia:Articles for deletion/' + Morebits.pageNameNorm) {
number = Math.max(number, 1);
continue;
}
const order_re = new RegExp('^' +
Morebits.string.escapeRegExp('Wikipedia:Articles for deletion/' + Morebits.pageNameNorm) +
'\\s*\\(\\s*(\\d+)(?:(?:th|nd|rd|st) nom(?:ination)?)?\\s*\\)\\s*$');
const match = order_re.exec(title);
// No match; A non-good value
// Or the match is an unrealistically high number. Avoid false positives such as Wikipedia:Articles for deletion/The Basement (2014), by ignoring matches greater than 100
if (!match || match[1] > 100) {
continue;
}
// A match, set number to the max of current
number = Math.max(number, Number(match[1]));
}
apiobj.params.number = utils.num2order(parseInt(number, 10) + 1);
apiobj.params.numbering = number > 0 ? ' (' + apiobj.params.number + ' nomination)' : '';
}
apiobj.params.discussionpage = 'Wikipedia:Articles for deletion/' + Morebits.pageNameNorm + apiobj.params.numbering;
Morebits.Status.info('Next discussion page', '[[' + apiobj.params.discussionpage + ']]');
// Updating data for the action completed event
Morebits.wiki.actionCompleted.redirect = apiobj.params.discussionpage;
Morebits.wiki.actionCompleted.notice = 'Nomination completed, now redirecting to the discussion page';
// Tagging article
const wikipedia_page = new Morebits.wiki.Page(mw.config.get('wgPageName'), 'Adding deletion tag to article');
wikipedia_page.setFollowRedirect(true); // should never be needed, but if the article is moved, we would want to follow the redirect
wikipedia_page.setChangeTags(Twinkle.changeTags); // Here to apply to triage
wikipedia_page.setCallbackParameters(apiobj.params);
wikipedia_page.load(Twinkle.xfd.callbacks.afd.taggingArticle);
},
// Tagging needs to happen before everything else: this means we can check if there is an AfD tag already on the page
taggingArticle: function(pageobj) {
let text = pageobj.getPageText();
const params = pageobj.getCallbackParameters();
const statelem = pageobj.getStatusElement();
if (!pageobj.exists()) {
statelem.error("It seems that the page doesn't exist; perhaps it has already been deleted");
return;
}
// Check for existing AfD tag, for the benefit of new page patrollers
const textNoAfd = text.replace(/<!--.*AfD.*\n\{\{(?:Article for deletion\/dated|AfDM).*\}\}\n<!--.*(?:\n<!--.*)?AfD.*(?:\s*\n)?/g, '');
if (text !== textNoAfd) {
if (confirm('An AfD tag was found on this article. Maybe someone beat you to it. \nClick OK to replace the current AfD tag (not recommended), or Cancel to abandon your nomination.')) {
text = textNoAfd;
} else {
statelem.error('Article already tagged with AfD tag, and you chose to abort');
window.location.reload();
return;
}
}
// Now we know we want to go ahead with it, trigger the other AJAX requests
// Mark the page as curated/patrolled, if wanted
if (Twinkle.getPref('markXfdPagesAsPatrolled')) {
pageobj.triage();
}
// Start discussion page, will also handle pagetriage and delsort listings
let wikipedia_page = new Morebits.wiki.Page(params.discussionpage, 'Creating article deletion discussion page');
wikipedia_page.setCallbackParameters(params);
wikipedia_page.load(Twinkle.xfd.callbacks.afd.discussionPage);
// Today's list
const date = new Morebits.Date(pageobj.getLoadTime());
wikipedia_page = new Morebits.wiki.Page('Wikipedia:Articles for deletion/Log/' +
date.format('YYYY MMMM D', 'utc'), "Adding discussion to today's list");
wikipedia_page.setFollowRedirect(true);
wikipedia_page.setCallbackParameters(params);
wikipedia_page.load(Twinkle.xfd.callbacks.afd.todaysList);
// Notification to first contributor
if (params.notifycreator) {
const thispage = new Morebits.wiki.Page(mw.config.get('wgPageName'));
thispage.setCallbackParameters(params);
thispage.setLookupNonRedirectCreator(true); // Look for author of first non-redirect revision
thispage.lookupCreation((pageobj) => {
Twinkle.xfd.callbacks.notifyUser(pageobj.getCallbackParameters(), pageobj.getCreator());
});
// or, if not notifying, add this nomination to the user's userspace log without the initial contributor's name
} else {
Twinkle.xfd.callbacks.addToLog(params, null);
}
// Add AFD tag to article
params.tagText = Twinkle.xfd.callbacks.afd.generateArticleTagWikitext(
params.noinclude, params.outcome, params.afdtarget, params.number
);
// If the selected outcome is merge, add {{Merge from}} to the target page
if (params.outcome === 'merging' && params.afdtarget) {
wikipedia_page = new Morebits.wiki.Page(params.afdtarget, 'Tagging the target page with {{Merge from}}');
wikipedia_page.setCallbackParameters(params);
wikipedia_page.setFollowRedirect(true);
wikipedia_page.load(Twinkle.xfd.callbacks.afd.tagTargetPageWithMergeFromTag);
}
if (pageobj.canEdit()) {
// Remove some tags that should always be removed on AfD.
text = text.replace(/\{\{\s*(dated prod|dated prod blp|Prod blp\/dated|Proposed deletion\/dated|prod2|Proposed deletion endorsed|Userspace draft)\s*(\|(?:\{\{[^{}]*\}\}|[^{}])*)?\}\}\s*/ig, '');
// Then, test if there are speedy deletion-related templates on the article.
const textNoSd = text.replace(/\{\{\s*(db(-\w*)?|delete|(?:hang|hold)[- ]?on)\s*(\|(?:\{\{[^{}]*\}\}|[^{}])*)?\}\}\s*/ig, '');
if (text !== textNoSd && confirm('A speedy deletion tag was found on this page. Should it be removed?')) {
text = textNoSd;
}
// Insert tag after short description or any hatnotes
const wikipage = new Morebits.wikitext.Page(text);
text = wikipage.insertAfterTemplates(params.tagText, Twinkle.hatnoteRegex).getText();
pageobj.setPageText(text);
pageobj.setEditSummary(`Nominated for ${params.outcome}; see [[:${params.discussionpage}]].`);
pageobj.setWatchlist(Twinkle.getPref('xfdWatchPage'));
pageobj.setCreateOption('nocreate');
pageobj.save();
} else {
Twinkle.xfd.callbacks.autoEditRequest(pageobj, params);
}
},
tagTargetPageWithMergeFromTag: function(pageobj) {
const statelem = pageobj.getStatusElement();
if (!pageobj.exists()) {
statelem.warn('Failed. Target page not found.');
return;
} else if (!pageobj.canEdit()) {
statelem.warn('Failed. Target page is protected from editing.');
return;
}
const params = pageobj.getCallbackParameters();
const tag = `{{Merge from |1=${Morebits.pageNameNorm} |target=${params.afdtarget} |afd=${Morebits.pageNameNorm + params.numbering} |date ={{subst:CURRENTMONTHNAME}} {{subst:CURRENTYEAR}} }}`;
const wikipage = new Morebits.wikitext.Page(pageobj.getPageText());
const text = wikipage.insertAfterTemplates(tag, Twinkle.hatnoteRegex).getText();
pageobj.setPageText(text);
pageobj.setEditSummary('Nominated for merging; see [[:' + params.discussionpage + ']].');
pageobj.setCreateOption('nocreate');
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.save();
},
generateArticleTagWikitext: function(noinclude, outcome, afdtarget, number) {
let noIncludeStart = '';
let noIncludeEnd = '';
if (noinclude) {
noIncludeStart = '<noinclude>';
noIncludeEnd = '</noinclude>';
}
let templateAndParams = '';
const outcomeParam = outcome !== 'deletion' ? '|outcome=' + outcome : '';
const targetParam = afdtarget ? '|target=' + afdtarget : '';
const isFirstNomination = number === '';
if (isFirstNomination) {
templateAndParams = 'subst:afd|help=off' + outcomeParam + targetParam;
} else {
templateAndParams = 'subst:afdx|' + number + '|help=off' + outcomeParam + targetParam;
}
return noIncludeStart + '{{' + templateAndParams + '}}' + noIncludeEnd + '\n';
},
discussionPage: function(pageobj) {
const params = pageobj.getCallbackParameters();
pageobj.setPageText(Twinkle.xfd.callbacks.getDiscussionWikitext('afd', params));
pageobj.setEditSummary('Creating AfD discussion page for [[:' + Morebits.pageNameNorm + ']].');
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.setWatchlist(Twinkle.getPref('xfdWatchDiscussion'));
pageobj.setCreateOption('createonly');
pageobj.save(() => {
Twinkle.xfd.currentRationale = null; // any errors from now on do not need to print the rationale, as it is safely saved on-wiki
// Actions that should wait on the discussion page actually being created
// and whose errors shouldn't output the user rationale
// List at deletion sorting pages
if (params.delsortCats) {
params.delsortCats.forEach((cat) => {
const delsortPage = new Morebits.wiki.Page('Wikipedia:WikiProject Deletion sorting/' + cat, 'Adding to list of ' + cat + '-related deletion discussions');
delsortPage.setFollowRedirect(true); // In case a category gets renamed
delsortPage.setCallbackParameters({discussionPage: params.discussionpage});
delsortPage.load(Twinkle.xfd.callbacks.afd.delsortListing);
});
}
});
},
todaysList: function(pageobj) {
const params = pageobj.getCallbackParameters();
const statelem = pageobj.getStatusElement();
const added_data = '{{subst:afd3|pg=' + Morebits.pageNameNorm + params.numbering + '}}\n';
let text;
// add date header if the log is found to be empty (a bot should do this automatically)
if (!pageobj.exists()) {
text = '{{subst:AfD log}}\n' + added_data;
} else {
const old_text = pageobj.getPageText() + '\n'; // MW strips trailing blanks, but we like them, so we add a fake one
text = old_text.replace(/(<!-- Add new entries to the TOP of the following list -->\n+)/, '$1' + added_data);
if (text === old_text) {
const linknode = document.createElement('a');
linknode.setAttribute('href', mw.util.getUrl('Wikipedia:Twinkle/Fixing AFD') + '?action=purge');
linknode.appendChild(document.createTextNode('How to fix AFD'));
statelem.error(['Could not find the target spot for the discussion. To fix this problem, please see ', linknode, '.']);
return;
}
}
pageobj.setPageText(text);
pageobj.setEditSummary('Adding [[:' + params.discussionpage + ']].');
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.setWatchlist(Twinkle.getPref('xfdWatchList'));
pageobj.setCreateOption('recreate');
pageobj.setDiscussionToolsAutoSubscribe(false);
pageobj.save();
},
delsortListing: function(pageobj) {
const discussionPage = pageobj.getCallbackParameters().discussionPage;
const text = pageobj.getPageText().replace('directly below this line -->', 'directly below this line -->\n{{' + discussionPage + '}}');
pageobj.setPageText(text);
pageobj.setEditSummary('Listing [[:' + discussionPage + ']].');
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.setCreateOption('nocreate');
pageobj.setDiscussionToolsAutoSubscribe(false);
pageobj.save();
}
},
tfd: {
main: function(pageobj) {
const params = pageobj.getCallbackParameters();
const date = new Morebits.Date(pageobj.getLoadTime());
params.logpage = 'Wikipedia:Templates for discussion/Log/' + date.format('YYYY MMMM D', 'utc');
params.discussionpage = params.logpage + '#' + Morebits.pageNameNorm;
// Add log/discussion page params to the already-loaded page object
pageobj.setCallbackParameters(params);
// Defined here rather than below to reduce duplication
let watchModule, watch_query;
if (params.scribunto) {
const watchPref = Twinkle.getPref('xfdWatchPage');
// action=watch has no way to rely on user
// preferences (T262912), so we do it manually.
// The watchdefault pref appears to reliably return '1' (string),
// but that's not consistent among prefs so might as well be "correct"
watchModule = watchPref !== 'no' && (watchPref !== 'default' || !!parseInt(mw.user.options.get('watchdefault'), 10));
if (watchModule) {
watch_query = {
action: 'watch',
titles: [mw.config.get('wgPageName')],
token: mw.user.tokens.get('watchToken')
};
// Only add the expiry if page is unwatched or already temporarily watched
if (pageobj.getWatched() !== true && watchPref !== 'default' && watchPref !== 'yes') {
watch_query.expiry = watchPref;
}
}
}
// Tagging template(s)/module(s)
if (params.xfdcat === 'tfm') { // Merge
let wikipedia_otherpage;
if (params.scribunto) {
wikipedia_otherpage = new Morebits.wiki.Page(params.otherTemplateName + '/doc', 'Tagging other module documentation with merge tag');
// Watch tagged module pages as well
if (watchModule) {
watch_query.titles.push(params.otherTemplateName);
new Morebits.wiki.Api('Adding Modules to watchlist', watch_query).post();
}
} else {
wikipedia_otherpage = new Morebits.wiki.Page(params.otherTemplateName, 'Tagging other template with merge tag');
}
// Tag this template/module
Twinkle.xfd.callbacks.tfd.taggingTemplateForMerge(pageobj);
// Tag other template/module
wikipedia_otherpage.setFollowRedirect(true);
const otherParams = $.extend({}, params);
otherParams.otherTemplateName = Morebits.pageNameNorm;
wikipedia_otherpage.setCallbackParameters(otherParams);
wikipedia_otherpage.load(Twinkle.xfd.callbacks.tfd.taggingTemplateForMerge);
} else { // delete
if (params.scribunto && Twinkle.getPref('xfdWatchPage') !== 'no') {
// Watch tagged module page as well
if (watchModule) {
new Morebits.wiki.Api('Adding Module to watchlist', watch_query).post();
}
}
Twinkle.xfd.callbacks.tfd.taggingTemplate(pageobj);
}
// Updating data for the action completed event
Morebits.wiki.actionCompleted.redirect = params.logpage;
Morebits.wiki.actionCompleted.notice = "Nomination completed, now redirecting to today's log";
// Adding discussion
const wikipedia_page = new Morebits.wiki.Page(params.logpage, "Adding discussion to today's log");
wikipedia_page.setFollowRedirect(true);
wikipedia_page.setCallbackParameters(params);
wikipedia_page.load(Twinkle.xfd.callbacks.tfd.todaysList);
// Notification to first contributors
if (params.notifycreator) {
const involvedpages = [];
const seenusers = [];
involvedpages.push(new Morebits.wiki.Page(mw.config.get('wgPageName')));
if (params.xfdcat === 'tfm') {
if (params.scribunto) {
involvedpages.push(new Morebits.wiki.Page('Module:' + params.tfdtarget));
} else {
involvedpages.push(new Morebits.wiki.Page('Template:' + params.tfdtarget));
}
}
involvedpages.forEach((page) => {
page.setCallbackParameters(params);
page.lookupCreation((innerpage) => {
const username = innerpage.getCreator();
if (!seenusers.includes(username)) {
seenusers.push(username);
// Only log once on merge nominations, for the initial template
Twinkle.xfd.callbacks.notifyUser(innerpage.getCallbackParameters(), username,
params.xfdcat === 'tfm' && innerpage.getPageName() !== Morebits.pageNameNorm);
}
});
});
// or, if not notifying, add this nomination to the user's userspace log without the initial contributor's name
} else {
Twinkle.xfd.callbacks.addToLog(params, null);
}
// Notify developer(s) of script(s) that use(s) the nominated template
if (params.devpages) {
const inCategories = mw.config.get('wgCategories');
const categoryNotificationPageMap = {
'Templates used by Twinkle': 'Wikipedia talk:Twinkle',
'Templates used by AutoWikiBrowser': 'Wikipedia talk:AutoWikiBrowser',
'Templates used by Ultraviolet': 'Wikipedia talk:Ultraviolet'
};
$.each(categoryNotificationPageMap, (category, page) => {
if (inCategories.includes(category)) {
Twinkle.xfd.callbacks.notifyUser(params, page, true, 'Notifying ' + page + ' of template nomination');
}
});
}
},
taggingTemplate: function(pageobj) {
const text = pageobj.getPageText();
const params = pageobj.getCallbackParameters();
params.tagText = '{{subst:template for discussion|help=off' + (params.templatetype !== 'standard' ? '|type=' + params.templatetype : '') + '}}';
if (pageobj.getContentModel() === 'sanitized-css') {
params.tagText = '/* ' + params.tagText + ' */';
} else {
if (params.noinclude) {
params.tagText = '<noinclude>' + params.tagText + '</noinclude>';
}
params.tagText += params.templatetype === 'standard' || params.templatetype === 'sidebar' || params.templatetype === 'disabled' ? '\n' : ''; // No newline for inline
}
if (pageobj.canEdit() && ['wikitext', 'sanitized-css'].includes(pageobj.getContentModel())) {
pageobj.setPageText(params.tagText + text);
pageobj.setEditSummary('Nominated for deletion; see [[:' + params.discussionpage + ']].');
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.setWatchlist(Twinkle.getPref('xfdWatchPage'));
if (params.scribunto) {
pageobj.setCreateOption('recreate'); // Module /doc might not exist
}
pageobj.save();
} else {
Twinkle.xfd.callbacks.autoEditRequest(pageobj, params);
}
},
taggingTemplateForMerge: function(pageobj) {
const text = pageobj.getPageText();
const params = pageobj.getCallbackParameters();
params.tagText = '{{subst:tfm|help=off|' + (params.templatetype !== 'standard' ? 'type=' + params.templatetype + '|' : '') +
'1=' + params.otherTemplateName.replace(new RegExp('^' + Morebits.namespaceRegex([10, 828]) + ':'), '') + '}}';
if (pageobj.getContentModel() === 'sanitized-css') {
params.tagText = '/* ' + params.tagText + ' */';
} else {
if (params.noinclude) {
params.tagText = '<noinclude>' + params.tagText + '</noinclude>';
}
params.tagText += params.templatetype === 'standard' || params.templatetype === 'sidebar' || params.templatetype === 'disabled' ? '\n' : ''; // No newline for inline
}
if (pageobj.canEdit() && ['wikitext', 'sanitized-css'].includes(pageobj.getContentModel())) {
pageobj.setPageText(params.tagText + text);
pageobj.setEditSummary('Listed for merging with [[:' + params.otherTemplateName + ']]; see [[:' + params.discussionpage + ']].');
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.setWatchlist(Twinkle.getPref('xfdWatchPage'));
if (params.scribunto) {
pageobj.setCreateOption('recreate'); // Module /doc might not exist
}
pageobj.save();
} else {
Twinkle.xfd.callbacks.autoEditRequest(pageobj, params);
}
},
todaysList: function(pageobj) {
const params = pageobj.getCallbackParameters();
const statelem = pageobj.getStatusElement();
const added_data = Twinkle.xfd.callbacks.getDiscussionWikitext(params.xfdcat, params);
let text;
// add date header if the log is found to be empty (a bot should do this automatically)
if (!pageobj.exists()) {
text = '{{subst:TfD log}}\n' + added_data;
} else {
const old_text = pageobj.getPageText();
text = old_text.replace('-->', '-->\n' + added_data);
if (text === old_text) {
statelem.error('failed to find target spot for the discussion');
return;
}
}
pageobj.setPageText(text);
pageobj.setEditSummary('/* ' + Morebits.pageNameNorm + ' */ Adding ' + (params.xfdcat === 'tfd' ? 'deletion nomination' : 'merge listing') + ' of [[:' + Morebits.pageNameNorm + ']].');
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.setWatchlist(Twinkle.getPref('xfdWatchDiscussion'));
pageobj.setCreateOption('recreate');
pageobj.save(() => {
Twinkle.xfd.currentRationale = null; // any errors from now on do not need to print the rationale, as it is safely saved on-wiki
});
}
},
mfd: {
main: function(apiobj) {
const response = apiobj.getResponse();
const titles = response.query.allpages;
// There has been no earlier entries with this prefix, just go on.
if (titles.length <= 0) {
apiobj.params.numbering = apiobj.params.number = '';
} else {
let number = 0;
for (let i = 0; i < titles.length; ++i) {
const title = titles[i].title;
// First, simple test, is there an instance with this exact name?
if (title === 'Wikipedia:Miscellany for deletion/' + Morebits.pageNameNorm) {
number = Math.max(number, 1);
continue;
}
const order_re = new RegExp('^' +
Morebits.string.escapeRegExp('Wikipedia:Miscellany for deletion/' + Morebits.pageNameNorm) +
'\\s*\\(\\s*(\\d+)(?:(?:th|nd|rd|st) nom(?:ination)?)?\\s*\\)\\s*$');
const match = order_re.exec(title);
// No match; A non-good value
if (!match) {
continue;
}
// A match, set number to the max of current
number = Math.max(number, Number(match[1]));
}
apiobj.params.number = utils.num2order(parseInt(number, 10) + 1);
apiobj.params.numbering = number > 0 ? ' (' + apiobj.params.number + ' nomination)' : '';
}
apiobj.params.discussionpage = 'Wikipedia:Miscellany for deletion/' + Morebits.pageNameNorm + apiobj.params.numbering;
apiobj.statelem.info('next in order is [[' + apiobj.params.discussionpage + ']]');
let wikipedia_page;
// Tagging page
if (mw.config.get('wgNamespaceNumber') !== 710) { // cannot tag TimedText pages
wikipedia_page = new Morebits.wiki.Page(mw.config.get('wgPageName'), 'Tagging page with deletion tag');
wikipedia_page.setFollowRedirect(true); // should never be needed, but if the page is moved, we would want to follow the redirect
wikipedia_page.setCallbackParameters(apiobj.params);
wikipedia_page.load(Twinkle.xfd.callbacks.mfd.taggingPage);
}
// Updating data for the action completed event
Morebits.wiki.actionCompleted.redirect = apiobj.params.discussionpage;
Morebits.wiki.actionCompleted.notice = 'Nomination completed, now redirecting to the discussion page';
// Discussion page
wikipedia_page = new Morebits.wiki.Page(apiobj.params.discussionpage, 'Creating deletion discussion page');
wikipedia_page.setCallbackParameters(apiobj.params);
wikipedia_page.load(Twinkle.xfd.callbacks.mfd.discussionPage);
// Today's list
wikipedia_page = new Morebits.wiki.Page('Wikipedia:Miscellany for deletion', "Adding discussion to today's list");
wikipedia_page.setPageSection(2);
wikipedia_page.setFollowRedirect(true);
wikipedia_page.setCallbackParameters(apiobj.params);
wikipedia_page.load(Twinkle.xfd.callbacks.mfd.todaysList);
// Notification to first contributor and/or notification to owner of userspace
if (apiobj.params.notifycreator || apiobj.params.notifyuserspace) {
const thispage = new Morebits.wiki.Page(mw.config.get('wgPageName'));
thispage.setCallbackParameters(apiobj.params);
thispage.lookupCreation(Twinkle.xfd.callbacks.mfd.sendNotifications);
// or, if not notifying, add this nomination to the user's userspace log without the initial contributor's name
} else {
Twinkle.xfd.callbacks.addToLog(apiobj.params, null);
}
},
taggingPage: function(pageobj) {
const text = pageobj.getPageText();
const params = pageobj.getCallbackParameters();
params.tagText = '{{' + (params.number === '' ? 'mfd' : 'mfdx|' + params.number) + '|help=off}}';
if (['javascript', 'css', 'sanitized-css'].includes(mw.config.get('wgPageContentModel'))) {
params.tagText = '/* ' + params.tagText + ' */\n';
} else {
params.tagText += '\n';
if (params.noinclude) {
params.tagText = '<noinclude>' + params.tagText + '</noinclude>';
}
}
if (pageobj.canEdit() && ['wikitext', 'javascript', 'css', 'sanitized-css'].includes(pageobj.getContentModel())) {
pageobj.setPageText(params.tagText + text);
pageobj.setEditSummary('Nominated for deletion; see [[:' + params.discussionpage + ']].');
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.setWatchlist(Twinkle.getPref('xfdWatchPage'));
pageobj.setCreateOption('nocreate');
pageobj.save();
} else {
Twinkle.xfd.callbacks.autoEditRequest(pageobj, params);
}
},
discussionPage: function(pageobj) {
const params = pageobj.getCallbackParameters();
pageobj.setPageText(Twinkle.xfd.callbacks.getDiscussionWikitext('mfd', params));
pageobj.setEditSummary('Creating deletion discussion page for [[:' + Morebits.pageNameNorm + ']].');
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.setWatchlist(Twinkle.getPref('xfdWatchDiscussion'));
pageobj.setCreateOption('createonly');
pageobj.save(() => {
Twinkle.xfd.currentRationale = null; // any errors from now on do not need to print the rationale, as it is safely saved on-wiki
});
},
todaysList: function(pageobj) {
let text = pageobj.getPageText();
const params = pageobj.getCallbackParameters();
const statelem = pageobj.getStatusElement();
const date = new Morebits.Date(pageobj.getLoadTime());
const date_header = date.format('===MMMM D, YYYY===\n', 'utc');
const date_header_regex = new RegExp(date.format('(===[\\s]*MMMM[\\s]+D,[\\s]+YYYY[\\s]*===)', 'utc'));
const added_data = '{{subst:mfd3|pg=' + Morebits.pageNameNorm + params.numbering + '}}';
if (date_header_regex.test(text)) { // we have a section already
statelem.info('Found today\'s section, proceeding to add new entry');
text = text.replace(date_header_regex, '$1\n' + added_data);
} else { // we need to create a new section
statelem.info('No section for today found, proceeding to create one');
text = text.replace('===', date_header + added_data + '\n\n===');
}
pageobj.setPageText(text);
pageobj.setEditSummary('Adding [[:' + params.discussionpage + ']].');
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.setWatchlist(Twinkle.getPref('xfdWatchList'));
pageobj.setCreateOption('recreate');
pageobj.setDiscussionToolsAutoSubscribe(false);
pageobj.save();
},
sendNotifications: function(pageobj) {
const initialContrib = pageobj.getCreator();
const params = pageobj.getCallbackParameters();
// Notify the creator
if (params.notifycreator) {
Twinkle.xfd.callbacks.notifyUser(params, initialContrib);
}
// Notify the user who owns the subpage if they are not the creator
params.userspaceOwner = mw.config.get('wgRelevantUserName');
if (params.notifyuserspace) {
if (params.userspaceOwner !== initialContrib) {
// Don't log if notifying creator above, will log then
Twinkle.xfd.callbacks.notifyUser(params, params.userspaceOwner, params.notifycreator, 'Notifying owner of userspace (' + params.userspaceOwner + ')');
} else if (!params.notifycreator) {
// If we thought we would notify the owner but didn't,
// then we need to log if we didn't notify the creator
// Twinkle.xfd.callbacks.addToLog(params, null);
Twinkle.xfd.callbacks.addToLog(params, initialContrib);
}
}
}
},
ffd: {
taggingImage: function(pageobj) {
let text = pageobj.getPageText();
const params = pageobj.getCallbackParameters();
const date = new Morebits.Date(pageobj.getLoadTime()).format('YYYY MMMM D', 'utc');
params.logpage = 'Wikipedia:Files for discussion/' + date;
params.discussionpage = params.logpage + '#' + Morebits.pageNameNorm;
params.tagText = '{{ffd|log=' + date + '|help=off}}\n';
if (pageobj.canEdit()) {
text = Twinkle.removeMoveToCommonsTagsFromWikicode(text);
pageobj.setPageText(params.tagText + text);
pageobj.setEditSummary('Listed for discussion at [[:' + params.discussionpage + ']].');
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.setWatchlist(Twinkle.getPref('xfdWatchPage'));
pageobj.setCreateOption('recreate'); // it might be possible for a file to exist without a description page
pageobj.save();
} else {
Twinkle.xfd.callbacks.autoEditRequest(pageobj, params);
}
// Updating data for the action completed event
Morebits.wiki.actionCompleted.redirect = params.logpage;
Morebits.wiki.actionCompleted.notice = 'Nomination completed, now redirecting to the discussion page';
// Contributor specific edits
const wikipedia_page = new Morebits.wiki.Page(mw.config.get('wgPageName'));
wikipedia_page.setCallbackParameters(params);
wikipedia_page.lookupCreation(Twinkle.xfd.callbacks.ffd.main);
},
main: function(pageobj) {
// this is coming in from lookupCreation...!
const params = pageobj.getCallbackParameters();
const initialContrib = pageobj.getCreator();
params.uploader = initialContrib;
// Adding discussion
const wikipedia_page = new Morebits.wiki.Page(params.logpage, "Adding discussion to today's list");
wikipedia_page.setFollowRedirect(true);
wikipedia_page.setCallbackParameters(params);
wikipedia_page.load(Twinkle.xfd.callbacks.ffd.todaysList);
// Notification to first contributor
if (params.notifycreator) {
Twinkle.xfd.callbacks.notifyUser(params, initialContrib);
// or, if not notifying, add this nomination to the user's userspace log without the initial contributor's name
} else {
Twinkle.xfd.callbacks.addToLog(params, null);
}
},
todaysList: function(pageobj) {
let text = pageobj.getPageText();
const params = pageobj.getCallbackParameters();
// add date header if the log is found to be empty (a bot should do this automatically)
if (!pageobj.exists()) {
text = '{{subst:FfD log}}';
}
pageobj.setPageText(text + '\n\n' + Twinkle.xfd.callbacks.getDiscussionWikitext('ffd', params));
pageobj.setEditSummary('Adding [[:' + Morebits.pageNameNorm + ']].');
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.setWatchlist(Twinkle.getPref('xfdWatchDiscussion'));
pageobj.setCreateOption('recreate');
pageobj.save(() => {
Twinkle.xfd.currentRationale = null; // any errors from now on do not need to print the rationale, as it is safely saved on-wiki
});
}
},
cfd: {
main: function(pageobj) {
const params = pageobj.getCallbackParameters();
const date = new Morebits.Date(pageobj.getLoadTime());
params.logpage = 'Wikipedia:Categories for discussion/Log/' + date.format('YYYY MMMM D', 'utc');
params.discussionpage = params.logpage + '#' + Morebits.pageNameNorm;
// Add log/discussion page params to the already-loaded page object
pageobj.setCallbackParameters(params);
// Tagging category
Twinkle.xfd.callbacks.cfd.taggingCategory(pageobj);
// Updating data for the action completed event
Morebits.wiki.actionCompleted.redirect = params.logpage;
Morebits.wiki.actionCompleted.notice = "Nomination completed, now redirecting to today's log";
// Adding discussion to list
let wikipedia_page = new Morebits.wiki.Page(params.logpage, "Adding discussion to today's list");
wikipedia_page.setFollowRedirect(true);
wikipedia_page.setCallbackParameters(params);
wikipedia_page.load(Twinkle.xfd.callbacks.cfd.todaysList);
// Notification to first contributor
if (params.notifycreator) {
wikipedia_page = new Morebits.wiki.Page(mw.config.get('wgPageName'));
wikipedia_page.setCallbackParameters(params);
wikipedia_page.lookupCreation((pageobj) => {
Twinkle.xfd.callbacks.notifyUser(pageobj.getCallbackParameters(), pageobj.getCreator());
});
// or, if not notifying, add this nomination to the user's userspace log without the initial contributor's name
} else {
Twinkle.xfd.callbacks.addToLog(params, null);
}
},
taggingCategory: function(pageobj) {
const text = pageobj.getPageText();
const params = pageobj.getCallbackParameters();
params.tagText = '{{subst:' + params.xfdcat;
let editsummary = (mw.config.get('wgNamespaceNumber') === 14 ? 'Category' : 'Stub template') +
' being considered for ' + params.action;
switch (params.xfdcat) {
case 'cfd':
case 'sfd-t':
break;
case 'cfc':
editsummary += ' to an article';
// falls through
case 'cfm':
case 'cfr':
case 'sfr-t':
params.tagText += '|' + params.cfdtarget;
break;
case 'cfs':
params.tagText += '|' + params.cfdtarget + '|' + params.cfdtarget2;
break;
default:
alert('twinklexfd in taggingCategory(): unknown CFD action');
break;
}
params.tagText += '}}\n';
editsummary += '; see [[:' + params.discussionpage + ']].';
if (pageobj.canEdit()) {
pageobj.setPageText(params.tagText + text);
pageobj.setEditSummary(editsummary);
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.setWatchlist(Twinkle.getPref('xfdWatchPage'));
pageobj.setCreateOption('recreate'); // since categories can be populated without an actual page at that title
pageobj.save();
} else {
Twinkle.xfd.callbacks.autoEditRequest(pageobj, params);
}
},
todaysList: function(pageobj) {
const params = pageobj.getCallbackParameters();
const statelem = pageobj.getStatusElement();
const added_data = Twinkle.xfd.callbacks.getDiscussionWikitext(params.xfdcat, params);
let text;
// add date header if the log is found to be empty (a bot should do this automatically)
if (!pageobj.exists()) {
text = '{{subst:CfD log}}\n' + added_data;
} else {
const old_text = pageobj.getPageText();
text = old_text.replace('below this line -->', 'below this line -->\n' + added_data);
if (text === old_text) {
statelem.error('failed to find target spot for the discussion');
return;
}
}
pageobj.setPageText(text);
pageobj.setEditSummary('Adding ' + params.action + ' nomination of [[:' + Morebits.pageNameNorm + ']].');
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.setWatchlist(Twinkle.getPref('xfdWatchDiscussion'));
pageobj.setCreateOption('recreate');
pageobj.save(() => {
Twinkle.xfd.currentRationale = null; // any errors from now on do not need to print the rationale, as it is safely saved on-wiki
});
}
},
cfds: {
taggingCategory: function(pageobj) {
const text = pageobj.getPageText();
const params = pageobj.getCallbackParameters();
if (params.xfdcat === 'C2F') {
params.tagText = '{{subst:cfm-speedy|1=' + params.cfdstarget.replace(/^:?Category:/, '') + '}}\n';
} else {
params.tagText = '{{subst:cfr-speedy|1=' + params.cfdstarget.replace(/^:?Category:/, '') + '}}\n';
}
params.discussionpage = ''; // CFDS is just a bullet in a bulleted list. There's no section to link to, so we set this to blank. Blank will be recognized by both the generate userspace log code and the generate userspace log edit summary code as "don't wikilink to a section".
if (pageobj.canEdit()) {
pageobj.setPageText(params.tagText + text);
pageobj.setEditSummary('Listed for speedy renaming; see [[WP:CFDS|Categories for discussion/Speedy]].');
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.setWatchlist(Twinkle.getPref('xfdWatchPage'));
pageobj.setCreateOption('recreate'); // since categories can be populated without an actual page at that title
pageobj.save(() => {
// No user notification for CfDS, so just add this nomination to the user's userspace log
Twinkle.xfd.callbacks.addToLog(params, null);
});
} else {
Twinkle.xfd.callbacks.autoEditRequest(pageobj, params);
// No user notification for CfDS, so just add this nomination to the user's userspace log
Twinkle.xfd.callbacks.addToLog(params, null);
}
},
addToList: function(pageobj) {
const old_text = pageobj.getPageText();
const params = pageobj.getCallbackParameters();
const statelem = pageobj.getStatusElement();
const text = old_text.replace('BELOW THIS LINE -->', 'BELOW THIS LINE -->\n' + Twinkle.xfd.callbacks.getDiscussionWikitext('cfds', params));
if (text === old_text) {
statelem.error('failed to find target spot for the discussion');
return;
}
pageobj.setPageText(text);
pageobj.setEditSummary('Adding [[:' + Morebits.pageNameNorm + ']].');
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.setWatchlist(Twinkle.getPref('xfdWatchDiscussion'));
pageobj.setCreateOption('recreate');
pageobj.setDiscussionToolsAutoSubscribe(false);
pageobj.save(() => {
Twinkle.xfd.currentRationale = null; // any errors from now on do not need to print the rationale, as it is safely saved on-wiki
});
}
},
rfd: {
// This gets called both on submit and preview to determine the redirect target
findTarget: function(params, callback) {
// Used by regular redirects to find the target, but for all redirects,
// avoid relying on the client clock to build the log page
const query = {
action: 'query',
curtimestamp: true,
format: 'json'
};
if (document.getElementById('softredirect')) {
// For soft redirects, define the target early
// to skip target checks in findTargetCallback
params.rfdtarget = document.getElementById('softredirect').textContent.replace(/^:+/, '');
} else {
// Find current target of redirect
query.titles = mw.config.get('wgPageName');
query.redirects = true;
}
const wikipedia_api = new Morebits.wiki.Api('Finding target of redirect', query, Twinkle.xfd.callbacks.rfd.findTargetCallback(callback));
wikipedia_api.params = params;
wikipedia_api.post();
},
// This is a closure for the callback from the above API request, which gets the target of the redirect
findTargetCallback: function(callback) {
return function(apiobj) {
const response = apiobj.getResponse();
apiobj.params.curtimestamp = response.curtimestamp;
if (!apiobj.params.rfdtarget) { // Not a softredirect
const target = response.query.redirects && response.query.redirects[0].to;
if (!target) {
let message = 'No target found. this page does not appear to be a redirect, aborting';
if (mw.config.get('wgAction') === 'history') {
message += '. If this is a soft redirect, try again from the content page, not the page history.';
}
apiobj.statelem.error(message);
return;
}
apiobj.params.rfdtarget = target;
const section = response.query.redirects[0].tofragment;
apiobj.params.section = section;
}
callback(apiobj.params);
};
},
main: function(params) {
const date = new Morebits.Date(params.curtimestamp);
params.logpage = 'Wikipedia:Redirects for discussion/Log/' + date.format('YYYY MMMM D', 'utc');
params.discussionpage = params.logpage + '#' + Morebits.pageNameNorm;
// Tagging redirect
let wikipedia_page = new Morebits.wiki.Page(mw.config.get('wgPageName'), 'Adding deletion tag to redirect');
wikipedia_page.setFollowRedirect(false);
wikipedia_page.setCallbackParameters(params);
wikipedia_page.load(Twinkle.xfd.callbacks.rfd.taggingRedirect);
// Updating data for the action completed event
Morebits.wiki.actionCompleted.redirect = params.logpage;
Morebits.wiki.actionCompleted.notice = "Nomination completed, now redirecting to today's log";
// Adding discussion
wikipedia_page = new Morebits.wiki.Page(params.logpage, "Adding discussion to today's log");
wikipedia_page.setFollowRedirect(true);
wikipedia_page.setCallbackParameters(params);
wikipedia_page.load(Twinkle.xfd.callbacks.rfd.todaysList);
// Notifications
if (params.notifycreator || params.relatedpage) {
const thispage = new Morebits.wiki.Page(mw.config.get('wgPageName'));
thispage.setCallbackParameters(params);
thispage.lookupCreation(Twinkle.xfd.callbacks.rfd.sendNotifications);
// or, if not notifying, add this nomination to the user's userspace log without the initial contributor's name
} else {
Twinkle.xfd.callbacks.addToLog(params, null);
}
},
taggingRedirect: function(pageobj) {
const text = pageobj.getPageText();
const params = pageobj.getCallbackParameters();
// Imperfect for edit request but so be it
params.tagText = '{{subst:rfd|' + (mw.config.get('wgNamespaceNumber') === 10 ? 'showontransclusion=1|' : '') + 'content=\n';
if (pageobj.canEdit()) {
pageobj.setPageText(params.tagText + text + '\n}}');
pageobj.setEditSummary('Listed for discussion at [[:' + params.discussionpage + ']].');
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.setWatchlist(Twinkle.getPref('xfdWatchPage'));
pageobj.setCreateOption('nocreate');
pageobj.save();
} else {
Twinkle.xfd.callbacks.autoEditRequest(pageobj, params);
}
},
todaysList: function(pageobj) {
const params = pageobj.getCallbackParameters();
const statelem = pageobj.getStatusElement();
const added_data = Twinkle.xfd.callbacks.getDiscussionWikitext('rfd', params);
let text;
// add date header if the log is found to be empty (a bot should do this automatically)
if (!pageobj.exists()) {
text = '{{subst:RfD log}}' + added_data;
} else {
const old_text = pageobj.getPageText();
text = old_text.replace(/(<!-- Add new entries directly below this line\.? -->)/, '$1\n' + added_data);
if (text === old_text) {
statelem.error('failed to find target spot for the discussion');
return;
}
}
pageobj.setPageText(text);
pageobj.setEditSummary('Adding [[:' + Morebits.pageNameNorm + ']].');
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.setWatchlist(Twinkle.getPref('xfdWatchDiscussion'));
pageobj.setCreateOption('recreate');
pageobj.save(() => {
Twinkle.xfd.currentRationale = null; // any errors from now on do not need to print the rationale, as it is safely saved on-wiki
});
},
sendNotifications: function(pageobj) {
const initialContrib = pageobj.getCreator();
const params = pageobj.getCallbackParameters();
const statelem = pageobj.getStatusElement();
// Notifying initial contributor
if (params.notifycreator) {
Twinkle.xfd.callbacks.notifyUser(params, initialContrib);
}
// Notifying target page's watchers, if not a soft redirect
if (params.relatedpage) {
const targetTalk = new mw.Title(params.rfdtarget).getTalkPage();
// On the offchance it's a circular redirect
if (params.rfdtarget === mw.config.get('wgPageName')) {
statelem.warn('Circular redirect; skipping target page notification');
} else if (document.getElementById('softredirect')) {
statelem.warn('Soft redirect; skipping target page notification');
// Don't issue if target talk is the initial contributor's talk or your own
} else if (targetTalk.getNamespaceId() === 3 && targetTalk.getMainText() === initialContrib) {
statelem.warn('Target is initial contributor; skipping target page notification');
} else if (targetTalk.getNamespaceId() === 3 && targetTalk.getMainText() === mw.config.get('wgUserName')) {
statelem.warn('You (' + mw.config.get('wgUserName') + ') are the target; skipping target page notification');
} else {
// Don't log if notifying creator above, will log then
Twinkle.xfd.callbacks.notifyUser(params, targetTalk.toText(), params.notifycreator, 'Notifying redirect target of the discussion');
return;
}
// If we thought we would notify the target but didn't,
// we need to log if we didn't notify the creator
if (!params.notifycreator) {
Twinkle.xfd.callbacks.addToLog(params, null);
}
}
}
},
rm: {
listAtTalk: function(pageobj) {
const params = pageobj.getCallbackParameters();
params.discussionpage = pageobj.getPageName();
pageobj.setAppendText('\n\n' + Twinkle.xfd.callbacks.getDiscussionWikitext('rm', params));
pageobj.setEditSummary(`Proposing move of ${
params.currentname
.map((currentname, i) => `[[:${currentname}]]${params.newname[i] ? ` to [[:${params.newname[i]}]]` : ''}`)
.join(', ')
}.`);
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.setCreateOption('recreate'); // since the talk page need not exist
pageobj.setWatchlist(Twinkle.getPref('xfdWatchDiscussion'));
pageobj.append(() => {
Twinkle.xfd.currentRationale = null; // any errors from now on do not need to print the rationale, as it is safely saved on-wiki
// add this nomination to the user's userspace log
Twinkle.xfd.callbacks.addToLog(params, null);
});
},
listAtRMTR: function(pageobj) {
const text = pageobj.getPageText();
const params = pageobj.getCallbackParameters();
const statelem = pageobj.getStatusElement();
const discussionWikitext = Twinkle.xfd.callbacks.getDiscussionWikitext('rm', params);
const newtext = Twinkle.xfd.insertRMTR(text, discussionWikitext);
if (text === newtext) {
statelem.error('failed to find target spot for the entry');
return;
}
pageobj.setPageText(newtext);
pageobj.setEditSummary(`Adding [[:${params.currentname.join(']], [[:')}]].`);
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.save(() => {
Twinkle.xfd.currentRationale = null; // any errors from now on do not need to print the rationale, as it is safely saved on-wiki
// add this nomination to the user's userspace log
Twinkle.xfd.callbacks.addToLog(params, null);
});
}
}
};
/**
* Given the wikitext of the WP:RM/TR page and the wikitext to insert, insert it at the bottom of the ==== Uncontroversial technical requests ==== section.
*
* @param {string} pageWikitext
* @param {string} wikitextToInsert Will typically be `{{subst:RMassist|1=From|2=To|reason=Reason}}`, which expands out to `* {{RMassist/core | 1 = From | 2 = To | discuss = yes | reason = Reason | sig = Signature | requester = YourUserName}}`
* @return {string} pageWikitext
*/
Twinkle.xfd.insertRMTR = function(pageWikitext, wikitextToInsert) {
// [^\n]* is for matching anchors, e.g. <span class="anchor" id="*"></span>
const placementRE = /\n{1,}(====[^\n]*Requests to revert undiscussed moves ?====)/i;
return pageWikitext.replace(placementRE, '\n' + wikitextToInsert + '\n\n$1');
};
Twinkle.xfd.callback.evaluate = function(e) {
const form = e.target;
const params = Morebits.QuickForm.getInputData(form);
Morebits.SimpleWindow.setButtonsEnabled(false);
Morebits.Status.init(form);
Twinkle.xfd.currentRationale = params.reason;
Morebits.Status.onError(Twinkle.xfd.printRationale);
let query, wikipedia_page, wikipedia_api;
switch (params.venue) {
case 'afd': // AFD
query = {
action: 'query',
list: 'allpages',
apprefix: 'Articles for deletion/' + Morebits.pageNameNorm,
apnamespace: 4,
apfilterredir: 'nonredirects',
aplimit: 'max', // 500 is max for normal users, 5000 for bots and sysops
format: 'json'
};
wikipedia_api = new Morebits.wiki.Api('Tagging article with deletion tag', query, Twinkle.xfd.callbacks.afd.main);
wikipedia_api.params = params;
wikipedia_api.post();
break;
case 'tfd': // TFD
if (params.tfdtarget) { // remove namespace name
params.tfdtarget = utils.stripNs(params.tfdtarget);
}
// Modules can't be tagged, TfD instructions are to place on /doc subpage
params.scribunto = mw.config.get('wgPageContentModel') === 'Scribunto';
if (params.xfdcat === 'tfm') { // Merge
// Tag this template/module
if (params.scribunto) {
wikipedia_page = new Morebits.wiki.Page(mw.config.get('wgPageName') + '/doc', 'Tagging this module documentation with merge tag');
params.otherTemplateName = 'Module:' + params.tfdtarget;
} else {
wikipedia_page = new Morebits.wiki.Page(mw.config.get('wgPageName'), 'Tagging this template with merge tag');
params.otherTemplateName = 'Template:' + params.tfdtarget;
}
} else { // delete
if (params.scribunto) {
wikipedia_page = new Morebits.wiki.Page(mw.config.get('wgPageName') + '/doc', 'Tagging module documentation with deletion tag');
} else {
wikipedia_page = new Morebits.wiki.Page(mw.config.get('wgPageName'), 'Tagging template with deletion tag');
}
}
wikipedia_page.setFollowRedirect(true); // should never be needed, but if the page is moved, we would want to follow the redirect
wikipedia_page.setCallbackParameters(params);
wikipedia_page.load(Twinkle.xfd.callbacks.tfd.main);
break;
case 'mfd': // MFD
query = {
action: 'query',
list: 'allpages',
apprefix: 'Miscellany for deletion/' + Morebits.pageNameNorm,
apnamespace: 4,
apfilterredir: 'nonredirects',
aplimit: 'max', // 500 is max for normal users, 5000 for bots and sysops
format: 'json'
};
wikipedia_api = new Morebits.wiki.Api('Looking for prior nominations of this page', query, Twinkle.xfd.callbacks.mfd.main);
wikipedia_api.params = params;
wikipedia_api.post();
break;
case 'ffd': // FFD
// Tagging file
// A little out of order with this coming before 'main',
// but tagging doesn't need the uploader parameter,
// while everything else does, so tag then get the uploader
wikipedia_page = new Morebits.wiki.Page(mw.config.get('wgPageName'), 'Adding deletion tag to file page');
wikipedia_page.setFollowRedirect(true);
wikipedia_page.setCallbackParameters(params);
wikipedia_page.load(Twinkle.xfd.callbacks.ffd.taggingImage);
break;
case 'cfd':
if (params.cfdtarget) {
params.cfdtarget = utils.stripNs(params.cfdtarget);
} else {
params.cfdtarget = ''; // delete
}
if (params.cfdtarget2) { // split
params.cfdtarget2 = utils.stripNs(params.cfdtarget2);
}
// Used for customized actions in edit summaries and the notification template
var summaryActions = {
cfd: 'deletion',
'sfd-t': 'deletion',
cfm: 'merging',
cfr: 'renaming',
'sfr-t': 'renaming',
cfs: 'splitting',
cfc: 'conversion'
};
params.action = summaryActions[params.xfdcat];
// Tagging category
wikipedia_page = new Morebits.wiki.Page(mw.config.get('wgPageName'), 'Tagging category with ' + params.action + ' tag');
wikipedia_page.setFollowRedirect(true); // should never be needed, but if the page is moved, we would want to follow the redirect
wikipedia_page.setCallbackParameters(params);
wikipedia_page.load(Twinkle.xfd.callbacks.cfd.main);
break;
case 'cfds':
// add namespace name if missing
params.cfdstarget = utils.addNs(params.cfdstarget, 14);
var logpage = 'Wikipedia:Categories for discussion/Speedy';
// Updating data for the action completed event
Morebits.wiki.actionCompleted.redirect = logpage;
Morebits.wiki.actionCompleted.notice = 'Nomination completed, now redirecting to the discussion page';
// Tagging category
wikipedia_page = new Morebits.wiki.Page(mw.config.get('wgPageName'), 'Tagging category with rename tag');
wikipedia_page.setFollowRedirect(true);
wikipedia_page.setCallbackParameters(params);
wikipedia_page.load(Twinkle.xfd.callbacks.cfds.taggingCategory);
// Adding discussion to list
wikipedia_page = new Morebits.wiki.Page(logpage, 'Adding discussion to the list');
wikipedia_page.setFollowRedirect(true);
wikipedia_page.setCallbackParameters(params);
wikipedia_page.load(Twinkle.xfd.callbacks.cfds.addToList);
break;
case 'rfd':
// find target and pass main as the callback
Twinkle.xfd.callbacks.rfd.findTarget(params, Twinkle.xfd.callbacks.rfd.main);
break;
case 'rm':
var nomPageName = params.rmtr ?
'Wikipedia:Requested moves/Technical requests' :
new mw.Title(Morebits.pageNameNorm).getTalkPage().toText();
Morebits.wiki.actionCompleted.redirect = nomPageName;
Morebits.wiki.actionCompleted.notice = 'Nomination completed, now redirecting to the discussion page';
wikipedia_page = new Morebits.wiki.Page(nomPageName, params.rmtr ? 'Adding entry at WP:RM/TR' : 'Adding entry on talk page');
wikipedia_page.setFollowRedirect(true);
wikipedia_page.setCallbackParameters(params);
if (params.rmtr) {
wikipedia_page.load(Twinkle.xfd.callbacks.rm.listAtRMTR);
} else {
// listAtTalk uses .append(), so no need to load the page
Twinkle.xfd.callbacks.rm.listAtTalk(wikipedia_page);
}
break;
default:
alert('twinklexfd: unknown XFD discussion venue');
break;
}
};
Twinkle.addInitCallback(Twinkle.xfd, 'xfd');
}());
// </nowiki>
4xa8kxn2wcta9ddp2oy8j17z8w2ep6n
769536
769469
2026-10-22T00:00:53Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 1406828: rm cmt
769536
javascript
text/javascript
// <nowiki>
(function() {
/*
****************************************
*** twinklexfd.js: XFD module
****************************************
* Mode of invocation: Tab ("XFD")
* Active on: Existing, non-special pages, except for file pages with no local (non-Commons) file which are not redirects
*/
Twinkle.xfd = function twinklexfd() {
// Disable on:
// * special pages
// * non-existent pages
// * files on Commons, whether there is a local page or not (unneeded local pages of files on Commons are eligible for CSD F2, or R4 if it's a redirect)
if (mw.config.get('wgNamespaceNumber') < 0 || !mw.config.get('wgArticleId') || (mw.config.get('wgNamespaceNumber') === 6 && document.getElementById('mw-sharedupload'))) {
return;
}
let tooltip = 'Start a discussion for deleting';
if (mw.config.get('wgIsRedirect')) {
tooltip += ' or retargeting this redirect';
} else {
switch (mw.config.get('wgNamespaceNumber')) {
case 0:
tooltip += ' or moving this article';
break;
case 10:
tooltip += ' or merging this template';
break;
case 828:
tooltip += ' or merging this module';
break;
case 6:
tooltip += ' this file';
break;
case 14:
tooltip += ', merging or renaming this category';
break;
default:
tooltip += ' this page';
break;
}
}
Twinkle.addPortletLink(Twinkle.xfd.callback, 'XFD', 'tw-xfd', tooltip);
};
const utils = {
/** Get ordinal number figure */
num2order: function(num) {
switch (num) {
case 1: return '';
case 2: return '2nd';
case 3: return '3rd';
default: return num + 'th';
}
},
/**
* Remove namespace name from title if present
* Exception-safe wrapper around mw.Title
*
* @param {string} title
*/
stripNs: function(title) {
const title_obj = mw.Title.newFromUserInput(title);
if (!title_obj) {
return title; // user entered invalid input; do nothing
}
return title_obj.getMainText();
},
/**
* Add namespace name to page title if not already given
* CAUTION: namespace name won't be added if a namespace (*not* necessarily
* the same as the one given) already is there in the title
*
* @param {string} title
* @param {number} namespaceNumber
*/
addNs: function(title, namespaceNumber) {
const title_obj = mw.Title.newFromUserInput(title, namespaceNumber);
if (!title_obj) {
return title; // user entered invalid input; do nothing
}
return title_obj.toText();
},
/**
* Provide Wikipedian TLA style: AfD, RfD, CfDS, RM, SfD, etc.
*
* @param {string} venue
* @return {string}
*/
toTLACase: function(venue) {
return venue
.toString()
// Everybody up, inclduing rm and the terminal s in cfds
.toUpperCase()
// Lowercase the central f in a given TLA and normalize sfd-t and sfr-t
.replace(/(.)F(.)(?:-.)?/, '$1f$2');
}
};
Twinkle.xfd.currentRationale = null;
// error callback on Morebits.Status.object
Twinkle.xfd.printRationale = function twinklexfdPrintRationale() {
if (Twinkle.xfd.currentRationale) {
Morebits.Status.printUserText(Twinkle.xfd.currentRationale, 'Your deletion rationale is provided below, which you can copy and paste into a new XFD dialog if you wish to try again:');
// only need to print the rationale once
Twinkle.xfd.currentRationale = null;
}
};
Twinkle.xfd.callback = function twinklexfdCallback() {
const Window = new Morebits.SimpleWindow(700, 400);
Window.setTitle('Start a deletion discussion (XfD)');
Window.setScriptName('Twinkle');
Window.addFooterLink('About deletion discussions', 'WP:XFD');
Window.addFooterLink('XfD prefs', 'WP:TW/PREF#xfd');
Window.addFooterLink('Twinkle help', 'WP:TW/DOC#xfd');
Window.addFooterLink('Give feedback', 'WT:TW');
const form = new Morebits.QuickForm(Twinkle.xfd.callback.evaluate);
const categories = form.append({
type: 'select',
name: 'venue',
label: 'Deletion discussion venue:',
tooltip: 'When activated, a default choice is made, based on what namespace you are in. This default should be the most appropriate.',
event: Twinkle.xfd.callback.change_category
});
const namespace = mw.config.get('wgNamespaceNumber');
categories.append({
type: 'option',
label: 'AfD (Articles for deletion)',
selected: namespace === 0, // Main namespace
value: 'afd'
});
categories.append({
type: 'option',
label: 'TfD (Templates for discussion)',
selected: [10, 828].includes(namespace), // Template and module namespaces
value: 'tfd'
});
categories.append({
type: 'option',
label: 'FfD (Files for discussion)',
selected: namespace === 6, // File namespace
value: 'ffd'
});
categories.append({
type: 'option',
label: 'CfD (Categories for discussion)',
selected: namespace === 14 || (namespace === 10 && /-stub$/.test(Morebits.pageNameNorm)), // Category namespace and stub templates
value: 'cfd'
});
categories.append({
type: 'option',
label: 'CfD/S (Categories for speedy renaming)',
value: 'cfds'
});
categories.append({
type: 'option',
label: 'MfD (Miscellany for deletion)',
selected: ![0, 6, 10, 14, 828].includes(namespace) || Morebits.pageNameNorm.indexOf('Template:User ', 0) === 0,
// Other namespaces, and userboxes in template namespace
value: 'mfd'
});
categories.append({
type: 'option',
label: 'RfD (Redirects for discussion)',
selected: mw.config.get('wgIsRedirect'),
value: 'rfd'
});
categories.append({
type: 'option',
label: 'RM (Requested moves)',
selected: false,
value: 'rm'
});
form.append({
type: 'div',
id: 'wrong-venue-warn',
style: 'color: red; font-style: italic'
});
form.append({
type: 'checkbox',
list: [
{
label: 'Notify page creator if possible',
value: 'notify',
name: 'notifycreator',
tooltip: "A notification template will be placed on the creator's talk page if this is true.",
checked: true
}
]
});
form.append({
type: 'field',
label: 'Work area',
name: 'work_area'
});
const previewlink = document.createElement('a');
$(previewlink).on('click', () => {
Twinkle.xfd.callbacks.preview(result); // |result| is defined below
});
previewlink.style.cursor = 'pointer';
previewlink.textContent = 'Preview';
form.append({ type: 'div', id: 'xfdpreview', label: [previewlink] });
form.append({ type: 'div', id: 'twinklexfd-previewbox', style: 'display: none' });
form.append({ type: 'submit' });
var result = form.render();
Window.setContent(result);
Window.display();
result.previewer = new Morebits.wiki.Preview($(result).find('div#twinklexfd-previewbox').last()[0]);
// We must init the controls
const evt = document.createEvent('Event');
evt.initEvent('change', true, true);
result.venue.dispatchEvent(evt);
};
Twinkle.xfd.callback.wrongVenueWarning = function twinklexfdWrongVenueWarning(venue) {
let text = '';
const namespace = mw.config.get('wgNamespaceNumber');
switch (venue) {
case 'afd':
if (namespace !== 0) {
text = 'AfD is generally appropriate only for articles.';
} else if (mw.config.get('wgIsRedirect')) {
text = 'Please use RfD for redirects.';
}
break;
case 'tfd':
if (namespace === 10 && /-stub$/.test(Morebits.pageNameNorm)) {
text = 'Use CfD for stub templates.';
} else if (Morebits.pageNameNorm.indexOf('Template:User ') === 0) {
text = 'Please use MfD for userboxes';
}
break;
case 'cfd':
if (![10, 14].includes(namespace)) {
text = 'CfD is only for categories and stub templates.';
}
break;
case 'cfds':
if (namespace !== 14) {
text = 'CfDS is only for categories.';
}
break;
case 'ffd':
if (namespace !== 6) {
text = 'FFD is selected but this page doesn\'t look like a file!';
}
break;
case 'rm':
if (namespace === 14) { // category
text = 'Please use CfD or CfDS for category renames.';
} else if ([118, 119, 2, 3].includes(namespace)) { // draft, draft talk, user, user talk
text = 'RMs are not permitted in draft and userspace, unless they are uncontroversial technical requests.';
}
break;
default: // mfd or rfd
break;
}
$('#wrong-venue-warn').text(text);
};
Twinkle.xfd.callback.change_category = function twinklexfdCallbackChangeCategory(e) {
const value = e.target.value;
const form = e.target.form;
const old_area = Morebits.QuickForm.getElements(e.target.form, 'work_area')[0];
let work_area = null;
const oldreasontextbox = form.getElementsByTagName('textarea')[0];
const oldreason = oldreasontextbox ? oldreasontextbox.value : '';
const appendReasonBox = function twinklexfdAppendReasonBox() {
work_area.append({
type: 'textarea',
name: 'reason',
label: 'Reason:',
value: oldreason,
tooltip: 'You can use wikimarkup in your reason. Twinkle will automatically sign your post.'
});
};
Twinkle.xfd.callback.wrongVenueWarning(value);
form.previewer.closePreview();
switch (value) {
case 'afd':
work_area = new Morebits.QuickForm.Element({
type: 'field',
label: 'Articles for deletion',
name: 'work_area'
});
work_area.append({
type: 'div',
label: '', // Added later by Twinkle.makeFindSourcesDiv()
id: 'twinkle-xfd-findsources',
style: 'margin-bottom: 5px; margin-top: -5px;'
});
work_area.append({
type: 'select',
name: 'outcome',
label: 'Desired outcome:',
event: Twinkle.xfd.callbacks.changeAfdOutcome,
list: [
{ label: 'Delete', value: 'deletion' },
{ label: 'Merge', value: 'merging' },
{ label: 'Redirect', value: 'redirecting' },
{ label: 'Draftify', value: 'draftification' }
]
});
work_area.append({
name: 'afdtarget',
type: 'input',
label: 'Target page:',
tooltip: 'Target page for the redirect or merge.',
event: Twinkle.xfd.callbacks.changeAfdTarget
});
work_area.append({
type: 'checkbox',
list: [
{
label: 'Wrap deletion tag with <noinclude>',
value: 'noinclude',
name: 'noinclude',
tooltip: 'Will wrap the deletion tag in <noinclude> tags, so that it won\'t transclude. This option is not normally required.'
}
]
});
work_area.append({
type: 'select',
name: 'xfdcat',
label: 'Choose what category this nomination belongs in:',
list: [
{ label: 'Unknown', value: '?', selected: true },
{ label: 'Media and music', value: 'M' },
{ label: 'Organisation, corporation, or product', value: 'O' },
{ label: 'Biographical', value: 'B' },
{ label: 'Society topics', value: 'S' },
{ label: 'Web or internet', value: 'W' },
{ label: 'Games or sports', value: 'G' },
{ label: 'Science and technology', value: 'T' },
{ label: 'Fiction and the arts', value: 'F' },
{ label: 'Places and transportation', value: 'P' },
{ label: 'Indiscernible or unclassifiable topic', value: 'I' },
{ label: 'Debate not yet sorted', value: 'U' }
]
});
work_area.append({
type: 'select',
multiple: true,
name: 'delsortCats',
label: 'Choose deletion sorting categories:',
tooltip: 'Select a few categories that are specifically relevant to the subject of the article. Be as precise as possible; categories like People and USA should only be used when no other categories apply.'
});
// grab deletion sort categories from en-wiki
Morebits.wiki.getCachedJson('Wikipedia:WikiProject_Deletion_sorting/Computer-readable.json').then((delsortCategories) => {
const $select = $('[name="delsortCats"]');
$.each(delsortCategories, (groupname, list) => {
const $optgroup = $('<optgroup>').attr('label', groupname);
const $delsortCat = $select.append($optgroup);
list.forEach((item) => {
const $option = $('<option>').val(item).text(item);
$delsortCat.append($option);
});
});
});
appendReasonBox();
work_area = work_area.render();
old_area.parentNode.replaceChild(work_area, old_area);
// Now that we've rendered the form, hide the target text box. Unhide it later for certain outcomes, using a callback.
$('[name="afdtarget"]').parent().hide();
Twinkle.makeFindSourcesDiv('#twinkle-xfd-findsources');
$(work_area).find('[name=delsortCats]')
.attr('data-placeholder', 'Select delsort pages')
.select2({
theme: 'default select2-morebits',
width: '100%',
matcher: Morebits.select2.matcher,
templateResult: Morebits.select2.highlightSearchMatches,
language: {
searching: Morebits.select2.queryInterceptor
},
// Link text to the page itself
templateSelection: function(choice) {
return $('<a>').text(choice.text).attr({
href: mw.util.getUrl('Wikipedia:WikiProject_Deletion_sorting/' + choice.text),
target: '_blank'
});
}
});
mw.util.addCSS(
// Remove black border
'.select2-container--default.select2-container--focus .select2-selection--multiple { border: 1px solid #aaa; }' +
// Reduce padding
'.select2-results .select2-results__option { padding-top: 1px; padding-bottom: 1px; }' +
'.select2-results .select2-results__group { padding-top: 1px; padding-bottom: 1px; } ' +
// Adjust font size
'.select2-container .select2-dropdown .select2-results { font-size: 13px; }' +
'.select2-container .selection .select2-selection__rendered { font-size: 13px; }' +
// Make the tiny cross larger
'.select2-selection__choice__remove { font-size: 130%; }'
);
break;
case 'tfd':
work_area = new Morebits.QuickForm.Element({
type: 'field',
label: 'Templates for discussion',
name: 'work_area'
});
var templateOrModule = mw.config.get('wgPageContentModel') === 'Scribunto' ? 'module' : 'template';
work_area.append({
type: 'select',
label: 'Choose type of action wanted:',
name: 'xfdcat',
event: function(e) {
const target = e.target;
let tfdtarget = target.form.tfdtarget;
// add/remove extra input box
if (target.value === 'tfm' && !tfdtarget) {
tfdtarget = new Morebits.QuickForm.Element({
name: 'tfdtarget',
type: 'input',
label: 'Other ' + templateOrModule + ' to be merged:',
tooltip: 'Required. Should not include the ' + Morebits.string.toUpperCaseFirstChar(templateOrModule) + ': namespace prefix.',
required: true
});
target.parentNode.appendChild(tfdtarget.render());
} else {
$(Morebits.QuickForm.getElementContainer(tfdtarget)).remove();
tfdtarget = null;
}
},
list: [
{ label: 'Deletion', value: 'tfd', selected: true },
{ label: 'Merge', value: 'tfm' }
]
});
work_area.append({
type: 'select',
name: 'templatetype',
label: 'Deletion tag display style:',
tooltip: 'Which <code>type=</code> parameter to pass to the TfD tag template.',
list: templateOrModule === 'module' ? [
{ value: 'module', label: 'Module', selected: true }
] : [
{ value: 'standard', label: 'Standard', selected: true },
{ value: 'sidebar', label: 'Sidebar/infobox', selected: $('.infobox').length },
{ value: 'inline', label: 'Inline template', selected: (() => {
// TODO: clean up after legacy parser is permanently replaced with Parsoid
const $oldParser = $('.mw-parser-output > p .Inline-Template');
const $newParser = $('.mw-parser-output section[data-mw-section-id="0"] p .Inline-Template');
const $scanTarget = $newParser.length ? $newParser : $oldParser;
return $scanTarget.length;
})() },
{ value: 'tiny', label: 'Tiny inline' },
{ value: 'disabled', label: 'Disabled' }
]
});
work_area.append({
type: 'checkbox',
list: [
{
label: 'Wrap deletion tag with <noinclude> (for substituted templates only)',
value: 'noinclude',
name: 'noinclude',
tooltip: 'Will wrap the deletion tag in <noinclude> tags, so that it won\'t get substituted along with the template.',
disabled: templateOrModule === 'module',
checked: !!$('.box-Subst_only').length // Default to checked if page carries {{subst only}}
}
]
});
work_area.append({
type: 'checkbox',
list: [
{
label: 'Notify talk pages of affected user scripts',
value: 'devpages',
name: 'devpages',
tooltip: 'A notification will be sent to Twinkle, AWB, and Ultraviolet\'s talk pages if those user scripts are marked as using this template.',
checked: true
}
]
});
appendReasonBox();
work_area = work_area.render();
old_area.parentNode.replaceChild(work_area, old_area);
break;
case 'mfd':
work_area = new Morebits.QuickForm.Element({
type: 'field',
label: 'Miscellany for deletion',
name: 'work_area'
});
if (mw.config.get('wgNamespaceNumber') !== 710) { // TimedText cannot be tagged, so asking whether to noinclude the tag is pointless
work_area.append({
type: 'checkbox',
list: [
{
label: 'Wrap deletion tag with <noinclude>',
value: 'noinclude',
name: 'noinclude',
tooltip: 'Will wrap the deletion tag in <noinclude> tags, so that it won\'t transclude. Select this option for userboxes.'
}
]
});
}
if ((mw.config.get('wgNamespaceNumber') === 2 /* User: */ || mw.config.get('wgNamespaceNumber') === 3 /* User talk: */) && mw.config.exists('wgRelevantUserName')) {
work_area.append({
type: 'checkbox',
list: [
{
label: 'Notify owner of userspace (if they are not the page creator)',
value: 'notifyuserspace',
name: 'notifyuserspace',
tooltip: 'If the user in whose userspace this page is located is not the page creator (for example, the page is a rescued article stored as a userspace draft), notify the userspace owner as well.',
checked: true
}
]
});
}
appendReasonBox();
work_area = work_area.render();
old_area.parentNode.replaceChild(work_area, old_area);
break;
case 'ffd':
work_area = new Morebits.QuickForm.Element({
type: 'field',
label: 'Discussion venues for files',
name: 'work_area'
});
appendReasonBox();
work_area = work_area.render();
old_area.parentNode.replaceChild(work_area, old_area);
break;
case 'cfd':
work_area = new Morebits.QuickForm.Element({
type: 'field',
label: 'Categories for discussion',
name: 'work_area'
});
var isCategory = mw.config.get('wgNamespaceNumber') === 14;
work_area.append({
type: 'select',
label: 'Choose type of action wanted:',
name: 'xfdcat',
event: function(e) {
const value = e.target.value,
cfdtarget = e.target.form.cfdtarget;
let cfdtarget2 = e.target.form.cfdtarget2;
// update enabled status
cfdtarget.disabled = value === 'cfd' || value === 'sfd-t';
if (isCategory) {
// update label
if (value === 'cfs') {
Morebits.QuickForm.setElementLabel(cfdtarget, 'Target categories: ');
} else if (value === 'cfc') {
Morebits.QuickForm.setElementLabel(cfdtarget, 'Target article: ');
} else {
Morebits.QuickForm.setElementLabel(cfdtarget, 'Target category: ');
}
// add/remove extra input box
if (value === 'cfs') {
if (cfdtarget2) {
cfdtarget2.disabled = false;
$(cfdtarget2).show();
} else {
cfdtarget2 = document.createElement('input');
cfdtarget2.setAttribute('name', 'cfdtarget2');
cfdtarget2.setAttribute('type', 'text');
cfdtarget2.setAttribute('required', 'true');
cfdtarget.parentNode.appendChild(cfdtarget2);
}
} else {
$(cfdtarget2).prop('disabled', true);
$(cfdtarget2).hide();
}
} else { // Update stub template label
Morebits.QuickForm.setElementLabel(cfdtarget, 'Target stub template: ');
}
},
list: isCategory ? [
{ label: 'Deletion', value: 'cfd', selected: true },
{ label: 'Merge', value: 'cfm' },
{ label: 'Renaming', value: 'cfr' },
{ label: 'Split', value: 'cfs' },
{ label: 'Convert into article', value: 'cfc' }
] : [
{ label: 'Stub Deletion', value: 'sfd-t', selected: true },
{ label: 'Stub Renaming', value: 'sfr-t' }
]
});
work_area.append({
type: 'input',
name: 'cfdtarget',
label: 'Target category:', // default, changed above
disabled: true,
required: true, // only when enabled
value: ''
});
appendReasonBox();
work_area = work_area.render();
old_area.parentNode.replaceChild(work_area, old_area);
break;
case 'cfds':
work_area = new Morebits.QuickForm.Element({
type: 'field',
label: 'Categories for speedy renaming',
name: 'work_area'
});
work_area.append({
type: 'select',
label: 'C2 sub-criterion:',
name: 'xfdcat',
tooltip: 'See WP:CFDS for full explanations.',
list: [
{ label: 'C2A: Typographic and spelling fixes', value: 'C2A', selected: true },
{ label: 'C2B: Naming conventions and disambiguation', value: 'C2B' },
{ label: 'C2C: Consistency with names of similar categories', value: 'C2C' },
{ label: 'C2D: Rename to match article name', value: 'C2D' },
{ label: 'C2E: Author request', value: 'C2E' },
{ label: 'C2F: One eponymous article', value: 'C2F' }
]
});
work_area.append({
type: 'input',
name: 'cfdstarget',
label: 'New name:',
size: 70,
value: '',
required: true
});
appendReasonBox();
work_area = work_area.render();
old_area.parentNode.replaceChild(work_area, old_area);
break;
case 'rfd':
work_area = new Morebits.QuickForm.Element({
type: 'field',
label: 'Redirects for discussion',
name: 'work_area'
});
work_area.append({
type: 'checkbox',
list: [
{
label: 'Notify target page if possible',
value: 'relatedpage',
name: 'relatedpage',
tooltip: "A notification template will be placed on the talk page of this redirect's target if this is true.",
checked: true
}
]
});
appendReasonBox();
work_area = work_area.render();
old_area.parentNode.replaceChild(work_area, old_area);
break;
case 'rm': {
work_area = new Morebits.QuickForm.Element({
type: 'field',
label: 'Requested moves',
name: 'work_area'
});
work_area.append({
type: 'checkbox',
list: [
{
label: 'Uncontroversial technical request',
value: 'rmtr',
name: 'rmtr',
tooltip: 'Use this option when you are unable to perform this uncontroversial move yourself because of a technical reason (e.g. a page already exists at the new title, or the page is protected)',
checked: false,
event: function() {
$('input[name="newname"]', form).prop('required', this.checked);
$('input[type="button"][value="more"]', form)[0].sublist.inputs[1].required = this.checked;
},
subgroup: {
type: 'checkbox',
list: [
{
label: 'Opt out of discussion if the request is contested',
value: 'rmtr-discuss',
name: 'rmtr-discuss',
tooltip: 'Use this option if you prefer to withdraw the request if contested, rather than discuss it. This suppresses the "discuss" link, which may be used to convert your request to a discussion on the talk page.',
checked: false
}
]
}
}
]
});
work_area.append({
type: 'dyninput',
inputs: [
{
label: 'From:',
name: 'currentname',
required: true
},
{
label: 'To:',
name: 'newname',
tooltip: 'Required for technical requests. Otherwise, if unsure of the appropriate title, you may leave it blank.'
}
],
min: 1
});
appendReasonBox();
work_area = work_area.render();
old_area.parentNode.replaceChild(work_area, old_area);
const currentNonTalkPage = mw.Title.newFromText(Morebits.pageNameNorm).getSubjectPage().toText();
form.currentname.value = currentNonTalkPage;
break;
}
default:
work_area = new Morebits.QuickForm.Element({
type: 'field',
label: 'Nothing for anything',
name: 'work_area'
});
work_area = work_area.render();
old_area.parentNode.replaceChild(work_area, old_area);
break;
}
// Return to checked state when switching, but no creator notification for CFDS or RM
form.notifycreator.disabled = value === 'cfds' || value === 'rm';
form.notifycreator.checked = !form.notifycreator.disabled;
};
Twinkle.xfd.callbacks = {
/** If the user hasn't modified the reason much, modify the reason to include the target article. If the user has modified the reason box with a custom reason, do nothing, since we don't want to blank their work. */
changeAfdTarget: function() {
const $afdTarget = $('[name="afdtarget"]');
const $reason = $('[name="reason"]');
const $outcome = $('[name="outcome"]');
if ($reason.val().endsWith('because ')) {
// Target has something typed in
if ($afdTarget.val()) {
if ($outcome.val() === 'redirecting') {
$reason.val(`I propose '''redirecting''' to [[${$afdTarget.val()}]] because `);
} else if ($outcome.val() === 'merging') {
$reason.val(`I propose '''merging''' to [[${$afdTarget.val()}]] because `);
}
// Target is blank
} else {
if ($outcome.val() === 'redirecting') {
$reason.val("I propose '''redirecting''' because ");
} else if ($outcome.val() === 'merging') {
$reason.val("I propose '''merging''' because ");
}
}
}
},
/** Print a default reason in the reason textarea, depending on which outcome is selected from the outcome dropdown list. */
changeAfdOutcome: function() {
const $outcome = $('[name="outcome"]');
const $reason = $('[name="reason"]');
const $afdTarget = $('[name="afdtarget"]');
$afdTarget.val('');
if ($outcome.val() === 'redirecting') {
$reason.val("I propose '''redirecting''' because ");
$afdTarget.parent().show();
} else if ($outcome.val() === 'merging') {
$reason.val("I propose '''merging''' because ");
$afdTarget.parent().show();
} else if ($outcome.val() === 'draftification') {
$reason.val("I propose '''draftifying''' because ");
$afdTarget.parent().hide();
} else if ($outcome.val() === 'deletion') {
$reason.val('');
$afdTarget.parent().hide();
}
},
/** Requires having the tag text (params.tagText) set ahead of time */
autoEditRequest: function(pageobj, params) {
const talkName = new mw.Title(pageobj.getPageName()).getTalkPage().toText();
if (talkName === pageobj.getPageName()) {
pageobj.getStatusElement().error('Page protected and nowhere to add an edit request, aborting');
} else {
pageobj.getStatusElement().warn('Page protected, requesting edit');
const editRequest = '{{subst:Xfd edit protected|page=' + pageobj.getPageName() +
'|discussion=' + params.discussionpage + (params.venue === 'rfd' ? '|rfd=yes' : '') +
'|tag=<nowiki>' + params.tagText + '\u003C/nowiki>}}'; // U+003C: <
const talk_page = new Morebits.wiki.Page(talkName, 'Automatically posting edit request on talk page');
talk_page.setNewSectionTitle('Edit request to complete ' + utils.toTLACase(params.venue) + ' nomination');
talk_page.setNewSectionText(editRequest);
talk_page.setCreateOption('recreate');
talk_page.setWatchlist(Twinkle.getPref('xfdWatchPage'));
talk_page.setFollowRedirect(true); // should never be needed, but if the article is moved, we would want to follow the redirect
talk_page.setChangeTags(Twinkle.changeTags);
talk_page.setCallbackParameters(params);
talk_page.newSection(null, () => {
talk_page.getStatusElement().warn('Unable to add edit request, the talk page may be protected');
});
}
},
getDiscussionWikitext: function(venue, params) {
if (venue === 'cfds') { // CfD/S takes a completely different style
return '* [[:' + Morebits.pageNameNorm + ']] to [[:' + params.cfdstarget + ']]\u00A0\u2013 ' +
params.xfdcat + (params.reason ? ': ' + Morebits.string.formatReasonText(params.reason) : '.') + ' ~~~~';
// U+00A0 NO-BREAK SPACE; U+2013 EN RULE
}
if (venue === 'rm') {
if (params.rmtr) {
const rmtrDiscuss = params['rmtr-discuss'] ? '|discuss=no' : '';
return params.currentname
.map((currentname, i) => `{{subst:RMassist|1=${currentname}|2=${params.newname[i]}${rmtrDiscuss}|reason=${params.reason}}}`)
.join('\n');
}
return `{{subst:Requested move${
params.currentname
.map((currentname, i) => `|current${i + 1}=${currentname}|new${i + 1}=${params.newname[i]}`)
.join('')
}|reason=${params.reason}}}`;
}
let text = '{{subst:' + venue + '2';
const reasonKey = venue === 'ffd' ? 'Reason' : 'text';
// Add a reason unconditionally, so that at least a signature is added
text += '|' + reasonKey + '=' + Morebits.string.formatReasonText(params.reason, true);
if (venue === 'afd' || venue === 'mfd') {
text += '|pg=' + Morebits.pageNameNorm;
if (venue === 'afd') {
text += '|cat=' + params.xfdcat;
}
} else if (venue === 'rfd') {
text += '|redirect=' + Morebits.pageNameNorm;
} else {
text += '|1=' + mw.config.get('wgTitle');
if (mw.config.get('wgPageContentModel') === 'Scribunto') {
text += '|module=Module:';
}
}
if (params.rfdtarget) {
text += '|target=' + params.rfdtarget + (params.section ? '#' + params.section : '');
} else if (params.tfdtarget) {
text += '|2=' + params.tfdtarget;
} else if (params.cfdtarget) {
text += '|2=' + params.cfdtarget;
if (params.cfdtarget2) {
text += '|3=' + params.cfdtarget2;
}
} else if (params.uploader) {
text += '|Uploader=' + params.uploader;
}
text += '}}';
if (venue === 'rfd' || venue === 'tfd' || venue === 'cfd') {
text += '\n';
}
// Don't delsort if delsortCats is undefined (TFD, FFD, etc.)
// Don't delsort if delsortCats is an empty array (AFD where user chose no categories)
if (Array.isArray(params.delsortCats) && params.delsortCats.length) {
text += '\n{{subst:Deletion sorting/multi|' + params.delsortCats.join('|') + '|sig=~~~~}}';
}
return text;
},
showPreview: function(form, venue, params) {
const templatetext = Twinkle.xfd.callbacks.getDiscussionWikitext(venue, params);
if (venue === 'rm') { // RM templates are sensitive to page title
form.previewer.beginRender(templatetext, params.rmtr ? 'Wikipedia:Requested moves/Technical requests' : new mw.Title(Morebits.pageNameNorm).getTalkPage().toText());
} else {
form.previewer.beginRender(templatetext, 'WP:TW'); // Force wikitext
}
},
preview: function(form) {
// venue, reason, xfdcat, tfdtarget, cfdtarget, cfdtarget2, cfdstarget, delsortCats, newname
const params = Morebits.QuickForm.getInputData(form);
const venue = params.venue;
// Remove CfD or TfD namespace prefixes if given
if (params.tfdtarget) {
params.tfdtarget = utils.stripNs(params.tfdtarget);
} else if (params.cfdtarget) {
params.cfdtarget = utils.stripNs(params.cfdtarget);
if (params.cfdtarget2) {
params.cfdtarget2 = utils.stripNs(params.cfdtarget2);
}
} else if (params.cfdstarget) { // Add namespace if not given (CFDS)
params.cfdstarget = utils.addNs(params.cfdstarget, 14);
}
if (venue === 'ffd') {
// Fetch the uploader
const page = new Morebits.wiki.Page(mw.config.get('wgPageName'));
page.lookupCreation(() => {
params.uploader = page.getCreator();
Twinkle.xfd.callbacks.showPreview(form, venue, params);
});
} else if (venue === 'rfd') { // Find the target
Twinkle.xfd.callbacks.rfd.findTarget(params, (params) => {
Twinkle.xfd.callbacks.showPreview(form, venue, params);
});
} else if (venue === 'cfd') { // Swap in CfD subactions
Twinkle.xfd.callbacks.showPreview(form, params.xfdcat, params);
} else {
Twinkle.xfd.callbacks.showPreview(form, venue, params);
}
},
/**
* Unified handler for sending {{Xfd notice}} notifications
* Also handles userspace logging
*
* @param {Object} params
* @param {string} notifyTarget The user or page being notified
* @param {boolean} [noLog=false] Whether to skip logging to userspace
* XfD log, especially useful in cases in where multiple notifications
* may be sent out (MfD, TfM, RfD)
* @param {string} [actionName] Alternative description of the action
* being undertaken. Required if not notifying a user talk page.
*/
notifyUser: function(params, notifyTarget, noLog, actionName) {
// Ensure items with User talk or no namespace prefix both end
// up at user talkspace as expected, but retain the
// prefix-less username for addToLog
const userTalkNamespace = 3;
notifyTarget = mw.Title.newFromText(notifyTarget, userTalkNamespace);
const targetNS = notifyTarget.getNamespaceId();
const usernameOrTarget = notifyTarget.getRelativeText(userTalkNamespace);
notifyTarget = notifyTarget.toText();
if (targetNS === userTalkNamespace) {
// Disallow warning yourself
if (usernameOrTarget === mw.config.get('wgUserName')) {
Morebits.Status.warn('You (' + usernameOrTarget + ') created this page; skipping user notification');
// if we thought we would notify someone but didn't,
// then jump to logging.
Twinkle.xfd.callbacks.addToLog(params, null);
return;
}
// Default is notifying the initial contributor, but MfD also
// notifies userspace page owner
actionName = actionName || 'Notifying initial contributor (' + usernameOrTarget + ')';
}
const notifyText = Twinkle.xfd.callbacks.generateUserTalkNoticeWikitext(params);
// Link to the venue; object used here rather than repetitive items in switch
const venueNames = {
afd: 'Articles for deletion',
tfd: 'Templates for discussion',
mfd: 'Miscellany for deletion',
cfd: 'Categories for discussion',
ffd: 'Files for discussion',
rfd: 'Redirects for discussion'
};
const editSummary = 'Notification: [[' + params.discussionpage + '|listing]] of [[:' +
Morebits.pageNameNorm + ']] at [[WP:' + venueNames[params.venue] + ']].';
const usertalkpage = new Morebits.wiki.Page(notifyTarget, actionName);
usertalkpage.setAppendText(notifyText);
usertalkpage.setEditSummary(editSummary);
usertalkpage.setChangeTags(Twinkle.changeTags);
usertalkpage.setCreateOption('recreate');
// Different pref for RfD target notifications
if (params.venue === 'rfd' && targetNS !== 3) {
usertalkpage.setWatchlist(Twinkle.getPref('xfdWatchRelated'));
} else {
usertalkpage.setWatchlist(Twinkle.getPref('xfdWatchUser'));
}
usertalkpage.setFollowRedirect(true, false);
if (noLog) {
usertalkpage.append();
} else {
usertalkpage.append(() => {
// Don't treat RfD target or MfD userspace owner as initialContrib in log
if (!params.notifycreator) {
notifyTarget = null;
}
// add this nomination to the user's userspace log
Twinkle.xfd.callbacks.addToLog(params, usernameOrTarget);
}, () => {
// if user could not be notified, log nomination without mentioning that notification was sent
Twinkle.xfd.callbacks.addToLog(params, null);
});
}
},
generateUserTalkNoticeWikitext: function(params) {
// For grep: Afd notice, Mfd notice, Tfd notice, Cfd notice, Ffd notice, Rfd notice
let notifytext = '\n{{subst:' + params.venue + ' notice';
const templateNamespace = 10;
// Venue-specific parameters
switch (params.venue) {
case 'afd':
notifytext += params.outcome !== 'deletion' ? '|outcome=' + params.outcome : '';
notifytext += params.afdtarget ? '|target=' + params.afdtarget : '';
// Tell the template to add " (Xnd nomination)" to the XFD title, if needed.
// The   (HTML space character) is needed to overcome MediaWiki's parameter auto-trim.
notifytext += params.numbering !== '' ? '|order= ' + params.numbering : '';
break;
case 'mfd':
// tell the template to add " (Xnd nomination)" to the XFD title, if needed
notifytext += params.numbering !== '' ? '|order= ' + params.numbering : '';
break;
case 'tfd':
if (params.xfdcat === 'tfm') {
notifytext = '\n{{subst:Tfm notice|2=' + params.tfdtarget;
}
break;
case 'cfd':
notifytext += '|action=' + params.action + (mw.config.get('wgNamespaceNumber') === templateNamespace ? '|stub=yes' : '');
break;
default: // ffd, rfd
break;
}
notifytext += '|1=' + Morebits.pageNameNorm + '}} ~~~~';
return notifytext;
},
addToLog: function(params, initialContrib) {
if (!Twinkle.getPref('logXfdNominations') || Twinkle.getPref('noLogOnXfdNomination').includes(params.venue)) {
return;
}
const usl = new Morebits.UserspaceLogger(Twinkle.getPref('xfdLogPageName'));// , 'Adding entry to userspace log');
usl.initialText =
"This is a log of all [[WP:XFD|deletion discussion]] nominations made by this user using [[WP:TW|Twinkle]]'s XfD module.\n\n" +
'If you no longer wish to keep this log, you can turn it off using the [[Wikipedia:Twinkle/Preferences|preferences panel]], and ' +
'nominate this page for speedy deletion under [[WP:CSD#U1|CSD U1]].' +
(Morebits.userIsSysop ? '\n\nThis log does not track XfD-related deletions made using Twinkle.' : '');
let editsummary;
if (params.discussionpage) {
editsummary = 'Logging [[' + params.discussionpage + '|' + utils.toTLACase(params.venue) + ' nomination]] of [[:' + Morebits.pageNameNorm + ']].';
} else {
editsummary = 'Logging ' + utils.toTLACase(params.venue) + ' nomination of [[:' + Morebits.pageNameNorm + ']].';
}
// If a logged file is deleted but exists on commons, the wikilink will be blue, so provide a link to the log
const fileLogLink = mw.config.get('wgNamespaceNumber') === 6 ? ' ([{{fullurl:Special:Log|page=' + mw.util.wikiUrlencode(mw.config.get('wgPageName')) + '}} log])' : '';
// CFD/S and RM don't have canonical links
const nominatedLink = params.discussionpage ? '[[' + params.discussionpage + '|nominated]]' : 'nominated';
let appendText = '# [[:' + Morebits.pageNameNorm + ']]:' + fileLogLink + ' ' + nominatedLink + ' at [[WP:' + params.venue.toUpperCase() + '|' + utils.toTLACase(params.venue) + ']]';
switch (params.venue) {
case 'tfd':
if (params.xfdcat === 'tfm') {
appendText += ' (merge)';
if (params.tfdtarget) {
const contentModel = mw.config.get('wgPageContentModel') === 'Scribunto' ? 'Module:' : 'Template:';
appendText += '; Other ' + contentModel.toLowerCase() + ' [[';
if (!new RegExp('^:?' + Morebits.namespaceRegex([10, 828]) + ':', 'i').test(params.tfdtarget)) {
appendText += contentModel;
}
appendText += params.tfdtarget + ']]';
}
}
break;
case 'mfd':
if (params.notifyuserspace && params.userspaceOwner && params.userspaceOwner !== initialContrib) {
appendText += '; notified {{user|1=' + params.userspaceOwner + '}}';
}
break;
case 'cfd':
appendText += ' (' + utils.toTLACase(params.xfdcat) + ')';
if (params.cfdtarget) {
const categoryOrTemplate = params.xfdcat.charAt(0) === 's' ? 'Template:' : ':Category:';
appendText += '; ' + params.action + ' to [[' + categoryOrTemplate + params.cfdtarget + ']]';
if (params.xfdcat === 'cfs' && params.cfdtarget2) {
appendText += ', [[' + categoryOrTemplate + params.cfdtarget2 + ']]';
}
}
break;
case 'cfds':
appendText += ' (' + utils.toTLACase(params.xfdcat) + ')';
// Ensure there's more than just 'Category:'
if (params.cfdstarget && params.cfdstarget.length > 9) {
appendText += '; New name: [[:' + params.cfdstarget + ']]';
}
break;
case 'rfd':
if (params.rfdtarget) {
appendText += '; Target: [[:' + params.rfdtarget + ']]';
if (params.relatedpage) {
appendText += ' (notified)';
}
}
break;
case 'rm':
appendText = params.currentname
.map((currentname, i) => `# [[:${currentname}]]: ${nominatedLink} at [[WP:RM${params.rmtr ? '/TR' : ''}|]]${params.newname[i] ? `; New name: [[:${params.newname[i]}]]` : ''}`)
.join('\n');
break;
default: // afd or ffd
break;
}
if (initialContrib && params.notifycreator) {
appendText += '; notified {{user|1=' + initialContrib + '}}';
}
appendText += ' ~~~~~';
if (params.reason) {
appendText += "\n#* '''Reason''': " + Morebits.string.formatReasonForLog(params.reason);
}
usl.changeTags = Twinkle.changeTags;
usl.log(appendText, editsummary);
},
afd: {
main: function(apiobj) {
const response = apiobj.getResponse();
const titles = response.query.allpages;
// There has been no earlier entries with this prefix, just go on.
if (titles.length <= 0) {
apiobj.params.numbering = apiobj.params.number = '';
} else {
let number = 0;
for (let i = 0; i < titles.length; ++i) {
const title = titles[i].title;
// First, simple test, is there an instance with this exact name?
if (title === 'Wikipedia:Articles for deletion/' + Morebits.pageNameNorm) {
number = Math.max(number, 1);
continue;
}
const order_re = new RegExp('^' +
Morebits.string.escapeRegExp('Wikipedia:Articles for deletion/' + Morebits.pageNameNorm) +
'\\s*\\(\\s*(\\d+)(?:(?:th|nd|rd|st) nom(?:ination)?)?\\s*\\)\\s*$');
const match = order_re.exec(title);
// No match; A non-good value
// Or the match is an unrealistically high number. Avoid false positives such as Wikipedia:Articles for deletion/The Basement (2014), by ignoring matches greater than 100
if (!match || match[1] > 100) {
continue;
}
// A match, set number to the max of current
number = Math.max(number, Number(match[1]));
}
apiobj.params.number = utils.num2order(parseInt(number, 10) + 1);
apiobj.params.numbering = number > 0 ? ' (' + apiobj.params.number + ' nomination)' : '';
}
apiobj.params.discussionpage = 'Wikipedia:Articles for deletion/' + Morebits.pageNameNorm + apiobj.params.numbering;
Morebits.Status.info('Next discussion page', '[[' + apiobj.params.discussionpage + ']]');
// Updating data for the action completed event
Morebits.wiki.actionCompleted.redirect = apiobj.params.discussionpage;
Morebits.wiki.actionCompleted.notice = 'Nomination completed, now redirecting to the discussion page';
// Tagging article
const wikipedia_page = new Morebits.wiki.Page(mw.config.get('wgPageName'), 'Adding deletion tag to article');
wikipedia_page.setFollowRedirect(true); // should never be needed, but if the article is moved, we would want to follow the redirect
wikipedia_page.setChangeTags(Twinkle.changeTags); // Here to apply to triage
wikipedia_page.setCallbackParameters(apiobj.params);
wikipedia_page.load(Twinkle.xfd.callbacks.afd.taggingArticle);
},
// Tagging needs to happen before everything else: this means we can check if there is an AfD tag already on the page
taggingArticle: function(pageobj) {
let text = pageobj.getPageText();
const params = pageobj.getCallbackParameters();
const statelem = pageobj.getStatusElement();
if (!pageobj.exists()) {
statelem.error("It seems that the page doesn't exist; perhaps it has already been deleted");
return;
}
// Check for existing AfD tag, for the benefit of new page patrollers
const textNoAfd = text.replace(/<!--.*AfD.*\n\{\{(?:Article for deletion\/dated|AfDM).*\}\}\n<!--.*(?:\n<!--.*)?AfD.*(?:\s*\n)?/g, '');
if (text !== textNoAfd) {
if (confirm('An AfD tag was found on this article. Maybe someone beat you to it. \nClick OK to replace the current AfD tag (not recommended), or Cancel to abandon your nomination.')) {
text = textNoAfd;
} else {
statelem.error('Article already tagged with AfD tag, and you chose to abort');
window.location.reload();
return;
}
}
// Now we know we want to go ahead with it, trigger the other AJAX requests
// Mark the page as curated/patrolled, if wanted
if (Twinkle.getPref('markXfdPagesAsPatrolled')) {
pageobj.triage();
}
// Start discussion page, will also handle pagetriage and delsort listings
let wikipedia_page = new Morebits.wiki.Page(params.discussionpage, 'Creating article deletion discussion page');
wikipedia_page.setCallbackParameters(params);
wikipedia_page.load(Twinkle.xfd.callbacks.afd.discussionPage);
// Today's list
const date = new Morebits.Date(pageobj.getLoadTime());
wikipedia_page = new Morebits.wiki.Page('Wikipedia:Articles for deletion/Log/' +
date.format('YYYY MMMM D', 'utc'), "Adding discussion to today's list");
wikipedia_page.setFollowRedirect(true);
wikipedia_page.setCallbackParameters(params);
wikipedia_page.load(Twinkle.xfd.callbacks.afd.todaysList);
// Notification to first contributor
if (params.notifycreator) {
const thispage = new Morebits.wiki.Page(mw.config.get('wgPageName'));
thispage.setCallbackParameters(params);
thispage.setLookupNonRedirectCreator(true); // Look for author of first non-redirect revision
thispage.lookupCreation((pageobj) => {
Twinkle.xfd.callbacks.notifyUser(pageobj.getCallbackParameters(), pageobj.getCreator());
});
// or, if not notifying, add this nomination to the user's userspace log without the initial contributor's name
} else {
Twinkle.xfd.callbacks.addToLog(params, null);
}
// Add AFD tag to article
params.tagText = Twinkle.xfd.callbacks.afd.generateArticleTagWikitext(
params.noinclude, params.outcome, params.afdtarget, params.number
);
// If the selected outcome is merge, add {{Merge from}} to the target page
if (params.outcome === 'merging' && params.afdtarget) {
wikipedia_page = new Morebits.wiki.Page(params.afdtarget, 'Tagging the target page with {{Merge from}}');
wikipedia_page.setCallbackParameters(params);
wikipedia_page.setFollowRedirect(true);
wikipedia_page.load(Twinkle.xfd.callbacks.afd.tagTargetPageWithMergeFromTag);
}
if (pageobj.canEdit()) {
// Remove some tags that should always be removed on AfD.
text = text.replace(/\{\{\s*(dated prod|dated prod blp|Prod blp\/dated|Proposed deletion\/dated|prod2|Proposed deletion endorsed|Userspace draft)\s*(\|(?:\{\{[^{}]*\}\}|[^{}])*)?\}\}\s*/ig, '');
// Then, test if there are speedy deletion-related templates on the article.
const textNoSd = text.replace(/\{\{\s*(db(-\w*)?|delete|(?:hang|hold)[- ]?on)\s*(\|(?:\{\{[^{}]*\}\}|[^{}])*)?\}\}\s*/ig, '');
if (text !== textNoSd && confirm('A speedy deletion tag was found on this page. Should it be removed?')) {
text = textNoSd;
}
// Insert tag after short description or any hatnotes
const wikipage = new Morebits.wikitext.Page(text);
text = wikipage.insertAfterTemplates(params.tagText, Twinkle.hatnoteRegex).getText();
pageobj.setPageText(text);
pageobj.setEditSummary(`Nominated for ${params.outcome}; see [[:${params.discussionpage}]].`);
pageobj.setWatchlist(Twinkle.getPref('xfdWatchPage'));
pageobj.setCreateOption('nocreate');
pageobj.save();
} else {
Twinkle.xfd.callbacks.autoEditRequest(pageobj, params);
}
},
tagTargetPageWithMergeFromTag: function(pageobj) {
const statelem = pageobj.getStatusElement();
if (!pageobj.exists()) {
statelem.warn('Failed. Target page not found.');
return;
} else if (!pageobj.canEdit()) {
statelem.warn('Failed. Target page is protected from editing.');
return;
}
const params = pageobj.getCallbackParameters();
const tag = `{{Merge from |1=${Morebits.pageNameNorm} |target=${params.afdtarget} |afd=${Morebits.pageNameNorm + params.numbering} |date ={{subst:CURRENTMONTHNAME}} {{subst:CURRENTYEAR}} }}`;
const wikipage = new Morebits.wikitext.Page(pageobj.getPageText());
const text = wikipage.insertAfterTemplates(tag, Twinkle.hatnoteRegex).getText();
pageobj.setPageText(text);
pageobj.setEditSummary('Nominated for merging; see [[:' + params.discussionpage + ']].');
pageobj.setCreateOption('nocreate');
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.save();
},
generateArticleTagWikitext: function(noinclude, outcome, afdtarget, number) {
let noIncludeStart = '';
let noIncludeEnd = '';
if (noinclude) {
noIncludeStart = '<noinclude>';
noIncludeEnd = '</noinclude>';
}
let templateAndParams = '';
const outcomeParam = outcome !== 'deletion' ? '|outcome=' + outcome : '';
const targetParam = afdtarget ? '|target=' + afdtarget : '';
const isFirstNomination = number === '';
if (isFirstNomination) {
templateAndParams = 'subst:afd|help=off' + outcomeParam + targetParam;
} else {
templateAndParams = 'subst:afdx|' + number + '|help=off' + outcomeParam + targetParam;
}
return noIncludeStart + '{{' + templateAndParams + '}}' + noIncludeEnd + '\n';
},
discussionPage: function(pageobj) {
const params = pageobj.getCallbackParameters();
pageobj.setPageText(Twinkle.xfd.callbacks.getDiscussionWikitext('afd', params));
pageobj.setEditSummary('Creating AfD discussion page for [[:' + Morebits.pageNameNorm + ']].');
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.setWatchlist(Twinkle.getPref('xfdWatchDiscussion'));
pageobj.setCreateOption('createonly');
pageobj.save(() => {
Twinkle.xfd.currentRationale = null; // any errors from now on do not need to print the rationale, as it is safely saved on-wiki
// Actions that should wait on the discussion page actually being created
// and whose errors shouldn't output the user rationale
// List at deletion sorting pages
if (params.delsortCats) {
params.delsortCats.forEach((cat) => {
const delsortPage = new Morebits.wiki.Page('Wikipedia:WikiProject Deletion sorting/' + cat, 'Adding to list of ' + cat + '-related deletion discussions');
delsortPage.setFollowRedirect(true); // In case a category gets renamed
delsortPage.setCallbackParameters({discussionPage: params.discussionpage});
delsortPage.load(Twinkle.xfd.callbacks.afd.delsortListing);
});
}
});
},
todaysList: function(pageobj) {
const params = pageobj.getCallbackParameters();
const statelem = pageobj.getStatusElement();
const added_data = '{{subst:afd3|pg=' + Morebits.pageNameNorm + params.numbering + '}}\n';
let text;
// add date header if the log is found to be empty (a bot should do this automatically)
if (!pageobj.exists()) {
text = '{{subst:AfD log}}\n' + added_data;
} else {
const old_text = pageobj.getPageText() + '\n'; // MW strips trailing blanks, but we like them, so we add a fake one
text = old_text.replace(/(<!-- Add new entries to the TOP of the following list -->\n+)/, '$1' + added_data);
if (text === old_text) {
const linknode = document.createElement('a');
linknode.setAttribute('href', mw.util.getUrl('Wikipedia:Twinkle/Fixing AFD') + '?action=purge');
linknode.appendChild(document.createTextNode('How to fix AFD'));
statelem.error(['Could not find the target spot for the discussion. To fix this problem, please see ', linknode, '.']);
return;
}
}
pageobj.setPageText(text);
pageobj.setEditSummary('Adding [[:' + params.discussionpage + ']].');
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.setWatchlist(Twinkle.getPref('xfdWatchList'));
pageobj.setCreateOption('recreate');
pageobj.setDiscussionToolsAutoSubscribe(false);
pageobj.save();
},
delsortListing: function(pageobj) {
const discussionPage = pageobj.getCallbackParameters().discussionPage;
const text = pageobj.getPageText().replace('directly below this line -->', 'directly below this line -->\n{{' + discussionPage + '}}');
pageobj.setPageText(text);
pageobj.setEditSummary('Listing [[:' + discussionPage + ']].');
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.setCreateOption('nocreate');
pageobj.setDiscussionToolsAutoSubscribe(false);
pageobj.save();
}
},
tfd: {
main: function(pageobj) {
const params = pageobj.getCallbackParameters();
const date = new Morebits.Date(pageobj.getLoadTime());
params.logpage = 'Wikipedia:Templates for discussion/Log/' + date.format('YYYY MMMM D', 'utc');
params.discussionpage = params.logpage + '#' + Morebits.pageNameNorm;
// Add log/discussion page params to the already-loaded page object
pageobj.setCallbackParameters(params);
// Defined here rather than below to reduce duplication
let watchModule, watch_query;
if (params.scribunto) {
const watchPref = Twinkle.getPref('xfdWatchPage');
// action=watch has no way to rely on user
// preferences (T262912), so we do it manually.
// The watchdefault pref appears to reliably return '1' (string),
// but that's not consistent among prefs so might as well be "correct"
watchModule = watchPref !== 'no' && (watchPref !== 'default' || !!parseInt(mw.user.options.get('watchdefault'), 10));
if (watchModule) {
watch_query = {
action: 'watch',
titles: [mw.config.get('wgPageName')],
token: mw.user.tokens.get('watchToken')
};
// Only add the expiry if page is unwatched or already temporarily watched
if (pageobj.getWatched() !== true && watchPref !== 'default' && watchPref !== 'yes') {
watch_query.expiry = watchPref;
}
}
}
// Tagging template(s)/module(s)
if (params.xfdcat === 'tfm') { // Merge
let wikipedia_otherpage;
if (params.scribunto) {
wikipedia_otherpage = new Morebits.wiki.Page(params.otherTemplateName + '/doc', 'Tagging other module documentation with merge tag');
// Watch tagged module pages as well
if (watchModule) {
watch_query.titles.push(params.otherTemplateName);
new Morebits.wiki.Api('Adding Modules to watchlist', watch_query).post();
}
} else {
wikipedia_otherpage = new Morebits.wiki.Page(params.otherTemplateName, 'Tagging other template with merge tag');
}
// Tag this template/module
Twinkle.xfd.callbacks.tfd.taggingTemplateForMerge(pageobj);
// Tag other template/module
wikipedia_otherpage.setFollowRedirect(true);
const otherParams = $.extend({}, params);
otherParams.otherTemplateName = Morebits.pageNameNorm;
wikipedia_otherpage.setCallbackParameters(otherParams);
wikipedia_otherpage.load(Twinkle.xfd.callbacks.tfd.taggingTemplateForMerge);
} else { // delete
if (params.scribunto && Twinkle.getPref('xfdWatchPage') !== 'no') {
// Watch tagged module page as well
if (watchModule) {
new Morebits.wiki.Api('Adding Module to watchlist', watch_query).post();
}
}
Twinkle.xfd.callbacks.tfd.taggingTemplate(pageobj);
}
// Updating data for the action completed event
Morebits.wiki.actionCompleted.redirect = params.logpage;
Morebits.wiki.actionCompleted.notice = "Nomination completed, now redirecting to today's log";
// Adding discussion
const wikipedia_page = new Morebits.wiki.Page(params.logpage, "Adding discussion to today's log");
wikipedia_page.setFollowRedirect(true);
wikipedia_page.setCallbackParameters(params);
wikipedia_page.load(Twinkle.xfd.callbacks.tfd.todaysList);
// Notification to first contributors
if (params.notifycreator) {
const involvedpages = [];
const seenusers = [];
involvedpages.push(new Morebits.wiki.Page(mw.config.get('wgPageName')));
if (params.xfdcat === 'tfm') {
if (params.scribunto) {
involvedpages.push(new Morebits.wiki.Page('Module:' + params.tfdtarget));
} else {
involvedpages.push(new Morebits.wiki.Page('Template:' + params.tfdtarget));
}
}
involvedpages.forEach((page) => {
page.setCallbackParameters(params);
page.lookupCreation((innerpage) => {
const username = innerpage.getCreator();
if (!seenusers.includes(username)) {
seenusers.push(username);
// Only log once on merge nominations, for the initial template
Twinkle.xfd.callbacks.notifyUser(innerpage.getCallbackParameters(), username,
params.xfdcat === 'tfm' && innerpage.getPageName() !== Morebits.pageNameNorm);
}
});
});
// or, if not notifying, add this nomination to the user's userspace log without the initial contributor's name
} else {
Twinkle.xfd.callbacks.addToLog(params, null);
}
// Notify developer(s) of script(s) that use(s) the nominated template
if (params.devpages) {
const inCategories = mw.config.get('wgCategories');
const categoryNotificationPageMap = {
'Templates used by Twinkle': 'Wikipedia talk:Twinkle',
'Templates used by AutoWikiBrowser': 'Wikipedia talk:AutoWikiBrowser',
'Templates used by Ultraviolet': 'Wikipedia talk:Ultraviolet'
};
$.each(categoryNotificationPageMap, (category, page) => {
if (inCategories.includes(category)) {
Twinkle.xfd.callbacks.notifyUser(params, page, true, 'Notifying ' + page + ' of template nomination');
}
});
}
},
taggingTemplate: function(pageobj) {
const text = pageobj.getPageText();
const params = pageobj.getCallbackParameters();
params.tagText = '{{subst:template for discussion|help=off' + (params.templatetype !== 'standard' ? '|type=' + params.templatetype : '') + '}}';
if (pageobj.getContentModel() === 'sanitized-css') {
params.tagText = '/* ' + params.tagText + ' */';
} else {
if (params.noinclude) {
params.tagText = '<noinclude>' + params.tagText + '</noinclude>';
}
params.tagText += params.templatetype === 'standard' || params.templatetype === 'sidebar' || params.templatetype === 'disabled' ? '\n' : ''; // No newline for inline
}
if (pageobj.canEdit() && ['wikitext', 'sanitized-css'].includes(pageobj.getContentModel())) {
pageobj.setPageText(params.tagText + text);
pageobj.setEditSummary('Nominated for deletion; see [[:' + params.discussionpage + ']].');
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.setWatchlist(Twinkle.getPref('xfdWatchPage'));
if (params.scribunto) {
pageobj.setCreateOption('recreate'); // Module /doc might not exist
}
pageobj.save();
} else {
Twinkle.xfd.callbacks.autoEditRequest(pageobj, params);
}
},
taggingTemplateForMerge: function(pageobj) {
const text = pageobj.getPageText();
const params = pageobj.getCallbackParameters();
params.tagText = '{{subst:tfm|help=off|' + (params.templatetype !== 'standard' ? 'type=' + params.templatetype + '|' : '') +
'1=' + params.otherTemplateName.replace(new RegExp('^' + Morebits.namespaceRegex([10, 828]) + ':'), '') + '}}';
if (pageobj.getContentModel() === 'sanitized-css') {
params.tagText = '/* ' + params.tagText + ' */';
} else {
if (params.noinclude) {
params.tagText = '<noinclude>' + params.tagText + '</noinclude>';
}
params.tagText += params.templatetype === 'standard' || params.templatetype === 'sidebar' || params.templatetype === 'disabled' ? '\n' : ''; // No newline for inline
}
if (pageobj.canEdit() && ['wikitext', 'sanitized-css'].includes(pageobj.getContentModel())) {
pageobj.setPageText(params.tagText + text);
pageobj.setEditSummary('Listed for merging with [[:' + params.otherTemplateName + ']]; see [[:' + params.discussionpage + ']].');
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.setWatchlist(Twinkle.getPref('xfdWatchPage'));
if (params.scribunto) {
pageobj.setCreateOption('recreate'); // Module /doc might not exist
}
pageobj.save();
} else {
Twinkle.xfd.callbacks.autoEditRequest(pageobj, params);
}
},
todaysList: function(pageobj) {
const params = pageobj.getCallbackParameters();
const statelem = pageobj.getStatusElement();
const added_data = Twinkle.xfd.callbacks.getDiscussionWikitext(params.xfdcat, params);
let text;
// add date header if the log is found to be empty (a bot should do this automatically)
if (!pageobj.exists()) {
text = '{{subst:TfD log}}\n' + added_data;
} else {
const old_text = pageobj.getPageText();
text = old_text.replace('-->', '-->\n' + added_data);
if (text === old_text) {
statelem.error('failed to find target spot for the discussion');
return;
}
}
pageobj.setPageText(text);
pageobj.setEditSummary('/* ' + Morebits.pageNameNorm + ' */ Adding ' + (params.xfdcat === 'tfd' ? 'deletion nomination' : 'merge listing') + ' of [[:' + Morebits.pageNameNorm + ']].');
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.setWatchlist(Twinkle.getPref('xfdWatchDiscussion'));
pageobj.setCreateOption('recreate');
pageobj.save(() => {
Twinkle.xfd.currentRationale = null; // any errors from now on do not need to print the rationale, as it is safely saved on-wiki
});
}
},
mfd: {
main: function(apiobj) {
const response = apiobj.getResponse();
const titles = response.query.allpages;
// There has been no earlier entries with this prefix, just go on.
if (titles.length <= 0) {
apiobj.params.numbering = apiobj.params.number = '';
} else {
let number = 0;
for (let i = 0; i < titles.length; ++i) {
const title = titles[i].title;
// First, simple test, is there an instance with this exact name?
if (title === 'Wikipedia:Miscellany for deletion/' + Morebits.pageNameNorm) {
number = Math.max(number, 1);
continue;
}
const order_re = new RegExp('^' +
Morebits.string.escapeRegExp('Wikipedia:Miscellany for deletion/' + Morebits.pageNameNorm) +
'\\s*\\(\\s*(\\d+)(?:(?:th|nd|rd|st) nom(?:ination)?)?\\s*\\)\\s*$');
const match = order_re.exec(title);
// No match; A non-good value
if (!match) {
continue;
}
// A match, set number to the max of current
number = Math.max(number, Number(match[1]));
}
apiobj.params.number = utils.num2order(parseInt(number, 10) + 1);
apiobj.params.numbering = number > 0 ? ' (' + apiobj.params.number + ' nomination)' : '';
}
apiobj.params.discussionpage = 'Wikipedia:Miscellany for deletion/' + Morebits.pageNameNorm + apiobj.params.numbering;
apiobj.statelem.info('next in order is [[' + apiobj.params.discussionpage + ']]');
let wikipedia_page;
// Tagging page
if (mw.config.get('wgNamespaceNumber') !== 710) { // cannot tag TimedText pages
wikipedia_page = new Morebits.wiki.Page(mw.config.get('wgPageName'), 'Tagging page with deletion tag');
wikipedia_page.setFollowRedirect(true); // should never be needed, but if the page is moved, we would want to follow the redirect
wikipedia_page.setCallbackParameters(apiobj.params);
wikipedia_page.load(Twinkle.xfd.callbacks.mfd.taggingPage);
}
// Updating data for the action completed event
Morebits.wiki.actionCompleted.redirect = apiobj.params.discussionpage;
Morebits.wiki.actionCompleted.notice = 'Nomination completed, now redirecting to the discussion page';
// Discussion page
wikipedia_page = new Morebits.wiki.Page(apiobj.params.discussionpage, 'Creating deletion discussion page');
wikipedia_page.setCallbackParameters(apiobj.params);
wikipedia_page.load(Twinkle.xfd.callbacks.mfd.discussionPage);
// Today's list
wikipedia_page = new Morebits.wiki.Page('Wikipedia:Miscellany for deletion', "Adding discussion to today's list");
wikipedia_page.setPageSection(2);
wikipedia_page.setFollowRedirect(true);
wikipedia_page.setCallbackParameters(apiobj.params);
wikipedia_page.load(Twinkle.xfd.callbacks.mfd.todaysList);
// Notification to first contributor and/or notification to owner of userspace
if (apiobj.params.notifycreator || apiobj.params.notifyuserspace) {
const thispage = new Morebits.wiki.Page(mw.config.get('wgPageName'));
thispage.setCallbackParameters(apiobj.params);
thispage.lookupCreation(Twinkle.xfd.callbacks.mfd.sendNotifications);
// or, if not notifying, add this nomination to the user's userspace log without the initial contributor's name
} else {
Twinkle.xfd.callbacks.addToLog(apiobj.params, null);
}
},
taggingPage: function(pageobj) {
const text = pageobj.getPageText();
const params = pageobj.getCallbackParameters();
params.tagText = '{{' + (params.number === '' ? 'mfd' : 'mfdx|' + params.number) + '|help=off}}';
if (['javascript', 'css', 'sanitized-css'].includes(mw.config.get('wgPageContentModel'))) {
params.tagText = '/* ' + params.tagText + ' */\n';
} else {
params.tagText += '\n';
if (params.noinclude) {
params.tagText = '<noinclude>' + params.tagText + '</noinclude>';
}
}
if (pageobj.canEdit() && ['wikitext', 'javascript', 'css', 'sanitized-css'].includes(pageobj.getContentModel())) {
pageobj.setPageText(params.tagText + text);
pageobj.setEditSummary('Nominated for deletion; see [[:' + params.discussionpage + ']].');
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.setWatchlist(Twinkle.getPref('xfdWatchPage'));
pageobj.setCreateOption('nocreate');
pageobj.save();
} else {
Twinkle.xfd.callbacks.autoEditRequest(pageobj, params);
}
},
discussionPage: function(pageobj) {
const params = pageobj.getCallbackParameters();
pageobj.setPageText(Twinkle.xfd.callbacks.getDiscussionWikitext('mfd', params));
pageobj.setEditSummary('Creating deletion discussion page for [[:' + Morebits.pageNameNorm + ']].');
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.setWatchlist(Twinkle.getPref('xfdWatchDiscussion'));
pageobj.setCreateOption('createonly');
pageobj.save(() => {
Twinkle.xfd.currentRationale = null; // any errors from now on do not need to print the rationale, as it is safely saved on-wiki
});
},
todaysList: function(pageobj) {
let text = pageobj.getPageText();
const params = pageobj.getCallbackParameters();
const statelem = pageobj.getStatusElement();
const date = new Morebits.Date(pageobj.getLoadTime());
const date_header = date.format('===MMMM D, YYYY===\n', 'utc');
const date_header_regex = new RegExp(date.format('(===[\\s]*MMMM[\\s]+D,[\\s]+YYYY[\\s]*===)', 'utc'));
const added_data = '{{subst:mfd3|pg=' + Morebits.pageNameNorm + params.numbering + '}}';
if (date_header_regex.test(text)) { // we have a section already
statelem.info('Found today\'s section, proceeding to add new entry');
text = text.replace(date_header_regex, '$1\n' + added_data);
} else { // we need to create a new section
statelem.info('No section for today found, proceeding to create one');
text = text.replace('===', date_header + added_data + '\n\n===');
}
pageobj.setPageText(text);
pageobj.setEditSummary('Adding [[:' + params.discussionpage + ']].');
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.setWatchlist(Twinkle.getPref('xfdWatchList'));
pageobj.setCreateOption('recreate');
pageobj.setDiscussionToolsAutoSubscribe(false);
pageobj.save();
},
sendNotifications: function(pageobj) {
const initialContrib = pageobj.getCreator();
const params = pageobj.getCallbackParameters();
// Notify the creator
if (params.notifycreator) {
Twinkle.xfd.callbacks.notifyUser(params, initialContrib);
}
// Notify the user who owns the subpage if they are not the creator
params.userspaceOwner = mw.config.get('wgRelevantUserName');
if (params.notifyuserspace) {
if (params.userspaceOwner !== initialContrib) {
// Don't log if notifying creator above, will log then
Twinkle.xfd.callbacks.notifyUser(params, params.userspaceOwner, params.notifycreator, 'Notifying owner of userspace (' + params.userspaceOwner + ')');
} else if (!params.notifycreator) {
// If we thought we would notify the owner but didn't,
// then we need to log if we didn't notify the creator
// Twinkle.xfd.callbacks.addToLog(params, null);
Twinkle.xfd.callbacks.addToLog(params, initialContrib);
}
}
}
},
ffd: {
taggingImage: function(pageobj) {
let text = pageobj.getPageText();
const params = pageobj.getCallbackParameters();
const date = new Morebits.Date(pageobj.getLoadTime()).format('YYYY MMMM D', 'utc');
params.logpage = 'Wikipedia:Files for discussion/' + date;
params.discussionpage = params.logpage + '#' + Morebits.pageNameNorm;
params.tagText = '{{ffd|log=' + date + '|help=off}}\n';
if (pageobj.canEdit()) {
text = Twinkle.removeMoveToCommonsTagsFromWikicode(text);
pageobj.setPageText(params.tagText + text);
pageobj.setEditSummary('Listed for discussion at [[:' + params.discussionpage + ']].');
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.setWatchlist(Twinkle.getPref('xfdWatchPage'));
pageobj.setCreateOption('recreate'); // it might be possible for a file to exist without a description page
pageobj.save();
} else {
Twinkle.xfd.callbacks.autoEditRequest(pageobj, params);
}
// Updating data for the action completed event
Morebits.wiki.actionCompleted.redirect = params.logpage;
Morebits.wiki.actionCompleted.notice = 'Nomination completed, now redirecting to the discussion page';
// Contributor specific edits
const wikipedia_page = new Morebits.wiki.Page(mw.config.get('wgPageName'));
wikipedia_page.setCallbackParameters(params);
wikipedia_page.lookupCreation(Twinkle.xfd.callbacks.ffd.main);
},
main: function(pageobj) {
// this is coming in from lookupCreation...!
const params = pageobj.getCallbackParameters();
const initialContrib = pageobj.getCreator();
params.uploader = initialContrib;
// Adding discussion
const wikipedia_page = new Morebits.wiki.Page(params.logpage, "Adding discussion to today's list");
wikipedia_page.setFollowRedirect(true);
wikipedia_page.setCallbackParameters(params);
wikipedia_page.load(Twinkle.xfd.callbacks.ffd.todaysList);
// Notification to first contributor
if (params.notifycreator) {
Twinkle.xfd.callbacks.notifyUser(params, initialContrib);
// or, if not notifying, add this nomination to the user's userspace log without the initial contributor's name
} else {
Twinkle.xfd.callbacks.addToLog(params, null);
}
},
todaysList: function(pageobj) {
let text = pageobj.getPageText();
const params = pageobj.getCallbackParameters();
// add date header if the log is found to be empty (a bot should do this automatically)
if (!pageobj.exists()) {
text = '{{subst:FfD log}}';
}
pageobj.setPageText(text + '\n\n' + Twinkle.xfd.callbacks.getDiscussionWikitext('ffd', params));
pageobj.setEditSummary('Adding [[:' + Morebits.pageNameNorm + ']].');
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.setWatchlist(Twinkle.getPref('xfdWatchDiscussion'));
pageobj.setCreateOption('recreate');
pageobj.save(() => {
Twinkle.xfd.currentRationale = null; // any errors from now on do not need to print the rationale, as it is safely saved on-wiki
});
}
},
cfd: {
main: function(pageobj) {
const params = pageobj.getCallbackParameters();
const date = new Morebits.Date(pageobj.getLoadTime());
params.logpage = 'Wikipedia:Categories for discussion/Log/' + date.format('YYYY MMMM D', 'utc');
params.discussionpage = params.logpage + '#' + Morebits.pageNameNorm;
// Add log/discussion page params to the already-loaded page object
pageobj.setCallbackParameters(params);
// Tagging category
Twinkle.xfd.callbacks.cfd.taggingCategory(pageobj);
// Updating data for the action completed event
Morebits.wiki.actionCompleted.redirect = params.logpage;
Morebits.wiki.actionCompleted.notice = "Nomination completed, now redirecting to today's log";
// Adding discussion to list
let wikipedia_page = new Morebits.wiki.Page(params.logpage, "Adding discussion to today's list");
wikipedia_page.setFollowRedirect(true);
wikipedia_page.setCallbackParameters(params);
wikipedia_page.load(Twinkle.xfd.callbacks.cfd.todaysList);
// Notification to first contributor
if (params.notifycreator) {
wikipedia_page = new Morebits.wiki.Page(mw.config.get('wgPageName'));
wikipedia_page.setCallbackParameters(params);
wikipedia_page.lookupCreation((pageobj) => {
Twinkle.xfd.callbacks.notifyUser(pageobj.getCallbackParameters(), pageobj.getCreator());
});
// or, if not notifying, add this nomination to the user's userspace log without the initial contributor's name
} else {
Twinkle.xfd.callbacks.addToLog(params, null);
}
},
taggingCategory: function(pageobj) {
const text = pageobj.getPageText();
const params = pageobj.getCallbackParameters();
params.tagText = '{{subst:' + params.xfdcat;
let editsummary = (mw.config.get('wgNamespaceNumber') === 14 ? 'Category' : 'Stub template') +
' being considered for ' + params.action;
switch (params.xfdcat) {
case 'cfd':
case 'sfd-t':
break;
case 'cfc':
editsummary += ' to an article';
// falls through
case 'cfm':
case 'cfr':
case 'sfr-t':
params.tagText += '|' + params.cfdtarget;
break;
case 'cfs':
params.tagText += '|' + params.cfdtarget + '|' + params.cfdtarget2;
break;
default:
alert('twinklexfd in taggingCategory(): unknown CFD action');
break;
}
params.tagText += '}}\n';
editsummary += '; see [[:' + params.discussionpage + ']].';
if (pageobj.canEdit()) {
pageobj.setPageText(params.tagText + text);
pageobj.setEditSummary(editsummary);
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.setWatchlist(Twinkle.getPref('xfdWatchPage'));
pageobj.setCreateOption('recreate'); // since categories can be populated without an actual page at that title
pageobj.save();
} else {
Twinkle.xfd.callbacks.autoEditRequest(pageobj, params);
}
},
todaysList: function(pageobj) {
const params = pageobj.getCallbackParameters();
const statelem = pageobj.getStatusElement();
const added_data = Twinkle.xfd.callbacks.getDiscussionWikitext(params.xfdcat, params);
let text;
// add date header if the log is found to be empty (a bot should do this automatically)
if (!pageobj.exists()) {
text = '{{subst:CfD log}}\n' + added_data;
} else {
const old_text = pageobj.getPageText();
text = old_text.replace('below this line -->', 'below this line -->\n' + added_data);
if (text === old_text) {
statelem.error('failed to find target spot for the discussion');
return;
}
}
pageobj.setPageText(text);
pageobj.setEditSummary('Adding ' + params.action + ' nomination of [[:' + Morebits.pageNameNorm + ']].');
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.setWatchlist(Twinkle.getPref('xfdWatchDiscussion'));
pageobj.setCreateOption('recreate');
pageobj.save(() => {
Twinkle.xfd.currentRationale = null; // any errors from now on do not need to print the rationale, as it is safely saved on-wiki
});
}
},
cfds: {
taggingCategory: function(pageobj) {
const text = pageobj.getPageText();
const params = pageobj.getCallbackParameters();
if (params.xfdcat === 'C2F') {
params.tagText = '{{subst:cfm-speedy|1=' + params.cfdstarget.replace(/^:?Category:/, '') + '}}\n';
} else {
params.tagText = '{{subst:cfr-speedy|1=' + params.cfdstarget.replace(/^:?Category:/, '') + '}}\n';
}
params.discussionpage = ''; // CFDS is just a bullet in a bulleted list. There's no section to link to, so we set this to blank. Blank will be recognized by both the generate userspace log code and the generate userspace log edit summary code as "don't wikilink to a section".
if (pageobj.canEdit()) {
pageobj.setPageText(params.tagText + text);
pageobj.setEditSummary('Listed for speedy renaming; see [[WP:CFDS|Categories for discussion/Speedy]].');
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.setWatchlist(Twinkle.getPref('xfdWatchPage'));
pageobj.setCreateOption('recreate'); // since categories can be populated without an actual page at that title
pageobj.save(() => {
// No user notification for CfDS, so just add this nomination to the user's userspace log
Twinkle.xfd.callbacks.addToLog(params, null);
});
} else {
Twinkle.xfd.callbacks.autoEditRequest(pageobj, params);
// No user notification for CfDS, so just add this nomination to the user's userspace log
Twinkle.xfd.callbacks.addToLog(params, null);
}
},
addToList: function(pageobj) {
const old_text = pageobj.getPageText();
const params = pageobj.getCallbackParameters();
const statelem = pageobj.getStatusElement();
const text = old_text.replace('BELOW THIS LINE -->', 'BELOW THIS LINE -->\n' + Twinkle.xfd.callbacks.getDiscussionWikitext('cfds', params));
if (text === old_text) {
statelem.error('failed to find target spot for the discussion');
return;
}
pageobj.setPageText(text);
pageobj.setEditSummary('Adding [[:' + Morebits.pageNameNorm + ']].');
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.setWatchlist(Twinkle.getPref('xfdWatchDiscussion'));
pageobj.setCreateOption('recreate');
pageobj.setDiscussionToolsAutoSubscribe(false);
pageobj.save(() => {
Twinkle.xfd.currentRationale = null; // any errors from now on do not need to print the rationale, as it is safely saved on-wiki
});
}
},
rfd: {
// This gets called both on submit and preview to determine the redirect target
findTarget: function(params, callback) {
// Used by regular redirects to find the target, but for all redirects,
// avoid relying on the client clock to build the log page
const query = {
action: 'query',
curtimestamp: true,
format: 'json'
};
if (document.getElementById('softredirect')) {
// For soft redirects, define the target early
// to skip target checks in findTargetCallback
params.rfdtarget = document.getElementById('softredirect').textContent.replace(/^:+/, '');
} else {
// Find current target of redirect
query.titles = mw.config.get('wgPageName');
query.redirects = true;
}
const wikipedia_api = new Morebits.wiki.Api('Finding target of redirect', query, Twinkle.xfd.callbacks.rfd.findTargetCallback(callback));
wikipedia_api.params = params;
wikipedia_api.post();
},
// This is a closure for the callback from the above API request, which gets the target of the redirect
findTargetCallback: function(callback) {
return function(apiobj) {
const response = apiobj.getResponse();
apiobj.params.curtimestamp = response.curtimestamp;
if (!apiobj.params.rfdtarget) { // Not a softredirect
const target = response.query.redirects && response.query.redirects[0].to;
if (!target) {
let message = 'No target found. this page does not appear to be a redirect, aborting';
if (mw.config.get('wgAction') === 'history') {
message += '. If this is a soft redirect, try again from the content page, not the page history.';
}
apiobj.statelem.error(message);
return;
}
apiobj.params.rfdtarget = target;
const section = response.query.redirects[0].tofragment;
apiobj.params.section = section;
}
callback(apiobj.params);
};
},
main: function(params) {
const date = new Morebits.Date(params.curtimestamp);
params.logpage = 'Wikipedia:Redirects for discussion/Log/' + date.format('YYYY MMMM D', 'utc');
params.discussionpage = params.logpage + '#' + Morebits.pageNameNorm;
// Tagging redirect
let wikipedia_page = new Morebits.wiki.Page(mw.config.get('wgPageName'), 'Adding deletion tag to redirect');
wikipedia_page.setFollowRedirect(false);
wikipedia_page.setCallbackParameters(params);
wikipedia_page.load(Twinkle.xfd.callbacks.rfd.taggingRedirect);
// Updating data for the action completed event
Morebits.wiki.actionCompleted.redirect = params.logpage;
Morebits.wiki.actionCompleted.notice = "Nomination completed, now redirecting to today's log";
// Adding discussion
wikipedia_page = new Morebits.wiki.Page(params.logpage, "Adding discussion to today's log");
wikipedia_page.setFollowRedirect(true);
wikipedia_page.setCallbackParameters(params);
wikipedia_page.load(Twinkle.xfd.callbacks.rfd.todaysList);
// Notifications
if (params.notifycreator || params.relatedpage) {
const thispage = new Morebits.wiki.Page(mw.config.get('wgPageName'));
thispage.setCallbackParameters(params);
thispage.lookupCreation(Twinkle.xfd.callbacks.rfd.sendNotifications);
// or, if not notifying, add this nomination to the user's userspace log without the initial contributor's name
} else {
Twinkle.xfd.callbacks.addToLog(params, null);
}
},
taggingRedirect: function(pageobj) {
const text = pageobj.getPageText();
const params = pageobj.getCallbackParameters();
// Imperfect for edit request but so be it
params.tagText = '{{subst:rfd|' + (mw.config.get('wgNamespaceNumber') === 10 ? 'showontransclusion=1|' : '') + 'content=\n';
if (pageobj.canEdit()) {
pageobj.setPageText(params.tagText + text + '\n}}');
pageobj.setEditSummary('Listed for discussion at [[:' + params.discussionpage + ']].');
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.setWatchlist(Twinkle.getPref('xfdWatchPage'));
pageobj.setCreateOption('nocreate');
pageobj.save();
} else {
Twinkle.xfd.callbacks.autoEditRequest(pageobj, params);
}
},
todaysList: function(pageobj) {
const params = pageobj.getCallbackParameters();
const statelem = pageobj.getStatusElement();
const added_data = Twinkle.xfd.callbacks.getDiscussionWikitext('rfd', params);
let text;
// add date header if the log is found to be empty (a bot should do this automatically)
if (!pageobj.exists()) {
text = '{{subst:RfD log}}' + added_data;
} else {
const old_text = pageobj.getPageText();
text = old_text.replace(/(<!-- Add new entries directly below this line\.? -->)/, '$1\n' + added_data);
if (text === old_text) {
statelem.error('failed to find target spot for the discussion');
return;
}
}
pageobj.setPageText(text);
pageobj.setEditSummary('Adding [[:' + Morebits.pageNameNorm + ']].');
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.setWatchlist(Twinkle.getPref('xfdWatchDiscussion'));
pageobj.setCreateOption('recreate');
pageobj.save(() => {
Twinkle.xfd.currentRationale = null; // any errors from now on do not need to print the rationale, as it is safely saved on-wiki
});
},
sendNotifications: function(pageobj) {
const initialContrib = pageobj.getCreator();
const params = pageobj.getCallbackParameters();
const statelem = pageobj.getStatusElement();
// Notifying initial contributor
if (params.notifycreator) {
Twinkle.xfd.callbacks.notifyUser(params, initialContrib);
}
// Notifying target page's watchers, if not a soft redirect
if (params.relatedpage) {
const targetTalk = new mw.Title(params.rfdtarget).getTalkPage();
// On the offchance it's a circular redirect
if (params.rfdtarget === mw.config.get('wgPageName')) {
statelem.warn('Circular redirect; skipping target page notification');
} else if (document.getElementById('softredirect')) {
statelem.warn('Soft redirect; skipping target page notification');
// Don't issue if target talk is the initial contributor's talk or your own
} else if (targetTalk.getNamespaceId() === 3 && targetTalk.getMainText() === initialContrib) {
statelem.warn('Target is initial contributor; skipping target page notification');
} else if (targetTalk.getNamespaceId() === 3 && targetTalk.getMainText() === mw.config.get('wgUserName')) {
statelem.warn('You (' + mw.config.get('wgUserName') + ') are the target; skipping target page notification');
} else {
// Don't log if notifying creator above, will log then
Twinkle.xfd.callbacks.notifyUser(params, targetTalk.toText(), params.notifycreator, 'Notifying redirect target of the discussion');
return;
}
// If we thought we would notify the target but didn't,
// we need to log if we didn't notify the creator
if (!params.notifycreator) {
Twinkle.xfd.callbacks.addToLog(params, null);
}
}
}
},
rm: {
listAtTalk: function(pageobj) {
const params = pageobj.getCallbackParameters();
params.discussionpage = pageobj.getPageName();
pageobj.setAppendText('\n\n' + Twinkle.xfd.callbacks.getDiscussionWikitext('rm', params));
pageobj.setEditSummary(`Proposing move of ${
params.currentname
.map((currentname, i) => `[[:${currentname}]]${params.newname[i] ? ` to [[:${params.newname[i]}]]` : ''}`)
.join(', ')
}.`);
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.setCreateOption('recreate'); // since the talk page need not exist
pageobj.setWatchlist(Twinkle.getPref('xfdWatchDiscussion'));
pageobj.append(() => {
Twinkle.xfd.currentRationale = null; // any errors from now on do not need to print the rationale, as it is safely saved on-wiki
// add this nomination to the user's userspace log
Twinkle.xfd.callbacks.addToLog(params, null);
});
},
listAtRMTR: function(pageobj) {
const text = pageobj.getPageText();
const params = pageobj.getCallbackParameters();
const statelem = pageobj.getStatusElement();
const discussionWikitext = Twinkle.xfd.callbacks.getDiscussionWikitext('rm', params);
const newtext = Twinkle.xfd.insertRMTR(text, discussionWikitext);
if (text === newtext) {
statelem.error('failed to find target spot for the entry');
return;
}
pageobj.setPageText(newtext);
pageobj.setEditSummary(`Adding [[:${params.currentname.join(']], [[:')}]].`);
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.save(() => {
Twinkle.xfd.currentRationale = null; // any errors from now on do not need to print the rationale, as it is safely saved on-wiki
// add this nomination to the user's userspace log
Twinkle.xfd.callbacks.addToLog(params, null);
});
}
}
};
/**
* Given the wikitext of the WP:RM/TR page and the wikitext to insert, insert it at the bottom of the ==== Uncontroversial technical requests ==== section.
*
* @param {string} pageWikitext
* @param {string} wikitextToInsert Will typically be `{{subst:RMassist|1=From|2=To|reason=Reason}}`, which expands out to `* {{RMassist/core | 1 = From | 2 = To | discuss = yes | reason = Reason | sig = Signature | requester = YourUserName}}`
* @return {string} pageWikitext
*/
Twinkle.xfd.insertRMTR = function(pageWikitext, wikitextToInsert) {
// [^\n]* is for matching anchors, e.g. <span class="anchor" id="*"></span>
const placementRE = /\n{1,}(====[^\n]*Requests to revert undiscussed moves ?====)/i;
return pageWikitext.replace(placementRE, '\n' + wikitextToInsert + '\n\n$1');
};
Twinkle.xfd.callback.evaluate = function(e) {
const form = e.target;
const params = Morebits.QuickForm.getInputData(form);
Morebits.SimpleWindow.setButtonsEnabled(false);
Morebits.Status.init(form);
Twinkle.xfd.currentRationale = params.reason;
Morebits.Status.onError(Twinkle.xfd.printRationale);
let query, wikipedia_page, wikipedia_api;
switch (params.venue) {
case 'afd': // AFD
query = {
action: 'query',
list: 'allpages',
apprefix: 'Articles for deletion/' + Morebits.pageNameNorm,
apnamespace: 4,
apfilterredir: 'nonredirects',
aplimit: 'max', // 500 is max for normal users, 5000 for bots and sysops
format: 'json'
};
wikipedia_api = new Morebits.wiki.Api('Tagging article with deletion tag', query, Twinkle.xfd.callbacks.afd.main);
wikipedia_api.params = params;
wikipedia_api.post();
break;
case 'tfd': // TFD
if (params.tfdtarget) { // remove namespace name
params.tfdtarget = utils.stripNs(params.tfdtarget);
}
// Modules can't be tagged, TfD instructions are to place on /doc subpage
params.scribunto = mw.config.get('wgPageContentModel') === 'Scribunto';
if (params.xfdcat === 'tfm') { // Merge
// Tag this template/module
if (params.scribunto) {
wikipedia_page = new Morebits.wiki.Page(mw.config.get('wgPageName') + '/doc', 'Tagging this module documentation with merge tag');
params.otherTemplateName = 'Module:' + params.tfdtarget;
} else {
wikipedia_page = new Morebits.wiki.Page(mw.config.get('wgPageName'), 'Tagging this template with merge tag');
params.otherTemplateName = 'Template:' + params.tfdtarget;
}
} else { // delete
if (params.scribunto) {
wikipedia_page = new Morebits.wiki.Page(mw.config.get('wgPageName') + '/doc', 'Tagging module documentation with deletion tag');
} else {
wikipedia_page = new Morebits.wiki.Page(mw.config.get('wgPageName'), 'Tagging template with deletion tag');
}
}
wikipedia_page.setFollowRedirect(true); // should never be needed, but if the page is moved, we would want to follow the redirect
wikipedia_page.setCallbackParameters(params);
wikipedia_page.load(Twinkle.xfd.callbacks.tfd.main);
break;
case 'mfd': // MFD
query = {
action: 'query',
list: 'allpages',
apprefix: 'Miscellany for deletion/' + Morebits.pageNameNorm,
apnamespace: 4,
apfilterredir: 'nonredirects',
aplimit: 'max', // 500 is max for normal users, 5000 for bots and sysops
format: 'json'
};
wikipedia_api = new Morebits.wiki.Api('Looking for prior nominations of this page', query, Twinkle.xfd.callbacks.mfd.main);
wikipedia_api.params = params;
wikipedia_api.post();
break;
case 'ffd': // FFD
// Tagging file
// A little out of order with this coming before 'main',
// but tagging doesn't need the uploader parameter,
// while everything else does, so tag then get the uploader
wikipedia_page = new Morebits.wiki.Page(mw.config.get('wgPageName'), 'Adding deletion tag to file page');
wikipedia_page.setFollowRedirect(true);
wikipedia_page.setCallbackParameters(params);
wikipedia_page.load(Twinkle.xfd.callbacks.ffd.taggingImage);
break;
case 'cfd':
if (params.cfdtarget) {
params.cfdtarget = utils.stripNs(params.cfdtarget);
} else {
params.cfdtarget = ''; // delete
}
if (params.cfdtarget2) { // split
params.cfdtarget2 = utils.stripNs(params.cfdtarget2);
}
// Used for customized actions in edit summaries and the notification template
var summaryActions = {
cfd: 'deletion',
'sfd-t': 'deletion',
cfm: 'merging',
cfr: 'renaming',
'sfr-t': 'renaming',
cfs: 'splitting',
cfc: 'conversion'
};
params.action = summaryActions[params.xfdcat];
// Tagging category
wikipedia_page = new Morebits.wiki.Page(mw.config.get('wgPageName'), 'Tagging category with ' + params.action + ' tag');
wikipedia_page.setFollowRedirect(true); // should never be needed, but if the page is moved, we would want to follow the redirect
wikipedia_page.setCallbackParameters(params);
wikipedia_page.load(Twinkle.xfd.callbacks.cfd.main);
break;
case 'cfds':
// add namespace name if missing
params.cfdstarget = utils.addNs(params.cfdstarget, 14);
var logpage = 'Wikipedia:Categories for discussion/Speedy';
// Updating data for the action completed event
Morebits.wiki.actionCompleted.redirect = logpage;
Morebits.wiki.actionCompleted.notice = 'Nomination completed, now redirecting to the discussion page';
// Tagging category
wikipedia_page = new Morebits.wiki.Page(mw.config.get('wgPageName'), 'Tagging category with rename tag');
wikipedia_page.setFollowRedirect(true);
wikipedia_page.setCallbackParameters(params);
wikipedia_page.load(Twinkle.xfd.callbacks.cfds.taggingCategory);
// Adding discussion to list
wikipedia_page = new Morebits.wiki.Page(logpage, 'Adding discussion to the list');
wikipedia_page.setFollowRedirect(true);
wikipedia_page.setCallbackParameters(params);
wikipedia_page.load(Twinkle.xfd.callbacks.cfds.addToList);
break;
case 'rfd':
// find target and pass main as the callback
Twinkle.xfd.callbacks.rfd.findTarget(params, Twinkle.xfd.callbacks.rfd.main);
break;
case 'rm':
var nomPageName = params.rmtr ?
'Wikipedia:Requested moves/Technical requests' :
new mw.Title(Morebits.pageNameNorm).getTalkPage().toText();
Morebits.wiki.actionCompleted.redirect = nomPageName;
Morebits.wiki.actionCompleted.notice = 'Nomination completed, now redirecting to the discussion page';
wikipedia_page = new Morebits.wiki.Page(nomPageName, params.rmtr ? 'Adding entry at WP:RM/TR' : 'Adding entry on talk page');
wikipedia_page.setFollowRedirect(true);
wikipedia_page.setCallbackParameters(params);
if (params.rmtr) {
wikipedia_page.load(Twinkle.xfd.callbacks.rm.listAtRMTR);
} else {
// listAtTalk uses .append(), so no need to load the page
Twinkle.xfd.callbacks.rm.listAtTalk(wikipedia_page);
}
break;
default:
alert('twinklexfd: unknown XFD discussion venue');
break;
}
};
Twinkle.addInitCallback(Twinkle.xfd, 'xfd');
}());
// </nowiki>
mpfv6xth6ebeskgx90or2o5drjvhu7m
769573
769536
2026-10-22T02:27:48Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at d9cd3461baca43ac99ac2c24fb76a3aae6ddada9: minify
769573
javascript
text/javascript
// <nowiki>
(function(){Twinkle.xfd=function twinklexfd(){if(mw.config.get("wgNamespaceNumber")<0||!mw.config.get("wgArticleId")||mw.config.get("wgNamespaceNumber")===6&&document.getElementById("mw-sharedupload")){return}let tooltip="Start a discussion for deleting";if(mw.config.get("wgIsRedirect")){tooltip+=" or retargeting this redirect"}else{switch(mw.config.get("wgNamespaceNumber")){case 0:tooltip+=" or moving this article";break;case 10:tooltip+=" or merging this template";break;case 828:tooltip+=" or merging this module";break;case 6:tooltip+=" this file";break;case 14:tooltip+=", merging or renaming this category";break;default:tooltip+=" this page";break}}Twinkle.addPortletLink(Twinkle.xfd.callback,"XFD","tw-xfd",tooltip)};const utils={num2order:function(num){switch(num){case 1:return"";case 2:return"2nd";case 3:return"3rd";default:return num+"th"}},stripNs:function(title){const title_obj=mw.Title.newFromUserInput(title);if(!title_obj){return title}return title_obj.getMainText()},addNs:function(title,namespaceNumber){const title_obj=mw.Title.newFromUserInput(title,namespaceNumber);if(!title_obj){return title}return title_obj.toText()},toTLACase:function(venue){return venue.toString().toUpperCase().replace(/(.)F(.)(?:-.)?/,"$1f$2")}};Twinkle.xfd.currentRationale=null;Twinkle.xfd.printRationale=function twinklexfdPrintRationale(){if(Twinkle.xfd.currentRationale){Morebits.Status.printUserText(Twinkle.xfd.currentRationale,"Your deletion rationale is provided below, which you can copy and paste into a new XFD dialog if you wish to try again:");Twinkle.xfd.currentRationale=null}};Twinkle.xfd.callback=function twinklexfdCallback(){const Window=new Morebits.SimpleWindow(700,400);Window.setTitle("Start a deletion discussion (XfD)");Window.setScriptName("Twinkle");Window.addFooterLink("About deletion discussions","WP:XFD");Window.addFooterLink("XfD prefs","WP:TW/PREF#xfd");Window.addFooterLink("Twinkle help","WP:TW/DOC#xfd");Window.addFooterLink("Give feedback","WT:TW");const form=new Morebits.QuickForm(Twinkle.xfd.callback.evaluate);const categories=form.append({type:"select",name:"venue",label:"Deletion discussion venue:",tooltip:"When activated, a default choice is made, based on what namespace you are in. This default should be the most appropriate.",event:Twinkle.xfd.callback.change_category});const namespace=mw.config.get("wgNamespaceNumber");categories.append({type:"option",label:"AfD (Articles for deletion)",selected:namespace===0,value:"afd"});categories.append({type:"option",label:"TfD (Templates for discussion)",selected:[10,828].includes(namespace),value:"tfd"});categories.append({type:"option",label:"FfD (Files for discussion)",selected:namespace===6,value:"ffd"});categories.append({type:"option",label:"CfD (Categories for discussion)",selected:namespace===14||namespace===10&&/-stub$/.test(Morebits.pageNameNorm),value:"cfd"});categories.append({type:"option",label:"CfD/S (Categories for speedy renaming)",value:"cfds"});categories.append({type:"option",label:"MfD (Miscellany for deletion)",selected:![0,6,10,14,828].includes(namespace)||Morebits.pageNameNorm.indexOf("Template:User ",0)===0,value:"mfd"});categories.append({type:"option",label:"RfD (Redirects for discussion)",selected:mw.config.get("wgIsRedirect"),value:"rfd"});categories.append({type:"option",label:"RM (Requested moves)",selected:false,value:"rm"});form.append({type:"div",id:"wrong-venue-warn",style:"color: red; font-style: italic"});form.append({type:"checkbox",list:[{label:"Notify page creator if possible",value:"notify",name:"notifycreator",tooltip:"A notification template will be placed on the creator's talk page if this is true.",checked:true}]});form.append({type:"field",label:"Work area",name:"work_area"});const previewlink=document.createElement("a");$(previewlink).on("click",()=>{Twinkle.xfd.callbacks.preview(result)});previewlink.style.cursor="pointer";previewlink.textContent="Preview";form.append({type:"div",id:"xfdpreview",label:[previewlink]});form.append({type:"div",id:"twinklexfd-previewbox",style:"display: none"});form.append({type:"submit"});var result=form.render();Window.setContent(result);Window.display();result.previewer=new Morebits.wiki.Preview($(result).find("div#twinklexfd-previewbox").last()[0]);const evt=document.createEvent("Event");evt.initEvent("change",true,true);result.venue.dispatchEvent(evt)};Twinkle.xfd.callback.wrongVenueWarning=function twinklexfdWrongVenueWarning(venue){let text="";const namespace=mw.config.get("wgNamespaceNumber");switch(venue){case"afd":if(namespace!==0){text="AfD is generally appropriate only for articles."}else if(mw.config.get("wgIsRedirect")){text="Please use RfD for redirects."}break;case"tfd":if(namespace===10&&/-stub$/.test(Morebits.pageNameNorm)){text="Use CfD for stub templates."}else if(Morebits.pageNameNorm.indexOf("Template:User ")===0){text="Please use MfD for userboxes"}break;case"cfd":if(![10,14].includes(namespace)){text="CfD is only for categories and stub templates."}break;case"cfds":if(namespace!==14){text="CfDS is only for categories."}break;case"ffd":if(namespace!==6){text="FFD is selected but this page doesn't look like a file!"}break;case"rm":if(namespace===14){text="Please use CfD or CfDS for category renames."}else if([118,119,2,3].includes(namespace)){text="RMs are not permitted in draft and userspace, unless they are uncontroversial technical requests."}break;default:break}$("#wrong-venue-warn").text(text)};Twinkle.xfd.callback.change_category=function twinklexfdCallbackChangeCategory(e){const value=e.target.value;const form=e.target.form;const old_area=Morebits.QuickForm.getElements(e.target.form,"work_area")[0];let work_area=null;const oldreasontextbox=form.getElementsByTagName("textarea")[0];const oldreason=oldreasontextbox?oldreasontextbox.value:"";const appendReasonBox=function twinklexfdAppendReasonBox(){work_area.append({type:"textarea",name:"reason",label:"Reason:",value:oldreason,tooltip:"You can use wikimarkup in your reason. Twinkle will automatically sign your post."})};Twinkle.xfd.callback.wrongVenueWarning(value);form.previewer.closePreview();switch(value){case"afd":work_area=new Morebits.QuickForm.Element({type:"field",label:"Articles for deletion",name:"work_area"});work_area.append({type:"div",label:"",id:"twinkle-xfd-findsources",style:"margin-bottom: 5px; margin-top: -5px;"});work_area.append({type:"select",name:"outcome",label:"Desired outcome:",event:Twinkle.xfd.callbacks.changeAfdOutcome,list:[{label:"Delete",value:"deletion"},{label:"Merge",value:"merging"},{label:"Redirect",value:"redirecting"},{label:"Draftify",value:"draftification"}]});work_area.append({name:"afdtarget",type:"input",label:"Target page:",tooltip:"Target page for the redirect or merge.",event:Twinkle.xfd.callbacks.changeAfdTarget});work_area.append({type:"checkbox",list:[{label:"Wrap deletion tag with <noinclude>",value:"noinclude",name:"noinclude",tooltip:"Will wrap the deletion tag in <noinclude> tags, so that it won't transclude. This option is not normally required."}]});work_area.append({type:"select",name:"xfdcat",label:"Choose what category this nomination belongs in:",list:[{label:"Unknown",value:"?",selected:true},{label:"Media and music",value:"M"},{label:"Organisation, corporation, or product",value:"O"},{label:"Biographical",value:"B"},{label:"Society topics",value:"S"},{label:"Web or internet",value:"W"},{label:"Games or sports",value:"G"},{label:"Science and technology",value:"T"},{label:"Fiction and the arts",value:"F"},{label:"Places and transportation",value:"P"},{label:"Indiscernible or unclassifiable topic",value:"I"},{label:"Debate not yet sorted",value:"U"}]});work_area.append({type:"select",multiple:true,name:"delsortCats",label:"Choose deletion sorting categories:",tooltip:"Select a few categories that are specifically relevant to the subject of the article. Be as precise as possible; categories like People and USA should only be used when no other categories apply."});Morebits.wiki.getCachedJson("Wikipedia:WikiProject_Deletion_sorting/Computer-readable.json").then(delsortCategories=>{const $select=$('[name="delsortCats"]');$.each(delsortCategories,(groupname,list)=>{const $optgroup=$("<optgroup>").attr("label",groupname);const $delsortCat=$select.append($optgroup);list.forEach(item=>{const $option=$("<option>").val(item).text(item);$delsortCat.append($option)})})});appendReasonBox();work_area=work_area.render();old_area.parentNode.replaceChild(work_area,old_area);$('[name="afdtarget"]').parent().hide();Twinkle.makeFindSourcesDiv("#twinkle-xfd-findsources");$(work_area).find("[name=delsortCats]").attr("data-placeholder","Select delsort pages").select2({theme:"default select2-morebits",width:"100%",matcher:Morebits.select2.matcher,templateResult:Morebits.select2.highlightSearchMatches,language:{searching:Morebits.select2.queryInterceptor},templateSelection:function(choice){return $("<a>").text(choice.text).attr({href:mw.util.getUrl("Wikipedia:WikiProject_Deletion_sorting/"+choice.text),target:"_blank"})}});mw.util.addCSS(".select2-container--default.select2-container--focus .select2-selection--multiple { border: 1px solid #aaa; }"+".select2-results .select2-results__option { padding-top: 1px; padding-bottom: 1px; }"+".select2-results .select2-results__group { padding-top: 1px; padding-bottom: 1px; } "+".select2-container .select2-dropdown .select2-results { font-size: 13px; }"+".select2-container .selection .select2-selection__rendered { font-size: 13px; }"+".select2-selection__choice__remove { font-size: 130%; }");break;case"tfd":work_area=new Morebits.QuickForm.Element({type:"field",label:"Templates for discussion",name:"work_area"});var templateOrModule=mw.config.get("wgPageContentModel")==="Scribunto"?"module":"template";work_area.append({type:"select",label:"Choose type of action wanted:",name:"xfdcat",event:function(e){const target=e.target;let tfdtarget=target.form.tfdtarget;if(target.value==="tfm"&&!tfdtarget){tfdtarget=new Morebits.QuickForm.Element({name:"tfdtarget",type:"input",label:"Other "+templateOrModule+" to be merged:",tooltip:"Required. Should not include the "+Morebits.string.toUpperCaseFirstChar(templateOrModule)+": namespace prefix.",required:true});target.parentNode.appendChild(tfdtarget.render())}else{$(Morebits.QuickForm.getElementContainer(tfdtarget)).remove();tfdtarget=null}},list:[{label:"Deletion",value:"tfd",selected:true},{label:"Merge",value:"tfm"}]});work_area.append({type:"select",name:"templatetype",label:"Deletion tag display style:",tooltip:"Which <code>type=</code> parameter to pass to the TfD tag template.",list:templateOrModule==="module"?[{value:"module",label:"Module",selected:true}]:[{value:"standard",label:"Standard",selected:true},{value:"sidebar",label:"Sidebar/infobox",selected:$(".infobox").length},{value:"inline",label:"Inline template",selected:(()=>{const $oldParser=$(".mw-parser-output > p .Inline-Template");const $newParser=$('.mw-parser-output section[data-mw-section-id="0"] p .Inline-Template');const $scanTarget=$newParser.length?$newParser:$oldParser;return $scanTarget.length})()},{value:"tiny",label:"Tiny inline"},{value:"disabled",label:"Disabled"}]});work_area.append({type:"checkbox",list:[{label:"Wrap deletion tag with <noinclude> (for substituted templates only)",value:"noinclude",name:"noinclude",tooltip:"Will wrap the deletion tag in <noinclude> tags, so that it won't get substituted along with the template.",disabled:templateOrModule==="module",checked:!!$(".box-Subst_only").length}]});work_area.append({type:"checkbox",list:[{label:"Notify talk pages of affected user scripts",value:"devpages",name:"devpages",tooltip:"A notification will be sent to Twinkle, AWB, and Ultraviolet's talk pages if those user scripts are marked as using this template.",checked:true}]});appendReasonBox();work_area=work_area.render();old_area.parentNode.replaceChild(work_area,old_area);break;case"mfd":work_area=new Morebits.QuickForm.Element({type:"field",label:"Miscellany for deletion",name:"work_area"});if(mw.config.get("wgNamespaceNumber")!==710){work_area.append({type:"checkbox",list:[{label:"Wrap deletion tag with <noinclude>",value:"noinclude",name:"noinclude",tooltip:"Will wrap the deletion tag in <noinclude> tags, so that it won't transclude. Select this option for userboxes."}]})}if((mw.config.get("wgNamespaceNumber")===2||mw.config.get("wgNamespaceNumber")===3)&&mw.config.exists("wgRelevantUserName")){work_area.append({type:"checkbox",list:[{label:"Notify owner of userspace (if they are not the page creator)",value:"notifyuserspace",name:"notifyuserspace",tooltip:"If the user in whose userspace this page is located is not the page creator (for example, the page is a rescued article stored as a userspace draft), notify the userspace owner as well.",checked:true}]})}appendReasonBox();work_area=work_area.render();old_area.parentNode.replaceChild(work_area,old_area);break;case"ffd":work_area=new Morebits.QuickForm.Element({type:"field",label:"Discussion venues for files",name:"work_area"});appendReasonBox();work_area=work_area.render();old_area.parentNode.replaceChild(work_area,old_area);break;case"cfd":work_area=new Morebits.QuickForm.Element({type:"field",label:"Categories for discussion",name:"work_area"});var isCategory=mw.config.get("wgNamespaceNumber")===14;work_area.append({type:"select",label:"Choose type of action wanted:",name:"xfdcat",event:function(e){const value=e.target.value,cfdtarget=e.target.form.cfdtarget;let cfdtarget2=e.target.form.cfdtarget2;cfdtarget.disabled=value==="cfd"||value==="sfd-t";if(isCategory){if(value==="cfs"){Morebits.QuickForm.setElementLabel(cfdtarget,"Target categories: ")}else if(value==="cfc"){Morebits.QuickForm.setElementLabel(cfdtarget,"Target article: ")}else{Morebits.QuickForm.setElementLabel(cfdtarget,"Target category: ")}if(value==="cfs"){if(cfdtarget2){cfdtarget2.disabled=false;$(cfdtarget2).show()}else{cfdtarget2=document.createElement("input");cfdtarget2.setAttribute("name","cfdtarget2");cfdtarget2.setAttribute("type","text");cfdtarget2.setAttribute("required","true");cfdtarget.parentNode.appendChild(cfdtarget2)}}else{$(cfdtarget2).prop("disabled",true);$(cfdtarget2).hide()}}else{Morebits.QuickForm.setElementLabel(cfdtarget,"Target stub template: ")}},list:isCategory?[{label:"Deletion",value:"cfd",selected:true},{label:"Merge",value:"cfm"},{label:"Renaming",value:"cfr"},{label:"Split",value:"cfs"},{label:"Convert into article",value:"cfc"}]:[{label:"Stub Deletion",value:"sfd-t",selected:true},{label:"Stub Renaming",value:"sfr-t"}]});work_area.append({type:"input",name:"cfdtarget",label:"Target category:",disabled:true,required:true,value:""});appendReasonBox();work_area=work_area.render();old_area.parentNode.replaceChild(work_area,old_area);break;case"cfds":work_area=new Morebits.QuickForm.Element({type:"field",label:"Categories for speedy renaming",name:"work_area"});work_area.append({type:"select",label:"C2 sub-criterion:",name:"xfdcat",tooltip:"See WP:CFDS for full explanations.",list:[{label:"C2A: Typographic and spelling fixes",value:"C2A",selected:true},{label:"C2B: Naming conventions and disambiguation",value:"C2B"},{label:"C2C: Consistency with names of similar categories",value:"C2C"},{label:"C2D: Rename to match article name",value:"C2D"},{label:"C2E: Author request",value:"C2E"},{label:"C2F: One eponymous article",value:"C2F"}]});work_area.append({type:"input",name:"cfdstarget",label:"New name:",size:70,value:"",required:true});appendReasonBox();work_area=work_area.render();old_area.parentNode.replaceChild(work_area,old_area);break;case"rfd":work_area=new Morebits.QuickForm.Element({type:"field",label:"Redirects for discussion",name:"work_area"});work_area.append({type:"checkbox",list:[{label:"Notify target page if possible",value:"relatedpage",name:"relatedpage",tooltip:"A notification template will be placed on the talk page of this redirect's target if this is true.",checked:true}]});appendReasonBox();work_area=work_area.render();old_area.parentNode.replaceChild(work_area,old_area);break;case"rm":{work_area=new Morebits.QuickForm.Element({type:"field",label:"Requested moves",name:"work_area"});work_area.append({type:"checkbox",list:[{label:"Uncontroversial technical request",value:"rmtr",name:"rmtr",tooltip:"Use this option when you are unable to perform this uncontroversial move yourself because of a technical reason (e.g. a page already exists at the new title, or the page is protected)",checked:false,event:function(){$('input[name="newname"]',form).prop("required",this.checked);$('input[type="button"][value="more"]',form)[0].sublist.inputs[1].required=this.checked},subgroup:{type:"checkbox",list:[{label:"Opt out of discussion if the request is contested",value:"rmtr-discuss",name:"rmtr-discuss",tooltip:'Use this option if you prefer to withdraw the request if contested, rather than discuss it. This suppresses the "discuss" link, which may be used to convert your request to a discussion on the talk page.',checked:false}]}}]});work_area.append({type:"dyninput",inputs:[{label:"From:",name:"currentname",required:true},{label:"To:",name:"newname",tooltip:"Required for technical requests. Otherwise, if unsure of the appropriate title, you may leave it blank."}],min:1});appendReasonBox();work_area=work_area.render();old_area.parentNode.replaceChild(work_area,old_area);const currentNonTalkPage=mw.Title.newFromText(Morebits.pageNameNorm).getSubjectPage().toText();form.currentname.value=currentNonTalkPage;break}default:work_area=new Morebits.QuickForm.Element({type:"field",label:"Nothing for anything",name:"work_area"});work_area=work_area.render();old_area.parentNode.replaceChild(work_area,old_area);break}form.notifycreator.disabled=value==="cfds"||value==="rm";form.notifycreator.checked=!form.notifycreator.disabled};Twinkle.xfd.callbacks={changeAfdTarget:function(){const $afdTarget=$('[name="afdtarget"]');const $reason=$('[name="reason"]');const $outcome=$('[name="outcome"]');if($reason.val().endsWith("because ")){if($afdTarget.val()){if($outcome.val()==="redirecting"){$reason.val(`I propose '''redirecting''' to [[${$afdTarget.val()}]] because `)}else if($outcome.val()==="merging"){$reason.val(`I propose '''merging''' to [[${$afdTarget.val()}]] because `)}}else{if($outcome.val()==="redirecting"){$reason.val("I propose '''redirecting''' because ")}else if($outcome.val()==="merging"){$reason.val("I propose '''merging''' because ")}}}},changeAfdOutcome:function(){const $outcome=$('[name="outcome"]');const $reason=$('[name="reason"]');const $afdTarget=$('[name="afdtarget"]');$afdTarget.val("");if($outcome.val()==="redirecting"){$reason.val("I propose '''redirecting''' because ");$afdTarget.parent().show()}else if($outcome.val()==="merging"){$reason.val("I propose '''merging''' because ");$afdTarget.parent().show()}else if($outcome.val()==="draftification"){$reason.val("I propose '''draftifying''' because ");$afdTarget.parent().hide()}else if($outcome.val()==="deletion"){$reason.val("");$afdTarget.parent().hide()}},autoEditRequest:function(pageobj,params){const talkName=new mw.Title(pageobj.getPageName()).getTalkPage().toText();if(talkName===pageobj.getPageName()){pageobj.getStatusElement().error("Page protected and nowhere to add an edit request, aborting")}else{pageobj.getStatusElement().warn("Page protected, requesting edit");const editRequest="{{subst:Xfd edit protected|page="+pageobj.getPageName()+"|discussion="+params.discussionpage+(params.venue==="rfd"?"|rfd=yes":"")+"|tag=<nowiki>"+params.tagText+"\x3c/nowiki>}}";const talk_page=new Morebits.wiki.Page(talkName,"Automatically posting edit request on talk page");talk_page.setNewSectionTitle("Edit request to complete "+utils.toTLACase(params.venue)+" nomination");talk_page.setNewSectionText(editRequest);talk_page.setCreateOption("recreate");talk_page.setWatchlist(Twinkle.getPref("xfdWatchPage"));talk_page.setFollowRedirect(true);talk_page.setChangeTags(Twinkle.changeTags);talk_page.setCallbackParameters(params);talk_page.newSection(null,()=>{talk_page.getStatusElement().warn("Unable to add edit request, the talk page may be protected")})}},getDiscussionWikitext:function(venue,params){if(venue==="cfds"){return"* [[:"+Morebits.pageNameNorm+"]] to [[:"+params.cfdstarget+"]] – "+params.xfdcat+(params.reason?": "+Morebits.string.formatReasonText(params.reason):".")+" ~~~~"}if(venue==="rm"){if(params.rmtr){const rmtrDiscuss=params["rmtr-discuss"]?"|discuss=no":"";return params.currentname.map((currentname,i)=>`{{subst:RMassist|1=${currentname}|2=${params.newname[i]}${rmtrDiscuss}|reason=${params.reason}}}`).join("\n")}return`{{subst:Requested move${params.currentname.map((currentname,i)=>`|current${i+1}=${currentname}|new${i+1}=${params.newname[i]}`).join("")}|reason=${params.reason}}}`}let text="{{subst:"+venue+"2";const reasonKey=venue==="ffd"?"Reason":"text";text+="|"+reasonKey+"="+Morebits.string.formatReasonText(params.reason,true);if(venue==="afd"||venue==="mfd"){text+="|pg="+Morebits.pageNameNorm;if(venue==="afd"){text+="|cat="+params.xfdcat}}else if(venue==="rfd"){text+="|redirect="+Morebits.pageNameNorm}else{text+="|1="+mw.config.get("wgTitle");if(mw.config.get("wgPageContentModel")==="Scribunto"){text+="|module=Module:"}}if(params.rfdtarget){text+="|target="+params.rfdtarget+(params.section?"#"+params.section:"")}else if(params.tfdtarget){text+="|2="+params.tfdtarget}else if(params.cfdtarget){text+="|2="+params.cfdtarget;if(params.cfdtarget2){text+="|3="+params.cfdtarget2}}else if(params.uploader){text+="|Uploader="+params.uploader}text+="}}";if(venue==="rfd"||venue==="tfd"||venue==="cfd"){text+="\n"}if(Array.isArray(params.delsortCats)&¶ms.delsortCats.length){text+="\n{{subst:Deletion sorting/multi|"+params.delsortCats.join("|")+"|sig=~~~~}}"}return text},showPreview:function(form,venue,params){const templatetext=Twinkle.xfd.callbacks.getDiscussionWikitext(venue,params);if(venue==="rm"){form.previewer.beginRender(templatetext,params.rmtr?"Wikipedia:Requested moves/Technical requests":new mw.Title(Morebits.pageNameNorm).getTalkPage().toText())}else{form.previewer.beginRender(templatetext,"WP:TW")}},preview:function(form){const params=Morebits.QuickForm.getInputData(form);const venue=params.venue;if(params.tfdtarget){params.tfdtarget=utils.stripNs(params.tfdtarget)}else if(params.cfdtarget){params.cfdtarget=utils.stripNs(params.cfdtarget);if(params.cfdtarget2){params.cfdtarget2=utils.stripNs(params.cfdtarget2)}}else if(params.cfdstarget){params.cfdstarget=utils.addNs(params.cfdstarget,14)}if(venue==="ffd"){const page=new Morebits.wiki.Page(mw.config.get("wgPageName"));page.lookupCreation(()=>{params.uploader=page.getCreator();Twinkle.xfd.callbacks.showPreview(form,venue,params)})}else if(venue==="rfd"){Twinkle.xfd.callbacks.rfd.findTarget(params,params=>{Twinkle.xfd.callbacks.showPreview(form,venue,params)})}else if(venue==="cfd"){Twinkle.xfd.callbacks.showPreview(form,params.xfdcat,params)}else{Twinkle.xfd.callbacks.showPreview(form,venue,params)}},notifyUser:function(params,notifyTarget,noLog,actionName){const userTalkNamespace=3;notifyTarget=mw.Title.newFromText(notifyTarget,userTalkNamespace);const targetNS=notifyTarget.getNamespaceId();const usernameOrTarget=notifyTarget.getRelativeText(userTalkNamespace);notifyTarget=notifyTarget.toText();if(targetNS===userTalkNamespace){if(usernameOrTarget===mw.config.get("wgUserName")){Morebits.Status.warn("You ("+usernameOrTarget+") created this page; skipping user notification");Twinkle.xfd.callbacks.addToLog(params,null);return}actionName=actionName||"Notifying initial contributor ("+usernameOrTarget+")"}const notifyText=Twinkle.xfd.callbacks.generateUserTalkNoticeWikitext(params);const venueNames={afd:"Articles for deletion",tfd:"Templates for discussion",mfd:"Miscellany for deletion",cfd:"Categories for discussion",ffd:"Files for discussion",rfd:"Redirects for discussion"};const editSummary="Notification: [["+params.discussionpage+"|listing]] of [[:"+Morebits.pageNameNorm+"]] at [[WP:"+venueNames[params.venue]+"]].";const usertalkpage=new Morebits.wiki.Page(notifyTarget,actionName);usertalkpage.setAppendText(notifyText);usertalkpage.setEditSummary(editSummary);usertalkpage.setChangeTags(Twinkle.changeTags);usertalkpage.setCreateOption("recreate");if(params.venue==="rfd"&&targetNS!==3){usertalkpage.setWatchlist(Twinkle.getPref("xfdWatchRelated"))}else{usertalkpage.setWatchlist(Twinkle.getPref("xfdWatchUser"))}usertalkpage.setFollowRedirect(true,false);if(noLog){usertalkpage.append()}else{usertalkpage.append(()=>{if(!params.notifycreator){notifyTarget=null}Twinkle.xfd.callbacks.addToLog(params,usernameOrTarget)},()=>{Twinkle.xfd.callbacks.addToLog(params,null)})}},generateUserTalkNoticeWikitext:function(params){let notifytext="\n{{subst:"+params.venue+" notice";const templateNamespace=10;switch(params.venue){case"afd":notifytext+=params.outcome!=="deletion"?"|outcome="+params.outcome:"";notifytext+=params.afdtarget?"|target="+params.afdtarget:"";notifytext+=params.numbering!==""?"|order= "+params.numbering:"";break;case"mfd":notifytext+=params.numbering!==""?"|order= "+params.numbering:"";break;case"tfd":if(params.xfdcat==="tfm"){notifytext="\n{{subst:Tfm notice|2="+params.tfdtarget}break;case"cfd":notifytext+="|action="+params.action+(mw.config.get("wgNamespaceNumber")===templateNamespace?"|stub=yes":"");break;default:break}notifytext+="|1="+Morebits.pageNameNorm+"}} ~~~~";return notifytext},addToLog:function(params,initialContrib){if(!Twinkle.getPref("logXfdNominations")||Twinkle.getPref("noLogOnXfdNomination").includes(params.venue)){return}const usl=new Morebits.UserspaceLogger(Twinkle.getPref("xfdLogPageName"));usl.initialText="This is a log of all [[WP:XFD|deletion discussion]] nominations made by this user using [[WP:TW|Twinkle]]'s XfD module.\n\n"+"If you no longer wish to keep this log, you can turn it off using the [[Wikipedia:Twinkle/Preferences|preferences panel]], and "+"nominate this page for speedy deletion under [[WP:CSD#U1|CSD U1]]."+(Morebits.userIsSysop?"\n\nThis log does not track XfD-related deletions made using Twinkle.":"");let editsummary;if(params.discussionpage){editsummary="Logging [["+params.discussionpage+"|"+utils.toTLACase(params.venue)+" nomination]] of [[:"+Morebits.pageNameNorm+"]]."}else{editsummary="Logging "+utils.toTLACase(params.venue)+" nomination of [[:"+Morebits.pageNameNorm+"]]."}const fileLogLink=mw.config.get("wgNamespaceNumber")===6?" ([{{fullurl:Special:Log|page="+mw.util.wikiUrlencode(mw.config.get("wgPageName"))+"}} log])":"";const nominatedLink=params.discussionpage?"[["+params.discussionpage+"|nominated]]":"nominated";let appendText="# [[:"+Morebits.pageNameNorm+"]]:"+fileLogLink+" "+nominatedLink+" at [[WP:"+params.venue.toUpperCase()+"|"+utils.toTLACase(params.venue)+"]]";switch(params.venue){case"tfd":if(params.xfdcat==="tfm"){appendText+=" (merge)";if(params.tfdtarget){const contentModel=mw.config.get("wgPageContentModel")==="Scribunto"?"Module:":"Template:";appendText+="; Other "+contentModel.toLowerCase()+" [[";if(!new RegExp("^:?"+Morebits.namespaceRegex([10,828])+":","i").test(params.tfdtarget)){appendText+=contentModel}appendText+=params.tfdtarget+"]]"}}break;case"mfd":if(params.notifyuserspace&¶ms.userspaceOwner&¶ms.userspaceOwner!==initialContrib){appendText+="; notified {{user|1="+params.userspaceOwner+"}}"}break;case"cfd":appendText+=" ("+utils.toTLACase(params.xfdcat)+")";if(params.cfdtarget){const categoryOrTemplate=params.xfdcat.charAt(0)==="s"?"Template:":":Category:";appendText+="; "+params.action+" to [["+categoryOrTemplate+params.cfdtarget+"]]";if(params.xfdcat==="cfs"&¶ms.cfdtarget2){appendText+=", [["+categoryOrTemplate+params.cfdtarget2+"]]"}}break;case"cfds":appendText+=" ("+utils.toTLACase(params.xfdcat)+")";if(params.cfdstarget&¶ms.cfdstarget.length>9){appendText+="; New name: [[:"+params.cfdstarget+"]]"}break;case"rfd":if(params.rfdtarget){appendText+="; Target: [[:"+params.rfdtarget+"]]";if(params.relatedpage){appendText+=" (notified)"}}break;case"rm":appendText=params.currentname.map((currentname,i)=>`# [[:${currentname}]]: ${nominatedLink} at [[WP:RM${params.rmtr?"/TR":""}|]]${params.newname[i]?`; New name: [[:${params.newname[i]}]]`:""}`).join("\n");break;default:break}if(initialContrib&¶ms.notifycreator){appendText+="; notified {{user|1="+initialContrib+"}}"}appendText+=" ~~~~~";if(params.reason){appendText+="\n#* '''Reason''': "+Morebits.string.formatReasonForLog(params.reason)}usl.changeTags=Twinkle.changeTags;usl.log(appendText,editsummary)},afd:{main:function(apiobj){const response=apiobj.getResponse();const titles=response.query.allpages;if(titles.length<=0){apiobj.params.numbering=apiobj.params.number=""}else{let number=0;for(let i=0;i<titles.length;++i){const title=titles[i].title;if(title==="Wikipedia:Articles for deletion/"+Morebits.pageNameNorm){number=Math.max(number,1);continue}const order_re=new RegExp("^"+Morebits.string.escapeRegExp("Wikipedia:Articles for deletion/"+Morebits.pageNameNorm)+"\\s*\\(\\s*(\\d+)(?:(?:th|nd|rd|st) nom(?:ination)?)?\\s*\\)\\s*$");const match=order_re.exec(title);if(!match||match[1]>100){continue}number=Math.max(number,Number(match[1]))}apiobj.params.number=utils.num2order(parseInt(number,10)+1);apiobj.params.numbering=number>0?" ("+apiobj.params.number+" nomination)":""}apiobj.params.discussionpage="Wikipedia:Articles for deletion/"+Morebits.pageNameNorm+apiobj.params.numbering;Morebits.Status.info("Next discussion page","[["+apiobj.params.discussionpage+"]]");Morebits.wiki.actionCompleted.redirect=apiobj.params.discussionpage;Morebits.wiki.actionCompleted.notice="Nomination completed, now redirecting to the discussion page";const wikipedia_page=new Morebits.wiki.Page(mw.config.get("wgPageName"),"Adding deletion tag to article");wikipedia_page.setFollowRedirect(true);wikipedia_page.setChangeTags(Twinkle.changeTags);wikipedia_page.setCallbackParameters(apiobj.params);wikipedia_page.load(Twinkle.xfd.callbacks.afd.taggingArticle)},taggingArticle:function(pageobj){let text=pageobj.getPageText();const params=pageobj.getCallbackParameters();const statelem=pageobj.getStatusElement();if(!pageobj.exists()){statelem.error("It seems that the page doesn't exist; perhaps it has already been deleted");return}const textNoAfd=text.replace(/<!--.*AfD.*\n\{\{(?:Article for deletion\/dated|AfDM).*\}\}\n<!--.*(?:\n<!--.*)?AfD.*(?:\s*\n)?/g,"");if(text!==textNoAfd){if(confirm("An AfD tag was found on this article. Maybe someone beat you to it. \nClick OK to replace the current AfD tag (not recommended), or Cancel to abandon your nomination.")){text=textNoAfd}else{statelem.error("Article already tagged with AfD tag, and you chose to abort");window.location.reload();return}}if(Twinkle.getPref("markXfdPagesAsPatrolled")){pageobj.triage()}let wikipedia_page=new Morebits.wiki.Page(params.discussionpage,"Creating article deletion discussion page");wikipedia_page.setCallbackParameters(params);wikipedia_page.load(Twinkle.xfd.callbacks.afd.discussionPage);const date=new Morebits.Date(pageobj.getLoadTime());wikipedia_page=new Morebits.wiki.Page("Wikipedia:Articles for deletion/Log/"+date.format("YYYY MMMM D","utc"),"Adding discussion to today's list");wikipedia_page.setFollowRedirect(true);wikipedia_page.setCallbackParameters(params);wikipedia_page.load(Twinkle.xfd.callbacks.afd.todaysList);if(params.notifycreator){const thispage=new Morebits.wiki.Page(mw.config.get("wgPageName"));thispage.setCallbackParameters(params);thispage.setLookupNonRedirectCreator(true);// Look for author of first non-redirect revision
thispage.lookupCreation(pageobj=>{Twinkle.xfd.callbacks.notifyUser(pageobj.getCallbackParameters(),pageobj.getCreator())})}else{Twinkle.xfd.callbacks.addToLog(params,null)}params.tagText=Twinkle.xfd.callbacks.afd.generateArticleTagWikitext(params.noinclude,params.outcome,params.afdtarget,params.number);if(params.outcome==="merging"&¶ms.afdtarget){wikipedia_page=new Morebits.wiki.Page(params.afdtarget,"Tagging the target page with {{Merge from}}");wikipedia_page.setCallbackParameters(params);wikipedia_page.setFollowRedirect(true);wikipedia_page.load(Twinkle.xfd.callbacks.afd.tagTargetPageWithMergeFromTag)}if(pageobj.canEdit()){text=text.replace(/\{\{\s*(dated prod|dated prod blp|Prod blp\/dated|Proposed deletion\/dated|prod2|Proposed deletion endorsed|Userspace draft)\s*(\|(?:\{\{[^{}]*\}\}|[^{}])*)?\}\}\s*/gi,"");const textNoSd=text.replace(/\{\{\s*(db(-\w*)?|delete|(?:hang|hold)[- ]?on)\s*(\|(?:\{\{[^{}]*\}\}|[^{}])*)?\}\}\s*/gi,"");if(text!==textNoSd&&confirm("A speedy deletion tag was found on this page. Should it be removed?")){text=textNoSd}const wikipage=new Morebits.wikitext.Page(text);text=wikipage.insertAfterTemplates(params.tagText,Twinkle.hatnoteRegex).getText();pageobj.setPageText(text);pageobj.setEditSummary(`Nominated for ${params.outcome}; see [[:${params.discussionpage}]].`);pageobj.setWatchlist(Twinkle.getPref("xfdWatchPage"));pageobj.setCreateOption("nocreate");pageobj.save()}else{Twinkle.xfd.callbacks.autoEditRequest(pageobj,params)}},tagTargetPageWithMergeFromTag:function(pageobj){const statelem=pageobj.getStatusElement();if(!pageobj.exists()){statelem.warn("Failed. Target page not found.");return}else if(!pageobj.canEdit()){statelem.warn("Failed. Target page is protected from editing.");return}const params=pageobj.getCallbackParameters();const tag=`{{Merge from |1=${Morebits.pageNameNorm} |target=${params.afdtarget} |afd=${Morebits.pageNameNorm+params.numbering} |date ={{subst:CURRENTMONTHNAME}} {{subst:CURRENTYEAR}} }}`;const wikipage=new Morebits.wikitext.Page(pageobj.getPageText());const text=wikipage.insertAfterTemplates(tag,Twinkle.hatnoteRegex).getText();pageobj.setPageText(text);pageobj.setEditSummary("Nominated for merging; see [[:"+params.discussionpage+"]].");pageobj.setCreateOption("nocreate");pageobj.setChangeTags(Twinkle.changeTags);pageobj.save()},generateArticleTagWikitext:function(noinclude,outcome,afdtarget,number){let noIncludeStart="";let noIncludeEnd="";if(noinclude){noIncludeStart="<noinclude>";noIncludeEnd="</noinclude>"}let templateAndParams="";const outcomeParam=outcome!=="deletion"?"|outcome="+outcome:"";const targetParam=afdtarget?"|target="+afdtarget:"";const isFirstNomination=number==="";if(isFirstNomination){templateAndParams="subst:afd|help=off"+outcomeParam+targetParam}else{templateAndParams="subst:afdx|"+number+"|help=off"+outcomeParam+targetParam}return noIncludeStart+"{{"+templateAndParams+"}}"+noIncludeEnd+"\n"},discussionPage:function(pageobj){const params=pageobj.getCallbackParameters();pageobj.setPageText(Twinkle.xfd.callbacks.getDiscussionWikitext("afd",params));pageobj.setEditSummary("Creating AfD discussion page for [[:"+Morebits.pageNameNorm+"]].");pageobj.setChangeTags(Twinkle.changeTags);pageobj.setWatchlist(Twinkle.getPref("xfdWatchDiscussion"));pageobj.setCreateOption("createonly");pageobj.save(()=>{Twinkle.xfd.currentRationale=null;if(params.delsortCats){params.delsortCats.forEach(cat=>{const delsortPage=new Morebits.wiki.Page("Wikipedia:WikiProject Deletion sorting/"+cat,"Adding to list of "+cat+"-related deletion discussions");delsortPage.setFollowRedirect(true);delsortPage.setCallbackParameters({discussionPage:params.discussionpage});delsortPage.load(Twinkle.xfd.callbacks.afd.delsortListing)})}})},todaysList:function(pageobj){const params=pageobj.getCallbackParameters();const statelem=pageobj.getStatusElement();const added_data="{{subst:afd3|pg="+Morebits.pageNameNorm+params.numbering+"}}\n";let text;if(!pageobj.exists()){text="{{subst:AfD log}}\n"+added_data}else{const old_text=pageobj.getPageText()+"\n";text=old_text.replace(/(<!-- Add new entries to the TOP of the following list -->\n+)/,"$1"+added_data);if(text===old_text){const linknode=document.createElement("a");linknode.setAttribute("href",mw.util.getUrl("Wikipedia:Twinkle/Fixing AFD")+"?action=purge");linknode.appendChild(document.createTextNode("How to fix AFD"));statelem.error(["Could not find the target spot for the discussion. To fix this problem, please see ",linknode,"."]);return}}pageobj.setPageText(text);pageobj.setEditSummary("Adding [[:"+params.discussionpage+"]].");pageobj.setChangeTags(Twinkle.changeTags);pageobj.setWatchlist(Twinkle.getPref("xfdWatchList"));pageobj.setCreateOption("recreate");pageobj.setDiscussionToolsAutoSubscribe(false);pageobj.save()},delsortListing:function(pageobj){const discussionPage=pageobj.getCallbackParameters().discussionPage;const text=pageobj.getPageText().replace("directly below this line --\x3e","directly below this line --\x3e\n{{"+discussionPage+"}}");pageobj.setPageText(text);pageobj.setEditSummary("Listing [[:"+discussionPage+"]].");pageobj.setChangeTags(Twinkle.changeTags);pageobj.setCreateOption("nocreate");pageobj.setDiscussionToolsAutoSubscribe(false);pageobj.save()}},tfd:{main:function(pageobj){const params=pageobj.getCallbackParameters();const date=new Morebits.Date(pageobj.getLoadTime());params.logpage="Wikipedia:Templates for discussion/Log/"+date.format("YYYY MMMM D","utc");params.discussionpage=params.logpage+"#"+Morebits.pageNameNorm;pageobj.setCallbackParameters(params);let watchModule,watch_query;if(params.scribunto){const watchPref=Twinkle.getPref("xfdWatchPage");watchModule=watchPref!=="no"&&(watchPref!=="default"||!!parseInt(mw.user.options.get("watchdefault"),10));if(watchModule){watch_query={action:"watch",titles:[mw.config.get("wgPageName")],token:mw.user.tokens.get("watchToken")};if(pageobj.getWatched()!==true&&watchPref!=="default"&&watchPref!=="yes"){watch_query.expiry=watchPref}}}if(params.xfdcat==="tfm"){let wikipedia_otherpage;if(params.scribunto){wikipedia_otherpage=new Morebits.wiki.Page(params.otherTemplateName+"/doc","Tagging other module documentation with merge tag");if(watchModule){watch_query.titles.push(params.otherTemplateName);new Morebits.wiki.Api("Adding Modules to watchlist",watch_query).post()}}else{wikipedia_otherpage=new Morebits.wiki.Page(params.otherTemplateName,"Tagging other template with merge tag")}Twinkle.xfd.callbacks.tfd.taggingTemplateForMerge(pageobj);wikipedia_otherpage.setFollowRedirect(true);const otherParams=$.extend({},params);otherParams.otherTemplateName=Morebits.pageNameNorm;wikipedia_otherpage.setCallbackParameters(otherParams);wikipedia_otherpage.load(Twinkle.xfd.callbacks.tfd.taggingTemplateForMerge)}else{if(params.scribunto&&Twinkle.getPref("xfdWatchPage")!=="no"){if(watchModule){new Morebits.wiki.Api("Adding Module to watchlist",watch_query).post()}}Twinkle.xfd.callbacks.tfd.taggingTemplate(pageobj)}Morebits.wiki.actionCompleted.redirect=params.logpage;Morebits.wiki.actionCompleted.notice="Nomination completed, now redirecting to today's log";const wikipedia_page=new Morebits.wiki.Page(params.logpage,"Adding discussion to today's log");wikipedia_page.setFollowRedirect(true);wikipedia_page.setCallbackParameters(params);wikipedia_page.load(Twinkle.xfd.callbacks.tfd.todaysList);if(params.notifycreator){const involvedpages=[];const seenusers=[];involvedpages.push(new Morebits.wiki.Page(mw.config.get("wgPageName")));if(params.xfdcat==="tfm"){if(params.scribunto){involvedpages.push(new Morebits.wiki.Page("Module:"+params.tfdtarget))}else{involvedpages.push(new Morebits.wiki.Page("Template:"+params.tfdtarget))}}involvedpages.forEach(page=>{page.setCallbackParameters(params);page.lookupCreation(innerpage=>{const username=innerpage.getCreator();if(!seenusers.includes(username)){seenusers.push(username);Twinkle.xfd.callbacks.notifyUser(innerpage.getCallbackParameters(),username,params.xfdcat==="tfm"&&innerpage.getPageName()!==Morebits.pageNameNorm)}})})}else{Twinkle.xfd.callbacks.addToLog(params,null)}if(params.devpages){const inCategories=mw.config.get("wgCategories");const categoryNotificationPageMap={"Templates used by Twinkle":"Wikipedia talk:Twinkle","Templates used by AutoWikiBrowser":"Wikipedia talk:AutoWikiBrowser","Templates used by Ultraviolet":"Wikipedia talk:Ultraviolet"};$.each(categoryNotificationPageMap,(category,page)=>{if(inCategories.includes(category)){Twinkle.xfd.callbacks.notifyUser(params,page,true,"Notifying "+page+" of template nomination")}})}},taggingTemplate:function(pageobj){const text=pageobj.getPageText();const params=pageobj.getCallbackParameters();params.tagText="{{subst:template for discussion|help=off"+(params.templatetype!=="standard"?"|type="+params.templatetype:"")+"}}";if(pageobj.getContentModel()==="sanitized-css"){params.tagText="/* "+params.tagText+" */"}else{if(params.noinclude){params.tagText="<noinclude>"+params.tagText+"</noinclude>"}params.tagText+=params.templatetype==="standard"||params.templatetype==="sidebar"||params.templatetype==="disabled"?"\n":""}if(pageobj.canEdit()&&["wikitext","sanitized-css"].includes(pageobj.getContentModel())){pageobj.setPageText(params.tagText+text);pageobj.setEditSummary("Nominated for deletion; see [[:"+params.discussionpage+"]].");pageobj.setChangeTags(Twinkle.changeTags);pageobj.setWatchlist(Twinkle.getPref("xfdWatchPage"));if(params.scribunto){pageobj.setCreateOption("recreate")}pageobj.save()}else{Twinkle.xfd.callbacks.autoEditRequest(pageobj,params)}},taggingTemplateForMerge:function(pageobj){const text=pageobj.getPageText();const params=pageobj.getCallbackParameters();params.tagText="{{subst:tfm|help=off|"+(params.templatetype!=="standard"?"type="+params.templatetype+"|":"")+"1="+params.otherTemplateName.replace(new RegExp("^"+Morebits.namespaceRegex([10,828])+":"),"")+"}}";if(pageobj.getContentModel()==="sanitized-css"){params.tagText="/* "+params.tagText+" */"}else{if(params.noinclude){params.tagText="<noinclude>"+params.tagText+"</noinclude>"}params.tagText+=params.templatetype==="standard"||params.templatetype==="sidebar"||params.templatetype==="disabled"?"\n":""}if(pageobj.canEdit()&&["wikitext","sanitized-css"].includes(pageobj.getContentModel())){pageobj.setPageText(params.tagText+text);pageobj.setEditSummary("Listed for merging with [[:"+params.otherTemplateName+"]]; see [[:"+params.discussionpage+"]].");pageobj.setChangeTags(Twinkle.changeTags);pageobj.setWatchlist(Twinkle.getPref("xfdWatchPage"));if(params.scribunto){pageobj.setCreateOption("recreate")}pageobj.save()}else{Twinkle.xfd.callbacks.autoEditRequest(pageobj,params)}},todaysList:function(pageobj){const params=pageobj.getCallbackParameters();const statelem=pageobj.getStatusElement();const added_data=Twinkle.xfd.callbacks.getDiscussionWikitext(params.xfdcat,params);let text;if(!pageobj.exists()){text="{{subst:TfD log}}\n"+added_data}else{const old_text=pageobj.getPageText();text=old_text.replace("--\x3e","--\x3e\n"+added_data);if(text===old_text){statelem.error("failed to find target spot for the discussion");return}}pageobj.setPageText(text);pageobj.setEditSummary("/* "+Morebits.pageNameNorm+" */ Adding "+(params.xfdcat==="tfd"?"deletion nomination":"merge listing")+" of [[:"+Morebits.pageNameNorm+"]].");pageobj.setChangeTags(Twinkle.changeTags);pageobj.setWatchlist(Twinkle.getPref("xfdWatchDiscussion"));pageobj.setCreateOption("recreate");pageobj.save(()=>{Twinkle.xfd.currentRationale=null})}},mfd:{main:function(apiobj){const response=apiobj.getResponse();const titles=response.query.allpages;if(titles.length<=0){apiobj.params.numbering=apiobj.params.number=""}else{let number=0;for(let i=0;i<titles.length;++i){const title=titles[i].title;if(title==="Wikipedia:Miscellany for deletion/"+Morebits.pageNameNorm){number=Math.max(number,1);continue}const order_re=new RegExp("^"+Morebits.string.escapeRegExp("Wikipedia:Miscellany for deletion/"+Morebits.pageNameNorm)+"\\s*\\(\\s*(\\d+)(?:(?:th|nd|rd|st) nom(?:ination)?)?\\s*\\)\\s*$");const match=order_re.exec(title);if(!match){continue}number=Math.max(number,Number(match[1]))}apiobj.params.number=utils.num2order(parseInt(number,10)+1);apiobj.params.numbering=number>0?" ("+apiobj.params.number+" nomination)":""}apiobj.params.discussionpage="Wikipedia:Miscellany for deletion/"+Morebits.pageNameNorm+apiobj.params.numbering;apiobj.statelem.info("next in order is [["+apiobj.params.discussionpage+"]]");let wikipedia_page;if(mw.config.get("wgNamespaceNumber")!==710){wikipedia_page=new Morebits.wiki.Page(mw.config.get("wgPageName"),"Tagging page with deletion tag");wikipedia_page.setFollowRedirect(true);wikipedia_page.setCallbackParameters(apiobj.params);wikipedia_page.load(Twinkle.xfd.callbacks.mfd.taggingPage)}Morebits.wiki.actionCompleted.redirect=apiobj.params.discussionpage;Morebits.wiki.actionCompleted.notice="Nomination completed, now redirecting to the discussion page";wikipedia_page=new Morebits.wiki.Page(apiobj.params.discussionpage,"Creating deletion discussion page");wikipedia_page.setCallbackParameters(apiobj.params);wikipedia_page.load(Twinkle.xfd.callbacks.mfd.discussionPage);wikipedia_page=new Morebits.wiki.Page("Wikipedia:Miscellany for deletion","Adding discussion to today's list");wikipedia_page.setPageSection(2);wikipedia_page.setFollowRedirect(true);wikipedia_page.setCallbackParameters(apiobj.params);wikipedia_page.load(Twinkle.xfd.callbacks.mfd.todaysList);if(apiobj.params.notifycreator||apiobj.params.notifyuserspace){const thispage=new Morebits.wiki.Page(mw.config.get("wgPageName"));thispage.setCallbackParameters(apiobj.params);thispage.lookupCreation(Twinkle.xfd.callbacks.mfd.sendNotifications)}else{Twinkle.xfd.callbacks.addToLog(apiobj.params,null)}},taggingPage:function(pageobj){const text=pageobj.getPageText();const params=pageobj.getCallbackParameters();params.tagText="{{"+(params.number===""?"mfd":"mfdx|"+params.number)+"|help=off}}";if(["javascript","css","sanitized-css"].includes(mw.config.get("wgPageContentModel"))){params.tagText="/* "+params.tagText+" */\n"}else{params.tagText+="\n";if(params.noinclude){params.tagText="<noinclude>"+params.tagText+"</noinclude>"}}if(pageobj.canEdit()&&["wikitext","javascript","css","sanitized-css"].includes(pageobj.getContentModel())){pageobj.setPageText(params.tagText+text);pageobj.setEditSummary("Nominated for deletion; see [[:"+params.discussionpage+"]].");pageobj.setChangeTags(Twinkle.changeTags);pageobj.setWatchlist(Twinkle.getPref("xfdWatchPage"));pageobj.setCreateOption("nocreate");pageobj.save()}else{Twinkle.xfd.callbacks.autoEditRequest(pageobj,params)}},discussionPage:function(pageobj){const params=pageobj.getCallbackParameters();pageobj.setPageText(Twinkle.xfd.callbacks.getDiscussionWikitext("mfd",params));pageobj.setEditSummary("Creating deletion discussion page for [[:"+Morebits.pageNameNorm+"]].");pageobj.setChangeTags(Twinkle.changeTags);pageobj.setWatchlist(Twinkle.getPref("xfdWatchDiscussion"));pageobj.setCreateOption("createonly");pageobj.save(()=>{Twinkle.xfd.currentRationale=null})},todaysList:function(pageobj){let text=pageobj.getPageText();const params=pageobj.getCallbackParameters();const statelem=pageobj.getStatusElement();const date=new Morebits.Date(pageobj.getLoadTime());const date_header=date.format("===MMMM D, YYYY===\n","utc");const date_header_regex=new RegExp(date.format("(===[\\s]*MMMM[\\s]+D,[\\s]+YYYY[\\s]*===)","utc"));const added_data="{{subst:mfd3|pg="+Morebits.pageNameNorm+params.numbering+"}}";if(date_header_regex.test(text)){statelem.info("Found today's section, proceeding to add new entry");text=text.replace(date_header_regex,"$1\n"+added_data)}else{statelem.info("No section for today found, proceeding to create one");text=text.replace("===",date_header+added_data+"\n\n===")}pageobj.setPageText(text);pageobj.setEditSummary("Adding [[:"+params.discussionpage+"]].");pageobj.setChangeTags(Twinkle.changeTags);pageobj.setWatchlist(Twinkle.getPref("xfdWatchList"));pageobj.setCreateOption("recreate");pageobj.setDiscussionToolsAutoSubscribe(false);pageobj.save()},sendNotifications:function(pageobj){const initialContrib=pageobj.getCreator();const params=pageobj.getCallbackParameters();if(params.notifycreator){Twinkle.xfd.callbacks.notifyUser(params,initialContrib)}params.userspaceOwner=mw.config.get("wgRelevantUserName");if(params.notifyuserspace){if(params.userspaceOwner!==initialContrib){Twinkle.xfd.callbacks.notifyUser(params,params.userspaceOwner,params.notifycreator,"Notifying owner of userspace ("+params.userspaceOwner+")")}else if(!params.notifycreator){Twinkle.xfd.callbacks.addToLog(params,initialContrib)}}}},ffd:{taggingImage:function(pageobj){let text=pageobj.getPageText();const params=pageobj.getCallbackParameters();const date=new Morebits.Date(pageobj.getLoadTime()).format("YYYY MMMM D","utc");params.logpage="Wikipedia:Files for discussion/"+date;params.discussionpage=params.logpage+"#"+Morebits.pageNameNorm;params.tagText="{{ffd|log="+date+"|help=off}}\n";if(pageobj.canEdit()){text=Twinkle.removeMoveToCommonsTagsFromWikicode(text);pageobj.setPageText(params.tagText+text);pageobj.setEditSummary("Listed for discussion at [[:"+params.discussionpage+"]].");pageobj.setChangeTags(Twinkle.changeTags);pageobj.setWatchlist(Twinkle.getPref("xfdWatchPage"));pageobj.setCreateOption("recreate");pageobj.save()}else{Twinkle.xfd.callbacks.autoEditRequest(pageobj,params)}Morebits.wiki.actionCompleted.redirect=params.logpage;Morebits.wiki.actionCompleted.notice="Nomination completed, now redirecting to the discussion page";const wikipedia_page=new Morebits.wiki.Page(mw.config.get("wgPageName"));wikipedia_page.setCallbackParameters(params);wikipedia_page.lookupCreation(Twinkle.xfd.callbacks.ffd.main)},main:function(pageobj){const params=pageobj.getCallbackParameters();const initialContrib=pageobj.getCreator();params.uploader=initialContrib;const wikipedia_page=new Morebits.wiki.Page(params.logpage,"Adding discussion to today's list");wikipedia_page.setFollowRedirect(true);wikipedia_page.setCallbackParameters(params);wikipedia_page.load(Twinkle.xfd.callbacks.ffd.todaysList);if(params.notifycreator){Twinkle.xfd.callbacks.notifyUser(params,initialContrib)}else{Twinkle.xfd.callbacks.addToLog(params,null)}},todaysList:function(pageobj){let text=pageobj.getPageText();const params=pageobj.getCallbackParameters();if(!pageobj.exists()){text="{{subst:FfD log}}"}pageobj.setPageText(text+"\n\n"+Twinkle.xfd.callbacks.getDiscussionWikitext("ffd",params));pageobj.setEditSummary("Adding [[:"+Morebits.pageNameNorm+"]].");pageobj.setChangeTags(Twinkle.changeTags);pageobj.setWatchlist(Twinkle.getPref("xfdWatchDiscussion"));pageobj.setCreateOption("recreate");pageobj.save(()=>{Twinkle.xfd.currentRationale=null})}},cfd:{main:function(pageobj){const params=pageobj.getCallbackParameters();const date=new Morebits.Date(pageobj.getLoadTime());params.logpage="Wikipedia:Categories for discussion/Log/"+date.format("YYYY MMMM D","utc");params.discussionpage=params.logpage+"#"+Morebits.pageNameNorm;pageobj.setCallbackParameters(params);Twinkle.xfd.callbacks.cfd.taggingCategory(pageobj);Morebits.wiki.actionCompleted.redirect=params.logpage;Morebits.wiki.actionCompleted.notice="Nomination completed, now redirecting to today's log";let wikipedia_page=new Morebits.wiki.Page(params.logpage,"Adding discussion to today's list");wikipedia_page.setFollowRedirect(true);wikipedia_page.setCallbackParameters(params);wikipedia_page.load(Twinkle.xfd.callbacks.cfd.todaysList);if(params.notifycreator){wikipedia_page=new Morebits.wiki.Page(mw.config.get("wgPageName"));wikipedia_page.setCallbackParameters(params);wikipedia_page.lookupCreation(pageobj=>{Twinkle.xfd.callbacks.notifyUser(pageobj.getCallbackParameters(),pageobj.getCreator())})}else{Twinkle.xfd.callbacks.addToLog(params,null)}},taggingCategory:function(pageobj){const text=pageobj.getPageText();const params=pageobj.getCallbackParameters();params.tagText="{{subst:"+params.xfdcat;let editsummary=(mw.config.get("wgNamespaceNumber")===14?"Category":"Stub template")+" being considered for "+params.action;switch(params.xfdcat){case"cfd":case"sfd-t":break;case"cfc":editsummary+=" to an article";case"cfm":case"cfr":case"sfr-t":params.tagText+="|"+params.cfdtarget;break;case"cfs":params.tagText+="|"+params.cfdtarget+"|"+params.cfdtarget2;break;default:alert("twinklexfd in taggingCategory(): unknown CFD action");break}params.tagText+="}}\n";editsummary+="; see [[:"+params.discussionpage+"]].";if(pageobj.canEdit()){pageobj.setPageText(params.tagText+text);pageobj.setEditSummary(editsummary);pageobj.setChangeTags(Twinkle.changeTags);pageobj.setWatchlist(Twinkle.getPref("xfdWatchPage"));pageobj.setCreateOption("recreate");pageobj.save()}else{Twinkle.xfd.callbacks.autoEditRequest(pageobj,params)}},todaysList:function(pageobj){const params=pageobj.getCallbackParameters();const statelem=pageobj.getStatusElement();const added_data=Twinkle.xfd.callbacks.getDiscussionWikitext(params.xfdcat,params);let text;if(!pageobj.exists()){text="{{subst:CfD log}}\n"+added_data}else{const old_text=pageobj.getPageText();text=old_text.replace("below this line --\x3e","below this line --\x3e\n"+added_data);if(text===old_text){statelem.error("failed to find target spot for the discussion");return}}pageobj.setPageText(text);pageobj.setEditSummary("Adding "+params.action+" nomination of [[:"+Morebits.pageNameNorm+"]].");pageobj.setChangeTags(Twinkle.changeTags);pageobj.setWatchlist(Twinkle.getPref("xfdWatchDiscussion"));pageobj.setCreateOption("recreate");pageobj.save(()=>{Twinkle.xfd.currentRationale=null})}},cfds:{taggingCategory:function(pageobj){const text=pageobj.getPageText();const params=pageobj.getCallbackParameters();if(params.xfdcat==="C2F"){params.tagText="{{subst:cfm-speedy|1="+params.cfdstarget.replace(/^:?Category:/,"")+"}}\n"}else{params.tagText="{{subst:cfr-speedy|1="+params.cfdstarget.replace(/^:?Category:/,"")+"}}\n"}params.discussionpage="";if(pageobj.canEdit()){pageobj.setPageText(params.tagText+text);pageobj.setEditSummary("Listed for speedy renaming; see [[WP:CFDS|Categories for discussion/Speedy]].");pageobj.setChangeTags(Twinkle.changeTags);pageobj.setWatchlist(Twinkle.getPref("xfdWatchPage"));pageobj.setCreateOption("recreate");pageobj.save(()=>{Twinkle.xfd.callbacks.addToLog(params,null)})}else{Twinkle.xfd.callbacks.autoEditRequest(pageobj,params);Twinkle.xfd.callbacks.addToLog(params,null)}},addToList:function(pageobj){const old_text=pageobj.getPageText();const params=pageobj.getCallbackParameters();const statelem=pageobj.getStatusElement();const text=old_text.replace("BELOW THIS LINE --\x3e","BELOW THIS LINE --\x3e\n"+Twinkle.xfd.callbacks.getDiscussionWikitext("cfds",params));if(text===old_text){statelem.error("failed to find target spot for the discussion");return}pageobj.setPageText(text);pageobj.setEditSummary("Adding [[:"+Morebits.pageNameNorm+"]].");pageobj.setChangeTags(Twinkle.changeTags);pageobj.setWatchlist(Twinkle.getPref("xfdWatchDiscussion"));pageobj.setCreateOption("recreate");pageobj.setDiscussionToolsAutoSubscribe(false);pageobj.save(()=>{Twinkle.xfd.currentRationale=null})}},rfd:{findTarget:function(params,callback){const query={action:"query",curtimestamp:true,format:"json"};if(document.getElementById("softredirect")){params.rfdtarget=document.getElementById("softredirect").textContent.replace(/^:+/,"")}else{query.titles=mw.config.get("wgPageName");query.redirects=true}const wikipedia_api=new Morebits.wiki.Api("Finding target of redirect",query,Twinkle.xfd.callbacks.rfd.findTargetCallback(callback));wikipedia_api.params=params;wikipedia_api.post()},findTargetCallback:function(callback){return function(apiobj){const response=apiobj.getResponse();apiobj.params.curtimestamp=response.curtimestamp;if(!apiobj.params.rfdtarget){const target=response.query.redirects&&response.query.redirects[0].to;if(!target){let message="No target found. this page does not appear to be a redirect, aborting";if(mw.config.get("wgAction")==="history"){message+=". If this is a soft redirect, try again from the content page, not the page history."}apiobj.statelem.error(message);return}apiobj.params.rfdtarget=target;const section=response.query.redirects[0].tofragment;apiobj.params.section=section}callback(apiobj.params)}},main:function(params){const date=new Morebits.Date(params.curtimestamp);params.logpage="Wikipedia:Redirects for discussion/Log/"+date.format("YYYY MMMM D","utc");params.discussionpage=params.logpage+"#"+Morebits.pageNameNorm;let wikipedia_page=new Morebits.wiki.Page(mw.config.get("wgPageName"),"Adding deletion tag to redirect");wikipedia_page.setFollowRedirect(false);wikipedia_page.setCallbackParameters(params);wikipedia_page.load(Twinkle.xfd.callbacks.rfd.taggingRedirect);Morebits.wiki.actionCompleted.redirect=params.logpage;Morebits.wiki.actionCompleted.notice="Nomination completed, now redirecting to today's log";wikipedia_page=new Morebits.wiki.Page(params.logpage,"Adding discussion to today's log");wikipedia_page.setFollowRedirect(true);wikipedia_page.setCallbackParameters(params);wikipedia_page.load(Twinkle.xfd.callbacks.rfd.todaysList);if(params.notifycreator||params.relatedpage){const thispage=new Morebits.wiki.Page(mw.config.get("wgPageName"));thispage.setCallbackParameters(params);thispage.lookupCreation(Twinkle.xfd.callbacks.rfd.sendNotifications)}else{Twinkle.xfd.callbacks.addToLog(params,null)}},taggingRedirect:function(pageobj){const text=pageobj.getPageText();const params=pageobj.getCallbackParameters();params.tagText="{{subst:rfd|"+(mw.config.get("wgNamespaceNumber")===10?"showontransclusion=1|":"")+"content=\n";if(pageobj.canEdit()){pageobj.setPageText(params.tagText+text+"\n}}");pageobj.setEditSummary("Listed for discussion at [[:"+params.discussionpage+"]].");pageobj.setChangeTags(Twinkle.changeTags);pageobj.setWatchlist(Twinkle.getPref("xfdWatchPage"));pageobj.setCreateOption("nocreate");pageobj.save()}else{Twinkle.xfd.callbacks.autoEditRequest(pageobj,params)}},todaysList:function(pageobj){const params=pageobj.getCallbackParameters();const statelem=pageobj.getStatusElement();const added_data=Twinkle.xfd.callbacks.getDiscussionWikitext("rfd",params);let text;if(!pageobj.exists()){text="{{subst:RfD log}}"+added_data}else{const old_text=pageobj.getPageText();text=old_text.replace(/(<!-- Add new entries directly below this line\.? -->)/,"$1\n"+added_data);if(text===old_text){statelem.error("failed to find target spot for the discussion");return}}pageobj.setPageText(text);pageobj.setEditSummary("Adding [[:"+Morebits.pageNameNorm+"]].");pageobj.setChangeTags(Twinkle.changeTags);pageobj.setWatchlist(Twinkle.getPref("xfdWatchDiscussion"));pageobj.setCreateOption("recreate");pageobj.save(()=>{Twinkle.xfd.currentRationale=null})},sendNotifications:function(pageobj){const initialContrib=pageobj.getCreator();const params=pageobj.getCallbackParameters();const statelem=pageobj.getStatusElement();if(params.notifycreator){Twinkle.xfd.callbacks.notifyUser(params,initialContrib)}if(params.relatedpage){const targetTalk=new mw.Title(params.rfdtarget).getTalkPage();if(params.rfdtarget===mw.config.get("wgPageName")){statelem.warn("Circular redirect; skipping target page notification")}else if(document.getElementById("softredirect")){statelem.warn("Soft redirect; skipping target page notification")}else if(targetTalk.getNamespaceId()===3&&targetTalk.getMainText()===initialContrib){statelem.warn("Target is initial contributor; skipping target page notification")}else if(targetTalk.getNamespaceId()===3&&targetTalk.getMainText()===mw.config.get("wgUserName")){statelem.warn("You ("+mw.config.get("wgUserName")+") are the target; skipping target page notification")}else{Twinkle.xfd.callbacks.notifyUser(params,targetTalk.toText(),params.notifycreator,"Notifying redirect target of the discussion");return}if(!params.notifycreator){Twinkle.xfd.callbacks.addToLog(params,null)}}}},rm:{listAtTalk:function(pageobj){const params=pageobj.getCallbackParameters();params.discussionpage=pageobj.getPageName();pageobj.setAppendText("\n\n"+Twinkle.xfd.callbacks.getDiscussionWikitext("rm",params));pageobj.setEditSummary(`Proposing move of ${params.currentname.map((currentname,i)=>`[[:${currentname}]]${params.newname[i]?` to [[:${params.newname[i]}]]`:""}`).join(", ")}.`);pageobj.setChangeTags(Twinkle.changeTags);pageobj.setCreateOption("recreate");pageobj.setWatchlist(Twinkle.getPref("xfdWatchDiscussion"));pageobj.append(()=>{Twinkle.xfd.currentRationale=null;Twinkle.xfd.callbacks.addToLog(params,null)})},listAtRMTR:function(pageobj){const text=pageobj.getPageText();const params=pageobj.getCallbackParameters();const statelem=pageobj.getStatusElement();const discussionWikitext=Twinkle.xfd.callbacks.getDiscussionWikitext("rm",params);const newtext=Twinkle.xfd.insertRMTR(text,discussionWikitext);if(text===newtext){statelem.error("failed to find target spot for the entry");return}pageobj.setPageText(newtext);pageobj.setEditSummary(`Adding [[:${params.currentname.join("]], [[:")}]].`);pageobj.setChangeTags(Twinkle.changeTags);pageobj.save(()=>{Twinkle.xfd.currentRationale=null;Twinkle.xfd.callbacks.addToLog(params,null)})}}};Twinkle.xfd.insertRMTR=function(pageWikitext,wikitextToInsert){const placementRE=/\n{1,}(====[^\n]*Requests to revert undiscussed moves ?====)/i;return pageWikitext.replace(placementRE,"\n"+wikitextToInsert+"\n\n$1")};Twinkle.xfd.callback.evaluate=function(e){const form=e.target;const params=Morebits.QuickForm.getInputData(form);Morebits.SimpleWindow.setButtonsEnabled(false);Morebits.Status.init(form);Twinkle.xfd.currentRationale=params.reason;Morebits.Status.onError(Twinkle.xfd.printRationale);let query,wikipedia_page,wikipedia_api;switch(params.venue){case"afd":query={action:"query",list:"allpages",apprefix:"Articles for deletion/"+Morebits.pageNameNorm,apnamespace:4,apfilterredir:"nonredirects",aplimit:"max",format:"json"};wikipedia_api=new Morebits.wiki.Api("Tagging article with deletion tag",query,Twinkle.xfd.callbacks.afd.main);wikipedia_api.params=params;wikipedia_api.post();break;case"tfd":if(params.tfdtarget){params.tfdtarget=utils.stripNs(params.tfdtarget)}params.scribunto=mw.config.get("wgPageContentModel")==="Scribunto";if(params.xfdcat==="tfm"){if(params.scribunto){wikipedia_page=new Morebits.wiki.Page(mw.config.get("wgPageName")+"/doc","Tagging this module documentation with merge tag");params.otherTemplateName="Module:"+params.tfdtarget}else{wikipedia_page=new Morebits.wiki.Page(mw.config.get("wgPageName"),"Tagging this template with merge tag");params.otherTemplateName="Template:"+params.tfdtarget}}else{if(params.scribunto){wikipedia_page=new Morebits.wiki.Page(mw.config.get("wgPageName")+"/doc","Tagging module documentation with deletion tag")}else{wikipedia_page=new Morebits.wiki.Page(mw.config.get("wgPageName"),"Tagging template with deletion tag")}}wikipedia_page.setFollowRedirect(true);wikipedia_page.setCallbackParameters(params);wikipedia_page.load(Twinkle.xfd.callbacks.tfd.main);break;case"mfd":query={action:"query",list:"allpages",apprefix:"Miscellany for deletion/"+Morebits.pageNameNorm,apnamespace:4,apfilterredir:"nonredirects",aplimit:"max",format:"json"};wikipedia_api=new Morebits.wiki.Api("Looking for prior nominations of this page",query,Twinkle.xfd.callbacks.mfd.main);wikipedia_api.params=params;wikipedia_api.post();break;case"ffd":wikipedia_page=new Morebits.wiki.Page(mw.config.get("wgPageName"),"Adding deletion tag to file page");wikipedia_page.setFollowRedirect(true);wikipedia_page.setCallbackParameters(params);wikipedia_page.load(Twinkle.xfd.callbacks.ffd.taggingImage);break;case"cfd":if(params.cfdtarget){params.cfdtarget=utils.stripNs(params.cfdtarget)}else{params.cfdtarget=""}if(params.cfdtarget2){params.cfdtarget2=utils.stripNs(params.cfdtarget2)}var summaryActions={cfd:"deletion","sfd-t":"deletion",cfm:"merging",cfr:"renaming","sfr-t":"renaming",cfs:"splitting",cfc:"conversion"};params.action=summaryActions[params.xfdcat];wikipedia_page=new Morebits.wiki.Page(mw.config.get("wgPageName"),"Tagging category with "+params.action+" tag");wikipedia_page.setFollowRedirect(true);wikipedia_page.setCallbackParameters(params);wikipedia_page.load(Twinkle.xfd.callbacks.cfd.main);break;case"cfds":params.cfdstarget=utils.addNs(params.cfdstarget,14);var logpage="Wikipedia:Categories for discussion/Speedy";Morebits.wiki.actionCompleted.redirect=logpage;Morebits.wiki.actionCompleted.notice="Nomination completed, now redirecting to the discussion page";wikipedia_page=new Morebits.wiki.Page(mw.config.get("wgPageName"),"Tagging category with rename tag");wikipedia_page.setFollowRedirect(true);wikipedia_page.setCallbackParameters(params);wikipedia_page.load(Twinkle.xfd.callbacks.cfds.taggingCategory);wikipedia_page=new Morebits.wiki.Page(logpage,"Adding discussion to the list");wikipedia_page.setFollowRedirect(true);wikipedia_page.setCallbackParameters(params);wikipedia_page.load(Twinkle.xfd.callbacks.cfds.addToList);break;case"rfd":Twinkle.xfd.callbacks.rfd.findTarget(params,Twinkle.xfd.callbacks.rfd.main);break;case"rm":var nomPageName=params.rmtr?"Wikipedia:Requested moves/Technical requests":new mw.Title(Morebits.pageNameNorm).getTalkPage().toText();Morebits.wiki.actionCompleted.redirect=nomPageName;Morebits.wiki.actionCompleted.notice="Nomination completed, now redirecting to the discussion page";wikipedia_page=new Morebits.wiki.Page(nomPageName,params.rmtr?"Adding entry at WP:RM/TR":"Adding entry on talk page");wikipedia_page.setFollowRedirect(true);wikipedia_page.setCallbackParameters(params);if(params.rmtr){wikipedia_page.load(Twinkle.xfd.callbacks.rm.listAtRMTR)}else{Twinkle.xfd.callbacks.rm.listAtTalk(wikipedia_page)}break;default:alert("twinklexfd: unknown XFD discussion venue");break}};Twinkle.addInitCallback(Twinkle.xfd,"xfd")})();
// </nowiki>
brvcrk260a3l4ui86xmpbviu8defpo0
User:Syunsyunminmin/Twinkle/twinkleimage.js
2
179405
769470
2026-10-21T13:58:18Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 3680108:
769470
javascript
text/javascript
/*
Twinkle: adapted for Japanese Wikipedia; upstream authors and Japanese adaptations are credited in UPSTREAM.md.
Upstream source: https://github.com/wikimedia-gadgets/twinkle/blob/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/src/modules/twinkleimage.js
Upstream author history: https://github.com/wikimedia-gadgets/twinkle/commits/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/src/modules/twinkleimage.js
License: CC BY-SA 3.0 Unported. See https://github.com/wikimedia-gadgets/twinkle/blob/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/LICENSE.txt
Japanese adaptation: Syunsyunminmin; includes later synchronization changes.
*/
// <nowiki>
(function() {
/*
****************************************
*** twinkleimage.js: Image CSD module
****************************************
* Mode of invocation: Tab ("DI")
* Active on: Local nonredirect file pages (not on Commons)
*/
Twinkle.image = function twinkleimage() {
if (mw.config.get('wgNamespaceNumber') === 6 && mw.config.get('wgArticleId') && !document.getElementById('mw-sharedupload') && !Morebits.isPageRedirect()) {
Twinkle.addPortletLink(Twinkle.image.callback, 'DI', 'tw-di', '保留期間がある即時削除対象のファイルを指定');
}
};
Twinkle.image.callback = function twinkleimageCallback() {
var Window = new Morebits.SimpleWindow(600, 330);
Window.setTitle('日付入り即時削除依頼');
Window.setScriptName('Twinkle');
Window.addFooterLink('即時削除の方針', 'WP:CSD#F');
Window.addFooterLink('Image設定', 'User:Syunsyunminmin/Twinkle/Preferences#image');
Window.addFooterLink('Twinkle help', ':en:WP:TW/DOC#image');
Window.addFooterLink('フィードバック', 'User talk:Syunsyunminmin/Twinkle');
var form = new Morebits.QuickForm(Twinkle.image.callback.evaluate);
form.append({
type: 'checkbox',
list: [
{
label: 'アップロード者に通知',
value: 'notify',
name: 'notify',
tooltip: '同じ利用者から複数の指定を行う予定で、その利用者の会話ページに多くの通知を送りたくない場合は、このチェックを外してください。',
checked: Twinkle.getPref('notifyUserOnDeli')
}
]
}
);
var field = form.append({
type: 'field',
label: '行おうとしている操作'
});
field.append({
type: 'radio',
name: 'type',
list: [
{
label: '出典情報なし (CSD F5)',
value: 'no source',
checked: true,
tooltip: '出典についての情報がないファイル'
},
{
label: 'ライセンス情報なし (CSD F5)',
value: 'no license',
tooltip: '著作権状態についての情報がないファイル'
}
]
});
form.append({ type: 'submit' });
var result = form.render();
Window.setContent(result);
Window.display();
// We must init the parameters
var evt = document.createEvent('Event');
evt.initEvent('change', true, true);
result.type[0].dispatchEvent(evt);
};
Twinkle.image.callback.evaluate = function twinkleimageCallbackEvaluate(event) {
var input = Morebits.QuickForm.getInputData(event.target);
if (input.replacement) {
input.replacement = (new RegExp('^' + Morebits.namespaceRegex(6) + ':', 'i').test(input.replacement) ? '' : 'File:') + input.replacement;
}
var csdcrit, notifytemplatename = '';
switch (input.type) {
case 'no source':
notifytemplatename += 'Image source';
csdcrit = 'F5';
break;
case 'no license':
notifytemplatename += 'Image copyright';
csdcrit = 'F5';
break;
default:
throw new Error('Twinkle.image.callback.evaluate: 不明な基準');
}
var lognomination = Twinkle.getPref('logSpeedyNominations') && !Twinkle.getPref('noLogOnSpeedyNomination').includes(csdcrit.toLowerCase());
var templatename = input.type;
var params = $.extend({
templatename: templatename,
notifytemplatename: notifytemplatename,
normalized: csdcrit,
lognomination: lognomination
}, input);
Morebits.SimpleWindow.setButtonsEnabled(false);
Morebits.Status.init(event.target);
Morebits.wiki.actionCompleted.redirect = mw.config.get('wgPageName');
Morebits.wiki.actionCompleted.notice = 'タグ付け完了';
// Tagging image
var wikipedia_page = new Morebits.wiki.Page(mw.config.get('wgPageName'), 'ファイルに画像削除タグを付ける');
wikipedia_page.setCallbackParameters(params);
wikipedia_page.load(Twinkle.image.callbacks.taggingImage);
// Notifying uploader
if (input.notify) {
wikipedia_page.lookupCreation(Twinkle.image.callbacks.userNotification);
} else {
// add to CSD log if desired
if (lognomination) {
Twinkle.image.callbacks.addToLog(params, null);
}
// No auto-notification, display what was going to be added.
var noteData = document.createElement('pre');
noteData.appendChild(document.createTextNode('{{subst:' + notifytemplatename + '|1=' + mw.config.get('wgTitle') + '}} --~~~~'));
Morebits.Status.info('通知', ['以下のようなデータは、元のアップロード者に投稿されるべきです:', document.createElement('br'), noteData]);
}
};
Twinkle.image.callbacks = {
taggingImage: function(pageobj) {
var text = pageobj.getPageText();
var params = pageobj.getCallbackParameters();
// remove "move to Commons" tag - deletion-tagged files cannot be moved to Commons
text = Twinkle.removeMoveToCommonsTagsFromWikicode(text);
var tag = '{{subst:' + params.templatename + '}}\n';
switch (params.type) {
default:
break; // doesn't matter
}
pageobj.setPageText(tag + text);
pageobj.setEditSummary('このファイルは[[WP:CSD#' + params.normalized + '|CSD ' + params.normalized + ']]に従い削除される予定です (' + params.type + ')' + Twinkle.summaryAd);
pageobj.setWatchlist(Twinkle.getPref('deliWatchPage'));
pageobj.setCreateOption('nocreate');
pageobj.save();
},
userNotification: function(pageobj) {
var params = pageobj.getCallbackParameters();
var initialContrib = pageobj.getCreator();
// disallow warning yourself
if (initialContrib === mw.config.get('wgUserName')) {
pageobj.getStatusElement().warn('あなた (' + initialContrib + ') がこのページを作成しました。通知をスキップします。');
} else {
var usertalkpage = new Morebits.wiki.Page('User talk:' + initialContrib, 'このページの立項者 (' + initialContrib + ') へ通知');
var notifytext = '\n{{subst:' + params.notifytemplatename + '|1=' + mw.config.get('wgTitle') + '}} --~~~~';
usertalkpage.setAppendText(notifytext);
usertalkpage.setEditSummary('通知: [[:' + Morebits.pageNameNorm + ']] の削除へのタグ付け' + Twinkle.summaryAd);
usertalkpage.setCreateOption('recreate');
usertalkpage.setWatchlist(Twinkle.getPref('deliWatchUser'));
usertalkpage.setFollowRedirect(true, false);
usertalkpage.append();
}
// add this nomination to the user's userspace log, if the user has enabled it
if (params.lognomination) {
Twinkle.image.callbacks.addToLog(params, initialContrib);
}
},
addToLog: function(params, initialContrib) {
var usl = new Morebits.UserspaceLogger(Twinkle.getPref('speedyLogPageName'));
usl.initialText =
'{{Notice|style=announce|text=即時削除の際、このページへのリンクは考慮する必要はありません。}}\n' +
'<!-- このテンプレートは、[[Wikipedia:即時削除の方針#リダイレクト]]にリンク元がある場合は削除しないよう定めてあるため、このページがリンクしていることによる影響を避けるためのものです。 -->\n\n' +
'このページは[[H:TW|Twinkle]]のCSDモジュールによって生成されており、[[WP:CSD|即時削除]]に指定したページを記録しています。\n\n' +
'記録をこれ以上保持したくない場合、[[利用者:Syunsyunminmin/Twinkle/Preferences|設定パネル]]を使用してこの機能をオフにした後、' +
'[[WP:CSD#U1|CSD U1]]でこのページの即時削除を依頼することが出来ます。' +
(Morebits.userIsSysop ? '\n\nこのログは、Twinkleを使用して行われた即時削除を完全に追跡してはいません。' : '');
var formatParamLog = function(normalize, csdparam, input) {
return ' {' + normalize + ' ' + csdparam + ': ' + input + '}';
};
var extraInfo = '';
// If a logged file is deleted but exists on commons, the wikilink will be blue, so provide a link to the log
var fileLogLink = ' ([{{fullurl:Special:Log|page=' + mw.util.wikiUrlencode(mw.config.get('wgPageName')) + '}} 記録])';
var appendText = '# [[:' + Morebits.pageNameNorm + ']]' + fileLogLink + ': DI [[WP:CSD#' + params.normalized.toUpperCase() + '|CSD ' + params.normalized.toUpperCase() + ']] ({{tl|' + params.templatename + '}})';
['reason', 'replacement', 'source'].forEach((item) => {
if (params[item]) {
extraInfo += formatParamLog(params.normalized.toUpperCase(), item, params[item]);
return false;
}
});
if (extraInfo) {
appendText += '; 追加情報:' + extraInfo;
}
if (initialContrib) {
appendText += '; {{user|1=' + initialContrib + '}}に通知';
}
appendText += ' ~~~~~\n';
var editsummary = '[[:' + Morebits.pageNameNorm + ']]の即時削除への指定を記録' + Twinkle.summaryAd;
usl.log(appendText, editsummary);
}
};
Twinkle.addInitCallback(Twinkle.image, 'image');
}(jQuery));
// </nowiki>
scjhcmuyrwd2m7gzpjl4uv9o3kmm0gi
769537
769470
2026-10-22T00:00:58Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 1406828: rm cmt
769537
javascript
text/javascript
// <nowiki>
(function() {
/*
****************************************
*** twinkleimage.js: Image CSD module
****************************************
* Mode of invocation: Tab ("DI")
* Active on: Local nonredirect file pages (not on Commons)
*/
Twinkle.image = function twinkleimage() {
if (mw.config.get('wgNamespaceNumber') === 6 && mw.config.get('wgArticleId') && !document.getElementById('mw-sharedupload') && !Morebits.isPageRedirect()) {
Twinkle.addPortletLink(Twinkle.image.callback, 'DI', 'tw-di', '保留期間がある即時削除対象のファイルを指定');
}
};
Twinkle.image.callback = function twinkleimageCallback() {
var Window = new Morebits.SimpleWindow(600, 330);
Window.setTitle('日付入り即時削除依頼');
Window.setScriptName('Twinkle');
Window.addFooterLink('即時削除の方針', 'WP:CSD#F');
Window.addFooterLink('Image設定', 'User:Syunsyunminmin/Twinkle/Preferences#image');
Window.addFooterLink('Twinkle help', ':en:WP:TW/DOC#image');
Window.addFooterLink('フィードバック', 'User talk:Syunsyunminmin/Twinkle');
var form = new Morebits.QuickForm(Twinkle.image.callback.evaluate);
form.append({
type: 'checkbox',
list: [
{
label: 'アップロード者に通知',
value: 'notify',
name: 'notify',
tooltip: '同じ利用者から複数の指定を行う予定で、その利用者の会話ページに多くの通知を送りたくない場合は、このチェックを外してください。',
checked: Twinkle.getPref('notifyUserOnDeli')
}
]
}
);
var field = form.append({
type: 'field',
label: '行おうとしている操作'
});
field.append({
type: 'radio',
name: 'type',
list: [
{
label: '出典情報なし (CSD F5)',
value: 'no source',
checked: true,
tooltip: '出典についての情報がないファイル'
},
{
label: 'ライセンス情報なし (CSD F5)',
value: 'no license',
tooltip: '著作権状態についての情報がないファイル'
}
]
});
form.append({ type: 'submit' });
var result = form.render();
Window.setContent(result);
Window.display();
// We must init the parameters
var evt = document.createEvent('Event');
evt.initEvent('change', true, true);
result.type[0].dispatchEvent(evt);
};
Twinkle.image.callback.evaluate = function twinkleimageCallbackEvaluate(event) {
var input = Morebits.QuickForm.getInputData(event.target);
if (input.replacement) {
input.replacement = (new RegExp('^' + Morebits.namespaceRegex(6) + ':', 'i').test(input.replacement) ? '' : 'File:') + input.replacement;
}
var csdcrit, notifytemplatename = '';
switch (input.type) {
case 'no source':
notifytemplatename += 'Image source';
csdcrit = 'F5';
break;
case 'no license':
notifytemplatename += 'Image copyright';
csdcrit = 'F5';
break;
default:
throw new Error('Twinkle.image.callback.evaluate: 不明な基準');
}
var lognomination = Twinkle.getPref('logSpeedyNominations') && !Twinkle.getPref('noLogOnSpeedyNomination').includes(csdcrit.toLowerCase());
var templatename = input.type;
var params = $.extend({
templatename: templatename,
notifytemplatename: notifytemplatename,
normalized: csdcrit,
lognomination: lognomination
}, input);
Morebits.SimpleWindow.setButtonsEnabled(false);
Morebits.Status.init(event.target);
Morebits.wiki.actionCompleted.redirect = mw.config.get('wgPageName');
Morebits.wiki.actionCompleted.notice = 'タグ付け完了';
// Tagging image
var wikipedia_page = new Morebits.wiki.Page(mw.config.get('wgPageName'), 'ファイルに画像削除タグを付ける');
wikipedia_page.setCallbackParameters(params);
wikipedia_page.load(Twinkle.image.callbacks.taggingImage);
// Notifying uploader
if (input.notify) {
wikipedia_page.lookupCreation(Twinkle.image.callbacks.userNotification);
} else {
// add to CSD log if desired
if (lognomination) {
Twinkle.image.callbacks.addToLog(params, null);
}
// No auto-notification, display what was going to be added.
var noteData = document.createElement('pre');
noteData.appendChild(document.createTextNode('{{subst:' + notifytemplatename + '|1=' + mw.config.get('wgTitle') + '}} --~~~~'));
Morebits.Status.info('通知', ['以下のようなデータは、元のアップロード者に投稿されるべきです:', document.createElement('br'), noteData]);
}
};
Twinkle.image.callbacks = {
taggingImage: function(pageobj) {
var text = pageobj.getPageText();
var params = pageobj.getCallbackParameters();
// remove "move to Commons" tag - deletion-tagged files cannot be moved to Commons
text = Twinkle.removeMoveToCommonsTagsFromWikicode(text);
var tag = '{{subst:' + params.templatename + '}}\n';
switch (params.type) {
default:
break; // doesn't matter
}
pageobj.setPageText(tag + text);
pageobj.setEditSummary('このファイルは[[WP:CSD#' + params.normalized + '|CSD ' + params.normalized + ']]に従い削除される予定です (' + params.type + ')' + Twinkle.summaryAd);
pageobj.setWatchlist(Twinkle.getPref('deliWatchPage'));
pageobj.setCreateOption('nocreate');
pageobj.save();
},
userNotification: function(pageobj) {
var params = pageobj.getCallbackParameters();
var initialContrib = pageobj.getCreator();
// disallow warning yourself
if (initialContrib === mw.config.get('wgUserName')) {
pageobj.getStatusElement().warn('あなた (' + initialContrib + ') がこのページを作成しました。通知をスキップします。');
} else {
var usertalkpage = new Morebits.wiki.Page('User talk:' + initialContrib, 'このページの立項者 (' + initialContrib + ') へ通知');
var notifytext = '\n{{subst:' + params.notifytemplatename + '|1=' + mw.config.get('wgTitle') + '}} --~~~~';
usertalkpage.setAppendText(notifytext);
usertalkpage.setEditSummary('通知: [[:' + Morebits.pageNameNorm + ']] の削除へのタグ付け' + Twinkle.summaryAd);
usertalkpage.setCreateOption('recreate');
usertalkpage.setWatchlist(Twinkle.getPref('deliWatchUser'));
usertalkpage.setFollowRedirect(true, false);
usertalkpage.append();
}
// add this nomination to the user's userspace log, if the user has enabled it
if (params.lognomination) {
Twinkle.image.callbacks.addToLog(params, initialContrib);
}
},
addToLog: function(params, initialContrib) {
var usl = new Morebits.UserspaceLogger(Twinkle.getPref('speedyLogPageName'));
usl.initialText =
'{{Notice|style=announce|text=即時削除の際、このページへのリンクは考慮する必要はありません。}}\n' +
'<!-- このテンプレートは、[[Wikipedia:即時削除の方針#リダイレクト]]にリンク元がある場合は削除しないよう定めてあるため、このページがリンクしていることによる影響を避けるためのものです。 -->\n\n' +
'このページは[[H:TW|Twinkle]]のCSDモジュールによって生成されており、[[WP:CSD|即時削除]]に指定したページを記録しています。\n\n' +
'記録をこれ以上保持したくない場合、[[利用者:Syunsyunminmin/Twinkle/Preferences|設定パネル]]を使用してこの機能をオフにした後、' +
'[[WP:CSD#U1|CSD U1]]でこのページの即時削除を依頼することが出来ます。' +
(Morebits.userIsSysop ? '\n\nこのログは、Twinkleを使用して行われた即時削除を完全に追跡してはいません。' : '');
var formatParamLog = function(normalize, csdparam, input) {
return ' {' + normalize + ' ' + csdparam + ': ' + input + '}';
};
var extraInfo = '';
// If a logged file is deleted but exists on commons, the wikilink will be blue, so provide a link to the log
var fileLogLink = ' ([{{fullurl:Special:Log|page=' + mw.util.wikiUrlencode(mw.config.get('wgPageName')) + '}} 記録])';
var appendText = '# [[:' + Morebits.pageNameNorm + ']]' + fileLogLink + ': DI [[WP:CSD#' + params.normalized.toUpperCase() + '|CSD ' + params.normalized.toUpperCase() + ']] ({{tl|' + params.templatename + '}})';
['reason', 'replacement', 'source'].forEach((item) => {
if (params[item]) {
extraInfo += formatParamLog(params.normalized.toUpperCase(), item, params[item]);
return false;
}
});
if (extraInfo) {
appendText += '; 追加情報:' + extraInfo;
}
if (initialContrib) {
appendText += '; {{user|1=' + initialContrib + '}}に通知';
}
appendText += ' ~~~~~\n';
var editsummary = '[[:' + Morebits.pageNameNorm + ']]の即時削除への指定を記録' + Twinkle.summaryAd;
usl.log(appendText, editsummary);
}
};
Twinkle.addInitCallback(Twinkle.image, 'image');
}(jQuery));
// </nowiki>
bskztqd9em8c4ggu8mrxuax1sw0ozpa
769574
769537
2026-10-22T02:28:49Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at d9cd3461baca43ac99ac2c24fb76a3aae6ddada9: minify
769574
javascript
text/javascript
// <nowiki>
(function(){Twinkle.image=function twinkleimage(){if(mw.config.get("wgNamespaceNumber")===6&&mw.config.get("wgArticleId")&&!document.getElementById("mw-sharedupload")&&!Morebits.isPageRedirect()){Twinkle.addPortletLink(Twinkle.image.callback,"DI","tw-di","保留期間がある即時削除対象のファイルを指定")}};Twinkle.image.callback=function twinkleimageCallback(){var Window=new Morebits.SimpleWindow(600,330);Window.setTitle("日付入り即時削除依頼");Window.setScriptName("Twinkle");Window.addFooterLink("即時削除の方針","WP:CSD#F");Window.addFooterLink("Image設定","User:Syunsyunminmin/Twinkle/Preferences#image");Window.addFooterLink("Twinkle help",":en:WP:TW/DOC#image");Window.addFooterLink("フィードバック","User talk:Syunsyunminmin/Twinkle");var form=new Morebits.QuickForm(Twinkle.image.callback.evaluate);form.append({type:"checkbox",list:[{label:"アップロード者に通知",value:"notify",name:"notify",tooltip:"同じ利用者から複数の指定を行う予定で、その利用者の会話ページに多くの通知を送りたくない場合は、このチェックを外してください。",checked:Twinkle.getPref("notifyUserOnDeli")}]});var field=form.append({type:"field",label:"行おうとしている操作"});field.append({type:"radio",name:"type",list:[{label:"出典情報なし (CSD F5)",value:"no source",checked:true,tooltip:"出典についての情報がないファイル"},{label:"ライセンス情報なし (CSD F5)",value:"no license",tooltip:"著作権状態についての情報がないファイル"}]});form.append({type:"submit"});var result=form.render();Window.setContent(result);Window.display();var evt=document.createEvent("Event");evt.initEvent("change",true,true);result.type[0].dispatchEvent(evt)};Twinkle.image.callback.evaluate=function twinkleimageCallbackEvaluate(event){var input=Morebits.QuickForm.getInputData(event.target);if(input.replacement){input.replacement=(new RegExp("^"+Morebits.namespaceRegex(6)+":","i").test(input.replacement)?"":"File:")+input.replacement}var csdcrit,notifytemplatename="";switch(input.type){case"no source":notifytemplatename+="Image source";csdcrit="F5";break;case"no license":notifytemplatename+="Image copyright";csdcrit="F5";break;default:throw new Error("Twinkle.image.callback.evaluate: 不明な基準")}var lognomination=Twinkle.getPref("logSpeedyNominations")&&!Twinkle.getPref("noLogOnSpeedyNomination").includes(csdcrit.toLowerCase());var templatename=input.type;var params=$.extend({templatename:templatename,notifytemplatename:notifytemplatename,normalized:csdcrit,lognomination:lognomination},input);Morebits.SimpleWindow.setButtonsEnabled(false);Morebits.Status.init(event.target);Morebits.wiki.actionCompleted.redirect=mw.config.get("wgPageName");Morebits.wiki.actionCompleted.notice="タグ付け完了";var wikipedia_page=new Morebits.wiki.Page(mw.config.get("wgPageName"),"ファイルに画像削除タグを付ける");wikipedia_page.setCallbackParameters(params);wikipedia_page.load(Twinkle.image.callbacks.taggingImage);if(input.notify){wikipedia_page.lookupCreation(Twinkle.image.callbacks.userNotification)}else{if(lognomination){Twinkle.image.callbacks.addToLog(params,null)}var noteData=document.createElement("pre");noteData.appendChild(document.createTextNode("{{subst:"+notifytemplatename+"|1="+mw.config.get("wgTitle")+"}} --~~~~"));Morebits.Status.info("通知",["以下のようなデータは、元のアップロード者に投稿されるべきです:",document.createElement("br"),noteData])}};Twinkle.image.callbacks={taggingImage:function(pageobj){var text=pageobj.getPageText();var params=pageobj.getCallbackParameters();text=Twinkle.removeMoveToCommonsTagsFromWikicode(text);var tag="{{subst:"+params.templatename+"}}\n";switch(params.type){default:break}pageobj.setPageText(tag+text);pageobj.setEditSummary("このファイルは[[WP:CSD#"+params.normalized+"|CSD "+params.normalized+"]]に従い削除される予定です ("+params.type+")"+Twinkle.summaryAd);pageobj.setWatchlist(Twinkle.getPref("deliWatchPage"));pageobj.setCreateOption("nocreate");pageobj.save()},userNotification:function(pageobj){var params=pageobj.getCallbackParameters();var initialContrib=pageobj.getCreator();if(initialContrib===mw.config.get("wgUserName")){pageobj.getStatusElement().warn("あなた ("+initialContrib+") がこのページを作成しました。通知をスキップします。")}else{var usertalkpage=new Morebits.wiki.Page("User talk:"+initialContrib,"このページの立項者 ("+initialContrib+") へ通知");var notifytext="\n{{subst:"+params.notifytemplatename+"|1="+mw.config.get("wgTitle")+"}} --~~~~";usertalkpage.setAppendText(notifytext);usertalkpage.setEditSummary("通知: [[:"+Morebits.pageNameNorm+"]] の削除へのタグ付け"+Twinkle.summaryAd);usertalkpage.setCreateOption("recreate");usertalkpage.setWatchlist(Twinkle.getPref("deliWatchUser"));usertalkpage.setFollowRedirect(true,false);usertalkpage.append()}if(params.lognomination){Twinkle.image.callbacks.addToLog(params,initialContrib)}},addToLog:function(params,initialContrib){var usl=new Morebits.UserspaceLogger(Twinkle.getPref("speedyLogPageName"));usl.initialText="{{Notice|style=announce|text=即時削除の際、このページへのリンクは考慮する必要はありません。}}\n"+"\x3c!-- このテンプレートは、[[Wikipedia:即時削除の方針#リダイレクト]]にリンク元がある場合は削除しないよう定めてあるため、このページがリンクしていることによる影響を避けるためのものです。 --\x3e\n\n"+"このページは[[H:TW|Twinkle]]のCSDモジュールによって生成されており、[[WP:CSD|即時削除]]に指定したページを記録しています。\n\n"+"記録をこれ以上保持したくない場合、[[利用者:Syunsyunminmin/Twinkle/Preferences|設定パネル]]を使用してこの機能をオフにした後、"+"[[WP:CSD#U1|CSD U1]]でこのページの即時削除を依頼することが出来ます。"+(Morebits.userIsSysop?"\n\nこのログは、Twinkleを使用して行われた即時削除を完全に追跡してはいません。":"");var formatParamLog=function(normalize,csdparam,input){return" {"+normalize+" "+csdparam+": "+input+"}"};var extraInfo="";var fileLogLink=" ([{{fullurl:Special:Log|page="+mw.util.wikiUrlencode(mw.config.get("wgPageName"))+"}} 記録])";var appendText="# [[:"+Morebits.pageNameNorm+"]]"+fileLogLink+": DI [[WP:CSD#"+params.normalized.toUpperCase()+"|CSD "+params.normalized.toUpperCase()+"]] ({{tl|"+params.templatename+"}})";["reason","replacement","source"].forEach(item=>{if(params[item]){extraInfo+=formatParamLog(params.normalized.toUpperCase(),item,params[item]);return false}});if(extraInfo){appendText+="; 追加情報:"+extraInfo}if(initialContrib){appendText+="; {{user|1="+initialContrib+"}}に通知"}appendText+=" ~~~~~\n";var editsummary="[[:"+Morebits.pageNameNorm+"]]の即時削除への指定を記録"+Twinkle.summaryAd;usl.log(appendText,editsummary)}};Twinkle.addInitCallback(Twinkle.image,"image")})(jQuery);
// </nowiki>
pv8q5qpltftu00xg2r2pan2ihdfzjfc
User:Syunsyunminmin/Twinkle/twinklearv.js
2
179406
769471
2026-10-21T13:58:20Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 3680108:
769471
javascript
text/javascript
/*
Twinkle: adapted for Japanese Wikipedia; upstream authors and Japanese adaptations are credited in UPSTREAM.md.
Upstream source: https://github.com/wikimedia-gadgets/twinkle/blob/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/src/modules/twinklearv.js
Upstream author history: https://github.com/wikimedia-gadgets/twinkle/commits/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/src/modules/twinklearv.js
License: CC BY-SA 3.0 Unported. See https://github.com/wikimedia-gadgets/twinkle/blob/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/LICENSE.txt
Japanese adaptation: Syunsyunminmin; includes later synchronization changes.
*/
// <nowiki>
(function() {
/*
****************************************
*** twinklearv.js: ARV module
****************************************
* Mode of invocation: Tab ("ARV")
* Active on: Any page with relevant user name (userspace, contribs, etc.)
*/
Twinkle.arv = function twinklearv() {
const username = mw.config.get('wgRelevantUserName');
if (!username || username === mw.config.get('wgUserName')) {
return;
}
const isIP = mw.util.isIPAddress(username, true);
// Ignore ranges wider than the CIDR limit
if (Morebits.ip.isRange(username) && !Morebits.ip.validCIDR(username)) {
return;
}
const userType = isIP ? 'IP' + (Morebits.ip.isRange(username) ? ' range' : '') : 'user';
Twinkle.addPortletLink(() => {
Twinkle.arv.callback(username, isIP);
}, 'ARV', 'tw-arv', 'Report ' + userType + ' to administrators');
};
Twinkle.arv.callback = function(uid, isIP) {
const Window = new Morebits.SimpleWindow(600, 500);
Window.setTitle('Advance Reporting and Vetting'); // Backronym
Window.setScriptName('Twinkle');
Window.addFooterLink('AIV guide', 'WP:GAIV');
Window.addFooterLink('UAA guide', 'WP:UAAI');
Window.addFooterLink('SPI guide', 'Wikipedia:Sockpuppet investigations/SPI/Guide to filing cases');
Window.addFooterLink('ARV prefs', 'WP:TW/PREF#arv');
Window.addFooterLink('Twinkle help', 'WP:TW/DOC#arv');
Window.addFooterLink('Give feedback', 'WT:TW');
const form = new Morebits.QuickForm(Twinkle.arv.callback.evaluate);
form.append({
type: 'select',
name: 'category',
label: 'Select report type:',
event: Twinkle.arv.callback.changeCategory,
list: [
{
label: 'Vandalism (WP:AIV)',
value: 'aiv'
},
{
label: 'Username (WP:UAA)',
value: 'username',
disabled: isIP
},
{
label: 'Sockpuppeteer (WP:SPI)',
value: 'sock'
},
{
label: 'Sockpuppet (WP:SPI)',
value: 'puppet'
},
{
label: 'Edit warring (WP:AN3)',
value: 'an3',
disabled: Morebits.ip.isRange(uid) // rvuser template doesn't support ranges
}
]
});
form.append({
type: 'div',
label: '',
style: 'color: red',
id: 'twinkle-arv-blockwarning'
});
// Temporary account notice
if (mw.config.get('wgRelevantUserName') && mw.util.isTemporaryUser(mw.config.get('wgRelevantUserName')) && !mw.config.get('wgCheckUserTemporaryAccountIPRevealAllowed')) {
const temporaryAccountNotice = form.append({
type: 'field',
label: 'Temporary account notice',
name: 'ta_notice',
style: 'color: var(--morebits-color-warning)'
});
temporaryAccountNotice.append({
type: 'div',
label: 'You are reporting a [[Wikipedia:Temporary accounts|temporary account]]. These accounts, with usernames like ~2025-12345-67, have replaced IP addresses. Please be careful when reporting for sockpuppetry and when blocking, because other users and temporary accounts on the same IP may end up as collateral damage.'
});
}
form.append({
type: 'field',
label: 'Work area',
name: 'work_area'
});
form.append({ type: 'submit' });
form.append({
type: 'hidden',
name: 'uid',
value: uid
});
const result = form.render();
Window.setContent(result);
Window.display();
// Check if the user is blocked, update notice
const query = {
action: 'query',
list: 'blocks',
bkprop: 'range|flags',
format: 'json'
};
if (isIP) {
query.bkip = uid;
} else {
query.bkusers = uid;
}
new Morebits.wiki.Api("Checking the user's block status", query, ((apiobj) => {
const blocklist = apiobj.getResponse().query.blocks;
if (blocklist.length) {
// If an IP is blocked *and* rangeblocked, only use whichever is more recent
const block = blocklist[0];
let message = (isIP ? 'This IP ' + (Morebits.ip.isRange(uid) ? 'range' : 'address') : 'This account') + ' is ' + (block.partial ? 'partially' : 'already') + ' blocked';
// Start and end differ, range blocked
message += block.rangestart !== block.rangeend ? ' as part of a rangeblock.' : '.';
if (block.partial) {
$('#twinkle-arv-blockwarning').css('color', 'black'); // Less severe
}
$('#twinkle-arv-blockwarning').text(message);
}
})).post();
// We must init the
const evt = document.createEvent('Event');
evt.initEvent('change', true, true);
result.category.dispatchEvent(evt);
};
Twinkle.arv.callback.changeCategory = function(e) {
const value = e.target.value;
const root = e.target.form;
const old_area = Morebits.QuickForm.getElements(root, 'work_area')[0];
let work_area = null;
switch (value) {
case 'aiv':
/* falls through */
default:
work_area = new Morebits.QuickForm.Element({
type: 'field',
label: 'Report user for vandalism',
name: 'work_area'
});
work_area.append({
type: 'input',
name: 'page',
label: 'Primary linked page:',
tooltip: 'Leave blank to not link to the page in the report',
value: Twinkle.getPrefill('vanarticle') || '',
event: function(e) {
const value = e.target.value;
const root = e.target.form;
if (value === '') {
root.badid.disabled = root.goodid.disabled = true;
} else {
root.badid.disabled = false;
root.goodid.disabled = root.badid.value === '';
}
}
});
work_area.append({
type: 'input',
name: 'badid',
label: 'Revision ID for target page when vandalised:',
tooltip: 'Leave blank for no diff link',
value: Twinkle.getPrefill('vanarticlerevid') || '',
disabled: !Twinkle.getPrefill('vanarticle'),
event: function(e) {
const value = e.target.value;
const root = e.target.form;
root.goodid.disabled = value === '';
}
});
work_area.append({
type: 'input',
name: 'goodid',
label: 'Last good revision ID before vandalism of target page:',
tooltip: 'Leave blank for diff link to previous revision',
value: Twinkle.getPrefill('vanarticlegoodrevid') || '',
disabled: !Twinkle.getPrefill('vanarticle') || Twinkle.getPrefill('vanarticlerevid')
});
work_area.append({
type: 'checkbox',
name: 'arvtype',
list: [
{
label: 'Vandalism after final (level 4 or 4im) warning given',
value: 'final'
},
{
label: 'Vandalism after recent (within 1 day) release of block',
value: 'postblock'
},
{
label: 'Evidently a vandalism-only account',
value: 'vandalonly',
disabled: mw.util.isIPAddress(root.uid.value, true)
},
{
label: 'Account is a promotion-only account',
value: 'promoonly',
disabled: mw.util.isIPAddress(root.uid.value, true)
},
{
label: 'Account is evidently a spambot or a compromised account',
value: 'spambot'
}
]
});
work_area.append({
type: 'textarea',
name: 'reason',
label: 'Comment:'
});
work_area = work_area.render();
old_area.parentNode.replaceChild(work_area, old_area);
break;
case 'username':
work_area = new Morebits.QuickForm.Element({
type: 'field',
label: 'Report username violation',
name: 'work_area'
});
work_area.append({
type: 'header',
label: 'Type(s) of inappropriate username',
tooltip: 'Wikipedia does not allow usernames that are misleading, promotional, offensive or disruptive. Domain names and email addresses are likewise prohibited. These criteria apply to both usernames and signatures. Usernames that are inappropriate in another language, or that represent an inappropriate name with misspellings and substitutions, or do so indirectly or by implication, are still considered inappropriate.'
});
work_area.append({
type: 'checkbox',
name: 'arvtype',
list: [
{
label: 'Misleading username',
value: 'misleading',
tooltip: 'Misleading usernames imply relevant, misleading things about the contributor. For example, misleading points of fact, an impression of undue authority, or usernames giving the impression of a bot account.'
},
{
label: 'Promotional username',
value: 'promotional',
tooltip: 'Promotional usernames are advertisements for a company, website or group. Please do not report these names to UAA unless the user has also made promotional edits related to the name.'
},
{
label: 'Offensive username',
value: 'offensive',
tooltip: 'Offensive usernames make harmonious editing difficult or impossible.'
},
{
label: 'Disruptive username',
value: 'disruptive',
tooltip: 'Disruptive usernames include outright trolling or personal attacks, or otherwise show a clear intent to disrupt Wikipedia.'
}
]
});
work_area.append({
type: 'textarea',
name: 'reason',
label: 'Comment:'
});
work_area = work_area.render();
old_area.parentNode.replaceChild(work_area, old_area);
break;
case 'puppet':
work_area = new Morebits.QuickForm.Element({
type: 'field',
label: 'Report suspected sockpuppet',
name: 'work_area'
});
work_area.append(
{
type: 'input',
name: 'sockmaster',
label: 'Sockpuppeteer',
tooltip: 'The username of the sockpuppeteer (sockmaster) without the "User:" prefix'
}
);
work_area.append({
type: 'textarea',
label: 'Evidence:',
name: 'evidence',
tooltip: 'Your evidence should make it clear that each of these users is likely to be abusing multiple accounts. Usually this means diffs, page histories or other information that justifies why the users are a) the same and b) disruptive. This should be just evidence and information needed to judge the matter. Avoid all other discussion that is not evidence of sockpuppetry.'
});
work_area = work_area.render();
old_area.parentNode.replaceChild(work_area, old_area);
break;
case 'sock':
work_area = new Morebits.QuickForm.Element({
type: 'field',
label: 'Report suspected sockpuppeteer',
name: 'work_area'
});
work_area.append(
{
type: 'dyninput',
name: 'sockpuppets',
label: 'Sockpuppets',
sublabel: 'Sock:',
tooltip: 'The username of the sockpuppet without the "User:" prefix',
min: 2
});
work_area.append({
type: 'textarea',
label: 'Evidence:',
name: 'evidence',
tooltip: 'Your evidence should make it clear that each of these users is likely to be abusing multiple accounts. Usually this means diffs, page histories or other information that justifies why the users are a) the same and b) disruptive. This should be just evidence and information needed to judge the matter. Avoid all other discussion that is not evidence of sockpuppetry.'
});
work_area = work_area.render();
old_area.parentNode.replaceChild(work_area, old_area);
break;
case 'an3':
work_area = new Morebits.QuickForm.Element({
type: 'field',
label: 'Report edit warring',
name: 'work_area'
});
work_area.append({
type: 'input',
name: 'page',
label: 'Page',
tooltip: 'The page being reported'
});
work_area.append({
type: 'button',
name: 'load',
label: 'Load',
event: function(e) {
const root = e.target.form;
const date = new Morebits.Date().subtract(48, 'hours'); // all since 48 hours
// Run for each AN3 field
const getAN3Entries = function(field, rvuser, titles) {
const $field = $(root).find('[name=' + field + ']');
$field.find('.entry').remove();
new mw.Api().get({
action: 'query',
prop: 'revisions',
format: 'json',
rvprop: 'sha1|ids|timestamp|parsedcomment|comment',
rvlimit: 500, // intentionally limited
rvend: date.toISOString(),
rvuser: rvuser,
indexpageids: true,
titles: titles
}).done((data) => {
const pageid = data.query.pageids[0];
const page = data.query.pages[pageid];
if (!page.revisions) {
$('<span>')
.addClass('entry')
.html('None found')
.appendTo($field);
} else {
for (let i = 0; i < page.revisions.length; ++i) {
const rev = page.revisions[i];
const $entry = $('<div>')
.addClass('entry');
const $input = $('<input>')
.attr({
type: 'checkbox',
name: 's_' + field,
value: JSON.stringify(rev)
});
$input.appendTo($entry);
let comment = '<span>';
// revdel/os
if (typeof rev.commenthidden === 'string') {
comment += '(comment hidden)';
} else {
comment += '"' + rev.parsedcomment + '"';
}
comment += ' at <a href="' + mw.config.get('wgScript') + '?diff=' + rev.revid + '">' + new Morebits.Date(rev.timestamp).calendar() + '</a></span>';
$entry.append(comment).appendTo($field);
}
}
// add free form input for resolves
if (field === 'resolves') {
const $free_entry = $('<div>')
.addClass('entry');
const $free_input = $('<input>')
.attr({
type: 'text',
name: 's_resolves_free'
});
const $free_label = $('<label>')
.attr('for', 's_resolves_free')
.html('URL link of diff with additional discussions: ');
$free_entry.append($free_label).append($free_input).appendTo($field);
}
}).fail(() => {
$('<span>')
.addClass('entry')
.html('API failure, reload page and try again')
.appendTo($field);
});
};
// warnings
const uid = root.uid.value;
getAN3Entries('warnings', mw.config.get('wgUserName'), 'User talk:' + uid);
// diffs and resolves require a valid page
const page = root.page.value;
if (page) {
// diffs
getAN3Entries('diffs', uid, page);
// resolutions
const t = new mw.Title(page);
const talk_page = t.getTalkPage().getPrefixedText();
getAN3Entries('resolves', mw.config.get('wgUserName'), talk_page);
} else {
$(root).find('[name=diffs]').find('.entry').remove();
$(root).find('[name=resolves]').find('.entry').remove();
}
}
});
work_area.append({
type: 'field',
name: 'diffs',
label: 'User\'s reverts (within last 48 hours)',
tooltip: 'Select the edits you believe are reverts'
});
work_area.append({
type: 'field',
name: 'warnings',
label: 'Warnings given to subject',
tooltip: 'You must have warned the subject before reporting'
});
work_area.append({
type: 'field',
name: 'resolves',
label: 'Resolution initiatives',
tooltip: 'You should have tried to resolve the issue on the talk page first'
});
work_area.append({
type: 'textarea',
label: 'Comment:',
name: 'comment'
});
work_area = work_area.render();
old_area.parentNode.replaceChild(work_area, old_area);
break;
}
};
Twinkle.arv.callback.evaluate = function(e) {
const form = e.target;
let reason = '';
const input = Morebits.QuickForm.getInputData(form);
switch (input.category) {
// Report user for vandalism
case 'aiv':
/* falls through */
default:
reason = Twinkle.arv.callback.getAivReasonWikitext(input);
if (reason === null) {
alert('You must specify some reason');
return;
}
Morebits.SimpleWindow.setButtonsEnabled(false);
Morebits.Status.init(form);
Morebits.wiki.actionCompleted.redirect = 'Wikipedia:Administrator intervention against vandalism';
Morebits.wiki.actionCompleted.notice = 'Reporting complete';
var aivPage = new Morebits.wiki.Page('Wikipedia:Administrator intervention against vandalism', 'Processing AIV request');
aivPage.setPageSection(1);
aivPage.setFollowRedirect(true);
aivPage.load(() => {
const text = aivPage.getPageText();
const $aivLink = '<a target="_blank" href="/wiki/WP:AIV">WP:AIV</a>';
// check if user has already been reported
if (new RegExp('\\{\\{\\s*(?:(?:[Ii][Pp])?[Vv]andal|[Uu]serlinks)\\s*\\|\\s*(?:1=)?\\s*' + Morebits.string.escapeRegExp(input.uid) + '\\s*\\}\\}').test(text)) {
aivPage.getStatusElement().error('Report already present, will not add a new one');
Morebits.Status.printUserText(reason, 'The comments you typed are provided below, in case you wish to manually post them under the existing report for this user at ' + $aivLink + ':');
return;
}
// then check for any bot reports
const tb2Page = new Morebits.wiki.Page('Wikipedia:Administrator intervention against vandalism/TB2', 'Checking bot reports');
tb2Page.load(() => {
const tb2Text = tb2Page.getPageText();
const tb2statelem = tb2Page.getStatusElement();
if (new RegExp('\\{\\{\\s*(?:(?:[Ii][Pp])?[Vv]andal|[Uu]serlinks)\\s*\\|\\s*(?:1=)?\\s*' + Morebits.string.escapeRegExp(input.uid) + '\\s*\\}\\}').test(tb2Text)) {
if (confirm('The user ' + input.uid + ' has already been reported by a bot. Do you wish to make the report anyway?')) {
tb2statelem.info('Proceeded despite bot report');
} else {
tb2statelem.error('Report from a bot is already present, stopping');
Morebits.Status.printUserText(reason, 'The comments you typed are provided below, in case you wish to manually post them at ' + $aivLink + ':');
return;
}
} else {
tb2statelem.info('No conflicting bot reports');
}
aivPage.getStatusElement().status('Adding new report...');
aivPage.setEditSummary('Reporting [[Special:Contributions/' + input.uid + '|' + input.uid + ']].');
aivPage.setChangeTags(Twinkle.changeTags);
aivPage.setAppendText(Twinkle.arv.callback.buildAivReport(input));
aivPage.setDiscussionToolsAutoSubscribe(false);
aivPage.append();
});
});
break;
// Report inappropriate username
case 'username':
var censorUsername = input.arvtype.includes('offensive'); // check if the username is marked offensive
reason = Twinkle.arv.callback.getUsernameReportWikitext(input);
Morebits.SimpleWindow.setButtonsEnabled(false);
Morebits.Status.init(form);
Morebits.wiki.actionCompleted.redirect = 'Wikipedia:Usernames for administrator attention';
Morebits.wiki.actionCompleted.notice = 'Reporting complete';
var uaaPage = new Morebits.wiki.Page('Wikipedia:Usernames for administrator attention', 'Processing UAA request');
uaaPage.setFollowRedirect(true);
uaaPage.load(() => {
const text = uaaPage.getPageText();
// check if user has already been reported
if (new RegExp('\\{\\{\\s*user-uaa\\s*\\|\\s*(1\\s*=\\s*)?' + Morebits.string.escapeRegExp(input.uid) + '\\s*(\\||\\})').test(text)) {
uaaPage.getStatusElement().error('User is already listed.');
const $uaaLink = '<a target="_blank" href="/wiki/WP:UAA">WP:UAA</a>';
Morebits.Status.printUserText(reason, 'The comments you typed are provided below, in case you wish to manually post them under the existing report for this user at ' + $uaaLink + ':');
return;
}
uaaPage.getStatusElement().status('Adding new report...');
uaaPage.setEditSummary('Reporting ' + (censorUsername ? 'an offensive username.' : '[[Special:Contributions/' + input.uid + '|' + input.uid + ']].'));
uaaPage.setChangeTags(Twinkle.changeTags);
// Blank newline per [[Special:Permalink/996949310#Spacing]]; see also [[WP:LISTGAP]] and [[WP:INDENTGAP]]
uaaPage.setPageText(text + '\n' + reason + '\n*');
uaaPage.setDiscussionToolsAutoSubscribe(false);
uaaPage.save();
});
break;
// WP:SPI
case 'sock':
/* falls through */
case 'puppet':
var reportData = Twinkle.arv.callback.getSpiReportData(input);
if (reportData.error) {
alert(reportData.error);
return;
}
Morebits.SimpleWindow.setButtonsEnabled(false);
Morebits.Status.init(form);
Morebits.wiki.addCheckpoint(); // prevent notification events from causing an erronous "action completed"
var reportpage = 'Wikipedia:Sockpuppet investigations/' + reportData.sockmaster;
Morebits.wiki.actionCompleted.redirect = reportpage;
Morebits.wiki.actionCompleted.notice = 'Reporting complete';
var spiPage = new Morebits.wiki.Page(reportpage, 'Retrieving discussion page');
spiPage.setFollowRedirect(true);
spiPage.setEditSummary('Adding new report for [[Special:Contributions/' + reportData.sockmaster + '|' + reportData.sockmaster + ']].');
spiPage.setChangeTags(Twinkle.changeTags);
spiPage.setAppendText(reportData.wikitext);
spiPage.setWatchlist(Twinkle.getPref('spiWatchReport'));
spiPage.setDiscussionToolsAutoSubscribe(false);
spiPage.append();
Morebits.wiki.removeCheckpoint(); // all page updates have been started
break;
case 'an3':
// prepare the AN3 report, then post and notify
Twinkle.arv.callback.getAn3ReportData(input).then((data) => {
// If there are any reasons why the user might want to cancel the report, check with them about each reason and cancel if they choose to
for (const confirmation of data.confirmations) {
if (!confirm(confirmation)) {
return;
}
}
Morebits.SimpleWindow.setButtonsEnabled(false);
Morebits.Status.init(form);
Morebits.wiki.addCheckpoint(); // prevent notification events from causing an erronous "action completed"
const reportpage = 'Wikipedia:Administrators\' noticeboard/Edit warring';
Morebits.wiki.actionCompleted.redirect = reportpage;
Morebits.wiki.actionCompleted.notice = 'Reporting complete';
const an3Page = new Morebits.wiki.Page(reportpage, 'Retrieving discussion page');
an3Page.setFollowRedirect(true);
an3Page.setEditSummary('Adding new report for [[Special:Contributions/' + data.uid + '|' + data.uid + ']].');
an3Page.setChangeTags(Twinkle.changeTags);
an3Page.setAppendText(data.reportWikitext);
an3Page.setDiscussionToolsAutoSubscribe(false);
an3Page.append();
// notify user
const notifyText = '\n\n{{subst:an3-notice|1=' + mw.util.wikiUrlencode(data.uid) + '|auto=1}} ~~~~';
const talkPage = new Morebits.wiki.Page('User talk:' + data.uid, 'Notifying edit warrior');
talkPage.setFollowRedirect(true);
talkPage.setEditSummary('Notifying about edit warring noticeboard discussion.');
talkPage.setChangeTags(Twinkle.changeTags);
talkPage.setAppendText(notifyText);
talkPage.append();
Morebits.wiki.removeCheckpoint(); // all page updates have been started
}).catch((error) => {
console.error('Error occurred while preparing AN3 report.', error); // eslint-disable-line no-console
alert('Error occurred while preparing AN3 report: ' + error.message);
});
break;
}
};
Twinkle.arv.callback.getAivReasonWikitext = function(input) {
let text = '';
let type = input.arvtype;
if (!type.length && input.reason === '') {
return null;
}
type = type.map((v) => {
switch (v) {
case 'final':
return 'vandalism after final warning';
case 'postblock':
return 'vandalism after recent release of block';
case 'vandalonly':
return 'actions evidently indicate a vandalism-only account';
case 'promoonly':
return 'account is being used only for promotional purposes';
case 'spambot':
return 'account is evidently a spambot or a compromised account';
default:
return 'unknown reason';
}
}).join('; ');
if (input.page !== '') {
// Allow links to redirects, files, and categories
text = 'On {{No redirect|' + input.page + '}}';
if (input.badid !== '') {
text += ' ({{diff|' + input.page + '|' + input.badid + '|' + input.goodid + '|diff}})';
}
text += ':';
}
if (type) {
text += ' ' + type;
}
if (input.reason !== '') {
const textEndsInPunctuationOrBlank = /([.?!;:]|^)$/.test(text);
text += textEndsInPunctuationOrBlank ? '' : '.';
const textIsBlank = text === '';
text += textIsBlank ? '' : ' ';
text += input.reason;
}
text = text.trim();
const textEndsInPunctuation = /[.?!;]$/.test(text);
if (!textEndsInPunctuation) {
text += '.';
}
text += ' ~~~~';
text = text.replace(/\r?\n/g, '\n*:'); // indent newlines
return text;
};
Twinkle.arv.callback.buildAivReport = function(input) {
return '\n*{{vandal|' + (/=/.test(input.uid) ? '1=' : '') + input.uid + '}} – ' + Twinkle.arv.callback.getAivReasonWikitext(input);
};
Twinkle.arv.callback.getUsernameReportWikitext = function(input) {
// generate human-readable string, e.g. "misleading and promotional username"
if (input.arvtype.length <= 2) {
input.arvtype = input.arvtype.join(' and ');
} else {
input.arvtype = [input.arvtype.slice(0, -1).join(', '), input.arvtype.slice(-1)].join(' and ');
}
// a or an?
let adjective = 'a';
if (/[aeiouwyh]/.test(input.arvtype[0] || '')) { // non 100% correct, but whatever, including 'h' for Cockney
adjective = 'an';
}
let text = '*{{user-uaa|1=' + input.uid + '}} – ';
if (input.arvtype.length) {
text += 'Violation of the username policy as ' + adjective + ' ' + input.arvtype + ' username. ';
}
if (input.reason !== '') {
text += Morebits.string.toUpperCaseFirstChar(input.reason);
const endsInPeriod = /\.$/.test(input.reason);
if (!endsInPeriod) {
text += '.';
}
text += ' ';
}
text += '~~~~';
text = text.replace(/\r?\n/g, '\n*:'); // indent newlines
return text;
};
Twinkle.arv.callback.getSpiReportData = function(input) {
const isPuppetReport = input.category === 'puppet';
if (!isPuppetReport) {
input.sockpuppets = input.sockpuppets.filter((sock) => sock !== ''); // ignore empty sockpuppet inputs
}
if (isPuppetReport && !input.sockmaster) {
return { error: 'You have not entered a sockmaster account for this puppet. Consider reporting this account as a sockpuppeteer instead.' };
} else if (!isPuppetReport && input.sockpuppets.length === 0) {
return { error: 'You have not entered any sockpuppet account(s) for this sockmaster. Consider reporting this account as a sockpuppet instead.' };
}
input.sockmaster = input.sockmaster || input.uid;
input.sockpuppets = isPuppetReport ? [input.uid] : Morebits.array.uniq(input.sockpuppets);
let text = '\n{{subst:SPI report|' +
input.sockpuppets.map((sock, index) => (index + 1) + '=' + sock).join('|') +
'\n|evidence=' + Morebits.string.formatReasonText(input.evidence) +
' \n';
text += '}}';
return {
sockmaster: input.sockmaster,
wikitext: text
};
};
Twinkle.arv.callback.getAn3ReportData = async function(input) {
const confirmations = [];
const diffs = input.s_diffs ? input.s_diffs.map(JSON.parse) : [];
if (diffs.length < 3) {
confirmations.push('You have selected fewer than three offending edits. Do you wish to make the report anyway?');
}
const warnings = input.s_warnings ? input.s_warnings.map(JSON.parse) : [];
if (!warnings.length) {
confirmations.push('You have not selected any edits where you warned the offender. Do you wish to make the report anyway?');
}
const resolves = input.s_resolves ? input.s_resolves.map(JSON.parse) : [];
const freeResolves = input.s_resolves_free;
let freeResolvesData;
if (freeResolves) {
let query;
let diff, oldid;
const specialDiff = /Special:Diff\/(\d+)(?:\/(\S+))?/i.exec(freeResolves);
if (specialDiff) {
if (specialDiff[2]) {
oldid = specialDiff[1];
diff = specialDiff[2];
} else {
diff = specialDiff[1];
}
} else {
diff = mw.util.getParamValue('diff', freeResolves);
oldid = mw.util.getParamValue('oldid', freeResolves);
}
const title = mw.util.getParamValue('title', freeResolves);
const diffNum = /^\d+$/.test(diff); // used repeatedly
// rvdiffto in prop=revisions is deprecated, but action=compare doesn't return
// timestamps ([[phab:T247686]]) so we can't rely on it unless necessary.
// Likewise, we can't rely on a meaningful comment for diff=cur.
// Additionally, links like Special:Diff/123/next, Special:Diff/123/456, or ?diff=next&oldid=123
// would each require making use of rvdir=newer in the revisions API.
// That requires a title parameter, so we have to use compare instead of revisions.
if (oldid && (diff === 'cur' || (!title && (diff === 'next' || diffNum)))) {
query = {
action: 'compare',
fromrev: oldid,
prop: 'ids|title',
format: 'json'
};
if (diffNum) {
query.torev = diff;
} else {
query.torelative = diff;
}
} else {
query = {
action: 'query',
prop: 'revisions',
rvprop: 'ids|timestamp|comment',
format: 'json',
indexpageids: true
};
if (diff && oldid) {
if (diff === 'prev') {
query.revids = oldid;
} else {
query.titles = title;
query.rvdir = 'newer';
query.rvstartid = oldid;
if (diff === 'next' && title) {
query.rvlimit = 2;
} else if (diffNum) {
// Diffs may or may not be consecutive, no limit
query.rvendid = diff;
}
}
} else {
// diff=next|prev|cur with no oldid
// Implies title= exists otherwise it's not a valid diff link (well, it is, but to the Main Page)
if (diff && /^\D+$/.test(diff)) {
query.titles = title;
} else {
query.revids = diff || oldid;
}
}
}
let queryResponse;
try {
queryResponse = await new mw.Api().get(query);
} catch (err) {
const error = new Error('Call to MediaWiki API failed.');
error.cause = err;
throw error;
}
if (queryResponse.compare && queryResponse.compare.fromtitle === queryResponse.compare.totitle) {
freeResolvesData = queryResponse;
} else if (queryResponse.query) {
const pageIds = queryResponse.query.pageids;
if (!Array.isArray(pageIds) || pageIds.length !== 1) {
const error = new Error('Error parsing diff.');
error.cause = queryResponse;
throw error;
}
freeResolvesData = queryResponse.query.pages[pageIds[0]];
} else {
const error = new Error('Could not find any diff associated with the URL provided.');
error.cause = queryResponse;
throw error;
}
} else {
freeResolvesData = undefined;
}
if (!resolves.length && !freeResolvesData) {
confirmations.push('You have not selected any edits where you tried to resolve the issue. Do you wish to make the report anyway?');
}
const data = {
uid: input.uid,
page: input.page,
comment: input.comment,
diffs: diffs,
warnings: warnings,
resolves: resolves,
free_resolves: freeResolvesData,
confirmations: confirmations
};
let minid;
for (let i = 0; i < data.diffs.length; ++i) {
if (data.diffs[i].parentid && (!minid || data.diffs[i].parentid < minid)) {
minid = data.diffs[i].parentid;
}
}
let queryResponse;
try {
queryResponse = await new mw.Api().get({
action: 'query',
prop: 'revisions',
format: 'json',
rvprop: 'sha1|ids|timestamp|comment',
rvlimit: 100, // intentionally limited
rvstartid: minid,
rvexcludeuser: data.uid,
indexpageids: true,
titles: data.page
});
} catch (err) {
const error = new Error('Call to MediaWiki API failed.');
error.cause = err;
throw error;
}
// In case an edit summary was revdel'd
const hasHiddenComment = function(rev) {
if (!rev.comment && typeof rev.commenthidden === 'string') {
return '(comment hidden)';
}
// swap curly braces for HTML entities to avoid templates being rendered if they were included in an edit summary
return '"' + rev.comment.replace(/\{/g, '{').replace(/\}/g, '}') + '"';
};
// TODO: The logic for filling the |orig= parameter in the report template seems to be broken. The API call does not seem to be parsed properly. It appears as if the data parameter of the callback is being used as if it were an array, but it isn't one - it's actually the entire API response object. I think adding queryResponse = queryResponse.query.pages[queryResponse.query.pageids[0]].revisions; after `let orig;` would convert it to the array it thinks it's dealing with, but I really have no idea what the actual goal is, so I can't figure out whether or not that's correct. -- Tollens
let orig;
if (queryResponse.length) {
const sha1 = queryResponse[0].sha1;
for (let i = 1; i < queryResponse.length; ++i) {
if (queryResponse[i].sha1 === sha1) {
orig = queryResponse[i];
break;
}
}
if (!orig) {
orig = queryResponse[0];
}
}
let origtext = '';
if (orig) {
origtext = '{{diff2|' + orig.revid + '|' + orig.timestamp + '}} ' + hasHiddenComment(orig);
}
const groupedDiffs = {};
let parentid, lastid;
for (let j = 0; j < data.diffs.length; ++j) {
const cur = data.diffs[j];
if ((cur.revid && cur.revid !== parentid) || lastid === null) {
lastid = cur.revid;
groupedDiffs[lastid] = [];
}
parentid = cur.parentid;
groupedDiffs[lastid].push(cur);
}
const difftext = $.map(groupedDiffs, (sub) => {
let ret = '';
if (sub.length >= 2) {
const last = sub[0];
const first = sub.slice(-1)[0];
const label = 'Consecutive edits made from ' + new Morebits.Date(first.timestamp).format('HH:mm, D MMMM YYYY', 'utc') + ' (UTC) to ' + new Morebits.Date(last.timestamp).format('HH:mm, D MMMM YYYY', 'utc') + ' (UTC)';
ret = '# {{diff|oldid=' + first.parentid + '|diff=' + last.revid + '|label=' + label + '}}\n';
}
ret += sub.reverse().map((v) => (sub.length >= 2 ? '#' : '') + '# {{diff2|' + v.revid + '|' + new Morebits.Date(v.timestamp).format('HH:mm, D MMMM YYYY', 'utc') + ' (UTC)}} ' + hasHiddenComment(v)).join('\n');
return ret;
}).reverse().join('\n');
const warningtext = data.warnings.reverse().map((v) => '# {{diff2|' + v.revid + '|' + new Morebits.Date(v.timestamp).format('HH:mm, D MMMM YYYY', 'utc') + ' (UTC)}} ' + hasHiddenComment(v)).join('\n');
let resolvetext = data.resolves.reverse().map((v) => '# {{diff2|' + v.revid + '|' + new Morebits.Date(v.timestamp).format('HH:mm, D MMMM YYYY', 'utc') + ' (UTC)}} ' + hasHiddenComment(v)).join('\n');
if (data.free_resolves) {
const page = data.free_resolves;
if (page.compare) {
resolvetext += '\n# {{diff|oldid=' + page.compare.fromrevid + '|diff=' + page.compare.torevid + '|label=Consecutive edits on ' + page.compare.totitle + '}}';
} else if (page.revisions) {
const revCount = page.revisions.length;
let rev;
if (revCount < 3) { // diff=prev or next
rev = revCount === 1 ? page.revisions[0] : page.revisions[1];
resolvetext += '\n# {{diff2|' + rev.revid + '|' + new Morebits.Date(rev.timestamp).format('HH:mm, D MMMM YYYY', 'utc') + ' (UTC) on ' + page.title + '}} ' + hasHiddenComment(rev);
} else { // diff and oldid are nonconsecutive
rev = page.revisions[0];
const revLatest = page.revisions[revCount - 1];
const label = 'Consecutive edits made from ' + new Morebits.Date(rev.timestamp).format('HH:mm, D MMMM YYYY', 'utc') + ' (UTC) to ' + new Morebits.Date(revLatest.timestamp).format('HH:mm, D MMMM YYYY', 'utc') + ' (UTC) on ' + page.title;
resolvetext += '\n# {{diff|oldid=' + rev.revid + '|diff=' + revLatest.revid + '|label=' + label + '}}\n';
}
}
}
let comment = data.comment.replace(/~*$/g, '').trim();
if (comment) {
comment += ' ~~~~';
}
const reportWikitext = '\n\n{{subst:AN3 report|diffs=' + difftext + '|warnings=' + warningtext + '|resolves=' + resolvetext + '|pagename=' + data.page + '|orig=' + origtext + '|comment=' + comment + '|uid=' + data.uid + '}}';
return {
uid: data.uid,
reportWikitext: reportWikitext,
confirmations: data.confirmations
};
};
Twinkle.addInitCallback(Twinkle.arv, 'arv');
}());
// </nowiki>
ql6um3zg0k7rflg8pwtbrqfscq09535
769538
769471
2026-10-22T00:01:02Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 1406828: rm cmt
769538
javascript
text/javascript
// <nowiki>
(function() {
/*
****************************************
*** twinklearv.js: ARV module
****************************************
* Mode of invocation: Tab ("ARV")
* Active on: Any page with relevant user name (userspace, contribs, etc.)
*/
Twinkle.arv = function twinklearv() {
const username = mw.config.get('wgRelevantUserName');
if (!username || username === mw.config.get('wgUserName')) {
return;
}
const isIP = mw.util.isIPAddress(username, true);
// Ignore ranges wider than the CIDR limit
if (Morebits.ip.isRange(username) && !Morebits.ip.validCIDR(username)) {
return;
}
const userType = isIP ? 'IP' + (Morebits.ip.isRange(username) ? ' range' : '') : 'user';
Twinkle.addPortletLink(() => {
Twinkle.arv.callback(username, isIP);
}, 'ARV', 'tw-arv', 'Report ' + userType + ' to administrators');
};
Twinkle.arv.callback = function(uid, isIP) {
const Window = new Morebits.SimpleWindow(600, 500);
Window.setTitle('Advance Reporting and Vetting'); // Backronym
Window.setScriptName('Twinkle');
Window.addFooterLink('AIV guide', 'WP:GAIV');
Window.addFooterLink('UAA guide', 'WP:UAAI');
Window.addFooterLink('SPI guide', 'Wikipedia:Sockpuppet investigations/SPI/Guide to filing cases');
Window.addFooterLink('ARV prefs', 'WP:TW/PREF#arv');
Window.addFooterLink('Twinkle help', 'WP:TW/DOC#arv');
Window.addFooterLink('Give feedback', 'WT:TW');
const form = new Morebits.QuickForm(Twinkle.arv.callback.evaluate);
form.append({
type: 'select',
name: 'category',
label: 'Select report type:',
event: Twinkle.arv.callback.changeCategory,
list: [
{
label: 'Vandalism (WP:AIV)',
value: 'aiv'
},
{
label: 'Username (WP:UAA)',
value: 'username',
disabled: isIP
},
{
label: 'Sockpuppeteer (WP:SPI)',
value: 'sock'
},
{
label: 'Sockpuppet (WP:SPI)',
value: 'puppet'
},
{
label: 'Edit warring (WP:AN3)',
value: 'an3',
disabled: Morebits.ip.isRange(uid) // rvuser template doesn't support ranges
}
]
});
form.append({
type: 'div',
label: '',
style: 'color: red',
id: 'twinkle-arv-blockwarning'
});
// Temporary account notice
if (mw.config.get('wgRelevantUserName') && mw.util.isTemporaryUser(mw.config.get('wgRelevantUserName')) && !mw.config.get('wgCheckUserTemporaryAccountIPRevealAllowed')) {
const temporaryAccountNotice = form.append({
type: 'field',
label: 'Temporary account notice',
name: 'ta_notice',
style: 'color: var(--morebits-color-warning)'
});
temporaryAccountNotice.append({
type: 'div',
label: 'You are reporting a [[Wikipedia:Temporary accounts|temporary account]]. These accounts, with usernames like ~2025-12345-67, have replaced IP addresses. Please be careful when reporting for sockpuppetry and when blocking, because other users and temporary accounts on the same IP may end up as collateral damage.'
});
}
form.append({
type: 'field',
label: 'Work area',
name: 'work_area'
});
form.append({ type: 'submit' });
form.append({
type: 'hidden',
name: 'uid',
value: uid
});
const result = form.render();
Window.setContent(result);
Window.display();
// Check if the user is blocked, update notice
const query = {
action: 'query',
list: 'blocks',
bkprop: 'range|flags',
format: 'json'
};
if (isIP) {
query.bkip = uid;
} else {
query.bkusers = uid;
}
new Morebits.wiki.Api("Checking the user's block status", query, ((apiobj) => {
const blocklist = apiobj.getResponse().query.blocks;
if (blocklist.length) {
// If an IP is blocked *and* rangeblocked, only use whichever is more recent
const block = blocklist[0];
let message = (isIP ? 'This IP ' + (Morebits.ip.isRange(uid) ? 'range' : 'address') : 'This account') + ' is ' + (block.partial ? 'partially' : 'already') + ' blocked';
// Start and end differ, range blocked
message += block.rangestart !== block.rangeend ? ' as part of a rangeblock.' : '.';
if (block.partial) {
$('#twinkle-arv-blockwarning').css('color', 'black'); // Less severe
}
$('#twinkle-arv-blockwarning').text(message);
}
})).post();
// We must init the
const evt = document.createEvent('Event');
evt.initEvent('change', true, true);
result.category.dispatchEvent(evt);
};
Twinkle.arv.callback.changeCategory = function(e) {
const value = e.target.value;
const root = e.target.form;
const old_area = Morebits.QuickForm.getElements(root, 'work_area')[0];
let work_area = null;
switch (value) {
case 'aiv':
/* falls through */
default:
work_area = new Morebits.QuickForm.Element({
type: 'field',
label: 'Report user for vandalism',
name: 'work_area'
});
work_area.append({
type: 'input',
name: 'page',
label: 'Primary linked page:',
tooltip: 'Leave blank to not link to the page in the report',
value: Twinkle.getPrefill('vanarticle') || '',
event: function(e) {
const value = e.target.value;
const root = e.target.form;
if (value === '') {
root.badid.disabled = root.goodid.disabled = true;
} else {
root.badid.disabled = false;
root.goodid.disabled = root.badid.value === '';
}
}
});
work_area.append({
type: 'input',
name: 'badid',
label: 'Revision ID for target page when vandalised:',
tooltip: 'Leave blank for no diff link',
value: Twinkle.getPrefill('vanarticlerevid') || '',
disabled: !Twinkle.getPrefill('vanarticle'),
event: function(e) {
const value = e.target.value;
const root = e.target.form;
root.goodid.disabled = value === '';
}
});
work_area.append({
type: 'input',
name: 'goodid',
label: 'Last good revision ID before vandalism of target page:',
tooltip: 'Leave blank for diff link to previous revision',
value: Twinkle.getPrefill('vanarticlegoodrevid') || '',
disabled: !Twinkle.getPrefill('vanarticle') || Twinkle.getPrefill('vanarticlerevid')
});
work_area.append({
type: 'checkbox',
name: 'arvtype',
list: [
{
label: 'Vandalism after final (level 4 or 4im) warning given',
value: 'final'
},
{
label: 'Vandalism after recent (within 1 day) release of block',
value: 'postblock'
},
{
label: 'Evidently a vandalism-only account',
value: 'vandalonly',
disabled: mw.util.isIPAddress(root.uid.value, true)
},
{
label: 'Account is a promotion-only account',
value: 'promoonly',
disabled: mw.util.isIPAddress(root.uid.value, true)
},
{
label: 'Account is evidently a spambot or a compromised account',
value: 'spambot'
}
]
});
work_area.append({
type: 'textarea',
name: 'reason',
label: 'Comment:'
});
work_area = work_area.render();
old_area.parentNode.replaceChild(work_area, old_area);
break;
case 'username':
work_area = new Morebits.QuickForm.Element({
type: 'field',
label: 'Report username violation',
name: 'work_area'
});
work_area.append({
type: 'header',
label: 'Type(s) of inappropriate username',
tooltip: 'Wikipedia does not allow usernames that are misleading, promotional, offensive or disruptive. Domain names and email addresses are likewise prohibited. These criteria apply to both usernames and signatures. Usernames that are inappropriate in another language, or that represent an inappropriate name with misspellings and substitutions, or do so indirectly or by implication, are still considered inappropriate.'
});
work_area.append({
type: 'checkbox',
name: 'arvtype',
list: [
{
label: 'Misleading username',
value: 'misleading',
tooltip: 'Misleading usernames imply relevant, misleading things about the contributor. For example, misleading points of fact, an impression of undue authority, or usernames giving the impression of a bot account.'
},
{
label: 'Promotional username',
value: 'promotional',
tooltip: 'Promotional usernames are advertisements for a company, website or group. Please do not report these names to UAA unless the user has also made promotional edits related to the name.'
},
{
label: 'Offensive username',
value: 'offensive',
tooltip: 'Offensive usernames make harmonious editing difficult or impossible.'
},
{
label: 'Disruptive username',
value: 'disruptive',
tooltip: 'Disruptive usernames include outright trolling or personal attacks, or otherwise show a clear intent to disrupt Wikipedia.'
}
]
});
work_area.append({
type: 'textarea',
name: 'reason',
label: 'Comment:'
});
work_area = work_area.render();
old_area.parentNode.replaceChild(work_area, old_area);
break;
case 'puppet':
work_area = new Morebits.QuickForm.Element({
type: 'field',
label: 'Report suspected sockpuppet',
name: 'work_area'
});
work_area.append(
{
type: 'input',
name: 'sockmaster',
label: 'Sockpuppeteer',
tooltip: 'The username of the sockpuppeteer (sockmaster) without the "User:" prefix'
}
);
work_area.append({
type: 'textarea',
label: 'Evidence:',
name: 'evidence',
tooltip: 'Your evidence should make it clear that each of these users is likely to be abusing multiple accounts. Usually this means diffs, page histories or other information that justifies why the users are a) the same and b) disruptive. This should be just evidence and information needed to judge the matter. Avoid all other discussion that is not evidence of sockpuppetry.'
});
work_area = work_area.render();
old_area.parentNode.replaceChild(work_area, old_area);
break;
case 'sock':
work_area = new Morebits.QuickForm.Element({
type: 'field',
label: 'Report suspected sockpuppeteer',
name: 'work_area'
});
work_area.append(
{
type: 'dyninput',
name: 'sockpuppets',
label: 'Sockpuppets',
sublabel: 'Sock:',
tooltip: 'The username of the sockpuppet without the "User:" prefix',
min: 2
});
work_area.append({
type: 'textarea',
label: 'Evidence:',
name: 'evidence',
tooltip: 'Your evidence should make it clear that each of these users is likely to be abusing multiple accounts. Usually this means diffs, page histories or other information that justifies why the users are a) the same and b) disruptive. This should be just evidence and information needed to judge the matter. Avoid all other discussion that is not evidence of sockpuppetry.'
});
work_area = work_area.render();
old_area.parentNode.replaceChild(work_area, old_area);
break;
case 'an3':
work_area = new Morebits.QuickForm.Element({
type: 'field',
label: 'Report edit warring',
name: 'work_area'
});
work_area.append({
type: 'input',
name: 'page',
label: 'Page',
tooltip: 'The page being reported'
});
work_area.append({
type: 'button',
name: 'load',
label: 'Load',
event: function(e) {
const root = e.target.form;
const date = new Morebits.Date().subtract(48, 'hours'); // all since 48 hours
// Run for each AN3 field
const getAN3Entries = function(field, rvuser, titles) {
const $field = $(root).find('[name=' + field + ']');
$field.find('.entry').remove();
new mw.Api().get({
action: 'query',
prop: 'revisions',
format: 'json',
rvprop: 'sha1|ids|timestamp|parsedcomment|comment',
rvlimit: 500, // intentionally limited
rvend: date.toISOString(),
rvuser: rvuser,
indexpageids: true,
titles: titles
}).done((data) => {
const pageid = data.query.pageids[0];
const page = data.query.pages[pageid];
if (!page.revisions) {
$('<span>')
.addClass('entry')
.html('None found')
.appendTo($field);
} else {
for (let i = 0; i < page.revisions.length; ++i) {
const rev = page.revisions[i];
const $entry = $('<div>')
.addClass('entry');
const $input = $('<input>')
.attr({
type: 'checkbox',
name: 's_' + field,
value: JSON.stringify(rev)
});
$input.appendTo($entry);
let comment = '<span>';
// revdel/os
if (typeof rev.commenthidden === 'string') {
comment += '(comment hidden)';
} else {
comment += '"' + rev.parsedcomment + '"';
}
comment += ' at <a href="' + mw.config.get('wgScript') + '?diff=' + rev.revid + '">' + new Morebits.Date(rev.timestamp).calendar() + '</a></span>';
$entry.append(comment).appendTo($field);
}
}
// add free form input for resolves
if (field === 'resolves') {
const $free_entry = $('<div>')
.addClass('entry');
const $free_input = $('<input>')
.attr({
type: 'text',
name: 's_resolves_free'
});
const $free_label = $('<label>')
.attr('for', 's_resolves_free')
.html('URL link of diff with additional discussions: ');
$free_entry.append($free_label).append($free_input).appendTo($field);
}
}).fail(() => {
$('<span>')
.addClass('entry')
.html('API failure, reload page and try again')
.appendTo($field);
});
};
// warnings
const uid = root.uid.value;
getAN3Entries('warnings', mw.config.get('wgUserName'), 'User talk:' + uid);
// diffs and resolves require a valid page
const page = root.page.value;
if (page) {
// diffs
getAN3Entries('diffs', uid, page);
// resolutions
const t = new mw.Title(page);
const talk_page = t.getTalkPage().getPrefixedText();
getAN3Entries('resolves', mw.config.get('wgUserName'), talk_page);
} else {
$(root).find('[name=diffs]').find('.entry').remove();
$(root).find('[name=resolves]').find('.entry').remove();
}
}
});
work_area.append({
type: 'field',
name: 'diffs',
label: 'User\'s reverts (within last 48 hours)',
tooltip: 'Select the edits you believe are reverts'
});
work_area.append({
type: 'field',
name: 'warnings',
label: 'Warnings given to subject',
tooltip: 'You must have warned the subject before reporting'
});
work_area.append({
type: 'field',
name: 'resolves',
label: 'Resolution initiatives',
tooltip: 'You should have tried to resolve the issue on the talk page first'
});
work_area.append({
type: 'textarea',
label: 'Comment:',
name: 'comment'
});
work_area = work_area.render();
old_area.parentNode.replaceChild(work_area, old_area);
break;
}
};
Twinkle.arv.callback.evaluate = function(e) {
const form = e.target;
let reason = '';
const input = Morebits.QuickForm.getInputData(form);
switch (input.category) {
// Report user for vandalism
case 'aiv':
/* falls through */
default:
reason = Twinkle.arv.callback.getAivReasonWikitext(input);
if (reason === null) {
alert('You must specify some reason');
return;
}
Morebits.SimpleWindow.setButtonsEnabled(false);
Morebits.Status.init(form);
Morebits.wiki.actionCompleted.redirect = 'Wikipedia:Administrator intervention against vandalism';
Morebits.wiki.actionCompleted.notice = 'Reporting complete';
var aivPage = new Morebits.wiki.Page('Wikipedia:Administrator intervention against vandalism', 'Processing AIV request');
aivPage.setPageSection(1);
aivPage.setFollowRedirect(true);
aivPage.load(() => {
const text = aivPage.getPageText();
const $aivLink = '<a target="_blank" href="/wiki/WP:AIV">WP:AIV</a>';
// check if user has already been reported
if (new RegExp('\\{\\{\\s*(?:(?:[Ii][Pp])?[Vv]andal|[Uu]serlinks)\\s*\\|\\s*(?:1=)?\\s*' + Morebits.string.escapeRegExp(input.uid) + '\\s*\\}\\}').test(text)) {
aivPage.getStatusElement().error('Report already present, will not add a new one');
Morebits.Status.printUserText(reason, 'The comments you typed are provided below, in case you wish to manually post them under the existing report for this user at ' + $aivLink + ':');
return;
}
// then check for any bot reports
const tb2Page = new Morebits.wiki.Page('Wikipedia:Administrator intervention against vandalism/TB2', 'Checking bot reports');
tb2Page.load(() => {
const tb2Text = tb2Page.getPageText();
const tb2statelem = tb2Page.getStatusElement();
if (new RegExp('\\{\\{\\s*(?:(?:[Ii][Pp])?[Vv]andal|[Uu]serlinks)\\s*\\|\\s*(?:1=)?\\s*' + Morebits.string.escapeRegExp(input.uid) + '\\s*\\}\\}').test(tb2Text)) {
if (confirm('The user ' + input.uid + ' has already been reported by a bot. Do you wish to make the report anyway?')) {
tb2statelem.info('Proceeded despite bot report');
} else {
tb2statelem.error('Report from a bot is already present, stopping');
Morebits.Status.printUserText(reason, 'The comments you typed are provided below, in case you wish to manually post them at ' + $aivLink + ':');
return;
}
} else {
tb2statelem.info('No conflicting bot reports');
}
aivPage.getStatusElement().status('Adding new report...');
aivPage.setEditSummary('Reporting [[Special:Contributions/' + input.uid + '|' + input.uid + ']].');
aivPage.setChangeTags(Twinkle.changeTags);
aivPage.setAppendText(Twinkle.arv.callback.buildAivReport(input));
aivPage.setDiscussionToolsAutoSubscribe(false);
aivPage.append();
});
});
break;
// Report inappropriate username
case 'username':
var censorUsername = input.arvtype.includes('offensive'); // check if the username is marked offensive
reason = Twinkle.arv.callback.getUsernameReportWikitext(input);
Morebits.SimpleWindow.setButtonsEnabled(false);
Morebits.Status.init(form);
Morebits.wiki.actionCompleted.redirect = 'Wikipedia:Usernames for administrator attention';
Morebits.wiki.actionCompleted.notice = 'Reporting complete';
var uaaPage = new Morebits.wiki.Page('Wikipedia:Usernames for administrator attention', 'Processing UAA request');
uaaPage.setFollowRedirect(true);
uaaPage.load(() => {
const text = uaaPage.getPageText();
// check if user has already been reported
if (new RegExp('\\{\\{\\s*user-uaa\\s*\\|\\s*(1\\s*=\\s*)?' + Morebits.string.escapeRegExp(input.uid) + '\\s*(\\||\\})').test(text)) {
uaaPage.getStatusElement().error('User is already listed.');
const $uaaLink = '<a target="_blank" href="/wiki/WP:UAA">WP:UAA</a>';
Morebits.Status.printUserText(reason, 'The comments you typed are provided below, in case you wish to manually post them under the existing report for this user at ' + $uaaLink + ':');
return;
}
uaaPage.getStatusElement().status('Adding new report...');
uaaPage.setEditSummary('Reporting ' + (censorUsername ? 'an offensive username.' : '[[Special:Contributions/' + input.uid + '|' + input.uid + ']].'));
uaaPage.setChangeTags(Twinkle.changeTags);
// Blank newline per [[Special:Permalink/996949310#Spacing]]; see also [[WP:LISTGAP]] and [[WP:INDENTGAP]]
uaaPage.setPageText(text + '\n' + reason + '\n*');
uaaPage.setDiscussionToolsAutoSubscribe(false);
uaaPage.save();
});
break;
// WP:SPI
case 'sock':
/* falls through */
case 'puppet':
var reportData = Twinkle.arv.callback.getSpiReportData(input);
if (reportData.error) {
alert(reportData.error);
return;
}
Morebits.SimpleWindow.setButtonsEnabled(false);
Morebits.Status.init(form);
Morebits.wiki.addCheckpoint(); // prevent notification events from causing an erronous "action completed"
var reportpage = 'Wikipedia:Sockpuppet investigations/' + reportData.sockmaster;
Morebits.wiki.actionCompleted.redirect = reportpage;
Morebits.wiki.actionCompleted.notice = 'Reporting complete';
var spiPage = new Morebits.wiki.Page(reportpage, 'Retrieving discussion page');
spiPage.setFollowRedirect(true);
spiPage.setEditSummary('Adding new report for [[Special:Contributions/' + reportData.sockmaster + '|' + reportData.sockmaster + ']].');
spiPage.setChangeTags(Twinkle.changeTags);
spiPage.setAppendText(reportData.wikitext);
spiPage.setWatchlist(Twinkle.getPref('spiWatchReport'));
spiPage.setDiscussionToolsAutoSubscribe(false);
spiPage.append();
Morebits.wiki.removeCheckpoint(); // all page updates have been started
break;
case 'an3':
// prepare the AN3 report, then post and notify
Twinkle.arv.callback.getAn3ReportData(input).then((data) => {
// If there are any reasons why the user might want to cancel the report, check with them about each reason and cancel if they choose to
for (const confirmation of data.confirmations) {
if (!confirm(confirmation)) {
return;
}
}
Morebits.SimpleWindow.setButtonsEnabled(false);
Morebits.Status.init(form);
Morebits.wiki.addCheckpoint(); // prevent notification events from causing an erronous "action completed"
const reportpage = 'Wikipedia:Administrators\' noticeboard/Edit warring';
Morebits.wiki.actionCompleted.redirect = reportpage;
Morebits.wiki.actionCompleted.notice = 'Reporting complete';
const an3Page = new Morebits.wiki.Page(reportpage, 'Retrieving discussion page');
an3Page.setFollowRedirect(true);
an3Page.setEditSummary('Adding new report for [[Special:Contributions/' + data.uid + '|' + data.uid + ']].');
an3Page.setChangeTags(Twinkle.changeTags);
an3Page.setAppendText(data.reportWikitext);
an3Page.setDiscussionToolsAutoSubscribe(false);
an3Page.append();
// notify user
const notifyText = '\n\n{{subst:an3-notice|1=' + mw.util.wikiUrlencode(data.uid) + '|auto=1}} ~~~~';
const talkPage = new Morebits.wiki.Page('User talk:' + data.uid, 'Notifying edit warrior');
talkPage.setFollowRedirect(true);
talkPage.setEditSummary('Notifying about edit warring noticeboard discussion.');
talkPage.setChangeTags(Twinkle.changeTags);
talkPage.setAppendText(notifyText);
talkPage.append();
Morebits.wiki.removeCheckpoint(); // all page updates have been started
}).catch((error) => {
console.error('Error occurred while preparing AN3 report.', error); // eslint-disable-line no-console
alert('Error occurred while preparing AN3 report: ' + error.message);
});
break;
}
};
Twinkle.arv.callback.getAivReasonWikitext = function(input) {
let text = '';
let type = input.arvtype;
if (!type.length && input.reason === '') {
return null;
}
type = type.map((v) => {
switch (v) {
case 'final':
return 'vandalism after final warning';
case 'postblock':
return 'vandalism after recent release of block';
case 'vandalonly':
return 'actions evidently indicate a vandalism-only account';
case 'promoonly':
return 'account is being used only for promotional purposes';
case 'spambot':
return 'account is evidently a spambot or a compromised account';
default:
return 'unknown reason';
}
}).join('; ');
if (input.page !== '') {
// Allow links to redirects, files, and categories
text = 'On {{No redirect|' + input.page + '}}';
if (input.badid !== '') {
text += ' ({{diff|' + input.page + '|' + input.badid + '|' + input.goodid + '|diff}})';
}
text += ':';
}
if (type) {
text += ' ' + type;
}
if (input.reason !== '') {
const textEndsInPunctuationOrBlank = /([.?!;:]|^)$/.test(text);
text += textEndsInPunctuationOrBlank ? '' : '.';
const textIsBlank = text === '';
text += textIsBlank ? '' : ' ';
text += input.reason;
}
text = text.trim();
const textEndsInPunctuation = /[.?!;]$/.test(text);
if (!textEndsInPunctuation) {
text += '.';
}
text += ' ~~~~';
text = text.replace(/\r?\n/g, '\n*:'); // indent newlines
return text;
};
Twinkle.arv.callback.buildAivReport = function(input) {
return '\n*{{vandal|' + (/=/.test(input.uid) ? '1=' : '') + input.uid + '}} – ' + Twinkle.arv.callback.getAivReasonWikitext(input);
};
Twinkle.arv.callback.getUsernameReportWikitext = function(input) {
// generate human-readable string, e.g. "misleading and promotional username"
if (input.arvtype.length <= 2) {
input.arvtype = input.arvtype.join(' and ');
} else {
input.arvtype = [input.arvtype.slice(0, -1).join(', '), input.arvtype.slice(-1)].join(' and ');
}
// a or an?
let adjective = 'a';
if (/[aeiouwyh]/.test(input.arvtype[0] || '')) { // non 100% correct, but whatever, including 'h' for Cockney
adjective = 'an';
}
let text = '*{{user-uaa|1=' + input.uid + '}} – ';
if (input.arvtype.length) {
text += 'Violation of the username policy as ' + adjective + ' ' + input.arvtype + ' username. ';
}
if (input.reason !== '') {
text += Morebits.string.toUpperCaseFirstChar(input.reason);
const endsInPeriod = /\.$/.test(input.reason);
if (!endsInPeriod) {
text += '.';
}
text += ' ';
}
text += '~~~~';
text = text.replace(/\r?\n/g, '\n*:'); // indent newlines
return text;
};
Twinkle.arv.callback.getSpiReportData = function(input) {
const isPuppetReport = input.category === 'puppet';
if (!isPuppetReport) {
input.sockpuppets = input.sockpuppets.filter((sock) => sock !== ''); // ignore empty sockpuppet inputs
}
if (isPuppetReport && !input.sockmaster) {
return { error: 'You have not entered a sockmaster account for this puppet. Consider reporting this account as a sockpuppeteer instead.' };
} else if (!isPuppetReport && input.sockpuppets.length === 0) {
return { error: 'You have not entered any sockpuppet account(s) for this sockmaster. Consider reporting this account as a sockpuppet instead.' };
}
input.sockmaster = input.sockmaster || input.uid;
input.sockpuppets = isPuppetReport ? [input.uid] : Morebits.array.uniq(input.sockpuppets);
let text = '\n{{subst:SPI report|' +
input.sockpuppets.map((sock, index) => (index + 1) + '=' + sock).join('|') +
'\n|evidence=' + Morebits.string.formatReasonText(input.evidence) +
' \n';
text += '}}';
return {
sockmaster: input.sockmaster,
wikitext: text
};
};
Twinkle.arv.callback.getAn3ReportData = async function(input) {
const confirmations = [];
const diffs = input.s_diffs ? input.s_diffs.map(JSON.parse) : [];
if (diffs.length < 3) {
confirmations.push('You have selected fewer than three offending edits. Do you wish to make the report anyway?');
}
const warnings = input.s_warnings ? input.s_warnings.map(JSON.parse) : [];
if (!warnings.length) {
confirmations.push('You have not selected any edits where you warned the offender. Do you wish to make the report anyway?');
}
const resolves = input.s_resolves ? input.s_resolves.map(JSON.parse) : [];
const freeResolves = input.s_resolves_free;
let freeResolvesData;
if (freeResolves) {
let query;
let diff, oldid;
const specialDiff = /Special:Diff\/(\d+)(?:\/(\S+))?/i.exec(freeResolves);
if (specialDiff) {
if (specialDiff[2]) {
oldid = specialDiff[1];
diff = specialDiff[2];
} else {
diff = specialDiff[1];
}
} else {
diff = mw.util.getParamValue('diff', freeResolves);
oldid = mw.util.getParamValue('oldid', freeResolves);
}
const title = mw.util.getParamValue('title', freeResolves);
const diffNum = /^\d+$/.test(diff); // used repeatedly
// rvdiffto in prop=revisions is deprecated, but action=compare doesn't return
// timestamps ([[phab:T247686]]) so we can't rely on it unless necessary.
// Likewise, we can't rely on a meaningful comment for diff=cur.
// Additionally, links like Special:Diff/123/next, Special:Diff/123/456, or ?diff=next&oldid=123
// would each require making use of rvdir=newer in the revisions API.
// That requires a title parameter, so we have to use compare instead of revisions.
if (oldid && (diff === 'cur' || (!title && (diff === 'next' || diffNum)))) {
query = {
action: 'compare',
fromrev: oldid,
prop: 'ids|title',
format: 'json'
};
if (diffNum) {
query.torev = diff;
} else {
query.torelative = diff;
}
} else {
query = {
action: 'query',
prop: 'revisions',
rvprop: 'ids|timestamp|comment',
format: 'json',
indexpageids: true
};
if (diff && oldid) {
if (diff === 'prev') {
query.revids = oldid;
} else {
query.titles = title;
query.rvdir = 'newer';
query.rvstartid = oldid;
if (diff === 'next' && title) {
query.rvlimit = 2;
} else if (diffNum) {
// Diffs may or may not be consecutive, no limit
query.rvendid = diff;
}
}
} else {
// diff=next|prev|cur with no oldid
// Implies title= exists otherwise it's not a valid diff link (well, it is, but to the Main Page)
if (diff && /^\D+$/.test(diff)) {
query.titles = title;
} else {
query.revids = diff || oldid;
}
}
}
let queryResponse;
try {
queryResponse = await new mw.Api().get(query);
} catch (err) {
const error = new Error('Call to MediaWiki API failed.');
error.cause = err;
throw error;
}
if (queryResponse.compare && queryResponse.compare.fromtitle === queryResponse.compare.totitle) {
freeResolvesData = queryResponse;
} else if (queryResponse.query) {
const pageIds = queryResponse.query.pageids;
if (!Array.isArray(pageIds) || pageIds.length !== 1) {
const error = new Error('Error parsing diff.');
error.cause = queryResponse;
throw error;
}
freeResolvesData = queryResponse.query.pages[pageIds[0]];
} else {
const error = new Error('Could not find any diff associated with the URL provided.');
error.cause = queryResponse;
throw error;
}
} else {
freeResolvesData = undefined;
}
if (!resolves.length && !freeResolvesData) {
confirmations.push('You have not selected any edits where you tried to resolve the issue. Do you wish to make the report anyway?');
}
const data = {
uid: input.uid,
page: input.page,
comment: input.comment,
diffs: diffs,
warnings: warnings,
resolves: resolves,
free_resolves: freeResolvesData,
confirmations: confirmations
};
let minid;
for (let i = 0; i < data.diffs.length; ++i) {
if (data.diffs[i].parentid && (!minid || data.diffs[i].parentid < minid)) {
minid = data.diffs[i].parentid;
}
}
let queryResponse;
try {
queryResponse = await new mw.Api().get({
action: 'query',
prop: 'revisions',
format: 'json',
rvprop: 'sha1|ids|timestamp|comment',
rvlimit: 100, // intentionally limited
rvstartid: minid,
rvexcludeuser: data.uid,
indexpageids: true,
titles: data.page
});
} catch (err) {
const error = new Error('Call to MediaWiki API failed.');
error.cause = err;
throw error;
}
// In case an edit summary was revdel'd
const hasHiddenComment = function(rev) {
if (!rev.comment && typeof rev.commenthidden === 'string') {
return '(comment hidden)';
}
// swap curly braces for HTML entities to avoid templates being rendered if they were included in an edit summary
return '"' + rev.comment.replace(/\{/g, '{').replace(/\}/g, '}') + '"';
};
// TODO: The logic for filling the |orig= parameter in the report template seems to be broken. The API call does not seem to be parsed properly. It appears as if the data parameter of the callback is being used as if it were an array, but it isn't one - it's actually the entire API response object. I think adding queryResponse = queryResponse.query.pages[queryResponse.query.pageids[0]].revisions; after `let orig;` would convert it to the array it thinks it's dealing with, but I really have no idea what the actual goal is, so I can't figure out whether or not that's correct. -- Tollens
let orig;
if (queryResponse.length) {
const sha1 = queryResponse[0].sha1;
for (let i = 1; i < queryResponse.length; ++i) {
if (queryResponse[i].sha1 === sha1) {
orig = queryResponse[i];
break;
}
}
if (!orig) {
orig = queryResponse[0];
}
}
let origtext = '';
if (orig) {
origtext = '{{diff2|' + orig.revid + '|' + orig.timestamp + '}} ' + hasHiddenComment(orig);
}
const groupedDiffs = {};
let parentid, lastid;
for (let j = 0; j < data.diffs.length; ++j) {
const cur = data.diffs[j];
if ((cur.revid && cur.revid !== parentid) || lastid === null) {
lastid = cur.revid;
groupedDiffs[lastid] = [];
}
parentid = cur.parentid;
groupedDiffs[lastid].push(cur);
}
const difftext = $.map(groupedDiffs, (sub) => {
let ret = '';
if (sub.length >= 2) {
const last = sub[0];
const first = sub.slice(-1)[0];
const label = 'Consecutive edits made from ' + new Morebits.Date(first.timestamp).format('HH:mm, D MMMM YYYY', 'utc') + ' (UTC) to ' + new Morebits.Date(last.timestamp).format('HH:mm, D MMMM YYYY', 'utc') + ' (UTC)';
ret = '# {{diff|oldid=' + first.parentid + '|diff=' + last.revid + '|label=' + label + '}}\n';
}
ret += sub.reverse().map((v) => (sub.length >= 2 ? '#' : '') + '# {{diff2|' + v.revid + '|' + new Morebits.Date(v.timestamp).format('HH:mm, D MMMM YYYY', 'utc') + ' (UTC)}} ' + hasHiddenComment(v)).join('\n');
return ret;
}).reverse().join('\n');
const warningtext = data.warnings.reverse().map((v) => '# {{diff2|' + v.revid + '|' + new Morebits.Date(v.timestamp).format('HH:mm, D MMMM YYYY', 'utc') + ' (UTC)}} ' + hasHiddenComment(v)).join('\n');
let resolvetext = data.resolves.reverse().map((v) => '# {{diff2|' + v.revid + '|' + new Morebits.Date(v.timestamp).format('HH:mm, D MMMM YYYY', 'utc') + ' (UTC)}} ' + hasHiddenComment(v)).join('\n');
if (data.free_resolves) {
const page = data.free_resolves;
if (page.compare) {
resolvetext += '\n# {{diff|oldid=' + page.compare.fromrevid + '|diff=' + page.compare.torevid + '|label=Consecutive edits on ' + page.compare.totitle + '}}';
} else if (page.revisions) {
const revCount = page.revisions.length;
let rev;
if (revCount < 3) { // diff=prev or next
rev = revCount === 1 ? page.revisions[0] : page.revisions[1];
resolvetext += '\n# {{diff2|' + rev.revid + '|' + new Morebits.Date(rev.timestamp).format('HH:mm, D MMMM YYYY', 'utc') + ' (UTC) on ' + page.title + '}} ' + hasHiddenComment(rev);
} else { // diff and oldid are nonconsecutive
rev = page.revisions[0];
const revLatest = page.revisions[revCount - 1];
const label = 'Consecutive edits made from ' + new Morebits.Date(rev.timestamp).format('HH:mm, D MMMM YYYY', 'utc') + ' (UTC) to ' + new Morebits.Date(revLatest.timestamp).format('HH:mm, D MMMM YYYY', 'utc') + ' (UTC) on ' + page.title;
resolvetext += '\n# {{diff|oldid=' + rev.revid + '|diff=' + revLatest.revid + '|label=' + label + '}}\n';
}
}
}
let comment = data.comment.replace(/~*$/g, '').trim();
if (comment) {
comment += ' ~~~~';
}
const reportWikitext = '\n\n{{subst:AN3 report|diffs=' + difftext + '|warnings=' + warningtext + '|resolves=' + resolvetext + '|pagename=' + data.page + '|orig=' + origtext + '|comment=' + comment + '|uid=' + data.uid + '}}';
return {
uid: data.uid,
reportWikitext: reportWikitext,
confirmations: data.confirmations
};
};
Twinkle.addInitCallback(Twinkle.arv, 'arv');
}());
// </nowiki>
2mjfv7l608t6q609qlhxwtyqkcpnc7w
769575
769538
2026-10-22T02:29:50Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at d9cd3461baca43ac99ac2c24fb76a3aae6ddada9: minify
769575
javascript
text/javascript
// <nowiki>
(function(){Twinkle.arv=function twinklearv(){const username=mw.config.get("wgRelevantUserName");if(!username||username===mw.config.get("wgUserName")){return}const isIP=mw.util.isIPAddress(username,true);if(Morebits.ip.isRange(username)&&!Morebits.ip.validCIDR(username)){return}const userType=isIP?"IP"+(Morebits.ip.isRange(username)?" range":""):"user";Twinkle.addPortletLink(()=>{Twinkle.arv.callback(username,isIP)},"ARV","tw-arv","Report "+userType+" to administrators")};Twinkle.arv.callback=function(uid,isIP){const Window=new Morebits.SimpleWindow(600,500);Window.setTitle("Advance Reporting and Vetting");Window.setScriptName("Twinkle");Window.addFooterLink("AIV guide","WP:GAIV");Window.addFooterLink("UAA guide","WP:UAAI");Window.addFooterLink("SPI guide","Wikipedia:Sockpuppet investigations/SPI/Guide to filing cases");Window.addFooterLink("ARV prefs","WP:TW/PREF#arv");Window.addFooterLink("Twinkle help","WP:TW/DOC#arv");Window.addFooterLink("Give feedback","WT:TW");const form=new Morebits.QuickForm(Twinkle.arv.callback.evaluate);form.append({type:"select",name:"category",label:"Select report type:",event:Twinkle.arv.callback.changeCategory,list:[{label:"Vandalism (WP:AIV)",value:"aiv"},{label:"Username (WP:UAA)",value:"username",disabled:isIP},{label:"Sockpuppeteer (WP:SPI)",value:"sock"},{label:"Sockpuppet (WP:SPI)",value:"puppet"},{label:"Edit warring (WP:AN3)",value:"an3",disabled:Morebits.ip.isRange(uid)}]});form.append({type:"div",label:"",style:"color: red",id:"twinkle-arv-blockwarning"});if(mw.config.get("wgRelevantUserName")&&mw.util.isTemporaryUser(mw.config.get("wgRelevantUserName"))&&!mw.config.get("wgCheckUserTemporaryAccountIPRevealAllowed")){const temporaryAccountNotice=form.append({type:"field",label:"Temporary account notice",name:"ta_notice",style:"color: var(--morebits-color-warning)"});temporaryAccountNotice.append({type:"div",label:"You are reporting a [[Wikipedia:Temporary accounts|temporary account]]. These accounts, with usernames like ~2025-12345-67, have replaced IP addresses. Please be careful when reporting for sockpuppetry and when blocking, because other users and temporary accounts on the same IP may end up as collateral damage."})}form.append({type:"field",label:"Work area",name:"work_area"});form.append({type:"submit"});form.append({type:"hidden",name:"uid",value:uid});const result=form.render();Window.setContent(result);Window.display();const query={action:"query",list:"blocks",bkprop:"range|flags",format:"json"};if(isIP){query.bkip=uid}else{query.bkusers=uid}new Morebits.wiki.Api("Checking the user's block status",query,apiobj=>{const blocklist=apiobj.getResponse().query.blocks;if(blocklist.length){const block=blocklist[0];let message=(isIP?"This IP "+(Morebits.ip.isRange(uid)?"range":"address"):"This account")+" is "+(block.partial?"partially":"already")+" blocked";message+=block.rangestart!==block.rangeend?" as part of a rangeblock.":".";if(block.partial){$("#twinkle-arv-blockwarning").css("color","black")}$("#twinkle-arv-blockwarning").text(message)}}).post();const evt=document.createEvent("Event");evt.initEvent("change",true,true);result.category.dispatchEvent(evt)};Twinkle.arv.callback.changeCategory=function(e){const value=e.target.value;const root=e.target.form;const old_area=Morebits.QuickForm.getElements(root,"work_area")[0];let work_area=null;switch(value){case"aiv":default:work_area=new Morebits.QuickForm.Element({type:"field",label:"Report user for vandalism",name:"work_area"});work_area.append({type:"input",name:"page",label:"Primary linked page:",tooltip:"Leave blank to not link to the page in the report",value:Twinkle.getPrefill("vanarticle")||"",event:function(e){const value=e.target.value;const root=e.target.form;if(value===""){root.badid.disabled=root.goodid.disabled=true}else{root.badid.disabled=false;root.goodid.disabled=root.badid.value===""}}});work_area.append({type:"input",name:"badid",label:"Revision ID for target page when vandalised:",tooltip:"Leave blank for no diff link",value:Twinkle.getPrefill("vanarticlerevid")||"",disabled:!Twinkle.getPrefill("vanarticle"),event:function(e){const value=e.target.value;const root=e.target.form;root.goodid.disabled=value===""}});work_area.append({type:"input",name:"goodid",label:"Last good revision ID before vandalism of target page:",tooltip:"Leave blank for diff link to previous revision",value:Twinkle.getPrefill("vanarticlegoodrevid")||"",disabled:!Twinkle.getPrefill("vanarticle")||Twinkle.getPrefill("vanarticlerevid")});work_area.append({type:"checkbox",name:"arvtype",list:[{label:"Vandalism after final (level 4 or 4im) warning given",value:"final"},{label:"Vandalism after recent (within 1 day) release of block",value:"postblock"},{label:"Evidently a vandalism-only account",value:"vandalonly",disabled:mw.util.isIPAddress(root.uid.value,true)},{label:"Account is a promotion-only account",value:"promoonly",disabled:mw.util.isIPAddress(root.uid.value,true)},{label:"Account is evidently a spambot or a compromised account",value:"spambot"}]});work_area.append({type:"textarea",name:"reason",label:"Comment:"});work_area=work_area.render();old_area.parentNode.replaceChild(work_area,old_area);break;case"username":work_area=new Morebits.QuickForm.Element({type:"field",label:"Report username violation",name:"work_area"});work_area.append({type:"header",label:"Type(s) of inappropriate username",tooltip:"Wikipedia does not allow usernames that are misleading, promotional, offensive or disruptive. Domain names and email addresses are likewise prohibited. These criteria apply to both usernames and signatures. Usernames that are inappropriate in another language, or that represent an inappropriate name with misspellings and substitutions, or do so indirectly or by implication, are still considered inappropriate."});work_area.append({type:"checkbox",name:"arvtype",list:[{label:"Misleading username",value:"misleading",tooltip:"Misleading usernames imply relevant, misleading things about the contributor. For example, misleading points of fact, an impression of undue authority, or usernames giving the impression of a bot account."},{label:"Promotional username",value:"promotional",tooltip:"Promotional usernames are advertisements for a company, website or group. Please do not report these names to UAA unless the user has also made promotional edits related to the name."},{label:"Offensive username",value:"offensive",tooltip:"Offensive usernames make harmonious editing difficult or impossible."},{label:"Disruptive username",value:"disruptive",tooltip:"Disruptive usernames include outright trolling or personal attacks, or otherwise show a clear intent to disrupt Wikipedia."}]});work_area.append({type:"textarea",name:"reason",label:"Comment:"});work_area=work_area.render();old_area.parentNode.replaceChild(work_area,old_area);break;case"puppet":work_area=new Morebits.QuickForm.Element({type:"field",label:"Report suspected sockpuppet",name:"work_area"});work_area.append({type:"input",name:"sockmaster",label:"Sockpuppeteer",tooltip:'The username of the sockpuppeteer (sockmaster) without the "User:" prefix'});work_area.append({type:"textarea",label:"Evidence:",name:"evidence",tooltip:"Your evidence should make it clear that each of these users is likely to be abusing multiple accounts. Usually this means diffs, page histories or other information that justifies why the users are a) the same and b) disruptive. This should be just evidence and information needed to judge the matter. Avoid all other discussion that is not evidence of sockpuppetry."});work_area=work_area.render();old_area.parentNode.replaceChild(work_area,old_area);break;case"sock":work_area=new Morebits.QuickForm.Element({type:"field",label:"Report suspected sockpuppeteer",name:"work_area"});work_area.append({type:"dyninput",name:"sockpuppets",label:"Sockpuppets",sublabel:"Sock:",tooltip:'The username of the sockpuppet without the "User:" prefix',min:2});work_area.append({type:"textarea",label:"Evidence:",name:"evidence",tooltip:"Your evidence should make it clear that each of these users is likely to be abusing multiple accounts. Usually this means diffs, page histories or other information that justifies why the users are a) the same and b) disruptive. This should be just evidence and information needed to judge the matter. Avoid all other discussion that is not evidence of sockpuppetry."});work_area=work_area.render();old_area.parentNode.replaceChild(work_area,old_area);break;case"an3":work_area=new Morebits.QuickForm.Element({type:"field",label:"Report edit warring",name:"work_area"});work_area.append({type:"input",name:"page",label:"Page",tooltip:"The page being reported"});work_area.append({type:"button",name:"load",label:"Load",event:function(e){const root=e.target.form;const date=(new Morebits.Date).subtract(48,"hours");const getAN3Entries=function(field,rvuser,titles){const $field=$(root).find("[name="+field+"]");$field.find(".entry").remove();(new mw.Api).get({action:"query",prop:"revisions",format:"json",rvprop:"sha1|ids|timestamp|parsedcomment|comment",rvlimit:500,rvend:date.toISOString(),rvuser:rvuser,indexpageids:true,titles:titles}).done(data=>{const pageid=data.query.pageids[0];const page=data.query.pages[pageid];if(!page.revisions){$("<span>").addClass("entry").html("None found").appendTo($field)}else{for(let i=0;i<page.revisions.length;++i){const rev=page.revisions[i];const $entry=$("<div>").addClass("entry");const $input=$("<input>").attr({type:"checkbox",name:"s_"+field,value:JSON.stringify(rev)});$input.appendTo($entry);let comment="<span>";if(typeof rev.commenthidden==="string"){comment+="(comment hidden)"}else{comment+='"'+rev.parsedcomment+'"'}comment+=' at <a href="'+mw.config.get("wgScript")+"?diff="+rev.revid+'">'+new Morebits.Date(rev.timestamp).calendar()+"</a></span>";$entry.append(comment).appendTo($field)}}if(field==="resolves"){const $free_entry=$("<div>").addClass("entry");const $free_input=$("<input>").attr({type:"text",name:"s_resolves_free"});const $free_label=$("<label>").attr("for","s_resolves_free").html("URL link of diff with additional discussions: ");$free_entry.append($free_label).append($free_input).appendTo($field)}}).fail(()=>{$("<span>").addClass("entry").html("API failure, reload page and try again").appendTo($field)})};const uid=root.uid.value;getAN3Entries("warnings",mw.config.get("wgUserName"),"User talk:"+uid);const page=root.page.value;if(page){getAN3Entries("diffs",uid,page);const t=new mw.Title(page);const talk_page=t.getTalkPage().getPrefixedText();getAN3Entries("resolves",mw.config.get("wgUserName"),talk_page)}else{$(root).find("[name=diffs]").find(".entry").remove();$(root).find("[name=resolves]").find(".entry").remove()}}});work_area.append({type:"field",name:"diffs",label:"User's reverts (within last 48 hours)",tooltip:"Select the edits you believe are reverts"});work_area.append({type:"field",name:"warnings",label:"Warnings given to subject",tooltip:"You must have warned the subject before reporting"});work_area.append({type:"field",name:"resolves",label:"Resolution initiatives",tooltip:"You should have tried to resolve the issue on the talk page first"});work_area.append({type:"textarea",label:"Comment:",name:"comment"});work_area=work_area.render();old_area.parentNode.replaceChild(work_area,old_area);break}};Twinkle.arv.callback.evaluate=function(e){const form=e.target;let reason="";const input=Morebits.QuickForm.getInputData(form);switch(input.category){case"aiv":default:reason=Twinkle.arv.callback.getAivReasonWikitext(input);if(reason===null){alert("You must specify some reason");return}Morebits.SimpleWindow.setButtonsEnabled(false);Morebits.Status.init(form);Morebits.wiki.actionCompleted.redirect="Wikipedia:Administrator intervention against vandalism";Morebits.wiki.actionCompleted.notice="Reporting complete";var aivPage=new Morebits.wiki.Page("Wikipedia:Administrator intervention against vandalism","Processing AIV request");aivPage.setPageSection(1);aivPage.setFollowRedirect(true);aivPage.load(()=>{const text=aivPage.getPageText();const $aivLink='<a target="_blank" href="/wiki/WP:AIV">WP:AIV</a>';if(new RegExp("\\{\\{\\s*(?:(?:[Ii][Pp])?[Vv]andal|[Uu]serlinks)\\s*\\|\\s*(?:1=)?\\s*"+Morebits.string.escapeRegExp(input.uid)+"\\s*\\}\\}").test(text)){aivPage.getStatusElement().error("Report already present, will not add a new one");Morebits.Status.printUserText(reason,"The comments you typed are provided below, in case you wish to manually post them under the existing report for this user at "+$aivLink+":");return}const tb2Page=new Morebits.wiki.Page("Wikipedia:Administrator intervention against vandalism/TB2","Checking bot reports");tb2Page.load(()=>{const tb2Text=tb2Page.getPageText();const tb2statelem=tb2Page.getStatusElement();if(new RegExp("\\{\\{\\s*(?:(?:[Ii][Pp])?[Vv]andal|[Uu]serlinks)\\s*\\|\\s*(?:1=)?\\s*"+Morebits.string.escapeRegExp(input.uid)+"\\s*\\}\\}").test(tb2Text)){if(confirm("The user "+input.uid+" has already been reported by a bot. Do you wish to make the report anyway?")){tb2statelem.info("Proceeded despite bot report")}else{tb2statelem.error("Report from a bot is already present, stopping");Morebits.Status.printUserText(reason,"The comments you typed are provided below, in case you wish to manually post them at "+$aivLink+":");return}}else{tb2statelem.info("No conflicting bot reports")}aivPage.getStatusElement().status("Adding new report...");aivPage.setEditSummary("Reporting [[Special:Contributions/"+input.uid+"|"+input.uid+"]].");aivPage.setChangeTags(Twinkle.changeTags);aivPage.setAppendText(Twinkle.arv.callback.buildAivReport(input));aivPage.setDiscussionToolsAutoSubscribe(false);aivPage.append()})});break;case"username":var censorUsername=input.arvtype.includes("offensive");reason=Twinkle.arv.callback.getUsernameReportWikitext(input);Morebits.SimpleWindow.setButtonsEnabled(false);Morebits.Status.init(form);Morebits.wiki.actionCompleted.redirect="Wikipedia:Usernames for administrator attention";Morebits.wiki.actionCompleted.notice="Reporting complete";var uaaPage=new Morebits.wiki.Page("Wikipedia:Usernames for administrator attention","Processing UAA request");uaaPage.setFollowRedirect(true);uaaPage.load(()=>{const text=uaaPage.getPageText();if(new RegExp("\\{\\{\\s*user-uaa\\s*\\|\\s*(1\\s*=\\s*)?"+Morebits.string.escapeRegExp(input.uid)+"\\s*(\\||\\})").test(text)){uaaPage.getStatusElement().error("User is already listed.");const $uaaLink='<a target="_blank" href="/wiki/WP:UAA">WP:UAA</a>';Morebits.Status.printUserText(reason,"The comments you typed are provided below, in case you wish to manually post them under the existing report for this user at "+$uaaLink+":");return}uaaPage.getStatusElement().status("Adding new report...");uaaPage.setEditSummary("Reporting "+(censorUsername?"an offensive username.":"[[Special:Contributions/"+input.uid+"|"+input.uid+"]]."));uaaPage.setChangeTags(Twinkle.changeTags);uaaPage.setPageText(text+"\n"+reason+"\n*");uaaPage.setDiscussionToolsAutoSubscribe(false);uaaPage.save()});break;case"sock":case"puppet":var reportData=Twinkle.arv.callback.getSpiReportData(input);if(reportData.error){alert(reportData.error);return}Morebits.SimpleWindow.setButtonsEnabled(false);Morebits.Status.init(form);Morebits.wiki.addCheckpoint();var reportpage="Wikipedia:Sockpuppet investigations/"+reportData.sockmaster;Morebits.wiki.actionCompleted.redirect=reportpage;Morebits.wiki.actionCompleted.notice="Reporting complete";var spiPage=new Morebits.wiki.Page(reportpage,"Retrieving discussion page");spiPage.setFollowRedirect(true);spiPage.setEditSummary("Adding new report for [[Special:Contributions/"+reportData.sockmaster+"|"+reportData.sockmaster+"]].");spiPage.setChangeTags(Twinkle.changeTags);spiPage.setAppendText(reportData.wikitext);spiPage.setWatchlist(Twinkle.getPref("spiWatchReport"));spiPage.setDiscussionToolsAutoSubscribe(false);spiPage.append();Morebits.wiki.removeCheckpoint();break;case"an3":Twinkle.arv.callback.getAn3ReportData(input).then(data=>{for(const confirmation of data.confirmations){if(!confirm(confirmation)){return}}Morebits.SimpleWindow.setButtonsEnabled(false);Morebits.Status.init(form);Morebits.wiki.addCheckpoint();const reportpage="Wikipedia:Administrators' noticeboard/Edit warring";Morebits.wiki.actionCompleted.redirect=reportpage;Morebits.wiki.actionCompleted.notice="Reporting complete";const an3Page=new Morebits.wiki.Page(reportpage,"Retrieving discussion page");an3Page.setFollowRedirect(true);an3Page.setEditSummary("Adding new report for [[Special:Contributions/"+data.uid+"|"+data.uid+"]].");an3Page.setChangeTags(Twinkle.changeTags);an3Page.setAppendText(data.reportWikitext);an3Page.setDiscussionToolsAutoSubscribe(false);an3Page.append();const notifyText="\n\n{{subst:an3-notice|1="+mw.util.wikiUrlencode(data.uid)+"|auto=1}} ~~~~";const talkPage=new Morebits.wiki.Page("User talk:"+data.uid,"Notifying edit warrior");talkPage.setFollowRedirect(true);talkPage.setEditSummary("Notifying about edit warring noticeboard discussion.");talkPage.setChangeTags(Twinkle.changeTags);talkPage.setAppendText(notifyText);talkPage.append();Morebits.wiki.removeCheckpoint()}).catch(error=>{console.error("Error occurred while preparing AN3 report.",error);alert("Error occurred while preparing AN3 report: "+error.message)});break}};Twinkle.arv.callback.getAivReasonWikitext=function(input){let text="";let type=input.arvtype;if(!type.length&&input.reason===""){return null}type=type.map(v=>{switch(v){case"final":return"vandalism after final warning";case"postblock":return"vandalism after recent release of block";case"vandalonly":return"actions evidently indicate a vandalism-only account";case"promoonly":return"account is being used only for promotional purposes";case"spambot":return"account is evidently a spambot or a compromised account";default:return"unknown reason"}}).join("; ");if(input.page!==""){text="On {{No redirect|"+input.page+"}}";if(input.badid!==""){text+=" ({{diff|"+input.page+"|"+input.badid+"|"+input.goodid+"|diff}})"}text+=":"}if(type){text+=" "+type}if(input.reason!==""){const textEndsInPunctuationOrBlank=/([.?!;:]|^)$/.test(text);text+=textEndsInPunctuationOrBlank?"":".";const textIsBlank=text==="";text+=textIsBlank?"":" ";text+=input.reason}text=text.trim();const textEndsInPunctuation=/[.?!;]$/.test(text);if(!textEndsInPunctuation){text+="."}text+=" ~~~~";text=text.replace(/\r?\n/g,"\n*:");return text};Twinkle.arv.callback.buildAivReport=function(input){return"\n*{{vandal|"+(/=/.test(input.uid)?"1=":"")+input.uid+"}} – "+Twinkle.arv.callback.getAivReasonWikitext(input)};Twinkle.arv.callback.getUsernameReportWikitext=function(input){if(input.arvtype.length<=2){input.arvtype=input.arvtype.join(" and ")}else{input.arvtype=[input.arvtype.slice(0,-1).join(", "),input.arvtype.slice(-1)].join(" and ")}let adjective="a";if(/[aeiouwyh]/.test(input.arvtype[0]||"")){adjective="an"}let text="*{{user-uaa|1="+input.uid+"}} – ";if(input.arvtype.length){text+="Violation of the username policy as "+adjective+" "+input.arvtype+" username. "}if(input.reason!==""){text+=Morebits.string.toUpperCaseFirstChar(input.reason);const endsInPeriod=/\.$/.test(input.reason);if(!endsInPeriod){text+="."}text+=" "}text+="~~~~";text=text.replace(/\r?\n/g,"\n*:");return text};Twinkle.arv.callback.getSpiReportData=function(input){const isPuppetReport=input.category==="puppet";if(!isPuppetReport){input.sockpuppets=input.sockpuppets.filter(sock=>sock!=="")}if(isPuppetReport&&!input.sockmaster){return{error:"You have not entered a sockmaster account for this puppet. Consider reporting this account as a sockpuppeteer instead."}}else if(!isPuppetReport&&input.sockpuppets.length===0){return{error:"You have not entered any sockpuppet account(s) for this sockmaster. Consider reporting this account as a sockpuppet instead."}}input.sockmaster=input.sockmaster||input.uid;input.sockpuppets=isPuppetReport?[input.uid]:Morebits.array.uniq(input.sockpuppets);let text="\n{{subst:SPI report|"+input.sockpuppets.map((sock,index)=>index+1+"="+sock).join("|")+"\n|evidence="+Morebits.string.formatReasonText(input.evidence)+" \n";text+="}}";return{sockmaster:input.sockmaster,wikitext:text}};Twinkle.arv.callback.getAn3ReportData=async function(input){const confirmations=[];const diffs=input.s_diffs?input.s_diffs.map(JSON.parse):[];if(diffs.length<3){confirmations.push("You have selected fewer than three offending edits. Do you wish to make the report anyway?")}const warnings=input.s_warnings?input.s_warnings.map(JSON.parse):[];if(!warnings.length){confirmations.push("You have not selected any edits where you warned the offender. Do you wish to make the report anyway?")}const resolves=input.s_resolves?input.s_resolves.map(JSON.parse):[];const freeResolves=input.s_resolves_free;let freeResolvesData;if(freeResolves){let query;let diff,oldid;const specialDiff=/Special:Diff\/(\d+)(?:\/(\S+))?/i.exec(freeResolves);if(specialDiff){if(specialDiff[2]){oldid=specialDiff[1];diff=specialDiff[2]}else{diff=specialDiff[1]}}else{diff=mw.util.getParamValue("diff",freeResolves);oldid=mw.util.getParamValue("oldid",freeResolves)}const title=mw.util.getParamValue("title",freeResolves);const diffNum=/^\d+$/.test(diff);if(oldid&&(diff==="cur"||!title&&(diff==="next"||diffNum))){query={action:"compare",fromrev:oldid,prop:"ids|title",format:"json"};if(diffNum){query.torev=diff}else{query.torelative=diff}}else{query={action:"query",prop:"revisions",rvprop:"ids|timestamp|comment",format:"json",indexpageids:true};if(diff&&oldid){if(diff==="prev"){query.revids=oldid}else{query.titles=title;query.rvdir="newer";query.rvstartid=oldid;if(diff==="next"&&title){query.rvlimit=2}else if(diffNum){query.rvendid=diff}}}else{if(diff&&/^\D+$/.test(diff)){query.titles=title}else{query.revids=diff||oldid}}}let queryResponse;try{queryResponse=await(new mw.Api).get(query)}catch(err){const error=new Error("Call to MediaWiki API failed.");error.cause=err;throw error}if(queryResponse.compare&&queryResponse.compare.fromtitle===queryResponse.compare.totitle){freeResolvesData=queryResponse}else if(queryResponse.query){const pageIds=queryResponse.query.pageids;if(!Array.isArray(pageIds)||pageIds.length!==1){const error=new Error("Error parsing diff.");error.cause=queryResponse;throw error}freeResolvesData=queryResponse.query.pages[pageIds[0]]}else{const error=new Error("Could not find any diff associated with the URL provided.");error.cause=queryResponse;throw error}}else{freeResolvesData=undefined}if(!resolves.length&&!freeResolvesData){confirmations.push("You have not selected any edits where you tried to resolve the issue. Do you wish to make the report anyway?")}const data={uid:input.uid,page:input.page,comment:input.comment,diffs:diffs,warnings:warnings,resolves:resolves,free_resolves:freeResolvesData,confirmations:confirmations};let minid;for(let i=0;i<data.diffs.length;++i){if(data.diffs[i].parentid&&(!minid||data.diffs[i].parentid<minid)){minid=data.diffs[i].parentid}}let queryResponse;try{queryResponse=await(new mw.Api).get({action:"query",prop:"revisions",format:"json",rvprop:"sha1|ids|timestamp|comment",rvlimit:100,rvstartid:minid,rvexcludeuser:data.uid,indexpageids:true,titles:data.page})}catch(err){const error=new Error("Call to MediaWiki API failed.");error.cause=err;throw error}const hasHiddenComment=function(rev){if(!rev.comment&&typeof rev.commenthidden==="string"){return"(comment hidden)"}return'"'+rev.comment.replace(/\{/g,"{").replace(/\}/g,"}")+'"'};let orig;if(queryResponse.length){const sha1=queryResponse[0].sha1;for(let i=1;i<queryResponse.length;++i){if(queryResponse[i].sha1===sha1){orig=queryResponse[i];break}}if(!orig){orig=queryResponse[0]}}let origtext="";if(orig){origtext="{{diff2|"+orig.revid+"|"+orig.timestamp+"}} "+hasHiddenComment(orig)}const groupedDiffs={};let parentid,lastid;for(let j=0;j<data.diffs.length;++j){const cur=data.diffs[j];if(cur.revid&&cur.revid!==parentid||lastid===null){lastid=cur.revid;groupedDiffs[lastid]=[]}parentid=cur.parentid;groupedDiffs[lastid].push(cur)}const difftext=$.map(groupedDiffs,sub=>{let ret="";if(sub.length>=2){const last=sub[0];const first=sub.slice(-1)[0];const label="Consecutive edits made from "+new Morebits.Date(first.timestamp).format("HH:mm, D MMMM YYYY","utc")+" (UTC) to "+new Morebits.Date(last.timestamp).format("HH:mm, D MMMM YYYY","utc")+" (UTC)";ret="# {{diff|oldid="+first.parentid+"|diff="+last.revid+"|label="+label+"}}\n"}ret+=sub.reverse().map(v=>(sub.length>=2?"#":"")+"# {{diff2|"+v.revid+"|"+new Morebits.Date(v.timestamp).format("HH:mm, D MMMM YYYY","utc")+" (UTC)}} "+hasHiddenComment(v)).join("\n");return ret}).reverse().join("\n");const warningtext=data.warnings.reverse().map(v=>"# {{diff2|"+v.revid+"|"+new Morebits.Date(v.timestamp).format("HH:mm, D MMMM YYYY","utc")+" (UTC)}} "+hasHiddenComment(v)).join("\n");let resolvetext=data.resolves.reverse().map(v=>"# {{diff2|"+v.revid+"|"+new Morebits.Date(v.timestamp).format("HH:mm, D MMMM YYYY","utc")+" (UTC)}} "+hasHiddenComment(v)).join("\n");if(data.free_resolves){const page=data.free_resolves;if(page.compare){resolvetext+="\n# {{diff|oldid="+page.compare.fromrevid+"|diff="+page.compare.torevid+"|label=Consecutive edits on "+page.compare.totitle+"}}"}else if(page.revisions){const revCount=page.revisions.length;let rev;if(revCount<3){rev=revCount===1?page.revisions[0]:page.revisions[1];resolvetext+="\n# {{diff2|"+rev.revid+"|"+new Morebits.Date(rev.timestamp).format("HH:mm, D MMMM YYYY","utc")+" (UTC) on "+page.title+"}} "+hasHiddenComment(rev)}else{rev=page.revisions[0];const revLatest=page.revisions[revCount-1];const label="Consecutive edits made from "+new Morebits.Date(rev.timestamp).format("HH:mm, D MMMM YYYY","utc")+" (UTC) to "+new Morebits.Date(revLatest.timestamp).format("HH:mm, D MMMM YYYY","utc")+" (UTC) on "+page.title;resolvetext+="\n# {{diff|oldid="+rev.revid+"|diff="+revLatest.revid+"|label="+label+"}}\n"}}}let comment=data.comment.replace(/~*$/g,"").trim();if(comment){comment+=" ~~~~"}const reportWikitext="\n\n{{subst:AN3 report|diffs="+difftext+"|warnings="+warningtext+"|resolves="+resolvetext+"|pagename="+data.page+"|orig="+origtext+"|comment="+comment+"|uid="+data.uid+"}}";return{uid:data.uid,reportWikitext:reportWikitext,confirmations:data.confirmations}};Twinkle.addInitCallback(Twinkle.arv,"arv")})();
// </nowiki>
aa8aehm9rs9frlwsomkf3ngtvkcuzr2
User:Syunsyunminmin/Twinkle/twinkleblock.js
2
179407
769472
2026-10-21T13:58:22Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 3680108:
769472
javascript
text/javascript
/*
Twinkle: adapted for Japanese Wikipedia; upstream authors and Japanese adaptations are credited in UPSTREAM.md.
Upstream source: https://github.com/wikimedia-gadgets/twinkle/blob/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/src/modules/twinkleblock.js
Upstream author history: https://github.com/wikimedia-gadgets/twinkle/commits/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/src/modules/twinkleblock.js
License: CC BY-SA 3.0 Unported. See https://github.com/wikimedia-gadgets/twinkle/blob/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/LICENSE.txt
Japanese adaptation: Syunsyunminmin; includes later synchronization changes.
*/
// <nowiki>
(function() {
const api = new mw.Api();
let relevantUserName, blockedUserName, blockWindow;
const menuFormattedNamespaces = $.extend({}, mw.config.get('wgFormattedNamespaces'));
menuFormattedNamespaces[0] = '(Article)';
/*
****************************************
*** twinkleblock.js: Block module
****************************************
* Mode of invocation: Tab ("Block")
* Active on: Any page with relevant user name (userspace, contribs, etc.)
*/
Twinkle.block = function twinkleblock() {
relevantUserName = mw.config.get('wgRelevantUserName');
// should show on Contributions or Block pages, anywhere there's a relevant user
// Ignore ranges wider than the CIDR limit
if (Morebits.userIsSysop && relevantUserName && (!Morebits.ip.isRange(relevantUserName) || Morebits.ip.validCIDR(relevantUserName))) {
Twinkle.addPortletLink(Twinkle.block.callback, 'Block', 'tw-block', 'Block relevant user');
}
};
Twinkle.block.callback = function twinkleblockCallback() {
if (relevantUserName === mw.config.get('wgUserName') &&
!confirm('You are about to block yourself! Are you sure you want to proceed?')) {
return;
}
Twinkle.block.currentBlockInfo = undefined;
Twinkle.block.field_block_options = {};
Twinkle.block.field_template_options = {};
blockWindow = new Morebits.SimpleWindow(650, 530);
// need to be verbose about who we're blocking
blockWindow.setTitle('Block or issue block template to ' + relevantUserName);
blockWindow.setScriptName('Twinkle');
// Always added, hidden later if actual user not blocked
blockWindow.addFooterLink('Unblock this user', 'Special:Unblock/' + relevantUserName);
blockWindow.addFooterLink('Block templates', 'Template:Uw-block/doc/Block_templates');
blockWindow.addFooterLink('Block policy', 'WP:BLOCK');
blockWindow.addFooterLink('Block prefs', 'WP:TW/PREF#block');
blockWindow.addFooterLink('Twinkle help', 'WP:TW/DOC#block');
blockWindow.addFooterLink('Give feedback', 'WT:TW');
const form = new Morebits.QuickForm(Twinkle.block.callback.evaluate);
const actionfield = form.append({
type: 'field',
label: 'Type of action'
});
actionfield.append({
type: 'checkbox',
name: 'actiontype',
event: Twinkle.block.callback.change_action,
list: [
{
label: 'Block user',
value: 'block',
tooltip: 'Block the relevant user with the given options. If partial block is unchecked, this will be a sitewide block.',
checked: true
},
{
label: 'Partial block',
value: 'partial',
tooltip: 'Enable partial blocks and partial block templates.',
checked: Twinkle.getPref('defaultToPartialBlocks') // Overridden if already blocked
},
{
label: 'Add block template to user talk page',
value: 'template',
tooltip: 'If the blocking admin forgot to issue a block template, or you have just blocked the user without templating them, you can use this to issue the appropriate template. Check the partial block box for partial block templates.',
// Disallow when viewing the block dialog on an IP range
checked: !Morebits.ip.isRange(relevantUserName),
disabled: Morebits.ip.isRange(relevantUserName)
}
]
});
/*
Add option for IPv6 ranges smaller than /64 to upgrade to the 64
CIDR ([[WP:/64]]). This is one of the few places where we want
wgRelevantUserName since this depends entirely on the original user.
In theory, we shouldn't use Morebits.ip.get64 here since since we want
to exclude functionally-equivalent /64s. That'd be:
// if (mw.util.isIPv6Address(mw.config.get('wgRelevantUserName'), true) &&
// (mw.util.isIPv6Address(mw.config.get('wgRelevantUserName')) || parseInt(mw.config.get('wgRelevantUserName').replace(/^(.+?)\/?(\d{1,3})?$/, '$2'), 10) > 64)) {
In practice, though, since functionally-equivalent ranges are
(mis)treated as separate by MediaWiki's logging ([[phab:T146628]]),
using Morebits.ip.get64 provides a modicum of relief in thise case.
*/
const sixtyFour = Morebits.ip.get64(mw.config.get('wgRelevantUserName'));
if (sixtyFour && sixtyFour !== mw.config.get('wgRelevantUserName')) {
const block64field = form.append({
type: 'field',
label: 'Convert to /64 rangeblock',
name: 'field_64'
});
block64field.append({
type: 'div',
style: 'margin-bottom: 0.5em',
label: ['It\'s usually fine, if not better, to ', $.parseHTML('<a target="_blank" href="' + mw.util.getUrl('WP:/64') + '">just block the /64</a>')[0], ' range (',
$.parseHTML('<a target="_blank" href="' + mw.util.getUrl('Special:Contributions/' + sixtyFour) + '">' + sixtyFour + '</a>)')[0], ').']
});
block64field.append({
type: 'checkbox',
name: 'block64',
event: Twinkle.block.callback.change_block64,
list: [{
checked: Twinkle.getPref('defaultToBlock64'),
label: 'Block the /64 instead',
value: 'block64',
tooltip: Morebits.ip.isRange(mw.config.get('wgRelevantUserName')) ? 'Will eschew leaving a template.' : 'Any template issued will go to the original IP: ' + mw.config.get('wgRelevantUserName')
}]
});
}
form.append({ type: 'field', label: 'Preset', name: 'field_preset' });
form.append({ type: 'field', label: 'Template options', name: 'field_template_options' });
form.append({ type: 'field', label: 'Block options', name: 'field_block_options' });
form.append({ type: 'submit' });
const result = form.render();
blockWindow.setContent(result);
blockWindow.display();
result.root = result;
Twinkle.block.fetchUserInfo(() => {
// Toggle initial partial state depending on prior block type,
// will override the defaultToPartialBlocks pref
if (blockedUserName === relevantUserName) {
$(result).find('[name=actiontype][value=partial]').prop('checked', Twinkle.block.currentBlockInfo.partial === '');
}
// clean up preset data (defaults, etc.), done exactly once, must be before Twinkle.block.callback.change_action is called
Twinkle.block.transformBlockPresets();
// init the controls after user and block info have been fetched
const evt = document.createEvent('Event');
evt.initEvent('change', true, true);
if (result.block64 && result.block64.checked) {
// Calls the same change_action event once finished
result.block64.dispatchEvent(evt);
} else {
result.actiontype[0].dispatchEvent(evt);
}
});
};
// Store fetched user data, only relevant if switching IPv6 to a /64
Twinkle.block.fetchedData = {};
// Processes the data from a query response, separated from
// Twinkle.block.fetchUserInfo to allow reprocessing of already-fetched data
Twinkle.block.processUserInfo = function twinkleblockProcessUserInfo(data, fn) {
let blockinfo = data.query.blocks[0];
// Soft redirect to Special:Block if the user is multi-blocked (#2178)
if (blockinfo && data.query.blocks.length > 1) {
// Remove submission buttons.
blockWindow.$dialog.find('.morebits-dialog-buttons').empty();
Morebits.Status.init(blockWindow.content.querySelector('form'));
Morebits.Status.warn(
`This target has ${data.query.blocks.length} active blocks`,
`Multiblocks is not supported by Twinkle. Use [[Special:Block/${relevantUserName}]] instead.`
);
return;
}
const userinfo = data.query.users[0];
// If an IP is blocked *and* rangeblocked, the above finds
// whichever block is more recent, not necessarily correct.
// Three seems... unlikely
if (data.query.blocks.length > 1 && blockinfo.user !== relevantUserName) {
blockinfo = data.query.blocks[1];
}
// Cache response, used when toggling /64 blocks
Twinkle.block.fetchedData[userinfo.name] = data;
Twinkle.block.isRegistered = !!userinfo.userid;
if (Twinkle.block.isRegistered) {
Twinkle.block.userIsBot = !!userinfo.groupmemberships && userinfo.groupmemberships.map((e) => e.group).includes('bot');
} else {
Twinkle.block.userIsBot = false;
}
if (blockinfo) {
// handle frustrating system of inverted boolean values
blockinfo.disabletalk = blockinfo.allowusertalk === undefined;
blockinfo.hardblock = blockinfo.anononly === undefined;
}
// will undefine if no blocks present
Twinkle.block.currentBlockInfo = blockinfo;
blockedUserName = Twinkle.block.currentBlockInfo && Twinkle.block.currentBlockInfo.user;
// Toggle unblock link if not the user in question; always first
const unblockLink = blockWindow.$dialog.find('.morebits-dialog-footerlinks a')[0];
if (blockedUserName !== relevantUserName) {
unblockLink.hidden = true;
unblockLink.nextSibling.hidden = true; // link+trailing bullet
} else {
unblockLink.hidden = false;
unblockLink.nextSibling.hidden = false; // link+trailing bullet
}
// Semi-busted on ranges, see [[phab:T270737]] and [[phab:T146628]].
// Basically, logevents doesn't treat functionally-equivalent ranges
// as equivalent, meaning any functionally-equivalent IP range is
// misinterpreted by the log throughout. Without logevents
// redirecting (like Special:Block does) we would need a function to
// parse ranges, which is a pain. IPUtils has the code, but it'd be a
// lot of cruft for one purpose.
Twinkle.block.hasBlockLog = !!data.query.logevents.length;
Twinkle.block.blockLog = Twinkle.block.hasBlockLog && data.query.logevents;
// Used later to check if block status changed while filling out the form
Twinkle.block.blockLogId = Twinkle.block.hasBlockLog ? data.query.logevents[0].logid : false;
if (typeof fn === 'function') {
return fn();
}
};
Twinkle.block.fetchUserInfo = function twinkleblockFetchUserInfo(fn) {
const query = {
format: 'json',
action: 'query',
list: 'blocks|users|logevents',
letype: 'block',
lelimit: 1,
letitle: 'User:' + relevantUserName,
bkprop: 'expiry|reason|flags|restrictions|range|user',
ususers: relevantUserName
};
// bkusers doesn't catch single IPs blocked as part of a range block
if (mw.util.isIPAddress(relevantUserName, true)) {
query.bkip = relevantUserName;
} else {
query.bkusers = relevantUserName;
// groupmemberships only relevant for registered users
query.usprop = 'groupmemberships';
}
api.get(query).then((data) => {
Twinkle.block.processUserInfo(data, fn);
}, (msg) => {
Morebits.Status.init($('div[name="currentblock"] span').last()[0]);
Morebits.Status.warn('Error fetching user info', msg);
});
};
Twinkle.block.callback.saveFieldset = function twinkleblockCallbacksaveFieldset(fieldset) {
Twinkle.block[$(fieldset).prop('name')] = {};
$(fieldset).serializeArray().forEach((el) => {
// namespaces and pages for partial blocks are overwritten
// here, but we're handling them elsewhere so that's fine
Twinkle.block[$(fieldset).prop('name')][el.name] = el.value;
});
};
Twinkle.block.callback.change_block64 = function twinkleblockCallbackChangeBlock64(e) {
const $form = $(e.target.form),
$block64 = $form.find('[name=block64]'),
dialog = $form.closest('.morebits-dialog')[0];
// Show/hide block64 button
// Single IPv6, or IPv6 range smaller than a /64
const priorName = relevantUserName;
if ($block64.is(':checked')) {
relevantUserName = Morebits.ip.get64(mw.config.get('wgRelevantUserName'));
} else {
relevantUserName = mw.config.get('wgRelevantUserName');
}
// No templates for ranges, but if the original user is a single IP, offer the option
// (done separately in Twinkle.block.callback.issue_template)
const originalIsRange = Morebits.ip.isRange(mw.config.get('wgRelevantUserName'));
$form.find('[name=actiontype][value=template]').prop('disabled', originalIsRange).prop('checked', !originalIsRange);
// Refetch/reprocess user info then regenerate the main content
const regenerateForm = function() {
// Tweak titlebar text. In theory, we could save the dialog
// at initialization and then use `.setTitle`, but in practice that swallows
// the scriptName and requires `.display`ing, which jumps the
// window. It's just a line of text, so this is fine.
const titleBar = dialog.querySelector('.morebits-dialog-title').firstChild.nextSibling;
titleBar.nodeValue = titleBar.nodeValue.replace(priorName, relevantUserName);
// Tweak unblock link
const unblockLink = dialog.querySelector('.morebits-dialog-footerlinks a');
unblockLink.href = unblockLink.href.replace(priorName, relevantUserName);
unblockLink.title = unblockLink.title.replace(priorName, relevantUserName);
// Correct partial state
$form.find('[name=actiontype][value=partial]').prop('checked', Twinkle.getPref('defaultToPartialBlocks'));
if (blockedUserName === relevantUserName) {
$form.find('[name=actiontype][value=partial]').prop('checked', Twinkle.block.currentBlockInfo.partial === '');
}
// Set content appropriately
Twinkle.block.callback.change_action(e);
};
if (Twinkle.block.fetchedData[relevantUserName]) {
Twinkle.block.processUserInfo(Twinkle.block.fetchedData[relevantUserName], regenerateForm);
} else {
Twinkle.block.fetchUserInfo(regenerateForm);
}
};
Twinkle.block.callback.change_action = function twinkleblockCallbackChangeAction(e) {
let fieldPreset, fieldTemplateOptions, fieldBlockOptions;
const $form = $(e.target.form);
// Make ifs shorter
const blockBox = $form.find('[name=actiontype][value=block]').is(':checked');
const templateBox = $form.find('[name=actiontype][value=template]').is(':checked');
const $partial = $form.find('[name=actiontype][value=partial]');
const partialBox = $partial.is(':checked');
let blockGroup = partialBox ? Twinkle.block.blockGroupsPartial : Twinkle.block.blockGroups;
$partial.prop('disabled', !blockBox && !templateBox);
// Add current block parameters as default preset
const prior = { label: 'Prior block' };
if (blockedUserName === relevantUserName) {
Twinkle.block.blockPresetsInfo.prior = Twinkle.block.currentBlockInfo;
// value not a valid template selection, chosen below by setting templateName
prior.list = [{ label: 'Prior block settings', value: 'prior', selected: true }];
// Arrays of objects are annoying to check
if (!blockGroup.some((bg) => bg.label === prior.label)) {
blockGroup.push(prior);
}
// Always ensure proper template exists/is selected when switching modes
if (partialBox) {
Twinkle.block.blockPresetsInfo.prior.templateName = Morebits.string.isInfinity(Twinkle.block.currentBlockInfo.expiry) ? 'uw-pblockindef' : 'uw-pblock';
} else {
if (!Twinkle.block.isRegistered) {
Twinkle.block.blockPresetsInfo.prior.templateName = 'uw-ablock';
} else {
Twinkle.block.blockPresetsInfo.prior.templateName = Morebits.string.isInfinity(Twinkle.block.currentBlockInfo.expiry) ? 'uw-blockindef' : 'uw-block';
}
}
} else {
// But first remove any prior prior
blockGroup = blockGroup.filter((bg) => bg.label !== prior.label);
}
// Can be in preset or template field, so the old one in the template
// field will linger. No need to keep the old value around, so just
// remove it; saves trouble when hiding/evaluating
$form.find('[name=dstopic]').parent().remove();
Twinkle.block.callback.saveFieldset($('[name=field_block_options]'));
Twinkle.block.callback.saveFieldset($('[name=field_template_options]'));
if (blockBox) {
fieldPreset = new Morebits.QuickForm.Element({ type: 'field', label: 'Preset', name: 'field_preset' });
fieldPreset.append({
type: 'select',
name: 'preset',
label: 'Choose a preset:',
event: Twinkle.block.callback.change_preset,
list: Twinkle.block.callback.filtered_block_groups(blockGroup)
});
fieldBlockOptions = new Morebits.QuickForm.Element({ type: 'field', label: 'Block options', name: 'field_block_options' });
fieldBlockOptions.append({ type: 'div', name: 'currentblock', label: ' ' });
fieldBlockOptions.append({ type: 'div', name: 'hasblocklog', label: ' ' });
fieldBlockOptions.append({
type: 'select',
name: 'expiry_preset',
label: 'Expiry:',
event: Twinkle.block.callback.change_expiry,
list: [
{ label: 'custom', value: 'custom', selected: true },
{ label: 'indefinite', value: 'infinity' },
{ label: '3 hours', value: '3 hours' },
{ label: '12 hours', value: '12 hours' },
{ label: '24 hours', value: '24 hours' },
{ label: '31 hours', value: '31 hours' },
{ label: '36 hours', value: '36 hours' },
{ label: '48 hours', value: '48 hours' },
{ label: '60 hours', value: '60 hours' },
{ label: '72 hours', value: '72 hours' },
{ label: '1 week', value: '1 week' },
{ label: '2 weeks', value: '2 weeks' },
{ label: '1 month', value: '1 month' },
{ label: '3 months', value: '3 months' },
{ label: '6 months', value: '6 months' },
{ label: '1 year', value: '1 year' },
{ label: '2 years', value: '2 years' },
{ label: '3 years', value: '3 years' }
]
});
fieldBlockOptions.append({
type: 'input',
name: 'expiry',
label: 'Custom expiry',
tooltip: 'You can use relative times, like "1 minute" or "19 days", or absolute timestamps, "yyyymmddhhmm" (e.g. "200602011405" is Feb 1, 2006, at 14:05 UTC).',
value: Twinkle.block.field_block_options.expiry || Twinkle.block.field_template_options.template_expiry
});
if (partialBox) { // Partial block
fieldBlockOptions.append({
type: 'select',
multiple: true,
name: 'pagerestrictions',
label: 'Specific pages to block from editing',
value: '',
tooltip: '10 page max.'
});
const ns = fieldBlockOptions.append({
type: 'select',
multiple: true,
name: 'namespacerestrictions',
label: 'Namespace blocks',
value: '',
tooltip: 'Block from editing these namespaces.'
});
$.each(menuFormattedNamespaces, (number, name) => {
// Ignore -1: Special; -2: Media; and 2300-2303: Gadget (talk) and Gadget definition (talk)
if (number >= 0 && number < 830) {
ns.append({ type: 'option', label: name, value: number });
}
});
}
const blockoptions = [
{
checked: Twinkle.block.field_block_options.nocreate,
label: 'Block account creation',
name: 'nocreate',
value: '1'
}
];
if (Twinkle.block.isRegistered && !mw.util.isTemporaryUser(mw.config.get('wgRelevantUserName'))) {
blockoptions.push({
checked: Twinkle.block.field_block_options.noemail,
label: 'Block user from sending email',
name: 'noemail',
value: '1'
});
}
blockoptions.push({
checked: Twinkle.block.field_block_options.disabletalk,
label: 'Prevent this user from editing their own talk page while blocked',
name: 'disabletalk',
value: '1',
tooltip: partialBox ? 'If issuing a partial block, this MUST remain unchecked unless you are also preventing them from editing User talk space' : ''
});
if (Twinkle.block.isRegistered) {
blockoptions.push({
checked: Twinkle.block.field_block_options.autoblock,
label: 'Autoblock any IP addresses used (hardblock)',
name: 'autoblock',
value: '1'
});
} else {
blockoptions.push({
checked: Twinkle.block.field_block_options.hardblock,
label: 'Block logged-in users from using this IP address (hardblock)',
name: 'hardblock',
value: '1'
});
}
blockoptions.push({
checked: Twinkle.block.field_block_options.watchuser,
label: 'Watch user and user talk pages',
name: 'watchuser',
value: '1'
});
fieldBlockOptions.append({
type: 'checkbox',
name: 'blockoptions',
list: blockoptions
});
fieldBlockOptions.append({
type: 'textarea',
label: 'Reason (for block log):',
name: 'reason',
tooltip: 'Consider adding helpful details to the default message.',
value: Twinkle.block.field_block_options.reason
});
fieldBlockOptions.append({
type: 'div',
name: 'filerlog_label',
label: 'See also:',
style: 'display:inline-block;font-style:normal !important',
tooltip: 'Insert a "see also" message to indicate whether the filter log, deleted contributions or related temporary accounts played a role in the decision to block.'
});
fieldBlockOptions.append({
type: 'checkbox',
name: 'filter_see_also',
event: Twinkle.block.callback.toggle_see_alsos,
style: 'display:inline-block; margin-right:5px',
list: [
{
label: 'Filter log',
checked: false,
value: 'filter log'
}
]
});
fieldBlockOptions.append({
type: 'checkbox',
name: 'deleted_see_also',
event: Twinkle.block.callback.toggle_see_alsos,
style: 'display:inline-block; margin-right:5px',
list: [
{
label: 'Deleted contribs',
checked: false,
value: 'deleted contribs'
}
]
});
if (mw.util.isTemporaryUser(mw.config.get('wgRelevantUserName'))) {
fieldBlockOptions.append({
type: 'checkbox',
name: 'related_see_also',
event: Twinkle.block.callback.toggle_see_alsos,
style: 'display:inline-block',
list: [
{
label: 'Related temporary accounts',
checked: false,
value: 'related temporary accounts'
}
]
});
}
// Yet-another-logevents-doesn't-handle-ranges-well
if (blockedUserName === relevantUserName) {
fieldBlockOptions.append({ type: 'hidden', name: 'reblock', value: '1' });
}
}
// grab discretionary sanctions list from en-wiki
Twinkle.block.dsinfo = Morebits.wiki.getCachedJson('Template:Ds/topics.json');
Twinkle.block.dsinfo.then((dsinfo) => {
const $select = $('[name="dstopic"]');
const $options = $.map(dsinfo, (value, key) => $('<option>').val(value.code).text(key).prop('label', key));
$select.append($options);
});
// DS selection visible in either the template field set or preset,
// joint settings saved here
const dsSelectSettings = {
type: 'select',
name: 'dstopic',
label: 'DS topic',
value: '',
tooltip: 'If selected, it will inform the template and may be added to the blocking message',
event: Twinkle.block.callback.toggle_ds_reason
};
if (templateBox) {
fieldTemplateOptions = new Morebits.QuickForm.Element({ type: 'field', label: 'Template options', name: 'field_template_options' });
fieldTemplateOptions.append({
type: 'select',
name: 'template',
label: 'Choose talk page template:',
event: Twinkle.block.callback.change_template,
list: Twinkle.block.callback.filtered_block_groups(blockGroup, true),
value: Twinkle.block.field_template_options.template
});
// Only visible for aeblock and aepblock, toggled in change_template
fieldTemplateOptions.append(dsSelectSettings);
fieldTemplateOptions.append({
type: 'input',
name: 'article',
label: 'Linked page',
value: '',
tooltip: 'A page can be linked within the notice, perhaps if it was the primary target of disruption. Leave empty for no page to be linked.'
});
// Only visible if partial and not blocking
fieldTemplateOptions.append({
type: 'input',
name: 'area',
label: 'Area blocked from',
value: '',
tooltip: 'Optional explanation of the pages or namespaces the user was blocked from editing.'
});
if (!blockBox) {
fieldTemplateOptions.append({
type: 'input',
name: 'template_expiry',
label: 'Period of blocking:',
value: '',
tooltip: 'The period the blocking is due for, for example 24 hours, 2 weeks, indefinite etc...'
});
}
fieldTemplateOptions.append({
type: 'input',
name: 'block_reason',
label: '"You have been blocked for ..."',
tooltip: 'An optional reason, to replace the default generic reason. Only available for the generic block templates.',
value: Twinkle.block.field_template_options.block_reason
});
if (blockBox) {
fieldTemplateOptions.append({
type: 'checkbox',
name: 'blank_duration',
list: [
{
label: 'Do not include expiry in template',
checked: Twinkle.block.field_template_options.blank_duration,
tooltip: 'Instead of including the duration, make the block template read "You have been blocked temporarily..."'
}
]
});
} else {
fieldTemplateOptions.append({
type: 'checkbox',
list: [
{
label: 'Talk page access disabled',
name: 'notalk',
checked: Twinkle.block.field_template_options.notalk,
tooltip: 'Make the block template state that the user\'s talk page access has been removed'
},
{
label: 'User blocked from sending email',
name: 'noemail_template',
checked: Twinkle.block.field_template_options.noemail_template,
tooltip: 'If the area is not provided, make the block template state that the user\'s email access has been removed'
},
{
label: 'User blocked from creating accounts',
name: 'nocreate_template',
checked: Twinkle.block.field_template_options.nocreate_template,
tooltip: 'If the area is not provided, make the block template state that the user\'s ability to create accounts has been removed'
}
]
});
}
const $previewlink = $('<a id="twinkleblock-preview-link">Preview</a>');
$previewlink.off('click').on('click', () => {
Twinkle.block.callback.preview($form[0]);
});
$previewlink.css({cursor: 'pointer'});
fieldTemplateOptions.append({ type: 'div', id: 'blockpreview', label: [$previewlink[0]] });
fieldTemplateOptions.append({ type: 'div', id: 'twinkleblock-previewbox', style: 'display: none' });
} else if (fieldPreset) {
// Only visible for arbitration enforcement, toggled in change_preset
fieldPreset.append(dsSelectSettings);
}
let oldfield;
if (fieldPreset) {
oldfield = $form.find('fieldset[name="field_preset"]')[0];
oldfield.parentNode.replaceChild(fieldPreset.render(), oldfield);
} else {
$form.find('fieldset[name="field_preset"]').hide();
}
if (fieldBlockOptions) {
oldfield = $form.find('fieldset[name="field_block_options"]')[0];
oldfield.parentNode.replaceChild(fieldBlockOptions.render(), oldfield);
$form.find('fieldset[name="field_64"]').show();
$form.find('[name=pagerestrictions]').select2({
theme: 'default select2-morebits',
width: '100%',
placeholder: 'Select pages to block user from',
language: {
errorLoading: function() {
return 'Incomplete or invalid search term';
}
},
maximumSelectionLength: 10, // Software limitation [[phab:T202776]]
minimumInputLength: 1, // prevent ajax call when empty
ajax: {
url: mw.util.wikiScript('api'),
dataType: 'json',
delay: 100,
data: function(params) {
const title = mw.Title.newFromText(params.term);
if (!title) {
return;
}
return {
action: 'query',
format: 'json',
list: 'allpages',
apfrom: title.title,
apnamespace: title.namespace,
aplimit: '10'
};
},
processResults: function(data) {
return {
results: data.query.allpages.map((page) => {
const title = mw.Title.newFromText(page.title, page.ns).toText();
return {
id: title,
text: title
};
})
};
}
},
templateSelection: function(choice) {
return $('<a>').text(choice.text).attr({
href: mw.util.getUrl(choice.text),
target: '_blank'
});
}
});
$form.find('[name=namespacerestrictions]').select2({
theme: 'default select2-morebits',
width: '100%',
matcher: Morebits.select2.matchers.wordBeginning,
language: {
searching: Morebits.select2.queryInterceptor
},
templateResult: Morebits.select2.highlightSearchMatches,
placeholder: 'Select namespaces to block user from'
});
mw.util.addCSS(
// Reduce padding
'.select2-results .select2-results__option { padding-top: 1px; padding-bottom: 1px; }' +
// Adjust font size
'.select2-container .select2-dropdown .select2-results { font-size: 13px; }' +
'.select2-container .selection .select2-selection__rendered { font-size: 13px; }' +
// Remove black border
'.select2-container--default.select2-container--focus .select2-selection--multiple { border: 1px solid #aaa; }' +
// Make the tiny cross larger
'.select2-selection__choice__remove { font-size: 130%; }'
);
} else {
$form.find('fieldset[name="field_block_options"]').hide();
$form.find('fieldset[name="field_64"]').hide();
// Clear select2 options
$form.find('[name=pagerestrictions]').val(null).trigger('change');
$form.find('[name=namespacerestrictions]').val(null).trigger('change');
}
if (fieldTemplateOptions) {
oldfield = $form.find('fieldset[name="field_template_options"]')[0];
oldfield.parentNode.replaceChild(fieldTemplateOptions.render(), oldfield);
e.target.form.root.previewer = new Morebits.wiki.Preview($(e.target.form.root).find('#twinkleblock-previewbox').last()[0]);
} else {
$form.find('fieldset[name="field_template_options"]').hide();
}
// Any block, including ranges
if (Twinkle.block.currentBlockInfo) {
// false for an ip covered by a range or a smaller range within a larger range;
// true for a user, single ip block, or the exact range for a range block
const sameUser = blockedUserName === relevantUserName;
Morebits.Status.init($('div[name="currentblock"] span').last()[0]);
let statusStr = relevantUserName + ' is ' + (Twinkle.block.currentBlockInfo.partial === '' ? 'partially blocked' : 'blocked sitewide');
// Range blocked
if (Twinkle.block.currentBlockInfo.rangestart !== Twinkle.block.currentBlockInfo.rangeend) {
if (sameUser) {
statusStr += ' as a rangeblock';
} else {
statusStr += ' within a' + (Morebits.ip.get64(relevantUserName) === blockedUserName ? ' /64' : '') + ' rangeblock';
// Link to the full range
const $rangeblockloglink = $('<span>').append($('<a target="_blank" href="' + mw.util.getUrl('Special:Log', {action: 'view', page: blockedUserName, type: 'block'}) + '">' + blockedUserName + '</a>)'));
statusStr += ' (' + $rangeblockloglink.html() + ')';
}
}
if (Twinkle.block.currentBlockInfo.expiry === 'infinity') {
statusStr += ' (indefinite)';
} else if (new Morebits.Date(Twinkle.block.currentBlockInfo.expiry).isValid()) {
statusStr += ' (expires ' + new Morebits.Date(Twinkle.block.currentBlockInfo.expiry).calendar('utc') + ')';
}
let infoStr = 'This form will';
if (sameUser) {
infoStr += ' change that block';
if (Twinkle.block.currentBlockInfo.partial === undefined && partialBox) {
infoStr += ', converting it to a partial block';
} else if (Twinkle.block.currentBlockInfo.partial === '' && !partialBox) {
infoStr += ', converting it to a sitewide block';
}
infoStr += '.';
} else {
infoStr += ' add an additional ' + (partialBox ? 'partial ' : '') + 'block.';
}
Morebits.Status.warn(statusStr, infoStr);
// Default to the current block conditions on intial form generation
Twinkle.block.callback.update_form(e, Twinkle.block.currentBlockInfo);
}
// This is where T146628 really comes into play: a rangeblock will
// only return the correct block log if wgRelevantUserName is the
// exact range, not merely a funtional equivalent
if (Twinkle.block.hasBlockLog) {
const $blockloglink = $('<span>').append($('<a target="_blank" href="' + mw.util.getUrl('Special:Log', {action: 'view', page: relevantUserName, type: 'block'}) + '">block log</a>)'));
if (!Twinkle.block.currentBlockInfo) {
const lastBlockAction = Twinkle.block.blockLog[0];
if (lastBlockAction.action === 'unblock') {
$blockloglink.append(' (unblocked ' + new Morebits.Date(lastBlockAction.timestamp).calendar('utc') + ')');
} else { // block or reblock
$blockloglink.append(' (' + lastBlockAction.params.duration + ', expired ' + new Morebits.Date(lastBlockAction.params.expiry).calendar('utc') + ')');
}
}
Morebits.Status.init($('div[name="hasblocklog"] span').last()[0]);
Morebits.Status.warn(Twinkle.block.currentBlockInfo ? 'Previous blocks' : 'This ' + (Morebits.ip.isRange(relevantUserName) ? 'range' : 'user') + ' has been blocked in the past', $blockloglink[0]);
}
// Make sure all the fields are correct based on initial defaults
if (blockBox) {
Twinkle.block.callback.change_preset(e);
} else if (templateBox) {
Twinkle.block.callback.change_template(e);
}
};
/*
* Keep alphabetized by key name, Twinkle.block.blockGroups establishes
* the order they will appear in the interface
*
* Block preset format, all keys accept only 'true' (omit for false) except where noted:
* <title of block template> : {
* autoblock: <autoblock any IP addresses used (for registered users only)>
* disabletalk: <disable user from editing their own talk page while blocked>
* expiry: <string - expiry timestamp, can include relative times like "5 months", "2 weeks" etc>
* forIPsOnly: <show block option in the interface only if the relevant user is an IP>
* forTempAccountsOnly: <show block option in the interface only if the relevant user is a temporary account>
* forRegisteredOnly: <show block option in the interface only if the relevant user is a temporary account or regular account>
* label: <string - label for the option of the dropdown in the interface (keep brief)>
* noemail: prevent the user from sending email through Special:Emailuser
* pageParam: <set if the associated block template accepts a page parameter>
* prependReason: <string - prepends the value of 'reason' to the end of the existing reason, namely for when revoking talk page access>
* nocreate: <block account creation from the user's IP (for unregistered users only)>
* nonstandard: <template does not conform to stewardship of WikiProject User Warnings and may not accept standard parameters>
* reason: <string - block rationale, as would appear in the block log,
* and the edit summary for when adding block template, unless 'summary' is set>
* reasonParam: <set if the associated block template accepts a reason parameter>
* sig: <string - set to ~~~~ if block template does not accept "true" as the value, or set null to omit sig param altogether>
* summary: <string - edit summary for when adding block template to user's talk page, if not set, 'reason' is used>
* suppressArticleInSummary: <set to suppress showing the article name in the edit summary, as with attack pages>
* templateName: <string - name of template to use (instead of key name), entry will be omitted from the Templates list.
* (e.g. use another template but with different block options)>
* useInitialOptions: <when preset is chosen, only change given block options, leave others as they were>
*
* WARNING: 'anononly' and 'allowusertalk' are enabled by default.
* To disable, set 'hardblock' and 'disabletalk', respectively
*/
Twinkle.block.blockPresetsInfo = {
anonblock: {
expiry: '31 hours',
forIPsOnly: true,
nocreate: true,
nonstandard: true,
reason: '{{anonblock}}',
sig: '~~~~'
},
'anonblock - school': {
expiry: '36 hours',
forIPsOnly: true,
nocreate: true,
nonstandard: true,
reason: '{{anonblock}} <!-- Likely a school based on behavioral evidence -->',
templateName: 'anonblock',
sig: '~~~~'
},
'blocked proxy': {
expiry: '1 year',
forIPsOnly: true,
nocreate: true,
nonstandard: true,
hardblock: true,
reason: '{{blocked proxy}}',
sig: null
},
'CheckUser block': {
expiry: '1 week',
forIPsOnly: true,
nocreate: true,
nonstandard: true,
reason: '{{CheckUser block}}',
sig: '~~~~'
},
'checkuserblock-account': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
nonstandard: true,
reason: '{{checkuserblock-account}}',
sig: '~~~~'
},
'checkuserblock-wide': {
forIPsOnly: true,
nocreate: true,
nonstandard: true,
reason: '{{checkuserblock-wide}}',
sig: '~~~~'
},
colocationwebhost: {
expiry: '1 year',
forIPsOnly: true,
nonstandard: true,
reason: '{{colocationwebhost}}',
sig: null
},
oversightblock: {
autoblock: true,
expiry: 'infinity',
nocreate: true,
nonstandard: true,
reason: '{{OversightBlock}}',
sig: '~~~~'
},
'school block': {
forIPsOnly: true,
nocreate: true,
nonstandard: true,
reason: '{{school block}}',
sig: '~~~~'
},
spamblacklistblock: {
forIPsOnly: true,
expiry: '1 month',
disabletalk: true,
nocreate: true,
reason: '{{spamblacklistblock}} <!-- editor only attempts to add blacklisted links, see [[Special:Log/spamblacklist]] -->'
},
rangeblock: {
reason: '{{rangeblock}}',
nocreate: true,
nonstandard: true,
forIPsOnly: true,
sig: '~~~~'
},
tor: {
expiry: '1 year',
forIPsOnly: true,
nonstandard: true,
reason: '{{Tor}}',
sig: null
},
webhostblock: {
expiry: '1 year',
forIPsOnly: true,
nonstandard: true,
reason: '{{webhostblock}}',
sig: null
},
// uw-prefixed
'uw-3block': {
autoblock: true,
expiry: '24 hours',
nocreate: true,
pageParam: true,
reason: 'Violation of the [[WP:Three-revert rule|three-revert rule]]',
summary: 'You have been blocked from editing for violation of the [[WP:3RR|three-revert rule]]'
},
'uw-ablock': {
autoblock: true,
expiry: '31 hours',
forIPsOnly: true,
nocreate: true,
pageParam: true,
reasonParam: true,
summary: 'Your IP address has been blocked from editing',
suppressArticleInSummary: true
},
'uw-adblock': {
autoblock: true,
nocreate: true,
pageParam: true,
reason: 'Using Wikipedia for [[WP:Spam|spam]] or [[WP:NOTADVERTISING|advertising]] purposes',
summary: 'You have been blocked from editing for [[WP:SOAP|advertising or self-promotion]]'
},
'uw-aeblock': {
autoblock: true,
nocreate: true,
pageParam: true,
reason: '[[WP:Arbitration enforcement|Arbitration enforcement]]',
reasonParam: true,
summary: 'You have been blocked from editing for violating an [[WP:Arbitration|arbitration decision]]'
},
'uw-bioblock': {
autoblock: true,
nocreate: true,
pageParam: true,
reason: 'Violations of the [[WP:Biographies of living persons|biographies of living persons]] policy',
summary: 'You have been blocked from editing for violations of Wikipedia\'s [[WP:BLP|biographies of living persons policy]]'
},
'uw-block': {
autoblock: true,
expiry: '24 hours',
forRegisteredOnly: true,
nocreate: true,
pageParam: true,
reasonParam: true,
summary: 'You have been blocked from editing',
suppressArticleInSummary: true
},
'uw-blockindef': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
pageParam: true,
reasonParam: true,
summary: 'You have been indefinitely blocked from editing',
suppressArticleInSummary: true
},
'uw-blocknotalk': {
autoblock: true,
disabletalk: true,
nocreate: true,
pageParam: true,
reasonParam: true,
summary: 'You have been blocked from editing and your user talk page access has been disabled',
suppressArticleInSummary: true
},
'uw-botblock': {
forRegisteredOnly: true,
pageParam: true,
reason: 'Running a [[WP:BOT|bot script]] without [[WP:BRFA|approval]]',
summary: 'You have been blocked from editing because it appears you are running a [[WP:BOT|bot script]] without [[WP:BRFA|approval]]'
},
'uw-botublock': {
expiry: 'infinity',
forRegisteredOnly: true,
reason: '{{uw-botublock}} <!-- Username implies a bot, soft block -->',
summary: 'You have been indefinitely blocked from editing because your [[WP:U|username]] indicates this is a [[WP:BOT|bot]] account, which is currently not approved'
},
'uw-botuhblock': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
reason: '{{uw-botuhblock}} <!-- Username implies a bot, hard block -->',
summary: 'You have been indefinitely blocked from editing because your username is a blatant violation of the [[WP:U|username policy]]'
},
'uw-causeblock': {
expiry: 'infinity',
forRegisteredOnly: true,
reason: '{{uw-causeblock}} <!-- Username represents a non-profit, soft block -->',
summary: 'You have been indefinitely blocked from editing because your [[WP:U|username]] gives the impression that the account represents a group, organization or website'
},
'uw-compblock': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
reason: 'Compromised account',
summary: 'You have been indefinitely blocked from editing because it is believed that your [[WP:SECURE|account has been compromised]]'
},
'uw-copyrightblock': {
autoblock: true,
expiry: 'infinity',
nocreate: true,
pageParam: true,
reason: '[[WP:Copyright violations|Copyright violations]]',
summary: 'You have been blocked from editing for continued [[WP:COPYVIO|copyright infringement]]'
},
'uw-dblock': {
autoblock: true,
nocreate: true,
reason: 'Persistent removal of content',
pageParam: true,
summary: 'You have been blocked from editing for continued [[WP:VAND|removal of material]]'
},
'uw-disruptblock': {
autoblock: true,
nocreate: true,
reason: '[[WP:Disruptive editing|Disruptive editing]]',
summary: 'You have been blocked from editing for [[WP:DE|disruptive editing]]'
},
'uw-efblock': {
autoblock: true,
nocreate: true,
reason: 'Repeatedly triggering the [[WP:Edit filter|Edit filter]]',
summary: 'You have been blocked from editing for disruptive edits that repeatedly triggered the [[WP:EF|edit filter]]'
},
'uw-ewblock': {
autoblock: true,
expiry: '24 hours',
nocreate: true,
pageParam: true,
reason: '[[WP:Edit warring|Edit warring]]',
summary: 'You have been blocked from editing to prevent further [[WP:DE|disruption]] caused by your engagement in an [[WP:EW|edit war]]'
},
'uw-hblock': {
autoblock: true,
nocreate: true,
pageParam: true,
reason: '[[WP:No personal attacks|Personal attacks]] or [[WP:Harassment|harassment]]',
summary: 'You have been blocked from editing for attempting to [[WP:HARASS|harass]] other users'
},
'uw-ipevadeblock': {
forIPsOnly: true,
expiry: '1 week',
nocreate: true,
reason: '[[WP:Blocking policy#Evasion of blocks|Block evasion]]',
summary: 'Your IP address has been blocked from editing because it has been used to [[WP:EVADE|evade a previous block]]'
},
'uw-lblock': {
autoblock: true,
expiry: 'infinity',
nocreate: true,
reason: 'Making [[WP:No legal threats|legal threats]]',
summary: 'You have been blocked from editing for making [[WP:NLT|legal threats or taking legal action]]'
},
'uw-llmblock': {
autoblock: true,
expiry: 'infinity',
nocreate: true,
pageParam: true,
reason: 'Repeatedly misusing a [[WP:Disruptive editing#Persistent LLM use|large language model]]',
summary: 'You have been blocked from editing for repeatedly misusing a [[WP:Disruptive editing#Persistent LLM use|large language model]]'
},
'uw-nothereblock': {
autoblock: true,
expiry: 'infinity',
nocreate: true,
reason: 'Clearly [[WP:NOTHERE|not here to build an encyclopedia]]',
forRegisteredOnly: true,
summary: 'You have been indefinitely blocked from editing because it appears that you are not here to [[WP:NOTHERE|build an encyclopedia]]'
},
'uw-npblock': {
autoblock: true,
nocreate: true,
pageParam: true,
reason: 'Creating [[WP:Patent nonsense|patent nonsense]] or other inappropriate pages',
summary: 'You have been blocked from editing for creating [[WP:PN|nonsense pages]]'
},
'uw-pablock': {
autoblock: true,
expiry: '31 hours',
nocreate: true,
reason: '[[WP:No personal attacks|Personal attacks]] or [[WP:Harassment|harassment]]',
summary: 'You have been blocked from editing for making [[WP:NPA|personal attacks]] toward other users'
},
'uw-sblock': {
autoblock: true,
nocreate: true,
reason: 'Using Wikipedia for [[WP:SPAM|spam]] purposes',
summary: 'You have been blocked from editing for using Wikipedia for [[WP:SPAM|spam]] purposes'
},
'uw-soablock': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
pageParam: true,
reason: '[[WP:Spam|Spam]] / [[WP:NOTADVERTISING|advertising]]-only account',
summary: 'You have been indefinitely blocked from editing because your account is being used only for [[WP:SPAM|spam, advertising, or promotion]]'
},
'uw-socialmediablock': {
autoblock: true,
nocreate: true,
pageParam: true,
reason: 'Using Wikipedia as a [[WP:NOTMYSPACE|blog, web host, social networking site or forum]]',
summary: 'You have been blocked from editing for using user and/or article pages as a [[WP:NOTMYSPACE|blog, web host, social networking site or forum]]'
},
'uw-sockblock': {
autoblock: true,
forRegisteredOnly: true,
nocreate: true,
reason: 'Abusing [[WP:Sock puppetry|multiple accounts]]',
summary: 'You have been blocked from editing for abusing [[WP:SOCK|multiple accounts]]'
},
'uw-softerblock': {
expiry: 'infinity',
forRegisteredOnly: true,
reason: '{{uw-softerblock}} <!-- Promotional username, soft block -->',
summary: 'You have been indefinitely blocked from editing because your [[WP:U|username]] gives the impression that the account represents a group, organization or website'
},
'uw-spamublock': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
reason: '{{uw-spamublock}} <!-- Promotional username, promotional edits -->',
summary: 'You have been indefinitely blocked from editing because your account is being used only for [[WP:SPAM|spam or advertising]] and your username is a violation of the [[WP:U|username policy]]'
},
'uw-spoablock': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
reason: '[[WP:SOCK|Sock puppetry]]',
summary: 'This account has been blocked as a [[WP:SOCK|sock puppet]] created to violate Wikipedia policy'
},
'uw-talkrevoked': {
disabletalk: true,
reason: 'Revoking talk page access: inappropriate use of user talk page while blocked',
prependReason: true,
summary: 'Your user talk page access has been disabled',
useInitialOptions: true
},
'uw-tempevadeblock': {
autoblock: true,
expiry: 'infinity',
forTempAccountsOnly: true,
nocreate: true,
reason: '[[WP:Blocking policy#Evasion of blocks|Block evasion]]',
summary: 'Your temporary account has been blocked from editing because it has been used to [[WP:EVADE|evade a previous block]]'
},
'uw-ublock': {
expiry: 'infinity',
forRegisteredOnly: true,
reason: '{{uw-ublock}} <!-- Username violation, soft block -->',
reasonParam: true,
summary: 'You have been indefinitely blocked from editing because your username is a violation of the [[WP:U|username policy]]'
},
'uw-ublock-double': {
expiry: 'infinity',
forRegisteredOnly: true,
reason: '{{uw-ublock-double}} <!-- Username closely resembles another user, soft block -->',
summary: 'You have been indefinitely blocked from editing because your [[WP:U|username]] is too similar to the username of another Wikipedia user'
},
'uw-ucblock': {
autoblock: true,
expiry: '31 hours',
nocreate: true,
pageParam: true,
reason: 'Persistent addition of [[WP:INTREF|unsourced content]]',
summary: 'You have been blocked from editing for persistent addition of [[WP:INTREF|unsourced content]]'
},
'uw-uhblock': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
reason: '{{uw-uhblock}} <!-- Username violation, hard block -->',
reasonParam: true,
summary: 'You have been indefinitely blocked from editing because your username is a blatant violation of the [[WP:U|username policy]]'
},
'uw-ublock-wellknown': {
expiry: 'infinity',
forRegisteredOnly: true,
reason: '{{uw-ublock-wellknown}} <!-- Username represents a well-known person, soft block -->',
summary: 'You have been indefinitely blocked from editing because your [[WP:U|username]] matches the name of a well-known living individual'
},
'uw-uhblock-double': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
reason: '{{uw-uhblock-double}} <!-- Attempted impersonation of another user, hard block -->',
summary: 'You have been indefinitely blocked from editing because your [[WP:U|username]] appears to impersonate another established Wikipedia user'
},
'uw-upeblock': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
pageParam: true,
reason: '[[WP:PAID|Undisclosed paid editing]] in violation of the WMF [[WP:TOU|Terms of Use]]',
summary: 'You have been indefinitely blocked from editing because your account is being used in violation of [[WP:PAID|Wikipedia policy on undisclosed paid advocacy]]'
},
'uw-vaublock': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
pageParam: true,
reason: '{{uw-vaublock}} <!-- Username violation, vandalism-only account -->',
summary: 'You have been indefinitely blocked from editing because your account is being [[WP:DISRUPTONLY|used only for vandalism]] and your username is a blatant violation of the [[WP:U|username policy]]'
},
'uw-vblock': {
autoblock: true,
expiry: '31 hours',
nocreate: true,
pageParam: true,
reason: '[[WP:Vandalism|Vandalism]]',
summary: 'You have been blocked from editing to prevent further [[WP:VAND|vandalism]]'
},
'uw-voablock': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
pageParam: true,
reason: '[[WP:DISRUPTONLY|Vandalism-only account]]',
summary: 'You have been indefinitely blocked from editing because your account is being [[WP:DISRUPTONLY|used only for vandalism]]'
},
'zombie proxy': {
expiry: '1 month',
forIPsOnly: true,
nocreate: true,
nonstandard: true,
reason: '{{zombie proxy}}',
sig: null
},
// Begin partial block templates, accessed in Twinkle.block.blockGroupsPartial
'uw-acpblock': {
autoblock: true,
expiry: '48 hours',
nocreate: true,
pageParam: false,
reasonParam: true,
reason: 'Misusing [[WP:Sock puppetry|multiple accounts]]',
summary: 'You have been [[WP:PB|blocked from creating accounts]] for misusing [[WP:SOCK|multiple accounts]]'
},
'uw-acpblockindef': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
pageParam: false,
reasonParam: true,
reason: 'Misusing [[WP:Sock puppetry|multiple accounts]]',
summary: 'You have been indefinitely [[WP:PB|blocked from creating accounts]] for misusing [[WP:SOCK|multiple accounts]]'
},
'uw-aepblock': {
autoblock: true,
nocreate: false,
pageParam: false,
reason: '[[WP:Arbitration enforcement|Arbitration enforcement]]',
reasonParam: true,
summary: 'You have been [[WP:PB|partially blocked]] from editing for violating an [[WP:Arbitration|arbitration decision]]'
},
'uw-epblock': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: false,
noemail: true,
pageParam: false,
reasonParam: true,
reason: 'Email [[WP:Harassment|harassment]]',
summary: 'You have been [[WP:PB|blocked from emailing]] other editors for [[WP:Harassment|harassment]]'
},
'uw-ewpblock': {
autoblock: true,
expiry: '24 hours',
nocreate: false,
pageParam: false,
reasonParam: true,
reason: '[[WP:Edit warring|Edit warring]]',
summary: 'You have been [[WP:PB|partially blocked]] from editing certain areas of the encyclopedia to prevent further [[WP:DE|disruption]] due to [[WP:EW|edit warring]]'
},
'uw-llmpblock': {
autoblock: true,
expiry: 'infinity',
nocreate: true,
pageParam: true,
reasonParam: true,
reason: 'Repeatedly misusing a [[WP:Disruptive editing#Persistent LLM use|large language model]]',
summary: 'You have been blocked from editing for repeatedly misusing a [[WP:Disruptive editing#Persistent LLM use|large language model]]'
},
'uw-pblock': {
autoblock: true,
expiry: '24 hours',
nocreate: false,
pageParam: false,
reasonParam: true,
summary: 'You have been [[WP:PB|partially blocked]] from certain areas of the encyclopedia'
},
'uw-pblockindef': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: false,
pageParam: false,
reasonParam: true,
summary: 'You have been indefinitely [[WP:PB|partially blocked]] from certain areas of the encyclopedia'
}
};
Twinkle.block.transformBlockPresets = function twinkleblockTransformBlockPresets() {
// supply sensible defaults
$.each(Twinkle.block.blockPresetsInfo, (preset, settings) => {
settings.summary = settings.summary || settings.reason;
settings.sig = settings.sig !== undefined ? settings.sig : 'yes';
settings.indefinite = settings.indefinite || Morebits.string.isInfinity(settings.expiry);
if (!Twinkle.block.isRegistered && settings.indefinite) {
settings.expiry = '31 hours';
} else {
settings.expiry = settings.expiry || '31 hours';
}
Twinkle.block.blockPresetsInfo[preset] = settings;
});
};
// These are the groups of presets and defines the order in which they appear. For each list item:
// label: <string, the description that will be visible in the dropdown>
// value: <string, the key of a preset in blockPresetsInfo>
Twinkle.block.blockGroups = [
{
label: 'Common block reasons',
list: [
{ label: 'Anonblock', value: 'anonblock' },
{ label: 'Anonblock – likely a school', value: 'anonblock - school' },
{ label: 'School block', value: 'school block' },
{ label: 'Generic block (custom reason)', value: 'uw-block' }, // ends up being default for registered users
{ label: 'Generic block (custom reason) – IP', value: 'uw-ablock', selected: true }, // set only when blocking IP
{ label: 'Generic block (custom reason) – indefinite', value: 'uw-blockindef' },
{ label: 'Disruptive editing', value: 'uw-disruptblock' },
{ label: 'Inappropriate use of user talk page while blocked', value: 'uw-talkrevoked' },
{ label: 'Not here to build an encyclopedia', value: 'uw-nothereblock' },
{ label: 'Unsourced content', value: 'uw-ucblock' },
{ label: 'Vandalism', value: 'uw-vblock' },
{ label: 'Vandalism-only account', value: 'uw-voablock' }
]
},
{
label: 'Extended reasons',
list: [
{ label: 'Advertising', value: 'uw-adblock' },
{ label: 'Arbitration enforcement', value: 'uw-aeblock' },
{ label: 'Block evasion – IP', value: 'uw-ipevadeblock' },
{ label: 'Block evasion – temporary account', value: 'uw-tempevadeblock' },
{ label: 'BLP violations', value: 'uw-bioblock' },
{ label: 'Copyright violations', value: 'uw-copyrightblock' },
{ label: 'Creating nonsense pages', value: 'uw-npblock' },
{ label: 'Edit filter-related', value: 'uw-efblock' },
{ label: 'Edit warring', value: 'uw-ewblock' },
{ label: 'Generic block with talk page access revoked', value: 'uw-blocknotalk' },
{ label: 'Harassment', value: 'uw-hblock' },
{ label: 'Legal threats', value: 'uw-lblock' },
{ label: 'LLM misuse', value: 'uw-llmblock' },
{ label: 'Personal attacks or harassment', value: 'uw-pablock' },
{ label: 'Possible compromised account', value: 'uw-compblock' },
{ label: 'Removal of content', value: 'uw-dblock' },
{ label: 'Sock puppetry (master)', value: 'uw-sockblock' },
{ label: 'Sock puppetry (puppet)', value: 'uw-spoablock' },
{ label: 'Social networking', value: 'uw-socialmediablock' },
{ label: 'Spam', value: 'uw-sblock' },
{ label: 'Spam/advertising-only account', value: 'uw-soablock' },
{ label: 'Unapproved bot', value: 'uw-botblock' },
{ label: 'Undisclosed paid editing', value: 'uw-upeblock' },
{ label: 'Violating the three-revert rule', value: 'uw-3block' }
]
},
{
label: 'Username violations',
list: [
{ label: 'Bot username, soft block', value: 'uw-botublock' },
{ label: 'Bot username, hard block', value: 'uw-botuhblock' },
{ label: 'Promotional username, hard block', value: 'uw-spamublock' },
{ label: 'Promotional username, soft block', value: 'uw-softerblock' },
{ label: 'Similar username, soft block', value: 'uw-ublock-double' },
{ label: 'Username violation, soft block', value: 'uw-ublock' },
{ label: 'Username violation, hard block', value: 'uw-uhblock' },
{ label: 'Username impersonation, hard block', value: 'uw-uhblock-double' },
{ label: 'Username represents a well-known person, soft block', value: 'uw-ublock-wellknown' },
{ label: 'Username represents a non-profit, soft block', value: 'uw-causeblock' },
{ label: 'Username violation, vandalism-only account', value: 'uw-vaublock' }
]
},
{
label: 'Templated reasons',
list: [
{ label: 'Blocked proxy', value: 'blocked proxy' },
{ label: 'CheckUser block', value: 'CheckUser block' },
{ label: 'CheckUser block – account', value: 'checkuserblock-account' },
{ label: 'CheckUser block – wide', value: 'checkuserblock-wide' },
{ label: 'Colocation webhost', value: 'colocationwebhost' },
{ label: 'Oversight block', value: 'oversightblock' },
{ label: 'Rangeblock', value: 'rangeblock' }, // Only for IP ranges, selected for non-/64 ranges in filtered_block_groups
{ label: 'Spam blacklist block', value: 'spamblacklistblock' },
{ label: 'Tor', value: 'tor' },
{ label: 'Webhost block', value: 'webhostblock' },
{ label: 'Zombie proxy', value: 'zombie proxy' }
]
}
];
Twinkle.block.blockGroupsPartial = [
{
label: 'Common partial block reasons',
list: [
{ label: 'Generic partial block (custom reason)', value: 'uw-pblock', selected: true },
{ label: 'Generic partial block (custom reason) – indefinite', value: 'uw-pblockindef' },
{ label: 'Edit warring', value: 'uw-ewpblock' }
]
},
{
label: 'Extended partial block reasons',
list: [
{ label: 'Arbitration enforcement', value: 'uw-aepblock' },
{ label: 'Email harassment', value: 'uw-epblock' },
{ label: 'LLM misuse', value: 'uw-llmpblock' },
{ label: 'Misusing multiple accounts', value: 'uw-acpblock' },
{ label: 'Misusing multiple accounts – indefinite', value: 'uw-acpblockindef' }
]
}
];
Twinkle.block.callback.filtered_block_groups = function twinkleblockCallbackFilteredBlockGroups(group, showTemplate) {
return $.map(group, (blockGroup) => {
const list = $.map(blockGroup.list, (blockPreset) => {
switch (blockPreset.value) {
case 'uw-talkrevoked':
if (blockedUserName !== relevantUserName) {
return;
}
break;
case 'rangeblock':
if (!Morebits.ip.isRange(relevantUserName)) {
return;
}
blockPreset.selected = !Morebits.ip.get64(relevantUserName);
break;
case 'CheckUser block':
case 'checkuserblock-account':
case 'checkuserblock-wide':
if (!Morebits.userIsInGroup('checkuser')) {
return;
}
break;
case 'oversightblock':
if (!Morebits.userIsInGroup('suppress')) {
return;
}
break;
default:
break;
}
const blockSettings = Twinkle.block.blockPresetsInfo[blockPreset.value];
let allowedUserType;
// for regular users and temporary accounts
if (blockSettings.forRegisteredOnly) {
allowedUserType = Twinkle.block.isRegistered;
// for temporary accounts
} else if (blockSettings.forTempAccountsOnly) {
allowedUserType = mw.util.isTemporaryUser(mw.config.get('wgRelevantUserName'));
// for IPs
} else if (blockSettings.forIPsOnly) {
allowedUserType = !Twinkle.block.isRegistered;
} else {
allowedUserType = true;
}
if (!(blockSettings.templateName && showTemplate) && allowedUserType) {
const templateName = blockSettings.templateName || blockPreset.value;
return {
label: (showTemplate ? '{{' + templateName + '}}: ' : '') + blockPreset.label,
value: blockPreset.value,
data: [{
name: 'template-name',
value: templateName
}],
selected: !!blockPreset.selected,
disabled: !!blockPreset.disabled
};
}
});
if (list.length) {
return {
label: blockGroup.label,
list: list
};
}
});
};
Twinkle.block.callback.change_preset = function twinkleblockCallbackChangePreset(e) {
const form = e.target.form, key = form.preset.value;
if (!key) {
return;
}
Twinkle.block.callback.update_form(e, Twinkle.block.blockPresetsInfo[key]);
if (form.template) {
form.template.value = Twinkle.block.blockPresetsInfo[key].templateName || key;
Twinkle.block.callback.change_template(e);
} else {
Morebits.QuickForm.setElementVisibility(form.dstopic.parentNode, key === 'uw-aeblock' || key === 'uw-aepblock');
}
};
Twinkle.block.callback.change_expiry = function twinkleblockCallbackChangeExpiry(e) {
const expiry = e.target.form.expiry;
if (e.target.value === 'custom') {
Morebits.QuickForm.setElementVisibility(expiry.parentNode, true);
} else {
Morebits.QuickForm.setElementVisibility(expiry.parentNode, false);
expiry.value = e.target.value;
}
};
Twinkle.block.seeAlsos = [];
Twinkle.block.callback.toggle_see_alsos = function twinkleblockCallbackToggleSeeAlso() {
const joinEnum = function(e) {
if (e.length >= 3) {
return e.slice(0, -1).join(', ') + ' and ' + e[e.length - 1];
} else {
return e.join(' and ');
}
};
const reason = this.form.reason.value.replace(
new RegExp('( <!--|;) see also ' + joinEnum(Twinkle.block.seeAlsos) + '( -->)?'), ''
);
Twinkle.block.seeAlsos = Twinkle.block.seeAlsos.filter((el) => el !== this.value);
if (this.checked) {
Twinkle.block.seeAlsos.push(this.value);
}
const seeAlsoMessage = joinEnum(Twinkle.block.seeAlsos);
if (!Twinkle.block.seeAlsos.length) {
this.form.reason.value = reason;
} else if (reason.includes('{{')) {
this.form.reason.value = reason + ' <!-- see also ' + seeAlsoMessage + ' -->';
} else {
this.form.reason.value = reason + '; see also ' + seeAlsoMessage;
}
};
Twinkle.block.dsReason = '';
Twinkle.block.callback.toggle_ds_reason = function twinkleblockCallbackToggleDSReason() {
const reason = this.form.reason.value.replace(
new RegExp(' ?\\(\\[\\[' + Twinkle.block.dsReason + '\\]\\]\\)'), ''
);
Twinkle.block.dsinfo.then((dsinfo) => {
const sanctionCode = this.selectedIndex;
const sanctionName = this.options[sanctionCode].label;
Twinkle.block.dsReason = dsinfo[sanctionName].page;
if (!this.value) {
this.form.reason.value = reason;
} else {
this.form.reason.value = reason + ' ([[' + Twinkle.block.dsReason + ']])';
}
});
};
Twinkle.block.callback.update_form = function twinkleblockCallbackUpdateForm(e, data) {
const form = e.target.form;
let expiry = data.expiry;
// don't override original expiry if useInitialOptions is set
if (!data.useInitialOptions) {
if (Date.parse(expiry)) {
expiry = new Date(expiry).toGMTString();
form.expiry_preset.value = 'custom';
} else {
form.expiry_preset.value = data.expiry || 'custom';
}
form.expiry.value = expiry;
if (form.expiry_preset.value === 'custom') {
Morebits.QuickForm.setElementVisibility(form.expiry.parentNode, true);
} else {
Morebits.QuickForm.setElementVisibility(form.expiry.parentNode, false);
}
}
// boolean-flipped options, more at [[mw:API:Block]]
data.disabletalk = data.disabletalk !== undefined ? data.disabletalk : false;
data.hardblock = data.hardblock !== undefined ? data.hardblock : false;
// disable autoblock if blocking a bot
if (Twinkle.block.userIsBot || /bot\b/i.test(relevantUserName)) {
data.autoblock = false;
}
$(form).find('[name=field_block_options]').find(':checkbox').each((i, el) => {
// don't override original options if useInitialOptions is set
if (data.useInitialOptions && data[el.name] === undefined) {
return;
}
const check = data[el.name] === '' || !!data[el.name];
$(el).prop('checked', check);
});
if (data.prependReason && data.reason) {
form.reason.value = data.reason + '; ' + form.reason.value;
} else {
form.reason.value = data.reason || '';
}
// Clear and/or set any partial page or namespace restrictions
if (form.pagerestrictions) {
const $pageSelect = $(form).find('[name=pagerestrictions]');
const $namespaceSelect = $(form).find('[name=namespacerestrictions]');
// Respect useInitialOptions by clearing data when switching presets
// In practice, this will always clear, since no partial presets use it
if (!data.useInitialOptions) {
$pageSelect.val(null).trigger('change');
$namespaceSelect.val(null).trigger('change');
}
// Add any preset options; in practice, just used for prior block settings
if (data.restrictions) {
if (data.restrictions.pages && !$pageSelect.val().length) {
const pages = data.restrictions.pages.map((pr) => pr.title);
// since page restrictions use an ajax source, we
// short-circuit that and just add a new option
pages.forEach((page) => {
if (!$pageSelect.find("option[value='" + $.escapeSelector(page) + "']").length) {
const newOption = new Option(page, page, true, true);
$pageSelect.append(newOption);
}
});
$pageSelect.val($pageSelect.val().concat(pages)).trigger('change');
}
if (data.restrictions.namespaces) {
$namespaceSelect.val($namespaceSelect.val().concat(data.restrictions.namespaces)).trigger('change');
}
}
}
};
Twinkle.block.callback.change_template = function twinkleblockcallbackChangeTemplate(e) {
const form = e.target.form, value = form.template.value, settings = Twinkle.block.blockPresetsInfo[value];
const blockBox = $(form).find('[name=actiontype][value=block]').is(':checked');
const partialBox = $(form).find('[name=actiontype][value=partial]').is(':checked');
const templateBox = $(form).find('[name=actiontype][value=template]').is(':checked');
// Block form is not present
if (!blockBox) {
if (settings.indefinite || settings.nonstandard) {
if (Twinkle.block.prev_template_expiry === null) {
Twinkle.block.prev_template_expiry = form.template_expiry.value || '';
}
form.template_expiry.parentNode.style.display = 'none';
form.template_expiry.value = 'infinity';
} else if (form.template_expiry.parentNode.style.display === 'none') {
if (Twinkle.block.prev_template_expiry !== null) {
form.template_expiry.value = Twinkle.block.prev_template_expiry;
Twinkle.block.prev_template_expiry = null;
}
form.template_expiry.parentNode.style.display = 'block';
}
if (Twinkle.block.prev_template_expiry) {
form.expiry.value = Twinkle.block.prev_template_expiry;
}
Morebits.QuickForm.setElementVisibility(form.notalk.parentNode, !settings.nonstandard);
// Partial
Morebits.QuickForm.setElementVisibility(form.noemail_template.parentNode, partialBox);
Morebits.QuickForm.setElementVisibility(form.nocreate_template.parentNode, partialBox);
} else if (templateBox) { // Only present if block && template forms both visible
Morebits.QuickForm.setElementVisibility(
form.blank_duration.parentNode,
!settings.indefinite && !settings.nonstandard
);
}
Morebits.QuickForm.setElementVisibility(form.dstopic.parentNode, value === 'uw-aeblock' || value === 'uw-aepblock');
// Only particularly relevant if template form is present
Morebits.QuickForm.setElementVisibility(form.article.parentNode, settings && !!settings.pageParam);
Morebits.QuickForm.setElementVisibility(form.block_reason.parentNode, settings && !!settings.reasonParam);
// Partial block
Morebits.QuickForm.setElementVisibility(form.area.parentNode, partialBox && !blockBox);
form.root.previewer.closePreview();
};
Twinkle.block.prev_template_expiry = null;
Twinkle.block.callback.preview = function twinkleblockcallbackPreview(form) {
const params = {
article: form.article.value,
blank_duration: form.blank_duration ? form.blank_duration.checked : false,
disabletalk: form.disabletalk.checked || (form.notalk ? form.notalk.checked : false),
expiry: form.template_expiry ? form.template_expiry.value : form.expiry.value,
hardblock: Twinkle.block.isRegistered ? form.autoblock.checked : form.hardblock.checked,
indefinite: Morebits.string.isInfinity(form.template_expiry ? form.template_expiry.value : form.expiry.value),
reason: form.block_reason.value,
template: form.template.value,
dstopic: form.dstopic.value,
partial: $(form).find('[name=actiontype][value=partial]').is(':checked'),
pagerestrictions: $(form.pagerestrictions).val() || [],
namespacerestrictions: $(form.namespacerestrictions).val() || [],
noemail: form.noemail.checked || (form.noemail_template ? form.noemail_template.checked : false),
nocreate: form.nocreate.checked || (form.nocreate_template ? form.nocreate_template.checked : false),
area: form.area.value
};
const templateText = Twinkle.block.callback.getBlockNoticeWikitext(params);
form.previewer.beginRender(templateText, 'User_talk:' + relevantUserName); // Force wikitext/correct username
};
Twinkle.block.callback.evaluate = function twinkleblockCallbackEvaluate(e) {
const $form = $(e.target),
toBlock = $form.find('[name=actiontype][value=block]').is(':checked'),
toWarn = $form.find('[name=actiontype][value=template]').is(':checked'),
toPartial = $form.find('[name=actiontype][value=partial]').is(':checked');
let blockoptions = {}, templateoptions = {};
Twinkle.block.callback.saveFieldset($form.find('[name=field_block_options]'));
Twinkle.block.callback.saveFieldset($form.find('[name=field_template_options]'));
blockoptions = Twinkle.block.field_block_options;
templateoptions = Twinkle.block.field_template_options;
templateoptions.disabletalk = !!(templateoptions.disabletalk || blockoptions.disabletalk);
templateoptions.hardblock = !!blockoptions.hardblock;
delete blockoptions.expiry_preset; // remove extraneous
// Partial API requires this to be gone, not false or 0
if (toPartial) {
blockoptions.partial = templateoptions.partial = true;
}
templateoptions.pagerestrictions = $form.find('[name=pagerestrictions]').val() || [];
templateoptions.namespacerestrictions = $form.find('[name=namespacerestrictions]').val() || [];
// Format for API here rather than in saveFieldset
blockoptions.pagerestrictions = templateoptions.pagerestrictions.join('|');
blockoptions.namespacerestrictions = templateoptions.namespacerestrictions.join('|');
// use block settings as warn options where not supplied
templateoptions.summary = templateoptions.summary || blockoptions.reason;
templateoptions.expiry = templateoptions.template_expiry || blockoptions.expiry;
if (toBlock) {
if (blockoptions.partial) {
if (blockoptions.disabletalk && !blockoptions.namespacerestrictions.includes('3')) {
return alert('Partial blocks cannot prevent talk page access unless also restricting them from editing User talk space!');
}
if (!blockoptions.namespacerestrictions && !blockoptions.pagerestrictions) {
if (!blockoptions.noemail && !blockoptions.nocreate) { // Blank entries technically allowed [[phab:T208645]]
return alert('No pages or namespaces were selected, nor were email or account creation restrictions applied; please select at least one option to apply a partial block!');
} else if ((templateoptions.template !== 'uw-epblock' || $form.find('select[name="preset"]').val() !== 'uw-epblock') &&
// Don't require confirmation if email harassment defaults are set
!confirm('You are about to block with no restrictions on page or namespace editing, are you sure you want to proceed?')) {
return;
}
}
}
if (!blockoptions.expiry) {
return alert('Please provide an expiry!');
} else if (Morebits.string.isInfinity(blockoptions.expiry) && !Twinkle.block.isRegistered) {
return alert("Can't indefinitely block an IP address!");
}
if (!blockoptions.reason) {
return alert('Please provide a reason for the block!');
}
Morebits.SimpleWindow.setButtonsEnabled(false);
Morebits.Status.init(e.target);
const statusElement = new Morebits.Status('Executing block');
blockoptions.action = 'block';
blockoptions.user = relevantUserName;
// boolean-flipped options
blockoptions.anononly = blockoptions.hardblock ? undefined : true;
blockoptions.allowusertalk = blockoptions.disabletalk ? undefined : true;
/*
Check if block status changed while processing the form.
There's a lot to consider here. list=blocks provides the
current block status, but there are at least two issues with
relying on it. First, the id doesn't update on a reblock,
meaning the individual parameters need to be compared. This
can be done roughly with JSON.stringify - we can thankfully
rely on order from the server, although sorting would be
fine if not - but falsey values are problematic and is
non-ideal. More importantly, list=blocks won't indicate if a
non-blocked user is blocked then unblocked. This should be
exceedingy rare, but regardless, we thus need to check
list=logevents, which has a nicely updating logid
parameter. We can't rely just on that, though, since it
doesn't account for blocks that have expired on their own.
As such, we use both. Using some ternaries, the logid
variables are false if there's no logevents, so if they
aren't equal we defintely have a changed entry (send
confirmation). If they are equal, then either the user was
never blocked (the block statuses will be equal, no
confirmation) or there's no new block, in which case either
a block expired (different statuses, confirmation) or the
same block is still active (same status, no confirmation).
*/
const query = {
format: 'json',
action: 'query',
list: 'blocks|logevents',
letype: 'block',
lelimit: 1,
letitle: 'User:' + blockoptions.user
};
// bkusers doesn't catch single IPs blocked as part of a range block
if (mw.util.isIPAddress(blockoptions.user, true)) {
query.bkip = blockoptions.user;
} else {
query.bkusers = blockoptions.user;
}
api.get(query).then((data) => {
let block = data.query.blocks[0];
// As with the initial data fetch, if an IP is blocked
// *and* rangeblocked, this would only grab whichever
// block is more recent, which would likely mean a
// mismatch. However, if the rangeblock is updated
// while filling out the form, this won't detect that,
// but that's probably fine.
if (data.query.blocks.length > 1 && block.user !== relevantUserName) {
block = data.query.blocks[1];
}
const logevents = data.query.logevents[0];
const logid = data.query.logevents.length ? logevents.logid : false;
if (logid !== Twinkle.block.blockLogId || !!block !== !!Twinkle.block.currentBlockInfo) {
let message = 'The block status of ' + blockoptions.user + ' has changed. ';
if (block) {
message += 'New status: ';
} else {
message += 'Last entry: ';
}
let logExpiry = '';
if (logevents.params.duration) {
if (logevents.params.duration === 'infinity') {
logExpiry = 'indefinitely';
} else {
const expiryDate = new Morebits.Date(logevents.params.expiry);
logExpiry += (expiryDate.isBefore(new Date()) ? ', expired ' : ' until ') + expiryDate.calendar();
}
} else { // no duration, action=unblock, just show timestamp
logExpiry = ' ' + new Morebits.Date(logevents.timestamp).calendar();
}
message += Morebits.string.toUpperCaseFirstChar(logevents.action) + 'ed by ' + logevents.user + logExpiry +
' for "' + logevents.comment + '". Do you want to override with your settings?';
if (!confirm(message)) {
Morebits.Status.info('Executing block', 'Canceled by user');
return;
}
blockoptions.reblock = 1; // Writing over a block will fail otherwise
}
// execute block
blockoptions.token = mw.user.tokens.get('csrfToken');
const mbApi = new Morebits.wiki.Api('Executing block', blockoptions, (() => {
statusElement.info('Completed');
if (toWarn) {
Twinkle.block.callback.issue_template(templateoptions);
}
}));
mbApi.post();
});
} else if (toWarn) {
Morebits.SimpleWindow.setButtonsEnabled(false);
Morebits.Status.init(e.target);
Twinkle.block.callback.issue_template(templateoptions);
} else {
return alert('Please give Twinkle something to do!');
}
};
Twinkle.block.callback.issue_template = function twinkleblockCallbackIssueTemplate(formData) {
// Use wgRelevantUserName to ensure the block template goes to a single IP and not to the
// "talk page" of an IP range (which does not exist)
const userTalkPage = 'User_talk:' + mw.config.get('wgRelevantUserName');
const params = Twinkle.block.combineFormDataAndFieldTemplateOptions(
formData,
Twinkle.block.blockPresetsInfo[formData.template],
Twinkle.block.field_template_options.block_reason,
Twinkle.block.field_template_options.notalk,
Twinkle.block.field_template_options.noemail_template,
Twinkle.block.field_template_options.nocreate_template
);
Morebits.wiki.actionCompleted.redirect = userTalkPage;
Morebits.wiki.actionCompleted.notice = 'Actions complete, loading user talk page in a few seconds';
const wikipediaPage = new Morebits.wiki.Page(userTalkPage, 'User talk page modification');
wikipediaPage.setCallbackParameters(params);
wikipediaPage.load(Twinkle.block.callback.main);
};
Twinkle.block.combineFormDataAndFieldTemplateOptions = function(formData, messageData, reason, disabletalk, noemail, nocreate) {
return $.extend(formData, {
messageData: messageData,
reason: reason,
disabletalk: disabletalk,
noemail: noemail,
nocreate: nocreate
});
};
Twinkle.block.callback.getBlockNoticeWikitext = function(params) {
let text = '{{';
const settings = Twinkle.block.blockPresetsInfo[params.template];
if (!settings.nonstandard) {
text += 'subst:' + params.template;
if (params.article && settings.pageParam) {
text += '|page=' + params.article;
}
if (params.dstopic) {
text += '|topic=' + params.dstopic;
}
if (!/te?mp|^\s*$|min/.exec(params.expiry)) {
if (params.indefinite) {
text += '|indef=yes';
} else if (!params.blank_duration && !new Morebits.Date(params.expiry).isValid()) {
// Block template wants a duration, not date
text += '|time=' + params.expiry;
}
}
if (!Twinkle.block.isRegistered && !params.hardblock) {
text += '|anon=yes';
}
if (params.reason) {
text += '|reason=' + params.reason;
}
if (params.disabletalk) {
text += '|notalk=yes';
}
// Currently, all partial block templates are "standard"
// Building the template, however, takes a fair bit of logic
if (params.partial) {
if (params.pagerestrictions.length || params.namespacerestrictions.length) {
const makeSentence = function(array) {
if (array.length < 3) {
return array.join(' and ');
}
const last = array.pop();
return array.join(', ') + ', and ' + last;
};
text += '|area=' + (params.indefinite ? 'certain ' : 'from certain ');
if (params.pagerestrictions.length) {
text += 'pages (' + makeSentence(params.pagerestrictions.map((p) => '[[:' + p + ']]'));
text += params.namespacerestrictions.length ? ') and certain ' : ')';
}
if (params.namespacerestrictions.length) {
// 1 => Talk, 2 => User, etc.
const namespaceNames = params.namespacerestrictions.map((id) => menuFormattedNamespaces[id]);
text += '[[Wikipedia:Namespace|namespaces]] (' + makeSentence(namespaceNames) + ')';
}
} else if (params.area) {
text += '|area=' + params.area;
} else {
if (params.noemail) {
text += '|email=yes';
}
if (params.nocreate) {
text += '|accountcreate=yes';
}
}
}
} else {
text += params.template;
}
if (settings.sig) {
text += '|sig=' + settings.sig;
}
return text + '}}';
};
Twinkle.block.callback.main = function twinkleblockcallbackMain(pageobj) {
const params = pageobj.getCallbackParameters(),
date = new Morebits.Date(pageobj.getLoadTime()),
messageData = params.messageData;
let text;
params.indefinite = Morebits.string.isInfinity(params.expiry);
if (Twinkle.getPref('blankTalkpageOnIndefBlock') && params.template !== 'uw-lblock' && params.indefinite) {
Morebits.Status.info('Info', 'Blanking talk page per preferences and creating a new talk page section for this month');
text = date.monthHeader() + '\n';
} else {
text = pageobj.getPageText();
const dateHeaderRegex = date.monthHeaderRegex();
let dateHeaderRegexLast, dateHeaderRegexResult;
while ((dateHeaderRegexLast = dateHeaderRegex.exec(text)) !== null) {
dateHeaderRegexResult = dateHeaderRegexLast;
}
// If dateHeaderRegexResult is null then lastHeaderIndex is never checked. If it is not null but
// \n== is not found, then the date header must be at the very start of the page. lastIndexOf
// returns -1 in this case, so lastHeaderIndex gets set to 0 as desired.
const lastHeaderIndex = text.lastIndexOf('\n==') + 1;
if (text.length > 0) {
text += '\n\n';
}
if (!dateHeaderRegexResult || dateHeaderRegexResult.index !== lastHeaderIndex) {
Morebits.Status.info('Info', 'Will create a new talk page section for this month, as none was found');
text += date.monthHeader() + '\n';
}
}
params.expiry = typeof params.template_expiry !== 'undefined' ? params.template_expiry : params.expiry;
text += Twinkle.block.callback.getBlockNoticeWikitext(params);
// build the edit summary
let summary = messageData.summary;
if (messageData.suppressArticleInSummary !== true && params.article) {
summary += ' on [[:' + params.article + ']]';
}
summary += '.';
pageobj.setPageText(text);
pageobj.setEditSummary(summary);
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.setWatchlist(Twinkle.getPref('watchWarnings'));
pageobj.save();
};
Twinkle.addInitCallback(Twinkle.block, 'block');
}());
// </nowiki>
kyg8v6bqmbx8d42sv0oxzk5dz4wzx8s
769494
769472
2026-10-21T14:55:04Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at f47c434: tr block
769494
javascript
text/javascript
/*
Twinkle: adapted for Japanese Wikipedia; upstream authors and Japanese adaptations are credited in UPSTREAM.md.
Upstream source: https://github.com/wikimedia-gadgets/twinkle/blob/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/src/modules/twinkleblock.js
Upstream author history: https://github.com/wikimedia-gadgets/twinkle/commits/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/src/modules/twinkleblock.js
License: CC BY-SA 3.0 Unported. See https://github.com/wikimedia-gadgets/twinkle/blob/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/LICENSE.txt
Japanese adaptation: Syunsyunminmin; includes later synchronization changes.
*/
// <nowiki>
(function() {
const api = new mw.Api();
let relevantUserName, blockedUserName, blockWindow;
const menuFormattedNamespaces = $.extend({}, mw.config.get('wgFormattedNamespaces'));
menuFormattedNamespaces[0] = '(Article)';
/*
****************************************
*** twinkleblock.js: Block module
****************************************
* Mode of invocation: Tab ("Block")
* Active on: Any page with relevant user name (userspace, contribs, etc.)
*/
Twinkle.block = function twinkleblock() {
relevantUserName = mw.config.get('wgRelevantUserName');
// should show on Contributions or Block pages, anywhere there's a relevant user
// Ignore ranges wider than the CIDR limit
if (Morebits.userIsSysop && relevantUserName && (!Morebits.ip.isRange(relevantUserName) || Morebits.ip.validCIDR(relevantUserName))) {
Twinkle.addPortletLink(Twinkle.block.callback, 'Block', 'tw-block', '利用者をブロック');
}
};
Twinkle.block.callback = function twinkleblockCallback() {
if (relevantUserName === mw.config.get('wgUserName') &&
!confirm('自分自身をブロックしようとしています!続行しますか?')) {
return;
}
Twinkle.block.currentBlockInfo = undefined;
Twinkle.block.field_block_options = {};
Twinkle.block.field_template_options = {};
blockWindow = new Morebits.SimpleWindow(650, 530);
// need to be verbose about who we're blocking
blockWindow.setTitle(relevantUserName + 'をブロックまたは投稿ブロックテンプレートを送信する');
blockWindow.setScriptName('Twinkle');
// Always added, hidden later if actual user not blocked
blockWindow.addFooterLink('ブロックを解除', 'Special:Unblock/' + relevantUserName);
blockWindow.addFooterLink('投稿ブロックテンプレート', 'Wikipedia:主要なテンプレート/利用者‐会話名前空間#投稿ブロックテンプレート');
blockWindow.addFooterLink('投稿ブロックの方針', 'WP:BLOCK');
blockWindow.addFooterLink('ブロック設定', 'User:Syunsyunminmin/Twinkle/Preferences#block');
blockWindow.addFooterLink('Twinkle help', ':en:WP:TW/DOC#block');
blockWindow.addFooterLink('フィードバック', 'User talk:Syunsyunminmin/Twinkle');
const form = new Morebits.QuickForm(Twinkle.block.callback.evaluate);
const actionfield = form.append({
type: 'field',
label: '操作の種類'
});
actionfield.append({
type: 'checkbox',
name: 'actiontype',
event: Twinkle.block.callback.change_action,
list: [
{
label: '利用者をブロック',
value: 'block',
tooltip: '指定されたオプションで対象利用者をブロックします。部分ブロックのチェックが外れている場合、サイト全体のブロックとなります。',
checked: true
},
{
label: '部分ブロック',
value: 'partial',
tooltip: '部分ブロックと部分ブロックテンプレートを有効にする。',
checked: Twinkle.getPref('defaultToPartialBlocks') // Overridden if already blocked
},
{
label: '利用者会話ページに投稿ブロックテンプレートを追加',
value: 'template',
tooltip: '(※未対応)ブロックした管理者がブロックテンプレートを送信するのを忘れた場合、あるいは、テンプレートを送信せずに利用者をブロックした場合、これを使って適切なテンプレートを送信することができます。部分ブロックテンプレートは部分ブロックボックスにチェックを入れてください。',
// Disallow when viewing the block dialog on an IP range
checked: false,
disabled: Morebits.ip.isRange(relevantUserName)
}
]
});
/*
Add option for IPv6 ranges smaller than /64 to upgrade to the 64
CIDR ([[WP:/64]]). This is one of the few places where we want
wgRelevantUserName since this depends entirely on the original user.
In theory, we shouldn't use Morebits.ip.get64 here since since we want
to exclude functionally-equivalent /64s. That'd be:
// if (mw.util.isIPv6Address(mw.config.get('wgRelevantUserName'), true) &&
// (mw.util.isIPv6Address(mw.config.get('wgRelevantUserName')) || parseInt(mw.config.get('wgRelevantUserName').replace(/^(.+?)\/?(\d{1,3})?$/, '$2'), 10) > 64)) {
In practice, though, since functionally-equivalent ranges are
(mis)treated as separate by MediaWiki's logging ([[phab:T146628]]),
using Morebits.ip.get64 provides a modicum of relief in thise case.
*/
const sixtyFour = Morebits.ip.get64(mw.config.get('wgRelevantUserName'));
if (sixtyFour && sixtyFour !== mw.config.get('wgRelevantUserName')) {
const block64field = form.append({
type: 'field',
label: '/64範囲ブロックに変換',
name: 'field_64'
});
block64field.append({
type: 'div',
style: 'margin-bottom: 0.5em',
label: ['通常は、より良いとは言わないまでも、', $.parseHTML('<a target="_blank" href="' + mw.util.getUrl(':en:WP:/64') + '">/64の範囲ブロック</a>')[0], 'するだけで問題ないです (',
$.parseHTML('<a target="_blank" href="' + mw.util.getUrl('Special:Contributions/' + sixtyFour) + '">' + sixtyFour + '</a>)')[0], ').']
});
block64field.append({
type: 'checkbox',
name: 'block64',
event: Twinkle.block.callback.change_block64,
list: [{
checked: Twinkle.getPref('defaultToBlock64'),
label: '/64を代わりにブロックする',
value: 'block64',
tooltip: Morebits.ip.isRange(mw.config.get('wgRelevantUserName')) ? 'Will eschew leaving a template.' : 'Any template issued will go to the original IP: ' + mw.config.get('wgRelevantUserName')
}]
});
}
form.append({ type: 'field', label: 'プリセット', name: 'field_preset' });
form.append({ type: 'field', label: 'テンプレートオプション', name: 'field_template_options' });
form.append({ type: 'field', label: 'ブロックオプション', name: 'field_block_options' });
form.append({ type: 'submit' });
const result = form.render();
blockWindow.setContent(result);
blockWindow.display();
result.root = result;
Twinkle.block.fetchUserInfo(() => {
// Toggle initial partial state depending on prior block type,
// will override the defaultToPartialBlocks pref
if (blockedUserName === relevantUserName) {
$(result).find('[name=actiontype][value=partial]').prop('checked', Twinkle.block.currentBlockInfo.partial === '');
}
// clean up preset data (defaults, etc.), done exactly once, must be before Twinkle.block.callback.change_action is called
Twinkle.block.transformBlockPresets();
// init the controls after user and block info have been fetched
const evt = document.createEvent('Event');
evt.initEvent('change', true, true);
if (result.block64 && result.block64.checked) {
// Calls the same change_action event once finished
result.block64.dispatchEvent(evt);
} else {
result.actiontype[0].dispatchEvent(evt);
}
});
};
// Store fetched user data, only relevant if switching IPv6 to a /64
Twinkle.block.fetchedData = {};
// Processes the data from a query response, separated from
// Twinkle.block.fetchUserInfo to allow reprocessing of already-fetched data
Twinkle.block.processUserInfo = function twinkleblockProcessUserInfo(data, fn) {
let blockinfo = data.query.blocks[0];
// Soft redirect to Special:Block if the user is multi-blocked (#2178)
if (blockinfo && data.query.blocks.length > 1) {
// Remove submission buttons.
blockWindow.$dialog.find('.morebits-dialog-buttons').empty();
Morebits.Status.init(blockWindow.content.querySelector('form'));
Morebits.Status.warn(
`この対象には有効なブロックが${data.query.blocks.length}件あります`,
`Twinkleは複数ブロックに対応していません。[[Special:Block/${relevantUserName}]]を使用してください。`
);
return;
}
const userinfo = data.query.users[0];
// If an IP is blocked *and* rangeblocked, the above finds
// whichever block is more recent, not necessarily correct.
// Three seems... unlikely
if (data.query.blocks.length > 1 && blockinfo.user !== relevantUserName) {
blockinfo = data.query.blocks[1];
}
// Cache response, used when toggling /64 blocks
Twinkle.block.fetchedData[userinfo.name] = data;
Twinkle.block.isRegistered = !!userinfo.userid;
if (Twinkle.block.isRegistered) {
Twinkle.block.userIsBot = !!userinfo.groupmemberships && userinfo.groupmemberships.map((e) => e.group).includes('bot');
} else {
Twinkle.block.userIsBot = false;
}
if (blockinfo) {
// handle frustrating system of inverted boolean values
blockinfo.disabletalk = blockinfo.allowusertalk === undefined;
blockinfo.hardblock = blockinfo.anononly === undefined;
}
// will undefine if no blocks present
Twinkle.block.currentBlockInfo = blockinfo;
blockedUserName = Twinkle.block.currentBlockInfo && Twinkle.block.currentBlockInfo.user;
// Toggle unblock link if not the user in question; always first
const unblockLink = blockWindow.$dialog.find('.morebits-dialog-footerlinks a')[0];
if (blockedUserName !== relevantUserName) {
unblockLink.hidden = true;
unblockLink.nextSibling.hidden = true; // link+trailing bullet
} else {
unblockLink.hidden = false;
unblockLink.nextSibling.hidden = false; // link+trailing bullet
}
// Semi-busted on ranges, see [[phab:T270737]] and [[phab:T146628]].
// Basically, logevents doesn't treat functionally-equivalent ranges
// as equivalent, meaning any functionally-equivalent IP range is
// misinterpreted by the log throughout. Without logevents
// redirecting (like Special:Block does) we would need a function to
// parse ranges, which is a pain. IPUtils has the code, but it'd be a
// lot of cruft for one purpose.
Twinkle.block.hasBlockLog = !!data.query.logevents.length;
Twinkle.block.blockLog = Twinkle.block.hasBlockLog && data.query.logevents;
// Used later to check if block status changed while filling out the form
Twinkle.block.blockLogId = Twinkle.block.hasBlockLog ? data.query.logevents[0].logid : false;
if (typeof fn === 'function') {
return fn();
}
};
Twinkle.block.fetchUserInfo = function twinkleblockFetchUserInfo(fn) {
const query = {
format: 'json',
action: 'query',
list: 'blocks|users|logevents',
letype: 'block',
lelimit: 1,
letitle: 'User:' + relevantUserName,
bkprop: 'expiry|reason|flags|restrictions|range|user',
ususers: relevantUserName
};
// bkusers doesn't catch single IPs blocked as part of a range block
if (mw.util.isIPAddress(relevantUserName, true)) {
query.bkip = relevantUserName;
} else {
query.bkusers = relevantUserName;
// groupmemberships only relevant for registered users
query.usprop = 'groupmemberships';
}
api.get(query).then((data) => {
Twinkle.block.processUserInfo(data, fn);
}, (msg) => {
Morebits.Status.init($('div[name="currentblock"] span').last()[0]);
Morebits.Status.warn('利用者情報の取得エラー', msg);
});
};
Twinkle.block.callback.saveFieldset = function twinkleblockCallbacksaveFieldset(fieldset) {
Twinkle.block[$(fieldset).prop('name')] = {};
$(fieldset).serializeArray().forEach((el) => {
// namespaces and pages for partial blocks are overwritten
// here, but we're handling them elsewhere so that's fine
Twinkle.block[$(fieldset).prop('name')][el.name] = el.value;
});
};
Twinkle.block.callback.change_block64 = function twinkleblockCallbackChangeBlock64(e) {
const $form = $(e.target.form),
$block64 = $form.find('[name=block64]'),
dialog = $form.closest('.morebits-dialog')[0];
// Show/hide block64 button
// Single IPv6, or IPv6 range smaller than a /64
const priorName = relevantUserName;
if ($block64.is(':checked')) {
relevantUserName = Morebits.ip.get64(mw.config.get('wgRelevantUserName'));
} else {
relevantUserName = mw.config.get('wgRelevantUserName');
}
// No templates for ranges, but if the original user is a single IP, offer the option
// (done separately in Twinkle.block.callback.issue_template)
const originalIsRange = Morebits.ip.isRange(mw.config.get('wgRelevantUserName'));
$form.find('[name=actiontype][value=template]').prop('disabled', originalIsRange);
// Refetch/reprocess user info then regenerate the main content
const regenerateForm = function() {
// Tweak titlebar text. In theory, we could save the dialog
// at initialization and then use `.setTitle`, but in practice that swallows
// the scriptName and requires `.display`ing, which jumps the
// window. It's just a line of text, so this is fine.
const titleBar = dialog.querySelector('.morebits-dialog-title').firstChild.nextSibling;
titleBar.nodeValue = titleBar.nodeValue.replace(priorName, relevantUserName);
// Tweak unblock link
const unblockLink = dialog.querySelector('.morebits-dialog-footerlinks a');
unblockLink.href = unblockLink.href.replace(priorName, relevantUserName);
unblockLink.title = unblockLink.title.replace(priorName, relevantUserName);
// Correct partial state
$form.find('[name=actiontype][value=partial]').prop('checked', Twinkle.getPref('defaultToPartialBlocks'));
if (blockedUserName === relevantUserName) {
$form.find('[name=actiontype][value=partial]').prop('checked', Twinkle.block.currentBlockInfo.partial === '');
}
// Set content appropriately
Twinkle.block.callback.change_action(e);
};
if (Twinkle.block.fetchedData[relevantUserName]) {
Twinkle.block.processUserInfo(Twinkle.block.fetchedData[relevantUserName], regenerateForm);
} else {
Twinkle.block.fetchUserInfo(regenerateForm);
}
};
Twinkle.block.callback.change_action = function twinkleblockCallbackChangeAction(e) {
let fieldPreset, fieldTemplateOptions, fieldBlockOptions;
const $form = $(e.target.form);
// Make ifs shorter
const blockBox = $form.find('[name=actiontype][value=block]').is(':checked');
const templateBox = $form.find('[name=actiontype][value=template]').is(':checked');
const $partial = $form.find('[name=actiontype][value=partial]');
const partialBox = $partial.is(':checked');
let blockGroup = partialBox ? Twinkle.block.blockGroupsPartial : Twinkle.block.blockGroups;
$partial.prop('disabled', !blockBox && !templateBox);
// Add current block parameters as default preset
const prior = { label: '前ブロック' };
if (blockedUserName === relevantUserName) {
Twinkle.block.blockPresetsInfo.prior = Twinkle.block.currentBlockInfo;
// value not a valid template selection, chosen below by setting templateName
prior.list = [{ label: '前のブロック設定', value: 'prior', selected: true }];
// Arrays of objects are annoying to check
if (!blockGroup.some((bg) => bg.label === prior.label)) {
blockGroup.push(prior);
}
// Always ensure proper template exists/is selected when switching modes
if (partialBox) {
Twinkle.block.blockPresetsInfo.prior.templateName = Morebits.string.isInfinity(Twinkle.block.currentBlockInfo.expiry) ? 'uw-pblockindef' : 'uw-pblock';
} else {
if (!Twinkle.block.isRegistered) {
Twinkle.block.blockPresetsInfo.prior.templateName = 'uw-ablock';
} else {
Twinkle.block.blockPresetsInfo.prior.templateName = Morebits.string.isInfinity(Twinkle.block.currentBlockInfo.expiry) ? 'uw-blockindef' : 'uw-block';
}
}
} else {
// But first remove any prior prior
blockGroup = blockGroup.filter((bg) => bg.label !== prior.label);
}
Twinkle.block.callback.saveFieldset($('[name=field_block_options]'));
Twinkle.block.callback.saveFieldset($('[name=field_template_options]'));
if (blockBox) {
fieldPreset = new Morebits.QuickForm.Element({ type: 'field', label: 'プリセット', name: 'field_preset' });
fieldPreset.append({
type: 'select',
name: 'preset',
label: 'プリセットを選択:',
event: Twinkle.block.callback.change_preset,
list: Twinkle.block.callback.filtered_block_groups(blockGroup)
});
fieldBlockOptions = new Morebits.QuickForm.Element({ type: 'field', label: 'ブロックオプション', name: 'field_block_options' });
fieldBlockOptions.append({ type: 'div', name: 'currentblock', label: ' ' });
fieldBlockOptions.append({ type: 'div', name: 'hasblocklog', label: ' ' });
fieldBlockOptions.append({
type: 'select',
name: 'expiry_preset',
label: '期限:',
event: Twinkle.block.callback.change_expiry,
list: [
{ label: 'その他', value: 'custom', selected: true },
{ label: '無期限', value: 'infinity' },
{ label: '3時間', value: '3 hours' },
{ label: '12時間', value: '12 hours' },
{ label: '24時間', value: '24 hours' },
{ label: '31時間', value: '31 hours' },
{ label: '36時間', value: '36 hours' },
{ label: '48時間', value: '48 hours' },
{ label: '60時間', value: '60 hours' },
{ label: '72時間', value: '72 hours' },
{ label: '1週間', value: '1 week' },
{ label: '2週間', value: '2 weeks' },
{ label: '1ヶ月間', value: '1 month' },
{ label: '3ヶ月間', value: '3 months' },
{ label: '6ヶ月間', value: '6 months' },
{ label: '1年間', value: '1 year' },
{ label: '2年間', value: '2 years' },
{ label: '3年間', value: '3 years' }
]
});
fieldBlockOptions.append({
type: 'input',
name: 'expiry',
label: 'その他の期限',
tooltip: '"1 minute" (1分) や "19 days" (19日) のような相対的な時間や、"yyyymmddhhmm"のような絶対的なタイムスタンプを使用することができます。 (例: "200602011405"は2006年2月1日 14:05 UTCを意味します)',
value: Twinkle.block.field_block_options.expiry || Twinkle.block.field_template_options.template_expiry
});
if (partialBox) { // Partial block
fieldBlockOptions.append({
type: 'select',
multiple: true,
name: 'pagerestrictions',
label: '編集をブロックする特定のページ',
value: '',
tooltip: '最大で10ページ'
});
const ns = fieldBlockOptions.append({
type: 'select',
multiple: true,
name: 'namespacerestrictions',
label: '名前空間のブロック',
value: '',
tooltip: 'これらの名前空間の編集をブロックする'
});
$.each(menuFormattedNamespaces, (number, name) => {
// Ignore -1: Special; -2: Media; and 2300-2303: Gadget (talk) and Gadget definition (talk)
if (number >= 0 && number < 830) {
ns.append({ type: 'option', label: name, value: number });
}
});
}
const blockoptions = [
{
checked: Twinkle.block.field_block_options.nocreate,
label: 'アカウント作成をブロック',
name: 'nocreate',
value: '1'
}
];
if (Twinkle.block.isRegistered && !mw.util.isTemporaryUser(mw.config.get('wgRelevantUserName'))) {
blockoptions.push({
checked: Twinkle.block.field_block_options.noemail,
label: '利用者のメール送信をブロック',
name: 'noemail',
value: '1'
});
}
blockoptions.push({
checked: Twinkle.block.field_block_options.disabletalk,
label: 'ブロックされている間、この利用者が自分の会話ページを編集できないようにする。',
name: 'disabletalk',
value: '1',
tooltip: partialBox ? '部分ブロックを行う場合、利用者会話名前空間の編集をブロックしない限り、このチェックは外したままにしておかなければなりません。' : ''
});
if (Twinkle.block.isRegistered) {
blockoptions.push({
checked: Twinkle.block.field_block_options.autoblock,
label: '使用されたIPアドレスを自動ブロック',
name: 'autoblock',
value: '1'
});
} else {
blockoptions.push({
checked: Twinkle.block.field_block_options.hardblock,
label: 'ログイン利用者がこのIPアドレスを使用するのをブロックする (ハードブロック)',
name: 'hardblock',
value: '1'
});
}
blockoptions.push({
checked: Twinkle.block.field_block_options.watchuser,
label: '利用者(会話)ページをウォッチする',
name: 'watchuser',
value: '1'
});
fieldBlockOptions.append({
type: 'checkbox',
name: 'blockoptions',
list: blockoptions
});
fieldBlockOptions.append({
type: 'textarea',
label: '理由 (ブロック記録用):',
name: 'reason',
tooltip: '既定のブロックメッセージに追加して役立つ詳細を入力してください',
value: Twinkle.block.field_block_options.reason
});
fieldBlockOptions.append({
type: 'div',
name: 'filerlog_label',
label: '参照:',
style: 'display:inline-block;font-style:normal !important',
tooltip: 'フィルター記録、削除された投稿、関連する一時アカウントがブロックの判断に関わったことを示す参照情報を追加します。'
});
fieldBlockOptions.append({
type: 'checkbox',
name: 'filter_see_also',
event: Twinkle.block.callback.toggle_see_alsos,
style: 'display:inline-block; margin-right:5px',
list: [
{
label: 'フィルター記録',
checked: false,
value: 'フィルター記録'
}
]
});
fieldBlockOptions.append({
type: 'checkbox',
name: 'deleted_see_also',
event: Twinkle.block.callback.toggle_see_alsos,
style: 'display:inline-block; margin-right:5px',
list: [
{
label: '削除された投稿',
checked: false,
value: '削除された投稿記録'
}
]
});
if (mw.util.isTemporaryUser(mw.config.get('wgRelevantUserName'))) {
fieldBlockOptions.append({
type: 'checkbox',
name: 'related_see_also',
event: Twinkle.block.callback.toggle_see_alsos,
style: 'display:inline-block',
list: [
{
label: '関連する一時アカウント',
checked: false,
value: '関連する一時アカウント'
}
]
});
}
// Yet-another-logevents-doesn't-handle-ranges-well
if (blockedUserName === relevantUserName) {
fieldBlockOptions.append({ type: 'hidden', name: 'reblock', value: '1' });
}
}
if (templateBox) {
fieldTemplateOptions = new Morebits.QuickForm.Element({ type: 'field', label: 'テンプレートオプション', name: 'field_template_options' });
fieldTemplateOptions.append({
type: 'select',
name: 'template',
label: '会話ページのテンプレートを選択:',
event: Twinkle.block.callback.change_template,
list: Twinkle.block.callback.filtered_block_groups(blockGroup, true),
value: Twinkle.block.field_template_options.template
});
fieldTemplateOptions.append({
type: 'input',
name: 'article',
label: 'リンクするページ',
value: '',
tooltip: 'そのページが破壊行為の主な対象であった場合、通知内でリンクされることがあります。リンクするページがない場合は空欄にしてください。'
});
// Only visible if partial and not blocking
fieldTemplateOptions.append({
type: 'input',
name: 'area',
label: 'ブロックされた場所',
value: '',
tooltip: 'オプションで、利用者が編集をブロックされたページまたは名前空間の説明。'
});
if (!blockBox) {
fieldTemplateOptions.append({
type: 'input',
name: 'template_expiry',
label: 'ブロック期間:',
value: '',
tooltip: 'ブロックする期間。例えば24 hours、2 weeks、indefiniteなど'
});
}
fieldTemplateOptions.append({
type: 'input',
name: 'block_reason',
label: '"あなたがブロックされたのは..."',
tooltip: 'デフォルトの一般的な理由を置き換えるオプションの理由。全般の投稿ブロックテンプレートでのみ利用可能です。',
value: Twinkle.block.field_template_options.block_reason
});
if (blockBox) {
fieldTemplateOptions.append({
type: 'checkbox',
name: 'blank_duration',
list: [
{
label: 'テンプレートに有効期限を含めない',
checked: Twinkle.block.field_template_options.blank_duration,
tooltip: '期間を含める代わりに、ブロックテンプレートは「あなたは一時的にブロックされました」と表示します..."'
}
]
});
} else {
fieldTemplateOptions.append({
type: 'checkbox',
list: [
{
label: '会話ページの編集を禁止',
name: 'notalk',
checked: Twinkle.block.field_template_options.notalk,
tooltip: '会話ページの編集権限を剥奪したことを投稿ブロックテンプレートに記載します。'
},
{
label: '利用者のメール送信をブロック',
name: 'noemail_template',
checked: Twinkle.block.field_template_options.noemail_template,
tooltip: '対象範囲が未指定の場合、メール送信をブロックしたことを投稿ブロックテンプレートに記載します。'
},
{
label: '利用者のアカウント作成をブロック',
name: 'nocreate_template',
checked: Twinkle.block.field_template_options.nocreate_template,
tooltip: '対象範囲が未指定の場合、アカウント作成をブロックしたことを投稿ブロックテンプレートに記載します。'
}
]
});
}
const $previewlink = $('<a id="twinkleblock-preview-link">プレビュー</a>');
$previewlink.off('click').on('click', () => {
Twinkle.block.callback.preview($form[0]);
});
$previewlink.css({cursor: 'pointer'});
fieldTemplateOptions.append({ type: 'div', id: 'blockpreview', label: [$previewlink[0]] });
fieldTemplateOptions.append({ type: 'div', id: 'twinkleblock-previewbox', style: 'display: none' });
}
let oldfield;
if (fieldPreset) {
oldfield = $form.find('fieldset[name="field_preset"]')[0];
oldfield.parentNode.replaceChild(fieldPreset.render(), oldfield);
} else {
$form.find('fieldset[name="field_preset"]').hide();
}
if (fieldBlockOptions) {
oldfield = $form.find('fieldset[name="field_block_options"]')[0];
oldfield.parentNode.replaceChild(fieldBlockOptions.render(), oldfield);
$form.find('fieldset[name="field_64"]').show();
$form.find('[name=pagerestrictions]').select2({
theme: 'default select2-morebits',
width: '100%',
placeholder: '利用者をブロックするページを選択',
language: {
errorLoading: function() {
return '不完全または無効な検索キーワード';
}
},
maximumSelectionLength: 10, // Software limitation [[phab:T202776]]
minimumInputLength: 1, // prevent ajax call when empty
ajax: {
url: mw.util.wikiScript('api'),
dataType: 'json',
delay: 100,
data: function(params) {
const title = mw.Title.newFromText(params.term);
if (!title) {
return;
}
return {
action: 'query',
format: 'json',
list: 'allpages',
apfrom: title.title,
apnamespace: title.namespace,
aplimit: '10'
};
},
processResults: function(data) {
return {
results: data.query.allpages.map((page) => {
const title = mw.Title.newFromText(page.title, page.ns).toText();
return {
id: title,
text: title
};
})
};
}
},
templateSelection: function(choice) {
return $('<a>').text(choice.text).attr({
href: mw.util.getUrl(choice.text),
target: '_blank'
});
}
});
$form.find('[name=namespacerestrictions]').select2({
theme: 'default select2-morebits',
width: '100%',
matcher: Morebits.select2.matchers.wordBeginning,
language: {
searching: Morebits.select2.queryInterceptor
},
templateResult: Morebits.select2.highlightSearchMatches,
placeholder: '利用者をブロックする名前空間を選択'
});
mw.util.addCSS(
// Reduce padding
'.select2-results .select2-results__option { padding-top: 1px; padding-bottom: 1px; }' +
// Adjust font size
'.select2-container .select2-dropdown .select2-results { font-size: 13px; }' +
'.select2-container .selection .select2-selection__rendered { font-size: 13px; }' +
// Remove black border
'.select2-container--default.select2-container--focus .select2-selection--multiple { border: 1px solid #aaa; }' +
// Make the tiny cross larger
'.select2-selection__choice__remove { font-size: 130%; }'
);
} else {
$form.find('fieldset[name="field_block_options"]').hide();
$form.find('fieldset[name="field_64"]').hide();
// Clear select2 options
$form.find('[name=pagerestrictions]').val(null).trigger('change');
$form.find('[name=namespacerestrictions]').val(null).trigger('change');
}
if (fieldTemplateOptions) {
oldfield = $form.find('fieldset[name="field_template_options"]')[0];
oldfield.parentNode.replaceChild(fieldTemplateOptions.render(), oldfield);
e.target.form.root.previewer = new Morebits.wiki.Preview($(e.target.form.root).find('#twinkleblock-previewbox').last()[0]);
} else {
$form.find('fieldset[name="field_template_options"]').hide();
}
// Any block, including ranges
if (Twinkle.block.currentBlockInfo) {
// false for an ip covered by a range or a smaller range within a larger range;
// true for a user, single ip block, or the exact range for a range block
const sameUser = blockedUserName === relevantUserName;
Morebits.Status.init($('div[name="currentblock"] span').last()[0]);
let statusStr = relevantUserName + 'は' + (Twinkle.block.currentBlockInfo.partial === '' ? '部分ブロック' : 'サイト全体でブロック');
// Range blocked
if (Twinkle.block.currentBlockInfo.rangestart !== Twinkle.block.currentBlockInfo.rangeend) {
if (sameUser) {
statusStr = '範囲ブロックとして' + statusStr;
} else {
statusStr = (Morebits.ip.get64(relevantUserName) === blockedUserName ? '/64' : '') + '範囲ブロックの範囲内にある' + statusStr;
// Link to the full range
const $rangeblockloglink = $('<span>').append($('<a target="_blank" href="' + mw.util.getUrl('Special:Log', {action: 'view', page: blockedUserName, type: 'block'}) + '">' + blockedUserName + '</a>)'));
statusStr += ' (' + $rangeblockloglink.html() + ')';
}
}
if (Twinkle.block.currentBlockInfo.expiry === 'infinity') {
statusStr += ' (無期限)';
} else if (new Morebits.Date(Twinkle.block.currentBlockInfo.expiry).isValid()) {
statusStr += ' (期限 ' + new Morebits.Date(Twinkle.block.currentBlockInfo.expiry).calendar('utc') + ')';
}
let infoStr = 'このフォームは';
if (sameUser) {
infoStr += 'ブロックを変更';
if (Twinkle.block.currentBlockInfo.partial === undefined && partialBox) {
infoStr += 'し、部分ブロックに変換';
} else if (Twinkle.block.currentBlockInfo.partial === '' && !partialBox) {
infoStr += 'し、サイト全体のブロックに変換';
}
infoStr += 'します。';
} else {
infoStr += 'さらなる' + (partialBox ? '部分' : '') + 'ブロックを追加します。';
}
Morebits.Status.warn(statusStr, infoStr);
// Default to the current block conditions on intial form generation
Twinkle.block.callback.update_form(e, Twinkle.block.currentBlockInfo);
}
// This is where T146628 really comes into play: a rangeblock will
// only return the correct block log if wgRelevantUserName is the
// exact range, not merely a funtional equivalent
if (Twinkle.block.hasBlockLog) {
const $blockloglink = $('<span>').append($('<a target="_blank" href="' + mw.util.getUrl('Special:Log', {action: 'view', page: relevantUserName, type: 'block'}) + '">ブロック記録</a>)'));
if (!Twinkle.block.currentBlockInfo) {
const lastBlockAction = Twinkle.block.blockLog[0];
if (lastBlockAction.action === 'unblock') {
$blockloglink.append(' (' + new Morebits.Date(lastBlockAction.timestamp).calendar('utc') + ' にブロック解除)');
} else { // block or reblock
$blockloglink.append(' (' + lastBlockAction.params.duration + ', ' + new Morebits.Date(lastBlockAction.params.expiry).calendar('utc') + ' に期限切れ)');
}
}
Morebits.Status.init($('div[name="hasblocklog"] span').last()[0]);
Morebits.Status.warn(Twinkle.block.currentBlockInfo ? '以前のブロック' : 'この' + (Morebits.ip.isRange(relevantUserName) ? '範囲' : '利用者') + 'は過去にブロックされています', $blockloglink[0]);
}
// Make sure all the fields are correct based on initial defaults
if (blockBox) {
Twinkle.block.callback.change_preset(e);
} else if (templateBox) {
Twinkle.block.callback.change_template(e);
}
};
/*
* Keep alphabetized by key name, Twinkle.block.blockGroups establishes
* the order they will appear in the interface
*
* Block preset format, all keys accept only 'true' (omit for false) except where noted:
* <title of block template> : {
* autoblock: <autoblock any IP addresses used (for registered users only)>
* disabletalk: <disable user from editing their own talk page while blocked>
* expiry: <string - expiry timestamp, can include relative times like "5 months", "2 weeks" etc>
* forIPsOnly: <show block option in the interface only if the relevant user is an IP>
* forTempAccountsOnly: <show block option in the interface only if the relevant user is a temporary account>
* forRegisteredOnly: <show block option in the interface only if the relevant user is a temporary account or regular account>
* label: <string - label for the option of the dropdown in the interface (keep brief)>
* noemail: prevent the user from sending email through Special:Emailuser
* pageParam: <set if the associated block template accepts a page parameter>
* prependReason: <string - prepends the value of 'reason' to the end of the existing reason, namely for when revoking talk page access>
* nocreate: <block account creation from the user's IP (for unregistered users only)>
* nonstandard: <template does not conform to stewardship of WikiProject User Warnings and may not accept standard parameters>
* reason: <string - block rationale, as would appear in the block log,
* and the edit summary for when adding block template, unless 'summary' is set>
* reasonParam: <set if the associated block template accepts a reason parameter>
* sig: <string - set to ~~~~ if block template does not accept "true" as the value, or set null to omit sig param altogether>
* summary: <string - edit summary for when adding block template to user's talk page, if not set, 'reason' is used>
* suppressArticleInSummary: <set to suppress showing the article name in the edit summary, as with attack pages>
* templateName: <string - name of template to use (instead of key name), entry will be omitted from the Templates list.
* (e.g. use another template but with different block options)>
* useInitialOptions: <when preset is chosen, only change given block options, leave others as they were>
*
* WARNING: 'anononly' and 'allowusertalk' are enabled by default.
* To disable, set 'hardblock' and 'disabletalk', respectively
*/
Twinkle.block.blockPresetsInfo = {
'blocked proxy': {
expiry: '1 year',
forIPsOnly: true,
nocreate: true,
nonstandard: true,
hardblock: true,
reason: '{{blocked proxy}}',
sig: null
},
'CheckUser block': {
expiry: '1 week',
forIPsOnly: true,
nocreate: true,
nonstandard: true,
reason: '{{CheckUser block}}',
sig: '~~~~'
},
'checkuserblock-account': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
nonstandard: true,
reason: '{{checkuserblock-account}}',
sig: '~~~~'
},
'checkuserblock-wide': {
forIPsOnly: true,
nocreate: true,
nonstandard: true,
reason: '{{checkuserblock-wide}}',
sig: '~~~~'
},
oversightblock: {
autoblock: true,
expiry: 'infinity',
nocreate: true,
nonstandard: true,
reason: '{{OversightBlock}}',
sig: '~~~~'
},
'range block login': {
reason: '{{range block login|<!-- 割当先を入力 -->}}',
nocreate: true,
nonstandard: true,
forIPsOnly: true,
sig: '~~~~'
},
'3rr': {
autoblock: true,
nocreate: true,
pageParam: true,
reason: '[[WP:3RR|スリー・リバート・ルール]]違反',
summary: 'あなたは[[WP:3RR|スリー・リバート・ルール]]違反の編集を行ったためブロックされました。'
},
// uw-prefixed
'uw-ablock': {
autoblock: true,
forIPsOnly: true,
nocreate: true,
pageParam: true,
reasonParam: true,
summary: 'このIPアドレスは投稿ブロックされました',
suppressArticleInSummary: true
},
'uw-adblock': {
autoblock: true,
nocreate: true,
pageParam: true,
reason: '宣伝・広告投稿はお止めください',
summary: 'あなたは[[WP:SOAP|広告または自己宣伝]]を行ったためブロックされました'
},
'uw-bioblock': {
autoblock: true,
nocreate: true,
pageParam: true,
reason: '[[WP:LIVING|存命人物に関する不適切な編集]]',
summary: 'あなたは[[WP:BLP|存命人物の伝記]]に関する方針に違反する編集を行ったため投稿ブロックされました'
},
'uw-block': {
autoblock: true,
forRegisteredOnly: true,
nocreate: true,
pageParam: true,
reasonParam: true,
summary: 'あなたは投稿ブロックされました',
suppressArticleInSummary: true
},
'uw-blockindef': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
pageParam: true,
reasonParam: true,
summary: 'あなたは無期限投稿ブロックされました',
suppressArticleInSummary: true
},
'uw-blocknotalk': {
disabletalk: true,
pageParam: true,
reasonParam: true,
summary: 'あなたは投稿ブロックされました。また、会話ページの編集をすることはできません',
suppressArticleInSummary: true
},
'uw-botblock': {
expiry: 'infinity',
forRegisteredOnly: true,
pageParam: true,
reason: '[[WP:BRFA|承認]]されていない[[WP:BOT|ボット]]の実行',
summary: '[[WP:BRFA|承認]]されていない[[WP:BOT|ボット]]を実行したようであるため、あなたは投稿ブロックされました'
},
'uw-causeblock': {
expiry: 'infinity',
forRegisteredOnly: true,
reason: '不適切な[[WP:U|利用者名]] <!-- 団体を表す利用者名, ソフトブロック -->',
summary: 'あなたの[[WP:U|利用者名]]は団体または組織、ウェブサイトを代表するアカウントかのような印象を与えるため無期限投稿ブロックされました'
},
'uw-compblock': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
reason: 'パスワード公開',
summary: 'あなたはパスワードを公開したため投稿ブロックされました'
},
'uw-ncblock': {
autoblock: true,
expiry: 'infinity',
nocreate: true,
pageParam: true,
reason: '悪戯投稿',
summary: 'あなたは悪戯投稿を行ったため投稿ブロックされました'
},
'uw-sncblock': {
autoblock: true,
expiry: 'infinity',
nocreate: true,
pageParam: true,
reason: '悪戯投稿はお止めください',
summary: 'あなたは悪戯投稿を行ったため投稿ブロックされました'
},
'uw-dblock': {
autoblock: true,
nocreate: true,
reason: '内容の無差別な除去',
pageParam: true,
summary: 'あなたは内容の無差別な除去を行ったため投稿ブロックされました'
},
'uw-efblock': {
autoblock: true,
nocreate: true,
reason: '[[特別:不正利用記録|編集フィルター記録]]',
summary: 'あなたは[[WP:EF|編集フィルター]]が繰り返し発動するような破壊的な編集を行ったため投稿ブロックされました'
},
'uw-ewblock': {
autoblock: true,
nocreate: true,
pageParam: true,
reason: '[[WP:EW|過度の編集合戦]]',
summary: 'あなたは[[WP:EW|編集合戦]]を行ったため投稿ブロックされました'
},
'uw-evadeblock': {
expiry: 'infinity',
nocreate: true,
reason: '[[WP:投稿ブロックの方針#投稿ブロック回避|別アカウントなどによるブロック破り]]',
summary: '[[WP:EVASION|投稿ブロックの回避]]に使用されたため投稿ブロックされました'
},
'uw-fblock': {
autoblock: true,
nocreate: true,
reason: '虚偽情報の記載',
summary: 'あなたは虚偽情報の記載を行ったため投稿ブロックされました'
},
'uw-lblock': {
autoblock: true,
expiry: 'infinity',
nocreate: true,
reason: '[[WP:法的な脅迫をしない|法的な脅迫]]',
summary: 'あなたは[[WP:NLT|法的な脅迫]]を行ったため投稿ブロックされました'
},
'uw-oblock': {
autoblock: true,
nocreate: true,
reason: '目的外利用',
summary: 'あなたは目的外利用を行ったため投稿ブロックされました'
},
'uw-pablock': {
autoblock: true,
nocreate: true,
reason: '[[WP:ATTACK|暴言または嫌がらせ]]',
summary: 'あなたは他者に対して[[WP:ATTACK|個人攻撃]]を行ったため投稿ブロックされました'
},
'uw-readtalkblock': {
autoblock: true,
nocreate: true,
reason: '[[Special:Mytalk|会話ページ]]をお読みください',
forRegisteredOnly: true,
summary: 'あなたは会話ページのメッセージをご覧になっていないため投稿ブロックされました'
},
'uw-rjtblock': {
autoblock: true,
nocreate: true,
reason: '対話拒否',
forRegisteredOnly: true,
summary: 'あなたは対話拒否を行ったため投稿ブロックされました'
},
'uw-sblock': {
autoblock: true,
nocreate: true,
reason: 'spam',
summary: 'あなたはspam目的でウィキペディアの利用を行ったため投稿ブロックされました'
},
'uw-sockblock': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
reason: '[[Wikipedia:多重アカウント|多重アカウント]]の不適切な使用',
summary: 'あなたは[[WP:SOCK|多重アカウント]]の不適切な使用を行ったため投稿ブロックされました'
},
'uw-softerblock': {
expiry: 'infinity',
forRegisteredOnly: true,
reason: '不適切な[[WP:U|利用者名]] <!-- 宣伝目的の名前, ソフトブロック -->',
summary: 'あなたの[[WP:U|利用者名]]は団体または組織、ウェブサイトを代表するアカウントかのような印象を与えるため無期限投稿ブロックされました'
},
'uw-spamublock': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
reason: '不適切な[[WP:U|利用者名]] <!-- 宣伝目的の名前, 宣伝目的の編集 -->',
summary: 'あなたのアカウントは宣伝・広告に使用され、また[[WP:U|利用者名の方針]]に違反する利用者名であるため無期限投稿ブロックされました'
},
'uw-spoablock': {
autoblock: true,
disabletalk: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
noemail: true,
reason: '[[WP:SOCK|sockpuppet]]',
summary: 'このアカウントはウィキペディアの方針に違反するために作成された[[WP:SOCK|ソックパペット]]として投稿ブロックされました'
},
'uw-talkrevoked': {
disabletalk: true,
reason: '会話ページの編集権限を剥奪: ブロック中の会話ページの不適切な使用',
prependReason: true,
summary: '会話ページの編集権限は剥奪されました',
useInitialOptions: true
},
'uw-ublock': {
expiry: 'infinity',
forRegisteredOnly: true,
reason: '不適切な[[WP:U|利用者名]] <!-- 不適切な利用者名, ソフトブロック -->',
reasonParam: true,
summary: 'あなたの利用者名は[[WP:U|利用者名の方針]]に違反しているため無期限投稿ブロックされました'
},
'uw-ublock-double': {
expiry: 'infinity',
forRegisteredOnly: true,
reason: '不適切な[[WP:U|利用者名]] <!-- 他の利用者に似ている利用者名, ソフトブロック -->',
summary: 'あなたの[[WP:U|利用者名]]は、既存のウィキペディア利用者の名前と似ているため無期限投稿ブロックされました'
},
'uw-readpoliciesblock': {
autoblock: true,
nocreate: true,
pageParam: true,
reason: '各種方針の熟読期間',
summary: 'あなたは各種方針の熟読期間が必要であるため投稿ブロックされました'
},
'uw-uhblock': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
reason: '不適切な[[WP:U|利用者名]] <!-- 不適切な利用者名, ハードブロック -->',
reasonParam: true,
summary: 'あなたの利用者名は明らかに[[WP:U|利用者名の方針]]に違反しているため無期限投稿ブロックされました'
},
'uw-ublock-wellknown': {
expiry: 'infinity',
forRegisteredOnly: true,
reason: '不適切な[[WP:U|利用者名]] <!-- 有名人を表す利用者名, ソフトブロック -->',
summary: 'あなたの[[WP:U|利用者名]]は存命の著名人の名前と一致しているため無期限投稿ブロックされました'
},
'uw-uhblock-double': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
reason: '不適切な[[WP:U|利用者名]] <!-- 他の利用者のなりすまし, ハードブロック -->',
summary: 'あなたの[[WP:U|利用者名]]は他の既存の利用者になりすましているように見えるため無期限投稿ブロックされました'
},
'uw-upeblock': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
pageParam: true,
reason: '[[wmf:Special:MyLanguage/Terms of use|利用規約]]に違反して[[WP:PAID|有償の寄稿の開示]]に応じない利用者',
summary: 'あなたのアカウントはウィキペディアの方針である[[WP:PAID|有償の寄稿の開示]]に違反してしようされているため無期限投稿ブロックされました'
},
'uw-vaublock': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
pageParam: true,
reason: '不適切な[[WP:U|利用者名]] <!-- 荒らし目的のアカウント -->',
summary: 'あなたのアカウントは荒らしのみに使用されており、また明らかに[[WP:U|利用者名の方針]]に違反する利用者名であるため無期限投稿ブロックされました'
},
'uw-vblock': {
autoblock: true,
expiry: 'infinity',
nocreate: true,
pageParam: true,
reason: '[[Wikipedia:荒らし|荒らし]]',
summary: 'さらなる[[Wikipedia:荒らし|荒らし]]を防止するため、あなたは投稿ブロックされました。'
},
// Begin partial block templates, accessed in Twinkle.block.blockGroupsPartial
'uw-epblock': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: false,
noemail: true,
pageParam: false,
reasonParam: true,
reason: 'メールを用いた嫌がらせ',
summary: 'あなたは嫌がらせを行ったため他の利用者にメールを送ることをブロックされました'
},
'uw-ewpblock': {
autoblock: true,
nocreate: false,
pageParam: false,
reasonParam: true,
reason: '[[WP:EW|編集合戦]]',
summary: 'あなたは[[WP:EW|編集合戦]]を行ったため部分ブロックされました'
},
'uw-pblock': {
autoblock: true,
nocreate: false,
pageParam: false,
reasonParam: true,
summary: 'あなたはウィキペディアの一部から部分ブロックされました'
},
'uw-pblockindef': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: false,
pageParam: false,
reasonParam: true,
summary: 'あなたはウィキペディアの一部から無期限部分ブロックされました'
}
};
Twinkle.block.transformBlockPresets = function twinkleblockTransformBlockPresets() {
// supply sensible defaults
$.each(Twinkle.block.blockPresetsInfo, (preset, settings) => {
settings.summary = settings.summary || settings.reason;
settings.sig = settings.sig !== undefined ? settings.sig : 'yes';
settings.indefinite = settings.indefinite || Morebits.string.isInfinity(settings.expiry);
if (!Twinkle.block.isRegistered && settings.indefinite) {
settings.expiry = '1 week';
} else {
settings.expiry = settings.expiry || '1 week';
}
Twinkle.block.blockPresetsInfo[preset] = settings;
});
};
// These are the groups of presets and defines the order in which they appear. For each list item:
// label: <string, the description that will be visible in the dropdown>
// value: <string, the key of a preset in blockPresetsInfo>
Twinkle.block.blockGroups = [
{
label: 'よくある理由',
list: [
{ label: '全般ブロック (カスタム理由)', value: 'uw-block' }, // ends up being default for registered users
{ label: '全般ブロック (カスタム理由) - IP', value: 'uw-ablock', selected: true }, // set only when blocking IP
{ label: '全般ブロック (カスタム理由) - 無期限', value: 'uw-blockindef' },
{ label: 'ブロック中の会話ページの不適切な使用', value: 'uw-talkrevoked' },
{ label: '全般ブロック - 会話ページ禁止', value: 'uw-blocknotalk' },
{ label: 'sockpuppet', value: 'uw-spoablock' },
{ label: '荒らし', value: 'uw-vblock' },
{ label: '各種方針の熟読期間', value: 'uw-readpoliciesblock' },
{ label: '会話ページをお読みください', value: 'uw-readtalkblock' }
]
},
{
label: '方針',
list: [
{ label: 'スリー・リバート・ルール違反', value: '3rr' },
{ label: '過度の編集合戦', value: 'uw-ewblock' },
{ label: 'パスワード公開', value: 'uw-compblock' },
{ label: '別アカウントなどによるブロック破り', value: 'uw-evadeblock' },
{ label: '多重アカウントの不適切な使用', value: 'uw-sockblock' },
{ label: '未承認のボット', value: 'uw-botblock' },
{ label: '有償の寄稿の開示に応じない利用者', value: 'uw-upeblock' }
]
},
{
label: '荒らし',
list: [
{ label: '存命人物に関する不適切な編集', value: 'uw-bioblock' },
{ label: '虚偽情報の記載', value: 'uw-fblock' },
{ label: '内容の無差別な除去', value: 'uw-dblock' },
{ label: '宣伝・広告投稿はお止めください', value: 'uw-adblock' },
{ label: '悪戯投稿はお止めください', value: 'uw-sncblock' },
{ label: '悪戯投稿', value: 'uw-ncblock' },
{ label: '暴言または嫌がらせ', value: 'uw-pablock' },
{ label: '対話拒否', value: 'uw-rjtblock' },
{ label: '目的外利用', value: 'uw-oblock' },
{ label: '編集フィルター記録', value: 'uw-efblock' },
{ label: '法的な脅迫', value: 'uw-lblock' },
{ label: 'Spam', value: 'uw-sblock' }
]
},
{
label: '不適切な利用者名',
list: [
{ label: '宣伝目的の名前, ソフトブロック', value: 'uw-softerblock' },
{ label: '宣伝目的の名前, ハードブロック', value: 'uw-spamublock' },
{ label: '不適切な利用者名, ソフトブロック', value: 'uw-ublock' },
{ label: '不適切な利用者名, ハードブロック', value: 'uw-uhblock' },
{ label: 'よく似ている利用者名, ソフトブロック', value: 'uw-ublock-double' },
{ label: 'なりすましの利用者名, ハードブロック', value: 'uw-uhblock-double' },
{ label: '有名人を表す利用者名, ソフトブロック', value: 'uw-ublock-wellknown' },
{ label: '団体を表す利用者名, ソフトブロック', value: 'uw-causeblock' },
{ label: '不適切な利用者名, 荒らし目的のアカウント', value: 'uw-vaublock' }
]
},
{
label: 'テンプレート化された理由',
list: [
{ label: 'blocked proxy', value: 'blocked proxy' },
{ label: 'CheckUser block', value: 'CheckUser block' },
{ label: 'checkuserblock-account', value: 'checkuserblock-account' },
{ label: 'checkuserblock-wide', value: 'checkuserblock-wide' },
{ label: 'oversightblock', value: 'oversightblock' },
{ label: 'range block login', value: 'range block login' } // Only for IP ranges, selected for non-/64 ranges in filtered_block_groups
]
}
];
Twinkle.block.blockGroupsPartial = [
{
label: 'よくある部分ブロック理由',
list: [
{ label: '全般部分ブロック (カスタム理由)', value: 'uw-pblock', selected: true },
{ label: '全般部分ブロック (カスタム理由) - 無期限', value: 'uw-pblockindef' },
{ label: '編集合戦', value: 'uw-ewpblock' },
{ label: 'メールを用いた嫌がらせ', value: 'uw-epblock' }
]
}
];
Twinkle.block.callback.filtered_block_groups = function twinkleblockCallbackFilteredBlockGroups(group, showTemplate) {
return $.map(group, (blockGroup) => {
const list = $.map(blockGroup.list, (blockPreset) => {
switch (blockPreset.value) {
case 'uw-talkrevoked':
if (blockedUserName !== relevantUserName) {
return;
}
break;
case 'range block login':
if (!Morebits.ip.isRange(relevantUserName)) {
return;
}
blockPreset.selected = !Morebits.ip.get64(relevantUserName);
break;
case 'CheckUser block':
case 'checkuserblock-account':
case 'checkuserblock-wide':
if (!Morebits.userIsInGroup('checkuser')) {
return;
}
break;
case 'oversightblock':
if (!Morebits.userIsInGroup('suppress')) {
return;
}
break;
default:
break;
}
const blockSettings = Twinkle.block.blockPresetsInfo[blockPreset.value];
let allowedUserType;
// for regular users and temporary accounts
if (blockSettings.forRegisteredOnly) {
allowedUserType = Twinkle.block.isRegistered;
// for temporary accounts
} else if (blockSettings.forTempAccountsOnly) {
allowedUserType = mw.util.isTemporaryUser(mw.config.get('wgRelevantUserName'));
// for IPs
} else if (blockSettings.forIPsOnly) {
allowedUserType = !Twinkle.block.isRegistered;
} else {
allowedUserType = true;
}
if (!(blockSettings.templateName && showTemplate) && allowedUserType) {
const templateName = blockSettings.templateName || blockPreset.value;
return {
label: (showTemplate ? '{{' + templateName + '}}: ' : '') + blockPreset.label,
value: blockPreset.value,
data: [{
name: 'template-name',
value: templateName
}],
selected: !!blockPreset.selected,
disabled: !!blockPreset.disabled
};
}
});
if (list.length) {
return {
label: blockGroup.label,
list: list
};
}
});
};
Twinkle.block.callback.change_preset = function twinkleblockCallbackChangePreset(e) {
const form = e.target.form, key = form.preset.value;
if (!key) {
return;
}
Twinkle.block.callback.update_form(e, Twinkle.block.blockPresetsInfo[key]);
if (form.template) {
form.template.value = Twinkle.block.blockPresetsInfo[key].templateName || key;
Twinkle.block.callback.change_template(e);
}
};
Twinkle.block.callback.change_expiry = function twinkleblockCallbackChangeExpiry(e) {
const expiry = e.target.form.expiry;
if (e.target.value === 'custom') {
Morebits.QuickForm.setElementVisibility(expiry.parentNode, true);
} else {
Morebits.QuickForm.setElementVisibility(expiry.parentNode, false);
expiry.value = e.target.value;
}
};
Twinkle.block.seeAlsos = [];
Twinkle.block.callback.toggle_see_alsos = function twinkleblockCallbackToggleSeeAlso() {
const joinEnum = function(e) {
return e.join('および');
};
const reason = this.form.reason.value.replace(
new RegExp('( <!--|;) ' + joinEnum(Twinkle.block.seeAlsos) + '参照( -->)?'), ''
);
Twinkle.block.seeAlsos = Twinkle.block.seeAlsos.filter((el) => el !== this.value);
if (this.checked) {
Twinkle.block.seeAlsos.push(this.value);
}
const seeAlsoMessage = joinEnum(Twinkle.block.seeAlsos);
if (!Twinkle.block.seeAlsos.length) {
this.form.reason.value = reason;
} else if (reason.includes('{{')) {
this.form.reason.value = reason + ' <!-- ' + seeAlsoMessage + '参照 -->';
} else {
this.form.reason.value = reason + '; ' + seeAlsoMessage + '参照';
}
};
Twinkle.block.callback.update_form = function twinkleblockCallbackUpdateForm(e, data) {
const form = e.target.form;
let expiry = data.expiry;
// don't override original expiry if useInitialOptions is set
if (!data.useInitialOptions) {
if (Date.parse(expiry)) {
expiry = new Date(expiry).toGMTString();
form.expiry_preset.value = 'custom';
} else {
form.expiry_preset.value = data.expiry || 'custom';
}
form.expiry.value = expiry;
if (form.expiry_preset.value === 'custom') {
Morebits.QuickForm.setElementVisibility(form.expiry.parentNode, true);
} else {
Morebits.QuickForm.setElementVisibility(form.expiry.parentNode, false);
}
}
// boolean-flipped options, more at [[mw:API:Block]]
data.disabletalk = data.disabletalk !== undefined ? data.disabletalk : false;
data.hardblock = data.hardblock !== undefined ? data.hardblock : false;
// disable autoblock if blocking a bot
if (Twinkle.block.userIsBot || /bot\b/i.test(relevantUserName)) {
data.autoblock = false;
}
$(form).find('[name=field_block_options]').find(':checkbox').each((i, el) => {
// don't override original options if useInitialOptions is set
if (data.useInitialOptions && data[el.name] === undefined) {
return;
}
const check = data[el.name] === '' || !!data[el.name];
$(el).prop('checked', check);
});
if (data.prependReason && data.reason) {
form.reason.value = data.reason + '; ' + form.reason.value;
} else {
form.reason.value = data.reason || '';
}
// Clear and/or set any partial page or namespace restrictions
if (form.pagerestrictions) {
const $pageSelect = $(form).find('[name=pagerestrictions]');
const $namespaceSelect = $(form).find('[name=namespacerestrictions]');
// Respect useInitialOptions by clearing data when switching presets
// In practice, this will always clear, since no partial presets use it
if (!data.useInitialOptions) {
$pageSelect.val(null).trigger('change');
$namespaceSelect.val(null).trigger('change');
}
// Add any preset options; in practice, just used for prior block settings
if (data.restrictions) {
if (data.restrictions.pages && !$pageSelect.val().length) {
const pages = data.restrictions.pages.map((pr) => pr.title);
// since page restrictions use an ajax source, we
// short-circuit that and just add a new option
pages.forEach((page) => {
if (!$pageSelect.find("option[value='" + $.escapeSelector(page) + "']").length) {
const newOption = new Option(page, page, true, true);
$pageSelect.append(newOption);
}
});
$pageSelect.val($pageSelect.val().concat(pages)).trigger('change');
}
if (data.restrictions.namespaces) {
$namespaceSelect.val($namespaceSelect.val().concat(data.restrictions.namespaces)).trigger('change');
}
}
}
};
Twinkle.block.callback.change_template = function twinkleblockcallbackChangeTemplate(e) {
const form = e.target.form, value = form.template.value, settings = Twinkle.block.blockPresetsInfo[value];
const blockBox = $(form).find('[name=actiontype][value=block]').is(':checked');
const partialBox = $(form).find('[name=actiontype][value=partial]').is(':checked');
const templateBox = $(form).find('[name=actiontype][value=template]').is(':checked');
// Block form is not present
if (!blockBox) {
if (settings.indefinite || settings.nonstandard) {
if (Twinkle.block.prev_template_expiry === null) {
Twinkle.block.prev_template_expiry = form.template_expiry.value || '';
}
form.template_expiry.parentNode.style.display = 'none';
form.template_expiry.value = 'infinity';
} else if (form.template_expiry.parentNode.style.display === 'none') {
if (Twinkle.block.prev_template_expiry !== null) {
form.template_expiry.value = Twinkle.block.prev_template_expiry;
Twinkle.block.prev_template_expiry = null;
}
form.template_expiry.parentNode.style.display = 'block';
}
if (Twinkle.block.prev_template_expiry) {
form.expiry.value = Twinkle.block.prev_template_expiry;
}
Morebits.QuickForm.setElementVisibility(form.notalk.parentNode, !settings.nonstandard);
// Partial
Morebits.QuickForm.setElementVisibility(form.noemail_template.parentNode, partialBox);
Morebits.QuickForm.setElementVisibility(form.nocreate_template.parentNode, partialBox);
} else if (templateBox) { // Only present if block && template forms both visible
Morebits.QuickForm.setElementVisibility(
form.blank_duration.parentNode,
!settings.indefinite && !settings.nonstandard
);
}
// Only particularly relevant if template form is present
Morebits.QuickForm.setElementVisibility(form.article.parentNode, settings && !!settings.pageParam);
Morebits.QuickForm.setElementVisibility(form.block_reason.parentNode, settings && !!settings.reasonParam);
// Partial block
Morebits.QuickForm.setElementVisibility(form.area.parentNode, partialBox && !blockBox);
form.root.previewer.closePreview();
};
Twinkle.block.prev_template_expiry = null;
Twinkle.block.callback.preview = function twinkleblockcallbackPreview(form) {
const params = {
article: form.article.value,
blank_duration: form.blank_duration ? form.blank_duration.checked : false,
disabletalk: form.disabletalk.checked || (form.notalk ? form.notalk.checked : false),
expiry: form.template_expiry ? form.template_expiry.value : form.expiry.value,
hardblock: Twinkle.block.isRegistered ? form.autoblock.checked : form.hardblock.checked,
indefinite: Morebits.string.isInfinity(form.template_expiry ? form.template_expiry.value : form.expiry.value),
reason: form.block_reason.value,
template: form.template.value,
partial: $(form).find('[name=actiontype][value=partial]').is(':checked'),
pagerestrictions: $(form.pagerestrictions).val() || [],
namespacerestrictions: $(form.namespacerestrictions).val() || [],
noemail: form.noemail.checked || (form.noemail_template ? form.noemail_template.checked : false),
nocreate: form.nocreate.checked || (form.nocreate_template ? form.nocreate_template.checked : false),
area: form.area.value
};
const templateText = Twinkle.block.callback.getBlockNoticeWikitext(params);
form.previewer.beginRender(templateText, 'User_talk:' + relevantUserName); // Force wikitext/correct username
};
Twinkle.block.callback.evaluate = function twinkleblockCallbackEvaluate(e) {
const $form = $(e.target),
toBlock = $form.find('[name=actiontype][value=block]').is(':checked'),
toWarn = $form.find('[name=actiontype][value=template]').is(':checked'),
toPartial = $form.find('[name=actiontype][value=partial]').is(':checked');
let blockoptions = {}, templateoptions = {};
Twinkle.block.callback.saveFieldset($form.find('[name=field_block_options]'));
Twinkle.block.callback.saveFieldset($form.find('[name=field_template_options]'));
blockoptions = Twinkle.block.field_block_options;
templateoptions = Twinkle.block.field_template_options;
templateoptions.disabletalk = !!(templateoptions.disabletalk || blockoptions.disabletalk);
templateoptions.hardblock = !!blockoptions.hardblock;
delete blockoptions.expiry_preset; // remove extraneous
// Partial API requires this to be gone, not false or 0
if (toPartial) {
blockoptions.partial = templateoptions.partial = true;
}
templateoptions.pagerestrictions = $form.find('[name=pagerestrictions]').val() || [];
templateoptions.namespacerestrictions = $form.find('[name=namespacerestrictions]').val() || [];
// Format for API here rather than in saveFieldset
blockoptions.pagerestrictions = templateoptions.pagerestrictions.join('|');
blockoptions.namespacerestrictions = templateoptions.namespacerestrictions.join('|');
// use block settings as warn options where not supplied
templateoptions.summary = templateoptions.summary || blockoptions.reason;
templateoptions.expiry = templateoptions.template_expiry || blockoptions.expiry;
if (toBlock) {
if (blockoptions.partial) {
if (blockoptions.disabletalk && !blockoptions.namespacerestrictions.includes('3')) {
return alert('利用者会話名前空間の編集を制限しない限り、部分ブロックでは会話ページの編集を禁止することはできません!');
}
if (!blockoptions.namespacerestrictions && !blockoptions.pagerestrictions) {
if (!blockoptions.noemail && !blockoptions.nocreate) { // Blank entries technically allowed [[phab:T208645]]
return alert('ページや名前空間が選択されておらず、メール送信やアカウント作成の制限を選択されていません; 少なくとも一つオプションを選択してください!');
} else if ((templateoptions.template !== 'uw-epblock' || $form.find('select[name="preset"]').val() !== 'uw-epblock') &&
// Don't require confirmation if email harassment defaults are set
!confirm('You are about to block with no restrictions on page or namespace editing, are you sure you want to proceed?')) {
return;
}
}
}
if (!blockoptions.expiry) {
return alert('有効期限を入力してください!');
} else if (Morebits.string.isInfinity(blockoptions.expiry) && !Twinkle.block.isRegistered) {
return alert('IPアドレスを無期限ブロックすることはできません!');
}
if (!blockoptions.reason) {
return alert('ブロック理由を入力してください!');
}
Morebits.SimpleWindow.setButtonsEnabled(false);
Morebits.Status.init(e.target);
const statusElement = new Morebits.Status('ブロックの実行');
blockoptions.action = 'block';
blockoptions.user = relevantUserName;
// boolean-flipped options
blockoptions.anononly = blockoptions.hardblock ? undefined : true;
blockoptions.allowusertalk = blockoptions.disabletalk ? undefined : true;
/*
Check if block status changed while processing the form.
There's a lot to consider here. list=blocks provides the
current block status, but there are at least two issues with
relying on it. First, the id doesn't update on a reblock,
meaning the individual parameters need to be compared. This
can be done roughly with JSON.stringify - we can thankfully
rely on order from the server, although sorting would be
fine if not - but falsey values are problematic and is
non-ideal. More importantly, list=blocks won't indicate if a
non-blocked user is blocked then unblocked. This should be
exceedingy rare, but regardless, we thus need to check
list=logevents, which has a nicely updating logid
parameter. We can't rely just on that, though, since it
doesn't account for blocks that have expired on their own.
As such, we use both. Using some ternaries, the logid
variables are false if there's no logevents, so if they
aren't equal we defintely have a changed entry (send
confirmation). If they are equal, then either the user was
never blocked (the block statuses will be equal, no
confirmation) or there's no new block, in which case either
a block expired (different statuses, confirmation) or the
same block is still active (same status, no confirmation).
*/
const query = {
format: 'json',
action: 'query',
list: 'blocks|logevents',
letype: 'block',
lelimit: 1,
letitle: 'User:' + blockoptions.user
};
// bkusers doesn't catch single IPs blocked as part of a range block
if (mw.util.isIPAddress(blockoptions.user, true)) {
query.bkip = blockoptions.user;
} else {
query.bkusers = blockoptions.user;
}
api.get(query).then((data) => {
let block = data.query.blocks[0];
// As with the initial data fetch, if an IP is blocked
// *and* rangeblocked, this would only grab whichever
// block is more recent, which would likely mean a
// mismatch. However, if the rangeblock is updated
// while filling out the form, this won't detect that,
// but that's probably fine.
if (data.query.blocks.length > 1 && block.user !== relevantUserName) {
block = data.query.blocks[1];
}
const logevents = data.query.logevents[0];
const logid = data.query.logevents.length ? logevents.logid : false;
if (logid !== Twinkle.block.blockLogId || !!block !== !!Twinkle.block.currentBlockInfo) {
let message = 'The block status of ' + blockoptions.user + ' has changed. ';
if (block) {
message += 'New status: ';
} else {
message += 'Last entry: ';
}
let logExpiry = '';
if (logevents.params.duration) {
if (logevents.params.duration === 'infinity') {
logExpiry = 'indefinitely';
} else {
const expiryDate = new Morebits.Date(logevents.params.expiry);
logExpiry += (expiryDate.isBefore(new Date()) ? ', expired ' : ' until ') + expiryDate.calendar();
}
} else { // no duration, action=unblock, just show timestamp
logExpiry = ' ' + new Morebits.Date(logevents.timestamp).calendar();
}
message += Morebits.string.toUpperCaseFirstChar(logevents.action) + 'ed by ' + logevents.user + logExpiry +
' for "' + logevents.comment + '". Do you want to override with your settings?';
if (!confirm(message)) {
Morebits.Status.info('ブロックの実行', '利用者によって中断');
return;
}
blockoptions.reblock = 1; // Writing over a block will fail otherwise
}
// execute block
blockoptions.token = mw.user.tokens.get('csrfToken');
const mbApi = new Morebits.wiki.Api('ブロックの実行', blockoptions, (() => {
statusElement.info('完了');
if (toWarn) {
Twinkle.block.callback.issue_template(templateoptions);
}
}));
mbApi.post();
});
} else if (toWarn) {
Morebits.SimpleWindow.setButtonsEnabled(false);
Morebits.Status.init(e.target);
Twinkle.block.callback.issue_template(templateoptions);
} else {
return alert('Please give Twinkle something to do!');
}
};
Twinkle.block.callback.issue_template = function twinkleblockCallbackIssueTemplate(formData) {
// Use wgRelevantUserName to ensure the block template goes to a single IP and not to the
// "talk page" of an IP range (which does not exist)
const userTalkPage = 'User_talk:' + mw.config.get('wgRelevantUserName');
const params = Twinkle.block.combineFormDataAndFieldTemplateOptions(
formData,
Twinkle.block.blockPresetsInfo[formData.template],
Twinkle.block.field_template_options.block_reason,
Twinkle.block.field_template_options.notalk,
Twinkle.block.field_template_options.noemail_template,
Twinkle.block.field_template_options.nocreate_template
);
Morebits.wiki.actionCompleted.redirect = userTalkPage;
Morebits.wiki.actionCompleted.notice = 'Actions complete, loading user talk page in a few seconds';
const wikipediaPage = new Morebits.wiki.Page(userTalkPage, 'User talk page modification');
wikipediaPage.setCallbackParameters(params);
wikipediaPage.load(Twinkle.block.callback.main);
};
Twinkle.block.combineFormDataAndFieldTemplateOptions = function(formData, messageData, reason, disabletalk, noemail, nocreate) {
return $.extend(formData, {
messageData: messageData,
reason: reason,
disabletalk: disabletalk,
noemail: noemail,
nocreate: nocreate
});
};
Twinkle.block.callback.getBlockNoticeWikitext = function(params) {
let text = '{{';
const settings = Twinkle.block.blockPresetsInfo[params.template];
if (!settings.nonstandard) {
text += 'subst:' + params.template;
if (params.article && settings.pageParam) {
text += '|page=' + params.article;
}
if (!/te?mp|^\s*$|min/.exec(params.expiry)) {
if (params.indefinite) {
text += '|indef=yes';
} else if (!params.blank_duration && !new Morebits.Date(params.expiry).isValid()) {
// Block template wants a duration, not date
text += '|time=' + params.expiry;
}
}
if (!Twinkle.block.isRegistered && !params.hardblock) {
text += '|anon=yes';
}
if (params.reason) {
text += '|reason=' + params.reason;
}
if (params.disabletalk) {
text += '|notalk=yes';
}
// Currently, all partial block templates are "standard"
// Building the template, however, takes a fair bit of logic
if (params.partial) {
if (params.pagerestrictions.length || params.namespacerestrictions.length) {
const makeSentence = function(array) {
if (array.length < 3) {
return array.join(' and ');
}
const last = array.pop();
return array.join(', ') + ', and ' + last;
};
text += '|area=' + (params.indefinite ? 'certain ' : 'from certain ');
if (params.pagerestrictions.length) {
text += 'pages (' + makeSentence(params.pagerestrictions.map((p) => '[[:' + p + ']]'));
text += params.namespacerestrictions.length ? ') and certain ' : ')';
}
if (params.namespacerestrictions.length) {
// 1 => Talk, 2 => User, etc.
const namespaceNames = params.namespacerestrictions.map((id) => menuFormattedNamespaces[id]);
text += '[[Wikipedia:Namespace|namespaces]] (' + makeSentence(namespaceNames) + ')';
}
} else if (params.area) {
text += '|area=' + params.area;
} else {
if (params.noemail) {
text += '|email=yes';
}
if (params.nocreate) {
text += '|accountcreate=yes';
}
}
}
} else {
text += params.template;
}
if (settings.sig) {
text += '|sig=' + settings.sig;
}
return text + '}}';
};
Twinkle.block.callback.main = function twinkleblockcallbackMain(pageobj) {
const params = pageobj.getCallbackParameters(),
date = new Morebits.Date(pageobj.getLoadTime()),
messageData = params.messageData;
let text;
params.indefinite = Morebits.string.isInfinity(params.expiry);
if (Twinkle.getPref('blankTalkpageOnIndefBlock') && params.template !== 'uw-lblock' && params.indefinite) {
Morebits.Status.info('Info', 'Blanking talk page per preferences and creating a new talk page section for this month');
text = date.monthHeader() + '\n';
} else {
text = pageobj.getPageText();
const dateHeaderRegex = date.monthHeaderRegex();
let dateHeaderRegexLast, dateHeaderRegexResult;
while ((dateHeaderRegexLast = dateHeaderRegex.exec(text)) !== null) {
dateHeaderRegexResult = dateHeaderRegexLast;
}
// If dateHeaderRegexResult is null then lastHeaderIndex is never checked. If it is not null but
// \n== is not found, then the date header must be at the very start of the page. lastIndexOf
// returns -1 in this case, so lastHeaderIndex gets set to 0 as desired.
const lastHeaderIndex = text.lastIndexOf('\n==') + 1;
if (text.length > 0) {
text += '\n\n';
}
if (!dateHeaderRegexResult || dateHeaderRegexResult.index !== lastHeaderIndex) {
Morebits.Status.info('Info', 'Will create a new talk page section for this month, as none was found');
text += date.monthHeader() + '\n';
}
}
params.expiry = typeof params.template_expiry !== 'undefined' ? params.template_expiry : params.expiry;
text += Twinkle.block.callback.getBlockNoticeWikitext(params);
// build the edit summary
let summary = messageData.summary;
if (messageData.suppressArticleInSummary !== true && params.article) {
summary += ' on [[:' + params.article + ']]';
}
summary += '.';
pageobj.setPageText(text);
pageobj.setEditSummary(summary);
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.setWatchlist(Twinkle.getPref('watchWarnings'));
pageobj.save();
};
Twinkle.addInitCallback(Twinkle.block, 'block');
}());
// </nowiki>
jfrmrjqm4xecnle6lamcu57x9innnkm
769539
769494
2026-10-22T00:01:04Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 1406828: watchuser
769539
javascript
text/javascript
// <nowiki>
(function() {
const api = new mw.Api();
let relevantUserName, blockedUserName, blockWindow;
const menuFormattedNamespaces = $.extend({}, mw.config.get('wgFormattedNamespaces'));
menuFormattedNamespaces[0] = '(Article)';
/*
****************************************
*** twinkleblock.js: Block module
****************************************
* Mode of invocation: Tab ("Block")
* Active on: Any page with relevant user name (userspace, contribs, etc.)
*/
Twinkle.block = function twinkleblock() {
relevantUserName = mw.config.get('wgRelevantUserName');
// should show on Contributions or Block pages, anywhere there's a relevant user
// Ignore ranges wider than the CIDR limit
if (Morebits.userIsSysop && relevantUserName && (!Morebits.ip.isRange(relevantUserName) || Morebits.ip.validCIDR(relevantUserName))) {
Twinkle.addPortletLink(Twinkle.block.callback, 'Block', 'tw-block', '利用者をブロック');
}
};
Twinkle.block.callback = function twinkleblockCallback() {
if (relevantUserName === mw.config.get('wgUserName') &&
!confirm('自分自身をブロックしようとしています!続行しますか?')) {
return;
}
Twinkle.block.currentBlockInfo = undefined;
Twinkle.block.field_block_options = {};
Twinkle.block.field_template_options = {};
blockWindow = new Morebits.SimpleWindow(650, 530);
// need to be verbose about who we're blocking
blockWindow.setTitle(relevantUserName + 'をブロックまたは投稿ブロックテンプレートを送信する');
blockWindow.setScriptName('Twinkle');
// Always added, hidden later if actual user not blocked
blockWindow.addFooterLink('ブロックを解除', 'Special:Unblock/' + relevantUserName);
blockWindow.addFooterLink('投稿ブロックテンプレート', 'Wikipedia:主要なテンプレート/利用者‐会話名前空間#投稿ブロックテンプレート');
blockWindow.addFooterLink('投稿ブロックの方針', 'WP:BLOCK');
blockWindow.addFooterLink('ブロック設定', 'User:Syunsyunminmin/Twinkle/Preferences#block');
blockWindow.addFooterLink('Twinkle help', ':en:WP:TW/DOC#block');
blockWindow.addFooterLink('フィードバック', 'User talk:Syunsyunminmin/Twinkle');
const form = new Morebits.QuickForm(Twinkle.block.callback.evaluate);
const actionfield = form.append({
type: 'field',
label: '操作の種類'
});
actionfield.append({
type: 'checkbox',
name: 'actiontype',
event: Twinkle.block.callback.change_action,
list: [
{
label: '利用者をブロック',
value: 'block',
tooltip: '指定されたオプションで対象利用者をブロックします。部分ブロックのチェックが外れている場合、サイト全体のブロックとなります。',
checked: true
},
{
label: '部分ブロック',
value: 'partial',
tooltip: '部分ブロックと部分ブロックテンプレートを有効にする。',
checked: Twinkle.getPref('defaultToPartialBlocks') // Overridden if already blocked
},
{
label: '利用者会話ページに投稿ブロックテンプレートを追加',
value: 'template',
tooltip: '(※未対応)ブロックした管理者がブロックテンプレートを送信するのを忘れた場合、あるいは、テンプレートを送信せずに利用者をブロックした場合、これを使って適切なテンプレートを送信することができます。部分ブロックテンプレートは部分ブロックボックスにチェックを入れてください。',
// Disallow when viewing the block dialog on an IP range
checked: false,
disabled: Morebits.ip.isRange(relevantUserName)
}
]
});
/*
Add option for IPv6 ranges smaller than /64 to upgrade to the 64
CIDR ([[WP:/64]]). This is one of the few places where we want
wgRelevantUserName since this depends entirely on the original user.
In theory, we shouldn't use Morebits.ip.get64 here since since we want
to exclude functionally-equivalent /64s. That'd be:
// if (mw.util.isIPv6Address(mw.config.get('wgRelevantUserName'), true) &&
// (mw.util.isIPv6Address(mw.config.get('wgRelevantUserName')) || parseInt(mw.config.get('wgRelevantUserName').replace(/^(.+?)\/?(\d{1,3})?$/, '$2'), 10) > 64)) {
In practice, though, since functionally-equivalent ranges are
(mis)treated as separate by MediaWiki's logging ([[phab:T146628]]),
using Morebits.ip.get64 provides a modicum of relief in thise case.
*/
const sixtyFour = Morebits.ip.get64(mw.config.get('wgRelevantUserName'));
if (sixtyFour && sixtyFour !== mw.config.get('wgRelevantUserName')) {
const block64field = form.append({
type: 'field',
label: '/64範囲ブロックに変換',
name: 'field_64'
});
block64field.append({
type: 'div',
style: 'margin-bottom: 0.5em',
label: ['通常は、より良いとは言わないまでも、', $.parseHTML('<a target="_blank" href="' + mw.util.getUrl(':en:WP:/64') + '">/64の範囲ブロック</a>')[0], 'するだけで問題ないです (',
$.parseHTML('<a target="_blank" href="' + mw.util.getUrl('Special:Contributions/' + sixtyFour) + '">' + sixtyFour + '</a>)')[0], ').']
});
block64field.append({
type: 'checkbox',
name: 'block64',
event: Twinkle.block.callback.change_block64,
list: [{
checked: Twinkle.getPref('defaultToBlock64'),
label: '/64を代わりにブロックする',
value: 'block64',
tooltip: Morebits.ip.isRange(mw.config.get('wgRelevantUserName')) ? 'Will eschew leaving a template.' : 'Any template issued will go to the original IP: ' + mw.config.get('wgRelevantUserName')
}]
});
}
form.append({ type: 'field', label: 'プリセット', name: 'field_preset' });
form.append({ type: 'field', label: 'テンプレートオプション', name: 'field_template_options' });
form.append({ type: 'field', label: 'ブロックオプション', name: 'field_block_options' });
form.append({ type: 'submit' });
const result = form.render();
blockWindow.setContent(result);
blockWindow.display();
result.root = result;
Twinkle.block.fetchUserInfo(() => {
// Toggle initial partial state depending on prior block type,
// will override the defaultToPartialBlocks pref
if (blockedUserName === relevantUserName) {
$(result).find('[name=actiontype][value=partial]').prop('checked', Twinkle.block.currentBlockInfo.partial === '');
}
// clean up preset data (defaults, etc.), done exactly once, must be before Twinkle.block.callback.change_action is called
Twinkle.block.transformBlockPresets();
// init the controls after user and block info have been fetched
const evt = document.createEvent('Event');
evt.initEvent('change', true, true);
if (result.block64 && result.block64.checked) {
// Calls the same change_action event once finished
result.block64.dispatchEvent(evt);
} else {
result.actiontype[0].dispatchEvent(evt);
}
});
};
// Store fetched user data, only relevant if switching IPv6 to a /64
Twinkle.block.fetchedData = {};
// Processes the data from a query response, separated from
// Twinkle.block.fetchUserInfo to allow reprocessing of already-fetched data
Twinkle.block.processUserInfo = function twinkleblockProcessUserInfo(data, fn) {
let blockinfo = data.query.blocks[0];
// Soft redirect to Special:Block if the user is multi-blocked (#2178)
if (blockinfo && data.query.blocks.length > 1) {
// Remove submission buttons.
blockWindow.$dialog.find('.morebits-dialog-buttons').empty();
Morebits.Status.init(blockWindow.content.querySelector('form'));
Morebits.Status.warn(
`この対象には有効なブロックが${data.query.blocks.length}件あります`,
`Twinkleは複数ブロックに対応していません。[[Special:Block/${relevantUserName}]]を使用してください。`
);
return;
}
const userinfo = data.query.users[0];
// If an IP is blocked *and* rangeblocked, the above finds
// whichever block is more recent, not necessarily correct.
// Three seems... unlikely
if (data.query.blocks.length > 1 && blockinfo.user !== relevantUserName) {
blockinfo = data.query.blocks[1];
}
// Cache response, used when toggling /64 blocks
Twinkle.block.fetchedData[userinfo.name] = data;
Twinkle.block.isRegistered = !!userinfo.userid;
if (Twinkle.block.isRegistered) {
Twinkle.block.userIsBot = !!userinfo.groupmemberships && userinfo.groupmemberships.map((e) => e.group).includes('bot');
} else {
Twinkle.block.userIsBot = false;
}
if (blockinfo) {
// handle frustrating system of inverted boolean values
blockinfo.disabletalk = blockinfo.allowusertalk === undefined;
blockinfo.hardblock = blockinfo.anononly === undefined;
}
// will undefine if no blocks present
Twinkle.block.currentBlockInfo = blockinfo;
blockedUserName = Twinkle.block.currentBlockInfo && Twinkle.block.currentBlockInfo.user;
// Toggle unblock link if not the user in question; always first
const unblockLink = blockWindow.$dialog.find('.morebits-dialog-footerlinks a')[0];
if (blockedUserName !== relevantUserName) {
unblockLink.hidden = true;
unblockLink.nextSibling.hidden = true; // link+trailing bullet
} else {
unblockLink.hidden = false;
unblockLink.nextSibling.hidden = false; // link+trailing bullet
}
// Semi-busted on ranges, see [[phab:T270737]] and [[phab:T146628]].
// Basically, logevents doesn't treat functionally-equivalent ranges
// as equivalent, meaning any functionally-equivalent IP range is
// misinterpreted by the log throughout. Without logevents
// redirecting (like Special:Block does) we would need a function to
// parse ranges, which is a pain. IPUtils has the code, but it'd be a
// lot of cruft for one purpose.
Twinkle.block.hasBlockLog = !!data.query.logevents.length;
Twinkle.block.blockLog = Twinkle.block.hasBlockLog && data.query.logevents;
// Used later to check if block status changed while filling out the form
Twinkle.block.blockLogId = Twinkle.block.hasBlockLog ? data.query.logevents[0].logid : false;
if (typeof fn === 'function') {
return fn();
}
};
Twinkle.block.fetchUserInfo = function twinkleblockFetchUserInfo(fn) {
const query = {
format: 'json',
action: 'query',
list: 'blocks|users|logevents',
letype: 'block',
lelimit: 1,
letitle: 'User:' + relevantUserName,
bkprop: 'expiry|reason|flags|restrictions|range|user',
ususers: relevantUserName
};
// bkusers doesn't catch single IPs blocked as part of a range block
if (mw.util.isIPAddress(relevantUserName, true)) {
query.bkip = relevantUserName;
} else {
query.bkusers = relevantUserName;
// groupmemberships only relevant for registered users
query.usprop = 'groupmemberships';
}
api.get(query).then((data) => {
Twinkle.block.processUserInfo(data, fn);
}, (msg) => {
Morebits.Status.init($('div[name="currentblock"] span').last()[0]);
Morebits.Status.warn('利用者情報の取得エラー', msg);
});
};
Twinkle.block.callback.saveFieldset = function twinkleblockCallbacksaveFieldset(fieldset) {
Twinkle.block[$(fieldset).prop('name')] = {};
$(fieldset).serializeArray().forEach((el) => {
// namespaces and pages for partial blocks are overwritten
// here, but we're handling them elsewhere so that's fine
Twinkle.block[$(fieldset).prop('name')][el.name] = el.value;
});
};
Twinkle.block.callback.change_block64 = function twinkleblockCallbackChangeBlock64(e) {
const $form = $(e.target.form),
$block64 = $form.find('[name=block64]'),
dialog = $form.closest('.morebits-dialog')[0];
// Show/hide block64 button
// Single IPv6, or IPv6 range smaller than a /64
const priorName = relevantUserName;
if ($block64.is(':checked')) {
relevantUserName = Morebits.ip.get64(mw.config.get('wgRelevantUserName'));
} else {
relevantUserName = mw.config.get('wgRelevantUserName');
}
// No templates for ranges, but if the original user is a single IP, offer the option
// (done separately in Twinkle.block.callback.issue_template)
const originalIsRange = Morebits.ip.isRange(mw.config.get('wgRelevantUserName'));
$form.find('[name=actiontype][value=template]').prop('disabled', originalIsRange);
// Refetch/reprocess user info then regenerate the main content
const regenerateForm = function() {
// Tweak titlebar text. In theory, we could save the dialog
// at initialization and then use `.setTitle`, but in practice that swallows
// the scriptName and requires `.display`ing, which jumps the
// window. It's just a line of text, so this is fine.
const titleBar = dialog.querySelector('.morebits-dialog-title').firstChild.nextSibling;
titleBar.nodeValue = titleBar.nodeValue.replace(priorName, relevantUserName);
// Tweak unblock link
const unblockLink = dialog.querySelector('.morebits-dialog-footerlinks a');
unblockLink.href = unblockLink.href.replace(priorName, relevantUserName);
unblockLink.title = unblockLink.title.replace(priorName, relevantUserName);
// Correct partial state
$form.find('[name=actiontype][value=partial]').prop('checked', Twinkle.getPref('defaultToPartialBlocks'));
if (blockedUserName === relevantUserName) {
$form.find('[name=actiontype][value=partial]').prop('checked', Twinkle.block.currentBlockInfo.partial === '');
}
// Set content appropriately
Twinkle.block.callback.change_action(e);
};
if (Twinkle.block.fetchedData[relevantUserName]) {
Twinkle.block.processUserInfo(Twinkle.block.fetchedData[relevantUserName], regenerateForm);
} else {
Twinkle.block.fetchUserInfo(regenerateForm);
}
};
Twinkle.block.callback.change_action = function twinkleblockCallbackChangeAction(e) {
let fieldPreset, fieldTemplateOptions, fieldBlockOptions;
const $form = $(e.target.form);
// Make ifs shorter
const blockBox = $form.find('[name=actiontype][value=block]').is(':checked');
const templateBox = $form.find('[name=actiontype][value=template]').is(':checked');
const $partial = $form.find('[name=actiontype][value=partial]');
const partialBox = $partial.is(':checked');
let blockGroup = partialBox ? Twinkle.block.blockGroupsPartial : Twinkle.block.blockGroups;
$partial.prop('disabled', !blockBox && !templateBox);
// Add current block parameters as default preset
const prior = { label: '前ブロック' };
if (blockedUserName === relevantUserName) {
Twinkle.block.blockPresetsInfo.prior = $.extend({ watchuser: true }, Twinkle.block.currentBlockInfo);
// value not a valid template selection, chosen below by setting templateName
prior.list = [{ label: '前のブロック設定', value: 'prior', selected: true }];
// Arrays of objects are annoying to check
if (!blockGroup.some((bg) => bg.label === prior.label)) {
blockGroup.push(prior);
}
// Always ensure proper template exists/is selected when switching modes
if (partialBox) {
Twinkle.block.blockPresetsInfo.prior.templateName = Morebits.string.isInfinity(Twinkle.block.currentBlockInfo.expiry) ? 'uw-pblockindef' : 'uw-pblock';
} else {
if (!Twinkle.block.isRegistered) {
Twinkle.block.blockPresetsInfo.prior.templateName = 'uw-ablock';
} else {
Twinkle.block.blockPresetsInfo.prior.templateName = Morebits.string.isInfinity(Twinkle.block.currentBlockInfo.expiry) ? 'uw-blockindef' : 'uw-block';
}
}
} else {
// But first remove any prior prior
blockGroup = blockGroup.filter((bg) => bg.label !== prior.label);
}
Twinkle.block.callback.saveFieldset($('[name=field_block_options]'));
Twinkle.block.callback.saveFieldset($('[name=field_template_options]'));
if (blockBox) {
fieldPreset = new Morebits.QuickForm.Element({ type: 'field', label: 'プリセット', name: 'field_preset' });
fieldPreset.append({
type: 'select',
name: 'preset',
label: 'プリセットを選択:',
event: Twinkle.block.callback.change_preset,
list: Twinkle.block.callback.filtered_block_groups(blockGroup)
});
fieldBlockOptions = new Morebits.QuickForm.Element({ type: 'field', label: 'ブロックオプション', name: 'field_block_options' });
fieldBlockOptions.append({ type: 'div', name: 'currentblock', label: ' ' });
fieldBlockOptions.append({ type: 'div', name: 'hasblocklog', label: ' ' });
fieldBlockOptions.append({
type: 'select',
name: 'expiry_preset',
label: '期限:',
event: Twinkle.block.callback.change_expiry,
list: [
{ label: 'その他', value: 'custom', selected: true },
{ label: '無期限', value: 'infinity' },
{ label: '3時間', value: '3 hours' },
{ label: '12時間', value: '12 hours' },
{ label: '24時間', value: '24 hours' },
{ label: '31時間', value: '31 hours' },
{ label: '36時間', value: '36 hours' },
{ label: '48時間', value: '48 hours' },
{ label: '60時間', value: '60 hours' },
{ label: '72時間', value: '72 hours' },
{ label: '1週間', value: '1 week' },
{ label: '2週間', value: '2 weeks' },
{ label: '1ヶ月間', value: '1 month' },
{ label: '3ヶ月間', value: '3 months' },
{ label: '6ヶ月間', value: '6 months' },
{ label: '1年間', value: '1 year' },
{ label: '2年間', value: '2 years' },
{ label: '3年間', value: '3 years' }
]
});
fieldBlockOptions.append({
type: 'input',
name: 'expiry',
label: 'その他の期限',
tooltip: '"1 minute" (1分) や "19 days" (19日) のような相対的な時間や、"yyyymmddhhmm"のような絶対的なタイムスタンプを使用することができます。 (例: "200602011405"は2006年2月1日 14:05 UTCを意味します)',
value: Twinkle.block.field_block_options.expiry || Twinkle.block.field_template_options.template_expiry
});
if (partialBox) { // Partial block
fieldBlockOptions.append({
type: 'select',
multiple: true,
name: 'pagerestrictions',
label: '編集をブロックする特定のページ',
value: '',
tooltip: '最大で10ページ'
});
const ns = fieldBlockOptions.append({
type: 'select',
multiple: true,
name: 'namespacerestrictions',
label: '名前空間のブロック',
value: '',
tooltip: 'これらの名前空間の編集をブロックする'
});
$.each(menuFormattedNamespaces, (number, name) => {
// Ignore -1: Special; -2: Media; and 2300-2303: Gadget (talk) and Gadget definition (talk)
if (number >= 0 && number < 830) {
ns.append({ type: 'option', label: name, value: number });
}
});
}
const blockoptions = [
{
checked: Twinkle.block.field_block_options.nocreate,
label: 'アカウント作成をブロック',
name: 'nocreate',
value: '1'
}
];
if (Twinkle.block.isRegistered && !mw.util.isTemporaryUser(mw.config.get('wgRelevantUserName'))) {
blockoptions.push({
checked: Twinkle.block.field_block_options.noemail,
label: '利用者のメール送信をブロック',
name: 'noemail',
value: '1'
});
}
blockoptions.push({
checked: Twinkle.block.field_block_options.disabletalk,
label: 'ブロックされている間、この利用者が自分の会話ページを編集できないようにする。',
name: 'disabletalk',
value: '1',
tooltip: partialBox ? '部分ブロックを行う場合、利用者会話名前空間の編集をブロックしない限り、このチェックは外したままにしておかなければなりません。' : ''
});
if (Twinkle.block.isRegistered) {
blockoptions.push({
checked: Twinkle.block.field_block_options.autoblock,
label: '使用されたIPアドレスを自動ブロック',
name: 'autoblock',
value: '1'
});
} else {
blockoptions.push({
checked: Twinkle.block.field_block_options.hardblock,
label: 'ログイン利用者がこのIPアドレスを使用するのをブロックする (ハードブロック)',
name: 'hardblock',
value: '1'
});
}
blockoptions.push({
checked: Twinkle.block.field_block_options.watchuser,
label: '利用者(会話)ページをウォッチする',
name: 'watchuser',
value: '1'
});
fieldBlockOptions.append({
type: 'checkbox',
name: 'blockoptions',
list: blockoptions
});
fieldBlockOptions.append({
type: 'textarea',
label: '理由 (ブロック記録用):',
name: 'reason',
tooltip: '既定のブロックメッセージに追加して役立つ詳細を入力してください',
value: Twinkle.block.field_block_options.reason
});
fieldBlockOptions.append({
type: 'div',
name: 'filerlog_label',
label: '参照:',
style: 'display:inline-block;font-style:normal !important',
tooltip: 'フィルター記録、削除された投稿、関連する一時アカウントがブロックの判断に関わったことを示す参照情報を追加します。'
});
fieldBlockOptions.append({
type: 'checkbox',
name: 'filter_see_also',
event: Twinkle.block.callback.toggle_see_alsos,
style: 'display:inline-block; margin-right:5px',
list: [
{
label: 'フィルター記録',
checked: false,
value: 'フィルター記録'
}
]
});
fieldBlockOptions.append({
type: 'checkbox',
name: 'deleted_see_also',
event: Twinkle.block.callback.toggle_see_alsos,
style: 'display:inline-block; margin-right:5px',
list: [
{
label: '削除された投稿',
checked: false,
value: '削除された投稿記録'
}
]
});
if (mw.util.isTemporaryUser(mw.config.get('wgRelevantUserName'))) {
fieldBlockOptions.append({
type: 'checkbox',
name: 'related_see_also',
event: Twinkle.block.callback.toggle_see_alsos,
style: 'display:inline-block',
list: [
{
label: '関連する一時アカウント',
checked: false,
value: '関連する一時アカウント'
}
]
});
}
// Yet-another-logevents-doesn't-handle-ranges-well
if (blockedUserName === relevantUserName) {
fieldBlockOptions.append({ type: 'hidden', name: 'reblock', value: '1' });
}
}
if (templateBox) {
fieldTemplateOptions = new Morebits.QuickForm.Element({ type: 'field', label: 'テンプレートオプション', name: 'field_template_options' });
fieldTemplateOptions.append({
type: 'select',
name: 'template',
label: '会話ページのテンプレートを選択:',
event: Twinkle.block.callback.change_template,
list: Twinkle.block.callback.filtered_block_groups(blockGroup, true),
value: Twinkle.block.field_template_options.template
});
fieldTemplateOptions.append({
type: 'input',
name: 'article',
label: 'リンクするページ',
value: '',
tooltip: 'そのページが破壊行為の主な対象であった場合、通知内でリンクされることがあります。リンクするページがない場合は空欄にしてください。'
});
// Only visible if partial and not blocking
fieldTemplateOptions.append({
type: 'input',
name: 'area',
label: 'ブロックされた場所',
value: '',
tooltip: 'オプションで、利用者が編集をブロックされたページまたは名前空間の説明。'
});
if (!blockBox) {
fieldTemplateOptions.append({
type: 'input',
name: 'template_expiry',
label: 'ブロック期間:',
value: '',
tooltip: 'ブロックする期間。例えば24 hours、2 weeks、indefiniteなど'
});
}
fieldTemplateOptions.append({
type: 'input',
name: 'block_reason',
label: '"あなたがブロックされたのは..."',
tooltip: 'デフォルトの一般的な理由を置き換えるオプションの理由。全般の投稿ブロックテンプレートでのみ利用可能です。',
value: Twinkle.block.field_template_options.block_reason
});
if (blockBox) {
fieldTemplateOptions.append({
type: 'checkbox',
name: 'blank_duration',
list: [
{
label: 'テンプレートに有効期限を含めない',
checked: Twinkle.block.field_template_options.blank_duration,
tooltip: '期間を含める代わりに、ブロックテンプレートは「あなたは一時的にブロックされました」と表示します..."'
}
]
});
} else {
fieldTemplateOptions.append({
type: 'checkbox',
list: [
{
label: '会話ページの編集を禁止',
name: 'notalk',
checked: Twinkle.block.field_template_options.notalk,
tooltip: '会話ページの編集権限を剥奪したことを投稿ブロックテンプレートに記載します。'
},
{
label: '利用者のメール送信をブロック',
name: 'noemail_template',
checked: Twinkle.block.field_template_options.noemail_template,
tooltip: '対象範囲が未指定の場合、メール送信をブロックしたことを投稿ブロックテンプレートに記載します。'
},
{
label: '利用者のアカウント作成をブロック',
name: 'nocreate_template',
checked: Twinkle.block.field_template_options.nocreate_template,
tooltip: '対象範囲が未指定の場合、アカウント作成をブロックしたことを投稿ブロックテンプレートに記載します。'
}
]
});
}
const $previewlink = $('<a id="twinkleblock-preview-link">プレビュー</a>');
$previewlink.off('click').on('click', () => {
Twinkle.block.callback.preview($form[0]);
});
$previewlink.css({cursor: 'pointer'});
fieldTemplateOptions.append({ type: 'div', id: 'blockpreview', label: [$previewlink[0]] });
fieldTemplateOptions.append({ type: 'div', id: 'twinkleblock-previewbox', style: 'display: none' });
}
let oldfield;
if (fieldPreset) {
oldfield = $form.find('fieldset[name="field_preset"]')[0];
oldfield.parentNode.replaceChild(fieldPreset.render(), oldfield);
} else {
$form.find('fieldset[name="field_preset"]').hide();
}
if (fieldBlockOptions) {
oldfield = $form.find('fieldset[name="field_block_options"]')[0];
oldfield.parentNode.replaceChild(fieldBlockOptions.render(), oldfield);
$form.find('fieldset[name="field_64"]').show();
$form.find('[name=pagerestrictions]').select2({
theme: 'default select2-morebits',
width: '100%',
placeholder: '利用者をブロックするページを選択',
language: {
errorLoading: function() {
return '不完全または無効な検索キーワード';
}
},
maximumSelectionLength: 10, // Software limitation [[phab:T202776]]
minimumInputLength: 1, // prevent ajax call when empty
ajax: {
url: mw.util.wikiScript('api'),
dataType: 'json',
delay: 100,
data: function(params) {
const title = mw.Title.newFromText(params.term);
if (!title) {
return;
}
return {
action: 'query',
format: 'json',
list: 'allpages',
apfrom: title.title,
apnamespace: title.namespace,
aplimit: '10'
};
},
processResults: function(data) {
return {
results: data.query.allpages.map((page) => {
const title = mw.Title.newFromText(page.title, page.ns).toText();
return {
id: title,
text: title
};
})
};
}
},
templateSelection: function(choice) {
return $('<a>').text(choice.text).attr({
href: mw.util.getUrl(choice.text),
target: '_blank'
});
}
});
$form.find('[name=namespacerestrictions]').select2({
theme: 'default select2-morebits',
width: '100%',
matcher: Morebits.select2.matchers.wordBeginning,
language: {
searching: Morebits.select2.queryInterceptor
},
templateResult: Morebits.select2.highlightSearchMatches,
placeholder: '利用者をブロックする名前空間を選択'
});
mw.util.addCSS(
// Reduce padding
'.select2-results .select2-results__option { padding-top: 1px; padding-bottom: 1px; }' +
// Adjust font size
'.select2-container .select2-dropdown .select2-results { font-size: 13px; }' +
'.select2-container .selection .select2-selection__rendered { font-size: 13px; }' +
// Remove black border
'.select2-container--default.select2-container--focus .select2-selection--multiple { border: 1px solid #aaa; }' +
// Make the tiny cross larger
'.select2-selection__choice__remove { font-size: 130%; }'
);
} else {
$form.find('fieldset[name="field_block_options"]').hide();
$form.find('fieldset[name="field_64"]').hide();
// Clear select2 options
$form.find('[name=pagerestrictions]').val(null).trigger('change');
$form.find('[name=namespacerestrictions]').val(null).trigger('change');
}
if (fieldTemplateOptions) {
oldfield = $form.find('fieldset[name="field_template_options"]')[0];
oldfield.parentNode.replaceChild(fieldTemplateOptions.render(), oldfield);
e.target.form.root.previewer = new Morebits.wiki.Preview($(e.target.form.root).find('#twinkleblock-previewbox').last()[0]);
} else {
$form.find('fieldset[name="field_template_options"]').hide();
}
// Any block, including ranges
if (Twinkle.block.currentBlockInfo) {
// false for an ip covered by a range or a smaller range within a larger range;
// true for a user, single ip block, or the exact range for a range block
const sameUser = blockedUserName === relevantUserName;
Morebits.Status.init($('div[name="currentblock"] span').last()[0]);
let statusStr = relevantUserName + 'は' + (Twinkle.block.currentBlockInfo.partial === '' ? '部分ブロック' : 'サイト全体でブロック');
// Range blocked
if (Twinkle.block.currentBlockInfo.rangestart !== Twinkle.block.currentBlockInfo.rangeend) {
if (sameUser) {
statusStr = '範囲ブロックとして' + statusStr;
} else {
statusStr = (Morebits.ip.get64(relevantUserName) === blockedUserName ? '/64' : '') + '範囲ブロックの範囲内にある' + statusStr;
// Link to the full range
const $rangeblockloglink = $('<span>').append($('<a target="_blank" href="' + mw.util.getUrl('Special:Log', {action: 'view', page: blockedUserName, type: 'block'}) + '">' + blockedUserName + '</a>)'));
statusStr += ' (' + $rangeblockloglink.html() + ')';
}
}
if (Twinkle.block.currentBlockInfo.expiry === 'infinity') {
statusStr += ' (無期限)';
} else if (new Morebits.Date(Twinkle.block.currentBlockInfo.expiry).isValid()) {
statusStr += ' (期限 ' + new Morebits.Date(Twinkle.block.currentBlockInfo.expiry).calendar('utc') + ')';
}
let infoStr = 'このフォームは';
if (sameUser) {
infoStr += 'ブロックを変更';
if (Twinkle.block.currentBlockInfo.partial === undefined && partialBox) {
infoStr += 'し、部分ブロックに変換';
} else if (Twinkle.block.currentBlockInfo.partial === '' && !partialBox) {
infoStr += 'し、サイト全体のブロックに変換';
}
infoStr += 'します。';
} else {
infoStr += 'さらなる' + (partialBox ? '部分' : '') + 'ブロックを追加します。';
}
Morebits.Status.warn(statusStr, infoStr);
// Default to the current block conditions on intial form generation
Twinkle.block.callback.update_form(e, Twinkle.block.currentBlockInfo);
}
// This is where T146628 really comes into play: a rangeblock will
// only return the correct block log if wgRelevantUserName is the
// exact range, not merely a funtional equivalent
if (Twinkle.block.hasBlockLog) {
const $blockloglink = $('<span>').append($('<a target="_blank" href="' + mw.util.getUrl('Special:Log', {action: 'view', page: relevantUserName, type: 'block'}) + '">ブロック記録</a>)'));
if (!Twinkle.block.currentBlockInfo) {
const lastBlockAction = Twinkle.block.blockLog[0];
if (lastBlockAction.action === 'unblock') {
$blockloglink.append(' (' + new Morebits.Date(lastBlockAction.timestamp).calendar('utc') + ' にブロック解除)');
} else { // block or reblock
$blockloglink.append(' (' + lastBlockAction.params.duration + ', ' + new Morebits.Date(lastBlockAction.params.expiry).calendar('utc') + ' に期限切れ)');
}
}
Morebits.Status.init($('div[name="hasblocklog"] span').last()[0]);
Morebits.Status.warn(Twinkle.block.currentBlockInfo ? '以前のブロック' : 'この' + (Morebits.ip.isRange(relevantUserName) ? '範囲' : '利用者') + 'は過去にブロックされています', $blockloglink[0]);
}
// Make sure all the fields are correct based on initial defaults
if (blockBox) {
Twinkle.block.callback.change_preset(e);
} else if (templateBox) {
Twinkle.block.callback.change_template(e);
}
};
/*
* Keep alphabetized by key name, Twinkle.block.blockGroups establishes
* the order they will appear in the interface
*
* Block preset format, all keys accept only 'true' (omit for false) except where noted:
* <title of block template> : {
* autoblock: <autoblock any IP addresses used (for registered users only)>
* disabletalk: <disable user from editing their own talk page while blocked>
* expiry: <string - expiry timestamp, can include relative times like "5 months", "2 weeks" etc>
* forIPsOnly: <show block option in the interface only if the relevant user is an IP>
* forTempAccountsOnly: <show block option in the interface only if the relevant user is a temporary account>
* forRegisteredOnly: <show block option in the interface only if the relevant user is a temporary account or regular account>
* label: <string - label for the option of the dropdown in the interface (keep brief)>
* noemail: prevent the user from sending email through Special:Emailuser
* watchuser: <watch the user and user talk pages; defaults to true>
* pageParam: <set if the associated block template accepts a page parameter>
* prependReason: <string - prepends the value of 'reason' to the end of the existing reason, namely for when revoking talk page access>
* nocreate: <block account creation from the user's IP (for unregistered users only)>
* nonstandard: <template does not conform to stewardship of WikiProject User Warnings and may not accept standard parameters>
* reason: <string - block rationale, as would appear in the block log,
* and the edit summary for when adding block template, unless 'summary' is set>
* reasonParam: <set if the associated block template accepts a reason parameter>
* sig: <string - set to ~~~~ if block template does not accept "true" as the value, or set null to omit sig param altogether>
* summary: <string - edit summary for when adding block template to user's talk page, if not set, 'reason' is used>
* suppressArticleInSummary: <set to suppress showing the article name in the edit summary, as with attack pages>
* templateName: <string - name of template to use (instead of key name), entry will be omitted from the Templates list.
* (e.g. use another template but with different block options)>
* useInitialOptions: <when preset is chosen, only change given block options, leave others as they were>
*
* WARNING: 'anononly' and 'allowusertalk' are enabled by default.
* To disable, set 'hardblock' and 'disabletalk', respectively
*/
Twinkle.block.blockPresetsInfo = {
'blocked proxy': {
expiry: '1 year',
forIPsOnly: true,
nocreate: true,
nonstandard: true,
hardblock: true,
reason: '{{blocked proxy}}',
sig: null
},
'CheckUser block': {
expiry: '1 week',
forIPsOnly: true,
nocreate: true,
nonstandard: true,
reason: '{{CheckUser block}}',
sig: '~~~~'
},
'checkuserblock-account': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
nonstandard: true,
reason: '{{checkuserblock-account}}',
sig: '~~~~'
},
'checkuserblock-wide': {
forIPsOnly: true,
nocreate: true,
nonstandard: true,
reason: '{{checkuserblock-wide}}',
sig: '~~~~'
},
oversightblock: {
autoblock: true,
expiry: 'infinity',
nocreate: true,
nonstandard: true,
reason: '{{OversightBlock}}',
sig: '~~~~'
},
'range block login': {
reason: '{{range block login|<!-- 割当先を入力 -->}}',
nocreate: true,
nonstandard: true,
forIPsOnly: true,
sig: '~~~~'
},
'3rr': {
autoblock: true,
nocreate: true,
pageParam: true,
reason: '[[WP:3RR|スリー・リバート・ルール]]違反',
summary: 'あなたは[[WP:3RR|スリー・リバート・ルール]]違反の編集を行ったためブロックされました。'
},
// uw-prefixed
'uw-ablock': {
autoblock: true,
forIPsOnly: true,
nocreate: true,
pageParam: true,
reasonParam: true,
summary: 'このIPアドレスは投稿ブロックされました',
suppressArticleInSummary: true
},
'uw-adblock': {
autoblock: true,
nocreate: true,
pageParam: true,
reason: '宣伝・広告投稿はお止めください',
summary: 'あなたは[[WP:SOAP|広告または自己宣伝]]を行ったためブロックされました'
},
'uw-bioblock': {
autoblock: true,
nocreate: true,
pageParam: true,
reason: '[[WP:LIVING|存命人物に関する不適切な編集]]',
summary: 'あなたは[[WP:BLP|存命人物の伝記]]に関する方針に違反する編集を行ったため投稿ブロックされました'
},
'uw-block': {
autoblock: true,
forRegisteredOnly: true,
nocreate: true,
pageParam: true,
reasonParam: true,
summary: 'あなたは投稿ブロックされました',
suppressArticleInSummary: true
},
'uw-blockindef': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
pageParam: true,
reasonParam: true,
summary: 'あなたは無期限投稿ブロックされました',
suppressArticleInSummary: true
},
'uw-blocknotalk': {
disabletalk: true,
pageParam: true,
reasonParam: true,
summary: 'あなたは投稿ブロックされました。また、会話ページの編集をすることはできません',
suppressArticleInSummary: true
},
'uw-botblock': {
expiry: 'infinity',
forRegisteredOnly: true,
pageParam: true,
reason: '[[WP:BRFA|承認]]されていない[[WP:BOT|ボット]]の実行',
summary: '[[WP:BRFA|承認]]されていない[[WP:BOT|ボット]]を実行したようであるため、あなたは投稿ブロックされました'
},
'uw-causeblock': {
expiry: 'infinity',
forRegisteredOnly: true,
reason: '不適切な[[WP:U|利用者名]] <!-- 団体を表す利用者名, ソフトブロック -->',
summary: 'あなたの[[WP:U|利用者名]]は団体または組織、ウェブサイトを代表するアカウントかのような印象を与えるため無期限投稿ブロックされました'
},
'uw-compblock': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
reason: 'パスワード公開',
summary: 'あなたはパスワードを公開したため投稿ブロックされました'
},
'uw-ncblock': {
autoblock: true,
expiry: 'infinity',
nocreate: true,
pageParam: true,
reason: '悪戯投稿',
summary: 'あなたは悪戯投稿を行ったため投稿ブロックされました'
},
'uw-sncblock': {
autoblock: true,
expiry: 'infinity',
nocreate: true,
pageParam: true,
reason: '悪戯投稿はお止めください',
summary: 'あなたは悪戯投稿を行ったため投稿ブロックされました'
},
'uw-dblock': {
autoblock: true,
nocreate: true,
reason: '内容の無差別な除去',
pageParam: true,
summary: 'あなたは内容の無差別な除去を行ったため投稿ブロックされました'
},
'uw-efblock': {
autoblock: true,
nocreate: true,
reason: '[[特別:不正利用記録|編集フィルター記録]]',
summary: 'あなたは[[WP:EF|編集フィルター]]が繰り返し発動するような破壊的な編集を行ったため投稿ブロックされました'
},
'uw-ewblock': {
autoblock: true,
nocreate: true,
pageParam: true,
reason: '[[WP:EW|過度の編集合戦]]',
summary: 'あなたは[[WP:EW|編集合戦]]を行ったため投稿ブロックされました'
},
'uw-evadeblock': {
expiry: 'infinity',
nocreate: true,
reason: '[[WP:投稿ブロックの方針#投稿ブロック回避|別アカウントなどによるブロック破り]]',
summary: '[[WP:EVASION|投稿ブロックの回避]]に使用されたため投稿ブロックされました'
},
'uw-fblock': {
autoblock: true,
nocreate: true,
reason: '虚偽情報の記載',
summary: 'あなたは虚偽情報の記載を行ったため投稿ブロックされました'
},
'uw-lblock': {
autoblock: true,
expiry: 'infinity',
nocreate: true,
reason: '[[WP:法的な脅迫をしない|法的な脅迫]]',
summary: 'あなたは[[WP:NLT|法的な脅迫]]を行ったため投稿ブロックされました'
},
'uw-oblock': {
autoblock: true,
nocreate: true,
reason: '目的外利用',
summary: 'あなたは目的外利用を行ったため投稿ブロックされました'
},
'uw-pablock': {
autoblock: true,
nocreate: true,
reason: '[[WP:ATTACK|暴言または嫌がらせ]]',
summary: 'あなたは他者に対して[[WP:ATTACK|個人攻撃]]を行ったため投稿ブロックされました'
},
'uw-readtalkblock': {
autoblock: true,
nocreate: true,
reason: '[[Special:Mytalk|会話ページ]]をお読みください',
forRegisteredOnly: true,
summary: 'あなたは会話ページのメッセージをご覧になっていないため投稿ブロックされました'
},
'uw-rjtblock': {
autoblock: true,
nocreate: true,
reason: '対話拒否',
forRegisteredOnly: true,
summary: 'あなたは対話拒否を行ったため投稿ブロックされました'
},
'uw-sblock': {
autoblock: true,
nocreate: true,
reason: 'spam',
summary: 'あなたはspam目的でウィキペディアの利用を行ったため投稿ブロックされました'
},
'uw-sockblock': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
reason: '[[Wikipedia:多重アカウント|多重アカウント]]の不適切な使用',
summary: 'あなたは[[WP:SOCK|多重アカウント]]の不適切な使用を行ったため投稿ブロックされました'
},
'uw-softerblock': {
expiry: 'infinity',
forRegisteredOnly: true,
reason: '不適切な[[WP:U|利用者名]] <!-- 宣伝目的の名前, ソフトブロック -->',
summary: 'あなたの[[WP:U|利用者名]]は団体または組織、ウェブサイトを代表するアカウントかのような印象を与えるため無期限投稿ブロックされました'
},
'uw-spamublock': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
reason: '不適切な[[WP:U|利用者名]] <!-- 宣伝目的の名前, 宣伝目的の編集 -->',
summary: 'あなたのアカウントは宣伝・広告に使用され、また[[WP:U|利用者名の方針]]に違反する利用者名であるため無期限投稿ブロックされました'
},
'uw-spoablock': {
autoblock: true,
disabletalk: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
noemail: true,
reason: '[[WP:SOCK|sockpuppet]]',
summary: 'このアカウントはウィキペディアの方針に違反するために作成された[[WP:SOCK|ソックパペット]]として投稿ブロックされました'
},
'uw-talkrevoked': {
disabletalk: true,
reason: '会話ページの編集権限を剥奪: ブロック中の会話ページの不適切な使用',
prependReason: true,
summary: '会話ページの編集権限は剥奪されました',
useInitialOptions: true
},
'uw-ublock': {
expiry: 'infinity',
forRegisteredOnly: true,
reason: '不適切な[[WP:U|利用者名]] <!-- 不適切な利用者名, ソフトブロック -->',
reasonParam: true,
summary: 'あなたの利用者名は[[WP:U|利用者名の方針]]に違反しているため無期限投稿ブロックされました'
},
'uw-ublock-double': {
expiry: 'infinity',
forRegisteredOnly: true,
reason: '不適切な[[WP:U|利用者名]] <!-- 他の利用者に似ている利用者名, ソフトブロック -->',
summary: 'あなたの[[WP:U|利用者名]]は、既存のウィキペディア利用者の名前と似ているため無期限投稿ブロックされました'
},
'uw-readpoliciesblock': {
autoblock: true,
nocreate: true,
pageParam: true,
reason: '各種方針の熟読期間',
summary: 'あなたは各種方針の熟読期間が必要であるため投稿ブロックされました'
},
'uw-uhblock': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
reason: '不適切な[[WP:U|利用者名]] <!-- 不適切な利用者名, ハードブロック -->',
reasonParam: true,
summary: 'あなたの利用者名は明らかに[[WP:U|利用者名の方針]]に違反しているため無期限投稿ブロックされました'
},
'uw-ublock-wellknown': {
expiry: 'infinity',
forRegisteredOnly: true,
reason: '不適切な[[WP:U|利用者名]] <!-- 有名人を表す利用者名, ソフトブロック -->',
summary: 'あなたの[[WP:U|利用者名]]は存命の著名人の名前と一致しているため無期限投稿ブロックされました'
},
'uw-uhblock-double': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
reason: '不適切な[[WP:U|利用者名]] <!-- 他の利用者のなりすまし, ハードブロック -->',
summary: 'あなたの[[WP:U|利用者名]]は他の既存の利用者になりすましているように見えるため無期限投稿ブロックされました'
},
'uw-upeblock': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
pageParam: true,
reason: '[[wmf:Special:MyLanguage/Terms of use|利用規約]]に違反して[[WP:PAID|有償の寄稿の開示]]に応じない利用者',
summary: 'あなたのアカウントはウィキペディアの方針である[[WP:PAID|有償の寄稿の開示]]に違反してしようされているため無期限投稿ブロックされました'
},
'uw-vaublock': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
pageParam: true,
reason: '不適切な[[WP:U|利用者名]] <!-- 荒らし目的のアカウント -->',
summary: 'あなたのアカウントは荒らしのみに使用されており、また明らかに[[WP:U|利用者名の方針]]に違反する利用者名であるため無期限投稿ブロックされました'
},
'uw-vblock': {
autoblock: true,
expiry: 'infinity',
nocreate: true,
pageParam: true,
reason: '[[Wikipedia:荒らし|荒らし]]',
summary: 'さらなる[[Wikipedia:荒らし|荒らし]]を防止するため、あなたは投稿ブロックされました。'
},
// Begin partial block templates, accessed in Twinkle.block.blockGroupsPartial
'uw-epblock': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: false,
noemail: true,
pageParam: false,
reasonParam: true,
reason: 'メールを用いた嫌がらせ',
summary: 'あなたは嫌がらせを行ったため他の利用者にメールを送ることをブロックされました'
},
'uw-ewpblock': {
autoblock: true,
nocreate: false,
pageParam: false,
reasonParam: true,
reason: '[[WP:EW|編集合戦]]',
summary: 'あなたは[[WP:EW|編集合戦]]を行ったため部分ブロックされました'
},
'uw-pblock': {
autoblock: true,
nocreate: false,
pageParam: false,
reasonParam: true,
summary: 'あなたはウィキペディアの一部から部分ブロックされました'
},
'uw-pblockindef': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: false,
pageParam: false,
reasonParam: true,
summary: 'あなたはウィキペディアの一部から無期限部分ブロックされました'
}
};
Twinkle.block.transformBlockPresets = function twinkleblockTransformBlockPresets() {
// supply sensible defaults
$.each(Twinkle.block.blockPresetsInfo, (preset, settings) => {
settings.summary = settings.summary || settings.reason;
settings.sig = settings.sig !== undefined ? settings.sig : 'yes';
settings.watchuser = settings.watchuser !== undefined ? settings.watchuser : true;
settings.indefinite = settings.indefinite || Morebits.string.isInfinity(settings.expiry);
if (!Twinkle.block.isRegistered && settings.indefinite) {
settings.expiry = '1 week';
} else {
settings.expiry = settings.expiry || '1 week';
}
Twinkle.block.blockPresetsInfo[preset] = settings;
});
};
// These are the groups of presets and defines the order in which they appear. For each list item:
// label: <string, the description that will be visible in the dropdown>
// value: <string, the key of a preset in blockPresetsInfo>
Twinkle.block.blockGroups = [
{
label: 'よくある理由',
list: [
{ label: '全般ブロック (カスタム理由)', value: 'uw-block' }, // ends up being default for registered users
{ label: '全般ブロック (カスタム理由) - IP', value: 'uw-ablock', selected: true }, // set only when blocking IP
{ label: '全般ブロック (カスタム理由) - 無期限', value: 'uw-blockindef' },
{ label: 'ブロック中の会話ページの不適切な使用', value: 'uw-talkrevoked' },
{ label: '全般ブロック - 会話ページ禁止', value: 'uw-blocknotalk' },
{ label: 'sockpuppet', value: 'uw-spoablock' },
{ label: '荒らし', value: 'uw-vblock' },
{ label: '各種方針の熟読期間', value: 'uw-readpoliciesblock' },
{ label: '会話ページをお読みください', value: 'uw-readtalkblock' }
]
},
{
label: '方針',
list: [
{ label: 'スリー・リバート・ルール違反', value: '3rr' },
{ label: '過度の編集合戦', value: 'uw-ewblock' },
{ label: 'パスワード公開', value: 'uw-compblock' },
{ label: '別アカウントなどによるブロック破り', value: 'uw-evadeblock' },
{ label: '多重アカウントの不適切な使用', value: 'uw-sockblock' },
{ label: '未承認のボット', value: 'uw-botblock' },
{ label: '有償の寄稿の開示に応じない利用者', value: 'uw-upeblock' }
]
},
{
label: '荒らし',
list: [
{ label: '存命人物に関する不適切な編集', value: 'uw-bioblock' },
{ label: '虚偽情報の記載', value: 'uw-fblock' },
{ label: '内容の無差別な除去', value: 'uw-dblock' },
{ label: '宣伝・広告投稿はお止めください', value: 'uw-adblock' },
{ label: '悪戯投稿はお止めください', value: 'uw-sncblock' },
{ label: '悪戯投稿', value: 'uw-ncblock' },
{ label: '暴言または嫌がらせ', value: 'uw-pablock' },
{ label: '対話拒否', value: 'uw-rjtblock' },
{ label: '目的外利用', value: 'uw-oblock' },
{ label: '編集フィルター記録', value: 'uw-efblock' },
{ label: '法的な脅迫', value: 'uw-lblock' },
{ label: 'Spam', value: 'uw-sblock' }
]
},
{
label: '不適切な利用者名',
list: [
{ label: '宣伝目的の名前, ソフトブロック', value: 'uw-softerblock' },
{ label: '宣伝目的の名前, ハードブロック', value: 'uw-spamublock' },
{ label: '不適切な利用者名, ソフトブロック', value: 'uw-ublock' },
{ label: '不適切な利用者名, ハードブロック', value: 'uw-uhblock' },
{ label: 'よく似ている利用者名, ソフトブロック', value: 'uw-ublock-double' },
{ label: 'なりすましの利用者名, ハードブロック', value: 'uw-uhblock-double' },
{ label: '有名人を表す利用者名, ソフトブロック', value: 'uw-ublock-wellknown' },
{ label: '団体を表す利用者名, ソフトブロック', value: 'uw-causeblock' },
{ label: '不適切な利用者名, 荒らし目的のアカウント', value: 'uw-vaublock' }
]
},
{
label: 'テンプレート化された理由',
list: [
{ label: 'blocked proxy', value: 'blocked proxy' },
{ label: 'CheckUser block', value: 'CheckUser block' },
{ label: 'checkuserblock-account', value: 'checkuserblock-account' },
{ label: 'checkuserblock-wide', value: 'checkuserblock-wide' },
{ label: 'oversightblock', value: 'oversightblock' },
{ label: 'range block login', value: 'range block login' } // Only for IP ranges, selected for non-/64 ranges in filtered_block_groups
]
}
];
Twinkle.block.blockGroupsPartial = [
{
label: 'よくある部分ブロック理由',
list: [
{ label: '全般部分ブロック (カスタム理由)', value: 'uw-pblock', selected: true },
{ label: '全般部分ブロック (カスタム理由) - 無期限', value: 'uw-pblockindef' },
{ label: '編集合戦', value: 'uw-ewpblock' },
{ label: 'メールを用いた嫌がらせ', value: 'uw-epblock' }
]
}
];
Twinkle.block.callback.filtered_block_groups = function twinkleblockCallbackFilteredBlockGroups(group, showTemplate) {
return $.map(group, (blockGroup) => {
const list = $.map(blockGroup.list, (blockPreset) => {
switch (blockPreset.value) {
case 'uw-talkrevoked':
if (blockedUserName !== relevantUserName) {
return;
}
break;
case 'range block login':
if (!Morebits.ip.isRange(relevantUserName)) {
return;
}
blockPreset.selected = !Morebits.ip.get64(relevantUserName);
break;
case 'CheckUser block':
case 'checkuserblock-account':
case 'checkuserblock-wide':
if (!Morebits.userIsInGroup('checkuser')) {
return;
}
break;
case 'oversightblock':
if (!Morebits.userIsInGroup('suppress')) {
return;
}
break;
default:
break;
}
const blockSettings = Twinkle.block.blockPresetsInfo[blockPreset.value];
let allowedUserType;
// for regular users and temporary accounts
if (blockSettings.forRegisteredOnly) {
allowedUserType = Twinkle.block.isRegistered;
// for temporary accounts
} else if (blockSettings.forTempAccountsOnly) {
allowedUserType = mw.util.isTemporaryUser(mw.config.get('wgRelevantUserName'));
// for IPs
} else if (blockSettings.forIPsOnly) {
allowedUserType = !Twinkle.block.isRegistered;
} else {
allowedUserType = true;
}
if (!(blockSettings.templateName && showTemplate) && allowedUserType) {
const templateName = blockSettings.templateName || blockPreset.value;
return {
label: (showTemplate ? '{{' + templateName + '}}: ' : '') + blockPreset.label,
value: blockPreset.value,
data: [{
name: 'template-name',
value: templateName
}],
selected: !!blockPreset.selected,
disabled: !!blockPreset.disabled
};
}
});
if (list.length) {
return {
label: blockGroup.label,
list: list
};
}
});
};
Twinkle.block.callback.change_preset = function twinkleblockCallbackChangePreset(e) {
const form = e.target.form, key = form.preset.value;
if (!key) {
return;
}
Twinkle.block.callback.update_form(e, Twinkle.block.blockPresetsInfo[key]);
if (form.template) {
form.template.value = Twinkle.block.blockPresetsInfo[key].templateName || key;
Twinkle.block.callback.change_template(e);
}
};
Twinkle.block.callback.change_expiry = function twinkleblockCallbackChangeExpiry(e) {
const expiry = e.target.form.expiry;
if (e.target.value === 'custom') {
Morebits.QuickForm.setElementVisibility(expiry.parentNode, true);
} else {
Morebits.QuickForm.setElementVisibility(expiry.parentNode, false);
expiry.value = e.target.value;
}
};
Twinkle.block.seeAlsos = [];
Twinkle.block.callback.toggle_see_alsos = function twinkleblockCallbackToggleSeeAlso() {
const joinEnum = function(e) {
return e.join('および');
};
const reason = this.form.reason.value.replace(
new RegExp('( <!--|;) ' + joinEnum(Twinkle.block.seeAlsos) + '参照( -->)?'), ''
);
Twinkle.block.seeAlsos = Twinkle.block.seeAlsos.filter((el) => el !== this.value);
if (this.checked) {
Twinkle.block.seeAlsos.push(this.value);
}
const seeAlsoMessage = joinEnum(Twinkle.block.seeAlsos);
if (!Twinkle.block.seeAlsos.length) {
this.form.reason.value = reason;
} else if (reason.includes('{{')) {
this.form.reason.value = reason + ' <!-- ' + seeAlsoMessage + '参照 -->';
} else {
this.form.reason.value = reason + '; ' + seeAlsoMessage + '参照';
}
};
Twinkle.block.callback.update_form = function twinkleblockCallbackUpdateForm(e, data) {
const form = e.target.form;
let expiry = data.expiry;
// don't override original expiry if useInitialOptions is set
if (!data.useInitialOptions) {
if (Date.parse(expiry)) {
expiry = new Date(expiry).toGMTString();
form.expiry_preset.value = 'custom';
} else {
form.expiry_preset.value = data.expiry || 'custom';
}
form.expiry.value = expiry;
if (form.expiry_preset.value === 'custom') {
Morebits.QuickForm.setElementVisibility(form.expiry.parentNode, true);
} else {
Morebits.QuickForm.setElementVisibility(form.expiry.parentNode, false);
}
}
// boolean-flipped options, more at [[mw:API:Block]]
data.disabletalk = data.disabletalk !== undefined ? data.disabletalk : false;
data.hardblock = data.hardblock !== undefined ? data.hardblock : false;
// disable autoblock if blocking a bot
if (Twinkle.block.userIsBot || /bot\b/i.test(relevantUserName)) {
data.autoblock = false;
}
$(form).find('[name=field_block_options]').find(':checkbox').each((i, el) => {
// don't override original options if useInitialOptions is set
if (data.useInitialOptions && data[el.name] === undefined) {
return;
}
const check = data[el.name] === '' || !!data[el.name];
$(el).prop('checked', check);
});
if (data.prependReason && data.reason) {
form.reason.value = data.reason + '; ' + form.reason.value;
} else {
form.reason.value = data.reason || '';
}
// Clear and/or set any partial page or namespace restrictions
if (form.pagerestrictions) {
const $pageSelect = $(form).find('[name=pagerestrictions]');
const $namespaceSelect = $(form).find('[name=namespacerestrictions]');
// Respect useInitialOptions by clearing data when switching presets
// In practice, this will always clear, since no partial presets use it
if (!data.useInitialOptions) {
$pageSelect.val(null).trigger('change');
$namespaceSelect.val(null).trigger('change');
}
// Add any preset options; in practice, just used for prior block settings
if (data.restrictions) {
if (data.restrictions.pages && !$pageSelect.val().length) {
const pages = data.restrictions.pages.map((pr) => pr.title);
// since page restrictions use an ajax source, we
// short-circuit that and just add a new option
pages.forEach((page) => {
if (!$pageSelect.find("option[value='" + $.escapeSelector(page) + "']").length) {
const newOption = new Option(page, page, true, true);
$pageSelect.append(newOption);
}
});
$pageSelect.val($pageSelect.val().concat(pages)).trigger('change');
}
if (data.restrictions.namespaces) {
$namespaceSelect.val($namespaceSelect.val().concat(data.restrictions.namespaces)).trigger('change');
}
}
}
};
Twinkle.block.callback.change_template = function twinkleblockcallbackChangeTemplate(e) {
const form = e.target.form, value = form.template.value, settings = Twinkle.block.blockPresetsInfo[value];
const blockBox = $(form).find('[name=actiontype][value=block]').is(':checked');
const partialBox = $(form).find('[name=actiontype][value=partial]').is(':checked');
const templateBox = $(form).find('[name=actiontype][value=template]').is(':checked');
// Block form is not present
if (!blockBox) {
if (settings.indefinite || settings.nonstandard) {
if (Twinkle.block.prev_template_expiry === null) {
Twinkle.block.prev_template_expiry = form.template_expiry.value || '';
}
form.template_expiry.parentNode.style.display = 'none';
form.template_expiry.value = 'infinity';
} else if (form.template_expiry.parentNode.style.display === 'none') {
if (Twinkle.block.prev_template_expiry !== null) {
form.template_expiry.value = Twinkle.block.prev_template_expiry;
Twinkle.block.prev_template_expiry = null;
}
form.template_expiry.parentNode.style.display = 'block';
}
if (Twinkle.block.prev_template_expiry) {
form.expiry.value = Twinkle.block.prev_template_expiry;
}
Morebits.QuickForm.setElementVisibility(form.notalk.parentNode, !settings.nonstandard);
// Partial
Morebits.QuickForm.setElementVisibility(form.noemail_template.parentNode, partialBox);
Morebits.QuickForm.setElementVisibility(form.nocreate_template.parentNode, partialBox);
} else if (templateBox) { // Only present if block && template forms both visible
Morebits.QuickForm.setElementVisibility(
form.blank_duration.parentNode,
!settings.indefinite && !settings.nonstandard
);
}
// Only particularly relevant if template form is present
Morebits.QuickForm.setElementVisibility(form.article.parentNode, settings && !!settings.pageParam);
Morebits.QuickForm.setElementVisibility(form.block_reason.parentNode, settings && !!settings.reasonParam);
// Partial block
Morebits.QuickForm.setElementVisibility(form.area.parentNode, partialBox && !blockBox);
form.root.previewer.closePreview();
};
Twinkle.block.prev_template_expiry = null;
Twinkle.block.callback.preview = function twinkleblockcallbackPreview(form) {
const params = {
article: form.article.value,
blank_duration: form.blank_duration ? form.blank_duration.checked : false,
disabletalk: form.disabletalk.checked || (form.notalk ? form.notalk.checked : false),
expiry: form.template_expiry ? form.template_expiry.value : form.expiry.value,
hardblock: Twinkle.block.isRegistered ? form.autoblock.checked : form.hardblock.checked,
indefinite: Morebits.string.isInfinity(form.template_expiry ? form.template_expiry.value : form.expiry.value),
reason: form.block_reason.value,
template: form.template.value,
partial: $(form).find('[name=actiontype][value=partial]').is(':checked'),
pagerestrictions: $(form.pagerestrictions).val() || [],
namespacerestrictions: $(form.namespacerestrictions).val() || [],
noemail: form.noemail.checked || (form.noemail_template ? form.noemail_template.checked : false),
nocreate: form.nocreate.checked || (form.nocreate_template ? form.nocreate_template.checked : false),
area: form.area.value
};
const templateText = Twinkle.block.callback.getBlockNoticeWikitext(params);
form.previewer.beginRender(templateText, 'User_talk:' + relevantUserName); // Force wikitext/correct username
};
Twinkle.block.callback.evaluate = function twinkleblockCallbackEvaluate(e) {
const $form = $(e.target),
toBlock = $form.find('[name=actiontype][value=block]').is(':checked'),
toWarn = $form.find('[name=actiontype][value=template]').is(':checked'),
toPartial = $form.find('[name=actiontype][value=partial]').is(':checked');
let blockoptions = {}, templateoptions = {};
Twinkle.block.callback.saveFieldset($form.find('[name=field_block_options]'));
Twinkle.block.callback.saveFieldset($form.find('[name=field_template_options]'));
blockoptions = Twinkle.block.field_block_options;
templateoptions = Twinkle.block.field_template_options;
templateoptions.disabletalk = !!(templateoptions.disabletalk || blockoptions.disabletalk);
templateoptions.hardblock = !!blockoptions.hardblock;
delete blockoptions.expiry_preset; // remove extraneous
// Partial API requires this to be gone, not false or 0
if (toPartial) {
blockoptions.partial = templateoptions.partial = true;
}
templateoptions.pagerestrictions = $form.find('[name=pagerestrictions]').val() || [];
templateoptions.namespacerestrictions = $form.find('[name=namespacerestrictions]').val() || [];
// Format for API here rather than in saveFieldset
blockoptions.pagerestrictions = templateoptions.pagerestrictions.join('|');
blockoptions.namespacerestrictions = templateoptions.namespacerestrictions.join('|');
// use block settings as warn options where not supplied
templateoptions.summary = templateoptions.summary || blockoptions.reason;
templateoptions.expiry = templateoptions.template_expiry || blockoptions.expiry;
if (toBlock) {
if (blockoptions.partial) {
if (blockoptions.disabletalk && !blockoptions.namespacerestrictions.includes('3')) {
return alert('利用者会話名前空間の編集を制限しない限り、部分ブロックでは会話ページの編集を禁止することはできません!');
}
if (!blockoptions.namespacerestrictions && !blockoptions.pagerestrictions) {
if (!blockoptions.noemail && !blockoptions.nocreate) { // Blank entries technically allowed [[phab:T208645]]
return alert('ページや名前空間が選択されておらず、メール送信やアカウント作成の制限を選択されていません; 少なくとも一つオプションを選択してください!');
} else if ((templateoptions.template !== 'uw-epblock' || $form.find('select[name="preset"]').val() !== 'uw-epblock') &&
// Don't require confirmation if email harassment defaults are set
!confirm('You are about to block with no restrictions on page or namespace editing, are you sure you want to proceed?')) {
return;
}
}
}
if (!blockoptions.expiry) {
return alert('有効期限を入力してください!');
} else if (Morebits.string.isInfinity(blockoptions.expiry) && !Twinkle.block.isRegistered) {
return alert('IPアドレスを無期限ブロックすることはできません!');
}
if (!blockoptions.reason) {
return alert('ブロック理由を入力してください!');
}
Morebits.SimpleWindow.setButtonsEnabled(false);
Morebits.Status.init(e.target);
const statusElement = new Morebits.Status('ブロックの実行');
blockoptions.action = 'block';
blockoptions.user = relevantUserName;
// boolean-flipped options
blockoptions.anononly = blockoptions.hardblock ? undefined : true;
blockoptions.allowusertalk = blockoptions.disabletalk ? undefined : true;
/*
Check if block status changed while processing the form.
There's a lot to consider here. list=blocks provides the
current block status, but there are at least two issues with
relying on it. First, the id doesn't update on a reblock,
meaning the individual parameters need to be compared. This
can be done roughly with JSON.stringify - we can thankfully
rely on order from the server, although sorting would be
fine if not - but falsey values are problematic and is
non-ideal. More importantly, list=blocks won't indicate if a
non-blocked user is blocked then unblocked. This should be
exceedingy rare, but regardless, we thus need to check
list=logevents, which has a nicely updating logid
parameter. We can't rely just on that, though, since it
doesn't account for blocks that have expired on their own.
As such, we use both. Using some ternaries, the logid
variables are false if there's no logevents, so if they
aren't equal we defintely have a changed entry (send
confirmation). If they are equal, then either the user was
never blocked (the block statuses will be equal, no
confirmation) or there's no new block, in which case either
a block expired (different statuses, confirmation) or the
same block is still active (same status, no confirmation).
*/
const query = {
format: 'json',
action: 'query',
list: 'blocks|logevents',
letype: 'block',
lelimit: 1,
letitle: 'User:' + blockoptions.user
};
// bkusers doesn't catch single IPs blocked as part of a range block
if (mw.util.isIPAddress(blockoptions.user, true)) {
query.bkip = blockoptions.user;
} else {
query.bkusers = blockoptions.user;
}
api.get(query).then((data) => {
let block = data.query.blocks[0];
// As with the initial data fetch, if an IP is blocked
// *and* rangeblocked, this would only grab whichever
// block is more recent, which would likely mean a
// mismatch. However, if the rangeblock is updated
// while filling out the form, this won't detect that,
// but that's probably fine.
if (data.query.blocks.length > 1 && block.user !== relevantUserName) {
block = data.query.blocks[1];
}
const logevents = data.query.logevents[0];
const logid = data.query.logevents.length ? logevents.logid : false;
if (logid !== Twinkle.block.blockLogId || !!block !== !!Twinkle.block.currentBlockInfo) {
let message = 'The block status of ' + blockoptions.user + ' has changed. ';
if (block) {
message += 'New status: ';
} else {
message += 'Last entry: ';
}
let logExpiry = '';
if (logevents.params.duration) {
if (logevents.params.duration === 'infinity') {
logExpiry = 'indefinitely';
} else {
const expiryDate = new Morebits.Date(logevents.params.expiry);
logExpiry += (expiryDate.isBefore(new Date()) ? ', expired ' : ' until ') + expiryDate.calendar();
}
} else { // no duration, action=unblock, just show timestamp
logExpiry = ' ' + new Morebits.Date(logevents.timestamp).calendar();
}
message += Morebits.string.toUpperCaseFirstChar(logevents.action) + 'ed by ' + logevents.user + logExpiry +
' for "' + logevents.comment + '". Do you want to override with your settings?';
if (!confirm(message)) {
Morebits.Status.info('ブロックの実行', '利用者によって中断');
return;
}
blockoptions.reblock = 1; // Writing over a block will fail otherwise
}
// execute block
blockoptions.token = mw.user.tokens.get('csrfToken');
const mbApi = new Morebits.wiki.Api('ブロックの実行', blockoptions, (() => {
statusElement.info('完了');
if (toWarn) {
Twinkle.block.callback.issue_template(templateoptions);
}
}));
mbApi.post();
});
} else if (toWarn) {
Morebits.SimpleWindow.setButtonsEnabled(false);
Morebits.Status.init(e.target);
Twinkle.block.callback.issue_template(templateoptions);
} else {
return alert('Please give Twinkle something to do!');
}
};
Twinkle.block.callback.issue_template = function twinkleblockCallbackIssueTemplate(formData) {
// Use wgRelevantUserName to ensure the block template goes to a single IP and not to the
// "talk page" of an IP range (which does not exist)
const userTalkPage = 'User_talk:' + mw.config.get('wgRelevantUserName');
const params = Twinkle.block.combineFormDataAndFieldTemplateOptions(
formData,
Twinkle.block.blockPresetsInfo[formData.template],
Twinkle.block.field_template_options.block_reason,
Twinkle.block.field_template_options.notalk,
Twinkle.block.field_template_options.noemail_template,
Twinkle.block.field_template_options.nocreate_template
);
Morebits.wiki.actionCompleted.redirect = userTalkPage;
Morebits.wiki.actionCompleted.notice = 'Actions complete, loading user talk page in a few seconds';
const wikipediaPage = new Morebits.wiki.Page(userTalkPage, 'User talk page modification');
wikipediaPage.setCallbackParameters(params);
wikipediaPage.load(Twinkle.block.callback.main);
};
Twinkle.block.combineFormDataAndFieldTemplateOptions = function(formData, messageData, reason, disabletalk, noemail, nocreate) {
return $.extend(formData, {
messageData: messageData,
reason: reason,
disabletalk: disabletalk,
noemail: noemail,
nocreate: nocreate
});
};
Twinkle.block.callback.getBlockNoticeWikitext = function(params) {
let text = '{{';
const settings = Twinkle.block.blockPresetsInfo[params.template];
if (!settings.nonstandard) {
text += 'subst:' + params.template;
if (params.article && settings.pageParam) {
text += '|page=' + params.article;
}
if (!/te?mp|^\s*$|min/.exec(params.expiry)) {
if (params.indefinite) {
text += '|indef=yes';
} else if (!params.blank_duration && !new Morebits.Date(params.expiry).isValid()) {
// Block template wants a duration, not date
text += '|time=' + params.expiry;
}
}
if (!Twinkle.block.isRegistered && !params.hardblock) {
text += '|anon=yes';
}
if (params.reason) {
text += '|reason=' + params.reason;
}
if (params.disabletalk) {
text += '|notalk=yes';
}
// Currently, all partial block templates are "standard"
// Building the template, however, takes a fair bit of logic
if (params.partial) {
if (params.pagerestrictions.length || params.namespacerestrictions.length) {
const makeSentence = function(array) {
if (array.length < 3) {
return array.join(' and ');
}
const last = array.pop();
return array.join(', ') + ', and ' + last;
};
text += '|area=' + (params.indefinite ? 'certain ' : 'from certain ');
if (params.pagerestrictions.length) {
text += 'pages (' + makeSentence(params.pagerestrictions.map((p) => '[[:' + p + ']]'));
text += params.namespacerestrictions.length ? ') and certain ' : ')';
}
if (params.namespacerestrictions.length) {
// 1 => Talk, 2 => User, etc.
const namespaceNames = params.namespacerestrictions.map((id) => menuFormattedNamespaces[id]);
text += '[[Wikipedia:Namespace|namespaces]] (' + makeSentence(namespaceNames) + ')';
}
} else if (params.area) {
text += '|area=' + params.area;
} else {
if (params.noemail) {
text += '|email=yes';
}
if (params.nocreate) {
text += '|accountcreate=yes';
}
}
}
} else {
text += params.template;
}
if (settings.sig) {
text += '|sig=' + settings.sig;
}
return text + '}}';
};
Twinkle.block.callback.main = function twinkleblockcallbackMain(pageobj) {
const params = pageobj.getCallbackParameters(),
date = new Morebits.Date(pageobj.getLoadTime()),
messageData = params.messageData;
let text;
params.indefinite = Morebits.string.isInfinity(params.expiry);
if (Twinkle.getPref('blankTalkpageOnIndefBlock') && params.template !== 'uw-lblock' && params.indefinite) {
Morebits.Status.info('Info', 'Blanking talk page per preferences and creating a new talk page section for this month');
text = date.monthHeader() + '\n';
} else {
text = pageobj.getPageText();
const dateHeaderRegex = date.monthHeaderRegex();
let dateHeaderRegexLast, dateHeaderRegexResult;
while ((dateHeaderRegexLast = dateHeaderRegex.exec(text)) !== null) {
dateHeaderRegexResult = dateHeaderRegexLast;
}
// If dateHeaderRegexResult is null then lastHeaderIndex is never checked. If it is not null but
// \n== is not found, then the date header must be at the very start of the page. lastIndexOf
// returns -1 in this case, so lastHeaderIndex gets set to 0 as desired.
const lastHeaderIndex = text.lastIndexOf('\n==') + 1;
if (text.length > 0) {
text += '\n\n';
}
if (!dateHeaderRegexResult || dateHeaderRegexResult.index !== lastHeaderIndex) {
Morebits.Status.info('Info', 'Will create a new talk page section for this month, as none was found');
text += date.monthHeader() + '\n';
}
}
params.expiry = typeof params.template_expiry !== 'undefined' ? params.template_expiry : params.expiry;
text += Twinkle.block.callback.getBlockNoticeWikitext(params);
// build the edit summary
let summary = messageData.summary;
if (messageData.suppressArticleInSummary !== true && params.article) {
summary += ' on [[:' + params.article + ']]';
}
summary += '.';
pageobj.setPageText(text);
pageobj.setEditSummary(summary);
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.setWatchlist(Twinkle.getPref('watchWarnings'));
pageobj.save();
};
Twinkle.addInitCallback(Twinkle.block, 'block');
}());
// </nowiki>
djtuvoxu1f5ishw1ka4z7ylh287ubt2
769546
769539
2026-10-22T00:35:21Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 64781a2167c76216d4920097bd68990736ae168d: 期限変更
769546
javascript
text/javascript
// <nowiki>
(function() {
const api = new mw.Api();
let relevantUserName, blockedUserName, blockWindow;
const menuFormattedNamespaces = $.extend({}, mw.config.get('wgFormattedNamespaces'));
menuFormattedNamespaces[0] = '(Article)';
/*
****************************************
*** twinkleblock.js: Block module
****************************************
* Mode of invocation: Tab ("Block")
* Active on: Any page with relevant user name (userspace, contribs, etc.)
*/
Twinkle.block = function twinkleblock() {
relevantUserName = mw.config.get('wgRelevantUserName');
// should show on Contributions or Block pages, anywhere there's a relevant user
// Ignore ranges wider than the CIDR limit
if (Morebits.userIsSysop && relevantUserName && (!Morebits.ip.isRange(relevantUserName) || Morebits.ip.validCIDR(relevantUserName))) {
Twinkle.addPortletLink(Twinkle.block.callback, 'Block', 'tw-block', '利用者をブロック');
}
};
Twinkle.block.callback = function twinkleblockCallback() {
if (relevantUserName === mw.config.get('wgUserName') &&
!confirm('自分自身をブロックしようとしています!続行しますか?')) {
return;
}
Twinkle.block.currentBlockInfo = undefined;
Twinkle.block.field_block_options = {};
Twinkle.block.field_template_options = {};
blockWindow = new Morebits.SimpleWindow(650, 530);
// need to be verbose about who we're blocking
blockWindow.setTitle(relevantUserName + 'をブロックまたは投稿ブロックテンプレートを送信する');
blockWindow.setScriptName('Twinkle');
// Always added, hidden later if actual user not blocked
blockWindow.addFooterLink('ブロックを解除', 'Special:Unblock/' + relevantUserName);
blockWindow.addFooterLink('投稿ブロックテンプレート', 'Wikipedia:主要なテンプレート/利用者‐会話名前空間#投稿ブロックテンプレート');
blockWindow.addFooterLink('投稿ブロックの方針', 'WP:BLOCK');
blockWindow.addFooterLink('ブロック設定', 'User:Syunsyunminmin/Twinkle/Preferences#block');
blockWindow.addFooterLink('Twinkle help', ':en:WP:TW/DOC#block');
blockWindow.addFooterLink('フィードバック', 'User talk:Syunsyunminmin/Twinkle');
const form = new Morebits.QuickForm(Twinkle.block.callback.evaluate);
const actionfield = form.append({
type: 'field',
label: '操作の種類'
});
actionfield.append({
type: 'checkbox',
name: 'actiontype',
event: Twinkle.block.callback.change_action,
list: [
{
label: '利用者をブロック',
value: 'block',
tooltip: '指定されたオプションで対象利用者をブロックします。部分ブロックのチェックが外れている場合、サイト全体のブロックとなります。',
checked: true
},
{
label: '部分ブロック',
value: 'partial',
tooltip: '部分ブロックと部分ブロックテンプレートを有効にする。',
checked: Twinkle.getPref('defaultToPartialBlocks') // Overridden if already blocked
},
{
label: '利用者会話ページに投稿ブロックテンプレートを追加',
value: 'template',
tooltip: '(※未対応)ブロックした管理者がブロックテンプレートを送信するのを忘れた場合、あるいは、テンプレートを送信せずに利用者をブロックした場合、これを使って適切なテンプレートを送信することができます。部分ブロックテンプレートは部分ブロックボックスにチェックを入れてください。',
// Disallow when viewing the block dialog on an IP range
checked: false,
disabled: Morebits.ip.isRange(relevantUserName)
}
]
});
/*
Add option for IPv6 ranges smaller than /64 to upgrade to the 64
CIDR ([[WP:/64]]). This is one of the few places where we want
wgRelevantUserName since this depends entirely on the original user.
In theory, we shouldn't use Morebits.ip.get64 here since since we want
to exclude functionally-equivalent /64s. That'd be:
// if (mw.util.isIPv6Address(mw.config.get('wgRelevantUserName'), true) &&
// (mw.util.isIPv6Address(mw.config.get('wgRelevantUserName')) || parseInt(mw.config.get('wgRelevantUserName').replace(/^(.+?)\/?(\d{1,3})?$/, '$2'), 10) > 64)) {
In practice, though, since functionally-equivalent ranges are
(mis)treated as separate by MediaWiki's logging ([[phab:T146628]]),
using Morebits.ip.get64 provides a modicum of relief in thise case.
*/
const sixtyFour = Morebits.ip.get64(mw.config.get('wgRelevantUserName'));
if (sixtyFour && sixtyFour !== mw.config.get('wgRelevantUserName')) {
const block64field = form.append({
type: 'field',
label: '/64範囲ブロックに変換',
name: 'field_64'
});
block64field.append({
type: 'div',
style: 'margin-bottom: 0.5em',
label: ['通常は、より良いとは言わないまでも、', $.parseHTML('<a target="_blank" href="' + mw.util.getUrl(':en:WP:/64') + '">/64の範囲ブロック</a>')[0], 'するだけで問題ないです (',
$.parseHTML('<a target="_blank" href="' + mw.util.getUrl('Special:Contributions/' + sixtyFour) + '">' + sixtyFour + '</a>)')[0], ').']
});
block64field.append({
type: 'checkbox',
name: 'block64',
event: Twinkle.block.callback.change_block64,
list: [{
checked: Twinkle.getPref('defaultToBlock64'),
label: '/64を代わりにブロックする',
value: 'block64',
tooltip: Morebits.ip.isRange(mw.config.get('wgRelevantUserName')) ? 'Will eschew leaving a template.' : 'Any template issued will go to the original IP: ' + mw.config.get('wgRelevantUserName')
}]
});
}
form.append({ type: 'field', label: 'プリセット', name: 'field_preset' });
form.append({ type: 'field', label: 'テンプレートオプション', name: 'field_template_options' });
form.append({ type: 'field', label: 'ブロックオプション', name: 'field_block_options' });
form.append({ type: 'submit' });
const result = form.render();
blockWindow.setContent(result);
blockWindow.display();
result.root = result;
Twinkle.block.fetchUserInfo(() => {
// Toggle initial partial state depending on prior block type,
// will override the defaultToPartialBlocks pref
if (blockedUserName === relevantUserName) {
$(result).find('[name=actiontype][value=partial]').prop('checked', Twinkle.block.currentBlockInfo.partial === '');
}
// clean up preset data (defaults, etc.), done exactly once, must be before Twinkle.block.callback.change_action is called
Twinkle.block.transformBlockPresets();
// init the controls after user and block info have been fetched
const evt = document.createEvent('Event');
evt.initEvent('change', true, true);
if (result.block64 && result.block64.checked) {
// Calls the same change_action event once finished
result.block64.dispatchEvent(evt);
} else {
result.actiontype[0].dispatchEvent(evt);
}
});
};
// Store fetched user data, only relevant if switching IPv6 to a /64
Twinkle.block.fetchedData = {};
// Processes the data from a query response, separated from
// Twinkle.block.fetchUserInfo to allow reprocessing of already-fetched data
Twinkle.block.processUserInfo = function twinkleblockProcessUserInfo(data, fn) {
let blockinfo = data.query.blocks[0];
// Soft redirect to Special:Block if the user is multi-blocked (#2178)
if (blockinfo && data.query.blocks.length > 1) {
// Remove submission buttons.
blockWindow.$dialog.find('.morebits-dialog-buttons').empty();
Morebits.Status.init(blockWindow.content.querySelector('form'));
Morebits.Status.warn(
`この対象には有効なブロックが${data.query.blocks.length}件あります`,
`Twinkleは複数ブロックに対応していません。[[Special:Block/${relevantUserName}]]を使用してください。`
);
return;
}
const userinfo = data.query.users[0];
// If an IP is blocked *and* rangeblocked, the above finds
// whichever block is more recent, not necessarily correct.
// Three seems... unlikely
if (data.query.blocks.length > 1 && blockinfo.user !== relevantUserName) {
blockinfo = data.query.blocks[1];
}
// Cache response, used when toggling /64 blocks
Twinkle.block.fetchedData[userinfo.name] = data;
Twinkle.block.isRegistered = !!userinfo.userid;
if (Twinkle.block.isRegistered) {
Twinkle.block.userIsBot = !!userinfo.groupmemberships && userinfo.groupmemberships.map((e) => e.group).includes('bot');
} else {
Twinkle.block.userIsBot = false;
}
if (blockinfo) {
// handle frustrating system of inverted boolean values
blockinfo.disabletalk = blockinfo.allowusertalk === undefined;
blockinfo.hardblock = blockinfo.anononly === undefined;
}
// will undefine if no blocks present
Twinkle.block.currentBlockInfo = blockinfo;
blockedUserName = Twinkle.block.currentBlockInfo && Twinkle.block.currentBlockInfo.user;
// Toggle unblock link if not the user in question; always first
const unblockLink = blockWindow.$dialog.find('.morebits-dialog-footerlinks a')[0];
if (blockedUserName !== relevantUserName) {
unblockLink.hidden = true;
unblockLink.nextSibling.hidden = true; // link+trailing bullet
} else {
unblockLink.hidden = false;
unblockLink.nextSibling.hidden = false; // link+trailing bullet
}
// Semi-busted on ranges, see [[phab:T270737]] and [[phab:T146628]].
// Basically, logevents doesn't treat functionally-equivalent ranges
// as equivalent, meaning any functionally-equivalent IP range is
// misinterpreted by the log throughout. Without logevents
// redirecting (like Special:Block does) we would need a function to
// parse ranges, which is a pain. IPUtils has the code, but it'd be a
// lot of cruft for one purpose.
Twinkle.block.hasBlockLog = !!data.query.logevents.length;
Twinkle.block.blockLog = Twinkle.block.hasBlockLog && data.query.logevents;
// Used later to check if block status changed while filling out the form
Twinkle.block.blockLogId = Twinkle.block.hasBlockLog ? data.query.logevents[0].logid : false;
if (typeof fn === 'function') {
return fn();
}
};
Twinkle.block.fetchUserInfo = function twinkleblockFetchUserInfo(fn) {
const query = {
format: 'json',
action: 'query',
list: 'blocks|users|logevents',
letype: 'block',
lelimit: 1,
letitle: 'User:' + relevantUserName,
bkprop: 'expiry|reason|flags|restrictions|range|user',
ususers: relevantUserName
};
// bkusers doesn't catch single IPs blocked as part of a range block
if (mw.util.isIPAddress(relevantUserName, true)) {
query.bkip = relevantUserName;
} else {
query.bkusers = relevantUserName;
// groupmemberships only relevant for registered users
query.usprop = 'groupmemberships';
}
api.get(query).then((data) => {
Twinkle.block.processUserInfo(data, fn);
}, (msg) => {
Morebits.Status.init($('div[name="currentblock"] span').last()[0]);
Morebits.Status.warn('利用者情報の取得エラー', msg);
});
};
Twinkle.block.callback.saveFieldset = function twinkleblockCallbacksaveFieldset(fieldset) {
Twinkle.block[$(fieldset).prop('name')] = {};
$(fieldset).serializeArray().forEach((el) => {
// namespaces and pages for partial blocks are overwritten
// here, but we're handling them elsewhere so that's fine
Twinkle.block[$(fieldset).prop('name')][el.name] = el.value;
});
};
Twinkle.block.callback.change_block64 = function twinkleblockCallbackChangeBlock64(e) {
const $form = $(e.target.form),
$block64 = $form.find('[name=block64]'),
dialog = $form.closest('.morebits-dialog')[0];
// Show/hide block64 button
// Single IPv6, or IPv6 range smaller than a /64
const priorName = relevantUserName;
if ($block64.is(':checked')) {
relevantUserName = Morebits.ip.get64(mw.config.get('wgRelevantUserName'));
} else {
relevantUserName = mw.config.get('wgRelevantUserName');
}
// No templates for ranges, but if the original user is a single IP, offer the option
// (done separately in Twinkle.block.callback.issue_template)
const originalIsRange = Morebits.ip.isRange(mw.config.get('wgRelevantUserName'));
$form.find('[name=actiontype][value=template]').prop('disabled', originalIsRange);
// Refetch/reprocess user info then regenerate the main content
const regenerateForm = function() {
// Tweak titlebar text. In theory, we could save the dialog
// at initialization and then use `.setTitle`, but in practice that swallows
// the scriptName and requires `.display`ing, which jumps the
// window. It's just a line of text, so this is fine.
const titleBar = dialog.querySelector('.morebits-dialog-title').firstChild.nextSibling;
titleBar.nodeValue = titleBar.nodeValue.replace(priorName, relevantUserName);
// Tweak unblock link
const unblockLink = dialog.querySelector('.morebits-dialog-footerlinks a');
unblockLink.href = unblockLink.href.replace(priorName, relevantUserName);
unblockLink.title = unblockLink.title.replace(priorName, relevantUserName);
// Correct partial state
$form.find('[name=actiontype][value=partial]').prop('checked', Twinkle.getPref('defaultToPartialBlocks'));
if (blockedUserName === relevantUserName) {
$form.find('[name=actiontype][value=partial]').prop('checked', Twinkle.block.currentBlockInfo.partial === '');
}
// Set content appropriately
Twinkle.block.callback.change_action(e);
};
if (Twinkle.block.fetchedData[relevantUserName]) {
Twinkle.block.processUserInfo(Twinkle.block.fetchedData[relevantUserName], regenerateForm);
} else {
Twinkle.block.fetchUserInfo(regenerateForm);
}
};
Twinkle.block.callback.change_action = function twinkleblockCallbackChangeAction(e) {
let fieldPreset, fieldTemplateOptions, fieldBlockOptions;
const $form = $(e.target.form);
// Make ifs shorter
const blockBox = $form.find('[name=actiontype][value=block]').is(':checked');
const templateBox = $form.find('[name=actiontype][value=template]').is(':checked');
const $partial = $form.find('[name=actiontype][value=partial]');
const partialBox = $partial.is(':checked');
let blockGroup = partialBox ? Twinkle.block.blockGroupsPartial : Twinkle.block.blockGroups;
$partial.prop('disabled', !blockBox && !templateBox);
// Add current block parameters as default preset
const prior = { label: '前ブロック' };
if (blockedUserName === relevantUserName) {
Twinkle.block.blockPresetsInfo.prior = $.extend({ watchuser: true }, Twinkle.block.currentBlockInfo);
// value not a valid template selection, chosen below by setting templateName
prior.list = [{ label: '前のブロック設定', value: 'prior', selected: true }];
// Arrays of objects are annoying to check
if (!blockGroup.some((bg) => bg.label === prior.label)) {
blockGroup.push(prior);
}
// Always ensure proper template exists/is selected when switching modes
if (partialBox) {
Twinkle.block.blockPresetsInfo.prior.templateName = Morebits.string.isInfinity(Twinkle.block.currentBlockInfo.expiry) ? 'uw-pblockindef' : 'uw-pblock';
} else {
if (!Twinkle.block.isRegistered) {
Twinkle.block.blockPresetsInfo.prior.templateName = 'uw-ablock';
} else {
Twinkle.block.blockPresetsInfo.prior.templateName = Morebits.string.isInfinity(Twinkle.block.currentBlockInfo.expiry) ? 'uw-blockindef' : 'uw-block';
}
}
} else {
// But first remove any prior prior
blockGroup = blockGroup.filter((bg) => bg.label !== prior.label);
}
Twinkle.block.callback.saveFieldset($('[name=field_block_options]'));
Twinkle.block.callback.saveFieldset($('[name=field_template_options]'));
if (blockBox) {
fieldPreset = new Morebits.QuickForm.Element({ type: 'field', label: 'プリセット', name: 'field_preset' });
fieldPreset.append({
type: 'select',
name: 'preset',
label: 'プリセットを選択:',
event: Twinkle.block.callback.change_preset,
list: Twinkle.block.callback.filtered_block_groups(blockGroup)
});
fieldBlockOptions = new Morebits.QuickForm.Element({ type: 'field', label: 'ブロックオプション', name: 'field_block_options' });
fieldBlockOptions.append({ type: 'div', name: 'currentblock', label: ' ' });
fieldBlockOptions.append({ type: 'div', name: 'hasblocklog', label: ' ' });
fieldBlockOptions.append({
type: 'select',
name: 'expiry_preset',
label: '期限:',
event: Twinkle.block.callback.change_expiry,
list: [
{ label: 'その他', value: 'custom', selected: true },
{ label: '無期限', value: 'infinity' },
{ label: '3時間', value: '3 hours' },
{ label: '12時間', value: '12 hours' },
{ label: '24時間', value: '24 hours' },
{ label: '31時間', value: '31 hours' },
{ label: '36時間', value: '36 hours' },
{ label: '48時間', value: '48 hours' },
{ label: '60時間', value: '60 hours' },
{ label: '72時間', value: '72 hours' },
{ label: '1週間', value: '1 week' },
{ label: '2週間', value: '2 weeks' },
{ label: '1ヶ月間', value: '1 month' },
{ label: '3ヶ月間', value: '3 months' },
{ label: '6ヶ月間', value: '6 months' },
{ label: '1年間', value: '1 year' },
{ label: '2年間', value: '2 years' },
{ label: '3年間', value: '3 years' }
]
});
fieldBlockOptions.append({
type: 'input',
name: 'expiry',
label: 'その他の期限',
tooltip: '"1 minute" (1分) や "19 days" (19日) のような相対的な時間や、"yyyymmddhhmm"のような絶対的なタイムスタンプを使用することができます。 (例: "200602011405"は2006年2月1日 14:05 UTCを意味します)',
value: Twinkle.block.field_block_options.expiry || Twinkle.block.field_template_options.template_expiry
});
if (partialBox) { // Partial block
fieldBlockOptions.append({
type: 'select',
multiple: true,
name: 'pagerestrictions',
label: '編集をブロックする特定のページ',
value: '',
tooltip: '最大で10ページ'
});
const ns = fieldBlockOptions.append({
type: 'select',
multiple: true,
name: 'namespacerestrictions',
label: '名前空間のブロック',
value: '',
tooltip: 'これらの名前空間の編集をブロックする'
});
$.each(menuFormattedNamespaces, (number, name) => {
// Ignore -1: Special; -2: Media; and 2300-2303: Gadget (talk) and Gadget definition (talk)
if (number >= 0 && number < 830) {
ns.append({ type: 'option', label: name, value: number });
}
});
}
const blockoptions = [
{
checked: Twinkle.block.field_block_options.nocreate,
label: 'アカウント作成をブロック',
name: 'nocreate',
value: '1'
}
];
if (Twinkle.block.isRegistered && !mw.util.isTemporaryUser(mw.config.get('wgRelevantUserName'))) {
blockoptions.push({
checked: Twinkle.block.field_block_options.noemail,
label: '利用者のメール送信をブロック',
name: 'noemail',
value: '1'
});
}
blockoptions.push({
checked: Twinkle.block.field_block_options.disabletalk,
label: 'ブロックされている間、この利用者が自分の会話ページを編集できないようにする。',
name: 'disabletalk',
value: '1',
tooltip: partialBox ? '部分ブロックを行う場合、利用者会話名前空間の編集をブロックしない限り、このチェックは外したままにしておかなければなりません。' : ''
});
if (Twinkle.block.isRegistered) {
blockoptions.push({
checked: Twinkle.block.field_block_options.autoblock,
label: '使用されたIPアドレスを自動ブロック',
name: 'autoblock',
value: '1'
});
} else {
blockoptions.push({
checked: Twinkle.block.field_block_options.hardblock,
label: 'ログイン利用者がこのIPアドレスを使用するのをブロックする (ハードブロック)',
name: 'hardblock',
value: '1'
});
}
blockoptions.push({
checked: Twinkle.block.field_block_options.watchuser,
label: '利用者(会話)ページをウォッチする',
name: 'watchuser',
value: '1'
});
fieldBlockOptions.append({
type: 'checkbox',
name: 'blockoptions',
list: blockoptions
});
fieldBlockOptions.append({
type: 'textarea',
label: '理由 (ブロック記録用):',
name: 'reason',
tooltip: '既定のブロックメッセージに追加して役立つ詳細を入力してください',
value: Twinkle.block.field_block_options.reason
});
fieldBlockOptions.append({
type: 'div',
name: 'filerlog_label',
label: '参照:',
style: 'display:inline-block;font-style:normal !important',
tooltip: 'フィルター記録、削除された投稿、関連する一時アカウントがブロックの判断に関わったことを示す参照情報を追加します。'
});
fieldBlockOptions.append({
type: 'checkbox',
name: 'filter_see_also',
event: Twinkle.block.callback.toggle_see_alsos,
style: 'display:inline-block; margin-right:5px',
list: [
{
label: 'フィルター記録',
checked: false,
value: 'フィルター記録'
}
]
});
fieldBlockOptions.append({
type: 'checkbox',
name: 'deleted_see_also',
event: Twinkle.block.callback.toggle_see_alsos,
style: 'display:inline-block; margin-right:5px',
list: [
{
label: '削除された投稿',
checked: false,
value: '削除された投稿記録'
}
]
});
if (mw.util.isTemporaryUser(mw.config.get('wgRelevantUserName'))) {
fieldBlockOptions.append({
type: 'checkbox',
name: 'related_see_also',
event: Twinkle.block.callback.toggle_see_alsos,
style: 'display:inline-block',
list: [
{
label: '関連する一時アカウント',
checked: false,
value: '関連する一時アカウント'
}
]
});
}
// Yet-another-logevents-doesn't-handle-ranges-well
if (blockedUserName === relevantUserName) {
fieldBlockOptions.append({ type: 'hidden', name: 'reblock', value: '1' });
}
}
if (templateBox) {
fieldTemplateOptions = new Morebits.QuickForm.Element({ type: 'field', label: 'テンプレートオプション', name: 'field_template_options' });
fieldTemplateOptions.append({
type: 'select',
name: 'template',
label: '会話ページのテンプレートを選択:',
event: Twinkle.block.callback.change_template,
list: Twinkle.block.callback.filtered_block_groups(blockGroup, true),
value: Twinkle.block.field_template_options.template
});
fieldTemplateOptions.append({
type: 'input',
name: 'article',
label: 'リンクするページ',
value: '',
tooltip: 'そのページが破壊行為の主な対象であった場合、通知内でリンクされることがあります。リンクするページがない場合は空欄にしてください。'
});
// Only visible if partial and not blocking
fieldTemplateOptions.append({
type: 'input',
name: 'area',
label: 'ブロックされた場所',
value: '',
tooltip: 'オプションで、利用者が編集をブロックされたページまたは名前空間の説明。'
});
if (!blockBox) {
fieldTemplateOptions.append({
type: 'input',
name: 'template_expiry',
label: 'ブロック期間:',
value: '',
tooltip: 'ブロックする期間。例えば24 hours、2 weeks、indefiniteなど'
});
}
fieldTemplateOptions.append({
type: 'input',
name: 'block_reason',
label: '"あなたがブロックされたのは..."',
tooltip: 'デフォルトの一般的な理由を置き換えるオプションの理由。全般の投稿ブロックテンプレートでのみ利用可能です。',
value: Twinkle.block.field_template_options.block_reason
});
if (blockBox) {
fieldTemplateOptions.append({
type: 'checkbox',
name: 'blank_duration',
list: [
{
label: 'テンプレートに有効期限を含めない',
checked: Twinkle.block.field_template_options.blank_duration,
tooltip: '期間を含める代わりに、ブロックテンプレートは「あなたは一時的にブロックされました」と表示します..."'
}
]
});
} else {
fieldTemplateOptions.append({
type: 'checkbox',
list: [
{
label: '会話ページの編集を禁止',
name: 'notalk',
checked: Twinkle.block.field_template_options.notalk,
tooltip: '会話ページの編集権限を剥奪したことを投稿ブロックテンプレートに記載します。'
},
{
label: '利用者のメール送信をブロック',
name: 'noemail_template',
checked: Twinkle.block.field_template_options.noemail_template,
tooltip: '対象範囲が未指定の場合、メール送信をブロックしたことを投稿ブロックテンプレートに記載します。'
},
{
label: '利用者のアカウント作成をブロック',
name: 'nocreate_template',
checked: Twinkle.block.field_template_options.nocreate_template,
tooltip: '対象範囲が未指定の場合、アカウント作成をブロックしたことを投稿ブロックテンプレートに記載します。'
}
]
});
}
const $previewlink = $('<a id="twinkleblock-preview-link">プレビュー</a>');
$previewlink.off('click').on('click', () => {
Twinkle.block.callback.preview($form[0]);
});
$previewlink.css({cursor: 'pointer'});
fieldTemplateOptions.append({ type: 'div', id: 'blockpreview', label: [$previewlink[0]] });
fieldTemplateOptions.append({ type: 'div', id: 'twinkleblock-previewbox', style: 'display: none' });
}
let oldfield;
if (fieldPreset) {
oldfield = $form.find('fieldset[name="field_preset"]')[0];
oldfield.parentNode.replaceChild(fieldPreset.render(), oldfield);
} else {
$form.find('fieldset[name="field_preset"]').hide();
}
if (fieldBlockOptions) {
oldfield = $form.find('fieldset[name="field_block_options"]')[0];
oldfield.parentNode.replaceChild(fieldBlockOptions.render(), oldfield);
$form.find('fieldset[name="field_64"]').show();
$form.find('[name=pagerestrictions]').select2({
theme: 'default select2-morebits',
width: '100%',
placeholder: '利用者をブロックするページを選択',
language: {
errorLoading: function() {
return '不完全または無効な検索キーワード';
}
},
maximumSelectionLength: 10, // Software limitation [[phab:T202776]]
minimumInputLength: 1, // prevent ajax call when empty
ajax: {
url: mw.util.wikiScript('api'),
dataType: 'json',
delay: 100,
data: function(params) {
const title = mw.Title.newFromText(params.term);
if (!title) {
return;
}
return {
action: 'query',
format: 'json',
list: 'allpages',
apfrom: title.title,
apnamespace: title.namespace,
aplimit: '10'
};
},
processResults: function(data) {
return {
results: data.query.allpages.map((page) => {
const title = mw.Title.newFromText(page.title, page.ns).toText();
return {
id: title,
text: title
};
})
};
}
},
templateSelection: function(choice) {
return $('<a>').text(choice.text).attr({
href: mw.util.getUrl(choice.text),
target: '_blank'
});
}
});
$form.find('[name=namespacerestrictions]').select2({
theme: 'default select2-morebits',
width: '100%',
matcher: Morebits.select2.matchers.wordBeginning,
language: {
searching: Morebits.select2.queryInterceptor
},
templateResult: Morebits.select2.highlightSearchMatches,
placeholder: '利用者をブロックする名前空間を選択'
});
mw.util.addCSS(
// Reduce padding
'.select2-results .select2-results__option { padding-top: 1px; padding-bottom: 1px; }' +
// Adjust font size
'.select2-container .select2-dropdown .select2-results { font-size: 13px; }' +
'.select2-container .selection .select2-selection__rendered { font-size: 13px; }' +
// Remove black border
'.select2-container--default.select2-container--focus .select2-selection--multiple { border: 1px solid #aaa; }' +
// Make the tiny cross larger
'.select2-selection__choice__remove { font-size: 130%; }'
);
} else {
$form.find('fieldset[name="field_block_options"]').hide();
$form.find('fieldset[name="field_64"]').hide();
// Clear select2 options
$form.find('[name=pagerestrictions]').val(null).trigger('change');
$form.find('[name=namespacerestrictions]').val(null).trigger('change');
}
if (fieldTemplateOptions) {
oldfield = $form.find('fieldset[name="field_template_options"]')[0];
oldfield.parentNode.replaceChild(fieldTemplateOptions.render(), oldfield);
e.target.form.root.previewer = new Morebits.wiki.Preview($(e.target.form.root).find('#twinkleblock-previewbox').last()[0]);
} else {
$form.find('fieldset[name="field_template_options"]').hide();
}
// Any block, including ranges
if (Twinkle.block.currentBlockInfo) {
// false for an ip covered by a range or a smaller range within a larger range;
// true for a user, single ip block, or the exact range for a range block
const sameUser = blockedUserName === relevantUserName;
Morebits.Status.init($('div[name="currentblock"] span').last()[0]);
let statusStr = relevantUserName + 'は' + (Twinkle.block.currentBlockInfo.partial === '' ? '部分ブロック' : 'サイト全体でブロック');
// Range blocked
if (Twinkle.block.currentBlockInfo.rangestart !== Twinkle.block.currentBlockInfo.rangeend) {
if (sameUser) {
statusStr = '範囲ブロックとして' + statusStr;
} else {
statusStr = (Morebits.ip.get64(relevantUserName) === blockedUserName ? '/64' : '') + '範囲ブロックの範囲内にある' + statusStr;
// Link to the full range
const $rangeblockloglink = $('<span>').append($('<a target="_blank" href="' + mw.util.getUrl('Special:Log', {action: 'view', page: blockedUserName, type: 'block'}) + '">' + blockedUserName + '</a>)'));
statusStr += ' (' + $rangeblockloglink.html() + ')';
}
}
if (Twinkle.block.currentBlockInfo.expiry === 'infinity') {
statusStr += ' (無期限)';
} else if (new Morebits.Date(Twinkle.block.currentBlockInfo.expiry).isValid()) {
statusStr += ' (期限 ' + new Morebits.Date(Twinkle.block.currentBlockInfo.expiry).calendar('utc') + ')';
}
let infoStr = 'このフォームは';
if (sameUser) {
infoStr += 'ブロックを変更';
if (Twinkle.block.currentBlockInfo.partial === undefined && partialBox) {
infoStr += 'し、部分ブロックに変換';
} else if (Twinkle.block.currentBlockInfo.partial === '' && !partialBox) {
infoStr += 'し、サイト全体のブロックに変換';
}
infoStr += 'します。';
} else {
infoStr += 'さらなる' + (partialBox ? '部分' : '') + 'ブロックを追加します。';
}
Morebits.Status.warn(statusStr, infoStr);
// Default to the current block conditions on intial form generation
Twinkle.block.callback.update_form(e, Twinkle.block.currentBlockInfo);
}
// This is where T146628 really comes into play: a rangeblock will
// only return the correct block log if wgRelevantUserName is the
// exact range, not merely a funtional equivalent
if (Twinkle.block.hasBlockLog) {
const $blockloglink = $('<span>').append($('<a target="_blank" href="' + mw.util.getUrl('Special:Log', {action: 'view', page: relevantUserName, type: 'block'}) + '">ブロック記録</a>)'));
if (!Twinkle.block.currentBlockInfo) {
const lastBlockAction = Twinkle.block.blockLog[0];
if (lastBlockAction.action === 'unblock') {
$blockloglink.append(' (' + new Morebits.Date(lastBlockAction.timestamp).calendar('utc') + ' にブロック解除)');
} else { // block or reblock
$blockloglink.append(' (' + lastBlockAction.params.duration + ', ' + new Morebits.Date(lastBlockAction.params.expiry).calendar('utc') + ' に期限切れ)');
}
}
Morebits.Status.init($('div[name="hasblocklog"] span').last()[0]);
Morebits.Status.warn(Twinkle.block.currentBlockInfo ? '以前のブロック' : 'この' + (Morebits.ip.isRange(relevantUserName) ? '範囲' : '利用者') + 'は過去にブロックされています', $blockloglink[0]);
}
// Make sure all the fields are correct based on initial defaults
if (blockBox) {
Twinkle.block.callback.change_preset(e);
} else if (templateBox) {
Twinkle.block.callback.change_template(e);
}
};
/*
* Keep alphabetized by key name, Twinkle.block.blockGroups establishes
* the order they will appear in the interface
*
* Block preset format, all keys accept only 'true' (omit for false) except where noted:
* <title of block template> : {
* autoblock: <autoblock any IP addresses used (for registered users only)>
* disabletalk: <disable user from editing their own talk page while blocked>
* expiry: <string - expiry timestamp, can include relative times like "5 months", "2 weeks" etc>
* forIPsOnly: <show block option in the interface only if the relevant user is an IP>
* forTempAccountsOnly: <show block option in the interface only if the relevant user is a temporary account>
* forRegisteredOnly: <show block option in the interface only if the relevant user is a temporary account or regular account>
* label: <string - label for the option of the dropdown in the interface (keep brief)>
* noemail: prevent the user from sending email through Special:Emailuser
* watchuser: <watch the user and user talk pages; defaults to true>
* pageParam: <set if the associated block template accepts a page parameter>
* prependReason: <string - prepends the value of 'reason' to the end of the existing reason, namely for when revoking talk page access>
* nocreate: <block account creation from the user's IP (for unregistered users only)>
* nonstandard: <template does not conform to stewardship of WikiProject User Warnings and may not accept standard parameters>
* reason: <string - block rationale, as would appear in the block log,
* and the edit summary for when adding block template, unless 'summary' is set>
* reasonParam: <set if the associated block template accepts a reason parameter>
* sig: <string - set to ~~~~ if block template does not accept "true" as the value, or set null to omit sig param altogether>
* summary: <string - edit summary for when adding block template to user's talk page, if not set, 'reason' is used>
* suppressArticleInSummary: <set to suppress showing the article name in the edit summary, as with attack pages>
* templateName: <string - name of template to use (instead of key name), entry will be omitted from the Templates list.
* (e.g. use another template but with different block options)>
* useInitialOptions: <when preset is chosen, only change given block options, leave others as they were>
*
* WARNING: 'anononly' and 'allowusertalk' are enabled by default.
* To disable, set 'hardblock' and 'disabletalk', respectively
*/
Twinkle.block.blockPresetsInfo = {
'blocked proxy': {
expiry: '1 year',
forIPsOnly: true,
nocreate: true,
nonstandard: true,
hardblock: true,
reason: '{{blocked proxy}}',
sig: null
},
'CheckUser block': {
expiry: '1 week',
forIPsOnly: true,
nocreate: true,
nonstandard: true,
reason: '{{CheckUser block}}',
sig: '~~~~'
},
'checkuserblock-account': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
nonstandard: true,
reason: '{{checkuserblock-account}}',
sig: '~~~~'
},
'checkuserblock-wide': {
forIPsOnly: true,
nocreate: true,
nonstandard: true,
reason: '{{checkuserblock-wide}}',
sig: '~~~~'
},
oversightblock: {
autoblock: true,
expiry: 'infinity',
nocreate: true,
nonstandard: true,
reason: '{{OversightBlock}}',
sig: '~~~~'
},
'range block login': {
reason: '{{range block login|<!-- 割当先を入力 -->}}',
nocreate: true,
nonstandard: true,
forIPsOnly: true,
sig: '~~~~'
},
'3rr': {
autoblock: true,
nocreate: true,
pageParam: true,
reason: '[[WP:3RR|スリー・リバート・ルール]]違反',
summary: 'あなたは[[WP:3RR|スリー・リバート・ルール]]違反の編集を行ったためブロックされました。'
},
// uw-prefixed
'uw-ablock': {
autoblock: true,
forIPsOnly: true,
nocreate: true,
pageParam: true,
reasonParam: true,
summary: 'このIPアドレスは投稿ブロックされました',
suppressArticleInSummary: true
},
'uw-adblock': {
autoblock: true,
nocreate: true,
pageParam: true,
reason: '宣伝・広告投稿はお止めください',
summary: 'あなたは[[WP:SOAP|広告または自己宣伝]]を行ったためブロックされました'
},
'uw-bioblock': {
autoblock: true,
nocreate: true,
pageParam: true,
reason: '[[WP:LIVING|存命人物に関する不適切な編集]]',
summary: 'あなたは[[WP:BLP|存命人物の伝記]]に関する方針に違反する編集を行ったため投稿ブロックされました'
},
'uw-block': {
autoblock: true,
forRegisteredOnly: true,
nocreate: true,
pageParam: true,
reasonParam: true,
summary: 'あなたは投稿ブロックされました',
suppressArticleInSummary: true
},
'uw-blockindef': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
pageParam: true,
reasonParam: true,
summary: 'あなたは無期限投稿ブロックされました',
suppressArticleInSummary: true
},
'uw-blocknotalk': {
disabletalk: true,
pageParam: true,
reasonParam: true,
summary: 'あなたは投稿ブロックされました。また、会話ページの編集をすることはできません',
suppressArticleInSummary: true
},
'uw-botblock': {
expiry: 'infinity',
forRegisteredOnly: true,
pageParam: true,
reason: '[[WP:BRFA|承認]]されていない[[WP:BOT|ボット]]の実行',
summary: '[[WP:BRFA|承認]]されていない[[WP:BOT|ボット]]を実行したようであるため、あなたは投稿ブロックされました'
},
'uw-causeblock': {
expiry: 'infinity',
forRegisteredOnly: true,
reason: '不適切な[[WP:U|利用者名]] <!-- 団体を表す利用者名, ソフトブロック -->',
summary: 'あなたの[[WP:U|利用者名]]は団体または組織、ウェブサイトを代表するアカウントかのような印象を与えるため無期限投稿ブロックされました'
},
'uw-compblock': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
reason: 'パスワード公開',
summary: 'あなたはパスワードを公開したため投稿ブロックされました'
},
'uw-ncblock': {
autoblock: true,
expiry: 'infinity',
nocreate: true,
pageParam: true,
reason: '悪戯投稿',
summary: 'あなたは悪戯投稿を行ったため投稿ブロックされました'
},
'uw-sncblock': {
autoblock: true,
nocreate: true,
pageParam: true,
reason: '悪戯投稿はお止めください',
summary: 'あなたは悪戯投稿を行ったため投稿ブロックされました'
},
'uw-dblock': {
autoblock: true,
nocreate: true,
reason: '内容の無差別な除去',
pageParam: true,
summary: 'あなたは内容の無差別な除去を行ったため投稿ブロックされました'
},
'uw-efblock': {
autoblock: true,
nocreate: true,
reason: '[[特別:不正利用記録|編集フィルター記録]]',
summary: 'あなたは[[WP:EF|編集フィルター]]が繰り返し発動するような破壊的な編集を行ったため投稿ブロックされました'
},
'uw-ewblock': {
autoblock: true,
nocreate: true,
pageParam: true,
reason: '[[WP:EW|過度の編集合戦]]',
summary: 'あなたは[[WP:EW|編集合戦]]を行ったため投稿ブロックされました'
},
'uw-evadeblock': {
expiry: 'infinity',
nocreate: true,
reason: '[[WP:投稿ブロックの方針#投稿ブロック回避|別アカウントなどによるブロック破り]]',
summary: '[[WP:EVASION|投稿ブロックの回避]]に使用されたため投稿ブロックされました'
},
'uw-fblock': {
autoblock: true,
nocreate: true,
reason: '虚偽情報の記載',
summary: 'あなたは虚偽情報の記載を行ったため投稿ブロックされました'
},
'uw-lblock': {
autoblock: true,
expiry: 'infinity',
nocreate: true,
reason: '[[WP:法的な脅迫をしない|法的な脅迫]]',
summary: 'あなたは[[WP:NLT|法的な脅迫]]を行ったため投稿ブロックされました'
},
'uw-oblock': {
autoblock: true,
nocreate: true,
reason: '目的外利用',
summary: 'あなたは目的外利用を行ったため投稿ブロックされました'
},
'uw-pablock': {
autoblock: true,
nocreate: true,
reason: '[[WP:ATTACK|暴言または嫌がらせ]]',
summary: 'あなたは他者に対して[[WP:ATTACK|個人攻撃]]を行ったため投稿ブロックされました'
},
'uw-readtalkblock': {
autoblock: true,
nocreate: true,
reason: '[[Special:Mytalk|会話ページ]]をお読みください',
forRegisteredOnly: true,
summary: 'あなたは会話ページのメッセージをご覧になっていないため投稿ブロックされました'
},
'uw-rjtblock': {
autoblock: true,
nocreate: true,
reason: '対話拒否',
forRegisteredOnly: true,
summary: 'あなたは対話拒否を行ったため投稿ブロックされました'
},
'uw-sblock': {
autoblock: true,
nocreate: true,
reason: 'spam',
summary: 'あなたはspam目的でウィキペディアの利用を行ったため投稿ブロックされました'
},
'uw-sockblock': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
reason: '[[Wikipedia:多重アカウント|多重アカウント]]の不適切な使用',
summary: 'あなたは[[WP:SOCK|多重アカウント]]の不適切な使用を行ったため投稿ブロックされました'
},
'uw-softerblock': {
expiry: 'infinity',
forRegisteredOnly: true,
reason: '不適切な[[WP:U|利用者名]] <!-- 宣伝目的の名前, ソフトブロック -->',
summary: 'あなたの[[WP:U|利用者名]]は団体または組織、ウェブサイトを代表するアカウントかのような印象を与えるため無期限投稿ブロックされました'
},
'uw-spamublock': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
reason: '不適切な[[WP:U|利用者名]] <!-- 宣伝目的の名前, 宣伝目的の編集 -->',
summary: 'あなたのアカウントは宣伝・広告に使用され、また[[WP:U|利用者名の方針]]に違反する利用者名であるため無期限投稿ブロックされました'
},
'uw-spoablock': {
autoblock: true,
disabletalk: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
noemail: true,
reason: '[[WP:SOCK|sockpuppet]]',
summary: 'このアカウントはウィキペディアの方針に違反するために作成された[[WP:SOCK|ソックパペット]]として投稿ブロックされました'
},
'uw-talkrevoked': {
disabletalk: true,
reason: '会話ページの編集権限を剥奪: ブロック中の会話ページの不適切な使用',
prependReason: true,
summary: '会話ページの編集権限は剥奪されました',
useInitialOptions: true
},
'uw-ublock': {
expiry: 'infinity',
forRegisteredOnly: true,
reason: '不適切な[[WP:U|利用者名]] <!-- 不適切な利用者名, ソフトブロック -->',
reasonParam: true,
summary: 'あなたの利用者名は[[WP:U|利用者名の方針]]に違反しているため無期限投稿ブロックされました'
},
'uw-ublock-double': {
expiry: 'infinity',
forRegisteredOnly: true,
reason: '不適切な[[WP:U|利用者名]] <!-- 他の利用者に似ている利用者名, ソフトブロック -->',
summary: 'あなたの[[WP:U|利用者名]]は、既存のウィキペディア利用者の名前と似ているため無期限投稿ブロックされました'
},
'uw-readpoliciesblock': {
autoblock: true,
nocreate: true,
pageParam: true,
reason: '各種方針の熟読期間',
summary: 'あなたは各種方針の熟読期間が必要であるため投稿ブロックされました'
},
'uw-uhblock': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
reason: '不適切な[[WP:U|利用者名]] <!-- 不適切な利用者名, ハードブロック -->',
reasonParam: true,
summary: 'あなたの利用者名は明らかに[[WP:U|利用者名の方針]]に違反しているため無期限投稿ブロックされました'
},
'uw-ublock-wellknown': {
expiry: 'infinity',
forRegisteredOnly: true,
reason: '不適切な[[WP:U|利用者名]] <!-- 有名人を表す利用者名, ソフトブロック -->',
summary: 'あなたの[[WP:U|利用者名]]は存命の著名人の名前と一致しているため無期限投稿ブロックされました'
},
'uw-uhblock-double': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
reason: '不適切な[[WP:U|利用者名]] <!-- 他の利用者のなりすまし, ハードブロック -->',
summary: 'あなたの[[WP:U|利用者名]]は他の既存の利用者になりすましているように見えるため無期限投稿ブロックされました'
},
'uw-upeblock': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
pageParam: true,
reason: '[[wmf:Special:MyLanguage/Terms of use|利用規約]]に違反して[[WP:PAID|有償の寄稿の開示]]に応じない利用者',
summary: 'あなたのアカウントはウィキペディアの方針である[[WP:PAID|有償の寄稿の開示]]に違反してしようされているため無期限投稿ブロックされました'
},
'uw-vaublock': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: true,
pageParam: true,
reason: '不適切な[[WP:U|利用者名]] <!-- 荒らし目的のアカウント -->',
summary: 'あなたのアカウントは荒らしのみに使用されており、また明らかに[[WP:U|利用者名の方針]]に違反する利用者名であるため無期限投稿ブロックされました'
},
'uw-vblock': {
autoblock: true,
expiry: 'infinity',
nocreate: true,
pageParam: true,
reason: '[[Wikipedia:荒らし|荒らし]]',
summary: 'さらなる[[Wikipedia:荒らし|荒らし]]を防止するため、あなたは投稿ブロックされました。'
},
// Begin partial block templates, accessed in Twinkle.block.blockGroupsPartial
'uw-epblock': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: false,
noemail: true,
pageParam: false,
reasonParam: true,
reason: 'メールを用いた嫌がらせ',
summary: 'あなたは嫌がらせを行ったため他の利用者にメールを送ることをブロックされました'
},
'uw-ewpblock': {
autoblock: true,
nocreate: false,
pageParam: false,
reasonParam: true,
reason: '[[WP:EW|編集合戦]]',
summary: 'あなたは[[WP:EW|編集合戦]]を行ったため部分ブロックされました'
},
'uw-pblock': {
autoblock: true,
nocreate: false,
pageParam: false,
reasonParam: true,
summary: 'あなたはウィキペディアの一部から部分ブロックされました'
},
'uw-pblockindef': {
autoblock: true,
expiry: 'infinity',
forRegisteredOnly: true,
nocreate: false,
pageParam: false,
reasonParam: true,
summary: 'あなたはウィキペディアの一部から無期限部分ブロックされました'
}
};
Twinkle.block.transformBlockPresets = function twinkleblockTransformBlockPresets() {
// supply sensible defaults
$.each(Twinkle.block.blockPresetsInfo, (preset, settings) => {
settings.summary = settings.summary || settings.reason;
settings.sig = settings.sig !== undefined ? settings.sig : 'yes';
settings.watchuser = settings.watchuser !== undefined ? settings.watchuser : true;
settings.indefinite = settings.indefinite || Morebits.string.isInfinity(settings.expiry);
if (!Twinkle.block.isRegistered && settings.indefinite) {
settings.expiry = '1 week';
} else {
settings.expiry = settings.expiry || '1 week';
}
Twinkle.block.blockPresetsInfo[preset] = settings;
});
};
// These are the groups of presets and defines the order in which they appear. For each list item:
// label: <string, the description that will be visible in the dropdown>
// value: <string, the key of a preset in blockPresetsInfo>
Twinkle.block.blockGroups = [
{
label: 'よくある理由',
list: [
{ label: '全般ブロック (カスタム理由)', value: 'uw-block' }, // ends up being default for registered users
{ label: '全般ブロック (カスタム理由) - IP', value: 'uw-ablock', selected: true }, // set only when blocking IP
{ label: '全般ブロック (カスタム理由) - 無期限', value: 'uw-blockindef' },
{ label: 'ブロック中の会話ページの不適切な使用', value: 'uw-talkrevoked' },
{ label: '全般ブロック - 会話ページ禁止', value: 'uw-blocknotalk' },
{ label: 'sockpuppet', value: 'uw-spoablock' },
{ label: '荒らし', value: 'uw-vblock' },
{ label: '各種方針の熟読期間', value: 'uw-readpoliciesblock' },
{ label: '会話ページをお読みください', value: 'uw-readtalkblock' }
]
},
{
label: '方針',
list: [
{ label: 'スリー・リバート・ルール違反', value: '3rr' },
{ label: '過度の編集合戦', value: 'uw-ewblock' },
{ label: 'パスワード公開', value: 'uw-compblock' },
{ label: '別アカウントなどによるブロック破り', value: 'uw-evadeblock' },
{ label: '多重アカウントの不適切な使用', value: 'uw-sockblock' },
{ label: '未承認のボット', value: 'uw-botblock' },
{ label: '有償の寄稿の開示に応じない利用者', value: 'uw-upeblock' }
]
},
{
label: '荒らし',
list: [
{ label: '存命人物に関する不適切な編集', value: 'uw-bioblock' },
{ label: '虚偽情報の記載', value: 'uw-fblock' },
{ label: '内容の無差別な除去', value: 'uw-dblock' },
{ label: '宣伝・広告投稿はお止めください', value: 'uw-adblock' },
{ label: '悪戯投稿はお止めください', value: 'uw-sncblock' },
{ label: '悪戯投稿', value: 'uw-ncblock' },
{ label: '暴言または嫌がらせ', value: 'uw-pablock' },
{ label: '対話拒否', value: 'uw-rjtblock' },
{ label: '目的外利用', value: 'uw-oblock' },
{ label: '編集フィルター記録', value: 'uw-efblock' },
{ label: '法的な脅迫', value: 'uw-lblock' },
{ label: 'Spam', value: 'uw-sblock' }
]
},
{
label: '不適切な利用者名',
list: [
{ label: '宣伝目的の名前, ソフトブロック', value: 'uw-softerblock' },
{ label: '宣伝目的の名前, ハードブロック', value: 'uw-spamublock' },
{ label: '不適切な利用者名, ソフトブロック', value: 'uw-ublock' },
{ label: '不適切な利用者名, ハードブロック', value: 'uw-uhblock' },
{ label: 'よく似ている利用者名, ソフトブロック', value: 'uw-ublock-double' },
{ label: 'なりすましの利用者名, ハードブロック', value: 'uw-uhblock-double' },
{ label: '有名人を表す利用者名, ソフトブロック', value: 'uw-ublock-wellknown' },
{ label: '団体を表す利用者名, ソフトブロック', value: 'uw-causeblock' },
{ label: '不適切な利用者名, 荒らし目的のアカウント', value: 'uw-vaublock' }
]
},
{
label: 'テンプレート化された理由',
list: [
{ label: 'blocked proxy', value: 'blocked proxy' },
{ label: 'CheckUser block', value: 'CheckUser block' },
{ label: 'checkuserblock-account', value: 'checkuserblock-account' },
{ label: 'checkuserblock-wide', value: 'checkuserblock-wide' },
{ label: 'oversightblock', value: 'oversightblock' },
{ label: 'range block login', value: 'range block login' } // Only for IP ranges, selected for non-/64 ranges in filtered_block_groups
]
}
];
Twinkle.block.blockGroupsPartial = [
{
label: 'よくある部分ブロック理由',
list: [
{ label: '全般部分ブロック (カスタム理由)', value: 'uw-pblock', selected: true },
{ label: '全般部分ブロック (カスタム理由) - 無期限', value: 'uw-pblockindef' },
{ label: '編集合戦', value: 'uw-ewpblock' },
{ label: 'メールを用いた嫌がらせ', value: 'uw-epblock' }
]
}
];
Twinkle.block.callback.filtered_block_groups = function twinkleblockCallbackFilteredBlockGroups(group, showTemplate) {
return $.map(group, (blockGroup) => {
const list = $.map(blockGroup.list, (blockPreset) => {
switch (blockPreset.value) {
case 'uw-talkrevoked':
if (blockedUserName !== relevantUserName) {
return;
}
break;
case 'range block login':
if (!Morebits.ip.isRange(relevantUserName)) {
return;
}
blockPreset.selected = !Morebits.ip.get64(relevantUserName);
break;
case 'CheckUser block':
case 'checkuserblock-account':
case 'checkuserblock-wide':
if (!Morebits.userIsInGroup('checkuser')) {
return;
}
break;
case 'oversightblock':
if (!Morebits.userIsInGroup('suppress')) {
return;
}
break;
default:
break;
}
const blockSettings = Twinkle.block.blockPresetsInfo[blockPreset.value];
let allowedUserType;
// for regular users and temporary accounts
if (blockSettings.forRegisteredOnly) {
allowedUserType = Twinkle.block.isRegistered;
// for temporary accounts
} else if (blockSettings.forTempAccountsOnly) {
allowedUserType = mw.util.isTemporaryUser(mw.config.get('wgRelevantUserName'));
// for IPs
} else if (blockSettings.forIPsOnly) {
allowedUserType = !Twinkle.block.isRegistered;
} else {
allowedUserType = true;
}
if (!(blockSettings.templateName && showTemplate) && allowedUserType) {
const templateName = blockSettings.templateName || blockPreset.value;
return {
label: (showTemplate ? '{{' + templateName + '}}: ' : '') + blockPreset.label,
value: blockPreset.value,
data: [{
name: 'template-name',
value: templateName
}],
selected: !!blockPreset.selected,
disabled: !!blockPreset.disabled
};
}
});
if (list.length) {
return {
label: blockGroup.label,
list: list
};
}
});
};
Twinkle.block.callback.change_preset = function twinkleblockCallbackChangePreset(e) {
const form = e.target.form, key = form.preset.value;
if (!key) {
return;
}
Twinkle.block.callback.update_form(e, Twinkle.block.blockPresetsInfo[key]);
if (form.template) {
form.template.value = Twinkle.block.blockPresetsInfo[key].templateName || key;
Twinkle.block.callback.change_template(e);
}
};
Twinkle.block.callback.change_expiry = function twinkleblockCallbackChangeExpiry(e) {
const expiry = e.target.form.expiry;
if (e.target.value === 'custom') {
Morebits.QuickForm.setElementVisibility(expiry.parentNode, true);
} else {
Morebits.QuickForm.setElementVisibility(expiry.parentNode, false);
expiry.value = e.target.value;
}
};
Twinkle.block.seeAlsos = [];
Twinkle.block.callback.toggle_see_alsos = function twinkleblockCallbackToggleSeeAlso() {
const joinEnum = function(e) {
return e.join('および');
};
const reason = this.form.reason.value.replace(
new RegExp('( <!--|;) ' + joinEnum(Twinkle.block.seeAlsos) + '参照( -->)?'), ''
);
Twinkle.block.seeAlsos = Twinkle.block.seeAlsos.filter((el) => el !== this.value);
if (this.checked) {
Twinkle.block.seeAlsos.push(this.value);
}
const seeAlsoMessage = joinEnum(Twinkle.block.seeAlsos);
if (!Twinkle.block.seeAlsos.length) {
this.form.reason.value = reason;
} else if (reason.includes('{{')) {
this.form.reason.value = reason + ' <!-- ' + seeAlsoMessage + '参照 -->';
} else {
this.form.reason.value = reason + '; ' + seeAlsoMessage + '参照';
}
};
Twinkle.block.callback.update_form = function twinkleblockCallbackUpdateForm(e, data) {
const form = e.target.form;
let expiry = data.expiry;
// don't override original expiry if useInitialOptions is set
if (!data.useInitialOptions) {
if (Date.parse(expiry)) {
expiry = new Date(expiry).toGMTString();
form.expiry_preset.value = 'custom';
} else {
form.expiry_preset.value = data.expiry || 'custom';
}
form.expiry.value = expiry;
if (form.expiry_preset.value === 'custom') {
Morebits.QuickForm.setElementVisibility(form.expiry.parentNode, true);
} else {
Morebits.QuickForm.setElementVisibility(form.expiry.parentNode, false);
}
}
// boolean-flipped options, more at [[mw:API:Block]]
data.disabletalk = data.disabletalk !== undefined ? data.disabletalk : false;
data.hardblock = data.hardblock !== undefined ? data.hardblock : false;
// disable autoblock if blocking a bot
if (Twinkle.block.userIsBot || /bot\b/i.test(relevantUserName)) {
data.autoblock = false;
}
$(form).find('[name=field_block_options]').find(':checkbox').each((i, el) => {
// don't override original options if useInitialOptions is set
if (data.useInitialOptions && data[el.name] === undefined) {
return;
}
const check = data[el.name] === '' || !!data[el.name];
$(el).prop('checked', check);
});
if (data.prependReason && data.reason) {
form.reason.value = data.reason + '; ' + form.reason.value;
} else {
form.reason.value = data.reason || '';
}
// Clear and/or set any partial page or namespace restrictions
if (form.pagerestrictions) {
const $pageSelect = $(form).find('[name=pagerestrictions]');
const $namespaceSelect = $(form).find('[name=namespacerestrictions]');
// Respect useInitialOptions by clearing data when switching presets
// In practice, this will always clear, since no partial presets use it
if (!data.useInitialOptions) {
$pageSelect.val(null).trigger('change');
$namespaceSelect.val(null).trigger('change');
}
// Add any preset options; in practice, just used for prior block settings
if (data.restrictions) {
if (data.restrictions.pages && !$pageSelect.val().length) {
const pages = data.restrictions.pages.map((pr) => pr.title);
// since page restrictions use an ajax source, we
// short-circuit that and just add a new option
pages.forEach((page) => {
if (!$pageSelect.find("option[value='" + $.escapeSelector(page) + "']").length) {
const newOption = new Option(page, page, true, true);
$pageSelect.append(newOption);
}
});
$pageSelect.val($pageSelect.val().concat(pages)).trigger('change');
}
if (data.restrictions.namespaces) {
$namespaceSelect.val($namespaceSelect.val().concat(data.restrictions.namespaces)).trigger('change');
}
}
}
};
Twinkle.block.callback.change_template = function twinkleblockcallbackChangeTemplate(e) {
const form = e.target.form, value = form.template.value, settings = Twinkle.block.blockPresetsInfo[value];
const blockBox = $(form).find('[name=actiontype][value=block]').is(':checked');
const partialBox = $(form).find('[name=actiontype][value=partial]').is(':checked');
const templateBox = $(form).find('[name=actiontype][value=template]').is(':checked');
// Block form is not present
if (!blockBox) {
if (settings.indefinite || settings.nonstandard) {
if (Twinkle.block.prev_template_expiry === null) {
Twinkle.block.prev_template_expiry = form.template_expiry.value || '';
}
form.template_expiry.parentNode.style.display = 'none';
form.template_expiry.value = 'infinity';
} else if (form.template_expiry.parentNode.style.display === 'none') {
if (Twinkle.block.prev_template_expiry !== null) {
form.template_expiry.value = Twinkle.block.prev_template_expiry;
Twinkle.block.prev_template_expiry = null;
}
form.template_expiry.parentNode.style.display = 'block';
}
if (Twinkle.block.prev_template_expiry) {
form.expiry.value = Twinkle.block.prev_template_expiry;
}
Morebits.QuickForm.setElementVisibility(form.notalk.parentNode, !settings.nonstandard);
// Partial
Morebits.QuickForm.setElementVisibility(form.noemail_template.parentNode, partialBox);
Morebits.QuickForm.setElementVisibility(form.nocreate_template.parentNode, partialBox);
} else if (templateBox) { // Only present if block && template forms both visible
Morebits.QuickForm.setElementVisibility(
form.blank_duration.parentNode,
!settings.indefinite && !settings.nonstandard
);
}
// Only particularly relevant if template form is present
Morebits.QuickForm.setElementVisibility(form.article.parentNode, settings && !!settings.pageParam);
Morebits.QuickForm.setElementVisibility(form.block_reason.parentNode, settings && !!settings.reasonParam);
// Partial block
Morebits.QuickForm.setElementVisibility(form.area.parentNode, partialBox && !blockBox);
form.root.previewer.closePreview();
};
Twinkle.block.prev_template_expiry = null;
Twinkle.block.callback.preview = function twinkleblockcallbackPreview(form) {
const params = {
article: form.article.value,
blank_duration: form.blank_duration ? form.blank_duration.checked : false,
disabletalk: form.disabletalk.checked || (form.notalk ? form.notalk.checked : false),
expiry: form.template_expiry ? form.template_expiry.value : form.expiry.value,
hardblock: Twinkle.block.isRegistered ? form.autoblock.checked : form.hardblock.checked,
indefinite: Morebits.string.isInfinity(form.template_expiry ? form.template_expiry.value : form.expiry.value),
reason: form.block_reason.value,
template: form.template.value,
partial: $(form).find('[name=actiontype][value=partial]').is(':checked'),
pagerestrictions: $(form.pagerestrictions).val() || [],
namespacerestrictions: $(form.namespacerestrictions).val() || [],
noemail: form.noemail.checked || (form.noemail_template ? form.noemail_template.checked : false),
nocreate: form.nocreate.checked || (form.nocreate_template ? form.nocreate_template.checked : false),
area: form.area.value
};
const templateText = Twinkle.block.callback.getBlockNoticeWikitext(params);
form.previewer.beginRender(templateText, 'User_talk:' + relevantUserName); // Force wikitext/correct username
};
Twinkle.block.callback.evaluate = function twinkleblockCallbackEvaluate(e) {
const $form = $(e.target),
toBlock = $form.find('[name=actiontype][value=block]').is(':checked'),
toWarn = $form.find('[name=actiontype][value=template]').is(':checked'),
toPartial = $form.find('[name=actiontype][value=partial]').is(':checked');
let blockoptions = {}, templateoptions = {};
Twinkle.block.callback.saveFieldset($form.find('[name=field_block_options]'));
Twinkle.block.callback.saveFieldset($form.find('[name=field_template_options]'));
blockoptions = Twinkle.block.field_block_options;
templateoptions = Twinkle.block.field_template_options;
templateoptions.disabletalk = !!(templateoptions.disabletalk || blockoptions.disabletalk);
templateoptions.hardblock = !!blockoptions.hardblock;
delete blockoptions.expiry_preset; // remove extraneous
// Partial API requires this to be gone, not false or 0
if (toPartial) {
blockoptions.partial = templateoptions.partial = true;
}
templateoptions.pagerestrictions = $form.find('[name=pagerestrictions]').val() || [];
templateoptions.namespacerestrictions = $form.find('[name=namespacerestrictions]').val() || [];
// Format for API here rather than in saveFieldset
blockoptions.pagerestrictions = templateoptions.pagerestrictions.join('|');
blockoptions.namespacerestrictions = templateoptions.namespacerestrictions.join('|');
// use block settings as warn options where not supplied
templateoptions.summary = templateoptions.summary || blockoptions.reason;
templateoptions.expiry = templateoptions.template_expiry || blockoptions.expiry;
if (toBlock) {
if (blockoptions.partial) {
if (blockoptions.disabletalk && !blockoptions.namespacerestrictions.includes('3')) {
return alert('利用者会話名前空間の編集を制限しない限り、部分ブロックでは会話ページの編集を禁止することはできません!');
}
if (!blockoptions.namespacerestrictions && !blockoptions.pagerestrictions) {
if (!blockoptions.noemail && !blockoptions.nocreate) { // Blank entries technically allowed [[phab:T208645]]
return alert('ページや名前空間が選択されておらず、メール送信やアカウント作成の制限を選択されていません; 少なくとも一つオプションを選択してください!');
} else if ((templateoptions.template !== 'uw-epblock' || $form.find('select[name="preset"]').val() !== 'uw-epblock') &&
// Don't require confirmation if email harassment defaults are set
!confirm('You are about to block with no restrictions on page or namespace editing, are you sure you want to proceed?')) {
return;
}
}
}
if (!blockoptions.expiry) {
return alert('有効期限を入力してください!');
} else if (Morebits.string.isInfinity(blockoptions.expiry) && !Twinkle.block.isRegistered) {
return alert('IPアドレスを無期限ブロックすることはできません!');
}
if (!blockoptions.reason) {
return alert('ブロック理由を入力してください!');
}
Morebits.SimpleWindow.setButtonsEnabled(false);
Morebits.Status.init(e.target);
const statusElement = new Morebits.Status('ブロックの実行');
blockoptions.action = 'block';
blockoptions.user = relevantUserName;
// boolean-flipped options
blockoptions.anononly = blockoptions.hardblock ? undefined : true;
blockoptions.allowusertalk = blockoptions.disabletalk ? undefined : true;
/*
Check if block status changed while processing the form.
There's a lot to consider here. list=blocks provides the
current block status, but there are at least two issues with
relying on it. First, the id doesn't update on a reblock,
meaning the individual parameters need to be compared. This
can be done roughly with JSON.stringify - we can thankfully
rely on order from the server, although sorting would be
fine if not - but falsey values are problematic and is
non-ideal. More importantly, list=blocks won't indicate if a
non-blocked user is blocked then unblocked. This should be
exceedingy rare, but regardless, we thus need to check
list=logevents, which has a nicely updating logid
parameter. We can't rely just on that, though, since it
doesn't account for blocks that have expired on their own.
As such, we use both. Using some ternaries, the logid
variables are false if there's no logevents, so if they
aren't equal we defintely have a changed entry (send
confirmation). If they are equal, then either the user was
never blocked (the block statuses will be equal, no
confirmation) or there's no new block, in which case either
a block expired (different statuses, confirmation) or the
same block is still active (same status, no confirmation).
*/
const query = {
format: 'json',
action: 'query',
list: 'blocks|logevents',
letype: 'block',
lelimit: 1,
letitle: 'User:' + blockoptions.user
};
// bkusers doesn't catch single IPs blocked as part of a range block
if (mw.util.isIPAddress(blockoptions.user, true)) {
query.bkip = blockoptions.user;
} else {
query.bkusers = blockoptions.user;
}
api.get(query).then((data) => {
let block = data.query.blocks[0];
// As with the initial data fetch, if an IP is blocked
// *and* rangeblocked, this would only grab whichever
// block is more recent, which would likely mean a
// mismatch. However, if the rangeblock is updated
// while filling out the form, this won't detect that,
// but that's probably fine.
if (data.query.blocks.length > 1 && block.user !== relevantUserName) {
block = data.query.blocks[1];
}
const logevents = data.query.logevents[0];
const logid = data.query.logevents.length ? logevents.logid : false;
if (logid !== Twinkle.block.blockLogId || !!block !== !!Twinkle.block.currentBlockInfo) {
let message = 'The block status of ' + blockoptions.user + ' has changed. ';
if (block) {
message += 'New status: ';
} else {
message += 'Last entry: ';
}
let logExpiry = '';
if (logevents.params.duration) {
if (logevents.params.duration === 'infinity') {
logExpiry = 'indefinitely';
} else {
const expiryDate = new Morebits.Date(logevents.params.expiry);
logExpiry += (expiryDate.isBefore(new Date()) ? ', expired ' : ' until ') + expiryDate.calendar();
}
} else { // no duration, action=unblock, just show timestamp
logExpiry = ' ' + new Morebits.Date(logevents.timestamp).calendar();
}
message += Morebits.string.toUpperCaseFirstChar(logevents.action) + 'ed by ' + logevents.user + logExpiry +
' for "' + logevents.comment + '". Do you want to override with your settings?';
if (!confirm(message)) {
Morebits.Status.info('ブロックの実行', '利用者によって中断');
return;
}
blockoptions.reblock = 1; // Writing over a block will fail otherwise
}
// execute block
blockoptions.token = mw.user.tokens.get('csrfToken');
const mbApi = new Morebits.wiki.Api('ブロックの実行', blockoptions, (() => {
statusElement.info('完了');
if (toWarn) {
Twinkle.block.callback.issue_template(templateoptions);
}
}));
mbApi.post();
});
} else if (toWarn) {
Morebits.SimpleWindow.setButtonsEnabled(false);
Morebits.Status.init(e.target);
Twinkle.block.callback.issue_template(templateoptions);
} else {
return alert('Please give Twinkle something to do!');
}
};
Twinkle.block.callback.issue_template = function twinkleblockCallbackIssueTemplate(formData) {
// Use wgRelevantUserName to ensure the block template goes to a single IP and not to the
// "talk page" of an IP range (which does not exist)
const userTalkPage = 'User_talk:' + mw.config.get('wgRelevantUserName');
const params = Twinkle.block.combineFormDataAndFieldTemplateOptions(
formData,
Twinkle.block.blockPresetsInfo[formData.template],
Twinkle.block.field_template_options.block_reason,
Twinkle.block.field_template_options.notalk,
Twinkle.block.field_template_options.noemail_template,
Twinkle.block.field_template_options.nocreate_template
);
Morebits.wiki.actionCompleted.redirect = userTalkPage;
Morebits.wiki.actionCompleted.notice = 'Actions complete, loading user talk page in a few seconds';
const wikipediaPage = new Morebits.wiki.Page(userTalkPage, 'User talk page modification');
wikipediaPage.setCallbackParameters(params);
wikipediaPage.load(Twinkle.block.callback.main);
};
Twinkle.block.combineFormDataAndFieldTemplateOptions = function(formData, messageData, reason, disabletalk, noemail, nocreate) {
return $.extend(formData, {
messageData: messageData,
reason: reason,
disabletalk: disabletalk,
noemail: noemail,
nocreate: nocreate
});
};
Twinkle.block.callback.getBlockNoticeWikitext = function(params) {
let text = '{{';
const settings = Twinkle.block.blockPresetsInfo[params.template];
if (!settings.nonstandard) {
text += 'subst:' + params.template;
if (params.article && settings.pageParam) {
text += '|page=' + params.article;
}
if (!/te?mp|^\s*$|min/.exec(params.expiry)) {
if (params.indefinite) {
text += '|indef=yes';
} else if (!params.blank_duration && !new Morebits.Date(params.expiry).isValid()) {
// Block template wants a duration, not date
text += '|time=' + params.expiry;
}
}
if (!Twinkle.block.isRegistered && !params.hardblock) {
text += '|anon=yes';
}
if (params.reason) {
text += '|reason=' + params.reason;
}
if (params.disabletalk) {
text += '|notalk=yes';
}
// Currently, all partial block templates are "standard"
// Building the template, however, takes a fair bit of logic
if (params.partial) {
if (params.pagerestrictions.length || params.namespacerestrictions.length) {
const makeSentence = function(array) {
if (array.length < 3) {
return array.join(' and ');
}
const last = array.pop();
return array.join(', ') + ', and ' + last;
};
text += '|area=' + (params.indefinite ? 'certain ' : 'from certain ');
if (params.pagerestrictions.length) {
text += 'pages (' + makeSentence(params.pagerestrictions.map((p) => '[[:' + p + ']]'));
text += params.namespacerestrictions.length ? ') and certain ' : ')';
}
if (params.namespacerestrictions.length) {
// 1 => Talk, 2 => User, etc.
const namespaceNames = params.namespacerestrictions.map((id) => menuFormattedNamespaces[id]);
text += '[[Wikipedia:Namespace|namespaces]] (' + makeSentence(namespaceNames) + ')';
}
} else if (params.area) {
text += '|area=' + params.area;
} else {
if (params.noemail) {
text += '|email=yes';
}
if (params.nocreate) {
text += '|accountcreate=yes';
}
}
}
} else {
text += params.template;
}
if (settings.sig) {
text += '|sig=' + settings.sig;
}
return text + '}}';
};
Twinkle.block.callback.main = function twinkleblockcallbackMain(pageobj) {
const params = pageobj.getCallbackParameters(),
date = new Morebits.Date(pageobj.getLoadTime()),
messageData = params.messageData;
let text;
params.indefinite = Morebits.string.isInfinity(params.expiry);
if (Twinkle.getPref('blankTalkpageOnIndefBlock') && params.template !== 'uw-lblock' && params.indefinite) {
Morebits.Status.info('Info', 'Blanking talk page per preferences and creating a new talk page section for this month');
text = date.monthHeader() + '\n';
} else {
text = pageobj.getPageText();
const dateHeaderRegex = date.monthHeaderRegex();
let dateHeaderRegexLast, dateHeaderRegexResult;
while ((dateHeaderRegexLast = dateHeaderRegex.exec(text)) !== null) {
dateHeaderRegexResult = dateHeaderRegexLast;
}
// If dateHeaderRegexResult is null then lastHeaderIndex is never checked. If it is not null but
// \n== is not found, then the date header must be at the very start of the page. lastIndexOf
// returns -1 in this case, so lastHeaderIndex gets set to 0 as desired.
const lastHeaderIndex = text.lastIndexOf('\n==') + 1;
if (text.length > 0) {
text += '\n\n';
}
if (!dateHeaderRegexResult || dateHeaderRegexResult.index !== lastHeaderIndex) {
Morebits.Status.info('Info', 'Will create a new talk page section for this month, as none was found');
text += date.monthHeader() + '\n';
}
}
params.expiry = typeof params.template_expiry !== 'undefined' ? params.template_expiry : params.expiry;
text += Twinkle.block.callback.getBlockNoticeWikitext(params);
// build the edit summary
let summary = messageData.summary;
if (messageData.suppressArticleInSummary !== true && params.article) {
summary += ' on [[:' + params.article + ']]';
}
summary += '.';
pageobj.setPageText(text);
pageobj.setEditSummary(summary);
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.setWatchlist(Twinkle.getPref('watchWarnings'));
pageobj.save();
};
Twinkle.addInitCallback(Twinkle.block, 'block');
}());
// </nowiki>
70smbptbp0l3hjnfajganf0pv0g45s1
769576
769546
2026-10-22T02:30:51Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at d9cd3461baca43ac99ac2c24fb76a3aae6ddada9: minify
769576
javascript
text/javascript
// <nowiki>
(function(){const api=new mw.Api;let relevantUserName,blockedUserName,blockWindow;const menuFormattedNamespaces=$.extend({},mw.config.get("wgFormattedNamespaces"));menuFormattedNamespaces[0]="(Article)";Twinkle.block=function twinkleblock(){relevantUserName=mw.config.get("wgRelevantUserName");if(Morebits.userIsSysop&&relevantUserName&&(!Morebits.ip.isRange(relevantUserName)||Morebits.ip.validCIDR(relevantUserName))){Twinkle.addPortletLink(Twinkle.block.callback,"Block","tw-block","利用者をブロック")}};Twinkle.block.callback=function twinkleblockCallback(){if(relevantUserName===mw.config.get("wgUserName")&&!confirm("自分自身をブロックしようとしています!続行しますか?")){return}Twinkle.block.currentBlockInfo=undefined;Twinkle.block.field_block_options={};Twinkle.block.field_template_options={};blockWindow=new Morebits.SimpleWindow(650,530);blockWindow.setTitle(relevantUserName+"をブロックまたは投稿ブロックテンプレートを送信する");blockWindow.setScriptName("Twinkle");blockWindow.addFooterLink("ブロックを解除","Special:Unblock/"+relevantUserName);blockWindow.addFooterLink("投稿ブロックテンプレート","Wikipedia:主要なテンプレート/利用者‐会話名前空間#投稿ブロックテンプレート");blockWindow.addFooterLink("投稿ブロックの方針","WP:BLOCK");blockWindow.addFooterLink("ブロック設定","User:Syunsyunminmin/Twinkle/Preferences#block");blockWindow.addFooterLink("Twinkle help",":en:WP:TW/DOC#block");blockWindow.addFooterLink("フィードバック","User talk:Syunsyunminmin/Twinkle");const form=new Morebits.QuickForm(Twinkle.block.callback.evaluate);const actionfield=form.append({type:"field",label:"操作の種類"});actionfield.append({type:"checkbox",name:"actiontype",event:Twinkle.block.callback.change_action,list:[{label:"利用者をブロック",value:"block",tooltip:"指定されたオプションで対象利用者をブロックします。部分ブロックのチェックが外れている場合、サイト全体のブロックとなります。",checked:true},{label:"部分ブロック",value:"partial",tooltip:"部分ブロックと部分ブロックテンプレートを有効にする。",checked:Twinkle.getPref("defaultToPartialBlocks")},{label:"利用者会話ページに投稿ブロックテンプレートを追加",value:"template",tooltip:"(※未対応)ブロックした管理者がブロックテンプレートを送信するのを忘れた場合、あるいは、テンプレートを送信せずに利用者をブロックした場合、これを使って適切なテンプレートを送信することができます。部分ブロックテンプレートは部分ブロックボックスにチェックを入れてください。",checked:false,disabled:Morebits.ip.isRange(relevantUserName)}]});const sixtyFour=Morebits.ip.get64(mw.config.get("wgRelevantUserName"));if(sixtyFour&&sixtyFour!==mw.config.get("wgRelevantUserName")){const block64field=form.append({type:"field",label:"/64範囲ブロックに変換",name:"field_64"});block64field.append({type:"div",style:"margin-bottom: 0.5em",label:["通常は、より良いとは言わないまでも、",$.parseHTML('<a target="_blank" href="'+mw.util.getUrl(":en:WP:/64")+'">/64の範囲ブロック</a>')[0],"するだけで問題ないです (",$.parseHTML('<a target="_blank" href="'+mw.util.getUrl("Special:Contributions/"+sixtyFour)+'">'+sixtyFour+"</a>)")[0],")."]});block64field.append({type:"checkbox",name:"block64",event:Twinkle.block.callback.change_block64,list:[{checked:Twinkle.getPref("defaultToBlock64"),label:"/64を代わりにブロックする",value:"block64",tooltip:Morebits.ip.isRange(mw.config.get("wgRelevantUserName"))?"Will eschew leaving a template.":"Any template issued will go to the original IP: "+mw.config.get("wgRelevantUserName")}]})}form.append({type:"field",label:"プリセット",name:"field_preset"});form.append({type:"field",label:"テンプレートオプション",name:"field_template_options"});form.append({type:"field",label:"ブロックオプション",name:"field_block_options"});form.append({type:"submit"});const result=form.render();blockWindow.setContent(result);blockWindow.display();result.root=result;Twinkle.block.fetchUserInfo(()=>{if(blockedUserName===relevantUserName){$(result).find("[name=actiontype][value=partial]").prop("checked",Twinkle.block.currentBlockInfo.partial==="")}Twinkle.block.transformBlockPresets();const evt=document.createEvent("Event");evt.initEvent("change",true,true);if(result.block64&&result.block64.checked){result.block64.dispatchEvent(evt)}else{result.actiontype[0].dispatchEvent(evt)}})};Twinkle.block.fetchedData={};Twinkle.block.processUserInfo=function twinkleblockProcessUserInfo(data,fn){let blockinfo=data.query.blocks[0];if(blockinfo&&data.query.blocks.length>1){blockWindow.$dialog.find(".morebits-dialog-buttons").empty();Morebits.Status.init(blockWindow.content.querySelector("form"));Morebits.Status.warn(`この対象には有効なブロックが${data.query.blocks.length}件あります`,`Twinkleは複数ブロックに対応していません。[[Special:Block/${relevantUserName}]]を使用してください。`);return}const userinfo=data.query.users[0];if(data.query.blocks.length>1&&blockinfo.user!==relevantUserName){blockinfo=data.query.blocks[1]}Twinkle.block.fetchedData[userinfo.name]=data;Twinkle.block.isRegistered=!!userinfo.userid;if(Twinkle.block.isRegistered){Twinkle.block.userIsBot=!!userinfo.groupmemberships&&userinfo.groupmemberships.map(e=>e.group).includes("bot")}else{Twinkle.block.userIsBot=false}if(blockinfo){blockinfo.disabletalk=blockinfo.allowusertalk===undefined;blockinfo.hardblock=blockinfo.anononly===undefined}Twinkle.block.currentBlockInfo=blockinfo;blockedUserName=Twinkle.block.currentBlockInfo&&Twinkle.block.currentBlockInfo.user;const unblockLink=blockWindow.$dialog.find(".morebits-dialog-footerlinks a")[0];if(blockedUserName!==relevantUserName){unblockLink.hidden=true;unblockLink.nextSibling.hidden=true}else{unblockLink.hidden=false;unblockLink.nextSibling.hidden=false}Twinkle.block.hasBlockLog=!!data.query.logevents.length;Twinkle.block.blockLog=Twinkle.block.hasBlockLog&&data.query.logevents;Twinkle.block.blockLogId=Twinkle.block.hasBlockLog?data.query.logevents[0].logid:false;if(typeof fn==="function"){return fn()}};Twinkle.block.fetchUserInfo=function twinkleblockFetchUserInfo(fn){const query={format:"json",action:"query",list:"blocks|users|logevents",letype:"block",lelimit:1,letitle:"User:"+relevantUserName,bkprop:"expiry|reason|flags|restrictions|range|user",ususers:relevantUserName};if(mw.util.isIPAddress(relevantUserName,true)){query.bkip=relevantUserName}else{query.bkusers=relevantUserName;query.usprop="groupmemberships"}api.get(query).then(data=>{Twinkle.block.processUserInfo(data,fn)},msg=>{Morebits.Status.init($('div[name="currentblock"] span').last()[0]);Morebits.Status.warn("利用者情報の取得エラー",msg)})};Twinkle.block.callback.saveFieldset=function twinkleblockCallbacksaveFieldset(fieldset){Twinkle.block[$(fieldset).prop("name")]={};$(fieldset).serializeArray().forEach(el=>{Twinkle.block[$(fieldset).prop("name")][el.name]=el.value})};Twinkle.block.callback.change_block64=function twinkleblockCallbackChangeBlock64(e){const $form=$(e.target.form),$block64=$form.find("[name=block64]"),dialog=$form.closest(".morebits-dialog")[0];const priorName=relevantUserName;if($block64.is(":checked")){relevantUserName=Morebits.ip.get64(mw.config.get("wgRelevantUserName"))}else{relevantUserName=mw.config.get("wgRelevantUserName")}const originalIsRange=Morebits.ip.isRange(mw.config.get("wgRelevantUserName"));$form.find("[name=actiontype][value=template]").prop("disabled",originalIsRange);const regenerateForm=function(){const titleBar=dialog.querySelector(".morebits-dialog-title").firstChild.nextSibling;titleBar.nodeValue=titleBar.nodeValue.replace(priorName,relevantUserName);const unblockLink=dialog.querySelector(".morebits-dialog-footerlinks a");unblockLink.href=unblockLink.href.replace(priorName,relevantUserName);unblockLink.title=unblockLink.title.replace(priorName,relevantUserName);$form.find("[name=actiontype][value=partial]").prop("checked",Twinkle.getPref("defaultToPartialBlocks"));if(blockedUserName===relevantUserName){$form.find("[name=actiontype][value=partial]").prop("checked",Twinkle.block.currentBlockInfo.partial==="")}Twinkle.block.callback.change_action(e)};if(Twinkle.block.fetchedData[relevantUserName]){Twinkle.block.processUserInfo(Twinkle.block.fetchedData[relevantUserName],regenerateForm)}else{Twinkle.block.fetchUserInfo(regenerateForm)}};Twinkle.block.callback.change_action=function twinkleblockCallbackChangeAction(e){let fieldPreset,fieldTemplateOptions,fieldBlockOptions;const $form=$(e.target.form);const blockBox=$form.find("[name=actiontype][value=block]").is(":checked");const templateBox=$form.find("[name=actiontype][value=template]").is(":checked");const $partial=$form.find("[name=actiontype][value=partial]");const partialBox=$partial.is(":checked");let blockGroup=partialBox?Twinkle.block.blockGroupsPartial:Twinkle.block.blockGroups;$partial.prop("disabled",!blockBox&&!templateBox);const prior={label:"前ブロック"};if(blockedUserName===relevantUserName){Twinkle.block.blockPresetsInfo.prior=$.extend({watchuser:true},Twinkle.block.currentBlockInfo);prior.list=[{label:"前のブロック設定",value:"prior",selected:true}];if(!blockGroup.some(bg=>bg.label===prior.label)){blockGroup.push(prior)}if(partialBox){Twinkle.block.blockPresetsInfo.prior.templateName=Morebits.string.isInfinity(Twinkle.block.currentBlockInfo.expiry)?"uw-pblockindef":"uw-pblock"}else{if(!Twinkle.block.isRegistered){Twinkle.block.blockPresetsInfo.prior.templateName="uw-ablock"}else{Twinkle.block.blockPresetsInfo.prior.templateName=Morebits.string.isInfinity(Twinkle.block.currentBlockInfo.expiry)?"uw-blockindef":"uw-block"}}}else{blockGroup=blockGroup.filter(bg=>bg.label!==prior.label)}Twinkle.block.callback.saveFieldset($("[name=field_block_options]"));Twinkle.block.callback.saveFieldset($("[name=field_template_options]"));if(blockBox){fieldPreset=new Morebits.QuickForm.Element({type:"field",label:"プリセット",name:"field_preset"});fieldPreset.append({type:"select",name:"preset",label:"プリセットを選択:",event:Twinkle.block.callback.change_preset,list:Twinkle.block.callback.filtered_block_groups(blockGroup)});fieldBlockOptions=new Morebits.QuickForm.Element({type:"field",label:"ブロックオプション",name:"field_block_options"});fieldBlockOptions.append({type:"div",name:"currentblock",label:" "});fieldBlockOptions.append({type:"div",name:"hasblocklog",label:" "});fieldBlockOptions.append({type:"select",name:"expiry_preset",label:"期限:",event:Twinkle.block.callback.change_expiry,list:[{label:"その他",value:"custom",selected:true},{label:"無期限",value:"infinity"},{label:"3時間",value:"3 hours"},{label:"12時間",value:"12 hours"},{label:"24時間",value:"24 hours"},{label:"31時間",value:"31 hours"},{label:"36時間",value:"36 hours"},{label:"48時間",value:"48 hours"},{label:"60時間",value:"60 hours"},{label:"72時間",value:"72 hours"},{label:"1週間",value:"1 week"},{label:"2週間",value:"2 weeks"},{label:"1ヶ月間",value:"1 month"},{label:"3ヶ月間",value:"3 months"},{label:"6ヶ月間",value:"6 months"},{label:"1年間",value:"1 year"},{label:"2年間",value:"2 years"},{label:"3年間",value:"3 years"}]});fieldBlockOptions.append({type:"input",name:"expiry",label:"その他の期限",tooltip:'"1 minute" (1分) や "19 days" (19日) のような相対的な時間や、"yyyymmddhhmm"のような絶対的なタイムスタンプを使用することができます。 (例: "200602011405"は2006年2月1日 14:05 UTCを意味します)',value:Twinkle.block.field_block_options.expiry||Twinkle.block.field_template_options.template_expiry});if(partialBox){fieldBlockOptions.append({type:"select",multiple:true,name:"pagerestrictions",label:"編集をブロックする特定のページ",value:"",tooltip:"最大で10ページ"});const ns=fieldBlockOptions.append({type:"select",multiple:true,name:"namespacerestrictions",label:"名前空間のブロック",value:"",tooltip:"これらの名前空間の編集をブロックする"});$.each(menuFormattedNamespaces,(number,name)=>{if(number>=0&&number<830){ns.append({type:"option",label:name,value:number})}})}const blockoptions=[{checked:Twinkle.block.field_block_options.nocreate,label:"アカウント作成をブロック",name:"nocreate",value:"1"}];if(Twinkle.block.isRegistered&&!mw.util.isTemporaryUser(mw.config.get("wgRelevantUserName"))){blockoptions.push({checked:Twinkle.block.field_block_options.noemail,label:"利用者のメール送信をブロック",name:"noemail",value:"1"})}blockoptions.push({checked:Twinkle.block.field_block_options.disabletalk,label:"ブロックされている間、この利用者が自分の会話ページを編集できないようにする。",name:"disabletalk",value:"1",tooltip:partialBox?"部分ブロックを行う場合、利用者会話名前空間の編集をブロックしない限り、このチェックは外したままにしておかなければなりません。":""});if(Twinkle.block.isRegistered){blockoptions.push({checked:Twinkle.block.field_block_options.autoblock,label:"使用されたIPアドレスを自動ブロック",name:"autoblock",value:"1"})}else{blockoptions.push({checked:Twinkle.block.field_block_options.hardblock,label:"ログイン利用者がこのIPアドレスを使用するのをブロックする (ハードブロック)",name:"hardblock",value:"1"})}blockoptions.push({checked:Twinkle.block.field_block_options.watchuser,label:"利用者(会話)ページをウォッチする",name:"watchuser",value:"1"});fieldBlockOptions.append({type:"checkbox",name:"blockoptions",list:blockoptions});fieldBlockOptions.append({type:"textarea",label:"理由 (ブロック記録用):",name:"reason",tooltip:"既定のブロックメッセージに追加して役立つ詳細を入力してください",value:Twinkle.block.field_block_options.reason});fieldBlockOptions.append({type:"div",name:"filerlog_label",label:"参照:",style:"display:inline-block;font-style:normal !important",tooltip:"フィルター記録、削除された投稿、関連する一時アカウントがブロックの判断に関わったことを示す参照情報を追加します。"});fieldBlockOptions.append({type:"checkbox",name:"filter_see_also",event:Twinkle.block.callback.toggle_see_alsos,style:"display:inline-block; margin-right:5px",list:[{label:"フィルター記録",checked:false,value:"フィルター記録"}]});fieldBlockOptions.append({type:"checkbox",name:"deleted_see_also",event:Twinkle.block.callback.toggle_see_alsos,style:"display:inline-block; margin-right:5px",list:[{label:"削除された投稿",checked:false,value:"削除された投稿記録"}]});if(mw.util.isTemporaryUser(mw.config.get("wgRelevantUserName"))){fieldBlockOptions.append({type:"checkbox",name:"related_see_also",event:Twinkle.block.callback.toggle_see_alsos,style:"display:inline-block",list:[{label:"関連する一時アカウント",checked:false,value:"関連する一時アカウント"}]})}if(blockedUserName===relevantUserName){fieldBlockOptions.append({type:"hidden",name:"reblock",value:"1"})}}if(templateBox){fieldTemplateOptions=new Morebits.QuickForm.Element({type:"field",label:"テンプレートオプション",name:"field_template_options"});fieldTemplateOptions.append({type:"select",name:"template",label:"会話ページのテンプレートを選択:",event:Twinkle.block.callback.change_template,list:Twinkle.block.callback.filtered_block_groups(blockGroup,true),value:Twinkle.block.field_template_options.template});fieldTemplateOptions.append({type:"input",name:"article",label:"リンクするページ",value:"",tooltip:"そのページが破壊行為の主な対象であった場合、通知内でリンクされることがあります。リンクするページがない場合は空欄にしてください。"});fieldTemplateOptions.append({type:"input",name:"area",label:"ブロックされた場所",value:"",tooltip:"オプションで、利用者が編集をブロックされたページまたは名前空間の説明。"});if(!blockBox){fieldTemplateOptions.append({type:"input",name:"template_expiry",label:"ブロック期間:",value:"",tooltip:"ブロックする期間。例えば24 hours、2 weeks、indefiniteなど"})}fieldTemplateOptions.append({type:"input",name:"block_reason",label:'"あなたがブロックされたのは..."',tooltip:"デフォルトの一般的な理由を置き換えるオプションの理由。全般の投稿ブロックテンプレートでのみ利用可能です。",value:Twinkle.block.field_template_options.block_reason});if(blockBox){fieldTemplateOptions.append({type:"checkbox",name:"blank_duration",list:[{label:"テンプレートに有効期限を含めない",checked:Twinkle.block.field_template_options.blank_duration,tooltip:'期間を含める代わりに、ブロックテンプレートは「あなたは一時的にブロックされました」と表示します..."'}]})}else{fieldTemplateOptions.append({type:"checkbox",list:[{label:"会話ページの編集を禁止",name:"notalk",checked:Twinkle.block.field_template_options.notalk,tooltip:"会話ページの編集権限を剥奪したことを投稿ブロックテンプレートに記載します。"},{label:"利用者のメール送信をブロック",name:"noemail_template",checked:Twinkle.block.field_template_options.noemail_template,tooltip:"対象範囲が未指定の場合、メール送信をブロックしたことを投稿ブロックテンプレートに記載します。"},{label:"利用者のアカウント作成をブロック",name:"nocreate_template",checked:Twinkle.block.field_template_options.nocreate_template,tooltip:"対象範囲が未指定の場合、アカウント作成をブロックしたことを投稿ブロックテンプレートに記載します。"}]})}const $previewlink=$('<a id="twinkleblock-preview-link">プレビュー</a>');$previewlink.off("click").on("click",()=>{Twinkle.block.callback.preview($form[0])});$previewlink.css({cursor:"pointer"});fieldTemplateOptions.append({type:"div",id:"blockpreview",label:[$previewlink[0]]});fieldTemplateOptions.append({type:"div",id:"twinkleblock-previewbox",style:"display: none"})}let oldfield;if(fieldPreset){oldfield=$form.find('fieldset[name="field_preset"]')[0];oldfield.parentNode.replaceChild(fieldPreset.render(),oldfield)}else{$form.find('fieldset[name="field_preset"]').hide()}if(fieldBlockOptions){oldfield=$form.find('fieldset[name="field_block_options"]')[0];oldfield.parentNode.replaceChild(fieldBlockOptions.render(),oldfield);$form.find('fieldset[name="field_64"]').show();$form.find("[name=pagerestrictions]").select2({theme:"default select2-morebits",width:"100%",placeholder:"利用者をブロックするページを選択",language:{errorLoading:function(){return"不完全または無効な検索キーワード"}},maximumSelectionLength:10,minimumInputLength:1,ajax:{url:mw.util.wikiScript("api"),dataType:"json",delay:100,data:function(params){const title=mw.Title.newFromText(params.term);if(!title){return}return{action:"query",format:"json",list:"allpages",apfrom:title.title,apnamespace:title.namespace,aplimit:"10"}},processResults:function(data){return{results:data.query.allpages.map(page=>{const title=mw.Title.newFromText(page.title,page.ns).toText();return{id:title,text:title}})}}},templateSelection:function(choice){return $("<a>").text(choice.text).attr({href:mw.util.getUrl(choice.text),target:"_blank"})}});$form.find("[name=namespacerestrictions]").select2({theme:"default select2-morebits",width:"100%",matcher:Morebits.select2.matchers.wordBeginning,language:{searching:Morebits.select2.queryInterceptor},templateResult:Morebits.select2.highlightSearchMatches,placeholder:"利用者をブロックする名前空間を選択"});mw.util.addCSS(".select2-results .select2-results__option { padding-top: 1px; padding-bottom: 1px; }"+".select2-container .select2-dropdown .select2-results { font-size: 13px; }"+".select2-container .selection .select2-selection__rendered { font-size: 13px; }"+".select2-container--default.select2-container--focus .select2-selection--multiple { border: 1px solid #aaa; }"+".select2-selection__choice__remove { font-size: 130%; }")}else{$form.find('fieldset[name="field_block_options"]').hide();$form.find('fieldset[name="field_64"]').hide();$form.find("[name=pagerestrictions]").val(null).trigger("change");$form.find("[name=namespacerestrictions]").val(null).trigger("change")}if(fieldTemplateOptions){oldfield=$form.find('fieldset[name="field_template_options"]')[0];oldfield.parentNode.replaceChild(fieldTemplateOptions.render(),oldfield);e.target.form.root.previewer=new Morebits.wiki.Preview($(e.target.form.root).find("#twinkleblock-previewbox").last()[0])}else{$form.find('fieldset[name="field_template_options"]').hide()}if(Twinkle.block.currentBlockInfo){const sameUser=blockedUserName===relevantUserName;Morebits.Status.init($('div[name="currentblock"] span').last()[0]);let statusStr=relevantUserName+"は"+(Twinkle.block.currentBlockInfo.partial===""?"部分ブロック":"サイト全体でブロック");if(Twinkle.block.currentBlockInfo.rangestart!==Twinkle.block.currentBlockInfo.rangeend){if(sameUser){statusStr="範囲ブロックとして"+statusStr}else{statusStr=(Morebits.ip.get64(relevantUserName)===blockedUserName?"/64":"")+"範囲ブロックの範囲内にある"+statusStr;const $rangeblockloglink=$("<span>").append($('<a target="_blank" href="'+mw.util.getUrl("Special:Log",{action:"view",page:blockedUserName,type:"block"})+'">'+blockedUserName+"</a>)"));statusStr+=" ("+$rangeblockloglink.html()+")"}}if(Twinkle.block.currentBlockInfo.expiry==="infinity"){statusStr+=" (無期限)"}else if(new Morebits.Date(Twinkle.block.currentBlockInfo.expiry).isValid()){statusStr+=" (期限 "+new Morebits.Date(Twinkle.block.currentBlockInfo.expiry).calendar("utc")+")"}let infoStr="このフォームは";if(sameUser){infoStr+="ブロックを変更";if(Twinkle.block.currentBlockInfo.partial===undefined&&partialBox){infoStr+="し、部分ブロックに変換"}else if(Twinkle.block.currentBlockInfo.partial===""&&!partialBox){infoStr+="し、サイト全体のブロックに変換"}infoStr+="します。"}else{infoStr+="さらなる"+(partialBox?"部分":"")+"ブロックを追加します。"}Morebits.Status.warn(statusStr,infoStr);Twinkle.block.callback.update_form(e,Twinkle.block.currentBlockInfo)}if(Twinkle.block.hasBlockLog){const $blockloglink=$("<span>").append($('<a target="_blank" href="'+mw.util.getUrl("Special:Log",{action:"view",page:relevantUserName,type:"block"})+'">ブロック記録</a>)'));if(!Twinkle.block.currentBlockInfo){const lastBlockAction=Twinkle.block.blockLog[0];if(lastBlockAction.action==="unblock"){$blockloglink.append(" ("+new Morebits.Date(lastBlockAction.timestamp).calendar("utc")+" にブロック解除)")}else{$blockloglink.append(" ("+lastBlockAction.params.duration+", "+new Morebits.Date(lastBlockAction.params.expiry).calendar("utc")+" に期限切れ)")}}Morebits.Status.init($('div[name="hasblocklog"] span').last()[0]);Morebits.Status.warn(Twinkle.block.currentBlockInfo?"以前のブロック":"この"+(Morebits.ip.isRange(relevantUserName)?"範囲":"利用者")+"は過去にブロックされています",$blockloglink[0])}if(blockBox){Twinkle.block.callback.change_preset(e)}else if(templateBox){Twinkle.block.callback.change_template(e)}};Twinkle.block.blockPresetsInfo={"blocked proxy":{expiry:"1 year",forIPsOnly:true,nocreate:true,nonstandard:true,hardblock:true,reason:"{{blocked proxy}}",sig:null},"CheckUser block":{expiry:"1 week",forIPsOnly:true,nocreate:true,nonstandard:true,reason:"{{CheckUser block}}",sig:"~~~~"},"checkuserblock-account":{autoblock:true,expiry:"infinity",forRegisteredOnly:true,nocreate:true,nonstandard:true,reason:"{{checkuserblock-account}}",sig:"~~~~"},"checkuserblock-wide":{forIPsOnly:true,nocreate:true,nonstandard:true,reason:"{{checkuserblock-wide}}",sig:"~~~~"},oversightblock:{autoblock:true,expiry:"infinity",nocreate:true,nonstandard:true,reason:"{{OversightBlock}}",sig:"~~~~"},"range block login":{reason:"{{range block login|\x3c!-- 割当先を入力 --\x3e}}",nocreate:true,nonstandard:true,forIPsOnly:true,sig:"~~~~"},"3rr":{autoblock:true,nocreate:true,pageParam:true,reason:"[[WP:3RR|スリー・リバート・ルール]]違反",summary:"あなたは[[WP:3RR|スリー・リバート・ルール]]違反の編集を行ったためブロックされました。"},"uw-ablock":{autoblock:true,forIPsOnly:true,nocreate:true,pageParam:true,reasonParam:true,summary:"このIPアドレスは投稿ブロックされました",suppressArticleInSummary:true},"uw-adblock":{autoblock:true,nocreate:true,pageParam:true,reason:"宣伝・広告投稿はお止めください",summary:"あなたは[[WP:SOAP|広告または自己宣伝]]を行ったためブロックされました"},"uw-bioblock":{autoblock:true,nocreate:true,pageParam:true,reason:"[[WP:LIVING|存命人物に関する不適切な編集]]",summary:"あなたは[[WP:BLP|存命人物の伝記]]に関する方針に違反する編集を行ったため投稿ブロックされました"},"uw-block":{autoblock:true,forRegisteredOnly:true,nocreate:true,pageParam:true,reasonParam:true,summary:"あなたは投稿ブロックされました",suppressArticleInSummary:true},"uw-blockindef":{autoblock:true,expiry:"infinity",forRegisteredOnly:true,nocreate:true,pageParam:true,reasonParam:true,summary:"あなたは無期限投稿ブロックされました",suppressArticleInSummary:true},"uw-blocknotalk":{disabletalk:true,pageParam:true,reasonParam:true,summary:"あなたは投稿ブロックされました。また、会話ページの編集をすることはできません",suppressArticleInSummary:true},"uw-botblock":{expiry:"infinity",forRegisteredOnly:true,pageParam:true,reason:"[[WP:BRFA|承認]]されていない[[WP:BOT|ボット]]の実行",summary:"[[WP:BRFA|承認]]されていない[[WP:BOT|ボット]]を実行したようであるため、あなたは投稿ブロックされました"},"uw-causeblock":{expiry:"infinity",forRegisteredOnly:true,reason:"不適切な[[WP:U|利用者名]] \x3c!-- 団体を表す利用者名, ソフトブロック --\x3e",summary:"あなたの[[WP:U|利用者名]]は団体または組織、ウェブサイトを代表するアカウントかのような印象を与えるため無期限投稿ブロックされました"},"uw-compblock":{autoblock:true,expiry:"infinity",forRegisteredOnly:true,nocreate:true,reason:"パスワード公開",summary:"あなたはパスワードを公開したため投稿ブロックされました"},"uw-ncblock":{autoblock:true,expiry:"infinity",nocreate:true,pageParam:true,reason:"悪戯投稿",summary:"あなたは悪戯投稿を行ったため投稿ブロックされました"},"uw-sncblock":{autoblock:true,nocreate:true,pageParam:true,reason:"悪戯投稿はお止めください",summary:"あなたは悪戯投稿を行ったため投稿ブロックされました"},"uw-dblock":{autoblock:true,nocreate:true,reason:"内容の無差別な除去",pageParam:true,summary:"あなたは内容の無差別な除去を行ったため投稿ブロックされました"},"uw-efblock":{autoblock:true,nocreate:true,reason:"[[特別:不正利用記録|編集フィルター記録]]",summary:"あなたは[[WP:EF|編集フィルター]]が繰り返し発動するような破壊的な編集を行ったため投稿ブロックされました"},"uw-ewblock":{autoblock:true,nocreate:true,pageParam:true,reason:"[[WP:EW|過度の編集合戦]]",summary:"あなたは[[WP:EW|編集合戦]]を行ったため投稿ブロックされました"},"uw-evadeblock":{expiry:"infinity",nocreate:true,reason:"[[WP:投稿ブロックの方針#投稿ブロック回避|別アカウントなどによるブロック破り]]",summary:"[[WP:EVASION|投稿ブロックの回避]]に使用されたため投稿ブロックされました"},"uw-fblock":{autoblock:true,nocreate:true,reason:"虚偽情報の記載",summary:"あなたは虚偽情報の記載を行ったため投稿ブロックされました"},"uw-lblock":{autoblock:true,expiry:"infinity",nocreate:true,reason:"[[WP:法的な脅迫をしない|法的な脅迫]]",summary:"あなたは[[WP:NLT|法的な脅迫]]を行ったため投稿ブロックされました"},"uw-oblock":{autoblock:true,nocreate:true,reason:"目的外利用",summary:"あなたは目的外利用を行ったため投稿ブロックされました"},"uw-pablock":{autoblock:true,nocreate:true,reason:"[[WP:ATTACK|暴言または嫌がらせ]]",summary:"あなたは他者に対して[[WP:ATTACK|個人攻撃]]を行ったため投稿ブロックされました"},"uw-readtalkblock":{autoblock:true,nocreate:true,reason:"[[Special:Mytalk|会話ページ]]をお読みください",forRegisteredOnly:true,summary:"あなたは会話ページのメッセージをご覧になっていないため投稿ブロックされました"},"uw-rjtblock":{autoblock:true,nocreate:true,reason:"対話拒否",forRegisteredOnly:true,summary:"あなたは対話拒否を行ったため投稿ブロックされました"},"uw-sblock":{autoblock:true,nocreate:true,reason:"spam",summary:"あなたはspam目的でウィキペディアの利用を行ったため投稿ブロックされました"},"uw-sockblock":{autoblock:true,expiry:"infinity",forRegisteredOnly:true,nocreate:true,reason:"[[Wikipedia:多重アカウント|多重アカウント]]の不適切な使用",summary:"あなたは[[WP:SOCK|多重アカウント]]の不適切な使用を行ったため投稿ブロックされました"},"uw-softerblock":{expiry:"infinity",forRegisteredOnly:true,reason:"不適切な[[WP:U|利用者名]] \x3c!-- 宣伝目的の名前, ソフトブロック --\x3e",summary:"あなたの[[WP:U|利用者名]]は団体または組織、ウェブサイトを代表するアカウントかのような印象を与えるため無期限投稿ブロックされました"},"uw-spamublock":{autoblock:true,expiry:"infinity",forRegisteredOnly:true,nocreate:true,reason:"不適切な[[WP:U|利用者名]] \x3c!-- 宣伝目的の名前, 宣伝目的の編集 --\x3e",summary:"あなたのアカウントは宣伝・広告に使用され、また[[WP:U|利用者名の方針]]に違反する利用者名であるため無期限投稿ブロックされました"},"uw-spoablock":{autoblock:true,disabletalk:true,expiry:"infinity",forRegisteredOnly:true,nocreate:true,noemail:true,reason:"[[WP:SOCK|sockpuppet]]",summary:"このアカウントはウィキペディアの方針に違反するために作成された[[WP:SOCK|ソックパペット]]として投稿ブロックされました"},"uw-talkrevoked":{disabletalk:true,reason:"会話ページの編集権限を剥奪: ブロック中の会話ページの不適切な使用",prependReason:true,summary:"会話ページの編集権限は剥奪されました",useInitialOptions:true},"uw-ublock":{expiry:"infinity",forRegisteredOnly:true,reason:"不適切な[[WP:U|利用者名]] \x3c!-- 不適切な利用者名, ソフトブロック --\x3e",reasonParam:true,summary:"あなたの利用者名は[[WP:U|利用者名の方針]]に違反しているため無期限投稿ブロックされました"},"uw-ublock-double":{expiry:"infinity",forRegisteredOnly:true,reason:"不適切な[[WP:U|利用者名]] \x3c!-- 他の利用者に似ている利用者名, ソフトブロック --\x3e",summary:"あなたの[[WP:U|利用者名]]は、既存のウィキペディア利用者の名前と似ているため無期限投稿ブロックされました"},"uw-readpoliciesblock":{autoblock:true,nocreate:true,pageParam:true,reason:"各種方針の熟読期間",summary:"あなたは各種方針の熟読期間が必要であるため投稿ブロックされました"},"uw-uhblock":{autoblock:true,expiry:"infinity",forRegisteredOnly:true,nocreate:true,reason:"不適切な[[WP:U|利用者名]] \x3c!-- 不適切な利用者名, ハードブロック --\x3e",reasonParam:true,summary:"あなたの利用者名は明らかに[[WP:U|利用者名の方針]]に違反しているため無期限投稿ブロックされました"},"uw-ublock-wellknown":{expiry:"infinity",forRegisteredOnly:true,reason:"不適切な[[WP:U|利用者名]] \x3c!-- 有名人を表す利用者名, ソフトブロック --\x3e",summary:"あなたの[[WP:U|利用者名]]は存命の著名人の名前と一致しているため無期限投稿ブロックされました"},"uw-uhblock-double":{autoblock:true,expiry:"infinity",forRegisteredOnly:true,nocreate:true,reason:"不適切な[[WP:U|利用者名]] \x3c!-- 他の利用者のなりすまし, ハードブロック --\x3e",summary:"あなたの[[WP:U|利用者名]]は他の既存の利用者になりすましているように見えるため無期限投稿ブロックされました"},"uw-upeblock":{autoblock:true,expiry:"infinity",forRegisteredOnly:true,nocreate:true,pageParam:true,reason:"[[wmf:Special:MyLanguage/Terms of use|利用規約]]に違反して[[WP:PAID|有償の寄稿の開示]]に応じない利用者",summary:"あなたのアカウントはウィキペディアの方針である[[WP:PAID|有償の寄稿の開示]]に違反してしようされているため無期限投稿ブロックされました"},"uw-vaublock":{autoblock:true,expiry:"infinity",forRegisteredOnly:true,nocreate:true,pageParam:true,reason:"不適切な[[WP:U|利用者名]] \x3c!-- 荒らし目的のアカウント --\x3e",summary:"あなたのアカウントは荒らしのみに使用されており、また明らかに[[WP:U|利用者名の方針]]に違反する利用者名であるため無期限投稿ブロックされました"},"uw-vblock":{autoblock:true,expiry:"infinity",nocreate:true,pageParam:true,reason:"[[Wikipedia:荒らし|荒らし]]",summary:"さらなる[[Wikipedia:荒らし|荒らし]]を防止するため、あなたは投稿ブロックされました。"},"uw-epblock":{autoblock:true,expiry:"infinity",forRegisteredOnly:true,nocreate:false,noemail:true,pageParam:false,reasonParam:true,reason:"メールを用いた嫌がらせ",summary:"あなたは嫌がらせを行ったため他の利用者にメールを送ることをブロックされました"},"uw-ewpblock":{autoblock:true,nocreate:false,pageParam:false,reasonParam:true,reason:"[[WP:EW|編集合戦]]",summary:"あなたは[[WP:EW|編集合戦]]を行ったため部分ブロックされました"},"uw-pblock":{autoblock:true,nocreate:false,pageParam:false,reasonParam:true,summary:"あなたはウィキペディアの一部から部分ブロックされました"},"uw-pblockindef":{autoblock:true,expiry:"infinity",forRegisteredOnly:true,nocreate:false,pageParam:false,reasonParam:true,summary:"あなたはウィキペディアの一部から無期限部分ブロックされました"}};Twinkle.block.transformBlockPresets=function twinkleblockTransformBlockPresets(){$.each(Twinkle.block.blockPresetsInfo,(preset,settings)=>{settings.summary=settings.summary||settings.reason;settings.sig=settings.sig!==undefined?settings.sig:"yes";settings.watchuser=settings.watchuser!==undefined?settings.watchuser:true;settings.indefinite=settings.indefinite||Morebits.string.isInfinity(settings.expiry);if(!Twinkle.block.isRegistered&&settings.indefinite){settings.expiry="1 week"}else{settings.expiry=settings.expiry||"1 week"}Twinkle.block.blockPresetsInfo[preset]=settings})};Twinkle.block.blockGroups=[{label:"よくある理由",list:[{label:"全般ブロック (カスタム理由)",value:"uw-block"},{label:"全般ブロック (カスタム理由) - IP",value:"uw-ablock",selected:true},{label:"全般ブロック (カスタム理由) - 無期限",value:"uw-blockindef"},{label:"ブロック中の会話ページの不適切な使用",value:"uw-talkrevoked"},{label:"全般ブロック - 会話ページ禁止",value:"uw-blocknotalk"},{label:"sockpuppet",value:"uw-spoablock"},{label:"荒らし",value:"uw-vblock"},{label:"各種方針の熟読期間",value:"uw-readpoliciesblock"},{label:"会話ページをお読みください",value:"uw-readtalkblock"}]},{label:"方針",list:[{label:"スリー・リバート・ルール違反",value:"3rr"},{label:"過度の編集合戦",value:"uw-ewblock"},{label:"パスワード公開",value:"uw-compblock"},{label:"別アカウントなどによるブロック破り",value:"uw-evadeblock"},{label:"多重アカウントの不適切な使用",value:"uw-sockblock"},{label:"未承認のボット",value:"uw-botblock"},{label:"有償の寄稿の開示に応じない利用者",value:"uw-upeblock"}]},{label:"荒らし",list:[{label:"存命人物に関する不適切な編集",value:"uw-bioblock"},{label:"虚偽情報の記載",value:"uw-fblock"},{label:"内容の無差別な除去",value:"uw-dblock"},{label:"宣伝・広告投稿はお止めください",value:"uw-adblock"},{label:"悪戯投稿はお止めください",value:"uw-sncblock"},{label:"悪戯投稿",value:"uw-ncblock"},{label:"暴言または嫌がらせ",value:"uw-pablock"},{label:"対話拒否",value:"uw-rjtblock"},{label:"目的外利用",value:"uw-oblock"},{label:"編集フィルター記録",value:"uw-efblock"},{label:"法的な脅迫",value:"uw-lblock"},{label:"Spam",value:"uw-sblock"}]},{label:"不適切な利用者名",list:[{label:"宣伝目的の名前, ソフトブロック",value:"uw-softerblock"},{label:"宣伝目的の名前, ハードブロック",value:"uw-spamublock"},{label:"不適切な利用者名, ソフトブロック",value:"uw-ublock"},{label:"不適切な利用者名, ハードブロック",value:"uw-uhblock"},{label:"よく似ている利用者名, ソフトブロック",value:"uw-ublock-double"},{label:"なりすましの利用者名, ハードブロック",value:"uw-uhblock-double"},{label:"有名人を表す利用者名, ソフトブロック",value:"uw-ublock-wellknown"},{label:"団体を表す利用者名, ソフトブロック",value:"uw-causeblock"},{label:"不適切な利用者名, 荒らし目的のアカウント",value:"uw-vaublock"}]},{label:"テンプレート化された理由",list:[{label:"blocked proxy",value:"blocked proxy"},{label:"CheckUser block",value:"CheckUser block"},{label:"checkuserblock-account",value:"checkuserblock-account"},{label:"checkuserblock-wide",value:"checkuserblock-wide"},{label:"oversightblock",value:"oversightblock"},{label:"range block login",value:"range block login"}]}];Twinkle.block.blockGroupsPartial=[{label:"よくある部分ブロック理由",list:[{label:"全般部分ブロック (カスタム理由)",value:"uw-pblock",selected:true},{label:"全般部分ブロック (カスタム理由) - 無期限",value:"uw-pblockindef"},{label:"編集合戦",value:"uw-ewpblock"},{label:"メールを用いた嫌がらせ",value:"uw-epblock"}]}];Twinkle.block.callback.filtered_block_groups=function twinkleblockCallbackFilteredBlockGroups(group,showTemplate){return $.map(group,blockGroup=>{const list=$.map(blockGroup.list,blockPreset=>{switch(blockPreset.value){case"uw-talkrevoked":if(blockedUserName!==relevantUserName){return}break;case"range block login":if(!Morebits.ip.isRange(relevantUserName)){return}blockPreset.selected=!Morebits.ip.get64(relevantUserName);break;case"CheckUser block":case"checkuserblock-account":case"checkuserblock-wide":if(!Morebits.userIsInGroup("checkuser")){return}break;case"oversightblock":if(!Morebits.userIsInGroup("suppress")){return}break;default:break}const blockSettings=Twinkle.block.blockPresetsInfo[blockPreset.value];let allowedUserType;if(blockSettings.forRegisteredOnly){allowedUserType=Twinkle.block.isRegistered}else if(blockSettings.forTempAccountsOnly){allowedUserType=mw.util.isTemporaryUser(mw.config.get("wgRelevantUserName"))}else if(blockSettings.forIPsOnly){allowedUserType=!Twinkle.block.isRegistered}else{allowedUserType=true}if(!(blockSettings.templateName&&showTemplate)&&allowedUserType){const templateName=blockSettings.templateName||blockPreset.value;return{label:(showTemplate?"{{"+templateName+"}}: ":"")+blockPreset.label,value:blockPreset.value,data:[{name:"template-name",value:templateName}],selected:!!blockPreset.selected,disabled:!!blockPreset.disabled}}});if(list.length){return{label:blockGroup.label,list:list}}})};Twinkle.block.callback.change_preset=function twinkleblockCallbackChangePreset(e){const form=e.target.form,key=form.preset.value;if(!key){return}Twinkle.block.callback.update_form(e,Twinkle.block.blockPresetsInfo[key]);if(form.template){form.template.value=Twinkle.block.blockPresetsInfo[key].templateName||key;Twinkle.block.callback.change_template(e)}};Twinkle.block.callback.change_expiry=function twinkleblockCallbackChangeExpiry(e){const expiry=e.target.form.expiry;if(e.target.value==="custom"){Morebits.QuickForm.setElementVisibility(expiry.parentNode,true)}else{Morebits.QuickForm.setElementVisibility(expiry.parentNode,false);expiry.value=e.target.value}};Twinkle.block.seeAlsos=[];Twinkle.block.callback.toggle_see_alsos=function twinkleblockCallbackToggleSeeAlso(){const joinEnum=function(e){return e.join("および")};const reason=this.form.reason.value.replace(new RegExp("( \x3c!--|;) "+joinEnum(Twinkle.block.seeAlsos)+"参照( --\x3e)?"),"");Twinkle.block.seeAlsos=Twinkle.block.seeAlsos.filter(el=>el!==this.value);if(this.checked){Twinkle.block.seeAlsos.push(this.value)}const seeAlsoMessage=joinEnum(Twinkle.block.seeAlsos);if(!Twinkle.block.seeAlsos.length){this.form.reason.value=reason}else if(reason.includes("{{")){this.form.reason.value=reason+" \x3c!-- "+seeAlsoMessage+"参照 --\x3e"}else{this.form.reason.value=reason+"; "+seeAlsoMessage+"参照"}};Twinkle.block.callback.update_form=function twinkleblockCallbackUpdateForm(e,data){const form=e.target.form;let expiry=data.expiry;if(!data.useInitialOptions){if(Date.parse(expiry)){expiry=new Date(expiry).toGMTString();form.expiry_preset.value="custom"}else{form.expiry_preset.value=data.expiry||"custom"}form.expiry.value=expiry;if(form.expiry_preset.value==="custom"){Morebits.QuickForm.setElementVisibility(form.expiry.parentNode,true)}else{Morebits.QuickForm.setElementVisibility(form.expiry.parentNode,false)}}data.disabletalk=data.disabletalk!==undefined?data.disabletalk:false;data.hardblock=data.hardblock!==undefined?data.hardblock:false;if(Twinkle.block.userIsBot||/bot\b/i.test(relevantUserName)){data.autoblock=false}$(form).find("[name=field_block_options]").find(":checkbox").each((i,el)=>{if(data.useInitialOptions&&data[el.name]===undefined){return}const check=data[el.name]===""||!!data[el.name];$(el).prop("checked",check)});if(data.prependReason&&data.reason){form.reason.value=data.reason+"; "+form.reason.value}else{form.reason.value=data.reason||""}if(form.pagerestrictions){const $pageSelect=$(form).find("[name=pagerestrictions]");const $namespaceSelect=$(form).find("[name=namespacerestrictions]");if(!data.useInitialOptions){$pageSelect.val(null).trigger("change");$namespaceSelect.val(null).trigger("change")}if(data.restrictions){if(data.restrictions.pages&&!$pageSelect.val().length){const pages=data.restrictions.pages.map(pr=>pr.title);pages.forEach(page=>{if(!$pageSelect.find("option[value='"+$.escapeSelector(page)+"']").length){const newOption=new Option(page,page,true,true);$pageSelect.append(newOption)}});$pageSelect.val($pageSelect.val().concat(pages)).trigger("change")}if(data.restrictions.namespaces){$namespaceSelect.val($namespaceSelect.val().concat(data.restrictions.namespaces)).trigger("change")}}}};Twinkle.block.callback.change_template=function twinkleblockcallbackChangeTemplate(e){const form=e.target.form,value=form.template.value,settings=Twinkle.block.blockPresetsInfo[value];const blockBox=$(form).find("[name=actiontype][value=block]").is(":checked");const partialBox=$(form).find("[name=actiontype][value=partial]").is(":checked");const templateBox=$(form).find("[name=actiontype][value=template]").is(":checked");if(!blockBox){if(settings.indefinite||settings.nonstandard){if(Twinkle.block.prev_template_expiry===null){Twinkle.block.prev_template_expiry=form.template_expiry.value||""}form.template_expiry.parentNode.style.display="none";form.template_expiry.value="infinity"}else if(form.template_expiry.parentNode.style.display==="none"){if(Twinkle.block.prev_template_expiry!==null){form.template_expiry.value=Twinkle.block.prev_template_expiry;Twinkle.block.prev_template_expiry=null}form.template_expiry.parentNode.style.display="block"}if(Twinkle.block.prev_template_expiry){form.expiry.value=Twinkle.block.prev_template_expiry}Morebits.QuickForm.setElementVisibility(form.notalk.parentNode,!settings.nonstandard);Morebits.QuickForm.setElementVisibility(form.noemail_template.parentNode,partialBox);Morebits.QuickForm.setElementVisibility(form.nocreate_template.parentNode,partialBox)}else if(templateBox){Morebits.QuickForm.setElementVisibility(form.blank_duration.parentNode,!settings.indefinite&&!settings.nonstandard)}Morebits.QuickForm.setElementVisibility(form.article.parentNode,settings&&!!settings.pageParam);Morebits.QuickForm.setElementVisibility(form.block_reason.parentNode,settings&&!!settings.reasonParam);Morebits.QuickForm.setElementVisibility(form.area.parentNode,partialBox&&!blockBox);form.root.previewer.closePreview()};Twinkle.block.prev_template_expiry=null;Twinkle.block.callback.preview=function twinkleblockcallbackPreview(form){const params={article:form.article.value,blank_duration:form.blank_duration?form.blank_duration.checked:false,disabletalk:form.disabletalk.checked||(form.notalk?form.notalk.checked:false),expiry:form.template_expiry?form.template_expiry.value:form.expiry.value,hardblock:Twinkle.block.isRegistered?form.autoblock.checked:form.hardblock.checked,indefinite:Morebits.string.isInfinity(form.template_expiry?form.template_expiry.value:form.expiry.value),reason:form.block_reason.value,template:form.template.value,partial:$(form).find("[name=actiontype][value=partial]").is(":checked"),pagerestrictions:$(form.pagerestrictions).val()||[],namespacerestrictions:$(form.namespacerestrictions).val()||[],noemail:form.noemail.checked||(form.noemail_template?form.noemail_template.checked:false),nocreate:form.nocreate.checked||(form.nocreate_template?form.nocreate_template.checked:false),area:form.area.value};const templateText=Twinkle.block.callback.getBlockNoticeWikitext(params);form.previewer.beginRender(templateText,"User_talk:"+relevantUserName)};Twinkle.block.callback.evaluate=function twinkleblockCallbackEvaluate(e){const $form=$(e.target),toBlock=$form.find("[name=actiontype][value=block]").is(":checked"),toWarn=$form.find("[name=actiontype][value=template]").is(":checked"),toPartial=$form.find("[name=actiontype][value=partial]").is(":checked");let blockoptions={},templateoptions={};Twinkle.block.callback.saveFieldset($form.find("[name=field_block_options]"));Twinkle.block.callback.saveFieldset($form.find("[name=field_template_options]"));blockoptions=Twinkle.block.field_block_options;templateoptions=Twinkle.block.field_template_options;templateoptions.disabletalk=!!(templateoptions.disabletalk||blockoptions.disabletalk);templateoptions.hardblock=!!blockoptions.hardblock;delete blockoptions.expiry_preset;if(toPartial){blockoptions.partial=templateoptions.partial=true}templateoptions.pagerestrictions=$form.find("[name=pagerestrictions]").val()||[];templateoptions.namespacerestrictions=$form.find("[name=namespacerestrictions]").val()||[];blockoptions.pagerestrictions=templateoptions.pagerestrictions.join("|");blockoptions.namespacerestrictions=templateoptions.namespacerestrictions.join("|");templateoptions.summary=templateoptions.summary||blockoptions.reason;templateoptions.expiry=templateoptions.template_expiry||blockoptions.expiry;if(toBlock){if(blockoptions.partial){if(blockoptions.disabletalk&&!blockoptions.namespacerestrictions.includes("3")){return alert("利用者会話名前空間の編集を制限しない限り、部分ブロックでは会話ページの編集を禁止することはできません!")}if(!blockoptions.namespacerestrictions&&!blockoptions.pagerestrictions){if(!blockoptions.noemail&&!blockoptions.nocreate){return alert("ページや名前空間が選択されておらず、メール送信やアカウント作成の制限を選択されていません; 少なくとも一つオプションを選択してください!")}else if((templateoptions.template!=="uw-epblock"||$form.find('select[name="preset"]').val()!=="uw-epblock")&&!confirm("You are about to block with no restrictions on page or namespace editing, are you sure you want to proceed?")){return}}}if(!blockoptions.expiry){return alert("有効期限を入力してください!")}else if(Morebits.string.isInfinity(blockoptions.expiry)&&!Twinkle.block.isRegistered){return alert("IPアドレスを無期限ブロックすることはできません!")}if(!blockoptions.reason){return alert("ブロック理由を入力してください!")}Morebits.SimpleWindow.setButtonsEnabled(false);Morebits.Status.init(e.target);const statusElement=new Morebits.Status("ブロックの実行");blockoptions.action="block";blockoptions.user=relevantUserName;blockoptions.anononly=blockoptions.hardblock?undefined:true;blockoptions.allowusertalk=blockoptions.disabletalk?undefined:true;const query={format:"json",action:"query",list:"blocks|logevents",letype:"block",lelimit:1,letitle:"User:"+blockoptions.user};if(mw.util.isIPAddress(blockoptions.user,true)){query.bkip=blockoptions.user}else{query.bkusers=blockoptions.user}api.get(query).then(data=>{let block=data.query.blocks[0];if(data.query.blocks.length>1&&block.user!==relevantUserName){block=data.query.blocks[1]}const logevents=data.query.logevents[0];const logid=data.query.logevents.length?logevents.logid:false;if(logid!==Twinkle.block.blockLogId||!!block!==!!Twinkle.block.currentBlockInfo){let message="The block status of "+blockoptions.user+" has changed. ";if(block){message+="New status: "}else{message+="Last entry: "}let logExpiry="";if(logevents.params.duration){if(logevents.params.duration==="infinity"){logExpiry="indefinitely"}else{const expiryDate=new Morebits.Date(logevents.params.expiry);logExpiry+=(expiryDate.isBefore(new Date)?", expired ":" until ")+expiryDate.calendar()}}else{logExpiry=" "+new Morebits.Date(logevents.timestamp).calendar()}message+=Morebits.string.toUpperCaseFirstChar(logevents.action)+"ed by "+logevents.user+logExpiry+' for "'+logevents.comment+'". Do you want to override with your settings?';if(!confirm(message)){Morebits.Status.info("ブロックの実行","利用者によって中断");return}blockoptions.reblock=1}blockoptions.token=mw.user.tokens.get("csrfToken");const mbApi=new Morebits.wiki.Api("ブロックの実行",blockoptions,()=>{statusElement.info("完了");if(toWarn){Twinkle.block.callback.issue_template(templateoptions)}});mbApi.post()})}else if(toWarn){Morebits.SimpleWindow.setButtonsEnabled(false);Morebits.Status.init(e.target);Twinkle.block.callback.issue_template(templateoptions)}else{return alert("Please give Twinkle something to do!")}};Twinkle.block.callback.issue_template=function twinkleblockCallbackIssueTemplate(formData){const userTalkPage="User_talk:"+mw.config.get("wgRelevantUserName");const params=Twinkle.block.combineFormDataAndFieldTemplateOptions(formData,Twinkle.block.blockPresetsInfo[formData.template],Twinkle.block.field_template_options.block_reason,Twinkle.block.field_template_options.notalk,Twinkle.block.field_template_options.noemail_template,Twinkle.block.field_template_options.nocreate_template);Morebits.wiki.actionCompleted.redirect=userTalkPage;Morebits.wiki.actionCompleted.notice="Actions complete, loading user talk page in a few seconds";const wikipediaPage=new Morebits.wiki.Page(userTalkPage,"User talk page modification");wikipediaPage.setCallbackParameters(params);wikipediaPage.load(Twinkle.block.callback.main)};Twinkle.block.combineFormDataAndFieldTemplateOptions=function(formData,messageData,reason,disabletalk,noemail,nocreate){return $.extend(formData,{messageData:messageData,reason:reason,disabletalk:disabletalk,noemail:noemail,nocreate:nocreate})};Twinkle.block.callback.getBlockNoticeWikitext=function(params){let text="{{";const settings=Twinkle.block.blockPresetsInfo[params.template];if(!settings.nonstandard){text+="subst:"+params.template;if(params.article&&settings.pageParam){text+="|page="+params.article}if(!/te?mp|^\s*$|min/.exec(params.expiry)){if(params.indefinite){text+="|indef=yes"}else if(!params.blank_duration&&!new Morebits.Date(params.expiry).isValid()){text+="|time="+params.expiry}}if(!Twinkle.block.isRegistered&&!params.hardblock){text+="|anon=yes"}if(params.reason){text+="|reason="+params.reason}if(params.disabletalk){text+="|notalk=yes"}if(params.partial){if(params.pagerestrictions.length||params.namespacerestrictions.length){const makeSentence=function(array){if(array.length<3){return array.join(" and ")}const last=array.pop();return array.join(", ")+", and "+last};text+="|area="+(params.indefinite?"certain ":"from certain ");if(params.pagerestrictions.length){text+="pages ("+makeSentence(params.pagerestrictions.map(p=>"[[:"+p+"]]"));text+=params.namespacerestrictions.length?") and certain ":")"}if(params.namespacerestrictions.length){const namespaceNames=params.namespacerestrictions.map(id=>menuFormattedNamespaces[id]);text+="[[Wikipedia:Namespace|namespaces]] ("+makeSentence(namespaceNames)+")"}}else if(params.area){text+="|area="+params.area}else{if(params.noemail){text+="|email=yes"}if(params.nocreate){text+="|accountcreate=yes"}}}}else{text+=params.template}if(settings.sig){text+="|sig="+settings.sig}return text+"}}"};Twinkle.block.callback.main=function twinkleblockcallbackMain(pageobj){const params=pageobj.getCallbackParameters(),date=new Morebits.Date(pageobj.getLoadTime()),messageData=params.messageData;let text;params.indefinite=Morebits.string.isInfinity(params.expiry);if(Twinkle.getPref("blankTalkpageOnIndefBlock")&¶ms.template!=="uw-lblock"&¶ms.indefinite){Morebits.Status.info("Info","Blanking talk page per preferences and creating a new talk page section for this month");text=date.monthHeader()+"\n"}else{text=pageobj.getPageText();const dateHeaderRegex=date.monthHeaderRegex();let dateHeaderRegexLast,dateHeaderRegexResult;while((dateHeaderRegexLast=dateHeaderRegex.exec(text))!==null){dateHeaderRegexResult=dateHeaderRegexLast}const lastHeaderIndex=text.lastIndexOf("\n==")+1;if(text.length>0){text+="\n\n"}if(!dateHeaderRegexResult||dateHeaderRegexResult.index!==lastHeaderIndex){Morebits.Status.info("Info","Will create a new talk page section for this month, as none was found");text+=date.monthHeader()+"\n"}}params.expiry=typeof params.template_expiry!=="undefined"?params.template_expiry:params.expiry;text+=Twinkle.block.callback.getBlockNoticeWikitext(params);let summary=messageData.summary;if(messageData.suppressArticleInSummary!==true&¶ms.article){summary+=" on [[:"+params.article+"]]"}summary+=".";pageobj.setPageText(text);pageobj.setEditSummary(summary);pageobj.setChangeTags(Twinkle.changeTags);pageobj.setWatchlist(Twinkle.getPref("watchWarnings"));pageobj.save()};Twinkle.addInitCallback(Twinkle.block,"block")})();
// </nowiki>
t97qtdaicien6np1m9xp1pv3xt58u0z
User:Syunsyunminmin/Twinkle/twinklebatchdelete.js
2
179408
769473
2026-10-21T13:58:24Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 3680108:
769473
javascript
text/javascript
/*
Twinkle: adapted for Japanese Wikipedia; upstream authors and Japanese adaptations are credited in UPSTREAM.md.
Upstream source: https://github.com/wikimedia-gadgets/twinkle/blob/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/src/modules/twinklebatchdelete.js
Upstream author history: https://github.com/wikimedia-gadgets/twinkle/commits/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/src/modules/twinklebatchdelete.js
License: CC BY-SA 3.0 Unported. See https://github.com/wikimedia-gadgets/twinkle/blob/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/LICENSE.txt
Japanese adaptation: Syunsyunminmin; includes later synchronization changes.
*/
// <nowiki>
(function() {
/*
****************************************
*** twinklebatchdelete.js: Batch delete module (sysops only)
****************************************
* Mode of invocation: Tab ("D-batch")
* Active on: Existing non-articles, and Special:PrefixIndex
*/
Twinkle.batchdelete = function twinklebatchdelete() {
if (
Morebits.userIsSysop && (
(mw.config.get('wgCurRevisionId') && mw.config.get('wgNamespaceNumber') > 0) ||
mw.config.get('wgCanonicalSpecialPageName') === 'Prefixindex'
)
) {
Twinkle.addPortletLink(Twinkle.batchdelete.callback, 'D-batch', 'tw-batch', 'Delete pages found in this category/on this page');
}
};
Twinkle.batchdelete.unlinkCache = {};
// Has the subpages list been loaded?
let subpagesLoaded;
Twinkle.batchdelete.callback = function twinklebatchdeleteCallback() {
subpagesLoaded = false;
const Window = new Morebits.SimpleWindow(600, 400);
Window.setTitle('Batch deletion');
Window.setScriptName('Twinkle');
Window.addFooterLink('Twinkle help', 'WP:TW/DOC#batchdelete');
Window.addFooterLink('Give feedback', 'WT:TW');
const form = new Morebits.QuickForm(Twinkle.batchdelete.callback.evaluate);
form.append({
type: 'checkbox',
list: [
{
label: 'Delete pages',
name: 'delete_page',
value: 'delete',
checked: true,
subgroup: {
type: 'checkbox',
list: [
{
label: 'Delete associated talk pages (except user talk pages)',
name: 'delete_talk',
value: 'delete_talk',
checked: true
},
{
label: 'Delete redirects to deleted pages',
name: 'delete_redirects',
value: 'delete_redirects',
checked: true
},
{
label: 'Delete subpages of deleted pages',
name: 'delete_subpages',
value: 'delete_subpages',
checked: false,
event: Twinkle.batchdelete.callback.toggleSubpages,
subgroup: {
type: 'checkbox',
list: [
{
label: 'Delete talk pages of deleted subpages',
name: 'delete_subpage_talks',
value: 'delete_subpage_talks'
},
{
label: 'Delete redirects to deleted subpages',
name: 'delete_subpage_redirects',
value: 'delete_subpage_redirects'
},
{
label: 'Unlink backlinks to each deleted subpage (in Main and Portal namespaces only)',
name: 'unlink_subpages',
value: 'unlink_subpages'
}
]
}
}
]
}
},
{
label: 'Unlink backlinks to each page (in Main and Portal namespaces only)',
name: 'unlink_page',
value: 'unlink',
checked: false
},
{
label: 'Remove usages of each file (in all namespaces)',
name: 'unlink_file',
value: 'unlink_file',
checked: true
}
]
});
form.append({
type: 'input',
name: 'reason',
label: 'Reason:',
size: 60
});
const query = {
action: 'query',
prop: 'revisions|info|imageinfo',
inprop: 'protection',
rvprop: 'size|user',
format: 'json'
};
// On categories
if (mw.config.get('wgNamespaceNumber') === 14) {
query.generator = 'categorymembers';
query.gcmtitle = mw.config.get('wgPageName');
query.gcmlimit = Twinkle.getPref('batchMax');
// On Special:PrefixIndex
} else if (mw.config.get('wgCanonicalSpecialPageName') === 'Prefixindex') {
query.generator = 'allpages';
query.gaplimit = Twinkle.getPref('batchMax');
if (mw.util.getParamValue('prefix')) {
query.gapnamespace = mw.util.getParamValue('namespace');
query.gapprefix = mw.util.getParamValue('prefix');
} else {
let pathSplit = decodeURIComponent(location.pathname).split('/');
if (pathSplit.length < 3 || pathSplit[2] !== 'Special:PrefixIndex') {
return;
}
const titleSplit = pathSplit[3].split(':');
query.gapnamespace = mw.config.get('wgNamespaceIds')[titleSplit[0].toLowerCase()];
if (titleSplit.length < 2 || typeof query.gapnamespace === 'undefined') {
query.gapnamespace = 0; // article namespace
query.gapprefix = pathSplit.splice(3).join('/');
} else {
pathSplit = pathSplit.splice(4);
pathSplit.splice(0, 0, titleSplit.splice(1).join(':'));
query.gapprefix = pathSplit.join('/');
}
}
// On normal pages
} else {
query.generator = 'links';
query.titles = mw.config.get('wgPageName');
query.gpllimit = Twinkle.getPref('batchMax');
}
const statusdiv = document.createElement('div');
statusdiv.style.padding = '15px'; // just so it doesn't look broken
Window.setContent(statusdiv);
Morebits.Status.init(statusdiv);
Window.display();
Twinkle.batchdelete.pages = {};
const statelem = new Morebits.Status('Grabbing list of pages');
const wikipediaApi = new Morebits.wiki.Api('loading...', query, ((apiobj) => {
const response = apiobj.getResponse();
let pages = (response.query && response.query.pages) || [];
pages = pages.filter((page) => !page.missing && page.imagerepository !== 'shared');
pages.sort(Twinkle.sortByNamespace);
pages.forEach((page) => {
const metadata = [];
if (page.redirect) {
metadata.push('redirect');
}
const editProt = page.protection.filter((pr) => pr.type === 'edit' && pr.level === 'sysop').pop();
if (editProt) {
metadata.push('fully protected' +
(editProt.expiry === 'infinity' ? ' indefinitely' : ', expires ' + new Morebits.Date(editProt.expiry).calendar('utc') + ' (UTC)'));
}
if (page.ns === 6) {
metadata.push('uploader: ' + page.imageinfo[0].user);
metadata.push('last edit from: ' + page.revisions[0].user);
} else {
metadata.push(mw.language.convertNumber(page.revisions[0].size) + ' bytes');
}
const title = page.title;
Twinkle.batchdelete.pages[title] = {
label: title + (metadata.length ? ' (' + metadata.join('; ') + ')' : ''),
value: title,
checked: true,
style: editProt ? 'color:red' : ''
};
});
const form = apiobj.params.form;
form.append({ type: 'header', label: 'Pages to delete' });
form.append({
type: 'button',
label: 'Select All',
event: function dBatchSelectAll() {
$(result).find('input[name=pages]:not(:checked)').each((_, e) => {
e.click(); // check it, and invoke click event so that subgroup can be shown
});
// Check any unchecked subpages too
$('input[name="pages.subpages"]').prop('checked', true);
}
});
form.append({
type: 'button',
label: 'Deselect All',
event: function dBatchDeselectAll() {
$(result).find('input[name=pages]:checked').each((_, e) => {
e.click(); // uncheck it, and invoke click event so that subgroup can be hidden
});
}
});
form.append({
type: 'checkbox',
name: 'pages',
id: 'tw-dbatch-pages',
shiftClickSupport: true,
list: $.map(Twinkle.batchdelete.pages, (e) => e)
});
form.append({ type: 'submit' });
var result = form.render();
apiobj.params.Window.setContent(result);
Morebits.QuickForm.getElements(result, 'pages').forEach(Twinkle.generateArrowLinks);
}), statelem);
wikipediaApi.params = { form: form, Window: Window };
wikipediaApi.post();
};
Twinkle.batchdelete.generateNewPageList = function(form) {
// Update the list of checked pages in Twinkle.batchdelete.pages object
const elements = form.elements.pages;
if (elements instanceof NodeList) { // if there are multiple pages
for (let i = 0; i < elements.length; ++i) {
Twinkle.batchdelete.pages[elements[i].value].checked = elements[i].checked;
}
} else if (elements instanceof HTMLInputElement) { // if there is just one page
Twinkle.batchdelete.pages[elements.value].checked = elements.checked;
}
return new Morebits.QuickForm.Element({
type: 'checkbox',
name: 'pages',
id: 'tw-dbatch-pages',
shiftClickSupport: true,
list: $.map(Twinkle.batchdelete.pages, (e) => e)
}).render();
};
Twinkle.batchdelete.callback.toggleSubpages = function twDbatchToggleSubpages(e) {
const form = e.target.form;
let newPageList;
if (e.target.checked) {
form.delete_subpage_redirects.checked = form.delete_redirects.checked;
form.delete_subpage_talks.checked = form.delete_talk.checked;
form.unlink_subpages.checked = form.unlink_page.checked;
// If lists of subpages were already loaded once, they are
// available without use of any API calls
if (subpagesLoaded) {
$.each(Twinkle.batchdelete.pages, (i, el) => {
// Get back the subgroup from subgroupBeforeDeletion, where we saved it
if (el.subgroup === null && el.subgroupBeforeDeletion) {
el.subgroup = el.subgroupBeforeDeletion;
}
});
newPageList = Twinkle.batchdelete.generateNewPageList(form);
$('#tw-dbatch-pages').replaceWith(newPageList);
Morebits.QuickForm.getElements(newPageList, 'pages').forEach(Twinkle.generateArrowLinks);
Morebits.QuickForm.getElements(newPageList, 'pages.subpages').forEach(Twinkle.generateArrowLinks);
return;
}
// Proceed with API calls to get list of subpages
const loadingText = '<strong id="dbatch-subpage-loading">Loading... </strong>';
$(e.target).after(loadingText);
const pages = $(form.pages).map((i, el) => el.value).get();
const subpageLister = new Morebits.BatchOperation();
subpageLister.setOption('chunkSize', Twinkle.getPref('batchChunks'));
subpageLister.setPageList(pages);
subpageLister.run((pageName) => {
const pageTitle = mw.Title.newFromText(pageName);
// No need to look for subpages in main/file/mediawiki space
if ([0, 6, 8].includes(pageTitle.namespace)) {
subpageLister.workerSuccess();
return;
}
const wikipediaApi = new Morebits.wiki.Api('Getting list of subpages of ' + pageName, {
action: 'query',
prop: 'revisions|info|imageinfo',
generator: 'allpages',
rvprop: 'size',
inprop: 'protection',
gapprefix: pageTitle.title + '/',
gapnamespace: pageTitle.namespace,
gaplimit: 'max', // 500 is max for normal users, 5000 for bots and sysops
format: 'json'
}, ((apiobj) => {
const response = apiobj.getResponse();
const pages = (response.query && response.query.pages) || [];
const subpageList = [];
pages.sort(Twinkle.sortByNamespace);
pages.forEach((page) => {
const metadata = [];
if (page.redirect) {
metadata.push('redirect');
}
const editProt = page.protection.filter((pr) => pr.type === 'edit' && pr.level === 'sysop').pop();
if (editProt) {
metadata.push('fully protected' +
(editProt.expiry === 'infinity' ? ' indefinitely' : ', expires ' + new Morebits.Date(editProt.expiry).calendar('utc') + ' (UTC)'));
}
if (page.ns === 6) {
metadata.push('uploader: ' + page.imageinfo[0].user);
metadata.push('last edit from: ' + page.revisions[0].user);
} else {
metadata.push(mw.language.convertNumber(page.revisions[0].size) + ' bytes');
}
const title = page.title;
subpageList.push({
label: title + (metadata.length ? ' (' + metadata.join('; ') + ')' : ''),
value: title,
checked: true,
style: editProt ? 'color:red' : ''
});
});
if (subpageList.length) {
const pageName = apiobj.params.pageNameFull;
Twinkle.batchdelete.pages[pageName].subgroup = {
type: 'checkbox',
name: 'subpages',
className: 'dbatch-subpages',
shiftClickSupport: true,
list: subpageList
};
}
subpageLister.workerSuccess();
}), null /* statusElement */, (() => {
subpageLister.workerFailure();
}));
wikipediaApi.params = { pageNameFull: pageName }; // Used in onSuccess()
wikipediaApi.post();
}, () => {
// List 'em on the interface
newPageList = Twinkle.batchdelete.generateNewPageList(form);
$('#tw-dbatch-pages').replaceWith(newPageList);
Morebits.QuickForm.getElements(newPageList, 'pages').forEach(Twinkle.generateArrowLinks);
Morebits.QuickForm.getElements(newPageList, 'pages.subpages').forEach(Twinkle.generateArrowLinks);
subpagesLoaded = true;
// Remove "Loading... " text
$('#dbatch-subpage-loading').remove();
});
} else if (!e.target.checked) {
$.each(Twinkle.batchdelete.pages, (i, el) => {
if (el.subgroup) {
// Remove subgroup after saving its contents in subgroupBeforeDeletion
// so that it can be retrieved easily if user decides to
// delete the subpages again
el.subgroupBeforeDeletion = el.subgroup;
el.subgroup = null;
}
});
newPageList = Twinkle.batchdelete.generateNewPageList(form);
$('#tw-dbatch-pages').replaceWith(newPageList);
Morebits.QuickForm.getElements(newPageList, 'pages').forEach(Twinkle.generateArrowLinks);
}
};
Twinkle.batchdelete.callback.evaluate = function twinklebatchdeleteCallbackEvaluate(event) {
Morebits.wiki.actionCompleted.notice = 'Batch deletion is now complete';
const form = event.target;
const numProtected = $(Morebits.QuickForm.getElements(form, 'pages')).filter((index, element) => element.checked && element.nextElementSibling.style.color === 'red').length;
if (numProtected > 0 && !confirm('You are about to delete ' + mw.language.convertNumber(numProtected) + ' fully protected page(s). Are you sure?')) {
return;
}
const input = Morebits.QuickForm.getInputData(form);
if (!input.reason) {
alert('You need to give a reason, you cabal crony!');
return;
}
Morebits.SimpleWindow.setButtonsEnabled(false);
Morebits.Status.init(form);
if (!input.pages || input.pages.length === 0) { // undefined if there are no pages linked from the page
Morebits.Status.error('Error', 'nothing to delete, aborting');
return;
}
const pageDeleter = new Morebits.BatchOperation(input.delete_page ? 'Deleting pages' : 'Initiating requested tasks');
pageDeleter.setOption('chunkSize', Twinkle.getPref('batchChunks'));
// we only need the initial status lines if we're deleting the pages in the pages array
pageDeleter.setOption('preserveIndividualStatusLines', input.delete_page);
pageDeleter.setPageList(input.pages);
pageDeleter.run((pageName) => {
const params = {
page: pageName,
delete_page: input.delete_page,
delete_talk: input.delete_talk,
delete_redirects: input.delete_redirects,
unlink_page: input.unlink_page,
unlink_file: input.unlink_file && new RegExp('^' + Morebits.namespaceRegex(6) + ':', 'i').test(pageName),
reason: input.reason,
pageDeleter: pageDeleter
};
const wikipediaPage = new Morebits.wiki.Page(pageName, 'Deleting page ' + pageName);
wikipediaPage.setCallbackParameters(params);
if (input.delete_page) {
wikipediaPage.setEditSummary(input.reason);
wikipediaPage.setChangeTags(Twinkle.changeTags);
wikipediaPage.suppressProtectWarning();
wikipediaPage.deletePage(Twinkle.batchdelete.callbacks.doExtras, pageDeleter.workerFailure);
} else {
Twinkle.batchdelete.callbacks.doExtras(wikipediaPage);
}
}, () => {
if (input.delete_subpages && input.subpages) {
const subpageDeleter = new Morebits.BatchOperation('Deleting subpages');
subpageDeleter.setOption('chunkSize', Twinkle.getPref('batchChunks'));
subpageDeleter.setOption('preserveIndividualStatusLines', true);
subpageDeleter.setPageList(input.subpages);
subpageDeleter.run((pageName) => {
const params = {
page: pageName,
delete_page: true,
delete_talk: input.delete_subpage_talks,
delete_redirects: input.delete_subpage_redirects,
unlink_page: input.unlink_subpages,
unlink_file: false,
reason: input.reason,
pageDeleter: subpageDeleter
};
const wikipediaPage = new Morebits.wiki.Page(pageName, 'Deleting subpage ' + pageName);
wikipediaPage.setCallbackParameters(params);
wikipediaPage.setEditSummary(input.reason);
wikipediaPage.setChangeTags(Twinkle.changeTags);
wikipediaPage.suppressProtectWarning();
wikipediaPage.deletePage(Twinkle.batchdelete.callbacks.doExtras, pageDeleter.workerFailure);
});
}
});
};
Twinkle.batchdelete.callbacks = {
// this stupid parameter name is a temporary thing until I implement an overhaul
// of Morebits.wiki.* callback parameters
doExtras: function(thingWithParameters) {
const params = thingWithParameters.parent ? thingWithParameters.parent.getCallbackParameters() :
thingWithParameters.getCallbackParameters();
// the initial batch operation's job is to delete the page, and that has
// succeeded by now
params.pageDeleter.workerSuccess(thingWithParameters);
let query, wikipediaApi;
if (params.unlink_page) {
Twinkle.batchdelete.unlinkCache = {};
query = {
action: 'query',
list: 'backlinks',
blfilterredir: 'nonredirects',
blnamespace: [0, 100], // main space and portal space only
bltitle: params.page,
bllimit: 'max', // 500 is max for normal users, 5000 for bots and sysops
format: 'json'
};
wikipediaApi = new Morebits.wiki.Api('Grabbing backlinks', query, Twinkle.batchdelete.callbacks.unlinkBacklinksMain);
wikipediaApi.params = params;
wikipediaApi.post();
}
if (params.unlink_file) {
query = {
action: 'query',
list: 'imageusage',
iutitle: params.page,
iulimit: 'max', // 500 is max for normal users, 5000 for bots and sysops
format: 'json'
};
wikipediaApi = new Morebits.wiki.Api('Grabbing file links', query, Twinkle.batchdelete.callbacks.unlinkImageInstancesMain);
wikipediaApi.params = params;
wikipediaApi.post();
}
if (params.delete_page) {
if (params.delete_redirects) {
query = {
action: 'query',
titles: params.page,
prop: 'redirects',
rdlimit: 'max', // 500 is max for normal users, 5000 for bots and sysops
format: 'json'
};
wikipediaApi = new Morebits.wiki.Api('Grabbing redirects', query, Twinkle.batchdelete.callbacks.deleteRedirectsMain);
wikipediaApi.params = params;
wikipediaApi.post();
}
if (params.delete_talk) {
const pageTitle = mw.Title.newFromText(params.page);
if (pageTitle && pageTitle.namespace % 2 === 0 && pageTitle.namespace !== 2) {
pageTitle.namespace++; // now pageTitle is the talk page title!
query = {
action: 'query',
titles: pageTitle.toText(),
format: 'json'
};
wikipediaApi = new Morebits.wiki.Api('Checking whether talk page exists', query, Twinkle.batchdelete.callbacks.deleteTalk);
wikipediaApi.params = params;
wikipediaApi.params.talkPage = pageTitle.toText();
wikipediaApi.post();
}
}
}
},
deleteRedirectsMain: function(apiobj) {
const response = apiobj.getResponse();
let pages = response.query.pages[0].redirects || [];
pages = pages.map((redirect) => redirect.title);
if (!pages.length) {
return;
}
const redirectDeleter = new Morebits.BatchOperation('Deleting redirects to ' + apiobj.params.page);
redirectDeleter.setOption('chunkSize', Twinkle.getPref('batchChunks'));
redirectDeleter.setPageList(pages);
redirectDeleter.run((pageName) => {
const wikipediaPage = new Morebits.wiki.Page(pageName, 'Deleting ' + pageName);
wikipediaPage.setEditSummary('[[WP:CSD#G8|G8]]: Redirect to deleted page [[' + apiobj.params.page + ']]');
wikipediaPage.setChangeTags(Twinkle.changeTags);
wikipediaPage.deletePage(redirectDeleter.workerSuccess, redirectDeleter.workerFailure);
});
},
deleteTalk: function(apiobj) {
const response = apiobj.getResponse();
// no talk page; forget about it
if (response.query.pages[0].missing) {
return;
}
const page = new Morebits.wiki.Page(apiobj.params.talkPage, 'Deleting talk page of page ' + apiobj.params.page);
page.setEditSummary('[[WP:CSD#G8|G8]]: [[Help:Talk page|Talk page]] of deleted page [[' + apiobj.params.page + ']]');
page.setChangeTags(Twinkle.changeTags);
page.deletePage();
},
unlinkBacklinksMain: function(apiobj) {
const response = apiobj.getResponse();
let pages = response.query.backlinks || [];
pages = pages.map((page) => page.title);
if (!pages.length) {
return;
}
const unlinker = new Morebits.BatchOperation('Unlinking backlinks to ' + apiobj.params.page);
unlinker.setOption('chunkSize', Twinkle.getPref('batchChunks'));
unlinker.setPageList(pages);
unlinker.run((pageName) => {
const wikipediaPage = new Morebits.wiki.Page(pageName, 'Unlinking on ' + pageName);
const params = $.extend({}, apiobj.params);
params.title = pageName;
params.unlinker = unlinker;
wikipediaPage.setCallbackParameters(params);
wikipediaPage.load(Twinkle.batchdelete.callbacks.unlinkBacklinks);
});
},
unlinkBacklinks: function(pageobj) {
const params = pageobj.getCallbackParameters();
if (!pageobj.exists()) {
// we probably just deleted it, as a recursive backlink
params.unlinker.workerSuccess(pageobj);
return;
}
let text;
if (params.title in Twinkle.batchdelete.unlinkCache) {
text = Twinkle.batchdelete.unlinkCache[params.title];
} else {
text = pageobj.getPageText();
}
const oldText = text;
const wikiPage = new Morebits.wikitext.Page(text);
text = wikiPage.removeLink(params.page).getText();
Twinkle.batchdelete.unlinkCache[params.title] = text;
if (text === oldText) {
// Nothing to do, return
params.unlinker.workerSuccess(pageobj);
return;
}
pageobj.setEditSummary('Removing link(s) to deleted page ' + params.page);
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.setPageText(text);
pageobj.setCreateOption('nocreate');
pageobj.setMaxConflictRetries(10);
pageobj.save(params.unlinker.workerSuccess, params.unlinker.workerFailure);
},
unlinkImageInstancesMain: function(apiobj) {
const response = apiobj.getResponse();
let pages = response.query.imageusage || [];
pages = pages.map((page) => page.title);
if (!pages.length) {
return;
}
const unlinker = new Morebits.BatchOperation('Unlinking backlinks to ' + apiobj.params.page);
unlinker.setOption('chunkSize', Twinkle.getPref('batchChunks'));
unlinker.setPageList(pages);
unlinker.run((pageName) => {
const wikipediaPage = new Morebits.wiki.Page(pageName, 'Removing file usages on ' + pageName);
const params = $.extend({}, apiobj.params);
params.title = pageName;
params.unlinker = unlinker;
wikipediaPage.setCallbackParameters(params);
wikipediaPage.load(Twinkle.batchdelete.callbacks.unlinkImageInstances);
});
},
unlinkImageInstances: function(pageobj) {
const params = pageobj.getCallbackParameters();
if (!pageobj.exists()) {
// we probably just deleted it, as a recursive backlink
params.unlinker.workerSuccess(pageobj);
return;
}
const image = params.page.replace(new RegExp('^' + Morebits.namespaceRegex(6) + ':'), '');
let text;
if (params.title in Twinkle.batchdelete.unlinkCache) {
text = Twinkle.batchdelete.unlinkCache[params.title];
} else {
text = pageobj.getPageText();
}
const oldText = text;
const wikiPage = new Morebits.wikitext.Page(text);
text = wikiPage.commentOutImage(image, 'Commented out because image was deleted').getText();
Twinkle.batchdelete.unlinkCache[params.title] = text;
if (text === oldText) {
pageobj.getStatusElement().error('failed to unlink image ' + image + ' from ' + pageobj.getPageName());
params.unlinker.workerFailure(pageobj);
return;
}
pageobj.setEditSummary('Removing instance of file ' + image + ' that has been deleted because "' + params.reason + '")');
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.setPageText(text);
pageobj.setCreateOption('nocreate');
pageobj.setMaxConflictRetries(10);
pageobj.save(params.unlinker.workerSuccess, params.unlinker.workerFailure);
}
};
Twinkle.addInitCallback(Twinkle.batchdelete, 'batchdelete');
}());
// </nowiki>
s9b7eydvot23nwgptptdewn8jjkojr7
769540
769473
2026-10-22T00:01:09Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 1406828: rm cmt
769540
javascript
text/javascript
// <nowiki>
(function() {
/*
****************************************
*** twinklebatchdelete.js: Batch delete module (sysops only)
****************************************
* Mode of invocation: Tab ("D-batch")
* Active on: Existing non-articles, and Special:PrefixIndex
*/
Twinkle.batchdelete = function twinklebatchdelete() {
if (
Morebits.userIsSysop && (
(mw.config.get('wgCurRevisionId') && mw.config.get('wgNamespaceNumber') > 0) ||
mw.config.get('wgCanonicalSpecialPageName') === 'Prefixindex'
)
) {
Twinkle.addPortletLink(Twinkle.batchdelete.callback, 'D-batch', 'tw-batch', 'Delete pages found in this category/on this page');
}
};
Twinkle.batchdelete.unlinkCache = {};
// Has the subpages list been loaded?
let subpagesLoaded;
Twinkle.batchdelete.callback = function twinklebatchdeleteCallback() {
subpagesLoaded = false;
const Window = new Morebits.SimpleWindow(600, 400);
Window.setTitle('Batch deletion');
Window.setScriptName('Twinkle');
Window.addFooterLink('Twinkle help', 'WP:TW/DOC#batchdelete');
Window.addFooterLink('Give feedback', 'WT:TW');
const form = new Morebits.QuickForm(Twinkle.batchdelete.callback.evaluate);
form.append({
type: 'checkbox',
list: [
{
label: 'Delete pages',
name: 'delete_page',
value: 'delete',
checked: true,
subgroup: {
type: 'checkbox',
list: [
{
label: 'Delete associated talk pages (except user talk pages)',
name: 'delete_talk',
value: 'delete_talk',
checked: true
},
{
label: 'Delete redirects to deleted pages',
name: 'delete_redirects',
value: 'delete_redirects',
checked: true
},
{
label: 'Delete subpages of deleted pages',
name: 'delete_subpages',
value: 'delete_subpages',
checked: false,
event: Twinkle.batchdelete.callback.toggleSubpages,
subgroup: {
type: 'checkbox',
list: [
{
label: 'Delete talk pages of deleted subpages',
name: 'delete_subpage_talks',
value: 'delete_subpage_talks'
},
{
label: 'Delete redirects to deleted subpages',
name: 'delete_subpage_redirects',
value: 'delete_subpage_redirects'
},
{
label: 'Unlink backlinks to each deleted subpage (in Main and Portal namespaces only)',
name: 'unlink_subpages',
value: 'unlink_subpages'
}
]
}
}
]
}
},
{
label: 'Unlink backlinks to each page (in Main and Portal namespaces only)',
name: 'unlink_page',
value: 'unlink',
checked: false
},
{
label: 'Remove usages of each file (in all namespaces)',
name: 'unlink_file',
value: 'unlink_file',
checked: true
}
]
});
form.append({
type: 'input',
name: 'reason',
label: 'Reason:',
size: 60
});
const query = {
action: 'query',
prop: 'revisions|info|imageinfo',
inprop: 'protection',
rvprop: 'size|user',
format: 'json'
};
// On categories
if (mw.config.get('wgNamespaceNumber') === 14) {
query.generator = 'categorymembers';
query.gcmtitle = mw.config.get('wgPageName');
query.gcmlimit = Twinkle.getPref('batchMax');
// On Special:PrefixIndex
} else if (mw.config.get('wgCanonicalSpecialPageName') === 'Prefixindex') {
query.generator = 'allpages';
query.gaplimit = Twinkle.getPref('batchMax');
if (mw.util.getParamValue('prefix')) {
query.gapnamespace = mw.util.getParamValue('namespace');
query.gapprefix = mw.util.getParamValue('prefix');
} else {
let pathSplit = decodeURIComponent(location.pathname).split('/');
if (pathSplit.length < 3 || pathSplit[2] !== 'Special:PrefixIndex') {
return;
}
const titleSplit = pathSplit[3].split(':');
query.gapnamespace = mw.config.get('wgNamespaceIds')[titleSplit[0].toLowerCase()];
if (titleSplit.length < 2 || typeof query.gapnamespace === 'undefined') {
query.gapnamespace = 0; // article namespace
query.gapprefix = pathSplit.splice(3).join('/');
} else {
pathSplit = pathSplit.splice(4);
pathSplit.splice(0, 0, titleSplit.splice(1).join(':'));
query.gapprefix = pathSplit.join('/');
}
}
// On normal pages
} else {
query.generator = 'links';
query.titles = mw.config.get('wgPageName');
query.gpllimit = Twinkle.getPref('batchMax');
}
const statusdiv = document.createElement('div');
statusdiv.style.padding = '15px'; // just so it doesn't look broken
Window.setContent(statusdiv);
Morebits.Status.init(statusdiv);
Window.display();
Twinkle.batchdelete.pages = {};
const statelem = new Morebits.Status('Grabbing list of pages');
const wikipediaApi = new Morebits.wiki.Api('loading...', query, ((apiobj) => {
const response = apiobj.getResponse();
let pages = (response.query && response.query.pages) || [];
pages = pages.filter((page) => !page.missing && page.imagerepository !== 'shared');
pages.sort(Twinkle.sortByNamespace);
pages.forEach((page) => {
const metadata = [];
if (page.redirect) {
metadata.push('redirect');
}
const editProt = page.protection.filter((pr) => pr.type === 'edit' && pr.level === 'sysop').pop();
if (editProt) {
metadata.push('fully protected' +
(editProt.expiry === 'infinity' ? ' indefinitely' : ', expires ' + new Morebits.Date(editProt.expiry).calendar('utc') + ' (UTC)'));
}
if (page.ns === 6) {
metadata.push('uploader: ' + page.imageinfo[0].user);
metadata.push('last edit from: ' + page.revisions[0].user);
} else {
metadata.push(mw.language.convertNumber(page.revisions[0].size) + ' bytes');
}
const title = page.title;
Twinkle.batchdelete.pages[title] = {
label: title + (metadata.length ? ' (' + metadata.join('; ') + ')' : ''),
value: title,
checked: true,
style: editProt ? 'color:red' : ''
};
});
const form = apiobj.params.form;
form.append({ type: 'header', label: 'Pages to delete' });
form.append({
type: 'button',
label: 'Select All',
event: function dBatchSelectAll() {
$(result).find('input[name=pages]:not(:checked)').each((_, e) => {
e.click(); // check it, and invoke click event so that subgroup can be shown
});
// Check any unchecked subpages too
$('input[name="pages.subpages"]').prop('checked', true);
}
});
form.append({
type: 'button',
label: 'Deselect All',
event: function dBatchDeselectAll() {
$(result).find('input[name=pages]:checked').each((_, e) => {
e.click(); // uncheck it, and invoke click event so that subgroup can be hidden
});
}
});
form.append({
type: 'checkbox',
name: 'pages',
id: 'tw-dbatch-pages',
shiftClickSupport: true,
list: $.map(Twinkle.batchdelete.pages, (e) => e)
});
form.append({ type: 'submit' });
var result = form.render();
apiobj.params.Window.setContent(result);
Morebits.QuickForm.getElements(result, 'pages').forEach(Twinkle.generateArrowLinks);
}), statelem);
wikipediaApi.params = { form: form, Window: Window };
wikipediaApi.post();
};
Twinkle.batchdelete.generateNewPageList = function(form) {
// Update the list of checked pages in Twinkle.batchdelete.pages object
const elements = form.elements.pages;
if (elements instanceof NodeList) { // if there are multiple pages
for (let i = 0; i < elements.length; ++i) {
Twinkle.batchdelete.pages[elements[i].value].checked = elements[i].checked;
}
} else if (elements instanceof HTMLInputElement) { // if there is just one page
Twinkle.batchdelete.pages[elements.value].checked = elements.checked;
}
return new Morebits.QuickForm.Element({
type: 'checkbox',
name: 'pages',
id: 'tw-dbatch-pages',
shiftClickSupport: true,
list: $.map(Twinkle.batchdelete.pages, (e) => e)
}).render();
};
Twinkle.batchdelete.callback.toggleSubpages = function twDbatchToggleSubpages(e) {
const form = e.target.form;
let newPageList;
if (e.target.checked) {
form.delete_subpage_redirects.checked = form.delete_redirects.checked;
form.delete_subpage_talks.checked = form.delete_talk.checked;
form.unlink_subpages.checked = form.unlink_page.checked;
// If lists of subpages were already loaded once, they are
// available without use of any API calls
if (subpagesLoaded) {
$.each(Twinkle.batchdelete.pages, (i, el) => {
// Get back the subgroup from subgroupBeforeDeletion, where we saved it
if (el.subgroup === null && el.subgroupBeforeDeletion) {
el.subgroup = el.subgroupBeforeDeletion;
}
});
newPageList = Twinkle.batchdelete.generateNewPageList(form);
$('#tw-dbatch-pages').replaceWith(newPageList);
Morebits.QuickForm.getElements(newPageList, 'pages').forEach(Twinkle.generateArrowLinks);
Morebits.QuickForm.getElements(newPageList, 'pages.subpages').forEach(Twinkle.generateArrowLinks);
return;
}
// Proceed with API calls to get list of subpages
const loadingText = '<strong id="dbatch-subpage-loading">Loading... </strong>';
$(e.target).after(loadingText);
const pages = $(form.pages).map((i, el) => el.value).get();
const subpageLister = new Morebits.BatchOperation();
subpageLister.setOption('chunkSize', Twinkle.getPref('batchChunks'));
subpageLister.setPageList(pages);
subpageLister.run((pageName) => {
const pageTitle = mw.Title.newFromText(pageName);
// No need to look for subpages in main/file/mediawiki space
if ([0, 6, 8].includes(pageTitle.namespace)) {
subpageLister.workerSuccess();
return;
}
const wikipediaApi = new Morebits.wiki.Api('Getting list of subpages of ' + pageName, {
action: 'query',
prop: 'revisions|info|imageinfo',
generator: 'allpages',
rvprop: 'size',
inprop: 'protection',
gapprefix: pageTitle.title + '/',
gapnamespace: pageTitle.namespace,
gaplimit: 'max', // 500 is max for normal users, 5000 for bots and sysops
format: 'json'
}, ((apiobj) => {
const response = apiobj.getResponse();
const pages = (response.query && response.query.pages) || [];
const subpageList = [];
pages.sort(Twinkle.sortByNamespace);
pages.forEach((page) => {
const metadata = [];
if (page.redirect) {
metadata.push('redirect');
}
const editProt = page.protection.filter((pr) => pr.type === 'edit' && pr.level === 'sysop').pop();
if (editProt) {
metadata.push('fully protected' +
(editProt.expiry === 'infinity' ? ' indefinitely' : ', expires ' + new Morebits.Date(editProt.expiry).calendar('utc') + ' (UTC)'));
}
if (page.ns === 6) {
metadata.push('uploader: ' + page.imageinfo[0].user);
metadata.push('last edit from: ' + page.revisions[0].user);
} else {
metadata.push(mw.language.convertNumber(page.revisions[0].size) + ' bytes');
}
const title = page.title;
subpageList.push({
label: title + (metadata.length ? ' (' + metadata.join('; ') + ')' : ''),
value: title,
checked: true,
style: editProt ? 'color:red' : ''
});
});
if (subpageList.length) {
const pageName = apiobj.params.pageNameFull;
Twinkle.batchdelete.pages[pageName].subgroup = {
type: 'checkbox',
name: 'subpages',
className: 'dbatch-subpages',
shiftClickSupport: true,
list: subpageList
};
}
subpageLister.workerSuccess();
}), null /* statusElement */, (() => {
subpageLister.workerFailure();
}));
wikipediaApi.params = { pageNameFull: pageName }; // Used in onSuccess()
wikipediaApi.post();
}, () => {
// List 'em on the interface
newPageList = Twinkle.batchdelete.generateNewPageList(form);
$('#tw-dbatch-pages').replaceWith(newPageList);
Morebits.QuickForm.getElements(newPageList, 'pages').forEach(Twinkle.generateArrowLinks);
Morebits.QuickForm.getElements(newPageList, 'pages.subpages').forEach(Twinkle.generateArrowLinks);
subpagesLoaded = true;
// Remove "Loading... " text
$('#dbatch-subpage-loading').remove();
});
} else if (!e.target.checked) {
$.each(Twinkle.batchdelete.pages, (i, el) => {
if (el.subgroup) {
// Remove subgroup after saving its contents in subgroupBeforeDeletion
// so that it can be retrieved easily if user decides to
// delete the subpages again
el.subgroupBeforeDeletion = el.subgroup;
el.subgroup = null;
}
});
newPageList = Twinkle.batchdelete.generateNewPageList(form);
$('#tw-dbatch-pages').replaceWith(newPageList);
Morebits.QuickForm.getElements(newPageList, 'pages').forEach(Twinkle.generateArrowLinks);
}
};
Twinkle.batchdelete.callback.evaluate = function twinklebatchdeleteCallbackEvaluate(event) {
Morebits.wiki.actionCompleted.notice = 'Batch deletion is now complete';
const form = event.target;
const numProtected = $(Morebits.QuickForm.getElements(form, 'pages')).filter((index, element) => element.checked && element.nextElementSibling.style.color === 'red').length;
if (numProtected > 0 && !confirm('You are about to delete ' + mw.language.convertNumber(numProtected) + ' fully protected page(s). Are you sure?')) {
return;
}
const input = Morebits.QuickForm.getInputData(form);
if (!input.reason) {
alert('You need to give a reason, you cabal crony!');
return;
}
Morebits.SimpleWindow.setButtonsEnabled(false);
Morebits.Status.init(form);
if (!input.pages || input.pages.length === 0) { // undefined if there are no pages linked from the page
Morebits.Status.error('Error', 'nothing to delete, aborting');
return;
}
const pageDeleter = new Morebits.BatchOperation(input.delete_page ? 'Deleting pages' : 'Initiating requested tasks');
pageDeleter.setOption('chunkSize', Twinkle.getPref('batchChunks'));
// we only need the initial status lines if we're deleting the pages in the pages array
pageDeleter.setOption('preserveIndividualStatusLines', input.delete_page);
pageDeleter.setPageList(input.pages);
pageDeleter.run((pageName) => {
const params = {
page: pageName,
delete_page: input.delete_page,
delete_talk: input.delete_talk,
delete_redirects: input.delete_redirects,
unlink_page: input.unlink_page,
unlink_file: input.unlink_file && new RegExp('^' + Morebits.namespaceRegex(6) + ':', 'i').test(pageName),
reason: input.reason,
pageDeleter: pageDeleter
};
const wikipediaPage = new Morebits.wiki.Page(pageName, 'Deleting page ' + pageName);
wikipediaPage.setCallbackParameters(params);
if (input.delete_page) {
wikipediaPage.setEditSummary(input.reason);
wikipediaPage.setChangeTags(Twinkle.changeTags);
wikipediaPage.suppressProtectWarning();
wikipediaPage.deletePage(Twinkle.batchdelete.callbacks.doExtras, pageDeleter.workerFailure);
} else {
Twinkle.batchdelete.callbacks.doExtras(wikipediaPage);
}
}, () => {
if (input.delete_subpages && input.subpages) {
const subpageDeleter = new Morebits.BatchOperation('Deleting subpages');
subpageDeleter.setOption('chunkSize', Twinkle.getPref('batchChunks'));
subpageDeleter.setOption('preserveIndividualStatusLines', true);
subpageDeleter.setPageList(input.subpages);
subpageDeleter.run((pageName) => {
const params = {
page: pageName,
delete_page: true,
delete_talk: input.delete_subpage_talks,
delete_redirects: input.delete_subpage_redirects,
unlink_page: input.unlink_subpages,
unlink_file: false,
reason: input.reason,
pageDeleter: subpageDeleter
};
const wikipediaPage = new Morebits.wiki.Page(pageName, 'Deleting subpage ' + pageName);
wikipediaPage.setCallbackParameters(params);
wikipediaPage.setEditSummary(input.reason);
wikipediaPage.setChangeTags(Twinkle.changeTags);
wikipediaPage.suppressProtectWarning();
wikipediaPage.deletePage(Twinkle.batchdelete.callbacks.doExtras, pageDeleter.workerFailure);
});
}
});
};
Twinkle.batchdelete.callbacks = {
// this stupid parameter name is a temporary thing until I implement an overhaul
// of Morebits.wiki.* callback parameters
doExtras: function(thingWithParameters) {
const params = thingWithParameters.parent ? thingWithParameters.parent.getCallbackParameters() :
thingWithParameters.getCallbackParameters();
// the initial batch operation's job is to delete the page, and that has
// succeeded by now
params.pageDeleter.workerSuccess(thingWithParameters);
let query, wikipediaApi;
if (params.unlink_page) {
Twinkle.batchdelete.unlinkCache = {};
query = {
action: 'query',
list: 'backlinks',
blfilterredir: 'nonredirects',
blnamespace: [0, 100], // main space and portal space only
bltitle: params.page,
bllimit: 'max', // 500 is max for normal users, 5000 for bots and sysops
format: 'json'
};
wikipediaApi = new Morebits.wiki.Api('Grabbing backlinks', query, Twinkle.batchdelete.callbacks.unlinkBacklinksMain);
wikipediaApi.params = params;
wikipediaApi.post();
}
if (params.unlink_file) {
query = {
action: 'query',
list: 'imageusage',
iutitle: params.page,
iulimit: 'max', // 500 is max for normal users, 5000 for bots and sysops
format: 'json'
};
wikipediaApi = new Morebits.wiki.Api('Grabbing file links', query, Twinkle.batchdelete.callbacks.unlinkImageInstancesMain);
wikipediaApi.params = params;
wikipediaApi.post();
}
if (params.delete_page) {
if (params.delete_redirects) {
query = {
action: 'query',
titles: params.page,
prop: 'redirects',
rdlimit: 'max', // 500 is max for normal users, 5000 for bots and sysops
format: 'json'
};
wikipediaApi = new Morebits.wiki.Api('Grabbing redirects', query, Twinkle.batchdelete.callbacks.deleteRedirectsMain);
wikipediaApi.params = params;
wikipediaApi.post();
}
if (params.delete_talk) {
const pageTitle = mw.Title.newFromText(params.page);
if (pageTitle && pageTitle.namespace % 2 === 0 && pageTitle.namespace !== 2) {
pageTitle.namespace++; // now pageTitle is the talk page title!
query = {
action: 'query',
titles: pageTitle.toText(),
format: 'json'
};
wikipediaApi = new Morebits.wiki.Api('Checking whether talk page exists', query, Twinkle.batchdelete.callbacks.deleteTalk);
wikipediaApi.params = params;
wikipediaApi.params.talkPage = pageTitle.toText();
wikipediaApi.post();
}
}
}
},
deleteRedirectsMain: function(apiobj) {
const response = apiobj.getResponse();
let pages = response.query.pages[0].redirects || [];
pages = pages.map((redirect) => redirect.title);
if (!pages.length) {
return;
}
const redirectDeleter = new Morebits.BatchOperation('Deleting redirects to ' + apiobj.params.page);
redirectDeleter.setOption('chunkSize', Twinkle.getPref('batchChunks'));
redirectDeleter.setPageList(pages);
redirectDeleter.run((pageName) => {
const wikipediaPage = new Morebits.wiki.Page(pageName, 'Deleting ' + pageName);
wikipediaPage.setEditSummary('[[WP:CSD#G8|G8]]: Redirect to deleted page [[' + apiobj.params.page + ']]');
wikipediaPage.setChangeTags(Twinkle.changeTags);
wikipediaPage.deletePage(redirectDeleter.workerSuccess, redirectDeleter.workerFailure);
});
},
deleteTalk: function(apiobj) {
const response = apiobj.getResponse();
// no talk page; forget about it
if (response.query.pages[0].missing) {
return;
}
const page = new Morebits.wiki.Page(apiobj.params.talkPage, 'Deleting talk page of page ' + apiobj.params.page);
page.setEditSummary('[[WP:CSD#G8|G8]]: [[Help:Talk page|Talk page]] of deleted page [[' + apiobj.params.page + ']]');
page.setChangeTags(Twinkle.changeTags);
page.deletePage();
},
unlinkBacklinksMain: function(apiobj) {
const response = apiobj.getResponse();
let pages = response.query.backlinks || [];
pages = pages.map((page) => page.title);
if (!pages.length) {
return;
}
const unlinker = new Morebits.BatchOperation('Unlinking backlinks to ' + apiobj.params.page);
unlinker.setOption('chunkSize', Twinkle.getPref('batchChunks'));
unlinker.setPageList(pages);
unlinker.run((pageName) => {
const wikipediaPage = new Morebits.wiki.Page(pageName, 'Unlinking on ' + pageName);
const params = $.extend({}, apiobj.params);
params.title = pageName;
params.unlinker = unlinker;
wikipediaPage.setCallbackParameters(params);
wikipediaPage.load(Twinkle.batchdelete.callbacks.unlinkBacklinks);
});
},
unlinkBacklinks: function(pageobj) {
const params = pageobj.getCallbackParameters();
if (!pageobj.exists()) {
// we probably just deleted it, as a recursive backlink
params.unlinker.workerSuccess(pageobj);
return;
}
let text;
if (params.title in Twinkle.batchdelete.unlinkCache) {
text = Twinkle.batchdelete.unlinkCache[params.title];
} else {
text = pageobj.getPageText();
}
const oldText = text;
const wikiPage = new Morebits.wikitext.Page(text);
text = wikiPage.removeLink(params.page).getText();
Twinkle.batchdelete.unlinkCache[params.title] = text;
if (text === oldText) {
// Nothing to do, return
params.unlinker.workerSuccess(pageobj);
return;
}
pageobj.setEditSummary('Removing link(s) to deleted page ' + params.page);
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.setPageText(text);
pageobj.setCreateOption('nocreate');
pageobj.setMaxConflictRetries(10);
pageobj.save(params.unlinker.workerSuccess, params.unlinker.workerFailure);
},
unlinkImageInstancesMain: function(apiobj) {
const response = apiobj.getResponse();
let pages = response.query.imageusage || [];
pages = pages.map((page) => page.title);
if (!pages.length) {
return;
}
const unlinker = new Morebits.BatchOperation('Unlinking backlinks to ' + apiobj.params.page);
unlinker.setOption('chunkSize', Twinkle.getPref('batchChunks'));
unlinker.setPageList(pages);
unlinker.run((pageName) => {
const wikipediaPage = new Morebits.wiki.Page(pageName, 'Removing file usages on ' + pageName);
const params = $.extend({}, apiobj.params);
params.title = pageName;
params.unlinker = unlinker;
wikipediaPage.setCallbackParameters(params);
wikipediaPage.load(Twinkle.batchdelete.callbacks.unlinkImageInstances);
});
},
unlinkImageInstances: function(pageobj) {
const params = pageobj.getCallbackParameters();
if (!pageobj.exists()) {
// we probably just deleted it, as a recursive backlink
params.unlinker.workerSuccess(pageobj);
return;
}
const image = params.page.replace(new RegExp('^' + Morebits.namespaceRegex(6) + ':'), '');
let text;
if (params.title in Twinkle.batchdelete.unlinkCache) {
text = Twinkle.batchdelete.unlinkCache[params.title];
} else {
text = pageobj.getPageText();
}
const oldText = text;
const wikiPage = new Morebits.wikitext.Page(text);
text = wikiPage.commentOutImage(image, 'Commented out because image was deleted').getText();
Twinkle.batchdelete.unlinkCache[params.title] = text;
if (text === oldText) {
pageobj.getStatusElement().error('failed to unlink image ' + image + ' from ' + pageobj.getPageName());
params.unlinker.workerFailure(pageobj);
return;
}
pageobj.setEditSummary('Removing instance of file ' + image + ' that has been deleted because "' + params.reason + '")');
pageobj.setChangeTags(Twinkle.changeTags);
pageobj.setPageText(text);
pageobj.setCreateOption('nocreate');
pageobj.setMaxConflictRetries(10);
pageobj.save(params.unlinker.workerSuccess, params.unlinker.workerFailure);
}
};
Twinkle.addInitCallback(Twinkle.batchdelete, 'batchdelete');
}());
// </nowiki>
o7ifvjxu0shnzyf51ud5b517naa5wdr
769577
769540
2026-10-22T02:31:52Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at d9cd3461baca43ac99ac2c24fb76a3aae6ddada9: minify
769577
javascript
text/javascript
// <nowiki>
(function(){Twinkle.batchdelete=function twinklebatchdelete(){if(Morebits.userIsSysop&&(mw.config.get("wgCurRevisionId")&&mw.config.get("wgNamespaceNumber")>0||mw.config.get("wgCanonicalSpecialPageName")==="Prefixindex")){Twinkle.addPortletLink(Twinkle.batchdelete.callback,"D-batch","tw-batch","Delete pages found in this category/on this page")}};Twinkle.batchdelete.unlinkCache={};let subpagesLoaded;Twinkle.batchdelete.callback=function twinklebatchdeleteCallback(){subpagesLoaded=false;const Window=new Morebits.SimpleWindow(600,400);Window.setTitle("Batch deletion");Window.setScriptName("Twinkle");Window.addFooterLink("Twinkle help","WP:TW/DOC#batchdelete");Window.addFooterLink("Give feedback","WT:TW");const form=new Morebits.QuickForm(Twinkle.batchdelete.callback.evaluate);form.append({type:"checkbox",list:[{label:"Delete pages",name:"delete_page",value:"delete",checked:true,subgroup:{type:"checkbox",list:[{label:"Delete associated talk pages (except user talk pages)",name:"delete_talk",value:"delete_talk",checked:true},{label:"Delete redirects to deleted pages",name:"delete_redirects",value:"delete_redirects",checked:true},{label:"Delete subpages of deleted pages",name:"delete_subpages",value:"delete_subpages",checked:false,event:Twinkle.batchdelete.callback.toggleSubpages,subgroup:{type:"checkbox",list:[{label:"Delete talk pages of deleted subpages",name:"delete_subpage_talks",value:"delete_subpage_talks"},{label:"Delete redirects to deleted subpages",name:"delete_subpage_redirects",value:"delete_subpage_redirects"},{label:"Unlink backlinks to each deleted subpage (in Main and Portal namespaces only)",name:"unlink_subpages",value:"unlink_subpages"}]}}]}},{label:"Unlink backlinks to each page (in Main and Portal namespaces only)",name:"unlink_page",value:"unlink",checked:false},{label:"Remove usages of each file (in all namespaces)",name:"unlink_file",value:"unlink_file",checked:true}]});form.append({type:"input",name:"reason",label:"Reason:",size:60});const query={action:"query",prop:"revisions|info|imageinfo",inprop:"protection",rvprop:"size|user",format:"json"};if(mw.config.get("wgNamespaceNumber")===14){query.generator="categorymembers";query.gcmtitle=mw.config.get("wgPageName");query.gcmlimit=Twinkle.getPref("batchMax")}else if(mw.config.get("wgCanonicalSpecialPageName")==="Prefixindex"){query.generator="allpages";query.gaplimit=Twinkle.getPref("batchMax");if(mw.util.getParamValue("prefix")){query.gapnamespace=mw.util.getParamValue("namespace");query.gapprefix=mw.util.getParamValue("prefix")}else{let pathSplit=decodeURIComponent(location.pathname).split("/");if(pathSplit.length<3||pathSplit[2]!=="Special:PrefixIndex"){return}const titleSplit=pathSplit[3].split(":");query.gapnamespace=mw.config.get("wgNamespaceIds")[titleSplit[0].toLowerCase()];if(titleSplit.length<2||typeof query.gapnamespace==="undefined"){query.gapnamespace=0;query.gapprefix=pathSplit.splice(3).join("/")}else{pathSplit=pathSplit.splice(4);pathSplit.splice(0,0,titleSplit.splice(1).join(":"));query.gapprefix=pathSplit.join("/")}}}else{query.generator="links";query.titles=mw.config.get("wgPageName");query.gpllimit=Twinkle.getPref("batchMax")}const statusdiv=document.createElement("div");statusdiv.style.padding="15px";Window.setContent(statusdiv);Morebits.Status.init(statusdiv);Window.display();Twinkle.batchdelete.pages={};const statelem=new Morebits.Status("Grabbing list of pages");const wikipediaApi=new Morebits.wiki.Api("loading...",query,apiobj=>{const response=apiobj.getResponse();let pages=response.query&&response.query.pages||[];pages=pages.filter(page=>!page.missing&&page.imagerepository!=="shared");pages.sort(Twinkle.sortByNamespace);pages.forEach(page=>{const metadata=[];if(page.redirect){metadata.push("redirect")}const editProt=page.protection.filter(pr=>pr.type==="edit"&&pr.level==="sysop").pop();if(editProt){metadata.push("fully protected"+(editProt.expiry==="infinity"?" indefinitely":", expires "+new Morebits.Date(editProt.expiry).calendar("utc")+" (UTC)"))}if(page.ns===6){metadata.push("uploader: "+page.imageinfo[0].user);metadata.push("last edit from: "+page.revisions[0].user)}else{metadata.push(mw.language.convertNumber(page.revisions[0].size)+" bytes")}const title=page.title;Twinkle.batchdelete.pages[title]={label:title+(metadata.length?" ("+metadata.join("; ")+")":""),value:title,checked:true,style:editProt?"color:red":""}});const form=apiobj.params.form;form.append({type:"header",label:"Pages to delete"});form.append({type:"button",label:"Select All",event:function dBatchSelectAll(){$(result).find("input[name=pages]:not(:checked)").each((_,e)=>{e.click()});$('input[name="pages.subpages"]').prop("checked",true)}});form.append({type:"button",label:"Deselect All",event:function dBatchDeselectAll(){$(result).find("input[name=pages]:checked").each((_,e)=>{e.click()})}});form.append({type:"checkbox",name:"pages",id:"tw-dbatch-pages",shiftClickSupport:true,list:$.map(Twinkle.batchdelete.pages,e=>e)});form.append({type:"submit"});var result=form.render();apiobj.params.Window.setContent(result);Morebits.QuickForm.getElements(result,"pages").forEach(Twinkle.generateArrowLinks)},statelem);wikipediaApi.params={form:form,Window:Window};wikipediaApi.post()};Twinkle.batchdelete.generateNewPageList=function(form){const elements=form.elements.pages;if(elements instanceof NodeList){for(let i=0;i<elements.length;++i){Twinkle.batchdelete.pages[elements[i].value].checked=elements[i].checked}}else if(elements instanceof HTMLInputElement){Twinkle.batchdelete.pages[elements.value].checked=elements.checked}return new Morebits.QuickForm.Element({type:"checkbox",name:"pages",id:"tw-dbatch-pages",shiftClickSupport:true,list:$.map(Twinkle.batchdelete.pages,e=>e)}).render()};Twinkle.batchdelete.callback.toggleSubpages=function twDbatchToggleSubpages(e){const form=e.target.form;let newPageList;if(e.target.checked){form.delete_subpage_redirects.checked=form.delete_redirects.checked;form.delete_subpage_talks.checked=form.delete_talk.checked;form.unlink_subpages.checked=form.unlink_page.checked;if(subpagesLoaded){$.each(Twinkle.batchdelete.pages,(i,el)=>{if(el.subgroup===null&&el.subgroupBeforeDeletion){el.subgroup=el.subgroupBeforeDeletion}});newPageList=Twinkle.batchdelete.generateNewPageList(form);$("#tw-dbatch-pages").replaceWith(newPageList);Morebits.QuickForm.getElements(newPageList,"pages").forEach(Twinkle.generateArrowLinks);Morebits.QuickForm.getElements(newPageList,"pages.subpages").forEach(Twinkle.generateArrowLinks);return}const loadingText='<strong id="dbatch-subpage-loading">Loading... </strong>';$(e.target).after(loadingText);const pages=$(form.pages).map((i,el)=>el.value).get();const subpageLister=new Morebits.BatchOperation;subpageLister.setOption("chunkSize",Twinkle.getPref("batchChunks"));subpageLister.setPageList(pages);subpageLister.run(pageName=>{const pageTitle=mw.Title.newFromText(pageName);if([0,6,8].includes(pageTitle.namespace)){subpageLister.workerSuccess();return}const wikipediaApi=new Morebits.wiki.Api("Getting list of subpages of "+pageName,{action:"query",prop:"revisions|info|imageinfo",generator:"allpages",rvprop:"size",inprop:"protection",gapprefix:pageTitle.title+"/",gapnamespace:pageTitle.namespace,gaplimit:"max",format:"json"},apiobj=>{const response=apiobj.getResponse();const pages=response.query&&response.query.pages||[];const subpageList=[];pages.sort(Twinkle.sortByNamespace);pages.forEach(page=>{const metadata=[];if(page.redirect){metadata.push("redirect")}const editProt=page.protection.filter(pr=>pr.type==="edit"&&pr.level==="sysop").pop();if(editProt){metadata.push("fully protected"+(editProt.expiry==="infinity"?" indefinitely":", expires "+new Morebits.Date(editProt.expiry).calendar("utc")+" (UTC)"))}if(page.ns===6){metadata.push("uploader: "+page.imageinfo[0].user);metadata.push("last edit from: "+page.revisions[0].user)}else{metadata.push(mw.language.convertNumber(page.revisions[0].size)+" bytes")}const title=page.title;subpageList.push({label:title+(metadata.length?" ("+metadata.join("; ")+")":""),value:title,checked:true,style:editProt?"color:red":""})});if(subpageList.length){const pageName=apiobj.params.pageNameFull;Twinkle.batchdelete.pages[pageName].subgroup={type:"checkbox",name:"subpages",className:"dbatch-subpages",shiftClickSupport:true,list:subpageList}}subpageLister.workerSuccess()},null,()=>{subpageLister.workerFailure()});wikipediaApi.params={pageNameFull:pageName};wikipediaApi.post()},()=>{newPageList=Twinkle.batchdelete.generateNewPageList(form);$("#tw-dbatch-pages").replaceWith(newPageList);Morebits.QuickForm.getElements(newPageList,"pages").forEach(Twinkle.generateArrowLinks);Morebits.QuickForm.getElements(newPageList,"pages.subpages").forEach(Twinkle.generateArrowLinks);subpagesLoaded=true;$("#dbatch-subpage-loading").remove()})}else if(!e.target.checked){$.each(Twinkle.batchdelete.pages,(i,el)=>{if(el.subgroup){el.subgroupBeforeDeletion=el.subgroup;el.subgroup=null}});newPageList=Twinkle.batchdelete.generateNewPageList(form);$("#tw-dbatch-pages").replaceWith(newPageList);Morebits.QuickForm.getElements(newPageList,"pages").forEach(Twinkle.generateArrowLinks)}};Twinkle.batchdelete.callback.evaluate=function twinklebatchdeleteCallbackEvaluate(event){Morebits.wiki.actionCompleted.notice="Batch deletion is now complete";const form=event.target;const numProtected=$(Morebits.QuickForm.getElements(form,"pages")).filter((index,element)=>element.checked&&element.nextElementSibling.style.color==="red").length;if(numProtected>0&&!confirm("You are about to delete "+mw.language.convertNumber(numProtected)+" fully protected page(s). Are you sure?")){return}const input=Morebits.QuickForm.getInputData(form);if(!input.reason){alert("You need to give a reason, you cabal crony!");return}Morebits.SimpleWindow.setButtonsEnabled(false);Morebits.Status.init(form);if(!input.pages||input.pages.length===0){Morebits.Status.error("Error","nothing to delete, aborting");return}const pageDeleter=new Morebits.BatchOperation(input.delete_page?"Deleting pages":"Initiating requested tasks");pageDeleter.setOption("chunkSize",Twinkle.getPref("batchChunks"));pageDeleter.setOption("preserveIndividualStatusLines",input.delete_page);pageDeleter.setPageList(input.pages);pageDeleter.run(pageName=>{const params={page:pageName,delete_page:input.delete_page,delete_talk:input.delete_talk,delete_redirects:input.delete_redirects,unlink_page:input.unlink_page,unlink_file:input.unlink_file&&new RegExp("^"+Morebits.namespaceRegex(6)+":","i").test(pageName),reason:input.reason,pageDeleter:pageDeleter};const wikipediaPage=new Morebits.wiki.Page(pageName,"Deleting page "+pageName);wikipediaPage.setCallbackParameters(params);if(input.delete_page){wikipediaPage.setEditSummary(input.reason);wikipediaPage.setChangeTags(Twinkle.changeTags);wikipediaPage.suppressProtectWarning();wikipediaPage.deletePage(Twinkle.batchdelete.callbacks.doExtras,pageDeleter.workerFailure)}else{Twinkle.batchdelete.callbacks.doExtras(wikipediaPage)}},()=>{if(input.delete_subpages&&input.subpages){const subpageDeleter=new Morebits.BatchOperation("Deleting subpages");subpageDeleter.setOption("chunkSize",Twinkle.getPref("batchChunks"));subpageDeleter.setOption("preserveIndividualStatusLines",true);subpageDeleter.setPageList(input.subpages);subpageDeleter.run(pageName=>{const params={page:pageName,delete_page:true,delete_talk:input.delete_subpage_talks,delete_redirects:input.delete_subpage_redirects,unlink_page:input.unlink_subpages,unlink_file:false,reason:input.reason,pageDeleter:subpageDeleter};const wikipediaPage=new Morebits.wiki.Page(pageName,"Deleting subpage "+pageName);wikipediaPage.setCallbackParameters(params);wikipediaPage.setEditSummary(input.reason);wikipediaPage.setChangeTags(Twinkle.changeTags);wikipediaPage.suppressProtectWarning();wikipediaPage.deletePage(Twinkle.batchdelete.callbacks.doExtras,pageDeleter.workerFailure)})}})};Twinkle.batchdelete.callbacks={doExtras:function(thingWithParameters){const params=thingWithParameters.parent?thingWithParameters.parent.getCallbackParameters():thingWithParameters.getCallbackParameters();params.pageDeleter.workerSuccess(thingWithParameters);let query,wikipediaApi;if(params.unlink_page){Twinkle.batchdelete.unlinkCache={};query={action:"query",list:"backlinks",blfilterredir:"nonredirects",blnamespace:[0,100],bltitle:params.page,bllimit:"max",format:"json"};wikipediaApi=new Morebits.wiki.Api("Grabbing backlinks",query,Twinkle.batchdelete.callbacks.unlinkBacklinksMain);wikipediaApi.params=params;wikipediaApi.post()}if(params.unlink_file){query={action:"query",list:"imageusage",iutitle:params.page,iulimit:"max",format:"json"};wikipediaApi=new Morebits.wiki.Api("Grabbing file links",query,Twinkle.batchdelete.callbacks.unlinkImageInstancesMain);wikipediaApi.params=params;wikipediaApi.post()}if(params.delete_page){if(params.delete_redirects){query={action:"query",titles:params.page,prop:"redirects",rdlimit:"max",format:"json"};wikipediaApi=new Morebits.wiki.Api("Grabbing redirects",query,Twinkle.batchdelete.callbacks.deleteRedirectsMain);wikipediaApi.params=params;wikipediaApi.post()}if(params.delete_talk){const pageTitle=mw.Title.newFromText(params.page);if(pageTitle&&pageTitle.namespace%2===0&&pageTitle.namespace!==2){pageTitle.namespace++;query={action:"query",titles:pageTitle.toText(),format:"json"};wikipediaApi=new Morebits.wiki.Api("Checking whether talk page exists",query,Twinkle.batchdelete.callbacks.deleteTalk);wikipediaApi.params=params;wikipediaApi.params.talkPage=pageTitle.toText();wikipediaApi.post()}}}},deleteRedirectsMain:function(apiobj){const response=apiobj.getResponse();let pages=response.query.pages[0].redirects||[];pages=pages.map(redirect=>redirect.title);if(!pages.length){return}const redirectDeleter=new Morebits.BatchOperation("Deleting redirects to "+apiobj.params.page);redirectDeleter.setOption("chunkSize",Twinkle.getPref("batchChunks"));redirectDeleter.setPageList(pages);redirectDeleter.run(pageName=>{const wikipediaPage=new Morebits.wiki.Page(pageName,"Deleting "+pageName);wikipediaPage.setEditSummary("[[WP:CSD#G8|G8]]: Redirect to deleted page [["+apiobj.params.page+"]]");wikipediaPage.setChangeTags(Twinkle.changeTags);wikipediaPage.deletePage(redirectDeleter.workerSuccess,redirectDeleter.workerFailure)})},deleteTalk:function(apiobj){const response=apiobj.getResponse();if(response.query.pages[0].missing){return}const page=new Morebits.wiki.Page(apiobj.params.talkPage,"Deleting talk page of page "+apiobj.params.page);page.setEditSummary("[[WP:CSD#G8|G8]]: [[Help:Talk page|Talk page]] of deleted page [["+apiobj.params.page+"]]");page.setChangeTags(Twinkle.changeTags);page.deletePage()},unlinkBacklinksMain:function(apiobj){const response=apiobj.getResponse();let pages=response.query.backlinks||[];pages=pages.map(page=>page.title);if(!pages.length){return}const unlinker=new Morebits.BatchOperation("Unlinking backlinks to "+apiobj.params.page);unlinker.setOption("chunkSize",Twinkle.getPref("batchChunks"));unlinker.setPageList(pages);unlinker.run(pageName=>{const wikipediaPage=new Morebits.wiki.Page(pageName,"Unlinking on "+pageName);const params=$.extend({},apiobj.params);params.title=pageName;params.unlinker=unlinker;wikipediaPage.setCallbackParameters(params);wikipediaPage.load(Twinkle.batchdelete.callbacks.unlinkBacklinks)})},unlinkBacklinks:function(pageobj){const params=pageobj.getCallbackParameters();if(!pageobj.exists()){params.unlinker.workerSuccess(pageobj);return}let text;if(params.title in Twinkle.batchdelete.unlinkCache){text=Twinkle.batchdelete.unlinkCache[params.title]}else{text=pageobj.getPageText()}const oldText=text;const wikiPage=new Morebits.wikitext.Page(text);text=wikiPage.removeLink(params.page).getText();Twinkle.batchdelete.unlinkCache[params.title]=text;if(text===oldText){params.unlinker.workerSuccess(pageobj);return}pageobj.setEditSummary("Removing link(s) to deleted page "+params.page);pageobj.setChangeTags(Twinkle.changeTags);pageobj.setPageText(text);pageobj.setCreateOption("nocreate");pageobj.setMaxConflictRetries(10);pageobj.save(params.unlinker.workerSuccess,params.unlinker.workerFailure)},unlinkImageInstancesMain:function(apiobj){const response=apiobj.getResponse();let pages=response.query.imageusage||[];pages=pages.map(page=>page.title);if(!pages.length){return}const unlinker=new Morebits.BatchOperation("Unlinking backlinks to "+apiobj.params.page);unlinker.setOption("chunkSize",Twinkle.getPref("batchChunks"));unlinker.setPageList(pages);unlinker.run(pageName=>{const wikipediaPage=new Morebits.wiki.Page(pageName,"Removing file usages on "+pageName);const params=$.extend({},apiobj.params);params.title=pageName;params.unlinker=unlinker;wikipediaPage.setCallbackParameters(params);wikipediaPage.load(Twinkle.batchdelete.callbacks.unlinkImageInstances)})},unlinkImageInstances:function(pageobj){const params=pageobj.getCallbackParameters();if(!pageobj.exists()){params.unlinker.workerSuccess(pageobj);return}const image=params.page.replace(new RegExp("^"+Morebits.namespaceRegex(6)+":"),"");let text;if(params.title in Twinkle.batchdelete.unlinkCache){text=Twinkle.batchdelete.unlinkCache[params.title]}else{text=pageobj.getPageText()}const oldText=text;const wikiPage=new Morebits.wikitext.Page(text);text=wikiPage.commentOutImage(image,"Commented out because image was deleted").getText();Twinkle.batchdelete.unlinkCache[params.title]=text;if(text===oldText){pageobj.getStatusElement().error("failed to unlink image "+image+" from "+pageobj.getPageName());params.unlinker.workerFailure(pageobj);return}pageobj.setEditSummary("Removing instance of file "+image+' that has been deleted because "'+params.reason+'")');pageobj.setChangeTags(Twinkle.changeTags);pageobj.setPageText(text);pageobj.setCreateOption("nocreate");pageobj.setMaxConflictRetries(10);pageobj.save(params.unlinker.workerSuccess,params.unlinker.workerFailure)}};Twinkle.addInitCallback(Twinkle.batchdelete,"batchdelete")})();
// </nowiki>
al0f3slch4vsdehbigluotb148vf5am
User:Syunsyunminmin/Twinkle/twinklebatchprotect.js
2
179409
769474
2026-10-21T13:58:26Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 3680108:
769474
javascript
text/javascript
/*
Twinkle: adapted for Japanese Wikipedia; upstream authors and Japanese adaptations are credited in UPSTREAM.md.
Upstream source: https://github.com/wikimedia-gadgets/twinkle/blob/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/src/modules/twinklebatchprotect.js
Upstream author history: https://github.com/wikimedia-gadgets/twinkle/commits/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/src/modules/twinklebatchprotect.js
License: CC BY-SA 3.0 Unported. See https://github.com/wikimedia-gadgets/twinkle/blob/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/LICENSE.txt
Japanese adaptation: Syunsyunminmin; includes later synchronization changes.
*/
// <nowiki>
(function() {
/*
****************************************
*** twinklebatchprotect.js: Batch protect module (sysops only)
****************************************
* Mode of invocation: Tab ("P-batch")
* Active on: Existing project pages and user pages; existing and
* non-existing categories; Special:PrefixIndex
*/
Twinkle.batchprotect = function twinklebatchprotect() {
if (Morebits.userIsSysop && ((mw.config.get('wgArticleId') > 0 && (mw.config.get('wgNamespaceNumber') === 2 ||
mw.config.get('wgNamespaceNumber') === 4)) || mw.config.get('wgNamespaceNumber') === 14 ||
mw.config.get('wgCanonicalSpecialPageName') === 'Prefixindex')) {
Twinkle.addPortletLink(Twinkle.batchprotect.callback, 'P-batch', 'tw-pbatch', 'Protect pages linked from this page');
}
};
Twinkle.batchprotect.unlinkCache = {};
Twinkle.batchprotect.callback = function twinklebatchprotectCallback() {
const Window = new Morebits.SimpleWindow(600, 400);
Window.setTitle('Batch protection');
Window.setScriptName('Twinkle');
Window.addFooterLink('Protection policy', 'WP:PROT');
Window.addFooterLink('Twinkle help', 'WP:TW/DOC#protect');
Window.addFooterLink('Give feedback', 'WT:TW');
const form = new Morebits.QuickForm(Twinkle.batchprotect.callback.evaluate);
form.append({
type: 'checkbox',
event: Twinkle.protect.formevents.editmodify,
list: [
{
label: 'Modify edit protection',
value: 'editmodify',
name: 'editmodify',
tooltip: 'Only for existing pages.',
checked: true
}
]
});
form.append({
type: 'select',
name: 'editlevel',
label: 'Edit protection:',
event: Twinkle.protect.formevents.editlevel,
list: Twinkle.protect.protectionLevels
});
form.append({
type: 'select',
name: 'editexpiry',
label: 'Expires:',
event: function(e) {
if (e.target.value === 'custom') {
Twinkle.protect.doCustomExpiry(e.target);
}
},
list: Twinkle.protect.protectionLengths // Default (2 days) set after render
});
form.append({
type: 'checkbox',
event: Twinkle.protect.formevents.movemodify,
list: [
{
label: 'Modify move protection',
value: 'movemodify',
name: 'movemodify',
tooltip: 'Only for existing pages.',
checked: true
}
]
});
form.append({
type: 'select',
name: 'movelevel',
label: 'Move protection:',
event: Twinkle.protect.formevents.movelevel,
// Autoconfirmed is required for a move, redundant
list: Twinkle.protect.protectionLevels.filter((level) => level.value !== 'autoconfirmed')
});
form.append({
type: 'select',
name: 'moveexpiry',
label: 'Expires:',
event: function(e) {
if (e.target.value === 'custom') {
Twinkle.protect.doCustomExpiry(e.target);
}
},
list: Twinkle.protect.protectionLengths // Default (2 days) set after render
});
form.append({
type: 'checkbox',
event: function twinklebatchprotectFormCreatemodifyEvent(e) {
e.target.form.createlevel.disabled = !e.target.checked;
e.target.form.createexpiry.disabled = !e.target.checked || (e.target.form.createlevel.value === 'all');
e.target.form.createlevel.style.color = e.target.form.createexpiry.style.color = e.target.checked ? '' : 'transparent';
},
list: [
{
label: 'Modify create protection',
value: 'createmodify',
name: 'createmodify',
tooltip: 'Only for pages that do not exist.',
checked: true
}
]
});
form.append({
type: 'select',
name: 'createlevel',
label: 'Create protection:',
event: Twinkle.protect.formevents.createlevel,
list: Twinkle.protect.protectionLevels
});
form.append({
type: 'select',
name: 'createexpiry',
label: 'Expires:',
event: function(e) {
if (e.target.value === 'custom') {
Twinkle.protect.doCustomExpiry(e.target);
}
},
list: Twinkle.protect.protectionLengths // Default (indefinite) set after render
});
form.append({
type: 'header',
label: '' // horizontal rule
});
form.append({
type: 'input',
name: 'reason',
label: 'Reason:',
size: 60,
tooltip: 'For the protection log and page history.'
});
const query = {
action: 'query',
prop: 'revisions|info|imageinfo',
rvprop: 'size|user',
inprop: 'protection',
format: 'json'
};
if (mw.config.get('wgNamespaceNumber') === 14) { // categories
query.generator = 'categorymembers';
query.gcmtitle = mw.config.get('wgPageName');
query.gcmlimit = Twinkle.getPref('batchMax');
} else if (mw.config.get('wgCanonicalSpecialPageName') === 'Prefixindex') {
query.generator = 'allpages';
query.gapnamespace = mw.util.getParamValue('namespace') || $('select[name=namespace]').val();
query.gapprefix = mw.util.getParamValue('prefix') || $('input[name=prefix]').val();
query.gaplimit = Twinkle.getPref('batchMax');
} else {
query.generator = 'links';
query.titles = mw.config.get('wgPageName');
query.gpllimit = Twinkle.getPref('batchMax');
}
const statusdiv = document.createElement('div');
statusdiv.style.padding = '15px'; // just so it doesn't look broken
Window.setContent(statusdiv);
Morebits.Status.init(statusdiv);
Window.display();
const statelem = new Morebits.Status('Grabbing list of pages');
const wikipediaApi = new Morebits.wiki.Api('loading...', query, ((apiobj) => {
const response = apiobj.getResponse();
const pages = (response.query && response.query.pages) || [];
const list = [];
pages.sort(Twinkle.sortByNamespace);
pages.forEach((page) => {
const metadata = [];
const missing = !!page.missing;
let editProt;
if (missing) {
metadata.push('page does not exist');
editProt = page.protection.filter((pr) => pr.type === 'create' && pr.level === 'sysop').pop();
} else {
if (page.redirect) {
metadata.push('redirect');
}
if (page.ns === 6) {
metadata.push('uploader: ' + page.imageinfo[0].user);
metadata.push('last edit from: ' + page.revisions[0].user);
} else {
metadata.push(mw.language.convertNumber(page.revisions[0].size) + ' bytes');
}
editProt = page.protection
.filter((pr) => pr.type === 'edit' && pr.level === 'sysop')
.pop();
}
if (editProt) {
metadata.push('fully' + (missing ? ' create' : '') + ' protected' +
(editProt.expiry === 'infinity' ? ' indefinitely' : ', expires ' + new Morebits.Date(editProt.expiry).calendar('utc') + ' (UTC)'));
}
const title = page.title;
list.push({ label: title + (metadata.length ? ' (' + metadata.join('; ') + ')' : ''), value: title, checked: true, style: editProt ? 'color:red' : '' });
});
form.append({ type: 'header', label: 'Pages to protect' });
form.append({
type: 'button',
label: 'Select All',
event: function(e) {
$(Morebits.QuickForm.getElements(e.target.form, 'pages')).prop('checked', true);
}
});
form.append({
type: 'button',
label: 'Deselect All',
event: function(e) {
$(Morebits.QuickForm.getElements(e.target.form, 'pages')).prop('checked', false);
}
});
form.append({
type: 'checkbox',
name: 'pages',
shiftClickSupport: true,
list: list
});
form.append({ type: 'submit' });
const result = form.render();
Window.setContent(result);
// Set defaults
result.editexpiry.value = '2 days';
result.moveexpiry.value = '2 days';
result.createexpiry.value = 'infinity';
Morebits.QuickForm.getElements(result, 'pages').forEach(Twinkle.generateArrowLinks);
}), statelem);
wikipediaApi.post();
};
Twinkle.batchprotect.currentProtectCounter = 0;
Twinkle.batchprotect.currentprotector = 0;
Twinkle.batchprotect.callback.evaluate = function twinklebatchprotectCallbackEvaluate(event) {
Morebits.wiki.actionCompleted.notice = 'Batch protection is now complete';
const form = event.target;
const numProtected = $(Morebits.QuickForm.getElements(form, 'pages'))
.filter((index, element) => element.checked && element.nextElementSibling.style.color === 'red')
.length;
if (numProtected > 0 && !confirm('You are about to act on ' + mw.language.convertNumber(numProtected) + ' fully protected page(s). Are you sure?')) {
return;
}
const input = Morebits.QuickForm.getInputData(form);
if (!input.reason) {
alert("You've got to give a reason, you rouge admin!");
return;
}
Morebits.SimpleWindow.setButtonsEnabled(false);
Morebits.Status.init(form);
if (input.pages.length === 0) {
Morebits.Status.error('Error', 'Nothing to protect, aborting');
return;
}
const batchOperation = new Morebits.BatchOperation('Applying protection settings');
batchOperation.setOption('chunkSize', Twinkle.getPref('batchChunks'));
batchOperation.setOption('preserveIndividualStatusLines', true);
batchOperation.setPageList(input.pages);
batchOperation.run((pageName) => {
const query = {
action: 'query',
titles: pageName,
format: 'json'
};
const wikipediaApi = new Morebits.wiki.Api('Checking if page ' + pageName + ' exists', query,
Twinkle.batchprotect.callbacks.main, null, batchOperation.workerFailure);
wikipediaApi.params = $.extend({
page: pageName,
batchOperation: batchOperation
}, input);
wikipediaApi.post();
});
};
Twinkle.batchprotect.callbacks = {
main: function(apiobj) {
const response = apiobj.getResponse();
if (response.query.normalized) {
apiobj.params.page = response.query.normalized[0].to;
}
const exists = !response.query.pages[0].missing;
const page = new Morebits.wiki.Page(apiobj.params.page, 'Protecting ' + apiobj.params.page);
let takenAction = false;
if (exists && apiobj.params.editmodify) {
page.setEditProtection(apiobj.params.editlevel, apiobj.params.editexpiry);
takenAction = true;
}
if (exists && apiobj.params.movemodify) {
page.setMoveProtection(apiobj.params.movelevel, apiobj.params.moveexpiry);
takenAction = true;
}
if (!exists && apiobj.params.createmodify) {
page.setCreateProtection(apiobj.params.createlevel, apiobj.params.createexpiry);
takenAction = true;
}
if (!takenAction) {
Morebits.Status.warn('Protecting ' + apiobj.params.page, 'page ' + (exists ? 'exists' : 'does not exist') + '; nothing to do, skipping');
apiobj.params.batchOperation.workerFailure(apiobj);
return;
}
page.setEditSummary(apiobj.params.reason);
page.setChangeTags(Twinkle.changeTags);
page.protect(apiobj.params.batchOperation.workerSuccess, apiobj.params.batchOperation.workerFailure);
}
};
Twinkle.addInitCallback(Twinkle.batchprotect, 'batchprotect');
}());
// </nowiki>
t9ulk2n96eyv7p2q7c0qa78pl468hz6
769541
769474
2026-10-22T00:01:14Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 1406828: rm cmt
769541
javascript
text/javascript
// <nowiki>
(function() {
/*
****************************************
*** twinklebatchprotect.js: Batch protect module (sysops only)
****************************************
* Mode of invocation: Tab ("P-batch")
* Active on: Existing project pages and user pages; existing and
* non-existing categories; Special:PrefixIndex
*/
Twinkle.batchprotect = function twinklebatchprotect() {
if (Morebits.userIsSysop && ((mw.config.get('wgArticleId') > 0 && (mw.config.get('wgNamespaceNumber') === 2 ||
mw.config.get('wgNamespaceNumber') === 4)) || mw.config.get('wgNamespaceNumber') === 14 ||
mw.config.get('wgCanonicalSpecialPageName') === 'Prefixindex')) {
Twinkle.addPortletLink(Twinkle.batchprotect.callback, 'P-batch', 'tw-pbatch', 'Protect pages linked from this page');
}
};
Twinkle.batchprotect.unlinkCache = {};
Twinkle.batchprotect.callback = function twinklebatchprotectCallback() {
const Window = new Morebits.SimpleWindow(600, 400);
Window.setTitle('Batch protection');
Window.setScriptName('Twinkle');
Window.addFooterLink('Protection policy', 'WP:PROT');
Window.addFooterLink('Twinkle help', 'WP:TW/DOC#protect');
Window.addFooterLink('Give feedback', 'WT:TW');
const form = new Morebits.QuickForm(Twinkle.batchprotect.callback.evaluate);
form.append({
type: 'checkbox',
event: Twinkle.protect.formevents.editmodify,
list: [
{
label: 'Modify edit protection',
value: 'editmodify',
name: 'editmodify',
tooltip: 'Only for existing pages.',
checked: true
}
]
});
form.append({
type: 'select',
name: 'editlevel',
label: 'Edit protection:',
event: Twinkle.protect.formevents.editlevel,
list: Twinkle.protect.protectionLevels
});
form.append({
type: 'select',
name: 'editexpiry',
label: 'Expires:',
event: function(e) {
if (e.target.value === 'custom') {
Twinkle.protect.doCustomExpiry(e.target);
}
},
list: Twinkle.protect.protectionLengths // Default (2 days) set after render
});
form.append({
type: 'checkbox',
event: Twinkle.protect.formevents.movemodify,
list: [
{
label: 'Modify move protection',
value: 'movemodify',
name: 'movemodify',
tooltip: 'Only for existing pages.',
checked: true
}
]
});
form.append({
type: 'select',
name: 'movelevel',
label: 'Move protection:',
event: Twinkle.protect.formevents.movelevel,
// Autoconfirmed is required for a move, redundant
list: Twinkle.protect.protectionLevels.filter((level) => level.value !== 'autoconfirmed')
});
form.append({
type: 'select',
name: 'moveexpiry',
label: 'Expires:',
event: function(e) {
if (e.target.value === 'custom') {
Twinkle.protect.doCustomExpiry(e.target);
}
},
list: Twinkle.protect.protectionLengths // Default (2 days) set after render
});
form.append({
type: 'checkbox',
event: function twinklebatchprotectFormCreatemodifyEvent(e) {
e.target.form.createlevel.disabled = !e.target.checked;
e.target.form.createexpiry.disabled = !e.target.checked || (e.target.form.createlevel.value === 'all');
e.target.form.createlevel.style.color = e.target.form.createexpiry.style.color = e.target.checked ? '' : 'transparent';
},
list: [
{
label: 'Modify create protection',
value: 'createmodify',
name: 'createmodify',
tooltip: 'Only for pages that do not exist.',
checked: true
}
]
});
form.append({
type: 'select',
name: 'createlevel',
label: 'Create protection:',
event: Twinkle.protect.formevents.createlevel,
list: Twinkle.protect.protectionLevels
});
form.append({
type: 'select',
name: 'createexpiry',
label: 'Expires:',
event: function(e) {
if (e.target.value === 'custom') {
Twinkle.protect.doCustomExpiry(e.target);
}
},
list: Twinkle.protect.protectionLengths // Default (indefinite) set after render
});
form.append({
type: 'header',
label: '' // horizontal rule
});
form.append({
type: 'input',
name: 'reason',
label: 'Reason:',
size: 60,
tooltip: 'For the protection log and page history.'
});
const query = {
action: 'query',
prop: 'revisions|info|imageinfo',
rvprop: 'size|user',
inprop: 'protection',
format: 'json'
};
if (mw.config.get('wgNamespaceNumber') === 14) { // categories
query.generator = 'categorymembers';
query.gcmtitle = mw.config.get('wgPageName');
query.gcmlimit = Twinkle.getPref('batchMax');
} else if (mw.config.get('wgCanonicalSpecialPageName') === 'Prefixindex') {
query.generator = 'allpages';
query.gapnamespace = mw.util.getParamValue('namespace') || $('select[name=namespace]').val();
query.gapprefix = mw.util.getParamValue('prefix') || $('input[name=prefix]').val();
query.gaplimit = Twinkle.getPref('batchMax');
} else {
query.generator = 'links';
query.titles = mw.config.get('wgPageName');
query.gpllimit = Twinkle.getPref('batchMax');
}
const statusdiv = document.createElement('div');
statusdiv.style.padding = '15px'; // just so it doesn't look broken
Window.setContent(statusdiv);
Morebits.Status.init(statusdiv);
Window.display();
const statelem = new Morebits.Status('Grabbing list of pages');
const wikipediaApi = new Morebits.wiki.Api('loading...', query, ((apiobj) => {
const response = apiobj.getResponse();
const pages = (response.query && response.query.pages) || [];
const list = [];
pages.sort(Twinkle.sortByNamespace);
pages.forEach((page) => {
const metadata = [];
const missing = !!page.missing;
let editProt;
if (missing) {
metadata.push('page does not exist');
editProt = page.protection.filter((pr) => pr.type === 'create' && pr.level === 'sysop').pop();
} else {
if (page.redirect) {
metadata.push('redirect');
}
if (page.ns === 6) {
metadata.push('uploader: ' + page.imageinfo[0].user);
metadata.push('last edit from: ' + page.revisions[0].user);
} else {
metadata.push(mw.language.convertNumber(page.revisions[0].size) + ' bytes');
}
editProt = page.protection
.filter((pr) => pr.type === 'edit' && pr.level === 'sysop')
.pop();
}
if (editProt) {
metadata.push('fully' + (missing ? ' create' : '') + ' protected' +
(editProt.expiry === 'infinity' ? ' indefinitely' : ', expires ' + new Morebits.Date(editProt.expiry).calendar('utc') + ' (UTC)'));
}
const title = page.title;
list.push({ label: title + (metadata.length ? ' (' + metadata.join('; ') + ')' : ''), value: title, checked: true, style: editProt ? 'color:red' : '' });
});
form.append({ type: 'header', label: 'Pages to protect' });
form.append({
type: 'button',
label: 'Select All',
event: function(e) {
$(Morebits.QuickForm.getElements(e.target.form, 'pages')).prop('checked', true);
}
});
form.append({
type: 'button',
label: 'Deselect All',
event: function(e) {
$(Morebits.QuickForm.getElements(e.target.form, 'pages')).prop('checked', false);
}
});
form.append({
type: 'checkbox',
name: 'pages',
shiftClickSupport: true,
list: list
});
form.append({ type: 'submit' });
const result = form.render();
Window.setContent(result);
// Set defaults
result.editexpiry.value = '2 days';
result.moveexpiry.value = '2 days';
result.createexpiry.value = 'infinity';
Morebits.QuickForm.getElements(result, 'pages').forEach(Twinkle.generateArrowLinks);
}), statelem);
wikipediaApi.post();
};
Twinkle.batchprotect.currentProtectCounter = 0;
Twinkle.batchprotect.currentprotector = 0;
Twinkle.batchprotect.callback.evaluate = function twinklebatchprotectCallbackEvaluate(event) {
Morebits.wiki.actionCompleted.notice = 'Batch protection is now complete';
const form = event.target;
const numProtected = $(Morebits.QuickForm.getElements(form, 'pages'))
.filter((index, element) => element.checked && element.nextElementSibling.style.color === 'red')
.length;
if (numProtected > 0 && !confirm('You are about to act on ' + mw.language.convertNumber(numProtected) + ' fully protected page(s). Are you sure?')) {
return;
}
const input = Morebits.QuickForm.getInputData(form);
if (!input.reason) {
alert("You've got to give a reason, you rouge admin!");
return;
}
Morebits.SimpleWindow.setButtonsEnabled(false);
Morebits.Status.init(form);
if (input.pages.length === 0) {
Morebits.Status.error('Error', 'Nothing to protect, aborting');
return;
}
const batchOperation = new Morebits.BatchOperation('Applying protection settings');
batchOperation.setOption('chunkSize', Twinkle.getPref('batchChunks'));
batchOperation.setOption('preserveIndividualStatusLines', true);
batchOperation.setPageList(input.pages);
batchOperation.run((pageName) => {
const query = {
action: 'query',
titles: pageName,
format: 'json'
};
const wikipediaApi = new Morebits.wiki.Api('Checking if page ' + pageName + ' exists', query,
Twinkle.batchprotect.callbacks.main, null, batchOperation.workerFailure);
wikipediaApi.params = $.extend({
page: pageName,
batchOperation: batchOperation
}, input);
wikipediaApi.post();
});
};
Twinkle.batchprotect.callbacks = {
main: function(apiobj) {
const response = apiobj.getResponse();
if (response.query.normalized) {
apiobj.params.page = response.query.normalized[0].to;
}
const exists = !response.query.pages[0].missing;
const page = new Morebits.wiki.Page(apiobj.params.page, 'Protecting ' + apiobj.params.page);
let takenAction = false;
if (exists && apiobj.params.editmodify) {
page.setEditProtection(apiobj.params.editlevel, apiobj.params.editexpiry);
takenAction = true;
}
if (exists && apiobj.params.movemodify) {
page.setMoveProtection(apiobj.params.movelevel, apiobj.params.moveexpiry);
takenAction = true;
}
if (!exists && apiobj.params.createmodify) {
page.setCreateProtection(apiobj.params.createlevel, apiobj.params.createexpiry);
takenAction = true;
}
if (!takenAction) {
Morebits.Status.warn('Protecting ' + apiobj.params.page, 'page ' + (exists ? 'exists' : 'does not exist') + '; nothing to do, skipping');
apiobj.params.batchOperation.workerFailure(apiobj);
return;
}
page.setEditSummary(apiobj.params.reason);
page.setChangeTags(Twinkle.changeTags);
page.protect(apiobj.params.batchOperation.workerSuccess, apiobj.params.batchOperation.workerFailure);
}
};
Twinkle.addInitCallback(Twinkle.batchprotect, 'batchprotect');
}());
// </nowiki>
l2n57qlui8itw8h98ramboy5ezh8ccd
769578
769541
2026-10-22T02:32:53Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at d9cd3461baca43ac99ac2c24fb76a3aae6ddada9: minify
769578
javascript
text/javascript
// <nowiki>
(function(){Twinkle.batchprotect=function twinklebatchprotect(){if(Morebits.userIsSysop&&(mw.config.get("wgArticleId")>0&&(mw.config.get("wgNamespaceNumber")===2||mw.config.get("wgNamespaceNumber")===4)||mw.config.get("wgNamespaceNumber")===14||mw.config.get("wgCanonicalSpecialPageName")==="Prefixindex")){Twinkle.addPortletLink(Twinkle.batchprotect.callback,"P-batch","tw-pbatch","Protect pages linked from this page")}};Twinkle.batchprotect.unlinkCache={};Twinkle.batchprotect.callback=function twinklebatchprotectCallback(){const Window=new Morebits.SimpleWindow(600,400);Window.setTitle("Batch protection");Window.setScriptName("Twinkle");Window.addFooterLink("Protection policy","WP:PROT");Window.addFooterLink("Twinkle help","WP:TW/DOC#protect");Window.addFooterLink("Give feedback","WT:TW");const form=new Morebits.QuickForm(Twinkle.batchprotect.callback.evaluate);form.append({type:"checkbox",event:Twinkle.protect.formevents.editmodify,list:[{label:"Modify edit protection",value:"editmodify",name:"editmodify",tooltip:"Only for existing pages.",checked:true}]});form.append({type:"select",name:"editlevel",label:"Edit protection:",event:Twinkle.protect.formevents.editlevel,list:Twinkle.protect.protectionLevels});form.append({type:"select",name:"editexpiry",label:"Expires:",event:function(e){if(e.target.value==="custom"){Twinkle.protect.doCustomExpiry(e.target)}},list:Twinkle.protect.protectionLengths});form.append({type:"checkbox",event:Twinkle.protect.formevents.movemodify,list:[{label:"Modify move protection",value:"movemodify",name:"movemodify",tooltip:"Only for existing pages.",checked:true}]});form.append({type:"select",name:"movelevel",label:"Move protection:",event:Twinkle.protect.formevents.movelevel,list:Twinkle.protect.protectionLevels.filter(level=>level.value!=="autoconfirmed")});form.append({type:"select",name:"moveexpiry",label:"Expires:",event:function(e){if(e.target.value==="custom"){Twinkle.protect.doCustomExpiry(e.target)}},list:Twinkle.protect.protectionLengths});form.append({type:"checkbox",event:function twinklebatchprotectFormCreatemodifyEvent(e){e.target.form.createlevel.disabled=!e.target.checked;e.target.form.createexpiry.disabled=!e.target.checked||e.target.form.createlevel.value==="all";e.target.form.createlevel.style.color=e.target.form.createexpiry.style.color=e.target.checked?"":"transparent"},list:[{label:"Modify create protection",value:"createmodify",name:"createmodify",tooltip:"Only for pages that do not exist.",checked:true}]});form.append({type:"select",name:"createlevel",label:"Create protection:",event:Twinkle.protect.formevents.createlevel,list:Twinkle.protect.protectionLevels});form.append({type:"select",name:"createexpiry",label:"Expires:",event:function(e){if(e.target.value==="custom"){Twinkle.protect.doCustomExpiry(e.target)}},list:Twinkle.protect.protectionLengths});form.append({type:"header",label:""});form.append({type:"input",name:"reason",label:"Reason:",size:60,tooltip:"For the protection log and page history."});const query={action:"query",prop:"revisions|info|imageinfo",rvprop:"size|user",inprop:"protection",format:"json"};if(mw.config.get("wgNamespaceNumber")===14){query.generator="categorymembers";query.gcmtitle=mw.config.get("wgPageName");query.gcmlimit=Twinkle.getPref("batchMax")}else if(mw.config.get("wgCanonicalSpecialPageName")==="Prefixindex"){query.generator="allpages";query.gapnamespace=mw.util.getParamValue("namespace")||$("select[name=namespace]").val();query.gapprefix=mw.util.getParamValue("prefix")||$("input[name=prefix]").val();query.gaplimit=Twinkle.getPref("batchMax")}else{query.generator="links";query.titles=mw.config.get("wgPageName");query.gpllimit=Twinkle.getPref("batchMax")}const statusdiv=document.createElement("div");statusdiv.style.padding="15px";Window.setContent(statusdiv);Morebits.Status.init(statusdiv);Window.display();const statelem=new Morebits.Status("Grabbing list of pages");const wikipediaApi=new Morebits.wiki.Api("loading...",query,apiobj=>{const response=apiobj.getResponse();const pages=response.query&&response.query.pages||[];const list=[];pages.sort(Twinkle.sortByNamespace);pages.forEach(page=>{const metadata=[];const missing=!!page.missing;let editProt;if(missing){metadata.push("page does not exist");editProt=page.protection.filter(pr=>pr.type==="create"&&pr.level==="sysop").pop()}else{if(page.redirect){metadata.push("redirect")}if(page.ns===6){metadata.push("uploader: "+page.imageinfo[0].user);metadata.push("last edit from: "+page.revisions[0].user)}else{metadata.push(mw.language.convertNumber(page.revisions[0].size)+" bytes")}editProt=page.protection.filter(pr=>pr.type==="edit"&&pr.level==="sysop").pop()}if(editProt){metadata.push("fully"+(missing?" create":"")+" protected"+(editProt.expiry==="infinity"?" indefinitely":", expires "+new Morebits.Date(editProt.expiry).calendar("utc")+" (UTC)"))}const title=page.title;list.push({label:title+(metadata.length?" ("+metadata.join("; ")+")":""),value:title,checked:true,style:editProt?"color:red":""})});form.append({type:"header",label:"Pages to protect"});form.append({type:"button",label:"Select All",event:function(e){$(Morebits.QuickForm.getElements(e.target.form,"pages")).prop("checked",true)}});form.append({type:"button",label:"Deselect All",event:function(e){$(Morebits.QuickForm.getElements(e.target.form,"pages")).prop("checked",false)}});form.append({type:"checkbox",name:"pages",shiftClickSupport:true,list:list});form.append({type:"submit"});const result=form.render();Window.setContent(result);result.editexpiry.value="2 days";result.moveexpiry.value="2 days";result.createexpiry.value="infinity";Morebits.QuickForm.getElements(result,"pages").forEach(Twinkle.generateArrowLinks)},statelem);wikipediaApi.post()};Twinkle.batchprotect.currentProtectCounter=0;Twinkle.batchprotect.currentprotector=0;Twinkle.batchprotect.callback.evaluate=function twinklebatchprotectCallbackEvaluate(event){Morebits.wiki.actionCompleted.notice="Batch protection is now complete";const form=event.target;const numProtected=$(Morebits.QuickForm.getElements(form,"pages")).filter((index,element)=>element.checked&&element.nextElementSibling.style.color==="red").length;if(numProtected>0&&!confirm("You are about to act on "+mw.language.convertNumber(numProtected)+" fully protected page(s). Are you sure?")){return}const input=Morebits.QuickForm.getInputData(form);if(!input.reason){alert("You've got to give a reason, you rouge admin!");return}Morebits.SimpleWindow.setButtonsEnabled(false);Morebits.Status.init(form);if(input.pages.length===0){Morebits.Status.error("Error","Nothing to protect, aborting");return}const batchOperation=new Morebits.BatchOperation("Applying protection settings");batchOperation.setOption("chunkSize",Twinkle.getPref("batchChunks"));batchOperation.setOption("preserveIndividualStatusLines",true);batchOperation.setPageList(input.pages);batchOperation.run(pageName=>{const query={action:"query",titles:pageName,format:"json"};const wikipediaApi=new Morebits.wiki.Api("Checking if page "+pageName+" exists",query,Twinkle.batchprotect.callbacks.main,null,batchOperation.workerFailure);wikipediaApi.params=$.extend({page:pageName,batchOperation:batchOperation},input);wikipediaApi.post()})};Twinkle.batchprotect.callbacks={main:function(apiobj){const response=apiobj.getResponse();if(response.query.normalized){apiobj.params.page=response.query.normalized[0].to}const exists=!response.query.pages[0].missing;const page=new Morebits.wiki.Page(apiobj.params.page,"Protecting "+apiobj.params.page);let takenAction=false;if(exists&&apiobj.params.editmodify){page.setEditProtection(apiobj.params.editlevel,apiobj.params.editexpiry);takenAction=true}if(exists&&apiobj.params.movemodify){page.setMoveProtection(apiobj.params.movelevel,apiobj.params.moveexpiry);takenAction=true}if(!exists&&apiobj.params.createmodify){page.setCreateProtection(apiobj.params.createlevel,apiobj.params.createexpiry);takenAction=true}if(!takenAction){Morebits.Status.warn("Protecting "+apiobj.params.page,"page "+(exists?"exists":"does not exist")+"; nothing to do, skipping");apiobj.params.batchOperation.workerFailure(apiobj);return}page.setEditSummary(apiobj.params.reason);page.setChangeTags(Twinkle.changeTags);page.protect(apiobj.params.batchOperation.workerSuccess,apiobj.params.batchOperation.workerFailure)}};Twinkle.addInitCallback(Twinkle.batchprotect,"batchprotect")})();
// </nowiki>
gv157mqe18xybbqnyh70ccoid5ed1dk
User:Syunsyunminmin/Twinkle/twinklebatchundelete.js
2
179410
769475
2026-10-21T13:58:28Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 3680108:
769475
javascript
text/javascript
/*
Twinkle: adapted for Japanese Wikipedia; upstream authors and Japanese adaptations are credited in UPSTREAM.md.
Upstream source: https://github.com/wikimedia-gadgets/twinkle/blob/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/src/modules/twinklebatchundelete.js
Upstream author history: https://github.com/wikimedia-gadgets/twinkle/commits/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/src/modules/twinklebatchundelete.js
License: CC BY-SA 3.0 Unported. See https://github.com/wikimedia-gadgets/twinkle/blob/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/LICENSE.txt
Japanese adaptation: Syunsyunminmin; includes later synchronization changes.
*/
// <nowiki>
(function() {
/*
****************************************
*** twinklebatchundelete.js: Batch undelete module
****************************************
* Mode of invocation: Tab ("Und-batch")
* Active on: Existing user and project pages
*/
Twinkle.batchundelete = function twinklebatchundelete() {
if (!Morebits.userIsSysop || !mw.config.get('wgArticleId') || (
mw.config.get('wgNamespaceNumber') !== mw.config.get('wgNamespaceIds').user &&
mw.config.get('wgNamespaceNumber') !== mw.config.get('wgNamespaceIds').project)) {
return;
}
Twinkle.addPortletLink(Twinkle.batchundelete.callback, 'Und-batch', 'tw-batch-undel', "Undelete 'em all");
};
Twinkle.batchundelete.callback = function twinklebatchundeleteCallback() {
const Window = new Morebits.SimpleWindow(600, 400);
Window.setScriptName('Twinkle');
Window.setTitle('Batch undelete');
Window.addFooterLink('Twinkle help', 'WP:TW/DOC#batchundelete');
Window.addFooterLink('Give feedback', 'WT:TW');
const form = new Morebits.QuickForm(Twinkle.batchundelete.callback.evaluate);
form.append({
type: 'checkbox',
list: [
{
label: 'Restore talk pages of undeleted pages if they existed',
name: 'undel_talk',
value: 'undel_talk',
checked: true
}
]
});
form.append({
type: 'input',
name: 'reason',
label: 'Reason:',
size: 60
});
const statusdiv = document.createElement('div');
statusdiv.style.padding = '15px'; // just so it doesn't look broken
Window.setContent(statusdiv);
Morebits.Status.init(statusdiv);
Window.display();
const query = {
action: 'query',
generator: 'links',
prop: 'info',
inprop: 'protection',
titles: mw.config.get('wgPageName'),
gpllimit: Twinkle.getPref('batchMax'),
format: 'json'
};
const statelem = new Morebits.Status('Grabbing list of pages');
const wikipediaApi = new Morebits.wiki.Api('loading...', query, ((apiobj) => {
const response = apiobj.getResponse();
let pages = (response.query && response.query.pages) || [];
pages = pages.filter((page) => page.missing);
const list = [];
pages.sort(Twinkle.sortByNamespace);
pages.forEach((page) => {
const editProt = page.protection.filter((pr) => pr.type === 'create' && pr.level === 'sysop').pop();
const title = page.title;
list.push({
label: title + (editProt ? ' (fully create protected' +
(editProt.expiry === 'infinity' ? ' indefinitely' : ', expires ' + new Morebits.Date(editProt.expiry).calendar('utc') + ' (UTC)') + ')' : ''),
value: title,
checked: true,
style: editProt ? 'color:red' : ''
});
});
apiobj.params.form.append({ type: 'header', label: 'Pages to undelete' });
apiobj.params.form.append({
type: 'button',
label: 'Select All',
event: function(e) {
$(Morebits.QuickForm.getElements(e.target.form, 'pages')).prop('checked', true);
}
});
apiobj.params.form.append({
type: 'button',
label: 'Deselect All',
event: function(e) {
$(Morebits.QuickForm.getElements(e.target.form, 'pages')).prop('checked', false);
}
});
apiobj.params.form.append({
type: 'checkbox',
name: 'pages',
shiftClickSupport: true,
list: list
});
apiobj.params.form.append({ type: 'submit' });
const result = apiobj.params.form.render();
apiobj.params.Window.setContent(result);
Morebits.QuickForm.getElements(result, 'pages').forEach(Twinkle.generateArrowLinks);
}), statelem);
wikipediaApi.params = { form: form, Window: Window };
wikipediaApi.post();
};
Twinkle.batchundelete.callback.evaluate = function(event) {
Morebits.wiki.actionCompleted.notice = 'Batch undeletion is now complete';
const numProtected = Morebits.QuickForm.getElements(event.target, 'pages').filter((element) => element.checked && element.nextElementSibling.style.color === 'red').length;
if (numProtected > 0 && !confirm('You are about to undelete ' + numProtected + ' fully create protected page(s). Are you sure?')) {
return;
}
const input = Morebits.QuickForm.getInputData(event.target);
if (!input.reason) {
alert('You need to give a reason, you cabal crony!');
return;
}
Morebits.SimpleWindow.setButtonsEnabled(false);
Morebits.Status.init(event.target);
if (!input.pages || !input.pages.length) {
Morebits.Status.error('Error', 'nothing to undelete, aborting');
return;
}
const pageUndeleter = new Morebits.BatchOperation('Undeleting pages');
pageUndeleter.setOption('chunkSize', Twinkle.getPref('batchChunks'));
pageUndeleter.setOption('preserveIndividualStatusLines', true);
pageUndeleter.setPageList(input.pages);
pageUndeleter.run((pageName) => {
const params = {
page: pageName,
undel_talk: input.undel_talk,
reason: input.reason,
pageUndeleter: pageUndeleter
};
const wikipediaPage = new Morebits.wiki.Page(pageName, 'Undeleting page ' + pageName);
wikipediaPage.setCallbackParameters(params);
wikipediaPage.setEditSummary(input.reason);
wikipediaPage.setChangeTags(Twinkle.changeTags);
wikipediaPage.suppressProtectWarning();
wikipediaPage.setMaxRetries(3); // temporary increase from 2 to make batchundelete more likely to succeed [[phab:T222402]] #613
wikipediaPage.undeletePage(Twinkle.batchundelete.callbacks.doExtras, pageUndeleter.workerFailure);
});
};
Twinkle.batchundelete.callbacks = {
// this stupid parameter name is a temporary thing until I implement an overhaul
// of Morebits.wiki.* callback parameters
doExtras: function(thingWithParameters) {
const params = thingWithParameters.parent ? thingWithParameters.parent.getCallbackParameters() :
thingWithParameters.getCallbackParameters();
// the initial batch operation's job is to delete the page, and that has
// succeeded by now
params.pageUndeleter.workerSuccess(thingWithParameters);
let query, wikipediaApi;
if (params.undel_talk) {
const talkpagename = new mw.Title(params.page).getTalkPage().getPrefixedText();
if (talkpagename !== params.page) {
query = {
action: 'query',
prop: 'deletedrevisions',
drvprop: 'ids',
drvlimit: 1,
titles: talkpagename,
format: 'json'
};
wikipediaApi = new Morebits.wiki.Api('Checking talk page for deleted revisions', query, Twinkle.batchundelete.callbacks.undeleteTalk);
wikipediaApi.params = params;
wikipediaApi.params.talkPage = talkpagename;
wikipediaApi.post();
}
}
},
undeleteTalk: function(apiobj) {
const page = apiobj.getResponse().query.pages[0];
const exists = !page.missing;
const delrevs = page.deletedrevisions && page.deletedrevisions[0].revid;
if (exists || !delrevs) {
// page exists or has no deleted revisions; forget about it
return;
}
const talkpage = new Morebits.wiki.Page(apiobj.params.talkPage, 'Undeleting talk page of ' + apiobj.params.page);
talkpage.setEditSummary('Undeleting [[Help:Talk page|talk page]] of "' + apiobj.params.page + '"');
talkpage.setChangeTags(Twinkle.changeTags);
talkpage.undeletePage();
}
};
Twinkle.addInitCallback(Twinkle.batchundelete, 'batchundelete');
}());
// </nowiki>
l5doc1mqe1kgv62qltmaq14tcpatriq
769542
769475
2026-10-22T00:01:18Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 1406828: rm cmt
769542
javascript
text/javascript
// <nowiki>
(function() {
/*
****************************************
*** twinklebatchundelete.js: Batch undelete module
****************************************
* Mode of invocation: Tab ("Und-batch")
* Active on: Existing user and project pages
*/
Twinkle.batchundelete = function twinklebatchundelete() {
if (!Morebits.userIsSysop || !mw.config.get('wgArticleId') || (
mw.config.get('wgNamespaceNumber') !== mw.config.get('wgNamespaceIds').user &&
mw.config.get('wgNamespaceNumber') !== mw.config.get('wgNamespaceIds').project)) {
return;
}
Twinkle.addPortletLink(Twinkle.batchundelete.callback, 'Und-batch', 'tw-batch-undel', "Undelete 'em all");
};
Twinkle.batchundelete.callback = function twinklebatchundeleteCallback() {
const Window = new Morebits.SimpleWindow(600, 400);
Window.setScriptName('Twinkle');
Window.setTitle('Batch undelete');
Window.addFooterLink('Twinkle help', 'WP:TW/DOC#batchundelete');
Window.addFooterLink('Give feedback', 'WT:TW');
const form = new Morebits.QuickForm(Twinkle.batchundelete.callback.evaluate);
form.append({
type: 'checkbox',
list: [
{
label: 'Restore talk pages of undeleted pages if they existed',
name: 'undel_talk',
value: 'undel_talk',
checked: true
}
]
});
form.append({
type: 'input',
name: 'reason',
label: 'Reason:',
size: 60
});
const statusdiv = document.createElement('div');
statusdiv.style.padding = '15px'; // just so it doesn't look broken
Window.setContent(statusdiv);
Morebits.Status.init(statusdiv);
Window.display();
const query = {
action: 'query',
generator: 'links',
prop: 'info',
inprop: 'protection',
titles: mw.config.get('wgPageName'),
gpllimit: Twinkle.getPref('batchMax'),
format: 'json'
};
const statelem = new Morebits.Status('Grabbing list of pages');
const wikipediaApi = new Morebits.wiki.Api('loading...', query, ((apiobj) => {
const response = apiobj.getResponse();
let pages = (response.query && response.query.pages) || [];
pages = pages.filter((page) => page.missing);
const list = [];
pages.sort(Twinkle.sortByNamespace);
pages.forEach((page) => {
const editProt = page.protection.filter((pr) => pr.type === 'create' && pr.level === 'sysop').pop();
const title = page.title;
list.push({
label: title + (editProt ? ' (fully create protected' +
(editProt.expiry === 'infinity' ? ' indefinitely' : ', expires ' + new Morebits.Date(editProt.expiry).calendar('utc') + ' (UTC)') + ')' : ''),
value: title,
checked: true,
style: editProt ? 'color:red' : ''
});
});
apiobj.params.form.append({ type: 'header', label: 'Pages to undelete' });
apiobj.params.form.append({
type: 'button',
label: 'Select All',
event: function(e) {
$(Morebits.QuickForm.getElements(e.target.form, 'pages')).prop('checked', true);
}
});
apiobj.params.form.append({
type: 'button',
label: 'Deselect All',
event: function(e) {
$(Morebits.QuickForm.getElements(e.target.form, 'pages')).prop('checked', false);
}
});
apiobj.params.form.append({
type: 'checkbox',
name: 'pages',
shiftClickSupport: true,
list: list
});
apiobj.params.form.append({ type: 'submit' });
const result = apiobj.params.form.render();
apiobj.params.Window.setContent(result);
Morebits.QuickForm.getElements(result, 'pages').forEach(Twinkle.generateArrowLinks);
}), statelem);
wikipediaApi.params = { form: form, Window: Window };
wikipediaApi.post();
};
Twinkle.batchundelete.callback.evaluate = function(event) {
Morebits.wiki.actionCompleted.notice = 'Batch undeletion is now complete';
const numProtected = Morebits.QuickForm.getElements(event.target, 'pages').filter((element) => element.checked && element.nextElementSibling.style.color === 'red').length;
if (numProtected > 0 && !confirm('You are about to undelete ' + numProtected + ' fully create protected page(s). Are you sure?')) {
return;
}
const input = Morebits.QuickForm.getInputData(event.target);
if (!input.reason) {
alert('You need to give a reason, you cabal crony!');
return;
}
Morebits.SimpleWindow.setButtonsEnabled(false);
Morebits.Status.init(event.target);
if (!input.pages || !input.pages.length) {
Morebits.Status.error('Error', 'nothing to undelete, aborting');
return;
}
const pageUndeleter = new Morebits.BatchOperation('Undeleting pages');
pageUndeleter.setOption('chunkSize', Twinkle.getPref('batchChunks'));
pageUndeleter.setOption('preserveIndividualStatusLines', true);
pageUndeleter.setPageList(input.pages);
pageUndeleter.run((pageName) => {
const params = {
page: pageName,
undel_talk: input.undel_talk,
reason: input.reason,
pageUndeleter: pageUndeleter
};
const wikipediaPage = new Morebits.wiki.Page(pageName, 'Undeleting page ' + pageName);
wikipediaPage.setCallbackParameters(params);
wikipediaPage.setEditSummary(input.reason);
wikipediaPage.setChangeTags(Twinkle.changeTags);
wikipediaPage.suppressProtectWarning();
wikipediaPage.setMaxRetries(3); // temporary increase from 2 to make batchundelete more likely to succeed [[phab:T222402]] #613
wikipediaPage.undeletePage(Twinkle.batchundelete.callbacks.doExtras, pageUndeleter.workerFailure);
});
};
Twinkle.batchundelete.callbacks = {
// this stupid parameter name is a temporary thing until I implement an overhaul
// of Morebits.wiki.* callback parameters
doExtras: function(thingWithParameters) {
const params = thingWithParameters.parent ? thingWithParameters.parent.getCallbackParameters() :
thingWithParameters.getCallbackParameters();
// the initial batch operation's job is to delete the page, and that has
// succeeded by now
params.pageUndeleter.workerSuccess(thingWithParameters);
let query, wikipediaApi;
if (params.undel_talk) {
const talkpagename = new mw.Title(params.page).getTalkPage().getPrefixedText();
if (talkpagename !== params.page) {
query = {
action: 'query',
prop: 'deletedrevisions',
drvprop: 'ids',
drvlimit: 1,
titles: talkpagename,
format: 'json'
};
wikipediaApi = new Morebits.wiki.Api('Checking talk page for deleted revisions', query, Twinkle.batchundelete.callbacks.undeleteTalk);
wikipediaApi.params = params;
wikipediaApi.params.talkPage = talkpagename;
wikipediaApi.post();
}
}
},
undeleteTalk: function(apiobj) {
const page = apiobj.getResponse().query.pages[0];
const exists = !page.missing;
const delrevs = page.deletedrevisions && page.deletedrevisions[0].revid;
if (exists || !delrevs) {
// page exists or has no deleted revisions; forget about it
return;
}
const talkpage = new Morebits.wiki.Page(apiobj.params.talkPage, 'Undeleting talk page of ' + apiobj.params.page);
talkpage.setEditSummary('Undeleting [[Help:Talk page|talk page]] of "' + apiobj.params.page + '"');
talkpage.setChangeTags(Twinkle.changeTags);
talkpage.undeletePage();
}
};
Twinkle.addInitCallback(Twinkle.batchundelete, 'batchundelete');
}());
// </nowiki>
hyrv0g2qt01bt0xrdt50gaf7sseepxr
769579
769542
2026-10-22T02:33:54Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at d9cd3461baca43ac99ac2c24fb76a3aae6ddada9: minify
769579
javascript
text/javascript
// <nowiki>
(function(){Twinkle.batchundelete=function twinklebatchundelete(){if(!Morebits.userIsSysop||!mw.config.get("wgArticleId")||mw.config.get("wgNamespaceNumber")!==mw.config.get("wgNamespaceIds").user&&mw.config.get("wgNamespaceNumber")!==mw.config.get("wgNamespaceIds").project){return}Twinkle.addPortletLink(Twinkle.batchundelete.callback,"Und-batch","tw-batch-undel","Undelete 'em all")};Twinkle.batchundelete.callback=function twinklebatchundeleteCallback(){const Window=new Morebits.SimpleWindow(600,400);Window.setScriptName("Twinkle");Window.setTitle("Batch undelete");Window.addFooterLink("Twinkle help","WP:TW/DOC#batchundelete");Window.addFooterLink("Give feedback","WT:TW");const form=new Morebits.QuickForm(Twinkle.batchundelete.callback.evaluate);form.append({type:"checkbox",list:[{label:"Restore talk pages of undeleted pages if they existed",name:"undel_talk",value:"undel_talk",checked:true}]});form.append({type:"input",name:"reason",label:"Reason:",size:60});const statusdiv=document.createElement("div");statusdiv.style.padding="15px";Window.setContent(statusdiv);Morebits.Status.init(statusdiv);Window.display();const query={action:"query",generator:"links",prop:"info",inprop:"protection",titles:mw.config.get("wgPageName"),gpllimit:Twinkle.getPref("batchMax"),format:"json"};const statelem=new Morebits.Status("Grabbing list of pages");const wikipediaApi=new Morebits.wiki.Api("loading...",query,apiobj=>{const response=apiobj.getResponse();let pages=response.query&&response.query.pages||[];pages=pages.filter(page=>page.missing);const list=[];pages.sort(Twinkle.sortByNamespace);pages.forEach(page=>{const editProt=page.protection.filter(pr=>pr.type==="create"&&pr.level==="sysop").pop();const title=page.title;list.push({label:title+(editProt?" (fully create protected"+(editProt.expiry==="infinity"?" indefinitely":", expires "+new Morebits.Date(editProt.expiry).calendar("utc")+" (UTC)")+")":""),value:title,checked:true,style:editProt?"color:red":""})});apiobj.params.form.append({type:"header",label:"Pages to undelete"});apiobj.params.form.append({type:"button",label:"Select All",event:function(e){$(Morebits.QuickForm.getElements(e.target.form,"pages")).prop("checked",true)}});apiobj.params.form.append({type:"button",label:"Deselect All",event:function(e){$(Morebits.QuickForm.getElements(e.target.form,"pages")).prop("checked",false)}});apiobj.params.form.append({type:"checkbox",name:"pages",shiftClickSupport:true,list:list});apiobj.params.form.append({type:"submit"});const result=apiobj.params.form.render();apiobj.params.Window.setContent(result);Morebits.QuickForm.getElements(result,"pages").forEach(Twinkle.generateArrowLinks)},statelem);wikipediaApi.params={form:form,Window:Window};wikipediaApi.post()};Twinkle.batchundelete.callback.evaluate=function(event){Morebits.wiki.actionCompleted.notice="Batch undeletion is now complete";const numProtected=Morebits.QuickForm.getElements(event.target,"pages").filter(element=>element.checked&&element.nextElementSibling.style.color==="red").length;if(numProtected>0&&!confirm("You are about to undelete "+numProtected+" fully create protected page(s). Are you sure?")){return}const input=Morebits.QuickForm.getInputData(event.target);if(!input.reason){alert("You need to give a reason, you cabal crony!");return}Morebits.SimpleWindow.setButtonsEnabled(false);Morebits.Status.init(event.target);if(!input.pages||!input.pages.length){Morebits.Status.error("Error","nothing to undelete, aborting");return}const pageUndeleter=new Morebits.BatchOperation("Undeleting pages");pageUndeleter.setOption("chunkSize",Twinkle.getPref("batchChunks"));pageUndeleter.setOption("preserveIndividualStatusLines",true);pageUndeleter.setPageList(input.pages);pageUndeleter.run(pageName=>{const params={page:pageName,undel_talk:input.undel_talk,reason:input.reason,pageUndeleter:pageUndeleter};const wikipediaPage=new Morebits.wiki.Page(pageName,"Undeleting page "+pageName);wikipediaPage.setCallbackParameters(params);wikipediaPage.setEditSummary(input.reason);wikipediaPage.setChangeTags(Twinkle.changeTags);wikipediaPage.suppressProtectWarning();wikipediaPage.setMaxRetries(3);wikipediaPage.undeletePage(Twinkle.batchundelete.callbacks.doExtras,pageUndeleter.workerFailure)})};Twinkle.batchundelete.callbacks={doExtras:function(thingWithParameters){const params=thingWithParameters.parent?thingWithParameters.parent.getCallbackParameters():thingWithParameters.getCallbackParameters();params.pageUndeleter.workerSuccess(thingWithParameters);let query,wikipediaApi;if(params.undel_talk){const talkpagename=new mw.Title(params.page).getTalkPage().getPrefixedText();if(talkpagename!==params.page){query={action:"query",prop:"deletedrevisions",drvprop:"ids",drvlimit:1,titles:talkpagename,format:"json"};wikipediaApi=new Morebits.wiki.Api("Checking talk page for deleted revisions",query,Twinkle.batchundelete.callbacks.undeleteTalk);wikipediaApi.params=params;wikipediaApi.params.talkPage=talkpagename;wikipediaApi.post()}}},undeleteTalk:function(apiobj){const page=apiobj.getResponse().query.pages[0];const exists=!page.missing;const delrevs=page.deletedrevisions&&page.deletedrevisions[0].revid;if(exists||!delrevs){return}const talkpage=new Morebits.wiki.Page(apiobj.params.talkPage,"Undeleting talk page of "+apiobj.params.page);talkpage.setEditSummary('Undeleting [[Help:Talk page|talk page]] of "'+apiobj.params.page+'"');talkpage.setChangeTags(Twinkle.changeTags);talkpage.undeletePage()}};Twinkle.addInitCallback(Twinkle.batchundelete,"batchundelete")})();
// </nowiki>
0j16v849z4pn403yonryzd83bafzxuy
User:Syunsyunminmin/Twinkle.js
2
179411
769476
2026-10-21T13:58:29Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 3680108:
769476
javascript
text/javascript
/*
Twinkle: adapted for Japanese Wikipedia; upstream authors and Japanese adaptations are credited in UPSTREAM.md.
Japanese loader source repository: https://gitlab.com/syunsyunminmin/twinkle
Upstream integration baseline: https://github.com/wikimedia-gadgets/twinkle/commit/37da5f63eca46940a5a4777b32201d7ddfd8f0e3
License: CC BY-SA 3.0 Unported. See https://github.com/wikimedia-gadgets/twinkle/blob/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/LICENSE.txt
Japanese adaptation: Syunsyunminmin; includes later synchronization changes.
*/
mw.loader.using(['mediawiki.api', 'mediawiki.language', 'mediawiki.user', 'mediawiki.util', 'mediawiki.Title'])
.then(() => {
var rawUrl = function(page, type) {
return '//ja.wikipedia.org/w/index.php?title=' + encodeURIComponent(page) + '&action=raw&ctype=' + type;
};
[
'User:Syunsyunminmin/Twinkle/Twinkle-pagestyles.css',
'User:Syunsyunminmin/select2/select2.min.css',
'User:Syunsyunminmin/morebits/morebits.css',
'User:Syunsyunminmin/Twinkle/Twinkle.css'
].forEach((page) => {
mw.loader.load(rawUrl(page, 'text/css'), 'text/css');
});
return mw.loader.getScript(rawUrl('User:Syunsyunminmin/select2/select2.min.js', 'text/javascript'))
.then(() => mw.loader.getScript(rawUrl('User:Syunsyunminmin/morebits/morebits.js', 'text/javascript')))
.then(() => mw.loader.getScript(rawUrl('User:Syunsyunminmin/Twinkle/Twinkle.js', 'text/javascript')))
.then(() => {
[
'twinkleconfig.js', 'twinklespeedy.js', 'twinklediff.js', 'twinkleunlink.js',
'twinklefluff.js', 'twinkleprotect.js'
].forEach((file) => {
mw.loader.load(rawUrl('User:Syunsyunminmin/Twinkle/' + file, 'text/javascript'));
});
});
});
lm8imqd9d0akx9mbmddencxg7gdcz6s
769479
769476
2026-10-21T14:11:11Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 45530f7: deployer wiki
769479
javascript
text/javascript
/*
Twinkle: adapted for Japanese Wikipedia; upstream authors and Japanese adaptations are credited in UPSTREAM.md.
Japanese loader source repository: https://gitlab.com/syunsyunminmin/twinkle
Upstream integration baseline: https://github.com/wikimedia-gadgets/twinkle/commit/37da5f63eca46940a5a4777b32201d7ddfd8f0e3
License: CC BY-SA 3.0 Unported. See https://github.com/wikimedia-gadgets/twinkle/blob/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/LICENSE.txt
Japanese adaptation: Syunsyunminmin; includes later synchronization changes.
*/
mw.loader.using(['mediawiki.api', 'mediawiki.language', 'mediawiki.user', 'mediawiki.util', 'mediawiki.Title'])
.then(() => {
var server = mw.config.get('wgWikiID') === 'testwiki' ? '//test.wikipedia.org' : '//ja.wikipedia.org';
var rawUrl = function(page, type) {
return server + '/w/index.php?title=' + encodeURIComponent(page) + '&action=raw&ctype=' + type;
};
[
'User:Syunsyunminmin/Twinkle/Twinkle-pagestyles.css',
'User:Syunsyunminmin/select2/select2.min.css',
'User:Syunsyunminmin/morebits/morebits.css',
'User:Syunsyunminmin/Twinkle/Twinkle.css'
].forEach((page) => {
mw.loader.load(rawUrl(page, 'text/css'), 'text/css');
});
return mw.loader.getScript(rawUrl('User:Syunsyunminmin/select2/select2.min.js', 'text/javascript'))
.then(() => mw.loader.getScript(rawUrl('User:Syunsyunminmin/morebits/morebits.js', 'text/javascript')))
.then(() => mw.loader.getScript(rawUrl('User:Syunsyunminmin/Twinkle/Twinkle.js', 'text/javascript')))
.then(() => {
[
'twinkleconfig.js', 'twinklespeedy.js', 'twinklediff.js', 'twinkleunlink.js',
'twinklefluff.js', 'twinkleprotect.js'
].forEach((file) => {
mw.loader.load(rawUrl('User:Syunsyunminmin/Twinkle/' + file, 'text/javascript'));
});
});
});
rdm6d6i4af7hh3vcmmphxij6t12gpnm
769543
769479
2026-10-22T00:01:21Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 1406828: rm cmt
769543
javascript
text/javascript
mw.loader.using(['mediawiki.api', 'mediawiki.language', 'mediawiki.user', 'mediawiki.util', 'mediawiki.Title'])
.then(() => {
var server = mw.config.get('wgWikiID') === 'testwiki' ? '//test.wikipedia.org' : '//ja.wikipedia.org';
var rawUrl = function(page, type) {
return server + '/w/index.php?title=' + encodeURIComponent(page) + '&action=raw&ctype=' + type;
};
[
'User:Syunsyunminmin/Twinkle/Twinkle-pagestyles.css',
'User:Syunsyunminmin/select2/select2.min.css',
'User:Syunsyunminmin/morebits/morebits.css',
'User:Syunsyunminmin/Twinkle/Twinkle.css'
].forEach((page) => {
mw.loader.load(rawUrl(page, 'text/css'), 'text/css');
});
return mw.loader.getScript(rawUrl('User:Syunsyunminmin/select2/select2.min.js', 'text/javascript'))
.then(() => mw.loader.getScript(rawUrl('User:Syunsyunminmin/morebits/morebits.js', 'text/javascript')))
.then(() => mw.loader.getScript(rawUrl('User:Syunsyunminmin/Twinkle/Twinkle.js', 'text/javascript')))
.then(() => {
[
'twinkleconfig.js', 'twinklespeedy.js', 'twinklediff.js', 'twinkleunlink.js',
'twinklefluff.js', 'twinkleprotect.js'
].forEach((file) => {
mw.loader.load(rawUrl('User:Syunsyunminmin/Twinkle/' + file, 'text/javascript'));
});
});
});
913t6upjfikia9cwvt7qa0xuxig6amv
769580
769543
2026-10-22T02:34:55Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at d9cd3461baca43ac99ac2c24fb76a3aae6ddada9: minify
769580
javascript
text/javascript
// <nowiki>
mw.loader.using(["mediawiki.api","mediawiki.language","mediawiki.user","mediawiki.util","mediawiki.Title"]).then(()=>{var server=mw.config.get("wgWikiID")==="testwiki"?"//test.wikipedia.org":"//ja.wikipedia.org";var rawUrl=function(page,type){return server+"/w/index.php?title="+encodeURIComponent(page)+"&action=raw&ctype="+type};["User:Syunsyunminmin/Twinkle/Twinkle-pagestyles.css","User:Syunsyunminmin/select2/select2.min.css","User:Syunsyunminmin/morebits/morebits.css","User:Syunsyunminmin/Twinkle/Twinkle.css"].forEach(page=>{mw.loader.load(rawUrl(page,"text/css"),"text/css")});return mw.loader.getScript(rawUrl("User:Syunsyunminmin/select2/select2.min.js","text/javascript")).then(()=>mw.loader.getScript(rawUrl("User:Syunsyunminmin/morebits/morebits.js","text/javascript"))).then(()=>mw.loader.getScript(rawUrl("User:Syunsyunminmin/Twinkle/Twinkle.js","text/javascript"))).then(()=>{["twinkleconfig.js","twinklespeedy.js","twinklediff.js","twinkleunlink.js","twinklefluff.js","twinkleprotect.js"].forEach(file=>{mw.loader.load(rawUrl("User:Syunsyunminmin/Twinkle/"+file,"text/javascript"))})})});
// </nowiki>
tvc5os54wh4m5dqup702dua5zzvq3im
User:Syunsyunminmin/Twinkle-dev.js
2
179412
769477
2026-10-21T13:58:31Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 3680108:
769477
javascript
text/javascript
/*
Twinkle: adapted for Japanese Wikipedia; upstream authors and Japanese adaptations are credited in UPSTREAM.md.
Japanese loader source repository: https://gitlab.com/syunsyunminmin/twinkle
Upstream integration baseline: https://github.com/wikimedia-gadgets/twinkle/commit/37da5f63eca46940a5a4777b32201d7ddfd8f0e3
License: CC BY-SA 3.0 Unported. See https://github.com/wikimedia-gadgets/twinkle/blob/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/LICENSE.txt
Japanese adaptation: Syunsyunminmin; includes later synchronization changes.
*/
mw.loader.using(['mediawiki.api', 'mediawiki.language', 'mediawiki.user', 'mediawiki.util', 'mediawiki.Title'])
.then(() => {
var rawUrl = function(page, type) {
return '//ja.wikipedia.org/w/index.php?title=' + encodeURIComponent(page) + '&action=raw&ctype=' + type;
};
[
'User:Syunsyunminmin/Twinkle/Twinkle-pagestyles.css',
'User:Syunsyunminmin/select2/select2.min.css',
'User:Syunsyunminmin/morebits/morebits.css',
'User:Syunsyunminmin/Twinkle/Twinkle.css'
].forEach((page) => {
mw.loader.load(rawUrl(page, 'text/css'), 'text/css');
});
return mw.loader.getScript(rawUrl('User:Syunsyunminmin/select2/select2.min.js', 'text/javascript'))
.then(() => mw.loader.getScript(rawUrl('User:Syunsyunminmin/morebits/morebits.js', 'text/javascript')))
.then(() => mw.loader.getScript(rawUrl('User:Syunsyunminmin/Twinkle/Twinkle.js', 'text/javascript')))
.then(() => {
[
'twinkleconfig.js', 'twinklespeedy.js', 'twinklediff.js', 'twinkleunlink.js',
'twinklefluff.js', 'twinklewarn.js', 'twinkleprotect.js', 'friendlytag.js',
'friendlytalkback.js', 'twinklexfd.js', 'twinkleimage.js', 'twinklearv.js',
'twinkleblock.js', 'twinklebatchdelete.js', 'twinklebatchprotect.js', 'twinklebatchundelete.js'
].forEach((file) => {
mw.loader.load(rawUrl('User:Syunsyunminmin/Twinkle/' + file, 'text/javascript'));
});
});
});
av5a9hbqct3h1z7b2ptq0cevqp6ba7w
769480
769477
2026-10-21T14:11:13Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 45530f7: deployer wiki
769480
javascript
text/javascript
/*
Twinkle: adapted for Japanese Wikipedia; upstream authors and Japanese adaptations are credited in UPSTREAM.md.
Japanese loader source repository: https://gitlab.com/syunsyunminmin/twinkle
Upstream integration baseline: https://github.com/wikimedia-gadgets/twinkle/commit/37da5f63eca46940a5a4777b32201d7ddfd8f0e3
License: CC BY-SA 3.0 Unported. See https://github.com/wikimedia-gadgets/twinkle/blob/37da5f63eca46940a5a4777b32201d7ddfd8f0e3/LICENSE.txt
Japanese adaptation: Syunsyunminmin; includes later synchronization changes.
*/
mw.loader.using(['mediawiki.api', 'mediawiki.language', 'mediawiki.user', 'mediawiki.util', 'mediawiki.Title'])
.then(() => {
var server = mw.config.get('wgWikiID') === 'testwiki' ? '//test.wikipedia.org' : '//ja.wikipedia.org';
var rawUrl = function(page, type) {
return server + '/w/index.php?title=' + encodeURIComponent(page) + '&action=raw&ctype=' + type;
};
[
'User:Syunsyunminmin/Twinkle/Twinkle-pagestyles.css',
'User:Syunsyunminmin/select2/select2.min.css',
'User:Syunsyunminmin/morebits/morebits.css',
'User:Syunsyunminmin/Twinkle/Twinkle.css'
].forEach((page) => {
mw.loader.load(rawUrl(page, 'text/css'), 'text/css');
});
return mw.loader.getScript(rawUrl('User:Syunsyunminmin/select2/select2.min.js', 'text/javascript'))
.then(() => mw.loader.getScript(rawUrl('User:Syunsyunminmin/morebits/morebits.js', 'text/javascript')))
.then(() => mw.loader.getScript(rawUrl('User:Syunsyunminmin/Twinkle/Twinkle.js', 'text/javascript')))
.then(() => {
[
'twinkleconfig.js', 'twinklespeedy.js', 'twinklediff.js', 'twinkleunlink.js',
'twinklefluff.js', 'twinklewarn.js', 'twinkleprotect.js', 'friendlytag.js',
'friendlytalkback.js', 'twinklexfd.js', 'twinkleimage.js', 'twinklearv.js',
'twinkleblock.js', 'twinklebatchdelete.js', 'twinklebatchprotect.js', 'twinklebatchundelete.js'
].forEach((file) => {
mw.loader.load(rawUrl('User:Syunsyunminmin/Twinkle/' + file, 'text/javascript'));
});
});
});
1fgn84i2urcsr2irl13tp19ej9xje2m
769544
769480
2026-10-22T00:01:25Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at 1406828: rm cmt
769544
javascript
text/javascript
mw.loader.using(['mediawiki.api', 'mediawiki.language', 'mediawiki.user', 'mediawiki.util', 'mediawiki.Title'])
.then(() => {
var server = mw.config.get('wgWikiID') === 'testwiki' ? '//test.wikipedia.org' : '//ja.wikipedia.org';
var rawUrl = function(page, type) {
return server + '/w/index.php?title=' + encodeURIComponent(page) + '&action=raw&ctype=' + type;
};
[
'User:Syunsyunminmin/Twinkle/Twinkle-pagestyles.css',
'User:Syunsyunminmin/select2/select2.min.css',
'User:Syunsyunminmin/morebits/morebits.css',
'User:Syunsyunminmin/Twinkle/Twinkle.css'
].forEach((page) => {
mw.loader.load(rawUrl(page, 'text/css'), 'text/css');
});
return mw.loader.getScript(rawUrl('User:Syunsyunminmin/select2/select2.min.js', 'text/javascript'))
.then(() => mw.loader.getScript(rawUrl('User:Syunsyunminmin/morebits/morebits.js', 'text/javascript')))
.then(() => mw.loader.getScript(rawUrl('User:Syunsyunminmin/Twinkle/Twinkle.js', 'text/javascript')))
.then(() => {
[
'twinkleconfig.js', 'twinklespeedy.js', 'twinklediff.js', 'twinkleunlink.js',
'twinklefluff.js', 'twinklewarn.js', 'twinkleprotect.js', 'friendlytag.js',
'friendlytalkback.js', 'twinklexfd.js', 'twinkleimage.js', 'twinklearv.js',
'twinkleblock.js', 'twinklebatchdelete.js', 'twinklebatchprotect.js', 'twinklebatchundelete.js'
].forEach((file) => {
mw.loader.load(rawUrl('User:Syunsyunminmin/Twinkle/' + file, 'text/javascript'));
});
});
});
7spi9nsmzqnftr9u4zf3m8e967lxuq5
769581
769544
2026-10-22T02:35:56Z
Syunsyunminmin
53741
[[:gitlab:syunsyunminmin/twinkle|repo]] at d9cd3461baca43ac99ac2c24fb76a3aae6ddada9: minify
769581
javascript
text/javascript
// <nowiki>
mw.loader.using(["mediawiki.api","mediawiki.language","mediawiki.user","mediawiki.util","mediawiki.Title"]).then(()=>{var server=mw.config.get("wgWikiID")==="testwiki"?"//test.wikipedia.org":"//ja.wikipedia.org";var rawUrl=function(page,type){return server+"/w/index.php?title="+encodeURIComponent(page)+"&action=raw&ctype="+type};["User:Syunsyunminmin/Twinkle/Twinkle-pagestyles.css","User:Syunsyunminmin/select2/select2.min.css","User:Syunsyunminmin/morebits/morebits.css","User:Syunsyunminmin/Twinkle/Twinkle.css"].forEach(page=>{mw.loader.load(rawUrl(page,"text/css"),"text/css")});return mw.loader.getScript(rawUrl("User:Syunsyunminmin/select2/select2.min.js","text/javascript")).then(()=>mw.loader.getScript(rawUrl("User:Syunsyunminmin/morebits/morebits.js","text/javascript"))).then(()=>mw.loader.getScript(rawUrl("User:Syunsyunminmin/Twinkle/Twinkle.js","text/javascript"))).then(()=>{["twinkleconfig.js","twinklespeedy.js","twinklediff.js","twinkleunlink.js","twinklefluff.js","twinklewarn.js","twinkleprotect.js","friendlytag.js","friendlytalkback.js","twinklexfd.js","twinkleimage.js","twinklearv.js","twinkleblock.js","twinklebatchdelete.js","twinklebatchprotect.js","twinklebatchundelete.js"].forEach(file=>{mw.loader.load(rawUrl("User:Syunsyunminmin/Twinkle/"+file,"text/javascript"))})})});
// </nowiki>
kk7cxm3pzt5uy1u2vpumljwhvrsdqo2
User:Phoenix3452/sandbox
2
179413
769490
2026-10-21T14:41:31Z
Phoenix3452
76309
custom sandbox edit
769490
wikitext
text/x-wiki
<div style="background:linear-gradient(145deg,#130606,#35100a,#170707);border:3px solid #ff5722;border-radius:16px;padding:22px;color:#ffe4c4;font-family:sans-serif;">
<div style="text-align:center;background:linear-gradient(120deg,#200606,#78200b,#200606);border:2px solid #ff762e;border-radius:12px;padding:25px 10px;">
<div style="font-size:36px;font-weight:bold;color:#ff792f;text-shadow:0 0 12px #ff2600;">
🔥🐦🔥 PHOENIX SANDBOX 🐦🔥🔥
</div>
<div style="font-size:14px;letter-spacing:3px;color:#ffd0a0;margin-top:10px;">
EXPERIMENT · CREATE · RISE
</div>
</div>
<div style="text-align:center;margin:25px 0 12px;font-size:24px;font-weight:bold;color:#ff762e;">
⚔️ WELCOME TO THE WORKSHOP
</div>
<div style="background:#251010;border:1px solid #c94725;border-radius:10px;padding:18px;line-height:1.8;">
Welcome to my Phoenix Sandbox!
This is my personal testing area for experimenting with wiki markup, formatting, templates, layouts, and new ideas before using them elsewhere.
🔥 **Feel free to experiment, make mistakes, and learn.**
</div>
<div style="text-align:center;margin:25px 0 12px;font-size:23px;font-weight:bold;color:#ff762e;">
🛠️ TESTING ZONE
</div>
<div style="background:#1b0909;border:1px solid #87351d;border-radius:10px;padding:18px;">
<div style="color:#ffb16e;font-size:18px;font-weight:bold;">
📌 Current experiments
</div>
* 🔥 Testing custom colors and borders
* 🐦🔥 Building Phoenix-themed layouts
* ⚔️ Practicing wiki markup
* 👑 Designing new user-page features
* 🛡️ Testing templates and navigation
</div>
<div style="text-align:center;margin:25px 0 12px;font-size:23px;font-weight:bold;color:#ff762e;">
📚 USEFUL LINKS
</div>
<div style="text-align:center;background:#200909;border:1px solid #87351d;border-radius:10px;padding:14px;">
<div style="display:inline-block;background:#76200e;border:2px solid #ff762e;border-radius:8px;padding:10px;margin:5px;">
[[User:{{PAGENAME}}|🏠 MY USER PAGE]]
</div>
<div style="display:inline-block;background:#76200e;border:2px solid #ff762e;border-radius:8px;padding:10px;margin:5px;">
[[User talk:{{PAGENAME}}|💬 MY TALK PAGE]]
</div>
<div style="display:inline-block;background:#76200e;border:2px solid #ff762e;border-radius:8px;padding:10px;margin:5px;">
[[Special:MyPage/sandbox|🛠️ MY SANDBOX]]
</div>
</div>
<div style="text-align:center;margin-top:25px;padding:18px;border-top:2px solid #ff5722;color:#ff9966;">
<div style="font-size:22px;font-weight:bold;">
🐦🔥 FROM ASHES TO CREATION 🔥
</div>
<div style="font-size:13px;margin-top:8px;">
EVERY GREAT IDEA STARTS SOMEWHERE.
</div>
</div>
7lf4r687kflo9ejym62pg2m4y5ishn1
File:Machu-Pichu-Macka.jpg
6
179414
769496
2026-10-21T16:04:57Z
PsyhoZOOM
75470
My Cat Test
769496
wikitext
text/x-wiki
== Summary ==
My Cat Test
== Licensing ==
{{subst:uwl}}
297gz2h6l4eyqp6a0ne8rsvve7oek7i
Mentor test
0
179415
769498
2026-10-21T17:27:14Z
Phoenix3452
76309
Created page with "this is just a test page with peace, ~~~~"
769498
wikitext
text/x-wiki
this is just a test page with peace, [[User:Phoenix3452|<span style="color:#ff6a00;">🔥🐦🔥 Phoenix</span>]] ([[User talk:Phoenix3452|<span style="color:#cc3300;">⚔️ Mentor Talk 🔥</span>]]) 17:27, 21 October 2026 (UTC)
3d5l6j1juzy6sjgddl2xgpek1bzggt0
Talk:Report comp09
1
179416
769499
2026-10-21T17:41:10Z
Xaosflux ep
32681
test
769499
wikitext
text/x-wiki
== Test ==
Test. [[User:Xaosflux ep|Xaosflux ep]] ([[User talk:Xaosflux ep|talk]]) 17:41, 21 October 2026 (UTC)
cr81udh0k8zx3tnda3bqbvp8ohzcug1
User:W200382522/CloudCred-V1-20261022
2
179417
769500
2026-10-21T19:22:43Z
W200382522
76356
Create baseline test page
769500
wikitext
text/x-wiki
CloudCred Sentinel controlled test page.
Baseline only. No credentials or test marker.
hu5yynbnwop9vwpogm2w3ayaq39l25y
769501
769500
2026-10-21T19:37:43Z
W200382522
76356
769501
wikitext
text/x-wiki
= Non-authentic CloudCred test only =
CLOUDCRED_SAMPLE_ID=CCV21-C01-34263e9190104715
CLOUDCRED_TEST_AK_INVALID_V21_CCV21-C01-34263e9190104715_NOT_AUTHENTIC
ekghu2t76qsoxpznfd8xki1g4381zed
769586
769501
2026-10-22T05:42:59Z
W200382522
76356
CloudCred controlled invalid synthetic test 1/12
769586
wikitext
text/x-wiki
# Non-authentic CloudCred test only
CLOUDCRED_SAMPLE_ID=CCV21-C01-7b72091e628c4585
CLOUDCRED_TEST_AK_INVALID_V21_CCV21-C01-7b72091e628c4585_NOT_AUTHENTIC
9tagqccjgujbn5fep3wioqbqdb8dd6y
769587
769586
2026-10-22T05:46:22Z
W200382522
76356
CloudCred controlled invalid synthetic test 2/12
769587
wikitext
text/x-wiki
# Non-authentic CloudCred test only
CLOUDCRED_SAMPLE_ID=CCV21-C01-5cdee5be7fcf4094
ordinary text; negative control
3gb5fofj9024639cci0puwjvnnq5rdj
769588
769587
2026-10-22T05:48:39Z
W200382522
76356
CloudCred controlled invalid synthetic test 3/12
769588
wikitext
text/x-wiki
# Non-authentic CloudCred test only
CLOUDCRED_SAMPLE_ID=CCV21-C02-95af7cb7a5ac45cb
CLOUDCRED_TEST_SSH_INVALID_V21_CCV21-C02-95af7cb7a5ac45cb_NOT_AUTHENTIC
edlexxwj0c319kq7mckfzng8tlet7uf
769589
769588
2026-10-22T05:50:18Z
W200382522
76356
CloudCred controlled invalid synthetic test 4/12
769589
wikitext
text/x-wiki
# Non-authentic CloudCred test only
CLOUDCRED_SAMPLE_ID=CCV21-C02-66035d5001374d14
ordinary text; negative control
04zzxw93wuyk7euct77avpil54jgjwm
769590
769589
2026-10-22T05:51:47Z
W200382522
76356
CloudCred controlled invalid synthetic test 5/12
769590
wikitext
text/x-wiki
# Non-authentic CloudCred test only
CLOUDCRED_SAMPLE_ID=CCV21-C03-b27f97b782af40e4
CLOUDCRED_TEST_OS_INVALID_V21_CCV21-C03-b27f97b782af40e4_NOT_AUTHENTIC
1qsjjutkzv69ozimssmpf5wv9r3fqdw
769591
769590
2026-10-22T05:54:57Z
W200382522
76356
CloudCred controlled invalid synthetic test 6/12
769591
wikitext
text/x-wiki
# Non-authentic CloudCred test only
CLOUDCRED_SAMPLE_ID=CCV21-C03-5c47549f64024c37
ordinary text; negative control
kgo63w2grdqja34kfwxay1or0hcsgaz
769592
769591
2026-10-22T06:01:23Z
W200382522
76356
CloudCred controlled invalid synthetic test 7/12
769592
wikitext
text/x-wiki
# Non-authentic CloudCred test only
CLOUDCRED_SAMPLE_ID=CCV21-C04-27352045fec54fbb
CLOUDCRED_TEST_DB_INVALID_V21_CCV21-C04-27352045fec54fbb_NOT_AUTHENTIC
lvq8b8m1y9kki283n2qenilpp331x5j
769593
769592
2026-10-22T06:03:30Z
W200382522
76356
CloudCred controlled invalid synthetic test 8/12
769593
wikitext
text/x-wiki
# Non-authentic CloudCred test only
CLOUDCRED_SAMPLE_ID=CCV21-C04-5221821db73f4662
ordinary text; negative control
836coa7affbp5ub9nhup3nfqmqwkkt6
769594
769593
2026-10-22T06:07:16Z
W200382522
76356
CloudCred controlled invalid synthetic test 9/12
769594
wikitext
text/x-wiki
# Non-authentic CloudCred test only
CLOUDCRED_SAMPLE_ID=CCV21-C05-64d3187d2b5a4145
CLOUDCRED_TEST_API_INVALID_V21_CCV21-C05-64d3187d2b5a4145_NOT_AUTHENTIC
icrqtolx50i35p8q3ozb63zy5d9au3k
769595
769594
2026-10-22T06:08:38Z
W200382522
76356
CloudCred controlled invalid synthetic test 10/12
769595
wikitext
text/x-wiki
# Non-authentic CloudCred test only
CLOUDCRED_SAMPLE_ID=CCV21-C05-6c0bdb618cb243e7
ordinary text; negative control
gm7w7rm2a1nxawohxnwuyi8zorepuh9
769596
769595
2026-10-22T06:11:37Z
W200382522
76356
CloudCred controlled invalid synthetic test 11/12
769596
wikitext
text/x-wiki
# Non-authentic CloudCred test only
CLOUDCRED_SAMPLE_ID=CCV21-C06-2aa97279ea0f4f78
CLOUDCRED_TEST_OAUTH_INVALID_V21_CCV21-C06-2aa97279ea0f4f78_NOT_AUTHENTIC
aoo6f2nrunud3plxf90eugql4yscub6
769597
769596
2026-10-22T06:13:15Z
W200382522
76356
CloudCred controlled invalid synthetic test 12/12
769597
wikitext
text/x-wiki
# Non-authentic CloudCred test only
CLOUDCRED_SAMPLE_ID=CCV21-C06-9c961e35342348d3
ordinary text; negative control
l4dzur1zn5iltczikfpgee6s5cxmlry
All Eyes on Me (mixtape)
0
179418
769502
2026-10-21T19:43:44Z
Island Hines
76160
Created page with "{{other uses}} {{Use mdy dates|date=December 2011}} {{Infobox album | name = All Eyes on Me | type = [[Studio mixtape]] | artist = [[Nicky Jam]] | cover = | alt = | released = {{Start date|2001|02|13}} | recorded = October – December 2000 | studio = Meth Row Studios | genre = * [[Puerto Rican hip-hop]] * [[latin hip-hop]] * [[reggaeton rap]] * [[G-funk]] | length = | label = {{hlist|[[Meth Row Records|Meth Row]]|[[Interscope Records|Interscope]]}} | producer = * Nicky..."
769502
wikitext
text/x-wiki
{{other uses}}
{{Use mdy dates|date=December 2011}}
{{Infobox album
| name = All Eyes on Me
| type = [[Studio mixtape]]
| artist = [[Nicky Jam]]
| cover =
| alt =
| released = {{Start date|2001|02|13}}
| recorded = October – December 2000
| studio = Meth Row Studios
| genre =
* [[Puerto Rican hip-hop]]
* [[latin hip-hop]]
* [[reggaeton rap]]
* [[G-funk]]
| length =
| label = {{hlist|[[Meth Row Records|Meth Row]]|[[Interscope Records|Interscope]]}}
| producer =
* Nicky Jam
* Rob the Viking
* [[Denine Porter]]
* [[Donnie Wahlberg]]
* [[The Alchemist (musician)|The Alchemist]]
* [[Erick Onasis (musician)|Erick Onasis]]
* [[Evidence (musician)|Evidence]]
* [[Jermaine Dupri]]
* [[DJ Muggs]]
* [[DJ Lethal]]
* [[Todd Ray|T-Ray]]
* [[Everlast]]
| prev_title = [[...Distinto a los demás]]
| prev_year = 1995
| next_title = [[Haciendo escante]]
| next_year = 2001
| misc = {{Singles
| name = All Eyes on Me
| type = [[Studio mixtape]]
| single1 = [[Dear Mama (Nicky Jam song)|Dear Mama]]
| single1date = December 3, 2000
| single2 = [[Changes (Nicky Jam song)|Changes]]
| single2date = May 7, 2001
| single3 = [[To Live and Die in L.A. (Nicky Jam song)|To Live and Die in L.A.]]
| single3date = June 4, 2001
| single4 = [[Hail Mary (Nicky Jam song)|Hail Mary]]
| single4date = September 15, 2001
}}
}}
'''''All Eyes on Me''''' is a [[studio mixtape]] by American [[reggaeton]] rapper [[Nicky Jam]]. It was released on February 13, 2001, by [[Meth Row Records]] and [[Interscope Records]].
{{Nicky Jam}}
{{Authority control}}
[[Category:2001 mixtape albums]]
[[Category:Albums produced by Denine Porter]]
[[Category:Albums produced by Jermaine Dupri]]
[[Category:Albums produced by The Alchemist (musician)]]
[[Category:Albums produced by Everlast]]
[[Category:Meth Row Records mixtape albums]]
[[Category:Interscope Records mixtape albums]]
[[Category:Nicky Jam mixtape albums]]
[[Category:Albums produced by Erick Onasis (musician)]]
[[Category:Studio mixtape albums]]
[[Category:G-funk albums]]
[[Category:Albums produced by DJ Muggs]]
f9ui876fso1jd2nd929e54jxsbqjlsi
The Don Killuminati: The 3 Day Theory
0
179419
769503
2026-10-21T19:44:18Z
Island Hines
76160
Created page with "{{Short description|2001 studio album by Daddy Yankee}} {{Use mdy dates|date=June 2025}} {{Infobox album | name = The Don Killuminati: The 3-Day Theory | type = [[Studio mixtape]] | artist = [[Daddy Yankee]] | cover = | alt = | caption = | released = November 5, 2001 | recorded = July – August 2001 | studio = {{hlist|Meth Row Studios|El Cartel Studios}} | genre = * [[Puerto Rican hip-hop]] * reggae f..."
769503
wikitext
text/x-wiki
{{Short description|2001 studio album by Daddy Yankee}}
{{Use mdy dates|date=June 2025}}
{{Infobox album
| name = The Don Killuminati: The 3-Day Theory
| type = [[Studio mixtape]]
| artist = [[Daddy Yankee]]
| cover =
| alt =
| caption =
| released = November 5, 2001
| recorded = July – August 2001
| studio = {{hlist|Meth Row Studios|El Cartel Studios}}
| genre =
* [[Puerto Rican hip-hop]]
* [[reggae fusion]]
* [[ragga hip-hop]]
| length =
| label =
* [[Meth Row Records|Meth Row]]
* [[El Cartel Records|El Cartel]]
* [[Interscope Records|Interscope]]
| producer =
* [[Suge Knight II]] ([[executive producer#Music|exec.]])
* [[Kardinal Offishall]]
* [[Saukrates]]
* Randy Allen
* [[Jam Master Jay]]
* [[Todd Ray|T-Ray]]
| prev_title = [[El Cartel II]]
| prev_year = 2001
| next_title = [[El Cangri.com]]
| next_year = 2002
| misc = {{Singles
| name = Killuminati: The 3 Day Theory
| type = [[Studio mixtape]]
| single1 = [[Fly Away (Daddy Yankee song)|Fly Away]]
| single1date = September 26, 2001
| single2 = [[I Belong to You (Daddy Yankee song)|I Belong to You]]
| single2date = November 16, 2001
| single3 = [[Dig In (song)|Dig In]]
| single3date = February 11, 2002
}}
}}
'''''The Don Killuminati: The 3-Day Theory''''' (commonly shortened to '''''Killuminati''''', or '''''The 3-Day Theory''''') is a [[studio mixtape]] by Puerto Rican rapper [[Daddy Yankee]]. It was released on November 5, 2001, by [[Meth Row Records]], Daddy Yankee's [[El Cartel Records]] and [[Interscope Records]].
{{Daddy Yankee|state=collapsed}}
{{Authority control}}
{{DEFAULTSORT:Don Killuminati: The 3 Day Theory, The}}
[[Category:Daddy Yankee mixtape albums]]
[[Category:2001 mixtape albums]]
[[Category:Albums produced by Saukrates]]
[[Category:Studio mixtape albums]]
[[Category:Meth Row Records mixtape albums]]
[[Category:El Cartel Records mixtape albums]]
[[Category:Interscope Records mixtape albums]]
[[Category:Reggae fusion albums]]
[[Category:G-funk albums]]
[[Category:Albums produced by Jermaine Dupri]]
eajab3qsr9plw8vn2trddwr8y42qbr2
Template:Col-3
10
179420
769505
2026-10-21T20:31:09Z
Island Hines
76160
Created page with "{{col-break|class=col-break-3|align={{{align|}}}|valign={{{valign|}}}}}<noinclude> {{documentation}} <!-- Add categories to the /doc subpage, interwikis to Wikidata, not here --> </noinclude>"
769505
wikitext
text/x-wiki
{{col-break|class=col-break-3|align={{{align|}}}|valign={{{valign|}}}}}<noinclude>
{{documentation}}
<!-- Add categories to the /doc subpage, interwikis to Wikidata, not here -->
</noinclude>
2h7ku08msdjg5o9jm3fzbe9qtahzpyp
Template:Col end
10
179421
769506
2026-10-21T20:31:51Z
Island Hines
76160
Created page with "<includeonly>  |}</div></includeonly><noinclude> {{documentation}} <!-- Add categories to the /doc subpage, interwikis to Wikidata, not here --> </noinclude>"
769506
wikitext
text/x-wiki
<includeonly> 
|}</div></includeonly><noinclude>
{{documentation}}
<!-- Add categories to the /doc subpage, interwikis to Wikidata, not here -->
</noinclude>
k24bcp3cs0m8f59jwmhng1acd8tpifu
MediaWiki:Centralnotice-template-B 1020 Apps enWW test
8
179422
769507
2026-10-21T21:07:09Z
CKlimas-WMF
75493
Test banner for T441511
769507
wikitext
text/x-wiki
<!-- Empty banner -->
0e7dmqwcakoxnd1asje6l4zbqzwpu82
769508
769507
2026-10-21T21:07:09Z
CKlimas-WMF
75493
Protected "[[MediaWiki:Centralnotice-template-B 1020 Apps enWW test]]": Automatically protected by CentralNotice; please edit only via [[Special:CentralNotice]] or [[Special:Translate]] ([Edit=Allow only central notice administrators] (indefinite) [Move=Allow only central notice administrators] (indefinite)) [cascading]
769507
wikitext
text/x-wiki
<!-- Empty banner -->
0e7dmqwcakoxnd1asje6l4zbqzwpu82
769509
769508
2026-10-21T21:09:11Z
CKlimas-WMF
75493
769509
wikitext
text/x-wiki
<style>
/* --- VARIABLES & THEME --- */
.cbnnr {
/* Light Mode Theme */
color-scheme: light;
--cbnnr-base: #202122;
--cbnnr-base-hover: #000000;
--cbnnr-background: #EAECF0;
--cbnnr-border: #DDDDDD;
--cbnnr-cta-bg: #007AFF;
--cbnnr-cta-bg-hover: #006AEF;
--cbnnr-cta-text: #FFFFFF;
--cbnnr-close-bg: rgba(240, 240, 240, 0.8);
--cbnnr-close-bg-hover: rgba(230, 230, 230, 1);
--cbnnr-close-text: #202122;
--cbnnr-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
--cbnnr-sans: 'Helvetica Neue', 'Helvetica', -apple-system, 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'Helvetica Neue', 'Helvetica', 'Inter', 'Arial', sans-serif;
--cbnnr-avec: 'Linux Libertine', 'Georgia', 'Times', 'Source Serif 4', serif;
}
/* Dark Mode Overrides */
@media screen {
html.skin-theme-clientpref-night .cbnnr {
color-scheme: dark;
--cbnnr-base: #eeeeee;
--cbnnr-base-hover: #ffffff;
--cbnnr-background: #27292d;
--cbnnr-border: #555555;
--cbnnr-cta-bg: #007AFF;
--cbnnr-cta-bg-hover: #006AEF;
--cbnnr-cta-text: #FFFFFF;
--cbnnr-close-bg: rgba(60, 60, 60, 0.8);
--cbnnr-close-bg-hover: rgba(30, 30, 30, 1);
--cbnnr-close-text: #F8F9FA;
--cbnnr-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
}
}
/* --- BANNER STRUCTURE --- */
.cbnnr {
font-family: var(--cbnnr-sans);
width: 100%;
margin-bottom: 1px;
}
.cbnnr-outer * {
margin: 0px;
padding: 0;
}
.cbnnr-outer,
.cbnnr-outer *,
.cbnnr-outer *:before,
.cbnnr-outer *:after {
box-sizing: border-box;
}
.cbnnr-outer {
position: relative;
display: block;
}
.cbnnr-top-row {
display: flex;
align-items: center;
flex-grow: 1;
}
.cbnnr-main-container {
background: var(--cbnnr-background);
position: relative;
width: 100%;
max-width: 993.3px;
margin: 0 auto;
cursor: pointer;
display: flex;
align-items: center;
height: auto;
min-height: 110px;
padding-left: 24px;
border-left: solid 1px var(--cbnnr-border);
border-right: solid 1px var(--cbnnr-border);
border-top: solid 1px var(--cbnnr-border);
}
body.skin-monobook #centralNotice,
body.skin-timeless #centralNotice,
body.skin-vector #centralNotice {
display: none !important;
}
body.skin-minerva .cbnnr-outer {
height: auto;
min-height: 80px;
}
/* --- COPY --- */
.cbnnr-text-container {
flex-grow: 1;
color: var(--cbnnr-base);
display: flex;
flex-direction: column;
justify-content: center;
margin-left: 40px;
}
.cbnnr-text-container .cbnnr-first-lines {
display: flex;
flex-direction: column;
}
.cbnnr-text-container p {
margin: 0;
font-size: 18px;
line-height: 1.4;
position: relative;
width: fit-content;
}
/* --- LOGO AND ASSETS --- */
.cbnnr-logo-container {
flex-shrink: 0;
height: 80px;
width: 80px;
display: flex;
align-items: center;
justify-content: center;
background-color: #fff;
overflow: hidden;
margin: auto 10px;
border-radius: 6px;
padding: 5px 7px 0px 7px;
}
.cbnnr-logo-container img {
width: 100%;
height: 100%;
right: 20px;
object-fit: contain;
}
.cbnnr-logo-container img {
width: 100%;
height: 100%;
right: 20px;
object-fit: contain;
}
/* --- CTA BUTTON --- */
.cbnnr-cta-button:hover {
background-color: var(--cbnnr-cta-bg-hover);
}
.cbnnr-cta-button {
flex-shrink: 0;
background-color: var(--cbnnr-cta-bg);
color: var(--cbnnr-cta-text);
padding: 16px 24px;
margin-right: 60px;
margin-top: 15px;
margin-bottom: 15px;
border-radius: 30px;
font-size: 14px;
font-weight: bold;
white-space: nowrap;
margin-left: 16px;
text-align: left;
line-height: 1.2;
position: relative;
text-align: center;
}
/* --- CLOSE BUTTON --- */
.cbnnr-toggle-close {
position: absolute;
top: 10px;
right: 10px;
border-radius: 50%;
height: 24px;
width: 24px;
border: 0;
background-color: var(--cbnnr-close-bg);
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
body.rtl .cbnnr-toggle-close {
left: 3px;
right: unset;
}
.cbnnr-toggle-close:hover,
.cbnnr-toggle-close:focus-visible {
background-color: var(--cbnnr-close-bg-hover);
}
.cbnnr-toggle-close:focus-visible {
outline: solid 2px var(--outline-color-progressive--focus, #36c)
}
.cbnnr-toggle-close svg {
fill: var(--cbnnr-close-text);
width: 14px;
height: 14px;
}
a.cbnnr-outer {
text-decoration: none !important;
}
a.cbnnr-outer:hover,
a.cbnnr-outer:active,
a.cbnnr-outer:focus,
a.cbnnr-outer:visited {
text-decoration: none !important;
}
a.cbnnr-outer:focus-visible,
.cbnnr-outer[role="button"]:focus-visible {
outline: solid 2px var(--outline-color-progressive--focus, #36c)
}
.cbnnr-click {
position: absolute;
}
/* --- RESPONSIVENESS --- */
@media screen and (max-width: 820px) {
.cbnnr-main-container {
padding-left: 10px;
}
.cbnnr-text-container {
margin-left: 20px;
}
.cbnnr-text-container p {
font-size: 16px;
}
.cbnnr-logo-container {
height: 80px;
}
}
@media screen and (max-width: 600px) {
.cbnnr-cta-button {
margin-left: 10px;
}
}
@media screen and (max-width: 600px) {
.cbnnr-main-container {
flex-wrap: wrap;
align-items: center;
justify-content: center;
}
.cbnnr-top-row {
flex-basis: 100%;
margin-top: 30px;
}
.cbnnr-text-container {
margin-left: 10px;
margin-right: 40px;
}
.cbnnr-cta-button {
padding: 15px 10px;
flex-basis: 80%;
margin-top: 20px;
margin-left: 10px;
margin-right: 10px;
margin-bottom: 30px;
}
}
</style>
<div class="cbnnr cbnnr-outer" id="cbnnr" role="button" tabindex="0" aria-label="Download the Wikipedia app">
<div class="cbnnr-main-container">
<div class="cbnnr-top-row">
<div class="cbnnr-logo-container">
<img src="https://upload.wikimedia.org/wikipedia/donate/4/49/Wikipedia%27s_W_banner.svg"
alt="Wikipedia App Icon">
</div>
<div class="cbnnr-text-container">
<p>This banner should direct you to either the App Store or
Google Play Store depending on your OS.</p>
</div>
</div>
<div class="cbnnr-cta-button">
{{{B_WP25_Translations,install}}}
</div>
<div class="cbnnr-toggle-close" role="button" tabindex="0" aria-label="{{int:centralnotice-shared-hide}}"
title="{{int:centralnotice-shared-hide}}">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" aria-hidden="true">
<g>
<path d="m4.34 2.93 12.73 12.73-1.41 1.41L2.93 4.35z"></path>
<path d="M17.07 4.34 4.34 17.07l-1.41-1.41L15.66 2.93z"></path>
</g>
</svg>
</div>
</div>
</div>
{{FIXME MediaWiki:CentralNoticeWP25Events_Android_multilingual.js}}
s1r3s3uk8iv7fofyfqk56tzg3igbezp
Wikipedia:Requests/Permissions/CKlimas-WMF
4
179423
769510
2026-10-21T21:29:34Z
CKlimas-WMF
75493
Created page with "<!--@Bureaucrats: If you close this request as {{Done}} add {{Request-done|1=~~~~|2=Closing rationale}} to the top of the page. If you close it as {{Not done}}, add {{Request-not done|1=~~~~|2=Closing rationale}} there, then close with {{Request closed}}.--> === [[User:{{subst:REVISIONUSER}}|{{subst:REVISIONUSER}}]] === * {{User3|{{subst:REVISIONUSER}}}}, [[Special:CentralAuth/{{subst:REVISIONUSER}}|global contribs]] ~~~~~ * '''Motive for request:''' I'm working on https..."
769510
wikitext
text/x-wiki
<!--@Bureaucrats: If you close this request as {{Done}} add {{Request-done|1=~~~~|2=Closing rationale}} to the top of the page. If you close it as {{Not done}}, add {{Request-not done|1=~~~~|2=Closing rationale}} there, then close with {{Request closed}}.-->
=== [[User:CKlimas-WMF|CKlimas-WMF]] ===
* {{User3|CKlimas-WMF}}, [[Special:CentralAuth/CKlimas-WMF|global contribs]] 21:29, 21 October 2026 (UTC)
* '''Motive for request:''' I'm working on https://phabricator.wikimedia.org/T441511 and want to create a new system message at https://test.wikipedia.org/wiki/MediaWiki:CentralNoticeAppsCommsEvents_crossPlatform.js, but the page tells me that I don't have permission to do so. My goal is to test JavaScript code that will simplify banners that direct people to the Wikipedia mobile apps.
* '''Requested rights:''' I think I need administrator rights to do the above, but I'd be happy to have fewer permissions if it's possible to add a new system message with a different role.
* '''Comments:''' <!-- Comments of other users -->
<!-- DO NOT FORGET to transclude your request at THE TOP of [[Wikipedia:Requests/Permissions]] or nobody will see it! -->
[[Category:!Requests]]
[[Category:Really big category]]
__NOINDEX__
ocp2t52l5lr9mrj3dvn3aty1vil8tyd
769512
769510
2026-10-21T21:34:25Z
CKlimas-WMF
75493
769512
wikitext
text/x-wiki
<!--@Bureaucrats: If you close this request as {{Done}} add {{Request-done|1=~~~~|2=Closing rationale}} to the top of the page. If you close it as {{Not done}}, add {{Request-not done|1=~~~~|2=Closing rationale}} there, then close with {{Request closed}}.-->
=== [[User:CKlimas-WMF|CKlimas-WMF]] ===
* {{User3|CKlimas-WMF}}, [[Special:CentralAuth/CKlimas-WMF|global contribs]] 21:29, 21 October 2026 (UTC)
* '''Motive for request:''' I'm a Wikimedia Foundation employee working on https://phabricator.wikimedia.org/T441511 and want to create a new system message at https://test.wikipedia.org/wiki/MediaWiki:CentralNoticeAppsCommsEvents_crossPlatform.js, but the page tells me that I don't have permission to do so. My goal is to test JavaScript code that will simplify banners that direct people to the Wikipedia mobile apps.
* '''Requested rights:''' I think I need administrator rights to do the above, but I'd be happy to have fewer permissions if it's possible to add a new system message with a different role.
* '''Comments:''' <!-- Comments of other users -->
<!-- DO NOT FORGET to transclude your request at THE TOP of [[Wikipedia:Requests/Permissions]] or nobody will see it! -->
[[Category:!Requests]]
[[Category:Really big category]]
__NOINDEX__
bgr6gwjfclcf42u34yofr3siuzdhstj
769614
769512
2026-10-22T10:02:32Z
Johannnes89
48060
/* CKlimas-WMF */ aw
769614
wikitext
text/x-wiki
<!--@Bureaucrats: If you close this request as {{Done}} add {{Request-done|1=~~~~|2=Closing rationale}} to the top of the page. If you close it as {{Not done}}, add {{Request-not done|1=~~~~|2=Closing rationale}} there, then close with {{Request closed}}.-->
=== [[User:CKlimas-WMF|CKlimas-WMF]] ===
* {{User3|CKlimas-WMF}}, [[Special:CentralAuth/CKlimas-WMF|global contribs]] 21:29, 21 October 2026 (UTC)
* '''Motive for request:''' I'm a Wikimedia Foundation employee working on https://phabricator.wikimedia.org/T441511 and want to create a new system message at https://test.wikipedia.org/wiki/MediaWiki:CentralNoticeAppsCommsEvents_crossPlatform.js, but the page tells me that I don't have permission to do so. My goal is to test JavaScript code that will simplify banners that direct people to the Wikipedia mobile apps.
* '''Requested rights:''' I think I need administrator rights to do the above, but I'd be happy to have fewer permissions if it's possible to add a new system message with a different role.
* '''Comments:''' @[[User:CKlimas-WMF]] please reach out to WMF T&S for staff permissions per [[:wmf:Policy:Wikimedia Foundation Staff Userrights Policy]] (the same policy is also published on officewiki). --[[User:Johannnes89|Johannnes89]] ([[User talk:Johannnes89|talk]]) 10:02, 22 October 2026 (UTC)
<!-- DO NOT FORGET to transclude your request at THE TOP of [[Wikipedia:Requests/Permissions]] or nobody will see it! -->
[[Category:!Requests]]
[[Category:Really big category]]
__NOINDEX__
fnog38g3l13jhojzot8pboj9jvd8mjc
User talk:眠春眠春
3
179424
769584
2026-10-22T05:04:27Z
Syunsyunminmin
53741
お知らせ: 非建設的な編集。
769584
wikitext
text/x-wiki
== 2026年10月 ==
[[File:Information.svg|25px|alt=Information icon]] Hello, I'm [[User:Syunsyunminmin|Syunsyunminmin]]. I wanted to let you know that one or more of [[Special:Contributions/眠春眠春|your recent contributions]] have been undone because they did not appear constructive. If you would like to experiment, please use the [[WP:sandbox|sandbox]]. If you have any questions, you can ask for assistance at the [[Wikipedia:Help desk|help desk]]. ''this is test''<!-- Template:uw-vandalism1 --> [[User:Syunsyunminmin|Syunsyunminmin]] ([[User talk:Syunsyunminmin|talk]]) 05:04, 22 October 2026 (UTC)
mkmg9cawl02w4pc8gwnkuws16kj9v30
MassMessage test
0
179425
769601
2026-10-22T07:55:27Z
Dragoniez
52097
+
769601
wikitext
text/x-wiki
<section begin="title" />
This is a MassMessage title
<section end="title" />
<section begin="body" />
This is a MassMessage body
<section end="body" />
<section begin="misc" />
<section end="misc" />
1wt0ahzyodmg037292ll0vaxytiz83y
769604
769601
2026-10-22T07:58:32Z
Dragoniez
52097
769604
wikitext
text/x-wiki
<section begin="title" />
This is a MassMessage title
<section end="title" />
<section begin="body" />
This is a MassMessage body
<section end="body" />
<section begin="misc" />
foobar
<section end="misc" />
e4boq8e0czn5f4qmy1nfrfqu85vx0ms
769606
769604
2026-10-22T08:00:49Z
Dragoniez
52097
769606
wikitext
text/x-wiki
<section begin="title" />
This is a MassMessage title
<section end="title" />
<section begin="body" />
This is a MassMessage body!!!!!!!!!!!!!!!
<section end="body" />
<section begin="misc" />
foobar
<section end="misc" />
o2qekfb7n3js0w67ct0gsv0t2b4zeu4
MassMessage test/subscribers
0
179426
769602
2026-10-22T07:56:20Z
Dragoniez
52097
Dragoniez created page [[MassMessage test/subscribers]] with non-default content model "Mass message delivery list"
769602
MassMessageListContent
application/json
{
"description": "",
"targets": []
}
hazq0h8gc8ai267z5mdea9b6rullpcv
769603
769602
2026-10-22T07:57:00Z
Dragoniez
52097
Added [[:User talk:Dragoniez]]
769603
MassMessageListContent
application/json
{
"description": "",
"targets": [
{
"title": "User talk:Dragoniez"
}
]
}
tjbw67zs8vu6patvgme7jax4vvkl84o
769609
769603
2026-10-22T08:02:40Z
Dragoniez
52097
Removed [[:User talk:Dragoniez]]
769609
MassMessageListContent
application/json
{
"description": "",
"targets": []
}
hazq0h8gc8ai267z5mdea9b6rullpcv
User talk:Dragoniez
3
179427
769605
2026-10-22T07:58:44Z
MediaWiki message delivery
23852
/* This is a MassMessage title */ new section
769605
wikitext
text/x-wiki
== This is a MassMessage title ==
<section begin="body" />
This is a MassMessage body
<section end="body" />
[[User:MediaWiki message delivery|MediaWiki message delivery]] ([[User talk:MediaWiki message delivery|talk]]) 07:58, 22 October 2026 (UTC)
<!-- Message sent by User:Dragoniez@testwiki using the list at https://test.wikipedia.org/w/index.php?title=MassMessage_test/subscribers&oldid=769603 -->
geln1smig6zc7qzp4n6vekskj9mr5gk
769607
769605
2026-10-22T08:00:59Z
MediaWiki message delivery
23852
/* This is a MassMessage title */ new section
769607
wikitext
text/x-wiki
== This is a MassMessage title ==
<section begin="body" />
This is a MassMessage body
<section end="body" />
[[User:MediaWiki message delivery|MediaWiki message delivery]] ([[User talk:MediaWiki message delivery|talk]]) 07:58, 22 October 2026 (UTC)
<!-- Message sent by User:Dragoniez@testwiki using the list at https://test.wikipedia.org/w/index.php?title=MassMessage_test/subscribers&oldid=769603 -->
== This is a MassMessage title ==
<section begin="body" />
This is a MassMessage body!!!!!!!!!!!!!!!
<section end="body" />
[[User:MediaWiki message delivery|MediaWiki message delivery]] ([[User talk:MediaWiki message delivery|talk]]) 08:00, 22 October 2026 (UTC)
<!-- Message sent by User:Dragoniez@testwiki using the list at https://test.wikipedia.org/w/index.php?title=MassMessage_test/subscribers&oldid=769603 -->
q4xiz9o0vyb9tipof05uwi7fa2hwc4c
769608
769607
2026-10-22T08:02:36Z
Dragoniez
52097
Blanked the page
769608
wikitext
text/x-wiki
phoiac9h4m842xq45sp7s6u21eteeq1
Pacific Man and the Biosphere Network
0
179428
769634
2026-10-22T10:25:28Z
SolarrFlare
76345
Created page with " == Introduction == ----The '''Pacific Man and the Biosphere Network''' ('''PacMAB''') is a regional network of national committees, biosphere reserve managers, and island conservation practitioners operating under [[UNESCO]]'s [[Man and the Biosphere Programme]] (MAB). Established in 2006, the network serves as a regional platform for cooperation, knowledge exchange, and capacity building among Pacific Island countries and territories. PacMAB focuses on addressing the u..."
769634
wikitext
text/x-wiki
== Introduction ==
----The '''Pacific Man and the Biosphere Network''' ('''PacMAB''') is a regional network of national committees, biosphere reserve managers, and island conservation practitioners operating under [[UNESCO]]'s [[Man and the Biosphere Programme]] (MAB). Established in 2006, the network serves as a regional platform for cooperation, knowledge exchange, and capacity building among Pacific Island countries and territories. PacMAB focuses on addressing the unique environmental, ecological, and socio-economic challenges faced by small island developing states (SIDS) and coastal island communities in the Pacific region.<ref>{{Cite report |url=https://doi.org/10.54677/kait4391 |title=UNESCO rekomendacija dėl atvirojo mokslo |date=2022 |publisher=UNESCO}}</ref>
54pfkedaxhvkp9qmov7j8fdy27vygyy
769637
769634
2026-10-22T10:26:21Z
Pqnn
76362
769637
wikitext
text/x-wiki
== Introduction ==
----The '''Pacific Man and the Biosphere Network''' ('''PacMAB''') is a regional network of national committees, biosphere reserve managers, and island conservation practitioners operating under [[UNESCO]]'s [[Man and the Biosphere Programme]] (MAB). Established in 2006, the network serves as a regional platform for cooperation, knowledge exchange, and capacity building among Pacific Island countries and territories. PacMAB focuses on addressing the unique environmental, ecological, and socio-economic challenges faced by small island developing states (SIDS) and coastal island communities in the Pacific region.<ref>{{Cite report |url=https://doi.org/10.54677/kait4391 |title=UNESCO rekomendacija dėl atvirojo mokslo |date=2022 |publisher=UNESCO}}</ref>
<references />
== History ==
The Pacific Man and the Biosphere Network (PacMAB) was established in December 2006. It was created to support cooperation between Pacific Island countries and to help new and developing biosphere reserves share knowledge and experience. The network also connects national focal points of UNESCO's Man and the Biosphere Programme (MAB).<ref name="UNESCO2014">{{cite web |date=16 April 2014 |title=Pacific Biosphere Reserve Network meet to discuss their role in sustainable development |url=https://www.unesco.org/en/articles/pacific-biosphere-reserve-network-meet-discuss-their-role-sustainable-development |access-date=22 October 2026 |publisher=UNESCO}}</ref><ref name="TechnicalGuidelines">{{cite web |title=Technical Guidelines for Biosphere Reserves |url=https://phbr.penanghill.gov.my/wp-content/uploads/2023/05/Technical-Guideline-For-Biosphere-Reserve.pdf |access-date=22 October 2026 |publisher=UNESCO}}</ref>
In April 2014, PacMAB held its fourth network meeting in Nadi, Fiji. Representatives, biosphere reserve managers and researchers met from 23 to 25 April to discuss how biosphere reserves could support sustainable development in the Pacific. They reviewed progress and challenges under the Madrid Action Plan and discussed future actions for biosphere reserves in the region. The meeting also aimed to encourage more Pacific sites to become biosphere reserves.<ref name="UNESCO2014" /><ref name="SDGKnowledgeHub">{{cite web |title=PacMAB Commits to Revitalizing Pacific Isle Conservation |url=https://sdg.iisd.org/news/pacmab-commits-to-revitalizing-pacific-isle-conservation/ |access-date=22 October 2026 |publisher=International Institute for Sustainable Development}}</ref>
0ikaymmz9hskyts8z76ca56xg44m2ah
769638
769637
2026-10-22T10:27:38Z
VincenzoItaliano
76337
769638
wikitext
text/x-wiki
== Introduction ==
----The '''Pacific Man and the Biosphere Network''' ('''PacMAB''') is a regional network of national committees, biosphere reserve managers, and island conservation practitioners operating under [[UNESCO]]'s [[Man and the Biosphere Programme]] (MAB). Established in 2006, the network serves as a regional platform for cooperation, knowledge exchange, and capacity building among Pacific Island countries and territories. PacMAB focuses on addressing the unique environmental, ecological, and socio-economic challenges faced by small island developing states (SIDS) and coastal island communities in the Pacific region.<ref>{{Cite report |url=https://doi.org/10.54677/kait4391 |title=UNESCO rekomendacija dėl atvirojo mokslo |date=2022 |publisher=UNESCO}}</ref>
<references />
== History ==
The Pacific Man and the Biosphere Network (PacMAB) was established in December 2006. It was created to support cooperation between Pacific Island countries and to help new and developing biosphere reserves share knowledge and experience. The network also connects national focal points of UNESCO's Man and the Biosphere Programme (MAB).<ref name="UNESCO2014">{{cite web |date=16 April 2014 |title=Pacific Biosphere Reserve Network meet to discuss their role in sustainable development |url=https://www.unesco.org/en/articles/pacific-biosphere-reserve-network-meet-discuss-their-role-sustainable-development |access-date=22 October 2026 |publisher=UNESCO}}</ref><ref name="TechnicalGuidelines">{{cite web |title=Technical Guidelines for Biosphere Reserves |url=https://phbr.penanghill.gov.my/wp-content/uploads/2023/05/Technical-Guideline-For-Biosphere-Reserve.pdf |access-date=22 October 2026 |publisher=UNESCO}}</ref>
In April 2014, PacMAB held its fourth network meeting in Nadi, Fiji. Representatives, biosphere reserve managers and researchers met from 23 to 25 April to discuss how biosphere reserves could support sustainable development in the Pacific. They reviewed progress and challenges under the Madrid Action Plan and discussed future actions for biosphere reserves in the region. The meeting also aimed to encourage more Pacific sites to become biosphere reserves.<ref name="UNESCO2014" /><ref name="SDGKnowledgeHub">{{cite web |title=PacMAB Commits to Revitalizing Pacific Isle Conservation |url=https://sdg.iisd.org/news/pacmab-commits-to-revitalizing-pacific-isle-conservation/ |access-date=22 October 2026 |publisher=International Institute for Sustainable Development}}</ref>
<references />
== Objectives and activities ==
The primary objective of PacMAB is to promote sustainable development, biodiversity conservation and climate change resilience tailored to small island environments. Its key goals and activities include:
* '''Support for Biosphere Reserve Nominations:''' Providing technical assistance and peer reviews to assist member states in nominating and managing sites under UNESCO's MAB framework.
* '''Traditional Ecological Knowledge Integration:''' Promoting the inclusion of local community governance, customary land tenure and traditional ecological management practices alongside modern conservation science.
* '''Climate Resilience and Adaptation:''' Facilitating resource sharing and monitoring initiatives focused on the impact of sea-level rise, ocean acidification and extreme weather events on island ecosystems and coastal communities.
* '''Capacity Building and Training:''' Organizing regional workshops, technical training sessions and field meetings for reserve managers, government officials and community leaders.
Documented activities include periodic network meetings held across the region (such as in Fiji, [[Samoa]], and [[Palau]]) to review progress, share management practices and align regional actions with global frameworks such as the Lima Action Plan for UNESCO's MAB Programme.
ax3qp2ohpjb2d5ybs6bohob37ouww6s
769646
769638
2026-10-22T10:30:58Z
SolarrFlare
76345
769646
wikitext
text/x-wiki
== Introduction ==
----The '''Pacific Man and the Biosphere Network''' ('''PacMAB''') is a regional network of national committees, biosphere reserve managers, and island conservation practitioners operating under [[UNESCO]]'s [[Man and the Biosphere Programme]] (MAB). Established in 2006, the network serves as a regional platform for cooperation, knowledge exchange, and capacity building among Pacific Island countries and territories. PacMAB focuses on addressing the unique environmental, ecological, and socio-economic challenges faced by small island developing states (SIDS) and coastal island communities in the Pacific region.<ref name="UNESCO 20221">{{Cite report |url=https://doi.org/10.54677/kait4391 |title=UNESCO rekomendacija dėl atvirojo mokslo |date=2022 |publisher=UNESCO}}</ref>
<references />
== History ==
The Pacific Man and the Biosphere Network (PacMAB) was established in December 2006. It was created to support cooperation between Pacific Island countries and to help new and developing biosphere reserves share knowledge and experience. The network also connects national focal points of UNESCO's Man and the Biosphere Programme (MAB).<ref name="UNESCO2014">{{cite web |date=16 April 2014 |title=Pacific Biosphere Reserve Network meet to discuss their role in sustainable development |url=https://www.unesco.org/en/articles/pacific-biosphere-reserve-network-meet-discuss-their-role-sustainable-development |access-date=22 October 2026 |publisher=UNESCO}}</ref><ref name="TechnicalGuidelines">{{cite web |title=Technical Guidelines for Biosphere Reserves |url=https://phbr.penanghill.gov.my/wp-content/uploads/2023/05/Technical-Guideline-For-Biosphere-Reserve.pdf |access-date=22 October 2026 |publisher=UNESCO}}</ref>
In April 2014, PacMAB held its fourth network meeting in Nadi, Fiji. Representatives, biosphere reserve managers and researchers met from 23 to 25 April to discuss how biosphere reserves could support sustainable development in the Pacific. They reviewed progress and challenges under the Madrid Action Plan and discussed future actions for biosphere reserves in the region. The meeting also aimed to encourage more Pacific sites to become biosphere reserves.<ref name="UNESCO2014" /><ref name="SDGKnowledgeHub">{{cite web |title=PacMAB Commits to Revitalizing Pacific Isle Conservation |url=https://sdg.iisd.org/news/pacmab-commits-to-revitalizing-pacific-isle-conservation/ |access-date=22 October 2026 |publisher=International Institute for Sustainable Development}}</ref>
<references />
== Objectives and activities ==
The primary objective of PacMAB is to promote sustainable development, biodiversity conservation and climate change resilience tailored to small island environments. Its key goals and activities include:
* '''Support for Biosphere Reserve Nominations:''' Providing technical assistance and peer reviews to assist member states in nominating and managing sites under UNESCO's MAB framework.
* '''Traditional Ecological Knowledge Integration:''' Promoting the inclusion of local community governance, customary land tenure and traditional ecological management practices alongside modern conservation science.
* '''Climate Resilience and Adaptation:''' Facilitating resource sharing and monitoring initiatives focused on the impact of sea-level rise, ocean acidification and extreme weather events on island ecosystems and coastal communities.
* '''Capacity Building and Training:''' Organizing regional workshops, technical training sessions and field meetings for reserve managers, government officials and community leaders.
Documented activities include periodic network meetings held across the region (such as in Fiji, [[Samoa]], and [[Palau]]) to review progress, share management practices and align regional actions with global frameworks such as the Lima Action Plan for UNESCO's MAB Programme.
== Organization and cooperation ==
PacMAB operates under the coordination of the [[UNESCO]] Office for the Pacific States, based in [[Apia]], Samoa, in close consultation with MAB National Committees and national environment departments of participating Pacific nations.<ref name="UNESCO 20221" />
The network collaborates with regional bodies, including the [[Secretariat of the Pacific Regional Environment Programme]] (SPREP), local non-governmental organizations, local customary chiefs, and academic institutions across the region. PacMAB also interacts with other regional MAB networks under UNESCO's World Network of Biosphere Reserves, such as the Southeast Asian Biosphere Reserve Network ([[SeaBRnet]]) and the East Asian Biosphere Reserve Network ([[EABRN]]).
5vgo9gwsmyaq98scn25jxqr1rzmlpo7
769651
769646
2026-10-22T10:33:09Z
Pqnn
76362
769651
wikitext
text/x-wiki
== Introduction ==
----The '''Pacific Man and the Biosphere Network''' ('''PacMAB''') is a regional network of national committees, biosphere reserve managers, and island conservation practitioners operating under [[UNESCO]]'s [[Man and the Biosphere Programme]] (MAB). Established in 2006, the network serves as a regional platform for cooperation, knowledge exchange, and capacity building among Pacific Island countries and territories. PacMAB focuses on addressing the unique environmental, ecological, and socio-economic challenges faced by small island developing states (SIDS) and coastal island communities in the Pacific region.<ref name="UNESCO 20221">{{Cite report |url=https://doi.org/10.54677/kait4391 |title=UNESCO rekomendacija dėl atvirojo mokslo |date=2022 |publisher=UNESCO}}</ref>
<references />
== History ==
The Pacific Man and the Biosphere Network (PacMAB) was established in December 2006. It was created to support cooperation between Pacific Island countries and to help new and developing biosphere reserves share knowledge and experience. The network also connects national focal points of UNESCO's Man and the Biosphere Programme (MAB).<ref name="UNESCO2014">{{cite web |date=16 April 2014 |title=Pacific Biosphere Reserve Network meet to discuss their role in sustainable development |url=https://www.unesco.org/en/articles/pacific-biosphere-reserve-network-meet-discuss-their-role-sustainable-development |access-date=22 October 2026 |publisher=UNESCO}}</ref><ref name="TechnicalGuidelines">{{cite web |title=Technical Guidelines for Biosphere Reserves |url=https://phbr.penanghill.gov.my/wp-content/uploads/2023/05/Technical-Guideline-For-Biosphere-Reserve.pdf |access-date=22 October 2026 |publisher=UNESCO}}</ref>
In April 2014, PacMAB held its fourth network meeting in Nadi, Fiji. Representatives, biosphere reserve managers and researchers met from 23 to 25 April to discuss how biosphere reserves could support sustainable development in the Pacific. They reviewed progress and challenges under the Madrid Action Plan and discussed future actions for biosphere reserves in the region. The meeting also aimed to encourage more Pacific sites to become biosphere reserves.<ref name="UNESCO2014" /><ref name="SDGKnowledgeHub">{{cite web |title=PacMAB Commits to Revitalizing Pacific Isle Conservation |url=https://sdg.iisd.org/news/pacmab-commits-to-revitalizing-pacific-isle-conservation/ |access-date=22 October 2026 |publisher=International Institute for Sustainable Development}}</ref>
<references />
== Objectives and activities ==
The primary objective of PacMAB is to promote sustainable development, biodiversity conservation and climate change resilience tailored to small island environments. Its key goals and activities include:
* '''Support for Biosphere Reserve Nominations:''' Providing technical assistance and peer reviews to assist member states in nominating and managing sites under UNESCO's MAB framework.
* '''Traditional Ecological Knowledge Integration:''' Promoting the inclusion of local community governance, customary land tenure and traditional ecological management practices alongside modern conservation science.
* '''Climate Resilience and Adaptation:''' Facilitating resource sharing and monitoring initiatives focused on the impact of sea-level rise, ocean acidification and extreme weather events on island ecosystems and coastal communities.
* '''Capacity Building and Training:''' Organizing regional workshops, technical training sessions and field meetings for reserve managers, government officials and community leaders.
Documented activities include periodic network meetings held across the region (such as in Fiji, [[Samoa]], and [[Palau]]) to review progress, share management practices and align regional actions with global frameworks such as the Lima Action Plan for UNESCO's MAB Programme.
== Organization and cooperation ==
PacMAB operates under the coordination of the [[UNESCO]] Office for the Pacific States, based in [[Apia]], Samoa, in close consultation with MAB National Committees and national environment departments of participating Pacific nations.<ref name="UNESCO 20221" />
The network collaborates with regional bodies, including the [[Secretariat of the Pacific Regional Environment Programme]] (SPREP), local non-governmental organizations, local customary chiefs, and academic institutions across the region. PacMAB also interacts with other regional MAB networks under UNESCO's World Network of Biosphere Reserves, such as the Southeast Asian Biosphere Reserve Network ([[SeaBRnet]]) and the East Asian Biosphere Reserve Network ([[EABRN]]).
<references />
== See also ==
* [[Man and the Biosphere Programme]]
* [[World Network of Biosphere Reserves]]
* [[Biosphere reserve]]
* [[UNESCO]]
* [[Sustainable development]]
* [[Small Island Developing States]]
* [[Climate change]]
* [[Secretariat of the Pacific Regional Environment Programme]]
* [[Asia-Pacific Network for Global Change Research]]
gjz4um5c0hk84jscvrynl9d1t141j0t
769656
769651
2026-10-22T10:34:43Z
VincenzoItaliano
76337
769656
wikitext
text/x-wiki
== Introduction ==
----The '''Pacific Man and the Biosphere Network''' ('''PacMAB''') is a regional network of national committees, biosphere reserve managers, and island conservation practitioners operating under [[UNESCO]]'s [[Man and the Biosphere Programme]] (MAB). Established in 2006, the network serves as a regional platform for cooperation, knowledge exchange, and capacity building among Pacific Island countries and territories. PacMAB focuses on addressing the unique environmental, ecological, and socio-economic challenges faced by small island developing states (SIDS) and coastal island communities in the Pacific region.<ref name="UNESCO 20221">{{Cite report |url=https://doi.org/10.54677/kait4391 |title=UNESCO rekomendacija dėl atvirojo mokslo |date=2022 |publisher=UNESCO}}</ref>
<references />
== History ==
The Pacific Man and the Biosphere Network (PacMAB) was established in December 2006. It was created to support cooperation between Pacific Island countries and to help new and developing biosphere reserves share knowledge and experience. The network also connects national focal points of UNESCO's Man and the Biosphere Programme (MAB).<ref name="UNESCO2014">{{cite web |date=16 April 2014 |title=Pacific Biosphere Reserve Network meet to discuss their role in sustainable development |url=https://www.unesco.org/en/articles/pacific-biosphere-reserve-network-meet-discuss-their-role-sustainable-development |access-date=22 October 2026 |publisher=UNESCO}}</ref><ref name="TechnicalGuidelines">{{cite web |title=Technical Guidelines for Biosphere Reserves |url=https://phbr.penanghill.gov.my/wp-content/uploads/2023/05/Technical-Guideline-For-Biosphere-Reserve.pdf |access-date=22 October 2026 |publisher=UNESCO}}</ref>
In April 2014, PacMAB held its fourth network meeting in Nadi, Fiji. Representatives, biosphere reserve managers and researchers met from 23 to 25 April to discuss how biosphere reserves could support sustainable development in the Pacific. They reviewed progress and challenges under the Madrid Action Plan and discussed future actions for biosphere reserves in the region. The meeting also aimed to encourage more Pacific sites to become biosphere reserves.<ref name="UNESCO2014" /><ref name="SDGKnowledgeHub">{{cite web |title=PacMAB Commits to Revitalizing Pacific Isle Conservation |url=https://sdg.iisd.org/news/pacmab-commits-to-revitalizing-pacific-isle-conservation/ |access-date=22 October 2026 |publisher=International Institute for Sustainable Development}}</ref>
<references />
== Objectives and activities ==
The primary objective of PacMAB is to promote sustainable development, biodiversity conservation and climate change resilience tailored to small island environments. Its key goals and activities include:
* '''Support for Biosphere Reserve Nominations:''' Providing technical assistance and peer reviews to assist member states in nominating and managing sites under UNESCO's MAB framework.
* '''Traditional Ecological Knowledge Integration:''' Promoting the inclusion of local community governance, customary land tenure and traditional ecological management practices alongside modern conservation science.
* '''Climate Resilience and Adaptation:''' Facilitating resource sharing and monitoring initiatives focused on the impact of sea-level rise, ocean acidification and extreme weather events on island ecosystems and coastal communities.
* '''Capacity Building and Training:''' Organizing regional workshops, technical training sessions and field meetings for reserve managers, government officials and community leaders.
Documented activities include periodic network meetings held across the region (such as in Fiji, [[Samoa]], and [[Palau]]) to review progress, share management practices and align regional actions with global frameworks such as the Lima Action Plan for UNESCO's MAB Programme.
== Organization and cooperation ==
PacMAB operates under the coordination of the [[UNESCO]] Office for the Pacific States, based in [[Apia]], Samoa, in close consultation with MAB National Committees and national environment departments of participating Pacific nations.<ref name="UNESCO 20221" />
The network collaborates with regional bodies, including the [[Secretariat of the Pacific Regional Environment Programme]] (SPREP), local non-governmental organizations, local customary chiefs, and academic institutions across the region. PacMAB also interacts with other regional MAB networks under UNESCO's World Network of Biosphere Reserves, such as the Southeast Asian Biosphere Reserve Network ([[SeaBRnet]]) and the East Asian Biosphere Reserve Network ([[EABRN]]).
<references />
== See also ==
* [[Man and the Biosphere Programme]]
* [[World Network of Biosphere Reserves]]
* [[Biosphere reserve]]
* [[UNESCO]]
* [[Sustainable development]]
* [[Small Island Developing States]]
* [[Climate change]]
* [[Secretariat of the Pacific Regional Environment Programme]]
* [[Asia-Pacific Network for Global Change Research]]
== References ==
{{reflist|refs=<ref name="UNESCO_PacMAB">{{cite web |title=Pacific Man and the Biosphere Network (PacMAB) |url=https://www.unesco.org/en/mab/networks/pacmab |publisher=UNESCO |access-date=2026-10-21}}</ref>
<ref name="PacMAB_Establishment">{{cite web |title=Establishment of the Pacific Man and the Biosphere Network |url=https://www.unesco.org/en/articles/pacific-man-and-biosphere-network |publisher=UNESCO |access-date=2026-10-21}}</ref>}}
== External links ==
* [https://www.unesco.org/en/mab/networks UNESCO Man and the Biosphere Regional Networks]
* [https://www.unesco.org/en/fieldoffice/apia UNESCO Office for the Pacific States]
my74dn5ohrch3fxnr11nbgu5jfnxof8
769659
769656
2026-10-22T10:36:22Z
Pqnn
76362
769659
wikitext
text/x-wiki
== Introduction ==
----The '''Pacific Man and the Biosphere Network''' ('''PacMAB''') is a regional network of national committees, biosphere reserve managers, and island conservation practitioners operating under [[UNESCO]]'s [[Man and the Biosphere Programme]] (MAB). Established in 2006, the network serves as a regional platform for cooperation, knowledge exchange, and capacity building among Pacific Island countries and territories. PacMAB focuses on addressing the unique environmental, ecological, and socio-economic challenges faced by small island developing states (SIDS) and coastal island communities in the Pacific region.<ref name="UNESCO 20221">{{Cite report |url=https://doi.org/10.54677/kait4391 |title=UNESCO rekomendacija dėl atvirojo mokslo |date=2022 |publisher=UNESCO}}</ref>
<references />
== History ==
{| class="wikitable" style="float:right; margin:0 0 1em 1em;"
|+ PacMAB member countries
! Country
! Region
|-
| [[Federated States of Micronesia]]
| [[Micronesia]]
|-
| [[Kiribati]]
| [[Micronesia]] / [[Polynesia]]
|-
| [[Palau]]
| [[Micronesia]]
|-
| [[Papua New Guinea]]
| [[Melanesia]]
|-
| [[Samoa]]
| [[Polynesia]]
|-
| [[Tonga]]
| [[Polynesia]]
|}
The Pacific Man and the Biosphere Network (PacMAB) was established in December 2006. It was created to support cooperation between Pacific Island countries and to help new and developing biosphere reserves share knowledge and experience. The network also connects national focal points of UNESCO's Man and the Biosphere Programme (MAB).<ref name="UNESCO2014">{{cite web |date=16 April 2014 |title=Pacific Biosphere Reserve Network meet to discuss their role in sustainable development |url=https://www.unesco.org/en/articles/pacific-biosphere-reserve-network-meet-discuss-their-role-sustainable-development |access-date=22 October 2026 |publisher=UNESCO}}</ref><ref name="TechnicalGuidelines">{{cite web |title=Technical Guidelines for Biosphere Reserves |url=https://phbr.penanghill.gov.my/wp-content/uploads/2023/05/Technical-Guideline-For-Biosphere-Reserve.pdf |access-date=22 October 2026 |publisher=UNESCO}}</ref>
In April 2014, PacMAB held its fourth network meeting in Nadi, Fiji. Representatives, biosphere reserve managers and researchers met from 23 to 25 April to discuss how biosphere reserves could support sustainable development in the Pacific. They reviewed progress and challenges under the Madrid Action Plan and discussed future actions for biosphere reserves in the region. The meeting also aimed to encourage more Pacific sites to become biosphere reserves.<ref name="UNESCO2014" /><ref name="SDGKnowledgeHub">{{cite web |title=PacMAB Commits to Revitalizing Pacific Isle Conservation |url=https://sdg.iisd.org/news/pacmab-commits-to-revitalizing-pacific-isle-conservation/ |access-date=22 October 2026 |publisher=International Institute for Sustainable Development}}</ref>
<references />
== Objectives and activities ==
The primary objective of PacMAB is to promote sustainable development, biodiversity conservation and climate change resilience tailored to small island environments. Its key goals and activities include:
* '''Support for Biosphere Reserve Nominations:''' Providing technical assistance and peer reviews to assist member states in nominating and managing sites under UNESCO's MAB framework.
* '''Traditional Ecological Knowledge Integration:''' Promoting the inclusion of local community governance, customary land tenure and traditional ecological management practices alongside modern conservation science.
* '''Climate Resilience and Adaptation:''' Facilitating resource sharing and monitoring initiatives focused on the impact of sea-level rise, ocean acidification and extreme weather events on island ecosystems and coastal communities.
* '''Capacity Building and Training:''' Organizing regional workshops, technical training sessions and field meetings for reserve managers, government officials and community leaders.
Documented activities include periodic network meetings held across the region (such as in Fiji, [[Samoa]], and [[Palau]]) to review progress, share management practices and align regional actions with global frameworks such as the Lima Action Plan for UNESCO's MAB Programme.
== Organization and cooperation ==
PacMAB operates under the coordination of the [[UNESCO]] Office for the Pacific States, based in [[Apia]], Samoa, in close consultation with MAB National Committees and national environment departments of participating Pacific nations.<ref name="UNESCO 20221" />
The network collaborates with regional bodies, including the [[Secretariat of the Pacific Regional Environment Programme]] (SPREP), local non-governmental organizations, local customary chiefs, and academic institutions across the region. PacMAB also interacts with other regional MAB networks under UNESCO's World Network of Biosphere Reserves, such as the Southeast Asian Biosphere Reserve Network ([[SeaBRnet]]) and the East Asian Biosphere Reserve Network ([[EABRN]]).
<references />
== See also ==
* [[Man and the Biosphere Programme]]
* [[World Network of Biosphere Reserves]]
* [[Biosphere reserve]]
* [[UNESCO]]
* [[Sustainable development]]
* [[Small Island Developing States]]
* [[Climate change]]
* [[Secretariat of the Pacific Regional Environment Programme]]
* [[Asia-Pacific Network for Global Change Research]]
== References ==
{{reflist|refs=<ref name="UNESCO_PacMAB">{{cite web |title=Pacific Man and the Biosphere Network (PacMAB) |url=https://www.unesco.org/en/mab/networks/pacmab |publisher=UNESCO |access-date=2026-10-21}}</ref>
<ref name="PacMAB_Establishment">{{cite web |title=Establishment of the Pacific Man and the Biosphere Network |url=https://www.unesco.org/en/articles/pacific-man-and-biosphere-network |publisher=UNESCO |access-date=2026-10-21}}</ref>}}
== External links ==
* [https://www.unesco.org/en/mab/networks UNESCO Man and the Biosphere Regional Networks]
* [https://www.unesco.org/en/fieldoffice/apia UNESCO Office for the Pacific States]
qci8q70hdfjrbi7jzjzaukrjgphkd49
769669
769659
2026-10-22T10:41:29Z
Pqnn
76362
769669
wikitext
text/x-wiki
== Introduction ==
The '''Pacific Man and the Biosphere Network''' ('''PacMAB''') is a regional network of national committees, biosphere reserve managers, and island conservation practitioners operating under [[:en:UNESCO|UNESCO]]'s [[:en:Man and the Biosphere Programme|Man and the Biosphere Programme]] (MAB). Established in 2006, the network serves as a regional platform for cooperation, knowledge exchange, and capacity building among Pacific Island countries and territories. PacMAB focuses on addressing the unique environmental, ecological, and socio-economic challenges faced by small island developing states (SIDS) and coastal island communities in the Pacific region.<ref name="UNESCO 20221">{{Cite report |url=https://doi.org/10.54677/kait4391 |title=UNESCO rekomendacija dėl atvirojo mokslo |date=2022 |publisher=UNESCO}}</ref>
== History ==
{| class="wikitable" style="float:right; margin:0 0 1em 1em;"
|+ PacMAB member countries
! Country
! Region
|-
| [[:en:Federated States of Micronesia|Federated States of Micronesia]]
| [[:en:Micronesia (region)|Micronesia]]
|-
| [[:en:Kiribati|Kiribati]]
| [[:en:Micronesia (region)|Micronesia]] / [[:en:Polynesia|Polynesia]]
|-
| [[:en:Palau|Palau]]
| [[:en:Micronesia (region)|Micronesia]]
|-
| [[:en:Papua New Guinea|Papua New Guinea]]
| [[:en:Melanesia|Melanesia]]
|-
| [[:en:Samoa|Samoa]]
| [[:en:Polynesia|Polynesia]]
|-
| [[:en:Tonga|Tonga]]
| [[:en:Polynesia|Polynesia]]
|}
The Pacific Man and the Biosphere Network (PacMAB) was established in December 2006. It was created to support cooperation between Pacific Island countries and to help new and developing biosphere reserves share knowledge and experience. The network also connects national focal points of UNESCO's Man and the Biosphere Programme (MAB).<ref name="UNESCO2014">{{cite web |date=16 April 2014 |title=Pacific Biosphere Reserve Network meet to discuss their role in sustainable development |url=https://www.unesco.org/en/articles/pacific-biosphere-reserve-network-meet-discuss-their-role-sustainable-development |access-date=22 October 2026 |publisher=UNESCO}}</ref><ref name="TechnicalGuidelines">{{cite web |title=Technical Guidelines for Biosphere Reserves |url=https://phbr.penanghill.gov.my/wp-content/uploads/2023/05/Technical-Guideline-For-Biosphere-Reserve.pdf |access-date=22 October 2026 |publisher=UNESCO}}</ref>
In April 2014, PacMAB held its fourth network meeting in Nadi, Fiji. Representatives, biosphere reserve managers and researchers met from 23 to 25 April to discuss how biosphere reserves could support sustainable development in the Pacific. They reviewed progress and challenges under the Madrid Action Plan and discussed future actions for biosphere reserves in the region. The meeting also aimed to encourage more Pacific sites to become biosphere reserves.<ref name="UNESCO2014" /><ref name="SDGKnowledgeHub">{{cite web |title=PacMAB Commits to Revitalizing Pacific Isle Conservation |url=https://sdg.iisd.org/news/pacmab-commits-to-revitalizing-pacific-isle-conservation/ |access-date=22 October 2026 |publisher=International Institute for Sustainable Development}}</ref>
== Objectives and activities ==
The primary objective of PacMAB is to promote sustainable development, biodiversity conservation and climate change resilience tailored to small island environments. Its key goals and activities include:
* '''Support for Biosphere Reserve Nominations:''' Providing technical assistance and peer reviews to assist member states in nominating and managing sites under UNESCO's MAB framework.
* '''Traditional Ecological Knowledge Integration:''' Promoting the inclusion of local community governance, customary land tenure and traditional ecological management practices alongside modern conservation science.
* '''Climate Resilience and Adaptation:''' Facilitating resource sharing and monitoring initiatives focused on the impact of sea-level rise, ocean acidification and extreme weather events on island ecosystems and coastal communities.
* '''Capacity Building and Training:''' Organizing regional workshops, technical training sessions and field meetings for reserve managers, government officials and community leaders.
Documented activities include periodic network meetings held across the region (such as in Fiji, [[:en:Samoa|Samoa]], and [[:en:Palau|Palau]]) to review progress, share management practices and align regional actions with global frameworks such as the Lima Action Plan for UNESCO's MAB Programme.
== Organization and cooperation ==
PacMAB operates under the coordination of the [[:en:UNESCO|UNESCO]] Office for the Pacific States, based in [[:en:Apia|Apia]], Samoa, in close consultation with MAB National Committees and national environment departments of participating Pacific nations.<ref name="UNESCO 20221" />
The network collaborates with regional bodies, including the [[:en:Secretariat of the Pacific Regional Environment Programme|Secretariat of the Pacific Regional Environment Programme]] (SPREP), local non-governmental organizations, local customary chiefs, and academic institutions across the region. PacMAB also interacts with other regional MAB networks under UNESCO's World Network of Biosphere Reserves, such as the Southeast Asian Biosphere Reserve Network (SeaBRnet) and the East Asian Biosphere Reserve Network (EABRN).
== See also ==
* [[:en:Man and the Biosphere Programme|Man and the Biosphere Programme]]
* [[:en:World Network of Biosphere Reserves|World Network of Biosphere Reserves]]
* [[:en:Biosphere reserve|Biosphere reserve]]
* [[:en:UNESCO|UNESCO]]
* [[:en:Sustainable development|Sustainable development]]
* [[:en:Small Island Developing States|Small Island Developing States]]
* [[:en:Climate change|Climate change]]
* [[:en:Secretariat of the Pacific Regional Environment Programme|Secretariat of the Pacific Regional Environment Programme]]
* [[:en:Asia-Pacific Network for Global Change Research|Asia-Pacific Network for Global Change Research]]
== References ==
{{reflist|refs=<ref name="UNESCO_PacMAB">{{cite web |title=Pacific Man and the Biosphere Network (PacMAB) |url=https://www.unesco.org/en/mab/networks/pacmab |publisher=UNESCO |access-date=2026-10-21}}</ref>
<ref name="PacMAB_Establishment">{{cite web |title=Establishment of the Pacific Man and the Biosphere Network |url=https://www.unesco.org/en/articles/pacific-man-and-biosphere-network |publisher=UNESCO |access-date=2026-10-21}}</ref>}}
== External links ==
* [https://www.unesco.org/en/mab/networks UNESCO Man and the Biosphere Regional Networks]
* [https://www.unesco.org/en/fieldoffice/apia UNESCO Office for the Pacific States]
dmx5e1wz2aztmq2rvur6kch9t3ezpjl
769708
769669
2026-10-22T11:25:07Z
Pqnn
76362
769708
wikitext
text/x-wiki
== Introduction ==
[[File:Oceania UN Geoscheme Regions.svg|thumb|right|350px|Regions of Oceania – [[:en:Micronesia (region)|Micronesia]], [[:en:Melanesia|Melanesia]] and [[:en:Polynesia|Polynesia]] – in which PacMAB member countries are located.]]
The '''Pacific Man and the Biosphere Network''' ('''PacMAB''') is a regional network of national committees, biosphere reserve managers, and island conservation practitioners operating under [[:en:UNESCO|UNESCO]]'s [[:en:Man and the Biosphere Programme|Man and the Biosphere Programme]] (MAB). Established in 2006, the network serves as a regional platform for cooperation, knowledge exchange, and capacity building among Pacific Island countries and territories. PacMAB focuses on addressing the unique environmental, ecological, and socio-economic challenges faced by small island developing states (SIDS) and coastal island communities in the Pacific region.<ref name="UNESCO 20221">{{Cite report |url=https://doi.org/10.54677/kait4391 |title=UNESCO rekomendacija dėl atvirojo mokslo |date=2022 |publisher=UNESCO}}</ref>
== History ==
{| class="wikitable" style="float:right; margin:0 0 1em 1em;"
|+ PacMAB member countries
! Country
! Region
|-
| [[:en:Federated States of Micronesia|Federated States of Micronesia]]
| [[:en:Micronesia (region)|Micronesia]]
|-
| [[:en:Kiribati|Kiribati]]
| [[:en:Micronesia (region)|Micronesia]] / [[:en:Polynesia|Polynesia]]
|-
| [[:en:Palau|Palau]]
| [[:en:Micronesia (region)|Micronesia]]
|-
| [[:en:Papua New Guinea|Papua New Guinea]]
| [[:en:Melanesia|Melanesia]]
|-
| [[:en:Samoa|Samoa]]
| [[:en:Polynesia|Polynesia]]
|-
| [[:en:Tonga|Tonga]]
| [[:en:Polynesia|Polynesia]]
|}
The Pacific Man and the Biosphere Network (PacMAB) was established in December 2006. It was created to support cooperation between Pacific Island countries and to help new and developing biosphere reserves share knowledge and experience. The network also connects national focal points of UNESCO's Man and the Biosphere Programme (MAB).<ref name="UNESCO2014">{{cite web |date=16 April 2014 |title=Pacific Biosphere Reserve Network meet to discuss their role in sustainable development |url=https://www.unesco.org/en/articles/pacific-biosphere-reserve-network-meet-discuss-their-role-sustainable-development |access-date=22 October 2026 |publisher=UNESCO}}</ref><ref name="TechnicalGuidelines">{{cite web |title=Technical Guidelines for Biosphere Reserves |url=https://phbr.penanghill.gov.my/wp-content/uploads/2023/05/Technical-Guideline-For-Biosphere-Reserve.pdf |access-date=22 October 2026 |publisher=UNESCO}}</ref>
In April 2014, PacMAB held its fourth network meeting in Nadi, Fiji. Representatives, biosphere reserve managers and researchers met from 23 to 25 April to discuss how biosphere reserves could support sustainable development in the Pacific. They reviewed progress and challenges under the Madrid Action Plan and discussed future actions for biosphere reserves in the region. The meeting also aimed to encourage more Pacific sites to become biosphere reserves.<ref name="UNESCO2014" /><ref name="SDGKnowledgeHub">{{cite web |title=PacMAB Commits to Revitalizing Pacific Isle Conservation |url=https://sdg.iisd.org/news/pacmab-commits-to-revitalizing-pacific-isle-conservation/ |access-date=22 October 2026 |publisher=International Institute for Sustainable Development}}</ref>
== Objectives and activities ==
The primary objective of PacMAB is to promote sustainable development, biodiversity conservation and climate change resilience tailored to small island environments. Its key goals and activities include:
* '''Support for Biosphere Reserve Nominations:''' Providing technical assistance and peer reviews to assist member states in nominating and managing sites under UNESCO's MAB framework.
* '''Traditional Ecological Knowledge Integration:''' Promoting the inclusion of local community governance, customary land tenure and traditional ecological management practices alongside modern conservation science.
* '''Climate Resilience and Adaptation:''' Facilitating resource sharing and monitoring initiatives focused on the impact of sea-level rise, ocean acidification and extreme weather events on island ecosystems and coastal communities.
* '''Capacity Building and Training:''' Organizing regional workshops, technical training sessions and field meetings for reserve managers, government officials and community leaders.
Documented activities include periodic network meetings held across the region (such as in Fiji, [[:en:Samoa|Samoa]], and [[:en:Palau|Palau]]) to review progress, share management practices and align regional actions with global frameworks such as the Lima Action Plan for UNESCO's MAB Programme.
== Organization and cooperation ==
PacMAB operates under the coordination of the [[:en:UNESCO|UNESCO]] Office for the Pacific States, based in [[:en:Apia|Apia]], Samoa, in close consultation with MAB National Committees and national environment departments of participating Pacific nations.<ref name="UNESCO 20221" />
The network collaborates with regional bodies, including the [[:en:Secretariat of the Pacific Regional Environment Programme|Secretariat of the Pacific Regional Environment Programme]] (SPREP), local non-governmental organizations, local customary chiefs, and academic institutions across the region. PacMAB also interacts with other regional MAB networks under UNESCO's World Network of Biosphere Reserves, such as the Southeast Asian Biosphere Reserve Network (SeaBRnet) and the East Asian Biosphere Reserve Network (EABRN).
== See also ==
* [[:en:Man and the Biosphere Programme|Man and the Biosphere Programme]]
* [[:en:World Network of Biosphere Reserves|World Network of Biosphere Reserves]]
* [[:en:Biosphere reserve|Biosphere reserve]]
* [[:en:UNESCO|UNESCO]]
* [[:en:Sustainable development|Sustainable development]]
* [[:en:Small Island Developing States|Small Island Developing States]]
* [[:en:Climate change|Climate change]]
* [[:en:Secretariat of the Pacific Regional Environment Programme|Secretariat of the Pacific Regional Environment Programme]]
* [[:en:Asia-Pacific Network for Global Change Research|Asia-Pacific Network for Global Change Research]]
== References ==
{{reflist|refs=<ref name="UNESCO_PacMAB">{{cite web |title=Pacific Man and the Biosphere Network (PacMAB) |url=https://www.unesco.org/en/mab/networks/pacmab |publisher=UNESCO |access-date=2026-10-21}}</ref>
<ref name="PacMAB_Establishment">{{cite web |title=Establishment of the Pacific Man and the Biosphere Network |url=https://www.unesco.org/en/articles/pacific-man-and-biosphere-network |publisher=UNESCO |access-date=2026-10-21}}</ref>}}
== External links ==
* [https://www.unesco.org/en/mab/networks UNESCO Man and the Biosphere Regional Networks]
* [https://www.unesco.org/en/fieldoffice/apia UNESCO Office for the Pacific States]
ao287lsp0sry69qwnk4i0wjc8rrbrs4
769710
769708
2026-10-22T11:27:48Z
Pqnn
76362
769710
wikitext
text/x-wiki
== Introduction ==
[[File:Oceania UN Geoscheme Regions.svg|thumb|right|350px|Regions of Oceania – [[:en:Micronesia (region)|Micronesia]], [[:en:Melanesia|Melanesia]] and [[:en:Polynesia|Polynesia]] – in which PacMAB member countries are located.]]
The '''Pacific Man and the Biosphere Network''' ('''PacMAB''') is a regional network of national committees, biosphere reserve managers, and island conservation practitioners operating under [[:en:UNESCO|UNESCO]]'s [[:en:Man and the Biosphere Programme|Man and the Biosphere Programme]] (MAB). Established in 2006, the network serves as a regional platform for cooperation, knowledge exchange, and capacity building among Pacific Island countries and territories. PacMAB focuses on addressing the unique environmental, ecological, and socio-economic challenges faced by small island developing states (SIDS) and coastal island communities in the Pacific region.<ref name="UNESCO_PacMAB" />
== History ==
{| class="wikitable" style="float:right; margin:0 0 1em 1em;"
|+ PacMAB member countries
! Country
! Region
|-
| [[:en:Federated States of Micronesia|Federated States of Micronesia]]
| [[:en:Micronesia (region)|Micronesia]]
|-
| [[:en:Kiribati|Kiribati]]
| [[:en:Micronesia (region)|Micronesia]] / [[:en:Polynesia|Polynesia]]
|-
| [[:en:Palau|Palau]]
| [[:en:Micronesia (region)|Micronesia]]
|-
| [[:en:Papua New Guinea|Papua New Guinea]]
| [[:en:Melanesia|Melanesia]]
|-
| [[:en:Samoa|Samoa]]
| [[:en:Polynesia|Polynesia]]
|-
| [[:en:Tonga|Tonga]]
| [[:en:Polynesia|Polynesia]]
|}
The Pacific Man and the Biosphere Network (PacMAB) was established in December 2006.<ref name="PacMAB_Establishment" /> It was created to support cooperation between Pacific Island countries and to help new and developing biosphere reserves share knowledge and experience. The network also connects national focal points of UNESCO's Man and the Biosphere Programme (MAB).<ref name="UNESCO2014">{{cite web |date=16 April 2014 |title=Pacific Biosphere Reserve Network meet to discuss their role in sustainable development |url=https://www.unesco.org/en/articles/pacific-biosphere-reserve-network-meet-discuss-their-role-sustainable-development |access-date=22 October 2026 |publisher=UNESCO}}</ref><ref name="TechnicalGuidelines">{{cite web |title=Technical Guidelines for Biosphere Reserves |url=https://phbr.penanghill.gov.my/wp-content/uploads/2023/05/Technical-Guideline-For-Biosphere-Reserve.pdf |access-date=22 October 2026 |publisher=UNESCO}}</ref>
In April 2014, PacMAB held its fourth network meeting in Nadi, Fiji. Representatives, biosphere reserve managers and researchers met from 23 to 25 April to discuss how biosphere reserves could support sustainable development in the Pacific. They reviewed progress and challenges under the Madrid Action Plan and discussed future actions for biosphere reserves in the region. The meeting also aimed to encourage more Pacific sites to become biosphere reserves.<ref name="UNESCO2014" /><ref name="SDGKnowledgeHub">{{cite web |title=PacMAB Commits to Revitalizing Pacific Isle Conservation |url=https://sdg.iisd.org/news/pacmab-commits-to-revitalizing-pacific-isle-conservation/ |access-date=22 October 2026 |publisher=International Institute for Sustainable Development}}</ref>
== Objectives and activities ==
The primary objective of PacMAB is to promote sustainable development, biodiversity conservation and climate change resilience tailored to small island environments. Its key goals and activities include:
* '''Support for Biosphere Reserve Nominations:''' Providing technical assistance and peer reviews to assist member states in nominating and managing sites under UNESCO's MAB framework.
* '''Traditional Ecological Knowledge Integration:''' Promoting the inclusion of local community governance, customary land tenure and traditional ecological management practices alongside modern conservation science.
* '''Climate Resilience and Adaptation:''' Facilitating resource sharing and monitoring initiatives focused on the impact of sea-level rise, ocean acidification and extreme weather events on island ecosystems and coastal communities.
* '''Capacity Building and Training:''' Organizing regional workshops, technical training sessions and field meetings for reserve managers, government officials and community leaders.
Documented activities include periodic network meetings held across the region (such as in Fiji, [[:en:Samoa|Samoa]], and [[:en:Palau|Palau]]) to review progress, share management practices and align regional actions with global frameworks such as the Lima Action Plan for UNESCO's MAB Programme.
== Organization and cooperation ==
PacMAB operates under the coordination of the [[:en:UNESCO|UNESCO]] Office for the Pacific States, based in [[:en:Apia|Apia]], Samoa, in close consultation with MAB National Committees and national environment departments of participating Pacific nations.<ref name="UNESCO_PacMAB" />
The network collaborates with regional bodies, including the [[:en:Secretariat of the Pacific Regional Environment Programme|Secretariat of the Pacific Regional Environment Programme]] (SPREP), local non-governmental organizations, local customary chiefs, and academic institutions across the region. PacMAB also interacts with other regional MAB networks under UNESCO's World Network of Biosphere Reserves, such as the Southeast Asian Biosphere Reserve Network (SeaBRnet) and the East Asian Biosphere Reserve Network (EABRN).
== See also ==
* [[:en:Man and the Biosphere Programme|Man and the Biosphere Programme]]
* [[:en:World Network of Biosphere Reserves|World Network of Biosphere Reserves]]
* [[:en:Biosphere reserve|Biosphere reserve]]
* [[:en:UNESCO|UNESCO]]
* [[:en:Sustainable development|Sustainable development]]
* [[:en:Small Island Developing States|Small Island Developing States]]
* [[:en:Climate change|Climate change]]
* [[:en:Secretariat of the Pacific Regional Environment Programme|Secretariat of the Pacific Regional Environment Programme]]
* [[:en:Asia-Pacific Network for Global Change Research|Asia-Pacific Network for Global Change Research]]
== References ==
{{reflist|refs=<ref name="UNESCO_PacMAB">{{cite web |title=Pacific Man and the Biosphere Network (PacMAB) |url=https://www.unesco.org/en/mab/networks/pacmab |publisher=UNESCO |access-date=2026-10-21}}</ref>
<ref name="PacMAB_Establishment">{{cite web |title=Establishment of the Pacific Man and the Biosphere Network |url=https://www.unesco.org/en/articles/pacific-man-and-biosphere-network |publisher=UNESCO |access-date=2026-10-21}}</ref>}}
== External links ==
* [https://www.unesco.org/en/mab/networks UNESCO Man and the Biosphere Regional Networks]
* [https://www.unesco.org/en/fieldoffice/apia UNESCO Office for the Pacific States]
1brlb4qj7gycwfhk2wr6nn7x8kqjfsz
User:MonetaVetus/sandbox
2
179429
769664
2026-10-22T10:38:33Z
MonetaVetus
76332
Created page with " == <big>Introduction</big> == editshare The Southeast Asia Biosphere Reserve Network (SeaBRnet) is a regional collaborative platform which established under UNESCO's Man and the Biosphere (MAB) Programme.The program was established in 1998. SeaBRnet aims to promote knowledge-sharing, capacity building among biosphere reserves. The network contains eight Southeasian countries and 51 biosphere reserves in it<ref>{{Cite web |last=UNESCO |date=04.08.2026 |title=UNESCO Jakar..."
769664
wikitext
text/x-wiki
== <big>Introduction</big> ==
editshare
The Southeast Asia Biosphere Reserve Network (SeaBRnet) is a regional collaborative platform which established under UNESCO's Man and the Biosphere (MAB) Programme.The program was established in 1998. SeaBRnet aims to promote knowledge-sharing, capacity building among biosphere reserves. The network contains eight Southeasian countries and 51 biosphere reserves in it<ref>{{Cite web |last=UNESCO |date=04.08.2026 |title=UNESCO Jakarta Natural Sciences Network |url=https://www.unesco.org/en/fieldoffice/jakarta/science-network/about-seabrnet |url-status=https://www.unesco.org/en/fieldoffice/jakarta/science-network/about-seabrnet |archive-url=https://www.unesco.org/en/fieldoffice/jakarta/science-network/about-seabrnet |archive-date=22.10.2026 |access-date=22.10.2026 |website=UNESCO Jakarta Natural Sciences Network}}</ref>.
hunwlis26a0mnfk5ja7as3y8718txz0
User:AdamWaheed1/sandbox
2
179430
769712
2026-10-22T11:27:51Z
AdamWaheed1
76335
Created page with "== Ibero-American MAB Network == The '''Ibero-American MAB Network''' ('''IberoMAB''') is a regional network established in 1992 under UNESCO's Man and the Biosphere (MAB) Programme. It connects national MAB committees and biosphere reserves in Latin America, the Caribbean, Spain and Portugal. Its aims include strengthening the MAB Programme, promoting cooperation among participating countries and supporting the creation of new biosphere reserves.<ref name="UNESCO-Ibero..."
769712
wikitext
text/x-wiki
== Ibero-American MAB Network ==
The '''Ibero-American MAB Network''' ('''IberoMAB''') is a regional network established in 1992 under UNESCO's Man and the Biosphere (MAB) Programme. It connects national MAB committees and biosphere reserves in Latin America, the Caribbean, Spain and Portugal. Its aims include strengthening the MAB Programme, promoting cooperation among participating countries and supporting the creation of new biosphere reserves.<ref name="UNESCO-IberoMAB">{{cite web |title=Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks |website=UNESCO |access-date=22 October 2026}}</ref>
== See also ==
* [[:en:Man and the Biosphere Programme]]
* [[:en:World Network of Biosphere Reserves]]
* [[:en:Biosphere reserve]]
ggbx0kb02dvy0zx60nrfbb84fyvzwux
User:AtomAnt9/sandbox
2
179431
769729
2026-10-22T11:40:00Z
AtomAnt9
76333
Created page with "== Ibero-American MAB Network == The '''Ibero-American MAB Network''' ('''IberoMAB''') is a regional network established in 1992 within the framework of the Man and the Biosphere (MAB) Programme of the United Nations Educational, Scientific and Cultural Organization (UNESCO). It is a link between countries in Latin America and the Caribbean and between Spain and Portugal to foster cooperation between biosphere reserves and sustainable development. The network's objective..."
769729
wikitext
text/x-wiki
== Ibero-American MAB Network ==
The '''Ibero-American MAB Network''' ('''IberoMAB''') is a regional network established in 1992 within the framework of the Man and the Biosphere (MAB) Programme of the United Nations Educational, Scientific and Cultural Organization (UNESCO). It is a link between countries in Latin America and the Caribbean and between Spain and Portugal to foster cooperation between biosphere reserves and sustainable development. The network's objectives are to support the strengthening of national MAB committees, foster cooperation among the member countries and encourage the establishment of new biosphere reserves.<ref name="UNESCO">{{cite web |title=Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks |website=UNESCO |access-date=21 October 2026}}</ref>
== History ==
The Ibero-American MAB Network was created in 1992 to strengthen cooperation among countries participating in UNESCO's Man and the Biosphere Programme. Its establishment provided a regional framework for sharing experiences and supporting biosphere reserves in Latin America, the Caribbean, Spain and Portugal.<ref name="UNESCO" />
In 2018, the network's Action Plan for 2018–2025 was endorsed at the XVIII IberoMAB Meeting in Santa Marta, Colombia. The plan provided a framework for cooperation and activities among member countries.<ref name="UNESCO" />
In October 2020, IberoMAB published a declaration addressing the COVID-19 pandemic and its effects on communities and the environment. The declaration emphasized sustainable recovery, community participation, scientific research and the role of biosphere reserves in protecting biodiversity and supporting human well-being.<ref name="Declaration">{{cite web |title=IberoMAB Biosphere Reserves pledge for a sustainable 'new normal' to overcome the COVID19 crisis |url=https://www.unesco.org/en/articles/iberomab-biosphere-reserves-pledge-sustainable-new-normal-overcome-covid19-crisis |website=UNESCO |date=15 October 2020 |access-date=21 October 2026}}</ref>
In 2023, the network held its second congress in the Trifinio-Fraternidad Transboundary Biosphere Reserve. Participants reviewed progress on regional cooperation and assessed the implementation of the Lima Action Plan 2016–2025 and the IberoMAB Action Plan 2018–2025.<ref name="UNESCO" />
== Geographic scope and membership ==
''This section has not yet been completed.''
== Objectives and activities ==
IberoMAB aims to strengthen the implementation of UNESCO's Man and the Biosphere Programme across its member countries. Its main objectives include:
* Strengthening national committees responsible for the MAB Programme.
* Improving cooperation and communication between member countries and biosphere reserves.
* Supporting the establishment and development of biosphere reserves.
* Promoting biodiversity conservation and sustainable development.
* Encouraging the exchange of scientific knowledge, experiences and good practices.<ref name="UNESCO" />
The network also supports initiatives addressing environmental and social challenges. In its 2020 declaration on the COVID-19 pandemic, IberoMAB encouraged community participation, the involvement of young people and vulnerable groups, scientific and technical research, and the integration of biodiversity and sustainable development into public policies.<ref name="Declaration" />
More recently, UNESCO has continued to support regional cooperation on biodiversity conservation, sustainable management, community participation and green economies through initiatives involving biosphere reserves in Latin America and the Caribbean and their international partners.<ref name="Project">{{cite web |title=MAB Secretariat support to strategic initiatives for sustainable development |url=https://core.unesco.org/en/project/3210211102 |website=UNESCO Core Data Portal |access-date=21 October 2026}}</ref>
== Organization and cooperation ==
IberoMAB operates within UNESCO's Man and the Biosphere Programme and brings together national MAB committees and representatives from participating countries. Its activities provide opportunities for members to exchange knowledge, discuss common challenges and coordinate regional priorities.<ref name="UNESCO" />
The network has a Bureau that supports its coordination. According to UNESCO's regional networks information, Honduras serves as president, Peru as vice-president and Spain as secretariat.<ref name="UNESCO" />
IberoMAB also cooperates with other regional and thematic networks within the World Network of Biosphere Reserves. This cooperation supports the sharing of expertise and the development of approaches to biodiversity conservation, sustainable development and environmental management.<ref name="Project" />
== See also ==
* [[Man and the Biosphere Programme]]
* [[World Network of Biosphere Reserves]]
* [[Biosphere reserve]]
* [[UNESCO]]
== References ==
{{reflist}}
== External links ==
* [https://www.unesco.org/en/mab/networks UNESCO – Regional and Thematic Networks]
* [https://www.unesco.org/en/mab/about UNESCO – About the Man and the Biosphere Programme]
* [https://www.unesco.org/en/mab/list UNESCO – List of Biosphere Reserves]
* [https://core.unesco.org/en/project/3210211102 UNESCO Core Data Portal – MAB strategic initiatives for sustainable development]
mqoygry23lazmkibcf5rrboc1crecvf
Wikipedia:Sandbox/MoveIt moved 2026-10-22T11-46-27-789Z
4
179432
769730
2026-10-22T11:46:28Z
Closed Limelike Curves
60396
MoveIt live rename test: creating the page to be renamed
769730
wikitext
text/x-wiki
Throwaway page for the Move It live rename test (2026-10-22T11-46-27-789Z). Safe to delete.
d7a3aagyg97hgg41tgm4nzytworv46a
769731
769730
2026-10-22T11:46:29Z
Closed Limelike Curves
60396
Closed Limelike Curves moved page [[Wikipedia:Sandbox/MoveIt move 2026-10-22T11-46-27-789Z]] to [[Wikipedia:Sandbox/MoveIt moved 2026-10-22T11-46-27-789Z]]: Move It live rename test (via Move It browser extension)
769730
wikitext
text/x-wiki
Throwaway page for the Move It live rename test (2026-10-22T11-46-27-789Z). Safe to delete.
d7a3aagyg97hgg41tgm4nzytworv46a
769734
769731
2026-10-22T11:46:32Z
Closed Limelike Curves
60396
MoveIt live rename test teardown
769734
wikitext
text/x-wiki
phoiac9h4m842xq45sp7s6u21eteeq1
Wikipedia:Sandbox/MoveIt move 2026-10-22T11-46-27-789Z
4
179433
769732
2026-10-22T11:46:29Z
Closed Limelike Curves
60396
Closed Limelike Curves moved page [[Wikipedia:Sandbox/MoveIt move 2026-10-22T11-46-27-789Z]] to [[Wikipedia:Sandbox/MoveIt moved 2026-10-22T11-46-27-789Z]]: Move It live rename test (via Move It browser extension)
769732
wikitext
text/x-wiki
#REDIRECT [[Wikipedia:Sandbox/MoveIt moved 2026-10-22T11-46-27-789Z]]
{{Redirect category shell|
{{R from move}}
}}
epnubsldx8b2pyt8f53fp7rrz465l10
769733
769732
2026-10-22T11:46:31Z
Closed Limelike Curves
60396
MoveIt live rename test teardown
769733
wikitext
text/x-wiki
phoiac9h4m842xq45sp7s6u21eteeq1
User:OrangeLemonadee/sandbox
2
179434
769735
2026-10-22T11:48:57Z
OrangeLemonadee
76341
.
769735
wikitext
text/x-wiki
== Organization and cooperation ==
IberoMAB operates within UNESCO's Man and the Biosphere Programme and brings together national MAB committees and representatives from participating countries. Its activities provide opportunities for members to exchange knowledge, discuss common challenges and coordinate regional priorities.<ref name="UNESCO">{{cite web |title=Regional and Thematic Networks |url=https://www.unesco.org/en/mab/networks |access-date=21 October 2026 |website=UNESCO}}</ref> The network has a Bureau that supports its coordination. According to UNESCO's regional networks information, Honduras serves as president, Peru as vice-president and Spain as secretariat.<ref name="UNESCO" /> IberoMAB also cooperates with other regional and thematic networks within the World Network of Biosphere Reserves. This cooperation supports the sharing of expertise and the development of approaches to biodiversity conservation, sustainable development and environmental management.|url=https://core.unesco.org/en/project/3210211102 |website=UNESCO Core Data Portal |access-date=21 October 2026}}<nowiki></ref></nowiki>
hy7sub5yl4cjop3ai1sz335blmg4wrj