diff --git a/build.js b/build.js index 7968408..b74919a 100644 --- a/build.js +++ b/build.js @@ -189,15 +189,17 @@ async function updateGraphWithBlogNodes(posts, graphPath) { * Bundle all JavaScript files into a single bundle.js using esbuild */ async function bundleJavaScript() { - const entryPoint = path.join(__dirname, 'website', 'scripts', 'main.js'); - const outfile = path.join(__dirname, 'build', 'scripts', 'bundle.js'); + const entryPointMain = path.join(__dirname, 'website', 'scripts', 'main.js'); + const entryPointBlog = path.join(__dirname, 'website', 'scripts', 'blogPage.js'); + const outdir = path.join(__dirname, 'build', 'scripts'); const scriptsDir = path.join(__dirname, 'build', 'scripts'); try { await esbuild.build({ - entryPoints: [entryPoint], + entryPoints: [entryPointMain, entryPointBlog], bundle: true, - outfile: outfile, + outdir: outdir, + entryNames: '[name].bundle', format: 'esm', platform: 'browser', target: 'es2022', @@ -219,7 +221,7 @@ async function bundleJavaScript() { await removeJsFiles(fullPath); // Remove empty directory await fs.rmdir(fullPath); - } else if (item.endsWith('.js') && item !== 'bundle.js') { + } else if (item.endsWith('.js') && !item.endsWith('.bundle.js')) { await fs.unlink(fullPath); } } @@ -305,19 +307,48 @@ async function minifyCSS() { * Update HTML files to use bundled JavaScript */ async function updateHtmlForBundle() { - const htmlPath = path.join(__dirname, 'build', 'index.html'); + const buildDir = path.join(__dirname, 'build'); + + /** + * @param {string} filePath + * @returns {Promise} + */ + async function updateHtmlFile(filePath) { + let html = await fs.readFile(filePath, 'utf-8'); - try { - let html = await fs.readFile(htmlPath, 'utf-8'); - - // Replace module script tag with bundle script tag (keep type="module" for esm format) html = html.replace( /' + '' ); + + html = html.replace(/src="\/scripts\/blogPage\.js"/g, 'src="/scripts/blogPage.bundle.js"'); - await fs.writeFile(htmlPath, html); - console.log('✓ Updated HTML to use bundle.js'); + await fs.writeFile(filePath, html); + } + + /** + * @param {string} dir + * @returns {Promise} + */ + async function walkAndUpdateHtml(dir) { + const items = await fs.readdir(dir); + for (const item of items) { + const fullPath = path.join(dir, item); + const stats = await fs.stat(fullPath); + if (stats.isDirectory()) { + await walkAndUpdateHtml(fullPath); + continue; + } + + if (item.endsWith('.html')) { + await updateHtmlFile(fullPath); + } + } + } + + try { + await walkAndUpdateHtml(buildDir); + console.log('✓ Updated HTML to use JavaScript bundles'); } catch (error) { console.error('✗ Failed to update HTML:', error); throw error; @@ -334,9 +365,11 @@ async function build() { // Report final sizes console.log('\n=== Build Summary ==='); - const bundleStats = await fs.stat(path.join(__dirname, 'build', 'scripts', 'bundle.js')); + const mainBundleStats = await fs.stat(path.join(__dirname, 'build', 'scripts', 'main.bundle.js')); + const blogBundleStats = await fs.stat(path.join(__dirname, 'build', 'scripts', 'blogPage.bundle.js')); const cssStats = await fs.stat(path.join(__dirname, 'build', 'styles', 'main.css')); - console.log(`bundle.js: ${(bundleStats.size / 1024).toFixed(2)} KB`); + console.log(`main.bundle.js: ${(mainBundleStats.size / 1024).toFixed(2)} KB`); + console.log(`blogPage.bundle.js: ${(blogBundleStats.size / 1024).toFixed(2)} KB`); console.log(`main.css: ${(cssStats.size / 1024).toFixed(2)} KB`); console.log('=====================\n'); } diff --git a/tools/StaticBlogGenerator.js b/tools/StaticBlogGenerator.js index 29e9fac..7aea770 100644 --- a/tools/StaticBlogGenerator.js +++ b/tools/StaticBlogGenerator.js @@ -98,7 +98,7 @@ class StaticBlogGenerator { ? `` : ''; - const contentHtml = StaticMarkdownRenderer.render(post.content || ''); + const markdownJson = JSON.stringify(post.content || '').replace(/<\//g, '<\\/'); const content = [ '
', @@ -109,7 +109,8 @@ class StaticBlogGenerator { metaBits.length > 0 ? ` ` : '', tagsHtml, ' ', - `
${contentHtml}
`, + '
', + ` `, '', '
' ].join('\n'); @@ -137,11 +138,21 @@ class StaticBlogGenerator { ` `, ' ', ' ', + ' ', ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', '', '', this.renderHeaderLinks(), bodyHtml, + ' ', '', '' ].join('\n'); @@ -204,178 +215,6 @@ class StaticBlogGenerator { } } -/** - * Lightweight markdown renderer for static blog generation. - */ -class StaticMarkdownRenderer { - /** - * @param {string} markdown - * @returns {string} - */ - static render(markdown) { - const normalized = String(markdown || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n').trim(); - if (!normalized) return ''; - - const codeBlocks = []; - const withCodePlaceholders = normalized.replace(/```([\w-]*)(?:\s*\{[^}]*\})?\s*\n([\s\S]*?)\n?```/g, (_, lang, code) => { - const index = codeBlocks.length; - codeBlocks.push({ lang: lang || '', code: code || '' }); - return `\n__CODEBLOCK_${index}__\n`; - }); - - const blocks = withCodePlaceholders.split(/\n{2,}/); - const parts = blocks.map((block) => { - const trimmed = block.trim(); - if (!trimmed) return ''; - - const codeMatch = trimmed.match(/^__CODEBLOCK_(\d+)__$/); - if (codeMatch) { - const item = codeBlocks[Number(codeMatch[1])]; - const languageClass = item.lang ? ` language-${this.escapeHtml(item.lang)}` : ''; - return `
${this.escapeHtml(item.code)}
`; - } - - if (/^[-*_]{3,}$/.test(trimmed)) { - return '
'; - } - - const heading = trimmed.match(/^(#{1,6})\s+(.+)$/); - if (heading && trimmed.split('\n').length === 1) { - const level = heading[1].length; - return `${this.renderInline(heading[2])}`; - } - - if (/^>\s?/.test(trimmed)) { - const quoteText = trimmed.split('\n').map((line) => line.replace(/^>\s?/, '')).join('\n'); - return `
${this.renderInline(quoteText).replace(/\n/g, '
')}
`; - } - - if (/^[-*+]\s+/.test(trimmed)) { - const items = trimmed - .split('\n') - .filter((line) => /^\s*[-*+]\s+/.test(line)) - .map((line) => line.replace(/^\s*[-*+]\s+/, '')); - return ``; - } - - if (/^\d+\.\s+/.test(trimmed)) { - const items = trimmed - .split('\n') - .filter((line) => /^\s*\d+\.\s+/.test(line)) - .map((line) => line.replace(/^\s*\d+\.\s+/, '')); - return `
    ${items.map((item) => `
  1. ${this.renderInline(item)}
  2. `).join('')}
`; - } - - const imageOnly = trimmed.match(/^!\[([^\]]*)\]\(([^)\s]+)(?:\s+"([^"]*)")?\)$/); - if (imageOnly) { - const imageMeta = this.parseImageMeta(imageOnly[1], imageOnly[2], imageOnly[3] || ''); - const captionHtml = imageMeta.caption ? `
${this.escapeHtml(imageMeta.caption)}
` : ''; - return `
${this.escapeHtml(imageMeta.alt)}${captionHtml}
`; - } - - const lines = trimmed.split('\n').map((line) => this.renderInline(line)); - return `

${lines.join('
')}

`; - }); - - return parts.filter(Boolean).join('\n'); - } - - /** - * @param {string} text - * @returns {string} - */ - static renderInline(text) { - const codeSpans = []; - const withCodePlaceholders = text.replace(/`([^`]+)`/g, (_, code) => { - const index = codeSpans.length; - codeSpans.push(code); - return `__INLINE_CODE_${index}__`; - }); - - let html = this.escapeHtml(withCodePlaceholders); - - html = html.replace(/!\[([^\]]*)\]\(([^)\s]+)(?:\s+"([^&]*)")?\)/g, (_, alt, url, title) => { - const imageMeta = this.parseImageMeta(alt, url, title || ''); - return `${this.escapeHtml(imageMeta.alt)}`; - }); - - html = html.replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+"([^&]*)")?\)/g, (_, label, url, title) => { - const href = this.normalizeUrl(url); - const rel = /^https?:\/\//i.test(href) ? ' rel="noopener noreferrer" target="_blank"' : ''; - const titleAttr = title ? ` title="${this.escapeHtml(title)}"` : ''; - return `${label}`; - }); - - html = html.replace(/\*\*\*([^*]+)\*\*\*/g, '$1'); - html = html.replace(/\*\*([^*]+)\*\*/g, '$1'); - html = html.replace(/\*([^*]+)\*/g, '$1'); - - html = html.replace(/__INLINE_CODE_(\d+)__/g, (_, idx) => { - const code = codeSpans[Number(idx)] || ''; - return `${this.escapeHtml(code)}`; - }); - - return html; - } - - /** - * @param {string} alt - * @param {string} rawUrl - * @param {string} title - * @returns {{src: string, alt: string, caption: string, attributes: string}} - */ - static parseImageMeta(alt, rawUrl, title) { - const scale = this.parseScale(alt); - const src = this.normalizeUrl(rawUrl); - const safeAlt = scale !== null ? '' : String(alt || ''); - const caption = title ? String(title) : (scale !== null ? '' : String(alt || '')); - const widthAttr = scale !== null ? ` style="width:${Math.round(scale * 10000) / 100}%;"` : ''; - - return { - src, - alt: safeAlt, - caption, - attributes: widthAttr - }; - } - - /** - * @param {string} value - * @returns {number | null} - */ - static parseScale(value) { - const normalized = String(value || '').trim(); - if (!/^\d*\.?\d+$/.test(normalized)) return null; - const parsed = Number(normalized); - if (!Number.isFinite(parsed) || parsed <= 0 || parsed > 1) return null; - return parsed; - } - - /** - * @param {string} url - * @returns {string} - */ - static normalizeUrl(url) { - const value = String(url || '').trim(); - if (!value) return '#'; - if (/^(https?:|mailto:|tel:|#|\/)/i.test(value)) return value; - return `/${value.replace(/^\.\//, '').replace(/\\/g, '/')}`; - } - - /** - * @param {string} value - * @returns {string} - */ - static escapeHtml(value) { - return String(value) - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); - } -} - module.exports = { StaticBlogGenerator }; diff --git a/website/blog.html b/website/blog.html index ba57a47..ffa4f0c 100644 --- a/website/blog.html +++ b/website/blog.html @@ -8,7 +8,16 @@ + + + + + + + + +