poststore
Version:
PostStore can be used with NEXT.JS to create markdown-based blogs.
1,545 lines (1,516 loc) • 49.5 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', { value: true });
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
function _interopNamespace(e) {
if (e && e.__esModule) { return e; } else {
var n = {};
if (e) {
Object.keys(e).forEach(function (k) {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () {
return e[k];
}
});
});
}
n['default'] = e;
return n;
}
}
var path = require('path');
var path__default = _interopDefault(path);
var process$1 = require('process');
var fs = _interopDefault(require('fs'));
var format = _interopDefault(require('date-fns/format'));
var matter = _interopDefault(require('gray-matter'));
var unified = _interopDefault(require('unified'));
var remark = _interopDefault(require('remark-parse'));
var remark2rehype = _interopDefault(require('remark-rehype'));
var rehypeSlug = _interopDefault(require('rehype-slug'));
var rehypeHtml = _interopDefault(require('rehype-stringify'));
var url = _interopDefault(require('url'));
var unistUtilSelect = require('unist-util-select');
var crypto = _interopDefault(require('crypto'));
var visit$1 = _interopDefault(require('unist-util-visit'));
var Prism = _interopDefault(require('prismjs'));
var rehypeParse = _interopDefault(require('rehype-parse'));
var chalk = _interopDefault(require('chalk'));
var loadLanguage = _interopDefault(require('prismjs/components/'));
var rehypeSanitize = _interopDefault(require('rehype-sanitize'));
var sharp = _interopDefault(require('sharp'));
var chokidar = require('chokidar');
const MODE_DEV = process.env['NODE_ENV'] === 'development' ? true : false;
const MODE_TEST = process.env['NODE_ENV'] === 'test' ? true : false;
const MODE_PRODUCTION = process.env['NODE_ENV'] === 'production' ? true : false;
const PLATFROM_DARWIN = process$1.platform === 'darwin';
const HASH_ALGORITHM = 'sha1';
const NODE_TYPE_CATEGORY = 'category';
const NODE_TYPE_POST = 'post';
const DEFAULT_PARAM_VALUE = 'slug';
const DEFAULT_PERPAGE_VALUE = 10;
const DEFAULT_BUILDINFO_PATH = './.poststore.buildInfo';
const DEFAULT_CONFIG_PATH = './poststore.config.js';
const DEFAULT_ASSET_DIRNAME = 'assets';
const DEFAULT_PUBLICDIR_PATH = './public';
const DEFAULT_PARAM_OPTION = {
page: DEFAULT_PARAM_VALUE,
category: DEFAULT_PARAM_VALUE,
post: DEFAULT_PARAM_VALUE,
tag: DEFAULT_PARAM_VALUE,
};
const DEFAULT_PERPAGE_OPTION = {
page: DEFAULT_PERPAGE_VALUE,
category: DEFAULT_PERPAGE_VALUE,
tag: DEFAULT_PERPAGE_VALUE,
};
const CONFIG_EXAMPLE = `export default {
storeOption: StoreOption | StoreOption[]
}
interface StoreOption {
postDir: string;
storeName?: string;
perPage?: number | PerPageOption;
pageParam?: string | PageParamOption;
shouldUpdate?: boolean;
watchMode?: boolean;
incremental?: boolean;
}`;
const storeMap = new Map();
const watcherMap = new Map();
function* visit(rootNode, fn) {
yield [fn(rootNode), rootNode];
if (rootNode.children) {
for (const node of rootNode.children) {
yield* visit(node, fn);
}
}
}
const findNode = (rootNode, fn) => {
for (const [isTrue, node] of visit(rootNode, fn)) {
if (isTrue)
return node;
}
};
const findNodeAll = (rootNode, fn) => {
const nodeList = [];
for (const [isTrue, node] of visit(rootNode, fn)) {
if (isTrue)
nodeList.push(node);
}
return nodeList;
};
const isCategoryNode = (node) => node.type === NODE_TYPE_CATEGORY;
const isPostNode = (node) => node.type === NODE_TYPE_POST;
const findCategory = (rootNode, categories) => {
let now = rootNode;
for (const category of categories) {
const finded = findNode(now, (node) => isCategoryNode(node) && node.slug === category);
if (!finded)
break;
now = finded;
}
return now;
};
const getPostsAll = (rootNode) => findNodeAll(rootNode, isPostNode);
const getCategoriesAll = (rootNode) => findNodeAll(rootNode, isCategoryNode);
const getTagsAll = (rootNode) => {
const posts = getPostsAll(rootNode);
const tags = posts.reduce((tagList, now) => {
now.postData.tags.forEach((tag) => tagList.add(tag));
return tagList;
}, new Set());
return [...tags];
};
const getPostBy = (propName) => (rootNode, value) => {
const finded = findNode(rootNode, (node) => isPostNode(node) && node[propName] === value);
if (finded && isPostNode(finded))
return finded;
};
const getPostBySlug = getPostBy('slug');
const getPostByPath = getPostBy('path');
const getPostsByCategories = (rootNode, categories) => {
const categoryNode = findCategory(rootNode, categories);
const posts = getPostsAll(categoryNode);
return posts;
};
const getPostsByTags = (rootNode, tags) => {
return findNodeAll(rootNode, (node) => {
if (!isPostNode(node))
return false;
let isMatch = false;
const postTags = node.postData.tags;
isMatch = tags.some((tag) => postTags.includes(tag));
return isMatch;
});
};
const getPostsByPage = (postList, page, perPage = 5) => {
const start = (page - 1) * perPage;
const end = start + perPage;
return postList.slice(start, end);
};
const getTotalPage = (total, perPage) => {
return Math.ceil(total / perPage);
};
const isPageSlug = (slug) => {
const pagePointer = slug.length - 2;
if (pagePointer < 0)
return false;
if (slug[pagePointer].toLowerCase() === 'page')
return true;
return false;
};
const trimPagePath = (slug) => {
if (!isPageSlug(slug))
return slug;
return slug.slice(0, -2);
};
const getPageNum = (slug) => {
if (!isPageSlug(slug))
return -1;
return +slug[slug.length - 1];
};
const pagePathFilter = (pathList, slug) => pathList
.map((path) => path.params[slug])
.filter((slug) => !isPageSlug(slug));
const makeHash = (content, encoding = 'base64') => crypto.createHash(HASH_ALGORITHM).update(content, 'utf8').digest(encoding);
const makeSetLike = (arr) => [...new Set(arr)];
const isSubDir = (parent, child, includeSelf = true) => {
const [normalizedParentPath, normalizedChildPath] = [parent, child].map(path__default.normalize);
const relativePath = path__default.relative(normalizedParentPath, normalizedChildPath);
if (!relativePath)
return includeSelf;
const splittedPath = relativePath.split(path__default.sep);
const isDotSegment = splittedPath[0] !== '..';
return isDotSegment ? true : false;
};
const debounce = (fn, time = 300) => {
let timerId;
return (...args) => {
if (timerId)
clearTimeout(timerId);
timerId = setTimeout(() => {
fn(...args);
}, time);
};
};
const fillToOwnProperty = (obj, value) => {
const coppied = { ...obj };
Object.getOwnPropertyNames(obj).forEach((key) => (coppied[key] = value));
return coppied;
};
const sortRule = (a, b) => {
const dateDiff = b.postData.date - a.postData.date;
if (dateDiff === 0) {
const aTitle = a.postData.title.toLowerCase();
const bTitle = b.postData.title.toLowerCase();
const firstCharDiff = aTitle[0].localeCompare(bTitle[0]);
if (firstCharDiff === 0)
return aTitle.localeCompare(bTitle);
return firstCharDiff;
}
return dateDiff;
};
const isURL = (input) => !!url.parse(input).protocol;
var rehypeAsset = (filePath, info) => () => {
return (root) => {
info.assets = [];
const imageNodes = unistUtilSelect.selectAll('element[tagName="img"]', root);
const imageNodeHandler = (node) => {
const { src } = node.properties;
const decodedPath = decodeURIComponent(src);
const { isAbsolute } = path__default;
if (isURL(decodedPath))
return;
let sourcePath = decodedPath;
let extName = '';
if (!isAbsolute(sourcePath)) {
const fileDir = path__default.dirname(filePath);
sourcePath = path__default.resolve(fileDir, decodedPath);
extName = path__default.extname(sourcePath);
}
const hashedPath = makeHash(sourcePath, 'hex');
const targetPath = `/${DEFAULT_ASSET_DIRNAME}/${hashedPath}${extName}`;
const assetInfo = {
sourcePath,
targetPath,
};
node.properties.src = targetPath;
if (node.properties.alt === 'thumbnail') {
if (!node.properties.className)
node.properties.className = [];
node.properties.className.push('thumbnail-image');
info.thumbnail = targetPath;
}
info.assets.push(assetInfo);
};
imageNodes.forEach(imageNodeHandler);
if (!info.thumbnail) {
info.thumbnail = '';
}
return root;
};
};
const styledInputValue = (inputValue) => inputValue ? chalk `{bold (${inputValue})}` : '';
const getStyledErrorMsg = (msg, inputValue) => chalk `${formatDate()} - 🍎 {red.bold ERROR:} {yellow ${msg}} {red ${styledInputValue(inputValue)}}`;
const getStyledCautionMsg = (msg, inputValue) => chalk `${formatDate()} - 🍋 {yellow.bold Caution:} {magenta ${msg}} {yellow ${styledInputValue(inputValue)}}`;
const getStyledInfoMsg = (msg, inputValue) => chalk `${formatDate()} - 🥬 {green.bold INFO} {yellow ${msg}} {green ${styledInputValue(inputValue)}}`;
const getStyledLogMsg = (msg, inputValue) => chalk `${formatDate()} - 🥑 {cyan.bold LOG} {green ${msg}} {cyan ${styledInputValue(inputValue)}}`;
const formatDate = () => format(Date.now(), 'yyyy-MM-dd HH:mm:ss');
loadLanguage(['jsx', 'tsx', 'typescript', 'scss', 'sql', 'json', 'bash']);
const langPrefix = 'language-';
const getLanguage = (node) => {
const className = node.properties && node.properties.className;
if (!Array.isArray(className))
return '';
const languageClass = className.find((name) => name.startsWith(langPrefix));
if (!languageClass)
return '';
const language = languageClass.substr(langPrefix.length);
return language;
};
const checkCode = (node) => {
if (!node.children || !node.children.length)
return false;
if (node.children[0].type !== 'text' || !node.children[0].value)
return false;
return true;
};
const getCode = (node) => {
return checkCode(node) ? node.children[0].value : '';
};
const setCode = (node, code) => {
const fragment = unified()
.use(rehypeParse, { fragment: true })
.parse(code);
if (fragment.children) {
node.children = [...fragment.children];
}
};
const nodeEditor = (node) => {
if (node.tagName === 'code') {
const language = getLanguage(node);
const code = getCode(node);
if (!language || !code)
return;
try {
const highlightedCode = Prism.highlight(code, Prism.languages[language], language);
setCode(node, highlightedCode);
}
catch {
console.log(getStyledErrorMsg('Code Syntax highlighter error', `language: ${language}, code: ${code.replace('\n', '').substr(0, 32) + '...'}`));
return;
}
}
};
var rehypePrism = () => {
return (root) => {
return visit$1(root, 'element', nodeEditor);
};
};
const embedableLinkRegex = /https?:\/\/(www\.)?youtube\.com\/watch\?v=([a-zA-Z0-9_\-]+)(\?start=(\d+))?[-a-zA-Z0-9()@:%_\+.~#?&//=]*!!/i;
const getIframeCode = ({ videoID, startTime }) => {
const appendix = startTime ? `?start=${startTime}` : '';
const result = `<iframe src="https://www.youtube.com/embed/${videoID}${appendix}" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>`;
return result;
};
const isLinkNode = (node) => {
if (node.tagName === 'a' && node.properties && node.properties.href != null)
return true;
return false;
};
const isEmbedable = (href) => embedableLinkRegex.test(href);
const getVideoInfo = (href) => {
const executed = embedableLinkRegex.exec(href);
if (!executed)
return;
const videoID = executed[2];
const startTime = executed[4];
return {
videoID,
startTime,
};
};
const setCode$1 = (node, code) => {
const fragment = unified()
.use(rehypeParse, { fragment: true })
.parse(code);
if (!fragment.children)
return;
node.tagName = fragment.tagName;
node.type = fragment.type;
if (!node.properties.className)
node.properties.className = [];
node.properties.className.push('youtube-video-iframe');
node.children = [...fragment.children];
};
const nodeEditor$1 = (node) => {
if (isLinkNode(node)) {
const href = node.properties.href;
if (!isEmbedable(href))
return;
const videoInfo = getVideoInfo(href);
if (!videoInfo)
return;
const iframeCode = getIframeCode(videoInfo);
setCode$1(node, iframeCode);
}
return;
};
var rehypeYoutube = () => {
return (root) => {
return visit$1(root, 'element', nodeEditor$1);
};
};
var rehypeExcerpt = (info, length = 180) => () => {
return (root) => {
const nodes = unistUtilSelect.selectAll('element[tagName="p"] text', root);
const fullText = nodes.reduce((excerpt, { value }) => {
const trimmedValue = value?.trim();
if (!trimmedValue)
return excerpt;
return `${excerpt} ${trimmedValue}`;
}, '');
const trimmedFullText = fullText.trim();
if (!trimmedFullText) {
info.excerpt = '';
return root;
}
const subText = [...trimmedFullText].slice(0, length).join('');
const ellipsis = trimmedFullText.length > length ? '...' : '';
info.excerpt = subText.trim() + ellipsis;
return root;
};
};
var strip = [
"script"
];
var clobberPrefix = "user-content-";
var clobber = [
"name",
"id"
];
var ancestors = {
tbody: [
"table"
],
tfoot: [
"table"
],
thead: [
"table"
],
td: [
"table"
],
th: [
"table"
],
tr: [
"table"
]
};
var protocols = {
href: [
"http",
"https",
"mailto",
"xmpp",
"irc",
"ircs"
],
cite: [
"http",
"https"
],
src: [
"http",
"https"
],
longDesc: [
"http",
"https"
]
};
var tagNames = [
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"br",
"b",
"i",
"strong",
"em",
"a",
"pre",
"code",
"img",
"tt",
"div",
"ins",
"del",
"sup",
"sub",
"p",
"ol",
"ul",
"table",
"thead",
"tbody",
"tfoot",
"blockquote",
"dl",
"dt",
"dd",
"kbd",
"q",
"samp",
"var",
"hr",
"ruby",
"rt",
"rp",
"li",
"tr",
"td",
"th",
"s",
"strike",
"summary",
"details",
"caption",
"figure",
"figcaption",
"abbr",
"bdo",
"cite",
"dfn",
"mark",
"small",
"span",
"time",
"wbr",
"input"
];
var attributes = {
code: [
"className"
],
a: [
"href"
],
img: [
"src",
"longDesc"
],
input: [
[
"type",
"checkbox"
],
[
"disabled",
true
]
],
li: [
[
"className",
"task-list-item"
]
],
div: [
"itemScope",
"itemType"
],
blockquote: [
"cite"
],
del: [
"cite"
],
ins: [
"cite"
],
q: [
"cite"
],
"*": [
"abbr",
"accept",
"acceptCharset",
"accessKey",
"action",
"align",
"alt",
"ariaDescribedBy",
"ariaHidden",
"ariaLabel",
"ariaLabelledBy",
"axis",
"border",
"cellPadding",
"cellSpacing",
"char",
"charOff",
"charSet",
"checked",
"clear",
"cols",
"colSpan",
"color",
"compact",
"coords",
"dateTime",
"dir",
"disabled",
"encType",
"htmlFor",
"frame",
"headers",
"height",
"hrefLang",
"hSpace",
"isMap",
"id",
"label",
"lang",
"maxLength",
"media",
"method",
"multiple",
"name",
"noHref",
"noShade",
"noWrap",
"open",
"prompt",
"readOnly",
"rel",
"rev",
"rows",
"rowSpan",
"rules",
"scope",
"selected",
"shape",
"size",
"span",
"start",
"summary",
"tabIndex",
"target",
"title",
"type",
"useMap",
"vAlign",
"value",
"vSpace",
"width",
"itemProp"
]
};
var required = {
input: {
type: "checkbox",
disabled: true
}
};
var sanitizeSchema = {
strip: strip,
clobberPrefix: clobberPrefix,
clobber: clobber,
ancestors: ancestors,
protocols: protocols,
tagNames: tagNames,
attributes: attributes,
required: required
};
const spaceRegex = /\s+/g;
const nonAllowedRegex = /[^가-힣a-z\d\s\.\-]/gi;
const slugify = (name, delimeter = '-') => {
const result = name
.normalize()
.replace(nonAllowedRegex, '')
.trim()
.replace(spaceRegex, delimeter);
if (!result)
throw new Error(getStyledErrorMsg(`The name cannot be slugified by the slugify module.`, `name : ${name}`));
return result;
};
const buildInfoPath = DEFAULT_BUILDINFO_PATH;
let buildInfo;
const getCachedData = (filePath, newContent) => {
if (MODE_TEST)
return;
const newHash = makeHash(newContent);
const buildInfo = loadBuildInfo(buildInfoPath);
const cachedInfo = buildInfo[filePath];
if (!cachedInfo)
return;
const { hash: oldHash, content } = cachedInfo;
if (newHash === oldHash)
return content;
};
const saveCache = (filePath, content, data) => {
if (MODE_TEST)
return;
const hash = makeHash(content);
const deepCopied = JSON.parse(JSON.stringify(data));
const cacheData = {
hash,
content: deepCopied,
};
buildInfo[filePath] = cacheData;
};
const loadBuildInfo = (buildInfoPath) => {
if (buildInfo)
return buildInfo;
try {
const rawInfo = fs.readFileSync(buildInfoPath, 'utf8');
buildInfo = JSON.parse(rawInfo);
}
catch {
buildInfo = {};
}
return buildInfo;
};
const buildInfoFileSave = () => {
const stringified = JSON.stringify(buildInfo, null, 2);
if (!stringified)
return;
const writeBuildInfo = () => {
fs.promises.writeFile(buildInfoPath, stringified, 'utf8');
};
fs.promises.unlink(buildInfoPath).then(writeBuildInfo, writeBuildInfo);
};
const fsPromise = fs.promises;
const makePost = async ({ filePath, slugMap, useCache = true, }) => {
const rawText = await fsPromise.readFile(filePath, 'utf8');
const cachedData = useCache
? getCachedData(filePath, rawText)
: undefined;
const { data: { title, date, tags = [], published = true }, content = '', } = matter(rawText);
const refinedDate = await refineDate(filePath, date);
const parsedSlug = parseSlug(filePath, refinedDate);
const refinedSlug = !slugMap
? parsedSlug
: makeUnique(parsedSlug, filePath, slugMap);
const post = await createPostData(filePath, {
title,
tags,
slug: refinedSlug,
date: refinedDate,
categories: [],
isPublished: published,
}, content, cachedData);
if (!cachedData || !useCache)
saveCache(filePath, rawText, {
html: post.html,
tags: post.tags,
assets: post.assets,
excerpt: post.excerpt,
thumbnail: post.thumbnail,
});
return post;
};
const parseSlug = (filePath, timestamp) => {
const basename = path__default.basename(filePath, '.md');
const datePrepended = prependDate(basename, timestamp);
const slugified = slugify(datePrepended);
return slugified;
};
const refineDate = async (filePath, date) => {
if (date == null) {
if (MODE_TEST)
return new Date('1990-04-10').valueOf();
const ctime = (await fsPromise.stat(filePath)).ctime;
return ctime.valueOf();
}
return date.valueOf();
};
const prependDate = (name, timestamp) => {
return `${format(new Date(timestamp), 'yyyy-MM-dd')}-${name}`;
};
const makeUnique = (slug, nodePath, slugMap, salt = 0, newSlug) => {
const target = newSlug || slug;
const isDupl = slugMap.get(target) === true ? true : false;
if (isDupl) {
const nowDir = path__default.dirname(nodePath);
const parentDir = path__default.resolve(nowDir, '..');
const relativeDir = path__default.relative(parentDir, nowDir);
const hash = salt === 0 ? '' : makeHash(relativeDir + `${salt}`, 'hex').substr(0, 5);
const saltedSlug = slug + hash;
return makeUnique(slug, nodePath, slugMap, salt + 1, saltedSlug);
}
slugMap.set(target, true);
return target;
};
const createPostData = async (filePath, { slug, title, date, tags, categories, isPublished }, rawContent, cachedData) => {
const postData = {
slug,
title: !title ? slug : title,
date,
html: '',
tags: makeSetLike(tags.map((tag) => slugify(tag))),
categories: categories ? categories : [],
isPublished,
};
if (cachedData) {
postData.html = cachedData.html;
postData.tags = makeSetLike(cachedData.tags);
postData.assets = cachedData.assets;
postData.excerpt = cachedData.excerpt;
postData.thumbnail = cachedData.thumbnail;
return postData;
}
const { html, info } = await parseMarkdown(filePath, rawContent);
postData.html = html;
postData.assets = info.assets;
postData.excerpt = info.excerpt;
postData.thumbnail = info.thumbnail;
return postData;
};
const parseMarkdown = async (filePath, markdown) => {
const info = {};
const parser = unified()
.use(remark)
.use(remark2rehype)
.use(rehypeSanitize, sanitizeSchema)
.use(rehypeYoutube)
.use(rehypeExcerpt(info))
.use(rehypeAsset(filePath, info))
.use(rehypeSlug)
.use(rehypePrism)
.use(rehypeHtml, {
collapseEmptyAttributes: true,
});
const parsedData = await parser.process(markdown);
const html = parsedData.contents.toString();
return {
html,
info,
};
};
const fsPromise$1 = fs.promises;
const markdownRegex = new RegExp(/\.mdx?$/);
const isDirectory = (dirent) => dirent.isDirectory();
const isMarkdown = (dirent) => dirent.isFile() && markdownRegex.test(dirent.name);
async function getNodeTree({ rootPath, slugMap = new Map(), }) {
const node = await createCategoryNode({
slugMap,
nodePath: rootPath,
});
const subFileList = await fsPromise$1.readdir(rootPath, {
withFileTypes: true,
encoding: 'utf-8',
});
if (!subFileList.length)
return node;
node.children = [];
for (const currentFile of subFileList) {
const currentFilePath = path__default.resolve(rootPath, currentFile.name);
let childNode;
if (isDirectory(currentFile)) {
childNode = await getNodeTree({
slugMap,
rootPath: currentFilePath,
});
if (!childNode.children || childNode.children.length === 0) {
continue;
}
}
else if (isMarkdown(currentFile)) {
try {
childNode = await createPostNode({
slugMap,
nodePath: currentFilePath,
});
const { isPublished, html } = childNode.postData;
if (!isPublished || !html)
continue;
}
catch {
console.log(getStyledCautionMsg(chalk `Failed to parse {bold [ ${currentFile.name} ]} file. Please check the file content.`));
continue;
}
}
else {
continue;
}
node.children.push(childNode);
}
node.children = sortChildren(node.children);
return node;
}
const createNode = (type) => async ({ nodePath, slugMap, }) => {
const name = path__default.basename(nodePath).normalize();
const newNode = {
type,
name,
path: nodePath,
};
if (type === NODE_TYPE_POST) {
const postData = await makePost({ filePath: nodePath, slugMap });
const slug = postData.slug;
newNode.slug = slug;
newNode.postData = postData;
return newNode;
}
newNode.slug = slugify(name);
return newNode;
};
const createCategoryNode = createNode(NODE_TYPE_CATEGORY);
const createPostNode = createNode(NODE_TYPE_POST);
const sortChildren = (nodeList) => {
const categoryList = nodeList.filter(isCategoryNode);
const postList = nodeList.filter(isPostNode);
categoryList.sort(({ name: aName }, { name: bName }) => aName.localeCompare(bName));
postList.sort(sortRule);
return [...categoryList, ...postList];
};
const convertToPath = (paramName) => (slug) => ({
params: {
[paramName]: slug,
},
});
const getCategoriesPaths = (rootNode, perPage = 10, parents = []) => {
const result = [];
if (!rootNode.children)
return result;
if (parents.length) {
result.push(parents);
const pagePaths = getPagePaths(rootNode, perPage);
pagePaths.forEach((path) => result.push([...parents, ...path]));
}
for (const node of rootNode.children) {
if (isCategoryNode(node)) {
const category = [...parents, node.slug];
result.push(...getCategoriesPaths(node, perPage, category));
}
}
return result;
};
const getTagsPaths = (rootNode, perPage = 10) => {
const tagPaths = [];
const tags = getTagsAll(rootNode);
for (const tag of tags) {
tagPaths.push([tag]);
const pagePaths = getPagePaths(rootNode, perPage, (node) => isPostNode(node) && node.postData.tags.includes(tag));
pagePaths.forEach((path) => tagPaths.push([tag, ...path]));
}
return tagPaths;
};
const getPagePaths = (rootNode, perPage = 5, nodeCondition = isPostNode) => {
const pagePaths = [];
const posts = findNodeAll(rootNode, nodeCondition);
const length = posts.length;
const pageCount = getTotalPage(length, perPage);
const prefix = 'page';
for (let i = 1; i <= pageCount; i++) {
pagePaths.push([prefix, `${i}`]);
}
return pagePaths;
};
const getPathList = ({ rootNode, paramOption: { category: categoryParam, page: pageParam, post: postParam, tag: tagParam, }, perPageOption: { page: pagesPerMain, category: pagesPerCategoryPage, tag: pagesPerTagPage, }, }) => {
const convertToPostPath = convertToPath(postParam);
const convertToCategoryPath = convertToPath(categoryParam);
const convertToPagePath = convertToPath(pageParam);
const convertToTagPath = convertToPath(tagParam);
const postList = getPostsAll(rootNode);
const post = postList.map(({ slug }) => convertToPostPath(slug));
const category = getCategoriesPaths(rootNode, pagesPerCategoryPage).map(convertToCategoryPath);
const page = getPagePaths(rootNode, pagesPerMain).map(convertToPagePath);
const tag = getTagsPaths(rootNode, pagesPerTagPage).map(convertToTagPath);
return {
post,
category,
page,
tag,
};
};
const makePropList = ({ rootNode, pathList, paramOption, perPageOption, }) => {
const global = makeGlobalProp(rootNode);
const category = makeListPageProp({
rootNode,
pathList: pathList.category,
perPage: perPageOption.category,
pageParam: paramOption.category,
getPostsFn: getPostsByCategories,
pageCategory: 'category',
});
const tag = makeListPageProp({
rootNode,
pathList: pathList.tag,
perPage: perPageOption.tag,
pageParam: paramOption.tag,
getPostsFn: getPostsByTags,
pageCategory: 'tag',
});
const page = makeListPageProp({
rootNode,
perPage: perPageOption.page,
pathList: pathList.page,
pageParam: paramOption.page,
getPostsFn: getPostsAll,
sort: true,
pageCategory: 'page',
});
const post = makePostPageProp(rootNode, pathList.post, paramOption.post);
return {
global,
category,
page,
tag,
post,
};
};
const makeGlobalProp = (rootNode) => {
const categoryCount = getCategoriesAll(rootNode).length - 1;
const postCount = getPostsAll(rootNode).length;
const tagCount = getTagsAll(rootNode).length;
const buildTime = MODE_DEV || MODE_TEST ? 0 : Date.now();
const categoryTree = makeCategoryTree(rootNode);
const tagList = makeTagList(rootNode);
return {
categoryCount,
postCount,
tagCount,
categoryTree,
tagList,
buildTime,
};
};
const makeCategoryTree = (rootNode, isRoot = true) => {
const newNode = {
name: rootNode.name,
slug: rootNode.slug,
postCount: getPostsAll(rootNode).length,
};
if (!rootNode.children)
return newNode;
for (const child of rootNode.children) {
if (isCategoryNode(child)) {
if (!newNode.childList)
newNode.childList = [];
if (isRoot) {
newNode.childList.push(makeCategoryTree(child, false));
continue;
}
const childNode = makeCategoryTree({
...child,
...{ slug: `${newNode.slug}/${child.slug}` },
}, false);
newNode.childList.push(childNode);
}
}
return newNode;
};
const makeTagList = (rootNode) => {
const tags = getTagsAll(rootNode);
const infoList = tags.map((tag) => ({
name: tag,
slug: tag,
postCount: getPostsByTags(rootNode, [tag]).length,
}));
return infoList;
};
const makeListPageProp = ({ rootNode, pathList, pageParam, pageCategory, perPage = 10, getPostsFn = getPostsAll, sort = false, }) => {
const propMap = {};
for (const path of pathList) {
const slug = path.params[pageParam];
const isPage = isPageSlug(slug);
const trimmedSlug = trimPagePath(slug);
const posts = getPostsFn(rootNode, trimmedSlug);
const totalPage = getTotalPage(posts.length, perPage);
if (sort)
posts.sort(sortRule);
const currentPage = isPage ? getPageNum(slug) : 1;
const postList = getPostsByPage(posts, currentPage, perPage).map((node) => node.postData);
const count = postList.length;
const joinedSlug = slug.join('/');
const prop = {
slug: joinedSlug,
pageCategory,
count,
currentPage,
perPage,
postList,
totalPage,
};
propMap[joinedSlug] = prop;
}
return propMap;
};
const makePostPageProp = (rootNode, pathList, pageParam) => {
const postMap = {};
for (const path of pathList) {
const slug = path.params[pageParam];
const post = getPostBySlug(rootNode, slug);
if (!post)
continue;
const postData = post.postData;
const categories = postData.categories;
let relatedPosts = [];
if (categories.length > 0) {
relatedPosts = getPostsByCategories(rootNode, categories)
.filter((node) => node.slug !== slug)
.map((node) => node.postData);
}
postMap[slug] = { slug, postData, relatedPosts, pageCategory: 'post' };
}
return postMap;
};
const fsPromise$2 = fs.promises;
const copyAssetsTo = (publicDir, imageMaxWidth = 600) => async (assetList) => {
const assetDir = path__default.resolve(publicDir, `./${DEFAULT_ASSET_DIRNAME}`);
try {
if (MODE_PRODUCTION) {
await fsPromise$2.rmdir(assetDir, { recursive: true });
}
await fsPromise$2.stat(assetDir);
}
catch (e) {
if (e.code === 'ENOENT')
fsPromise$2.mkdir(assetDir, { recursive: true });
}
const operations = assetList.map(async ({ sourcePath, targetPath }) => {
const writePath = path__default.join(publicDir, targetPath);
try {
await fsPromise$2.stat(writePath);
}
catch {
const ext = path.extname(sourcePath);
if (['.jpeg', '.jpg', '.png'].includes(ext)) {
const image = sharp(sourcePath);
const metadata = await image.metadata();
const width = metadata.width;
if (width && width > imageMaxWidth) {
image.resize({ width: imageMaxWidth });
}
if (ext === '.png') {
image.png();
}
else if (['.jpeg', '.jpg'].includes(ext)) {
image.jpeg();
}
return image.trim().toFile(writePath);
}
return fsPromise$2.copyFile(sourcePath, path__default.join(publicDir, targetPath), fs.constants.COPYFILE_EXCL | fs.constants.COPYFILE_FICLONE);
}
return Promise.reject();
});
const settledResult = (await Promise.allSettled(operations)).map((res) => res.status);
const result = {
fulfilled: settledResult.filter((status) => status === 'fulfilled'),
rejected: settledResult.filter((status) => status === 'rejected'),
};
const settledLength = result.fulfilled.length;
if (settledLength > 0 && !MODE_PRODUCTION) {
const resultMsg = `${result.fulfilled.length} image files have been created.`;
console.log(getStyledLogMsg(resultMsg));
}
return result;
};
const getAssetList = (rootNode) => {
const postList = getPostsAll(rootNode).map((node) => node.postData);
const assetList = postList.reduce((acc, now) => {
if (now.assets)
acc = acc.concat(now.assets);
return acc;
}, []);
return assetList;
};
const makeStore = async ({ postDir, storeName, perPage, pageParam, incremental, imageMaxWidth, }) => {
const [paramOption, perPageOption] = normalizeOption(pageParam, perPage);
const rootNode = await getNodeTree({
rootPath: postDir,
});
const pathList = getPathList({
paramOption,
perPageOption,
rootNode: rootNode,
});
appendExtraToPost({
rootNode,
categoryPathList: pathList.category,
categoryParamName: paramOption.category,
});
const propList = makePropList({
paramOption,
perPageOption,
rootNode: rootNode,
pathList: pathList,
});
const info = {
name: storeName ? storeName : rootNode.name,
postDir: postDir,
};
const options = {
postDir,
perPage: perPageOption,
pageParam: paramOption,
};
const assetList = getAssetList(rootNode);
await copyAssetsTo(DEFAULT_PUBLICDIR_PATH, imageMaxWidth)(assetList);
const store = {
rootNode,
pathList,
propList,
info,
options,
};
if (incremental)
buildInfoFileSave();
storeMap.set(postDir, store);
return store;
};
const makePathStore = async ({ postDir, perPage, pageParam, }) => {
const [paramOption, perPageOption] = normalizeOption(pageParam, perPage);
const rootNode = await getNodeTree({
rootPath: postDir,
});
const pathList = getPathList({
paramOption,
perPageOption,
rootNode: rootNode,
});
return {
pathList,
};
};
const appendExtraToPost = ({ rootNode, categoryPathList, categoryParamName, }) => {
const trimmedCategories = pagePathFilter(categoryPathList, categoryParamName);
for (const category of trimmedCategories) {
const posts = getPostsByCategories(rootNode, category);
let prev;
for (const { postData: now } of posts) {
if (prev) {
now.prevPost = {
slug: prev.slug,
title: prev.title,
};
prev.nextPost = {
slug: now.slug,
title: now.title,
};
}
now.categories = category;
prev = now;
}
}
};
const normalizeOption = (paramOption, perPageOption) => {
let paramOptionResult = typeof paramOption === 'object'
? { ...DEFAULT_PARAM_OPTION, ...paramOption }
: paramOption
? fillToOwnProperty(DEFAULT_PARAM_OPTION, paramOption)
: DEFAULT_PARAM_OPTION;
let countOptionResult = typeof perPageOption === 'object'
? { ...DEFAULT_PERPAGE_OPTION, ...perPageOption }
: perPageOption
? fillToOwnProperty(DEFAULT_PERPAGE_OPTION, perPageOption)
: DEFAULT_PERPAGE_OPTION;
return [paramOptionResult, countOptionResult];
};
const startWatchMode = (props) => {
console.log(getStyledInfoMsg('Start Watch mode..', props.postDir));
const parentWatcherKey = getParentWatcherKey(props.postDir);
const watcher = parentWatcherKey
? watcherMap.get(parentWatcherKey)
: chokidar.watch(props.postDir, {
persistent: true,
ignoreInitial: true,
interval: 100,
binaryInterval: 3000,
});
watcherMap.set(props.postDir, watcher);
const updateStore = debounce(async (msg) => {
await makeStore(props);
console.log(msg);
}, 200);
const changeFileHandler = async (filePath) => {
const store = storeMap.get(props.postDir);
let newPostData;
try {
newPostData = await makePost({
filePath: filePath,
useCache: false,
});
}
catch {
console.log(getStyledCautionMsg(chalk `Failed to parse {bold [ ${path__default.basename(filePath)} ]} file. Please check the file content.`));
return;
}
if (!newPostData.html)
return;
const post = getPostByPath(store.rootNode, filePath);
if (!post) {
updateStore(getStyledLogMsg(chalk `{red.bold There is no stored post corresponding to the [ ${path__default.basename(filePath)} ] file.} The Store is created again.`, `store : ${store.info.name}`));
return;
}
updatePostData(post.postData, newPostData, [
'title',
'html',
'tags',
'date',
'excerpt',
'thumbnail',
]);
if (newPostData.assets) {
await copyAssetsTo(DEFAULT_PUBLICDIR_PATH)(newPostData.assets);
}
console.log(getStyledLogMsg(chalk `{bold [ ${newPostData.title} ]} Post updated.`, `store : ${store.info.name}`));
if (props.incremental)
buildInfoFileSave();
};
const rawHandlerForDarwin = async (eventName, filePath, detail) => {
if (MODE_TEST)
console.log({
event: detail.event,
flags: detail.flags,
type: detail.type,
});
const { info: storeInfo } = storeMap.get(props.postDir);
const fileDirPath = path__default.dirname(filePath);
if (!isSubDir(storeInfo.postDir, fileDirPath))
return;
if (eventName === 'modified' &&
detail.type === 'file' &&
extName(filePath) === '.md') {
await changeFileHandler(filePath);
return;
}
if (['error', 'ready'].includes(eventName))
return;
if (['created', 'moved'].includes(eventName) &&
detail.type === 'file' &&
!['.md', '.jpg', '.png', '.jpeg', '.gif', '.svg'].includes(extName(filePath)))
return;
await restEventHandler(filePath, detail.type);
};
const restEventHandler = async (filePath, type = 'file') => {
const { info: storeInfo } = storeMap.get(props.postDir);
updateStore(getStyledLogMsg(chalk `{bold [ ${path__default.basename(filePath)} ${type} ]} has changed. The Store is created again.`, `store : ${storeInfo.name}`));
};
if (!PLATFROM_DARWIN) {
watcher.on('change', changeFileHandler);
watcher.on('add', restEventHandler);
watcher.on('addDir', restEventHandler);
watcher.on('unlink', restEventHandler);
watcher.on('unlinkDir', restEventHandler);
watcher.on('all', async (eventName, path) => {
if (eventName === 'change')
return;
await restEventHandler(path);
});
return;
}
watcher.on('raw', rawHandlerForDarwin);
};
const extName = (filePath) => path__default.extname(filePath).normalize().toLowerCase();
const getParentWatcherKey = (postDir) => [...watcherMap.keys()].find((watcherKey) => isSubDir(watcherKey, postDir));
const updatePostData = (targetPostData, newPostData, keys) => keys.forEach((key) => {
targetPostData[key] = newPostData[key];
});
const getStore = async ({ postDir, storeName, perPage = DEFAULT_PERPAGE_VALUE, pageParam = DEFAULT_PARAM_VALUE, shouldUpdate = MODE_TEST, watchMode = MODE_DEV, incremental = true, imageMaxWidth, }) => {
const cachedStore = storeMap.get(postDir);
if (cachedStore && (!shouldUpdate || watchMode))
return cachedStore;
const paramToMakeStore = {
postDir,
storeName,
perPage,
pageParam,
incremental,
imageMaxWidth,
};
const store = await makeStore(paramToMakeStore);
if (watchMode && !MODE_PRODUCTION)
startWatchMode(paramToMakeStore);
if (MODE_PRODUCTION || MODE_TEST)
return store;
console.log(getStyledInfoMsg('New store has been created.', `name : ${store.info.name}`));
return store;
};
const loadConfig = async ({ storeName, configPath = DEFAULT_CONFIG_PATH, }) => {
const { storeOption } = await getConfig(path__default.resolve(configPath));
if (Array.isArray(storeOption)) {
if (!storeName) {
throw new Error(getStyledErrorMsg('If the storeOption property in the config object is an array, storeName is required.'));
}
const findedOption = storeOption.find((option) => option.storeName === storeName);
if (!findedOption) {
throw new Error(getStyledErrorMsg('There is no option corresponding to storeName.', `name: ${storeName}`));
}
return findedOption;
}
return storeOption;
};
const getConfig = async (configPath) => {
let config;
try {
fs.statSync(configPath);
}
catch {
throw new Error(getStyledErrorMsg('Please ensure that the config file exists.', `path: ${configPath}`));
}
try {
const importedModule = await Promise.resolve().then(function () { return _interopNamespace(require(configPath)); });
if (importedModule.default) {
config = importedModule.default;
}
else {
config = importedModule;
}
}
catch {
throw new Error(getStyledErrorMsg('This is not a module file.', `path: ${configPath}`));
}
if (!validateConfig(config)) {
throw new Error(getStyledErrorMsg('The configuration should look like below.') +
chalk `\n{yellow.bold ${CONFIG_EXAMPLE}}`);
}
return config;
};
const validateConfig = (config) => {
if (!config || !config.storeOption)
return false;
const storeOption = config.storeOption;
if (Array.isArray(storeOption)) {
return storeOption.every((sOption) => validateStoreOption(sOption));
}
return validateStoreOption(storeOption);
};
const validateStoreOption = ({ postDir, shouldUpdate, watchMode, storeName, }) => {
if (!postDir)
return false;
if (shouldUpdate && watchMode === true) {
throw new Error(getStyledErrorMsg('Both "shouldUpdate" and "watchMode" cannot be true. Please check the configuration.', storeName && `store: ${storeName}`));
}
return true;
};
const makePageHandler = (pageCategory) => (option) => {
const storeOption = normalizeOption$1(option);
async function getPathsBySlug() {
const { postDir, perPage, pageParam } = await storeOption;
const store = await makePathStore({
postDir,
perPage,
pageParam,
});
const pathList = store.pathList[pageCategory];
return pathList;
}
async function getPropsBySlug(param) {
const store = await getStore(await storeOption);
const propList = store.propList[pageCategory];
const key = Array.isArray(param) ? param.join('/') : param;
const mainProp = propList[key];
if (!mainProp) {
throw new Error(getStyledErrorMsg(`The data corresponding to the param does not exist.`, `input param : ${param}`));
}
if (isListPage(mainProp)) {
const refinedPostList = mainProp.postList.map(({ slug, title, date, thumbnail, excerpt, categories, tags }) => ({
slug,
title,
date,
thumbnail,
excerpt,
categories,
tags,
}));
const newMainProp = { ...mainProp };
newMainProp.postList = refinedPostList;
return {
param: key,
global: store.propList.global,
main: newMainProp,
};
}
return {
param: key,
global: store.propList.global,
main: mainProp,
};
}
return {
getPathsBySlug,
getPropsBySlug,
};
};
const getMainPageHandler = (option) => {
const storeOption = normalizeOption$1(option);
async function getMainProps() {
const store = await getStore(await storeOption);
const propList = store.propList.page;
const mainKey = 'page/1';
const mainProp = propList[mainKey];
const emptyProp = {
slug: mainKey,
pageCategory: 'page',
count: 0,
currentPage: 0,
postList: [],
totalPage: 0,
perPage: 0,
};
if (!mainProp) {
console.log(getStyledCautionMsg('Since There are no posts in the [postDir] path, the following default values is delivered.', `postDir: ${path__default.basename(store.info.postDir)}`));
console.log('\u001b[2m──────────────\u001b[22m');
console.log(chalk `{blue.bold Default Prop :}\n{yellow ${JSON.stringify({
global: store.propList.global,
main: emptyProp,
}, null, 2)}}`);
}
return {
param: mainKey,
global: store.propList.global,
main: mainProp == null ? emptyProp : mainProp,
};
}
return { getMainProps };
};
const getGlobalPageHandler = (option) => {
const storeOption = normalizeOption$1(option);
async function getMainProps() {
const store = await getStore(await storeOption);
return {
...store.propList.global,
};
}
return { getMainProps };
};
const isConfigOption = (option) => {
if (option.useConfig)
return true;
return false;
};
const normalizeOption$1 = async (option) => {
if (isConfigOption(option)) {
const storeOption = await loadConfig(option);
return storeOption;
}
if (!option.postDir) {
throw new Error(getStyledErrorMsg('The postDir parameter is required.'));
}
const storeOption = option;
return storeOption;
};
const isListPage = (a) => {
return a.pageCategory !== 'post';
};
const getCategoryPageHandler = makePageHandler('category');
const getPostPageHandler = makePageHandler('post');
const getTagPageHandler = makePageHandler('tag');
const getPageHandler = makePageHandler('page');
exports.getCategoryPageHandler = getCategoryPageHandler;
exports.getGlobalPageHandler = getGlobalPageHandler;
exports.getMainPageHandler = getMainPageHandler;
exports.getPageHandler = getPageHandler;
exports.getPostPageHandler = getPostPageHandler;
exports.getTagPageHandler = getTagPageHandler;