cosmolo 0.3.3 → 0.3.4

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.
@@ -1,190 +0,0 @@
1
- import * as readline from 'readline';
2
- import * as fs from 'fs';
3
- import * as path from 'path';
4
- import { DEFAULT_CONFIG } from '../config.js';
5
- import type { ResolvedCosmoloConfig } from '../types.js';
6
-
7
- // ─── config ──────────────────────────────────────────────────────────────────
8
-
9
- async function loadConfig(): Promise<ResolvedCosmoloConfig> {
10
- const root = process.cwd();
11
- for (const name of ['cosmolo.config.ts', 'cosmolo.config.js']) {
12
- const p = path.join(root, name);
13
- if (fs.existsSync(p)) {
14
- try {
15
- const mod = await import(p);
16
- return mod.default as ResolvedCosmoloConfig;
17
- } catch {
18
- // fall through to defaults
19
- }
20
- }
21
- }
22
- return DEFAULT_CONFIG;
23
- }
24
-
25
- // ─── helpers ─────────────────────────────────────────────────────────────────
26
-
27
- function ask(rl: readline.Interface, question: string, fallback = ''): Promise<string> {
28
- const hint = fallback ? ` [${fallback}]` : '';
29
- return new Promise((resolve) =>
30
- rl.question(` ${question}${hint}: `, (ans) => resolve(ans.trim() || fallback))
31
- );
32
- }
33
-
34
- function slugify(text: string): string {
35
- return text
36
- .toLowerCase()
37
- .replace(/[^\w\s-]/g, '')
38
- .trim()
39
- .replace(/[\s_]+/g, '-')
40
- .replace(/-+/g, '-');
41
- }
42
-
43
- function today(): string {
44
- return new Date().toISOString().split('T')[0];
45
- }
46
-
47
- function readCategories(config: ResolvedCosmoloConfig): Record<string, { label: string; description: string }> {
48
- const p = path.resolve(process.cwd(), config.categoriesConfigPath);
49
- if (!fs.existsSync(p)) return {};
50
- return JSON.parse(fs.readFileSync(p, 'utf-8'));
51
- }
52
-
53
- // ─── generators ──────────────────────────────────────────────────────────────
54
-
55
- async function generateArticle(rl: readline.Interface, config: ResolvedCosmoloConfig): Promise<void> {
56
- console.log('\nGenerate Article\n');
57
-
58
- const title = await ask(rl, 'Title');
59
- if (!title) { console.error('\nError: Title is required.'); process.exit(1); }
60
-
61
- const defaultSlug = slugify(title);
62
- const slug = await ask(rl, 'Slug', defaultSlug);
63
-
64
- const articlesDir = path.resolve(process.cwd(), config.articlesDir);
65
- const filePath = path.join(articlesDir, `${slug}.md`);
66
-
67
- if (fs.existsSync(filePath)) {
68
- console.error(`\nError: ${config.articlesDir}/${slug}.md already exists.`);
69
- process.exit(1);
70
- }
71
-
72
- const categories = readCategories(config);
73
- const catKeys = Object.keys(categories);
74
- if (catKeys.length > 0) console.log(`\n Categories: ${catKeys.join(', ')}`);
75
-
76
- const category = await ask(rl, 'Category', catKeys[0] ?? 'other');
77
- const excerpt = await ask(rl, 'Excerpt', '');
78
- const tagsRaw = await ask(rl, 'Tags (comma-separated)', '');
79
- const tags = tagsRaw ? tagsRaw.split(',').map((t) => t.trim()).filter(Boolean) : [];
80
- const sort = await ask(rl, 'Sort', '0');
81
- const date = await ask(rl, 'Date', today());
82
- const draftAns = await ask(rl, 'Draft? (y/N)', 'N');
83
- const draft = draftAns.toLowerCase() === 'y';
84
- const series = await ask(rl, 'Series (optional)', '');
85
- const seriesOrder = series ? await ask(rl, 'Series order', '1') : '';
86
-
87
- const lines = [
88
- '---',
89
- `title: "${title}"`,
90
- `category: "${category}"`,
91
- `excerpt: "${excerpt}"`,
92
- `sort: ${parseInt(sort, 10) || 0}`,
93
- `date: "${date}"`,
94
- ];
95
- if (tags.length > 0) lines.push(`tags: [${tags.map((t) => `"${t}"`).join(', ')}]`);
96
- if (series) {
97
- lines.push(`series: "${series}"`);
98
- lines.push(`seriesOrder: ${parseInt(seriesOrder, 10) || 1}`);
99
- }
100
- if (draft) lines.push('draft: true');
101
- lines.push('---', '', '');
102
-
103
- fs.mkdirSync(articlesDir, { recursive: true });
104
- fs.writeFileSync(filePath, lines.join('\n'));
105
- console.log(`\nCreated: ${config.articlesDir}/${slug}.md`);
106
- }
107
-
108
- async function generatePage(rl: readline.Interface, config: ResolvedCosmoloConfig): Promise<void> {
109
- console.log('\nGenerate Page\n');
110
-
111
- const title = await ask(rl, 'Title');
112
- if (!title) { console.error('\nError: Title is required.'); process.exit(1); }
113
-
114
- const defaultSlug = slugify(title);
115
- const slug = await ask(rl, 'Slug', defaultSlug);
116
-
117
- const pagesDir = path.resolve(process.cwd(), config.pagesDir);
118
- const filePath = path.join(pagesDir, `${slug}.md`);
119
-
120
- if (fs.existsSync(filePath)) {
121
- console.error(`\nError: ${config.pagesDir}/${slug}.md already exists.`);
122
- process.exit(1);
123
- }
124
-
125
- const content = ['---', `title: "${title}"`, '---', '', ''].join('\n');
126
- fs.mkdirSync(pagesDir, { recursive: true });
127
- fs.writeFileSync(filePath, content);
128
- console.log(`\nCreated: ${config.pagesDir}/${slug}.md`);
129
- }
130
-
131
- async function generateCategory(rl: readline.Interface, config: ResolvedCosmoloConfig): Promise<void> {
132
- console.log('\nGenerate Category\n');
133
-
134
- const key = await ask(rl, 'Key (slug)');
135
- if (!key) { console.error('\nError: Key is required.'); process.exit(1); }
136
-
137
- const categoriesPath = path.resolve(process.cwd(), config.categoriesConfigPath);
138
- const categories = readCategories(config);
139
-
140
- if (categories[key]) {
141
- console.error(`\nError: Category "${key}" already exists.`);
142
- process.exit(1);
143
- }
144
-
145
- const label = await ask(rl, 'Label');
146
- const description = await ask(rl, 'Description', '');
147
-
148
- categories[key] = { label, description };
149
- fs.mkdirSync(path.dirname(categoriesPath), { recursive: true });
150
- fs.writeFileSync(categoriesPath, JSON.stringify(categories, null, '\t') + '\n');
151
- console.log(`\nAdded: "${key}" to ${config.categoriesConfigPath}`);
152
- }
153
-
154
- // ─── main ─────────────────────────────────────────────────────────────────────
155
-
156
- export async function main(): Promise<void> {
157
- const config = await loadConfig();
158
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
159
-
160
- const subcommand = process.argv[3]; // argv[2] is "generate"
161
-
162
- const commands: Record<string, (rl: readline.Interface, config: ResolvedCosmoloConfig) => Promise<void>> = {
163
- article: generateArticle,
164
- page: generatePage,
165
- category: generateCategory,
166
- };
167
-
168
- try {
169
- if (subcommand && commands[subcommand]) {
170
- await commands[subcommand](rl, config);
171
- } else if (subcommand) {
172
- console.error(`Unknown subcommand: ${subcommand}`);
173
- console.error('Available: article, page, category');
174
- process.exit(1);
175
- } else {
176
- console.log('Cosmolo Generator\n');
177
- console.log(' 1. Article');
178
- console.log(' 2. Page');
179
- console.log(' 3. Category');
180
- const choice = await new Promise<string>((resolve) =>
181
- rl.question('\nWhat would you like to generate? (1/2/3): ', (ans) => resolve(ans.trim()))
182
- );
183
- const picked = ({ '1': generateArticle, '2': generatePage, '3': generateCategory } as Record<string, typeof generateArticle>)[choice];
184
- if (!picked) { console.error('Invalid choice.'); process.exit(1); }
185
- await picked(rl, config);
186
- }
187
- } finally {
188
- rl.close();
189
- }
190
- }
package/src/cli/index.ts DELETED
@@ -1,22 +0,0 @@
1
- #!/usr/bin/env bun
2
- export {};
3
-
4
- const cmd = process.argv[2];
5
-
6
- switch (cmd) {
7
- case 'init':
8
- await (await import('./init.js')).main();
9
- break;
10
- case 'generate':
11
- await (await import('./generate.js')).main();
12
- break;
13
- default: {
14
- const isUnknown = Boolean(cmd);
15
- if (isUnknown) console.error(`Unknown command: ${cmd}\n`);
16
- console.log('Usage: cosmolo <command>\n');
17
- console.log('Commands:');
18
- console.log(' init Scaffold routes into an existing SvelteKit project');
19
- console.log(' generate [article|page|category] Create content files');
20
- process.exit(isUnknown ? 1 : 0);
21
- }
22
- }
package/src/cli/init.ts DELETED
@@ -1,175 +0,0 @@
1
- import * as fs from 'fs';
2
- import * as path from 'path';
3
- import * as readline from 'readline';
4
- import { fileURLToPath } from 'url';
5
-
6
- // ─── helpers ─────────────────────────────────────────────────────────────────
7
-
8
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
- const TEMPLATE_DIR = path.resolve(__dirname, '../../templates');
10
-
11
- function ask(rl: readline.Interface, question: string): Promise<string> {
12
- return new Promise((resolve) => rl.question(question, resolve));
13
- }
14
-
15
- function copyFile(src: string, dest: string): void {
16
- fs.mkdirSync(path.dirname(dest), { recursive: true });
17
- fs.copyFileSync(src, dest);
18
- }
19
-
20
- function writeFile(dest: string, content: string): void {
21
- fs.mkdirSync(path.dirname(dest), { recursive: true });
22
- fs.writeFileSync(dest, content, 'utf-8');
23
- }
24
-
25
- function collectFiles(dir: string): Array<[string, string]> {
26
- const results: Array<[string, string]> = [];
27
- function walk(current: string) {
28
- for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
29
- const full = path.join(current, entry.name);
30
- if (entry.isDirectory()) walk(full);
31
- else results.push([full, path.relative(dir, full)]);
32
- }
33
- }
34
- walk(dir);
35
- return results;
36
- }
37
-
38
- function destPath(relativePath: string, projectRoot: string): string {
39
- const mapped = relativePath
40
- .replace(/^routes\//, 'src/routes/')
41
- .replace(/^lib\//, 'src/lib/');
42
- return path.join(projectRoot, mapped);
43
- }
44
-
45
- function injectPackageScripts(projectRoot: string): void {
46
- const pkgPath = path.join(projectRoot, 'package.json');
47
- if (!fs.existsSync(pkgPath)) return;
48
-
49
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
50
- pkg.scripts = pkg.scripts ?? {};
51
-
52
- let added = false;
53
- const scripts: Record<string, string> = {
54
- 'generate:article': 'cosmolo generate article',
55
- 'generate:page': 'cosmolo generate page',
56
- 'generate:category': 'cosmolo generate category',
57
- };
58
- for (const [key, val] of Object.entries(scripts)) {
59
- if (!pkg.scripts[key]) {
60
- pkg.scripts[key] = val;
61
- added = true;
62
- }
63
- }
64
-
65
- if (added) {
66
- fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, '\t') + '\n');
67
- console.log(' updated package.json (added generate:* scripts)');
68
- }
69
- }
70
-
71
- // ─── main ─────────────────────────────────────────────────────────────────────
72
-
73
- export async function main(): Promise<void> {
74
- const PROJECT_ROOT = process.cwd();
75
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
76
-
77
- console.log('\ncosmolo init\n');
78
-
79
- // ── Mode selection ──────────────────────────────────────────────────────
80
- console.log('Choose an initialization mode:\n');
81
- console.log(' 1) Full — server routes + Svelte page components');
82
- console.log(' 2) Slim — server routes only (bring your own Svelte UI)\n');
83
-
84
- let modeRaw = '';
85
- while (!['1', '2'].includes(modeRaw)) {
86
- modeRaw = (await ask(rl, 'Mode [1/2]: ')).trim();
87
- if (!['1', '2'].includes(modeRaw)) console.log(' Please enter 1 or 2.');
88
- }
89
- const mode: 'full' | 'slim' = modeRaw === '1' ? 'full' : 'slim';
90
-
91
- // ── Adapter selection ───────────────────────────────────────────────────
92
- console.log('\nChoose your deployment adapter:\n');
93
- console.log(' 1) SSG — @sveltejs/adapter-static (Cloudflare Pages static, GitHub Pages, etc.)');
94
- console.log(' 2) Serverless/SSR — Cloudflare Workers, Vercel, Node, etc.\n');
95
-
96
- let adapterRaw = '';
97
- while (!['1', '2'].includes(adapterRaw)) {
98
- adapterRaw = (await ask(rl, 'Adapter [1/2]: ')).trim();
99
- if (!['1', '2'].includes(adapterRaw)) console.log(' Please enter 1 or 2.');
100
- }
101
- const isSSG = adapterRaw === '1';
102
-
103
- // ── Collect files ───────────────────────────────────────────────────────
104
- const sharedFiles = collectFiles(path.join(TEMPLATE_DIR, 'shared'));
105
- const fullFiles = mode === 'full' ? collectFiles(path.join(TEMPLATE_DIR, 'full')) : [];
106
-
107
- const allFiles: Array<[string, string]> = [
108
- ...sharedFiles.map(([src, rel]) => [src, rel] as [string, string]),
109
- ...fullFiles.map(([src, rel]) => [src, rel] as [string, string]),
110
- ];
111
-
112
- const layoutTsPath = path.join(PROJECT_ROOT, 'src/routes/+layout.ts');
113
- const layoutTsContent = 'export const prerender = true;\n';
114
-
115
- // ── Conflict detection ──────────────────────────────────────────────────
116
- const conflicts: string[] = [];
117
-
118
- for (const [, rel] of allFiles) {
119
- const dest = destPath(rel, PROJECT_ROOT);
120
- if (fs.existsSync(dest)) conflicts.push(path.relative(PROJECT_ROOT, dest));
121
- }
122
- if (isSSG && fs.existsSync(layoutTsPath)) {
123
- conflicts.push(path.relative(PROJECT_ROOT, layoutTsPath));
124
- }
125
-
126
- if (conflicts.length > 0) {
127
- console.log('\nThe following files already exist:\n');
128
- for (const f of conflicts) console.log(` ${f}`);
129
-
130
- let overwriteRaw = '';
131
- while (!['y', 'n'].includes(overwriteRaw)) {
132
- overwriteRaw = (await ask(rl, '\nOverwrite all? [y/N]: ')).trim().toLowerCase() || 'n';
133
- if (!['y', 'n'].includes(overwriteRaw)) console.log(' Please enter y or n.');
134
- }
135
-
136
- if (overwriteRaw === 'n') {
137
- console.log('\nAborted. No files were written.\n');
138
- rl.close();
139
- process.exit(0);
140
- }
141
- }
142
-
143
- rl.close();
144
-
145
- // ── Write files ─────────────────────────────────────────────────────────
146
- for (const [src, rel] of allFiles) {
147
- const dest = destPath(rel, PROJECT_ROOT);
148
- copyFile(src, dest);
149
- console.log(` created ${path.relative(PROJECT_ROOT, dest)}`);
150
- }
151
-
152
- if (isSSG) {
153
- writeFile(layoutTsPath, layoutTsContent);
154
- console.log(` created src/routes/+layout.ts`);
155
- }
156
-
157
- injectPackageScripts(PROJECT_ROOT);
158
-
159
- // ── Next steps ──────────────────────────────────────────────────────────
160
- console.log('\nDone! Next steps:\n');
161
- console.log(' 1. Install cosmolo: npm install cosmolo (or bun add cosmolo)');
162
- if (isSSG) {
163
- console.log(' 2. Install adapter: npm install -D @sveltejs/adapter-static');
164
- } else {
165
- console.log(' 2. Install adapter: npm install -D @sveltejs/adapter-cloudflare (or your adapter)');
166
- }
167
- if (mode === 'full') {
168
- console.log(' 3. Install sass: npm install -D sass (SCSS used in Svelte templates)');
169
- console.log(' 4. Run: npm run dev');
170
- } else {
171
- console.log(' 3. Add your own +page.svelte files for each route.');
172
- console.log(' 4. Run: npm run dev');
173
- }
174
- console.log('\n See https://github.com/alcogy/cosmolo for full documentation.\n');
175
- }
package/src/config.ts DELETED
@@ -1,12 +0,0 @@
1
- import type { CosmoloConfig, ResolvedCosmoloConfig } from './types.js';
2
-
3
- export const DEFAULT_CONFIG: ResolvedCosmoloConfig = {
4
- articlesDir: 'src/content/articles',
5
- pagesDir: 'src/content/pages',
6
- siteConfigPath: 'config/site.json',
7
- categoriesConfigPath: 'config/categories.json',
8
- };
9
-
10
- export function resolveConfig(userConfig: CosmoloConfig = {}): ResolvedCosmoloConfig {
11
- return { ...DEFAULT_CONFIG, ...userConfig };
12
- }
package/src/index.ts DELETED
@@ -1,34 +0,0 @@
1
- export type {
2
- CosmoloConfig,
3
- ResolvedCosmoloConfig,
4
- TocEntry,
5
- ArticleFrontmatter,
6
- Article,
7
- Page,
8
- CategoryEntry,
9
- SiteConfig,
10
- } from './types.js';
11
-
12
- export { DEFAULT_CONFIG, resolveConfig } from './config.js';
13
- export {
14
- getAllCategories,
15
- isKnownCategory,
16
- getCategoryLabel,
17
- getCategoryDescription,
18
- getCategorySlugs,
19
- loadSiteConfig,
20
- } from './categories.js';
21
- export { articleFrontmatterSchema, getArticles, getArticle, getSlugs, getArticlesByCategory, getArticlesByTag, getArticlesBySeries, getTags, getSvxComponent } from './articles.js';
22
- export { getPageSlugs, getPage } from './pages.js';
23
- export { renderMarkdown, generateToc } from './markdown.js';
24
- export {
25
- createArticlesLoader,
26
- createArticleLoader,
27
- createCategoryLoader,
28
- createTagLoader,
29
- createPageLoader,
30
- createArticleEntries,
31
- createCategoryEntries,
32
- createTagEntries,
33
- createPageEntries,
34
- } from './loaders.js';
package/src/loaders.ts DELETED
@@ -1,135 +0,0 @@
1
- import { getArticle, getArticles, getArticlesByCategory, getArticlesBySeries, getArticlesByTag, getSlugs, getTags } from './articles.js';
2
- import { getCategorySlugs, getCategoryLabel, getCategoryDescription } from './categories.js';
3
- import { getPage, getPageSlugs } from './pages.js';
4
- import type { Article, ResolvedCosmoloConfig } from './types.js';
5
-
6
- /**
7
- * Factory for the article listing page load function.
8
- *
9
- * @example
10
- * // src/routes/+page.server.ts
11
- * import { createArticlesLoader } from 'cosmolo';
12
- * import config from '../../cosmolo.config';
13
- * export const load = createArticlesLoader(config);
14
- */
15
- export function createArticlesLoader(config: ResolvedCosmoloConfig) {
16
- return () => ({ articles: getArticles(config) });
17
- }
18
-
19
- /**
20
- * Factory for a single article page load function.
21
- * Resolves manual related articles, series prev/next, and git updated date.
22
- *
23
- * @example
24
- * // src/routes/articles/[slug]/+page.server.ts
25
- * import { createArticleLoader } from 'cosmolo';
26
- * import config from '../../../../cosmolo.config';
27
- * export const entries = () => getSlugs(config).map(slug => ({ slug }));
28
- * export const load = createArticleLoader(config);
29
- */
30
- export function createArticleLoader(
31
- config: ResolvedCosmoloConfig,
32
- options: { getUpdatedAt?: (slug: string) => string } = {}
33
- ) {
34
- return async ({ params }: { params: { slug: string } }) => {
35
- const article = await getArticle(config, params.slug);
36
- const updatedAt = options.getUpdatedAt?.(params.slug) ?? '';
37
-
38
- let related: Article[];
39
- if (article.related.length > 0) {
40
- const all = getArticles(config);
41
- related = article.related
42
- .map((s) => all.find((a) => a.slug === s))
43
- .filter((a): a is Article => a !== undefined)
44
- .slice(0, 4);
45
- } else {
46
- related = getArticlesByCategory(config, article.category, params.slug).slice(0, 4);
47
- }
48
-
49
- let seriesPrev: Article | null = null;
50
- let seriesNext: Article | null = null;
51
- let seriesTotal = 0;
52
- if (article.series) {
53
- const seriesArticles = getArticlesBySeries(config, article.series);
54
- seriesTotal = seriesArticles.length;
55
- const idx = seriesArticles.findIndex((a) => a.slug === params.slug);
56
- seriesPrev = idx > 0 ? seriesArticles[idx - 1] : null;
57
- seriesNext = idx < seriesArticles.length - 1 ? seriesArticles[idx + 1] : null;
58
- }
59
-
60
- return { article, related, updatedAt, seriesPrev, seriesNext, seriesTotal };
61
- };
62
- }
63
-
64
- /**
65
- * Factory for the category listing page load function.
66
- *
67
- * @example
68
- * // src/routes/categories/[slug]/+page.server.ts
69
- * import { createCategoryLoader } from 'cosmolo';
70
- * import config from '../../../../cosmolo.config';
71
- * export const entries = () => getCategorySlugs(config).map(slug => ({ slug }));
72
- * export const load = createCategoryLoader(config);
73
- */
74
- export function createCategoryLoader(config: ResolvedCosmoloConfig) {
75
- return ({ params }: { params: { slug: string } }) => {
76
- const { slug } = params;
77
- return {
78
- slug,
79
- label: getCategoryLabel(config, slug),
80
- description: getCategoryDescription(config, slug),
81
- articles: getArticlesByCategory(config, slug),
82
- };
83
- };
84
- }
85
-
86
- /**
87
- * Factory for the tag listing page load function.
88
- *
89
- * @example
90
- * // src/routes/tags/[tag]/+page.server.ts
91
- * import { createTagLoader } from 'cosmolo';
92
- * import config from '../../../../cosmolo.config';
93
- * export const entries = () => getTags(config).map(tag => ({ tag }));
94
- * export const load = createTagLoader(config);
95
- */
96
- export function createTagLoader(config: ResolvedCosmoloConfig) {
97
- return ({ params }: { params: { tag: string } }) => ({
98
- tag: params.tag,
99
- articles: getArticlesByTag(config, params.tag),
100
- });
101
- }
102
-
103
- /**
104
- * Factory for the static page load function.
105
- *
106
- * @example
107
- * // src/routes/(pages)/[slug]/+page.server.ts
108
- * import { createPageLoader } from 'cosmolo';
109
- * import config from '../../../../cosmolo.config';
110
- * export const entries = () => getPageSlugs(config).map(slug => ({ slug }));
111
- * export const load = createPageLoader(config);
112
- */
113
- export function createPageLoader(config: ResolvedCosmoloConfig) {
114
- return async ({ params }: { params: { slug: string } }) => ({
115
- page: await getPage(config, params.slug),
116
- });
117
- }
118
-
119
- // ─── entries generators ───────────────────────────────────────────────────────
120
-
121
- export function createArticleEntries(config: ResolvedCosmoloConfig) {
122
- return () => getSlugs(config).map((slug) => ({ slug }));
123
- }
124
-
125
- export function createCategoryEntries(config: ResolvedCosmoloConfig) {
126
- return () => getCategorySlugs(config).map((slug) => ({ slug }));
127
- }
128
-
129
- export function createTagEntries(config: ResolvedCosmoloConfig) {
130
- return () => getTags(config).map((tag) => ({ tag }));
131
- }
132
-
133
- export function createPageEntries(config: ResolvedCosmoloConfig) {
134
- return () => getPageSlugs(config).map((slug) => ({ slug }));
135
- }
package/src/markdown.ts DELETED
@@ -1,74 +0,0 @@
1
- import { marked } from 'marked';
2
- import type { TocEntry } from './types.js';
3
-
4
- function slugifyHeading(text: string): string {
5
- return text
6
- .toLowerCase()
7
- .replace(/<[^>]+>/g, '')
8
- .replace(/[^\w\s-]/g, '')
9
- .trim()
10
- .replace(/\s+/g, '-');
11
- }
12
-
13
- marked.use({
14
- extensions: [
15
- {
16
- name: 'youtube',
17
- level: 'block',
18
- start(src: string) {
19
- return src.indexOf('::youtube[');
20
- },
21
- tokenizer(src: string) {
22
- const match = /^::youtube\[([^\]]+)\]/.exec(src);
23
- if (match) return { type: 'youtube', raw: match[0], videoId: match[1].trim() };
24
- },
25
- renderer(token) {
26
- const { videoId } = token as unknown as { videoId: string };
27
- return `<div class="youtube-embed"><iframe src="https://www.youtube.com/embed/${videoId}" title="YouTube video" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div>\n`;
28
- },
29
- },
30
- ],
31
- });
32
-
33
- marked.use({
34
- renderer: {
35
- link({ href, title, text }: { href: string; title?: string | null; text: string }) {
36
- const isExternal = /^https?:\/\//.test(href ?? '');
37
- const rel = isExternal ? ' target="_blank" rel="noopener noreferrer"' : '';
38
- const titleAttr = title ? ` title="${title}"` : '';
39
- return `<a href="${href}"${titleAttr}${rel}>${text}</a>`;
40
- },
41
- heading({ text, depth }: { text: string; depth: number }) {
42
- const id = slugifyHeading(text);
43
- return `<h${depth} id="${id}">${text}</h${depth}>\n`;
44
- },
45
- },
46
- });
47
-
48
- export async function renderMarkdown(content: string): Promise<string> {
49
- return marked.parse(content);
50
- }
51
-
52
- export function generateToc(content: string): TocEntry[] {
53
- const entries: TocEntry[] = [];
54
- const seenIds = new Map<string, number>();
55
- const headingRegex = /^(#{2,6})\s+(.+?)$/gm;
56
- let match;
57
- while ((match = headingRegex.exec(content)) !== null) {
58
- const level = match[1].length;
59
- const rawText = match[2].trim();
60
- const plainText = rawText
61
- .replace(/\*\*([^*]+)\*\*/g, '$1')
62
- .replace(/\*([^*]+)\*/g, '$1')
63
- .replace(/__([^_]+)__/g, '$1')
64
- .replace(/_([^_]+)_/g, '$1')
65
- .replace(/`([^`]+)`/g, '$1')
66
- .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
67
- const baseId = slugifyHeading(plainText);
68
- const count = seenIds.get(baseId) ?? 0;
69
- const id = count === 0 ? baseId : `${baseId}-${count}`;
70
- seenIds.set(baseId, count + 1);
71
- entries.push({ level, id, text: plainText });
72
- }
73
- return entries;
74
- }
package/src/pages.ts DELETED
@@ -1,23 +0,0 @@
1
- import matter from 'gray-matter';
2
- import { renderMarkdown } from './markdown.js';
3
- import type { Page, ResolvedCosmoloConfig } from './types.js';
4
- import { rawPageFiles } from 'cosmolo:content';
5
-
6
- function slugFromPath(filePath: string, dir: string): string {
7
- const prefix = dir.replace(/^\//, '');
8
- return filePath.replace(new RegExp(`^/?${prefix}/`), '').replace(/\.md$/, '');
9
- }
10
-
11
- export function getPageSlugs(config: ResolvedCosmoloConfig): string[] {
12
- return Object.keys(rawPageFiles).map((p) => slugFromPath(p, config.pagesDir));
13
- }
14
-
15
- export async function getPage(config: ResolvedCosmoloConfig, slug: string): Promise<Page> {
16
- const dir = config.pagesDir.replace(/^\//, '');
17
- const filePath = `/${dir}/${slug}.md`;
18
- const raw = rawPageFiles[filePath];
19
- if (raw === undefined) throw new Error(`Page not found: ${slug}`);
20
- const { data, content } = matter(raw);
21
- const html = await renderMarkdown(content);
22
- return { slug, title: String(data.title ?? slug), html };
23
- }