wikimem 0.8.4 → 0.8.6

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,760 @@
1
+ /**
2
+ * Content Distribution Engine — generates static site, RSS, JSON feed, and digest
3
+ * from wiki content. The outbound complement to ingest/scrape/improve.
4
+ */
5
+ import { mkdirSync, writeFileSync, statSync } from 'node:fs';
6
+ import { join, basename, extname, relative } from 'node:path';
7
+ import { getVaultConfig, listWikiPages, readWikiPage, getVaultStats } from './vault.js';
8
+ // ── Main publish function ──────────────────────────────────────────────────
9
+ export function publishWiki(vaultRoot, options) {
10
+ const config = getVaultConfig(vaultRoot);
11
+ const pages = listWikiPages(config.wikiDir);
12
+ if (pages.length === 0) {
13
+ return { pagesPublished: 0, filesWritten: 0, outputDir: options.outDir, formats: [] };
14
+ }
15
+ mkdirSync(options.outDir, { recursive: true });
16
+ // Parse all pages
17
+ const pageMetas = [];
18
+ for (const pagePath of pages) {
19
+ try {
20
+ const page = readWikiPage(pagePath);
21
+ const slug = basename(pagePath, extname(pagePath));
22
+ const category = page.frontmatter['type']
23
+ ?? page.frontmatter['category']
24
+ ?? inferCategory(pagePath, config);
25
+ const summary = page.frontmatter['summary']
26
+ ?? page.content.split(/[.!?]\s/)[0]?.substring(0, 160)
27
+ ?? '';
28
+ const updated = getUpdatedDate(page, pagePath);
29
+ pageMetas.push({
30
+ slug,
31
+ title: page.title,
32
+ category,
33
+ summary,
34
+ wordCount: page.wordCount,
35
+ updated,
36
+ content: page.content,
37
+ wikilinks: page.wikilinks,
38
+ path: relative(config.wikiDir, pagePath),
39
+ });
40
+ }
41
+ catch {
42
+ // Skip unreadable pages
43
+ }
44
+ }
45
+ // Sort by updated date (newest first)
46
+ pageMetas.sort((a, b) => b.updated.localeCompare(a.updated));
47
+ let filesWritten = 0;
48
+ const formatsWritten = [];
49
+ for (const fmt of options.format) {
50
+ switch (fmt) {
51
+ case 'html': {
52
+ const count = generateStaticSite(pageMetas, options, config);
53
+ filesWritten += count;
54
+ formatsWritten.push('html');
55
+ break;
56
+ }
57
+ case 'rss': {
58
+ generateRssFeed(pageMetas, options);
59
+ filesWritten++;
60
+ formatsWritten.push('rss');
61
+ break;
62
+ }
63
+ case 'json-feed': {
64
+ generateJsonFeed(pageMetas, options);
65
+ filesWritten++;
66
+ formatsWritten.push('json-feed');
67
+ break;
68
+ }
69
+ case 'digest': {
70
+ generateDigest(pageMetas, options, config);
71
+ filesWritten++;
72
+ formatsWritten.push('digest');
73
+ break;
74
+ }
75
+ }
76
+ }
77
+ return {
78
+ pagesPublished: pageMetas.length,
79
+ filesWritten,
80
+ outputDir: options.outDir,
81
+ formats: formatsWritten,
82
+ };
83
+ }
84
+ // ── Static HTML Site ───────────────────────────────────────────────────────
85
+ function generateStaticSite(pages, options, config) {
86
+ const pagesDir = join(options.outDir, 'pages');
87
+ mkdirSync(pagesDir, { recursive: true });
88
+ const titleMap = new Map(pages.map((p) => [p.title, p.slug]));
89
+ const titleMapLower = new Map(pages.map((p) => [p.title.toLowerCase(), p.slug]));
90
+ let count = 0;
91
+ // Generate individual pages
92
+ for (const page of pages) {
93
+ const html = renderPageHtml(page, options, titleMap, titleMapLower);
94
+ writeFileSync(join(pagesDir, `${page.slug}.html`), html, 'utf-8');
95
+ count++;
96
+ }
97
+ // Generate index
98
+ const indexHtml = renderIndexHtml(pages, options, config);
99
+ writeFileSync(join(options.outDir, 'index.html'), indexHtml, 'utf-8');
100
+ count++;
101
+ // Generate CSS
102
+ writeFileSync(join(options.outDir, 'style.css'), getStylesheet(), 'utf-8');
103
+ count++;
104
+ return count;
105
+ }
106
+ function renderPageHtml(page, options, titleMap, titleMapLower) {
107
+ const renderedContent = markdownToHtml(page.content, titleMap, titleMapLower);
108
+ const categoryLabel = page.category.charAt(0).toUpperCase() + page.category.slice(1);
109
+ return `<!DOCTYPE html>
110
+ <html lang="en">
111
+ <head>
112
+ <meta charset="UTF-8">
113
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
114
+ <title>${escapeHtml(page.title)} — ${escapeHtml(options.title)}</title>
115
+ <meta name="description" content="${escapeHtml(page.summary)}">
116
+ <link rel="stylesheet" href="../style.css">
117
+ <link rel="alternate" type="application/rss+xml" title="${escapeHtml(options.title)}" href="../feed.xml">
118
+ </head>
119
+ <body>
120
+ <nav class="topbar">
121
+ <a href="../index.html" class="site-title">${escapeHtml(options.title)}</a>
122
+ <span class="breadcrumb">${escapeHtml(categoryLabel)} / ${escapeHtml(page.title)}</span>
123
+ </nav>
124
+ <main class="page-content">
125
+ <article>
126
+ <header class="page-header">
127
+ <span class="category-badge category-${page.category}">${escapeHtml(categoryLabel)}</span>
128
+ <h1>${escapeHtml(page.title)}</h1>
129
+ <div class="page-meta">
130
+ <span>${page.wordCount.toLocaleString()} words</span>
131
+ <span>Updated ${page.updated}</span>
132
+ ${page.wikilinks.length > 0 ? `<span>${page.wikilinks.length} links</span>` : ''}
133
+ </div>
134
+ </header>
135
+ <div class="prose">
136
+ ${renderedContent}
137
+ </div>
138
+ ${page.wikilinks.length > 0 ? renderRelatedLinks(page.wikilinks, titleMap, titleMapLower) : ''}
139
+ </article>
140
+ </main>
141
+ <footer class="site-footer">
142
+ <p>Built with <a href="https://www.npmjs.com/package/wikimem">wikimem</a></p>
143
+ </footer>
144
+ </body>
145
+ </html>`;
146
+ }
147
+ function renderRelatedLinks(wikilinks, titleMap, titleMapLower) {
148
+ const links = wikilinks.map((link) => {
149
+ const slug = titleMap.get(link) ?? titleMapLower.get(link.toLowerCase());
150
+ if (slug) {
151
+ return `<a href="${slug}.html">${escapeHtml(link)}</a>`;
152
+ }
153
+ return `<span class="broken-link">${escapeHtml(link)}</span>`;
154
+ });
155
+ return `<aside class="related-pages">
156
+ <h3>Related Pages</h3>
157
+ <div class="related-grid">${links.join('')}</div>
158
+ </aside>`;
159
+ }
160
+ function renderIndexHtml(pages, options, config) {
161
+ const stats = getVaultStats(config);
162
+ const grouped = groupByCategory(pages);
163
+ const sections = Object.entries(grouped)
164
+ .sort(([a], [b]) => a.localeCompare(b))
165
+ .map(([category, categoryPages]) => {
166
+ const label = category.charAt(0).toUpperCase() + category.slice(1);
167
+ const items = categoryPages
168
+ .map((p) => `<li>
169
+ <a href="pages/${p.slug}.html">${escapeHtml(p.title)}</a>
170
+ <span class="item-meta">${p.wordCount} words</span>
171
+ </li>`)
172
+ .join('\n');
173
+ return `<section class="category-section">
174
+ <h2><span class="category-badge category-${category}">${escapeHtml(label)}</span> ${escapeHtml(label)} <span class="count">(${categoryPages.length})</span></h2>
175
+ <ul>${items}</ul>
176
+ </section>`;
177
+ })
178
+ .join('\n');
179
+ return `<!DOCTYPE html>
180
+ <html lang="en">
181
+ <head>
182
+ <meta charset="UTF-8">
183
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
184
+ <title>${escapeHtml(options.title)}</title>
185
+ <meta name="description" content="${escapeHtml(options.description)}">
186
+ <link rel="stylesheet" href="style.css">
187
+ <link rel="alternate" type="application/rss+xml" title="${escapeHtml(options.title)}" href="feed.xml">
188
+ <link rel="alternate" type="application/feed+json" title="${escapeHtml(options.title)}" href="feed.json">
189
+ </head>
190
+ <body>
191
+ <nav class="topbar">
192
+ <a href="index.html" class="site-title">${escapeHtml(options.title)}</a>
193
+ </nav>
194
+ <main class="index-content">
195
+ <header class="hero">
196
+ <h1>${escapeHtml(options.title)}</h1>
197
+ <p class="hero-desc">${escapeHtml(options.description)}</p>
198
+ <div class="stats-bar">
199
+ <span>${stats.pageCount} pages</span>
200
+ <span>${stats.wordCount.toLocaleString()} words</span>
201
+ <span>${stats.sourceCount} sources</span>
202
+ <span>${stats.wikilinks} links</span>
203
+ </div>
204
+ </header>
205
+ ${sections}
206
+ </main>
207
+ <footer class="site-footer">
208
+ <p>Built with <a href="https://www.npmjs.com/package/wikimem">wikimem</a></p>
209
+ <p class="feed-links">
210
+ <a href="feed.xml">RSS Feed</a> · <a href="feed.json">JSON Feed</a>
211
+ </p>
212
+ </footer>
213
+ </body>
214
+ </html>`;
215
+ }
216
+ // ── RSS Feed ───────────────────────────────────────────────────────────────
217
+ function generateRssFeed(pages, options) {
218
+ const recent = pages.slice(0, 50);
219
+ const now = new Date().toUTCString();
220
+ const baseUrl = options.baseUrl.replace(/\/$/, '');
221
+ const items = recent
222
+ .map((p) => ` <item>
223
+ <title>${xmlEscape(p.title)}</title>
224
+ <link>${xmlEscape(baseUrl)}/pages/${p.slug}.html</link>
225
+ <guid isPermaLink="true">${xmlEscape(baseUrl)}/pages/${p.slug}.html</guid>
226
+ <description>${xmlEscape(p.summary)}</description>
227
+ <category>${xmlEscape(p.category)}</category>
228
+ <pubDate>${new Date(p.updated).toUTCString()}</pubDate>
229
+ </item>`)
230
+ .join('\n');
231
+ const rss = `<?xml version="1.0" encoding="UTF-8"?>
232
+ <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
233
+ <channel>
234
+ <title>${xmlEscape(options.title)}</title>
235
+ <link>${xmlEscape(baseUrl)}</link>
236
+ <description>${xmlEscape(options.description)}</description>
237
+ <language>en</language>
238
+ <lastBuildDate>${now}</lastBuildDate>
239
+ <atom:link href="${xmlEscape(baseUrl)}/feed.xml" rel="self" type="application/rss+xml"/>
240
+ <generator>wikimem</generator>
241
+ ${items}
242
+ </channel>
243
+ </rss>`;
244
+ writeFileSync(join(options.outDir, 'feed.xml'), rss, 'utf-8');
245
+ }
246
+ // ── JSON Feed ──────────────────────────────────────────────────────────────
247
+ function generateJsonFeed(pages, options) {
248
+ const recent = pages.slice(0, 50);
249
+ const baseUrl = options.baseUrl.replace(/\/$/, '');
250
+ const feed = {
251
+ version: 'https://jsonfeed.org/version/1.1',
252
+ title: options.title,
253
+ home_page_url: baseUrl,
254
+ feed_url: `${baseUrl}/feed.json`,
255
+ description: options.description,
256
+ authors: [{ name: options.author }],
257
+ items: recent.map((p) => ({
258
+ id: `${baseUrl}/pages/${p.slug}.html`,
259
+ url: `${baseUrl}/pages/${p.slug}.html`,
260
+ title: p.title,
261
+ summary: p.summary,
262
+ date_modified: new Date(p.updated).toISOString(),
263
+ tags: [p.category],
264
+ content_text: p.content.substring(0, 2000),
265
+ })),
266
+ };
267
+ writeFileSync(join(options.outDir, 'feed.json'), JSON.stringify(feed, null, 2), 'utf-8');
268
+ }
269
+ // ── Digest ─────────────────────────────────────────────────────────────────
270
+ function generateDigest(pages, options, config) {
271
+ const stats = getVaultStats(config);
272
+ const now = new Date().toISOString().split('T')[0] ?? '';
273
+ // Group recent changes (last 7 days)
274
+ const weekAgo = new Date();
275
+ weekAgo.setDate(weekAgo.getDate() - 7);
276
+ const recent = pages.filter((p) => new Date(p.updated) >= weekAgo);
277
+ const grouped = groupByCategory(recent);
278
+ let md = `# ${options.title} — Weekly Digest\n\n`;
279
+ md += `**${now}** · ${stats.pageCount} pages · ${stats.wordCount.toLocaleString()} words · ${stats.wikilinks} links\n\n`;
280
+ if (recent.length === 0) {
281
+ md += 'No changes in the last 7 days.\n';
282
+ }
283
+ else {
284
+ md += `## ${recent.length} pages updated this week\n\n`;
285
+ for (const [category, categoryPages] of Object.entries(grouped).sort(([a], [b]) => a.localeCompare(b))) {
286
+ const label = category.charAt(0).toUpperCase() + category.slice(1);
287
+ md += `### ${label}\n\n`;
288
+ for (const p of categoryPages) {
289
+ md += `- **${p.title}** — ${p.summary || `${p.wordCount} words`}\n`;
290
+ }
291
+ md += '\n';
292
+ }
293
+ }
294
+ // Top connected pages
295
+ const topLinked = [...pages]
296
+ .sort((a, b) => b.wikilinks.length - a.wikilinks.length)
297
+ .slice(0, 10);
298
+ md += `## Most Connected Pages\n\n`;
299
+ for (const p of topLinked) {
300
+ md += `- **${p.title}** — ${p.wikilinks.length} outbound links\n`;
301
+ }
302
+ md += `\n---\n*Generated by [wikimem](https://www.npmjs.com/package/wikimem)*\n`;
303
+ writeFileSync(join(options.outDir, 'digest.md'), md, 'utf-8');
304
+ // Also write HTML version
305
+ const htmlDigest = `<!DOCTYPE html>
306
+ <html lang="en">
307
+ <head>
308
+ <meta charset="UTF-8">
309
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
310
+ <title>${escapeHtml(options.title)} — Digest</title>
311
+ <link rel="stylesheet" href="style.css">
312
+ </head>
313
+ <body>
314
+ <nav class="topbar">
315
+ <a href="index.html" class="site-title">${escapeHtml(options.title)}</a>
316
+ <span class="breadcrumb">Digest</span>
317
+ </nav>
318
+ <main class="page-content">
319
+ <article class="prose">
320
+ ${markdownToHtml(md, new Map(), new Map())}
321
+ </article>
322
+ </main>
323
+ <footer class="site-footer">
324
+ <p>Built with <a href="https://www.npmjs.com/package/wikimem">wikimem</a></p>
325
+ </footer>
326
+ </body>
327
+ </html>`;
328
+ writeFileSync(join(options.outDir, 'digest.html'), htmlDigest, 'utf-8');
329
+ }
330
+ // ── Helpers ────────────────────────────────────────────────────────────────
331
+ function inferCategory(pagePath, config) {
332
+ const rel = relative(config.wikiDir, pagePath);
333
+ const dir = rel.split('/')[0] ?? '';
334
+ if (['sources', 'entities', 'concepts', 'syntheses'].includes(dir))
335
+ return dir.replace(/s$/, '');
336
+ return 'page';
337
+ }
338
+ function getUpdatedDate(page, filePath) {
339
+ const fmDate = page.frontmatter['updated']
340
+ ?? page.frontmatter['created'];
341
+ if (fmDate)
342
+ return fmDate;
343
+ try {
344
+ return statSync(filePath).mtime.toISOString().split('T')[0] ?? '';
345
+ }
346
+ catch {
347
+ return new Date().toISOString().split('T')[0] ?? '';
348
+ }
349
+ }
350
+ function groupByCategory(pages) {
351
+ const grouped = {};
352
+ for (const p of pages) {
353
+ const key = p.category;
354
+ if (!grouped[key])
355
+ grouped[key] = [];
356
+ grouped[key].push(p);
357
+ }
358
+ return grouped;
359
+ }
360
+ /**
361
+ * Minimal markdown → HTML converter. Handles headings, paragraphs, bold,
362
+ * italic, links, code, lists, blockquotes, and wikilinks. No external deps.
363
+ */
364
+ function markdownToHtml(md, titleMap, titleMapLower) {
365
+ let html = md;
366
+ // Wikilinks → real links
367
+ html = html.replace(/\[\[([^\]]+)\]\]/g, (_, title) => {
368
+ const slug = titleMap.get(title) ?? titleMapLower.get(title.toLowerCase());
369
+ if (slug) {
370
+ return `<a href="${slug}.html" class="wiki-link">${escapeHtml(title)}</a>`;
371
+ }
372
+ return `<span class="wiki-link broken">${escapeHtml(title)}</span>`;
373
+ });
374
+ // Fenced code blocks
375
+ html = html.replace(/```(\w*)\n([\s\S]*?)```/g, (_, lang, code) => {
376
+ return `<pre><code class="language-${lang || 'text'}">${escapeHtml(code.trim())}</code></pre>`;
377
+ });
378
+ // Inline code
379
+ html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
380
+ // Headings
381
+ html = html.replace(/^#### (.+)$/gm, '<h4>$1</h4>');
382
+ html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');
383
+ html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>');
384
+ html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>');
385
+ // Bold + italic
386
+ html = html.replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>');
387
+ html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
388
+ html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
389
+ // Links
390
+ html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
391
+ // Horizontal rules
392
+ html = html.replace(/^---+$/gm, '<hr>');
393
+ // Blockquotes
394
+ html = html.replace(/^> (.+)$/gm, '<blockquote>$1</blockquote>');
395
+ // Unordered lists
396
+ html = html.replace(/^- (.+)$/gm, '<li>$1</li>');
397
+ html = html.replace(/(<li>[\s\S]*?<\/li>)/g, (match) => {
398
+ if (!match.startsWith('<ul>'))
399
+ return `<ul>${match}</ul>`;
400
+ return match;
401
+ });
402
+ // Wrap remaining lines in paragraphs (skip already-wrapped content)
403
+ const lines = html.split('\n');
404
+ const result = [];
405
+ for (const line of lines) {
406
+ const trimmed = line.trim();
407
+ if (!trimmed) {
408
+ result.push('');
409
+ }
410
+ else if (trimmed.startsWith('<h') ||
411
+ trimmed.startsWith('<pre') ||
412
+ trimmed.startsWith('<ul') ||
413
+ trimmed.startsWith('<li') ||
414
+ trimmed.startsWith('<blockquote') ||
415
+ trimmed.startsWith('<hr') ||
416
+ trimmed.startsWith('<aside') ||
417
+ trimmed.startsWith('<div') ||
418
+ trimmed.startsWith('<article') ||
419
+ trimmed.startsWith('<nav') ||
420
+ trimmed.startsWith('<footer') ||
421
+ trimmed.startsWith('<header') ||
422
+ trimmed.startsWith('<section')) {
423
+ result.push(trimmed);
424
+ }
425
+ else {
426
+ result.push(`<p>${trimmed}</p>`);
427
+ }
428
+ }
429
+ return result.join('\n');
430
+ }
431
+ function escapeHtml(s) {
432
+ return s
433
+ .replace(/&/g, '&amp;')
434
+ .replace(/</g, '&lt;')
435
+ .replace(/>/g, '&gt;')
436
+ .replace(/"/g, '&quot;');
437
+ }
438
+ function xmlEscape(s) {
439
+ return escapeHtml(s).replace(/'/g, '&apos;');
440
+ }
441
+ // ── Stylesheet ─────────────────────────────────────────────────────────────
442
+ function getStylesheet() {
443
+ return `*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
444
+
445
+ :root {
446
+ --bg: #1a1a2e;
447
+ --bg-surface: #16213e;
448
+ --bg-card: #1e2a47;
449
+ --bg-hover: #243352;
450
+ --border: #2a3a5c;
451
+ --text: #e0e0e0;
452
+ --text-bright: #f0f0f0;
453
+ --text-secondary: #a0a8c0;
454
+ --text-dim: #6b7394;
455
+ --accent: #4f9eff;
456
+ --accent-hover: #6db3ff;
457
+ --green: #4ec9b0;
458
+ --amber: #d7ba7d;
459
+ --purple: #9c78d8;
460
+ --orange: #ce9178;
461
+ --red: #f14c4c;
462
+ --font: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
463
+ --font-mono: 'JetBrains Mono', 'Fira Code', monospace;
464
+ --font-display: 'Instrument Serif', Georgia, serif;
465
+ --radius: 6px;
466
+ }
467
+
468
+ body {
469
+ font-family: var(--font);
470
+ background: var(--bg);
471
+ color: var(--text);
472
+ line-height: 1.7;
473
+ min-height: 100vh;
474
+ display: flex;
475
+ flex-direction: column;
476
+ }
477
+
478
+ .topbar {
479
+ background: var(--bg-surface);
480
+ border-bottom: 1px solid var(--border);
481
+ padding: 12px 24px;
482
+ display: flex;
483
+ align-items: center;
484
+ gap: 16px;
485
+ position: sticky;
486
+ top: 0;
487
+ z-index: 10;
488
+ }
489
+
490
+ .site-title {
491
+ font-family: var(--font-display);
492
+ font-size: 1.25rem;
493
+ color: var(--text-bright);
494
+ text-decoration: none;
495
+ }
496
+
497
+ .breadcrumb {
498
+ color: var(--text-dim);
499
+ font-size: 0.85rem;
500
+ }
501
+
502
+ .hero {
503
+ text-align: center;
504
+ padding: 64px 24px 48px;
505
+ }
506
+
507
+ .hero h1 {
508
+ font-family: var(--font-display);
509
+ font-size: 2.5rem;
510
+ color: var(--text-bright);
511
+ margin-bottom: 12px;
512
+ }
513
+
514
+ .hero-desc {
515
+ color: var(--text-secondary);
516
+ font-size: 1.1rem;
517
+ max-width: 600px;
518
+ margin: 0 auto 24px;
519
+ }
520
+
521
+ .stats-bar {
522
+ display: flex;
523
+ justify-content: center;
524
+ gap: 24px;
525
+ font-size: 0.85rem;
526
+ color: var(--text-dim);
527
+ }
528
+
529
+ .stats-bar span {
530
+ background: var(--bg-card);
531
+ padding: 4px 12px;
532
+ border-radius: var(--radius);
533
+ border: 1px solid var(--border);
534
+ }
535
+
536
+ .index-content, .page-content {
537
+ max-width: 860px;
538
+ margin: 0 auto;
539
+ padding: 32px 24px;
540
+ flex: 1;
541
+ width: 100%;
542
+ }
543
+
544
+ .category-section {
545
+ margin-bottom: 32px;
546
+ }
547
+
548
+ .category-section h2 {
549
+ font-size: 1.2rem;
550
+ color: var(--text-bright);
551
+ margin-bottom: 12px;
552
+ display: flex;
553
+ align-items: center;
554
+ gap: 8px;
555
+ }
556
+
557
+ .count {
558
+ color: var(--text-dim);
559
+ font-weight: 400;
560
+ font-size: 0.85rem;
561
+ }
562
+
563
+ .category-section ul {
564
+ list-style: none;
565
+ display: grid;
566
+ gap: 4px;
567
+ }
568
+
569
+ .category-section li {
570
+ display: flex;
571
+ justify-content: space-between;
572
+ align-items: center;
573
+ padding: 8px 12px;
574
+ border-radius: var(--radius);
575
+ transition: background 0.15s;
576
+ }
577
+
578
+ .category-section li:hover {
579
+ background: var(--bg-hover);
580
+ }
581
+
582
+ .category-section li a {
583
+ color: var(--accent);
584
+ text-decoration: none;
585
+ }
586
+
587
+ .category-section li a:hover {
588
+ color: var(--accent-hover);
589
+ text-decoration: underline;
590
+ }
591
+
592
+ .item-meta {
593
+ color: var(--text-dim);
594
+ font-size: 0.8rem;
595
+ }
596
+
597
+ .category-badge {
598
+ display: inline-block;
599
+ font-size: 0.7rem;
600
+ font-weight: 600;
601
+ text-transform: uppercase;
602
+ letter-spacing: 0.05em;
603
+ padding: 2px 8px;
604
+ border-radius: 3px;
605
+ color: var(--text-bright);
606
+ }
607
+
608
+ .category-source { background: rgba(79,158,255,0.2); color: var(--accent); }
609
+ .category-entity { background: rgba(78,201,176,0.2); color: var(--green); }
610
+ .category-concept { background: rgba(156,120,216,0.2); color: var(--purple); }
611
+ .category-synthesi, .category-synthesis { background: rgba(206,145,120,0.2); color: var(--orange); }
612
+ .category-page { background: rgba(215,186,125,0.2); color: var(--amber); }
613
+
614
+ .page-header {
615
+ margin-bottom: 32px;
616
+ padding-bottom: 16px;
617
+ border-bottom: 1px solid var(--border);
618
+ }
619
+
620
+ .page-header h1 {
621
+ font-family: var(--font-display);
622
+ font-size: 2rem;
623
+ color: var(--text-bright);
624
+ margin: 8px 0 12px;
625
+ }
626
+
627
+ .page-meta {
628
+ display: flex;
629
+ gap: 16px;
630
+ font-size: 0.8rem;
631
+ color: var(--text-dim);
632
+ }
633
+
634
+ .prose h1 { font-size: 1.8rem; margin: 32px 0 16px; color: var(--text-bright); }
635
+ .prose h2 { font-size: 1.4rem; margin: 28px 0 12px; color: var(--text-bright); }
636
+ .prose h3 { font-size: 1.15rem; margin: 24px 0 8px; color: var(--text-bright); }
637
+ .prose h4 { font-size: 1rem; margin: 20px 0 8px; color: var(--text-secondary); }
638
+
639
+ .prose p { margin: 0 0 16px; }
640
+
641
+ .prose a { color: var(--accent); text-decoration: none; }
642
+ .prose a:hover { text-decoration: underline; }
643
+
644
+ .prose .wiki-link { color: var(--green); border-bottom: 1px dashed var(--green); }
645
+ .prose .wiki-link.broken { color: var(--red); border-bottom: 1px dashed var(--red); }
646
+
647
+ .prose code {
648
+ background: var(--bg-card);
649
+ padding: 2px 6px;
650
+ border-radius: 3px;
651
+ font-family: var(--font-mono);
652
+ font-size: 0.9em;
653
+ }
654
+
655
+ .prose pre {
656
+ background: var(--bg-card);
657
+ padding: 16px;
658
+ border-radius: var(--radius);
659
+ overflow-x: auto;
660
+ margin: 16px 0;
661
+ border: 1px solid var(--border);
662
+ }
663
+
664
+ .prose pre code {
665
+ background: none;
666
+ padding: 0;
667
+ }
668
+
669
+ .prose blockquote {
670
+ border-left: 3px solid var(--accent);
671
+ padding: 8px 16px;
672
+ margin: 16px 0;
673
+ color: var(--text-secondary);
674
+ background: rgba(79,158,255,0.05);
675
+ border-radius: 0 var(--radius) var(--radius) 0;
676
+ }
677
+
678
+ .prose ul {
679
+ margin: 8px 0 16px 20px;
680
+ }
681
+
682
+ .prose li {
683
+ margin: 4px 0;
684
+ }
685
+
686
+ .prose hr {
687
+ border: none;
688
+ border-top: 1px solid var(--border);
689
+ margin: 32px 0;
690
+ }
691
+
692
+ .related-pages {
693
+ margin-top: 48px;
694
+ padding-top: 24px;
695
+ border-top: 1px solid var(--border);
696
+ }
697
+
698
+ .related-pages h3 {
699
+ font-size: 0.9rem;
700
+ color: var(--text-dim);
701
+ text-transform: uppercase;
702
+ letter-spacing: 0.05em;
703
+ margin-bottom: 12px;
704
+ }
705
+
706
+ .related-grid {
707
+ display: flex;
708
+ flex-wrap: wrap;
709
+ gap: 8px;
710
+ }
711
+
712
+ .related-grid a, .related-grid .broken-link {
713
+ display: inline-block;
714
+ padding: 4px 12px;
715
+ border-radius: var(--radius);
716
+ font-size: 0.85rem;
717
+ text-decoration: none;
718
+ transition: background 0.15s;
719
+ }
720
+
721
+ .related-grid a {
722
+ background: rgba(78,201,176,0.1);
723
+ color: var(--green);
724
+ border: 1px solid rgba(78,201,176,0.2);
725
+ }
726
+
727
+ .related-grid a:hover {
728
+ background: rgba(78,201,176,0.2);
729
+ }
730
+
731
+ .related-grid .broken-link {
732
+ background: rgba(241,76,76,0.1);
733
+ color: var(--text-dim);
734
+ border: 1px solid rgba(241,76,76,0.15);
735
+ }
736
+
737
+ .site-footer {
738
+ text-align: center;
739
+ padding: 24px;
740
+ border-top: 1px solid var(--border);
741
+ color: var(--text-dim);
742
+ font-size: 0.8rem;
743
+ }
744
+
745
+ .site-footer a { color: var(--accent); text-decoration: none; }
746
+ .site-footer a:hover { text-decoration: underline; }
747
+
748
+ .feed-links {
749
+ margin-top: 8px;
750
+ }
751
+
752
+ @media (max-width: 640px) {
753
+ .hero h1 { font-size: 1.8rem; }
754
+ .stats-bar { flex-wrap: wrap; }
755
+ .page-header h1 { font-size: 1.5rem; }
756
+ .page-meta { flex-wrap: wrap; }
757
+ }
758
+ `;
759
+ }
760
+ //# sourceMappingURL=publish.js.map