1172 lines
34 KiB
JavaScript
1172 lines
34 KiB
JavaScript
(function (exports) {
|
|
"use strict";
|
|
|
|
var path = exports.path || require("path");
|
|
var Mustache = exports.Mustache || require("mustache");
|
|
var forEachAsync =
|
|
exports.forEachAsync || require("foreachasync").forEachAsync;
|
|
var THEME_PREFIX = "themes";
|
|
//var sha1sum = exports.sha1sum || require('./lib/node-adaptors').sha1sum
|
|
//var safeResolve = exports.safeResolve || require('./lib/utils').safeResolve
|
|
//var UUID = exports.uuid || require('node-uuid')
|
|
var pforms;
|
|
|
|
function Desi() {}
|
|
|
|
if (!exports.window) {
|
|
Desi.sha1sum = require("./lib/node-adapters").sha1sum;
|
|
Desi.fsapi = require("./lib/node-adapters").fsapi;
|
|
Desi.realFsapi = require("./lib/node-adapters").realFsapi;
|
|
|
|
// adds helper methods to fsapi
|
|
require("./lib/utils").create(Desi);
|
|
// adds Desi.Frontmatter
|
|
require("./lib/frontmatter").create(Desi);
|
|
}
|
|
|
|
Desi.slugify = function (title) {
|
|
return title
|
|
.toLowerCase()
|
|
.replace(/["']/g, "")
|
|
.replace(/\W/g, "-")
|
|
.replace(/^-+/g, "")
|
|
.replace(/-+$/g, "")
|
|
.replace(/--/g, "-");
|
|
};
|
|
|
|
Desi.slugifyPath = function (filepath) {
|
|
// because not all filepaths are url-safe
|
|
return filepath
|
|
.toLowerCase()
|
|
.replace(/\//g, "___SLASH___")
|
|
.replace(/["']/g, "")
|
|
.replace(/\W/g, "-")
|
|
.replace(/^-+/g, "")
|
|
.replace(/-+$/g, "")
|
|
.replace(/--/g, "-")
|
|
.replace(/___SLASH___/g, "/");
|
|
};
|
|
|
|
Desi.pad = function (str, n, c) {
|
|
c = c || "0";
|
|
if (0 !== n && !n) {
|
|
n = 2;
|
|
}
|
|
str = str.toString();
|
|
|
|
if (str.length < 2) {
|
|
return c + str;
|
|
}
|
|
|
|
return str;
|
|
};
|
|
|
|
Desi.firstCap = function (str) {
|
|
return str.replace(/^./, function ($1) {
|
|
return $1.toUpperCase();
|
|
});
|
|
};
|
|
|
|
// See https://github.com/janl/mustache.js/issues/415
|
|
Desi.num2str = function (obj) {
|
|
return JSON.parse(
|
|
JSON.stringify(obj, function (key, val) {
|
|
if ("number" === typeof val) {
|
|
val = val.toString();
|
|
}
|
|
return val;
|
|
})
|
|
);
|
|
};
|
|
|
|
pforms = {
|
|
year: function (entity) {
|
|
return entity.year;
|
|
},
|
|
month: function (entity) {
|
|
return Desi.pad(entity.month, 2);
|
|
},
|
|
day: function (entity) {
|
|
return Desi.pad(entity.day, 2);
|
|
},
|
|
path: function (entity) {
|
|
return entity.relativePath.toLowerCase().replace(/^\//, "");
|
|
},
|
|
relative_path: function (entity) {
|
|
// TODO slug the path in desirae proper?
|
|
// TODO remove collection from start of path instead
|
|
// of blindly assuming one directory at start of path
|
|
// entity.collection.name
|
|
return entity.relativePath.toLowerCase().replace(/^\/?[^\/]+\//, "");
|
|
},
|
|
filename: function (entity) {
|
|
// don't put .html
|
|
return entity.name.toLowerCase().replace(/\.\w+$/, "");
|
|
},
|
|
slug: function (entity) {
|
|
// alias of title
|
|
return entity.slug;
|
|
},
|
|
title: function (entity) {
|
|
return entity.slug;
|
|
},
|
|
name: function (entity) {
|
|
// alias of title
|
|
return entity.slug;
|
|
},
|
|
collection: function (entity) {
|
|
// TODO implement in desirae
|
|
return (
|
|
(entity.collection && entity.collection.name) ||
|
|
entity.collectionname ||
|
|
entity.collection ||
|
|
""
|
|
);
|
|
},
|
|
categories: function (entity) {
|
|
return entity.categories[0] || "";
|
|
},
|
|
i_month: function (entity) {
|
|
return parseInt(entity.month, 10) || 0;
|
|
},
|
|
i_day: function (entity) {
|
|
return parseInt(entity.day, 10) || 0;
|
|
},
|
|
};
|
|
|
|
Desi.permalinkify = function (desi, purl, entity) {
|
|
var parts = purl.split("/");
|
|
// when created from the web or cmd the file doesn't yet exist
|
|
if (!entity.name) {
|
|
entity.name = entity.slug + ".html";
|
|
}
|
|
if (!entity.relativePath) {
|
|
entity.relativePath =
|
|
entity.collection || Object.keys(desi.config.collections)[0] || "posts";
|
|
}
|
|
|
|
parts.forEach(function (part, i) {
|
|
var re = /:(\w+)/g,
|
|
m,
|
|
// needs to be a copy, not a reference
|
|
opart = part.toString();
|
|
/* jshint -W084 */
|
|
while (null !== (m = re.exec(opart))) {
|
|
if (pforms[m[1]]) {
|
|
part = part.replace(":" + m[1], pforms[m[1]](entity));
|
|
}
|
|
}
|
|
/* jshint +W084 */
|
|
|
|
parts[i] = part || "";
|
|
});
|
|
|
|
parts.unshift("/");
|
|
purl = path.join.apply(null, parts);
|
|
if (!/(\/|\.html?)$/.test(purl)) {
|
|
// we just default to a slash if you were ambiguous
|
|
purl += "/";
|
|
}
|
|
|
|
return purl;
|
|
};
|
|
|
|
function readFrontmatter(things) {
|
|
return forEachAsync(things, function (file) {
|
|
var parts = Desi.Frontmatter.parse(file.contents);
|
|
if (!file.sha1) {
|
|
// TODO sha1sum
|
|
}
|
|
|
|
file.yml = parts.yml;
|
|
file.frontmatter = parts.frontmatter;
|
|
file.body = parts.body;
|
|
});
|
|
}
|
|
|
|
// TODO redo entirely
|
|
function getDirty(cacheByPath, cacheBySha1, thingies, deps) {
|
|
var byDirty = {};
|
|
Object.keys(thingies).forEach(function (key) {
|
|
var files = thingies[key],
|
|
cached,
|
|
cdate,
|
|
fdate;
|
|
|
|
files.forEach(function (file) {
|
|
var pathname = path.join(file.relativePath + "/" + file.name);
|
|
// TODO legitimately checkout layout dependencies
|
|
if (deps && Object.keys(deps).length) {
|
|
byDirty[pathname] = file;
|
|
return;
|
|
}
|
|
|
|
if (!cacheByPath[pathname]) {
|
|
if (cacheBySha1[file.sha1]) {
|
|
// TODO rename
|
|
}
|
|
|
|
byDirty[pathname] = file;
|
|
return;
|
|
}
|
|
|
|
cached = cacheByPath[pathname];
|
|
cached.visited = true;
|
|
|
|
if (cached.sha1 && file.sha1) {
|
|
if (file.sha1 && cached.sha1 !== file.sha1) {
|
|
byDirty[pathname] = file;
|
|
return;
|
|
}
|
|
|
|
cdate = cached.lastModifiedDate && new Date(cached.lastModifiedDate);
|
|
fdate = file.lastModifiedDate && new Date(file.lastModifiedDate);
|
|
|
|
if (!cdate || !fdate || cdate !== fdate) {
|
|
byDirty[pathname] = file;
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
return byDirty;
|
|
}
|
|
|
|
function getLayout(desi, themename, layoutname, arr, i) {
|
|
if (i > (desi.config.max_layouth_depth || 10)) {
|
|
console.error(
|
|
"desi.config.yml:max_layouth_depth if your layouts intentionally nest more than " +
|
|
i +
|
|
" levels deep"
|
|
);
|
|
throw new Error(
|
|
"Possible circular dependency in theme '" +
|
|
themename +
|
|
"', layout '" +
|
|
layoutname +
|
|
"'"
|
|
);
|
|
}
|
|
i = i || 0;
|
|
// TODO meta.layout for each entity
|
|
arr = arr || [];
|
|
|
|
var layoutdir = "layouts",
|
|
themepath,
|
|
file;
|
|
|
|
if (!themename) {
|
|
themename = desi.site.theme || desi.config.theme;
|
|
}
|
|
if (!desi.config.themes[themename]) {
|
|
console.error("'" + themename + "' was not found among the themes");
|
|
return arr;
|
|
}
|
|
|
|
// TODO NO DEFAULTS
|
|
if ("__page__" === layoutname) {
|
|
layoutname = desi.config.themes[themename].pageLayout || "page";
|
|
} else if ("__post__" === layoutname) {
|
|
layoutname = desi.config.themes[themename].postLayout || "post";
|
|
} else if (!layoutname) {
|
|
// TODO assign __post__ in a previous step and do away with this case
|
|
layoutname = desi.config.themes[themename].postLayout || "post";
|
|
}
|
|
|
|
// THEME PREFIX
|
|
themepath = path.join(THEME_PREFIX, themename, layoutdir, layoutname);
|
|
|
|
desi.content.themes.some(function (theme) {
|
|
// TODO what if it isn't html?
|
|
if (theme.path === themepath || theme.path.match(themepath + "\\.html")) {
|
|
file = theme;
|
|
theme.ext = path.extname(file.path);
|
|
// TODO merge with yml?
|
|
theme.config = desi.config.themes[themename];
|
|
theme.themename = themename;
|
|
theme.layoutname = layoutname;
|
|
arr.push(theme);
|
|
return true;
|
|
}
|
|
});
|
|
|
|
if (!file) {
|
|
console.error("could not find " + themepath);
|
|
return;
|
|
}
|
|
|
|
// TODO handle possible circular dep condition page -> post -> page
|
|
if (!file.yml || !file.yml.layout) {
|
|
// return the chain page -> posts -> default -> ruhoh-twitter
|
|
return arr;
|
|
}
|
|
|
|
if (!file.yml || !file.yml.layout) {
|
|
return arr;
|
|
}
|
|
|
|
return getLayout(desi, themename, file.yml.layout, arr, i + 1);
|
|
}
|
|
|
|
/* function clone(obj) { return JSON.parse(JSON.stringify(obj)); } */
|
|
|
|
Desi.YAML = {
|
|
parse: (exports.jsyaml || require("js-yaml")).load,
|
|
stringify: (exports.jsyaml || require("js-yaml")).dump,
|
|
};
|
|
|
|
Desi.toDesiDate = Desi.toLocaleDate = function (d) {
|
|
return (
|
|
d.getFullYear() +
|
|
"-" +
|
|
Desi.pad(d.getMonth() + 1) +
|
|
"-" +
|
|
Desi.pad(d.getDate()) +
|
|
" " +
|
|
(d.getHours() % 12) +
|
|
":" +
|
|
Desi.pad(d.getMinutes()) +
|
|
" " +
|
|
(d.getHours() - 12 >= 0 ? "pm" : "am")
|
|
);
|
|
};
|
|
|
|
Desi.fromLocaleDate = function (str) {
|
|
// handles ISO and ISO-ish dates
|
|
var m = str.match(
|
|
/(\d\d\d\d)-(\d{1,2})-(\d{1,2})([T\s](\d{1,2}):(\d{1,2})(:(\d{1,2}))?)?/
|
|
),
|
|
d = {};
|
|
if (!m) {
|
|
return [];
|
|
}
|
|
|
|
d.year = m[1];
|
|
d.month = m[2];
|
|
d.day = m[3];
|
|
|
|
d.hour = m[4] = Desi.pad(m[5] || "00"); // hours
|
|
d.minute = m[5] = Desi.pad(m[6] || "00"); // minutes
|
|
d.second = m[6] = Desi.pad(m[8] || "00"); // seconds
|
|
|
|
if (parseInt(m[4], 10) > 12) {
|
|
d.twelve_hour = m[7] = m[4] - 12; // 12-hour
|
|
d.meridian = m[8] = "pm"; // am/pm
|
|
} else {
|
|
d.twelve_hour = m[7] = m[4];
|
|
d.meridian = m[8] = "am";
|
|
}
|
|
|
|
// 0 -> 12
|
|
if (!parseInt(d.twelve_hour, 10)) {
|
|
d.twelve_hour = "12";
|
|
}
|
|
|
|
return d;
|
|
};
|
|
|
|
// read config and such
|
|
Desi._initFileAdapter = function (env) {
|
|
if (!exports.window) {
|
|
// TODO pull state out of this later
|
|
Desi.realFsapi.create(Desi, env);
|
|
}
|
|
return Promise.resolve();
|
|
};
|
|
|
|
Desi.init = function (desi, env) {
|
|
Desi._initFileAdapter(env);
|
|
|
|
if (!exports.window) {
|
|
// TODO pull state out of this later
|
|
Desi.realFsapi.create(Desi, env);
|
|
}
|
|
|
|
// config.yml, data.yml, site.yml, authors
|
|
return Promise.all([
|
|
Desi.fsapi.getAllConfigFiles(),
|
|
/*, fsapi.getBlogdir()*/
|
|
])
|
|
.then(function (plop) {
|
|
var arr = plop[0];
|
|
//, blogdir = plop[1]
|
|
//desi.blogdir = blogdir;
|
|
//desi.originals = {};
|
|
//desi.copies = {};
|
|
|
|
Object.keys(arr).forEach(function (key) {
|
|
desi[key] = arr[key];
|
|
//desi.originals[key] = arr[key];
|
|
//desi.copies[key] = clone(arr[key]);
|
|
//desi[key] = clone(arr[key]);
|
|
});
|
|
|
|
// TODO just walk all of ./*.yml authors, posts, themes, _root from the get-go
|
|
desi.config.rootdir = desi.config.rootdir || "_root";
|
|
// TODO create a config linter as a separate module
|
|
/*
|
|
if ('object' !== typeof desi.config.collections || !Object.keys(desi.config.collections).length) {
|
|
desi.config.collections = { 'posts': {} };
|
|
}
|
|
if ('object' !== typeof desi.config.themes || !Object.keys(desi.config.themes).length) {
|
|
desi.config.themes = { 'ruhoh-twitter': {} };
|
|
}
|
|
if ('object' !== typeof desi.config.assets || !Object.keys(desi.config.assets).length) {
|
|
desi.config.assets = { 'media': {} };
|
|
}
|
|
|
|
if ('string' !== typeof desi.site.theme) {
|
|
desi.site.theme = 'ruhoh-twitter';
|
|
}
|
|
*/
|
|
if (
|
|
!Array.isArray(desi.site.navigation) ||
|
|
!desi.site.navigation.length
|
|
) {
|
|
desi.site.navigation = []; // ['archive'];
|
|
}
|
|
|
|
var collectionnames = Object.keys(desi.config.collections),
|
|
themenames = Object.keys(desi.config.themes).filter(function (k) {
|
|
return "default" !== k;
|
|
}),
|
|
//.map(function (n) { return path.join(n, 'layouts'); })
|
|
assetnames = Object.keys(desi.config.assets);
|
|
// TODO make document configurability
|
|
return Promise.all([
|
|
Desi.fsapi.getMeta(
|
|
themenames.map(function (n) {
|
|
return path.join(THEME_PREFIX, n);
|
|
}),
|
|
{
|
|
dotfiles: false,
|
|
extensions: Object.keys(Desi._exts.themes),
|
|
}
|
|
),
|
|
Desi.fsapi.getMeta([desi.config.rootdir], {
|
|
dotfiles: false,
|
|
extensions: Object.keys(Desi._exts.collections),
|
|
}),
|
|
Desi.fsapi.getMeta(collectionnames, {
|
|
dotfiles: false,
|
|
extensions: Object.keys(Desi._exts.collections),
|
|
}),
|
|
Desi.fsapi.getMeta(assetnames, {
|
|
dotfiles: false,
|
|
//, extensions: Object.keys(Desi._exts.assets)
|
|
}),
|
|
Desi.fsapi.getCache(),
|
|
]);
|
|
})
|
|
.then(function (things) {
|
|
function noErrors(map) {
|
|
Object.keys(map).forEach(function (pathname) {
|
|
map[pathname] = map[pathname].filter(function (metaf) {
|
|
if (!metaf.relativePath) {
|
|
metaf.relativePath = path.dirname(metaf.path);
|
|
metaf.name = metaf.name || path.basename(metaf.path);
|
|
}
|
|
|
|
metaf.path = path.join(metaf.relativePath, metaf.name);
|
|
|
|
if (metaf.error) {
|
|
console.error("Couldn't read '" + metaf.path + "'");
|
|
console.error(metaf.error);
|
|
throw new Error(metaf.error);
|
|
//return false;
|
|
}
|
|
|
|
if (!metaf.size) {
|
|
console.error("Junk file (0 bytes) '" + metaf.path + "'");
|
|
console.error(metaf.error);
|
|
throw new Error(metaf.error);
|
|
//return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
});
|
|
|
|
return map;
|
|
}
|
|
|
|
var themes = noErrors(things[0]),
|
|
root = noErrors(things[1])[desi.config.rootdir],
|
|
collections = noErrors(things[2]),
|
|
assets = noErrors(things[3]),
|
|
cache = noErrors(things[4]);
|
|
if (!themes[Object.keys(themes)[0]].length) {
|
|
console.error("Missing THEMES!");
|
|
throw new Error("It seems that your themes directory is missing");
|
|
}
|
|
|
|
if (!root.length) {
|
|
console.error("Missing ROOT!");
|
|
throw new Error("It seems that your root directory is missing");
|
|
}
|
|
|
|
/*
|
|
if (!collections[Object.keys(collections)[0]].length) {
|
|
console.error('Missing Collections!');
|
|
throw new Error('It seems that your collections are missing');
|
|
}
|
|
*/
|
|
|
|
desi.cache = cache;
|
|
desi.meta = {
|
|
themes: themes,
|
|
collections: collections,
|
|
root: root,
|
|
assets: assets,
|
|
};
|
|
desi.styles = [];
|
|
|
|
return desi;
|
|
});
|
|
};
|
|
|
|
Desi.getDirtyFiles = function (desi, env) {
|
|
var cache = desi.cache,
|
|
//, config = desi.config
|
|
cacheByPath = {},
|
|
cacheBySha1 = {},
|
|
dfiles,
|
|
dthemes,
|
|
droot;
|
|
|
|
cache.sources = cache.sources || [];
|
|
cache.sources.forEach(function (source) {
|
|
cacheByPath[source.path] = source;
|
|
cacheBySha1[source.sha1] = source;
|
|
});
|
|
|
|
dthemes = getDirty(cacheByPath, cacheBySha1, desi.meta.themes);
|
|
droot = getDirty(cacheByPath, cacheBySha1, [desi.meta.root], dthemes);
|
|
dfiles = getDirty(cacheByPath, cacheBySha1, desi.meta.collections, dthemes);
|
|
|
|
return Promise.all([
|
|
Object.keys(droot).length
|
|
? Desi.fsapi.getContents(Object.keys(droot))
|
|
: Promise.resolve([]),
|
|
Object.keys(dfiles).length
|
|
? Desi.fsapi.getContents(Object.keys(dfiles))
|
|
: Promise.resolve([]),
|
|
Object.keys(dthemes).length
|
|
? Desi.fsapi.getContents(Object.keys(dthemes))
|
|
: Promise.resolve([]),
|
|
]).then(function (arr) {
|
|
var result = { root: [], collections: [], themes: [] };
|
|
function noErrors(collectionBase, arr, entity) {
|
|
// FOO FOO FOO
|
|
if (!entity.error) {
|
|
if (!entity.relativePath) {
|
|
entity.relativePath = path.dirname(entity.path);
|
|
entity.name = entity.name || path.basename(entity.path);
|
|
}
|
|
|
|
entity.relativePath = entity.relativePath
|
|
.replace(desi.config.rootdir, "")
|
|
.replace(/^\//, "");
|
|
entity.path = path.join(entity.relativePath, entity.name);
|
|
entity.ext = path.extname(entity.path);
|
|
|
|
// TODO better collection detection
|
|
if ("root" === collectionBase) {
|
|
// TODO how to reconcile rootdir vs _root vs root?
|
|
entity.collection = "root";
|
|
entity.collectionType = "root";
|
|
// desi.config.rootdir;
|
|
} else if ("collections" === collectionBase) {
|
|
entity.collection = entity.relativePath.split("/")[0];
|
|
entity.collectionType = "collections";
|
|
} else if ("themes" === collectionBase) {
|
|
entity.collection = entity.relativePath.split("/")[1];
|
|
entity.collectionType = "themes";
|
|
}
|
|
|
|
arr.push(entity);
|
|
return Promise.resolve();
|
|
}
|
|
|
|
if (env.onError) {
|
|
return env.onError(entity.error);
|
|
} else {
|
|
console.error("Couldn't get file contents for '" + entity.path + "'");
|
|
console.error(entity.error);
|
|
return Promise.reject(entity.error);
|
|
}
|
|
}
|
|
|
|
return forEachAsync(arr[0], function (o) {
|
|
return noErrors("root", result.root, o);
|
|
})
|
|
.then(function () {
|
|
return forEachAsync(arr[1], function (o) {
|
|
return noErrors("collections", result.collections, o);
|
|
});
|
|
})
|
|
.then(function () {
|
|
return forEachAsync(arr[2], function (o) {
|
|
return noErrors("themes", result.themes, o);
|
|
});
|
|
})
|
|
.then(function () {
|
|
desi.content = result;
|
|
|
|
return desi;
|
|
});
|
|
});
|
|
};
|
|
|
|
Desi.copyAssets = function (desi, env) {
|
|
var files = {};
|
|
Object.keys(desi.meta.assets).forEach(function (key) {
|
|
var assets = desi.meta.assets[key];
|
|
assets.forEach(function (asset) {
|
|
files[path.join(asset.relativePath, asset.name)] = path.join(
|
|
env.compiled_path,
|
|
"assets",
|
|
asset.relativePath,
|
|
asset.name
|
|
);
|
|
});
|
|
});
|
|
|
|
return (
|
|
(Object.keys(files).length &&
|
|
Desi.fsapi.copy(files).then(function (copied) {
|
|
if (copied.error) {
|
|
console.error(copied.error);
|
|
throw new Error(copied.error);
|
|
}
|
|
|
|
if (copied.errors && copied.errors.length) {
|
|
console.error("Errors copying assets...");
|
|
copied.errors.forEach(function (err) {
|
|
console.error(err);
|
|
throw new Error(err);
|
|
});
|
|
}
|
|
|
|
return desi;
|
|
})) ||
|
|
Promise.resolve(desi)
|
|
);
|
|
};
|
|
|
|
Desi.parseFrontmatter = function (desi) {
|
|
return readFrontmatter(
|
|
desi.content.root.concat(
|
|
desi.content.themes.concat(desi.content.collections)
|
|
)
|
|
).then(function () {
|
|
return desi;
|
|
});
|
|
};
|
|
|
|
Desi.getNav = function (desi) {
|
|
var alwaysAdd = true;
|
|
if (desi.site.navigation.length) {
|
|
alwaysAdd = false;
|
|
}
|
|
|
|
// TODO add missing metadata and resave file
|
|
desi.navigation = desi.site.navigation || [];
|
|
|
|
desi.content.root.forEach(function (entity) {
|
|
var name = path.join(
|
|
entity.relativePath,
|
|
path.basename(entity.name, path.extname(entity.name))
|
|
),
|
|
nindex;
|
|
|
|
// The index should not show up in the automatic listing
|
|
if (alwaysAdd && "index" === name) {
|
|
return;
|
|
}
|
|
|
|
nindex = desi.site.navigation.indexOf(name);
|
|
if (!alwaysAdd && -1 === nindex) {
|
|
return;
|
|
} else {
|
|
nindex = desi.navigation.length;
|
|
}
|
|
|
|
// TODO this happens before normalization... should fix that...
|
|
desi.navigation[nindex] = {
|
|
title: (entity.yml && entity.yml.title) || Desi.firstCap(name),
|
|
name: name,
|
|
active: false, // placeholder
|
|
};
|
|
});
|
|
|
|
// transform spare array into compact array
|
|
desi.navigation = desi.navigation.filter(function (n) {
|
|
return n;
|
|
});
|
|
|
|
return Promise.resolve(desi);
|
|
};
|
|
|
|
Desi.runTransforms = function (desi, env) {
|
|
desi.permalinks = desi.permalinks || {};
|
|
|
|
function makeTransformer(type) {
|
|
return function (entity, i, entities) {
|
|
var collection;
|
|
|
|
if ("root" === type) {
|
|
// TODO 'no magic', 'defaults in the app, not the lib'
|
|
collection = { permalink: "/:filename/" };
|
|
} else {
|
|
collection = desi.config[type][entity.collection];
|
|
}
|
|
|
|
Desi._transformers[type].forEach(function (obj) {
|
|
try {
|
|
obj.transform(desi, env, collection, entity, i, entities);
|
|
} catch (e) {
|
|
console.error("[ERROR]");
|
|
console.error(
|
|
"Transform " + obj.name + " failed on " + entity.path
|
|
);
|
|
console.error(e.message);
|
|
throw e;
|
|
}
|
|
});
|
|
};
|
|
}
|
|
|
|
desi.content.root.forEach(makeTransformer("root"));
|
|
desi.content.collections.forEach(makeTransformer("collections"));
|
|
|
|
return Promise.resolve(desi);
|
|
};
|
|
|
|
Desi._aggregations = [];
|
|
Desi.registerAggregator = function (fn) {
|
|
Desi._aggregations.push(fn);
|
|
};
|
|
Desi.runAggregations = function (desi, env /*, collectionname*/) {
|
|
return forEachAsync(Desi._aggregations, function (fn) {
|
|
return fn(desi, env);
|
|
}).then(function () {
|
|
return desi;
|
|
});
|
|
};
|
|
|
|
Desi._datamaps = {};
|
|
Desi.registerDataMapper = function (name, fn) {
|
|
if (!Desi._datamaps[name]) {
|
|
Desi._datamaps[name] = fn;
|
|
} else {
|
|
throw new Error(
|
|
"cannot add additional data mapper for '" +
|
|
name +
|
|
"' (there's already one assigned)"
|
|
);
|
|
}
|
|
};
|
|
|
|
Desi._transformers = { root: [], collections: [], assets: [], themes: [] };
|
|
Desi.registerTransform = function (name, fn, opts) {
|
|
["root", "collections", "themes", "assets"].forEach(function (thingname) {
|
|
if (!opts[thingname]) {
|
|
return;
|
|
}
|
|
|
|
if (
|
|
Desi._transformers[thingname].some(function (obj) {
|
|
if (name === obj.name || fn === obj.transform) {
|
|
return true;
|
|
}
|
|
})
|
|
) {
|
|
throw new Error(
|
|
"cannot add additional transformer for '" +
|
|
name +
|
|
"' (there's already one assigned)"
|
|
);
|
|
}
|
|
|
|
Desi._transformers[thingname].push({
|
|
name: name,
|
|
transform: fn,
|
|
root: opts.root,
|
|
assets: opts.assets,
|
|
themes: opts.themes,
|
|
collections: opts.collections,
|
|
});
|
|
});
|
|
};
|
|
|
|
Desi._exts = { root: {}, collections: {}, assets: {}, themes: {} };
|
|
|
|
Desi._renderers = { root: {}, collections: {}, assets: {}, themes: {} };
|
|
Desi.registerRenderer = function (ext, fn, opts) {
|
|
opts = opts || {};
|
|
if (!("root" in opts)) {
|
|
opts.root = true;
|
|
}
|
|
if (!("collections" in opts)) {
|
|
opts.collections = true;
|
|
}
|
|
|
|
ext = ext.replace(/^\./, "");
|
|
|
|
["root", "collections", "themes", "assets"].forEach(function (key) {
|
|
if (!opts[key]) {
|
|
return;
|
|
}
|
|
|
|
Desi._exts[key][ext] = true;
|
|
|
|
if (!Desi._renderers[key][ext]) {
|
|
Desi._renderers[key][ext] = [];
|
|
}
|
|
// LIFO
|
|
Desi._renderers[key][ext].unshift(fn);
|
|
});
|
|
};
|
|
Desi.render = function (ext, content, view) {
|
|
var type = view.entity.collectionType;
|
|
ext = (ext || "").toLowerCase().replace(/^\./, "");
|
|
|
|
if (Desi._renderers[type][ext] && Desi._renderers[type][ext].length) {
|
|
return Desi._renderers[type][ext][0](content, view);
|
|
}
|
|
return Promise.reject(
|
|
new Error("no '" + type + "' renderer registered for '." + ext + "'")
|
|
);
|
|
};
|
|
|
|
function renderLayers(desi, env, view, entity) {
|
|
var mustached = "",
|
|
layers;
|
|
|
|
// BUG XXX the entity doesn't get a datamap (though it probably doesn't need one)
|
|
layers = getLayout(desi, entity.theme, entity.layout, [entity]);
|
|
|
|
return forEachAsync(layers, function (current) {
|
|
var body = (current.body || current.contents || "").trim();
|
|
return Desi.render(current.ext, body, view)
|
|
.then(function (html) {
|
|
// TODO organize datamap inheritence
|
|
var datamap =
|
|
Desi._datamaps[current.config && current.datamap] ||
|
|
Desi._datamaps[env.datamap] ||
|
|
Desi._datamaps[entity.datamap] ||
|
|
Desi._datamaps["ruhoh@2.6"],
|
|
newview;
|
|
|
|
view.contents = mustached;
|
|
|
|
// shallowClone to prevent perfect object equality (and potential template caching)
|
|
view.entity.original_base_path = view.entity.base_path;
|
|
view.entity.home_path = view.entity.base_path + "/index.html";
|
|
env.original_base_path = env.base_path;
|
|
if (env.explicitIndexes) {
|
|
view.entity.base_path = path.join(
|
|
view.entity.base_path,
|
|
"index.html"
|
|
);
|
|
env.base_path = path.join(env.base_path, "index.html");
|
|
}
|
|
newview = datamap(view);
|
|
env.base_path = env.original_base_path;
|
|
view.entity.base_path = view.entity.original_base_path;
|
|
mustached = Mustache.render(html, newview, desi.partials);
|
|
|
|
return mustached;
|
|
})
|
|
.catch(function (e) {
|
|
console.error(current);
|
|
if (env.onError) {
|
|
return env.onError(e);
|
|
} else {
|
|
console.error(
|
|
"no registered renderer for " +
|
|
entity.path +
|
|
" or rendering failed"
|
|
);
|
|
throw e;
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
Desi.build = function (desi, env) {
|
|
var compiled = [];
|
|
if (/dropbox/.test(env.base_url)) {
|
|
env.explicitIndexes = true;
|
|
}
|
|
|
|
/*
|
|
function compileScriptEntity(entity, i, arr) {
|
|
}
|
|
*/
|
|
function compileThemeEntity(entity, i, arr) {
|
|
var view;
|
|
|
|
console.info(
|
|
"[themes] compiling " + (i + 1) + "/" + arr.length + " " + entity.path
|
|
);
|
|
// TODO generate per-page for a more full 'view' object?
|
|
view = {
|
|
entity: { collectionType: "themes" },
|
|
url: path.join(env.base_path, entity.path),
|
|
};
|
|
// TODO this is more 'preprocessing' than 'rendering', perse
|
|
return Desi.render(entity.ext, entity.body || entity.contents, view).then(
|
|
function (css) {
|
|
compiled.push({ contents: css, path: entity.path });
|
|
// TODO read theme config
|
|
if (
|
|
/stylesheets.*\.css/.test(entity.path) &&
|
|
(!/google/.test(entity.path) || /obsid/.test(entity.path))
|
|
) {
|
|
desi.styles.push(
|
|
Mustache.render(desi.partials.stylesheet_link, view)
|
|
);
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
function compileContentEntity(entity, i, arr) {
|
|
console.info(
|
|
"compiling " + (i + 1) + "/" + arr.length + " " + entity.path
|
|
);
|
|
|
|
var navigation = JSON.parse(JSON.stringify(desi.navigation)),
|
|
author =
|
|
desi.authors[entity.yml.author] ||
|
|
desi.authors[Object.keys(desi.authors)[0]],
|
|
view,
|
|
themename = entity.theme || desi.site.theme;
|
|
if (!author) {
|
|
console.error("\n\n\n");
|
|
console.error("You don't have any files in authors/*.yml");
|
|
console.error("Please create authors/your-name.yml and fill it out");
|
|
console.error("For example:");
|
|
console.error("\n");
|
|
console.error("name: John Doe");
|
|
console.error("bio: One cool dude.");
|
|
console.error("email: john.doe@email.com");
|
|
console.error("website: http://john.example.com");
|
|
console.error("facebook: http://fb.com/john.doe");
|
|
console.error("\n\n\n");
|
|
throw new Error("author file not found");
|
|
}
|
|
|
|
// TODO nested names?
|
|
navigation.forEach(function (nav) {
|
|
// TODO allow permalink
|
|
nav.href = path.join(env.base_path, nav.name);
|
|
|
|
if (!/\.x?html?$/.test(nav.href)) {
|
|
// add trailing slash
|
|
nav.href += "/";
|
|
}
|
|
|
|
if (
|
|
nav.href.replace(/(\/)?(\/index)?(\.html)?$/i, "") ===
|
|
entity.relative_url.replace(/(\/)?(\/index)?(\.html)?$/i, "")
|
|
) {
|
|
nav.active = true;
|
|
}
|
|
|
|
if (env.explicitIndexes && !/\.x?html?$/.test(nav.href)) {
|
|
nav.href = path.join(nav.href, "index.html");
|
|
}
|
|
|
|
nav.path = nav.href;
|
|
});
|
|
|
|
view = {
|
|
env: env,
|
|
config: desi.config,
|
|
site: desi.site,
|
|
data: desi.data,
|
|
entity: entity,
|
|
entity_index: i,
|
|
entities: arr,
|
|
desi: desi,
|
|
navigation: navigation,
|
|
author: Desi.num2str(author),
|
|
};
|
|
|
|
desi.allStyles = desi.styles.slice(0);
|
|
desi.styles = desi.styles.filter(function (str) {
|
|
// TODO better matching
|
|
return str.match("/" + themename + "/");
|
|
});
|
|
|
|
return renderLayers(desi, env, view, entity)
|
|
.then(function (html) {
|
|
desi.styles = desi.allStyles;
|
|
// NOTE: by now, all permalinks should be in the format
|
|
// /path/to/page.html or /path/to/page/index.html
|
|
if (/^(index)?(\/?index.html)?$/.test(entity.permalink)) {
|
|
console.info("[index] compiling " + (entity.path || entity.name));
|
|
compiled.push({
|
|
contents: html,
|
|
path: path.join("index.html"),
|
|
});
|
|
} else {
|
|
//console.info("[collection] compiling " + entity.path, entity.relative_file);
|
|
compiled.push({
|
|
contents: html,
|
|
path: path.join(entity.relative_file.replace(env.base_path, "")),
|
|
});
|
|
}
|
|
|
|
// catches /a, /a/index.html, a/index.html
|
|
// but not index.html /index.html
|
|
if (/^.+\/(index\.x?html?)$/.test(entity.permalink)) {
|
|
entity.redirects.push(
|
|
entity.permalink.replace(/\/(index.x?html?)?$/, ".html")
|
|
);
|
|
} else if (/\.x?html?$/.test(entity.permalink)) {
|
|
entity.redirects.push(
|
|
entity.permalink.replace(/\.x?html?$/, "/index.html")
|
|
);
|
|
} else {
|
|
// found index, ignoring redirect
|
|
}
|
|
|
|
// TODO why are redirects broken?
|
|
var redirectHtml = Mustache.render(desi.partials.redirect, view);
|
|
entity.redirects.forEach(function (redirect) {
|
|
compiled.push({
|
|
contents: redirectHtml,
|
|
path: redirect,
|
|
});
|
|
});
|
|
})
|
|
.catch(function (e) {
|
|
if (env.onError) {
|
|
return env.onError(e);
|
|
} else {
|
|
console.error("couldn't render " + entity.path);
|
|
console.error(entity);
|
|
console.error(e);
|
|
throw e;
|
|
}
|
|
});
|
|
}
|
|
|
|
function doStuff() {
|
|
var themes = desi.content.themes.filter(function (f) {
|
|
return !/\blayouts\b/.test(f.path);
|
|
});
|
|
console.info("[first] compiling theme assets");
|
|
return forEachAsync(themes, compileThemeEntity).then(function () {
|
|
console.info("compiling article pages");
|
|
return forEachAsync(desi.content.collections, compileContentEntity)
|
|
.then(function () {
|
|
console.info("compiling root pages");
|
|
return forEachAsync(desi.content.root, compileContentEntity);
|
|
})
|
|
.then(function () {
|
|
desi.compiled = compiled;
|
|
return desi;
|
|
});
|
|
});
|
|
}
|
|
|
|
if (!desi.partials) {
|
|
return Desi.fsapi.getAllPartials().then(function (partials) {
|
|
if (partials.error) {
|
|
throw partials.error;
|
|
}
|
|
|
|
desi.partials = partials;
|
|
return doStuff();
|
|
});
|
|
} else {
|
|
return doStuff();
|
|
}
|
|
};
|
|
|
|
Desi.buildAll = function (desi, env) {
|
|
return Desi.getDirtyFiles(desi, env)
|
|
.then(Desi.parseFrontmatter)
|
|
.then(Desi.getNav)
|
|
.then(function () {
|
|
return Desi.runTransforms(desi, env);
|
|
})
|
|
.then(function () {
|
|
return Desi.runAggregations(desi, env);
|
|
})
|
|
.then(function () {
|
|
return Desi.build(desi, env);
|
|
})
|
|
.then(function () {
|
|
return Desi.copyAssets(desi, env);
|
|
})
|
|
.catch(function (e) {
|
|
if (env.onError) {
|
|
return env.onError(e);
|
|
} else {
|
|
console.error("buildAll failed somewhere");
|
|
console.error(e);
|
|
throw e;
|
|
}
|
|
});
|
|
};
|
|
|
|
Desi.write = function (desi, env) {
|
|
var compiled = desi.compiled.slice(0),
|
|
batches = [],
|
|
now,
|
|
size = 0;
|
|
if (!compiled.length) {
|
|
return;
|
|
}
|
|
|
|
compiled.forEach(function (thing) {
|
|
thing.path = path.join(env.compiled_path, thing.path);
|
|
});
|
|
|
|
// because some servers / proxies are terrible at handling large uploads (>= 100k)
|
|
// (vagrant? or express? one of the two is CRAZY slow)
|
|
while (compiled.length) {
|
|
batches.push(compiled.splice(0, 500));
|
|
}
|
|
|
|
now = Date.now();
|
|
return forEachAsync(batches, function (files) {
|
|
return Desi.fsapi.putFiles(files).then(function (saved) {
|
|
// TODO reduce from files
|
|
size += saved.size;
|
|
|
|
if (saved.error) {
|
|
console.error("[ERROR] saving fsapi batch at root");
|
|
console.error(saved.error);
|
|
throw new Error(saved.error);
|
|
}
|
|
|
|
if (!saved.errors || !saved.errors.length) {
|
|
return;
|
|
}
|
|
|
|
saved.errors.forEach(function (e) {
|
|
console.error("[ERROR] saving fsapi batch");
|
|
console.error(e);
|
|
throw new Error(e);
|
|
});
|
|
});
|
|
}).then(function () {
|
|
return {
|
|
numFiles: desi.compiled.length,
|
|
size: size,
|
|
start: now,
|
|
end: Date.now(),
|
|
};
|
|
});
|
|
};
|
|
|
|
exports.Desirae = Desi.Desirae = Desi;
|
|
})(("undefined" !== typeof exports && exports) || window);
|