# =====================================
# Requires
# Standard Library
util = require('util')
pathUtil = require('path')
# External
isTextOrBinary = require('istextorbinary')
typeChecker = require('typechecker')
{TaskGroup} = require('taskgroup')
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')
# =====================================
# Classes
###*
# The FileModel class is DocPad's representation
# of a file in the file system.
# Extends the DocPad Model class
# https://github.com/docpad/docpad/blob/master/src/lib/base.coffee#L49.
# 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
# ---------------------------------
# Properties
###*
# The file model class. This should
# be overridden in any descending classes.
# @private
# @property {Object} klass
###
klass: FileModel
###*
# String name of the model type.
# In this case, 'file'. This should
# be overridden in any descending classes.
# @private
# @property {String} type
###
type: 'file'
###*
# Task Group Class
# @private
# @property {Object} TaskGroup
###
TaskGroup: null
###*
# The out directory path to put the relative path.
# @property {String} rootOutDirPath
###
rootOutDirPath: null
###*
# Whether or not we should detect encoding
# @property {Boolean} detectEncoding
###
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
###
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
###
buffer: null
###*
# Buffer time.
# @property {Object} bufferTime
###
bufferTime: null
###*
# The parsed file meta data (header).
# Is a Model instance.
# @private
# @property {Object} meta
###
meta: null
###*
# Locale information for the file
# @private
# @property {Object} locale
###
locale: null
###*
# Get the file's locale information
# @method getLocale
# @return {Object} the locale
###
getLocale: -> @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, @rootOutDirPath, @locale, @stat, @buffer, @meta, @TaskGroup}
###*
# Checks whether the passed key is one
# of the options.
# @private
# @method isOption
# @param {String} key
# @return {Boolean}
###
isOption: (key) ->
names = ['detectEncoding', 'rootOutDirPath', 'locale', 'stat', 'data', 'buffer', 'meta', 'TaskGroup']
result = key in names
return result
###*
# Extract Options.
# @private
# @method extractOptions
# @param {Object} attrs
# @return {Object} the options object
###
extractOptions: (attrs) ->
# Prepare
result = {}
# Extract
for own key,value of attrs
if @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.TaskGroup?
@TaskGroup = attrs.TaskGroup
delete @attributes.TaskGroup
# Root Out Path
if attrs.detectEncoding?
@rootOutDirPath = attrs.detectEncoding
delete @attributes.detectEncoding
# Root Out Path
if attrs.rootOutDirPath?
@rootOutDirPath = attrs.rootOutDirPath
delete @attributes.rootOutDirPath
# Locale
if attrs.locale?
@locale = attrs.locale
delete @attributes.locale
# Stat
if attrs.stat?
@setStat(attrs.stat)
delete @attributes.stat
# Data
if attrs.data?
@setBuffer(attrs.data)
delete @attributes.data
# Buffer
if attrs.buffer?
@setBuffer(attrs.buffer)
delete @attributes.buffer
# Meta
if attrs.meta?
@setMeta(attrs.meta)
delete @attributes.meta
# Chain
@
###*
# Clone the model and return the newly cloned model.
# @method clone
# @return {Object} cloned file model
###
clone: ->
# Fetch
attrs = @getAttributes()
opts = @getOptions()
# Clean up
delete attrs.id
delete attrs.meta.id
delete opts.meta.id
delete opts.meta.attributes.id
# Clone
clonedModel = new @klass(attrs, opts)
# Emit clone event so parent can re-attach listeners
@emit('clone', clonedModel)
# Return
return clonedModel
# ---------------------------------
# Attributes
###*
# The default attributes for any file model.
# @private
# @property {Object}
###
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
# Whether or not this file should be re-rendered on each request
dynamic: 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
# ---------------------------------
# Helpers
###*
# File encoding helper
# opts = {path, to, from, content}
# @private
# @method encode
# @param {Object} opts
# @return {Object} encoded result
###
encode: (opts) ->
# Prepare
locale = @getLocale()
result = opts.content
opts.to ?= 'utf8'
opts.from ?= 'utf8'
# Import optional dependencies
try encodingUtil ?= require('encoding')
# Convert
if encodingUtil?
@log 'info', util.format(locale.fileEncode, opts.to, opts.from, opts.path)
try
result = encodingUtil.convert(opts.content, opts.to, opts.from)
catch err
@log 'warn', util.format(locale.fileEncodeConvertError, opts.to, opts.from, opts.path)
else
@log 'warn', util.format(locale.fileEncodeConvertError, opts.to, opts.from, opts.path)
# 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) ->
buffer = new Buffer(buffer) unless Buffer.isBuffer(buffer)
@bufferTime = @get('mtime') or new Date()
@buffer = buffer
@
###*
# Get the file model's buffer object.
# Returns a node.js buffer object.
# @method getBuffer
# @return {Object} node.js buffer object
###
getBuffer: ->
return @buffer
###*
# Is Buffer Outdated
# True if there is no buffer OR the buffer time is outdated
# @method isBufferOutdated
# @return {Boolean}
###
isBufferOutdated: ->
return @buffer? is false or @bufferTime < (@get('mtime') or new Date())
###*
# Set the node.js file stat.
# @method setStat
# @param {Object} stat
###
setStat: (stat) ->
@stat = stat
@set(
ctime: new Date(stat.ctime)
mtime: new Date(stat.mtime)
)
@
###*
# Get the node.js file stat.
# @method getStat
# @return {Object} the file stat
###
getStat: ->
return @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) ->
attrs = @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) ->
data = super
data.meta = @getMeta().toJSON()
data = extendr.dereference(data) if dereference is true
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...) ->
@meta = new Model() if @meta is null
if args.length
return @meta.get(args...)
else
return @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) ->
# Check
if typeChecker.isString(attrs)
newAttrs = {}
newAttrs[attrs] = opts
return @set(newAttrs, opts)
# Prepare
attrs = attrs.toJSON?() ? attrs
# Extract options
options = @extractOptions(attrs)
# Perform the set
super(attrs, opts)
# Apply the options
@setOptions(options, opts)
# Chain
@
###*
# 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) ->
# Prepare
attrs = attrs.toJSON?() ? attrs
# Extract options
options = @extractOptions(attrs)
# Apply
super(attrs, opts)
# Apply the options
@setOptions(options, opts)
# Chain
return @
###*
# 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) ->
# Prepare
attrs = attrs.toJSON?() ? attrs
# Extract options
options = @extractOptions(attrs)
# Apply
@getMeta().set(attrs, opts)
@set(attrs, opts)
# Apply the options
@setOptions(options, opts)
# Chain
return @
###*
# 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) ->
# Prepare
attrs = attrs.toJSON?() ? attrs
# Extract options
options = @extractOptions(attrs)
# Apply
@getMeta().setDefaults(attrs, opts)
@setDefaults(attrs, opts)
# Apply the options
@setOptions(options, opts)
# Chain
return @
###*
# 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={}) ->
# Prepare
{fullPath,relativePath,filename} = opts
# Determine
result = (filename ? @get('filename'))
if !result
result = (fullPath ? @get('fullPath')) or (relativePath ? @get('relativePath'))
result = pathUtil.basename(result) if result
result or= 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={}) ->
# Prepare
{fullPath,relativePath,filename} = opts
# Determine
result = (fullPath ? @get('fullPath')) or (relativePath ? @get('relativePath')) or (filename ? @get('filename')) or 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 or= @get('extensions') or null
if (extensions or []).length is 0
filename = @getFilename({filename})
if filename
extensions = docpadUtil.getExtensions(filename)
return extensions or 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 @get('content') or @getBuffer()
###*
# Get the file content for output.
# @method getOutContent
# @return {String or Object}
###
getOutContent: ->
return @getContent()
###*
# Is this a text file? ie - not
# a binary file.
# @method isText
# @return {Boolean}
###
isText: ->
return @get('encoding') isnt 'binary'
###*
# Is this a binary file?
# @method isBinary
# @return {Boolean}
###
isBinary: ->
return @get('encoding') is 'binary'
###*
# Set the url for the file
# @method setUrl
# @param {String} url
###
setUrl: (url) ->
@addUrl(url)
@set({url})
@
###*
# 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) ->
# Multiple Urls
if url instanceof Array
for newUrl in url
@addUrl(newUrl)
# Single Url
else if url
found = false
urls = @get('urls')
for existingUrl in urls
if existingUrl is url
found = true
break
urls.push(url) if not found
@trigger('change:urls', @, urls, {})
@trigger('change', @, {})
# Chain
@
###*
# 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) ->
urls = @get('urls')
for url,index in urls
if url is userUrl
urls.splice(index,1)
break
@
###*
# 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) ->
if /^\./.test(relativePath)
relativeDirPath = @get('relativeDirPath')
path = pathUtil.join(relativeDirPath, relativePath)
else
if parentPath
path = pathUtil.join(parentPath, relativePath)
else
path = relativePath
return path
# ---------------------------------
# Actions
###*
# The action runner instance bound to DocPad
# @private
# @property {Object} actionRunnerInstance
###
actionRunnerInstance: null
###*
# Get the action runner instance bound to DocPad
# @method getActionRunner
# @return {Object}
###
getActionRunner: -> @actionRunnerInstance
###*
# Apply an action with the supplied arguments.
# @method action
# @param {Object} args...
###
action: (args...) => docpadUtil.action.apply(@, 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={}) ->
# Defaults
file = @
@attributes ?= {}
@attributes.extensions ?= []
@attributes.urls ?= []
now = new Date()
@attributes.ctime ?= now
@attributes.mtime ?= now
# Id
@id ?= @attributes.id ?= @cid
# Options
@setOptions(opts)
# Error
if @rootOutDirPath? is false or @locale? is false
throw new Error("Use docpad.createModel to create the file or document model")
# Create our action runner
@actionRunnerInstance = new @TaskGroup("file action runner").whenDone (err) ->
file.emit('error', err) if err
# Apply
@emit('init')
# Chain
@
###*
# 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) ->
# Prepare
[opts,next] = extractOptsAndCallback(opts,next)
file = @
opts.exists ?= null
# Fetch
fullPath = @get('fullPath')
filePath = @getFilePath({fullPath})
# Apply options
file.set(exists: opts.exists) if opts.exists?
file.setStat(opts.stat) if opts.stat?
file.setBuffer(opts.buffer) if opts.buffer?
# Tasks
tasks = new @TaskGroup("load tasks for file: #{filePath}", {next})
.on('item.run', (item) ->
file.log("debug", "#{item.getConfig().name}: #{file.type}: #{filePath}")
)
# Detect the file
tasks.addTask "Detect the file", (complete) ->
if fullPath and opts.exists is null
safefs.exists fullPath, (exists) ->
opts.exists = exists
file.set(exists: opts.exists)
return complete()
else
return complete()
tasks.addTask "Stat the file and cache the result", (complete) ->
# Otherwise fetch new stat
if fullPath and opts.exists and opts.stat? is false
return safefs.stat fullPath, (err,fileStat) ->
return complete(err) if err
file.setStat(fileStat)
return complete()
else
return complete()
# Process the file
tasks.addTask "Read the file and cache the result", (complete) ->
# Otherwise fetch new buffer
if fullPath and opts.exists and opts.buffer? is false and file.isBufferOutdated()
return safefs.readFile fullPath, (err,buffer) ->
return complete(err) if err
file.setBuffer(buffer)
return complete()
else
return complete()
tasks.addTask "Load -> Parse", (complete) ->
file.parse(complete)
tasks.addTask "Parse -> Normalize", (complete) ->
file.normalize(complete)
tasks.addTask "Normalize -> Contextualize", (complete) ->
file.contextualize(complete)
# Run the tasks
tasks.run()
# Chain
@
###*
# Parse our buffer and extract meaningful data from it.
# next(err).
# @method parse
# @param {Object} [opts={}]
# @param {Object} next callback
###
parse: (opts={},next) ->
# Prepare
[opts,next] = extractOptsAndCallback(opts, next)
buffer = @getBuffer()
relativePath = @get('relativePath')
encoding = opts.encoding or @get('encoding') or null
changes = {}
# Detect Encoding
if buffer and encoding? is false or opts.reencode is true
isText = isTextOrBinary.isTextSync(relativePath, buffer)
# Text
if isText is true
# Detect source encoding if not manually specified
if @detectEncoding
jschardet ?= require('jschardet')
encoding ?= jschardet.detect(buffer)?.encoding
# Default the encoding
encoding or= 'utf8'
# Convert into utf8
if docpadUtil.isStandardEncoding(encoding) is false
buffer = @encode({
path: relativePath
to: 'utf8'
from: encoding
content: buffer
})
# Apply
changes.encoding = encoding
# Binary
else
# Set
encoding = changes.encoding = 'binary'
# Binary
if encoding is 'binary'
# Set
content = source = ''
# Apply
changes.content = content
changes.source = source
# Text
else
# Default
encoding = changes.encoding = 'utf8' if encoding? is false
# Set
source = buffer?.toString('utf8') or ''
content = source
# Apply
changes.content = content
changes.source = source
# Apply
@set(changes)
# Next
next()
@
###*
# 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) ->
# Prepare
[opts,next] = extractOptsAndCallback(opts,next)
changes = {}
meta = @getMeta()
locale = @getLocale()
# App specified
filename = opts.filename or @get('filename') or null
relativePath = opts.relativePath or @get('relativePath') or null
fullPath = opts.fullPath or @get('fullPath') or null
mtime = opts.mtime or @get('mtime') or null
# User specified
tags = opts.tags or meta.get('tags') or null
date = opts.date or meta.get('date') or null
name = opts.name or meta.get('name') or null
slug = opts.slug or meta.get('slug') or null
url = opts.url or meta.get('url') or null
contentType = opts.contentType or meta.get('contentType') or null
outContentType = opts.outContentType or meta.get('outContentType') or null
outFilename = opts.outFilename or meta.get('outFilename') or null
outExtension = opts.outExtension or meta.get('outExtension') or null
outPath = opts.outPath or meta.get('outPath') or 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 = @getFilename({filename, relativePath, fullPath})
# check
if !filename
err = new Error(locale.filenameMissingError)
return next(err)
# relativePath
if !relativePath and filename
changes.relativePath = relativePath = filename
# force basename
changes.basename = basename = docpadUtil.getBasename(filename)
# force extensions
changes.extensions = extensions = @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 =
if relativeDirPath
pathUtil.join(relativeDirPath, basename)
else
basename
# force contentType
if !contentType
changes.contentType = contentType = mime.lookup(fullPath or relativePath)
# adjust tags
if tags and typeChecker.isArray(tags) is false
changes.tags = tags = String(tags).split(/[\s,]+/)
# force date
if !date
changes.date = date = mtime or @get('date') or new Date()
# force outFilename
if !outFilename and !outPath
changes.outFilename = outFilename = docpadUtil.getOutFilename(basename, outExtension or extensions.join('.'))
# force outPath
if !outPath
changes.outPath = outPath =
if @rootOutDirPath
pathUtil.resolve(@rootOutDirPath, relativeDirPath, outFilename)
else
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 =
if outPath
docpadUtil.getDirPath(outPath)
else
null
# force outBasename
changes.outBasename = outBasename = docpadUtil.getBasename(outFilename)
# force outExtension
changes.outExtension = outExtension = docpadUtil.getExtension(outFilename)
# force relativeOutPath
changes.relativeOutPath = relativeOutPath =
if outPath
outPath.replace(@rootOutDirPath, '').replace(/^[\/\\]/, '')
else
pathUtil.join(relativeDirPath, outFilename)
# force relativeOutDirPath
changes.relativeOutDirPath = relativeOutDirPath = docpadUtil.getDirPath(relativeOutPath)
# force relativeOutBase
changes.relativeOutBase = relativeOutBase = pathUtil.join(relativeOutDirPath, outBasename)
# force name
if !name
changes.name = name = outFilename
# force url
_defaultUrl = docpadUtil.getUrl(relativeOutPath)
if url
@setUrl(url)
@addUrl(_defaultUrl)
else
@setUrl(_defaultUrl)
# force outContentType
if !outContentType and contentType
changes.outContentType = outContentType = mime.lookup(outPath or relativeOutPath) or contentType
# force slug
if !slug
changes.slug = slug = docpadUtil.getSlug(relativeOutBase)
# Force date objects
changes.wtime = wtime = new Date(wtime) if typeof wtime is 'string'
changes.rtime = rtime = new Date(rtime) if typeof rtime is 'string'
changes.ctime = ctime = new Date(ctime) if typeof ctime is 'string'
changes.mtime = mtime = new Date(mtime) if typeof mtime is 'string'
changes.date = date = new Date(date) if typeof date is 'string'
# Apply
@set(changes)
# Next
next()
@
###*
# 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()
@
###*
# 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) ->
# Prepare
[opts,next] = extractOptsAndCallback(opts, next)
file = @
# Apply
file.attributes.rtime = new Date()
# Forward
next(null, file.getOutContent(), file)
@
# ---------------------------------
# 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) ->
# Prepare
[opts,next] = extractOptsAndCallback(opts, next)
file = @
locale = @getLocale()
# Fetch
opts.path or= file.get('outPath')
opts.encoding or= file.get('encoding') or 'utf8'
opts.content or= file.getOutContent()
opts.type or= 'out file'
# Check
# Sometimes the out path could not be set if we are early on in the process
unless opts.path
next()
return @
# Convert utf8 to original encoding
unless opts.encoding.toLowerCase() in ['ascii','utf8','utf-8','binary']
opts.content = @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, (err) ->
# Check
return next(err) if err
# Update the wtime
if opts.type is 'out file'
file.attributes.wtime = new Date()
# Log
file.log 'debug', util.format(locale.fileWrote, opts.type, opts.path, opts.encoding)
# Next
return next()
# Chain
@
###*
# 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) ->
# Prepare
[opts,next] = extractOptsAndCallback(opts, next)
file = @
# Fetch
opts.path or= file.get('fullPath')
opts.content or= (file.getContent() or '').toString('')
opts.type or= 'source file'
# Write data
@write(opts, next)
# Chain
@
###*
# 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) ->
# Prepare
[opts,next] = extractOptsAndCallback(opts, next)
file = @
locale = @getLocale()
# Fetch
opts.path or= file.get('outPath')
opts.type or= 'out file'
# Check
# Sometimes the out path could not be set if we are early on in the process
unless opts.path
next()
return @
# Log
file.log 'debug', util.format(locale.fileDelete, opts.type, opts.path)
# Check existance
safefs.exists opts.path, (exists) ->
# Exit if it doesn't exist
return next() unless exists
# If it does exist delete it
safefs.unlink opts.path, (err) ->
# Check
return next(err) if err
# Log
file.log 'debug', util.format(locale.fileDeleted, opts.type, opts.path)
# Next
next()
# Chain
@
###*
# 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) ->
# Prepare
[opts,next] = extractOptsAndCallback(opts, next)
file = @
# Fetch
opts.path or= file.get('fullPath')
opts.type or= 'source file'
# Write data
@delete(opts, next)
# Chain
@
# ---------------------------------
# Export
module.exports = FileModel