Guest User

HackerNews JS

a guest
Nov 3rd, 2026
11
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. (function () {
  2.     'use strict';
  3.  
  4.     /* ==========================================================
  5.    HACKER NEWS COMMENT ENHANCER
  6.    ----------------------------------------------------------
  7.    This script improves Hacker News comment readability and
  8.    structure by performing several transformations:
  9.  
  10.    Processing pipeline:
  11.  
  12.    1. Identify the OP in comment threads
  13.    2. Convert "> quote" text into styled blockquotes
  14.    3. Convert "*" bullet lines into diamond bullets
  15.    4. Normalize header navigation markup
  16.    5. Apply syntax highlighting to code blocks
  17.    6. Enhance code blocks with line numbers
  18.    7. Apply typographic improvements (Typeset.js)
  19.  
  20.    Each step is isolated so features can be added or removed
  21.    without affecting the rest of the pipeline.
  22.    ========================================================== */
  23.  
  24.     /* ==========================================================
  25.    1. OP COMMENT INDICATOR
  26.    ----------------------------------------------------------
  27.    Hacker News does not visually distinguish comments made
  28.    by the original poster (OP).
  29.  
  30.    This function finds the OP username from the story header
  31.    and adds a small "OP" badge next to any comment written
  32.    by that same user.
  33.    ========================================================== */
  34.  
  35.     function addOPIndicators() {
  36.         const opUser = document.querySelector('.fatitem .hnuser');
  37.         if (!opUser) return;
  38.  
  39.         const opName = opUser.textContent.trim();
  40.  
  41.         document.querySelectorAll('tr.athing.comtr').forEach((row) => {
  42.             const user = row.querySelector('.hnuser');
  43.             if (!user) return;
  44.  
  45.             if (user.textContent.trim() === opName) {
  46.                 // Prevent duplicate badges if script runs twice
  47.                 if (user.nextElementSibling?.classList?.contains('hn-op-badge')) return;
  48.  
  49.                 const badge = document.createElement('span');
  50.  
  51.                 badge.className = 'hn-op-badge';
  52.                 badge.textContent = ' OP';
  53.  
  54.                 badge.style.fontSize = '11px';
  55.                 badge.style.marginLeft = '6px';
  56.                 badge.style.padding = '2px 5px';
  57.                 badge.style.borderRadius = '4px';
  58.                 badge.style.background = 'rgba(90,169,255,0.15)';
  59.                 badge.style.color = '#5aa9ff';
  60.  
  61.                 user.after(badge);
  62.             }
  63.         });
  64.     }
  65.  
  66.     /* ==========================================================
  67.    2. CONVERT TEXT QUOTES → BLOCKQUOTES
  68.    ----------------------------------------------------------
  69.    Hacker News users often write quotes using:
  70.  
  71.       > quoted text
  72.  
  73.    This function detects those patterns and converts them
  74.    into actual <blockquote> elements so they can be styled
  75.    properly with CSS.
  76.    ========================================================== */
  77.  
  78.     function convertQuotes() {
  79.         document.querySelectorAll('.commtext').forEach((comm) => {
  80.             if (comm.dataset.hnQuotesProcessed) return;
  81.             comm.dataset.hnQuotesProcessed = 'true';
  82.  
  83.             // TreeWalker allows scanning only text nodes
  84.             const walker = document.createTreeWalker(comm, NodeFilter.SHOW_TEXT);
  85.  
  86.             const toReplace = [];
  87.  
  88.             while (walker.nextNode()) {
  89.                 const node = walker.currentNode;
  90.                 const trimmed = node.nodeValue.trim();
  91.  
  92.                 if (trimmed.startsWith('> ')) {
  93.                     toReplace.push(node);
  94.                 }
  95.             }
  96.  
  97.             toReplace.forEach((node) => {
  98.                 const text = node.nodeValue.trim().replace(/^>\s?/, '');
  99.  
  100.                 const blockquote = document.createElement('blockquote');
  101.                 blockquote.textContent = text;
  102.  
  103.                 node.parentNode.replaceChild(blockquote, node);
  104.             });
  105.         });
  106.     }
  107.  
  108.     /* ==========================================================
  109.    3. NORMALIZE HEADER NAV
  110.    ----------------------------------------------------------
  111.    Hacker News wraps the active navigation link inside <b>.
  112.    That breaks modern flex layouts.
  113.  
  114.    This function unwraps the <b> tag so navigation becomes
  115.    structurally consistent and easier to style.
  116.    ========================================================== */
  117.  
  118.     function normalizeHeaderNav() {
  119.         const nav = document.querySelector('.pagetop');
  120.         if (!nav) return;
  121.  
  122.         nav.querySelectorAll('b:not(.hnname)').forEach((b) => {
  123.             const link = b.querySelector('a');
  124.             if (!link) return;
  125.  
  126.             b.replaceWith(link);
  127.         });
  128.     }
  129.  
  130.     /* ==========================================================
  131.    4. CODE BLOCK ENHANCEMENTS
  132.    ----------------------------------------------------------
  133.    Hacker News renders code inside:
  134.  
  135.        <pre><code>
  136.  
  137.    This function:
  138.    • Normalizes indentation
  139.    • Adds line numbers
  140.    • Wraps each line in a structured element
  141.  
  142.    This enables nicer styling and readable line numbers.
  143.    ========================================================== */
  144.  
  145.     function enhanceCodeBlocks() {
  146.         document.querySelectorAll('pre > code').forEach((code) => {
  147.             if (code.dataset.hnCodeEnhanced) return;
  148.             code.dataset.hnCodeEnhanced = true;
  149.  
  150.             const rawLines = code.textContent.replace(/\n$/, '').split('\n');
  151.  
  152.             // Detect common indentation and remove it
  153.             const baseIndent = rawLines[0].match(/^\s*/)[0].length;
  154.             const lines = rawLines.map((l) => l.slice(baseIndent));
  155.  
  156.             code.innerHTML = '';
  157.  
  158.             lines.forEach((line, i) => {
  159.                 const row = document.createElement('div');
  160.                 row.className = 'hn-code-line';
  161.  
  162.                 const num = document.createElement('span');
  163.                 num.className = 'hn-code-number';
  164.                 num.textContent = i + 1;
  165.  
  166.                 const text = document.createElement('span');
  167.                 text.className = 'hn-code-text';
  168.                 text.textContent = line || ' ';
  169.  
  170.                 row.appendChild(num);
  171.                 row.appendChild(text);
  172.  
  173.                 code.appendChild(row);
  174.             });
  175.         });
  176.     }
  177.  
  178.     /* ==========================================================
  179.    5. CONVERT * BULLETS → DIAMOND BULLETS
  180.    ----------------------------------------------------------
  181.    Hacker News does not support lists in comments.
  182.  
  183.    Users often simulate lists using:
  184.  
  185.       * item
  186.  
  187.    This function converts those lines into structured
  188.    elements that can be styled with CSS.
  189.    ========================================================== */
  190.  
  191.     function convertStarBullets() {
  192.         document.querySelectorAll('.commtext').forEach((comm) => {
  193.             if (comm.dataset.hnBulletsProcessed) return;
  194.             comm.dataset.hnBulletsProcessed = 'true';
  195.  
  196.             const walker = document.createTreeWalker(comm, NodeFilter.SHOW_TEXT);
  197.  
  198.             const targets = [];
  199.  
  200.             while (walker.nextNode()) {
  201.                 const node = walker.currentNode;
  202.                 const text = node.nodeValue.trim();
  203.  
  204.                 if (/^[-*]\s+/.test(text)) {
  205.                     targets.push(node);
  206.                 }
  207.             }
  208.  
  209.             targets.forEach((node) => {
  210.                 const text = node.nodeValue.trim().replace(/^[-*]\s+/, '');
  211.  
  212.                 const bullet = document.createElement('div');
  213.                 bullet.className = 'hn-bullet';
  214.                 bullet.textContent = text;
  215.  
  216.                 node.parentNode.replaceChild(bullet, node);
  217.             });
  218.         });
  219.     }
  220.  
  221.     /* ==========================================================
  222.    6. CONVERT NUMBERED LISTS → <ol>
  223.    ----------------------------------------------------------
  224.    Hacker News users often simulate ordered lists using:
  225.  
  226.       1. item
  227.       2. item
  228.       3. item
  229.  
  230.    This function detects those paragraphs and converts them
  231.    into a proper <ol> list.
  232.    ========================================================== */
  233.  
  234.     function convertNumberedLists() {
  235.         document.querySelectorAll('.commtext').forEach((comm) => {
  236.             if (comm.dataset.hnNumbersProcessed) return;
  237.             comm.dataset.hnNumbersProcessed = 'true';
  238.  
  239.             const nodes = Array.from(comm.querySelectorAll('p'));
  240.  
  241.             let list = null;
  242.  
  243.             nodes.forEach((p) => {
  244.                 const text = p.textContent.trim();
  245.                 const match = text.match(/^(\d+)\.\s+(.*)/);
  246.  
  247.                 if (match) {
  248.                     if (!list) {
  249.                         list = document.createElement('ol');
  250.                         list.className = 'hn-auto-list';
  251.                         p.before(list);
  252.                     }
  253.  
  254.                     const li = document.createElement('li');
  255.                     li.textContent = match[2];
  256.  
  257.                     list.appendChild(li);
  258.                     p.remove();
  259.                 } else {
  260.                     list = null;
  261.                 }
  262.             });
  263.         });
  264.     }
  265.  
  266.     /* ==========================================================
  267.    6. SYNTAX HIGHLIGHTING
  268.    ----------------------------------------------------------
  269.    Uses Highlight.js if it is available.
  270.  
  271.    This step runs before code enhancement so that syntax
  272.    highlighting is applied to the raw code content.
  273.    ========================================================== */
  274.  
  275.     function highlightCodeBlocks() {
  276.         if (typeof hljs === 'undefined') return;
  277.  
  278.         document.querySelectorAll('pre code').forEach((block) => {
  279.             const text = block.textContent;
  280.  
  281.             const looksLikeCode =
  282.                 /[{}();=<>]/.test(text) ||
  283.                 /\b(function|const|let|var|class|return|if|for|while)\b/.test(text);
  284.  
  285.             if (!looksLikeCode) return;
  286.  
  287.             block.dataset.hnRealCode = 'true';
  288.  
  289.             hljs.highlightElement(block);
  290.         });
  291.     }
  292.  
  293.     /* ==========================================================
  294.    7. TYPOGRAPHY (TYPESET.JS)
  295.    ----------------------------------------------------------
  296.    Applies typographic improvements such as:
  297.  
  298.    • smart quotes
  299.    • proper apostrophes
  300.    • typographic ellipses
  301.  
  302.    Only runs if Typeset.js is available.
  303.    ========================================================== */
  304.  
  305.     function typesetComments() {
  306.         if (!window.Typeset) return;
  307.  
  308.         document.querySelectorAll('.commtext').forEach((comment) => {
  309.             if (comment.dataset.typesetDone) return;
  310.             comment.dataset.typesetDone = 'true';
  311.  
  312.             Typeset.typesetElement(comment);
  313.         });
  314.     }
  315.  
  316.     /* ==========================================================
  317.    CONVERT HN HTML QUOTE PATTERN
  318.    ----------------------------------------------------------
  319.    Hacker News sometimes renders quotes like:
  320.  
  321.       &gt;<i>quoted text</i>
  322.  
  323.    instead of plain "> quoted text".
  324.  
  325.    This converts that structure into a proper blockquote.
  326.    ========================================================== */
  327.  
  328.     function convertInlineQuotes() {
  329.         document.querySelectorAll('.commtext').forEach((comm) => {
  330.             const walker = document.createTreeWalker(comm, NodeFilter.SHOW_TEXT);
  331.  
  332.             const targets = [];
  333.  
  334.             while (walker.nextNode()) {
  335.                 const node = walker.currentNode;
  336.  
  337.                 if (node.nodeValue.trim() === '>') {
  338.                     const next = node.nextSibling;
  339.  
  340.                     if (next && next.nodeType === 1) {
  341.                         targets.push({ marker: node, content: next });
  342.                     }
  343.                 }
  344.             }
  345.  
  346.             targets.forEach(({ marker, content }) => {
  347.                 const blockquote = document.createElement('blockquote');
  348.  
  349.                 blockquote.appendChild(content.cloneNode(true));
  350.  
  351.                 marker.parentNode.replaceChild(blockquote, marker);
  352.                 content.remove();
  353.             });
  354.         });
  355.     }
  356.  
  357.     /* ==========================================================
  358.    INIT PIPELINE
  359.    ----------------------------------------------------------
  360.    Executes all transformations in the correct order.
  361.  
  362.    Order matters:
  363.    - text transforms
  364.    - code transforms
  365.    - typography last
  366.    ========================================================== */
  367.  
  368.     function init() {
  369.         addOPIndicators();
  370.  
  371.         convertQuotes();
  372.         convertInlineQuotes();
  373.         convertStarBullets();
  374.         convertNumberedLists();
  375.  
  376.         normalizeHeaderNav();
  377.  
  378.         highlightCodeBlocks();
  379.         enhanceCodeBlocks();
  380.  
  381.         typesetComments();
  382.     }
  383.  
  384.     /* ==========================================================
  385.    START SCRIPT
  386.    ----------------------------------------------------------
  387.    Ensures the DOM is ready before modifying the page.
  388.    ========================================================== */
  389.  
  390.     if (document.readyState === 'loading') {
  391.         document.addEventListener('DOMContentLoaded', init);
  392.     } else {
  393.         init();
  394.     }
  395. })();
  396.  
Advertisement
Add Comment
Please, Sign In to add comment