| <!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> |