Jump to content
Wikimedia Meta-Wiki

User:Svartava/CommentsInLocalTime.js

From Meta, a Wikimedia project coordination wiki

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
 // Copied from [[w:User:Gary/comments in local time.js]] with minor changes
 /**
  * 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.
  */
 functionaddLeadingZero(number){
 constnumberArg=number;

 if(numberArg<10){
 return`0${numberArg}`;
 }

 returnnumberArg;
 }

 functionconvertMonthToNumber(month){
 returnnewDate(`${month} 1, 2001`).getMonth();
 }

 functiongetDates(time){
 const[,oldHour,oldMinute,oldDay,oldMonth,oldYear]=time;

 // Today
 consttoday=newDate();

 // Yesterday
 constyesterday=newDate();

 yesterday.setDate(yesterday.getDate()-1);

 // Tomorrow
 consttomorrow=newDate();

 tomorrow.setDate(tomorrow.getDate()+1);

 // Set the date entered.
 constnewTime=newDate();

 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
  */
 functionpluralize(term,count,plural=null){
 letpluralArg=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){
 returnterm;
 }

 // There are multiple items, so use the plural word.
 returnpluralArg;
 }

 classCommentsInLocalTime{
 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,''];
 }

 constdate=this.determineDateText({
 time,
 today,
 tomorrow,
 yesterday,
 });

 const{ampm,hour}=this.getHour(time);
 constminute=addLeadingZero(time.getMinutes());
 constfinalTime=`${hour}:${minute}${ampm}`;

 // Determine the time offset.
 constutcValue=(-1*time.getTimezoneOffset())/60;
 constutcOffset=
 utcValue>=0?`+${utcValue}`:`−${Math.abs(utcValue.toFixed(1))}`;

 constutcPart=`(UTC${utcOffset})`;

 constreturnDate=this.LocalComments.timeFirst
 ?`${finalTime}, ${date}${utcPart}`
 :`${date}, ${finalTime}${utcPart}`;

 returnreturnDate;
 }

 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
 constdayNames=[
 this.language.Sunday,
 this.language.Monday,
 this.language.Tuesday,
 this.language.Wednesday,
 this.language.Thursday,
 this.language.Friday,
 this.language.Saturday,
 ];
 constdayOfTheWeek=dayNames[time.getDay()];
 letdescriptiveDifference='';
 letlast='';

 // Create a relative descriptive difference
 if(this.LocalComments.dateDifference){
 ({descriptiveDifference,last}=this.createRelativeDate(
 today,
 time
 ));
 }

 constmonthName=this.convertNumberToMonth(time.getMonth());

 // Format the date according to user preferences
 letformattedDate='';

 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)}`;
 }

 constformattedDayOfTheWeek=this.LocalComments.dayOfWeek
 ?`, ${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
  */
 createRelativeDate(today,time){
 /**
  * The time difference from today, in milliseconds.
  *
  * @type {number}
  */
 constmillisecondsAgo=today.getTime()-time.getTime();

 /**
  * The number of days ago, that we will display. It's not necessarily the
  * total days ago.
  *
  * @type {number}
  */
 letdaysAgo=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}
  */
 letmonthsAgo=Math.floor((daysAgo/365)*12);

 /**
  * The total amount of time ago, in months.
  *
  * @type {number}
  */
 consttotalMonthsAgo=monthsAgo;

 /**
  * The number of years ago that we will display. It's not necessarily the
  * total years ago.
  *
  * @type {number}
  */
 letyearsAgo=Math.floor(totalMonthsAgo/12);

 if(totalMonthsAgo<this.LocalComments.dropMonths){
 yearsAgo=0;
 }elseif(this.LocalComments.dropMonths>0){
 monthsAgo=0;
 }else{
 monthsAgo-=yearsAgo*12;
 }

 if(daysAgo<this.LocalComments.dropDays){
 monthsAgo=0;
 yearsAgo=0;
 }elseif(this.LocalComments.dropDays>0&&totalMonthsAgo>=1){
 daysAgo=0;
 }else{
 daysAgo-=Math.floor((totalMonthsAgo*365)/12);
 }

 constdescriptiveParts=[];

 // 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.
 constyear=time.getFullYear();
 constmonth=addLeadingZero(time.getMonth()+1);
 constday=time.getDate();

 // Return 'today' or 'yesterday' if that is the case
 if(
 year===today.getFullYear()&&
 month===addLeadingZero(today.getMonth()+1)&&
 day===today.getDate()
 ){
 returnthis.language.Today;
 }

 if(
 year===yesterday.getFullYear()&&
 month===addLeadingZero(yesterday.getMonth()+1)&&
 day===yesterday.getDate()
 ){
 returnthis.language.Yesterday;
 }

 if(
 year===tomorrow.getFullYear()&&
 month===addLeadingZero(tomorrow.getMonth()+1)&&
 day===tomorrow.getDate()
 ){
 returnthis.language.Tomorrow;
 }

 returnthis.createDateText({day,month,time,today,year});
 }

 getHour(time){
 letampm;
 lethour=Number.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;
 }elseif(hour===0){
 hour=12;
 }
 }

 return{ampm,hour};
 }

 relativeText({daysAgo,millisecondsAgo}){
 letdifferenceWord='';
 letlast='';

 // 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};
 }

 replaceText(node,search){
 if(!node){
 return;
 }

 // Check if this is a text node.
 if(node.nodeType===3){
 // Don't continue if this text node's parent tag is one of these.
 if(['CODE','PRE'].includes(node.parentNode.nodeName)){
 return;
 }

 constvalue=node.nodeValue;
 constmatches=value.match(search);

 if(matches){
 // Only act on the first timestamp we found in this node. This is for
 // the rare occassion that there is more than one timestamp in the
 // same text node.
 const[match]=matches;
 constposition=value.search(search);
 conststringLength=match.toString().length;

 // Grab the text content before and after the matching timestamp,
 // which we'll then wrap in their own SPAN nodes.
 constbeforeMatch=value.slice(0,position);
 constafterMatch=value.slice(position+stringLength);
 constreturnDate=this.adjustTime(match.toString(),search);

 // Create the code to display the new local comments content.
 const$span=$(
 `<span class="localcomments" style="font-size: 95%;" title="${match}">${returnDate}</span>`
 );

 // Replace the existing text node in the page with our new local
 // comments node.
 $(node).replaceWith($span);

 // Replace the text content that appears before the timestamp.
 if(beforeMatch){
 $span.before(
 `<span class="before-localcomments">${beforeMatch}</span>`
 );
 }

 // Replace the text content that appears after the timestamp.
 if(afterMatch){
 $span.after(
 `<span class="after-localcomments">${afterMatch}</span>`
 );
 }
 }
 }else{
 constchildren=[];
 letchild;

 [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.
 constisDisabledUrl=['action=history'].some((disabledUrl)=>
 document.location.href.includes(disabledUrl)
 );

 if(isDisabledUrl){
 return;
 }

 this.replaceText(
 document.querySelector('.mw-body-content .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){
 returnfalse;
 }

 // The first arg is an object, so just set that data directly onto the
 // settings object. like {setting 1: true, setting 2: false}
 if(typeofargs[0]==='object'){
 const[settings]=args;

 // Loop through each setting.
 Object.keys(settings).forEach((name)=>{
 constvalue=settings[name];

 if(typeofthis.LocalComments[name]==='undefined'){
 this.LocalComments[name]=value;
 }
 });

 returnsettings;
 }

 // 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(typeofthis.LocalComments[name]==='undefined'){
 this.LocalComments[name]=setting;
 }

 returnthis.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;

 constcommentsInLocalTime=newCommentsInLocalTime();

 commentsInLocalTime.run();
 });

AltStyle によって変換されたページ (->オリジナル) /