the home site for me: also iteration 3 or 4 of my site
1#!/usr/bin/env bun
2
3import fs from 'fs';
4import path from 'path';
5import { glob } from 'glob';
6
7const contentDir = process.argv[2] || 'content';
8
9function splitByCodeBlocks(content: string): { text: string; isCode: boolean }[] {
10 const parts: { text: string; isCode: boolean }[] = [];
11 const codeBlockRegex = /^(```|~~~)/gm;
12
13 let lastIndex = 0;
14 let inCodeBlock = false;
15 let match;
16
17 codeBlockRegex.lastIndex = 0;
18
19 while ((match = codeBlockRegex.exec(content)) !== null) {
20 const segment = content.slice(lastIndex, match.index);
21 if (segment) {
22 parts.push({ text: segment, isCode: inCodeBlock });
23 }
24 inCodeBlock = !inCodeBlock;
25 lastIndex = match.index;
26 }
27
28 // Add remaining content
29 if (lastIndex < content.length) {
30 parts.push({ text: content.slice(lastIndex), isCode: inCodeBlock });
31 }
32
33 return parts;
34}
35
36function transformCallouts(content: string): string {
37 return content.replace(
38 /^> \[!(INFO|WARNING|WARN|DANGER|ERROR|TIP|HINT|NOTE)\]\n((?:> .*\n?)*)/gm,
39 (match, type, body) => {
40 const cleanBody = body.replace(/^> /gm, '').trim();
41 const normalizedType = type.toLowerCase() === 'warn' ? 'warning' :
42 type.toLowerCase() === 'error' ? 'danger' :
43 type.toLowerCase() === 'hint' ? 'tip' :
44 type.toLowerCase();
45 return `{% callout(type="${normalizedType}") %}\n${cleanBody}\n{% end %}\n`;
46 }
47 );
48}
49
50function transformImages(content: string): string {
51 return content.replace(
52 /!\[([^\]]*)\]\(([^)]+)\)\{([^}]+)\}/g,
53 (match, alt, url, attrs) => {
54 const params: string[] = [`id="${url}"`];
55
56 if (alt) {
57 params.push(`alt="${alt}"`);
58 }
59
60 const classes = attrs.match(/\.([a-zA-Z0-9_-]+)/g)?.map(c => c.slice(1)) || [];
61 if (classes.length) {
62 params.push(`class="${classes.join(' ')}"`);
63 }
64
65 const keyValueMatches = attrs.matchAll(/([a-zA-Z]+)=["']?([^"'\s}]+)["']?/g);
66 for (const [, key, value] of keyValueMatches) {
67 if (key !== 'class') {
68 params.push(`${key}="${value.replace(/["']/g, '')}"`);
69 }
70 }
71
72 return `{{ img(${params.join(', ')}) }}`;
73 }
74 );
75}
76
77function processFile(filePath: string): void {
78 let content = fs.readFileSync(filePath, 'utf8');
79 const originalContent = content;
80
81 // Split by code blocks and only transform non-code parts
82 const parts = splitByCodeBlocks(content);
83 content = parts.map(part => {
84 if (part.isCode) {
85 return part.text; // Don't transform code blocks
86 }
87 let text = part.text;
88 text = transformCallouts(text);
89 text = transformImages(text);
90 return text;
91 }).join('');
92
93 if (content !== originalContent) {
94 fs.writeFileSync(filePath, content);
95 }
96}
97
98const files = glob.sync(`${contentDir}/**/*.md`);
99files.forEach(processFile);
100