Fireball is the game engine for the future.
/*jshint onevar:false */
/**
* Utilities modules
* @module utils
* @main utils
*/
const _ = require('underscore');
const fs = require('graceful-fs');
const path = require('path');
const minimatch = require('minimatch');
const HTML_CHARS = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'/': '/',
'`': '`'
};
/**
* Format the process string to array
* @method fmtProcess
* @param {String} process
* @return {Array} The process array
*/
function fmtProcess (process) {
return process.split(',').map(safetrim);
}
exports.fmtProcess = fmtProcess;
/**
* Get the namespace string from a target object
* @method getNamespace
* @param {Object} target
* @param {String} target.module
* @param {String} target.clazz
* @param {String} target.name
* @return {String} The namespace string
*/
function getNamespace (target) {
var nssource = [target.module, target.clazz, target.name];
return nssource.filter(function (item) {
return !!item;
}).join('.');
}
exports.getNamespace = getNamespace;
/**
* Escapes HTML characters in _html_.
*
* @method escapeHTML
* @param {String} html String to escape.
* @return {String} Escaped string.
**/
function escapeHTML (html) {
return html.replace(/[&<>"'\/`]/g, function (m) {
return HTML_CHARS[m];
});
}
exports.escapeHTML = escapeHTML;
/**
* Trim in safe mode
*
* @method safetrim
* @param {String} str
* @return {String} trimed string or other types if invalid
*/
function safetrim (str) {
if (str && _.isFunction(str.trim)) {
return str.trim();
} else {
return String(str || '').trim();
}
}
exports.safetrim = safetrim;
/**
* Normalizes the initial indentation of the given _content_ so that the first line
* is unindented, and all other lines are unindented to the same degree as the
* first line. So if the first line has four spaces at the beginning, then all
* lines will be unindented four spaces. Ported from [Selleck](https://github.com/rgrove/selleck)
*
* @method unindent
* @param {String} content Text to unindent.
* @return {String} Unindented text.
**/
function unindent (contents) {
var indent = contents.match(/^(\s+)/);
if (indent) {
contents = contents.replace(new RegExp('^' + indent[1], 'gm'), '');
}
return contents;
}
exports.unindent = unindent;
/**
* 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
*/
function filterFileName (f) {
return (f || '').replace(/[\/\\]/g, '_');
}
exports.filterFileName = filterFileName;
/**
* Parses file and line number from an item object and build's an HREF
* @method getFoundAt
* @param {Object} obj - The item to parse
* @param {Object} options - The options
* @param {Boolean} options.markdown - If in markdown mode
* @param {Boolean} options.nocode - If no code
* @return {String} The parsed HREF
*/
function getFoundAt (obj, options) {
var ext = options.markdown ? '.md' : '.html';
var ret = '';
if (obj.file && obj.line && !options.nocode) {
if (obj.path) {
ret = obj.path + '#l' + obj.line;
} else {
ret = '../files/' + filterFileName(obj.file) + ext + '#l' + obj.line;
}
}
return ret;
}
exports.getFoundAt = getFoundAt;
/**
* Like `getPages()`, but returns only the files under the `layout/` subdirectory
* of the specified _dir_.
*
* @method getLayouts
* @param {String} dir Directory path.
* @return {Object} Mapping of layout names to layout content.
**/
function getLayouts (dir, useMarkdown) {
if (!_.isString(dir)) return {};
return getPages(path.join(dir, 'layouts'), useMarkdown);
}
exports.getLayouts = getLayouts;
/**
* Loads pages (files with a `.handlebars` extension) in the specified directory and
* returns an object containing a mapping of page names (the part of the filename)
* preceding the `.handlebars` extension) to page content.
*
* @method getPages
* @param {String} dir Directory path.
* @return {Object} Mapping of page names to page content.
**/
var cache = {};
function getPages (dir, useMarkdown) {
if (cache[dir]) {
return cache[dir];
}
var pages = {};
var stat = fs.statSync(dir);
if (!stat.isDirectory()) {
return pages;
}
_.each(
fs.readdirSync(dir),
function (name) {
var p = path.join(dir, name);
var ext = useMarkdown ? '.mdt' : '.handlebars';
if (path.extname(name) === ext && fs.statSync(p).isFile()) {
var name = path.basename(name, ext);
var text = fs.readFileSync(p, 'utf8');
Object.defineProperty(pages, name, {
enumerable: true,
get: function () {
return text
},
set: function () {}
});
}
}
);
cache[dir] = pages;
return pages;
}
exports.getPages = getPages;
/**
* Like `getPages()`, but returns only the files under the `partial/` subdirectory
* of the specified _dir_.
*
* @method getPartials
* @param {String} dir Directory path.
* @return {Object} Mapping of partial names to partial content.
**/
function getPartials (dir, useMarkdown) {
if (!_.isString(dir)) return {};
return getPages(path.join(dir, 'partials'), useMarkdown);
}
exports.getPartials = getPartials;
/**
* Mix/merge/munge data into the template.
*
* @method prepare
* @param {String} inDir The starting directory
* @param {Object} options The `options` for the meta data.
* @param {callback} callback The callback to excecute when complete
* @param {Error} callback.err
* @param {Object} callback.options Merged options.
**/
function prepare (inDirs, options, callback) {
var layouts, partials, type = 'project';
var defaults = {
'meta': {
'project': options.project,
'component': {}
},
'pages': {},
'layouts': {},
'partials': {},
'viewClass': require('./docview').DocView
};
if (options && options.skipLoad) {
// Skip loading layouts, metadata, pages, and partials and assume that
// the caller has provided them if they want them.
options = _.extend(defaults, options);
} else {
// Gather layouts, metadata, pages, and partials from the specified
// input directory, then merge them into the provided options (if any).
//
// Gathered data will override provided data if there are conflicts, in
// order to support a use case where global data are provided by the
// caller and overridden by more specific component-level data gathered
// from the input directory.
//
// The metadata inheritance chain looks like this:
//
// - override metadata specified via CLI (highest precedence)
// - component metadata (if this is a component)
// - project-level component default metadata (if specified and this is a component)
// - theme-level component default metadata (if specified and this is a component)
// - project metadata
// - theme metadata (lowest precedence)
if (inDirs[0] === inDirs[1]) {
layouts = getLayouts(inDirs[0], options.markdown);
partials = getPartials(inDirs[0], options.markdown);
} else {
layouts = _.extend(
getLayouts(inDirs[0], options.markdown),
getLayouts(inDirs[1], options.markdown)
);
partials = _.extend(
getPartials(inDirs[0], options.markdown),
getPartials(inDirs[1], options.markdown)
);
}
options = _.extend(defaults, options);
options = _.extend(options, {
'layouts': layouts,
'partials': partials,
});
}
// Set a default asset path if one isn't specified in the metadata.
if (!options.meta.component.assets && options.component) {
options.meta.component.assets = '../assets/' + options.meta.name;
}
if (_.isUndefined(options.meta.layout)) {
options.meta.layout = options.layouts[type] ? type : 'main';
}
if (_.isFunction(callback)) {
callback(null, options);
}
return options;
}
exports.prepare = prepare;
/**
* Takes a type string and converts it to a "First letter upper cased" type.
* e.g. `(string -> String, object -> Object)`
*
* @method fixType
* @param {String} t The type string to convert
* @return {String} The fixed string
*/
function fixType (t) {
t = safetrim(t);
if (t && t.indexOf('.') === -1) {
t = t.replace(/{/g, '').replace(/}/g, '');
var firstChar = t.charAt(0),
upperFirstChar = firstChar.toUpperCase();
if (firstChar !== upperFirstChar) {
return upperFirstChar + t.substring(1);
}
}
return t;
};
exports.fixType = fixType;
/**
* Produces a normalized web path by joining all the parts and normalizing the
* filesystem-like path into web compatible url.
* Supports relative and absolute paths.
* Courtesy of [Mojito's utils](https://github.com/yahoo/mojito/)
*
* @method webpath
* @param {Array|String*} url the list of parts to be joined and normalized
* @return {String} The joined and normalized url
**/
function webpath (url) {
var args = [].concat.apply([], arguments),
parts = path.join.apply(path, args).split(/[\\\/]/);
return parts.join('/');
}
exports.webpath = webpath;
/**
* Localize the string via current Y.options
*
* @method localize
* @param str {String} the original string that you want to input
* @param lang {String} the language
* @return {String} localized string from the param `str`
*/
function localize (str, lang) {
var splitedStrArr = (str || '').split('!#');
var supportedLang = ['en', 'zh'];
return splitedStrArr.map(function(block) {
var langFlag = block.slice(0, 2);
var selectedLang = supportedLang.indexOf(langFlag);
if (selectedLang === -1) {
// default language is 'en'
return {
raw: block,
all: true
};
} else {
return {
raw: block.slice(2),
lang: supportedLang[selectedLang]
};
}
}).filter(function(block) {
if (!block || !block.raw) {
return false;
}
if (block.all) {
return true;
}
return block.lang === (lang || 'en');
}).map(function(block) {
return block.raw.trim();
}).join('\n');
}
exports.localize = localize;
/**
* convert string to markdown link
*
* @method markdownLink
* @param {String} str - The original string that you want to input
* @return {String} marked string from the param `str`
*/
function markdownLink (str) {
return str
.replace(/[:,]/g, '-')
.replace(/[\s\(\)\[\]=]/g, '')
.toLowerCase();
}
exports.markdownLink = markdownLink;
/**
* build file tree object
*
* @method buildFileTree
* @param {Array} files
* @return {Object}
*/
function buildFileTree (files) {
var tree = {};
_.each(files, function (v) {
var p = v.name.split('/');
var 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,
name: filterFileName(v.name)
};
}
par = par[i];
}
});
});
return tree;
}
exports.buildFileTree = buildFileTree;
/**
* Parses the JSON data and formats it into a nice log string for
* filename and line number: `/file/name.js:123`
* @method stringlog
* @private
* @param {Object} data The data block from the parser
* @return {String} The formatted string.
* @for DocParser
*/
function stringlog(data) {
var line, file;
if (data.file && data.line) {
file = data.file;
line = data.line;
} else {
data.forEach(function (d) {
if (d.tag === 'file') {
file = d.value;
}
if (d.tag === 'line') {
line = d.value;
}
});
}
return ' ' + file + ':' + line;
}
exports.stringlog = stringlog;
/**
* Define the readonly properties
* @method defineReadonly
*/
function defineReadonly (obj, name, val) {
Object.defineProperty(obj, name, {
get: function () {
return val;
}
});
}
exports.defineReadonly = defineReadonly;