UNPKG

unbroken-doc

Version:

A solution to create steady documentation for your project.

306 lines (253 loc) 10.8 kB
var chokidar = require('chokidar'); var npath = require('path'); var fs = require('fs'); var md5 = require('md5'); var _ = require('lodash'); var config = require('./config'); (function () { var taskDefs = {}; var rgxRawMarker = new RegExp( '@@\\{' // 预生成标记的起始token + '([^{}\\n]*)' // 在 @@{ /* 不应该包含任何的 { 和 } 或 换行 */ } 中 + '\\}', 'g' ), rgxExistingMarker = new RegExp( '@\\{\\{' // 已生成标记的起始token为 @{{ + '\\s*' + '(?:#(\\d+))?' // #DDD 为标记的权重, 目前没有作用 + '\\s*' + '@\\[([^\\[\\]\\n]*)\\]' // 前面是 [ /* 标记名称 */ ] + '\\{([^{}\\n]+)\\}' // 后面是 { /* 标记的Key */ } + '[^]*?' // 标记中, 可以包含简单的内容 + '\\}\\}@', // 结束 token 'g' ); var ensurePath = function (path) { if (!fs.existsSync(path)) { ensurePath(npath.dirname(path)); fs.mkdirSync(path); } }; var docIndexUpdated = 0, _docIndex; var getDocIndex = function () { if (fs.existsSync(config.docIndexFilePath)) { var stat = fs.statSync(config.docIndexFilePath); if (stat.mtime.getTime() > docIndexUpdated) { _docIndex = JSON.parse(fs.readFileSync(config.docIndexFilePath).toString()); docIndexUpdated = stat.atime.getTime(); console.log('index file renewed.'); } return _docIndex; } else { if (!_docIndex) { _docIndex = { seq: 0, keys: {}, files: {} }; debounceSaveDocIndex(); } return _docIndex; } }; var debounceSaveDocIndex = _.debounce(function () { fs.writeFileSync(config.docIndexFilePath, JSON.stringify(_docIndex, null, ' ')); docIndexUpdated = new Date().getTime(); }, 800); var createWatcher = function () { return chokidar.watch(config.srcPath, { ignored: config.ignores }); }; /*@{{ @[ doc 项目监控处理程序 ]{_unbroken_doc_0e9af2478_} }}@*/ taskDefs.doc = function () { var allTypeMap = {}, allTypes = []; var generateKey = function () { var docIndex = getDocIndex(); docIndex.seq++; debounceSaveDocIndex(); return '_' + config.projectKey + '_' + md5(Math.random()).substr(0, 8) + docIndex.seq.toString(36) + '_'; }; var fileQueue = {}; /*@{{ @[ 隔时批量索引项目内容 ]{_unbroken_doc_0e5174551_} 在 chokidar 获知文件更改的时候, 不是立即处理, 而是放在一个队列里, 隔时批量处理, 避免多次反复操作. }}@*/ setInterval(function () { var docIndex; for (var path in fileQueue) { if (!docIndex) { var docIndex = getDocIndex() } if (fs.existsSync(path)) { var logic = fileQueue[path]; if (typeof logic.comment == 'string') { logic.comment = config.commentSyntax[logic.comment]; } if (!logic.comment) { console.log('No comment syntax set for %s, process next file.', path); continue; } var stat = fs.statSync(path); //console.log('stat', stat, docIndex.files[path].atime); // 使用 stat.atime 来检测文件是否需要重新索引 if (!docIndex.files || !docIndex.files[path] || stat.atime.getTime() > docIndex.files[path].atime) { var fileInfo = docIndex.files[path] = docIndex.files[path] || {atime: stat.atime.getTime()}; var content = fs.readFileSync(path).toString(); var newContent = content.replace(rgxRawMarker, function (match, name) { name = ' ' + _.trim(name) + ' '; var key = generateKey(); return logic.comment.start + '@{{\n@[' + name + ']{' + key + '}\n}}@' + logic.comment.end; }); if (newContent != content) { fs.writeFileSync(path, newContent); } newContent.replace(rgxExistingMarker, function (match, rank, name, key) { docIndex.keys[key] = { name: name, path: path, rank: rank }; debounceSaveDocIndex(); console.log('existing - ', name, key, rank); }); fileInfo.atime = new Date().getTime(); debounceSaveDocIndex(); } } } fileQueue = {}; }, 200); /*@{{ @[ 加入批处理队列 ]{_unbroken_doc_da5379fc3_} }}@*/ var processFile = function (path, extname) { path = path.split('\\').join('/'); var logic = config.fileTypes[extname.substr(1)]; if (logic) { fileQueue[path] = logic; } }; /*@{{ @[ 监视文件更改 ]{_unbroken_doc_d5b774c32_} }}@*/ var watcher = createWatcher() .on('add', function (path) { // 获取所有文件类型 var extname = npath.extname(path); allTypeMap[extname] = 1; processFile(path, extname); //console.log(path); }) .on('change', function (path) { var extname = npath.extname(path); processFile(path, extname); }) .on('ready', function () { // 输出文件类型列表 var allTypes = _.keys(allTypeMap); console.log('all types detected : %s', allTypes.map(function (type) { return JSON.stringify(type); }).join()); }); }; var validators = { /*@{{ @[ 校对cache中的文件路径 ]{_unbroken_doc_236ad77f4_} }}@*/ validateFiles: function (files, docIndex) { var newFiles = {}; for (var path in files) { if (!fs.existsSync(path)) { console.log('path %s does not exist anymore, removed.', path); } else { newFiles[path] = files[path]; } } docIndex.files = newFiles; debounceSaveDocIndex(); }, /*@{{ @[ 校对cache中的标记keys ]{_unbroken_doc_4fb26dc65_} }}@*/ validateKeys: function (keys, docIndex) { var tmpKeys = {}; var watcher = createWatcher() .on('add', function (path) { path = path.split('\\').join('/'); var content = fs.readFileSync(path).toString(); content.replace(rgxExistingMarker, function (match, rank, name, key) { tmpKeys[key] = { name: name, path: path, rank: rank }; console.log('existing - ', name, key, rank); }); }) .on('ready', function () { var missingKeys = {}, keysNotAdded = {}; // 检测在index文件中定义, 但在项目内容中未找到的标记. // 做警示提醒, 不做自动操作 _.each(tmpKeys, function (content, key) { if (!keys[key]) { keys[key] = content; console.log('New key added %s (%s) from %s.', key, content.name, content.path); debounceSaveDocIndex(); } }); // 检测在项目内容中找到, 但没在index文件中定义的标记. // 如果有, 则自动添加到index 文件 _.each(keys, function (content, key) { if (!tmpKeys[key]) { missingKeys[key] = content; console.log('WARNING: %s (%s) from %s is missing.', key, content.name, content.path); } }); watcher.close(); }); } }; /*@{{ @[ validate 校对任务 ]{_unbroken_doc_7f7ba06d6_} }}@*/ taskDefs.validate = function () { if (fs.existsSync(config.docIndexFilePath)) { var docIndex = getDocIndex(); validators.validateFiles(docIndex.files, docIndex); validators.validateKeys(docIndex.keys, docIndex); // in future, validate backlinks? content? references? } else { console.log('Doc index文件还没有生成, 请先运行: gulp doc'); } }; module.exports = { /*@{{ @[ unbroken-doc 初始化 ]{_unbroken_doc_c067c8957_} }}@*/ init: function (projectKey, configOrFunc) { if (!projectKey) { throw Error('You should specify a proper project key as it is the key element of your markers.' + '\n on .init() method.'); } config.projectKey = projectKey; if (typeof configOrFunc == 'function') { config = configOrFunc(config); } else if (configOrFunc) { _.extend(config, configOrFunc); } this.applyConfig(); }, applyConfig: function () { config.docCacheFolderPath += '/'; config.projectKey = config.projectKey.replace(/\W+/g, '_'); config.ignores = config.ignores.concat(config.addIgnores); config.docIndexFilePath = config.docCacheFolderPath + 'unbroken-doc-index.json'; ensurePath(config.docCacheFolderPath); }, tasks: taskDefs } })();