docpad
Version:
DocPad is a dynamic static site generator. Write your content as files, or import your content from other external sources. Render the content with plugins. And deploy your static or dynamic website to your favourite hosting provider.
1,458 lines (1,376 loc) • 43.9 kB
JavaScript
// Generated by CoffeeScript 2.5.1
// =====================================
// Requires
// Standard
var Errlop, FileModel, Model, docpadUtil, encodingUtil, extendr, extractOptsAndCallback, isText, jschardet, mime, pathUtil, safefs, typeChecker, util,
indexOf = [].indexOf,
hasProp = {}.hasOwnProperty,
boundMethodCheck = function(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new Error('Bound instance method accessed before binding'); } };
util = require('util');
pathUtil = require('path');
// External
Errlop = require('errlop').default;
({isText} = require('istextorbinary'));
typeChecker = require('typechecker');
safefs = require('safefs');
mime = require('mime');
extendr = require('extendr');
extractOptsAndCallback = require('extract-opts');
// Optional
jschardet = null;
encodingUtil = null;
// Local
({Model} = require('../base'));
docpadUtil = require('../util');
FileModel = (function() {
// =====================================
// Classes
/**
* The FileModel class is DocPad's representation
* of a file in the file system.
* Extends the DocPad Model class
* FileModel manages the loading
* of a file and parsing both the content and the metadata (if any).
* Once loaded, the content, metadata and file stat (file info)
* properties of the FileModel are populated, as well
* as a number of DocPad specific attributes and properties.
* Typically we do not need to create FileModels ourselves as
* DocPad handles all of that. But it is possible that a plugin
* may need to manually create FileModels for some reason.
*
* attrs =
* fullPath: 'file/path/to/somewhere'
* opts = {}
* #we only really need the path to the source file to create
* #a new file model
* model = new FileModel(attrs, opts)
*
* The FileModel forms the base class for the DocPad DocumentModel class.
* @class FileModel
* @constructor
* @extends Model
*/
class FileModel extends Model {
constructor() {
super(...arguments);
/**
* Apply an action with the supplied arguments.
* @method action
* @param {Object} args...
*/
this.action = this.action.bind(this);
}
/**
* Get the file's locale information
* @method getLocale
* @return {Object} the locale
*/
getLocale() {
return this.locale;
}
/**
* Get Options. Returns an object containing
* the properties detectEncoding, rootOutDirPath
* locale, stat, buffer, meta and TaskGroup.
* @private
* @method getOptions
* @return {Object}
*/
// @TODO: why does this not use the isOption way?
getOptions() {
return {detectEncoding: this.detectEncoding, rootOutDirPath: this.rootOutDirPath, locale: this.locale, stat: this.stat, buffer: this.buffer, meta: this.meta, createTaskGroup: this.createTaskGroup};
}
/**
* Checks whether the passed key is one
* of the options.
* @private
* @method isOption
* @param {String} key
* @return {Boolean}
*/
isOption(key) {
var names, result;
names = ['detectEncoding', 'rootOutDirPath', 'locale', 'stat', 'data', 'buffer', 'meta', 'createTaskGroup'];
result = indexOf.call(names, key) >= 0;
return result;
}
/**
* Extract Options.
* @private
* @method extractOptions
* @param {Object} attrs
* @return {Object} the options object
*/
extractOptions(attrs) {
var key, result, value;
// Prepare
result = {};
for (key in attrs) {
if (!hasProp.call(attrs, key)) continue;
value = attrs[key];
if (this.isOption(key)) {
result[key] = value;
delete attrs[key];
}
}
// Return
return result;
}
/**
* Set the options for the file model.
* Valid properties for the attrs parameter:
* TaskGroup, detectEncoding, rootOutDirPath,
* locale, stat, data, buffer, meta.
* @method setOptions
* @param {Object} [attrs={}]
*/
setOptions(attrs = {}) {
// TaskGroup
if (attrs.createTaskGroup != null) {
this.createTaskGroup = attrs.createTaskGroup;
delete this.attributes.createTaskGroup;
}
// Root Out Path
if (attrs.detectEncoding != null) {
this.rootOutDirPath = attrs.detectEncoding;
delete this.attributes.detectEncoding;
}
// Root Out Path
if (attrs.rootOutDirPath != null) {
this.rootOutDirPath = attrs.rootOutDirPath;
delete this.attributes.rootOutDirPath;
}
// Locale
if (attrs.locale != null) {
this.locale = attrs.locale;
delete this.attributes.locale;
}
// Stat
if (attrs.stat != null) {
this.setStat(attrs.stat);
delete this.attributes.stat;
}
// Data
if (attrs.data != null) {
this.setBuffer(attrs.data);
delete this.attributes.data;
}
// Buffer
if (attrs.buffer != null) {
this.setBuffer(attrs.buffer);
delete this.attributes.buffer;
}
// Meta
if (attrs.meta != null) {
this.setMeta(attrs.meta);
delete this.attributes.meta;
}
return this;
}
/**
* Clone the model and return the newly cloned model.
* @method clone
* @return {Object} cloned file model
*/
clone() {
var attrs, clonedModel, opts;
// Fetch
attrs = this.getAttributes();
opts = this.getOptions();
// Clean up
delete attrs.id;
delete attrs.meta.id;
delete opts.meta.id;
delete opts.meta.attributes.id;
// Clone
clonedModel = new this.klass(attrs, opts);
// Emit clone event so parent can re-attach listeners
this.emit('clone', clonedModel);
// Return
return clonedModel;
}
// ---------------------------------
// Helpers
/**
* File encoding helper
* opts = {path, to, from, content}
* @private
* @method encode
* @param {Object} opts
* @return {Object} encoded result
*/
encode(opts) {
var encodeError, err, locale, result;
// Prepare
locale = this.getLocale();
result = opts.content;
if (opts.to == null) {
opts.to = 'utf8';
}
if (opts.from == null) {
opts.from = 'utf8';
}
try {
// Import optional dependencies
if (encodingUtil == null) {
encodingUtil = require('encoding');
}
} catch (error) {}
// Convert
if (encodingUtil != null) {
this.log('info', util.format(locale.fileEncode, opts.to, opts.from, opts.path));
try {
result = encodingUtil.convert(opts.content, opts.to, opts.from);
} catch (error) {
encodeError = error;
err = new Errlop(util.format(locale.fileEncodeConvertError, opts.to, opts.from, opts.path), encodeError);
this.log('warn', err);
}
} else {
err = new Errlop(util.format(locale.fileEncodeConvertError, opts.to, opts.from, opts.path));
this.log('warn', err);
}
// Return
return result;
}
/**
* Set the file model's buffer.
* Creates a new node.js buffer
* object if a buffer object is
* is not passed as the parameter
* @method setBuffer
* @param {Object} [buffer]
*/
setBuffer(buffer) {
if (!Buffer.isBuffer(buffer)) {
buffer = docpadUtil.newBuffer(buffer);
}
this.bufferTime = this.get('mtime') || new Date();
this.buffer = buffer;
return this;
}
/**
* Get the file model's buffer object.
* Returns a node.js buffer object.
* @method getBuffer
* @return {Object} node.js buffer object
*/
getBuffer() {
return this.buffer;
}
/**
* Is Buffer Outdated
* True if there is no buffer OR the buffer time is outdated
* @method isBufferOutdated
* @return {Boolean}
*/
isBufferOutdated() {
return (this.buffer != null) === false || this.bufferTime < (this.get('mtime') || new Date());
}
/**
* Set the node.js file stat.
* @method setStat
* @param {Object} stat
*/
setStat(stat) {
this.stat = stat;
this.set({
ctime: new Date(stat.ctime),
mtime: new Date(stat.mtime)
});
return this;
}
/**
* Get the node.js file stat.
* @method getStat
* @return {Object} the file stat
*/
getStat() {
return this.stat;
}
/**
* Get the file model attributes.
* By default the attributes will be
* dereferenced from the file model.
* To maintain a reference, pass false
* as the parameter. The returned object
* will NOT contain the file model's ID attribute.
* @method getAttributes
* @param {Object} [dereference=true]
* @return {Object}
*/
//NOTE: will the file model's ID be deleted if
//dereference=false is passed??
getAttributes(dereference = true) {
var attrs;
attrs = this.toJSON(dereference);
delete attrs.id;
return attrs;
}
/**
* Get the file model attributes.
* By default the attributes will
* maintain a reference to the file model.
* To return a dereferenced object, pass true
* as the parameter. The returned object
* will contain the file model's ID attribute.
* @method toJSON
* @param {Object} [dereference=false]
* @return {Object}
*/
toJSON(dereference = false) {
var data;
data = super.toJSON();
data.meta = this.getMeta().toJSON();
if (dereference === true) {
data = extendr.dereferenceJSON(data);
}
return data;
}
/**
* Get the file model metadata object.
* Optionally pass a list of metadata property
* names corresponding to those properties that
* you want returned.
* @method getMeta
* @param {Object} [args...]
* @return {Object}
*/
getMeta(...args) {
if (this.meta === null) {
this.meta = new Model();
}
if (args.length) {
return this.meta.get(...args);
} else {
return this.meta;
}
}
/**
* Assign attributes and options to the file model.
* @method set
* @param {Array} attrs the attributes to be applied
* @param {Object} opts the options to be applied
*/
set(attrs, opts) {
var newAttrs, options, ref;
// Check
if (typeChecker.isString(attrs)) {
newAttrs = {};
newAttrs[attrs] = opts;
return this.set(newAttrs, opts);
}
// Prepare
attrs = (ref = typeof attrs.toJSON === "function" ? attrs.toJSON() : void 0) != null ? ref : attrs;
// Extract options
options = this.extractOptions(attrs);
// Perform the set
super.set(attrs, opts);
// Apply the options
this.setOptions(options, opts);
return this;
}
/**
* Set defaults. Apply default attributes
* and options to the file model
* @method setDefaults
* @param {Object} attrs the attributes to be applied
* @param {Object} opts the options to be applied
*/
setDefaults(attrs, opts) {
var options, ref;
// Prepare
attrs = (ref = typeof attrs.toJSON === "function" ? attrs.toJSON() : void 0) != null ? ref : attrs;
// Extract options
options = this.extractOptions(attrs);
// Apply
super.setDefaults(attrs, opts);
// Apply the options
this.setOptions(options, opts);
// Chain
return this;
}
/**
* Set the file model meta data,
* attributes and options in one go.
* @method setMeta
* @param {Object} attrs the attributes to be applied
* @param {Object} opts the options to be applied
*/
setMeta(attrs, opts) {
var options, ref;
// Prepare
attrs = (ref = typeof attrs.toJSON === "function" ? attrs.toJSON() : void 0) != null ? ref : attrs;
// Extract options
options = this.extractOptions(attrs);
// Apply
this.getMeta().set(attrs, opts);
this.set(attrs, opts);
// Apply the options
this.setOptions(options, opts);
// Chain
return this;
}
/**
* Set the file model meta data defaults
* @method setMetaDefaults
* @param {Object} attrs the attributes to be applied
* @param {Object} opts the options to be applied
*/
setMetaDefaults(attrs, opts) {
var options, ref;
// Prepare
attrs = (ref = typeof attrs.toJSON === "function" ? attrs.toJSON() : void 0) != null ? ref : attrs;
// Extract options
options = this.extractOptions(attrs);
// Apply
this.getMeta().setDefaults(attrs, opts);
this.setDefaults(attrs, opts);
// Apply the options
this.setOptions(options, opts);
// Chain
return this;
}
/**
* Get the file name. Depending on the
* parameters passed this will either be
* the file model's filename property or,
* the filename determined from the fullPath
* or relativePath property. Valid values for
* the opts parameter are: fullPath, relativePath
* or filename. Format: {filename}
* @method getFilename
* @param {Object} [opts]
* @return {String}
*/
getFilename(opts = {}) {
var filename, fullPath, relativePath, result;
// Prepare
({fullPath, relativePath, filename} = opts);
// Determine
result = filename != null ? filename : this.get('filename');
if (!result) {
result = (fullPath != null ? fullPath : this.get('fullPath')) || (relativePath != null ? relativePath : this.get('relativePath'));
if (result) {
result = pathUtil.basename(result);
}
}
result || (result = null);
// REturn
return result;
}
/**
* Get the file path. Depending on the
* parameters passed this will either be
* the file model's fullPath property, the
* relativePath property or the filename property.
* Valid values for the opts parameter are:
* fullPath, relativePath
* or filename. Format: {fullPath}
* @method getFilePath
* @param {Object} [opts]
* @return {String}
*/
getFilePath(opts = {}) {
var filename, fullPath, relativePath, result;
// Prepare
({fullPath, relativePath, filename} = opts);
// Determine
result = (fullPath != null ? fullPath : this.get('fullPath')) || (relativePath != null ? relativePath : this.get('relativePath')) || (filename != null ? filename : this.get('filename')) || null;
// Return
return result;
}
/**
* Get file extensions. Depending on the
* parameters passed this will either be
* the file model's extensions property or
* the extensions extracted from the file model's
* filename property. The opts parameter is passed
* in the format: {extensions,filename}.
* @method getExtensions
* @param {Object} opts
* @return {Array} array of extension names
*/
getExtensions({extensions, filename}) {
extensions || (extensions = this.get('extensions') || null);
if ((extensions || []).length === 0) {
filename = this.getFilename({filename});
if (filename) {
extensions = docpadUtil.getExtensions(filename);
}
}
return extensions || null;
}
/**
* Get the file content. This will be
* the text content if loaded or the file buffer object.
* @method getContent
* @return {String or Object}
*/
getContent() {
return this.get('content') || this.getBuffer();
}
/**
* Get the file content for output.
* @method getOutContent
* @return {String or Object}
*/
getOutContent() {
return this.getContent();
}
/**
* Get the content type header for the file.
* @method getContentTypeHeader
* @return {String}
*/
getContentTypeHeader() {
var charset, contentType, contentTypeHeader, encoding, locale, parts;
locale = this.getLocale();
contentType = this.get('outContentType') || this.get('contentType');
if (!contentType) {
throw new Errlop(util.format(locale.documentMissingContentType, this.getFilePath()));
}
encoding = this.get('encoding');
parts = [contentType];
if (encoding) {
if (encoding === 'utf8' || encoding === 'utf-8') {
charset = 'utf-8';
} else {
charset = encoding;
}
parts.push(`charset=${charset}`);
}
contentTypeHeader = parts.join('; ');
return contentTypeHeader;
}
/**
* Is this a text file? ie - not
* a binary file.
* @method isText
* @return {Boolean}
*/
isText() {
return this.get('encoding') !== 'binary';
}
/**
* Is this a binary file?
* @method isBinary
* @return {Boolean}
*/
isBinary() {
return this.get('encoding') === 'binary';
}
/**
* Set the url for the file
* @method setUrl
* @param {String} url
*/
setUrl(url) {
this.addUrl(url);
this.set({url});
return this;
}
/**
* A file can have multiple urls.
* This method adds either a single url
* or an array of urls to the file model.
* @method addUrl
* @param {String or Array} url
*/
addUrl(url) {
var existingUrl, found, i, j, len, len1, newUrl, urls;
// Multiple Urls
if (url instanceof Array) {
for (i = 0, len = url.length; i < len; i++) {
newUrl = url[i];
this.addUrl(newUrl);
}
// Single Url
} else if (url) {
found = false;
urls = this.get('urls');
for (j = 0, len1 = urls.length; j < len1; j++) {
existingUrl = urls[j];
if (existingUrl === url) {
found = true;
break;
}
}
if (!found) {
urls.push(url);
}
this.trigger('change:urls', this, urls, {});
this.trigger('change', this, {});
}
return this;
}
/**
* Removes a url from the file
* model (files can have more than one url).
* @method removeUrl
* @param {Object} userUrl the url to be removed
*/
removeUrl(userUrl) {
var i, index, len, url, urls;
urls = this.get('urls');
for (index = i = 0, len = urls.length; i < len; index = ++i) {
url = urls[index];
if (url === userUrl) {
urls.splice(index, 1);
break;
}
}
return this;
}
/**
* Get a file path.
* If the relativePath parameter starts with `.` then we get the
* path in relation to the document that is calling it.
* Otherwise we just return it as normal
* @method getPath
* @param {String} relativePath
* @param {String} parentPath
* @return {String}
*/
getPath(relativePath, parentPath) {
var path, relativeDirPath;
if (/^\./.test(relativePath)) {
relativeDirPath = this.get('relativeDirPath');
path = pathUtil.join(relativeDirPath, relativePath);
} else {
if (parentPath) {
path = pathUtil.join(parentPath, relativePath);
} else {
path = relativePath;
}
}
return path;
}
/**
* Get the action runner instance bound to DocPad
* @method getActionRunner
* @return {Object}
*/
getActionRunner() {
return this.actionRunnerInstance;
}
action(...args) {
boundMethodCheck(this, FileModel);
return docpadUtil.action.apply(this, args);
}
/**
* Initialize the file model with the passed
* attributes and options. Emits the init event.
* @method initialize
* @param {Object} attrs the file model attributes
* @param {Object} [opts] the file model options
*/
initialize(attrs, opts) {
var base, base1, base2, base3, base4, file, now;
// Defaults
file = this;
if (this.attributes == null) {
this.attributes = {};
}
if ((base = this.attributes).extensions == null) {
base.extensions = [];
}
if ((base1 = this.attributes).urls == null) {
base1.urls = [];
}
now = new Date();
if ((base2 = this.attributes).ctime == null) {
base2.ctime = now;
}
if ((base3 = this.attributes).mtime == null) {
base3.mtime = now;
}
// Id
if (this.id == null) {
this.id = (base4 = this.attributes).id != null ? base4.id : base4.id = this.cid;
}
// Options
this.setOptions(opts);
// Error
if ((this.rootOutDirPath != null) === false || (this.locale != null) === false) {
throw new Errlop("Use docpad.createModel to create the file or document model");
}
// Create our action runner
this.actionRunnerInstance = this.createTaskGroup("file action runner", {
abortOnError: false,
destroyOnceDone: false
}).whenDone(function(err) {
if (err) {
return file.emit('error', err);
}
});
// Apply
this.emit('init');
return this;
}
/**
* Load the file from the file system.
* If the fullPath exists, load the file.
* If it doesn't, then parse and normalize the file.
* Optionally pass file options as a parameter.
* @method load
* @param {Object} [opts]
* @param {Function} next callback
*/
load(opts, next) {
var file, filePath, fullPath, tasks;
// Prepare
[opts, next] = extractOptsAndCallback(opts, next);
file = this;
if (opts.exists == null) {
opts.exists = null;
}
// Fetch
fullPath = this.get('fullPath');
filePath = this.getFilePath({fullPath});
if (opts.exists != null) {
// Apply options
file.set({
exists: opts.exists
});
}
if (opts.stat != null) {
file.setStat(opts.stat);
}
if (opts.buffer != null) {
file.setBuffer(opts.buffer);
}
// Tasks
tasks = this.createTaskGroup(`load tasks for file: ${filePath}`, {next}).on('item.run', function(item) {
return file.log("debug", `${item.getConfig().name}: ${file.type}: ${filePath}`);
});
// Detect the file
tasks.addTask("Detect the file", function(complete) {
if (fullPath && opts.exists === null) {
return safefs.exists(fullPath, function(exists) {
opts.exists = exists;
file.set({
exists: opts.exists
});
return complete();
});
} else {
return complete();
}
});
tasks.addTask("Stat the file and cache the result", function(complete) {
// Otherwise fetch new stat
if (fullPath && opts.exists && (opts.stat != null) === false) {
return safefs.stat(fullPath, function(err, fileStat) {
if (err) {
return complete(err);
}
file.setStat(fileStat);
return complete();
});
} else {
return complete();
}
});
// Process the file
tasks.addTask("Read the file and cache the result", function(complete) {
// Otherwise fetch new buffer
if (fullPath && opts.exists && (opts.buffer != null) === false && file.isBufferOutdated()) {
return safefs.readFile(fullPath, function(err, buffer) {
if (err) {
return complete(err);
}
file.setBuffer(buffer);
return complete();
});
} else {
return complete();
}
});
tasks.addTask("Load -> Parse", function(complete) {
return file.parse(complete);
});
tasks.addTask("Parse -> Normalize", function(complete) {
return file.normalize(complete);
});
tasks.addTask("Normalize -> Contextualize", function(complete) {
return file.contextualize(complete);
});
// Run the tasks
tasks.run();
return this;
}
/**
* Parse our buffer and extract meaningful data from it.
* next(err)
* @method parse
* @param {Object} [opts]
* @param {Object} next callback
*/
parse(opts, next) {
var buffer, changes, content, encoding, ref, relativePath, source;
// Prepare
[opts, next] = extractOptsAndCallback(opts, next);
buffer = this.getBuffer();
relativePath = this.get('relativePath');
encoding = opts.encoding || this.get('encoding') || null;
changes = {};
// Detect Encoding
if (buffer && (encoding != null) === false || opts.reencode === true) {
// Text
if (isText(relativePath, buffer)) {
// Detect source encoding if not manually specified
if (this.detectEncoding) {
if (jschardet == null) {
jschardet = require('jschardet');
}
if (encoding == null) {
encoding = (ref = jschardet.detect(buffer)) != null ? ref.encoding : void 0;
}
}
// Default the encoding
encoding || (encoding = 'utf8');
// Convert into utf8
if (docpadUtil.isStandardEncoding(encoding) === false) {
buffer = this.encode({
path: relativePath,
to: 'utf8',
from: encoding,
content: buffer
});
}
// Apply
changes.encoding = encoding;
} else {
// Set
// Binary
encoding = changes.encoding = 'binary';
}
}
// Binary
if (encoding === 'binary') {
// Set
content = source = '';
// Apply
changes.content = content;
changes.source = source;
} else {
if ((encoding != null) === false) {
// Default
// Text
encoding = changes.encoding = 'utf8';
}
// Set
source = (buffer != null ? buffer.toString('utf8') : void 0) || '';
content = source;
// Apply
changes.content = content;
changes.source = source;
}
// Apply
this.set(changes);
// Next
next();
return this;
}
/**
* Normalize any parsing we have done, because if a value has
* updates it may have consequences on another value.
* This will ensure everything is okay.
* next(err)
* @method normalize
* @param {Object} [opts]
* @param {Object} next callback
*/
normalize(opts, next) {
var _defaultUrl, basename, changes, contentType, ctime, date, err, extension, extensions, filename, fullDirPath, fullPath, locale, meta, mtime, name, outBasename, outContentType, outDirPath, outExtension, outFilename, outPath, relativeBase, relativeDirPath, relativeOutBase, relativeOutDirPath, relativeOutPath, relativePath, rtime, slug, tags, url, wtime;
// Prepare
[opts, next] = extractOptsAndCallback(opts, next);
changes = {};
meta = this.getMeta();
locale = this.getLocale();
// App specified
filename = opts.filename || this.get('filename') || null;
relativePath = opts.relativePath || this.get('relativePath') || null;
fullPath = opts.fullPath || this.get('fullPath') || null;
mtime = opts.mtime || this.get('mtime') || null;
// User specified
tags = opts.tags || meta.get('tags') || null;
date = opts.date || meta.get('date') || null;
name = opts.name || meta.get('name') || null;
slug = opts.slug || meta.get('slug') || null;
url = opts.url || meta.get('url') || null;
contentType = opts.contentType || meta.get('contentType') || null;
outContentType = opts.outContentType || meta.get('outContentType') || null;
outFilename = opts.outFilename || meta.get('outFilename') || null;
outExtension = opts.outExtension || meta.get('outExtension') || null;
outPath = opts.outPath || meta.get('outPath') || null;
// Force specifeid
extensions = null;
extension = null;
basename = null;
outBasename = null;
relativeOutPath = null;
relativeDirPath = null;
relativeOutDirPath = null;
relativeBase = null;
relativeOutBase = null;
outDirPath = null;
fullDirPath = null;
// filename
changes.filename = filename = this.getFilename({filename, relativePath, fullPath});
if (!filename) {
err = new Errlop(locale.filenameMissingError);
return next(err);
}
if (!relativePath && filename) {
changes.relativePath = relativePath = filename;
}
// force basename
changes.basename = basename = docpadUtil.getBasename(filename);
// force extensions
changes.extensions = extensions = this.getExtensions({filename});
// force extension
changes.extension = extension = docpadUtil.getExtension(extensions);
// force fullDirPath
if (fullPath) {
changes.fullDirPath = fullDirPath = docpadUtil.getDirPath(fullPath);
}
// force relativeDirPath
changes.relativeDirPath = relativeDirPath = docpadUtil.getDirPath(relativePath);
// force relativeBase
changes.relativeBase = relativeBase = relativeDirPath ? pathUtil.join(relativeDirPath, basename) : basename;
if (!contentType) {
changes.contentType = contentType = mime.getType(fullPath || relativePath);
}
// adjust tags
if (tags && typeChecker.isArray(tags) === false) {
changes.tags = tags = String(tags).split(/[\s,]+/);
}
if (!date) {
changes.date = date = mtime || this.get('date') || new Date();
}
if (!outFilename && !outPath) {
changes.outFilename = outFilename = docpadUtil.getOutFilename(basename, outExtension || extensions.join('.'));
}
if (!outPath) {
changes.outPath = outPath = this.rootOutDirPath ? pathUtil.resolve(this.rootOutDirPath, relativeDirPath, outFilename) : null;
}
// ^ we still do this set as outPath is a meta, and it may still be set as an attribute
// refresh outFilename
if (outPath) {
changes.outFilename = outFilename = docpadUtil.getFilename(outPath);
}
// force outDirPath
changes.outDirPath = outDirPath = outPath ? docpadUtil.getDirPath(outPath) : null;
// force outBasename
changes.outBasename = outBasename = docpadUtil.getBasename(outFilename);
// force outExtension
changes.outExtension = outExtension = docpadUtil.getExtension(outFilename);
// force relativeOutPath
changes.relativeOutPath = relativeOutPath = outPath ? outPath.replace(this.rootOutDirPath, '').replace(/^[\/\\]/, '') : pathUtil.join(relativeDirPath, outFilename);
// force relativeOutDirPath
changes.relativeOutDirPath = relativeOutDirPath = docpadUtil.getDirPath(relativeOutPath);
// force relativeOutBase
changes.relativeOutBase = relativeOutBase = pathUtil.join(relativeOutDirPath, outBasename);
if (!name) {
changes.name = name = outFilename;
}
// force url
_defaultUrl = docpadUtil.getUrl(relativeOutPath);
if (url) {
this.setUrl(url);
this.addUrl(_defaultUrl);
} else {
this.setUrl(_defaultUrl);
}
if (!outContentType && contentType) {
changes.outContentType = outContentType = mime.getType(outPath || relativeOutPath) || contentType;
}
if (!slug) {
changes.slug = slug = docpadUtil.getSlug(relativeOutBase);
}
if (typeof wtime === 'string') {
// Force date objects
changes.wtime = wtime = new Date(wtime);
}
if (typeof rtime === 'string') {
changes.rtime = rtime = new Date(rtime);
}
if (typeof ctime === 'string') {
changes.ctime = ctime = new Date(ctime);
}
if (typeof mtime === 'string') {
changes.mtime = mtime = new Date(mtime);
}
if (typeof date === 'string') {
changes.date = date = new Date(date);
}
// Apply
this.set(changes);
// Next
next();
return this;
}
/**
* Contextualize the data. In other words,
* put our data into the perspective of the bigger picture of the data.
* For instance, generate the url for it's rendered equivalant.
* next(err)
* @method contextualize
* @param {Object} [opts]
* @param {Object} next callback
*/
contextualize(opts, next) {
// Prepare
[opts, next] = extractOptsAndCallback(opts, next);
// Forward
next();
return this;
}
/**
* Render this file. The file model output content is
* returned to the passed callback in the
* result (2nd) parameter. The file model itself is returned
* in the callback's document (3rd) parameter.
* next(err,result,document)
* @method render
* @param {Object} [opts]
* @param {Object} next callback
*/
render(opts, next) {
var file;
// Prepare
[opts, next] = extractOptsAndCallback(opts, next);
file = this;
// Apply
file.attributes.rtime = new Date();
// Forward
next(null, file.getOutContent(), file);
return this;
}
// ---------------------------------
// CRUD
/**
* Write the out file. The out file
* may be different from the input file.
* Often the input file is transformed in some way
* and saved as another file format. A common example
* is transforming a markdown input file to a HTML
* output file.
* next(err)
* @method write
* @param {Object} opts
* @param {Function} next callback
*/
write(opts, next) {
var file, locale, ref;
// Prepare
[opts, next] = extractOptsAndCallback(opts, next);
file = this;
locale = this.getLocale();
// Fetch
opts.path || (opts.path = file.get('outPath'));
opts.encoding || (opts.encoding = file.get('encoding') || 'utf8');
opts.content || (opts.content = file.getOutContent());
opts.type || (opts.type = 'out file');
// Check
// Sometimes the out path could not be set if we are early on in the process
if (!opts.path) {
next();
return this;
}
// Convert utf8 to original encoding
if ((ref = opts.encoding.toLowerCase()) !== 'ascii' && ref !== 'utf8' && ref !== 'utf-8' && ref !== 'binary') {
opts.content = this.encode({
path: opts.path,
to: opts.encoding,
from: 'utf8',
content: opts.content
});
}
// Log
file.log('debug', util.format(locale.fileWrite, opts.type, opts.path, opts.encoding));
// Write data
safefs.writeFile(opts.path, opts.content, function(err) {
if (err) {
// Check
return next(err);
}
// Update the wtime
if (opts.type === 'out file') {
file.attributes.wtime = new Date();
}
// Log
file.log('debug', util.format(locale.fileWrote, opts.type, opts.path, opts.encoding));
// Next
return next();
});
return this;
}
/**
* Write the source file. Optionally pass
* the opts parameter to modify or set the file's
* path, content or type.
* next(err)
* @method writeSource
* @param {Object} [opts]
* @param {Object} next callback
*/
writeSource(opts, next) {
var file;
// Prepare
[opts, next] = extractOptsAndCallback(opts, next);
file = this;
// Fetch
opts.path || (opts.path = file.get('fullPath'));
opts.content || (opts.content = (file.getContent() || '').toString(''));
opts.type || (opts.type = 'source file');
// Write data
this.write(opts, next);
return this;
}
/**
* Delete the out file, perhaps ahead of regeneration.
* Optionally pass the opts parameter to set the file path or type.
* next(err)
* @method delete
* @param {Object} [opts]
* @param {Object} next callback
*/
['delete'](opts, next) {
var file, locale;
// Prepare
[opts, next] = extractOptsAndCallback(opts, next);
file = this;
locale = this.getLocale();
// Fetch
opts.path || (opts.path = file.get('outPath'));
opts.type || (opts.type = 'out file');
// Check
// Sometimes the out path could not be set if we are early on in the process
if (!opts.path) {
next();
return this;
}
// Log
file.log('debug', util.format(locale.fileDelete, opts.type, opts.path));
// Check existance
safefs.exists(opts.path, function(exists) {
if (!exists) {
// Exit if it doesn't exist
return next();
}
// If it does exist delete it
return safefs.unlink(opts.path, function(err) {
if (err) {
// Check
return next(err);
}
// Log
file.log('debug', util.format(locale.fileDeleted, opts.type, opts.path));
// Next
return next();
});
});
return this;
}
/**
* Delete the source file.
* Optionally pass the opts parameter to set the file path or type.
* next(err)
* @method deleteSource
* @param {Object} [opts]
* @param {Object} next callback
*/
deleteSource(opts, next) {
var file;
// Prepare
[opts, next] = extractOptsAndCallback(opts, next);
file = this;
// Fetch
opts.path || (opts.path = file.get('fullPath'));
opts.type || (opts.type = 'source file');
// Write data
this.delete(opts, next);
return this;
}
};
// ---------------------------------
// Properties
/**
* The file model class. This should
* be overridden in any descending classes.
* @private
* @property {Object} klass
*/
FileModel.prototype.klass = FileModel;
/**
* String name of the model type.
* In this case, 'file'. This should
* be overridden in any descending classes.
* @private
* @property {String} type
*/
FileModel.prototype.type = 'file';
/**
* Task Group creator method
* @private
* @property {Object} Function
*/
FileModel.prototype.createTaskGroup = null;
/**
* The out directory path to put the relative path.
* @property {String} rootOutDirPath
*/
FileModel.prototype.rootOutDirPath = null;
/**
* Whether or not we should detect encoding
* @property {Boolean} detectEncoding
*/
FileModel.prototype.detectEncoding = false;
/**
* Node.js file stat object.
* https://nodejs.org/api/fs.html#fs_class_fs_stats.
* Basically, information about a file, including file
* dates and size.
* @property {Object} stat
*/
FileModel.prototype.stat = null;
/**
* File buffer. Node.js Buffer object.
* https://nodejs.org/api/buffer.html#buffer_class_buffer.
* Provides methods for dealing with binary data directly.
* @property {Object} buffer
*/
FileModel.prototype.buffer = null;
/**
* Buffer time.
* @property {Object} bufferTime
*/
FileModel.prototype.bufferTime = null;
/**
* The parsed file meta data (header).
* Is a Model instance.
* @private
* @property {Object} meta
*/
FileModel.prototype.meta = null;
/**
* Locale information for the file
* @private
* @property {Object} locale
*/
FileModel.prototype.locale = null;
// ---------------------------------
// Attributes
/**
* The default attributes for any file model.
* @private
* @property {Object}
*/
FileModel.prototype.defaults = {
// ---------------------------------
// Automaticly set variables
// The unique document identifier
id: null,
// The file's name without the extension
basename: null,
// The out file's name without the extension
outBasename: null,
// The file's last extension
// "hello.md.eco" -> "eco"
extension: null,
// The extension used for our output file
outExtension: null,
// The file's extensions as an array
// "hello.md.eco" -> ["md","eco"]
extensions: null, // Array
// The file's name with the extension
filename: null,
// The full path of our source file, only necessary if called by @load
fullPath: null,
// The full directory path of our source file
fullDirPath: null,
// The output path of our file
outPath: null,
// The output path of our file's directory
outDirPath: null,
// The file's name with the rendered extension
outFilename: null,
// The relative path of our source file (with extensions)
relativePath: null,
// The relative output path of our file
relativeOutPath: null,
// The relative directory path of our source file
relativeDirPath: null,
// The relative output path of our file's directory
relativeOutDirPath: null,
// The relative base of our source file (no extension)
relativeBase: null,
// The relative base of our out file (no extension)
relativeOutBase: null,
// The MIME content-type for the source file
contentType: null,
// The MIME content-type for the out file
outContentType: null,
// The date object for when this document was created
ctime: null,
// The date object for when this document was last modified
mtime: null,
// The date object for when this document was last rendered
rtime: null,
// The date object for when this document was last written
wtime: null,
// Does the file actually exist on the file system
exists: null,
// ---------------------------------
// Content variables
// The encoding of the file
encoding: null,
// The raw contents of the file, stored as a String
source: null,
// The contents of the file, stored as a String
content: null,
// ---------------------------------
// User set variables
// The tags for this document
tags: null, // CSV/Array
// Whether or not we should render this file
render: false,
// Whether or not we should write this file to the output directory
write: true,
// Whether or not we should write this file to the source directory
writeSource: false,
// The title for this document
// Useful for page headings
title: null,
// The name for this document, defaults to the outFilename
// Useful for navigation listings
name: null,
// The date object for this document, defaults to mtime
date: null,
// The generated slug (url safe seo title) for this document
slug: null,
// The url for this document
url: null,
// Alternative urls for this document
urls: null, // Array
// Whether or not we ignore this file
ignored: false,
// Whether or not we should treat this file as standalone (that nothing depends on it)
standalone: false
};
// ---------------------------------
// Actions
/**
* The action runner instance bound to DocPad
* @private
* @property {Object} actionRunnerInstance
*/
FileModel.prototype.actionRunnerInstance = null;
return FileModel;
}).call(this);
// ---------------------------------
// Export
module.exports = FileModel;