@dzhechkov/p-replicator 1.5.5 → 1.5.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,553 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * build.js — generates index.html from sibling .md files.
6
+ *
7
+ * Usage: node build.js
8
+ * Reads: ../{README,01_quickstart,02_user_guide,...}.md
9
+ * Writes: ./index.html
10
+ *
11
+ * Zero dependencies — uses only Node built-ins. Inlines a minimal
12
+ * Markdown → HTML parser sufficient for the documentation features
13
+ * actually used in our .md files.
14
+ */
15
+
16
+ const fs = require('node:fs');
17
+ const path = require('node:path');
18
+
19
+ const SOURCE_DIR = path.resolve(__dirname, '..');
20
+ const OUTPUT = path.join(__dirname, 'index.html');
21
+
22
+ const FILES = [
23
+ { id: 'index', source: 'README.md', title: 'Главная', nav: 'Главная' },
24
+ { id: 'quickstart', source: '01_quickstart.md', title: '01. Быстрый старт', nav: '01. Быстрый старт' },
25
+ { id: 'user-guide', source: '02_user_guide.md', title: '02. Руководство пользователя', nav: '02. Руководство' },
26
+ { id: 'admin-guide', source: '03_admin_guide.md', title: '03. Admin Guide', nav: '03. Admin Guide' },
27
+ { id: 'api-reference', source: '04_api_reference.md', title: '04. API Reference', nav: '04. API Reference' },
28
+ { id: 'architecture', source: '05_architecture.md', title: '05. Архитектура', nav: '05. Архитектура' },
29
+ { id: 'troubleshooting',source: '06_troubleshooting.md',title: '06. Troubleshooting', nav: '06. Troubleshooting' },
30
+ { id: 'changelog', source: '07_changelog.md', title: '07. Changelog', nav: '07. Changelog' },
31
+ ];
32
+
33
+ // ─── Cyrillic → Latin transliteration for slugs ────────────────────────────
34
+ const TRANS = {
35
+ 'а':'a','б':'b','в':'v','г':'g','д':'d','е':'e','ё':'yo','ж':'zh','з':'z',
36
+ 'и':'i','й':'y','к':'k','л':'l','м':'m','н':'n','о':'o','п':'p','р':'r',
37
+ 'с':'s','т':'t','у':'u','ф':'f','х':'h','ц':'ts','ч':'ch','ш':'sh','щ':'sch',
38
+ 'ъ':'','ы':'y','ь':'','э':'e','ю':'yu','я':'ya',
39
+ };
40
+ function transliterate(s) {
41
+ return s.toLowerCase().split('').map((c) => TRANS[c] !== undefined ? TRANS[c] : c).join('');
42
+ }
43
+ function slugify(text) {
44
+ return transliterate(text)
45
+ .toLowerCase()
46
+ .replace(/[^a-z0-9\s-]/g, '')
47
+ .replace(/\s+/g, '-')
48
+ .replace(/-+/g, '-')
49
+ .replace(/^-|-$/g, '')
50
+ .slice(0, 80);
51
+ }
52
+
53
+ // ─── Section map: source filename → section id (built lazily after FILES) ─
54
+ const SECTION_MAP = new Map();
55
+ function buildSectionMap() {
56
+ if (SECTION_MAP.size > 0) return;
57
+ for (const f of FILES) {
58
+ SECTION_MAP.set(f.source, f.id);
59
+ }
60
+ }
61
+
62
+ // ─── Rewrite cross-section .md links to in-page anchors ──────────────────
63
+ // Returns rewritten URL, or null if URL should be left untouched.
64
+ function rewriteInternalLink(url) {
65
+ // External / mailto / pure-anchor URLs — leave alone
66
+ if (/^https?:/i.test(url) || /^mailto:/i.test(url) || url.startsWith('#')) {
67
+ return null;
68
+ }
69
+ buildSectionMap();
70
+
71
+ // Strip leading ./
72
+ let p = url.startsWith('./') ? url.slice(2) : url;
73
+
74
+ // Cross-language link to English README (no English HTML exists yet) → GitHub blob URL
75
+ if (p.startsWith('../eng/')) {
76
+ const tail = p.slice('../'.length); // "eng/README.md"
77
+ return `https://github.com/dzhechko/pu-unicorn-replicate/blob/main/packages/p-replicator/README/${tail}`;
78
+ }
79
+
80
+ // Same-folder .md reference: "<filename>.md" or "<filename>.md#<fragment>"
81
+ const m = p.match(/^([^#?]+\.md)(?:#(.*))?$/);
82
+ if (!m) return null;
83
+
84
+ const filename = m[1];
85
+ const sectionId = SECTION_MAP.get(filename);
86
+ if (!sectionId) {
87
+ // .md file outside the section index (e.g., KNOWN_LIMITATIONS.md, CHANGELOG.md,
88
+ // .claude/commands/replicate.md) — point at the GitHub source.
89
+ if (filename.match(/^\.claude\//) || /^[A-Z_]+\.md$/.test(filename) || filename === 'CHANGELOG.md') {
90
+ return `https://github.com/dzhechko/pu-unicorn-replicate/blob/main/packages/p-replicator/${p}`;
91
+ }
92
+ return null;
93
+ }
94
+
95
+ if (m[2]) {
96
+ // Anchor present — fragment may be cyrillic; slugify to match heading id
97
+ const fragment = decodeURIComponent(m[2]);
98
+ const slug = slugify(fragment);
99
+ return `#${sectionId}-${slug}`;
100
+ }
101
+ return `#${sectionId}`;
102
+ }
103
+
104
+ // ─── HTML escape ──────────────────────────────────────────────────────────
105
+ function escapeHtml(text) {
106
+ return String(text).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
107
+ }
108
+ function escapeAttr(text) {
109
+ return escapeHtml(text).replace(/"/g, '&quot;');
110
+ }
111
+
112
+ // ─── Inline markdown ──────────────────────────────────────────────────────
113
+ function processInline(text) {
114
+ // 1. Stash inline code so its content isn't processed further
115
+ const codes = [];
116
+ text = text.replace(/`([^`]+)`/g, (_, c) => {
117
+ codes.push(escapeHtml(c));
118
+ return `\x00CODE${codes.length - 1}\x00`;
119
+ });
120
+
121
+ // 2. Stash links (so brackets aren't escaped)
122
+ const links = [];
123
+ text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, url) => {
124
+ links.push({ label, url });
125
+ return `\x00LINK${links.length - 1}\x00`;
126
+ });
127
+
128
+ // 3. Escape remaining HTML entities
129
+ text = escapeHtml(text);
130
+
131
+ // 4. Bold + italic + strikethrough on the escaped text
132
+ text = text.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
133
+ text = text.replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, '<em>$1</em>');
134
+ text = text.replace(/~~([^~]+)~~/g, '<del>$1</del>');
135
+
136
+ // 5. Restore links (label needs inline processing too — re-run bold/italic/code on it)
137
+ text = text.replace(/\x00LINK(\d+)\x00/g, (_, idx) => {
138
+ const { label, url } = links[idx];
139
+ const safeLabel = escapeHtml(label)
140
+ .replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
141
+ .replace(/(?<!\*)\*([^*]+)\*(?!\*)/g, '<em>$1</em>');
142
+ // Cross-section .md links → in-page anchors. External URLs stay as-is.
143
+ const rewritten = rewriteInternalLink(url);
144
+ const finalUrl = rewritten !== null ? rewritten : url;
145
+ const isExternal = /^https?:/i.test(finalUrl);
146
+ const attrs = isExternal ? ' target="_blank" rel="noopener noreferrer"' : '';
147
+ return `<a href="${escapeAttr(finalUrl)}"${attrs}>${safeLabel}</a>`;
148
+ });
149
+
150
+ // 6. Restore code
151
+ text = text.replace(/\x00CODE(\d+)\x00/g, (_, idx) => `<code>${codes[idx]}</code>`);
152
+
153
+ return text;
154
+ }
155
+
156
+ // ─── Block parsers ────────────────────────────────────────────────────────
157
+ function renderTable(lines) {
158
+ if (lines.length < 2) return '';
159
+ const splitRow = (l) => l.replace(/^\||\|$/g, '').split('|').map((s) => s.trim());
160
+ const header = splitRow(lines[0]);
161
+ const rows = lines.slice(2).map(splitRow);
162
+
163
+ let html = '<div class="table-wrap"><table><thead><tr>';
164
+ for (const h of header) html += `<th>${processInline(h)}</th>`;
165
+ html += '</tr></thead><tbody>';
166
+ for (const row of rows) {
167
+ html += '<tr>';
168
+ for (const cell of row) html += `<td>${processInline(cell)}</td>`;
169
+ html += '</tr>';
170
+ }
171
+ html += '</tbody></table></div>';
172
+ return html;
173
+ }
174
+
175
+ function renderList(items, tag) {
176
+ let html = `<${tag}>`;
177
+ for (const item of items) {
178
+ html += `<li>${processInline(item)}</li>`;
179
+ }
180
+ html += `</${tag}>`;
181
+ return html;
182
+ }
183
+
184
+ function renderCode(code, lang) {
185
+ return `<pre data-lang="${escapeAttr(lang)}"><code class="language-${escapeAttr(lang || 'text')}">${escapeHtml(code)}</code></pre>`;
186
+ }
187
+
188
+ // ─── Main parser ──────────────────────────────────────────────────────────
189
+ function parseMarkdown(md, idPrefix = '') {
190
+ // Normalize line endings
191
+ const lines = md.replace(/\r\n/g, '\n').split('\n');
192
+ const out = [];
193
+ let i = 0;
194
+
195
+ function isBlockBoundary(line) {
196
+ return (
197
+ /^#{1,6}\s/.test(line) ||
198
+ /^---+\s*$/.test(line) ||
199
+ /^[-*+]\s+/.test(line) ||
200
+ /^\d+\.\s+/.test(line) ||
201
+ /^```/.test(line) ||
202
+ /^\|/.test(line) ||
203
+ /^>\s/.test(line) ||
204
+ line.trim() === ''
205
+ );
206
+ }
207
+
208
+ while (i < lines.length) {
209
+ const line = lines[i];
210
+
211
+ // ─── Code block (```)
212
+ if (/^```/.test(line)) {
213
+ const lang = line.slice(3).trim();
214
+ const codeLines = [];
215
+ i++;
216
+ while (i < lines.length && !/^```\s*$/.test(lines[i])) {
217
+ codeLines.push(lines[i]);
218
+ i++;
219
+ }
220
+ i++; // skip closing ```
221
+ out.push(renderCode(codeLines.join('\n'), lang));
222
+ continue;
223
+ }
224
+
225
+ // ─── Heading
226
+ const h = line.match(/^(#{1,6})\s+(.+?)\s*#*\s*$/);
227
+ if (h) {
228
+ const level = h[1].length;
229
+ const text = h[2].trim();
230
+ const baseId = slugify(text);
231
+ const id = idPrefix && level > 1 ? `${idPrefix}-${baseId}` : baseId || idPrefix || 'section';
232
+ out.push(`<h${level} id="${escapeAttr(id)}">${processInline(text)}</h${level}>`);
233
+ i++;
234
+ continue;
235
+ }
236
+
237
+ // ─── HR
238
+ if (/^---+\s*$/.test(line)) {
239
+ out.push('<hr>');
240
+ i++;
241
+ continue;
242
+ }
243
+
244
+ // ─── Blockquote
245
+ if (/^>\s?/.test(line)) {
246
+ const qLines = [];
247
+ while (i < lines.length && /^>\s?/.test(lines[i])) {
248
+ qLines.push(lines[i].replace(/^>\s?/, ''));
249
+ i++;
250
+ }
251
+ out.push(`<blockquote>${parseMarkdown(qLines.join('\n'), idPrefix)}</blockquote>`);
252
+ continue;
253
+ }
254
+
255
+ // ─── Table
256
+ if (/^\|.*\|/.test(line) && i + 1 < lines.length && /^\|[\s:|-]+\|/.test(lines[i + 1])) {
257
+ const tableLines = [];
258
+ while (i < lines.length && /^\|/.test(lines[i])) {
259
+ tableLines.push(lines[i]);
260
+ i++;
261
+ }
262
+ out.push(renderTable(tableLines));
263
+ continue;
264
+ }
265
+
266
+ // ─── Unordered list
267
+ if (/^[-*+]\s+/.test(line)) {
268
+ const items = [];
269
+ while (i < lines.length && /^[-*+]\s+/.test(lines[i])) {
270
+ items.push(lines[i].replace(/^[-*+]\s+/, ''));
271
+ i++;
272
+ // collect indented continuation lines into the previous item
273
+ while (i < lines.length && /^\s{2,}\S/.test(lines[i])) {
274
+ items[items.length - 1] += ' ' + lines[i].replace(/^\s+/, '');
275
+ i++;
276
+ }
277
+ }
278
+ out.push(renderList(items, 'ul'));
279
+ continue;
280
+ }
281
+
282
+ // ─── Ordered list
283
+ if (/^\d+\.\s+/.test(line)) {
284
+ const items = [];
285
+ while (i < lines.length && /^\d+\.\s+/.test(lines[i])) {
286
+ items.push(lines[i].replace(/^\d+\.\s+/, ''));
287
+ i++;
288
+ while (i < lines.length && /^\s{2,}\S/.test(lines[i])) {
289
+ items[items.length - 1] += ' ' + lines[i].replace(/^\s+/, '');
290
+ i++;
291
+ }
292
+ }
293
+ out.push(renderList(items, 'ol'));
294
+ continue;
295
+ }
296
+
297
+ // ─── Empty line
298
+ if (line.trim() === '') {
299
+ i++;
300
+ continue;
301
+ }
302
+
303
+ // ─── Paragraph (collect until block boundary)
304
+ const para = [line];
305
+ i++;
306
+ while (i < lines.length && !isBlockBoundary(lines[i])) {
307
+ para.push(lines[i]);
308
+ i++;
309
+ }
310
+ out.push(`<p>${processInline(para.join(' ').replace(/\s+/g, ' ').trim())}</p>`);
311
+ }
312
+
313
+ return out.join('\n');
314
+ }
315
+
316
+ // ─── TOC extraction (h2 + h3) ─────────────────────────────────────────────
317
+ function extractToc(html) {
318
+ const items = [];
319
+ const re = /<h(2|3)\s+id="([^"]+)">([\s\S]*?)<\/h\1>/g;
320
+ let m;
321
+ while ((m = re.exec(html)) !== null) {
322
+ items.push({
323
+ level: parseInt(m[1], 10),
324
+ id: m[2],
325
+ // Strip any inline tags from the heading text for TOC display
326
+ text: m[3].replace(/<[^>]+>/g, '').trim(),
327
+ });
328
+ }
329
+ return items;
330
+ }
331
+
332
+ // ─── Build sidebar TOC ────────────────────────────────────────────────────
333
+ function renderSidebarToc(sections) {
334
+ let html = '<ul>';
335
+ for (const s of sections) {
336
+ const subItems = (s.toc || []).filter((t) => t.level === 2);
337
+ html += `<li><a href="#${escapeAttr(s.id)}" class="toc-section">${escapeHtml(s.nav)}</a>`;
338
+ if (subItems.length > 0) {
339
+ html += '<ul>';
340
+ for (const sub of subItems.slice(0, 12)) {
341
+ html += `<li><a href="#${escapeAttr(sub.id)}">${escapeHtml(sub.text)}</a></li>`;
342
+ }
343
+ html += '</ul>';
344
+ }
345
+ html += '</li>';
346
+ }
347
+ html += '</ul>';
348
+ return html;
349
+ }
350
+
351
+ // ─── Render section nav (prev/next) ───────────────────────────────────────
352
+ function renderSectionNav(sections, idx) {
353
+ const prev = idx > 0 ? sections[idx - 1] : null;
354
+ const next = idx < sections.length - 1 ? sections[idx + 1] : null;
355
+ let html = '<nav class="section-nav" aria-label="Навигация по секциям">';
356
+ if (prev) {
357
+ html += `<a href="#${escapeAttr(prev.id)}" class="prev"><span class="nav-arrow">←</span> <span class="nav-text"><span class="nav-label">Предыдущее</span><span class="nav-title">${escapeHtml(prev.nav)}</span></span></a>`;
358
+ } else {
359
+ html += '<span></span>';
360
+ }
361
+ if (next) {
362
+ html += `<a href="#${escapeAttr(next.id)}" class="next"><span class="nav-text"><span class="nav-label">Далее</span><span class="nav-title">${escapeHtml(next.nav)}</span></span> <span class="nav-arrow">→</span></a>`;
363
+ } else {
364
+ html += '<span></span>';
365
+ }
366
+ html += '</nav>';
367
+ return html;
368
+ }
369
+
370
+ // ─── Build sections HTML ──────────────────────────────────────────────────
371
+ function renderSection(s) {
372
+ return `<article id="${escapeAttr(s.id)}" class="section" data-section-title="${escapeAttr(s.title)}">
373
+ ${s.html}
374
+ ${s.nav_html}
375
+ </article>`;
376
+ }
377
+
378
+ // ─── Read + parse all sources ─────────────────────────────────────────────
379
+ console.log('[build] Reading source files from', SOURCE_DIR);
380
+
381
+ const sections = FILES.map((f) => {
382
+ const srcPath = path.join(SOURCE_DIR, f.source);
383
+ const md = fs.readFileSync(srcPath, 'utf8');
384
+ const html = parseMarkdown(md, f.id);
385
+ return {
386
+ ...f,
387
+ html,
388
+ toc: extractToc(html),
389
+ };
390
+ });
391
+
392
+ console.log(`[build] Parsed ${sections.length} sections`);
393
+
394
+ // Add prev/next nav HTML to each section
395
+ sections.forEach((s, idx) => {
396
+ s.nav_html = renderSectionNav(sections, idx);
397
+ });
398
+
399
+ // ─── Final HTML template ──────────────────────────────────────────────────
400
+ const tocHtml = renderSidebarToc(sections);
401
+ const sectionsHtml = sections.map(renderSection).join('\n');
402
+
403
+ const META_DESC = '@dzhechkov/p-replicator — toolkit для AI-assisted разработки в Claude Code (Vibe Coding). 11 slash-команд, 10 skills, hooks, statusline, --feature-branches workflow для обучения. v1.5.0.';
404
+ const META_KEYWORDS = 'p-replicator, claude code, vibe coding, sparc, ai-assisted, npm, dzhechkov, /replicate, /run, /feature, statusline, claude-code-toolkit, prd, sparc-mini, requirements-validator';
405
+
406
+ const STRUCTURED_DATA = JSON.stringify({
407
+ '@context': 'https://schema.org',
408
+ '@type': 'TechArticle',
409
+ 'headline': '@dzhechkov/p-replicator v1.5.0 — Документация',
410
+ 'description': META_DESC,
411
+ 'author': { '@type': 'Person', 'name': 'dzhechko' },
412
+ 'datePublished': '2026-05-07',
413
+ 'inLanguage': 'ru',
414
+ 'about': {
415
+ '@type': 'SoftwareSourceCode',
416
+ 'name': '@dzhechkov/p-replicator',
417
+ 'codeRepository': 'https://github.com/dzhechko/pu-unicorn-replicate',
418
+ 'programmingLanguage': 'JavaScript',
419
+ 'softwareVersion': '1.5.0',
420
+ 'license': 'https://opensource.org/licenses/MIT',
421
+ },
422
+ 'isPartOf': {
423
+ '@type': 'WebSite',
424
+ 'name': '@dzhechkov/p-replicator Documentation',
425
+ 'url': 'https://github.com/dzhechko/pu-unicorn-replicate',
426
+ },
427
+ }, null, 2);
428
+
429
+ const html = `<!DOCTYPE html>
430
+ <html lang="ru" data-theme="auto">
431
+ <head>
432
+ <meta charset="UTF-8">
433
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
434
+ <title>@dzhechkov/p-replicator v1.5.0 — Документация</title>
435
+ <meta name="description" content="${escapeAttr(META_DESC)}">
436
+ <meta name="keywords" content="${escapeAttr(META_KEYWORDS)}">
437
+ <meta name="author" content="dzhechko">
438
+ <meta name="robots" content="index, follow">
439
+
440
+ <!-- Open Graph -->
441
+ <meta property="og:title" content="@dzhechkov/p-replicator v1.5.0">
442
+ <meta property="og:description" content="${escapeAttr(META_DESC)}">
443
+ <meta property="og:type" content="website">
444
+ <meta property="og:locale" content="ru_RU">
445
+ <meta property="og:site_name" content="P-Replicator Docs">
446
+
447
+ <!-- Twitter -->
448
+ <meta name="twitter:card" content="summary">
449
+ <meta name="twitter:title" content="@dzhechkov/p-replicator v1.5.0">
450
+ <meta name="twitter:description" content="${escapeAttr(META_DESC)}">
451
+
452
+ <!-- Theme color (browser chrome) -->
453
+ <meta name="theme-color" content="#0969da" media="(prefers-color-scheme: light)">
454
+ <meta name="theme-color" content="#0d1117" media="(prefers-color-scheme: dark)">
455
+
456
+ <!-- Canonical -->
457
+ <link rel="canonical" href="https://github.com/dzhechko/pu-unicorn-replicate#readme">
458
+
459
+ <!-- Structured data -->
460
+ <script type="application/ld+json">${STRUCTURED_DATA}</script>
461
+
462
+ <!-- Inline critical CSS for instant first paint (avoid FOUC) -->
463
+ <script>
464
+ // Apply theme BEFORE first paint to prevent flash
465
+ (function() {
466
+ try {
467
+ var t = localStorage.getItem('p-replicator-theme') || 'auto';
468
+ if (t !== 'auto') document.documentElement.setAttribute('data-theme', t);
469
+ } catch (_) {}
470
+ })();
471
+ </script>
472
+
473
+ <link rel="stylesheet" href="style.css">
474
+ </head>
475
+ <body>
476
+ <a href="#main" class="skip-to-content">Перейти к содержимому</a>
477
+
478
+ <div class="progress-bar" id="progressBar" aria-hidden="true"></div>
479
+
480
+ <header class="site-header" role="banner">
481
+ <button class="menu-toggle" id="menuToggle" aria-label="Открыть меню" aria-expanded="false">
482
+ <span aria-hidden="true">☰</span>
483
+ </button>
484
+ <a href="#index" class="site-title">
485
+ <strong>P-Replicator</strong>
486
+ <span class="version">v1.5.0</span>
487
+ </a>
488
+ <nav class="header-nav" aria-label="Внешние ссылки">
489
+ <a href="https://www.npmjs.com/package/@dzhechkov/p-replicator" target="_blank" rel="noopener noreferrer">npm</a>
490
+ <a href="https://github.com/dzhechko/pu-unicorn-replicate" target="_blank" rel="noopener noreferrer">GitHub</a>
491
+ <a href="https://t.me/llm_notes" target="_blank" rel="noopener noreferrer">Telegram</a>
492
+ </nav>
493
+ <button class="theme-toggle" id="themeToggle" aria-label="Переключить тему">
494
+ <span aria-hidden="true">🌙</span>
495
+ </button>
496
+ </header>
497
+
498
+ <div class="layout">
499
+ <aside class="sidebar" id="sidebar" aria-label="Содержание">
500
+ <div class="search-wrap">
501
+ <input type="search" class="search-input" id="searchInput"
502
+ placeholder="🔍 Поиск по документации"
503
+ aria-label="Поиск по документации"
504
+ autocomplete="off"
505
+ spellcheck="false">
506
+ <kbd class="search-kbd">/</kbd>
507
+ </div>
508
+ <div class="search-results" id="searchResults" role="listbox" hidden></div>
509
+ <nav class="toc" id="toc" aria-label="Содержание документации">
510
+ ${tocHtml}
511
+ </nav>
512
+ </aside>
513
+
514
+ <main id="main" class="main-content" role="main">
515
+ ${sectionsHtml}
516
+
517
+ <footer class="site-footer">
518
+ <div class="footer-row">
519
+ <strong>@dzhechkov/p-replicator</strong> v1.5.0 · MIT License
520
+ </div>
521
+ <div class="footer-row footer-muted">
522
+ Сгенерировано из <code>README/ru/*.md</code> командой <code>/docs</code>.
523
+ </div>
524
+ <div class="footer-row footer-muted">
525
+ Re-build: <code>cd packages/p-replicator/README/ru/html &amp;&amp; node build.js</code>
526
+ </div>
527
+ <div class="footer-row footer-links">
528
+ <a href="https://github.com/dzhechko/pu-unicorn-replicate" target="_blank" rel="noopener noreferrer">GitHub</a>
529
+ ·
530
+ <a href="https://www.npmjs.com/package/@dzhechkov/p-replicator" target="_blank" rel="noopener noreferrer">npm</a>
531
+ ·
532
+ <a href="https://t.me/llm_notes" target="_blank" rel="noopener noreferrer">Telegram</a>
533
+ </div>
534
+ </footer>
535
+ </main>
536
+ </div>
537
+
538
+ <div class="sidebar-overlay" id="sidebarOverlay" aria-hidden="true"></div>
539
+
540
+ <button class="back-to-top" id="backToTop" aria-label="Наверх" hidden>
541
+ <span aria-hidden="true">↑</span>
542
+ </button>
543
+
544
+ <script src="script.js"></script>
545
+ </body>
546
+ </html>
547
+ `;
548
+
549
+ // ─── Write output ─────────────────────────────────────────────────────────
550
+ fs.writeFileSync(OUTPUT, html, 'utf8');
551
+ const sizeKb = (Buffer.byteLength(html, 'utf8') / 1024).toFixed(1);
552
+ console.log(`[build] Wrote ${OUTPUT} (${sizeKb} KB, ${sections.length} sections)`);
553
+ console.log(`[build] Sections: ${sections.map((s) => s.id).join(', ')}`);