UNPKG

9.01 kBJavaScriptView Raw
1var path = require('path');
2
3var Metalsmith = require('metalsmith');
4var handlebars = require('handlebars');
5var templates = require('metalsmith-layouts');
6var marked = require('marked');
7var pkg = require('../package.json');
8
9var markupRegEx = /([^\/^\.]*)\.html$/;
10var cleanupJSRegEx = /.*(\/\/ NOCOMPILE|goog\.require\(.*\);)[\r\n]*/g;
11var requiresRegEx = /.*goog\.require\('(ol\.\S*)'\);/g;
12var isCssRegEx = /\.css$/;
13var isJsRegEx = /\.js(\?.*)?$/;
14
15var srcDir = path.join(__dirname, '..', 'examples');
16var destDir = path.join(__dirname, '..', 'build', 'examples');
17var templatesDir = path.join(__dirname, '..', 'config', 'examples');
18
19/**
20 * Returns an array of names that are explicitly required inside the source
21 * by calling `goog.require('ol.…')`. Only returns `ol.` prefixed names.
22 *
23 * @param {string} src The JavaScript sourcecode to search for goog.require.
24 * @returns {Array.<string>} An array of `ol.*` names.
25 */
26function getRequires(src) {
27 var requires = [];
28 var match = requiresRegEx.exec(src);
29 while (match) {
30 requires.push(match[1]);
31 match = requiresRegEx.exec(src);
32 }
33 return requires;
34}
35
36/**
37 * Takes an array of the names of required OpenLayers symbols and returns an
38 * HTML-snippet with an unordered list to the API-docs for the particular
39 * classes.
40 *
41 * @param {Array.<string>} requires An array of `ol.` names that the source
42 * requires.
43 * @returns {string} The HTML-snippet with the list of links to API-docs.
44 */
45function getLinkToApiHtml(requires) {
46 var lis = requires.map(function(symb) {
47 var href = '../apidoc/' + symb + '.html';
48 return '<li><a href="' + href + '" title="API documentation for ' +
49 symb + '">' + symb + '</a></li>';
50 });
51 return '<ul class="inline">' + lis.join() + '</ul>';
52}
53
54/**
55 * A Metalsmith plugin that adds metadata to the example HTML files. For each
56 * example HTML file, this adds metadata for related js and css resources. When
57 * these files are run through the example template, the extra metadata is used
58 * to show the complete example source in the textarea and submit the parts to
59 * CodePen.
60 *
61 * @param {Object} files The file lookup provided by Metalsmith. Property names
62 * are file paths relative to the source directory. The file objects
63 * include any existing metadata (e.g. from YAML front-matter), the file
64 * contents, and stats.
65 * @param {Object} metalsmith The metalsmith instance the plugin is being used
66 * with.
67 * @param {function(Error)} done Called when done (with any error).
68 */
69function augmentExamples(files, metalsmith, done) {
70 setImmediate(done); // all remaining code is synchronous
71 for (var filename in files) {
72 var file = files[filename];
73 var match = filename.match(markupRegEx);
74 if (match && filename !== 'index.html') {
75 if (!file.layout) {
76 throw new Error(filename + ': Missing "layout" in YAML front-matter');
77 }
78 var id = match[1];
79
80 // add js tag and source
81 var jsFilename = id + '.js';
82 if (!(jsFilename in files)) {
83 throw new Error('No .js file found for ' + filename);
84 }
85 var jsSource = files[jsFilename].contents.toString()
86 // Change data paths to absolute urls
87 .replace(/'data\//g, '\'https://openlayers.org/en/v' + pkg.version + '/examples/data/');
88 if (file.cloak) {
89 for (var key in file.cloak) {
90 jsSource = jsSource.replace(new RegExp(key, 'g'), file.cloak[key]);
91 }
92 }
93 var requires = getRequires(jsSource);
94 file.requires = requires;
95 file.js = {
96 tag: '<script src="loader.js?id=' + id + '"></script>',
97 source: jsSource.replace(cleanupJSRegEx, ''),
98 apiHtml: getLinkToApiHtml(requires)
99 };
100
101 // add css tag and source
102 var cssFilename = id + '.css';
103 if (cssFilename in files) {
104 file.css = {
105 tag: '<link rel="stylesheet" href="' + cssFilename + '">',
106 source: files[cssFilename].contents.toString()
107 };
108 }
109
110 // add additional resources
111 if (file.resources) {
112 var resources = [];
113 var remoteResources = [];
114 var codePenResources = [];
115 for (var i = 0, ii = file.resources.length; i < ii; ++i) {
116 var resource = file.resources[i];
117 var remoteResource = resource.indexOf('//') === -1 ?
118 'https://openlayers.org/en/v' + pkg.version + '/examples/' +
119 resource : resource;
120 codePenResources[i] = remoteResource;
121 if (isJsRegEx.test(resource)) {
122 resources[i] = '<script src="' + resource + '"></script>';
123 remoteResources[i] = '<script src="' + remoteResource +
124 '"></script>';
125 } else if (isCssRegEx.test(resource)) {
126 if (resource.indexOf('bootstrap.min.css') === -1) {
127 resources[i] = '<link rel="stylesheet" href="' + resource + '">';
128 }
129 remoteResources[i] = '<link rel="stylesheet" href="' +
130 remoteResource + '">';
131 } else {
132 throw new Error('Invalid value for resource: ' +
133 resource + ' is not .js or .css: ' + filename);
134 }
135 }
136 file.extraHead = {
137 local: resources.join('\n'),
138 remote: remoteResources.join('\n')
139 };
140 file.extraResources = file.resources.length ?
141 ',' + codePenResources.join(',') : '';
142 }
143 }
144 }
145}
146
147/**
148 * Create an inverted index of keywords from examples. Property names are
149 * lowercased words. Property values are objects mapping example index to word
150 * count.
151 * @param {Array.<Object>} exampleInfos Array of example info objects.
152 * @return {Object} Word index.
153 */
154function createWordIndex(exampleInfos) {
155 var index = {};
156 var keys = ['shortdesc', 'title', 'tags', 'requires'];
157 exampleInfos.forEach(function(info, i) {
158 keys.forEach(function(key) {
159 var text = info[key];
160 if (Array.isArray(text)) {
161 text = text.join(' ');
162 }
163 var words = text ? text.split(/\W+/) : [];
164 words.forEach(function(word) {
165 if (word) {
166 word = word.toLowerCase();
167 var counts = index[word];
168 if (counts) {
169 if (index in counts) {
170 counts[i] += 1;
171 } else {
172 counts[i] = 1;
173 }
174 } else {
175 counts = {};
176 counts[i] = 1;
177 index[word] = counts;
178 }
179 }
180 });
181 });
182 });
183 return index;
184}
185
186/**
187 * A plugin that generates the example index.js file. This file includes a
188 * list of example metadata objects and a word index used when searching for
189 * examples.
190 * @param {Object} files The file lookup provided by Metalsmith. Property names
191 * are file paths relative to the source directory. The file objects
192 * include any existing metadata (e.g. from YAML front-matter), the file
193 * contents, and stats.
194 * @param {Object} metalsmith The metalsmith instance the plugin is being used
195 * with.
196 * @param {function(Error)} done Called when done (with any error).
197 */
198function createIndex(files, metalsmith, done) {
199 setImmediate(done); // all remaining code is synchronous
200 var exampleInfos = [];
201 for (var filename in files) {
202 var example = files[filename];
203 if (markupRegEx.test(filename) && filename !== 'index.html') {
204 exampleInfos.push({
205 link: filename,
206 example: filename,
207 title: example.title,
208 shortdesc: example.shortdesc,
209 tags: example.tags,
210 requires: example.requires
211 });
212 }
213 }
214 var info = {
215 examples: exampleInfos,
216 index: createWordIndex(exampleInfos)
217 };
218 files['index.js'] = {
219 contents: new Buffer('var info = ' + JSON.stringify(info)),
220 mode: '0644'
221 };
222}
223
224function main(callback) {
225 var smith = new Metalsmith('.')
226 .source(srcDir)
227 .destination(destDir)
228 .concurrency(25)
229 .metadata({
230 olVersion: pkg.version
231 })
232 .use(augmentExamples)
233 .use(createIndex)
234 .use(templates({
235 engine: 'handlebars',
236 directory: templatesDir,
237 helpers: {
238 md: function(str) {
239 return new handlebars.SafeString(marked(str));
240 },
241 indent: function(text, options) {
242 if (!text) {
243 return text;
244 }
245 var count = options.hash.spaces || 2;
246 var spaces = new Array(count + 1).join(' ');
247 return text.split('\n').map(function(line) {
248 return line ? spaces + line : '';
249 }).join('\n');
250 }
251 }
252 }))
253 .build(function(err) {
254 callback(err);
255 });
256 return smith;
257}
258
259if (require.main === module) {
260 main(function(err) {
261 if (err) {
262 process.stderr.write(
263 'Building examples failed. See the full trace below.\n\n' +
264 err.stack + '\n');
265 process.exit(1);
266 } else {
267 process.exit(0);
268 }
269 });
270}
271
272module.exports = main;