From 6b5a1b1907d07708abec7ada44259a7c4933e306 Mon Sep 17 00:00:00 2001 From: rclee91 <32626950+rclee91@users.noreply.github.com> Date: Thu, 14 Oct 2021 15:17:37 -0400 Subject: [PATCH] Refactoring index.js to improve code maintainability: *Renamed readStream and writeStream variables *Extracted File class *Extracted InputData class, extracted processOptions function to get rid of some global variables *Extracted getHtmlContent function *Split index.js and data.js --- bin/data.js | 268 ++++++++++++++++++++++++++++++++++++++++ bin/index.js | 338 ++++++++------------------------------------------- 2 files changed, 317 insertions(+), 289 deletions(-) create mode 100644 bin/data.js diff --git a/bin/data.js b/bin/data.js new file mode 100644 index 0000000..3dd7ca0 --- /dev/null +++ b/bin/data.js @@ -0,0 +1,268 @@ +const { Transform } = require('stream'); +const fs = require('fs'); +const path = require('path'); + +class Data{ + inputPath_; + outputPath_; + stylesheetUrl_; + extensions = [".txt", ".md"]; + + constructor(inputPath, outputPath, stylesheetUrl){ + if(!fs.existsSync(inputPath)){ + console.error(`Input file or directory "${inputPath}" doesn't exist.`); + process.exit(-1); + } + this.inputPath_ = inputPath; + this.outputPath_ = outputPath; + this.stylesheetUrl_ = stylesheetUrl; + } + + /* + Method processes the input data + */ + processInput(){ + fs.access(this.inputPath_, fs.constants.R_OK, (err)=>{ + if(err){ + console.error(`Can't access file or directory ${this.inputPath_}`); + process.exit(-1); + } + else{ + //Remove old output directory + fs.rmdir(this.outputPath_, {recursive: true, force: true}, (err)=>{ + if(err){ + console.error(`Error removing directory at "${this.outputPath_}`); + process.exit(-1); + } + + //Create new output directory + fs.mkdirSync(this.outputPath_, {recursive: true}); + + //Check if the input is a file or a directory + let fStats = fs.statSync(this.inputPath_); + //if the input is a file + if(fStats.isFile()){ + this.extensions.forEach(extension =>{ + if(this.inputPath_.endsWith(extension)){ + let tempFile = new File(this.inputPath_, extension); + this.createFile(tempFile); + } + }); + //if the file is a directory + } else if (fStats.isDirectory()){ + let fileNames = []; + this.extensions.forEach(extension => { + fileNames = fileNames.concat(this.processFiles(extension)); + }); + + // Creating index.js for files + let indexContent = this.CreateIndexHtml(fileNames); + fs.writeFileSync(path.join(this.outputPath_,"index.html"), indexContent, (err)=>{ + if(err){ + console.error(`Error creating index.html file`); + process.exit(-1); + } + }); + } + });//end of fs.rmdir + } + });//end of fs.access + } + + /* + Method Creates html files from all files within the tree of directories ending with extension. + + Parameters: + extension - string: extension of files to parse to html + + Return: + fileNames - array: array of filenames parsed to html. + */ + processFiles(extension){ + let files = this.findInDir(this.inputPath_, extension); + let fileNames = []; + if(Array.isArray(files)){ + files.forEach(file => { + fileNames.push(path.basename(file, extension)); + let tempFile = new File(file, extension); + this.createFile(tempFile); + }); + } + + return fileNames; + } + + /* + Function finds all files ending with extension in a tree of directories + + Parameters: + filepath - string: filepath of file or directory + extension - string: extension of files to parse to html + + Return: + results - array: array consisting all filepaths of files ending with extension in a tree of directories + */ + findInDir(filepath, extension){ + let results =[]; + let files=fs.readdirSync(filepath); + for(let i = 0; i < files.length; ++i){ + let filename = path.join(filepath,files[i]); + let stat = fs.lstatSync(filename); + + //recursively find all files ending with extension + if(stat.isDirectory()){ + results = results.concat(findInDir(filename, extension)); + } + else{ + if(filename.endsWith(extension)) + results.push(filename); + } + } + return results; + } + + createFile(file){ + let stylesheetUrl = this.stylesheetUrl_; + let readStream = fs.createReadStream(file.getFilePath()); + readStream.setEncoding("utf8"); + readStream.on("data", () =>{}); + readStream.on("end", ()=>{}); + readStream.on("error", (err) =>{ + console.error(`ReadStream encountered an error: ${err}`); + process.exit(-1); + }); + + let writeStream = fs.createWriteStream(path.join(this.outputPath_, file.fileName_.replace(/\s+/g, '_')+".html")); + writeStream.on("finish", ()=>{}); + writeStream.on("error", (err) =>{ + console.error(`WriteStream encountered an error: ${err}`); + writeStream.end(); + process.exit(-1); + }); + + let toHtmlStream = new Transform({ + objectMode: true, + transform(chunk, encoding, callback){ + this.push(file.toHtml(chunk.toString()), stylesheetUrl); + return callback(); + }, + }); + toHtmlStream.on('error', (err) =>{ + console.error(`toHtmlStream encountered an error: ${err}`); + process.exit(-1); + }); + + //Piping data + readStream.pipe(toHtmlStream).pipe(writeStream); + } + + CreateIndexHtml(filenames){ + let title = "Generated Pages" + let bodyContent = ""; + + filenames.forEach(filename =>{ + if(typeof filename === 'string') + bodyContent += `\r\n\t\t

\r\n\t\t\t${filename}\r\n\t\t

`; + }); + + return getHtmlContent(title, this.stylesheetUrl_, bodyContent); + } +} + +class File{ + filePath_ = ""; + extension_ = ""; + fileName_ = ""; + + constructor(filePath, extension){ + if(filePath.endsWith(extension)) + this.fileName_ = path.basename(filePath, extension); + else{ + console.error(`error getting filename from file ${path}`); + process.exit(-1); + } + + this.filePath_ = filePath; + this.extension_ = extension; + } + + getFilePath(){ + return this.filePath_; + } + + /* + Method gets the htmlContent to be written into html files + + Parameters: + data - string/array: Data to be processed for html files + stylesheetUrl - stylesheet Url + + Return: + htmlContent - string: content string to be written into files + */ + toHtml(data, stylesheetUrl = ""){ + let title = ""; + let bodyContent = ""; + //Processing files ending with extensions in the extensions array + if (typeof data === "string" && this.extension_ == ".txt") { + let lines = data.split(/\r?\n\r?\n\r?\n/); + if(lines.length > 1){ + title = lines[0]; + lines.shift(); + } + bodyContent += lines[0].split(/\r?\n\r?\n/g).map(line => `\r\n\t\t

${line}

`).join("\n"); + } else if(typeof data == "string" && this.extension_ == ".md") { + bodyContent = this.markdownContent(data); + } + + return getHtmlContent(title, stylesheetUrl, bodyContent); + } + + markdownContent(data) { + // Using replace method on string & regular expression + let convertedText = data + .replace(/^# (.*$)/gim, '

$1

') // Heading 1 + .replace(/^### (.*$)/gim, '

$1

') // Heading 3 + .replace(/^## (.*$)/gim, '

$1

') // Heading 2 + .replace(/\*\*(.*)\*\*/gim, '$1') // Bold + .replace(/\*(.*)\*/gim, '$1') // Italic + .replace(/\*\*(.*)\*\*/gim, '$1') // Bold nested in Italic + .replace(/\*(.*)\*/gim, '$1') // Italic nested in Bold + .replace(/\[(.*?)\]\((.*?)\)/gim, "$1") // Link + .replace(/`(.*?)`/gim, '$1') //inline code + .replace(/-{3,}$/gim, '
') // Horizontal Rule + .replace(/\n$/gim, '
') // Break line + + return convertedText + } +} + +/* + Functions generates html block + + Parameters: + title - : html title + stylesheetUrl - stylesheet Url + bodyContent - html body + + Return: + htmlContent - string: content string to be written into files + */ +function getHtmlContent(title, stylesheetUrl, bodyContent){ + let htmlContent = ''+ + '\r\n\t'+ + '\r\n\t' + + '\r\n\t\t' + + `\r\n\t\t${title != "" ? title : ""}` + + `\r\n\t\t`+ + `${stylesheetUrl != "" ? `\r\n\t\t` : ""}` + + '\r\n\t'+ + '\r\n\t' + + `${title != "" ? `\r\n\t\t

${title}

`: ""}`+ + `${bodyContent}`+ + '\r\n\t\r\n'; + + return htmlContent; +} + +module.exports.Data = Data; \ No newline at end of file diff --git a/bin/index.js b/bin/index.js index b64b083..f924e58 100755 --- a/bin/index.js +++ b/bin/index.js @@ -3,303 +3,63 @@ const pkjson = require('../package.json'); const fs = require('fs'); const path = require('path'); -const { Transform } = require('stream'); const { Command } = require('commander'); - -const extensions = [".txt", ".md"]; - -//path to current directory for dist folder -const dist = path.join(process.cwd(), "dist"); -let outputPath = dist; - -//stylesheet url -let stylesheetUrl = undefined; - -const program = new Command(); -program - .version(`Name: ${pkjson.name} \nVersion: ${pkjson.version}`, '-v, --version', 'Output the current version') - .option('-i, --input ', 'Designate an input file or directory') - .option('-o, --output ', 'Designate an ouput directory', dist) - .option('-s, --stylesheet ', 'Link to a stylesheet url', '') - .option('-c, --config ', 'Link to file that specifies all SSG options') - .showHelpAfterError(); -program.parse(process.argv); - -const options = program.opts(); -//Config file option -if(options.config !== undefined) { - try { - let configData = fs.readFileSync(options.config); - let configOptions = JSON.parse(configData); - for(const [key, value] of Object.entries(configOptions)) { - value || value.length > 0 ? options[`${key}`] = `${value}` : options[`${key}`] = undefined; - } - if(!options.input) { - console.error(`error: input is not specified in config file ${options.config}`); - process.exit(-1); - } - } catch(error) { - console.error(`Can't read or parse config file ${options.config}\n ${error}`); - process.exit(-1); - } -} -//Output option -if(options.output !== dist){ - if(fs.existsSync(options.output)){ - outputPath = options.output; - } - else{ - console.log(`Output directory ${options.output} doesn't exist, outputting all files to ./dist`); - } -} -//Stylesheet option -if(options.stylesheet !== undefined) - stylesheetUrl = options.stylesheet; -//Input option -if(options.input !== undefined) - processInput(options.input); -else { - console.error("error: required option input is not specified"); - process.exit(-1); -} -/* - Function processes the input option - - Parameters: - filepath - string: filepath of input file or directory -*/ -function processInput(filepath){ - //Check to see if the file or directory exists - if(!fs.existsSync(filepath)){ - console.error(`Input file or directory "${filepath}" doesn't exist.`); - process.exit(-1); - } - else - { - //Check for read access to file or directory - fs.access(filepath, fs.constants.R_OK, (err)=>{ - if(err){ - console.error(`Can't access file or directory ${filepath}`); - process.exit(-1); +const { Data } = require('./data') + +function processOptions(){ + const dist = path.join(process.cwd(), "dist"); + const program = new Command(); + program + .version(`Name: ${pkjson.name} \nVersion: ${pkjson.version}`, '-v, --version', 'Output the current version') + .option('-i, --input ', 'Designate an input file or directory') + .option('-o, --output ', 'Designate an ouput directory', dist) + .option('-s, --stylesheet ', 'Link to a stylesheet url', '') + .option('-c, --config ', 'Link to file that specifies all SSG options') + .showHelpAfterError(); + program.parse(process.argv); + const options = program.opts(); + + let outputPath = dist; + let stylesheetUrl; + //Config file option + if(options.config !== undefined) { + try { + let configData = fs.readFileSync(options.config); + let configOptions = JSON.parse(configData); + for(const [key, value] of Object.entries(configOptions)) { + value || value.length > 0 ? options[`${key}`] = `${value}` : options[`${key}`] = undefined; } - else{ - //Remove old output directory - fs.rmdir(outputPath, {recursive: true, force: true}, (err)=>{ - if(err){ - console.error(`Error removing directory at "${outputPath}`); - process.exit(-1); - } - - //Create new output directory - fs.mkdirSync(outputPath, {recursive: true}); - - //Check if the input is a file or a directory - let fStats = fs.statSync(filepath); - //if the input is a file - if(fStats.isFile()){ - extensions.forEach(extension =>{ - if(filepath.endsWith(extension)) - createFile(filepath, extension); - }); - //if the file is a directory - } else if (fStats.isDirectory()){ - let fileNames = []; - extensions.forEach(extension => { - fileNames = fileNames.concat(processFiles(filepath, extension)); - }); - - // Creating index.js for files - let indexContent = htmlContent(fileNames, "index", ".html"); - fs.writeFileSync(path.join(outputPath,"index.html"), indexContent, (err)=>{ - if(err){ - console.error(`Error creating index.html file`); - process.exit(-1); - } - }); - } - });//end of fs.rmdir + if(!options.input) { + console.error(`error: input is not specified in config file ${options.config}`); + process.exit(-1); } - });//end of fs.access - } -} - -/* - Function Creates html files from all files within the tree of directories ending with extension. - - Parameters: - filepath - string: filepath of file or directory - extension - string: extension of files to parse to html - - Return: - fileNames - array: array of filenames parsed to html. -*/ -function processFiles(filepath, extension){ - let files = findInDir(filepath, extension); - let fileNames = []; - if(Array.isArray(files)){ - files.forEach(file => { - fileNames.push(path.basename(file, extension)); - createFile(file, extension); - }); - } - - return fileNames; -} - -/* - Function creates a single html file - - Parameters: - filepath - string: filepath of file or directory - extension - string: extension of files to parse to html -*/ -function createFile(filepath, extension){ - if(filepath.endsWith(extension)){ - let filename = path.basename(filepath, extension); - - const rs = fs.createReadStream(filepath); - rs.setEncoding("utf8"); - rs.on("data", () =>{}); - rs.on("end", ()=>{}); - rs.on("error", (err) =>{ - console.error(`ReadStream encountered an error: ${err}`); - process.exit(-1); - }); - - let ws = fs.createWriteStream(path.join(outputPath, filename.replace(/\s+/g, '_')+".html")); - ws.on("finish", ()=>{}); - ws.on("error", (err) =>{ - console.error(`WriteStream encountered an error: ${err}`); - ws.end(); + } catch(error) { + onsole.error(`Can't read or parse config file ${options.config}\n ${error}`); process.exit(-1); - }); - - const toHtmlStream = new Transform({ - objectMode: true, - transform(chunk, encoding, callback){ - this.push(htmlContent(chunk.toString(), filename, extension)); - return callback(); - }, - }); - - toHtmlStream.on('error', (err) =>{ - console.error(`toHtmlStream encountered an error: ${err}`); - process.exit(-1); - }); - - //Piping data - rs.pipe(toHtmlStream).pipe(ws); + } } -} - - -/* - Function gets the htmlContent to be written into html files - Parameters: - data - string/array: Data to be processed for html files - filename - string: name of file - extension - string: extension of files to parse to html - - Return: - htmlContent - string: content string to be written into files -*/ -function htmlContent(data, filename, extension){ - let title = ""; - let bodyContent = ""; - - //Processing files ending with extensions in the extensions array - if (typeof data === "string" && extension == ".txt") { - let lines = data.split(/\r?\n\r?\n\r?\n/); - if(lines.length > 1){ - title = lines[0]; - lines.shift(); + //Output option + if(options.output !== dist){ + if(fs.existsSync(options.output)){ + outputPath = options.output; } - bodyContent += lines[0].split(/\r?\n\r?\n/g).map(line => `\r\n\t\t

${line}

`).join("\n"); - } else if(typeof data == "string" && extension == ".md") { - bodyContent = markdownContent(data, filename) - } - else{ //Creating index.html - title = "Generated Pages"; - if(Array.isArray(data)){ - data.forEach(filename =>{ - if(typeof filename === 'string') - bodyContent += `\r\n\t\t

\r\n\t\t\t${filename}\r\n\t\t

`; - }); + else{ + console.log(`Output directory ${options.output} doesn't exist, outputting all files to ./dist`); } } - - //Forming html with indents - let htmlContent = ''+ - '\r\n\t'+ - '\r\n\t' + - '\r\n\t\t' + - `\r\n\t\t${title != "" ? title : filename}` + - `\r\n\t\t`+ - `${stylesheetUrl != "" ? `\r\n\t\t` : ""}` + - '\r\n\t'+ - '\r\n\t' + - `${title != "" ? `\r\n\t\t

${title}

`: ""}`+ - `${bodyContent}`+ - '\r\n\t\r\n'; - - return htmlContent; -} - -// Creates Markdown content -/* - Function creates Markdown content for the html file. - Added support for the following features. - - Parameters - data - string: text data from .md file - - Returns - html - string: content of the html file - */ -function markdownContent(data) { - // Using replace method on string & regular expression - const convertedText = data - .replace(/^# (.*$)/gim, '

$1

') // Heading 1 - .replace(/^### (.*$)/gim, '

$1

') // Heading 3 - .replace(/^## (.*$)/gim, '

$1

') // Heading 2 - .replace(/\*\*(.*)\*\*/gim, '$1') // Bold - .replace(/\*(.*)\*/gim, '$1') // Italic - .replace(/\*\*(.*)\*\*/gim, '$1') // Bold nested in Italic - .replace(/\*(.*)\*/gim, '$1') // Italic nested in Bold - .replace(/\[(.*?)\]\((.*?)\)/gim, "$1") // Link - .replace(/`(.*?)`/gim, '$1') //inline code - .replace(/-{3,}$/gim, '
') // Horizontal Rule - .replace(/\n$/gim, '
') // Break line - - return convertedText + //Stylesheet option + if(options.stylesheet !== undefined) + stylesheetUrl = options.stylesheet; + //Input option + if(options.input !== undefined){ + let inputData = new Data(options.input, outputPath, stylesheetUrl); + inputData.processInput(); + } + else { + console.error("error: required option input is not specified"); + process.exit(-1); + } } -/* - Function finds all files ending with extension in a tree of directories - - Parameters: - filepath - string: filepath of file or directory - extension - string: extension of files to parse to html - - Return: - results - array: array consisting all filepaths of files ending with extension in a tree of directories -*/ -function findInDir(filepath, extension){ - let results =[]; - let files=fs.readdirSync(filepath); - for(let i = 0; i < files.length; ++i){ - let filename = path.join(filepath,files[i]); - let stat = fs.lstatSync(filename); - - //recursively find all files ending with extension - if(stat.isDirectory()){ - results = results.concat(findInDir(filename, extension)); - } - else{ - if(filename.endsWith(extension)) - results.push(filename); - } - } - return results; -} \ No newline at end of file +processOptions(); \ No newline at end of file