Skip to content

Instantly share code, notes, and snippets.

@msenturk
Created December 29, 2025 10:46
Show Gist options
  • Select an option

  • Save msenturk/d81f97f2daf33b85006599e974166106 to your computer and use it in GitHub Desktop.

Select an option

Save msenturk/d81f97f2daf33b85006599e974166106 to your computer and use it in GitHub Desktop.
hn.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>monospace.news</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--color-black: #000000;
--color-dark-gray: #333333;
--color-medium-gray: #666666;
--color-light-gray: #999999;
--color-white: #ffffff;
--color-off-white: #f8f8f8;
--color-accent: #3366cc;
--color-border: #cccccc;
--color-hover: #f5f5f5;
--color-error: #cc0000;
--comment-indent: 20px;
}
body {
font-family: monospace;
font-size: 16px;
line-height: 1.5;
color: var(--color-black);
background-color: var(--color-white);
margin: 0;
padding: 0;
}
.container {
max-width: 1000px;
margin: 0 auto;
padding: 0 20px;
}
/* Header */
header {
border-bottom: 1px solid var(--color-border);
padding: 20px 0;
margin-bottom: 20px;
background-color: var(--color-off-white);
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
font-size: 24px;
font-weight: bold;
text-decoration: none;
color: var(--color-black);
display: flex;
align-items: center;
}
.logo::before {
content: "$ ";
color: var(--color-accent);
}
nav {
display: flex;
gap: 20px;
}
nav a {
color: var(--color-dark-gray);
text-decoration: none;
padding: 5px 10px;
transition: color 0.1s;
}
nav a:hover {
color: var(--color-accent);
}
/* Main Content */
main {
min-height: calc(100vh - 200px);
}
.controls {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
border-bottom: 1px solid var(--color-border);
padding-bottom: 10px;
}
.tabs {
display: flex;
gap: 15px;
}
.tab {
background: none;
border: none;
color: var(--color-medium-gray);
cursor: pointer;
font-family: inherit;
font-size: 16px;
padding: 5px 10px;
transition: color 0.1s;
}
.tab:hover {
color: var(--color-dark-gray);
}
.tab.active {
color: var(--color-black);
border-bottom: 1px solid var(--color-black);
font-weight: bold;
}
.search-box {
background: var(--color-white);
border: 1px solid var(--color-border);
color: var(--color-black);
padding: 5px 10px;
font-family: inherit;
width: 200px;
}
.search-box:focus {
outline: none;
border-color: var(--color-accent);
}
/* Content Area */
.content {
border: 1px solid var(--color-border);
padding: 20px;
margin-bottom: 20px;
background-color: var(--color-white);
}
/* Loading State */
.loading {
text-align: center;
padding: 20px 0;
color: var(--color-medium-gray);
}
.error {
text-align: center;
padding: 20px 0;
color: var(--color-error);
}
.retry-button {
background: var(--color-white);
border: 1px solid var(--color-error);
color: var(--color-error);
padding: 8px 20px;
cursor: pointer;
font-family: inherit;
margin-top: 10px;
}
.retry-button:hover {
background: var(--color-error);
color: var(--color-white);
}
/* Story List */
.story-list {
display: flex;
flex-direction: column;
}
.story-item {
display: flex;
gap: 15px;
padding: 10px 0;
border-bottom: 1px solid var(--color-border);
}
.story-item:last-child {
border-bottom: none;
}
.story-number {
color: var(--color-medium-gray);
min-width: 30px;
text-align: right;
}
.story-content {
flex: 1;
}
.story-title {
color: var(--color-black);
text-decoration: none;
display: block;
margin-bottom: 5px;
}
.story-title:hover {
color: var(--color-accent);
text-decoration: underline;
}
.story-domain {
color: var(--color-medium-gray);
font-size: 14px;
}
.story-meta {
display: flex;
gap: 10px;
margin-top: 5px;
font-size: 14px;
color: var(--color-medium-gray);
}
.story-meta a {
color: var(--color-medium-gray);
text-decoration: none;
}
.story-meta a:hover {
color: var(--color-accent);
text-decoration: underline;
}
/* Comments Section */
.comments-section {
margin-top: 15px;
display: none;
}
.comments-section.active {
display: block;
}
.comment-toggle {
cursor: pointer;
color: var(--color-medium-gray);
}
.comment-toggle:hover {
color: var(--color-accent);
}
.comment-container {
margin-top: 10px;
border-left: 1px solid var(--color-border);
padding-left: var(--comment-indent);
}
.comment {
margin-bottom: 15px;
padding-bottom: 15px;
border-bottom: 1px solid var(--color-border);
}
.comment:last-child {
border-bottom: none;
margin-bottom: 0;
padding-bottom: 0;
}
.comment-header {
color: var(--color-medium-gray);
font-size: 14px;
margin-bottom: 5px;
display: flex;
justify-content: space-between;
}
.comment-text {
color: var(--color-dark-gray);
font-size: 14px;
white-space: pre-wrap;
}
.comment-text a {
color: var(--color-accent);
}
.comment-replies {
margin-top: 10px;
}
.reply-toggle {
font-size: 12px;
color: var(--color-medium-gray);
cursor: pointer;
margin-top: 5px;
}
.reply-toggle:hover {
color: var(--color-accent);
}
.reply-loading {
font-size: 12px;
color: var(--color-medium-gray);
font-style: italic;
margin-top: 5px;
}
/* Pagination */
.pagination {
text-align: center;
padding: 20px 0;
border-top: 1px solid var(--color-border);
margin-top: 20px;
}
.more-button {
background: var(--color-white);
border: 1px solid var(--color-border);
color: var(--color-dark-gray);
padding: 10px 30px;
cursor: pointer;
font-family: inherit;
transition: all 0.1s;
}
.more-button:hover {
background: var(--color-hover);
border-color: var(--color-accent);
color: var(--color-accent);
}
.more-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Footer */
footer {
border-top: 1px solid var(--color-border);
padding: 20px 0;
margin-top: 40px;
text-align: center;
color: var(--color-medium-gray);
font-size: 14px;
background-color: var(--color-off-white);
}
/* Status Indicator */
.status {
position: fixed;
bottom: 20px;
right: 20px;
background: var(--color-off-white);
border: 1px solid var(--color-border);
padding: 10px 15px;
font-size: 12px;
color: var(--color-medium-gray);
display: none;
}
.status.show {
display: block;
}
/* Responsive */
@media (max-width: 768px) {
.header-content {
flex-direction: column;
gap: 15px;
}
.controls {
flex-direction: column;
gap: 15px;
}
.tabs {
flex-wrap: wrap;
}
.search-box {
width: 100%;
}
}
</style>
</head>
<body>
<header>
<div class="container">
<div class="header-content">
<a href="#" class="logo">monospace.news</a>
<nav>
<a href="#" data-tab="top">top</a>
<a href="#" data-tab="new">new</a>
<a href="#" data-tab="best">best</a>
<a href="#" data-tab="ask">ask</a>
<a href="#" data-tab="show">show</a>
<a href="#" data-tab="jobs">jobs</a>
</nav>
</div>
</div>
</header>
<main>
<div class="container">
<div class="controls">
<div class="tabs">
<button class="tab active" data-category="top">top</button>
<button class="tab" data-category="new">new</button>
<button class="tab" data-category="best">best</button>
<button class="tab" data-category="ask">ask</button>
<button class="tab" data-category="show">show</button>
<button class="tab" data-category="jobs">jobs</button>
</div>
<input type="text" class="search-box" placeholder="search..." id="searchInput">
</div>
<div class="content">
<div id="storiesContainer">
<div class="loading">Loading stories...</div>
</div>
<div class="pagination" id="pagination" style="display: none;">
<button class="more-button" id="moreButton">More</button>
</div>
</div>
</div>
</main>
<footer>
<div class="container">
<p>powered by HackerNews API | cached data | lazy loading</p>
</div>
</footer>
<div class="status" id="status"></div>
<script>
// HackerNews API Base URL
const API_BASE = 'https://hacker-news.firebaseio.com/v0';
// Application State
const state = {
currentCategory: 'top',
stories: [],
storyIds: [],
currentPage: 0,
storiesPerPage: 30,
searchTerm: '',
isLoading: false,
cache: {
stories: new Map(),
comments: new Map(),
storyIds: new Map(),
timestamps: new Map()
},
expandedComments: new Set(),
expandedReplies: new Set(),
loadingComments: new Set()
};
// Cache expiration time (5 minutes)
const CACHE_EXPIRY = 5 * 60 * 1000;
// DOM Elements
const storiesContainer = document.getElementById('storiesContainer');
const pagination = document.getElementById('pagination');
const moreButton = document.getElementById('moreButton');
const tabs = document.querySelectorAll('.tab');
const navLinks = document.querySelectorAll('nav a');
const searchInput = document.getElementById('searchInput');
const status = document.getElementById('status');
// Initialize
document.addEventListener('DOMContentLoaded', () => {
console.log('Application initialized');
attachEventListeners();
loadStories('top');
});
// Event Listeners
function attachEventListeners() {
tabs.forEach(tab => {
tab.addEventListener('click', (e) => {
const category = e.target.dataset.category;
switchCategory(category);
});
});
navLinks.forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const category = e.target.dataset.tab;
if (category) {
switchCategory(category);
}
});
});
searchInput.addEventListener('input', (e) => {
state.searchTerm = e.target.value.toLowerCase();
filterAndRenderStories();
});
moreButton.addEventListener('click', loadMoreStories);
}
// Utility function for fetch with timeout
async function fetchWithTimeout(url, timeout = 10000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, { signal: controller.signal });
clearTimeout(timeoutId);
return response;
} catch (error) {
clearTimeout(timeoutId);
throw error;
}
}
// Category Switching
function switchCategory(category) {
if (state.currentCategory === category) return;
console.log(`Switching to category: ${category}`);
state.currentCategory = category;
state.currentPage = 0;
state.stories = [];
state.expandedComments.clear();
state.expandedReplies.clear();
state.loadingComments.clear();
// Update active tab
tabs.forEach(tab => {
tab.classList.toggle('active', tab.dataset.category === category);
});
// Load stories for new category
loadStories(category);
}
// Cache Management
function isCacheValid(key) {
const timestamp = state.cache.timestamps.get(key);
return timestamp && (Date.now() - timestamp < CACHE_EXPIRY);
}
function setCache(key, data) {
state.cache.stories.set(key, data);
state.cache.timestamps.set(key, Date.now());
}
// API Functions with better error handling
async function fetchStoryIds(category) {
const cacheKey = `ids_${category}`;
if (isCacheValid(cacheKey)) {
console.log(`Using cached IDs for ${category}`);
return state.cache.storyIds.get(cacheKey);
}
const endpoints = {
'top': '/topstories.json',
'new': '/newstories.json',
'best': '/beststories.json',
'ask': '/askstories.json',
'show': '/showstories.json',
'jobs': '/jobstories.json'
};
const endpoint = endpoints[category] || endpoints['top'];
const url = `${API_BASE}${endpoint}`;
console.log(`Fetching story IDs from: ${url}`);
try {
const response = await fetchWithTimeout(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const ids = await response.json();
console.log(`Fetched ${ids.length} story IDs for ${category}`);
state.cache.storyIds.set(cacheKey, ids);
state.cache.timestamps.set(cacheKey, Date.now());
return ids;
} catch (error) {
console.error(`Failed to fetch story IDs for ${category}:`, error);
throw new Error(`Failed to fetch ${category} stories: ${error.message}`);
}
}
async function fetchStoryDetails(storyId) {
const cacheKey = `story_${storyId}`;
if (isCacheValid(cacheKey)) {
return state.cache.stories.get(cacheKey);
}
const url = `${API_BASE}/item/${storyId}.json`;
try {
const response = await fetchWithTimeout(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const story = await response.json();
setCache(cacheKey, story);
return story;
} catch (error) {
console.error(`Failed to fetch story ${storyId}:`, error);
throw error;
}
}
async function fetchComment(commentId) {
const cacheKey = `comment_${commentId}`;
if (isCacheValid(cacheKey)) {
return state.cache.comments.get(cacheKey);
}
const url = `${API_BASE}/item/${commentId}.json`;
try {
const response = await fetchWithTimeout(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const comment = await response.json();
state.cache.comments.set(cacheKey, comment);
state.cache.timestamps.set(cacheKey, Date.now());
return comment;
} catch (error) {
console.error(`Failed to fetch comment ${commentId}:`, error);
throw error;
}
}
// Story Loading
async function loadStories(category) {
console.log(`Loading stories for category: ${category}`);
state.isLoading = true;
storiesContainer.innerHTML = '<div class="loading">Loading stories...</div>';
try {
const storyIds = await fetchStoryIds(category);
if (!storyIds || storyIds.length === 0) {
throw new Error('No stories found');
}
state.storyIds = storyIds;
state.hasMore = storyIds.length > state.storiesPerPage;
await loadPage(0);
} catch (error) {
console.error('Error loading stories:', error);
storiesContainer.innerHTML = `
<div class="error">
<div>Failed to load stories: ${error.message}</div>
<button class="retry-button" onclick="loadStories('${category}')">Retry</button>
</div>
`;
} finally {
state.isLoading = false;
}
}
async function loadPage(page) {
const startIndex = page * state.storiesPerPage;
const endIndex = startIndex + state.storiesPerPage;
const pageIds = state.storyIds.slice(startIndex, endIndex);
if (pageIds.length === 0) {
state.hasMore = false;
return;
}
console.log(`Loading page ${page} with ${pageIds.length} stories`);
try {
const storyPromises = pageIds.map(id =>
fetchStoryDetails(id).catch(err => {
console.error(`Failed to fetch story ${id}:`, err);
return null;
})
);
const pageStories = await Promise.all(storyPromises);
const validStories = pageStories.filter(story => story !== null);
console.log(`Successfully loaded ${validStories.length} stories`);
if (page === 0) {
state.stories = validStories;
} else {
state.stories = [...state.stories, ...validStories];
}
state.currentPage = page;
state.hasMore = endIndex < state.storyIds.length;
filterAndRenderStories();
updatePagination();
} catch (error) {
console.error('Error loading page:', error);
storiesContainer.innerHTML = `
<div class="error">
<div>Failed to load stories: ${error.message}</div>
<button class="retry-button" onclick="loadPage(${page})">Retry</button>
</div>
`;
}
}
async function loadMoreStories() {
if (state.isLoading || !state.hasMore) return;
state.isLoading = true;
moreButton.disabled = true;
moreButton.textContent = 'Loading...';
try {
await loadPage(state.currentPage + 1);
} finally {
state.isLoading = false;
moreButton.disabled = false;
moreButton.textContent = 'More';
}
}
function showStatus(message) {
status.textContent = message;
status.classList.add('show');
setTimeout(() => {
status.classList.remove('show');
}, 2000);
}
// Story Rendering
function filterAndRenderStories() {
let filteredStories = state.stories;
if (state.searchTerm) {
filteredStories = state.stories.filter(story =>
story.title.toLowerCase().includes(state.searchTerm) ||
(story.text && story.text.toLowerCase().includes(state.searchTerm))
);
}
renderStories(filteredStories);
}
function renderStories(storiesToRender) {
if (storiesToRender.length === 0) {
storiesContainer.innerHTML = '<div class="loading">No stories found</div>';
return;
}
const storyListHTML = storiesToRender.map((story, index) => {
const domain = getDomain(story.url);
const timeAgo = getTimeAgo(story.time);
const isExpanded = state.expandedComments.has(story.id);
const isLoading = state.loadingComments.has(story.id);
return `
<div class="story-item">
<span class="story-number">${index + 1}.</span>
<div class="story-content">
<a href="${story.url || '#'}" target="_blank" class="story-title">
${escapeHtml(story.title)}
</a>
${domain ? `<span class="story-domain">(${domain})</span>` : ''}
<div class="story-meta">
<span>${story.score || 0} points</span>
<span>by <a href="#">${escapeHtml(story.by || 'unknown')}</a></span>
<span>${timeAgo}</span>
<span>|</span>
<span class="comment-toggle" onclick="toggleComments(${story.id}); return false;">
${isLoading ? 'Loading...' : (isExpanded ? '[-]' : '[+]')} ${story.descendants || 0} comments
</span>
</div>
<div class="comments-section ${isExpanded ? 'active' : ''}" id="comments-${story.id}">
${isExpanded ? renderCommentsContainer(story.id) : ''}
</div>
</div>
</div>
`;
}).join('');
storiesContainer.innerHTML = `<div class="story-list">${storyListHTML}</div>`;
// Re-attach loading states for comments that were being loaded
state.loadingComments.forEach(storyId => {
const commentsSection = document.getElementById(`comments-${storyId}`);
if (commentsSection && !commentsSection.innerHTML) {
commentsSection.innerHTML = renderCommentsContainer(storyId);
}
});
}
function renderCommentsContainer(storyId) {
return `
<div class="comment-container">
<div id="comment-loading-${storyId}" class="loading">Loading comments...</div>
<div id="comment-list-${storyId}" style="display: none;"></div>
</div>
`;
}
function updatePagination() {
if (state.hasMore) {
pagination.style.display = 'block';
} else {
pagination.style.display = 'none';
}
}
// Helper Functions
function getDomain(url) {
if (!url) return null;
try {
const domain = new URL(url).hostname;
return domain.replace('www.', '');
} catch {
return null;
}
}
function getTimeAgo(timestamp) {
const seconds = Math.floor(Date.now() / 1000 - timestamp);
const intervals = {
year: 31536000,
month: 2592000,
week: 604800,
day: 86400,
hour: 3600,
minute: 60
};
for (const [unit, secondsInUnit] of Object.entries(intervals)) {
const interval = Math.floor(seconds / secondsInUnit);
if (interval >= 1) {
return `${interval} ${unit}${interval === 1 ? '' : 's'} ago`;
}
}
return 'just now';
}
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Interactions
async function toggleComments(storyId) {
// Prevent multiple simultaneous loads
if (state.loadingComments.has(storyId)) {
return;
}
const isExpanded = state.expandedComments.has(storyId);
if (isExpanded) {
// Collapse comments
state.expandedComments.delete(storyId);
state.loadingComments.delete(storyId);
const commentsSection = document.getElementById(`comments-${storyId}`);
if (commentsSection) {
commentsSection.classList.remove('active');
}
// Re-render to update toggle state
filterAndRenderStories();
} else {
// Expand and load comments
state.expandedComments.add(storyId);
state.loadingComments.add(storyId);
// Update UI immediately to show loading state
filterAndRenderStories();
// Use requestAnimationFrame to ensure DOM is updated
requestAnimationFrame(() => {
// Ensure comments section exists
const commentsSection = document.getElementById(`comments-${storyId}`);
if (commentsSection) {
commentsSection.classList.add('active');
// Create container if it doesn't exist
if (!commentsSection.innerHTML) {
commentsSection.innerHTML = renderCommentsContainer(storyId);
}
// Load comments
loadTopLevelComments(storyId);
}
});
}
}
async function loadTopLevelComments(storyId) {
let loadingEl = document.getElementById(`comment-loading-${storyId}`);
let listEl = document.getElementById(`comment-list-${storyId}`);
// Wait for elements to be created if they don't exist
let attempts = 0;
while ((!loadingEl || !listEl) && attempts < 10) {
await new Promise(resolve => setTimeout(resolve, 100));
loadingEl = document.getElementById(`comment-loading-${storyId}`);
listEl = document.getElementById(`comment-list-${storyId}`);
attempts++;
}
// If still not found, abort
if (!loadingEl || !listEl) {
console.error(`Comment elements not found for story ${storyId} after 10 attempts`);
state.loadingComments.delete(storyId);
filterAndRenderStories();
return;
}
try {
const story = await fetchStoryDetails(storyId);
if (!story.kids || story.kids.length === 0) {
loadingEl.textContent = 'No comments yet.';
state.loadingComments.delete(storyId);
return;
}
loadingEl.style.display = 'none';
listEl.style.display = 'block';
const commentPromises = story.kids.slice(0, 10).map(id =>
fetchComment(id).catch(err => {
console.error(`Failed to fetch comment ${id}:`, err);
return null;
})
);
const comments = await Promise.all(commentPromises);
const validComments = comments.filter(comment => comment && !comment.deleted);
const commentsHTML = validComments.map(comment =>
renderComment(comment, 0)
).join('');
listEl.innerHTML = commentsHTML;
state.loadingComments.delete(storyId);
} catch (error) {
loadingEl.textContent = 'Failed to load comments.';
state.loadingComments.delete(storyId);
console.error('Error loading comments:', error);
}
}
function renderComment(comment, depth) {
const timeAgo = getTimeAgo(comment.time);
const hasReplies = comment.kids && comment.kids.length > 0;
const isExpanded = state.expandedReplies.has(comment.id);
return `
<div class="comment" style="margin-left: ${depth * 20}px;" data-comment-id="${comment.id}">
<div class="comment-header">
<span>${escapeHtml(comment.by || 'unknown')} ${timeAgo}</span>
${hasReplies ?
`<span class="reply-toggle" onclick="toggleReplies(${comment.id}); return false;">
${isExpanded ? '[-]' : '[+]'} ${comment.kids.length} ${comment.kids.length === 1 ? 'reply' : 'replies'}
</span>` :
''}
</div>
<div class="comment-text">${parseCommentText(comment.text || '')}</div>
${hasReplies && isExpanded ?
`<div class="comment-replies" id="replies-${comment.id}">
<div class="reply-loading">Loading replies...</div>
</div>` :
''}
</div>
`;
}
async function toggleReplies(commentId) {
const repliesEl = document.getElementById(`replies-${commentId}`);
const isExpanded = state.expandedReplies.has(commentId);
if (isExpanded) {
state.expandedReplies.delete(commentId);
if (repliesEl) {
repliesEl.remove();
}
} else {
state.expandedReplies.add(commentId);
await loadReplies(commentId);
}
}
async function loadReplies(commentId) {
let repliesEl = document.getElementById(`replies-${commentId}`);
// Create replies container if it doesn't exist
if (!repliesEl) {
const commentEl = document.querySelector(`[data-comment-id="${commentId}"]`);
if (!commentEl) {
console.error(`Comment element not found for comment ${commentId}`);
return;
}
repliesEl = document.createElement('div');
repliesEl.className = 'comment-replies';
repliesEl.id = `replies-${commentId}`;
repliesEl.innerHTML = '<div class="reply-loading">Loading replies...</div>';
commentEl.appendChild(repliesEl);
}
try {
const comment = await fetchComment(commentId);
if (!comment.kids || comment.kids.length === 0) {
repliesEl.innerHTML = '<div class="comment">No replies.</div>';
return;
}
const replyPromises = comment.kids.slice(0, 5).map(id =>
fetchComment(id).catch(err => {
console.error(`Failed to fetch reply ${id}:`, err);
return null;
})
);
const replies = await Promise.all(replyPromises);
const validReplies = replies.filter(reply => reply && !reply.deleted);
const parentComment = document.querySelector(`[data-comment-id="${commentId}"]`);
const depth = parentComment ? parseInt(parentComment.style.marginLeft) / 20 : 0;
const repliesHTML = validReplies.map(reply =>
renderComment(reply, depth + 1)
).join('');
repliesEl.innerHTML = repliesHTML;
} catch (error) {
repliesEl.innerHTML = '<div class="comment">Failed to load replies.</div>';
console.error('Error loading replies:', error);
}
}
function parseCommentText(text) {
if (!text) return '';
return text
.replace(/<p>/g, '</p><p>')
.replace(/<\/?a[^>]*>/g, '')
.replace(/<i>/g, '<em>')
.replace(/<\/i>/g, '</em>')
.replace(/<code>/g, '')
.replace(/<\/code>/g, '')
.replace(/&#x2F;/g, '/')
.replace(/&#x27;/g, "'")
.replace(/&quot;/g, '"')
.replace(/&gt;/g, '>')
.replace(/&lt;/g, '<')
.replace(/&amp;/g, '&');
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment