UNPKG

portfolio-xs

Version:

This is a tool to generate portfolio based with your markdown file

206 lines (164 loc) 6.49 kB
import fs from 'fs'; import path from 'path'; import fse from 'fs-extra'; import { marked } from 'marked'; import { fileURLToPath } from 'url'; const renderer = { image({ href, title, text }) { console.log('Image href:', href); console.log('Alt text:', text); return `<img src="${href}" alt="${text}" class="mark-img" />`; } }; marked.use({ renderer }); function injectSettings(html, settings) { return html .replace(/<title>.*<\/title>/, `<title>${settings.title || ''}</title>`) .replace( '</head>', `${settings.favicon ? `<link rel="icon" href="${settings.favicon}">` : ''}</head>` ); } function copyLocalImages(markdown, srcDir, destDir, assetPrefix = '') { const imageRegex = /!\[[^\]]*\]\((.*?)\)/g; const copiedMap = new Map(); let match; while ((match = imageRegex.exec(markdown)) !== null) { const imagePath = match[1]; if (!imagePath.startsWith('http') && !copiedMap.has(imagePath)) { const absSource = path.resolve(srcDir, imagePath); const absTarget = path.resolve(destDir, imagePath); if (fs.existsSync(absSource)) { fse.ensureDirSync(path.dirname(absTarget)); fs.copyFileSync(absSource, absTarget); copiedMap.set(imagePath, true); } } } return markdown.replace(imageRegex, (full, imgPath) => { if (imgPath.startsWith('http')) return full; return full.replace(imgPath, path.join(assetPrefix, imgPath)); }); } function renderAboutPage(workDir, buildDir) { const aboutPath = path.join(workDir, 'about.md'); const pagesDir = path.join(buildDir, 'pages'); const assetsDir = path.join(buildDir, 'assets', 'about'); fse.ensureDirSync(pagesDir); if (!fs.existsSync(aboutPath)) return; const markdown = fs.readFileSync(aboutPath, 'utf-8'); const updatedMarkdown = copyLocalImages(markdown, path.dirname(aboutPath), assetsDir, '/assets/about'); const htmlContent = marked.parse(updatedMarkdown); const componentContent = ` import React from 'react'; export default function About() { return ( <div className="about-markdown" dangerouslySetInnerHTML={{ __html: ${JSON.stringify(htmlContent)} }} /> ); }`; fs.writeFileSync(path.join(pagesDir, `About.js`), componentContent, 'utf-8'); } function renderMarkdownPages(docDir, buildDir) { const pagesDir = path.join(buildDir, 'pages'); const coversDir = path.join(buildDir, 'covers'); const metaFile = path.join(buildDir, 'rawList.json'); const metadataList = []; const pageMapImports = []; const pageMapEntries = []; fse.ensureDirSync(pagesDir); fse.ensureDirSync(coversDir); const folders = fs.readdirSync(docDir).filter(folder => fs.lstatSync(path.join(docDir, folder)).isDirectory() ); folders.forEach(folder => { const folderPath = path.join(docDir, folder); const mdPath = path.join(folderPath, 'index.md'); if (!fs.existsSync(mdPath)) return; let markdown = fs.readFileSync(mdPath, 'utf-8'); const metaMatch = markdown.match(/---([\s\S]*?)---/); const meta = { title: folder, description: '', category: '', createDate: '', updateDate: '' }; if (metaMatch) { const lines = metaMatch[1].split('\n').map(l => l.trim()).filter(Boolean); lines.forEach(line => { const [key, ...rest] = line.split(':'); if (key && rest.length) meta[key.trim()] = rest.join(':').trim(); }); markdown = markdown.replace(metaMatch[0], '').trim(); } const assetsDir = path.join(buildDir, 'assets', folder); markdown = copyLocalImages(markdown, folderPath, assetsDir, `/assets/${folder}`); const htmlContent = `<div style="padding: 9rem 4.5rem;"><h1>${meta.title}</h1><h2 style="color: #4a4a4a; font-weight: normal;">${meta.description}</h2></div>` + '<div style="padding: 0 4.5rem 4.5rem;">' + marked.parse(markdown) + '</div>'; const componentName = folder .replace(/[^a-zA-Z0-9]/g, '_') .replace(/^(\d)/, '_$1') .replace(/(^\w|_\w)/g, m => m.replace('_', '').toUpperCase()); const sourceCover = path.join(folderPath, 'index.png'); let coverUrl = ''; if (fs.existsSync(sourceCover)) { const destCover = path.join(coversDir, `${componentName}.png`); fs.copyFileSync(sourceCover, destCover); coverUrl = `./covers/${componentName}.png`; } metadataList.push({ path: `/${componentName}`, coverUrl, ...meta }); const componentContent = ` import React from 'react'; export default function ${componentName}() { return ( <div dangerouslySetInnerHTML={{ __html: ${JSON.stringify(htmlContent)} }} /> ); }`; fs.writeFileSync(path.join(pagesDir, `${componentName}.js`), componentContent, 'utf-8'); pageMapImports.push(`import ${componentName} from "./pages/${componentName}.js";`); pageMapEntries.push(` "/${componentName}": ${componentName},`); }); fs.writeFileSync(metaFile, JSON.stringify(metadataList, null, 2), 'utf-8'); const pageMapContent = `${pageMapImports.join('\n')} const pageMap = { ${pageMapEntries.join('\n')} }; export default pageMap; `; fs.writeFileSync(path.join(buildDir, 'pageMap.js'), pageMapContent, 'utf-8'); } function generateSite() { const cwd = process.cwd(); // Current command execution path const docDir = path.join(cwd, 'doc'); const configPath = path.join(cwd, 'setting.json'); const __filename = fileURLToPath(import.meta.url); // path to current file const __dirname = path.dirname(__filename); const buildDir = path.join(__dirname, '../', 'src'); const libDir = __dirname; const templateDir = path.join(__dirname, 'template'); if (fs.existsSync(buildDir)) { fse.removeSync(buildDir); } fse.ensureDirSync(buildDir); const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); fse.copySync(templateDir, buildDir); const indexPath = path.join(buildDir, 'index.html'); let indexHtml = fs.readFileSync(indexPath, 'utf-8'); indexHtml = injectSettings(indexHtml, config); fs.writeFileSync(indexPath, indexHtml, 'utf-8'); if (config.favicon && fs.existsSync(config.favicon)) { const destFavicon = path.join(buildDir, path.basename(config.favicon)); fs.copyFileSync(config.favicon, destFavicon); } if (fs.existsSync(docDir) && fs.existsSync(libDir)) { renderMarkdownPages(docDir, buildDir); renderAboutPage(cwd, buildDir); } console.log(`✅ Portfolio site generated in ${buildDir}`); } export { generateSite };