1/** 2 * Help/Documentation panel module 3 * Loads markdown files from /docs/ and renders them using marked.js 4 * Panel opens without blocking editor interaction 5 */ 6 7const DOCS_BASE = '/docs/'; 8 9// Image paths in the markdown are relative to the .md file. 10function resolveDocImages(html, filename) { 11 const dir = DOCS_BASE + filename.replace(/[^/]*$/, ''); 12 return html.replace(/(<img[^>]*\ssrc=")(?!https?:|\/)/g, `$1${dir}`); 13} 14 15let docsDrawer = null; 16let docsContent = null; 17let docsNav = null; 18let docsBackBtn = null; 19let docsCloseBtn = null; 20let helpBtn = null; 21 22let currentDoc = 'index.md'; 23let history = []; 24let docCache = {}; 25 26/** 27 * Check if drawer is open 28 */ 29function isOpen() { 30 return docsDrawer && docsDrawer.classList.contains('open'); 31} 32 33/** 34 * Open the help drawer 35 */ 36export function openDocs() { 37 if (docsDrawer) { 38 docsDrawer.classList.add('open'); 39 docsDrawer.setAttribute('aria-hidden', 'false'); 40 // Load initial doc if not loaded 41 if (!docsContent.innerHTML || docsContent.querySelector('.docs-loading')) { 42 loadDoc(currentDoc); 43 } 44 } 45} 46 47/** 48 * Close the help drawer 49 */ 50export function closeDocs() { 51 if (docsDrawer) { 52 docsDrawer.classList.remove('open'); 53 docsDrawer.setAttribute('aria-hidden', 'true'); 54 } 55} 56 57/** 58 * Toggle the help drawer 59 */ 60export function toggleDocs() { 61 if (isOpen()) { 62 closeDocs(); 63 } else { 64 openDocs(); 65 } 66} 67 68/** 69 * Load and render a markdown document 70 */ 71async function loadDoc(filename, addToHistory = true) { 72 if (!docsContent) return; 73 74 // Update navigation 75 updateNavActive(filename); 76 77 // Show loading state 78 docsContent.innerHTML = '<div class="docs-loading">ÐагÑÑзка...</div>'; 79 80 try { 81 const { loadMarked } = await import('./vendor.js'); 82 const marked = await loadMarked(); 83 let markdown; 84 85 // Check cache first 86 if (docCache[filename]) { 87 markdown = docCache[filename]; 88 } else { 89 const response = await fetch(DOCS_BASE + filename); 90 if (!response.ok) { 91 throw new Error(`HTTP ${response.status}`); 92 } 93 markdown = await response.text(); 94 docCache[filename] = markdown; 95 } 96 97 // Render markdown 98 if (typeof marked !== 'undefined') { 99 // Configure marked for proper rendering 100 marked.setOptions({ 101 breaks: false, 102 gfm: true, 103 headerIds: true, 104 mangle: false 105 }); 106 docsContent.innerHTML = resolveDocImages(marked.parse(markdown), filename); 107 } else { 108 // Fallback: show raw markdown 109 docsContent.innerHTML = `<pre>${escapeHtml(markdown)}</pre>`; 110 } 111 112 // Handle internal links to other .md files 113 docsContent.querySelectorAll('a').forEach(link => { 114 const href = link.getAttribute('href'); 115 if (href && href.endsWith('.md') && !href.startsWith('http')) { 116 link.addEventListener('click', (e) => { 117 e.preventDefault(); 118 loadDoc(href, true); 119 }); 120 } 121 }); 122 123 // Scroll to top 124 docsContent.scrollTop = 0; 125 126 // Update history 127 if (addToHistory && currentDoc !== filename) { 128 history.push(currentDoc); 129 } 130 currentDoc = filename; 131 updateBackButton(); 132 133 } catch (err) { 134 console.error('Failed to load doc:', filename, err); 135 docsContent.innerHTML = `<div class="docs-error"> 136 <p>Ðе ÑдалоÑÑ Ð·Ð°Ð³ÑÑзиÑÑ Ð´Ð¾ÐºÑменÑ</p> 137 <p><small>${escapeHtml(err.message)}</small></p> 138 </div>`; 139 } 140} 141 142/** 143 * Go back in history 144 */ 145function goBack() { 146 if (history.length > 0) { 147 const prev = history.pop(); 148 loadDoc(prev, false); 149 } 150} 151 152/** 153 * Update the active state of navigation links 154 */ 155function updateNavActive(filename) { 156 if (!docsNav) return; 157 docsNav.querySelectorAll('.docs-nav-link').forEach(link => { 158 const doc = link.getAttribute('data-doc'); 159 if (doc === filename) { 160 link.classList.add('active'); 161 } else { 162 link.classList.remove('active'); 163 } 164 }); 165} 166 167/** 168 * Update back button state 169 */ 170function updateBackButton() { 171 if (docsBackBtn) { 172 docsBackBtn.disabled = history.length === 0; 173 } 174} 175 176/** 177 * Escape HTML entities 178 */ 179function escapeHtml(text) { 180 const div = document.createElement('div'); 181 div.textContent = text; 182 return div.innerHTML; 183} 184 185/** 186 * Initialize the help panel 187 */ 188export function initDocs({ bindOpenButtons = true } = {}) { 189 docsDrawer = document.getElementById('docs-drawer'); 190 docsContent = document.getElementById('docs-content'); 191 docsNav = document.getElementById('docs-nav'); 192 docsBackBtn = document.getElementById('docs-back'); 193 docsCloseBtn = document.getElementById('docs-close'); 194 helpBtn = document.getElementById('btn-help'); 195 196 if (!docsDrawer || !docsContent) { 197 console.warn('Help panel elements not found'); 198 return; 199 } 200 201 // Help button in header 202 if (bindOpenButtons && helpBtn) { 203 helpBtn.addEventListener('click', toggleDocs); 204 } 205 206 // Docs page button in footer - opens full docs page 207 const docsPageBtn = document.getElementById('docs-page-btn'); 208 if (bindOpenButtons && docsPageBtn) { 209 docsPageBtn.addEventListener('click', () => { 210 window.open('/docs.html', '_blank'); 211 }); 212 } 213 214 // Close button 215 if (docsCloseBtn) { 216 docsCloseBtn.addEventListener('click', closeDocs); 217 } 218 219 // Back button 220 if (docsBackBtn) { 221 docsBackBtn.addEventListener('click', goBack); 222 } 223 224 // Navigation links 225 if (docsNav) { 226 docsNav.querySelectorAll('.docs-nav-link').forEach(link => { 227 link.addEventListener('click', (e) => { 228 e.preventDefault(); 229 const doc = link.getAttribute('data-doc'); 230 if (doc) { 231 loadDoc(doc, true); 232 } 233 }); 234 }); 235 } 236 237 // Escape key to close (optional, since panel doesn't block) 238 document.addEventListener('keydown', (e) => { 239 if (e.key === 'Escape' && isOpen()) { 240 closeDocs(); 241 } 242 }); 243}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.