Greasy Fork 还支持 简体中文。

AO3 Word Count Script

Adds word counts to chapter links on AO3 Chapter Index pages and in Stats on each chapter page.

  1. // ==UserScript==
  2. // @name AO3 Word Count Script
  3. // @namespace ao3chapterwordcounter
  4. // @version 4.1
  5. // @description Adds word counts to chapter links on AO3 Chapter Index pages and in Stats on each chapter page.
  6. // @author Anton Dumov
  7. // @license MIT
  8. // @match https://archiveofourown.org/*/navigate
  9. // @match https://archiveofourown.org/*/chapters/*
  10. // @grant none
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. const uri = location.protocol+'//'+
  17. location.hostname+
  18. (location.port?":"+location.port:"")+
  19. location.pathname+
  20. (location.search?location.search:"");
  21. const wordCountRegex = /\s+/g;
  22. const chapterUrlRegex = new RegExp("https://archiveofourown\\.org/works/\\d+/chapters/\\d+/?");
  23. const cacheKeyPrefix = "ao3-word-count-cache-";
  24. const cacheDurationMs = 30 * 24 * 60 * 60 * 1000;
  25.  
  26. const getCachedWordCount = link => {
  27. const cacheKey = cacheKeyPrefix + link.href;
  28. const cachedValue = localStorage.getItem(cacheKey);
  29. if (cachedValue) {
  30. const { timestamp, wordCount } = JSON.parse(cachedValue);
  31. if (Date.now() - timestamp < cacheDurationMs && wordCount !== 0) {
  32. return wordCount;
  33. } else {
  34. localStorage.removeItem(cacheKey);
  35. }
  36. }
  37. return null;
  38. };
  39.  
  40. const setCachedWordCount = (url, wordCount) => {
  41. const cacheKey = cacheKeyPrefix + url;
  42. const cacheValue = JSON.stringify({ timestamp: Date.now(), wordCount });
  43. localStorage.setItem(cacheKey, cacheValue);
  44. };
  45.  
  46. let fetchInProgress = false;
  47.  
  48. const countWords = (doc) => {
  49. const article = doc.querySelector("div[role=article]");
  50. return article ? article.textContent.trim().split(wordCountRegex).length : 0;
  51. };
  52.  
  53. const fetchWordCount = async (url) => {
  54. try {
  55. if (fetchInProgress) {
  56. // Wait for the previous request to complete
  57. await new Promise(resolve => {
  58. const interval = setInterval(() => {
  59. if (!fetchInProgress) {
  60. clearInterval(interval);
  61. resolve();
  62. }
  63. }, 2000);
  64. });
  65. }
  66. fetchInProgress = true;
  67.  
  68. const response = await fetch(url);
  69. const text = await response.text();
  70. const parser = new DOMParser();
  71. const doc = parser.parseFromString(text, "text/html");
  72. const wordCount = countWords(doc);
  73. setCachedWordCount(url, wordCount);
  74. fetchInProgress = false;
  75. return wordCount;
  76. } catch (error) {
  77. console.log(error);
  78. fetchInProgress = false;
  79. }
  80. };
  81.  
  82. const getWordCount = async (link, maxWidth, longTitles) => {
  83. const cachedWordCount = getCachedWordCount(link);
  84. let wordCount;
  85. if (cachedWordCount) {
  86. wordCount = cachedWordCount;
  87. } else {
  88. wordCount = await fetchWordCount(link.href);
  89. }
  90. const wordCountElement = document.createElement("span");
  91. wordCountElement.textContent = `(${wordCount} words)`;
  92. if (!longTitles){
  93. const spanElement = link.parentElement.querySelector('span.datetime');
  94. const margin = maxWidth - link.getBoundingClientRect().width + 7;
  95. wordCountElement.style.marginLeft = `${margin}px`;
  96. spanElement.parentNode.insertBefore(wordCountElement, spanElement.nextSibling);
  97. } else {
  98. link.parentNode.insertBefore(wordCountElement, link);
  99. link.parentElement.style.paddingLeft = `7.5em`;
  100. wordCountElement.style.position = 'absolute';
  101. wordCountElement.style.left = '0';
  102. }
  103. };
  104.  
  105. if (uri.endsWith("navigate")){
  106. const chapterLinks = document.querySelectorAll("ol.chapter.index.group li a");
  107.  
  108. const parentWidth = chapterLinks[0].parentElement.getBoundingClientRect().width;
  109. let maxWidth = 0;
  110. let longTitles = false;
  111.  
  112. chapterLinks.forEach(link => {
  113. const width = link.getBoundingClientRect().width;
  114. if (width > maxWidth) {
  115. maxWidth = width;
  116. }
  117. if (width + 175 >= parentWidth) {
  118. longTitles = true;
  119. }
  120. });
  121.  
  122. chapterLinks.forEach(link => {
  123. getWordCount(link, maxWidth, longTitles);
  124. });
  125. } else if (chapterUrlRegex.test(uri)) {
  126. const wordsCount = countWords(document);
  127. const statsElement = document.querySelector('dl.stats');
  128. const ddElement = document.createElement('dd');
  129. ddElement.classList.add('chapter-words');
  130. ddElement.textContent = wordsCount;
  131. const dtElement = document.createElement('dt');
  132. dtElement.classList.add('chapter-words');
  133. dtElement.textContent = 'Chapter Words:';
  134. statsElement.appendChild(dtElement);
  135. statsElement.appendChild(ddElement);
  136. setCachedWordCount(uri, wordsCount);
  137. }
  138. })();