Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

* Post-specific stylesheets

* Support CDNs and caching for figures and post-specific stylesheets
* New unit tests
commit cce690ab12878c35e3d3b497a0e492ce9701aa2d 1 parent 1816d6d
@joehewitt authored
View
195 lib/Blog.js
@@ -19,6 +19,7 @@ var NerveAPI = require('./NerveAPI').NerveAPI;
var Post = require('./Post').Post;
var NerveTransformer = require('./NerveTransformer').NerveTransformer;
var syndicate = require('./syndicate');
+var appjs = require('app.js');
// *************************************************************************************************
@@ -41,7 +42,6 @@ function Blog(conf, cb) {
this._assignConf(conf);
this.reload = aggregate(this.reload);
- this.reload(cb);
}
exports.Blog = Blog;
@@ -55,20 +55,6 @@ function subclass(cls, supercls, proto) {
}
subclass(Blog, events.EventEmitter, {
- useApp: function(app) {
- this.app = app;
-
- if (app) {
- this._findContent(_.bind(function() {
- this.contentPaths.forEach(function(contentPath) {
- if (contentPath.all) {
- app.paths.unshift(contentPath.path);
- }
- });
- }, this));
- }
- },
-
rssRoute: function(numberOfPosts) {
var rssRoute = syndicate.route(this, numberOfPosts);
if (this.cache) {
@@ -226,6 +212,17 @@ subclass(Blog, events.EventEmitter, {
}, this), 50);
},
+ useApp: function(app, cb) {
+ this.app = app;
+ if (app) {
+ if (this.searchPaths) {
+ this.app.paths.push.apply(this.app.paths, this.searchPaths);
+ }
+
+ this.reload(cb);
+ }
+ },
+
reload: function(cb) {
if (!this.isInvalid) {
if (cb) cb(0);
@@ -505,7 +502,7 @@ subclass(Blog, events.EventEmitter, {
this.renderPosts(posts, cb);
} else {
cb(0, posts);
- }
+ }
},
renderPost: function(post, cb) {
@@ -513,21 +510,54 @@ subclass(Blog, events.EventEmitter, {
var transformer = new NerveTransformer(this);
transformer.visit(post.tree);
- if (!transformer.embeds.length) {
+ // Asynchronously render each of the embeds
+ async.mapSeries(transformer.embeds,
+ _.bind(function(embed, cb2) {
+ this.renderEmbed(embed, post, cb2);
+ }, this),
+
+ abind(function(err, embeds) {
+ step2.apply(this);
+ }, cb, this)
+ );
+
+ function step2() {
+ async.mapSeries(transformer.figures, _.bind(function(node, cb2) {
+ this._findScript(node.attributes['require'], _.bind(function(err, result) {
+ if (err) {
+ node.addClass('figureNotFound');
+ node.removeAttribute('figure');
+ node.removeAttribute('require');
+ cb2(0);
+ } else {
+ if (process.env.NODE_ENV == "production") {
+ node.setAttribute('timestamp', result.timestamp);
+ }
+ node.setAttribute('require', result.name);
+ cb2(0);
+ }
+ }));
+ }, this),
+ _.bind(function(err) {
+ step3.apply(this);
+ }, this));
+ }
+
+ function step3() {
post.body = markdom.toHTML(post.tree);
- cb(0, post);
- } else {
- // Asynchronously render each of the embeds
- async.mapSeries(transformer.embeds,
- _.bind(function(embed, cb2) {
- this.renderEmbed(embed, post, cb2);
- }, this),
-
- abind(function(err, embeds) {
- post.body = markdom.toHTML(post.tree);
+
+ if (post.stylesheetName) {
+ this._findStylesheet(post.stylesheetName, _.bind(function(err, result) {
+ if (!err) {
+ var stylesheetURL = this.app.staticPath + '/' + result.path;
+ stylesheetURL = this.app.normalizeURL(stylesheetURL, null, result.timestamp);
+ post.stylesheets = [stylesheetURL];
+ }
cb(0, post);
- }, cb, this)
- );
+ }, this));
+ } else {
+ cb(0, post);
+ }
}
},
@@ -557,6 +587,73 @@ subclass(Blog, events.EventEmitter, {
// *********************************************************************************************
+ _findStylesheet: function(stylesheetName, cb) {
+ // assert(this.app, "Can't include stylesheet without an app");
+
+ var relativePath = './' + stylesheetName + '.css';
+ appjs.searchScript(this.app.client, _.bind(function(err, result) {
+ var searchPaths = [result.path];
+ appjs.searchStatic(relativePath, searchPaths, _.bind(function(err, cssPath) {
+ if (err) {
+ // Treat the stylesheet name as a top-level module
+ relativePath = names[0] + '/' + names[0] + '.css';
+ searchPaths = this.app.paths;
+ appjs.searchStatic(relativePath, searchPaths, function(err, cssPath) {
+ if (!err) {
+ returnResult.call(this, cssPath);
+ } else {
+ cb(err);
+ }
+ })
+ } else {
+ returnResult.call(this, cssPath);
+ }
+ }, this));
+ }, this));
+
+ function returnResult(stylesheetPath) {
+ appjs.shortenStaticPath(stylesheetPath, abind(function(err, shortPath) {
+ var result = {path: shortPath};
+ fs.lstat(stylesheetPath, abind(function(err, stat) {
+ result.timestamp = stat.mtime.getTime();
+ cb(0, result);
+ }, cb, this));
+ }, cb, this));
+ }
+ },
+
+ _findScript: function(scriptName, cb) {
+ // assert(this.app, "Can't include script without an app");
+
+ // First, look for script relative to client module
+ var relativePath = './' + scriptName;
+ var appSearchPaths = this.app.paths;
+ appjs.searchScript(this.app.client, function(err, result) {
+ var searchPaths = [result.path];
+ appjs.searchScript(relativePath, searchPaths, function(err, result) {
+ if (err) {
+ // Treat the script name as a top-level module
+ appjs.searchScript(scriptName, appSearchPaths, function(err, result) {
+ if (!err) {
+ returnResult(result);
+ } else {
+ cb(err);
+ }
+ })
+ } else {
+ returnResult(result);
+ }
+ });
+ });
+
+ function returnResult(result) {
+ fs.lstat(result.path, abind(function(err, stat) {
+ result.timestamp = stat.mtime.getTime();
+ cb(0, result);
+ }, cb, this));
+ }
+ },
+
_renderEmbed: function(key, embed, post, cb) {
D&&D('render embed', key);
embed.transform(post, abind(function(err, result) {
@@ -571,7 +668,7 @@ subclass(Blog, events.EventEmitter, {
};
if (this.cache) {
// XXXjoe Temporarily disable caching
- // this.cache.store(key, entry);
+ this.cache.store(key, entry);
}
if (result && result.content) {
@@ -618,7 +715,8 @@ subclass(Blog, events.EventEmitter, {
this.host = conf.host;
this.contentPattern = fixPath(conf.content);
this.postsPerPage = conf.postsPerPage || 10;
-
+ this.searchPaths = conf.paths || [];
+
this.api = new NerveAPI(this, conf.api || defaultApiPath);
if (conf.cache) {
@@ -633,9 +731,6 @@ subclass(Blog, events.EventEmitter, {
var ImageTransformer = require('./ImageTransformer').ImageTransformer;
this.addTransform(new ImageTransformer());
- var FigureTransformer = require('./FigureTransformer').FigureTransformer;
- this.addTransform(new FigureTransformer());
-
// XXXjoe Not yet implemented
// if (conf.facebook) {
// var FacebookTransformer = require('./FacebookTransformer').FacebookTransformer;
@@ -665,7 +760,7 @@ subclass(Blog, events.EventEmitter, {
if (node instanceof markdom.nodeTypes.Header && node.level == 1) {
var header = node.content.toHTML();
var info = this._parseHeader(header);
-
+
var slug = info.title.toLowerCase().split(/\s+/).join('-');
slug = slug.replace(/[^a-z0-9\-]/g, '');
@@ -686,6 +781,7 @@ subclass(Blog, events.EventEmitter, {
currentPost.group = info.group;
currentPost.tree = new markdom.nodeTypes.NodeSet([]);
currentPost.postIndex = ++postIndex;
+ currentPost.stylesheetName = info.stylesheetName;
posts.push(currentPost);
} else if (currentPost) {
currentPost.tree.nodes.push(node);
@@ -700,18 +796,33 @@ subclass(Blog, events.EventEmitter, {
},
_parseHeader: function(header) {
- var reTitle = /(.*?)\s*\[(.*?)\]/;
- var m = reTitle.exec(header);
- if (m && m[2]) {
- var date = new Date(m[2] + " " + defaultTimeZone);
+ var reStyle = /\(@(.*?)\)/;
+ var reGroup = /\[(.*?)\]/;
+
+ var title, stylesheetName, group, date;
+ var result = {};
+
+ var m = reStyle.exec(header);
+ if (m && m[1]) {
+ result.stylesheetName = m[1];
+ header = header.replace(reStyle, '')
+ }
+ m = reGroup.exec(header);
+ if (m && m[1]) {
+ var date = new Date(m[1] + " " + defaultTimeZone);
if (isNaN(date.getTime())) {
- return {title: m[1], group: m[2]};
+ result.group = m[1];
} else {
- return {title: m[1], date: date};
+ result.date = date;
}
+
+ header = header.replace(reGroup, '')
} else {
- return {title: header, group: 'drafts'};
+ result.group = 'drafts';
}
+
+ result.title = header.trim();
+ return result;
},
_postsAreEqual: function(a, b) {
View
31 lib/FigureTransformer.js
@@ -1,31 +0,0 @@
-
-var _ = require('underscore');
-var path = require('path');
-var fs = require('fs');
-var abind = require('dandy/errors').abind;
-var ibind = require('dandy/errors').ibind;
-
-// ************************************************************************************************
-
-function FigureTransformer() {
-}
-exports.FigureTransformer = FigureTransformer;
-
-FigureTransformer.prototype = {
- pattern: /^figure:(.*?)\/(.*?)$/,
-
- transform: function(post, projectName, figureName, url, title, alt, query, cb) {
- if (!projectName || !figureName) { cb(new Error("Invalid figure URL")); return; }
-
- var divClass = "figure-" + projectName + "-" + figureName;
-
- var timestamp = '';
- if (timestamp) {
- projectName += '@' + timestamp;
- }
-
- var tag = '<div class="figure" require="' + projectName + '" figure="' + figureName + '">' + alt + '</div>';
-
- cb(0, {content: tag});
- }
-};
View
3  lib/NerveAPI.js
@@ -136,7 +136,8 @@ function postsForClient(posts, format) {
url: post.url,
group: post.group,
body: post.body,
- attachments: post.attachments
+ attachments: post.attachments,
+ stylesheets: post.stylesheets
});
}
}
View
14 lib/NerveTransformer.js
@@ -10,7 +10,7 @@ var markdom = require('markdom'),
// *************************************************************************************************
-var reMetadata = /^((.|\n)*?)\s*\((([\.@][A-Za-z0-9\/_-]+\s*)+)\)\s*$/;
+var reMetadata = /^((.|\n)*?)\s*\((([\.@][A-Za-z0-9\.\/_-]+\s*)+)\)\s*$/;
var reFigure = /@[A-Za-z0-9\.\/_-]+/;
var reStyle = /(\.[A-Za-z0-9_-]+)+/;
@@ -19,6 +19,7 @@ var reStyle = /(\.[A-Za-z0-9_-]+)+/;
function NerveTransformer(blog) {
this.blog = blog;
this.embeds = [];
+ this.figures = [];
}
NerveTransformer.prototype = _.extend(new markdom.NodeTransformer(), {
@@ -82,12 +83,12 @@ NerveTransformer.prototype = _.extend(new markdom.NodeTransformer(), {
},
text: function(node) {
- parseStyling(node);
+ parseStyling(node, this.figures);
return node;
},
blockCode: function(node) {
- parseStyling(node);
+ parseStyling(node, this.figures);
return node;
},
@@ -107,7 +108,7 @@ NerveTransformer.prototype = _.extend(new markdom.NodeTransformer(), {
exports.NerveTransformer = NerveTransformer;
-function parseStyling(node) {
+function parseStyling(node, figures) {
var m = reMetadata.exec(node.text);
if (m) {
node.text = node.text.slice(0, m.index+m[1].length);
@@ -120,14 +121,15 @@ function parseStyling(node) {
if (result.figure.length == 1) {
projectName = figureName = result.figure[0];
} else {
- projectName = result.figure[0];
- figureName = result.figure[1];
+ figureName = result.figure.pop();
+ projectName = result.figure.join('/');
}
node.figure = result.figure;
node.addClass('figure');
node.setAttribute('require', projectName);
node.setAttribute('figure', figureName);
+ figures.push(node);
}
}
}
View
1  package.json
@@ -13,6 +13,7 @@
"author": "Joe Hewitt <joe@joehewitt.com>",
"contributors": [],
"dependencies": {
+ "app.js": "",
"api.js": "",
"async": "",
"bcrypt": "",
View
0  test/blogs/a/fig.js
No changes.
View
14 test/blogs/a/package.json
@@ -2,18 +2,6 @@
"name": "a",
"main": "./server.js",
"app.js": {
- "client": "./client.js"
- },
- "nerve": {
- "title": "A",
- "hostName": "*.foo.com",
- "postsPerPage": 1,
- "configs": {
- "mac": {
- "baseURL": "http://foo.com",
- "logsPath": "/var/tmp/nerve",
- "cachePath": "/var/tmp/nerve"
- }
- }
+ "static": "./static"
}
}
View
0  test/blogs/a/static/foofy.css
No changes.
13 test/blogs/c/posts.md
@@ -97,16 +97,25 @@ Monkey do.
Post 12 [11/1/11]
==========
-Simple figure. (@example)
+Simple figure. (@fig)
+
+Figure with slash. (@fig/bar)
Figure with dot. (@example.com)
-Figure with slash. (@example.com/bar)
+Figure with dot and slash. (@example.com/bar)
Figure and class names. (@example.com .foo.bar)
+Figure not found. (@gone)
+
Post 13 [11/1/11]
==========
Line 1 (.line1)
Line 2 (.line2)
+
+Post 14 (@foofy) [11/1/11]
+==========
+
+..
View
5 test/blogs/node_modules/example.com/client.js
@@ -0,0 +1,5 @@
+
+require.ready(function() {
+ var x = 5;
+ // alert(x);
+});
View
7 test/blogs/node_modules/example.com/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "example.com",
+ "main": "./server.js",
+ "app.js": {
+ "static": "./static"
+ }
+}
View
5 test/blogs/node_modules/example.com/server.js
@@ -0,0 +1,5 @@
+
+require.ready(function() {
+ var x = 5;
+ // alert(x);
+});
View
41 test/nerve-test.js
@@ -2,7 +2,8 @@ var path = require('path'),
assert = require('assert'),
vows = require('vows'),
_ = require('underscore'),
- datetime = require('datetime');
+ datetime = require('datetime'),
+ appjs = require('app.js');
require.paths.unshift(path.join(__dirname, '..', 'lib'));
@@ -12,16 +13,30 @@ var nerve = require('nerve');
function createBlog(pattern) {
return function() {
- var appPath = path.join(__dirname, 'blogs/a');
- var contentPath = path.join(__dirname, pattern);
- var blog = new nerve.Blog({
- app: appPath,
- content: contentPath,
- host: 'example.com',
- }, _.bind(function(err, app) {
+ var blogsPath = path.join(__dirname, 'blogs');
+ var appPath = path.join(blogsPath, 'a');
+ var clientPath = path.join(appPath, 'client.js');
+ appjs.searchScript(appPath, _.bind(function(err, result) {
if (err) { console.trace(err.stack); this.callback(err); return; }
- this.callback(0, blog);
- }, this));
+
+ var app = new appjs.App({
+ title: "Test",
+ client: clientPath
+ });
+
+ app.paths.push(blogsPath);
+
+ var contentPath = path.join(__dirname, pattern);
+ var blog = new nerve.Blog({
+ content: contentPath,
+ host: 'example.com',
+ });
+
+ blog.useApp(app, _.bind(function(err) {
+ if (err) { console.trace(err.stack); this.callback(err); return; }
+ this.callback(0, blog);
+ }, this));
+ }, this));
}
}
@@ -129,7 +144,7 @@ var contentTests = {
'figure names': function(posts) {
assert.equal(posts[12].body,
- '<p class="figure" require="example" figure="example">Simple figure.</p><p class="figure" require="example.com" figure="example.com">Figure with dot.</p><p class="figure" require="example.com" figure="bar">Figure with slash.</p><p class="foo bar figure" require="example.com" figure="example.com">Figure and class names.</p>'
+ '<p class="figure" require="a/fig" figure="fig">Simple figure.</p><p class="figure" require="a/fig" figure="bar">Figure with slash.</p><p class="figure" require="example.com" figure="example.com">Figure with dot.</p><p class="figure" require="example.com" figure="bar">Figure with dot and slash.</p><p class="foo bar figure" require="example.com" figure="example.com">Figure and class names.</p><p class="figure figureNotFound">Figure not found.</p>'
);
},
@@ -138,6 +153,10 @@ var contentTests = {
'<p class="line2">Line 1 (.line1)\nLine 2</p>'
);
},
+
+ 'css import': function(posts) {
+ assert.deepEqual(posts[14].stylesheets, ['/app.js/static/a/foofy.css']);
+ },
};
// *************************************************************************************************
Please sign in to comment.
Something went wrong with that request. Please try again.