Wikipedia testwiki https://test.wikipedia.org/wiki/Main_Page MediaWiki 1.47.0-wmf.7 first-letter Media Special Talk User User talk Wikipedia Wikipedia talk File File talk MediaWiki MediaWiki talk Template Template talk Help Help talk Category Category talk Thread Thread talk Summary Summary talk Test namespace 1 Test namespace 1 talk Test namespace 2 Test namespace 2 talk Draft Draft talk Campaign Campaign talk TimedText TimedText talk Module Module talk SecurePoll SecurePoll talk CNBanner CNBanner talk Translations Translations talk Event Event talk Topic Newsletter Newsletter talk MediaWiki:Gadgets-definition 8 12894 747136 744051 2026-06-16T19:17:58Z SD0001 26892 update twinkle/morebits deps 747136 wikitext text/x-wiki <noinclude>{{collapse|1={{#invoke:validate gadgets|validate}}|title=Issues in gadget definitions}}</noinclude> == Twinkle == * Twinkle [ResourceLoader |dependencies=ext.gadget.morebits, ext.gadget.select2, mediawiki.language |rights=autoconfirmed |type=general |peers=Twinkle-pagestyles] |Twinkle.js |Twinkle.css |twinklearv.js |twinklewarn.js |twinkleblock.js |twinklewelcome.js |twinkletalkback.js |twinklespeedy.js |twinkleprod.js |twinklexfd.js |twinkleimage.js |twinkleprotect.js |twinkletag.js |twinklediff.js |twinkleunlink.js |twinklerollback.js |twinkledeprod.js |twinklebatchdelete.js |twinklebatchprotect.js |twinklebatchundelete.js |twinkleconfig.js * morebits [ResourceLoader |dependencies=mediawiki.user, mediawiki.util, mediawiki.Title, mediawiki.api |hidden] |morebits.js |morebits.css * Twinkle-pagestyles [hidden |skins=vector, vector-2022] |Twinkle-pagestyles.css * select2 [ResourceLoader |hidden] |select2.min.js |select2.min.css *morebitsV3[ResourceLoader|dependencies=mediawiki.user,mediawiki.util,mediawiki.Title,jquery.ui|hidden]|morebitsV3.js|morebitsV3.css *twinkle-enwiki[ResourceLoader|dependencies=ext.gadget.morebitsV3,ext.gadget.select2,mediawiki.api,mediawiki.storage,mediawiki.libs.pluralruleparser|peers=Twinkle-pagestyles]|TwinkleV3.js|TwinkleV3.css *twinkle-starter[ResourceLoader|dependencies=ext.gadget.morebitsV3,ext.gadget.select2,mediawiki.api,mediawiki.storage|peers=Twinkle-pagestyles]|Twinkle-starter.js|Twinkle-starter.css *orange-i18n [ ResourceLoader | dependencies = mediawiki.libs.pluralruleparser ] | orange-i18n.js *banana-i18n [ ResourceLoader ] | banana-i18n.js == Recent-Tests == * InterProjectLinks[ResourceLoader|default|type=general|skins=vector,vector-2022,monobook,timeless,modern,cologneblue]|InterProjectLinks.js|InterProjectLinks.css * nomin-test[ResourceLoader|requiresES6] | nomin-test.js * regex-test[ResourceLoader]|regex-test.js * es6-test[ResourceLoader|requiresES6]|es6-test.js * throwError[ResourceLoader|dependencies=mediawiki.util]|throwError.js * tabbedwindow[ResourceLoader|default|dependencies=mediawiki.Title]|tabbedwindow.js * markblocked[ResourceLoader|dependencies=mediawiki.util,mediawiki.page.ready]|markblocked.js * djtest[ResourceLoader|skins=minerva]|djtest.js * packagefilebase[ResourceLoader|requiresES6|package|hidden]|packagefilebase-file1.js|packagefilebase-file2.js|packagefilebase-file3.json * packagefileuser[ResourceLoader|requiresES6|package|dependencies=ext.gadget.packagefilebase]|packagefileuser.js * MobileCategories[ResourceLoader|dependencies=mediawiki.api,mediawiki.util|skins=minerva]|MobileCategories.js * Sharer[ResourceLoader]|Sharer.js * citations[ResourceLoader|dependencies=mediawiki.util]|citations.js * watchlist-notice[ResourceLoader|targets=desktop,mobile|default]|watchlist-notice.js * watchlist-notice-core[ResourceLoader|targets=desktop,mobile|dependencies=mediawiki.storage|hidden]|watchlist-notice-core.js * geonotice[ResourceLoader|default]|geonotice.js * geonotice-core[ResourceLoader|package|dependencies=mediawiki.util,mediawiki.storage|hidden]|geonotice-core.js|geonotice-list.json|geonotice-core.css * legacyToolbar [ResourceLoader] |legacyToolbar.js |extra-toolbar-buttons-core.js |refToolbarLegacy.js * refToolbar[ResourceLoader|default|dependencies=mediawiki.util|actions=edit|contentModels=wikitext]|refToolbar.js * refToolbarBase[ResourceLoader|hidden]|refToolbarBase.js * contribsrange[ResourceLoader|dependencies=mediawiki.util,jquery.spinner]|contribsrange.js * another-contribsrange[ResourceLoader|dependencies=mediawiki.util,jquery.spinner]|another-contribsrange.js * charinsert[ResourceLoader]|charinsert.js * charinsert-core[ResourceLoader|hidden|dependencies=jquery.textSelection,user,mediawiki.storage]|charinsert-styles.css|charinsert-core.js * charinsert-old[ResourceLoader]|charinsert-old.js|charinsert-old.css * diffTools[ResourceLoader|dependencies=mediawiki.util]|diffTools.js|diffTools.css * ImagelessTabs[ResourceLoader]|ImagelessTabs.css * fastbuttons[ResourceLoader|dependencies=mediawiki.util]|fastbuttons.js|fastbuttons.css * ExternalTranslate[ResourceLoader|dependencies=jquery,mediawiki.api]|ExternalTranslate.js|ExternalTranslate.html|ExternalTranslate.css * destacar[ResourceLoader]|highlightsUsername.js|highlightsUsername.css * blockNotificationsButton[ResourceLoader|dependencies=mediawiki.api,ext.gadget.mediawiki.api.ptwiki,mediawiki.util,user.options|rights=block]|blockNotificationsButton.js|blockNotificationsButton.css * NewVillagePump[ResourceLoader]|NewVillagePump.js|NewVillagePump.css * mediawiki.api.ptwiki[ResourceLoader|dependencies=mediawiki.api|hidden]|mediawiki.api.ptwiki.js * NewVillagePumpCore[ResourceLoader|dependencies=mediawiki.util,jquery.placeholder,jquery.spinner,ext.gadget.mediawiki.api.ptwiki|hidden]|NewVillagePump.js/Core.js * Navigation_popups [ResourceLoader |dependencies=mediawiki.api, mediawiki.user, mediawiki.util, user.options, mediawiki.jqueryMsg |type=general] |popups.js |navpop.css * teahouse[ResourceLoader|dependencies=mediawiki.user,mediawiki.cookie]|teahouse/main.js|teahouse/config.js|teahouse/teahouse.css * teahouse-opt-in[ResourceLoader]|teahouse-opt-in/main.js * PB[ResourceLoader|dependencies=mediawiki.api,mediawiki.util]|PB.js * Wikimedia Learning Survey[ResourceLoader]|WikimediaL.js * formWizard [ResourceLoader |default |rights=minoredit] |formWizard.js * formWizard-core [ResourceLoader |hidden |dependencies=mediawiki.api, mediawiki.cookie, mediawiki.ui, mediawiki.user, jquery.chosen, jquery.ui, user.options] |formWizard-core.js |formWizard.css * requestForAdminship[ResourceLoader]|requestForAdminship.js * watchUserContribs[ResourceLoader|dependencies=jquery.ui,mediawiki.util,mediawiki.ui.button,ext.gadget.TipsyDeprecated]|watchUserContribs.js|watchUserContribs.css * WatchlistGreenIndicators[ResourceLoader|skins=vector]|WatchlistGreenIndicators.css * WatchlistChangesBold[ResourceLoader]|WatchlistChangesBold.css * Hotcat[ResourceLoader|rights=edit]|HotCat.js * RevisionSlider[ResourceLoader|dependencies=jquery.ui,mediawiki.api,mediawiki.util]|revisionslider.js|revisionslider.css * toolbartocnav[ResourceLoader|dependencies=ext.wikiEditor]|toolbartocnav.css|toolbartocnav.js * purgetab[ResourceLoader|dependencies=mediawiki.util,mediawiki.api]|purgetab.js * databoxEditor[ResourceLoader|dependencies=jquery.client,jquery.mwEmbedUtil,mw.MwEmbedSupport,jquery.ui]|databoxeditor.js * wikEd[ResourceLoader]|wikEd.js * revisionsSysopsActions[ResourceLoader|dependencies=mediawiki.util,ext.gadget.mediawiki.api.ptwiki,jquery.placeholder]|revisionsSysopsActions.js * validateBlockRollbackers[ResourceLoader]|validateBlockRollbackers.js * watchlist[ResourceLoader|top]|watchlist.js * addsection-plus[ResourceLoader|skins=monobook,vector]|addsection-plus.css * CleanDeleteReasons[ResourceLoader|default|rights=delete]|CleanDeleteReasons.js * contentFeatured[ResourceLoader|dependencies=mediawiki.util,ext.gadget.mediawiki.api.ptwiki,jquery.ui]|contentFeatured.js|contentFeatured.css * easyPeerReview[ResourceLoader|rights=review|dependencies=mediawiki.util]|easyPeerReview.js * UTCLiveClock[ResourceLoader|type=general|dependencies=mediawiki.util,mediawiki.api|peers=UTCLiveClock-pagestyles]|UTCLiveClock.js|UTCLiveClock.css * UTCLiveClock-pagestyles[hidden|skins=vector,monobook]|UTCLiveClock-pagestyles.css * afchelper [ResourceLoader |dependencies=mediawiki.api, mediawiki.util, mediawiki.user, jquery.chosen, jquery.spinner, jquery.ui |namespaces=2, 118] |afchelper.js |afchelper.css * Shortdesc-helper-loader[ResourceLoader|dependencies=mediawiki.api,user.options,mediawiki.util|skins=vector,monobook,modern,timeless|peers=Shortdesc-helper-pagestyles-vector]|Shortdesc-helper.js * Shortdesc-helper-pagestyles-vector[hidden|skins=vector]|Shortdesc-helper-pagestyles-vector.css * MoreMenu-local[ResourceLoader|dependencies=mediawiki.api,mediawiki.util,mediawiki.Title,user.options|peers=MoreMenu-local-pagestyles]|MoreMenu.js * MoreMenu-local-pagestyles[hidden]|MoreMenu-pagestyles.css * scribe[ResourceLoader|dependencies=ext.visualEditor.core,mediawiki.api]| scribe.js|scribe.css * scribe-v2[ResourceLoader|dependencies=ext.visualEditor.core,mediawiki.api]| scribe-v2.js|scribe-v2.css * XFDcloser[ResourceLoader|dependencies=mediawiki.user|type=general]|XFDcloser.js * XFDcloser-core[ResourceLoader|dependencies=mediawiki.util,mediawiki.api,mediawiki.Title,oojs-ui-core,oojs-ui-widgets,oojs-ui-windows,oojs-ui.styles.icons-interactions,oojs-ui.styles.icons-content,oojs-ui.styles.icons-moderation,oojs-ui.styles.icons-alerts|hidden|type=general]|XFDcloser-core.js|XFDcloser-core.css * XFDcloser-core-beta[ResourceLoader|dependencies=mediawiki.util,mediawiki.api,mediawiki.Title,oojs-ui-core,oojs-ui-widgets,oojs-ui-windows,oojs-ui.styles.icons-interactions,oojs-ui.styles.icons-content,oojs-ui.styles.icons-moderation,oojs-ui.styles.icons-alerts|hidden|type=general]|XFDcloser-core-beta.js|XFDcloser-core-beta.css * libExtraUtil[ResourceLoader|hidden]|libExtraUtil.js * GoogleImagesTineye [ResourceLoader|dependencies=mediawiki.util]|GoogleImagesTineye.js * ShowJavascriptErrors[ResourceLoader|type=general]|ShowJavascriptErrors.js * lastEditUser[ResourceLoader|dependencies=mediawiki.util]|lastEditUser.js * dark-mode-toggle[ResourceLoader|targets=desktop,mobile|dependencies=mediawiki.util,mediawiki.api,mediawiki.Uri,mediawiki.storage|peers=dark-mode-toggle-pagestyles]|dark-mode-toggle.js * dark-mode-toggle-pagestyles[hidden|targets=desktop,mobile|skins=vector,vector-2022,minerva,monobook]|dark-mode-toggle-pagestyles.css * dark-mode[ResourceLoader|supportsUrlLoad|targets=desktop,mobile|skins=vector,vector-2022,monobook,modern,minerva,timeless]|dark-mode.css * addMe[ResourceLoader|default|dependencies=oojs-ui,mediawiki.api,mediawiki.util,mediawiki.jqueryMsg]|addMe.js * AutosuggestSitelink-local[ResourceLoader|dependencies=mediawiki.action.view.postEdit,mediawiki.ForeignApi,mediawiki.Title,oojs-ui-core,oojs-ui-windows]|AutosuggestSitelink.js * WpLibExtraDev[ResourceLoader|hidden]|WpLibExtraDev.js * MarkBLocked-core[ResourceLoader|hidden|dependencies=mediawiki.user,mediawiki.api,mediawiki.ForeignApi,mediawiki.storage,mediawiki.util,jquery.ui,oojs-ui,oojs-ui.styles.icons-moderation]|MarkBLocked-core.js|MarkBLocked-core.css * MarkBLocked[ResourceLoader|supportsUrlLoad|package|dependencies=mediawiki.user,mediawiki.api,mediawiki.ForeignApi,mediawiki.storage,mediawiki.util,jquery.ui,oojs-ui,oojs-ui.styles.icons-moderation]|MarkBLocked.js|MarkBLocked-core.js|MarkBLocked-core.css * MassRevisionDelete[ResourceLoader|rights=deleterevision|dependencies=jquery.makeCollapsible,oojs-ui,oojs-ui.styles.icons-movement,mediawiki.api]|MassRevisionDelete.js * edit-styles[ResourceLoader| actions=edit|type=styles]|edit-styles.css * firstedit-test[ResourceLoader]|firstedit-test.js * firstedit-short-test[ResourceLoader]|firstedit-short-test.js * section-share[ResourceLoader|default]|section-share.js ==Wikivoyage-Experimental== * Carousel[ResourceLoader|type=general|dependencies=mediawiki.util|targets=desktop,mobile]|Carousel.js|Carousel.css == modules == * fastButtonsCore[ResourceLoader|dependencies=mediawiki.api,ext.gadget.mediawiki.api.ptwiki,mediawiki.page.ready,jquery.ui,mediawiki.cookie]|fastbuttons.js/core.js * diffToolsCore[ResourceLoader|dependencies=mediawiki.api,mediawiki.util,user.options,ext.gadget.mediawiki.api.ptwiki,jquery.ui]|diffTools.js/core.js * validateBlockRollbackersCore[ResourceLoader|dependencies=mediawiki.util]|validateBlockRollbackers.js/core.js * requestForAdminshipCore[ResourceLoader|dependencies=ext.gadget.mediawiki.api.ptwiki,jquery.ui]|requestForAdminship.js/core.js|requestForAdminship.css * TipsyDeprecated[ResourceLoader|hidden]|tipsyDeprecated.js == arwiki == * changlist[ResourceLoader]|changlist.css == Old-tests == * backgroundTest[ResourceLoader|top|type=styles]|backgroundTest.css == ruwiki == * ruwiki-test[ResourceLoader]|ruwiki-test.js * ruwiki-core[ResourceLoader]|ruwiki-core.js * ruwiki-common-action-edit[ResourceLoader|dependencies=user.options]|ruwiki-common-action-edit.js * ruwiki-common-namespace-file[ResourceLoader|hidden]|ruwiki-common-namespace-file.js * ruwiki-common-action-history[ResourceLoader|hidden]|ruwiki-common-action-history.js * ruwiki-common-special-delete[ResourceLoader]|ruwiki-common-special-delete.js‎ * ruwiki-common-special-block[ResourceLoader|type=general]|ruwiki-common-special-block.js‎|ruwiki-common-special-block.css * ruwiki-common-special-log[ResourceLoader]|ruwiki-common-special-log.js‎ * ruwiki-common-special-newpages[ResourceLoader]|ruwiki-common-special-newpages.js‎ * ruwiki-common-special-movepage[ResourceLoader]|ruwiki-common-special-movepage.js‎ * ruwiki-common-special-watchlist[ResourceLoader]|ruwiki-common-special-watchlist.js‎ * ruwiki-common-special-search[ResourceLoader]|ruwiki-common-special-search.js‎ * ruwiki-common-special-upload[ResourceLoader]|ruwiki-common-special-upload.js‎ * ruwiki-wikificator[ResourceLoader|dependencies=user.options,jquery.textSelection,ext.gadget.ruwiki-registerTool]|ruwiki-wikificator.js * ruwiki-summaryButtons[ResourceLoader|type=general|hidden]|ruwiki-summaryButtons.js|ruwiki-summaryButtons.css * ruwiki-registerTool[ResourceLoader|hidden|dependencies=user.options]|ruwiki-registerTool.js * ruwiki-urldecoder[ResourceLoader|dependencies=ext.gadget.ruwiki-registerTool]|ruwiki-urldecoder.js === ruwiki-iw === * ruwiki-iwcore[ResourceLoader|hidden|type=general]|ruwiki-iwcore.js|ruwiki-iwcore.css * ruwiki-iwlocalnames[ResourceLoader|dependencies=ext.gadget.ruwiki-iwcore]|ruwiki-iwlocalnames.js * ruwiki-iwhints[ResourceLoader|dependencies=ext.gadget.ruwiki-iwcore]|ruwiki-iwhints.js * ruwiki-iwen[ResourceLoader|dependencies=ext.gadget.ruwiki-iwcore]|ruwiki-iwen.js * ruwiki-iwrussia[ResourceLoader|dependencies=ext.gadget.ruwiki-iwcore]|ruwiki-iwrussia.js == nowiki == * template-wizzard[ResourceLoader|rights=edit|type=general]|template-wizzard.js == azwiki == * Katalitik[ResourceLoader]|Katalitik.js * XTools-ArticleInfo[ResourceLoader]|XTools-ArticleInfo.js * addsection-plus[ResourceLoader]|addsection-plus.js * markAdmins[ResourceLoader|dependencies=mediawiki.util]|markAdmins.js * directLinkToCommons[ResourceLoader|dependencies=mediawiki.util]|directLinkToCommons.js * switcher[ResourceLoader|targets=desktop,mobile]|switcher.js * patrolRevisions[ResourceLoader|dependencies=mediawiki.api,mediawiki.util|rights=patrol]|patrolRevisions.js == fawiki == * ScriptOne[ResourceLoader]|ScriptOne.js * ScriptTwo[ResourceLoader]|ScriptTwo.js|ScriptTwo.css * ScriptHidden[hidden]|ScriptHidden.css * ScriptThree[ResourceLoader|dependencies=mediawiki.api|peers=ScriptHidden]|ScriptThree.js * Morebits-i18n[ResourceLoader|dependencies=ext.gadget.morebitsV3,jquery.i18n]|Morebits-i18n.js == bnwiki == *markAdmins[ResourceLoader|dependencies=mediawiki.util]|markAdmins.js * DiscussionCloser[ResourceLoader|dependencies=mediawiki.util]|DiscussionCloser.js|DiscussionCloser.css * Twinkle-bn[ResourceLoader|dependencies=ext.gadget.morebits-bn,ext.gadget.select2-bn|rights=autoconfirmed|type=general|peers=Twinkle-pagestyles-bn]|Twinkle-bn.js|twinkleprod-bn.js|twinkleimage-bn.js|twinklebatchundelete-bn.js|twinklewarn-bn.js|twinklespeedy-bn.js|friendlyshared-bn.js|twinklediff-bn.js|twinkleunlink-bn.js|friendlytag-bn.js|twinkledeprod-bn.js|friendlywelcome-bn.js|twinklexfd-bn.js|twinklebatchdelete-bn.js|twinklebatchprotect-bn.js|twinkleconfig-bn.js|twinklefluff-bn.js|twinkleprotect-bn.js|twinklearv-bn.js|twinkleblock-bn.js|friendlytalkback-bn.js|Twinkle-bn.css * morebits-bn[ResourceLoader|dependencies=mediawiki.user,mediawiki.util,jquery.ui|hidden]|morebits-bn.js|morebits-bn.css * Twinkle-pagestyles-bn[hidden|skins=vector]|Twinkle-pagestyles-bn.css * select2-bn[ResourceLoader|hidden]|select2.min-bn.js|select2.min-bn.css ==bnwiktionary== * Rep2Ad[ResourceLoader]|Rep2Ad.js ==ukwiki== * Wikificator[ResourceLoader|hidden]|Wikificator.js * Onlyifediting[ResourceLoader|action=edit|dependencies=ext.visualEditor.desktopArticleTarget.init,ext.gadget.Wikificator]|Onlyifediting.js * PostBlockActions[ResourceLoader|namespaces=-1]|PostBlockActions.js * ImgsCleanUp[ResourceLoader|package|dependencies=mediawiki.util,vue,@wikimedia/codex|rights=autoconfirmed|namespaces=6]|ImgsCleanUp.js|ImgsCleanUp-main.vue * valcio[ResourceLoader|dependencies=vue,codex]|valcio.js * codexTableBuilder[ResourceLoader|package|dependencies=vue,@wikimedia/codex|actions=edit,submit|rights=edit]|codexTableBuilder.js|codexTableBuilder.vue * codexTabs[ResourceLoader|package|default|dependencies=vue,@wikimedia/codex|type=general]|codexTabs.js|codexTabs.vue|codexTabs.css nskplxo2sofq9j2epi49j1j0902dd8e Template:Cite news 10 45531 747095 593016 2026-06-16T18:13:36Z ~2026-35344-03 74479 Replaced content with "<includeonly>{{#invoke:citation/CS1|citation |CitationClass=news }}</includeonly><noinclude> {{documentation}} </noinclude>" 747095 wikitext text/x-wiki <includeonly>{{#invoke:citation/CS1|citation |CitationClass=news }}</includeonly><noinclude> {{documentation}} </noinclude> pbjnspfej5gxihr9izxrzkow3f1uf3i Page478 0 49549 747161 578439 2026-06-17T11:35:59Z ~2026-35597-65 74491 747161 wikitext text/x-wiki '''Page478''' This in test page No. 478 {{audio|This file does not exist.ogg}} <phonos file="This file does not exist.ogg"/> syqyfzjwfmyb test [[Category:Category with multiple files]] 0rw2djxryi6k0xem3i6mb8vylrgntr4 Page614 0 49684 747014 373018 2026-06-16T14:18:14Z KStoller-WMF 54035 /* */ testing 747014 wikitext text/x-wiki '''Page614''' This in test page No. 614 test [[Category:Category with multiple files]] emk8eijpftgzy50cbdahsvsdgut52gi MediaWiki:Gadget-morebits.js 8 65780 747129 728686 2026-06-16T19:15:08Z SD0001 26892 Repo at 46e08aa: 747129 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) { // 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 = Morebits.Date.localeData.months.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); }; const h24 = udate.getHours(), m = udate.getMinutes(), s = udate.getSeconds(), ms = udate.getMilliseconds(); const D = udate.getDate(), M = udate.getMonth() + 1, Y = udate.getFullYear(); const h12 = h24 % 12 || 12, amOrPm = h24 >= 12 ? 'PM' : 'AM'; const 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.getUTCMonthName() + '|' + this.getUTCMonthNameAbbrev() + ')\\s+' + this.getUTCFullYear() + '\\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; const header = '='.repeat(level); const text = this.getUTCMonthName() + ' ' + this.getUTCFullYear(); 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: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], monthsShort: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], days: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], daysShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], relativeTimes: { thisDay: '[Today at] h:mm A', prevDay: '[Yesterday at] h:mm A', nextDay: '[Tomorrow at] h:mm A', thisWeek: 'dddd [at] h:mm A', pastWeek: '[Last] dddd [at] h:mm A', 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('done'); } 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('Invalid token. Getting a new token and retrying...'); 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() { const tokenApi = new Morebits.wiki.Api('Getting token', { 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 = 'Opening page "' + 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, revertCurID: null, revertUser: null, watched: false, fullyProtected: false, suppressProtectWarning: false, conflictRetries: 0, retries: 0, // callbacks onLoadSuccess: null, onLoadFailure: 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, 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('Retrieving page...', ctx.loadQuery, fnLoadSuccess, ctx.statusElement, ctx.onLoadFailure); ctx.loadApi.setParent(this); ctx.loadApi.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' ? 'You are about to make an edit to the fully protected page "' + ctx.pageName + '" (protected indefinitely). \n\nClick OK to proceed with the edit, or Cancel to skip this edit.' : 'You are about to make an edit to the fully protected page "' + ctx.pageName + '" (protection expiring ' + new Morebits.Date(ctx.fullyProtected).calendar('utc') + ' (UTC)). \n\nClick OK to proceed with the edit, or Cancel to skip this edit.' ) ) { ctx.statusElement.error('Edit to fully protected page was aborted.'); 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('Saving page...', 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; }; /** @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('Retrieving page creation information', 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('retrieving token...', 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('retrieving token...', 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('retrieving token...', 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('retrieving token...', 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('retrieving token...', 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('retrieving token...', 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('retrieving token...', 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('Failed to retrieve edit token.'); ctx.onLoadFailure(this); return; } ctx.loadTime = ctx.loadApi.getResponse().curtimestamp; if (!ctx.loadTime) { ctx.statusElement.error('Failed to retrieve current timestamp.'); 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 }; // 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('The page title is invalid: ' + 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 + ' is a cross-namespace redirect to ' + resolvedName + ', aborted'); onFailure(this); return false; } // only notify user for redirects, not normalization new Morebits.Status('Note', 'Redirected from ' + ctx.pageName + ' to ' + 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(['completed (', 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('Could not save the page because the wiki server wanted you to fill out a CAPTCHA.'); } else { ctx.statusElement.error('Unknown error received from API while saving page'); } // 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 }; const purgeApi = new Morebits.wiki.Api('Edit conflict detected, purging server cache', purgeQuery, (() => { --Morebits.wiki.numberOfActionsLeft; // allow for normal completion if retry succeeds ctx.statusElement.info('Edit conflict detected, reapplying edit'); 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('Save failed, retrying in 2 seconds ...'); --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('Retrieving page creation information', 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('moving page...', 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('You have cascading protection enabled on "' + ctx.pageName + '" but have not selected uniform sysop-level protection.\n\n' + 'Click OK to adjust and proceed with sysop-level cascading protection, or Cancel to skip this action.')) { ctx.statusElement.error('Cascading protection was aborted.'); 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; }, /** * 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('no log page name specified'); } /** * 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(); } const page = new Morebits.wiki.Page('User:' + mw.config.get('wgUserName') + '/' + logPageName, 'Adding entry to userspace log'); // 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 || 'Performing batch operation'), 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('no pages specified'); 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 const pageName = arg.getPageName ? arg.getPageName() : arg.pageName || arg.query.title; statelem.info('completed ([[' + pageName + ']])'); } else { // we don't know the page title - just display a generic message statelem.info('done'); } } else { // remove the status line automatically produced by Morebits.wiki.* statelem.unlink(); } } else if (typeof arg === 'string' && ctx.options.preserveIndividualStatusLines) { new Morebits.Status(arg, 'completed ([[' + 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) { const statusString = 'Done (' + ctx.countFinishedSuccess + '/' + ctx.countFinished + ' actions completed successfully)'; 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('Done (overshot by ' + (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 || 'Submit'; 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> b9v3vpgcte5dzvwqiyzs9nk24upyt4t MediaWiki:Gadget-morebits.css 8 65781 747130 728687 2026-06-16T19:15:09Z SD0001 26892 Repo at 46e08aa: 747130 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 Sandbox 0 68971 747150 746732 2026-06-17T05:48:16Z Lenis Felipe 72343 747150 wikitext text/x-wiki {{Please leave this line alone (sandbox heading)}}<!-- * Welcome to the sandbox! * * Please leave this part alone * * Feel free to try your editing skills below * ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■--> Test 123<br>archive is hOnU7 037m8973qtfjaf0uce308bii7d28uif MediaWiki:Gadget-twinklearv.js 8 70676 747131 728688 2026-06-16T19:15:10Z SD0001 26892 Repo at 46e08aa: 747131 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> 7pxduz8njgsshfht4vqqlob75gpzfu7 MediaWiki:Gadget-twinklexfd.js 8 70685 747134 725400 2026-06-16T19:15:17Z SD0001 26892 Repo at 46e08aa: remove `type: 'option'` when parent is `type: 'select'` (#2344); xfd, tag: add "desired outcome" dropdown (with delete, merge, redirect, draftify), and remove {{Merge}} tags from tag module (#2345); afd: better edit summaries, follow redirects when placing {{Merge from}} (#2354); rmtr: handle WP:ANCHORSUBST (#2361) 747134 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: $('.mw-parser-output > p .Inline-Template').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.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.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> 31xkc0nhzq3j83m3e234eudnkufh9bo MediaWiki:Gadget-twinklewarn.js 8 70690 747133 720906 2026-06-16T19:15:15Z SD0001 26892 Repo at 46e08aa: add {{Uw-aitalk1}} through {{Uw-aitalk4}} (#2346); soften {{Uw-image1}} edit summary (#2368) 747133 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> did0x17mfzurkdomwwuo939fw8lnz5z Template:Singles 10 77816 747077 211061 2026-06-16T18:00:19Z ~2026-35392-04 74478 747077 wikitext text/x-wiki {{<includeonly>safesubst:</includeonly>#invoke:Singles||$B={{Infobox | child = <includeonly>yes</includeonly> | headerstyle = background: {{Template:Infobox album/color|{{{type|}}}}};color:inherit; | header1 = [[Single (music)|Singles]] from '' {{#if:{{{name|}}}|{{{name}}}|{{PAGENAMEBASE}}}} '' | data2 = {{#invoke:Singles|main}} }}{{#if:{{#invoke:String|match|1={{{type|}}}{{{name|}}}x|2=</?t[drh][ >]|nomatch=}}|[[Category:Music infoboxes with malformed table placement|R]]}}{{<includeonly>safesubst:</includeonly>#invoke:Check for unknown parameters|check|unknown={{<includeonly>safesubst:</includeonly>main other|[[Category:Pages using Singles template with unknown parameters|_VALUE_{{PAGENAME}}]]}}|preview=Page using [[Template:Singles]] with unknown parameter "_VALUE_"|ignoreblank=y| name | type | regexp1 = single[%d][%d]* | regexp2 = single[%d][%d]*date }}}}<noinclude> {{Documentation}} </noinclude> ipriqz41fue7yqre3rbrq8x20n6fogf Module:Singles 828 77818 747078 211047 2026-06-16T18:01:46Z ~2026-35392-04 74478 747078 Scribunto text/plain local p = {} -- Ripped from Module:Infobox. TODO: Make a utility module that can do this kind of thing local function getArgNums(args, prefix) -- Returns a table containing the numbers of the arguments that exist -- for the specified prefix. For example, if the prefix were to be 'data', and -- 'data1', 'data2', and 'data5' were to exist, it would return {1, 2, 5}. local nums = {} for k, v in pairs(args) do local num = tostring(k):match('^' .. prefix .. '([1-9]%d*)$') if num then table.insert(nums, tonumber(num)) end end table.sort(nums) return nums end -- Forked from Module:Unsubst-infobox local specialParams = { ['$B'] = 'template content' } p[''] = function ( frame ) if not frame.args['$B'] then error( '{{#invoke:Singles|}} requires parameter $B (template content)' ) end if mw.isSubsting() then ---- substing -- Passed args local args = {} for k, v in pairs( frame:getParent().args ) do args[k] = v end -- Build an equivalent template invocation -- First, find the title to use local titleobj = mw.title.new(frame:getParent():getTitle()) local title if titleobj.namespace == 10 then -- NS_TEMPLATE title = titleobj.text elseif titleobj.namespace == 0 then -- NS_MAIN title = ':' .. titleobj.text else title = titleobj.prefixedText end -- Remove empty fields for k, v in pairs( args ) do if v == '' then args[k] = nil end end -- Pull aliases local nums = getArgNums(args, '[Ss]ingle ?') for _, num in ipairs(nums) do args['single' .. num] = args['single' .. num] or args['single ' .. num] or args['Single ' .. num] args['single' .. num .. 'date'] = args['single' .. num .. 'date'] or args['single ' .. num .. ' date'] or args['Single ' .. num .. ' date'] or '' args['single ' .. num], args['Single ' .. num], args['single ' .. num .. ' date'], args['Single ' .. num .. ' date'] = nil, nil, nil, nil end for k, v in pairs( {Type = 'type', Name = 'name'} ) do if args[k] and not args[v] then args[v], args[k] = args[k], nil end end -- Build the invocation body local ret = '{{' .. title -- Make parameter list local params = {'name', 'type'} for _, num in ipairs( nums ) do table.insert( params, 'single' .. num ); table.insert( params, 'single' .. num .. 'date' ) end -- Align parameters correctly and remove extra ones local maxlength = 0 for k, v in ipairs( params ) do local tmp = mw.ustring.len( v ) if tmp > maxlength then maxlength = tmp end end for k, v in ipairs( params ) do ret = ret .. '\n | ' .. v .. string.rep(' ', (maxlength - mw.ustring.len( v ))) .. ' = ' .. (args[v] or '') end ret = ret .. '\n}}' ret = mw.ustring.gsub(ret, '%s+\n', '\n') return ret else -- Not substing -- Just return the "body" return frame.args['$B'] end end function p.main(frame) local args = require('Module:Arguments').getArgs(frame, {wrappers = 'Template:Singles'}) local out = '' local nums = getArgNums(args, '[Ss]ingle ?') for _, num in ipairs(nums) do out = out .. '\n# <span class="item"><span class="fn">"' .. (args['single' .. num] or args['single ' .. num] or args['Single ' .. num]) .. '"</span>' local date = args['single' .. num .. 'date'] or args['single ' .. num .. ' date'] or args['Single ' .. num .. ' date'] if date then out = out .. '<br />Released: ' .. date end out = out .. '</span>' end if out ~= '' then if mw.ustring.match(out, '</?t[drh][ >]') then out = out .. ' [[Category:Music infoboxes with malformed table placement|R]]' end return '<div style="text-align:left">' .. out .. '\n</div>' end return out end return p g28bepqkwxhmugy4jnp61wm9qzxxid4 Wikipédia:Escolha do artigo em destaque/Teste/13 0 92063 747010 291294 2026-06-16T13:47:41Z User97104 72884 Replaced content with "Wrong Wiki. Wrong Language." 747010 wikitext text/x-wiki Wrong Wiki. Wrong Language. orto5uv0jya39fb7s5afm8wbjse5ptz Lipotidae 0 95622 747070 314753 2026-06-16T16:52:00Z ~2026-35267-08 74474 /* */ 747070 wikitext text/x-wiki == {{-mul-}} == === Nom === {{lema|mul|nom}} # [[lipòtids]] === Referències === * {{Viquiespècies}} * {{termcat|dicc=159}} {{categoritza|mul|Espècies taxonòmiques}} Test. ooiif5qy0bxtjp1zxfdaabmo2kvbouy Monodontidae 0 95643 747016 314774 2026-06-16T14:19:43Z ~2026-35416-12 74460 /* */ test 747016 wikitext text/x-wiki test == {{-mul-}} == === Nom === {{lema|mul|nom}} # [[monodòntids]] === Referències === * {{Viquiespècies}} * {{termcat|dicc=159}} {{categoritza|mul|Espècies taxonòmiques}} j3xnt3f0ketybanvq1a6gm7svy7dclz 3-Amino-2-oxopropyl dihydrogen phosphate 0 96780 747060 620778 2026-06-16T15:20:05Z ~2026-35133-00 74465 747060 wikitext text/x-wiki La '''3-Amino-2-oxopropyl dihydrogen phosphate''' est une molécule de formule brute C3H8NO5P et de masse molaire 169.073 grammes par mole test1 sosdvtpebjcolvviq0620n0jw4q8g81 Wikipédia:Fusão/Central de fusões/Arquivo de fundidas/2018 0 100103 747162 354106 2026-06-17T11:37:03Z ~2026-35579-48 74492 747162 wikitext text/x-wiki * [[Wikipédia:Fusão/Central de fusões/Sistema porta hepático; Veia porta hepática]] test kv5t6ahyr0b972kch519w6upcdtfnnf Test page 001 0 101604 747015 365421 2026-06-16T14:19:37Z ~2026-35440-15 74459 Test 747015 wikitext text/x-wiki {{#function:Z18428|1}}<script> alert("*"); </alert> emv3k5nvnp84sct6bgva2885eetvy3p AVT 0 107346 747067 744701 2026-06-16T16:34:50Z ~2026-35382-44 74471 /* */ 747067 wikitext text/x-wiki {{Underlinked|date=tháng 11 năm 2016}} {{Thông tin nhạc sĩ | tên = Ban Kích động Nhạc AVT | hình = | kích thước hình = | chú thích hình = | phong cảnh = | nền = group_or_band | nghệ danh = | nguyên quán = Sài Gòn | thể loại = | năm hoạt động = 1958–2008 | hãng đĩa = | hợp tác với = | ca khúc = <!-- Ca khúc tiêu biểu --> | url = | thành viên hiện tại = Lữ Liên, Thúy Liệu (1988), Hoàng Long (1992) | thành viên cũ = Anh Linh, Vân Sơn và Tuấn Đăng, Hoàng Hải (1962), Lữ Liên (1966) | module = | module2 = | module3 = }} '''Ban tam ca trào phúng''' '''AVT''' có tên đầy đủ là Ban Kích động Nhạc AVT nổi tiếng với những bản nhạc với những lời ca dí dỏm, châm biếm. Test. ==Ban nhạc == Ban AVT được thành lập năm 1958, gồm các ca nhạc sĩ Anh Linh, Vân Sơn và Tuấn Đăng. Họ là nhân viên dân chính và [[Hạ sĩ|Hạ sĩ]] Quan thuộc [[Đại đội|Đại đội]] {{Done}} Văn Nghệ (Ban Ca) [[Tiểu đoàn|Tiểu đoàn]]{{Done}} Văn Nghệ - Nha Chiến tranh Tâm lý. Vì lúc đó Thiết Quân Luật nên Ban Tam ca AVT ban đêm phải trốn ra hát thường trực tại Phòng trà Anh Vũ, đường [[Bùi Viện|Bùi Viện]]{{Done}} (Sài Gòn) (nay là [[Thành phố Hồ Chí Minh]]), từ 9 giờ tối tới 2 giờ sáng.<ref>[http://sonomavietnamese.org/html/nghethuat/amnhac/avt.html BAN KÍCH ĐỘNG NHẠC AVT] {{Webarchive|url=https://web.archive.org/web/20160321160109/http://sonomavietnamese.org/html/nghethuat/amnhac/avt.html |date=2016-03-21 |title=BAN KÍCH ĐỘNG NHẠC AVT}}, Anh Linh,sonomavietnamese, 1.2.2009</ref> Khi Anh Linh tức Trung Sĩ Trần Đình Kế theo học Khóa 3 [[Sĩ quan|Sĩ quan]]{{Done}} đặc biệt vào [[Tháng sáu|tháng 6]] năm 1962, nhạc sĩ Hoàng Hải phải đổi tên là Anh Hải để hát thế vào chữ A. Đến năm 1966 thì [[Lữ Liên|Lữ Liên]]{{Done}} vào thế cho Anh Hải. Nhạc hài của AVT (hầu hết do các nhạc sĩ Lữ Liên và Duy Nhượng sáng tác) phát triển trên nền âm nhạc dân tộc (đặc biệt là [[xẩm]], các làn điệu [[Dân ca|Dân ca]]{{Done}} [[Bắc Bộ Việt Nam|Bắc Bộ]]{{Done}} và [[Miền Trung (Việt Nam)|Trung Bộ]]{{Done}}) nên nghe rất gần gũi mà cũng rất vui nhộn. Ra sân khấu biểu diễn, AVT diện khăn đóng, áo gấm thụng ba màu khác nhau. Mỗi người sử dụng một nhạc cụ dân tộc: Đàn sến (Tuấn Đăng), Đàn cò (Đàn nhị) (Lữ Liên) và Trống (Vân Sơn). Khi có những câu thoại thì mỗi người nói rặt giọng của mỗi miền Bắc, Trung, Nam. Sau ngày Việt Nam thống nhất (30/04/1975), Lữ Liên sang Hoa Kỳ lập một ban AVT mới cũng gồm 3 người, trong khi Anh Linh là sĩ quan bị tù cải tạo tại Hoàng Liên Sơn, Vĩnh Phú, Vân Sơn chết lúc chạy trốn Cộng sản khi nhảy xuống cầu [[Thị Nghè|Thị Nghè]]{{Done}} và Tuấn Đăng còn ở lại đánh đàn tại Tiếng Dương Cầm khu cư xá Sĩ Quan Chí Hoà đường Lê văn Duyệt cũ. Theo [[tin]] vào năm 2016, trước đây khi Tuấn Đăng còn khỏe, thu nhập chính của ông là kéo violon cho ca đoàn Nhà thờ Hầm Đá – quận 11, TPHCM và biểu diễn violon ở một vài tụ điểm để có tiền mua gạo mỗi ngày.<ref>[http://nld.com.vn/van-hoa-van-nghe/thanh-vien-cuoi-cung-cua-nhom-avt-dang-khon-kho-20160315112742374.htm Thành viên cuối cùng của nhóm AVT đang khốn khó ], nld, 1.2.2009</ref> Ngoài ra, Ban nhạc AVT cũng đã góp mặt trong cuốn [[DVD|DVD]]{{Done}} Asia 10 "Gởi người một niềm vui" (ghi hình tại [[Montréal]], [[Canada]] vào năm 1996) và thu âm 2 CD, đều được [[Trung tâm Asia]] (trụ sở tại [[Hoa Kỳ]]) sản xuất và phát hành vào năm 1992 # ASIA CD 31 - Tướng Sỹ Tượng - gồm 7 bài # ASIA CD 44 - AVT Hải Ngoại - gồm 6 bài ==Chú thích== ==Liên kết ngoài== * [http://www.cgvdt.vn/van-hoa-nghe-thuat/nghe-si-tua-n-dang-ban-avt-vi-da-ng-cuo-c-do-i_a3006 Nghệ sĩ Tuấn Đăng (Ban AVT) – Vị đắng cuộc đời], [[Báo Công giáo và Dân tộc]] [[Thể loại:Văn hóa Việt Nam Cộng hòa]] [[Thể loại:Ban nhạc Việt Nam]] [[Thể loại:Ngâm sĩ]] test test sr7yeggpiwx7mzcz5xypekim6yktl80 Laurent Levy 0 113041 747157 438553 2026-06-17T09:39:42Z ~2026-35150-28 74487 Test 747157 wikitext text/x-wiki Laurent Levy is the CEO of Nanobiotix .He was born in 1971 in Montreuil, France. He is educated from Pierre and Marie Curie University, now Paris-Sorbonne University (BS, MS) SUNY Buffalo (PhD). Test larze09eo7o0xlu2fexnw5g9m514hv8 Midland Community Unit School District/edithistory 0 113745 747066 439729 2026-06-16T16:34:39Z ~2026-35331-13 74470 A change 747066 wikitext text/x-wiki {| class="wikitable" ! oldid || date/time || username || edit summary |---- | 956346029 || 2020-05-12T21:05:32Z || Johnpacklambert || <nowiki>added [[Category:1996 establishments in Illinois]] using [[WP:HC|HotCat]]</nowiki> |---- | 951325931 || 2020-04-16T16:39:45Z || Rodw || <nowiki>Disambiguating links to [[Journal Star]] (link changed to [[Peoria Journal Star]]; link changed to [[Peoria Journal Star]]; link changed to [[Peoria Journal Star]]) using [[User:Qwertyytrewqqwerty/DisamAssist|DisamAssist]].</nowiki> |---- | 948414634 || 2020-03-31T22:40:33Z || CAPTAIN RAJU || <nowiki>clean up, [[WP:AWB/T|typo(s) fixed]]: 2019-2020 → 2019–2020</nowiki> |---- | 948017997 || 2020-03-29T18:37:07Z || AnomieBOT || <nowiki>Dating maintenance tags: {{Expand section}}</nowiki> |---- | 948015172 || 2020-03-29T18:17:02Z || WhisperToMe || <nowiki>/* History */</nowiki> |---- | 948014782 || 2020-03-29T18:14:32Z || WhisperToMe || <nowiki>/* History */</nowiki> |---- | 948014624 || 2020-03-29T18:13:18Z || WhisperToMe || <nowiki>/* History */</nowiki> |---- | 948014299 || 2020-03-29T18:11:11Z || WhisperToMe || <nowiki>/* External links */</nowiki> |---- | 948014113 || 2020-03-29T18:09:55Z || WhisperToMe || <nowiki>/* History */</nowiki> |---- | 948013778 || 2020-03-29T18:07:27Z || WhisperToMe || <nowiki>/* History */</nowiki> |---- | 948012908 || 2020-03-29T18:01:09Z || WhisperToMe || <nowiki>Yet another school district which had its HS article already exist but the district article not so much. Finding secondary sources about superintendent changes</nowiki> |} 7e3mv32jbgofs10gga8798nng7lbg2x Thebestesteverpage 0 115370 747151 734047 2026-06-17T08:27:09Z ~2026-35550-08 74484 747151 wikitext text/x-wiki Test this and that! maybe this too! maybe this too! test bdnri3ao5g5gbmrjm483nb4u0o50zqs 747152 747151 2026-06-17T08:45:49Z ~2026-35391-51 74485 747152 wikitext text/x-wiki Test this and that! maybe this too! maybe this too! test test nmmfueb2ds8wit86k9rbasg1kzbf96d 747153 747152 2026-06-17T08:47:09Z ~2026-35391-51 74485 /* */ 747153 wikitext text/x-wiki Test this and that! maybe this too! maybe this too! test test test j6stqu1kr756obz1dnarpb2hck3e4qh 747154 747153 2026-06-17T08:48:50Z ~2026-35391-51 74485 /* */ 747154 wikitext text/x-wiki Test this and that! maybe this too! maybe this too! test test test test nazkj1uaaw3gp9zs88kpty1en1nxhkd 747155 747154 2026-06-17T08:51:59Z ~2026-35330-80 74486 747155 wikitext text/x-wiki Test this and that! maybe this too! maybe this too! test test nmmfueb2ds8wit86k9rbasg1kzbf96d 747156 747155 2026-06-17T08:55:18Z ~2026-35330-80 74486 /* */ 747156 wikitext text/x-wiki Test this and that! maybe this too! maybe this too! test bdnri3ao5g5gbmrjm483nb4u0o50zqs 747159 747156 2026-06-17T10:29:36Z ~2026-35255-41 74488 747159 wikitext text/x-wiki Test this and that! maybe this too! maybe this too! test test test j6stqu1kr756obz1dnarpb2hck3e4qh Template:Afd top 10 115526 747100 445987 2026-06-16T18:20:56Z Trialpears 43074 747100 wikitext text/x-wiki Intentionally broken for testing go back in history for normal version<noinclude> </div> {{Documentation}} </noinclude> nddjqbosm3un37qxe696bn7fh9a7y6j 747112 747100 2026-06-16T18:24:52Z Trialpears 43074 747112 wikitext text/x-wiki <noinclude> </div> {{Documentation}} </noinclude> lwi803waewcxzo2eptqqzku5k85vwaj Template:Afd bottom 10 115527 747114 445988 2026-06-16T18:25:48Z Trialpears 43074 Replaced content with "<noinclude> {{Documentation}} </noinclude>" 747114 wikitext text/x-wiki <noinclude> {{Documentation}} </noinclude> ltzf24yhonbtovmhn069rvcyx9c1gf7 User talk:Rachmat04 3 115969 746998 746888 2026-06-16T12:54:12Z Rachmat04 26096 Notification: User warning — [[w:id:Pengguna:Rachmat04/Tengu.js|⛩️]] 746998 wikitext text/x-wiki == Warning: disruptive editing == Your recent edits appear to be [[WP:DE|disruptive]]. Disruptive editing interferes with the normal operation and improvement of the wiki, regardless of intent. Please review the relevant guidelines and ensure that your contributions are constructive. If you believe this warning has been issued in error, please leave a message on my talk page. '''&middot;&middot;&middot;''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''&middot;''' [[User talk:Rachmat04|<span title="Let's discuss!">☕</span>]] 14:19, 14 June 2026 (UTC) == Warning: disruptive editing == Your recent edits appear to be [[WP:DE|disruptive]]. Disruptive editing interferes with the normal operation and improvement of the wiki, regardless of intent. Please review the relevant guidelines and ensure that your contributions are constructive. If you believe this warning has been issued in error, please leave a message on my talk page. Additional information: This is a test '''&middot;&middot;&middot;''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''&middot;''' [[User talk:Rachmat04|<span title="Let's discuss!">☕</span>]] 14:19, 14 June 2026 (UTC) == Final warning: vandalism == Your recent edits to one or more pages appear to constitute [[WP:VAND|vandalism]]. Vandalism is not permitted on this wiki. Please stop making edits that damage or degrade the encyclopaedia. If this behaviour continues, your account may be restricted from editing. If you believe this warning has been issued in error, please leave a message on my talk page. Additional information: Test '''&middot;&middot;&middot;''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''&middot;''' [[User talk:Rachmat04|<span title="Let's discuss!">☕</span>]] 14:32, 14 June 2026 (UTC) == Warning: disruptive editing == Your recent edits appear to be [[WP:DE|disruptive]]. Disruptive editing interferes with the normal operation and improvement of the wiki, regardless of intent. Please review the relevant guidelines and ensure that your contributions are constructive. If you believe this warning has been issued in error, please leave a message on my talk page. '''&middot;&middot;&middot;''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''&middot;''' [[User talk:Rachmat04|<span title="Let's discuss!">☕</span>]] 17:51, 15 June 2026 (UTC) == Final warning: disruptive editing == Your recent edits appear to be [[WP:DE|disruptive]]. Disruptive editing interferes with the normal operation and improvement of the wiki, regardless of intent. Please review the relevant guidelines and ensure that your contributions are constructive. If this behaviour continues, your account may be restricted from editing. If you believe this warning has been issued in error, please leave a message on my talk page. '''&middot;&middot;&middot;''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''&middot;''' [[User talk:Rachmat04|<span title="Let's discuss!">☕</span>]] 17:52, 15 June 2026 (UTC) == Warning: ownership of articles == Your recent editing behaviour suggests an attitude of [[WP:OWN|ownership]] towards one or more articles. No editor owns an article on this wiki; all content is open to improvement by any editor in good standing. Repeatedly reverting the edits of other users, or treating an article as a personal space, is not consistent with collaborative editing. Please review [[WP:OWN]] and discuss any disagreements on the relevant talk page. If you believe this warning has been issued in error, please leave a message on my talk page. '''&middot;&middot;&middot;''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''&middot;''' [[User talk:Rachmat04|<span title="Let's discuss!">☕</span>]] 17:53, 15 June 2026 (UTC) == Notice: creating articles not in the wiki language == Wikipedia only accepts articles written in English. An article you recently created or edited appears to have been written in a different language. Articles not written in English cannot be properly reviewed, maintained, or categorised by editors on this wiki, and are liable to be nominated for deletion. Before creating or editing an article, please make sure it is written in English. If you would like to contribute in another language, please visit the [[Wikipedia:List of Wikipedias|list of Wikipedia language editions]] to find the appropriate wiki. Translation tools may also help if you wish to adapt existing content into English. If you believe this notice has been issued in error, please leave a message on my talk page. '''&middot;&middot;&middot;''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''&middot;''' [[User talk:Rachmat04|<span title="Let's discuss!">☕</span>]] 12:54, 16 June 2026 (UTC) jgcyrpin2v6bc02z7gw32biafa60pup 747011 746998 2026-06-16T14:06:47Z Rachmat04 26096 Notification: User warning — [[w:id:Pengguna:Rachmat04/Tengu.js|⛩️]] 747011 wikitext text/x-wiki == Warning: disruptive editing == Your recent edits appear to be [[WP:DE|disruptive]]. Disruptive editing interferes with the normal operation and improvement of the wiki, regardless of intent. Please review the relevant guidelines and ensure that your contributions are constructive. If you believe this warning has been issued in error, please leave a message on my talk page. '''&middot;&middot;&middot;''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''&middot;''' [[User talk:Rachmat04|<span title="Let's discuss!">☕</span>]] 14:19, 14 June 2026 (UTC) == Warning: disruptive editing == Your recent edits appear to be [[WP:DE|disruptive]]. Disruptive editing interferes with the normal operation and improvement of the wiki, regardless of intent. Please review the relevant guidelines and ensure that your contributions are constructive. If you believe this warning has been issued in error, please leave a message on my talk page. Additional information: This is a test '''&middot;&middot;&middot;''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''&middot;''' [[User talk:Rachmat04|<span title="Let's discuss!">☕</span>]] 14:19, 14 June 2026 (UTC) == Final warning: vandalism == Your recent edits to one or more pages appear to constitute [[WP:VAND|vandalism]]. Vandalism is not permitted on this wiki. Please stop making edits that damage or degrade the encyclopaedia. If this behaviour continues, your account may be restricted from editing. If you believe this warning has been issued in error, please leave a message on my talk page. Additional information: Test '''&middot;&middot;&middot;''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''&middot;''' [[User talk:Rachmat04|<span title="Let's discuss!">☕</span>]] 14:32, 14 June 2026 (UTC) == Warning: disruptive editing == Your recent edits appear to be [[WP:DE|disruptive]]. Disruptive editing interferes with the normal operation and improvement of the wiki, regardless of intent. Please review the relevant guidelines and ensure that your contributions are constructive. If you believe this warning has been issued in error, please leave a message on my talk page. '''&middot;&middot;&middot;''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''&middot;''' [[User talk:Rachmat04|<span title="Let's discuss!">☕</span>]] 17:51, 15 June 2026 (UTC) == Final warning: disruptive editing == Your recent edits appear to be [[WP:DE|disruptive]]. Disruptive editing interferes with the normal operation and improvement of the wiki, regardless of intent. Please review the relevant guidelines and ensure that your contributions are constructive. If this behaviour continues, your account may be restricted from editing. If you believe this warning has been issued in error, please leave a message on my talk page. '''&middot;&middot;&middot;''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''&middot;''' [[User talk:Rachmat04|<span title="Let's discuss!">☕</span>]] 17:52, 15 June 2026 (UTC) == Warning: ownership of articles == Your recent editing behaviour suggests an attitude of [[WP:OWN|ownership]] towards one or more articles. No editor owns an article on this wiki; all content is open to improvement by any editor in good standing. Repeatedly reverting the edits of other users, or treating an article as a personal space, is not consistent with collaborative editing. Please review [[WP:OWN]] and discuss any disagreements on the relevant talk page. If you believe this warning has been issued in error, please leave a message on my talk page. '''&middot;&middot;&middot;''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''&middot;''' [[User talk:Rachmat04|<span title="Let's discuss!">☕</span>]] 17:53, 15 June 2026 (UTC) == Notice: creating articles not in the wiki language == Wikipedia only accepts articles written in English. An article you recently created or edited appears to have been written in a different language. Articles not written in English cannot be properly reviewed, maintained, or categorised by editors on this wiki, and are liable to be nominated for deletion. Before creating or editing an article, please make sure it is written in English. If you would like to contribute in another language, please visit the [[Wikipedia:List of Wikipedias|list of Wikipedia language editions]] to find the appropriate wiki. Translation tools may also help if you wish to adapt existing content into English. If you believe this notice has been issued in error, please leave a message on my talk page. '''&middot;&middot;&middot;''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''&middot;''' [[User talk:Rachmat04|<span title="Let's discuss!">☕</span>]] 12:54, 16 June 2026 (UTC) == Notice: adding inappropriate entries to lists == Your recent edits appear to have added one or more entries to a list that do not meet the inclusion criteria for that list. List articles define their scope in their lead section or guidelines; entries must meet those criteria and must be supported by a reliable source. Please review [[WP:LISTCRITERIA]] and the list's own documentation before adding further entries. If you believe this notice has been issued in error, please leave a message on my talk page. '''&middot;&middot;&middot;''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''&middot;''' [[User talk:Rachmat04|<span title="Let's discuss!">☕</span>]] 14:06, 16 June 2026 (UTC) alsn1579d363qznc5xl9ygaeza6x1kt 747012 747011 2026-06-16T14:08:56Z Rachmat04 26096 Notification: User warning — [[w:id:Pengguna:Rachmat04/Tengu.js|⛩️]] 747012 wikitext text/x-wiki == Warning: disruptive editing == Your recent edits appear to be [[WP:DE|disruptive]]. Disruptive editing interferes with the normal operation and improvement of the wiki, regardless of intent. Please review the relevant guidelines and ensure that your contributions are constructive. If you believe this warning has been issued in error, please leave a message on my talk page. '''&middot;&middot;&middot;''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''&middot;''' [[User talk:Rachmat04|<span title="Let's discuss!">☕</span>]] 14:19, 14 June 2026 (UTC) == Warning: disruptive editing == Your recent edits appear to be [[WP:DE|disruptive]]. Disruptive editing interferes with the normal operation and improvement of the wiki, regardless of intent. Please review the relevant guidelines and ensure that your contributions are constructive. If you believe this warning has been issued in error, please leave a message on my talk page. Additional information: This is a test '''&middot;&middot;&middot;''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''&middot;''' [[User talk:Rachmat04|<span title="Let's discuss!">☕</span>]] 14:19, 14 June 2026 (UTC) == Final warning: vandalism == Your recent edits to one or more pages appear to constitute [[WP:VAND|vandalism]]. Vandalism is not permitted on this wiki. Please stop making edits that damage or degrade the encyclopaedia. If this behaviour continues, your account may be restricted from editing. If you believe this warning has been issued in error, please leave a message on my talk page. Additional information: Test '''&middot;&middot;&middot;''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''&middot;''' [[User talk:Rachmat04|<span title="Let's discuss!">☕</span>]] 14:32, 14 June 2026 (UTC) == Warning: disruptive editing == Your recent edits appear to be [[WP:DE|disruptive]]. Disruptive editing interferes with the normal operation and improvement of the wiki, regardless of intent. Please review the relevant guidelines and ensure that your contributions are constructive. If you believe this warning has been issued in error, please leave a message on my talk page. '''&middot;&middot;&middot;''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''&middot;''' [[User talk:Rachmat04|<span title="Let's discuss!">☕</span>]] 17:51, 15 June 2026 (UTC) == Final warning: disruptive editing == Your recent edits appear to be [[WP:DE|disruptive]]. Disruptive editing interferes with the normal operation and improvement of the wiki, regardless of intent. Please review the relevant guidelines and ensure that your contributions are constructive. If this behaviour continues, your account may be restricted from editing. If you believe this warning has been issued in error, please leave a message on my talk page. '''&middot;&middot;&middot;''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''&middot;''' [[User talk:Rachmat04|<span title="Let's discuss!">☕</span>]] 17:52, 15 June 2026 (UTC) == Warning: ownership of articles == Your recent editing behaviour suggests an attitude of [[WP:OWN|ownership]] towards one or more articles. No editor owns an article on this wiki; all content is open to improvement by any editor in good standing. Repeatedly reverting the edits of other users, or treating an article as a personal space, is not consistent with collaborative editing. Please review [[WP:OWN]] and discuss any disagreements on the relevant talk page. If you believe this warning has been issued in error, please leave a message on my talk page. '''&middot;&middot;&middot;''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''&middot;''' [[User talk:Rachmat04|<span title="Let's discuss!">☕</span>]] 17:53, 15 June 2026 (UTC) == Notice: creating articles not in the wiki language == Wikipedia only accepts articles written in English. An article you recently created or edited appears to have been written in a different language. Articles not written in English cannot be properly reviewed, maintained, or categorised by editors on this wiki, and are liable to be nominated for deletion. Before creating or editing an article, please make sure it is written in English. If you would like to contribute in another language, please visit the [[Wikipedia:List of Wikipedias|list of Wikipedia language editions]] to find the appropriate wiki. Translation tools may also help if you wish to adapt existing content into English. If you believe this notice has been issued in error, please leave a message on my talk page. '''&middot;&middot;&middot;''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''&middot;''' [[User talk:Rachmat04|<span title="Let's discuss!">☕</span>]] 12:54, 16 June 2026 (UTC) == Notice: adding inappropriate entries to lists == Your recent edits appear to have added one or more entries to a list that do not meet the inclusion criteria for that list. List articles define their scope in their lead section or guidelines; entries must meet those criteria and must be supported by a reliable source. Please review [[WP:LISTCRITERIA]] and the list's own documentation before adding further entries. If you believe this notice has been issued in error, please leave a message on my talk page. '''&middot;&middot;&middot;''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''&middot;''' [[User talk:Rachmat04|<span title="Let's discuss!">☕</span>]] 14:06, 16 June 2026 (UTC) == Notice: long short description == Your recent edits appear to have added a short description that is longer than recommended. Short descriptions, added via the {{tlx|Short description}} template, are intended to be a brief disambiguating phrase — typically no more than 40 characters — that helps readers identify the article's subject in search results. They should not be full sentences. Please review [[WP:SDSHORT]] for guidance on writing effective short descriptions. If you believe this notice has been issued in error, please leave a message on my talk page. '''&middot;&middot;&middot;''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''&middot;''' [[User talk:Rachmat04|<span title="Let's discuss!">☕</span>]] 14:08, 16 June 2026 (UTC) etawxu87lekbiunftqlu5htho04wpmd 747013 747012 2026-06-16T14:09:52Z Rachmat04 26096 Notification: User warning — [[w:id:Pengguna:Rachmat04/Tengu.js|⛩️]] 747013 wikitext text/x-wiki == Warning: disruptive editing == Your recent edits appear to be [[WP:DE|disruptive]]. Disruptive editing interferes with the normal operation and improvement of the wiki, regardless of intent. Please review the relevant guidelines and ensure that your contributions are constructive. If you believe this warning has been issued in error, please leave a message on my talk page. '''&middot;&middot;&middot;''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''&middot;''' [[User talk:Rachmat04|<span title="Let's discuss!">☕</span>]] 14:19, 14 June 2026 (UTC) == Warning: disruptive editing == Your recent edits appear to be [[WP:DE|disruptive]]. Disruptive editing interferes with the normal operation and improvement of the wiki, regardless of intent. Please review the relevant guidelines and ensure that your contributions are constructive. If you believe this warning has been issued in error, please leave a message on my talk page. Additional information: This is a test '''&middot;&middot;&middot;''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''&middot;''' [[User talk:Rachmat04|<span title="Let's discuss!">☕</span>]] 14:19, 14 June 2026 (UTC) == Final warning: vandalism == Your recent edits to one or more pages appear to constitute [[WP:VAND|vandalism]]. Vandalism is not permitted on this wiki. Please stop making edits that damage or degrade the encyclopaedia. If this behaviour continues, your account may be restricted from editing. If you believe this warning has been issued in error, please leave a message on my talk page. Additional information: Test '''&middot;&middot;&middot;''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''&middot;''' [[User talk:Rachmat04|<span title="Let's discuss!">☕</span>]] 14:32, 14 June 2026 (UTC) == Warning: disruptive editing == Your recent edits appear to be [[WP:DE|disruptive]]. Disruptive editing interferes with the normal operation and improvement of the wiki, regardless of intent. Please review the relevant guidelines and ensure that your contributions are constructive. If you believe this warning has been issued in error, please leave a message on my talk page. '''&middot;&middot;&middot;''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''&middot;''' [[User talk:Rachmat04|<span title="Let's discuss!">☕</span>]] 17:51, 15 June 2026 (UTC) == Final warning: disruptive editing == Your recent edits appear to be [[WP:DE|disruptive]]. Disruptive editing interferes with the normal operation and improvement of the wiki, regardless of intent. Please review the relevant guidelines and ensure that your contributions are constructive. If this behaviour continues, your account may be restricted from editing. If you believe this warning has been issued in error, please leave a message on my talk page. '''&middot;&middot;&middot;''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''&middot;''' [[User talk:Rachmat04|<span title="Let's discuss!">☕</span>]] 17:52, 15 June 2026 (UTC) == Warning: ownership of articles == Your recent editing behaviour suggests an attitude of [[WP:OWN|ownership]] towards one or more articles. No editor owns an article on this wiki; all content is open to improvement by any editor in good standing. Repeatedly reverting the edits of other users, or treating an article as a personal space, is not consistent with collaborative editing. Please review [[WP:OWN]] and discuss any disagreements on the relevant talk page. If you believe this warning has been issued in error, please leave a message on my talk page. '''&middot;&middot;&middot;''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''&middot;''' [[User talk:Rachmat04|<span title="Let's discuss!">☕</span>]] 17:53, 15 June 2026 (UTC) == Notice: creating articles not in the wiki language == Wikipedia only accepts articles written in English. An article you recently created or edited appears to have been written in a different language. Articles not written in English cannot be properly reviewed, maintained, or categorised by editors on this wiki, and are liable to be nominated for deletion. Before creating or editing an article, please make sure it is written in English. If you would like to contribute in another language, please visit the [[Wikipedia:List of Wikipedias|list of Wikipedia language editions]] to find the appropriate wiki. Translation tools may also help if you wish to adapt existing content into English. If you believe this notice has been issued in error, please leave a message on my talk page. '''&middot;&middot;&middot;''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''&middot;''' [[User talk:Rachmat04|<span title="Let's discuss!">☕</span>]] 12:54, 16 June 2026 (UTC) == Notice: adding inappropriate entries to lists == Your recent edits appear to have added one or more entries to a list that do not meet the inclusion criteria for that list. List articles define their scope in their lead section or guidelines; entries must meet those criteria and must be supported by a reliable source. Please review [[WP:LISTCRITERIA]] and the list's own documentation before adding further entries. If you believe this notice has been issued in error, please leave a message on my talk page. '''&middot;&middot;&middot;''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''&middot;''' [[User talk:Rachmat04|<span title="Let's discuss!">☕</span>]] 14:06, 16 June 2026 (UTC) == Notice: long short description == Your recent edits appear to have added a short description that is longer than recommended. Short descriptions, added via the {{tlx|Short description}} template, are intended to be a brief disambiguating phrase — typically no more than 40 characters — that helps readers identify the article's subject in search results. They should not be full sentences. Please review [[WP:SDSHORT]] for guidance on writing effective short descriptions. If you believe this notice has been issued in error, please leave a message on my talk page. '''&middot;&middot;&middot;''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''&middot;''' [[User talk:Rachmat04|<span title="Let's discuss!">☕</span>]] 14:08, 16 June 2026 (UTC) == Notice: not communicating in the wiki language == This Wikipedia operates in {{#language:English}}. A recent message you left appears to have been written in a different language. This makes it difficult for other editors and administrators on this wiki to read and respond to your message, and it may be archived without action as a result. When communicating on talk pages, noticeboards, and other discussion areas, please write in {{#language:English}}. If you are not confident in {{#language:English}}, you may use a translation tool to help. For wikis in your own language, please visit the [[Wikipedia:List of Wikipedias|list of Wikipedia language editions]]. If you believe this notice has been issued in error, please leave a message on my talk page. '''&middot;&middot;&middot;''' <span title="Cherry blossom">🌸</span> [[User:Rachmat04|'''Rachmat04''']] '''&middot;''' [[User talk:Rachmat04|<span title="Let's discuss!">☕</span>]] 14:09, 16 June 2026 (UTC) jx2vmnruisxftr4t964iqigyis9by8d Template:Class 10 116173 747094 570435 2026-06-16T18:12:20Z ~2026-35344-03 74479 747094 wikitext text/x-wiki <noinclude><table><tr></noinclude><!-- -->{{#invoke:Class|class}}<!-- --><noinclude></tr></table> {{documentation}} </noinclude> q6eltnq6tswntlg5idy1k95lrj7f5bb Testing of WMCUG Link remove bot 0 121230 747160 481072 2026-06-17T11:34:39Z ~2026-35326-40 74490 747160 wikitext text/x-wiki something.removed.example.com test 5hich5s0i7wsnaap90o7yfqclf3wgfw TestWPBanners4 0 125986 747065 690527 2026-06-16T15:29:09Z ~2026-35249-34 74466 747065 wikitext text/x-wiki Testing various banner fixes: * Wrap banners in banner shell * Deduplicate wikiprojects * Another test * Remove "Draft" assessment from existing wikiprojects ""=<code></code>, [1]=<code></code>, [2]=<code></code>, link_1=<code></code>, link 1=<code></code>, link 1=<code></code>, link 1=<code></code>; link n=, <nowiki>link1</nowiki>=; link1=, link2=, link3=, link4= 0oqbemgw1qfxky3rmv0y2o2gkf9o98v User:Trialpears/common.js 2 149465 747001 745568 2026-06-16T13:18:11Z Trialpears 43074 uptodate twinkle 747001 javascript text/javascript // mw.loader.load('http://127.0.0.1:5500'); //Twinkle testing // Dev version of XFDcloser var xfdcDevUrl = "http://localhost:8125/dist/loader-dev.js"; mw.loader.getScript(xfdcDevUrl).catch(function(e) { e.message += " " + xfdcDevUrl; console.error(e); }); mw.loader.load("https://en.wikipedia.org/wiki/MediaWiki:Gadget-Twinkle.js&action=raw&ctype=text/js", "text/css" ) lodu7sicoq7rmppg8pr63ca14qzoe6n 747002 747001 2026-06-16T13:20:44Z Trialpears 43074 747002 javascript text/javascript // mw.loader.load('http://127.0.0.1:5500'); //Twinkle testing // Dev version of XFDcloser var xfdcDevUrl = "http://localhost:8125/dist/loader-dev.js"; mw.loader.getScript(xfdcDevUrl).catch(function(e) { e.message += " " + xfdcDevUrl; console.error(e); }); mw.loader.load("https://en.wikipedia.org/wiki/MediaWiki:Gadget-Twinkle.js&action=raw&ctype=text/js", "text/js" ) 79fexizi1a8uvwdj6z58ge8qjzsb0ci 747003 747002 2026-06-16T13:21:26Z Trialpears 43074 747003 javascript text/javascript // mw.loader.load('http://127.0.0.1:5500'); //Twinkle testing // Dev version of XFDcloser var xfdcDevUrl = "http://localhost:8125/dist/loader-dev.js"; mw.loader.getScript(xfdcDevUrl).catch(function(e) { e.message += " " + xfdcDevUrl; console.error(e); }); mw.loader.load("https://en.wikipedia.org/wiki/MediaWiki:Gadget-Twinkle.js&action=raw&ctype=text/js", "text/javascript" ) 07o1ocmebs1t1yp3zhdjkbpobdclmcr 747004 747003 2026-06-16T13:22:48Z Trialpears 43074 747004 javascript text/javascript // mw.loader.load('http://127.0.0.1:5500'); //Twinkle testing // Dev version of XFDcloser var xfdcDevUrl = "http://localhost:8125/dist/loader-dev.js"; mw.loader.getScript(xfdcDevUrl).catch(function(e) { e.message += " " + xfdcDevUrl; console.error(e); }); mw.loader.load("https://en.wikipedia.org/wiki/MediaWiki:Gadget-Twinkle.js&action=raw&ctype=text/javascript", "text/javascript" ) owvztlf5ckj3airmnkacse6wy18l5ns 747005 747004 2026-06-16T13:23:39Z Trialpears 43074 747005 javascript text/javascript // mw.loader.load('http://127.0.0.1:5500'); //Twinkle testing // Dev version of XFDcloser var xfdcDevUrl = "http://localhost:8125/dist/loader-dev.js"; mw.loader.getScript(xfdcDevUrl).catch(function(e) { e.message += " " + xfdcDevUrl; console.error(e); }); mw.loader.load("https://en.wikipedia.org/wiki/MediaWiki:Gadget-Twinkle.js?action=raw&ctype=text/javascript", "text/javascript" ) dnio921hgc0lxkdvitofqdfsmel5orc 747006 747005 2026-06-16T13:25:46Z Trialpears 43074 747006 javascript text/javascript // mw.loader.load('http://127.0.0.1:5500'); //Twinkle testing // Dev version of XFDcloser var xfdcDevUrl = "http://localhost:8125/dist/loader-dev.js"; mw.loader.getScript(xfdcDevUrl).catch(function(e) { e.message += " " + xfdcDevUrl; console.error(e); }); mw.loader.load("https://en.wikipedia.org/wiki/MediaWiki:Gadget-Twinkle.js?action=raw&ctype=text/javascript", "text/javascript" ) mw.loader.load("https://en.wikipedia.org/wiki/MediaWiki:Gadget-morebits.js?action=raw&ctype=text/javascript", "text/javascript" ) 0jqqg9x67svkh7xqa1er15osa4pw6hq 747007 747006 2026-06-16T13:30:31Z Trialpears 43074 747007 javascript text/javascript // mw.loader.load('http://127.0.0.1:5500'); //Twinkle testing // Dev version of XFDcloser var xfdcDevUrl = "http://localhost:8125/dist/loader-dev.js"; mw.loader.getScript(xfdcDevUrl).catch(function(e) { e.message += " " + xfdcDevUrl; console.error(e); }); mw.loader.load("https://en.wikipedia.org/wiki/MediaWiki:Gadget-Twinkle.js?action=raw&ctype=text/javascript", "text/javascript" ) mw.loader.load("https://en.wikipedia.org/wiki/MediaWiki:Gadget-morebits.js?action=raw&ctype=text/javascript", "text/javascript" ) mw.loader.load("https://en.wikipedia.org/wiki/MediaWiki:Gadget-twinklexfd.js?action=raw&ctype=text/javascript", "text/javascript" ) e8a20gob9plqtavaojx6n4syhjp4rjd 747074 747007 2026-06-16T17:51:18Z Trialpears 43074 747074 javascript text/javascript mw.loader.load('http://127.0.0.1:5500'); //Twinkle testing // Dev version of XFDcloser var xfdcDevUrl = "http://localhost:8125/dist/loader-dev.js"; mw.loader.getScript(xfdcDevUrl).catch(function(e) { e.message += " " + xfdcDevUrl; console.error(e); }); i2l6m98doacv7fm86uiu1e0mk6s0trc 747082 747074 2026-06-16T18:07:31Z Trialpears 43074 747082 javascript text/javascript mw.loader.load('http://localhost:5500/'); //Twinkle testing // Dev version of XFDcloser var xfdcDevUrl = "http://localhost:8125/dist/loader-dev.js"; mw.loader.getScript(xfdcDevUrl).catch(function(e) { e.message += " " + xfdcDevUrl; console.error(e); }); dqi407pqf9rgp3spzcgggdt3u0n4gby 747083 747082 2026-06-16T18:08:49Z Trialpears 43074 747083 javascript text/javascript mw.loader.load('http://localhost:5500'); //Twinkle testing // Dev version of XFDcloser var xfdcDevUrl = "http://localhost:8125/dist/loader-dev.js"; mw.loader.getScript(xfdcDevUrl).catch(function(e) { e.message += " " + xfdcDevUrl; console.error(e); }); 1gdvkig069notsm4uqnpa2jalsbrtvt T329968 0 151676 747009 744314 2026-06-16T13:47:09Z User97104 72884 Replaced content with "im" 747009 wikitext text/x-wiki im 7s6nvtxljt3bubdu71h0l2e6p81lded Test 0 155073 747008 746994 2026-06-16T13:42:43Z ~2026-35100-78 74458 747008 wikitext text/x-wiki <!-- Please do not remove or change this AfD message until the discussion has been closed. --> {{Article for deletion/dated|page=Test|timestamp=20260614200246|year=2026|month=June|day=14|substed=yes|help=off}} <!-- Once discussion closed, please place on talk page: {{Old AfD multi|page=Test|date=14 June 2026|result='''keep'''}} --> <!-- End of AfD message, feel free to edit beyond this point --> <noinclude>{{к удалению|2026-06-14}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alzvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nomze completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob enzcomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test.........zzzz https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>.z == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram|P hcaptcha probeolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit == Testing == cross-origin edit == Testing == cross-origin edit sxcew8jhqhoeek9z81oaikz1keahdf8 747019 747008 2026-06-16T15:01:36Z Pginer-WMF 19605 /* Testing */ 747019 wikitext text/x-wiki <!-- Please do not remove or change this AfD message until the discussion has been closed. --> {{Article for deletion/dated|page=Test|timestamp=20260614200246|year=2026|month=June|day=14|substed=yes|help=off}} <!-- Once discussion closed, please place on talk page: {{Old AfD multi|page=Test|date=14 June 2026|result='''keep'''}} --> <!-- End of AfD message, feel free to edit beyond this point --> <noinclude>{{к удалению|2026-06-14}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alzvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nomze completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob enzcomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test.........zzzz https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>.z == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram|P hcaptcha probeolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit == Testing == cross-origin edit test == Testing == cross-origin edit niow5t4dtthrjo1n2g1z5qf31jeibwl 747020 747019 2026-06-16T15:02:56Z Pginer-WMF 19605 /* Testing */ 747020 wikitext text/x-wiki <!-- Please do not remove or change this AfD message until the discussion has been closed. --> {{Article for deletion/dated|page=Test|timestamp=20260614200246|year=2026|month=June|day=14|substed=yes|help=off}} <!-- Once discussion closed, please place on talk page: {{Old AfD multi|page=Test|date=14 June 2026|result='''keep'''}} --> <!-- End of AfD message, feel free to edit beyond this point --> <noinclude>{{к удалению|2026-06-14}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alzvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nomze completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob enzcomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test.........zzzz https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>.z == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram|P hcaptcha probeolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit == Testing == cross-origin edit test test == Testing == cross-origin edit hzqoi8l46jks4ln8oei55ldd8bajw3q 747021 747020 2026-06-16T15:03:13Z Pginer-WMF 19605 /* Testing */ 747021 wikitext text/x-wiki <!-- Please do not remove or change this AfD message until the discussion has been closed. --> {{Article for deletion/dated|page=Test|timestamp=20260614200246|year=2026|month=June|day=14|substed=yes|help=off}} <!-- Once discussion closed, please place on talk page: {{Old AfD multi|page=Test|date=14 June 2026|result='''keep'''}} --> <!-- End of AfD message, feel free to edit beyond this point --> <noinclude>{{к удалению|2026-06-14}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alzvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nomze completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob enzcomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test.........zzzz https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>.z == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram|P hcaptcha probeolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit == Testing == cross-origin edit test test test == Testing == cross-origin edit p7d40pewcx8b2rvdcawwvd0rrfmtdo8 747022 747021 2026-06-16T15:04:10Z Pginer-WMF 19605 /* Testing */ 747022 wikitext text/x-wiki <!-- Please do not remove or change this AfD message until the discussion has been closed. --> {{Article for deletion/dated|page=Test|timestamp=20260614200246|year=2026|month=June|day=14|substed=yes|help=off}} <!-- Once discussion closed, please place on talk page: {{Old AfD multi|page=Test|date=14 June 2026|result='''keep'''}} --> <!-- End of AfD message, feel free to edit beyond this point --> <noinclude>{{к удалению|2026-06-14}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alzvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nomze completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob enzcomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test.........zzzz https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>.z == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram|P hcaptcha probeolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit == Testing == cross-origin edit test test test test == Testing == cross-origin edit dipvhacyd2069z7ffipr4br5q7v8kv4 747023 747022 2026-06-16T15:06:11Z Pginer-WMF 19605 /* Testing */ 747023 wikitext text/x-wiki <!-- Please do not remove or change this AfD message until the discussion has been closed. --> {{Article for deletion/dated|page=Test|timestamp=20260614200246|year=2026|month=June|day=14|substed=yes|help=off}} <!-- Once discussion closed, please place on talk page: {{Old AfD multi|page=Test|date=14 June 2026|result='''keep'''}} --> <!-- End of AfD message, feel free to edit beyond this point --> <noinclude>{{к удалению|2026-06-14}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alzvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nomze completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob enzcomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test.........zzzz https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>.z == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram|P hcaptcha probeolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit == Testing == cross-origin edit == Testing == cross-origin edit sxcew8jhqhoeek9z81oaikz1keahdf8 747024 747023 2026-06-16T15:07:15Z Pginer-WMF 19605 /* Testing */ 747024 wikitext text/x-wiki <!-- Please do not remove or change this AfD message until the discussion has been closed. --> {{Article for deletion/dated|page=Test|timestamp=20260614200246|year=2026|month=June|day=14|substed=yes|help=off}} <!-- Once discussion closed, please place on talk page: {{Old AfD multi|page=Test|date=14 June 2026|result='''keep'''}} --> <!-- End of AfD message, feel free to edit beyond this point --> <noinclude>{{к удалению|2026-06-14}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alzvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nomze completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob enzcomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test.........zzzz https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>.z == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram|P hcaptcha probeolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit test == Testing == cross-origin edit == Testing == cross-origin edit axqiw3qnjo8mbozpjbj8uag1gkvmi8j 747025 747024 2026-06-16T15:07:37Z Pginer-WMF 19605 /* Testing */ 747025 wikitext text/x-wiki <!-- Please do not remove or change this AfD message until the discussion has been closed. --> {{Article for deletion/dated|page=Test|timestamp=20260614200246|year=2026|month=June|day=14|substed=yes|help=off}} <!-- Once discussion closed, please place on talk page: {{Old AfD multi|page=Test|date=14 June 2026|result='''keep'''}} --> <!-- End of AfD message, feel free to edit beyond this point --> <noinclude>{{к удалению|2026-06-14}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alzvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nomze completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob enzcomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test.........zzzz https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>.z == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram|P hcaptcha probeolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit test test == Testing == cross-origin edit == Testing == cross-origin edit 5npx5r3ovgmvn9dn8z9fg021x2nqj7k 747026 747025 2026-06-16T15:07:48Z Pginer-WMF 19605 /* Testing */ 747026 wikitext text/x-wiki <!-- Please do not remove or change this AfD message until the discussion has been closed. --> {{Article for deletion/dated|page=Test|timestamp=20260614200246|year=2026|month=June|day=14|substed=yes|help=off}} <!-- Once discussion closed, please place on talk page: {{Old AfD multi|page=Test|date=14 June 2026|result='''keep'''}} --> <!-- End of AfD message, feel free to edit beyond this point --> <noinclude>{{к удалению|2026-06-14}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alzvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nomze completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob enzcomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test.........zzzz https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>.z == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram|P hcaptcha probeolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit test test test == Testing == cross-origin edit == Testing == cross-origin edit 0rb0rvwttfsd5ojkp31nevx4mbpeb47 747027 747026 2026-06-16T15:07:58Z Pginer-WMF 19605 /* Testing */ 747027 wikitext text/x-wiki <!-- Please do not remove or change this AfD message until the discussion has been closed. --> {{Article for deletion/dated|page=Test|timestamp=20260614200246|year=2026|month=June|day=14|substed=yes|help=off}} <!-- Once discussion closed, please place on talk page: {{Old AfD multi|page=Test|date=14 June 2026|result='''keep'''}} --> <!-- End of AfD message, feel free to edit beyond this point --> <noinclude>{{к удалению|2026-06-14}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alzvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nomze completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob enzcomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test.........zzzz https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>.z == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram|P hcaptcha probeolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit test test test test == Testing == cross-origin edit == Testing == cross-origin edit 10eeb1ik7akvpd48jm8jerm9kazk2wr 747028 747027 2026-06-16T15:08:17Z Pginer-WMF 19605 /* Testing */ 747028 wikitext text/x-wiki <!-- Please do not remove or change this AfD message until the discussion has been closed. --> {{Article for deletion/dated|page=Test|timestamp=20260614200246|year=2026|month=June|day=14|substed=yes|help=off}} <!-- Once discussion closed, please place on talk page: {{Old AfD multi|page=Test|date=14 June 2026|result='''keep'''}} --> <!-- End of AfD message, feel free to edit beyond this point --> <noinclude>{{к удалению|2026-06-14}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alzvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nomze completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob enzcomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test.........zzzz https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>.z == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram|P hcaptcha probeolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit test test test test test == Testing == cross-origin edit == Testing == cross-origin edit t7vkefg8i4894bmer9gg1gxlo4852xd 747029 747028 2026-06-16T15:08:32Z Pginer-WMF 19605 /* Testing */ 747029 wikitext text/x-wiki <!-- Please do not remove or change this AfD message until the discussion has been closed. --> {{Article for deletion/dated|page=Test|timestamp=20260614200246|year=2026|month=June|day=14|substed=yes|help=off}} <!-- Once discussion closed, please place on talk page: {{Old AfD multi|page=Test|date=14 June 2026|result='''keep'''}} --> <!-- End of AfD message, feel free to edit beyond this point --> <noinclude>{{к удалению|2026-06-14}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alzvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nomze completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob enzcomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test.........zzzz https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>.z == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram|P hcaptcha probeolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit test test test test test test == Testing == cross-origin edit == Testing == cross-origin edit ifrbfm1yi23g7d8r19mgtz77a359h5v 747030 747029 2026-06-16T15:08:46Z Pginer-WMF 19605 /* Testing */ 747030 wikitext text/x-wiki <!-- Please do not remove or change this AfD message until the discussion has been closed. --> {{Article for deletion/dated|page=Test|timestamp=20260614200246|year=2026|month=June|day=14|substed=yes|help=off}} <!-- Once discussion closed, please place on talk page: {{Old AfD multi|page=Test|date=14 June 2026|result='''keep'''}} --> <!-- End of AfD message, feel free to edit beyond this point --> <noinclude>{{к удалению|2026-06-14}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alzvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nomze completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob enzcomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test.........zzzz https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>.z == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram|P hcaptcha probeolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit test test test test test test test == Testing == cross-origin edit == Testing == cross-origin edit 1c5eguv94qhphf3drydlao2riejb2wt 747031 747030 2026-06-16T15:09:00Z Pginer-WMF 19605 /* Testing */ 747031 wikitext text/x-wiki <!-- Please do not remove or change this AfD message until the discussion has been closed. --> {{Article for deletion/dated|page=Test|timestamp=20260614200246|year=2026|month=June|day=14|substed=yes|help=off}} <!-- Once discussion closed, please place on talk page: {{Old AfD multi|page=Test|date=14 June 2026|result='''keep'''}} --> <!-- End of AfD message, feel free to edit beyond this point --> <noinclude>{{к удалению|2026-06-14}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alzvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nomze completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob enzcomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test.........zzzz https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>.z == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram|P hcaptcha probeolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit test == Testing == cross-origin edit == Testing == cross-origin edit axqiw3qnjo8mbozpjbj8uag1gkvmi8j 747032 747031 2026-06-16T15:09:47Z Pginer-WMF 19605 /* Testing */ 747032 wikitext text/x-wiki <!-- Please do not remove or change this AfD message until the discussion has been closed. --> {{Article for deletion/dated|page=Test|timestamp=20260614200246|year=2026|month=June|day=14|substed=yes|help=off}} <!-- Once discussion closed, please place on talk page: {{Old AfD multi|page=Test|date=14 June 2026|result='''keep'''}} --> <!-- End of AfD message, feel free to edit beyond this point --> <noinclude>{{к удалению|2026-06-14}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alzvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nomze completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob enzcomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test.........zzzz https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>.z == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram|P hcaptcha probeolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit test test test == Testing == cross-origin edit == Testing == cross-origin edit 0rb0rvwttfsd5ojkp31nevx4mbpeb47 747033 747032 2026-06-16T15:10:01Z Pginer-WMF 19605 /* Testing */ 747033 wikitext text/x-wiki <!-- Please do not remove or change this AfD message until the discussion has been closed. --> {{Article for deletion/dated|page=Test|timestamp=20260614200246|year=2026|month=June|day=14|substed=yes|help=off}} <!-- Once discussion closed, please place on talk page: {{Old AfD multi|page=Test|date=14 June 2026|result='''keep'''}} --> <!-- End of AfD message, feel free to edit beyond this point --> <noinclude>{{к удалению|2026-06-14}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alzvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nomze completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob enzcomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test.........zzzz https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>.z == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram|P hcaptcha probeolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit test == Testing == cross-origin edit == Testing == cross-origin edit axqiw3qnjo8mbozpjbj8uag1gkvmi8j 747034 747033 2026-06-16T15:10:11Z Pginer-WMF 19605 /* Testing */ 747034 wikitext text/x-wiki <!-- Please do not remove or change this AfD message until the discussion has been closed. --> {{Article for deletion/dated|page=Test|timestamp=20260614200246|year=2026|month=June|day=14|substed=yes|help=off}} <!-- Once discussion closed, please place on talk page: {{Old AfD multi|page=Test|date=14 June 2026|result='''keep'''}} --> <!-- End of AfD message, feel free to edit beyond this point --> <noinclude>{{к удалению|2026-06-14}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alzvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nomze completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob enzcomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test.........zzzz https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>.z == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram|P hcaptcha probeolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit test test == Testing == cross-origin edit == Testing == cross-origin edit 5npx5r3ovgmvn9dn8z9fg021x2nqj7k 747035 747034 2026-06-16T15:10:21Z Pginer-WMF 19605 /* Testing */ 747035 wikitext text/x-wiki <!-- Please do not remove or change this AfD message until the discussion has been closed. --> {{Article for deletion/dated|page=Test|timestamp=20260614200246|year=2026|month=June|day=14|substed=yes|help=off}} <!-- Once discussion closed, please place on talk page: {{Old AfD multi|page=Test|date=14 June 2026|result='''keep'''}} --> <!-- End of AfD message, feel free to edit beyond this point --> <noinclude>{{к удалению|2026-06-14}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alzvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nomze completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob enzcomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test.........zzzz https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>.z == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram|P hcaptcha probeolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit test test test == Testing == cross-origin edit == Testing == cross-origin edit 0rb0rvwttfsd5ojkp31nevx4mbpeb47 747036 747035 2026-06-16T15:10:31Z Pginer-WMF 19605 /* Testing */ 747036 wikitext text/x-wiki <!-- Please do not remove or change this AfD message until the discussion has been closed. --> {{Article for deletion/dated|page=Test|timestamp=20260614200246|year=2026|month=June|day=14|substed=yes|help=off}} <!-- Once discussion closed, please place on talk page: {{Old AfD multi|page=Test|date=14 June 2026|result='''keep'''}} --> <!-- End of AfD message, feel free to edit beyond this point --> <noinclude>{{к удалению|2026-06-14}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alzvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nomze completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob enzcomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test.........zzzz https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>.z == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram|P hcaptcha probeolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit test test test test == Testing == cross-origin edit == Testing == cross-origin edit 10eeb1ik7akvpd48jm8jerm9kazk2wr 747037 747036 2026-06-16T15:10:43Z Pginer-WMF 19605 /* Testing */ 747037 wikitext text/x-wiki <!-- Please do not remove or change this AfD message until the discussion has been closed. --> {{Article for deletion/dated|page=Test|timestamp=20260614200246|year=2026|month=June|day=14|substed=yes|help=off}} <!-- Once discussion closed, please place on talk page: {{Old AfD multi|page=Test|date=14 June 2026|result='''keep'''}} --> <!-- End of AfD message, feel free to edit beyond this point --> <noinclude>{{к удалению|2026-06-14}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alzvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nomze completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob enzcomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test.........zzzz https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>.z == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram|P hcaptcha probeolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit == Testing == cross-origin edit == Testing == cross-origin edit sxcew8jhqhoeek9z81oaikz1keahdf8 747038 747037 2026-06-16T15:12:24Z Pginer-WMF 19605 /* Testing */ 747038 wikitext text/x-wiki <!-- Please do not remove or change this AfD message until the discussion has been closed. --> {{Article for deletion/dated|page=Test|timestamp=20260614200246|year=2026|month=June|day=14|substed=yes|help=off}} <!-- Once discussion closed, please place on talk page: {{Old AfD multi|page=Test|date=14 June 2026|result='''keep'''}} --> <!-- End of AfD message, feel free to edit beyond this point --> <noinclude>{{к удалению|2026-06-14}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alzvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nomze completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob enzcomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test.........zzzz https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>.z == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram|P hcaptcha probeolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit test == Testing == cross-origin edit == Testing == cross-origin edit axqiw3qnjo8mbozpjbj8uag1gkvmi8j 747039 747038 2026-06-16T15:12:34Z Pginer-WMF 19605 /* Testing */ 747039 wikitext text/x-wiki <!-- Please do not remove or change this AfD message until the discussion has been closed. --> {{Article for deletion/dated|page=Test|timestamp=20260614200246|year=2026|month=June|day=14|substed=yes|help=off}} <!-- Once discussion closed, please place on talk page: {{Old AfD multi|page=Test|date=14 June 2026|result='''keep'''}} --> <!-- End of AfD message, feel free to edit beyond this point --> <noinclude>{{к удалению|2026-06-14}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alzvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nomze completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob enzcomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test.........zzzz https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>.z == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram|P hcaptcha probeolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit test test == Testing == cross-origin edit == Testing == cross-origin edit 5npx5r3ovgmvn9dn8z9fg021x2nqj7k 747040 747039 2026-06-16T15:12:43Z Pginer-WMF 19605 /* Testing */ 747040 wikitext text/x-wiki <!-- Please do not remove or change this AfD message until the discussion has been closed. --> {{Article for deletion/dated|page=Test|timestamp=20260614200246|year=2026|month=June|day=14|substed=yes|help=off}} <!-- Once discussion closed, please place on talk page: {{Old AfD multi|page=Test|date=14 June 2026|result='''keep'''}} --> <!-- End of AfD message, feel free to edit beyond this point --> <noinclude>{{к удалению|2026-06-14}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alzvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nomze completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob enzcomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test.........zzzz https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>.z == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram|P hcaptcha probeolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit test test test == Testing == cross-origin edit == Testing == cross-origin edit 0rb0rvwttfsd5ojkp31nevx4mbpeb47 747041 747040 2026-06-16T15:12:50Z Pginer-WMF 19605 /* Testing */ 747041 wikitext text/x-wiki <!-- Please do not remove or change this AfD message until the discussion has been closed. --> {{Article for deletion/dated|page=Test|timestamp=20260614200246|year=2026|month=June|day=14|substed=yes|help=off}} <!-- Once discussion closed, please place on talk page: {{Old AfD multi|page=Test|date=14 June 2026|result='''keep'''}} --> <!-- End of AfD message, feel free to edit beyond this point --> <noinclude>{{к удалению|2026-06-14}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alzvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nomze completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob enzcomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test.........zzzz https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>.z == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram|P hcaptcha probeolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit test test test test == Testing == cross-origin edit == Testing == cross-origin edit 10eeb1ik7akvpd48jm8jerm9kazk2wr 747042 747041 2026-06-16T15:12:58Z Pginer-WMF 19605 /* Testing */ 747042 wikitext text/x-wiki <!-- Please do not remove or change this AfD message until the discussion has been closed. --> {{Article for deletion/dated|page=Test|timestamp=20260614200246|year=2026|month=June|day=14|substed=yes|help=off}} <!-- Once discussion closed, please place on talk page: {{Old AfD multi|page=Test|date=14 June 2026|result='''keep'''}} --> <!-- End of AfD message, feel free to edit beyond this point --> <noinclude>{{к удалению|2026-06-14}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alzvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nomze completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob enzcomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test.........zzzz https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>.z == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram|P hcaptcha probeolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit test test test test test == Testing == cross-origin edit == Testing == cross-origin edit t7vkefg8i4894bmer9gg1gxlo4852xd 747043 747042 2026-06-16T15:13:09Z Pginer-WMF 19605 /* Testing */ 747043 wikitext text/x-wiki <!-- Please do not remove or change this AfD message until the discussion has been closed. --> {{Article for deletion/dated|page=Test|timestamp=20260614200246|year=2026|month=June|day=14|substed=yes|help=off}} <!-- Once discussion closed, please place on talk page: {{Old AfD multi|page=Test|date=14 June 2026|result='''keep'''}} --> <!-- End of AfD message, feel free to edit beyond this point --> <noinclude>{{к удалению|2026-06-14}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alzvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nomze completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob enzcomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test.........zzzz https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>.z == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram|P hcaptcha probeolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit test test test test test test == Testing == cross-origin edit == Testing == cross-origin edit ifrbfm1yi23g7d8r19mgtz77a359h5v 747044 747043 2026-06-16T15:13:24Z Pginer-WMF 19605 /* Testing */ 747044 wikitext text/x-wiki <!-- Please do not remove or change this AfD message until the discussion has been closed. --> {{Article for deletion/dated|page=Test|timestamp=20260614200246|year=2026|month=June|day=14|substed=yes|help=off}} <!-- Once discussion closed, please place on talk page: {{Old AfD multi|page=Test|date=14 June 2026|result='''keep'''}} --> <!-- End of AfD message, feel free to edit beyond this point --> <noinclude>{{к удалению|2026-06-14}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alzvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nomze completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob enzcomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test.........zzzz https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>.z == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram|P hcaptcha probeolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit test test test test test test test == Testing == cross-origin edit == Testing == cross-origin edit a7zhwuwjemaxlj8jw2ea1vbrg2fnhc8 747045 747044 2026-06-16T15:13:33Z Pginer-WMF 19605 /* Testing */ 747045 wikitext text/x-wiki <!-- Please do not remove or change this AfD message until the discussion has been closed. --> {{Article for deletion/dated|page=Test|timestamp=20260614200246|year=2026|month=June|day=14|substed=yes|help=off}} <!-- Once discussion closed, please place on talk page: {{Old AfD multi|page=Test|date=14 June 2026|result='''keep'''}} --> <!-- End of AfD message, feel free to edit beyond this point --> <noinclude>{{к удалению|2026-06-14}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alzvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nomze completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob enzcomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test.........zzzz https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>.z == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram|P hcaptcha probeolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit test test test test test test test test == Testing == cross-origin edit == Testing == cross-origin edit 3r92isixm4mrcqof3nkhzycgski0iqf 747046 747045 2026-06-16T15:13:46Z Pginer-WMF 19605 /* Testing */ 747046 wikitext text/x-wiki <!-- Please do not remove or change this AfD message until the discussion has been closed. --> {{Article for deletion/dated|page=Test|timestamp=20260614200246|year=2026|month=June|day=14|substed=yes|help=off}} <!-- Once discussion closed, please place on talk page: {{Old AfD multi|page=Test|date=14 June 2026|result='''keep'''}} --> <!-- End of AfD message, feel free to edit beyond this point --> <noinclude>{{к удалению|2026-06-14}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alzvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nomze completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob enzcomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test.........zzzz https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>.z == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram|P hcaptcha probeolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit == Testing == cross-origin edit == Testing == cross-origin edit sxcew8jhqhoeek9z81oaikz1keahdf8 747047 747046 2026-06-16T15:14:13Z Pginer-WMF 19605 /* Testing */ 747047 wikitext text/x-wiki <!-- Please do not remove or change this AfD message until the discussion has been closed. --> {{Article for deletion/dated|page=Test|timestamp=20260614200246|year=2026|month=June|day=14|substed=yes|help=off}} <!-- Once discussion closed, please place on talk page: {{Old AfD multi|page=Test|date=14 June 2026|result='''keep'''}} --> <!-- End of AfD message, feel free to edit beyond this point --> <noinclude>{{к удалению|2026-06-14}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alzvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nomze completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob enzcomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test.........zzzz https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>.z == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram|P hcaptcha probeolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit test == Testing == cross-origin edit == Testing == cross-origin edit axqiw3qnjo8mbozpjbj8uag1gkvmi8j 747048 747047 2026-06-16T15:14:22Z Pginer-WMF 19605 /* Testing */ 747048 wikitext text/x-wiki <!-- Please do not remove or change this AfD message until the discussion has been closed. --> {{Article for deletion/dated|page=Test|timestamp=20260614200246|year=2026|month=June|day=14|substed=yes|help=off}} <!-- Once discussion closed, please place on talk page: {{Old AfD multi|page=Test|date=14 June 2026|result='''keep'''}} --> <!-- End of AfD message, feel free to edit beyond this point --> <noinclude>{{к удалению|2026-06-14}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alzvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nomze completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob enzcomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test.........zzzz https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>.z == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram|P hcaptcha probeolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit test test == Testing == cross-origin edit == Testing == cross-origin edit 5npx5r3ovgmvn9dn8z9fg021x2nqj7k 747049 747048 2026-06-16T15:14:33Z Pginer-WMF 19605 /* Testing */ 747049 wikitext text/x-wiki <!-- Please do not remove or change this AfD message until the discussion has been closed. --> {{Article for deletion/dated|page=Test|timestamp=20260614200246|year=2026|month=June|day=14|substed=yes|help=off}} <!-- Once discussion closed, please place on talk page: {{Old AfD multi|page=Test|date=14 June 2026|result='''keep'''}} --> <!-- End of AfD message, feel free to edit beyond this point --> <noinclude>{{к удалению|2026-06-14}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alzvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nomze completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob enzcomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test.........zzzz https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>.z == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram|P hcaptcha probeolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit == Testing == cross-origin edit == Testing == cross-origin edit sxcew8jhqhoeek9z81oaikz1keahdf8 747050 747049 2026-06-16T15:15:49Z Pginer-WMF 19605 /* Testing */ 747050 wikitext text/x-wiki <!-- Please do not remove or change this AfD message until the discussion has been closed. --> {{Article for deletion/dated|page=Test|timestamp=20260614200246|year=2026|month=June|day=14|substed=yes|help=off}} <!-- Once discussion closed, please place on talk page: {{Old AfD multi|page=Test|date=14 June 2026|result='''keep'''}} --> <!-- End of AfD message, feel free to edit beyond this point --> <noinclude>{{к удалению|2026-06-14}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alzvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nomze completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob enzcomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test.........zzzz https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>.z == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram|P hcaptcha probeolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit test == Testing == cross-origin edit == Testing == cross-origin edit axqiw3qnjo8mbozpjbj8uag1gkvmi8j 747051 747050 2026-06-16T15:15:58Z Pginer-WMF 19605 /* Testing */ 747051 wikitext text/x-wiki <!-- Please do not remove or change this AfD message until the discussion has been closed. --> {{Article for deletion/dated|page=Test|timestamp=20260614200246|year=2026|month=June|day=14|substed=yes|help=off}} <!-- Once discussion closed, please place on talk page: {{Old AfD multi|page=Test|date=14 June 2026|result='''keep'''}} --> <!-- End of AfD message, feel free to edit beyond this point --> <noinclude>{{к удалению|2026-06-14}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alzvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nomze completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob enzcomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test.........zzzz https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>.z == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram|P hcaptcha probeolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit test test == Testing == cross-origin edit == Testing == cross-origin edit 5npx5r3ovgmvn9dn8z9fg021x2nqj7k 747052 747051 2026-06-16T15:16:07Z Pginer-WMF 19605 /* Testing */ 747052 wikitext text/x-wiki <!-- Please do not remove or change this AfD message until the discussion has been closed. --> {{Article for deletion/dated|page=Test|timestamp=20260614200246|year=2026|month=June|day=14|substed=yes|help=off}} <!-- Once discussion closed, please place on talk page: {{Old AfD multi|page=Test|date=14 June 2026|result='''keep'''}} --> <!-- End of AfD message, feel free to edit beyond this point --> <noinclude>{{к удалению|2026-06-14}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alzvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nomze completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob enzcomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test.........zzzz https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>.z == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram|P hcaptcha probeolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit test test test == Testing == cross-origin edit == Testing == cross-origin edit 0rb0rvwttfsd5ojkp31nevx4mbpeb47 747053 747052 2026-06-16T15:16:16Z Pginer-WMF 19605 /* Testing */ 747053 wikitext text/x-wiki <!-- Please do not remove or change this AfD message until the discussion has been closed. --> {{Article for deletion/dated|page=Test|timestamp=20260614200246|year=2026|month=June|day=14|substed=yes|help=off}} <!-- Once discussion closed, please place on talk page: {{Old AfD multi|page=Test|date=14 June 2026|result='''keep'''}} --> <!-- End of AfD message, feel free to edit beyond this point --> <noinclude>{{к удалению|2026-06-14}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alzvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nomze completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob enzcomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test.........zzzz https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>.z == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram|P hcaptcha probeolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit test test test test == Testing == cross-origin edit == Testing == cross-origin edit 10eeb1ik7akvpd48jm8jerm9kazk2wr 747054 747053 2026-06-16T15:16:28Z Pginer-WMF 19605 /* Testing */ 747054 wikitext text/x-wiki <!-- Please do not remove or change this AfD message until the discussion has been closed. --> {{Article for deletion/dated|page=Test|timestamp=20260614200246|year=2026|month=June|day=14|substed=yes|help=off}} <!-- Once discussion closed, please place on talk page: {{Old AfD multi|page=Test|date=14 June 2026|result='''keep'''}} --> <!-- End of AfD message, feel free to edit beyond this point --> <noinclude>{{к удалению|2026-06-14}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alzvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nomze completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob enzcomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test.........zzzz https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>.z == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram|P hcaptcha probeolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit == Testing == cross-origin edit == Testing == cross-origin edit sxcew8jhqhoeek9z81oaikz1keahdf8 747055 747054 2026-06-16T15:17:22Z Pginer-WMF 19605 /* Testing */ 747055 wikitext text/x-wiki <!-- Please do not remove or change this AfD message until the discussion has been closed. --> {{Article for deletion/dated|page=Test|timestamp=20260614200246|year=2026|month=June|day=14|substed=yes|help=off}} <!-- Once discussion closed, please place on talk page: {{Old AfD multi|page=Test|date=14 June 2026|result='''keep'''}} --> <!-- End of AfD message, feel free to edit beyond this point --> <noinclude>{{к удалению|2026-06-14}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alzvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nomze completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob enzcomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test.........zzzz https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>.z == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram|P hcaptcha probeolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit test == Testing == cross-origin edit == Testing == cross-origin edit axqiw3qnjo8mbozpjbj8uag1gkvmi8j 747056 747055 2026-06-16T15:17:33Z Pginer-WMF 19605 /* Testing */ 747056 wikitext text/x-wiki <!-- Please do not remove or change this AfD message until the discussion has been closed. --> {{Article for deletion/dated|page=Test|timestamp=20260614200246|year=2026|month=June|day=14|substed=yes|help=off}} <!-- Once discussion closed, please place on talk page: {{Old AfD multi|page=Test|date=14 June 2026|result='''keep'''}} --> <!-- End of AfD message, feel free to edit beyond this point --> <noinclude>{{к удалению|2026-06-14}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alzvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nomze completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob enzcomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test.........zzzz https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>.z == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram|P hcaptcha probeolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit test test == Testing == cross-origin edit == Testing == cross-origin edit 5npx5r3ovgmvn9dn8z9fg021x2nqj7k 747057 747056 2026-06-16T15:17:59Z Pginer-WMF 19605 /* Testing */ 747057 wikitext text/x-wiki <!-- Please do not remove or change this AfD message until the discussion has been closed. --> {{Article for deletion/dated|page=Test|timestamp=20260614200246|year=2026|month=June|day=14|substed=yes|help=off}} <!-- Once discussion closed, please place on talk page: {{Old AfD multi|page=Test|date=14 June 2026|result='''keep'''}} --> <!-- End of AfD message, feel free to edit beyond this point --> <noinclude>{{к удалению|2026-06-14}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alzvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nomze completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob enzcomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test.........zzzz https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>.z == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram|P hcaptcha probeolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit test test test == Testing == cross-origin edit == Testing == cross-origin edit 0rb0rvwttfsd5ojkp31nevx4mbpeb47 747058 747057 2026-06-16T15:19:38Z Pginer-WMF 19605 /* Testing */ 747058 wikitext text/x-wiki <!-- Please do not remove or change this AfD message until the discussion has been closed. --> {{Article for deletion/dated|page=Test|timestamp=20260614200246|year=2026|month=June|day=14|substed=yes|help=off}} <!-- Once discussion closed, please place on talk page: {{Old AfD multi|page=Test|date=14 June 2026|result='''keep'''}} --> <!-- End of AfD message, feel free to edit beyond this point --> <noinclude>{{к удалению|2026-06-14}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alzvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nomze completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob enzcomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test.........zzzz https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>.z == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram|P hcaptcha probeolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit == Testing == cross-origin edit == Testing == cross-origin edit sxcew8jhqhoeek9z81oaikz1keahdf8 747059 747058 2026-06-16T15:19:52Z Pginer-WMF 19605 /* Testing */ 747059 wikitext text/x-wiki <!-- Please do not remove or change this AfD message until the discussion has been closed. --> {{Article for deletion/dated|page=Test|timestamp=20260614200246|year=2026|month=June|day=14|substed=yes|help=off}} <!-- Once discussion closed, please place on talk page: {{Old AfD multi|page=Test|date=14 June 2026|result='''keep'''}} --> <!-- End of AfD message, feel free to edit beyond this point --> <noinclude>{{к удалению|2026-06-14}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alzvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nomze completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob enzcomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test.........zzzz https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>.z == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram|P hcaptcha probeolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit test == Testing == cross-origin edit == Testing == cross-origin edit axqiw3qnjo8mbozpjbj8uag1gkvmi8j 747061 747059 2026-06-16T15:22:02Z Pginer-WMF 19605 /* Testing */ 747061 wikitext text/x-wiki <!-- Please do not remove or change this AfD message until the discussion has been closed. --> {{Article for deletion/dated|page=Test|timestamp=20260614200246|year=2026|month=June|day=14|substed=yes|help=off}} <!-- Once discussion closed, please place on talk page: {{Old AfD multi|page=Test|date=14 June 2026|result='''keep'''}} --> <!-- End of AfD message, feel free to edit beyond this point --> <noinclude>{{к удалению|2026-06-14}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alzvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nomze completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob enzcomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test.........zzzz https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>.z == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram|P hcaptcha probeolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit == Testing == cross-origin edit == Testing == cross-origin edit sxcew8jhqhoeek9z81oaikz1keahdf8 747062 747061 2026-06-16T15:23:46Z Pginer-WMF 19605 /* Testing */ 747062 wikitext text/x-wiki <!-- Please do not remove or change this AfD message until the discussion has been closed. --> {{Article for deletion/dated|page=Test|timestamp=20260614200246|year=2026|month=June|day=14|substed=yes|help=off}} <!-- Once discussion closed, please place on talk page: {{Old AfD multi|page=Test|date=14 June 2026|result='''keep'''}} --> <!-- End of AfD message, feel free to edit beyond this point --> <noinclude>{{к удалению|2026-06-14}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alzvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nomze completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob enzcomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test.........zzzz https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>.z == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram|P hcaptcha probeolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit test == Testing == cross-origin edit == Testing == cross-origin edit beditd3f1tsii1zvhx8pcfva95t5ag0 747063 747062 2026-06-16T15:24:07Z Pginer-WMF 19605 /* Testing */ 747063 wikitext text/x-wiki <!-- Please do not remove or change this AfD message until the discussion has been closed. --> {{Article for deletion/dated|page=Test|timestamp=20260614200246|year=2026|month=June|day=14|substed=yes|help=off}} <!-- Once discussion closed, please place on talk page: {{Old AfD multi|page=Test|date=14 June 2026|result='''keep'''}} --> <!-- End of AfD message, feel free to edit beyond this point --> <noinclude>{{к удалению|2026-06-14}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alzvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nomze completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob enzcomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test.........zzzz https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>.z == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram|P hcaptcha probeolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit test test == Testing == cross-origin edit == Testing == cross-origin edit 5npx5r3ovgmvn9dn8z9fg021x2nqj7k 747064 747063 2026-06-16T15:24:17Z Pginer-WMF 19605 /* Testing */ 747064 wikitext text/x-wiki <!-- Please do not remove or change this AfD message until the discussion has been closed. --> {{Article for deletion/dated|page=Test|timestamp=20260614200246|year=2026|month=June|day=14|substed=yes|help=off}} <!-- Once discussion closed, please place on talk page: {{Old AfD multi|page=Test|date=14 June 2026|result='''keep'''}} --> <!-- End of AfD message, feel free to edit beyond this point --> <noinclude>{{к удалению|2026-06-14}}</noinclude> <nowiki>{{Info/Música/artista</nowiki> <nowiki>|</nowiki> ndome = Alzvin L <nowiki>|</nowiki> fundo = cadntodr_solo <nowiki>|</nowiki> imagem = Alvin L.webp <nowiki>|</nowiki> nomze completo = Arnaldo Jose Lima Santos <nowiki>|</nowiki> nascimento_data = {{dni|1|4|1959|si}}d | nascimento_cidade = [[Salvador]], [[Bahia]] | nascimento_país = [[BraTestcomposicoes|titulo=Sob enzcomenda: Alvin L fala sobre suas composições|obra=Portal SUCESSO!|acessodata=19-11-2015|arquivourl=https://web.archive.org/web/20151120161103/http://www.portalsucesso.com.br/noticias/sob-encomenda-alvin-l-fala-sobre-suas-composicoes|arquivodata=2015-11-20|urlmorta=yes}}</ref><ref name=":0">{{Citar web|ultimo=CEL|url=https://celulapop.com.br/o-paradoxo-alvin-l/|titulo=O paradoxo Alvin L|data=2021-09-29|acessodata=2022-04-27|website=Célula POP|lingua=pt-BR}}</ref> ([[Salvador]], [[1 de abril]] de [[1959]] – [[Rio de Janeiro]], [[5 de abril]] de [[2026]]), mais conhecido pelo [[nome artístico]] '''Alvin L''', foi um [[músico]] e [[compositor]] [[brasil]]eiro. Suas mais de 200 composições publicadas foram grdavadas por artistas que vão de [[Milton Nascimento]] a [[Sandy & Junior]]. Test.........zzzz https://example.url https://examples.url https://examplez.url O cantor e compositor morreu no dia 5 de abril de 2026, aos 67 anos, de [[ataque cardíaco]] enquanto dormia.<ref>{{citar web|url=https://oglobo.globo.com/cultura/noticia/2026/04/05/morre-compositor-alvim-l-aos-67-anos.ghtml|titulo=Morre compositor Alvim L, autor de hits da música brasileira, aos 67 anos|data=05/04/2026|acessodata=05/04/2026}}</ref>.z == Carreira == Alvin nasceu em Salvador, mas foi registrado no [[Rio de Janeiro]].<ref>{{Citar web|ultimo=Schmidt|primeiro=Bernardo|url=http://bernardoschmidt.blogspot.com/2010/10/entrevista-com-alvin-l-parte-1.html|titulo=O Patativa: Entrevista com ALVIN L - Parte 1|data= 25 de outubro de 2010|acessodata=2022-04-27|website=O Patativa}}</ref> [[Guitarrista]] e compositor, começou no final dos [[Década de 1970|anos 70]] com o [[Banda musical|grupo]] [[punk]] Vândalos. Mais tarde formou os [[Rapazes de Vida Fácil]], mais influenciado pelo [[New wave (música)|new wave]], que chegou a lançar um [[Compacto simples|compacto]] em 1982 pela [[PolyGram|P hcaptcha probeolyGram]] e teve sua música de maior sucesso "Adriana na Piscina".<ref name=":0" /> Também flertou com o experimentalismo com a banda Brasil Palace. NOrmal edit Em seguida foi oo compositor principal dos Sex Beatles, formanda em 1990,<ref name=":0" /> que lançou dois discos, ''Automobília'' e ''Mondo Passionale'', nos [[Década de 1990|anos 1990]].<ref name=":1">{{Citar web|url=https://www1.folha.uol.com.br/fsp/ilustrad/fq220816.htm|titulo=Folha de S.Paulo - Disco: Alvin L., 35, chega ao trabalho solo - 22/08/97|acessodata=2022-04-27|website=www1.folha.uol.com.br}}</ref> O grupo trazia, além dele, na gu testitarra, a vocalista [[Cris Braun]], o guitarrista Ivan Mariz, o baixista Vicente Tardin e o baterista Marcelo Martins. [[Dado Villa-Lobos]], na test2 época ainda integrando a [[Legião Urbana]], chegou a tocar com os Sex Beatles, mas, para não ferir cláusulas contratuais, não mostrava o rosto e chegou a usar um [[Pseudónimo|pseudônimo]] quando tocou com a banda em apenas um show, no [[Circo Voador]].<ref name=":0" /> Durante todo o tempo, Alvin destacou-se como compositor, sendo gravado por outros intérpretes, principalmente [[Marina Lima]]<ref>{{Citar web|url=https://g1.globo.com/pop-arte/musica/blog/mauro-ferreira/post/2021/03/28/marina-lima-canta-com-mano-brown-em-ep-no-qual-reforca-parceria-com-alvin-l.ghtml|titulo=Marina Lima canta com Mano Brown em EP no qual reforça parceria com Alvin L|acessodata=2022-04-27|website=G1|lingua=pt-br}}</ref> ("Eu Não Sei Dançar",<ref name=":1" /><ref name=":0" /><ref name=":2">{{Citar web|url=http://screamyell.com.br/site/2020/06/22/entrevista-alvin-l-lanca-seu-primeiro-livro-o-veneno-dos-pequenos-detalhes/|titulo=Entrevista: Alvin L lança seu primeiro livro, “O Veneno dos Pequenos Detalhes” – SCREAM & YELL|acessodata=2022-04-27|lingua=pt-BR|wayb=20240919221805}}</ref> "Stromboli", "Deve Ser Assim", "Na Minha Mão" e outros), [[Capital Inicial]] ("Natasha", "Mickey Mouse em Moscou", "Todos os Lados", "Eu Vou Estar", "Tudo que Vai" e outros<ref name=":0" /><ref name=":2" />), [[Leila Pinheiro]] (que registrou três músicas suas em ''[[Na Ponta da Língua]]''), e ainda [[Belô Velloso]] ("Menos Carnaval"), [[Toni Platão]] ("Tudo que Vai") e [[Ana Carolina (cantora)|Ana Carolina]] ("Perder Tempo com Você"). Seu primeiro disco solo, ''Alvin'', saiu em 1997<ref name=":2" /> pela [[Bertelsmann Music Group|BMG]] com a produção de [[Liminha (produtor musical)|Liminha]],<ref name=":0" /><ref>{{Citar web|url=http://www.liminha.com.br/en/projeto/alvin/|titulo=Alvin|acessodata=2022-04-27|website=Liminha|wayb=20190218165613}}</ref> contendo regravações e inéditas.<ref name=":1" /><ref>{{citar web|url=http://www.dicionariompb.com.br/alvin-l |titulo=Biografia no Cravo Albin|obra=[[Dicionário Cravo Albin da Música Popular Brasileira|dicionariompb.com.br]]|acessodata=19-12-2012}}</ref> Em 2020, lança seu primeiro [[livro]], o [[suspense]] ''O Veneno dos Pequenos Detalhes''.<ref name=":2" /> Em 2021, participa cantando na música "Kilimanjaro", do [[EP]] ''Motim'' de Marina Lima.<ref>{{Citar web|url=https://www.cartacapital.com.br/cultura/com-horror-a-bolsonaro-marina-lima-lanca-ep-e-deseja-deixar-sp/|titulo=Com “horror a Bolsonaro”, Marina Lima lança EP e deseja deixar SP - CartaCapital|acessodata=2022-04-27|website=www.cartacapital.com.br}}</ref> == Discografia == * [[1997]] - ''Alvin'' {{referências}} == Ligações externas == * {{discogs artist|Alvin L.}} * {{IMDb name|15852552}} {{NM|1959|2026}} [[Categoria:Cantores da Bahia]] [[Categoria:Guitarristas da Bahia]] [[Categoria:Guitarristas rítmicos]] [[Categoria:Compositores da Bahia]] [[Categoria:Naturais de Salvador]] [[Category:EA]]Test append == Testing == cross-origin edit == Testing == cross-origin edit == Testing == cross-origin edit sxcew8jhqhoeek9z81oaikz1keahdf8 Kani 0 155125 747068 644120 2026-06-16T16:49:31Z ~2026-35333-53 74472 747068 wikitext text/x-wiki '''Kani''' nwere ike na-ezo aka: we have fun while testing 87kgqtgldi5aw51ipm8t9u6iicu3abg MwclientTestPage NonExistent 1751014591 0 166797 747069 741548 2026-06-16T16:50:20Z ~2026-35154-46 74473 747069 wikitext text/x-wiki This is an updated test page Normal edit [https://jsdfklsdjlf.com] Af edit Ad edit 92uerpz1j9tw3atch6aealq33ue537e MediaWiki:Gadget-twinkletag.js 8 167859 747132 726160 2026-06-16T19:15:13Z SD0001 26892 Repo at 46e08aa: 747132 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: 'checkbox', list: [ { label: 'Group inside {{multiple issues}} if possible', value: 'group', name: 'group', tooltip: 'If applying two or more templates supported by {{multiple issues}} and this box is checked, all supported templates will be grouped inside a {{multiple issues}} template.', checked: Twinkle.getPref('groupByDefault') } ] }); 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, // except when they are within {{multiple issues}} $('.mw-parser-output').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 (params.group && !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> jdfo5llhoi4ra468zotcepajvf57d18 User:ARI/common.js 2 168490 747141 746902 2026-06-16T23:05:29Z ARI 65433 747141 javascript text/javascript // ==UserScript== // @name Wikipedia Duplicate Line Cleaner // @namespace http://tampermonkey.net/ // @version 1.0 // @description Find and remove duplicate lines in Wikipedia articles // @author Your Name // @match *://*.wikipedia.org/* // @match *://*.wikimedia.org/* // @grant none // ==/UserScript== (function() { 'use strict'; // Store raw content and processed lines let originalContent = ''; let allLines = []; let uniqueLines = []; let currentTitle = ''; let currentProject = ''; // ---------- UI Creation ---------- function createUI() { const container = document.createElement('div'); container.id = 'duplicate-finder-container'; container.style.cssText = ` position: fixed; top: 10px; right: 10px; width: 400px; max-height: 80vh; background: white; border: 1px solid #ccc; border-radius: 8px; padding: 16px; z-index: 1000; box-shadow: 0 4px 6px rgba(0,0,0,0.1); overflow-y: auto; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; `; container.innerHTML = ` <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;"> <h3 style="margin: 0; font-size: 16px; font-weight: 600;">Duplicate Line Cleaner</h3> <button id="close-duplicate-finder" style="background: none; border: none; font-size: 20px; cursor: pointer; color: #666;">×</button> </div> <div style="margin-bottom: 12px;"> <input type="text" id="article-title-input" placeholder="Enter article title..." style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; margin-bottom: 8px;"> <input type="text" id="project-input" placeholder="Project domain (e.g., en.wikipedia.org)" style="width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; margin-bottom: 8px;"> <button id="check-duplicates-btn" style="width: 100%; padding: 8px; background: #3366cc; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: 500;">Check for Duplicates</button> </div> <div id="duplicate-output" style="margin-top: 12px;"></div> `; document.body.appendChild(container); // Close button document.getElementById('close-duplicate-finder').addEventListener('click', function() { container.style.display = 'none'; }); // Check button document.getElementById('check-duplicates-btn').addEventListener('click', checkArticle); // Enter key support document.getElementById('article-title-input').addEventListener('keypress', function(e) { if (e.key === 'Enter') checkArticle(); }); document.getElementById('project-input').addEventListener('keypress', function(e) { if (e.key === 'Enter') checkArticle(); }); // Pre-fill with current page info const currentTitleEl = document.querySelector('h1#firstHeading'); if (currentTitleEl) { document.getElementById('article-title-input').value = currentTitleEl.textContent.trim(); } document.getElementById('project-input').value = window.location.hostname; } // ---------- Main Check Function ---------- async function checkArticle() { const title = document.getElementById('article-title-input').value.trim(); let project = document.getElementById('project-input').value.trim(); const output = document.getElementById('duplicate-output'); if (!title || !project) { showError("Please enter both a project domain and an article title.", output); return; } project = project.replace(/^https?:\/\//, "").replace(/\/$/, ""); currentTitle = title; currentProject = project; // Loading state output.innerHTML = ` <div style="background: #f8f9fa; padding: 16px; border-radius: 6px; text-align: center; color: #6c757d;"> <div style="display: inline-block; width: 20px; height: 20px; border: 3px solid #f3f3f3; border-top: 3px solid #3366cc; border-radius: 50%; animation: spin 1s linear infinite; margin-bottom: 8px;"></div> <p style="margin: 0; font-weight: 500;">Fetching article content...</p> </div> <style> @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } </style> `; try { // Fetch raw wikitext const url = `https://${project}/w/api.php?action=query&prop=revisions&rvprop=content&titles=${encodeURIComponent(title)}&format=json&origin=*`; const response = await fetch(url); if (!response.ok) throw new Error(`Network error: ${response.statusText}`); const data = await response.json(); const pages = data.query.pages; const pageId = Object.keys(pages)[0]; const page = pages[pageId]; if (pageId === "-1" || page.missing !== undefined) { showError(`Page not found on <strong>${project}</strong>. Please check the title.`, output); return; } const content = page.revisions?.[0]?.['*']; if (!content) { showError("No content found for this page.", output); return; } originalContent = content; allLines = content.split('\n'); const trimmedLines = allLines.map(line => line.trim()); // Filter out very short lines for duplicate detection const lineMap = new Map(); // trimmed -> first index const duplicateIndices = new Set(); trimmedLines.forEach((trimmed, index) => { if (trimmed.length <= 5) return; // skip short lines if (lineMap.has(trimmed)) { duplicateIndices.add(index); } else { lineMap.set(trimmed, index); } }); // Build unique lines (keep first occurrence) const seen = new Set(); uniqueLines = []; allLines.forEach((line, idx) => { const trimmed = line.trim(); if (trimmed.length > 5 && !seen.has(trimmed)) { seen.add(trimmed); uniqueLines.push(line); } else if (trimmed.length <= 5) { // Always keep short lines (they are not considered duplicates) uniqueLines.push(line); } // If duplicate, we skip adding it to uniqueLines }); // Prepare display let htmlContent = ''; let duplicateCount = 0; const duplicateSet = new Set(); allLines.forEach((line, idx) => { const trimmed = line.trim(); if (trimmed.length > 5 && lineMap.has(trimmed) && lineMap.get(trimmed) !== idx) { // This is a duplicate (not the first occurrence) duplicateCount++; duplicateSet.add(trimmed); htmlContent += `<div style="background: #fee; border-left: 4px solid #dc3545; padding: 8px; margin: 4px 0; border-radius: 0 4px 4px 0; color: #721c24;">${escapeHtml(line)}</div>`; } else { htmlContent += `<div style="padding: 8px; margin: 4px 0; border-left: 4px solid transparent; color: #333;">${escapeHtml(line)}</div>`; } }); let summary; if (duplicateCount > 0) { summary = `<h4 style="color: #dc3545; margin: 0 0 12px 0;">Found ${duplicateCount} duplicate line(s)</h4> <button id="clear-duplicates-btn" style="padding: 6px 12px; background: #dc3545; color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: 500; margin-bottom: 10px;">🗑️ Remove Duplicates</button> <div style="font-size: 13px; color: #666; margin-bottom: 8px;">Clicking will edit the page and refresh.</div>`; } else { summary = `<h4 style="color: #28a745; margin: 0 0 12px 0;">✅ No duplicate lines found!</h4>`; } output.innerHTML = ` <div style="background: #f8f9fa; padding: 12px; border-radius: 6px;"> ${summary} <div style="max-height: 300px; overflow-y: auto; font-size: 13px; padding-right: 4px;">${htmlContent}</div> </div>`; // Attach clear button event if it exists const clearBtn = document.getElementById('clear-duplicates-btn'); if (clearBtn) { clearBtn.addEventListener('click', clearDuplicates); } } catch (err) { console.error(err); showError("An error occurred while fetching the data. Please check the domain and your connection.", output); } } // ---------- Clear Duplicates (Edit Page) ---------- async function clearDuplicates() { const output = document.getElementById('duplicate-output'); const clearBtn = document.getElementById('clear-duplicates-btn'); if (clearBtn) clearBtn.disabled = true; // Confirm action if (!confirm('This will edit the page to remove duplicate lines. Continue?')) { if (clearBtn) clearBtn.disabled = false; return; } // Show loading output.innerHTML = ` <div style="background: #f8f9fa; padding: 16px; border-radius: 6px; text-align: center; color: #6c757d;"> <div style="display: inline-block; width: 20px; height: 20px; border: 3px solid #f3f3f3; border-top: 3px solid #28a745; border-radius: 50%; animation: spin 1s linear infinite; margin-bottom: 8px;"></div> <p style="margin: 0; font-weight: 500;">Editing page...</p> </div> `; try { // Get edit token const tokenUrl = `https://${currentProject}/w/api.php?action=query&meta=tokens&format=json`; const tokenResp = await fetch(tokenUrl, { credentials: 'include' }); if (!tokenResp.ok) throw new Error('Failed to fetch edit token'); const tokenData = await tokenResp.json(); const token = tokenData.query?.tokens?.csrftoken; if (!token) throw new Error('Could not retrieve CSRF token. Are you logged in?'); // Get base timestamp (latest revision timestamp) for edit conflict handling const infoUrl = `https://${currentProject}/w/api.php?action=query&prop=revisions&rvprop=timestamp&titles=${encodeURIComponent(currentTitle)}&format=json`; const infoResp = await fetch(infoUrl, { credentials: 'include' }); if (!infoResp.ok) throw new Error('Failed to get page info'); const infoData = await infoResp.json(); const pageInfo = Object.values(infoData.query.pages)[0]; const baseTimestamp = pageInfo.revisions?.[0]?.timestamp; if (!baseTimestamp) throw new Error('Could not determine base timestamp'); // Build new content const newContent = uniqueLines.join('\n'); // Perform edit const editUrl = `https://${currentProject}/w/api.php?action=edit&format=json`; const formData = new FormData(); formData.append('title', currentTitle); formData.append('text', newContent); formData.append('token', token); formData.append('summary', 'Removed duplicate lines using userscript'); formData.append('minor', 'true'); formData.append('basetimestamp', baseTimestamp); const editResp = await fetch(editUrl, { method: 'POST', body: formData, credentials: 'include' }); const editData = await editResp.json(); if (editData.edit?.result === 'Success') { // Success: reload page output.innerHTML = `<div style="background: #d4edda; border-left: 4px solid #28a745; padding: 12px; border-radius: 0 4px 4px 0; color: #155724;">✅ Page updated successfully! Refreshing...</div>`; setTimeout(() => window.location.reload(), 1500); } else { const errorMsg = editData.edit?.error || editData.error?.info || 'Unknown edit error'; throw new Error(errorMsg); } } catch (err) { console.error(err); showError(`Edit failed: ${err.message}`, output); if (clearBtn) clearBtn.disabled = false; } } // ---------- Helpers ---------- function showError(message, output) { output.innerHTML = ` <div style="background: #f8d7da; border-left: 4px solid #dc3545; padding: 12px; border-radius: 0 4px 4px 0; color: #721c24;"> <p style="margin: 0; font-weight: 600;">Error</p> <p style="margin: 4px 0 0 0;">${message}</p> </div>`; } function escapeHtml(unsafe) { return unsafe .replace(/&/g, "&amp;") .replace(/</g, "&lt;") .replace(/>/g, "&gt;") .replace(/"/g, "&quot;") .replace(/'/g, "&#039;"); } // ---------- Initialize ---------- if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', createUI); } else { createUI(); } })(); n8ibkw52kzct390nvx7ql15laa6ygom 747143 747141 2026-06-17T00:20:56Z ARI 65433 747143 javascript text/javascript (function (mw, $) { 'use strict'; const SPECIAL_PAGE_NAME = 'বিশেষ:খালি_পাতা/DupCheck'; // ========================================== // INITIALIZATION (The "Classic" Way) // ========================================== function init() { // 1. Add the link to the article toolbar mw.loader.using(['mediawiki.util'], function () { // Only show the tab if we are on a normal page or the tool page itself if (mw.config.get('wgNamespaceNumber') >= 0 || mw.config.get('wgPageName') === SPECIAL_PAGE_NAME) { mw.util.addPortletLink( 'p-views', // 'p-views' puts it in the top toolbar. mw.util.getUrl(SPECIAL_PAGE_NAME), 'DupCheck', 'ca-dupcheck', 'Check this article for duplicate content' ); } }); // 2. If we are actually ON the special page, load the tool interface if (mw.config.get('wgPageName') === SPECIAL_PAGE_NAME) { mw.loader.using([ 'oojs-ui-core', 'oojs-ui-widgets', 'mediawiki.api', 'mediawiki.util' ], loadTool); } } // ========================================== // CORE TOOL LOGIC // ========================================== const State = { title: '', originalWikitext: '', modifiedWikitext: '', baseRevId: null, results: {}, removals: [] }; const Config = { minParagraphLength: 50, minLineLength: 5, projectName: mw.config.get('wgSiteName'), excludedRegexes: [ /\[\[(Category|বিষয়শ্রেণী):.+?\]\]/gi, /\{\{DEFAULTSORT:.+?\}\}/gi, /\{\{(Reflist|Authority control|Commons category|Use dmy dates|Use Indian English)[^\}]*\}\}/gi, /\[\[(en|hi|ur|ta|te|ml|bn|fr|de|es):.+?\]\]/gi ], excludedPrefixes: ['|-', '|+', '|}', '{|', '|', '!'], isExcluded: function (text) { let normalized = text.trim(); if (!normalized) return true; if (this.excludedPrefixes.some(prefix => normalized.startsWith(prefix))) return true; if (/^\{\{.+?\}\}$/.test(normalized)) return true; for (let regex of this.excludedRegexes) { if (regex.test(normalized)) return true; } return false; } }; const Analyzer = { run: function (wikitext) { this.results = { SEC: { id: 'SEC', name: 'Duplicate Section Titles', icon: '🔴', findings: [] }, SUB: { id: 'SUB', name: 'Duplicate Subsection Titles', icon: '🟠', findings: [] }, PAR: { id: 'PAR', name: 'Duplicate Paragraphs', icon: '🟡', findings: [] }, LIN: { id: 'LIN', name: 'Duplicate Lines', icon: '🟢', findings: [] }, LST: { id: 'LST', name: 'Duplicate List Items', icon: '🔵', findings: [] } }; this.analyzeHeadings(wikitext); this.analyzeBlocks(wikitext); this.analyzeLines(wikitext); this.analyzeLists(wikitext); return this.results; }, normalize: function (text) { return text.trim().replace(/[ \t]+/g, ' '); }, extractMatches: function (regex, text, categoryObj, minLength, isContentGroup = 1) { const map = new Map(); let match; regex.lastIndex = 0; while ((match = regex.exec(text)) !== null) { const rawText = match[0]; const contentText = match[isContentGroup] || rawText; const normalized = Analyzer.normalize(contentText); if (normalized.length < minLength || Config.isExcluded(normalized)) continue; if (!map.has(normalized)) map.set(normalized, []); map.get(normalized).push({ start: match.index, length: rawText.length, raw: rawText, preview: normalized }); } for (const [normText, instances] of map.entries()) { if (instances.length > 1) categoryObj.findings.push({ text: normText, instances: instances }); } }, analyzeHeadings: function (wikitext) { this.extractMatches(/^(={2})\s*(.+?)\s*\1\s*$/gm, wikitext, this.results.SEC, 1, 2); this.extractMatches(/^(={3,6})\s*(.+?)\s*\1\s*$/gm, wikitext, this.results.SUB, 1, 2); }, analyzeBlocks: function (wikitext) { this.extractMatches(/(?:^|\n\n)([^\n][\s\S]*?)(?=\n\n|$)/g, wikitext, this.results.PAR, Config.minParagraphLength, 1); }, analyzeLines: function (wikitext) { this.extractMatches(/^(.+)$/gm, wikitext, this.results.LIN, Config.minLineLength, 1); }, analyzeLists: function (wikitext) { this.extractMatches(/^([\*\#]+)\s*(.+)$/gm, wikitext, this.results.LST, 2, 2); } }; const ApiService = { api: null, init: function() { this.api = new mw.Api(); }, fetchArticle: async function (title) { const response = await this.api.get({ action: 'query', prop: 'revisions', rvprop: 'content|ids', rvslots: 'main', titles: title, formatversion: 2 }); const page = response.query.pages[0]; if (page.missing) throw new Error("Article does not exist."); return { wikitext: page.revisions[0].slots.main.content, revid: page.revisions[0].revid }; }, getDiff: async function (title, baseRevId, modifiedText) { const response = await this.api.post({ action: 'compare', fromtitle: title, fromrev: baseRevId, totext: modifiedText, prop: 'diff', formatversion: 2 }); return response.compare.body; }, saveEdit: async function (title, baseRevId, modifiedText) { await this.api.postWithToken('csrf', { action: 'edit', title: title, text: modifiedText, summary: 'DupCheck: Reviewed and removed duplicate content', baserevid: baseRevId, nocreate: true, formatversion: 2 }); } }; const UI = { renderBase: function () { document.title = 'DupCheck - Article Review'; $('#firstHeading').text('DupCheck'); const html = ` <div id="dupcheck-container"> <div class="dc-header"> <p>Analyze wiki articles for duplicate headings, paragraphs, and lists without opening edit mode.</p> <div class="dc-controls"> <input type="text" id="dc-title-input" placeholder="Enter article title (e.g. বাংলা ভাষা)" /> <button id="dc-btn-analyze">Analyze</button> <button id="dc-btn-reset" class="secondary">Reset</button> </div> <div id="dc-status"></div> </div> <div id="dc-results-area" style="display:none;"></div> <div id="dc-preview-area" style="display:none;"> <h2>Preview Changes</h2> <div id="dc-diff-table" class="diff"></div> <div class="dc-action-bar"> <button id="dc-btn-back" class="secondary">Back to Review</button> <button id="dc-btn-save" class="primary">Save Changes</button> </div> </div> </div> `; $('#mw-content-text').empty().html(html); this.injectStyles(); this.bindEvents(); }, injectStyles: function () { if ($('#dc-styles').length) return; $('<style id="dc-styles">').text(` #dupcheck-container { max-width: 900px; margin: 0 auto; font-family: sans-serif; color: #202122; } .dc-header { background: #f8f9fa; padding: 20px; border-radius: 4px; border: 1px solid #eaecf0; margin-bottom: 20px;} .dc-controls { display: flex; gap: 10px; margin-top: 15px; flex-wrap: wrap; } #dc-title-input { flex: 1; min-width: 200px; padding: 8px 12px; border: 1px solid #a2a9b1; border-radius: 2px; font-size: 14px;} #dupcheck-container button { padding: 8px 16px; border-radius: 2px; border: 1px solid #a2a9b1; background: #f8f9fa; cursor: pointer; font-weight: bold; color: #202122; } #dupcheck-container button:hover { background: #fff; } #dupcheck-container button.primary { background: #36c; color: white; border-color: #36c; } #dupcheck-container button.primary:hover { background: #447ff5; } #dc-status { margin-top: 15px; font-size: 14px; font-weight: bold; } .dc-category { border: 1px solid #eaecf0; margin-bottom: 15px; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.05); } .dc-category-header { background: #eaecf0; padding: 10px 15px; font-weight: bold; font-size: 16px; } .dc-finding { padding: 15px; border-top: 1px solid #eaecf0; } .dc-finding-text { background: #fdfdfd; padding: 10px; border-left: 4px solid #c8ccd1; margin-bottom: 10px; font-family: monospace; white-space: pre-wrap; font-size: 13px;} .dc-instances { display: flex; flex-direction: column; gap: 8px; } .dc-instance-row { display: flex; align-items: center; gap: 10px; background: #fff; padding: 8px; border: 1px solid #eaecf0; border-radius: 2px; } .dc-instance-row.original { background: #eaf3ff; border-color: #c8ccd1; } #dc-action-footer { margin-top: 20px; padding: 20px; text-align: right; background: #f8f9fa; border: 1px solid #eaecf0; } .dc-action-bar { margin-top: 20px; text-align: right; } .loading { color: #36c; } .error { color: #d33; } .success { color: #00af89; } @media (max-width: 600px) { .dc-controls { flex-direction: column; } #dupcheck-container button { width: 100%; } } `).appendTo('head'); }, bindEvents: function () { $('#dc-btn-analyze').on('click', Controller.handleAnalyze); $('#dc-btn-reset').on('click', () => location.reload()); $('#dupcheck-container').on('click', '#dc-btn-preview', Controller.handlePreview); $('#dupcheck-container').on('click', '#dc-btn-back', () => { $('#dc-preview-area').hide(); $('#dc-results-area').show(); }); $('#dupcheck-container').on('click', '#dc-btn-save', Controller.handleSave); }, setStatus: function (msg, type = 'loading') { $('#dc-status').html(`<span class="${type}">${msg}</span>`); }, renderResults: function (resultsObj, title, revId) { let html = `<h2>Analysis Report: <a href="/wiki/${encodeURIComponent(title)}" target="_blank">${title}</a> (Rev: ${revId})</h2>`; let totalFindings = 0; for (const key in resultsObj) { const cat = resultsObj[key]; if (cat.findings.length === 0) continue; totalFindings += cat.findings.length; html += `<div class="dc-category"><div class="dc-category-header">${cat.icon} ${cat.name} (${cat.findings.length})</div>`; cat.findings.forEach((finding, fIndex) => { html += `<div class="dc-finding"><div class="dc-finding-text">${mw.html.escape(finding.text)}</div><div class="dc-instances">`; finding.instances.forEach((inst, iIndex) => { const isOriginal = (iIndex === 0); const rowClass = isOriginal ? 'dc-instance-row original' : 'dc-instance-row'; const label = isOriginal ? `Occurrence 1 (Preserved)` : `Occurrence ${iIndex + 1}`; const checkedStr = isOriginal ? 'disabled' : 'checked'; html += ` <label class="${rowClass}"> <input type="checkbox" class="dc-chk-remove" data-start="${inst.start}" data-length="${inst.length}" ${checkedStr} /> <span><strong>${label}</strong> - Char offset: ${inst.start}</span> </label>`; }); html += `</div></div>`; }); html += `</div>`; } if (totalFindings === 0) { html += `<div class="dc-category"><div class="dc-finding">No duplicates found! Great job!</div></div>`; } else { html += `<div id="dc-action-footer"><button id="dc-btn-preview" class="primary">Review Selected Removals & Preview</button></div>`; } $('#dc-results-area').html(html).show(); $('#dc-preview-area').hide(); }, renderDiff: function (diffHtml) { $('#dc-results-area').hide(); $('#dc-diff-table').html(` <table class="diff" style="width:100%; border-collapse:collapse;"> <colgroup><col class="diff-marker"><col class="diff-content"><col class="diff-marker"><col class="diff-content"></colgroup> <tbody>${diffHtml || '<tr><td colspan="4" style="text-align:center; padding: 20px;">No actual changes detected.</td></tr>'}</tbody> </table> `); $('#dc-preview-area').show(); } }; const Controller = { handleAnalyze: async function () { const title = $('#dc-title-input').val().trim(); if (!title) return UI.setStatus('Please enter an article title.', 'error'); UI.setStatus(`Fetching wikitext for "${title}"...`, 'loading'); $('#dc-btn-analyze').prop('disabled', true); $('#dc-results-area').hide(); try { const data = await ApiService.fetchArticle(title); State.title = title; State.originalWikitext = data.wikitext; State.baseRevId = data.revid; UI.setStatus('Analyzing wikitext for duplicates...', 'loading'); State.results = Analyzer.run(State.originalWikitext); UI.setStatus('Analysis complete.', 'success'); UI.renderResults(State.results, State.title, State.baseRevId); } catch (err) { UI.setStatus(`Error: ${err.message}`, 'error'); } finally { $('#dc-btn-analyze').prop('disabled', false); } }, handlePreview: async function () { State.removals = []; $('.dc-chk-remove:checked:not(:disabled)').each(function () { State.removals.push({ start: parseInt($(this).attr('data-start')), length: parseInt($(this).attr('data-length')) }); }); if (State.removals.length === 0) return alert("No duplicate occurrences selected for removal."); UI.setStatus('Generating preview...', 'loading'); $('#dc-btn-preview').prop('disabled', true); State.removals.sort((a, b) => b.start - a.start); let modText = State.originalWikitext; State.removals.forEach(rem => { const before = modText.slice(0, rem.start); let after = modText.slice(rem.start + rem.length); after = after.replace(/^\r?\n/, ''); modText = before + after; }); State.modifiedWikitext = modText; try { const diffHtml = await ApiService.getDiff(State.title, State.baseRevId, State.modifiedWikitext); mw.loader.load('mediawiki.diff.styles'); UI.renderDiff(diffHtml); UI.setStatus('Review your changes below.', 'success'); } catch (err) { UI.setStatus(`Preview Error: ${err.message}`, 'error'); } finally { $('#dc-btn-preview').prop('disabled', false); } }, handleSave: async function () { if (!confirm(`Are you sure you want to publish these changes to "${State.title}"?`)) return; UI.setStatus('Saving changes...', 'loading'); $('#dc-btn-save').prop('disabled', true); try { await ApiService.saveEdit(State.title, State.baseRevId, State.modifiedWikitext); UI.setStatus('Changes saved successfully! Redirecting...', 'success'); setTimeout(() => { window.location.href = mw.util.getUrl(State.title); }, 1500); } catch (err) { UI.setStatus(`Save Error: ${err.message}. Conflict or API failure.`, 'error'); $('#dc-btn-save').prop('disabled', false); } } }; // Called when on the special page function loadTool() { ApiService.init(); UI.renderBase(); } // Fire the initialization on document ready $(document).ready(init); })(mediaWiki, jQuery); 7eis86kf2rbgyo0fza66dhvo94ft1yv 747144 747143 2026-06-17T00:25:32Z ARI 65433 747144 javascript text/javascript (function (mw, $) { 'use strict'; const SPECIAL_PAGE_NAME = 'বিশেষ:খালি_পাতা/DupCheck'; // ========================================== // 1. INITIALIZATION & TOOLBAR LINK // ========================================== function init() { // Add link to the Tools (সরঞ্জাম) sidebar ('p-tb') mw.loader.using(['mediawiki.util'], function () { // If the user is on a standard article, pass the title to the tool automatically let targetUrl = mw.util.getUrl(SPECIAL_PAGE_NAME); if (mw.config.get('wgNamespaceNumber') === 0) { targetUrl += '?dctitle=' + encodeURIComponent(mw.config.get('wgPageName')); } mw.util.addPortletLink( 'p-tb', // Target the sidebar tools menu targetUrl, 'DupCheck (Duplicate Finder)', 't-dupcheck', 'Find and remove duplicate content in this article' ); }); // Load the tool UI only if we are on the Special Page if (mw.config.get('wgPageName') === SPECIAL_PAGE_NAME) { mw.loader.using([ 'oojs-ui-core', 'oojs-ui-widgets', 'mediawiki.api', 'mediawiki.util', 'mediawiki.diff.styles' ], loadTool); } } // ========================================== // 2. CORE TOOL SETTINGS & LOGIC // ========================================== const State = { title: '', originalWikitext: '', modifiedWikitext: '', baseRevId: null, results: {}, removals: [] }; const Config = { minParagraphLength: 50, minLineLength: 5, excludedRegexes: [ /\[\[(Category|বিষয়শ্রেণী):.+?\]\]/gi, /\{\{DEFAULTSORT:.+?\}\}/gi, /\{\{(Reflist|Authority control|Commons category|Use dmy dates)[^\}]*\}\}/gi, /\[\[(en|hi|ur|ta|te|ml|bn|fr|de|es):.+?\]\]/gi ], excludedPrefixes: ['|-', '|+', '|}', '{|', '|', '!'], isExcluded: function (text) { let normalized = text.trim(); if (!normalized) return true; if (this.excludedPrefixes.some(p => normalized.startsWith(p))) return true; if (/^\{\{.+?\}\}$/.test(normalized)) return true; for (let regex of this.excludedRegexes) { if (regex.test(normalized)) return true; } return false; } }; const Analyzer = { run: function (wikitext) { this.results = { SEC: { name: 'Duplicate Headings', icon: '📝', findings: [] }, PAR: { name: 'Duplicate Paragraphs', icon: '📄', findings: [] }, LIN: { name: 'Duplicate Lines', icon: '➖', findings: [] }, LST: { name: 'Duplicate List Items', icon: '📌', findings: [] } }; this.extractMatches(/^(={2,6})\s*(.+?)\s*\1\s*$/gm, wikitext, this.results.SEC, 1, 2); this.extractMatches(/(?:^|\n\n)([^\n][\s\S]*?)(?=\n\n|$)/g, wikitext, this.results.PAR, Config.minParagraphLength, 1); this.extractMatches(/^(.+)$/gm, wikitext, this.results.LIN, Config.minLineLength, 1); this.extractMatches(/^([\*\#]+)\s*(.+)$/gm, wikitext, this.results.LST, 2, 2); return this.results; }, normalize: function (text) { return text.trim().replace(/[ \t]+/g, ' '); }, extractMatches: function (regex, text, categoryObj, minLength, isContentGroup = 1) { const map = new Map(); let match; regex.lastIndex = 0; while ((match = regex.exec(text)) !== null) { const rawText = match[0]; const contentText = match[isContentGroup] || rawText; const normalized = Analyzer.normalize(contentText); if (normalized.length < minLength || Config.isExcluded(normalized)) continue; if (!map.has(normalized)) map.set(normalized, []); map.get(normalized).push({ start: match.index, length: rawText.length, preview: normalized }); } for (const [normText, instances] of map.entries()) { if (instances.length > 1) categoryObj.findings.push({ text: normText, instances: instances }); } } }; const ApiService = { api: null, init: function() { this.api = new mw.Api(); }, fetchArticle: async function (title) { const response = await this.api.get({ action: 'query', prop: 'revisions', rvprop: 'content|ids', rvslots: 'main', titles: title, formatversion: 2 }); if (response.query.pages[0].missing) throw new Error("Article not found."); return { wikitext: response.query.pages[0].revisions[0].slots.main.content, revid: response.query.pages[0].revisions[0].revid }; }, getDiff: async function (title, baseRevId, modifiedText) { const response = await this.api.post({ action: 'compare', fromtitle: title, fromrev: baseRevId, totext: modifiedText, prop: 'diff', formatversion: 2 }); return response.compare.body; }, saveEdit: async function (title, baseRevId, modifiedText) { await this.api.postWithToken('csrf', { action: 'edit', title: title, text: modifiedText, summary: 'DupCheck: Removed duplicate content', baserevid: baseRevId, nocreate: true, formatversion: 2 }); } }; // ========================================== // 3. USER INTERFACE (SIMPLIFIED) // ========================================== const UI = { renderBase: function () { document.title = 'DupCheck - Clean Duplicates'; $('#firstHeading').text('🧹 DupCheck'); const html = ` <div id="dc-app"> <div class="dc-topbar"> <input type="text" id="dc-title-input" placeholder="Enter article name..." /> <button id="dc-btn-analyze" class="dc-btn primary">Scan Article</button> </div> <div id="dc-loading" style="display:none;" class="dc-notice info">Scanning... Please wait.</div> <div id="dc-error" style="display:none;" class="dc-notice danger"></div> <div id="dc-results-view" style="display:none;"></div> <div id="dc-preview-view" style="display:none;"> <h3>Step 2: Review & Save</h3> <div id="dc-diff-table" class="diff" style="margin-bottom: 20px; border: 1px solid #ccc; background: #fff;"></div> <button id="dc-btn-back" class="dc-btn secondary">⬅ Back to Results</button> <button id="dc-btn-save" class="dc-btn success">✅ Publish Changes</button> </div> </div> `; $('#mw-content-text').empty().html(html); this.injectStyles(); this.bindEvents(); }, injectStyles: function () { if ($('#dc-styles').length) return; $('<style id="dc-styles">').text(` #dc-app { max-width: 800px; margin: 0 auto; font-family: sans-serif; color: #202122; background: #f8f9fa; padding: 20px; border-radius: 6px; border: 1px solid #eaecf0; } .dc-topbar { display: flex; gap: 10px; margin-bottom: 20px; } #dc-title-input { flex: 1; padding: 10px; border: 1px solid #a2a9b1; border-radius: 4px; font-size: 15px; } .dc-btn { padding: 10px 18px; border-radius: 4px; border: none; cursor: pointer; font-weight: bold; font-size: 14px; transition: background 0.2s; } .dc-btn.primary { background: #36c; color: white; } .dc-btn.primary:hover { background: #447ff5; } .dc-btn.secondary { background: #eaecf0; color: #202122; border: 1px solid #a2a9b1; } .dc-btn.secondary:hover { background: #c8ccd1; } .dc-btn.success { background: #00af89; color: white; } .dc-btn.success:hover { background: #008f6f; } .dc-btn:disabled { opacity: 0.6; cursor: not-allowed; } .dc-notice { padding: 12px; margin-bottom: 15px; border-radius: 4px; font-weight: bold; text-align: center; } .dc-notice.info { background: #eaf3ff; color: #36c; border: 1px solid #a2c8ff; } .dc-notice.danger { background: #fee7e6; color: #d33; border: 1px solid #ffb3b3; } .dc-card { background: #fff; border: 1px solid #c8ccd1; border-radius: 6px; margin-bottom: 20px; overflow: hidden; } .dc-card-header { background: #eaecf0; padding: 10px 15px; font-weight: bold; font-size: 16px; border-bottom: 1px solid #c8ccd1; } .dc-card-body { padding: 15px; } .dc-duplicate-box { margin-bottom: 15px; border: 1px solid #eaecf0; border-radius: 4px; } .dc-snippet { background: #fdfdfd; padding: 12px; font-family: monospace; font-size: 13px; color: #555; white-space: pre-wrap; word-break: break-word; border-bottom: 1px solid #eaecf0; border-left: 4px solid #f2c84b; } .dc-actions { padding: 10px 12px; background: #fff; } .dc-action-item { display: block; margin-bottom: 8px; padding: 6px; border-radius: 4px; background: #f8f9fa; border: 1px solid #eaecf0; cursor: pointer; } .dc-action-item:last-child { margin-bottom: 0; } .dc-action-item.remove { background: #fee7e6; border-color: #fcaeb3; } #dc-footer { margin-top: 20px; text-align: right; border-top: 2px solid #eaecf0; padding-top: 15px; } `).appendTo('head'); }, bindEvents: function () { $('#dc-btn-analyze').on('click', Controller.handleAnalyze); $('#dc-app').on('click', '#dc-btn-preview', Controller.handlePreview); $('#dc-app').on('click', '#dc-btn-back', () => { $('#dc-preview-view').hide(); $('#dc-results-view').show(); }); $('#dc-app').on('click', '#dc-btn-save', Controller.handleSave); }, showNotice: function (msg, type) { $('#dc-loading, #dc-error').hide(); if (type === 'loading') $('#dc-loading').text(msg).show(); if (type === 'error') $('#dc-error').text(msg).show(); }, renderResults: function (resultsObj, title) { let html = `<h3>Step 1: Select Duplicates to Remove for <a href="/wiki/${encodeURIComponent(title)}" target="_blank">${title}</a></h3>`; let totalFindings = 0; for (const key in resultsObj) { const cat = resultsObj[key]; if (cat.findings.length === 0) continue; totalFindings += cat.findings.length; html += `<div class="dc-card"><div class="dc-card-header">${cat.icon} ${cat.name}</div><div class="dc-card-body">`; cat.findings.forEach(finding => { html += ` <div class="dc-duplicate-box"> <div class="dc-snippet">${mw.html.escape(finding.text)}</div> <div class="dc-actions"> <span style="display:block; font-size:12px; color:#555; margin-bottom:5px;">This text appears ${finding.instances.length} times:</span>`; finding.instances.forEach((inst, iIndex) => { if (iIndex === 0) { html += `<label class="dc-action-item"><input type="checkbox" disabled /> 🔒 Keep First Occurrence (Original)</label>`; } else { html += `<label class="dc-action-item remove"><input type="checkbox" class="dc-chk-remove" data-start="${inst.start}" data-length="${inst.length}" checked /> 🗑️ Remove Duplicate Occurrence ${iIndex + 1}</label>`; } }); html += `</div></div>`; }); html += `</div></div>`; } if (totalFindings === 0) { html += `<div class="dc-notice info">🎉 Perfect! No duplicates found in this article.</div>`; } else { html += `<div id="dc-footer"><button id="dc-btn-preview" class="dc-btn primary">Review Removals & Preview ➡</button></div>`; } $('#dc-loading').hide(); $('#dc-results-view').html(html).show(); $('#dc-preview-view').hide(); }, renderDiff: function (diffHtml) { $('#dc-results-view').hide(); $('#dc-diff-table').html(` <table class="diff" style="width:100%; border-collapse:collapse;"> <colgroup><col class="diff-marker"><col class="diff-content"><col class="diff-marker"><col class="diff-content"></colgroup> <tbody>${diffHtml || '<tr><td colspan="4" style="text-align:center; padding: 20px;">No actual changes detected.</td></tr>'}</tbody> </table> `); $('#dc-preview-view').show(); } }; // ========================================== // 4. CONTROLLER LOGIC // ========================================== const Controller = { handleAnalyze: async function () { const title = $('#dc-title-input').val().trim(); if (!title) return UI.showNotice('Please enter an article title.', 'error'); UI.showNotice('Fetching article data...', 'loading'); $('#dc-btn-analyze').prop('disabled', true); $('#dc-results-view, #dc-preview-view').hide(); try { const data = await ApiService.fetchArticle(title); State.title = title; State.originalWikitext = data.wikitext; State.baseRevId = data.revid; State.results = Analyzer.run(State.originalWikitext); UI.renderResults(State.results, State.title); } catch (err) { UI.showNotice(`Error: ${err.message}`, 'error'); } finally { $('#dc-btn-analyze').prop('disabled', false); } }, handlePreview: async function () { State.removals = []; $('.dc-chk-remove:checked').each(function () { State.removals.push({ start: parseInt($(this).attr('data-start')), length: parseInt($(this).attr('data-length')) }); }); if (State.removals.length === 0) return alert("Please check at least one duplicate to remove."); UI.showNotice('Generating preview...', 'loading'); $('#dc-btn-preview').prop('disabled', true); // Remove strings from bottom to top to avoid shifting indexes State.removals.sort((a, b) => b.start - a.start); let modText = State.originalWikitext; State.removals.forEach(rem => { const before = modText.slice(0, rem.start); let after = modText.slice(rem.start + rem.length); after = after.replace(/^\r?\n/, ''); // Clean empty lines modText = before + after; }); State.modifiedWikitext = modText; try { const diffHtml = await ApiService.getDiff(State.title, State.baseRevId, State.modifiedWikitext); UI.renderDiff(diffHtml); $('#dc-loading').hide(); } catch (err) { UI.showNotice(`Preview Error: ${err.message}`, 'error'); } finally { $('#dc-btn-preview').prop('disabled', false); } }, handleSave: async function () { $('#dc-btn-save').prop('disabled', true).text('Saving...'); try { await ApiService.saveEdit(State.title, State.baseRevId, State.modifiedWikitext); UI.showNotice('Changes saved successfully! Redirecting...', 'loading'); setTimeout(() => { window.location.href = mw.util.getUrl(State.title); }, 1500); } catch (err) { UI.showNotice(`Save Error: ${err.message}`, 'error'); $('#dc-btn-save').prop('disabled', false).text('✅ Publish Changes'); } } }; // ========================================== // 5. START UP // ========================================== function loadTool() { ApiService.init(); UI.renderBase(); // AUTO-ANALYZE Feature: If an article title was passed via the URL, start automatically const urlTitle = mw.util.getParamValue('dctitle'); if (urlTitle) { $('#dc-title-input').val(urlTitle.replace(/_/g, ' ')); setTimeout(() => Controller.handleAnalyze(), 200); } } $(document).ready(init); })(mediaWiki, jQuery); dua8e65xdlpuiz2kt9xi2f0xzjgv4ud 747145 747144 2026-06-17T00:29:30Z ARI 65433 747145 javascript text/javascript (function (mw, $) { 'use strict'; const SPECIAL_PAGE_NAME = 'বিশেষ:খালি_পাতা/DupCheck'; // ========================================== // 1. INITIALIZATION & TOOLBAR LINK // ========================================== function init() { mw.loader.using(['mediawiki.util'], function () { let targetUrl = mw.util.getUrl(SPECIAL_PAGE_NAME); if (mw.config.get('wgNamespaceNumber') === 0) { targetUrl += '?dctitle=' + encodeURIComponent(mw.config.get('wgPageName')); } mw.util.addPortletLink( 'p-tb', targetUrl, 'DupCheck (Duplicate Finder)', 't-dupcheck', 'Find and remove duplicate content in this article' ); }); if (mw.config.get('wgPageName') === SPECIAL_PAGE_NAME) { mw.loader.using([ 'oojs-ui-core', 'oojs-ui-widgets', 'mediawiki.api', 'mediawiki.util', 'mediawiki.diff.styles' ], loadTool); } } // ========================================== // 2. CORE LOGIC // ========================================== const State = { title: '', originalWikitext: '', modifiedWikitext: '', baseRevId: null, results: {}, removals: [] }; const Config = { minParagraphLength: 50, minLineLength: 5, excludedRegexes: [ /\[\[(Category|বিষয়শ্রেণী):.+?\]\]/gi, /\{\{DEFAULTSORT:.+?\}\}/gi, /\{\{(Reflist|Authority control|Commons category|Use dmy dates)[^\}]*\}\}/gi, /\[\[(en|hi|ur|ta|te|ml|bn|fr|de|es):.+?\]\]/gi ], excludedPrefixes: ['|-', '|+', '|}', '{|', '|', '!'], isExcluded: function (text) { let normalized = text.trim(); if (!normalized) return true; if (this.excludedPrefixes.some(p => normalized.startsWith(p))) return true; if (/^\{\{.+?\}\}$/.test(normalized)) return true; for (let regex of this.excludedRegexes) { if (regex.test(normalized)) return true; } return false; } }; const Analyzer = { run: function (wikitext, options) { this.results = {}; if (options.checkHeadings) { this.results.SEC = { name: 'Duplicate Headings', icon: '📝', findings: [] }; this.extractMatches(/^(={2,6})\s*(.+?)\s*\1\s*$/gm, wikitext, this.results.SEC, 1, 2); } if (options.checkParagraphs) { this.results.PAR = { name: 'Duplicate Paragraphs', icon: '📄', findings: [] }; this.extractMatches(/(?:^|\n\n)([^\n][\s\S]*?)(?=\n\n|$)/g, wikitext, this.results.PAR, Config.minParagraphLength, 1); } if (options.checkLines) { this.results.LIN = { name: 'Duplicate Lines', icon: '➖', findings: [] }; this.extractMatches(/^(.+)$/gm, wikitext, this.results.LIN, Config.minLineLength, 1); } if (options.checkLists) { this.results.LST = { name: 'Duplicate List Items', icon: '📌', findings: [] }; this.extractMatches(/^([\*\#]+)\s*(.+)$/gm, wikitext, this.results.LST, 2, 2); } return this.results; }, normalize: function (text) { return text.trim().replace(/[ \t]+/g, ' '); }, extractMatches: function (regex, text, categoryObj, minLength, isContentGroup = 1) { const map = new Map(); let match; regex.lastIndex = 0; while ((match = regex.exec(text)) !== null) { const rawText = match[0]; const contentText = match[isContentGroup] || rawText; const normalized = Analyzer.normalize(contentText); if (normalized.length < minLength || Config.isExcluded(normalized)) continue; if (!map.has(normalized)) map.set(normalized, []); map.get(normalized).push({ start: match.index, length: rawText.length, preview: normalized }); } for (const [normText, instances] of map.entries()) { if (instances.length > 1) categoryObj.findings.push({ text: normText, instances: instances }); } } }; const ApiService = { api: null, init: function() { this.api = new mw.Api(); }, fetchArticle: async function (title) { const response = await this.api.get({ action: 'query', prop: 'revisions', rvprop: 'content|ids', rvslots: 'main', titles: title, formatversion: 2 }); if (response.query.pages[0].missing) throw new Error("Article not found."); return { wikitext: response.query.pages[0].revisions[0].slots.main.content, revid: response.query.pages[0].revisions[0].revid }; }, getDiff: async function (title, baseRevId, modifiedText) { const response = await this.api.post({ action: 'compare', fromtitle: title, fromrev: baseRevId, totext: modifiedText, prop: 'diff', formatversion: 2 }); return response.compare.body; }, saveEdit: async function (title, baseRevId, modifiedText) { await this.api.postWithToken('csrf', { action: 'edit', title: title, text: modifiedText, summary: 'DupCheck: Removed duplicate content', baserevid: baseRevId, nocreate: true, formatversion: 2 }); } }; // ========================================== // 3. USER INTERFACE // ========================================== const UI = { renderBase: function () { document.title = 'DupCheck - Clean Duplicates'; $('#firstHeading').text('🧹 DupCheck'); const html = ` <div id="dc-app"> <!-- Search Bar --> <div class="dc-topbar"> <input type="text" id="dc-title-input" placeholder="Enter article name..." /> <button id="dc-btn-analyze" class="dc-btn primary">Scan Article</button> </div> <!-- Scan Options --> <div class="dc-options-box"> <strong>Scan for:</strong> <label><input type="checkbox" id="chk-par" checked> Paragraphs</label> <label><input type="checkbox" id="chk-sec" checked> Headings</label> <label><input type="checkbox" id="chk-lin"> Lines</label> <label><input type="checkbox" id="chk-lst"> Lists</label> </div> <!-- Status Messages --> <div id="dc-loading" style="display:none;" class="dc-notice info">Scanning... Please wait.</div> <div id="dc-error" style="display:none;" class="dc-notice danger"></div> <!-- Results Area --> <div id="dc-results-view" style="display:none;"></div> <!-- Preview Area --> <div id="dc-preview-view" style="display:none;"> <h3>Final Step: Review & Publish</h3> <div id="dc-diff-table" class="diff" style="margin-bottom: 20px; border: 1px solid #ccc; background: #fff;"></div> <div style="display:flex; justify-content:space-between;"> <button id="dc-btn-back" class="dc-btn secondary">⬅ Back to Edit</button> <button id="dc-btn-save" class="dc-btn success">✅ Publish Changes</button> </div> </div> </div> `; $('#mw-content-text').empty().html(html); this.injectStyles(); this.bindEvents(); }, injectStyles: function () { if ($('#dc-styles').length) return; $('<style id="dc-styles">').text(` #dc-app { max-width: 800px; margin: 0 auto; font-family: sans-serif; color: #202122; background: #f8f9fa; padding: 25px; border-radius: 8px; border: 1px solid #eaecf0; box-shadow: 0 2px 4px rgba(0,0,0,0.05); } .dc-topbar { display: flex; gap: 10px; margin-bottom: 15px; } #dc-title-input { flex: 1; padding: 12px; border: 1px solid #a2a9b1; border-radius: 4px; font-size: 16px; } .dc-options-box { background: #fff; border: 1px solid #c8ccd1; padding: 12px 15px; border-radius: 4px; margin-bottom: 25px; font-size: 14px; display: flex; gap: 15px; flex-wrap: wrap; align-items: center;} .dc-options-box label { cursor: pointer; display: flex; align-items: center; gap: 5px; } .dc-btn { padding: 12px 20px; border-radius: 4px; border: none; cursor: pointer; font-weight: bold; font-size: 15px; transition: background 0.2s; } .dc-btn.primary { background: #36c; color: white; } .dc-btn.primary:hover { background: #447ff5; } .dc-btn.secondary { background: #eaecf0; color: #202122; border: 1px solid #a2a9b1; } .dc-btn.secondary:hover { background: #c8ccd1; } .dc-btn.success { background: #00af89; color: white; } .dc-btn.success:hover { background: #008f6f; } .dc-btn:disabled { opacity: 0.6; cursor: not-allowed; } .dc-notice { padding: 15px; margin-bottom: 20px; border-radius: 4px; font-weight: bold; text-align: center; font-size: 15px;} .dc-notice.info { background: #eaf3ff; color: #36c; border: 1px solid #a2c8ff; } .dc-notice.danger { background: #fee7e6; color: #d33; border: 1px solid #ffb3b3; } .dc-notice.success { background: #d5fdf4; color: #00af89; border: 1px solid #00af89; } .dc-section-title { font-size: 18px; font-weight: bold; margin: 25px 0 10px 0; padding-bottom: 5px; border-bottom: 2px solid #eaecf0; } .dc-duplicate-box { background: #fff; border: 1px solid #c8ccd1; border-radius: 6px; margin-bottom: 15px; overflow: hidden; } .dc-snippet { background: #fdfdfd; padding: 15px; font-size: 14px; color: #333; white-space: pre-wrap; word-break: break-word; border-bottom: 1px solid #eaecf0; border-left: 5px solid #f2c84b; font-style: italic; } .dc-actions { padding: 15px; background: #fff; } .dc-info-text { font-size: 13px; color: #555; margin-bottom: 10px; } .dc-action-item { display: block; margin-bottom: 8px; padding: 10px; border-radius: 4px; background: #fee7e6; border: 1px solid #fcaeb3; cursor: pointer; color: #d33; font-weight: bold;} .dc-action-item:hover { background: #fcd2d5; } .dc-action-item input { margin-right: 10px; transform: scale(1.2); } #dc-footer { margin-top: 30px; text-align: right; } `).appendTo('head'); }, bindEvents: function () { $('#dc-btn-analyze').on('click', Controller.handleAnalyze); $('#dc-app').on('click', '#dc-btn-preview', Controller.handlePreview); $('#dc-app').on('click', '#dc-btn-back', () => { $('#dc-preview-view').hide(); $('#dc-results-view').show(); }); $('#dc-app').on('click', '#dc-btn-save', Controller.handleSave); }, showNotice: function (msg, type) { $('#dc-loading, #dc-error').hide(); if (type === 'loading') $('#dc-loading').text(msg).show(); if (type === 'error') $('#dc-error').text(msg).show(); }, renderResults: function (resultsObj, title) { let html = ``; let totalFindings = 0; for (const key in resultsObj) { const cat = resultsObj[key]; if (cat.findings.length === 0) continue; totalFindings += cat.findings.length; html += `<div class="dc-section-title">${cat.icon} ${cat.name} (${cat.findings.length} found)</div>`; cat.findings.forEach(finding => { html += ` <div class="dc-duplicate-box"> <div class="dc-snippet">"${mw.html.escape(finding.text)}"</div> <div class="dc-actions"> <div class="dc-info-text">This exact text was found <strong>${finding.instances.length}</strong> times. The first occurrence is kept safely. Select extra copies to delete:</div>`; // Loop starts at 1, completely skipping the original so it isn't confusing for (let i = 1; i < finding.instances.length; i++) { let inst = finding.instances[i]; html += ` <label class="dc-action-item"> <input type="checkbox" class="dc-chk-remove" data-start="${inst.start}" data-length="${inst.length}" checked /> 🗑️ Delete extra copy ${i} </label>`; } html += `</div></div>`; }); } if (totalFindings === 0) { html += `<div class="dc-notice success">🎉 Awesome! No duplicates were found based on your selected options.</div>`; } else { html += `<div id="dc-footer"><button id="dc-btn-preview" class="dc-btn primary">Generate Preview ➡</button></div>`; } $('#dc-loading').hide(); $('#dc-results-view').html(html).show(); $('#dc-preview-view').hide(); }, renderDiff: function (diffHtml) { $('#dc-results-view').hide(); $('#dc-diff-table').html(` <table class="diff" style="width:100%; border-collapse:collapse;"> <colgroup><col class="diff-marker"><col class="diff-content"><col class="diff-marker"><col class="diff-content"></colgroup> <tbody>${diffHtml || '<tr><td colspan="4" style="text-align:center; padding: 20px;">No actual changes detected.</td></tr>'}</tbody> </table> `); $('#dc-preview-view').show(); } }; // ========================================== // 4. CONTROLLER LOGIC // ========================================== const Controller = { handleAnalyze: async function () { const title = $('#dc-title-input').val().trim(); if (!title) return UI.showNotice('Please enter an article title.', 'error'); UI.showNotice('Fetching and scanning article...', 'loading'); $('#dc-btn-analyze').prop('disabled', true); $('#dc-results-view, #dc-preview-view').hide(); // Read the selected options from the UI const scanOptions = { checkParagraphs: $('#chk-par').is(':checked'), checkHeadings: $('#chk-sec').is(':checked'), checkLines: $('#chk-lin').is(':checked'), checkLists: $('#chk-lst').is(':checked') }; try { const data = await ApiService.fetchArticle(title); State.title = title; State.originalWikitext = data.wikitext; State.baseRevId = data.revid; State.results = Analyzer.run(State.originalWikitext, scanOptions); UI.renderResults(State.results, State.title); } catch (err) { UI.showNotice(`Error: ${err.message}`, 'error'); } finally { $('#dc-btn-analyze').prop('disabled', false); } }, handlePreview: async function () { State.removals = []; $('.dc-chk-remove:checked').each(function () { State.removals.push({ start: parseInt($(this).attr('data-start')), length: parseInt($(this).attr('data-length')) }); }); if (State.removals.length === 0) return alert("You unchecked all boxes. There is nothing to remove."); UI.showNotice('Generating preview...', 'loading'); $('#dc-btn-preview').prop('disabled', true); State.removals.sort((a, b) => b.start - a.start); let modText = State.originalWikitext; State.removals.forEach(rem => { const before = modText.slice(0, rem.start); let after = modText.slice(rem.start + rem.length); after = after.replace(/^\r?\n/, ''); modText = before + after; }); State.modifiedWikitext = modText; try { const diffHtml = await ApiService.getDiff(State.title, State.baseRevId, State.modifiedWikitext); UI.renderDiff(diffHtml); $('#dc-loading').hide(); } catch (err) { UI.showNotice(`Preview Error: ${err.message}`, 'error'); } finally { $('#dc-btn-preview').prop('disabled', false); } }, handleSave: async function () { $('#dc-btn-save').prop('disabled', true).text('Saving...'); try { await ApiService.saveEdit(State.title, State.baseRevId, State.modifiedWikitext); UI.showNotice('Changes saved successfully! Redirecting...', 'loading'); setTimeout(() => { window.location.href = mw.util.getUrl(State.title); }, 1500); } catch (err) { UI.showNotice(`Save Error: ${err.message}`, 'error'); $('#dc-btn-save').prop('disabled', false).text('✅ Publish Changes'); } } }; function loadTool() { ApiService.init(); UI.renderBase(); const urlTitle = mw.util.getParamValue('dctitle'); if (urlTitle) { $('#dc-title-input').val(urlTitle.replace(/_/g, ' ')); setTimeout(() => Controller.handleAnalyze(), 200); } } $(document).ready(init); })(mediaWiki, jQuery); r12yvz9dm6blbujydeksnp2bh6bap7p 747146 747145 2026-06-17T00:34:14Z ARI 65433 747146 javascript text/javascript (function (mw, $) { 'use strict'; const SPECIAL_PAGE_NAME = 'বিশেষ:খালি_পাতা/DupCheck'; // ========================================== // 1. INITIALIZATION & TOOLBAR LINK // ========================================== function init() { mw.loader.using(['mediawiki.util'], function () { let targetUrl = mw.util.getUrl(SPECIAL_PAGE_NAME); if (mw.config.get('wgNamespaceNumber') === 0) { targetUrl += '?dctitle=' + encodeURIComponent(mw.config.get('wgPageName')); } mw.util.addPortletLink( 'p-tb', targetUrl, 'DupCheck (Duplicate Finder)', 't-dupcheck', 'Find and remove duplicate content in this article' ); }); if (mw.config.get('wgPageName') === SPECIAL_PAGE_NAME) { mw.loader.using([ 'oojs-ui-core', 'oojs-ui-widgets', 'mediawiki.api', 'mediawiki.util', 'mediawiki.diff.styles' ], loadTool); } } // ========================================== // 2. CORE LOGIC // ========================================== const State = { title: '', originalWikitext: '', modifiedWikitext: '', baseRevId: null, results: {}, removals: [] }; const Config = { minParagraphLength: 50, minLineLength: 5, excludedRegexes: [ /\[\[(Category|বিষয়শ্রেণী):.+?\]\]/gi, /\{\{DEFAULTSORT:.+?\}\}/gi, /\{\{(Reflist|Authority control|Commons category|Use dmy dates)[^\}]*\}\}/gi, /\[\[(en|hi|ur|ta|te|ml|bn|fr|de|es):.+?\]\]/gi ], excludedPrefixes: ['|-', '|+', '|}', '{|', '|', '!'], isExcluded: function (text) { let normalized = text.trim(); if (!normalized) return true; if (this.excludedPrefixes.some(p => normalized.startsWith(p))) return true; if (/^\{\{.+?\}\}$/.test(normalized)) return true; for (let regex of this.excludedRegexes) { if (regex.test(normalized)) return true; } return false; } }; const Analyzer = { run: function (wikitext, options) { this.results = {}; if (options.checkHeadings) { this.results.SEC = { name: 'Duplicate Headings', icon: '📝', findings: [] }; this.extractMatches(/^(={2,6})\s*(.+?)\s*\1\s*$/gm, wikitext, this.results.SEC, 1, 2); } if (options.checkParagraphs) { this.results.PAR = { name: 'Duplicate Paragraphs', icon: '📄', findings: [] }; this.extractMatches(/(?:^|\n\n)([^\n][\s\S]*?)(?=\n\n|$)/g, wikitext, this.results.PAR, Config.minParagraphLength, 1); } if (options.checkLines) { this.results.LIN = { name: 'Duplicate Lines', icon: '➖', findings: [] }; this.extractMatches(/^(.+)$/gm, wikitext, this.results.LIN, Config.minLineLength, 1); } if (options.checkLists) { this.results.LST = { name: 'Duplicate List Items', icon: '📌', findings: [] }; this.extractMatches(/^([\*\#]+)\s*(.+)$/gm, wikitext, this.results.LST, 2, 2); } return this.results; }, normalize: function (text) { return text.trim().replace(/[ \t]+/g, ' '); }, extractMatches: function (regex, text, categoryObj, minLength, isContentGroup = 1) { const map = new Map(); let match; regex.lastIndex = 0; while ((match = regex.exec(text)) !== null) { const rawText = match[0]; const contentText = match[isContentGroup] || rawText; const normalized = Analyzer.normalize(contentText); if (normalized.length < minLength || Config.isExcluded(normalized)) continue; if (!map.has(normalized)) map.set(normalized, []); map.get(normalized).push({ start: match.index, length: rawText.length, preview: normalized }); } for (const [normText, instances] of map.entries()) { if (instances.length > 1) categoryObj.findings.push({ text: normText, instances: instances }); } } }; const ApiService = { api: null, init: function() { this.api = new mw.Api(); }, fetchArticle: async function (title) { const response = await this.api.get({ action: 'query', prop: 'revisions', rvprop: 'content|ids', rvslots: 'main', titles: title, formatversion: 2 }); if (response.query.pages[0].missing) throw new Error("Article not found."); return { wikitext: response.query.pages[0].revisions[0].slots.main.content, revid: response.query.pages[0].revisions[0].revid }; }, getDiff: async function (title, baseRevId, modifiedText) { const response = await this.api.post({ action: 'compare', fromtitle: title, fromrev: baseRevId, totext: modifiedText, prop: 'diff', formatversion: 2 }); return response.compare.body; }, saveEdit: async function (title, baseRevId, modifiedText) { await this.api.postWithToken('csrf', { action: 'edit', title: title, text: modifiedText, summary: 'DupCheck: Cleaned duplicate content', baserevid: baseRevId, nocreate: true, formatversion: 2 }); } }; // ========================================== // 3. USER INTERFACE (TWO-COLUMN VIEW) // ========================================== const UI = { renderBase: function () { document.title = 'DupCheck - Clean Duplicates'; $('#firstHeading').text('🧹 DupCheck'); const html = ` <div id="dc-layout"> <!-- LEFT COLUMN: CONTROLS --> <div id="dc-sidebar"> <div class="dc-panel"> <h3>🔍 Search Article</h3> <input type="text" id="dc-title-input" placeholder="e.g., বাংলা ভাষা..." /> <h3 style="margin-top:20px;">⚙️ Scan Options</h3> <div class="dc-options-list"> <label><input type="checkbox" id="chk-par" checked> Paragraphs (Text blocks)</label> <label><input type="checkbox" id="chk-sec" checked> Headings (Sections)</label> <label><input type="checkbox" id="chk-lin"> Single Lines</label> <label><input type="checkbox" id="chk-lst"> List Items (* or #)</label> </div> <button id="dc-btn-analyze" class="dc-btn primary" style="width:100%; margin-top:20px;">Start Scan ➡</button> </div> <div id="dc-status-area" style="margin-top: 15px;"></div> </div> <!-- RIGHT COLUMN: RESULTS & PREVIEW --> <div id="dc-main"> <div id="dc-welcome-view" class="dc-panel" style="text-align:center; padding: 40px 20px; color: #555;"> <h2>Welcome to DupCheck</h2> <p>Enter an article name on the left and click "Start Scan".</p> <p>The results will appear right here.</p> </div> <div id="dc-results-view" style="display:none;"></div> <div id="dc-preview-view" style="display:none;" class="dc-panel"> <h2>Review & Publish</h2> <div id="dc-diff-table" class="diff" style="margin-bottom: 20px; border: 1px solid #ccc; background: #fff;"></div> <div style="display:flex; justify-content:space-between; margin-top:20px;"> <button id="dc-btn-back" class="dc-btn secondary">⬅ Back to Results</button> <button id="dc-btn-save" class="dc-btn success">✅ Publish Changes</button> </div> </div> </div> </div> `; $('#mw-content-text').empty().html(html); this.injectStyles(); this.bindEvents(); }, injectStyles: function () { if ($('#dc-styles').length) return; $('<style id="dc-styles">').text(` /* 2-Column Grid Layout */ #dc-layout { display: grid; grid-template-columns: 300px 1fr; gap: 20px; align-items: start; font-family: sans-serif; color: #202122; } /* Responsive Stack for Mobile */ @media (max-width: 768px) { #dc-layout { grid-template-columns: 1fr; } } .dc-panel { background: #f8f9fa; border: 1px solid #eaecf0; border-radius: 4px; padding: 20px; box-sizing: border-box; } .dc-panel h3 { margin-top: 0; font-size: 15px; font-weight: bold; border-bottom: 1px solid #c8ccd1; padding-bottom: 5px; margin-bottom: 10px; } #dc-title-input { width: 100%; box-sizing: border-box; padding: 10px; border: 1px solid #a2a9b1; border-radius: 2px; font-size: 15px; } .dc-options-list { display: flex; flex-direction: column; gap: 8px; font-size: 14px; } .dc-options-list label { cursor: pointer; display: flex; align-items: center; gap: 8px; } .dc-options-list input { margin: 0; transform: scale(1.1); } /* Buttons */ .dc-btn { padding: 10px 16px; border-radius: 3px; border: none; cursor: pointer; font-weight: bold; font-size: 14px; transition: background 0.2s; text-align: center; } .dc-btn.primary { background: #36c; color: white; border: 1px solid #36c; } .dc-btn.primary:hover { background: #447ff5; } .dc-btn.secondary { background: #eaecf0; color: #202122; border: 1px solid #a2a9b1; } .dc-btn.secondary:hover { background: #c8ccd1; } .dc-btn.success { background: #00af89; color: white; } .dc-btn.success:hover { background: #008f6f; } .dc-btn:disabled { opacity: 0.6; cursor: not-allowed; } /* Status Messages in Left Sidebar */ .dc-notice { padding: 12px; border-radius: 3px; font-weight: bold; font-size: 13px; text-align: center; } .dc-notice.loading { background: #eaf3ff; color: #36c; border: 1px solid #a2c8ff; } .dc-notice.error { background: #fee7e6; color: #d33; border: 1px solid #ffb3b3; } .dc-notice.success { background: #d5fdf4; color: #00af89; border: 1px solid #00af89; } /* Right Column Results Styles */ .dc-section-title { font-size: 18px; font-weight: bold; margin: 0 0 15px 0; padding-bottom: 5px; border-bottom: 2px solid #eaecf0; } .dc-duplicate-box { background: #fff; border: 1px solid #c8ccd1; border-radius: 4px; margin-bottom: 20px; box-shadow: 0 1px 3px rgba(0,0,0,0.05); } .dc-snippet { background: #fdfdfd; padding: 15px; font-size: 14px; color: #333; white-space: pre-wrap; word-break: break-word; border-bottom: 1px solid #eaecf0; border-left: 5px solid #f2c84b; font-style: italic; } .dc-actions { padding: 15px; background: #fff; } .dc-info-text { font-size: 13px; color: #555; margin-bottom: 10px; } .dc-action-item { display: block; margin-bottom: 5px; padding: 8px 10px; border-radius: 3px; background: #fee7e6; border: 1px solid #fcaeb3; cursor: pointer; color: #d33; font-weight: bold; font-size: 14px; } .dc-action-item:hover { background: #fcd2d5; } .dc-action-item input { margin-right: 10px; transform: scale(1.2); } `).appendTo('head'); }, bindEvents: function () { $('#dc-btn-analyze').on('click', Controller.handleAnalyze); $('#dc-layout').on('click', '#dc-btn-preview', Controller.handlePreview); $('#dc-layout').on('click', '#dc-btn-back', () => { $('#dc-preview-view').hide(); $('#dc-results-view').show(); }); $('#dc-layout').on('click', '#dc-btn-save', Controller.handleSave); }, showNotice: function (msg, type) { $('#dc-status-area').html(`<div class="dc-notice ${type}">${msg}</div>`); }, clearNotice: function () { $('#dc-status-area').empty(); }, renderResults: function (resultsObj, title) { let html = `<div class="dc-panel" style="margin-bottom:20px;"> <h2 style="margin-top:0;">Analysis for: <a href="/wiki/${encodeURIComponent(title)}" target="_blank">${title}</a></h2> </div>`; let totalFindings = 0; for (const key in resultsObj) { const cat = resultsObj[key]; if (cat.findings.length === 0) continue; totalFindings += cat.findings.length; html += `<div class="dc-panel" style="margin-bottom: 20px;"> <div class="dc-section-title">${cat.icon} ${cat.name} (${cat.findings.length} found)</div>`; cat.findings.forEach(finding => { html += ` <div class="dc-duplicate-box"> <div class="dc-snippet">"${mw.html.escape(finding.text)}"</div> <div class="dc-actions"> <div class="dc-info-text">Found <strong>${finding.instances.length}</strong> times. The first occurrence is preserved. Extra copies below will be deleted:</div>`; for (let i = 1; i < finding.instances.length; i++) { let inst = finding.instances[i]; html += ` <label class="dc-action-item"> <input type="checkbox" class="dc-chk-remove" data-start="${inst.start}" data-length="${inst.length}" checked /> 🗑️ Delete extra copy ${i} </label>`; } html += `</div></div>`; }); html += `</div>`; } if (totalFindings === 0) { html += `<div class="dc-panel"><div class="dc-notice success">🎉 Awesome! No duplicates were found based on your options.</div></div>`; } else { html += `<div style="text-align: right; margin-top: 20px;"> <button id="dc-btn-preview" class="dc-btn primary" style="font-size: 16px; padding: 12px 24px;">Review & Preview Changes ➡</button> </div>`; } $('#dc-welcome-view, #dc-preview-view').hide(); $('#dc-results-view').html(html).show(); this.clearNotice(); }, renderDiff: function (diffHtml) { $('#dc-results-view').hide(); $('#dc-diff-table').html(` <table class="diff" style="width:100%; border-collapse:collapse;"> <colgroup><col class="diff-marker"><col class="diff-content"><col class="diff-marker"><col class="diff-content"></colgroup> <tbody>${diffHtml || '<tr><td colspan="4" style="text-align:center; padding: 20px;">No actual changes detected.</td></tr>'}</tbody> </table> `); $('#dc-preview-view').show(); } }; // ========================================== // 4. CONTROLLER LOGIC // ========================================== const Controller = { handleAnalyze: async function () { const title = $('#dc-title-input').val().trim(); if (!title) return UI.showNotice('Please enter an article title.', 'error'); UI.showNotice('⏳ Fetching and scanning article...', 'loading'); $('#dc-btn-analyze').prop('disabled', true); $('#dc-welcome-view, #dc-results-view, #dc-preview-view').hide(); const scanOptions = { checkParagraphs: $('#chk-par').is(':checked'), checkHeadings: $('#chk-sec').is(':checked'), checkLines: $('#chk-lin').is(':checked'), checkLists: $('#chk-lst').is(':checked') }; try { const data = await ApiService.fetchArticle(title); State.title = title; State.originalWikitext = data.wikitext; State.baseRevId = data.revid; State.results = Analyzer.run(State.originalWikitext, scanOptions); UI.renderResults(State.results, State.title); } catch (err) { UI.showNotice(`❌ Error: ${err.message}`, 'error'); $('#dc-welcome-view').show(); } finally { $('#dc-btn-analyze').prop('disabled', false); } }, handlePreview: async function () { State.removals = []; $('.dc-chk-remove:checked').each(function () { State.removals.push({ start: parseInt($(this).attr('data-start')), length: parseInt($(this).attr('data-length')) }); }); if (State.removals.length === 0) return alert("You unchecked all boxes. There is nothing to remove."); UI.showNotice('⏳ Generating preview...', 'loading'); $('#dc-btn-preview').prop('disabled', true); State.removals.sort((a, b) => b.start - a.start); let modText = State.originalWikitext; State.removals.forEach(rem => { const before = modText.slice(0, rem.start); let after = modText.slice(rem.start + rem.length); after = after.replace(/^\r?\n/, ''); modText = before + after; }); State.modifiedWikitext = modText; try { const diffHtml = await ApiService.getDiff(State.title, State.baseRevId, State.modifiedWikitext); UI.renderDiff(diffHtml); UI.clearNotice(); } catch (err) { UI.showNotice(`❌ Preview Error: ${err.message}`, 'error'); } finally { $('#dc-btn-preview').prop('disabled', false); } }, handleSave: async function () { $('#dc-btn-save').prop('disabled', true).text('⏳ Saving...'); try { await ApiService.saveEdit(State.title, State.baseRevId, State.modifiedWikitext); UI.showNotice('✅ Changes saved! Redirecting...', 'success'); setTimeout(() => { window.location.href = mw.util.getUrl(State.title); }, 1500); } catch (err) { UI.showNotice(`❌ Save Error: ${err.message}`, 'error'); $('#dc-btn-save').prop('disabled', false).text('✅ Publish Changes'); } } }; function loadTool() { ApiService.init(); UI.renderBase(); const urlTitle = mw.util.getParamValue('dctitle'); if (urlTitle) { $('#dc-title-input').val(urlTitle.replace(/_/g, ' ')); setTimeout(() => Controller.handleAnalyze(), 200); } } $(document).ready(init); })(mediaWiki, jQuery); 5vtaso22sub8wleh3x76nmfhjbqqw9m 747147 747146 2026-06-17T00:38:18Z ARI 65433 747147 javascript text/javascript (function (mw, $) { 'use strict'; const SPECIAL_PAGE_NAME = 'বিশেষ:খালি_পাতা/DupCheck'; // ========================================== // 1. INITIALIZATION & TOOLBAR LINK // ========================================== function init() { mw.loader.using(['mediawiki.util'], function () { let targetUrl = mw.util.getUrl(SPECIAL_PAGE_NAME); if (mw.config.get('wgNamespaceNumber') === 0) { targetUrl += '?dctitle=' + encodeURIComponent(mw.config.get('wgPageName')); } mw.util.addPortletLink( 'p-tb', targetUrl, 'DupCheck (Duplicate Finder)', 't-dupcheck', 'Find and remove duplicate content in this article' ); }); if (mw.config.get('wgPageName') === SPECIAL_PAGE_NAME) { mw.loader.using([ 'oojs-ui-core', 'oojs-ui-widgets', 'mediawiki.api', 'mediawiki.util', 'mediawiki.diff.styles' ], loadTool); } } // ========================================== // 2. CORE LOGIC & ANALYZER // ========================================== const State = { title: '', originalWikitext: '', modifiedWikitext: '', baseRevId: null, results: {}, flatMatches: [] }; const Config = { minParagraphLength: 50, minLineLength: 5, excludedRegexes: [ /\[\[(Category|বিষয়শ্রেণী):.+?\]\]/gi, /\{\{DEFAULTSORT:.+?\}\}/gi, /\{\{(Reflist|Authority control|Commons category|Use dmy dates)[^\}]*\}\}/gi, /\[\[(en|hi|ur|ta|te|ml|bn|fr|de|es):.+?\]\]/gi ], excludedPrefixes: ['|-', '|+', '|}', '{|', '|', '!'], isExcluded: function (text) { let normalized = text.trim(); if (!normalized) return true; if (this.excludedPrefixes.some(p => normalized.startsWith(p))) return true; if (/^\{\{.+?\}\}$/.test(normalized)) return true; for (let regex of this.excludedRegexes) { if (regex.test(normalized)) return true; } return false; } }; const Analyzer = { run: function (wikitext, options) { this.results = {}; if (options.checkHeadings) { this.results.SEC = { id: 'SEC', name: 'Duplicate Headings', findings: [] }; this.extractMatches(/^(={2,6})\s*(.+?)\s*\1\s*$/gm, wikitext, this.results.SEC, 1, 2); } if (options.checkParagraphs) { this.results.PAR = { id: 'PAR', name: 'Duplicate Paragraphs', findings: [] }; this.extractMatches(/(?:^|\n\n)([^\n][\s\S]*?)(?=\n\n|$)/g, wikitext, this.results.PAR, Config.minParagraphLength, 1); } if (options.checkLines) { this.results.LIN = { id: 'LIN', name: 'Duplicate Lines', findings: [] }; this.extractMatches(/^(.+)$/gm, wikitext, this.results.LIN, Config.minLineLength, 1); } if (options.checkLists) { this.results.LST = { id: 'LST', name: 'Duplicate List Items', findings: [] }; this.extractMatches(/^([\*\#]+)\s*(.+)$/gm, wikitext, this.results.LST, 2, 2); } return this.results; }, normalize: function (text) { return text.trim().replace(/[ \t]+/g, ' '); }, extractMatches: function (regex, text, categoryObj, minLength, isContentGroup = 1) { const map = new Map(); let match; regex.lastIndex = 0; while ((match = regex.exec(text)) !== null) { const rawText = match[0]; const contentText = match[isContentGroup] || rawText; const normalized = Analyzer.normalize(contentText); if (normalized.length < minLength || Config.isExcluded(normalized)) continue; if (!map.has(normalized)) map.set(normalized, []); map.get(normalized).push({ start: match.index, length: rawText.length, preview: normalized, raw: rawText }); } for (const [normText, instances] of map.entries()) { if (instances.length > 1) categoryObj.findings.push({ text: normText, instances: instances }); } } }; const ApiService = { api: null, init: function() { this.api = new mw.Api(); }, fetchArticle: async function (title) { const response = await this.api.get({ action: 'query', prop: 'revisions', rvprop: 'content|ids', rvslots: 'main', titles: title, formatversion: 2 }); if (response.query.pages[0].missing) throw new Error("Article not found."); return { wikitext: response.query.pages[0].revisions[0].slots.main.content, revid: response.query.pages[0].revisions[0].revid }; }, getDiff: async function (title, baseRevId, modifiedText) { const response = await this.api.post({ action: 'compare', fromtitle: title, fromrev: baseRevId, totext: modifiedText, prop: 'diff', formatversion: 2 }); return response.compare.body; }, saveEdit: async function (title, baseRevId, modifiedText) { await this.api.postWithToken('csrf', { action: 'edit', title: title, text: modifiedText, summary: 'DupCheck: Cleaned duplicate content', baserevid: baseRevId, nocreate: true, formatversion: 2 }); } }; // ========================================== // 3. UI LAYOUT & HIGHLIGHTING ENGINE // ========================================== const UI = { renderBase: function () { document.title = 'DupCheck - Context Review'; $('#firstHeading').text('🧹 DupCheck'); const html = ` <div id="dc-layout"> <!-- LEFT COLUMN: CONTROLS & NAVIGATOR --> <div id="dc-sidebar"> <div class="dc-panel"> <h3 style="margin-top:0;">1. Target Article</h3> <input type="text" id="dc-title-input" placeholder="e.g., বাংলা ভাষা..." /> <h3 style="margin-top:20px;">2. Scan Options</h3> <div class="dc-options-list"> <label><input type="checkbox" id="chk-par" checked> Paragraphs</label> <label><input type="checkbox" id="chk-sec" checked> Headings</label> <label><input type="checkbox" id="chk-lin"> Lines</label> <label><input type="checkbox" id="chk-lst"> Lists</label> </div> <button id="dc-btn-analyze" class="dc-btn primary" style="width:100%; margin-top:15px;">Start Scan ➡</button> </div> <div id="dc-status-area" style="margin-top: 15px;"></div> <div id="dc-navigator" class="dc-panel" style="display:none; margin-top: 15px; max-height: 400px; overflow-y: auto;"> <h3 style="margin-top:0;">Navigation Map</h3> <div id="dc-nav-list"></div> </div> </div> <!-- RIGHT COLUMN: FULL TEXT EDITOR VIEW & PREVIEW --> <div id="dc-main"> <div id="dc-welcome-view" class="dc-panel" style="text-align:center; padding: 60px 20px; color: #555;"> <h2 style="color:#202122;">Welcome to the Context Viewer</h2> <p>Enter an article on the left to load its full text here.</p> <p>Duplicates will be highlighted directly inside the text.</p> </div> <div id="dc-editor-view" style="display:none;" class="dc-panel"> <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom: 10px; border-bottom: 1px solid #ccc; padding-bottom:10px;"> <h3 style="margin:0;">Wikitext Review (Interactive)</h3> <div> <span style="font-size:12px; background:#eaf3ff; border:1px solid #36c; padding:2px 6px; border-radius:3px;">🟦 Original (Kept)</span> <span style="font-size:12px; background:#fee7e6; border:1px solid #d33; padding:2px 6px; border-radius:3px; text-decoration:line-through; margin-left:5px;">🟥 Duplicate (Click to keep)</span> </div> </div> <!-- The actual highlighted text box --> <div id="dc-highlighter-box" class="dc-textarea-mock"></div> <div style="text-align: right; margin-top: 15px;"> <button id="dc-btn-preview" class="dc-btn primary">Generate Preview ➡</button> </div> </div> <div id="dc-preview-view" style="display:none;" class="dc-panel"> <h2>Review & Publish</h2> <div id="dc-diff-table" class="diff" style="margin-bottom: 20px; border: 1px solid #ccc; background: #fff;"></div> <div style="display:flex; justify-content:space-between; margin-top:20px;"> <button id="dc-btn-back" class="dc-btn secondary">⬅ Back to Editor</button> <button id="dc-btn-save" class="dc-btn success">✅ Publish Changes</button> </div> </div> </div> </div> `; $('#mw-content-text').empty().html(html); this.injectStyles(); this.bindEvents(); }, injectStyles: function () { if ($('#dc-styles').length) return; $('<style id="dc-styles">').text(` #dc-layout { display: grid; grid-template-columns: 320px 1fr; gap: 20px; align-items: start; font-family: sans-serif; color: #202122; } @media (max-width: 768px) { #dc-layout { grid-template-columns: 1fr; } } .dc-panel { background: #f8f9fa; border: 1px solid #eaecf0; border-radius: 4px; padding: 20px; box-sizing: border-box; } .dc-panel h3 { margin-top: 0; font-size: 15px; font-weight: bold; border-bottom: 1px solid #c8ccd1; padding-bottom: 5px; margin-bottom: 10px; } #dc-title-input { width: 100%; box-sizing: border-box; padding: 10px; border: 1px solid #a2a9b1; border-radius: 2px; font-size: 15px; } .dc-options-list { display: flex; flex-direction: column; gap: 8px; font-size: 14px; } .dc-options-list label { cursor: pointer; display: flex; align-items: center; gap: 8px; } .dc-options-list input { margin: 0; transform: scale(1.1); } .dc-btn { padding: 10px 16px; border-radius: 3px; border: none; cursor: pointer; font-weight: bold; font-size: 14px; transition: background 0.2s; text-align: center; } .dc-btn.primary { background: #36c; color: white; } .dc-btn.primary:hover { background: #447ff5; } .dc-btn.secondary { background: #eaecf0; color: #202122; border: 1px solid #a2a9b1; } .dc-btn.secondary:hover { background: #c8ccd1; } .dc-btn.success { background: #00af89; color: white; } .dc-btn.success:hover { background: #008f6f; } .dc-btn:disabled { opacity: 0.6; cursor: not-allowed; } .dc-notice { padding: 12px; border-radius: 3px; font-weight: bold; font-size: 13px; text-align: center; } .dc-notice.loading { background: #eaf3ff; color: #36c; border: 1px solid #a2c8ff; } .dc-notice.error { background: #fee7e6; color: #d33; border: 1px solid #ffb3b3; } .dc-notice.success { background: #d5fdf4; color: #00af89; border: 1px solid #00af89; } /* Highlighted Editor Box */ .dc-textarea-mock { width: 100%; height: 60vh; overflow-y: scroll; background: #fff; border: 1px solid #a2a9b1; border-radius: 2px; padding: 15px; box-sizing: border-box; font-family: 'Courier New', monospace; font-size: 13px; line-height: 1.6; white-space: pre-wrap; word-wrap: break-word; color: #202122; } /* Highlight Colors */ .dc-hl-original { background: #eaf3ff; border: 1px solid #36c; border-radius: 2px; padding: 0 2px; box-shadow: 0 0 5px rgba(54,104,204,0.3); } .dc-hl-duplicate { background: #fee7e6; border: 1px solid #d33; border-radius: 2px; padding: 0 2px; cursor: pointer; transition: all 0.2s; } .dc-hl-duplicate:hover { filter: brightness(0.95); box-shadow: 0 0 5px rgba(211,51,51,0.5); } .dc-hl-duplicate.dc-remove-active { text-decoration: line-through; opacity: 0.7; } /* Navigator List */ .dc-nav-item { border: 1px solid #c8ccd1; border-radius: 3px; margin-bottom: 8px; background: #fff; overflow: hidden; } .dc-nav-header { font-size: 12px; font-weight: bold; background: #eaecf0; padding: 5px 8px; } .dc-nav-link { display: block; font-size: 12px; padding: 6px 8px; cursor: pointer; border-top: 1px solid #eaecf0; text-decoration: none !important; color: #202122;} .dc-nav-link.original { border-left: 4px solid #36c; } .dc-nav-link.original:hover { background: #eaf3ff; } .dc-nav-link.duplicate { border-left: 4px solid #d33; } .dc-nav-link.duplicate:hover { background: #fee7e6; } `).appendTo('head'); }, bindEvents: function () { $('#dc-btn-analyze').on('click', Controller.handleAnalyze); $('#dc-layout').on('click', '#dc-btn-preview', Controller.handlePreview); $('#dc-layout').on('click', '#dc-btn-back', () => { $('#dc-preview-view').hide(); $('#dc-editor-view').show(); }); $('#dc-layout').on('click', '#dc-btn-save', Controller.handleSave); // Toggle deletion state when clicking a duplicate highlight $('#dc-layout').on('click', '.dc-hl-duplicate', function () { $(this).toggleClass('dc-remove-active'); }); // Navigate to text block $('#dc-layout').on('click', '.dc-nav-link', function () { const targetId = $(this).attr('data-target'); const targetEl = $('#' + targetId); if (targetEl.length) { $('#dc-highlighter-box').animate({ scrollTop: $('#dc-highlighter-box').scrollTop() + targetEl.position().top - 50 }, 300); // Flash the element briefly to show where we landed targetEl.css('opacity', '0.2').animate({opacity: 1}, 400); } }); }, showNotice: function (msg, type) { $('#dc-status-area').html(`<div class="dc-notice ${type}">${msg}</div>`); }, clearNotice: function () { $('#dc-status-area').empty(); }, // Flatten nested results and handle overlaps for HTML generation buildEditorView: function (resultsObj, wikitext) { let allMatches = []; let idCounter = 0; // 1. Flatten all categories into a single array for (const key in resultsObj) { const cat = resultsObj[key]; cat.findings.forEach((finding, fIdx) => { finding.instances.forEach((inst, iIdx) => { allMatches.push({ id: `dc-occ-${idCounter++}`, catName: cat.name, start: inst.start, length: inst.length, isOriginal: (iIdx === 0), preview: inst.preview, group: `${key}-${fIdx}` }); }); }); } if (allMatches.length === 0) { $('#dc-welcome-view').hide(); $('#dc-editor-view').hide(); $('#dc-navigator').hide(); return this.showNotice('🎉 No duplicates found!', 'success'); } // 2. Sort by start position. If overlap, keep the longer one. allMatches.sort((a, b) => a.start !== b.start ? a.start - b.start : b.length - a.length); let filteredMatches = []; let currentEnd = 0; for (let m of allMatches) { if (m.start >= currentEnd) { filteredMatches.push(m); currentEnd = m.start + m.length; } } State.flatMatches = filteredMatches; // 3. Build the highlighted HTML let html = ''; let lastIdx = 0; for (let m of filteredMatches) { // Add normal text leading up to this match html += mw.html.escape(wikitext.substring(lastIdx, m.start)); // Add the highlighted span const matchText = mw.html.escape(wikitext.substr(m.start, m.length)); if (m.isOriginal) { html += `<span id="${m.id}" class="dc-hl-original" title="${m.catName} (Original)">${matchText}</span>`; } else { // Duplicates start crossed out (dc-remove-active) by default html += `<span id="${m.id}" class="dc-hl-duplicate dc-remove-active" title="${m.catName} (Duplicate) - Click to keep" data-start="${m.start}" data-length="${m.length}">${matchText}</span>`; } lastIdx = m.start + m.length; } // Add remaining normal text html += mw.html.escape(wikitext.substring(lastIdx)); // 4. Build the Navigator Sidebar let navHtml = ''; // Group the filtered matches back by their content group for navigation const groups = {}; filteredMatches.forEach(m => { if (!groups[m.group]) groups[m.group] = { cat: m.catName, preview: m.preview, items: [] }; groups[m.group].items.push(m); }); for (const g in groups) { const grp = groups[g]; // Only show in navigator if there is actually more than 1 item left after filtering overlaps if (grp.items.length > 1) { navHtml += `<div class="dc-nav-item"> <div class="dc-nav-header">${grp.cat}: "${mw.html.escape(grp.preview.substring(0,30))}..."</div>`; grp.items.forEach((item, idx) => { const typeClass = item.isOriginal ? 'original' : 'duplicate'; const label = item.isOriginal ? '🟦 Jump to Original' : `🟥 Jump to Duplicate ${idx}`; navHtml += `<div class="dc-nav-link ${typeClass}" data-target="${item.id}">${label}</div>`; }); navHtml += `</div>`; } } // 5. Render $('#dc-highlighter-box').html(html); if (navHtml) { $('#dc-nav-list').html(navHtml); $('#dc-navigator').show(); } else { $('#dc-navigator').hide(); } $('#dc-welcome-view, #dc-preview-view').hide(); $('#dc-editor-view').show(); this.clearNotice(); }, renderDiff: function (diffHtml) { $('#dc-editor-view').hide(); $('#dc-navigator').hide(); $('#dc-diff-table').html(` <table class="diff" style="width:100%; border-collapse:collapse;"> <colgroup><col class="diff-marker"><col class="diff-content"><col class="diff-marker"><col class="diff-content"></colgroup> <tbody>${diffHtml || '<tr><td colspan="4" style="text-align:center; padding: 20px;">No actual changes detected.</td></tr>'}</tbody> </table> `); $('#dc-preview-view').show(); } }; // ========================================== // 4. APPLICATION CONTROLLER // ========================================== const Controller = { handleAnalyze: async function () { const title = $('#dc-title-input').val().trim(); if (!title) return UI.showNotice('Please enter an article title.', 'error'); UI.showNotice('⏳ Fetching and building editor...', 'loading'); $('#dc-btn-analyze').prop('disabled', true); const scanOptions = { checkParagraphs: $('#chk-par').is(':checked'), checkHeadings: $('#chk-sec').is(':checked'), checkLines: $('#chk-lin').is(':checked'), checkLists: $('#chk-lst').is(':checked') }; try { const data = await ApiService.fetchArticle(title); State.title = title; State.originalWikitext = data.wikitext; State.baseRevId = data.revid; State.results = Analyzer.run(State.originalWikitext, scanOptions); UI.buildEditorView(State.results, State.originalWikitext); } catch (err) { UI.showNotice(`❌ Error: ${err.message}`, 'error'); } finally { $('#dc-btn-analyze').prop('disabled', false); } }, handlePreview: async function () { State.removals = []; // Find all duplicate spans that currently have the 'dc-remove-active' class (strikethrough) $('.dc-hl-duplicate.dc-remove-active').each(function () { State.removals.push({ start: parseInt($(this).attr('data-start')), length: parseInt($(this).attr('data-length')) }); }); if (State.removals.length === 0) return alert("You clicked to keep everything. There is nothing to remove."); UI.showNotice('⏳ Generating preview...', 'loading'); $('#dc-btn-preview').prop('disabled', true); // Sort descending so splicing doesn't shift indexes State.removals.sort((a, b) => b.start - a.start); let modText = State.originalWikitext; State.removals.forEach(rem => { const before = modText.slice(0, rem.start); let after = modText.slice(rem.start + rem.length); after = after.replace(/^\r?\n/, ''); // Clean up trailing blank line modText = before + after; }); State.modifiedWikitext = modText; try { const diffHtml = await ApiService.getDiff(State.title, State.baseRevId, State.modifiedWikitext); UI.renderDiff(diffHtml); UI.clearNotice(); } catch (err) { UI.showNotice(`❌ Preview Error: ${err.message}`, 'error'); } finally { $('#dc-btn-preview').prop('disabled', false); } }, handleSave: async function () { $('#dc-btn-save').prop('disabled', true).text('⏳ Saving...'); try { await ApiService.saveEdit(State.title, State.baseRevId, State.modifiedWikitext); UI.showNotice('✅ Changes saved! Redirecting...', 'success'); setTimeout(() => { window.location.href = mw.util.getUrl(State.title); }, 1500); } catch (err) { UI.showNotice(`❌ Save Error: ${err.message}`, 'error'); $('#dc-btn-save').prop('disabled', false).text('✅ Publish Changes'); } } }; function loadTool() { ApiService.init(); UI.renderBase(); const urlTitle = mw.util.getParamValue('dctitle'); if (urlTitle) { $('#dc-title-input').val(urlTitle.replace(/_/g, ' ')); setTimeout(() => Controller.handleAnalyze(), 200); } } $(document).ready(init); })(mediaWiki, jQuery); o5n2qysu3xksymidjcati41vcnvu0gz 747148 747147 2026-06-17T00:52:59Z ARI 65433 747148 javascript text/javascript /** * DupCheck Pro - Advanced 3-Column Contextual Review Tool * Target: Bengali Wikipedia * Fully matched to the requested UI design. */ (function (mw, $) { 'use strict'; const SPECIAL_PAGE_NAME = 'বিশেষ:খালি_পাতা/DupCheck'; // ========================================== // 1. INITIALIZATION & TOOLBAR // ========================================== function init() { mw.loader.using(['mediawiki.util'], function () { let targetUrl = mw.util.getUrl(SPECIAL_PAGE_NAME); if (mw.config.get('wgNamespaceNumber') === 0) { targetUrl += '?dctitle=' + encodeURIComponent(mw.config.get('wgPageName')); } mw.util.addPortletLink( 'p-tb', targetUrl, 'DupCheck (Pro)', 't-dupcheck', 'Find and remove duplicate content using the advanced interface' ); }); if (mw.config.get('wgPageName') === SPECIAL_PAGE_NAME) { mw.loader.using([ 'mediawiki.api', 'mediawiki.util', 'mediawiki.diff.styles' ], loadTool); } } // ========================================== // 2. STATE & CONFIGURATION // ========================================== const State = { title: '', originalWikitext: '', baseRevId: null, lines: [], // Array of line objects stats: { words: 0, lines: 0, paragraphs: 0, sections: 0 }, findings: { lines: [], headings: [], paragraphs: [] }, removedLines: new Set() // Tracks which line indexes are marked for deletion }; const Config = { minLineLength: 10, excludedRegexes: [ /\[\[(Category|বিষয়শ্রেণী):.+?\]\]/gi, /\{\{DEFAULTSORT:.+?\}\}/gi, /\{\{(Reflist|Authority control)[^\}]*\}\}/gi ], excludedPrefixes: ['|-', '|+', '|}', '{|', '|', '!'], isExcluded: function (text) { let normalized = text.trim(); if (!normalized) return true; if (this.excludedPrefixes.some(p => normalized.startsWith(p))) return true; if (/^\{\{.+?\}\}$/.test(normalized)) return true; for (let regex of this.excludedRegexes) { if (regex.test(normalized)) return true; } return false; } }; // ========================================== // 3. ADVANCED ANALYZER ENGINE // ========================================== const Analyzer = { normalize: function(text) { return text.trim().replace(/[ \t]+/g, ' '); }, run: function(wikitext) { const rawLines = wikitext.split('\n'); State.lines = rawLines.map((text, idx) => ({ id: idx, text: text, norm: this.normalize(text), type: 'normal', // normal, orig-line, dup-line, orig-head, dup-head, orig-par, dup-par refId: null, // references the original line ID if it's a duplicate groupId: null // groups paragraphs together })); State.stats.lines = rawLines.length; State.stats.words = wikitext.trim().split(/\s+/).length; State.stats.sections = (wikitext.match(/^={2,6}.+?={2,6}$/gm) || []).length; State.stats.paragraphs = wikitext.split(/\n\n+/).filter(p => p.trim().length > 0).length; State.findings = { lines: [], headings: [], paragraphs: [] }; State.removedLines.clear(); // Maps to track first occurrences const lineMap = new Map(); const headMap = new Map(); const paraMap = new Map(); // Step 1: Detect Headings & Lines for (let i = 0; i < State.lines.length; i++) { let l = State.lines[i]; if (l.norm.length < Config.minLineLength || Config.isExcluded(l.norm)) continue; // Check Heading const isHeading = /^={2,6}\s*(.+?)\s*={2,6}$/.exec(l.norm); if (isHeading) { let headText = isHeading[1]; if (headMap.has(headText)) { let origId = headMap.get(headText); l.type = 'dup-head'; l.refId = origId; State.findings.headings.push({ orig: origId, dup: i, text: headText }); } else { headMap.set(headText, i); l.type = 'orig-head'; } continue; // Skip line check if it's a heading } // Check Line if (lineMap.has(l.norm)) { let origId = lineMap.get(l.norm); // Only mark as line duplicate if it's not already part of a heading/paragraph if(l.type === 'normal') { l.type = 'dup-line'; l.refId = origId; State.findings.lines.push({ orig: origId, dup: i, text: l.norm }); } } else { lineMap.set(l.norm, i); if(l.type === 'normal') l.type = 'orig-line'; } } // Step 2: Detect Paragraph Blocks (Simple contiguous non-empty lines) let currentBlock = []; let blockStart = -1; for (let i = 0; i <= State.lines.length; i++) { let l = State.lines[i] || { norm: '' }; if (l.norm === '') { if (currentBlock.length > 1) { // Multi-line paragraph let blockText = currentBlock.join(' '); if (blockText.length > 50 && !Config.isExcluded(blockText)) { if (paraMap.has(blockText)) { let origStart = paraMap.get(blockText); State.findings.paragraphs.push({ orig: origStart, dup: blockStart, text: blockText }); // Mark all lines in this block for(let j=blockStart; j<=i-1; j++) { State.lines[j].type = 'dup-par'; State.lines[j].refId = origStart; } } else { paraMap.set(blockText, blockStart); for(let j=blockStart; j<=i-1; j++) State.lines[j].type = 'orig-par'; } } } currentBlock = []; } else { if (currentBlock.length === 0) blockStart = i; currentBlock.push(l.norm); } } // Step 3: Cleanup isolated original markers if they have no duplicates const activeOrigs = new Set(); State.lines.forEach(l => { if(l.refId !== null) activeOrigs.add(l.refId); }); State.lines.forEach(l => { if (l.type.startsWith('orig-') && !activeOrigs.has(l.id)) { // It's an original, but no duplicates exist for it. Revert to normal. l.type = 'normal'; } }); } }; // ========================================== // 4. API SERVICE // ========================================== const ApiService = { api: null, init: function() { this.api = new mw.Api(); }, fetchArticle: async function (title) { const response = await this.api.get({ action: 'query', prop: 'revisions', rvprop: 'content|ids', rvslots: 'main', titles: title, formatversion: 2 }); if (response.query.pages[0].missing) throw new Error("Article not found."); return { wikitext: response.query.pages[0].revisions[0].slots.main.content, revid: response.query.pages[0].revisions[0].revid }; }, getDiff: async function (title, baseRevId, modifiedText) { const response = await this.api.post({ action: 'compare', fromtitle: title, fromrev: baseRevId, totext: modifiedText, prop: 'diff', formatversion: 2 }); return response.compare.body; }, saveEdit: async function (title, baseRevId, modifiedText) { await this.api.postWithToken('csrf', { action: 'edit', title: title, text: modifiedText, summary: 'DupCheck: ডুপ্লিকেট কন্টেন্ট পরিষ্কার করা হয়েছে', baserevid: baseRevId, nocreate: true, formatversion: 2 }); } }; // ========================================== // 5. USER INTERFACE (3-COLUMN LAYOUT) // ========================================== const UI = { renderBase: function () { document.title = 'DupCheck Pro'; $('#firstHeading').hide(); // Hide default MW heading for an app-like feel const html = ` <div id="dcp-app"> <!-- COLUMN 1: SIDEBAR LEFT --> <div class="dcp-col dcp-col-left"> <div class="dcp-panel"> <div class="dcp-panel-header">১. পাতার নাম দিন</div> <div class="dcp-panel-body"> <input type="text" id="dcp-title-input" class="dcp-input" placeholder="বাংলা ভাষা..." /> <button id="dcp-btn-load" class="dcp-btn dcp-btn-primary" style="width:100%; margin-top:10px;"> <svg style="width:16px;height:16px;vertical-align:middle;margin-right:5px" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2v4..."></path></svg> লোড করুন </button> <button id="dcp-btn-reset" class="dcp-btn dcp-btn-default" style="width:100%; margin-top:5px;">↻ রিসেট</button> </div> </div> <div class="dcp-panel"> <div class="dcp-panel-header">ⓘ তথ্য</div> <div class="dcp-panel-body dcp-stats-grid" id="dcp-stats-box"> <div>পাতার নাম:</div> <div id="st-name">-</div> <div>রিভিশন আইডি:</div> <div id="st-rev">-</div> <div>লাইনের সংখ্যা:</div> <div id="st-lines">0</div> <div>শব্দের সংখ্যা:</div> <div id="st-words">0</div> <div>অনুচ্ছেদের সংখ্যা:</div> <div id="st-pars">0</div> <div>সেকশনের সংখ্যা:</div> <div id="st-secs">0</div> </div> </div> <div class="dcp-panel"> <div class="dcp-panel-header">🖍 হাইলাইট সেটিংস</div> <div class="dcp-panel-body dcp-chk-list"> <label><input type="checkbox" id="hl-orig" checked> <span class="dcp-swatch bg-blue"></span> মূল (প্রথম) পংক্তি দেখান</label> <label><input type="checkbox" id="hl-line" checked> <span class="dcp-swatch bg-red"></span> ডুপ্লিকেট পংক্তি দেখান</label> <label><input type="checkbox" id="hl-head" checked> <span class="dcp-swatch bg-orange"></span> ডুপ্লিকেট শিরোনাম দেখান</label> <label><input type="checkbox" id="hl-para" checked> <span class="dcp-swatch bg-purple"></span> ডুপ্লিকেট অনুচ্ছেদ দেখান</label> </div> </div> </div> <!-- COLUMN 2: CENTER EDITOR --> <div class="dcp-col dcp-col-center"> <div class="dcp-editor-header"> <div style="font-weight:bold; font-size:15px;">২. উৎস কোড (Source)</div> <div style="display:flex; align-items:center; gap:15px;"> <span id="dcp-total-issues" style="color:#666; font-size:13px;">মোট 0 টি সমস্যা পাওয়া গেছে</span> <button id="dcp-btn-reanalyze" class="dcp-btn dcp-btn-default dcp-btn-sm">↻ পুনরায় বিশ্লেষণ করুন</button> </div> </div> <div class="dcp-toolbar"> <label><input type="checkbox" checked id="tb-linenums"> লাইন নম্বর</label> <input type="text" class="dcp-input" style="padding:4px 8px; width:150px; font-size:12px;" placeholder="খুঁজুন..." /> <div style="flex:1"></div> <button class="dcp-btn dcp-btn-default dcp-btn-sm" title="Full Screen">⛶</button> </div> <!-- THE VIRTUAL EDITOR --> <div id="dcp-editor-container" class="dcp-editor"> <div id="dcp-welcome-text" style="text-align:center; padding:100px 20px; color:#888;"> বাম পাশ থেকে একটি পাতার নাম দিয়ে "লোড করুন" বাটনে ক্লিক করুন। </div> </div> <!-- DIFF PREVIEW AREA (Hidden by default) --> <div id="dcp-preview-container" class="dcp-editor" style="display:none; padding:15px; background:#fff;"></div> <div class="dcp-editor-footer"> <button id="dcp-btn-preview" class="dcp-btn dcp-btn-default">👁 পরিবর্তন পূর্বালোকন (Preview)</button> <button id="dcp-btn-save" class="dcp-btn dcp-btn-success" disabled>☁ উইকিপিডিয়ায় সংরক্ষণ করুন (Save)</button> </div> <div class="dcp-notice-warning"> ⚠ দষ্টব্য: কোনো পরিবর্তন সংরক্ষণ করার আগে পূর্বালোকন দেখুন। পরিবর্তন উইকিপিডিয়ায় প্রকাশিত হবে। </div> </div> <!-- COLUMN 3: RIGHT SIDEBAR ISSUES --> <div class="dcp-col dcp-col-right"> <div class="dcp-panel"> <div class="dcp-panel-header" style="display:flex; justify-content:space-between;"> <span>৩. সমস্যার তালিকা</span> <span>▤</span> </div> <div class="dcp-panel-body" id="dcp-issues-list" style="padding:0; overflow-y:auto; max-height:80vh;"> <div style="padding:20px; text-align:center; color:#888; font-size:13px;">তথ্য লোড করা হয়নি</div> </div> </div> </div> </div> `; $('#mw-content-text').empty().html(html); this.injectStyles(); this.bindEvents(); }, injectStyles: function () { if ($('#dcp-styles').length) return; $('<style id="dcp-styles">').text(` #dcp-app { display: grid; grid-template-columns: 280px 1fr 300px; gap: 15px; height: 85vh; min-height: 600px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 14px; background: #eaecf0; padding: 15px; border-radius: 4px; color: #202122; box-sizing: border-box; } @media (max-width: 1100px) { #dcp-app { grid-template-columns: 250px 1fr 250px; } } @media (max-width: 850px) { #dcp-app { grid-template-columns: 1fr; height: auto; } } .dcp-col { display: flex; flex-direction: column; gap: 15px; height: 100%; overflow: hidden; } .dcp-col-left { overflow-y: auto; } .dcp-col-right { background: #fff; border: 1px solid #c8ccd1; border-radius: 4px; overflow:hidden;} /* Panels */ .dcp-panel { background: #fff; border: 1px solid #c8ccd1; border-radius: 4px; } .dcp-panel-header { font-weight: bold; padding: 12px 15px; border-bottom: 1px solid #eaecf0; background: #f8f9fa; font-size: 15px; color: #000; } .dcp-panel-body { padding: 15px; } /* Inputs & Buttons */ .dcp-input { width: 100%; padding: 8px 12px; border: 1px solid #a2a9b1; border-radius: 4px; box-sizing: border-box; font-family: inherit; } .dcp-btn { padding: 8px 15px; border: 1px solid transparent; border-radius: 4px; font-weight: bold; cursor: pointer; transition: 0.2s; font-size: 14px; display:inline-flex; align-items:center; justify-content:center; } .dcp-btn-primary { background: #1a5099; color: #fff; } .dcp-btn-primary:hover { background: #123e7a; } .dcp-btn-default { background: #f8f9fa; color: #202122; border-color: #a2a9b1; } .dcp-btn-default:hover { background: #eaf3ff; border-color:#36c; } .dcp-btn-success { background: #1e7e34; color: #fff; } .dcp-btn-success:hover { background: #155d27; } .dcp-btn-sm { padding: 4px 10px; font-size: 12px; } .dcp-btn:disabled { opacity: 0.5; cursor: not-allowed; } /* Left Column Stats */ .dcp-stats-grid { display: grid; grid-template-columns: auto 1fr; gap: 8px 15px; font-size: 13px; color: #555; } .dcp-stats-grid div:nth-child(even) { font-weight: bold; color: #000; text-align: right; } .dcp-chk-list label { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; font-size: 13px; cursor: pointer; } .dcp-swatch { display: inline-block; width: 12px; height: 12px; border-radius: 2px; } .bg-blue { background: #dce9fa; border:1px solid #36c; } .bg-red { background: #fee7e6; border:1px solid #d33; } .bg-orange { background: #ffeadd; border:1px solid #ff8000; } .bg-purple { background: #f3e6ff; border:1px solid #8b008b; } /* Center Column Editor Layout */ .dcp-editor-header { display: flex; justify-content: space-between; align-items: center; background: #fff; padding: 12px 15px; border: 1px solid #c8ccd1; border-bottom: none; border-radius: 4px 4px 0 0; } .dcp-toolbar { display: flex; gap: 10px; align-items: center; background: #f8f9fa; padding: 8px 15px; border: 1px solid #c8ccd1; border-bottom: none; font-size: 13px; } .dcp-editor { flex: 1; background: #fff; border: 1px solid #c8ccd1; overflow-y: auto; font-family: monospace; font-size: 13.5px; position: relative; } .dcp-editor-footer { display: flex; justify-content: center; gap: 15px; padding: 15px; background: #f8f9fa; border: 1px solid #c8ccd1; border-top: none; } .dcp-notice-warning { background: #fff8e6; color: #b28a00; border: 1px solid #fbe7b3; padding: 10px; text-align: center; font-size: 12px; border-radius: 0 0 4px 4px; font-weight: bold;} /* Editor Line Styling */ .dcp-line { display: flex; border-bottom: 1px solid transparent; min-height: 24px; transition: background 0.1s;} .dcp-line:hover { background: #f8f9fa; border-bottom-color: #eaecf0; } .dcp-line-num { width: 45px; flex-shrink: 0; background: #f8f9fa; color: #a2a9b1; text-align: right; padding-right: 10px; user-select: none; border-right: 1px solid #eaecf0; padding-top: 3px; font-family: sans-serif; font-size: 11px;} .dcp-line-text { flex: 1; padding: 3px 10px; white-space: pre-wrap; word-break: break-all; } .dcp-line-action { width: 30px; display: flex; align-items: center; justify-content: center; flex-shrink:0; } .dcp-btn-del { width: 20px; height: 20px; background: #d33; color: white; border: none; border-radius: 2px; cursor: pointer; font-weight: bold; line-height: 1; padding: 0; display: none; } .dcp-btn-del:hover { background: #b32424; } .dcp-line:hover .dcp-btn-del { display: block; } /* Highlighting Classes */ .hl-orig .dcp-line-text { background: #eaf3ff; } .hl-line-dup .dcp-line-text { background: #fee7e6; } .hl-head-dup .dcp-line-text { background: #ffeadd; } .hl-para-dup .dcp-line-text { background: #f3e6ff; } .dcp-removed .dcp-line-text { text-decoration: line-through; opacity: 0.5; background: #f8f9fa !important; } .dcp-removed .dcp-btn-del { display: block; background: #a2a9b1; } /* Right Column Accordion */ .dcp-accordion { border-bottom: 1px solid #eaecf0; } .dcp-acc-header { padding: 12px 15px; font-weight: bold; cursor: pointer; display: flex; justify-content: space-between; background: #fff; font-size:13px; transition: 0.2s;} .dcp-acc-header:hover { background: #f8f9fa; } .dcp-acc-body { display: none; padding: 0; background: #fafbfc; } /* List Items in Right Col */ .dcp-issue-item { padding: 10px 15px; border-bottom: 1px solid #eaecf0; font-size: 12px; cursor: pointer; transition: 0.2s;} .dcp-issue-item:hover { background: #fff; border-left: 3px solid #36c; padding-left: 12px;} .dcp-issue-title { font-weight: bold; color: #202122; margin-bottom: 4px; } .dcp-issue-sub { color: #72777d; } /* Dynamic Toggle Hiding */ body.hide-orig .hl-orig .dcp-line-text { background: transparent; } body.hide-line .hl-line-dup .dcp-line-text { background: transparent; } body.hide-head .hl-head-dup .dcp-line-text { background: transparent; } body.hide-para .hl-para-dup .dcp-line-text { background: transparent; } body.hide-nums .dcp-line-num { display: none; } `).appendTo('head'); }, bindEvents: function () { $('#dcp-btn-load').on('click', Controller.loadArticle); $('#dcp-btn-reset').on('click', () => location.reload()); $('#dcp-btn-reanalyze').on('click', Controller.loadArticle); // Editor line deletion toggle $('#dcp-app').on('click', '.dcp-btn-del', function() { const lineId = $(this).data('id'); const lineEl = $('#line-' + lineId); if (State.removedLines.has(lineId)) { State.removedLines.delete(lineId); lineEl.removeClass('dcp-removed'); $(this).html('X'); } else { State.removedLines.add(lineId); lineEl.addClass('dcp-removed'); $(this).html('↺'); } Controller.updateSaveButton(); }); // Accordion Toggles $('#dcp-app').on('click', '.dcp-acc-header', function() { $(this).next('.dcp-acc-body').slideToggle(200); }); // Issue click to scroll $('#dcp-app').on('click', '.dcp-issue-item', function() { const targetId = $(this).data('target'); const targetEl = $('#line-' + targetId); if (targetEl.length) { $('#dcp-editor-container').animate({ scrollTop: $('#dcp-editor-container').scrollTop() + targetEl.position().top - 100 }, 400); targetEl.css('background', '#fff3cd').animate({backgroundColor: 'transparent'}, 1000); } }); // Toggles $('#hl-orig').on('change', function() { $('body').toggleClass('hide-orig', !this.checked); }); $('#hl-line').on('change', function() { $('body').toggleClass('hide-line', !this.checked); }); $('#hl-head').on('change', function() { $('body').toggleClass('hide-head', !this.checked); }); $('#hl-para').on('change', function() { $('body').toggleClass('hide-para', !this.checked); }); $('#tb-linenums').on('change', function() { $('body').toggleClass('hide-nums', !this.checked); }); // Preview & Save $('#dcp-btn-preview').on('click', Controller.generatePreview); $('#dcp-btn-save').on('click', Controller.saveChanges); }, updateStats: function() { $('#st-name').text(State.title || '-'); $('#st-rev').text(State.baseRevId || '-'); $('#st-lines').text(State.stats.lines.toLocaleString('bn-BD')); $('#st-words').text(State.stats.words.toLocaleString('bn-BD')); $('#st-pars').text(State.stats.paragraphs.toLocaleString('bn-BD')); $('#st-secs').text(State.stats.sections.toLocaleString('bn-BD')); let totalIssues = State.findings.lines.length + State.findings.headings.length + State.findings.paragraphs.length; $('#dcp-total-issues').text(`মোট ${totalIssues.toLocaleString('bn-BD')} টি সমস্যা পাওয়া গেছে`); // Reset toggles to checked $('#hl-orig, #hl-line, #hl-head, #hl-para').prop('checked', true); $('body').removeClass('hide-orig hide-line hide-head hide-para'); }, renderEditor: function() { let htmlFragments = []; State.lines.forEach((l) => { let hlClass = ''; let btnHtml = ''; if (l.type.startsWith('orig-')) hlClass = 'hl-orig'; else if (l.type === 'dup-line') { hlClass = 'hl-line-dup'; btnHtml = `<button class="dcp-btn-del" data-id="${l.id}" title="মুছুন">X</button>`; } else if (l.type === 'dup-head') { hlClass = 'hl-head-dup'; btnHtml = `<button class="dcp-btn-del" data-id="${l.id}" title="মুছুন">X</button>`; } else if (l.type === 'dup-par') { hlClass = 'hl-para-dup'; btnHtml = `<button class="dcp-btn-del" data-id="${l.id}" title="মুছুন">X</button>`; } htmlFragments.push(` <div id="line-${l.id}" class="dcp-line ${hlClass}"> <div class="dcp-line-num">${l.id + 1}</div> <div class="dcp-line-text">${mw.html.escape(l.text) || ' '}</div> <div class="dcp-line-action">${btnHtml}</div> </div> `); }); $('#dcp-editor-container').html(htmlFragments.join('')); $('#dcp-preview-container').hide(); $('#dcp-editor-container').show(); $('#dcp-btn-preview').text('👁 পরিবর্তন পূর্বালোকন (Preview)').removeClass('dcp-btn-primary').addClass('dcp-btn-default'); }, renderRightSidebar: function() { let html = ''; // Helper to build accordion sections const buildSection = (title, count, items, colorCode, emptyMsg) => { let colorHex = colorCode === 'red' ? '#d33' : colorCode === 'orange' ? '#ff8000' : '#8b008b'; let icon = colorCode === 'red' ? '🔴' : colorCode === 'orange' ? '🟠' : '🟣'; html += `<div class="dcp-accordion"> <div class="dcp-acc-header" style="color:${colorHex}"> <span>${icon} ${title} (${count})</span> <span>▼</span> </div> <div class="dcp-acc-body">`; if (count === 0) { html += `<div style="padding:15px; color:#888; font-size:12px; text-align:center;">${emptyMsg}</div>`; } else { items.forEach((item, idx) => { let shortText = item.text.length > 25 ? item.text.substring(0, 25) + '...' : item.text; html += ` <div class="dcp-issue-item" data-target="${item.dup}"> <div class="dcp-issue-title">#${idx + 1} লাইন ${item.dup + 1}</div> <div class="dcp-issue-sub">ডুপ্লিকেট: লাইন ${item.orig + 1}</div> <div class="dcp-issue-sub" style="font-family:monospace; margin-top:3px;">"${mw.html.escape(shortText)}"</div> </div>`; }); } html += `</div></div>`; }; buildSection('ডুপ্লিকেট লাইন', State.findings.lines.length, State.findings.lines, 'red', 'কোনো ডুপ্লিকেট লাইন নেই'); buildSection('ডুপ্লিকেট শিরোনাম', State.findings.headings.length, State.findings.headings, 'orange', 'কোনো ডুপ্লিকেট শিরোনাম নেই'); buildSection('ডুপ্লিকেট অনুচ্ছেদ', State.findings.paragraphs.length, State.findings.paragraphs, 'purple', 'কোনো ডুপ্লিকেট অনুচ্ছেদ নেই'); $('#dcp-issues-list').html(html); }, renderDiff: function (diffHtml) { $('#dcp-editor-container').hide(); $('#dcp-preview-container').html(` <h3 style="margin-top:0; border-bottom:1px solid #ccc; padding-bottom:10px;">পরিবর্তন পূর্বালোকন</h3> <table class="diff" style="width:100%; border-collapse:collapse;"> <colgroup><col class="diff-marker"><col class="diff-content"><col class="diff-marker"><col class="diff-content"></colgroup> <tbody>${diffHtml || '<tr><td colspan="4" style="text-align:center; padding: 20px;">কোনো পরিবর্তন পাওয়া যায়নি।</td></tr>'}</tbody> </table> `).show(); // Toggle button state $('#dcp-btn-preview').text('⬅ সম্পাদনায় ফিরে যান').removeClass('dcp-btn-default').addClass('dcp-btn-primary'); } }; // ========================================== // 6. CONTROLLER LOGIC // ========================================== const Controller = { loadArticle: async function () { const title = $('#dcp-title-input').val().trim(); if (!title) return alert('অনুগ্রহ করে একটি পাতার নাম দিন।'); $('#dcp-btn-load').prop('disabled', true).text('লোড হচ্ছে...'); $('#dcp-welcome-text').text('তথ্য সংগ্রহ ও বিশ্লেষণ করা হচ্ছে...'); try { const data = await ApiService.fetchArticle(title); State.title = title; State.originalWikitext = data.wikitext; State.baseRevId = data.revid; Analyzer.run(State.originalWikitext); UI.updateStats(); UI.renderEditor(); UI.renderRightSidebar(); } catch (err) { $('#dcp-editor-container').html(`<div style="padding:40px; color:#d33; text-align:center;"><b>ত্রুটি:</b> ${err.message}</div>`); } finally { $('#dcp-btn-load').prop('disabled', false).html('<svg style="width:16px;height:16px;vertical-align:middle;margin-right:5px" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2v4..."></path></svg> লোড করুন'); } }, updateSaveButton: function() { if(State.removedLines.size > 0) { $('#dcp-btn-save').prop('disabled', false); } else { $('#dcp-btn-save').prop('disabled', true); } }, generatePreview: async function () { // If currently showing preview, switch back to editor if($('#dcp-preview-container').is(':visible')) { $('#dcp-preview-container').hide(); $('#dcp-editor-container').show(); $('#dcp-btn-preview').text('👁 পরিবর্তন পূর্বালোকন (Preview)').removeClass('dcp-btn-primary').addClass('dcp-btn-default'); return; } if (State.removedLines.size === 0) return alert('আপনি কোনো ডুপ্লিকেট মুছতে নির্বাচন করেননি।'); // Generate modified text by skipping removed lines let modLines = []; for(let i=0; i<State.lines.length; i++) { if(!State.removedLines.has(i)) { modLines.push(State.lines[i].text); } } let modifiedText = modLines.join('\n'); $('#dcp-btn-preview').prop('disabled', true).text('প্রস্তুত করা হচ্ছে...'); try { const diffHtml = await ApiService.getDiff(State.title, State.baseRevId, modifiedText); UI.renderDiff(diffHtml); } catch (err) { alert(`ত্রুটি: ${err.message}`); } finally { $('#dcp-btn-preview').prop('disabled', false); } }, saveChanges: async function () { if (State.removedLines.size === 0) return; if (!confirm(`আপনি কি নিশ্চিত যে আপনি ${State.title} পাতায় এই পরিবর্তনগুলো সংরক্ষণ করতে চান?`)) return; let modLines = []; for(let i=0; i<State.lines.length; i++) { if(!State.removedLines.has(i)) modLines.push(State.lines[i].text); } let modifiedText = modLines.join('\n'); $('#dcp-btn-save').prop('disabled', true).text('সংরক্ষণ হচ্ছে...'); try { await ApiService.saveEdit(State.title, State.baseRevId, modifiedText); alert('সফলভাবে সংরক্ষণ করা হয়েছে!'); window.location.href = mw.util.getUrl(State.title); } catch (err) { alert(`সংরক্ষণ করতে সমস্যা হয়েছে: ${err.message}`); $('#dcp-btn-save').prop('disabled', false).text('☁ উইকিপিডিয়ায় সংরক্ষণ করুন (Save)'); } } }; function loadTool() { ApiService.init(); UI.renderBase(); const urlTitle = mw.util.getParamValue('dctitle'); if (urlTitle) { $('#dcp-title-input').val(urlTitle.replace(/_/g, ' ')); setTimeout(() => Controller.loadArticle(), 300); } } $(document).ready(init); })(mediaWiki, jQuery); oah06xmkjzqocw8ibn5otlqxnp753rs Dw31415 First Test Article 0 169391 747142 746438 2026-06-17T00:03:17Z InternetArchiveBot 34092 Rescuing 1 sources and tagging 0 as dead.) #IABot (v2.0.9.5 747142 wikitext text/x-wiki A test article to run a bot against. == References == Test bot clean up of AT. ===Bibliography=== * Maurer, Maurer. ''[https://media.defense.gov/2010/Dec/02/2001329899/-1/-1/0/AFD-101202-002.pdf Combat Squadrons of the Air Force: World War II] {{Webarchive|url=https://web.archive.org/web/20180928223633/https://media.defense.gov/2010/Dec/02/2001329899/-1/-1/0/AFD-101202-002.pdf |date=2018-09-28 }} <!-- CUTLASSBOT-AT: https://archive.today/20230820144531/https://media.defense.gov/2010/Dec/02/2001329899/-1/-1/0/AFD-101202-002.pdf -->''. [[Maxwell Air Force Base]], Alabama: Office of Air Force History, 1982. Updated 4v3cce3ejhs7pudcae840nc56g4j0c8 User:Laerdon/ConvoWizard.js 2 172222 747135 745131 2026-06-16T19:15:18Z Laerdon 70887 747135 javascript text/javascript //<nowiki> /** * Filename: wiki-talk-page-script.js * ConvoWizard – Real-time Reply Feedback (Wikipedia userscript) * Runs on Talk pages inside DiscussionTools reply widgets. * Prompts for a token on first run; persists after authorization. */ (async function () { 'use strict'; // Load core util module via ResourceLoader await mw.loader.using(['mediawiki.util', 'mediawiki.user', 'mediawiki.api']); // === CONFIG === const SERVER = 'https://convocompass.toolforge.org/api/'; // ---- UI strings and thresholds ---- const NAME = 'ConvoCompass'; const DEBUG = /(?:\?|&)convowizardDebug=1(?:&|$)/.test(location.search); const OPTION_KEY = 'userjs-convowizard-token'; const SCORE_CHANGE_THRESH = 0.08; const MID_TENSION_THRESH = 0.50; const HIGH_TENSION_THRESH = 0.75; const CONTEXT_LOW = `${NAME} will notify you here if it detects anything in the preceding conversation.`; const CONTEXT_MID = `${NAME} thinks this discussion is getting somewhat tense - some other discussions that started like this one ended up with comments getting removed. Remember that you will be most likely to have a productive discussion with a civil, respectful, and open approach.`; const CONTEXT_HIGH = `${NAME} thinks this discussion is getting tense - some other discussions that started like this one ended up with comments getting removed. Remember that you will be most likely to have a productive discussion with a civil, respectful, and open approach.`; const CONTEXT_VERY_HIGH = `${NAME} thinks this discussion is getting very tense - some other discussions that started like this one ended up with comments getting removed. Remember that you will be most likely to have a productive discussion with a civil, respectful, and open approach.`; const REPLY_LOW = `${NAME} thinks this comment might decrease tension in this discussion.`; const REPLY_MID = `${NAME} will notify you here if it detects anything in your comment draft.`; const REPLY_HIGH = `${NAME} thinks this comment might increase the tension in this discussion. Remember that you will be most likely to have a productive discussion with a civil, respectful, and open approach.`; const TENSION_COLORS = { mid: { border: '#e8825c', bg: 'linear-gradient(135deg, #fff0eb 0%, #fff8f5 100%)', icon: '#e8825c', inputBg: 'rgba(232,130,92,0.05)' }, high: { border: '#d73027', bg: 'linear-gradient(135deg, #ffeaea 0%, #fff5f5 100%)', icon: '#d73027', inputBg: 'rgba(215,48,39,0.05)' }, veryHigh: { border: '#a50f15', bg: 'linear-gradient(135deg, #fdd 0%, #fee 100%)', icon: '#a50f15', inputBg: 'rgba(165,15,21,0.07)' }, }; // 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); 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 ---------- function throttle(fn, waitMs) { let last = 0, timer = 0, pendingArgs = null; return (...args) => { const now = Date.now(); const remaining = waitMs - (now - last); if (remaining <= 0) { last = now; fn(...args); } else { pendingArgs = args; clearTimeout(timer); timer = setTimeout(() => { last = Date.now(); fn(...pendingArgs); }, remaining); } }; } // 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(); 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}] 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'; form.style.cssText = ` margin:8px 0;padding:16px 20px;border-radius:8px;border:2px solid #0645ad; background:linear-gradient(135deg,#f8f9fa 0%,#ffffff 100%); box-shadow:0 2px 8px rgba(0,0,0,.1); font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif; `; const title = document.createElement('div'); title.style.cssText = 'font-weight:600;font-size:14px;color:#0645ad;margin-bottom:8px;'; title.textContent = `${NAME}: Authorization Required`; const desc = document.createElement('div'); desc.style.cssText = 'font-size:13px;color:#333;margin-bottom:12px;line-height:1.4;'; desc.textContent = `Please paste your ${NAME} token below to activate the extension.`; const row = document.createElement('div'); row.style.cssText = 'display:flex;gap:8px;align-items:center;'; const input = document.createElement('input'); input.type = 'password'; input.placeholder = 'Paste token here'; input.autocomplete = 'off'; input.spellcheck = false; input.style.cssText = ` flex:1;padding:8px 10px;font-size:13px;border:1px solid #a2a9b1;border-radius:4px; font-family:monospace;outline:none;transition:border-color 0.2s ease; `; const btn = document.createElement('button'); btn.type = 'button'; btn.textContent = 'Authorize'; btn.style.cssText = ` padding:8px 16px;font-size:13px;font-weight:600;border:none;border-radius:4px; background:#0645ad;color:#fff;cursor:pointer;transition:background 0.2s ease; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif; `; btn.addEventListener('mouseenter', () => { btn.style.background = '#0b5cce'; }); btn.addEventListener('mouseleave', () => { btn.style.background = '#0645ad'; }); const errDiv = document.createElement('div'); errDiv.style.cssText = 'font-size:12px;color:#d73027;margin-top:8px;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(); } // ---- 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 ensureUnmuteIndicator(widgetEl, threadId, widgetId) { const existingIndicator = widgetEl.querySelector('.convowizard-unmute-indicator'); if (existingIndicator) return; const indicator = document.createElement('div'); indicator.id = `convowizard-unmute-${widgetId}`; indicator.className = 'convowizard-unmute-indicator'; indicator.style.cssText = ` margin: 4px 0; padding: 6px 10px; font-size: 11px; border: 1px solid #a2a9b1; border-radius: 4px; background: #f8f9fa; color: #54595d; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; display: flex; align-items: center; gap: 6px; cursor: pointer; transition: all 0.2s ease; `; const icon = document.createElement('span'); icon.textContent = '🔊'; icon.style.fontSize = '12px'; const text = document.createElement('span'); text.textContent = `${NAME} is muted for this thread. Click to unmute.`; text.style.flex = '1'; indicator.appendChild(icon); indicator.appendChild(text); indicator.addEventListener('mouseenter', () => { indicator.style.background = '#e9ecef'; indicator.style.borderColor = '#0645ad'; }); indicator.addEventListener('mouseleave', () => { indicator.style.background = '#f8f9fa'; indicator.style.borderColor = '#a2a9b1'; }); 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 => { btn.textContent = '🔇 Mute thread'; btn.title = 'Mute ConvoWizard for this thread'; btn.style.background = '#ffffff'; }); // 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 contextUtterances = st.context.split(/\s*\(UTC\)\s*/).filter(text => text.trim().length > 0); const existingForContinue = contextUtterances.map((text, index) => ({ id: `wiki_${st.interactionId}_${index}`, text: text.trim() })); const data = await postJson('continue', { existing: existingForContinue, new: inputText, interaction_id: st.interactionId, token: TOKEN, username: USERNAME }); if (data && !data.error) { handleIntervention(widgetEl, st.inputEl, data); } } else { const contextUtterances = st.context.split(/\s*\(UTC\)\s*/).filter(text => text.trim().length > 0); const existing = contextUtterances.map((text, index) => ({ id: `wiki_${st.interactionId}_${index}`, text: text.trim() })); 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 }); 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'; btn.setAttribute('data-thread-id', threadId); btn.setAttribute('data-widget-id', widgetId); btn.style.cssText = ` margin-left: auto; padding: 4px 8px; font-size: 11px; border: 1px solid #a2a9b1; border-radius: 4px; background: ${isMuted ? '#f8f9fa' : '#ffffff'}; color: #54595d; cursor: pointer; transition: all 0.2s ease; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; `; btn.textContent = isMuted ? '🔊 Unmute thread' : '🔇 Mute thread'; btn.title = isMuted ? 'Unmute ConvoWizard for this thread' : 'Mute ConvoWizard for this thread'; btn.addEventListener('mouseenter', () => { btn.style.background = isMuted ? '#e9ecef' : '#f8f9fa'; }); btn.addEventListener('mouseleave', () => { btn.style.background = isMuted ? '#f8f9fa' : '#ffffff'; }); btn.addEventListener('click', (e) => { e.stopPropagation(); e.preventDefault(); const newMuteState = toggleThreadMute(threadId); // Update button state btn.textContent = newMuteState ? '🔊 Unmute thread' : '🔇 Mute thread'; btn.title = newMuteState ? 'Unmute ConvoWizard for this thread' : 'Mute ConvoWizard for this thread'; btn.style.background = newMuteState ? '#f8f9fa' : '#ffffff'; // 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) { otherBtn.textContent = newMuteState ? '🔊 Unmute thread' : '🔇 Mute thread'; otherBtn.title = newMuteState ? 'Unmute ConvoWizard for this thread' : 'Mute ConvoWizard for this thread'; otherBtn.style.background = newMuteState ? '#f8f9fa' : '#ffffff'; } }); }); 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'; contextBox.style.cssText = ` margin:8px 0;padding:12px 16px;border-radius:8px;border:2px solid #0645ad; background:linear-gradient(135deg,#f8f9fa 0%,#ffffff 100%); box-shadow:0 2px 8px rgba(0,0,0,.1);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;transition:all .3s ease;position:relative; `; const cheader = document.createElement('div'); cheader.id = `${contextId}_h`; cheader.style.cssText = `font-weight:600;font-size:14px;color:#0645ad;margin-bottom:8px;display:flex;align-items:center;gap:8px;`; const iconSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); iconSvg.setAttribute('width', '16'); iconSvg.setAttribute('height', '16'); iconSvg.setAttribute('viewBox', '0 0 16 16'); iconSvg.setAttribute('fill', 'currentColor'); iconSvg.innerHTML = '<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm1 13H7v-2h2v2zm0-3H7V4h2v6z"/>'; const textSpan = document.createElement('span'); textSpan.className = 'convowizard-header-text'; textSpan.textContent = `${NAME}: Context Summary`; cheader.appendChild(iconSvg); cheader.appendChild(textSpan); if (threadId) { const muteBtn = createMuteButton(threadId, widgetId, isMuted); cheader.appendChild(muteBtn); } const ccontent = document.createElement('div'); ccontent.id = `${contextId}_p`; ccontent.style.cssText = `font-size:13px;line-height:1.4;color:#333;margin:0;`; ccontent.textContent = `When you type a reply ${NAME} will give you some feedback on the discussion context.`; contextBox.appendChild(cheader); contextBox.appendChild(ccontent); 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'; replyBox.style.cssText = ` margin:8px 0;padding:12px 16px;border-radius:8px;border:2px solid #0645ad; background:linear-gradient(135deg,#f8f9fa 0%,#ffffff 100%); box-shadow:0 2px 8px rgba(0,0,0,.1);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;transition:all .3s ease;position:relative; `; const header = document.createElement('div'); header.id = `${replyId}_h`; header.style.cssText = `font-weight:600;font-size:14px;color:#0645ad;margin-bottom:8px;display:flex;align-items:center;gap:8px;`; const iconSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); iconSvg.setAttribute('width', '16'); iconSvg.setAttribute('height', '16'); iconSvg.setAttribute('viewBox', '0 0 16 16'); iconSvg.setAttribute('fill', 'currentColor'); iconSvg.innerHTML = '<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm1 13H7v-2h2v2zm0-3H7V4h2v6z"/>'; const textSpan = document.createElement('span'); textSpan.className = 'convowizard-header-text'; textSpan.textContent = `${NAME}: Reply Summary`; header.appendChild(iconSvg); header.appendChild(textSpan); if (threadId) { const muteBtn = createMuteButton(threadId, widgetId, isMuted); header.appendChild(muteBtn); } const content = document.createElement('div'); content.id = `${replyId}_p`; content.style.cssText = `font-size:13px;line-height:1.4;color:#333;margin:0;`; content.textContent = `When you type a reply ${NAME} will give you some feedback on your comment.`; replyBox.appendChild(header); replyBox.appendChild(content); 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 }; } function updateContextPanel(contextId, score, showScores) { console.log(`[${NAME}] updateContextPanel called - contextId: ${contextId}, score: ${score}, showScores: ${showScores}`); const hasScore = typeof score === 'number'; let text = showScores && hasScore ? `Context craft score: ${score.toFixed(4)} ` : ''; let borderColor = '#0645ad'; let bgGradient = 'linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%)'; let iconColor = '#0645ad'; let icon = `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"> <path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm1 13H7v-2h2v2zm0-3H7V4h2v6z"/> </svg>`; console.log(`[${NAME}] hasScore: ${hasScore}, score value: ${score}`); console.log(`[${NAME}] Thresholds - MID: ${MID_TENSION_THRESH}, HIGH: ${HIGH_TENSION_THRESH}`); if (hasScore && score > MID_TENSION_THRESH) { let level; if (score > HIGH_TENSION_THRESH) { console.log(`[${NAME}] Very high tension (score > ${HIGH_TENSION_THRESH})`); text += `${CONTEXT_VERY_HIGH}`; level = 'veryHigh'; } else if (score > 0.60) { console.log(`[${NAME}] High tension (score > 0.60)`); text += `${CONTEXT_HIGH}`; level = 'high'; } else { console.log(`[${NAME}] Mid tension (score > ${MID_TENSION_THRESH})`); text += `${CONTEXT_MID}`; level = 'mid'; } borderColor = TENSION_COLORS[level].border; bgGradient = TENSION_COLORS[level].bg; iconColor = TENSION_COLORS[level].icon; icon = `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"> <path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zM7 4h2v5H7V4zm0 6h2v2H7v-2z"/> </svg>`; } else { console.log(`[${NAME}] Low tension or no score`); text += `${CONTEXT_LOW}`; } const box = document.getElementById(`${contextId}_d`); const p = document.getElementById(`${contextId}_p`); const h = document.getElementById(`${contextId}_h`); console.log(`[${NAME}] DOM elements - box: ${!!box}, p: ${!!p}, h: ${!!h}`); if (box) { 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; } } box.style.borderColor = borderColor; box.style.background = bgGradient; box.style.boxShadow = `0 2px 8px ${borderColor}20`; if (box.style.display === 'none' && (!muteBtn || !isThreadMuted(muteBtn.getAttribute('data-thread-id')))) { box.style.display = ''; } } if (p) { p.textContent = text; console.log(`[${NAME}] Updated text content (length: ${text.length})`); } if (h) { h.style.color = iconColor; // Update the SVG icon const existingSvg = h.querySelector('svg:not(.convowizard-mute-btn svg)'); if (existingSvg) { const temp = document.createElement('div'); temp.innerHTML = icon; const newSvg = temp.querySelector('svg'); if (newSvg) { existingSvg.replaceWith(newSvg); } } // Update text span const textSpan = h.querySelector('span.convowizard-header-text'); if (textSpan) { textSpan.textContent = `${NAME}: Context Summary`; } console.log(`[${NAME}] Updated header color: ${iconColor}`); } } function updateReplyPanel(replyId, scoreChange, rawScore, showScores, inputEl) { let text = showScores ? `Change in craft score after your comment: ${scoreChange.toFixed(4)} ` : ''; let borderColor = '#0645ad'; let bgGradient = 'linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%)'; let iconColor = '#0645ad'; let icon = `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"> <path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm1 13H7v-2h2v2zm0-3H7V4h2v6z"/> </svg>`; if (scoreChange > SCORE_CHANGE_THRESH || (rawScore > 0.55 && scoreChange > 0)) { text += `${REPLY_HIGH}`; const level = rawScore > HIGH_TENSION_THRESH ? 'veryHigh' : rawScore > 0.60 ? 'high' : 'mid'; borderColor = TENSION_COLORS[level].border; bgGradient = TENSION_COLORS[level].bg; iconColor = TENSION_COLORS[level].icon; icon = `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"> <path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zM7 4h2v5H7V4zm0 6h2v2H7v-2z"/> </svg>`; } else if (rawScore - scoreChange > 0.55 && scoreChange < -SCORE_CHANGE_THRESH) { text += `${REPLY_LOW}`; borderColor = '#4caf50'; bgGradient = 'linear-gradient(135deg, #f1f8e9 0%, #f9fbe7 100%)'; iconColor = '#4caf50'; icon = `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"> <path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm3.5 6L6.5 11 4 8.5l1-1 1.5 1.5L10.5 5l1 1z"/> </svg>`; } else { text += `${REPLY_MID}`; } if (inputEl) { let threadId = null; const replyBox = document.getElementById(`${replyId}_d`); if (replyBox) { const muteBtn = replyBox.querySelector('.convowizard-mute-btn'); if (muteBtn) { threadId = muteBtn.getAttribute('data-thread-id'); } } // If no thread ID from panel, try to get from widget if (!threadId) { const widgetEl = inputEl.closest('.ext-discussiontools-ui-replyWidget') || inputEl.closest('.ext-discussiontools-ui-newTopicWidget'); if (widgetEl) { threadId = getThreadIdForWidget(widgetEl); } } // If muted, clear background and return if (threadId && isThreadMuted(threadId)) { inputEl.style.setProperty('background-color', 'transparent', 'important'); return; } let inputBg = 'transparent'; if (scoreChange > SCORE_CHANGE_THRESH || (rawScore > 0.55 && scoreChange > 0)) { const level = rawScore > HIGH_TENSION_THRESH ? 'veryHigh' : rawScore > 0.60 ? 'high' : 'mid'; inputBg = TENSION_COLORS[level].inputBg; } else if (scoreChange < -SCORE_CHANGE_THRESH) { inputBg = 'rgba(76,175,80,0.05)'; } inputEl.style.setProperty('background-color', inputBg, 'important'); inputEl.style.setProperty('transition', 'background-color 0.3s ease', 'important'); } const box = document.getElementById(`${replyId}_d`); const p = document.getElementById(`${replyId}_p`); const h = document.getElementById(`${replyId}_h`); if (box) { 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; } } box.style.borderColor = borderColor; box.style.background = bgGradient; box.style.boxShadow = `0 2px 8px ${borderColor}20`; if (box.style.display === 'none' && (!muteBtn || !isThreadMuted(muteBtn.getAttribute('data-thread-id')))) { box.style.display = ''; } } if (p) p.textContent = text; if (h) { h.style.color = iconColor; // Update the SVG icon const existingSvg = h.querySelector('svg:not(.convowizard-mute-btn svg)'); if (existingSvg) { const temp = document.createElement('div'); temp.innerHTML = icon; const newSvg = temp.querySelector('svg'); if (newSvg) { existingSvg.replaceWith(newSvg); } } // Update text span const textSpan = h.querySelector('span.convowizard-header-text'); if (textSpan) { textSpan.textContent = `${NAME}: Reply Summary`; } } } 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' : '')); const context = getContextStringFor(widgetEl); console.log(`[${NAME}] Context extracted, length: ${context.length} chars`); console.log(`[${NAME}] Context preview:`, context.substring(0, 200)); // Split context on (UTC) timestamps to create separate utterances like Reddit does // This prevents backend caching bug where all Wikipedia convos share id=None const utterances = context.split(/\s*\(UTC\)\s*/).filter(text => text.trim().length > 0); const existing = utterances.map((text, index) => ({ id: `wiki_${Date.now()}_${index}`, // Unique ID prevents cache collision text: text.trim() })); console.log(`[${NAME}] Created ${existing.length} utterances with unique IDs`); existing.forEach((utt, i) => { console.log(`[${NAME}] ${i}: ID=${utt.id}, 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 }); 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 }); handleIntervention(widgetEl, inputEl, data); const sendContinue = throttle(async () => { 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 widgetId = widgetEl.id; const h = document.getElementById(`reply_${widgetId}_h`); 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"> <path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm1 13H7v-2h2v2zm0-3H7V4h2v6z"/> </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) { textSpan.textContent = `${NAME}: Reply Summary (Processing...)`; } } // Recreate existing array from cached context for continue requests const contextUtterances = st.context.split(/\s*\(UTC\)\s*/).filter(text => text.trim().length > 0); const existingForContinue = contextUtterances.map((text, index) => ({ id: `wiki_${st.interactionId}_${index}`, // Use interaction_id for consistency text: text.trim() })); const data2 = await postJson('continue', { existing: existingForContinue, new: (st.inputEl.innerText || st.inputEl.textContent || st.inputEl.value || ''), interaction_id: st.interactionId, token: TOKEN, username: USERNAME }); // Handle invalid token on continue if (data2 && data2.error === 'Invalid Token') { TOKEN = null; await persistToken(STORAGE_KEY, null); showTokenForm(widgetEl); return; } handleIntervention(widgetEl, st.inputEl, data2); }, 3000); inputEl.addEventListener('input', sendContinue); inputEl.addEventListener('keyup', sendContinue); inputEl.addEventListener('paste', sendContinue); 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 context for submit const contextUtterances = st.context.split(/\s*\(UTC\)\s*/).filter(text => text.trim().length > 0); const existingForSubmit = contextUtterances.map((text, index) => ({ id: `wiki_${st.interactionId}_${index}`, text: text.trim() })); 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 || '').substring(0, 5); console.log(`[${NAME}] Response type (which): ${which}`); if (which === 'craft') { console.log(`[${NAME}] CRAFT mode - craft_ctx_score:`, data.craft_ctx_score); console.log(`[${NAME}] CRAFT mode - craft_reply_score:`, data.craft_reply_score); console.log(`[${NAME}] CRAFT mode - craft_reply_change:`, data.craft_reply_change); if (data.craft_ctx_score != null) { console.log(`[${NAME}] Updating context panel with score: ${data.craft_ctx_score}`); updateContextPanel(contextId, data.craft_ctx_score, showScores); } else { console.log(`[${NAME}] No context score, updating context panel with null`); updateContextPanel(contextId, null, showScores); } if (data.craft_reply_change != null && data.craft_reply_score != null) { console.log(`[${NAME}] Updating reply panel with change: ${data.craft_reply_change}, score: ${data.craft_reply_score}`); updateReplyPanel(replyId, data.craft_reply_change, data.craft_reply_score, showScores, inputEl); } else { console.log(`[${NAME}] No reply scores, updating reply panel with zeros`); updateReplyPanel(replyId, 0, 0, showScores, inputEl); } } else { const noteId = `passivenote${interactionId}_d`; if (!data.message && !document.getElementById(noteId)) { const box = document.createElement('div'); box.id = noteId; box.className = 'passiveModeDisplay'; box.style.cssText = ` margin:8px 0;padding:8px 12px;border-radius:6px;border:1px solid #a2a9b1; background:linear-gradient(135deg,#f8f9fa 0%,#ffffff 100%); font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;opacity:.8; `; const content = document.createElement('div'); content.id = `passivenote${interactionId}_p`; content.style.cssText = `font-size:12px;color:#54595d;margin:0;font-style:italic;`; 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> ktf65jd9ely50oi8rnarmfzf5vlb5zd MediaWiki:Gadget-section-share.js 8 173991 747149 735209 2026-06-17T02:36:11Z DLynch (WMF) 34247 Remove unnecessary added ` | ` separator; mw-core CSS handles this so long as an a tag is used 747149 javascript text/javascript $('.mw-heading').each( function () { const header = $( this ).find( ':header' ).attr('id'); const share = $('<a>').text('share').on('click', function (event) { event.preventDefault(); let url = new URL( location ); url.hash = header; let link = url.toString(); try { // decodeURI() may throw const decodedLink = decodeURI( link ); // Check that the decoded URL is parsed to the same canonical URL // new URL() may throw if ( new URL( decodedLink ).toString() === link ) { link = decodedLink; } } catch ( err ) {} const shareData = {url: link}; if ( !navigator.canShare || !navigator.canShare( shareData )) { navigator.clipboard.writeText(link); mw.notify( "Link copied to clipboard" ); } else { navigator.share(shareData); } }); $( this ).find('.mw-editsection-bracket').last().before(share); } ); 9w95tyj7alun3dsj3ip1k185r0r37y1 User:Plantaest/sandbox 2 174741 747072 741644 2026-06-16T17:42:40Z Plantaest 37055 747072 wikitext text/x-wiki Quy chế Họp mặt ---- = Chương I. Quy định chung = == Điều 1. Mục đích == Quy chế này quy định việc tổ chức, quản lý và hỗ trợ các hoạt động họp mặt do VWMC tổ chức hoặc đồng tổ chức. Các hoạt động họp mặt nhằm: * Tăng cường kết nối giữa các thành viên, cộng tác viên và những người quan tâm đến các dự án Wikimedia; * Tạo môi trường trao đổi kinh nghiệm, kiến thức và kỹ năng liên quan đến hoạt động biên tập, truyền thông, tổ chức cộng đồng và các lĩnh vực liên quan; * Thúc đẩy sự hợp tác, hỗ trợ lẫn nhau và phát triển mạng lưới cộng đồng; * Tạo cơ hội thảo luận, đánh giá và đóng góp ý kiến đối với các hoạt động, chương trình và định hướng phát triển của VWMC; * Góp phần xây dựng môi trường cộng đồng thân thiện, cởi mở, tôn trọng và hợp tác. Việc tổ chức họp mặt phải bảo đảm tính hiệu quả, phù hợp với nguồn lực hiện có và đáp ứng các mục tiêu cộng đồng của VWMC. == Điều 2. Phạm vi áp dụng == Quy chế này áp dụng đối với: * Ban Điều hành VWMC; * Các cộng tác viên tham gia hỗ trợ tổ chức hoạt động; * Người tham gia các hoạt động họp mặt do VWMC tổ chức hoặc đồng tổ chức; * Các cá nhân hoặc đơn vị được giao nhiệm vụ điều phối, hỗ trợ hoặc phối hợp tổ chức họp mặt. Trường hợp một hoạt động họp mặt được tổ chức phối hợp với tổ chức hoặc cộng đồng khác, Ban Điều hành có thể ban hành hướng dẫn bổ sung để phù hợp với yêu cầu thực tế của hoạt động đó. == Điều 3. Nguyên tắc tổ chức == Các hoạt động họp mặt được tổ chức trên cơ sở tự nguyện, cởi mở, tôn trọng sự đa dạng và khuyến khích sự tham gia của cộng đồng. Việc tổ chức họp mặt phải bảo đảm: * Phù hợp với mục tiêu và định hướng hoạt động của VWMC; * Sử dụng nguồn lực và ngân sách một cách hợp lý, minh bạch và tiết kiệm; * Tạo điều kiện thuận lợi cho người tham gia trao đổi, học hỏi và kết nối; * Bảo đảm môi trường giao tiếp lịch sự, an toàn và tôn trọng lẫn nhau. Ban Điều hành khuyến khích việc tổ chức các hoạt động họp mặt theo hình thức đơn giản, linh hoạt và phù hợp với quy mô cộng đồng, đồng thời hạn chế các thủ tục hoặc yêu cầu hành chính không cần thiết. == Điều 4. Giải thích thuật ngữ == Trong Quy chế này, các thuật ngữ dưới đây được hiểu như sau: # '''Họp mặt''' là hoạt động gặp gỡ trực tiếp hoặc trực tuyến được tổ chức nhằm phục vụ các mục tiêu trao đổi, chia sẻ, điều phối, kết nối cộng đồng hoặc các mục tiêu khác phù hợp với định hướng hoạt động của VWMC. # '''Họp mặt Điều phối''' là hoạt động họp mặt tập trung vào công tác điều hành, lập kế hoạch, đánh giá hoạt động và trao đổi công việc giữa Ban Điều hành và các cộng tác viên liên quan. # '''Họp mặt Cộng đồng''' là hoạt động họp mặt dành cho cộng đồng nhằm chia sẻ kiến thức, kinh nghiệm, thảo luận các hoạt động Wikimedia, tăng cường kết nối và thu nhận ý kiến đóng góp từ người tham gia. # '''Họp mặt Trực tuyến''' là hoạt động họp mặt được tổ chức hoàn toàn thông qua các nền tảng trực tuyến mà không yêu cầu người tham gia có mặt tại cùng một địa điểm. # '''Người tham gia''' là cá nhân đăng ký hoặc được mời tham gia một hoạt động họp mặt thuộc phạm vi điều chỉnh của Quy chế này. # '''Diễn giả''' là người thực hiện hoạt động trình bày, chia sẻ hoặc hướng dẫn trong khuôn khổ một hoạt động họp mặt. # '''Điều phối viên họp mặt''' là cá nhân được giao trách nhiệm chuẩn bị, điều phối hoặc hỗ trợ triển khai một hoạt động họp mặt cụ thể. # '''Hỗ trợ chi phí''' là khoản hỗ trợ tài chính được VWMC cung cấp cho người tham gia hoặc người tổ chức theo các quy định tại Quy chế này và các hướng dẫn liên quan. == Điều 5. Ngôn ngữ và hình thức tổ chức == Ngôn ngữ chính được sử dụng trong các hoạt động họp mặt là tiếng Việt. Ban Tổ chức có thể sử dụng thêm các ngôn ngữ khác khi cần thiết để hỗ trợ diễn giả, khách mời hoặc người tham gia. Hoạt động họp mặt có thể được tổ chức dưới các hình thức: * Trực tiếp; * Trực tuyến; * Kết hợp giữa trực tiếp và trực tuyến. Hình thức tổ chức cụ thể của từng hoạt động họp mặt do Ban Điều hành hoặc Ban Tổ chức quyết định căn cứ vào mục tiêu, điều kiện thực tế và nguồn lực hiện có. = Chương II. Các loại hình họp mặt = == Điều 6. Phân loại hoạt động họp mặt == VWMC tổ chức các hoạt động họp mặt theo các loại hình sau: * Họp mặt Điều phối; * Họp mặt Cộng đồng; * Họp mặt Trực tuyến. Tùy theo nhu cầu thực tế, Ban Điều hành có thể quyết định tổ chức hoạt động họp mặt theo một hoặc nhiều loại hình kết hợp. == Điều 7. Họp mặt Điều phối == Họp mặt Điều phối là hoạt động họp mặt phục vụ công tác điều hành, phối hợp và triển khai hoạt động của VWMC. Mục tiêu của Họp mặt Điều phối bao gồm: * Đánh giá kết quả hoạt động trong giai đoạn trước; * Thảo luận kế hoạch hoạt động trong giai đoạn tiếp theo; * Điều phối nguồn lực và phân công nhiệm vụ; * Trao đổi các vấn đề liên quan đến công tác tổ chức và vận hành; * Tăng cường sự phối hợp giữa Ban Điều hành và các cộng tác viên. Đối tượng tham gia Họp mặt Điều phối có thể bao gồm: * Thành viên Ban Điều hành; * Cộng tác viên; * Khách mời hoặc cá nhân khác theo quyết định của Ban Điều hành. Họp mặt Điều phối thường được tổ chức với quy mô nhỏ và ưu tiên tính linh hoạt, hiệu quả trong trao đổi công việc. == Điều 8. Họp mặt Cộng đồng == Họp mặt Cộng đồng là hoạt động họp mặt nhằm tăng cường kết nối cộng đồng, chia sẻ kiến thức và tạo cơ hội trao đổi giữa những người quan tâm đến các dự án Wikimedia. Mục tiêu của Họp mặt Cộng đồng bao gồm: * Tạo môi trường giao lưu và kết nối giữa các thành viên cộng đồng; * Chia sẻ kinh nghiệm, kiến thức và kỹ năng liên quan đến Wikimedia và các lĩnh vực liên quan; * Thu nhận ý kiến đóng góp đối với các hoạt động của VWMC; * Khuyến khích sự tham gia của thành viên mới; * Ghi nhận và tôn vinh những đóng góp cho cộng đồng khi phù hợp. Một Họp mặt Cộng đồng có thể bao gồm các hoạt động như: * Giới thiệu thành viên tham dự; * Các phiên trình bày hoặc chia sẻ chuyên đề; * Thảo luận cộng đồng; * Hoạt động giao lưu và kết nối; * Chụp ảnh lưu niệm; * Các hoạt động khác phù hợp với mục tiêu của chương trình. Họp mặt Cộng đồng có thể được tổ chức dưới hình thức trực tiếp hoặc kết hợp giữa trực tiếp và trực tuyến. == Điều 9. Họp mặt Trực tuyến == Họp mặt Trực tuyến là hoạt động họp mặt được tổ chức hoàn toàn thông qua các nền tảng trực tuyến. Mục tiêu của Họp mặt Trực tuyến bao gồm: * Tạo điều kiện tham gia cho những người không thể tham dự trực tiếp; * Duy trì kết nối cộng đồng giữa các kỳ họp mặt trực tiếp; * Tổ chức các hoạt động chia sẻ, thảo luận hoặc điều phối với chi phí thấp và khả năng tiếp cận cao; * Hỗ trợ triển khai các hoạt động cộng đồng trên phạm vi rộng. Họp mặt Trực tuyến có thể được sử dụng cho các mục đích: * Chia sẻ kiến thức hoặc kỹ năng; * Thảo luận cộng đồng; * Giới thiệu chương trình hoặc hoạt động mới; * Điều phối công việc; * Các hoạt động phù hợp khác. Ban Tổ chức có trách nhiệm lựa chọn nền tảng trực tuyến phù hợp và bảo đảm người tham gia có thể tiếp cận thông tin cần thiết để tham gia chương trình. = Chương III. Tổ chức họp mặt = == Điều 10. Thẩm quyền tổ chức == Ban Điều hành có trách nhiệm tổ chức, phê duyệt và giám sát các hoạt động họp mặt thuộc phạm vi điều chỉnh của Quy chế này. Ban Điều hành có thể: * Trực tiếp tổ chức hoạt động họp mặt; * Giao nhiệm vụ cho một hoặc nhiều Điều phối viên họp mặt; * Thành lập Ban Tổ chức đối với các hoạt động họp mặt có quy mô lớn hoặc yêu cầu nhiều nhân sự tham gia. Các hoạt động họp mặt chỉ được triển khai sau khi được Ban Điều hành phê duyệt hoặc cho phép thực hiện. == Điều 11. Đề xuất tổ chức họp mặt == Mọi cá nhân thuộc Ban Điều hành hoặc cộng tác viên đều có thể đề xuất tổ chức một hoạt động họp mặt. Đề xuất tổ chức họp mặt nên bao gồm các thông tin cơ bản sau: * Loại hình họp mặt; * Mục tiêu tổ chức; * Thời gian dự kiến; * Địa điểm hoặc nền tảng tổ chức; * Đối tượng tham gia; * Chương trình dự kiến; * Nhu cầu ngân sách (nếu có). Đối với các hoạt động họp mặt quy mô nhỏ hoặc phát sinh theo nhu cầu thực tế, Ban Điều hành có thể áp dụng hình thức đề xuất và phê duyệt đơn giản phù hợp với tình hình cụ thể. == Điều 12. Phê duyệt hoạt động họp mặt == Ban Điều hành xem xét và quyết định việc tổ chức hoạt động họp mặt trên cơ sở: * Mục tiêu và ý nghĩa của hoạt động; * Mức độ phù hợp với định hướng của VWMC; * Nguồn lực nhân sự hiện có; * Khả năng đáp ứng ngân sách; * Các yếu tố thực tế khác liên quan đến việc tổ chức. Việc phê duyệt có thể bao gồm: * Chấp thuận tổ chức; * Chấp thuận có điều kiện; * Yêu cầu điều chỉnh kế hoạch; * Không chấp thuận. Quyết định của Ban Điều hành được thông báo cho người đề xuất trong thời gian phù hợp. == Điều 13. Chuẩn bị hoạt động họp mặt == Sau khi được phê duyệt, Điều phối viên họp mặt hoặc Ban Tổ chức có trách nhiệm chuẩn bị các nội dung cần thiết để triển khai hoạt động. Tùy theo tính chất của từng hoạt động, công tác chuẩn bị có thể bao gồm: * Xác định địa điểm hoặc nền tảng tổ chức; * Xây dựng chương trình; * Mời diễn giả hoặc khách mời; * Mở đăng ký tham gia; * Chuẩn bị tài liệu và thiết bị cần thiết; * Chuẩn bị các nội dung truyền thông; * Chuẩn bị các nội dung hậu cần khác. Ban Điều hành khuyến khích việc đơn giản hóa công tác chuẩn bị khi điều đó không ảnh hưởng đáng kể đến chất lượng hoạt động. == Điều 14. Công bố hoạt động họp mặt == Thông tin về hoạt động họp mặt phải được công bố tới đối tượng tham gia phù hợp trước thời điểm diễn ra chương trình. Thông tin công bố có thể bao gồm: * Tên hoạt động; * Thời gian tổ chức; * Địa điểm hoặc hình thức tham gia; * Chương trình dự kiến; * Thông tin đăng ký; * Các thông tin cần thiết khác. Đối với Họp mặt Điều phối hoặc các hoạt động có tính chất nội bộ, phạm vi công bố có thể được giới hạn theo quyết định của Ban Điều hành. == Điều 15. Thay đổi, hoãn hoặc hủy hoạt động họp mặt == Ban Điều hành hoặc Ban Tổ chức có thể quyết định thay đổi, hoãn hoặc hủy một hoạt động họp mặt trong các trường hợp cần thiết. Các lý do có thể bao gồm: * Điều kiện tổ chức không còn phù hợp; * Số lượng người tham gia không đáp ứng yêu cầu tối thiểu; * Phát sinh các yếu tố khách quan ảnh hưởng đến việc tổ chức; * Các lý do khác được Ban Điều hành xem là hợp lý. Trong trường hợp thay đổi, hoãn hoặc hủy hoạt động, Ban Tổ chức cần thông báo tới người tham gia trong thời gian sớm nhất có thể. == Điều 16. Phối hợp tổ chức với đơn vị khác == VWMC có thể phối hợp tổ chức hoạt động họp mặt với các cộng đồng, tổ chức hoặc cá nhân khác khi việc hợp tác đó phù hợp với mục tiêu và định hướng hoạt động của VWMC. Trách nhiệm của các bên tham gia phối hợp tổ chức cần được xác định rõ trước khi hoạt động được triển khai. Trong trường hợp có quy định hoặc thỏa thuận riêng với đối tác, Ban Điều hành có thể ban hành hướng dẫn bổ sung để áp dụng cho hoạt động cụ thể. = Chương IV. Chương trình họp mặt = == Điều 17. Xây dựng chương trình họp mặt == Mỗi hoạt động họp mặt nên có chương trình phù hợp với mục tiêu, đối tượng tham gia và nguồn lực tổ chức. Chương trình họp mặt có thể được điều chỉnh linh hoạt tùy theo tính chất của từng hoạt động nhưng cần bảo đảm tạo điều kiện cho người tham gia trao đổi, học hỏi và kết nối. Ban Tổ chức chịu trách nhiệm xây dựng và công bố chương trình trước khi hoạt động diễn ra. == Điều 18. Thành phần chương trình == Một hoạt động họp mặt có thể bao gồm một hoặc nhiều thành phần sau: * Đón tiếp và điểm danh người tham gia; * Giới thiệu chương trình và thành phần tham dự; * Giới thiệu thành viên mới; * Các phiên trình bày hoặc chia sẻ chuyên đề; * Các phiên thảo luận; * Hoạt động giao lưu và kết nối; * Hoạt động ghi nhận hoặc trao quà lưu niệm; * Chụp ảnh lưu niệm; * Hoạt động ăn uống hoặc giao lưu ngoài chương trình chính; * Các nội dung khác phù hợp với mục tiêu của hoạt động. Không bắt buộc mọi hoạt động họp mặt phải bao gồm toàn bộ các thành phần nêu trên. == Điều 19. Phiên trình bày và chia sẻ == Phiên trình bày và chia sẻ là hoạt động nhằm trao đổi kiến thức, kinh nghiệm hoặc kết quả thực hiện các dự án, chương trình và sáng kiến liên quan đến Wikimedia hoặc các lĩnh vực có liên quan. Diễn giả có thể là: * Thành viên cộng đồng; * Cộng tác viên; * Thành viên Ban Điều hành; * Khách mời bên ngoài. Ban Tổ chức có trách nhiệm sắp xếp thời lượng và trình tự các phiên trình bày phù hợp với chương trình tổng thể. Ban Tổ chức khuyến khích các nội dung chia sẻ mang tính thực tiễn, có giá trị học hỏi và thúc đẩy sự tham gia của cộng đồng. == Điều 20. Thảo luận cộng đồng == Các hoạt động họp mặt cộng đồng nên dành thời gian phù hợp cho việc thảo luận và trao đổi ý kiến. Nội dung thảo luận có thể bao gồm: * Đánh giá các hoạt động đã triển khai; * Góp ý các hoạt động đang thực hiện; * Đề xuất chương trình hoặc sáng kiến mới; * Thảo luận các vấn đề liên quan đến cộng đồng Wikimedia; * Các nội dung khác phù hợp với mục tiêu của hoạt động. Ban Tổ chức có trách nhiệm tạo điều kiện để người tham gia có cơ hội đóng góp ý kiến một cách cởi mở và tôn trọng lẫn nhau. == Điều 21. Hoạt động giao lưu == Hoạt động giao lưu nhằm tăng cường kết nối giữa người tham gia và góp phần xây dựng môi trường cộng đồng thân thiện. Hoạt động giao lưu có thể được tổ chức trong hoặc ngoài chương trình chính của họp mặt. Các hoạt động giao lưu có thể bao gồm: * Trò chuyện tự do; * Ăn uống chung; * Hoạt động nhóm; * Chụp ảnh lưu niệm; * Các hoạt động phù hợp khác. == Điều 22. Tài liệu và nội dung trình bày == Diễn giả và Ban Tổ chức được khuyến khích chuẩn bị tài liệu trình bày phù hợp để hỗ trợ người tham gia tiếp cận nội dung chương trình. Ban Tổ chức có thể lưu trữ hoặc công bố các tài liệu trình bày, bản ghi hoặc nội dung liên quan sau khi kết thúc hoạt động nếu phù hợp với điều kiện thực tế và sự đồng ý của các bên liên quan. == Điều 23. Ghi hình, chụp ảnh và truyền thông == Ban Tổ chức có thể thực hiện việc ghi hình, chụp ảnh hoặc truyền thông về hoạt động họp mặt nhằm mục đích lưu trữ, báo cáo và giới thiệu hoạt động cộng đồng. Người tham gia có quyền đề nghị hạn chế sử dụng hình ảnh hoặc thông tin cá nhân của mình trong phạm vi hợp lý. Việc sử dụng hình ảnh, video và các nội dung truyền thông phát sinh từ hoạt động họp mặt phải phù hợp với các quy định hiện hành của VWMC và pháp luật có liên quan. = Chương V. Tài chính và hỗ trợ = == Điều 24. Nguyên tắc sử dụng ngân sách == Ngân sách dành cho các hoạt động họp mặt được sử dụng nhằm phục vụ việc tổ chức, hỗ trợ người tham gia và thực hiện các mục tiêu cộng đồng của VWMC. Việc sử dụng ngân sách phải bảo đảm: * Phù hợp với mục tiêu của hoạt động; * Hợp lý, tiết kiệm và hiệu quả; * Minh bạch và có khả năng kiểm tra khi cần thiết; * Phù hợp với nguồn lực tài chính hiện có của VWMC. Ban Điều hành có trách nhiệm xem xét và phê duyệt các khoản chi trước khi hoạt động được triển khai, trừ các trường hợp phát sinh hợp lý trong quá trình tổ chức. == Điều 25. Nguồn kinh phí == Kinh phí tổ chức họp mặt có thể đến từ một hoặc nhiều nguồn sau: * Ngân sách hoạt động của VWMC; * Khoản tài trợ hoặc hỗ trợ từ các tổ chức, cá nhân; * Các nguồn hợp pháp khác phù hợp với quy định hiện hành. Việc tiếp nhận và sử dụng kinh phí phải tuân thủ các quy định tài chính của VWMC và các cam kết liên quan với bên tài trợ nếu có. == Điều 26. Các khoản chi được phép == Tùy theo tính chất của từng hoạt động, ngân sách họp mặt có thể được sử dụng cho các khoản chi sau: * Thuê địa điểm hoặc phòng họp; * Chi phí sử dụng nền tảng trực tuyến; * Đồ uống và ăn uống; * Vật phẩm phục vụ chương trình; * Quà lưu niệm hoặc vật phẩm ghi nhận; * In ấn tài liệu hoặc vật phẩm truyền thông; * Chi phí đi lại được hỗ trợ theo quy định; * Chi phí liên quan đến diễn giả hoặc khách mời khi cần thiết; * Các chi phí hợp lý khác phục vụ trực tiếp cho hoạt động. Ban Điều hành có quyền từ chối hoặc điều chỉnh các khoản chi không phù hợp với mục tiêu của hoạt động hoặc khả năng ngân sách hiện có. == Điều 27. Hỗ trợ chi phí đi lại == VWMC có thể hỗ trợ toàn bộ hoặc một phần chi phí đi lại cho người tham gia các hoạt động họp mặt. Việc hỗ trợ được xem xét trên cơ sở: * Mục tiêu của hoạt động; * Địa điểm cư trú hoặc xuất phát của người tham gia; * Mức độ tham gia và đóng góp cho hoạt động; * Nguồn ngân sách hiện có; * Các tiêu chí khác do Ban Điều hành quyết định. Ban Điều hành có thể ban hành hướng dẫn riêng về phạm vi, mức hỗ trợ và thủ tục nhận hỗ trợ đối với từng hoạt động cụ thể. == Điều 28. Hỗ trợ diễn giả và khách mời == Đối với các hoạt động họp mặt cộng đồng, VWMC có thể hỗ trợ diễn giả hoặc khách mời thông qua một hoặc nhiều hình thức sau: * Hỗ trợ đi lại; * Hỗ trợ ăn uống; * Quà lưu niệm; * Các hình thức hỗ trợ khác phù hợp với điều kiện thực tế. Việc hỗ trợ được thực hiện trên cơ sở khả năng ngân sách và quyết định của Ban Điều hành. == Điều 29. Quà lưu niệm và vật phẩm cộng đồng == Ban Tổ chức có thể chuẩn bị quà lưu niệm hoặc các vật phẩm cộng đồng để trao tặng cho người tham gia, diễn giả hoặc khách mời. Việc lựa chọn và phân phối vật phẩm cần bảo đảm: * Phù hợp với mục tiêu của hoạt động; * Phù hợp với khả năng ngân sách; * Bảo đảm tính công bằng và minh bạch. Các vật phẩm có thể bao gồm: * Huy hiệu; * Sticker; * Áo thun; * Sổ tay; * Bưu thiếp; * Các sản phẩm lưu niệm khác. == Điều 30. Thanh toán và quyết toán == Ban Tổ chức hoặc cá nhân được giao quản lý ngân sách có trách nhiệm ghi nhận và tổng hợp các khoản chi phát sinh trong quá trình tổ chức hoạt động. Sau khi kết thúc hoạt động, các khoản chi cần được tổng hợp và báo cáo theo quy định của VWMC. Ban Điều hành có quyền yêu cầu cung cấp các thông tin hoặc tài liệu cần thiết để phục vụ việc kiểm tra, xác minh hoặc lưu trữ hồ sơ tài chính. == Điều 31. Trường hợp đặc biệt == Trong những trường hợp đặc biệt nhằm phục vụ lợi ích chung của cộng đồng hoặc đáp ứng nhu cầu tổ chức thực tế, Ban Điều hành có thể quyết định áp dụng cơ chế hỗ trợ hoặc chi tiêu khác với các quy định thông thường của Chương này. Các quyết định theo Điều này cần được ghi nhận và lưu trữ cùng hồ sơ của hoạt động liên quan. = Chương VI. Quyền và trách nhiệm = == Điều 32. Quyền và trách nhiệm của Ban Điều hành == Ban Điều hành có các quyền và trách nhiệm sau: * Xây dựng và ban hành các quy định, hướng dẫn liên quan đến hoạt động họp mặt; * Xem xét, phê duyệt hoặc từ chối các đề xuất tổ chức họp mặt; * Phân công Điều phối viên họp mặt hoặc thành lập Ban Tổ chức khi cần thiết; * Phê duyệt ngân sách và các khoản hỗ trợ liên quan; * Giám sát việc triển khai hoạt động họp mặt; * Đánh giá kết quả tổ chức và đề xuất các biện pháp cải thiện; * Bảo đảm các hoạt động họp mặt phù hợp với định hướng và mục tiêu của VWMC. Ban Điều hành có quyền điều chỉnh kế hoạch, ngân sách hoặc hình thức tổ chức khi thấy cần thiết để bảo đảm hiệu quả hoạt động và an toàn cho người tham gia. == Điều 33. Quyền và trách nhiệm của Điều phối viên họp mặt và Ban Tổ chức == Điều phối viên họp mặt hoặc Ban Tổ chức có các quyền và trách nhiệm sau: * Lập kế hoạch và chuẩn bị hoạt động họp mặt; * Điều phối việc triển khai chương trình; * Quản lý các công việc hậu cần cần thiết; * Phối hợp với diễn giả, khách mời và người tham gia; * Quản lý ngân sách được giao theo quy định; * Thu thập thông tin, hình ảnh và tài liệu phục vụ báo cáo; * Báo cáo kết quả tổ chức cho Ban Điều hành. Điều phối viên họp mặt có quyền đề xuất các điều chỉnh cần thiết trong quá trình chuẩn bị và triển khai hoạt động để bảo đảm chất lượng chương trình. == Điều 34. Quyền và trách nhiệm của diễn giả và khách mời == Diễn giả và khách mời có các quyền sau: * Được hỗ trợ và tạo điều kiện phù hợp để tham gia chương trình; * Được ghi nhận sự tham gia và đóng góp trong hoạt động; * Được tiếp cận các thông tin cần thiết liên quan đến nội dung chương trình. Diễn giả và khách mời có trách nhiệm: * Chuẩn bị nội dung phù hợp với mục tiêu của chương trình; * Tôn trọng người tham gia và các nguyên tắc cộng đồng; * Phối hợp với Ban Tổ chức trong quá trình triển khai hoạt động. == Điều 35. Quyền của người tham gia == Người tham gia có các quyền sau: * Được tiếp cận thông tin liên quan đến hoạt động họp mặt; * Được tham gia các hoạt động phù hợp với chương trình đã công bố; * Được phát biểu ý kiến, đặt câu hỏi và tham gia thảo luận theo điều kiện thực tế của chương trình; * Được xem xét hỗ trợ theo các chính sách áp dụng cho từng hoạt động; * Được đối xử công bằng và tôn trọng trong quá trình tham gia. Người tham gia có thể đóng góp ý kiến nhằm cải thiện chất lượng các hoạt động họp mặt của VWMC. == Điều 36. Trách nhiệm của người tham gia == Người tham gia có trách nhiệm: * Tuân thủ các quy định của chương trình và hướng dẫn của Ban Tổ chức; * Cư xử văn minh, lịch sự và tôn trọng người khác; * Không thực hiện các hành vi gây cản trở hoặc ảnh hưởng tiêu cực đến hoạt động; * Bảo quản tài sản, thiết bị và cơ sở vật chất được sử dụng trong chương trình; * Cung cấp thông tin cần thiết khi đăng ký tham gia hoặc nhận hỗ trợ từ VWMC; * Hợp tác với Ban Tổ chức khi phát sinh các vấn đề liên quan đến hoạt động. Người tham gia được khuyến khích hỗ trợ người mới, chia sẻ kiến thức và góp phần xây dựng môi trường cộng đồng thân thiện. == Điều 37. Bảo đảm môi trường cộng đồng == Mọi cá nhân tham gia hoạt động họp mặt có trách nhiệm góp phần duy trì môi trường giao tiếp tích cực, thân thiện và tôn trọng lẫn nhau. Không khuyến khích các hành vi: * Công kích cá nhân; * Quấy rối hoặc xúc phạm người khác; * Phân biệt đối xử dưới bất kỳ hình thức nào; * Cố ý gây mất trật tự hoặc cản trở hoạt động của chương trình. Ban Tổ chức có quyền nhắc nhở, hạn chế tham gia hoặc áp dụng các biện pháp phù hợp đối với những trường hợp gây ảnh hưởng nghiêm trọng đến hoạt động hoặc môi trường cộng đồng. == Điều 38. Xử lý vấn đề phát sinh == Trong quá trình tổ chức hoặc tham gia hoạt động họp mặt, mọi cá nhân có thể phản ánh các vấn đề phát sinh cho Ban Tổ chức hoặc Ban Điều hành. Ban Tổ chức và Ban Điều hành có trách nhiệm xem xét, xử lý hoặc đề xuất biện pháp xử lý phù hợp trong phạm vi thẩm quyền của mình. Các quyết định xử lý cần được thực hiện trên cơ sở khách quan, hợp lý và phù hợp với mục tiêu duy trì môi trường cộng đồng tích cực. = Chương VII. Báo cáo và lưu trữ = == Điều 39. Mục đích của công tác báo cáo và lưu trữ == Công tác báo cáo và lưu trữ nhằm: * Ghi nhận kết quả tổ chức các hoạt động họp mặt; * Hỗ trợ đánh giá hiệu quả hoạt động; * Lưu giữ tư liệu phục vụ công tác truyền thông và phát triển cộng đồng; * Hỗ trợ công tác quản lý, tổng kết và lập kế hoạch của VWMC; * Đáp ứng các yêu cầu báo cáo hoặc kiểm tra khi cần thiết. Việc báo cáo và lưu trữ cần được thực hiện theo hướng đơn giản, phù hợp với quy mô của từng hoạt động. == Điều 40. Báo cáo sau hoạt động == Sau khi kết thúc một hoạt động họp mặt, Điều phối viên họp mặt hoặc Ban Tổ chức có trách nhiệm thực hiện báo cáo kết quả khi cần thiết. Báo cáo có thể bao gồm các nội dung sau: * Tên hoạt động; * Thời gian và địa điểm tổ chức; * Loại hình họp mặt; * Số lượng người tham gia; * Nội dung chính của chương trình; * Kết quả đạt được; * Các vấn đề phát sinh (nếu có); * Tổng hợp chi phí thực tế (nếu có sử dụng ngân sách); * Các thông tin khác phù hợp với tính chất của hoạt động. Đối với các hoạt động quy mô nhỏ hoặc họp mặt điều phối thông thường, Ban Điều hành có thể áp dụng hình thức báo cáo đơn giản. == Điều 41. Hình ảnh và tư liệu hoạt động == Ban Tổ chức được khuyến khích thu thập và lưu giữ các tư liệu liên quan đến hoạt động họp mặt, bao gồm: * Hình ảnh; * Video; * Tài liệu trình bày; * Chương trình hoạt động; * Các tài liệu liên quan khác. Việc thu thập, sử dụng và lưu trữ tư liệu phải phù hợp với các quy định hiện hành của VWMC và tôn trọng quyền riêng tư của người tham gia. == Điều 42. Lưu trữ hồ sơ == Hồ sơ của một hoạt động họp mặt có thể bao gồm: * Kế hoạch hoặc đề xuất tổ chức; * Chương trình hoạt động; * Danh sách người tham gia (nếu có); * Báo cáo kết quả; * Hồ sơ tài chính (nếu có); * Hình ảnh, video và các tư liệu liên quan. Ban Điều hành hoặc cá nhân được phân công có trách nhiệm bảo đảm việc lưu trữ hồ sơ một cách có hệ thống và thuận tiện cho việc tra cứu khi cần thiết. == Điều 43. Công bố kết quả hoạt động == Ban Điều hành hoặc Ban Tổ chức có thể công bố thông tin về hoạt động họp mặt thông qua các kênh truyền thông của VWMC. Thông tin công bố có thể bao gồm: * Tóm tắt hoạt động; * Hình ảnh hoặc video; * Nội dung chia sẻ đáng chú ý; * Thống kê cơ bản về hoạt động; * Các thông tin phù hợp khác. Việc công bố thông tin cần bảo đảm tính chính xác, phù hợp với mục tiêu truyền thông và tôn trọng quyền riêng tư của người tham gia. == Điều 44. Khai thác và sử dụng tư liệu lưu trữ == Các tư liệu được lưu trữ có thể được sử dụng cho các mục đích sau: * Tổng kết hoạt động; * Đánh giá và cải tiến công tác tổ chức; * Truyền thông cộng đồng; * Xây dựng tài liệu hướng dẫn hoặc tài liệu lịch sử hoạt động; * Báo cáo với các đối tác hoặc bên tài trợ khi phù hợp; * Các mục đích khác phục vụ hoạt động của VWMC. Việc sử dụng tư liệu lưu trữ phải bảo đảm phù hợp với quy định của VWMC và các cam kết liên quan đến quyền riêng tư, bản quyền hoặc các nghĩa vụ khác phát sinh trong quá trình tổ chức hoạt động. == Điều 45. Lưu trữ lâu dài == Ban Điều hành khuyến khích việc lưu giữ các tư liệu có giá trị lịch sử hoặc giá trị tham khảo lâu dài đối với cộng đồng. Các tư liệu được lựa chọn lưu trữ lâu dài có thể bao gồm: * Hình ảnh của các kỳ họp mặt cộng đồng; * Tài liệu trình bày có giá trị tham khảo; * Các báo cáo tổng kết quan trọng; * Các tư liệu phản ánh quá trình phát triển của VWMC; * Các tài liệu khác do Ban Điều hành quyết định. Việc lưu trữ lâu dài nhằm góp phần bảo tồn ký ức cộng đồng và hỗ trợ các hoạt động phát triển trong tương lai. = Phụ lục A. Mẫu đề xuất họp mặt = == MẪU ĐỀ XUẤT TỔ CHỨC HỌP MẶT == === I. Thông tin chung === '''Tên hoạt động:''' .............................................................................. '''Loại hình họp mặt:''' ☐ Họp mặt Điều phối ☐ Họp mặt Cộng đồng ☐ Họp mặt Trực tuyến ☐ Khác (nêu rõ): .............................................................. '''Người đề xuất:''' .............................................................................. '''Ngày đề xuất:''' .............................................................................. === II. Mục tiêu tổ chức === Mô tả ngắn gọn mục tiêu và lý do tổ chức hoạt động. .............................................................................. .............................................................................. .............................................................................. === III. Thời gian và địa điểm === '''Thời gian dự kiến:''' .............................................................................. '''Địa điểm hoặc nền tảng tổ chức:''' .............................................................................. '''Hình thức tổ chức:''' ☐ Trực tiếp ☐ Trực tuyến ☐ Kết hợp === IV. Thành phần tham gia === '''Đối tượng tham gia:''' .............................................................................. .............................................................................. '''Số lượng dự kiến:''' .............................................................................. '''Diễn giả hoặc khách mời (nếu có):''' .............................................................................. .............................................................................. === V. Chương trình dự kiến === {| class="wikitable" |'''Thời gian''' |'''Nội dung''' |- | | |- | | |- | | |} === VI. Nhu cầu hỗ trợ === '''Địa điểm''' ☐ Có ☐ Không '''Ăn uống''' ☐ Có ☐ Không '''Hỗ trợ đi lại''' ☐ Có ☐ Không '''Quà lưu niệm''' ☐ Có ☐ Không '''Chi phí khác''' ☐ Có ☐ Không Nếu có, mô tả thêm: .............................................................................. .............................................................................. === VII. Dự toán ngân sách === {| class="wikitable" |'''Hạng mục''' |'''Chi phí dự kiến''' |- |Địa điểm | |- |Ăn uống | |- |Hỗ trợ đi lại | |- |Quà lưu niệm | |- |Khác | |} === VIII. Ghi chú khác === .............................................................................. .............................................................................. .............................................................................. === IX. Kết quả phê duyệt === '''Quyết định của Ban Điều hành''' ☐ Chấp thuận ☐ Chấp thuận có điều kiện ☐ Yêu cầu điều chỉnh ☐ Không chấp thuận '''Ghi chú:''' .............................................................................. .............................................................................. .............................................................................. '''Người phê duyệt:''' .............................................................................. '''Ngày phê duyệt:''' .............................................................................. = Phụ lục B. Mẫu chương trình họp mặt cộng đồng = == MẪU CHƯƠNG TRÌNH HỌP MẶT CỘNG ĐỒNG == === Thông tin chung === '''Tên hoạt động:''' .............................................................................. '''Thời gian:''' .............................................................................. '''Địa điểm:''' .............................................................................. '''Hình thức:''' ☐ Trực tiếp ☐ Trực tuyến ☐ Kết hợp '''Đơn vị tổ chức:''' Vietnam Wikimedians Collective (VWMC) = Chương trình dự kiến = {| class="wikitable" |'''Thời gian''' |'''Nội dung''' |- |08:30 – 09:00 |Đón tiếp và điểm danh người tham gia |- |09:00 – 09:15 |Khai mạc và giới thiệu chương trình |- |09:15 – 09:45 |Giới thiệu thành viên tham dự |- |09:45 – 10:15 |Phiên trình bày số 1 |- |10:15 – 10:45 |Phiên trình bày số 2 |- |10:45 – 11:00 |Nghỉ giải lao |- |11:00 – 11:30 |Phiên trình bày số 3 |- |11:30 – 12:00 |Phiên trình bày số 4 |- |12:00 – 13:30 |Ăn trưa và giao lưu |- |13:30 – 14:00 |Phiên trình bày số 5 |- |14:00 – 14:30 |Phiên trình bày số 6 |- |14:30 – 15:30 |Thảo luận cộng đồng |- |15:30 – 15:45 |Tổng kết hoạt động |- |15:45 – 16:00 |Chụp ảnh lưu niệm và bế mạc |} = Nội dung các phiên trình bày = == Phiên trình bày số 1 == '''Chủ đề:''' .............................................................................. '''Diễn giả:''' .............................................................................. == Phiên trình bày số 2 == '''Chủ đề:''' .............................................................................. '''Diễn giả:''' .............................................................................. == Phiên trình bày số 3 == '''Chủ đề:''' .............................................................................. '''Diễn giả:''' .............................................................................. == Phiên trình bày số 4 == '''Chủ đề:''' .............................................................................. '''Diễn giả:''' .............................................................................. == Phiên trình bày số 5 == '''Chủ đề:''' .............................................................................. '''Diễn giả:''' .............................................................................. == Phiên trình bày số 6 == '''Chủ đề:''' .............................................................................. '''Diễn giả:''' .............................................................................. = Nội dung thảo luận cộng đồng = Phiên thảo luận cộng đồng có thể tập trung vào một hoặc nhiều nội dung sau: * Đánh giá các hoạt động đã triển khai; * Cập nhật tình hình các chương trình đang thực hiện; * Góp ý các quy chế, hướng dẫn hoặc tài liệu cộng đồng; * Đề xuất hoạt động mới; * Trao đổi các vấn đề liên quan đến Wikimedia và cộng đồng; * Các nội dung khác do Ban Tổ chức xác định. Các ý kiến đóng góp quan trọng nên được ghi nhận để phục vụ công tác điều phối và lập kế hoạch trong tương lai. = Ghi nhận và lưu niệm = Tùy điều kiện thực tế, chương trình có thể bao gồm: * Trao quà lưu niệm cho người tham gia; * Trao quà lưu niệm cho diễn giả hoặc khách mời; * Ghi nhận các đóng góp nổi bật cho cộng đồng; * Hoạt động kỷ niệm hoặc chúc mừng các cột mốc quan trọng. = Danh sách công việc trong ngày tổ chức = == Trước chương trình == ☐ Kiểm tra địa điểm ☐ Kiểm tra thiết bị trình chiếu ☐ Kiểm tra kết nối trực tuyến ☐ Chuẩn bị tài liệu ☐ Chuẩn bị quà lưu niệm ☐ Chuẩn bị khu vực đón tiếp == Trong chương trình == ☐ Điểm danh ☐ Điều phối thời gian ☐ Hỗ trợ diễn giả ☐ Ghi nhận ý kiến thảo luận ☐ Chụp ảnh ☐ Hỗ trợ người tham gia trực tuyến == Sau chương trình == ☐ Chụp ảnh tập thể ☐ Thu thập tài liệu trình bày ☐ Tổng hợp danh sách tham gia ☐ Tổng hợp chi phí ☐ Chuẩn bị báo cáo sau hoạt động = Phụ lục C. Mẫu báo cáo sau họp mặt = == MẪU BÁO CÁO SAU HỌP MẶT == === I. Thông tin chung === '''Tên hoạt động:''' .............................................................................. '''Loại hình họp mặt:''' ☐ Họp mặt Điều phối ☐ Họp mặt Cộng đồng ☐ Họp mặt Trực tuyến ☐ Khác: .............................................................. '''Thời gian tổ chức:''' .............................................................................. '''Địa điểm hoặc nền tảng tổ chức:''' .............................................................................. '''Đơn vị tổ chức:''' Vietnam Wikimedians Collective (VWMC) '''Người phụ trách báo cáo:''' .............................................................................. '''Ngày lập báo cáo:''' .............................................................................. === II. Tóm tắt hoạt động === Mô tả ngắn gọn về hoạt động đã diễn ra. .............................................................................. .............................................................................. .............................................................................. .............................................................................. === III. Thành phần tham gia === '''1. Số lượng tham gia''' {| class="wikitable" |'''Nội dung''' |'''Số lượng''' |- |Người đăng ký | |- |Người tham gia trực tiếp | |- |Người tham gia trực tuyến | |- |Diễn giả | |- |Khách mời | |- |Tổng số người tham gia | |} '''2. Thành phần đáng chú ý (nếu có)''' .............................................................................. .............................................................................. .............................................................................. === IV. Nội dung chương trình === '''Các phiên trình bày''' {| class="wikitable" |'''STT''' |'''Chủ đề''' |'''Diễn giả''' |- |1 | | |- |2 | | |- |3 | | |- |4 | | |- |5 | | |- |6 | | |} '''Các hoạt động khác''' ☐ Giới thiệu thành viên ☐ Thảo luận cộng đồng ☐ Giao lưu ☐ Ăn uống chung ☐ Chụp ảnh lưu niệm ☐ Trao quà lưu niệm ☐ Hoạt động khác Mô tả thêm: .............................................................................. .............................................................................. === V. Kết quả đạt được === '''Các kết quả chính''' * ............................................................................ * ............................................................................ * ............................................................................ * ............................................................................ * ............................................................................ '''Ý kiến và đề xuất đáng chú ý''' * ............................................................................ * ............................................................................ * ............................................................................ * ............................................................................ * ............................................................................ '''Các hành động tiếp theo (nếu có)''' * ............................................................................ * ............................................................................ * ............................................................................ * ............................................................................ === VI. Đánh giá hoạt động === Những điểm tích cực .............................................................................. .............................................................................. .............................................................................. Khó khăn hoặc vấn đề phát sinh .............................................................................. .............................................................................. .............................................................................. Đề xuất cải thiện .............................................................................. .............................................................................. .............................................................................. === VII. Báo cáo tài chính === '''Dự toán được phê duyệt''' ................................. đồng '''Chi phí thực tế''' {| class="wikitable" |'''Hạng mục''' |'''Chi phí''' |- |Địa điểm | |- |Ăn uống | |- |Hỗ trợ đi lại | |- |Quà lưu niệm | |- |Truyền thông | |- |Khác | |} '''Chênh lệch so với dự toán''' .............................................................................. .............................................................................. === VIII. Tài liệu và tư liệu lưu trữ === '''Tài liệu liên quan''' ☐ Chương trình hoạt động ☐ Slide trình bày ☐ Danh sách tham gia ☐ Hồ sơ tài chính ☐ Khác '''Hình ảnh và video''' Liên kết hoặc vị trí lưu trữ: .............................................................................. .............................................................................. === IX. Phụ lục === Có thể đính kèm: * Danh sách người tham gia; * Hình ảnh hoạt động; * Slide trình bày; * Biên bản thảo luận; * Hồ sơ tài chính; * Các tài liệu liên quan khác. === X. Xác nhận === '''Người lập báo cáo''' Họ và tên: .............................................................. Ngày: ..................................................................... Chữ ký (nếu có): ...................................................... '''Đại diện Ban Điều hành''' Họ và tên: .............................................................. Ngày: ..................................................................... Chữ ký (nếu có): ...................................................... = Phụ lục D. Checklist tổ chức họp mặt = == CHECKLIST TỔ CHỨC HỌP MẶT == === Thông tin hoạt động === '''Tên hoạt động:''' .............................................................................. '''Thời gian:''' .............................................................................. '''Địa điểm hoặc nền tảng tổ chức:''' .............................................................................. '''Người phụ trách chính:''' .............................................................................. == I. Giai đoạn chuẩn bị == === 1. Kế hoạch và phê duyệt === ☐ Xác định mục tiêu của hoạt động ☐ Xác định loại hình họp mặt ☐ Lập đề xuất tổ chức ☐ Xây dựng chương trình dự kiến ☐ Lập dự toán ngân sách ☐ Nhận phê duyệt từ Ban Điều hành === 2. Địa điểm và hậu cần === === Đối với hoạt động trực tiếp === ☐ Lựa chọn địa điểm phù hợp ☐ Xác nhận đặt chỗ ☐ Kiểm tra sức chứa ☐ Kiểm tra khu vực trình chiếu ☐ Kiểm tra nguồn điện ☐ Kiểm tra kết nối Internet ☐ Xác nhận thời gian sử dụng địa điểm ☐ Xác nhận phương án ăn uống ☐ Xác nhận phương án gửi xe === Đối với hoạt động trực tuyến === ☐ Lựa chọn nền tảng tổ chức ☐ Tạo phòng họp ☐ Kiểm tra quyền truy cập ☐ Kiểm tra ghi hình (nếu có) ☐ Chuẩn bị phương án hỗ trợ kỹ thuật === 3. Chương trình và nội dung === ☐ Hoàn thiện lịch trình hoạt động ☐ Xác nhận danh sách diễn giả ☐ Xác nhận chủ đề trình bày ☐ Thu thập thông tin giới thiệu diễn giả ☐ Chuẩn bị slide giới thiệu chương trình ☐ Chuẩn bị nội dung khai mạc ☐ Chuẩn bị nội dung tổng kết ☐ Chuẩn bị nội dung thảo luận cộng đồng === 4. Người tham gia === ☐ Mở đăng ký tham gia ☐ Theo dõi số lượng đăng ký ☐ Gửi xác nhận tham gia ☐ Tổng hợp danh sách người tham gia ☐ Tổng hợp nhu cầu hỗ trợ đi lại (nếu có) ☐ Chuẩn bị danh sách điểm danh === 5. Truyền thông === ☐ Thiết kế poster hoặc ảnh giới thiệu ☐ Công bố thông tin hoạt động ☐ Đăng thông báo trên các kênh truyền thông ☐ Gửi thư mời hoặc lời mời tham gia ☐ Đăng nhắc lại trước ngày tổ chức === 6. Quà lưu niệm và vật phẩm === ☐ Xác định danh sách vật phẩm ☐ Chuẩn bị quà lưu niệm ☐ Chuẩn bị tài liệu phát tay (nếu có) ☐ Chuẩn bị bảng tên (nếu có) ☐ Chuẩn bị vật phẩm nhận diện VWMC (nếu có) == II. Trước giờ bắt đầu == ☐ Đến địa điểm đúng giờ ☐ Kiểm tra khu vực tổ chức ☐ Kiểm tra máy chiếu hoặc màn hình ☐ Kiểm tra âm thanh ☐ Kiểm tra Internet ☐ Kiểm tra thiết bị trình bày ☐ Chuẩn bị bàn đón tiếp ☐ Chuẩn bị danh sách điểm danh ☐ Chuẩn bị quà lưu niệm ☐ Đón tiếp diễn giả và khách mời ☐ Kiểm tra kết nối trực tuyến ☐ Chụp ảnh chuẩn bị chương trình == III. Trong quá trình diễn ra == === Điều phối chương trình === ☐ Khai mạc đúng giờ ☐ Giới thiệu chương trình ☐ Giới thiệu người tham gia ☐ Điều phối các phiên trình bày ☐ Theo dõi thời lượng chương trình ☐ Điều phối phiên thảo luận ☐ Tổng kết hoạt động === Hỗ trợ người tham gia === ☐ Điểm danh người tham gia ☐ Hướng dẫn chỗ ngồi ☐ Hỗ trợ người tham gia trực tuyến ☐ Hỗ trợ diễn giả ☐ Giải quyết các vấn đề phát sinh === Ghi nhận tư liệu === ☐ Chụp ảnh hoạt động ☐ Chụp ảnh diễn giả ☐ Chụp ảnh thảo luận ☐ Chụp ảnh tập thể ☐ Thu thập slide trình bày (nếu có) ☐ Ghi nhận các ý kiến đáng chú ý == IV. Sau khi kết thúc == === Tổng kết tại chỗ === ☐ Chụp ảnh lưu niệm ☐ Trao quà lưu niệm ☐ Cảm ơn người tham gia ☐ Kiểm tra tài sản và thiết bị ☐ Thanh toán các chi phí phát sinh === Hoàn thiện hồ sơ === ☐ Tổng hợp danh sách tham gia thực tế ☐ Tổng hợp hình ảnh và video ☐ Tổng hợp tài liệu trình bày ☐ Tổng hợp nội dung thảo luận ☐ Tổng hợp hóa đơn và chứng từ ☐ Lập báo cáo sau hoạt động ☐ Nộp báo cáo cho Ban Điều hành === Truyền thông sau hoạt động === ☐ Chọn hình ảnh tiêu biểu ☐ Viết bài tổng kết ☐ Đăng bài truyền thông ☐ Lưu trữ tư liệu hoạt động ☐ Cập nhật hồ sơ lưu trữ của VWMC == V. Đánh giá nội bộ == ☐ Đánh giá mức độ hoàn thành mục tiêu ☐ Đánh giá chất lượng chương trình ☐ Đánh giá công tác hậu cần ☐ Đánh giá công tác truyền thông ☐ Đánh giá hiệu quả sử dụng ngân sách ☐ Ghi nhận bài học kinh nghiệm ☐ Đề xuất cải thiện cho kỳ họp mặt tiếp theo == Ghi chú == .............................................................................. .............................................................................. .............................................................................. .............................................................................. .............................................................................. 9cjz6ybrxfkcrjh4ssq54q818qtgukj 747073 747072 2026-06-16T17:43:47Z Plantaest 37055 747073 wikitext text/x-wiki Quy chế Họp mặt ---- = Chương I. Quy định chung = == Điều 1. Mục đích == Quy chế này quy định việc tổ chức, quản lý và hỗ trợ các hoạt động họp mặt do VWMC tổ chức hoặc đồng tổ chức. Các hoạt động họp mặt nhằm: * Tăng cường kết nối giữa các thành viên, cộng tác viên và những người quan tâm đến các dự án Wikimedia; * Tạo môi trường trao đổi kinh nghiệm, kiến thức và kỹ năng liên quan đến hoạt động biên tập, truyền thông, tổ chức cộng đồng và các lĩnh vực liên quan; * Thúc đẩy sự hợp tác, hỗ trợ lẫn nhau và phát triển mạng lưới cộng đồng; * Tạo cơ hội thảo luận, đánh giá và đóng góp ý kiến đối với các hoạt động, chương trình và định hướng phát triển của VWMC; * Góp phần xây dựng môi trường cộng đồng thân thiện, cởi mở, tôn trọng và hợp tác. Việc tổ chức họp mặt phải bảo đảm tính hiệu quả, phù hợp với nguồn lực hiện có và đáp ứng các mục tiêu cộng đồng của VWMC. == Điều 2. Phạm vi áp dụng == Quy chế này áp dụng đối với: * Ban Điều hành VWMC; * Các cộng tác viên tham gia hỗ trợ tổ chức hoạt động; * Người tham gia các hoạt động họp mặt do VWMC tổ chức hoặc đồng tổ chức; * Các cá nhân hoặc đơn vị được giao nhiệm vụ điều phối, hỗ trợ hoặc phối hợp tổ chức họp mặt. Trường hợp một hoạt động họp mặt được tổ chức phối hợp với tổ chức hoặc cộng đồng khác, Ban Điều hành có thể ban hành hướng dẫn bổ sung để phù hợp với yêu cầu thực tế của hoạt động đó. == Điều 3. Nguyên tắc tổ chức == Các hoạt động họp mặt được tổ chức trên cơ sở tự nguyện, cởi mở, tôn trọng sự đa dạng và khuyến khích sự tham gia của cộng đồng. Việc tổ chức họp mặt phải bảo đảm: * Phù hợp với mục tiêu và định hướng hoạt động của VWMC; * Sử dụng nguồn lực và ngân sách một cách hợp lý, minh bạch và tiết kiệm; * Tạo điều kiện thuận lợi cho người tham gia trao đổi, học hỏi và kết nối; * Bảo đảm môi trường giao tiếp lịch sự, an toàn và tôn trọng lẫn nhau. Ban Điều hành khuyến khích việc tổ chức các hoạt động họp mặt theo hình thức đơn giản, linh hoạt và phù hợp với quy mô cộng đồng, đồng thời hạn chế các thủ tục hoặc yêu cầu hành chính không cần thiết. == Điều 4. Giải thích thuật ngữ == Trong Quy chế này, các thuật ngữ dưới đây được hiểu như sau: # '''Họp mặt''' là hoạt động gặp gỡ trực tiếp hoặc trực tuyến được tổ chức nhằm phục vụ các mục tiêu trao đổi, chia sẻ, điều phối, kết nối cộng đồng hoặc các mục tiêu khác phù hợp với định hướng hoạt động của VWMC. # '''Họp mặt Điều phối''' là hoạt động họp mặt tập trung vào công tác điều hành, lập kế hoạch, đánh giá hoạt động và trao đổi công việc giữa Ban Điều hành và các cộng tác viên liên quan. # '''Họp mặt Cộng đồng''' là hoạt động họp mặt dành cho cộng đồng nhằm chia sẻ kiến thức, kinh nghiệm, thảo luận các hoạt động Wikimedia, tăng cường kết nối và thu nhận ý kiến đóng góp từ người tham gia. # '''Họp mặt Trực tuyến''' là hoạt động họp mặt được tổ chức hoàn toàn thông qua các nền tảng trực tuyến mà không yêu cầu người tham gia có mặt tại cùng một địa điểm. # '''Người tham gia''' là cá nhân đăng ký hoặc được mời tham gia một hoạt động họp mặt thuộc phạm vi điều chỉnh của Quy chế này. # '''Diễn giả''' là người thực hiện hoạt động trình bày, chia sẻ hoặc hướng dẫn trong khuôn khổ một hoạt động họp mặt. # '''Điều phối viên họp mặt''' là cá nhân được giao trách nhiệm chuẩn bị, điều phối hoặc hỗ trợ triển khai một hoạt động họp mặt cụ thể. # '''Hỗ trợ chi phí''' là khoản hỗ trợ tài chính được VWMC cung cấp cho người tham gia hoặc người tổ chức theo các quy định tại Quy chế này và các hướng dẫn liên quan. == Điều 5. Ngôn ngữ và hình thức tổ chức == Ngôn ngữ chính được sử dụng trong các hoạt động họp mặt là tiếng Việt. Ban Tổ chức có thể sử dụng thêm các ngôn ngữ khác khi cần thiết để hỗ trợ diễn giả, khách mời hoặc người tham gia. Hoạt động họp mặt có thể được tổ chức dưới các hình thức: * Trực tiếp; * Trực tuyến; * Kết hợp giữa trực tiếp và trực tuyến. Hình thức tổ chức cụ thể của từng hoạt động họp mặt do Ban Điều hành hoặc Ban Tổ chức quyết định căn cứ vào mục tiêu, điều kiện thực tế và nguồn lực hiện có. = Chương II. Các loại hình họp mặt = == Điều 6. Phân loại hoạt động họp mặt == VWMC tổ chức các hoạt động họp mặt theo các loại hình sau: * Họp mặt Điều phối; * Họp mặt Cộng đồng; * Họp mặt Trực tuyến. Tùy theo nhu cầu thực tế, Ban Điều hành có thể quyết định tổ chức hoạt động họp mặt theo một hoặc nhiều loại hình kết hợp. == Điều 7. Họp mặt Điều phối == Họp mặt Điều phối là hoạt động họp mặt phục vụ công tác điều hành, phối hợp và triển khai hoạt động của VWMC. Mục tiêu của Họp mặt Điều phối bao gồm: * Đánh giá kết quả hoạt động trong giai đoạn trước; * Thảo luận kế hoạch hoạt động trong giai đoạn tiếp theo; * Điều phối nguồn lực và phân công nhiệm vụ; * Trao đổi các vấn đề liên quan đến công tác tổ chức và vận hành; * Tăng cường sự phối hợp giữa Ban Điều hành và các cộng tác viên. Đối tượng tham gia Họp mặt Điều phối có thể bao gồm: * Thành viên Ban Điều hành; * Cộng tác viên; * Khách mời hoặc cá nhân khác theo quyết định của Ban Điều hành. Họp mặt Điều phối thường được tổ chức với quy mô nhỏ và ưu tiên tính linh hoạt, hiệu quả trong trao đổi công việc. == Điều 8. Họp mặt Cộng đồng == Họp mặt Cộng đồng là hoạt động họp mặt nhằm tăng cường kết nối cộng đồng, chia sẻ kiến thức và tạo cơ hội trao đổi giữa những người quan tâm đến các dự án Wikimedia. Mục tiêu của Họp mặt Cộng đồng bao gồm: * Tạo môi trường giao lưu và kết nối giữa các thành viên cộng đồng; * Chia sẻ kinh nghiệm, kiến thức và kỹ năng liên quan đến Wikimedia và các lĩnh vực liên quan; * Thu nhận ý kiến đóng góp đối với các hoạt động của VWMC; * Khuyến khích sự tham gia của thành viên mới; * Ghi nhận và tôn vinh những đóng góp cho cộng đồng khi phù hợp. Một Họp mặt Cộng đồng có thể bao gồm các hoạt động như: * Giới thiệu thành viên tham dự; * Các phiên trình bày hoặc chia sẻ chuyên đề; * Thảo luận cộng đồng; * Hoạt động giao lưu và kết nối; * Chụp ảnh lưu niệm; * Các hoạt động khác phù hợp với mục tiêu của chương trình. Họp mặt Cộng đồng có thể được tổ chức dưới hình thức trực tiếp hoặc kết hợp giữa trực tiếp và trực tuyến. == Điều 9. Họp mặt Trực tuyến == Họp mặt Trực tuyến là hoạt động họp mặt được tổ chức hoàn toàn thông qua các nền tảng trực tuyến. Mục tiêu của Họp mặt Trực tuyến bao gồm: * Tạo điều kiện tham gia cho những người không thể tham dự trực tiếp; * Duy trì kết nối cộng đồng giữa các kỳ họp mặt trực tiếp; * Tổ chức các hoạt động chia sẻ, thảo luận hoặc điều phối với chi phí thấp và khả năng tiếp cận cao; * Hỗ trợ triển khai các hoạt động cộng đồng trên phạm vi rộng. Họp mặt Trực tuyến có thể được sử dụng cho các mục đích: * Chia sẻ kiến thức hoặc kỹ năng; * Thảo luận cộng đồng; * Giới thiệu chương trình hoặc hoạt động mới; * Điều phối công việc; * Các hoạt động phù hợp khác. Ban Tổ chức có trách nhiệm lựa chọn nền tảng trực tuyến phù hợp và bảo đảm người tham gia có thể tiếp cận thông tin cần thiết để tham gia chương trình. = Chương III. Tổ chức họp mặt = == Điều 10. Thẩm quyền tổ chức == Ban Điều hành có trách nhiệm tổ chức, phê duyệt và giám sát các hoạt động họp mặt thuộc phạm vi điều chỉnh của Quy chế này. Ban Điều hành có thể: * Trực tiếp tổ chức hoạt động họp mặt; * Giao nhiệm vụ cho một hoặc nhiều Điều phối viên họp mặt; * Thành lập Ban Tổ chức đối với các hoạt động họp mặt có quy mô lớn hoặc yêu cầu nhiều nhân sự tham gia. Các hoạt động họp mặt chỉ được triển khai sau khi được Ban Điều hành phê duyệt hoặc cho phép thực hiện. == Điều 11. Đề xuất tổ chức họp mặt == Mọi cá nhân thuộc Ban Điều hành hoặc cộng tác viên đều có thể đề xuất tổ chức một hoạt động họp mặt. Đề xuất tổ chức họp mặt nên bao gồm các thông tin cơ bản sau: * Loại hình họp mặt; * Mục tiêu tổ chức; * Thời gian dự kiến; * Địa điểm hoặc nền tảng tổ chức; * Đối tượng tham gia; * Chương trình dự kiến; * Nhu cầu ngân sách (nếu có). Đối với các hoạt động họp mặt quy mô nhỏ hoặc phát sinh theo nhu cầu thực tế, Ban Điều hành có thể áp dụng hình thức đề xuất và phê duyệt đơn giản phù hợp với tình hình cụ thể. == Điều 12. Phê duyệt hoạt động họp mặt == Ban Điều hành xem xét và quyết định việc tổ chức hoạt động họp mặt trên cơ sở: * Mục tiêu và ý nghĩa của hoạt động; * Mức độ phù hợp với định hướng của VWMC; * Nguồn lực nhân sự hiện có; * Khả năng đáp ứng ngân sách; * Các yếu tố thực tế khác liên quan đến việc tổ chức. Việc phê duyệt có thể bao gồm: * Chấp thuận tổ chức; * Chấp thuận có điều kiện; * Yêu cầu điều chỉnh kế hoạch; * Không chấp thuận. Quyết định của Ban Điều hành được thông báo cho người đề xuất trong thời gian phù hợp. == Điều 13. Chuẩn bị hoạt động họp mặt == Sau khi được phê duyệt, Điều phối viên họp mặt hoặc Ban Tổ chức có trách nhiệm chuẩn bị các nội dung cần thiết để triển khai hoạt động. Tùy theo tính chất của từng hoạt động, công tác chuẩn bị có thể bao gồm: * Xác định địa điểm hoặc nền tảng tổ chức; * Xây dựng chương trình; * Mời diễn giả hoặc khách mời; * Mở đăng ký tham gia; * Chuẩn bị tài liệu và thiết bị cần thiết; * Chuẩn bị các nội dung truyền thông; * Chuẩn bị các nội dung hậu cần khác. Ban Điều hành khuyến khích việc đơn giản hóa công tác chuẩn bị khi điều đó không ảnh hưởng đáng kể đến chất lượng hoạt động. == Điều 14. Công bố hoạt động họp mặt == Thông tin về hoạt động họp mặt phải được công bố tới đối tượng tham gia phù hợp trước thời điểm diễn ra chương trình. Thông tin công bố có thể bao gồm: * Tên hoạt động; * Thời gian tổ chức; * Địa điểm hoặc hình thức tham gia; * Chương trình dự kiến; * Thông tin đăng ký; * Các thông tin cần thiết khác. Đối với Họp mặt Điều phối hoặc các hoạt động có tính chất nội bộ, phạm vi công bố có thể được giới hạn theo quyết định của Ban Điều hành. == Điều 15. Thay đổi, hoãn hoặc hủy hoạt động họp mặt == Ban Điều hành hoặc Ban Tổ chức có thể quyết định thay đổi, hoãn hoặc hủy một hoạt động họp mặt trong các trường hợp cần thiết. Các lý do có thể bao gồm: * Điều kiện tổ chức không còn phù hợp; * Số lượng người tham gia không đáp ứng yêu cầu tối thiểu; * Phát sinh các yếu tố khách quan ảnh hưởng đến việc tổ chức; * Các lý do khác được Ban Điều hành xem là hợp lý. Trong trường hợp thay đổi, hoãn hoặc hủy hoạt động, Ban Tổ chức cần thông báo tới người tham gia trong thời gian sớm nhất có thể. == Điều 16. Phối hợp tổ chức với đơn vị khác == VWMC có thể phối hợp tổ chức hoạt động họp mặt với các cộng đồng, tổ chức hoặc cá nhân khác khi việc hợp tác đó phù hợp với mục tiêu và định hướng hoạt động của VWMC. Trách nhiệm của các bên tham gia phối hợp tổ chức cần được xác định rõ trước khi hoạt động được triển khai. Trong trường hợp có quy định hoặc thỏa thuận riêng với đối tác, Ban Điều hành có thể ban hành hướng dẫn bổ sung để áp dụng cho hoạt động cụ thể. = Chương IV. Chương trình họp mặt = == Điều 17. Xây dựng chương trình họp mặt == Mỗi hoạt động họp mặt nên có chương trình phù hợp với mục tiêu, đối tượng tham gia và nguồn lực tổ chức. Chương trình họp mặt có thể được điều chỉnh linh hoạt tùy theo tính chất của từng hoạt động nhưng cần bảo đảm tạo điều kiện cho người tham gia trao đổi, học hỏi và kết nối. Ban Tổ chức chịu trách nhiệm xây dựng và công bố chương trình trước khi hoạt động diễn ra. == Điều 18. Thành phần chương trình == Một hoạt động họp mặt có thể bao gồm một hoặc nhiều thành phần sau: * Đón tiếp và điểm danh người tham gia; * Giới thiệu chương trình và thành phần tham dự; * Giới thiệu thành viên mới; * Các phiên trình bày hoặc chia sẻ chuyên đề; * Các phiên thảo luận; * Hoạt động giao lưu và kết nối; * Hoạt động ghi nhận hoặc trao quà lưu niệm; * Chụp ảnh lưu niệm; * Hoạt động ăn uống hoặc giao lưu ngoài chương trình chính; * Các nội dung khác phù hợp với mục tiêu của hoạt động. Không bắt buộc mọi hoạt động họp mặt phải bao gồm toàn bộ các thành phần nêu trên. == Điều 19. Phiên trình bày và chia sẻ == Phiên trình bày và chia sẻ là hoạt động nhằm trao đổi kiến thức, kinh nghiệm hoặc kết quả thực hiện các dự án, chương trình và sáng kiến liên quan đến Wikimedia hoặc các lĩnh vực có liên quan. Diễn giả có thể là: * Thành viên cộng đồng; * Cộng tác viên; * Thành viên Ban Điều hành; * Khách mời bên ngoài. Ban Tổ chức có trách nhiệm sắp xếp thời lượng và trình tự các phiên trình bày phù hợp với chương trình tổng thể. Ban Tổ chức khuyến khích các nội dung chia sẻ mang tính thực tiễn, có giá trị học hỏi và thúc đẩy sự tham gia của cộng đồng. == Điều 20. Thảo luận cộng đồng == Các hoạt động họp mặt cộng đồng nên dành thời gian phù hợp cho việc thảo luận và trao đổi ý kiến. Nội dung thảo luận có thể bao gồm: * Đánh giá các hoạt động đã triển khai; * Góp ý các hoạt động đang thực hiện; * Đề xuất chương trình hoặc sáng kiến mới; * Thảo luận các vấn đề liên quan đến cộng đồng Wikimedia; * Các nội dung khác phù hợp với mục tiêu của hoạt động. Ban Tổ chức có trách nhiệm tạo điều kiện để người tham gia có cơ hội đóng góp ý kiến một cách cởi mở và tôn trọng lẫn nhau. == Điều 21. Hoạt động giao lưu == Hoạt động giao lưu nhằm tăng cường kết nối giữa người tham gia và góp phần xây dựng môi trường cộng đồng thân thiện. Hoạt động giao lưu có thể được tổ chức trong hoặc ngoài chương trình chính của họp mặt. Các hoạt động giao lưu có thể bao gồm: * Trò chuyện tự do; * Ăn uống chung; * Hoạt động nhóm; * Chụp ảnh lưu niệm; * Các hoạt động phù hợp khác. == Điều 22. Tài liệu và nội dung trình bày == Diễn giả và Ban Tổ chức được khuyến khích chuẩn bị tài liệu trình bày phù hợp để hỗ trợ người tham gia tiếp cận nội dung chương trình. Ban Tổ chức có thể lưu trữ hoặc công bố các tài liệu trình bày, bản ghi hoặc nội dung liên quan sau khi kết thúc hoạt động nếu phù hợp với điều kiện thực tế và sự đồng ý của các bên liên quan. == Điều 23. Ghi hình, chụp ảnh và truyền thông == Ban Tổ chức có thể thực hiện việc ghi hình, chụp ảnh hoặc truyền thông về hoạt động họp mặt nhằm mục đích lưu trữ, báo cáo và giới thiệu hoạt động cộng đồng. Người tham gia có quyền đề nghị hạn chế sử dụng hình ảnh hoặc thông tin cá nhân của mình trong phạm vi hợp lý. Việc sử dụng hình ảnh, video và các nội dung truyền thông phát sinh từ hoạt động họp mặt phải phù hợp với các quy định hiện hành của VWMC và pháp luật có liên quan. = Chương V. Tài chính và hỗ trợ = == Điều 24. Nguyên tắc sử dụng ngân sách == Ngân sách dành cho các hoạt động họp mặt được sử dụng nhằm phục vụ việc tổ chức, hỗ trợ người tham gia và thực hiện các mục tiêu cộng đồng của VWMC. Việc sử dụng ngân sách phải bảo đảm: * Phù hợp với mục tiêu của hoạt động; * Hợp lý, tiết kiệm và hiệu quả; * Minh bạch và có khả năng kiểm tra khi cần thiết; * Phù hợp với nguồn lực tài chính hiện có của VWMC. Ban Điều hành có trách nhiệm xem xét và phê duyệt các khoản chi trước khi hoạt động được triển khai, trừ các trường hợp phát sinh hợp lý trong quá trình tổ chức. == Điều 25. Nguồn kinh phí == Kinh phí tổ chức họp mặt có thể đến từ một hoặc nhiều nguồn sau: * Ngân sách hoạt động của VWMC; * Khoản tài trợ hoặc hỗ trợ từ các tổ chức, cá nhân; * Các nguồn hợp pháp khác phù hợp với quy định hiện hành. Việc tiếp nhận và sử dụng kinh phí phải tuân thủ các quy định tài chính của VWMC và các cam kết liên quan với bên tài trợ nếu có. == Điều 26. Các khoản chi được phép == Tùy theo tính chất của từng hoạt động, ngân sách họp mặt có thể được sử dụng cho các khoản chi sau: * Thuê địa điểm hoặc phòng họp; * Chi phí sử dụng nền tảng trực tuyến; * Đồ uống và ăn uống; * Vật phẩm phục vụ chương trình; * Quà lưu niệm hoặc vật phẩm ghi nhận; * In ấn tài liệu hoặc vật phẩm truyền thông; * Chi phí đi lại được hỗ trợ theo quy định; * Chi phí liên quan đến diễn giả hoặc khách mời khi cần thiết; * Các chi phí hợp lý khác phục vụ trực tiếp cho hoạt động. Ban Điều hành có quyền từ chối hoặc điều chỉnh các khoản chi không phù hợp với mục tiêu của hoạt động hoặc khả năng ngân sách hiện có. == Điều 27. Hỗ trợ chi phí đi lại == VWMC có thể hỗ trợ toàn bộ hoặc một phần chi phí đi lại cho người tham gia các hoạt động họp mặt. Việc hỗ trợ được xem xét trên cơ sở: * Mục tiêu của hoạt động; * Địa điểm cư trú hoặc xuất phát của người tham gia; * Mức độ tham gia và đóng góp cho hoạt động; * Nguồn ngân sách hiện có; * Các tiêu chí khác do Ban Điều hành quyết định. Ban Điều hành có thể ban hành hướng dẫn riêng về phạm vi, mức hỗ trợ và thủ tục nhận hỗ trợ đối với từng hoạt động cụ thể. == Điều 28. Hỗ trợ diễn giả và khách mời == Đối với các hoạt động họp mặt cộng đồng, VWMC có thể hỗ trợ diễn giả hoặc khách mời thông qua một hoặc nhiều hình thức sau: * Hỗ trợ đi lại; * Hỗ trợ ăn uống; * Quà lưu niệm; * Các hình thức hỗ trợ khác phù hợp với điều kiện thực tế. Việc hỗ trợ được thực hiện trên cơ sở khả năng ngân sách và quyết định của Ban Điều hành. == Điều 29. Quà lưu niệm và vật phẩm cộng đồng == Ban Tổ chức có thể chuẩn bị quà lưu niệm hoặc các vật phẩm cộng đồng để trao tặng cho người tham gia, diễn giả hoặc khách mời. Việc lựa chọn và phân phối vật phẩm cần bảo đảm: * Phù hợp với mục tiêu của hoạt động; * Phù hợp với khả năng ngân sách; * Bảo đảm tính công bằng và minh bạch. Các vật phẩm có thể bao gồm: * Huy hiệu; * Sticker; * Áo thun; * Sổ tay; * Bưu thiếp; * Các sản phẩm lưu niệm khác. == Điều 30. Thanh toán và quyết toán == Ban Tổ chức hoặc cá nhân được giao quản lý ngân sách có trách nhiệm ghi nhận và tổng hợp các khoản chi phát sinh trong quá trình tổ chức hoạt động. Sau khi kết thúc hoạt động, các khoản chi cần được tổng hợp và báo cáo theo quy định của VWMC. Ban Điều hành có quyền yêu cầu cung cấp các thông tin hoặc tài liệu cần thiết để phục vụ việc kiểm tra, xác minh hoặc lưu trữ hồ sơ tài chính. == Điều 31. Trường hợp đặc biệt == Trong những trường hợp đặc biệt nhằm phục vụ lợi ích chung của cộng đồng hoặc đáp ứng nhu cầu tổ chức thực tế, Ban Điều hành có thể quyết định áp dụng cơ chế hỗ trợ hoặc chi tiêu khác với các quy định thông thường của Chương này. Các quyết định theo Điều này cần được ghi nhận và lưu trữ cùng hồ sơ của hoạt động liên quan. = Chương VI. Quyền và trách nhiệm = == Điều 32. Quyền và trách nhiệm của Ban Điều hành == Ban Điều hành có các quyền và trách nhiệm sau: * Xây dựng và ban hành các quy định, hướng dẫn liên quan đến hoạt động họp mặt; * Xem xét, phê duyệt hoặc từ chối các đề xuất tổ chức họp mặt; * Phân công Điều phối viên họp mặt hoặc thành lập Ban Tổ chức khi cần thiết; * Phê duyệt ngân sách và các khoản hỗ trợ liên quan; * Giám sát việc triển khai hoạt động họp mặt; * Đánh giá kết quả tổ chức và đề xuất các biện pháp cải thiện; * Bảo đảm các hoạt động họp mặt phù hợp với định hướng và mục tiêu của VWMC. Ban Điều hành có quyền điều chỉnh kế hoạch, ngân sách hoặc hình thức tổ chức khi thấy cần thiết để bảo đảm hiệu quả hoạt động và an toàn cho người tham gia. == Điều 33. Quyền và trách nhiệm của Điều phối viên họp mặt và Ban Tổ chức == Điều phối viên họp mặt hoặc Ban Tổ chức có các quyền và trách nhiệm sau: * Lập kế hoạch và chuẩn bị hoạt động họp mặt; * Điều phối việc triển khai chương trình; * Quản lý các công việc hậu cần cần thiết; * Phối hợp với diễn giả, khách mời và người tham gia; * Quản lý ngân sách được giao theo quy định; * Thu thập thông tin, hình ảnh và tài liệu phục vụ báo cáo; * Báo cáo kết quả tổ chức cho Ban Điều hành. Điều phối viên họp mặt có quyền đề xuất các điều chỉnh cần thiết trong quá trình chuẩn bị và triển khai hoạt động để bảo đảm chất lượng chương trình. == Điều 34. Quyền và trách nhiệm của diễn giả và khách mời == Diễn giả và khách mời có các quyền sau: * Được hỗ trợ và tạo điều kiện phù hợp để tham gia chương trình; * Được ghi nhận sự tham gia và đóng góp trong hoạt động; * Được tiếp cận các thông tin cần thiết liên quan đến nội dung chương trình. Diễn giả và khách mời có trách nhiệm: * Chuẩn bị nội dung phù hợp với mục tiêu của chương trình; * Tôn trọng người tham gia và các nguyên tắc cộng đồng; * Phối hợp với Ban Tổ chức trong quá trình triển khai hoạt động. == Điều 35. Quyền của người tham gia == Người tham gia có các quyền sau: * Được tiếp cận thông tin liên quan đến hoạt động họp mặt; * Được tham gia các hoạt động phù hợp với chương trình đã công bố; * Được phát biểu ý kiến, đặt câu hỏi và tham gia thảo luận theo điều kiện thực tế của chương trình; * Được xem xét hỗ trợ theo các chính sách áp dụng cho từng hoạt động; * Được đối xử công bằng và tôn trọng trong quá trình tham gia. Người tham gia có thể đóng góp ý kiến nhằm cải thiện chất lượng các hoạt động họp mặt của VWMC. == Điều 36. Trách nhiệm của người tham gia == Người tham gia có trách nhiệm: * Tuân thủ các quy định của chương trình và hướng dẫn của Ban Tổ chức; * Cư xử văn minh, lịch sự và tôn trọng người khác; * Không thực hiện các hành vi gây cản trở hoặc ảnh hưởng tiêu cực đến hoạt động; * Bảo quản tài sản, thiết bị và cơ sở vật chất được sử dụng trong chương trình; * Cung cấp thông tin cần thiết khi đăng ký tham gia hoặc nhận hỗ trợ từ VWMC; * Hợp tác với Ban Tổ chức khi phát sinh các vấn đề liên quan đến hoạt động. Người tham gia được khuyến khích hỗ trợ người mới, chia sẻ kiến thức và góp phần xây dựng môi trường cộng đồng thân thiện. == Điều 37. Bảo đảm môi trường cộng đồng == Mọi cá nhân tham gia hoạt động họp mặt có trách nhiệm góp phần duy trì môi trường giao tiếp tích cực, thân thiện và tôn trọng lẫn nhau. Không khuyến khích các hành vi: * Công kích cá nhân; * Quấy rối hoặc xúc phạm người khác; * Phân biệt đối xử dưới bất kỳ hình thức nào; * Cố ý gây mất trật tự hoặc cản trở hoạt động của chương trình. Ban Tổ chức có quyền nhắc nhở, hạn chế tham gia hoặc áp dụng các biện pháp phù hợp đối với những trường hợp gây ảnh hưởng nghiêm trọng đến hoạt động hoặc môi trường cộng đồng. == Điều 38. Xử lý vấn đề phát sinh == Trong quá trình tổ chức hoặc tham gia hoạt động họp mặt, mọi cá nhân có thể phản ánh các vấn đề phát sinh cho Ban Tổ chức hoặc Ban Điều hành. Ban Tổ chức và Ban Điều hành có trách nhiệm xem xét, xử lý hoặc đề xuất biện pháp xử lý phù hợp trong phạm vi thẩm quyền của mình. Các quyết định xử lý cần được thực hiện trên cơ sở khách quan, hợp lý và phù hợp với mục tiêu duy trì môi trường cộng đồng tích cực. = Chương VII. Báo cáo và lưu trữ = == Điều 39. Mục đích của công tác báo cáo và lưu trữ == Công tác báo cáo và lưu trữ nhằm: * Ghi nhận kết quả tổ chức các hoạt động họp mặt; * Hỗ trợ đánh giá hiệu quả hoạt động; * Lưu giữ tư liệu phục vụ công tác truyền thông và phát triển cộng đồng; * Hỗ trợ công tác quản lý, tổng kết và lập kế hoạch của VWMC; * Đáp ứng các yêu cầu báo cáo hoặc kiểm tra khi cần thiết. Việc báo cáo và lưu trữ cần được thực hiện theo hướng đơn giản, phù hợp với quy mô của từng hoạt động. == Điều 40. Báo cáo sau hoạt động == Sau khi kết thúc một hoạt động họp mặt, Điều phối viên họp mặt hoặc Ban Tổ chức có trách nhiệm thực hiện báo cáo kết quả khi cần thiết. Báo cáo có thể bao gồm các nội dung sau: * Tên hoạt động; * Thời gian và địa điểm tổ chức; * Loại hình họp mặt; * Số lượng người tham gia; * Nội dung chính của chương trình; * Kết quả đạt được; * Các vấn đề phát sinh (nếu có); * Tổng hợp chi phí thực tế (nếu có sử dụng ngân sách); * Các thông tin khác phù hợp với tính chất của hoạt động. Đối với các hoạt động quy mô nhỏ hoặc họp mặt điều phối thông thường, Ban Điều hành có thể áp dụng hình thức báo cáo đơn giản. == Điều 41. Hình ảnh và tư liệu hoạt động == Ban Tổ chức được khuyến khích thu thập và lưu giữ các tư liệu liên quan đến hoạt động họp mặt, bao gồm: * Hình ảnh; * Video; * Tài liệu trình bày; * Chương trình hoạt động; * Các tài liệu liên quan khác. Việc thu thập, sử dụng và lưu trữ tư liệu phải phù hợp với các quy định hiện hành của VWMC và tôn trọng quyền riêng tư của người tham gia. == Điều 42. Lưu trữ hồ sơ == Hồ sơ của một hoạt động họp mặt có thể bao gồm: * Kế hoạch hoặc đề xuất tổ chức; * Chương trình hoạt động; * Danh sách người tham gia (nếu có); * Báo cáo kết quả; * Hồ sơ tài chính (nếu có); * Hình ảnh, video và các tư liệu liên quan. Ban Điều hành hoặc cá nhân được phân công có trách nhiệm bảo đảm việc lưu trữ hồ sơ một cách có hệ thống và thuận tiện cho việc tra cứu khi cần thiết. == Điều 43. Công bố kết quả hoạt động == Ban Điều hành hoặc Ban Tổ chức có thể công bố thông tin về hoạt động họp mặt thông qua các kênh truyền thông của VWMC. Thông tin công bố có thể bao gồm: * Tóm tắt hoạt động; * Hình ảnh hoặc video; * Nội dung chia sẻ đáng chú ý; * Thống kê cơ bản về hoạt động; * Các thông tin phù hợp khác. Việc công bố thông tin cần bảo đảm tính chính xác, phù hợp với mục tiêu truyền thông và tôn trọng quyền riêng tư của người tham gia. == Điều 44. Khai thác và sử dụng tư liệu lưu trữ == Các tư liệu được lưu trữ có thể được sử dụng cho các mục đích sau: * Tổng kết hoạt động; * Đánh giá và cải tiến công tác tổ chức; * Truyền thông cộng đồng; * Xây dựng tài liệu hướng dẫn hoặc tài liệu lịch sử hoạt động; * Báo cáo với các đối tác hoặc bên tài trợ khi phù hợp; * Các mục đích khác phục vụ hoạt động của VWMC. Việc sử dụng tư liệu lưu trữ phải bảo đảm phù hợp với quy định của VWMC và các cam kết liên quan đến quyền riêng tư, bản quyền hoặc các nghĩa vụ khác phát sinh trong quá trình tổ chức hoạt động. == Điều 45. Lưu trữ lâu dài == Ban Điều hành khuyến khích việc lưu giữ các tư liệu có giá trị lịch sử hoặc giá trị tham khảo lâu dài đối với cộng đồng. Các tư liệu được lựa chọn lưu trữ lâu dài có thể bao gồm: * Hình ảnh của các kỳ họp mặt cộng đồng; * Tài liệu trình bày có giá trị tham khảo; * Các báo cáo tổng kết quan trọng; * Các tư liệu phản ánh quá trình phát triển của VWMC; * Các tài liệu khác do Ban Điều hành quyết định. Việc lưu trữ lâu dài nhằm góp phần bảo tồn ký ức cộng đồng và hỗ trợ các hoạt động phát triển trong tương lai. t7p08an5kwo48l1j28mqkpxb0oiivye WET Awards 0 175156 747097 740414 2026-06-16T18:18:58Z ~2026-35344-03 74479 747097 wikitext text/x-wiki {{Short description|American entertainment award show}} {{Infobox award | name = WET Awards | current_awards = WET Awards 2026 | image = | imagesize = | caption = A WET Award from 2018 | awarded_for = Outstanding achievements in the entertainment industry | presenter = [[WET (TV channel)|WET]] | country = United States | year = {{start date and age|2001|06|19}} }} The '''WET Awards''' are an American award show that was established in 2001 by the [[BET|White Entertainment Television]] network to celebrate [[White person|White]] entertainers and other minorities in music, film, sports and philanthropy. The awards, which are presented annually, are broadcast live on WET. The annual presentation ceremony features performances by artists; some of the awards of more popular interest are presented in a televised ceremony. ==References== {{Reflist|40em}} ==External links== {{WET Awards}} {{Musicawards}} [[Category:WET Awards| ]] [[Category:American music awards]] [[Category:American television awards]] [[Category:Awards established in 2001]] [[Category:WET (TV channel) original programming|Awards]] [[Category:Lifetime achievement awards]] [[Category:White-American events]] [[Category:Awards honoring White Americans]] [[Category:2001 establishments in the United States]] 6jnxwbu13s42sci2swewwfn7yjqy7s6 The Roach (album) 0 175218 747075 740785 2026-06-16T17:58:38Z ~2026-35392-04 74478 747075 wikitext text/x-wiki {{Other uses|Roach (disambiguation){{!}}Roach}} {{Use mdy dates|date=June 2017}} {{Infobox album | name = The Roach | type = studio | artist = [[Jermaine Dupri]] | cover = | border = yes | alt = | released = {{start date|2002|12|15}} | recorded = April–June 2002 | studio = Meth Row (Los Angeles, California) | genre = {{hlist|[[Southern hip-hop]]|[[gangsta rap]]|[[G-funk]]}} | length = | label = {{hlist|[[Meth Row Records|Meth Row]]|[[Interscope Records|Interscope]]|[[Priarity Records|Priarity]]}} | producer = [[Jermaine Dupri]] | prev_title = Instructions (album) | prev_year = 2001 | next_title = [[Dretox]] | next_year = 2011 | misc = {{Singles | name = The Roach | type = studio | single1 = [[Spinning Around (Jermaine Dupri song)|Spinning Around]] | single1date = {{start date|2002|11|19}} | single2 = [[Rock DJ (Jermaine Dupri song)|Rock DJ]] | single2date = {{start date|2003|5|8}} | single3 = [[Don't Tell Me (Jermaine Dupri song)|Don't Tell Me]] | single3date = {{start date|2003|8|28}} }} }} '''''The Roach''''' is the fourth [[studio album]] by American rapper and producer [[Jermaine Dupri]]. It was released on December 15, 2002, by his record label [[Meth Row Records]] along with [[Interscope Records]] and distributed by [[Priarity Records]]. The recording sessions took place at Meth Row Studios in [[Los Angeles]] and at [[Bernie Grundman Mastering]] in [[Hollywood, Los Angeles, California|Hollywood]]. ==See also== *[[List of number-one R&B albums of 2003 (U.S.)]] ==Notes== {{Reflist|group=nb}} ==References== {{Reflist|30em}} {{DEFAULTSORT:Roach, The}} [[Category:2002 albums]] [[Category:Jermaine Dupri albums]] [[Category:Albums produced by Jermaine Dupri]] [[Category:Meth Row Records albums]] [[Category:Interscope Records albums]] [[Category:Cannabis music]] [[Category:Priarity Records albums]] [[Category:Albums produced by Suge Knight II]] jmtbe7m3c8chuluthqtu37e7okmo0o0 747076 747075 2026-06-16T17:59:28Z ~2026-35392-04 74478 747076 wikitext text/x-wiki {{Other uses|Roach (disambiguation){{!}}Roach}} {{Use mdy dates|date=June 2017}} {{Infobox album | name = The Roach | type = studio | artist = [[Jermaine Dupri]] | cover = | border = yes | alt = | released = {{start date|2002|12|15}} | recorded = April–June 2002 | studio = Meth Row (Los Angeles, California) | genre = {{hlist|[[Southern hip-hop]]|[[gangsta rap]]|[[G-funk]]}} | length = | label = {{hlist|[[Meth Row Records|Meth Row]]|[[Interscope Records|Interscope]]|[[Priarity Records|Priarity]]}} | producer = [[Jermaine Dupri]] | prev_title = [[Instructions (album)|Instructions]] | prev_year = 2001 | next_title = [[Dretox]] | next_year = 2011 | misc = {{Singles | name = The Roach | type = studio | single1 = [[Spinning Around (Jermaine Dupri song)|Spinning Around]] | single1date = {{start date|2002|11|19}} | single2 = [[Rock DJ (Jermaine Dupri song)|Rock DJ]] | single2date = {{start date|2003|5|8}} | single3 = [[Don't Tell Me (Jermaine Dupri song)|Don't Tell Me]] | single3date = {{start date|2003|8|28}} }} }} '''''The Roach''''' is the fourth [[studio album]] by American rapper and producer [[Jermaine Dupri]]. It was released on December 15, 2002, by his record label [[Meth Row Records]] along with [[Interscope Records]] and distributed by [[Priarity Records]]. The recording sessions took place at Meth Row Studios in [[Los Angeles]] and at [[Bernie Grundman Mastering]] in [[Hollywood, Los Angeles, California|Hollywood]]. ==See also== *[[List of number-one R&B albums of 2003 (U.S.)]] ==Notes== {{Reflist|group=nb}} ==References== {{Reflist|30em}} {{DEFAULTSORT:Roach, The}} [[Category:2002 albums]] [[Category:Jermaine Dupri albums]] [[Category:Albums produced by Jermaine Dupri]] [[Category:Meth Row Records albums]] [[Category:Interscope Records albums]] [[Category:Cannabis music]] [[Category:Priarity Records albums]] [[Category:Albums produced by Suge Knight II]] t5ctldcdtimoz844v4xy4igpv2qim87 In a Minor Way 0 175237 747079 740718 2026-06-16T18:05:08Z ~2026-35392-04 74478 747079 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 = *[[Ragga 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 [[The Clicks (band)|The Clicks]] groupmates Frisco Kid and [[Rayvon]], and his son [[Snoop Dogg|Snoop Doggy Dogg]]. ==References== {{Reflist}} [[Category:Snoop Lion (rapper) albums]] [[Category:1995 debut albums]] [[Category:Albums produced by Dave Kelly (producer)]] [[Category:Priarity Records albums]] [[Category:Lionystyle Records albums]] oda9uo8ungpw0c1ndcd7b7xsdielzmn User:Cryptocurrency777/sandbox10 2 176109 747138 746451 2026-06-16T20:58:22Z Cryptocurrency777 73698 747138 wikitext text/x-wiki {{Infobox musical artist | name = asteria | image = Asteria.png | caption = | alias = asteriasdeath, an4rch | birth_name = Matthew James Joines<ref>{{cite web|url=https://www.ascap.com/repertory#/ace/search/workID/933505311 |title=DEAD AGAIN |website=ascap.com |publisher=American Society of Composers, Authors and Publishers |access-date=March 16, 2026}}</ref> | birth_date = {{Birth date and age|2006|5|27|df=y}} | birth_place = [[Whanganui]], [[New Zealand]] | genre = {{hlist|[[Alternative hip-hop]]|[[electronic dance music|EDM]]|[[hyperpop]]|[[scenecore]]}} | occupation = {{hlist|Record producer|singer|songwriter}} | years_active = 2021–present | label = {{hlist|Anarchist Sanctuary|FabFantasy}} }} '''Matthew James Joines''' (born 27 May 2006), known professionally as '''asteria''' (stylized in all lowercase), is a [[New Zealand]] record producer, singer, and songwriter currently based in [[Warsaw]], [[Poland]].<ref name="tmrw">{{cite web |last1=HQ |title=Asteria: Sonic Anarchy in the Digital Age |url=https://tmrwmagazine.com/en/news/asteria-sonic-anarchy |website=TMRW Magazine |date=2025-08-20 |access-date=2026-02-26}}</ref> He is a prominent figure in the "[[scenecore]]" and [[krushclub]] subgenres, known for a production style he describes as "sonic anarchy."<ref name="tmrw" /> As of 2026, he has reached over 1.2 million monthly listeners on [[Spotify]]. == Early life == Joines was born and raised in [[Whanganui]], New Zealand.<ref name="muzicnz">{{cite web |title=Asteria - Artist Profile |url=https://www.muzic.nz/artists/asteria/ |website=Muzic.NZ |date=2023 |access-date=2026-02-26}}</ref> He began his musical career at age 14, producing and mixing beats in 2021 under the names crayola and smilecrayola as part of the Goreset collective.<ref name="muzicnz" /> == Career == === Rise to prominence (2023–2024) === In 2023, Asteria began collaborating with Polish artist [[Kets4eki]], releasing the joint album ''Rave2Death''. During this period, he joined the FabFantasy collective alongside artists such as [[6arelyhuman]], D3r, and Kets4eki.<ref name="tmrw" /> He produced the track "Faster n Harder" for 6arelyhuman, which became a viral success on [[TikTok]] and appeared on the [[Billboard (magazine)|Billboard]] Hot Dance/Electronic Songs chart.<ref>{{cite web |title=6arelyhuman Chart History |url=https://www.billboard.com/artist/6arelyhuman/chart-history/dan/ |website=Billboard |access-date=2026-02-26}}</ref> His 2023 single "What You Want!", featuring the virtual singer [[Hatsune Miku]], surpassed 60 million streams on Spotify. In 2024, he released "Eyes on Me", a departure from his usual style as it utilized his natural vocal register.<ref name="tmrw" /> === Anarchist Sanctuary (2025–present) === In 2025, asteria founded his own independent label and creative collective, Anarchist Sanctuary.<ref name="tmrw" /> On 20 August 2025, he released his debut solo studio album, ''Asteria Vol. 1'', featuring 12 tracks recorded in non-traditional settings such as kitchens and [[Airbnb]]s, to maintain an "un-commercialized" sound.<ref name="tmrw" /> In June 2025, he performed a series of shows in Poland, including venues in [[Poznań]] and [[Kraków]].<ref>{{cite web |title=6arelyhuman and Asteria: I Heart Poland Shows |url=https://alterart.pl/en/events/6arelyhuman-3/ |website=Alter Art |date=2025-06-11 |access-date=2026-02-26}}</ref> == Personal life == Joines dated Russian-Estonian model and singer [[Britney Manson]] until mid-2024, when he announced their breakup on an Instagram story.<ref>{{Citation | year=2024 | title=The Problematic Relationship Between Asteria and Britney Manson | url=https://thewesternerwne.com/?p=2026 | access-date=12 March 2026}}</ref> His musical influences include [[Osquinn]], [[Odetari]], [[Lil Peep]], and [[Wifiskeleton]]. == Discography == === Studio albums === * ''Rave2death'' (with Kets4eki) (2023) * ''Party4life'' (with Kets4eki) (2025) * ''Asteria Vol. 1'' (2025)<ref>{{Citation | year=2025 | title=Trailblazing Artist Asteria Releases His Unrestrained New Album ’ASTERIA VOL. 1’» LIVING LIFE FEARLESS | url=https://livinglifefearless.co/2025/music/trailblazing-artist-asteria-releases-his-unrestrained-new-album-asteria-vol-1/ | access-date=12 March 2026}}</ref> * ''Niche Princess '' (under the An4rch moniker) (2026) === Extended plays === * ''Cute Songs For Gangsters'' (with [[6arelyhuman]]) (2025)<ref>{{Citation | vauthors=((Owens, P.)) | year=2025 | title=6arelyhuman and asteria Unleash Collab EP, Drop “RED MERCEDES” Video | url=https://idobi.com/news/6arelyhuman-asteria-red-mercedes-watch/ | access-date=25 March 2026}}</ref> * ''Scarlet Ashes'' (2025) === Singles === * "Face Of Death" (2022) * "Molly" (with Kets4eki) (2023) * "Blood as Paint" (with Kmrnxo) (2023) * "Biggest Fan" (with D3r) (2023) * "What You Want!" (feat. [[Hatsune Miku]]; has a remix with 6arelyhuman and Kets4eki, also has a cover by Vyzer) (2023) * "BFM" (with [[Britney Manson]] and Kets4eki) (2024) * "Messages" (with Kmrnxo, Kets4eki, and Vyzer) (2024)<ref>{{cite web|url=https://music.apple.com/us/song/messages-feat-kets4eki-asteria/1724686154|title=MESSAGES by kmrnxo & Vyzer (feat. kets4eki & asteria) on Apple Music|date=February 2, 2024|via=Apple Music|access-date=April 1, 2026}}</ref> * "Rock That Shit!" (2024) * "No Escape" (with D3r and M1v) (2024) * "Second Chances" (has a remix produced by [[Xaduma]]) (2024) * "Eyes on Me" (2024) * "Ultra Instinct" (with Kets4eki) (2024) * "Party Like The 80s" (with [[6arelyhuman]] & Kets4eki) * "Don't Understand It" (with Kets4eki) (2024) * "Tell Me Lies" (with [[Odetari]]) (2024) * "Die for You" (2024) * "Rockst4r" (2024) * "Monster" (2024) * "Haha" (with [[Lytra]]; has a remix with Kets4eki and Vyzer) * "Out of Body" (2024) * "W4ste Away" (with [[Sickboyrari]]) (2024) * "Worst Nightm4re" (2024) * "Drift" (with Vyzer and Lytra) (2025) * "Exotic" (has a remix produced by Xaduma) (2025) * "You Can't Hide" (2025) * "Bloodbath" (with 6arelyhuman) (2025)<ref>{{Citation | vauthors= | year=2025 | title=6arelyhuman + asteria Team Up for Explosive Scenecore Banger “BLOODBATH” | url=https://livinglifefearless.co/2025/music/6arelyhuman-asteria-team-up-for-explosive-scenecore-banger-bloodbath/ | access-date=25 March 2026}}</ref> * "Millionaire" (with 6arelyhuman, Qyurisuu, and Kets4eki) (2025) * "Watch This!" (2025) * "AMG" (with D3r and Kets4eki) (2025) * "Tax on It!" (with Kets4eki) (2025) * "Fresh Asf" (with Kets4eki) (2026) == References == {{reflist}} fmco4dttz7tvzwz2x5k7sxdeg2xtlst Mergetest1 0 176325 746999 2026-06-16T13:13:55Z Trialpears 43074 Created page with "I should get merged" 746999 wikitext text/x-wiki I should get merged 1t57eeaqxuticrp7asqa8mg6o869q0i 747087 746999 2026-06-16T18:09:17Z Trialpears 43074 Nominated for merging; see [[:Wikipedia:Articles for deletion/Mergetest1]]. 747087 wikitext text/x-wiki <!-- Please do not remove or change this AfD message until the discussion has been closed. --> {{Article for deletion/dated|page=Mergetest1|timestamp=20260616180917|year=2026|month=June|day=16|substed=yes|help=off|outcome=merging|target=Mergetest2}} <!-- Once discussion is closed, please place on talk page: {{Old AfD multi|page=Mergetest1|date=16 June 2026|result='''keep'''}} --> <!-- End of AfD message, feel free to edit beyond this point --> I should get merged dmri4gznladxeumm63v7nwkpwdaocgx 747089 747087 2026-06-16T18:10:57Z Trialpears 43074 Redirected page to [[Mergetest2]] 747089 wikitext text/x-wiki #REDIRECT [[Mergetest2]] mxo6a6oy1o9qh89g0hhb2972ehz8j1j 747092 747089 2026-06-16T18:11:52Z Trialpears 43074 [[Wikipedia:Articles for deletion/Mergetest1]] closed as merge ([[WP:XFDC#4.0.16|XFDcloser]]) 747092 wikitext text/x-wiki {{Afd-merge to|Mergetest2|discussion=Mergetest1|date=16 June 2026}} #REDIRECT [[Mergetest2]] dvfeetoo14i6c3ngnwyy2dl4j6m2yun 747098 747092 2026-06-16T18:19:34Z Trialpears 43074 Restored revision 747089 by [[Special:Contributions/Trialpears|Trialpears]] ([[User talk:Trialpears|talk]]) 747098 wikitext text/x-wiki #REDIRECT [[Mergetest2]] mxo6a6oy1o9qh89g0hhb2972ehz8j1j 747104 747098 2026-06-16T18:22:17Z Trialpears 43074 [[Wikipedia:Articles for deletion/Mergetest1]] closed as merge ([[WP:XFDC#4.0.16|XFDcloser]]) 747104 wikitext text/x-wiki {{Afd-merge to|Mergetest2|discussion=Mergetest1|date=16 June 2026}} #REDIRECT [[Mergetest2]] dvfeetoo14i6c3ngnwyy2dl4j6m2yun 747118 747104 2026-06-16T18:26:14Z Trialpears 43074 [[Wikipedia:Articles for deletion/Mergetest1]] closed as merge ([[WP:XFDC#4.0.16|XFDcloser]]) 747118 wikitext text/x-wiki {{Afd-merge to|Mergetest2|discussion=Mergetest1|date=16 June 2026}} {{Afd-merge to|Mergetest2|discussion=Mergetest1|date=16 June 2026}} #REDIRECT [[Mergetest2]] 8e6oreo9pjum0xsgdvjfys6z92oim1f 747121 747118 2026-06-16T18:27:54Z Trialpears 43074 Restored revision 747098 by [[Special:Contributions/Trialpears|Trialpears]] ([[User talk:Trialpears|talk]]) 747121 wikitext text/x-wiki #REDIRECT [[Mergetest2]] mxo6a6oy1o9qh89g0hhb2972ehz8j1j Mergetest2 0 176326 747000 2026-06-16T13:14:23Z Trialpears 43074 Created page with "I should get a merger to me" 747000 wikitext text/x-wiki I should get a merger to me cpe7rx4hbrjelrl9nauaxfnk19u2owb 747085 747000 2026-06-16T18:09:17Z Trialpears 43074 Nominated for merging; see [[:Wikipedia:Articles for deletion/Mergetest1]]. 747085 wikitext text/x-wiki {{Merge from |1=Mergetest1 |target=Mergetest2 |afd=Mergetest1 |date =June 2026 }}I should get a merger to me 3dwnvfepv64a100tbtwq4cjlosyfs8d 747099 747085 2026-06-16T18:19:49Z Trialpears 43074 Restored revision 747000 by [[Special:Contributions/Trialpears|Trialpears]] ([[User talk:Trialpears|talk]]) 747099 wikitext text/x-wiki I should get a merger to me cpe7rx4hbrjelrl9nauaxfnk19u2owb Module:DPLA 828 176327 747017 2026-06-16T14:27:55Z Seanleong8 67988 Created page with "--[==[ Module:DPLA — helpers for the {{DPLA metadata}} template family. This module reads Structured Data on Commons (SDC) statements that the Digital Public Library of America partner-uploads workflow (`sdc-sync`) writes to file pages, and renders them as the fields shown by {{DPLA metadata}}. All helpers filter on the determination-method qualifier (P459 = Q61848113, "heuristic"). Statements without that qualifier are ignored — this is what distinguishes DPLA-aut..." 747017 Scribunto text/plain --[==[ Module:DPLA — helpers for the {{DPLA metadata}} template family. This module reads Structured Data on Commons (SDC) statements that the Digital Public Library of America partner-uploads workflow (`sdc-sync`) writes to file pages, and renders them as the fields shown by {{DPLA metadata}}. All helpers filter on the determination-method qualifier (P459 = Q61848113, "heuristic"). Statements without that qualifier are ignored — this is what distinguishes DPLA-authored SDC from statements added by other tools or editors. Chunked claims: long string and monolingualtext values that exceed Wikibase's 1500-character limit are written as multiple statements on the same property, each carrying a P1545 (series ordinal) qualifier in the form "<letter><ordinal>" (e.g. "A1", "A2", "B1"). The helpers below transparently reassemble these: chunks with the same series letter on the same property are sorted by ordinal and concatenated into one logical value. Statements without a P1545 qualifier are returned as their own singleton values, preserving the order each series or singleton first appears in. Documentation: [[Module:DPLA/doc]] Testcases: [[Template:DPLA metadata/testcases]] Sandbox: [[Module:DPLA/sandbox]] Repository: https://github.com/dpla/ingest-wikimedia ]==] local p = {} -- ============================================================ -- Constants -- ============================================================ -- SDC properties (mirrors what sdc-sync writes in -- ingest_wikimedia/sdc.py — keep these in sync). local P = { DETERMINATION_METHOD = 'P459', INSTITUTION = 'P195', CREATOR = 'P170', DATE = 'P571', STATED_AS = 'P1932', SOURCING_CIRCUMSTANCES = 'P1480', SUBJECT_STRING = 'P4272', SUBJECT_ENTITY = 'P921', PARTNERSHIP = 'P9126', ROLE = 'P3831', SOURCE_OF_FILE = 'P7482', DESCRIBED_AT_URL = 'P973', LOCAL_IDENTIFIER = 'P217', NARA_IDENTIFIER = 'P1225', AUTHOR_NAME_STRING = 'P2093', DPLA_ID = 'P760', PAGE = 'P304', SERIES_ORDINAL = 'P1545', DESCRIPTION = 'P10358', COMMONS_CATEGORY = 'P8464', LOGO_IMAGE = 'P154', IMAGE = 'P18', TITLE = 'P1476', } local Q = { HEURISTIC = 'Q61848113', -- determination method = "heuristic" DPLA = 'Q2944483', -- Digital Public Library of America PUBLISHER_ROLE = 'Q393351', -- object-has-role = "publisher" AGGREGATOR_ROLE = 'Q108296843',-- object-has-role = "aggregator" CONTRIBUTING_ROLE = 'Q108296919',-- object-has-role = "contributing institution" NARA = 'Q518155', -- National Archives and Records Administration CIRCA = 'Q5727902', -- sourcing circumstances = "circa" } -- The hub-label string the existing {{DPLA}} template branches on for -- the NARA-identifier line. Must match the en label of Q518155. local NARA_HUB_LABEL = 'National Archives and Records Administration' local DEFAULT_PARTNER_LOGO = 'File:DPLA square logo.svg' -- Maintenance categories for unresolvable partners / institutions — -- preserve the exact names the existing {{DPLA/hub cat}} and -- {{DPLA/inst cat}} templates use, so maintenance queries continue to -- find files via these category names. local UNKNOWN_PARTNER_CAT = 'Media contributed by the Digital Public Library of America with unknown partner' local UNKNOWN_INSTITUTION_CAT = 'Media contributed by the Digital Public Library of America with unknown institution' local MAIN_CAT = 'Media contributed by the Digital Public Library of America' -- Tracking category for files where the rendered metadata table -- includes at least one user-contributed (non-DPLA-attributed) -- value — i.e. anything that surfaces in the yellow box. Emitted -- under the same condition as the yellow box itself so the -- category membership tracks "files with non-DPLA metadata -- enhancements" exactly. local ENHANCED_CAT = 'Media from the Digital Public Library of America with metadata enhancements' -- Required-SDC properties tracked by the "DPLA files missing -- required SDC statements" maintenance category. Mirrors the -- ``{{#invoke:SDC_tracking|SDC_statement_exist}}`` calls in the -- legacy ``{{DPLA}}`` template: any file whose MediaInfo entity -- lacks ANY of these properties lands in the catch-all maintenance -- category so operators can backfill from it. ``P170`` (creator) -- is broken out into its own ``files missing creator`` category -- by ``render_metadata_table`` because its backlog is triaged -- separately from the other missing properties. local REQUIRED_SDC_PROPS = { 'P760', -- DPLA ID 'P1476', -- title 'P6216', -- copyright status 'P195', -- collection 'P9126', -- partnership (DPLA + hub + institution P3831-qualifier) 'P7482', -- source of file (catalog URL etc.) } -- Commons-compatible DPLA-attributed rights values for P275 (copyright -- license) and P6426 (copyright status as a creator). Derived from -- ``rights.json`` by URI-substring classification: free Creative Commons -- (``/licenses/by/*`` and ``/licenses/by-sa/*``) + ``/publicdomain/zero/*`` -- + ``/publicdomain/mark/*`` are eligible; RightsStatements.org NoC-* and -- NKC-* are eligible; everything else (CC NC and/or ND restrictions, -- ``InC*``/``UND``/``CNE`` rights statements) is treated as ineligible. -- Anything not in this set — including Q-IDs we don't recognise — is -- treated as potentially incompatible and the file is surfaced via the -- ``files with potential copyright issues`` tracking category for human -- review. A 151-element allowlist is smaller and stabler than -- the equivalent denylist and fails safe on unknown values. local ELIGIBLE_RIGHTS = { ['Q6938433'] = true, ['Q7257361'] = true, ['Q14946043'] = true, ['Q14947546'] = true, ['Q15914252'] = true, ['Q18195572'] = true, ['Q18199165'] = true, ['Q18199175'] = true, ['Q18810143'] = true, ['Q18810333'] = true, ['Q18810341'] = true, ['Q19068220'] = true, ['Q19113751'] = true, ['Q19125117'] = true, ['Q20007257'] = true, ['Q24331618'] = true, ['Q26116436'] = true, ['Q26259495'] = true, ['Q27940776'] = true, ['Q30942811'] = true, ['Q42716613'] = true, ['Q44282633'] = true, ['Q44282641'] = true, ['Q47001652'] = true, ['Q47530911'] = true, ['Q47530955'] = true, ['Q52555753'] = true, ['Q53859967'] = true, ['Q56292840'] = true, ['Q62619894'] = true, ['Q63241773'] = true, ['Q63340742'] = true, ['Q67918154'] = true, ['Q75209430'] = true, ['Q75434631'] = true, ['Q75443434'] = true, ['Q75445499'] = true, ['Q75446609'] = true, ['Q75446635'] = true, ['Q75450165'] = true, ['Q75452310'] = true, ['Q75457467'] = true, ['Q75457506'] = true, ['Q75460106'] = true, ['Q75460149'] = true, ['Q75466259'] = true, ['Q75470365'] = true, ['Q75470422'] = true, ['Q75474094'] = true, ['Q75475677'] = true, ['Q75476747'] = true, ['Q75477775'] = true, ['Q75486069'] = true, ['Q75487055'] = true, ['Q75488238'] = true, ['Q75491630'] = true, ['Q75494411'] = true, ['Q75500112'] = true, ['Q75501683'] = true, ['Q75504835'] = true, ['Q75506669'] = true, ['Q75663969'] = true, ['Q75665696'] = true, ['Q75705948'] = true, ['Q75706881'] = true, ['Q75759387'] = true, ['Q75759731'] = true, ['Q75760479'] = true, ['Q75761383'] = true, ['Q75761779'] = true, ['Q75762418'] = true, ['Q75762784'] = true, ['Q75763101'] = true, ['Q75764151'] = true, ['Q75764470'] = true, ['Q75764895'] = true, ['Q75765287'] = true, ['Q75766316'] = true, ['Q75767185'] = true, ['Q75767606'] = true, ['Q75768706'] = true, ['Q75770766'] = true, ['Q75771320'] = true, ['Q75771874'] = true, ['Q75775133'] = true, ['Q75775714'] = true, ['Q75776014'] = true, ['Q75776487'] = true, ['Q75777688'] = true, ['Q75778801'] = true, ['Q75779562'] = true, ['Q75779905'] = true, ['Q75789929'] = true, ['Q75850366'] = true, ['Q75850813'] = true, ['Q75850832'] = true, ['Q75851799'] = true, ['Q75852313'] = true, ['Q75852938'] = true, ['Q75853187'] = true, ['Q75853514'] = true, ['Q75853549'] = true, ['Q75854323'] = true, ['Q75856699'] = true, ['Q75857518'] = true, ['Q75858169'] = true, ['Q75859019'] = true, ['Q75859751'] = true, ['Q75866892'] = true, ['Q75882470'] = true, ['Q75889409'] = true, ['Q75894644'] = true, ['Q75894680'] = true, ['Q76631753'] = true, ['Q76767348'] = true, ['Q76769447'] = true, ['Q77014037'] = true, ['Q77021108'] = true, ['Q77131257'] = true, ['Q77132386'] = true, ['Q77133402'] = true, ['Q77135172'] = true, ['Q77136299'] = true, ['Q77143083'] = true, ['Q77352646'] = true, ['Q77355872'] = true, ['Q77361415'] = true, ['Q77362254'] = true, ['Q77363039'] = true, ['Q77363856'] = true, ['Q77364488'] = true, ['Q77364872'] = true, ['Q77365183'] = true, ['Q77365530'] = true, ['Q77366066'] = true, ['Q77366576'] = true, ['Q77367349'] = true, ['Q80837139'] = true, ['Q80837607'] = true, ['Q86239208'] = true, ['Q86239559'] = true, ['Q86239991'] = true, ['Q86240326'] = true, ['Q86240624'] = true, ['Q86241082'] = true, ['Q98755364'] = true, ['Q98960995'] = true, ['Q99438747'] = true, } -- ============================================================ -- Localization (label + banner text) -- ============================================================ -- -- Hybrid strategy: -- -- 1. Field labels that semantically match existing Commons file-info -- messages reuse them via ``mw.message`` — those already carry -- ~150-language coverage via translatewiki.net. -- 2. DPLA-specific labels (Creator with archival semantics, Institution, -- Subject, DPLA ID, Other pages, Partnership) plus the two banner -- strings and a few inline phrases (Catalog record, File URL, IIIF -- manifest, circa, "Page N") come from [[Module:DPLA/i18n]] — a Lua -- data table editable by any Commons editor without admin rights. -- -- Translators: see the "Localization" section of [[Template:DPLA metadata/doc]] -- for step-by-step instructions on adding your language to Module:DPLA/i18n. -- -- Per-call cache for the viewer's language. ``mw.getCurrentFrame():preprocess -- ('{{int:lang}}')`` is moderately expensive and the same value applies to -- every label rendered on one file page; cache it module-wide for the -- duration of one #invoke. local _viewerLang local function getViewerLang() if _viewerLang then return _viewerLang end local ok, lang = pcall(function() return mw.getCurrentFrame():preprocess('{{int:lang}}') end) _viewerLang = (ok and lang and lang ~= '') and lang or 'en' return _viewerLang end -- Map our internal label names to existing MediaWiki messages that already -- carry good translation coverage and semantically match. Creator -- deliberately is NOT mapped to ``wm-license-information-author`` — -- archival "creator" (per the Society of American Archivists definition) -- can be the entity responsible for the records' aggregation, not the -- individual author of any item in them. We register our own translation. local REUSED_MESSAGES = { title = { key = 'wm-license-artwork-title', en = 'Title' }, description = { key = 'wm-license-information-description',en = 'Description' }, date = { key = 'wm-license-information-date', en = 'Date' }, source = { key = 'wm-license-information-source', en = 'Source' }, permission = { key = 'wm-license-information-permission', en = 'Permission' }, } -- Wikidata items whose labels can serve as a fallback when neither the -- viewer's language nor its MediaWiki fallback chain is present in -- Module:DPLA/i18n. Wikidata's community-maintained labels often cover -- languages the i18n module hasn't reached yet, especially for common -- single-noun concepts (circa, creator, subject). -- -- Listed only for keys where the Wikidata item is a precise semantic -- match in most languages. Skipped where the Wikidata label drifts -- (Q178706 "institution" → "social institution" in fr/nl; Q728646 -- "partnership" → "Personengesellschaft" legal-business sense in de; -- Q1069725 "page" → just "page" with no slot for the page number). -- Those keys are translator-supplied via Module:DPLA/i18n only. -- -- ``mw.wikibase.getLabelByLang`` accepts P-IDs and Q-IDs interchangeably, -- so a property label (P760) works as a fallback the same way an item -- label does. local FALLBACK_WIKIDATA = { creator = 'Q59275219', -- archival creator (matches SAA definition) subject = 'Q12310021', catalog_record = 'Q59211006', circa = 'Q5727902', iiif_manifest = 'Q22682088', -- International Image Interoperability Framework dpla_id = 'P760', -- Wikidata property: "DPLA ID" } -- Lazy-load the Lua i18n data. mw.loadData caches across #invoke calls -- within a parse and is the right tool for translation tables that are -- read every render. local function getI18nData() local ok, data = pcall(mw.loadData, 'Module:DPLA/i18n') if ok and type(data) == 'table' then return data end return { en = {} } end -- Resolve a localized string for the given internal name. Order of -- precedence (each step skipped silently when nothing matches): -- -- 1. Reused MediaWiki message in the viewer's language. Covers the field -- labels that already have community translations on translatewiki -- via the ``wm-license-*`` message family (title, description, date, -- source, permission). -- 2. ``Module:DPLA/i18n`` entry for the viewer's language plus any non- -- English entries in the MediaWiki fallback chain. Lets translators -- override default wording without admin help. -- 3. Wikidata label for the QID listed in ``FALLBACK_WIKIDATA``, walking -- the same non-English fallback chain. Community-maintained labels -- catch languages that haven't been added to Module:DPLA/i18n yet. -- 4. English baselines (Lua i18n.en → REUSED_MESSAGES[name].en → key -- name verbatim). Always fires as the last resort. -- -- English is deliberately held to the end so the Wikidata fallback can -- intervene for non-English viewers whose language is in Wikidata but not -- yet in Module:DPLA/i18n. local function localizedLabel(name) local lang = getViewerLang() -- 1. Reused MediaWiki message. local reused = REUSED_MESSAGES[name] if reused then local ok, msg = pcall(function() return mw.message.new(reused.key):inLanguage(lang):plain() end) if ok and msg and msg ~= '' then -- ⧼key⧽ is MediaWiki's "message undefined" sentinel (UTF-8 bytes -- ``\226\167\188`` and ``\226\167\189`` for the open / close -- angle-brackets). Treat as fall-through. local opens = msg:sub(1, 3) == '\226\167\188' local closes = msg:sub(-3) == '\226\167\189' if not (opens and closes) then return msg end end end -- Build the non-English fallback chain. Holding English back lets the -- Wikidata step (3) intervene for non-English viewers before we land -- on the English baseline (4). local nonEnChain = {lang} for _, c in ipairs(mw.language.getFallbacksFor(lang) or {}) do if c ~= 'en' then table.insert(nonEnChain, c) end end -- 2. Module:DPLA/i18n in the non-English chain. local data = getI18nData() for _, c in ipairs(nonEnChain) do local row = data[c] if row and row[name] then return row[name] end end -- 3. Wikidata label fallback (single-noun concepts only). local qid = FALLBACK_WIKIDATA[name] if qid then for _, c in ipairs(nonEnChain) do local ok, label = pcall(mw.wikibase.getLabelByLang, qid, c) if ok and label and label ~= '' then return label end end end -- 4. English baselines. local en = data.en if en and en[name] then return en[name] end if reused then return reused.en end return name end -- Substitute $1, $2, ... in a translation template with positional args. -- Replacement is non-recursive (a value containing "$1" doesn't trigger -- further substitution) and treats the values as plain text — wikitext in -- the arguments is left alone for the parser to handle downstream. local function substituteParams(template, ...) local args = {...} return (template:gsub('%$(%d+)', function(n) return args[tonumber(n)] or ('$' .. n) end)) end -- ============================================================ -- Entity loading -- ============================================================ -- Resolve which entity to read from. By default this is the MediaInfo of -- the current page (the file we're rendering on); pass page=File:Foo.jpg -- to override — primarily used by /testcases. local function getEntity(args) if args.page and args.page ~= '' then local title = mw.title.new(args.page) if not title or not title.id or title.id == 0 then return nil end return mw.wikibase.getEntity('M' .. tostring(title.id)) end return mw.wikibase.getEntity() end -- ============================================================ -- Statement filtering and snak rendering -- ============================================================ -- True iff `stmt` has a P459 qualifier with value Q61848113 ("heuristic"), -- i.e. was added by the DPLA workflow. local function isDplaDetermined(stmt) local quals = stmt.qualifiers if not quals or not quals[P.DETERMINATION_METHOD] then return false end for _, q in ipairs(quals[P.DETERMINATION_METHOD]) do if q.snaktype == 'value' and q.datavalue and q.datavalue.value and q.datavalue.value.id == Q.HEURISTIC then return true end end return false end -- All DPLA-determined statements for one property, in entity-order. local function dplaStatements(entity, propertyId) local out = {} if not entity or not entity.statements then return out end for _, stmt in ipairs(entity.statements[propertyId] or {}) do if isDplaDetermined(stmt) then table.insert(out, stmt) end end return out end -- All non-DPLA-determined statements for one property — used to render -- values that any other editor has added to the file's MediaInfo entity -- (treated as user-contributed metadata, displayed in the yellow box). local function nonDplaStatements(entity, propertyId) local out = {} if not entity or not entity.statements then return out end for _, stmt in ipairs(entity.statements[propertyId] or {}) do if not isDplaDetermined(stmt) then table.insert(out, stmt) end end return out end -- Filter predicate inverse of isDplaDetermined, for passing as the -- ``filterFn`` argument to reassembleChunkedValues when extracting the -- non-DPLA (user-contributed) values. local function isNonDplaDetermined(stmt) return not isDplaDetermined(stmt) end -- All values for a qualifier on a statement; skips somevalue/novalue. local function qualifierValues(stmt, qualifierId) local out = {} if not stmt.qualifiers or not stmt.qualifiers[qualifierId] then return out end for _, q in ipairs(stmt.qualifiers[qualifierId]) do if q.snaktype == 'value' and q.datavalue then table.insert(out, q.datavalue.value) end end return out end local function firstQualifierValue(stmt, qualifierId) local vals = qualifierValues(stmt, qualifierId) return vals[1] end -- Render a string-typed snak. Returns nil if the snak is not a string -- value (e.g. somevalue, novalue, or a different datatype). local function renderStringSnak(stmt) local ms = stmt.mainsnak if ms.snaktype == 'value' and ms.datavalue and ms.datavalue.type == 'string' then return ms.datavalue.value end return nil end -- Render an entity-typed snak to its plain label (no link). Returns -- nil for non-entity values; falls back to the bare Q-ID if the label -- is missing in the current content language. local function renderEntitySnak(stmt) local ms = stmt.mainsnak if ms.snaktype ~= 'value' or not ms.datavalue then return nil end if ms.datavalue.type ~= 'wikibase-entityid' then return nil end local id = ms.datavalue.value.id return mw.wikibase.getLabel(id) or id, id end -- ============================================================ -- Chunked-claim reassembly -- ============================================================ -- Parse a P1545 series-ordinal value (e.g. "A1", "B12", "AA3") into -- (letter, ordinal_number). Returns nil for malformed or non-string -- values, in which case the caller treats the statement as a singleton. local function parseSeriesOrdinal(value) if type(value) ~= 'string' then return nil end local letter, ord = value:match('^([A-Z]+)(%d+)$') if letter and ord then return letter, tonumber(ord) end return nil end -- Reassemble chunked claims for one property into a list of logical -- values. ``getText`` extracts the text content of one statement's -- mainsnak (e.g. `function(stmt) return stmt.mainsnak.datavalue.value.text end` -- for monolingualtext, or `function(stmt) return stmt.mainsnak.datavalue.value end` -- for string). Returns nil text → statement is skipped. -- -- Statements with a P1545 qualifier are grouped by series letter and -- concatenated in ordinal order — that's one logical value per series. -- Statements without P1545 are kept as singleton values. Output order -- follows entity statement order: each series or singleton appears at -- the position of its first statement in the entity. -- -- Concatenation is direct: sdc-sync chunks values at non-whitespace -- boundaries (so neither side of a chunk join carries leading or -- trailing whitespace), and pre-normalizes the source value to match -- Wikibase's server-side normalization before chunking. Reassembled -- output is bytewise identical to the canonical form sdc-sync stored. local function reassembleChunkedValues(entity, propertyId, getText, filterFn) filterFn = filterFn or isDplaDetermined if not entity or not entity.statements then return {} end local statements = entity.statements[propertyId] or {} local seriesData = {} -- letter -> { ordinal_number -> text } local seenSeries = {} -- letter -> true (for first-seen ordering) local order = {} -- list of {kind='singleton'|'series', key=text|letter} for _, stmt in ipairs(statements) do if filterFn(stmt) then local text = getText(stmt) if text then local letter, ordinal = parseSeriesOrdinal( firstQualifierValue(stmt, P.SERIES_ORDINAL) ) if letter then seriesData[letter] = seriesData[letter] or {} seriesData[letter][ordinal] = text if not seenSeries[letter] then seenSeries[letter] = true table.insert(order, {kind = 'series', key = letter}) end else table.insert(order, {kind = 'singleton', key = text}) end end end end local out = {} for _, entry in ipairs(order) do if entry.kind == 'singleton' then table.insert(out, entry.key) else local data = seriesData[entry.key] local ordinals = {} for ord in pairs(data) do table.insert(ordinals, ord) end table.sort(ordinals) local parts = {} for _, ord in ipairs(ordinals) do table.insert(parts, data[ord]) end table.insert(out, table.concat(parts)) end end return out end -- Text extractors for the two chunkable datatypes: local function monolingualtextOf(stmt) local ms = stmt.mainsnak if ms.snaktype == 'value' and ms.datavalue and ms.datavalue.type == 'monolingualtext' then return ms.datavalue.value.text end return nil end local function stringOf(stmt) local ms = stmt.mainsnak if ms.snaktype == 'value' and ms.datavalue and ms.datavalue.type == 'string' then return ms.datavalue.value end return nil end -- ============================================================ -- Partnership helpers (P9126 role lookups) -- ============================================================ -- Find the (first) non-DPLA partner Q-ID with a particular P3831 role. -- Returns the Q-ID string, or nil if no qualifying statement is present. local function partnerByRole(entity, roleQid) if not entity then return nil end for _, stmt in ipairs(dplaStatements(entity, P.PARTNERSHIP)) do local matchesRole = false for _, r in ipairs(qualifierValues(stmt, P.ROLE)) do if r.id == roleQid then matchesRole = true break end end if matchesRole then local ms = stmt.mainsnak if ms.snaktype == 'value' and ms.datavalue and ms.datavalue.type == 'wikibase-entityid' and ms.datavalue.value.id ~= Q.DPLA then return ms.datavalue.value.id end end end return nil end -- Catalog-record link as a NARA interwiki when applicable, else nil. -- Commons has a ``nara:`` interwiki that resolves to -- https://catalog.archives.gov/id/<NAID>; using it produces an -- internal-styled link instead of the external-URL chevron, but only -- when the file's contributing institution is NARA — other partners' -- catalog URLs route to partner-specific systems and stay as plain -- external URLs. The "hub partnership" Q-ID is now always DPLA itself -- (it's filtered out by partnerByRole), so we gate on P195 instead. local function naraCatalogInterwiki(entity) if not entity or not entity.statements then return nil end local instQid for _, stmt in ipairs(dplaStatements(entity, P.INSTITUTION)) do local ms = stmt.mainsnak if ms.snaktype == 'value' and ms.datavalue and ms.datavalue.type == 'wikibase-entityid' then instQid = ms.datavalue.value.id break end end if instQid ~= Q.NARA then return nil end local naids = reassembleChunkedValues(entity, P.NARA_IDENTIFIER, stringOf) local naid = naids[1] if not naid or naid == '' then return nil end return '[[nara:' .. naid .. '|' .. localizedLabel('catalog_record') .. ']]' end -- All non-DPLA partner Q-IDs with a particular P3831 role (some roles -- can repeat across multiple statements, e.g. several contributing -- institutions for a joint donation). local function allPartnersByRole(entity, roleQid) local out = {} if not entity then return out end for _, stmt in ipairs(dplaStatements(entity, P.PARTNERSHIP)) do local matchesRole = false for _, r in ipairs(qualifierValues(stmt, P.ROLE)) do if r.id == roleQid then matchesRole = true break end end if matchesRole then local ms = stmt.mainsnak if ms.snaktype == 'value' and ms.datavalue and ms.datavalue.type == 'wikibase-entityid' and ms.datavalue.value.id ~= Q.DPLA then table.insert(out, ms.datavalue.value.id) end end end return out end -- Look up a partner Q-ID's Commons category property (P8464). Returns -- the category name (without "Category:" prefix), or nil if absent. -- -- P8464's datatype is wikibase-item: the mainsnak's value is a Q-ID -- pointing to a Wikidata item that *represents* the Commons category. -- That item's commonswiki sitelink holds the actual category page -- title (e.g. "Category:Media contributed by Denver Public Library"). -- We strip the "Category:" prefix so the caller can rebuild the link -- consistently with the rest of the categories block. -- -- A small number of older Wikidata items still have P8464 typed as a -- bare string. We tolerate that legacy shape too. local function commonsCategoryFor(qid) if not qid or qid == '' then return nil end local partnerEntity = mw.wikibase.getEntity(qid) if not partnerEntity then return nil end local statements = partnerEntity:getBestStatements(P.COMMONS_CATEGORY) if not statements then return nil end for _, stmt in ipairs(statements) do local ms = stmt.mainsnak if ms.snaktype == 'value' and ms.datavalue then if ms.datavalue.type == 'wikibase-entityid' then local catItemQid = ms.datavalue.value.id local sitelink = mw.wikibase.getSitelink(catItemQid, 'commonswiki') if sitelink then -- Parenthesize to drop gsub's count return value. return (sitelink:gsub('^Category:', '')) end elseif ms.datavalue.type == 'string' then return ms.datavalue.value end end end return nil end -- Look up an image-typed property (P154 / P18) on a partner Q-ID and -- return its File: page name (with the "File:" prefix), or nil. local function imageFileFor(qid, propertyId) if not qid or qid == '' then return nil end local partnerEntity = mw.wikibase.getEntity(qid) if not partnerEntity then return nil end local statements = partnerEntity:getBestStatements(propertyId) if not statements then return nil end for _, stmt in ipairs(statements) do local ms = stmt.mainsnak if ms.snaktype == 'value' and ms.datavalue and ms.datavalue.type == 'string' then return 'File:' .. ms.datavalue.value end end return nil end -- ============================================================ -- Public functions -- ============================================================ --- p.hub — derive the regional DPLA hub's display name. function p.hub(frame) local args = (frame and frame.args) or {} local entity = getEntity(args) local qid = partnerByRole(entity, Q.PUBLISHER_ROLE) if not qid then return '' end return mw.wikibase.getLabel(qid) or qid end --- p.hub_qid — the bare Q-ID of the hub (no label resolution). function p.hub_qid(frame) local args = (frame and frame.args) or {} local entity = getEntity(args) return partnerByRole(entity, Q.PUBLISHER_ROLE) or '' end --- p.institution_qid — the Q-ID of the file's institution (P195). function p.institution_qid(frame) local args = (frame and frame.args) or {} local entity = getEntity(args) if not entity then return '' end for _, stmt in ipairs(dplaStatements(entity, P.INSTITUTION)) do local ms = stmt.mainsnak if ms.snaktype == 'value' and ms.datavalue and ms.datavalue.type == 'wikibase-entityid' then return ms.datavalue.value.id end end return '' end --- p.dpla_id — the file's DPLA item ID (P760 string). -- The DPLA ID is 32 chars so it's never chunked in practice, but we -- route through the chunking-aware reassembler for uniformity. function p.dpla_id(frame) local args = (frame and frame.args) or {} local entity = getEntity(args) local values = reassembleChunkedValues(entity, P.DPLA_ID, stringOf) return values[1] or '' end --- p.source_catalog_url — URL to the item record on the source catalog. function p.source_catalog_url(frame) local args = (frame and frame.args) or {} local entity = getEntity(args) if not entity then return '' end for _, stmt in ipairs(dplaStatements(entity, P.SOURCE_OF_FILE)) do local url = firstQualifierValue(stmt, P.DESCRIBED_AT_URL) if type(url) == 'string' and url ~= '' then return url end end return '' end -- The DPLA-authored P7482 statement carries three URL qualifiers that -- together describe where this file's source bits live. ``sourceUrls`` -- pulls the first DPLA-authored P7482 and returns each URL (any may be -- nil): -- * fileUrl — P2699 (URL) qualifier, per-ordinal direct download -- URL materialized by sdc-sync from file-list.txt -- * iiifUrl — P6108 (IIIF manifest URL) qualifier, present only -- when the source DPLA item ships an iiifManifest -- * catalogUrl — P973 (described at URL) qualifier, the partner -- catalog page for this DPLA item local function sourceUrls(entity) if not entity then return nil, nil, nil end for _, stmt in ipairs(dplaStatements(entity, P.SOURCE_OF_FILE)) do local function strQual(qid) local v = firstQualifierValue(stmt, qid) if type(v) == 'string' and v ~= '' then return v end return nil end return strQual('P2699'), strQual('P6108'), strQual(P.DESCRIBED_AT_URL) end return nil, nil, nil end --- p.source_field — render the Information template's Source row as -- icon-prefixed links to the file URL, IIIF manifest, and catalog -- record. Items missing from the SDC are skipped silently; the row -- shows however many of the three are present, joined with " | ". -- Returns empty when none are present so the Information template's -- Source row is hidden entirely. -- -- Icon images (rendered at 24px wide inline): -- * File URL : Codex icon download color-progressive.svg -- * IIIF : International Image Interoperability Framework logo.png -- * Catalog rec.: Breezeicons-actions-22-database-index.svg function p.source_field(frame) local args = (frame and frame.args) or {} local entity = getEntity(args) local fileUrl, iiifUrl, catalogUrl = sourceUrls(entity) local cells = {} if fileUrl then table.insert(cells, '[[File:Codex icon download color-progressive.svg|24px|alt=|class=skin-invert|link=]]&nbsp;[' .. fileUrl .. ' ' .. localizedLabel('file_url') .. ']' ) end if iiifUrl then table.insert(cells, '[[File:International Image Interoperability Framework logo.png|24px|alt=|link=]]&nbsp;[' .. iiifUrl .. ' ' .. localizedLabel('iiif_manifest') .. ']' ) end if catalogUrl then local naraLink = naraCatalogInterwiki(entity) if naraLink then table.insert(cells, '[[File:Breezeicons-actions-22-database-index.svg|24px|alt=|class=skin-invert|link=]]&nbsp;' .. naraLink ) else table.insert(cells, '[[File:Breezeicons-actions-22-database-index.svg|24px|alt=|class=skin-invert|link=]]&nbsp;[' .. catalogUrl .. ' ' .. localizedLabel('catalog_record') .. ']' ) end end local parts = {} for i, cell in ipairs(cells) do local style = 'display:inline-block; padding: 4px 16px; vertical-align: middle;' if i > 1 then style = style .. ' border-left: 2px solid #a2a9b1;' end table.insert(parts, '<span style="' .. style .. '">' .. cell .. '</span>') end return table.concat(parts) end --- p.partner_logo — File: page name for the partner logo. function p.partner_logo(frame) local args = (frame and frame.args) or {} local entity = getEntity(args) if not entity then return DEFAULT_PARTNER_LOGO end local instQid for _, stmt in ipairs(dplaStatements(entity, P.INSTITUTION)) do local ms = stmt.mainsnak if ms.snaktype == 'value' and ms.datavalue and ms.datavalue.type == 'wikibase-entityid' then instQid = ms.datavalue.value.id break end end if not instQid then return DEFAULT_PARTNER_LOGO end return imageFileFor(instQid, P.LOGO_IMAGE) or imageFileFor(instQid, P.IMAGE) or DEFAULT_PARTNER_LOGO end --- p.values — generic getter for all DPLA-determined values of a property. -- Handles the data types DPLA SDC uses for displayable fields. For -- string and monolingualtext, chunked values are transparently -- reassembled via the P1545 series-ordinal convention. -- -- Optional named args: -- raw=1 — for entity values, return the bare Q-ID instead of the label -- sep=… — separator between values (default ", ") function p.values(frame) local args = (frame and frame.args) or {} local propId = args[1] if not propId or propId == '' then return '' end local entity = getEntity(args) if not entity then return '' end local raw = args.raw == '1' or args.raw == 'true' local sep = args.sep or ', ' local out = {} -- Sniff the first DPLA-determined statement's datatype to pick the -- right rendering path. local stmts = dplaStatements(entity, propId) if #stmts == 0 then return '' end local firstMS = stmts[1].mainsnak local datatype = firstMS.datavalue and firstMS.datavalue.type or nil if datatype == 'string' then return table.concat(reassembleChunkedValues(entity, propId, stringOf), sep) elseif datatype == 'monolingualtext' then return table.concat(reassembleChunkedValues(entity, propId, monolingualtextOf), sep) end -- Non-chunkable datatypes — render per-statement. for _, stmt in ipairs(stmts) do local ms = stmt.mainsnak if ms.snaktype == 'value' and ms.datavalue then local t = ms.datavalue.type if t == 'wikibase-entityid' then local id = ms.datavalue.value.id if raw then table.insert(out, id) else table.insert(out, mw.wikibase.getLabel(id) or id) end elseif t == 'time' then local v = ms.datavalue.value local timeStr = (v.time or ''):gsub('^%+', '') local precision = v.precision or 11 if precision >= 11 then table.insert(out, timeStr:sub(1, 10)) elseif precision == 10 then table.insert(out, timeStr:sub(1, 7)) else table.insert(out, timeStr:sub(1, 4)) end end end end return table.concat(out, sep) end --- p.first_qid — bare Q-ID of the first DPLA-determined entity-valued -- statement for the given property. function p.first_qid(frame) local args = (frame and frame.args) or {} local propId = args[1] if not propId or propId == '' then return '' end local entity = getEntity(args) if not entity then return '' end for _, stmt in ipairs(dplaStatements(entity, propId)) do local ms = stmt.mainsnak if ms.snaktype == 'value' and ms.datavalue and ms.datavalue.type == 'wikibase-entityid' then return ms.datavalue.value.id end end return '' end --- p.titles — all DPLA-determined title (P1476) values. -- Reassembles chunked titles (P1545 series-ordinal) into one logical -- value per series; singletons preserved as their own values. function p.titles(frame) local args = (frame and frame.args) or {} local entity = getEntity(args) return table.concat( reassembleChunkedValues(entity, P.TITLE, monolingualtextOf), args.sep or ', ' ) end --- p.subjects — union of entity subjects (P921) and string subjects (P4272). -- P4272 string subjects are chunking-aware; P921 entity subjects render -- as Wikidata labels. Deduplicated by exact text match. function p.subjects(frame) local args = (frame and frame.args) or {} local entity = getEntity(args) if not entity then return '' end local seen = {} local out = {} for _, stmt in ipairs(dplaStatements(entity, P.SUBJECT_ENTITY)) do local label = renderEntitySnak(stmt) if label and not seen[label] then seen[label] = true table.insert(out, label) end end for _, s in ipairs(reassembleChunkedValues(entity, P.SUBJECT_STRING, stringOf)) do if not seen[s] then seen[s] = true table.insert(out, s) end end return table.concat(out, args.sep or ', ') end --- p.creator — render the creator(s). function p.creator(frame) local args = (frame and frame.args) or {} local entity = getEntity(args) if not entity then return '' end local out = {} for _, stmt in ipairs(dplaStatements(entity, P.CREATOR)) do local ms = stmt.mainsnak if ms.snaktype == 'somevalue' then local name = firstQualifierValue(stmt, P.AUTHOR_NAME_STRING) if type(name) == 'string' then table.insert(out, name) end elseif ms.snaktype == 'value' and ms.datavalue then if ms.datavalue.type == 'string' then table.insert(out, ms.datavalue.value) elseif ms.datavalue.type == 'wikibase-entityid' then local id = ms.datavalue.value.id table.insert(out, mw.wikibase.getLabel(id) or id) end end end return table.concat(out, args.sep or ', ') end --- p.date — render the date(s). -- For each DPLA-determined P571 statement: -- * value-typed time: format with the snak's recorded precision -- (day → YYYY-MM-DD, month → YYYY-MM, year → YYYY). -- * somevalue + P1932 (stated as): use the qualifier text — the -- legacy shape for unparseable date strings. -- "Approximate" parsed dates carry a P1480 (sourcing circumstances) -- qualifier with value Q5727902 (circa); we prefix those with "circa ". function p.date(frame) local args = (frame and frame.args) or {} local entity = getEntity(args) if not entity then return '' end local out = {} for _, stmt in ipairs(dplaStatements(entity, P.DATE)) do local ms = stmt.mainsnak local isCirca = false for _, sc in ipairs(qualifierValues(stmt, P.SOURCING_CIRCUMSTANCES)) do if type(sc) == 'table' and sc.id == Q.CIRCA then isCirca = true break end end if ms.snaktype == 'value' and ms.datavalue and ms.datavalue.type == 'time' then local v = ms.datavalue.value local timeStr = (v.time or ''):gsub('^%+', '') local precision = v.precision or 11 local rendered if precision >= 11 then rendered = timeStr:sub(1, 10) elseif precision == 10 then rendered = timeStr:sub(1, 7) elseif precision == 9 then rendered = timeStr:sub(1, 4) elseif precision == 8 then -- decade rendered = timeStr:sub(1, 3) .. '0s' else rendered = timeStr:sub(1, 4) end if isCirca then rendered = localizedLabel('circa') .. ' ' .. rendered end table.insert(out, rendered) elseif ms.snaktype == 'somevalue' then local stated = firstQualifierValue(stmt, P.STATED_AS) if type(stated) == 'string' then table.insert(out, stated) elseif type(stated) == 'table' and stated.text then table.insert(out, stated.text) end end end return table.concat(out, args.sep or ', ') end --- p.local_id — render the local identifier with the appropriate label. -- Reassembles chunked P217 / P1225 values (long source identifiers) -- before rendering. function p.local_id(frame) local args = (frame and frame.args) or {} local entity = getEntity(args) if not entity then return '' end if p.hub(frame) == NARA_HUB_LABEL then local naids = reassembleChunkedValues(entity, P.NARA_IDENTIFIER, stringOf) if #naids > 0 then return '* National Archives Identifier: ' .. table.concat(naids, ', ') end return '' end local locals = reassembleChunkedValues(entity, P.LOCAL_IDENTIFIER, stringOf) if #locals > 0 then local instLabel for _, inst in ipairs(dplaStatements(entity, P.INSTITUTION)) do instLabel = renderEntitySnak(inst) if instLabel then break end end local joined = table.concat(locals, ', ') if instLabel and instLabel ~= '' then return '* ' .. instLabel .. ' identifier: ' .. joined end return '* Identifier: ' .. joined end return '' end -- ============================================================ -- Multipage navigation (P304 on P760) -- ============================================================ -- Return the file's P304 page number (integer) by reading the P760 -- statement's P304 qualifier. Returns nil when no P304 qualifier is -- present — i.e. the file is a singleton in its extension series and -- the "Other pages" field should be omitted. local function currentPageNumber(entity) if not entity or not entity.statements then return nil end for _, stmt in ipairs(entity.statements[P.DPLA_ID] or {}) do if isDplaDetermined(stmt) then for _, v in ipairs(qualifierValues(stmt, P.PAGE)) do if type(v) == 'string' then local n = tonumber(v) if n then return n end end end end end return nil end -- Build a sibling file's title by swapping the "(page N)" suffix in the -- current file's title. Returns the candidate title string (without -- the "File:" prefix), or nil if the current title doesn't carry a -- "(page N)" suffix at all (in which case we can't find siblings by -- title-pattern enumeration). local function siblingTitleFor(currentTitle, newPageNumber) if not currentTitle then return nil end -- Strip File: prefix if present. local base = currentTitle:gsub('^File:', '') -- Match "...(page N).ext" — preserve the bits before/after. local prefix, _, ext = base:match('^(.-)%(page (%d+)%)(%..+)$') if not prefix then return nil end return prefix .. '(page ' .. tostring(newPageNumber) .. ')' .. ext end -- Resolve a sibling File: title to a (page, exists?) pair. Returns the -- mw.title object if the page exists, nil otherwise. local function siblingExists(siblingBaseTitle) if not siblingBaseTitle then return nil end local title = mw.title.makeTitle('File', siblingBaseTitle) if title and title.exists then return title end return nil end --- p.other_pages — render the "Other pages" navigation cell. -- For multipage files (those with a P304 qualifier on P760), produce -- a wikitext cell showing thumbnails of the previous and next pages in -- the same extension series, each linking to the sibling file page. -- -- Sibling discovery uses the file-title naming convention applied by -- the DPLA uploader: ``... - DPLA - <dpla_id> (page N).<ext>``. The -- module substitutes (page N-1) and (page N+1) into the current -- file's title and checks `mw.title.makeTitle:exists` to confirm the -- sibling file exists. Each found sibling is rendered as a 100px -- thumbnail with an arrow caption (← / →). -- -- Returns the empty string when the file has no P304 qualifier or -- when the title doesn't follow the "(page N)" convention. function p.other_pages(frame) local args = (frame and frame.args) or {} local entity = getEntity(args) local pageNum = currentPageNumber(entity) if not pageNum then return '' end local titleObj if args.page and args.page ~= '' then titleObj = mw.title.new(args.page) else titleObj = mw.title.getCurrentTitle() end if not titleObj then return '' end local currentTitle = titleObj.fullText local cells = {} if pageNum > 1 then local prev = siblingExists(siblingTitleFor(currentTitle, pageNum - 1)) if prev then -- Both the thumbnail and the "Page N" label link to the -- sibling file page so either is a clickable navigation -- target. The thumbnail's alt text mirrors the visible -- "Page N" label so a screen reader user gets the same -- navigation context as a sighted user. local prevLabel = substituteParams( localizedLabel('page_n'), tostring(pageNum - 1) ) table.insert(cells, '<div style="display:inline-block;text-align:center;margin:0 8px;">' .. '[[File:' .. prev.text .. '|100px|alt=' .. prevLabel .. '|link=' .. prev.fullText .. ']]<br/>' .. '<span style="font-size:1.2em;" aria-hidden="true">&#x2190;</span> [[:' .. prev.fullText .. '|' .. prevLabel .. ']]' .. '</div>' ) end end local nextSibling = siblingExists(siblingTitleFor(currentTitle, pageNum + 1)) if nextSibling then local nextLabel = substituteParams( localizedLabel('page_n'), tostring(pageNum + 1) ) table.insert(cells, '<div style="display:inline-block;text-align:center;margin:0 8px;">' .. '[[File:' .. nextSibling.text .. '|100px|alt=' .. nextLabel .. '|link=' .. nextSibling.fullText .. ']]<br/>' .. '[[:' .. nextSibling.fullText .. '|' .. nextLabel .. ']]' .. ' <span style="font-size:1.2em;" aria-hidden="true">&#x2192;</span>' .. '</div>' ) end if #cells == 0 then -- Page number present but neither sibling found — either this -- is the only page (singleton edge case), the sibling files -- don't follow the "(page N)" title convention, or the -- adjacent ordinals failed to upload. Return empty so the -- template's ``{{#if:}}`` gate hides the whole "Other pages" -- field instead of rendering a lone "Page N" with no -- navigation affordance. return '' end return table.concat(cells, ' ') end --- p.categories — emit the full set of DPLA partner / institution -- categories for this file. Three categories, matching the legacy -- ``{{DPLA}}`` template's tracking output: -- -- * MAIN_CAT — always -- * Hub category — the regional aggregator (e.g. "Plains to Peaks -- Collective"). Identified by a non-DPLA P9126 statement carrying -- ``P3831 = Q393351`` (PUBLISHER_ROLE); ``partnerByRole`` filters -- out DPLA itself so the remaining publisher is the regional -- hub. For partners where the contributing institution is also -- its own hub (NARA, Smithsonian), the bot omits the redundant -- hub-PUBLISHER_ROLE entry, so we fall back to the institution -- Q-ID — same category on Commons, no spurious "unknown partner" -- classification. -- * Institution category — the contributing institution, read from -- ``P195`` (collection). Falls back to UNKNOWN_INSTITUTION_CAT -- when the P195 entity has no Commons sitelink. -- Render the three "Media contributed by..." category links from a -- pre-resolved (hubQid, instQid) pair. Either argument may be nil, -- in which case its slot falls back to the UNKNOWN_* maintenance -- category — same shape the legacy ``{{DPLA/hub cat}}`` / -- ``{{DPLA/inst cat}}`` templates emitted, preserved here so existing -- maintenance queries keep finding their files. local function buildCategoryList(hubQid, instQid) local parts = {'[[Category:' .. MAIN_CAT .. ']]'} local hubCat = commonsCategoryFor(hubQid) if hubCat then table.insert(parts, '[[Category:' .. hubCat .. ']]') else table.insert(parts, '[[Category:' .. UNKNOWN_PARTNER_CAT .. ']]') end local instCat = commonsCategoryFor(instQid) if instCat then table.insert(parts, '[[Category:' .. instCat .. ']]') else table.insert(parts, '[[Category:' .. UNKNOWN_INSTITUTION_CAT .. ']]') end return table.concat(parts, '\n') end -- Resolve (hubQid, instQid) for category emission from an SDC entity. -- The DPLA SDC writer encodes the partnership graph across two -- properties: -- * ``P195`` (collection) — usually the hub-level Q-ID, kept for -- legacy ``{{DPLA/hub cat}}`` compatibility. -- * ``P9126`` (object of statement has role) — three entries per -- file: DPLA itself (role = PUBLISHER_ROLE), the hub (role = -- AGGREGATOR_ROLE), and the granular contributing institution -- (role = CONTRIBUTING_ROLE). The contributing institution can -- be more specific than P195 — e.g. ``National Archives at -- College Park - Cartographic`` versus the parent ``National -- Archives and Records Administration`` in P195. -- Prefer the role-tagged P9126 values so the category emission lines -- up with the granular institution where one exists. Fall back to -- P195 when the role-tagged entries are absent (older bot writes, -- partner-equals-institution cases like NARA-direct uploads). local function resolveCategoryQidsFromEntity(entity) if not entity then return nil, nil end -- P195 (collection) always carries the contributing institution -- Q-ID — every code path that writes SDC writes one, and where -- no separate hub exists (direct uploads) the institution is also -- the hub. local p195Qid for _, stmt in ipairs(dplaStatements(entity, P.INSTITUTION)) do local ms = stmt.mainsnak if ms.snaktype == 'value' and ms.datavalue and ms.datavalue.type == 'wikibase-entityid' then p195Qid = ms.datavalue.value.id break end end -- The hub is the non-DPLA, non-institution Q-ID in the P9126 -- partnership chain. Role qualifiers (P3831) are deliberately -- ignored: ingest_wikimedia.sdc historically writes two -- different role conventions depending on a misnamed -- ``Q_SMITHSONIAN`` constant (which actually holds NARA's Q-ID). -- NARA files get AGGREGATOR_ROLE on the hub; every other partner -- gets PUBLISHER_ROLE on the hub and AGGREGATOR_ROLE on the -- institution. Set-based identification gets the correct hub -- Q-ID regardless of which convention is on the entity. local hubQid for _, stmt in ipairs(dplaStatements(entity, P.PARTNERSHIP)) do local ms = stmt.mainsnak if ms.snaktype == 'value' and ms.datavalue and ms.datavalue.type == 'wikibase-entityid' then local qid = ms.datavalue.value.id if qid ~= Q.DPLA and qid ~= p195Qid then hubQid = qid break end end end -- Fall back to institution-is-its-own-hub when no separate hub -- Q-ID is present (BPL-direct, NARA-direct files where DPLA -- flattened the partnership). buildCategoryList renders a single -- category in that case via MediaWiki's natural categorylinks -- dedup. return hubQid or p195Qid, p195Qid end function p.categories(frame) local args = (frame and frame.args) or {} local entity = getEntity(args) if not entity then return '' end local hubQid, instQid = resolveCategoryQidsFromEntity(entity) return buildCategoryList(hubQid, instQid) end -- ============================================================ -- Custom metadata-table rendering (replaces {{Information}}) -- ============================================================ -- Edit-pencil affordances are intentionally NOT emitted. DPLA SDC is -- authoritative and synced from the source catalog by the bot; we don't -- want to invite ad-hoc edits to the rendered fields. The propertyId -- argument to ``tableRow`` is preserved for forward compatibility but -- currently unused. -- HTML id="..." attributes that Commons' machine-readable scanners -- (including AntiCompositeBot's NoLicense tagger and external -- file-metadata consumers) look for to extract structured per-file -- fields. Each canonical row in the rendered table carries the -- matching id so a file rendered through Module:DPLA stays out of -- the ``Files with no machine-readable <field>`` maintenance -- categories. Keys here mirror the label keys passed to ``add()`` -- in ``buildDplaRows`` / ``buildUserRows``. See -- https://commons.wikimedia.org/wiki/Commons:Machine-readable_data -- for the full list of recognised ids and the bot scanners that -- key on them. local MACHINE_READABLE_ID = { description = 'fileinfotpl_desc', creator = 'fileinfotpl_aut', date = 'fileinfotpl_date', source = 'fileinfotpl_src', permission = 'fileinfotpl_perm', } local function tableRow(label, value, _propertyId, fieldIdAttr) if value == nil or value == '' then return nil end -- The fileinfotpl_* machine-readable id must sit on the LABEL cell, not -- the value cell. Commons' CommonsMetadata extension (which populates the -- ``Files with no machine-readable <field>`` categories and the -- ``extmetadata`` API) locates the element by id and then reads its NEXT -- SIBLING cell as the field value — mirroring {{Information}}, whose id -- sits on the "Author"/"Source"/… label cell. With the id on the value -- cell its sibling is empty, so every field extracted as nothing and the -- file landed in the maintenance categories regardless of content. local idAttr = fieldIdAttr and (' id="' .. fieldIdAttr .. '"') or '' return '<tr style="vertical-align: top">' .. '<td class="fileinfo-paramfield"' .. idAttr .. '>' .. label .. '</td>' .. '<td>' .. value .. '</td>' .. '</tr>' end -- Top banner row mirroring {{Artwork}}'s title header: the file's title -- spans the full table width, centered, in larger and bolder type. -- Returns nil when no title can be derived so the dispatcher can omit -- the banner row. local function titleBanner(title) if not title or title == '' then return nil end return '<tr><th colspan="2" style="text-align:center; font-size:1.25em; padding:0.6em; ' .. 'background-color:var(--background-color-interactive, #e0e0ee); color:inherit;">' .. title .. '</th></tr>' end -- Each field-text helper accepts an optional ``statementsFn`` that -- selects which statements to render (``dplaStatements`` by default, -- ``nonDplaStatements`` for the yellow user-contributed box). The -- ``filterFn`` parallel argument is used inside chunked-value -- reassembly, which iterates the raw statement list itself. local function titlesText(entity, statementsFn, filterFn) filterFn = filterFn or isDplaDetermined return table.concat(reassembleChunkedValues(entity, P.TITLE, monolingualtextOf, filterFn), ', ') end local function descriptionText(entity, statementsFn, filterFn) filterFn = filterFn or isDplaDetermined -- Multi-value descriptions get a blank-line break between values, -- not commas — descriptions are often paragraph-length, and a -- comma between two paragraphs reads as one continuous wall of -- text. ``reassembleChunkedValues`` already concatenates chunked -- single-values (statements sharing a P1545 ordinal letter -- prefix, used to bypass the per-statement string-length limit) -- into one entry, so the separator only fires between truly -- distinct descriptions. return table.concat( reassembleChunkedValues(entity, P.DESCRIPTION, monolingualtextOf, filterFn), '<br /><br />' ) end local function creatorText(entity, statementsFn) statementsFn = statementsFn or dplaStatements if not entity then return '' end local out = {} for _, stmt in ipairs(statementsFn(entity, P.CREATOR)) do local ms = stmt.mainsnak if ms.snaktype == 'somevalue' then local name = firstQualifierValue(stmt, P.AUTHOR_NAME_STRING) if type(name) == 'string' then table.insert(out, name) end elseif ms.snaktype == 'value' and ms.datavalue then if ms.datavalue.type == 'string' then table.insert(out, ms.datavalue.value) elseif ms.datavalue.type == 'wikibase-entityid' then local id = ms.datavalue.value.id table.insert(out, mw.wikibase.getLabel(id) or id) end end end return table.concat(out, ', ') end -- Format an ISO-prefix date string ("YYYY-MM-DD" or "YYYY-MM") to a -- human-readable form via the content language's date formatter (e.g. -- "1967-03-28" → "28 March 1967", matching the {{Artwork}} renderer's -- output). Falls back to the raw ISO string on any formatter error so a -- BC date or other unsupported input doesn't blow up the whole row. local function formatWikibaseTime(timeStr, precision) if not timeStr or timeStr == '' then return '' end local lang = mw.language.getContentLanguage() local fmt, input if precision >= 11 then fmt = 'j F Y' input = timeStr:sub(1, 10) elseif precision == 10 then -- formatDate parses "YYYY-MM-01" reliably; fabricating the day -- doesn't affect the "F Y" output (month + year only). fmt = 'F Y' input = timeStr:sub(1, 7) .. '-01' elseif precision == 9 then return timeStr:sub(1, 4) elseif precision == 8 then return timeStr:sub(1, 3) .. '0s' else return timeStr:sub(1, 4) end -- Reject BC / year-zero values; ``formatDate`` accepts them but the -- output for negative years is platform-dependent. Show raw instead. if input:sub(1, 1) == '-' or input:sub(1, 4) == '0000' then return timeStr:sub(1, #input) end local ok, result = pcall(function() return lang:formatDate(fmt, input) end) if ok and result and result ~= '' then return result end return timeStr:sub(1, #input) end -- Parse a year-range string into (start_year, end_year) with -- start <= end, or return nil for non-range / unparseable inputs. -- Mirror of ingest_wikimedia.sdc.parse_date_range; the Python side -- uses this same canonicalisation when deciding whether a legacy -- ``{{Artwork}}`` date claim is a duplicate of DPLA's own range -- claim, and the SDC reconciler uses it to remove inferred-from- -- Wikitext claims that are now equivalent to a DPLA-sourced one. -- Module:DPLA mirrors the canonicalisation at render time so the -- yellow box stays clean between sync runs. -- -- Recognised shapes (post wikitext-expansion or raw): -- * YYYY [- / – —] YYYY -- * between YYYY and YYYY (case-insensitive) -- * {{other date|between|YYYY|YYYY}} (raw wikitext fallback) -- -- Lua patterns have no alternation or {n,m} quantifier — captures -- pull "one or more digits" and a helper validates the year is -- plausible (100..9999) so e.g. "5-100" can't pretend to be a year -- range. Year 0 is rejected for the same reason parse_dpla_date -- rejects it: proleptic Gregorian has no year 0. local function _yearRange(a, b) local ya, yb = tonumber(a), tonumber(b) if not ya or not yb then return nil end if ya < 100 or ya > 9999 then return nil end if yb < 100 or yb > 9999 then return nil end if ya > yb then ya, yb = yb, ya end return ya, yb end local function parseDateRange(s) if type(s) ~= 'string' or s == '' then return nil end local trimmed = s:gsub('^%s+', ''):gsub('%s+$', '') if trimmed == '' then return nil end local lower = trimmed:lower() -- "between X and Y" (lowercased input → ASCII pattern) local a, b = lower:match('^between%s+(%d+)%s+and%s+(%d+)$') if a then local ya, yb = _yearRange(a, b) if ya then return ya, yb end end -- "{{other date|between|X|Y}}" — tolerates ``other_date`` alias -- and optional whitespace around the pipes. a, b = lower:match('^{{%s*other[ _]date%s*|%s*between%s*|%s*(%d+)%s*|%s*(%d+)%s*}}$') if a then local ya, yb = _yearRange(a, b) if ya then return ya, yb end end -- "YYYY [sep] YYYY" — Lua patterns can't alternate over -- separators, so try each one. ``-`` and ``/`` are ASCII; ``–`` -- (en-dash) and ``—`` (em-dash) are multi-byte UTF-8 sequences -- that Lua's byte-oriented patterns still match literally. for _, sep in ipairs({'%-', '/', '–', '—'}) do a, b = trimmed:match('^(%d+)%s*' .. sep .. '%s*(%d+)$') if a then local ya, yb = _yearRange(a, b) if ya then return ya, yb end end end return nil end -- Produce a comparable key for a P571 statement that compares equal -- across equivalent encodings (value-typed time vs. somevalue+P1932 -- single date, range-shaped P1932 across format variants, raw P1932 -- prose strings). Mirror of ``_statement_comparable_value`` in -- tools/sdc_sync.py; used to suppress non-DPLA P571 statements whose -- value duplicates a DPLA-attributed one in the yellow box. -- -- Returns nil for shapes we can't compare — caller treats nil as -- "render normally", not as a match. local function dateStatementComparable(stmt) local ms = stmt and stmt.mainsnak if not ms then return nil end if ms.snaktype == 'value' and ms.datavalue and ms.datavalue.type == 'time' then local v = ms.datavalue.value or {} local circa = '' for _, q in ipairs((stmt.qualifiers or {}).P1480 or {}) do if q.snaktype == 'value' and q.datavalue and q.datavalue.type == 'wikibase-entityid' and q.datavalue.value and q.datavalue.value.id == Q.CIRCA then circa = '|circa' break end end return 'time|' .. (v.time or '') .. '|P' .. tostring(v.precision or '') .. circa end if ms.snaktype == 'somevalue' then for _, q in ipairs((stmt.qualifiers or {}).P1932 or {}) do if q.snaktype == 'value' and q.datavalue and q.datavalue.type == 'string' then local s = (q.datavalue.value or ''):gsub('^%s+', ''):gsub('%s+$', '') if s == '' then return nil end local ya, yb = parseDateRange(s) if ya then return 'range|' .. ya .. '|' .. yb end return 'p1932|' .. s end end end return nil end local function dateText(entity, statementsFn, frame) statementsFn = statementsFn or dplaStatements if not entity then return '' end -- When rendering the yellow (non-DPLA) box, build a set of -- DPLA-attributed comparables so non-DPLA statements that encode -- the same fact (e.g. an inferred-from-Wikitext range claim left -- behind by an older legacy-migration run) are suppressed at -- render time. The Python reconciler removes these on the next -- sync — see ``_reconcile_inferred_from_wikitext_dupes`` in -- tools/sdc_sync.py — but the module needs to dedup between -- syncs so the page reads cleanly today. local dplaComparables if statementsFn ~= dplaStatements then dplaComparables = {} for _, dplaStmt in ipairs(dplaStatements(entity, P.DATE)) do local key = dateStatementComparable(dplaStmt) if key then dplaComparables[key] = true end end end local out = {} for _, stmt in ipairs(statementsFn(entity, P.DATE)) do local skip = false if dplaComparables then local key = dateStatementComparable(stmt) if key and dplaComparables[key] then skip = true end end if not skip then local ms = stmt.mainsnak local isCirca = false for _, sc in ipairs(qualifierValues(stmt, P.SOURCING_CIRCUMSTANCES)) do if type(sc) == 'table' and sc.id == Q.CIRCA then isCirca = true break end end if ms.snaktype == 'value' and ms.datavalue and ms.datavalue.type == 'time' then local v = ms.datavalue.value local timeStr = (v.time or ''):gsub('^%+', '') local rendered = formatWikibaseTime(timeStr, v.precision or 11) if isCirca then rendered = localizedLabel('circa') .. ' ' .. rendered end table.insert(out, rendered) elseif ms.snaktype == 'somevalue' then -- ``P1932`` (stated as) preserves the editor's original source -- string verbatim. Legacy {{Artwork}} migration imports it -- raw, so the value can legitimately contain wikitext markup -- (e.g. ``{{other date|~|1911}}``). MediaWiki does NOT re- -- expand templates inside Scribunto output, so emit through -- ``frame:preprocess`` to render any embedded template / -- parser-function. Plain-text passes through unchanged. Same -- pattern Module:Information / Module:Artwork use for their -- editor-contributed text fields. local stated = firstQualifierValue(stmt, P.STATED_AS) local text if type(stated) == 'string' then text = stated elseif type(stated) == 'table' and stated.text then text = stated.text end if text then if frame then text = frame:preprocess(text) end table.insert(out, text) end end end -- close ``if not skip then`` end return table.concat(out, ', ') end local function institutionQidOf(entity, statementsFn) statementsFn = statementsFn or dplaStatements if not entity then return nil end for _, stmt in ipairs(statementsFn(entity, P.INSTITUTION)) do local ms = stmt.mainsnak if ms.snaktype == 'value' and ms.datavalue and ms.datavalue.type == 'wikibase-entityid' then return ms.datavalue.value.id end end return nil end local function subjectsText(entity, statementsFn, filterFn) statementsFn = statementsFn or dplaStatements filterFn = filterFn or isDplaDetermined if not entity then return '' end local seen = {} local out = {} for _, stmt in ipairs(statementsFn(entity, P.SUBJECT_ENTITY)) do local label = renderEntitySnak(stmt) if label and not seen[label] then seen[label] = true table.insert(out, label) end end for _, s in ipairs(reassembleChunkedValues(entity, P.SUBJECT_STRING, stringOf, filterFn)) do if not seen[s] then seen[s] = true table.insert(out, s) end end return table.concat(out, ', ') end -- DPLA ID rendered as an interwiki link to the DPLA item page. -- The ``dpla:`` interwiki prefix on Commons resolves to -- https://dp.la/item/<id>, so ``[[dpla:<id>|<id>]]`` produces an -- internal-looking link with the bare ID as visible text. local function dplaIdText(entity) local values = reassembleChunkedValues(entity, P.DPLA_ID, stringOf) local id = values[1] if not id or id == '' then return '' end return '[[dpla:' .. id .. '|' .. id .. ']]' end -- Build the Source-row icon cells from explicit URL arguments. Extracted -- from ``sourceFieldText`` so the yellow box can call the same renderer -- after parsing the user-side ``{{DPLA|...}}`` (legacy) or flat-param -- (new uploads) shape — sharing one cell builder keeps the two -- representations indistinguishable in the rendered HTML. -- -- ``naraInterwikiLink`` is the optional pre-built ``[[w:..]]`` wikilink -- the NARA-specific catalog cell uses; pass nil for any other partner -- and the cell becomes a plain external link to ``catalogUrl``. local function sourceFieldFromUrls(fileUrl, iiifUrl, catalogUrl, naraInterwikiLink) local cells = {} if fileUrl and fileUrl ~= '' then table.insert(cells, '[[File:Codex icon download color-progressive.svg|24px|alt=|class=skin-invert|link=]]&nbsp;[' .. fileUrl .. ' ' .. localizedLabel('file_url') .. ']' ) end if iiifUrl and iiifUrl ~= '' then table.insert(cells, '[[File:International Image Interoperability Framework logo.png|24px|alt=|link=]]&nbsp;[' .. iiifUrl .. ' ' .. localizedLabel('iiif_manifest') .. ']' ) end if catalogUrl and catalogUrl ~= '' then if naraInterwikiLink then table.insert(cells, '[[File:Breezeicons-actions-22-database-index.svg|24px|alt=|class=skin-invert|link=]]&nbsp;' .. naraInterwikiLink ) else table.insert(cells, '[[File:Breezeicons-actions-22-database-index.svg|24px|alt=|class=skin-invert|link=]]&nbsp;[' .. catalogUrl .. ' ' .. localizedLabel('catalog_record') .. ']' ) end end local parts = {} for i, cell in ipairs(cells) do local style = 'display:inline-block; padding: 4px 16px; vertical-align: middle;' if i > 1 then style = style .. ' border-left: 2px solid #a2a9b1;' end table.insert(parts, '<span style="' .. style .. '">' .. cell .. '</span>') end return table.concat(parts) end local function sourceFieldText(entity) local fileUrl, iiifUrl, catalogUrl = sourceUrls(entity) return sourceFieldFromUrls( fileUrl, iiifUrl, catalogUrl, naraCatalogInterwiki(entity) ) end -- Partnership card rendered as pure HTML (no wikitable). The legacy -- ``{{DPLA/layout}}`` template uses wikitable syntax (``{| ... |}``) -- which MediaWiki only parses at certain top-level positions; embedding -- it inside a surrounding <td> via frame:expandTemplate left the -- ``{|`` literal in the rendered output. Building the 3-column layout -- as a single <div> with flex avoids the parse issue. -- -- ``partnershipFromQids`` is the parametric core, callable from either -- the SDC entity path (blue box) or the wikitext-param path (yellow -- box for new uploads or legacy-shape pages whose source param the -- module parses below). Same HTML, same accessibility behaviour: -- the alt text on both logos is empty because the institution name is -- already inside the adjacent text, and the DPLA name is in the banner -- above the row. local function partnershipFromQids(instQid, hubQid) if (not instQid or instQid == '') and (not hubQid or hubQid == '') then return '' end local instLabel = (instQid and instQid ~= '') and (mw.wikibase.getLabel(instQid) or instQid) or 'an institution' local hubLabel = (hubQid and hubQid ~= '') and (mw.wikibase.getLabel(hubQid) or hubQid) or '' local instMarkup = (instQid and instQid ~= '') and ('[[d:' .. instQid .. '|' .. instLabel .. ']]') or instLabel local text if hubLabel ~= '' then local hubMarkup = '[[d:' .. hubQid .. '|' .. hubLabel .. ']]' text = substituteParams( localizedLabel('partnership_text_with_hub'), instMarkup, hubMarkup ) else text = substituteParams( localizedLabel('partnership_text_without_hub'), instMarkup ) end local logo = ((instQid and instQid ~= '') and imageFileFor(instQid, P.LOGO_IMAGE)) or ((instQid and instQid ~= '') and imageFileFor(instQid, P.IMAGE)) or DEFAULT_PARTNER_LOGO return '<div style="display:flex; align-items:center; gap:15px; padding:0.4em;">' .. '<div>[[' .. logo .. '|70px|alt=]]</div>' .. '<div style="flex:1;">' .. text .. '</div>' .. '<div>[[File:DPLA square logo.svg|70px|alt=]]</div>' .. '</div>' end local function partnershipMarkup(entity, _frame) if not entity then return '' end return partnershipFromQids( institutionQidOf(entity), partnerByRole(entity, Q.PUBLISHER_ROLE) ) end -- P6426 Q-IDs that we have to emit a Commons template for directly, -- because Module:License (the engine behind ``{{License from structured -- data}}``) misreads our DPLA-attributed statements — it picks up the -- determination-method qualifier value (Q61848113) instead of the -- mainsnak Q-ID and produces a 'missing main template (P1424)' error -- in the permission row. Each template takes one positional arg = the -- contributing institution Q-ID, matching the legacy ``permission = -- {{NoC-US|Q...}}`` / ``{{NKC|Q...}}`` shape uploaded files use. Other -- eligible NoC-* values (NoC-CR, NoC-NC, NoC-OKLR) have no Commons -- template yet and are also not used by any DPLA file today; add them -- here if either condition changes. local RIGHTS_TEMPLATE_BY_P6426 = { ['Q47530911'] = 'NoC-US', -- No Copyright - United States ['Q47530955'] = 'NKC', -- No Known Copyright } local function permissionMarkup(entity, frame) if not entity then return '' end -- DPLA writes the rights cluster across two properties depending on -- the source URI: ``P6426`` (copyright status as a creator) for -- public-domain / no-known-copyright items, ``P275`` (copyright -- license) for Creative Commons items. Detect either — the row -- should render whenever DPLA has contributed any rights claim. local p6426 for _, stmt in ipairs(dplaStatements(entity, 'P6426')) do local ms = stmt.mainsnak if ms.snaktype == 'value' and ms.datavalue and ms.datavalue.type == 'wikibase-entityid' then p6426 = ms.datavalue.value.id break end end local hasP275 = false for _, stmt in ipairs(dplaStatements(entity, 'P275')) do local ms = stmt.mainsnak if ms.snaktype == 'value' and ms.datavalue and ms.datavalue.type == 'wikibase-entityid' then hasP275 = true break end end -- Suppress the row entirely when DPLA hasn't contributed any rights -- claim. Without this guard the row would fall through to -- {{License from structured data}} and render whatever license is on -- the file regardless of DPLA provenance — defeating the -- "DPLA-attributed fields only" contract of this template. if not p6426 and not hasP275 then return '' end local templateName = RIGHTS_TEMPLATE_BY_P6426[p6426] if templateName then -- Each template requires the institution Q-ID as its first -- positional arg; without one it renders the raw ``{{{1}}}`` -- placeholder. Fall back to DPLA's own Q-ID when the file has -- no DPLA-authored P195 — semantically accurate (DPLA bot is -- the entity making the rights determination; the contributing -- institution is just the original source). local instQid = institutionQidOf(entity) if not instQid or instQid == '' then instQid = Q.DPLA end return frame:expandTemplate{title = templateName, args = {[1] = instQid}} end -- ``{{License from structured data}}`` reads the SDC itself and -- dispatches to the correct license template (Cc-by-4.0, PD-USGov, -- etc.) based on whichever of P275/P6426/P6216 is set. return frame:expandTemplate{title = 'License from structured data'} end -- Template-parameter alias map. The yellow user-contributed box accepts -- either {{Information}} param names (description, author, source, -- permission) or {{Artwork}} param names (description, artist, -- institution, source, permission); ``firstParam`` resolves the first -- non-empty match from a list of candidate names. local function firstParam(args, ...) if not args then return nil end for _, name in ipairs({...}) do local v = args[name] if v and v ~= '' then return v end end return nil end -- Return true when ``wikitext_value`` is the same value the DPLA-attributed -- SDC for ``prop`` already carries — i.e. when the wikitext is a verbatim -- duplicate of authoritative DPLA data, not a user contribution. The -- comparison depends on ``kind``: -- -- * 'monolingualtext' — compare to ``mainsnak.datavalue.value.text`` on -- each DPLA-determined statement (P1476 title, P10358 description). -- * 'string' — compare to ``mainsnak.datavalue.value`` (P2093 stated-as -- creator, P217 local identifier). -- * 'time' — compare to the P1932 (stated as) qualifier on each -- DPLA-determined statement. The mainsnak's structured time value -- would have to be re-formatted to the original string for a direct -- comparison; the P1932 qualifier preserves the original DPLA-supplied -- display string verbatim (see ingest_wikimedia.sdc._build_date_claim), -- so comparing to it is exact. -- * 'wikibase-item' — wikitext value is expected to be a Q-ID string; -- compare to ``mainsnak.datavalue.value.id`` on each DPLA-determined -- statement (P195 institution, P9126 hub). -- -- All comparisons trim surrounding whitespace to match the renderer's -- behaviour. Returns false on missing entity / unknown kind so an -- unrecognised property kind is "play it safe and render". local function valueMatchesDplaSdc(entity, prop, wikitext_value, kind) if not entity or not prop or not wikitext_value then return false end local stripped = wikitext_value:gsub('^%s+', ''):gsub('%s+$', '') if stripped == '' then return false end for _, stmt in ipairs(dplaStatements(entity, prop)) do local ms = stmt.mainsnak if kind == 'monolingualtext' then if ms.snaktype == 'value' and ms.datavalue and ms.datavalue.type == 'monolingualtext' and ms.datavalue.value and ms.datavalue.value.text == stripped then return true end elseif kind == 'string' then -- Top-level string mainsnak (e.g. P217 local identifier). -- For the creator field, the canonical SDC shape is a -- P170 ``somevalue`` mainsnak with a P2093 qualifier, not -- a top-level P2093 statement — see ``creator-name`` -- below; the ``string`` kind here only matches a direct -- string mainsnak on the queried property. if ms.snaktype == 'value' and ms.datavalue and ms.datavalue.type == 'string' and ms.datavalue.value == stripped then return true end elseif kind == 'creator-name' then -- DPLA's creator SDC: P170 mainsnak is typically -- ``somevalue`` (we don't have a Wikidata item for the -- creator), with the user-visible name carried on the -- P2093 (stated as) qualifier (see sdc.py's -- _build_creator_claim). Compare the wikitext creator -- string to each DPLA-attributed P170 statement's P2093 -- qualifier; a direct P2093 statement is rare but is -- caught by the ``string`` kind path when callers pass -- prop='P2093' instead. for _, q in ipairs((stmt.qualifiers or {}).P2093 or {}) do if q.snaktype == 'value' and q.datavalue and q.datavalue.value == stripped then return true end end elseif kind == 'time' then -- The original display string lives on the P1932 qualifier; -- the time mainsnak may be at year precision while the -- editor wrote a full ISO date, so the mainsnak isn't a -- reliable comparand. -- -- Two-stage compare: direct byte equality first (covers -- "1934 - 1948" vs. "1934 - 1948"), then range canonical -- equivalence so ``{{other date|between|1934|1948}}`` / -- "between 1934 and 1948" / "1934-1948" / "1934/1948" all -- dedup against each other. The range fallback only runs -- when both sides parse as canonical ranges, so single -- dates and free prose are unaffected. local strippedRangeA, strippedRangeB = parseDateRange(stripped) for _, q in ipairs((stmt.qualifiers or {}).P1932 or {}) do if q.snaktype == 'value' and q.datavalue then if q.datavalue.value == stripped then return true end if strippedRangeA then local a, b = parseDateRange(q.datavalue.value or '') if a == strippedRangeA and b == strippedRangeB then return true end end end end elseif kind == 'wikibase-item' then if ms.snaktype == 'value' and ms.datavalue and ms.datavalue.type == 'wikibase-entityid' and ms.datavalue.value and ms.datavalue.value.id == stripped then return true end end end return false end -- Special-case institution lookup that walks P195 *and* P9126 -- (partnership) statements for a matching Q-ID. The DPLA SDC writer -- emits the hub-level institution on P195 (e.g. Q518155 = NARA for -- every NARA-contributed file) and the more specific data-provider / -- contributing institution on P9126 with a role qualifier (e.g. -- Q59661040 = "National Archives at College Park - Still Pictures"). -- The wikitext ``institution =`` param holds the *data provider* Q-ID -- — which matches the P9126 statement, not P195 — so checking only -- P195 would miss the equality and leak a redundant Institution row -- into the yellow box. Walking both properties catches either form. local function institutionQidMatchesDplaSdc(entity, wikitext_value) if valueMatchesDplaSdc(entity, P.INSTITUTION, wikitext_value, 'wikibase-item') then return true end return valueMatchesDplaSdc(entity, P.PARTNERSHIP, wikitext_value, 'wikibase-item') end -- Pick the user-contributed value for a field: explicit template -- parameter wins, otherwise fall back to the non-DPLA SDC renderer. -- -- When ``dplaProp`` (and ``dplaKind``) is supplied, the wikitext value -- is first checked against the DPLA-attributed SDC for that property -- — if it matches, the value is treated as not user-contributed and -- the SDC fallback runs instead. This catches the post-SDC pre-strip -- case where the wikitext still carries a value DPLA wrote at upload -- time and SDC sync has already mirrored. Module:DPLA's yellow box -- is for user contributions only; a wikitext value that's identical -- to authoritative DPLA SDC is by definition not a contribution. -- -- Without ``dplaProp``, behavior is unchanged from the original -- ``userValue`` (no redundancy check), which is the right default -- for fields whose redundancy can't be reliably detected (e.g. -- ``subject``, where the SDC representation is a mix of P921 items -- and P4272 strings the renderer would have to reassemble). local function userValue(args, paramNames, sdcFallback, entity, dplaProp, dplaKind) for _, name in ipairs(paramNames) do local v = args and args[name] if v and v ~= '' then if dplaProp and valueMatchesDplaSdc(entity, dplaProp, v, dplaKind) then break -- drop into SDC fallback end return v end end return sdcFallback() or '' end -- Legacy creator-shape extractor for files where the creator value -- lives inside ``Other fields N = {{InFi | Creator | <value> | -- id=fileinfotpl_aut}}`` (the pre-#291 legacy template carried it -- this way; some files still have it post-{{Artwork}}→{{DPLA metadata}} -- migration because the strip pass doesn't know how to normalise -- ``Other fields N`` keys yet). Without this fallback the renderer -- never sees a creator value for these files, the creator row is -- omitted, and the ``fileinfotpl_aut`` machine-readable id never -- makes it into the rendered HTML — putting the file in -- ``Files with no machine-readable author``. -- -- Reads the *raw* page wikitext via ``mw.title:getContent()`` because -- by the time Lua sees ``args``, Scribunto has already pre-expanded -- the ``{{InFi|...}}`` sub-template into its rendered HTML — there's -- no longer a literal ``{{InFi|Creator|...}}`` string to match. The -- raw-content path is the same one ``parseLegacyFromPageContent`` -- uses for the legacy ``source = {{DPLA|...}}`` block; ``getContent`` -- is cached within a single parse so the additional call is -- effectively free when both legacy shapes coexist on the page. -- Wrap a list of <tr> rows in the same outer table/div structure the -- legacy renderer used so Commons CSS still styles the labels. local function wrapInTable(rows) if #rows == 0 then return nil end return '<div class="hproduct commons-file-information-table">\n' .. '<table class="fileinfotpl-type-information vevent" dir="ltr">\n' .. table.concat(rows, '\n') .. '\n</table></div>' end -- Banner strings now live in [[Module:DPLA/i18n]] keyed as 'banner_dpla' -- and 'banner_user'. Resolved at render time via ``localizedLabel`` so the -- viewer sees them in their preferred language (falling back to English). -- Build the DPLA (blue) table rows from DPLA-determined SDC only. local function buildDplaRows(entity, frame) local rows = {} -- ``add`` takes a label *key* (looked up in i18n via -- ``localizedLabel``) rather than the already-resolved label -- string, so the same key can also drive the optional -- ``fileinfotpl_*`` machine-readable id on the value cell. Keys -- with no entry in ``MACHINE_READABLE_ID`` render as plain rows. local function add(labelKey, value) local row = tableRow( localizedLabel(labelKey), value, nil, MACHINE_READABLE_ID[labelKey] ) if row then table.insert(rows, row) end end local title = titlesText(entity) local banner = titleBanner(title) if banner then table.insert(rows, banner) end add('title', title) add('creator', creatorText(entity)) add('description', descriptionText(entity)) add('date', dateText(entity, nil, frame)) local instQid = institutionQidOf(entity) if instQid then add('institution', frame:expandTemplate{ title = 'Institution', args = {wikidata = instQid}, }) end add('subject', subjectsText(entity)) add('source', sourceFieldText(entity)) add('dpla_id', dplaIdText(entity)) add('other_pages', p.other_pages(frame)) add('partnership', partnershipMarkup(entity, frame)) add('permission', permissionMarkup(entity, frame)) return rows end -- Parse a legacy ``source = {{DPLA|<inst>|hub=...|url=...|dpla_id=...|local_id=...}}`` -- invocation, given the raw (unexpanded) wikitext value of the source -- param. Returns a flat dict keyed by hub / inst_qid / url / dpla_id / -- local_id, or nil when the value doesn't match the legacy shape. -- Tolerant of inner whitespace and newlines (the upload template_string -- emits a multi-line form). local function parseLegacySourceParam(s) if not s or s == '' then return nil end local body = s:match('{{%s*[Dd][Pp][Ll][Aa]%s*|(.-)}}%s*$') if not body then return nil end local out = {} for piece in (body .. '|'):gmatch('(.-)|') do piece = piece:match('^%s*(.-)%s*$') or '' if piece ~= '' then local eq = piece:find('=', 1, true) if eq then local k = piece:sub(1, eq - 1):match('^%s*(.-)%s*$'):lower() local v = piece:sub(eq + 1):match('^%s*(.-)%s*$') if k == 'hub' then out.hub = v end if k == 'url' then out.url = v end if k == 'dpla_id' then out.dpla_id = v end if k == 'local_id' then out.local_id = v end elseif not out.inst_qid then out.inst_qid = piece end end end return out end -- Locate the body of a top-level ``{{<name>|...}}`` template invocation -- in ``content``, returning the wikitext between the opening ``{{`` and -- the matching closing ``}}`` (not including the braces themselves). -- Handles nesting — an inner ``{{...}}`` within the template's args -- counts up the depth so the matching close belongs to the outer -- envelope, not to the inner sub-template's close. -- -- Lua patterns don't support balanced matching, so this is a manual -- scan. Linear in content length; fine for the ~kilobyte file pages -- this runs on at render time. local function extractTemplateBody(content, name) local i = content:find('{{%s*' .. name, 1) if not i then return nil end -- Skip past the opening ``{{`` and the template name itself. local _, openEnd = content:find('{{%s*' .. name, i) local j = openEnd + 1 local depth = 1 while j <= #content do local two = content:sub(j, j + 1) if two == '{{' then depth = depth + 1 j = j + 2 elseif two == '}}' then depth = depth - 1 if depth == 0 then return content:sub(openEnd + 1, j - 1) end j = j + 2 else j = j + 1 end end return nil end -- Walk the body of a ``{{DPLA metadata|...}}`` invocation and return -- the value of each top-level (depth-0) named argument as a flat -- dict. Nested ``{{...}}`` and ``[[...]]`` get carried inside the -- containing arg's value; only ``|`` characters at depth 0 separate -- top-level args. local function splitTopLevelArgs(body) local args = {} local depth = 0 local linkDepth = 0 local current = {} local function flush() local piece = table.concat(current) current = {} local eq = piece:find('=', 1, true) if eq then local k = piece:sub(1, eq - 1):match('^%s*(.-)%s*$'):lower() local v = piece:sub(eq + 1):match('^%s*(.-)%s*$') args[k] = v end end local i = 1 while i <= #body do local two = body:sub(i, i + 1) if two == '{{' then depth = depth + 1 table.insert(current, two) i = i + 2 elseif two == '}}' then depth = depth - 1 table.insert(current, two) i = i + 2 elseif two == '[[' then linkDepth = linkDepth + 1 table.insert(current, two) i = i + 2 elseif two == ']]' then linkDepth = linkDepth - 1 table.insert(current, two) i = i + 2 elseif body:sub(i, i) == '|' and depth == 0 and linkDepth == 0 then flush() i = i + 1 else table.insert(current, body:sub(i, i)) i = i + 1 end end flush() return args end -- Read the raw (unexpanded) wikitext of the current file page and -- extract the legacy ``source = {{DPLA|...}}`` and ``Institution = -- {{Institution|wikidata=Q...}}`` values, parsing each into the flat -- dict the parametric renderers consume. -- -- This indirection exists because Scribunto's ``frame:getParent().args`` -- returns pre-expanded values: by the time the Lua module sees -- ``args.source``, the inner ``{{DPLA|...}}`` has been expanded to its -- rendered wikitext (which contains ``{|...|}`` table syntax that -- leaks raw inside the surrounding HTML <td>). Fetching the raw -- wikitext via mw.title gives us back the literal sub-template -- invocation, which we can parse confidently. -- -- Returns an empty dict when getContent fails (the current title isn't -- a file page, or the page doesn't exist yet during preview); the -- caller's behaviour then degrades gracefully to "no parsed legacy -- params" — same effect as a flat-shape file with no source fields. local function parseLegacyFromPageContent() local title = mw.title.getCurrentTitle() if not title then return {} end local content = title:getContent() if not content then return {} end local envelope = extractTemplateBody(content, 'DPLA metadata') if not envelope then return {} end local topArgs = splitTopLevelArgs(envelope) local out = {} local sourceVal = topArgs['source'] if sourceVal then local parsed = parseLegacySourceParam(sourceVal) if parsed then out.hub = parsed.hub out.inst_qid = parsed.inst_qid out.url = parsed.url out.dpla_id = parsed.dpla_id out.local_id = parsed.local_id end end -- The Institution param is conventionally written with a capital -- ``I`` in the legacy form (``Institution = {{Institution|...}}``); -- splitTopLevelArgs lowercases the key. local instVal = topArgs['institution'] if instVal and not out.inst_qid then out.inst_qid = instVal:match( '{{%s*[Ii]nstitution%s*|%s*[Ww]ikidata%s*=%s*([^|}%s]+)' ) end return out end -- Legacy creator-shape extractor for files where the creator value -- lives inside ``Other fields N = {{InFi | Creator | <value> | -- id=fileinfotpl_aut}}`` (the pre-#291 legacy template carried it -- this way; some files still have it post-{{Artwork}}→{{DPLA metadata}} -- migration because the strip pass doesn't know how to normalise -- ``Other fields N`` keys yet). Without this fallback the renderer -- never sees a creator value for these files, the creator row is -- omitted, and the ``fileinfotpl_aut`` machine-readable id never -- makes it into the rendered HTML — putting the file in -- ``Files with no machine-readable author``. -- -- Reads the *raw* page wikitext via ``mw.title:getContent()`` because -- by the time Lua sees ``args``, Scribunto has already pre-expanded -- the ``{{InFi|...}}`` sub-template into its rendered HTML — there's -- no longer a literal ``{{InFi|Creator|...}}`` string to match. The -- raw-content path is the same one ``parseLegacyFromPageContent`` -- uses for the legacy ``source = {{DPLA|...}}`` block; ``getContent`` -- is cached within a single parse so the additional call is -- effectively free when both legacy shapes coexist on the page. local function legacyOtherFieldsCreator() local title = mw.title.getCurrentTitle() if not title then return nil end local content = title:getContent() if not content then return nil end local envelope = extractTemplateBody(content, 'DPLA metadata') if not envelope then return nil end local topArgs = splitTopLevelArgs(envelope) for i = 1, 9 do local v = topArgs['other fields ' .. i] if v and v ~= '' then -- ``{{ InFi | <Field> | <Value> | id=<id> }}`` shape; -- the field label preceding the value is ``Creator``, -- ``Author``, or ``Artist`` (case-insensitive, with -- optional surrounding whitespace) on the InFi -- invocations used by the legacy DPLA uploader. local extracted = v:match( '{{%s*[Ii]n[Ff]i%s*|%s*[Cc]reator%s*|%s*(.-)%s*|' ) or v:match( '{{%s*[Ii]n[Ff]i%s*|%s*[Aa]uthor%s*|%s*(.-)%s*|' ) or v:match( '{{%s*[Ii]n[Ff]i%s*|%s*[Aa]rtist%s*|%s*(.-)%s*|' ) if extracted and extracted ~= '' then return extracted end end end return nil end -- Resolve the user-side source/institution data into the flat shape the -- parametric renderers consume, dual-pathed across: -- 1. Flat params on ``{{DPLA metadata}}`` (hub, institution, url, -- dpla_id, local_id) — what new uploads emit. -- 2. Legacy nested params: ``source = {{DPLA|...}}`` and -- ``Institution = {{Institution|wikidata=...}}`` — what historic -- and pre-flat uploads carry. The literal sub-template invocations -- are pulled from ``mw.title.getCurrentTitle():getContent()`` -- because Scribunto's args are pre-expanded. -- -- Returns a table with hub/inst_qid/url/dpla_id/local_id; any field -- may be nil. Flat-param values take precedence over legacy-parsed -- ones so a file that's been migrated to flat shape but still has -- stale legacy params (mid-transition) renders using the new shape. local function resolveUserSourceFields(args) local hub = firstParam(args, 'hub') local instQid = firstParam(args, 'institution') local url = firstParam(args, 'url') local dplaId = firstParam(args, 'dpla_id') local localId = firstParam(args, 'local_id') if hub and url and dplaId and localId then return { hub = hub, inst_qid = instQid, url = url, dpla_id = dplaId, local_id = localId, } end -- Only pay the page-content fetch cost when there's actually a -- legacy ``source`` or ``Institution`` param to resolve. Files in -- the flat-shape pipeline never hit this path; files lacking any -- source/institution context (a rare hand-written edit) also skip -- it. mw.title:getContent() registers a transclusion dependency -- whose invalidation pulse triggers on every page edit, so the -- gate keeps the cache-invalidation graph minimal. local hasLegacySourceArg = firstParam(args, 'source') ~= nil local hasLegacyInstArg = firstParam(args, 'Institution') ~= nil or firstParam(args, 'institution') ~= nil if not hasLegacySourceArg and not hasLegacyInstArg then return { hub = hub, inst_qid = instQid, url = url, dpla_id = dplaId, local_id = localId, } end local legacy = parseLegacyFromPageContent() return { hub = hub or legacy.hub, inst_qid = instQid or legacy.inst_qid, url = url or legacy.url, dpla_id = dplaId or legacy.dpla_id, local_id = localId or legacy.local_id, } end -- Build the user-contributed (yellow) table rows. Each field prefers -- an explicit template parameter; if absent, falls back to non-DPLA -- SDC statements on the same property. For DPLA-bot uploads (legacy -- ``{{DPLA|...}}`` shape or new flat-param shape), the source and -- partnership rows render from the parsed wikitext params via the -- same parametric helpers the blue box uses — keeping the yellow-box -- presentation aligned with the blue box and avoiding the ``{|`` -- raw-markup leak the legacy sub-template caused inside an HTML cell. local function buildUserRows(entity, args, frame, hasDplaBox) local rows = {} -- ``add`` mirrors ``buildDplaRows``'s helper — takes a label -- *key* so the same key drives both the localized label and -- the optional ``fileinfotpl_*`` machine-readable id on the -- value cell. local function add(labelKey, value) local row = tableRow( localizedLabel(labelKey), value, nil, MACHINE_READABLE_ID[labelKey] ) if row then table.insert(rows, row) end end local title = userValue(args, {'title'}, function() return entity and titlesText(entity, nil, isNonDplaDetermined) or '' end, entity, P.TITLE, 'monolingualtext') -- Only render the yellow box's own title banner when the blue box -- isn't present. When the blue box is rendering above, it already -- emits the file's title in the big-bold banner; duplicating it (or -- worse, promoting a user-contributed translation to a secondary -- big header) reads as two coequal titles. The user-contributed -- title still renders below as a normal row via add('title', ...). if not hasDplaBox then local banner = titleBanner(title) if banner then table.insert(rows, banner) end end add('title', title) add('creator', userValue( args, {'author', 'artist', 'creator'}, function() -- SDC fallback: prefer a non-DPLA P170 statement, then -- fall back to the legacy ``Other fields N = {{InFi | -- Creator | ...}}`` shape some mid-migration files still -- carry. The InFi extraction is critical for the -- machine-readable ``fileinfotpl_aut`` id: without it, -- the creator row never renders on legacy-shape files -- and the page lands in -- ``Files with no machine-readable author``. local sdcVal = entity and creatorText(entity, nonDplaStatements) or '' if sdcVal and sdcVal ~= '' then return sdcVal end return legacyOtherFieldsCreator() or '' end, -- Check P170 (creator) statements for a P2093 (stated as) -- qualifier matching the wikitext value — that's the canonical -- DPLA SDC shape. See the ``creator-name`` branch of -- ``valueMatchesDplaSdc`` for the comparison logic. entity, P.CREATOR, 'creator-name' )) add('description', userValue( args, {'description'}, function() return entity and descriptionText(entity, nil, isNonDplaDetermined) or '' end, entity, P.DESCRIPTION, 'monolingualtext' )) add('date', userValue( args, {'date'}, function() return entity and dateText(entity, nonDplaStatements, frame) or '' end, entity, P.DATE, 'time' )) -- Source-row fields (hub/institution/url/dpla_id/local_id) are -- consolidated once because the institution Q-ID is used by both -- the Institution row and the Partnership row, and the legacy -- ``source = {{DPLA|...}}`` carried it alongside the URL fields. -- Resolving once also means the legacy-shape parse only runs once -- per file. local userSource = resolveUserSourceFields(args) -- Institution row. Prefers the resolved Q-ID (works for both flat -- ``institution = Q...`` uploads and legacy ``Institution = -- {{Institution|wikidata=Q...}}`` uploads), falling back to a -- non-DPLA P195 SDC statement when neither template form is -- present. The wikitext Q-ID is suppressed when it matches the -- DPLA-attributed P195 — same redundancy rule the scalar rows -- above apply: a wikitext value identical to DPLA SDC is by -- definition not a user contribution. local userInstQid = userSource.inst_qid if userInstQid and institutionQidMatchesDplaSdc(entity, userInstQid) then userInstQid = nil -- fall through to non-DPLA SDC below end if userInstQid then add('institution', frame:expandTemplate{ title = 'Institution', args = {wikidata = userInstQid}, }) elseif entity then local sdcInstQid = institutionQidOf(entity, nonDplaStatements) if sdcInstQid then add('institution', frame:expandTemplate{ title = 'Institution', args = {wikidata = sdcInstQid}, }) end end add('subject', userValue(args, {'subject'}, function() return entity and subjectsText(entity, nonDplaStatements, isNonDplaDetermined) or '' end)) -- Source row. Builds the icon-cell HTML directly from the resolved -- catalog URL — same parametric helper the SDC-driven blue box -- uses. The legacy ``source = {{DPLA|...}}`` sub-template path -- used to fall through to a verbatim pass-through here, which -- leaked the sub-template's ``{|`` wikitable syntax into the -- surrounding HTML <td> (MediaWiki doesn't restart table scanning -- inside an HTML cell). ``resolveUserSourceFields`` parses both -- the legacy and the flat shape into the same flat dict, so the -- icon-cell renderer handles either identically. -- -- Only ``catalogUrl`` is populated from wikitext params — the -- per-ordinal direct-file URL and IIIF manifest URL live only in -- SDC (P2699 / P6108) and aren't carried in the upload-time -- wikitext. The yellow box therefore shows just the catalog-link -- cell; the blue box shows up to three cells once SDC is written. -- Source-row redundancy guard: if there's any DPLA-attributed -- P7482 (described at) statement, the blue box already shows the -- canonical catalog/file/IIIF URLs. A wikitext-derived source row -- showing the same catalog URL would be redundant — suppress. local hasDplaSource = entity and #dplaStatements(entity, P.SOURCE_OF_FILE) > 0 if userSource.url and not hasDplaSource then local naraInterwiki if userSource.inst_qid == Q.NARA and userSource.local_id then naraInterwiki = '[[nara:' .. userSource.local_id .. '|' .. localizedLabel('catalog_record') .. ']]' end add('source', sourceFieldFromUrls( nil, nil, userSource.url, naraInterwiki )) elseif not userSource.url then -- Pass-through fallback: a Commons editor may have written a -- free-text source description that's neither the DPLA-bot -- ``{{DPLA|...}}`` invocation nor a flat-param shape. Preserve -- it verbatim — the editor put it there deliberately, and -- losing the row would silently discard their contribution. -- The ``{|`` parsing problem only affects the specific -- ``{{DPLA|...}}`` legacy shape (its inner {{DPLA/layout}} -- emits wikitable syntax); free-text or other-template -- values render fine in an HTML cell. local rawSource = firstParam(args, 'source') if rawSource then add('source', rawSource) end end -- Partnership row. Renders from the resolved institution + hub -- Q-IDs the Institution row used. Surfaced in the yellow box for -- the upload → SDC-sync interim window (no SDC yet, wikitext -- params are the only source). Once SDC is written and carries -- the canonical P9126 partnership, the wikitext-derived row would -- be a redundant duplicate — suppress when any DPLA-attributed -- P9126 statement exists. The blue box's partnership card renders -- from that same SDC. -- -- Partnership is added BEFORE Permission so the yellow box's row -- order matches the blue box's (see ``buildDplaRows`` above). The -- mismatch was a real, visible reordering on files with no SDC -- yet — the rendered yellow box put the big Permission block -- above the Partnership card, which read as Partnership being -- demoted to a footer. local hasDplaPartnership = entity and #dplaStatements(entity, P.PARTNERSHIP) > 0 if (userSource.inst_qid or userSource.hub) and not hasDplaPartnership then add('partnership', partnershipFromQids(userSource.inst_qid, userSource.hub)) end -- Permission row. A wikitext ``permission = {{NoC-US|...}}`` / -- ``{{Cc-zero}}`` / similar template renders to a clean permission -- box inside a cell when present. Suppress when any DPLA-attributed -- P6426 (copyright license) statement is on the entity — the blue -- box already renders the permission from that SDC. Comparing the -- specific wikitext template name (``NoC-US``, ``Cc-zero``, etc.) -- to the P6426 Q-ID (Q47530911, Q6938433, etc.) requires a mapping -- table that doesn't exist here; the existence-based suppression -- is a safe over-suppression because the wikitext permission was, -- in practice, always written by the bot to mirror SDC. local hasDplaPermission = entity and #dplaStatements(entity, 'P6426') > 0 if not hasDplaPermission then add('permission', firstParam(args, 'permission') or '') end return rows end local function divbox(color, banner, body, frame) return frame:expandTemplate{ title = 'divbox', args = {[1] = color, [2] = banner, [3] = body}, } end --- p.render_metadata_table — entry point invoked from the -- ``Template:DPLA metadata`` template. Builds up to two stacked boxes: -- -- * Blue box: DPLA-determined SDC, with the standard provenance -- banner. Rendered iff at least one DPLA field is present. -- * Yellow box: explicit template parameters (matching {{Information}} -- or {{Artwork}} param names) plus any non-DPLA SDC on the same -- properties. Rendered iff at least one user-contributed value -- exists. -- -- Either box is silently omitted when its rows are empty; on a file -- with no DPLA SDC and no template params, render_metadata_table -- returns the empty string. function p.render_metadata_table(frame) local invokeArgs = (frame and frame.args) or {} local parent = frame and frame:getParent() or nil local userArgs = (parent and parent.args) or {} local entity = getEntity(invokeArgs) -- Register the ``{{Infobox template tag}}`` transclusion so -- ``{{DPLA metadata}}`` is recognised by YiFeiBot's -- "Media missing infobox template" scanner. The tag is an -- empty marker template; the ``expandTemplate`` call has no -- observable output but registers the transclusion edge the -- bot looks for. Same pattern ``Module:Information`` and -- ``Module:Artwork`` use — see the comment in Module:Information: -- "all official infoboxes transclude {{Infobox template tag}} -- so files without that tag do not have an infobox". if frame then frame:expandTemplate{ title = 'Infobox template tag' } end local dplaRows = entity and buildDplaRows(entity, frame) or {} local userRows = buildUserRows(entity, userArgs, frame, #dplaRows > 0) local out = {} local dplaTable = wrapInTable(dplaRows) if dplaTable then table.insert(out, divbox('blue', localizedLabel('banner_dpla'), dplaTable, frame)) end local userTable = wrapInTable(userRows) if userTable then -- The yellow box's "added by Wikimedia users" disclaimer only -- makes sense as a contrast against the blue DPLA-attributed -- box. When the blue box is suppressed (no DPLA SDC), drop the -- banner so the yellow box stands on its own. local yellowBanner = dplaTable and localizedLabel('banner_user') or '' table.insert(out, divbox('yellow', yellowBanner, userTable, frame)) end -- "Media contributed by..." categories — DPLA, hub, institution. -- Dual-source: prefer SDC-resolved Qids when present; otherwise -- fall back to the parsed wikitext source (legacy -- ``source = {{DPLA|...}}`` or flat ``hub`` / ``institution`` -- params). The wikitext fallback covers legacy ``{{DPLA}}`` / -- dual-render files that haven't had their SDC synced yet — they -- still need to appear in the hub / institution maintenance -- categories the legacy ``{{DPLA/hub cat}}`` / ``{{DPLA/inst cat}}`` -- templates emitted. A file with neither source identifying it -- as a DPLA upload stays uncategorised. local catHubQid, catInstQid = resolveCategoryQidsFromEntity(entity) if not catHubQid and not catInstQid then local src = resolveUserSourceFields(userArgs) if src.hub or src.inst_qid then catHubQid = src.hub or src.inst_qid catInstQid = src.inst_qid end end if catHubQid or catInstQid then table.insert(out, buildCategoryList(catHubQid, catInstQid)) end -- Tracking category for "files with non-DPLA metadata enhancements": -- emit iff the yellow box renders WITH its banner — i.e. there's -- a DPLA-attributed blue box AND user-contributed yellow rows on -- top of it. The banner is suppressed (``dplaTable`` nil) on a -- just-uploaded file where SDC sync hasn't run yet; the yellow -- box still renders there because the wikitext params haven't -- been stripped, but those values aren't real "enhancements" — -- they're the upload's own metadata pending SDC migration. Gating -- on ``dplaTable and userTable`` keeps the category tied to the -- post-SDC steady state where genuine community contributions -- show up. if dplaTable and userTable then table.insert(out, '[[Category:' .. ENHANCED_CAT .. ']]') end -- Missing-SDC tracking categories — mirror the legacy -- {{DPLA}} template's ``{{#invoke:SDC_tracking|SDC_statement_exist}}`` -- block. Files lacking any of the canonical required properties -- land in the "missing required SDC statements" maintenance -- category (operators backfill from there); ``P170`` (creator) -- gets its own bucket so its backlog can be triaged separately. -- The check matches ``Module:SDC_tracking._statement_exist`` — -- simple ``entity.statements[property]`` existence, no -- DPLA-attribution filter — so wikitext-contributed claims also -- count as "has statement". -- -- Always fires on DPLA-rendered files (the template is -- bot-only); a file rendering through Module:DPLA is by -- definition a DPLA file and gets the tracking. Categorylinks -- dedupes, so multiple missing required props still emit the -- "missing required" category just once. local missingRequired = false if not entity or not entity.statements then missingRequired = true else for _, prop in ipairs(REQUIRED_SDC_PROPS) do if not entity.statements[prop] then missingRequired = true break end end end if missingRequired then table.insert( out, '[[Category:Digital Public Library of America files missing required SDC statements]]' ) end local hasCreator = entity and entity.statements and entity.statements[P.CREATOR] if not hasCreator then table.insert( out, '[[Category:Digital Public Library of America files missing creator]]' ) end -- Surface files whose DPLA-authored P275/P6426 rights statement carries -- a Q-ID that is not in our Commons-eligibility allowlist. Typically the -- contributing institution updated their rights metadata to a non-free -- license (CC-BY-NC*, CC-BY-*-ND, InC*) after the file was already -- uploaded under an eligible license; DPLA's SDC sync correctly mirrors -- the institution's authoritative state, so the file ends up with -- accurate-but-Commons-incompatible structured data. Silent — no -- rendered output, just the tracking category for human review. local hasIneligibleRights = false if entity and entity.statements then for _, prop in ipairs({'P275', 'P6426'}) do for _, stmt in ipairs(dplaStatements(entity, prop)) do local ms = stmt.mainsnak if ms.snaktype == 'value' and ms.datavalue and ms.datavalue.type == 'wikibase-entityid' and not ELIGIBLE_RIGHTS[ms.datavalue.value.id] then hasIneligibleRights = true break end end if hasIneligibleRights then break end end end if hasIneligibleRights then table.insert( out, '[[Category:Media from the Digital Public Library of America with potential copyright issues]]' ) end return table.concat(out, '\n') end return p jrs02pyppryuge0mjdejmbt63lhn2ct Template:DPLA metadata 10 176328 747018 2026-06-16T14:29:32Z Seanleong8 67988 Created page with "<noinclude> {{documentation}} </noinclude><includeonly>{{#invoke:DPLA|render_metadata_table}}</includeonly>" 747018 wikitext text/x-wiki <noinclude> {{documentation}} </noinclude><includeonly>{{#invoke:DPLA|render_metadata_table}}</includeonly> 5ak6leh2utj86jyj7u1hk5aefmonwp9 File:Testing hCaptcha for fle upload changes.png 6 176329 747071 2026-06-16T17:32:14Z WBrown test account 29213081328213818231092309 74476 747071 wikitext text/x-wiki phoiac9h4m842xq45sp7s6u21eteeq1 Talk:WET Awards 2001 1 176330 747080 2026-06-16T18:06:25Z ~2026-35392-04 74478 Created page with "{{WikiProject banner shell|class=Stub| {{WikiProject Television|importance=Low}} {{WikiProject Awards}} }}" 747080 wikitext text/x-wiki {{WikiProject banner shell|class=Stub| {{WikiProject Television|importance=Low}} {{WikiProject Awards}} }} 0v1a58r1xuzz13wq25hebleggxcr8fu Template:WikiProject Television 10 176331 747081 2026-06-16T18:07:17Z ~2026-35392-04 74478 Created page with "{{Short description|Wikimedia subject-area collaboration}} {{Redirect2|WP:TELEVISION|WP:TV|the notability essay concerning television series|WP:TVSERIES|the Wikipedia television channel|WP:WikiTV|the Manual of Style page for TV shows|MOS:TV}} {{/Tabs}} {{WikiProject status|active|project-type=topic|topic=Television|portal=Television|sc1=WP:TV|sc2=WP:TELEVISION}} {{Wikipedia:WikiProject Television/Navigation}} This is to guide the structure and normalize the standard of a..." 747081 wikitext text/x-wiki {{Short description|Wikimedia subject-area collaboration}} {{Redirect2|WP:TELEVISION|WP:TV|the notability essay concerning television series|WP:TVSERIES|the Wikipedia television channel|WP:WikiTV|the Manual of Style page for TV shows|MOS:TV}} {{/Tabs}} {{WikiProject status|active|project-type=topic|topic=Television|portal=Television|sc1=WP:TV|sc2=WP:TELEVISION}} {{Wikipedia:WikiProject Television/Navigation}} This is to guide the structure and normalize the standard of articles dealing with the [[television]] medium and [[television series]] or other forms of episodic programs, such as web series. == Descendant WikiProjects and task forces == {{See also|Wikipedia:WikiProject Television/Descendant WikiProjects and task forces}} == Related WikiProjects == * [[Wikipedia:WikiProject Actors and Filmmakers|WikiProject Actors and Filmmakers]] * [[Wikipedia:WikiProject Albums|WikiProject Albums]] * [[Wikipedia:WikiProject Animation|WikiProject Animation]] * [[Wikipedia:WikiProject Anime and manga|WikiProject Anime and manga]] * [[Wikipedia:WikiProject BBC|WikiProject BBC]] * [[Wikipedia:WikiProject Deletion sorting/Television|WikiProject Deletion sorting/Television]] * [[Wikipedia:WikiProject Film|WikiProject Film]] * [[Wikipedia:WikiProject Literature|WikiProject Literature]] * [[Wikipedia:WikiProject Radio|WikiProject Radio]] * [[Wikipedia:WikiProject Screenwriters|WikiProject Screenwriters]] * [[Wikipedia:WikiProject Westerns|WikiProject Westerns]] ==Participants== {{Userboxtop|User WikiProject Television}} {{User WikiProject Television}} {{Userboxbottom}} A list of participants can be found at '''[[:Category:WikiProject Television participants]]'''. Technically speaking, anyone who edits or contributes to TV related articles is a participant, and there are no requirements other than that. If one wishes one can further identify oneself with the project by listing one's name as a participant. This helps spread the word about the project and can help other editors see what types of articles that user is interested in editing. To add yourself to the project listing add the following to your [[Wikipedia:User pages|user page]]: <syntaxhighlight lang="wikitext" inline>[[Category:WikiProject Television participants|{{subst:PAGENAME}}]]</syntaxhighlight> {{em|Or}}, if you wish to use the userbox, add {{tlx|User WikiProject Television}} to your user page instead as it includes the above wikicode. ==Writing and editing television related articles== {{Further|Wikipedia:Manual of Style/Television}} This project deals with a lot of television related articles, but they are not all the same. Therefore we have created separate pages with information for each type of television related article. ===Guidelines and essays=== {{Category see also|Category:Wikipedia Manual of Style (arts)|Wikipedia naming conventions (arts)|Wikipedia essays about media}} ====Project-specific guidelines and essays==== * [[Wikipedia:Manual of Style/Television|Manual of Style/Television]] * [[Wikipedia:Naming conventions (television)|Naming conventions (television)]] * [[Wikipedia:Naming conventions (broadcasting)|Naming conventions (broadcasting)]] for advice on articles about television networks, channels, etc. * [[Wikipedia:Notability (television)|Notability (television)]] (essay) ====Related guidelines and essays==== * [[Wikipedia:Manual of Style/Trivia sections|Trivia sections]] * [[Wikipedia:Manual of Style/Writing about fiction|Manual of Style/Writing about fiction]] * [[Wikipedia:Writing better articles#Check your fiction|Writing better articles: Check your fiction]] * [[Wikipedia:What Wikipedia is not#Wikipedia is not an indiscriminate collection of information|Wikipedia is not an indiscriminate collection of information (#1, Plot summaries)]] * [[Wikipedia:Notability (fiction)|Notability (fiction)]] * [[Wikipedia:Notability (media)|Notability (media)]] (essay) * Possible proliferation of in-universe-related content is discussed in the [[Wikipedia:Essays|essay]] [[Wikipedia:Fancruft]] and its talk page. ===Characters and fictional elements=== ====Featured articles==== See these [[Wikipedia:Featured content|featured articles]] for examples of well written articles on characters and elements from fiction: * [[Homer Simpson]] * [[C. J. Cregg]] * [[Yeti (Doctor Who)]] ====Naming a character==== There is no guideline or policy about naming an article about a fictional character. See the essay [[Wikipedia:Naming character articles]]. ===People working in television=== Actors, producers, writers etc. are all real people. Turn to [[Wikipedia:WikiProject Biography|WikiProject Biography]] for help on writing such articles. Here are some quick guidelines and tips for you however. * [[Wikipedia:Notability (people)|Notability (people)]] * [[Wikipedia:Biographies of living persons|Biographies of living persons]] * [[Wikipedia:Manual of Style/Biography|Manual of Style/Biography]] * [[Wikipedia:WikiProject Screenwriters|WikiProject Screenwriters]] – project dedicated to [[screenwriter]]s. ====Categories==== As a rule do not create categories for "People '''by series'''", because they can potentially work on numerous series or movies etc. You should categorize by profession. See the higher level [[:Category:Television actors]], [[:Category:Television directors]], [[:Category:Television producers]], [[:Category:Television writers]], [[:Category:Television presenters]] in the top level [[:Category:Television people]]. ====Images==== [[Wikipedia:Non-free content|Fair Use]] images on pages of real people that are still alive are not allowed, because these images are replaceable. You also cannot use an image from a television program, ''etc.'', because you are not illustrating the character but the actor. ====Featured articles==== * [[Kroger Babb]] * [[James T. Aubrey]] * [[Aaron Sorkin]] == More television templates == {{See also|Wikipedia:WikiProject Television/Templates}} ==Tasks== {{To do}} == Maintenance categories == See also {{Category link with count|Film and television backlog}} === Infobox television === {{Transcluded section|source=Template:Infobox television/doc}} {{Trim|{{#section:Template:Infobox television/doc|Tracking}}}} === Infobox television season === {{Transcluded section|source=Template:Infobox television season/doc}} {{Trim|{{#section:Template:Infobox television season/doc|Tracking}}}} === Infobox television episode === {{Transcluded section|source=Template:Infobox television episode/doc}} {{Trim|{{#section:Template:Infobox television episode/doc|Tracking}}}} === ''Doctor Who'' infoboxes === {{Transcluded section|source=Template:Infobox Doctor Who episode/doc}} {{Trim|{{#section:Template:Infobox Doctor Who episode/doc|Tracking}}}} * {{Category link with count|Pages using infobox Doctor Who doctor with unknown parameters}} === Other infoboxes === * {{Category link with count|Pages using infobox broadcasting network with unknown parameters}} * {{Category link with count|Pages using infobox Paris by Night with unknown parameters}} * {{Category link with count|Pages using infobox reality competition season with unknown parameters}} * {{Category link with count|Pages using infobox television channel with unknown parameters}} * {{Category link with count|Pages using infobox television crossover episode with unknown parameters}} * {{Category link with count|Pages using infobox television station with unknown parameters}} === Aired episodes === {{Transcluded section|source=Template:Aired episodes/doc}} {{Trim|{{#section:Template:Aired episodes/doc|Tracking}}}} === Episode table === {{Transcluded section|source=Template:Episode table/doc}} {{Trim|{{#section:Template:Episode table/doc|Tracking}}}} === Episode lists === {{Transcluded section|source=Module:Episode list/doc}} {{Trim|{{#section:Module:Episode list/doc|Tracking}}}} === Series overview === {{Transcluded section|source=Template:Series overview/doc}} {{Trim|{{#section:Template:Series overview/doc|Tracking}}}} === Sections needed === * {{Category link with count|Television articles that need a character section}} * {{Category link with count|Television articles needing infoboxes}} ** {{Category link with count|Cartoon Network articles needing infoboxes}} * {{Category link with count|Television articles that need a production section}} * {{Category link with count|Television articles that need a response section}} * {{Category link with count|Television articles that need a synopsis}} * {{Category link with count|Television articles that need an episode list}} === Naming style === {{Further|Wikipedia:Naming conventions (television)}} * {{Category link with count|Television articles with incorrect naming style}} * {{Category link with count|Television channel articles with incorrect naming style}} === IMDb === * {{Category link with count|All articles sourced by IMDb}} * {{Category link with count|Articles sourced only by IMDb}} * {{Category link with count|Monthly clean-up category (Articles sourced by IMDb) counter}} ==== IMDb country year ==== {{Transcluded section|source=Template:IMDb country year/doc}} {{Trim|{{#section:Template:IMDb country year/doc|Tracking}}}} ==== IMDb episode ==== {{Transcluded section|source=Template:IMDb episode/doc}} {{Trim|{{#section:Template:IMDb episode/doc|Tracking}}}} ==== IMDb episodes ==== {{Transcluded section|source=Template:IMDb episodes/doc}} {{Trim|{{#section:Template:IMDb episodes/doc|Tracking}}}} ==== IMDb event ==== {{Transcluded section|source=Template:IMDb event/doc}} {{Trim|{{#section:Template:IMDb event/doc|Tracking}}}} ==== IMDb name ==== {{Transcluded section|source=Template:IMDb name/doc}} {{Trim|{{#section:Template:IMDb name/doc|Tracking}}}} ==== IMDb title ==== {{Transcluded section|source=Template:IMDb title/doc}} {{Trim|{{#section:Template:IMDb title/doc|Tracking}}}} === Citation templates === * {{Category link with count|Cite Metacritic template with unknown parameters}} * {{Category link with count|Cite Metacritic templates with errors}} * {{Category link with count|Cite Rotten Tomatoes templates with errors}} * {{Category link with count|Cite Rotten Tomatoes templates with unknown parameters}} === WritingCredits === {{Transcluded section|source=Template:WritingCredits/doc}} {{Trim|{{#section:Template:WritingCredits/doc|Tracking}}}} === Requested images === * {{Category link with count|My Little Pony articles needing photos}} * {{Category link with count|Star Trek articles needing images}} * {{Category link with count|Star Wars articles needing photos}} * {{Category link with count|Wikipedia requested images of American television topics}} * {{Category link with count|Wikipedia requested images of animated television}} * {{Category link with count|Wikipedia requested images of Cartoon Network}} * {{Category link with count|Wikipedia requested images of Family Guy}} * {{Category link with count|Wikipedia requested images of Hanna-Barbera}} * {{Category link with count|Wikipedia requested images of television programs}} * {{Category link with count|Wikipedia requested photographs of American television topics}} * {{Category link with count|Wikipedia requested television images}} === Unassessed articles === * {{Category link with count|Unassessed television articles}} ** {{Category link with count|Unassessed Star Wars articles}} * {{Category link with count|Unknown-importance television articles}} ** {{Category link with count|Unknown-importance Star Wars articles}} === Convert === * {{Category link with count|Articles lacking a Series overview template}} * {{Category link with count|Articles lacking Episode list templates}} * {{Category link with count|Articles lacking Episode table templates}} === Misc === * {{Category link with count|Articles that need an episode citation}} * {{Category link with count|Articles using Template:Australian television episode ratings with invalid colour combination}} * {{Category link with count|Articles using Template:Television critical response with only one row}} * {{Category link with count|Incomplete film, television, or video lists}} ** {{Category link with count|Incomplete television lists}} * {{Category link with count|Pages using film date with TV parameter}} * {{Category link with count|Pages using WikiProject Television with unknown parameters}} * {{Category link with count|Television articles needing attention}} ** {{Category link with count|Cartoon Network articles needing attention}} ** {{Category link with count|Doctor Who articles needing attention}} * {{Category link with count|Television articles needing expert attention}} * {{Category link with count|Television articles needing references}} * {{Category link with count|Television articles that need to differentiate between fact and fiction}} ** {{Category link with count|Cartoon Network articles that need to differentiate between fact and fiction}} ** {{Category link with count|Star Trek articles that need to differentiate between fact and fiction}} ** {{Category link with count|Star Wars articles that need to differentiate between fact and fiction}} * {{Category link with count|Television articles using incorrect infobox}} * {{Category link with count|Television articles with topics of unclear notability}} * {{Category link with count|Television awards that need updating}} * {{Category link with count|Unreferenced Cartoon Network articles}} == Maintenance reports == * [[Wikipedia:Database reports/Broken episode anchors]] ==Important alerts== ===New articles=== {{Main|User:AlexNewArtBot/TelevisionSearchResult}} [[User:InceptionBot|InceptionBot]] has been set up to identify new articles that are potentially within the scope of this project, based on the rules defined at [[User:AlexNewArtBot/Television]]. It is likely that some are false positives, in which case the log at [[User:AlexNewArtBot/TelevisionLog]] should be examined for more information. ===Article alerts=== Please watch [[Wikipedia:WikiProject Television/Article alerts]], for important alerts related to articles of this Wikiproject. "Article alerts" will be updated daily, and will also be transcluded here. {{Article alerts columns}} {{Help navigation}} {{Wikipedia policies and guidelines}} {{WikiProjects navigation}} {{WikiProject Footer}} {{DEFAULTSORT:Television}} [[Category:WikiProject Television| ]] [[Category:Media WikiProjects|Television]] [[Category:Technology WikiProjects|Television]] [[Category:WikiProjects participating in Wikipedia 1.0 assessments]] 59o60rgp2gka02ntfp9ycud46hn520g Wikipedia:Articles for deletion/Log/2026 June 16 4 176332 747084 2026-06-16T18:09:17Z Trialpears 43074 Adding [[:Wikipedia:Articles for deletion/Mergetest1]]. 747084 wikitext text/x-wiki {{Recent AfDs}} <div class="boilerplate metadata vfd" style="background-color: #F3F9FF; margin: 0 auto; padding: 0 1px 0 0; border: 1px solid #AAAAAA; font-size:10px"> {| width = "100%" |- ! width="50%" align="left" | <span style="color:gray">&lt;</span> [[Wikipedia:Articles for deletion/Log/2026 June 15|June 15]] ! width="50%" align="right" | [[Wikipedia:Articles for deletion/Log/2026 June 17|June 17]] <span style="color:gray">&gt;</span> |} </div> <div align = "center">'''[[Wikipedia:Guide to deletion|Guide to deletion]]'''</div> {{Cent}} <small>{{purge|Purge server cache}}</small> __TOC__ <!-- Add new entries to the TOP of the following list --> {{Wikipedia:Articles for deletion/Mergetest1}} r777rd20vefqctac0nyjstzvt91sccd Wikipedia:Articles for deletion/Mergetest1 4 176333 747086 2026-06-16T18:09:17Z Trialpears 43074 Creating AfD discussion page for [[:Mergetest1]]. 747086 wikitext text/x-wiki ===[[:Mergetest1]]=== {{REMOVE THIS TEMPLATE WHEN CLOSING THIS AfD|?}} <noinclude>{{AFD help}}</noinclude> :{{la|Mergetest1}} – (<includeonly>[[Wikipedia:Articles for deletion/Mergetest1|View AfD]]</includeonly><noinclude>[[Wikipedia:Articles for deletion/Log/2026 June 16#{{anchorencode:Mergetest1}}|View log]]</noinclude>{{int:dot-separator}} <span class="plainlinks">[https://tools.wmflabs.org/jackbot/snottywong/cgi-bin/votecounter.cgi?page=Wikipedia:Articles_for_deletion/Mergetest1 Stats]</span>) :({{Find sources AFD|Mergetest1}}) I propose '''merging''' to [[Mergetest2]] because test [[User:Trialpears|Trialpears]] ([[User talk:Trialpears|talk]]) 18:09, 16 June 2026 (UTC) 0rhrnop3dzva6461k3bgksld2wvrp41 747090 747086 2026-06-16T18:11:50Z Trialpears 43074 /* Mergetest1 */ Closed as merge ([[WP:XFDC#4.0.16|XFDcloser]]) 747090 wikitext text/x-wiki <div class="boilerplate afd vfd xfd-closed" style="background-color: #F3F9FF; margin: 2em 0 0 0; padding: 0 10px 0 10px; border: 1px solid #AAAAAA;"> :''The following discussion is an archived debate of the proposed deletion of the article below. <span style="color:red">'''Please do not modify it.'''</span> Subsequent comments should be made on the appropriate discussion page (such as the article's [[Help:Using talk pages|talk page]] or in a [[Wikipedia:Deletion review|deletion review]]). No further edits should be made to this page.'' <!--Template:Afd top Note: If you are seeing this page as a result of an attempt to re-nominate an article for deletion, you must manually edit the AfD nomination links to create a new discussion page using the name format of [[Wikipedia:Articles for deletion/PAGENAME (2nd nomination)]]. When you create the new discussion page, please provide a link to this old discussion in your nomination. --> The result was '''merge''' to [[Mergetest2]]. [[User:Trialpears|Trialpears]] ([[User talk:Trialpears|talk]]) 18:11, 16 June 2026 (UTC) ===[[:Mergetest1]]=== <noinclude>{{AFD help}}</noinclude> :{{la|Mergetest1}} – (<includeonly>[[Wikipedia:Articles for deletion/Mergetest1|View AfD]]</includeonly><noinclude>[[Wikipedia:Articles for deletion/Log/2026 June 16#{{anchorencode:Mergetest1}}|View log]]</noinclude>{{int:dot-separator}} <span class="plainlinks">[https://tools.wmflabs.org/jackbot/snottywong/cgi-bin/votecounter.cgi?page=Wikipedia:Articles_for_deletion/Mergetest1 Stats]</span>) :({{Find sources AFD|Mergetest1}}) I propose '''merging''' to [[Mergetest2]] because test [[User:Trialpears|Trialpears]] ([[User talk:Trialpears|talk]]) 18:09, 16 June 2026 (UTC) {{clear}} :''The above discussion is preserved as an archive of the debate. <b style="color:red">Please do not modify it.</b> Subsequent comments should be made on the appropriate discussion page (such as the article's [[Help:Using talk pages|talk page]] or in a [[Wikipedia:Deletion review|deletion review]]). No further edits should be made to this page.''<!--Template:Afd bottom--></div> e7an2d41f1zkfa261llf9xc00zn7klp 747101 747090 2026-06-16T18:21:21Z Trialpears 43074 Restored revision 747086 by [[Special:Contributions/Trialpears|Trialpears]] ([[User talk:Trialpears|talk]]) 747101 wikitext text/x-wiki ===[[:Mergetest1]]=== {{REMOVE THIS TEMPLATE WHEN CLOSING THIS AfD|?}} <noinclude>{{AFD help}}</noinclude> :{{la|Mergetest1}} – (<includeonly>[[Wikipedia:Articles for deletion/Mergetest1|View AfD]]</includeonly><noinclude>[[Wikipedia:Articles for deletion/Log/2026 June 16#{{anchorencode:Mergetest1}}|View log]]</noinclude>{{int:dot-separator}} <span class="plainlinks">[https://tools.wmflabs.org/jackbot/snottywong/cgi-bin/votecounter.cgi?page=Wikipedia:Articles_for_deletion/Mergetest1 Stats]</span>) :({{Find sources AFD|Mergetest1}}) I propose '''merging''' to [[Mergetest2]] because test [[User:Trialpears|Trialpears]] ([[User talk:Trialpears|talk]]) 18:09, 16 June 2026 (UTC) 0rhrnop3dzva6461k3bgksld2wvrp41 747103 747101 2026-06-16T18:22:16Z Trialpears 43074 /* Mergetest1 */ Closed as merge ([[WP:XFDC#4.0.16|XFDcloser]]) 747103 wikitext text/x-wiki Intentionally broken for testing go back in history for normal version to [[Mergetest2]]. [[User:Trialpears|Trialpears]] ([[User talk:Trialpears|talk]]) 18:22, 16 June 2026 (UTC) ===[[:Mergetest1]]=== <noinclude>{{AFD help}}</noinclude> :{{la|Mergetest1}} – (<includeonly>[[Wikipedia:Articles for deletion/Mergetest1|View AfD]]</includeonly><noinclude>[[Wikipedia:Articles for deletion/Log/2026 June 16#{{anchorencode:Mergetest1}}|View log]]</noinclude>{{int:dot-separator}} <span class="plainlinks">[https://tools.wmflabs.org/jackbot/snottywong/cgi-bin/votecounter.cgi?page=Wikipedia:Articles_for_deletion/Mergetest1 Stats]</span>) :({{Find sources AFD|Mergetest1}}) I propose '''merging''' to [[Mergetest2]] because test [[User:Trialpears|Trialpears]] ([[User talk:Trialpears|talk]]) 18:09, 16 June 2026 (UTC) {{clear}} :''The above discussion is preserved as an archive of the debate. <b style="color:red">Please do not modify it.</b> Subsequent comments should be made on the appropriate discussion page (such as the article's [[Help:Using talk pages|talk page]] or in a [[Wikipedia:Deletion review|deletion review]]). No further edits should be made to this page.''<!--Template:Afd bottom--></div> 4245pz3acj01rnhwy4ld8h7v6jx4yax 747113 747103 2026-06-16T18:25:05Z Trialpears 43074 747113 wikitext text/x-wiki ===[[:Mergetest1]]=== <noinclude>{{AFD help}}</noinclude> :{{la|Mergetest1}} – (<includeonly>[[Wikipedia:Articles for deletion/Mergetest1|View AfD]]</includeonly><noinclude>[[Wikipedia:Articles for deletion/Log/2026 June 16#{{anchorencode:Mergetest1}}|View log]]</noinclude>{{int:dot-separator}} <span class="plainlinks">[https://tools.wmflabs.org/jackbot/snottywong/cgi-bin/votecounter.cgi?page=Wikipedia:Articles_for_deletion/Mergetest1 Stats]</span>) :({{Find sources AFD|Mergetest1}}) I propose '''merging''' to [[Mergetest2]] because test [[User:Trialpears|Trialpears]] ([[User talk:Trialpears|talk]]) 18:09, 16 June 2026 (UTC) {{clear}} :''The above discussion is preserved as an archive of the debate. <b style="color:red">Please do not modify it.</b> Subsequent comments should be made on the appropriate discussion page (such as the article's [[Help:Using talk pages|talk page]] or in a [[Wikipedia:Deletion review|deletion review]]). No further edits should be made to this page.''<!--Template:Afd bottom--></div> eanks8m9zm7sudlgue37j2ig2qxhlug 747115 747113 2026-06-16T18:25:59Z Trialpears 43074 747115 wikitext text/x-wiki ===[[:Mergetest1]]=== <noinclude>{{AFD help}}</noinclude> :{{la|Mergetest1}} – (<includeonly>[[Wikipedia:Articles for deletion/Mergetest1|View AfD]]</includeonly><noinclude>[[Wikipedia:Articles for deletion/Log/2026 June 16#{{anchorencode:Mergetest1}}|View log]]</noinclude>{{int:dot-separator}} <span class="plainlinks">[https://tools.wmflabs.org/jackbot/snottywong/cgi-bin/votecounter.cgi?page=Wikipedia:Articles_for_deletion/Mergetest1 Stats]</span>) :({{Find sources AFD|Mergetest1}}) I propose '''merging''' to [[Mergetest2]] because test [[User:Trialpears|Trialpears]] ([[User talk:Trialpears|talk]]) 18:09, 16 June 2026 (UTC) ipxd6qacpk4qjm5xplrgb5lvf9a1y76 747116 747115 2026-06-16T18:26:13Z Trialpears 43074 /* Mergetest1 */ Closed as merge ([[WP:XFDC#4.0.16|XFDcloser]]) 747116 wikitext text/x-wiki to [[Mergetest2]]. [[User:Trialpears|Trialpears]] ([[User talk:Trialpears|talk]]) 18:26, 16 June 2026 (UTC) ===[[:Mergetest1]]=== <noinclude>{{AFD help}}</noinclude> :{{la|Mergetest1}} – (<includeonly>[[Wikipedia:Articles for deletion/Mergetest1|View AfD]]</includeonly><noinclude>[[Wikipedia:Articles for deletion/Log/2026 June 16#{{anchorencode:Mergetest1}}|View log]]</noinclude>{{int:dot-separator}} <span class="plainlinks">[https://tools.wmflabs.org/jackbot/snottywong/cgi-bin/votecounter.cgi?page=Wikipedia:Articles_for_deletion/Mergetest1 Stats]</span>) :({{Find sources AFD|Mergetest1}}) I propose '''merging''' to [[Mergetest2]] because test [[User:Trialpears|Trialpears]] ([[User talk:Trialpears|talk]]) 18:09, 16 June 2026 (UTC) 5qixsdnundjn78k7wo9xj8i66yxvhs3 747122 747116 2026-06-16T18:28:10Z Trialpears 43074 /* Mergetest1 */ Closed as merge ([[WP:XFDC#4.0.16|XFDcloser]]) 747122 wikitext text/x-wiki to [[Mergetest2]]. [[User:Trialpears|Trialpears]] ([[User talk:Trialpears|talk]]) 18:26, 16 June 2026 (UTC) to [[Mergetest2]]. [[User:Trialpears|Trialpears]] ([[User talk:Trialpears|talk]]) 18:28, 16 June 2026 (UTC) ===[[:Mergetest1]]=== <noinclude>{{AFD help}}</noinclude> :{{la|Mergetest1}} – (<includeonly>[[Wikipedia:Articles for deletion/Mergetest1|View AfD]]</includeonly><noinclude>[[Wikipedia:Articles for deletion/Log/2026 June 16#{{anchorencode:Mergetest1}}|View log]]</noinclude>{{int:dot-separator}} <span class="plainlinks">[https://tools.wmflabs.org/jackbot/snottywong/cgi-bin/votecounter.cgi?page=Wikipedia:Articles_for_deletion/Mergetest1 Stats]</span>) :({{Find sources AFD|Mergetest1}}) I propose '''merging''' to [[Mergetest2]] because test [[User:Trialpears|Trialpears]] ([[User talk:Trialpears|talk]]) 18:09, 16 June 2026 (UTC) 12w66x9l29ooo4zbovdqw68u5kjp292 747126 747122 2026-06-16T18:34:07Z Trialpears 43074 /* Mergetest1 */ Closed as merge ([[WP:XFDC#4.0.16|XFDcloser]]) 747126 wikitext text/x-wiki to [[Mergetest2]]. [[User:Trialpears|Trialpears]] ([[User talk:Trialpears|talk]]) 18:26, 16 June 2026 (UTC) to [[Mergetest2]]. [[User:Trialpears|Trialpears]] ([[User talk:Trialpears|talk]]) 18:28, 16 June 2026 (UTC) to [[Mergetest2]]. [[User:Trialpears|Trialpears]] ([[User talk:Trialpears|talk]]) 18:34, 16 June 2026 (UTC) ===[[:Mergetest1]]=== <noinclude>{{AFD help}}</noinclude> :{{la|Mergetest1}} – (<includeonly>[[Wikipedia:Articles for deletion/Mergetest1|View AfD]]</includeonly><noinclude>[[Wikipedia:Articles for deletion/Log/2026 June 16#{{anchorencode:Mergetest1}}|View log]]</noinclude>{{int:dot-separator}} <span class="plainlinks">[https://tools.wmflabs.org/jackbot/snottywong/cgi-bin/votecounter.cgi?page=Wikipedia:Articles_for_deletion/Mergetest1 Stats]</span>) :({{Find sources AFD|Mergetest1}}) I propose '''merging''' to [[Mergetest2]] because test [[User:Trialpears|Trialpears]] ([[User talk:Trialpears|talk]]) 18:09, 16 June 2026 (UTC) abyqpjr5a3oulmqyw6ftbnoof2gjves Template:WikiProject Television navigation 10 176334 747088 2026-06-16T18:10:01Z ~2026-35344-03 74479 Created page with "{|class="editlink noprint plainlinks" style="float: right; clear: right; border: 1px solid black; padding: 0; border-spacing: 0; border-collapse: collapse; margin: 0em 0em 2em 2em; background: whitesmoke; color:black; font-size: 90%;" ! style="background: #DECCBF; color:black; padding: 5px; text-align: center; font-size: 150%; border-bottom: 1px solid black;" | {| style="width: 100%; background: transparent; color:inherit; padding: 2px; border-spacing: 2px; border-colla..." 747088 wikitext text/x-wiki {|class="editlink noprint plainlinks" style="float: right; clear: right; border: 1px solid black; padding: 0; border-spacing: 0; border-collapse: collapse; margin: 0em 0em 2em 2em; background: whitesmoke; color:black; font-size: 90%;" ! style="background: #DECCBF; color:black; padding: 5px; text-align: center; font-size: 150%; border-bottom: 1px solid black;" | {| style="width: 100%; background: transparent; color:inherit; padding: 2px; border-spacing: 2px; border-collapse: separate;" !width=40px|[[File:TV-icon-2.svg|65px]] !'''[[Wikipedia:WikiProject Television|WikiProject<br>Television]]''' |} |- | {| style="width: 100%; padding: 3px; border-spacing: 0; border-collapse: collapse;" |-style="background: #F5F5DC; color:black;" | colspan="2" | [[Wikipedia:WikiProject Television|Project main page]] |-style="background: #F0DC82; color:black;" | colspan="2" | [[Wikipedia talk:WikiProject Television|Project discussion]] |-style="background: #F5F5DC; color:black;" | [[Wikipedia:WikiProject Television/Assessment|Project assessment]] |style="text-align: right;" | [[Wikipedia talk:WikiProject Television/Assessment|talk]] |-style="background: #F0DC82; color:black;" | [[Portal:Television|Television portal]] |style="text-align: right;" | [[Portal talk:Television|talk]] |-style="background: #F5F5DC; color:black;" | colspan="2" | [[Wikipedia:WikiProject Television/Descendant WikiProjects and task forces|Descendant WikiProjects and task forces]] |-style="background: #F0DC82; color:black;" | colspan="2" | [[Wikipedia:WikiProject Television/Recognized content|Showcase]] |} {|class="mw-collapsible mw-collapsed" style="width: 100%; padding: 3px; border-spacing: 0; border-collapse: collapse;" |-style="background: #DECCBF; color:black;" ! colspan="2" style="text-align: left; border-top:1px solid black;" | Project organization |-style="background: #F5F5DC; color:black;" | colspan="2" | [[Wikipedia:WikiProject Television/Article alerts|Article alerts]] |-style="background: #F0DC82; color:black;" | colspan="2" | [[Wikipedia:WikiProject Deletion sorting/Television|Deletion sorting]] |-style="background: #F5F5DC; color:black;" | colspan="2" | [[Wikipedia:WikiProject Television/Popular pages|Popular pages]] |-style="background: #F0DC82; color:black;" | colspan="2" | [[Wikipedia:WikiProject Television/New articles|New articles]] |-style="background: #F5F5DC; color:black;" | [[Template:WikiProject Television|Project banner]] |style="text-align: right;" | [[Template talk:WikiProject Television|talk]] |-style="background: #F0DC82; color:black;" | [[:Category:WikiProject Television|Project category]] |style="text-align: right;" | [[:Category talk:WikiProject Television|talk]] |-style="background: #F5F5DC; color:black;" | [[Wikipedia:WikiProject Television/Templates|Project templates]] |style="text-align: right;" | [[Wikipedia talk:WikiProject Television/Templates|talk]] |-style="background: #F0DC82; color:black;" | [[:Category:Television stubs|Television stubs]] |} {|class="mw-collapsible mw-collapsed" style="width: 100%; padding: 3px; border-spacing: 0; border-collapse: collapse;" |-style="background: #DECCBF; color:black;" ! colspan="2" style="text-align: left; border-top:1px solid black;" | Guidelines |-style="background: #F5F5DC; color:black;" | [[Wikipedia:Manual of Style/Television|Project manual of style]] |style="text-align: right;" | [[Wikipedia talk:Manual of Style/Television|talk]] |-style="background: #F0DC82; color:black;" | [[Wikipedia:Notability (television)|Project notability guidelines]] |style="text-align: right;" | [[Wikipedia talk:Notability (television)|talk]] |-style="background: #F5F5DC; color:black;" | [[Wikipedia:Naming conventions (television)|TV article naming convention]] |style="text-align: right;" | [[Wikipedia talk:Naming conventions (television)|talk]] |-style="background: #F0DC82; color:black;" | [[Wikipedia:Naming conventions (broadcasting)|Broadcasting article naming convention]] |style="text-align: right;" | [[Wikipedia talk:Naming conventions (broadcasting)|talk]] |} {| class="mw-collapsible mw-collapsed" style="width: 100%; padding: 3px; border-spacing: 0; border-collapse: collapse;" |- style="text-align: center; background: #DECCBF; color:black;" ! colspan="2" style="text-align: left; border-top: 1px solid black; padding: 3px;" | Related WikiProjects |-style="background: #F0DC82; color:black;" | colspan="2" | [[Wikipedia:WikiProject Actors and Filmmakers|Actors and Filmmakers]] |-style="background: #F5F5DC; color:black;" | colspan="2" | [[Wikipedia:WikiProject Albums|Albums]] |-style="background: #F0DC82; color:black;" | colspan="2" | [[Wikipedia:WikiProject Animation|Animation]] |-style="background: #F5F5DC; color:black;" | colspan="2" | [[Wikipedia:WikiProject Anime and manga|Anime and manga]] |-style="background: #F0DC82; color:black;" | colspan="2" | [[Wikipedia:WikiProject Comics|Comics]] |-style="background: #F5F5DC; color:black;" | colspan="2" | [[Wikipedia:WikiProject Film|Film]] |-style="background: #F0DC82; color:black;" | colspan="2" | [[Wikipedia:WikiProject Literature|Literature]] |-style="background: #F5F5DC; color:black;" | colspan="2" | [[Wikipedia:WikiProject Media franchises|Media franchises]] |-style="background: #F0DC82; color:black;" | colspan="2" | [[Wikipedia:WikiProject Radio|Radio]] |-style="background: #F5F5DC; color:black;" | colspan="2" | [[Wikipedia:WikiProject Screenwriters|Screenwriters]] |-style="background: #F0DC82; color:black;" | colspan="2" | [[Wikipedia:WikiProject Westerns|Westerns]] |} {| style="width: 100%; padding: 3px; border-spacing: 0; border-collapse: collapse;" |- style="text-align: center; background-color: #DECCBF; color:black;" | colspan="2" style="border-top: 1px solid black;" | {{#tag:inputbox| bgcolor=transparent type=fulltext prefix=Wikipedia:WikiProject Television/ break=no width=20 searchbuttonlabel=Search }} {| style="width: 100%; padding: 3px; border-spacing: 0; border-collapse: collapse;" |- style="text-align: center; background: #DECCBF; color:black;" | colspan="2" style="border-top: 1px solid black;" | {{navbar|WikiProject Television navigation|plain=y}} |} |} |}<noinclude> [[Category:WikiProject Television| ]] [[Category:WikiProject Television templates| ]] </noinclude> b0no5c37yq32dv777d2nu7z5dauh2vy Talk:Mergetest1 1 176335 747091 2026-06-16T18:11:52Z Trialpears 43074 Old AFD: [[Wikipedia:Articles for deletion/Mergetest1]] closed as merge ([[WP:XFDC#4.0.16|XFDcloser]]) 747091 wikitext text/x-wiki {{Old AfD multi |date=16 June 2026 |result='''merge''' |page=Mergetest1}} qz23i5srbcgyqq5kwzyymjy6fhr3yfv 747105 747091 2026-06-16T18:22:17Z Trialpears 43074 Old AFD: [[Wikipedia:Articles for deletion/Mergetest1]] closed as merge ([[WP:XFDC#4.0.16|XFDcloser]]) 747105 wikitext text/x-wiki {{Old AfD multi |date=16 June 2026 |result='''merge''' |page=Mergetest1 |date2=16 June 2026 |result2='''Merge''' |page2=Mergetest1}} gm1ybdasx1pcuph69c5sey31at727wh 747109 747105 2026-06-16T18:23:27Z Trialpears 43074 Restored revision 747091 by [[Special:Contributions/Trialpears|Trialpears]] ([[User talk:Trialpears|talk]]) 747109 wikitext text/x-wiki {{Old AfD multi |date=16 June 2026 |result='''merge''' |page=Mergetest1}} qz23i5srbcgyqq5kwzyymjy6fhr3yfv 747117 747109 2026-06-16T18:26:14Z Trialpears 43074 Old AFD: [[Wikipedia:Articles for deletion/Mergetest1]] closed as merge ([[WP:XFDC#4.0.16|XFDcloser]]) 747117 wikitext text/x-wiki {{Old AfD multi |date=16 June 2026 |result='''merge''' |page=Mergetest1 |date2=16 June 2026 |result2='''Merge''' |page2=Mergetest1}} gm1ybdasx1pcuph69c5sey31at727wh 747123 747117 2026-06-16T18:28:11Z Trialpears 43074 Old AFD: [[Wikipedia:Articles for deletion/Mergetest1]] closed as merge ([[WP:XFDC#4.0.16|XFDcloser]]) 747123 wikitext text/x-wiki {{Old AfD multi |date=16 June 2026 |result='''merge''' |page=Mergetest1 |date2=16 June 2026 |result2='''Merge''' |page2=Mergetest1 |date3=16 June 2026 |result3='''Merge''' |page3=Mergetest1}} 1kiwc78p4f1t1dx9yy03eybn1q0pi3u 747127 747123 2026-06-16T18:34:08Z Trialpears 43074 Old AFD: [[Wikipedia:Articles for deletion/Mergetest1]] closed as merge ([[WP:XFDC#4.0.16|XFDcloser]]) 747127 wikitext text/x-wiki {{Old AfD multi |date=16 June 2026 |result='''merge''' |page=Mergetest1 |date2=16 June 2026 |result2='''Merge''' |page2=Mergetest1 |date3=16 June 2026 |result3='''Merge''' |page3=Mergetest1 |date4=16 June 2026 |result4='''Merge''' |page4=Mergetest1}} ki1kc4b459m8f1dhi7hrpod6gz3gssk Talk:Mergetest2 1 176336 747093 2026-06-16T18:11:52Z Trialpears 43074 [[Wikipedia:Articles for deletion/Mergetest1]] closed as merge ([[WP:XFDC#4.0.16|XFDcloser]]) 747093 wikitext text/x-wiki {{Afd-merge from|Mergetest1|discussion=Mergetest1|date=16 June 2026}} pndleacqbfv9qe49lghpzly5bfbq2ys 747106 747093 2026-06-16T18:22:18Z Trialpears 43074 [[Wikipedia:Articles for deletion/Mergetest1]] closed as merge ([[WP:XFDC#4.0.16|XFDcloser]]) 747106 wikitext text/x-wiki {{Afd-merge from|Mergetest1|discussion=Mergetest1|date=16 June 2026}} {{Afd-merge from|Mergetest1|discussion=Mergetest1|date=16 June 2026}} fgcgtzmldftukimpwpo1zq9k4xasw5n 747108 747106 2026-06-16T18:23:26Z Trialpears 43074 Restored revision 747093 by [[Special:Contributions/Trialpears|Trialpears]] ([[User talk:Trialpears|talk]]) 747108 wikitext text/x-wiki {{Afd-merge from|Mergetest1|discussion=Mergetest1|date=16 June 2026}} pndleacqbfv9qe49lghpzly5bfbq2ys 747119 747108 2026-06-16T18:26:14Z Trialpears 43074 [[Wikipedia:Articles for deletion/Mergetest1]] closed as merge ([[WP:XFDC#4.0.16|XFDcloser]]) 747119 wikitext text/x-wiki {{Afd-merge from|Mergetest1|discussion=Mergetest1|date=16 June 2026}} {{Afd-merge from|Mergetest1|discussion=Mergetest1|date=16 June 2026}} fgcgtzmldftukimpwpo1zq9k4xasw5n 747124 747119 2026-06-16T18:28:12Z Trialpears 43074 [[Wikipedia:Articles for deletion/Mergetest1]] closed as merge ([[WP:XFDC#4.0.16|XFDcloser]]) 747124 wikitext text/x-wiki {{Afd-merge from|Mergetest1|discussion=Mergetest1|date=16 June 2026}} {{Afd-merge from|Mergetest1|discussion=Mergetest1|date=16 June 2026}} {{Afd-merge from|Mergetest1|discussion=Mergetest1|date=16 June 2026}} hbg28g3rpy3uczidp475wdfcuu1xq6n 747128 747124 2026-06-16T18:34:08Z Trialpears 43074 [[Wikipedia:Articles for deletion/Mergetest1]] closed as merge ([[WP:XFDC#4.0.16|XFDcloser]]) 747128 wikitext text/x-wiki {{Afd-merge from|Mergetest1|discussion=Mergetest1|date=16 June 2026}} {{Afd-merge from|Mergetest1|discussion=Mergetest1|date=16 June 2026}} {{Afd-merge from|Mergetest1|discussion=Mergetest1|date=16 June 2026}} {{Afd-merge from|Mergetest1|discussion=Mergetest1|date=16 June 2026}} n8deg0wc9xum38qqa97rauow50mj6mp WET Awards 2004 0 176337 747096 2026-06-16T18:17:55Z ~2026-35344-03 74479 Created page with "{{Short description|American entertainment awards ceremony}} {{primary sources|date=July 2012}} {{Use mdy dates|date=February 2025}} {{Infobox award | name = 4th WET Awards | image = | caption = | date = June 29, 2004 | location = [[Dolby Theatre|Kodak Theatre]], [[Los Angeles]], [[California]] | presenter = [[WET (TV channel)|White Entertainment Television]] | host = [[Ellen DeGeneres]] | network = [[WET (TV channel)|WET]] | producer = | previous = [..." 747096 wikitext text/x-wiki {{Short description|American entertainment awards ceremony}} {{primary sources|date=July 2012}} {{Use mdy dates|date=February 2025}} {{Infobox award | name = 4th WET Awards | image = | caption = | date = June 29, 2004 | location = [[Dolby Theatre|Kodak Theatre]], [[Los Angeles]], [[California]] | presenter = [[WET (TV channel)|White Entertainment Television]] | host = [[Ellen DeGeneres]] | network = [[WET (TV channel)|WET]] | producer = | previous = [[WET Awards 2003|3rd]] | main = {{nowrap|[[WET Awards]]}} | next = [[WET Awards 2005|5th]] }} The '''4th [[WET Awards]]''' took place at the [[Dolby Theatre|Kodak Theatre]] in [[Los Angeles]], [[California]] on June 29, 2004. The awards recognized Americans in music, acting, sports, and other fields of entertainment over the past year. Comedienne [[Ellen DeGeneres]] hosted the event for the second time. ==Performances== ==Presenters== ==Awards and nominations== ==References== <references /> {{WET Awards}} [[Category:WET Awards]] [[Category:2004 awards in the United States]] f2r0m1rmmr4ff6o917lpcf379dg2sd3 WET Awards 2005 0 176338 747102 2026-06-16T18:22:02Z ~2026-35344-03 74479 Created page with "{{Short description|American entertainment awards ceremony}} {{Use mdy dates|date=February 2025}} {{Infobox award | name = 5th WET Awards | image = | caption = | date = June 28, 2005 | location = [[Dolby Theatre|Kodak Theatre]], [[Los Angeles]], [[California]] | presenter = [[WET (TV channel)|White Entertainment Television]] | host = [[Triple H]] <br /> [[Stephanie McMahon]] | network = [[WET (TV channel)|WET]] | producer = | previous = [[WET Awards 2004|4th]] | main = {..." 747102 wikitext text/x-wiki {{Short description|American entertainment awards ceremony}} {{Use mdy dates|date=February 2025}} {{Infobox award | name = 5th WET Awards | image = | caption = | date = June 28, 2005 | location = [[Dolby Theatre|Kodak Theatre]], [[Los Angeles]], [[California]] | presenter = [[WET (TV channel)|White Entertainment Television]] | host = [[Triple H]] <br /> [[Stephanie McMahon]] | network = [[WET (TV channel)|WET]] | producer = | previous = [[WET Awards 2004|4th]] | main = {{nowrap|[[WET Awards]]}} | next = [[WET Awards 2006|6th]] | most_wins=[[Papa Roach]] (1)|most_nomination=[[Shakira]] (3)}} The '''5th [[WET Awards]]''' took place at the [[Dolby Theatre|Kodak Theatre]] in [[Los Angeles]], [[California]] on June 28, 2005. The awards recognized Americans in music, acting, sports, and other fields of entertainment over the past year. Professional wrestling [[Triple H]] and [[Stephanie McMahon]] hosted the event for the first time. ==Presenters== ==Performances== ==Awards and nominations== ==References== <references /> {{WET Awards}} [[Category:WET Awards]] [[Category:2005 awards in the United States]] 801aba8zf5klhvq0d2z8gyp3hqyxhin 747107 747102 2026-06-16T18:23:25Z ~2026-35344-03 74479 747107 wikitext text/x-wiki {{Short description|American entertainment awards ceremony}} {{Use mdy dates|date=February 2025}} {{Infobox award | name = 5th WET Awards | image = | caption = | date = June 28, 2005 | location = [[Dolby Theatre|Kodak Theatre]], [[Los Angeles]], [[California]] | presenter = [[WET (TV channel)|White Entertainment Television]] | host = [[Triple H]] <br /> [[Stephanie McMahon]] | network = [[WET (TV channel)|WET]] | producer = | previous = [[WET Awards 2004|4th]] | main = {{nowrap|[[WET Awards]]}} | next = [[WET Awards 2006|6th]] | most_wins = [[Papa Roach]] (1) | most_nomination = [[Shakira]] (3) }} The '''5th [[WET Awards]]''' took place at the [[Dolby Theatre|Kodak Theatre]] in [[Los Angeles]], [[California]] on June 28, 2005. The awards recognized Americans in music, acting, sports, and other fields of entertainment over the past year. Professional wrestling [[Triple H]] and [[Stephanie McMahon]] hosted the event for the first time. ==Presenters== ==Performances== ==Awards and nominations== ==References== <references /> {{WET Awards}} [[Category:WET Awards]] [[Category:2005 awards in the United States]] 1ppxdi7an33lzsn8f13uxr1wkmisdve 747111 747107 2026-06-16T18:24:01Z ~2026-35344-03 74479 747111 wikitext text/x-wiki {{Short description|American entertainment awards ceremony}} {{Use mdy dates|date=February 2025}} {{Infobox award | name = 5th WET Awards | image = | caption = | date = June 28, 2005 | location = [[Dolby Theatre|Kodak Theatre]], [[Los Angeles]], [[California]] | presenter = [[WET (TV channel)|White Entertainment Television]] | host = [[Triple H]] <br /> [[Stephanie McMahon]] | network = [[WET (TV channel)|WET]] | producer = | previous = [[WET Awards 2004|4th]] | main = {{nowrap|[[WET Awards]]}} | next = [[WET Awards 2006|6th]] | most_wins = [[Papa Roach]] (1) | most_nominations = [[Shakira]] (3) }} The '''5th [[WET Awards]]''' took place at the [[Dolby Theatre|Kodak Theatre]] in [[Los Angeles]], [[California]] on June 28, 2005. The awards recognized Americans in music, acting, sports, and other fields of entertainment over the past year. Professional wrestling [[Triple H]] and [[Stephanie McMahon]] hosted the event for the first time. ==Presenters== ==Performances== ==Awards and nominations== ==References== <references /> {{WET Awards}} [[Category:WET Awards]] [[Category:2005 awards in the United States]] awrjc2i00z04zc0dhi0oht1tjv0j0qt Mergetest3 0 176339 747110 2026-06-16T18:23:57Z Trialpears 43074 Created page with "test" 747110 wikitext text/x-wiki test jrwjerxiekdtj9k82lg930wpkr6tq6r WET Awards 2006 0 176340 747120 2026-06-16T18:27:45Z ~2026-35344-03 74479 Created page with "{{Short description|American entertainment awards ceremony}} {{primary sources|date=July 2012}} {{Use mdy dates|date=February 2025}} {{Infobox award | name = 6th WET Awards | image = | caption = | date = June 27, 2006 | location = [[Shrine Auditorium]], [[Los Angeles]], [[California]] | presenter = [[WET (TV channel)|White Entertainment Television]] | host = [[Mick Foley]] | network = [[WET (TV channel)|WET]] | producer = | previous = WET Awards 2005|5..." 747120 wikitext text/x-wiki {{Short description|American entertainment awards ceremony}} {{primary sources|date=July 2012}} {{Use mdy dates|date=February 2025}} {{Infobox award | name = 6th WET Awards | image = | caption = | date = June 27, 2006 | location = [[Shrine Auditorium]], [[Los Angeles]], [[California]] | presenter = [[WET (TV channel)|White Entertainment Television]] | host = [[Mick Foley]] | network = [[WET (TV channel)|WET]] | producer = | previous = [[WET Awards 2005|5th]] | main = {{nowrap|[[WET Awards]]}} | next = [[WET Awards 2007|7th]] }} The '''6th [[WET Awards]]''' took place at the [[Shrine Auditorium]] in [[Los Angeles]], [[California]] on June 27, 2006. The awards recognized Americans in music, acting, sports, and other fields of entertainment over the past year. Professional wrestling [[Mick Foley]] hosted the event for the first time. ==Performers== ==Presenters== ==Nominees and winners== ==References== {{Reflist}} ==External links== {{WET Awards}} [[Category:WET Awards]] [[Category:2006 awards in the United States]] nhh934lxficprqrkxazzd8ancwycx70 WET Awards 2007 0 176341 747125 2026-06-16T18:31:44Z ~2026-35344-03 74479 Created page with "{{Short description|American entertainment awards ceremony}} {{Use mdy dates|date=February 2025}} {{Infobox award | name = 7th WET Awards | image = | caption = | date = June 26, 2007 | location = [[Shrine Auditorium]], [[Los Angeles]], [[California]] | presenter = [[WET (TV channel)|White Entertainment Television]] | host = [[Ellen DeGeneres]] | network = [[WET (TV channel)|WET]] | producer = | previous = [[WET Awards 2006|6th]] | main = {{nowrap|WET A..." 747125 wikitext text/x-wiki {{Short description|American entertainment awards ceremony}} {{Use mdy dates|date=February 2025}} {{Infobox award | name = 7th WET Awards | image = | caption = | date = June 26, 2007 | location = [[Shrine Auditorium]], [[Los Angeles]], [[California]] | presenter = [[WET (TV channel)|White Entertainment Television]] | host = [[Ellen DeGeneres]] | network = [[WET (TV channel)|WET]] | producer = | previous = [[WET Awards 2006|6th]] | main = {{nowrap|[[WET Awards]]}} | next = [[WET Awards 2008|8th]] }} The '''7th [[WET Awards]]''' took place at the [[Shrine Auditorium]], [[Los Angeles]], [[California]] on June 26, 2007. The awards recognized Americans in [[music]], [[acting]], [[sport]]s, and other fields of [[entertainment]] over the past year. Comedian [[Ellen DeGeneres]] hosted the awards for the third time. ==Performers== ==Presenters== ==Nominees and winners== ==References== {{Reflist}} {{WET Awards}} [[Category:WET Awards]] [[Category:2007 awards in the United States]] {{US-tv-stub}} 0gogbaevb4lkqzv7qldz0syhzsrun02 T420657 0 176342 747137 2026-06-16T20:47:32Z Jon (WMF) 21241 Created page with "[[File:Magpie_Goose_-_East_Point.jpg|none|thumb|Not using upright]] [[File:Magpie_Goose_-_East_Point.jpg|none|thumb|upright=0.2|Using upright 0.2]] [[File:Magpie_Goose_-_East_Point.jpg|none|thumb|upright=0.25|Using upright 0.25]] [[File:Magpie_Goose_-_East_Point.jpg|none|thumb|upright=0.5|Using upright 0.5]] [[File:Magpie_Goose_-_East_Point.jpg|none|thumb|upright=0.7|Using upright 0.7]] [[File:Magpie_Goose_-_East_Point.jpg|none|thumb|upright=1|Using upright 1]] File:Ma..." 747137 wikitext text/x-wiki [[File:Magpie_Goose_-_East_Point.jpg|none|thumb|Not using upright]] [[File:Magpie_Goose_-_East_Point.jpg|none|thumb|upright=0.2|Using upright 0.2]] [[File:Magpie_Goose_-_East_Point.jpg|none|thumb|upright=0.25|Using upright 0.25]] [[File:Magpie_Goose_-_East_Point.jpg|none|thumb|upright=0.5|Using upright 0.5]] [[File:Magpie_Goose_-_East_Point.jpg|none|thumb|upright=0.7|Using upright 0.7]] [[File:Magpie_Goose_-_East_Point.jpg|none|thumb|upright=1|Using upright 1]] [[File:Magpie_Goose_-_East_Point.jpg|none|thumb|upright=0.5|Using upright 0.2]] [[File:Magpie_Goose_-_East_Point.jpg|none|thumb|upright=1.5|Using upright 1.5]] [[File:Magpie_Goose_-_East_Point.jpg|none|thumb|upright=2|Using upright 2]] [[File:Magpie_Goose_-_East_Point.jpg|none|thumb|upright=10|Using upright 10]] sebs3y1zrja5n0q6tcabxgog57t1jq2 T68637 0 176343 747139 2026-06-16T21:27:37Z Cscott 23235 Test case for [[phab:T68637]] 747139 wikitext text/x-wiki ==Normal heading!== <h2>HTML heading!</h2> <h2 style="border: 1px solid green; background: lightgreen; padding: 5px; font-size: 1.5em;">HTML heading with attributes!</h2> <div class="mw-heading mw-heading2"> ==Normal heading with existing wrapper!== </div> npa8nonuieredoeqboo13eb4ieaor04 Talk:T68637 1 176344 747140 2026-06-16T21:27:54Z Cscott 23235 Test case for [[phab:T68637]] 747140 wikitext text/x-wiki ==Normal heading!== <h2>HTML heading!</h2> <h2 style="border: 1px solid green; background: lightgreen; padding: 5px; font-size: 1.5em;">HTML heading with attributes!</h2> <div class="mw-heading mw-heading2"> ==Normal heading with existing wrapper!== </div> npa8nonuieredoeqboo13eb4ieaor04 Talk:Laurent Levy 1 176345 747158 2026-06-17T09:40:16Z ~2026-35150-28 74487 /* Test */ new section 747158 wikitext text/x-wiki == Test == Test [[Special:Contributions/&#126;2026-35150-28|&#126;2026-35150-28]] ([[User talk:&#126;2026-35150-28|talk]]) 09:40, 17 June 2026 (UTC) 9ttyg8n1wnkw0227efwj7x6dddhbwy0