Created
February 28, 2019 04:54
-
-
Save AnonymerNiklasistanonym/f62856485623b791a6301b3d3daab1ce to your computer and use it in GitHub Desktop.
Merges Markdown files into one file. (With node and file streams [very fast])
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const fs = require('fs') | |
const colors = require('colors') | |
/** | |
* Info option, when true enable performance tracking and other infos | |
*/ | |
let enableInfo = false | |
/** | |
* The new file path | |
*/ | |
let newFilePath = null | |
/** | |
* List for all the files that should be merged | |
*/ | |
let files = [] | |
/** | |
* CLI Help output | |
*/ | |
const helpOutput = () => { | |
console.log('Description:\n' + | |
'\tMerges Markdown files into one file.') | |
console.log('Example:\n\t$ node ' + | |
colors.yellow('.\\notableNoteToTypora.js ') + | |
colors.green('"newFilePath" ') + | |
colors.cyan('filePath1.md\n\t filePath2.md filePathN.md ') + | |
colors.red('-enableOptionalOption') + | |
'\n\tFile was written: "newFilePath.md"') | |
console.log('Options:\n' + '\t-info\t\t\t\t' + | |
colors.grey('Get information about performance\n\t\t\t\t\tand other things')) | |
} | |
/** | |
* CLI Version output | |
*/ | |
const versionOutput = () => { | |
console.log('0.0.1') | |
} | |
// Check if instead of a file help/version is wanted | |
if (process.argv[2] === '--help' || | |
process.argv[2] === '-help' || | |
process.argv[2] === 'help') { | |
helpOutput() | |
process.exit(0) | |
} else if (process.argv[2] === '--version' || | |
process.argv[2] === '-version' || | |
process.argv[2] === 'version') { | |
versionOutput() | |
process.exit(0) | |
} | |
// Check if a file was given | |
if (process.argv.length < 4) { | |
console.error('At least one file path needs to be specified!\n') | |
helpOutput() | |
process.exit(1) | |
} | |
// Check if options are correct | |
if (process.argv.length >= 4) { | |
let infoAlreadyEnabled = false | |
let newFilePathSelected = false | |
for (let i = 2; i < process.argv.length; i++) { | |
if (process.argv[i] === '-info') { | |
if (infoAlreadyEnabled) { | |
console.error(`'-info' option cannot bet enabled twice!\n`) | |
helpOutput() | |
process.exit(1) | |
} else { | |
infoAlreadyEnabled = true | |
enableInfo = true | |
} | |
} else { | |
if (newFilePathSelected) { | |
// Add file to the list | |
files.push(process.argv[i]) | |
} else { | |
newFilePath = process.argv[i] + '.md' | |
newFilePathSelected = true | |
} | |
} | |
} | |
if (!newFilePathSelected) { | |
console.error('No new file path was specified!\n') | |
helpOutput() | |
process.exit(1) | |
} | |
if (files.length <= 0) { | |
console.error('At least one file path needs to be specified!\n') | |
helpOutput() | |
process.exit(1) | |
} | |
} | |
// Check if all files exists | |
files.forEach(filePath => { | |
if (!fs.existsSync(filePath)) { | |
console.error(`File was not found: "${colors.red(filePath)}"\n`) | |
helpOutput() | |
process.exit(1) | |
} | |
}) | |
/** | |
* A function that can execute functions that return when they are executed | |
* promises sequentially one after the other | |
* @param {[function():Promise<*>]} promiseFunctionList Promise | |
* method list | |
* @returns {Promise<[*]>} Promise that resolves after all the others and with | |
* a list of all results | |
* @example | |
* ```js | |
* const timeoutTest = aNumber => new Promise(resolve => | |
* setTimeout(() => resolve(aNumber), Math.floor((Math.random() * 100) + 1))) | |
* const test = [] | |
* for (let index = 0; index < 99; index++) { | |
* test.push(() => timeoutTest(index)) | |
* } | |
* executePromisesSequentially(test) | |
* .then(console.log).catch(console.error) | |
* // [ 0, 1, ..., 97, 98 ] | |
* ``` | |
*/ | |
const executePromisesSequentially = promiseFunctionList => | |
// Reduce with the start value of a promise that returns an empty list | |
// This means the function will in it's first run execute that promise and | |
// return an empty array. Then the first function in the list of functions | |
// that return a promise when they are executed will be executed to get the | |
// promise. The logic is made that when the promise resolves the return value | |
// will be concatenated with the empty array so that it is contained. | |
// The thing that keeps the promises from executing together is that the | |
// function that returns the next promise is only then called when the last | |
// promise has resolved. | |
promiseFunctionList.reduce((preValuePromise, curValuePromiseFunc) => | |
preValuePromise.then(result => curValuePromiseFunc() | |
.then(newResult => result.concat(newResult))), Promise.resolve([])) | |
/** | |
* Extract all matches from the file via a fs.stream (much faster | |
* when executing on large files) | |
* @param {string} filePath File that should be read | |
* @returns {Promise<Buffer>} Array with buffer for file to edit | |
*/ | |
async function createdBufferFromFileViaStream (filePath) { | |
return new Promise((resolve, reject) => { | |
const label = `createdBufferFromFileViaStream (${filePath})` | |
if (enableInfo) { console.time(label) } | |
let buffer = null | |
const stream = fs.createReadStream(filePath, { encoding: 'utf8' }) | |
stream.on('data', data => { | |
buffer = Buffer.from(data) | |
stream.destroy() | |
}).on('error', err => { | |
if (enableInfo) { console.timeEnd(label) } | |
reject(err) | |
}).on('close', () => { | |
if (enableInfo) { console.timeEnd(label) } | |
resolve(buffer) | |
}) | |
}) | |
} | |
/** | |
* Write file | |
* @param {string} filePath | |
* @param {Buffer} buffer | |
*/ | |
async function writeToFile (filePath, buffer) { | |
return new Promise((resolve, reject) => { | |
const label = `writeBufferToNewFile (${filePath})` | |
if (enableInfo) { console.time(label) } | |
fs.open(filePath, 'w', function (err, fd) { | |
if (err) { | |
if (enableInfo) { console.timeEnd(label) } | |
return reject(err) | |
} | |
fs.write(fd, buffer, 0, buffer.length, () => { }, (err) => { | |
if (err) { | |
if (enableInfo) { console.timeEnd(label) } | |
return reject(err) | |
} | |
fs.close(fd, () => { | |
if (enableInfo) { console.timeEnd(label) } | |
resolve() | |
}) | |
}) | |
}) | |
}) | |
} | |
// MAIN | |
// Create new file that contains all input files in the input order | |
executePromisesSequentially(files.map(filePath => | |
() => createdBufferFromFileViaStream(filePath))) | |
.then(buffers => { | |
writeToFile(newFilePath, Buffer.concat(buffers)).then(() => { | |
console.log(`File was written: "${newFilePath}"`) | |
}).catch(console.error) | |
}) | |
.catch(console.error) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Description: | |
Merges Markdown files into one file. | |
Example: | |
$ node .\notableNoteToTypora.js "newFilePath" filePath1.md | |
filePath2.md filePathN.md -enableOptionalOption | |
File was written: "newFilePath.md" | |
Options: | |
-info Get information about performance | |
and other things |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment