c4dslbuilder
Version:
A CLI tool designed to compile a folder structure of markdowns and mermaid files into a site, pdf, single file markdown or a collection of markdowns with links - inspired by c4builder
180 lines (179 loc) • 5.94 kB
JavaScript
import path from 'path';
import { promises as fs } from 'fs';
import fsExtra from 'fs-extra';
import { CliLogger } from './cli-logger.js';
export class SafeFiles {
logger;
constructor(logger = new CliLogger(SafeFiles.name)) {
this.logger = logger;
}
getFolderName(dir, root, homepage) {
return path.resolve(dir) === path.resolve(root) ? homepage : path.parse(dir).base;
}
async pathExists(filePath) {
try {
return await fsExtra.pathExists(filePath);
}
catch (error) {
this.logger.error(`Error checking file existence: ${filePath}`, error);
return false;
}
}
async createDir(dir) {
try {
await fsExtra.mkdirp(dir);
}
catch (error) {
this.logger.error(`Error creating directory ${dir}`, error);
}
}
async emptySubFolder(dir) {
const resolvedDir = path.resolve(dir);
const cwd = process.cwd();
if (!resolvedDir.startsWith(cwd + path.sep)) {
this.logger.warn(`Refusing to empty non-subdirectory path: ${resolvedDir}`);
return false;
}
try {
await fsExtra.emptyDir(dir);
return true;
}
catch (error) {
this.logger.error(`Error emptying directory: ${dir}`, error);
return false;
}
}
async copyFile(src, dest) {
try {
await fsExtra.copy(src, dest);
}
catch (error) {
this.logger.error(`Error copying file from ${src} to ${dest}`, error);
}
}
async removeFile(path) {
try {
await fs.unlink(path);
}
catch (error) {
this.logger.error(`Error removing file ${path}`, error);
}
}
async accessFile(path) {
try {
await fs.access(path);
}
catch (error) {
this.logger.error(`Error accessing file ${path}`, error);
}
}
async readDir(dir) {
try {
const files = await fs.readdir(dir);
return files.filter((x) => !x.startsWith('_'));
}
catch (error) {
this.logger.error(`Error reading directory ${dir}`, error);
return [];
}
}
async readFileAsString(filePath) {
try {
const content = await fs.readFile(filePath, 'utf-8');
return content.toString();
}
catch (error) {
this.logger.error(`Error reading file: ${filePath}`, error);
return null;
}
}
async writeFile(filePath, data) {
try {
await fs.writeFile(filePath, data);
}
catch (error) {
this.logger.error(`Error writing file: ${filePath}`, error);
}
}
async stat(filePath) {
try {
return await fs.stat(filePath);
}
catch (error) {
this.logger.error(`Error getting stats for file: ${filePath}`, error);
return null;
}
}
async ensureDir(dir) {
try {
await fsExtra.ensureDir(dir);
}
catch (error) {
this.logger.error(`Error ensuring directory: ${dir}`, error);
}
}
async ingestMarkdownFiles(dir, files, treeItem) {
const mdFiles = files.filter((x) => path.extname(x).toLowerCase() === '.md');
for (const mdFile of mdFiles) {
const fileContents = await this.readFileAsString(path.join(dir, mdFile));
if (fileContents) {
treeItem.mdFiles.push({ name: mdFile, content: fileContents });
}
}
}
async ingestMermaidFiles(dir, files, treeItem) {
const mmdFiles = files.filter((x) => path.extname(x).toLowerCase() === '.mmd');
for (const mmdFile of mmdFiles) {
const fileContents = await this.readFileAsString(path.join(dir, mmdFile));
if (fileContents) {
treeItem.mmdFiles.push({
name: mmdFile,
content: fileContents.toString(),
});
}
}
}
async itemiseTreeFolder(dir, baseFolder, rootFolder, parent, homepageName, tree) {
const rootFolderName = this.getFolderName(dir, baseFolder, homepageName);
const treeItem = {
dir: dir,
name: rootFolderName,
level: dir.split(path.sep).length - baseFolder.split(path.sep).length,
parent: parent,
mdFiles: [],
mmdFiles: [],
descendants: [],
};
const files = await this.readDir(dir);
for (const file of files) {
const filePath = path.join(dir, file);
const stats = await this.stat(filePath);
if (!stats)
continue;
if (stats.isDirectory()) {
treeItem.descendants.push(file);
await this.ensureDir(path.join(baseFolder, dir.replace(rootFolder, ''), file));
const childTreeItem = await this.itemiseTreeFolder(filePath, baseFolder, rootFolder, dir, homepageName, tree);
if (childTreeItem) {
tree.unshift(childTreeItem);
}
}
}
await this.ingestMarkdownFiles(dir, files, treeItem);
await this.ingestMermaidFiles(dir, files, treeItem);
if (treeItem.mdFiles.length > 0 ||
treeItem.mmdFiles.length > 0 ||
treeItem.descendants.length > 0) {
return treeItem;
}
return null;
}
async generateTree(baseFolder, rootFolder, homepageName) {
const tree = [];
const rootBranch = await this.itemiseTreeFolder(baseFolder, baseFolder, rootFolder, null, homepageName, tree);
if (rootBranch) {
tree.unshift(rootBranch);
}
return tree;
}
}