/* 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 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', staticFiles = ['about.html', 'contributing.html', 'array.html']; // Set variables for script let snippets = {}, archivedSnippets = {}, glossarySnippets = {}, startPart = '', endPart = '', output = '', archivedStartPart = '', archivedEndPart = '', archivedOutput = '', glossaryStartPart = '', glossaryEndPart = '', 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 = fs.readFileSync(path.join(staticPartsPath, 'page-start.html'), 'utf8'); endPart = fs.readFileSync(path.join(staticPartsPath, '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 += '
' + `
` + 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 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 static files staticFiles.forEach(f => { try { 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');