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, '&#34;') + '">' + 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 &lt;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 = &middot; ); } 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, '&#34;') + '">' + 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 &lt;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 = &middot; ); } 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, '&#34;') + '">' + 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 &lt;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 = &middot; ); } 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,"&#34;")+'">'+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&&params.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&&params.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={"\\":"&#92;","&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;","/":"&#47;"};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">&times;</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()+'">&times;</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={"\\":"&#92;","&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;","/":"&#47;"};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">&times;</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()+'">&times;</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&&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();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&&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();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]&&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){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&&params.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&&sections.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> &bull; </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: '保護テンプレートを&lt;noinclude&gt;で囲む', tooltip: '参照読み込みされないように保護テンプレートを&lt;noinclude&gt;で囲む', 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: '保護テンプレートを&lt;noinclude&gt;で囲む', tooltip: '参照読み込みされないように保護依頼テンプレートを&lt;noinclude&gt;で囲む', 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> &bull; </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: '保護テンプレートを&lt;noinclude&gt;で囲む', tooltip: '参照読み込みされないように保護テンプレートを&lt;noinclude&gt;で囲む', 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: '保護テンプレートを&lt;noinclude&gt;で囲む', tooltip: '参照読み込みされないように保護依頼テンプレートを&lt;noinclude&gt;で囲む', 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> &bull; </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:"保護テンプレートを&lt;noinclude&gt;で囲む",tooltip:"参照読み込みされないように保護テンプレートを&lt;noinclude&gt;で囲む",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:"保護テンプレートを&lt;noinclude&gt;で囲む",tooltip:"参照読み込みされないように保護依頼テンプレートを&lt;noinclude&gt;で囲む",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"&&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);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"&&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}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")&&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}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 &lt;noinclude&gt;', value: 'noinclude', name: 'noinclude', tooltip: 'Will wrap the deletion tag in &lt;noinclude&gt; 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 &lt;noinclude&gt; (for substituted templates only)', value: 'noinclude', name: 'noinclude', tooltip: 'Will wrap the deletion tag in &lt;noinclude&gt; 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 &lt;noinclude&gt;', value: 'noinclude', name: 'noinclude', tooltip: 'Will wrap the deletion tag in &lt;noinclude&gt; 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 &#32; (HTML space character) is needed to overcome MediaWiki's parameter auto-trim. notifytext += params.numbering !== '' ? '|order=&#32;' + params.numbering : ''; break; case 'mfd': // tell the template to add " (Xnd nomination)" to the XFD title, if needed notifytext += params.numbering !== '' ? '|order=&#32;' + 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 &lt;noinclude&gt;', value: 'noinclude', name: 'noinclude', tooltip: 'Will wrap the deletion tag in &lt;noinclude&gt; 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 &lt;noinclude&gt; (for substituted templates only)', value: 'noinclude', name: 'noinclude', tooltip: 'Will wrap the deletion tag in &lt;noinclude&gt; 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 &lt;noinclude&gt;', value: 'noinclude', name: 'noinclude', tooltip: 'Will wrap the deletion tag in &lt;noinclude&gt; 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 &#32; (HTML space character) is needed to overcome MediaWiki's parameter auto-trim. notifytext += params.numbering !== '' ? '|order=&#32;' + params.numbering : ''; break; case 'mfd': // tell the template to add " (Xnd nomination)" to the XFD title, if needed notifytext += params.numbering !== '' ? '|order=&#32;' + 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 &lt;noinclude&gt;",value:"noinclude",name:"noinclude",tooltip:"Will wrap the deletion tag in &lt;noinclude&gt; 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 &lt;noinclude&gt; (for substituted templates only)",value:"noinclude",name:"noinclude",tooltip:"Will wrap the deletion tag in &lt;noinclude&gt; 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 &lt;noinclude&gt;",value:"noinclude",name:"noinclude",tooltip:"Will wrap the deletion tag in &lt;noinclude&gt; 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)&&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"){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=&#32;"+params.numbering:"";break;case"mfd":notifytext+=params.numbering!==""?"|order=&#32;"+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&&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)+")";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: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;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"&&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()){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 + '}} &ndash; ' + 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 + '}} &ndash; '; 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, '&#123;').replace(/\}/g, '&#125;') + '"'; }; // 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 + '}} &ndash; ' + 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 + '}} &ndash; '; 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, '&#123;').replace(/\}/g, '&#125;') + '"'; }; // 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+"}} &ndash; "+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+"}} &ndash; ";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,"&#123;").replace(/\}/g,"&#125;")+'"'};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")&&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}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&&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> 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>&#32; |}</div></includeonly><noinclude> {{documentation}} <!-- Add categories to the /doc subpage, interwikis to Wikidata, not here --> </noinclude>" 769506 wikitext text/x-wiki <includeonly>&#32; |}</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