API Docs for: 0.1.0

lib/builder.js

var markdown = require("node-markdown").Markdown,
    fs = require('fs'),
    handlebars = require('./handlebars').Handlebars,
    noop = function() {},
    path = require('path'),
    TEMPLATE;


/**
* Takes the JSON data from the DocParser class, creates and parses markdown and handlebars
based templates to generate static HTML content
* @class DocBuilder
* @module yuidoc
*/

YUI.add('doc-builder', function(Y) {

    var print = function(items) {
        var out = '<ul>';
        
        Y.each(items, function(i, k) {
            out += '<li>';
            if (Y.Lang.isObject(i)) {
                if (!i.path) {
                    out += k + '/' + print(i);
                } else {
                    out += '<a href="../files/' + i.name + '.html">' + k + '</a>';
                }
            }
            out += '</li>';
        });

        out += '</ul>';
        return out;
    };

    handlebars.registerHelper('buildFileTree', function(items, fn) {
        return print(items);
    });


    var themeDir = path.join(__dirname, '../', 'themes', 'default');

    Y.DocBuilder = function(options, data) {
        this.options = options;
        if (options.themeDir) {
            themeDir = options.themeDir;
        }
        this.data = data;
        Y.log('Building..', 'info', 'builder');
        this.files = 0;
        var self = this;

        handlebars.registerHelper('crossLink', function(item, fn) {
            var str = '';
            if (!item) {
                item = '';
            }
            if (item.indexOf('|') > 0) {
                var parts = item.split('|'),
                p = [];
                Y.each(parts, function(i) {
                    p.push(self._parseCrossLink.call(self, i));
                });
                str = p.join(' | ');
            } else {
                str = self._parseCrossLink.call(self, item);
            }
            return str;
        });

    };

    Y.DocBuilder.prototype = {
        _parseCrossLink: function(item) {
            var self = this;
            var base = '../',
                newWin = false,
                className = 'crosslink';


            item = Y.Lang.trim(item.replace('{', '').replace('}', ''));
            var link = false;
            if (self.data.classes[item]) {
                link = true;
            } else {
                if (self.data.classes[item.replace('.', '')]) {
                    link = true;
                    item = item.replace('.', '');
                }
            }
            if (!link && self.options.externalData) {
                var d = self.options.externalData;
                if (d.classes[item]) {
                    base = d.base;
                    className += ' external';
                    newWin = true;
                    link = true;
                }
            }
            if (item === 'Object' || item === 'Array') {
                link = false;
            }
            var href = path.join(base, 'classes', item + '.html');
            if (base.match(/^https?:\/\//)) {
                href = base + path.join('classes', item + '.html');
            }
            if (!link && self.options.linkNatives) {
                if (self.NATIVES && self.NATIVES[item]) {
                    href = self.NATIVES_LINKER(item);
                    if (href) {
                        className += ' external';
                        newWin = true;
                        link = true;
                    }
                }
            }
            if (link) {
                item = '<a href="' + href + '" class="' + className + '"' + ((newWin) ? ' target="_blank"' : '') + '>' + item + '</a>';
            }
            return item;
        },
        NATIVES: {
            'Array': 1,
            'Boolean': 1,
            'Date': 1,
            'decodeURI': 1,
            'decodeURIComponent': 1,
            'encodeURI': 1,
            'encodeURIComponent': 1,
            'eval': 1,
            'Error': 1,
            'EvalError': 1,
            'Function': 1,
            'Infinity': 1,
            'isFinite': 1,
            'isNaN': 1,
            'Math': 1,
            'NaN': 1,
            'Number': 1,
            'Object': 1,
            'parseFloat': 1,
            'parseInt': 1,
            'RangeError': 1,
            'ReferenceError': 1,
            'RegExp': 1,
            'String': 1,
            'SyntaxError': 1,
            'TypeError': 1,
            'undefined': 1,
            'URIError': 1
        },
        NATIVES_LINKER: function(name) {
            return 'https:/'+'/developer.mozilla.org/en/JavaScript/Reference/Global_Objects/' + name;
        },
        _mixExternal: function(err, data) {
            var self = this;
            Y.log('External data received, mixing', 'warn', 'builder');
            self.options.externalData = data;
            /* TODO
            var mixData = [];
            Y.each(self.data.classes, function(i) {
                if (i.extends) {
                    console.error(i.name, 'extends', i.extends);
                    if (!self.data.classes[i.extends]) {
                        console.error('Extended Class not found in local data, checking external');
                        if (data.classes[i.extends]) {
                            console.error('Found extended class in external data');
                            self.data.classes[i.extends] = data.classes[i.extends];
                            Y.each(data.classitems, function(o, k) {
                                if (o.class === i.extends) {
                                    self.data.classitems.push(data.classitems[k]);
                                }
                            });
                            Y.each(data.files, function(o, k) {
                                if (i.extends in o.classes) {
                                    o.path = path.join(data.base, o.name);
                                    self.data.files[k] = o;
                                }
                            });
                        }
                    }
                }
            });
            */
        },
        mixExternal: function(cb) {
            var self = this,
                info = self.options.external;

            if (info) {
                Y.log('External data support is not implemented yet.', 'warn', 'builder');
            } else {
                cb();
                return;
            }
            if (!info.merge) {
                info.merge = 'mix';
            }
            if (!info.data) {
                Y.log('External config found but no data path defined, skipping import.', 'warn', 'builder');
                return;
            }
            if (info.data.match(/^https?:\/\//)) {
                info.base = info.data.replace('data.json', '');
                Y.use('io', function() {
                    Y.io(info.data, {
                        on: {
                            complete: function(id, e) {
                                var data = JSON.parse(e.responseText);
                                data.base = info.base;
                                self._mixExternal(null, data);
                                cb();
                            }
                        }
                    });
                });
            } else {
                info.base = path.dirname(path.resolve(info.data));
                Y.log('Local data mixing is not implemented yet', 'error', 'builder');
                var data = Y.Files.getJSON(info.data);
                data.base = info.base;
                self._mixExternal(null, data);
                cb();
            }
        },
        /**
        * The default tags to use in params descriptions (for Markdown).
        * @property defaultTags
        * @type String
        */
        defaultTags: 'code|em|strong|span|a|pre|dl|dd|dt',
        /**
        * File counter
        * @property files
        * @type Number
        */
        files: null,
        /**
        * Prep the meta data to be fed to Selleck
        * @method getProjectMeta
        * @return {Object} The project metadata
        */
        getProjectMeta: function() {
            var obj = {
                meta: {
                    yuiSeedUrl: 'http://yui.yahooapis.com/3.3.0/build/yui/yui-min.js',
                    yuiGridsUrl: 'http://yui.yahooapis.com/3.3.0/build/cssgrids/grids-min.css'
                }
            };
            Y.each(this.data.project, function(v, k) {
                var key = k.substring(0, 1).toUpperCase() + k.substring(1, k.length);
                obj.meta['project' + key] = v;
            });
            return obj
        },
        /**
        * Populate the meta data for classes
        * @method populateClasses
        * @param {Object} opts The original options
        * @return {Object} The modified options
        */
        populateClasses: function(opts) {
            opts.meta.classes = [];
            Y.each(this.data.classes, function(v) {
                opts.meta.classes.push({ displayName: v.name, name: v.name});
            });
            opts.meta.classes.sort(this.nameSort);
            return opts;
        },
        /**
        * Populate the meta data for modules
        * @method populateModules
        * @param {Object} opts The original options
        * @return {Object} The modified options
        */
        populateModules: function(opts) {
            var self = this;
            opts.meta.modules = [];
            Y.each(this.data.modules, function(v) {
                if (!v.is_submodule) {
                    var o = { displayName: v.name, name: v.name };
                    if (v.submodules) {
                        o.submodules = [];
                        Y.each(v.submodules, function(i, k) {
                            o.submodules.push({ displayName: k, description: self.data.modules[k].description });
                        });
                    }
                    opts.meta.modules.push(o);
                }
            });
            return opts;
        },
        /**
        * Populate the meta data for files
        * @method populateFiles
        * @param {Object} opts The original options
        * @return {Object} The modified options
        */
        populateFiles: function(opts) {
            var self = this;
            opts.meta.files = [];
            Y.each(this.data.files, function(v) {
                opts.meta.files.push({ displayName: v.name, name: self.filterFileName(v.name), path: v.path || v.name });
            });

            var tree = {};
            var files = [];
            Y.each(this.data.files, function(v) {
                files.push(v.name);
            });
            files.sort();
            Y.each(files, function(v) {
                var p = v.split('/'),
                    par;
                p.forEach(function(i, k) {
                    if (!par) {
                        if (!tree[i]) {
                            tree[i] = {};
                        }
                        par = tree[i];
                    } else {
                        if (!par[i]) {
                            par[i] = {};
                        }
                        if (k + 1 === p.length) {
                            par[i] = {
                                path: v,
                                name: self.filterFileName(v)
                            };
                        }
                        par = par[i];
                    }
                });
            });

            opts.meta.fileTree = tree;

            return opts;
        },
        addFoundAt: function(a) {
            var self = this;
            if (a.file && a.line) {
                a.foundAt = 'files/' + self.filterFileName(a.file) + '.html#LINE_' + a.line;
            }
            return a;
        },
        /**
        * Augments the **DocParser** meta data to provide default values for certain keys as well as parses all descriptions
        * with the Markdown Parser
        * @method augmentData
        * @param {Object} o The object to recurse and augment
        * @return {Object} The augmented object
        */
        augmentData: function(o) {
            var self = this;
            o = self.addFoundAt(o);
            Y.each(o, function(i, k1) {
                if (i && i.forEach) {
                    Y.each(i, function(a, k) {
                        if (!(a instanceof Object)) {
                            return;
                        }
                        if (!a.type) {
                            a.type = 'Object'; //Default type is Object
                        }
                        if (a.final === '') {
                            a.final = true;
                        }
                        if (!a.description) {
                            a.description = ' ';
                        } else {
                            a.description = markdown(a.description, true, self.defaultTags);
                        }
                        if (a.example) {
                            a.example = markdown(a.example, true, self.defaultTags);
                        }
                        a = self.addFoundAt(a);

                        Y.each(a, function(c, d) {
                            if (c.forEach || (c instanceof Object)) {
                                c = self.augmentData(c);
                                a[d] = c;
                            }
                        });

                        o[k1][k] = a;
                    });
                } else if (i instanceof Object) {
                    i = self.addFoundAt(i);
                    Y.each(i, function(v, k) {
                        if (k === 'final') {
                            o[k1][k] = true;
                        }
                        if (k === 'description' || k === 'example') {
                            o[k1][k] = markdown(v, true, self.defaultTags);
                        }
                    });
                } else if (k1 === 'description' || k1 === 'example') {
                    o[k1] = markdown(i, true, self.defaultTags);
                }
            });
            return o;
        },
        /**
        * Makes the default directories needed 
        * @method makeDirs
        * @param {Callback} cb The callback to execute after it's completed
        */
        makeDirs: function(cb) {
            var self = this;
            var dirs = ['classes', 'modules', 'files'];
            var stack = new Y.Parallel();
            Y.log('Making default directories: ' + dirs.join(','), 'info', 'builder');
            dirs.forEach(function(d) {
                var dir = path.join(self.options.outdir, d);
                path.exists(dir, stack.add(function(x) {
                    if (!x) {
                        fs.mkdir(dir, 0777, stack.add(noop));
                    }
                }));
            });
            stack.done(function() {
                if (cb) {
                    cb();
                }
            });
        },
        /**
        * Parses &lt;pre&gt;&lt;code&gt; tags and adds the __prettyprint__ className to them
        * @method _parseCode
        * @private
        * @param {HTML} html The HTML to parse
        * @return {HTML} The parsed HTML
        */
        _parseCode: function (html) {
            html = html.replace(/<pre><code>/g, '<pre class="code"><code class="prettyprint">');
            return html;
        },
        /**
        * Ported from [Selleck](https://github.com/rgrove/selleck), this handles `'s in fields
        that are not parsed by the **Markdown** parser.
        * @method _inlineCode
        * @private
        * @param {HTML} html The HTML to parse
        * @return {HTML} The parsed HTML
        */
        _inlineCode: function(html) {
            html = html.replace(/\`/g, '`');
            
            html = html.replace(/(.+?)/g, function (match, code) {
                return '<code>' + Y.escapeHTML(code) + '</code>';
            });

            html = html.replace(/__\{\{SELLECK_BACKTICK\}\}__/g, '`');

            return html;
        },
        /**
        * Ported from [Selleck](https://github.com/rgrove/selleck)  
        Renders the handlebars templates with the default View class.
        * @method render
        * @param {HTML} source The default template to parse
        * @param {Class} view The default view handler
        * @param {HTML} [layout=null] The HTML from the layout to use.
        * @param {Object} [partials=object] List of partials to include in this template
        * @param {Callback} callback
        * @param {Error} callback.err
        * @param {HTML} callback.html The assembled template markup
        */
        render: function(source, view, layout, partials, callback) {
            var html = [];

            function buffer(line) {
                html.push(line);
            }

            // Allow callback as third or fourth param.
            if (typeof partials === 'function') {
                callback = partials;
                partials = {};
            } else if (typeof layout === 'function') {
                callback = layout;
                layout = null;
            }
            var parts = Y.merge(partials || {}, { layout_content: source });
            Y.each(parts, function(source, name) {
                handlebars.registerPartial(name, source);
            });

            if (!TEMPLATE) {
                TEMPLATE = handlebars.compile(layout);
            }
            
            
            var _v = {};
            for (var k in view) {
                if (Y.Lang.isFunction(view[k])) {
                    _v[k] = view[k]();
                } else {
                    _v[k] = view[k];
                }
            };
            html = TEMPLATE(_v);
            
            html = this._inlineCode(html);
            callback(null, html);
        },
        /**
        * Generates the index.html file
        * @method writeIndex
        * @param {Callback} cb The callback to execute after it's completed
        */
        writeIndex: function(cb) {
            var self = this;
            Y.prepare(themeDir, self.getProjectMeta(), function(err, opts) {
                Y.log('Preparing index.html', 'info', 'builder');

                //opts.meta.htmlTitle = self.data.project.name;
                opts.meta.title = self.data.project.name;
                opts.meta.projectRoot = './';
                opts.meta.projectAssets = './assets';

                opts = self.populateClasses(opts);
                opts = self.populateModules(opts);

                var view   = new Y.DocView(opts.meta);
                self.render('{{>index}}', view, opts.layouts.main, opts.partials, function(err, html) {
                    Y.log('Writing index.html', 'info', 'builder');
                    self.files++;
                    Y.Files.writeFile(path.join(self.options.outdir, 'index.html'), html, cb);
                });
            });
        },
        /**
        * Generates the module files under "out"/modules/
        * @method writeModules
        * @param {Callback} cb The callback to execute after it's completed
        */
        writeModules: function(cb) {
            var self = this;
            var stack = new Y.Parallel();
            Y.log('Writing (' + Object.keys(self.data.modules).length + ') module pages', 'info', 'builder');
            Y.each(this.data.modules, function(v) {
                Y.prepare(themeDir, self.getProjectMeta(), function(err, opts) {
                    //opts.meta.htmlTitle = v.name + ': ' + self.data.project.name;
                    opts.meta.title = self.data.project.name;

                    opts.meta.moduleName = v.name;
                    opts.meta.moduleDescription = self._parseCode(markdown(v.description || ' '));
                    opts.meta.file = v.file;
                    opts.meta.line = v.line;
                    opts.meta = self.addFoundAt(opts.meta);
                    opts.meta.projectRoot = '../';
                    opts.meta.projectAssets = '../assets';

                    opts = self.populateClasses(opts);
                    opts = self.populateModules(opts);
                    opts = self.populateFiles(opts);

                    if (v.classes && Object.keys(v.classes).length) {
                        opts.meta.moduleClasses = [];
                        Y.each(Object.keys(v.classes), function(name) {
                            var i = self.data.classes[name];
                            if (i) {
                                opts.meta.moduleClasses.push({ name: i.name, displayName: i.name });
                            }
                        });
                        opts.meta.moduleClasses.sort(self.nameSort);
                    }
                    if (v.submodules && Object.keys(v.submodules).length) {
                        opts.meta.subModules = [];
                        Y.each(Object.keys(v.submodules), function(name) {
                            var i = self.data.modules[name];
                            opts.meta.subModules.push({ name: i.name, displayName: i.name, description: i.description });
                        });
                        opts.meta.subModules.sort(self.nameSort);
                    }

                    var view   = new Y.DocView(opts.meta);
                    self.render('{{>module}}', view, opts.layouts.main, opts.partials, stack.add(function(err, html) {
                        self.files++;
                        Y.Files.writeFile(path.join(self.options.outdir, 'modules', v.name + '.html'), html, stack.add(noop));
                    }));
                });
            });

            stack.done(function() {
                Y.log('Finished writing module files', 'info', 'builder');
                cb();
            });
        },
        hasProperty: function(a, b) {
            var other;
            var h = Y.some(a, function(i, k) {
                if ((i.itemtype === b.itemtype) && (i.name === b.name)) {
                    other = k;
                    return true;
                }
            });
            return other;
        },
        mergeExtends: function(info, classItems) {
            var self = this;
            if (info.extends || info.uses) {
                var hasItems = {};
                hasItems[info.extends] = 1;
                if (info.uses) {
                    info.uses.forEach(function(v) {
                        hasItems[v] = 1;
                    });
                }
                self.data.classitems.forEach(function(v) {
                    //console.error(v.class, '==', info.extends);
                    if (hasItems[v.class]) {
                        if (!v.static) {
                            var override = self.hasProperty(classItems, v);
                            if (!override) {
                                //This method was extended from the parent class but not over written
                                //console.error('Merging extends from', v.class, 'onto', info.name);
                                var q = Y.merge({}, v);
                                q.extended_from = v.class;
                                classItems.push(q);
                            } else {
                                //This method was extended from the parent and overwritten in this class
                                var q = Y.merge({}, v);
                                q = self.augmentData(q);
                                classItems[override].overwritten_from = q;
                            }
                        }
                    }
                });
                if (self.data.classes[info.extends]) {
                    if (self.data.classes[info.extends].extends || self.data.classes[info.extends].uses) {
                        //console.error('Stepping down to:', self.data.classes[info.extends]);
                        classItems = self.mergeExtends(self.data.classes[info.extends], classItems);
                    }
                }
            }
            return classItems;
        },
        /**
        * Generates the class files under "out"/classes/
        * @method writeClasses
        * @param {Callback} cb The callback to execute after it's completed
        */
        writeClasses: function(cb) {
            var self = this;
            var stack = new Y.Parallel();
            
            Y.log('Writing (' + Object.keys(self.data.classes).length + ') class pages', 'info', 'builder');
            Y.each(self.data.classes, function(v) {
                Y.prepare(themeDir, self.getProjectMeta(), function(err, opts) {
                    //console.log(opts);
                    if (err) {
                        console.log(err);
                    }
                    opts.meta.title = self.data.project.name;
                    opts.meta.moduleName = v.name;
                    opts.meta.file = v.file;
                    opts.meta.line = v.line;
                    opts.meta = self.addFoundAt(opts.meta);
                    opts.meta.projectRoot = '../';
                    opts.meta.projectAssets = '../assets';

                    opts = self.populateClasses(opts);
                    opts = self.populateModules(opts);
                    opts = self.populateFiles(opts);

                    opts.meta.classDescription = self._parseCode(markdown(v.description || ' '));

                    opts.meta.methods = [];
                    opts.meta.properties = [];
                    opts.meta.attrs = [];
                    opts.meta.events = [];
                    if (v.uses) {
                        opts.meta.uses = v.uses;
                    }
                    if (v.extends) {
                        opts.meta.extends = v.extends;
                    }

                    var classItems = [];
                    self.data.classitems.forEach(function(i) {
                        if (i.class === v.name) {
                            classItems.push(i);
                        }
                    });

                    classItems = self.mergeExtends(v, classItems);

                    if (v.is_constructor) {
                        var i = Y.mix({}, v);
                        i = self.augmentData(i);
                        i.paramsList = [];
                        if (i.params) {
                            i.params.forEach(function(p, v) {
                                var name = p.name;
                                if (p.optional) {
                                    name = '[' + name + ((p.optdefault) ? '=' + p.optdefault : '') + ']'
                                }
                                i.paramsList.push(name);
                            });
                        }
                        //i.methodDescription = self._parseCode(markdown(i.description));
                        i.hasAccessType = i.access;
                        i.hasParams = i.paramsList.length;
                        if (i.paramsList.length) {
                            i.paramsList = i.paramsList.join(', ');
                        } else {
                            i.paramsList = ' ';
                        }
                        i.returnType = ' ';
                        if (i.return) {
                            i.hasReturn = true;
                            i.returnType = i.return.type;
                        }
                        //console.error(i);
                        opts.meta.is_constructor = [i]; 
                    }

                    classItems.forEach(function(i) {
                        switch (i.itemtype) {
                            case 'method':
                                i = self.augmentData(i);
                                i.paramsList = [];
                                if (i.params) {
                                    i.params.forEach(function(p, v) {
                                        var name = p.name;
                                        if (p.optional) {
                                            name = '[' + name + ((p.optdefault) ? '=' + p.optdefault : '') + ']'
                                        }
                                        i.paramsList.push(name);
                                    });
                                }
                                i.methodDescription = self._parseCode(markdown(i.description));
                                if (i.example && i.example.length) {
                                    if (i.example.forEach) {
                                        var e = '';
                                        i.example.forEach(function(v) {
                                            e += self._parseCode(markdown(v));
                                        });
                                        i.example = e;
                                    } else {
                                        i.example = self._parseCode(markdown(i.example));
                                    }
                                }
                                i.hasAccessType = i.access;
                                i.hasParams = i.paramsList.length;
                                if (i.paramsList.length) {
                                    i.paramsList = i.paramsList.join(', ');
                                } else {
                                    i.paramsList = ' ';
                                }
                                i.returnType = ' ';
                                if (i.return) {
                                    i.hasReturn = true;
                                    i.returnType = i.return.type;
                                }
                                opts.meta.methods.push(i);
                                break;
                            case 'property':
                                i = self.augmentData(i);
                                i.propertyDescription = self._parseCode(markdown(i.description));
                                if (!i.type) {
                                    i.type = 'unknown';
                                }
                                if (i.final === '') {
                                    i.final = true;
                                }
                                opts.meta.properties.push(i);
                                break;
                            case 'attribute':
                                i = self.augmentData(i);
                                i.emit = self.options.attributesEmit;
                                i.attrDescription = self._parseCode(markdown(i.description));
                                opts.meta.attrs.push(i);
                                break;
                            case 'event':
                                i = self.augmentData(i);
                                i.eventDescription = self._parseCode(markdown(i.description));
                                opts.meta.events.push(i);
                                break;
                        }
                    });
                    opts.meta.methods.sort(self.nameSort);
                    opts.meta.properties.sort(self.nameSort);

                    if (!opts.meta.methods.length) {
                        delete opts.meta.methods;
                    }
                    if (!opts.meta.properties.length) {
                        delete opts.meta.properties;
                    }
                    if (!opts.meta.attrs.length) {
                        delete opts.meta.attrs;
                    }
                    if (!opts.meta.events.length) {
                        delete opts.meta.events;
                    }

                    var view   = new Y.DocView(opts.meta);
                    self.render('{{>classes}}', view, opts.layouts.main, opts.partials, stack.add(function(err, html) {
                        self.files++;
                        Y.Files.writeFile(path.join(self.options.outdir, 'classes', v.name + '.html'), html, stack.add(noop));
                    }));
                });
            });

            stack.done(function() {
                Y.log('Finished writing class files', 'info', 'builder');
                cb();
            });
        },
        /**
        * Sort method of array of objects with a property called __name__
        * @method nameSort
        * @param {Object} a First object to compare
        * @param {Object} b Second object to compare
        * @return {Number} 1, -1 or 0 for sorting.
        */
        nameSort: function(a, b) {
            if (!a.name || !b.name) {
                return 0;
            }
            var an = a.name.toLowerCase(),
                bn = b.name.toLowerCase(),
                ret = 0;

            if (an < bn) {
                ret = -1;
            }
            if (an > bn) {
                ret =  1
            }
            return ret;
        },
        /**
        * Generates the syntax files under "out"/files/
        * @method writeFiles
        * @param {Callback} cb The callback to execute after it's completed
        */
        writeFiles: function(cb) {
            var self = this;
            var stack = new Y.Parallel();
            
            Y.log('Writing (' + Object.keys(self.data.files).length + ') source files', 'info', 'builder');
            Y.each(self.data.files, function(v) {
                Y.prepare(themeDir, self.getProjectMeta(), function(err, opts) {
                    if (err) {
                        console.log(err);
                    }
                    if (!v.name) {
                        return;
                    }
                    opts.meta.title = self.data.project.name;

                    opts.meta.moduleName = v.name;
                    opts.meta.projectRoot = '../';
                    opts.meta.projectAssets = '../assets';

                    opts = self.populateClasses(opts);
                    opts = self.populateModules(opts);
                    opts = self.populateFiles(opts);
                    
                    opts.meta.fileName = v.name;
                    Y.Files.readFile((v.path || v.name), encoding='utf8', stack.add(Y.rbind(function(err, data, opts, v) {
                        opts.meta.fileData = data;
                        var view   = new Y.DocView(opts.meta, 'index');
                        self.render('{{>files}}', view, opts.layouts.main, opts.partials, function(err, html) {
                            self.files++;
                            Y.Files.writeFile(path.join(self.options.outdir, 'files', self.filterFileName(v.name) + '.html'), html, stack.add(noop));
                        });

                    }, this, opts, v)));
                });
            });

            stack.done(function() {
                Y.log('Finished writing source files', 'info', 'builder');
                cb();
            });
        },
        /**
        * Normalizes a file path to a writable filename:
        * 
        *    var path = 'lib/file.js';
        *    returns 'lib_file.js';
        * 
        * @method filterFileName
        * @param {String} f The filename to normalize
        * @return {String} The filtered file path
        */
        filterFileName: function(f) {
            return f.replace(/\//g, '_');
        },
        /**
        * Compiles the templates from the meta-data provided by DocParser
        * @method compile
        * @param {Callback} cb The callback to execute after it's completed
        */
        compile: function(cb) {
            var self = this;
            var starttime = (new Date()).getTime();
            Y.log('Compiling Templates', 'info', 'builder');


            this.mixExternal(function() {
                self.makeDirs(function() {
                    Y.log('Copying Assets', 'info', 'builder');
                    Y.Files.copyAssets(path.join(themeDir, 'assets'), path.join(self.options.outdir, 'assets'), true, function() {
                        var cstack = new Y.Parallel();
                        self.writeModules(cstack.add(function() {
                            self.writeClasses(cstack.add(noop));
                        }));
                        self.writeFiles(cstack.add(noop));
                        self.writeIndex(cstack.add(noop));
                        cstack.done(function() {
                            var endtime = (new Date()).getTime();
                            var timer = ((endtime - starttime) / 1000) + ' seconds';
                            Y.log('Finished writing ' + self.files + ' files in ' + timer, 'info', 'builder');
                            if (cb) { cb(); }
                        });
                    });
                });
            });
        }
    }
});