the home site for me: also iteration 3 or 4 of my site
at main 4.4 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, doubleQuoted, singleQuoted, unquoted] of keyValueMatches) { 78 if (key !== 'class') { 79 const value = doubleQuoted || singleQuoted || unquoted; 80 params.push(`${key}="${value}"`); 81 } 82 } 83 } 84 85 return `{{ imgs(${params.join(', ')}) }}`; 86 } 87 ); 88 89 // Transform single images: ![alt](url){attrs} 90 content = content.replace( 91 /!\[([^\]]*)\]\(([^)]+)\)(?:\{([^}]+)\})?/g, 92 (match, alt, url, attrs) => { 93 const params: string[] = [`id="${url}"`]; 94 95 if (alt) { 96 params.push(`alt="${alt}"`); 97 } 98 99 if (attrs) { 100 const classes = attrs.match(/\.([a-zA-Z0-9_-]+)/g)?.map(c => c.slice(1)) || []; 101 if (classes.length) { 102 params.push(`class="${classes.join(' ')}"`); 103 } 104 105 const keyValueMatches = attrs.matchAll(/([a-zA-Z]+)=(?:"([^"]*)"|'([^']*)'|([^\s}]+))/g); 106 for (const [, key, doubleQuoted, singleQuoted, unquoted] of keyValueMatches) { 107 if (key !== 'class') { 108 const value = doubleQuoted || singleQuoted || unquoted; 109 params.push(`${key}="${value}"`); 110 } 111 } 112 } 113 114 return `{{ img(${params.join(', ')}) }}`; 115 } 116 ); 117 118 return content; 119} 120 121function processFile(filePath: string): void { 122 let content = fs.readFileSync(filePath, 'utf8'); 123 const originalContent = content; 124 125 // Split by code blocks and only transform non-code parts 126 const parts = splitByCodeBlocks(content); 127 content = parts.map(part => { 128 if (part.isCode) { 129 return part.text; // Don't transform code blocks 130 } 131 let text = part.text; 132 text = transformCallouts(text); 133 text = transformImages(text); 134 return text; 135 }).join(''); 136 137 if (content !== originalContent) { 138 fs.writeFileSync(filePath, content); 139 } 140} 141 142const files = glob.sync(`${contentDir}/**/*.md`); 143files.forEach(processFile); 144