Files
30-seconds-of-code/scripts/web.js
Angelos Chalaris b8b99bc399 Updated web script to generate landing page
Prioritized webber over builder to make sure it never times out. Prioritized the index page over the rest of the pages as it takes long due to api calls.
2018-04-07 17:40:44 +03:00

276 lines
14 KiB
JavaScript

/*
This is the web builder script that generates the README file.
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 =>
({
'&': '&',
'&lt;': '<',
'&gt;': '>',
'&#39;': "'",
'&quot;': '"'
}[tag] || tag)
);
if(util.isTravisCI() && /^Travis build: \d+/g.test(process.env['TRAVIS_COMMIT_MESSAGE'])) {
console.log(`${chalk.green('NOBUILD')} index build terminated, parent commit is a Travis build!`);
process.exit(0);
}
// Compile the mini.css framework and custom CSS styles, using `node-sass`.
const sass = require('node-sass');
sass.render(
{
file: path.join('docs', 'mini', 'flavor.scss'),
outFile: path.join('docs', 'mini.css'),
outputStyle: 'compressed'
},
function(err, result) {
if (!err) {
fs.writeFile(path.join('docs', 'mini.css'), result.css, function(err2) {
if (!err2) console.log(`${chalk.green('SUCCESS!')} mini.css file generated!`);
else console.log(`${chalk.red('ERROR!')} During mini.css file generation: ${err}`);
});
} else {
console.log(`${chalk.red('ERROR!')} During mini.css file generation: ${err}`);
}
}
);
// Set variables for paths
const snippetsPath = './snippets',
staticPartsPath = './static-parts',
docsPath = './docs';
// Set variables for script
let snippets = {},
beginnerSnippetNames = ['everyNth', 'filterNonUnique', 'last', 'maxN', 'minN', 'nthElement', 'sample', 'similarity', 'tail', 'currentURL', 'hasClass', 'getMeridiemSuffixOfInteger', 'factorial', 'fibonacci', 'gcd', 'isDivisible', 'isEven', 'isPrime', 'lcm', 'randomIntegerInRange', 'sum', 'reverseString', 'truncateString'],
startPart = '',
endPart = '',
output = '',
beginnerStartPart = '',
beginnerEndPart = '',
beginnerOutput = '',
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);
// 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');
indexStaticFile = fs.readFileSync(path.join(staticPartsPath, 'index.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 the index.html file (only locally or on Travis CRON or custom job)
if(!util.isTravisCI() || (util.isTravisCI() && (process.env['TRAVIS_EVENT_TYPE'] === 'cron' || process.env['TRAVIS_EVENT_TYPE'] === 'api'))) {
try {
// Shuffle the array of snippets, pick 3
let indexDailyPicks = '';
let shuffledSnippets = util.shuffle(Object.keys(snippets)).slice(0,3);
const dailyPicks = Object.keys(snippets)
.filter(key => shuffledSnippets.includes(key))
.reduce((obj, key) => {
obj[key] = snippets[key];
return obj;
}, {});
// Generate the html for the picked snippets
for (let snippet of Object.entries(dailyPicks))
indexDailyPicks +=
'<div class="card fluid pick">' +
md
.render(`\n${snippets[snippet[0]]}`)
.replace(/<h3/g, `<h3 id="${snippet[0].toLowerCase()}" class="section double-padded"`)
.replace(/<\/h3>/g, `${snippet[1].includes('advanced')?'<mark class="tag">advanced</mark>':''}</h3>`)
.replace(/<\/h3>/g, '</h3><div class="section double-padded">')
.replace(/<pre><code class="language-js">([^\0]*?)<\/code><\/pre>/gm, (match, p1) => `<pre class="language-js">${Prism.highlight(unescapeHTML(p1), Prism.languages.javascript)}</pre>`)
.replace(/<\/pre>\s+<pre/g, '</pre><label class="collapse">Show examples</label><pre') +
'<button class="primary clipboard-copy">&#128203;&nbsp;Copy to clipboard</button>' +
'</div></div>';
// Select the first snippet from today's picks
indexDailyPicks = indexDailyPicks.replace('card fluid pick', 'card fluid pick selected');
// Optimize punctuation nodes
indexDailyPicks = util.optimizeNodes(indexDailyPicks, /<span class="token punctuation">([^\0<]*?)<\/span>([\n\r\s]*)<span class="token punctuation">([^\0]*?)<\/span>/gm, (match, p1, p2, p3) => `<span class="token punctuation">${p1}${p2}${p3}</span>`);
// Optimize operator nodes
indexDailyPicks = util.optimizeNodes(indexDailyPicks, /<span class="token operator">([^\0<]*?)<\/span>([\n\r\s]*)<span class="token operator">([^\0]*?)<\/span>/gm, (match, p1, p2, p3) => `<span class="token operator">${p1}${p2}${p3}</span>`);
// Optimize keyword nodes
indexDailyPicks = util.optimizeNodes(indexDailyPicks, /<span class="token keyword">([^\0<]*?)<\/span>([\n\r\s]*)<span class="token keyword">([^\0]*?)<\/span>/gm, (match, p1, p2, p3) => `<span class="token keyword">${p1}${p2}${p3}</span>`);
// Put the daily picks into the page
indexStaticFile = indexStaticFile.replace('$daily-picks',indexDailyPicks);
// Use the Github API to get the needed data
const githubApi = 'api.github.com';
const headers = { 'User-Agent' : '30-seconds-of-code'};
// Send requests and wait for responses, write to the page
https.get({host: githubApi, path: '/repos/chalarangelo/30-seconds-of-code/commits?per_page=1', headers: headers}, resCommits =>{
https.get({host: githubApi, path: '/repos/chalarangelo/30-seconds-of-code/contributors?per_page=1', headers: headers}, resContributors => {
https.get({host: githubApi, path: '/repos/chalarangelo/30-seconds-of-code/stargazers?per_page=1', headers: headers}, resStars => {
let commits = resCommits.headers.link.split('&').slice(-1)[0].replace(/[^\d]/g,''),
contribs = resContributors.headers.link.split('&').slice(-1)[0].replace(/[^\d]/g,''),
stars = resStars.headers.link.split('&').slice(-1)[0].replace(/[^\d]/g,'');
indexStaticFile = indexStaticFile.replace(/\$snippet-count/g, Object.keys(snippets).length).replace(/\$commit-count/g,commits).replace(/\$contrib-count/g,contribs).replace(/\$star-count/g,stars);
// Generate 'index.html' file
fs.writeFileSync(path.join(docsPath, 'index.html'), indexStaticFile);
console.log(`${chalk.green('SUCCESS!')} index.html file generated!`);
});
});
});
} catch (err) {
console.log(`${chalk.red('ERROR!')} During index.html generation: ${err}`);
process.exit(1);
}
}
// 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 += `<h3>` +
md
.render(`${util.capitalize(tag, true)}\n`)
.replace(/<p>/g, '')
.replace(/<\/p>/g, '') +
`</h3>`;
for (let taggedSnippet of Object.entries(tagDbData).filter(v => v[1][0] === tag))
output += md
.render(`[${taggedSnippet[0]}](./${tag}.html#${taggedSnippet[0].toLowerCase()})\n`)
.replace(/<p>/g, '')
.replace(/<\/p>/g, '')
.replace(/<a/g, `<a class="sublink-1" tags="${taggedSnippet[1].join(',')}"`);
output += '\n';
}
output += `</nav><main class="col-sm-12 col-md-8 col-lg-9" style="height: 100%;overflow-y: auto; background: #eceef2; padding: 0;">`;
output += `<a id="top">&nbsp;</a>`;
// Loop over tags and snippets to create the list of snippets
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))) {
let localOutput = output.replace(/\$tag/g, util.capitalize(tag)).replace(new RegExp(`./${tag}.html#`,'g'),'#');
localOutput += md
.render(`## ${util.capitalize(tag, true)}\n`)
.replace(/<h2>/g, '<h2 style="text-align:center;">');
for (let taggedSnippet of Object.entries(tagDbData).filter(v => v[1][0] === tag))
localOutput +=
'<div class="card fluid">' +
md
.render(`\n${snippets[taggedSnippet[0] + '.md']}`)
.replace(/<h3/g, `<h3 id="${taggedSnippet[0].toLowerCase()}" class="section double-padded"`)
.replace(/<\/h3>/g, `${taggedSnippet[1].includes('advanced')?'<mark class="tag">advanced</mark>':''}</h3>`)
.replace(/<\/h3>/g, '</h3><div class="section double-padded">')
.replace(/<pre><code class="language-js">([^\0]*?)<\/code><\/pre>/gm, (match, p1) => `<pre class="language-js">${Prism.highlight(unescapeHTML(p1), Prism.languages.javascript)}</pre>`)
.replace(/<\/pre>\s+<pre/g, '</pre><label class="collapse">Show examples</label><pre') +
'<button class="primary clipboard-copy">&#128203;&nbsp;Copy to clipboard</button>' +
'</div></div>';
// Add the ending static part
localOutput += `\n${endPart + '\n'}`;
// Optimize punctuation nodes
localOutput = util.optimizeNodes(localOutput, /<span class="token punctuation">([^\0<]*?)<\/span>([\n\r\s]*)<span class="token punctuation">([^\0]*?)<\/span>/gm, (match, p1, p2, p3) => `<span class="token punctuation">${p1}${p2}${p3}</span>`);
// Optimize operator nodes
localOutput = util.optimizeNodes(localOutput, /<span class="token operator">([^\0<]*?)<\/span>([\n\r\s]*)<span class="token operator">([^\0]*?)<\/span>/gm, (match, p1, p2, p3) => `<span class="token operator">${p1}${p2}${p3}</span>`);
// Optimize keyword nodes
localOutput = util.optimizeNodes(localOutput, /<span class="token keyword">([^\0<]*?)<\/span>([\n\r\s]*)<span class="token keyword">([^\0]*?)<\/span>/gm, (match, p1, p2, p3) => `<span class="token keyword">${p1}${p2}${p3}</span>`);
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+'.html'), page.content);
console.log(`${chalk.green('SUCCESS!')} ${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 +=
'<div class="row">' +
'<div class="col-sm-12 col-md-10 col-lg-8 col-md-offset-1 col-lg-offset-2">' +
'<div class="card fluid">' +
md
.render(`\n${snippets[snippet[0]]}`)
.replace(/<h3/g, `<h3 id="${snippet[0].toLowerCase()}" class="section double-padded"`)
.replace(/<\/h3>/g, `${snippet[1].includes('advanced')?'<mark class="tag">advanced</mark>':''}</h3>`)
.replace(/<\/h3>/g, '</h3><div class="section double-padded">')
.replace(/<pre><code class="language-js">([^\0]*?)<\/code><\/pre>/gm, (match, p1) => `<pre class="language-js">${Prism.highlight(unescapeHTML(p1), Prism.languages.javascript)}</pre>`)
.replace(/<\/pre>\s+<pre/g, '</pre><label class="collapse">Show examples</label><pre') +
'<button class="primary clipboard-copy">&#128203;&nbsp;Copy to clipboard</button>' +
'</div></div></div></div>';
// Optimize punctuation nodes
beginnerOutput = util.optimizeNodes(beginnerOutput, /<span class="token punctuation">([^\0<]*?)<\/span>([\n\r\s]*)<span class="token punctuation">([^\0]*?)<\/span>/gm, (match, p1, p2, p3) => `<span class="token punctuation">${p1}${p2}${p3}</span>`);
// Optimize operator nodes
beginnerOutput = util.optimizeNodes(beginnerOutput, /<span class="token operator">([^\0<]*?)<\/span>([\n\r\s]*)<span class="token operator">([^\0]*?)<\/span>/gm, (match, p1, p2, p3) => `<span class="token operator">${p1}${p2}${p3}</span>`);
// Optimize keyword nodes
beginnerOutput = util.optimizeNodes(beginnerOutput, /<span class="token keyword">([^\0<]*?)<\/span>([\n\r\s]*)<span class="token keyword">([^\0]*?)<\/span>/gm, (match, p1, p2, p3) => `<span class="token keyword">${p1}${p2}${p3}</span>`);
beginnerOutput += `${beginnerEndPart}`;
// Generate 'beginner.html' file
fs.writeFileSync(path.join(docsPath, 'beginner.html'), beginnerOutput);
console.log(`${chalk.green('SUCCESS!')} beginner.html file generated!`);
} catch (err) {
console.log(`${chalk.red('ERROR!')} During beginner.html generation: ${err}`);
process.exit(1);
}
// Log the time taken
console.timeEnd('Webber');