PageSourceSearch

https://qumir.dev/docs.js

js qumir.dev collected 2026-10-06 06:53:56 UTC 5,837 bytes, 243 lines download raw bytes

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.