Skip to content

Instantly share code, notes, and snippets.

@AnonymerNiklasistanonym
Created February 28, 2019 04:54
Show Gist options
  • Save AnonymerNiklasistanonym/f62856485623b791a6301b3d3daab1ce to your computer and use it in GitHub Desktop.
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])
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)
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