apexfile 1.1.0

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,983 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * ApexDoc HTML Renderer
5
+ * Walks the resolved AST and produces a full HTML document string.
6
+ */
7
+
8
+ class HTMLRenderer {
9
+ constructor(ast) {
10
+ this.ast = ast;
11
+ this.meta = ast.meta?.fields || {};
12
+ this.style = ast.style || {};
13
+ this._ids = new Set(); // for deduplicating anchor IDs
14
+ }
15
+
16
+ // ── Entry Point ────────────────────────────────────────────────
17
+
18
+ render() {
19
+ const bodyHTML = this._renderNodes(this.ast.body);
20
+ const styleCSS = this._buildCSS();
21
+ const title = this.meta.title || 'ApexDoc Document';
22
+
23
+ return `<!DOCTYPE html>
24
+ <html lang="${this.meta.lang || 'en'}" dir="${this.meta.direction || 'ltr'}">
25
+ <head>
26
+ <meta charset="UTF-8">
27
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
28
+ <title>${this._esc(title)}</title>
29
+ ${this.meta.description ? `<meta name="description" content="${this._esc(this.meta.description)}">` : ''}
30
+ ${this.meta.author ? `<meta name="author" content="${this._esc(Array.isArray(this.meta.author) ? this.meta.author.join(', ') : this.meta.author)}">` : ''}
31
+ <style>
32
+ ${this._baseCSS()}
33
+ ${styleCSS}
34
+ </style>
35
+ </head>
36
+ <body class="apx-doc theme-${this.meta.theme || 'default'}">
37
+ <div class="apx-content">
38
+ ${bodyHTML}
39
+ </div>
40
+ ${this._footerScripts()}
41
+ </body>
42
+ </html>`;
43
+ }
44
+
45
+ // ── Node Renderer ──────────────────────────────────────────────
46
+
47
+ _renderNodes(nodes) {
48
+ if (!nodes || nodes.length === 0) return '';
49
+ return nodes.map(n => this._renderNode(n)).filter(Boolean).join('\n');
50
+ }
51
+
52
+ _renderNode(node) {
53
+ if (!node) return '';
54
+
55
+ switch (node.type) {
56
+
57
+ case 'Heading': return this._renderHeading(node);
58
+ case 'Paragraph': return this._renderParagraph(node);
59
+ case 'Text': return this._esc(node.value);
60
+ case 'Bold': return `<strong>${this._renderChildren(node)}</strong>`;
61
+ case 'Italic': return `<em>${this._renderChildren(node)}</em>`;
62
+ case 'Underline': return `<u>${this._renderChildren(node)}</u>`;
63
+ case 'Strikethrough': return `<s>${this._renderChildren(node)}</s>`;
64
+ case 'Superscript': return `<sup>${this._renderChildren(node)}</sup>`;
65
+ case 'Subscript': return `<sub>${this._renderChildren(node)}</sub>`;
66
+ case 'CodeInline': return `<code class="apx-code-inline">${this._esc(node.value)}</code>`;
67
+ case 'Highlight': return `<mark>${this._renderChildren(node)}</mark>`;
68
+ case 'MathInline': return `<span class="apx-math-inline">\\(${this._esc(node.value)}\\)</span>`;
69
+ case 'Expression': return this._esc(String(node.resolved ?? node.value));
70
+ case 'FootnoteRef': return `<sup><a href="#fn-${node.id}" id="fnref-${node.id}" class="apx-fn-ref">[${node.id}]</a></sup>`;
71
+ case 'FootnoteDef': return `<div id="fn-${node.id}" class="apx-fn-def"><strong>[${node.id}]</strong> ${this._esc(node.content)}</div>`;
72
+ case 'HR': return `<hr class="apx-hr apx-hr-${node.style === '===' ? 'double' : node.style === '~~~' ? 'wave' : 'solid'}">`;
73
+ case 'Newline': return '';
74
+
75
+ case 'InlineStyled': return this._renderInlineStyled(node);
76
+ case 'Link': return this._renderLink(node);
77
+ case 'MathBlock': return this._renderMathBlock(node);
78
+ case 'List': return this._renderList(node);
79
+ case 'Blockquote': return this._renderBlockquote(node);
80
+ case 'Block': return this._renderBlock(node);
81
+ case 'SelfCloseBlock': return this._renderSelfCloseBlock(node);
82
+ case 'Keyframe': return this._renderKeyframe(node);
83
+
84
+ default:
85
+ return '';
86
+ }
87
+ }
88
+
89
+ // ── Heading ────────────────────────────────────────────────────
90
+
91
+ _renderHeading(node) {
92
+ const text = this._renderChildren(node);
93
+ const raw = node.children.map(c => c.value || '').join('');
94
+ const id = this._headingId(raw);
95
+ const style = this._propsToStyle(node.props);
96
+ const tag = `h${node.level}`;
97
+ return `<${tag} id="${id}" class="apx-heading apx-h${node.level}"${style}>${text}</${tag}>`;
98
+ }
99
+
100
+ _headingId(text) {
101
+ let id = text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
102
+ // Deduplicate
103
+ if (this._ids.has(id)) {
104
+ let n = 1;
105
+ while (this._ids.has(`${id}-${n}`)) n++;
106
+ id = `${id}-${n}`;
107
+ }
108
+ this._ids.add(id);
109
+ return id;
110
+ }
111
+
112
+ // ── Paragraph ──────────────────────────────────────────────────
113
+
114
+ _renderParagraph(node) {
115
+ const content = this._renderChildren(node);
116
+ if (!content.trim()) return '';
117
+ return `<p class="apx-p">${content}</p>`;
118
+ }
119
+
120
+ // ── Inline Styled ──────────────────────────────────────────────
121
+
122
+ _renderInlineStyled(node) {
123
+ const content = this._renderChildren(node);
124
+ const style = this._propsToStyle(node.props);
125
+ const cls = node.props.animation ? ` apx-animated` : '';
126
+ // Tooltip
127
+ const tooltip = node.props.tooltip ? ` title="${this._esc(node.props.tooltip)}"` : '';
128
+ // Glossary
129
+ const glossary = node.props.glossary ? ` class="apx-glossary"` : '';
130
+ return `<span class="apx-styled${cls}"${style}${tooltip}${glossary}>${content}</span>`;
131
+ }
132
+
133
+ // ── Link ───────────────────────────────────────────────────────
134
+
135
+ _renderLink(node) {
136
+ const content = this._renderChildren(node);
137
+ const href = node.href || '';
138
+
139
+ // Internal anchor
140
+ if (href.startsWith('#')) return `<a href="${href}" class="apx-link-internal">${content}</a>`;
141
+
142
+ // Modal trigger
143
+ if (href.startsWith('modal:')) {
144
+ return `<a href="#" class="apx-modal-trigger" data-modal="${href.slice(6)}">${content}</a>`;
145
+ }
146
+
147
+ // External
148
+ return `<a href="${this._esc(href)}" class="apx-link" target="_blank" rel="noopener">${content}</a>`;
149
+ }
150
+
151
+ // ── List ───────────────────────────────────────────────────────
152
+
153
+ _renderList(node) {
154
+ const tag = node.ordered ? 'ol' : 'ul';
155
+ const items = node.items.map(item => {
156
+ if (item.type === 'TaskItem') {
157
+ const checked = item.state === 'done';
158
+ const progress = item.state === 'progress';
159
+ const cls = `apx-task-item apx-task-${item.state}`;
160
+ const icon = checked ? '✓' : progress ? '◑' : '○';
161
+ return `<li class="${cls}"><span class="apx-task-icon">${icon}</span>${this._renderChildren(item)}</li>`;
162
+ }
163
+ return `<li class="apx-li">${this._renderChildren(item)}</li>`;
164
+ }).join('\n');
165
+
166
+ return `<${tag} class="apx-list apx-${node.ordered ? 'ol' : 'ul'}">\n${items}\n</${tag}>`;
167
+ }
168
+
169
+ // ── Blockquote ─────────────────────────────────────────────────
170
+
171
+ _renderBlockquote(node) {
172
+ const content = this._renderChildren(node);
173
+ return `<blockquote class="apx-blockquote apx-bq-level-${node.level}">${content}</blockquote>`;
174
+ }
175
+
176
+ // ── Math Block ─────────────────────────────────────────────────
177
+
178
+ _renderMathBlock(node) {
179
+ const raw = node.children?.[0]?.value || node.value || '';
180
+ const dialect = node.dialect || 'latex';
181
+ const label = node.props?.label || '';
182
+ const numbered = node.props?.numbered ? ` <span class="apx-eq-num">(${label})</span>` : '';
183
+ return `<div class="apx-math-block" data-dialect="${dialect}" id="${label}">\\[${this._esc(raw)}\\]${numbered}</div>`;
184
+ }
185
+
186
+ // ── Generic Block Dispatcher ───────────────────────────────────
187
+
188
+ _renderBlock(node) {
189
+ const dispatch = {
190
+ // Content
191
+ box: () => this._renderBox(node),
192
+ note: () => this._renderCallout(node, 'note', '📝'),
193
+ tip: () => this._renderCallout(node, 'tip', '💡'),
194
+ warning: () => this._renderCallout(node, 'warning', '⚠️'),
195
+ danger: () => this._renderCallout(node, 'danger', '🚨'),
196
+ success: () => this._renderCallout(node, 'success', '✅'),
197
+ info: () => this._renderCallout(node, 'info', 'ℹ️'),
198
+ collapse: () => this._renderCollapse(node),
199
+ tabs: () => this._renderTabs(node),
200
+ grid: () => this._renderGrid(node),
201
+ animate: () => this._renderAnimate(node),
202
+ stagger: () => this._renderStagger(node),
203
+ // Code & math
204
+ code: () => this._renderCode(node),
205
+ math: () => this._renderMathBlock(node),
206
+ chem: () => this._renderChem(node),
207
+ terminal: () => this._renderTerminal(node),
208
+ diff: () => this._renderDiff(node),
209
+ // Data
210
+ table: () => this._renderTable(node),
211
+ data: () => this._renderData(node),
212
+ stats: () => this._renderStats(node),
213
+ // Charts
214
+ chart: () => this._renderChart(node),
215
+ // Interactive
216
+ poll: () => this._renderPoll(node),
217
+ quiz: () => this._renderQuiz(node),
218
+ kanban: () => this._renderKanban(node),
219
+ timeline: () => this._renderTimeline(node),
220
+ flowchart: () => this._renderFlowchart(node),
221
+ mindmap: () => this._renderMindmap(node),
222
+ modal: () => this._renderModal(node),
223
+ // Media
224
+ gallery: () => this._renderGallery(node),
225
+ svg: () => this._renderSVG(node),
226
+ // Proof / math
227
+ theorem: () => this._renderProofBlock(node, 'theorem'),
228
+ proof: () => this._renderProofBlock(node, 'proof'),
229
+ lemma: () => this._renderProofBlock(node, 'lemma'),
230
+ corollary: () => this._renderProofBlock(node, 'corollary'),
231
+ // i18n
232
+ lang: () => this._renderLang(node),
233
+ restrict: () => this._renderRestrict(node),
234
+ redact: () => this._renderRedact(node),
235
+ // Define / import — handled at parse time, not render time
236
+ define: () => '',
237
+ import: () => '',
238
+ // Slide
239
+ slide: () => this._renderSlide(node),
240
+ };
241
+
242
+ const fn = dispatch[node.name];
243
+ if (fn) return fn();
244
+
245
+ // Generic fallback: render as a div with data attributes
246
+ const style = this._propsToStyle(node.props);
247
+ const children = this._renderNodes(node.children);
248
+ return `<div class="apx-block apx-block-${node.name}"${style}>${children}</div>`;
249
+ }
250
+
251
+ // ── Self-Closing Block Dispatcher ──────────────────────────────
252
+
253
+ _renderSelfCloseBlock(node) {
254
+ const dispatch = {
255
+ chart: () => this._renderChart(node),
256
+ image: () => this._renderImage(node),
257
+ video: () => this._renderVideo(node),
258
+ audio: () => this._renderAudio(node),
259
+ clock: () => this._renderClock(node),
260
+ countdown: () => this._renderCountdown(node),
261
+ divider: () => `<hr class="apx-divider apx-divider-${node.props.style || 'solid'}" style="color:${node.props.color || 'inherit'};margin:${node.props.margin || '1rem'} 0">`,
262
+ spacer: () => `<div class="apx-spacer" style="height:${node.props.height || '1rem'}"></div>`,
263
+ progress: () => this._renderProgress(node),
264
+ qr: () => this._renderQR(node),
265
+ map: () => this._renderMap(node),
266
+ lottie: () => this._renderLottie(node),
267
+ icon: () => this._renderIcon(node),
268
+ rating: () => this._renderRating(node),
269
+ 'back-to-top': () => `<a href="#" class="apx-back-to-top">${node.props.label || '↑ Top'}</a>`,
270
+ breadcrumbs: () => `<nav class="apx-breadcrumbs" aria-label="Breadcrumb"></nav>`,
271
+ 'progress-bar': () => `<div class="apx-reading-progress" style="background:${node.props.color || 'var(--primary)'}"></div>`,
272
+ sidenav: () => `<nav class="apx-sidenav"></nav>`,
273
+ toc: () => `<nav class="apx-toc"><h3>${node.props.title || 'Contents'}</h3></nav>`,
274
+ 'theme-switcher': () => this._renderThemeSwitcher(node),
275
+ slider: () => this._renderSlider(node),
276
+ toggle: () => this._renderToggle(node),
277
+ signature: () => this._renderSignature(node),
278
+ feed: () => this._renderFeed(node),
279
+ stat: () => this._renderStat(node),
280
+ '3d': () => `<div class="apx-3d" data-src="${node.props.src || ''}"></div>`,
281
+ };
282
+
283
+ const fn = dispatch[node.name];
284
+ if (fn) return fn();
285
+
286
+ return `<div class="apx-block apx-sc-${node.name}" data-props='${JSON.stringify(node.props)}'></div>`;
287
+ }
288
+
289
+ // ── Block Renderers ────────────────────────────────────────────
290
+
291
+ _renderBox(node) {
292
+ const style = this._boxStyle(node.props);
293
+ const children = this._renderNodes(node.children);
294
+ return `<div class="apx-box"${style}>${children}</div>`;
295
+ }
296
+
297
+ _renderCallout(node, type, icon) {
298
+ const title = node.props.title || type.charAt(0).toUpperCase() + type.slice(1);
299
+ const children = this._renderNodes(node.children);
300
+ return `<div class="apx-callout apx-callout-${type}" role="note">
301
+ <div class="apx-callout-header">${icon} <strong>${this._esc(title)}</strong></div>
302
+ <div class="apx-callout-body">${children}</div>
303
+ </div>`;
304
+ }
305
+
306
+ _renderCollapse(node) {
307
+ const title = node.props.title || 'Details';
308
+ const open = node.props.open !== false ? ' open' : '';
309
+ const children = this._renderNodes(node.children);
310
+ return `<details class="apx-collapse"${open}>
311
+ <summary class="apx-collapse-title">${this._esc(title)}</summary>
312
+ <div class="apx-collapse-body">${children}</div>
313
+ </details>`;
314
+ }
315
+
316
+ _renderTabs(node) {
317
+ const tabs = node.children.filter(c => c.name === 'tab');
318
+ const headers = tabs.map((tab, i) =>
319
+ `<button class="apx-tab-btn${i === 0 ? ' active' : ''}" data-tab="${i}">${this._esc(tab.props.label || `Tab ${i+1}`)}</button>`
320
+ ).join('');
321
+ const panels = tabs.map((tab, i) =>
322
+ `<div class="apx-tab-panel${i === 0 ? ' active' : ''}" data-tab="${i}">${this._renderNodes(tab.children)}</div>`
323
+ ).join('');
324
+ return `<div class="apx-tabs">
325
+ <div class="apx-tab-headers">${headers}</div>
326
+ <div class="apx-tab-panels">${panels}</div>
327
+ </div>`;
328
+ }
329
+
330
+ _renderGrid(node) {
331
+ const cols = node.props.cols || 2;
332
+ const mobileCols = node.props['mobile-cols'] || 1;
333
+ const gap = node.props.gap || '1rem';
334
+ const cells = node.children.filter(c => c.name === 'cell' || c.type === 'Block');
335
+ const cellsHTML = cells.map(cell =>
336
+ `<div class="apx-cell">${this._renderNodes(cell.children)}</div>`
337
+ ).join('\n');
338
+ return `<div class="apx-grid" style="--apx-cols:${cols};--apx-mobile-cols:${mobileCols};gap:${gap}">${cellsHTML}</div>`;
339
+ }
340
+
341
+ _renderAnimate(node) {
342
+ const effect = node.props.effect || 'fadeIn';
343
+ const duration = node.props.duration || '0.5s';
344
+ const delay = node.props.delay || '0s';
345
+ const trigger = node.props.trigger || 'load';
346
+ const children = this._renderNodes(node.children);
347
+ return `<div class="apx-animate" data-effect="${effect}" data-duration="${duration}" data-delay="${delay}" data-trigger="${trigger}">${children}</div>`;
348
+ }
349
+
350
+ _renderStagger(node) {
351
+ const effect = node.props.effect || 'fadeIn';
352
+ const delay = node.props.delay || '0.1s';
353
+ const children = this._renderNodes(node.children);
354
+ return `<div class="apx-stagger" data-effect="${effect}" data-delay="${delay}">${children}</div>`;
355
+ }
356
+
357
+ _renderCode(node) {
358
+ const raw = node.children?.[0]?.value || '';
359
+ const lang = node.props.lang || 'text';
360
+ const lines = node.props.lines !== false;
361
+ const title = node.props.title || '';
362
+ const copy = node.props.copy !== false;
363
+ const hl = node.props.highlight ? JSON.stringify(node.props.highlight) : '[]';
364
+ return `<div class="apx-code-block" data-lang="${lang}">
365
+ ${title ? `<div class="apx-code-title">${this._esc(title)}</div>` : ''}
366
+ ${copy ? `<button class="apx-copy-btn" onclick="apxCopy(this)">Copy</button>` : ''}
367
+ <pre class="apx-pre${lines ? ' line-numbers' : ''}" data-highlight='${hl}'><code class="language-${lang}">${this._esc(raw)}</code></pre>
368
+ </div>`;
369
+ }
370
+
371
+ _renderChem(node) {
372
+ const raw = node.children?.[0]?.value || '';
373
+ return `<div class="apx-chem">\\[${this._esc(raw)}\\]</div>`;
374
+ }
375
+
376
+ _renderTerminal(node) {
377
+ const raw = node.children?.[0]?.value || '';
378
+ const title = node.props.title || 'terminal';
379
+ return `<div class="apx-terminal">
380
+ <div class="apx-terminal-bar"><span>${this._esc(title)}</span></div>
381
+ <pre class="apx-terminal-body">${this._esc(raw)}</pre>
382
+ </div>`;
383
+ }
384
+
385
+ _renderDiff(node) {
386
+ const raw = node.children?.[0]?.value || '';
387
+ const lang = node.props.lang || 'text';
388
+ const lines = raw.split('\n').map(l => {
389
+ if (l.startsWith('+ ')) return `<div class="apx-diff-add">${this._esc(l.slice(2))}</div>`;
390
+ if (l.startsWith('- ')) return `<div class="apx-diff-del">${this._esc(l.slice(2))}</div>`;
391
+ return `<div class="apx-diff-ctx">${this._esc(l)}</div>`;
392
+ }).join('');
393
+ return `<div class="apx-diff" data-lang="${lang}">${lines}</div>`;
394
+ }
395
+
396
+ _renderTable(node) {
397
+ const props = node.props;
398
+ const striped = props.striped ? ' apx-table-striped' : '';
399
+ const bordered = props.border ? ' apx-table-bordered' : '';
400
+ const rows = node.children.filter(c => c.type === 'Paragraph' || c.type === 'Text');
401
+
402
+ // Parse markdown table rows from children text
403
+ const rawText = node.children.map(c => {
404
+ if (c.type === 'Text') return c.value;
405
+ if (c.children) return c.children.map(ch => ch.value || '').join('');
406
+ return '';
407
+ }).join('\n');
408
+
409
+ const tableLines = rawText.split('\n').filter(l => l.trim().startsWith('|'));
410
+ if (tableLines.length === 0) {
411
+ return `<table class="apx-table${striped}${bordered}">${this._renderNodes(node.children)}</table>`;
412
+ }
413
+
414
+ const parseRow = (line) =>
415
+ line.split('|').slice(1, -1).map(c => c.trim());
416
+
417
+ const [headerLine, , ...bodyLines] = tableLines;
418
+ const headers = parseRow(headerLine);
419
+ const theadHTML = `<thead><tr>${headers.map(h => `<th>${this._esc(h)}</th>`).join('')}</tr></thead>`;
420
+ const tbodyHTML = `<tbody>${bodyLines.map(l =>
421
+ `<tr>${parseRow(l).map(c => `<td>${this._esc(c)}</td>`).join('')}</tr>`
422
+ ).join('')}</tbody>`;
423
+
424
+ return `<table class="apx-table${striped}${bordered}">${theadHTML}${tbodyHTML}</table>`;
425
+ }
426
+
427
+ _renderData(node) {
428
+ const id = node.props.id || '';
429
+ return `<script type="application/apx-data" id="apx-data-${id}">${this._renderNodes(node.children)}</script>`;
430
+ }
431
+
432
+ _renderStats(node) {
433
+ const cols = node.props.cols || 4;
434
+ const stats = node.children.filter(c => c.type === 'SelfCloseBlock' && c.name === 'stat');
435
+ const items = stats.map(s => this._renderStat(s)).join('');
436
+ return `<div class="apx-stats" style="--apx-stat-cols:${cols}">${items}</div>`;
437
+ }
438
+
439
+ _renderStat(node) {
440
+ const { label, value, trend, icon, color } = node.props;
441
+ const trendClass = trend && trend.startsWith('+') ? 'up' : trend ? 'down' : '';
442
+ return `<div class="apx-stat"${color ? ` style="--apx-stat-color:${color}"` : ''}>
443
+ ${icon ? `<div class="apx-stat-icon">${this._esc(icon)}</div>` : ''}
444
+ <div class="apx-stat-value">${this._esc(String(value || ''))}</div>
445
+ <div class="apx-stat-label">${this._esc(label || '')}</div>
446
+ ${trend ? `<div class="apx-stat-trend apx-trend-${trendClass}">${this._esc(trend)}</div>` : ''}
447
+ </div>`;
448
+ }
449
+
450
+ _renderChart(node) {
451
+ const props = JSON.stringify(node.props || {});
452
+ return `<div class="apx-chart" data-props='${props}'><canvas></canvas></div>`;
453
+ }
454
+
455
+ _renderPoll(node) {
456
+ const question = node.props.question || '';
457
+ const options = node.children.filter(c => c.type === 'List').flatMap(l => l.items);
458
+ const opts = options.map(o => {
459
+ const label = o.children?.map(c => c.value || '').join('') || '';
460
+ return `<button class="apx-poll-opt" data-poll="${node.props.id || ''}">${this._esc(label)}</button>`;
461
+ }).join('');
462
+ return `<div class="apx-poll" id="poll-${node.props.id || ''}">
463
+ <p class="apx-poll-q">${this._esc(question)}</p>
464
+ <div class="apx-poll-opts">${opts}</div>
465
+ <div class="apx-poll-results"></div>
466
+ </div>`;
467
+ }
468
+
469
+ _renderQuiz(node) {
470
+ return `<div class="apx-quiz" data-id="${node.props.id || ''}">${this._renderNodes(node.children)}</div>`;
471
+ }
472
+
473
+ _renderKanban(node) {
474
+ const cols = node.children.filter(c => c.name === 'column');
475
+ const colsHTML = cols.map(col => {
476
+ const cards = col.children.filter(c => c.name === 'card');
477
+ const cardsHTML = cards.map(card =>
478
+ `<div class="apx-kanban-card${card.props.done ? ' done' : ''}">
479
+ <div class="apx-card-title">${this._esc(card.props.title || '')}</div>
480
+ ${card.props.assignee ? `<div class="apx-card-assignee">${this._esc(card.props.assignee)}</div>` : ''}
481
+ </div>`).join('');
482
+ return `<div class="apx-kanban-col">
483
+ <div class="apx-kanban-col-title">${this._esc(col.props.title || '')}</div>
484
+ ${cardsHTML}
485
+ </div>`;
486
+ }).join('');
487
+ return `<div class="apx-kanban">${colsHTML}</div>`;
488
+ }
489
+
490
+ _renderTimeline(node) {
491
+ const dir = node.props.direction || 'vertical';
492
+ const events = node.children.filter(c => c.name === 'event');
493
+ const items = events.map(ev => {
494
+ const body = this._renderNodes(ev.children);
495
+ const cls = ev.props.active ? ' active' : '';
496
+ return `<div class="apx-timeline-event${cls}">
497
+ <div class="apx-tl-date">${this._esc(ev.props.date || '')}</div>
498
+ <div class="apx-tl-dot"></div>
499
+ <div class="apx-tl-body">
500
+ <div class="apx-tl-title">${this._esc(ev.props.title || '')}</div>
501
+ ${body}
502
+ </div>
503
+ </div>`;
504
+ }).join('');
505
+ return `<div class="apx-timeline apx-tl-${dir}">${items}</div>`;
506
+ }
507
+
508
+ _renderFlowchart(node) {
509
+ const raw = node.children?.[0]?.value || '';
510
+ return `<div class="apx-flowchart" data-definition="${this._esc(raw)}"></div>`;
511
+ }
512
+
513
+ _renderMindmap(node) {
514
+ const root = node.props.root || 'Root';
515
+ return `<div class="apx-mindmap" data-root="${this._esc(root)}">${this._renderNodes(node.children)}</div>`;
516
+ }
517
+
518
+ _renderModal(node) {
519
+ const id = node.props.id || '';
520
+ const title = node.props.title || '';
521
+ const children = this._renderNodes(node.children);
522
+ return `<div class="apx-modal" id="modal-${id}" role="dialog" aria-modal="true" aria-label="${this._esc(title)}">
523
+ <div class="apx-modal-overlay"></div>
524
+ <div class="apx-modal-box">
525
+ <div class="apx-modal-header"><strong>${this._esc(title)}</strong><button class="apx-modal-close">×</button></div>
526
+ <div class="apx-modal-body">${children}</div>
527
+ </div>
528
+ </div>`;
529
+ }
530
+
531
+ _renderGallery(node) {
532
+ const cols = node.props.cols || 3;
533
+ const lightbox = node.props.lightbox !== false;
534
+ const images = node.children.filter(c => c.type === 'SelfCloseBlock' && c.name === 'image');
535
+ const imgsHTML = images.map(img =>
536
+ `<div class="apx-gallery-item">${this._renderImage(img, lightbox)}</div>`
537
+ ).join('');
538
+ return `<div class="apx-gallery" style="--apx-gallery-cols:${cols}">${imgsHTML}</div>`;
539
+ }
540
+
541
+ _renderSVG(node) {
542
+ const raw = node.children?.[0]?.value || '';
543
+ return `<div class="apx-svg"><svg xmlns="http://www.w3.org/2000/svg">${raw}</svg></div>`;
544
+ }
545
+
546
+ _renderProofBlock(node, kind) {
547
+ const title = node.props.title || kind.charAt(0).toUpperCase() + kind.slice(1);
548
+ const num = node.props.numbered ? ` (${node.props.numbered})` : '';
549
+ const children = this._renderNodes(node.children);
550
+ return `<div class="apx-proof apx-proof-${kind}">
551
+ <div class="apx-proof-title"><em>${this._esc(title)}${num}.</em></div>
552
+ <div class="apx-proof-body">${children}</div>
553
+ </div>`;
554
+ }
555
+
556
+ _renderLang(node) {
557
+ const lang = node.props.value || node.value || 'en';
558
+ const dir = node.props.dir || 'ltr';
559
+ const children = this._renderNodes(node.children);
560
+ return `<div class="apx-lang" lang="${lang}" dir="${dir}">${children}</div>`;
561
+ }
562
+
563
+ _renderRestrict(node) {
564
+ const role = node.props.role || 'user';
565
+ return `<div class="apx-restricted" data-role="${role}">${this._renderNodes(node.children)}</div>`;
566
+ }
567
+
568
+ _renderRedact(node) {
569
+ return `<span class="apx-redacted" aria-label="Redacted content">█████████</span>`;
570
+ }
571
+
572
+ _renderSlide(node) {
573
+ const transition = node.props.transition || 'fade';
574
+ const bg = node.props.bg || '';
575
+ const children = this._renderNodes(node.children);
576
+ return `<section class="apx-slide" data-transition="${transition}"${bg ? ` style="background:${bg}"` : ''}>${children}</section>`;
577
+ }
578
+
579
+ _renderKeyframe(node) {
580
+ const steps = Object.entries(node.steps).map(([k, v]) => `${k} { ${v} }`).join(' ');
581
+ return `<style>@keyframes ${node.name} { ${steps} }</style>`;
582
+ }
583
+
584
+ // ── Self-Closing Renderers ─────────────────────────────────────
585
+
586
+ _renderImage(node, lightbox = false) {
587
+ const { src, alt, width, caption, align, radius } = node.props;
588
+ const style = [
589
+ width ? `width:${width}` : '',
590
+ radius ? `border-radius:${radius}` : '',
591
+ ].filter(Boolean).join(';');
592
+ const img = `<img src="${this._esc(src || '')}" alt="${this._esc(alt || '')}"${style ? ` style="${style}"` : ''} class="apx-img" loading="lazy">`;
593
+ const wrapped = lightbox ? `<a href="${this._esc(src || '')}" class="apx-lightbox-link">${img}</a>` : img;
594
+ const cap = caption ? `<figcaption class="apx-caption">${this._esc(caption)}</figcaption>` : '';
595
+ return `<figure class="apx-figure apx-align-${align || 'left'}">${wrapped}${cap}</figure>`;
596
+ }
597
+
598
+ _renderVideo(node) {
599
+ const { src, type, autoplay, controls, width } = node.props;
600
+ if (type === 'youtube') {
601
+ const id = (src || '').match(/v=([^&]+)/)?.[1] || src;
602
+ return `<div class="apx-video apx-video-embed"><iframe src="https://www.youtube.com/embed/${id}" frameborder="0" allowfullscreen></iframe></div>`;
603
+ }
604
+ return `<video class="apx-video" src="${this._esc(src || '')}"${controls !== false ? ' controls' : ''}${autoplay ? ' autoplay muted' : ''}${width ? ` style="width:${width}"` : ''}></video>`;
605
+ }
606
+
607
+ _renderAudio(node) {
608
+ const { src, controls } = node.props;
609
+ return `<div class="apx-audio"><audio src="${this._esc(src || '')}"${controls !== false ? ' controls' : ''}></audio></div>`;
610
+ }
611
+
612
+ _renderClock(node) {
613
+ const { format, timezone, style: s } = node.props;
614
+ return `<div class="apx-clock apx-clock-${s || 'digital'}" data-format="${format || 'HH:mm:ss'}" data-tz="${timezone || 'UTC'}"></div>`;
615
+ }
616
+
617
+ _renderCountdown(node) {
618
+ const { to, label, style: s } = node.props;
619
+ return `<div class="apx-countdown apx-countdown-${s || 'default'}" data-to="${to || ''}" data-label="${this._esc(label || '')}"></div>`;
620
+ }
621
+
622
+ _renderProgress(node) {
623
+ const { value, max, color, label, animated } = node.props;
624
+ const pct = Math.min(100, Math.max(0, ((value || 0) / (max || 100)) * 100));
625
+ return `<div class="apx-progress${animated ? ' apx-progress-animated' : ''}">
626
+ ${label ? `<div class="apx-progress-label">${this._esc(label)} ${Math.round(pct)}%</div>` : ''}
627
+ <div class="apx-progress-track"><div class="apx-progress-fill" style="width:${pct}%;background:${color || 'var(--primary)'}"></div></div>
628
+ </div>`;
629
+ }
630
+
631
+ _renderQR(node) {
632
+ const { data, size, color } = node.props;
633
+ return `<div class="apx-qr" data-qr="${this._esc(data || '')}" data-size="${size || '200px'}" data-color="${color || '#000'}"><canvas></canvas></div>`;
634
+ }
635
+
636
+ _renderMap(node) {
637
+ const { lat, lng, zoom, label } = node.props;
638
+ return `<div class="apx-map" data-lat="${lat || 0}" data-lng="${lng || 0}" data-zoom="${zoom || 12}" data-label="${this._esc(label || '')}"></div>`;
639
+ }
640
+
641
+ _renderLottie(node) {
642
+ const { src, loop, autoplay, width } = node.props;
643
+ return `<div class="apx-lottie"${width ? ` style="width:${width}"` : ''} data-src="${this._esc(src || '')}" data-loop="${loop !== false}" data-autoplay="${autoplay !== false}"></div>`;
644
+ }
645
+
646
+ _renderIcon(node) {
647
+ const { name, size, color } = node.props;
648
+ return `<span class="apx-icon apx-icon-${name || 'default'}" style="font-size:${size || '1em'};color:${color || 'inherit'}" aria-hidden="true"></span>`;
649
+ }
650
+
651
+ _renderRating(node) {
652
+ const { max, value, readonly, id } = node.props;
653
+ const stars = Array.from({ length: max || 5 }, (_, i) =>
654
+ `<span class="apx-star${i < Math.floor(value || 0) ? ' filled' : ''}" data-val="${i+1}">★</span>`
655
+ ).join('');
656
+ return `<div class="apx-rating${readonly ? ' readonly' : ''}" id="rating-${id || ''}">${stars}</div>`;
657
+ }
658
+
659
+ _renderThemeSwitcher(node) {
660
+ const themes = node.props.themes || [];
661
+ const buttons = (Array.isArray(themes) ? themes : [themes]).map(t =>
662
+ `<button class="apx-theme-btn" data-theme="${t}">${this._esc(t)}</button>`
663
+ ).join('');
664
+ return `<div class="apx-theme-switcher">${buttons}</div>`;
665
+ }
666
+
667
+ _renderSlider(node) {
668
+ const { id, min, max, value, label } = node.props;
669
+ return `<div class="apx-slider-wrap">
670
+ ${label ? `<label for="slider-${id}">${this._esc(label)}</label>` : ''}
671
+ <input type="range" id="slider-${id || ''}" class="apx-slider" min="${min || 0}" max="${max || 100}" value="${value || 50}">
672
+ <span class="apx-slider-val">${value || 50}</span>
673
+ </div>`;
674
+ }
675
+
676
+ _renderToggle(node) {
677
+ const { id, label, 'default': def } = node.props;
678
+ return `<label class="apx-toggle-wrap">
679
+ <input type="checkbox" id="toggle-${id || ''}" class="apx-toggle"${def ? ' checked' : ''}>
680
+ <span class="apx-toggle-knob"></span>
681
+ ${label ? `<span class="apx-toggle-label">${this._esc(label)}</span>` : ''}
682
+ </label>`;
683
+ }
684
+
685
+ _renderSignature(node) {
686
+ const { id, label, required } = node.props;
687
+ return `<div class="apx-signature" id="sig-${id || ''}"${required ? ' data-required="true"' : ''}>
688
+ ${label ? `<div class="apx-sig-label">${this._esc(label)}</div>` : ''}
689
+ <canvas class="apx-sig-canvas"></canvas>
690
+ <button class="apx-sig-clear">Clear</button>
691
+ </div>`;
692
+ }
693
+
694
+ _renderFeed(node) {
695
+ const { src, refresh, limit } = node.props;
696
+ return `<div class="apx-feed" data-src="${this._esc(src || '')}" data-refresh="${refresh || '5s'}" data-limit="${limit || 10}"></div>`;
697
+ }
698
+
699
+ // ── CSS Builder ────────────────────────────────────────────────
700
+
701
+ _buildCSS() {
702
+ const { variables, themes } = this.style;
703
+ const lines = [];
704
+
705
+ // Root variables
706
+ if (Object.keys(variables).length > 0) {
707
+ lines.push(':root {');
708
+ for (const [k, v] of Object.entries(variables)) {
709
+ lines.push(` ${k}: ${v};`);
710
+ }
711
+ lines.push('}');
712
+ }
713
+
714
+ // Theme classes
715
+ for (const [name, vars] of Object.entries(themes)) {
716
+ lines.push(`\n.theme-${name} {`);
717
+ for (const [k, v] of Object.entries(vars)) {
718
+ lines.push(` ${k}: ${v};`);
719
+ }
720
+ lines.push('}');
721
+ }
722
+
723
+ return lines.join('\n');
724
+ }
725
+
726
+ // ── Base CSS ───────────────────────────────────────────────────
727
+
728
+ _baseCSS() {
729
+ return `
730
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
731
+ :root {
732
+ --primary: #00f5d4; --bg: #0d0d0d; --surface: #161616;
733
+ --text: #f0f0f0; --muted: #888; --radius: 8px;
734
+ --font: system-ui, sans-serif; --font-mono: monospace;
735
+ }
736
+ body.apx-doc { background: var(--bg); color: var(--text); font-family: var(--font); line-height: 1.7; }
737
+ .apx-content { max-width: 860px; margin: 0 auto; padding: 2rem 1.5rem; }
738
+ .apx-heading { margin: 2rem 0 1rem; line-height: 1.3; }
739
+ .apx-h1 { font-size: 2.5rem; } .apx-h2 { font-size: 2rem; }
740
+ .apx-h3 { font-size: 1.5rem; } .apx-h4 { font-size: 1.25rem; }
741
+ .apx-p { margin: 1rem 0; }
742
+ .apx-code-inline { background: var(--surface); padding: 0.1em 0.4em; border-radius: 4px; font-family: var(--font-mono); font-size: 0.9em; }
743
+ .apx-code-block { position: relative; margin: 1.5rem 0; background: #111; border-radius: var(--radius); overflow: hidden; }
744
+ .apx-code-title { padding: 0.5rem 1rem; background: #1a1a1a; font-size: 0.8rem; color: var(--muted); }
745
+ .apx-pre { padding: 1.25rem; overflow-x: auto; font-family: var(--font-mono); font-size: 0.9rem; }
746
+ .apx-copy-btn { position: absolute; top: 0.5rem; right: 0.5rem; padding: 0.25rem 0.6rem; background: var(--surface); border: none; border-radius: 4px; color: var(--muted); cursor: pointer; font-size: 0.75rem; }
747
+ .apx-box { border-radius: var(--radius); padding: 1.25rem; margin: 1rem 0; }
748
+ .apx-callout { border-radius: var(--radius); padding: 1rem 1.25rem; margin: 1rem 0; border-left: 4px solid; }
749
+ .apx-callout-note { background: rgba(0,150,255,0.08); border-color: #0096ff; }
750
+ .apx-callout-tip { background: rgba(0,200,100,0.08); border-color: #00c864; }
751
+ .apx-callout-warning { background: rgba(255,180,0,0.08); border-color: #ffb800; }
752
+ .apx-callout-danger { background: rgba(255,50,50,0.08); border-color: #ff3232; }
753
+ .apx-callout-success { background: rgba(0,200,100,0.08); border-color: #00c864; }
754
+ .apx-callout-header { font-weight: 600; margin-bottom: 0.5rem; }
755
+ .apx-table { width: 100%; border-collapse: collapse; margin: 1rem 0; }
756
+ .apx-table th, .apx-table td { padding: 0.6rem 0.8rem; text-align: left; }
757
+ .apx-table th { background: var(--surface); font-weight: 600; }
758
+ .apx-table-striped tr:nth-child(even) { background: rgba(255,255,255,0.03); }
759
+ .apx-table-bordered th, .apx-table-bordered td { border: 1px solid rgba(255,255,255,0.1); }
760
+ .apx-blockquote { border-left: 3px solid var(--primary); padding-left: 1rem; color: var(--muted); margin: 1rem 0; }
761
+ .apx-hr { border: none; border-top: 1px solid rgba(255,255,255,0.1); margin: 2rem 0; }
762
+ .apx-list { padding-left: 1.5rem; margin: 1rem 0; }
763
+ .apx-li { margin: 0.25rem 0; }
764
+ .apx-task-icon { margin-right: 0.5rem; }
765
+ .apx-task-done { opacity: 0.6; text-decoration: line-through; }
766
+ .apx-grid { display: grid; grid-template-columns: repeat(var(--apx-cols, 2), 1fr); }
767
+ .apx-stats { display: grid; grid-template-columns: repeat(var(--apx-stat-cols, 4), 1fr); gap: 1rem; margin: 1rem 0; }
768
+ .apx-stat { background: var(--surface); border-radius: var(--radius); padding: 1.25rem; }
769
+ .apx-stat-value { font-size: 2rem; font-weight: 700; color: var(--apx-stat-color, var(--primary)); }
770
+ .apx-stat-label { font-size: 0.85rem; color: var(--muted); }
771
+ .apx-stat-trend { font-size: 0.8rem; margin-top: 0.25rem; }
772
+ .apx-trend-up { color: #00c896; } .apx-trend-down { color: #ff4d4d; }
773
+ .apx-chart { margin: 1.5rem 0; background: var(--surface); border-radius: var(--radius); padding: 1rem; }
774
+ .apx-tabs { margin: 1rem 0; }
775
+ .apx-tab-headers { display: flex; gap: 0.25rem; border-bottom: 1px solid rgba(255,255,255,0.1); }
776
+ .apx-tab-btn { padding: 0.5rem 1rem; background: none; border: none; color: var(--muted); cursor: pointer; border-bottom: 2px solid transparent; }
777
+ .apx-tab-btn.active { color: var(--primary); border-bottom-color: var(--primary); }
778
+ .apx-tab-panel { display: none; padding: 1rem 0; }
779
+ .apx-tab-panel.active { display: block; }
780
+ .apx-collapse { margin: 1rem 0; background: var(--surface); border-radius: var(--radius); }
781
+ .apx-collapse-title { padding: 0.8rem 1rem; cursor: pointer; font-weight: 600; }
782
+ .apx-collapse-body { padding: 0 1rem 1rem; }
783
+ .apx-timeline { margin: 1.5rem 0; }
784
+ .apx-tl-vertical .apx-timeline-event { display: grid; grid-template-columns: 100px 20px 1fr; gap: 1rem; margin-bottom: 1.5rem; align-items: start; }
785
+ .apx-tl-dot { width: 12px; height: 12px; border-radius: 50%; background: var(--primary); margin-top: 4px; }
786
+ .apx-tl-date { font-size: 0.8rem; color: var(--muted); text-align: right; }
787
+ .apx-tl-title { font-weight: 600; margin-bottom: 0.25rem; }
788
+ .apx-kanban { display: flex; gap: 1rem; overflow-x: auto; margin: 1rem 0; }
789
+ .apx-kanban-col { min-width: 200px; background: var(--surface); border-radius: var(--radius); padding: 1rem; }
790
+ .apx-kanban-col-title { font-weight: 700; margin-bottom: 1rem; color: var(--primary); }
791
+ .apx-kanban-card { background: rgba(255,255,255,0.04); border-radius: 6px; padding: 0.75rem; margin-bottom: 0.5rem; }
792
+ .apx-terminal { background: #0a0a0a; border-radius: var(--radius); overflow: hidden; margin: 1rem 0; }
793
+ .apx-terminal-bar { background: #1a1a1a; padding: 0.5rem 1rem; font-size: 0.8rem; color: var(--muted); }
794
+ .apx-terminal-body { padding: 1rem; font-family: var(--font-mono); font-size: 0.9rem; color: #00ff41; white-space: pre; }
795
+ .apx-progress { margin: 1rem 0; }
796
+ .apx-progress-track { background: rgba(255,255,255,0.1); border-radius: 999px; height: 8px; overflow: hidden; }
797
+ .apx-progress-fill { height: 100%; border-radius: 999px; transition: width 0.5s ease; }
798
+ .apx-diff { font-family: var(--font-mono); font-size: 0.9rem; background: #111; border-radius: var(--radius); padding: 1rem; margin: 1rem 0; }
799
+ .apx-diff-add { background: rgba(0,200,100,0.1); color: #00c864; }
800
+ .apx-diff-del { background: rgba(255,50,50,0.1); color: #ff4d4d; }
801
+ .apx-diff-ctx { color: var(--muted); }
802
+ .apx-math-block { overflow-x: auto; margin: 1.5rem 0; }
803
+ .apx-proof { margin: 1.5rem 0; border-left: 3px solid var(--primary); padding-left: 1rem; }
804
+ .apx-modal { display: none; position: fixed; inset: 0; z-index: 1000; align-items: center; justify-content: center; }
805
+ .apx-modal.open { display: flex; }
806
+ .apx-modal-overlay { position: absolute; inset: 0; background: rgba(0,0,0,0.7); }
807
+ .apx-modal-box { position: relative; background: var(--surface); border-radius: var(--radius); padding: 1.5rem; max-width: 600px; width: 90%; z-index: 1; }
808
+ .apx-modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; }
809
+ .apx-modal-close { background: none; border: none; color: var(--muted); cursor: pointer; font-size: 1.25rem; }
810
+ .apx-rating { display: flex; gap: 0.25rem; font-size: 1.5rem; cursor: pointer; }
811
+ .apx-star { color: var(--muted); transition: color 0.2s; }
812
+ .apx-star.filled { color: #ffb800; }
813
+ .apx-toggle-wrap { display: flex; align-items: center; gap: 0.5rem; cursor: pointer; }
814
+ .apx-toggle { display: none; }
815
+ .apx-toggle-knob { width: 40px; height: 22px; background: var(--muted); border-radius: 999px; position: relative; transition: background 0.2s; }
816
+ .apx-toggle:checked + .apx-toggle-knob { background: var(--primary); }
817
+ .apx-slider { width: 100%; accent-color: var(--primary); }
818
+ .apx-back-to-top { position: fixed; bottom: 2rem; right: 2rem; background: var(--primary); color: var(--bg); padding: 0.5rem 1rem; border-radius: var(--radius); text-decoration: none; font-weight: 600; }
819
+ .apx-redacted { background: currentColor; border-radius: 3px; user-select: none; }
820
+ .apx-lang[dir=rtl] { text-align: right; }
821
+ .apx-fn-ref { color: var(--primary); text-decoration: none; font-size: 0.75rem; }
822
+ .apx-fn-def { font-size: 0.85rem; color: var(--muted); margin: 0.5rem 0; }
823
+ .apx-map, .apx-qr canvas, .apx-3d, .apx-lottie, .apx-feed { min-height: 200px; background: var(--surface); border-radius: var(--radius); display: flex; align-items: center; justify-content: center; }
824
+ .apx-gallery { display: grid; grid-template-columns: repeat(var(--apx-gallery-cols, 3), 1fr); gap: 0.5rem; margin: 1rem 0; }
825
+ .apx-img { max-width: 100%; height: auto; display: block; }
826
+ .apx-video-embed iframe { width: 100%; aspect-ratio: 16/9; border: none; border-radius: var(--radius); }
827
+ @keyframes apx-fadeIn { from { opacity: 0 } to { opacity: 1 } }
828
+ @keyframes apx-slideUp { from { opacity: 0; transform: translateY(20px) } to { opacity: 1; transform: translateY(0) } }
829
+ @keyframes apx-zoomIn { from { opacity: 0; transform: scale(0.95) } to { opacity: 1; transform: scale(1) } }
830
+ .apx-animate[data-effect=fadeIn] { animation: apx-fadeIn var(--apx-dur, 0.5s) ease both; }
831
+ .apx-animate[data-effect=slideUp] { animation: apx-slideUp var(--apx-dur, 0.5s) ease both; }
832
+ .apx-animate[data-effect=zoomIn] { animation: apx-zoomIn var(--apx-dur, 0.5s) ease both; }
833
+ @media (max-width: 768px) {
834
+ .apx-grid { grid-template-columns: repeat(var(--apx-mobile-cols, 1), 1fr); }
835
+ .apx-stats { grid-template-columns: repeat(2, 1fr); }
836
+ .apx-gallery { grid-template-columns: repeat(2, 1fr); }
837
+ }
838
+ `;
839
+ }
840
+
841
+ // ── Footer Scripts ─────────────────────────────────────────────
842
+
843
+ _footerScripts() {
844
+ return `
845
+ <script>
846
+ // ApexDoc Runtime — Tabs
847
+ document.querySelectorAll('.apx-tab-btn').forEach(btn => {
848
+ btn.addEventListener('click', () => {
849
+ const parent = btn.closest('.apx-tabs');
850
+ parent.querySelectorAll('.apx-tab-btn, .apx-tab-panel').forEach(el => el.classList.remove('active'));
851
+ btn.classList.add('active');
852
+ parent.querySelector('.apx-tab-panel[data-tab="' + btn.dataset.tab + '"]').classList.add('active');
853
+ });
854
+ });
855
+
856
+ // Modals
857
+ document.querySelectorAll('.apx-modal-trigger').forEach(link => {
858
+ link.addEventListener('click', e => {
859
+ e.preventDefault();
860
+ document.getElementById('modal-' + link.dataset.modal)?.classList.add('open');
861
+ });
862
+ });
863
+ document.querySelectorAll('.apx-modal-close, .apx-modal-overlay').forEach(el => {
864
+ el.addEventListener('click', () => el.closest('.apx-modal')?.classList.remove('open'));
865
+ });
866
+
867
+ // Copy button
868
+ function apxCopy(btn) {
869
+ const code = btn.closest('.apx-code-block').querySelector('code');
870
+ navigator.clipboard.writeText(code.textContent).then(() => {
871
+ btn.textContent = 'Copied!';
872
+ setTimeout(() => btn.textContent = 'Copy', 2000);
873
+ });
874
+ }
875
+
876
+ // Sliders
877
+ document.querySelectorAll('.apx-slider').forEach(sl => {
878
+ sl.addEventListener('input', () => {
879
+ sl.closest('.apx-slider-wrap').querySelector('.apx-slider-val').textContent = sl.value;
880
+ });
881
+ });
882
+
883
+ // Ratings
884
+ document.querySelectorAll('.apx-rating:not(.readonly) .apx-star').forEach(star => {
885
+ star.addEventListener('click', () => {
886
+ const val = parseInt(star.dataset.val);
887
+ star.closest('.apx-rating').querySelectorAll('.apx-star').forEach((s, i) => {
888
+ s.classList.toggle('filled', i < val);
889
+ });
890
+ });
891
+ });
892
+
893
+ // Theme switcher
894
+ document.querySelectorAll('.apx-theme-btn').forEach(btn => {
895
+ btn.addEventListener('click', () => {
896
+ const body = document.body;
897
+ body.className = body.className.replace(/theme-\S+/, '') + ' theme-' + btn.dataset.theme;
898
+ });
899
+ });
900
+
901
+ // Scroll-triggered animations
902
+ const apxObserver = new IntersectionObserver(entries => {
903
+ entries.forEach(entry => {
904
+ if (entry.isIntersecting) {
905
+ const el = entry.target;
906
+ el.style.setProperty('--apx-dur', el.dataset.duration || '0.5s');
907
+ el.style.animationDelay = el.dataset.delay || '0s';
908
+ el.classList.add('apx-triggered');
909
+ }
910
+ });
911
+ }, { threshold: 0.2 });
912
+ document.querySelectorAll('.apx-animate[data-trigger=scroll]').forEach(el => apxObserver.observe(el));
913
+
914
+ // Clocks
915
+ document.querySelectorAll('.apx-clock').forEach(clock => {
916
+ const fmt = clock.dataset.format || 'HH:mm:ss';
917
+ const update = () => {
918
+ const now = new Date();
919
+ const pad = n => String(n).padStart(2, '0');
920
+ clock.textContent = fmt
921
+ .replace('HH', pad(now.getHours()))
922
+ .replace('mm', pad(now.getMinutes()))
923
+ .replace('ss', pad(now.getSeconds()));
924
+ };
925
+ update(); setInterval(update, 1000);
926
+ });
927
+
928
+ // Collapse toggle fix
929
+ document.querySelectorAll('.apx-collapse summary').forEach(s => {
930
+ s.style.listStyle = 'none';
931
+ });
932
+ </script>`;
933
+ }
934
+
935
+ // ── Utility ────────────────────────────────────────────────────
936
+
937
+ _renderChildren(node) {
938
+ if (!node.children || node.children.length === 0) return '';
939
+ return node.children.map(c => this._renderNode(c)).join('');
940
+ }
941
+
942
+ _propsToStyle(props) {
943
+ if (!props || Object.keys(props).length === 0) return '';
944
+ const pairs = [];
945
+ const map = {
946
+ color: 'color',
947
+ bg: 'background',
948
+ size: 'font-size',
949
+ weight: 'font-weight',
950
+ align: 'text-align',
951
+ radius: 'border-radius',
952
+ padding: 'padding',
953
+ margin: 'margin',
954
+ border: 'border',
955
+ gradient: 'background',
956
+ };
957
+ for (const [k, v] of Object.entries(props)) {
958
+ if (map[k] && v) pairs.push(`${map[k]}:${v}`);
959
+ }
960
+ if (props.animation) pairs.push(`animation:${props.animation}`);
961
+ return pairs.length > 0 ? ` style="${pairs.join(';')}"` : '';
962
+ }
963
+
964
+ _boxStyle(props) {
965
+ const pairs = [];
966
+ if (props.bg) pairs.push(`background:${props.bg}`);
967
+ if (props.border) pairs.push(`border:1px solid ${props.border}`);
968
+ if (props.radius) pairs.push(`border-radius:${props.radius}`);
969
+ if (props.padding) pairs.push(`padding:${props.padding}`);
970
+ return pairs.length > 0 ? ` style="${pairs.join(';')}"` : '';
971
+ }
972
+
973
+ _esc(str) {
974
+ return String(str || '')
975
+ .replace(/&/g, '&amp;')
976
+ .replace(/</g, '&lt;')
977
+ .replace(/>/g, '&gt;')
978
+ .replace(/"/g, '&quot;')
979
+ .replace(/'/g, '&#39;');
980
+ }
981
+ }
982
+
983
+ module.exports = HTMLRenderer;