|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Markdown Story Viewer</title> |
|
<script src="https://cdn.tailwindcss.com"></script> |
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
|
<style> |
|
.markdown-body { |
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; |
|
line-height: 1.6; |
|
color: #374151; |
|
width: 100%; |
|
max-width: 100%; |
|
overflow-wrap: break-word; |
|
word-wrap: break-word; |
|
hyphens: auto; |
|
} |
|
.markdown-body h1 { |
|
font-size: 2em; |
|
border-bottom: 1px solid #e5e7eb; |
|
padding-bottom: 0.3em; |
|
margin-top: 1em; |
|
margin-bottom: 0.6em; |
|
overflow-wrap: break-word; |
|
} |
|
.markdown-body h2 { |
|
font-size: 1.5em; |
|
border-bottom: 1px solid #e5e7eb; |
|
padding-bottom: 0.3em; |
|
margin-top: 1em; |
|
margin-bottom: 0.6em; |
|
overflow-wrap: break-word; |
|
} |
|
.markdown-body h3 { |
|
font-size: 1.25em; |
|
margin-top: 1em; |
|
margin-bottom: 0.6em; |
|
overflow-wrap: break-word; |
|
} |
|
.markdown-body p { |
|
margin-top: 0.8em; |
|
margin-bottom: 0.8em; |
|
overflow-wrap: break-word; |
|
} |
|
.markdown-body a { |
|
color: #3b82f6; |
|
text-decoration: none; |
|
overflow-wrap: break-word; |
|
} |
|
.markdown-body a:hover { |
|
text-decoration: underline; |
|
} |
|
.markdown-body blockquote { |
|
border-left: 4px solid #e5e7eb; |
|
padding-left: 1em; |
|
color: #6b7280; |
|
margin-left: 0; |
|
margin-right: 0; |
|
overflow-wrap: break-word; |
|
} |
|
.markdown-body code { |
|
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace; |
|
background-color: rgba(175, 184, 193, 0.2); |
|
border-radius: 6px; |
|
padding: 0.2em 0.4em; |
|
font-size: 85%; |
|
overflow-wrap: break-word; |
|
white-space: pre-wrap; |
|
} |
|
.markdown-body pre { |
|
background-color: #f9fafb; |
|
border-radius: 6px; |
|
padding: 1em; |
|
overflow: auto; |
|
line-height: 1.45; |
|
max-width: 100%; |
|
} |
|
.markdown-body pre code { |
|
background-color: transparent; |
|
padding: 0; |
|
border-radius: 0; |
|
white-space: pre; |
|
overflow-x: auto; |
|
display: block; |
|
} |
|
.markdown-body img { |
|
max-width: 100%; |
|
height: auto; |
|
border-radius: 6px; |
|
margin: 1em 0; |
|
} |
|
.markdown-body ul, .markdown-body ol { |
|
padding-left: 2em; |
|
margin-top: 0.8em; |
|
margin-bottom: 0.8em; |
|
overflow-wrap: break-word; |
|
} |
|
.markdown-body li { |
|
margin-bottom: 0.4em; |
|
overflow-wrap: break-word; |
|
} |
|
.markdown-body table { |
|
border-collapse: collapse; |
|
width: 100%; |
|
margin: 1em 0; |
|
display: block; |
|
overflow-x: auto; |
|
white-space: nowrap; |
|
} |
|
.markdown-body th, .markdown-body td { |
|
border: 1px solid #e5e7eb; |
|
padding: 0.5em 1em; |
|
} |
|
.markdown-body th { |
|
background-color: #f9fafb; |
|
font-weight: 600; |
|
} |
|
.markdown-body hr { |
|
border: none; |
|
border-top: 1px solid #e5e7eb; |
|
margin: 1.5em 0; |
|
} |
|
.fade-in { |
|
animation: fadeIn 0.3s ease-in-out; |
|
} |
|
@keyframes fadeIn { |
|
from { opacity: 0; } |
|
to { opacity: 1; } |
|
} |
|
.sidebar-item:hover { |
|
background-color: rgba(59, 130, 246, 0.1); |
|
} |
|
.sidebar-item.active { |
|
background-color: rgba(59, 130, 246, 0.2); |
|
border-left: 3px solid #3b82f6; |
|
} |
|
.content-container { |
|
scroll-behavior: smooth; |
|
overflow-x: hidden; |
|
} |
|
.scroll-top { |
|
opacity: 0; |
|
transition: all 0.3s ease; |
|
} |
|
.scroll-top.visible { |
|
opacity: 1; |
|
} |
|
|
|
.responsive-content { |
|
width: 100%; |
|
padding: 0 1rem; |
|
box-sizing: border-box; |
|
} |
|
@media (min-width: 640px) { |
|
.responsive-content { |
|
padding: 0 2rem; |
|
} |
|
} |
|
@media (min-width: 1024px) { |
|
.responsive-content { |
|
max-width: 80ch; |
|
margin: 0 auto; |
|
} |
|
} |
|
</style> |
|
</head> |
|
<body class="bg-gray-50 min-h-screen"> |
|
<div class="flex flex-col md:flex-row h-screen"> |
|
|
|
<div class="w-full md:w-64 bg-white border-r border-gray-200 overflow-y-auto flex-shrink-0"> |
|
<div class="p-4 border-b border-gray-200"> |
|
<h1 class="text-xl font-bold text-gray-800">Story Collection</h1> |
|
<div class="mt-2 relative"> |
|
<input type="text" id="searchInput" placeholder="Search stories..." |
|
class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"> |
|
<i class="fas fa-search absolute right-3 top-2.5 text-gray-400"></i> |
|
</div> |
|
</div> |
|
<div id="storyList" class="divide-y divide-gray-200"> |
|
|
|
<div class="p-4 text-gray-500 text-center"> |
|
<i class="fas fa-book-open text-2xl mb-2"></i> |
|
<p>No stories loaded</p> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="flex-1 flex flex-col overflow-hidden"> |
|
|
|
<div class="bg-white border-b border-gray-200 p-4 flex justify-between items-center"> |
|
<div class="min-w-0"> |
|
<h2 id="currentStoryTitle" class="text-lg font-semibold text-gray-800 truncate">No Story Selected</h2> |
|
<p id="storyInfo" class="text-sm text-gray-500 truncate">Select a story to begin reading</p> |
|
</div> |
|
<div class="flex space-x-2 flex-shrink-0"> |
|
<button id="fontSizeDown" class="p-2 rounded-md hover:bg-gray-100 text-gray-700"> |
|
<i class="fas fa-font text-sm"></i> <i class="fas fa-minus text-xs"></i> |
|
</button> |
|
<button id="fontSizeUp" class="p-2 rounded-md hover:bg-gray-100 text-gray-700"> |
|
<i class="fas fa-font text-sm"></i> <i class="fas fa-plus text-xs"></i> |
|
</button> |
|
<button id="darkModeToggle" class="p-2 rounded-md hover:bg-gray-100 text-gray-700"> |
|
<i class="fas fa-moon"></i> |
|
</button> |
|
<button id="scrollTopBtn" class="scroll-top p-2 rounded-md hover:bg-gray-100 text-gray-700"> |
|
<i class="fas fa-arrow-up"></i> |
|
</button> |
|
</div> |
|
</div> |
|
|
|
|
|
<div id="contentContainer" class="content-container flex-1 overflow-y-auto bg-white"> |
|
<div id="markdownContent" class="markdown-body responsive-content"> |
|
<div class="text-center py-20 text-gray-400"> |
|
<i class="fas fa-book-open text-5xl mb-4"></i> |
|
<h3 class="text-xl font-medium text-gray-500">Select a story from the sidebar</h3> |
|
<p class="mt-2">Or upload your own Markdown file to read</p> |
|
<button id="uploadBtn" class="mt-4 bg-blue-500 hover:bg-blue-600 text-white font-medium py-2 px-6 rounded-lg transition"> |
|
<i class="fas fa-upload mr-2"></i> Upload Markdown |
|
</button> |
|
<input type="file" id="fileInput" class="hidden" accept=".md,.markdown"> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="bg-white border-t border-gray-200 p-4 text-center text-sm text-gray-500"> |
|
<p>Markdown Story Viewer • Use arrow keys to navigate</p> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
document.addEventListener('DOMContentLoaded', function() { |
|
|
|
const storyList = document.getElementById('storyList'); |
|
const contentContainer = document.getElementById('contentContainer'); |
|
const markdownContent = document.getElementById('markdownContent'); |
|
const currentStoryTitle = document.getElementById('currentStoryTitle'); |
|
const storyInfo = document.getElementById('storyInfo'); |
|
const uploadBtn = document.getElementById('uploadBtn'); |
|
const fileInput = document.getElementById('fileInput'); |
|
const searchInput = document.getElementById('searchInput'); |
|
const fontSizeDown = document.getElementById('fontSizeDown'); |
|
const fontSizeUp = document.getElementById('fontSizeUp'); |
|
const darkModeToggle = document.getElementById('darkModeToggle'); |
|
const scrollTopBtn = document.getElementById('scrollTopBtn'); |
|
|
|
|
|
let stories = []; |
|
let currentStoryIndex = -1; |
|
let fontSize = 16; |
|
let isDarkMode = false; |
|
|
|
|
|
const sampleStories = [ |
|
{ |
|
id: 'sample1', |
|
title: 'The Adventure Begins', |
|
content: `# The Adventure Begins |
|
|
|
## Chapter 1: A Mysterious Letter |
|
|
|
It was a dark and stormy night when the letter arrived. The *wind howled* through the trees as **Professor Langdon** sat by the fireplace in his study. |
|
|
|
> "Strange things are afoot," he muttered to himself, adjusting his spectacles. |
|
|
|
The letter contained only three words: |
|
|
|
1. **Find** |
|
2. The |
|
3. *Orb* |
|
|
|
[Continue reading](#)`, |
|
wordCount: 85, |
|
lastUpdated: '2023-05-15' |
|
}, |
|
{ |
|
id: 'sample2', |
|
title: 'The Lost City', |
|
content: `# The Lost City |
|
|
|
## Discovery in the Desert |
|
|
|
The team had been searching for months when they finally found the entrance to the ancient city. The sandstone walls were covered in strange symbols: |
|
|
|
\`\`\` |
|
▲ ▲ ▼ ▼ ◀ ▶ ◀ ▶ B A |
|
\`\`\` |
|
|
|
### What They Found Inside |
|
|
|
- Golden artifacts |
|
- Ancient scrolls |
|
- A map to... somewhere else |
|
|
|
`, |
|
wordCount: 62, |
|
lastUpdated: '2023-06-22' |
|
} |
|
]; |
|
|
|
|
|
loadSampleStories(); |
|
|
|
|
|
uploadBtn.addEventListener('click', () => fileInput.click()); |
|
fileInput.addEventListener('change', handleFileUpload); |
|
searchInput.addEventListener('input', filterStories); |
|
fontSizeDown.addEventListener('click', decreaseFontSize); |
|
fontSizeUp.addEventListener('click', increaseFontSize); |
|
darkModeToggle.addEventListener('click', toggleDarkMode); |
|
scrollTopBtn.addEventListener('click', scrollToTop); |
|
contentContainer.addEventListener('scroll', handleScroll); |
|
|
|
|
|
window.addEventListener('resize', handleResize); |
|
|
|
|
|
document.addEventListener('keydown', (e) => { |
|
|
|
if (currentStoryIndex === -1) return; |
|
|
|
switch(e.key) { |
|
case 'ArrowUp': |
|
case 'ArrowLeft': |
|
showPreviousStory(); |
|
break; |
|
case 'ArrowDown': |
|
case 'ArrowRight': |
|
showNextStory(); |
|
break; |
|
case 'Home': |
|
scrollToTop(); |
|
break; |
|
case 'End': |
|
scrollToBottom(); |
|
break; |
|
case '+': |
|
increaseFontSize(); |
|
break; |
|
case '-': |
|
decreaseFontSize(); |
|
break; |
|
case 'd': |
|
case 'D': |
|
if (e.ctrlKey) toggleDarkMode(); |
|
break; |
|
} |
|
}); |
|
|
|
|
|
function loadSampleStories() { |
|
stories = [...sampleStories]; |
|
updateStoryList(); |
|
} |
|
|
|
|
|
function handleFileUpload() { |
|
const file = fileInput.files[0]; |
|
if (!file) return; |
|
|
|
const reader = new FileReader(); |
|
reader.onload = (e) => { |
|
const content = e.target.result; |
|
const wordCount = content.split(/\s+/).length; |
|
|
|
const newStory = { |
|
id: 'uploaded-' + Date.now(), |
|
title: file.name.replace(/\.[^/.]+$/, ""), |
|
content: content, |
|
wordCount: wordCount, |
|
lastUpdated: new Date().toISOString().split('T')[0] |
|
}; |
|
|
|
stories.unshift(newStory); |
|
updateStoryList(); |
|
showStory(0); |
|
}; |
|
reader.readAsText(file); |
|
} |
|
|
|
|
|
function updateStoryList() { |
|
storyList.innerHTML = ''; |
|
|
|
if (stories.length === 0) { |
|
storyList.innerHTML = ` |
|
<div class="p-4 text-gray-500 text-center"> |
|
<i class="fas fa-book-open text-2xl mb-2"></i> |
|
<p>No stories available</p> |
|
</div> |
|
`; |
|
return; |
|
} |
|
|
|
stories.forEach((story, index) => { |
|
const storyElement = document.createElement('div'); |
|
storyElement.className = `sidebar-item p-4 cursor-pointer ${index === currentStoryIndex ? 'active' : ''}`; |
|
storyElement.innerHTML = ` |
|
<h3 class="font-medium text-gray-800 truncate">${story.title}</h3> |
|
<div class="flex justify-between items-center mt-1"> |
|
<span class="text-xs text-gray-500">${story.wordCount} words</span> |
|
<span class="text-xs text-gray-400">${story.lastUpdated}</span> |
|
</div> |
|
`; |
|
|
|
storyElement.addEventListener('click', () => { |
|
showStory(index); |
|
}); |
|
|
|
storyList.appendChild(storyElement); |
|
}); |
|
} |
|
|
|
|
|
function filterStories() { |
|
const searchTerm = searchInput.value.toLowerCase(); |
|
|
|
document.querySelectorAll('.sidebar-item').forEach((item, index) => { |
|
const story = stories[index]; |
|
const matches = story.title.toLowerCase().includes(searchTerm) || |
|
story.content.toLowerCase().includes(searchTerm); |
|
|
|
item.style.display = matches ? 'block' : 'none'; |
|
}); |
|
} |
|
|
|
|
|
function showStory(index) { |
|
if (index < 0 || index >= stories.length) return; |
|
|
|
currentStoryIndex = index; |
|
const story = stories[index]; |
|
|
|
|
|
document.querySelectorAll('.sidebar-item').forEach((item, i) => { |
|
if (i === index) { |
|
item.classList.add('active'); |
|
} else { |
|
item.classList.remove('active'); |
|
} |
|
}); |
|
|
|
|
|
currentStoryTitle.textContent = story.title; |
|
storyInfo.textContent = `${story.wordCount} words • Last updated ${story.lastUpdated}`; |
|
|
|
|
|
const htmlContent = convertMarkdownToHtml(story.content); |
|
|
|
|
|
markdownContent.innerHTML = htmlContent; |
|
markdownContent.style.fontSize = `${fontSize}px`; |
|
|
|
|
|
handleResize(); |
|
|
|
|
|
scrollToTop(); |
|
} |
|
|
|
|
|
function convertMarkdownToHtml(markdown) { |
|
|
|
let html = markdown |
|
.replace(/^# (.*$)/gm, '<h1>$1</h1>') |
|
.replace(/^## (.*$)/gm, '<h2>$1</h2>') |
|
.replace(/^### (.*$)/gm, '<h3>$1</h3>') |
|
.replace(/^#### (.*$)/gm, '<h4>$1</h4>'); |
|
|
|
|
|
html = html |
|
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') |
|
.replace(/\*(.*?)\*/g, '<em>$1</em>') |
|
.replace(/\_(.*?)\_/g, '<em>$1</em>'); |
|
|
|
|
|
html = html.replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2">$1</a>'); |
|
|
|
|
|
html = html.replace(/!\[(.*?)\]\((.*?)\)/g, '<img src="$2" alt="$1" class="rounded shadow max-w-full h-auto">'); |
|
|
|
|
|
html = html.replace(/^\> (.*$)/gm, '<blockquote>$1</blockquote>'); |
|
|
|
|
|
html = html.replace(/^\* (.*$)/gm, '<li>$1</li>'); |
|
html = html.replace(/^\- (.*$)/gm, '<li>$1</li>'); |
|
html = html.replace(/^\+ (.*$)/gm, '<li>$1</li>'); |
|
|
|
|
|
html = html.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>'); |
|
html = html.replace(/`(.*?)`/g, '<code>$1</code>'); |
|
|
|
|
|
html = html.replace(/(^|\n)([^\n].+?)(\n|$)/g, function(match, p1, p2, p3) { |
|
|
|
if (p2.startsWith('<') || p2.trim() === '') return match; |
|
return p1 + '<p>' + p2 + '</p>' + p3; |
|
}); |
|
|
|
|
|
html = html.replace(/^\-\-\-$/gm, '<hr>'); |
|
|
|
return html; |
|
} |
|
|
|
|
|
function showPreviousStory() { |
|
if (currentStoryIndex > 0) { |
|
showStory(currentStoryIndex - 1); |
|
} |
|
} |
|
|
|
function showNextStory() { |
|
if (currentStoryIndex < stories.length - 1) { |
|
showStory(currentStoryIndex + 1); |
|
} |
|
} |
|
|
|
|
|
function increaseFontSize() { |
|
if (fontSize < 24) { |
|
fontSize += 1; |
|
markdownContent.style.fontSize = `${fontSize}px`; |
|
handleResize(); |
|
} |
|
} |
|
|
|
function decreaseFontSize() { |
|
if (fontSize > 12) { |
|
fontSize -= 1; |
|
markdownContent.style.fontSize = `${fontSize}px`; |
|
handleResize(); |
|
} |
|
} |
|
|
|
|
|
function toggleDarkMode() { |
|
isDarkMode = !isDarkMode; |
|
|
|
if (isDarkMode) { |
|
document.body.classList.add('bg-gray-900'); |
|
document.body.classList.remove('bg-gray-50'); |
|
markdownContent.classList.add('text-gray-200'); |
|
markdownContent.classList.remove('text-gray-800'); |
|
darkModeToggle.innerHTML = '<i class="fas fa-sun"></i>'; |
|
|
|
|
|
const elements = ['h1', 'h2', 'h3', 'h4', 'p', 'li', 'blockquote', 'code', 'a']; |
|
elements.forEach(el => { |
|
document.querySelectorAll(`.markdown-body ${el}`).forEach(item => { |
|
if (el === 'a') { |
|
item.classList.add('text-blue-400'); |
|
item.classList.remove('text-blue-600'); |
|
} else if (el === 'code') { |
|
|
|
} else { |
|
item.classList.add('text-gray-300'); |
|
item.classList.remove('text-gray-800'); |
|
} |
|
}); |
|
}); |
|
|
|
document.querySelectorAll('.markdown-body pre').forEach(pre => { |
|
pre.classList.add('bg-gray-800'); |
|
pre.classList.remove('bg-gray-100'); |
|
}); |
|
} else { |
|
document.body.classList.remove('bg-gray-900'); |
|
document.body.classList.add('bg-gray-50'); |
|
markdownContent.classList.remove('text-gray-200'); |
|
markdownContent.classList.add('text-gray-800'); |
|
darkModeToggle.innerHTML = '<i class="fas fa-moon"></i>'; |
|
|
|
|
|
const elements = ['h1', 'h2', 'h3', 'h4', 'p', 'li', 'blockquote', 'code', 'a']; |
|
elements.forEach(el => { |
|
document.querySelectorAll(`.markdown-body ${el}`).forEach(item => { |
|
if (el === 'a') { |
|
item.classList.remove('text-blue-400'); |
|
item.classList.add('text-blue-600'); |
|
} else if (el === 'code') { |
|
|
|
} else { |
|
item.classList.remove('text-gray-300'); |
|
item.classList.add('text-gray-800'); |
|
} |
|
}); |
|
}); |
|
|
|
document.querySelectorAll('.markdown-body pre').forEach(pre => { |
|
pre.classList.remove('bg-gray-800'); |
|
pre.classList.add('bg-gray-100'); |
|
}); |
|
} |
|
} |
|
|
|
|
|
function scrollToTop() { |
|
contentContainer.scrollTo({ |
|
top: 0, |
|
behavior: 'smooth' |
|
}); |
|
} |
|
|
|
function scrollToBottom() { |
|
contentContainer.scrollTo({ |
|
top: contentContainer.scrollHeight, |
|
behavior: 'smooth' |
|
}); |
|
} |
|
|
|
function handleScroll() { |
|
|
|
if (contentContainer.scrollTop > 300) { |
|
scrollTopBtn.classList.add('visible'); |
|
} else { |
|
scrollTopBtn.classList.remove('visible'); |
|
} |
|
} |
|
|
|
|
|
function handleResize() { |
|
|
|
document.querySelectorAll('.markdown-body pre').forEach(pre => { |
|
pre.style.maxWidth = '100%'; |
|
pre.style.overflowX = 'auto'; |
|
}); |
|
|
|
|
|
document.querySelectorAll('.markdown-body img').forEach(img => { |
|
img.style.maxWidth = '100%'; |
|
img.style.height = 'auto'; |
|
}); |
|
|
|
|
|
document.querySelectorAll('.markdown-body table').forEach(table => { |
|
table.style.display = 'block'; |
|
table.style.overflowX = 'auto'; |
|
table.style.whiteSpace = 'nowrap'; |
|
}); |
|
} |
|
}); |
|
</script> |
|
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=jsfs11/md-reader" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
|
</html> |