UNPKG

documon

Version:

A documentation system for mortals. Use with any language.

447 lines (329 loc) 11.5 kB
/* Part of Documon. Copyright (c) Michael Gieson. www.documon.net */ /** Discovers, parses, converts markdown to HTML and injects menu with "more" docs. Processes the "more" markdown folder by: - Building the menu to reflect the "more folder" directory structure. - Translates markdown into HTML @class More @package documon @private @param {object} params - The primary documon configuration [documon.js::mainConf](documon.documon.mainConf), which should contain: @param {object} [params.moreQuirkDelimiter="."] - Set's the [quirkDelimiter](#quirkDelimiter). @param {object} params.outputAssetsFolder - The asset folder to copy into the final documentation. The More pages may refer to assets outside of the template, such as css, images, etc. @param {object} params.moreFolder - The folder to process. @param {object} params.templateFolder - The path to the template folder. @param {object} params.outputFolder - THe destination folder. @param {object} params.gati - Your Google Analytics Tracking ID @param {object} sourceDocsMenu - The menu for the parsed source code documentation. We'll merge it into the "more" menu. @param {object} searchDB - The searchData (local to documon's [run](documon.documon.run) function, as generated by [seeder](documon.documon.seeder) ), we'll include our "more" docs for searching. @return {menu | undefined} - Serves 2 purposes, when returning __undefined__ notifies callee that there aren't any "more" docs. Otherwise the modified menu is return with the original sourceDocsMenu either tacked onto the end of the "more" menu, or incorporated into the "more" menu at the "DOCS-GO-HERE" injection point. */ var utils = require('./utils'); var fs = require('fs'); var du = require('./dirutils'); var fu = require('./fileutils'); var path = require('./npath'); var markdown = require('./markdown'); var searchPrep = require("./searchPrep"); /** * @property {string} flat - A place to store generated page objects with a direct reference to their ID via flat[id]. * @private */ var flat = {}; /** * @property {string} quirkDelimiter - Used to mark where filename numbering terminates and the "title" begins. User configurable via the [moreQuirkDelimiter option](more.options). * @private */ var quirkDelimiter = "."; // TODO: Make metaString and docGoesHereStr configurable. /** * @property {string} metaString - The delimiting string used to seperate the meta JSON from normal markdown. */ var metaString = "__meta__"; /** * @property {string} docGoesHereStr - The string used as the flag as to where to insert the parsed source documents into the menu. * @private */ var docGoesHereStr = "DOCS-GO-HERE"; /** * @property {string} metaRx - The regular expression use to split the doc on the [metaString](#metaString) * @private */ var metaRx = new RegExp(metaString, "gi"); /** * Opens, catalogs and builds a new page from the provided file path. * @method newItem * @private * @param {string} url - The path to the markdown file. * @param {boolean} amFolder - Process as a folder? * @return {object} - An object containing structured data: * { id , url // as originally provided , label // Cleaned up (no numbering) filename , name : // same as label but the page.jst template needs the "name" property. , kind : // The CSS class used on the icon( amFolder ? "more-folder" : "more-file" ) , amFolder : // is a folder? , basepath : // The parent folder. , parentID : // The parent's id (e.g.more.foo) , children : // Folders have a children array } */ function newItem(url, amFolder){ var Aurl = url.split("/"); /* // path.parse('/home/user/dir/file.txt') // // Yeilds // { // root : "/", // dir : "/home/user/dir", // base : "file.txt", // ext : ".txt", // name : "file" // } var Nurl = path.parse(url); */ var filename = Aurl.pop(); var parentURL = Aurl.join("/"); var basepath if( amFolder ){ basepath = parentURL; } else { var tempFN = filename.split("."); tempFN.pop(); basepath = parentURL + "/" + tempFN.join("."); } // Build id, and prevent dots (.) and non-alphanum chars var Aid = []; for(var i=0; i<Aurl.length; i++){ var cid = cleanID( Aurl[i], true ) Aid.push( cid ); } var cid = cleanID(filename, amFolder); Aid.push( cid ); Aurl.push(cid); var id = Aid.join("."); Aid.pop(); var parentID = Aid.join("."); var quirkyFilename = quirkyName(filename, amFolder); var obj = { id : id , url : amFolder ? "" : id + ".html" // recombine original , label : quirkyFilename , name : quirkyFilename // for page.ctx , kind : ( amFolder ? "more-folder" : "more-file" ) , amFolder : amFolder , basepath : basepath //, sourceURL : url //, parentURL : parentURL , parentID : parentID } if(amFolder){ obj.children = []; } if( parentID ) { if( ! flat[parentID] ){ flat[parentID] = newItem(parentURL, true); } if( ! flat[parentID].children ){ flat[parentID].children = []; } flat[parentID].children.push(obj); } return obj; } /** * Cleans the ID so the ID only contains lapha-numeric characters. Non-alphanumeric characters are translated into an underscore character. * @method cleanID * @private * @param {type} id description * @param {type} amFolder description * @return {type} description */ function cleanID(id, amFolder){ id = quirkyName(id, amFolder); return id.replace(/[^A-Za-z0-9_]/g, "_").toLowerCase(); } /** * splits the numbering off of eight filename and returns the filename with out numbering * @method quirkyName * @private * @param {type} filename description * @param {type} amFolder description * @return {type} description */ function quirkyName(filename, amFolder){ // Remove extension on files var Afilename = filename.split("."); if( ! amFolder ){ Afilename.pop(); //filename = Afilename.join("."); } // Re-apply naked name var basename = Afilename.join("."); // Build label sans quirks sorting Afilename = basename.split(quirkDelimiter); if(Afilename.length > 1){ // Ensure character immediately before the dot (or quirkDelimiter) is a numeric value. var numberVal = Afilename.shift(); // path.substr(-1) == path.slice(-1) var rightEdge = numberVal.slice(-1); if(parseInt(rightEdge) == rightEdge){ return Afilename.join(quirkDelimiter); } } return filename; } /** * A safe replacement for standard JSON parsing that mitigates errors. * @method parseJSON * @private * @param {string} val * @return {any} */ function parseJSON (val){ if (typeof val != 'string') { return null; } try { return JSON.parse( val.replace(/[\t\r\n]/g, "") ); } catch(e) { return null; } } /** * See class description. * @method init */ function init(params, sourceDocsMenu, searchDB, shouldIgnore){ quirkDelimiter = params.moreQuirkDelimiter || quirkDelimiter; var assetsDestination = path.addTrailingSlash(params.outputAssetsFolder); var moreFolder = path.addTrailingSlash(params.moreFolder); var Tpage = require( path.addTrailingSlash(params.templateFolder) + "page.jst"); var todoList = du.readExt(moreFolder, "md", true); // Weed out anything we need/want to ignore var dirList = []; for(var i=0; i<todoList.length; i++){ var item = todoList[i]; if( ! shouldIgnore(item) ){ dirList.push(item); } } if(dirList.length < 1){ return sourceDocsMenu; } var massest = moreFolder + "assets"; if( du.exists(massest) ) { du.copy(path.addTrailingSlash(massest), assetsDestination); } // Some systems sort by mod or creation date, ensure the list is sorted by path/name. dirList.sort(); var outputFolder = path.addTrailingSlash(params.outputFolder); var docTarget; var docTargetParentId; var docTargetId; for(var i=0; i<dirList.length; i++){ var loc = dirList[i]; // The "more/" is establishing the xpath we use in the menu (not the file path) var ctx = newItem( "more/" + loc.substring(moreFolder.length) ); // hacky ctx.gati = params.gati; flat[ctx.id] = ctx; var skip = false; if(loc.indexOf(docGoesHereStr) > -1) { // Only the first one. In case user has more than one DOCS-GO-HERE // placeholders lingering in the folder struct. if( ! docTargetId ){ docTargetId = ctx.id; docTargetParentId = ctx.parentID; } skip = true; } if( ! skip ){ // working: // want to incorporate meta object for url, icon, and anything else // { // "url" : "http://www.boob.com", // "icon" : "example.jpg", // "css" : "fa star" // } var src = fu.read(loc).trim(); var meta = null; // since we're on a loop, we've gotta reset it. if( metaRx.test(src) ) { // The the front half of the meta var Asrc = src.split(metaString); meta = parseJSON( Asrc.shift() ); // if json parsing fails, don't rewrite the source if(meta){ src = Asrc.join(metaString); // Just in case someone references the meta string elsewhere down in the page. } } var isExternal = false; if(meta){ if(meta.url){ isExternal = true; ctx.url = meta.url; } if(meta.icon){ ctx.kind = meta.icon; } } if( ! isExternal ){ var html = '<div class="more">' + markdown(src) + '</div>'; var str = Tpage(ctx, html); fu.write( outputFolder + ctx.url, str ); var searchText = searchPrep(src); if(searchText){ searchDB[ctx.id] = searchText; } } } } var moreMenu = flat.more.children; // will return undefined if no "more" docs. var returnMenu; // docTargetId is a suffficent test of existance. //if(moreMenu && moreMenu.length){ // See if there is a "DOC-GO-HERE.md" markdown file target. When this exists, we swap and // inject sourceDocsMenu into where it is. If not, we simply tack sourceDocsMenu onto the // end of our "more" doc menu, since I believe the "more" docs should go at the top of the menu. if(docTargetId){ // Get the item that was the "DOC-GO-HERE.md" file var docTarget = flat[docTargetId]; // ... and a reference to it's parent var docTargetParent = flat[docTargetParentId]; // Splice the existing DOC menu into the parent's children array. var kidList = docTargetParent.children; var idx = kidList.indexOf(docTarget); // If must exist //if(idx > -1){ var front = kidList.slice(0, idx); var back = kidList.slice(idx+1); // plus one to skip the placeholder DOCS-GO-HERE (effectively extracting the "DOC-GO-HERE.md" and replacing it with the sourceDocsMenu list) docTargetParent.children = front.concat(sourceDocsMenu, back); returnMenu = flat.more.children; //} else { // // Houston? This should never happen // throw new Error("more.js: \"DOC-GO-HERE\" exists but couldn't find it in the parent.") //} } else { // Tack sourceDocsMenu onto the end. returnMenu = flat.more.children.concat(sourceDocsMenu); } //} return returnMenu; } /* var dev = { more : "/Volumes/Drives/projects/documon/test_md/", outputFolder : "/Volumes/Drives/projects/documon/test_md_out/", templateFolder : "/Volumes/Drives/projects/documon/documon/template/", } init(dev); */ module.exports = init;