注意:在发布之后,您可能需要清除浏览器缓存才能看到所作出的更改的影响。
- Firefox或Safari:按住Shift的同时单击刷新,或按Ctrl-F5或Ctrl-R(Mac为⌘-R)
- Google Chrome:按Ctrl-Shift-R(Mac为⌘-Shift-R)
- Edge:按住Ctrl的同时单击刷新,或按Ctrl-F5。
/** * COMMENTS IN LOCAL TIME * * Description: * Changes [[Coordinated Universal Time|UTC]]-based times and dates, * such as those used in signatures, to be relative to local time. * * Documentation: * [[Wikipedia:Comments in Local Time]] */ $(() => { /** * Given a number, add a leading zero if necessary, so that the final number * has two characters. * * @param {number} number Number * @returns {string} The number with a leading zero, if necessary. */ function addLeadingZero(number) { const numberArg = number; if (numberArg < 10) { return `0${numberArg}`; } return numberArg; } function convertMonthToNumber(month) { return new Date(`${month} 1, 2001`).getMonth(); } function getDates(time) { const [, oldHour, oldMinute, oldDay, oldMonth, oldYear] = time; // Today const today = new Date(); // Yesterday const yesterday = new Date(); yesterday.setDate(yesterday.getDate() - 1); // Tomorrow const tomorrow = new Date(); tomorrow.setDate(tomorrow.getDate() + 1); // Set the date entered. const newTime = new Date(); newTime.setUTCFullYear(oldYear, convertMonthToNumber(oldMonth), oldDay); newTime.setUTCHours(oldHour); newTime.setUTCMinutes(oldMinute); return { time: newTime, today, tomorrow, yesterday }; } /** * Determine whether to use the singular or plural word, and use that. * * @param {string} term Original term * @param {number} count Count of items * @param {string} plural Pluralized term * @returns {string} The word to use */ function pluralize(term, count, plural = null) { let pluralArg = plural; // No unique pluralized word is found, so just use a general one. if (!pluralArg) { pluralArg = `${term}s`; } // There's only one item, so just use the singular word. if (count === 1) { return term; } // There are multiple items, so use the plural word. return pluralArg; } class CommentsInLocalTime { constructor() { this.language = ''; this.LocalComments = {}; /** * Settings */ this.settings(); this.language = this.setDefaultSetting( 'language', this.LocalComments.language ); // These values are also reflected in the documentation: // https://en.wikipedia.org/wiki/Wikipedia:Comments_in_Local_Time#Default_settings this.setDefaultSetting({ dateDifference: true, dateFormat: 'dmy', dayOfWeek: true, dropDays: 0, dropMonths: 0, timeFirst: true, twentyFourHours: false, }); } adjustTime(originalTimestamp, search) { const { time, today, tomorrow, yesterday } = getDates( originalTimestamp.match(search) ); // A string matching the date pattern was found, but it cannot be // converted to a Date object. Return it with no changes made. if (Number.isNaN(time)) { return [originalTimestamp, '']; } const date = this.determineDateText({ time, today, tomorrow, yesterday, }); const { ampm, hour } = this.getHour(time); const minute = addLeadingZero(time.getMinutes()); const finalTime = `${hour}:${minute}${ampm}`; let returnDate; // Determine the time offset. const utcValue = (-1 * time.getTimezoneOffset()) / 60; const utcOffset = utcValue >= 0 ? `+${utcValue}` : `−${Math.abs(utcValue.toFixed(1))}`; if (this.LocalComments.timeFirst) { returnDate = `${finalTime}, ${date} (UTC${utcOffset})`; } else { returnDate = `${date}, ${finalTime} (UTC${utcOffset})`; } return { returnDate, time }; } convertNumberToMonth(number) { return [ this.language.January, this.language.February, this.language.March, this.language.April, this.language.May, this.language.June, this.language.July, this.language.August, this.language.September, this.language.October, this.language.November, this.language.December, ][number]; } createDateText({ day, month, time, today, year }) { // calculate day of week const dayNames = [ this.language.Sunday, this.language.Monday, this.language.Tuesday, this.language.Wednesday, this.language.Thursday, this.language.Friday, this.language.Saturday, ]; const dayOfTheWeek = dayNames[time.getDay()]; let descriptiveDifference = ''; let last = ''; // Create a relative descriptive difference if (this.LocalComments.dateDifference) { ({ descriptiveDifference, last } = this.createRelativeDate( today, time )); } const monthName = this.convertNumberToMonth(time.getMonth()); // format the date according to user preferences let formattedDate = ''; switch (this.LocalComments.dateFormat.toLowerCase()) { case 'dmy': formattedDate = `${day} ${monthName} ${year}`; break; case 'mdy': formattedDate = `${monthName} ${day}, ${year}`; break; default: formattedDate = `${year}-${month}-${addLeadingZero(day)}`; } let formattedDayOfTheWeek = ''; if (this.LocalComments.dayOfWeek) { formattedDayOfTheWeek = `, ${last}${dayOfTheWeek}`; } return formattedDate + formattedDayOfTheWeek + descriptiveDifference; } /** * Create relative date data. * * @param {Date} today Today * @param {Date} time The timestamp from a comment * @returns {Object.<string, *>} Relative date data */ // eslint-disable-next-line max-statements createRelativeDate(today, time) { /** * The time difference from today, in milliseconds. * * @type {number} */ const millisecondsAgo = today.getTime() - time.getTime(); /** * The number of days ago, that we will display. It's not necessarily the * total days ago. * * @type {number} */ let daysAgo = Math.abs(Math.round(millisecondsAgo / 1000 / 60 / 60 / 24)); const { differenceWord, last } = this.relativeText({ daysAgo, millisecondsAgo, }); // This method of computing the years and months is not exact. However, // it's better than the previous method that used 1 January + delta days. // That was usually quite off because it mapped the second delta month to // February, which has only 28 days. This method is usually not more than // one day off, except perhaps over very distant dates. /** * The number of months ago, that we will display. It's not necessarily * the total months ago. * * @type {number} */ let monthsAgo = Math.floor((daysAgo / 365) * 12); /** * The total amount of time ago, in months. * * @type {number} */ const totalMonthsAgo = monthsAgo; /** * The number of years ago that we will display. It's not necessarily the * total years ago. * * @type {number} */ let yearsAgo = Math.floor(totalMonthsAgo / 12); if (totalMonthsAgo < this.LocalComments.dropMonths) { yearsAgo = 0; } else if (this.LocalComments.dropMonths > 0) { monthsAgo = 0; } else { monthsAgo -= yearsAgo * 12; } if (daysAgo < this.LocalComments.dropDays) { monthsAgo = 0; yearsAgo = 0; } else if (this.LocalComments.dropDays > 0 && totalMonthsAgo >= 1) { daysAgo = 0; } else { daysAgo -= Math.floor((totalMonthsAgo * 365) / 12); } const descriptiveParts = []; // There is years text to add. if (yearsAgo > 0) { descriptiveParts.push( `${yearsAgo} ${pluralize( this.language.year, yearsAgo, this.language.years )}` ); } // There is months text to add. if (monthsAgo > 0) { descriptiveParts.push( `${monthsAgo} ${pluralize( this.language.month, monthsAgo, this.language.months )}` ); } // There is days text to add. if (daysAgo > 0) { descriptiveParts.push( `${daysAgo} ${pluralize( this.language.day, daysAgo, this.language.days )}` ); } return { descriptiveDifference: ` (${descriptiveParts.join( ', ' )} ${differenceWord})`, last, }; } determineDateText({ time, today, tomorrow, yesterday }) { // Set the date bits to output. const year = time.getFullYear(); const month = addLeadingZero(time.getMonth() + 1); const day = time.getDate(); // return 'today' or 'yesterday' if that is the case if ( year === today.getFullYear() && month === addLeadingZero(today.getMonth() + 1) && day === today.getDate() ) { return this.language.Today; } if ( year === yesterday.getFullYear() && month === addLeadingZero(yesterday.getMonth() + 1) && day === yesterday.getDate() ) { return this.language.Yesterday; } if ( year === tomorrow.getFullYear() && month === addLeadingZero(tomorrow.getMonth() + 1) && day === tomorrow.getDate() ) { return this.language.Tomorrow; } return this.createDateText({ day, month, time, today, year }); } getHour(time) { let ampm; let hour = parseInt(time.getHours(), 10); if (this.LocalComments.twentyFourHours) { ampm = ''; hour = addLeadingZero(hour); } else { // Output am or pm depending on the date. ampm = hour <= 11 ? ' am' : ' pm'; if (hour > 12) { hour -= 12; } else if (hour === 0) { hour = 12; } } return { ampm, hour }; } relativeText({ daysAgo, millisecondsAgo }) { let differenceWord = ''; let last = ''; // The date is in the past. if (millisecondsAgo >= 0) { differenceWord = this.language.ago; if (daysAgo <= 7) { last = `${this.language.last} `; } // The date is in the future. } else { differenceWord = this.language['from now']; if (daysAgo <= 7) { last = `${this.language.this} `; } } return { differenceWord, last }; } // eslint-disable-next-line max-statements replaceText(node, search) { if (!node) { return; } // Check if this is a text node. if (node.nodeType === 3) { let parent = node.parentNode; const parentNodeName = parent.nodeName; if (['CODE', 'PRE'].includes(parentNodeName)) { return; } const value = node.nodeValue; const matches = value.match(search); // Stick with manipulating the DOM directly rather than using jQuery. // I've got more than a 100% speed improvement afterward. if (matches) { // Only act on the first timestamp we found in this node. This is // really a temporary fix for the situation in which there are two or // more timestamps in the same node. const [match] = matches; const position = value.search(search); const stringLength = match.toString().length; const beforeMatch = value.substring(0, position); const afterMatch = value.substring(position + stringLength); const { returnDate, time } = this.adjustTime( match.toString(), search ); const timestamp = time ? time.getTime() : ''; // Is the "timestamp" attribute used for microformats? const span = document.createElement('span'); span.className = 'localcomments'; span.style.fontSize = '95%'; span.style.whiteSpace = 'nowrap'; span.setAttribute('timestamp', timestamp); span.title = match; span.append(document.createTextNode(returnDate)); parent = node.parentNode; parent.replaceChild(span, node); const before = document.createElement('span'); before.className = 'before-localcomments'; before.append(document.createTextNode(beforeMatch)); const after = document.createElement('span'); after.className = 'after-localcomments'; after.append(document.createTextNode(afterMatch)); parent.insertBefore(before, span); parent.insertBefore(after, span.nextSibling); } } else { const children = []; let child; [child] = node.childNodes; while (child) { children.push(child); child = child.nextSibling; } // Loop through children and run this func on it again, recursively. children.forEach((child2) => { this.replaceText(child2, search); }); } } run() { if ( ['', 'MediaWiki', 'Special'].includes( mw.config.get('wgCanonicalNamespace') ) ) { return; } // Check for disabled URLs. const isDisabledUrl = ['action=history'].some((disabledUrl) => document.location.href.includes(disabledUrl) ); if (isDisabledUrl) { return; } this.replaceText( document.querySelector('.mw-parser-output'), /(\d{1,2}):(\d{2}), (\d{1,2}) ([A-Z][a-z]+) (\d{4}) \(UTC\)/ ); } setDefaultSetting(...args) { // There are no arguments. if (args.length === 0) { return false; } // The first arg is an object, so just set that data directly onto the // settings object. like {setting 1: true, setting 2: false} if (typeof args[0] === 'object') { const [settings] = args; // Loop through each setting. Object.keys(settings).forEach((name) => { const value = settings[name]; if (typeof this.LocalComments[name] === 'undefined') { this.LocalComments[name] = value; } }); return settings; } // The first arg is a string, so use the first arg as the settings key, // and the second arg as the value to set it to. const [name, setting] = args; if (typeof this.LocalComments[name] === 'undefined') { this.LocalComments[name] = setting; } return this.LocalComments[name]; } /** * Set the script's settings. * * @returns {undefined} */ settings() { // The user has set custom settings, so use those. if (window.LocalComments) { this.LocalComments = window.LocalComments; } /** * Language * * LOCALIZING THIS SCRIPT * To localize this script, change the terms below, * to the RIGHT of the colons, to the correct term used in that language. * * For example, in the French language, * * 'Today' : 'Today', * * would be * * 'Today' : "Aujourd'hui", */ this.LocalComments.language = { // Relative terms Today: 'Today', Yesterday: 'Yesterday', Tomorrow: 'Tomorrow', last: 'last', this: 'this', // Days of the week Sunday: 'Sunday', Monday: 'Monday', Tuesday: 'Tuesday', Wednesday: 'Wednesday', Thursday: 'Thursday', Friday: 'Friday', Saturday: 'Saturday', // Months of the year January: 'January', February: 'February', March: 'March', April: 'April', May: 'May', June: 'June', July: 'July', August: 'August', September: 'September', October: 'October', November: 'November', December: 'December', // Difference words ago: 'ago', 'from now': 'from now', // Date phrases year: 'year', years: 'years', month: 'month', months: 'months', day: 'day', days: 'days', }; } } // Check if we've already ran this script. if (window.commentsInLocalTimeWasRun) { return; } window.commentsInLocalTimeWasRun = true; const commentsInLocalTime = new CommentsInLocalTime(); commentsInLocalTime.run(); });