diff --git a/examples/readme.md b/examples/readme.md
index a32f178dbb..9a1b4c49e9 100644
--- a/examples/readme.md
+++ b/examples/readme.md
@@ -1,8 +1,8 @@
# Code examples
-The `.html` files in this folder are built by applying the templates in the `config/examples/` folder. Examples have [YAML front-matter](http://www.metalsmith.io) headers with the following properties:
+The `.html` files in this folder are built by applying the templates in the `templates` folder. Examples have [YAML front-matter](http://www.metalsmith.io) headers with the following properties:
-* layout: The template from the `config/examples/` directory to use for this example
+* layout: The template from the `templates` directory to use for this example
* title: The title of the example
* shortdesc: A short description for the example index
* docs: Documentation of the example. Can be markdown.
diff --git a/config/examples/example-verbatim.html b/examples/templates/example-verbatim.html
similarity index 100%
rename from config/examples/example-verbatim.html
rename to examples/templates/example-verbatim.html
diff --git a/config/examples/example.html b/examples/templates/example.html
similarity index 96%
rename from config/examples/example.html
rename to examples/templates/example.html
index 16b86ba499..92e80cdb0b 100644
--- a/config/examples/example.html
+++ b/examples/templates/example.html
@@ -52,7 +52,6 @@
{{ shortdesc }}
{{ md docs }}
-
Related API documentation: {{{ js.apiHtml }}}
@@ -105,7 +104,7 @@
var branchSearch = url.match(/\/([^\/]*)\/examples\//);
var cookieText = 'dismissed=-' + latestVersion + '-';
var dismissed = document.cookie.indexOf(cookieText) != -1;
- if (!dismissed && /^v[0-9\.]*$/.test(branchSearch[1]) && '{{ olVersion }}' != latestVersion) {
+ if (branchSearch && !dismissed && /^v[0-9\.]*$/.test(branchSearch[1]) && '{{ olVersion }}' != latestVersion) {
var link = url.replace(branchSearch[0], '/latest/examples/');
fetch(link, {method: 'head'}).then(function(response) {
var a = document.getElementById('latest-link');
diff --git a/config/examples/readme.md b/examples/templates/readme.md
similarity index 100%
rename from config/examples/readme.md
rename to examples/templates/readme.md
diff --git a/examples/webpack/.eslintrc b/examples/webpack/.eslintrc
new file mode 100644
index 0000000000..92ab127997
--- /dev/null
+++ b/examples/webpack/.eslintrc
@@ -0,0 +1,16 @@
+{
+ "env": {
+ "node": true,
+ "es6": true
+ },
+ "parserOptions": {
+ "ecmaVersion": 2017
+ },
+ "rules": {
+ "space-before-function-paren": ["error", {
+ "anonymous": "never",
+ "named": "never",
+ "asyncArrow": "always"
+ }]
+ }
+}
diff --git a/examples/webpack/config.js b/examples/webpack/config.js
new file mode 100644
index 0000000000..dddddd3c76
--- /dev/null
+++ b/examples/webpack/config.js
@@ -0,0 +1,43 @@
+const CopyPlugin = require('copy-webpack-plugin');
+const ExampleBuilder = require('./example-builder');
+const fs = require('fs');
+const path = require('path');
+const webpack = require('webpack');
+
+const src = path.join(__dirname, '..');
+
+const examples = fs.readdirSync(src)
+ .filter(name => /^(?!index).*\.html$/.test(name))
+ .map(name => name.replace(/\.html$/, ''));
+
+const entry = {};
+examples.forEach(example => {
+ entry[example] = `./${example}.js`;
+});
+
+module.exports = {
+ context: src,
+ target: 'web',
+ entry: entry,
+ plugins: [
+ new webpack.optimize.CommonsChunkPlugin({
+ name: 'common',
+ minChunks: 2
+ }),
+ new ExampleBuilder({
+ templates: path.join(__dirname, '..', 'templates'),
+ common: 'common'
+ }),
+ new CopyPlugin([
+ {from: '../css', to: 'css'},
+ {from: 'data', to: 'data'},
+ {from: 'resources', to: 'resources'},
+ {from: 'Jugl.js', to: 'Jugl.js'},
+ {from: 'index.html', to: 'index.html'}
+ ])
+ ],
+ output: {
+ filename: '[name].js',
+ path: path.join(__dirname, '..', '..', 'build', 'examples')
+ }
+};
diff --git a/examples/webpack/example-builder.js b/examples/webpack/example-builder.js
new file mode 100644
index 0000000000..5932b7a011
--- /dev/null
+++ b/examples/webpack/example-builder.js
@@ -0,0 +1,211 @@
+const frontMatter = require('front-matter');
+const fs = require('fs');
+const handlebars = require('handlebars');
+const marked = require('marked');
+const path = require('path');
+const pkg = require('../../package.json');
+const promisify = require('util').promisify;
+
+const readFile = promisify(fs.readFile);
+const isCssRegEx = /\.css$/;
+const isJsRegEx = /\.js(\?.*)?$/;
+
+handlebars.registerHelper('md', str => new handlebars.SafeString(marked(str)));
+
+handlebars.registerHelper('indent', (text, options) => {
+ if (!text) {
+ return text;
+ }
+ const count = options.hash.spaces || 2;
+ const spaces = new Array(count + 1).join(' ');
+ return text.split('\n').map(line => line ? spaces + line : '').join('\n');
+});
+
+/**
+ * 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.} exampleData Array of example data objects.
+ * @return {Object} Word index.
+ */
+function createWordIndex(exampleData) {
+ const index = {};
+ const keys = ['shortdesc', 'title', 'tags'];
+ exampleData.forEach((data, i) => {
+ keys.forEach(key => {
+ let text = data[key];
+ if (Array.isArray(text)) {
+ text = text.join(' ');
+ }
+ let words = text ? text.split(/\W+/) : [];
+ words.forEach(word => {
+ if (word) {
+ word = word.toLowerCase();
+ let 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 webpack plugin that builds the html files for our examples.
+ * @param {Object} config Plugin configuration. Requires a `templates` property
+ * with the path to templates and a `common` property with the name of the
+ * common chunk.
+ * @constructor
+ */
+function ExampleBuilder(config) {
+ this.templates = config.templates;
+ this.common = config.common;
+}
+
+/**
+ * Called by webpack.
+ * @param {Object} compiler The webpack compiler.
+ */
+ExampleBuilder.prototype.apply = function(compiler) {
+ compiler.plugin('emit', async (compilation, callback) => {
+ const chunks = compilation.getStats().toJson().chunks
+ .filter(chunk => chunk.names[0] !== this.common);
+
+ const exampleData = [];
+ const promises = chunks.map(async chunk => {
+ const [assets, data] = await this.render(compiler.context, chunk);
+
+ exampleData.push({
+ link: data.filename,
+ example: data.filename,
+ title: data.title,
+ shortdesc: data.shortdesc,
+ tags: data.tags
+ });
+
+ for (const file in assets) {
+ compilation.assets[file] = {
+ source: () => assets[file],
+ size: () => assets[file].length
+ };
+ }
+ });
+
+ try {
+ await Promise.all(promises);
+ } catch (err) {
+ callback(err);
+ return;
+ }
+
+ const info = {
+ examples: exampleData,
+ index: createWordIndex(exampleData)
+ };
+
+ const indexSource = `var info = ${JSON.stringify(info)}`;
+ compilation.assets['index.js'] = {
+ source: () => indexSource,
+ size: () => indexSource.length
+ };
+
+ callback();
+ });
+};
+
+ExampleBuilder.prototype.render = async function(dir, chunk) {
+ const name = chunk.names[0];
+
+ const assets = {};
+ const readOptions = {encoding: 'utf8'};
+
+ const htmlName = `${name}.html`;
+ const htmlPath = path.join(dir, htmlName);
+ const htmlSource = await readFile(htmlPath, readOptions);
+
+ const {attributes, body} = frontMatter(htmlSource);
+ const data = Object.assign(attributes, {contents: body});
+
+ data.olVersion = pkg.version;
+ data.filename = htmlName;
+
+ // add in script tag
+ const jsName = `${name}.js`;
+ let jsSource = chunk.modules[0].source;
+ if (data.cloak) {
+ for (const key in data.cloak) {
+ jsSource = jsSource.replace(new RegExp(key, 'g'), data.cloak[key]);
+ }
+ }
+ data.js = {
+ tag: ``,
+ source: jsSource
+ };
+
+ // check for example css
+ const cssName = `${name}.css`;
+ const cssPath = path.join(dir, cssName);
+ let cssSource;
+ try {
+ cssSource = await readFile(cssPath, readOptions);
+ } catch (err) {
+ // pass
+ }
+ if (cssSource) {
+ data.css = {
+ tag: ` `,
+ source: cssSource
+ };
+ assets[cssName] = cssSource;
+ }
+
+ // add additional resources
+ if (data.resources) {
+ const resources = [];
+ const remoteResources = [];
+ const codePenResources = [];
+ for (let i = 0, ii = data.resources.length; i < ii; ++i) {
+ const resource = data.resources[i];
+ const remoteResource = resource.indexOf('//') === -1 ?
+ `https://openlayers.org/en/v${pkg.version}/examples/${resource}` : resource;
+ codePenResources[i] = remoteResource;
+ if (isJsRegEx.test(resource)) {
+ resources[i] = ``;
+ remoteResources[i] = ``;
+ } else if (isCssRegEx.test(resource)) {
+ if (resource.indexOf('bootstrap.min.css') === -1) {
+ resources[i] = ' ';
+ }
+ remoteResources[i] = ' ';
+ } else {
+ throw new Error('Invalid value for resource: ' +
+ resource + ' is not .js or .css: ' + htmlName);
+ }
+ }
+ data.extraHead = {
+ local: resources.join('\n'),
+ remote: remoteResources.join('\n')
+ };
+ data.extraResources = data.resources.length ?
+ ',' + codePenResources.join(',') : '';
+ }
+
+ const templatePath = path.join(this.templates, attributes.layout);
+ const templateSource = await readFile(templatePath, readOptions);
+
+ assets[htmlName] = handlebars.compile(templateSource)(data);
+ return [assets, data];
+};
+
+module.exports = ExampleBuilder;
diff --git a/package.json b/package.json
index 27ed53fcd1..dba538b3c2 100644
--- a/package.json
+++ b/package.json
@@ -22,7 +22,8 @@
"changecase-src": "node tasks/filename-case-from-module.js",
"transform-examples": "jscodeshift --transform transforms/module.js examples",
"transform-test": "jscodeshift --transform transforms/module.js test",
- "transform": "npm run changecase-src && npm run transform-src && npm run transform-examples && npm run transform-test && npm run lint -- --fix"
+ "transform": "npm run changecase-src && npm run transform-src && npm run transform-examples && npm run transform-test && npm run lint -- --fix",
+ "build-examples": "webpack --config examples/webpack/config.js"
},
"main": "dist/ol.js",
"repository": {
@@ -58,12 +59,15 @@
"coveralls": "3.0.0",
"debounce": "^1.1.0",
"eslint": "4.13.1",
+ "copy-webpack-plugin": "^4.0.1",
"eslint-config-openlayers": "7.0.0",
"eslint-plugin-openlayers-internal": "^3.1.0",
"expect.js": "0.3.1",
+ "front-matter": "^2.1.2",
"gaze": "^1.0.0",
"glob": "7.1.1",
"handlebars": "4.0.11",
+ "html-webpack-plugin": "^2.30.1",
"istanbul": "0.4.5",
"jquery": "3.2.1",
"jscodeshift": "^0.4.0",
@@ -85,7 +89,9 @@
"serve-files": "1.0.1",
"sinon": "4.1.3",
"slimerjs": "0.10.3",
- "url-polyfill": "^1.0.7"
+ "url-polyfill": "^1.0.7",
+ "webpack": "^3.5.5",
+ "webpack-dev-server": "^2.7.1"
},
"eslintConfig": {
"extends": "openlayers",
@@ -138,8 +144,7 @@
]
}
},
- "ext": [
- {
+ "ext": [{
"module": "rbush"
},
{