UNPKG

12.6 kBJavaScriptView Raw
1/*jshint onevar:false */
2
3/**
4 * Utilities modules
5 * @module utils
6 * @main utils
7 */
8
9const _ = require('underscore');
10const fs = require('graceful-fs');
11const path = require('path');
12const minimatch = require('minimatch');
13const HTML_CHARS = {
14 '&': '&',
15 '<': '&lt;',
16 '>': '&gt;',
17 '"': '&quot;',
18 "'": '&#x27;',
19 '/': '&#x2F;',
20 '`': '&#x60;'
21};
22
23/**
24 * Format the process string to array
25 * @method fmtProcess
26 * @param {String} process
27 * @return {Array} The process array
28 */
29function fmtProcess (process) {
30 return process.split(',').map(safetrim);
31}
32exports.fmtProcess = fmtProcess;
33
34/**
35 * Get the namespace string from a target object
36 * @method getNamespace
37 * @param {Object} target
38 * @param {String} target.module
39 * @param {String} target.clazz
40 * @param {String} target.name
41 * @return {String} The namespace string
42 */
43function getNamespace (target) {
44 var nssource = [target.module, target.clazz, target.name];
45 return nssource.filter(function (item) {
46 return !!item;
47 }).join('.');
48}
49exports.getNamespace = getNamespace;
50
51/**
52 * Escapes HTML characters in _html_.
53 *
54 * @method escapeHTML
55 * @param {String} html String to escape.
56 * @return {String} Escaped string.
57 **/
58function escapeHTML (html) {
59 return html.replace(/[&<>"'\/`]/g, function (m) {
60 return HTML_CHARS[m];
61 });
62}
63exports.escapeHTML = escapeHTML;
64
65/**
66 * Trim in safe mode
67 *
68 * @method safetrim
69 * @param {String} str
70 * @return {String} trimed string or other types if invalid
71 */
72function safetrim (str) {
73 if (str && _.isFunction(str.trim)) {
74 return str.trim();
75 } else {
76 return String(str || '').trim();
77 }
78}
79exports.safetrim = safetrim;
80
81/**
82 * Normalizes the initial indentation of the given _content_ so that the first line
83 * is unindented, and all other lines are unindented to the same degree as the
84 * first line. So if the first line has four spaces at the beginning, then all
85 * lines will be unindented four spaces. Ported from [Selleck](https://github.com/rgrove/selleck)
86 *
87 * @method unindent
88 * @param {String} content Text to unindent.
89 * @return {String} Unindented text.
90**/
91function unindent (contents) {
92 var indent = contents.match(/^(\s+)/);
93 if (indent) {
94 contents = contents.replace(new RegExp('^' + indent[1], 'gm'), '');
95 }
96 return contents;
97}
98exports.unindent = unindent;
99
100/**
101 * Normalizes a file path to a writable filename:
102 *
103 * var path = 'lib/file.js';
104 * returns 'lib_file.js';
105 *
106 * @method filterFileName
107 * @param {String} f The filename to normalize
108 * @return {String} The filtered file path
109 */
110function filterFileName (f) {
111 return (f || '').replace(/[\/\\]/g, '_');
112}
113exports.filterFileName = filterFileName;
114
115/**
116 * Parses file and line number from an item object and build's an HREF
117 * @method getFoundAt
118 * @param {Object} obj - The item to parse
119 * @param {Object} options - The options
120 * @param {Boolean} options.markdown - If in markdown mode
121 * @param {Boolean} options.nocode - If no code
122 * @return {String} The parsed HREF
123 */
124function getFoundAt (obj, options) {
125 var ext = options.markdown ? '.md' : '.html';
126 var ret = '';
127 if (obj.file && obj.line && !options.nocode) {
128 if (obj.path) {
129 ret = obj.path + '#l' + obj.line;
130 } else {
131 ret = '../files/' + filterFileName(obj.file) + ext + '#l' + obj.line;
132 }
133 }
134 return ret;
135}
136exports.getFoundAt = getFoundAt;
137
138/**
139 * Like `getPages()`, but returns only the files under the `layout/` subdirectory
140 * of the specified _dir_.
141 *
142 * @method getLayouts
143 * @param {String} dir Directory path.
144 * @return {Object} Mapping of layout names to layout content.
145 **/
146function getLayouts (dir, useMarkdown) {
147 if (!_.isString(dir)) return {};
148 return getPages(path.join(dir, 'layouts'), useMarkdown);
149}
150exports.getLayouts = getLayouts;
151
152/**
153 * Loads pages (files with a `.handlebars` extension) in the specified directory and
154 * returns an object containing a mapping of page names (the part of the filename)
155 * preceding the `.handlebars` extension) to page content.
156 *
157 * @method getPages
158 * @param {String} dir Directory path.
159 * @return {Object} Mapping of page names to page content.
160 **/
161var cache = {};
162function getPages (dir, useMarkdown) {
163 if (cache[dir]) {
164 return cache[dir];
165 }
166 var pages = {};
167 var stat = fs.statSync(dir);
168 if (!stat.isDirectory()) {
169 return pages;
170 }
171
172 _.each(
173 fs.readdirSync(dir),
174 function (name) {
175 var p = path.join(dir, name);
176 var ext = useMarkdown ? '.mdt' : '.handlebars';
177 if (path.extname(name) === ext && fs.statSync(p).isFile()) {
178 var name = path.basename(name, ext);
179 var text = fs.readFileSync(p, 'utf8');
180 Object.defineProperty(pages, name, {
181 enumerable: true,
182 get: function () {
183 return text
184 },
185 set: function () {}
186 });
187 }
188 }
189 );
190 cache[dir] = pages;
191 return pages;
192}
193exports.getPages = getPages;
194
195/**
196 * Like `getPages()`, but returns only the files under the `partial/` subdirectory
197 * of the specified _dir_.
198 *
199 * @method getPartials
200 * @param {String} dir Directory path.
201 * @return {Object} Mapping of partial names to partial content.
202 **/
203function getPartials (dir, useMarkdown) {
204 if (!_.isString(dir)) return {};
205 return getPages(path.join(dir, 'partials'), useMarkdown);
206}
207exports.getPartials = getPartials;
208
209/**
210 * Mix/merge/munge data into the template.
211 *
212 * @method prepare
213 * @param {String} inDir The starting directory
214 * @param {Object} options The `options` for the meta data.
215 * @param {callback} callback The callback to excecute when complete
216 * @param {Error} callback.err
217 * @param {Object} callback.options Merged options.
218 **/
219function prepare (inDirs, options, callback) {
220 var layouts, partials, type = 'project';
221 var defaults = {
222 'meta': {
223 'project': options.project,
224 'component': {}
225 },
226 'pages': {},
227 'layouts': {},
228 'partials': {},
229 'viewClass': require('./docview').DocView
230 };
231
232 if (options && options.skipLoad) {
233 // Skip loading layouts, metadata, pages, and partials and assume that
234 // the caller has provided them if they want them.
235 options = _.extend(defaults, options);
236 } else {
237 // Gather layouts, metadata, pages, and partials from the specified
238 // input directory, then merge them into the provided options (if any).
239 //
240 // Gathered data will override provided data if there are conflicts, in
241 // order to support a use case where global data are provided by the
242 // caller and overridden by more specific component-level data gathered
243 // from the input directory.
244 //
245 // The metadata inheritance chain looks like this:
246 //
247 // - override metadata specified via CLI (highest precedence)
248 // - component metadata (if this is a component)
249 // - project-level component default metadata (if specified and this is a component)
250 // - theme-level component default metadata (if specified and this is a component)
251 // - project metadata
252 // - theme metadata (lowest precedence)
253 if (inDirs[0] === inDirs[1]) {
254 layouts = getLayouts(inDirs[0], options.markdown);
255 partials = getPartials(inDirs[0], options.markdown);
256 } else {
257 layouts = _.extend(
258 getLayouts(inDirs[0], options.markdown),
259 getLayouts(inDirs[1], options.markdown)
260 );
261 partials = _.extend(
262 getPartials(inDirs[0], options.markdown),
263 getPartials(inDirs[1], options.markdown)
264 );
265 }
266 options = _.extend(defaults, options);
267 options = _.extend(options, {
268 'layouts': layouts,
269 'partials': partials,
270 });
271 }
272
273 // Set a default asset path if one isn't specified in the metadata.
274 if (!options.meta.component.assets && options.component) {
275 options.meta.component.assets = '../assets/' + options.meta.name;
276 }
277 if (_.isUndefined(options.meta.layout)) {
278 options.meta.layout = options.layouts[type] ? type : 'main';
279 }
280 if (_.isFunction(callback)) {
281 callback(null, options);
282 }
283 return options;
284}
285exports.prepare = prepare;
286
287/**
288 * Takes a type string and converts it to a "First letter upper cased" type.
289 * e.g. `(string -> String, object -> Object)`
290 *
291 * @method fixType
292 * @param {String} t The type string to convert
293 * @return {String} The fixed string
294 */
295function fixType (t) {
296 t = safetrim(t);
297 t = t.replace(/{/g, '').replace(/}/g, '');
298 if (t && t.indexOf('.') === -1) {
299 var firstChar = t.charAt(0),
300 upperFirstChar = firstChar.toUpperCase();
301 if (firstChar !== upperFirstChar) {
302 return upperFirstChar + t.substring(1);
303 }
304 }
305 return t;
306};
307exports.fixType = fixType;
308
309/**
310 * Produces a normalized web path by joining all the parts and normalizing the
311 * filesystem-like path into web compatible url.
312 * Supports relative and absolute paths.
313 * Courtesy of [Mojito's utils](https://github.com/yahoo/mojito/)
314 *
315 * @method webpath
316 * @param {Array|String*} url the list of parts to be joined and normalized
317 * @return {String} The joined and normalized url
318 **/
319function webpath (url) {
320 var args = [].concat.apply([], arguments),
321 parts = path.join.apply(path, args).split(/[\\\/]/);
322 return parts.join('/');
323}
324exports.webpath = webpath;
325
326/**
327 * Localize the string via current Y.options
328 *
329 * @method localize
330 * @param str {String} the original string that you want to input
331 * @param lang {String} the language
332 * @return {String} localized string from the param `str`
333 */
334function localize (str, lang) {
335 var splitedStrArr = (str || '').split('!#');
336 var supportedLang = ['en', 'zh'];
337 return splitedStrArr.map(function(block) {
338 var langFlag = block.slice(0, 2);
339 var selectedLang = supportedLang.indexOf(langFlag);
340 if (selectedLang === -1) {
341 // default language is 'en'
342 return {
343 raw: block,
344 all: true
345 };
346 } else {
347 return {
348 raw: block.slice(2),
349 lang: supportedLang[selectedLang]
350 };
351 }
352 }).filter(function(block) {
353 if (!block || !block.raw) {
354 return false;
355 }
356 if (block.all) {
357 return true;
358 }
359 return block.lang === (lang || 'en');
360 }).map(function(block) {
361 return block.raw.trim();
362 }).join('\n');
363}
364exports.localize = localize;
365
366/**
367 * convert string to markdown link
368 *
369 * @method markdownLink
370 * @param {String} str - The original string that you want to input
371 * @return {String} marked string from the param `str`
372 */
373function markdownLink (str) {
374 return str
375 .replace(/[:,]/g, '-')
376 .replace(/[\s\(\)\[\]=]/g, '')
377 .toLowerCase();
378}
379exports.markdownLink = markdownLink;
380
381/**
382 * build file tree object
383 *
384 * @method buildFileTree
385 * @param {Array} files
386 * @return {Object}
387 */
388function buildFileTree (files) {
389 var tree = {};
390 _.each(files, function (v) {
391 var p = v.name.split('/');
392 var par;
393 p.forEach(function (i, k) {
394 if (!par) {
395 if (!tree[i]) {
396 tree[i] = {};
397 }
398 par = tree[i];
399 } else {
400 if (!par[i]) {
401 par[i] = {};
402 }
403 if (k + 1 === p.length) {
404 par[i] = {
405 path: v.name,
406 name: filterFileName(v.name)
407 };
408 }
409 par = par[i];
410 }
411 });
412 });
413 return tree;
414}
415exports.buildFileTree = buildFileTree;
416
417/**
418 * Parses the JSON data and formats it into a nice log string for
419 * filename and line number: `/file/name.js:123`
420 * @method stringlog
421 * @private
422 * @param {Object} data The data block from the parser
423 * @return {String} The formatted string.
424 * @for DocParser
425 */
426function stringlog(data) {
427 var line, file;
428 if (data.file && data.line) {
429 file = data.file;
430 line = data.line;
431 } else {
432 data.forEach(function (d) {
433 if (d.tag === 'file') {
434 file = d.value;
435 }
436 if (d.tag === 'line') {
437 line = d.value;
438 }
439 });
440 }
441 return ' ' + file + ':' + line;
442}
443exports.stringlog = stringlog;
444
445/**
446 * Define the readonly properties
447 * @method defineReadonly
448 */
449function defineReadonly (obj, name, val) {
450 Object.defineProperty(obj, name, {
451 get: function () {
452 return val;
453 }
454 });
455}
456exports.defineReadonly = defineReadonly;
457