// ==UserScript== // @name Cornucopia Enhancement Suite // @namespace github.com/lundgren // @version 0.0.8 // @description Variuos enhancements to Cornucopia.se // @author You // @match https://cornucopia.se/* // @icon https://www.google.com/s2/favicons?sz=64&domain=cornucopia.se // @grant GM.setValue // @grant GM.getValue // ==/UserScript== const DEFAULT_PREFERENCES = { hideFirstVisit: true, highlightParagraphs: true, highlightAdmin: true, favoriteMe: true, autoHideByDefault: false, jumpToFirstUnread: false, colorParagraphs: "#84dc23", colorComments: "#dc2328", colorFavorites: "#23dcd7", colorNavigation: "#7b23dc", favoriteWords: [], favoriteAuthors: [], hiddenAuthors: [], }; const SETTINGS_ICN = ``; const HIDE_COMMENTS_ICN = ``; const FAVORITE_COMMENTS_ICN = ``; const EMPTY_STAR_BASE64 = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/PjxzdmcgaWQ9Ikljb25zIiB2aWV3Qm94PSIwIDAgMjQgMjQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PHN0eWxlPi5jbHMtMXtmaWxsOiMyMzIzMjM7fTwvc3R5bGU+PC9kZWZzPjxwYXRoIGNsYXNzPSJjbHMtMSIgZD0iTTIzLjA1MywxMi42ODNhMy4xMzIsMy4xMzIsMCwwLDAtMS43MzctNS4zNDFsLTMuOTA5LS41NjhhMS4xMywxLjEzLDAsMCwxLS44NTEtLjYxOUwxNC44MDgsMi42MTRhMy4xMzEsMy4xMzEsMCwwLDAtNS42MTYsMEw3LjQ0NCw2LjE1NWExLjEzLDEuMTMsMCwwLDEtLjg1MS42MTlsLTMuOTA5LjU2OEEzLjEzMiwzLjEzMiwwLDAsMCwuOTQ3LDEyLjY4NEwzLjc3NiwxNS40NGExLjEzMSwxLjEzMSwwLDAsMSwuMzI2LDFsLS42NjcsMy44OTJhMy4xMzEsMy4xMzEsMCwwLDAsNC41NDIsMy4zbDMuNS0xLjgzOGExLjEyNSwxLjEyNSwwLDAsMSwxLjA1MiwwaDBsMy41LDEuODM4YTMuMTEsMy4xMSwwLDAsMCwzLjMtLjIzOSwzLjEwOSwzLjEwOSwwLDAsMCwxLjI0NS0zLjA2M0wxOS45LDE2LjQ0MWExLjEzLDEuMTMsMCwwLDEsLjMyNi0xWm0tNC4yMjYsMS4zMjVhMy4xMzEsMy4xMzEsMCwwLDAtLjksMi43NzJsLjY2NywzLjg5MmExLjEzMSwxLjEzMSwwLDAsMS0xLjY0MiwxLjE5M2wtMy41LTEuODM4YTMuMTM0LDMuMTM0LDAsMCwwLTIuOTE0LDBsLTMuNSwxLjgzOGExLjEzMSwxLjEzMSwwLDAsMS0xLjY0Mi0xLjE5M2wuNjY3LTMuODkxYTMuMTMyLDMuMTMyLDAsMCwwLS45LTIuNzczTDIuMzQ0LDExLjI1MWExLjEzMiwxLjEzMiwwLDAsMSwuNjI3LTEuOTNMNi44OCw4Ljc1M0EzLjEyOCwzLjEyOCwwLDAsMCw5LjIzNyw3LjA0TDEwLjk4NSwzLjVhMS4xNjUsMS4xNjUsMCwwLDEsMi4wMywwTDE0Ljc2Myw3LjA0QTMuMTI4LDMuMTI4LDAsMCwwLDE3LjEyLDguNzUzbDMuOTA5LjU2OGExLjEzMiwxLjEzMiwwLDAsMSwuNjI3LDEuOTNaIi8+PC9zdmc+"; const FILLED_STAR_BASE64 = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/PjxzdmcgaWQ9Ikljb25zIiB2aWV3Qm94PSIwIDAgMjQgMjQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPjxkZWZzPjxzdHlsZT4uY2xzLTF7ZmlsbDp1cmwoI2xpbmVhci1ncmFkaWVudCk7fTwvc3R5bGU+PGxpbmVhckdyYWRpZW50IGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiBpZD0ibGluZWFyLWdyYWRpZW50IiB4MT0iMTIiIHgyPSIxMiIgeTE9IjEuNzU1IiB5Mj0iMjMuMDc2Ij48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiNmZmY2NTAiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiNmZmFiMTciLz48L2xpbmVhckdyYWRpZW50PjwvZGVmcz48cGF0aCBjbGFzcz0iY2xzLTEiIGQ9Ik0xMi45OTIsMjAuOTEybDMuNSwxLjgzOEEyLjEzMSwyLjEzMSwwLDAsMCwxOS41OCwyMC41bC0uNjY3LTMuODkzYTIuMTI5LDIuMTI5LDAsMCwxLC42MTMtMS44ODdsMi44MjgtMi43NTdhMi4xMzEsMi4xMzEsMCwwLDAtMS4xODEtMy42MzVsLTMuOTA5LS41NjhhMi4xMzMsMi4xMzMsMCwwLDEtMS42LTEuMTY2TDEzLjkxMSwzLjA1NmEyLjEzMSwyLjEzMSwwLDAsMC0zLjgyMiwwTDguMzQxLDYuNmEyLjEzMywyLjEzMywwLDAsMS0xLjYsMS4xNjZsLTMuOTA5LjU2OGEyLjEzMSwyLjEzMSwwLDAsMC0xLjE4MSwzLjYzNWwyLjgyOCwyLjc1N2EyLjEyOSwyLjEyOSwwLDAsMSwuNjEzLDEuODg3TDQuNDIsMjAuNUEyLjEzMSwyLjEzMSwwLDAsMCw3LjUxMiwyMi43NWwzLjUtMS44MzhBMi4xMzUsMi4xMzUsMCwwLDEsMTIuOTkyLDIwLjkxMloiLz48L3N2Zz4="; const ADMIN_COLOR = "#d3d3d3"; const StateFlags = { Read: 1 << 0, Hidden: 1 << 1, Favorite: 1 << 2, }; (async () => { const pageId = window.location.pathname; if (pageId === "/") { // We only enhance the article page for now return; } // Fetch info about previous visits const pageState = await readStorage(pageId, { readBefore: false }); const preferences = { ...DEFAULT_PREFERENCES, ...(await readStorage("preferences", {})) }; const articleReadBefore = pageState.readBefore; // Setup a state object to simplify working with flags const state = { isUnread: (id) => pageState[id] === undefined, isHidden: (id) => pageState[id] & StateFlags.Hidden, isFavorite: (id) => pageState[id] & StateFlags.Favorite, setAsRead: (id) => (pageState[id] |= StateFlags.Read), setHidden: (id, hidden) => { if (hidden) { pageState[id] |= StateFlags.Hidden; } else { pageState[id] &= ~StateFlags.Hidden; } }, setFavorite: (id, favorite) => { if (favorite) { pageState[id] |= StateFlags.Favorite; } else { pageState[id] &= ~StateFlags.Favorite; } }, isHideReadThreads: () => pageState.hideReadThreads, setHideReadThreads: (hide) => (pageState.hideReadThreads = hide), }; // Try to find the logged in user so we can automatically favorite their comments const loggedInUser = findLoggedInUser(); // Setup a canvas to draw the minimap on const $canvas = document.createElement("canvas"); document.body.appendChild($canvas); // Get a list of all comments and paragraphs const comments = getComments(); const paragraphs = getParagraphs(); const allItems = [...paragraphs, ...comments]; const redrawHighlightsAndMinimap = () => { drawHighlights(allItems); drawMinimap($canvas, allItems); }; // Add state to all comments and make them clickable for (const comment of comments) { comment.unread = state.isUnread(comment.id); comment.hasFavoriteWords = containsAny(comment.$el, preferences.favoriteWords); state.setAsRead(comment.id); const shouldAutoHideAuthor = preferences.hiddenAuthors.includes(comment.author); if (comment.unread && shouldAutoHideAuthor) { state.setHidden(comment.id, true); comment.hidden = true; } if (comment.unread && comment.author === loggedInUser && preferences.favoriteMe) { state.setFavorite(comment.id, true); } if (comment.unread && comment.hasFavoriteWords) { highlightWords(comment.$el, preferences.favoriteWords, preferences.colorFavorites); } comment.isHidden = () => state.isHidden(comment.id); comment.isFavorite = () => state.isFavorite(comment.id); comment.isParentFavorite = () => { let node = comment; while (node) { if (node.isFavorite()) { return true; } node = node.parent; } return false; }; // Return how the comment should be highlighted, undefined if not at all comment.highlightColor = () => { let color; if (comment.fromAdmin && preferences.highlightAdmin) { color = ADMIN_COLOR; } if (comment.unread) { if (articleReadBefore || !preferences.hideFirstVisit) { color = preferences.colorComments; } const isFavoriteAuthor = preferences.favoriteAuthors.includes(comment.author); if (isFavoriteAuthor || comment.isParentFavorite() || comment.hasFavoriteWords) { color = preferences.colorFavorites; } } return color; }; comment.setHidden = (hidden) => { state.setHidden(comment.id, hidden); writeStorage(pageId, pageState); }; comment.setFavorite = (favorite) => { state.setFavorite(comment.id, favorite); writeStorage(pageId, pageState); redrawHighlightsAndMinimap(); }; } // We cant count unread children until all comments have been processed let totalUnread = 0; for (const comment of comments) { totalUnread += comment.unread ? 1 : 0; comment.count = getCommentCount(comment); makeCommentClickable(comment); } // Add unread count to the comment count header updateCommentCount(totalUnread); // Add state to all paragraphs for (const paragraph of paragraphs) { paragraph.unread = state.isUnread(paragraph.id); state.setAsRead(paragraph.id); paragraph.highlightColor = () => { const hightlight = preferences.highlightParagraphs && paragraph.unread && (articleReadBefore || !preferences.hideFirstVisit); return hightlight ? preferences.colorParagraphs : undefined; }; } // Highlight all new content and comments drawHighlights(allItems); // Setup comments options const toggleReadThreads = (hide) => { state.setHideReadThreads(hide); writeStorage(pageId, pageState); for (const comment of comments) { const topCommentWithoutUnread = !comment.parent && comment.count.unread == 0; if (topCommentWithoutUnread) { if (hide) { comment.$el.temporarilyHideIt(); } else { comment.$el.temporarilyShowIt(); } } } }; const hideReadComments = state.isHideReadThreads() || (preferences.autoHideByDefault && !articleReadBefore); if (hideReadComments) { toggleReadThreads(true); } addOptionsControls(state.isHideReadThreads(), toggleReadThreads); // Add hotkeys to navigate between paragraphs and comments const jumpToFirstUnread = preferences.jumpToFirstUnread && articleReadBefore; enableHotkeyNavigationFor(allItems, preferences.colorNavigation, jumpToFirstUnread); // Make sure the minimap is redrawn when the window is resized const redrawMinimap = () => drawMinimap($canvas, allItems); new ResizeObserver(redrawMinimap).observe(document.body); addEventListener("resize", redrawMinimap); // Save the info about this visit pageState.lastReadTs = Date.now(); pageState.readBefore = true; writeStorage(pageId, pageState); })(); async function readStorage(key, defaultVal) { // Firefox if (typeof browser !== "undefined") { return (await browser.storage.local.get(key))[key] || defaultVal; } // Chromium based browsers if (typeof chrome !== "undefined") { return (await chrome.storage.local.get(key))[key] || defaultVal; } // Userscripts if (typeof GM !== "undefined") { return await GM.getValue(key, defaultVal); } throw new Error("No storage available"); } async function writeStorage(key, value) { // Firefox if (typeof browser !== "undefined") { return await browser.storage.local.set({ [key]: value }); } // Chromium based browsers if (typeof chrome !== "undefined") { return await chrome.storage.local.set({ [key]: value }); } // Userscripts if (typeof GM !== "undefined") { return await GM.setValue(key, value); } throw new Error("No storage available"); } function findLoggedInUser() { const $wpAdmin = document.getElementById("wp-admin-bar-my-account"); if ($wpAdmin) { return ($wpAdmin.getElementsByClassName("display-name")[0]?.innerText || "") .trim() .toLowerCase(); } return null; } function getDepth(element) { for (const className of element.classList) { if (className.startsWith("depth-")) { return parseInt(className.split("-")[1], 10); } } return 0; } function getCommentsChildren($el) { const $children = $el.getElementsByClassName(`comment depth-${getDepth($el) + 1}`); const children = []; for (let $child of $children) { children.push({ $el: $child, children: getCommentsChildren($child), }); } return children; } // getComments will return a list of all comments on the page function getComments() { const comments = []; const traverseAndSave = ($el, parent) => { const id = $el.id; const author = ($el.getElementsByClassName("author")[0]?.innerText || "").trim().toLowerCase(); const $commentText = $el.getElementsByClassName("comment-text")[0]; $commentText.style.transition = "color 500ms"; const comment = { id: id, $el: $commentText, type: "comment", author: author, fromAdmin: $el.classList.contains("bypostauthor") || $el.classList.contains("comment-author-admin"), children: [], parent: parent, }; comments.push(comment); const $children = $el.getElementsByClassName(`comment depth-${getDepth($el) + 1}`); for (let $child of $children) { comment.children.push(traverseAndSave($child, comment)); } return comment; }; const $commentsContainer = document.getElementById("comments"); if ($commentsContainer) { const $children = $commentsContainer.getElementsByClassName(`comment depth-1`); for (let $child of $children) { traverseAndSave($child, undefined); } } return comments; } // getParagraphs will return a list of all paragraphs function getParagraphs() { const paragraphs = []; const $post = document.getElementById("penci-post-entry-inner"); for (let $paragraph of $post.children) { if ($paragraph.nodeName === "P" || $paragraph.nodeName === "BLOCKQUOTE") { if ($paragraph.nodeName === "BLOCKQUOTE") { // Blockquotes uses the left border to indicate quotes, so we wrap it in a div we can style const $wrapper = document.createElement("div"); $paragraph.replaceWith($wrapper); $wrapper.append($paragraph); $paragraph = $wrapper; } paragraphs.push({ id: hashCode($paragraph.outerHTML), type: "paragraph", $el: $paragraph, }); } } // Add next post link as a paragraph const $nextPost = document.getElementsByClassName("post-pagination")[0]; if ($nextPost) { paragraphs.push({ id: hashCode($nextPost.innerText), type: "nextpost", $el: $nextPost, }); } return paragraphs; } function drawHighlights(items) { for (let item of items) { const color = item.highlightColor(); if (color) { item.$el.style.borderLeft = `3px solid ${color}`; item.$el.style.paddingLeft = "6px"; } else if (item.$el.style.borderLeft != "") { // Remove possible highlights item.$el.style.borderLeft = ""; item.$el.style.paddingLeft = ""; } } } function containsAny($el, words = []) { const $commentText = $el.getElementsByClassName("comment-content")[0]; if ($commentText) { const lowerText = $commentText.textContent.toLowerCase(); return words.some((word) => lowerText.includes(word)); } return false; } function highlightWords($el, words = [], color) { const $commentText = $el.getElementsByClassName("comment-content")[0]; for (let i = 0; i < $commentText.children.length; i++) { const $child = $commentText.children[i]; if ($child.tagName === "P") { for (let word of words) { $child.innerHTML = $child.innerHTML.replace( new RegExp(`(${word})`, "ig"), `$1` ); } } } } function updateCommentCount(count) { const $comments = document.getElementById("comments"); if ($comments) { const $commentCount = $comments.getElementsByClassName("post-box-title")[0]; if ($commentCount) { $commentCount.textContent = `${$commentCount.textContent} (${count} olästa)`; } } } function addOptionsControls(isChecked, onHideThreadsChanged) { const $comments = document.getElementById("comments"); var $commentList = $comments.getElementsByClassName("comments")[0]; if ($commentList) { // Settings button to open the options page const $settingsBtn = strToElement(SETTINGS_ICN); $settingsBtn.href = chrome.runtime.getURL("options.html"); $settingsBtn.addEventListener("click", () => { chrome.runtime.sendMessage({ action: "openOptionsPage" }); }); const $spacing = document.createElement("span"); $spacing.style.padding = "0 10px"; $spacing.appendChild(document.createTextNode(" | ")); // Checkbox to hide read threads const $checkbox = document.createElement("input"); $checkbox.type = "checkbox"; $checkbox.checked = isChecked; $checkbox.id = "hide-read-threads"; $checkbox.addEventListener("change", (e) => onHideThreadsChanged(e.target.checked)); const $label = document.createElement("label"); $label.htmlFor = "hide-read-threads"; $label.title = "This will hide all top level comments that have no unread replies to minimize distractions.\nIf a thread gets a reply in your next visit it will show up again."; $label.appendChild(document.createTextNode(" Hide threads without unread replies")); // Add the options to the DOM const $container = document.createElement("div"); $container.style.padding = "10px"; $container.appendChild($settingsBtn); $container.appendChild($spacing); $container.appendChild($checkbox); $container.appendChild($label); $comments.insertBefore($container, $commentList); } } // enableHotkeyNavigationFor will // - allow the user to navigate new comments and paragraphs using the J and K keys // - allow the user to navigate to the first new comment or paragraph using the I key // - allow the user to minimize navigated comments by pressing the M key function enableHotkeyNavigationFor(allItems, navigationColor, jumpToFirstUnread) { const entries = allItems.filter((item) => item.unread); let navigatedEntry; let styleBefore; addEventListener("scroll", (e) => { if (navigatedEntry) { navigatedEntry.$el.style.borderLeft = styleBefore; navigatedEntry = null; } }); // Scroll to center of element or top of element if it's too tall // Also highlight the element for a short time const scrollTo = (entry) => { const rect = entry.$el.getBoundingClientRect(); if (rect.height > window.innerHeight) { window.scrollTo(0, rect.top + window.scrollY - 50); } else { entry.$el.scrollIntoView({ block: "center" }); } setTimeout(() => { navigatedEntry = entry; styleBefore = entry.$el.style.borderLeft; entry.$el.style.borderLeft = `3px solid ${navigationColor}`; }, 50); }; const jumpToFirst = () => { for (let i = 0; i < entries.length; i++) { const isMinimized = entries[i].$el.getBoundingClientRect().height === 0; if (!isMinimized) { scrollTo(entries[i]); return; } } // If no unread, jump to comment section const $comments = document.getElementById("comments"); if ($comments) { let position = $comments.getBoundingClientRect(); window.scrollTo(0, position.top + window.scrollY - 50); } }; addEventListener("keydown", (e) => { const focused = document.activeElement && document.activeElement.nodeName; if (focused === "INPUT" || focused === "TEXTAREA") { return; } const center = window.innerHeight / 2; if (e.code === "KeyJ") { for (let i = 0; i < entries.length; i++) { const rect = entries[i].$el.getBoundingClientRect(); const isMinimized = rect.height === 0; const isBelowCenter = rect.top + rect.height / 2 - 50 > center; const isAlreadyNavigated = entries[i] === navigatedEntry; if (isBelowCenter && !isMinimized && !isAlreadyNavigated) { scrollTo(entries[i]); return; } } } else if (e.code === "KeyK") { for (let i = entries.length - 1; i >= 0; i--) { const rect = entries[i].$el.getBoundingClientRect(); const isMinimized = rect.height === 0; const isAboveCenter = rect.top + rect.height / 2 + 50 < center; const isAlreadyNavigated = entries[i] === navigatedEntry; if (isAboveCenter && !isMinimized && !isAlreadyNavigated) { scrollTo(entries[i]); return; } } } else if (e.code === "KeyI") { jumpToFirst(); } else if (e.code === "KeyM") { if (navigatedEntry && navigatedEntry.type === "comment") { navigatedEntry.$el.hideIt(); } } else if (e.code === "KeyN") { if (navigatedEntry && navigatedEntry.type === "comment") { navigatedEntry.$el.favoriteIt(); } } }); if (jumpToFirstUnread) { if ("scrollRestoration" in history) { history.scrollRestoration = "manual"; } jumpToFirst(); } } // drawMinimap will // - draw a minimap on the right side of the screen function drawMinimap($canvas, entries) { const scrollOffset = window.innerWidth - document.documentElement.clientWidth; $canvas.style = `position:fixed; left: ${ document.body.clientWidth - 5 }px; top: ${scrollOffset}px; width: 5px; height: ${ window.innerHeight - scrollOffset * 2 }px; z-index: 9999;`; $canvas.width = 5; $canvas.height = window.innerHeight; const q = window.innerHeight / document.documentElement.scrollHeight; var ctx = $canvas.getContext("2d"); ctx.clearRect(0, 0, $canvas.width, $canvas.height); for (const item of entries) { const color = item.highlightColor(); if (color) { const rect = item.$el.getBoundingClientRect(); ctx.fillStyle = color; ctx.fillRect(0, (window.scrollY + rect.top) * q, 5, rect.height * q); } } } function makeCommentClickable(comment) { const $container = comment.$el.parentElement.parentElement; const $hiddenContainer = createHiddenThreadContainer($container, comment); $container.parentNode.insertBefore($hiddenContainer, $container); const hideComments = (e) => { e?.preventDefault(); $container.style.display = "none"; $hiddenContainer.style.display = ""; comment.setHidden(true); }; const temporarilyHideComments = (e) => { e?.preventDefault(); $container.style.display = "none"; $hiddenContainer.style.display = ""; }; const showComments = (e) => { e?.preventDefault(); $container.style.display = ""; $hiddenContainer.style.display = "none"; comment.setHidden(false); }; const temporarilyShowComments = (e) => { e?.preventDefault(); $container.style.display = ""; $hiddenContainer.style.display = "none"; comment.setHidden(false); }; if (comment.isHidden()) { $container.style.display = "none"; } else { $hiddenContainer.style.display = "none"; } // Find author element and hide comments when it's clicked const $author = comment.$el.getElementsByClassName("author")[0]; $author.title = "Hide comments"; $author.style.cursor = "pointer"; $author.prepend(strToElement(HIDE_COMMENTS_ICN)); $author.addEventListener("click", hideComments); comment.$el.hideIt = hideComments; comment.$el.temporarilyHideIt = temporarilyHideComments; // Show comments again when hidden container is clicked $hiddenContainer.addEventListener("click", showComments); comment.$el.showIt = showComments; comment.$el.temporarilyShowIt = temporarilyShowComments; // Add favorite button const $favoriteBtn = strToElement(FAVORITE_COMMENTS_ICN); $favoriteBtn.title = "Favorite comments"; $favoriteBtn.style.cursor = "pointer"; $favoriteBtn.src = comment.isFavorite() ? FILLED_STAR_BASE64 : EMPTY_STAR_BASE64; $author.prepend($favoriteBtn); const toggleFavorite = (e) => { e?.stopImmediatePropagation(); if (comment.isFavorite()) { $favoriteBtn.src = EMPTY_STAR_BASE64; comment.setFavorite(false); } else { $favoriteBtn.src = FILLED_STAR_BASE64; comment.setFavorite(true); } }; $favoriteBtn.addEventListener("click", toggleFavorite); comment.$el.favoriteIt = toggleFavorite; } function createHiddenThreadContainer($container, comment) { const $hiddenContainer = document.createElement("div"); const $text = document.createElement("p"); $hiddenContainer.appendChild($text); const $authorContent = comment.$el.getElementsByClassName("author")[0]; const $commentContent = comment.$el.getElementsByClassName("comment-content")[0]; const summary = getSummary($authorContent, $commentContent); if (comment.count.unread > 0) { $text.innerText = `[${comment.count.total} comments, ${comment.count.unread} unread] - ${summary} (${comment.count.unread} unread)`; } else { $text.innerText = `[${comment.count.total} comments] - ${summary}`; } $hiddenContainer.className = $container.className; $hiddenContainer.title = "Show comments"; $hiddenContainer.style.cursor = "pointer"; $hiddenContainer.style.height = "30px"; $hiddenContainer.style.borderTop = "1px solid #dedede"; if ($container.className.includes("depth-1")) { $hiddenContainer.style.backgroundColor = "#fff9ec"; } return $hiddenContainer; } function getSummary($author, $comment) { const author = $author.innerText || $author.textContent || ""; const comment = $comment.innerText || $comment.textContent || ""; return `${author}: ${comment}`.replace(/(\r\n|\n|\r)/gm, " ").substring(0, 75) + "..."; } function getCommentCount(comment) { let total = 1; let unread = comment.unread ? 1 : 0; for (const child of comment.children) { const count = getCommentCount(child); total += count.total; unread += count.unread; } return { total, unread }; } function strToElement(str) { const temp = document.createElement("div"); temp.innerHTML = str; return temp.firstChild; } // https://stackoverflow.com/a/52171480 function hashCode(str) { let h1 = 0xdeadbeef ^ 0, h2 = 0x41c6ce57 ^ 0; for (let i = 0, ch; i < str.length; i++) { ch = str.charCodeAt(i); h1 = Math.imul(h1 ^ ch, 2654435761); h2 = Math.imul(h2 ^ ch, 1597334677); } h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909); h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909); return 4294967296 * (2097151 & h2) + (h1 >>> 0); }