#! /usr/bin/env node const collectImages = require('./funcs/collectImages'); const toWebTitle = require('./funcs/toWebTitle'); const fs = require('fs-extra'); // get fileSystem const concatStream = require('concat-stream'); //put a streaming chunked file into one glob const q = require('q'); //so we can defer const inquirer = require('inquirer'); //so we can ask questions const openFile = require('open'); //so we can open files const vinylFs = require('vinyl-fs'); const stripTags = require('striptags'); const vinylFtp = require('vinyl-ftp'); const removeMD = require('remove-markdown'); const entities = require('entities'); const ssmlVal = require('ssml-validator'); const typeset = require('typeset'); const { replaceQuotes } = require('curly-q'); const shell = require('shelljs'); const bloomfile = './bloom.json'; declare namespace NodeJS { interface Global extends Bloom {} } interface Bloomfile { title: string; author: string; subtitle: string; words: boolean; alphabetical: boolean; hideIncomplete: boolean; sequential: boolean; ssml: boolean; mp3: boolean; ftp: boolean; googleAnalyticsID: string; } interface UploadOptions { hostname: string; username: string; password: string; remotePath: string; } interface Bloom extends Bloomfile, UploadOptions { headerString: string; headerMarkup: string; projectTitle: string; projectSubtitle: string; projectAuthor: string; sequentialLinks: boolean; showWords: boolean; googleAnalyticsScript: string; bloomFileSettings: Bloomfile; useBloomFile: boolean; makeBloomFile: boolean; } global.headerString = ''; global.headerMarkup = ''; global.projectTitle = ''; global.projectSubtitle = ''; global.projectAuthor = ''; global.sequentialLinks = false; global.showWords = false; global.hideIncomplete = true; global.alphabetical = false; global.ftp = false; global.hostname = ''; global.username = ''; global.password = ''; global.remotePath = ''; global.ssml = false; global.mp3 = false; global.googleAnalyticsID = ''; global.googleAnalyticsScript = ``; global.bloomFileSettings = {} as Bloomfile; global.useBloomFile = false; global.makeBloomFile = false; const userArgs = process.argv.slice(2); const coverFileLocation = 'images/cover.jpg'; let fileToBloomFrom = userArgs[0]; if (!fileToBloomFrom) { fileToBloomFrom = 'index.html'; //assume it's index.html if they didn't provide a file } const isThereABloomFile = fs.existsSync(bloomfile); if (isThereABloomFile) { fs.readJson(bloomfile, (error, data) => { if (error) { console.log(error); } Object.assign(global.bloomFileSettings, data); }); } const inputFile = fs.createReadStream(fileToBloomFrom); const headerFileExists = fs.existsSync(__dirname + '/header.html'); let headerFile = ''; if (headerFileExists) { headerFile = fs.createReadStream(__dirname + '/header.html'); } const cssFileExists = fs.existsSync('css/style.css'); let cssFile; if (cssFileExists) { cssFile = fs.createReadStream('css/style.css'); } const indexStyles = ``; const coverImageExists = fs.existsSync(coverFileLocation); const outDirName = fileToBloomFrom.replace('.html', '') + '-bloomed'; const imageFolderExists = fs.existsSync(`./${outDirName}/images`); let coverImage; function makeFileAString(file) { // make a new deferred object so we can chain const deferred = q.defer(); file.pipe( concatStream((data) => { deferred.resolve(data.toString()); }) ); return deferred.promise; } function answersCallback(answers: Bloomfile & { makeBloomFile?: true }) { global.alphabetical = answers.alphabetical; global.projectTitle = answers.title; global.projectSubtitle = answers.subtitle; global.projectAuthor = answers.author; global.mp3 = answers.mp3; global.googleAnalyticsID = answers.googleAnalyticsID; global.showWords = answers.words; global.ssml = answers.ssml; global.sequentialLinks = answers.sequential; global.makeBloomFile = answers.makeBloomFile; global.ftp = answers.ftp; global.hideIncomplete = answers.hideIncomplete; global.headerMarkup = global.headerString.replace( '', `${answers.title}` ); if (answers.ftp) { inquirer .prompt([ { name: 'hostname', message: 'Enter your hostname (exclude ftp:// or www prefixes)' }, { name: 'username', message: 'Enter your username for that host' }, { name: 'password', type: 'password', message: 'Enter your password for that host' }, { name: 'remotePath', message: "Type a remote directory you'd like to bloom into (ex: html/project)" } ]) .then((moreAnswers: UploadOptions) => { global.hostname = moreAnswers.hostname; global.username = moreAnswers.username; global.password = moreAnswers.password; global.remotePath = moreAnswers.remotePath; runProgram(); }); } else { runProgram(); } } function askTheQuestions() { inquirer .prompt([ { name: 'title', message: 'What title should appear on the index page?' }, { name: 'subtitle', message: 'What subtitle should appear below the title?' }, { name: 'author', message: 'Author name, if any?' }, { type: 'confirm', name: 'words', default: false, message: 'Show word counts next to index links?' }, { type: 'confirm', name: 'alphabetical', default: false, message: 'Should the links on the index page be alphabetized?' }, { type: 'confirm', name: 'sequential', default: false, message: 'Should each page link to the next page instead of back to index page?' }, { type: 'confirm', name: 'hideIncomplete', default: true, message: 'Hide incomplete stories with % in the title?' }, { type: 'confirm', name: 'ssml', default: false, message: 'Generate a folder of SSML for using with Amazon Polly?' }, { type: 'confirm', name: 'mp3', default: false, message: 'Use SSML to make MP3s and add an audio player to each page?' }, { name: 'googleAnalyticsID', type: 'input', default: '', message: 'Google Analytics ID (if Bloom should add a script on every page).' }, { type: 'confirm', name: 'makeBloomFile', default: false, message: 'Create/overwrite bloom.json file in this folder, with these answers?' }, { type: 'confirm', name: 'ftp', default: false, message: 'Bloom can upload your files for you, if you provide FTP details. Yeah?' } ]) .then((answers) => { answersCallback(answers); }); } if (isThereABloomFile) { inquirer .prompt([ { name: 'useBloomFile', message: "Use the settings in this folder's bloomfile?", type: 'confirm', default: true } ]) .then((answer: { useBloomFile: boolean }) => { global.useBloomFile = answer.useBloomFile; if (answer.useBloomFile) { answersCallback(global.bloomFileSettings); } else { askTheQuestions(); } }); } else { askTheQuestions(); } function generateBloomFile() { const file = bloomfile; const obj: Bloomfile = { title: global.projectTitle, author: global.projectAuthor, subtitle: global.projectSubtitle, words: global.showWords, alphabetical: global.alphabetical, hideIncomplete: global.hideIncomplete, sequential: global.sequentialLinks, ssml: global.ssml, mp3: global.mp3, ftp: global.ftp, googleAnalyticsID: global.googleAnalyticsID }; fs.writeJson(file, obj, { spaces: 2 }, (err) => { if (err) { console.log(err); } }); } function numberWithCommas(x) { if (!x) { return ''; } return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); } //PROGRAM STARTS //make a folder fs.ensureDir('./' + outDirName, (err) => { console.log(err); }); let jsonOut = { indexFile: { fileContents: '', projectAuthor: '' }, pages: [] }; if (headerFileExists) { makeFileAString(headerFile).then((data) => { global.headerString = data; }); } else { console.log('There is no header file'); } if (cssFileExists) { makeFileAString(cssFile).then((data) => { fs.outputFile(outDirName + '/style.css', data, (err) => { console.log(err); }); }); } function runProgram() { if (global.makeBloomFile) { generateBloomFile(); // before anything else, generate a bloomfile if we're supposed to } if (!imageFolderExists) { fs.ensureDir(`./${outDirName}/images`, (err) => { console.log(err); }); } if (global.ssml) { fs.ensureDir(`./${outDirName}/ssml`, (err) => { console.log(err); }); } makeFileAString(inputFile).then((data) => { const splitter = '

'; // we could have this be a prompted answer const images = collectImages(data); if (images !== null) { images.forEach((img) => { fs.copy('./' + img, outDirName + '/' + img, (err) => { console.log(err); }); }); } const textArray = data.split(splitter); const includeInIndex = ''; if (coverImageExists) { fs.createReadStream(coverFileLocation).pipe( fs.createWriteStream(outDirName + '/' + coverFileLocation) ); coverImage = ``; } else { coverImage = ''; } const datetime = new Date(); const commentDate = ''; let indexStr = commentDate + '

')[0] : 'index'; let hideThis = false; let nextTitle = next < textArray.length ? textArray[next].split('')[0] : 'index'; let lastTitle = last > 0 ? textArray[last].split('')[0] : ''; if (title.indexOf('%') > -1) { if (!global.hideIncomplete) { title = title.replace('%', ''); } else { hideThis = true; } } title = stripTags(title); //strip tags lastTitle = stripTags(lastTitle); nextTitle = stripTags(nextTitle); title = title.replace(new RegExp('"', 'g'), ''); lastTitle = lastTitle.replace(new RegExp('"', 'g'), ''); nextTitle = nextTitle.replace(new RegExp('"', 'g'), ''); const webTitle = toWebTitle(title, textArray[i]); const lastWebTitle = lastTitle !== '' ? toWebTitle(lastTitle, textArray[last]) : ''; const nextWebTitle = nextTitle !== 'index' ? toWebTitle(nextTitle, textArray[next]) : ''; if (title !== 'index') { indexStr = hideThis ? indexStr : indexStr + `
  • ${title}
  • `; } const backButtonMarkup = global.sequentialLinks && lastWebTitle !== '' ? `

    home

    previous` : "

    back"; const nextButtonMarkup = global.sequentialLinks && nextWebTitle !== '' ? `

    next



    ` : "

    back



    "; const audioMarkup = global.mp3 ? `

    ` : ''; const backButton = title === 'index' || (lastWebTitle.indexOf('%') > -1 && global.sequentialLinks) ? '' : backButtonMarkup; const nextButton = title === 'index' || nextWebTitle.indexOf('%') > -1 ? '' : nextButtonMarkup; let finalText = global.projectAuthor ? textArray[i].replace( '', `

    by ${global.projectAuthor}

    ` ) : textArray[i]; finalText = splitter + finalText; finalText = replaceQuotes(finalText); finalText = typeset(finalText); let fileContents = commentDate + backButton + audioMarkup + finalText + nextButton + analytics + ''; const wordCount = finalText.split(' ').length; pageObject.title = title; pageObject.lastTitle = lastTitle; pageObject.nextTitle = nextTitle; pageObject.webTitle = webTitle; pageObject.lastWebTitle = lastWebTitle; pageObject.nextWebTitle = nextWebTitle; pageObject.backButton = backButtonMarkup; pageObject.nextButton = nextButtonMarkup; pageObject.audioPlayer = audioMarkup; pageObject.fileContents = splitter + finalText; pageObject.wordCount = wordCount; //spit out the file in this next part if (title !== 'index' && !hideThis && webTitle !== '') { const thisHeader = global.headerString.replace( '', '' + title + '' ); const htmlFileLocation = webTitle + '.html'; pageObject.htmlFile = htmlFileLocation; fs.outputFile( outDirName + '/' + htmlFileLocation, thisHeader + fileContents, (err) => { // this is htmlencoding automatically so we don't do it here if (err) { console.log(err); } } ); if (global.ssml) { finalText = finalText.replace( new RegExp(//, 'g'), '-BREAKSPACE-' ); let file = entities.decodeHTML( removeMD(splitter + finalText) ); const prepend = ''; const append = ''; const authorOrNot = global.projectAuthor ? 'by ' + global.projectAuthor : global.projectAuthor; const breakOrNot = global.projectAuthor ? ' by ' + global.projectAuthor + '' : ''; file = file.replace( new RegExp(authorOrNot, 'i'), breakOrNot ); file = file.replace( new RegExp(/-BREAKSPACE-/, 'g'), '' ); file = file.replace( new RegExp(/\n\n/, 'g'), '' ); file = file.replace( new RegExp(/ -- /, 'g'), '' ); file = file.replace( new RegExp(/ - /, 'g'), '' ); file = file.replace( new RegExp(/ – /, 'g'), '' ); file = file.replace( new RegExp(/\n/, 'g'), '' ); file = file.replace( new RegExp(/\? /, 'g'), '?' ); file = file.replace( new RegExp(/\.\.\./, 'g'), '?' ); file = prepend + file + append; file = ssmlVal.correct(file); const ssmlFileLocation = 'ssml/' + webTitle + '.xml'; fs.outputFile( outDirName + '/' + ssmlFileLocation, file, (err) => { pageObject.ssmlFile = ssmlFileLocation; if (err) { console.log(err); } else if (global.mp3) { const mp3FileLocation = 'ssml/' + webTitle + '.mp3'; pageObject.mp3File = mp3FileLocation; fs.pathExists( outDirName + '/' + mp3FileLocation, (err, exists) => { if (err) { console.log(err); } //if the path exists, we already have an mp3 of this version, don't need to make a new one if (!exists) { if (!shell.which('tts')) { //if the user doesn't have tts, we shouldn't try to do this. shell.echo( 'You need the tts package to create MP3s.' ); shell.exit(1); } else { let femaleNarrator = false; const howManyIs = file.match(/ i /gi) !== null ? file.match(/ i /gi) .length : 0; const howManyHes = file.match(/ he /gi) !== null ? file.match(/ he /gi) .length : 0; const howManyShes = file.match(/ she /gi) !== null ? file.match(/ she /gi) .length : 0; if ( howManyIs < howManyShes && howManyShes > howManyHes ) { femaleNarrator = true; } const genderFlag = femaleNarrator ? '--voice Joanna' : '--voice Matthew'; //path doesn't exist, user has tts, make mp3 file shell.exec( `tts ${outDirName}/ssml/${webTitle}.xml ${outDirName}/ssml/${webTitle}.mp3 --type ssml --sample-rate 16000 ${genderFlag}` ); } } } ); } } ); } } //after making file, gather stuff for index file if (global.showWords && title !== '' && title !== 'index') { title = title + ' (' + numberWithCommas(wordCount) + ' words)'; indexStr = indexStr.replace( new RegExp(title), title + ' (' + wordCount + ' words)' ); } if (title !== '' && title !== 'index') { titleArray.push(title); webTitleArray.push(webTitle); jsonOut.pages.push(pageObject); } } if (global.alphabetical) { titleArray.sort((a, b) => { return a.toLowerCase().localeCompare(b.toLowerCase()); }); webTitleArray.sort(); console.log(titleArray, webTitleArray); } indexStr = commentDate + '
      '; //start over with this string let j = -1; titleArray.map((one) => { j++; if (one.indexOf('%') > -1) { return ''; } if (one === 'index') { return ''; } const nonWebTitle = one.split(' ('); let webTitleTwo = webTitleArray[j]; webTitleTwo = webTitleTwo.split('-('); if (nonWebTitle.length > 1) { indexStr += "
    • " + nonWebTitle[0] + ' (' + nonWebTitle[1] + '
    • '; } else { indexStr += "
    • " + nonWebTitle[0] + '
    • '; } }); const projectAuthor = global.projectAuthor ? '
      by ' + global.projectAuthor + '
      ' : ''; const finalFile = global.headerMarkup + indexStyles + coverImage + '

      ' + global.projectTitle + '

      ' + global.projectSubtitle + '

      ' + projectAuthor + includeInIndex + indexStr + '
    ' + analytics + ''; jsonOut.indexFile.fileContents = finalFile; jsonOut.indexFile.projectAuthor = global.projectAuthor; fs.writeJson( outDirName + '/bloomed.json', jsonOut, { spaces: 2 }, (err) => { if (err) { console.log(err); } } ); fs.outputFile(outDirName + '/index.html', finalFile, (err) => { if (err) { console.log(err); } //launch the static site in the user's browser if (global.ftp) { const conn = new vinylFtp({ host: global.hostname, user: global.username, password: global.password, parallel: 10, log: function (item) { console.log(item); } }); vinylFs .src([outDirName + '/**'], { buffer: false }) .pipe(conn.dest(global.remotePath)); } openFile(outDirName + '/index.html'); }); }); }