MeFi Navigator Redux

MetaFilter: navigate through users' comments, and highlight comments by OP and yourself

  1. // ==UserScript==
  2. // @name MeFi Navigator Redux
  3. // @namespace https://github.com/klipspringr/mefi-userscripts
  4. // @version 2025-04-10
  5. // @description MetaFilter: navigate through users' comments, and highlight comments by OP and yourself
  6. // @author Klipspringer
  7. // @supportURL https://github.com/klipspringr/mefi-userscripts
  8. // @license MIT
  9. // @match *://*.metafilter.com/*
  10. // @grant none
  11. // ==/UserScript==
  12.  
  13. const SVG_UP = `<svg xmlns="http://www.w3.org/2000/svg" hidden style="display:none"><path id="mfnr-up" fill="currentColor" d="M 0 93.339 L 50 6.661 L 100 93.339 L 50 64.399 L 0 93.339 Z" /></svg>`;
  14. const SVG_DOWN = `<svg xmlns="http://www.w3.org/2000/svg" hidden style="display:none"><path id="mfnr-down" fill="currentColor" d="M 100 6.69 L 50 93.31 L 0 6.69 L 50 35.607 L 100 6.69 Z" /></svg>`;
  15.  
  16. const ATTR_BYLINE = "data-mfnr-byline";
  17. const ATTR_NAVIGATOR = "data-mfnr-nav";
  18.  
  19. const getCookie = (key) => {
  20. const s = RegExp(key + "=([^;]+)").exec(document.cookie);
  21. if (!s || !s[1]) return null;
  22. return decodeURIComponent(s[1]);
  23. };
  24.  
  25. const markSelf = (targetNode) => {
  26. const span = document.createElement("span");
  27. span.style["margin-left"] = "4px";
  28. span.style["padding"] = "0 4px";
  29. span.style["border-radius"] = "2px";
  30. span.style["background-color"] = "#C8E0A1";
  31. span.style["color"] = "#000";
  32. span.style["font-size"] = "0.8em";
  33. span.textContent = "me";
  34. targetNode.after(span);
  35. };
  36.  
  37. const markOP = (targetNode) => {
  38. const wrapper = targetNode.parentNode.parentNode;
  39. wrapper.style["border-left"] = "5px solid #0004";
  40. wrapper.style["padding-left"] = "10px";
  41. };
  42.  
  43. const createLink = (href, svgHref) => {
  44. const a = document.createElement("a");
  45. a.setAttribute("href", "#" + href);
  46. const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
  47. svg.setAttribute("width", "1em");
  48. svg.setAttribute("viewBox", "0 0 100 100");
  49. svg.setAttribute("style", "vertical-align: middle; top: -1px;");
  50. const use = document.createElementNS("http://www.w3.org/2000/svg", "use");
  51. use.setAttribute("href", "#" + svgHref);
  52. svg.appendChild(use);
  53. a.appendChild(svg);
  54. return a;
  55. };
  56.  
  57. const processByline = (
  58. bylineNode,
  59. user,
  60. anchor,
  61. anchors,
  62. firstRun,
  63. self = null,
  64. op = null
  65. ) => {
  66. // don't mark self or OP more than once
  67. if (firstRun || !bylineNode.hasAttribute(ATTR_BYLINE)) {
  68. if (self !== null && user === self) markSelf(bylineNode);
  69. if (op !== null && user === op) markOP(bylineNode);
  70. bylineNode.setAttribute(ATTR_BYLINE, "");
  71. }
  72.  
  73. if (anchors.length <= 1) return;
  74.  
  75. const i = anchors.indexOf(anchor);
  76. const previous = anchors[i - 1];
  77. const next = anchors[i + 1];
  78.  
  79. const navigator = document.createElement("span");
  80. navigator.setAttribute(ATTR_NAVIGATOR, "");
  81.  
  82. const nodes = ["["];
  83. if (previous) nodes.push(createLink(previous, "mfnr-up"));
  84. nodes.push(anchors.length);
  85. if (next) nodes.push(createLink(next, "mfnr-down"));
  86. nodes.push("]");
  87.  
  88. navigator.append(...nodes);
  89. bylineNode.parentElement.appendChild(navigator);
  90. };
  91.  
  92. const run = (firstRun = false) => {
  93. const start = performance.now();
  94.  
  95. const subsite = window.location.hostname.split(".")[0];
  96. const opHighlight = subsite !== "ask" && subsite !== "projects"; // don't highlight OP on subsites with this built in
  97. const self = getCookie("USER_NAME");
  98.  
  99. // if not first run, remove any existing navigators (from both post and comments)
  100. if (!firstRun) {
  101. document
  102. .querySelectorAll(`span[${ATTR_NAVIGATOR}]`)
  103. .forEach((n) => n.remove());
  104. }
  105.  
  106. // post node
  107. // tested on all subsites, modern and classic, 2025-04-10
  108. const postNode = document.querySelector(
  109. "div.copy > span.smallcopy > a:first-child"
  110. );
  111. const op = postNode.textContent;
  112.  
  113. // initialise with post
  114. const bylines = [[op, "top"]];
  115. const mapUsersAnchors = new Map([[op, ["top"]]]);
  116.  
  117. // comment nodes, excluding live preview
  118. // tested on all subsites, modern and classic, 2025-04-10
  119. const commentNodes = document.querySelectorAll(
  120. "div.comments:not(#commentform *) > span.smallcopy > a:first-child"
  121. );
  122.  
  123. for (const node of commentNodes) {
  124. const user = node.textContent;
  125.  
  126. const anchorElement =
  127. node.parentElement.parentElement.previousElementSibling;
  128. const anchor = anchorElement.getAttribute("name");
  129.  
  130. bylines.push([user, anchor]);
  131.  
  132. const anchors = mapUsersAnchors.get(user) ?? [];
  133. mapUsersAnchors.set(user, anchors.concat(anchor));
  134. }
  135.  
  136. for (const [i, bylineNode] of [postNode, ...commentNodes].entries()) {
  137. processByline(
  138. bylineNode,
  139. bylines[i][0],
  140. bylines[i][1],
  141. mapUsersAnchors.get(bylines[i][0]),
  142. firstRun,
  143. self,
  144. opHighlight && i > 0 ? op : null
  145. );
  146. }
  147.  
  148. console.log(
  149. "mefi-navigator-redux",
  150. firstRun ? "first-run" : "new-comments",
  151. 1 + commentNodes.length,
  152. Math.round(performance.now() - start) + "ms"
  153. );
  154. };
  155.  
  156. (() => {
  157. if (
  158. !/^\/(\d|comments\.mefi)/.test(window.location.pathname) ||
  159. /rss$/.test(window.location.pathname)
  160. )
  161. return;
  162.  
  163. document.body.insertAdjacentHTML("beforeend", [SVG_UP, SVG_DOWN].join(""));
  164.  
  165. const newCommentsElement = document.getElementById("newcomments");
  166. if (newCommentsElement) {
  167. const observer = new MutationObserver(() => run(false));
  168. observer.observe(newCommentsElement, { childList: true });
  169. }
  170.  
  171. run(true);
  172. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址