md-reader / index.html
jsfs11's picture
Add 2 files
7f76615 verified
<!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 scaling */
.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">
<!-- Sidebar -->
<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">
<!-- Stories will be loaded here -->
<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>
<!-- Main content -->
<div class="flex-1 flex flex-col overflow-hidden">
<!-- Toolbar -->
<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>
<!-- Content area -->
<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>
<!-- Footer -->
<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() {
// DOM elements
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');
// State variables
let stories = [];
let currentStoryIndex = -1;
let fontSize = 16; // base font size in px
let isDarkMode = false;
// Sample stories (could be loaded from an API or localStorage)
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
![Ancient City](https://images.unsplash.com/photo-1467269204594-9661b134dd2b?w=800&auto=format)`,
wordCount: 62,
lastUpdated: '2023-06-22'
}
];
// Initialize with sample stories
loadSampleStories();
// Event listeners
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);
// Handle window resize
window.addEventListener('resize', handleResize);
// Keyboard navigation
document.addEventListener('keydown', (e) => {
// Only handle key events when a story is selected
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;
}
});
// Load sample stories
function loadSampleStories() {
stories = [...sampleStories];
updateStoryList();
}
// Handle file upload
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(/\.[^/.]+$/, ""), // Remove extension
content: content,
wordCount: wordCount,
lastUpdated: new Date().toISOString().split('T')[0]
};
stories.unshift(newStory);
updateStoryList();
showStory(0); // Show the newly uploaded story
};
reader.readAsText(file);
}
// Update the story list in sidebar
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);
});
}
// Filter stories based on search input
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';
});
}
// Show story at specified index
function showStory(index) {
if (index < 0 || index >= stories.length) return;
currentStoryIndex = index;
const story = stories[index];
// Update active item in sidebar
document.querySelectorAll('.sidebar-item').forEach((item, i) => {
if (i === index) {
item.classList.add('active');
} else {
item.classList.remove('active');
}
});
// Update header
currentStoryTitle.textContent = story.title;
storyInfo.textContent = `${story.wordCount} words • Last updated ${story.lastUpdated}`;
// Convert markdown to HTML (simplified - in a real app you might use a library like marked.js)
const htmlContent = convertMarkdownToHtml(story.content);
// Display the content
markdownContent.innerHTML = htmlContent;
markdownContent.style.fontSize = `${fontSize}px`;
// Ensure all content fits without horizontal scrolling
handleResize();
// Scroll to top
scrollToTop();
}
// Simple markdown to HTML converter (basic functionality)
function convertMarkdownToHtml(markdown) {
// Headers
let html = markdown
.replace(/^# (.*$)/gm, '<h1>$1</h1>')
.replace(/^## (.*$)/gm, '<h2>$1</h2>')
.replace(/^### (.*$)/gm, '<h3>$1</h3>')
.replace(/^#### (.*$)/gm, '<h4>$1</h4>');
// Bold and italic
html = html
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.*?)\*/g, '<em>$1</em>')
.replace(/\_(.*?)\_/g, '<em>$1</em>');
// Links
html = html.replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2">$1</a>');
// Images
html = html.replace(/!\[(.*?)\]\((.*?)\)/g, '<img src="$2" alt="$1" class="rounded shadow max-w-full h-auto">');
// Blockquotes
html = html.replace(/^\> (.*$)/gm, '<blockquote>$1</blockquote>');
// Lists
html = html.replace(/^\* (.*$)/gm, '<li>$1</li>');
html = html.replace(/^\- (.*$)/gm, '<li>$1</li>');
html = html.replace(/^\+ (.*$)/gm, '<li>$1</li>');
// Code blocks
html = html.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>');
html = html.replace(/`(.*?)`/g, '<code>$1</code>');
// Paragraphs (handle line breaks)
html = html.replace(/(^|\n)([^\n].+?)(\n|$)/g, function(match, p1, p2, p3) {
// Don't wrap already wrapped elements
if (p2.startsWith('<') || p2.trim() === '') return match;
return p1 + '<p>' + p2 + '</p>' + p3;
});
// Horizontal rule
html = html.replace(/^\-\-\-$/gm, '<hr>');
return html;
}
// Navigation functions
function showPreviousStory() {
if (currentStoryIndex > 0) {
showStory(currentStoryIndex - 1);
}
}
function showNextStory() {
if (currentStoryIndex < stories.length - 1) {
showStory(currentStoryIndex + 1);
}
}
// Font size adjustment
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();
}
}
// Dark mode toggle
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>';
// Update all markdown elements
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') {
// No change for 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>';
// Revert all markdown elements
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') {
// No change for 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');
});
}
}
// Scroll functions
function scrollToTop() {
contentContainer.scrollTo({
top: 0,
behavior: 'smooth'
});
}
function scrollToBottom() {
contentContainer.scrollTo({
top: contentContainer.scrollHeight,
behavior: 'smooth'
});
}
function handleScroll() {
// Show/hide scroll to top button
if (contentContainer.scrollTop > 300) {
scrollTopBtn.classList.add('visible');
} else {
scrollTopBtn.classList.remove('visible');
}
}
// Handle window resize and ensure content fits
function handleResize() {
// Ensure code blocks don't cause horizontal scrolling
document.querySelectorAll('.markdown-body pre').forEach(pre => {
pre.style.maxWidth = '100%';
pre.style.overflowX = 'auto';
});
// Ensure images don't overflow
document.querySelectorAll('.markdown-body img').forEach(img => {
img.style.maxWidth = '100%';
img.style.height = 'auto';
});
// Ensure tables are scrollable if needed
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>