UNPKG

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,434 lines (1,185 loc) 33.2 kB
# ===================================== # Requires # Standard 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') # ===================================== # 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 # --------------------------------- # 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 creator method # @private # @property {Object} Function ### createTaskGroup: 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, @createTaskGroup} ###* # 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', 'createTaskGroup'] 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.createTaskGroup? @createTaskGroup = attrs.createTaskGroup delete @attributes.createTaskGroup # 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 # 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 encodeError err = new Errlop( util.format(locale.fileEncodeConvertError, opts.to, opts.from, opts.path), encodeError ) @log 'warn', err else err = new Errlop( util.format(locale.fileEncodeConvertError, opts.to, opts.from, opts.path) ) @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) -> buffer = docpadUtil.newBuffer(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.dereferenceJSON(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() ###* # Get the content type header for the file. # @method getContentTypeHeader # @return {String} ### getContentTypeHeader: -> locale = @getLocale() contentType = @get('outContentType') or @get('contentType') unless contentType throw new Errlop(util.format(locale.documentMissingContentType, @getFilePath())) encoding = @get('encoding') parts = [contentType] if encoding if encoding in ['utf8', '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 @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 Errlop("Use docpad.createModel to create the file or document model") # Create our action runner @actionRunnerInstance = @createTaskGroup("file action runner", {abortOnError: false, destroyOnceDone: false}).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 = @createTaskGroup("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 # Text if isText(relativePath, buffer) # 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 Errlop(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.getType(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.getType(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