the home site for me: also iteration 3 or 4 of my site
at main 4.2 kB view raw
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 // Transform multiple images: !![alt1](url1)[alt2](url2){attrs} 52 content = content.replace( 53 /!!(\[([^\]]*)\]\(([^)]+)\))+(?:\{([^}]+)\})?/g, 54 (match) => { 55 // Extract all [alt](url) pairs 56 const pairs = [...match.matchAll(/\[([^\]]*)\]\(([^)]+)\)/g)]; 57 const urls = pairs.map(p => p[2]).join(', '); 58 const alts = pairs.map(p => p[1]).join(', '); 59 60 // Extract attrs if present 61 const attrsMatch = match.match(/\{([^}]+)\}$/); 62 const attrs = attrsMatch ? attrsMatch[1] : ''; 63 64 const params: string[] = [`id="${urls}"`]; 65 66 if (alts.trim()) { 67 params.push(`alt="${alts}"`); 68 } 69 70 if (attrs) { 71 const classes = attrs.match(/\.([a-zA-Z0-9_-]+)/g)?.map(c => c.slice(1)) || []; 72 if (classes.length) { 73 params.push(`class="${classes.join(' ')}"`); 74 } 75 76 const keyValueMatches = attrs.matchAll(/([a-zA-Z]+)=["']?([^"'\s}]+)["']?/g); 77 for (const [, key, value] of keyValueMatches) { 78 if (key !== 'class') { 79 params.push(`${key}="${value.replace(/["']/g, '')}"`); 80 } 81 } 82 } 83 84 return `{{ imgs(${params.join(', ')}) }}`; 85 } 86 ); 87 88 // Transform single images: ![alt](url){attrs} 89 content = content.replace( 90 /!\[([^\]]*)\]\(([^)]+)\)(?:\{([^}]+)\})?/g, 91 (match, alt, url, attrs) => { 92 const params: string[] = [`id="${url}"`]; 93 94 if (alt) { 95 params.push(`alt="${alt}"`); 96 } 97 98 if (attrs) { 99 const classes = attrs.match(/\.([a-zA-Z0-9_-]+)/g)?.map(c => c.slice(1)) || []; 100 if (classes.length) { 101 params.push(`class="${classes.join(' ')}"`); 102 } 103 104 const keyValueMatches = attrs.matchAll(/([a-zA-Z]+)=["']?([^"'\s}]+)["']?/g); 105 for (const [, key, value] of keyValueMatches) { 106 if (key !== 'class') { 107 params.push(`${key}="${value.replace(/["']/g, '')}"`); 108 } 109 } 110 } 111 112 return `{{ img(${params.join(', ')}) }}`; 113 } 114 ); 115 116 return content; 117} 118 119function processFile(filePath: string): void { 120 let content = fs.readFileSync(filePath, 'utf8'); 121 const originalContent = content; 122 123 // Split by code blocks and only transform non-code parts 124 const parts = splitByCodeBlocks(content); 125 content = parts.map(part => { 126 if (part.isCode) { 127 return part.text; // Don't transform code blocks 128 } 129 let text = part.text; 130 text = transformCallouts(text); 131 text = transformImages(text); 132 return text; 133 }).join(''); 134 135 if (content !== originalContent) { 136 fs.writeFileSync(filePath, content); 137 } 138} 139 140const files = glob.sync(`${contentDir}/**/*.md`); 141files.forEach(processFile); 142