Skip to content

Instantly share code, notes, and snippets.

@postmalloc
Last active August 30, 2023 08:10
Show Gist options
  • Save postmalloc/e2602752d46c5b9dee24462356f96cca to your computer and use it in GitHub Desktop.
Save postmalloc/e2602752d46c5b9dee24462356f96cca to your computer and use it in GitHub Desktop.
Hacker News comments sidebar bookmarklet
// A handy bookmarklet to display comments from the top-rated Hacker News thread related to the current page
// Written with the help of GPT-4
javascript:(function() {
const createCommentElement = (comment, depth) => {
const commentWrapper = document.createElement('div');
commentWrapper.style.paddingLeft = (depth * 20) + 'px';
commentWrapper.style.marginBottom = '10px';
commentWrapper.style.marginLeft = '10px';
commentWrapper.style.color = '#333';
commentWrapper.style.background = '#f6f6ef';
commentWrapper.style.fontFamily = 'Verdana, Geneva, sans-serif';
commentWrapper.style.fontSize = '14px';
const toggleButton = document.createElement('button');
toggleButton.textContent = '[-]';
toggleButton.style.marginRight = '10px';
toggleButton.style.background = 'none';
toggleButton.style.border = 'none';
toggleButton.style.color = '#888';
toggleButton.style.cursor = 'pointer';
commentWrapper.appendChild(toggleButton);
const commentText = document.createElement('span');
const decoded = new DOMParser().parseFromString(comment.text, 'text/html').body.textContent || '';
const author = comment.author || 'Anonymous';
const formattedDate = new Date(comment.created_at).toLocaleString();
const authorElement = document.createElement('i');
authorElement.textContent = author;
const dateElement = document.createElement('span');
dateElement.textContent = formattedDate;
dateElement.style.color = '#888';
const contentElement = document.createElement('span');
insertTextWithLinks(decoded, contentElement);
commentText.appendChild(authorElement);
commentText.appendChild(document.createTextNode(' '));
commentText.appendChild(dateElement);
commentText.appendChild(document.createElement('br'));
commentText.appendChild(contentElement);
commentWrapper.appendChild(commentText);
const childrenWrapper = document.createElement('div');
childrenWrapper.style.marginLeft = '20px';
childrenWrapper.style.marginTop = '10px';
commentWrapper.appendChild(childrenWrapper);
let isCollapsed = false;
toggleButton.addEventListener('click', () => {
if(isCollapsed) {
commentText.style.display = '';
childrenWrapper.style.display = '';
toggleButton.textContent = '[-]';
isCollapsed = false;
} else {
commentText.style.display = 'none';
childrenWrapper.style.display = 'none';
toggleButton.textContent = '[+]';
isCollapsed = true;
}
});
return { commentWrapper, childrenWrapper };
};
const addCommentsToSidebar = (comments, depth, sidebarContent) => {
for(let c of comments) {
if(c.text){
const { commentWrapper, childrenWrapper } = createCommentElement(c, depth);
sidebarContent.appendChild(commentWrapper);
if (c.children) {
addCommentsToSidebar(c.children, depth + 1, childrenWrapper);
}
}
}
};
const insertTextWithLinks = (text, contentElement) => {
const urlRegex = /(https?:\/\/[^\s]+)/g;
let lastIndex = 0;
let match;
while (match = urlRegex.exec(text)) {
if (match.index > lastIndex) {
contentElement.appendChild(document.createTextNode(text.slice(lastIndex, match.index)));
}
const anchor = document.createElement('a');
anchor.href = match[0];
anchor.target = "_blank";
anchor.rel = "noopener noreferrer";
anchor.style.color = "#333";
anchor.style.textDecoration = "underline";
anchor.textContent = match[0];
contentElement.appendChild(anchor);
lastIndex = urlRegex.lastIndex;
}
if (lastIndex < text.length) {
contentElement.appendChild(document.createTextNode(text.slice(lastIndex)));
}
};
const url = `https://hn.algolia.com/api/v1/search?query=${encodeURIComponent(document.location.href)}&tags=story`;
fetch(url)
.then((response) => response.json())
.then((data) => {
if(data.hits.length === 0){
alert('This page has not been posted on Hacker News.');
return;
}
const highestScoringStory = data.hits.reduce((prev, cur) => (prev.points > cur.points) ? prev : cur);
const storyId = highestScoringStory.objectID;
const commentsUrl = `https://hn.algolia.com/api/v1/items/${storyId}`;
fetch(commentsUrl)
.then((response) => response.json())
.then((data) => {
const sidebar = document.createElement('div');
sidebar.className = 'hn-comments-sidebar'; /* Add a unique class name for CSS scoping */
sidebar.innerHTML = `
<style>
.hn-comments-sidebar {
position:fixed;
right:0;
top:0;
width:600px;
height:100%;
background-color:#f6f6ef;
overflow-y:auto;
padding:1em;
z-index:999999;
font-family:Verdana, Geneva, sans-serif;
font-size:14px;
color:#333;
}
.hn-comments-sidebar a {
color:#333;
text-decoration:underline;
}
</style>
`;
const sidebarContent = document.createElement('div');
sidebarContent.style.overflowY = 'scroll';
sidebarContent.style.height = '100%';
sidebar.appendChild(sidebarContent);
addCommentsToSidebar(data.children, 0, sidebarContent);
const dragHandle = document.createElement('div');
dragHandle.style.cssText = 'position:absolute;top:0;left:0;bottom:0;width:10px;background:rgba(0,0,0,0.1);cursor:ew-resize;';
sidebar.appendChild(dragHandle);
let isDragging = false;
let initialX = 0;
let initialWidth = 0;
dragHandle.addEventListener('mousedown', (e) => {
isDragging = true;
initialX = e.clientX;
initialWidth = sidebar.offsetWidth;
});
window.addEventListener('mousemove', (e) => {
if(isDragging) {
const offsetX = e.clientX - initialX;
const newWidth = Math.max(400, initialWidth - offsetX);
sidebar.style.width = newWidth + 'px';
}
});
window.addEventListener('mouseup', () => {
isDragging = false;
});
sidebar.addEventListener('scroll', (e) => {
dragHandle.style.top = sidebar.scrollTop + 'px';
});
document.body.appendChild(sidebar);
})
.catch((error) => console.error('Error:', error));
})
.catch((error) => console.error('Error:', error));
})();
@postmalloc
Copy link
Author

Update: As suggested in the HN discussion, I removed the reliance on escaping, and instead create DOM nodes directly. This should minimise the chances of XSS.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment