openlayers
Version:
Build tools and sources for developing OpenLayers based mapping applications
273 lines (258 loc) • 9.01 kB
JavaScript
var path = require('path');
var Metalsmith = require('metalsmith');
var handlebars = require('handlebars');
var templates = require('metalsmith-layouts');
var marked = require('marked');
var pkg = require('../package.json');
var markupRegEx = /([^\/^\.]*)\.html$/;
var cleanupJSRegEx = /.*(\/\/ NOCOMPILE|goog\.require\(.*\);)[\r\n]*/g;
var requiresRegEx = /.*goog\.require\('(ol\.\S*)'\);/g;
var isCssRegEx = /\.css$/;
var isJsRegEx = /\.js$/;
var srcDir = path.join(__dirname, '..', 'examples');
var destDir = path.join(__dirname, '..', 'build', 'examples');
var templatesDir = path.join(__dirname, '..', 'config', 'examples');
/**
* Returns an array of names that are explicitly required inside the source
* by calling `goog.require('ol.…')`. Only returns `ol.` prefixed names.
*
* @param {string} src The JavaScript sourcecode to search for goog.require.
* @returns {Array.<string>} An array of `ol.*` names.
*/
function getRequires(src) {
var requires = [];
var match = requiresRegEx.exec(src);
while (match) {
requires.push(match[1]);
match = requiresRegEx.exec(src);
}
return requires;
}
/**
* Takes an array of the names of required OpenLayers symbols and returns an
* HTML-snippet with an unordered list to the API-docs for the particular
* classes.
*
* @param {Array.<string>} requires An array of `ol.` names that the source
* requires.
* @returns {string} The HTML-snippet with the list of links to API-docs.
*/
function getLinkToApiHtml(requires) {
var lis = requires.map(function(symb) {
var href = '../apidoc/' + symb + '.html';
return '<li><a href="' + href + '" title="API documentation for ' +
symb + '">' + symb + '</a></li>';
});
return '<ul class="inline">' + lis.join() + '</ul>';
}
/**
* A Metalsmith plugin that adds metadata to the example HTML files. For each
* example HTML file, this adds metadata for related js and css resources. When
* these files are run through the example template, the extra metadata is used
* to show the complete example source in the textarea and submit the parts to
* jsFiddle.
*
* @param {Object} files The file lookup provided by Metalsmith. Property names
* are file paths relative to the source directory. The file objects
* include any existing metadata (e.g. from YAML front-matter), the file
* contents, and stats.
* @param {Object} metalsmith The metalsmith instance the plugin is being used
* with.
* @param {function(Error)} done Called when done (with any error).
*/
function augmentExamples(files, metalsmith, done) {
setImmediate(done); // all remaining code is synchronous
for (var filename in files) {
var file = files[filename];
var match = filename.match(markupRegEx);
if (match && filename !== 'index.html') {
if (!file.layout) {
throw new Error(filename + ': Missing "layout" in YAML front-matter');
}
var id = match[1];
// add js tag and source
var jsFilename = id + '.js';
if (!(jsFilename in files)) {
throw new Error('No .js file found for ' + filename);
}
var jsSource = files[jsFilename].contents.toString()
// Change data paths to absolute urls
.replace(/'data\//g, '\'https://openlayers.org/en/v' + pkg.version + '/examples/data/');
if (file.cloak) {
for (var key in file.cloak) {
jsSource = jsSource.replace(new RegExp(key, 'g'), file.cloak[key]);
}
}
var requires = getRequires(jsSource);
file.requires = requires;
file.js = {
tag: '<script src="loader.js?id=' + id + '"></script>',
source: jsSource.replace(cleanupJSRegEx, ''),
apiHtml: getLinkToApiHtml(requires)
};
// add css tag and source
var cssFilename = id + '.css';
if (cssFilename in files) {
file.css = {
tag: '<link rel="stylesheet" href="' + cssFilename + '">',
source: files[cssFilename].contents.toString()
};
}
// add additional resources
if (file.resources) {
var resources = [];
var remoteResources = [];
var fiddleResources = [];
for (var i = 0, ii = file.resources.length; i < ii; ++i) {
var resource = file.resources[i];
var remoteResource = resource.indexOf('//') === -1 ?
'https://openlayers.org/en/v' + pkg.version + '/examples/' +
resource : resource;
fiddleResources[i] = remoteResource;
if (isJsRegEx.test(resource)) {
resources[i] = '<script src="' + resource + '"></script>';
remoteResources[i] = '<script src="' + remoteResource +
'"></script>';
} else if (isCssRegEx.test(resource)) {
if (resource.indexOf('bootstrap.min.css') === -1) {
resources[i] = '<link rel="stylesheet" href="' + resource + '">';
}
remoteResources[i] = '<link rel="stylesheet" href="' +
remoteResource + '">';
} else {
throw new Error('Invalid value for resource: ' +
resource + ' is not .js or .css: ' + filename);
}
}
file.extraHead = {
local: resources.join('\n'),
remote: remoteResources.join('\n')
};
file.extraResources = file.resources.length ?
',' + fiddleResources.join(',') : '';
}
}
}
}
/**
* Create an inverted index of keywords from examples. Property names are
* lowercased words. Property values are objects mapping example index to word
* count.
* @param {Array.<Object>} exampleInfos Array of example info objects.
* @return {Object} Word index.
*/
function createWordIndex(exampleInfos) {
var index = {};
var keys = ['shortdesc', 'title', 'tags', 'requires'];
exampleInfos.forEach(function(info, i) {
keys.forEach(function(key) {
var text = info[key];
if (Array.isArray(text)) {
text = text.join(' ');
}
var words = text ? text.split(/\W+/) : [];
words.forEach(function(word) {
if (word) {
word = word.toLowerCase();
var counts = index[word];
if (counts) {
if (index in counts) {
counts[i] += 1;
} else {
counts[i] = 1;
}
} else {
counts = {};
counts[i] = 1;
index[word] = counts;
}
}
});
});
});
return index;
}
/**
* A plugin that generates the example index.js file. This file includes a
* list of example metadata objects and a word index used when searching for
* examples.
* @param {Object} files The file lookup provided by Metalsmith. Property names
* are file paths relative to the source directory. The file objects
* include any existing metadata (e.g. from YAML front-matter), the file
* contents, and stats.
* @param {Object} metalsmith The metalsmith instance the plugin is being used
* with.
* @param {function(Error)} done Called when done (with any error).
*/
function createIndex(files, metalsmith, done) {
setImmediate(done); // all remaining code is synchronous
var exampleInfos = [];
for (var filename in files) {
var example = files[filename];
if (markupRegEx.test(filename) && filename !== 'index.html') {
exampleInfos.push({
link: filename,
example: filename,
title: example.title,
shortdesc: example.shortdesc,
tags: example.tags,
requires: example.requires
});
}
}
var info = {
examples: exampleInfos,
index: createWordIndex(exampleInfos)
};
files['index.js'] = {
contents: new Buffer('var info = ' + JSON.stringify(info)),
mode: '0644'
};
}
function main(callback) {
var smith = new Metalsmith('.')
.source(srcDir)
.destination(destDir)
.concurrency(25)
.metadata({
olVersion: pkg.version
})
.use(augmentExamples)
.use(createIndex)
.use(templates({
engine: 'handlebars',
directory: templatesDir,
helpers: {
md: function(str) {
return new handlebars.SafeString(marked(str));
},
indent: function(text, options) {
if (!text) {
return text;
}
var count = options.hash.spaces || 2;
var spaces = new Array(count + 1).join(' ');
return text.split('\n').map(function(line) {
return line ? spaces + line : '';
}).join('\n');
}
}
}))
.build(function(err) {
callback(err);
});
return smith;
}
if (require.main === module) {
main(function(err) {
if (err) {
process.stderr.write(
'Building examples failed. See the full trace below.\n\n' +
err.stack + '\n');
process.exit(1);
} else {
process.exit(0);
}
});
}
module.exports = main;