UNPKG

never-write

Version:
448 lines (371 loc) 13 kB
import path from 'path'; import { parseMarkdown } from '../parse-markdown'; import { getEachFilePath } from '../utils/each-file'; import { chunk, trim, assign, isString } from 'lodash'; import dayjs from 'dayjs'; import fs from 'fs-extra'; import ejs, { render } from 'ejs'; import cheerio, { html } from 'cheerio'; import { DEFAULT_EJS_TEMPLATE, NeverWriteConfig } from '../get-config/config'; import { minify } from 'html-minifier-terser'; const htmlMini = (html: string, needMini: boolean) => { if (!needMini) { return html; } return minify(html, { minifyCSS: true, minifyJS: true, collapseWhitespace: true, removeComments: true, }); }; // ------------------------------------------------------------------------- const presetTemplates: Record<string, { index: string; post: string; tagIndexes: string; }> = { plain: { index: '../../../templates/theme/plain/index.html', tagIndexes: '../../../templates/theme/plain/index.html', post: '../../../templates/theme/plain/post.html', }, 'plain-dark': { index: '../../../templates/theme/plain-dark/index.html', post: '../../../templates/theme/plain-dark/post.html', tagIndexes: '../../../templates/theme/plain-dark/index.html', }, 'cactus-dark': { index: '../../../templates/theme/cactus-dark/index.html', post: '../../../templates/theme/cactus-dark/post.html', tagIndexes: '../../../templates/theme/cactus-dark/index.html', }, }; // ------------------------------------------------------------------------- // output interface interface Out { relativePath: string; sourceFileStat: fs.Stats; filename: string; filenameWithoutExt: string; site: Record<string, any>; render: Record<string, any>; meta: Record<string, any>; html: string; dateFormat: { mtime: string; }; text: string; summary: string; postSavedDir: string; postSavedFullPath: string; staticPath: string; namespace: string; home: string; } // ------------------------------------------------------------------------- // generate posts export const generator = async (root: string, config: NeverWriteConfig) => { const { build, site, render: renderConfig } = config; const { outDir } = build; const { pageSize } = renderConfig; const distDir = path.resolve(root, outDir); const postsRoot = path.resolve(root, build.postsRootPath); const home = `${build.publicPath}index.html`; const postTemplate = (() => { let filePath = ''; if (isString(renderConfig.template)) { filePath = path.resolve(__dirname, `${presetTemplates[renderConfig.template].post}`) } else if (renderConfig.template.post) { filePath = path.join(root, renderConfig.template.post); } else { filePath = path.resolve(__dirname, `${presetTemplates[DEFAULT_EJS_TEMPLATE].post}`) } try { return fs.readFileSync(filePath, 'utf-8'); } catch (err) { console.log(`[INFO] ❌ missing template: ${filePath}`); throw err; } })(); const indexedTemplate = (() => { let filePath = ''; if (isString(renderConfig.template)) { filePath = path.resolve(__dirname, `${presetTemplates[renderConfig.template].index}`) } else if (renderConfig.template.index) { filePath = path.join(root, renderConfig.template.index); } else { filePath = path.resolve(__dirname, `${presetTemplates[DEFAULT_EJS_TEMPLATE].index}`) } try { return fs.readFileSync(filePath, 'utf-8'); } catch (err) { console.log(`[INFO] ❌ missing template: ${filePath}`); throw err; } })(); const tagIndexesTemplate = (() => { let filePath = ''; if (isString(renderConfig.template)) { filePath = path.resolve(__dirname, `${presetTemplates[renderConfig.template].tagIndexes}`) } else if (renderConfig.template.tagIndexes) { filePath = path.join(root, renderConfig.template.tagIndexes); } else { filePath = path.resolve(__dirname, `${presetTemplates[DEFAULT_EJS_TEMPLATE].tagIndexes}`) } try { return fs.readFileSync(filePath, 'utf-8'); } catch (err) { console.log(`[INFO] ❌ missing template: ${filePath}`); throw err; } })(); const hook = (() => { if (build.hook) { try { return require(path.join(root,build.hook)); } catch (err) { console.log(`[INFO] ❌ missing hook file: ${build.hook}`); throw err; } } return {}; })(); // let outs: Out[] = []; // ------------------------------------------------------------------------- // got each post meta const sourceMarkdownFiles = getEachFilePath(postsRoot) .filter(({ dir }) => { return dir.endsWith('.md'); }); let outs: Out[] = sourceMarkdownFiles // ------------------------------------------------------------------------- // make each post configuration // if some day its slow, can use multi thread .map(({ dir }: { dir: string }) => { const sourceStat = fs.statSync(dir); const str = fs.readFileSync(dir, 'utf-8'); const { content: md, meta, } = parseMarkdown(str); const relativePath = path.relative(postsRoot, dir); const dirChunk = relativePath.split('/'); const [filename] = dirChunk.pop()?.split('.')!; const relativeDir = dirChunk.join('/'); const postSavedDir = path.resolve(distDir, relativeDir); const savedFilename = `${filename}.html`; const $ = cheerio.load(md); const textContent = trim($.text()); const postRelativePathInSite = path.join(relativeDir, savedFilename); const out: Out = { meta, relativePath: postRelativePathInSite, sourceFileStat: sourceStat, filename: savedFilename, filenameWithoutExt: filename, site: config.site as any, html: md, dateFormat: { mtime: dayjs(sourceStat.mtime).format(renderConfig.dateFormatter), }, text: textContent, summary: textContent.length > renderConfig.summaryLength ? (`${textContent.substring(0, renderConfig.summaryLength)}...`) : textContent, postSavedDir, postSavedFullPath: path.resolve(postSavedDir, savedFilename), staticPath: `${build.publicPath}${postRelativePathInSite}`, namespace: 'post', home, render: renderConfig, }; return out; }); // ------------------------------------------------------------------------- // default is sort by modify time outs = hook.sortBy ? outs.sort(hook.sortBy) : outs.sort((a: Out, b: Out) => { const mTimeA = a.sourceFileStat.mtime.getTime(); const mTimeB = b.sourceFileStat.mtime.getTime(); return mTimeB - mTimeA; }); // ------------------------------------------------------------------------- // generate each post // tags interface Tag { [propType: string]: { posts: Out[]; staticPath: string; }; }; const tags: Tag = {}; for (let idx = 0, len = outs.length; idx < len; idx++) { const post = outs[idx]; const html = await htmlMini(ejs.render(postTemplate, assign(post, { prevPost: outs[idx - 1], nextPost: outs[idx + 1], })), build.htmlMinify); if (hook.eachBeforeRenderPost) { await hook.eachBeforeRenderPost(post); } console.log(`[info] saving ${post.relativePath}`); // make sure has dir await fs.mkdirp(post.postSavedDir); fs.writeFile(post.postSavedFullPath, html); if (hook.eachAfterRenderPost) { await hook.eachAfterRenderPost(post); } // got tag info let tag = post.meta.tag || []; tag.forEach((tagName: string) => { if (!tags[tagName]) { tags[tagName] = { posts: [], staticPath: `${build.publicPath}tags/${tagName}/1.html`, }; } tags[tagName].posts.push(post); }); } // ------------------------------------------------------------------------- // generate tags { const allTagStoreRoot = path.join(distDir, '/tags/'); for (let i of Object.entries(tags)) { const [tagName, tagInfo] = i; const { posts } = tagInfo; const storeRoot = path.join(allTagStoreRoot, tagName); const chunks = chunk(posts, pageSize); const totalCount = posts.length; for (let pageIndex = 0, len = chunks.length; pageIndex < len; pageIndex++) { const chunk = chunks[pageIndex]; const page = pageIndex + 1; const options = { posts: chunk, page, pageTotal: len, totalCount, selfPath: `/tags/${tagName}/${page}.html`, prevPagePath: page === 1 ? '' : `/tags/${tagName}/${page - 1}.html`, nextPagePath: page === len ? '' : `/tags/${tagName}/${page + 1}.html`, selfStaticPath: `${build.publicPath}tags/${tagName}/${page}.html`, prevPageStaticPath: page === 1 ? '' : `${build.publicPath}tags/${tagName}/${page - 1}.html`, nextPageStaticPath: page === len ? '' : `${build.publicPath}tags/${tagName}/${page + 1}.html`, isFirstPage: page === 1, isEndPage: page === len, site: config.site, namespace: 'tag', tag: tagName, tags, home, render: renderConfig, }; const postListHtml = await htmlMini(ejs.render(tagIndexesTemplate, options), build.htmlMinify); const filename = `${page}.html`; const pathPrefix = path.join('/tags/', tagName); const hookOptions = { ...options, html: postListHtml, storeRoot, filename, pathPrefix, }; await fs.mkdirp(storeRoot); if (hook.eachBeforeRenderTagsIndexes) { hook.eachBeforeRenderTagsIndexes(hookOptions); } console.log(`[info] saving ${path.join(pathPrefix, filename)}`); fs.writeFileSync(path.join(storeRoot, filename), postListHtml); if (hook.eachAfterRenderTagsIndexes) { hook.eachAfterRenderTagsIndexes(hookOptions); } } } } // ------------------------------------------------------------------------- // generate indexes { const chunks = chunk(outs, pageSize); const totalCount = outs.length; for (let pageIndex = 0, len = chunks.length; pageIndex < len; pageIndex++) { const chunk = chunks[pageIndex]; const page = pageIndex + 1; const options = { posts: chunk, page, pageTotal: len, totalCount, selfPath: `/page/${page}.html`, prevPagePath: page === 2 ? '/index.html' : `/page/${page - 1}.html`, nextPagePath: `/page/${page + 1}.html`, selfStaticPath: `${build.publicPath}page/${page}.html`, prevPageStaticPath: page === 2 ? `${build.publicPath}index.html` : `${build.publicPath}page/${page - 1}.html`, nextPageStaticPath: `${build.publicPath}page/${page + 1}.html`, isFirstPage: page === 1, isEndPage: page === len, site: config.site, namespace: 'indexes', tags, tag: '', home, render: renderConfig, }; const postListHtml = await htmlMini( ejs.render(indexedTemplate, options), build.htmlMinify, ); let filename = 'index.html'; let pathPrefix = '/'; if (pageIndex !== 0) { filename = `${page}.html`; pathPrefix = '/page/'; } const storeRoot = path.join(distDir, pathPrefix); const hookOptions = { ...options, html: postListHtml, storeRoot, filename, pathPrefix, }; if (hook.eachBeforeRenderIndexes) { hook.eachBeforeRenderIndexes(hookOptions); } await fs.mkdirp(storeRoot); console.log(`[info] saving ${path.join(pathPrefix, filename)}`); fs.writeFileSync(path.join(storeRoot, filename), postListHtml); if (hook.eachAfterRenderIndexes) { hook.eachAfterRenderIndexes(hookOptions); } } } // ------------------------------------------------------------------------- // copy public resource to dist root if (build.publicResourcePath) { try { const publicResourcePath = path.resolve(root, build.publicResourcePath); fs.statSync(publicResourcePath); fs.copySync(publicResourcePath, path.resolve(distDir, build.publicResourcePath)); } catch (err) { throw err; } } else { // ------------------------------------------------------------------------- // default copy `${root}/public` to `${dist}/public` const defaultPublicResourcePath = './public'; try { const publicResourcePath = path.resolve(root, defaultPublicResourcePath); fs.copySync(publicResourcePath, path.resolve(distDir, defaultPublicResourcePath)); } catch {} } };