Skip to content

Recent Components page, RSS feed #131

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Dec 21, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 14 additions & 10 deletions build.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,16 @@ const startTime = process.hrtime();
// require('./src/home-build')()
// console.log('home build complete')

const componentsBuildList = require('./src/components-build-list');
const componentsBuildIndex = require('./src/components-build-index');
const componentsBuildRSS = require('./src/components-build-rss');
const componentsBuildPages = require('./src/components-build-pages');
const componentsBuildScreenshots = require('./src/components-build-screenshots');

// See src/components-build-defaults for list of options that can be overriden
const options = {
components: {
globPattern: 'src/components/banners/*.html',
// globPattern: 'src/components/article**/*.html',
// frontMatter: {
// bodyClass: 'bg-red',
// screenshot: {
Expand All @@ -35,20 +37,22 @@ const options = {
// },
};

// Note that componentsBuildIndex() generates the index *and* the JSON
// file listing all the components pages and screenshots should be built for.
// Scenario: if you are working on one component, uncomment and set componentsGlobPattern
// in the options above to only (re-)generate the index, pages, and screenshots for the
// corresponding category. When you are done, comment componentsGlobPattern back, comment
// Note that componentsBuildList() generates a temporary components list (saved as JSON),
// which is re-used by later steps (for performance).
// If you are working on one component, uncomment and set components.GlobPattern
// in the options above to only (re-)generate the list, index, pages, and screenshots for the
// corresponding category. Once you are done, comment components.GlobPattern back, comment
// the line below that generate the screenshots, then run the script -- this will
// re-create the full index page for all components, re-generate all the pages (since they need
// re-create the full index pages for all components, re-generate all the pages (since they need
// to cross-reference the new component), and use the previously generated screenshots as well
// as the new one (i.e. no need to re-generate *all* the screenshots unless you made
// modifications to the screenshots script itself).
co(function* generator() {
yield componentsBuildIndex(options); // <- builds component index page and JSON index
yield componentsBuildPages(options); // <- comment to skip building pages
yield componentsBuildScreenshots(options); // <- comment to skip building screenshots
yield componentsBuildList(options); // <- builds temporary components list (JSON)
yield componentsBuildIndex(options); // <- builds index pages (by category & most recent)
yield componentsBuildRSS(options); // <- builds RSS feed
yield componentsBuildPages(options); // <- comment to skip building pages
yield componentsBuildScreenshots(options); // <- comment to skip building screenshots
}).then(() => {
const elapsed = process.hrtime(startTime);
console.log(chalk.green('All done'), chalk.dim(prettyHrtime(elapsed)));
Expand Down
39 changes: 19 additions & 20 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,7 @@
"url": "https://github.com/tachyons-css/tachyons-css.github.io/issues"
},
"dependencies": {
"chalk": "^1.1.3",
"co": "^4.6.0",
"express": "^4.14.0",
"filesize": "^3.3.0",
"imagemin": "^5.2.2",
"imagemin-mozjpeg": "^6.0.0",
"jimp": "^0.2.27",
"lazysizes": "^3.0.0-rc2",
"nightmare": "^2.8.1",
"pretty-hrtime": "^1.0.3",
"tachyons": "^4.5.4",
"tachyons-background-size": "^5.0.3",
"tachyons-base": "^1.2.5",
"tachyons-border-colors": "^4.2.2",
Expand All @@ -62,7 +53,7 @@
"tachyons-debug": "^1.1.7",
"tachyons-debug-grid": "^1.2.0",
"tachyons-display": "^5.0.1",
"tachyons-flexbox": "^2.0.0",
"tachyons-flexbox": "^2.0.4",
"tachyons-floats": "^3.0.1",
"tachyons-font-family": "^4.3.0",
"tachyons-font-style": "^4.0.1",
Expand All @@ -76,11 +67,14 @@
"tachyons-links": "^3.0.6",
"tachyons-lists": "^2.0.9",
"tachyons-max-widths": "^4.0.1",
"tachyons-modules": "^1.1.8",
"tachyons-opacity": "^1.1.5",
"tachyons-outlines": "^1.0.0",
"tachyons-overflow": "^4.0.1",
"tachyons-position": "^6.0.1",
"tachyons-queries": "^0.3.1",
"tachyons-skins": "^4.0.0",
"tachyons-skins-pseudo": "^1.0.1",
"tachyons-spacing": "^6.0.1",
"tachyons-tables": "^1.0.4",
"tachyons-text-align": "^3.0.1",
Expand All @@ -94,31 +88,40 @@
"tachyons-white-space": "^4.0.1",
"tachyons-widths": "^5.0.1",
"tachyons-word-break": "3.0.1",
"tachyons-z-index": "^1.0.4",
"tmp": "0.0.31"
"tachyons-z-index": "^1.0.4"
},
"devDependencies": {
"autoprefixer": "^6.5.1",
"browser-sync": "^2.17.5",
"camelize": "^1.0.0",
"chalk": "^1.1.3",
"co": "^4.6.0",
"color": "^0.11.3",
"colorable": "^1.0.5",
"css": "^2.2.1",
"css-mqpacker": "^5.0.1",
"css-parse": "^2.0.0",
"cssstats": "^3.0.0-beta.2",
"escape-html": "^1.0.3",
"express": "^4.14.0",
"filesize": "^3.3.0",
"get-classes-from-html": "^1.0.1",
"glob": "^7.1.1",
"gzip-size": "^3.0.0",
"imagemin": "^5.2.2",
"imagemin-mozjpeg": "^6.0.0",
"immutable-css": "^1.1.2",
"is-blank": "^1.1.0",
"is-css-root": "^1.0.1",
"jimp": "^0.2.27",
"json-front-matter": "^1.0.0",
"lazysizes": "^3.0.0-rc2",
"lodash": "^4.16.6",
"mkdirp": "^0.5.1",
"moment": "^2.17.1",
"nightmare": "^2.8.1",
"node-minify": "^1.3.9",
"nodegit": "^0.16.0",
"normalize": "^0.3.1",
"perfectionist": "^2.3.1",
"pixrem": "^3.0.2",
Expand All @@ -132,16 +135,12 @@
"postcss-discard-empty": "^2.1.0",
"postcss-import": "^8.1.2",
"postcss-select": "^2.1.0",
"pretty-hrtime": "^1.0.3",
"remove-html-extension": "0.0.1",
"rss": "^1.2.1",
"s3": "^4.4.0",
"tachyons": "^4.5.4",
"tachyons-cli": "^1.0.9",
"tachyons-debug-grid": "^1.2.0",
"tachyons-flexbox": "^2.0.1",
"tachyons-modules": "^1.1.8",
"tachyons-queries": "^0.3.1",
"tachyons-skins-pseudo": "^1.0.1",
"titleize": "^1.0.0",
"tmp": "0.0.31",
"watch": "^1.0.1"
},
"contributors": [
Expand Down
89 changes: 68 additions & 21 deletions src/components-build-defaults.js
Original file line number Diff line number Diff line change
@@ -1,33 +1,80 @@
const moment = require('moment');
const titleize = require('titleize');

const cleanTitleize = str => titleize(str.replace(/(_|-)/g, ' '));
const creationTimeToYMD = c => moment(c.creationTime).format('YYYY-MM-DD');

module.exports = {
siteUrl: 'http://tachyons.io', // needed for RSS feed
siteDescription: 'Tachyons. Functional CSS for humans.',
// Components
components: {
globPattern: 'src/components/**/*.html', // source components to process
forNavPath: 'tmp/componentsForNav.json', // temporary file built by the index
indexPath: 'components/index.html', // target location of components index
buildPages: true, // false to skip building pages
buildScreenshots: true, // false to skip building screenshots
frontMatter: { // font matter defaults (i.e. optional)
name: undefined, // undefined to infer component name
title: undefined, // undefined to infer component title
bodyClass: 'bg-white', // class to apply on <body>
screenshot: { // per-component screenshot options
selector: '[data-name="component"]', // DOM element to capture
autocrop: true, // autocrop the capture
'background-position': 'center center', // CSS background position
'background-size': 'cover', // CSS background size ('cover', 'contain', 'auto')
globPattern: 'src/components/**/*.html', // source components to process
tempListPath: 'tmp/components.json', // temporary JSON file built by the index
build: {
pages: true, // false to skip building pages
screenshots: true, // false to skip building screenshots
},
index: { // list index pages to build
byCategory: { // components by category
title: 'Components',
path: 'components/index.html', // target location of index by category
sortAllBy: [['src'], ['asc']], // sort by file location will do
limitAll: false, // use all components
createSectionsBy: 'category', // create a section for each category
showSectionsTOC: true, // show Table of Contents (e.g. categories)
},
mostRecent: { // most recent components
title: 'Recent Components',
path: 'components/recent.html', // target location of recent index
sortAllBy: [['creationTime'], ['desc']], // sort by most recent component first
limitAll: 50, // use the 50 most recent ones
createSectionsBy: creationTimeToYMD, // group by day
prettifySection: v => moment(v).format('LL'), // display as day
showSectionsTOC: false, // no need for Table of Contents
},
},
rss: { // RSS feed
title: 'Tachyons Recent Components',
categories: ['CSS', 'Functional CSS'], // Categories this feed belongs to
ttl: 60, // Number of mins feed can be cached
path: 'components/rss.xml', // target location of feed (sync head.html)
count: 20, // how many in feed
},
page: { // options related to each component page
composeTitle: (category, name) => `${category} | ${name}`, // compose title from cat, name
},
prettify: {
id: cleanTitleize, // prettify component id into component name
category: cleanTitleize, // prettify component category
creationTime: v => moment(v).format('LLL'), // prettify component creation time
},
frontMatter: { // font matter defaults (i.e. optional)
name: undefined, // undefined to infer component name
title: undefined, // undefined to infer component title
bodyClass: 'bg-white', // class to apply on <body>
screenshot: { // per-component screenshot options
selector: '[data-name="component"]', // DOM element to capture
autocrop: true, // autocrop the capture
'background-position': 'center center', // CSS bg position
'background-size': 'cover', // CSS bg size ('cover', 'contain', 'auto')
},
},
},
// Screenshot options
// (components only for now, but could apply to other areas for consistency)
screenshot: {
basename: 'screenshot.jpg', // name of *JPEG* screenshot
aspectRatio: '4x3', // Tachyons aspect ratio of screenshots
viewportWidth: 1024, // viewport width used for capture
viewportHeight: 768, // viewport height used for capture
targetMinWidth: 400, // min width of target (final) screenshot
targetMinHeight: 160, // min height of target (final) screenshot
mozjpegQuality: 90, // mozjpeg optimizer quality (default 75)
basename: 'screenshot.jpg', // name of *JPEG* screenshot
aspectRatio: '4x3', // Tachyons aspect ratio of screenshots
viewport: {
width: 1024, // viewport width used for capture
height: 768, // viewport height used for capture
},
target: {
minWidth: 400, // min width of target (final) screenshot
minHeight: 160, // min height of target (final) screenshot
},
mozjpegQuality: 90, // mozjpeg optimizer quality (default 75)
},
// Misc
tachyonsCssPath: 'src/css/tachyons.css',
Expand Down
125 changes: 34 additions & 91 deletions src/components-build-index.js
Original file line number Diff line number Diff line change
@@ -1,106 +1,49 @@
const _ = require('lodash');
const chalk = require('chalk');
const crypto = require('crypto');
const fm = require('json-front-matter');
const fs = require('fs');
const glob = require('glob');
const mkdirp = require('mkdirp');
const path = require('path');
const prettyHrtime = require('pretty-hrtime');
const rmHtmlExt = require('remove-html-extension');
const titleize = require('titleize');

const defaults = require('./components-build-defaults');

const getTitle = (component) => {
const title = rmHtmlExt(component).replace('src/components/', '').replace(/(\/|_|-)/g, ' ');
return titleize(title);
};

const getName = component => titleize(getTitle(component.split('/')[3]));
const createSections = require('./components-build-sections');

module.exports = _options => new Promise((resolve, reject) => {
const options = _.merge({}, defaults, _options);
const startTime = process.hrtime();
glob(options.components.globPattern, {}, (err, components) => {
console.log(chalk.magenta('Working on components index...'));
if (err) {
reject(err);
return;
}

const npmPackage = JSON.parse(fs.readFileSync('package.json', 'utf8'));

const componentsForNav = {};
components.forEach((component) => {
const componentTokens = component.replace('src/components/', '').split('/');
const category = componentTokens[0];

const componentHtml = fs.readFileSync(component, 'utf8');
const fmParsed = fm.parse(componentHtml);
const srcFrontMatter = fmParsed.attributes || {};
const frontMatter = _.merge({}, options.components.frontMatter, srcFrontMatter);
const dir = component.replace('src/', '').replace('.html', '');

// Compute component signature based on the Tachyons version and the contents of the
// component itself. This can be used to bust the browser cache of screenshots.
const md5sum = crypto.createHash('md5');
md5sum.update(npmPackage.version);
md5sum.update(componentHtml);
const signature = md5sum.digest('hex');

componentsForNav[category] = componentsForNav[category] || [];
componentsForNav[category].push({
name: frontMatter.name || getName(component),
title: frontMatter.title || getTitle(component),
src: component,
path: `${dir}/index.html`,
href: `/${dir}/index.html`,
screenshot: {
path: `${dir}/${options.screenshot.basename}`,
href: `/${dir}/${options.screenshot.basename}?version=${signature}`,
},
signature,
// This is the raw front matter, as found in the component source, NOT merged
// with any defaults, so that it is easier to spot the overrides.
// It is up to build scripts to merge with options.components.frontMatter down the road.
frontMatter: srcFrontMatter,
});
});

const categories = Object.keys(componentsForNav);
console.log(
'- Found', components.length, components.length > 1 ? 'components' : 'component',
'in', categories.length, categories.length > 1 ? 'categories' : 'category'
);

mkdirp.sync(path.dirname(options.components.forNavPath));
fs.writeFileSync(options.components.forNavPath, JSON.stringify(componentsForNav, undefined, 2));
console.log('- Created navigation JSON:', options.components.forNavPath);

const analytics = fs.readFileSync(options.templates.analyticsPath, 'utf8');
const footer = fs.readFileSync(options.templates.footerPath, 'utf8');
const head = fs.readFileSync(options.templates.headPath, 'utf8');
const header = fs.readFileSync(options.templates.headerPath, 'utf8');
const componentsIndexTemplate = fs.readFileSync(options.templates.componentsIndexPath, 'utf8');
const lazysizesTemplate = fs.readFileSync(options.templates.lazysizesPath, 'utf8');

const compiledPage = _.template(componentsIndexTemplate)({
componentsForNav,
title: 'Components',
analytics,
footer,
head,
header,
lazysizesTemplate,
console.log(chalk.magenta('Working on components index...'));
if (options.components.tempListPath === undefined ||
!fs.existsSync(options.components.tempListPath)) {
reject('Can not find components list (JSON)');
return;
}

const components = JSON.parse(fs.readFileSync(options.components.tempListPath, 'utf8'));

const templates = {
analytics: fs.readFileSync(options.templates.analyticsPath, 'utf8'),
footer: fs.readFileSync(options.templates.footerPath, 'utf8'),
head: fs.readFileSync(options.templates.headPath, 'utf8'),
header: fs.readFileSync(options.templates.headerPath, 'utf8'),
componentsIndex: fs.readFileSync(options.templates.componentsIndexPath, 'utf8'),
lazysizes: fs.readFileSync(options.templates.lazysizesPath, 'utf8'),
};

Object.keys(options.components.index).forEach((key) => {
const index = options.components.index[key];
const componentsBySections = createSections(components, index, options);
const compiledIndexPage = _.template(templates.componentsIndex)({
index,
componentsBySections,
templates,
options,
});
mkdirp.sync(path.dirname(options.components.indexPath));
fs.writeFileSync(options.components.indexPath, compiledPage);
console.log('- Created index:', options.components.indexPath);

const elapsed = process.hrtime(startTime);
console.log(chalk.magenta('Done with components index!'), chalk.dim(prettyHrtime(elapsed)));
resolve();
}); // glob
mkdirp.sync(path.dirname(index.path));
fs.writeFileSync(index.path, compiledIndexPage);
console.log(`- Created index "${index.title}":`, index.path);
});

const elapsed = process.hrtime(startTime);
console.log(chalk.magenta('Done with components index!'), chalk.dim(prettyHrtime(elapsed)));
resolve();
}); // return promise
Loading