@progress/dss
Version:
Documented Style Sheets
684 lines (581 loc) • 21.5 kB
JavaScript
const _has = require('lodash.has');
// DSS Object
let dss = ( function() {
// Store reference
let _dss = function() {};
// Default detect function
_dss.detect = function() {
return true;
};
/**
* Modify detector method.
*
* @param {function} callback - The callback to be used to detect variables.
*/
_dss.detector = function( callback ) {
_dss.detect = callback;
};
// Store parsers
_dss.parsers = {};
/**
* Add a parser for a specific variable.
*
* @param {string} name - The name of the variable.
* @param {function} callback - The callback to be executed at parse time.
*/
_dss.parser = function( name, callback ) {
_dss.parsers[ name ] = callback;
};
// Store aliases
_dss.aliases = {};
/**
* Add an alias for a parser.
*
* @param {String} aliasParserName - The name of the new parser.
* @param {String} existingParserName - The name of the existing parser.
*/
_dss.alias = function( aliasParserName, existingParserName ) {
_dss.aliases[ aliasParserName ] = existingParserName;
_dss.parsers[ aliasParserName ] = _dss.parsers[ existingParserName ];
};
/**
* Trim whitespace from string.
*
* @param {string} str - The string to be trimmed.
* @returns {string} - The trimmed string.
*/
_dss.trim = function( str, arr ) {
/* eslint-disable no-param-reassign */
let defaults = [ /^\s\s*/, /\s\s*$/ ];
arr = ( _dss.isArray( arr ) ) ? arr.concat( defaults ) : defaults;
arr.forEach( function( regEx ) {
str = str.replace( regEx, '' );
} );
return str;
/* eslint-enable no-param-reassign */
};
/**
* Check if object is an array.
*
* @param {object} obj - The object to check.
* @returns {boolean} - The result of the test.
*/
_dss.isArray = function( obj ) {
return toString.call( obj ) === '[object Array]';
};
/**
* Check the size of an object.
*
* @param {object} obj - The object to check.
* @returns {boolean} - The result of the test.
*/
_dss.size = function( obj ) {
let size = 0;
for ( let key in obj ) {
if ( Object.prototype.hasOwnProperty.call( obj, key ) ) {size++;}
}
return size;
};
/**
* Iterate over an object.
*
* @param {object} obj - The object to iterate over.
* @param {function} iterator - Callback function to use when iterating.
* @param {object} context - Optional context to pass to iterator.
*/
_dss.each = function( obj, iterator, context ) {
if ( obj === null ) {
return;
}
if ( obj.length === Number(obj.length) ) {
for ( let i = 0, l = obj.length; i < l; i++ ) {
if ( iterator.call( context, obj[ i ], i, obj ) === {} ) {
return;
}
}
} else {
for ( let key in obj ) {
if ( _has( obj, key ) ) {
if ( iterator.call( context, obj[ key ], key, obj ) === {} ) {
return;
}
}
}
}
};
/**
* Extend an object.
*
* @param {object} obj - The object to extend.
*/
_dss.extend = function( obj ) {
_dss.each( Array.prototype.slice.call( arguments, 1 ), function( source ) {
if ( source ) {
for ( let prop in source ) {
obj[ prop ] = source[ prop ];
}
}
});
return obj;
};
/**
* Squeeze unnecessary extra characters/string.
*
* @param {string} str - The string to be squeeze.
* @param {string} def - The string to be matched.
* @returns {string} - The modified string.
*/
_dss.squeeze = function( str, def ) {
return str.replace( /\s{2,}/g, def );
};
/**
* Normalizes the comment block to ignore any consistent preceding
* whitespace. Consistent means the same amount of whitespace on every line
* of the comment block. Also strips any whitespace at the start and end of
* the whole block.
*
* @param {string} block - Text block.
* @returns {string} - A cleaned up text block.
*/
_dss.normalize = function( block ) {
// Strip out any preceding [whitespace]* that occur on every line. Not
// the smartest, but I wonder if I care.
let textBlock = block.replace( /^(\s*\*+)/, '' );
// Strip consistent indenting by measuring first line's whitespace
let indentSize = false;
// eslint-disable-next-line no-unused-vars
let unindented = ( function( lines ) {
return lines.map( function( line ) {
let precedingWhitespace = line.match( /^\s*/ )[ 0 ].length;
if ( !indentSize ) {
indentSize = precedingWhitespace;
}
if ( line === '' ) {
return '';
} else if ( indentSize <= precedingWhitespace && indentSize > 0 ) {
return line.slice( indentSize, ( line.length - 1 ) );
}
return line;
} ).join( '\n' );
} )( textBlock.split( '\n' ) );
return _dss.trim( textBlock );
};
/**
* Takes a file and extracts comments from it.
*
* @param {string} lines - Lines to parse.
* @param {object} options - Options.
* @param {function} callback - Callback.
*/
_dss.parse = function( lines, options, callback ) {
// Options
// eslint-disable-next-line no-param-reassign
options = ( options ) ? options : {};
options.preserveWhitespace = Boolean(options.preserveWhitespace);
// Setup
let currentBlock = '';
let insideSingleLineBlock = false;
let insideMultiLineBlock = false;
let _blocks = [];
let _blockValues = [];
let parsed = '';
let blocks = [];
let temp = {};
let lineNum = 0;
const overridableNames = [ 'key', 'type', 'description' ];
/**
* Parses a line.
*
* @param {object} temp - The temporary object value.
* @param {string} line - The line to parse.
* @param {string} block - Parsed block.
* @param {string} file - Parsed file.
* @param {number} lineIndex - Index of the line.
* @returns {object} - Result of parsing.
*/
let parser = function( temp, line, block, file, lineIndex ) {
/* eslint-disable no-param-reassign */
let indexer = function( str, find ) {
return ( str.indexOf( find ) > 0 ) ? str.indexOf( find ) : false;
};
let parts = line.replace( /[^@]*@/, '' );
let i = indexer( parts, ' ' ) || indexer( parts, '\n' ) || indexer( parts, '\r' ) || parts.length;
let name = _dss.trim( parts.substr( 0, i ) );
let annotationName = name;
let description = _dss.trim( parts.substr( i ) );
let variable = _dss.parsers[ name ];
let restOfBlock = block.split( '\n' ).splice(lineIndex).join( '\n' );
let index = restOfBlock.indexOf( line );
if ( _dss.aliases[name] ) {
name = _dss.aliases[name];
}
line = {};
line[ name ] = ( variable ) ? variable.apply( null, [ index, description, restOfBlock, file, annotationName ] ) : ''; // eslint-disable-line no-useless-call
if ( (overridableNames.indexOf(name) === -1) && temp[ name ] ) {
if ( !_dss.isArray( temp[ name ] ) ) {
temp[name] = [ temp[ name ] ];
}
if ( !_dss.isArray( line[ name ] ) ) {
temp[ name ].push( line[ name ] );
} else {
temp[ name ].push( line[ name ][ 0 ] );
}
} else {
temp = _dss.extend( temp, line );
}
return temp;
/* eslint-enable no-param-reassign */
};
/**
* Check for single-line comment.
*
* @param {string} line - Line to parse/check.
* @returns {boolean} - Result of check.
*/
let singleLineComment = function( line ) {
return Boolean(line.match( /^\s*\/\/\// ));
};
/**
* Checks for start of a multi-line comment.
*
* @param {string} line - Line to parse/check.
* @returns {boolean} - Result of check.
*/
let startMultiLineComment = function( line ) {
return Boolean(line.match( /^\s*\/\*/ ));
};
/**
* Check for end of a multi-line comment.
*
* @param {string} line - Line to parse/check.
* @returns {boolean} - Result of check.
*/
let endMultiLineComment = function( line ) {
if ( singleLineComment( line ) ) {
return false;
}
return Boolean(line.match( /.*\*\// ));
};
/**
* Removes comment identifiers for single-line comments.
*
* @param {string} line - Line to parse/check.
* @returns {boolean} - Result of check.
*/
let parseSingleLine = function( line ) {
return line.replace( /\s*\/\/\//, '' );
};
/**
* Remove comment identifiers for multi-line comments.
*
* @param {string} line - Line to parse/check.
* @returns {boolean} - Result of check.
*/
let parseMultiLine = function( line ) {
let cleaned = line.replace( /\s*\/\*/, '' ).trim();
cleaned = cleaned.replace( /\*\//, '' );
return cleaned.replace( /^\*/, '' );
};
/* eslint-disable no-param-reassign */
lines = String(lines);
lines.split( /\n/ ).forEach(( line, index, linesArr ) => {
lineNum = lineNum + 1;
line = String(line);
// Parse Single line comment
if ( singleLineComment( line ) ) {
parsed = parseSingleLine( line );
if ( insideSingleLineBlock ) {
currentBlock += '\n' + parsed;
} else {
currentBlock = parsed;
insideSingleLineBlock = true;
}
}
// Parse multi-line comments
if ( startMultiLineComment( line ) || insideMultiLineBlock ) {
parsed = parseMultiLine( line );
if ( insideMultiLineBlock ) {
currentBlock += '\n' + parsed;
} else {
currentBlock += parsed;
insideMultiLineBlock = true;
}
}
// End a multi-line block
if ( endMultiLineComment( line ) ) {
insideMultiLineBlock = false;
}
// Store current block if done
if ( !singleLineComment( line ) && !insideMultiLineBlock ) {
if ( currentBlock ) {
const lineToParse = endMultiLineComment(line) ? String(linesArr[index + 1]) : line;
const type = _dss.getKeyType(_dss.trim(lineToParse));
const key = _dss.getKey(_dss.trim(lineToParse), type);
const description = _dss.getDescription(_dss.trim(currentBlock));
_blocks.push( _dss.normalize( currentBlock ) );
_blockValues.push({
'type': type,
'key': key,
'description': description
});
}
insideSingleLineBlock = false;
currentBlock = '';
}
});
/* eslint-enable no-param-reassign */
// Create new blocks with custom parsing
_blocks.forEach( function( block, i ) {
/* eslint-disable no-param-reassign */
// Remove extra whitespace
let currentBlockValues = _blockValues[i];
for ( let key in currentBlockValues ) {
temp[key] = currentBlockValues[key];
}
block = block.split( '\n' ).filter( function( line ) {
return ( _dss.trim( _dss.normalize( line ) ) );
} ).join( '\n' );
// Split block into lines
block.split( '\n' ).forEach( (line, lineIndex) => {
if ( _dss.detect( line ) ) {
temp = parser( temp, _dss.normalize( line ), block, lines, lineIndex);
}
});
// Push to blocks if object isn't empty
if ( _dss.size( temp ) ) {
blocks.push( temp );
}
temp = {};
/* eslint-enable no-param-reassign */
});
// Execute callback with filename and blocks
callback( { blocks: blocks } );
};
/**
* Used by parsers to get the content of multi-line annotations.
*
* @param {number} i - Index of the parser.
* @param {string} line - Line to parse.
* @param {string} block - Parsed block.
* @param {string} file - Parsed file.
* @param {string} parserName - Name of the parser.
* @param {boolean} [includeParserLine=true] - A flag that indicates if first line should be included in the returned string.
* @returns {string} - Result of parsing.
*/
_dss.getMultiLineContent = (i, line, block, file, parserName, includeParserLine = true) => {
/* eslint-disable no-param-reassign */
// find the next instance of a parser (if there is one based on the @ symbol)
// in order to isolate the current multi-line parser
let nextParserIndex = block.indexOf('\n @', i + 1);
let markupLength = (nextParserIndex > -1) ? nextParserIndex - i : block.length;
let markup = _dss.trim(block.split('').splice(i, markupLength).join(''));
let parserLine = line;
let parserMarker = '@' + parserName;
markup = ((markup) => {
let ret = [];
let lines = markup.split('\n');
lines.forEach((line) => {
let pattern = '*';
let index = line.indexOf(pattern);
if (index > 0 && index < 10) {
line = line.split('').splice((index + pattern.length), line.length).join('');
}
// multiline
if (lines.length <= 2) {
line = _dss.trim(line);
}
if (line && line.indexOf(parserMarker) === -1) {
ret.push(line);
}
if (line.indexOf(parserMarker) !== -1) {
line = _dss.trim(line.replace(parserMarker, ''));
if (!includeParserLine) {
line = _dss.trim(line.replace(parserLine, ''));
}
if (line.length > 0) {
ret.push(line);
}
}
});
return ret.join('\n');
})( markup );
/* eslint-enable no-param-reassign */
return markup;
};
/**
* Get the tags information.
*
* @param {string} line - Line to parse.
* @returns {arr} - The array with the values.
*/
_dss.extractJSDocTags = (line) => {
let currentLine = line;
const state = {};
const typeRegEx = /\{[^]+\}/;
const nameRegEx = /[^-]*/;
const descriptionRegEx = /[^-]*-\s/;
const match = currentLine.match(typeRegEx);
if (match) {
state.type = match[0];
currentLine = currentLine.replace(typeRegEx, '');
} else {
state.type = null;
}
currentLine = _dss.trim(currentLine);
if (currentLine.indexOf('-') === -1) {
state.name = currentLine;
state.description = null;
} else {
state.name = _dss.trim(currentLine.match(nameRegEx)[0]);
if (state.name.length === 0) {
state.name = null;
}
currentLine = currentLine.replace(nameRegEx, '');
currentLine = currentLine.replace(descriptionRegEx, '');
currentLine = _dss.trim(currentLine);
state.description = currentLine;
}
return state;
};
/**
* Get the Key of the block.
*
* @param {string} line - Line to parse.
* @param {string} type - Type of the key.
* @returns - The key if defined.
*/
_dss.getKey = (line, type) => {
let key = null;
let match = null;
const matchers = {
'variable': /\$[^:]+/,
'selector': /\.[^\s,{]+/,
'function': /(?<=@function[\s]+)([^(\s]+)/
};
if (type === null) {
return key;
}
match = line.match(matchers[type]);
if ( match ) {
key = match[0];
}
return key;
};
/**
* Get the KeyType of the block.
*
* @param {string} line - Line to parse.
* @returns - The KeyType if defined.
*/
_dss.getKeyType = (line) => {
if ( line.startsWith('$') ) {
return 'variable';
}
if ( line.startsWith('.') ) {
return 'selector';
}
if ( line.startsWith('@function') ) {
return 'function';
}
return null;
};
/**
* Get the default description of the block that is not passed via an annotation.
* If description annotation is also passed, it will override the default one.
*
* @param {string} block - Block to parse.
* @returns - The default description if defined.
*/
_dss.getDescription = (block) => {
let description = null;
if ( !block.startsWith('@') ) {
let nextParserIndex = block.indexOf('\n @');
let markupLength = (nextParserIndex > -1) ? nextParserIndex : block.length;
description = block.substring(0, markupLength);
}
return description;
};
// Return function
return _dss;
})();
// Describe detection pattern
dss.detector((line) => {
if (typeof line !== 'string') {
return false;
}
let reference = line.split( '\n\n' ).pop();
return Boolean(reference.match(/.*@/));
});
// Describe parsing a name
dss.parser('name', (i, line, block, file) => { // eslint-disable-line no-unused-vars
return line;
});
// Describe parsing a description
dss.parser('description', (i, line, block, file, parserName) => {
const description = dss.getMultiLineContent(i, line, block, file, parserName);
return description;
});
// Describe parsing a state
dss.parser('state', (i, line, block, file) => { // eslint-disable-line no-unused-vars
let state = line.split(' - ');
return [ {
name: (state[0]) ? dss.trim(state[0]) : '',
escaped: (state[0]) ? dss.trim(state[0].replace('.', ' ').replace(':', ' pseudo-class-')) : '',
description: (state[1]) ? dss.trim(state[1]) : ''
} ];
});
// Describe parsing example
dss.parser('example', (i, line, block, file, parserName) => {
const example = dss.getMultiLineContent(i, line, block, file, parserName, false);
return {
type: (line.length) ? line.toLowerCase() : null,
example: example,
escaped: example.replace(/</g, '<').replace(/>/g, '>')
};
});
// Describe parsing a deprecated version
dss.parser('deprecated', (i, line, block, file) => { // eslint-disable-line no-unused-vars
return line;
});
// Describe parsing a deprecated description
dss.parser('deprecatedDescription', (i, line, block, file) => { // eslint-disable-line no-unused-vars
return line;
});
// Describe parsing a group
dss.parser('group', (i, line, block, file) => { // eslint-disable-line no-unused-vars
return line.toLowerCase();
});
// Describe parsing a type
dss.parser('type', (i, line, block, file) => { // eslint-disable-line no-unused-vars
return line.toLowerCase();
});
// Describe parsing a subtype
dss.parser('subtype', (i, line, block, file) => { // eslint-disable-line no-unused-vars
return line.toLowerCase();
});
// Describe parsing a key
dss.parser('key', (i, line, block, file) => { // eslint-disable-line no-unused-vars
return line;
});
// Describe parsing a param
dss.parser('param', (i, line, block, file) => { // eslint-disable-line no-unused-vars
const state = dss.extractJSDocTags(line);
return [ {
type: state.type,
name: state.name,
description: state.description
} ];
});
// Describe parsing a return
dss.parser('returns', (i, line, block, file) => { // eslint-disable-line no-unused-vars
const state = dss.extractJSDocTags(line);
return {
type: state.type,
name: state.name,
description: state.description
};
});
// Aliases
dss.alias('return', 'returns');
dss.alias('markup', 'example');
module.exports = dss;