/* This is the web builder script that generates the web files. Run using `npm run webber`. */ // Load modules const fs = require('fs-extra'), path = require('path'), chalk = require('chalk'), md = require('markdown-it')(), minify = require('html-minifier').minify; const util = require('./util'); var Prism = require('prismjs'); const minifyHTML = str => minify(str, { collapseBooleanAttributes: true, collapseWhitespace: true, decodeEntities: false, minifyCSS: true, minifyJS: true, keepClosingSlash: true, processConditionalComments: true, removeAttributeQuotes: false, removeComments: true, removeEmptyAttributes: false, removeOptionalTags: false, removeScriptTypeAttributes: false, removeStyleLinkTypeAttributes: false, trimCustomFragments: true }); const unescapeHTML = str => str.replace( /&|<|>|'|"/g, tag => ({ '&': '&', '<': '<', '>': '>', ''': "'", '"': '"' }[tag] || tag) ); const generateSnippetCard = ( snippetList, snippetKey, addCornerTag = false ) => `
${ addCornerTag ? `
` : '' } ${md .render(`\n${addCornerTag ? snippetList[snippetKey[0] + '.md'] : snippetList[snippetKey[0]]}`) .replace(/

/g, '

') .replace( /
/m,
      '
'
    )
    .replace(
      /
([^\0]*?)<\/code><\/pre>/gm,
      (match, p1) =>
        `
${Prism.highlight(
          unescapeHTML(p1),
          Prism.languages.javascript
        )}
` ) .replace(/<\/div>\s*
\s+
examples
${['adapter','array','function','object'].includes(tag) ? '

Recommended Resource - ES6: The Right Parts

Learn new ES6 JavaScript language features like arrow function, destructuring, generators & more to write cleaner and more productive, readable programs.

' : ['browser', 'node', 'date'].includes(tag) ? '

Recommended Resource - JavaScript: The Hard Parts

Take your JavaScript to the next level. Gain an understanding of callbacks, higher order functions, closure, asynchronous and object-oriented JavaScript!

' : '

Recommended Resource - JavaScript: From Fundamentals to Functional JS

Learn higher-order functions, closures, scope, master key functional methods like map, reduce and filter and promises and ES6+ asynchronous JavaScript.

' }
`; const filterSnippets = (snippetList, excludedFiles) => Object.keys(snippetList) .filter(key => !excludedFiles.includes(key)) .reduce((obj, key) => { obj[key] = snippetList[key]; return obj; }, {}); if ( util.isTravisCI() && /^Travis build: \d+/g.test(process.env['TRAVIS_COMMIT_MESSAGE']) && process.env['TRAVIS_EVENT_TYPE'] !== 'cron' && process.env['TRAVIS_EVENT_TYPE'] !== 'api' ) { console.log( `${chalk.green('NOBUILD')} website build terminated, parent commit is a Travis build!` ); process.exit(0); } // Compile the SCSS file, using `node-sass`. const sass = require('node-sass'); sass.render( { file: path.join('docs', 'scss', 'style.scss'), outFile: path.join('docs', 'style.css'), outputStyle: 'compressed' }, function(err, result) { if (!err) { fs.writeFile(path.join('docs', 'style.css'), result.css, function(err2) { if (!err2) console.log(`${chalk.green('SUCCESS!')} style.css file generated!`); else console.log(`${chalk.red('ERROR!')} During style.css file generation: ${err}`); }); } else console.log(`${chalk.red('ERROR!')} During style.css file generation: ${err}`); } ); // Set variables for paths const snippetsPath = './snippets', archivedSnippetsPath = './snippets_archive', glossarySnippetsPath = './glossary', staticPartsPath = './static-parts', docsPath = './docs', staticFiles = ['about.html', 'contributing.html', 'array.html']; // Set variables for script let snippets = {}, archivedSnippets = {}, glossarySnippets = {}, startPart = '', endPart = '', output = '', archivedOutput = '', glossaryOutput = '', pagesOutput = [], tagDbData = {}; // Start the timer of the script console.time('Webber'); // Synchronously read all snippets and sort them as necessary (case-insensitive) snippets = util.readSnippets(snippetsPath); archivedSnippets = util.readSnippets(archivedSnippetsPath); glossarySnippets = util.readSnippets(glossarySnippetsPath); // Load static parts for all pages try { [startPart, endPart, staticPageStartPart, staticPageEndPart] = [ 'page-start.html', 'page-end.html', 'static-page-start.html', 'static-page-end.html' ].map(filename => fs.readFileSync(path.join(staticPartsPath, filename), 'utf8')); } catch (err) { // Handle errors (hopefully not!) console.log(`${chalk.red('ERROR!')} During static part loading: ${err}`); process.exit(1); } // Load tag data from the database tagDbData = util.readTags(); // Create the output for individual category pages try { let taggedData = util.prepTaggedData(tagDbData); // Add the start static part output += `${startPart}${'\n'}`; // Loop over tags and snippets to create the table of contents for (let tag of taggedData) { output += '

' + md .render(`${util.capitalize(tag, true)}\n`) .replace(/

/g, '') .replace(/<\/p>/g, '') + '

    '; for (let taggedSnippet of Object.entries(tagDbData).filter(v => v[1][0] === tag)) { output += md .render( `[${taggedSnippet[0]}](./${ tag === 'array' ? 'index' : tag }#${taggedSnippet[0].toLowerCase()})\n` ) .replace(/

    /g, '') .replace(/<\/p>/g, '') .replace(/Archive



    `; // Loop over tags and snippets to create the list of snippets for (let tag of taggedData) { let notEmbedded = true; let localOutput = output .replace(/\$tag/g, util.capitalize(tag)) .replace(new RegExp(`./${tag}#`, 'g'), '#'); if (tag === 'array') localOutput = localOutput.replace(new RegExp('./index#', 'g'), '#'); localOutput += md .render(`## ${util.capitalize(tag, true)}\n`) .replace(/

    /g, '

    '); for (let taggedSnippet of Object.entries(tagDbData).filter(v => v[1][0] === tag)) { localOutput += generateSnippetCard(snippets, taggedSnippet, true); if (Object.entries(tagDbData).filter(v => v[1][0] === tag).findIndex(v => v[0] === taggedSnippet[0]) >= Object.entries(tagDbData).filter(v => v[1][0] === tag).length * 0.5 && notEmbedded) { notEmbedded = !notEmbedded; localOutput += embedCard(tag); } } // Add the ending static part localOutput += `\n${endPart + '\n'}`; // Optimize punctuation nodes localOutput = util.optimizeNodes( localOutput, /([^\0<]*?)<\/span>([\n\r\s]*)([^\0]*?)<\/span>/gm, (match, p1, p2, p3) => `${p1}${p2}${p3}` ); // Optimize operator nodes localOutput = util.optimizeNodes( localOutput, /([^\0<]*?)<\/span>([\n\r\s]*)([^\0]*?)<\/span>/gm, (match, p1, p2, p3) => `${p1}${p2}${p3}` ); // Optimize keyword nodes localOutput = util.optimizeNodes( localOutput, /([^\0<]*?)<\/span>([\n\r\s]*)([^\0]*?)<\/span>/gm, (match, p1, p2, p3) => `${p1}${p2}${p3}` ); pagesOutput.push({ tag, content: localOutput }); } // Minify output pagesOutput.forEach(page => { page.content = minifyHTML(page.content); fs.writeFileSync( path.join(docsPath, (page.tag === 'array' ? 'index' : page.tag) + '.html'), page.content ); console.log( `${chalk.green('SUCCESS!')} ${page.tag === 'array' ? 'index' : page.tag}.html file generated!` ); }); } catch (err) { // Handle errors (hopefully not!) console.log(`${chalk.red('ERROR!')} During category page generation: ${err}`); process.exit(1); } const generateMenuForStaticPage = staticPart => { let taggedData = util.prepTaggedData(tagDbData); // Add the start static part let htmlCode; for (let tag of taggedData) { htmlCode += '

    ' + md .render(`${util.capitalize(tag, true)}\n`) .replace(/

    /g, '') .replace(/<\/p>/g, '') + '



    ${heading}

    ${description}


    `; return htmlCode.replace(/\$page_name/g, util.capitalize(heading)); }; // Create the output for the archive.html file try { // Add the static part let heading = 'Snippets Archive'; let description = "These snippets, while useful and interesting, didn't quite make it into the repository due to either having very specific use-cases or being outdated. However we felt like they might still be useful to some readers, so here they are."; let htmlCode = staticPageStartGenerator(staticPageStartPart, heading, description); archivedOutput += htmlCode; // Filter README.md from folder const filteredArchivedSnippets = filterSnippets(archivedSnippets, ['README.md']); // Generate archived snippets from md files for (let snippet of Object.entries(filteredArchivedSnippets)) archivedOutput += generateSnippetCard(filteredArchivedSnippets, snippet, false); // Optimize punctuation nodes archivedOutput = util.optimizeNodes( archivedOutput, /([^\0<]*?)<\/span>([\n\r\s]*)([^\0]*?)<\/span>/gm, (match, p1, p2, p3) => `${p1}${p2}${p3}` ); // Optimize operator nodes archivedOutput = util.optimizeNodes( archivedOutput, /([^\0<]*?)<\/span>([\n\r\s]*)([^\0]*?)<\/span>/gm, (match, p1, p2, p3) => `${p1}${p2}${p3}` ); // Optimize keyword nodes archivedOutput = util.optimizeNodes( archivedOutput, /([^\0<]*?)<\/span>([\n\r\s]*)([^\0]*?)<\/span>/gm, (match, p1, p2, p3) => `${p1}${p2}${p3}` ); archivedOutput += `${staticPageEndPart}`; // Generate and minify 'archive.html' file const minifiedArchivedOutput = minifyHTML(archivedOutput); fs.writeFileSync(path.join(docsPath, 'archive.html'), minifiedArchivedOutput); console.log(`${chalk.green('SUCCESS!')} archive.html file generated!`); } catch (err) { console.log(`${chalk.red('ERROR!')} During archive.html generation: ${err}`); process.exit(1); } // Create the output for the glossary.html file try { // Add the static part let heading = 'Glossary'; let description = 'Developers use a lot of terminology daily. Every once in a while, you might find a term you do not know. We know how frustrating that can get, so we provide you with a handy glossary of frequently used web development terms.'; let htmlCode = staticPageStartGenerator(staticPageStartPart, heading, description); glossaryOutput += htmlCode; // Filter README.md from folder const filteredGlossarySnippets = filterSnippets(glossarySnippets, ['README.md']); // Generate glossary snippets from md files for (let snippet of Object.entries(filteredGlossarySnippets)) { glossaryOutput += '
    ' + md .render(`\n${filteredGlossarySnippets[snippet[0]]}`) .replace(/

    /g, '

    ') + '
    '; } glossaryOutput += `${staticPageEndPart}`; // Generate and minify 'glossary.html' file const minifiedGlossaryOutput = minifyHTML(glossaryOutput); fs.writeFileSync(path.join(docsPath, 'glossary.html'), minifiedGlossaryOutput); console.log(`${chalk.green('SUCCESS!')} glossary.html file generated!`); } catch (err) { console.log(`${chalk.red('ERROR!')} During glossary.html generation: ${err}`); process.exit(1); } // Copy static files staticFiles.forEach(f => { try { if(f !== 'array.html') { let fileData = fs.readFileSync(path.join(staticPartsPath, f), 'utf8'); fs.writeFileSync(path.join(docsPath, f), generateMenuForStaticPage(fileData)); } else fs.copyFileSync(path.join(staticPartsPath, f), path.join(docsPath, f)); console.log(`${chalk.green('SUCCESS!')} ${f} file copied!`); } catch (err) { console.log(`${chalk.red('ERROR!')} During ${f} copying: ${err}`); process.exit(1); } }); // Log the time taken console.timeEnd('Webber');