Skip to content

Commit

Permalink
Added inital version of the documentation generation for config files
Browse files Browse the repository at this point in the history
  • Loading branch information
dlmr committed Dec 9, 2015
1 parent 016d8ee commit bdaf193
Show file tree
Hide file tree
Showing 7 changed files with 580 additions and 20 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"colors": "~1.1.2",
"deep-extend": "~0.4.0",
"lodash": "~3.10.1",
"source-map-support": "~0.3.3"
"source-map-support": "~0.3.3",
"strip-ansi": "~3.0.0"
}
}
66 changes: 66 additions & 0 deletions src/documentation/build-documentation-object.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { isPlainObject, isString, isFunction } from 'lodash';

import { toCliFlag } from './helpers';

export default function buildDocumentationObject(
initalObject, initalGroups, initalDescriptions, initalValidations, initalFilter = []
) {
const allObjects = (object, callback) => {
return Object.keys(object).map(callback).filter((value) => value !== undefined);
};

const getDefaultValue = (object) => {
if (Array.isArray(object) && !object.length ||
isString(object) && !object ||
isPlainObject(object) && Object.keys(object).length === 0) {
return null;
}

return JSON.stringify(object);
};

const manageGroup = (object, name, group = {}, description, validation, parents, level) => {
const groupDescription = isPlainObject(group) ? undefined : group;
return {
name,
level,
description: groupDescription,
objects: recursiveHelper(object, group, description, validation, [], parents, level + 1, true),
children: recursiveHelper(object, group, description, validation, [], parents, level + 1)
};
};

const manageLeaf = (object, name, description, validation = () => ({type: 'Unknown'}), parents) => {
const { type, required } = isFunction(validation) ?
validation(null, true) :
({type: validation.toString(), req: false });

return {
name,
description,
type,
required,
path: parents.join('.'),
cli: toCliFlag(parents),
defaultValue: getDefaultValue(object)
};
};

function recursiveHelper(object, groups = {}, descriptions = {}, validations = {}, filter = [],
initalParents = [], level = 0, leaves = false) {
return allObjects(object, (key) => {
// Make sure that we either have no filter or that there is a match
if (filter.length === 0 || filter.indexOf(key) !== -1) {
const parents = [].concat(initalParents, key);
const value = object[key];
if (isPlainObject(value) && Object.keys(value).length > 0 && !leaves) {
return manageGroup(value, key, groups[key], descriptions[key], validations[key], parents, level);
} else if ((!isPlainObject(value) || Object.keys(value).length === 0) && leaves) {
return manageLeaf(value, key, descriptions[key], validations[key], parents);
}
}
});
}

return recursiveHelper(initalObject, initalGroups, initalDescriptions, initalValidations, initalFilter);
}
134 changes: 134 additions & 0 deletions src/documentation/generate-table.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import deepExtend from 'deep-extend';
import { isUndefined, isString } from 'lodash';
import stripAnsi from 'strip-ansi';

import { pad, addPadding } from './helpers';

const defaultSettings = {
groupTitleWrapper: (name) => name,
cellDivider: '|',
rowWrapper: (input) => `|${input}|`,
cellWrapper: (input) => ` ${input} `,
header: true,
compact: false
};

export default function generateTable(initalDocumentationObject, header, settings) {
settings = deepExtend({}, defaultSettings, settings);
const headerLengths = createLengthObject(header, header, {}, true);
const lengths = createLengthObject(initalDocumentationObject, header, headerLengths);

const printTableCell = (key, element, renderer = (input) => input, fill = false) => {
if (fill) {
return settings.cellWrapper(pad(lengths[key], '-'));
}
const padding = isUndefined(header[key].padding) ? true : header[key].padding;
const text = padding ?
addPadding(renderer(element), lengths[key]) :
renderer(element);

return settings.cellWrapper(text);
};

const printTableRow = (object, isHeader = false) => {
return settings.rowWrapper(Object.keys(header).map((key) => {
const { value, renderer } = getValueAndRenderer(isHeader, header[key], object[key]);

return printTableCell(key, value, renderer);
}).join(settings.cellDivider));
};

const printTableSplitter = () => {
return settings.rowWrapper(Object.keys(header).map((key) =>
printTableCell(key, undefined, undefined, true)
).join(settings.cellDivider));
};

const printTableHead = (name, description, level) => {
const rows = [];
rows.push(settings.groupTitleWrapper(name, level));

if (description) {
rows.push(description);
}

// Add a new empty row
rows.push('');

if (settings.header) {
rows.push(printTableRow(header, true));
rows.push(printTableSplitter());
}

return rows;
};

const recursiveHelper = (documentationObject) => {
const spacing = settings.compact ? [] : [''];

return documentationObject.map((group) => {
let rows = [];
if (group.level === 0 || !settings.compact) {
rows = rows.concat(printTableHead(group.name, group.description, group.level));
}

rows = rows.concat(group.objects.map((element) =>
printTableRow(element)));

rows = rows.concat(recursiveHelper(group.children));

if (group.level === 0 && settings.compact) {
rows.push('');
}

return rows;
}).reduce((a, b) => a.concat(b), spacing);
};

return recursiveHelper(initalDocumentationObject).join('\n');
}

function createLengthObject(initalElements, header, initalLengths, isHeader = false) {
const getObjectLength = (object, renderer = (input) => input) => {
return stripAnsi(renderer(object)).length;
};

const getLength = (newLength, currentLength = 0) => {
return newLength > currentLength ? newLength : currentLength;
};

const recursiveHelper = (elements, lengths = {}) => {
let newLengths = { ...lengths };
elements.forEach((element) => {
element.objects.forEach((object) => {
Object.keys(header).forEach((key) => {
const { value, renderer } = getValueAndRenderer(isHeader, header[key], object[key]);
if (isString(value)) {
newLengths[key] = getLength(getObjectLength(value, renderer), newLengths[key]);
}
});
});

newLengths = recursiveHelper(element.children, newLengths);
});

return newLengths;
};

// Make sure the data matches the expected format
if (!Array.isArray(initalElements)) {
initalElements = [{objects: [initalElements], children: []}];
}

return recursiveHelper(initalElements, initalLengths);
}

function getValueAndRenderer(isHeader, headerObject, object) {
const value = isHeader ? object.name : object;
const renderer = isHeader ? (input) => input : headerObject.renderer;

return {
value,
renderer
};
}
18 changes: 18 additions & 0 deletions src/documentation/helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import stripAnsi from 'strip-ansi';

export function pad(length, character = ' ') {
return Array(length + 1).join(character);
}

export function addPadding(string, length) {
string = string || '';
return string + pad(length - stripAnsi(string).length);
}

export function toCliFlag(configPaths) {
// Runtime should be added directly
if (configPaths[0] === 'runtime') {
configPaths.shift();
}
return '--' + configPaths.join('-');
}
145 changes: 139 additions & 6 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ import path from 'path';
import fs from 'fs';
import colors from 'colors/safe';
import deepExtend from 'deep-extend';
import { isPlainObject } from 'lodash';
import { isPlainObject, escape } from 'lodash';

import { isValid } from './helpers';
import buildDocumentationObject from './documentation/build-documentation-object';
import generateTable from './documentation/generate-table';
import { pad } from './documentation/helpers';

let onceApp = true;
let onceTemp = true;
Expand Down Expand Up @@ -237,14 +240,144 @@ function throwError(field, message, value) {
}

/**
* Generates documentation for the provided configuration object
* Generates markdown documentation for the provided configuration object
*
* @todo Implement
* @param {object} config - the configuration object to generate documentation for
* @param {object} metaConfig - the meta configuration object that has information about the configuration object
* @param {string[]} [filter=[]] - the groups that should be includes, by default all will be used
* @returns {object} A markdown string
*/
export function generateMarkdownDocumentation(config, metaConfig, filter = []) {
const documentationObject = buildDocumentationObject(
config, metaConfig.groups, metaConfig.descriptions, metaConfig.validation, filter
);

const header = {
name: {
name: 'Name'
},
description: {
name: 'Description',
renderer: (input) => escape(input)
},
path: {
name: 'Path'
},
cli: {
name: 'CLI Flag'
},
defaultValue: {
name: 'Default',
renderer: (input) => input && `\`${input}\``
},
type: {
name: 'Type',
renderer: (input) => input && `\`${input}\``
},
required: {
name: 'Required',
renderer: (input) => {
if (input === true) {
return 'Yes';
}
return 'No';
}
}
};

const groupTitleWrapper = (name, level) => pad(level + 1, '#') + ' ' + name.charAt(0).toUpperCase() + name.slice(1);

return console.log(generateTable(documentationObject, header, {
groupTitleWrapper
}));
}

/**
* Generates plain text documentation for the provided configuration object
*
* @param {object} config - the configuration object to generate documentation for
* @param {object} metaConfig - the meta configuration object that has information about the configuration object
* @returns {object} Some configuration documentation
* @param {string[]} [filter=[]] - the groups that should be includes, by default all will be used
* @returns {object} A string
*/
export function documentation(config, metaConfig) {
return console.log('Does nothing yet.', config, metaConfig);
export function generateTextDocumentation(config, metaConfig, filter = []) {
const documentationObject = buildDocumentationObject(
config, metaConfig.groups, metaConfig.descriptions, metaConfig.validation, filter
);

const header = {
description: {
name: 'Description',
renderer: (input) => input.substr(0, 100) + '…'
},
path: {
name: 'Path'
},
defaultValue: {
name: 'Default',
renderer: (input) => {
if (!input) {
return colors.yellow('No default value');
}
return input;
}
},
cli: {
name: 'CLI Flag'
},
required: {
name: 'Required',
renderer: (input) => {
if (input === true) {
return colors.green('Yes');
}
return colors.red('No');
}
}
};

return console.log(generateTable(documentationObject, header));
}

/**
* Generates plain text documentation for the provided configuration object
*
* Prints the documentation directly to the console.log
*
* @param {object} config - the configuration object to generate documentation for
* @param {object} metaConfig - the meta configuration object that has information about the configuration object
* @param {string[]} [filter=[]] - the groups that should be includes, by default all will be used
*/
export function generateCliDocumentation(config, metaConfig, filter = []) {
const documentationObject = buildDocumentationObject(
config, metaConfig.groups, metaConfig.descriptions, metaConfig.validation, filter
);

const header = {
cli: {
name: 'CLI Flag'
},
description: {
name: 'Description',
padding: false
},
defaultValue: {
name: 'Default',
renderer: (input) => {
if (!input) {
return colors.yellow('No default value');
}
return colors.cyan(input);
}
}
};

console.log(generateTable(documentationObject, header, {
compact: true,
titleWrapper: (name) => name + ':',
cellDivider: '',
rowWrapper: (input) => `${input}`,
header: false,
groupTitleWrapper: (input) => input + ':'
}));
}
Loading

0 comments on commit bdaf193

Please sign in to comment.