WordPress Reader Utilities

Common utilities for readers of WordPress-based sites.

  1. // ==UserScript==
  2. // @name WordPress Reader Utilities
  3. // @namespace https://github.com/BobbyWibowo
  4. // @version 1.0.5
  5. // @description Common utilities for readers of WordPress-based sites.
  6. // @author Bobby Wibowo
  7. // @license MIT
  8. // @match *://*/*
  9. // @icon https://www.google.com/s2/favicons?sz=64&domain=wordpress.org
  10. // @run-at document-end
  11. // @grant GM_addStyle
  12. // ==/UserScript==
  13.  
  14. (function () {
  15. 'use strict';
  16.  
  17. // Helper functions
  18.  
  19. const log = (msg, type = 'log') => console[type]('[WRU] ' + msg);
  20.  
  21. const getElement = (selectors = []) => {
  22. let element;
  23. let index;
  24. for (index = 0; index < selectors.length; index++) {
  25. element = document.querySelector(selectors[index]);
  26. if (element) break;
  27. }
  28. return { element, index };
  29. };
  30.  
  31. const capitalizeFirstLetter = string => {
  32. return string.charAt(0).toUpperCase() + string.slice(1);
  33. };
  34.  
  35. // Detect WordPress
  36.  
  37. const isWordPress = () => {
  38. const config = [
  39. {
  40. selector: 'meta[name="generator"]',
  41. test: element => /^WordPress/.test(element.getAttribute('content'))
  42. },
  43. { selector: '#footer .footer-wrap a[href*="wordpress.com"]' },
  44. { selector: '#footer2 a[href*="wordpress.org"]' },
  45. { selector: 'footer .site-info a[href*="wordpress.org"]' }
  46. ];
  47. const selectors = config.map(conf => conf.selector);
  48. const detect = getElement(selectors);
  49. if (detect.element) {
  50. if (typeof config[detect.index].test === 'function') { return config[detect.index].test(detect.element); }
  51. return true;
  52. }
  53. return false;
  54. };
  55.  
  56. if (!isWordPress()) return;
  57. log('Current page is a WordPress-based site.');
  58.  
  59. // Global style
  60.  
  61. let globalStyle = `
  62. #wcc-dates-container { text-align: center; margin-bottom: 25px }
  63. #wcc-toggle-container { text-align: center; margin-bottom: 25px }
  64. #wcc-toggle { width: 100% }
  65. `;
  66.  
  67. // Configurations
  68.  
  69. const dateLocale = undefined; // undefined => Browser's locale
  70. const dateOptions = {
  71. weekday: 'long',
  72. year: 'numeric',
  73. month: 'long',
  74. day: 'numeric',
  75. hour: 'numeric',
  76. minute: 'numeric',
  77. second: 'numeric',
  78. hour12: true
  79. };
  80.  
  81. // Add detailed published/modified dates at the top of post.
  82.  
  83. const getDates = () => {
  84. const tags = {
  85. 'article:published_time': 'published',
  86. 'article:modified_time': 'modified'
  87. };
  88. const results = {};
  89. const tagsKeys = Object.keys(tags);
  90. for (const key of tagsKeys) {
  91. const meta = document.head.querySelector('meta[property="' + key + '"]');
  92. if (!meta) continue;
  93. results[tags[key]] = new Date(meta.getAttribute('content'));
  94. }
  95. return results;
  96. };
  97.  
  98. const displayDates = () => {
  99. const dates = getDates();
  100. const datesKeys = Object.keys(dates);
  101. if (!datesKeys.length) { return log('Current page does not have date meta tags.'); }
  102.  
  103. const selectors = ['header.entry-header', 'h1.entry-title', 'h1.uk-article-title'];
  104. const title = getElement(selectors);
  105. if (!title.element) { return log('Current page does not have a title element.'); }
  106.  
  107. const datesContainer = document.createElement('div');
  108. datesContainer.id = 'wcc-dates-container';
  109. datesContainer.innerHTML = '';
  110. title.element.insertAdjacentElement('afterend', datesContainer);
  111. log('Dates container appended.');
  112.  
  113. for (const key of datesKeys) {
  114. const formattedDate = new Intl.DateTimeFormat(dateLocale, dateOptions).format(dates[key]);
  115. datesContainer.innerHTML += '<b>' + capitalizeFirstLetter(key) + ':</b> ' + formattedDate + '<br>';
  116. }
  117. };
  118.  
  119. displayDates();
  120.  
  121. // Add an expand/collapse button to comments container.
  122.  
  123. const appendToggler = () => {
  124. const selectors = [
  125. '.content-comments.container',
  126. '.comments-area',
  127. '#disqus_thread',
  128. '#fastcomments-widget'
  129. ];
  130.  
  131. const comments = getElement(selectors);
  132. if (!comments.element) { return log('Current page does not have a comments section.'); }
  133.  
  134. globalStyle += selectors[comments.index] + ':not([data-expanded="1"]) { height: 0; overflow: hidden }';
  135.  
  136. const commentHash = location.hash || '';
  137. const hasCommentHash = /^#comment(s|-\d+)$/.test(commentHash);
  138. if (hasCommentHash) {
  139. comments.element.dataset.expanded = '1';
  140. log('Comment hash detected, comments expanded.');
  141. }
  142.  
  143. const toggleContainer = document.createElement('div');
  144. toggleContainer.id = 'wcc-toggle-container';
  145. comments.element.insertAdjacentElement('beforebegin', toggleContainer);
  146. log('Toggle container appended.');
  147.  
  148. const toggle = document.createElement('button');
  149. toggle.id = 'wcc-toggle';
  150.  
  151. const updateBtn = () => {
  152. toggle.innerHTML = (comments.element.dataset.expanded ? 'Collapse' : 'Expand') + ' Comments';
  153. return toggle.innerHTML;
  154. };
  155. updateBtn();
  156.  
  157. toggle.addEventListener('click', () => {
  158. if (comments.element.dataset.expanded) delete comments.element.dataset.expanded;
  159. else comments.element.dataset.expanded = '1';
  160. updateBtn();
  161. });
  162.  
  163. toggleContainer.appendChild(toggle);
  164. log('Toggle appended.');
  165. };
  166. appendToggler();
  167.  
  168. // Add styling
  169.  
  170. GM_addStyle(globalStyle);
  171. log('Styling added.');
  172. })();