/**
* Module that serves as an entry point.
*
* @module app
*/
import defaultConfig from '../config/default';
import configUrls from '../config/urls.json';
import i18nList from '../data/i18nList.json';
import languageFallbacks from '../data/languageFallbacks.json';
import { addCommentLinksToSpecialSearch } from './addCommentLinks';
import cd from './cd';
import controller from './controller';
import debug from './debug';
import { mergeRegexps, unique } from './utils-general.js';
import { getFooter } from './utils-window.js';
let config;
if (IS_SINGLE) {
try {
config = require(`../config/${CONFIG_FILE_NAME}`).default;
} catch {
// Empty
}
// A copy of the function in misc/utils.js. If altering it, make sure they are synchronized.
const replaceEntities = (string) => (
string
.replace(/ /g, '\xa0')
.replace(/ /g, ' ')
.replace(/‏/g, '\u200f')
.replace(/‎/g, '\u200e')
);
cd.i18n = {};
cd.i18n.en = require('../i18n/en.json');
Object.keys(cd.i18n.en).forEach((name) => {
cd.i18n.en[name] = replaceEntities(cd.i18n.en[name]);
});
if (LANG_CODE !== 'en') {
cd.i18n[LANG_CODE] = require(`../i18n/${LANG_CODE}.json`);
const langObj = cd.i18n[LANG_CODE];
Object.keys(cd.i18n[LANG_CODE])
.filter((name) => typeof langObj[name] === 'string')
.forEach((name) => {
langObj[name] = replaceEntities(langObj[name]);
});
langObj.dayjsLocale = require(`dayjs/locale/${LANG_CODE}`);
langObj.dateFnsLocale = require(`date-fns/locale`)[LANG_CODE];
}
}
/**
* Add the script's strings to `mw.messages`.
*
* @private
*/
function setStrings() {
// Strings that should be displayed in the site language, not the user language.
const contentStrings = [
'es-',
'cf-autocomplete-commentlinktext',
'move-',
];
if (!IS_SINGLE) {
require('../dist/convenientDiscussions-i18n/en.js');
}
const strings = {};
Object.keys(cd.i18n.en).forEach((name) => {
const relevantLang = contentStrings.some((contentStringName) => (
name === contentStringName ||
(contentStringName.endsWith('-') && name.startsWith(contentStringName))
)) ?
cd.g.contentLanguage :
cd.g.userLanguage;
strings[name] = cd.i18n[relevantLang]?.[name] || cd.i18n.en[name];
});
Object.keys(strings).forEach((name) => {
mw.messages.set(`convenient-discussions-${name}`, strings[name]);
});
}
/**
* Add a footer link to enable/disable CD on this page once.
*
* @private
*/
function maybeAddFooterSwitcher() {
if (!mw.config.get('wgIsArticle')) return;
const enable = !controller.isTalkPage();
const url = new URL(location.href);
url.searchParams.set('cdtalkpage', enable ? '1' : '0');
const $li = $('<li>').attr('id', 'footer-togglecd');
const $a = $('<a>')
.attr('href', url.toString())
.addClass('noprint')
.text(cd.s(enable ? 'footer-runcd' : 'footer-dontruncd'))
.appendTo($li);
if (enable) {
$a.on('click', (e) => {
if (e.ctrlKey || e.shiftKey || e.metaKey) return;
e.preventDefault();
history.pushState(history.state, '', url.toString());
$li.remove();
go();
});
}
getFooter().append($li);
}
/**
* Change the destination of the "Add topic" button to redirect topic creation to the script's form.
* This is not done on `action=view` pages to make sure the user can open the classic form in a new
* tab. The exception is when the new topic tool is enabled with the "Offer to add a new topic"
* setting: in that case, the classic form doesn't open anyway. So we add `dtenable=0` to the
* button.
*
* @private
*/
function maybeTweakAddTopicButton() {
const dtCreatePage = (
cd.g.isDtNewTopicToolEnabled &&
mw.user.options.get('discussiontools-newtopictool-createpage')
);
if (!controller.isArticlePageTalkPage() || cd.g.pageAction === 'view' && !dtCreatePage) return;
const $button = $('#ca-addsection a');
const href = $button.prop('href');
if (href) {
const url = new URL(href);
if (dtCreatePage) {
url.searchParams.set('dtenable', 0);
}
if (!dtCreatePage || cd.g.pageAction !== 'view') {
url.searchParams.delete('action');
url.searchParams.delete('section');
url.searchParams.set('cdaddtopic', 1);
}
$button.attr('href', url);
}
}
/**
* Function executed after the config and localization strings are ready.
*
* @fires preprocessed
* @private
*/
async function go() {
debug.startTimer('start');
require('./convenientDiscussions');
// Don't run again if go() runs the second time (see maybeAddFooterSwitcher()).
if (cd.g.pageWhitelistRegexp === undefined) {
/**
* Script configuration. The default configuration is in {@link module:defaultConfig}.
*
* @name config
* @type {object}
* @memberof convenientDiscussions
*/
cd.config = Object.assign(defaultConfig, cd.config);
cd.g.pageWhitelistRegexp = mergeRegexps(cd.config.pageWhitelist);
cd.g.pageBlacklistRegexp = mergeRegexps(cd.config.pageBlacklist);
setStrings();
}
controller.init();
maybeAddFooterSwitcher();
maybeTweakAddTopicButton();
addCommentLinksToSpecialSearch();
if (!controller.isBooting()) {
debug.stopTimer('start');
}
/**
* The page has been preprocessed (not parsed yet, but its type has been checked and some
* important mechanisms have been initialized).
*
* @event preprocessed
* @param {object} cd {@link convenientDiscussions} object.
* @global
*/
mw.hook('convenientDiscussions.preprocessed').fire(cd);
}
/**
* Set language properties of the global object, taking fallback languages into account.
*
* @returns {boolean} Are fallbacks employed.
* @private
*/
function setLanguages() {
const languageOrFallback = (lang) => (
i18nList.includes(lang) ?
lang :
(languageFallbacks[lang] || []).find((fallback) => i18nList.includes(fallback)) || 'en'
);
cd.g.userLanguage = languageOrFallback(mw.config.get('wgUserLanguage'));
// Should we use a fallback for the content language? Maybe, but in case of MediaWiki messages
// used for signature parsing we have to use the real content language (see init.loadSiteData()).
// As a result, we use cd.g.contentLanguage only for the script's own messages, not the native
// MediaWiki messages.
cd.g.contentLanguage = languageOrFallback(mw.config.get('wgContentLanguage'));
return !(
cd.g.userLanguage === mw.config.get('wgUserLanguage') &&
cd.g.contentLanguage === mw.config.get('wgContentLanguage')
);
}
/**
* Load and execute the configuration script if available.
*
* @returns {Promise.<undefined>}
* @private
*/
function getConfig() {
return new Promise((resolve, reject) => {
let key = mw.config.get('wgServerName');
if (IS_TEST) {
key += '.test';
}
const configUrl = configUrls[key] || configUrls[mw.config.get('wgServerName')];
if (configUrl) {
const rejectWithMsg = (e) => {
reject(['Convenient Discussions can\'t run: couldn\'t load the configuration.', e]);
};
const [, gadgetName] = configUrl.match(/modules=ext.gadget.([^?&]+)/) || [];
if (gadgetName && mw.user.options.get(`gadget-${gadgetName}`)) {
// A gadget is enabled on the wiki, and it should be loaded and executed without any
// additional requests; we just wait until it happens.
mw.loader.using(`ext.gadget.${gadgetName}`).then(() => {
resolve();
});
return;
}
mw.loader.getScript(configUrl).then(
() => {
resolve();
},
rejectWithMsg
);
} else {
resolve();
}
});
}
/**
* Load and add localization strings to the `cd.i18n` object. Use fallback languages if default
* languages are unavailable.
*
* @returns {Promise.<undefined>}
* @private
*/
function getStrings() {
const requests = [cd.g.userLanguage, cd.g.contentLanguage]
.filter(unique)
.filter((lang) => lang !== 'en' && !cd.i18n?.[lang])
.map((lang) => {
const url = `https://commons.wikimedia.org/w/index.php?title=User:Jack_who_built_the_house/convenientDiscussions-i18n/${lang}.js&action=raw&ctype=text/javascript`;
return mw.loader.getScript(url);
});
// We assume it's OK to fall back to English if the translation is unavailable for any reason.
return Promise.all(requests).catch(() => {});
}
/**
* The main script function.
*
* @fires launched
* @private
*/
async function app() {
if (cd.isRunning) {
console.warn('One instance of Convenient Discussions is already running.');
return;
}
/**
* Is the script running.
*
* @name isRunning
* @type {boolean}
* @memberof convenientDiscussions
*/
cd.isRunning = true;
if (
/(^|\.)m\./.test(location.hostname) ||
/[?&]cdenable=(0|false|no|n)(?=&|$)/.test(location.search) ||
mw.config.get('wgPageContentModel') !== 'wikitext' ||
// Liquid Threads, for example https://en.wiktionary.org/wiki/User_talk:Yair_rand/newentrywiz.js
$('.lqt-talkpage').length ||
mw.config.get('wgIsMainPage')
) {
return;
}
if (IS_SINGLE) {
cd.config = config;
}
cd.g = {};
debug.init();
debug.startTimer('total time');
debug.startTimer('load config and strings');
/**
* The script has launched.
*
* @event launched
* @param {object} cd {@link convenientDiscussions} object.
* @global
*/
mw.hook('convenientDiscussions.launched').fire(cd);
const areLanguageFallbacksEmployed = setLanguages();
const getStringsPromise = areLanguageFallbacksEmployed ?
getStrings() :
// cd.getStringsPromise may be set in the configuration file.
!cd.i18n && (cd.getStringsPromise || getStrings());
try {
await Promise.all([!cd.config && getConfig(), getStringsPromise]);
} catch (e) {
console.error(e);
return;
}
debug.stopTimer('load config and strings');
$(go);
}
app();