/* This is the web builder script that generates the web files. Run using `npm run webber`. */ // Load modules const fs = require('fs-extra'), https = require('https'), path = require('path'), chalk = require('chalk'), md = require('markdown-it')(), minify = require('html-minifier').minify; const util = require('./util'); var Prism = require('prismjs'); const unescapeHTML = str => str.replace( /&|<|>|'|"/g, tag => ({ '&': '&', '<': '<', '>': '>', ''': "'", '"': '"' }[tag] || tag) ); 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'; // Set variables for script let snippets = {}, archivedSnippets = {}, beginnerSnippetNames = [ 'allEqual', 'everyNth', 'filterNonUnique', 'last', 'maxN', 'minN', 'nthElement', 'offset', 'sample', 'similarity', 'tail', 'currentURL', 'hasClass', 'getMeridiemSuffixOfInteger', 'factorial', 'fibonacci', 'gcd', 'isDivisible', 'isEven', 'isPrime', 'lcm', 'randomIntegerInRange', 'sum', 'reverseString', 'truncateString' ], startPart = '', endPart = '', output = '', beginnerStartPart = '', beginnerEndPart = '', beginnerOutput = '', archivedStartPart = '', archivedEndPart = '', archivedOutput = '', glossaryStartPart = '', glossaryEndPart = '', glossaryOutput = '', indexStaticFile = '', 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 = fs.readFileSync(path.join(staticPartsPath, 'page-start.html'), 'utf8'); endPart = fs.readFileSync(path.join(staticPartsPath, 'page-end.html'), 'utf8'); beginnerStartPart = fs.readFileSync( path.join(staticPartsPath, 'beginner-page-start.html'), 'utf8' ); beginnerEndPart = fs.readFileSync(path.join(staticPartsPath, 'beginner-page-end.html'), 'utf8'); archivedStartPart = fs.readFileSync( path.join(staticPartsPath, 'archived-page-start.html'), 'utf8' ); archivedEndPart = fs.readFileSync(path.join(staticPartsPath, 'archived-page-end.html'), 'utf8'); glossaryStartPart = fs.readFileSync( path.join(staticPartsPath, 'glossary-page-start.html'), 'utf8' ); glossaryEndPart = fs.readFileSync(path.join(staticPartsPath, 'glossary-page-end.html'), '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 { // Add the start static part output += `${startPart}${'\n'}`; // Loop over tags and snippets to create the table of contents for (let tag of [...new Set(Object.entries(tagDbData).map(t => t[1][0]))] .filter(v => v) .sort( (a, b) => util.capitalize(a, true) === 'Uncategorized' ? 1 : util.capitalize(b, true) === 'Uncategorized' ? -1 : a.localeCompare(b) )) { 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(/ t[1][0]))] .filter(v => v) .sort( (a, b) => util.capitalize(a, true) === 'Uncategorized' ? 1 : util.capitalize(b, true) === 'Uncategorized' ? -1 : a.localeCompare(b) )) { 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 += '
' + `
${taggedSnippet[1].includes('advanced') ? 'advanced' : taggedSnippet[1].includes('beginner') ? 'beginner' : 'intermediate'}
` + md .render(`\n${snippets[taggedSnippet[0] + '.md']}`) .replace( /

/g, '

' ) .replace( /
/m,
            '
'
          )
          .replace(
            /
([^\0]*?)<\/code><\/pre>/gm,
            (match, p1) =>
              `
${Prism.highlight(
                unescapeHTML(p1),
                Prism.languages.javascript
              )}
` ) .replace(/<\/div>\s*
\s+
examples
([^\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: tag, content: localOutput });
  }
  // Minify output
  pagesOutput.forEach(page => {
    page.content = minify(page.content, {
      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
    });
    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);
}

/*
// Create the output for the beginner.html file
try {
  // Add the static part
  beginnerOutput += `${beginnerStartPart + '\n'}`;

  // Filter begginer snippets
  const filteredBeginnerSnippets = Object.keys(snippets)
    .filter(key => beginnerSnippetNames.map(name => name + '.md').includes(key))
    .reduce((obj, key) => {
      obj[key] = snippets[key];
      return obj;
    }, {});

  for (let snippet of Object.entries(filteredBeginnerSnippets))
    beginnerOutput +=
      '
' + '
' + '
' + md .render(`\n${snippets[snippet[0]]}`) .replace(/

/g, `${snippet[1].includes('advanced') ? 'advanced' : ''}

` ) .replace(/<\/h3>/g, '

') .replace( /
([^\0]*?)<\/code><\/pre>/gm,
          (match, p1) =>
            `
${Prism.highlight(
              unescapeHTML(p1),
              Prism.languages.javascript
            )}
` ) .replace(/<\/pre>\s+
📋 Copy to clipboard' +
      '
'; // Optimize punctuation nodes beginnerOutput = util.optimizeNodes( beginnerOutput, /([^\0<]*?)<\/span>([\n\r\s]*)([^\0]*?)<\/span>/gm, (match, p1, p2, p3) => `${p1}${p2}${p3}` ); // Optimize operator nodes beginnerOutput = util.optimizeNodes( beginnerOutput, /([^\0<]*?)<\/span>([\n\r\s]*)([^\0]*?)<\/span>/gm, (match, p1, p2, p3) => `${p1}${p2}${p3}` ); // Optimize keyword nodes beginnerOutput = util.optimizeNodes( beginnerOutput, /([^\0<]*?)<\/span>([\n\r\s]*)([^\0]*?)<\/span>/gm, (match, p1, p2, p3) => `${p1}${p2}${p3}` ); beginnerOutput += `${beginnerEndPart}`; // Generate and minify 'beginner.html' file const minifiedBeginnerOutput = minify(beginnerOutput, { 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 }); fs.writeFileSync(path.join(docsPath, 'beginner.html'), minifiedBeginnerOutput); console.log(`${chalk.green('SUCCESS!')} beginner.html file generated!`); } catch (err) { console.log(`${chalk.red('ERROR!')} During beginner.html generation: ${err}`); process.exit(1); } */ // Create the output for the archive.html file try { // Add the static part archivedOutput += `${archivedStartPart + '\n'}`; // Filter README.md from folder const excludeFiles = ['README.md']; const filteredArchivedSnippets = Object.keys(archivedSnippets) .filter(key => !excludeFiles.includes(key)) .reduce((obj, key) => { obj[key] = archivedSnippets[key]; return obj; }, {}); // Generate archived snippets from md files for (let snippet of Object.entries(filteredArchivedSnippets)) archivedOutput += '
' + md .render(`\n${filteredArchivedSnippets[snippet[0]]}`) .replace( /

/g, '

' ) .replace( /
/m,
          '
'
        )
        .replace(
          /
([^\0]*?)<\/code><\/pre>/gm,
          (match, p1) =>
            `
${Prism.highlight(
              unescapeHTML(p1),
              Prism.languages.javascript
            )}
` ) .replace(/<\/div>\s*
\s+
examples
([^\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 += `${archivedEndPart}`;

  // Generate and minify 'archive.html' file
  const minifiedArchivedOutput = minify(archivedOutput, {
    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
  });

  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
  glossaryOutput += `${glossaryStartPart + '\n'}`;

  // Filter README.md from folder
  const excludeFiles = ['README.md'];

  const filteredGlossarySnippets = Object.keys(glossarySnippets)
    .filter(key => !excludeFiles.includes(key))
    .reduce((obj, key) => {
      obj[key] = glossarySnippets[key];
      return obj;
    }, {});

  // Generate glossary snippets from md files
  for (let snippet of Object.entries(filteredGlossarySnippets))
    glossaryOutput +=
      '
' + md .render(`\n${filteredGlossarySnippets[snippet[0]]}`) .replace( /

/g, '

' ) + '
'; glossaryOutput += `${glossaryEndPart}`; // Generate and minify 'glossary.html' file const minifiedGlossaryOutput = minify(glossaryOutput, { 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 }); 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 about.html try { fs.copyFileSync(path.join(staticPartsPath, 'about.html'), path.join(docsPath, 'about.html')); console.log(`${chalk.green('SUCCESS!')} about.html file copied!`); } catch (err) { console.log(`${chalk.red('ERROR!')} During about.html copying: ${err}`); process.exit(1); } // Copy contributing.html try { fs.copyFileSync(path.join(staticPartsPath, 'contributing.html'), path.join(docsPath, 'contributing.html')); console.log(`${chalk.green('SUCCESS!')} contributing.html file copied!`); } catch (err) { console.log(`${chalk.red('ERROR!')} During contributing.html copying: ${err}`); process.exit(1); } // Log the time taken console.timeEnd('Webber');