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 }}
-
@@ -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" }, {