diff --git a/.gitignore b/.gitignore index 66be20f..0173852 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ +bower_components node_modules coverage -vendor/closure-compiler* diff --git a/.jshintrc b/.jshintrc index 96bf7e7..ffcbf50 100644 --- a/.jshintrc +++ b/.jshintrc @@ -63,5 +63,13 @@ "-W053": true, "-W086": true, - "predef": ["define", "environment", "Packages", "Benchmark", "_", "phantom"] -} \ No newline at end of file + "globals": { + "define": false, + "environment": false, + "Packages": false, + "phantom": false, + + "Benchmark": true, + "_": true + } +} diff --git a/.travis.yml b/.travis.yml index 224d55a..e2b11c4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -43,6 +43,8 @@ before_install: - "[ $BIN == 'ringo' ] && sudo ln -s /opt/ringojs-0.9/bin/ringo /usr/local/bin/ringo && sudo chmod +x /usr/local/bin/ringo || true" install: - "npm install" + - "npm install -g bower" + - "bower install" script: - "node build.js" - "cd ./test" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0975f8b..2b5ecf9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ Please use the [GitHub issue tracker](https://github.com/bestiejs/json3/issues) ## Pull Requests ## -If you'd like to contribute a feature or bug fix, you can [fork](http://help.github.com/fork-a-repo/) JSON 3, commit your changes, and [send a pull request](http://help.github.com/send-pull-requests/) **against the `dev` branch**. Please make sure to update the unit tests in the `test` directory as well. +If you'd like to contribute a feature or bug fix, you can [fork](http://help.github.com/fork-a-repo/) JSON 3, commit your changes, and [send a pull request](http://help.github.com/send-pull-requests/) **against the `master` branch**. Please make sure to update the unit tests in the `test` directory as well. **Please do not send pull requests against `gh-pages`**; this branch is reserved for releases, builder updates, and project page changes. @@ -82,12 +82,16 @@ To add a test: t.done(15); }); -### Project Page and Minification ### +### Builds ### The builder (`build.js`) is a Node script that: -* Regenerates the [GitHub project page](https://bestiejs.github.io/json3/) from `README.md`... -* Downloads the [Closure Compiler](https://developers.google.com/closure/compiler/) if it's not already on your system, and... -* Minifies `lib/json3.js`. +* Regenerates the [GitHub project pages](https://bestiejs.github.io/json3/)... +* Builds `lib/json3.js` using [webpack](https://webpack.github.io/), and... +* Minifies `lib/json3.js` using the [Closure Compiler](https://developers.google.com/closure/compiler/). -You don't need to run the builder before submitting your pull request; we'll do this before each release. +You'll need [Node](https://nodejs.org/) (or [io.js](https://iojs.org)), [npm](https://docs.npmjs.com/getting-started/installing-node), and [Bower](http://bower.io/) installed to run the build script. To update the development dependencies and build JSON 3: + + $ npm install + $ bower install + $ node build diff --git a/README.md b/README.md index 46fd060..3a68cf1 100644 --- a/README.md +++ b/README.md @@ -150,6 +150,8 @@ Check out a working copy of the JSON 3 source code with [Git](http://git-scm.com $ git clone git://github.com/bestiejs/json3.git $ cd json3 + $ npm install + $ bower install We ♥ bug reports, suggestions, questions, and pull requests! Please see our [contribution guidelines](https://bestiejs.github.io/json3/contribute.html) if you'd like to contribute. diff --git a/bower.json b/bower.json index 7215dad..d555a10 100644 --- a/bower.json +++ b/bower.json @@ -34,5 +34,8 @@ "authors": [ "Kit Cambridge " ], - "license": "MIT" + "license": "MIT", + "devDependencies": { + "closure-compiler": "https://dl.google.com/closure-compiler/compiler-latest.tar.gz" + } } diff --git a/build.js b/build.js index 53ce94d..f57bc0d 100755 --- a/build.js +++ b/build.js @@ -1,670 +1,193 @@ #!/usr/bin/env node /* JSON 3 Builder | https://bestiejs.github.io/json3 */ -var fs = require("fs"), - https = require("https"), - path = require("path"), - spawn = require("child_process").spawn, - url = require("url"), - util = require("util"), - zlib = require("zlib"); +var fs = require("fs"); +var path = require("path"); +var zlib = require("zlib"); -var vendorPath = path.join(__dirname, "vendor"), - highlightAuto = require("highlight.js").highlightAuto, - marked = require("marked"), - package = require(path.join(__dirname, "package.json")), - tar = require("tar"); +var async = require("async"); +var webpack = require("webpack"); -// The path to the Closure Compiler `.jar` file and ETag. -var closurePath = path.join(vendorPath, "closure-compiler.jar"), - closureETag = path.join(vendorPath, "closure-compiler-etag.txt"); - -// The path to the project page template. -var pageTemplatePath = path.join(path.join(__dirname, "page", "page.html")); - -var indexPageSrc = path.join(__dirname, "README.md"), - contribPageSrc = path.join(__dirname, "CONTRIBUTING.md"), - relPageSrc = path.join(__dirname, "CHANGELOG.md"); - -var indexPageDest = path.join(__dirname, "index.html"), - contribPageDest = path.join(__dirname, "contribute.html"), - relPageDest = path.join(__dirname, "changes.html"); - -// The Closure Compiler options: enable advanced optimizations and suppress all -// warnings apart from syntax and optimization errors. -var closureOptions = ["--compilation_level=ADVANCED_OPTIMIZATIONS", "--warning_level=QUIET"]; - -// A RegExp used to detect the `define` pragma used by asynchronous module -// loaders. -var definePattern = RegExp('(?:' + - // `typeof define == "function"`. Matches `==` and `===`; `'` and `"`. - 'typeof\\s+define\\s*===?\\s*([\'"])function\\1|' + - // `"function" == typeof define`. Same rules as above. - '([\'"])function\\2\\s*===?\\s*typeof\\s+define' + -')' + -// `&&`. -'\\s*&&\\s*(?:' + - // `!!` (optional Boolean coercion) - '(?:!!)?' + - // `define`. - 'define\\s*(?:' + - // `.amd`. - '\\.\\s*amd|' + - // `["amd"]` | `['amd']`. - '\\[\\s*([\'"])amd\\3\\s*\\]' + - ')|' + - '(?:' + - '(?:' + - // `typeof define.amd`. - 'typeof\\s+define\\.\\s*amd|' + - // `typeof define["amd"]` or `typeof define['amd']`. - 'typeof\\s+define\\[\\s*([\'"])amd\\4\\s*\\]' + - ')' + - // `=== "object"`. Same rules for quotes and equality operators. - '\\s*===?\\s*([\'"])object\\5' + - '|' + - // `"object" ===`. - '([\'"])object\\6\\s*===?\\s*' + - '(?:' + - 'typeof\\s+define\\.\\s*amd|' + - 'typeof\\s+define\\[\\s*([\'"])amd\\7\\s*\\]' + - ')' + - ')' + - '(?:' + - // `&&` (optional Boolean test for `define.amd`). - '\\s*&&\\s*' + - '(?:' + - // `!!` (optional Boolean coercion) - '(?:!!)?' + - // `define.amd`. - 'define\\.\\s*amd|' + - // `define["amd"] | define['amd']`. - 'define\\[\\s*([\'"])amd\\8\\s*\\]' + - ')' + - ')?' + -')', 'g'); - -// List of properties to prevent the Closure Compiler from minifying. -var propertyWhitelist = [ - 'Date', - 'JSON', - 'JSON3', - 'Math', - 'Number', - 'Object', - 'String', - 'SyntaxError', - 'TypeError', - 'global', - 'parse', - 'runInContext', - 'self', - 'stringify', - 'window' -]; - -function PageRenderer(options) { - marked.Renderer.call(this); - if (options) { - this.issueFormatters = options.issueFormatters; - this.imgClasses = options.imgClasses; - this.linkClasses = options.linkClasses; - } -} - -util.inherits(PageRenderer, marked.Renderer); - -PageRenderer.issuePattern = /(\s*|[\[])\s*(issue|pr)\s*#(\d+)([;\]]|\s*)/gi; - -PageRenderer.prototype.issueFormatters = null; -PageRenderer.prototype.imgClasses = null; -PageRenderer.prototype.linkClasses = null; - -PageRenderer.prototype.listitem = function listitem(text) { - var self = this; - if (this.issueFormatters) { - text = text.replace(PageRenderer.issuePattern, function linkIssue(match, pre, type, id, post) { - var getURI = self.issueFormatters[type.toLowerCase()]; - if (!getURI) { - return match; - } - return pre + util.format('#%s', - getURI(id), id) + post; - }); - } - return marked.Renderer.prototype.listitem.call(this, text); -}; - -PageRenderer.prototype.image = function image(src, title, alt) { - var attrs = [util.format('src="%s" alt="%s"', src, alt)]; - if (title) { - attrs.push(util.format('title="%s"', title)); - } - if (this.imgClasses) { - var className = this.imgClasses[path.basename(src)]; - if (className) { - attrs.push(util.format('class="%s"', className)); - } - } - return util.format('', attrs.join(" ")); -}; - -PageRenderer.prototype.link = function link(href, title, text) { - var uri = url.parse(href); - if (this.options && this.options.sanitize && uri.protocol == "javascript") { - return ''; - } - var attrs = [util.format('href="%s"', href)]; - if (title) { - attrs.push(util.format('title="%s"', title)); - } - if (this.linkClasses) { - var className = this.linkClasses[uri.hostname]; - if (className) { - attrs.push(util.format('class="%s"', className)); - } - } - return util.format("%s", attrs.join(" "), text); -}; - -marked.setOptions({ - "smartypants": true, - "highlight": function highlight(source) { - return highlightAuto(source).value; - } -}); - -main(); +var genPage = require("./build/genPage"); +var minify = require("./build/minify"); +var package = require("./package.json"); +var PageRenderer = require("./build/renderer"); +var pp = require("./build/pp"); +var WrapperPlugin = require("./build/wrapper"); function main() { - genIndex(); - genContrib(); - genRel(); - minify(); -} - -function genIndex() { - var r = new PageRenderer({ - imgClasses: { "logo.png": "logo" }, - linkClasses: { "travis-ci.org": "travis-ci" } - }); - genPage("JSON 3", indexPageDest, indexPageSrc, - { "renderer": r }, function afterGen(err) { - console.log(err || "GitHub project page generated successfully."); - }); -} - -function genContrib() { - var r = new PageRenderer(); - genPage("Contribute | JSON 3", contribPageDest, contribPageSrc, - { "renderer": r }, function afterGen(err) { - - console.log(err || "Contributing page generated successfully."); - }); -} - -function genRel() { - var r = new PageRenderer({ - issueFormatters: { - issue: function formatIssue(id) { - return "https://github.com/bestiejs/json3/issues/" + id; - }, - pr: function formatPR(id) { - return "https://github.com/bestiejs/json3/pull/" + id; - } - } - }); - genPage("Releases | JSON 3", relPageDest, relPageSrc, - {"renderer": r }, function afterGen(err) { - - console.log(err || "Change log generated successfully."); - }); -} - -// Compress JSON 3 using the Closure Compiler. -function minify() { - getCompiler(function hasCompiler(error) { - if (error) { - console.log(error); - return; - } - - fs.readFile(path.join(__dirname, "lib", "json3.js"), "utf8", function readSource(error, source) { - if (error) { - console.log(error); - return; - } - - console.log("Development version size: %d KB.", Math.round(Buffer.byteLength(source) / 1024 * 100) / 100); - // Shell out to the Closure Compiler. Requires Java 7 or higher. - var error = [], errorLength = 0; - var output = [], outputLength = 0; - var compiler = spawn("java", ["-jar", closurePath].concat(closureOptions)); - compiler.stdout.on("data", function onData(data) { - // Append the data to the output stream. - output.push(data); - outputLength += data.length; - }); - compiler.stderr.on("data", function onError(data) { - // Append the error message to the error stream. - error.push(data); - errorLength += data.length; - }); - compiler.on("exit", function onExit(status) { - var exception; - // `status` specifies the process exit code. - if (status) { - exception = new Error(Buffer.concat(error, errorLength)); - exception.status = status; - } - compressSource(exception, '' + Buffer.concat(output, outputLength)); - }); - // Proxy the preprocessed source to the Closure Compiler. - compiler.stdin.end(preprocessSource(source)); - - // Post-processes the compressed source and writes the result to disk. - function compressSource(exception, compressed) { - if (exception) { - console.log(exception); - } else { - // Extract the JSON 3 header and clean up the minified source. - compressed = extractComments(source)[0] + '\n' + postprocessSource(compressed); - // Write the compressed version to disk. - fs.writeFile(path.join(__dirname, "lib", "json3.min.js"), compressed, writeSource); + async.auto({ + pages: function pages(nextTask) { + writePages([{ + "title": "JSON 3", + "src": path.join(__dirname, "README.md"), + "dest": path.join(__dirname, "index.html"), + "markedOptions": { + "renderer": new PageRenderer({ + "imgClasses": { "logo.png": "logo" }, + "linkClasses": { "travis-ci.org": "travis-ci" } + }) } - // Checks the `gzip`-ped size of the compressed version. - function writeSource(exception) { - console.log(exception || "Compressed version generated successfully."); - zlib.gzip(compressed, function (exception, results) { - console.log("Compressed version size: %d KB.", Math.round(results.length / 1024 * 100) / 100); - }); + }, { + "title": "Contribute | JSON 3", + "src": path.join(__dirname, "CONTRIBUTING.md"), + "dest": path.join(__dirname, "contribute.html"), + "markedOptions": { + "renderer": new PageRenderer() } - } - }); - }); -} - -// Internal: Extracts line and block comments from a JavaScript `source` -// string. Returns an array containing the comments. -function extractComments(source) { - var index = 0, length = source.length, results = [], symbol, position, original; - while (index < length) { - symbol = source[index]; - switch (symbol) { - // Parse line and block comments. - case "/": - original = symbol; - symbol = source[++index]; - switch (symbol) { - // Extract line comments. - case "/": - position = source.indexOf("\n", index); - if (position < 0) { - // Check for CR line endings. - position = source.indexOf("\r", index); + }, { + "title": "Releases | JSON 3", + "src": path.join(__dirname, "CHANGELOG.md"), + "dest": path.join(__dirname, "changes.html"), + "markedOptions": { + "renderer": new PageRenderer({ + "issueFormatters": { + "issue": function formatIssue(id) { + return "https://github.com/bestiejs/json3/issues/" + id; + }, + "pr": function formatPR(id) { + return "https://github.com/bestiejs/json3/pull/" + id; + } } - results.push(original + source.slice(index, index = position < 0 ? length : position)); - break; - // Extract block comments. - case "*": - position = source.indexOf("*/", index); - if (position < 0) { - throw SyntaxError("Unterminated block comment."); - } - // Advance past the end of the comment. - results.push(original + source.slice(index, index = position += 2)); - break; - default: - index++; + }) } - break; - // Parse strings separately to ensure that any JavaScript comments within - // them are preserved. - case '"': - case "'": - for (position = index, original = symbol; index < length;) { - symbol = source[++index]; - if (symbol == "\\") { - // Skip past escaped characters. - index++; - } else if ("\n\r\u2028\u2029".indexOf(symbol) > -1) { - // According to the ES 5.1 spec, strings may not contain unescaped - // line terminators. - throw SyntaxError("Illegal line continuation."); - } else if (symbol == original) { - break; - } + }], nextTask); + }, + + build: function build(nextTask) { + var compiler = webpack({ + context: path.join(__dirname, "src"), + entry: { + json3: "./runInContext.js" + }, + output: { + path: path.join(__dirname, "lib"), + filename: "[name].js" + }, + module: { + loaders: [] + }, + resolve: { + modulesDirectories: [] + }, + bail: true, + node: { + console: false, + global: false, + process: false, + Buffer: false, + __filename: false, + __dirname: false } - if (source[index] == original) { - index++; - break; + }); + compiler.plugin("compilation", function gotCompilation(compilation) { + compilation.apply(new WrapperPlugin(package.version)); + }); + compiler.run(function afterRun(err, stats) { + if (err) { + console.error("Error building JSON 3: %s", err); + nextTask(err); } - throw SyntaxError("Unterminated string."); - default: - // Advance to the next character. - index++; - } - } - return results; -} - -function preprocessSource(source) { - source = source.replace(definePattern, 'typeof define === "function" && define["amd"]'); - // Add brackets to whitelisted properties so the Closure Compiler won't minify them. - // https://developers.google.com/closure/compiler/docs/api-tutorial3#export - return source.replace(RegExp('(["\'])(?:(?!\\1)[^\\n\\\\]|\\\\.)*\\1|\\.(' + propertyWhitelist.join('|') + ')\\b', 'g'), function(match, quote, prop) { - return quote ? match : "['" + prop + "']"; - }); -} - -function postprocessSource(source) { - // Shift variables in the global scope into the IIFE and fix the - // `define` pragma. - var result = source.replace(/^(var [^;]*;)\s*(\(function\([^)]*\)\{)/m, '\n;$2$1'); - return result.replace(definePattern, 'typeof define==="function"&&define.amd'); -} - -// Internal: Lazily downloads the Closure Compiler. -function getCompiler(callback) { - var hasCompiler = false, - isFinalized = false, - eTag; - - var prePending = 2, - preDone = false; - - var postPending = 1, - postDone = false; - - // Step one: determine if the Closure Compiler has already been downloaded. - fs.stat(closurePath, function getStats(error, stats) { - if (error) { - if (error.code != "ENOENT") { - return updatePre(error); - } - return updatePre(); - } - if (!stats.isFile()) { - return updatePre(new Error(util.format("`%s` must be a file.", closurePath))); - } - hasCompiler = true; - console.log('The Closure Compiler has already been downloaded.'); - updatePre(); - }); - - // Step two: Retrieve the locally-cached `ETag` from the previous download. - fs.readFile(closureETag, "utf8", function readETag(error, source) { - if (error) { - if (error.code != "ENOENT") { - return updatePre(error); - } - return updatePre(); - } - eTag = source; - updatePre(); - }); - - // Step three: download the Closure Compiler. - function download() { - var headers = { - "user-agent": util.format("JSON/%s", package.version) - }; - if (eTag && hasCompiler) { - headers["if-none-match"] = eTag; - } - var request = https.request({ - "hostname": "dl.google.com", - "port": 443, - "path": "/closure-compiler/compiler-latest.tar.gz", - "headers": headers, - "agent": false // Disable keep-alive. - }); - - request.on("response", onResponse); - function onResponse(response) { - request.removeListener("response", onResponse); - request.removeListener("error", onError); - - if (response.statusCode == 304) { - // If the cached `ETag` matches that of the entity, there is no - // need to download the Compiler tarball or extract the `.jar`. - // Skip all post-processing steps. - console.log('No updates available.'); - return finalize(); - } - - // Step four: write the new `ETag` to disk. - var eTag = response.headers.etag; - saveETag(eTag); - - // Step five: extract the Compiler `.jar` from the downloaded - // tarball using `node-tar`'s streaming `.tar` parser. - var parser = new tar.Parse(); - - parser.on("entry", onEntry); - function onEntry(entry) { - if (path.basename(entry.path) != "compiler.jar") { + console.log(stats.toString()); + nextTask(); + }); + }, + + compress: ["build", function compress(nextTask) { + var srcPath = path.join(__dirname, "lib", "json3.js"); + var destPath = path.join(__dirname, "lib", "json3.min.js"); + writeMinified(srcPath, destPath, function afterMinify(err, fileSizes) { + if (err) { + console.error("Error compressing JSON 3: %s", err); + nextTask(err); return; } - parser.removeListener("entry", onEntry); - - // Step six: write the Compiler to disk. - console.log('Extracting the Closure Compiler...'); - var writeStream = fs.createWriteStream(closurePath); - - writeStream.on("close", onClose); - function onClose() { - writeStream.removeListener("close", onClose); - writeStream.removeListener("error", onError); - // The Compiler has been successfully downloaded. - hasCompiler = true; - updatePost(); - } - - writeStream.on("error", onError); - function onError(error) { - writeStream.removeListener("close", onClose); - writeStream.removeListener("error", onError); - updatePost(error); - } - - entry.pipe(writeStream); - } - - parser.on("error", onError); - function onError(error) { - parser.removeListener("entry", onEntry); - parser.removeListener("error", onError); - parser.removeListener("end", onEnd); - updatePost(error); - } - - // Clean up attached event handlers. - parser.on("end", onEnd); - function onEnd() { - parser.removeListener("entry", onEntry); - parser.removeListener("error", onError); - parser.removeListener("end", onEnd); - } - - // Begin downloading the Compiler tarball. - console.log('Downloading the Closure Compiler...'); - response.pipe(zlib.createGunzip()).pipe(parser); - } - - request.on("error", onError); - function onError(error) { - request.removeListener("response", onResponse); - request.removeListener("error", onError); - // Connection errors are not fatal, as it is possible to use the - // previously-downloaded copy of Closure Compiler. If a cached copy - // is not available, the `hasCompiler` flag will be set to `false`, - // and `finalize` will yield an error to the `callback`. - console.log('A new version of the Closure Compiler could not be downloaded.'); - updatePost(); - } - - console.log('Updating the Closure Compiler...'); - request.end(); - } - - function finalize(error) { - if (isFinalized) { - return; - } - isFinalized = true; - if (error) { - return callback(error); - } - if (!hasCompiler) { - return callback(new Error("The Closure Compiler is required to build JSON 3.")); - } - callback(); - } - - function saveETag(eTag) { - postPending++; - fs.writeFile(closureETag, eTag, function writeETag(error) { - if (error) { - // This error is not fatal. If the write fails, the Compiler will be - // downloaded again when `getCompiler` is called. - console.log("The Closure Compiler `ETag` could not be written to disk."); - } - updatePost(); - }); - } - - function updatePre(error) { - if (preDone) { - return; - } - if (error) { - preDone = true; - return finalize(error); - } - prePending--; - if (!prePending) { - preDone = true; - download(); - } - } - - function updatePost(error) { - if (postDone) { + console.log("Development size: %d KB; compressed size: %d KB", + fileSizes.src, fileSizes.dest); + nextTask(); + }); + }] + }, function afterBuild(err) { + if (err) { + process.exit(1); return; } - if (error) { - postDone = true; - return finalize(error); - } - postPending--; - if (!postPending) { - postDone = true; - finalize(); - } - } -} - -// Generates the navigation section for a Markdown document. -// Ported from `mdtoc.rb` by Sam Stephenson. -function genNav(source) { - var headers = []; - var lines = source.split(/\r?\n/); - // First pass: Scan the Markdown source looking for titles of the format: - // `### Title ###`. Record the line number, header level (number of - // octothorpes), and text of each matching title. - lines.forEach(function (line, index) { - var match = /^(\#{1,6})\s+(.+?)\s+\1$/.exec(line); - if (match) { - headers.push([index, match[1].length, match[2]]); - } - }); - // Second pass: Iterate over all matched titles and compute their - // corresponding section numbers. Then replace the titles with annotated - // anchors. - var lastSection, lastLevel; - headers.forEach(function (value) { - var index = value[0], level = value[1], text = value[2], section, length; - if (lastSection) { - // Clone the last section metadata array. - section = lastSection.slice(0); - if (lastLevel < level) { - section.push(1); - } else { - length = lastLevel - level; - while (length--) { - section.pop(); - } - section[section.length - 1] += 1; - } - } else { - section = [1]; - } - lines[index] = Array(level + 1).join("#") + "" + text; - value.push(section); - lastSection = section; - lastLevel = level; - }); - // Third pass: Iterate over matched titles once more to produce the table of - // contents. - var navigation = headers.map(function (value) { - var index = value[0], level = value[1], text = value[2], section = value[3], name = section.join("."); - return "
  • " + text + "
  • "; + process.exit(0); }); - navigation.push(""); - return { - "navigation": navigation, - "lines": lines - }; } -function buildPage(title, template, src, options) { - var parts = genNav(src); - return template.replace(/<%=\s*(.+?)\s*%>/g, function interpolate(match, section) { - if (section == "title") { - return title; - } - if (section == "navigation") { - // Insert the table of contents directly into the template. - return parts.navigation.join("\n"); - } - if (section == "source") { - // Convert the page to HTML and insert it into the body. - return marked(parts.lines.join("\n"), options); +function writePages(pages, callback) { + async.each(pages, function eachPage(page, nextPage) { + genPage(page.title, page.dest, page.src, page.markedOptions, afterGen); + function afterGen(err) { + if (err) { + console.log("Error generating project page %j: %s", page.title, err); + nextPage(err); + return; + } + console.log("Project page %j generated successfully.", page.title); + nextPage(); } - return ""; - }); + }, callback); } -// Generates a GitHub project page using the template. -function genPage(title, destPath, srcPath, options, callback) { - var pendingReads = 2; - var contents = { "src": null, "tmpl": null }; - function afterRead(err, content, prop) { - if (pendingReads === 0) { - return; - } +// Compress JSON 3 using the Closure Compiler. +function writeMinified(srcPath, destPath, callback) { + var fileSizes = { + "src": 0, + "dest": 0 + }; + var header, minSrc; + fs.readFile(srcPath, afterRead); + function afterRead(err, srcBytes) { if (err) { - pendingReads = 0; callback(err); return; } - pendingReads--; - contents[prop] = content; - if (pendingReads > 0) { + fileSizes.src = getKilobytes(srcBytes.length); + var src = srcBytes.toString("utf8"); + // Extract the JSON 3 header. + header = pp.extractComments(src)[0]; + minify(pp.preprocessSource(src), afterMinify); + } + function afterMinify(err, minSrcBytes) { + if (err) { + callback(err); return; } - // Interpolate the page template. - var page = buildPage(title, contents.tmpl, contents.src, options); - // Write the page to disk. - fs.writeFile(destPath, page, callback); + // Post-process the compressed source. + minSrc = new Buffer([ + header, + pp.postprocessSource(minSrcBytes.toString("utf8")) + ].join("\n")); + async.each([ + // Write the compressed version to disk. + function write(nextFunc) { + fs.writeFile(destPath, minSrc, nextFunc); + }, + // Calculate the `gzip`-ped size of the compressed version. + function gzipSize(nextFunc) { + zlib.gzip(minSrc, function afterGzip(err, results) { + if (!err) { + fileSizes.dest = getKilobytes(results.length); + } + nextFunc(); + }); + } + ], function eachFunc(func, nextFunc) { + func(nextFunc); + }, function afterFinish(err) { + if (err) { + callback(err); + return; + } + callback(null, fileSizes); + }); } - fs.readFile(srcPath, "utf8", function afterReadSrc(err, src) { - afterRead(err, src, "src"); - }); - fs.readFile(pageTemplatePath, "utf8", - function afterReadTemplate(err, template) { +} - afterRead(err, template, "tmpl"); - }); +function getKilobytes(byteLength) { + return Math.round(byteLength / 1024 * 100) / 100; } + +main(); diff --git a/build/assets/footer.js b/build/assets/footer.js new file mode 100644 index 0000000..8e5ef5f --- /dev/null +++ b/build/assets/footer.js @@ -0,0 +1,22 @@ + )(root); + + if (freeExports && !isLoader) { + // Export for CommonJS environments. Since each module receives its + // own `freeExports` object, `noConflict` is a no-op. + runInContext(root, freeExports, true); + } else { + // Export for web browsers and JavaScript engines. + var JSON3 = root.JSON3 = runInContext(root); + root.JSON = { + "parse": JSON3.parse, + "stringify": JSON3.stringify + }; + } + + // Export for asynchronous module loaders. + if (isLoader) { + define(function () { + return JSON3; + }); + } +}).call(this); diff --git a/build/assets/header.js b/build/assets/header.js new file mode 100644 index 0000000..f99a2b7 --- /dev/null +++ b/build/assets/header.js @@ -0,0 +1,21 @@ +/*! JSON v3.3.2 | https://bestiejs.github.io/json3 | Copyright 2012-2015, Kit Cambridge, Benjamin Tan | http://kit.mit-license.org */ +;(function () { + // Detect the `define` function exposed by asynchronous module loaders. The + // strict `define` check is necessary for compatibility with `r.js`. + var isLoader = typeof define === "function" && define.amd; + + // Detect the `exports` object exposed by CommonJS implementations. + var freeExports = typeof exports == "object" && exports && !exports.nodeType && exports; + + // Use the `global` object exposed by Node (including Browserify via + // `insert-module-globals`), Narwhal, and Ringo as the default context, + // and the `window` object in browsers. Rhino exports a `global` function + // instead. + var root = typeof window == "object" && window || this, + freeGlobal = freeExports && typeof module == "object" && module && !module.nodeType && typeof global == "object" && global; + + if (freeGlobal && (freeGlobal.global === freeGlobal || freeGlobal.window === freeGlobal || freeGlobal.self === freeGlobal)) { + root = freeGlobal; + } + + var runInContext = ( diff --git a/build/assets/index.js b/build/assets/index.js new file mode 100644 index 0000000..5bfeea5 --- /dev/null +++ b/build/assets/index.js @@ -0,0 +1,7 @@ +var fs = require("fs"); + +module.exports = { + "header": fs.readFileSync(__dirname + "/header.js", "utf8"), + "footer": fs.readFileSync(__dirname + "/footer.js", "utf8"), + "page": fs.readFileSync(__dirname + "/page.html", "utf8") +}; diff --git a/page/page.html b/build/assets/page.html similarity index 85% rename from page/page.html rename to build/assets/page.html index dfb89f2..f32e9ff 100644 --- a/page/page.html +++ b/build/assets/page.html @@ -8,10 +8,10 @@
    - <%= source %> + <%= marked(lines.join("\n"), markedOptions) %>