Merge pull request #3542 from tschaub/examples

Generate example index and rebuild examples on source changes.
This commit is contained in:
Tim Schaub
2015-04-13 06:13:12 -06:00
11 changed files with 167 additions and 2274 deletions

View File

@@ -1,5 +1,2 @@
*.pyc
/build/
/examples/*.html.png
/examples/example-list.js
/examples/example-list.xml

File diff suppressed because it is too large Load Diff

View File

@@ -1,269 +0,0 @@
#!/usr/bin/env python
import sys
import os
import re
import time
from xml.dom.minidom import Document
try:
import xml.etree.ElementTree as ElementTree
except ImportError:
try:
import cElementTree as ElementTree # NOQA
except ImportError:
try:
import elementtree.ElementTree as ElementTree # NOQA
except ImportError:
import lxml.etree as ElementTree # NOQA
missing_deps = False
try:
import json
except ImportError:
try:
import simplejson as json # NOQA
except ImportError, E:
missing_deps = E
try:
from BeautifulSoup import BeautifulSoup
except ImportError, E:
missing_deps = E
feedName = "example-list.xml"
feedPath = "http://openlayers.github.io/ol3/master/examples/"
def getListOfExamples(relPath):
"""
returns list of .html filenames within a given path - excludes
index.html
"""
examples = os.listdir(relPath)
examples = [example for example in examples if
example.endswith('.html') and example != "index.html"]
return examples
def getExampleHtml(path):
"""
returns html of a specific example
"""
print '.',
f = open(path)
html = f.read()
f.close()
return html
def extractById(soup, tagId, value=None):
"""
returns full contents of a particular tag id
"""
beautifulTag = soup.find(id=tagId)
if beautifulTag:
if beautifulTag.contents:
value = str(beautifulTag.renderContents()).strip()
value = value.replace('\t', '')
value = value.replace('\n', '')
return value
def getRelatedClasses(html):
"""
parses the html, and returns a list of all OpenLayers Classes
used within (ie what parts of OL the javascript uses).
"""
rawstr = r'''(?P<class>ol\..*?)\('''
return re.findall(rawstr, html)
def parseHtml(html, ids):
"""
returns dictionary of items of interest
"""
soup = BeautifulSoup(html)
d = {}
for tagId in ids:
d[tagId] = extractById(soup, tagId)
#classes should eventually be parsed from docs - not automatically created.
classes = getRelatedClasses(html)
d['classes'] = classes
return d
def getGitInfo(exampleDir, exampleName):
orig = os.getcwd()
os.chdir(exampleDir)
h = os.popen("git log -n 1 --pretty=format:'%an|%ai' " + exampleName)
os.chdir(orig)
log = h.read()
h.close()
d = {}
if log:
parts = log.split("|")
d["author"] = parts[0]
# compensate for spaces in git log time
td = parts[1].split(" ")
td.insert(1, "T")
d["date"] = "".join(td)
else:
d["author"] = ""
d["date"] = ""
return d
def createFeed(examples):
doc = Document()
atomuri = "http://www.w3.org/2005/Atom"
feed = doc.createElementNS(atomuri, "feed")
feed.setAttribute("xmlns", atomuri)
title = doc.createElementNS(atomuri, "title")
title.appendChild(doc.createTextNode("OpenLayers Examples"))
feed.appendChild(title)
link = doc.createElementNS(atomuri, "link")
link.setAttribute("rel", "self")
link.setAttribute("href", feedPath + feedName)
modtime = time.strftime("%Y-%m-%dT%I:%M:%SZ", time.gmtime())
id = doc.createElementNS(atomuri, "id")
id.appendChild(doc.createTextNode(
"%s%s#%s" % (feedPath, feedName, modtime)))
feed.appendChild(id)
updated = doc.createElementNS(atomuri, "updated")
updated.appendChild(doc.createTextNode(modtime))
feed.appendChild(updated)
examples.sort(key=lambda x: x["modified"])
for example in sorted(examples, key=lambda x: x["modified"], reverse=True):
entry = doc.createElementNS(atomuri, "entry")
title = doc.createElementNS(atomuri, "title")
title.appendChild(doc.createTextNode(example["title"] or
example["example"]))
entry.appendChild(title)
tags = doc.createElementNS(atomuri, "tags")
tags.appendChild(doc.createTextNode(example["tags"] or
example["example"]))
entry.appendChild(tags)
link = doc.createElementNS(atomuri, "link")
link.setAttribute("href", "%s%s" % (feedPath, example["example"]))
entry.appendChild(link)
summary = doc.createElementNS(atomuri, "summary")
summary.appendChild(doc.createTextNode(example["shortdesc"] or
example["example"]))
entry.appendChild(summary)
updated = doc.createElementNS(atomuri, "updated")
updated.appendChild(doc.createTextNode(example["modified"]))
entry.appendChild(updated)
author = doc.createElementNS(atomuri, "author")
name = doc.createElementNS(atomuri, "name")
name.appendChild(doc.createTextNode(example["author"]))
author.appendChild(name)
entry.appendChild(author)
id = doc.createElementNS(atomuri, "id")
id.appendChild(doc.createTextNode("%s%s#%s" % (feedPath,
example["example"],
example["modified"])))
entry.appendChild(id)
feed.appendChild(entry)
doc.appendChild(feed)
return doc
def wordIndex(examples):
"""
Create an inverted index based on words in title and shortdesc. Keys are
lower cased words. Values are dictionaries with example index keys and
count values.
"""
index = {}
unword = re.compile("\\W+")
keys = ["shortdesc", "title", "tags"]
for i in range(len(examples)):
for key in keys:
text = examples[i][key]
if text:
words = unword.split(text)
for word in words:
if word:
word = word.lower()
if word in index:
if i in index[word]:
index[word][i] += 1
else:
index[word][i] = 1
else:
index[word] = {i: 1}
return index
if __name__ == "__main__":
if missing_deps:
print """This script requires json or simplejson and BeautifulSoup.
You don't have them. \n(%s)""" % E
sys.exit()
if len(sys.argv) == 3:
inExampleDir = sys.argv[1]
outExampleDir = sys.argv[2]
else:
inExampleDir = "../examples"
outExampleDir = "../examples"
outFile = open(os.path.join(outExampleDir, "example-list.js"), "w")
print 'Reading examples from %s and writing out to %s' % (inExampleDir,
outFile.name)
exampleList = []
docIds = ['title', 'shortdesc', 'tags']
examples = getListOfExamples(inExampleDir)
modtime = time.strftime("%Y-%m-%dT%I:%M:%SZ", time.gmtime())
for example in examples:
path = os.path.join(inExampleDir, example)
html = getExampleHtml(path)
tagvalues = parseHtml(html, docIds)
tagvalues['example'] = example
# add in author/date info
d = getGitInfo(inExampleDir, example)
tagvalues["author"] = d["author"] or "anonymous"
tagvalues["modified"] = d["date"] or modtime
tagvalues['link'] = example
exampleList.append(tagvalues)
print
exampleList.sort(key=lambda x: x['example'].lower())
index = wordIndex(exampleList)
json = json.dumps({"examples": exampleList, "index": index})
# Give the json a global variable we can use in our js.
# This should be replaced or made optional.
json = 'var info=' + json + ';'
outFile.write(json)
outFile.close()
outFeedPath = os.path.join(outExampleDir, feedName)
print "writing feed to %s " % outFeedPath
atom = open(outFeedPath, 'w')
doc = createFeed(exampleList)
atom.write(doc.toxml())
atom.close()
print 'complete'

View File

@@ -112,23 +112,24 @@ EXECUTABLES = [variables.CLEANCSS, variables.GIT, variables.GJSLINT,
variables.JSDOC, variables.JSHINT, variables.PYTHON,
variables.PHANTOMJS]
EXAMPLES = [path
for path in ifind('examples')
if path.endswith('.html')
if path != 'examples/index.html']
EXAMPLES_SRC_ALL = [path for path in ifind('examples_src')]
EXAMPLES_SRC = [path
for path in ifind('examples')
if path.endswith('.js')
if not path.endswith('.combined.js')
if path != 'examples/Jugl.js'
if path != 'examples/example-list.js']
EXAMPLES_SRC_HTML = [path
for path in EXAMPLES_SRC_ALL
if path.endswith('.html')
if path != 'examples_src/index.html']
EXAMPLES_SRC_JS = [example.replace('.html', '.js')
for example in EXAMPLES_SRC_HTML]
EXAMPLES_DEST_ALL = [path.replace('examples_src', 'examples')
for path in EXAMPLES_SRC_ALL]
EXAMPLES_JSON = ['build/' + example.replace('.html', '.json')
for example in EXAMPLES]
for example in EXAMPLES_SRC_HTML]
EXAMPLES_COMBINED = ['build/' + example.replace('.html', '.combined.js')
for example in EXAMPLES]
for example in EXAMPLES_SRC_HTML]
GLSL_SRC = [path
for path in ifind('src')
@@ -261,17 +262,15 @@ virtual('build-examples', 'examples', 'build/examples/all.combined.js',
EXAMPLES_COMBINED)
virtual('examples', 'examples/example-list.xml', EXAMPLES_JSON)
virtual('examples', EXAMPLES_DEST_ALL)
@target('examples/example-list.xml', 'examples/example-list.js')
def examples_examples_list_xml(t):
t.touch() # already generated by bin/exampleparser.py
@target('examples/example-list.js', 'bin/exampleparser.py', EXAMPLES)
def examples_examples_list_js(t):
t.run('%(PYTHON)s', 'bin/exampleparser.py', 'examples', 'examples')
@rule(r'\Aexamples/(?P<filepath>.*)\Z')
def examples_dest(name, match):
def action(t):
t.run('node', 'tasks/build-examples.js')
dependencies = ['examples_src/%(filepath)s' % match.groupdict()]
return Target(name, action=action, dependencies=dependencies)
@target('build/examples/all.combined.js', 'build/examples/all.js',
@@ -282,7 +281,7 @@ def build_examples_all_combined_js(t):
report_sizes(t)
@target('build/examples/all.js', EXAMPLES_SRC)
@target('build/examples/all.js', EXAMPLES_SRC_JS)
def build_examples_all_js(t):
t.output('%(PYTHON)s', 'bin/combine-examples.py', t.dependencies)
@@ -405,7 +404,7 @@ def examples_star_combined_js(name, match):
return Target(name, action=action, dependencies=dependencies)
@target('serve', 'examples', 'build/test_requires.js', 'build/test_rendering_requires.js',
@target('serve', 'build/test_requires.js', 'build/test_rendering_requires.js',
NPM_INSTALL)
def serve(t):
t.run('node', 'tasks/serve.js')
@@ -415,7 +414,7 @@ virtual('lint', 'build/lint-timestamp', 'build/check-requires-timestamp',
'build/check-whitespace-timestamp', 'jshint')
@target('build/lint-timestamp', SRC, EXAMPLES_SRC, SPEC, SPEC_RENDERING,
@target('build/lint-timestamp', SRC, EXAMPLES_SRC_JS, SPEC, SPEC_RENDERING,
precious=True)
def build_lint_src_timestamp(t):
t.run('%(GJSLINT)s',
@@ -427,7 +426,7 @@ def build_lint_src_timestamp(t):
virtual('jshint', 'build/jshint-timestamp')
@target('build/jshint-timestamp', SRC, EXAMPLES_SRC, SPEC, SPEC_RENDERING,
@target('build/jshint-timestamp', SRC, EXAMPLES_SRC_JS, SPEC, SPEC_RENDERING,
TASKS, NPM_INSTALL, precious=True)
def build_jshint_timestamp(t):
t.run(variables.JSHINT, '--verbose', t.newer(t.dependencies))
@@ -457,7 +456,7 @@ def _strip_comments(lines):
yield lineno, line
@target('build/check-requires-timestamp', SRC, EXAMPLES_SRC, SHADER_SRC,
@target('build/check-requires-timestamp', SRC, EXAMPLES_SRC_JS, SHADER_SRC,
SPEC, SPEC_RENDERING)
def build_check_requires_timestamp(t):
unused_count = 0
@@ -598,7 +597,7 @@ def build_check_requires_timestamp(t):
t.touch()
@target('build/check-whitespace-timestamp', SRC, EXAMPLES_SRC,
@target('build/check-whitespace-timestamp', SRC, EXAMPLES_SRC_JS,
SPEC, SPEC_RENDERING, JSDOC_SRC, precious=True)
def build_check_whitespace_timestamp(t):
CR_RE = re.compile(r'\r')
@@ -691,23 +690,16 @@ def host_examples(t):
closure_lib_path = output('node', '-e',
'process.stdout.write(require("closure-util").getLibraryPath())')
t.rm_rf(examples_dir)
t.makedirs(examples_dir)
t.cp_r('examples', examples_dir)
for example in EXAMPLES_SRC_JS:
split_example_file(example, examples_dir % vars(variables))
t.cp('bin/loader_hosted_examples.js', examples_dir + '/loader.js')
t.rm_rf(build_dir)
t.makedirs(build_dir)
t.rm_rf(css_dir)
t.makedirs(css_dir)
t.cp(EXAMPLES, examples_dir)
for example in [path.replace('.html', '.js') for path in EXAMPLES]:
split_example_file(example, examples_dir % vars(variables))
for example in [path.replace('.html', '.css') for path in EXAMPLES]:
if os.path.isfile(example):
t.cp(example, examples_dir)
t.cp_r('examples/data', examples_dir + '/data')
t.cp('bin/loader_hosted_examples.js', examples_dir + '/loader.js')
t.cp('build/ol.js', 'build/ol-debug.js', build_dir)
t.cp('build/ol.css', css_dir)
t.cp('examples/index.html', 'examples/example-list.js',
'examples/example-list.xml', 'examples/Jugl.js', examples_dir)
t.rm_rf('build/hosted/%(BRANCH)s/closure-library')
t.cp_r(closure_lib_path, 'build/hosted/%(BRANCH)s/closure-library')
t.rm_rf('build/hosted/%(BRANCH)s/ol')
@@ -726,8 +718,8 @@ def host_examples(t):
@target('check-examples', 'host-examples', phony=True)
def check_examples(t):
examples = ['build/hosted/%(BRANCH)s/' + e
for e in EXAMPLES
examples = ['build/hosted/%(BRANCH)s/' + e.replace('examples_src', 'examples')
for e in EXAMPLES_SRC_HTML
if not open(e.replace('.html', '.js'), 'rU').readline().startswith('// NOCOMPILE')]
all_examples = [e + '?mode=advanced' for e in examples]
# Run the examples checks in a pool of threads

View File

@@ -48,7 +48,7 @@
}
</style>
<script type="text/javascript" src="Jugl.js"></script>
<script type="text/javascript" src="example-list.js"></script>
<script type="text/javascript" src="index.js"></script>
<script type="text/javascript">
var template, target;

View File

@@ -46,7 +46,9 @@
"devDependencies": {
"clean-css": "2.2.16",
"coveralls": "2.11.2",
"debounce": "^1.0.0",
"expect.js": "0.3.1",
"gaze": "^0.5.1",
"istanbul": "0.3.13",
"jquery": "2.1.1",
"jshint": "2.5.6",
@@ -54,9 +56,9 @@
"mocha-phantomjs": "3.5.1",
"phantomjs": "1.9.10",
"proj4": "2.3.3",
"resemblejs": "1.2.0",
"sinon": "1.10.3",
"slimerjs-edge": "0.10.0-pre-2",
"resemblejs": "1.2.0"
"slimerjs-edge": "0.10.0-pre-2"
},
"ext": [
"rbush"

View File

@@ -81,14 +81,88 @@ function augmentExamples(files, metalsmith, done) {
}
}
/**
* 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'];
exampleInfos.forEach(function(info, i) {
keys.forEach(function(key) {
var text = info[key];
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)) {
exampleInfos.push({
link: filename,
example: filename,
title: example.title,
shortdesc: example.shortdesc,
tags: example.tags
});
}
}
var info = {
examples: exampleInfos,
index: createWordIndex(exampleInfos)
};
files['index.js'] = {
contents: new Buffer('var info = ' + JSON.stringify(info)),
mode: '0644'
};
}
function main(callback) {
new Metalsmith('.')
var smith = new Metalsmith('.')
.source(srcDir)
.destination(destDir)
.metadata({
olVersion: pkg.version
})
.use(augmentExamples)
.use(createIndex)
.use(templates({
engine: 'handlebars',
directory: templatesDir,
@@ -101,12 +175,15 @@ function main(callback) {
.build(function(err) {
callback(err);
});
return smith;
}
if (require.main === module) {
main(function(err) {
if (err) {
process.stderr.write(err.message + '\n');
process.stderr.write(
'Building examples failed. See the full trace below.\n\n' +
err.stack + '\n');
process.exit(1);
} else {
process.exit(0);

View File

@@ -1,17 +1,12 @@
var async = require('async');
var buildExt = require('./build-ext');
var parseExamples = require('./parse-examples');
/**
* Parse examples and build external modules.
*/
async.waterfall([
parseExamples,
buildExt
], function(err) {
buildExt(function(err) {
if (err) {
process.stderr.write(err + '\n');
process.exit(1);
} else {
process.exit(0);
}
});

View File

@@ -1,163 +0,0 @@
var fs = require('fs');
var path = require('path');
var async = require('async');
var Parser = require('htmlparser2').Parser;
var buildExamples = require('./build-examples');
var exampleDir = path.join(__dirname, '..', 'examples');
/**
* List all .html files in the example directory (excluding index.html).
* @param {function(Error, Array.<string>)} callback Called with any error or
* the list of paths to examples.
*/
function listExamples(callback) {
fs.readdir(exampleDir, function(err, items) {
if (err) {
return callback(err);
}
var examplePaths = items.filter(function(item) {
return /\.html$/i.test(item) && item !== 'index.html';
}).map(function(item) {
return path.join(exampleDir, item);
});
callback(null, examplePaths);
});
}
/**
* Parse info from examples.
* @param {Array.<string>} examplePaths Paths to examples.
* @param {function(Error, Array.<Object>)} callback Called with any error or
* the list of example info objects.
*/
function parseExamples(examplePaths, callback) {
async.map(examplePaths, function(examplePath, next) {
fs.readFile(examplePath, function(err, data) {
if (err) {
return next(err);
}
var name = path.basename(examplePath);
var info = {
link: name,
example: name,
title: '',
shortdesc: '',
tags: ''
};
var key;
var openTag;
var parser = new Parser({
onopentag: function(tag, attrs) {
if (attrs.id in info) {
key = attrs.id;
openTag = tag;
}
},
ontext: function(text) {
if (key) {
info[key] += text.replace(/\n/g, '').trim() + ' ';
}
},
onclosetag: function(tag) {
if (tag === openTag) {
info[key] = info[key].trim();
key = undefined;
openTag = undefined;
}
},
onerror: function(err2) {
var message = 'Trouble parsing ' + examplePath + '\n' + err2.message;
next(new Error(message));
}
});
parser.write(data.toString('utf8'));
parser.end();
next(null, info);
});
}, callback);
}
/**
* 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'];
exampleInfos.forEach(function(info, i) {
keys.forEach(function(key) {
var text = info[key];
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;
}
/**
* Write the example-list.js file with example info and word index.
* @param {Array.<Object>} exampleInfos Array of example info objects.
* @param {function(Error)} callback Called with any error.
*/
function writeExampleList(exampleInfos, callback) {
var info = {
examples: exampleInfos,
index: createWordIndex(exampleInfos)
};
var indexPath = path.join(exampleDir, 'example-list.js');
var str = 'var info = ' + JSON.stringify(info);
fs.writeFile(indexPath, str, callback);
}
/**
* List examples, parse them, and write example list.
* @param {function(Error)} callback Called with any error.
*/
function main(callback) {
async.waterfall([
buildExamples,
listExamples,
parseExamples,
writeExampleList
], callback);
}
if (require.main === module) {
main(function(err) {
if (err) {
process.stderr.write(err.message + '\n');
process.exit(1);
} else {
process.exit(0);
}
});
}
module.exports = main;

View File

@@ -107,13 +107,10 @@ This is useful for projects that use the Closure Compiler to build, but want to
Called internally to parse the library for annotations and write out a `build/info.json` file.
## `parse-examples.js`
Called after install to generate an example index. After new examples are added, run `node tasks/parse-examples.js` to regenerate the example index.
## `build-examples.js`
Called internally by `parse-examples.js` to build the examples from templates.
Builds examples and the example index.
## `serve.js`

View File

@@ -7,9 +7,12 @@
var path = require('path');
var url = require('url');
var Gaze = require('gaze').Gaze;
var closure = require('closure-util');
var debounce = require('debounce');
var nomnom = require('nomnom');
var buildExamples = require('./build-examples');
var log = closure.log;
@@ -68,6 +71,26 @@ var createServer = exports.createServer = function(callback) {
});
};
/**
* Build the examples and exit on any error.
* @param {Function=} opt_callback Called when done building examples.
*/
function buildExamplesOrFatal(opt_callback) {
log.info('serve', 'Building examples.');
buildExamples(function(err) {
if (err) {
log.error('serve', 'Building examples failed.');
log.error('serve', err.message);
log.error('serve', 'Use "verbose" logging to see the full stack trace.');
log.verbose('serve', err.stack);
process.exit(1);
}
log.verbose('serve', 'Done building examples.');
if (opt_callback) {
opt_callback();
}
});
}
/**
* If running this module directly start the server.
@@ -92,21 +115,30 @@ if (require.main === module) {
/** @type {string} */
log.level = options.loglevel;
log.info('serve', 'Parsing dependencies ...');
createServer(function(err, server) {
if (err) {
log.error('serve', 'Parsing failed');
log.error('serve', err.message);
process.exit(1);
}
server.listen(options.port, function() {
log.info('serve', 'Listening on http://localhost:' +
options.port + '/ (Ctrl+C to stop)');
});
server.on('error', function(err) {
log.error('serve', 'Server failed to start: ' + err.message);
process.exit(1);
buildExamplesOrFatal(function() {
log.info('serve', 'Parsing dependencies.');
createServer(function(err, server) {
if (err) {
log.error('serve', 'Parsing failed');
log.error('serve', err.message);
process.exit(1);
}
server.listen(options.port, function() {
log.info('serve', 'Listening on http://localhost:' +
options.port + '/ (Ctrl+C to stop)');
});
server.on('error', function(err) {
log.error('serve', 'Server failed to start: ' + err.message);
process.exit(1);
});
});
var gaze = new Gaze('examples_src/**/*');
var debouncedBuild = debounce(buildExamplesOrFatal, 250);
gaze.on('all', function(event, filepath) {
log.verbose('serve', 'Watch event: ' + event + ' ' + filepath);
debouncedBuild();
});
});
}