wbyoung/azul

View on GitHub
docs/scripts/build

Summary

Maintainability
Test Coverage
#!/usr/bin/env node

var _ = require('lodash');
var Metalsmith = require('metalsmith');
var templates = require('metalsmith-templates');
var markdown = require('metalsmith-markdown');
var sass = require('metalsmith-sass');
var metallic = require('metalsmith-metallic');
var serve = require('metalsmith-serve');
var watch = require('metalsmith-watch');
var path = require('path');
var cp = require('child_process');
var semver = require('semver');
var cheerio = require('cheerio');
var commander = require('commander');
var program = new commander.Command()
  .option('-s --serve', 'serve and watch for changes')

program.parse(process.argv);


/**
 * Set all known tags as `azulTags`.
 *
 * @return {MetalsmithPlugin}
 */
var addKnownTags = function() {
  var args = ['tag', '-l'];
  var extract = function(v) { return v; };
  var filter = function(v) { return v; };
  if (process.env.TRAVIS) {
    args = ['ls-remote', '--tags', 'origin']
    extract = function(v) {
      var match = v.match(/refs\/tags\/([^^]*)$/);
      return match && match[1];
    };
  }

  var tags = cp.spawnSync('git', args, { encoding: 'utf8' })
    .stdout.trim().split('\n')
    .map(extract).filter(filter)
    .sort(semver.compare)
    .reverse();

  return function addKnownTags(files, metalsmith, done) {
    _.forEach(files, function(data, file) {
      data.azulTags = tags;
    });
    done();
  };
};

/**
 * Set current tag as `azulTag`.
 *
 * @return {MetalsmithPlugin}
 */
var addCurrentTag = function() {
  var tag = cp.spawnSync('git', ['describe', '--exact-match'],
    { encoding: 'utf8' }).stdout.trim();

  return function addCurrentTag(files, metalsmith, done) {
    _.forEach(files, function(data, file) {
      data.azulTag = tag;
    });
    done();
  };
};

/**
 * Creates version specific documentation `azulTag` has been set.
 *
 * This moves all documentation pages into a sub-directory for the specific
 * tag/version. It prevents the generation of any other pages. It also updates
 * all href links so the user remains on the current version documentation. The
 * changes made are:
 *
 *   /guides/* -> /version/guides/*
 *   /docs/* -> /version/docs/*
 *
 * @return {MetalsmithPlugin}
 */
var version = function() {
  return function version(files, metalsmith, done) {
    _.forEach(files, function(data, file) {
      if (!data.azulTag) { return; }
      if (file.match(/^(guides|docs)/)) {
        files[path.join(data.azulTag, file)] = data;

        var $ = cheerio.load(data.contents);
        $('a[href^="/guides"], a[href^="/docs"]').each(function() {
          $(this).attr('href', '/' + data.azulTag + $(this).attr('href'));
        });
        data.contents = new Buffer($.html());
      }
      delete files[file];
    });
    done();
  };
};

/**
 * Moves simple HTML pages into a subdirectory by the same name for pretty links.
 * For instance, `page.html` would become `page/index.html`.
 *
 * @return {MetalsmithPlugin}
 */
var indexify = function() {
  return function indexify(files, metalsmith, done) {
    _.forEach(files, function(data, file) {
      var match = file.match(/(.*)\.html?$/i);
      if (match && !file.match(/index\.html?/i)) {
        files[match[1] + '/index.html'] = data;
        delete files[file];
      }
    });
    done();
  };
};

/**
 * Generate a table of contents from the page content & make it available to
 * templates.
 *
 * @return {MetalsmithPlugin}
 */
var toc = function() {
  return function toc(files, metalsmith, done) {
    _.forEach(files, function(data, file) {
      if (!data.toc) { return; }
      var $ = cheerio.load(data.contents);
      var level = 0;
      var result = [];
      var root = { children: result };
      var nodes = [root];

      $('h1[id], h2[id], h3[id], h4[id]').each(function() {
        var headerLevel = this.tagName.match(/h(\d)/)[1] - 1;

        while (headerLevel < level) {
          delete nodes[level];
          level -= 1;
        }

        while (headerLevel > level) {
          level += 1;
          if (!nodes[level]) {
            throw new Error('Header level skipped, cannot generate TOC.');
          }
          nodes[level].children = nodes[level].children || [];
        }

        var parent = nodes[headerLevel];
        var header = $(this);
        var title = header.text();
        var id = header.attr('id');

        // check if this is a method name & if so, fix up the links
        var codeTitle = header.find('code:first-of-type').text();
        if (codeTitle) {
          title = codeTitle.replace(/^(\w*[#.]?\w+).*/i, '$1');
          id = title.toLowerCase().replace(/[^a-z]/, '-').replace(/^-/, '');
          if (codeTitle.match(/\w+\(.*\)/)) {
            title = title + '()';
          }
        }

        // change ids so that they are all more unique (based on parent ids)
        if (level > 1 && parent && parent.id) {
          header.attr('id', [parent.id, id].join('-'));
        }

        var node = {
          id: header.attr('id'),
          title: title
        };

        parent.children.push(node);
        nodes[headerLevel+1] = node;
      });

      data.toc = result;
      data.contents = new Buffer($.html());
    });

    done();
  };
};

var metalsmith = Metalsmith(path.join(__dirname, '..'))
  .source('./source')
  .destination('./build')
  .use(addKnownTags())
  .use(addCurrentTag())
  .use(sass())
  .use(metallic())
  .use(markdown())
  .use(toc())
  .use(templates('swig'))
  .use(indexify())
  .use(version());

if (program.serve) {
  metalsmith = metalsmith
    .use(watch({}))
    .use(serve());
}

metalsmith.build(function(err){
  if (err) { throw err; }
});